diff --git a/doc/SIGN_QUERY_FEATURE.md b/doc/SIGN_QUERY_FEATURE.md index 6d68b79..489d563 100644 --- a/doc/SIGN_QUERY_FEATURE.md +++ b/doc/SIGN_QUERY_FEATURE.md @@ -7,10 +7,11 @@ 1. **数据不准确**:AI 只能根据上下文猜测或给出模糊答案 2. **无法查询历史**:询问超过一天的数据时,AI 一问三不知 3. **功能不完整**:数据库层已有完善的查询能力(`CountSigns`、`CountSignsByDay`),但工具层未暴露给 AI +4. **状态判断不准**:用户问"我今天签到了吗"时,AI 基于对话记忆判断而非查询数据库,导致误判 ## 解决方案 -为签到工具添加查询功能,支持以下两种操作模式: +为签到工具添加查询和检查功能,支持以下三种操作模式: ### 1. 签到操作 (action=sign) @@ -21,13 +22,20 @@ ### 2. 查询操作 (action=query) -新增查询功能: +查询签到统计功能: - 查询指定日期或日期范围的签到统计 - 返回总人次和按天分组的统计数据 - 可选参数: - `date`: 查询日期(格式:YYYY-MM-DD),默认今天 - `days`: 查询最近 N 天,默认只查询 date 指定的那一天 +### 3. 检查操作 (action=check) **新增** + +检查当前节点今天是否已签到: +- 用于回答"我今天签到了吗"之类的问题 +- 直接查询数据库,不依赖对话历史 +- 返回明确的已签到/未签到状态 + ## 修改内容 ### 1. 扩展 SignStore 接口 @@ -82,6 +90,27 @@ type signParams struct { ### AI 调用示例 +#### 检查今天是否签到(check 操作) + +**用户**:"我今天签到了吗?" + +AI 调用: +```json +{ + "action": "check" +} +``` + +返回(未签到): +``` +Test Node 今天还没有签到。 +``` + +返回(已签到): +``` +Test Node 今天已经签到过了。 +``` + #### 查询今天的签到情况 ```json { diff --git a/internal/agents/sign/sign.go b/internal/agents/sign/sign.go index 17de9a5..d5ccaa2 100644 --- a/internal/agents/sign/sign.go +++ b/internal/agents/sign/sign.go @@ -45,9 +45,10 @@ func (t *Tool) Enabled() bool { return t.enabled && t.store != nil } // ToolDefinition returns the OpenAI tool definition func (t *Tool) ToolDefinition(description string) *model.Tool { - desc := "签到工具。支持签到和查询两种操作:\n" + + desc := "签到工具。支持三种操作:\n" + "1. 签到操作(action=sign):记录节点今日签到信息,每个节点每天只能签到一次。必填参数:地区、名字、设备\n" + - "2. 查询操作(action=query):查询签到统计。可按日期范围查询,默认查询今天。返回签到总数和按天的统计数据" + "2. 查询操作(action=query):查询签到统计。可按日期范围查询,默认查询今天。返回签到总数和按天的统计数据\n" + + "3. 检查操作(action=check):检查当前节点今天是否已签到。用于回答用户'我今天签到了吗'之类的问题" if description != "" { desc = description } @@ -61,8 +62,8 @@ func (t *Tool) ToolDefinition(description string) *model.Tool { "properties": map[string]any{ "action": map[string]any{ "type": "string", - "enum": []string{"sign", "query"}, - "description": "操作类型:sign=签到,query=查询签到统计", + "enum": []string{"sign", "query", "check"}, + "description": "操作类型:sign=签到,query=查询签到统计,check=检查今天是否已签到", }, "region": map[string]any{ "type": "string", @@ -122,10 +123,12 @@ func (t *Tool) Execute(ctx context.Context, args string, runtime agenttool.Runti switch strings.ToLower(strings.TrimSpace(params.Action)) { case "query": return t.executeQuery(ctx, params, runtime) + case "check": + return t.executeCheck(ctx, params, runtime) case "sign", "": return t.executeSign(ctx, params, runtime) default: - return "", fmt.Errorf("无效的操作类型:%s,只支持 sign 或 query", params.Action) + return "", fmt.Errorf("无效的操作类型:%s,只支持 sign、query 或 check", params.Action) } } @@ -176,6 +179,31 @@ func (t *Tool) executeSign(ctx context.Context, params signParams, runtime agent return fmt.Sprintf("签到成功!%s\n签到内容:%s", displayName(node), record.SignText), nil } +// executeCheck 检查当前节点今天是否已签到 +func (t *Tool) executeCheck(ctx context.Context, params signParams, runtime agenttool.Runtime) (string, error) { + // 节点身份来自消息上下文 + node, ok := agenttool.NodeContextFromContext(ctx) + if !ok || strings.TrimSpace(node.NodeID) == "" { + return "", fmt.Errorf("缺少发送节点上下文,无法检查签到状态") + } + + now := runtime.Now + if now.IsZero() { + now = time.Now() + } + + // 查询数据库检查今天是否已签到 + signed, err := t.store.HasSignedOnDay(node.NodeID, now) + if err != nil { + return "", fmt.Errorf("检查签到状态失败:%w", err) + } + + if signed { + return fmt.Sprintf("%s 今天已经签到过了。", displayName(node)), nil + } + return fmt.Sprintf("%s 今天还没有签到。", displayName(node)), nil +} + // executeQuery 执行查询操作 func (t *Tool) executeQuery(ctx context.Context, params signParams, runtime agenttool.Runtime) (string, error) { now := runtime.Now diff --git a/internal/agents/sign/sign_test.go b/internal/agents/sign/sign_test.go index d0868fb..02147cb 100644 --- a/internal/agents/sign/sign_test.go +++ b/internal/agents/sign/sign_test.go @@ -218,3 +218,100 @@ func TestSignTool_SignAction(t *testing.T) { t.Errorf("Expected 1 sign record, got %d", len(store.signs)) } } + +func TestSignTool_CheckAction(t *testing.T) { + now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC) + + // 测试场景1:今天未签到 + t.Run("今天未签到", func(t *testing.T) { + store := &mockSignStore{ + signs: []storepkg.SignRecord{}, + nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord), + } + + tool := &Tool{ + enabled: true, + store: store, + } + + params := signParams{ + Action: "check", + } + argsJSON, _ := json.Marshal(params) + + nodeCtx := agenttool.NodeContext{ + NodeID: "test_node_123", + LongName: "Test Node", + ShortName: "TN", + } + ctx := agenttool.WithNodeContext(context.Background(), nodeCtx) + + runtime := agenttool.Runtime{Now: now} + result, err := tool.Execute(ctx, string(argsJSON), runtime) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + t.Logf("Check result (not signed): %s", result) + + if !contains(result, "还没有签到") && !contains(result, "没有签到") { + t.Errorf("Expected result to indicate not signed yet") + } + }) + + // 测试场景2:今天已签到 + t.Run("今天已签到", func(t *testing.T) { + store := &mockSignStore{ + signs: []storepkg.SignRecord{ + { + NodeID: "test_node_123", + SignText: "上海-TestUser-TestDevice签到", + SignTime: now, + }, + }, + nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord), + } + + tool := &Tool{ + enabled: true, + store: store, + } + + params := signParams{ + Action: "check", + } + argsJSON, _ := json.Marshal(params) + + nodeCtx := agenttool.NodeContext{ + NodeID: "test_node_123", + LongName: "Test Node", + ShortName: "TN", + } + ctx := agenttool.WithNodeContext(context.Background(), nodeCtx) + + runtime := agenttool.Runtime{Now: now} + result, err := tool.Execute(ctx, string(argsJSON), runtime) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + t.Logf("Check result (already signed): %s", result) + + if !contains(result, "已经签到") { + t.Errorf("Expected result to indicate already signed") + } + }) +} + +func contains(s, substr string) bool { + return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr))) +} + +func containsMiddle(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +}