From b889934bba394f9ead7a9b0e87cf3ed91659a225 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 6 Sep 2026 15:07:14 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=88=B0=20v1.1.1=EF=BC=8C=E8=A1=A5=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E8=AE=B0=E5=BF=86=E4=B8=8E=E6=8E=A5=E5=8F=A3=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 五处文档此前停在 v1.0.0,而 v1.1.0/v1.1.1 都已发布并在现网运行。 本轮补齐三层记忆的媒体架构、模型工具的媒体参数、通信面 method 数, 以及冻结解除后的替代约束。 ## 中英双语 OVERVIEW.md 三层记忆段只写了 Context/Document/Graph 三层,而 v1.1.0 起图片/音频是 三层里的一类节点。补上媒体记忆的架构概要:CAS + 引用计数 GC + 标记格式 (描述文本才是持久语义记忆,blob 是可淘汰的缓存)+ 插件边界贯通。 ## 中英双语 ARCHITECTURE.md - 「记忆工具」表补 `memory_commit`/`doc_commit` 的 `media_digests` 与 `sentence_text`(后者从未暴露给模型,而它是媒体绑定链的必经环节) - 「三层记忆」段新增媒体记忆子节:CAS 设计表(寻址/完整性/写入原子性/引用/GC)、 标记格式、可选性(`enabled=false` 时整条链路静默退化) - 「三个通信面」从 51 改为 55 个 method,新增四个 media method 的说明 - 「SDK 四通道」代码示例补媒体注入三方法 + 与 SetToolBlocks 的区别 ## plugin-interface-matrix.md - 状态从「完成 v2」改「完成 v3」,v3 记录 v1.1.1 的接口扩展 - §二 A3 DocMemoryAPI 补 InsertWithMedia - §二 A4 补三个媒体注入方法 + 为何不能搭 SetToolBlocks 的车 - §二 A5 补 MediaAttachment 类型 + Triple/Doc/TextEvent 的扩展字段 - §六「新获得的能力」补 SetToolBlocks 已落地、媒体入记忆、插件主动发起 带媒体的对话 - §七 冻结检查点补第 5 条(冻结已解除,取代它的是 §九) - 新增 §九「v1.1.x 的接口扩展规则」:冻结解除后的三条硬约束 (只增不减签名不改 / 新增方法方向 / 模板接线六处失败链)+ 验证方式 (存量插件 17/17、旧产物 4/4 建链、模板断言、压测 -race) ## 为何分立两笔 commit 前一笔(SDK 仓 README + 内核 README)改的是给**插件开发者**看的文本, 本笔改的是给**架构师与维护者**看的技术文档。受众与改动层次不同, 放在同一个 commit 会让追溯时看不出"文档在哪一层跟上了代码"。 --- assets/docs/en/ARCHITECTURE.md | 50 +++++++++++++++++- assets/docs/en/OVERVIEW.md | 13 +++++ assets/docs/zh/ARCHITECTURE.md | 48 ++++++++++++++++- assets/docs/zh/OVERVIEW.md | 11 ++++ docs/zh/plugin-interface-matrix.md | 82 ++++++++++++++++++++++++++++-- 5 files changed, 198 insertions(+), 6 deletions(-) diff --git a/assets/docs/en/ARCHITECTURE.md b/assets/docs/en/ARCHITECTURE.md index 3c5ded6..a898776 100644 --- a/assets/docs/en/ARCHITECTURE.md +++ b/assets/docs/en/ARCHITECTURE.md @@ -189,6 +189,39 @@ All vectorization unified under `StaticEmbedder` (`internal/memory/static_embedd | `doc_query` | Search from Document | | `doc_commit` | Write to Document | +Since v1.1.1 `memory_commit` and `doc_commit` accept `media_digests`, and the kernel appends the +`[ ] ` marker into the sentence/body — **the kernel builds the +marker, the model only supplies the digest**. Requiring the caller to know the format would mean a +single typo silently breaks reference binding with no error anywhere in the chain. `memory_commit` +also gained `sentence_text`: media references hang off a sentence, so with no sentence there is +nowhere to attach them. + +### Media Memory (since v1.1.0) + +`internal/memory/media/` — `Store`, content-addressed (CAS) + +| Concern | Approach | Why | +|---|---|---| +| Addressing | sha256 digest; metadata in SQLite, blobs on disk | Identical bytes stored once; metadata must be queryable, blobs must not live in the database | +| Integrity | Every `Get` re-verifies the digest | Silently returning corrupt data on disk damage is far worse than an error | +| Write atomicity | `.tmp` + rename | A half-written file taken as complete content would permanently poison that digest | +| References | `owner_kind/owner_id/digest` composite primary key, `AddRef` idempotent | Three owner kinds: `context` (context events), `document`, `graph_sentence` | +| GC | Two-stage with `minAge`, **referenced items are never deleted** | Description text stays in the text layers while blobs may be evicted — semantic memory and byte cache are decoupled | + +**How media is represented in plain-text memory** is the marker `[ ] `: + +``` +[image/png a1b2c3d4e5f6] a purple-blue-red three-band chart +``` + +Why it must ride on text: `Doc.Content`, `sentences.text` and text memory's `Input` are all strings +— there is no field to carry structured data. **The description text is the durable semantic +memory** (retrieval uses it); the digest is the key back to the bytes (reverse lookup uses it). +After capacity GC evicts a blob, the description remains in the L0/L2/L3 text. + +The media store is **optional throughout**: with `core.memory.media.enabled=false` or no +configuration, the whole chain silently degrades to plain-text behaviour — no errors, no panics. + ### Other Memory Layers - **Social** (`internal/memory/social/social.go`) — Persona traits and relationship network, wraps GraphDB entity types @@ -296,7 +329,7 @@ Common ground: | Plane | Mechanism | Why this choice | |---|---|---| -| Control | stdio JSON-RPC (NDJSON frames), 51 `core.*` methods | The process boundary *is* the ABI boundary—no need to maintain three platform-specific dynamic-library loaders | +| Control | stdio JSON-RPC (NDJSON frames), 55 `core.*` methods | The process boundary *is* the ABI boundary—no need to maintain three platform-specific dynamic-library loaders | | Data | Shared memory segment, **one segment shared by all subprocesses** | One segment per plugin would degrade "kernel ctx → segment → plugin mutates → read back" into the copy model under concurrency, reproducing lost updates exactly | | Notification | Event ring + platform notify (Linux eventfd / macOS pipe / Windows Event) | The kernel must never block on a consumer: streaming output publishes per token, so any wait shows up as stutter | @@ -334,8 +367,23 @@ sdk.Memory().Recall/Commit sdk.Knowledge().Search/Create sdk.Settings().Get/Set/List sdk.RegisterOutputChannel("qq", sdk.CapText|sdk.CapAudio|sdk.CapImage, "QQ channel, see output_send__qq_help for details", handler) + +// v1.1.1 media APIs (all additive, no signature changes) +sdk.DocMemory().InsertWithMedia(doc, attachments) // attachments with Data land in CAS; Digest-only ones reference existing content +sdk.InjectInputMedia(source, channel, text, blocks) // media reaches the model in *this* turn +sdk.InjectInputMediaSync(...) // same, and waits for the reply +sdk.InjectInterruptMedia(...) // media-bearing interrupt, can preempt current processing ``` +How media injection differs from `SetToolBlocks`: the latter is only callable inside a tool handler +and its media reaches the model with the **next** tool message; these three let a plugin +**initiate a turn that carries media** — it goes out with this turn's message and is automatically +stored in CAS with a memory reference attached. `Triple` and `Doc` gained `MediaDigests` / +`Attachments` correspondingly. + +`internal/sdk/` is the bridge implementation for this layer and is not subject to the public +interface freeze (see `docs/git-branching.md` §6). + ### Plugin Interface ```go diff --git a/assets/docs/en/OVERVIEW.md b/assets/docs/en/OVERVIEW.md index ee913c3..c71263b 100644 --- a/assets/docs/en/OVERVIEW.md +++ b/assets/docs/en/OVERVIEW.md @@ -25,6 +25,19 @@ The significance lies in clear responsibility boundaries: the kernel focuses on Three progressive layers — context, cold archive, long-term graph memory — form an information decay and consolidation pipeline from short-term to persistent storage. +**Media Memory (since v1.1.0)** — Images and audio are not attachments; they are a kind of node in all three layers: +- **Content-addressed store (CAS)**: addressed by digest, metadata in SQLite and blobs on disk, identical bytes + stored once. Every `Get` re-verifies the digest (silently returning corrupt data is worse than an error). +- **Reference-counted GC**: `owner_kind/owner_id/digest` is the primary key; context events, documents and graph + sentences each hold their own references. **Referenced items are never deleted** — only unowned content past + `minAge` is reclaimed. +- **The description text is the durable semantic memory**: what the vision model produced is written into + plain-text memory as a `[ ] ` marker and participates in vector retrieval and + distillation; the blob is only a cache that capacity GC may evict. Months later "that purple-blue-red + three-band chart" is still findable — via the description, not the bytes. +- **Reaches the plugin boundary since v1.1.1**: plugins read and write media through `InsertWithMedia` / + `InjectInputMedia`; the model attaches media via the `media_digests` argument of `memory_commit` / `doc_commit`. + ## What It Actually Does Code is in the project root, implemented in Go. diff --git a/assets/docs/zh/ARCHITECTURE.md b/assets/docs/zh/ARCHITECTURE.md index a54bb9d..8068644 100644 --- a/assets/docs/zh/ARCHITECTURE.md +++ b/assets/docs/zh/ARCHITECTURE.md @@ -189,6 +189,36 @@ eventLoop() → processTextInput() | `doc_query` | 从 Document 搜索 | | `doc_commit` | 写入 Document | +`memory_commit` 与 `doc_commit` 自 v1.1.1 起接受 `media_digests`,并由内核把 +`[ <短digest>] <描述>` 标记补进句子/正文——**标记由内核拼,模型只给 digest**。 +要求调用方知道格式,等于让一个拼写错误静默切断引用绑定而全链路无人报错。 +`memory_commit` 同时新增 `sentence_text`:媒体引用挂在句子上,没有句子就无处可挂。 + +### 媒体记忆(v1.1.0 起) + +`internal/memory/media/` — `Store`,内容寻址(CAS) + +| 关注点 | 做法 | 为何 | +|---|---|---| +| 寻址 | sha256 digest,元数据在 SQLite、blob 在磁盘 | 相同字节只存一份;元数据要可查询,blob 不该进数据库 | +| 完整性 | 每次 `Get` 重校 digest | 磁盘损坏时静默返回脏数据比报错危险得多 | +| 写入原子性 | `.tmp` + rename | 半个文件被当成完整内容会永久污染那个 digest | +| 引用 | `owner_kind/owner_id/digest` 三元组主键,`AddRef` 幂等 | 三个 owner 类型:`context`(上下文事件)、`document`(文档)、`graph_sentence`(图谱句子) | +| GC | 两阶段 + `minAge`,**有引用者绝不删** | 描述文本留在文本层,blob 可淘汰——语义记忆与字节缓存分离 | + +**媒体在纯文本记忆里的表示**是标记 `[ <短digest>] <描述>`: + +``` +[image/png a1b2c3d4e5f6] 一张紫蓝红三色带图 +``` + +之所以必须借文本承载:`Doc.Content`、`sentences.text`、文本记忆的 `Input` 全是字符串, +没有字段能挂结构化数据。**描述文本才是持久的语义记忆**(检索靠它),digest 是回到字节的 +钥匙(反查靠它)。blob 被容量 GC 淘汰后,描述仍留在 L0/L2/L3 的文本里。 + +媒体存储**全程可选**:`core.memory.media.enabled=false` 或未配置时,整条链路静默退化为 +纯文本行为,不报错不 panic。 + ### 其他记忆层 - **Social** (`internal/memory/social/social.go`) — 人格特质和关系网,包装 GraphDB 实体类型 @@ -294,10 +324,14 @@ Lua 脚本插件加载:`internal/plugin/` → gopher-lua 解释器执行 `main | 面 | 机制 | 为何这么选 | |---|---|---| -| 控制面 | stdio JSON-RPC(NDJSON 帧),51 个 `core.*` method | 进程边界即 ABI 边界,无需维护三套平台特定的动态库加载代码 | +| 控制面 | stdio JSON-RPC(NDJSON 帧),55 个 `core.*` method | 进程边界即 ABI 边界,无需维护三套平台特定的动态库加载代码 | | 数据面 | 共享内存段,**全部子进程共用一块** | 每插件一段会让「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成副本模型,lost update 原样复现 | | 通知面 | 事件环 + 平台通知(Linux eventfd / macOS pipe / Windows Event) | 内核发事件绕不等消费者,流式输出逐 token 发布时任何等待都会造成卡顿 | +v1.1.1 新增 4 个 method(51 → 55):`doc.insertWithMedia`、`io.injectMedia`、 +`io.injectMediaSync`、`io.injectInterruptMedia`。**媒体块走 JSON 而非共享段二进制通道**—— +data URL 本身已是 base64 文本,包进二进制传输省不了空间,还要跟其余 51 个 method 分道。 + **子进程生命周期管理**: - 每子进程一根专职 `waitLoop`(`cmd.Wait()` 唯一调用点)——不依赖 stdout EOF, 因为插件 fork 的孙子进程(browser 拉 chromium、editdoc 拉 python)继承同一 stdout, @@ -330,8 +364,20 @@ sdk.Memory().Recall/Commit sdk.Knowledge().Search/Create sdk.Settings().Get/Set/List sdk.RegisterOutputChannel("qq", sdk.CapText|sdk.CapAudio|sdk.CapImage, "QQ消息通道,详见 output_send__qq_help", handler) + +// v1.1.1 媒体接口(全部新增,无签名变更) +sdk.DocMemory().InsertWithMedia(doc, attachments) // 带 Data 的落进 CAS,只给 Digest 的引用已有内容 +sdk.InjectInputMedia(source, channel, text, blocks) // 媒体在「本轮」就发给模型 +sdk.InjectInputMediaSync(...) // 同上并同步等回复 +sdk.InjectInterruptMedia(...) // 带媒体的中断,可抢占当前处理 ``` +媒体注入与 `SetToolBlocks` 的区别:后者只能在工具处理函数内部调用,且媒体要等**下一条** +tool message 才到模型手上;前三个是插件**主动发起一轮带媒体的对话**,媒体随本轮消息发出, +并自动落进 CAS、挂上媒体记忆引用。`Triple` 与 `Doc` 相应新增 `MediaDigests`、`Attachments`。 + +`internal/sdk/` 是这层的桥接实现,不受公开接口冻结约束(见 `docs/git-branching.md` §六)。 + ### Plugin 接口 ```go diff --git a/assets/docs/zh/OVERVIEW.md b/assets/docs/zh/OVERVIEW.md index f1a7005..bfa1a9c 100644 --- a/assets/docs/zh/OVERVIEW.md +++ b/assets/docs/zh/OVERVIEW.md @@ -25,6 +25,17 @@ HomeAgent 是一个持续运行的个人智能 Agent 框架。 三层递进:上下文 → 冷归档 → 长期图记忆,构成从短期到持久的信息衰减与整合管道。 +**媒体记忆(v1.1.0 起)** — 图片/音频不是附属物,而是三层里的一类节点: +- **内容寻址存储(CAS)**:digest 寻址,元数据在 SQLite、blob 在磁盘,相同字节只存一份, + 每次 `Get` 重校 digest(磁盘损坏静默返回脏数据比报错更危险) +- **引用计数 GC**:`owner_kind/owner_id/digest` 三元组为主键,上下文事件/文档/图谱句子各自持引用; + **有引用者绝不删除**,仅回收无主且超过 `minAge` 的内容 +- **描述文本才是持久语义记忆**:视觉模型生成的描述以 + `[ <短digest>] <描述>` 标记形式写进纯文本记忆,参与向量检索与蒸馏; + blob 只是可被容量 GC 淘汰的缓存。几个月后“那张紫蓝红三色带图”仍可检索,靠的是描述而不是字节 +- **v1.1.1 起贯通插件边界**:插件可通过 `InsertWithMedia` / `InjectInputMedia` 读写媒体, + 模型可用 `memory_commit` / `doc_commit` 的 `media_digests` 参数关联媒体 + ## 它实际做了什么 代码位于项目仓库根目录,Go 语言实现。 diff --git a/docs/zh/plugin-interface-matrix.md b/docs/zh/plugin-interface-matrix.md index e771b94..9a7abc9 100644 --- a/docs/zh/plugin-interface-matrix.md +++ b/docs/zh/plugin-interface-matrix.md @@ -1,12 +1,16 @@ # 外部插件接口不变矩阵(多进程化整改基线) -> 状态:**完成 v2**(2026-09-03)——迁移已落地并上生产,内核 v1.0.0。 +> 状态:**完成 v3**(2026-09-06)——v2 的迁移已上生产(内核 v1.0.0);v3 记录 v1.1.1 的公开接口**扩展**。 > 目的:钉死「暴露给外部插件的接口不变」这一约束的**合同面**——迁移前、迁移后外部插件看到/调用的 SDK 接口完全一致; > 所有改造落在**核心(homed 侧)+ 工具链(plugindev)**,外部插件业务代码零改动,只需用新 plugindev 重编。 > > **结果(已验证)**:`git diff third_party/homeagent-sdk/sdk/` 全程为空;17 个 `example/*/plugin.go` 逐字节未改 > (`git status example/` 无输出);生产 17 插件全部经子进程通道运行。 > +> ⚠️ **v1.1.x 起冻结约束被有意解除**,因为「接口不变」这条约束本身是为**迁移期**设的: +> 它要保的是「换运行模型不动业务代码」。迁移完成后,SDK 需要能随功能演进而扩展, +> 否则多模态这类能力永远到不了插件手上。解除的边界见 §九:**只增不减,签名不改**。 +> > 维护规则:每次改动公开 SDK 接口面 `third_party/homeagent-sdk/sdk/` 或模板 `tools/plugindev/templates/` 后, > 必须同步更新本矩阵。 > @@ -67,7 +71,7 @@ type Plugin interface { |---|---|---| | `Settings()` | `SettingsAPI` | **17 插件全部使用**(Get/Set/List/GetCore/SetCore/ListCore/DataDir/GetPlugin/SetPlugin/ListPlugin/RegisterDef/Defs/Dump/Plugins) | | `Memory()` | `MemoryAPI`(Recall/Commit/Introspect/MergeEntities/Purge) | 低(controllable) | -| `DocMemory()` | `DocMemoryAPI`(Query/Insert/Remove/Stats) | 低 | +| `DocMemory()` | `DocMemoryAPI`(Query/Insert/**InsertWithMedia**/Remove/Stats) | 低(`InsertWithMedia` v1.1.0 新增) | | `TextMemory()` | `TextMemoryAPI`(Append) | 0 当前 | | `Knowledge()` | `KnowledgeAPI`(Search/Add/List) | 2 | | `LLM()` | `LLMAPI`(ListSources/SetSource/CurrentSource) | 0 当前 | @@ -85,7 +89,14 @@ type Plugin interface { | `InjectInterruptText` | `(source, channel, text string)` | example 使用 6 次 → case 6 | | `InjectTextNoMemory` | `(source, channel, text string)` | → case 7 | | `InjectInputSync` | `(source, channel, text string) string` | → case 47(例:qq 闭环) | -| `SetToolBlocks` | `(blocks []ContentBlock)` | **当前空实现**(C ABI 无对应),迁移后经 arena 二进制注入可实现 | +| `SetToolBlocks` | `(blocks []ContentBlock)` | ✅ **v1.1.1 已落地**(`io.setToolBlocks`);同版补上 `PluginSDK` 侧一直缺失的便捷包装——接口里有、便捷方法里没有,插件此前只能自己去拿 injector | +| `InjectInputMedia` | `(source, channel, text string, blocks []ContentBlock)` | **v1.1.0 新增** → `io.injectMedia`。与 `SetToolBlocks` 的区别见下方说明 | +| `InjectInputMediaSync` | `(source, channel, text string, blocks []ContentBlock) string` | **v1.1.0 新增** → `io.injectMediaSync` | +| `InjectInterruptMedia` | `(source, channel, text string, blocks []ContentBlock)` | **v1.1.0 新增** → `io.injectInterruptMedia` | + +**为何媒体注入不能搭 `SetToolBlocks` 的车**:后者只在**工具处理函数内部**可用,且媒体要等 +**下一条 tool message** 才到模型手上。插件主动发起一轮带媒体的对话、以及中断注入, +需要各自的签名,且媒体在**本轮**就随消息发出,并自动落进 CAS、挂上媒体记忆引用。 | `RegisterStopHandler` / `RunStopHandlers` | `(func())` / `()` | 已有(qq 等 1 次) | | `RegisterOnRemoveHandler` / `RunOnRemoveHandlers` | `(func())` / `()` | example 使用 3 次 | | `Set*`(SetIOInjector/SetMemoryAPI/.../SetPluginMgrAPI) | — | 供 bridge/核心启动时接线,插件不直接调 | @@ -99,8 +110,12 @@ type Plugin interface { | `ChannelDef` | NoMemory/Cleaner(func) | 同上 | | `ToolCall` / `ToolResult` / `MemItem` | ID/Name/Plugin/Arguments;CallID/Name/Plugin/Success/Result;Role/Content/Score | 全部纯 JSON 可序列化 | | `ContentBlock` / `ImageURL` / `AudioURL` | Type/Text/ImageURL/AudioURL;URL/Detail;URL | 全部可偏移化(迁移评估 3.3 已核实) | +| `MediaAttachment`(**v1.1.0 新增**) | Digest/MIME/Data/Name/Description | 一个类型服务两个方向:给 `Data`+`MIME` 是新内容(CAS 按字节去重),只给 `Digest` 是引用已有内容。**读路径不回 `Data`**——一次检索可能命中几十份媒体,全塞回去会撑爆跨进程消息 | | `Event` / `EventHandler` / `EventSubscriber` | Type/Source/Payload/Timestamp | 迁移后才对外部插件真正可用 | | `Triple` / `Entity` / `Relation` / `Doc` / `TextEvent` / `PersonProfile` / `SocialRelation` / `Knowledge` / `ConfigDef` | — | 全部 JSON 可序列化 | +| `Triple`(**v1.1.0 扩展**) | += `SentenceText` / `MediaDigests` | 媒体引用挂在**句子**上(`SentenceText` → `sentences` → `sentence_id` → `media_refs`),所以 `MediaDigests` 非空而 `SentenceText` 为空时内核会用媒体标记本身充当句子 | +| `Doc`(**v1.1.0 扩展**) | += `MediaDigests` / `Attachments` | `Query` 返回时由内核填充(仅元数据,不带字节) | +| `TextEvent`(**v1.1.0 扩展**) | += `Attachments` | 写入时内核把标记并进正文;`RecentEvents` 读回时从标记反解 | **函数类型字段盘点(唯一无法跨进程序列化的东西)**: - `ToolDef.Cleaner func(string) string` @@ -252,7 +267,9 @@ Part 0.2 先做了过渡补丁(只回传真正变更的字段);Part 4 的 | 能力 | 迁移前 | 迁移后 | 实际结果 | |---|---|---|---| | 事件订阅 `Events().Subscribe`(case 23/24) | ❌ 空实现 | ✅ 事件环(EvtRing + eventfd + 独立游标) | ✅ 已接线(当前零用户) | -| `SetToolBlocks` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arena,Slice 描述符回传 | ⚠️ method 已定义,内核侧仍未实现 | +| `SetToolBlocks` 多模态注入 | ❌ 空实现 | ✅ `io.setToolBlocks` | ✅ **v1.1.1 已落地**(走 JSON 而非共享段二进制通道,理由见 §九) | +| 媒体入记忆(`InsertWithMedia`、`Triple.MediaDigests`) | ❌ 不存在 | ✅ CAS + 引用计数 GC | ✅ **v1.1.0 类型 / v1.1.1 内核实现** | +| 插件主动发起带媒体的一轮对话(`InjectInputMedia*`) | ❌ 不存在 | ✅ 媒体在本轮就到模型手上 | ✅ **v1.1.1** | | `ContextMsgs`/`ReasoningContent`/`TokenUsage`/`Memory`/`Extra`/`Errors` | ❌ 看不到 | ✅ 共享内存全字段 | ✅ 18 字段全可见可写 | | 插件崩溃隔离 | ❌ panic 带崩 homed | ✅ 子进程独立崩溃 | ✅ 测试 + 生产验证 | | 热重载 `.so` | ❌ `DF_1_NODELETE` no-op | ✅ 同路径替换 `.bin` 即生效 | ✅ 生产实测 | @@ -291,6 +308,8 @@ C 结构体不好传函数指针(那是运气,任何人给 dispatch 加个 c 3. ✅ **阶段 5**:17 个外部插件全部 `.bin` 化、cabi 删除(-3198 行); `go build ./...` 与全仓 `go test ./...` 均通过。 4. ✅ **全程**:`git diff third_party/homeagent-sdk/sdk/` 为零——接口冻结的硬证据。 +5. ⚠️ **v1.1.x 起该检查项不再适用**:冻结是迁移期的约束,迁移完成即到期(见 §九)。 + 取代它的门禁是「存量插件零改动零重编」——见 §九的验证方式。 生产端到端(2026-09-03,真实 QQ 消息): @@ -304,6 +323,61 @@ tool output_send__qq result: 已通过 [qq] 通道发送: map[status:sent] --- +## 九、v1.1.x 的接口扩展规则(冻结解除后的替代约束) + +冻结约束是为**迁移期**设的:它要保的是「换运行模型不动业务代码」。迁移完成后继续冻结, +等于让 SDK 永远停在迁移那天的能力面——多模态这类功能永远到不了插件手上。 + +取代它的是三条更弱但仍然硬的约束: + +### 1. 只增不减,签名不改 + +新增字段、新增方法可以;**改已有方法的签名、删字段、改字段语义不行**。 + +实例:v1.1.0 想让插件能给三元组关联媒体,两条路——改 `Commit` 的签名加一个参数, +或新增 `CommitWithMedia`。选了后者。改签名会让每个调 `Commit` 的插件编译失败, +而那些插件根本不关心媒体。 + +### 2. 新增方法必须是「插件调用、内核实现」方向 + +这是**存量插件不需要重编**的技术原因:`IOInjector` 新增三个方法后,插件只是 +*多了可以调的东西*,没有新的实现义务。反过来若在 `Plugin` 接口上加方法, +每个存量插件都会因未实现而编译失败。 + +因此 `SDKCompatibleVersion` 与 SDK 的 `CoreVersion` 都不必随之跃迁: +1.1.0 的 SDK 配 1.0.0 编的插件仍然成立。 + +### 3. 生成模板必须同步接线,否则是**全体外部插件编译失败** + +公开接口加方法时,`tools/plugindev/templates/proc_main.go.tmpl` 里的 `procIO` / +`procDocMemory` 若不实现新方法,就不满足接口——**每个外部插件都编不过**,是硬失败 +不是软降级。v1.1.1 这一层是被 `go test` 抓出来的(`internal/plugin/proc` 的两个 +E2E 用例编译失败),不是靠人工检查发现的。 + +完整接线链共六处:`protocol.go` 的 method 常量 → `capability.go` 的能力归属 → +`corehandler.go` 的分派分支 → `proc_core.go` 的委托 → `proc_main.go.tmpl` 的模板实现 → +测试替身(`fakeCoreSDK`、`injectCapture`、`capability_test.go` 的手工方法清单)。 +还要同步 `yaegi/mocksdk`——它没有任何代码对着编译,所以漂移不会被编译器抓到 +(v1.1.1 修的时候发现它的 `Triple` 用的是 `Predicate`,而公开 SDK 一直叫 `Relation`)。 + +### 验证方式(取代「diff 为零」) + +| 检查 | 命令 | v1.1.1 结果 | +|---|---|---| +| 存量插件源码零改动 | `cd example/ && go vet ./...`(17 个) | ✅ 17/17 通过 | +| 旧产物仍能建链 | 用 SDK 0.9.2 编的 `plugin.bin` 跑 `TestRealPlugin_*` | ✅ 4/4 通过(握手校验 `ProtocolVersion=1`,不是 SDK 版本) | +| 模板已接线 | `cd tools/plugindev && go test ./...` | ✅ `TestProcTemplate_CoversAllCoreMethods` 含新 method | +| 并发安全 | `go test ./sdk/ -race -count=5` | ✅ 零 DATA RACE(13 例压测) | + +### 为何媒体块走 JSON 而不是共享段二进制通道 + +`SetToolBlocks` 的原设计是「二进制落 arena,Slice 描述符回传」。实际落地时改走 JSON: +data URL 本身已是 base64 文本,包进二进制传输省不了空间,还要让这四个 method 跟其余 +51 个分道扬镳。共享段的价值在于**并发改写同一份状态**(StageContext 的 lost update), +而媒体块是单向传递的不可变数据,没有这个问题。 + +--- + ## 八、关联文档 - `docs/zh/架构迁移评估.md` — 完整论证(§3.2 method id 平移、§3.3 数据面、§3.4 SDK 封装、§3.5 回调型资源、§3.8 能力对齐)