mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
Expose the LLM token-level streaming deltas (EventReasoningDelta /
EventContentDelta) to every client channel and add user-initiated
interrupt (cancel generation / send interrupt message) to all three
frontends, preserving the existing interrupt-injection semantics.
SDK/events:
- EventReasoningDelta, EventContentDelta constants exported in the
public/internal SDK event alias tables.
CLI plugin:
- handleChat subscribes to both delta events and forwards
reasoning_delta / content_delta JSON frames (channel-filtered);
aggregated reasoning/tool_call/response frames still fire as before.
- New /stop (alias /interrupt) builtin injects an interrupt via
InjectInterrupt(cliSource, cliChannel) - matches interceptLoop
semantics: cancels an active stream and re-injects the message as
a [中断消息] for a restarted turn; with no active LLM it behaves
as a plain input.
Waiter client (line mode + TUI):
- streamRender accumulates delta chunks and redraws the current line;
a reset frame (stream abandoned, e.g. user interrupt) flushes the
partial buffer so the next turn does not concatenate onto stale
content. Aggregated frames terminate the delta line and render the
final text (old servers without deltas behave exactly as before).
- TUI merges content_delta into the in-flight agent message and seals
it (final flag) on response/tool_call/error so subsequent deltas
never append to a finished message.
WebUI:
- SSE handler subscribes to the two delta events but does NOT record
them into the replay ring - reconnection replays only aggregated
events (the final truth), avoiding duplicate delta accumulation.
- POST /api/v1/chat/interrupt calls InjectInterrupt(webui, webui)
with optional message; fronted by a Stop button shown only while
a generation is in flight.
dashboard.html / GUI app.js:
- Stop button next to Send (hidden until chatLoading); interruptChat
POSTs /chat/interrupt. Delta listeners append incrementally;
agent_output (aggregated) now replaces (not appends) the in-flight
content and marks _final; reset frames finalize the partial message.
process.go:
- chatStreamWithFallback preserves the context.Canceled/
DeadlineExceeded contract: a user interrupt returns the canceled
error (never a partial-content success) so the existing continue
branch restarts the turn with the [中断消息]. A reset
EventContentDelta is published so connected clients drop stale
partial renderings before the new turn begins.
Verified: /stop 'msg' via waiter triggers 'interrupt from cli/cli' in
interceptLoop; unit TestChatStreamCancelPreservesInterrupt confirms the
canceled error propagates instead of being swallowed.
250 lines
6.2 KiB
Go
250 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
func handleBuiltin(cmd string, cfg *Config, state *State, reconnect func(), out io.Writer) bool {
|
|
switch {
|
|
case cmd == "/help":
|
|
fmt.Fprintln(out, `Built-in commands:
|
|
/help show this help
|
|
/stop [msg] stop generation / send interrupt (alias /interrupt)
|
|
/exit, /quit exit waiter
|
|
/clear clear screen
|
|
/reconnect force reconnection
|
|
/connect <path> switch to a different unix socket
|
|
/remote <url> switch to remote HTTP mode
|
|
/local switch back to local socket mode
|
|
/conn list list saved connections
|
|
/conn save <name> save current connection as <name>
|
|
/conn use <name> switch to saved connection
|
|
/conn del <name> delete saved connection
|
|
|
|
Server commands (sent to agent):
|
|
/status system status
|
|
/kernel kernel status
|
|
/settings [prefix] list settings
|
|
/settings set <k> <v> set a setting
|
|
/plugin list list installed plugins
|
|
/plugin install <url> install plugin
|
|
/plugin remove <name> remove plugin
|
|
/plugin info <name> plugin details
|
|
/memory query <text> query graph memory
|
|
/knowledge list knowledge base
|
|
/knowledge delete <name> delete knowledge item
|
|
/agents list agents
|
|
/chat <text> send to agent
|
|
|
|
Any other text is sent to the agent directly.`)
|
|
return true
|
|
|
|
case cmd == "/exit" || cmd == "/quit":
|
|
return true
|
|
|
|
case cmd == "/clear":
|
|
fmt.Fprint(out, "\033[H\033[2J")
|
|
return true
|
|
|
|
case cmd == "/reconnect":
|
|
printlnC(colorYellow, "reconnecting...")
|
|
reconnect()
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/connect "):
|
|
cfg.Socket = strings.TrimSpace(cmd[9:])
|
|
cfg.Remote = ""
|
|
reconnect()
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/remote "):
|
|
cfg.Remote = strings.TrimSpace(cmd[8:])
|
|
cfg.Socket = ""
|
|
reconnect()
|
|
return true
|
|
|
|
case cmd == "/local":
|
|
cfg.Remote = ""
|
|
cfg.Socket = discoverSocket("")
|
|
reconnect()
|
|
return true
|
|
|
|
case cmd == "/conn list":
|
|
if len(cfg.Connections) == 0 {
|
|
fmt.Fprintln(out, "no saved connections")
|
|
}
|
|
for _, c := range cfg.Connections {
|
|
mark := " "
|
|
if c.Name == cfg.Default {
|
|
mark = "*"
|
|
}
|
|
addr := c.Remote
|
|
if addr == "" {
|
|
addr = c.Socket
|
|
}
|
|
fmt.Fprintf(out, " %s %-15s %s\n", mark, c.Name, addr)
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/conn save "):
|
|
name := strings.TrimSpace(cmd[11:])
|
|
cfg.SaveConnection(name)
|
|
fmt.Fprintf(out, "connection saved as '%s' (default)\n", name)
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/conn use "):
|
|
name := strings.TrimSpace(cmd[10:])
|
|
if cfg.SwitchConnection(name) {
|
|
fmt.Fprintf(out, "switched to '%s'\n", name)
|
|
reconnect()
|
|
} else {
|
|
fmt.Fprintf(out, "connection '%s' not found\n", name)
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/conn del "):
|
|
name := strings.TrimSpace(cmd[10:])
|
|
if cfg.DeleteConnection(name) {
|
|
fmt.Fprintf(out, "connection '%s' deleted\n", name)
|
|
} else {
|
|
fmt.Fprintf(out, "connection '%s' not found\n", name)
|
|
}
|
|
return true
|
|
|
|
case cmd == "/status":
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/status", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send("/status")
|
|
}
|
|
return true
|
|
|
|
case cmd == "/kernel":
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/kernel", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send("/kernel")
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/settings set "):
|
|
parts := strings.SplitN(cmd[14:], " ", 2)
|
|
if len(parts) < 2 {
|
|
fmt.Fprintln(out, "usage: /settings set <key> <value>")
|
|
return true
|
|
}
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
body := fmt.Sprintf(`{"%s":%q}`, parts[0], parts[1])
|
|
rc.DoAPI("PUT", "/api/v1/settings", body)
|
|
fmt.Fprintln(out, "ok")
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/settings"):
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/settings", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case cmd == "/plugin list":
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/plugins", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send("/plugin list")
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/plugin install "):
|
|
url := strings.TrimSpace(cmd[16:])
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
body := fmt.Sprintf(`{"url":%q}`, url)
|
|
d, _ := rc.DoAPI("POST", "/api/v1/plugins", body)
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/plugin remove "):
|
|
name := strings.TrimSpace(cmd[15:])
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("DELETE", "/api/v1/plugins/"+name, "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/plugin info "):
|
|
name := strings.TrimSpace(cmd[13:])
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/plugins/"+name, "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/memory query "):
|
|
q := strings.TrimSpace(cmd[14:])
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/memory?query="+q, "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case strings.HasPrefix(cmd, "/knowledge delete "):
|
|
name := strings.TrimSpace(cmd[18:])
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("DELETE", "/api/v1/knowledge/"+name, "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send(cmd[1:])
|
|
}
|
|
return true
|
|
|
|
case cmd == "/knowledge":
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/knowledge", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send("/knowledge")
|
|
}
|
|
return true
|
|
|
|
case cmd == "/agents":
|
|
if rc := state.RemoteConn(); rc != nil {
|
|
d, _ := rc.DoAPI("GET", "/api/v1/agents", "")
|
|
printJSON(out, d)
|
|
} else {
|
|
state.Send("/agents")
|
|
}
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func printJSON(out io.Writer, d map[string]interface{}) {
|
|
if d == nil {
|
|
fmt.Fprintln(out, "(no data)")
|
|
return
|
|
}
|
|
b, _ := json.MarshalIndent(d, "", " ")
|
|
fmt.Fprintln(out, string(b))
|
|
}
|