2105 lines
81 KiB
Go
2105 lines
81 KiB
Go
package helpers
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||
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 {
|
||
// Product-level Agent routing Decl (migrated from selection/todo.json
|
||
// products.todo). Catalog assembly stamps provenance contract_final.
|
||
contract.RegisterProductDecl(contract.ProductDecl{
|
||
ID: "todo",
|
||
Selection: contract.ProductSelectionDecl{
|
||
AgentSummary: "管理待办任务、标签、子任务、执行人、参与人、评论、附件与提醒",
|
||
UseWhen: []string{
|
||
"查询、创建或更新个人待办及其协作信息时",
|
||
},
|
||
AvoidWhen: []string{
|
||
"不要用于 OA 审批流转、工作日志提交或日历日程管理",
|
||
},
|
||
},
|
||
})
|
||
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)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskCreateCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "create_personal_todo",
|
||
CanonicalPath: "todo.create_personal_todo",
|
||
CLIPath: "todo task create",
|
||
PrimaryCLIPath: "todo task create",
|
||
},
|
||
Description: "创建个人待办",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "create_personal_todo"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "创建个人待办",
|
||
UseWhen: []string{"需要在当前组织创建个人待办(标题与执行人必填;可选截止时间、优先级、按天循环)时"},
|
||
AvoidWhen: []string{
|
||
"只需查询已有待办时改用 dws todo task list",
|
||
"要创建子待办时改用 dws todo task create-sub;标题或执行人未确认时不要创建",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task create --title \"修复线上Bug\" --executors <USER_ID_1>,<USER_ID_2> --priority 40",
|
||
"dws todo task create --title \"每日站会\" --executors <USER_ID> --due \"2026-03-20T10:00:00+08:00\" --recurrence \"DTSTART:20260320T020000Z\\nRRULE:FREQ=DAILY;INTERVAL=1\"",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "due", Property: "PersonalTodoCreateVO.dueTime"},
|
||
{Name: "executors", Property: "PersonalTodoCreateVO.executorIds"},
|
||
{Name: "priority", Property: "PersonalTodoCreateVO.priority"},
|
||
{Name: "recurrence", Property: "PersonalTodoCreateVO.recurrence"},
|
||
{Name: "title", Property: "PersonalTodoCreateVO.subject"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskCreateSubCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "create_personal_sub_todo",
|
||
CanonicalPath: "todo.create_personal_sub_todo",
|
||
CLIPath: "todo task create-sub",
|
||
PrimaryCLIPath: "todo task create-sub",
|
||
},
|
||
Description: "创建个人子待办",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "create_personal_sub_todo"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "创建个人子待办",
|
||
UseWhen: []string{"父待办由本人创建且已确认 parentId,需要创建子待办(标题、执行人、可选截止时间/优先级)时"},
|
||
AvoidWhen: []string{
|
||
"只需创建顶层待办时改用 dws todo task create",
|
||
"父待办不是本人创建或 parentId 未确认时不要创建;子待办不支持独立循环规则",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task create-sub --parent-id <PARENT_TASK_ID> --title \"子任务标题\" --executors <USER_ID_1>,<USER_ID_2> --priority 40",
|
||
"dws todo task create-sub --parent-id <PARENT_TASK_ID> --title \"子任务标题\" --executors <USER_ID> --due \"2026-03-20T10:00:00+08:00\"",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "due", Property: "PersonalTodoCreateVO.dueTime"},
|
||
{Name: "executors", Property: "PersonalTodoCreateVO.executorIds"},
|
||
{Name: "parent-id", Property: "PersonalTodoCreateVO.parentId"},
|
||
{Name: "priority", Property: "PersonalTodoCreateVO.priority"},
|
||
{Name: "recurrence", Property: "PersonalTodoCreateVO.recurrence"},
|
||
{Name: "title", Property: "PersonalTodoCreateVO.subject"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskListCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "get_user_todos_in_current_org",
|
||
CanonicalPath: "todo.get_user_todos_in_current_org",
|
||
CLIPath: "todo task list",
|
||
PrimaryCLIPath: "todo task list",
|
||
},
|
||
Description: "查询当前组织待办,或通过 --query-all 跨组织查询全部待办",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "get_user_todos_in_current_org"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查询当前组织待办,或通过 --query-all 跨组织查询全部待办",
|
||
UseWhen: []string{"需要按完成状态、优先级、角色或截止日期范围查询当前用户待办列表时;跨组织范围时显式使用 --query-all"},
|
||
AvoidWhen: []string{"已知 taskId 需要完整单条详情时改用 dws todo task get"},
|
||
Examples: []string{
|
||
"dws todo task list --page 1 --size 20 --status false",
|
||
"dws todo task list --page 1 --size 20 --status false --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "query-all", Required: boolPtr(false), InterfaceType: "boolean", Description: "为 true 时跨组织查询全部待办;默认仅查询当前组织待办"},
|
||
{Name: "role-types", Property: "roleTypes", Required: boolPtr(false), Description: "角色类型列表;省略时运行时默认使用 executor"},
|
||
{Name: "page", Property: "pageNum"},
|
||
{Name: "priority", Property: "priorityList"},
|
||
{Name: "size", Property: "pageSize"},
|
||
{Name: "status", Property: "todoStatus"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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,
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskUpdateCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "update_todo_task",
|
||
CanonicalPath: "todo.update_todo_task",
|
||
CLIPath: "todo task update",
|
||
PrimaryCLIPath: "todo task update",
|
||
},
|
||
Description: "修改整个待办任务",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "update_todo_task"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "修改整个待办任务",
|
||
UseWhen: []string{"已知 taskId,需要修改待办标题、截止时间、优先级或完成标记等字段时"},
|
||
AvoidWhen: []string{
|
||
"只需改执行者完成状态时优先 dws todo task done",
|
||
"目标任务与待改字段未确认时不要更新",
|
||
},
|
||
Examples: []string{
|
||
"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\"",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "done", Property: "TodoUpdateRequest.isDone"},
|
||
{Name: "due", Property: "TodoUpdateRequest.dueTime"},
|
||
{Name: "priority", Property: "TodoUpdateRequest.priority"},
|
||
{Name: "task-id", Property: "TodoUpdateRequest.taskId"},
|
||
{Name: "title", Property: "TodoUpdateRequest.subject"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskDoneCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "update_todo_done_status",
|
||
CanonicalPath: "todo.update_todo_done_status",
|
||
CLIPath: "todo task done",
|
||
PrimaryCLIPath: "todo task done",
|
||
},
|
||
Description: "修改执行者的待办完成状态",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "update_todo_done_status"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "修改执行者的待办完成状态",
|
||
UseWhen: []string{"已知 taskId,需要把执行者侧完成状态设为已完成或未完成时"},
|
||
AvoidWhen: []string{
|
||
"要改标题/截止时间/优先级时改用 dws todo task update",
|
||
"执行人、目标待办或期望状态未确认时不要修改",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task done --task-id <taskId> --status true",
|
||
"dws todo task done --task-id <taskId> --status false",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "status", Property: "isDone"},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 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)
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskGetCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "get_todo_detail",
|
||
CanonicalPath: "todo.get_todo_detail",
|
||
CLIPath: "todo task get",
|
||
PrimaryCLIPath: "todo task get",
|
||
},
|
||
Description: "查询待办详情",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "get_todo_detail"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查询待办详情",
|
||
UseWhen: []string{"已知 taskId,且调用者是创建者或执行者,需要查看单条待办详情时"},
|
||
AvoidWhen: []string{
|
||
"需要按条件查多个待办时改用 dws todo task list",
|
||
"需要修改待办时改用 update/done/delete 等写命令",
|
||
"需要读取或验证提醒规则时不要使用;当前详情接口不返回 reminderRules",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task get --task-id <taskId>",
|
||
"dws todo task get --task-id <taskId> --format json",
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 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")
|
||
return callMCPTool("delete_todo", map[string]any{
|
||
"taskId": taskId,
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskDeleteCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "destructive", Risk: "high",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "delete_todo",
|
||
CanonicalPath: "todo.delete_todo",
|
||
CLIPath: "todo task delete",
|
||
PrimaryCLIPath: "todo task delete",
|
||
},
|
||
Description: "删除待办",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "delete_todo"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "删除待办",
|
||
UseWhen: []string{"用户明确要求删除指定待办(所有执行者侧一并删除)时"},
|
||
AvoidWhen: []string{
|
||
"用户未明确删除、目标 taskId 不确定或仍需保留任务记录时不要执行",
|
||
"只需标记完成时改用 dws todo task done",
|
||
},
|
||
Examples: []string{"dws todo task delete --task-id <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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskAddExecutorCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "add_task_executors",
|
||
CanonicalPath: "todo.add_task_executors",
|
||
CLIPath: "todo task add-executor",
|
||
PrimaryCLIPath: "todo task add-executor",
|
||
},
|
||
Description: "添加待办执行人",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "add_task_executors"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "添加待办执行人",
|
||
UseWhen: []string{"已知 taskId,需要为待办追加执行人(userId 列表)时"},
|
||
AvoidWhen: []string{
|
||
"要添加参与人时改用 dws todo task add-participant",
|
||
"要上传附件时改用 dws todo task add-attachment",
|
||
"待办或执行人未确认时不要添加",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task add-executor --task-id <taskId> --executors <USER_ID_1>,<USER_ID_2>",
|
||
"dws todo task add-executor --task-id <taskId> --executors userId1,userId2 --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "executors", Property: "todoExecutorsAddRequest.executorIds"},
|
||
{Name: "task-id", Property: "todoExecutorsAddRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskRemoveExecutorCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "remove_task_executors",
|
||
CanonicalPath: "todo.remove_task_executors",
|
||
CLIPath: "todo task remove-executor",
|
||
PrimaryCLIPath: "todo task remove-executor",
|
||
},
|
||
Description: "移除待办执行人",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "remove_task_executors"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "移除待办执行人",
|
||
UseWhen: []string{"用户明确要求从待办移除指定执行人时"},
|
||
AvoidWhen: []string{
|
||
"要添加执行人时改用 dws todo task add-executor",
|
||
"待办或执行人未确认时不要移除",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task remove-executor --task-id <taskId> --executors <USER_ID_1>,<USER_ID_2>",
|
||
"dws todo task remove-executor --task-id <taskId> --executors userId1 --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "executors", Property: "todoExecutorsRemoveRequest.executorIds"},
|
||
{Name: "task-id", Property: "todoExecutorsRemoveRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskAddParticipantCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "add_task_participants",
|
||
CanonicalPath: "todo.add_task_participants",
|
||
CLIPath: "todo task add-participant",
|
||
PrimaryCLIPath: "todo task add-participant",
|
||
},
|
||
Description: "添加待办参与人",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "add_task_participants"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "添加待办参与人",
|
||
UseWhen: []string{"已知 taskId,需要为待办追加参与人(userId 列表)时"},
|
||
AvoidWhen: []string{
|
||
"要添加执行人时改用 dws todo task add-executor",
|
||
"要上传附件时改用 dws todo task add-attachment",
|
||
"待办或参与人未确认时不要添加",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task add-participant --task-id <taskId> --participants <USER_ID_1>,<USER_ID_2>",
|
||
"dws todo task add-participant --task-id <taskId> --participants userId1,userId2 --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "participants", Property: "todoParticipantsAddRequest.participantIds"},
|
||
{Name: "task-id", Property: "todoParticipantsAddRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskRemoveParticipantCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "remove_task_participants",
|
||
CanonicalPath: "todo.remove_task_participants",
|
||
CLIPath: "todo task remove-participant",
|
||
PrimaryCLIPath: "todo task remove-participant",
|
||
},
|
||
Description: "移除待办参与人",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "remove_task_participants"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "移除待办参与人",
|
||
UseWhen: []string{"用户明确要求从待办移除指定参与人时"},
|
||
AvoidWhen: []string{
|
||
"要添加参与人时改用 dws todo task add-participant",
|
||
"待办或参与人未确认时不要移除",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task remove-participant --task-id <taskId> --participants <USER_ID_1>,<USER_ID_2>",
|
||
"dws todo task remove-participant --task-id <taskId> --participants userId1 --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "participants", Property: "todoParticipantsRemoveRequest.participantIds"},
|
||
{Name: "task-id", Property: "todoParticipantsRemoveRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskAddReminderCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "add_todo_reminder",
|
||
CanonicalPath: "todo.add_todo_reminder",
|
||
CLIPath: "todo task add-reminder",
|
||
PrimaryCLIPath: "todo task add-reminder",
|
||
},
|
||
Description: "写入一条待办提醒规则(上游不支持规则读回)",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "add_todo_reminder"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "写入一条待办提醒规则(上游不支持规则读回)",
|
||
UseWhen: []string{"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间);成功响应仅作为写入回执"},
|
||
AvoidWhen: []string{
|
||
"要重置/清除提醒规则时改用 dws todo task reset-reminder",
|
||
"提醒时间与目标待办未确认时不要添加",
|
||
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则",
|
||
},
|
||
Examples: []string{
|
||
"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\"",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "base-time", Property: "todoReminderAddRequest.baseTime"},
|
||
{Name: "due-date-offset", Property: "todoReminderAddRequest.dueDateOffset"},
|
||
{Name: "reminder-time-stamp", Property: "todoReminderAddRequest.reminderTimeStamp"},
|
||
{Name: "task-id", Property: "todoReminderAddRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskUpdateReminderCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "reset_todo_reminder",
|
||
CanonicalPath: "todo.reset_todo_reminder",
|
||
CLIPath: "todo task reset-reminder",
|
||
PrimaryCLIPath: "todo task reset-reminder",
|
||
},
|
||
Description: "整体替换或清除待办提醒规则(上游不支持规则读回)",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "reset_todo_reminder"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "整体替换或清除待办提醒规则(上游不支持规则读回)",
|
||
UseWhen: []string{"需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执"},
|
||
AvoidWhen: []string{
|
||
"只需追加一条提醒时改用 dws todo task add-reminder",
|
||
"新规则与目标待办未确认时不要重置",
|
||
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task reset-reminder --task-id <taskId>",
|
||
"dws todo task reset-reminder --task-id <taskId> --reminder-rules '[{\"dueDateOffset\":-30,\"baseTime\":\"dueTime\"},{\"reminderTimeStamp\":\"2026-03-10T18:00:00+08:00\",\"baseTime\":\"customTime\"}]'",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "reminder-rules", Property: "todoReminderUpdateRequest.reminderRules", Description: "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp"},
|
||
{Name: "task-id", Property: "todoReminderUpdateRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
|
||
todoTaskAddAttachment := &cobra.Command{
|
||
Use: "add-attachment",
|
||
Short: "上传待办附件",
|
||
Long: `上传待办附件
|
||
⚠️ 重要:该接口会上传文件到附件,不可用于测试或试探性调用。调用前必须确认待办存在。`,
|
||
Example: ` dws todo task add-attachment --task-id <taskId> --file <filePath>
|
||
# 查询 taskId: dws todo task list`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
filePath := flagOrFallback(cmd, "file-path", "file")
|
||
taskID := mustGetFlag(cmd, "task-id")
|
||
if taskID == "" && filePath == "" {
|
||
return validateRequiredFlags(cmd, "task-id", "file")
|
||
}
|
||
if taskID == "" {
|
||
return validateRequiredFlags(cmd, "task-id")
|
||
}
|
||
if filePath == "" {
|
||
return validateRequiredFlags(cmd, "file")
|
||
}
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskAddAttachment, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "add_todo_attachment",
|
||
CanonicalPath: "todo.add_todo_attachment",
|
||
CLIPath: "todo task add-attachment",
|
||
PrimaryCLIPath: "todo task add-attachment",
|
||
},
|
||
Description: "上传待办附件",
|
||
DryRun: &contract.DryRunSpec{PreviewKind: "plan", RemoteReads: false},
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "add_todo_attachment"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "上传待办附件",
|
||
UseWhen: []string{"已知 taskId 且待办已确认存在,需要上传本地文件为待办附件时"},
|
||
AvoidWhen: []string{
|
||
"待办尚未确认存在或仅试探调用时不要上传(会真实写附件)",
|
||
"要添加执行人/参与人/提醒时改用对应 add 命令",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task add-attachment --task-id <taskId> --file /path/to/file.pdf",
|
||
"dws todo task add-attachment --task-id <taskId> --file /path/to/file.pdf --format json",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "task-id", Property: "todoAttachmentAddRequest.taskId"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTaskListAttachmentCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "list_todo_attachment",
|
||
CanonicalPath: "todo.list_todo_attachment",
|
||
CLIPath: "todo task list-attachment",
|
||
PrimaryCLIPath: "todo task list-attachment",
|
||
},
|
||
Description: "查询待办附件列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: this executable CLI wrapper calls a remote helper that is absent from the pinned MCP metadata snapshot; no single pinned semantically equivalent interface_ref can represent the command.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "查询待办附件列表",
|
||
UseWhen: []string{"已知 taskId,需要查看该待办当前附件及其 attachmentId、文件名和大小时"},
|
||
AvoidWhen: []string{
|
||
"要上传附件时改用 dws todo task add-attachment",
|
||
"要删除附件时先用本命令确认 attachmentId,再改用 dws todo task remove-attachment",
|
||
},
|
||
Examples: []string{
|
||
"dws todo task list-attachment --task-id <taskId>",
|
||
"dws todo task list-attachment --task-id <taskId> --format json",
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
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 (必填)")
|
||
corecmd.RegisterFlags(todoTaskAddAttachment, []corecmd.FlagSpec{{
|
||
Name: "file",
|
||
Usage: "本地文件路径(必填)",
|
||
Aliases: []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"),
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoCommentAddCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "add_todo_comment",
|
||
CanonicalPath: "todo.add_todo_comment",
|
||
CLIPath: "todo comment add",
|
||
PrimaryCLIPath: "todo comment add",
|
||
},
|
||
Description: "给待办添加评论",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "add_todo_comment"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "给待办添加评论",
|
||
UseWhen: []string{"已知 taskId,需要给待办新增一条评论文本时"},
|
||
AvoidWhen: []string{
|
||
"要删除评论时改用 dws todo comment delete",
|
||
"要列出评论时改用 dws todo comment list",
|
||
},
|
||
Examples: []string{
|
||
"dws todo comment add --task-id <taskId> --content \"评论内容\"",
|
||
"dws todo comment add --task-id <taskId> --content \"已开始处理\" --format json",
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
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"),
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoCommentListCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "list_todo_comment",
|
||
CanonicalPath: "todo.list_todo_comment",
|
||
CLIPath: "todo comment list",
|
||
PrimaryCLIPath: "todo comment list",
|
||
},
|
||
Description: "获取待办评论列表",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "list_todo_comment"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "获取待办评论列表",
|
||
UseWhen: []string{"已知 taskId,需要分页查看该待办评论时"},
|
||
AvoidWhen: []string{
|
||
"要新增评论时改用 dws todo comment add",
|
||
"要删除评论时改用 dws todo comment delete",
|
||
},
|
||
Examples: []string{
|
||
"dws todo comment list --task-id <taskId>",
|
||
"dws todo comment list --task-id <taskId> --page 1 --size 20",
|
||
},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "size", Property: "pageSize"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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")
|
||
return callMCPTool("delete_todo_comment", map[string]any{
|
||
"taskId": mustGetFlag(cmd, "task-id"),
|
||
"commentId": commentId,
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoCommentDeleteCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "destructive", Risk: "high",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "delete_todo_comment",
|
||
CanonicalPath: "todo.delete_todo_comment",
|
||
CLIPath: "todo comment delete",
|
||
PrimaryCLIPath: "todo comment delete",
|
||
},
|
||
Description: "删除待办评论",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "mcp",
|
||
Availability: "available",
|
||
Ref: &contract.InterfaceRefSpec{ProductID: "todo", RPCName: "delete_todo_comment"},
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "删除待办评论",
|
||
UseWhen: []string{"用户明确要求删除指定待办下的某条评论时"},
|
||
AvoidWhen: []string{
|
||
"用户未明确删除,或 taskId/commentId 不确定时不要执行",
|
||
"要新增评论时改用 dws todo comment add;要列出评论时改用 dws todo comment list",
|
||
},
|
||
Examples: []string{"dws todo comment delete --task-id <taskId> --comment-id <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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTagAddCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "tag_todo",
|
||
CanonicalPath: "todo.tag_todo",
|
||
CLIPath: "todo tag add",
|
||
PrimaryCLIPath: "todo tag add",
|
||
},
|
||
Description: "把一个或多个现有标签添加到指定待办",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the executable CLI builds TodoTagRequest and calls todo/tag_todo, which is absent from the pinned MCP metadata snapshot.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "把一个或多个现有标签添加到指定待办",
|
||
UseWhen: []string{"已有 taskId 和标签 code,需要给该待办打标"},
|
||
AvoidWhen: []string{"尚无标签 code 时先用 todo tag list 或 todo tag create;修改标签定义应使用 todo tag update"},
|
||
Examples: []string{"dws todo tag add --task-id <taskId> --tag-codes code1,code2"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "tag-codes", Property: "tagCodes", Required: boolPtr(true), InterfaceType: "array"},
|
||
{Name: "task-id", Property: "taskId", Required: boolPtr(true)},
|
||
},
|
||
},
|
||
})
|
||
|
||
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")
|
||
}
|
||
return callMCPTool("delete_todo_tag", map[string]any{
|
||
"UserTagDeleteRequest": map[string]any{
|
||
"tagCodes": tagCodes,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTagDeleteCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "destructive", Risk: "high",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "delete_todo_tag",
|
||
CanonicalPath: "todo.delete_todo_tag",
|
||
CLIPath: "todo tag delete",
|
||
PrimaryCLIPath: "todo tag delete",
|
||
},
|
||
Description: "不可逆地删除当前用户的一个或多个待办标签",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the executable CLI parses tag codes into UserTagDeleteRequest and calls todo/delete_todo_tag, which is absent from the pinned MCP metadata snapshot.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "不可逆地删除当前用户的一个或多个待办标签",
|
||
UseWhen: []string{"用户明确要求删除已有标签 code,且已确认标签编码及不可逆影响"},
|
||
AvoidWhen: []string{"只需从某个待办移除标签或重命名标签时不要删除标签定义;未确认 code 时先用 todo tag list"},
|
||
Examples: []string{"dws todo tag delete --tag-codes code1,code2"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "tag-codes", Property: "tagCodes", Required: boolPtr(true), InterfaceType: "array"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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,
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTagUpdateCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "unknown",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "update_todo_tag",
|
||
CanonicalPath: "todo.update_todo_tag",
|
||
CLIPath: "todo tag update",
|
||
PrimaryCLIPath: "todo tag update",
|
||
},
|
||
Description: "批量更新已有待办标签的名称等定义",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the executable CLI decodes user tag JSON into UserTagAddRequest and calls todo/update_todo_tag, which is absent from the pinned MCP metadata snapshot.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "批量更新已有待办标签的名称等定义",
|
||
UseWhen: []string{"已有标签 code,需要按 JSON 数组更新一个或多个标签定义"},
|
||
AvoidWhen: []string{"给待办打标签应使用 todo tag add;创建没有 code 的新标签应使用 todo tag create"},
|
||
Examples: []string{"dws todo tag update --user-tags '[{\"code\":\"code1\",\"name\":\"新名称\"}]'"},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "user-tags", Property: "userTags", Required: boolPtr(true), InterfaceType: "array"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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{})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTagListCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "list_todo_tags",
|
||
CanonicalPath: "todo.list_todo_tags",
|
||
CLIPath: "todo tag list",
|
||
PrimaryCLIPath: "todo tag list",
|
||
},
|
||
Description: "列出当前用户可用的待办标签及其 code",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the executable CLI calls todo/list_todo_tags, which is absent from the pinned MCP metadata snapshot.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "列出当前用户可用的待办标签及其 code",
|
||
UseWhen: []string{"需要查看待办标签目录或先取得标签 code 供打标、更新、删除使用"},
|
||
AvoidWhen: []string{"查询待办任务列表应使用 todo task list;该命令只返回标签定义"},
|
||
Examples: []string{"dws todo tag list"},
|
||
},
|
||
},
|
||
})
|
||
|
||
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}},
|
||
},
|
||
})
|
||
},
|
||
}
|
||
DeclareLeafMetadata(todoTagCreateCmd, LeafSpec{
|
||
Safety: contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "not_required", Idempotency: "non_idempotent",
|
||
},
|
||
Contract: LeafContract{
|
||
Identity: contract.ToolIdentitySpec{
|
||
ProductID: "todo",
|
||
Name: "create_todo_tag",
|
||
CanonicalPath: "todo.create_todo_tag",
|
||
CLIPath: "todo tag create",
|
||
PrimaryCLIPath: "todo tag create",
|
||
},
|
||
Description: "为当前用户创建一个新的待办标签",
|
||
Interface: &contract.InterfaceSpec{
|
||
Mode: "composite",
|
||
Availability: "available",
|
||
Reason: "Reviewed unpinned remote adapter: the executable CLI wraps one trimmed name in UserTagAddRequest and calls todo/create_todo_tag, which is absent from the pinned MCP metadata snapshot.",
|
||
},
|
||
Selection: contract.SelectionSpec{
|
||
AgentSummary: "为当前用户创建一个新的待办标签",
|
||
UseWhen: []string{"用户明确要求创建可复用的待办标签,且已给出非空标签名称"},
|
||
AvoidWhen: []string{"给已有待办打上现有标签应使用 todo tag add;重命名已有标签应使用 todo tag update"},
|
||
Examples: []string{"dws todo tag create --name \"项目标签\""},
|
||
},
|
||
Parameters: []contract.ParamDecl{
|
||
{Name: "name", Property: "name", Required: boolPtr(true)},
|
||
},
|
||
},
|
||
})
|
||
|
||
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
|
||
}
|