78e3f450c2
- Fix: Session history flash (race condition + WS guard) - Fix: Chat background overlay + sidebar transparency - Fix: IoT device control (Chinese action names, status field) - Feat: Independent memory-service (port 8091, 13 endpoints) - Feat: Independent tool-engine service (port 8092, 13 tools) - Feat: Tool call logs with paginated DevTools panel - Feat: Thinking log records with DevTools panel - Feat: Future development roadmap document - Chore: Updated .gitignore, go.work, DevTools config - Chore: 5-service health check, project review docs
759 lines
21 KiB
Go
759 lines
21 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/joho/godotenv"
|
||
|
||
"github.com/yourname/cyrene-ai/ai-core/internal/background"
|
||
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"
|
||
"github.com/yourname/cyrene-ai/ai-core/internal/tools"
|
||
)
|
||
|
||
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 = memory.NewStore(cfg.DatabaseURL)
|
||
defer memStore.Close()
|
||
|
||
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("记忆提取器已就绪")
|
||
}
|
||
|
||
// 初始化会话历史存储
|
||
convStore := ctxbuild.NewConversationStore(50)
|
||
log.Println("会话历史存储已就绪 (上限50条)")
|
||
|
||
// 初始化上下文构建器
|
||
ctxBuilder := ctxbuild.NewBuilder(convStore)
|
||
|
||
// 初始化 IoT 客户端
|
||
var iotClient *tools.IoTClient
|
||
if cfg.IoTServiceURL != "" {
|
||
iotClient = tools.NewIoTClient(cfg.IoTServiceURL)
|
||
log.Printf("IoT 客户端已就绪: %s", cfg.IoTServiceURL)
|
||
} else {
|
||
log.Println("IoT 客户端未配置 (IOT_DEBUG_SERVICE_URL 为空)")
|
||
}
|
||
|
||
// 初始化工具注册中心
|
||
toolRegistry := tools.NewRegistry()
|
||
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
|
||
dataDir := getEnv("DATA_DIR", "/tmp/cyrene_data")
|
||
toolRegistry.Register(tools.NewFileTool(dataDir))
|
||
|
||
if iotClient != nil {
|
||
toolRegistry.Register(tools.NewIoTQueryTool(iotClient))
|
||
toolRegistry.Register(tools.NewIoTControlTool(iotClient))
|
||
}
|
||
log.Printf("工具注册中心已就绪: %d 个工具 (%v)", len(toolRegistry.ListTools()), toolRegistry.ListTools())
|
||
}
|
||
|
||
// 初始化后台思考器(增强版:支持工具调用和记忆管理)
|
||
thinkerCfg := background.DefaultThinkerConfig()
|
||
adminUserID := "admin_admin"
|
||
adminSessionID := "admin-session-main"
|
||
|
||
// 创建记忆服务 HTTP 客户端(用于持久化思考日志到 memory-service)
|
||
memServiceURL := getEnv("MEMORY_SERVICE_URL", "http://localhost:8091")
|
||
memClient := memory.NewClient(memServiceURL)
|
||
log.Printf("记忆服务客户端已就绪: %s", memServiceURL)
|
||
|
||
thinker := background.NewThinker(
|
||
thinkerCfg,
|
||
personaLoader,
|
||
memRetriever,
|
||
llmAdapter,
|
||
iotClient,
|
||
memStore,
|
||
memExtractor,
|
||
toolRegistry,
|
||
convStore,
|
||
adminUserID,
|
||
adminSessionID,
|
||
memClient,
|
||
)
|
||
thinker.Start()
|
||
defer thinker.Stop()
|
||
|
||
// 健康检查与对话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, iotClient, thinker, toolRegistry)
|
||
})
|
||
|
||
// 注册记忆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
|
||
IoTServiceURL 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(),
|
||
IoTServiceURL: getEnv("IOT_DEBUG_SERVICE_URL", ""),
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
func getEnvBool(key string, fallback bool) bool {
|
||
v := os.Getenv(key)
|
||
if v == "" {
|
||
return fallback
|
||
}
|
||
switch strings.ToLower(v) {
|
||
case "true", "1", "yes", "on":
|
||
return true
|
||
case "false", "0", "no", "off":
|
||
return false
|
||
default:
|
||
return fallback
|
||
}
|
||
}
|
||
|
||
// buildOpenAITools 将工具注册中心的定义转换为 LLM 层的 OpenAITool 格式
|
||
func buildOpenAITools(registry *tools.Registry) []llm.OpenAITool {
|
||
if registry == nil || !registry.IsEnabled() {
|
||
return nil
|
||
}
|
||
defs := registry.GetDefinitions()
|
||
if len(defs) == 0 {
|
||
return nil
|
||
}
|
||
result := make([]llm.OpenAITool, 0, len(defs))
|
||
for _, d := range defs {
|
||
result = append(result, llm.OpenAITool{
|
||
Type: "function",
|
||
Function: llm.OpenAIToolFunc{
|
||
Name: d.Name,
|
||
Description: d.Description,
|
||
Parameters: d.Parameters,
|
||
},
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// 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,
|
||
iotClient *tools.IoTClient,
|
||
thinker *background.Thinker,
|
||
toolRegistry *tools.Registry,
|
||
) {
|
||
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()
|
||
|
||
// 0. 记录用户活动(重置闲置计时器)
|
||
if thinker != nil {
|
||
thinker.RecordUserMessage()
|
||
}
|
||
|
||
// 0.1 缓存用户消息到会话历史
|
||
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
|
||
|
||
// 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
|
||
}
|
||
|
||
// 2.1 始终获取 IoT 设备状态(去掉关键词门控,让昔涟始终了解家里的状态)
|
||
var deviceContext string
|
||
if iotClient != nil {
|
||
devices := iotClient.GetDevicesForContext()
|
||
if len(devices) > 0 {
|
||
deviceInfos := make([]ctxbuild.DeviceInfo, 0, len(devices))
|
||
for _, d := range devices {
|
||
deviceInfos = append(deviceInfos, ctxbuild.DeviceInfo{
|
||
Name: d.Name,
|
||
Type: d.Type,
|
||
Status: d.Status,
|
||
Brightness: d.Brightness,
|
||
Color: d.Color,
|
||
Temperature: d.Temperature,
|
||
Mode: d.Mode,
|
||
Value: d.Value,
|
||
Unit: d.Unit,
|
||
Battery: d.Battery,
|
||
})
|
||
}
|
||
deviceContext = ctxbuild.InjectDeviceContext(deviceInfos)
|
||
log.Printf("[chat] 已注入 IoT 设备状态 (%d 个设备)", len(deviceInfos))
|
||
}
|
||
}
|
||
|
||
// 2.2 获取待处理的后台思考
|
||
var pendingThoughts []string
|
||
if thinker != nil && thinker.HasPendingThoughts() {
|
||
pts := thinker.GetPendingThoughts()
|
||
for _, pt := range pts {
|
||
if pt.Content != "" {
|
||
pendingThoughts = append(pendingThoughts, pt.Content)
|
||
}
|
||
}
|
||
if len(pendingThoughts) > 0 {
|
||
log.Printf("[chat] 注入 %d 条后台思考到上下文", len(pendingThoughts))
|
||
}
|
||
}
|
||
|
||
// 3. 构建对话上下文
|
||
llmMessages, err := ctxBuilder.Build(ctx, ctxbuild.BuildParams{
|
||
UserID: req.UserID,
|
||
SessionID: req.SessionID,
|
||
UserMessage: req.Message,
|
||
Persona: personaConfig,
|
||
Memories: memories,
|
||
HistoryLimit: 20,
|
||
DeviceContext: deviceContext,
|
||
PendingThoughts: pendingThoughts,
|
||
})
|
||
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. 准备工具定义
|
||
openAITools := buildOpenAITools(toolRegistry)
|
||
|
||
// 5.1 如果启用了工具,先进行同步调用检测是否需要工具调用
|
||
if len(openAITools) > 0 {
|
||
log.Printf("[chat] 启用工具调用: %d 个工具可用", len(openAITools))
|
||
|
||
syncResp, syncErr := llmAdapter.ChatWithTools(ctx, llmMessages, openAITools)
|
||
if syncErr != nil {
|
||
log.Printf("[chat] 工具检测调用失败: %v,降级为普通对话", syncErr)
|
||
} else if len(syncResp.ToolCalls) > 0 {
|
||
log.Printf("[chat] 模型请求 %d 个工具调用", len(syncResp.ToolCalls))
|
||
|
||
// 将助手消息(含工具调用)加入上下文
|
||
assistantMsg := model.LLMMessage{
|
||
Role: model.RoleAssistant,
|
||
Content: syncResp.Content,
|
||
ToolCalls: syncResp.ToolCalls,
|
||
ReasoningContent: syncResp.ReasoningContent,
|
||
}
|
||
llmMessages = append(llmMessages, assistantMsg)
|
||
|
||
// 执行每个工具调用并将结果加入上下文
|
||
for _, tc := range syncResp.ToolCalls {
|
||
var args map[string]interface{}
|
||
if err := json.Unmarshal([]byte(tc.Arguments), &args); err != nil {
|
||
log.Printf("[chat] 工具 %s 参数解析失败: %v", tc.Name, err)
|
||
args = make(map[string]interface{})
|
||
}
|
||
|
||
result, execErr := toolRegistry.Execute(ctx, tc.Name, args)
|
||
if execErr != nil {
|
||
log.Printf("[chat] 工具 %s 执行失败: %v", tc.Name, execErr)
|
||
}
|
||
|
||
resultJSON, _ := json.Marshal(result)
|
||
llmMessages = append(llmMessages, model.LLMMessage{
|
||
Role: model.RoleTool,
|
||
Content: string(resultJSON),
|
||
ToolCallID: tc.ID,
|
||
})
|
||
}
|
||
}
|
||
// 无论是否有工具调用,继续流式输出最终回复
|
||
}
|
||
|
||
// 5.2 调用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. 缓存 LLM 回复到会话历史
|
||
if fullContent != "" {
|
||
ctxBuilder.CacheMessage(req.SessionID, model.RoleAssistant, fullContent)
|
||
}
|
||
|
||
// 9. 异步提取记忆
|
||
if memExtractor != nil && fullContent != "" {
|
||
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent)
|
||
}
|
||
|
||
}
|
||
|
||
|
||
// 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 {
|
||
log.Printf("[memory] 记忆检索器未初始化: 数据库不可用")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"user_id": userID,
|
||
"query": query,
|
||
"memories": []interface{}{},
|
||
"error": "记忆系统未就绪",
|
||
"errorType": "memory_store_unavailable",
|
||
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
|
||
})
|
||
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": fmt.Sprintf("检索失败: %v", err),
|
||
"errorType": "retrieve_failed",
|
||
})
|
||
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 {
|
||
log.Printf("[memory] 记忆存储未初始化: 数据库不可用")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"user_id": userID,
|
||
"memories": []interface{}{},
|
||
"error": "记忆系统未就绪",
|
||
"errorType": "memory_store_unavailable",
|
||
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
|
||
})
|
||
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": fmt.Sprintf("查询失败: %v", err),
|
||
"errorType": "query_failed",
|
||
})
|
||
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 {
|
||
log.Printf("[memory] 记忆存储未初始化: 无法删除")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusServiceUnavailable)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"error": "记忆系统未就绪",
|
||
"errorType": "memory_store_unavailable",
|
||
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
|
||
})
|
||
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": fmt.Sprintf("删除失败: %v", err),
|
||
"errorType": "delete_failed",
|
||
})
|
||
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 {
|
||
log.Printf("[memory] 记忆存储未初始化: 无法保存")
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusServiceUnavailable)
|
||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
"error": "记忆系统未就绪",
|
||
"errorType": "memory_store_unavailable",
|
||
"hint": "PostgreSQL 数据库不可用,请检查数据库连接配置",
|
||
})
|
||
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": fmt.Sprintf("保存失败: %v", err),
|
||
"errorType": "save_failed",
|
||
})
|
||
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)
|
||
}
|
||
|
||
}
|