feat: restructure plugin system, add Lua plugin support, update docs

This commit is contained in:
JianFeeeee
2026-07-13 21:48:13 +08:00
parent e49bdca9ff
commit 950090959f
44 changed files with 3098 additions and 1097 deletions

View File

@ -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
}