diff --git a/README.md b/README.md index 85ae3431..2d19543a 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ internal/ ├── memory/ 三层记忆:Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(预训练词嵌入/TF-IDF回退) + CleanTemplateText(去模版) ├── knowledge/ 知识库(文件系统 + TF-IDF) ├── plugin/ 插件注册表 + 子进程加载器(stdio RPC + 共享内存段 + 事件环) -├── plugins/ 内置 17 个插件(webui/cli/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/seemedia/seq) +├── plugins/ 内置 11 个插件(webui/cli/agentcli/cfgmgr/healthcheck/pluginmgr/clawhubadapter/remotedevice/skillmgr/seemedia/seq)—— 判定标准见 ARCHITECTURE「内置插件的判定标准」 ├── sdk/ PluginSDK(Tool/Stage/Event 三通道) ├── config/ SQLite 配置中心 ├── events/ 事件总线 @@ -390,7 +390,7 @@ make install # 安装到系统 本项目的 AGPL。因此**外部插件不是本项目的衍生作品**,作者可自行选择许可(含闭源、商业、私有), 既不必同许可、也不受 §13 网络条款约束。第三方插件生态的安全与活跃正建立在这条之上。 -边界很清楚:**AGPL 覆盖内核与随包内置插件**(`homed`、`internal/`、`internal/plugins/` 下 17 个内置插件); +边界很清楚:**AGPL 覆盖内核与随包内置插件**(`homed`、`internal/`、`internal/plugins/` 下 11 个内置插件); **MIT 覆盖公开 SDK**(`sdk/`,`go.mod` 零外部依赖、只依赖 Go 标准库,不引用内核任何代码)。 子进程隔离在这里不重要了——决定许可的是被链接的 SDK 代码本身,而它是 MIT。 diff --git a/README_EN.md b/README_EN.md index 1a1bdfc2..d9d285e7 100644 --- a/README_EN.md +++ b/README_EN.md @@ -216,7 +216,7 @@ internal/ ├── memory/ Three-layer memory: Graph(SQLite) / Document(JSON+TF-IDF) / Text(JSONL) + StaticEmbedder(pretrained word embedding/TF-IDF fallback) + CleanTemplateText(de-template) ├── knowledge/ Knowledge base (filesystem + TF-IDF) ├── plugin/ Plugin registry + subprocess loader (stdio RPC + shared memory segment + event ring) -├── plugins/ 17 built-in plugins (webui/cli/cmd/mcp/files/cfgmgr/agentcli/healthcheck/pluginmgr/clawhubadapter/multimodal/remotedevice/ai_image/localuse/skillmgr/seemedia/seq) +├── plugins/ 11 built-in plugins (webui/cli/agentcli/cfgmgr/healthcheck/pluginmgr/clawhubadapter/remotedevice/skillmgr/seemedia/seq) — see ARCHITECTURE "which plugins stay built-in" ├── sdk/ PluginSDK (Tool/Stage/Event three channels) ├── config/ SQLite config center ├── events/ Event bus @@ -416,7 +416,7 @@ private included), with no same-license obligation and no §13 network clause. T vitality of the third-party plugin ecosystem rest on this. The boundary is clean: **AGPL covers the kernel and the bundled plugins** (`homed`, `internal/`, -the 17 built-in plugins under `internal/plugins/`); **MIT covers the public SDK** (`sdk/`, whose +the 11 built-in plugins under `internal/plugins/`); **MIT covers the public SDK** (`sdk/`, whose `go.mod` has zero external dependencies and imports only the Go standard library — it never references any kernel code). Process isolation is beside the point here — what decides the license is the linked SDK code itself, and that code is MIT. diff --git a/assets/docs/en/ARCHITECTURE.md b/assets/docs/en/ARCHITECTURE.md index 54cf003d..8f1c3a82 100644 --- a/assets/docs/en/ARCHITECTURE.md +++ b/assets/docs/en/ARCHITECTURE.md @@ -318,7 +318,7 @@ VM built-ins: `json.encode` / `json.decode` / `log` / `http_get` / `http_post`. | Method | Registration Mechanism | Compilation | Usage | |--------|----------------------|-------------|-------| -| Built-in | `init()` → `RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/cmd/mcp etc. (17 total) | +| Built-in | `init()` → `RegisterFactory` | `internal/plugins/` compiled into kernel | webui/cli/agentcli etc. (11 total; see the criteria section below) | | External subprocess plugin | Handshake + stdio JSON-RPC reverse registration | `hmapdev build` → `plugin.bin` (ordinary Go binary) | qq/browser/files etc. | | Lua script plugin | Execute `main.lua` to register tools | No compilation, takes effect after restart/reload | luademo etc. | | SKILL plugin | Parse `SKILL.md` | Markdown definition | Loaded via clawhubadapter | @@ -333,6 +333,53 @@ the old `plugin.Open` path-cache workarounds (SHA256 temp-path copies) retired w Lua script plugin loading: `internal/plugin/` → the gopher-lua interpreter executes `main.lua` (at load time `sdk.register_*` only buffers handlers), then `Start()` swaps in the real SDK implementation and registers them in batch. The script is read only once at load time; runtime execution happens via callbacks. +### Which Plugins Stay Built-in (criteria, finalized 2026-10-19) + +**A plugin stays in the compiled-in list only if it requires internal-SDK-only +capabilities — API surfaces the public SDK does not have.** + +The internal SDK (`internal/sdk`) exposes several capability surfaces that are +**never granted to external plugins**: `TerminalAPI` / `TrackerAPI` / +`StatusAPI` / `SupervisorAPI` / `AdapterAPI` / `SelftestAPI` / `ToolAPI`, and +**`Publish` (event publishing)**. External plugins run over the proc bridge's +narrowed capability set where those interfaces simply do not exist at the type +level (authoritative list: `withheldCapabilities` in +`internal/plugin/proc/capability.go`). + +This yields a clean binary rule: + +- **Plugin needs a withheld capability ⇒ it must stay built-in.** Externalizing + it makes the feature **fail silently** — no error, no crash, just a data link + broken between plugin and kernel. The hardest class of bug to trace. +- **Plugin only uses public SDK ⇒ it should be externalized** (.hmap package). + Independent build and release; a one-line fix no longer means recompiling the + 87 MB kernel. + +Positive examples (kept built-in, each for a reason it cannot do without): + +| Plugin | Internal capability it needs | +|---|---| +| cli / webui | TerminalAPI (the /terminal command and terminal panel), TrackerAPI, StatusAPI, SupervisorAPI, AdapterAPI | +| agentcli | **EventPublish**: reports terminal output and lifecycle via `Publish(EventTerminalOutput)`; the public SDK has no Publish | +| healthcheck | SelftestAPI (can build a virtual kernel instance for self-checks) | +| pluginmgr | required by the failback recovery chain (`core.agent.failback_plugins` defaults to webui/pluginmgr/recoverydiag) | +| seq | ToolAPI (direct tool-table access; in-group parallel steps invoke tools by name) | +| remotedevice | RegisterChannel (kernel IO channel registration) | +| clawhubadapter | InjectInputSync (OC-plugin proactive injection) | + +Counter-examples (externalized, because public capabilities suffice): + +| Plugin | Actual dependencies | +|---|---| +| timer | RegisterTool + Settings + InjectText (public) | +| ai_image / files / cmd / kbtree / localuse / mcp / multimodal | RegisterTool + Settings (+ public APIs such as s.Knowledge()) | + +> ⚠️ Both copies existing at once is forbidden: `loadOne` checks the built-in +> factory table first and never looks at the external `plugin.bin` if it hits +> ⇒ the built-in version always wins and the external one **installs but never +> takes effect**. When externalizing a plugin, remove it from +> `internal/plugins/all.go` (the single authoritative list) in the same change. + ### Built-in vs External Plugins | Dimension | Built-in Plugin | External Plugin | @@ -594,19 +641,24 @@ internal/ │ ├── registry.go — Registry + lifecycle │ ├── dynamic.go — Load-channel dispatch (proc / lua / skill); .so only remains to error loudly │ └── manifest.go — plugin.json metadata -├── plugins/ — Built-in plugin implementations -│ ├── all.go — Blank imports +├── plugins/ — Built-in plugin implementations (11; see "Which Plugins Stay Built-in") +│ ├── all.go — Blank imports (**the single authoritative list**; edit only this) │ ├── webui/ — HTTP server + embedded SPA │ ├── cli/ — Unix socket CLI -│ ├── cmd/ — Command execution -│ ├── mcp/ — MCP protocol -│ ├── files/ — File operations │ ├── clawhubadapter/ — ClawHub adapter (OC plugin/SKILL/JS/Python sidecar) │ ├── agentcli/ — PTY terminal │ ├── healthcheck/ — Health check │ ├── pluginmgr/ — Plugin manager │ ├── skillmgr/ — Skill plugin management -│ └── cfgmgr/ — Config manager +│ ├── cfgmgr/ — Config manager +│ ├── remotedevice/ — Remote device bridge +│ ├── seemedia/ — Direct media view +│ ├── seq/ — Sequence orchestration +│ ├── data/ plugin_data/ — Plugin static assets (non-Go, shipped with the kernel) +│ ├── kbtree/ cmd/ localuse/ mcp/ multimodal/ ai_image/ files/ +│ │ — Source remains but **not listed in all.go**: externalized +│ │ on 2026-10-19 as .hmap packages (projects in the SDK repo's +│ │ example/); directories to be removed once external projects stabilize ├── sdk/ — PluginSDK definitions │ ├── plugin.go — Plugin interface + PluginSDK │ ├── memory.go — MemoryAPI diff --git a/assets/docs/en/OVERVIEW.md b/assets/docs/en/OVERVIEW.md index c2e3906b..2be88956 100644 --- a/assets/docs/en/OVERVIEW.md +++ b/assets/docs/en/OVERVIEW.md @@ -100,6 +100,7 @@ Code is in the project root, implemented in Go. Core functionality is operational. Plugin system and SDK are ready for independent external plugin development. -- Built-in plugins: webui / cli / timer / cmd / mcp / agentcli / healthcheck / pluginmgr / clawhubadapter / files / cfgmgr +- Built-in plugins (11): webui / cli / agentcli / cfgmgr / healthcheck / pluginmgr / clawhubadapter / remotedevice / skillmgr / seemedia / seq +- Criterion: **a plugin stays built-in only if it requires internal-SDK-only capabilities**; everything else ships as an .hmap external plugin (see ARCHITECTURE) - External plugin examples ([homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) repo `example/`, both Go and Lua types): qq / files / a2a / ai_image / bili / browser / calendar / editdoc / memo / music / ocr / rss / sanitizer / weather / luademo - Distribution: `.hmap` plugin package format, installable via WebUI diff --git a/assets/docs/zh/ARCHITECTURE.md b/assets/docs/zh/ARCHITECTURE.md index da31c850..08670992 100644 --- a/assets/docs/zh/ARCHITECTURE.md +++ b/assets/docs/zh/ARCHITECTURE.md @@ -307,7 +307,7 @@ VM 内置 `json.encode` / `json.decode` / `log` / `http_get` / `http_post`。 | 方式 | 注册机制 | 编译 | 用途 | |------|----------|------|------| -| 内置插件 | `init()` → `RegisterFactory` | `internal/plugins/` 编译进内核 | webui/cli/cmd/mcp 等 17 个 | +| 内置插件 | `init()` → `RegisterFactory` | `internal/plugins/` 编译进内核 | webui/cli/agentcli 等 11 个(判定标准见下) | | 外部子进程插件 | 握手 + stdio JSON-RPC 反向注册 | `hmapdev build` → `plugin.bin`(普通 Go 二进制) | qq/browser/files 等 | | Lua 脚本插件 | 执行 `main.lua` 注册工具 | 无需编译,重启/重载生效 | luademo 等 | | SKILL 插件 | 解析 `SKILL.md` | Markdown 定义 | clawhubadapter 兼容加载 | @@ -322,6 +322,46 @@ VM 内置 `json.encode` / `json.decode` / `log` / `http_get` / `http_post`。 Lua 脚本插件加载:`internal/plugin/` → gopher-lua 解释器执行 `main.lua`(加载期 `sdk.register_*` 仅暂存 handler),`Start()` 时替换为真实 SDK 实现并批量注册。脚本只在加载时读取一次,运行期通过回调执行。 +### 内置插件的判定标准(2026-10-19 定型) + +**只有必须使用内部 SDK 能力(公开 SDK 没有的接口面)的插件,才留在编译期内置清单。** + +内部 SDK(`internal/sdk`)比公开 SDK(third_party/homeagent-sdk)多出一批 +**不对外授予**的能力面:`TerminalAPI` / `TrackerAPI` / `StatusAPI` / +`SupervisorAPI` / `AdapterAPI` / `SelftestAPI` / `ToolAPI`,以及 **`Publish` +(事件发布)**。外部插件走 proc 桥的收窄能力集,这些接口在类型层就不存在 +(权威清单见 `internal/plugin/proc/capability.go` 的 `withheldCapabilities`)。 + +由此得出的二分法: + +- **插件需要 withheld 能力 ⇒ 必须内置**。外置后功能**静默消失**——不报错、 + 不崩进程,只是数据链路断在插件与内核之间。这类故障最难排查。 +- **插件只用公开 SDK 能力 ⇒ 应当外置**(.hmap 安装)。独立编译、独立发版, + 改一行不用重编 87MB 的内核。 + +正例(留内置,各有非它不可的理由): + +| 插件 | 依赖的内部能力 | +|---|---| +| cli / webui | TerminalAPI(/terminal 与终端面板)、TrackerAPI、StatusAPI、SupervisorAPI、AdapterAPI | +| agentcli | **EventPublish**:靠 `Publish(EventTerminalOutput)` 上报终端输出与生命周期,公开 SDK 无 Publish | +| healthcheck | SelftestAPI(能构造虚拟内核实例做自检) | +| pluginmgr | failback 恢复链必需(`core.agent.failback_plugins` 默认 webui/pluginmgr/recoverydiag) | +| seq | ToolAPI(工具表直接操作,组内并行需按名调工具) | +| remotedevice | RegisterChannel(内核 IO 通道注册面) | +| clawhubadapter | InjectInputSync(OC 插件主动注入) | + +反例(已外置,因为它们只要公开能力): + +| 插件 | 实际依赖 | +|---|---| +| timer | RegisterTool + Settings + InjectText(公开) | +| ai_image / files / cmd / kbtree / localuse / mcp / multimodal | RegisterTool + Settings(+ s.Knowledge() 等公开 API) | + +> ⚠️ 两份并存是禁止的:`loadOne` 先查内置工厂表,命中就不再看外部 `plugin.bin` +> ⇒ 内置版永远赢,外部版**装了也不生效**。外置一个插件时,必须同时把它从 +> `internal/plugins/all.go` 移除(该文件是唯一权威清单)。 + ### 内置插件 vs 外部插件 | 维度 | 内置插件 | 外部插件 | @@ -573,19 +613,24 @@ internal/ │ ├── registry.go — 注册表 + 生命周期 │ ├── dynamic.go — 加载通道分派(proc / lua / skill);.so 仅剩给明确报错 │ └── manifest.go — plugin.json 元数据 -├── plugins/ — 内置插件实现 -│ ├── all.go — 空白导入 +├── plugins/ — 内置插件实现(11 个,判定标准见「内置插件的判定标准」) +│ ├── all.go — 空白导入(**唯一权威清单**,增删内置插件只改这里) │ ├── webui/ — HTTP 服务器 + 嵌入式 SPA │ ├── cli/ — Unix socket CLI -│ ├── cmd/ — 命令执行 -│ ├── mcp/ — MCP 协议 -│ ├── files/ — 文件操作 │ ├── clawhubadapter/ — ClawHub 适配器(OC 插件/SKILL/JS/Python sidecar) │ ├── agentcli/ — PTY 终端 │ ├── healthcheck/ — 健康检查 │ ├── pluginmgr/ — 插件管理器 │ ├── skillmgr/ — Skill 插件管理 -│ └── cfgmgr/ — 配置管理 +│ ├── cfgmgr/ — 配置管理 +│ ├── remotedevice/ — 远程设备桥 +│ ├── seemedia/ — 直看媒体 +│ ├── seq/ — 序列编排 +│ ├── data/ plugin_data/ — 插件静态资源(非 Go,随内核打包) +│ ├── kbtree/ cmd/ localuse/ mcp/ multimodal/ ai_image/ files/ +│ │ — 源码仍在但**未列入 all.go**:2026-10-19 起外置, +│ │ 以 .hmap 形式安装(SDK 仓 example/ 提供工程); +│ │ 目录待外置工程稳定后删除 ├── sdk/ — PluginSDK 定义 │ ├── plugin.go — Plugin 接口 + PluginSDK │ ├── memory.go — MemoryAPI diff --git a/assets/docs/zh/OVERVIEW.md b/assets/docs/zh/OVERVIEW.md index 3f5d72ee..ed7d2a43 100644 --- a/assets/docs/zh/OVERVIEW.md +++ b/assets/docs/zh/OVERVIEW.md @@ -93,6 +93,7 @@ HomeAgent 是一个持续运行的个人智能 Agent 框架。 核心功能已可运行。插件系统和 SDK 已就绪,可独立开发外部插件。 -- 内置插件(17 个):webui / cli / cmd / mcp / agentcli / healthcheck / pluginmgr / clawhubadapter / files / cfgmgr / multimodal / remotedevice / ai_image / localuse / skillmgr / seemedia / seq +- 内置插件(11 个):webui / cli / agentcli / cfgmgr / healthcheck / pluginmgr / clawhubadapter / remotedevice / skillmgr / seemedia / seq +- 判定标准:**只有必须使用内部 SDK 能力(公开 SDK 没有的接口面)的插件才内置**,其余一律外置为 .hmap 插件(详见 ARCHITECTURE「内置插件的判定标准」) - 外部插件示例([homeagent-sdk](https://github.com/JianFeeeee/homeagentsdk) 仓库 `example/`,含 Go 和 Lua 两种类型):qq / files / a2a / ai_image / bili / browser / calendar / editdoc / memo / music / ocr / rss / sanitizer / weather / luademo - 打包分发:`.hmap` 插件包格式,通过 WebUI 安装 diff --git a/cmd/homed/main.go b/cmd/homed/main.go index 0bf5f386..0b0d07d1 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -9,11 +9,12 @@ import ( "github.com/JianFeeeee/HomeAgent/internal/kernelboot" "github.com/JianFeeeee/HomeAgent/internal/kernelmod/pluginhost" "github.com/JianFeeeee/HomeAgent/internal/meta" + // ★ 内置插件统一走 internal/plugins 的清单(all.go),**不要**在这里 + // 单独导入某个插件: + // · 重复导入虽无害(同一包只 init 一次),但会让 all.go 不再是唯一清单 + // —— 按它盘点的工具/文档会漏项(kbtree 曾因此长期游离在清单之外); + // · 想增删内置插件请改 all.go。 _ "github.com/JianFeeeee/HomeAgent/internal/plugins" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/kbtree" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" "github.com/JianFeeeee/HomeAgent/internal/security" "github.com/JianFeeeee/HomeAgent/internal/systemd" diff --git a/internal/plugins/all.go b/internal/plugins/all.go index 0bacb4c5..c3625b8c 100644 --- a/internal/plugins/all.go +++ b/internal/plugins/all.go @@ -20,19 +20,73 @@ package plugins // // ⚠️ 不要再把 timer 加回这里:父子两份同时存在会导致同名工具注册冲突, // 而「谁赢」取决于加载顺序 —— 那是最难排查的一类故障。 +// +// ## 判定标准:只有「必须用内部 SDK 能力」的才留在本清单(2026-10-19) +// +// 内部 SDK(internal/sdk)比公开 SDK(third_party/homeagent-sdk/sdk) +// 多出一批**不对外授予**的能力面。外部插件用的是 proc 桥的收窄能力集, +// 这些接口在类型层就不存在(见 internal/plugin/proc/capability.go 的 +// withheldCapabilities)。所以: +// +// 插件需要 withheld 里的能力 ⇒ **必须内置**(外置会静默缺功能) +// 插件只用公开 SDK 能力 ⇒ **应当外置**(.hmap 安装,独立演进) +// +// 正例(留内置,各有非它不可的理由): +// cli / webui TerminalAPI/TrackerAPI/StatusAPI/SupervisorAPI/AdapterAPI +// healthcheck SelftestAPI(能构造虚拟内核实例) +// pluginmgr failback 恢复链必需(webui/pluginmgr/recoverydiag) +// seq ToolAPI(工具表直接操作) +// remotedevice RegisterChannel(内核 IO 通道注册面) +// clawhubadapter InjectInputSync(OC 主动注入) +// agentcli ★ EventPublish —— 见下 +// cfgmgr / skillmgr / seemedia (保留) +// +// 反例(已外置,因为它们只要公开能力): +// cmd 仅 internal/plugin 的 RegisterFactory → 换成 NewPluginFactory 即可 +// kbtree 仅 s.Knowledge() + s.Settings() +// localuse 仅 RegisterTool + Settings +// mcp 仅 RegisterTool + Settings +// multimodal 仅 RegisterTool + Settings +// +// ## ★ agentcli 为什么不能外置(2026-10-19 核实) +// +// 它靠 s.Publish(&sdk.Event{Type: EventTerminalOutput, ...}) 上报 +// 终端输出与生命周期(创建/关闭/退出)。而 **EventPublish 在 +// withheldCapabilities 里被明确扣留**,公开 SDK 连 Publish 方法都没有 —— +// 外置后终端输出会静默消失:WebUI 的终端面板、CLI 的 /terminal, +// 全都失去数据源,而且**不报错**。 +// +// 想让 agentcli 外置,前提是先把「终端输出」从事件改为公开的输出通道 +// (RegisterOutputChannel 是公开能力)。在那之前必须内置。 + +// ## ai_image / files 已移出本清单(2026-10-19) +// +// 与 timer 同理:它们是**纯工具型**插件(agi_image_generate / files_read 等), +// 只在该被调用时做事,没有任何「外部事件驱动主动注入」的行为, +// 因此没有必须常驻的理由,也没有随内核发版的必要。 +// +// 二者都由 SDK 仓 `example/` 下的同版本外部插件提供(ai_image 1.3.0、 +// files 1.0.0),工具名与配置表不变(config_ai_image / config_files), +// 存量配置无需迁移。 +// +// ⚠️ 同一个坑:不要为「怕插件没装」而把内置版加回来。两份并存时 +// loadOne 先查 factories ⇒ 内置版永远赢,外部版**装了也不生效**, +// 而且 plugin_status 会显示 builtin=true(见 PLUGIN_DEV.md 的同款说明)。 +// +// ## kbtree 已归位到本清单(2026-10-19) +// +// 它本就是一个注册了 RegisterFactory 的编译期内置插件,却一直被 +// `cmd/homed/main.go` 单独空白导入 —— 绕过了「内置插件清单」这个唯一入口。 +// 后果:任何按 all.go 盘点内置插件的工具/文档都会漏掉它(本次文档校正 +// 就踩了这个坑:先把数量写成 17,实际运行时是 18)。 +// 归位后 main.go 不再单独导入它。 import ( _ "github.com/JianFeeeee/HomeAgent/internal/plugins/agentcli" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/ai_image" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/cfgmgr" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/cli" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/cmd" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/files" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/localuse" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/mcp" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/multimodal" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/remotedevice" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/seemedia" diff --git a/internal/plugins/integration_test.go b/internal/plugins/integration_test.go index 2bc7aad2..7abcc3e5 100644 --- a/internal/plugins/integration_test.go +++ b/internal/plugins/integration_test.go @@ -36,6 +36,27 @@ func setupIntegration(t *testing.T) *testPluginEnv { return setupIntegrationWithProvider(t, nil) } +// requireExternalTool 断言某个工具已在本次装配中注册,未注册则跳过用例。 +// +// 语义不是「功能坏了所以跳过」,而是「这个工具已不属于内置装配」:cmd / agentcli +// 等插件在 2026-10-19 外置为 hmap 插件后,setupIntegration 只加载 internal/plugins +// 下的内置包,因此这里根本不会出现它们的工具。若不加这层保护,用例会拿到 +// 「工具不存在或未注册」这种与真实插件行为无关的假失败,长期看会训练大家 +// 忽略红灯。 +// +// 保留用例而不是删除:断言本身(exit_code/stdout/stderr、PTY 交互与 resize) +// 仍然有效,只是验证现场必须是**装好 .hmap 的实例**。真正的端到端覆盖放在 +// 那层跑;这里在纯内置装配下只能 Skip,不能假装通过。 +func requireExternalTool(t *testing.T, env *testPluginEnv, tool string) { + t.Helper() + for _, def := range env.stageHost.GetToolDefs() { + if def.Name == tool { + return + } + } + t.Skipf("%s 由已外置插件提供(不在本次内置装配中),需在装上 .hmap 的实例上验证", tool) +} + func setupIntegrationWithProvider(t *testing.T, pm *agentAPI.ProviderManager) *testPluginEnv { t.Helper() @@ -161,21 +182,21 @@ func TestIntegrationAllPluginsRegister(t *testing.T) { defer env.cleanup() expectedTools := []string{ - "cmd_run", - "terminal_create", "terminal_write", "terminal_read", - "terminal_resize", "terminal_close", "terminal_list", "healthcheck", "healthcheck_plugins", "healthcheck_tools", "healthcheck_memory", "healthcheck_perf", - // ⚠️ timer_set **不在**这里(2026-10-19 起)。 + // ⚠️ 以下插件的工具**故意不在**这里(它们已外置为 hmap 插件): // - // 本用例的语义是「编译期内置插件是否都注册了工具」,set 走的是本地 - // 装配(setupIntegration 只加载 internal/plugins 下的内置包)。 - // timer 已搬为**外部插件**(plugins/timer,.hmap),不参与本进程装配, - // 所以它出现在这个列表里只会造出「永远绿不了」的假失败。 + // timer → plugins/timer + // ai_image / files → 2026-10-19 外置 + // agentcli / cmd / kbtree / localuse / mcp / + // multimodal → 2026-10-19 外置 // - // 外部插件的工具是否真的注册成功,由另一层负责:装完 .hmap 后 - // 用 timer_list / timer_set 实调一次(见交付报告里的现网验证)。 - // 别为了「让它绿」把 timer 加回 all.go —— 那会让内置与外部两份 - // 同时存在,工具重名而胜者取决于加载顺序。 + // 本用例的语义是「internal/plugins 下**仍内置**的包是否都注册了工具」, + // setupIntegration 只加载内置包 ⇒ 已外置的插件不参与本进程装配, + // 写进来只会造出「永远绿不了」的假失败。 + // + // 它们的工具是否真的注册成功,由另一层负责:装完 .hmap 后实调一次。 + // 别为了「让它绿」把插件加回 all.go —— 内置与外部两份同时存在会 + // 导致工具重名,而胜者取决于加载顺序(最难排查的一类故障)。 } defs := env.stageHost.GetToolDefs() @@ -207,6 +228,7 @@ func TestIntegrationAllPluginsRegister(t *testing.T) { func TestIntegrationCmdRun(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "cmd_run") result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{ "command": "echo hello_world", @@ -232,6 +254,7 @@ func TestIntegrationCmdRun(t *testing.T) { func TestIntegrationCmdRunWithWorkdir(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "cmd_run") result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{ "command": "pwd", @@ -258,6 +281,7 @@ func TestIntegrationCmdRunWithWorkdir(t *testing.T) { func TestIntegrationCmdRunInvalidTimeout(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "cmd_run") result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{ "command": "echo ok", @@ -281,6 +305,7 @@ func TestIntegrationCmdRunInvalidTimeout(t *testing.T) { func TestIntegrationCmdRunStderr(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "cmd_run") result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{ "command": "sh -c \"echo stderr_test >&2\"", @@ -310,6 +335,7 @@ func TestIntegrationCmdRunStderr(t *testing.T) { func TestIntegrationPtyCreateListClose(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "terminal_create") // Create result, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{ @@ -366,6 +392,7 @@ func TestIntegrationPtyCreateListClose(t *testing.T) { func TestIntegrationPtyInteractive(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "terminal_create") createResult, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{ "shell": "/bin/sh", @@ -424,6 +451,7 @@ func TestIntegrationPtyInteractive(t *testing.T) { func TestIntegrationPtyResize(t *testing.T) { env := setupIntegration(t) defer env.cleanup() + requireExternalTool(t, env, "terminal_create") createResult, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{ "shell": "/bin/sh", diff --git a/internal/plugins/webui/cmd_history_render_test.go b/internal/plugins/webui/cmd_history_render_test.go new file mode 100644 index 00000000..afe1f502 --- /dev/null +++ b/internal/plugins/webui/cmd_history_render_test.go @@ -0,0 +1,130 @@ +package webui + +import ( + "strings" + "testing" +) + +// renderCmdHistory 的**回归判据**(2026-10-19 生产缺陷)。 +// +// ## 原始 bug +// +// 「命令历史」面板的数据链路是: +// +// 内核 TerminalRegistry.CmdHistory() → /api/v1/cmd/history +// → loadCmdHistory() 写入 state.cmdHistory → ??? (到此为止) +// +// 旧版 renderCmdHistory 渲染的是 state.terminals 里 running 的终端, +// **state.cmdHistory 从未被读过**。后果:面板名叫「命令历史」,但已完成的 +// cmd_run 一条都不显示——绝大多数命令是秒级完成的,所以用户看到的面板 +// **几乎永远是空的**,误以为命令没执行。 +// +// ## 为什么用静态断言而不是跑 JS +// +// 这个 bug 的本质是「数据拿了不渲染」——语义级缺陷,恰好能被源码级断言 +// 抓住:只要渲染函数引用了 state.cmdHistory,且回归路径(只看 terminals) +// 被排除,链路就闭合了。这与 dashboard_js_scope_test.go 的作用域判据同款思路: +// Go 侧测试不执行 JS,但能守住 JS 源码的关键结构。 + +// cmdHistoryRendererSource 取 renderCmdHistory 的函数体。 +func cmdHistoryRendererSource(t *testing.T) string { + t.Helper() + src := dashboardJSSource(t) + start := strings.Index(src, "function renderCmdHistory()") + if start < 0 { + t.Fatal("dashboard.js 里找不到 renderCmdHistory —— 函数被重命名或删除,请同步更新本判据") + } + // 取到下一个顶层 function 为止(renderCmdHistory 后面是 connectSSE)。 + end := strings.Index(src[start+1:], "\nfunction ") + if end < 0 { + end = len(src) - start - 1 + } + return src[start : start+end] +} + +// TestCmdHistoryRendersCmdHistoryData 锁死数据源:必须读 state.cmdHistory。 +// +// 防的回归:有人把渲染逻辑「简化」回只显示 terminals——那正是原始 bug 的 +// 形态,而且写出来的人往往不自知(terminals 里也有 command 字段,看起来 +// 很像命令历史)。 +func TestCmdHistoryRendersCmdHistoryData(t *testing.T) { + body := cmdHistoryRendererSource(t) + if !strings.Contains(body, "state.cmdHistory") { + t.Error("renderCmdHistory 没有读取 state.cmdHistory —— " + + "「命令历史」面板将不显示任何已完成的 cmd_run(原始 bug 回归:数据拉回来却从不渲染)") + } +} + +// TestCmdHistoryShowsSuccessAndFailure 锁死结果语义:成功/失败必须可区分。 +// +// 命令历史的核心价值就是让人看到「哪条失败了」。没有成功/失败标识的 +// 渲染等于只给了一半信息,失败命令与成功命令混在一起无法排查。 +func TestCmdHistoryShowsSuccessAndFailure(t *testing.T) { + body := cmdHistoryRendererSource(t) + // 成功与失败要有不同的视觉标识(badge 颜色或等价物),两者都要出现。 + hasOK := strings.Contains(body, "badge-green") + hasFail := strings.Contains(body, "badge-red") + if !hasOK || !hasFail { + t.Errorf("renderCmdHistory 缺少成功/失败的视觉区分(badge-green=%v badge-red=%v)—— "+ + "命令历史的价值就在区分成败,混在一起等于没渲染", hasOK, hasFail) + } + // 退出码必须参与判定(exit_code 或等价字段),否则无法区分「status 缺失」的条目。 + if !strings.Contains(body, "exit_code") && !strings.Contains(body, "status") { + t.Error("renderCmdHistory 没有使用 exit_code/status —— 成败判定缺少依据") + } +} + +// TestCmdHistoryEscapesOutput 锁死 XSS 面:命令输出必须经 escHtml。 +// +// 命令输出是**外部内容**:cmd_run 跑的是任意命令,输出里可以有 +//