feat: LLM可自主操作提醒 — reminder_create/list/delete 工具
- 新增 GatewayClient HTTP 客户端,调用 Gateway /api/v1/reminders API - 新增 3 个提醒工具:reminder_create / reminder_list / reminder_delete - main.go 注册提醒工具,需 GATEWAY_URL + INTERNAL_SERVICE_TOKEN 环境变量 - AutonomousToolPolicy 白名单加入提醒工具,允许自主思考中创建提醒 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,12 @@ func main() {
|
|||||||
log.Println("IoT 客户端未配置 (IOT_SERVICE_URL 和 IOT_DEBUG_SERVICE_URL 均为空)")
|
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())
|
hostSandbox := host.NewSandbox(host.DefaultSandboxConfig())
|
||||||
directBackend := host.NewDirectBackend(hostSandbox)
|
directBackend := host.NewDirectBackend(hostSandbox)
|
||||||
@@ -224,6 +230,14 @@ func main() {
|
|||||||
toolRegistry.Register(wrapTool(tools.NewOSSystemTool(osManager), "os_system", "OS System Info", "system"))
|
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
|
visionProvider = nil
|
||||||
if configLoader != nil && configLoader.HasConfig() {
|
if configLoader != nil && configLoader.HasConfig() {
|
||||||
cfg := configLoader.GetConfig()
|
cfg := configLoader.GetConfig()
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ func DefaultAutonomousToolPolicy() *AutonomousToolPolicy {
|
|||||||
"calculator", "datetime", "web_fetch",
|
"calculator", "datetime", "web_fetch",
|
||||||
"host_exec", "host_file", "host_system",
|
"host_exec", "host_file", "host_system",
|
||||||
"vision_analyze", "knowledge_search", "knowledge_ingest",
|
"vision_analyze", "knowledge_search", "knowledge_ingest",
|
||||||
|
"reminder_create", "reminder_list", "reminder_delete",
|
||||||
},
|
},
|
||||||
MaxToolCallsPerRound: 5,
|
MaxToolCallsPerRound: 5,
|
||||||
MaxHighRiskPerHour: 10,
|
MaxHighRiskPerHour: 10,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user