diff --git a/backend/ai-core/cmd/main.go b/backend/ai-core/cmd/main.go index 3570215..579f95a 100644 --- a/backend/ai-core/cmd/main.go +++ b/backend/ai-core/cmd/main.go @@ -158,6 +158,12 @@ func main() { log.Println("IoT 客户端未配置 (IOT_SERVICE_URL 和 IOT_DEBUG_SERVICE_URL 均为空)") } + // 初始化 Gateway HTTP 客户端(提醒/日程等工具用) + gwBaseURL := getEnv("GATEWAY_URL", "http://localhost:8080") + gwToken := os.Getenv("INTERNAL_SERVICE_TOKEN") + gwClient := tools.NewGatewayClient(gwBaseURL, gwToken) + log.Printf("Gateway 客户端已就绪: %s", gwBaseURL) + // 初始化主机操控管理器 (沙箱执行 + 文件系统隔离) hostSandbox := host.NewSandbox(host.DefaultSandboxConfig()) directBackend := host.NewDirectBackend(hostSandbox) @@ -224,6 +230,14 @@ func main() { toolRegistry.Register(wrapTool(tools.NewOSSystemTool(osManager), "os_system", "OS System Info", "system")) } + // 提醒/日程工具(需要 Gateway 客户端) + if gwClient != nil && gwToken != "" { + toolRegistry.Register(wrapTool(tools.NewReminderCreateTool(gwClient, adminUserID), "reminder_create", "Create Reminder", "reminder")) + toolRegistry.Register(wrapTool(tools.NewReminderListTool(gwClient, adminUserID), "reminder_list", "List Reminders", "reminder")) + toolRegistry.Register(wrapTool(tools.NewReminderDeleteTool(gwClient, adminUserID), "reminder_delete", "Delete Reminder", "reminder")) + log.Println("[工具] 提醒工具已注册 (reminder_create, reminder_list, reminder_delete)") + } + visionProvider = nil if configLoader != nil && configLoader.HasConfig() { cfg := configLoader.GetConfig() diff --git a/backend/ai-core/internal/background/thinker.go b/backend/ai-core/internal/background/thinker.go index 310d073..c9b98d6 100644 --- a/backend/ai-core/internal/background/thinker.go +++ b/backend/ai-core/internal/background/thinker.go @@ -211,6 +211,7 @@ func DefaultAutonomousToolPolicy() *AutonomousToolPolicy { "calculator", "datetime", "web_fetch", "host_exec", "host_file", "host_system", "vision_analyze", "knowledge_search", "knowledge_ingest", + "reminder_create", "reminder_list", "reminder_delete", }, MaxToolCallsPerRound: 5, MaxHighRiskPerHour: 10, diff --git a/backend/ai-core/internal/tools/gateway_client.go b/backend/ai-core/internal/tools/gateway_client.go new file mode 100644 index 0000000..fd69e90 --- /dev/null +++ b/backend/ai-core/internal/tools/gateway_client.go @@ -0,0 +1,123 @@ +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// GatewayClient is a lightweight HTTP client for calling Gateway internal APIs +// (reminders, schedules, etc.) from ai-core tools. +type GatewayClient struct { + baseURL string + internalToken string + httpClient *http.Client +} + +// NewGatewayClient creates a new Gateway API client. +func NewGatewayClient(baseURL, internalToken string) *GatewayClient { + return &GatewayClient{ + baseURL: baseURL, + internalToken: internalToken, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// Reminder represents a reminder from the Gateway API. +type Reminder struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + RemindAt string `json:"remind_at"` + Status string `json:"status"` + RepeatType string `json:"repeat_type"` +} + +// CreateReminder calls POST /api/v1/reminders on the Gateway. +func (c *GatewayClient) CreateReminder(ctx context.Context, userID, title, description, remindAt, repeatType, sessionID string) (*Reminder, error) { + body := map[string]interface{}{ + "user_id": userID, + "title": title, + "description": description, + "remind_at": remindAt, + "repeat_type": repeatType, + "session_id": sessionID, + } + reqBody, _ := json.Marshal(body) + + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/v1/reminders", bytes.NewReader(reqBody)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Internal-Token", c.internalToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("gateway request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var reminder Reminder + if err := json.NewDecoder(resp.Body).Decode(&reminder); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + return &reminder, nil +} + +// ListReminders calls GET /api/v1/reminders on the Gateway. +func (c *GatewayClient) ListReminders(ctx context.Context, userID, status string, limit int) ([]Reminder, error) { + url := fmt.Sprintf("%s/api/v1/reminders?user_id=%s&status=%s&limit=%d", c.baseURL, userID, status, limit) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Internal-Token", c.internalToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("gateway request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var reminders []Reminder + if err := json.NewDecoder(resp.Body).Decode(&reminders); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + return reminders, nil +} + +// DeleteReminder calls DELETE /api/v1/reminders/:id on the Gateway. +func (c *GatewayClient) DeleteReminder(ctx context.Context, reminderID string) error { + req, err := http.NewRequestWithContext(ctx, "DELETE", c.baseURL+"/api/v1/reminders/"+reminderID, nil) + if err != nil { + return err + } + req.Header.Set("X-Internal-Token", c.internalToken) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("gateway request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(bodyBytes)) + } + return nil +} diff --git a/backend/ai-core/internal/tools/reminder_tool.go b/backend/ai-core/internal/tools/reminder_tool.go new file mode 100644 index 0000000..8b3db9c --- /dev/null +++ b/backend/ai-core/internal/tools/reminder_tool.go @@ -0,0 +1,152 @@ +package tools + +import ( + "context" + "fmt" + "strings" +) + +// --- ReminderCreateTool --- + +type ReminderCreateTool struct { + gw *GatewayClient + adminID string +} + +func NewReminderCreateTool(gw *GatewayClient, adminID string) *ReminderCreateTool { + return &ReminderCreateTool{gw: gw, adminID: adminID} +} + +func (t *ReminderCreateTool) Definition() ToolDefinition { + return ToolDefinition{ + Name: "reminder_create", + Description: "创建一个定时提醒。可用于提醒用户做某事、定时通知等。支持重复类型(none/daily/weekly/monthly)。时间格式为 ISO8601(如 2026-06-23T08:00:00+08:00)。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "title": map[string]string{"type": "string", "description": "提醒标题"}, + "description": map[string]string{"type": "string", "description": "提醒详细描述(可选)"}, + "remind_at": map[string]string{"type": "string", "description": "提醒时间,ISO8601 格式,如 2026-06-23T08:00:00+08:00"}, + "repeat_type": map[string]string{"type": "string", "description": "重复类型:none(不重复), daily(每天), weekly(每周), monthly(每月),默认 none"}, + }, + "required": []string{"title", "remind_at"}, + }, + } +} + +func (t *ReminderCreateTool) Execute(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { + title, _ := args["title"].(string) + description, _ := args["description"].(string) + remindAt, _ := args["remind_at"].(string) + repeatType, _ := args["repeat_type"].(string) + if repeatType == "" { + repeatType = "none" + } + + if title == "" || remindAt == "" { + return &ToolResult{Success: false, Error: "title 和 remind_at 为必填项"}, nil + } + + reminder, err := t.gw.CreateReminder(ctx, t.adminID, title, description, remindAt, repeatType, "") + if err != nil { + return &ToolResult{Success: false, Error: err.Error()}, nil + } + + return &ToolResult{ + Success: true, + Data: fmt.Sprintf("✅ 提醒已创建\nID: %s\n标题: %s\n时间: %s\n重复: %s", reminder.ID, reminder.Title, reminder.RemindAt, reminder.RepeatType), + }, nil +} + +// --- ReminderListTool --- + +type ReminderListTool struct { + gw *GatewayClient + adminID string +} + +func NewReminderListTool(gw *GatewayClient, adminID string) *ReminderListTool { + return &ReminderListTool{gw: gw, adminID: adminID} +} + +func (t *ReminderListTool) Definition() ToolDefinition { + return ToolDefinition{ + Name: "reminder_list", + Description: "列出当前用户的提醒列表,可按状态过滤(pending/completed/cancelled)。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "status": map[string]string{"type": "string", "description": "过滤状态:pending(待执行), completed(已完成), cancelled(已取消),默认为空(全部)"}, + "limit": map[string]interface{}{"type": "integer", "description": "返回条数上限,默认 20"}, + }, + }, + } +} + +func (t *ReminderListTool) Execute(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { + status, _ := args["status"].(string) + limit := 20 + if l, ok := args["limit"].(float64); ok && l > 0 { + limit = int(l) + } + + reminders, err := t.gw.ListReminders(ctx, t.adminID, status, limit) + if err != nil { + return &ToolResult{Success: false, Error: err.Error()}, nil + } + + if len(reminders) == 0 { + return &ToolResult{Success: true, Data: "📋 暂无提醒"}, nil + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("📋 提醒列表(共 %d 条):\n", len(reminders))) + for _, r := range reminders { + icon := "⏳" + if r.Status == "completed" { + icon = "✅" + } else if r.Status == "cancelled" { + icon = "❌" + } + sb.WriteString(fmt.Sprintf("%s [%s] %s — %s (ID: %s)\n", icon, r.Status, r.Title, r.RemindAt, r.ID)) + } + return &ToolResult{Success: true, Data: sb.String()}, nil +} + +// --- ReminderDeleteTool --- + +type ReminderDeleteTool struct { + gw *GatewayClient + adminID string +} + +func NewReminderDeleteTool(gw *GatewayClient, adminID string) *ReminderDeleteTool { + return &ReminderDeleteTool{gw: gw, adminID: adminID} +} + +func (t *ReminderDeleteTool) Definition() ToolDefinition { + return ToolDefinition{ + Name: "reminder_delete", + Description: "删除一个提醒。只能删除自己创建的提醒。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "reminder_id": map[string]string{"type": "string", "description": "要删除的提醒 ID"}, + }, + "required": []string{"reminder_id"}, + }, + } +} + +func (t *ReminderDeleteTool) Execute(ctx context.Context, args map[string]interface{}) (*ToolResult, error) { + reminderID, _ := args["reminder_id"].(string) + if reminderID == "" { + return &ToolResult{Success: false, Error: "reminder_id 为必填项"}, nil + } + + if err := t.gw.DeleteReminder(ctx, reminderID); err != nil { + return &ToolResult{Success: false, Error: err.Error()}, nil + } + + return &ToolResult{Success: true, Data: fmt.Sprintf("✅ 提醒 %s 已删除", reminderID)}, nil +}