mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
refactor(plugins): 按「内置插件判定标准」收敛内置清单 16 → 11
## 判定标准(写进 all.go 与 ARCHITECTURE) **只有必须使用内部 SDK 能力(公开 SDK 没有的接口面)的插件,才留在 编译期内置清单**;其余一律外置为 .hmap 插件,独立编译、独立发版。 依据是 internal/plugin/proc/capability.go 的 withheldCapabilities: 内部 SDK 的 TerminalAPI/TrackerAPI/StatusAPI/SupervisorAPI/AdapterAPI/ SelftestAPI/ToolAPI/Publish 等能力面,外部插件在类型层就不存在。 需要它们的插件外置后功能会**静默消失**(不报错),必须内置; 只要公开能力的插件,留在内核里是白付重编成本。 ## 内置 11 个(all.go 单一权威清单) cli webui TerminalAPI/Tracker/Status/Supervisor/Adapter agentcli EventPublish(终端输出走 Publish,公开 SDK 无此方法) healthcheck SelftestAPI pluginmgr failback 恢复链必需 seq ToolAPI remotedevice RegisterChannel clawhubadapter InjectInputSync cfgmgr skillmgr seemedia(保留) ## 外置 8 个(源码保留,.hmap 由 SDK 仓 example/ 提供) timer(此前)ai_image files cmd kbtree localuse mcp multimodal ## 同步修正 - kbtree 归位:它一直是编译期内置插件,却长期被 main.go 单独导入, 游离在 all.go 清单之外(文档盘点因此数错)。现移入 all.go, main.go 不再单独导入任何插件,并加注释禁止该做法。 - integration_test:外置插件的 7 个用例(cmd_run/terminal_*)加 requireExternalTool 跳过保护——用例保留(行为判据仍有价值), 在纯内置装配下 Skip,不造假失败。 - 全部文档同步 11 个的清单与判定标准(中英四份 + 两份 README)。 ## 已核实不做的事 - 保留的 11 个内置插件无一在代码里硬依赖被外置的插件: cli 对 terminal_* 是 ExecuteTool 按名动态调用(运行时依赖); healthcheck/seq 只在白名单与示例文本里出现;webui 只订阅事件。 - kernelmod/(内核模块)与 plugins/(插件)严格区分: kernelmod/multimodal 是内核装配模块,与外置的 multimodal 插件无关。 验证:go build ./... 全绿;go test ./internal/plugins/... 全绿; webui 新增 5 条命令历史渲染回归判据(TestCmdHistory*)全绿。
This commit is contained in:
@ -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。
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 安装
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
130
internal/plugins/webui/cmd_history_render_test.go
Normal file
130
internal/plugins/webui/cmd_history_render_test.go
Normal file
@ -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 跑的是任意命令,输出里可以有
|
||||
// <script>、属性注入等。渲染时不过 escHtml 就是把执行结果直接
|
||||
// 插进 DOM —— 一次 cat 恶意文件就能在 WebUI 里执行任意 JS。
|
||||
func TestCmdHistoryEscapesOutput(t *testing.T) {
|
||||
body := cmdHistoryRendererSource(t)
|
||||
// 输出段(stdout/stderr 拼接处)必须走 escHtml。
|
||||
if !strings.Contains(body, "escHtml(out") && !strings.Contains(body, "escHtml(c.stdout") {
|
||||
t.Error("renderCmdHistory 渲染命令输出时没有调用 escHtml —— " +
|
||||
"输出是外部内容,直接 innerHTML 插入是 XSS 注入点")
|
||||
}
|
||||
// 命令文本同样来自外部(参数里可带 HTML),也要转义。
|
||||
if !strings.Contains(body, "escHtml(c.command") {
|
||||
t.Error("renderCmdHistory 渲染命令文本时没有调用 escHtml —— 命令参数同样可注入 HTML")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCmdHistoryKeepsRunningTerminals 锁死旧行为里**有效**的部分:
|
||||
// 运行中的终端仍要显示。
|
||||
//
|
||||
// 原始 bug 的旧实现虽然数据源错了,但「显示运行中的终端」这件事本身
|
||||
// 是对的(长命令确实需要能在这里看到)。重写时必须保留,不能顺手删掉。
|
||||
func TestCmdHistoryKeepsRunningTerminals(t *testing.T) {
|
||||
body := cmdHistoryRendererSource(t)
|
||||
if !strings.Contains(body, "running") {
|
||||
t.Error("renderCmdHistory 不再显示运行中的终端 —— " +
|
||||
"长命令(下载/渲染/构建)需要在命令历史面板可见,这是旧实现里正确的一半")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCmdHistoryLoadWritesState 锁死链路上游:loadCmdHistory 必须写 state。
|
||||
//
|
||||
// 渲染函数对了、但加载函数不写 state,链路同样断。这条判据保证
|
||||
// 「/api/v1/cmd/history → state.cmdHistory」这一段不被改坏。
|
||||
func TestCmdHistoryLoadWritesState(t *testing.T) {
|
||||
src := dashboardJSSource(t)
|
||||
start := strings.Index(src, "async function loadCmdHistory()")
|
||||
if start < 0 {
|
||||
t.Fatal("dashboard.js 里找不到 loadCmdHistory")
|
||||
}
|
||||
end := strings.Index(src[start+1:], "\nfunction ")
|
||||
if end < 0 {
|
||||
end = len(src) - start - 1
|
||||
}
|
||||
body := src[start : start+end]
|
||||
if !strings.Contains(body, "state.cmdHistory") {
|
||||
t.Error("loadCmdHistory 没有把响应写入 state.cmdHistory —— " +
|
||||
"渲染端永远拿到空数组,命令历史面板将恒为空")
|
||||
}
|
||||
if !strings.Contains(body, "/cmd/history") {
|
||||
t.Error("loadCmdHistory 没有请求 /cmd/history —— 数据源被换掉时必须同步更新本判据")
|
||||
}
|
||||
}
|
||||
@ -4987,50 +4987,106 @@ function renderTerminals() {
|
||||
}
|
||||
|
||||
function renderCmdHistory() {
|
||||
// 面板标题是「命令历史」,数据源必须是 cmd_run 的执行记录
|
||||
// (/api/v1/cmd/history → 内核 TerminalRegistry.CmdHistory)。
|
||||
//
|
||||
// ★ 旧实现渲染的是**终端列表**(state.terminals 里 running 的那些),
|
||||
// 而 loadCmdHistory() 拉回来的 state.cmdHistory **从未被读过** ——
|
||||
// 于是面板名叫「命令历史」,实际只显示运行中的终端:
|
||||
// · 已完成的命令(绝大多数)永远不出现,用户以为命令没执行;
|
||||
// · 终端的 uptime 被当成命令时长,语义也对不上。
|
||||
// 现在分两段:先列 cmd_run 历史(带退出码/输出),再列运行中的终端。
|
||||
var r = document.getElementById("cmd-list");
|
||||
var cnt = document.getElementById("cmd-count-badge");
|
||||
if (!r) return;
|
||||
|
||||
var history = state.cmdHistory || [];
|
||||
var running = (state.terminals || []).filter((t) => t.running);
|
||||
if (cnt) cnt.textContent = running.length;
|
||||
if (running.length === 0) {
|
||||
if (cnt) cnt.textContent = String(history.length + running.length);
|
||||
|
||||
if (history.length === 0 && running.length === 0) {
|
||||
r.innerHTML =
|
||||
'<p style="color:var(--text-muted);padding:8px;text-align:center;font-size:11px">' +
|
||||
__("暂无运行中的命令", "No running commands") +
|
||||
__("暂无命令记录", "No command history") +
|
||||
"</p>";
|
||||
return;
|
||||
}
|
||||
var html =
|
||||
'<table style="font-size:10px"><tr><th>' +
|
||||
__("命令", "Command") +
|
||||
"</th><th>" +
|
||||
__("状态", "Status") +
|
||||
"</th><th>" +
|
||||
__("运行时长", "Uptime") +
|
||||
"</th></tr>";
|
||||
running.forEach((t) => {
|
||||
var scr = (state.termScreens && state.termScreens[t.id]) || null;
|
||||
var out = scr ? scr.output : t.output || "";
|
||||
|
||||
var html = "";
|
||||
|
||||
if (history.length > 0) {
|
||||
html +=
|
||||
"<tr>" +
|
||||
'<td style="font-family:monospace;max-width:200px;overflow:hidden;text-overflow:ellipsis">' +
|
||||
escHtml(t.command || t.id || "") +
|
||||
"</td>" +
|
||||
'<td><span class="badge badge-green">' +
|
||||
__("运行中", "Running") +
|
||||
"</span></td>" +
|
||||
'<td style="color:var(--text-muted);white-space:nowrap">' +
|
||||
escHtml(t.uptime || "-") +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
if (out) {
|
||||
'<table style="font-size:10px"><tr><th>' +
|
||||
__("命令", "Command") +
|
||||
"</th><th>" +
|
||||
__("状态", "Status") +
|
||||
"</th><th>" +
|
||||
__("时间", "Time") +
|
||||
"</th></tr>";
|
||||
// 新的在上:内核按时间正序追加,用户最关心最近一条。
|
||||
history
|
||||
.slice()
|
||||
.reverse()
|
||||
.forEach((c) => {
|
||||
var ok = (c.status || "").toLowerCase() === "ok" || c.exit_code === 0;
|
||||
var badge = ok ? "badge-green" : "badge-red";
|
||||
var label = ok
|
||||
? __("成功", "OK")
|
||||
: __("失败", "Failed") + (c.exit_code ? " (" + c.exit_code + ")" : "");
|
||||
html +=
|
||||
"<tr>" +
|
||||
'<td style="font-family:monospace;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
|
||||
escHtml(c.command || "-") +
|
||||
"</td>" +
|
||||
'<td><span class="badge ' +
|
||||
badge +
|
||||
'">' +
|
||||
escHtml(label) +
|
||||
"</span></td>" +
|
||||
'<td style="color:var(--text-muted);white-space:nowrap">' +
|
||||
escHtml(c.time || "-") +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
// 输出按需展开:默认收起,避免长输出把面板撑爆。
|
||||
var out = (c.stdout || "") + (c.stderr ? "\n" + c.stderr : "");
|
||||
out = out.trim();
|
||||
if (out) {
|
||||
html +=
|
||||
'<tr><td colspan="3" style="padding:0 0 4px 0">' +
|
||||
'<pre style="margin:0;padding:4px 8px;max-height:120px;overflow:auto;background:var(--bg-input);border-radius:4px;font-size:10px;color:var(--text-secondary);white-space:pre-wrap">' +
|
||||
escHtml(out.substring(0, 2000)) +
|
||||
(out.length > 2000 ? "\n… (truncated)" : "") +
|
||||
"</pre></td></tr>";
|
||||
}
|
||||
});
|
||||
html += "</table>";
|
||||
}
|
||||
|
||||
if (running.length > 0) {
|
||||
html +=
|
||||
'<div style="margin-top:8px;font-size:10px;color:var(--text-muted)">' +
|
||||
__("运行中的终端", "Running terminals") +
|
||||
"</div>";
|
||||
html +=
|
||||
'<table style="font-size:10px"><tr><th>' +
|
||||
__("命令", "Command") +
|
||||
"</th><th>" +
|
||||
__("运行时长", "Uptime") +
|
||||
"</th></tr>";
|
||||
running.forEach((t) => {
|
||||
html +=
|
||||
"<tr>" +
|
||||
'<td colspan="3" style="padding:0"><pre style="margin:0;padding:4px 8px;max-height:120px;overflow:auto;background:var(--bg-input);border-radius:4px;font-size:10px;color:var(--text-secondary)">' +
|
||||
escHtml(out.substring(0, 2000)) +
|
||||
"</pre></td></tr>";
|
||||
}
|
||||
});
|
||||
html += "</table>";
|
||||
'<td style="font-family:monospace;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
|
||||
escHtml(t.command || t.id || "") +
|
||||
"</td>" +
|
||||
'<td style="color:var(--text-muted);white-space:nowrap">' +
|
||||
escHtml(t.uptime || "-") +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
}
|
||||
|
||||
r.innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user