From 69d813afefd4d260941ccf3ec08a79b1c7b9a888 Mon Sep 17 00:00:00 2001 From: Dennis Date: Tue, 28 Jul 2026 12:09:57 +0800 Subject: [PATCH] fix: address contract review feedback --- internal/cli/schema_agent_metadata/doc.json | 15 +- internal/cli/schema_agent_metadata/drive.json | 37 ++-- internal/cli/schema_agent_metadata/index.json | 2 +- internal/cli/schema_agent_metadata/todo.json | 39 ++-- internal/cli/schema_agent_metadata_audit.json | 6 +- internal/cli/schema_catalog.json | 174 +++++++++------- internal/cli/schema_hints/metadata/drive.json | 4 +- internal/cli/schema_hints/metadata/todo.json | 9 +- internal/cli/schema_hints/selection/doc.json | 5 +- .../cli/schema_hints/selection/drive.json | 7 +- internal/cli/schema_hints/selection/todo.json | 9 +- internal/helpers/common.go | 26 --- internal/helpers/contract_defects_test.go | 193 +++++++++++++++--- internal/helpers/doc.go | 2 +- internal/helpers/drive.go | 66 +++++- internal/helpers/todo.go | 105 +++++++--- .../helpers/todo_command_edges_more_test.go | 4 +- internal/helpers/todo_reminder_test.go | 116 ++++++++++- .../smart/compatibility_coverage_test.go | 2 + .../shortcut/smart/remind_contract_test.go | 48 +++++ skills/mono/references/products/drive.md | 2 +- skills/mono/references/products/todo.md | 7 +- skills/multi/dingtalk-drive/SKILL.md | 4 +- .../multi/dingtalk-drive/references/drive.md | 2 +- skills/multi/dingtalk-todo/SKILL.md | 2 + skills/multi/dingtalk-todo/references/todo.md | 7 +- 26 files changed, 670 insertions(+), 223 deletions(-) diff --git a/internal/cli/schema_agent_metadata/doc.json b/internal/cli/schema_agent_metadata/doc.json index d96e60d8..7b4d2a1f 100644 --- a/internal/cli/schema_agent_metadata/doc.json +++ b/internal/cli/schema_agent_metadata/doc.json @@ -12071,7 +12071,8 @@ "availability": "available", "avoid_when": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "confirmation": "not_required", "effect": "write", @@ -12121,7 +12122,8 @@ "avoid_when": { "value": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -12131,7 +12133,8 @@ { "value": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -12277,7 +12280,7 @@ }, "use_when": { "value": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -12286,7 +12289,7 @@ "candidates": [ { "value": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -12318,7 +12321,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#doc.rename_document" ], "use_when": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ] }, "doc search": { diff --git a/internal/cli/schema_agent_metadata/drive.json b/internal/cli/schema_agent_metadata/drive.json index aa7b5111..22fd9b37 100644 --- a/internal/cli/schema_agent_metadata/drive.json +++ b/internal/cli/schema_agent_metadata/drive.json @@ -7083,12 +7083,13 @@ ] }, "drive rename": { - "agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "agent_summary": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "agent_summary_source": "dws-agent-selection/drive", "availability": "available", "avoid_when": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "confirmation": "not_required", "effect": "write", @@ -7098,14 +7099,14 @@ ], "field_provenance": { "agent_summary": { - "value": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "value": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "candidates": [ { - "value": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "value": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", "selected": true, @@ -7118,14 +7119,14 @@ "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension." + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist." }, { "value": "available", @@ -7138,7 +7139,8 @@ "avoid_when": { "value": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", @@ -7148,7 +7150,8 @@ { "value": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", @@ -7224,14 +7227,14 @@ "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "candidates": [ { "value": "mcp", "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension." + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist." }, { "value": "mcp", @@ -7246,14 +7249,14 @@ "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "candidates": [ { "value": "doc.rename_document", "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension." + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist." }, { "value": "doc.rename_document", @@ -7268,14 +7271,14 @@ "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/drive.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension." + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist." }, { "value": true, @@ -7302,7 +7305,7 @@ }, "use_when": { "value": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ], "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", @@ -7311,7 +7314,7 @@ "candidates": [ { "value": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ], "source": "internal/cli/schema_hints/selection/drive.json", "precedence": "reviewed_explicit", @@ -7342,7 +7345,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#drive.rename_document" ], "use_when": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ] }, "drive search": { diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index c7fd2c22..96d8b976 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:696955f889cbb3a843a1afc91ee511cd54cf1dd5d96b0dabc20516f59719eecd", + "source_hash": "sha256:f71480e58eda8f2be041e07d7faf6fb20f5c21f861995d3c4dacc311b46c3e81", "surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed", "coverage": { "surface_products": 26, diff --git a/internal/cli/schema_agent_metadata/todo.json b/internal/cli/schema_agent_metadata/todo.json index 604c46b1..a200cf36 100644 --- a/internal/cli/schema_agent_metadata/todo.json +++ b/internal/cli/schema_agent_metadata/todo.json @@ -2324,11 +2324,12 @@ ] }, "todo +remind": { - "agent_summary": "给自己创建一条带截止/提醒时间的待办", + "agent_summary": "给自己创建一条带可选截止时间的待办", "agent_summary_source": "dws-agent-selection/todo", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "confirmation": "user_required", "effect": "write", @@ -2338,14 +2339,14 @@ ], "field_provenance": { "agent_summary": { - "value": "给自己创建一条带截止/提醒时间的待办", + "value": "给自己创建一条带可选截止时间的待办", "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "candidates": [ { - "value": "给自己创建一条带截止/提醒时间的待办", + "value": "给自己创建一条带可选截止时间的待办", "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", "selected": true, @@ -2371,7 +2372,8 @@ }, "avoid_when": { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -2380,7 +2382,8 @@ "candidates": [ { "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -2546,7 +2549,7 @@ }, "use_when": { "value": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -2555,7 +2558,7 @@ "candidates": [ { "value": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -2578,7 +2581,7 @@ "internal/cli/schema_hints/selection/todo.json" ], "use_when": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ] }, "todo +todo-done": { @@ -8604,14 +8607,14 @@ "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed" + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms." }, { "value": "available", @@ -8714,14 +8717,14 @@ "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "candidates": [ { "value": "mcp", "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed" + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms." }, { "value": "mcp", @@ -8750,14 +8753,14 @@ "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/todo.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed" + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms." }, { "value": true, @@ -8784,7 +8787,7 @@ }, "use_when": { "value": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -8793,7 +8796,7 @@ "candidates": [ { "value": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ], "source": "internal/cli/schema_hints/selection/todo.json", "precedence": "reviewed_explicit", @@ -8823,7 +8826,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#todo.reset_todo_reminder" ], "use_when": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ] }, "todo task update": { diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index 473d5982..61308aab 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:696955f889cbb3a843a1afc91ee511cd54cf1dd5d96b0dabc20516f59719eecd", + "source_hash": "sha256:f71480e58eda8f2be041e07d7faf6fb20f5c21f861995d3c4dacc311b46c3e81", "surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed", "source_files": 160, "hint_files": 54, @@ -3890,7 +3890,7 @@ { "tool_path": "todo task remove-attachment", "source": "skills/mono/references/products/todo.md", - "line": 438, + "line": 440, "candidates": [ "todo task add-attachment", "todo task add-executor", @@ -3904,7 +3904,7 @@ { "tool_path": "todo task list-sub", "source": "skills/mono/references/products/todo.md", - "line": 441, + "line": 443, "candidates": [ "todo task add-attachment", "todo task add-executor", diff --git a/internal/cli/schema_catalog.json b/internal/cli/schema_catalog.json index f64274e2..7160e01c 100644 --- a/internal/cli/schema_catalog.json +++ b/internal/cli/schema_catalog.json @@ -1,12 +1,12 @@ { "version": 1, - "source_hash": "sha256:a0e007ce9500f18abfca09ad2de259b8b574b8150c9c79ccc7f3d7b1ffe66f7d", + "source_hash": "sha256:91a6b7040ccc8fa16dce388a1ea8ac2aae808819af2e6e998f65f363685ca325", "surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed", "catalog": { "agent_metadata": { "products_with_metadata": 26, "source": "embedded-skill-metadata", - "source_hash": "sha256:696955f889cbb3a843a1afc91ee511cd54cf1dd5d96b0dabc20516f59719eecd", + "source_hash": "sha256:f71480e58eda8f2be041e07d7faf6fb20f5c21f861995d3c4dacc311b46c3e81", "surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed", "surface_products": 26, "surface_tools": 813, @@ -15008,7 +15008,8 @@ "availability": "available", "avoid_when": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "canonical_path": "doc.rename_document", "cli_name": "rename", @@ -15029,7 +15030,7 @@ "risk": "medium", "title": "重命名文档/文件", "use_when": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ] }, { @@ -16613,18 +16614,19 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "agent_summary": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "agent_summary_source": "dws-agent-selection/drive", "availability": "available", "avoid_when": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "canonical_path": "drive.rename_document", "cli_name": "rename", "cli_path": "drive rename", "confirmation": "not_required", - "description": "修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名;\n如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。\n\n权限要求: 对文档有\"编辑\"权限。", + "description": "修改文档空间中文档、文件或文件夹的名称。\n实际执行前会读取节点类型和当前扩展名:文件的新名称仅在末尾扩展名与当前扩展名一致时去掉一层,\n避免 report.txt.txt;文件夹和扩展名不匹配的名称保持不变。dry-run 不读取远端元数据,因此保留输入名称。\n\n权限要求: 对文档有\"编辑\"权限。", "effect": "write", "idempotency": "unknown", "interface_mode": "mcp", @@ -16639,7 +16641,7 @@ "risk": "medium", "title": "重命名文件/文档", "use_when": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ] }, { @@ -24777,7 +24779,7 @@ "cli_name": "reset-reminder", "cli_path": "todo task reset-reminder", "confirmation": "not_required", - "description": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。", + "description": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组,\n每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。", "effect": "write", "group": "task", "idempotency": "unknown", @@ -24793,7 +24795,7 @@ "risk": "medium", "title": "重置待办提醒", "use_when": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ] }, { @@ -25032,11 +25034,12 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "给自己创建一条带截止/提醒时间的待办", + "agent_summary": "给自己创建一条带可选截止时间的待办", "agent_summary_source": "dws-agent-selection/todo", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "canonical_path": "todo.shortcut_remind", "cli_name": "+remind", @@ -25053,7 +25056,7 @@ "risk": "medium", "title": "给自己创建一条带可选截止时间的待办", "use_when": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ] }, { @@ -352880,7 +352883,8 @@ "availability": "available", "avoid_when": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "canonical_path": "doc.rename_document", "cli_name": "rename", @@ -352941,7 +352945,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ] } ], @@ -352951,7 +352956,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ] }, "canonical_path": { @@ -353173,7 +353179,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ] } ], @@ -353182,7 +353188,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ] } }, @@ -353425,7 +353431,7 @@ "source": "reviewed_command_registry", "title": "重命名文档/文件", "use_when": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ] }, "doc.reply_comment": { @@ -390427,18 +390433,19 @@ "structured-hint:internal/cli/schema_hints/products/drive.json", "structured-hint:internal/cli/schema_hints/selection-review.json#drive.rename_document" ], - "agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "agent_summary": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "agent_summary_source": "dws-agent-selection/drive", "availability": "available", "avoid_when": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "canonical_path": "drive.rename_document", "cli_name": "rename", "cli_path": "drive rename", "confirmation": "not_required", - "description": "修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名;\n如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。\n\n权限要求: 对文档有\"编辑\"权限。", + "description": "修改文档空间中文档、文件或文件夹的名称。\n实际执行前会读取节点类型和当前扩展名:文件的新名称仅在末尾扩展名与当前扩展名一致时去掉一层,\n避免 report.txt.txt;文件夹和扩展名不匹配的名称保持不变。dry-run 不读取远端元数据,因此保留输入名称。\n\n权限要求: 对文档有\"编辑\"权限。", "display": "钉盘文件管理", "effect": "write", "effect_source": "command-verb", @@ -390453,20 +390460,20 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "selected": true, "source": "internal/cli/schema_hints/selection/drive.json", - "value": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名" + "value": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/drive.json", - "value": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名" + "value": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀" }, "availability": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": true, "source": "internal/cli/schema_hints/metadata/drive.json", "value": "available" @@ -390480,7 +390487,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "source": "internal/cli/schema_hints/metadata/drive.json", "value": "available" }, @@ -390493,7 +390500,8 @@ "source": "internal/cli/schema_hints/selection/drive.json", "value": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ] } ], @@ -390503,7 +390511,8 @@ "source": "internal/cli/schema_hints/selection/drive.json", "value": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ] }, "canonical_path": { @@ -390517,7 +390526,7 @@ }, { "precedence": "reviewed_manual", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": false, "source": "reviewed_manual_hint", "source_ref": "drive rename", @@ -390550,7 +390559,7 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名;\n如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。\n\n权限要求: 对文档有\"编辑\"权限。" + "value": "修改文档空间中文档、文件或文件夹的名称。\n实际执行前会读取节点类型和当前扩展名:文件的新名称仅在末尾扩展名与当前扩展名一致时去掉一层,\n避免 report.txt.txt;文件夹和扩展名不匹配的名称保持不变。dry-run 不读取远端元数据,因此保留输入名称。\n\n权限要求: 对文档有\"编辑\"权限。" }, { "precedence": "mcp_metadata", @@ -390562,7 +390571,7 @@ "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名;\n如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。\n\n权限要求: 对文档有\"编辑\"权限。" + "value": "修改文档空间中文档、文件或文件夹的名称。\n实际执行前会读取节点类型和当前扩展名:文件的新名称仅在末尾扩展名与当前扩展名一致时去掉一层,\n避免 report.txt.txt;文件夹和扩展名不匹配的名称保持不变。dry-run 不读取远端元数据,因此保留输入名称。\n\n权限要求: 对文档有\"编辑\"权限。" }, "effect": { "candidates": [ @@ -390616,7 +390625,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": true, "source": "internal/cli/schema_hints/metadata/drive.json", "value": "mcp" @@ -390630,7 +390639,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "source": "internal/cli/schema_hints/metadata/drive.json", "value": "mcp" }, @@ -390638,7 +390647,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": true, "source": "internal/cli/schema_hints/metadata/drive.json", "value": { @@ -390655,7 +390664,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "source": "internal/cli/schema_hints/metadata/drive.json", "value": { "product_id": "doc", @@ -390680,7 +390689,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": true, "source": "internal/cli/schema_hints/metadata/drive.json", "value": true @@ -390695,7 +390704,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "source": "internal/cli/schema_hints/metadata/drive.json", "value": true }, @@ -390741,7 +390750,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/drive.json", "value": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ] } ], @@ -390750,7 +390759,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/drive.json", "value": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ] } }, @@ -390767,22 +390776,22 @@ "parameter_count": 2, "parameters": { "name": { - "description": "新显示名称;服务端保留节点原扩展名,若传入常见文件扩展名,CLI 会自动去掉一层以避免双扩展名", + "description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名", "field_provenance": { "description": { "candidates": [ { "precedence": "reviewed_manual", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "selected": true, "source": "reviewed_manual_hint", - "value": "新显示名称;服务端保留节点原扩展名,若传入常见文件扩展名,CLI 会自动去掉一层以避免双扩展名" + "value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名" }, { "precedence": "cobra_contract", "selected": false, "source": "cobra_usage", - "value": "新名称 (必填;服务端保留原扩展名,传入常见扩展名时 CLI 会自动去掉一层)" + "value": "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)" }, { "precedence": "mcp_metadata", @@ -390799,9 +390808,9 @@ ], "precedence": "reviewed_manual", "resolution": "highest_precedence", - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "source": "reviewed_manual_hint", - "value": "新显示名称;服务端保留节点原扩展名,若传入常见文件扩展名,CLI 会自动去掉一层以避免双扩展名" + "value": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名" }, "property": { "candidates": [ @@ -391001,7 +391010,7 @@ "source": "reviewed_command_registry", "title": "重命名文件/文档", "use_when": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ] }, "drive.search_files": { @@ -566362,7 +566371,7 @@ "cli_name": "reset-reminder", "cli_path": "todo task reset-reminder", "confirmation": "not_required", - "description": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。", + "description": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组,\n每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。", "display": "待办任务管理", "effect": "write", "effect_source": "command-verb", @@ -566391,7 +566400,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "selected": true, "source": "internal/cli/schema_hints/metadata/todo.json", "value": "available" @@ -566405,7 +566414,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "source": "internal/cli/schema_hints/metadata/todo.json", "value": "available" }, @@ -566441,6 +566450,14 @@ "source": "reviewed_command_registry", "source_ref": "todo task reset-reminder", "value": "todo.reset_todo_reminder" + }, + { + "precedence": "reviewed_manual", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", + "selected": false, + "source": "reviewed_manual_hint", + "source_ref": "todo task reset-reminder", + "value": "todo.reset_todo_reminder" } ], "precedence": "command_registry", @@ -566469,7 +566486,7 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。" + "value": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组,\n每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。" }, { "precedence": "mcp_metadata", @@ -566481,7 +566498,7 @@ "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。" + "value": "整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组,\n每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。\n\n当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules;\n成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。" }, "effect": { "candidates": [ @@ -566537,7 +566554,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "selected": true, "source": "internal/cli/schema_hints/metadata/todo.json", "value": "mcp" @@ -566551,7 +566568,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "source": "internal/cli/schema_hints/metadata/todo.json", "value": "mcp" }, @@ -566593,7 +566610,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "selected": true, "source": "internal/cli/schema_hints/metadata/todo.json", "value": true @@ -566608,7 +566625,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "dws-tool-metadata/todo marks this tool as reviewed", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "source": "internal/cli/schema_hints/metadata/todo.json", "value": true }, @@ -566654,7 +566671,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ] } ], @@ -566663,7 +566680,7 @@ "review_reason": "结合本机 dws schema 实时描述、Skill 与 Cobra 手写选型;pinned schema_mcp_metadata 仅对照。", "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ] } }, @@ -566681,15 +566698,22 @@ "parameter_count": 2, "parameters": { "reminder-rules": { - "description": "提醒规则 JSON 数组 (可选,为空则清除提醒)", + "description": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp", "field_provenance": { "description": { "candidates": [ { - "precedence": "cobra_contract", + "precedence": "reviewed_manual", + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", "selected": true, + "source": "reviewed_manual_hint", + "value": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp" + }, + { + "precedence": "cobra_contract", + "selected": false, "source": "cobra_usage", - "value": "提醒规则 JSON 数组 (可选,为空则清除提醒)" + "value": "提醒规则 JSON 数组 (不传则清除;显式传值必须合法)" }, { "precedence": "default", @@ -566698,10 +566722,11 @@ "value": "" } ], - "precedence": "cobra_contract", + "precedence": "reviewed_manual", "resolution": "highest_precedence", - "source": "cobra_usage", - "value": "提醒规则 JSON 数组 (可选,为空则清除提醒)" + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", + "source": "reviewed_manual_hint", + "value": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp" }, "property": { "candidates": [ @@ -566875,7 +566900,7 @@ "source": "reviewed_command_registry", "title": "重置待办提醒", "use_when": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ] }, "todo.shortcut_assign": { @@ -571429,11 +571454,12 @@ "internal/cli/schema_hints/metadata/todo.json", "internal/cli/schema_hints/selection/todo.json" ], - "agent_summary": "给自己创建一条带截止/提醒时间的待办", + "agent_summary": "给自己创建一条带可选截止时间的待办", "agent_summary_source": "dws-agent-selection/todo", "availability": "available", "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "canonical_path": "todo.shortcut_remind", "cli_name": "+remind", @@ -571454,14 +571480,14 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "selected": true, "source": "internal/cli/schema_hints/selection/todo.json", - "value": "给自己创建一条带截止/提醒时间的待办" + "value": "给自己创建一条带可选截止时间的待办" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/todo.json", - "value": "给自己创建一条带截止/提醒时间的待办" + "value": "给自己创建一条带可选截止时间的待办" }, "availability": { "candidates": [ @@ -571487,7 +571513,8 @@ "selected": true, "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ] } ], @@ -571496,7 +571523,8 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ] }, "canonical_path": { @@ -571706,7 +571734,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ] } ], @@ -571715,7 +571743,7 @@ "review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.", "source": "internal/cli/schema_hints/selection/todo.json", "value": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ] } }, @@ -571916,7 +571944,7 @@ "source": "reviewed_command_registry", "title": "给自己创建一条带可选截止时间的待办", "use_when": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ] }, "todo.shortcut_todo_done": { diff --git a/internal/cli/schema_hints/metadata/drive.json b/internal/cli/schema_hints/metadata/drive.json index ff802f80..6873a74e 100644 --- a/internal/cli/schema_hints/metadata/drive.json +++ b/internal/cli/schema_hints/metadata/drive.json @@ -206,10 +206,10 @@ "reviewed": true, "parameters": { "name": { - "description": "新显示名称;服务端保留节点原扩展名,若传入常见文件扩展名,CLI 会自动去掉一层以避免双扩展名" + "description": "新显示名称;实际执行前读取节点类型与当前扩展名,仅对非文件夹且末尾后缀与当前扩展名一致的名称去掉一层,避免双扩展名" } }, - "review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.", + "review_reason": "The reviewed CLI reads node metadata before mutation and strips only the exact current extension for non-folder nodes, avoiding both duplicate extensions and dotted-folder corruption without a suffix whitelist.", "cli_path": "drive rename" }, "drive.search_files": { diff --git a/internal/cli/schema_hints/metadata/todo.json b/internal/cli/schema_hints/metadata/todo.json index 8b6a090a..96412455 100644 --- a/internal/cli/schema_hints/metadata/todo.json +++ b/internal/cli/schema_hints/metadata/todo.json @@ -126,7 +126,14 @@ "todo.reset_todo_reminder": { "interface_mode": "mcp", "availability": "available", - "reviewed": true + "reviewed": true, + "parameters": { + "reminder-rules": { + "description": "提醒规则 JSON 数组;不传表示清除,显式传值必须为对象数组,且每条按 baseTime 提供整数 dueDateOffset 或 ISO8601 reminderTimeStamp" + } + }, + "review_reason": "The reviewed CLI rejects malformed or structurally invalid non-empty reminder rules before the reset mutation, while preserving omission and [] as explicit clear forms.", + "cli_path": "todo task reset-reminder" }, "todo.update_todo_done_status": { "interface_mode": "mcp", diff --git a/internal/cli/schema_hints/selection/doc.json b/internal/cli/schema_hints/selection/doc.json index 9d7b48ca..2114cfc5 100644 --- a/internal/cli/schema_hints/selection/doc.json +++ b/internal/cli/schema_hints/selection/doc.json @@ -582,11 +582,12 @@ "doc.rename_document": { "agent_summary": "兼容入口:重命名文档或文件;文件重命名能力已迁移到 drive。", "use_when": [ - "用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端" + "用户要改在线文档在列表与链接中展示的名称时" ], "avoid_when": [ "改正文标题/章节标题改用 doc block update", - "不要用 update 或重新 create 来改名" + "不要用 update 或重新 create 来改名", + "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型和当前扩展名" ], "examples": [ "dws doc rename --node --name \"新名称\" --format json" diff --git a/internal/cli/schema_hints/selection/drive.json b/internal/cli/schema_hints/selection/drive.json index 47012981..333eadbb 100644 --- a/internal/cli/schema_hints/selection/drive.json +++ b/internal/cli/schema_hints/selection/drive.json @@ -545,13 +545,14 @@ ] }, "drive.rename_document": { - "agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名", + "agent_summary": "安全重命名文档空间中的文档、文件或文件夹,并按节点真实扩展名避免双后缀", "use_when": [ - "用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层" + "用户要重命名钉盘/文档空间中的文件、文档或文件夹时;实际执行会读取节点类型和当前扩展名,仅去掉完全匹配的一层后缀" ], "avoid_when": [ "要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename", - "只要复制或移动改用 copy/move" + "只要复制或移动改用 copy/move", + "dry-run 不读取节点元数据,输出名称尚未做基于当前扩展名的规范化" ], "examples": [ "dws drive rename --node --name \"新名称\" --format json" diff --git a/internal/cli/schema_hints/selection/todo.json b/internal/cli/schema_hints/selection/todo.json index b3d8df98..9910e7cb 100644 --- a/internal/cli/schema_hints/selection/todo.json +++ b/internal/cli/schema_hints/selection/todo.json @@ -360,7 +360,7 @@ "todo.reset_todo_reminder": { "agent_summary": "整体替换或清除待办提醒规则(上游不支持规则读回)", "use_when": [ - "需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执" + "需要清除或整体替换待办提醒规则时(不传 reminder-rules 或传 [] 可清除);非空规则会在远端调用前严格校验,成功响应仅作为写入回执" ], "avoid_when": [ "只需追加一条提醒时改用 dws todo task add-reminder", @@ -606,12 +606,13 @@ ] }, "todo.shortcut_remind": { - "agent_summary": "给自己创建一条带截止/提醒时间的待办", + "agent_summary": "给自己创建一条带可选截止时间的待办", "use_when": [ - "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。" + "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。" ], "avoid_when": [ - "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令" + "需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令", + "用户需要独立提醒规则时改用 dws todo task add-reminder;不要把 --at 解释成提醒时间" ], "examples": [ "dws todo +remind --task \"交周报\" --at 2026-03-10T18:00:00+08:00" diff --git a/internal/helpers/common.go b/internal/helpers/common.go index a8c32dca..b1c9adee 100644 --- a/internal/helpers/common.go +++ b/internal/helpers/common.go @@ -14,9 +14,6 @@ package helpers import ( - "path/filepath" - "strings" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/spf13/cobra" @@ -52,26 +49,3 @@ func commandBoolFlag(cmd *cobra.Command, name string) bool { } return false } - -// renameBaseName removes one familiar file extension before calling -// rename_document. That service preserves the node's current extension, so -// forwarding "report.txt" for a .txt file would otherwise produce -// "report.txt.txt". Unknown dotted suffixes are preserved because they may be -// part of a display name (for example "release.v2"). -func renameBaseName(name string) string { - trimmed := strings.TrimSpace(name) - extension := strings.ToLower(strings.TrimPrefix(filepath.Ext(trimmed), ".")) - if _, ok := renamePreservedExtensions[extension]; !ok { - return trimmed - } - return strings.TrimSuffix(trimmed, filepath.Ext(trimmed)) -} - -var renamePreservedExtensions = map[string]struct{}{ - "7z": {}, "able": {}, "adoc": {}, "adraw": {}, "aform": {}, "amind": {}, - "appt": {}, "awbd": {}, "axls": {}, "csv": {}, "doc": {}, "docx": {}, - "gif": {}, "gz": {}, "jpeg": {}, "jpg": {}, "json": {}, "md": {}, - "mp3": {}, "mp4": {}, "pdf": {}, "png": {}, "ppt": {}, "pptx": {}, - "rar": {}, "svg": {}, "tar": {}, "txt": {}, "wav": {}, "webp": {}, - "xls": {}, "xlsx": {}, "xml": {}, "zip": {}, -} diff --git a/internal/helpers/contract_defects_test.go b/internal/helpers/contract_defects_test.go index 2dcdd35b..b50a34bf 100644 --- a/internal/helpers/contract_defects_test.go +++ b/internal/helpers/contract_defects_test.go @@ -6,6 +6,8 @@ package helpers import ( "bytes" "context" + "encoding/json" + "errors" "io" "strings" "testing" @@ -15,13 +17,23 @@ import ( ) type contractDefectCaller struct { - dryRun bool - calls []guardedMutationCall + dryRun bool + calls []guardedMutationCall + responses map[string]string + errors map[string]error } func (c *contractDefectCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) { c.calls = append(c.calls, guardedMutationCall{productID: productID, toolName: toolName, args: args}) - return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil + key := productID + "/" + toolName + if err := c.errors[key]; err != nil { + return nil, err + } + text := c.responses[key] + if text == "" { + text = `{}` + } + return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}, nil } func (*contractDefectCaller) Format() string { return "json" } @@ -85,42 +97,175 @@ func TestDocVersionRevertDryRunSkipsRemotePreflightAndEmitsPreview(t *testing.T) } } -func TestRenameDocumentRemovesOnePreservedExtension(t *testing.T) { +func TestDocRenamePreservesCallerProvidedDisplayName(t *testing.T) { + caller := &contractDefectCaller{} + if _, err := executeContractDefectCommand(t, caller, newDocCommand, + "rename", "--node", "node-1", "--name", "release.v2"); err != nil { + t.Fatalf("doc rename returned error: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("tool calls = %#v, want one rename call", caller.calls) + } + if got := caller.calls[0].args["newName"]; got != "release.v2" { + t.Fatalf("newName = %#v, want release.v2", got) + } +} + +func TestDriveRenameUsesNodeTypeAndCurrentExtension(t *testing.T) { tests := []struct { - name string - build func() *cobra.Command - args []string + name string + metadata string + nodeID string + wantFileID string + requested string + wantNewName string }{ { - name: "doc", - build: newDocCommand, - args: []string{"rename", "--node", "node-1", "--name", "report.txt"}, + name: "file strips matching extension outside old whitelist", + metadata: `{"result":{"type":"file","extension":"heic"}}`, + nodeID: "https://alidocs.dingtalk.com/i/nodes/node-1", + wantFileID: "node-1", + requested: "photo.HEIC", + wantNewName: "photo", }, { - name: "drive", - build: newDriveCommand, - args: []string{"rename", "--node", "node-1", "--name", "report.txt"}, + name: "folder preserves dotted display name", + metadata: `{"result":{"type":"folder","extension":"v2"}}`, + requested: "release.v2", + wantNewName: "release.v2", + }, + { + name: "different current extension is preserved", + metadata: `{"result":{"nodeType":"file","fileExtension":"txt"}}`, + requested: "report.final", + wantNewName: "report.final", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - caller := &contractDefectCaller{} - if _, err := executeContractDefectCommand(t, caller, test.build, test.args...); err != nil { - t.Fatalf("rename returned error: %v", err) + caller := &contractDefectCaller{responses: map[string]string{ + "drive/get_file_info": test.metadata, + }} + nodeID := test.nodeID + if nodeID == "" { + nodeID = "node-1" } - if len(caller.calls) != 1 { - t.Fatalf("tool calls = %#v, want one rename call", caller.calls) + if _, err := executeContractDefectCommand(t, caller, newDriveCommand, + "rename", "--node", nodeID, "--name", test.requested); err != nil { + t.Fatalf("drive rename returned error: %v", err) } - if got := caller.calls[0].args["newName"]; got != "report" { - t.Fatalf("newName = %#v, want report", got) + if len(caller.calls) != 2 { + t.Fatalf("tool calls = %#v, want metadata read plus rename", caller.calls) + } + if caller.calls[0].productID != "drive" || caller.calls[0].toolName != "get_file_info" { + t.Fatalf("first call = %#v, want drive/get_file_info", caller.calls[0]) + } + wantFileID := test.wantFileID + if wantFileID == "" { + wantFileID = nodeID + } + if got := caller.calls[0].args["fileId"]; got != wantFileID { + t.Fatalf("metadata fileId = %#v, want %q", got, wantFileID) + } + if caller.calls[1].productID != "doc" || caller.calls[1].toolName != "rename_document" { + t.Fatalf("second call = %#v, want doc/rename_document", caller.calls[1]) + } + if got := caller.calls[1].args["newName"]; got != test.wantNewName { + t.Fatalf("newName = %#v, want %q", got, test.wantNewName) } }) } +} - if got := renameBaseName("release.v2"); got != "release.v2" { - t.Fatalf("unknown dotted suffix = %q, want release.v2", got) +func TestDriveRenameMetadataFailuresDoNotMutate(t *testing.T) { + tests := []struct { + name string + responses map[string]string + errors map[string]error + }{ + { + name: "metadata call error", + errors: map[string]error{"drive/get_file_info": errors.New("metadata unavailable")}, + }, + { + name: "invalid metadata JSON", + responses: map[string]string{"drive/get_file_info": `{`}, + }, + { + name: "metadata result missing", + responses: map[string]string{"drive/get_file_info": `{}`}, + }, } - if got := renameBaseName("REPORT.XLSX"); got != "REPORT" { - t.Fatalf("case-insensitive extension normalization = %q, want REPORT", got) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + caller := &contractDefectCaller{responses: test.responses, errors: test.errors} + _, err := executeContractDefectCommand(t, caller, newDriveCommand, + "rename", "--node", "node-1", "--name", "report.txt") + if err == nil { + t.Fatal("drive rename returned nil error") + } + if len(caller.calls) != 1 || caller.calls[0].toolName != "get_file_info" { + t.Fatalf("tool calls = %#v, want metadata read only", caller.calls) + } + }) + } +} + +func TestDriveRenameDryRunDefersMetadataNormalization(t *testing.T) { + caller := &contractDefectCaller{dryRun: true} + output, err := executeContractDefectCommand(t, caller, newDriveCommand, + "rename", "--node", "node-1", "--name", "photo.heic", "--dry-run") + if err != nil { + t.Fatalf("drive rename dry-run returned error: %v", err) + } + if len(caller.calls) != 0 { + t.Fatalf("dry-run tool calls = %#v, want none", caller.calls) + } + if !strings.Contains(output, `"newName": "photo.heic"`) { + t.Fatalf("dry-run output = %q, want unmodified input name", output) + } +} + +func TestDriveRenameBaseNameConservativeEdges(t *testing.T) { + tests := []struct { + name string + nodeType string + extension string + want string + }{ + {name: ".txt", nodeType: "file", extension: "txt", want: ".txt"}, + {name: "report.txt", nodeType: "directory", extension: "txt", want: "report.txt"}, + {name: "report.txt", nodeType: "dir", extension: "txt", want: "report.txt"}, + {name: " report.txt ", nodeType: "file", extension: "", want: "report.txt"}, + {name: "report.txt", nodeType: "file", extension: ".txt", want: "report"}, + } + for _, test := range tests { + if got := driveRenameBaseName(test.name, test.nodeType, test.extension); got != test.want { + t.Fatalf("driveRenameBaseName(%q, %q, %q) = %q, want %q", + test.name, test.nodeType, test.extension, got, test.want) + } + } +} + +func TestDriveInfoRestoresFileSizeWhenDocMetadataIsNull(t *testing.T) { + caller := &contractDefectCaller{responses: map[string]string{ + "drive/get_file_info": `{"result":{"fileId":"node-1","extension":"adoc","fileSize":93682}}`, + "doc/get_document_info": `{"result":{"title":"Doc","fileSize":null}}`, + }} + output, err := executeContractDefectCommand(t, caller, newDriveCommand, + "info", "--node", "node-1") + if err != nil { + t.Fatalf("drive info returned error: %v", err) + } + var payload struct { + Result struct { + FileSize int64 `json:"fileSize"` + } `json:"result"` + } + if err := json.Unmarshal([]byte(output), &payload); err != nil { + t.Fatalf("decode drive info output %q: %v", output, err) + } + if payload.Result.FileSize != 93682 { + t.Fatalf("fileSize = %d, want 93682", payload.Result.FileSize) } } diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index 1826d299..e96bd6bf 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -1548,7 +1548,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 } return callMCPTool("rename_document", map[string]any{ "nodeId": nodeID, - "newName": renameBaseName(flagOrFallback(cmd, "name", "title")), + "newName": flagOrFallback(cmd, "name", "title"), }) }, } diff --git a/internal/helpers/drive.go b/internal/helpers/drive.go index bc046e95..6b82fb0b 100644 --- a/internal/helpers/drive.go +++ b/internal/helpers/drive.go @@ -19,6 +19,51 @@ import ( // get_upload_info, commit_upload // ────────────────────────────────────────────────────────── +func driveRenameBaseName(name, nodeType, currentExtension string) string { + trimmed := strings.TrimSpace(name) + switch strings.ToLower(strings.TrimSpace(nodeType)) { + case "folder", "dir", "directory": + return trimmed + } + + extension := strings.TrimLeft(strings.TrimSpace(currentExtension), ".") + if extension == "" { + return trimmed + } + suffix := "." + extension + if len(trimmed) <= len(suffix) || !strings.EqualFold(trimmed[len(trimmed)-len(suffix):], suffix) { + return trimmed + } + return trimmed[:len(trimmed)-len(suffix)] +} + +func resolveDriveRenameName(ctx context.Context, nodeID, name string) (string, error) { + fileID := nodeID + if parsedNodeID := extractNodeIDFromDocURL(nodeID); parsedNodeID != "" { + fileID = parsedNodeID + } + text, err := callMCPToolReturnTextOnServer(ctx, "drive", "get_file_info", map[string]any{ + "fileId": fileID, + }) + if err != nil { + return "", fmt.Errorf("无法读取节点元数据,未执行重命名: %w", err) + } + + var response map[string]any + if err := json.Unmarshal([]byte(text), &response); err != nil { + return "", fmt.Errorf("无法解析节点元数据,未执行重命名: %w", err) + } + result, ok := response["result"].(map[string]any) + if !ok { + return "", fmt.Errorf("节点元数据缺少 result,未执行重命名") + } + return driveRenameBaseName( + name, + firstStringField(result, "type", "nodeType", "fileType"), + firstStringField(result, "extension", "fileExtension", "ext"), + ), nil +} + func runDriveUpload(cmd *cobra.Command, _ []string) error { filePath := mustGetFlag(cmd, "file") if filePath == "" { @@ -834,8 +879,9 @@ func newDriveCommand() *cobra.Command { driveRenameCmd := &cobra.Command{ Use: "rename", Short: "重命名文件/文档", - Long: `修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名; -如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。 + Long: `修改文档空间中文档、文件或文件夹的名称。 +实际执行前会读取节点类型和当前扩展名:文件的新名称仅在末尾扩展名与当前扩展名一致时去掉一层, +避免 report.txt.txt;文件夹和扩展名不匹配的名称保持不变。dry-run 不读取远端元数据,因此保留输入名称。 权限要求: 对文档有"编辑"权限。`, Example: ` dws drive rename --node DOC_ID --name "新名称"`, @@ -847,14 +893,21 @@ func newDriveCommand() *cobra.Command { if err != nil { return err } + newName := flagOrFallback(cmd, "name", "title") + if !commandDryRun(cmd) { + newName, err = resolveDriveRenameName(cmd.Context(), nodeID, newName) + if err != nil { + return err + } + } return callMCPToolOnServer("doc", "rename_document", map[string]any{ "nodeId": nodeID, - "newName": renameBaseName(flagOrFallback(cmd, "name", "title")), + "newName": newName, }) }, } driveRenameCmd.Flags().String("node", "", "文档/文件 ID 或 URL (必填)") - driveRenameCmd.Flags().String("name", "", "新名称 (必填;服务端保留原扩展名,传入常见扩展名时 CLI 会自动去掉一层)") + driveRenameCmd.Flags().String("name", "", "新名称 (必填;实际执行时仅去掉与节点当前扩展名完全匹配的一层后缀)") driveStatsCmd := &cobra.Command{ Use: "stats", @@ -1384,7 +1437,7 @@ func driveInfoWithDocFallback(fileID string, driveArgs map[string]any) error { ctx := context.Background() // Step 1: 调用 drive get_file_info - driveText, err := callMCPToolReturnText(ctx, "get_file_info", driveArgs) + driveText, err := callMCPToolReturnTextOnServer(ctx, "drive", "get_file_info", driveArgs) if err != nil { return err } @@ -1445,7 +1498,8 @@ func driveInfoWithDocFallback(fileID string, driveArgs map[string]any) error { driveOnlyFields := []string{"dentryId", "path", "fileSize", "extension", "type"} for _, field := range driveOnlyFields { if val, ok := driveResult[field]; ok { - if _, exists := docResult[field]; !exists { + current, exists := docResult[field] + if (!exists || current == nil) && val != nil { docResult[field] = val } } diff --git a/internal/helpers/todo.go b/internal/helpers/todo.go index 189162c0..9b46b53d 100644 --- a/internal/helpers/todo.go +++ b/internal/helpers/todo.go @@ -481,7 +481,8 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 todoTaskUpdateReminderCmd := &cobra.Command{ Use: "reset-reminder", Short: "重置待办提醒", - Long: `整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。 + Long: `整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。显式传值必须是合法 JSON 数组, +每条规则必须按 baseTime 提供 dueDateOffset 或 reminderTimeStamp;非法输入会在远端调用前失败。 当前上游没有提醒规则查询接口,task get/list 也不返回 reminderRules; 成功响应是写入回执,不代表 CLI 能再次读取并核验最终规则。`, @@ -493,28 +494,14 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 if err := validateRequiredFlags(cmd, "task-id"); err != nil { return err } - var reminderRules []any - if v, _ := cmd.Flags().GetString("reminder-rules"); v != "" { - var att []any - if err := json.Unmarshal([]byte(v), &att); err == nil && len(att) > 0 { - for i, item := range att { - m, ok := item.(map[string]any) - if !ok { - continue - } - if m["baseTime"] == "customTime" { - if ts, ok := m["reminderTimeStamp"].(string); ok && ts != "" { - ms, err := parseISOTimeToMillis("reminderTimeStamp", ts) - if err != nil { - return err - } - m["reminderTimeStamp"] = ms - att[i] = m - } - } - } - reminderRules = att + 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{ @@ -686,7 +673,7 @@ reset-reminder 写入的提醒规则。提醒写命令的成功响应只能作 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 数组 (可选,为空则清除提醒)") + todoTaskUpdateReminderCmd.Flags().String("reminder-rules", "", "提醒规则 JSON 数组 (不传则清除;显式传值必须合法)") todoTaskListSubCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskListAttachmentCmd.Flags().String("task-id", "", "待办任务 ID (必填)") todoTaskAddAttachment.Flags().String("task-id", "", "待办任务 ID (必填)") @@ -934,13 +921,81 @@ func rejectUnsupportedTodoReminderFlags(cmd *cobra.Command) error { return nil } return &CLIError{ - Code: CodeMissingParam, + Code: CodeInvalidParam, Message: fmt.Sprintf("todo 当前不支持独立 reminder 参数: %s", strings.Join(unsupported, ", ")), - Suggestion: "请先用 --due 设置截止时间;需要独立提醒时,再用 dws todo task add-reminder。当前上游不支持读取提醒规则,不能用 task get/list 验证写入结果。", + 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() diff --git a/internal/helpers/todo_command_edges_more_test.go b/internal/helpers/todo_command_edges_more_test.go index fa584872..036bcad3 100644 --- a/internal/helpers/todo_command_edges_more_test.go +++ b/internal/helpers/todo_command_edges_more_test.go @@ -137,6 +137,8 @@ func TestCrossPlatformCoverageTodoUpdateReminderAndDetailEdges(t *testing.T) { {"task", "update", "--task-id", "1", "--due", "bad"}, {"task", "add-reminder", "--task-id", "1", "--base-time", "customTime", "--reminder-time-stamp", "bad"}, {"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `[{"baseTime":"customTime","reminderTimeStamp":"bad"}]`}, + {"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `not-json`}, + {"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `[1,{"baseTime":"dueTime"},{"baseTime":"customTime","reminderTimeStamp":"` + validDate + `"}]`}, } for _, args := range errorCases { if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err == nil { @@ -148,8 +150,6 @@ func TestCrossPlatformCoverageTodoUpdateReminderAndDetailEdges(t *testing.T) { {"task", "update", "--task-id", "1", "--priority", "bad"}, {"task", "add-reminder", "--task-id", "1", "--base-time", "dueTime", "--due-date-offset", "-10"}, {"task", "add-reminder", "--task-id", "1", "--base-time", "customTime", "--reminder-time-stamp", validDate}, - {"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `not-json`}, - {"task", "reset-reminder", "--task-id", "1", "--reminder-rules", `[1,{"baseTime":"dueTime"},{"baseTime":"customTime","reminderTimeStamp":"` + validDate + `"}]`}, } for _, args := range validCases { if err := executeTodoEdge(t, &scriptedToolCaller{}, args...); err != nil { diff --git a/internal/helpers/todo_reminder_test.go b/internal/helpers/todo_reminder_test.go index d6159b4b..81d9bd61 100644 --- a/internal/helpers/todo_reminder_test.go +++ b/internal/helpers/todo_reminder_test.go @@ -33,6 +33,10 @@ func (*todoReminderCaller) Fields() string { return "" } func (*todoReminderCaller) JQ() string { return "" } func runTodoReminderCommand(t *testing.T, args ...string) (*todoReminderCaller, error) { + return runTodoTaskCommandForReminderTests(t, "add-reminder", args...) +} + +func runTodoTaskCommandForReminderTests(t *testing.T, leaf string, args ...string) (*todoReminderCaller, error) { t.Helper() previousDeps := deps t.Cleanup(func() { deps = previousDeps }) @@ -44,7 +48,7 @@ func runTodoReminderCommand(t *testing.T, args ...string) (*todoReminderCaller, cmd := newTodoCommand() cmd.SilenceErrors = true cmd.SilenceUsage = true - cmd.SetArgs(append([]string{"task", "add-reminder"}, args...)) + cmd.SetArgs(append([]string{"task", leaf}, args...)) return caller, cmd.Execute() } @@ -137,6 +141,116 @@ func TestTodoAddReminderMapsReviewedModes(t *testing.T) { } } +func TestTodoResetReminderRejectsInvalidNonEmptyRulesBeforeCallingTool(t *testing.T) { + tests := []struct { + name string + value string + wantErr string + }{ + {name: "explicit empty", value: "", wantErr: "不能为空"}, + {name: "whitespace", value: " ", wantErr: "不能为空"}, + {name: "malformed JSON", value: `[`, wantErr: "合法 JSON 数组"}, + {name: "object instead of array", value: `{}`, wantErr: "由对象组成的 JSON 数组"}, + {name: "scalar item", value: `[1]`, wantErr: "由对象组成的 JSON 数组"}, + {name: "null array", value: `null`, wantErr: "不能是 null"}, + {name: "null item", value: `[null]`, wantErr: "第 1 条必须是对象"}, + {name: "missing base time", value: `[{}]`, wantErr: "缺少字符串 baseTime"}, + {name: "non-string base time", value: `[{"baseTime":1}]`, wantErr: "缺少字符串 baseTime"}, + {name: "due time missing offset", value: `[{"baseTime":"dueTime"}]`, wantErr: "必须提供整数 dueDateOffset"}, + {name: "due time string offset", value: `[{"baseTime":"dueTime","dueDateOffset":"-30"}]`, wantErr: "必须提供整数 dueDateOffset"}, + {name: "due time fractional offset", value: `[{"baseTime":"dueTime","dueDateOffset":-1.5}]`, wantErr: "dueDateOffset 必须是整数"}, + {name: "custom time missing timestamp", value: `[{"baseTime":"customTime"}]`, wantErr: "必须提供 ISO8601 字符串"}, + {name: "custom time numeric timestamp", value: `[{"baseTime":"customTime","reminderTimeStamp":1}]`, wantErr: "必须提供 ISO8601 字符串"}, + {name: "custom time invalid timestamp", value: `[{"baseTime":"customTime","reminderTimeStamp":"tomorrow"}]`, wantErr: "reminderTimeStamp 无效"}, + {name: "unknown base time", value: `[{"baseTime":"deadline"}]`, wantErr: "baseTime 必须是 dueTime 或 customTime"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + caller, err := runTodoTaskCommandForReminderTests(t, "reset-reminder", + "--task-id", "task-smoke", "--reminder-rules", test.value) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("error = %v, want containing %q", err, test.wantErr) + } + if len(caller.calls) != 0 { + t.Fatalf("tool calls = %#v, want none", caller.calls) + } + }) + } +} + +func TestTodoResetReminderMapsValidatedRulesAndExplicitClears(t *testing.T) { + tests := []struct { + name string + args []string + wantRules []map[string]any + }{ + { + name: "omitted rules clear with null", + args: []string{"--task-id", "task-smoke"}, + wantRules: nil, + }, + { + name: "explicit empty array clears with array", + args: []string{"--task-id", "task-smoke", "--reminder-rules", `[]`}, + wantRules: []map[string]any{}, + }, + { + name: "valid rules are normalized", + args: []string{ + "--task-id", "task-smoke", + "--reminder-rules", + `[{"baseTime":"dueTime","dueDateOffset":-30},{"baseTime":"customTime","reminderTimeStamp":"2026-03-10T18:00:00+08:00","label":"keep"}]`, + }, + wantRules: []map[string]any{ + {"baseTime": "dueTime", "dueDateOffset": int64(-30)}, + {"baseTime": "customTime", "reminderTimeStamp": int64(1773136800000), "label": "keep"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + caller, err := runTodoTaskCommandForReminderTests(t, "reset-reminder", test.args...) + if err != nil { + t.Fatalf("todo reset-reminder returned error: %v", err) + } + if len(caller.calls) != 1 || caller.calls[0].toolName != "reset_todo_reminder" { + t.Fatalf("tool calls = %#v, want one reset_todo_reminder call", caller.calls) + } + request, ok := caller.calls[0].args["todoReminderUpdateRequest"].(map[string]any) + if !ok { + t.Fatalf("request = %#v, want object", caller.calls[0].args["todoReminderUpdateRequest"]) + } + if request["taskId"] != "task-smoke" { + t.Fatalf("taskId = %#v, want task-smoke", request["taskId"]) + } + rules, ok := request["reminderRules"].([]map[string]any) + if !ok { + t.Fatalf("reminderRules = %#v, want []map[string]any", request["reminderRules"]) + } + if !reflect.DeepEqual(rules, test.wantRules) { + t.Fatalf("reminderRules = %#v, want %#v", rules, test.wantRules) + } + }) + } +} + +func TestUnsupportedRemindAtGuidanceDoesNotRequireDueForCustomTime(t *testing.T) { + caller, err := runTodoTaskCommandForReminderTests(t, "create", + "--title", "x", "--executors", "user-1", "--remind-at", "2026-03-10T18:00:00+08:00") + if err == nil { + t.Fatal("todo create --remind-at returned nil error") + } + if !strings.Contains(err.Error(), "customTime") || + !strings.Contains(err.Error(), "不必先设置 --due") { + t.Fatalf("error = %v, want customTime guidance without due requirement", err) + } + if len(caller.calls) != 0 { + t.Fatalf("tool calls = %#v, want none", caller.calls) + } +} + func TestTodoRoleTypesCSVExampleUsesRuntimeParser(t *testing.T) { got, err := parseRoleTypes("creator,executor") if err != nil { diff --git a/internal/shortcut/smart/compatibility_coverage_test.go b/internal/shortcut/smart/compatibility_coverage_test.go index f2106084..d0b50ff9 100644 --- a/internal/shortcut/smart/compatibility_coverage_test.go +++ b/internal/shortcut/smart/compatibility_coverage_test.go @@ -40,6 +40,8 @@ func (f *platformCoverageCaller) CallTool(_ context.Context, product, tool strin switch product + "/" + tool { case "contact/search_contact_by_key_word": text = `{"result":[{"userId":"u1","name":"张三","openDingTalkId":"open1"}]}` + case "contact/get_current_user_profile": + text = `{"result":{"userId":"u1"}}` case "im/search_groups": text = `{"result":[{"openConversationId":"cid-1","title":"项目冲刺"}]}` } diff --git a/internal/shortcut/smart/remind_contract_test.go b/internal/shortcut/smart/remind_contract_test.go index bb4daf58..d18c3514 100644 --- a/internal/shortcut/smart/remind_contract_test.go +++ b/internal/shortcut/smart/remind_contract_test.go @@ -6,6 +6,8 @@ package smart import ( "strings" "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" ) func TestRemindShortcutDoesNotAdvertiseDueTimeAsReminder(t *testing.T) { @@ -18,3 +20,49 @@ func TestRemindShortcutDoesNotAdvertiseDueTimeAsReminder(t *testing.T) { } } } + +func TestRemindShortcutWritesAtOnlyAsDueTime(t *testing.T) { + fake := &platformCoverageCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "todo", "+remind", + "--task", "交周报", + "--at", "2026-03-10T18:00:00+08:00", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatalf("execute +remind: %v", err) + } + if len(fake.calls) != 2 { + t.Fatalf("tool calls = %#v, want profile read plus todo create", fake.calls) + } + call := fake.calls[1] + if call.product != "todo" || call.tool != "create_personal_todo" { + t.Fatalf("create call = %s/%s, want todo/create_personal_todo", call.product, call.tool) + } + request, ok := call.args["PersonalTodoCreateVO"].(map[string]any) + if !ok { + t.Fatalf("PersonalTodoCreateVO = %#v, want object", call.args["PersonalTodoCreateVO"]) + } + if got := request["dueTime"]; got != int64(1773136800000) { + t.Fatalf("dueTime = %#v, want 1773136800000", got) + } + if _, exists := request["reminderRules"]; exists { + t.Fatalf("unexpected reminderRules in %#v", request) + } +} + +func TestRemindShortcutRejectsInvalidAtBeforeTodoCreate(t *testing.T) { + fake := &platformCoverageCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{"todo", "+remind", "--task", "交周报", "--at", "tomorrow", "--yes"}) + err := root.Execute() + if err == nil || !strings.Contains(err.Error(), "--at 时间格式无效") { + t.Fatalf("error = %v, want invalid --at validation", err) + } + if len(fake.calls) != 1 || fake.calls[0].tool != "get_current_user_profile" { + t.Fatalf("tool calls = %#v, want profile read only", fake.calls) + } +} diff --git a/skills/mono/references/products/drive.md b/skills/mono/references/products/drive.md index 8b907dd8..3b9ed6fb 100644 --- a/skills/mono/references/products/drive.md +++ b/skills/mono/references/products/drive.md @@ -425,7 +425,7 @@ Flags: --name string 新名称 (仅 rename 必填) ``` -> **rename 只传主名,不要带扩展名**:服务端会按文件原扩展名自动补一个后缀。若 `--name` 里已带扩展名(如 `报告.txt`),回读会变成双扩展名 `报告.txt.txt`。正确做法:`dws drive rename --node --name "报告"`(不含 `.txt`),系统自动补回 `报告.txt`。 +> **rename 会按真实节点元数据处理扩展名**:实际执行前先读取节点类型和当前扩展名。文件的新名称若以当前扩展名结尾,只去掉完全匹配的一层再交给会保留原后缀的服务端;文件夹(包括 `release.v2` 这类带点名称)保持原样,也不再依赖扩展名白名单。`--dry-run` 不读取远端元数据,因此预览保留输入名称。 权限要求:copy 需对源文档有"阅读"权限且对目标文件夹有"编辑"权限;move 需对源文档有"管理"权限且对目标文件夹有"编辑"权限;rename 需对文档有"编辑"权限。 diff --git a/skills/mono/references/products/todo.md b/skills/mono/references/products/todo.md index ae88bde3..263efb5e 100644 --- a/skills/mono/references/products/todo.md +++ b/skills/mono/references/products/todo.md @@ -268,7 +268,7 @@ Example: dws todo task reset-reminder --task-id dws todo task reset-reminder --task-id --reminder-rules '[{"dueDateOffset":-30,"baseTime":"dueTime"},{"reminderTimeStamp":"2026-03-10T18:00:00+08:00","baseTime":"customTime"}]' Flags: - --reminder-rules string 提醒规则 JSON 数组 (可选,为空则清除提醒) + --reminder-rules string 提醒规则 JSON 数组 (不传则清除;显式传值必须合法) --task-id string 待办任务 ID (必填) ``` @@ -291,6 +291,8 @@ JSON 数组,每个元素为一条提醒规则,支持两种 `baseTime` 模式 ``` 以上表示两条提醒规则:第一条在截止时间前 30 分钟提醒,第二条在指定时间(ISO-8601)提醒。 +不传 `--reminder-rules` 或显式传 `[]` 表示清除提醒。其他显式值必须是合法对象数组:每条规则必须提供 `baseTime`;`dueTime` 必须带整数 `dueDateOffset`,`customTime` 必须带 ISO-8601 `reminderTimeStamp`。非法 JSON、`null`、标量元素或缺失模式字段都会在调用远端前报错,不会静默按 `null` 清空提醒。 + ### 给待办打标签 ``` Usage: @@ -472,7 +474,8 @@ dws todo task list-sub --task-id --format json - `task add-participant` / `task remove-participant` 用于管理待办的参与人,`--participants` 支持逗号分隔的多个 userId - 执行人 (executor) 与参与人 (participant) 的区别:执行人负责完成待办,参与人仅关注待办进度 - `task add-reminder` 用于为待办添加提醒,`--base-time` 支持 `dueTime`(基于截止时间偏移,待办必须有截止时间)和 `customTime`(自定义时间戳)两种模式 -- `task reset-reminder` 用于重置待办提醒规则,不传 `--reminder-rules` 则清除所有提醒 +- `customTime` 直接使用 `--reminder-time-stamp`,不要求先设置 `--due`;只有 `dueTime` 模式依赖待办截止时间 +- `task reset-reminder` 用于重置待办提醒规则,不传 `--reminder-rules` 或传 `[]` 则清除所有提醒;其他值会严格校验,非法输入不会发出远端调用 - `task add-attachment` / `list-attachment` / `remove-attachment` 三条附件命令均可用;`add-attachment` 会真实上传文件,勿用于试探性调用,先确认待办存在 - 附件 ID 的取法:`add-attachment` 从 `result.attachmentIds[]` 取,`list-attachment` 从顶层 `attachments[].attachmentId` 取;`remove-attachment` 用 `--attachment-id` + `--yes` - 子待办 ID 只能从 `task list-sub` 的顶层 `subTasks[].taskId` 取;`task get` 的 `result.todoDetailModel.subTodos[]` 没有 taskId 字段 diff --git a/skills/multi/dingtalk-drive/SKILL.md b/skills/multi/dingtalk-drive/SKILL.md index 4bf6a30d..95ff2ef6 100644 --- a/skills/multi/dingtalk-drive/SKILL.md +++ b/skills/multi/dingtalk-drive/SKILL.md @@ -55,7 +55,7 @@ metadata: | "上传本地文件" | `dws drive upload --file ./report.pdf [--folder ]` | | "覆盖钉盘或知识库里的已有文件" | `dws drive upload --file ./updated.pdf --node --dry-run`,确认后加 `--yes` | | "建文件夹" | `dws drive mkdir --name "<名称>" [--folder ]` | -| "复制 / 移动 / 重命名" | `dws drive copy` / `move` / `rename --node --name "<主名>"` | +| "复制 / 移动 / 重命名" | `dws drive copy` / `move` / `rename --node --name "<新名称>"` | | "删除文件 / 移到回收站(需确认)" | `dws drive delete --node --yes` | | "回收站 / 还原" | `dws drive recycle list` / `recycle restore --id ` | | "公开 / 取消公开 / 查公开状态" | `dws drive publish set` / `unset` / `get --node ` | @@ -65,7 +65,7 @@ metadata: - 找文件优先用 `drive search --query "<关键词>"`(不知道位置时全局搜);只有需要逐层浏览时才用 `drive list`。命中后必须 `drive info --node --format json` 回读元数据。 - **ID 字段选择**:`drive list` 返回同时有 `dentryId`(纯数字)和 `fileId`(UUID 格式)。所有 `--node` 和 `--folder` 参数**必须用 `fileId`**,纯数字 `dentryId` 会被拒绝。 - `drive list` 默认 `--limit 20`,最大 50;要更多用 `--cursor` 翻页,不要因参数边界报错反复重试。 -- `rename` 的 `--name` **只传主名,不带扩展名**;服务端会按原扩展名自动补后缀,带了扩展名会变成双扩展名(如 `报告.txt` → `报告.txt.txt`)。 +- `rename` 实际执行前读取节点类型和当前扩展名;仅对非文件夹且末尾后缀与当前扩展名完全匹配的名称去掉一层,避免双扩展名。带点文件夹名保持原样;`--dry-run` 不读取元数据,预览保留输入名称。 - `drive download` 需要 `--output` 指定本地保存路径或目录;不要省略必填输出位置。 - 删除、覆盖、移动、公开(publish set/unset)等破坏性操作必须先确认;上传、创建文件夹、下载后要读回或列目录验证。 - `drive upload --node ` 覆盖已有文件,`--node` 与新建文件使用的 `--folder` 互斥;先 `--dry-run`,获得明确确认后再加 `--yes`。 diff --git a/skills/multi/dingtalk-drive/references/drive.md b/skills/multi/dingtalk-drive/references/drive.md index 81ffdf02..79e65ee6 100644 --- a/skills/multi/dingtalk-drive/references/drive.md +++ b/skills/multi/dingtalk-drive/references/drive.md @@ -425,7 +425,7 @@ Flags: --name string 新名称 (仅 rename 必填) ``` -> **rename 只传主名,不要带扩展名**:服务端会按文件原扩展名自动补一个后缀。若 `--name` 里已带扩展名(如 `报告.txt`),回读会变成双扩展名 `报告.txt.txt`。正确做法:`dws drive rename --node --name "报告"`(不含 `.txt`),系统自动补回 `报告.txt`。 +> **rename 会按真实节点元数据处理扩展名**:实际执行前先读取节点类型和当前扩展名。文件的新名称若以当前扩展名结尾,只去掉完全匹配的一层再交给会保留原后缀的服务端;文件夹(包括 `release.v2` 这类带点名称)保持原样,也不再依赖扩展名白名单。`--dry-run` 不读取远端元数据,因此预览保留输入名称。 权限要求:copy 需对源文档有"阅读"权限且对目标文件夹有"编辑"权限;move 需对源文档有"管理"权限且对目标文件夹有"编辑"权限;rename 需对文档有"编辑"权限。 diff --git a/skills/multi/dingtalk-todo/SKILL.md b/skills/multi/dingtalk-todo/SKILL.md index 33ce05cc..4e98141f 100644 --- a/skills/multi/dingtalk-todo/SKILL.md +++ b/skills/multi/dingtalk-todo/SKILL.md @@ -71,6 +71,8 @@ metadata: - `--id` / `--ids` 是隐藏兼容别名,文档和生成命令统一写 `--task-id`,减少模型漂移。 - 优先级映射:低=10,普通=20,较高/高/重要=30,紧急/最高/P0/马上处理=40;不要把"较高"写成 40。 - 截止时间必须是 ISO-8601。相对日期按当前日期计算;例如周五说"下周二"就是紧接下一个自然周的周二,不要再加一周。 +- 独立提醒使用 `task add-reminder`:`dueTime` 依赖待办已有截止时间,`customTime` 直接传 `--reminder-time-stamp`,不要求先设置 `--due`。隐藏兼容参数 `--remind-at` 不可用。 +- `task reset-reminder` 不传 `--reminder-rules` 或传 `[]` 表示清除;其他显式值必须是合法对象数组并通过模式字段校验,非法输入禁止发出远端调用。 - 附件命令 `task add-attachment` / `list-attachment` / `remove-attachment` 均可用:`add-attachment --task-id --file-path <本地文件>`(真实上传,先确认待办存在,返回 `result.attachmentIds`);`list-attachment --task-id `(返回顶层 `attachments[].attachmentId`);`remove-attachment --task-id --attachment-id --yes`(不可逆,先确认)。 - `tag add` 只是把标签关联到任务;`tag delete` 删除标签定义且不可恢复,必须先确认再加 `--yes`。`tag add` 最多传 2 个标签编码。 - 创建、标记完成、重开、删除后必须 `task get` 或对应 `task list --status ...` 验证,不要只凭创建返回或口头计划结束。 diff --git a/skills/multi/dingtalk-todo/references/todo.md b/skills/multi/dingtalk-todo/references/todo.md index 8d36fab4..fa004c28 100644 --- a/skills/multi/dingtalk-todo/references/todo.md +++ b/skills/multi/dingtalk-todo/references/todo.md @@ -267,7 +267,7 @@ Example: dws todo task reset-reminder --task-id dws todo task reset-reminder --task-id --reminder-rules '[{"dueDateOffset":-30,"baseTime":"dueTime"},{"reminderTimeStamp":"2026-03-10T18:00:00+08:00","baseTime":"customTime"}]' Flags: - --reminder-rules string 提醒规则 JSON 数组 (可选,为空则清除提醒) + --reminder-rules string 提醒规则 JSON 数组 (不传则清除;显式传值必须合法) --task-id string 待办任务 ID (必填) ``` @@ -290,6 +290,8 @@ JSON 数组,每个元素为一条提醒规则,支持两种 `baseTime` 模式 ``` 以上表示两条提醒规则:第一条在截止时间前 30 分钟提醒,第二条在指定时间(ISO-8601)提醒。 +不传 `--reminder-rules` 或显式传 `[]` 表示清除提醒。其他显式值必须是合法对象数组:每条规则必须提供 `baseTime`;`dueTime` 必须带整数 `dueDateOffset`,`customTime` 必须带 ISO-8601 `reminderTimeStamp`。非法 JSON、`null`、标量元素或缺失模式字段都会在调用远端前报错,不会静默按 `null` 清空提醒。 + ### 给待办打标签 ``` Usage: @@ -470,7 +472,8 @@ dws todo task list-sub --task-id --format json - `task add-participant` / `task remove-participant` 用于管理待办的参与人,`--participants` 支持逗号分隔的多个 userId - 执行人 (executor) 与参与人 (participant) 的区别:执行人负责完成待办,参与人仅关注待办进度 - `task add-reminder` 用于为待办添加提醒,`--base-time` 支持 `dueTime`(基于截止时间偏移,待办必须有截止时间)和 `customTime`(自定义时间戳)两种模式 -- `task reset-reminder` 用于重置待办提醒规则,不传 `--reminder-rules` 则清除所有提醒 +- `customTime` 直接使用 `--reminder-time-stamp`,不要求先设置 `--due`;只有 `dueTime` 模式依赖待办截止时间 +- `task reset-reminder` 用于重置待办提醒规则,不传 `--reminder-rules` 或传 `[]` 则清除所有提醒;其他值会严格校验,非法输入不会发出远端调用 - `task add-attachment` / `list-attachment` / `remove-attachment` 三条附件命令均可用;`add-attachment` 会真实上传文件,勿用于试探性调用,先确认待办存在 - 附件 ID 的取法:`add-attachment` 从 `result.attachmentIds[]` 取,`list-attachment` 从顶层 `attachments[].attachmentId` 取;`remove-attachment` 用 `--attachment-id` + `--yes` - 子待办 ID 只能从 `task list-sub` 的顶层 `subTasks[].taskId` 取;`task get` 的 `result.todoDetailModel.subTodos[]` 没有 taskId 字段