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
+164 -14
View File
@@ -4,8 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/yourname/cyrene-ai/pkg/logger"
"sync"
"time"
"github.com/yourname/cyrene-ai/pkg/logger"
)
// ToolDefinition 工具定义(用于 LLM function calling
@@ -31,11 +33,93 @@ type ToolExecutor interface {
Definition() ToolDefinition
}
// 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) get(limit int) []CallLogRecord {
r.mu.Lock()
defer r.mu.Unlock()
if limit <= 0 || limit > r.size {
limit = r.size
}
result := make([]CallLogRecord, limit)
for i := 0; i < limit; 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
}
// Registry 工具注册中心
type Registry struct {
mu sync.RWMutex
tools map[string]ToolExecutor
enabled bool
mu sync.RWMutex
tools map[string]ToolExecutor
enabled bool
callLog *callLogRing
}
// NewRegistry 创建工具注册中心
@@ -43,6 +127,7 @@ func NewRegistry() *Registry {
return &Registry{
tools: make(map[string]ToolExecutor),
enabled: true,
callLog: newCallLogRing(500),
}
}
@@ -73,30 +158,38 @@ func (r *Registry) Execute(ctx context.Context, toolName string, arguments map[s
executor, ok := r.tools[toolName]
r.mu.RUnlock()
startTime := time.Now()
if !ok {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("未知工具: %s", toolName),
}, nil
errMsg := fmt.Sprintf("未知工具: %s", toolName)
r.callLog.push(CallLogRecord{
ToolName: toolName, Error: errMsg, Success: false, DurationMs: int(time.Since(startTime).Milliseconds()),
})
return &ToolResult{ToolName: toolName, Success: false, Error: errMsg}, nil
}
logger.Printf("[工具执行] 调用工具 %s,参数: %v", toolName, arguments)
result, err := executor.Execute(ctx, arguments)
durationMs := int(time.Since(startTime).Milliseconds())
if err != nil {
logger.Printf("[工具执行] 工具 %s 执行失败: %v", toolName, err)
return &ToolResult{
ToolName: toolName,
Success: false,
Error: err.Error(),
}, nil
r.callLog.push(CallLogRecord{
ToolName: toolName, Error: err.Error(), Success: false, DurationMs: durationMs,
})
return &ToolResult{ToolName: toolName, Success: false, Error: err.Error()}, nil
}
argsJSON, _ := json.Marshal(arguments)
if result.Success {
logger.Printf("[工具执行] 工具 %s 执行成功 (数据长度: %d)", toolName, len(result.Data))
} else {
logger.Printf("[工具执行] 工具 %s 返回错误: %s", toolName, result.Error)
}
r.callLog.push(CallLogRecord{
ToolName: toolName, Arguments: string(argsJSON), Output: result.Data,
Error: result.Error, Success: result.Success, DurationMs: durationMs,
})
return result, nil
}
@@ -135,6 +228,63 @@ func (r *Registry) ListTools() []string {
return names
}
// GetCallLogs 获取工具调用记录(最新在前)
func (r *Registry) GetCallLogs(toolName string, limit int) []CallLogRecord {
all := r.callLog.get(r.callLog.size)
if toolName == "" {
if limit > 0 && limit < len(all) {
all = 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 *Registry) 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,
}
}
// ToJSON 将工具定义序列化为 JSON(用于 LLM 请求)
func (r *Registry) ToJSON() ([]byte, error) {
defs := r.GetDefinitions()
@@ -1,225 +0,0 @@
package tools
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"strings"
"time"
)
// ToolEngineClient 工具引擎 HTTP 客户端
// 将工具执行请求转发到独立的 tool-engine 微服务
type ToolEngineClient struct {
baseURL string
httpClient *http.Client
}
// toolEngineToolDef 来自 tool-engine 的工具定义响应
type toolEngineToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// toolEngineResult 来自 tool-engine 的工具执行结果
type toolEngineResult struct {
ID string `json:"id"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
// NewToolEngineClient 创建工具引擎客户端
func NewToolEngineClient(baseURL string) *ToolEngineClient {
return &ToolEngineClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetDefinitions 从 tool-engine 获取所有工具定义
func (c *ToolEngineClient) GetDefinitions(ctx context.Context) ([]ToolDefinition, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/tools", nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("请求工具列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("获取工具列表返回状态码 %d: %s", resp.StatusCode, string(body))
}
var result struct {
Tools []toolEngineToolDef `json:"tools"`
Total int `json:"total"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析工具列表失败: %w", err)
}
defs := make([]ToolDefinition, 0, len(result.Tools))
for _, t := range result.Tools {
defs = append(defs, ToolDefinition{
Name: t.Name,
Description: t.Description,
Parameters: t.Parameters,
})
}
logger.Printf("[tool-engine-client] 从 tool-engine 获取了 %d 个工具定义", len(defs))
return defs, nil
}
// Execute 通过 tool-engine 执行工具调用
// 包含重试逻辑:最多重试 2 次(共 3 次尝试),间隔 100ms
func (c *ToolEngineClient) Execute(ctx context.Context, toolName string, arguments map[string]interface{}) (*ToolResult, error) {
const maxRetries = 2
const retryDelay = 100 * time.Millisecond
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
logger.Printf("[tool-engine-client] 工具 %s 第 %d 次重试 (上次错误: %v)", toolName, attempt, lastErr)
select {
case <-ctx.Done():
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("请求被取消: %v", ctx.Err()),
}, nil
case <-time.After(retryDelay):
}
}
result, err := c.executeOnce(ctx, toolName, arguments)
if err == nil && result.Success {
return result, nil
}
if result != nil {
lastErr = fmt.Errorf("%s", result.Error)
} else {
lastErr = err
}
// 不可重试的错误:工具不存在、参数序列化失败、创建请求失败
if result != nil && (strings.Contains(result.Error, "不存在") ||
strings.Contains(result.Error, "序列化") ||
strings.Contains(result.Error, "创建请求")) {
return result, nil
}
}
logger.Printf("[tool-engine-client] 工具 %s 所有重试均失败 (最后错误: %v)", toolName, lastErr)
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("请求 tool-engine 失败 (已重试 %d 次): %v", maxRetries, lastErr),
}, nil
}
// executeOnce 执行单次工具调用(不含重试逻辑)
func (c *ToolEngineClient) executeOnce(ctx context.Context, toolName string, arguments map[string]interface{}) (*ToolResult, error) {
body, err := json.Marshal(map[string]interface{}{
"arguments": arguments,
})
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("序列化参数失败: %v", err),
}, nil
}
url := fmt.Sprintf("%s/api/v1/tools/%s/execute", c.baseURL, toolName)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("创建请求失败: %v", err),
}, nil
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("请求 tool-engine 失败: %v", err),
}, nil
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("工具 %s 不存在", toolName),
}, nil
}
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("tool-engine 返回状态码 %d: %s", resp.StatusCode, string(respBody)),
}, nil
}
var result toolEngineResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("解析 tool-engine 响应失败: %v", err),
}, nil
}
if result.Error != "" {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: result.Error,
}, nil
}
return &ToolResult{
ToolName: toolName,
Success: true,
Data: result.Output,
}, nil
}
// HealthCheck 检查 tool-engine 服务是否可用
func (c *ToolEngineClient) HealthCheck(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/health", nil)
if err != nil {
return fmt.Errorf("创建健康检查请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("tool-engine 不可达: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("tool-engine 健康检查返回状态码 %d", resp.StatusCode)
}
return nil
}