d15acf587c
- docs: 17个文件重命名为 YYYY-MM-DD.HH-mm-SS-内容.md 格式 - config: 管理员凭据移至 backend/.env (ADMIN_USERNAME/PASSWORD) - gateway: 新增 SessionState 会话追踪 + GET /api/v1/admin/sessions - devtools: 新增7个代理端点 (dashboard/sessions/memory) - devtools: WebUI重构为侧边栏 + 5面板 (仪表盘/记忆/会话/服务/性能)
453 lines
12 KiB
Go
453 lines
12 KiB
Go
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 处理对话请求
|
||
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. 调用LLM
|
||
llmResp, err := llmAdapter.Chat(ctx, llmMessages)
|
||
if err != nil {
|
||
http.Error(w, fmt.Sprintf("LLM调用失败: %v", err), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// 5. 异步提取记忆
|
||
if memExtractor != nil {
|
||
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, llmResp.Content)
|
||
}
|
||
|
||
// 6. 构建响应
|
||
resp := map[string]interface{}{
|
||
"text": llmResp.Content,
|
||
"mode": req.Mode,
|
||
"message_id": fmt.Sprintf("msg-%d", time.Now().UnixNano()),
|
||
}
|
||
|
||
// 语音助手模式断句
|
||
if req.Mode == "voice_assistant" {
|
||
resp["segments"] = llm.SplitIntoSegments(llmResp.Content)
|
||
}
|
||
|
||
// Ensure unused variables don't cause compile errors
|
||
_ = personaLoader
|
||
_ = memRetriever
|
||
_ = memExtractor
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(resp)
|
||
}
|
||
|
||
// 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.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
|
||
}
|
||
|