mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
重构: 插件自注册 + .so 动态加载 + 中断打断机制
- 所有内置插件 init() 自注册 (plugin.RegisterFactory), 移除 main.go 硬编码 - 新增 .so 动态加载器 (internal/plugin/dynamic.go), 插件可编译为 plugin.so - 新增 plugin.json 元数据 (internal/plugin/manifest.go) - 新增 interceptLoop 独立 goroutine: (a) cancelLLM() 取消进行中的 HTTP 请求 (b) interceptCh → drainInterrupt() 注入 [打断消息] 到 LLM 上下文 (c) InjectInput 空闲时触发新处理循环 - 新增 internal/plugins/all.go 空白导入触发所有内置插件 init() - internal/sdk/ 作为 PluginSDK 正式 Go API - internal/api/ → internal/plugins/webui/ 迁移 - 删除旧 cmd/cli/, 使用 cmd/waiter/ 替代 - 更新 PLAN.md / ARCHITECTURE.md / README.md 文档
This commit is contained in:
87
internal/plugin/dynamic.go
Normal file
87
internal/plugin/dynamic.go
Normal file
@ -0,0 +1,87 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// .so 插件必须导出函数 NewPlugin,签名与 NativeFactory 一致:
|
||||
//
|
||||
// func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
// return &myPlugin{name: name}, nil
|
||||
// }
|
||||
const (
|
||||
soEntry = "plugin.so"
|
||||
luaEntry = "main.lua"
|
||||
metaEntry = "plugin.json"
|
||||
)
|
||||
|
||||
type dynamicPlugin struct {
|
||||
name string
|
||||
impl sdk.Plugin
|
||||
}
|
||||
|
||||
func (p *dynamicPlugin) Name() string { return p.name }
|
||||
func (p *dynamicPlugin) Start(s *sdk.PluginSDK) error { return p.impl.Start(s) }
|
||||
func (p *dynamicPlugin) Stop() error { return p.impl.Stop() }
|
||||
|
||||
// readManifest 读取插件目录下的 plugin.json。文件不存在时不报错。
|
||||
func readManifest(dir string) *PluginManifest {
|
||||
data, err := os.ReadFile(filepath.Join(dir, metaEntry))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var m PluginManifest
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
// tryLoadSO 尝试从插件目录加载 plugin.so(Go plugin -buildmode=plugin)。
|
||||
// 返回 nil,nil 表示目录中没有 plugin.so。
|
||||
func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
soPath := filepath.Join(dir, soEntry)
|
||||
if _, err := os.Stat(soPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
p, err := plugin.Open(soPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin.Open %s: %w", soPath, err)
|
||||
}
|
||||
|
||||
sym, err := p.Lookup("NewPlugin")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(".so %s must export NewPlugin: %w", soPath, err)
|
||||
}
|
||||
|
||||
fn, ok := sym.(func(string, map[string]interface{}) (sdk.Plugin, error))
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("NewPlugin in %s has wrong signature", soPath)
|
||||
}
|
||||
|
||||
plg, err := fn(name, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewPlugin %s: %w", name, 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/manifest.go
Normal file
11
internal/plugin/manifest.go
Normal file
@ -0,0 +1,11 @@
|
||||
package plugin
|
||||
|
||||
// PluginManifest 每个插件目录中的 plugin.json 元数据。
|
||||
type PluginManifest struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Entry string `json:"entry,omitempty"` // "plugin.so" | "main.lua" | ""
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
303
internal/plugin/registry.go
Normal file
303
internal/plugin/registry.go
Normal file
@ -0,0 +1,303 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
)
|
||||
|
||||
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
|
||||
|
||||
// globalFactories 是插件通过 init() 自注册的全局工厂表。
|
||||
// Registry.RegisterNative() 写入此表;Registry.Load() 从中查找。
|
||||
var globalFactories sync.Map
|
||||
|
||||
// RegisterFactory 供插件包在 init() 中调用,实现自注册。
|
||||
// plugin.RegisterFactory("timer", func(name string, cfg map[string]interface{}) (sdk.Plugin, error) { ... })
|
||||
func RegisterFactory(name string, factory NativeFactory) {
|
||||
globalFactories.Store(name, factory)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[string]sdk.Plugin
|
||||
instances []sdk.Plugin
|
||||
factories map[string]NativeFactory
|
||||
|
||||
iom *agentIO.IOManager
|
||||
evBus *events.Bus
|
||||
memDB *memory.GraphDB
|
||||
textMem *text.Memory
|
||||
docStore *doc.Store
|
||||
ks *knowledge.Store
|
||||
mgr *agentAPI.ProviderManager
|
||||
cfgReg *internalConfig.ConfigRegistry
|
||||
plgDir string
|
||||
|
||||
regTool sdk.ToolRegistrar
|
||||
regStage sdk.StageRegistrar
|
||||
regAPI sdk.APIRegistrar
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
plugins: make(map[string]sdk.Plugin),
|
||||
factories: make(map[string]NativeFactory),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) SetIOManager(iom *agentIO.IOManager) { r.iom = iom }
|
||||
func (r *Registry) SetEventBus(evBus *events.Bus) { r.evBus = evBus }
|
||||
func (r *Registry) SetMemory(memDB *memory.GraphDB) { r.memDB = memDB }
|
||||
func (r *Registry) SetTextMemory(tm *text.Memory) { r.textMem = tm }
|
||||
func (r *Registry) SetDocStore(ds *doc.Store) { r.docStore = ds }
|
||||
func (r *Registry) SetKnowledge(ks *knowledge.Store) { r.ks = ks }
|
||||
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
|
||||
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
|
||||
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
|
||||
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
|
||||
func (r *Registry) SetStageRegistrar(fn sdk.StageRegistrar) { r.regStage = fn }
|
||||
func (r *Registry) SetAPIRegistrar(fn sdk.APIRegistrar) { r.regAPI = fn }
|
||||
|
||||
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.factories[name] = factory
|
||||
globalFactories.Store(name, factory)
|
||||
}
|
||||
|
||||
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||||
sett := sdk.NewSettings(name, r.cfgReg)
|
||||
|
||||
regTool := r.regTool
|
||||
if regTool == nil {
|
||||
regTool = func(toolName string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
regStage := r.regStage
|
||||
if regStage == nil {
|
||||
regStage = func(stage sdk.Stage, handler sdk.StageHandler) {}
|
||||
}
|
||||
regAPI := r.regAPI
|
||||
if regAPI == nil {
|
||||
regAPI = func(name string) error { return nil }
|
||||
}
|
||||
|
||||
return sdk.New(name,
|
||||
r.iom, r.evBus,
|
||||
sdk.NewGraphMemory(r.memDB),
|
||||
sdk.NewTextMemory(r.textMem),
|
||||
sdk.NewDocMemory(r.docStore),
|
||||
sdk.NewKnowledge(r.ks),
|
||||
sdk.NewLLM(r.mgr),
|
||||
sett,
|
||||
regTool, regStage, regAPI,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Registry) Load(dir string) error {
|
||||
if dir == "" {
|
||||
dir = r.plgDir
|
||||
}
|
||||
os.MkdirAll(dir, 0755)
|
||||
|
||||
// 1) 扫描已有目录
|
||||
loaded := map[string]bool{}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
os.MkdirAll(dir, 0755)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
plgDir := filepath.Join(dir, name)
|
||||
if r.loadOne(plgDir, name) {
|
||||
loaded[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 对已注册但尚无目录的工厂,创建目录并加载
|
||||
r.mu.RLock()
|
||||
allFactories := make(map[string]NativeFactory)
|
||||
for name, f := range r.factories {
|
||||
allFactories[name] = f
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
globalFactories.Range(func(key, val interface{}) bool {
|
||||
name := key.(string)
|
||||
if _, ok := allFactories[name]; !ok {
|
||||
allFactories[name] = val.(NativeFactory)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for name, factory := range allFactories {
|
||||
if loaded[name] {
|
||||
continue
|
||||
}
|
||||
plgDir := filepath.Join(dir, name)
|
||||
os.MkdirAll(plgDir, 0755)
|
||||
|
||||
cfg := r.readConfig(plgDir)
|
||||
p, err := factory(name, cfg)
|
||||
if err != nil {
|
||||
log.Printf("[plugin] factory %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
plgSDK := r.buildSDK(name)
|
||||
if err := p.Start(plgSDK); err != nil {
|
||||
log.Printf("[plugin] start %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.plugins[name] = p
|
||||
r.instances = append(r.instances, p)
|
||||
r.mu.Unlock()
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) loadOne(plgDir, name string) bool {
|
||||
// 1) 查找工厂(init 自注册或 RegisterNative)
|
||||
r.mu.RLock()
|
||||
factory, hasFactory := r.factories[name]
|
||||
r.mu.RUnlock()
|
||||
if !hasFactory {
|
||||
v, ok := globalFactories.Load(name)
|
||||
if ok {
|
||||
factory = v.(NativeFactory)
|
||||
hasFactory = true
|
||||
}
|
||||
}
|
||||
|
||||
var plg sdk.Plugin
|
||||
|
||||
if hasFactory {
|
||||
cfg := r.readConfig(plgDir)
|
||||
p, err := factory(name, cfg)
|
||||
if err != nil {
|
||||
log.Printf("[plugin] factory %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
plg = p
|
||||
} else {
|
||||
// 2) 无工厂,尝试动态加载 .so / .lua
|
||||
dynCfg := r.readConfig(plgDir)
|
||||
p, err := r.tryDynamic(plgDir, name, dynCfg)
|
||||
if err != nil {
|
||||
log.Printf("[plugin] dynamic %s: %v", name, err)
|
||||
}
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
plg = p
|
||||
}
|
||||
|
||||
plgSDK := r.buildSDK(name)
|
||||
if err := plg.Start(plgSDK); err != nil {
|
||||
log.Printf("[plugin] start %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.plugins[name] = plg
|
||||
r.instances = append(r.instances, plg)
|
||||
r.mu.Unlock()
|
||||
log.Printf("[plugin] loaded: %s", name)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Registry) StopAll() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, p := range r.instances {
|
||||
if err := p.Stop(); err != nil {
|
||||
log.Printf("[plugin] stop %s: %v", p.Name(), err)
|
||||
}
|
||||
}
|
||||
r.plugins = make(map[string]sdk.Plugin)
|
||||
r.instances = nil
|
||||
}
|
||||
|
||||
func (r *Registry) Reload(dir string) (string, error) {
|
||||
r.StopAll()
|
||||
if err := r.Load(dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("loaded %d plugins", len(r.instances)), nil
|
||||
}
|
||||
|
||||
func (r *Registry) List() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
list := make([]string, 0, len(r.plugins))
|
||||
for name := range r.plugins {
|
||||
list = append(list, name)
|
||||
}
|
||||
sort.Strings(list)
|
||||
return list
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) sdk.Plugin {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if plg != nil {
|
||||
return plg, nil
|
||||
}
|
||||
return tryLoadLua(plgDir, name, config)
|
||||
}
|
||||
|
||||
func (r *Registry) readConfig(plgDir string) map[string]interface{} {
|
||||
cfg := map[string]interface{}{}
|
||||
data, err := os.ReadFile(filepath.Join(plgDir, "skill.json"))
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
var meta map[string]interface{}
|
||||
if err := json.Unmarshal(data, &meta); err == nil {
|
||||
return meta
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
Reference in New Issue
Block a user