package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/joho/godotenv" ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context" "github.com/yourname/cyrene-ai/ai-core/internal/llm" "github.com/yourname/cyrene-ai/ai-core/internal/memory" "github.com/yourname/cyrene-ai/ai-core/internal/model" "github.com/yourname/cyrene-ai/ai-core/internal/orchestrator" "github.com/yourname/cyrene-ai/ai-core/internal/persona" ) func main() { // 自动加载 .env 文件(来自 backend/.env) if err := godotenv.Load("../.env"); err != nil { log.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值") } log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("🧠 AI-Core 服务启动中...") // 加载配置 cfg := loadConfig() // 初始化人格加载器 personaDir := cfg.PersonaDir if personaDir == "" { personaDir = "./internal/persona" } personaLoader, err := persona.NewLoader(personaDir) if err != nil { log.Fatalf("加载人格配置失败: %v", err) } log.Printf("已加载 %d 个人格: %v", len(personaLoader.List()), personaLoader.List()) // 初始化LLM适配器 llmProvider := llm.NewOpenAIProvider(llm.OpenAIConfig{ BaseURL: cfg.LLMBaseURL, APIKey: cfg.LLMAPIKey, Model: cfg.LLMModel, FallbackModel: cfg.LLMFallbackModel, Timeout: 120 * time.Second, }) llmAdapter := llm.NewAdapter(llmProvider) log.Printf("LLM适配器已就绪: 模型=%s", llmAdapter.ModelName()) // 初始化记忆系统 var memStore *memory.Store var memRetriever *memory.Retriever var memExtractor *memory.Extractor if cfg.DatabaseURL != "" { memStore, err = memory.NewStore(cfg.DatabaseURL) if err != nil { log.Printf("⚠ 记忆存储初始化失败 (将跳过记忆功能): %v", err) } else { defer memStore.Close() log.Println("记忆存储已就绪") memRetriever = memory.NewRetriever(memStore, nil) // 记忆提取器使用LLM memExtractor = memory.NewExtractor(memStore, func(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) { return llmAdapter.Chat(ctx, messages) }) log.Println("记忆提取器已就绪") } } // 初始化上下文构建器 ctxBuilder := &ctxbuild.Builder{} // 健康检查与对话API的HTTP mux mux := http.NewServeMux() // 手动构建 orchestrator 用于处理(因为现有orchestrator结构体已定义但未导出构造函数) orch := &orchestrator.Orchestrator{} // 注册对话API端点 mux.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) { handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, memRetriever, memExtractor) }) // 注册记忆API端点 mux.HandleFunc("/api/v1/memory/search", func(w http.ResponseWriter, r *http.Request) { handleMemorySearch(w, r, memRetriever) }) mux.HandleFunc("/api/v1/memory", func(w http.ResponseWriter, r *http.Request) { handleMemoryCRUD(w, r, memStore, memExtractor) }) mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + llmAdapter.ModelName() + `"}`)) }) // 启动HTTP服务 srv := &http.Server{ Addr: ":" + cfg.Port, Handler: mux, } go func() { log.Printf("🚀 AI-Core 服务已启动在端口 %s", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("服务启动失败: %v", err) } }() // 优雅关闭 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("正在关闭 AI-Core 服务...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() srv.Shutdown(ctx) log.Println("AI-Core 服务已关闭") } // Config AI-Core配置 type Config struct { Port string PersonaDir string LLMBaseURL string LLMAPIKey string LLMModel string LLMFallbackModel string DatabaseURL string } func loadConfig() Config { return Config{ Port: getEnv("AI_CORE_PORT", "8081"), PersonaDir: getEnv("PERSONA_DIR", "./internal/persona"), LLMBaseURL: getEnv("LLM_API_URL", "https://api.openai.com/v1"), LLMAPIKey: getEnv("LLM_API_KEY", ""), LLMModel: getEnv("LLM_MODEL", "gpt-4o"), LLMFallbackModel: getEnv("LLM_FALLBACK_MODEL", "gpt-4o-mini"), DatabaseURL: buildDatabaseURL(), } } func buildDatabaseURL() string { host := getEnv("POSTGRES_HOST", "localhost") port := getEnv("POSTGRES_PORT", "5432") user := getEnv("POSTGRES_USER", "cyrene") password := getEnv("POSTGRES_PASSWORD", "change_me") dbname := getEnv("POSTGRES_DB", "cyrene_ai") sslmode := getEnv("POSTGRES_SSLMODE", "disable") return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", user, password, host, port, dbname, sslmode) } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // handleChat 处理对话请求(SSE 流式响应) func handleChat( w http.ResponseWriter, r *http.Request, _ *orchestrator.Orchestrator, ctxBuilder *ctxbuild.Builder, llmAdapter *llm.Adapter, personaLoader *persona.Loader, memRetriever *memory.Retriever, memExtractor *memory.Extractor, ) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 解析请求 var req struct { UserID string `json:"user_id"` SessionID string `json:"session_id"` Message string `json:"message"` Mode string `json:"mode"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "无效的请求体", http.StatusBadRequest) return } if req.Mode == "" { req.Mode = "text" } ctx := r.Context() // 1. 检索相关记忆 var memories []memory.MemoryEntry if memRetriever != nil { var err error memories, err = memRetriever.Retrieve(ctx, req.UserID, req.Message) if err != nil { log.Printf("[chat] 记忆检索失败: %v", err) } } // 2. 加载人格配置 personaConfig, err := personaLoader.Get("cyrene") if err != nil { http.Error(w, fmt.Sprintf("加载人格失败: %v", err), http.StatusInternalServerError) return } // 3. 构建对话上下文 llmMessages, err := ctxBuilder.Build(ctx, ctxbuild.BuildParams{ UserID: req.UserID, SessionID: req.SessionID, UserMessage: req.Message, Persona: personaConfig, Memories: memories, HistoryLimit: 20, }) if err != nil { http.Error(w, fmt.Sprintf("构建上下文失败: %v", err), http.StatusInternalServerError) return } // 4. 设置 SSE 响应头 w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming not supported", http.StatusInternalServerError) return } // 5. 调用LLM流式接口 chunkCh, err := llmAdapter.ChatStream(ctx, llmMessages) if err != nil { // 流式初始化失败,返回 SSE 格式错误 errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("LLM调用失败: %v", err)}) fmt.Fprintf(w, "data: %s\n\n", errData) flusher.Flush() fmt.Fprintf(w, "data: [DONE]\n\n") flusher.Flush() return } messageID := fmt.Sprintf("msg-%d", time.Now().UnixNano()) // 6. 逐 token 推送 SSE var fullContent string var segments []llm.Segment segmenter := llm.NewSegmenter() for chunk := range chunkCh { if chunk.Error != nil { log.Printf("[chat] 流式错误: %v", chunk.Error) errData, _ := json.Marshal(map[string]string{"delta": "", "error": chunk.Error.Error()}) fmt.Fprintf(w, "data: %s\n\n", errData) flusher.Flush() fmt.Fprintf(w, "data: [DONE]\n\n") flusher.Flush() return } if chunk.Done { // 流结束,flush 剩余片段 if remaining := segmenter.Flush(); remaining != nil { segments = append(segments, *remaining) } break } if chunk.Content != "" { fullContent += chunk.Content // 实时断句 newSegs := segmenter.Feed(chunk.Content) segments = append(segments, newSegs...) deltaData, _ := json.Marshal(map[string]string{ "delta": chunk.Content, "message_id": messageID, }) fmt.Fprintf(w, "data: %s\n\n", deltaData) flusher.Flush() } } // 7. 发送结束标记(附带元数据) endData, _ := json.Marshal(map[string]interface{}{ "message_id": messageID, "mode": req.Mode, "segments": segments, "done": true, }) fmt.Fprintf(w, "data: %s\n\n", endData) flusher.Flush() fmt.Fprintf(w, "data: [DONE]\n\n") flusher.Flush() // 8. 异步提取记忆 if memExtractor != nil && fullContent != "" { go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent) } // Ensure unused variables don't cause compile errors _ = personaLoader _ = memRetriever _ = memExtractor _ = messageID } // handleMemorySearch 处理记忆搜索请求 func handleMemorySearch( w http.ResponseWriter, r *http.Request, memRetriever *memory.Retriever, ) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } userID := r.URL.Query().Get("user_id") if userID == "" { http.Error(w, "缺少 user_id 参数", http.StatusBadRequest) return } query := r.URL.Query().Get("q") if query == "" { http.Error(w, "缺少 q 参数", http.StatusBadRequest) return } if memRetriever == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "query": query, "memories": []interface{}{}, "message": "记忆系统未就绪", }) return } ctx := r.Context() memories, err := memRetriever.Retrieve(ctx, userID, query) if err != nil { log.Printf("[memory] 检索失败: %v", err) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "query": query, "memories": []interface{}{}, "error": "检索失败", }) return } if memories == nil { memories = []memory.MemoryEntry{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "query": query, "memories": memories, "total": len(memories), }) } // handleMemoryCRUD 处理记忆的 CRUD 操作 func handleMemoryCRUD( w http.ResponseWriter, r *http.Request, memStore *memory.Store, memExtractor *memory.Extractor, ) { switch r.Method { case http.MethodGet: // 列出用户的所有记忆 userID := r.URL.Query().Get("user_id") if userID == "" { http.Error(w, "缺少 user_id 参数", http.StatusBadRequest) return } if memStore == nil { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "memories": []interface{}{}, "message": "记忆系统未就绪", }) return } ctx := r.Context() memories, err := memStore.Query(ctx, model.MemoryQuery{ UserID: userID, Limit: 50, }) if err != nil { log.Printf("[memory] 查询失败: %v", err) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "memories": []interface{}{}, "error": "查询失败", }) return } if memories == nil { memories = []model.MemoryEntry{} } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user_id": userID, "memories": memories, "total": len(memories), }) case http.MethodDelete: // 删除单条记忆: DELETE /api/v1/memory?id=xxx memoryID := r.URL.Query().Get("id") if memoryID == "" { http.Error(w, "缺少 id 参数", http.StatusBadRequest) return } if memStore == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "记忆系统未就绪", }) return } ctx := r.Context() if err := memStore.Delete(ctx, memoryID); err != nil { log.Printf("[memory] 删除失败: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "删除失败", }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "status": "deleted", "memory_id": memoryID, }) case http.MethodPost: // 手动添加记忆 var req struct { UserID string `json:"user_id"` Content string `json:"content"` Category string `json:"category"` Priority int `json:"priority"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "无效的请求体", http.StatusBadRequest) return } if req.UserID == "" || req.Content == "" { http.Error(w, "缺少 user_id 或 content", http.StatusBadRequest) return } if req.Category == "" { req.Category = "other" } if req.Priority <= 0 { req.Priority = 1 } if memStore == nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "记忆系统未就绪", }) return } entry := &model.MemoryEntry{ UserID: req.UserID, Content: req.Content, Category: model.MemoryCategory(req.Category), Priority: model.MemoryPriority(req.Priority), } ctx := r.Context() if err := memStore.Save(ctx, entry); err != nil { log.Printf("[memory] 保存失败: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "error": "保存失败", }) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "status": "saved", "memory": entry, }) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } // Ensure unused variables don't cause compile errors _ = memExtractor }