Files
HomeAgent/internal/plugin/registry.go
JianFeeeee 11c1bbcebb plugin: 子进程通道接通 registry(proc 通道端到端可运行)
Part 3 收尾。tryLoadProc 从桩位变成真实加载路径,plugin.bin 插件现在
经 registry 完整跑起来:spawn → 握手(共享段 fd 3)→ init/start →
反向注册 → 工具调用 → stage 共享内存读改写。

registry 侧:
- Registry 持有 procHost(惰性创建,**全部 .bin 插件共用一块段**)。
  每插件一段会让「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成
  副本模型,lost update 原样复现(§8.4 实测 35.8~36.8%)。
- tryDynamic 分派到 Registry.loadProc;tryLoadProc 退为纯静态校验
  (构造需要 Host,只有 Registry 有)。
- StopAll 在锁外释放共享段:插件还持有映射时拆段,它们下一次访问就是
  SIGBUS;且持锁调用会与 onProcCrash 回调产生锁序风险。
- onProcCrash 把子进程退出转成 EventSystem 事件,不在回调里直接重载
  (重载需 registry 锁,而回调可能来自持锁路径的 goroutine)。

proc_core.go —— 权限梯度的类型系统落点(§3.8):
- procCore 用**命名字段**持有 *isdk.PluginSDK,不是嵌入。嵌入会提升全部
  方法,外部插件就能经类型断言拿到 Supervisor/Tracker/Adapter/Indexer/
  Status/Selftest。命名字段下只有显式写出的方法存在——权限梯度从
  「C ABI 表达能力的意外产物」变成显式声明并强制的策略。
- 能力访问器把内部超集接口收窄到公开面(isdk.KnowledgeAPI 内嵌
  pubsdk.KnowledgeAPI 再加 Stats/Remove,isdk.MemoryAPI 加 GraphData,
  isdk.LLMAPI 加 Chat/ReloadFromConfig);nil 保护避免类型化 nil 让
  corehandler 的判空失效。
- procPluginAdapter 转接 Start(*isdk.PluginSDK) → Start(proc.CoreSDK),
  Close 对 closeDynamic 可见故重载能真 kill 子进程(对比 dlclose 对
  Go c-shared 是 no-op,§1.1)。

共享段分配按平台拆分(原先 host.go 直接调 unix.MemfdCreate,darwin/windows
交叉编译失败):Linux memfd;macOS 立即 unlink 的临时文件(无 memfd_create,
但语义一致:无残留、fd 可经 ExtraFiles 传递、子进程 mmap 同一 inode);
其余平台明确报错而非静默降级成「无共享段」——那会让 stage 静默失去数据面。

测试 +13 项:
- e2e_template_test.go 用**真实 plugindev 模板**(而非 testdata 手写假插件)
  编译插件跑全链路,验证「模板 ↔ 内核」协议/布局真的对齐,不只是内核自己
  跟自己对齐。含 lifecycle.autoRestart 上报、工具调用、stage 读改写、
  FinalText 回传(C ABI 下 after_toolcall 看不到此字段,§8.3 10→16)、
  只读插件不覆盖改写插件。
- proc_load_test.go 验证 Host 唯一性/惰性、chmod +x 错误提示、
  Close 可见性,以及 procCore 不暴露内核内部机制的断言。

验证:go build ./... 通过;go test -race ./internal/plugin/... 全绿;
全仓 go test 无新增失败;git diff third_party/homeagent-sdk/sdk/ 为空。
既有告警 cabi/loader.go:156 unsafe.Pointer 非本次引入。

Ref: docs/zh/架构迁移评估.md §3.3/§3.4/§3.8、docs/zh/plugin-migration-plan.md Part 3
2026-09-02 13:03:57 +08:00

956 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin/proc"
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/plugin.bin/main.lua)的 SHA256
// 供增量重载(Reload)对比:仅重载有变更的插件,避免全量 StopAll+Load 导致重复加载。
pluginHashes map[string]string
// procHost 是**全部子进程插件共享的那一块** StageContext 段§3.3/§3.4)。
//
// 懒创建(首个 .bin 插件加载时),随内核存活。共享而非每插件一段是关键:
// 每插件一段会让「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成副本模型
// lost update 原样复现§8.4 实测 35.8~36.8%)。
procHostMu sync.Mutex
procHost *proc.Host
}
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()
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)
r.mu.Unlock()
// 共享段在全部子进程退出后再释放:插件还持有映射时拆段,
// 它们下一次访问就是 SIGBUS。在锁外调用Close 不需 registry 锁,
// 而持锁调它会与 onProcCrash 路径(子进程退出回调)产生锁序风险。
r.closeProcHost()
}
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 := r.loadProc(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
}