Hermes-GUI: eingebaute hermes serve-Weboberflaeche same-origin in MC2 einbetten (ersetzt CLI-Terminal-Tab)
Statt des ttyd-CLI-Terminals zeigt der Tab jetzt die reiche eingebaute Hermes-Web-GUI (hermes serve/dashboard: Threads, sichtbare Tool-Calls, Sessions, Config) - same-origin, ohne offenen Port, ohne Login (Loopback :9119 = LAN-Trust wie Terminal/Konsole). - backend/routers/hermes_ui.py: generischer Reverse-Proxy /hermes-ui/* (alle Methoden + beliebige WebSockets) -> 127.0.0.1:9119; injiziert window.__HERMES_BASE_PATH__=/hermes-ui in die index.html, damit die SPA ihre API/WS-Calls unter dem Praefix absetzt (sonst Kollision mit MC2s /api). - deploy/hermes-builtin-ui.service: hermes serve --skip-build auf Loopback :9119. - deploy/deploy.sh: baut das GUI-Bundle mit Vite-base=/hermes-ui/ (Temp->Swap, ueberlebt Hermes- Updates) + installiert/startet den Dienst. - backend/app.py + config.py + services/agent.py: Router registriert, Upstream/Pfad + Status-Feld. - frontend: Terminal-Tab -> "Hermes GUI" (TerminalView iframe /hermes-ui/, nav-Label, vite-Dev-Proxy). E2E im Browser verifiziert: GUI rendert eingebettet, /hermes-ui/api/* liefern echte Daten (289 Sessions, verbundene Plattformen), Assets + Plugins laden, Basis-Pfad korrekt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -19,7 +19,7 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from config import FRONTEND_DIST, VERSION
|
from config import FRONTEND_DIST, VERSION
|
||||||
from routers import agent, connect, console, gateway_proxy, health, maintenance, memory, models, reminders as reminders_router, routing, system, voice
|
from routers import agent, connect, console, gateway_proxy, health, hermes_ui, maintenance, memory, models, reminders as reminders_router, routing, system, voice
|
||||||
from services import memory as memory_svc, reminders, sentry, warmer
|
from services import memory as memory_svc, reminders, sentry, warmer
|
||||||
|
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||||
@@ -90,6 +90,7 @@ app.include_router(reminders_router.router) # Erinnerungen/Routinen (A3) — fe
|
|||||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||||
app.include_router(maintenance.router)
|
app.include_router(maintenance.router)
|
||||||
app.include_router(console.router) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
app.include_router(console.router) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||||
|
app.include_router(hermes_ui.router) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
||||||
|
|
||||||
|
|
||||||
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ HERMES_TERMINAL_PATH = "/hermes-terminal/"
|
|||||||
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
|
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
|
||||||
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
||||||
BOX_CONSOLE_PATH = "/console/"
|
BOX_CONSOLE_PATH = "/console/"
|
||||||
|
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
|
||||||
|
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). Bindet NUR an Loopback
|
||||||
|
# (127.0.0.1:9119, kein Login — LAN-Trust wie Terminal/Konsole) und wird von MC2 same-origin unter
|
||||||
|
# /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle
|
||||||
|
# SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix
|
||||||
|
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
|
||||||
|
HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/")
|
||||||
|
HERMES_BUILTIN_UI_PATH = "/hermes-ui/"
|
||||||
# GitHub-Repo für Update-Checks.
|
# GitHub-Repo für Update-Checks.
|
||||||
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
||||||
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
|
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""
|
||||||
|
Same-origin Reverse-Proxy für die EINGEBAUTE Hermes-Web-GUI (`hermes serve`/`dashboard`).
|
||||||
|
|
||||||
|
Anders als das ttyd-Terminal (reiner Byte-Durchreicher, siehe routers/console.py) ist die
|
||||||
|
Hermes-GUI eine volle Single-Page-App: HTML + gehashte Assets + JSON-API + MEHRERE WebSockets
|
||||||
|
(/api/ws, /api/console, /api/pty, /api/events). Deshalb hier ein GENERISCHER Reverse-Proxy für
|
||||||
|
alles unter `/hermes-ui/*` (alle HTTP-Methoden + beliebige WebSockets).
|
||||||
|
|
||||||
|
Der Backend-Server läuft NUR auf Loopback (127.0.0.1:9119, `hermes serve --skip-build`, kein
|
||||||
|
Login — gleiches LAN-Trust-Modell wie Konsole/Terminal) und wird über den ohnehin offenen
|
||||||
|
MC2-Port (9001) same-origin durchgereicht → keine eigene Firewall-Freigabe, kein CORS.
|
||||||
|
|
||||||
|
Damit die absoluten Pfade der SPA (`/assets`, `/api`, `/api/ws`) nicht mit MC2s eigenen (`/assets`,
|
||||||
|
`/api`) kollidieren, wird das Hermes-Bundle mit Vite-`base=/hermes-ui/` gebaut (deploy.sh) — dann
|
||||||
|
liegen ALLE seine Pfade unter `/hermes-ui/`. Dieser Proxy streift das Präfix ab und leitet an
|
||||||
|
`:9119/...` weiter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import websockets
|
||||||
|
from fastapi import APIRouter, Request, WebSocket
|
||||||
|
from starlette.responses import RedirectResponse, Response
|
||||||
|
|
||||||
|
from config import HERMES_BUILTIN_UI_UPSTREAM
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
_BASE = "hermes-ui"
|
||||||
|
|
||||||
|
# Hop-by-hop- + Kodierungs-Header, die ein Proxy nicht 1:1 weiterreichen darf (Länge/Encoding
|
||||||
|
# rechnet Starlette neu; httpx liefert bereits dekodierten Body).
|
||||||
|
_DROP = {
|
||||||
|
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers",
|
||||||
|
"transfer-encoding", "upgrade", "content-encoding", "content-length", "host",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket(f"/{_BASE}/{{path:path}}")
|
||||||
|
async def _proxy_ws(ws: WebSocket, path: str) -> None:
|
||||||
|
"""Beliebige WebSocket unter /hermes-ui/* (z. B. /hermes-ui/api/ws) an :9119 durchreichen."""
|
||||||
|
ws_upstream = HERMES_BUILTIN_UI_UPSTREAM.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
query = ws.url.query
|
||||||
|
target = f"{ws_upstream}/{path}" + (f"?{query}" if query else "")
|
||||||
|
await ws.accept()
|
||||||
|
try:
|
||||||
|
async with websockets.connect(
|
||||||
|
target, open_timeout=10, max_size=None, ping_interval=None,
|
||||||
|
) as up:
|
||||||
|
async def c2u() -> None:
|
||||||
|
while True:
|
||||||
|
msg = await ws.receive()
|
||||||
|
if msg.get("type") == "websocket.disconnect":
|
||||||
|
return
|
||||||
|
if (t := msg.get("text")) is not None:
|
||||||
|
await up.send(t)
|
||||||
|
elif (b := msg.get("bytes")) is not None:
|
||||||
|
await up.send(b)
|
||||||
|
|
||||||
|
async def u2c() -> None:
|
||||||
|
async for m in up:
|
||||||
|
if isinstance(m, (bytes, bytearray)):
|
||||||
|
await ws.send_bytes(bytes(m))
|
||||||
|
else:
|
||||||
|
await ws.send_text(m)
|
||||||
|
|
||||||
|
_done, pending = await asyncio.wait(
|
||||||
|
[asyncio.create_task(c2u()), asyncio.create_task(u2c())],
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
except Exception:
|
||||||
|
log.debug("hermes-ui ws-proxy (%s) beendet/fehlgeschlagen", path, exc_info=True)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
await ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Die SPA liest ihren API/WS-Basis-Pfad zur Laufzeit aus `window.__HERMES_BASE_PATH__` (nf() im
|
||||||
|
# Bundle). `hermes serve` injiziert diese Zuweisung selbst ans Ende von <head> (Wert leer, weil es
|
||||||
|
# auf Root läuft) — dessen Wert überschreiben wir auf /hermes-ui, sonst setzt die GUI ihre /api-Calls
|
||||||
|
# auf MC2s eigenen /api-Namensraum (SPA-Fallback = HTML) ab. Die Assets liegen bereits per
|
||||||
|
# Vite-base=/hermes-ui/ richtig. MC2 macht die Ersetzung → überlebt Hermes-Updates, ohne die gebaute
|
||||||
|
# index.html patchen zu müssen. Fällt die Server-Injektion je weg, setzen wir sie ersatzweise selbst.
|
||||||
|
_BASE_PATH_RE = re.compile(rb'window\.__HERMES_BASE_PATH__\s*=\s*"[^"]*"')
|
||||||
|
_BASE_PATH_SET = b'window.__HERMES_BASE_PATH__="/' + _BASE.encode() + b'"'
|
||||||
|
_HEAD_INJECT = b"<head><script>" + _BASE_PATH_SET + b";</script>"
|
||||||
|
|
||||||
|
|
||||||
|
async def _proxy_http(request: Request, path: str = "") -> Response:
|
||||||
|
upstream = f"{HERMES_BUILTIN_UI_UPSTREAM}/{path}"
|
||||||
|
fwd_headers = {k: v for k, v in request.headers.items() if k.lower() not in _DROP}
|
||||||
|
# Upstream unkomprimiert anfordern: sonst greift die HTML-Injektion nicht und die
|
||||||
|
# content-encoding-Header würden nicht zum (bereits dekodierten) Body passen.
|
||||||
|
fwd_headers["accept-encoding"] = "identity"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=30.0, follow_redirects=False) as c:
|
||||||
|
r = await c.request(
|
||||||
|
request.method, upstream,
|
||||||
|
headers=fwd_headers, params=request.query_params, content=await request.body(),
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
log.debug("hermes-ui http-proxy (%s) nicht erreichbar: %s", path, exc)
|
||||||
|
return Response(
|
||||||
|
content=b"Hermes-WebUI (hermes serve) nicht erreichbar.",
|
||||||
|
status_code=502, media_type="text/plain",
|
||||||
|
)
|
||||||
|
body = r.content
|
||||||
|
if "text/html" in r.headers.get("content-type", "").lower():
|
||||||
|
if _BASE_PATH_RE.search(body):
|
||||||
|
body = _BASE_PATH_RE.sub(_BASE_PATH_SET, body)
|
||||||
|
elif b"<head>" in body:
|
||||||
|
body = body.replace(b"<head>", _HEAD_INJECT, 1)
|
||||||
|
resp_headers = {k: v for k, v in r.headers.items() if k.lower() not in _DROP}
|
||||||
|
return Response(content=body, status_code=r.status_code, headers=resp_headers)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(f"/{_BASE}")
|
||||||
|
async def _root_redirect() -> RedirectResponse:
|
||||||
|
# Ohne Schrägstrich → auf /hermes-ui/ umleiten (die SPA erwartet den Präfix mit Slash).
|
||||||
|
return RedirectResponse(url=f"/{_BASE}/")
|
||||||
|
|
||||||
|
|
||||||
|
router.add_api_route(
|
||||||
|
f"/{_BASE}/{{path:path}}", _proxy_http,
|
||||||
|
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
||||||
|
name="hermes_ui_proxy",
|
||||||
|
)
|
||||||
@@ -12,7 +12,8 @@ import httpx
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from config import (HERMES_TERMINAL_UPSTREAM, HERMES_TERMINAL_PATH, BOX_CONSOLE_UPSTREAM,
|
from config import (HERMES_TERMINAL_UPSTREAM, HERMES_TERMINAL_PATH, BOX_CONSOLE_UPSTREAM,
|
||||||
BOX_CONSOLE_PATH, HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL)
|
BOX_CONSOLE_PATH, HERMES_BUILTIN_UI_UPSTREAM, HERMES_BUILTIN_UI_PATH,
|
||||||
|
HERMES_API_URL, HERMES_HOME, PC_EXECUTOR_URL)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -144,10 +145,14 @@ def agent_status() -> dict:
|
|||||||
"terminal_url": HERMES_TERMINAL_PATH,
|
"terminal_url": HERMES_TERMINAL_PATH,
|
||||||
# Box-Konsole (ttyd → Login-Shell): same-origin über MC2 geproxyt (/console/).
|
# Box-Konsole (ttyd → Login-Shell): same-origin über MC2 geproxyt (/console/).
|
||||||
"box_console_url": BOX_CONSOLE_PATH,
|
"box_console_url": BOX_CONSOLE_PATH,
|
||||||
|
# Eingebaute Hermes-Web-GUI (hermes serve): same-origin über MC2 geproxyt (/hermes-ui/).
|
||||||
|
"hermes_ui_url": HERMES_BUILTIN_UI_PATH,
|
||||||
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
|
"gateway_reachable": _reach(HERMES_API_URL, "/health"),
|
||||||
# Erreichbarkeit der lokalen ttyd-Upstreams (Loopback, je base-path).
|
# Erreichbarkeit der lokalen ttyd-Upstreams (Loopback, je base-path).
|
||||||
"terminal_reachable": _reach(HERMES_TERMINAL_UPSTREAM, "/hermes-terminal/"),
|
"terminal_reachable": _reach(HERMES_TERMINAL_UPSTREAM, "/hermes-terminal/"),
|
||||||
"box_console_reachable": _reach(BOX_CONSOLE_UPSTREAM, "/console/"),
|
"box_console_reachable": _reach(BOX_CONSOLE_UPSTREAM, "/console/"),
|
||||||
|
# Erreichbarkeit der eingebauten Hermes-GUI (Loopback :9119).
|
||||||
|
"hermes_ui_reachable": _reach(HERMES_BUILTIN_UI_UPSTREAM, "/"),
|
||||||
"home_exists": home.exists(),
|
"home_exists": home.exists(),
|
||||||
"brain_model": brain_model,
|
"brain_model": brain_model,
|
||||||
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
|
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
|
||||||
|
|||||||
@@ -86,12 +86,29 @@ cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
|
|||||||
# Orchestrator-Skill (Autonomie): Werkstatt verallgemeinert — zerlegen → an Worker delegieren → gaten.
|
# Orchestrator-Skill (Autonomie): Werkstatt verallgemeinert — zerlegen → an Worker delegieren → gaten.
|
||||||
mkdir -p "$HOME/.hermes/skills/orchestrator"
|
mkdir -p "$HOME/.hermes/skills/orchestrator"
|
||||||
cp "$SRC/deploy/skills/orchestrator/SKILL.md" "$HOME/.hermes/skills/orchestrator/SKILL.md"
|
cp "$SRC/deploy/skills/orchestrator/SKILL.md" "$HOME/.hermes/skills/orchestrator/SKILL.md"
|
||||||
|
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
|
||||||
|
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
|
||||||
|
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
|
||||||
|
# Hermes-Update mit → hält die Einbettung frisch. Best-effort + Build-zu-Temp-dann-Swap, damit ein
|
||||||
|
# Fehlschlag die laufende web_dist NICHT leert (emptyOutDir).
|
||||||
|
cp "$SRC/deploy/hermes-builtin-ui.service" "$HOME/.config/systemd/user/hermes-builtin-ui.service"
|
||||||
|
_HUI_WEB="$HOME/.hermes/hermes-agent/web"
|
||||||
|
_HUI_DIST="$HOME/.hermes/hermes-agent/hermes_cli/web_dist"
|
||||||
|
if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
|
||||||
|
if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \
|
||||||
|
--base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then
|
||||||
|
rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut."
|
||||||
|
else
|
||||||
|
echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
||||||
systemctl --user enable hermes-terminal >/dev/null 2>&1 || true
|
systemctl --user enable hermes-terminal >/dev/null 2>&1 || true
|
||||||
systemctl --user enable box-console >/dev/null 2>&1 || true
|
systemctl --user enable box-console >/dev/null 2>&1 || true
|
||||||
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
||||||
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
||||||
|
systemctl --user enable hermes-builtin-ui >/dev/null 2>&1 || true
|
||||||
systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
||||||
systemctl --user enable --now mc2-autoupdate.timer >/dev/null 2>&1 || true
|
systemctl --user enable --now mc2-autoupdate.timer >/dev/null 2>&1 || true
|
||||||
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
||||||
@@ -100,6 +117,8 @@ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
|||||||
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||||
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
|
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
|
||||||
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
|
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
|
||||||
|
# Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein.
|
||||||
|
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
|
||||||
systemctl --user restart mission-control-2
|
systemctl --user restart mission-control-2
|
||||||
command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/dev/null || true
|
command -v ttyd >/dev/null 2>&1 && systemctl --user restart hermes-terminal 2>/dev/null || true
|
||||||
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hermes Built-in Web UI (`hermes serve` — Agent-Dashboard/Chat, Loopback only)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Serviert die EINGEBAUTE Hermes-Web-GUI (die reiche Agent-Oberflaeche mit Threads/Tool-Calls, die
|
||||||
|
# auch die Electron-Desktop-App umhuellt) headless.
|
||||||
|
# SICHERHEIT: Bindet NUR an Loopback (127.0.0.1:9119) und laeuft hinter dem MC2-Reverse-Proxy
|
||||||
|
# (routers/hermes_ui.py → same-origin /hermes-ui/ auf dem offenen Port 9001) — KEINE eigene
|
||||||
|
# Firewall-Freigabe, KEIN Login (LAN-Trust wie Konsole/Terminal). --skip-build nutzt das von
|
||||||
|
# deploy.sh mit Vite-base=/hermes-ui/ vorgebaute web_dist, damit alle SPA-Pfade unter /hermes-ui/
|
||||||
|
# liegen und der Proxy sie sauber durchreichen kann.
|
||||||
|
Type=simple
|
||||||
|
Environment=HERMES_HOME=%h/.hermes
|
||||||
|
ExecStart=%h/.hermes/hermes-agent/venv/bin/hermes serve --skip-build --host 127.0.0.1 --port 9119
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+111
-106
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BZIxDFd6.js"></script>
|
<script type="module" crossorigin src="/assets/index-_1h7akwb.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DfNbRJ2k.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DfNbRJ2k.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -373,9 +373,11 @@ export interface AgentStatus {
|
|||||||
gateway_url: string
|
gateway_url: string
|
||||||
terminal_url: string
|
terminal_url: string
|
||||||
box_console_url?: string
|
box_console_url?: string
|
||||||
|
hermes_ui_url?: string
|
||||||
gateway_reachable: boolean
|
gateway_reachable: boolean
|
||||||
terminal_reachable: boolean
|
terminal_reachable: boolean
|
||||||
box_console_reachable?: boolean
|
box_console_reachable?: boolean
|
||||||
|
hermes_ui_reachable?: boolean
|
||||||
home_exists: boolean
|
home_exists: boolean
|
||||||
brain_model?: string
|
brain_model?: string
|
||||||
has_config: boolean
|
has_config: boolean
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import {
|
|||||||
Brain,
|
Brain,
|
||||||
Plug,
|
Plug,
|
||||||
Bot,
|
Bot,
|
||||||
TerminalSquare,
|
AppWindow,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
@@ -26,7 +26,7 @@ export const NAV: NavItem[] = [
|
|||||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
||||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
||||||
{ id: "agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot },
|
{ id: "agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot },
|
||||||
{ id: "terminal", label: "Hermes-Terminal", hint: "Interaktives Hermes-Agent-Terminal", icon: TerminalSquare },
|
{ id: "terminal", label: "Hermes GUI", hint: "Eingebaute Hermes-Weboberfläche (Threads & Tool-Calls)", icon: AppWindow },
|
||||||
{ id: "konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal },
|
{ id: "konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal },
|
||||||
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
|
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { TerminalSquare, ExternalLink, AlertTriangle } from "lucide-react"
|
import { Bot, ExternalLink, AlertTriangle } from "lucide-react"
|
||||||
import { useAgentStatus } from "@/lib/queries"
|
import { useAgentStatus } from "@/lib/queries"
|
||||||
import { resolveExternalUrl } from "@/lib/utils"
|
import { resolveExternalUrl } from "@/lib/utils"
|
||||||
|
|
||||||
export function TerminalView() {
|
export function TerminalView() {
|
||||||
const { data: agent } = useAgentStatus(5_000)
|
const { data: agent } = useAgentStatus(5_000)
|
||||||
const url = agent?.terminal_url ? resolveExternalUrl(agent.terminal_url) : undefined
|
const url = agent?.hermes_ui_url ? resolveExternalUrl(agent.hermes_ui_url) : undefined
|
||||||
const ok = agent?.terminal_reachable
|
const ok = agent?.hermes_ui_reachable
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col gap-4">
|
<div className="flex h-full flex-col gap-4">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
|
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
|
||||||
Hermes Terminal
|
Hermes GUI
|
||||||
</h1>
|
</h1>
|
||||||
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
Interaktiver Agent (<code className="rounded bg-background/40 px-1 font-mono text-[11px] text-primary">hermes chat</code>) mit Tools & PC-Steuerung — „mehr als Chatten".
|
Die eingebaute Hermes-Weboberfläche — Threads, sichtbare Tool-Calls & Chat, direkt im Cockpit statt im Terminal.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -37,18 +37,18 @@ export function TerminalView() {
|
|||||||
{ok === false && (
|
{ok === false && (
|
||||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center">
|
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center">
|
||||||
<AlertTriangle className="h-8 w-8 text-amber-400" />
|
<AlertTriangle className="h-8 w-8 text-amber-400" />
|
||||||
<div className="text-sm font-semibold text-amber-300">Terminal nicht erreichbar</div>
|
<div className="text-sm font-semibold text-amber-300">Hermes-GUI nicht erreichbar</div>
|
||||||
<p className="max-w-sm text-[11px] leading-normal text-muted-foreground">
|
<p className="max-w-sm text-[11px] leading-normal text-muted-foreground">
|
||||||
Der <code className="font-mono text-primary">hermes-terminal</code>-Dienst (ttyd) läuft nicht.
|
Der <code className="font-mono text-primary">hermes-builtin-ui</code>-Dienst (<code className="font-mono">hermes serve</code>) läuft nicht.
|
||||||
Auf der Box: <code className="font-mono">systemctl --user restart hermes-terminal</code>.
|
Auf der Box: <code className="font-mono">systemctl --user restart hermes-builtin-ui</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<iframe src={url} title="Hermes Terminal" className="h-full w-full border-0" style={{ minHeight: "68vh" }} />
|
<iframe src={url} title="Hermes GUI" className="h-full w-full border-0" style={{ minHeight: "68vh" }} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-[68vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground">
|
<div className="flex min-h-[68vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground">
|
||||||
<TerminalSquare className="mr-2 h-4 w-4" /> Lade Terminal…
|
<Bot className="mr-2 h-4 w-4" /> Lade Hermes-GUI…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export default defineConfig({
|
|||||||
// Web-Terminals (ttyd) laufen same-origin über den MC2-Reverse-Proxy (/<name>/ + /<name>/ws).
|
// Web-Terminals (ttyd) laufen same-origin über den MC2-Reverse-Proxy (/<name>/ + /<name>/ws).
|
||||||
"/console": { target: process.env.MC_API_TARGET || "http://127.0.0.1:9000", changeOrigin: true, ws: true },
|
"/console": { target: process.env.MC_API_TARGET || "http://127.0.0.1:9000", changeOrigin: true, ws: true },
|
||||||
"/hermes-terminal": { target: process.env.MC_API_TARGET || "http://127.0.0.1:9000", changeOrigin: true, ws: true },
|
"/hermes-terminal": { target: process.env.MC_API_TARGET || "http://127.0.0.1:9000", changeOrigin: true, ws: true },
|
||||||
|
// Eingebaute Hermes-Web-GUI (hermes serve) — same-origin über den MC2-Reverse-Proxy, mit WS.
|
||||||
|
"/hermes-ui": { target: process.env.MC_API_TARGET || "http://127.0.0.1:9000", changeOrigin: true, ws: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: { outDir: "dist" },
|
build: { outDir: "dist" },
|
||||||
|
|||||||
Reference in New Issue
Block a user