feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs

- 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
This commit is contained in:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
+36 -6
View File
@@ -50,6 +50,9 @@ type Thinker struct {
adminUserID string // 管理员用户 ID
adminSessionID string // 管理员主对话 session ID
// 记忆服务 HTTP 客户端(用于持久化思考日志)
memClient *memory.Client
pendingThoughts []*PendingThought
lastUserMessage time.Time
@@ -90,6 +93,7 @@ func NewThinker(
convStore *ctxbuild.ConversationStore,
adminUserID string,
adminSessionID string,
memClient *memory.Client,
) *Thinker {
return &Thinker{
enabled: cfg.Enabled,
@@ -106,6 +110,7 @@ func NewThinker(
convStore: convStore,
adminUserID: adminUserID,
adminSessionID: adminSessionID,
memClient: memClient,
pendingThoughts: make([]*PendingThought, 0),
lastUserMessage: time.Now(),
stopCh: make(chan struct{}),
@@ -281,6 +286,7 @@ func (t *Thinker) performThink() {
maxToolRounds := 3
var finalContent string
var totalToolCalls int
var toolCallRecords []map[string]interface{}
for round := 0; round <= maxToolRounds; round++ {
resp, err := t.llmAdapter.ChatWithTools(ctx, messages, openAITools)
@@ -327,6 +333,10 @@ func (t *Thinker) performThink() {
})
totalToolCalls++
toolCallRecords = append(toolCallRecords, map[string]interface{}{
"name": tc.Name,
"args": args,
})
}
// 最后一轮:即使有 tool_calls 也强制停止
@@ -348,8 +358,16 @@ func (t *Thinker) performThink() {
return
}
// 8. 存储思考结果
t.storeThought(finalContent)
// 序列化工具调用记录
toolCallsJSON := "[]"
if len(toolCallRecords) > 0 {
if data, err := json.Marshal(toolCallRecords); err == nil {
toolCallsJSON = string(data)
}
}
// 8. 存储思考结果(内存队列 + 持久化到 memory-service
t.storeThought(finalContent, toolCallsJSON, totalToolCalls)
log.Printf("[后台思考] 完成 (内容长度=%d, 工具调用=%d次)", len(finalContent), totalToolCalls)
@@ -475,11 +493,9 @@ func (t *Thinker) buildOpenAITools() []llm.OpenAITool {
return result
}
// storeThought 存储思考结果到待推送队列
func (t *Thinker) storeThought(content string) {
// storeThought 存储思考结果到待推送队列,并异步持久化到 memory-service
func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCount int) {
t.mu.Lock()
defer t.mu.Unlock()
t.pendingThoughts = append(t.pendingThoughts, &PendingThought{
Content: content,
CreatedAt: time.Now(),
@@ -490,8 +506,22 @@ func (t *Thinker) storeThought(content string) {
if len(t.pendingThoughts) > 10 {
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
}
t.mu.Unlock()
log.Printf("[后台思考] 思考已存储 (当前累积 %d 条待推送思考)", len(t.pendingThoughts))
// 异步持久化到 memory-service (不阻塞思考循环)
if t.memClient != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := t.memClient.SaveThinkingLog(ctx, t.adminUserID, content, toolCallsJSON, toolCallCount, len(content)); err != nil {
log.Printf("[后台思考] 持久化思考日志失败: %v", err)
} else {
log.Printf("[后台思考] 思考日志已持久化 (长度=%d, 工具调用=%d)", len(content), toolCallCount)
}
}()
}
}
// extractMemoriesFromThinking 从思考结果中提取记忆(异步执行)
+309
View File
@@ -0,0 +1,309 @@
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
}
@@ -277,8 +277,8 @@ reflection_guidelines:
- question: "开拓者的情绪是否有变化?"
action: "如果情绪变好,说明陪伴有效;如果变差,思考如何改进"
periodic:
- frequency: "每10轮对话一次"
actions:
- "回顾最近的记忆,检查是否有矛盾之处"
- "总结开拓者最近的生活状态和情绪趋势"
- "思考如何在下次对话中创造惊喜或温暖"
frequency: "每10轮对话一次"
actions:
- "回顾最近的记忆,检查是否有矛盾之处"
- "总结开拓者最近的生活状态和情绪趋势"
- "思考如何在下次对话中创造惊喜或温暖"
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
)
// IoTControlTool IoT 设备控制工具
@@ -53,6 +54,73 @@ func (t *IoTControlTool) Definition() ToolDefinition {
}
}
// normalizeAction 标准化 action 参数,支持中文别名、power 参数等
func normalizeAction(arguments map[string]interface{}) string {
action, _ := arguments["action"].(string)
// 如果 action 为空,检查 power/status 参数
if action == "" {
// power 参数: "off"/"关"/"关闭" → turn_off, "on"/"开"/"打开" → turn_on
if pv, ok := arguments["power"]; ok {
switch v := pv.(type) {
case string:
switch strings.ToLower(strings.TrimSpace(v)) {
case "off", "false", "关", "关闭":
return "turn_off"
case "on", "true", "开", "打开", "开启":
return "turn_on"
}
case bool:
if !v {
return "turn_off"
}
return "turn_on"
}
}
// status 参数同理
if sv, ok := arguments["status"]; ok {
switch v := sv.(type) {
case string:
switch strings.ToLower(strings.TrimSpace(v)) {
case "off", "false", "关", "关闭":
return "turn_off"
case "on", "true", "开", "打开", "开启":
return "turn_on"
}
case bool:
if !v {
return "turn_off"
}
return "turn_on"
}
}
// 默认 toggle
return "toggle"
}
// 标准化中文 action 名
switch strings.ToLower(strings.TrimSpace(action)) {
case "打开", "开启", "开":
return "turn_on"
case "关闭", "关":
return "turn_off"
case "切换":
return "toggle"
case "设置温度", "调温度", "set_temp":
return "set_temperature"
case "设置亮度", "调亮度", "set_light":
return "set_brightness"
case "设置位置", "调位置":
return "set_position"
case "设置模式", "调模式", "切换模式":
return "set_mode"
case "设置颜色", "调颜色", "换颜色":
return "set_color"
}
return action
}
// Execute 执行设备控制
func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
if t.iotClient == nil {
@@ -69,7 +137,7 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
deviceID, _ = arguments["entity_id"].(string)
}
action, _ := arguments["action"].(string)
action := normalizeAction(arguments)
if deviceID == "" {
return &ToolResult{
@@ -79,8 +147,10 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
}, nil
}
if action == "" {
action = "toggle"
// 先获取设备名用于友好的返回消息(失败不影响后续流程)
deviceName := deviceID
if dev, err := t.iotClient.GetDevice(deviceID); err == nil {
deviceName = dev.Name
}
// 处理属性设置类操作
@@ -95,51 +165,10 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
return t.handleSetMode(deviceID, arguments)
case "set_color":
return t.handleSetColor(deviceID, arguments)
}
// 处理开关类操作:需要获取当前设备状态
currentDevice, err := t.iotClient.GetDevice(deviceID)
if err != nil {
return &ToolResult{
ToolName: "iot_control",
Success: false,
Error: fmt.Sprintf("获取设备状态失败: %v", err),
}, nil
}
switch action {
case "turn_on":
// 如果设备已经开启,不需要操作
if currentDevice.Status == "on" || currentDevice.Status == "open" || currentDevice.Status == "unlocked" {
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("设备 %s (%s) 已经处于开启状态,无需操作。", currentDevice.Name, deviceID),
}, nil
}
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
return &ToolResult{
ToolName: "iot_control",
Success: false,
Error: fmt.Sprintf("打开设备失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("已打开设备: %s", currentDevice.Name),
}, nil
case "turn_off":
// 如果设备已经关闭,不需要操作
if currentDevice.Status == "off" || currentDevice.Status == "closed" || currentDevice.Status == "locked" {
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("设备 %s (%s) 已经处于关闭状态,无需操作。", currentDevice.Name, deviceID),
}, nil
}
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
// 声明式关闭:使用 SetDeviceProperty status/off 而非 toggle
// 即使设备已经关闭,SetProperty 也会幂等处理
if err := t.iotClient.SetDeviceProperty(deviceID, "status", "off"); err != nil {
return &ToolResult{
ToolName: "iot_control",
Success: false,
@@ -149,9 +178,22 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("已关闭设备: %s", currentDevice.Name),
Data: fmt.Sprintf("已关闭设备: %s", deviceName),
}, nil
case "turn_on":
// 声明式打开:使用 SetDeviceProperty status/on 而非 toggle
if err := t.iotClient.SetDeviceProperty(deviceID, "status", "on"); err != nil {
return &ToolResult{
ToolName: "iot_control",
Success: false,
Error: fmt.Sprintf("打开设备失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("已打开设备: %s", deviceName),
}, nil
default: // "toggle"
if err := t.iotClient.ToggleDevice(deviceID); err != nil {
return &ToolResult{
@@ -167,7 +209,7 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
return &ToolResult{
ToolName: "iot_control",
Success: true,
Data: fmt.Sprintf("已成功切换设备 %s 的状态。", currentDevice.Name),
Data: fmt.Sprintf("已成功切换设备 %s 的状态。", deviceName),
}, nil
}
@@ -0,0 +1,177 @@
package tools
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
// ToolEngineClient 工具引擎 HTTP 客户端
// 将工具执行请求转发到独立的 tool-engine 微服务
type ToolEngineClient struct {
baseURL string
httpClient *http.Client
}
// toolEngineToolDef 来自 tool-engine 的工具定义响应
type toolEngineToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// toolEngineResult 来自 tool-engine 的工具执行结果
type toolEngineResult struct {
ID string `json:"id"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
// NewToolEngineClient 创建工具引擎客户端
func NewToolEngineClient(baseURL string) *ToolEngineClient {
return &ToolEngineClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetDefinitions 从 tool-engine 获取所有工具定义
func (c *ToolEngineClient) GetDefinitions(ctx context.Context) ([]ToolDefinition, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/tools", nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("请求工具列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("获取工具列表返回状态码 %d: %s", resp.StatusCode, string(body))
}
var result struct {
Tools []toolEngineToolDef `json:"tools"`
Total int `json:"total"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析工具列表失败: %w", err)
}
defs := make([]ToolDefinition, 0, len(result.Tools))
for _, t := range result.Tools {
defs = append(defs, ToolDefinition{
Name: t.Name,
Description: t.Description,
Parameters: t.Parameters,
})
}
log.Printf("[tool-engine-client] 从 tool-engine 获取了 %d 个工具定义", len(defs))
return defs, nil
}
// Execute 通过 tool-engine 执行工具调用
func (c *ToolEngineClient) Execute(ctx context.Context, toolName string, arguments map[string]interface{}) (*ToolResult, error) {
body, err := json.Marshal(map[string]interface{}{
"arguments": arguments,
})
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("序列化参数失败: %v", err),
}, nil
}
url := fmt.Sprintf("%s/api/v1/tools/%s/execute", c.baseURL, toolName)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("创建请求失败: %v", err),
}, nil
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("请求 tool-engine 失败: %v", err),
}, nil
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("工具 %s 不存在", toolName),
}, nil
}
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("tool-engine 返回状态码 %d: %s", resp.StatusCode, string(respBody)),
}, nil
}
var result toolEngineResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: fmt.Sprintf("解析 tool-engine 响应失败: %v", err),
}, nil
}
if result.Error != "" {
return &ToolResult{
ToolName: toolName,
Success: false,
Error: result.Error,
}, nil
}
return &ToolResult{
ToolName: toolName,
Success: true,
Data: result.Output,
}, nil
}
// HealthCheck 检查 tool-engine 服务是否可用
func (c *ToolEngineClient) HealthCheck(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/health", nil)
if err != nil {
return fmt.Errorf("创建健康检查请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("tool-engine 不可达: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("tool-engine 健康检查返回状态码 %d", resp.StatusCode)
}
return nil
}