Feat: Hermes-Terminal (ttyd) statt AnythingLLM + AnythingLLM komplett raus

Paket B des Plans. AnythingLLM war nur ein Fallback-Chat zu Hermes; ersetzt durch das
echte interaktive Agent-Terminal (`hermes chat`, mit Tools/PC) als eingebettetes
Web-Terminal.

Box: ttyd (apt) wrappt `hermes chat`; systemd-User-Unit deploy/hermes-terminal.service
(LAN-Bind eno1:7681, apt-Default-ttyd-Dienst deaktiviert). In deploy.sh verankert.

Backend: config HERMES_TERMINAL_URL statt ANYTHINGLLM_URL/_REPO; agent_status liefert
terminal_url/terminal_reachable; maintenance ohne _anythingllm_update; system.py Dienst-Liste
zeigt "Hermes-Terminal".

Frontend: neue Terminal-Seite (iframe auf ttyd) + Nav-Tab; AgentView/AgentStatusCard/nav/api
auf Terminal umgestellt; SystemDrawer toter hermes-dashboard raus, hermes-webui -> hermes-terminal;
Guide-Texte aktualisiert.

Cleanup: deploy/hermes-webui.service + deploy/lobechat/ entfernt (LobeChat-Migration hinfaellig),
HERMES_WEBUI_URL-Env raus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 18:23:37 +02:00
parent cf65589b93
commit dcfede7e69
23 changed files with 246 additions and 281 deletions
+3 -1
View File
@@ -7,6 +7,7 @@ import { ModelsView } from "@/views/ModelsView"
import { ConnectView } from "@/views/ConnectView"
import { MemoryView } from "@/views/MemoryView"
import { AgentView } from "@/views/AgentView"
import { TerminalView } from "@/views/TerminalView"
import { GuideView } from "@/views/GuideView"
import { Placeholder } from "@/views/Placeholder"
import { SystemDrawer } from "@/components/SystemDrawer"
@@ -180,8 +181,9 @@ export default function App() {
{view === "connect" && <ConnectView />}
{view === "memory" && <MemoryView />}
{view === "agent" && <AgentView />}
{view === "terminal" && <TerminalView />}
{view === "guide" && <GuideView />}
{!["dashboard", "models", "connect", "memory", "agent", "guide"].includes(view) && (
{!["dashboard", "models", "connect", "memory", "agent", "terminal", "guide"].includes(view) && (
<Placeholder title={active.label} hint={active.hint} />
)}
</main>
+1 -2
View File
@@ -15,8 +15,7 @@ const SERVICES = [
{ id: "llama-swap", label: "Llama Swap", type: "system" },
{ id: "mission-control-2", label: "Mission Control 2", type: "user" },
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user" },
{ id: "hermes-dashboard", label: "Hermes Dashboard", type: "user" },
{ id: "hermes-webui", label: "Hermes WebUI", type: "user" }
{ id: "hermes-terminal", label: "Hermes Terminal", type: "user" }
]
function formatBytes(bytes?: number) {
@@ -33,19 +33,19 @@ export function AgentStatusCard() {
<Bot className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Hermes Agent</h2>
</div>
{agent?.webui_url && (
{agent?.terminal_url && (
<a
href={resolveExternalUrl(agent.webui_url)}
href={resolveExternalUrl(agent.terminal_url)}
target="_blank"
rel="noopener"
className={cn(
"flex items-center gap-1 px-3 py-1 rounded-lg text-xs font-medium transition-all",
agent.webui_reachable
agent.terminal_reachable
? "bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-primary/20"
: "border border-border text-muted-foreground"
)}
>
<ExternalLink className="h-3 w-3" /> AnythingLLM öffnen
<ExternalLink className="h-3 w-3" /> Terminal öffnen
</a>
)}
</div>
@@ -60,10 +60,10 @@ export function AgentStatusCard() {
</div>
</div>
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
<div className="text-[10px] text-muted-foreground uppercase font-semibold">AnythingLLM</div>
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Terminal</div>
<div className="flex items-center gap-2 mt-1">
<span className={cn("h-2 w-2 rounded-full", agent.webui_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.webui_reachable ? "Online" : "Offline"}</span>
<span className={cn("h-2 w-2 rounded-full", agent.terminal_reachable ? "bg-emerald-500 animate-pulse" : "bg-amber-500")} />
<span className="text-xs font-medium">{agent.terminal_reachable ? "Online" : "Offline"}</span>
</div>
</div>
<div
+5 -5
View File
@@ -227,15 +227,15 @@ export interface SystemStatus {
export interface ServicesResp {
services: { name: string; url: string; ok: boolean }[]
links: { engine_ui: string; gateway: string; hermes_webui: string }
links: { engine_ui: string; gateway: string; hermes_terminal: string }
}
export interface ComponentUpdate {
key: string // "hermes_agent" | "anythingllm"
key: string // z.B. "hermes_agent"
name: string
current: string | null
latest: string | null
update: boolean | null // null = unbestimmbar (z.B. AnythingLLM-Version remote nicht abfragbar)
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
reachable: boolean | null
}
@@ -278,9 +278,9 @@ export interface DedupeResult {
export interface AgentStatus {
gateway_url: string
webui_url: string
terminal_url: string
gateway_reachable: boolean
webui_reachable: boolean
terminal_reachable: boolean
home_exists: boolean
brain_model?: string
has_config: boolean
+4 -2
View File
@@ -4,11 +4,12 @@ import {
Brain,
Plug,
Bot,
TerminalSquare,
HelpCircle,
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "models" | "memory" | "connect" | "agent" | "guide"
export type ViewId = "dashboard" | "models" | "memory" | "connect" | "agent" | "terminal" | "guide"
export interface NavItem {
id: ViewId
@@ -23,7 +24,8 @@ export const NAV: NavItem[] = [
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
{ id: "agent", label: "Hermes", hint: "Agent-Status & AnythingLLM öffnen", icon: Bot },
{ id: "agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot },
{ id: "terminal", label: "Terminal", hint: "Interaktives Hermes-Agent-Terminal", icon: TerminalSquare },
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
]
+17 -17
View File
@@ -135,20 +135,20 @@ export function AgentView() {
</p>
</div>
{s?.webui_url && (
{s?.terminal_url && (
<a
href={resolveExternalUrl(s.webui_url)}
href={resolveExternalUrl(s.terminal_url)}
target="_blank"
rel="noopener"
className={cn(
"flex h-9 px-4 items-center gap-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer shadow-md shrink-0 self-start",
s.webui_reachable
s.terminal_reachable
? "bg-primary text-primary-foreground hover:opacity-90 shadow-primary/10"
: "border border-border/60 text-muted-foreground bg-background/20"
)}
>
<ExternalLink className="h-4 w-4" />
<span>AnythingLLM öffnen</span>
<span>Terminal öffnen</span>
</a>
)}
</div>
@@ -170,9 +170,9 @@ export function AgentView() {
icon={Bot}
/>
<Tile
label="AnythingLLM"
ok={s.webui_reachable}
detail="Chat-UI (AnythingLLM)"
label="Terminal"
ok={s.terminal_reachable}
detail="Web-Terminal (hermes chat)"
icon={Activity}
/>
<Tile
@@ -213,9 +213,9 @@ export function AgentView() {
</linearGradient>
</defs>
{/* Connection: WebUI to Gateway */}
{/* Connection: Terminal to Gateway */}
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="url(#cyan-to-teal)" strokeWidth="1.5" fill="none" />
{(hoveredNode === "webui" || s.webui_reachable) && (
{(hoveredNode === "terminal" || s.terminal_reachable) && (
<path d={getCurvePath(w * 0.15, h * 0.5, w * 0.5, h * 0.5)} stroke="#06b6d4" strokeWidth="2.5" fill="none" className="svg-flow-path" />
)}
@@ -236,14 +236,14 @@ export function AgentView() {
<div
className="absolute cursor-pointer select-none z-10 w-36 h-9 -translate-y-1/2 -translate-x-1/2 rounded-xl border border-border/60 bg-card/75 backdrop-blur-md flex items-center justify-center gap-1.5 px-2.5 hover:border-primary transition-all text-[10px] font-semibold text-foreground shadow shadow-black/20"
style={{ left: "15%", top: "50%" }}
onMouseEnter={() => setHoveredNode("webui")}
onMouseEnter={() => setHoveredNode("terminal")}
onMouseLeave={() => setHoveredNode(null)}
onClick={() => s.webui_reachable && window.open(resolveExternalUrl(s.webui_url), "_blank")}
title={s.webui_reachable ? "Klicken um AnythingLLM zu öffnen" : "AnythingLLM offline"}
onClick={() => s.terminal_reachable && window.open(resolveExternalUrl(s.terminal_url), "_blank")}
title={s.terminal_reachable ? "Klicken um das Hermes-Terminal zu öffnen" : "Terminal offline"}
>
<Activity className={cn("h-3.5 w-3.5", s.webui_reachable ? "text-emerald-400" : "text-amber-500")} />
<span>AnythingLLM</span>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.webui_reachable ? "bg-emerald-500" : "bg-amber-500")} />
<Activity className={cn("h-3.5 w-3.5", s.terminal_reachable ? "text-emerald-400" : "text-amber-500")} />
<span>Terminal</span>
<span className={cn("w-1.5 h-1.5 rounded-full animate-pulse ml-auto", s.terminal_reachable ? "bg-emerald-500" : "bg-amber-500")} />
</div>
{/* COLUMN 2: Gateway */}
@@ -360,7 +360,7 @@ export function AgentView() {
PC Executor verbunden
</div>
<p className="text-[10px]">
Hermes kann jetzt per Telegram oder WebUI Befehle auf TobisNicerPC ausführen. Nutze <code className="text-foreground">pc_shell</code>, <code className="text-foreground">pc_screenshot</code> oder <code className="text-foreground">pc_open</code>.
Hermes kann jetzt per Telegram oder Terminal Befehle auf TobisNicerPC ausführen. Nutze <code className="text-foreground">pc_shell</code>, <code className="text-foreground">pc_screenshot</code> oder <code className="text-foreground">pc_open</code>.
</p>
</div>
) : (
@@ -395,7 +395,7 @@ export function AgentView() {
<div className="p-3.5 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1">
<div className="text-muted-foreground/60"># Dienste manuell auf der Box prüfen:</div>
<div className="text-primary">systemctl --user status hermes-gateway</div>
<div className="text-primary">systemctl --user status hermes-webui</div>
<div className="text-primary">systemctl --user status hermes-terminal</div>
</div>
<p>
+3 -3
View File
@@ -328,8 +328,8 @@ export function GuideView() {
<ul className="text-[11px] text-muted-foreground space-y-2 list-disc pl-4 leading-relaxed">
<li><strong>Keine Verbindung?</strong> Überprüfe im Live-Verbindungsprüfer oben, ob die Box-IP erreichbar ist. Stelle sicher, dass dein lokaler PC im selben WLAN/LAN-Netzwerk wie die Box eingeloggt ist.</li>
<li><strong>Modell antwortet nicht?</strong> Schaue unter <strong>Diagnose</strong>, ob der Dienst `llama-swap` aktiv (grün) is. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte in AnythingLLM einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
<li><strong>Modell antwortet nicht?</strong> Schaue in der <strong>Zentrale</strong> unter <strong>Dienste</strong>, ob `llama-swap` aktiv (grün) ist. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte im <strong>Terminal</strong> eine frische Session (neuer `hermes chat`). Ältere Sessions sammeln Kontext-Müll an.</li>
</ul>
</div>
</>
@@ -474,7 +474,7 @@ description: Drive development with strict TDD practices
<ul className="list-disc pl-4 space-y-1">
<li><strong>Chat-Kontext sauber halten:</strong> Starte regelmäßig neue Chats. Zu große Historien verlangsamen den Agenten und führen zu Halluzinationen.</li>
<li><strong>Gehirn festlegen:</strong> Konfiguriere im Gateway die Modell-Rolle <code>brain</code> für Hermes, damit er automatisch das passende Modell per Llama Swap lädt.</li>
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (AnythingLLM-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (`~/.hermes/SOUL.md` / Config), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
</ul>
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
import { TerminalSquare, ExternalLink, AlertTriangle } from "lucide-react"
import { useAgentStatus } from "@/lib/queries"
import { resolveExternalUrl } from "@/lib/utils"
export function TerminalView() {
const { data: agent } = useAgentStatus(5_000)
const url = agent?.terminal_url ? resolveExternalUrl(agent.terminal_url) : undefined
const ok = agent?.terminal_reachable
return (
<div className="flex h-full flex-col gap-4">
<div className="flex flex-wrap items-end justify-between gap-3">
<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">
Hermes Terminal
</h1>
<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 &amp; PC-Steuerung mehr als Chatten".
</p>
</div>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
<span className={`h-2 w-2 rounded-full ${ok ? "bg-emerald-500 animate-pulse" : "bg-amber-500"}`} />
{ok ? "online" : "offline"}
</span>
{url && (
<a href={url} target="_blank" rel="noopener"
className="flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground">
<ExternalLink className="h-3.5 w-3.5" /> In neuem Tab
</a>
)}
</div>
</div>
{url ? (
<div className="relative min-h-[68vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25">
{ok === false && (
<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" />
<div className="text-sm font-semibold text-amber-300">Terminal nicht erreichbar</div>
<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.
Auf der Box: <code className="font-mono">systemctl --user restart hermes-terminal</code>.
</p>
</div>
)}
<iframe src={url} title="Hermes Terminal" className="h-full w-full border-0" style={{ minHeight: "68vh" }} />
</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">
<TerminalSquare className="mr-2 h-4 w-4" /> Lade Terminal
</div>
)}
</div>
)
}