feat: 插件-工具合并 — 创建 pkg/plugins 共享模块并移除 tool-engine

- 新增 backend/pkg/plugins/ 共享模块:SDK 接口、PluginManager、ToolRegistry(含环形缓冲区调用日志)
- 13 个通用插件从 plugin-manager 迁移至共享模块(import 路径统一)
- ai-core 切换至共享 ToolRegistry,进程内执行(零网络开销),包装 6 个专属工具
- plugin-manager 迁移至共享模块,保留管理 REST API
- 新增 DevTools 插件管理面板(侧边栏 → 🔌 插件管理)
- 移除 tool-engine 服务(从 go.work、DevTools 配置、编译系统)
- 工具调用记录 API 从 Tool-Engine 迁至 AI-Core(/api/v1/tools/calls)
- ai-core ContextStore 启动时从 PostgreSQL 恢复会话历史
- 清理所有过时引用和备份文件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 20:52:39 +08:00
parent 5325eaca3f
commit 673ff752c5
78 changed files with 1313 additions and 5187 deletions
+226
View File
@@ -0,0 +1,226 @@
package manager
import (
"context"
"fmt"
"sync"
"time"
"github.com/yourname/cyrene-ai/pkg/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
}
+288
View File
@@ -0,0 +1,288 @@
package manager
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/yourname/cyrene-ai/pkg/plugins/sdk"
)
// CallLogRecord 工具调用记录
type CallLogRecord struct {
CallID string `json:"call_id"`
ToolName string `json:"tool_name"`
Arguments string `json:"arguments"`
Output string `json:"output"`
Error string `json:"error"`
Success bool `json:"success"`
DurationMs int `json:"duration_ms"`
Timestamp int64 `json:"timestamp"`
}
// callLogRing 线程安全的环形缓冲区
type callLogRing struct {
mu sync.Mutex
records []CallLogRecord
capacity int
head int
size int
}
func newCallLogRing(capacity int) *callLogRing {
return &callLogRing{capacity: capacity, records: make([]CallLogRecord, capacity)}
}
func (r *callLogRing) push(rec CallLogRecord) {
r.mu.Lock()
defer r.mu.Unlock()
rec.CallID = fmt.Sprintf("%d", time.Now().UnixNano())
rec.Timestamp = time.Now().UnixMilli()
r.records[r.head] = rec
r.head = (r.head + 1) % r.capacity
if r.size < r.capacity {
r.size++
}
}
func (r *callLogRing) getAll() []CallLogRecord {
r.mu.Lock()
defer r.mu.Unlock()
result := make([]CallLogRecord, r.size)
for i := 0; i < r.size; i++ {
idx := (r.head - 1 - i) % r.capacity
if idx < 0 {
idx += r.capacity
}
result[i] = r.records[idx]
}
return result
}
func (r *callLogRing) statsByTool() map[string]map[string]interface{} {
r.mu.Lock()
defer r.mu.Unlock()
byTool := make(map[string]map[string]interface{})
for i := 0; i < r.size; i++ {
idx := (r.head - 1 - i) % r.capacity
if idx < 0 {
idx += r.capacity
}
rec := r.records[idx]
if _, ok := byTool[rec.ToolName]; !ok {
byTool[rec.ToolName] = map[string]interface{}{
"tool_name": rec.ToolName, "count": 0, "success_count": 0,
"fail_count": 0, "total_duration_ms": 0,
}
}
s := byTool[rec.ToolName]
s["count"] = s["count"].(int) + 1
if rec.Success {
s["success_count"] = s["success_count"].(int) + 1
} else {
s["fail_count"] = s["fail_count"].(int) + 1
}
s["total_duration_ms"] = s["total_duration_ms"].(int) + rec.DurationMs
}
return byTool
}
// 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
callLog *callLogRing
enabled bool
}
func NewToolRegistry() *ToolRegistry {
return &ToolRegistry{
tools: make(map[string]sdk.Tool),
callLog: newCallLogRing(500),
enabled: true,
}
}
// IsEnabled returns whether tool execution is enabled.
func (r *ToolRegistry) IsEnabled() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.enabled
}
// SetEnabled enables or disables tool execution.
func (r *ToolRegistry) SetEnabled(enabled bool) {
r.mu.Lock()
defer r.mu.Unlock()
r.enabled = enabled
}
// DefinitionNames returns all registered tool names.
func (r *ToolRegistry) DefinitionNames() []string {
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, 0, len(r.tools))
for id := range r.tools {
names = append(names, id)
}
return names
}
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()
startTime := time.Now()
if !ok {
r.callLog.push(CallLogRecord{
ToolName: toolID, Error: fmt.Sprintf("tool %q not found", toolID),
Success: false, DurationMs: int(time.Since(startTime).Milliseconds()),
})
return nil, fmt.Errorf("tool %q not found", toolID)
}
if err := tool.Validate(args); err != nil {
r.callLog.push(CallLogRecord{
ToolName: toolID, Error: err.Error(), Success: false,
DurationMs: int(time.Since(startTime).Milliseconds()),
})
return &sdk.ToolResult{Success: false, Error: err.Error()}, nil
}
result, err := tool.Execute(ctx, args)
durationMs := int(time.Since(startTime).Milliseconds())
if err != nil {
r.callLog.push(CallLogRecord{
ToolName: toolID, Error: err.Error(), Success: false, DurationMs: durationMs,
})
return result, err
}
var argsJSON string
if args != nil {
if b, _ := json.Marshal(args); b != nil {
argsJSON = string(b)
}
}
r.callLog.push(CallLogRecord{
ToolName: toolID, Arguments: argsJSON, Output: result.Output,
Error: result.Error, Success: result.Success, DurationMs: durationMs,
})
return result, nil
}
// UnregisterAll removes all tools matching given IDs.
func (r *ToolRegistry) UnregisterAll(toolIDs []string) {
r.mu.Lock()
defer r.mu.Unlock()
for _, id := range toolIDs {
delete(r.tools, id)
}
}
// GetCallLogs 获取工具调用记录(最新在前,支持按工具名过滤)
func (r *ToolRegistry) GetCallLogs(toolName string, limit int) []CallLogRecord {
all := r.callLog.getAll()
if toolName == "" {
if limit > 0 && limit < len(all) {
return all[:limit]
}
return all
}
filtered := make([]CallLogRecord, 0)
for _, rec := range all {
if rec.ToolName == toolName {
filtered = append(filtered, rec)
if limit > 0 && len(filtered) >= limit {
break
}
}
}
return filtered
}
// GetCallStats 获取工具调用统计
func (r *ToolRegistry) GetCallStats() map[string]interface{} {
byTool := r.callLog.statsByTool()
totalCalls, successCount, failCount, totalDurationMs := 0, 0, 0, 0
toolStats := make([]map[string]interface{}, 0, len(byTool))
for _, s := range byTool {
count := s["count"].(int)
success := s["success_count"].(int)
fail := s["fail_count"].(int)
totalDur := s["total_duration_ms"].(int)
avgDur := 0.0
if count > 0 {
avgDur = float64(totalDur) / float64(count)
}
s["avg_duration_ms"] = avgDur
delete(s, "total_duration_ms")
toolStats = append(toolStats, s)
totalCalls += count
successCount += success
failCount += fail
totalDurationMs += totalDur
}
avgDuration := 0.0
if totalCalls > 0 {
avgDuration = float64(totalDurationMs) / float64(totalCalls)
}
successRate := 0.0
if totalCalls > 0 {
successRate = float64(successCount) / float64(totalCalls) * 100
}
return map[string]interface{}{
"total_calls": totalCalls, "success_count": successCount, "fail_count": failCount,
"success_rate": successRate, "avg_duration_ms": avgDuration, "by_tool": toolStats,
}
}