重构: 插件自注册 + .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:
root
2026-07-03 16:53:34 +08:00
parent 4442c9cea4
commit 2d314b3e9c
37 changed files with 3376 additions and 1576 deletions

View 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.soGo 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.luaLua 插件)。
// 返回 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)
}

View 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
View 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) {
// 优先尝试 .soGo plugin其次 .luaLua 脚本)
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
}