docs: 同步仓库文档到 v1.1.1,补媒体记忆与接口扩展规则

五处文档此前停在 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 会让追溯时看不出"文档在哪一层跟上了代码"。
This commit is contained in:
JianFeeeee
2026-09-06 15:07:14 +08:00
parent 22a5af6534
commit 7a94bf692c
5 changed files with 198 additions and 6 deletions

View File

@ -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
`[<mime> <short digest>] <description>` 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 `[<mime> <short digest>] <description>`:
```
[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

View File

@ -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 `[<mime> <short digest>] <description>` 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.

View File

@ -189,6 +189,36 @@ eventLoop() → processTextInput()
| `doc_query` | 从 Document 搜索 |
| `doc_commit` | 写入 Document |
`memory_commit``doc_commit` 自 v1.1.1 起接受 `media_digests`,并由内核把
`[<mime> <短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 可淘汰——语义记忆与字节缓存分离 |
**媒体在纯文本记忆里的表示**是标记 `[<mime> <短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-RPCNDJSON 帧51`core.*` method | 进程边界即 ABI 边界,无需维护三套平台特定的动态库加载代码 |
| 控制面 | stdio JSON-RPCNDJSON 帧55`core.*` method | 进程边界即 ABI 边界,无需维护三套平台特定的动态库加载代码 |
| 数据面 | 共享内存段,**全部子进程共用一块** | 每插件一段会让「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成副本模型lost update 原样复现 |
| 通知面 | 事件环 + 平台通知Linux eventfd / macOS pipe / Windows Event | 内核发事件绕不等消费者,流式输出逐 token 发布时任何等待都会造成卡顿 |
v1.1.1 新增 4 个 method51 → 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

View File

@ -25,6 +25,17 @@ HomeAgent 是一个持续运行的个人智能 Agent 框架。
三层递进:上下文 → 冷归档 → 长期图记忆,构成从短期到持久的信息衰减与整合管道。
**媒体记忆v1.1.0 起)** — 图片/音频不是附属物,而是三层里的一类节点:
- **内容寻址存储CAS**digest 寻址,元数据在 SQLite、blob 在磁盘,相同字节只存一份,
每次 `Get` 重校 digest磁盘损坏静默返回脏数据比报错更危险
- **引用计数 GC**`owner_kind/owner_id/digest` 三元组为主键,上下文事件/文档/图谱句子各自持引用;
**有引用者绝不删除**,仅回收无主且超过 `minAge` 的内容
- **描述文本才是持久语义记忆**:视觉模型生成的描述以
`[<mime> <短digest>] <描述>` 标记形式写进纯文本记忆,参与向量检索与蒸馏;
blob 只是可被容量 GC 淘汰的缓存。几个月后“那张紫蓝红三色带图”仍可检索,靠的是描述而不是字节
- **v1.1.1 起贯通插件边界**:插件可通过 `InsertWithMedia` / `InjectInputMedia` 读写媒体,
模型可用 `memory_commit` / `doc_commit``media_digests` 参数关联媒体
## 它实际做了什么
代码位于项目仓库根目录Go 语言实现。

View File

@ -1,12 +1,16 @@
# 外部插件接口不变矩阵(多进程化整改基线)
> 状态:**完成 v2**2026-09-03)——迁移已落地并上生产内核 v1.0.0。
> 状态:**完成 v3**2026-09-06)——v2 的迁移已上生产内核 v1.0.0v3 记录 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 47qq 闭环) |
| `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/ArgumentsCallID/Name/Plugin/Success/ResultRole/Content/Score | 全部纯 JSON 可序列化 |
| `ContentBlock` / `ImageURL` / `AudioURL` | Type/Text/ImageURL/AudioURLURL/DetailURL | 全部可偏移化(迁移评估 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` 多模态注入 | ❌ 空实现 | ✅ 二进制落 arenaSlice 描述符回传 | ⚠️ 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/<n> && 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 RACE13 例压测) |
### 为何媒体块走 JSON 而不是共享段二进制通道
`SetToolBlocks` 的原设计是「二进制落 arenaSlice 描述符回传」。实际落地时改走 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 能力对齐)