fix(v8.1): Kritische Bugs behoben — system_status, model-routing, UI

P0:
- hermes_agent: system_status Key-Mismatch behoben (cpu_pct→cpu.percent,
  ram_used_gb→ram.used/1024³, gtt analog) — zeigt jetzt echte Werte statt '?'
- guides-data: & in 4 Titel-Strings → & (Svelte text-node rendert keine HTML-Entities)

P1:
- hermes_agent: choose_model() um recherchier/such/vergleich/etc. erweitert;
  kein Downgrade wenn komplexes Modell bereits geladen
- routers/hermes: asyncio.get_event_loop() → get_running_loop() (Whisper+TTS)
- maintenance: llama-swap Update akzeptiert optionales sudo-PW via stdin;
  import os hinzugefügt (fix NameError in updates()); UpdateReq-Model
- ServerPanel: Engine-Update fragt jetzt nach sudo-PW (analog OS-Update)
- HermesPanel: Chat-History in sessionStorage (überlebt Reload);
  Stopp-Button während Hermes antwortet; Verlauf-löschen-Button
- ConnectPanel: opencode.json → opencode.jsonc; provider → providers;
  LAN-IP Override (localStorage) für NPM-Proxy-Setups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-23 18:19:01 +02:00
parent 8539999627
commit e8971fbf0c
8 changed files with 204 additions and 89 deletions
+27 -8
View File
@@ -6,17 +6,29 @@
const s = $derived(statusStore.value) const s = $derived(statusStore.value)
const allModels = $derived(((s?.models || []) as any[]).filter((m: any) => !m.incomplete)) const allModels = $derived(((s?.models || []) as any[]).filter((m: any) => !m.incomplete))
const url = $derived(baseUrl())
let selectedTool = $state('zed') let selectedTool = $state('zed')
let selectedModelIds = $state<string[]>([]) let selectedModelIds = $state<string[]>([])
let testResult = $state('') let testResult = $state('')
let testOk = $state(false) let testOk = $state(false)
let selectedMemTool = $state('cline') let selectedMemTool = $state('cline')
const memBlock = $derived(memMcpBlock(selectedMemTool)) let ipOverride = $state<string>(localStorage.getItem('mc_ip_override') || '')
function baseUrl() { return `http://${location.hostname}:8080/v1` }
function isLanIp(h: string) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) } function isLanIp(h: string) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) }
function behindProxy() { return location.protocol === 'https:' || !isLanIp(location.hostname) } function behindProxy() { return location.protocol === 'https:' || !isLanIp(location.hostname) }
function baseUrl() {
const host = ipOverride.trim() || location.hostname
return `http://${host}:8080/v1`
}
const url = $derived(baseUrl())
function saveIpOverride() {
const v = ipOverride.trim()
if (v) localStorage.setItem('mc_ip_override', v)
else localStorage.removeItem('mc_ip_override')
}
const memBlock = $derived(memMcpBlock(selectedMemTool))
const activeIds = $derived( const activeIds = $derived(
selectedModelIds.length ? selectedModelIds : allModels.map((m: any) => m.name) selectedModelIds.length ? selectedModelIds : allModels.map((m: any) => m.name)
@@ -42,7 +54,7 @@
const ctx = m.meta?.ctx || 131072 const ctx = m.meta?.ctx || 131072
return ` "${m.name}": { "name": "${m.name}", "contextWindowSize": ${ctx} }` return ` "${m.name}": { "name": "${m.name}", "contextWindowSize": ${ctx} }`
}).join(',\n') }).join(',\n')
return `"provider": {\n "llama-swap": {\n "npm": "@ai-sdk/openai-compatible",\n "name": "Bosgame (llama-swap)",\n "options": {\n "baseURL": "${url}",\n "apiKey": "local"\n },\n "models": {\n${modelMap}\n }\n }\n}` return `"providers": {\n "llama-swap": {\n "npm": "@ai-sdk/openai-compatible",\n "name": "Bosgame (llama-swap)",\n "options": {\n "baseURL": "${url}",\n "apiKey": "local"\n },\n "models": {\n${modelMap}\n }\n }\n}`
} }
function continueBlock(url: string, ids: string[]) { function continueBlock(url: string, ids: string[]) {
@@ -97,10 +109,16 @@
<div class="card-h"><h3>1 · Verbindung prüfen</h3></div> <div class="card-h"><h3>1 · Verbindung prüfen</h3></div>
<div class="card-sub">Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.</div> <div class="card-sub">Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.</div>
<label>Engine-Adresse (für alle Tools)</label> <label>Engine-Adresse (für alle Tools)</label>
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px"> <div class="flex gap-2" style="align-items:stretch;margin-bottom:4px">
<input class="mono-sm" readonly value={url} style="flex:1;margin:0"> <input class="mono-sm" readonly value={url} style="flex:1;margin:0">
<button class="ghost" onclick={e => { const inp = (e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement; copyVal(inp) }}>Kopieren</button> <button class="ghost" onclick={e => { const inp = (e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement; copyVal(inp) }}>Kopieren</button>
</div> </div>
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
<input class="mono-sm" placeholder="LAN-IP überschreiben, z.B. 192.168.178.151 (leer = auto)"
bind:value={ipOverride}
oninput={saveIpOverride}
style="flex:1;margin:0;font-size:12px;opacity:.7">
</div>
{#if behindProxy()} {#if behindProxy()}
<div class="alert warn" style="margin:0 0 12px"> <div class="alert warn" style="margin:0 0 12px">
<span class="a-dot"></span> <span class="a-dot"></span>
@@ -254,14 +272,15 @@
<div class="hint">Danach im Agent-Panel das Modell <b>bosgame / {firstModel}</b> auswählen.</div> <div class="hint">Danach im Agent-Panel das Modell <b>bosgame / {firstModel}</b> auswählen.</div>
{:else if selectedTool === 'opencode'} {:else if selectedTool === 'opencode'}
{@const block = openCodeBlock(url, activeIds)} {@const block = openCodeBlock(url, activeIds)}
<p>Datei anlegen oder öffnen:</p> <p>Datei anlegen oder öffnen (OpenCode Desktop nutzt <b>.jsonc</b> — JSON mit Kommentaren):</p>
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.json</code></p> <p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.jsonc</code></p>
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Nur den <code>"provider"</code>-Block einfügen. Falls bereits ein <code>"llama-swap"</code>-Eintrag vorhanden: <b>vorher löschen</b>, sonst erscheinen Modelle doppelt.</span></div> <div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Den <code>"providers"</code>-Block in die bestehende <code>opencode.jsonc</code> einfügen (nicht die ganze Datei ersetzen). Falls bereits ein <code>"llama-swap"</code>-Eintrag existiert: <b>vorher löschen</b>.</span></div>
<div style="position:relative"> <div style="position:relative">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button> <button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
<div class="log" style="max-height:none"><code>{block}</code></div> <div class="log" style="max-height:none"><code>{block}</code></div>
</div> </div>
<p style="margin-top:12px">Danach in OpenCode <code>/models</code><b>llama-swap/{firstModel}</b> wählen.</p> <p style="margin-top:12px">Danach in OpenCode <code>/models</code><b>llama-swap/{firstModel}</b> wählen.</p>
<div class="hint">OpenCode Desktop: <a href="https://opencode.ai" target="_blank" rel="noopener">opencode.ai</a></div>
{:else if selectedTool === 'cline'} {:else if selectedTool === 'cline'}
<p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen:</p> <p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen:</p>
<label>Basis-URL</label> <label>Basis-URL</label>
+48 -1
View File
@@ -6,10 +6,15 @@
type MsgRole = 'user' | 'hermes' | 'info' | 'tool' | 'error' type MsgRole = 'user' | 'hermes' | 'info' | 'tool' | 'error'
type Msg = { role: MsgRole; content: string; tool?: string; args?: any } type Msg = { role: MsgRole; content: string; tool?: string; args?: any }
const SESSION_KEY = 'hermes_chat_history'
// ---- State ---- // ---- State ----
let messages = $state<Msg[]>([]) let messages = $state<Msg[]>((() => {
try { return JSON.parse(sessionStorage.getItem(SESSION_KEY) || '[]') } catch { return [] }
})())
let input = $state('') let input = $state('')
let thinking = $state(false) let thinking = $state(false)
let stopping = $state(false)
let voiceOn = $state(false) let voiceOn = $state(false)
let recording = $state(false) let recording = $state(false)
let status = $state<any>(null) let status = $state<any>(null)
@@ -18,6 +23,11 @@
let ws = $state<WebSocket | null>(null) let ws = $state<WebSocket | null>(null)
let currentBot = $state('') // aktuell gestreamter Hermes-Token let currentBot = $state('') // aktuell gestreamter Hermes-Token
// Nachrichten in sessionStorage sichern (lebt nur für aktive Browser-Tab-Session)
$effect(() => {
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(messages)) } catch {}
})
// ---- WebSocket ---- // ---- WebSocket ----
function connect() { function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
@@ -56,6 +66,7 @@
messages = [...messages, { role: 'error', content: m.content }] messages = [...messages, { role: 'error', content: m.content }]
} else if (m.type === 'done') { } else if (m.type === 'done') {
thinking = false thinking = false
stopping = false
currentBot = '' currentBot = ''
if (voiceOn) speakLast() if (voiceOn) speakLast()
} }
@@ -69,6 +80,18 @@
ws = sock ws = sock
} }
// ---- Stopp ----
function stopGeneration() {
stopping = true
thinking = false
if (currentBot) {
messages = [...messages.slice(0, -1),
{ role: 'hermes', content: currentBot + ' ✋' }]
}
currentBot = ''
ws?.close() // onclose reconnects nach 3s automatisch
}
// ---- Senden ---- // ---- Senden ----
async function send() { async function send() {
const text = input.trim() const text = input.trim()
@@ -76,9 +99,17 @@
messages = [...messages, { role: 'user', content: text }] messages = [...messages, { role: 'user', content: text }]
input = '' input = ''
currentBot = '' currentBot = ''
stopping = false
ws.send(JSON.stringify({ message: text })) ws.send(JSON.stringify({ message: text }))
} }
// ---- Verlauf löschen ----
function clearHistory() {
messages = []
currentBot = ''
try { sessionStorage.removeItem(SESSION_KEY) } catch {}
}
// ---- Voice: Aufnahme ---- // ---- Voice: Aufnahme ----
let mediaRecorder: MediaRecorder | null = null let mediaRecorder: MediaRecorder | null = null
let audioChunks: Blob[] = [] let audioChunks: Blob[] = []
@@ -433,6 +464,13 @@
{recording ? '⏹' : '🎤'} {recording ? '⏹' : '🎤'}
</button> </button>
{#if thinking || stopping}
<!-- Stopp-Button während Hermes antwortet -->
<button class="ghost" style="padding:10px 18px;flex-shrink:0;border-color:var(--err);color:var(--err)"
onclick={stopGeneration}>
⏹ Stopp
</button>
{:else}
<!-- Senden-Button --> <!-- Senden-Button -->
<button <button
class="primary" class="primary"
@@ -441,7 +479,16 @@
onclick={send}> onclick={send}>
Senden Senden
</button> </button>
{/if}
</div> </div>
<!-- Fußzeile: Verlauf löschen -->
{#if messages.length > 0 && !thinking}
<div style="border-top:1px solid rgba(255,255,255,.04);padding:6px 16px;display:flex;justify-content:flex-end">
<button class="ghost" style="font-size:11px;padding:2px 8px;color:var(--mut)"
onclick={clearHistory}>Verlauf löschen</button>
</div>
{/if}
</div> </div>
<style> <style>
+3 -1
View File
@@ -74,7 +74,9 @@
async function engineUpdate() { async function engineUpdate() {
if (!await confirmModal({ title: 'LLM-Engine aktualisieren?', body: 'Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar.' })) return if (!await confirmModal({ title: 'LLM-Engine aktualisieren?', body: 'Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar.' })) return
try { const r = await api('/api/update', { method: 'POST' }); toast('LLM-Engine-Update läuft.'); watchJob(r.job_id, 'LLM-Engine-Update') } const pwd = await promptModal({ title: 'sudo-Passwort', body: 'Für das Engine-Update wird einmalig dein sudo-Passwort gebraucht (für den Update-Befehl).', placeholder: 'sudo-Passwort', password: true, confirmLabel: 'Update starten' })
if (pwd === null) return
try { const r = await api('/api/update', { method: 'POST', body: JSON.stringify({ password: pwd }) }); toast('LLM-Engine-Update läuft.'); watchJob(r.job_id, 'LLM-Engine-Update') }
catch (e: any) { toast(e.message, true) } catch (e: any) { toast(e.message, true) }
} }
+4 -4
View File
@@ -370,7 +370,7 @@ export const GUIDE_GROUPS: GuideGroup[] = [
<p><b>Merksatz:</b> Fehlt dem Agenten eine <em>Fähigkeit</em> MCP. <p><b>Merksatz:</b> Fehlt dem Agenten eine <em>Fähigkeit</em> MCP.
Soll er etwas nur <em>besser machen</em> Skill (nächster Abschnitt).</p>`], Soll er etwas nur <em>besser machen</em> Skill (nächster Abschnitt).</p>`],
["Skills &amp; Rules — Agenten Gewohnheiten beibringen", ` ["Skills & Rules — Agenten Gewohnheiten beibringen", `
<p><b>Skills</b> (auch <em>Rules</em>, <em>AGENTS.md</em> oder <em>Instructions</em> genannt) <p><b>Skills</b> (auch <em>Rules</em>, <em>AGENTS.md</em> oder <em>Instructions</em> genannt)
sind kurze Texte, die dem Agenten beibringen wie er in deinem Projekt arbeiten soll sind kurze Texte, die dem Agenten beibringen wie er in deinem Projekt arbeiten soll
immer, ohne dass du es jedes Mal neu sagen musst.</p> immer, ohne dass du es jedes Mal neu sagen musst.</p>
@@ -418,7 +418,7 @@ Bevor du Dateien änderst: erkläre kurz was du vorhast.</pre>
kritische Probleme (Prompt Injection, exponierte Secrets). Regel: Skill-Datei kurz lesen bevor du sie einsetzt.</p> kritische Probleme (Prompt Injection, exponierte Secrets). Regel: Skill-Datei kurz lesen bevor du sie einsetzt.</p>
<p style="margin-top:8px">Oder einfach fragen: <em>Schreib mir eine AGENTS.md für ein React + TypeScript Projekt mit Tailwind."</em></p>`], <p style="margin-top:8px">Oder einfach fragen: <em>Schreib mir eine AGENTS.md für ein React + TypeScript Projekt mit Tailwind."</em></p>`],
["Die Persona — Rolle, Beziehung &amp; Onboarding", ` ["Die Persona — Rolle, Beziehung & Onboarding", `
<p>Eine Persona hat <b>zwei Dimensionen</b> beide sind wichtig:</p> <p>Eine Persona hat <b>zwei Dimensionen</b> beide sind wichtig:</p>
<div class="tut-table"> <div class="tut-table">
<div class="tut-row"> <div class="tut-row">
@@ -503,7 +503,7 @@ Wenn du alle Antworten hast, erstelle daraus:
</div> </div>
<p class="tut-sources" style="margin-top:10px">Fertige Personas: <a href="https://skills.sh" target="_blank" rel="noopener">skills.sh</a> · <a href="https://cursor.directory" target="_blank" rel="noopener">cursor.directory</a></p>`], <p class="tut-sources" style="margin-top:10px">Fertige Personas: <a href="https://skills.sh" target="_blank" rel="noopener">skills.sh</a> · <a href="https://cursor.directory" target="_blank" rel="noopener">cursor.directory</a></p>`],
["AGENTS.MD &amp; SAVEPOINT.MD — Gedächtnis für den Agenten", ` ["AGENTS.MD & SAVEPOINT.MD — Gedächtnis für den Agenten", `
<p>Jedes Mal wenn du einen neuen Chat startest, weiß der Agent <b>nichts</b> über dein Projekt. <p>Jedes Mal wenn du einen neuen Chat startest, weiß der Agent <b>nichts</b> über dein Projekt.
Diese zwei Dateien lösen das Problem.</p> Diese zwei Dateien lösen das Problem.</p>
<p><b>AGENTS.md ist 2026 der plattformübergreifende Standard</b> nativ gelesen von: <p><b>AGENTS.md ist 2026 der plattformübergreifende Standard</b> nativ gelesen von:
@@ -593,7 +593,7 @@ src/
{ {
label: 'Wenn etwas nicht klappt', label: 'Wenn etwas nicht klappt',
tutorials: [ tutorials: [
["Häufige Probleme &amp; Lösungen", ` ["Häufige Probleme & Lösungen", `
<div class="tut-table"> <div class="tut-table">
<div class="tut-row"> <div class="tut-row">
<div class="tut-cell-hd">🔴 Engine offline, rotes Licht in der Topbar</div> <div class="tut-cell-hd">🔴 Engine offline, rotes Licht in der Topbar</div>
+46 -10
View File
@@ -197,13 +197,22 @@ def _exec_get_system_status() -> str:
model_str = "unbekannt" model_str = "unbekannt"
try: try:
sys = httpx.get(f"{base}/api/system/status", timeout=5).json() d = httpx.get(f"{base}/api/system/status", timeout=5).json()
cpu = d.get("cpu", {})
ram = d.get("ram", {})
gtt = d.get("gpu", {}).get("gtt", {})
cpu_pct = round(cpu.get("percent", 0), 1)
cpu_temp = cpu.get("temp", "?")
ram_used = round(ram.get("used", 0) / (1024 ** 3), 1)
ram_total = round(ram.get("total", 0) / (1024 ** 3), 1)
gtt_used = round(gtt.get("used", 0) / (1024 ** 3), 1)
gtt_total = round(gtt.get("total", 0) / (1024 ** 3), 1)
return ( return (
f"Aktives Modell: {model_str}\n" f"Aktives Modell: {model_str}\n"
f"CPU: {sys.get('cpu_pct', '?')}% | " f"CPU: {cpu_pct}% | "
f"RAM: {sys.get('ram_used_gb', '?')} / {sys.get('ram_total_gb', '?')} GB | " f"RAM: {ram_used} / {ram_total} GB | "
f"GPU: {sys.get('gpu_mem_used_gb', '?')} / {sys.get('gpu_mem_total_gb', '?')} GB GTT\n" f"GPU: {gtt_used} / {gtt_total} GB GTT\n"
f"Temp: CPU {sys.get('cpu_temp_c', '?')}°C" f"Temp: CPU {cpu_temp}°C"
) )
except Exception as e: except Exception as e:
return f"Aktives Modell: {model_str}\nSystem-Metriken nicht verfuegbar: {e}" return f"Aktives Modell: {model_str}\nSystem-Metriken nicht verfuegbar: {e}"
@@ -275,17 +284,44 @@ def execute_tool(name: str, args: dict) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_COMPLEX_KW = { _COMPLEX_KW = {
"schreib", "erstell", "baue", "plan", "refactor", "debug", "analysier", # Erstellen / Schreiben
"implementier", "entwickl", "code", "programm", "erklaer ausfuehrlich", "schreib", "schreibe", "erstell", "erstelle", "generier", "generiere",
"erstelle", "generier", "baue", "bau",
# Planen / Analysieren / Recherchieren
"plan", "analysier", "analysiere", "recherchier", "recherchiere",
"vergleich", "vergleiche", "bewert", "bewerte", "untersuche", "untersuchen",
# Entwicklung / Code
"refactor", "debug", "implementier", "implementiere", "entwickl", "entwickle",
"code", "programm", "programmier", "programmiere",
# Web-Suche / Recherche
"such im internet", "suche im internet", "such online", "suche online",
"such nach aktuell", "suche nach aktuell",
# Erklären (ausführlich)
"erklaer ausfuehrlich", "erklaere ausfuehrlich", "erklaer detailliert",
"erklaere detailliert", "erklaer mir genau", "was ist der unterschied",
# Optimieren / Verbessern
"optimier", "optimiere", "verbessere", "verbessern", "reparier", "repariere",
# Multi-Step / Komplex
"konfiguriere", "konfigurieren", "installier", "installiere",
"wie kann ich", "schritt fuer schritt", "schritt für schritt",
} }
def choose_model(message: str) -> str: def choose_model(message: str) -> str:
"""Waehlt simple/complex Modell — upgraded wenn noetig, degradiert nie."""
lower = message.lower() lower = message.lower()
if any(kw in lower for kw in _COMPLEX_KW): wants_complex = any(kw in lower for kw in _COMPLEX_KW)
# Nicht downgraden wenn komplexes Modell bereits geladen ist
try:
s = httpx.get("http://127.0.0.1:9000/api/status", timeout=3).json()
loaded = {m["name"] for m in s.get("models", []) if m.get("state") in ("running", "ready")}
if HERMES_COMPLEX_MODEL in loaded:
return HERMES_COMPLEX_MODEL return HERMES_COMPLEX_MODEL
return HERMES_SIMPLE_MODEL except Exception:
pass
return HERMES_COMPLEX_MODEL if wants_complex else HERMES_SIMPLE_MODEL
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# System-Prompt # System-Prompt
+2 -2
View File
@@ -148,7 +148,7 @@ async def transcribe_audio(audio: UploadFile = File(...)):
tmp = Path(f.name) tmp = Path(f.name)
tmp.write_bytes(await audio.read()) tmp.write_bytes(await audio.read())
try: try:
segments, _ = await asyncio.get_event_loop().run_in_executor( segments, _ = await asyncio.get_running_loop().run_in_executor(
None, lambda: model.transcribe(str(tmp), language="de") None, lambda: model.transcribe(str(tmp), language="de")
) )
text = " ".join(s.text.strip() for s in segments).strip() text = " ".join(s.text.strip() for s in segments).strip()
@@ -165,7 +165,7 @@ async def text_to_speech(body: dict):
text = (body.get("text") or "").strip() text = (body.get("text") or "").strip()
if not text: if not text:
return JSONResponse(status_code=400, content={"error": "Kein Text angegeben."}) return JSONResponse(status_code=400, content={"error": "Kein Text angegeben."})
audio = await asyncio.get_event_loop().run_in_executor(None, lambda: _tts(text)) audio = await asyncio.get_running_loop().run_in_executor(None, lambda: _tts(text))
if audio is None: if audio is None:
return JSONResponse( return JSONResponse(
status_code=503, status_code=503,
+14 -3
View File
@@ -6,6 +6,7 @@ laeuft als Hintergrund-Job mit Live-Log. Spaeter wandert hier ggf. mehr
Server-Wartung hinein (siehe Roadmap: Server-Management). Server-Wartung hinein (siehe Roadmap: Server-Management).
""" """
import os
import shlex import shlex
import subprocess import subprocess
import asyncio import asyncio
@@ -22,11 +23,21 @@ router = APIRouter(prefix="/api", dependencies=[Depends(auth)])
class PwdReq(BaseModel): class PwdReq(BaseModel):
password: str password: str
class UpdateReq(BaseModel):
password: str = ""
@router.post("/update") @router.post("/update")
def update(): def update(req: UpdateReq = None):
"""LLM-Engine (llama.cpp) aktualisieren — Befehl aus MC_UPDATE_CMD.""" """LLM-Engine (llama.cpp) aktualisieren — Befehl aus MC_UPDATE_CMD.
Optionales Passwort fuer sudo-Befehle (via stdin, nicht in argv)."""
if not UPDATE_CMD: if not UPDATE_CMD:
raise HTTPException(400, "Kein Update-Befehl gesetzt (MC_UPDATE_CMD).") raise HTTPException(400, "Kein Update-Befehl gesetzt (MC_UPDATE_CMD).")
pwd = (req.password if req else "").strip()
if pwd:
args = ["sudo", "-S", "bash", "-c", UPDATE_CMD]
job_id = start_job(args, "llama.cpp-Update", stdin_data=pwd + "\n",
log_cmd=f"$ sudo {UPDATE_CMD}")
else:
job_id = start_job(shlex.split(UPDATE_CMD), "llama.cpp-Update") job_id = start_job(shlex.split(UPDATE_CMD), "llama.cpp-Update")
return {"job_id": job_id} return {"job_id": job_id}
@@ -118,7 +129,7 @@ def updates():
apt_cache_age_h = None apt_cache_age_h = None
try: try:
import time as _time import time as _time
apt_cache_age_h = round((_time.time() - os.path.getmtime("/var/lib/apt/lists")) / 3600, 1) apt_cache_age_h = round((_time.time() - os.path.getmtime("/var/lib/apt/lists")) / 3600, 1) # noqa: PTH
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
pass pass
return {"os": os_count, "models": models, "engine": 1 if _engine_update_available() else 0, return {"os": os_count, "models": models, "engine": 1 if _engine_update_available() else 0,
+50 -50
View File
File diff suppressed because one or more lines are too long