feat: simplify Discover view into socket cards & backend upgrades

This commit is contained in:
Hitonabi
2026-06-26 07:53:41 +02:00
parent b90cef3968
commit bc3abb127a
21 changed files with 2019 additions and 715 deletions
+4
View File
@@ -115,6 +115,10 @@ def capabilities(name: str = "", cmd: str = "", gguf_path: str = "", hf: dict |
m = re.search(r"-(?:c|-ctx-size)\s+(\d+)", cmdl)
ctx = int(m.group(1)) if m else None
# Cap context length at 131072 for Qwen / Hermes models to prevent reporting scaled RoPE context of 256k+ which might OOM or be unstable.
if ctx and ctx > 131072 and ("qwen" in low or "hermes" in low):
ctx = 131072
tool_confirmed = "--jinja" in cmdl or "tool_call" in chat_tpl or "<tools>" in chat_tpl
tool_family = any(fam in low for fam in _TOOL_FAMILIES) or "function-calling" in tags
tools = "yes" if tool_confirmed else ("likely" if tool_family else "no")
+10 -2
View File
@@ -36,7 +36,7 @@ def build_snippets(host: str = DEFAULT_HOST,
}, indent=2)
opencode = json.dumps({
"providers": {
"provider": {
"bosgame": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bosgame Gateway",
@@ -46,6 +46,12 @@ def build_snippets(host: str = DEFAULT_HOST,
}
}, indent=2)
cursor = json.dumps({
"Base URL": gw,
"API Key": "local",
"Active Model": "auto"
}, indent=2)
zed = json.dumps({
"language_models": {
"openai_compatible": {
@@ -97,8 +103,10 @@ def build_snippets(host: str = DEFAULT_HOST,
"tools": {
"cline": {"label": "Roo Code / Cline (VS Code)", "lang": "json", "snippet": cline,
"note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."},
"cursor": {"label": "Cursor IDE", "lang": "json", "snippet": cursor,
"note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."},
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
"note": "Datei opencode.jsonc, Key 'providers'."},
"note": "Datei opencode.jsonc, Key 'provider'."},
"zed": {"label": "Zed", "lang": "json", "snippet": zed,
"note": "settings.json → language_models.openai_compatible."},
"continue": {"label": "Continue", "lang": "json", "snippet": cont,
+27 -5
View File
@@ -57,19 +57,39 @@ def _pump_output(job: dict, stream) -> None:
commit()
def _run_job(job_id: str, args: list[str], env: dict | None = None):
def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_password: str | None = None):
job = JOBS[job_id]
job["state"] = "running"
try:
actual_args = list(args)
if sudo_password is not None:
for i, arg in enumerate(actual_args):
if isinstance(arg, str):
actual_args[i] = arg.replace("sudo -n", "sudo -S").replace("sudo ", "sudo -S ")
proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0,
actual_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
stdin=subprocess.PIPE if sudo_password is not None else None,
bufsize=0,
env={**os.environ, **(env or {})},
)
_PROCS[job_id] = proc
if sudo_password is not None and proc.stdin:
proc.stdin.write((sudo_password + "\n").encode("utf-8"))
proc.stdin.flush()
proc.stdin.close()
_pump_output(job, proc.stdout)
proc.wait()
job["returncode"] = proc.returncode
job["state"] = "canceled" if job.get("canceled") else ("done" if proc.returncode == 0 else "failed")
# Check if failed due to sudo authorization failure
if proc.returncode != 0 and job["log"]:
log_str = "\n".join(job["log"])
if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str:
job["sudo_failed"] = True
except Exception as exc: # noqa: BLE001
_append_log(job, f"[mc] Fehler: {exc}")
job["state"] = "failed"
@@ -127,16 +147,18 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
threading.Thread(target=_watch, daemon=True).start()
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None) -> str:
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None, sudo_password: str | None = None) -> str:
job_id = uuid.uuid4().hex[:12]
# Mask password in log if present in args
log_args = list(args)
JOBS[job_id] = {
"id": job_id, "label": label, "state": "queued",
"log": ["$ " + " ".join(shlex.quote(a) for a in args)],
"log": ["$ " + " ".join(shlex.quote(a) for a in log_args)],
"returncode": None, "started_at": time.time(), "finished_at": None,
}
if on_done:
JOBS[job_id]["_on_done"] = on_done
threading.Thread(target=_run_job, args=(job_id, args, env), daemon=True).start()
threading.Thread(target=_run_job, args=(job_id, args, env, sudo_password), daemon=True).start()
return job_id
+62 -15
View File
@@ -83,45 +83,92 @@ def updates() -> dict:
"models": len(ups), "model_list": ups}
def _run(cmd: list[str]) -> dict:
def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
actual_cmd = list(cmd)
has_sudo = False
if cmd and cmd[0] == "sudo":
has_sudo = True
# If we have a password, use -S instead of -n
if sudo_password is not None:
if "-n" in actual_cmd:
actual_cmd = [x for x in actual_cmd if x != "-n"]
if "-S" not in actual_cmd:
actual_cmd.insert(1, "-S")
else:
# Force -n to fail cleanly if password is required
if "-S" in actual_cmd:
actual_cmd = [x for x in actual_cmd if x != "-S"]
if "-n" not in actual_cmd:
actual_cmd.insert(1, "-n")
try:
p = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
input_data = (sudo_password + "\n") if (has_sudo and sudo_password is not None) else None
p = subprocess.run(actual_cmd, input=input_data, capture_output=True, text=True, timeout=120)
err_msg = p.stderr or ""
if p.returncode != 0 and ("a password is required" in err_msg or "password" in err_msg.lower() or "sudo:" in err_msg):
if sudo_password is not None:
return {"ok": False, "status": "incorrect_password", "out": p.stdout or "", "err": "Falsches Sudo-Passwort."}
return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."}
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
except Exception as exc: # noqa: BLE001
return {"ok": False, "out": "", "err": str(exc)}
def restart_service(name: str) -> dict:
def check_sudo_needs_password(sudo_password: str | None = None) -> dict | None:
"""Checks if sudo needs a password. Returns error dict if password required/incorrect, else None."""
res = _run(["sudo", "true"], sudo_password=sudo_password)
if not res["ok"]:
return res
return None
def restart_service(name: str, sudo_password: str | None = None) -> dict:
if name in SYSTEM_SERVICES:
return _run(["sudo", "-n", "systemctl", "restart", name])
if err := check_sudo_needs_password(sudo_password):
return err
return _run(["sudo", "systemctl", "restart", name], sudo_password=sudo_password)
if name in USER_SERVICES:
return _run(["systemctl", "--user", "restart", name])
return {"ok": False, "err": f"Dienst '{name}' nicht erlaubt."}
def logs(service: str, lines: int = 200) -> dict:
def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> dict:
lines = max(1, min(lines, 1000))
if service in SYSTEM_SERVICES:
cmd = ["sudo", "-n", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
if err := check_sudo_needs_password(sudo_password):
return err
cmd = ["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
r = _run(cmd, sudo_password=sudo_password)
elif service in USER_SERVICES:
cmd = ["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"]
r = _run(cmd)
else:
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
r = _run(cmd)
return {"ok": r["ok"], "text": r["out"] or r["err"]}
def os_update_job() -> str:
cmd = "sudo -n apt-get update && sudo -n DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
return jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)")
def os_update_job(sudo_password: str | None = None) -> dict:
if err := check_sudo_needs_password(sudo_password):
return err
cmd = "sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y"
job_id = jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)", sudo_password=sudo_password)
return {"ok": True, "job_id": job_id}
def engine_update_job() -> str | None:
def engine_update_job(sudo_password: str | None = None) -> dict | None:
if not ENGINE_UPDATE_CMD:
return None
cmd = f"{ENGINE_UPDATE_CMD} && sudo -n systemctl restart llama-swap"
return jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)")
if err := check_sudo_needs_password(sudo_password):
return err
cmd = f"{ENGINE_UPDATE_CMD} && sudo systemctl restart llama-swap"
job_id = jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp)", sudo_password=sudo_password)
return {"ok": True, "job_id": job_id}
def reboot() -> dict:
return _run(["sudo", "-n", "reboot"])
def reboot(sudo_password: str | None = None) -> dict:
if err := check_sudo_needs_password(sudo_password):
return err
return _run(["sudo", "reboot"], sudo_password=sudo_password)
+107
View File
@@ -8,6 +8,7 @@ der Box, daher sysfs). Verschachtelte Struktur wie v1 (cpu.percent, ram.used Byt
import glob
import os
import subprocess
import psutil
@@ -65,6 +66,111 @@ def _temps() -> dict | None:
return out or None
def get_git_info(path: str) -> dict | None:
expanded = os.path.expanduser(path)
if not os.path.isdir(expanded) or not os.path.exists(os.path.join(expanded, ".git")):
return None
try:
res = subprocess.run(
["git", "log", "-1", "--format=%h|%cd|%s", "--date=short"],
cwd=expanded, capture_output=True, text=True, timeout=3
)
if res.returncode != 0:
return None
parts = res.stdout.strip().split("|", 2)
h = parts[0]
d = parts[1]
s = parts[2] if len(parts) > 2 else ""
branch_res = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=expanded, capture_output=True, text=True, timeout=2
)
branch = branch_res.stdout.strip() if branch_res.returncode == 0 else "unknown"
status_res = subprocess.run(
["git", "status", "--porcelain"],
cwd=expanded, capture_output=True, text=True, timeout=2
)
dirty = bool(status_res.stdout.strip()) if status_res.returncode == 0 else False
return {
"hash": h,
"date": d,
"subject": s,
"branch": branch,
"dirty": dirty,
"path": expanded
}
except Exception:
return None
def find_hermes_agent_git() -> dict | None:
env_path = os.environ.get("MC_HERMES_AGENT_PATH")
if env_path:
info = get_git_info(env_path)
if info:
return info
candidates = [
"~/hermes-agent",
"~/.hermes/hermes-agent",
"~/hermes-webui/hermes-agent",
"~/.hermes"
]
for c in candidates:
info = get_git_info(c)
if info:
return info
return None
def get_engine_version() -> dict:
engine_path = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp")
git_info = get_git_info(engine_path)
if git_info:
return {**git_info, "type": "git"}
try:
binary = os.path.join(engine_path, "llama-server")
if not os.path.exists(binary):
binary = os.path.join(engine_path, "bin", "llama-server")
if not os.path.exists(binary):
binary = "llama-server"
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
if res.returncode == 0:
lines = res.stdout.strip().splitlines()
ver = lines[0] if lines else "unknown"
return {"version_text": ver, "type": "binary"}
except Exception:
pass
return {"type": "unknown"}
_VERSION_CACHE = {"ts": 0.0, "data": {}}
def check_versions_cached() -> dict:
import time
now = time.time()
if now - _VERSION_CACHE["ts"] < 30.0:
return _VERSION_CACHE["data"]
mc2_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
data = {
"mc2": get_git_info(mc2_path),
"engine": get_engine_version(),
"hermes_ui": get_git_info("~/hermes-webui"),
"hermes_agent": find_hermes_agent_git()
}
_VERSION_CACHE["ts"] = now
_VERSION_CACHE["data"] = data
return data
def system_status() -> dict:
vm = psutil.virtual_memory()
try:
@@ -78,4 +184,5 @@ def system_status() -> dict:
"gpu": _gpu_sysfs(),
"temp": _temps(),
"disk": disk,
"versions": check_versions_cached(),
}