fix(lua): 同步注入在 Lua 中明确报不可用(避免自锁)+ 文档/mock 同步

sdk.inject_input_sync / *_opts / inject_input_media_sync* 在 Lua 里必然自锁:
Lua 代码只在 Start/工具/阶段/输出/事件回调中执行,这些路径都持有 plg.mu,
而同步注入要等本轮回复(回复路径上的回调又需要同一把锁)。原实现会挂死
直到超时;现改为立即返回明确错误,并在中英文 PLUGIN_DEV 里标注不可用 +
指向 Go 插件/异步注入。mock sdk.lua(SDK 仓为事实源)同步为同样的错误语义。
新增 TestLuaSyncInjectUnavailable 钉住不挂死。
This commit is contained in:
HomeAgent Agent
2026-09-13 21:59:23 +08:00
parent 52127a3323
commit 6878f0126d
6 changed files with 92 additions and 40 deletions

View File

@ -648,11 +648,11 @@ Writable fields: `raw_message`, `llm_text`, `final_text`, `user_id`, `group_id`,
| `sdk.inject_interrupt(source, channel, text)` | Interrupt delivery |
| `sdk.inject_text_no_memory(source, channel, text)` | Deliver without memory computation |
| `sdk.inject_text_opts` / `sdk.inject_interrupt_opts(source, channel, text, opts)` | Delivery with flags; `opts = { no_memory=bool, context_policy="none"|"prune", cleaner_name=string, priority="L1".."L3" }` |
| `sdk.inject_input_sync(source, channel, text)` | Inject synchronously and wait for this turn's reply; returns `(reply, err)`, reply is nil when there is none |
| `sdk.inject_input_sync_opts(source, channel, text, opts)` | Same, with flags |
| `sdk.inject_input_sync(source, channel, text)` | ⚠️ **Unavailable in Lua**: always returns `(nil, err)`. It waits for this turn's reply while a Lua callback holds the plugin lock, so it would self-deadlock. Use a Go plugin for synchronous waits, or the async injectors below |
| `sdk.inject_input_sync_opts(source, channel, text, opts)` | Same (unavailable) |
| `sdk.inject_input_media(source, channel, text, blocks)` | Inject text + multimodal content blocks |
| `sdk.inject_input_media_opts(source, channel, text, blocks, opts)` | Same, with flags |
| `sdk.inject_input_media_sync` / `..._sync_opts(...)` | Synchronous media injection; returns `(reply, err)` |
| `sdk.inject_input_media_sync` / `..._sync_opts(...)` | ⚠️ **Unavailable in Lua** (same as `inject_input_sync`) |
| `sdk.inject_interrupt_media(source, channel, text, blocks)` | Interrupt delivery with media |
| `sdk.inject_interrupt_media_opts(source, channel, text, blocks, opts)` | Same, with flags |
| `sdk.set_tool_blocks(blocks)` | Set multimodal blocks carried by the next tool message (lets the model see images / hear audio) |

View File

@ -641,11 +641,11 @@ end)
| `sdk.inject_interrupt(source, channel, text)` | 中断投递 |
| `sdk.inject_text_no_memory(source, channel, text)` | 免记忆投递 |
| `sdk.inject_text_opts` / `sdk.inject_interrupt_opts(source, channel, text, opts)` | 带标志位投递;`opts = { no_memory=bool, context_policy="none"|"prune", cleaner_name=string, priority="L1".."L3" }` |
| `sdk.inject_input_sync(source, channel, text)` | 同步注入并等本轮回复;返回 `(reply, err)`,无回复时 reply 为 nil |
| `sdk.inject_input_sync_opts(source, channel, text, opts)` | 同上带标志位 |
| `sdk.inject_input_sync(source, channel, text)` | ⚠️ **Lua 中不可用**:恒返回 `(nil, err)`。它要等本轮回复而 Lua 回调持有插件锁,必然自锁。需要同步等待请用 Go 插件,或用下面的异步注入 |
| `sdk.inject_input_sync_opts(source, channel, text, opts)` | 同上(不可用) |
| `sdk.inject_input_media(source, channel, text, blocks)` | 注入文本 + 多模态内容块 |
| `sdk.inject_input_media_opts(source, channel, text, blocks, opts)` | 同上带标志位 |
| `sdk.inject_input_media_sync` / `..._sync_opts(...)` | 带媒体的同步注入;返回 `(reply, err)` |
| `sdk.inject_input_media_sync` / `..._sync_opts(...)` | ⚠️ **Lua 中不可用**(同 `inject_input_sync` |
| `sdk.inject_interrupt_media(source, channel, text, blocks)` | 带媒体的中断注入 |
| `sdk.inject_interrupt_media_opts(source, channel, text, blocks, opts)` | 同上带标志位 |
| `sdk.set_tool_blocks(blocks)` | 设置下一轮 tool message 携带的多模态内容块(模型据此看图/听音频) |

View File

@ -82,11 +82,16 @@ function sdk.inject_interrupt_opts(source, channel, text, opts)
end
-- !impl
-- 同步注入:等待本轮回复 -> (reply, err);无回复时 reply 为 nil
function sdk.inject_input_sync(source, channel, text) return nil, nil end
-- 同步注入在 Lua 插件中**不可用**:会等本轮回复,而本轮正持有插件锁 ⇒ 必然自锁
-- 真实内核里恒返回 (nil, err);这里返回同样的错误,避免离线测试误以为可用。
function sdk.inject_input_sync(source, channel, text)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_text/inject_interrupt确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_input_sync_opts(source, channel, text, opts) return nil, nil end
function sdk.inject_input_sync_opts(source, channel, text, opts)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_text/inject_interrupt确需同步等待请改用 Go 插件。"
end
-- !impl
-- blocks: ContentBlock 数组,见 sdk.inject_input_media。
@ -109,10 +114,15 @@ function sdk.inject_input_media_opts(source, channel, text, blocks, opts)
end
-- !impl
function sdk.inject_input_media_sync(source, channel, text, blocks) return nil, nil end
-- 同 sdk.inject_input_syncLua 中不可用。
function sdk.inject_input_media_sync(source, channel, text, blocks)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_input_media确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_input_media_sync_opts(source, channel, text, blocks, opts) return nil, nil end
function sdk.inject_input_media_sync_opts(source, channel, text, blocks, opts)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_input_media_opts确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_interrupt_media(source, channel, text, blocks)

View File

@ -298,16 +298,15 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
return 0
}))
// pushReply 统一同步注入的返回约定:非空回复返回 (reply, nil)
// 无回复返回 (nil, nil),与数据类 API 的 (result, err) 约定一致。
pushReply := func(reply string) int {
if reply == "" {
L.Push(lua.LNil)
L.Push(lua.LNil)
return 2
}
L.Push(lua.LString(reply))
// 同步注入(文本/媒体)在 Lua 中**不可用**Lua 代码只在 Start / 工具 /
// 阶段 / 输出通道 / 事件回调里执行,这些路径都持有 plg.mu而 InjectInputSync
// 要等本轮回复,本轮回复的处理(以及回复路径上的阶段/工具回调)又需要同一把
// 锁 ⇒ 必然自锁。返回明确错误,而不是让插件在 30 分钟后超时。
// 需要同步等待的场景请改用 Go 插件(可在自己的 goroutine 里调),
// 或用 inject_text / inject_interrupt 异步投递。
luaSyncUnavailable := func(L *lua.LState) int {
L.Push(lua.LNil)
L.Push(lua.LString("同步注入在 Lua 插件中不可用:它要等本轮回复,而本轮正持有插件锁 ⇒ 必然自锁。请在事件回调/外部入口用 inject_text / inject_interrupt 异步投递;确需同步等待请改用 Go 插件。"))
return 2
}
@ -334,15 +333,9 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
return 0
}))
// ---- 同步注入:注入后等待本轮回复,返回 (reply, err) ----
// 注意:内置 SDK 的同名 InjectInputSync 是 (eventType, payload) 形态并遮蔽了
// 公共 SDK 的三参文本版本,故这里显式走 PluginSDK 的公共方法。
t.RawSetString("inject_input_sync", L.NewFunction(func(L *lua.LState) int {
return pushReply(s.PluginSDK.InjectInputSync(L.CheckString(1), L.CheckString(2), L.CheckString(3)))
}))
t.RawSetString("inject_input_sync_opts", L.NewFunction(func(L *lua.LState) int {
return pushReply(s.InjectInputSyncOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), parseInjectOptions(L, 4)))
}))
// ---- 同步注入:Lua 中不可用,统一返回明确错误(见 luaSyncUnavailable----
t.RawSetString("inject_input_sync", L.NewFunction(luaSyncUnavailable))
t.RawSetString("inject_input_sync_opts", L.NewFunction(luaSyncUnavailable))
// ---- 多模态注入1.1.0):内容块随下一次 LLM 请求送达 ----
t.RawSetString("set_tool_blocks", L.NewFunction(func(L *lua.LState) int {
@ -357,12 +350,8 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
s.InjectInputMediaOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4), parseInjectOptions(L, 5))
return 0
}))
t.RawSetString("inject_input_media_sync", L.NewFunction(func(L *lua.LState) int {
return pushReply(s.InjectInputMediaSync(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4)))
}))
t.RawSetString("inject_input_media_sync_opts", L.NewFunction(func(L *lua.LState) int {
return pushReply(s.InjectInputMediaSyncOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4), parseInjectOptions(L, 5)))
}))
t.RawSetString("inject_input_media_sync", L.NewFunction(luaSyncUnavailable))
t.RawSetString("inject_input_media_sync_opts", L.NewFunction(luaSyncUnavailable))
t.RawSetString("inject_interrupt_media", L.NewFunction(func(L *lua.LState) int {
s.InjectInterruptMedia(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4))
return 0

View File

@ -661,3 +661,46 @@ return plugin
}
bus.Publish(&events.Event{Type: events.EventAgentOutput, Source: "after-stop"})
}
// TestLuaSyncInjectUnavailable 钉住「Lua 同步注入必须立即返回明确错误、不能挂死」。
// 背景:同步注入要等本轮回复,而 Lua 回调持有插件锁 ⇒ 原实现必然自锁。
func TestLuaSyncInjectUnavailable(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"synclua","entry":"main.lua"}`), 0644)
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
local plugin = { name = "synclua" }
function plugin.start(sdk)
local r, err = sdk.inject_input_sync("src", "ch", "hello")
_G.sync_reply = r
_G.sync_err = err
local _, err2 = sdk.inject_input_sync_opts("src", "ch", "hello", { no_memory = true })
_G.sync_err2 = err2
local _, err3 = sdk.inject_input_media_sync("src", "ch", "hi", {})
_G.sync_media_err = err3
end
function plugin.stop() end
return plugin
`), 0644)
plg, err := tryLoadLua(dir, "synclua", nil)
if err != nil {
t.Fatalf("tryLoadLua failed: %v", err)
}
reg := internalConfig.NewConfigRegistry("")
sett := sdk.NewSettings("synclua", reg)
s := sdk.New("synclua", sdk.SDKConfig{Settings: sett})
if err := plg.Start(s); err != nil {
t.Fatalf("Start failed: %v", err)
}
defer plg.Stop()
L := plg.(*luaPlugin).L
if L.GetGlobal("sync_reply").Type() != lua.LTNil {
t.Errorf("sync inject should return nil reply, got %v", L.GetGlobal("sync_reply"))
}
for _, k := range []string{"sync_err", "sync_err2", "sync_media_err"} {
if L.GetGlobal(k).Type() != lua.LTString {
t.Errorf("%s should be an error string (unavailable), got %v", k, L.GetGlobal(k))
}
}
}

View File

@ -82,11 +82,16 @@ function sdk.inject_interrupt_opts(source, channel, text, opts)
end
-- !impl
-- 同步注入:等待本轮回复 -> (reply, err);无回复时 reply 为 nil
function sdk.inject_input_sync(source, channel, text) return nil, nil end
-- 同步注入在 Lua 插件中**不可用**:会等本轮回复,而本轮正持有插件锁 ⇒ 必然自锁
-- 真实内核里恒返回 (nil, err);这里返回同样的错误,避免离线测试误以为可用。
function sdk.inject_input_sync(source, channel, text)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_text/inject_interrupt确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_input_sync_opts(source, channel, text, opts) return nil, nil end
function sdk.inject_input_sync_opts(source, channel, text, opts)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_text/inject_interrupt确需同步等待请改用 Go 插件。"
end
-- !impl
-- blocks: ContentBlock 数组,见 sdk.inject_input_media。
@ -109,10 +114,15 @@ function sdk.inject_input_media_opts(source, channel, text, blocks, opts)
end
-- !impl
function sdk.inject_input_media_sync(source, channel, text, blocks) return nil, nil end
-- 同 sdk.inject_input_syncLua 中不可用。
function sdk.inject_input_media_sync(source, channel, text, blocks)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_input_media确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_input_media_sync_opts(source, channel, text, blocks, opts) return nil, nil end
function sdk.inject_input_media_sync_opts(source, channel, text, blocks, opts)
return nil, "同步注入在 Lua 插件中不可用:请在事件回调/外部入口用 inject_input_media_opts确需同步等待请改用 Go 插件。"
end
-- !impl
function sdk.inject_interrupt_media(source, channel, text, blocks)