feat(plugindev): 媒体块经共享内存传递(§13.13)

SetToolBlocks / InjectInputMedia / InjectInputMediaSync /
InjectInterruptMedia 原先把 blocks 内联在 RPC 报文里。本地生成的图/音频是
base64 data URL(一张图可达数 MB),内联时整份要在报文里再编码再拷贝一遍;
更关键的是内容本体不在共享段里,插件回调无法就地改写。

改为经 putValueInArena 传 blocks_ref,内核 resolveBlocks 读回;小 payload
仍内联。

同步调用用 mediaArgsOwned 返回的释放函数延迟释放:injectMediaSync 要等
应答,槽不能在应答到达前回收,否则内核读到已释放内存。

配套内核侧 15d912e(实现 setToolBlocks 桩 + resolveBlocks)。
This commit is contained in:
JianFeeeee
2026-09-10 22:46:41 +08:00
parent 9d930db4ea
commit 632f6743d3

View File

@ -233,6 +233,16 @@ func putInArena(payload string) (SharedRef, bool) {
return ref, true
}
// putValueInArena 把任意值 JSON 序列化后放进共享槽,太小或 arena 不可用时
// 返回 false调用方退到内联
func putValueInArena(v interface{}) (SharedRef, bool) {
blob, err := json.Marshal(v)
if err != nil {
return SharedRef{}, false
}
return putInArena(string(blob))
}
// callWithText 按 payload 大小自动选择共享槽或内联,发起一次带文本的业务 RPC。
//
// 共享内存对插件开发者完全透明SDK 层只看得到 string。
@ -686,7 +696,18 @@ func (procIO) InjectInputSync(s, c, t string) string {
return r.Reply
}
func (procIO) SetToolBlocks(blocks []sdk.ContentBlock) {
if err := callCoreVoid("io.setToolBlocks", map[string]interface{}{"blocks": blocks}); err != nil {
// 媒体块经共享内存blocks_ref本地生成的图/音频是 base64 data URL
// 一张图可达数 MB内联时整份 base64 还要在 RPC 报文里再编码/再拷贝一遍。
// 更重要的是内容本体落在共享段里,插件回调才能就地改写。
// 小 payload如纯文本块仍走内联省一次 RPC。
args := map[string]interface{}{}
if ref, ok := putValueInArena(blocks); ok {
defer arenaFree(ref)
args["blocks_ref"] = ref
} else {
args["blocks"] = blocks
}
if err := callCoreVoid("io.setToolBlocks", args); err != nil {
log.Printf("SetToolBlocks: %v", err)
}
}
@ -694,15 +715,13 @@ func (procIO) SetToolBlocks(blocks []sdk.ContentBlock) {
// 带媒体的注入:插件主动发起一轮带图/音频的对话。
// 与 SetToolBlocks 的区别是媒体在**本轮**就到模型手上,而不是等下一条 tool message。
func (procIO) InjectInputMedia(s, c, t string, blocks []sdk.ContentBlock) {
callCoreVoid("io.injectMedia", map[string]interface{}{
"source": s, "channel": c, "text": t, "blocks": blocks,
})
callCoreVoid("io.injectMedia", mediaArgs(s, c, t, blocks))
}
func (procIO) InjectInputMediaSync(s, c, t string, blocks []sdk.ContentBlock) string {
raw, err := callCore("io.injectMediaSync", map[string]interface{}{
"source": s, "channel": c, "text": t, "blocks": blocks,
})
args, free := mediaArgsOwned(s, c, t, blocks)
defer free()
raw, err := callCore("io.injectMediaSync", args)
if err != nil {
return ""
}
@ -714,9 +733,32 @@ func (procIO) InjectInputMediaSync(s, c, t string, blocks []sdk.ContentBlock) st
}
func (procIO) InjectInterruptMedia(s, c, t string, blocks []sdk.ContentBlock) {
callCoreVoid("io.injectInterruptMedia", map[string]interface{}{
"source": s, "channel": c, "text": t, "blocks": blocks,
})
args, free := mediaArgsOwned(s, c, t, blocks)
defer free()
callCoreVoid("io.injectInterruptMedia", args)
}
// mediaArgs 构造媒体注入参数,并在返回前释放临时共享槽。
// 只能用在调用结束后立即返回的路径fire-and-forget
func mediaArgs(s, c, t string, blocks []sdk.ContentBlock) map[string]interface{} {
args, free := mediaArgsOwned(s, c, t, blocks)
defer free()
return args
}
// mediaArgsOwned 构造媒体注入参数,并返回释放函数。
//
// 为什么要返回释放函数而不是自己 defer调用方可能是需要等应答的同步调用
// injectMediaSync槽在应答到达前不能被回收否则内核读到的是已释放的内存。
func mediaArgsOwned(s, c, t string, blocks []sdk.ContentBlock) (map[string]interface{}, func()) {
args := map[string]interface{}{"source": s, "channel": c, "text": t}
ref, ok := putValueInArena(blocks)
if !ok {
args["blocks"] = blocks
return args, func() {}
}
args["blocks_ref"] = ref
return args, func() { arenaFree(ref) }
}
type procMemory struct{}