From 6878f0126dac901e19efa288583dac6dbd329630 Mon Sep 17 00:00:00 2001 From: HomeAgent Agent Date: Sun, 13 Sep 2026 21:59:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(lua):=20=E5=90=8C=E6=AD=A5=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E5=9C=A8=20Lua=20=E4=B8=AD=E6=98=8E=E7=A1=AE=E6=8A=A5?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E7=94=A8=EF=BC=88=E9=81=BF=E5=85=8D=E8=87=AA?= =?UTF-8?q?=E9=94=81=EF=BC=89+=20=E6=96=87=E6=A1=A3/mock=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdk.inject_input_sync / *_opts / inject_input_media_sync* 在 Lua 里必然自锁: Lua 代码只在 Start/工具/阶段/输出/事件回调中执行,这些路径都持有 plg.mu, 而同步注入要等本轮回复(回复路径上的回调又需要同一把锁)。原实现会挂死 直到超时;现改为立即返回明确错误,并在中英文 PLUGIN_DEV 里标注不可用 + 指向 Go 插件/异步注入。mock sdk.lua(SDK 仓为事实源)同步为同样的错误语义。 新增 TestLuaSyncInjectUnavailable 钉住不挂死。 --- assets/docs/en/PLUGIN_DEV.md | 6 ++-- assets/docs/zh/PLUGIN_DEV.md | 6 ++-- internal/lua/sdk/sdk.lua | 20 ++++++++--- internal/plugin/lua_plugin.go | 37 +++++++------------ internal/plugin/lua_plugin_test.go | 43 +++++++++++++++++++++++ third_party/homeagent-sdk/sdk/lua/sdk.lua | 20 ++++++++--- 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/assets/docs/en/PLUGIN_DEV.md b/assets/docs/en/PLUGIN_DEV.md index 2276c1f..a025335 100644 --- a/assets/docs/en/PLUGIN_DEV.md +++ b/assets/docs/en/PLUGIN_DEV.md @@ -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) | diff --git a/assets/docs/zh/PLUGIN_DEV.md b/assets/docs/zh/PLUGIN_DEV.md index e3d5be5..856e0a5 100644 --- a/assets/docs/zh/PLUGIN_DEV.md +++ b/assets/docs/zh/PLUGIN_DEV.md @@ -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 携带的多模态内容块(模型据此看图/听音频) | diff --git a/internal/lua/sdk/sdk.lua b/internal/lua/sdk/sdk.lua index 2f4dfb9..6167dc6 100644 --- a/internal/lua/sdk/sdk.lua +++ b/internal/lua/sdk/sdk.lua @@ -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_sync:Lua 中不可用。 +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) diff --git a/internal/plugin/lua_plugin.go b/internal/plugin/lua_plugin.go index df121c5..590de17 100644 --- a/internal/plugin/lua_plugin.go +++ b/internal/plugin/lua_plugin.go @@ -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 diff --git a/internal/plugin/lua_plugin_test.go b/internal/plugin/lua_plugin_test.go index a2e2beb..c652ee0 100644 --- a/internal/plugin/lua_plugin_test.go +++ b/internal/plugin/lua_plugin_test.go @@ -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)) + } + } +} diff --git a/third_party/homeagent-sdk/sdk/lua/sdk.lua b/third_party/homeagent-sdk/sdk/lua/sdk.lua index 2f4dfb9..6167dc6 100644 --- a/third_party/homeagent-sdk/sdk/lua/sdk.lua +++ b/third_party/homeagent-sdk/sdk/lua/sdk.lua @@ -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_sync:Lua 中不可用。 +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)