feat: Phase 3 插件与工具系统 — Plugin SDK + Plugin Manager + 13内置插件 (40文件, 3293行)
- Plugin SDK: Plugin/Tool/ComplexTool/HostAPI 标准化接口 - Plugin Manager: 插件生命周期管理 (Install/Enable/Disable/Uninstall/Reload) - Tool Registry: 聚合工具注册表 (Register/Execute/Dispatch) - 13 个内置插件: 将原有硬编码工具迁移为标准插件格式 - REST API: 11 个端点 (net/http, 零外部依赖) - ai-core 集成: PluginManagerClient 替代本地工具调用 - plugin.json 元数据: 每个插件含完整 author/version/category/permissions Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
|
||||
)
|
||||
|
||||
// PluginManager manages the lifecycle of all plugins and their tools.
|
||||
type PluginManager struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[string]*pluginEntry // plugin name -> entry
|
||||
registry *ToolRegistry
|
||||
host sdk.HostAPI
|
||||
}
|
||||
|
||||
type pluginEntry struct {
|
||||
instance sdk.Plugin
|
||||
info sdk.PluginInfo
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewPluginManager(registry *ToolRegistry, host sdk.HostAPI) *PluginManager {
|
||||
return &PluginManager{
|
||||
plugins: make(map[string]*pluginEntry),
|
||||
registry: registry,
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// Install registers a plugin instance. For built-in plugins this is called at startup.
|
||||
func (m *PluginManager) Install(plugin sdk.Plugin) error {
|
||||
meta := plugin.Metadata()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.plugins[meta.Name]; exists {
|
||||
return fmt.Errorf("plugin %q is already installed", meta.Name)
|
||||
}
|
||||
|
||||
m.plugins[meta.Name] = &pluginEntry{
|
||||
instance: plugin,
|
||||
info: sdk.PluginInfo{
|
||||
Metadata: meta,
|
||||
Status: sdk.StatusInstalled,
|
||||
InstalledAt: time.Now(),
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable activates a plugin: Init → register tools → Start.
|
||||
func (m *PluginManager) Enable(ctx context.Context, pluginName string) error {
|
||||
m.mu.Lock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin %q not found", pluginName)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusLoaded
|
||||
m.mu.Unlock()
|
||||
|
||||
meta := entry.instance.Metadata()
|
||||
if err := entry.instance.Init(ctx, nil); err != nil {
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q init failed: %w", meta.Name, err)
|
||||
}
|
||||
|
||||
pluginCtx, cancel := context.WithCancel(context.Background())
|
||||
if err := entry.instance.Start(pluginCtx, m.host); err != nil {
|
||||
cancel()
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q start failed: %w", meta.Name, err)
|
||||
}
|
||||
|
||||
// Register all tools from this plugin.
|
||||
tools := entry.instance.Tools()
|
||||
toolIDs := make([]string, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
if err := m.registry.Register(t); err != nil {
|
||||
m.registry.UnregisterAll(toolIDs)
|
||||
cancel()
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q tool register failed: %w", meta.Name, err)
|
||||
}
|
||||
toolIDs = append(toolIDs, t.Definition().ID)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
entry.cancel = cancel
|
||||
entry.info.Status = sdk.StatusRunning
|
||||
entry.info.Enabled = true
|
||||
entry.info.Tools = toolIDs
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable stops a plugin and unregisters its tools.
|
||||
func (m *PluginManager) Disable(ctx context.Context, pluginName string) error {
|
||||
m.mu.Lock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin %q not found", pluginName)
|
||||
}
|
||||
|
||||
if err := entry.instance.Stop(ctx); err != nil {
|
||||
return fmt.Errorf("plugin %q stop failed: %w", pluginName, err)
|
||||
}
|
||||
if entry.cancel != nil {
|
||||
entry.cancel()
|
||||
}
|
||||
|
||||
m.registry.UnregisterAll(entry.info.Tools)
|
||||
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusDisabled
|
||||
entry.info.Enabled = false
|
||||
entry.info.Tools = nil
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall removes a plugin completely.
|
||||
func (m *PluginManager) Uninstall(ctx context.Context, pluginName string) error {
|
||||
if err := m.Disable(ctx, pluginName); err != nil {
|
||||
// If already disabled, continue.
|
||||
if entry, ok := m.plugins[pluginName]; !ok || entry.info.Status != sdk.StatusRunning {
|
||||
// not running, skip stop
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.plugins, pluginName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns info for all installed plugins.
|
||||
func (m *PluginManager) List() []sdk.PluginInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make([]sdk.PluginInfo, 0, len(m.plugins))
|
||||
for _, entry := range m.plugins {
|
||||
result = append(result, entry.info)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Get returns info for a single plugin.
|
||||
func (m *PluginManager) Get(pluginName string) (*sdk.PluginInfo, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
info := entry.info // copy
|
||||
return &info, true
|
||||
}
|
||||
|
||||
// Reload stops and re-starts a plugin.
|
||||
func (m *PluginManager) Reload(ctx context.Context, pluginName string) error {
|
||||
if err := m.Disable(ctx, pluginName); err != nil {
|
||||
return fmt.Errorf("reload disable: %w", err)
|
||||
}
|
||||
return m.Enable(ctx, pluginName)
|
||||
}
|
||||
|
||||
// EnableAll starts all installed plugins.
|
||||
func (m *PluginManager) EnableAll(ctx context.Context) []error {
|
||||
m.mu.RLock()
|
||||
names := make([]string, 0, len(m.plugins))
|
||||
for name := range m.plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if err := m.Enable(ctx, name); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", name, err))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Shutdown stops all running plugins gracefully.
|
||||
func (m *PluginManager) Shutdown(ctx context.Context) []error {
|
||||
m.mu.RLock()
|
||||
names := make([]string, 0, len(m.plugins))
|
||||
for name, entry := range m.plugins {
|
||||
if entry.info.Status == sdk.StatusRunning {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if err := m.Disable(ctx, name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Registry returns the aggregated tool registry.
|
||||
func (m *PluginManager) Registry() *ToolRegistry {
|
||||
return m.registry
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
|
||||
)
|
||||
|
||||
// ToolRegistry aggregates tool definitions from all running plugins and dispatches execution.
|
||||
type ToolRegistry struct {
|
||||
mu sync.RWMutex
|
||||
tools map[string]sdk.Tool // tool ID -> Tool
|
||||
}
|
||||
|
||||
func NewToolRegistry() *ToolRegistry {
|
||||
return &ToolRegistry{tools: make(map[string]sdk.Tool)}
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Register(tool sdk.Tool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
id := tool.Definition().ID
|
||||
if _, exists := r.tools[id]; exists {
|
||||
return fmt.Errorf("tool %q already registered", id)
|
||||
}
|
||||
r.tools[id] = tool
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Unregister(toolID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.tools, toolID)
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Get(toolID string) (sdk.Tool, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
t, ok := r.tools[toolID]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) List() []sdk.Tool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]sdk.Tool, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
result = append(result, t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Definitions() []sdk.ToolDefinition {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
defs := make([]sdk.ToolDefinition, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
defs = append(defs, t.Definition())
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Execute(ctx context.Context, toolID string, args map[string]interface{}) (*sdk.ToolResult, error) {
|
||||
r.mu.RLock()
|
||||
tool, ok := r.tools[toolID]
|
||||
r.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tool %q not found", toolID)
|
||||
}
|
||||
if err := tool.Validate(args); err != nil {
|
||||
return &sdk.ToolResult{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return tool.Execute(ctx, args)
|
||||
}
|
||||
|
||||
// UnregisterAll removes all tools matching a prefix (plugin's tools).
|
||||
func (r *ToolRegistry) UnregisterAll(toolIDs []string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, id := range toolIDs {
|
||||
delete(r.tools, id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user