mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
docs: 全面按当前源码更新文档 + 删除已过时文档
## 删除(内容已落地/已被替换,保留只会误导)
- demo.md ................... failback 与 recoverydiag 均已实现,0 引用
- docs/defect-qq-output-send-loop.md .. 已修复(本身也标了「已修复」),0 引用
- docs/embedding-comparison.md ....... 一次性选型报告,仅被 agent 产物引用
- docs/zh/plan.md ............ 描述的旧 nav 布局已重写、死配置已清,全部完成
- docs/zh/plugin-migration-plan.md ... 迁移已上生产,纯过程稿(Part 0~6 全完成)
## 更新(按当前源码核对)
- assets/docs/{zh,en}/ARCHITECTURE.md(README 指向的用户文档,最重要):
把只讲 cancel/intercept 的旧「中断机制」章节重写为「输入调度器与中断机制」——
补上两类别 + 四级中断(L1~L4,默认 L1、外部插件 L4 夹到 L3)+ 抢占/挂起/中断栈
+ 饥饿防护(PreemptCount 提升,封顶 L4)+ 抢占冷却(2s)+ 停止语义(cancelBudget)
+ 驻留子/分诊助手/残余任务;新增「上下文预算」章节(窗口 ≠ 工作面,600K 封顶,
预算是上限非填充目标)。中英章节数现已对齐(各 13 节)。
- assets/docs/{zh,en}/PLUGIN_DEV.md:插件示例表补 6 个缺失项
(acp/deepsearch/plugindev/recoverydiag/vanblog/vikunja);qq 工具数 17 → 20(实测)。
- README.md / README_EN.md:补 v1.3.x 线(此前只到 v1.2.0,而 1.3.x 已发布 12 个 patch)——
驻留式子 agent、输出通道寻址、输入调度器、轻量内核 profile、积压及时反馈。
- plan.md:开头两个「⚠️ 紧急/正在持续污染」是过期告警(实测 残留 = 0),
改为「已解决」并加文档定位说明;§13 仍是活跃路线图故保留。
- docs/zh/plugin-interface-matrix.md + 两处源码注释:清理指向已删文档的断链。
全仓 md 断链检查:仅剩 1 处,位于 third_party 的 oh_modules(第三方依赖,非本项目)。
This commit is contained in:
17
README.md
17
README.md
@ -195,6 +195,23 @@ internal/
|
||||
|
||||
## 项目状态
|
||||
|
||||
**v1.3.x 线**(v1.3.1–v1.3.12,最新已发布)—— **驻留式子 agent** + **输入调度器重做**。
|
||||
|
||||
- **驻留式子 agent**:内核可派驻轻量内核的子 agent(自己的调度器、自己的 temp 图记忆、
|
||||
共享通道登记表)。父经 `resident_agents`(list/create/send/inspect/compress/reclaim/destroy)
|
||||
派活与收活;inputch 可划给子,输入**在进内核之前**就已路由到子。
|
||||
- **输出通道可寻址到具体 agent**:`AllowedOutputs` 授权集合(三处过滤点一致),
|
||||
父/子之间可互相投递;设备能力也 outputch 化(每设备一个 `device/<id>` 通道)。
|
||||
- **输入调度器**:排队/中断两类别 + 四级中断(L1~L4)+ 抢占/挂起/恢复/中断栈;
|
||||
同级不抢占、有饥饿防护与抢占冷却;L4 只归内核与内核级插件(WebUI 终止按钮)。
|
||||
- **轻量内核 profile**:子的记忆面收窄为「传统上下文 + 图记忆」(窄接口,
|
||||
主库以 query_only 受限句柄打开,写走自己的 temp 实例)。
|
||||
- **积压及时反馈**(后续线):主 agent 长时间忙时,内核把排队输入交给临时**分诊助手** ——
|
||||
简单的直接处理并回复,需要主 agent 的立刻回「忙碌中,请稍候」,用户不再干等十几分钟。
|
||||
- 修掉一批真实缺陷:销毁驻留子时入站 inputch(`child/<id>`)注册残留、
|
||||
子的轮次永远显示 0(`info()` 根本没填)、子侧 childIO 空壳(未继承父的输出通道)、
|
||||
设备心跳 pong 忘了 Flush(每 60 秒掉线)、Lua 插件桥与 SDK 1.3.0 对齐。
|
||||
|
||||
**v1.2.0** — 统一多模态向量空间 + 媒体升为图记忆一等节点 + 数据面全量迁到共享内存。
|
||||
|
||||
- **模型中立的统一向量空间**:内核不再适配任何具体模型,只提供公共 provider SPI
|
||||
|
||||
25
README_EN.md
25
README_EN.md
@ -184,6 +184,31 @@ External plugin development: see [homeagent-sdk](https://gitcode.com/JianFeeeee/
|
||||
|
||||
## Project Status
|
||||
|
||||
**v1.3.x line** (v1.3.1–v1.3.12, latest released) — **resident sub-agents** + **input scheduler rework**.
|
||||
|
||||
- **Resident sub-agents**: the kernel can station lightweight-kernel child agents (their own
|
||||
scheduler, their own temp graph memory, sharing the channel registry). The parent dispatches
|
||||
and collects work via `resident_agents` (list/create/send/inspect/compress/reclaim/destroy).
|
||||
An inputch can be assigned to a child, so input is routed to it **before entering the kernel**.
|
||||
- **Output channels addressable to a specific agent**: the `AllowedOutputs` grant set
|
||||
(consistent across all three filter points) lets parent/child deliver to each other;
|
||||
device capabilities became output channels too (one `device/<id>` per device).
|
||||
- **Input scheduler**: two task classes (queued/interrupt) + four interrupt levels (L1–L4)
|
||||
+ preempt/suspend/resume/interrupt-stack; same level never preempts same level, with a
|
||||
starvation guard and preemption cooldown. L4 belongs only to the kernel and kernel-level
|
||||
plugins (e.g. the WebUI stop button).
|
||||
- **Lightweight kernel profile**: a child's memory surface narrows to "conventional context
|
||||
+ graph memory" (narrow interface; the main graph opens as a query_only handle, writes go
|
||||
to its own temp instance).
|
||||
- **Backlog timely feedback** (later in the line): when the main agent is busy for a long time,
|
||||
the kernel hands queued input to a temporary **triage assistant** — simple items are handled
|
||||
directly, items needing the main agent get an immediate "busy, please wait", so users no
|
||||
longer wait 10+ minutes in silence.
|
||||
- Fixed a batch of real defects: inbound inputch (`child/<id>`) registration leak on resident
|
||||
destruction, a child's round count always showing 0 (`info()` never filled it), an empty
|
||||
child-side childIO (output channels not inherited), device heartbeat pong missing Flush
|
||||
(dropping every 60s), and Lua plugin bridge alignment with SDK 1.3.0.
|
||||
|
||||
**v1.2.0** — unified multimodal vector space, media promoted to first-class graph memory, and the whole data plane moved into shared memory.
|
||||
|
||||
- **Model-neutral unified embedding space**: the kernel no longer adapts to any specific model.
|
||||
|
||||
@ -455,7 +455,74 @@ Extended fields:
|
||||
- Relation extension: Confidence
|
||||
|
||||
|
||||
## Interrupt Mechanism
|
||||
## Input Scheduler & Interrupt Mechanism
|
||||
|
||||
Inputs do not go straight to the LLM — they first enter the **input scheduler**
|
||||
(`internal/agent/core/scheduler.go`). Full design:
|
||||
[`docs/zh/input-scheduler-design.md`](../../../docs/zh/input-scheduler-design.md).
|
||||
|
||||
### Two task classes
|
||||
|
||||
| Class | Level | Meaning |
|
||||
|-------|-------|---------|
|
||||
| `TaskQueued` | none (always 0) | Pending work. Any interrupt (≥ L1) preempts it |
|
||||
| `TaskInterrupt` | L1–L4 | "How urgent is this", declared by the source via `InjectOptions.Priority` |
|
||||
|
||||
### Four interrupt levels
|
||||
|
||||
| Level | Meaning | Typical source |
|
||||
|-------|---------|----------------|
|
||||
| L1 Background | Fully deferrable | QQ/WeChat messages, bulk notifications |
|
||||
| L2 Message | General notice | Plugin hints that should be seen soon but aren't urgent |
|
||||
| L3 Interactive | Needs timely handling | Timer expiry, terminal output, resident-agent reports |
|
||||
| L4 Critical | **Kernel-exclusive** | panic, kernel events, kernel-level plugin stop button |
|
||||
|
||||
When no level is declared it defaults to **L1** — "explicit is a privilege", so a new
|
||||
plugin never gets preemption rights by accident. L4 declared by an external plugin is
|
||||
**clamped to L3** (`clampPluginLevel`).
|
||||
|
||||
### Preemption and suspension
|
||||
|
||||
- **Same level never preempts same level** (`canPreempt` requires strictly greater) —
|
||||
this is why messages normally wait for the running task to finish.
|
||||
- A preempted task is pushed onto the **interrupt stack** (LIFO) with its frame saved,
|
||||
and resumed later; the stack is never re-sorted by priority.
|
||||
- **Starvation guard**: preemption count raises the effective level
|
||||
(`effectiveLevel = Level + min(PreemptCount, 2)`, capped at L4).
|
||||
- **Preemption cooldown**: a just-preempted task cannot be preempted again for
|
||||
`preemptCooldown` (2s), so a high-priority stream cannot interrupt the same task forever.
|
||||
- The interrupt stack depth is structurally bounded (chain = queued ← L1 ← L2 ← L3 ← L4).
|
||||
|
||||
### Stop (user presses stop / `/stop`)
|
||||
|
||||
Stop is not an empty interrupt. It does two things: ① cancel the current LLM inference;
|
||||
② short-circuit the x messages **already queued at the moment of stop** during their
|
||||
pre-action phase (`cancelBudget` snapshot), instead of running them as new input.
|
||||
Inputs arriving **after** the stop are unaffected.
|
||||
|
||||
`PendingInputs()` must include the segment still sitting in `io.inputCh` (not yet moved
|
||||
into the queue by `pumpInbox`) — during a stop the scheduler is usually busy running a
|
||||
task, and counting only `sched.queue` yields 0.
|
||||
|
||||
### Resident sub-agents and timely feedback
|
||||
|
||||
Design: [`docs/zh/resident-subagent-design.md`](../../../docs/zh/resident-subagent-design.md).
|
||||
|
||||
- A **resident** is an independent lightweight-kernel agent: its own scheduler, its own
|
||||
temp graph memory, sharing the channel registry.
|
||||
- Parent→child control plane: `resident_agents`
|
||||
(list / create / send / inspect / compress / reclaim / destroy).
|
||||
- **Backlog feedback**: when the main agent is busy for a long time (default > 5m,
|
||||
configurable), the kernel hands queued inputs to a temporary **triage assistant**
|
||||
(`offload_*` config): simple ones are handled directly, ones needing the main agent
|
||||
get an immediate "busy, please wait". Users no longer wait 10+ minutes in silence.
|
||||
- The triage assistant gets **no inputch** (it receives no plugin user input) and
|
||||
**all output channels** (results must reach the original channel).
|
||||
- On reclaim/destroy, its **residual tasks are decided explicitly by the parent**:
|
||||
`residual=keep` (returned to the parent queue, default) or `drop` (explicitly
|
||||
discarded with a per-item log entry).
|
||||
|
||||
### Legacy three-path view (still present, now a layer beneath the scheduler)
|
||||
|
||||
```
|
||||
interceptLoop (goroutine)
|
||||
@ -465,15 +532,28 @@ interceptLoop (goroutine)
|
||||
└── (c) InjectInput() → Trigger new processing when idle
|
||||
```
|
||||
|
||||
Three delivery paths:
|
||||
Code: `internal/agent/core/scheduler.go` (scheduler), `eventloop.go` (intercept loop).
|
||||
|
||||
| Path | Effect | Timing |
|
||||
|------|--------|--------|
|
||||
| cancelLLM | Cancel current HTTP request | On context.Canceled |
|
||||
| interceptCh | Insert `[interrupt message]` in process() | Before each LLM call |
|
||||
| InjectInput | Trigger new processing when eventLoop is idle | No ongoing request |
|
||||
## Context Budget
|
||||
|
||||
Code: `internal/agent/core/eventloop.go` — `interceptLoop` / `drainInterrupts`
|
||||
`internal/agent/core/tokenbudget.go` — `ComputeTokenBudget`:
|
||||
|
||||
```
|
||||
maxCtx = provider.MaxContextTokens() // declared window (per-source context_window wins)
|
||||
targetUsage = min(maxCtx × 0.8, 600000) // working band, capped at 600K
|
||||
├── memory recall budget = (targetUsage - fixed) / 3
|
||||
└── context events budget = remaining 2/3
|
||||
```
|
||||
|
||||
★ **Window ≠ working band**: a source's real window may reach 1M, but near-full windows
|
||||
lose attention and cost/latency rise linearly, so `maxTargetTokens=600000` caps the
|
||||
working band separately. If the model name (e.g. `AUTO`) yields no window,
|
||||
`ModelContextWindow` **logs a warning** and falls back conservatively; operators should
|
||||
declare `core.llm.sources.<name>.context_window` explicitly.
|
||||
|
||||
**Budgets are ceilings, not fill targets**: memory is recall-ranked (it stops when nothing
|
||||
is relevant) and the timeline is taken newest-first within budget. Measured: with a 400K
|
||||
budget, actual injection was still a few hundred characters.
|
||||
|
||||
|
||||
## Configuration System
|
||||
|
||||
@ -804,7 +804,7 @@ Internal: records are stored in SQLite `disabled_plugins` table (`name`, `disabl
|
||||
|---------|------|----------|
|
||||
| [weather](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/weather) | Go | Weather queries (wttr.in); demonstrates NoMemory/Cleaner/stage hooks/channels/text memory |
|
||||
| [luademo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/luademo) | Lua | Full-featured Lua example covering the whole v0.8.0 Lua SDK surface |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 17 tools, full input/output channel wiring |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 20 tools, full input/output channel wiring |
|
||||
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | Memo management, PreAction injection + timed interrupt dual reminder |
|
||||
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | File system operations, 4 write modes, sandbox isolation |
|
||||
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | Web search + HTTP fetch (SSRF) + Chromium render (merged from web/webfetch) |
|
||||
@ -817,6 +817,12 @@ Internal: records are stored in SQLite `disabled_plugins` table (`name`, `disabl
|
||||
| [rss](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/rss) | Go | RSS subscriptions |
|
||||
| [ai_image](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ai_image) | Go | AI image generation |
|
||||
| [music](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/music) | Go | Music playback |
|
||||
| [vikunja](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/vikunja) | Go | Vikunja task management (projects/tasks/labels CRUD) |
|
||||
| [vanblog](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/vanblog) | Go | VanBlog publishing and management |
|
||||
| [deepsearch](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/deepsearch) | Go | Multi-round deep search (progressive focus + cited summary) |
|
||||
| [acp](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/acp) | Go | Agent Client Protocol (external editors/IDEs drive this agent) |
|
||||
| [recoverydiag](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/recoverydiag) | Go | Five-part fault diagnosis (triage / sqlite check / log signatures / diff / ranked conclusions); core plugin of failback mode |
|
||||
| [plugindev](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/plugindev) | Go | Plugin scaffolding: generate, build, install — lets the agent develop plugins itself |
|
||||
|
||||
### Built-in Plugins
|
||||
|
||||
|
||||
@ -444,7 +444,64 @@ type Plugin interface {
|
||||
- Relation 扩展:Confidence
|
||||
|
||||
|
||||
## 中断机制
|
||||
## 输入调度器与中断机制
|
||||
|
||||
输入不直接进 LLM —— 它们先进**输入调度器**(`internal/agent/core/scheduler.go`)。
|
||||
设计全文见 [`docs/zh/input-scheduler-design.md`](../../../docs/zh/input-scheduler-design.md)。
|
||||
|
||||
### 两类任务
|
||||
|
||||
| 类别 | 级别 | 语义 |
|
||||
|------|------|------|
|
||||
| `TaskQueued` 排队输入 | 无级别(恒 0) | 待办工作。任何中断(≥ L1)都能抢它 |
|
||||
| `TaskInterrupt` 中断 | L1~L4 | "这件事有多不能等",由来源在 `InjectOptions.Priority` 声明 |
|
||||
|
||||
### 四级中断
|
||||
|
||||
| 级别 | 含义 | 典型来源 |
|
||||
|------|------|----------|
|
||||
| L1 背景 | 完全可等 | QQ/微信消息、批量通知 |
|
||||
| L2 消息 | 一般提醒 | 插件希望尽快看到但不紧急的提示 |
|
||||
| L3 交互 | 需及时处理 | 定时器到达、终端输出、子 agent 汇报 |
|
||||
| L4 关键 | **内核独占** | panic、内核事件、内核级插件的终止按钮 |
|
||||
|
||||
未声明级别时取 **L1**(`DefaultLevel`)——「显式才是特权」,新插件不会默认拿到抢占权。
|
||||
外部插件声明 L4 会被**夹到 L3**(`clampPluginLevel`)。
|
||||
|
||||
### 抢占与挂起
|
||||
|
||||
- **同级不能抢占同级**(`canPreempt` 要求严格大于)——这是日常"消息排队等前面跑完"的成因。
|
||||
- 被抢占的任务压入**中断栈**(LIFO),用 `suspendStack` 保存现场,稍后恢复;
|
||||
栈内不做优先级重排("后被打断的先恢复"才是栈语义)。
|
||||
- **饥饿防护**:被抢占次数会提升有效级别(`effectiveLevel = Level + min(PreemptCount, 2)`,
|
||||
封顶 L4),确保低级别流不会被困。
|
||||
- **抢占冷却**:刚被抢占过的任务在 `preemptCooldown`(2s)内不再被抢,
|
||||
避免高优先级流把同一个任务反复打断到永不完结。
|
||||
- 中断栈帧数有**结构上界**(链条 = 排队 ← L1 ← L2 ← L3 ← L4,最多挂起 4 帧)。
|
||||
|
||||
### 停止(用户按停止按钮 / `/stop`)
|
||||
|
||||
停止 ≠ 空中断。它做两件事:① 立即结束当前 LLM 推理;② 对**停止那一刻已排队**
|
||||
的 x 条消息依次在 pre-action 阶段短路(`cancelBudget` 快照配额),而不是把它们
|
||||
当新输入再跑一遍。停止之后**新到**的输入不受影响。
|
||||
|
||||
`PendingInputs()` 必须把"还停在 `io.inputCh`、没被 `pumpInbox` 搬进队列"的那一段
|
||||
算进来 —— 停止时调度器多半正忙于当前任务,只数 `sched.queue` 会得到 0。
|
||||
|
||||
### 驻留式子 agent 与及时反馈
|
||||
|
||||
设计见 [`docs/zh/resident-subagent-design.md`](../../../docs/zh/resident-subagent-design.md)。
|
||||
|
||||
- **驻留子**是轻量内核的独立 agent:自己的调度器、自己的 temp 图记忆、共享的通道登记表。
|
||||
- 父对子的控制面:`resident_agents`(list / create / send / inspect / compress / reclaim / destroy)。
|
||||
- **积压及时反馈**:主 agent 长时间忙时(默认 > 5m,可配),内核把排队输入交给
|
||||
一个临时**分诊助手**(`offload_*` 配置):简单的直接处理并回复,需要主 agent 的
|
||||
立刻回「忙碌中,请稍候」。这样用户不会干等十几分钟。
|
||||
- 分诊助手**不配 inputch**(不接收插件用户输入)、**持有全部输出通道**(结果要能发回原通道)。
|
||||
- 回收/销毁时它手头的**残余任务由父显式决定**:`residual=keep`(转回父队列,默认)
|
||||
或 `drop`(明确丢弃,逐条记日志)。
|
||||
|
||||
### 旧版三路径(仍存在,但已是调度器之下的一层)
|
||||
|
||||
```
|
||||
interceptLoop (goroutine)
|
||||
@ -454,15 +511,26 @@ interceptLoop (goroutine)
|
||||
└── (c) InjectInput() → 空闲时触发新处理
|
||||
```
|
||||
|
||||
三种投递路径:
|
||||
代码:`internal/agent/core/scheduler.go`(调度器)、`eventloop.go`(拦截循环)。
|
||||
|
||||
| 路径 | 效果 | 时机 |
|
||||
|------|------|------|
|
||||
| cancelLLM | 取消当前 HTTP 请求 | 收到 context.Canceled |
|
||||
| interceptCh | process() 中插入 `[打断消息]` | 每个 LLM call 前 |
|
||||
| InjectInput | eventLoop 空闲时触发新处理 | 无进行中请求 |
|
||||
## 上下文预算
|
||||
|
||||
代码:`internal/agent/core/eventloop.go` — `interceptLoop` / `drainInterrupts`
|
||||
`internal/agent/core/tokenbudget.go` — `ComputeTokenBudget`:
|
||||
|
||||
```
|
||||
maxCtx = provider.MaxContextTokens() // 声明窗口(per-source context_window 优先)
|
||||
targetUsage = min(maxCtx × 0.8, 600000) // 工作面:封顶 600K
|
||||
├── 记忆召回预算 = (targetUsage - 固定开销) / 3
|
||||
└── 上下文事件预算 = 其余 2/3
|
||||
```
|
||||
|
||||
★ **窗口 ≠ 工作面**:源的真实窗口可能到 1M,但接近满窗口时注意力涣散、
|
||||
成本与延迟线性上升,因此 `maxTargetTokens=600000` 把工作面单独封顶。
|
||||
若模型名(如 `AUTO`)推断不出窗口,`ModelContextWindow` 会**打日志提醒**并回退保守值,
|
||||
部署方应用 `core.llm.sources.<name>.context_window` 显式声明。
|
||||
|
||||
**预算都是上限而非填充目标**:记忆按相关度召回(没相关就停),时间线按预算从新到旧取。
|
||||
实测:预算 400K 时实际注入仍只有几百字符。
|
||||
|
||||
|
||||
## 配置系统
|
||||
|
||||
@ -797,7 +797,7 @@ pmgr.ReloadPlugins() // 重载所有插件
|
||||
|------|------|------|
|
||||
| [weather](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/weather) | Go | 天气查询(wttr.in),演示 NoMemory/Cleaner/阶段钩子/通道/文本记忆 |
|
||||
| [luademo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/luademo) | Lua | Lua 全功能示例,覆盖 v0.8.0 Lua SDK 全部 API 面 |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接,17 个工具,输入/输出通道完整对接 |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接,20 个工具,输入/输出通道完整对接 |
|
||||
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | 备忘管理,PreAction 注入 + 定时打断双提醒 |
|
||||
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | 文件系统操作,4 种写入模式,沙箱隔离 |
|
||||
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | 网络搜索、网页抓取(SSRF)、浏览器渲染(合并自 web/webfetch) |
|
||||
@ -810,6 +810,12 @@ pmgr.ReloadPlugins() // 重载所有插件
|
||||
| [rss](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/rss) | Go | RSS 订阅 |
|
||||
| [ai_image](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ai_image) | Go | AI 图片生成 |
|
||||
| [music](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/music) | Go | 音乐播放 |
|
||||
| [vikunja](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/vikunja) | Go | Vikunja 任务管理对接(项目/任务/标签 CRUD) |
|
||||
| [vanblog](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/vanblog) | Go | VanBlog 博客发布与管理 |
|
||||
| [deepsearch](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/deepsearch) | Go | 多轮深度检索(逐层聚焦 + 引用汇总) |
|
||||
| [acp](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/acp) | Go | Agent Client Protocol 对接(外部编辑器/IDE 驱动本 agent) |
|
||||
| [recoverydiag](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/recoverydiag) | Go | 故障诊断五件套(分诊/sqlite 校验/日志签名/diff/结论排序),failback 模式的核心插件 |
|
||||
| [plugindev](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/plugindev) | Go | 插件脚手架:生成工程、构建、安装,供 agent 自助开发插件 |
|
||||
|
||||
### 内置插件
|
||||
|
||||
|
||||
364
demo.md
364
demo.md
@ -1,364 +0,0 @@
|
||||
# HomeAgent 自愈 / Failback 架构设计与讨论全程记录 (demo.md)
|
||||
|
||||
> 本文档按讨论演进顺序记录"守护 / 保活 / 文件追踪 / 崩溃自愈 / failback"整个设计过程,
|
||||
> 含代码勘查结论、现实日志记录模式分析,以及最终定稿的架构与尚未落地的接口清单。
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景与目标
|
||||
|
||||
框架目标:**内核零 IO、插件承载所有 IO**(`homed 内核 ← PluginSDK → 插件`),三层记忆 + 常用块。
|
||||
|
||||
**Failback 的定位:所有错误的一层兜底(Safe-Mode 式),而非针对单一场景。**
|
||||
|
||||
- 主 agent(全量 LLM agent)是一切骚操作的执行者,可能把自己搞到无法自愈的任意状态:
|
||||
LLM 源改坏 / 系统网络(proxy、DNS、host)破坏 / 配置文件损坏 / OOM / panic / 崩溃循环……
|
||||
这些错误无法在**同一个被污染环境内**用自身操作自救。
|
||||
- 故需要一层**脱离主 agent 坏环境的、最小厚度且独立可控的恢复层**——
|
||||
类似 Windows **安全模式 / 启动修复**:只带最小驱动集合 + 干净 LLM 源(锚定 IP),
|
||||
单一职责:**让主 agent 回到可用状态;若不可行,则做最后的系统级兜底(回滚快照 / 重启)。**
|
||||
- 它不替代主 agent 的功能,只在主 agent 无法自愈时作为最后一道防线出现。能省则省、能判定就不推理、有底即主张。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状盘点(代码勘查结论)
|
||||
|
||||
### 1.1 守护进程(`internal/supervisor/daemon.go`)
|
||||
|
||||
- 主 agent in-process 常驻,`agent.Start()` 在 `cmd/homed/main.go:468` 直接启动,**无独立进程边界**。
|
||||
- `healthLoop` → `checkAgent` 判 LLM 是否可达,判定**仅依赖 `network.Monitor` 的 `AggregateResult().LLMAPIReachable`**(`daemon.go:132`)。
|
||||
- `failCount >= MaxRetries`(默认 3)→ `handleFailure`:
|
||||
- 有 tracker → `trk.Rollback()`;失败才降级 `restartAgent`。
|
||||
- `restartAgent`(`daemon.go:166`)**只改内存状态再重新 Register,不真重启任何进程**,几乎空转。
|
||||
|
||||
关键问题:
|
||||
- 探测是系统级(HTTP/DNS/TCP),回滚只作用于 `<data>/agentfs` overlay 的 upper,**二者对象错位**。
|
||||
- `AgregateResult` 在无 LLM endpoint 时恒 healthy,机制形同虚设。
|
||||
- `lastHB` 每轮都置 `time.Now()`,`Uptime` 无意义。
|
||||
- `RollbackPolicy` 的 `HealthThreshold/CooldownPeriod/AutoRollback` 都是死字段,只用 `MaxRetries`。
|
||||
|
||||
### 1.2 文件追踪(`internal/tracker`)
|
||||
|
||||
- overlayfs 三层:`lower/upper/work → merged`(`tracker.go:156`)。
|
||||
- `captureFSState(upperDir)` 递归遍历 upper 并 sha256(`changeset.go:56`);`PreAction/PostAction` 前后 diff(`toolcall.go:86-94`)。
|
||||
- **lower 恒空**(`Init` 只 `MkdirAll`,从不填充)→ 无 canonical 基线可回滚。
|
||||
- `Rollback()` = `RemoveAll(upper)` 清空全部 changesets;`FileChange.Content`(本应存回滚原文)**从未回填**。
|
||||
|
||||
结论:overlay/tracker 对"LLM 可达性"这主场景**错位**,只能作数据兜底。
|
||||
|
||||
### 1.3 通信插件真相(`third_party/homeagent-sdk/example/qq/plugin.go`)
|
||||
|
||||
- agent 对外通信全部由**插件设置**驱动,存于 **ConfigRegistry / SQLite config.db**:
|
||||
`qq.napcat_url`、`qq.listen`、`qq.files_dir`、`qq.remote_dir`、`dm/group_policy`(`plugin.go:120-130`)。
|
||||
- 插件在 `Start()` 里 `getSetting(...)` 读设置(`plugin.go:134-143`)→ **改动配置需重载插件才生效**。
|
||||
- `cfgmgr` 提供 `config_set / config_batch_set` 可运行时改任意 core/插件配置(`cfgmgr/plugin.go:54,103`)。
|
||||
|
||||
### 1.4 LLM 源与"恢复即生效"
|
||||
|
||||
`internal/sdk/llm_impl.go:103 ReloadFromConfig()` **已存在**:
|
||||
- `cfg := cfgReg.ToConfig()` 从 config.db 重建(含 `core.llm.sources.*`,见 `registry.go:590`)
|
||||
- `mgr.Reset()` → 逐源 `NewLuaAdaptedProvider` → 重设默认。
|
||||
|
||||
即:**LLM 源的"恢复即生效"钩子已经具备**,缺的是"快照 + 探测 + 触发"三件事。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现实环境:日志记录模式内参
|
||||
|
||||
> 看真实 systemd 托管的 HomeAgent(`/home/newqqagent`)日志,**目的是弄清现有的日志模型**
|
||||
> (写哪、什么格式、工具调用打在哪),为 failback / recoveryDiag 的 `diag_log_scan` 提供准确的解析依据。
|
||||
|
||||
### 2.1 systemd 托管现状(样例)
|
||||
|
||||
```
|
||||
homeagent.service: Type=simple, ExecStart=/usr/local/bin/homed -data /home/newqqagent, Restart=always, RestartSec=10
|
||||
llm-mock.service: ExecStart=/usr/bin/python3 /opt/llm-mock/mock_server.py, Restart=always, RestartSec=3
|
||||
```
|
||||
|
||||
- 实测数据区:`/home/newqqagent/` 下有 `log/`、`config.db`、`agentfs/`(overlay merged)、`snapshots/`、`changesets/`、`knowledge/`、`memory/`、`plugins/`、
|
||||
`cli.sock`、`adapters/`、`homed.log`、`memos.json` 等——**日志以独立子目录 `log/` 存放,与配置/快照/knowledge 分置**。
|
||||
- 启动段确认:`[files] started, sandbox: /`(**files 沙箱=全主机 `/` 实锤**);`main agent started, model=mock-model base=http://127.0.0.1:18080/v1 sources=3 adapters=8`(LLM 走本地 mock)。
|
||||
|
||||
### 2.2 日志目录与格式(核心)
|
||||
|
||||
- **目录配置**:`core.log.path`,默认 `<dataDir>/log`(`internal/config/registry.go:415,508`)。
|
||||
- **单次运行文件**:每次启动新建 `homed_<YYYY-MM-DD_HH-MM-SS>.log`(`cmd/homed/main.go:75`),
|
||||
写入 `logDir` 下;`log.SetOutput(io.MultiWriter(os.Stderr, logFile))`(`main.go:80`)——
|
||||
**同时进 stderr(systemd 捕获到 journald/`journalctl -u`)与文件**。
|
||||
- **格式**:标准 Go `log.Printf`,即 `YYYY/MM/DD HH:MM:SS file.go:line: [module] message`。
|
||||
用户可看文件,也可用 `journalctl -u homeagent.service` 看同一来源(同一行)。
|
||||
- **层级压缩 + 保留**(`internal/log/manager.go:26-28` + `compressor.go`):
|
||||
- 周度压缩 → `week_<year>-W<ww>.tar.gz`;月度 → `month_<yyyy-mm>.tar.gz`;年度 `year_*.tar.gz`;
|
||||
raw 文件正则 `^homed_(\d{4}-\d{2}-\d{2})_\d{2}-\d{2}-\d{2}\.log$`(`compressor.go:16`)。
|
||||
- 保留策略:`core.log.retention`(default forever)、`core.log.retention_months`(default 3),
|
||||
`applyRetention` 只留当前周 + 近 N 月(`retention.go`)。
|
||||
实测:`log/` 下即为 `homed_2026-08-03_08-03-38.log` + `month_2026-*.tar.gz` + `week_2026-W31.tar.gz`,与代码一致。
|
||||
|
||||
### 2.3 工具调用日志打在哪儿(进程主循环 `internal/agent/core/process.go`)
|
||||
|
||||
| 位置 | 日志行内容 | 备注 |
|
||||
|---|---|---|
|
||||
| `process.go:36` | `[agent] tool call loop start, max_ctx=… target=… fixed=… mem=… ctx=… N tools, M events, personality=X, docs=K` | 每轮循环开头上下文统计 |
|
||||
| `process.go:196` | `[agent] executing tool: <name> (plugin=<p>, id=<id>)` | **只记工具名/插件/id,不记 args** |
|
||||
| `process.go:226` | `[agent] tool <name> result: <截断100字符>` | 结果截断到 100 字符(`truncateStr`)|
|
||||
| `process.go:218` | `[agent] skip tool <name>: plugin <name> unhealthy` | 插件崩溃态跳过 |
|
||||
| `process.go:89/109/121/125` | LLM fallback:`trying provider %q (#%d)` / `switched active provider` / `provider %q marked unavailable (HTTP %d)` / `provider %q failed` | 主循环内 LLM 商可观测 |
|
||||
| `toolcall.go:20` | `[agent] tool %s panic: %v` + `debug.Stack()` | 工具 panic + 完整栈 |
|
||||
| `toolcall.go:41` | `[agent] tool %s timed out after 60s` | 60s 超时 |
|
||||
| `toolcall.go:92` | `[agent] tool %s changed %d files (changeset: %s)` | overlay changeset 摘要 |
|
||||
| 插件侧 | Lua 插件 `sdk.log` → `print("[lua-plugin] <level>: <msg>")` | 模板见 `cmd_debug.go:74`/`templates.go` |
|
||||
|
||||
- 完整的工具**入参/结果**在 EventBus 事件 `EventToolCall`(`{tool, plugin, args, result, status}`,`process.go:184/205`)而非文件日志——**文件日志只是执行/结果的摘要指针**(结果被截断)。
|
||||
- 重要观察:日志里未见 shell/cmd 之类的操作系统执行类调用摘要落盘(`[cmd]` 只在工具结果里),
|
||||
需要的话由 `diag_log_scan` 对 `executing tool: cmd_*` 前缀做签名匹配即可。
|
||||
|
||||
### 2.4 崩溃 / 重启观察
|
||||
|
||||
- `NRestarts=0`;MainPID 自 08-03 起稳定 3330844。曾出现**真实重复 panic**(pid 3310036):
|
||||
`[stage] handler panic: runtime error: invalid memory address or nil pointer dereference`(03:23 / 05:23 / 07:23,约每 2h),
|
||||
被 `stages.go:139` 的 `RunStage` recover 吞掉 → **进程未真崩**,systemd 未见重启。
|
||||
- 08:03:38 有过一次干净 `[homed] stopped` → systemd `Started` → pid 3310036 → 3330844。
|
||||
- 对 failback 的意义:现有崩溃防护全赖 **in-process recover**,真实进程级崩溃从未被监督;
|
||||
且当前 `Restart=always` 由 systemd **直绑 worker 且无 StartLimit**——一旦真崩并陷入循环,
|
||||
systemd 每 10s 反复拉起,没有独立 failback/取证层。→ guard 取代点在此。
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计演进(讨论全过程)
|
||||
|
||||
### 3.0 起点:`internal/supervisor` + `internal/tracker` 我是"保活 + 文件追踪"
|
||||
|
||||
- 保活 = 网络健康感知 + 内存态重置;追踪 = overlayfs 变更集 + `Rollback` 清空。
|
||||
- 意图:LLM 不可达 → 回滚 agent 文件改动 → 自愈。
|
||||
|
||||
### 3.1 第一次纠正:回滚对象错位
|
||||
|
||||
- 回滚只作用于 overlayfs upper;而真正能改坏网络的路径(files 默认 `/`,可写 `/etc/resolv.conf`、`/etc/hosts`、代理配置)与 LLM 配置(config.db、adapters 目录)**都不在 overlay 内**。
|
||||
- → 检测命中但回滚删错对象,闭环在"回滚"这一环断掉。
|
||||
|
||||
### 3.2 第二次纠正:授权不能靠外部插件主动配合
|
||||
|
||||
- 阶段管道是被动通知(`RunStage` 收集 error,不主动拒绝),拒绝依赖各插件 handler。
|
||||
- 插件是外部不可控对象 → "安全 = 插件主动授权放行"不成立。
|
||||
- → 安全应做**默认拒绝**,由内核在 `executeToolCall` 分发点按 ToolDef 的 capability 裁决;配置快照 + replug 只兜底"可文件化"改动。
|
||||
- 中途又修正:真正的祸首不是 IO 组件,而是 LLM 源/系统网络配置;IO 组件崩溃是正交偶发轴,不并进来。
|
||||
|
||||
### 3.3 第三次纠正:LLM 源才是核心(config.db + ReloadFromConfig 已具钩子)
|
||||
|
||||
- `llm_set_source` 只切内存默认;真正改坏 LLM 源靠 `cfgmgr.config_set` 写 config.db `core.llm.sources.*`。
|
||||
- `ReloadFromConfig()` 已能"恢复即生效" → 只需补:**LLM 配置快照 + 真实 liveness 探测(QuickChat)+ 在 handleFailure 里触发 RFO 复检**。
|
||||
- 但用户进一步点明:**重点是 agent 改了系统网络配置文件(proxy/DNS)** —— 这类连救援 LLM 都连不上。
|
||||
|
||||
### 3.4 定稿架构一:lastFailback(独立进程,第一道防线)
|
||||
|
||||
- 独立进程 + agent 碰不到的 root:0600 配置 + 最小插件集(文件读写 + cmd)+ 单一任务。
|
||||
- 用**锚定 IP/干净 DNS** 的 LLM 源绕开坏掉的 DNS/proxy。
|
||||
- 若 failback 也失败 → agentfs 回滚最近快照 + 重启计算机(应用快照前系统配置)。
|
||||
- 兼顾 systemd 托管冲突(主 agent 崩溃循环被 systemd 反复拉起 → failback 无限增殖)。
|
||||
|
||||
### 3.5 定稿架构二:去掉强 systemd 依赖 → 独立 guard 守护进程
|
||||
|
||||
- systemd 只绑定**静态、极小、永驻的 guard**;guard 作为父进程拉起 worker(`homed` 全量 agent)。
|
||||
- guard 通过 IPC(unix socket / 匿名 pipe)收 worker 心跳;
|
||||
父进程 `waitpid` 天然 reap 崩溃/OOM/信号退出。
|
||||
- worker 可崩可换,systemd 完全看不见 → **failback 唯一性 + 无风暴天然成立**。
|
||||
- guard 抗 OOM:`oom_score_adj` 拉低,优先杀 worker 而非 guard。
|
||||
|
||||
### 3.6 定稿架构三:更纯粹 —— 同一二进制的两个 role,非独立二进制
|
||||
|
||||
- **不新建二进制**。`homed` 拆两个入口:
|
||||
- `homed --role=guard`:父守护进程,先起,负责拉起/监测/探活/裁决/恢复。
|
||||
- `homed --role=agent`:主 agent(工作进程,guard 的子进程)。
|
||||
- `homed --role=agent --boot=failback`:恢复用 agent(受限 bootstrap)。
|
||||
- guard 复用现有 homeagent 基础设置;检测到崩溃时拉起 failback agent,只加载
|
||||
**webfetch + 文件读写 + cmd** 三个插件,外加 **恢复知识库插件** 与 **常见错误检测插件**,
|
||||
复用 agent 核心以 `trigger_prompt` 初始化,要求其"尝试恢复主 agent"。
|
||||
- guard 配置**独立 YAML**,不复用 config.db(逃生舱知识必须脱离 agent 可达区)。
|
||||
|
||||
### 3.7 recoveryDiag:崩溃取证 / 根因定位插件(省 token 关键)
|
||||
|
||||
- 铁律:**工具返回结论,不返回原文**(签名式/统计式/确定性排序)。
|
||||
- 工具集:
|
||||
| 工具 | 作用 |
|
||||
|---|---|
|
||||
| `diag_triage` | exit code/信号+uptime+头部嫌疑,快速粗分"进程死亡 vs 配置类不可达" |
|
||||
| `diag_db` | config.db integrity_check + LLM 源解析校验,逐项 ok/fail |
|
||||
| `diag_log_scan` | 时间窗内命中已知错误签名(panic/provider failed/unreachable/sql/OOM)|
|
||||
| `diag_delta` | 崩溃前 config/agentfs 与 last-good 快照 diff("改了什么")|
|
||||
| `diag_loc` | 综合正交,输出按因果强度排序的定位结论 + 推荐动作 |
|
||||
|
||||
- 崩溃类别 → 恢复分支决策表:
|
||||
| 结论类 | 走分支 |
|
||||
|---|---|
|
||||
| 配置损坏类 | 还原 config 快照 + ReloadFromConfig + 拉活主 agent(无需 agent 推理)|
|
||||
| 系统网络类 | 还原 DNS/proxy → 重载主 agent(第一步小修命中即停)|
|
||||
| 进程失稳类(OOM/panic)| 不还原配置,检查内存/泄漏 → 重建 worker |
|
||||
| 未知/混合 | 放开 webfetch/知识库,用 rescue 源 + diag_loc 摘要最小推理 |
|
||||
|
||||
- 只有"未知/混合"消耗 token,前几类近乎 0 token。
|
||||
- 结论落盘 `recovery_kb/diag_<ts>.json`,回流知识库,同类崩溃下次直接命中,越用越省。
|
||||
|
||||
### 3.8 三层防御总览(最终)
|
||||
|
||||
```
|
||||
L0 平时:核心只读探活 + 写前快照(config_set 写 core.llm.* 前、files 写 /etc 前自动留档)
|
||||
L1 failback(guard 拉起,Safe-Mode 式兜底):按诊断分支逐类恢复——
|
||||
还原 DNS/proxy → 还原 config 快照 + ReloadFromConfig → QuickChat 复检 → 拉起主 agent
|
||||
L2 最后手段:agentfs 回滚最近快照 + 重启(应用快照前系统配置)
|
||||
```
|
||||
|
||||
- failback 是**所有错误(LLM 不可达 / 网络 / 配置损坏 / OOM / panic / 崩溃循环)的统一兜底层**,
|
||||
并非只针对某一条;`diag_*` 决定它走哪条恢复路径。
|
||||
|
||||
---
|
||||
|
||||
## 4. 最终架构(定稿)
|
||||
|
||||
### 4.1 进程拓扑(同一二进制,两个 role)
|
||||
|
||||
```
|
||||
systemd ──▶ homed --role=guard # 父守护进程,永驻、静态、极小
|
||||
├─ exec ──▶ homed --role=agent # 主 agent(可崩)
|
||||
└─ exec ──▶ homed --role=agent --boot=failback # 恢复用 agent
|
||||
```
|
||||
|
||||
- guard:先起,持有恢复知识(锚定源 / DNS/proxy 还原 / 配置快照 / failback 逻辑)。
|
||||
- worker:guard 子进程,心跳经 IPC,崩溃由 guard reap + 判型。
|
||||
- failback agent = 受限启动(webfetch+files+cmd + 恢复知识库 + recoveryDiag),单一任务"恢复主 agent",N 轮有界。
|
||||
|
||||
### 4.2 guard 独立 YAML 示例
|
||||
|
||||
> 现状实现(§5 已完成):`guard.yaml` 已落地为 `max_restarts / heartbeat_timeout / heartbeat_interval / llm_snapshot / failback_enabled / last_resort / restart_command / reboot_grace` 子集(`cmd/homed/guard.go`),恢复梯子=重试→LLM 基线恢复→failback 受限启动→last_resort。下表的 rescue 源 / trigger_prompt / N 轮 failback 推理是目标态,未实现。
|
||||
|
||||
```yaml
|
||||
role: guard
|
||||
|
||||
llm:
|
||||
sources:
|
||||
- name: rescue
|
||||
base_url: http://1.2.3.4:8080 # 锚定 IP 直连,绕开被破坏的 DNS/代理
|
||||
api_key: ${GUARD_RESCUE_KEY}
|
||||
adapter: ... # 锚定/SNI 型适配器
|
||||
|
||||
recovery:
|
||||
max_attempts: 4 # 可配置尝试轮次
|
||||
attempt_timeout: 120s
|
||||
knowledge_base: /opt/homeagent/recovery/
|
||||
trigger_prompt: "你是恢复 agent,唯一任务:让主 agent 恢复运行。优先还原 DNS/代理,再重载 LLM 源…"
|
||||
plugins: [webfetch, files, cmd]
|
||||
|
||||
last_resort:
|
||||
action: reboot # restart_app | reboot
|
||||
snapshot_before: true
|
||||
```
|
||||
|
||||
### 4.3 guard 恢复状态机(N 轮有界)
|
||||
|
||||
```
|
||||
guard 检测( exit≠0 | OOM | 心跳超时 | guard 锚定源探活失败 )
|
||||
1. 固化追溯:exit/信号、panic、journal、OOM 上下文 → 永久区
|
||||
2. 拉起 failback agent(受限插件 + rescue 源 + trigger_prompt + 知识库 + recoveryDiag)
|
||||
for attempt in 1..N:
|
||||
(可选先 diag_triage/diag_loc 判型)
|
||||
failback 尝试恢复
|
||||
guard 每轮复检主 agent 是否可达/存活
|
||||
├─ 成功 → 结束,交回主 agent
|
||||
└─ 超时/失败 → kill 重建,进入下一轮
|
||||
3. N 轮未成 → 取消 failback agent
|
||||
→ agentfs 回滚崩溃前最近快照
|
||||
→ 依 yaml 执行最后手段:restart_app 或 reboot
|
||||
```
|
||||
|
||||
### 4.4 systemd 绑定(极简,杜绝风暴)
|
||||
|
||||
```
|
||||
[Unit] # guard
|
||||
OnFailure=... # 备用,通常不触发(guard 稳定)
|
||||
|
||||
[Service] # guard
|
||||
Restart=always # guard 静态稳定 → 几乎不重启
|
||||
ExecStart=/usr/local/bin/homed --role=guard ...
|
||||
# No StartLimit needed for loop 情况;guard 不崩
|
||||
```
|
||||
|
||||
- 主 agent 崩 → 只触发 guard 内部 failback;systemd 仅看 guard,看不到 worker 崩溃循环。
|
||||
- failback 唯一性 + 无启动风暴:由"guard 永驻、唯一裁决"天然保证。
|
||||
- guard 抗 OOM:`oom_score_adj` 拉低。
|
||||
|
||||
---
|
||||
|
||||
## 5. 尚未落地的接口 / 下一步
|
||||
|
||||
**已完成**:
|
||||
|
||||
`recoverydiag` 快速检查插件(`third_party/homeagent-sdk/example/recoverydiag/`,外部插件)。
|
||||
- 五件套全实现:`diag_triage`(退出码/信号/存活粗分)、`diag_db`(config.db integrity_check + LLM 源字段校验,sqlite3 CLI 优先、缺失回退内核 Settings)、`diag_log_scan`(日志签名按类计数)、`diag_delta`(baseline vs 现状 diff)、`diag_loc`(四项结论正交排序 + 推荐恢复动作)。
|
||||
- 全部确定性、返回结论非原文、`NoMemory`;工具实际名带插件前缀 `recoverydiag_diag_*`。
|
||||
- 已通过 go vet + 6 个单测(对真实 config.db/日志跑通:3 个 LLM 源全 ok、日志命中 228 行主导 provider/fatal),并用**仓库内重建的 plugindev** 打出 `dist/recovery_diagnostics_linux_amd64.hmap`,装进运行实例(`/home/newqqagent/plugins/recoverydiag/`)加载成功、注册 5 工具。
|
||||
- **结论落盘 + 知识库回流**:`diag_loc` 增 `persist`(缺省 true)→ 写 `<data_dir>/recovery_kb/diag_<ts>.json`(可配 `recovery_kb_dir`),并经 `sdk.Knowledge().Add` 以 `diag:<cause>:<ts>` 回流知识库(同类崩溃下次直接命中,越用越省);失败不阻塞工具。新增 `TestDiagLocPersist`。
|
||||
- 顺带修复:仓库内 `plugindev` 需重编译(`/usr/local/bin/plugindev` 是旧版、桥模板缺 `InjectInputSync`);重编译见 `third_party/homeagent-sdk/tools/plugindev`,`go build -o ... .`。
|
||||
- 注意:本环境 `snapshots/`、`changesets/` 均为空(direct 模式无基线)→ `diag_delta` 需显式传入 baseline_dir;未来接 guard 时由快照解包目录提供。
|
||||
|
||||
`ConfigRegistry` 快照钩子(`internal/config/registry.go`)。
|
||||
- `SnapshotCoreLLM()`:抓全部 `core.llm.*` 键值快照;`RestoreCoreLLM(snap)`:精确还原(快照内键回写、快照外当前键删除)。
|
||||
- `SetLLMSnapshotFile(path)`:写前自动留档——此后任意写 `core.llm.*` 键先把当前 LLM 配置整体快照到该文件(guard 恢复的外部基线);homed 启动即挂 `<data>/llm_snapshot.json`。
|
||||
- 文件持久化对:`SaveLLMSnapshot/LoadLLMSnapshot`。新增 `TestSnapshotRestoreCoreLLM`、`TestLLMSnapshotFile`、`TestSetLLMSnapshotFile`。
|
||||
|
||||
`homed --role{guard,agent}` 入口拆分 + `--boot=failback` 受限插件集(`cmd/homed/`)。
|
||||
- `--role=guard` 父守护(永驻):读独立 `<data>/guard.yaml`(避开被改坏的 config.db),拉起 worker(`--role=agent`)、心跳探活 + waitpid 收割、信号转发停机。
|
||||
- `--role=agent` 工作进程:默认启动全插件;`--boot=failback` 走插件白名单(`core.agent.failback_plugins`,缺省 `webui,pluginmgr,recoverydiag`),内核 webfetch/files/cmd 仍内置可用。
|
||||
- guard 恢复梯子(已端到端实测):连续 `max_restarts` 次 normal 崩溃 → `restoreLLMBaseline`(从 llm_snapshot.json 恢复 core.llm.*)→ failback 受限启动 → failback 也崩 → `last_resort`(restart_app / reboot)。
|
||||
- 心跳:worker 每 5s 触碰 `<data>/heartbeat`(agent 角色 goroutine),guard 以 mtime 判定卡死(超 `heartbeat_timeout` 即 SIGKILL 计入崩溃)。
|
||||
- 插件注册表加 `SetLoadAllowlist(names)`:白名单外插件(含已注册工厂)一律跳过,failback 40 工具 → 4 工具实测通过。
|
||||
|
||||
现状 bug 修复(supervisor/network/tracker)。
|
||||
|
||||
- **monitor 无 endpoint 恒 healthy**(`internal/network/monitor.go` + `pkg/types`):`NetworkCheckResult` 增 `EndpointsConfigured`;无探活端点时不再谎报 `LLMAPIReachable=true`(置 false + Error),`NewMonitor` 初始化空切片消除启动竞态;daemon 仅在配置了端点时才据此判定降级。探活端点新增 `core.defaults.llm_endpoints`(逗号分隔,留空自动取 LLM 源 base_url),生产从此健康检查有真实目标。
|
||||
- **lastHB 恒置 now**(`internal/supervisor/daemon.go`):`checkAgent` 接入真实存活源 `SetHeartbeatSource`(homed 注册为 agent core `GetKernelStatus`),只在确认 agent 存活时更新 `lastHB`;无源置 `HealthUnknown`,存活源丢失置 `HealthDown` 且不再刷新 lastHB。
|
||||
- **restartAgent 只改内存空转**:增 `SetRestartHandler`(homed 注册为"清理后以 `exitRestartRequested=42` 退出"),不再假装成功;guard 把 42 识别为"请求重建"(`workerRestartRequested`,不计失败轮次直接重建),无 guard 时 systemd `Restart=always` 兜底。无 handler 时仅内存复位并打日志。
|
||||
- **tracker 三缺陷**(`internal/tracker/`):`captureFSStateWithContent` 为 before 基线捕获原文(上限 8MB)→ `diffStates` 对 modified/deleted 回填 `FileChange.Content`(回滚用原文);新增 `RollbackLatest()` 定向撤销最近一条 changeset;`Rollback()` 改为按时间逆序逐条逆应用(还原被改/被删文件原文、删除新增),无 changeset 时才退回整目录重置。新增 6 个测试覆盖。
|
||||
|
||||
剩余:
|
||||
|
||||
- guard ↔ agent 心跳 IPC 升级为带自诊断上报的 `PING/ACK`(当前为文件心跳 + 退出码)。
|
||||
- supervisor 适配成 guard 的探测/裁决逻辑;`ReloadFromConfig()` 复用为"恢复即生效"。
|
||||
- 生产实例迁移:编译新 homed、改 systemd 只托管 guard(`--role=guard`),确认 failback 插件(recoverydiag)就位。
|
||||
|
||||
---
|
||||
|
||||
## 附录:真实日志节选(systemd 托管示例,`/home/newqqagent`)
|
||||
|
||||
```
|
||||
# systemd unit
|
||||
homeagent.service: Type=simple, ExecStart=/usr/local/bin/homed -data /home/newqqagent, Restart=always, RestartSec=10
|
||||
llm-mock.service: ExecStart=/usr/bin/python3 /opt/llm-mock/mock_server.py, Restart=always, RestartSec=3
|
||||
|
||||
# 日志文件与格式(log.Printf 标准格式)
|
||||
2026/08/03 08:03:38 main.go:80: [homed] logging to /home/newqqagent/log/homed_2026-08-03_08-03-38.log
|
||||
2026/08/03 08:03:38 daemon.go:61: [homed] daemon started successfully
|
||||
2026/08/03 08:03:39 tracker.go:65: [tracker] initialized (work=/home/newqqagent/agentfs)
|
||||
|
||||
# 工具调用摘要(process.go)
|
||||
2026/08/03 10:03:52 process.go:36: [agent] tool call loop start, max_ctx=32768 target=26214 fixed=1306 mem=8302 ctx=16606 176 tools, 31 events, personality=true, docs=5589
|
||||
... process.go:196: [agent] executing tool: <name> (plugin=<p>, id=<id>)
|
||||
... process.go:226: [agent] tool <name> result: <截断100字符>
|
||||
|
||||
# 启动 & 沙箱
|
||||
homed[3330844]: [files] started, sandbox: /
|
||||
homed[3330844]: main agent started, model=mock-model base=http://127.0.0.1:18080/v1 sources=3 adapters=8
|
||||
|
||||
# 重复 in-process panic(被 RunStage recover 吞掉,未进程级崩溃)
|
||||
homed[3310036]: [stage] handler panic: runtime error: invalid memory address or nil pointer dereference # 03:23 / 05:23 / 07:23
|
||||
|
||||
# 一次性干净重启(systemd 手动/触发 Started,pid 3310036 → 3330844)
|
||||
homed[3310036]: [homed] stopped
|
||||
systemd[1]: Stopped homeagent.service - HomeAgent - 24/7 AI Butler.
|
||||
systemd[1]: Started homeagent.service - HomeAgent - 24/7 AI Butler.
|
||||
|
||||
# 运行状态
|
||||
systemctl show homeagent.service -p NRestarts → 0
|
||||
systemctl show homeagent.service -p MainPID → 3330844(自 08-03 起稳定)
|
||||
|
||||
# 归档
|
||||
/home/newqqagent/log/: homed_2026-08-03_08-03-38.log + week_2026-W31.tar.gz + month_2026-*.tar.gz
|
||||
```
|
||||
@ -1,248 +0,0 @@
|
||||
# QQ `output_send` 回声/无限循环(核心侧缺陷) QQ `output_send` 回声/无限循环(核心侧缺陷,非插件)
|
||||
|
||||
> 状态:**已修复**(核心已具备轮次上限:`core.agent.max_tool_turns`,默认 10,
|
||||
> 在 `internal/agent/core/task.go` 到达上限即强制收尾;回归测试
|
||||
> `TestMaxToolTurns_CapsRunawayLoop`。本文保留为缺陷定位过程记录。)
|
||||
>
|
||||
> 原始状态(修复前):**待修复**
|
||||
> 影响:Agent 单轮内每 ~10 秒调用一次 `output_send__qq`,持续数十分钟不结束(实测单轮 `1780624ms`,80+ 次工具调用)
|
||||
> 定位结论:**问题在核心(富回执 + 每轮重复追加同一条“继续”占位 + 无轮次上限),QQ 插件侧已是最小回执,改插件无效**
|
||||
|
||||
---
|
||||
|
||||
## 1. 现象
|
||||
|
||||
生产日志(`/home/newqqagent`,homed 运行期):
|
||||
|
||||
```
|
||||
18:31:39 process.go:299: [agent] tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]
|
||||
18:31:47 process.go:299: [agent] tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]
|
||||
18:31:58 process.go:299: [agent] tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]
|
||||
18:32:06 ...
|
||||
18:32:14 ...
|
||||
(每 8~12 秒一条,payload 长度各异:387/237/246/203/252/270/254/231/258/227/188/227/200/209/155/284/212/173/198/242/218/191…)
|
||||
18:31:19 eventloop.go:426: [agent] text from qq → response (1780624ms, tools=[... 80+ 项 ...])
|
||||
```
|
||||
|
||||
- 每条内容**都不同**,所以“相同参数才拦”的插件保险不会触发。
|
||||
- 不是 webhook 回声(15 分钟内只有 1 条真实入站中断)。
|
||||
- 是模型每轮都收到“发送成功的富回执”,把它当成“继续下一步”的信号。
|
||||
|
||||
---
|
||||
|
||||
## 2. 根因链(核心侧,三层)
|
||||
|
||||
### 2.1 QQ 插件已经返回最小回执 —— 但被核心丢弃
|
||||
|
||||
`third_party/homeagent-sdk/example/qq/plugin.go`(`handleChannelOutput` 尾部):
|
||||
|
||||
```go
|
||||
if sendErr != nil {
|
||||
return nil, sendErr
|
||||
}
|
||||
// 成功:返回极简标记。不再回传 NapCat 原始响应(含 message_id 等)给模型,
|
||||
// 避免模型把"发送成功"当成"上一步完成,继续下一步"的信号驱动循环。
|
||||
return "ok", nil
|
||||
```
|
||||
|
||||
插件返回的是字符串 `"ok"`。
|
||||
|
||||
### 2.2 核心 proc 桥丢弃它并伪造 `status:sent`
|
||||
|
||||
`internal/plugin/proc/plugin.go:336`(`(*Plugin).invokeOutput`):
|
||||
|
||||
```go
|
||||
raw, err := p.proc.Call(MethodOutputInvoke, OutputInvokeParams{Channel: channel, Args: args})
|
||||
if err != nil { return nil, err }
|
||||
if len(raw) == 0 {
|
||||
return map[string]interface{}{"status": "sent"}, nil
|
||||
}
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &res); err != nil {
|
||||
return map[string]interface{}{"status": "sent"}, nil // ← "ok" 不是 JSON object,落到这里
|
||||
}
|
||||
if _, ok := res["status"]; !ok {
|
||||
res["status"] = "sent" // ← 再兜底
|
||||
}
|
||||
return res, nil
|
||||
```
|
||||
|
||||
插件返回 `"ok"` → `json.Unmarshal` 进 `map[string]interface{}` 失败 → 核心合成 `{status: sent}`。
|
||||
**插件的返回值在这里被完全覆盖,所以只改插件永远修不掉回声。**
|
||||
|
||||
### 2.3 核心把这个富回执喂给模型
|
||||
|
||||
`internal/agent/core/output.go:81`(HEAD / 部署中的 homed 行为):
|
||||
|
||||
```go
|
||||
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
|
||||
// → "已通过 [qq] 通道发送: map[status:sent]"
|
||||
```
|
||||
|
||||
模型看到“发送成功 + 详情”后继续调用 `output_send__qq`,形成闭环。
|
||||
|
||||
### 2.4 核心没有工具轮次硬上限(放大器)
|
||||
|
||||
`core.agent.max_tool_turns` 只在配置层定义,**agent 循环里没有任何读取点**:
|
||||
|
||||
```
|
||||
internal/config/registry.go:551 set("core.agent.max_tool_turns", "10")
|
||||
internal/config/registry.go:669 reg(ConfigDef{Key: "core.agent.max_tool_turns", ...})
|
||||
$ grep -rn 'max_tool_turns\|MaxToolTurns' internal/agent/ → 无结果
|
||||
```
|
||||
|
||||
`internal/agent/core/process.go:242` 的唯一终止条件是:
|
||||
|
||||
```go
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
return resp.Content, toolsUsed, toolResults, nil
|
||||
}
|
||||
```
|
||||
|
||||
即:**模型不主动停,循环就永不结束**。`core.agent.max_tool_turns`(本机 DB 现为 `1000`)形同虚设。
|
||||
|
||||
### 2.5 每轮重复追加同一条 user 占位(“反复喂相同消息”的直接来源)
|
||||
|
||||
`internal/agent/core/process.go:77-82`,位置在 `for turn := 0; ; turn++` 循环的**顶部**:
|
||||
|
||||
```go
|
||||
for turn := 0; ; turn++ {
|
||||
for _, interrupt := range a.drainInterrupts() { ... }
|
||||
|
||||
// 工具轮产出的 tool/assistant 消息作结尾会被 400 拒绝,故补一条 user 占位。
|
||||
if last := msgs[len(msgs)-1]; last.Role == "assistant" || last.Role == "tool" {
|
||||
msgs = append(msgs, agentAPI.Message{ // ← process.go:79
|
||||
Role: "user",
|
||||
Content: "请根据以上工具结果继续。",
|
||||
})
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`msgs` 于 `process.go:32` 在循环**外**创建,循环内只增不减:
|
||||
|
||||
- 每轮工具调用结束后,`msgs` 尾部必然是 `tool` 消息;
|
||||
- 下一轮顶部判断成立,于是**再追加一条完全相同的** `请根据以上工具结果继续。`;
|
||||
- 不做替换、不做去重、不做裁剪(`ContextPolicy: prune` 只裁剪 `a.context`,不裁剪 `msgs`)。
|
||||
|
||||
跑 N 轮,模型收到的 prompt 里就叠了 N 条一模一样的“继续”指令。这才是“核心把前面相同消息反复喂给模型”的直接机制,也是把模型持续推向 `output_send` 的持续推力。
|
||||
|
||||
**预期行为**:占位消息应当(a)仅在没有尾部 user 消息时补一条,或(b)补之前先移除上一条同类占位,保持至多一条;绝不能线性累积。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现有未完成/未部署的修复
|
||||
|
||||
| 文件 | 状态 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| `internal/agent/core/output.go:81` | **已改,未提交** (`M`) | `return fmt.Sprintf("已通过 [%s] 通道发送: %v", ...)` → `return "ok"`(含解释回声的注释) |
|
||||
| `internal/plugin/proc/plugin.go:336` | **已改,未提交** (`M`) | 仍是伪造 `status:sent` 的版本,未处理非 map 返回值 |
|
||||
| `third_party/homeagent-sdk/example/qq/plugin.go` | **已改,未提交** (`M`) | `handleChannelOutput` 返回 `"ok"` |
|
||||
|
||||
运行中的 `homed` 是 **Sep 6 11:39** 构建的二进制,不含 `output.go` 的极简回执改动 → 仍回显富回执。
|
||||
另外该二进制用旧 SDK 协议(`shmMagic` 直连,无 `unifiedMagic`),而 `third_party/homeagent-sdk` 仓库 HEAD 已升级到统一区域协议(`fc23612` 起)。**重建并部署 homed 时二者必须对齐**(见 §5)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议修复
|
||||
|
||||
### 4.1 (必须)让模型只看到最小回执
|
||||
|
||||
**方案 A(最小改动,已在工作区)**:`internal/agent/core/output.go`
|
||||
|
||||
```go
|
||||
// internal/agent/core/output.go:81
|
||||
// 成功回执:只返回极简标记,不回传完整插件响应。
|
||||
// 「已通过 [qq] 通道发送: map[status:sent message_id:xxx]」这类富回执
|
||||
// 会驱动模型继续调用 output_send(回声效应),是 output loop 的根源之一。
|
||||
return "ok"
|
||||
```
|
||||
|
||||
**方案 B(同时修掉 proc 桥的伪造)**:`internal/plugin/proc/plugin.go:336`
|
||||
不要对非 map 结果伪造 `status:sent`,保留插件真实返回;例如:
|
||||
|
||||
```go
|
||||
if len(raw) == 0 {
|
||||
return map[string]interface{}{"status": "ok"}, nil
|
||||
}
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &res); err != nil {
|
||||
// 插件返回的是标量(如 "ok")——原样透传,不要伪造 status
|
||||
var scalar interface{}
|
||||
if err2 := json.Unmarshal(raw, &scalar); err2 == nil {
|
||||
return scalar, nil
|
||||
}
|
||||
return map[string]interface{}{"status": "ok"}, nil
|
||||
}
|
||||
```
|
||||
|
||||
注意:`output.go` 仍需要 `status == "unconfirmed"/"queued"` 的判定,改成标量透传时该判定自然跳过(非 map),语义正确。
|
||||
|
||||
### 4.2 (必须)工具循环硬上限
|
||||
|
||||
在 `internal/agent/core/process.go` 的工具循环里读取并强制 `core.agent.max_tool_turns`:
|
||||
|
||||
- 位置:`for turn := 0; ; turn++ {` 循环内,执行工具前/每轮结束后检查。
|
||||
- 语义:达到上限时追加一条系统消息(如 `[系统] 已达到最大工具轮次 N,请立即总结并停止调用工具`),并终止循环返回当前内容,而不是继续下一轮。
|
||||
- 至少要在 `turn > maxTurns` 时强制 `break`,避免模型不停调用。
|
||||
|
||||
### 4.3 (建议)QQ 插件侧保持最小回执
|
||||
|
||||
`third_party/homeagent-sdk/example/qq/plugin.go` 的 `return "ok", nil` 是正确的,保留即可。
|
||||
**不要**再依赖插件侧修这个回声——见 §2.2。
|
||||
|
||||
### 4.4 (必须)修掉每轮重复追加的 user 占位
|
||||
|
||||
`internal/agent/core/process.go:77-82`。改为“至多保留一条”,例如:
|
||||
|
||||
```go
|
||||
// 只在尾部是工具轮产物时补位;先移除上一条同类占位,避免线性累积。
|
||||
if last := msgs[len(msgs)-1]; last.Role == "assistant" || last.Role == "tool" {
|
||||
// 若尾部之上已经存在一条我们自己的占位,就不要重复追加。
|
||||
if !isContinuationPlaceholder(msgs[len(msgs)-1]) {
|
||||
msgs = append(msgs, agentAPI.Message{
|
||||
Role: "user",
|
||||
Content: continuationPlaceholder,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
更稳妥的写法:在追加前从 `msgs` 尾部回扫,删除所有此前由本机制插入的占位,再追加一条。判定不要只靠字符串相等,建议给占位加一个可识别标记(例如 `internal:continuation`)或单独的 `NoMemory/Role` 约定,避免误删真实用户消息。
|
||||
|
||||
同时建议给 `msgs` 加长度/ token 上限(或定期裁剪历史),防止长任务把上下文堆爆(这正是 §2.4 无轮次上限的伴生问题)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 部署前提与步骤
|
||||
|
||||
> ⚠️ 部署 homed 前必须先对齐 SDK 协议,否则所有子进程插件握手失败(`统一区域魔数不匹配`)。
|
||||
|
||||
1. **确认工具链协议与要部署的 homed 一致**
|
||||
- 现状:`/usr/local/bin/homed` = 旧协议;`/usr/local/bin/plugindev` 已替换为旧协议版本(备份 `/usr/local/bin/plugindev.bak-20260910-174547`)。
|
||||
- 若决定升级到统一区域协议,则需同时:升级 homed 二进制 + 用新 SDK(`third_party/homeagent-sdk` HEAD)重建全部插件。
|
||||
- 若维持旧协议:用 `/usr/local/bin/plugindev`(旧)重建插件即可,不要用仓库 HEAD 的 `tools/plugindev` 直接 `go run`。
|
||||
2. **构建 homed**:`go build ./...` 已验证通过;产出替换 `/usr/local/bin/homed`(按项目部署纪律:备份 → 原子替换)。
|
||||
3. **重启**:`systemctl restart homeagent.service`
|
||||
4. **重建受影响的子进程插件**(协议一致时):至少 `qq`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
修复后,发一条会触发回复的 QQ 消息,应满足:
|
||||
|
||||
1. 日志中 `output_send__qq` 的 tool result **不再包含** `已通过 [qq] 通道发送: map[status:sent]`;
|
||||
2. 单轮只发送 1 条(或模型明确决定的多条**不同**消息),**不出现每 ~10 秒一次的持续调用**;
|
||||
3. 当模型异常地持续调用工具时,日志出现达到 `core.agent.max_tool_turns` 的终止记录,且该轮在有限步内结束;
|
||||
4. `eventloop.go:426` 的 `text from qq → response` 耗时应回落到正常量级(秒级~分钟级),不再是 30 分钟;
|
||||
5. 抓取发往上游的请求(或用调试钩子 dump `req.Messages`),确认 `请根据以上工具结果继续。` 在整轮 prompt 中**至多出现一次**;修复前应为 N 条(N=轮数),这正是 §2.5 的判据。
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关背景(避免误修)
|
||||
|
||||
- QQ 插件的 `beforeToolcall` 循环保险只拦“参数完全相同的重复调用”(`max_duplicate_qq_send`),**拦不住内容各异的循环**;本次循环每条内容都不同,所以保险未触发。这是设计使然,不是 bug。
|
||||
- 15 分钟内仅 1 条真实 QQ 入站中断,说明**不是** webhook 把出站消息当入站回灌,**不是**插件回声。
|
||||
- `internal/plugin/proc/plugin.go:122` 的 `invokeCleaner` 签名不匹配(此前导致 `go build` 失败)**已被修复**,当前 `go build ./...` 通过。
|
||||
@ -1,124 +0,0 @@
|
||||
# 检索方案对比报告(2026-09-09)
|
||||
## 测试数据
|
||||
- 文档库:492 篇生产文档(过滤 108 条健康检查测试文档)
|
||||
- 媒体库:3 张生产图片(验证码、新闻截图、深色模式备忘录)
|
||||
- 文本查询:10 组(精确匹配、语义、跨语言、模糊表达)
|
||||
- 媒体查询:6 组(中文/英文查图片,3 张图片各 2 条)
|
||||
|
||||
---
|
||||
|
||||
## 一、文本检索对比(文档库)
|
||||
|
||||
| 方案 | Hit@1 | Hit@5 | MRR | 平均延迟 |
|
||||
|------|-------|-------|-----|----------|
|
||||
| TF-IDF | 3/10 | 7/10 | 0.457 | 0.3ms |
|
||||
| fastText(200k 中文+378k 英文) | 5/10 | 5/10 | 0.530 | 8.3ms |
|
||||
| TF-IDF + fastText RRF | 4/10 | 7/10 | 0.552 | 12.3ms |
|
||||
| **Jina v5-omni-nano** | **8/10** | **10/10** | **0.900** | **39.9ms** |
|
||||
|
||||
### 关键发现
|
||||
|
||||
1. **Jina 的优势来自"短语语义"能力**:
|
||||
- "邮件代理是否已经成功接入" → TF-IDF rank 5,Jina rank 1
|
||||
- "升级安装 QQ 插件包" → fastText rank 169,Jina rank 1(margin +0.30)
|
||||
- "我所在城市的天气预报" → fastText rank 44,Jina rank 1
|
||||
- "聊天输入区域文字多了会不会自动增高" → TF-IDF rank 1,Jina rank 1(margin +0.33)
|
||||
|
||||
2. **TF-IDF 在精确匹配上不可替代**:
|
||||
- "长期文档记忆功能是否健康" → TF-IDF rank 3,Jina rank 1
|
||||
- "重新加载全部扩展组件" → TF-IDF rank 0(完全未命中),Jina rank 2
|
||||
- TF-IDF 的 Hit@5 70% 证明精确关键词召回仍有价值
|
||||
|
||||
3. **RRF 融合反而变差**:
|
||||
- TF-IDF+fastText RRF MRR=0.552,低于 Jina 单路 0.900
|
||||
- 原因:两种稀疏向量的排序在语义查询上高度重叠,RRF 无法弥补各自短板
|
||||
|
||||
---
|
||||
|
||||
## 二、图片检索对比(同 3 张图片,6 条查询)
|
||||
|
||||
| 方案 | Hit@1 | MRR | 平均 margin |
|
||||
|------|-------|-----|-------------|
|
||||
| CLIP ViT-B/32 | 4/6 | 0.806 | -0.008(负值!) |
|
||||
| Jina v5-omni-nano | 4/6 | 0.833 | +0.024 |
|
||||
|
||||
### 逐条对比
|
||||
|
||||
| 查询 | CLIP rank | CLIP margin | Jina rank | Jina margin |
|
||||
|------|-----------|-------------|-----------|-------------|
|
||||
| 验证码图片(中) | 1 | +0.027 | 1 | +0.036 |
|
||||
| 验证码图片(英) | 1 | +0.063 | 1 | +0.077 |
|
||||
| 新闻截图(中) | 6 | -0.091 | 2 | -0.064 |
|
||||
| 新闻截图(英) | 1 | +0.008 | 2 | -0.028 |
|
||||
| 备忘录截图(中) | 3 | -0.045 | 1 | +0.045 |
|
||||
| 备忘录截图(英) | 1 | +0.051 | 1 | +0.079 |
|
||||
|
||||
### 关键发现
|
||||
|
||||
1. **中文文本→图片**:Jina 明显优于 CLIP(MRR 0.833 vs 0.611)
|
||||
- CLIP 中文查询余弦可低至 -0.076(完全反直觉)
|
||||
- Jina 最差也是 +0.045,正样本始终高于负样本
|
||||
|
||||
2. **新闻截图是共同弱点**:
|
||||
- CLIP 和 Jina 都被"深色模式备忘录"抢走新闻截图的排序
|
||||
- 原因:新闻截图的文字描述含"深色"、"备忘录"等词,与备忘录图片的视觉特征重叠
|
||||
- 这是描述质量 vs 视觉特征的竞争,不是模型问题
|
||||
|
||||
3. **margin 的实际意义**:
|
||||
- CLIP 的平均 margin = -0.008(负值意味着正样本平均不如负样本)
|
||||
- Jina 的平均 margin = +0.024(正样本始终略高于负样本)
|
||||
- 但两者的 margin 都很小(< 0.1),生产环境仍需阈值校准
|
||||
|
||||
---
|
||||
|
||||
## 三、延迟与资源
|
||||
|
||||
| 方案 | 单次查询延迟 | 索引构建 | 内存 |
|
||||
|------|-------------|----------|------|
|
||||
| TF-IDF | 0.3ms | <1s | ~50MB |
|
||||
| fastText | 8.3ms | <1s | ~200MB |
|
||||
| CLIP ONNX | 26ms | N/A | ~600MB |
|
||||
| Jina v5-omni CPU | 39.9ms | 78s(492篇) | ~4GB |
|
||||
|
||||
---
|
||||
|
||||
## 四、结论与建议
|
||||
|
||||
### 核心判断
|
||||
|
||||
| 维度 | TF-IDF/fastText | CLIP | Jina v5-omni |
|
||||
|------|-----------------|------|--------------|
|
||||
| 文本精确匹配 | ★★★★★ | N/A | ★★★★ |
|
||||
| 文本语义检索 | ★★ | N/A | ★★★★★ |
|
||||
| 中文文本→图片 | 无能力 | ★ | ★★★★ |
|
||||
| 英文文本→图片 | 无能力 | ★★★ | ★★★★ |
|
||||
| 图片→图片 | 无能力 | ★★★ | ★★★★ |
|
||||
| 多语言统一空间 | 无能力 | 有限 | ★★★★★ |
|
||||
| 延迟 | ★★★★★ | ★★★ | ★★ |
|
||||
|
||||
### 架构建议
|
||||
|
||||
1. **保留 TF-IDF 作为精确召回的一级通道**:
|
||||
- 0.3ms 延迟不可替代
|
||||
- Hit@5 70% 证明在关键词匹配场景仍有价值
|
||||
- 特别是"插件安装"、"设备查询"这类精确操作指令
|
||||
|
||||
2. **用 Jina 替换 fastText + CLIP 的稠密通道**:
|
||||
- Jina 单路 MRR=0.90,超过 fastText+CLIP 融合
|
||||
- 统一空间消除三条通道的维护成本
|
||||
- 中文文本→图片从"无法检索"提升到"可检索"
|
||||
|
||||
3. **两路融合:TF-IDF + Jina RRF**(而非 TF-IDF + fastText RRF):
|
||||
- TF-IDF 精确匹配 + Jina 语义覆盖
|
||||
- RRF 避免跨空间分数归一化问题
|
||||
- 预期 MRR > 0.90(精确匹配补 Jina 的语义盲区)
|
||||
|
||||
4. **图片检索仍需阈值校准**:
|
||||
- Jina 的 margin 平均 +0.024,生产环境需设置合理阈值
|
||||
- 建议:用真实正负样本对重新标定,而非沿用 CLIP 的 0.20 阈值
|
||||
|
||||
### 下一步
|
||||
|
||||
- 实现 TF-IDF + Jina RRF 融合,验证 MRR 是否能突破 0.90
|
||||
- 用更多生产图片标定 Jina 的图片检索阈值
|
||||
- 测试 fastText 词嵌入是否可以完全被 Jina 文本编码替代(L0 相关性计算)
|
||||
284
docs/zh/plan.md
284
docs/zh/plan.md
@ -1,284 +0,0 @@
|
||||
# WebUI 布局与配置归位修复计划
|
||||
|
||||
## 一、背景
|
||||
|
||||
上一轮 SDK 接口化改造完成并部署后,用户指出三个问题:
|
||||
|
||||
1. **WebUI 窄屏布局损坏**:顶部 `<nav>` 为桌面式横排(标题 + 6 tab + 连接指示器 + 语言 + 主题),
|
||||
窄屏断点仅缩小字号不换行,`body { overflow-x:hidden }` 直接把溢出的 tab 裁掉不可点击。
|
||||
2. **OpenClaw skills 目录被注册为核心配置**:`core.skills.path`("OpenClaw 技能存储目录")注册在核心
|
||||
配置表(`internal/config/registry.go`),但全仓无任何读取方(死配置);实际生效路径是
|
||||
clawhubadapter 自己的 `skills_dir` 配置(`config_clawhubadapter` 表 + `core.daemon.data_dir`/skills 兜底)。
|
||||
技能目录是 clawhubadapter 适配加载的领域,不应属于核心配置。
|
||||
3. **clawhubadapter 加载的微信插件成为独立配置项**:设置页出现 `channels.wechat.*`(核心表)、
|
||||
`plugin.wechat.*`(config_wechat 表)、`config_openclaw_weixin`(空表)等多处微信配置,
|
||||
全部为历史残留——当前代码零引用,OC 技能的配置实际在其自身 `~/.openclaw/openclaw.json`。
|
||||
设置页会把核心表全部键 + 全部插件表当作配置组展示,导致残留以"独立配置项"形态出现。
|
||||
|
||||
## 二、修复计划
|
||||
|
||||
| # | 动作 | 位置 | 风险 |
|
||||
|---|------|------|------|
|
||||
| A | 窄屏导航修复:<768px 下 nav 横向滚动、h1 缩写、连接指示器简化;body 溢出裁切改为 nav 内滚动 | `cmd/gui/renderer/style.css` | 无 |
|
||||
| B | 删除 `core.skills.path` 核心配置注册(set + RegisterDef 两处) | `internal/config/registry.go` | 无(无读取方) |
|
||||
| C | 备份后清理残留配置:`channels.wechat.*` 键、`config_wechat` / `config_openclaw_weixin` / `config_openclaw` 表(含微信 token,先备份) | 生产库 `/home/newqqagent/config.db` | 低(当前代码不读) |
|
||||
|
||||
## 三、实施记录
|
||||
|
||||
### 步骤 A:webui 窄屏导航修复(已完成)
|
||||
- 修复对象为 webui HTTP 服务真正前端 `internal/plugins/webui/dashboard.html`(`go:embed` 内嵌,
|
||||
登录后 `/` 返回,104KB;cmd/gui 是独立 electron 客户端,非 webui 一部分)。
|
||||
- `<768px` 断点:`nav { overflow-x:auto; scrollbar-width:none; flex-wrap:nowrap }` + `::-webkit-scrollbar { display:none }`;
|
||||
`nav a { white-space:nowrap; flex-shrink:0 }`;`nav h1 { font-size:0 }`(保留 logo 图、隐藏文字,弥补窄屏空间);
|
||||
`nav > div { flex-shrink:0 }` 右侧语言/主题/退出按钮不压缩。
|
||||
- 顺带在 cmd/gui(electron 客户端)同步了窄屏样式与消息来源徽标(`app.js`/`style.css`,客户端窗口缩放同样受益;
|
||||
客户端需另行构建 electron 应用才生效)。
|
||||
- 验证:部署后 `/` 返回的 dashboard 含 `scrollbar-width:none`/`font-size:0`/`::-webkit-scrollbar` 规则。
|
||||
|
||||
### 步骤 B:删除 core.skills.path 核心配置(已完成)
|
||||
- 删除 `internal/config/registry.go` 两处:`set("core.skills.path", ...)`(SeedDefaults)与
|
||||
`reg(ConfigDef{Key:"core.skills.path", ...})`(定义注册)。
|
||||
- 理由:该键全仓无读取方(grep 仅命中注册处),实际生效路径是 clawhubadapter 的 `skills_dir`
|
||||
(config_clawhubadapter 表 + `core.daemon.data_dir`/skills 兜底)。技能目录属 clawhubadapter 适配领域。
|
||||
- 验证:`grep -rn "skills.path" --include="*.go"` 零命中;部署后设置页无 `core.skills.path`。
|
||||
|
||||
### 步骤 C:清理生产库残留配置(已完成)
|
||||
- 操作前 `sqlite3 .backup /tmp/opencode/config.db.pre-clean.bak`(含微信 token 数据)。
|
||||
- 删除:`config` 表 `channels.wechat.*` 3 键 + `core.skills.path` 键;`DROP TABLE config_wechat /
|
||||
config_openclaw_weixin / config_openclaw`(三者均为历史残留:当前代码零引用,clawhubadapter 实际
|
||||
使用 config_clawhubadapter 表;OC 技能配置在其自身 `~/.openclaw/openclaw.json`)。
|
||||
- 验证:设置页总键数 138→129,无 wechat/weixin/skills.path 残留,`plugin.clawhubadapter.skills_dir /
|
||||
simulator_dir` 正常;服务 healthcheck ready、clawhubadapter "OC plugin manager started"。
|
||||
|
||||
---
|
||||
|
||||
# SDK Stop 注册接口(RegisterStopHandler)计划
|
||||
|
||||
## 一、背景
|
||||
|
||||
2026-08-01 20:00 起生产 homeagent 进入崩溃循环(`fatal error: thread exhaustion`,
|
||||
systemd 重启计数 61+)。排查定位为 SDK 示例插件 `calendar`(示例源码在 SDK 仓库
|
||||
`example/calendar`,生产以 plugin.so 形态加载)三个缺陷叠加:
|
||||
|
||||
1. **农历引擎 3 个 bug**(`daysInLunarYear` 位循环 `i > 0` 应 `i > 0x8`、缺闰月天数、
|
||||
`lunarToSolar` 内层重复加闰月)→ `lunarToSolar(2026,4,12)` 返回 **2062-11-16**(偏移 36 年),
|
||||
农历重复事件(`lunar_yearly`)的 next 被生成到遥远错误日期。
|
||||
2. **`cleanupPastEvents` 保留过时重复事件** → 每 30s ticker 对已到点的重复事件再生成一份 next,
|
||||
事件从 7 个爆炸到 **45612 个**(15MB events.json)。
|
||||
3. **无提醒投递保护**:15018 份同时到点的事件一次性 `go sdk.InjectInterruptText(...)` 投递
|
||||
→ interrupt 风暴 → goroutine/线程耗尽。
|
||||
|
||||
处置:修复农历引擎 3 处 + next 去重 + 清理过时重复事件,用**新版 SDK 仓库 + 新版 plugindev 工具链**
|
||||
重建 `calendar_linux_amd64.hmap`,经 **webui `POST /api/v1/plugins`**(透明代理到 pluginmgr 安装接口)
|
||||
重装,重启验证收敛(事件 4 个、next 正确生成 2027-05-17、0 崩溃)。
|
||||
|
||||
**过程中暴露的能力缺口**:SDK 只有 `Plugin` 接口的 `Name/Start/Stop`,**没有 stop 注册接口**
|
||||
(`RegisterStopHandler`/`OnStop` 均不存在,SDK v0.7.2/v0.8.0/master 一致)。插件停止时只能在自己的
|
||||
`Stop()` 里写清理逻辑,SDK 层无法统一执行"停止时清理"回调;calendar 的 `Stop() { p.saveEvents() }`
|
||||
还会用陈旧内存把已清理的数据写回磁盘(曾导致删除的重复事件复活)。
|
||||
|
||||
## 二、计划
|
||||
|
||||
| # | 动作 | 位置 | 风险 |
|
||||
|---|------|------|------|
|
||||
| 1 | 公共 SDK `PluginSDK` 加 `RegisterStopHandler(fn func())` + `RunStopHandlers()`(幂等、后注册先执行),两处同步 | `third_party/homeagent-sdk/sdk/plugin.go`、SDK 仓库 `sdk/plugin.go` | 低(纯新增,内置 SDK 内嵌透传) |
|
||||
| 2 | 内核 Registry 保存每插件 SDK 引用(`sdkRefs`),`StopAll`/`ReloadOne`/`DisablePlugin` 调 `Stop()` 前执行 `RunStopHandlers` | `internal/plugin/registry.go` | 中(生命周期路径,需回归 reload/disable) |
|
||||
| 3 | 工具链 plugindev:z_bridge 模板 `bridgeState` 存 SDK,`StopPlugin` 先 `RunStopHandlers()` 再 `plugin.Stop()`;init 脚手架模板加演示 | SDK 仓库 `tools/plugindev/templates.go`、`templates/main.go.tmpl` | 低 |
|
||||
| 4 | 内置示例插件演示(如 timer:ticker 停止改为 stop handler) | `internal/plugins/timer/plugin.go` | 低 |
|
||||
| 5 | 外部示例插件同步(`example/calendar` 的 `saveEvents` 改由 stop handler 执行,验证 z_bridge 链路;其余 example 加演示) | SDK 仓库 `example/*` | 低 |
|
||||
| 6 | 文档同步:SDK README 生命周期章节 + 主仓插件开发文档 | SDK 仓库 `README.md`/`README_EN.md` 等 | 无 |
|
||||
|
||||
## 三、实施记录
|
||||
|
||||
1. SDK 公共层(`RegisterStopHandler` + `RunStopHandlers`:后注册先执行、执行后清空幂等)已落地
|
||||
`third_party/homeagent-sdk/sdk/plugin.go`,并同步到 SDK 仓库 `/tmp/opencode/sdk-repo/sdk/plugin.go`(两处一致)。
|
||||
2. 内核 Registry(`internal/plugin/registry.go`)新增 `sdkRefs map[string]*sdk.PluginSDK` + `runStopHandlers`,
|
||||
`loadOne` 注册、`StopAll`/`ReloadOne`/`DisablePlugin` 在 `Stop()` 前执行(共 4 处调用点)。
|
||||
3. 工具链 plugindev(SDK 仓库):linux `tmplLinuxBridge` 的 `go_stop_plugin` 先 `RunStopHandlers()` 再 `plg.Stop()`;
|
||||
windows `tmplBridge` 的 `bridgeState` 加 `sdk` 字段、`StopPlugin` 同链路;`tmplPluginGo` + `main.go.tmpl`
|
||||
脚手架加 `RegisterStopHandler` 演示。plugindev 重新编译通过(GOPATH=/root/go)。
|
||||
4. 内置 timer 插件演示:`close(p.stopCh)` 移入 stop handler,`Stop()` 只 `wg.Wait()`。
|
||||
5. 外部示例:`example/calendar` 的 `saveEvents` 改为 `s.RegisterStopHandler(p.saveEvents)`,
|
||||
`Stop()` 删除写盘调用(持久化交由 stop handler,避免陈旧内存复活已删事件)。
|
||||
6. 文档:SDK 仓库 `README.md`/`README_EN.md` 生命周期章节补充 RegisterStopHandler 说明。
|
||||
7. 构建测试:主仓 `go build ./...` + `go test ./internal/sdk/... ./internal/plugin/...` 全绿;
|
||||
SDK 仓库 `go build ./...` + `go test ./sdk/...` 全绿。
|
||||
8. 生产部署验证:新 plugindev(--no-bundle)重建 `calendar_linux_amd64.hmap`(md5 084e97c0…,strings 确认
|
||||
`go_stop_plugin → RunStopHandlers → saveEvents` 编译进 plugin.so);webui API 删旧装新;重装新内核
|
||||
homed(含 sdkRefs/runStopHandlers);两次重启事件稳定 3 个不复活、events.json mtime 与 stop 时刻吻合
|
||||
(saveEvents 经 stop handler 真实执行)、0 次 thread exhaustion、服务 active。
|
||||
|
||||
|
||||
---
|
||||
|
||||
# clawhubadapter OpenClaw 通道插件兼容修复计划
|
||||
|
||||
## 一、背景
|
||||
|
||||
生产 `core.llm.provider` 已是 mock LLM 源(`core.llm.sources.mocktest`,base_url
|
||||
`http://127.0.0.1:18080/v1`、model mock-model、adapter openai),mock LLM 服务常驻运行。
|
||||
借助 **mock 通道插件**(`/tmp/opencode/mock-skills/mock-wechat/`,完全复刻 openclaw-weixin 的
|
||||
真实注册格式 `register(api) → api.registerChannel({ plugin: ChannelPlugin })`)放入生产 skills 目录
|
||||
端到端复现,得出如下结论:
|
||||
|
||||
**已验证可用链路**:manager 加载 mock 插件 → Go 端识别 `ocplugin mock-wechat handled by manager` →
|
||||
注册工具 `mock-wechat_read_mock_wechat_input`/`mock-wechat_mock_echo` → `RegisterOutputChannel("mock-wechat")`
|
||||
→ mock 自推消息经 `channel_input` 通知 → `[agent] interrupt from manager/mock-wechat` →
|
||||
mock LLM 正常回复(195ms)。
|
||||
|
||||
**复现的核心缺陷**(真实通道插件 wechat/dingding"根本不可用"的根因):
|
||||
|
||||
1. **输出断链**:manager `tools/call` 通道分支只认 `channelPlugin.outbound.sendText/sendMedia`
|
||||
(旧格式),真实 ChannelPlugin(openclaw-weixin 等)无 outbound →
|
||||
`tools/call mock-wechat → error: "channel mock-wechat has no output handler"`。
|
||||
2. **生命周期静止**:manager mock api 从不调用 `gateway.startAccount/stopAccount`,也无
|
||||
`api.runtime`/`channelRuntime` → 通道插件加载后永不启动(不登录、不轮询、不收消息)。
|
||||
3. **输入依赖错位**:真实插件把消息经 `channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher`
|
||||
推送(manager 完全无此对象),而不是调 `api.submitInput`。
|
||||
4. **stdout 污染**:插件 `console.log` 直接进 JSON-RPC 流,Go 端 readLoop 跳过非 JSON 行,有丢通知风险。
|
||||
|
||||
**wechat 通道的心跳机制**(`openclaw-weixin/dist/index.js` `pollLoop`,448 行起):每账号一个常驻
|
||||
`pollLoop`,循环 `POST ilink/bot/getupdates`(body `{get_updates_buf}`,超时 35s)——**长轮询即心跳**:
|
||||
服务器收到 poll 请求即知通道在线,新消息随 poll 响应 push 回来;超时视为空响应继续轮询,真错误延时
|
||||
5s 重试。**与 gateway 生命周期强绑定**:
|
||||
|
||||
- `pollLoop` 只由 `gateway.startAccount(ctx)` 启动;不被调用 → 心跳/收消息全断(服务器侧认为通道离线)。
|
||||
- `startAccount` 末尾 `await new Promise(()=>{})` **永久挂起**——OC gateway 靠它配合 health-monitor
|
||||
(startAccount 退出 → 判定账号崩溃 → 重启账号)。manager 调 `startAccount` 必须 **fire-and-forget**。
|
||||
- 停靠 `gateway.stopAccount(ctx)`(`ctx.account.accountId` 定位),停止时经 `statusSinks` 调
|
||||
`ctx.setStatus({running:false, connected:false, lastStopAt})`;启动即上报
|
||||
`ctx.getStatus()/ctx.setStatus({...running:true, connected:true, lastStartAt})`——`connected` 是
|
||||
gateway 判断账号存活的依据。
|
||||
- `sendTyping`(ilink/bot/sendtyping + typing_ticket)是打字指示,非心跳,无需支持。
|
||||
|
||||
## 二、修复计划
|
||||
|
||||
| # | 动作 | 位置 | 风险 |
|
||||
|---|------|------|------|
|
||||
| A | manager 提供 **gateway 生命周期桥**:channel 插件注册后自动 `gateway.startAccount(ctx)`(fire-and-forget,不等待挂起的 Promise),构造完整 ctx `{account, cfg, channelRuntime, getStatus, setStatus}`;Go 端 `channel_stop` 通知 → `stopAccount` | `internal/plugins/clawhubadapter/manager/main.js` | 中 |
|
||||
| B | 实现 **channelRuntime mock**:`reply.dispatchReplyWithBufferedBlockDispatcher`(deliver 回调 → `channel_output` 通知送 Go 端)、`getPolls`(OC 通用通道轮询输入)、`call` 透传 | 同上 | 中 |
|
||||
| C | `tools/call` 通道分支改造:无 `outbound` 的 ChannelPlugin 改走 channelRuntime 事件式发送(agent 输出 → deliver),不再报 "no output handler" | 同上 | 低 |
|
||||
| D | 状态上报透传:`setStatus` 经 `channel_status` 通知 → Go 端可查;health-monitor 语义(startAccount 保持挂起) | 同上 | 低 |
|
||||
| E | stdout 卫生:插件 `console.log` 重定向 stderr(或 JSON-RPC 流感知封装),杜绝污染 | 同上 | 低 |
|
||||
| F | Go 端:`channel_output`/`channel_status` 通知接入(事件分发),通道输出 handler 保持 `sp.CallTool` | `internal/plugins/clawhubadapter/registry.go`、`plugin.go` | 中 |
|
||||
| G | 端到端验证:mock 通道插件 + mock LLM(生产环境,临时放入/移出 skills 目录)复跑全链路(登录启动→收消息→回复→出站→停止) | 生产 | 低 |
|
||||
|
||||
## 三、实施记录
|
||||
|
||||
(逐步填写)
|
||||
|
||||
1. **manager/main.js — 通道运行时与生命周期桥(已完成,独立运行验证)**
|
||||
- `makeChannelRuntime(chName, ch)`:`reply.dispatchReplyWithBufferedBlockDispatcher(opts)` 提取
|
||||
`dispatcherOptions.deliver`/`typingCallbacks` 按 `ctx.AccountId` 挂到 `ch.deliverers`,随后
|
||||
`notify('channel_input', {channel, payload:{content: BodyForAgent||Body, from, sessionKey, accountId,
|
||||
messageSid, chatType, raw}})` 入站;返回 dispatcher(sendNow/addToBuffer/sendBuffer/closeBuffer)。
|
||||
`chatPolls`/`getPolls` 空转(防断连误判)、`call` 转发 `channel_output` 通知。
|
||||
- `startChannels(name)`:channel 插件注册后自动枚举账号(`config.listAccountIds`→`resolveAccount`,
|
||||
缺省 `['default']`),构造完整 ctx `{account, cfg, channelRuntime, getStatus, setStatus}`,
|
||||
**fire-and-forget** 调 `gateway.startAccount`(真实插件会永久挂起,绝不等待);崩溃/状态变更经
|
||||
`channel_status` 通知透传(health-monitor 语义:startAccount 不退出=账号存活)。
|
||||
- `stopChannels(name)`:逐个账号 `gateway.stopAccount`;进程 SIGTERM/SIGINT 时统一执行优雅停靠。
|
||||
- `tools/call` 通道分支:保留 outbound(旧格式)→ 新增 **deliver 事件式发送**
|
||||
(`deliverItem` 按 accountId 取 deliver + typingCallbacks.onReplyStart/onCleanup 包裹)→
|
||||
无 deliver 时降级 `channel_output` 通知 → 兜底报错。不再出现 "has no output handler"。
|
||||
- **stdout 卫生**:全局 `console.log` 重定向 stderr,JSON-RPC 流仅承载协议帧。
|
||||
2. **mock 插件升级(/tmp/opencode/mock-skills/mock-wechat/index.js)**:完全复刻真实 weixin 行为——
|
||||
`gateway.startAccount` 永久挂起 + `setStatus` 上报 + `setInterval` 心跳轮询 + 800ms 后经
|
||||
`dispatchReplyWithBufferedBlockDispatcher` 推送入站(deliver 本地记录发送);`stopAccount` 停轮询+状态置否。
|
||||
3. **manager 独立运行验证(通过)**:`channel_status` 启动上报(running=true connected=true);
|
||||
`tools/call mock-wechat` → `{"status":"sent","via":"channelRuntime.deliver"}`,插件 deliver 收到
|
||||
`text="hello from agent"` 且 typing onReplyStart/onCleanup 正确包裹;心跳 poll #1-4 常驻;
|
||||
SIGTERM → `stopAccount called` 退出码 0;插件 console 输出全部走 stderr(协议流零污染)。
|
||||
4. **Go 端通知接入(plugin.go translateAndRegister + registry.go 状态缓存)**:`channel_status` 存
|
||||
`channelStatus` map(可查)+ 日志;`channel_output` 降级事件日志。`go build ./...` 通过。
|
||||
5. **回复闭环修复(同步注入)**:`channel_input → InjectInterruptText` 的 InputEvent 不带 ResponseCh
|
||||
(internal/agent/io/channel.go:276),agent 回复在 emitResponse(eventloop.go:384)被静默丢弃。
|
||||
改为 `s.InjectInputSync(pluginName, channel, "text", payload)`(内部 SDK 已有 4 参版本,返回
|
||||
`*OutputEvent`)同步等待回复 → 提取 `Payload["content"]` → `sp.CallTool(channel, {payload, meta})`
|
||||
→ manager `tools/call` → deliver → 插件发送 → 微信送达。公共 SDK IOInjector 同步补
|
||||
`InjectInputSync(source, channel, text) string`(ioAdapter 实现,供外部插件一致使用)。
|
||||
6. **mock LLM 恒定文本化**:/opt/llm-mock/mock_server.py `decide()` 删除工具调用分支,一律回文本
|
||||
("无论收到什么消息都通过微信插件发送"),保证每条入站消息回复必然走通道输出。
|
||||
7. **生产微信闭环验证(通过)**:用户微信发"你好..." → pollLoop 收到 → dispatchReply →
|
||||
InjectInputSync → mock LLM 回文本 → CallTool(wechat) → deliver → `POST ilink/bot/sendmessage`
|
||||
→ **status=200 message_id=7489545365740590088**,微信收到"(mock)已收到消息,长度 324 字符。"
|
||||
8. **通用性审查(无硬编码)**:manager/plugin.go/registry.go 均无 weixin/wechat 特判,全部按 OC 规范
|
||||
字段实现(gateway/config/capabilities/channelRuntime/deliver/typingCallbacks)。修正规范签名参数
|
||||
约定:`listAccountIds(cfg)`、`resolveAccount(cfg, accountId)` 正确传 cfg。
|
||||
9. **已知边界**(非硬编码,架构性):a) `channelRuntime.getPolls/chatPolls` 返回空 msgs——依赖
|
||||
runtime 轮询输入的通用通道型插件收不到消息(weixin/dingding 类自带 pollLoop 的通道不受影响);
|
||||
b) `gatewayMethods` 登录流程(web.login.start/QR 扫码)未实现(CLI 有 stub),通道凭 token
|
||||
配置直连;c) startAccount ctx 提供 account/channelRuntime/cfg/getStatus/setStatus 核心字段。
|
||||
10. **补充边界 a) getPolls/chatPolls 消息源(已完成)**:manager `makeChannelRuntime` 的
|
||||
`chatPolls/getPolls` 改为读 `ch.pollQueues`(按 accountId 队列,poll 取走即消费);新增
|
||||
`channel/send` RPC(Go 端注入 → 队列 → 插件轮询取走);Go 端 `SendToChannel(channel, payload)`
|
||||
+ `ChannelSender()` 单例(Start 时置位)。验证:mock-poll 插件(纯 chatPolls 轮询型)——
|
||||
`channel/send` → `{"status":"queued"}` → `[mock-poll] poll got msg` → dispatchReply →
|
||||
`channel_input` 入站完整(content/from/sessionKey/accountId/messageSid/chatType)。
|
||||
微信链路回归正常(Polling started + channel_status running=true)。
|
||||
11. **边界 b) 登录流程核实(已解决,无需实现)**:真实登录机制是 **SKILL 脚本旁路**——
|
||||
`weixin-openclaw-login` SKILL 的 `scripts/get-login-url.js`(ilink 二维码 URL)+
|
||||
`poll-login-status.py`(轮询扫码状态)→ agent 经 exec 执行 → 拿 bot_token 写入
|
||||
`~/.openclaw-weixin/account.json`(2026-07-28 17:31 创建,token 有效)→ 插件启动
|
||||
`resolveAccountData` 直读。不依赖 manager gatewayMethods(web.login.start 等 OC gateway
|
||||
协议 stub 不影响真实使用)。
|
||||
12. **clawhubadapter 全量管理接口(已完成并验证)**:向 agent 暴露完整插件/通道管理面——
|
||||
- 新工具:`clawhubadapter_plugin_info`(类型/工具/关联通道详情)、`plugin_reload`(reloadPlugin)、
|
||||
`channel_list`(全部注册通道 + 实时状态)、`channel_send`(SendToChannel 投递)、
|
||||
`channel_start`/`channel_stop`(manager `channel/start`、`channel/stop` RPC)。
|
||||
- manager 新增 RPC:`plugins/channels`(registeredChannels 摘要含 status/accounts)、
|
||||
`channel/start`(fire-and-forget startChannels 恢复账号)、`channel/stop`(stopAccount,
|
||||
按 channel 全停或按 accountId 单停,省略 channel 则全部停止)。
|
||||
- Go 侧 `channelSummary(mgr)` 合并 manager 注册信息与 `channel_status` 实时缓存(缓存优先)。
|
||||
- 端到端验证(生产实例,LLM 为本地 mock 源):微信发"你好通道..." → agent 执行
|
||||
`channel_list` → `- wechat | plugin=openclaw-weixin type=text running=true connected=true
|
||||
accounts=[default]` → 回复送达;发"注入..." → 执行 `channel_send` → "消息已投递到通道
|
||||
wechat" → 回复送达。standalone manager 另验证 `channel/start`(mock-wechat startAccount
|
||||
重新执行、心跳恢复)与 `channel/stop`(stopAccount called)。
|
||||
13. **插件删除回调 onRemove 全套(已完成并验证)**:`RegisterOnRemoveHandler`(仅卸载触发、
|
||||
重载/禁用不触发,与 stop handler 互补——stop 每次停止都执行)。registry.RemovePlugin 流程:
|
||||
stop handlers → Stop → runOnRemoveHandlers → 移除 plugins/sdkRefs/instances →
|
||||
UnregisterPluginTools → **配置清理**。配置清理含两层:`ConfigRegistry.RemovePlugin`
|
||||
删除 defs 中 `plugin.<name>.*` 配置项定义 + DROP `config_<name>` 插件配置表
|
||||
(含用户设置值,ListPlugins 基于 config_% 表枚举故配置区完全消失);已用临时程序验证
|
||||
(before: defs=1/plugins=[timer] → after: defs=0/plugins=[])。示例盘点(SDK 仓库 15 个):
|
||||
calendar(events.json)、memo(memos.json)、rss(订阅数据目录)、weather(缓存目录)已加;
|
||||
files(filesDir 为用户配置的访问根目录,默认 /)、bili/qq(用户下载资产)、
|
||||
ocr(函数内 defer RemoveAll 自清理)按语义不加;plugindev 模板 main.go.tmpl + README.tmpl
|
||||
含 onRemove 演示;SDK README/README_EN 生命周期文档补"删除清理(onRemove)"小节。
|
||||
|
||||
14. [2026-08-03] SDK 工具链/打包/重装 + dlclose 修复:
|
||||
- 工具链源码位置澄清:SDK 仓完整内容位于 third_party/homeagent-sdk(主仓 .gitignore 仅跟踪
|
||||
sdk/meta/go.mod,"两个远程仓库各取所需";/tmp/opencode/sdk-repo 为工作克隆,远程=gitcode)。
|
||||
- 工具链支持公共 IOInjector.InjectInputSync:CORE_INJECT_INPUT_SYNC=47(C 桥 dispatchIO
|
||||
callString 回传回复文本);主仓 cabi loader case 47 用内部 4 参版 InjectInputSync 取
|
||||
OutputEvent.Payload["content"] setResult(meta.go ID 47 + loader.go,主仓 3f252ed)。
|
||||
- plugindev 构建环境:GOMODCACHE=/root/go/pkg/mod(yaegi 缓存所在)、GOPROXY=off。
|
||||
- 工具链打包 memo:plg.json BOM 去除、name_en "Memo/Notes"→"Memo"(toSnake 不处理斜杠,
|
||||
name_en 带 / 会使 hmap 名含子路径报错);bundle=true 时走全平台交叉编译(本机无 darwin
|
||||
工具链),打包用 `build --no-bundle --target linux/amd64`;产物 dist/memo_linux_amd64.hmap。
|
||||
- 重装:pluginmgr HTTP API(127.0.0.1:9876)DELETE /plugins/memo 卸载(走内核 RemovePlugin
|
||||
+ onRemove)→ POST /plugins binary body 传 hmap(返回 installed+checksum);生效用
|
||||
webui `POST /api/v1/plugins/reload`(X-API-Key,生产 admin123)。
|
||||
- 关键 bug:Linux dlopen 同路径复用旧句柄——RemovePlugin/ReloadOne 只 Stop 不 dlclose,
|
||||
插件二进制更新后重载仍执行旧代码(生产 memo 装新版仍注册旧 3 工具)。修复:
|
||||
cabiPlugin.Close()(handle.Close)+ Registry.closeDynamic 在卸载/重载时调用(主仓 649e312)。
|
||||
生产 homed-new7 验证:memo 6 工具(memo_todo_add/complete/list + memo_memo_create/list/delete)
|
||||
注册正常,wechat 通道 running。
|
||||
- SDK 仓推送 b6e30f9(工具链 47 + 重建 bin 二进制 + memo plg.json + sdk/plugin.go 注释精简)。
|
||||
15. [2026-08-03] 嵌套 git 恢复 + 工作区清理 + codegraph 索引修正:
|
||||
- 嵌套 git 恢复:third_party/homeagent-sdk 原本是"单仓库双提交"(目录内嵌套 .git 推 gitcode
|
||||
homeagent-sdk 仓,主仓 git 跟踪 sdk/meta/go.mod 推 HomeAgent 仓),嵌套 .git 此前被误删;
|
||||
已从 /tmp/opencode/sdk-repo 复制 .git 恢复(remote=homeagent-sdk.git,HEAD=b6e30f9,工作区干净),
|
||||
主仓 git 不受影响。以后 SDK 改动直接在 third_party 内 git commit+push(SDK 仓推送仍用带凭据
|
||||
URL https://JianFeeeee:BCkb32xBuLxWD9P4MmU8ydZ5@gitcode.com/JianFeeeee/homeagent-sdk.git),
|
||||
不再经 /tmp 中转。
|
||||
- /tmp 清理:删除 /tmp/opencode/sdk-repo、hasdk-fresh、plugindev-new、plugindev_new、mock-run、
|
||||
lunartest(SDK 中转/临时目录);保留 homed-new*(生产二进制备份)、mock-skills 等非 SDK 内容。
|
||||
- replace 修正:go.mod 第 17 行已是 `./third_party/homeagent-sdk`(正确);package-linux.sh
|
||||
prepare_gomod 优先用 $PROJECT_ROOT/third_party/homeagent-sdk,仅缺失时才 clone /tmp/homeagent-sdk
|
||||
兜底(主仓 108faac)。
|
||||
- codegraph 索引修正:根目录 codegraph.json(PROJECT_CONFIG_FILENAME)配
|
||||
includeIgnored+include: ["third_party/homeagent-sdk"],codegraph index 后 Files 179→233,
|
||||
third_party 文件 7→61,tools/plugindev 与 sdk/plugin.go(InjectInputSync 等)均可查询
|
||||
(此前嵌套 SDK 仓被主仓 .gitignore 挡在索引外);codegraph sync 不感知配置变更,需 index 全量重建。
|
||||
@ -420,7 +420,6 @@ data URL 本身已是 base64 文本,包进二进制传输省不了空间,还
|
||||
## 八、关联文档
|
||||
|
||||
- `docs/zh/架构迁移评估.md` — 完整论证(§3.2 method id 平移、§3.3 数据面、§3.4 SDK 封装、§3.5 回调型资源、§3.8 能力对齐)
|
||||
- `docs/zh/plugin-migration-plan.md` — Part 0~6 执行计划与完成实录(含 Part 6.5 生产切换、Part 6.6 压测)
|
||||
- `plan.md` §11 — 11.1~11.9 修复清单(唯一权威编号)
|
||||
- `third_party/homeagent-sdk/sdk/` — 合同面 A 的代码实现(全程零 diff)
|
||||
- `internal/plugin/proc/protocol.go` — 合同面 B 的代码实现(`Method*` 常量,取代已删的 bridge 模板)
|
||||
|
||||
@ -1,632 +0,0 @@
|
||||
# 外部插件多进程化适配计划(修改→审查→验证三步微循环)
|
||||
|
||||
> 分支:`update`
|
||||
> 基线:`docs/zh/plugin-interface-matrix.md`(合同面 A/B/C)+ `plan.md` §11 + `docs/zh/架构迁移评估.md`
|
||||
> 每部分 = 一个「修改 → 审查 → 验证」三步微循环。所有验证在 **update 分支**完成,可独立交付、可回退。
|
||||
>
|
||||
> **循环的铁律**(每部分适用):
|
||||
> - **修改**:只动核心侧 + 工具链,`third_party/homeagent-sdk/sdk/`(合同面 A)**零 diff**。
|
||||
> - **审查**:接口冻结检查(`git diff` 公开 SDK 为空)+ 代码 review + `go vet`。
|
||||
> - **验证**:`make test` + 针对性单测 + 端到端冒烟,产物 `.bin` 端到端可用。
|
||||
>
|
||||
> 标 `【M】`=修改部分、`【R】`=审查部分、`【V】`=验证部分。依赖前置部分完成后才可开始。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- **Part 0** 脆弱基线先行(不依赖迁移,现网可直接受益)— 0.1 ✅ / 0.2 ✅ / 0.3 ⏭️ / 0.4 ⏭️
|
||||
- **Part 1** 加载分派骨架(`entry` 双通道共存)— ✅ **已完成**
|
||||
- **Part 2** 子进程通道原型(spawn / JSON-RPC / procPlugin)— ✅ **已完成**
|
||||
- **Part 3** plugindev 工具链改造(`.bin` 产物)— ✅ **已完成**
|
||||
- **Part 4** 共享内存数据面(StageContext 跨进程并发改写)— ✅ **已完成**(段/编解码/锁仲裁 + RunStage 接线)
|
||||
- **Part 5** 通知面(事件环 + eventfd)— ✅ **核心已完成**
|
||||
- **Part 6** 迁移与收尾(17 插件逐个 + 删 cabi + 权限显式化)
|
||||
- 最终验收清单
|
||||
|
||||
> **进度快照(2026-09-02)**:分支 `feature/plugin-proc-migration`。
|
||||
> 已交付:现网止血 2 项(11.1/11.3)、entry 双通道分派、共享内存 stage 并发、
|
||||
> 子进程控制面(NDJSON RPC + 51 method 名平移)、plugindev `.bin` 构建、
|
||||
> registry 接线、**事件环(§3.6)**。**外部插件已可端到端跑在子进程 + 共享内存上**,
|
||||
> 且首次获得事件订阅能力(C ABI 下 case 23/24 一直是空实现)。
|
||||
> 测试:内核 `internal/plugin/proc` 38 项 + `internal/plugin` 16 项(含 `-race`),
|
||||
> SDK 仓 plugindev 16 项。
|
||||
> 下一步:Part 6 逐插件迁移 + 删 `internal/plugin/cabi/`。
|
||||
|
||||
---
|
||||
|
||||
## Part 0:脆弱基线先行(阶段 0,~1 人日)
|
||||
|
||||
> 依据:plan.md §11.1/11.3/11.6。不依赖任何新架构,独立交付,现网直接受益。
|
||||
> 目的:在副本模型内部打补丁,止血,为后续迁移争取时间。
|
||||
|
||||
### 0.1 output_send 假成功修复(11.1)— ✅ **已完成**(2026-08-31)
|
||||
|
||||
- 【M】✅ `internal/plugin/cabi/loader.go`——`CORE_REGISTER_OUTPUT_CH`(:454)的异步 output 从「goroutine 直接返回 queued」改为「goroutine + 带超时 channel 等真实结果」。
|
||||
新增 `awaitOutputResult`(:276)+ 可注入版 `awaitOutputResultWith`(:281)+ 常量 `outputSendTimeout = 10s`:
|
||||
```go
|
||||
resCh := make(chan error, 1)
|
||||
go func() { resCh <- invoke(pid, channel, argsJSON) }()
|
||||
select {
|
||||
case err := <-resCh:
|
||||
if err != nil { return nil, err } // 真实失败上报
|
||||
return map[string]interface{}{"status": "sent"}, nil
|
||||
case <-time.After(timeout):
|
||||
return map[string]interface{}{"status": "unconfirmed", "note": "..."}, nil
|
||||
}
|
||||
```
|
||||
关键:`dev.Execute` 由 `executeOutputSendTool` 从 Go 侧调起(不在 cgo 栈内),goroutine 内的 `pluginInvokeOutput` 才是 cgo,**不构成嵌套**。
|
||||
- 【M】✅ `internal/agent/core/output.go` `executeOutputSendTool`:识别 `status=unconfirmed|queued` → 返回「发送结果未确认:<note>」而非「已发送」,把未确认状态透传给模型。
|
||||
- 【R】✅ 无 cgo 嵌套(`awaitOutputResult` 只在 `RegisterOutputChannel` 的 handler 内被调用,该 handler 从 Go 侧调起);
|
||||
「超时未确认」措辞与 11.2 的"已取消"谎言区分——用 `unconfirmed` + 显式 note,不谎报成功也不谎报失败。
|
||||
- 【R】✅ 接口冻结:`git diff third_party/homeagent-sdk/sdk/` 为空。
|
||||
- 【V】✅ 新增 `internal/plugin/cabi/output_test.go` 三用例全绿:
|
||||
- `TestAwaitOutputResult_Success` → `status=sent`
|
||||
- `TestAwaitOutputResult_Failure`(模拟 meta 缺 user_id)→ **返回 error**(旧实现会谎报成功)
|
||||
- `TestAwaitOutputResult_Timeout` → `status=unconfirmed` 且不返回 error
|
||||
- 【V】✅ `go build ./...` exit 0;`go test ./internal/plugin/... ./internal/agent/...` 全绿。
|
||||
|
||||
### 0.2 stage lost update 补丁(11.3)— ✅ **已完成**(2026-08-31)
|
||||
|
||||
- 【M】✅ `templates.go`(**SDK 仓** update 分支 `5648519`)`go_invoke_stage` 改为 diff 回传:
|
||||
- 新增 `snapshotWritable(sc) map[string]string`——handler 前的**序列化**快照
|
||||
- 新增 `changedFieldsOnly(before, after)`——只回传变更字段,无变更零回传
|
||||
- ❗ **第一版踩坑并修正**:`stageContextWritable` 返回的切片字段与 `sc` **共享底层数组**,handler 原地改元素(`sc.ToolResults[0].Result = clean`)时 before 快照跟着变,diff 看不到变更 → 修复会静默失效。故 before 必须逐字段序列化成字符串。
|
||||
- 【M】✅ `internal/plugin/cabi/loader.go` `applyStageResult` 配套(本仓 `9bb9cb3`):`tool_calls`/`tool_results` 去掉 `len(v)>0` 拦截——改为键存在即应用,使插件「清空全部工具调用」的显式 `[]` 能被表达(旧插件仅 len>0 才带键,不会被误清空)。
|
||||
- 【R】✅ `changedFieldsOnly` 无竞态(纯函数,无共享状态);只读插件零回传(单测断言)。
|
||||
- 【R】✅ 接口冻结:两仓 `git diff sdk/` 均为空(只改 bridge 模版 + 内核)。
|
||||
- 【R】✅ bridge 模版可编译性:抽取 `tmplLinuxBridge` + 真实 `weather/plugin.go` 做 `go build -buildmode=c-shared` → exit 0。
|
||||
- 【V】✅ SDK 仓 `tools/plugindev/stagediff_test.go` 6 用例全绿:
|
||||
- `_ReadOnlyPluginReturnsNothing`(只读插件零回传——修复核心)
|
||||
- `_WriterReturnsOnlyChanged`(原地改切片元素仅回传 tool_results)
|
||||
- `_ScalarChange` / `_NewResponseIsReturned` / `_ClearedSliceIsReturnedAsEmpty`
|
||||
- `_ProductionScenarioNoOverwrite`(**复刻实验 13 现网场景**:sanitizer 清洗 + weather 只读,清洗结果不再被覆盖)
|
||||
- 【V】✅ 内核侧 `output_test.go` 新增 `TestApplyStageResult_ClearedSlicesAreApplied` / `_OnlyPresentKeysApplied` 全绿。
|
||||
- 【V】✅ `go build ./...` exit 0;`go test ./internal/plugin/... ./internal/agent/...` 全绿。
|
||||
- ⚠️ **待部署项**:需用新 plugindev 重编全部 17 个外部插件(bridge 模版变更),走 `plugin_install(overwrite=true)`。
|
||||
|
||||
### 0.3 reload 语义修正(11.6)— ⏭️ **已跳过**(2026-08-31 用户决策:直接进入进程化重构)
|
||||
|
||||
> 子进程模型下 `DF_1_NODELETE` 议题**整体消失**(§3.1)——同路径替换 `plugin.bin` 重启进程即生效。
|
||||
> 在 cabi 路径上补 ELF 检测属于「给即将删除的代码打补丁」,性价比低。
|
||||
> 现网仍受 reload 假成功影响,但 Part 1 的 entry 分派已为迁移铺路,迁移完成即根治。
|
||||
|
||||
- 【M】`dynamic_loader_unix.go`:ELF 检测 `DF_1_NODELETE` → 标记"不可热重载"。
|
||||
- 【M】`registry.go` 的 `ReloadOne`:对此类插件返回"需重启 homed"。
|
||||
- 【M】`pluginmgr/plugin.go` 的 `plugin_install`:返回 `restart_required` 替代 `reload_required`。
|
||||
- 【R】确认 `.so` 插件重载不再"假成功"。
|
||||
- 【V】单测:mock ELF 头带 NODELETE vs 不带 → 正确区分。
|
||||
|
||||
### 0.4 超时日志措辞修正 + 附带(11.2 短期项 + 11.4)— ⏭️ **已跳过**(同上)
|
||||
|
||||
> 11.2 的 cgo 超时不可中断在子进程模型下由 `Process.Kill()` 真正解决(§9.5);
|
||||
> 11.4 的 Lua 路径在迁移后统一走 RPC(三套 ABI 收敛),锁语义天然有边界。
|
||||
|
||||
- 【M】`internal/agent/core/toolcall.go:41`:日志从"已取消"改为"已放弃等待(插件仍在后台运行,其占用的线程无法回收)"。
|
||||
- 【M】`internal/plugin/lua_plugin.go:726`:stage 快照加 `sc.RLock()`/`RUnlock()`(11.4)。
|
||||
- 【R】措辞语义诚实;Lua 快照持锁。
|
||||
- 【V】`make test` 全绿;超时日志不再撒谎。
|
||||
|
||||
**Part 0 出口条件**:11.1/11.3/11.6 全部落地并有针对性测试;生产可先部署(现网止血)。
|
||||
|
||||
---
|
||||
|
||||
## Part 1:加载分派骨架(阶段 2.4,S)
|
||||
|
||||
> 依据:迁移评估 §2.4 / 3.2;plan.md 11.7。目标:让 registry 能按 entry 把插件分派到 `.so`(cabi)或 `.bin`(proc)两条通道——**双通道共存是整个计划可回退的前提**。
|
||||
|
||||
### 修改(核心)
|
||||
|
||||
- 【M】`internal/plugin/manifest.go`:`PluginManifest.Entry` 注释与 `IsPluginDir` 支持 `plugin.bin`。
|
||||
- 【M】`internal/plugin/dynamic.go`:新增 `binEntry = "plugin.bin"` 常量;`readManifest` 读取 entry。
|
||||
- 【M】`internal/plugin/registry.go` `loadOne`(~:376):把「无工厂 → `tryDynamic`」的分支改为按 entry 分派:
|
||||
```go
|
||||
switch entry {
|
||||
case soEntry, dllEntry: p, err = r.tryLoadSO(...) // 现有 cabi
|
||||
case binEntry: p, err = r.tryLoadProc(...) // 新增(Part 2 填充)
|
||||
default: p, err = r.tryOther(...) // lua / skill
|
||||
}
|
||||
```
|
||||
先保留一个 `tryLoadProc` 桩(返回"未实现"错误),保证分派骨架先成立、可测。
|
||||
- 【M】`internal/plugin/dynamic_loader_unix.go`:把 `tryLoadSO` 从 `tryDynamic` 拆出成 registry 可独立调用的函数。
|
||||
|
||||
### 审查
|
||||
|
||||
- 【R】确认内置插件(`hasFactory` 分支)完全不受影响——仍走 `RegisterNative` 进程内路径。
|
||||
- 【R】确认 `.so` 路径行为与今天逐字节一致(无回归)。
|
||||
- 【R】接口冻结:`git diff` 公开 SDK 为空。
|
||||
|
||||
### 验证
|
||||
|
||||
- 【V】单元测试:mock 三种 manifest(so/dll/bin/lua)→ 分派到正确通道;`.bin` 桩返回明确错误而非 panic。
|
||||
- 【V】既有 `.so` 插件加载 e2e 不回归(带一个真实 .so 冒烟)。
|
||||
|
||||
**Part 1 出口条件**:分派骨架在,`.bin` 有明确桩位,`.so` 全回归。
|
||||
#### ✅ **Part 1 已完成**(2026-08-31,commit `610e9d0`)
|
||||
|
||||
- 【M】✅ `dynamic.go`:新增 `binEntry`/`skillEntry` 常量 + `entryKind` 枚举 + `classifyEntry` / `detectEntryKind`
|
||||
- **manifest 的 entry 优先级最高**——把 entry 改回 `plugin.so` 即回退 cabi 通道(回退路径的保证)
|
||||
- 无 manifest 时按目录探测,`.bin` 优先于 `.so`(迁移期同目录两产物共存时走新通道)
|
||||
- 【M】✅ `registry.go` `tryDynamic`:按 entry 分派 proc/cabi;entry 声明 `.bin` 但二进制缺失时**报明确错误,不静默回退**
|
||||
- 【M】✅ `registry.go` `pluginEntryHash`:候选顺序与 `detectEntryKind` 对齐(`.bin` 优先),否则增量重载会用错文件算 hash
|
||||
- 【M】✅ `manifest.go`:`Entry` 字段注释补 `plugin.bin`
|
||||
- 【M】✅ `dynamic_proc_unix.go` / `dynamic_proc_windows.go`:`tryLoadProc` 桩位(存在性/类型/可执行权限校验已实现)
|
||||
- 【R】✅ 内置插件(`hasFactory` 分支)完全未受影响——仍走进程内 `RegisterNative`
|
||||
- 【R】✅ `.so` 路径行为与改动前一致(既有测试全绿,无回归)
|
||||
- 【R】✅ 接口冻结:`git diff third_party/homeagent-sdk/sdk/` 为空
|
||||
- 【V】✅ `entry_dispatch_test.go` 9 项全绿:
|
||||
- `TestClassifyEntry`(8 种 entry 分类)
|
||||
- `TestDetectEntryKind_ManifestWins` / `_ManifestCanForceRollback`(**回退路径验证**)
|
||||
- `TestDetectEntryKind_ProbeOrderPrefersBin` / `_ProbeFallbacks`(4 子例)
|
||||
- `TestTryLoadProc_MissingBinaryReturnsNil` / `_NonExecutableRejected`
|
||||
- `TestPluginEntryHash_PrefersBin` / `_EmptyForFactoryOnlyPlugin`
|
||||
- 【V】✅ `go build ./...` exit 0;`go test -race ./internal/plugin/...` 全绿;全量 32 个包测试通过
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Part 2:子进程通道原型(阶段 2.1~2.3/2.5/2.9,~3 周,核心风险点)
|
||||
|
||||
> 依据:迁移评估 §4.1 阶段 2;迁移评估指明可大幅参考 `clawhubadapter/sidecar.go:54-350`(已有 stdin/stdout + pending map + notifyCh)。
|
||||
> 目标:把单个外部插件(weather)以 `plugin.bin` 端到端跑通,验证"接口不变"假设。
|
||||
|
||||
### 修改(核心)
|
||||
|
||||
- 【M】新建 `internal/plugin/proc/`:
|
||||
- `process.go`——`procPlugin` 实现 `sdk.Plugin` 接口;`spawn`/健康检查/优雅停止/`Close()`=真 kill+wait。
|
||||
- **可参考** `clawhubadapter/sidecarProcess`:`exec.Cmd` + `stdin *bufio.Writer` + `readLoop`(scanner 大 buffer 64KB)+ `pending map[int]chan<- []byte` + `notifyCh chan OCNotification` + readerStop/readerWg。
|
||||
- `rpc.go`——双向 JSON-RPC 编解码:7 个 kernel→plugin 调用(`tool.invoke`/`stage.invoke`/`output.invoke`)+ 51 个 plugin→kernel 回调(平移自合同面 B 映射表)。
|
||||
- 【M】`internal/plugin/dynamic_loader_unix.go`:实现 `tryLoadProc`(spawn `.bin`,回连 stdio RPC)。
|
||||
- 【M】`internal/plugin/registry.go` `closePlugin`/卸载路径:对 proc 插件 `Close()` 真 kill。
|
||||
- 【M】`internal/agent/core/plugin_health.go` 调用侧:插件**退出码/EOF** → `recordCrash`(**逻辑完全复用**,仅把"panic 捕获"换成"进程退出检测",见迁移评估 §2.3)。
|
||||
|
||||
### 审查
|
||||
|
||||
- 【R】`readLoop` 鉴权:只接受来自本进程 spawn 的 stdout(防注入)。
|
||||
- 【R】JSON-RPC 帧边界处理(`bufio.Scanner` 长行截断风险——沿用 sidecar 64KB buffer)。
|
||||
- 【R】pending map 泄漏:超时清 map、退出时清 map。
|
||||
- 【R】崩溃重启:`SetAutoRestart(true)` 语义保留;`plugin_health` 冷却/自愈复用。
|
||||
- 【R】接口冻结:公开 SDK 零 diff。
|
||||
|
||||
### 验证
|
||||
|
||||
- 【V】单测:spawn→握手→工具调用往返→正常 Stop→kill 崩溃→退出码捕获。
|
||||
- 【V】weather `.bin` 端到端:`RegisterTool`/`Settings`/`InjectInputSync` 全部经 stdio RPC 打通。
|
||||
- 【V】与 Part 1 的 entry 分派联动:同目录 `.so` 与 `.bin` 共存互不干扰。
|
||||
|
||||
**Part 2 出口条件**:一个真实外部插件 `.bin` 全链路可用,崩溃隔离生效,接口零改动。
|
||||
|
||||
#### ✅ **Part 2 已完成**(2026-09-01,commit `d62430a` + `82dcc86`)
|
||||
|
||||
- `proc/protocol.go`:NDJSON 帧、**51 个 method id 平移为 method 名**(编号扔掉)、握手/stage/tool/output 参数类型。
|
||||
`case 25`(CORE_FREE_STRING) 无对应 method(GC 接管);`case 23/24`(事件订阅) 与 `io.setToolBlocks`
|
||||
明确返回未实现,**不静默成功**。
|
||||
- `proc/process.go`:Spawn/readLoop/CallContext/Notify/Stop/Kill/markExited;单帧上限 1MB。
|
||||
- `proc/corehandler.go`:51 case 平移 + `CoreSDK` 接口(**刻意排除**内核内部机制,见 Part 6 权限梯度)。
|
||||
- `proc/host.go`:**全部插件共享同一 memfd**。最初写成每插件一块段,尝试后发现
|
||||
那等于**副本模型换壳**(各写各段、各自回读、最后回读者覆盖前者),已改正。
|
||||
- `proc/stage.go`:RunStage 接线 + lockRegistry;`proc/plugin.go`:Plugin 实体。
|
||||
- 共享段分配按平台拆分(`shmalloc_linux.go` memfd / `shmalloc_darwin.go` 立即 unlink 的临时文件 /
|
||||
`shmalloc_other.go` 明确报错)——不静默降级成「无共享段」,那会让 stage 静默失去数据面。
|
||||
- registry 接线(commit `11c1bbc`):`tryDynamic` → `Registry.loadProc`;Host 惰创建且全局唯一;
|
||||
`StopAll` **锁外**释放共享段(插件还持有映射时拆段 → SIGBUS;持锁调与 onProcCrash 有锁序风险);
|
||||
`onProcCrash` 只发 EventSystem 事件,**不在回调里直接重载**(重载需 registry 锁)。
|
||||
- `proc_core.go` —— 权限梯度的类型系统落点:`procCore` 用**命名字段**持有 `*isdk.PluginSDK`,
|
||||
不是嵌入。嵌入会提升全部方法,外部插件就能经类型断言拿到
|
||||
Supervisor/Tracker/Adapter/Indexer/Status/Selftest。
|
||||
- 测试 36 项含 `-race`:`testdata/` 8 个假插件 + `e2e_template_test.go` 用**真实 plugindev 模板**
|
||||
编译插件跑全链路(验证「模板 ↔ 内核」协议/布局真的对齐,不只是内核自己跟自己对齐)。
|
||||
|
||||
---
|
||||
|
||||
## Part 3:plugindev 工具链改造(阶段 2.6/2.7/2.8,M,SDK 仓)
|
||||
|
||||
> 依据:合同面 B;迁移评估 §4.1。此部分在**独立 SDK 仓**维护(用户决策 sdk_repo_only)。
|
||||
> 目标:让外部插件能用普通 `go build` 产出 `.bin`,业务代码零改动。
|
||||
|
||||
### 修改(工具链)
|
||||
|
||||
- 【M】`tools/plugindev/templates.go`:新增 `tmplProcMain`——把 bridge 从「7 个 `//export` + `-buildmode=c-shared`」改为「`main()` + stdio JSON-RPC loop」;注册逻辑(`buildPluginSDK` 的 registar 闭包)从 `callVoid(id,...)` 改为 `sendRPC(methodName,...)`(合同面 B 的平移)。
|
||||
- 【M】`tools/plugindev/cmd_build.go`:
|
||||
- 新增目标 `plugin.bin`:`go build`(去 `-buildmode=c-shared`、`CGO_ENABLED=0`)→ `plugin.bin`。
|
||||
- bundle 平台表:`{"linux/amd64","plugin.bin"}`(替代 `.so`)。
|
||||
- `resolveBuild`:bin 分支不再需 C 编译器。
|
||||
- 【M】`tools/plugindev/cmd_build.go` `validBinaries`/打包:`.hmap` 内条目支持 `plugin.bin`(`plugin.json` entry 写 `plugin.bin`)。
|
||||
- 【M】`plg.json` 模板(`tmplPlgJSON`):`entry` 默认改为 `plugin.bin`(保留 `.so` 兼容)。
|
||||
|
||||
### 审查
|
||||
|
||||
- 【R】生成的 `tmplProcMain` 与旧 bridge 的 SDK 方法一一对应(对照合同面 B 51 行映射表逐行核对)。
|
||||
- 【R】业务代码**零改动**证据:同一 `plugin.go`,仅入口文件/构建命令不同。
|
||||
- 【R】交叉编译简化确认:`.bin` 无需 cgo 工具链,跨 GOOS 仅需目标 toolchain。
|
||||
|
||||
### 验证
|
||||
|
||||
- 【V】用新 plugindev 重编 `example/weather` → 产出 `plugin.bin`。
|
||||
- 【V】`.hmap` 打包/解包校验:`plugin.bin` 条目正确登记。
|
||||
- 【V】(与 Part 2 集成)weather.bin 被 homed proc 通道正确加载运行。
|
||||
|
||||
**Part 3 出口条件**:plugindev 一条命令产出 `.bin` + 正确 `.hmap`,外部插件源码零改动。
|
||||
|
||||
#### ✅ **Part 3 已完成**(2026-09-02,SDK 仓 commit `09b64dc`)
|
||||
|
||||
**模板落地方式换了**:不是计划里的 `templates.go` 新增 `tmplProcMain` raw string,
|
||||
而是真实 `.go` 源文件 `templates/proc_main.go.tmpl` + `//go:embed`(`proc_runtime.go`)。
|
||||
原因:900+ 行代码塞在字符串里写错只能等生成插件时才炸,作为源文件可被
|
||||
`go/parser`、`gofmt`、`go vet` 直接检查。这也是 `proc_runtime_test.go` 16 项
|
||||
静态检查得以存在的前提。
|
||||
|
||||
- `templates/proc_main.go.tmpl`(1113 行):51 个 method 的插件侧 RPC 实现
|
||||
(`procIO`/`procMemory`/`procSettings`/`procSocial`/`procLLM`/`procKnowledge`/
|
||||
`procDocMemory`/`procTextMemory`/`procPluginMgr`)、共享段访问(fd 3)与 16 字段
|
||||
StageContext 编解码、`handleStageInvoke`(拿锁 → 读段 → handler → **只写脏字段** → 放锁)。
|
||||
- `cmd_build.go`:`resolveBuild(target, proc)` 分派;proc 走 `go build -trimpath` + `CGO_ENABLED=0`,
|
||||
**交叉编译不再需要目标平台 C 工具链**。bundle 模式各平台产物同名(进程边界即 ABI 边界,
|
||||
无平台扩展名),故 zip 内加平台后缀 `plugin.bin.linux.amd64`。
|
||||
- `proc_runtime.go`:生成时清理残留 `z_bridge_gen.go`/`z_entry.c`——同目录两套 main 会编译冲突,
|
||||
这让 `.so` → `.bin` 切换无需人工清理。
|
||||
|
||||
**计划外补的一个真缺口**:`lifecycle.autoRestart` 没接线。公开 SDK 的 `SetAutoRestart`
|
||||
是纯 setter(`s.autoRestart = enabled`,无回调 hook)。C ABI 下内核在 `Start` 返回后
|
||||
直接读 `plgSDK.AutoRestart()`;子进程隔着进程边界读不到,插件调它只改自己进程内的副本。
|
||||
修法:模板在 `plg.Start()` 返回后显式上报一次(内核侧 `corehandler.go:145` 早已就绪)。
|
||||
**没有改公开 SDK 接口**。
|
||||
|
||||
验证(均已实测):
|
||||
```
|
||||
$ plugindev build # plg.json: entry = "plugin.bin"
|
||||
compiling linux/amd64 (子进程模式,CGO_ENABLED=0)...
|
||||
packaged weather_linux_amd64.hmap
|
||||
|
||||
build/plugin.bin → ELF 64-bit executable, statically linked ← 零 cgo
|
||||
dist/*.hmap → plugin.json + plugin.bin
|
||||
|
||||
$ diff example/weather/plugin.go <构建目录>/plugin.go
|
||||
✅ 逐字节一致 ← 业务代码零改动的硬证据
|
||||
|
||||
$ git diff third_party/homeagent-sdk/sdk/
|
||||
(空) ← 接口冻结保持
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4:共享内存数据面(阶段 3.1~3.5,~3 周,最高风险)
|
||||
|
||||
> 依据:迁移评估 §3.3 数据面 / 3.4 SDK 封装 / 3.7 锁仲裁;合同面 C。
|
||||
> 目标:多插件并发改写同一 `StageContext` 语义与今天一致(丢失率 → 0),外部插件看到全部 16 字段。
|
||||
|
||||
### 修改
|
||||
|
||||
- 【M】`internal/plugin/proc/` 新增 `shm.go`:
|
||||
- 共享段 schema:`ShmStageCtx` + `Slice{off,len}` 偏移描述符 + arena(append-only + 压实)。
|
||||
- arena 分配器:插件把 `FinalText` 从 10B 改 10KB 时分配新区域、旧区域留垃圾、stage 结束后压实。
|
||||
- 4 个 `Extra` 键(media_blocks/media_type/input_source/output_channel)提升为具名字段(迁移评估 §3.3 已核实全部使用点)。
|
||||
- 段生命周期:创建/挂载/插件崩溃后清理。
|
||||
- 【M】`internal/plugin/proc/shmcodec.go`:`StageContext` ↔ 共享段编解码(偏移↔Go 值转换)。
|
||||
- 【M】`internal/plugin/proc/lock.go`:**锁仲裁 RPC**——插件 `Lock/RLock` → `stage.lock`/`stage.unlock` → 内核 `sync.Mutex` 排队(迁移评估 §3.7 已裁定,实验 3+9 支撑)。
|
||||
- 【M】`internal/agent/core/stages.go` `RunStage`:改造为跨进程并发扇出(**保留并发语义,最难一环**)——内置插件仍进程内 `go func`,外部插件走共享段 + 锁仲裁。
|
||||
- 【M】SDK 侧(插件进程内)封装全部复杂度(迁移评估 §3.4):插件保留原生 `StageContext`,handler 照常读写,脏字段写回共享段。
|
||||
|
||||
### 审查(最高优先级 review)
|
||||
|
||||
- 【R】**并发语义一致性**:内置(0% 丢失)与外置(迁移前 35.8~36.8%)在共享内存下都收敛到 0% 丢失。
|
||||
- 【R】锁仲裁死锁:持锁进程崩溃自愈(实验 9 已证无需 robust mutex)。
|
||||
- 【R】arena 单 stage 写入上限:大写入在 SDK 层**报错**而非静默截断(迁移评估 §4.4)。
|
||||
- 【R】`Extra` 不引入通用 tagged union 成本(维持 4 键具名字段)。
|
||||
- 【R】接口冻结:`sdk/` 零 diff;`StageContext` 结构体字段序不变。
|
||||
|
||||
### 验证
|
||||
|
||||
- 【V】复刻实验 8:5 子进程 × 300 轮并发改写 → **零丢失零撕裂**。
|
||||
- 【V】复刻实验 13 现网场景:sanitizer(改 ToolResults)+ weather(只读)并发 → 清洗结果不再被覆盖。
|
||||
- 【V】改写型插件行为基线测试:`sanitizer`/`multimodal` 迁移前后行为对拍(迁移评估 §4.4 风险缓解)。
|
||||
|
||||
**Part 4 出口条件**:跨进程并发改写零丢失,内置/外置语义一致,16 字段全可见。
|
||||
#### ✅ **Part 4 核心已完成**(2026-08-31,commit `610e9d0`)—— 段 / 编解码 / 锁仲裁三件套
|
||||
|
||||
> 用户明确指出「基于共享内存的 stage 并发是最为关键的」,故先于 Part 2/3 落地数据面。
|
||||
> `RunStage` 的跨进程接线(3.4)待 Part 2 的进程通道就绪后进行。
|
||||
|
||||
- 【M】✅ `proc/shm.go` 段布局与 arena 分配器(§3.3)
|
||||
- `Header(64B) + ShmStageCtx(描述符数组 + 标志位) + append-only arena`
|
||||
- **相对偏移**:各进程 mmap 到不同虚拟地址仍能正确解引用
|
||||
- `NewSegment` / `AttachSegment` 带魔数 + 版本校验(版本不匹配显式报错,不静默错读)
|
||||
- **arena 用尽显式报错**而非静默截断(§4.4 风险登记的硬要求)
|
||||
- `Compact()` 回收 append-only 垃圾,须在无插件持锁时调用
|
||||
- 【M】✅ `proc/shmcodec.go` StageContext 16 字段跨进程编解码(§3.4)
|
||||
- **字段级描述符消除 lost update**:只改 `FinalText` 的插件完全不触碰 `ToolResults` 描述符
|
||||
- `WriteDirty` 只写脏字段——**只读插件零写入**,不可能覆盖他人改写
|
||||
- `Snapshot` 存**序列化字符串**(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过一次)
|
||||
- `Extra` 4 键提升为具名字段;`Response` 用标志位区分 nil 与空串(短路语义)
|
||||
- **全 16 字段可见**——今日经 C ABI 只有 10 个,`ContextMsgs`/`ReasoningContent`/`TokenUsage`/`Memory`/`Extra`/`Errors` 首次对外部插件可见
|
||||
- 【M】✅ `proc/lock.go` 锁仲裁回归内核(§3.7 已裁定,**零 cgo**)
|
||||
- `ForceRelease` 实现实验 9 的崩溃自愈 → 排除 robust pthread_mutex 必要性
|
||||
- 重复加锁**显式拒绝**(否则死锁 30s,比挂死更难排查)
|
||||
- 等待超时有补偿 goroutine 防锁永久泄漏
|
||||
- 【R】✅ 并发语义:`TestSegment_ConcurrentAppend_NoLostUpdate` 断言「各标记计数之和 == 最终长度 且 == 期望写入次数」,同时排除丢失与撕裂
|
||||
- 【R】✅ arena 上限报错(非静默截断):`TestSegment_ArenaExhaustionReturnsError`
|
||||
- 【R】✅ `Extra` 维持 4 键具名字段,未引入通用 tagged union 成本
|
||||
- 【R】✅ 接口冻结:`sdk/` 零 diff;`StageContext` 结构体未改
|
||||
- 【R】✅ `go vet` 干净(含 copylocks 检查)
|
||||
- 【V】✅ proc 包共享段部分 **16 项测试全绿(含 `-race`)**(全包现 36 项,含进程/端到端):
|
||||
- 段:魔数/版本校验、全 16 字段往返、Response nil vs 空串
|
||||
- 脏字段:只读零写回、原地改切片被识别、压实不破坏字段
|
||||
- **现网场景复刻**:`TestSegment_ProductionScenario_SanitizerNotOverwrittenByWeather`(sanitizer 清洗 + weather 只读并发,清洗结果不被覆盖)
|
||||
- **并发零丢失**:5 插件 × 40 轮读-改-写同一字段,200 次写入全部保留
|
||||
- 锁:互斥、串扰拒绝、未持锁释放拒绝、重复加锁拒绝、**崩溃自愈**、定向强制释放、临界区串行化
|
||||
|
||||
#### ✅ **Part 4 RunStage 接线已完成**(2026-09-01~09-02)
|
||||
|
||||
- `proc/stage.go` 把内核 `RunStage` 的并发扇出接到共享段:
|
||||
`Host.beginStage`(首个到达者独占段并写入 StageContext)→ `stage.invoke` RPC →
|
||||
插件侧 `stage.lock` → 读段 → handler → 只写脏字段 → `stage.unlock` →
|
||||
`Host.endStage`(最后离开者回读 + 压实 arena)。
|
||||
- **并发扇出保留**(§0.2 第 1 条:并发扇出是原始设计,不是缺陷);
|
||||
`stageMu` 串行化整次 stage 对共享段的独占(内核可能在不同路径并发触发
|
||||
RunStage,而段只有一份)。
|
||||
- 端到端验证(`e2e_template_test.go`,用**真实 plugindev 模板**编译的插件,
|
||||
而非 `testdata/` 手写假插件——后者只能验证内核自己跟自己对齐):
|
||||
- `TestE2E_RealTemplatePluginFullLifecycle`:握手 → init/start → 反向注册 →
|
||||
工具调用 → stage 读改写;同时验证 `FinalText` 回传
|
||||
(**C ABI 下 after_toolcall 看不到此字段**,§8.3 10→16)
|
||||
- `TestE2E_RealTemplateReadOnlyPluginDoesNotOverwrite`:两插件共享同一 Host 并发,
|
||||
只读插件不覆盖改写插件的结果(若每插件一块段,此测试必然失败)
|
||||
|
||||
**Part 4 已整体完成**。
|
||||
|
||||
---
|
||||
|
||||
## Part 5:通知面(阶段 4.1~4.5,~1.5 周)
|
||||
|
||||
> 依据:迁移评估 §3.6 事件环 / §2.4 约束 B / §3.8。目标:外部插件首次获得事件订阅能力,且不阻塞流式输出。
|
||||
|
||||
### 修改
|
||||
|
||||
- 【M】`internal/plugin/proc/eventring.go`:`EvtRing` + `Subscriber` schema(write_seq/read_seq/dropped/type_mask/last_seen),溢出计数、允许丢但让消费者知道丢了。
|
||||
- 【M】eventfd 通知 + Go netpoller 消费:`unix.Eventfd(EFD_NONBLOCK|EFD_CLOEXEC)` + `os.NewFile` 注册 netpoller(**不占 OS 线程**——实验 1 已证 200 goroutine 仅 +1 线程)。
|
||||
- 【M】`internal/events/bus.go` `Publish`:加事件环投递(**post-and-forget,绝不等待消费者**,满足约束 B)。
|
||||
- 【M】实现 `case 23/24`(今天空实现)——`Events().Subscribe` 对外部插件真正可用。
|
||||
- 【M】订阅者活性检测:`last_seen` 超时 → `recordCrash`。
|
||||
|
||||
### 审查
|
||||
|
||||
- 【R】`Bus.Publish` 路径**禁用任何锁/阻塞**——流式输出逐 token 发布,任何等待都会卡顿(迁移评估 §4.3 风险高)。
|
||||
- 【R】溢出语义:drops 计数暴露,不静默丢。
|
||||
- 【R】eventfd 计数合并:1000 token 事件只唤醒几次。
|
||||
|
||||
### 验证
|
||||
|
||||
- 【V】流式压测:长回复下 Publish 单次耗时不随订阅者数线性恶化。
|
||||
- 【V】复刻实验 4:post-and-forget 解耦(5s → 2.3ms 量级)。
|
||||
- 【V】外部插件订阅事件端到端(原空实现 case 23/24 现在可用)。
|
||||
|
||||
**Part 5 出口条件**:事件订阅对外可用,流式输出无卡顿。
|
||||
|
||||
---
|
||||
|
||||
## Part 6:迁移与收尾(阶段 5.1~5.4,~2 周)— ✅ **已完成**(2026-09-03)
|
||||
|
||||
> 依据:迁移评估 §4.5 双通道共存、§5 权限梯度。
|
||||
>
|
||||
> ⚠️ **实际执行偏离计划的一处**:原计划「逐插件迁移,随时回退」。
|
||||
> 用户决策改为**彻底舍弃 `.so` 能力,无回退通道**(不做 `--cabi` 开关),
|
||||
> 本轮直接删 `internal/plugin/cabi/`,生产全量切换。代价是某插件出问题
|
||||
> 只能紧急修复或 `git revert` 整批。因此下方【V】的「`.so` ↔ `.bin` 混跑」
|
||||
> 不再适用——新内核根本不认 `.so`。
|
||||
|
||||
### 修改
|
||||
|
||||
- ✅【M】**6.1** 工具链 entry 语义收敛(SDK 仓 `9f84412`):`isProcEntry` 删除,Go 插件一律产出 `plugin.bin` 不看 entry 值;`templates.go` 1296→516 行。
|
||||
- ✅【M】**6.3** 17 插件全量重编(`1d7f011`):16 个×3 平台 + qq×1;`git status example/` 无输出(业务代码零改动)。
|
||||
- ✅【M】**6.5** 生产切换(`62bdfa2`):经 `pluginmgr` 的 hmap 正规通道安装,17/17 成功且 `config_kept=true`。
|
||||
- ✅【M】**6.6** 压测 + 版本 1.0.0 + 文档(`2572688`、`670efcd`、tag `v1.0.0`)。
|
||||
- ✅【M】**6.2** 内核侧 Windows(`d027c96`)+ 删 C ABI(`b20121f`,-3198 行):删 `internal/plugin/cabi/`(1156)、`dynamic_dll_windows.go`(272)、`dynamic_loader_unix.go`(79) + bridge 模板;新增 `shmalloc_windows.go` + `evtfd_windows.go` + `shmpass_{unix,windows}.go`;顺带修 macOS pipe 写端被 GC 回收的真 bug。
|
||||
- ✅【M】**6.4** 权限梯度显式化(`2ebdb9a`):54 个 method 划入 11 个 capability 组;`coreHandler.Handle` 入口强制;`withheldCapabilities` 表记录 10 项刻意不提供的内核机制及理由(`SelftestAPI`/`SupervisorAPI`/`TrackerAPI`/`StatusAPI`/`AdapterAPI`/`ConfigAPI`/`ToolAPI`/`IndexerAPI`/`OutputChanRaw`/`EventPublish`)。
|
||||
- ⏭️【M】`lua_plugin.go`/`dynamic_lua.go` 统一走 RPC —— **留待后续**。Lua 走解释器不经 C ABI,不阻塞本轮目标(消除 C ABI 前提缺陷)。收敛第三套 ABI 是独立优化。
|
||||
- ✅【M】文档:本文与 `plugin-interface-matrix.md` 更新;切换实录见下方。
|
||||
|
||||
### 审查
|
||||
|
||||
- ✅【R】每删一个 cabi 依赖项,`go build ./...` + `go vet ./...` 干净。
|
||||
- ✅【R】权限梯度:被拒 API 在 RPC 边界返回**明确错误**(非忽略)。错误消息含四要素:哪个插件、哪个调用、缺什么能力、在哪声明。`TestCapability_DeniedErrorIsActionable` 守护。
|
||||
- ✅【R】接口冻结:`git diff third_party/homeagent-sdk/sdk/` 全程为空。
|
||||
|
||||
### 验证(全量回归)
|
||||
|
||||
- ✅【V】17 插件经 `plugin_install(overwrite=true)` 加载,工具/设置/通道/阶段 e2e。
|
||||
- ⏭️【V】~~`.so` ↔ `.bin` 混跑集群冒烟~~ —— 不适用(无回退通道,见上方偏离说明)。改为验证**新内核面对旧 `.so` 给可操作错误且不崩溃**,已在真实二进制上确认。
|
||||
- ✅【V】`make test` 全量绿 + `go build ./...`。
|
||||
- ⚠️【V】内存:**未达成计划目标**。15 个插件进程 RSS=88.0MB / PSS=87.9MB,远超「基线 +29MB」。根因是每插件静态链接整个 Go runtime,15 个不同二进制无共同物理页可映射(PSS/RSS 99.9% vs 基线 44%)。这是「每插件独立二进制」的固有代价,实际开销高于 §4.3 乐观估计。压缩方向:共享 launcher 二进制 + 各自业务模块。
|
||||
- ✅【V】工具调用 RPC 延迟 24.1µs(实验 11 基线 19.6µs,同量级)。
|
||||
|
||||
**Part 6 出口条件**:全部外部插件 `.bin` 化 ✅,cabi 删除 ✅,接口零改动 ✅,权限显式化 ✅,无回归 ✅。
|
||||
|
||||
---
|
||||
|
||||
## 最终验收清单(对照接口不变矩阵 §7 检查点)
|
||||
|
||||
| # | 检查点 | 通过标准 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | 公开 SDK 接口冻结 | `git diff third_party/homeagent-sdk/sdk/` **为空**(全程) | ✅ 每次审查均确认 |
|
||||
| 2 | 外部插件业务代码零改动 | 17 个 `example/*/plugin.go` 与基线逐字节可比 | ✅ `git status example/` 无输出 |
|
||||
| 3 | 17 插件 `.bin` 化 | 全部经 `plugin_install` 加载,工具/设置/通道/阶段 e2e | ✅ 17/17,`config_kept=true` |
|
||||
| 4 | cabi 删除 | `internal/plugin/cabi/` 与 bridge 模板不存在 | ✅ -3198 行(`b20121f`) |
|
||||
| 5 | 崩溃隔离 | 插件 kill 只退出自身,homed 存活 | ✅ `TestRealPlugin_CrashDoesNotKillKernel` |
|
||||
| 6 | 热重载 | 同路径换 `.bin` 即生效,无需重启 | ✅ 生产实测(`unloaded (config kept)` → 重载) |
|
||||
| 7 | 并发改写 | 跨进程 stage 丢失率 0%(对照今天 35.8~36.8%) | ✅ `TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate` |
|
||||
| 8 | 事件订阅 | 外部插件 `Events().Subscribe` 可用 | ✅ 事件环已接线(当前零用户) |
|
||||
| 9 | 多模态 | `SetToolBlocks` 非空实现 | ⚠️ method 已定义并划入 core 能力,内核侧仍返回未实现 |
|
||||
| 10 | 超时取消 | 工具超时可 `Process.Kill()`,零泄漏 | ✅ 整套新架构零 cgo |
|
||||
| 11 | output_send | 真实结果返回(非假成功) | ✅ 生产实测 `map[status:sent]` |
|
||||
| 12 | 权限梯度 | 内部专属 API 在 RPC 边界拒绝 | ✅ 12 项测试(`2ebdb9a`) |
|
||||
| 13 | 内存/延迟 | 常驻 +≤29MB,RPC p50 ≤20µs 量级 | ⚠️ 延迟 24.1µs 达标;内存 88MB **未达标** |
|
||||
|
||||
**两项未完全达标的说明**:
|
||||
|
||||
- **#9 SetToolBlocks**:`io.setToolBlocks` 已在 protocol 定义并划入 `CapCore`,
|
||||
但内核侧 handler 仍返回未实现。C ABI 时代它也是空实现(§1.4),
|
||||
故**不是回归**,但也没兑现 §3.8 的承诺。当前无插件使用。
|
||||
- **#13 内存**:15 个进程 RSS=88.0MB,远超「基线 +29MB」。根因是每插件
|
||||
静态链接整个 Go runtime,15 个不同二进制无共同物理页(PSS/RSS 99.9%
|
||||
vs 基线 44%)。实验 5 的基线用的是 2.68MB 最小插件,而真实插件 3.1~14.8MB,
|
||||
绝对数字不可比。结构性指标(均摊线程 5.5 vs 4.9)同量级。
|
||||
|
||||
---
|
||||
|
||||
## 风险与回退
|
||||
|
||||
| 风险 | 缓解 | 回退 |
|
||||
|---|---|---|
|
||||
| Part 2/4 `RunStage` 并发语义漂移 | 复刻实验 8/13 + sanitizer/multimodal 对拍(Part 4 review) | entry 分派切回 `.so`(Part 1 双通道) |
|
||||
| Part 4 `Bus.Publish` 阻塞卡顿 | 专项流式压测(Part 5) | 事件环投递后置,先降级进程内 |
|
||||
| Part 3 工具链 `.bin` 产物问题 | 单插件 weather 先行验证 | 保留 `.so` 构建分支 |
|
||||
| Part 6 17 插件回归 | 逐个迁移 + `plugin_install(overwrite)` | 任意一个失败立即回退该插件 entry |
|
||||
| 接口意外漂移 | 每部分【R】强制 `git diff sdk/` 检查 | 立即 revert,暴露合同面违约 |
|
||||
|
||||
---
|
||||
|
||||
*规划:2026-08-31,update 分支。Part 编号与其依赖的 plan.md/迁移评估阶段对应。*
|
||||
|
||||
---
|
||||
|
||||
## Part 6.5 生产切换实录(2026-09-03)
|
||||
|
||||
### 执行顺序(先换二进制,再装包)
|
||||
|
||||
```
|
||||
1. systemctl stop homeagent
|
||||
2. 换 /usr/local/bin/homed
|
||||
3. 起服务 —— 15 个 .so 插件报可操作错误被跳过,homed 与 16 个内置正常
|
||||
4. 逐个 POST 装 17 个 hmap(overwrite=true)
|
||||
5. 重启核对
|
||||
```
|
||||
|
||||
**为何不能反过来**:若先装包,旧 homed 的 `StopAndUnload` 会停掉 qq
|
||||
消息通道,而它又无法加载 `.bin`,会卡在「插件全挂」的状态。
|
||||
|
||||
第 3 步顺带在真实二进制上验证了 Part 6.2 的可操作错误:
|
||||
|
||||
```
|
||||
[plugin] dynamic weather: plugin weather: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。
|
||||
外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin(业务代码无需修改)
|
||||
```
|
||||
|
||||
不崩溃,只跳过该插件。
|
||||
|
||||
### 走 hmap 正规通道,而非手工拷贝
|
||||
|
||||
第一版切换脚本是手工拷 `plugin.bin` + 手改 `plugin.json` 的 entry ——
|
||||
那等于**重新实现了一遍 hmap 解包逻辑,且实现得更差**。漏掉的东西:
|
||||
|
||||
| | 手工拷贝 | hmap 正规通道 |
|
||||
|---|---|---|
|
||||
| `platforms` 字段 | 漏了 | 包内 manifest 本来就写对 |
|
||||
| 平台二进制选择 | 硬编码 `_linux_amd64` | `platformBinary()` 按 runtime 选 |
|
||||
| `overwrite` 语义 | 无 | `StopAndUnload` **保留配置表** |
|
||||
| 失败回滚 | 无 | `os.Rename` 备份,解包失败自动恢复 |
|
||||
| 校验 | 只查文件存在 | `validatePackage` 查 manifest + 各平台二进制齐全 |
|
||||
|
||||
配置保留那条尤其关键:生产 17 个插件都有配置(qq 账号、weather 默认城市、
|
||||
browser profile 路径)。手工脚本恰好没碰配置表所以侥幸不丢,但那是运气不是设计。
|
||||
|
||||
最终实现:POST 到 `127.0.0.1:9876/plugins`,传 `{path, overwrite:true}`。
|
||||
保留的一个设计是**先全部校验再动手**——任一插件缺 hmap 就整批中止,
|
||||
因为新 homed 不认 `.so`,「一半装了一半没装」的中间态最难排查。
|
||||
|
||||
### 结果
|
||||
|
||||
```
|
||||
17/17 成功,全部 config_kept=true
|
||||
0 个残留 .so;17 个 plugin.bin 均有执行位
|
||||
17 个 manifest 的 entry 均为 plugin.bin;无 .bak 残留
|
||||
bundle 包正确挑了当前平台(weather 目录只留 8.7MB 的 linux/amd64 那份)
|
||||
```
|
||||
|
||||
备份:`/home/newqqagent-migration-backup-20260902-214812`
|
||||
(plugins 全目录 + homed.old + homeagent.service,162MB)。
|
||||
**唯一回滚路径**是恢复该目录 + 回滚 homed 二进制。
|
||||
|
||||
### 生产端到端验证(真实 QQ 消息)
|
||||
|
||||
```
|
||||
input from qq → response (83293ms, tools=[qq_get_message qq_get_history
|
||||
output_send__qq output_send__qq qq_mark_read])
|
||||
```
|
||||
|
||||
逐环节:
|
||||
|
||||
- **输入**:qq 子进程收 webhook → 经 RPC 报给内核 → agent 主循环
|
||||
- **工具调用**:5 次跨进程调用全部成功(内核反向调用进子进程执行)
|
||||
- **stage 改写生效**(最关键的一条):
|
||||
```
|
||||
[sanitizer] cleanToolCallLeakage: 2 bytes removed
|
||||
[sanitizer] cleaned 2 bytes (before=13590 after=13588)
|
||||
[proc] sanitizer stage post_action 改写了 1 个字段
|
||||
```
|
||||
sanitizer 在**另一个进程里**改了 StageContext,内核读到了改写结果。
|
||||
13590 字节文本经共享段传递、被改写、写回,全程未拷贝整个上下文。
|
||||
- **输出真的送达**:`tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent]`
|
||||
—— 直接验证 Part 0.1 修的 output_send 假成功缺陷(§9.4)
|
||||
- **arena 生命周期正常**:每次 stage 结束都压实回收(单次最高 15802 字节),无泄漏累积
|
||||
|
||||
这一次对话触发约 20 次 stage、5 次工具调用、2 次输出发送,跨越 15 个插件子进程。
|
||||
旧架构下同样流程有三处会静默出问题:stage 并发写丢字段(§8.4 实测 35.8~36.8%
|
||||
lost update)、output_send 假成功、cgo 超时泄漏 goroutine。现在这些在日志里可见且正确。
|
||||
|
||||
---
|
||||
|
||||
## Part 6.6 压测与延迟实测
|
||||
|
||||
基准与压测在代码里(`internal/plugin/proc/bench_test.go` + `streaming_test.go`),
|
||||
非独立脚本——随代码演进自动跑,不会腐坏。
|
||||
|
||||
| 项目 | 实测 | 基线 | 判断 |
|
||||
|---|---|---|---|
|
||||
| 工具调用 RPC 往返 | 24.1 µs | 实验 11: 19.6 µs | 同量级 |
|
||||
| 锁仲裁(内核侧) | 0.76 µs | — | 见下注 |
|
||||
| 事件环写入 | 95 ns | — | 亚微秒 |
|
||||
| 事件环并发写入 | 83 ns | — | 无锁竞争恶化 |
|
||||
| 完整 stage 往返 | 132 µs | — | 含 3 次进程间往返 |
|
||||
| 共享段编解码 | 3.7 µs | — | 占 stage 的 2.8% |
|
||||
|
||||
**锁仲裁 0.76µs 不可与实验 3 的 19.40µs 对照**——测的不是同一个东西:
|
||||
实验 3 测插件经 RPC 请求锁的完整跨进程往返,本基准只测内核侧
|
||||
`lockRegistry.acquire/release`。真实成本仍在 20µs 量级。基准原名
|
||||
`BenchmarkStageLockRoundTrip` 有误导性,已改为 `BenchmarkStageLockArbitration`。
|
||||
|
||||
**stage 往返 132µs 的成本构成**:共享段编解码只占 3.7µs,其余是
|
||||
**一次 stage 要走 3 次进程间往返**(`stage.invoke` + 插件侧反向的
|
||||
`stage.lock` / `stage.unlock`)。相对 LLM 往返 2-8 秒可忽略;
|
||||
要优化的方向是把 lock/unlock 合入 `stage.invoke` 的请求/应答。
|
||||
|
||||
### 流式压测(§4.3 标记「风险高」的那一项)
|
||||
|
||||
```
|
||||
5000 次 Publish + 每条睡 20µs 的慢消费者
|
||||
实测 2.29ms,均摊 457 ns/token
|
||||
同步语义理论下限 100ms
|
||||
|
||||
订阅者 1 个:1.547ms(515 ns/次)
|
||||
订阅者 8 个:1.518ms(506 ns/次) ← 无线性恶化
|
||||
|
||||
环溢出(无消费者写 30000 次,cap=8192):均摊 35 ns/次 ← 仍 O(1)
|
||||
```
|
||||
|
||||
2.29ms 与实验 4 的数字完全一致(那次也是 2.29ms / 0.46µs per token),
|
||||
post-and-forget 在实现中成立。第三项的意义:消费者完全停摆时写端覆盖
|
||||
最旧 slot,这条路径仍是 O(1),故「插件卡住」不会连带拖慢内核主循环。
|
||||
|
||||
---
|
||||
|
||||
## 版本号
|
||||
|
||||
v1.0.0(tag 已打)。公开 SDK 接口零改动,但产物形态从 `plugin.so` 变为
|
||||
`plugin.bin`,0.9.x 内核不会识别——不可互操作的破坏性变化,故跃主版本号。
|
||||
|
||||
⚠️ **Makefile 陷阱**:`VERSION ?= $(shell git describe --tags --dirty)`
|
||||
意味着实际注入值来自 git tag,`meta.go` 里的默认值只在不带 ldflags 时生效。
|
||||
打 tag 前 `make build` 注入的是 `v0.9.1-56-g2572688-dirty`。
|
||||
|
||||
同时删掉 C ABI 时代的死常量(`ABIVersion`/`CABINum`/51 个 `Core<Method>`
|
||||
整数 ID)——随 Part 6.2 删 `internal/plugin/cabi/` 就已无使用者,
|
||||
留着会让人以为 C 层协商还在生效,或以为加 method 要同步维护那张整数表。
|
||||
@ -41,7 +41,7 @@ func tryLoadProc(dir, name string, config map[string]interface{}) (sdk.Plugin, e
|
||||
// 子进程插件加载(plugin.bin)——外部插件多进程化的加载入口。
|
||||
//
|
||||
// 设计依据:docs/zh/架构迁移评估.md §3(stdio JSON-RPC 控制面 + shm 数据面 + eventfd 通知面)
|
||||
// 实施计划:docs/zh/plugin-migration-plan.md Part 2/3
|
||||
// 实施记录见 docs/zh/架构迁移评估.md(子进程化论证与实验数据)
|
||||
|
||||
// validateProcBinary 校验 plugin.bin 是否存在且可执行。
|
||||
// 返回 ("", nil) 表示该目录不是 proc 插件。
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// entry 分派(docs/zh/plugin-migration-plan.md Part 1/6)。
|
||||
// entry 分派(见 docs/zh/架构迁移评估.md 的入口双通道章节)。
|
||||
//
|
||||
// C ABI 通道(.so/.dll/.dylib)已整体退场:外部插件统一走子进程 + stdio RPC。
|
||||
// 这些测试守住的是「旧产物给明确错误」而非「静默跳过」——后者会让
|
||||
|
||||
22
plan.md
22
plan.md
@ -1,5 +1,11 @@
|
||||
# HomeAgent 生产问题修复计划
|
||||
|
||||
> **文档定位(2026-09-19 核)**:本文是**历史工单 + 仍活跃路线图**的混合档。
|
||||
> 开头 §0.1/§0.2 的两个「紧急」项与 §1~§11 的历史条目**均已解决**(见各节勾选),
|
||||
> 下方 §13 是**仍活跃**的推进路线图。想知道当前架构请读
|
||||
> `assets/docs/zh/ARCHITECTURE.md`;想知道当前调度/驻留子设计请读
|
||||
> `docs/zh/input-scheduler-design.md` 与 `docs/zh/resident-subagent-design.md`。
|
||||
|
||||
## 设计意图备忘(核心架构原则)
|
||||
|
||||
本框架的两大核心设计意图,贯穿所有插件/记忆/工具设计,**所有改动必须符合**:
|
||||
@ -14,9 +20,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.1 紧急:healthcheck 健康检查污染真实存储 ⚠️ 正在持续污染
|
||||
## 0.1 ~~紧急~~ 已解决:healthcheck 健康检查污染真实存储
|
||||
|
||||
**现象**(2026-08-11 22:02 起,每 30 分钟一次):日志反复出现
|
||||
> **状态:已修复(2026-09-19 实测确认)**。本文保留为定位过程记录。
|
||||
> 验证方式:`ls /home/newqqagent/knowledge/ | grep -c _hc_` = 0;
|
||||
> `memory/graph.db` 内 `_hc_` 表/行 = 0(用 sqlite3 查 sqlite_master)。
|
||||
|
||||
**当时的现象**(2026-08-11 22:02 起,每 30 分钟一次):日志反复出现
|
||||
`[knowledge] added: _hc_knowledge_test_<ts>`,且知识库出现 `gotest`、`luatest`、`_hc_knowledge_test_*` 等测试残留。
|
||||
|
||||
**根因**:`internal/plugins/healthcheck/plugin.go` 的三个"写入通道"自检**全部在真实生产存储上写入再删除**:
|
||||
@ -41,7 +51,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.2 紧急:QQ 消息被无视(agentcli 幽灵终端自喂送风暴)⚠️ 优先处理
|
||||
## 0.2 ~~紧急~~ 已解决:QQ 消息被无视(agentcli 幽灵终端自喂送风暴)
|
||||
|
||||
**现象**(2026-08-11 20:4x):用户发 QQ 私聊消息,agent 不回应。日志显示 agent 被 `agentcli` 终端 echo 洪水完全阻塞。
|
||||
|
||||
@ -923,7 +933,8 @@ context 累积导致的内存增长。
|
||||
> 三项合入门禁**已全部通过**:`make test` 零失败、`go vet ./...` 无告警、
|
||||
> `git diff main -- third_party/homeagent-sdk/sdk/` 为空(接口冻结不变量)。
|
||||
>
|
||||
> 详细执行记录见 `docs/zh/plugin-migration-plan.md`(Part 0~6 全部标记完成)。
|
||||
> 迁移的执行记录(Part 0~6 过程稿)已随迁移完成而删除;论证与实验数据仍见
|
||||
> `docs/zh/架构迁移评估.md`。
|
||||
|
||||
### 12.1 ✅ 已完成:合并到 main + 发布分支(2026-09-03 ~ 09-06)
|
||||
|
||||
@ -956,8 +967,7 @@ SDK 仓 `v1.0.0` / `v1.1.0`。main 的版本路牌现为 `1.2.0`(尚无 tag)
|
||||
|
||||
### 12.2 验收清单里两项**未达成**的目标
|
||||
|
||||
这两项在 `docs/zh/plugin-migration-plan.md` 的最终验收清单里如实标了 ⚠️,
|
||||
不是遗漏而是明确的未兑现承诺。
|
||||
这两项在迁移过程稿的最终验收清单里如实标了 ⚠️,不是遗漏而是明确的未兑现承诺。
|
||||
|
||||
#### 12.2.1 `SetToolBlocks` 仍是未实现(承诺未兑现)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user