mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
Part 1 加载分派骨架(迁移可逐插件推进、随时回退的前提):
- dynamic.go: 新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry/detectEntryKind
manifest entry 优先级最高(改回 plugin.so 即回退 cabi);无 manifest 时按目录探测,.bin 优先
- registry.go: tryDynamic 按 entry 分派 proc/cabi 双通道;
entry 声明 .bin 但二进制缺失时报明确错误,不静默回退(否则'已迁移插件跑回旧通道'极难排查)
- registry.go: pluginEntryHash 候选顺序与 detectEntryKind 对齐(.bin 优先),
否则增量重载会用错文件算 hash
- dynamic_proc_{unix,windows}.go: tryLoadProc 桩位(权限/类型校验已实现,进程管理属 Part 2)
Part 4 共享内存数据面(迁移评估 §3.3/§3.4/§3.7,最关键一环):
- proc/shm.go: 段布局(Header + ShmStageCtx 描述符数组 + append-only arena)
相对偏移设计——各进程 mmap 到不同虚拟地址仍能正确解引用
arena 用尽显式报错而非静默截断(§4.4 风险登记);Compact() 回收 append-only 垃圾
- proc/shmcodec.go: StageContext 16 字段跨进程编解码
字段级描述符消除 lost update:只改 FinalText 的插件不触碰 ToolResults 描述符
WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
Snapshot 存序列化字符串(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过)
Extra 4 键提升为具名字段;Response 用标志位表达 nil vs 空串
- proc/lock.go: 锁仲裁回归内核(§3.7 已裁定,零 cgo)
ForceRelease 实现实验 9 的崩溃自愈——排除 robust pthread_mutex 必要性
重复加锁显式拒绝(否则死锁 30s);等待超时有补偿 goroutine 防锁泄漏
验证:
- proc 包 16 项测试全绿(含 -race):全字段往返/只读零写回/原地改切片识别/
现网 sanitizer+weather 场景/5插件×40轮并发零丢失/arena 耗尽报错/压实不破坏字段/
锁互斥·串扰拒绝·崩溃自愈·临界区串行化
- entry 分派 9 项测试全绿;go build ./... exit 0;接口冻结 git diff sdk/ 为空
942 lines
26 KiB
Go
942 lines
26 KiB
Go
package plugin
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
|
||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
|
||
"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"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
|
||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||
)
|
||
|
||
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
|
||
|
||
// PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta。
|
||
// 与内置 SDK 中的 PluginMeta 保持一致,供 PluginManager 接口返回。
|
||
type PluginMeta = sdk.PluginMeta
|
||
|
||
var globalPluginMeta sync.Map // name -> PluginMeta
|
||
|
||
// RegisterPluginMeta 供插件包在 init() 中调用,注册显示名称。
|
||
func RegisterPluginMeta(name, nameZh, nameEn string) {
|
||
globalPluginMeta.Store(name, sdk.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
|
||
|
||
// 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)
|
||
}
|
||
|
||
// PluginToolCleaner 定义插件工具注销接口,由 StageHost 实现。
|
||
type PluginToolCleaner interface {
|
||
UnregisterPluginTools(pluginName string)
|
||
}
|
||
|
||
type Registry struct {
|
||
mu sync.RWMutex
|
||
plugins map[string]sdk.Plugin
|
||
instances []sdk.Plugin
|
||
factories map[string]NativeFactory
|
||
|
||
pluginAutoRestart map[string]bool
|
||
sdkRefs map[string]*sdk.PluginSDK
|
||
|
||
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
|
||
dataDir string // 守护进程数据目录(注入给插件 SettingsAPI.DataDir)
|
||
lua *luaVM.VM
|
||
baseKey string
|
||
|
||
regTool sdk.ToolRegistrar
|
||
regStage sdk.StageRegistrar
|
||
regAPI sdk.APIRegistrar
|
||
|
||
toolCleaner PluginToolCleaner
|
||
|
||
status sdk.StatusAPI
|
||
sup sdk.SupervisorAPI
|
||
trk *tracker.Tracker
|
||
cfg *types.Config
|
||
stageHost sdk.ToolSource
|
||
idx *memory.Indexer
|
||
|
||
knownDisabled map[string]bool
|
||
allowlist map[string]bool
|
||
|
||
// pluginHashes 记录各插件二进制(plugin.so/main.lua)的 SHA256,
|
||
// 供增量重载(Reload)对比:仅重载有变更的插件,避免全量 StopAll+Load 导致重复加载。
|
||
pluginHashes map[string]string
|
||
}
|
||
|
||
func NewRegistry() *Registry {
|
||
return &Registry{
|
||
plugins: make(map[string]sdk.Plugin),
|
||
factories: make(map[string]NativeFactory),
|
||
pluginAutoRestart: make(map[string]bool),
|
||
sdkRefs: make(map[string]*sdk.PluginSDK),
|
||
knownDisabled: make(map[string]bool),
|
||
pluginHashes: make(map[string]string),
|
||
}
|
||
}
|
||
|
||
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) SetDataDir(dir string) { r.dataDir = dir }
|
||
func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm }
|
||
func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key }
|
||
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) SetToolCleaner(tc PluginToolCleaner) { r.toolCleaner = tc }
|
||
func (r *Registry) SetStatusProvider(sp sdk.StatusAPI) { r.status = sp }
|
||
func (r *Registry) SetSupervisor(sup sdk.SupervisorAPI) { r.sup = sup }
|
||
func (r *Registry) SetTracker(trk *tracker.Tracker) { r.trk = trk }
|
||
func (r *Registry) SetConfig(cfg *types.Config) { r.cfg = cfg }
|
||
func (r *Registry) SetStageHost(sh sdk.ToolSource) { r.stageHost = sh }
|
||
func (r *Registry) SetIndexer(idx *memory.Indexer) { r.idx = idx }
|
||
|
||
// SetLoadAllowlist 限制 Load 仅装载指定插件名(failback 受限启动用)。
|
||
// 空/未设置 = 装载全部。违反白名单的插件(含已注册工厂)一律跳过。
|
||
func (r *Registry) SetLoadAllowlist(names []string) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
r.allowlist = nil
|
||
if len(names) == 0 {
|
||
return
|
||
}
|
||
r.allowlist = make(map[string]bool, len(names))
|
||
for _, n := range names {
|
||
r.allowlist[strings.TrimSpace(n)] = true
|
||
}
|
||
}
|
||
|
||
func (r *Registry) allowlistAllows(name string) bool {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
if r.allowlist == nil {
|
||
return true
|
||
}
|
||
return r.allowlist[name]
|
||
}
|
||
|
||
func (r *Registry) RegisterNative(name string, factory NativeFactory) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
r.factories[name] = factory
|
||
globalFactories.Store(name, factory)
|
||
}
|
||
|
||
type channelDevice struct {
|
||
name string
|
||
desc string
|
||
caps agentIO.OutputCapability
|
||
handler sdk.ToolHandler
|
||
chDef agentIO.ChannelDef
|
||
}
|
||
|
||
func (d *channelDevice) Name() string { return d.name }
|
||
func (d *channelDevice) Type() agentIO.DeviceType { return agentIO.DeviceOutput }
|
||
func (d *channelDevice) Description() string { return d.desc }
|
||
func (d *channelDevice) Start() error { return nil }
|
||
func (d *channelDevice) Stop() error { return nil }
|
||
func (d *channelDevice) OutputCapabilities() agentIO.OutputCapability { return d.caps }
|
||
func (d *channelDevice) Tools() []agentIO.ToolDef { return nil }
|
||
func (d *channelDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||
return d.handler(args)
|
||
}
|
||
func (d *channelDevice) ChannelDef() agentIO.ChannelDef { return d.chDef }
|
||
|
||
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||
sett := sdk.NewSettings(name, r.cfgReg)
|
||
if sd, ok := sett.(interface{ SetDataDir(string) }); ok {
|
||
// 插件专属数据目录:<data>/plugin_data/<name>,保证存在
|
||
dir := filepath.Join(r.dataDir, "plugin_data", name)
|
||
os.MkdirAll(dir, 0755)
|
||
sd.SetDataDir(dir)
|
||
}
|
||
|
||
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 }
|
||
}
|
||
|
||
regOutput := func(chName string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error {
|
||
if r.iom == nil {
|
||
return nil
|
||
}
|
||
return r.iom.RegisterDevice(&channelDevice{
|
||
name: chName,
|
||
caps: agentIO.OutputCapability(caps),
|
||
desc: desc,
|
||
handler: handler,
|
||
chDef: agentIO.ChannelDef(def),
|
||
})
|
||
}
|
||
|
||
regInput := func(name string, def sdk.ChannelDef) error {
|
||
if r.iom == nil {
|
||
return nil
|
||
}
|
||
r.iom.RegisterInputChannel(name, agentIO.ChannelDef(def))
|
||
return nil
|
||
}
|
||
|
||
return sdk.New(name, sdk.SDKConfig{
|
||
IOManager: r.iom,
|
||
EventBus: r.evBus,
|
||
Memory: sdk.NewGraphMemory(r.memDB),
|
||
TextMemory: sdk.NewTextMemory(r.textMem),
|
||
DocMemory: sdk.NewDocMemory(r.docStore),
|
||
Knowledge: sdk.NewKnowledge(r.ks),
|
||
LLM: sdk.NewLLM(r.mgr, r.cfgReg, r.lua, r.baseKey),
|
||
Settings: sett,
|
||
RegTool: regTool,
|
||
RegStage: regStage,
|
||
RegAPI: regAPI,
|
||
RegOutput: regOutput,
|
||
RegInput: regInput,
|
||
PluginMgr: r,
|
||
|
||
Status: r.status,
|
||
Supervisor: r.sup,
|
||
Adapter: sdk.NewAdapter(r.lua),
|
||
Tracker: r.trk,
|
||
Config: sdk.NewConfig(r.cfg),
|
||
Tool: sdk.NewTool(r.stageHost, r.iom),
|
||
Indexer: sdk.NewIndexer(r.idx),
|
||
})
|
||
}
|
||
|
||
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()
|
||
if !r.allowlistAllows(name) {
|
||
continue
|
||
}
|
||
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
|
||
}
|
||
if !r.allowlistAllows(name) {
|
||
continue
|
||
}
|
||
if r.isDisabled(name) {
|
||
log.Printf("[plugin] %s is disabled, skipping", name)
|
||
r.mu.Lock()
|
||
r.knownDisabled[name] = true
|
||
r.mu.Unlock()
|
||
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.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||
r.instances = append(r.instances, p)
|
||
r.mu.Unlock()
|
||
log.Printf("[plugin] loaded: %s", name)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (r *Registry) isDisabled(name string) bool {
|
||
if r.cfgReg == nil {
|
||
return false
|
||
}
|
||
return r.cfgReg.IsPluginDisabled(name)
|
||
}
|
||
|
||
// pluginEntryHash 计算插件入口文件的 SHA256,用于增量重载对比。
|
||
// 无入口文件(内置纯工厂插件)返回空字符串(始终视为已加载)。
|
||
// plugin.bin 排在最前:与 detectEntryKind 保持一致的优先级,迁移期间同目录
|
||
// 两种产物共存时以子进程产物为准。
|
||
func pluginEntryHash(plgDir string) string {
|
||
for _, candidate := range []string{binEntry, soEntry, dllEntry, "plugin.dylib", luaEntry, skillEntry} {
|
||
path := filepath.Join(plgDir, candidate)
|
||
if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
|
||
sum := sha256.Sum256(data)
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (r *Registry) loadOne(plgDir, name string) bool {
|
||
if r.isDisabled(name) {
|
||
log.Printf("[plugin] %s is disabled, skipping", name)
|
||
r.mu.Lock()
|
||
r.knownDisabled[name] = true
|
||
r.mu.Unlock()
|
||
return false
|
||
}
|
||
|
||
// 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
|
||
|
||
// 读取 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)
|
||
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.pluginAutoRestart[name] = plgSDK.AutoRestart()
|
||
r.sdkRefs[name] = plgSDK
|
||
r.instances = append(r.instances, plg)
|
||
if h := pluginEntryHash(plgDir); h != "" {
|
||
r.pluginHashes[name] = h
|
||
} else {
|
||
delete(r.pluginHashes, name)
|
||
}
|
||
r.mu.Unlock()
|
||
log.Printf("[plugin] loaded: %s", name)
|
||
return true
|
||
}
|
||
|
||
// runStopHandlers 执行插件注册的停止清理回调(SDK 层),须在调用插件 Stop() 之前执行。
|
||
func (r *Registry) runStopHandlers(name string) {
|
||
if sdk, ok := r.sdkRefs[name]; ok {
|
||
sdk.RunStopHandlers()
|
||
}
|
||
}
|
||
|
||
// runOnRemoveHandlers 执行插件注册的删除清理回调(SDK 层),插件 Stop() 之后、从注册表移除前执行。
|
||
func (r *Registry) runOnRemoveHandlers(name string) {
|
||
if sdk, ok := r.sdkRefs[name]; ok {
|
||
sdk.RunOnRemoveHandlers()
|
||
}
|
||
}
|
||
|
||
func (r *Registry) StopAll() {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
for _, p := range r.instances {
|
||
r.runStopHandlers(p.Name())
|
||
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
|
||
r.pluginAutoRestart = make(map[string]bool)
|
||
r.sdkRefs = make(map[string]*sdk.PluginSDK)
|
||
}
|
||
|
||
func (r *Registry) Reload(dir string) (string, error) {
|
||
if dir == "" {
|
||
dir = r.plgDir
|
||
}
|
||
// 增量重载:扫描插件目录,对比入口文件 hash,仅 Stop+重载有变更的插件。
|
||
// 未变更插件保持运行,避免 plgreload 触发全量 StopAll+Load 导致所有插件重复加载
|
||
// 及内置插件(如 healthcheck)状态机错乱。
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
changed := 0
|
||
remaining := 0
|
||
for _, entry := range entries {
|
||
if !entry.IsDir() {
|
||
continue
|
||
}
|
||
name := entry.Name()
|
||
if !r.allowlistAllows(name) {
|
||
continue
|
||
}
|
||
plgDir := filepath.Join(dir, name)
|
||
h := pluginEntryHash(plgDir)
|
||
r.mu.RLock()
|
||
old := r.pluginHashes[name]
|
||
loaded := r.plugins[name] != nil
|
||
r.mu.RUnlock()
|
||
// 无入口文件(纯内置工厂插件)始终视为已加载;
|
||
// 有变更或首次出现且未加载 → 需要重载。
|
||
if !loaded {
|
||
if r.loadOne(plgDir, name) {
|
||
changed++
|
||
}
|
||
continue
|
||
}
|
||
if h == "" {
|
||
remaining++
|
||
continue
|
||
}
|
||
if old != h {
|
||
if err := r.ReloadOne(name); err != nil {
|
||
log.Printf("[plugin] reload %s: %v", name, err)
|
||
} else {
|
||
changed++
|
||
}
|
||
} else {
|
||
remaining++
|
||
}
|
||
}
|
||
return fmt.Sprintf("reloaded %d plugins, %d unchanged", changed, remaining), nil
|
||
}
|
||
|
||
func (r *Registry) ReloadOne(name string) error {
|
||
plgDir := filepath.Join(r.plgDir, name)
|
||
|
||
r.mu.Lock()
|
||
var removed sdk.Plugin
|
||
if p, ok := r.plugins[name]; ok {
|
||
r.runStopHandlers(name)
|
||
if err := p.Stop(); err != nil {
|
||
log.Printf("[plugin] stop %s for reload: %v", name, err)
|
||
}
|
||
delete(r.plugins, name)
|
||
delete(r.sdkRefs, name)
|
||
for i, inst := range r.instances {
|
||
if inst.Name() == name {
|
||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
removed = p
|
||
}
|
||
r.mu.Unlock()
|
||
|
||
r.closeDynamic(removed)
|
||
|
||
ok := r.loadOne(plgDir, name)
|
||
if !ok {
|
||
return fmt.Errorf("reload plugin %s failed", name)
|
||
}
|
||
log.Printf("[plugin] reloaded: %s", name)
|
||
return nil
|
||
}
|
||
|
||
// closeDynamic 释放动态加载插件的共享库句柄(dlclose)。
|
||
// Linux dlopen 对同一路径返回已加载的旧句柄,若不释放,插件二进制更新后
|
||
// 重载/卸载仍会执行旧代码。Go plugin.Open 路径(dynamicPlugin)不可卸载,跳过。
|
||
func (r *Registry) closeDynamic(p sdk.Plugin) {
|
||
if p == nil {
|
||
return
|
||
}
|
||
if c, ok := p.(interface{ Close() error }); ok {
|
||
if err := c.Close(); err != nil {
|
||
log.Printf("[plugin] close dynamic %s: %v", p.Name(), err)
|
||
}
|
||
}
|
||
}
|
||
|
||
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) AutoRestartEnabled(name string) bool {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
enabled, ok := r.pluginAutoRestart[name]
|
||
if !ok {
|
||
return true
|
||
}
|
||
return enabled
|
||
}
|
||
|
||
func (r *Registry) IsDisabled(name string) bool {
|
||
return r.isDisabled(name)
|
||
}
|
||
|
||
func (r *Registry) Enable(name string) error {
|
||
if r.cfgReg != nil {
|
||
r.cfgReg.RemoveDisabledPlugin(name)
|
||
}
|
||
r.mu.Lock()
|
||
delete(r.knownDisabled, name)
|
||
r.mu.Unlock()
|
||
plgDir := filepath.Join(r.plgDir, name)
|
||
if r.loadOne(plgDir, name) {
|
||
log.Printf("[plugin] enabled: %s", name)
|
||
return nil
|
||
}
|
||
return fmt.Errorf("enable plugin %s failed", name)
|
||
}
|
||
|
||
func (r *Registry) Disable(name string) error {
|
||
r.mu.Lock()
|
||
p, ok := r.plugins[name]
|
||
if ok {
|
||
r.runStopHandlers(name)
|
||
if err := p.Stop(); err != nil {
|
||
log.Printf("[plugin] stop %s for disable: %v", name, err)
|
||
}
|
||
delete(r.plugins, name)
|
||
delete(r.sdkRefs, name)
|
||
for i, inst := range r.instances {
|
||
if inst.Name() == name {
|
||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
r.knownDisabled[name] = true
|
||
r.mu.Unlock()
|
||
|
||
if r.toolCleaner != nil {
|
||
r.toolCleaner.UnregisterPluginTools(name)
|
||
}
|
||
|
||
if r.cfgReg != nil {
|
||
r.cfgReg.AddDisabledPlugin(name, "system")
|
||
}
|
||
log.Printf("[plugin] disabled: %s", name)
|
||
return nil
|
||
}
|
||
|
||
// ---- PluginManager interface ----
|
||
|
||
// PluginManager interface
|
||
|
||
// IsBuiltinPlugin 判断插件是否为内置插件(有编译期工厂,由 init() 注册)。
|
||
// 内置插件只能禁用/启用,不能卸载。
|
||
func (r *Registry) IsBuiltinPlugin(name string) bool {
|
||
if r == nil {
|
||
return false
|
||
}
|
||
r.mu.RLock()
|
||
_, ok := r.factories[name]
|
||
r.mu.RUnlock()
|
||
if ok {
|
||
return true
|
||
}
|
||
_, ok = globalFactories.Load(name)
|
||
return ok
|
||
}
|
||
|
||
// pluginInstalled 判断插件是否已安装(可用于禁用/启用等操作前的存在性校验)。
|
||
// 命中任一即视为已安装:
|
||
// 1. 已加载(plugins map 中)
|
||
// 2. 已注册工厂(内置插件,通过 init() 自注册,无需物理目录)
|
||
// 3. 插件目录 plgDir/<name> 存在(外部插件的安装目录)
|
||
func (r *Registry) pluginInstalled(name string) bool {
|
||
if r == nil || name == "" {
|
||
return false
|
||
}
|
||
r.mu.RLock()
|
||
_, loaded := r.plugins[name]
|
||
_, isFactory := r.factories[name]
|
||
r.mu.RUnlock()
|
||
if loaded || isFactory {
|
||
return true
|
||
}
|
||
if _, ok := globalFactories.Load(name); ok {
|
||
return true
|
||
}
|
||
if r.plgDir != "" {
|
||
if fi, err := os.Stat(filepath.Join(r.plgDir, name)); err == nil && fi.IsDir() {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (r *Registry) ListLoadedPlugins() []string { return r.List() }
|
||
|
||
func (r *Registry) ListDisabledPlugins() []sdk.DisabledPluginInfo {
|
||
if r.cfgReg == nil {
|
||
return nil
|
||
}
|
||
list, err := r.cfgReg.ListDisabledPlugins()
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
result := make([]sdk.DisabledPluginInfo, len(list))
|
||
for i, v := range list {
|
||
result[i] = sdk.DisabledPluginInfo(v)
|
||
}
|
||
return result
|
||
}
|
||
|
||
func (r *Registry) IsPluginDisabled(name string) bool { return r.isDisabled(name) }
|
||
|
||
func (r *Registry) DisablePlugin(name, by string) error {
|
||
// 插件不存在(未安装):拒绝并返回错误,避免把不存在的插件写进 disabled_plugins。
|
||
// 判断标准:已加载 / 已注册工厂(内置)/ 插件目录存在,任一命中视为已安装。
|
||
if !r.pluginInstalled(name) {
|
||
return fmt.Errorf("plugin %s not installed", name)
|
||
}
|
||
// Check not disabling self if running
|
||
if r.cfgReg != nil {
|
||
// If already disabled, no-op
|
||
if r.cfgReg.IsPluginDisabled(name) {
|
||
return fmt.Errorf("plugin %s already disabled", name)
|
||
}
|
||
}
|
||
|
||
r.mu.Lock()
|
||
p, ok := r.plugins[name]
|
||
if ok {
|
||
r.runStopHandlers(name)
|
||
if err := p.Stop(); err != nil {
|
||
log.Printf("[plugin] stop %s for disable: %v", name, err)
|
||
}
|
||
delete(r.plugins, name)
|
||
delete(r.sdkRefs, name)
|
||
for i, inst := range r.instances {
|
||
if inst.Name() == name {
|
||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
r.knownDisabled[name] = true
|
||
r.mu.Unlock()
|
||
|
||
if r.toolCleaner != nil {
|
||
r.toolCleaner.UnregisterPluginTools(name)
|
||
}
|
||
|
||
if r.cfgReg != nil {
|
||
r.cfgReg.AddDisabledPlugin(name, by)
|
||
}
|
||
log.Printf("[plugin] disabled: %s (by %s)", name, by)
|
||
return nil
|
||
}
|
||
|
||
func (r *Registry) EnablePlugin(name string) error { return r.Enable(name) }
|
||
|
||
// StopAndUnload 停止并从注册表移除插件,但保留其配置表(config_<name>)。
|
||
// 供插件更新/升级流程使用:换 so/文件不动配置,重装后配置原样生效。
|
||
// 不执行 onRemove 回调(那是删除专用语义)。目录由调用方管理。
|
||
func (r *Registry) StopAndUnload(name string) error {
|
||
r.mu.Lock()
|
||
var unloaded sdk.Plugin
|
||
p, ok := r.plugins[name]
|
||
if ok {
|
||
r.runStopHandlers(name)
|
||
if err := p.Stop(); err != nil {
|
||
log.Printf("[plugin] stop %s for unload: %v", name, err)
|
||
}
|
||
delete(r.plugins, name)
|
||
delete(r.sdkRefs, name)
|
||
for i, inst := range r.instances {
|
||
if inst.Name() == name {
|
||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
unloaded = p
|
||
}
|
||
r.mu.Unlock()
|
||
|
||
if r.toolCleaner != nil {
|
||
r.toolCleaner.UnregisterPluginTools(name)
|
||
}
|
||
r.closeDynamic(unloaded)
|
||
log.Printf("[plugin] unloaded (config kept): %s", name)
|
||
return nil
|
||
}
|
||
|
||
// RemovePlugin 卸载插件:先停止(stop handlers + Stop),再执行插件注册的 onRemove
|
||
// 回调(删除专用,重载不触发),最后从注册表移除并清理禁用/工具注册/配置。
|
||
// 插件目录的物理删除由调用方(pluginmgr)负责。更新场景请用 StopAndUnload。
|
||
func (r *Registry) RemovePlugin(name string) error {
|
||
r.mu.Lock()
|
||
var removed sdk.Plugin
|
||
p, ok := r.plugins[name]
|
||
if ok {
|
||
r.runStopHandlers(name)
|
||
if err := p.Stop(); err != nil {
|
||
log.Printf("[plugin] stop %s for remove: %v", name, err)
|
||
}
|
||
delete(r.plugins, name)
|
||
delete(r.sdkRefs, name)
|
||
for i, inst := range r.instances {
|
||
if inst.Name() == name {
|
||
r.instances = append(r.instances[:i], r.instances[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
removed = p
|
||
}
|
||
r.runOnRemoveHandlers(name)
|
||
r.mu.Unlock()
|
||
|
||
if r.toolCleaner != nil {
|
||
r.toolCleaner.UnregisterPluginTools(name)
|
||
}
|
||
if r.cfgReg != nil {
|
||
r.cfgReg.RemoveDisabledPlugin(name)
|
||
r.cfgReg.RemovePlugin(name)
|
||
}
|
||
r.closeDynamic(removed)
|
||
log.Printf("[plugin] removed: %s", name)
|
||
return nil
|
||
}
|
||
|
||
func (r *Registry) ReloadPlugins() (string, error) { return r.Reload(r.plgDir) }
|
||
|
||
// ListKnown 返回所有已知插件(已加载 + 已禁用 + 已安装但未加载)。
|
||
func (r *Registry) ListKnown() []string {
|
||
r.mu.RLock()
|
||
known := make(map[string]bool)
|
||
for name := range r.plugins {
|
||
known[name] = true
|
||
}
|
||
for name := range r.knownDisabled {
|
||
known[name] = true
|
||
}
|
||
r.mu.RUnlock()
|
||
|
||
if r.plgDir != "" {
|
||
entries, _ := os.ReadDir(r.plgDir)
|
||
for _, e := range entries {
|
||
if e.IsDir() {
|
||
known[e.Name()] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
r.mu.RLock()
|
||
for name := range r.factories {
|
||
known[name] = true
|
||
}
|
||
r.mu.RUnlock()
|
||
|
||
globalFactories.Range(func(key, val interface{}) bool {
|
||
known[key.(string)] = true
|
||
return true
|
||
})
|
||
|
||
list := make([]string, 0, len(known))
|
||
for name := range known {
|
||
list = append(list, name)
|
||
}
|
||
sort.Strings(list)
|
||
return list
|
||
}
|
||
|
||
func (r *Registry) PluginMetas() map[string]sdk.PluginMeta {
|
||
metas := make(map[string]sdk.PluginMeta)
|
||
globalPluginMeta.Range(func(key, val interface{}) bool {
|
||
metas[key.(string)] = val.(sdk.PluginMeta)
|
||
return true
|
||
})
|
||
return metas
|
||
}
|
||
|
||
func (r *Registry) PluginDir() string {
|
||
return r.plgDir
|
||
}
|
||
|
||
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
// 按 manifest entry 分派到对应加载通道(外部插件多进程化:.so/.dll 与 .bin 双通道共存)。
|
||
// 这使迁移可逐插件推进、随时回退——把 entry 改回 plugin.so 即回到旧通道。
|
||
if detectEntryKind(plgDir) == entryProc {
|
||
plg, err := tryLoadProc(plgDir, name, config)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if plg != nil {
|
||
log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name)
|
||
return plg, nil
|
||
}
|
||
// entry 声明了 plugin.bin 但文件不存在/不可用 → 不隐式回退到 cabi,
|
||
// 否则"已迁移插件静默跑回旧通道"极难排查。
|
||
return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry)
|
||
}
|
||
|
||
// 既有探测顺序(保持不变):.so → .dll → .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
|
||
}
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
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
|
||
}
|