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:
JianFeeeee
2026-09-17 18:56:15 +08:00
parent f671632915
commit 6b4f470b62
14 changed files with 595 additions and 330 deletions

View File

@ -167,6 +167,7 @@ type PluginSDK struct {
config ConfigAPI
tool ToolAPI
indexer IndexerAPI
terminal TerminalAPI
selftestMu sync.Mutex
selftest *VirtualInstance
@ -314,6 +315,7 @@ type SDKConfig struct {
Config ConfigAPI
Tool ToolAPI
Indexer IndexerAPI
Terminal TerminalAPI
}
func New(name string, cfg SDKConfig) *PluginSDK {
@ -353,6 +355,7 @@ func New(name string, cfg SDKConfig) *PluginSDK {
config: cfg.Config,
tool: cfg.Tool,
indexer: cfg.Indexer,
terminal: cfg.Terminal,
}
}
@ -399,6 +402,7 @@ func (s *PluginSDK) Tracker() TrackerAPI { return s.tracker }
func (s *PluginSDK) Config() ConfigAPI { return s.config }
func (s *PluginSDK) Tool() ToolAPI { return s.tool }
func (s *PluginSDK) Indexer() IndexerAPI { return s.indexer }
func (s *PluginSDK) Terminal() TerminalAPI { return s.terminal }
func (s *PluginSDK) InjectInput(source, channel, eventType string, payload map[string]interface{}) {
if s.iom != nil {

39
internal/sdk/terminal.go Normal file
View File

@ -0,0 +1,39 @@
package sdk
// TerminalAPI 是内核开放的终端会话与命令历史接口(「内核开,两个插件接」)。
//
// 为什么单开接口而不是塞进 KernelStatus:KernelStatus 是**全量运行态快照**,
// /api/v1/kernel 与前端总览页每 3 秒轮询一次;把每个终端最多 64KB 的输出
// 缓冲塞进去,会让每次轮询都背一份终端全屏内容。终端输出属于**按需拉取**的
// 明细,只该在 /terminals 被访问时取。
//
// 权威状态由内核维护(internal/agent/core/terminal_registry.go):内核订阅
// 自己的事件总线,归并 EventToolCall(terminal_create/close、cmd_run)与
// EventTerminalOutput(agentcli 生命周期 + 输出),产出唯一真相。
// WebUI 与 CLI 都从这里读,不再各自订阅推导。
type TerminalAPI interface {
// ListTerminals 返回终端会话快照(含输出缓冲与运行状态)。
ListTerminals() []TerminalStatus
// CmdHistory 返回命令执行历史快照(cmd_run,最近 100 条)。
CmdHistory() []CmdExecStatus
}
// TerminalStatus 与 WebUI termState / CLI cliTermState 同一 JSON 口径。
type TerminalStatus struct {
ID string `json:"id"`
Command string `json:"command"`
Running bool `json:"running"`
Output string `json:"output"`
CreatedAt string `json:"created_at"`
Uptime string `json:"uptime"`
}
// CmdExecStatus 与 WebUI CmdExec 同一 JSON 口径。
type CmdExecStatus struct {
Command string `json:"command"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
Status string `json:"status"`
Time string `json:"time"`
}