package helpers import ( "context" "encoding/json" "fmt" "net/url" "os" "path/filepath" "strconv" "strings" "time" "github.com/fatih/color" "github.com/spf13/cobra" ) // ────────────────────────────────────────────────────────── // dws todo — 待办 // MCP 工具名与入参以 tools/list 为准;待办详情 query_todo_detail、删除 delete_todo,入参 taskId。 // ────────────────────────────────────────────────────────── const todoListPageSizeMax = 20 var todoFileMD5Hex = fileMD5Hex func ensureTodoTaskExists(ctx context.Context, taskID string) error { text, err := callMCPToolReturnTextOnServer(ctx, "todo", "get_todo_detail", map[string]any{ "taskId": taskID, }) if err != nil { return fmt.Errorf("待办任务 %q 不存在或不可访问: %w", taskID, err) } var body map[string]any if err := json.Unmarshal([]byte(text), &body); err != nil { return fmt.Errorf("校验待办任务 %q 时无法解析详情响应: %w", taskID, err) } result, ok := body["result"].(map[string]any) if !ok { return fmt.Errorf("待办任务 %q 不存在或详情响应缺少 result", taskID) } detail, ok := result["todoDetailModel"].(map[string]any) if !ok { return fmt.Errorf("待办任务 %q 不存在或详情响应缺少 todoDetailModel", taskID) } returnedTaskID := stringFromJSONScalar(detail["taskId"]) if returnedTaskID == "" || returnedTaskID != taskID { return fmt.Errorf("待办任务 %q 不存在或详情响应中的 taskId 不匹配", taskID) } return nil } func newTodoCommand() *cobra.Command { todoCmd := &cobra.Command{ Use: "todo", Short: "待办任务管理", Long: `管理钉钉个人待办:创建、查询列表、查看详情、修改、标记完成、删除。`, RunE: groupRunE, } todoTaskCmd := &cobra.Command{Use: "task", Short: "创建 / 查询 / 更新 / 删除待办", RunE: groupRunE} todoTaskCreateCmd := &cobra.Command{ Use: "create", Short: "创建待办", Example: ` dws todo task create --title "修复线上Bug" --executors userId1,userId2 --priority 40 dws todo task create --title "提交报告" --executors userId1 --due "2026-03-10T18:00:00+08:00" # 查询 userId: dws contact user search --keyword "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := rejectUnsupportedTodoReminderFlags(cmd); err != nil { return err } if err := validateRequiredFlagWithAliases(cmd, "title", "subject", "content"); err != nil { return err } if err := validateRequiredFlags(cmd, "executors"); err != nil { return err } executorsStr := mustGetFlag(cmd, "executors") executorIds := parseExecutorIds(executorsStr) toolArgs := map[string]any{ "PersonalTodoCreateVO": map[string]any{ "subject": flagOrFallback(cmd, "title", "subject", "content"), "executorIds": executorIds, }, } if v, _ := cmd.Flags().GetString("due"); v != "" { ms, err := parseISOTimeToMillis("due", v) if err != nil { return err } toolArgs["PersonalTodoCreateVO"].(map[string]any)["dueTime"] = ms } if v, _ := cmd.Flags().GetString("priority"); v != "" { if n, err := strconv.Atoi(v); err == nil { toolArgs["PersonalTodoCreateVO"].(map[string]any)["priority"] = n } } if v, _ := cmd.Flags().GetString("recurrence"); v != "" { toolArgs["PersonalTodoCreateVO"].(map[string]any)["recurrence"] = v } return callMCPTool("create_personal_todo", toolArgs) }, } todoTaskCreateSubCmd := &cobra.Command{ Use: "create-sub", Short: "创建子待办", Example: ` dws todo task create-sub --parent-id --title "子任务标题" --executors userId1,userId2 --priority 40 dws todo task create-sub --parent-id --title "子任务标题" --executors userId1 --due "2026-03-10T18:00:00+08:00" # 查询 parentId: dws todo task list # 查询 userId: dws contact user search --keyword "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := rejectUnsupportedTodoReminderFlags(cmd); err != nil { return err } if err := validateRequiredFlagWithAliases(cmd, "title", "subject", "content"); err != nil { return err } if err := validateRequiredFlags(cmd, "executors", "parent-id"); err != nil { return err } parentId := mustGetFlag(cmd, "parent-id") if _, err := strconv.ParseInt(parentId, 10, 64); err != nil { return &CLIError{ Code: CodeMissingParam, Message: fmt.Sprintf("父待办 ID 必须是纯数字,当前值: %s", parentId), Suggestion: "请通过 'dws todo task list' 获取正确的父待办任务 ID。", Operation: "todo.task.create-sub.parent-id", } } executorsStr := mustGetFlag(cmd, "executors") executorIds := parseExecutorIds(executorsStr) toolArgs := map[string]any{ "PersonalTodoCreateVO": map[string]any{ "subject": flagOrFallback(cmd, "title", "subject", "content"), "executorIds": executorIds, "parentId": mustGetFlag(cmd, "parent-id"), }, } if v, _ := cmd.Flags().GetString("due"); v != "" { ms, err := parseISOTimeToMillis("due", v) if err != nil { return err } toolArgs["PersonalTodoCreateVO"].(map[string]any)["dueTime"] = ms } if v, _ := cmd.Flags().GetString("priority"); v != "" { if n, err := strconv.Atoi(v); err == nil { toolArgs["PersonalTodoCreateVO"].(map[string]any)["priority"] = n } } if v, _ := cmd.Flags().GetString("recurrence"); v != "" { toolArgs["PersonalTodoCreateVO"].(map[string]any)["recurrence"] = v } return callMCPTool("create_personal_sub_todo", toolArgs) }, } todoTaskListCmd := &cobra.Command{ Use: "list", Short: "查询待办列表", Example: ` dws todo task list --page 1 --size 20 --status false --priority 40,30,10 --role-types creator,executor,participant --plan-finish-date-start 2026-03-01T00:00:00+08:00 --plan-finish-date-end 2026-03-10T18:00:00+08:00 `, RunE: func(cmd *cobra.Command, args []string) error { pageStr := mustGetFlag(cmd, "page") sizeStr := mustGetFlag(cmd, "size") size, err := strconv.Atoi(sizeStr) if err != nil || size < 1 { size = 20 } if size <= todoListPageSizeMax { toolArgs := map[string]any{ "pageNum": pageStr, "pageSize": sizeStr, } err := buildListTodoTaskArgs(cmd, toolArgs) if err != nil { return err } return callMCPTool("get_user_todos_in_current_org", toolArgs) } return todoListAutoPage(cmd, pageStr, size) }, } todoTaskListSubCmd := &cobra.Command{ Use: "list-sub", Short: "查询子待办列表", Example: ` dws todo task list-sub --task-id # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } taskId := mustGetFlag(cmd, "task-id") toolArgs := map[string]any{ "taskId": taskId, } return callMCPTool("list_sub_tasks", map[string]any{ "todoSubTaskListRequest": toolArgs, }) }, } todoTaskUpdateCmd := &cobra.Command{ Use: "update", Short: "修改待办任务", Example: ` dws todo task update --task-id --title "新标题" dws todo task update --task-id --priority 40 --due "2026-03-10T18:00:00+08:00" dws todo task update --task-id --done true # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := rejectUnsupportedTodoReminderFlags(cmd); err != nil { return err } if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } inner := map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), } if v, _ := cmd.Flags().GetString("title"); v != "" { inner["subject"] = v } if v, _ := cmd.Flags().GetString("due"); v != "" { ms, err := parseISOTimeToMillis("due", v) if err != nil { return err } inner["dueTime"] = ms } if v, _ := cmd.Flags().GetString("priority"); v != "" { if n, err := strconv.Atoi(v); err == nil { inner["priority"] = n } } if v, _ := cmd.Flags().GetString("done"); v != "" { inner["isDone"] = v == "true" } return callMCPTool("update_todo_task", map[string]any{ "TodoUpdateRequest": inner, }) }, } todoTaskDoneCmd := &cobra.Command{ Use: "done", Short: "修改执行者的待办完成状态", Example: ` dws todo task done --task-id --status true dws todo task done --task-id --status false # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "status"); err != nil { return err } taskID := mustGetFlag(cmd, "task-id") if !deps.Caller.DryRun() { if err := ensureTodoTaskExists(cmd.Context(), taskID); err != nil { return err } } return callMCPTool("update_todo_done_status", map[string]any{ "taskId": taskID, "isDone": mustGetFlag(cmd, "status"), }) }, } // todoTaskGetCmd 对应 MCP 待办详情;入参以 tools/list 为准,当前为 taskId。 todoTaskGetCmd := &cobra.Command{ Use: "get", Short: "待办详情", Example: ` dws todo task get --task-id # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } raw, err := callMCPToolReturnText(context.Background(), "get_todo_detail", map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), }) if err != nil { return err } if raw == "" { return nil } var parsed map[string]any if err := json.Unmarshal([]byte(raw), &parsed); err != nil { deps.Out.PrintRaw(raw) return nil } // Transform detailUrl appUrl and pcUrl to dingtalk protocol links if r, ok := parsed["result"].(map[string]any); ok { if model, ok := r["todoDetailModel"].(map[string]any); ok { if detailUrl, ok := model["detailUrl"].(map[string]any); ok { const prefix = "dingtalk://dingtalkclient/page/link?pc_slide=true&url=" if appUrl, ok := detailUrl["appUrl"].(string); ok && appUrl != "" { detailUrl["appUrl"] = prefix + url.QueryEscape(appUrl) } if pcUrl, ok := detailUrl["pcUrl"].(string); ok && pcUrl != "" { detailUrl["pcUrl"] = prefix + url.QueryEscape(pcUrl) } } } } return deps.Out.PrintJSON(parsed) }, } // todoTaskDeleteCmd 对应 MCP 删除待办;入参以 tools/list 为准,当前为 taskId。 todoTaskDeleteCmd := &cobra.Command{ Use: "delete", Short: "删除待办", Example: ` dws todo task delete --task-id dws todo task delete --task-id --yes # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } taskId := mustGetFlag(cmd, "task-id") if !confirmDelete("待办", taskId) { return nil } return callMCPTool("delete_todo", map[string]any{ "taskId": taskId, }) }, } todoTaskAddExecutorCmd := &cobra.Command{ Use: "add-executor", Short: "添加待办执行人", Example: ` dws todo task add-executor --task-id --executors , # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "executors"); err != nil { return err } executorsStr := mustGetFlag(cmd, "executors") executorIds := parseExecutorIds(executorsStr) return callMCPTool("add_task_executors", map[string]any{ "todoExecutorsAddRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "executorIds": executorIds, }, }) }, } todoTaskRemoveExecutorCmd := &cobra.Command{ Use: "remove-executor", Short: "移除待办执行人", Example: ` dws todo task remove-executor --task-id --executors , # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "executors"); err != nil { return err } executorsStr := mustGetFlag(cmd, "executors") executorIds := parseExecutorIds(executorsStr) return callMCPTool("remove_task_executors", map[string]any{ "todoExecutorsRemoveRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "executorIds": executorIds, }, }) }, } todoTaskAddParticipantCmd := &cobra.Command{ Use: "add-participant", Short: "添加待办参与人", Example: ` dws todo task add-participant --task-id --participants , # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "participants"); err != nil { return err } participantsStr := mustGetFlag(cmd, "participants") participantIds := parseExecutorIds(participantsStr) return callMCPTool("add_task_participants", map[string]any{ "todoParticipantsAddRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "participantIds": participantIds, }, }) }, } todoTaskRemoveParticipantCmd := &cobra.Command{ Use: "remove-participant", Short: "移除待办参与人", Example: ` dws todo task remove-participant --task-id --participants , # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "participants"); err != nil { return err } participantsStr := mustGetFlag(cmd, "participants") participantIds := parseExecutorIds(participantsStr) return callMCPTool("remove_task_participants", map[string]any{ "todoParticipantsRemoveRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "participantIds": participantIds, }, }) }, } todoTaskAddReminderCmd := &cobra.Command{ Use: "add-reminder", Short: "添加待办提醒", Example: ` dws todo task add-reminder --task-id --base-time dueTime --due-date-offset -30 dws todo task add-reminder --task-id --base-time customTime --reminder-time-stamp 2026-03-10T18:00:00+08:00 # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "base-time"); err != nil { return err } baseTime := strings.TrimSpace(mustGetFlag(cmd, "base-time")) dueDateOffset := any(nil) reminderTimeStamp := any(nil) switch baseTime { case "dueTime": value := strings.TrimSpace(mustGetFlag(cmd, "due-date-offset")) if value == "" { return fmt.Errorf("--due-date-offset is required when --base-time=dueTime") } dueDateOffset = value case "customTime": value, _ := cmd.Flags().GetString("reminder-time-stamp") value = strings.TrimSpace(value) if value == "" { return fmt.Errorf("--reminder-time-stamp is required when --base-time=customTime") } ms, err := parseISOTimeToMillis("reminder-time-stamp", value) if err != nil { return err } reminderTimeStamp = ms default: return fmt.Errorf("--base-time must be one of dueTime or customTime, got %q", baseTime) } return callMCPTool("add_todo_reminder", map[string]any{ "todoReminderAddRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "baseTime": baseTime, "dueDateOffset": dueDateOffset, "reminderTimeStamp": reminderTimeStamp, }, }) }, } todoTaskUpdateReminderCmd := &cobra.Command{ Use: "reset-reminder", Short: "重置待办提醒", Example: ` dws todo task reset-reminder --task-id dws todo task reset-reminder --task-id --reminder-rules # 查询 taskId: dws todo task list # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } var reminderRules []any if v, _ := cmd.Flags().GetString("reminder-rules"); v != "" { var att []any if err := json.Unmarshal([]byte(v), &att); err == nil && len(att) > 0 { for i, item := range att { m, ok := item.(map[string]any) if !ok { continue } if m["baseTime"] == "customTime" { if ts, ok := m["reminderTimeStamp"].(string); ok && ts != "" { ms, err := parseISOTimeToMillis("reminderTimeStamp", ts) if err != nil { return err } m["reminderTimeStamp"] = ms att[i] = m } } } reminderRules = att } } return callMCPTool("reset_todo_reminder", map[string]any{ "todoReminderUpdateRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "reminderRules": reminderRules, }, }) }, } todoTaskAddAttachment := &cobra.Command{ Use: "add-attachment", Short: "上传待办附件", Long: `上传待办附件 ⚠️ 重要:该接口会上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。`, Example: ` dws todo task add-attachment --task-id --file-path # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "file-path"); err != nil { return err } filePath, _ := cmd.Flags().GetString("file-path") meta, err := buildTodoLocalFileMeta(filePath, "", "") if err != nil { return err } if deps.Caller.DryRun() { deps.Out.PrintKeyValue("操作", "上传本地文件并添加为待办附件") deps.Out.PrintKeyValue("文件", meta.LocalPath) deps.Out.PrintKeyValue("名称", meta.FileName) deps.Out.PrintKeyValue("大小", fmt.Sprintf("%d bytes", meta.FileSize)) return nil } ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Minute) defer cancel() commitText, err := uploadTodoLocalFile(ctx, meta) if err != nil { return err } dentryId, spaceId, err := parseTodoFileSendIDs(commitText) if err != nil { return err } var attachments []any attachments = append(attachments, map[string]any{ "fileId": strconv.FormatInt(dentryId, 10), "fileName": meta.FileName, "fileSize": meta.FileSize, "spaceId": strconv.FormatInt(spaceId, 10), "fileType": meta.FileType, }) return callMCPTool("add_todo_attachment", map[string]any{ "todoAttachmentAddRequest": map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "attachmentList": attachments, }, }) }, } todoTaskListAttachmentCmd := &cobra.Command{ Use: "list-attachment", Short: "查询待办任务的附件列表", Example: ` dws todo task list-attachment --task-id # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } taskID := mustGetFlag(cmd, "task-id") if !deps.Caller.DryRun() { if err := ensureTodoTaskExists(cmd.Context(), taskID); err != nil { return err } } return callMCPTool("list_todo_attachment", map[string]any{ "todoAttachmentListRequest": map[string]any{ "taskId": taskID, }, }) }, } todoTaskRemoveAttachmentCmd := &cobra.Command{ Use: "remove-attachment", Short: "删除待办任务的附件", Example: ` dws todo task remove-attachment --task-id --attachment-id # 查询 taskId: dws todo task list # 查询 attachmentId: dws todo task list-attachment --task-id`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } taskId := mustGetFlag(cmd, "task-id") attachmentId := mustGetFlag(cmd, "attachment-id") if !commandDryRun(cmd) && !confirmDelete("附件", attachmentId) { return nil } return callMCPTool("remove_todo_attachment", map[string]any{ "todoAttachmentRemoveRequest": map[string]any{ "taskId": taskId, "attachmentId": attachmentId, }, }) }, } todoTaskCreateCmd.Flags().String("title", "", "待办标题 (必填)") todoTaskCreateCmd.Flags().String("executors", "", "执行者 userId 列表 (必填)") todoTaskCreateCmd.Flags().String("due", "", "截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)") todoTaskCreateCmd.Flags().String("priority", "", "优先级: 10低/20普通/30较高/40紧急") todoTaskCreateCmd.Flags().String("recurrence", "", "循环待办 (需先设置 --due); 格式: DTSTART:20260320T100000Z\\nRRULE:FREQ=DAILY;INTERVAL=1") todoTaskCreateSubCmd.Flags().String("parent-id", "", "父待办任务 ID (必填)") todoTaskCreateSubCmd.Flags().String("title", "", "子待办标题 (必填)") todoTaskCreateSubCmd.Flags().String("executors", "", "执行者 userId 列表 (必填)") todoTaskCreateSubCmd.Flags().String("due", "", "截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)") todoTaskCreateSubCmd.Flags().String("priority", "", "优先级: 10低/20普通/30较高/40紧急") todoTaskCreateSubCmd.Flags().String("recurrence", "", "循环待办 (需先设置 --due); 格式: DTSTART:20260320T100000Z\\nRRULE:FREQ=DAILY;INTERVAL=1") todoTaskListCmd.Flags().String("page", "1", "页码(默认 1)") todoTaskListCmd.Flags().String("size", "20", "获取数量,超过 20 自动分页 (默认 20)") todoTaskListCmd.Flags().String("status", "", "true=已完成, false=未完成") todoTaskListCmd.Flags().String("priority", "", "优先级: 10 低/20 普通/30 较高/40 紧急") todoTaskListCmd.Flags().String("role-types", "", "角色类型: creator/executor/participant") todoTaskListCmd.Flags().String("plan-finish-date-start", "", "截止时间范围查询开始 ISO-8601 (如 2026-03-10T18:00:00+08:00)") todoTaskListCmd.Flags().String("plan-finish-date-end", "", "截止时间范围查询结束 ISO-8601 (如 2026-03-10T18:00:00+08:00)") todoTaskUpdateCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskUpdateCmd.Flags().String("title", "", "新标题") todoTaskUpdateCmd.Flags().String("due", "", "截止时间 ISO-8601 (如 2026-03-10T18:00:00+08:00)") todoTaskUpdateCmd.Flags().String("priority", "", "优先级: 10低/20普通/30较高/40紧急") todoTaskUpdateCmd.Flags().String("done", "", "完成状态: true/false") todoTaskDoneCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskDoneCmd.Flags().String("status", "", "完成状态: true=已完成, false=未完成 (必填)") todoTaskGetCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskDeleteCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskCreateCmd.Flags().String("subject", "", "--title 的别名") todoTaskCreateCmd.Flags().String("content", "", "--title 的别名") _ = todoTaskCreateCmd.Flags().MarkHidden("subject") _ = todoTaskCreateCmd.Flags().MarkHidden("content") todoTaskCreateSubCmd.Flags().String("subject", "", "--title 的别名") todoTaskCreateSubCmd.Flags().String("content", "", "--title 的别名") _ = todoTaskCreateSubCmd.Flags().MarkHidden("subject") _ = todoTaskCreateSubCmd.Flags().MarkHidden("content") todoTaskCmd.PersistentFlags().String("remind-at", "", "内部兼容标志") _ = todoTaskCmd.PersistentFlags().MarkHidden("remind-at") todoTaskAddExecutorCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddExecutorCmd.Flags().String("executors", "", "执行者 userId 列表 (必填)") todoTaskRemoveExecutorCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskRemoveExecutorCmd.Flags().String("executors", "", "执行者 userId 列表 (必填)") todoTaskAddParticipantCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddParticipantCmd.Flags().String("participants", "", "参与人 userId 列表 (必填)") todoTaskRemoveParticipantCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskRemoveParticipantCmd.Flags().String("participants", "", "参与人 userId 列表 (必填)") todoTaskAddReminderCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddReminderCmd.Flags().String("base-time", "", "提醒基准时间: dueTime/customTime (必填)") todoTaskAddReminderCmd.Flags().String("due-date-offset", "", "截止时间偏移量,为整数 (baseTime=dueTime 时必填)") todoTaskAddReminderCmd.Flags().String("reminder-time-stamp", "", "自定义提醒时间 ISO-8601 (如 2026-03-10T18:00:00+08:00,baseTime=customTime 时必填)") todoTaskUpdateReminderCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskUpdateReminderCmd.Flags().String("reminder-rules", "", "提醒规则 JSON 数组 (可选,为空则清除提醒)") todoTaskListSubCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskListAttachmentCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddAttachment.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddAttachment.Flags().String("file-path", "", "本地文件路径(必填)") todoTaskRemoveAttachmentCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskRemoveAttachmentCmd.Flags().String("attachment-id", "", "待办附件 ID(必填)") todoTaskCmd.AddCommand(todoTaskCreateCmd, todoTaskCreateSubCmd, todoTaskListCmd, todoTaskUpdateCmd, todoTaskDoneCmd, todoTaskGetCmd, todoTaskDeleteCmd, todoTaskAddExecutorCmd, todoTaskRemoveExecutorCmd, todoTaskAddParticipantCmd, todoTaskRemoveParticipantCmd, todoTaskAddReminderCmd, todoTaskUpdateReminderCmd, todoTaskListSubCmd, todoTaskListAttachmentCmd, todoTaskAddAttachment, todoTaskRemoveAttachmentCmd, ) todoCmd.AddCommand(todoTaskCmd) // ────────────────────────────────────────────────────────── // dws todo comment — 待办评论 // 对应 MCP:add_todo_comment / list_todo_comment / delete_todo_comment // ────────────────────────────────────────────────────────── todoCommentCmd := &cobra.Command{Use: "comment", Short: "待办评论:新增 / 列表 / 删除", RunE: groupRunE} todoCommentAddCmd := &cobra.Command{ Use: "add", Short: "新增待办评论", Example: ` dws todo comment add --task-id --content "评论内容" # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "content"); err != nil { return err } return callMCPTool("add_todo_comment", map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "content": mustGetFlag(cmd, "content"), }) }, } todoCommentListCmd := &cobra.Command{ Use: "list", Short: "查询待办评论列表", Example: ` dws todo comment list --task-id dws todo comment list --task-id --page 1 --size 20 # 查询 taskId: dws todo task list`, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } return callMCPTool("list_todo_comment", map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "page": mustGetFlag(cmd, "page"), "pageSize": mustGetFlag(cmd, "size"), }) }, } todoCommentDeleteCmd := &cobra.Command{ Use: "delete", Short: "删除待办评论", Example: ` dws todo comment delete --task-id --comment-id dws todo comment delete --task-id --comment-id --yes # 查询 commentId: dws todo comment list --task-id `, RunE: func(cmd *cobra.Command, args []string) error { if err := validateRequiredFlags(cmd, "task-id", "comment-id"); err != nil { return err } commentId := mustGetFlag(cmd, "comment-id") if !confirmDelete("待办评论", commentId) { return nil } return callMCPTool("delete_todo_comment", map[string]any{ "taskId": mustGetFlag(cmd, "task-id"), "commentId": commentId, }) }, } todoCommentAddCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoCommentAddCmd.Flags().String("content", "", "评论内容 (必填)") todoCommentListCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoCommentListCmd.Flags().String("page", "1", "页码 (默认 1)") todoCommentListCmd.Flags().String("size", "20", "每页数量 (默认 20)") todoCommentDeleteCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoCommentDeleteCmd.Flags().String("comment-id", "", "评论 ID (必填)") todoCommentCmd.AddCommand(todoCommentAddCmd, todoCommentListCmd, todoCommentDeleteCmd) todoCmd.AddCommand(todoCommentCmd) todoCmd.AddCommand( hintSubCmd("create", "use: dws todo task create"), hintSubCmd("list", "use: dws todo task list"), hintSubCmd("get", "use: dws todo task get"), hintSubCmd("delete", "use: dws todo task delete"), ) return todoCmd } func rejectUnsupportedTodoReminderFlags(cmd *cobra.Command) error { var unsupported []string if v, _ := cmd.Flags().GetString("remind-at"); v != "" { unsupported = append(unsupported, "--remind-at") } if len(unsupported) == 0 { return nil } return &CLIError{ Code: CodeMissingParam, Message: fmt.Sprintf("todo 当前不支持独立 reminder 参数: %s", strings.Join(unsupported, ", ")), Suggestion: "请使用 --due 表示截止时间;如果用户要的是精确提醒时间,请明确说明该能力当前不支持,而不是把 --due 当作 reminder。", Operation: "todo.task.reminder", } } // todoListAutoPage 当 size > 20 时自动分页请求并合并结果。pageStr 为起始页码,wantSize 为期望条数。 func todoListAutoPage(cmd *cobra.Command, pageStr string, wantSize int) error { ctx := context.Background() if deps.Caller.DryRun() { numPages := (wantSize + todoListPageSizeMax - 1) / todoListPageSizeMax bold := color.New(color.FgYellow, color.Bold) bold.Println("[DRY-RUN] 自动分页待办列表:") deps.Out.PrintKeyValue("Tool", "get_user_todos_in_current_org") deps.Out.PrintKeyValue("预计请求次数", fmt.Sprintf("%d (每页最多 %d 条)", numPages, todoListPageSizeMax)) return nil } startPage, _ := strconv.Atoi(pageStr) if startPage < 1 { startPage = 1 } var merged []any for pageNum := startPage; len(merged) < wantSize; pageNum++ { toolArgs := map[string]any{ "pageNum": fmt.Sprintf("%d", pageNum), "pageSize": fmt.Sprintf("%d", todoListPageSizeMax), } err := buildListTodoTaskArgs(cmd, toolArgs) if err != nil { return err } text, err := callMCPToolReturnText(ctx, "get_user_todos_in_current_org", toolArgs) if err != nil { return err } var resp struct { Result struct { TodoCards []any `json:"todoCards"` } `json:"result"` } if err := json.Unmarshal([]byte(text), &resp); err != nil { return fmt.Errorf("failed to parse todo response: %w", err) } cards := resp.Result.TodoCards if len(cards) == 0 { break } for _, c := range cards { merged = append(merged, c) if len(merged) >= wantSize { break } } if len(cards) < todoListPageSizeMax { break } } out := map[string]any{"result": map[string]any{"todoCards": merged}} flagFormat := deps.Caller.Format() if flagFormat == "json" { return deps.Out.PrintJSON(out) } raw, _ := json.MarshalIndent(out, "", " ") deps.Out.PrintRaw(string(raw)) return nil } // parseExecutorIds 将 "id1,id2" 转为 []string,供 MCP 数组类型 executorIds 使用 func parseExecutorIds(s string) []string { s = strings.TrimSpace(s) if s == "" { return nil } parts := strings.Split(s, ",") ids := make([]string, 0, len(parts)) for _, p := range parts { if id := strings.TrimSpace(p); id != "" { ids = append(ids, id) } } return ids } type todoLocalFileMeta struct { LocalPath string FileName string FileType string ContentPath string FileSize int64 MD5 string } func parseTodoFileSendIDs(text string) (int64, int64, error) { var data any if err := unmarshalJSONUseNumber(text, &data); err != nil { return 0, 0, fmt.Errorf("failed to parse uploaded file response JSON: %w", err) } dentryID, _ := findInt64Field(data, "dentryId", "dentryID") spaceID, _ := findInt64Field(data, "spaceId", "spaceID") if dentryID == 0 || spaceID == 0 { return 0, 0, fmt.Errorf("uploaded file response missing dentryId or spaceId") } return dentryID, spaceID, nil } func buildTodoLocalFileMeta(filePath, fileName, md5Value string) (todoLocalFileMeta, error) { fi, err := os.Stat(filePath) if err != nil { return todoLocalFileMeta{}, fmt.Errorf("cannot read file %s: %w", filePath, err) } if fi.IsDir() { return todoLocalFileMeta{}, fmt.Errorf("%s is a directory, not a file", filePath) } if fileName == "" { fileName = filepath.Base(filePath) } fileType := strings.TrimPrefix(filepath.Ext(fileName), ".") if md5Value == "" { md5Value, err = todoFileMD5Hex(filePath) if err != nil { return todoLocalFileMeta{}, err } } return todoLocalFileMeta{ LocalPath: filePath, FileName: fileName, FileType: fileType, ContentPath: "/" + fileName, FileSize: fi.Size(), MD5: md5Value, }, nil } func parseTodoFileUploadInfo(text string) (resourceURL, uploadKey string, headers map[string]string, err error) { var data map[string]any if err = unmarshalJSONUseNumber(text, &data); err != nil { return "", "", nil, fmt.Errorf("failed to parse upload credentials JSON: %w", err) } if result, ok := data["result"].(map[string]any); ok { data = result } resourceURL = firstStringField(data, "resourceUrl", "resourceURL", "url") if resourceURL == "" { if values, ok := data["resourceUrls"].([]any); ok && len(values) > 0 { resourceURL = stringFromJSONScalar(values[0]) } } uploadKey = firstStringField(data, "uploadKey", "key") if resourceURL == "" || uploadKey == "" { return "", "", nil, fmt.Errorf("incomplete upload credentials: resourceUrl=%q, uploadKey=%q", resourceURL, uploadKey) } headers = map[string]string{} for _, key := range []string{"headers", "ossHeaders"} { if h, ok := data[key].(map[string]any); ok { for name, value := range h { if s := stringFromJSONScalar(value); s != "" { headers[name] = s } } } } return resourceURL, uploadKey, headers, nil } func uploadTodoLocalFile(ctx context.Context, meta todoLocalFileMeta) (string, error) { initArgs := map[string]any{} initArgs["fileName"] = meta.FileName initArgs["fileSize"] = meta.FileSize initArgs["md5"] = meta.MD5 initText, err := callMCPToolReturnTextOnServer(ctx, "todo", "init_todo_file_upload", map[string]any{"todoAttachmentInitUploadInfoRequest": initArgs}) if err != nil { return "", err } resourceURL, uploadKey, headers, err := parseTodoFileUploadInfo(initText) if err != nil { return "", err } if err := httpPutFile(ctx, resourceURL, headers, meta.LocalPath, meta.FileSize); err != nil { return "", err } commitArgs := map[string]any{} commitArgs["uploadKey"] = uploadKey commitArgs["fileName"] = meta.FileName commitArgs["fileSize"] = meta.FileSize commitArgs["md5"] = meta.MD5 return callMCPToolReturnTextOnServer(ctx, "todo", "commit_todo_file_upload", map[string]any{"todoAttachmentCommitUploadInfoRequest": commitArgs}) } // parseIntList splits a comma-separated string into a slice of ints. // Empty segments and non-numeric values are skipped. Returns nil if no valid ints found. func parseIntList(s string) []int { s = strings.TrimSpace(s) if s == "" { return nil } parts := strings.Split(s, ",") result := make([]int, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } if n, err := strconv.Atoi(p); err == nil { result = append(result, n) } } if len(result) == 0 { return nil } return result } // parseStringList splits a comma-separated string into a slice of strings. // Empty segments are skipped. Returns nil if no valid strings found. func parseStringList(s string) []string { s = strings.TrimSpace(s) if s == "" { return nil } parts := strings.Split(s, ",") result := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } result = append(result, p) } if len(result) == 0 { return nil } return result } // parseRoleTypes parses a comma-separated role-types string and validates each value. // Only "creator", "executor", "participant" are allowed. Returns error on invalid values. func parseRoleTypes(s string) ([]string, error) { allowed := map[string]bool{"creator": true, "executor": true, "participant": true} parts := parseStringList(s) if parts == nil { return nil, nil } for _, p := range parts { if !allowed[p] { return nil, fmt.Errorf("invalid role-type %q, allowed values: creator, executor, participant", p) } } return parts, nil } func buildListTodoTaskArgs(cmd *cobra.Command, toolArgs map[string]any) error { if v, _ := cmd.Flags().GetString("status"); v != "" { toolArgs["todoStatus"] = v } if v, _ := cmd.Flags().GetString("priority"); v != "" { if priorities := parseIntList(v); priorities != nil { toolArgs["priorityList"] = priorities } } toolArgs["roleTypes"] = []string{"executor"} if v, _ := cmd.Flags().GetString("role-types"); v != "" { roleTypes, err := parseRoleTypes(v) if err != nil { return err } if roleTypes != nil { toolArgs["roleTypes"] = roleTypes } } if v, _ := cmd.Flags().GetString("plan-finish-date-start"); v != "" { ms, err := parseISOTimeToMillis("plan-finish-date-start", v) if err != nil { return err } toolArgs["planFinishDateStart"] = ms } if v, _ := cmd.Flags().GetString("plan-finish-date-end"); v != "" { ms, err := parseISOTimeToMillis("plan-finish-date-end", v) if err != nil { return err } toolArgs["planFinishDateEnd"] = ms } return nil }