Files
HomeAgent/internal/plugin/lua_plugin.go
HomeAgent Agent 5f63f6ec6c fix(lua): events.subscribe 改用内部 Subscribe + 订阅生命周期(修死锁/use-after-close)
上一版 Lua 对齐引入的 sdk.events.subscribe 有两个真问题,本提交修掉:

1) 用了公共 SDK 的 Events(),但本内核从未注入 event subscriber
   (SetEventSubscriber 全仓无调用点),拿到永远是 nil ⇒ subscribe 只会
   返回 "events unavailable"。改用内部 SDK 的 s.Subscribe——内置插件走的就是
   这条路径(cli/webui/skillmgr 全用它)。

2) 自死锁:subscribe 会在 Lua 的 plugin.start(sdk) 回调里被调用,而
   luaPlugin.Start 正持有 p.mu;原实现在 subscribe 里再 lock p.mu 追加 subs,
   不可重入 ⇒ 测试实测 30s 超时。改用独立的 subsMu。

3) use-after-close:Stop 会 Close LState,但事件订阅此前无人取消,残留回调
   再触发就会碰已关的 L。现在:Stop 先(不持 p.mu,避免与 Bus.Publish
   锁序反转)取 subsMu 取消全部订阅,再置 closed 并关 L;事件回调持 p.mu 后
   先查 closed,已进入等锁的旧回调会直接返回。

4) plugin_mgr 访问补 nil 保护(部分单测构造的 SDK 不含 pluginMgr)。

回归:TestLuaEventsSubscribeAndStopCleanup——订阅后 Publish 命中、Stop 后
再 Publish 不 panic。全套 Lua 测试在 -race 下通过。
2026-09-13 20:33:25 +08:00

1270 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package plugin
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"sync"
agentEvents "gitcode.com/JianFeeeee/HomeAgent/internal/events"
luaSDK "gitcode.com/JianFeeeee/HomeAgent/internal/lua/sdk"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
lua "github.com/yuin/gopher-lua"
)
type toolReg struct {
def sdk.ToolDef
handler *lua.LFunction
}
type outputChReg struct {
caps int
desc string
def sdk.ChannelDef
handler *lua.LFunction
}
type stageReg struct {
handler *lua.LFunction
scope sdk.StageScope
}
// luaPlugin wraps a Lua script as an sdk.Plugin.
type luaPlugin struct {
name string
L *lua.LState
tbl *lua.LTable
tools map[string]*toolReg
stages map[sdk.Stage]*stageReg
outputChs map[string]*outputChReg
inputDefs map[string]sdk.ChannelDef
// subs 是本插件注册的事件订阅取消函数Stop 时兜底取消,
// 避免 L 已 Close 后残留回调被触发use-after-close
// 用独立的 subsMu 而非 musubscribe 会在 Lua 的 start 回调里被调,
// 而 Start 正持着 mu —— 用 mu 就是不可重入的自死锁。
subs []func()
subsMu sync.Mutex
// closed 在 Stop 里置位(持 mu事件回调持 mu 后先查它,
// 防止“回调已通过取消订阅检查、但等锁期间 L 被 Close”的竞态。
closed bool
mu sync.Mutex
}
func newLuaPlugin(luaPath, name string) (*luaPlugin, error) {
L := lua.NewState()
// 1) 加载嵌入式 sdk.lua接口定义 + pure Lua mock 实现)
if err := L.DoString(luaSDK.SDKSource); err != nil {
L.Close()
return nil, fmt.Errorf("load sdk.lua: %w", err)
}
sdkTbl := L.GetGlobal("sdk")
sdkTable, ok := sdkTbl.(*lua.LTable)
if !ok {
L.Close()
return nil, fmt.Errorf("sdk.lua must set global 'sdk' table")
}
// 清除 DoString 留在栈上的返回值,栈顶归零
L.SetTop(0)
plg := &luaPlugin{
name: name,
L: L,
tools: make(map[string]*toolReg),
stages: make(map[sdk.Stage]*stageReg),
outputChs: make(map[string]*outputChReg),
inputDefs: make(map[string]sdk.ChannelDef),
}
// 2) 替换 !impl 函数为 Go stub暂存 handler等 Start 时注册到真实 SDK
replaceSDKStubs(L, sdkTable, plg)
// 3) 加载插件主脚本(此时 sdk.* 全局已就绪,带 stub 实现)
if err := L.DoFile(luaPath); err != nil {
L.Close()
return nil, fmt.Errorf("load %s: %w", luaPath, err)
}
// 4) 如果脚本返回了 table保存
if L.GetTop() > 0 {
if tbl, ok := L.Get(-1).(*lua.LTable); ok {
plg.tbl = tbl
L.Pop(1)
}
}
return plg, nil
}
// replaceSDKStubs 替换 sdk 表中的 !impl 函数为 Go stub。
// stub 暂存 handler等 Start 时才注册到真实 SDK。
func replaceSDKStubs(L *lua.LState, t *lua.LTable, plg *luaPlugin) {
t.RawSetString("log", L.NewFunction(func(L *lua.LState) int {
level := L.ToString(1)
msg := L.ToString(2)
fmt.Printf("[lua-plugin/%s] %s: %s\n", plg.name, level, msg)
return 0
}))
t.RawSetString("register_tool", L.NewFunction(func(L *lua.LState) int {
toolName := L.CheckString(1)
defTbl := L.CheckTable(2)
handler := L.CheckFunction(3)
goDef := parseToolDef(L, defTbl, plg, toolName)
plg.mu.Lock()
plg.tools[toolName] = &toolReg{def: goDef, handler: handler}
plg.mu.Unlock()
return 0
}))
t.RawSetString("register_stage", L.NewFunction(func(L *lua.LState) int {
stage := sdk.Stage(L.CheckString(1))
handler := L.CheckFunction(2)
scope := parseStageScope(L)
plg.mu.Lock()
plg.stages[stage] = &stageReg{handler: handler, scope: scope}
plg.mu.Unlock()
return 0
}))
t.RawSetString("register_api", L.NewFunction(func(L *lua.LState) int {
return 0
}))
t.RawSetString("register_output_channel", L.NewFunction(func(L *lua.LState) int {
name := L.CheckString(1)
caps := L.CheckInt(2)
desc := L.CheckString(3)
defTbl := L.CheckTable(4)
handler := L.CheckFunction(5)
chDef := parseChannelDef(L, defTbl, plg)
plg.mu.Lock()
plg.outputChs[name] = &outputChReg{caps: caps, desc: desc, def: chDef, handler: handler}
plg.mu.Unlock()
return 0
}))
t.RawSetString("register_input_channel", L.NewFunction(func(L *lua.LState) int {
name := L.CheckString(1)
defTbl := L.CheckTable(2)
chDef := parseChannelDef(L, defTbl, plg)
plg.mu.Lock()
plg.inputDefs[name] = chDef
plg.mu.Unlock()
return 0
}))
t.RawSetString("get_setting", L.NewFunction(func(L *lua.LState) int {
L.Push(lua.LNil)
return 1
}))
t.RawSetString("set_setting", L.NewFunction(func(L *lua.LState) int {
return 0
}))
t.RawSetString("inject_text", L.NewFunction(func(L *lua.LState) int { return 0 }))
t.RawSetString("inject_interrupt", L.NewFunction(func(L *lua.LState) int { return 0 }))
t.RawSetString("inject_text_no_memory", L.NewFunction(func(L *lua.LState) int { return 0 }))
// http 子表
if httpTable, ok := t.RawGetString("http").(*lua.LTable); ok {
httpTable.RawSetString("get", L.NewFunction(func(L *lua.LState) int {
url := L.CheckString(1)
resp, err := http.Get(url)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
result := L.NewTable()
result.RawSetString("status", lua.LNumber(resp.StatusCode))
result.RawSetString("body", lua.LString(string(body)))
headers := L.NewTable()
for k, v := range resp.Header {
headers.RawSetString(k, lua.LString(strings.Join(v, ", ")))
}
result.RawSetString("headers", headers)
L.Push(result)
L.Push(lua.LNil)
return 2
}))
httpTable.RawSetString("post", L.NewFunction(func(L *lua.LState) int {
url := L.CheckString(1)
body := L.CheckString(2)
contentType := L.OptString(3, "application/json")
resp, err := http.Post(url, contentType, strings.NewReader(body))
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
result := L.NewTable()
result.RawSetString("status", lua.LNumber(resp.StatusCode))
result.RawSetString("body", lua.LString(string(respBody)))
L.Push(result)
L.Push(lua.LNil)
return 2
}))
}
}
// replaceSDKReal 用真实 SDK 实现替换 sdk 表。
// 此时 plg.handlers/stages 已存有加载期间注册的 handler。
func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginSDK) {
t.RawSetString("register_tool", L.NewFunction(func(L *lua.LState) int {
toolName := L.CheckString(1)
defTbl := L.CheckTable(2)
handler := L.CheckFunction(3)
goDef := parseToolDef(L, defTbl, plg, toolName)
h := makeToolHandler(plg, toolName, handler)
if err := s.RegisterTool(toolName, goDef, h); err != nil {
L.RaiseError("register_tool: %v", err)
}
return 0
}))
t.RawSetString("register_stage", L.NewFunction(func(L *lua.LState) int {
stage := sdk.Stage(L.CheckString(1))
handler := L.CheckFunction(2)
scope := parseStageScope(L)
h := makeStageHandler(plg, stage, handler)
s.RegisterStage(stage, h, scope)
return 0
}))
t.RawSetString("register_api", L.NewFunction(func(L *lua.LState) int {
apiName := L.CheckString(1)
s.RegisterPluginAPI(apiName)
return 0
}))
t.RawSetString("register_output_channel", L.NewFunction(func(L *lua.LState) int {
name := L.CheckString(1)
caps := L.CheckInt(2)
desc := L.CheckString(3)
defTbl := L.CheckTable(4)
handler := L.CheckFunction(5)
chDef := parseChannelDef(L, defTbl, plg)
h := makeOutputHandler(plg, handler)
if err := s.RegisterOutputChannel(name, caps, desc, chDef, h); err != nil {
L.RaiseError("register_output_channel: %v", err)
}
return 0
}))
t.RawSetString("register_input_channel", L.NewFunction(func(L *lua.LState) int {
name := L.CheckString(1)
defTbl := L.CheckTable(2)
chDef := parseChannelDef(L, defTbl, plg)
if err := s.RegisterInputChannel(name, chDef); err != nil {
L.RaiseError("register_input_channel: %v", err)
}
return 0
}))
t.RawSetString("get_setting", L.NewFunction(func(L *lua.LState) int {
key := L.CheckString(1)
val, _ := s.Settings().Get(key)
L.Push(goValueToLua(L, val))
return 1
}))
t.RawSetString("set_setting", L.NewFunction(func(L *lua.LState) int {
key := L.CheckString(1)
val := luaValueToGo(L.CheckAny(2))
s.Settings().Set(key, val)
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))
L.Push(lua.LNil)
return 2
}
t.RawSetString("inject_text", L.NewFunction(func(L *lua.LState) int {
s.InjectText(L.CheckString(1), L.CheckString(2), L.CheckString(3))
return 0
}))
t.RawSetString("inject_interrupt", L.NewFunction(func(L *lua.LState) int {
s.InjectInterruptText(L.CheckString(1), L.CheckString(2), L.CheckString(3))
return 0
}))
t.RawSetString("inject_text_no_memory", L.NewFunction(func(L *lua.LState) int {
s.InjectTextNoMemory(L.CheckString(1), L.CheckString(2), L.CheckString(3))
return 0
}))
// ---- 1.2.0 注入标志位no_memory / context_policy / cleaner_name / priority----
t.RawSetString("inject_text_opts", L.NewFunction(func(L *lua.LState) int {
s.InjectTextOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), parseInjectOptions(L, 4))
return 0
}))
t.RawSetString("inject_interrupt_opts", L.NewFunction(func(L *lua.LState) int {
s.InjectInterruptTextOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), parseInjectOptions(L, 4))
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)))
}))
// ---- 多模态注入1.1.0):内容块随下一次 LLM 请求送达 ----
t.RawSetString("set_tool_blocks", L.NewFunction(func(L *lua.LState) int {
s.SetToolBlocks(luaToContentBlocks(L, 1))
return 0
}))
t.RawSetString("inject_input_media", L.NewFunction(func(L *lua.LState) int {
s.InjectInputMedia(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4))
return 0
}))
t.RawSetString("inject_input_media_opts", L.NewFunction(func(L *lua.LState) int {
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_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
}))
t.RawSetString("inject_interrupt_media_opts", L.NewFunction(func(L *lua.LState) int {
s.InjectInterruptMediaOpts(L.CheckString(1), L.CheckString(2), L.CheckString(3), luaToContentBlocks(L, 4), parseInjectOptions(L, 5))
return 0
}))
// ---- 1.3.0 动态输出通道注销:随资源生灭的通道(如远程设备)必须能注销,
// 否则 output_list_channels 会一直列着死通道骗模型。 ----
t.RawSetString("unregister_output_channel", L.NewFunction(func(L *lua.LState) int {
if err := s.UnregisterOutputChannel(L.CheckString(1)); err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LNil)
L.Push(lua.LNil)
return 2
}))
// ---- 数据类 API与 C ABI 外部插件面完全对齐)----
// 约定:结果型返回 (result, err)void 型返回 (nil, err),成功时 err 为 nil。
subTable := func(name string) *lua.LTable {
if v := t.RawGetString(name); v != nil {
if st, ok := v.(*lua.LTable); ok {
return st
}
}
st := L.NewTable()
t.RawSetString(name, st)
return st
}
pushVal := func(val interface{}) int {
L.Push(jsonToLuaValue(L, val))
L.Push(lua.LNil)
return 2
}
// pushList 归一化 nil/空 切片与 map 为 Lua 空表。
pushList := func(val interface{}) int {
if val == nil {
return pushVal([]interface{}{})
}
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
if v.Len() == 0 {
return pushVal([]interface{}{})
}
}
return pushVal(val)
}
pushErr := func(err error) int {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
pushNil := func() int {
L.Push(lua.LNil)
L.Push(lua.LNil)
return 2
}
// sdk.set_auto_restart(enabled)
t.RawSetString("set_auto_restart", L.NewFunction(func(L *lua.LState) int {
s.SetAutoRestart(L.CheckBool(1))
return 0
}))
// ---- sdk.memory.* (graph memory, 对齐 CORE_MEMORY_*) ----
memTbl := subTable("memory")
memTbl.RawSetString("recall", L.NewFunction(func(L *lua.LState) int {
if m := s.Memory(); m != nil {
var query []string
switch v := L.Get(1).(type) {
case *lua.LTable:
v.ForEach(func(_, e lua.LValue) { query = append(query, e.String()) })
default:
query = []string{L.CheckString(1)}
}
entities, relations, err := m.Recall(query, L.OptInt(2, 1))
if err != nil {
return pushErr(err)
}
return pushVal(map[string]interface{}{"entities": entities, "relations": relations})
}
return pushVal(map[string]interface{}{"entities": []interface{}{}, "relations": []interface{}{}})
}))
memTbl.RawSetString("commit", L.NewFunction(func(L *lua.LState) int {
var triples []sdk.Triple
if tbl := L.OptTable(1, nil); tbl != nil {
tbl.ForEach(func(_, v lua.LValue) {
if t2, ok := v.(*lua.LTable); ok {
// sentence_text / media_digests 是媒体绑定链的必经环节:
// 媒体引用挂在句子上,漏掉这两个字段会让图片永远绑不上记忆。
var digests []string
if mt, ok := t2.RawGetString("media_digests").(*lua.LTable); ok {
mt.ForEach(func(_, e lua.LValue) { digests = append(digests, e.String()) })
}
triples = append(triples, sdk.Triple{
Subject: t2.RawGetString("subject").String(),
Relation: t2.RawGetString("relation").String(),
Object: t2.RawGetString("object").String(),
Confidence: float64(lua.LVAsNumber(t2.RawGetString("confidence"))),
SubjectType: t2.RawGetString("subject_type").String(),
ObjectType: t2.RawGetString("object_type").String(),
SentenceText: t2.RawGetString("sentence_text").String(),
MediaDigests: digests,
})
}
})
}
if m := s.Memory(); m != nil {
if err := m.Commit(triples); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
memTbl.RawSetString("introspect", L.NewFunction(func(L *lua.LState) int {
if m := s.Memory(); m != nil {
r, err := m.Introspect()
if err != nil {
return pushErr(err)
}
return pushVal(r)
}
return pushVal(map[string]interface{}{})
}))
memTbl.RawSetString("merge", L.NewFunction(func(L *lua.LState) int {
if m := s.Memory(); m != nil {
n, err := m.MergeEntities(L.CheckString(1), L.CheckString(2))
if err != nil {
return pushErr(err)
}
return pushVal(n)
}
return pushVal(0)
}))
memTbl.RawSetString("purge", L.NewFunction(func(L *lua.LState) int {
mode := "soft"
if L.OptBool(2, false) {
mode = "hard"
}
criteria := map[string]string{}
if tbl := L.OptTable(1, nil); tbl != nil {
tbl.ForEach(func(k, v lua.LValue) {
criteria[k.String()] = v.String()
})
}
if m := s.Memory(); m != nil {
n, err := m.Purge(criteria, mode)
if err != nil {
return pushErr(err)
}
return pushVal(n)
}
return pushVal(0)
}))
// ---- sdk.doc.* (document memory, 对齐 CORE_DOC_*) ----
docTbl := subTable("doc")
docTbl.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
if dm := s.DocMemory(); dm != nil {
return pushList(dm.Query(L.CheckString(1), L.OptInt(2, 5)))
}
return pushVal([]interface{}{})
}))
docTbl.RawSetString("insert", L.NewFunction(func(L *lua.LState) int {
if dm := s.DocMemory(); dm != nil {
if err := dm.Insert(docFromLua(L.CheckTable(1))); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
// insert_with_media1.1.0):文档直接持有媒体块,文档向量融合其原生向量,
// 图片按自己的向量被召回,不依赖任何生成的描述文本。
docTbl.RawSetString("insert_with_media", L.NewFunction(func(L *lua.LState) int {
if dm := s.DocMemory(); dm != nil {
if err := dm.InsertWithMedia(docFromLua(L.CheckTable(1)), luaToAttachments(L, L.Get(2))); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
docTbl.RawSetString("remove", L.NewFunction(func(L *lua.LState) int {
if dm := s.DocMemory(); dm != nil {
dm.Remove(L.CheckString(1))
}
return pushNil()
}))
docTbl.RawSetString("stats", L.NewFunction(func(L *lua.LState) int {
if dm := s.DocMemory(); dm != nil {
return pushVal(dm.Stats())
}
return pushVal(map[string]interface{}{})
}))
// ---- sdk.knowledge.* (对齐 CORE_KNOWLEDGE_*) ----
knTbl := subTable("knowledge")
knTbl.RawSetString("search", L.NewFunction(func(L *lua.LState) int {
if kn := s.Knowledge(); kn != nil {
results, err := kn.Search(L.CheckString(1), L.OptInt(2, 5))
if err != nil {
return pushErr(err)
}
return pushList(results)
}
return pushVal([]interface{}{})
}))
knTbl.RawSetString("add", L.NewFunction(func(L *lua.LState) int {
if kn := s.Knowledge(); kn != nil {
if err := kn.Add(L.CheckString(1), L.CheckString(2)); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
knTbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
if kn := s.Knowledge(); kn != nil {
list, err := kn.List()
if err != nil {
return pushErr(err)
}
return pushList(list)
}
return pushVal([]interface{}{})
}))
// ---- sdk.text_memory.* (对齐 CORE_TEXT_MEMORY_APPEND) ----
tmTbl := subTable("text_memory")
tmTbl.RawSetString("append", L.NewFunction(func(L *lua.LState) int {
if tmem := s.TextMemory(); tmem != nil {
tbl := L.CheckTable(1)
if err := tmem.Append(sdk.TextEvent{
Role: tbl.RawGetString("role").String(),
Content: tbl.RawGetString("content").String(),
Timestamp: int64(lua.LVAsNumber(tbl.RawGetString("timestamp"))),
Channel: tbl.RawGetString("channel").String(),
Attachments: luaToAttachments(L, tbl.RawGetString("attachments")),
}); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
// ---- sdk.llm.* (对齐 CORE_LLM_*) ----
llmTbl := subTable("llm")
llmTbl.RawSetString("list_sources", L.NewFunction(func(L *lua.LState) int {
if llm := s.LLM(); llm != nil {
return pushList(llm.ListSources())
}
return pushVal([]interface{}{})
}))
llmTbl.RawSetString("set_source", L.NewFunction(func(L *lua.LState) int {
if llm := s.LLM(); llm != nil {
if err := llm.SetSource(L.CheckString(1)); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
llmTbl.RawSetString("current_source", L.NewFunction(func(L *lua.LState) int {
if llm := s.LLM(); llm != nil {
return pushVal(llm.CurrentSource())
}
return pushVal(nil)
}))
// ---- sdk.social.* (只读,对齐 CORE_SOCIAL_*,当前核心未装配 SocialAPI 时为 nil) ----
socTbl := subTable("social")
socTbl.RawSetString("get_person", L.NewFunction(func(L *lua.LState) int {
if social := s.Social(); social != nil {
p, err := social.GetPerson(L.CheckString(1))
if err != nil {
return pushErr(err)
}
return pushVal(p)
}
return pushVal(nil)
}))
socTbl.RawSetString("get_network", L.NewFunction(func(L *lua.LState) int {
if social := s.Social(); social != nil {
profiles, err := social.GetNetwork(L.CheckString(1), L.OptInt(2, 1))
if err != nil {
return pushErr(err)
}
return pushList(profiles)
}
return pushVal([]interface{}{})
}))
socTbl.RawSetString("get_trait", L.NewFunction(func(L *lua.LState) int {
if social := s.Social(); social != nil {
val, ok := social.GetTrait(L.CheckString(1), L.CheckString(2))
return pushVal(map[string]interface{}{"value": val, "found": ok})
}
return pushVal(map[string]interface{}{"value": nil, "found": false})
}))
socTbl.RawSetString("get_relations", L.NewFunction(func(L *lua.LState) int {
if social := s.Social(); social != nil {
rels, err := social.GetRelations(L.CheckString(1))
if err != nil {
return pushErr(err)
}
return pushList(rels)
}
return pushVal([]interface{}{})
}))
socTbl.RawSetString("list_persons", L.NewFunction(func(L *lua.LState) int {
if social := s.Social(); social != nil {
persons, err := social.ListPersons()
if err != nil {
return pushErr(err)
}
return pushList(persons)
}
return pushVal([]interface{}{})
}))
// ---- sdk.settings.* (作用域变体,对齐 CORE_SETTINGS_*) ----
settTbl := subTable("settings")
settTbl.RawSetString("get_core", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
v, err := st.GetCore(L.CheckString(1))
if err != nil {
return pushErr(err)
}
return pushVal(v)
}
return pushVal(nil)
}))
settTbl.RawSetString("set_core", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
if err := st.SetCore(L.CheckString(1), luaValueToGo(L.CheckAny(2))); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
settTbl.RawSetString("list_core", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
keys, err := st.ListCore(L.OptString(1, ""))
if err != nil {
return pushErr(err)
}
return pushList(keys)
}
return pushVal([]interface{}{})
}))
settTbl.RawSetString("get_plugin", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
v, err := st.GetPlugin(L.CheckString(1), L.CheckString(2))
if err != nil {
return pushErr(err)
}
return pushVal(v)
}
return pushVal(nil)
}))
settTbl.RawSetString("set_plugin", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
if err := st.SetPlugin(L.CheckString(1), L.CheckString(2), luaValueToGo(L.CheckAny(3))); err != nil {
return pushErr(err)
}
}
return pushNil()
}))
settTbl.RawSetString("list_plugin", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
keys, err := st.ListPlugin(L.CheckString(1), L.OptString(2, ""))
if err != nil {
return pushErr(err)
}
return pushList(keys)
}
return pushVal([]interface{}{})
}))
settTbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
keys, err := st.List(L.OptString(1, ""))
if err != nil {
return pushErr(err)
}
return pushList(keys)
}
return pushVal([]interface{}{})
}))
settTbl.RawSetString("register_def", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
tbl := L.CheckTable(1)
def := sdk.ConfigDef{
Key: tbl.RawGetString("key").String(),
Default: luaValueToGo(tbl.RawGetString("default")),
Type: tbl.RawGetString("type").String(),
DisplayName: tbl.RawGetString("display_name").String(),
Description: tbl.RawGetString("description").String(),
Category: tbl.RawGetString("category").String(),
Min: float64(lua.LVAsNumber(tbl.RawGetString("min"))),
Max: float64(lua.LVAsNumber(tbl.RawGetString("max"))),
Step: float64(lua.LVAsNumber(tbl.RawGetString("step"))),
Required: lua.LVAsBool(tbl.RawGetString("required")),
Secret: lua.LVAsBool(tbl.RawGetString("secret")),
}
if opts := tbl.RawGetString("options"); opts != nil {
if ot, ok := opts.(*lua.LTable); ok {
ot.ForEach(func(_, v lua.LValue) {
def.Options = append(def.Options, v.String())
})
}
}
st.RegisterDef(def)
}
return pushNil()
}))
settTbl.RawSetString("defs", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
return pushList(st.Defs(L.OptString(1, "")))
}
return pushVal([]interface{}{})
}))
settTbl.RawSetString("dump", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
return pushVal(st.Dump())
}
return pushVal(map[string]interface{}{})
}))
settTbl.RawSetString("plugins", L.NewFunction(func(L *lua.LState) int {
if st := s.Settings(); st != nil {
return pushList(st.Plugins())
}
return pushVal([]interface{}{})
}))
// ---- sdk.events.*(只读事件订阅)----
//
// 用内部 SDK 的 Subscribe内置插件用的是同一条路径
// 不用公共 SDK 的 Events()——那个 subscriber 在本内核里从未被注入
// SetEventSubscriber 无调用点),拿到的永远是 nil。
//
// 回调用内核事件发布 goroutine 上执行必须只做轻量转发Lua 单状态 + 互斥锁);
// 阻塞会卡死本插件的全部调用。返回一个取消订阅函数,并在 Stop 时兜底取消
// (否则插件停掉/重载后 L 已 Close残留回调再触发就是 use-after-close
evTbl := subTable("events")
evTbl.RawSetString("subscribe", L.NewFunction(func(L *lua.LState) int {
eventType := L.CheckString(1)
fn := L.CheckFunction(2)
unsub := s.Subscribe(agentEvents.EventType(eventType), func(evt *agentEvents.Event) {
plg.mu.Lock()
defer plg.mu.Unlock()
if plg.closed {
return
}
L2 := plg.L
tbl := L2.NewTable()
tbl.RawSetString("type", lua.LString(string(evt.Type)))
tbl.RawSetString("source", lua.LString(evt.Source))
tbl.RawSetString("timestamp", lua.LNumber(evt.Timestamp))
tbl.RawSetString("payload", goValueToLua(L2, evt.Payload))
L2.Push(fn)
L2.Push(tbl)
if err := L2.PCall(1, 0, nil); err != nil {
fmt.Printf("[lua-plugin/%s] event handler error: %v\n", plg.name, err)
}
})
plg.subsMu.Lock()
plg.subs = append(plg.subs, unsub)
plg.subsMu.Unlock()
L.Push(L.NewFunction(func(L *lua.LState) int {
unsub() // 事件总线的取消订阅是幂等的(重复调用只会匹配不到)
return 0
}))
L.Push(lua.LNil)
return 2
}))
// ---- sdk.plugin_mgr.*(插件管理,与外部插件的 PluginMgrAPI 对齐)----
// PluginMgr 可能未装配(如部分单测的 SDK 构造),此时返回"不可用"而不是 panic。
pmTbl := subTable("plugin_mgr")
pmTbl.RawSetString("reload_one", L.NewFunction(func(L *lua.LState) int {
pm := s.PluginMgr()
if pm == nil {
return pushErr(fmt.Errorf("plugin manager unavailable"))
}
if err := pm.ReloadOne(L.CheckString(1)); err != nil {
return pushErr(err)
}
return pushNil()
}))
pmTbl.RawSetString("list_loaded", L.NewFunction(func(L *lua.LState) int {
pm := s.PluginMgr()
if pm == nil {
return pushList([]interface{}{})
}
return pushList(pm.ListLoadedPlugins())
}))
pmTbl.RawSetString("is_disabled", L.NewFunction(func(L *lua.LState) int {
pm := s.PluginMgr()
if pm == nil {
return pushVal(false)
}
return pushVal(pm.IsPluginDisabled(L.CheckString(1)))
}))
}
func makeToolHandler(plg *luaPlugin, name string, fn *lua.LFunction) sdk.ToolHandler {
return func(args map[string]interface{}) (interface{}, error) {
plg.mu.Lock()
defer plg.mu.Unlock()
L := plg.L
L.Push(fn)
L.Push(goValueToLua(L, args))
if err := L.PCall(1, 1, nil); err != nil {
return nil, fmt.Errorf("lua tool %s: %w", name, err)
}
result := L.Get(-1)
L.Pop(1)
return luaValueToGo(result), nil
}
}
func makeStageHandler(plg *luaPlugin, stage sdk.Stage, fn *lua.LFunction) sdk.StageHandler {
return func(sc *sdk.StageContext) error {
plg.mu.Lock()
defer plg.mu.Unlock()
L := plg.L
ctx := map[string]interface{}{
"raw_message": sc.RawMessage,
"user_id": sc.UserID,
"group_id": sc.GroupID,
"phase": string(sc.Phase),
"llm_text": sc.LLMText,
"reasoning_content": sc.ReasoningContent,
"final_text": sc.FinalText,
"no_memory": sc.NoMemory,
}
if len(sc.ContextMsgs) > 0 {
ctx["context_msgs"] = jsonToIface(sc.ContextMsgs)
}
if len(sc.TokenUsage) > 0 {
ctx["token_usage"] = jsonToIface(sc.TokenUsage)
}
if len(sc.Memory) > 0 {
ctx["memory"] = jsonToIface(sc.Memory)
}
if len(sc.Extra) > 0 {
ctx["extra"] = jsonToIface(sc.Extra)
}
if len(sc.Errors) > 0 {
ctx["errors"] = jsonToIface(sc.Errors)
}
if sc.Response != nil {
ctx["response"] = *sc.Response
}
if len(sc.ToolCalls) > 0 {
ctx["tool_calls"] = jsonToIface(sc.ToolCalls)
}
if len(sc.ToolResults) > 0 {
ctx["tool_results"] = jsonToIface(sc.ToolResults)
}
ctxTbl := goValueToLua(L, ctx).(*lua.LTable)
L.Push(fn)
L.Push(ctxTbl)
if err := L.PCall(1, 0, nil); err != nil {
return fmt.Errorf("lua stage %s: %w", stage, err)
}
// 写回Lua handler 对 ctx table 的字段修改同步回内核 StageContext
applyLuaStageResult(sc, luaValueToGo(ctxTbl))
return nil
}
}
// applyLuaStageResult 将 Lua stage handler 修改后的 ctx 字段写回内核 StageContext。
// Lua 侧修改的字段以 Lua table引用形式读回仅回写插件有权改写的键。
func applyLuaStageResult(sc *sdk.StageContext, modified interface{}) {
m, ok := modified.(map[string]interface{})
if !ok {
return
}
sc.Lock()
defer sc.Unlock()
if v, ok := m["raw_message"].(string); ok {
sc.RawMessage = v
}
if v, ok := m["llm_text"].(string); ok {
sc.LLMText = v
}
if v, ok := m["final_text"].(string); ok {
sc.FinalText = v
}
if v, ok := m["user_id"].(string); ok {
sc.UserID = v
}
if v, ok := m["group_id"].(string); ok {
sc.GroupID = v
}
if v, ok := m["no_memory"].(bool); ok {
sc.NoMemory = v
}
if v, ok := m["response"].(string); ok {
vv := v
sc.Response = &vv
}
if v, ok := m["tool_calls"].([]interface{}); ok && len(v) > 0 {
if b, err := json.Marshal(v); err == nil {
var tcs []sdk.ToolCall
if json.Unmarshal(b, &tcs) == nil {
sc.ToolCalls = tcs
}
}
}
if v, ok := m["tool_results"].([]interface{}); ok && len(v) > 0 {
if b, err := json.Marshal(v); err == nil {
var trs []sdk.ToolResult
if json.Unmarshal(b, &trs) == nil {
sc.ToolResults = trs
}
}
}
}
func parseStageScope(L *lua.LState) sdk.StageScope {
if L.GetTop() >= 3 && L.ToString(3) == "own_tools" {
return sdk.StageScopeOwnTools
}
return sdk.StageScopeGlobal
}
func parseToolDef(L *lua.LState, defTbl *lua.LTable, plg *luaPlugin, name string) sdk.ToolDef {
goDef := sdk.ToolDef{Name: name, Plugin: plg.name}
goDef.Description = defTbl.RawGetString("description").String()
if v := defTbl.RawGetString("no_memory"); v != nil {
goDef.NoMemory = lua.LVAsBool(v)
}
goDef.ContextPolicy = defTbl.RawGetString("context_policy").String()
if v := defTbl.RawGetString("cleaner"); v != nil && v.Type() == lua.LTFunction {
goDef.Cleaner = makeLuaCleaner(plg, v.(*lua.LFunction))
}
if params := defTbl.RawGetString("parameters"); params != nil {
if pt, ok := params.(*lua.LTable); ok {
goDef.Parameters = make(map[string]interface{})
pt.ForEach(func(k, v lua.LValue) {
goDef.Parameters[k.String()] = luaValueToGo(v)
})
}
}
return goDef
}
func parseChannelDef(L *lua.LState, defTbl *lua.LTable, plg *luaPlugin) sdk.ChannelDef {
chDef := sdk.ChannelDef{}
if v := defTbl.RawGetString("no_memory"); v != nil {
chDef.NoMemory = lua.LVAsBool(v)
}
chDef.ContextPolicy = defTbl.RawGetString("context_policy").String()
if v := defTbl.RawGetString("cleaner"); v != nil && v.Type() == lua.LTFunction {
chDef.Cleaner = makeLuaCleaner(plg, v.(*lua.LFunction))
}
return chDef
}
// parseInjectOptions 解析 Lua 侧 options table 为 SDK InjectOptions。
// 支持的键no_memory(bool)、context_policy(string)、cleaner_name(string)、priority(string)。
// 缺省/非表等价于零值(记入记忆 + 不裁剪),与旧的三参数注入完全等价。
func parseInjectOptions(L *lua.LState, idx int) sdk.InjectOptions {
opts := sdk.InjectOptions{}
tbl, ok := L.Get(idx).(*lua.LTable)
if !ok {
return opts
}
if v := tbl.RawGetString("no_memory"); v != nil {
opts.NoMemory = lua.LVAsBool(v)
}
opts.ContextPolicy = tbl.RawGetString("context_policy").String()
opts.CleanerName = tbl.RawGetString("cleaner_name").String()
opts.Priority = tbl.RawGetString("priority").String()
return opts
}
// luaToContentBlocks 把 Lua 的 blocks 数组解析为 SDK ContentBlock。
// 每项形如:
//
// { type = "text", text = "..." }
// { type = "image_url", image_url = { url = "...", detail = "high" } }
// { type = "audio_url", audio_url = { url = "..." } }
func luaToContentBlocks(L *lua.LState, idx int) []sdk.ContentBlock {
tbl, ok := L.Get(idx).(*lua.LTable)
if !ok {
return nil
}
var blocks []sdk.ContentBlock
tbl.ForEach(func(_, v lua.LValue) {
bt, ok := v.(*lua.LTable)
if !ok {
return
}
b := sdk.ContentBlock{
Type: bt.RawGetString("type").String(),
Text: bt.RawGetString("text").String(),
}
if iu, ok := bt.RawGetString("image_url").(*lua.LTable); ok {
b.ImageURL = &sdk.ImageURL{
URL: iu.RawGetString("url").String(),
Detail: iu.RawGetString("detail").String(),
}
}
if au, ok := bt.RawGetString("audio_url").(*lua.LTable); ok {
b.AudioURL = &sdk.AudioURL{URL: au.RawGetString("url").String()}
}
blocks = append(blocks, b)
})
return blocks
}
// luaToAttachments 把 Lua 附件数组解析为 SDK MediaAttachment。
// 每项:{ digest=, mime=, name=, data=<base64 字符串> }。
// 带 data 的是新内容(内核落进内容寻址存储),只带 digest 的是引用已有内容。
// base64 解码失败时忽略 data不整单失败——坏附件不应阻断一条记忆写入。
func luaToAttachments(L *lua.LState, val lua.LValue) []sdk.MediaAttachment {
tbl, ok := val.(*lua.LTable)
if !ok {
return nil
}
var out []sdk.MediaAttachment
tbl.ForEach(func(_, v lua.LValue) {
at, ok := v.(*lua.LTable)
if !ok {
return
}
a := sdk.MediaAttachment{
Digest: at.RawGetString("digest").String(),
MIME: at.RawGetString("mime").String(),
Name: at.RawGetString("name").String(),
}
if s := at.RawGetString("data").String(); s != "" {
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
a.Data = b
}
}
out = append(out, a)
})
return out
}
func docFromLua(tbl *lua.LTable) *sdk.Doc {
return &sdk.Doc{
ID: tbl.RawGetString("id").String(),
Title: tbl.RawGetString("title").String(),
Content: tbl.RawGetString("content").String(),
}
}
// jsonToIface 通过 JSON 往返把任意 Go 值转换为 JSON 兼容的 interface{} 树。
func jsonToIface(v interface{}) interface{} {
b, err := json.Marshal(v)
if err != nil {
return nil
}
var m interface{}
if err := json.Unmarshal(b, &m); err != nil {
return nil
}
return m
}
// jsonToLuaValue 通过 JSON 往返把任意 Go 值struct/slice/map转换为 Lua 值,
// 语义与 C ABI 外部插件跨边界 JSON 序列化一致。
func jsonToLuaValue(L *lua.LState, v interface{}) lua.LValue {
return goValueToLua(L, jsonToIface(v))
}
func makeLuaCleaner(plg *luaPlugin, fn *lua.LFunction) func(string) string {
return func(s string) string {
plg.mu.Lock()
defer plg.mu.Unlock()
L := plg.L
L.Push(fn)
L.Push(lua.LString(s))
if err := L.PCall(1, 1, nil); err != nil {
return s
}
result := L.Get(-1)
L.Pop(1)
if str, ok := result.(lua.LString); ok {
return string(str)
}
return s
}
}
func makeOutputHandler(plg *luaPlugin, fn *lua.LFunction) sdk.ToolHandler {
return func(args map[string]interface{}) (interface{}, error) {
plg.mu.Lock()
defer plg.mu.Unlock()
L := plg.L
L.Push(fn)
L.Push(goValueToLua(L, args))
if err := L.PCall(1, 1, nil); err != nil {
return nil, fmt.Errorf("lua output channel: %w", err)
}
result := L.Get(-1)
L.Pop(1)
return luaValueToGo(result), nil
}
}
func (p *luaPlugin) Name() string { return p.name }
func (p *luaPlugin) Start(s *sdk.PluginSDK) error {
// 1) 用真实 SDK 实现替换 sdk 表函数
sdkTbl := p.L.GetGlobal("sdk")
if sdkTable, ok := sdkTbl.(*lua.LTable); ok {
replaceSDKReal(p.L, sdkTable, p, s)
}
// 2) 批量注册加载期已暂存的 tool / stage / channel handler
p.mu.Lock()
tools := make(map[string]*toolReg, len(p.tools))
for k, v := range p.tools {
tools[k] = v
}
stages := make(map[sdk.Stage]*stageReg, len(p.stages))
for k, v := range p.stages {
stages[k] = v
}
outputChs := make(map[string]*outputChReg, len(p.outputChs))
for k, v := range p.outputChs {
outputChs[k] = v
}
inputDefs := make(map[string]sdk.ChannelDef, len(p.inputDefs))
for k, v := range p.inputDefs {
inputDefs[k] = v
}
p.mu.Unlock()
for toolName, reg := range tools {
h := makeToolHandler(p, toolName, reg.handler)
s.RegisterTool(toolName, reg.def, h)
}
for stage, reg := range stages {
h := makeStageHandler(p, stage, reg.handler)
s.RegisterStage(stage, h, reg.scope)
}
for chName, reg := range outputChs {
h := makeOutputHandler(p, reg.handler)
s.RegisterOutputChannel(chName, reg.caps, reg.desc, reg.def, h)
}
for chName, def := range inputDefs {
s.RegisterInputChannel(chName, def)
}
// 3) 调用插件的 start(sdk) 回调
if p.tbl != nil {
fn := p.tbl.RawGetString("start")
if fn != nil && fn != lua.LNil {
p.mu.Lock()
L := p.L
L.Push(fn)
L.Push(sdkTbl)
err := L.PCall(1, 0, nil)
p.mu.Unlock()
if err != nil {
return fmt.Errorf("lua start %s: %w", p.name, err)
}
}
}
return nil
}
func (p *luaPlugin) Stop() error {
// ① 先取消事件订阅。**不持 p.mu**Bus.Publish 持总线锁回调 handler
// 而 handler 要 p.mu若此处持 p.mu 再取总线锁,就是锁序反转死锁。
p.subsMu.Lock()
subs := p.subs
p.subs = nil
p.subsMu.Unlock()
for _, unsub := range subs {
unsub()
}
// ② 置 closed 并关 L。置位在持锁下完成已进入但等锁的 event 回调
// 拿到锁后会先看到 closed 而直接返回,不会碰已关的 L。
p.mu.Lock()
defer p.mu.Unlock()
p.closed = true
if p.tbl != nil {
fn := p.tbl.RawGetString("stop")
if fn != nil && fn != lua.LNil {
L := p.L
L.Push(fn)
if err := L.PCall(0, 0, nil); err != nil {
p.L.Close()
return fmt.Errorf("lua stop %s: %w", p.name, err)
}
}
}
p.L.Close()
return nil
}