Files
dingtalk-workspace-cli/internal/helpers/todo.go

1289 lines
47 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"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 <parentId> --title "子任务标题" --executors userId1,userId2 --priority 40
dws todo task create-sub --parent-id <parentId> --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
}
toolName := "get_user_todos_in_current_org"
if queryAll, _ := cmd.Flags().GetBool("query-all"); queryAll {
toolName = "get_user_todos"
}
return callMCPTool(toolName, toolArgs)
}
return todoListAutoPage(cmd, pageStr, size)
},
}
todoTaskListSubCmd := &cobra.Command{
Use: "list-sub",
Short: "查询子待办列表",
Example: ` dws todo task list-sub --task-id <taskId>
# 查询 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 <taskId> --title "新标题"
dws todo task update --task-id <taskId> --priority 40 --due "2026-03-10T18:00:00+08:00"
dws todo task update --task-id <taskId> --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 <taskId> --status true
dws todo task done --task-id <taskId> --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: "待办详情",
Long: `查询单条待办的详情。
当前上游详情接口不返回 reminderRules;本命令不能读取或验证 add-reminder /
reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作为写入回执。`,
Example: ` dws todo task get --task-id <taskId>
# 查询 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 <taskId>
dws todo task delete --task-id <taskId> --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 <taskId> --executors <USER_ID_1>,<USER_ID_2>
# 查询 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 <taskId> --executors <USER_ID_1>,<USER_ID_2>
# 查询 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 <taskId> --participants <USER_ID_1>,<USER_ID_2>
# 查询 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 <taskId> --participants <USER_ID_1>,<USER_ID_2>
# 查询 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: "添加待办提醒",
Long: `为已有待办写入一条提醒规则。
当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;
成功响应是写入回执,不代表 CLI 能再次读取并核验该规则。`,
Example: ` dws todo task add-reminder --task-id <taskId> --base-time dueTime --due-date-offset -30
dws todo task add-reminder --task-id <taskId> --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: "重置待办提醒",
Long: `整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组,
每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。
当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;
成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。`,
Example: ` dws todo task reset-reminder --task-id <taskId>
dws todo task reset-reminder --task-id <taskId> --reminder-rules <reminderRules>
# 查询 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 []map[string]any
if cmd.Flags().Changed("reminder-rules") {
value, _ := cmd.Flags().GetString("reminder-rules")
parsedRules, err := parseTodoReminderRules(value)
if err != nil {
return err
}
reminderRules = parsedRules
}
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 <taskId> --file-path <filePath>
# 查询 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>
# 查询 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 <taskId> --attachment-id <attachmentId>
# 查询 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)")
todoTaskListCmd.Flags().Bool("query-all", false, "查询所有待办,而不是仅查询当前组织待办")
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 <taskId> --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 <taskId>
dws todo comment list --task-id <taskId> --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 <taskId> --comment-id <commentId>
dws todo comment delete --task-id <taskId> --comment-id <commentId> --yes
# 查询 commentId: dws todo comment list --task-id <taskId>`,
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)
// ──────────────────────────────────────────────────────────
// dws todo tag — 待办标签
// 对应 MCP:tag_todo / delete_todo_tag / update_todo_tag / list_todo_tags / create_todo_tag
// ──────────────────────────────────────────────────────────
todoTagCmd := &cobra.Command{Use: "tag", Short: "待办标签:打标 / 列表 / 创建 / 更新 / 删除", RunE: groupRunE}
todoTagAddCmd := &cobra.Command{
Use: "add",
Short: "给待办打标",
Example: ` dws todo tag add --task-id <taskId> --tag-codes code1,code2
# 查询 taskId: dws todo task list`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "task-id", "tag-codes"); err != nil {
return err
}
tagCodes := parseStringList(mustGetFlag(cmd, "tag-codes"))
if len(tagCodes) == 0 {
return apperrors.NewValidation("--tag-codes must contain at least one non-empty code")
}
return callMCPTool("tag_todo", map[string]any{
"TodoTagRequest": map[string]any{
"taskId": mustGetFlag(cmd, "task-id"),
"tagCodes": tagCodes,
},
})
},
}
todoTagDeleteCmd := &cobra.Command{
Use: "delete",
Short: "删除待办标签",
Long: `删除当前用户的待办标签。该操作不可逆;正式执行必须先获得用户确认并追加 --yes,可先使用 --dry-run 预览。`,
Example: ` dws todo tag delete --tag-codes code1,code2 --yes
# 查询 tag code: dws todo tag list`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "tag-codes"); err != nil {
return err
}
tagCodes := parseStringList(mustGetFlag(cmd, "tag-codes"))
if len(tagCodes) == 0 {
return apperrors.NewValidation("--tag-codes must contain at least one non-empty code")
}
if !deps.Caller.DryRun() && !commandBoolFlag(cmd, "yes") {
return apperrors.NewValidation(
"删除待办标签不可逆;获得用户确认后加 --yes 执行,或加 --dry-run 预览",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("先确认要删除的标签编码;用户明确同意后以相同参数追加 --yes"),
apperrors.WithActions("确认标签编码", "获得用户确认后使用 --yes 执行"),
)
}
return callMCPTool("delete_todo_tag", map[string]any{
"UserTagDeleteRequest": map[string]any{
"tagCodes": tagCodes,
},
})
},
}
todoTagUpdateCmd := &cobra.Command{
Use: "update",
Short: "更新待办标签",
Example: ` dws todo tag update --user-tags '[{"code":"code1","name":"新名称"}]'
# 查询 code: dws todo tag list`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "user-tags"); err != nil {
return err
}
var userTags []any
if err := json.Unmarshal([]byte(mustGetFlag(cmd, "user-tags")), &userTags); err != nil {
return &CLIError{
Code: CodeMissingParam,
Message: fmt.Sprintf("--user-tags 必须是合法的 JSON 数组: %v", err),
Suggestion: `示例: --user-tags '[{"code":"code1","name":"新名称"}]'`,
Operation: "todo.tag.update.user-tags",
}
}
if userTags == nil {
return apperrors.NewValidation("--user-tags must be a JSON array")
}
return callMCPTool("update_todo_tag", map[string]any{
"UserTagAddRequest": map[string]any{
"userTags": userTags,
},
})
},
}
todoTagListCmd := &cobra.Command{
Use: "list",
Short: "查询待办标签列表",
Example: ` dws todo tag list`,
RunE: func(cmd *cobra.Command, args []string) error {
return callMCPTool("list_todo_tags", map[string]any{})
},
}
todoTagCreateCmd := &cobra.Command{
Use: "create",
Short: "创建待办标签",
Example: ` dws todo tag create --name "标签名"`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRequiredFlags(cmd, "name"); err != nil {
return err
}
name := strings.TrimSpace(mustGetFlag(cmd, "name"))
if name == "" {
return apperrors.NewValidation("--name must not be blank")
}
return callMCPTool("create_todo_tag", map[string]any{
"UserTagAddRequest": map[string]any{
"userTags": []map[string]any{{"name": name}},
},
})
},
}
todoTagAddCmd.Flags().String("task-id", "", "待办任务 ID (必填)")
todoTagAddCmd.Flags().String("tag-codes", "", "标签编码列表,逗号分隔 (必填)")
todoTagDeleteCmd.Flags().String("tag-codes", "", "要删除的标签编码列表,逗号分隔 (必填)")
todoTagUpdateCmd.Flags().String("user-tags", "", "标签列表 JSON 数组 (必填)")
todoTagCreateCmd.Flags().String("name", "", "标签名称 (必填)")
todoTagCmd.AddCommand(todoTagAddCmd, todoTagDeleteCmd, todoTagUpdateCmd, todoTagListCmd, todoTagCreateCmd)
todoCmd.AddCommand(todoTagCmd)
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: CodeInvalidParam,
Message: fmt.Sprintf("todo 当前不支持独立 reminder 参数: %s", strings.Join(unsupported, ", ")),
Suggestion: "需要截止时间请改用 --due;需要独立提醒请用 dws todo task add-reminder。dueTime 模式要求待办已有截止时间,customTime 模式直接传 --reminder-time-stamp,不必先设置 --due。当前上游不支持读取提醒规则,不能用 task get/list 验证写入结果。",
Operation: "todo.task.reminder",
}
}
func parseTodoReminderRules(raw string) ([]map[string]any, error) {
value := strings.TrimSpace(raw)
if value == "" {
return nil, invalidTodoReminderRules(CodeInvalidParam, "显式传入的 --reminder-rules 不能为空;清除提醒请省略该参数或传 []", nil)
}
if !json.Valid([]byte(value)) {
return nil, invalidTodoReminderRules(CodeInvalidJSON, "--reminder-rules 必须是合法 JSON 数组", nil)
}
var rules []map[string]any
if err := unmarshalJSONUseNumber(value, &rules); err != nil {
return nil, invalidTodoReminderRules(CodeInvalidParam, "--reminder-rules 必须是由对象组成的 JSON 数组", err)
}
if rules == nil {
return nil, invalidTodoReminderRules(CodeInvalidParam, "--reminder-rules 不能是 null;清除提醒请省略该参数或传 []", nil)
}
for i, rule := range rules {
position := i + 1
if rule == nil {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条必须是对象", position), nil)
}
baseTime, ok := rule["baseTime"].(string)
baseTime = strings.TrimSpace(baseTime)
if !ok || baseTime == "" {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条缺少字符串 baseTime", position), nil)
}
rule["baseTime"] = baseTime
switch baseTime {
case "dueTime":
offset, ok := rule["dueDateOffset"].(json.Number)
if !ok {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条在 baseTime=dueTime 时必须提供整数 dueDateOffset", position), nil)
}
parsed, err := strconv.ParseInt(offset.String(), 10, 64)
if err != nil {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条的 dueDateOffset 必须是整数", position), err)
}
rule["dueDateOffset"] = parsed
case "customTime":
timestamp, ok := rule["reminderTimeStamp"].(string)
timestamp = strings.TrimSpace(timestamp)
if !ok || timestamp == "" {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条在 baseTime=customTime 时必须提供 ISO8601 字符串 reminderTimeStamp", position), nil)
}
parsed, err := parseISOTimeToMillis("reminderTimeStamp", timestamp)
if err != nil {
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条的 reminderTimeStamp 无效", position), err)
}
rule["reminderTimeStamp"] = parsed
default:
return nil, invalidTodoReminderRules(CodeInvalidParam, fmt.Sprintf("--reminder-rules 第 %d 条的 baseTime 必须是 dueTime 或 customTime", position), nil)
}
}
return rules, nil
}
func invalidTodoReminderRules(code, message string, cause error) error {
return &CLIError{
Code: code,
Message: message,
Suggestion: "使用 dueTime + 整数 dueDateOffset,或 customTime + ISO8601 reminderTimeStamp;清除提醒请省略 --reminder-rules 或传 []。",
Operation: "todo.task.reset-reminder.reminder-rules",
Cause: cause,
}
}
// todoListAutoPage 当 size > 20 时自动分页请求并合并结果。pageStr 为起始页码,wantSize 为期望条数。
func todoListAutoPage(cmd *cobra.Command, pageStr string, wantSize int) error {
ctx := context.Background()
toolName := "get_user_todos_in_current_org"
if queryAll, _ := cmd.Flags().GetBool("query-all"); queryAll {
toolName = "get_user_todos"
}
if deps.Caller.DryRun() {
numPages := (wantSize + todoListPageSizeMax - 1) / todoListPageSizeMax
bold := color.New(color.FgYellow, color.Bold)
bold.Println("[DRY-RUN] 自动分页待办列表:")
deps.Out.PrintKeyValue("Tool", toolName)
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, toolName, 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
}