mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 13:23:03 +00:00
refactor(terminal): 内核开终端/命令历史权威视图,WebUI 与 CLI 都改接内核
按「内核开,两个插件接」重构终端与命令历史的数据归属。
背景:此前 WebUI 与 CLI 各订 EventToolCall/EventTerminalOutput 攅一份状态,
同一件事两份推导,还各自踩过同一个坑——工具 result 是 Go 的 map 文本
(map[cols:80 ... id:term_2 ...]),断言成 map[string]interface{} 永远失败,
terminal_create 的 id 回填不生效,/terminals 因此恒空(WebUI 也一样)。
实测确认:WebUI 自己的 /api/v1/terminals 与 /api/v1/cmd/history 同样是空的。
内核开(权威唯一真相):
- internal/agent/core/terminal_registry.go:TerminalRegistry 归并两类事件——
EventToolCall(terminal_create/close、cmd_run,id/command 从 args 或 Go map
文本回填)与 EventTerminalOutput(agentcli 生命周期 + 输出,含 64KB 缓冲上限、
100 条命令历史、50 个终端上限)。
- internal/sdk/terminal.go:新增 TerminalAPI(ListTerminals/CmdHistory)与
TerminalStatus/CmdExecStatus DTO。**不塞进 KernelStatus**:那是全量快照,
前端每 3 秒轮询 /kernel,背上每终端最多 64KB 输出会让轮询成本爆炸;
终端输出是按需拉取的明细,另开接口。
- Agent 订阅自己的事件总线(subscribeTerminalRegistry),且**只根 agent 建**
(驻留子共用同一总线,每个子都建会 N+1 份重复记账)。
- SDKConfig/Registry/bootstrap 接线:pluginReg.SetTerminalAPI(agent)。
生产者补全(agentcli):终端无输出时 ticker 不发事件,内核就无从知道终端
存在。新增 emitTermState,在 handleCreate/handleClose/readLoop 退出(超时/
进程结束/读取错误/stopCh)显式上报 running 状态,并给输出事件补 command 字段。
handleClose 改为接收 *sdk.PluginSDK 以便上报。
两个插件接(消费方):
- WebUI:删掉本地 termStates/cmdHistory/subscribeTerminalStream/handleToolEvent
及不再使用的 getStr;/terminals 与 /cmd/history 直接读 s.Terminal()。
- CLI:删掉上一轮刚加的 subscribeToolEvents 与 cliTermState/cliCmdExec;
/terminals 与 /cmd/history 直接读 s.Terminal()。两条路(local/remote)都通。
测试:新增 terminal_registry_test.go,锁死 Go map 文本解析(旧缺陷根因)、
生命周期、CLI 直调路径(无 EventToolCall 仅凭 output 事件建条目)、历史与
终端数量上限。全量 go test ./internal/... ./cmd/... 通过。
This commit is contained in:
@ -351,7 +351,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"required": []string{"id"},
|
||||
},
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
return p.handleClose(args)
|
||||
return p.handleClose(s, args)
|
||||
})
|
||||
|
||||
s.RegisterTool("terminal_list", sdk.ToolDef{
|
||||
@ -493,6 +493,10 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in
|
||||
p.wg.Add(1)
|
||||
go p.readLoop(session, s)
|
||||
|
||||
// 生命周期事件:终端创建即时上报(带 command),让内核权威视图与所有
|
||||
// 订阅者(WebUI/CLI)即使在该终端无输出的情况下也能知道它的存在。
|
||||
emitTermState(s, session, true)
|
||||
|
||||
log.Printf("[agentcli] created terminal %s: command=%q timeout=%v rows=%d cols=%d", id, command, timeout, rows, cols)
|
||||
|
||||
return map[string]interface{}{
|
||||
@ -681,7 +685,7 @@ func (p *Plugin) handleResize(args map[string]interface{}) (interface{}, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleClose(args map[string]interface{}) (interface{}, error) {
|
||||
func (p *Plugin) handleClose(s *sdk.PluginSDK, args map[string]interface{}) (interface{}, error) {
|
||||
id, _ := args["id"].(string)
|
||||
if id == "" {
|
||||
return map[string]interface{}{"error": "id is required"}, nil
|
||||
@ -701,6 +705,9 @@ func (p *Plugin) handleClose(args map[string]interface{}) (interface{}, error) {
|
||||
session.Close()
|
||||
log.Printf("[agentcli] closed terminal %s", id)
|
||||
|
||||
// 生命周期事件:显式上报关闭(readLoop 退出时也会发,幂等)。
|
||||
emitTermState(s, session, false)
|
||||
|
||||
return map[string]interface{}{
|
||||
"status": "closed",
|
||||
"terminal": id,
|
||||
@ -795,6 +802,25 @@ func (p *Plugin) handleList() (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// emitTermState 把终端存活状态作为 EventTerminalOutput 事件上报。
|
||||
//
|
||||
// 生命周期事件(创建/关闭/退出/超时)必须显式发:终端无输出时 ticker
|
||||
// 不会发事件,内核的终端权威视图与所有插件(WebUI/CLI)都依赖这些事件
|
||||
// 才能知道终端的存在与终止。payload 带 command 供无 ToolCall 事件的
|
||||
// 直调路径(CLI /terminal create 经 ToolAPI.ExecuteTool)回填命令名。
|
||||
// 幂等:重复发同一 running 值不会产生状态跳变。
|
||||
func emitTermState(s *sdk.PluginSDK, t *TerminalSession, running bool) {
|
||||
if s == nil || t == nil {
|
||||
return
|
||||
}
|
||||
s.Publish(&sdk.Event{
|
||||
Type: sdk.EventTerminalOutput,
|
||||
Source: "agentcli",
|
||||
Payload: map[string]interface{}{"terminal_id": t.id, "command": t.command, "running": running},
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) readLoop(t *TerminalSession, s *sdk.PluginSDK) {
|
||||
defer p.wg.Done()
|
||||
defer close(t.done)
|
||||
@ -812,6 +838,9 @@ func (p *Plugin) readLoop(t *TerminalSession, s *sdk.PluginSDK) {
|
||||
flushTicker := time.NewTicker(200 * time.Millisecond)
|
||||
defer flushTicker.Stop()
|
||||
|
||||
// 退出时确保内核权威视图标记该终端为已停止(readLoop 的所有 return 点)。
|
||||
defer emitTermState(s, t, false)
|
||||
|
||||
// 立即发送首次"终端已启动"通知,让 agent 感知存在。
|
||||
// 用 NoMemory:这是状态提示,不是对话内容。不关掉的话每开一个终端都会
|
||||
// 在记忆里留下一条"[终端 X 已启动]",把真实内容挤掉。
|
||||
@ -888,7 +917,7 @@ func (p *Plugin) readLoop(t *TerminalSession, s *sdk.PluginSDK) {
|
||||
s.Publish(&sdk.Event{
|
||||
Type: sdk.EventTerminalOutput,
|
||||
Source: "agentcli",
|
||||
Payload: map[string]interface{}{"terminal_id": t.id, "output": streamData, "running": terminalRunning(t)},
|
||||
Payload: map[string]interface{}{"terminal_id": t.id, "command": t.command, "output": streamData, "running": terminalRunning(t)},
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user