From 50ba4a64cb347498f755dbb43fd110836387d1b4 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 10 Sep 2026 23:26:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(shm):=20=E6=96=87=E6=A1=A3/=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E6=AD=A3=E6=96=87=E5=85=A5=E5=85=B1=E4=BA=AB=E5=86=85?= =?UTF-8?q?=E5=AD=98=20+=20=E5=8D=8F=E8=AE=AE=E7=89=88=E6=9C=AC=20bump=20?= =?UTF-8?q?=E5=88=B0=202(=C2=A713.13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 数据面补齐 - doc.insert / doc.insertWithMedia:新增 doc_ref / attachments_ref, 模板序列化后 putValueInArena - knowledge.add:新增 content_ref(内容是 JSON 字符串,读出后再解一层) - 抽出通用 resolveJSONRef(resolveBlocks 也改用它),三处共用一套 “共享优先、内联回退”逻辑 ## 协议版本 bump:让错配显式失败,而不是静默坏 这是本轮更重要的部分。§13.6/§13.13 改了内核→插件 payload 的承载方式, 两种错配都不会报错、只会静默失效: - v1 插件只读内联 args(tool/cleaner/output)→ 遇到 v2 内核拿到空参数 - v2 插件发 blocks_ref → v1 内核反序列化时静默忽略(旧内核 io.setToolBlocks 还是桩实现) 现场表现为“输出变空 / 图注入没反应”,极难定位。所以把 ProtocolVersion 与模板 procProtocolVersion 一起 bump 到 2:双方都是等值校验,v1 插件遇上 v2 内核会在建链时明确报“协议版本不匹配…请用配套 plugindev 重编”。 测试里把“错误必须带出重编指令”也断言上了——生产上碰到它的现场就是 “只更新了内核没重编插件”,光报“不匹配”定位不到行动。 testdata 8 个插件的 protocol 同步更新(badprotoplugin 仍用 999 验证拒绝)。 工具链已重建并安装(协议 2,内嵌 blocks_ref/doc_ref/content_ref), 回滚副本 plugindev.bak-20260910-232544。 验证:-race 全绿。新增 KnowledgeAddViaArena(12000B 正文)、 KnowledgeAddInline、DocInsertViaArena、SetToolBlocks 三例 + 真实模板 e2e + 协议不匹配断言强化。 §13.13 第 5 条(反向大结果)范围更大——需把内核→插件的应答路径整体改成 “大结果写段 + 返回 ref”,涉及 callCore 的返回处理与 doc.query/llm.chat 等 所有读大结果的 method。已在 plan.md 标注未做,不冒充完成。 --- internal/plugin/proc/corehandler.go | 56 +++++-- internal/plugin/proc/plugin_test.go | 152 +++++++++++++++++- internal/plugin/proc/process_test.go | 9 ++ internal/plugin/proc/protocol.go | 10 +- internal/plugin/proc/testdata/appendplugin.go | 2 +- .../plugin/proc/testdata/callbackplugin.go | 2 +- internal/plugin/proc/testdata/crashplugin.go | 2 +- internal/plugin/proc/testdata/echoplugin.go | 2 +- internal/plugin/proc/testdata/forkplugin.go | 2 +- internal/plugin/proc/testdata/hangplugin.go | 2 +- .../plugin/proc/testdata/readonlyplugin.go | 2 +- internal/plugin/proc/testdata/stageplugin.go | 14 +- plan.md | 34 ++-- 13 files changed, 252 insertions(+), 37 deletions(-) diff --git a/internal/plugin/proc/corehandler.go b/internal/plugin/proc/corehandler.go index d5071b5..15cee2a 100644 --- a/internal/plugin/proc/corehandler.go +++ b/internal/plugin/proc/corehandler.go @@ -340,11 +340,16 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{} return nil, errUnavailable("doc memory") } var p struct { - Doc *pubsdk.Doc `json:"doc"` + Doc *pubsdk.Doc `json:"doc,omitempty"` + DocRef SharedRef `json:"doc_ref,omitempty"` } if err := unmarshal(params, &p); err != nil { return nil, err } + // 文档全文可达几十 KB~数 MB,优先走共享内存。 + if err := h.resolveJSONRef(p.DocRef, &p.Doc); err != nil { + return nil, err + } if p.Doc == nil { return nil, fmt.Errorf("doc.insert: 缺少 doc 字段") } @@ -356,12 +361,21 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{} return nil, errUnavailable("doc memory") } var p struct { - Doc *pubsdk.Doc `json:"doc"` - Attachments []pubsdk.MediaAttachment `json:"attachments"` + Doc *pubsdk.Doc `json:"doc,omitempty"` + Attachments []pubsdk.MediaAttachment `json:"attachments,omitempty"` + DocRef SharedRef `json:"doc_ref,omitempty"` + AttachRef SharedRef `json:"attachments_ref,omitempty"` } if err := unmarshal(params, &p); err != nil { return nil, err } + // 文档正文 + 附件(含媒体二进制/data URL)都优先走共享内存。 + if err := h.resolveJSONRef(p.DocRef, &p.Doc); err != nil { + return nil, err + } + if err := h.resolveJSONRef(p.AttachRef, &p.Attachments); err != nil { + return nil, err + } if p.Doc == nil { return nil, fmt.Errorf("doc.insertWithMedia: 缺少 doc 字段") } @@ -421,12 +435,18 @@ func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{} return nil, errUnavailable("knowledge") } var p struct { - Name string `json:"name"` - Content string `json:"content"` + Name string `json:"name"` + Content string `json:"content,omitempty"` + ContentRef SharedRef `json:"content_ref,omitempty"` } if err := unmarshal(params, &p); err != nil { return nil, err } + // 知识正文可达数十 KB,优先走共享内存。内容是 JSON 字符串, + // 所以从 ref 读出后需再解一层。 + if err := h.resolveJSONRef(p.ContentRef, &p.Content); err != nil { + return nil, err + } return nil, kn.Add(p.Name, p.Content) case MethodKnowledgeList: @@ -651,18 +671,32 @@ type injectMediaParams struct { BlocksRef SharedRef `json:"blocks_ref,omitempty"` } +// resolveJSONRef 若 ref 非零则从共享内存读取并 JSON 反序列化到 out; +// ref 为零时不动 out(调用方已填的内联值生效)。 +// +// 供「大 payload 优先走共享内存、否则内联」的字段对共用。 +func (h *coreHandler) resolveJSONRef(ref SharedRef, out interface{}) error { + if ref.IsZero() { + return nil + } + data, err := h.host.Arena().Read(ref, h.host.Generation()) + if err != nil { + return fmt.Errorf("读取共享内容失败: %w", err) + } + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("解析共享内容失败: %w", err) + } + return nil +} + // resolveBlocks 取出媒体块:优先共享内存,否则内联。 func (h *coreHandler) resolveBlocks(p injectMediaParams) ([]pubsdk.ContentBlock, error) { if p.BlocksRef.IsZero() { return p.Blocks, nil } - data, err := h.host.Arena().Read(p.BlocksRef, h.host.Generation()) - if err != nil { - return nil, fmt.Errorf("读取共享媒体块失败: %w", err) - } var blocks []pubsdk.ContentBlock - if err := json.Unmarshal(data, &blocks); err != nil { - return nil, fmt.Errorf("解析共享媒体块失败: %w", err) + if err := h.resolveJSONRef(p.BlocksRef, &blocks); err != nil { + return nil, err } return blocks, nil } diff --git a/internal/plugin/proc/plugin_test.go b/internal/plugin/proc/plugin_test.go index 5aaf5a0..aab9cd7 100644 --- a/internal/plugin/proc/plugin_test.go +++ b/internal/plugin/proc/plugin_test.go @@ -31,6 +31,9 @@ type fakeCoreSDK struct { injected []string // toolBlocks 累积 SetToolBlocks 收到的块(多模态注入通道)。 toolBlocks []pubsdk.ContentBlock + // 文档/知识:验证大正文经 doc_ref / content_ref 走共享内存。 + docMem *fakeDocMemory + knowledge *fakeKnowledge } func newFakeCore() *fakeCoreSDK { @@ -49,8 +52,8 @@ func (f *fakeCoreSDK) PluginName() string { return "fake" } func (f *fakeCoreSDK) Settings() pubsdk.SettingsAPI { return nil } func (f *fakeCoreSDK) Memory() pubsdk.MemoryAPI { return nil } func (f *fakeCoreSDK) TextMemory() pubsdk.TextMemoryAPI { return nil } -func (f *fakeCoreSDK) DocMemory() pubsdk.DocMemoryAPI { return nil } -func (f *fakeCoreSDK) Knowledge() pubsdk.KnowledgeAPI { return nil } +func (f *fakeCoreSDK) DocMemory() pubsdk.DocMemoryAPI { return f.docMem } +func (f *fakeCoreSDK) Knowledge() pubsdk.KnowledgeAPI { return f.knowledge } func (f *fakeCoreSDK) LLM() pubsdk.LLMAPI { return nil } func (f *fakeCoreSDK) Social() pubsdk.SocialAPI { return nil } func (f *fakeCoreSDK) PluginMgr() pubsdk.PluginMgrAPI { return nil } @@ -88,6 +91,58 @@ func (f *fakeCoreSDK) toolBlockCount() int { return len(f.toolBlocks) } +// fakeDocMemory 只实现测试需要的部分,记录 Insert 收到的文档。 +type fakeDocMemory struct { + mu sync.Mutex + got *pubsdk.Doc +} + +func (f *fakeDocMemory) Query(string, int) []*pubsdk.Doc { return nil } +func (f *fakeDocMemory) Insert(doc *pubsdk.Doc) error { + f.mu.Lock() + f.got = doc + f.mu.Unlock() + return nil +} +func (f *fakeDocMemory) InsertWithMedia(doc *pubsdk.Doc, _ []pubsdk.MediaAttachment) error { + return f.Insert(doc) +} +func (f *fakeDocMemory) Remove(string) {} +func (f *fakeDocMemory) Stats() map[string]interface{} { return nil } + +// fakeKnowledge 只实现测试需要的部分,记录 Add 收到的正文。 +type fakeKnowledge struct { + mu sync.Mutex + name string + body string +} + +func (f *fakeKnowledge) Search(string, int) ([]*pubsdk.Knowledge, error) { return nil, nil } +func (f *fakeKnowledge) Add(name, content string) error { + f.mu.Lock() + f.name, f.body = name, content + f.mu.Unlock() + return nil +} +func (f *fakeKnowledge) List() ([]string, error) { return nil, nil } + +// arenaPutForTest 把一段字节放进 arena 并返回引用(测试用)。 +func arenaPutForTest(t *testing.T, host *Host, blob []byte) SharedRef { + t.Helper() + arena := host.Arena() + gen := host.Generation() + ref, err := arena.Alloc(OwnerHost, len(blob), gen) + if err != nil { + t.Fatalf("Alloc: %v", err) + } + area, err := arena.Read(ref, gen) + if err != nil { + t.Fatalf("Read: %v", err) + } + copy(area[:len(blob)], blob) + return ref +} + func (f *fakeCoreSDK) SetAutoRestart(enabled bool) { f.autoStart = enabled } func (f *fakeCoreSDK) RegisterTool(name string, def pubsdk.ToolDef, h pubsdk.ToolHandler) error { @@ -513,6 +568,99 @@ func TestCoreHandler_SetToolBlocksEmptyRejected(t *testing.T) { } } +// §13.13:知识正文经共享内存(content_ref)送达内核。 +// +// 正文可达数十 KB;内联时整份要在 RPC 报文里再编码再拷贝一遍,且内容本体 +// 不在共享段里,插件回调无法就地改写。 +func TestCoreHandler_KnowledgeAddViaArena(t *testing.T) { + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + core := newFakeCore() + kn := &fakeKnowledge{} + core.knowledge = kn + h := &coreHandler{sdk: core, name: "x", host: host, locks: &lockRegistry{}} + + content := strings.Repeat("知识正文", 3000) // 12000 字节 + blob, err := json.Marshal(content) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + ref := arenaPutForTest(t, host, blob) + defer func() { _ = host.Arena().Free(OwnerHost, ref) }() + + params, _ := json.Marshal(map[string]interface{}{"name": "n", "content_ref": ref}) + if _, err := h.Handle(MethodKnowledgeAdd, params); err != nil { + t.Fatalf("knowledge.add 应成功: %v", err) + } + + kn.mu.Lock() + got, gotName := kn.body, kn.name + kn.mu.Unlock() + if got != content { + t.Fatalf("经共享内存送达的正文不一致(got len=%d want len=%d)", len(got), len(content)) + } + if gotName != "n" { + t.Fatalf("name 传错: %q", gotName) + } +} + +// 内联回退仍可用(直连 RPC 调用方 / arena 不可用)。 +func TestCoreHandler_KnowledgeAddInline(t *testing.T) { + core := newFakeCore() + kn := &fakeKnowledge{} + core.knowledge = kn + h := &coreHandler{sdk: core, name: "x", locks: &lockRegistry{}} + + params := json.RawMessage(`{"name":"n","content":"短正文"}`) + if _, err := h.Handle(MethodKnowledgeAdd, params); err != nil { + t.Fatalf("内联 knowledge.add 应成功: %v", err) + } + kn.mu.Lock() + got := kn.body + kn.mu.Unlock() + if got != "短正文" { + t.Fatalf("内联正文不一致: %q", got) + } +} + +// §13.13:文档正文经共享内存(doc_ref)送达内核。 +func TestCoreHandler_DocInsertViaArena(t *testing.T) { + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + core := newFakeCore() + dm := &fakeDocMemory{} + core.docMem = dm + h := &coreHandler{sdk: core, name: "x", host: host, locks: &lockRegistry{}} + + doc := &pubsdk.Doc{Title: "标题", Content: strings.Repeat("正文", 5000)} + blob, err := json.Marshal(doc) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + ref := arenaPutForTest(t, host, blob) + defer func() { _ = host.Arena().Free(OwnerHost, ref) }() + + params, _ := json.Marshal(map[string]interface{}{"doc_ref": ref}) + if _, err := h.Handle(MethodDocInsert, params); err != nil { + t.Fatalf("doc.insert 应成功: %v", err) + } + + dm.mu.Lock() + got := dm.got + dm.mu.Unlock() + if got == nil || got.Content != doc.Content { + t.Fatal("经共享内存送达的文档正文不一致") + } +} + // stage 锁在无进行中 stage 时申请应被拒绝(防止插件在 stage 外乱加锁)。 func TestCoreHandler_StageLockOutsideStageRejected(t *testing.T) { h := &coreHandler{sdk: newFakeCore(), name: "x", locks: &lockRegistry{}} diff --git a/internal/plugin/proc/process_test.go b/internal/plugin/proc/process_test.go index 14592e2..965c6ca 100644 --- a/internal/plugin/proc/process_test.go +++ b/internal/plugin/proc/process_test.go @@ -316,6 +316,10 @@ func TestProcess_ConcurrentCallsRouteCorrectly(t *testing.T) { } // 协议版本不匹配必须显式拒绝,不能半兼容运行。 +// +// v2 引入的必要性就靠这条:v1 插件(只读内联 args)遇上 v2 内核,跟 v2 插件 +// 发 blocks_ref 给 v1 内核,都会静默失效、不报任何错。只有在这里显式拦下, +// 那双错配才会变成一条带修复指令的启动失败。 func TestProcess_ProtocolMismatchRejected(t *testing.T) { bin := buildTestPlugin(t, "badprotoplugin.go") _, err := Spawn("badproto", bin, Options{Handler: noopHandler}) @@ -325,6 +329,11 @@ func TestProcess_ProtocolMismatchRejected(t *testing.T) { if !strings.Contains(err.Error(), "协议版本不匹配") { t.Errorf("错误应说明版本不匹配,实际: %v", err) } + // 运维可读性:光报“不匹配”不能定位到行动。生产上碰到它的现场是 + // “只更新了内核没重编插件”,所以错误里必须带出这条修复指令。 + if !strings.Contains(err.Error(), "plugindev") || !strings.Contains(err.Error(), "重编") { + t.Errorf("错误应给出重编插件的修复指令,实际: %v", err) + } } func TestProcess_SpawnRequiresHandler(t *testing.T) { diff --git a/internal/plugin/proc/protocol.go b/internal/plugin/proc/protocol.go index 90e96ff..a8fbe55 100644 --- a/internal/plugin/proc/protocol.go +++ b/internal/plugin/proc/protocol.go @@ -14,7 +14,15 @@ import "encoding/json" // 协议版本:与共享段版本独立演进。 // 插件握手时上报,内核校验——不匹配显式拒绝,避免半兼容导致的诡异行为。 -const ProtocolVersion = 1 +// +// v2(§13.6 / §13.13):内核→插件的 payload 改为调用帧承载。 +// v1 插件只读内联 args,遇上 v2 内核会拿到空参数;v2 插件发 blocks_ref, +// v1 内核反序列化时静默忽略(旧内核 io.setToolBlocks 还是桩)。两种错配 +// 都不会报错,只会静默失效——所以必须 bump 版本,让它在握手上就**显式**失败。 +// +// 部署纪律:内核与全部插件必须同批重建、同批安装;改协议就要改这个常量, +// 不得依赖“两边大致兼容”。 +const ProtocolVersion = 2 // Direction 无需显式字段:靠 Method 是否为空区分请求与响应 // (与 clawhubadapter/sidecar 的成熟做法一致)。 diff --git a/internal/plugin/proc/testdata/appendplugin.go b/internal/plugin/proc/testdata/appendplugin.go index 0d13a67..5d2d062 100644 --- a/internal/plugin/proc/testdata/appendplugin.go +++ b/internal/plugin/proc/testdata/appendplugin.go @@ -187,7 +187,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", + "protocol": 2, "sdk_version": "test", "plugin_name": "append-" + tag, "pid": os.Getpid(), }}) diff --git a/internal/plugin/proc/testdata/callbackplugin.go b/internal/plugin/proc/testdata/callbackplugin.go index 9945323..05bfb41 100644 --- a/internal/plugin/proc/testdata/callbackplugin.go +++ b/internal/plugin/proc/testdata/callbackplugin.go @@ -95,7 +95,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, + "protocol": 2, "sdk_version": "test", "plugin_name": "cb", "pid": os.Getpid(), diff --git a/internal/plugin/proc/testdata/crashplugin.go b/internal/plugin/proc/testdata/crashplugin.go index 0bd87f9..c483b24 100644 --- a/internal/plugin/proc/testdata/crashplugin.go +++ b/internal/plugin/proc/testdata/crashplugin.go @@ -40,7 +40,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", "plugin_name": "crash", "pid": os.Getpid(), + "protocol": 2, "sdk_version": "test", "plugin_name": "crash", "pid": os.Getpid(), }}) case "tool.invoke": // 模拟插件 bug:直接 panic,进程带非零码退出 diff --git a/internal/plugin/proc/testdata/echoplugin.go b/internal/plugin/proc/testdata/echoplugin.go index 1437a03..8428a60 100644 --- a/internal/plugin/proc/testdata/echoplugin.go +++ b/internal/plugin/proc/testdata/echoplugin.go @@ -43,7 +43,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, + "protocol": 2, "sdk_version": "test", "plugin_name": "echo", "pid": os.Getpid(), diff --git a/internal/plugin/proc/testdata/forkplugin.go b/internal/plugin/proc/testdata/forkplugin.go index aa768cc..be0d2c6 100644 --- a/internal/plugin/proc/testdata/forkplugin.go +++ b/internal/plugin/proc/testdata/forkplugin.go @@ -57,7 +57,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", "plugin_name": "fork", "pid": os.Getpid(), + "protocol": 2, "sdk_version": "test", "plugin_name": "fork", "pid": os.Getpid(), }}) case "tool.invoke": // 不回应答,直接退出:模拟插件突然死亡(崩溃/被 kill)。 diff --git a/internal/plugin/proc/testdata/hangplugin.go b/internal/plugin/proc/testdata/hangplugin.go index f98c9d5..c73e528 100644 --- a/internal/plugin/proc/testdata/hangplugin.go +++ b/internal/plugin/proc/testdata/hangplugin.go @@ -42,7 +42,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", "plugin_name": "hang", "pid": os.Getpid(), + "protocol": 2, "sdk_version": "test", "plugin_name": "hang", "pid": os.Getpid(), }}) case "tool.invoke": // 永久卡住,永不回应答 diff --git a/internal/plugin/proc/testdata/readonlyplugin.go b/internal/plugin/proc/testdata/readonlyplugin.go index 24c64f5..cb04d32 100644 --- a/internal/plugin/proc/testdata/readonlyplugin.go +++ b/internal/plugin/proc/testdata/readonlyplugin.go @@ -139,7 +139,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", "plugin_name": "readonly", "pid": os.Getpid(), + "protocol": 2, "sdk_version": "test", "plugin_name": "readonly", "pid": os.Getpid(), }}) case "plugin.init": diff --git a/internal/plugin/proc/testdata/stageplugin.go b/internal/plugin/proc/testdata/stageplugin.go index 2d8b936..570b812 100644 --- a/internal/plugin/proc/testdata/stageplugin.go +++ b/internal/plugin/proc/testdata/stageplugin.go @@ -354,7 +354,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 1, "sdk_version": "test", "plugin_name": "stage", "pid": os.Getpid(), + "protocol": 2, "sdk_version": "test", "plugin_name": "stage", "pid": os.Getpid(), }}) case "plugin.init": @@ -540,12 +540,12 @@ func main() { args := p.Args if !p.Frame.IsZero() { if blob := frameInput(p.Frame, p.ArgsLen); len(blob) > 0 { - var decoded map[string]interface{} - if err := json.Unmarshal(blob, &decoded); err != nil { - send(response{ID: id, Error: "解析输出参数: " + err.Error()}) - return - } - args = decoded + var decoded map[string]interface{} + if err := json.Unmarshal(blob, &decoded); err != nil { + send(response{ID: id, Error: "解析输出参数: " + err.Error()}) + return + } + args = decoded } } payload, _ := args["payload"].(string) diff --git a/plan.md b/plan.md index cc7136d..64d90ce 100644 --- a/plan.md +++ b/plan.md @@ -1407,19 +1407,35 @@ settings.*、lifecycle.*、arena.alloc/free 自身)不属于此列:它们不 2. ✅ ~~`io.injectMedia` / `injectMediaSync` / `injectInterruptMedia`~~ —— 同上(共用 `mediaArgsOwned`)。注意同步调用不能在应答返回前释放槽, 否则内核读到的是已释放的内存。 -3. **`doc.insert` / `doc.insertWithMedia`**——文档全文内联,且 `doc` 是可被 - 插件回调改写的内容。 -4. **`knowledge.add(name, content)`**——知识正文内联,同上。 +3. ✅ ~~`doc.insert` / `doc.insertWithMedia`~~ —— 已修。新增 `doc_ref` / + `attachments_ref`,模板序列化后 `putValueInArena`。 +4. ✅ ~~`knowledge.add(name, content)`~~ —— 已修。新增 `content_ref` + (内容是 JSON 字符串,读出后需再解一层)。 5. **反向结果**:插件反向调内核读大结果时仍内联(正向已有 `ResultRef`)。 + 这条范围比前四条大:需要把整个内核→插件的 Rust 应答路径改成 + “大结果写段 + 返回 ref”,涉及 `callCore` 的返回处理与所有读大结果的 + method(`doc.query` / `llm.chat` 等)。未做。 + +**协议版本已 bump 到 2**(§13.6/§13.13 的 payload 承载变更)。 +不再靠文档提醒,而是让错配在握手上**显式失败**: + +- 内核 `proc.ProtocolVersion = 2`;模板 `procProtocolVersion = 2` +- 双方都是等值校验 → v1 插件遇上 v2 内核会在建链时报 + “协议版本不匹配…请用配套 plugindev 重编”(带修复指令) +- 没有这个 bump 的话:v1 插件只读内联 args,遇到 v2 内核会拿到**空参数**; + v2 插件发 blocks_ref,v1 内核反序列化时**静默忽略**(旧内核 + `io.setToolBlocks` 还是桩)。两种都是静默失效,现场极难定位。 **验证**: -- [x] `TestCoreHandler_SetToolBlocksViaArena`:9000 字节 base64 图经 `blocks_ref` - 送达,内容一致;`TestCoreHandler_SetToolBlocksInline` 保内联回退; - `TestCoreHandler_SetToolBlocksEmptyRejected` 防空块静默成功 +- [x] `TestCoreHandler_SetToolBlocksViaArena` / `Inline` / `EmptyRejected` +- [x] `TestCoreHandler_KnowledgeAddViaArena` / `Inline`(12000 字节正文经 + `content_ref` 送达,内容一致) +- [x] `TestCoreHandler_DocInsertViaArena`(正文经 `doc_ref` 送达) - [x] `TestE2E_RealTemplateSetToolBlocksViaArena`:用**真实 SDK 模板**编译的 插件(生产插件走的就是模板,模板不走 blocks_ref 则内核实现了也收不到) -- [x] 工具链已同步:`/usr/local/bin/plugindev` 重建为新协议(内嵌 blocks_ref), - 回滚副本 `plugindev.bak-20260910-224255` -- [ ] 文档/知识正文走共享内存 +- [x] `TestProcess_ProtocolMismatchRejected` 加断言:错误必须含重编指令 +- [x] 工具链已同步:`/usr/local/bin/plugindev` = 协议 2(内嵌 blocks_ref / + doc_ref / content_ref),回滚副本 `plugindev.bak-20260910-232544` +- [ ] 反向结果入内存(第 5 条) - [ ] git commit -m "feat(shm): remaining data-plane payloads via shared refs"