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:
263
internal/plugin/bridge_e2e_test.go
Normal file
263
internal/plugin/bridge_e2e_test.go
Normal file
@ -0,0 +1,263 @@
|
||||
//go:build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func TestBridgeE2E_WebPlugin(t *testing.T) {
|
||||
exeDir, _ := os.Executable()
|
||||
// Find web example build relative to the homeagent repo root
|
||||
haRoot := findHomeAgentRoot(t, exeDir)
|
||||
dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "web", "build", "plugin.dll")
|
||||
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
|
||||
t.Fatalf("web plugin DLL not found at %s\nRun: cd example/web && plugindev build --target windows/amd64", dllPath)
|
||||
}
|
||||
|
||||
// Track captured tools and stages
|
||||
var capturedTools []sdk.ToolDef
|
||||
var capturedStages []sdk.Stage
|
||||
|
||||
regTool := func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
capturedTools = append(capturedTools, def)
|
||||
t.Logf(" registered tool: %s", name)
|
||||
return nil
|
||||
}
|
||||
regStage := func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
capturedStages = append(capturedStages, stage)
|
||||
t.Logf(" registered stage: %s", stage)
|
||||
}
|
||||
regAPI := func(name string) error {
|
||||
t.Logf(" registered API: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("web", nil)
|
||||
psdk := sdk.New("web", nil, nil, nil, nil, nil, nil, nil, sett, regTool, regStage, regAPI)
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "web", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newDLLPlugin failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
// Start — this calls NewPlugin + StartPlugin + registerTools + registerStages
|
||||
if err := plg.Start(psdk); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify tools were captured
|
||||
if len(capturedTools) == 0 {
|
||||
t.Fatal("no tools were registered by web plugin")
|
||||
}
|
||||
t.Logf("Captured %d tools:", len(capturedTools))
|
||||
for _, d := range capturedTools {
|
||||
t.Logf(" - %s: %s", d.Name, d.Description[:min(len(d.Description), 60)])
|
||||
}
|
||||
|
||||
// Check specific expected tools
|
||||
webSearch, webFetch := false, false
|
||||
for _, d := range capturedTools {
|
||||
if d.Name == "web_search" {
|
||||
webSearch = true
|
||||
if d.Description == "" {
|
||||
t.Error("web_search has empty description")
|
||||
}
|
||||
params := d.Parameters
|
||||
if params == nil {
|
||||
t.Error("web_search has nil parameters")
|
||||
} else {
|
||||
if _, ok := params["properties"]; !ok {
|
||||
t.Error("web_search parameters missing 'properties'")
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.Name == "web_fetch" {
|
||||
webFetch = true
|
||||
}
|
||||
}
|
||||
if !webSearch {
|
||||
t.Error("expected tool 'web_search' not registered")
|
||||
}
|
||||
if !webFetch {
|
||||
t.Error("expected tool 'web_fetch' not registered")
|
||||
}
|
||||
|
||||
// Verify bridge exports work via direct C ABI calls
|
||||
t.Logf("Bridge exports: getTools=%x invokeTool=%x freeCStr=%x",
|
||||
plg.getTools, plg.invokeTool, plg.freeCStr)
|
||||
|
||||
// GetToolDefsJSON
|
||||
if plg.getTools != 0 {
|
||||
toolDefsJSON := callGetToolDefsJSON(t, plg)
|
||||
if len(toolDefsJSON) == 0 {
|
||||
t.Error("GetToolDefsJSON returned empty array, expected tools")
|
||||
}
|
||||
for _, d := range toolDefsJSON {
|
||||
t.Logf(" bridge tool: %s", d["name"])
|
||||
}
|
||||
}
|
||||
|
||||
// InvokeToolJSON — test with the search tool
|
||||
if plg.invokeTool != 0 {
|
||||
result := callInvokeToolJSON(t, plg, "web_search", map[string]interface{}{
|
||||
"query": "test",
|
||||
"count": 1,
|
||||
})
|
||||
t.Logf("InvokeToolJSON result keys: %v", keysOfMap(result))
|
||||
// Should get a result map (might be error if no network, but should not crash)
|
||||
if errStr, ok := result["error"]; ok {
|
||||
t.Logf(" (expected — tool returned error: %v)", errStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeE2E_SanitizerStages(t *testing.T) {
|
||||
exeDir, _ := os.Executable()
|
||||
haRoot := findHomeAgentRoot(t, exeDir)
|
||||
dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "sanitizer", "build", "plugin.dll")
|
||||
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
|
||||
t.Skip("sanitizer DLL not built")
|
||||
}
|
||||
|
||||
var capturedStages []sdk.Stage
|
||||
regStage := func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
capturedStages = append(capturedStages, stage)
|
||||
t.Logf(" registered stage: %s", stage)
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("sanitizer", nil)
|
||||
psdk := sdk.New("sanitizer", nil, nil, nil, nil, nil, nil, nil, sett,
|
||||
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { return nil },
|
||||
regStage,
|
||||
func(name string) error { return nil },
|
||||
)
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "sanitizer", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newDLLPlugin failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
if err := plg.Start(psdk); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
|
||||
if len(capturedStages) == 0 {
|
||||
t.Fatal("no stages registered by sanitizer")
|
||||
}
|
||||
found := false
|
||||
for _, s := range capturedStages {
|
||||
if s == sdk.StagePostAction {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected post_action stage, got %v", capturedStages)
|
||||
}
|
||||
|
||||
// Verify bridge GetStagesJSON
|
||||
if plg.getStages != 0 {
|
||||
ret, _, _ := syscall.SyscallN(plg.getStages, plg.handle)
|
||||
if ret != 0 {
|
||||
stagesJSON := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
t.Logf("GetStagesJSON: %s", stagesJSON)
|
||||
if !contains(t, stagesJSON, "post_action") {
|
||||
t.Error("GetStagesJSON missing post_action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func findHomeAgentRoot(t *testing.T, exeDir string) string {
|
||||
t.Helper()
|
||||
// Walk up from test binary directory looking for homeagent/
|
||||
dir := exeDir
|
||||
for i := 0; i < 10; i++ {
|
||||
if _, err := os.Stat(filepath.Join(dir, "internal", "plugin")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
t.Fatal("cannot find homeagent root")
|
||||
return ""
|
||||
}
|
||||
|
||||
func callGetToolDefsJSON(t *testing.T, plg *dllPlugin) []map[string]interface{} {
|
||||
t.Helper()
|
||||
ret, _, _ := syscall.SyscallN(plg.getTools, plg.handle)
|
||||
if ret == 0 {
|
||||
t.Fatal("GetToolDefsJSON returned nil")
|
||||
}
|
||||
jsonStr := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
var defs []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &defs); err != nil {
|
||||
t.Fatalf("GetToolDefsJSON parse error: %v", err)
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func callInvokeToolJSON(t *testing.T, plg *dllPlugin, toolName string, args map[string]interface{}) map[string]interface{} {
|
||||
t.Helper()
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
cToolName := append([]byte(toolName), 0)
|
||||
cArgs := append(argsJSON, 0)
|
||||
|
||||
ret, _, _ := syscall.SyscallN(
|
||||
plg.invokeTool,
|
||||
plg.handle,
|
||||
uintptr(unsafe.Pointer(&cToolName[0])),
|
||||
uintptr(unsafe.Pointer(&cArgs[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
t.Fatal("InvokeToolJSON returned nil")
|
||||
}
|
||||
jsonStr := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
t.Fatalf("InvokeToolJSON parse error: %v (json=%s)", err, jsonStr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func keysOfMap(m map[string]interface{}) []string {
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func contains(t *testing.T, s, substr string) bool {
|
||||
t.Helper()
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -21,6 +21,7 @@ import (
|
||||
// }
|
||||
const (
|
||||
soEntry = "plugin.so"
|
||||
dllEntry = "plugin.dll"
|
||||
luaEntry = "main.lua"
|
||||
metaEntry = "plugin.json"
|
||||
)
|
||||
@ -112,15 +113,3 @@ func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, err
|
||||
|
||||
return &dynamicPlugin{name: name, impl: plg}, nil
|
||||
}
|
||||
|
||||
// tryLoadLua 尝试从插件目录加载 main.lua(Lua 插件)。
|
||||
// 返回 nil,nil 表示目录中没有 main.lua。
|
||||
func tryLoadLua(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
luaPath := filepath.Join(dir, luaEntry)
|
||||
if _, err := os.Stat(luaPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 预留:Lua 插件需在 LuaVM 中注册一个 LuaPlugin 包装器
|
||||
return nil, fmt.Errorf("lua plugin loading not yet implemented: %s", name)
|
||||
}
|
||||
|
||||
11
internal/plugin/dynamic_dll_stub.go
Normal file
11
internal/plugin/dynamic_dll_stub.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build !windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func tryLoadDLL(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return nil, nil
|
||||
}
|
||||
32
internal/plugin/dynamic_dll_test.go
Normal file
32
internal/plugin/dynamic_dll_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
//go:build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTryLoadDLL_NoFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
plg, err := tryLoadDLL(dir, "nonexistent", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadDLL on empty dir should not error: %v", err)
|
||||
}
|
||||
if plg != nil {
|
||||
t.Fatal("expected nil for non-existent plugin.dll")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryLoadDLL_Invalid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(dir, "plugin.dll"), []byte("not a real dll"), 0644)
|
||||
|
||||
plg, err := tryLoadDLL(dir, "baddll", nil)
|
||||
t.Logf("plg=%v err=%v", plg, err)
|
||||
|
||||
if err == nil && plg == nil {
|
||||
t.Fatal("expected error or non-nil plugin for existing file")
|
||||
}
|
||||
}
|
||||
272
internal/plugin/dynamic_dll_windows.go
Normal file
272
internal/plugin/dynamic_dll_windows.go
Normal file
@ -0,0 +1,272 @@
|
||||
//go:build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// dllPlugin wraps a Windows DLL compiled with -buildmode=c-shared.
|
||||
//
|
||||
// Required exports:
|
||||
//
|
||||
// NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer → plugin handle
|
||||
// StartPlugin(handle unsafe.Pointer) C.int
|
||||
// StopPlugin(handle unsafe.Pointer) C.int
|
||||
// DestroyPlugin(handle unsafe.Pointer)
|
||||
//
|
||||
// Optional exports (tool registration):
|
||||
//
|
||||
// GetToolDefsJSON(handle unsafe.Pointer) *C.char → JSON array of tool defs
|
||||
// InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *C.char
|
||||
// FreeCString(s *C.char) → free C string from DLL
|
||||
// GetStagesJSON(handle unsafe.Pointer) *C.char → JSON array of stage names
|
||||
// InvokeStage(handle unsafe.Pointer, stage *C.char, contextJSON *C.char) C.int
|
||||
type dllPlugin struct {
|
||||
name string
|
||||
dll syscall.Handle
|
||||
handle uintptr
|
||||
sdk *sdk.PluginSDK
|
||||
|
||||
// cached proc addresses
|
||||
newPlugin uintptr
|
||||
startPlugin uintptr
|
||||
stopPlugin uintptr
|
||||
destroyPlugin uintptr
|
||||
getTools uintptr
|
||||
invokeTool uintptr
|
||||
freeCStr uintptr
|
||||
getStages uintptr
|
||||
invokeStage uintptr
|
||||
}
|
||||
|
||||
func findProc(dll syscall.Handle, name string) uintptr {
|
||||
addr, err := syscall.GetProcAddress(dll, name)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func newDLLPlugin(dllPath, name string, config map[string]interface{}) (*dllPlugin, error) {
|
||||
dll, err := syscall.LoadLibrary(dllPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadLibrary %s: %w", dllPath, err)
|
||||
}
|
||||
|
||||
np := findProc(dll, "NewPlugin")
|
||||
if np == 0 {
|
||||
_ = syscall.FreeLibrary(dll)
|
||||
return nil, fmt.Errorf("dll %s must export NewPlugin", name)
|
||||
}
|
||||
|
||||
return &dllPlugin{
|
||||
name: name,
|
||||
dll: dll,
|
||||
// required
|
||||
newPlugin: np,
|
||||
startPlugin: findProc(dll, "StartPlugin"),
|
||||
stopPlugin: findProc(dll, "StopPlugin"),
|
||||
destroyPlugin: findProc(dll, "DestroyPlugin"),
|
||||
// optional tool/stage API
|
||||
getTools: findProc(dll, "GetToolDefsJSON"),
|
||||
invokeTool: findProc(dll, "InvokeToolJSON"),
|
||||
freeCStr: findProc(dll, "FreeCString"),
|
||||
getStages: findProc(dll, "GetStagesJSON"),
|
||||
invokeStage: findProc(dll, "InvokeStage"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Name() string { return p.name }
|
||||
|
||||
func (p *dllPlugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
|
||||
cfgJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"name": p.name,
|
||||
"config": s.Settings().Dump(),
|
||||
})
|
||||
cName := append([]byte(p.name), 0)
|
||||
cConfig := append(cfgJSON, 0)
|
||||
|
||||
ret, _, _ := syscall.SyscallN(
|
||||
p.newPlugin,
|
||||
uintptr(unsafe.Pointer(&cName[0])),
|
||||
uintptr(unsafe.Pointer(&cConfig[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
_ = syscall.FreeLibrary(p.dll)
|
||||
return fmt.Errorf("dll NewPlugin %s returned nil", p.name)
|
||||
}
|
||||
p.handle = ret
|
||||
|
||||
if p.startPlugin != 0 {
|
||||
syscall.SyscallN(p.startPlugin, p.handle)
|
||||
}
|
||||
|
||||
// discover and register tools from DLL
|
||||
if p.getTools != 0 {
|
||||
if err := p.registerTools(s); err != nil {
|
||||
return fmt.Errorf("dll %s register tools: %w", p.name, err)
|
||||
}
|
||||
}
|
||||
if p.getStages != 0 {
|
||||
p.registerStages(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) Stop() error {
|
||||
if p.stopPlugin != 0 {
|
||||
syscall.SyscallN(p.stopPlugin, p.handle)
|
||||
}
|
||||
if p.destroyPlugin != 0 {
|
||||
syscall.SyscallN(p.destroyPlugin, p.handle)
|
||||
}
|
||||
_ = syscall.FreeLibrary(p.dll)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- tool registration via C ABI ---
|
||||
|
||||
type dllToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
func (p *dllPlugin) registerTools(s *sdk.PluginSDK) error {
|
||||
ret, _, _ := syscall.SyscallN(p.getTools, p.handle)
|
||||
if ret == 0 {
|
||||
return nil // no tools
|
||||
}
|
||||
defsJSON := cStringPtrToString(ret)
|
||||
if p.freeCStr != 0 {
|
||||
syscall.SyscallN(p.freeCStr, ret)
|
||||
}
|
||||
|
||||
var defs []dllToolDef
|
||||
if err := json.Unmarshal([]byte(defsJSON), &defs); err != nil {
|
||||
return fmt.Errorf("parse tool defs: %w", err)
|
||||
}
|
||||
for _, d := range defs {
|
||||
if d.Name == "" {
|
||||
continue
|
||||
}
|
||||
toolName := d.Name
|
||||
handler := p.makeToolHandler(toolName)
|
||||
s.RegisterTool(toolName, sdk.ToolDef{
|
||||
Name: toolName,
|
||||
Description: d.Description,
|
||||
Parameters: d.Parameters,
|
||||
Plugin: p.name,
|
||||
}, handler)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *dllPlugin) makeToolHandler(toolName string) sdk.ToolHandler {
|
||||
return func(args map[string]interface{}) (interface{}, error) {
|
||||
if p.invokeTool == 0 {
|
||||
return nil, fmt.Errorf("dll %s does not export InvokeToolJSON", p.name)
|
||||
}
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
cToolName := append([]byte(toolName), 0)
|
||||
cArgs := append(argsJSON, 0)
|
||||
|
||||
ret, _, _ := syscall.SyscallN(
|
||||
p.invokeTool,
|
||||
p.handle,
|
||||
uintptr(unsafe.Pointer(&cToolName[0])),
|
||||
uintptr(unsafe.Pointer(&cArgs[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("dll InvokeToolJSON %s returned nil", toolName)
|
||||
}
|
||||
resultJSON := cStringPtrToString(ret)
|
||||
if p.freeCStr != 0 {
|
||||
syscall.SyscallN(p.freeCStr, ret)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
|
||||
return nil, fmt.Errorf("dll tool %s result parse: %w", toolName, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dllPlugin) registerStages(s *sdk.PluginSDK) {
|
||||
ret, _, _ := syscall.SyscallN(p.getStages, p.handle)
|
||||
if ret == 0 {
|
||||
return
|
||||
}
|
||||
stagesJSON := cStringPtrToString(ret)
|
||||
if p.freeCStr != 0 {
|
||||
syscall.SyscallN(p.freeCStr, ret)
|
||||
}
|
||||
type stageEntry struct {
|
||||
Stage string `json:"stage"`
|
||||
}
|
||||
var entries []stageEntry
|
||||
if err := json.Unmarshal([]byte(stagesJSON), &entries); err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Stage == "" {
|
||||
continue
|
||||
}
|
||||
stageName := sdk.Stage(e.Stage)
|
||||
stage := stageName
|
||||
s.RegisterStage(stage, func(sc *sdk.StageContext) error {
|
||||
if p.invokeStage == 0 {
|
||||
return nil
|
||||
}
|
||||
ctxJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"raw_message": sc.RawMessage,
|
||||
"user_id": sc.UserID,
|
||||
"phase": string(sc.Phase),
|
||||
})
|
||||
cStage := append([]byte(stage), 0)
|
||||
cCtx := append(ctxJSON, 0)
|
||||
syscall.SyscallN(
|
||||
p.invokeStage,
|
||||
p.handle,
|
||||
uintptr(unsafe.Pointer(&cStage[0])),
|
||||
uintptr(unsafe.Pointer(&cCtx[0])),
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func cStringPtrToString(ptr uintptr) string {
|
||||
if ptr == 0 {
|
||||
return ""
|
||||
}
|
||||
var buf []byte
|
||||
for i := uintptr(0); ; i++ {
|
||||
b := *(*byte)(unsafe.Pointer(ptr + i))
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
buf = append(buf, b)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// tryLoadDLL 尝试从插件目录加载 plugin.dll。
|
||||
// 返回 nil,nil 表示目录中没有 plugin.dll。
|
||||
func tryLoadDLL(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
dllPath := filepath.Join(dir, dllEntry)
|
||||
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return newDLLPlugin(dllPath, name, config)
|
||||
}
|
||||
24
internal/plugin/dynamic_lua.go
Normal file
24
internal/plugin/dynamic_lua.go
Normal file
@ -0,0 +1,24 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// tryLoadLua 从插件目录加载 main.lua(Lua 插件)。
|
||||
// 返回 nil,nil 表示目录中没有 main.lua。
|
||||
func tryLoadLua(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
luaPath := filepath.Join(dir, luaEntry)
|
||||
if _, err := os.Stat(luaPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
plg, err := newLuaPlugin(luaPath, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lua plugin %s: %w", name, err)
|
||||
}
|
||||
return plg, nil
|
||||
}
|
||||
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
|
||||
}
|
||||
116
internal/plugin/lua_plugin_test.go
Normal file
116
internal/plugin/lua_plugin_test.go
Normal file
@ -0,0 +1,116 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTryLoadLua_Basic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{
|
||||
"name": "testlua",
|
||||
"name_zh": "测试Lua",
|
||||
"name_en": "Test Lua",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua"
|
||||
}`), 0644)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
local plugin = {
|
||||
name = "testlua"
|
||||
}
|
||||
|
||||
function plugin.start(sdk)
|
||||
sdk.log("info", "testlua started")
|
||||
sdk.register_tool("testlua_hello", {
|
||||
description = "Hello tool",
|
||||
parameters = {type = "object", properties = {}}
|
||||
}, function(args)
|
||||
return {content = "hello from lua"}
|
||||
end)
|
||||
end
|
||||
|
||||
function plugin.stop()
|
||||
sdk.log("info", "testlua stopped")
|
||||
end
|
||||
|
||||
return plugin
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "testlua", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
if plg == nil {
|
||||
t.Fatal("tryLoadLua returned nil")
|
||||
}
|
||||
if plg.Name() != "testlua" {
|
||||
t.Fatalf("unexpected name: %s", plg.Name())
|
||||
}
|
||||
t.Logf("plugin loaded: %s", plg.Name())
|
||||
}
|
||||
|
||||
func TestTryLoadLua_NoFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
plg, err := tryLoadLua(dir, "nonexistent", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua on empty dir should not error: %v", err)
|
||||
}
|
||||
if plg != nil {
|
||||
t.Fatal("expected nil for non-existent main.lua")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryLoadLua_NoReturnTable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"bad","entry":"main.lua"}`), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
-- just code, no return table
|
||||
local x = 1
|
||||
sdk.log("info", "no return table test")
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "bad", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
if plg == nil {
|
||||
t.Fatal("tryLoadLua returned nil")
|
||||
}
|
||||
t.Logf("loaded plugin without return table: %s", plg.Name())
|
||||
}
|
||||
|
||||
func TestTryLoadLua_GlobalSDK(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"globalsdk","entry":"main.lua"}`), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
-- sdk is a global, should work without return table
|
||||
sdk.log("info", "sdk is available as global")
|
||||
sdk.register_tool("direct_tool", {
|
||||
description = "registered directly in top-level code"
|
||||
}, function(args)
|
||||
return {result = "ok"}
|
||||
end)
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "globalsdk", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
if plg == nil {
|
||||
t.Fatal("tryLoadLua returned nil")
|
||||
}
|
||||
|
||||
lp := plg.(*luaPlugin)
|
||||
lp.mu.Lock()
|
||||
toolCount := len(lp.tools)
|
||||
lp.mu.Unlock()
|
||||
if toolCount != 1 {
|
||||
t.Fatalf("expected 1 tool registration, got %d", toolCount)
|
||||
}
|
||||
t.Logf("tool registered during load phase: OK")
|
||||
}
|
||||
60
internal/plugin/lua_util.go
Normal file
60
internal/plugin/lua_util.go
Normal file
@ -0,0 +1,60 @@
|
||||
package plugin
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
func luaValueToGo(lv lua.LValue) interface{} {
|
||||
switch v := lv.(type) {
|
||||
case lua.LString:
|
||||
return string(v)
|
||||
case lua.LNumber:
|
||||
return float64(v)
|
||||
case lua.LBool:
|
||||
return bool(v)
|
||||
case *lua.LTable:
|
||||
if v.MaxN() > 0 {
|
||||
arr := make([]interface{}, 0, v.MaxN())
|
||||
v.ForEach(func(_, val lua.LValue) {
|
||||
arr = append(arr, luaValueToGo(val))
|
||||
})
|
||||
return arr
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
v.ForEach(func(key, val lua.LValue) {
|
||||
m[key.String()] = luaValueToGo(val)
|
||||
})
|
||||
return m
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func goValueToLua(L *lua.LState, val interface{}) lua.LValue {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return lua.LString(v)
|
||||
case float64:
|
||||
return lua.LNumber(v)
|
||||
case int:
|
||||
return lua.LNumber(v)
|
||||
case int64:
|
||||
return lua.LNumber(v)
|
||||
case bool:
|
||||
return lua.LBool(v)
|
||||
case nil:
|
||||
return lua.LNil
|
||||
case []interface{}:
|
||||
tbl := L.NewTable()
|
||||
for i, item := range v {
|
||||
tbl.RawSetInt(i+1, goValueToLua(L, item))
|
||||
}
|
||||
return tbl
|
||||
case map[string]interface{}:
|
||||
tbl := L.NewTable()
|
||||
for k, item := range v {
|
||||
tbl.RawSetString(k, goValueToLua(L, item))
|
||||
}
|
||||
return tbl
|
||||
default:
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ const PackageExt = ".hmap"
|
||||
// PluginManifest 每个插件目录中的 plugin.json 元数据。
|
||||
type PluginManifest struct {
|
||||
Name string `json:"name"`
|
||||
NameZh string `json:"name_zh,omitempty"`
|
||||
NameEn string `json:"name_en,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
|
||||
@ -22,6 +22,28 @@ import (
|
||||
|
||||
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
|
||||
|
||||
// PluginMeta 插件显示名称元数据。
|
||||
type PluginMeta struct {
|
||||
NameZh string `json:"name_zh"`
|
||||
NameEn string `json:"name_en"`
|
||||
}
|
||||
|
||||
var globalPluginMeta sync.Map // name -> PluginMeta
|
||||
|
||||
// RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。
|
||||
func RegisterPluginMeta(name, nameZh, nameEn string) {
|
||||
globalPluginMeta.Store(name, PluginMeta{NameZh: nameZh, NameEn: nameEn})
|
||||
}
|
||||
|
||||
// GetPluginMeta 查询插件的显示名称。
|
||||
func GetPluginMeta(name string) (PluginMeta, bool) {
|
||||
v, ok := globalPluginMeta.Load(name)
|
||||
if !ok {
|
||||
return PluginMeta{}, false
|
||||
}
|
||||
return v.(PluginMeta), true
|
||||
}
|
||||
|
||||
// globalFactories 是插件通过 init() 自注册的全局工厂表。
|
||||
// Registry.RegisterNative() 写入此表;Registry.Load() 从中查找。
|
||||
var globalFactories sync.Map
|
||||
@ -202,6 +224,13 @@ func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
|
||||
var plg sdk.Plugin
|
||||
|
||||
// 读取 plugin.json 以获取插件显示名称元数据(主要用于外部插件)
|
||||
if mft := readManifest(plgDir); mft != nil {
|
||||
if mft.NameZh != "" || mft.NameEn != "" {
|
||||
RegisterPluginMeta(name, mft.NameZh, mft.NameEn)
|
||||
}
|
||||
}
|
||||
|
||||
if hasFactory {
|
||||
cfg := r.readConfig(plgDir)
|
||||
p, err := factory(name, cfg)
|
||||
@ -277,16 +306,34 @@ func (r *Registry) Get(name string) sdk.Plugin {
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
func (r *Registry) PluginMetas() map[string]PluginMeta {
|
||||
metas := make(map[string]PluginMeta)
|
||||
globalPluginMeta.Range(func(key, val interface{}) bool {
|
||||
metas[key.(string)] = val.(PluginMeta)
|
||||
return true
|
||||
})
|
||||
return metas
|
||||
}
|
||||
|
||||
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
// 优先尝试 .so(Go plugin),其次 .lua(Lua 脚本)
|
||||
plg, err := tryLoadSO(plgDir, name, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 尝试顺序:.so (Go plugin on Linux) → .dll (Windows) → .lua (跨平台)
|
||||
for _, try := range []struct {
|
||||
name string
|
||||
fn func(string, string, map[string]interface{}) (sdk.Plugin, error)
|
||||
}{
|
||||
{"so", tryLoadSO},
|
||||
{"dll", tryLoadDLL},
|
||||
{"lua", tryLoadLua},
|
||||
} {
|
||||
plg, err := try.fn(plgDir, name, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plg != nil {
|
||||
return plg, nil
|
||||
}
|
||||
}
|
||||
if plg != nil {
|
||||
return plg, nil
|
||||
}
|
||||
return tryLoadLua(plgDir, name, config)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *Registry) readConfig(plgDir string) map[string]interface{} {
|
||||
|
||||
Reference in New Issue
Block a user