mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
feat: restructure plugin system, add Lua plugin support, update docs
This commit is contained in:
@ -4,6 +4,7 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@ -14,49 +15,37 @@ import (
|
||||
//go:embed adapters/*.lua
|
||||
var bundledAdapters embed.FS
|
||||
|
||||
type VM struct {
|
||||
mu sync.Mutex
|
||||
state *lua.LState
|
||||
adapterDir string
|
||||
loaded map[string]*lua.LTable
|
||||
}
|
||||
|
||||
type APIAdapter struct {
|
||||
Name string
|
||||
Version string
|
||||
Script string
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (v *VM) AdapterDir() string {
|
||||
return v.adapterDir
|
||||
// AdapterCache 预加载容器:启动时编译全部脚本到内存,运行时只读缓存,无文件 I/O
|
||||
type AdapterCache struct {
|
||||
mu sync.RWMutex
|
||||
state *lua.LState
|
||||
items map[string]*lua.LTable
|
||||
}
|
||||
|
||||
func NewVM(adapterDir string) *VM {
|
||||
return &VM{
|
||||
adapterDir: adapterDir,
|
||||
loaded: make(map[string]*lua.LTable),
|
||||
func newAdapterCache() *AdapterCache {
|
||||
return &AdapterCache{
|
||||
state: lua.NewState(),
|
||||
items: make(map[string]*lua.LTable),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VM) Start() error {
|
||||
os.MkdirAll(v.adapterDir, 0755)
|
||||
|
||||
if err := v.writeBundledAdapters(); err != nil {
|
||||
return fmt.Errorf("write bundled adapters: %w", err)
|
||||
}
|
||||
|
||||
v.state = lua.NewState()
|
||||
|
||||
v.state.SetGlobal("log", v.state.NewFunction(func(L *lua.LState) int {
|
||||
func (c *AdapterCache) setupGlobals() {
|
||||
s := c.state
|
||||
s.SetGlobal("log", s.NewFunction(func(L *lua.LState) int {
|
||||
level := L.ToString(1)
|
||||
msg := L.ToString(2)
|
||||
fmt.Printf("[lua/%s] %s\n", level, msg)
|
||||
return 0
|
||||
}))
|
||||
|
||||
jsonTable := v.state.NewTable()
|
||||
v.state.SetGlobal("json", jsonTable)
|
||||
v.state.SetField(jsonTable, "encode", v.state.NewFunction(func(L *lua.LState) int {
|
||||
jsonTable := s.NewTable()
|
||||
s.SetGlobal("json", jsonTable)
|
||||
s.SetField(jsonTable, "encode", s.NewFunction(func(L *lua.LState) int {
|
||||
val := L.CheckAny(1)
|
||||
goVal := luaValueToGo(val)
|
||||
b, err := json.Marshal(goVal)
|
||||
@ -67,7 +56,7 @@ func (v *VM) Start() error {
|
||||
L.Push(lua.LString(string(b)))
|
||||
return 1
|
||||
}))
|
||||
v.state.SetField(jsonTable, "decode", v.state.NewFunction(func(L *lua.LState) int {
|
||||
s.SetField(jsonTable, "decode", s.NewFunction(func(L *lua.LState) int {
|
||||
str := L.CheckString(1)
|
||||
var val interface{}
|
||||
if err := json.Unmarshal([]byte(str), &val); err != nil {
|
||||
@ -78,33 +67,120 @@ func (v *VM) Start() error {
|
||||
return 1
|
||||
}))
|
||||
|
||||
v.state.SetGlobal("http_get", v.state.NewFunction(func(L *lua.LState) int {
|
||||
s.SetGlobal("http_get", s.NewFunction(func(L *lua.LState) int {
|
||||
url := L.ToString(1)
|
||||
L.Push(lua.LString(fmt.Sprintf(`{"url":%q,"status":200,"body":"mock"}`, url)))
|
||||
return 1
|
||||
}))
|
||||
|
||||
v.state.SetGlobal("http_post", v.state.NewFunction(func(L *lua.LState) int {
|
||||
s.SetGlobal("http_post", s.NewFunction(func(L *lua.LState) int {
|
||||
url := L.ToString(1)
|
||||
body := L.ToString(2)
|
||||
L.Push(lua.LString(fmt.Sprintf(`{"url":%q,"body":%q,"status":200}`, url, body)))
|
||||
return 1
|
||||
}))
|
||||
}
|
||||
|
||||
if err := v.loadAdapters(); err != nil {
|
||||
return fmt.Errorf("load adapters: %w", err)
|
||||
// Preload 编译单个 Lua 适配器脚本并注入缓存
|
||||
func (c *AdapterCache) Preload(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read adapter: %w", err)
|
||||
}
|
||||
return c.PreloadSource(filepath.Base(path), string(data))
|
||||
}
|
||||
|
||||
// PreloadSource 从源码字符串编译适配器并注入缓存
|
||||
func (c *AdapterCache) PreloadSource(name, code string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if err := c.state.DoString(code); err != nil {
|
||||
return fmt.Errorf("compile adapter: %w", err)
|
||||
}
|
||||
|
||||
tbl, ok := c.state.Get(-1).(*lua.LTable)
|
||||
c.state.Pop(1)
|
||||
if !ok {
|
||||
return fmt.Errorf("adapter script must return a table")
|
||||
}
|
||||
|
||||
if n := tbl.RawGetString("name"); n != nil && n.String() != "" {
|
||||
name = n.String()
|
||||
}
|
||||
|
||||
c.items[name] = tbl
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VM) Stop() {
|
||||
if v.state != nil {
|
||||
v.state.Close()
|
||||
// Get 运行时从缓存读取已编译的适配器表(无文件 I/O)
|
||||
func (c *AdapterCache) Get(name string) *lua.LTable {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.items[name]
|
||||
}
|
||||
|
||||
// Remove 从缓存移除适配器
|
||||
func (c *AdapterCache) Remove(name string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, name)
|
||||
}
|
||||
|
||||
// List 返回缓存中所有适配器摘要
|
||||
func (c *AdapterCache) List() []APIAdapter {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
list := make([]APIAdapter, 0, len(c.items))
|
||||
for name, tbl := range c.items {
|
||||
a := APIAdapter{Name: name}
|
||||
if v := tbl.RawGetString("version"); v != nil {
|
||||
a.Version = v.String()
|
||||
}
|
||||
list = append(list, a)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Close 释放 Lua 状态
|
||||
func (c *AdapterCache) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.state != nil {
|
||||
c.state.Close()
|
||||
c.state = nil
|
||||
}
|
||||
c.items = nil
|
||||
}
|
||||
|
||||
// VM 运行时虚拟机,封装 AdapterCache 提供适配器调用
|
||||
type VM struct {
|
||||
mu sync.Mutex
|
||||
cache *AdapterCache
|
||||
adapterDir string
|
||||
}
|
||||
|
||||
func NewVM(adapterDir string) *VM {
|
||||
return &VM{
|
||||
adapterDir: adapterDir,
|
||||
cache: newAdapterCache(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VM) loadAdapters() error {
|
||||
func (v *VM) AdapterDir() string { return v.adapterDir }
|
||||
func (v *VM) Cache() *AdapterCache { return v.cache }
|
||||
|
||||
func (v *VM) Start() error {
|
||||
if err := os.MkdirAll(v.adapterDir, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir adapter dir: %w", err)
|
||||
}
|
||||
if err := v.writeBundledAdapters(); err != nil {
|
||||
return fmt.Errorf("write bundled adapters: %w", err)
|
||||
}
|
||||
|
||||
v.cache.setupGlobals()
|
||||
|
||||
// 预加载:扫描适配器目录,全部编译到缓存
|
||||
entries, err := os.ReadDir(v.adapterDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@ -112,134 +188,108 @@ func (v *VM) loadAdapters() error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if filepath.Ext(entry.Name()) != ".lua" {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(v.adapterDir, entry.Name())
|
||||
if err := v.LoadAdapter(path); err != nil {
|
||||
fmt.Printf("[lua] load %s: %v\n", entry.Name(), err)
|
||||
if err := v.cache.Preload(path); err != nil {
|
||||
fmt.Printf("[lua] preload %s: %v\n", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VM) Stop() {
|
||||
v.cache.Close()
|
||||
}
|
||||
|
||||
// LoadAdapter 对外接口:从文件加载并编译适配器到缓存(运行时安全,不影响其他适配器)
|
||||
func (v *VM) LoadAdapter(path string) error {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
return v.cache.Preload(path)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read adapter: %w", err)
|
||||
}
|
||||
// RemoveAdapter 对外接口:从缓存移除适配器(运行时安全)
|
||||
func (v *VM) RemoveAdapter(name string) {
|
||||
v.cache.Remove(name)
|
||||
}
|
||||
|
||||
script := string(data)
|
||||
|
||||
if err := v.state.DoString(script); err != nil {
|
||||
return fmt.Errorf("execute adapter script: %w", err)
|
||||
}
|
||||
|
||||
adapterTable := v.state.Get(-1)
|
||||
v.state.Pop(1)
|
||||
|
||||
tbl, ok := adapterTable.(*lua.LTable)
|
||||
if !ok {
|
||||
return fmt.Errorf("adapter script must return a table")
|
||||
}
|
||||
|
||||
name := ""
|
||||
if nameVal := tbl.RawGetString("name"); nameVal != nil {
|
||||
name = nameVal.String()
|
||||
}
|
||||
if name == "" {
|
||||
name = filepath.Base(path)
|
||||
}
|
||||
|
||||
v.loaded[name] = tbl
|
||||
fmt.Printf("[lua] loaded adapter: %s\n", name)
|
||||
return nil
|
||||
func (v *VM) ListAdapters() []APIAdapter {
|
||||
return v.cache.List()
|
||||
}
|
||||
|
||||
func (v *VM) CallTransformRequest(name, rawJSON string) (string, error) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapter, ok := v.loaded[name]
|
||||
if !ok {
|
||||
adapter := v.cache.Get(name)
|
||||
if adapter == nil {
|
||||
return "", fmt.Errorf("adapter %s not loaded", name)
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
fn := adapter.RawGetString("transform_request")
|
||||
if fn == nil {
|
||||
return "", fmt.Errorf("adapter %s missing transform_request", name)
|
||||
}
|
||||
|
||||
v.state.Push(fn)
|
||||
v.state.Push(lua.LString(rawJSON))
|
||||
|
||||
if err := v.state.PCall(1, 1, nil); err != nil {
|
||||
state := v.cache.state
|
||||
state.Push(fn)
|
||||
state.Push(lua.LString(rawJSON))
|
||||
if err := state.PCall(1, 1, nil); err != nil {
|
||||
return "", fmt.Errorf("transform_request: %w", err)
|
||||
}
|
||||
|
||||
result := v.state.Get(-1)
|
||||
v.state.Pop(1)
|
||||
|
||||
result := state.Get(-1)
|
||||
state.Pop(1)
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (v *VM) CallTransformResponse(name, rawJSON string) (string, error) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapter, ok := v.loaded[name]
|
||||
if !ok {
|
||||
adapter := v.cache.Get(name)
|
||||
if adapter == nil {
|
||||
return rawJSON, nil
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
fn := adapter.RawGetString("transform_response")
|
||||
if fn == nil {
|
||||
return rawJSON, nil
|
||||
}
|
||||
|
||||
v.state.Push(fn)
|
||||
v.state.Push(lua.LString(rawJSON))
|
||||
|
||||
if err := v.state.PCall(1, 1, nil); err != nil {
|
||||
state := v.cache.state
|
||||
state.Push(fn)
|
||||
state.Push(lua.LString(rawJSON))
|
||||
if err := state.PCall(1, 1, nil); err != nil {
|
||||
return "", fmt.Errorf("transform_response: %w", err)
|
||||
}
|
||||
|
||||
result := v.state.Get(-1)
|
||||
v.state.Pop(1)
|
||||
|
||||
result := state.Get(-1)
|
||||
state.Pop(1)
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (v *VM) CallTransformStreamChunk(name, rawLine string) (string, error) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapter, ok := v.loaded[name]
|
||||
if !ok {
|
||||
adapter := v.cache.Get(name)
|
||||
if adapter == nil {
|
||||
return rawLine, nil
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
fn := adapter.RawGetString("transform_stream_chunk")
|
||||
if fn == nil {
|
||||
return rawLine, nil
|
||||
}
|
||||
|
||||
v.state.Push(fn)
|
||||
v.state.Push(lua.LString(rawLine))
|
||||
|
||||
if err := v.state.PCall(1, 1, nil); err != nil {
|
||||
state := v.cache.state
|
||||
state.Push(fn)
|
||||
state.Push(lua.LString(rawLine))
|
||||
if err := state.PCall(1, 1, nil); err != nil {
|
||||
return "", fmt.Errorf("transform_stream_chunk: %w", err)
|
||||
}
|
||||
|
||||
result := v.state.Get(-1)
|
||||
v.state.Pop(1)
|
||||
|
||||
result := state.Get(-1)
|
||||
state.Pop(1)
|
||||
if result.String() == "" {
|
||||
return "", nil
|
||||
}
|
||||
@ -247,14 +297,10 @@ func (v *VM) CallTransformStreamChunk(name, rawLine string) (string, error) {
|
||||
}
|
||||
|
||||
func (v *VM) GetAdapterEndpoint(name string) string {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapter, ok := v.loaded[name]
|
||||
if !ok {
|
||||
adapter := v.cache.Get(name)
|
||||
if adapter == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if ep := adapter.RawGetString("endpoint"); ep != nil {
|
||||
return ep.String()
|
||||
}
|
||||
@ -262,14 +308,10 @@ func (v *VM) GetAdapterEndpoint(name string) string {
|
||||
}
|
||||
|
||||
func (v *VM) GetAdapterHeaders(name string) map[string]string {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapter, ok := v.loaded[name]
|
||||
if !ok {
|
||||
adapter := v.cache.Get(name)
|
||||
if adapter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
if ht := adapter.RawGetString("headers"); ht != nil {
|
||||
if tbl, ok := ht.(*lua.LTable); ok {
|
||||
@ -281,32 +323,45 @@ func (v *VM) GetAdapterHeaders(name string) map[string]string {
|
||||
return headers
|
||||
}
|
||||
|
||||
func (v *VM) ListAdapters() []APIAdapter {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
adapters := make([]APIAdapter, 0)
|
||||
for name, tbl := range v.loaded {
|
||||
adapter := APIAdapter{Name: name}
|
||||
if v := tbl.RawGetString("version"); v != nil {
|
||||
adapter.Version = v.String()
|
||||
func (v *VM) writeBundledAdapters() error {
|
||||
// Try multiple paths for compatibility
|
||||
tryPaths := []string{"adapters", ".", "lua/adapters"}
|
||||
var entries []fs.DirEntry
|
||||
var err error
|
||||
for _, p := range tryPaths {
|
||||
entries, err = bundledAdapters.ReadDir(p)
|
||||
if err == nil && len(entries) > 0 {
|
||||
break
|
||||
}
|
||||
adapters = append(adapters, adapter)
|
||||
}
|
||||
return adapters
|
||||
}
|
||||
|
||||
func (v *VM) ReloadAll() error {
|
||||
v.mu.Lock()
|
||||
v.loaded = make(map[string]*lua.LTable)
|
||||
v.mu.Unlock()
|
||||
|
||||
if v.state != nil {
|
||||
v.state.Close()
|
||||
if err != nil || len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
v.state = lua.NewState()
|
||||
|
||||
return v.Start()
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if filepath.Ext(entry.Name()) != ".lua" {
|
||||
continue
|
||||
}
|
||||
dstPath := filepath.Join(v.adapterDir, entry.Name())
|
||||
if _, err := os.Stat(dstPath); err == nil {
|
||||
continue
|
||||
}
|
||||
data, err := bundledAdapters.ReadFile(filepath.Join("adapters", entry.Name()))
|
||||
if err != nil {
|
||||
// try alternative paths
|
||||
data, err = bundledAdapters.ReadFile(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", entry.Name(), err)
|
||||
}
|
||||
fmt.Printf("[lua] installed bundled adapter: %s\n", entry.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func luaValueToGo(lv lua.LValue) interface{} {
|
||||
@ -365,33 +420,3 @@ func goValueToLua(L *lua.LState, val interface{}) lua.LValue {
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VM) writeBundledAdapters() error {
|
||||
entries, err := bundledAdapters.ReadDir("adapters")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
dstPath := filepath.Join(v.adapterDir, entry.Name())
|
||||
if _, err := os.Stat(dstPath); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := bundledAdapters.ReadFile(filepath.Join("adapters", entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", entry.Name(), err)
|
||||
}
|
||||
fmt.Printf("[lua] installed bundled adapter: %s\n", entry.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user