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:
+142
-32
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user