package memory import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "time" "github.com/yourname/cyrene-ai/ai-core/internal/model" ) // Client 记忆服务 HTTP 客户端 // ai-core 通过此客户端调用独立的 memory-service type Client struct { baseURL string httpClient *http.Client } // NewClient 创建记忆服务客户端 func NewClient(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 15 * time.Second, }, } } // Ping 检查记忆服务是否可用 func (c *Client) Ping(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v1/health", nil) if err != nil { return err } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("记忆服务健康检查失败: %d", resp.StatusCode) } return nil } // Save 保存记忆 func (c *Client) Save(ctx context.Context, entry *model.MemoryEntry) error { body, _ := json.Marshal(map[string]interface{}{ "user_id": entry.UserID, "content": entry.Content, "summary": entry.Summary, "category": string(entry.Category), "priority": int(entry.Priority), "importance": entry.Importance, "keywords": entry.Keywords, "session_id": entry.SessionID, "source": entry.Source, }) resp, err := c.doRequest(ctx, http.MethodPost, c.baseURL+"/api/v1/memories", body) if err != nil { return fmt.Errorf("保存记忆失败: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("保存记忆失败 (%d): %s", resp.StatusCode, string(respBody)) } // 解析返回以获取 ID 和 CreatedAt var result struct { Memory *model.MemoryEntry `json:"memory"` } if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && result.Memory != nil { entry.ID = result.Memory.ID entry.CreatedAt = result.Memory.CreatedAt } return nil } // Query 按条件查询记忆 func (c *Client) Query(ctx context.Context, q model.MemoryQuery) ([]model.MemoryEntry, error) { url := fmt.Sprintf("%s/api/v1/memories?user_id=%s", c.baseURL, q.UserID) if q.Category != "" { url += "&category=" + string(q.Category) } if q.MinImportance > 0 { url += fmt.Sprintf("&min_importance=%d", q.MinImportance) } if q.Limit > 0 { url += fmt.Sprintf("&limit=%d", q.Limit) } resp, err := c.doRequest(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("查询记忆失败: %w", err) } defer resp.Body.Close() var result struct { Memories []model.MemoryEntry `json:"memories"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析查询结果失败: %w", err) } return result.Memories, nil } // QueryByText 语义查询(POST /api/v1/memories/query) func (c *Client) QueryByText(ctx context.Context, userID, queryText, category string, minImportance, limit int) ([]model.MemoryEntry, error) { body, _ := json.Marshal(map[string]interface{}{ "user_id": userID, "query_text": queryText, "category": category, "min_importance": minImportance, "limit": limit, }) resp, err := c.doRequest(ctx, http.MethodPost, c.baseURL+"/api/v1/memories/query", body) if err != nil { return nil, fmt.Errorf("语义查询失败: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("语义查询失败 (%d): %s", resp.StatusCode, string(respBody)) } var result struct { Memories []model.MemoryEntry `json:"memories"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析查询结果失败: %w", err) } return result.Memories, nil } // GetByID 根据ID获取记忆 func (c *Client) GetByID(ctx context.Context, id string) (*model.MemoryEntry, error) { resp, err := c.doRequest(ctx, http.MethodGet, c.baseURL+"/api/v1/memories/"+id, nil) if err != nil { return nil, fmt.Errorf("获取记忆失败: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, nil } if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("获取记忆失败 (%d): %s", resp.StatusCode, string(respBody)) } var result struct { Memory model.MemoryEntry `json:"memory"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析获取结果失败: %w", err) } return &result.Memory, nil } // Delete 删除记忆 func (c *Client) Delete(ctx context.Context, id string) error { resp, err := c.doRequest(ctx, http.MethodDelete, c.baseURL+"/api/v1/memories/"+id, nil) if err != nil { return fmt.Errorf("删除记忆失败: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("删除记忆失败 (%d): %s", resp.StatusCode, string(respBody)) } return nil } // GetMemoriesByCategory 按分类获取记忆 func (c *Client) GetMemoriesByCategory(ctx context.Context, userID string, category model.MemoryCategory) ([]model.MemoryEntry, error) { return c.Query(ctx, model.MemoryQuery{ UserID: userID, Category: category, Limit: 50, }) } // ConsolidateMemories 合并相似记忆 func (c *Client) ConsolidateMemories(ctx context.Context, userID string) (int, error) { body, _ := json.Marshal(map[string]interface{}{ "user_id": userID, }) resp, err := c.doRequest(ctx, http.MethodPost, c.baseURL+"/api/v1/memories/consolidate", body) if err != nil { return 0, fmt.Errorf("合并记忆失败: %w", err) } defer resp.Body.Close() var result struct { Merged int `json:"merged"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return 0, fmt.Errorf("解析合并结果失败: %w", err) } return result.Merged, nil } // DecayMemories 衰减旧记忆 func (c *Client) DecayMemories(ctx context.Context, userID string) (int, int, error) { body, _ := json.Marshal(map[string]interface{}{ "user_id": userID, }) resp, err := c.doRequest(ctx, http.MethodPost, c.baseURL+"/api/v1/memories/decay", body) if err != nil { return 0, 0, fmt.Errorf("衰减记忆失败: %w", err) } defer resp.Body.Close() var result struct { Decayed int `json:"decayed"` Deleted int `json:"deleted"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return 0, 0, fmt.Errorf("解析衰减结果失败: %w", err) } return result.Decayed, result.Deleted, nil } // GetCategories 获取用户类别统计 func (c *Client) GetCategories(ctx context.Context, userID string) (map[string]int, error) { url := fmt.Sprintf("%s/api/v1/memories/categories?user_id=%s", c.baseURL, userID) resp, err := c.doRequest(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("获取类别统计失败: %w", err) } defer resp.Body.Close() var result struct { Categories map[string]int `json:"categories"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析类别统计失败: %w", err) } return result.Categories, nil } // SaveThinkingLog 持久化自主思考日志到 memory-service func (c *Client) SaveThinkingLog(ctx context.Context, userID, content, toolCalls string, toolCallCount, contentLength int) error { body, _ := json.Marshal(map[string]interface{}{ "user_id": userID, "content": content, "tool_calls": toolCalls, "tool_call_count": toolCallCount, "content_length": contentLength, }) resp, err := c.doRequest(ctx, http.MethodPost, c.baseURL+"/api/v1/thinking", body) if err != nil { return fmt.Errorf("保存思考日志失败: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("保存思考日志失败 (%d): %s", resp.StatusCode, string(respBody)) } return nil } // IsReady 检查记忆服务是否就绪 func (c *Client) IsReady() bool { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() return c.Ping(ctx) == nil } // doRequest 内部 HTTP 请求辅助方法 func (c *Client) doRequest(ctx context.Context, method, url string, body []byte) (*http.Response, error) { var reqBody io.Reader if body != nil { reqBody = bytes.NewReader(body) } req, err := http.NewRequestWithContext(ctx, method, url, reqBody) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.httpClient.Do(req) if err != nil { log.Printf("[memory-client] HTTP 请求失败 %s %s: %v", method, url, err) return nil, err } return resp, nil }