mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
feat(shm): 媒体块入共享内存 + 实现 setToolBlocks 桩(§13.13)
两个问题叠在一起,先发现的是第二个: 1. **io.setToolBlocks 在内核侧是桩实现**——直接返回“多模态注入待共享段 二进制通道落地”。也就是**子进程插件调 SetToolBlocks 必然失败**(模板 只 log 一行),只有内置插件(multimodal)能用。之前还有测试把这个错误 当作预期行为断言着。 2. 媒体块(injectMedia 系列 + setToolBlocks)把 blocks 内联在 RPC JSON 里, 而本地生成的图/音频是 base64 data URL,一张图可达数 MB。 顺带修掉一条与设计相悖的旧注释:injectMediaParams 写着“data URL 已是 base64 文本、再套一层二进制不会更小,所以走 JSON”。那只算了体积,漏了 两件更重要的事——内联时整份 base64 要在 RPC 报文里再编码/再拷贝一遍; 以及内容本体不在共享段里,插件回调就无法就地改写,只能各持一份拷贝。 共享内存的意义是后者。 实现: - injectMediaParams 加 BlocksRef;新增 resolveBlocks(共享优先、内联回退) - 真正实现 MethodIOSetToolBlocks(空块明确报错,不静默成功) - CoreSDK 接口加 SetToolBlocks,procCore 转调 internal/sdk - 模板:SetToolBlocks / injectMedia 三兄弟经 putValueInArena 传 blocks_ref; 同步调用用 mediaArgsOwned 拿释放函数,**不能在应答返回前释放槽**, 否则内核读到已释放内存 验证(-race 全绿): - TestCoreHandler_SetToolBlocksViaArena / Inline / EmptyRejected - TestE2E_RealTemplateSetToolBlocksViaArena:真实 SDK 模板编译的插件, 9000 字节 base64 图经 blocks_ref 完整送达 工具链已同步:/usr/local/bin/plugindev 重建为新协议(内嵌 blocks_ref), 回滚副本 plugindev.bak-20260910-224255。注意内核与全部插件必须同批替换 ——只换一边会静默坏(新内核+旧插件=输出 payload 变空;新插件+旧内核= 媒体注入静默失效),已记入长期记忆。
This commit is contained in:
@ -75,6 +75,26 @@ func (p *e2ePlugin) Start(s *sdk.PluginSDK) error {
|
||||
|
||||
s.RegisterInputChannel("e2e_in", sdk.ChannelDef{})
|
||||
|
||||
// §13.13:媒体块注入。SetToolBlocks 在内核侧曾是桩(直接报“待共享段
|
||||
// 二进制通道落地”),子进程插件调它必然失败。这里用大 base64 payload
|
||||
// 验证模板真的经 blocks_ref 走共享内存。
|
||||
s.RegisterTool("e2e_blocks", sdk.ToolDef{
|
||||
Description: "注入媒体块",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
},
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
u, _ := args["url"].(string)
|
||||
s.SetToolBlocks([]sdk.ContentBlock{{
|
||||
Type: "image_url",
|
||||
ImageURL: &sdk.ImageURL{URL: u},
|
||||
}})
|
||||
return "blocks-set", nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -337,3 +357,47 @@ func TestE2E_RealTemplateOutputPayloadViaFrame(t *testing.T) {
|
||||
t.Fatalf("调用结束后 arena 应归零,实际 used=%d", used)
|
||||
}
|
||||
}
|
||||
|
||||
// §13.13:SetToolBlocks 的媒体块经共享内存到达内核(用真实模板编译的插件)。
|
||||
//
|
||||
// 内核侧曾是桩实现,子进程插件调 SetToolBlocks 必然失败;模板又不经
|
||||
// blocks_ref 的话,即使内核实现了也收不到内容。两边必须同时到位。
|
||||
func TestE2E_RealTemplateSetToolBlocksViaArena(t *testing.T) {
|
||||
bin := buildPluginWithRealTemplate(t, e2ePluginSource)
|
||||
|
||||
host, err := NewHost()
|
||||
if err != nil {
|
||||
t.Fatalf("NewHost: %v", err)
|
||||
}
|
||||
defer host.Close()
|
||||
|
||||
core := newFakeCore()
|
||||
p := New("e2e", bin, t.TempDir(), nil, host, nil)
|
||||
if err := p.Start(core); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
core.mu.Lock()
|
||||
h, ok := core.tools["e2e_blocks"]
|
||||
core.mu.Unlock()
|
||||
if !ok {
|
||||
t.Fatal("插件应注册 e2e_blocks 工具")
|
||||
}
|
||||
|
||||
// 9000 字节 base64:远大于内联阈值,只有共享内存才能送到。
|
||||
big := "data:image/png;base64," + strings.Repeat("A", 8000)
|
||||
if _, err := h(map[string]interface{}{"url": big}); err != nil {
|
||||
t.Fatalf("调用 e2e_blocks: %v", err)
|
||||
}
|
||||
if n := core.toolBlockCount(); n != 1 {
|
||||
t.Fatalf("内核应收到 1 个媒体块,实际 %d(模板未走 blocks_ref?)", n)
|
||||
}
|
||||
|
||||
core.mu.Lock()
|
||||
got := core.toolBlocks[0]
|
||||
core.mu.Unlock()
|
||||
if got.ImageURL == nil || got.ImageURL.URL != big {
|
||||
t.Fatal("经共享内存送达的媒体块内容与发送的不一致")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user