mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat: restructure plugin system, add Lua plugin support, update docs
This commit is contained in:
347
internal/plugin/lua_plugin.go
Normal file
347
internal/plugin/lua_plugin.go
Normal file
@ -0,0 +1,347 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luaSDK "gitcode.com/JianFeeeee/HomeAgent/internal/lua/sdk"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
type toolReg struct {
|
||||
def sdk.ToolDef
|
||||
handler *lua.LFunction
|
||||
}
|
||||
|
||||
// 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]*lua.LFunction
|
||||
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]*lua.LFunction),
|
||||
}
|
||||
|
||||
// 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 := sdk.ToolDef{Name: toolName, Plugin: plg.name}
|
||||
goDef.Description = defTbl.RawGetString("description").String()
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
plg.mu.Lock()
|
||||
plg.stages[stage] = handler
|
||||
plg.mu.Unlock()
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_api", L.NewFunction(func(L *lua.LState) int {
|
||||
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 := sdk.ToolDef{Name: toolName, Plugin: plg.name}
|
||||
goDef.Description = defTbl.RawGetString("description").String()
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
h := makeStageHandler(plg, stage, handler)
|
||||
s.RegisterStage(stage, h)
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_api", L.NewFunction(func(L *lua.LState) int {
|
||||
apiName := L.CheckString(1)
|
||||
s.RegisterPluginAPI(apiName)
|
||||
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
|
||||
}))
|
||||
|
||||
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
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
L.Push(fn)
|
||||
L.Push(goValueToLua(L, map[string]interface{}{
|
||||
"raw_message": sc.RawMessage,
|
||||
"user_id": sc.UserID,
|
||||
"phase": string(sc.Phase),
|
||||
}))
|
||||
if err := L.PCall(1, 0, nil); err != nil {
|
||||
return fmt.Errorf("lua stage %s: %w", stage, err)
|
||||
}
|
||||
return 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 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]*lua.LFunction, len(p.stages))
|
||||
for k, v := range p.stages {
|
||||
stages[k] = v
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
for toolName, reg := range tools {
|
||||
h := makeToolHandler(p, toolName, reg.handler)
|
||||
s.RegisterTool(toolName, reg.def, h)
|
||||
}
|
||||
for stage, fn := range stages {
|
||||
h := makeStageHandler(p, stage, fn)
|
||||
s.RegisterStage(stage, h)
|
||||
}
|
||||
|
||||
// 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.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user