refactor(terminal): 终端逻辑回归 agentcli,内核不再编码终端语义

按用户指正调整架构:终端是 agentcli 的职责,内核不该有终端逻辑;插件与
agentcli 的联动应走事件总线,而不是内核内部组件。

## 删掉内核里的终端语义

`internal/agent/core/terminal_registry.go`(280 行)整体删除。它做了两件不该
内核做的事:定义「什么是终端会话」、从 EventToolCall 的 Go map 文本里解析
terminal id / command 并归并会话与命令历史。连带移除:

- Agent 的 terminalReg 字段、构造、subscribeTerminalRegistry 订阅
- StatusAPI 上的 ListTerminals / CmdHistory
- sdk.TerminalAPI + PluginSDK.Terminal() + Registry.SetTerminalAPI 及接线
- 对应测试(迁到插件侧视图包)

保留 `sdk.TerminalStatus` / `CmdExecStatus`:它们只是线上 JSON 格式,WebUI/CLI
返回给前端时口径一致。

## 插件侧订阅事件总线自攒视图

新增 `internal/pluginview`(TerminalView):把原先内核那份归并逻辑原样搬过来,
由插件自己持有。数据源仍是两条总线事件:

    EventTerminalOutput(agentcli 发)→ 会话输出 + 生命周期
    EventToolCall      (内核发)    → cmd_run 历史、terminal_create/close 归并

WebUI 在 NewHandler 订阅、Close 退订;CLI 在 Start 订阅、Stop 退订。
两边共用同一实现,避免历史上「两份推导各踩一次 Go map 文本」的坑。
视图不是权威:真值在 agentcli,插件晚于会话启动会缺早期会话(注释写明)。

## agentcli 保持内置

它现在仍需要 `Publish`(进程内事件总线写入)——外部插件没有上行事件通道
(`EventPublish` 在 withheldCapabilities 里,proc 只有 events.subscribe)。
按判定标准留内置;将来要外置,须先开一条受控的插件→内核上行事件通道,
本次刻意不开(用户决定:先保持内置)。

## 文档

中英 ARCHITECTURE 新增「终端归属」小节:说明职责边界、两条事件、视图非权威、
以及 agentcli 为何仍内置;内置判定表里 cli/webui 去掉 TerminalAPI 一项。

## 验收(本机实例实测)

- 内核编译通过,删除后 `go build ./... && go vet ./...` 干净,`go test ./internal/...` 全绿
- 实例重启后:让 agent 开终端跑 `echo term-view-test` →
  WebUI `GET /api/v1/terminals` 返回 term_1(output 正确、running=false)
  CLI `/terminals`(unix socket)返回同一份;
  两处都是从事件总线自攒,内核侧无终端代码参与
- cmd 历史分支由迁过来的单测覆盖(Go map 文本解析、生命周期、上限裁剪)
This commit is contained in:
root
2026-10-20 09:33:08 +08:00
parent b9097986c8
commit 3119d48625
17 changed files with 154 additions and 149 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/JianFeeeee/HomeAgent/internal/config"
"github.com/JianFeeeee/HomeAgent/internal/plugin"
"github.com/JianFeeeee/HomeAgent/internal/pluginview"
sdk "github.com/JianFeeeee/HomeAgent/internal/sdk"
)
@ -44,11 +45,13 @@ func init() {
}
type Plugin struct {
name string
socket string
ln net.Listener
mu sync.Mutex
wg sync.WaitGroup
name string
socket string
ln net.Listener
mu sync.Mutex
wg sync.WaitGroup
termView *pluginview.TerminalView
termUnsubs []func()
}
func New(name, socketPath string) *Plugin {
@ -62,9 +65,14 @@ func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
// 终端与命令历史的权威视图在内核(「内核开,两个插件接」),
// /terminals 与 /cmd/history 直接从 s.Status().GetKernelStatus() 读,
// 无需在此订阅事件自攒。
// 终端状态归 agentcli,内核不再维护终端语义(2026-10-20 调整)。
// CLI 与 WebUI 用同一份 internal/pluginview 实现订阅事件总线,
// /terminals 与 /cmd/history 读自己攒的视图。
p.termView = pluginview.NewTerminalView()
p.termUnsubs = append(p.termUnsubs,
s.Subscribe(sdk.EventTerminalOutput, func(evt *sdk.Event) { p.termView.OnTerminalOutput(evt.Payload) }),
s.Subscribe(sdk.EventToolCall, func(evt *sdk.Event) { p.termView.OnToolCall(evt.Payload) }),
)
// inputch 先登记:本插件既用 "cli" 作输出目标,也用它注入输入(终端行)。
// 输入侧必须显式登记,否则"把 inputch 划给驻留子"会找不到它。
@ -1077,37 +1085,29 @@ func (p *Plugin) cmdAgents(conn net.Conn, s *sdk.PluginSDK) {
// ======== /terminals /cmd/history /terminal ========
// 终端与命令历史的权威视图在内核(「内核开,两个插件接」)。
//
// 内核订自己的事件总线归并 EventToolCall(terminal_create/close、cmd_run)
// 与 EventTerminalOutput(agentcli 生命周期 + 输出),产出唯一真相挂在
// TerminalAPI(见 internal/agent/core/terminal_registry.go)。
// WebUI 与 CLI 都从 s.Status().GetKernelStatus() 读同一份,不再各自订阅推导——
// 之前两份推导还各自踩过同一个坑:工具 result 是 Go map 文本,断言成
// map[string]interface{} 永远失败 → /terminals 空空如也。
// 终端视图:订阅事件总线攒的(与 WebUI 共用 internal/pluginview 实现)。
// 状态真值在 agentcli;内核不参与终端语义。
// cmdTerminals 读内核权威快照,与 WebUI 的 GET /api/v1/terminals 同源。
// cmdTerminals 返回终端会话快照,与 WebUI 的 GET /api/v1/terminals 同口径。
func (p *Plugin) cmdTerminals(conn net.Conn, s *sdk.PluginSDK) {
t := s.Terminal()
if t == nil {
if p.termView == nil {
writeJSONContent(conn, map[string]interface{}{"terminals": []interface{}{}})
return
}
terms := t.ListTerminals()
terms, _ := p.termView.Snapshot()
if terms == nil {
terms = []sdk.TerminalStatus{}
}
writeJSONContent(conn, map[string]interface{}{"terminals": terms})
}
// cmdCmdHistory 读内核权威快照,与 WebUI 的 GET /api/v1/cmd/history 同源。
// cmdCmdHistory 返回命令历史快照,与 WebUI 的 GET /api/v1/cmd/history 同口径。
func (p *Plugin) cmdCmdHistory(conn net.Conn, s *sdk.PluginSDK) {
t := s.Terminal()
if t == nil {
if p.termView == nil {
writeJSONContent(conn, map[string]interface{}{"history": []interface{}{}})
return
}
cmds := t.CmdHistory()
_, cmds := p.termView.Snapshot()
if cmds == nil {
cmds = []sdk.CmdExecStatus{}
}
@ -1191,6 +1191,13 @@ func writeLine(conn net.Conn, v interface{}) {
}
func (p *Plugin) Stop() error {
for _, unsub := range p.termUnsubs {
if unsub != nil {
unsub()
}
}
p.termUnsubs = nil
p.mu.Lock()
defer p.mu.Unlock()
if p.ln != nil {