PluginManager 支持运行时重载、配置驱动的启用/禁用、失败插件的自动恢复、健康检查以及聚合指标。
Reload(ctx, pluginID) 在不重启 xbot 的情况下从磁盘重新加载单个插件:
- 若插件处于激活状态则先停用(
StateDeactivating→Deactivate→StateInactive)。 - 释放绑定在旧插件上下文上的
OnConfigChanged订阅。 - 移除旧条目并注销其全部 widget(
widgetRegistry.UnregisterAll)。 - 重新扫描插件目录(
findPluginDir,覆盖DefaultPluginDirs+ 附加目录)并重新加载清单(LoadManifest)。 - 重建存储(
NewFileStorage;失败时回退到noopStorage)并使插件配置缓存失效。 - 构建全新的
PluginEntry(新的PluginContext、日志器、widget 注册表),并通过RuntimeFactory.Create重建运行时。 - 若清单声明了
onStart激活事件,则自动重新激活。 - 发出
PluginEventReloaded事件并写入AuditReload审计条目。
if err := pm.Reload(ctx, "xbot.genui"); err != nil {
// 清单 / 运行时 / 激活错误
}
ReloadAll(ctx) 停用所有插件、清空条目映射、从磁盘重新发现并重新激活:
- 期间抑制 widget 更新(
widgetRegistry.SuppressUpdates),避免淹没 WebSocket 推送缓冲区。 DeactivateAll(ctx)—— 注意这也会停止自动重试 goroutine。- 注销所有 widget,然后用全新映射替换条目映射。
Discover(ctx)+ActivateAll(ctx)。- 异步(在 goroutine 中)调用已注册的
OnReload回调,使慢监听器(如 WebSocket widget 推送)无法阻塞 RPC 处理器。
pm.OnReload(func() { /* ReloadAll 完成后执行 */ })
if err := pm.ReloadAll(ctx); err != nil { /* 发现/激活错误 */ }
WatchConfig(configPath, interval) 轮询 config.json 并对 plugins.disabled_plugins 的变化做出响应:
stop := pm.WatchConfig("/home/user/.xbot/config.json", 30*time.Second)
// ...
close(stop)
- 轮询间隔最小为 5 秒。
- 每个 tick 比较配置文件的修改时间;发生变化时重新读取文件,并将
plugins.disabled_plugins列表与上一份快照做 diff。 - 新禁用的插件会被停用(
StateDeactivating→Deactivate→StateInactive)并加入disabled集合。 - 新启用的插件会从禁用集合移除,然后就地重新激活(条目存在、状态为
StateInactive、声明了onStart)或从磁盘发现并激活。
SetAutoRetry(enabled, maxRetries) 为处于错误状态的插件启动后台重试循环:
pm.SetAutoRetry(true, 5) // 每个插件最多重试 5 次;0 = 无限次
- goroutine(
retryLoop)以retryInterval为周期运行(默认 5 秒;SetRetryInterval仅供测试使用,不用于生产)。 - 每个 tick,
retryErrorPlugins扫描所有条目;对错误状态且retryCount低于maxRetries的插件按指数退避重试:1s * 2^(attempt-1),上限 30 秒(retryInitialDelay/retryMaxDelay)。 - 重试将条目置为
StateDiscovered并调用activate。成功时重置重试计数与lastError,并发出PluginEventActivated(携带{"recovered": true, "attempt": n});失败时记录lastError/lastErrorAt,并通过notifyPluginError调用插件的错误回调。
重要:
DeactivateAll(以及ReloadAll)会停止自动重试 goroutine 并将autoRetry置为false。如果在DeactivateAll之后手动激活插件,需要再次调用SetAutoRetry以恢复自动恢复能力。
插件可以实现可选的 HealthChecker 接口:
type HealthChecker interface {
HealthCheck(ctx context.Context) error
}
results := pm.HealthCheck(ctx) // map[pluginID]error — nil 表示健康
只有 ACTIVE 状态的插件会被检查;未实现 HealthChecker 的插件被视为健康(nil 错误)。
Metrics() 返回插件系统的聚合计数:
type PluginMetrics struct {
TotalPlugins int `json:"total_plugins"`
ActivePlugins int `json:"active_plugins"`
TotalTools int `json:"total_tools"`
TotalHooks int `json:"total_hooks"`
TotalEnrichers int `json:"total_enrichers"`
ToolCallCount int64 `json:"tool_call_count"` // 运行期累计工具执行次数
HookCallCount int64 `json:"hook_call_count"` // 运行期累计 hook 分发次数
}
工具/hook 数量与调用计数只汇总 ACTIVE 插件的 PluginContext。String() 打印简洁的状态摘要:PluginManager{total=5, active=3, error=1, disabled=1}。