v4 Schritt 3: 'Verbinden'-Tab (Orchestrierung)

- routers/integration.py: GET /api/integration/test ruft Engine /v1/models -> ok + Modell-Liste.
- connect.js: Verbindungstest-Button, Bild/Vision-Flow (erkennt Vision-Modell, sonst Cookbook-Hinweis,
  OpenAI-Vision-Snippet), Tool-Configs (OpenCode/Cline/Cursor/OpenWebUI) mit Copy.
- Nav-Eintrag 'Verbinden' + View + main.js Registrierung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 13:40:43 +02:00
parent 29f37bd916
commit 2a60def469
5 changed files with 167 additions and 2 deletions
+2 -1
View File
@@ -20,7 +20,7 @@ from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from routers import jobs, maintenance, models, system, cookbook
from routers import jobs, maintenance, models, system, cookbook, integration
app = FastAPI(title="Mission Control")
@@ -41,6 +41,7 @@ app.include_router(jobs.router)
app.include_router(maintenance.router)
app.include_router(system.router)
app.include_router(cookbook.router)
app.include_router(integration.router)
_STATIC = Path(__file__).parent / "static"
+24
View File
@@ -0,0 +1,24 @@
"""
Integrations-Router: testet die OpenAI-kompatible Verbindung zur LLM-Engine.
Damit der „Verbinden"-Assistent dem Nutzer sagen kann: funktioniert ✓ + welche Modelle.
"""
from fastapi import APIRouter, Depends
from auth import auth
from config import LLAMA_SWAP_URL
from llamaswap import _swap_get
router = APIRouter(prefix="/api/integration", dependencies=[Depends(auth)])
@router.get("/test")
def test_connection():
"""Ruft die OpenAI-kompatible Modell-Liste der Engine ab (das, was externe Tools nutzen)."""
try:
data = _swap_get("/v1/models")
items = data.get("data", data) if isinstance(data, dict) else data
models = [m.get("id") for m in (items or []) if isinstance(m, dict) and m.get("id")]
return {"ok": True, "models": models, "url": LLAMA_SWAP_URL}
except Exception as exc: # noqa: BLE001
return {"ok": False, "error": str(exc), "url": LLAMA_SWAP_URL}
+5
View File
@@ -19,6 +19,7 @@
<a class="nav-item" data-view="activity"><span class="ni-ic" data-ic="pulse"></span><span class="ni-tx">Aktivität</span></a>
<a class="nav-item" data-view="server"><span class="ni-ic" data-ic="server"></span><span class="ni-tx">Server</span></a>
<a class="nav-item" data-view="cookbook"><span class="ni-ic" data-ic="book"></span><span class="ni-tx">Cookbook</span></a>
<a class="nav-item" data-view="connect"><span class="ni-ic" data-ic="swap"></span><span class="ni-tx">Verbinden</span></a>
<a class="nav-item" data-view="guides"><span class="ni-ic" data-ic="help"></span><span class="ni-tx">Guides</span></a>
</nav>
<div class="side-foot">
@@ -72,6 +73,10 @@
<!-- Wird von cookbook.js gerendert -->
</section>
<section class="view" data-view="connect" hidden>
<!-- Wird von connect.js gerendert -->
</section>
<section class="view" data-view="guides" hidden>
<!-- Wird von guides.js gerendert -->
</section>
+2 -1
View File
@@ -10,9 +10,10 @@ import models from "./panels/models.js";
import server from "./panels/server.js";
import jobs from "./panels/jobs.js";
import cookbook from "./panels/cookbook.js";
import connect from "./panels/connect.js";
import guides from "./panels/guides.js";
const panels = [overview, models, server, jobs, cookbook, guides];
const panels = [overview, models, server, jobs, cookbook, connect, guides];
let lastJobs = [];
let lastSystem = null;
+134
View File
@@ -0,0 +1,134 @@
// connect.js — „Verbinden" (v4): IDEs/Agenten anbinden, Verbindung testen, Bild/Vision-Flow.
import { api } from "../core/api.js";
import { $, esc, icon } from "../core/ui.js";
let S = null;
let lastSig = "";
window.mcCopyVal = btn => {
const i = btn.previousElementSibling;
navigator.clipboard?.writeText(i.value);
const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1200);
};
window.mcCopyCode = btn => {
const code = btn.parentElement.querySelector("code");
navigator.clipboard?.writeText(code.textContent);
const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1200);
};
window.cnGo = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click();
function baseUrl() { return `${location.protocol}//${location.hostname}:8080/v1`; }
function models() { return S?.models || []; }
function field(label, value) {
return `<label>${label}</label>
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
<input class="mono-sm" readonly value="${esc(value)}" style="flex:1;margin:0">
<button class="ghost" onclick="window.mcCopyVal(this)">Kopieren</button>
</div>`;
}
const okChip = txt => `<span class="chip" style="background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a">${icon("check")}${esc(txt)}</span>`;
function render() {
const c = $(".view[data-view='connect']"); if (!c) return;
const url = baseUrl();
const ids = models().map(m => m.name);
const firstModel = ids[0] || "coder";
const visionModel = models().find(m => m.meta?.caps?.includes("Bild"))?.name;
c.innerHTML = `
<div class="pagehead"><div>
<h1>Verbinden</h1>
<div class="sub">Binde deine lokalen Modelle in IDEs, Agenten & Tools ein — getestet, mit Copy-Paste-Config.</div></div></div>
<div class="card">
<div class="card-h"><h3>Verbindung zur Engine</h3></div>
<div class="card-sub">Alle Tools nutzen diese OpenAI-kompatible Adresse. Das Auto-Swapping lädt das passende Modell je Anfrage automatisch.</div>
${field("Basis-URL (OpenAI-kompatibel)", url)}
<div class="btn-row"><button class="primary" id="cn-test">Verbindung testen</button></div>
<div id="cn-test-result" style="margin-top:12px"></div>
</div>
<div class="card">
<div class="card-h"><h3>Bilder einbinden (Vision)</h3></div>
<div class="card-sub">Für Fehleranalyse per Screenshot brauchst du ein Vision-Modell.</div>
<div id="cn-vision"></div>
</div>
<div class="card" style="padding:0;overflow:hidden">
<details class="guide-acc" open>
<summary>OpenCode (Windows)</summary>
<div class="acc-body">
<p>Lege in OpenCode einen OpenAI-kompatiblen Provider an:</p>
${field("Basis-URL", url)}
${field("Modell-ID", firstModel)}
${field("API-Key", "(dein Token oder leer lassen)")}
</div>
</details>
<details class="guide-acc">
<summary>Cline / Cursor</summary>
<div class="acc-body">
<p>Provider „OpenAI Compatible" wählen:</p>
${field("Basis-URL", url)}
${field("Modell-ID", firstModel)}
</div>
</details>
<details class="guide-acc">
<summary>OpenWebUI</summary>
<div class="acc-body">
<p>Settings → Admin → Connections → neue OpenAI-Verbindung:</p>
${field("Basis-URL", url)}
${field("API-Key", "dummy-key")}
</div>
</details>
</div>`;
$("#cn-test").addEventListener("click", testConnection);
renderVision(url, visionModel);
}
function renderVision(url, visionModel) {
const el = $("#cn-vision"); if (!el) return;
if (visionModel) {
const snippet = `curl ${url}/chat/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "${visionModel}",
"messages": [{"role":"user","content":[
{"type":"text","text":"Was ist auf dem Bild falsch?"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}
]}]
}'`;
el.innerHTML = `${okChip(`Vision-Modell „${visionModel}" bereit`)}
<div style="position:relative;margin-top:12px">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopyCode(this)">Kopieren</button>
<div class="log" style="max-height:none"><code>${esc(snippet)}</code></div>
</div>`;
} else {
el.innerHTML = `<div class="empty-c" style="padding:24px">
<div class="e-t">Noch kein Vision-Modell installiert</div>
<div class="e-s">Im Cookbook unter „Bilder verstehen" mit einem Klick einrichten.</div>
<button class="primary" style="margin-top:14px" onclick="window.cnGo('cookbook')">Zum Cookbook</button>
</div>`;
}
}
async function testConnection() {
const res = $("#cn-test-result"); res.innerHTML = `<div class="hint">Teste Verbindung…</div>`;
try {
const r = await api("/api/integration/test");
res.innerHTML = r.ok
? okChip(`Funktioniert — ${r.models.length} Modell(e): ${r.models.join(", ")}`)
: `<div class="alert warn" style="margin:0"><span class="a-dot"></span><span>Keine Verbindung zur Engine: ${esc(r.error)}</span></div>`;
} catch (e) { res.innerHTML = `<div class="alert" style="margin:0"><span class="a-dot"></span><span>${esc(e.message)}</span></div>`; }
}
function mount() { render(); }
function onStatus(s) {
S = s;
const sig = (s?.models || []).map(m => m.name).join(",") + "|" + !!s;
if (sig !== lastSig) { lastSig = sig; render(); }
}
export default { id: "connect", mount, onStatus };