Files
AskaEth 5c807d76a0 Initial commit: Cyrene Plugins SDK + Plugin Manager
Extracted from Cyrene main repo (backend/pkg/plugins + backend/plugin-manager).
Contains SDK interfaces (Plugin/Tool/HostAPI), 13 built-in plugins,
ToolRegistry with call log ring buffer, and Plugin Manager REST API service.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 09:49:12 +08:00

227 lines
5.3 KiB
Go

package manager
import (
"context"
"fmt"
"sync"
"time"
"git.yeij.top/AskaEth/Cyrene-Plugins/sdk"
)
// PluginManager manages the lifecycle of all plugins and their tools.
type PluginManager struct {
mu sync.RWMutex
plugins map[string]*pluginEntry
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.
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)
}
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
}
// 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
return &info, true
}
// 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
}
// Uninstall removes a plugin completely.
func (m *PluginManager) Uninstall(ctx context.Context, pluginName string) error {
m.mu.RLock()
entry, ok := m.plugins[pluginName]
m.mu.RUnlock()
if !ok {
return fmt.Errorf("plugin %q not found", pluginName)
}
if entry.info.Status == sdk.StatusRunning {
if err := m.Disable(ctx, pluginName); err != nil {
return err
}
}
m.mu.Lock()
defer m.mu.Unlock()
delete(m.plugins, pluginName)
return nil
}
// 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)
}
// 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
}