This repository has been archived on 2026-08-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Cyrene/backend/ai-core/cmd/main.go
T
AskaEth 186513f381 feat: 多功能升级 — 流式逐字渲染、对话缓存、会话组织优化、记忆管理修复、性能仪表盘
- 前端消息流式逐字渲染 (AI-Core ChatStream → SSE → Gateway → WebSocket stream_chunk → fadeInUp + cursorBlink)
- 后端对话缓存 (conversationCache sync.Map, GET /sessions/:id/messages)
- 前端侧边栏历史多轮对话显示
- DevTools 性能监控图标移至首页仪表盘
- DevTools 用户记忆查询/删减功能修复 (补全 DELETE 数据链路)
- 后端和 DevTools 按用户分类组织实时活动会话 (map[userID]map[sessionID]*Client)
- 新增 docs/api-reference/ 路由参考文档
- 新增 docs/message-flow-architecture.md 消息链路架构文档
2026-05-16 17:44:03 +08:00

545 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}