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
+142 -32
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
@@ -26,6 +27,20 @@ import (
"github.com/yourname/cyrene-ai/ai-core/internal/rag"
"github.com/yourname/cyrene-ai/ai-core/internal/subsession"
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
plgManager "github.com/yourname/cyrene-ai/pkg/plugins/manager"
plgSDK "github.com/yourname/cyrene-ai/pkg/plugins/sdk"
pluginCalc "github.com/yourname/cyrene-ai/pkg/plugins/calculator"
pluginCrypto "github.com/yourname/cyrene-ai/pkg/plugins/crypto"
pluginDate "github.com/yourname/cyrene-ai/pkg/plugins/datetime"
pluginFile "github.com/yourname/cyrene-ai/pkg/plugins/file"
pluginHTTP "github.com/yourname/cyrene-ai/pkg/plugins/http"
pluginJSON "github.com/yourname/cyrene-ai/pkg/plugins/json"
pluginMD "github.com/yourname/cyrene-ai/pkg/plugins/markdown"
pluginRand "github.com/yourname/cyrene-ai/pkg/plugins/random"
pluginText "github.com/yourname/cyrene-ai/pkg/plugins/text"
pluginWF "github.com/yourname/cyrene-ai/pkg/plugins/web_fetch"
pluginWS "github.com/yourname/cyrene-ai/pkg/plugins/web_search"
)
var cfg Config
@@ -113,6 +128,15 @@ func main() {
// 初始化会话历史存储
convStore := ctxbuild.NewConversationStore(50)
// 从数据库恢复主会话历史(避免重启丢失上下文)
adminUserID := "admin"
adminSessionID := "admin-session-main"
if cfg.DatabaseURL != "" {
if err := convStore.LoadFromDB(cfg.DatabaseURL, adminSessionID, 50); err != nil {
log.Printf("⚠ 从数据库恢复会话历史失败(不影响服务启动): %v", err)
}
}
log.Println("会话历史存储已就绪 (上限50条)")
// 初始化上下文构建器
@@ -141,36 +165,34 @@ func main() {
knowledgeRetriever := rag.NewRetriever(knowledgeStore)
log.Printf("RAG 知识库已就绪: 目录=%s, 嵌入模型=text-embedding-3-small", knowledgeDir)
// 初始化工具注册中心
toolRegistry := tools.NewRegistry()
// 初始化工具注册中心 (使用共享插件模块)
toolRegistry := plgManager.NewToolRegistry()
if getEnvBool("ENABLE_TOOLS", true) {
toolRegistry.Register(tools.NewWebFetchTool())
toolRegistry.Register(tools.NewWebSearchTool())
toolRegistry.Register(tools.NewCalculatorTool())
toolRegistry.Register(tools.NewDateTimeTool())
toolRegistry.Register(tools.NewHTTPTool())
toolRegistry.Register(tools.NewJSONTool())
toolRegistry.Register(tools.NewTextTool())
toolRegistry.Register(tools.NewRandomTool())
toolRegistry.Register(tools.NewCryptoTool())
toolRegistry.Register(tools.NewMarkdownTool())
// File tool uses DATA_DIR or defaults to /tmp/cyrene_data
toolRegistry.Register(tools.NewFileTool(dataDir))
// 11 个共享通用插件 — 注册其工具到统一注册中心
registerPluginTools(toolRegistry, &pluginCalc.CalculatorPlugin{})
registerPluginTools(toolRegistry, &pluginDate.DatetimePlugin{})
registerPluginTools(toolRegistry, &pluginText.TextPlugin{})
registerPluginTools(toolRegistry, &pluginCrypto.CryptoPlugin{})
registerPluginTools(toolRegistry, &pluginRand.RandomPlugin{})
registerPluginTools(toolRegistry, &pluginMD.MarkdownPlugin{})
registerPluginTools(toolRegistry, &pluginJSON.JSONPlugin{})
registerPluginTools(toolRegistry, pluginFile.NewFilePlugin(dataDir))
registerPluginTools(toolRegistry, pluginHTTP.NewHTTPPlugin())
registerPluginTools(toolRegistry, pluginWS.NewWebSearchPlugin())
registerPluginTools(toolRegistry, pluginWF.NewWebFetchPlugin())
// ai-core 专属工具 — 通过 sdk.Tool 适配器注册
if iotClient != nil {
toolRegistry.Register(tools.NewIoTQueryTool(iotClient))
toolRegistry.Register(tools.NewIoTControlTool(iotClient))
toolRegistry.Register(wrapTool(tools.NewIoTQueryTool(iotClient), "iot_query", "Query IoT Devices", "iot"))
toolRegistry.Register(wrapTool(tools.NewIoTControlTool(iotClient), "iot_control", "Control IoT Devices", "iot"))
}
// Phase 6.2: 主机操控工具
if hostManager != nil {
toolRegistry.Register(tools.NewHostExecTool(hostManager))
toolRegistry.Register(tools.NewHostFileTool(hostManager))
toolRegistry.Register(tools.NewHostSystemTool(hostManager))
toolRegistry.Register(wrapTool(tools.NewHostExecTool(hostManager), "host_exec", "Host Command Execution", "system"))
toolRegistry.Register(wrapTool(tools.NewHostFileTool(hostManager), "host_file", "Host File Operations", "system"))
toolRegistry.Register(wrapTool(tools.NewHostSystemTool(hostManager), "host_system", "Host System Info", "system"))
}
// Phase 6.3: 视觉理解工具 — 可选 LLM 增强,无视觉模型时回退 base64 模式
var visionProvider llm.LLMProvider
if configLoader != nil && configLoader.HasConfig() {
cfg := configLoader.GetConfig()
@@ -187,20 +209,17 @@ func main() {
if visionProvider == nil {
log.Println("视觉模型未配置,vision_analyze 将使用 base64 模式")
}
toolRegistry.Register(tools.NewVisionTool(visionProvider))
toolRegistry.Register(wrapTool(tools.NewVisionTool(visionProvider), "vision_analyze", "Image Vision Analysis & OCR", "multimodal"))
// Phase 6.6: 知识库 RAG 工具
if knowledgeRetriever != nil {
toolRegistry.Register(tools.NewKnowledgeSearchTool(knowledgeRetriever))
toolRegistry.Register(tools.NewKnowledgeIngestTool(knowledgeStore))
toolRegistry.Register(wrapTool(tools.NewKnowledgeSearchTool(knowledgeRetriever), "knowledge_search", "Search Knowledge Base", "knowledge"))
toolRegistry.Register(wrapTool(tools.NewKnowledgeIngestTool(knowledgeStore), "knowledge_ingest", "Ingest Knowledge Document", "knowledge"))
}
log.Printf("工具注册中心已就绪: %d 个工具 (%v)", len(toolRegistry.ListTools()), toolRegistry.ListTools())
log.Printf("工具注册中心已就绪: %d 个工具 (%v)", len(toolRegistry.DefinitionNames()), toolRegistry.DefinitionNames())
}
// 初始化后台思考器(增强版:支持工具调用和记忆管理)
thinkerCfg := background.DefaultThinkerConfig()
adminUserID := "admin"
adminSessionID := "admin-session-main"
// 创建记忆服务 HTTP 客户端(用于持久化思考日志到 memory-service
memServiceURL := getEnv("MEMORY_SERVICE_URL", "http://localhost:8091")
@@ -221,6 +240,18 @@ func main() {
adminSessionID,
memClient,
)
// 初始化动态调度加载器 (Phase: thinking-schedule)
schedulePath := getEnv("THINKING_SCHEDULE_PATH", "../thinking_schedule.json")
if scheduleLoader, err := background.NewScheduleLoader(schedulePath); err != nil {
log.Printf("⚠ 思考调度配置加载失败,使用固定间隔: %v", err)
} else if scheduleLoader.HasConfig() {
thinker.SetScheduleLoader(scheduleLoader)
log.Println("[后台思考] 动态调度已启用 (thinking_schedule.json)")
} else {
log.Println("[后台思考] 调度配置文件不存在,使用 .env 固定间隔")
}
thinker.Start()
defer thinker.Stop()
@@ -347,6 +378,44 @@ func main() {
})
})
// 工具调用记录
mux.HandleFunc("/api/v1/tools/calls", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
toolName := r.URL.Query().Get("tool_name")
limit := 50
if n, err := fmt.Sscanf(r.URL.Query().Get("limit"), "%d", &limit); n != 1 || err != nil || limit <= 0 {
limit = 50
}
if limit > 500 {
limit = 500
}
calls := toolRegistry.GetCallLogs(toolName, limit)
if calls == nil {
calls = []plgManager.CallLogRecord{}
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"calls": calls, "total": len(calls), "page": page, "limit": limit,
})
})
// 工具调用统计
mux.HandleFunc("/api/v1/tools/calls/stats", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(toolRegistry.GetCallStats())
})
// 启动HTTP服务
srv := &http.Server{
Addr: ":" + cfg.Port,
@@ -444,12 +513,53 @@ func getEnvBool(key string, fallback bool) bool {
}
}
// registerPluginTools 从插件实例注册其所有工具到注册中心
func registerPluginTools(registry *plgManager.ToolRegistry, plugin plgSDK.Plugin) {
for _, t := range plugin.Tools() {
if err := registry.Register(t); err != nil {
log.Printf("⚠ 注册工具失败: %v", err)
}
}
}
// wrapTool 包装 ai-core 旧 ToolExecutor 为 sdk.Tool
func wrapTool(executor tools.ToolExecutor, id, displayName, category string) plgSDK.Tool {
return &toolAdapter{
executor: executor,
def: plgSDK.ToolDefinition{
ID: id, Name: id, DisplayName: displayName,
Description: executor.Definition().Description,
Category: category,
Complexity: plgSDK.ComplexitySimple,
Parameters: executor.Definition().Parameters,
},
}
}
type toolAdapter struct {
plgSDK.BaseTool
executor tools.ToolExecutor
def plgSDK.ToolDefinition
}
func (a *toolAdapter) Definition() plgSDK.ToolDefinition { return a.def }
func (a *toolAdapter) Execute(ctx context.Context, args map[string]interface{}) (*plgSDK.ToolResult, error) {
result, err := a.executor.Execute(ctx, args)
if err != nil {
return nil, err
}
return &plgSDK.ToolResult{
ToolName: result.ToolName, Success: result.Success,
Output: result.Data, Error: result.Error,
}, nil
}
// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
func buildOpenAITools(registry *tools.Registry) []llm.OpenAITool {
func buildOpenAITools(registry *plgManager.ToolRegistry) []llm.OpenAITool {
if registry == nil || !registry.IsEnabled() {
return nil
}
defs := registry.GetDefinitions()
defs := registry.Definitions()
if len(defs) == 0 {
return nil
}
@@ -478,7 +588,7 @@ func handleChat(
_ *memory.Extractor,
iotClient *tools.IoTClient,
thinker *background.Thinker,
_ *tools.Registry,
_ *plgManager.ToolRegistry,
) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)