Files
HomeAgent/internal/plugin/lua_plugin.go
HomeAgent Agent 6878f0126d 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 钉住不挂死。
2026-09-13 21:59:23 +08:00

1259 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
}))
// 同步注入(文本/媒体)在 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
}
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
}))
// ---- 同步注入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 {
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(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
}))
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
}