fix: harden dws contract edge cases

This commit is contained in:
Dennis
2026-07-28 11:36:44 +08:00
parent a8e83e5e7e
commit ec7fdb0f0d
23 changed files with 758 additions and 266 deletions
+98 -40
View File
@@ -9377,7 +9377,7 @@
]
},
"doc info": {
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等,并从钉盘补齐缺失的文件大小)",
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
@@ -9386,21 +9386,21 @@
],
"confirmation": "not_required",
"effect": "read",
"effect_source": "command-verb",
"effect_source": "agent-hint",
"examples": [
"dws doc info --node \u003cDOC_ID\u003e --format json",
"dws doc info --node \"https://alidocs.dingtalk.com/i/nodes/\u003cDOC_UUID\u003e\" --format json"
],
"field_provenance": {
"agent_summary": {
"value": "获取文档元信息(标题/类型/创建者/权限等)",
"value": "获取文档元信息(标题/类型/创建者/权限等,并从钉盘补齐缺失的文件大小)",
"source": "internal/cli/schema_hints/selection/doc.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/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -9413,14 +9413,14 @@
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed"
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "available",
@@ -9454,29 +9454,45 @@
},
"confirmation": {
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "not_required",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "not_required",
"source": "risk-default",
"precedence": "inference_or_default",
"selected": true
"selected": false
}
]
},
"effect": {
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "read",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "read",
"source": "command-verb",
"precedence": "inference_or_default",
"selected": true
"selected": false
}
]
},
@@ -9504,31 +9520,39 @@
},
"idempotency": {
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "idempotent",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "idempotent",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
"selected": false
}
]
},
"interface_mode": {
"value": "mcp",
"value": "composite",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "mcp",
"value": "composite",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed"
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "mcp",
@@ -9538,17 +9562,43 @@
}
]
},
"interface_ref": {
"value": "doc.get_document_info",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"interface_reason": {
"value": "The CLI uses doc/get_document_info as the authoritative response and best-effort enriches a missing fileSize from drive/get_file_info, so no single MCP interface represents the delivered result.",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "The CLI uses doc/get_document_info as the authoritative response and best-effort enriches a missing fileSize from drive/get_file_info, so no single MCP interface represents the delivered result.",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
}
]
},
"interface_ref": {
"value": null,
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "interface_disposition_matrix",
"review_reason": "final interface mode composite forbids a direct MCP interface_ref",
"candidates": [
{
"value": null,
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "final interface mode composite forbids a direct MCP interface_ref"
}
],
"overridden_candidates": [
{
"value": "doc.get_document_info",
"source": "internal/cli/schema_hints/imported/wukong.json",
"precedence": "imported",
"selected": true
"selected": false
},
{
"value": "doc.get_document_info",
@@ -9563,14 +9613,14 @@
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/doc marks this tool as reviewed"
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": true,
@@ -9583,22 +9633,31 @@
},
"risk": {
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"candidates": [
{
"value": "low",
"source": "internal/cli/schema_hints/metadata/doc.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields."
},
{
"value": "low",
"source": "effect-default",
"precedence": "inference_or_default",
"selected": true
"selected": false
}
]
},
"use_when": {
"value": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时",
"文档接口本身缺少 fileSize,但用户需要读取上传文件大小时"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9608,7 +9667,8 @@
{
"value": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时",
"文档接口本身缺少 fileSize,但用户需要读取上传文件大小时"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -9619,11 +9679,8 @@
}
},
"idempotency": "idempotent",
"interface_mode": "mcp",
"interface_ref": {
"product_id": "doc",
"rpc_name": "get_document_info"
},
"interface_mode": "composite",
"interface_reason": "The CLI uses doc/get_document_info as the authoritative response and best-effort enriches a missing fileSize from drive/get_file_info, so no single MCP interface represents the delivered result.",
"reviewed": true,
"risk": "low",
"source_refs": [
@@ -9645,7 +9702,8 @@
],
"use_when": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时",
"文档接口本身缺少 fileSize,但用户需要读取上传文件大小时"
]
},
"doc list": {
@@ -12274,7 +12332,7 @@
},
"use_when": {
"value": [
"用户要改文档/文件在列表与链接中展示的名称时"
"用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -12283,7 +12341,7 @@
"candidates": [
{
"value": [
"用户要改文档/文件在列表与链接中展示的名称时"
"用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端"
],
"source": "internal/cli/schema_hints/selection/doc.json",
"precedence": "reviewed_explicit",
@@ -12315,7 +12373,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#doc.rename_document"
],
"use_when": [
"用户要改文档/文件在列表与链接中展示的名称时"
"用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端"
]
},
"doc search": {
+14 -14
View File
@@ -7083,7 +7083,7 @@
]
},
"drive rename": {
"agent_summary": "修改文档空间中文档或文件的名称",
"agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名",
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
@@ -7098,14 +7098,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 +7118,14 @@
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed",
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.",
"candidates": [
{
"value": "available",
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed"
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension."
},
{
"value": "available",
@@ -7224,14 +7224,14 @@
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed",
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.",
"candidates": [
{
"value": "mcp",
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed"
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension."
},
{
"value": "mcp",
@@ -7246,14 +7246,14 @@
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed",
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.",
"candidates": [
{
"value": "doc.rename_document",
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed"
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension."
},
{
"value": "doc.rename_document",
@@ -7268,14 +7268,14 @@
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed",
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.",
"candidates": [
{
"value": true,
"source": "internal/cli/schema_hints/metadata/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "dws-tool-metadata/drive marks this tool as reviewed"
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension."
},
{
"value": true,
@@ -7302,7 +7302,7 @@
},
"use_when": {
"value": [
"用户要重命名钉盘/文档空间中的文件或文档显示名称时"
"用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
@@ -7311,7 +7311,7 @@
"candidates": [
{
"value": [
"用户要重命名钉盘/文档空间中的文件或文档显示名称时"
"用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
@@ -7342,7 +7342,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#drive.rename_document"
],
"use_when": [
"用户要重命名钉盘/文档空间中的文件或文档显示名称时"
"用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层"
]
},
"drive search": {
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:557f4019cc83df15cbfe6d1611774331b0bdde87c0b5748c4d37d778b35abade",
"source_hash": "sha256:920c33b1853777cdb21a14d6e44ba75d3e0497f3470b3f04f10fa0a3f8715cb5",
"surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed",
"coverage": {
"surface_products": 26,
+32 -21
View File
@@ -5829,12 +5829,13 @@
]
},
"todo task add-reminder": {
"agent_summary": "添加待办提醒",
"agent_summary": "写入一条待办提醒规则(上游不支持规则读回)",
"agent_summary_source": "dws-agent-selection/todo",
"availability": "available",
"avoid_when": [
"要重置/清除提醒规则时改用 dws todo task reset-reminder",
"提醒时间与目标待办未确认时不要添加"
"提醒时间与目标待办未确认时不要添加",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"confirmation": "not_required",
"effect": "write",
@@ -5845,14 +5846,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "添加待办提醒",
"value": "写入一条待办提醒规则(上游不支持规则读回)",
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "结合本机 dws schema 实时描述、Skill 与 Cobra 手写选型;pinned schema_mcp_metadata 仅对照。",
"candidates": [
{
"value": "添加待办提醒",
"value": "写入一条待办提醒规则(上游不支持规则读回)",
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -5885,7 +5886,8 @@
"avoid_when": {
"value": [
"要重置/清除提醒规则时改用 dws todo task reset-reminder",
"提醒时间与目标待办未确认时不要添加"
"提醒时间与目标待办未确认时不要添加",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -5895,7 +5897,8 @@
{
"value": [
"要重置/清除提醒规则时改用 dws todo task reset-reminder",
"提醒时间与目标待办未确认时不要添加"
"提醒时间与目标待办未确认时不要添加",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -6043,7 +6046,7 @@
},
"use_when": {
"value": [
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间)"
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间);成功响应仅作为写入回执"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -6052,7 +6055,7 @@
"candidates": [
{
"value": [
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间)"
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间);成功响应仅作为写入回执"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -6078,11 +6081,12 @@
"internal/cli/schema_hints/selection/todo.json",
"internal/cli/schema_mcp_metadata.json#tools.todo.add_todo_reminder",
"live-dws-schema:todo.add_todo_reminder",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/todo.md",
"structured-hint:internal/cli/schema_hints/selection-review.json#todo.add_todo_reminder"
],
"use_when": [
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间)"
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间);成功响应仅作为写入回执"
]
},
"todo task create": {
@@ -6343,6 +6347,7 @@
"internal/cli/schema_hints/selection/todo.json",
"internal/cli/schema_mcp_metadata.json#tools.todo.create_personal_todo",
"live-dws-schema:todo.create_personal_todo",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/todo.md",
"structured-hint:internal/cli/schema_hints/imported/wukong.json#todo.create_personal_todo",
"structured-hint:internal/cli/schema_hints/selection-review.json#todo.create_personal_todo"
@@ -7183,7 +7188,8 @@
"availability": "available",
"avoid_when": [
"需要按条件查多个待办时改用 dws todo task list",
"需要修改待办时改用 update/done/delete 等写命令"
"需要修改待办时改用 update/done/delete 等写命令",
"需要读取或验证提醒规则时不要使用;当前详情接口不返回 reminderRules"
],
"confirmation": "not_required",
"effect": "read",
@@ -7234,7 +7240,8 @@
"avoid_when": {
"value": [
"需要按条件查多个待办时改用 dws todo task list",
"需要修改待办时改用 update/done/delete 等写命令"
"需要修改待办时改用 update/done/delete 等写命令",
"需要读取或验证提醒规则时不要使用;当前详情接口不返回 reminderRules"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -7244,7 +7251,8 @@
{
"value": [
"需要按条件查多个待办时改用 dws todo task list",
"需要修改待办时改用 update/done/delete 等写命令"
"需要修改待办时改用 update/done/delete 等写命令",
"需要读取或验证提醒规则时不要使用;当前详情接口不返回 reminderRules"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -8559,12 +8567,13 @@
]
},
"todo task reset-reminder": {
"agent_summary": "重置待办提醒",
"agent_summary": "整体替换或清除待办提醒规则(上游不支持规则读回)",
"agent_summary_source": "dws-agent-selection/todo",
"availability": "available",
"avoid_when": [
"只需追加一条提醒时改用 dws todo task add-reminder",
"新规则与目标待办未确认时不要重置"
"新规则与目标待办未确认时不要重置",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"confirmation": "not_required",
"effect": "write",
@@ -8575,14 +8584,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "重置待办提醒",
"value": "整体替换或清除待办提醒规则(上游不支持规则读回)",
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "结合本机 dws schema 实时描述、Skill 与 Cobra 手写选型;pinned schema_mcp_metadata 仅对照。",
"candidates": [
{
"value": "重置待办提醒",
"value": "整体替换或清除待办提醒规则(上游不支持规则读回)",
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -8615,7 +8624,8 @@
"avoid_when": {
"value": [
"只需追加一条提醒时改用 dws todo task add-reminder",
"新规则与目标待办未确认时不要重置"
"新规则与目标待办未确认时不要重置",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -8625,7 +8635,8 @@
{
"value": [
"只需追加一条提醒时改用 dws todo task add-reminder",
"新规则与目标待办未确认时不要重置"
"新规则与目标待办未确认时不要重置",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -8773,7 +8784,7 @@
},
"use_when": {
"value": [
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除)"
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -8782,7 +8793,7 @@
"candidates": [
{
"value": [
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除)"
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执"
],
"source": "internal/cli/schema_hints/selection/todo.json",
"precedence": "reviewed_explicit",
@@ -8812,7 +8823,7 @@
"structured-hint:internal/cli/schema_hints/selection-review.json#todo.reset_todo_reminder"
],
"use_when": [
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除)"
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执"
]
},
"todo task update": {
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:557f4019cc83df15cbfe6d1611774331b0bdde87c0b5748c4d37d778b35abade",
"source_hash": "sha256:920c33b1853777cdb21a14d6e44ba75d3e0497f3470b3f04f10fa0a3f8715cb5",
"surface_hash": "sha256:4cf8460240b19f896c3a330c69982cbb5f57aa8576cdf30373172082eea893ed",
"source_files": 160,
"hint_files": 54,
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -144,9 +144,16 @@
"runtime_gate": "none"
},
"doc.get_document_info": {
"interface_mode": "mcp",
"effect": "read",
"risk": "low",
"confirmation": "not_required",
"idempotency": "idempotent",
"interface_mode": "composite",
"availability": "available",
"reviewed": true
"interface_reason": "The CLI uses doc/get_document_info as the authoritative response and best-effort enriches a missing fileSize from drive/get_file_info, so no single MCP interface represents the delivered result.",
"reviewed": true,
"review_reason": "Reviewed composite metadata read fixes the document endpoint's missing fileSize without changing its authoritative title, type, owner, or permission fields.",
"runtime_gate": "none"
},
"doc.import_get": {
"effect": "write",
@@ -203,7 +203,14 @@
},
"interface_mode": "mcp",
"availability": "available",
"reviewed": true
"reviewed": true,
"parameters": {
"name": {
"description": "新显示名称;服务端保留节点原扩展名,若传入常见文件扩展名,CLI 会自动去掉一层以避免双扩展名"
}
},
"review_reason": "The reviewed CLI normalizes one familiar extension from --name because rename_document preserves the node's existing extension.",
"cli_path": "drive rename"
},
"drive.search_files": {
"interface_mode": "mcp",
+4 -3
View File
@@ -321,10 +321,11 @@
]
},
"doc.get_document_info": {
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等,并从钉盘补齐缺失的文件大小)",
"use_when": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时",
"文档接口本身缺少 fileSize,但用户需要读取上传文件大小时"
],
"avoid_when": [
"已确认是 adoc 且只要正文改用 dws doc read",
@@ -581,7 +582,7 @@
"doc.rename_document": {
"agent_summary": "兼容入口:重命名文档或文件;文件重命名能力已迁移到 drive。",
"use_when": [
"用户要改文档/文件在列表与链接中展示的名称时"
"用户要改文档/文件在列表与链接中展示的名称时;即使名称带常见扩展名,CLI 也会去掉一层再交给保留原扩展名的服务端"
],
"avoid_when": [
"改正文标题/章节标题改用 doc block update",
@@ -545,9 +545,9 @@
]
},
"drive.rename_document": {
"agent_summary": "修改文档空间中文档或文件的名称",
"agent_summary": "修改文档空间中文档或文件的名称,并避免服务端保留原扩展名导致双扩展名",
"use_when": [
"用户要重命名钉盘/文档空间中的文件或文档显示名称时"
"用户要重命名钉盘/文档空间中的文件或文档显示名称时;--name 带常见扩展名也可,CLI 会自动去掉一层"
],
"avoid_when": [
"要改正文里的标题/章节 H1 改用 dws doc block update,不要用 rename",
+10 -7
View File
@@ -103,13 +103,14 @@
]
},
"todo.add_todo_reminder": {
"agent_summary": "添加待办提醒",
"agent_summary": "写入一条待办提醒规则(上游不支持规则读回)",
"use_when": [
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间)"
"已知 taskId,需要按截止时间偏移或自定义时间戳添加提醒时(dueTime 模式要求待办已有截止时间);成功响应仅作为写入回执"
],
"avoid_when": [
"要重置/清除提醒规则时改用 dws todo task reset-reminder",
"提醒时间与目标待办未确认时不要添加"
"提醒时间与目标待办未确认时不要添加",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"examples": [
"dws todo task add-reminder --task-id <taskId> --base-time dueTime --due-date-offset -30",
@@ -224,7 +225,8 @@
],
"avoid_when": [
"需要按条件查多个待办时改用 dws todo task list",
"需要修改待办时改用 update/done/delete 等写命令"
"需要修改待办时改用 update/done/delete 等写命令",
"需要读取或验证提醒规则时不要使用;当前详情接口不返回 reminderRules"
],
"examples": [
"dws todo task get --task-id <taskId>",
@@ -356,13 +358,14 @@
]
},
"todo.reset_todo_reminder": {
"agent_summary": "重置待办提醒",
"agent_summary": "整体替换或清除待办提醒规则(上游不支持规则读回)",
"use_when": [
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除)"
"需要清除或整体替换待办提醒规则时(可不传 reminder-rules 以清除);成功响应仅作为写入回执"
],
"avoid_when": [
"只需追加一条提醒时改用 dws todo task add-reminder",
"新规则与目标待办未确认时不要重置"
"新规则与目标待办未确认时不要重置",
"业务要求写后读取并核验 reminderRules 时不要声称可验证;当前 task get/list 均不返回提醒规则"
],
"examples": [
"dws todo task reset-reminder --task-id <taskId>",
+26
View File
@@ -14,6 +14,9 @@
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"
@@ -49,3 +52,26 @@ 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": {},
}
+206
View File
@@ -0,0 +1,206 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type contractDefectCaller struct {
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})
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" }
func (c *contractDefectCaller) DryRun() bool { return c.dryRun }
func (*contractDefectCaller) Fields() string { return "" }
func (*contractDefectCaller) JQ() string { return "" }
func executeContractDefectCommand(t *testing.T, caller *contractDefectCaller, build func() *cobra.Command, args ...string) (string, error) {
t.Helper()
previousDeps := deps
t.Cleanup(func() { deps = previousDeps })
InitDeps(caller)
var stdout bytes.Buffer
deps.Out.w = &stdout
deps.Out.errW = io.Discard
root := build()
if root.PersistentFlags().Lookup("yes") == nil {
root.PersistentFlags().Bool("yes", false, "confirm high-risk operation")
}
if root.PersistentFlags().Lookup("dry-run") == nil {
root.PersistentFlags().Bool("dry-run", false, "preview without executing")
}
root.SilenceErrors = true
root.SilenceUsage = true
root.SetArgs(args)
err := root.Execute()
return stdout.String(), err
}
func TestApprovalRevokeDryRunSkipsConfirmationAndEmitsPreview(t *testing.T) {
caller := &contractDefectCaller{dryRun: true}
output, err := executeContractDefectCommand(t, caller, newOaCommand,
"approval", "revoke", "--instance-id", "instance-dry-run", "--dry-run")
if err != nil {
t.Fatalf("approval revoke 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, `"tool": "revoke_processInstance"`) ||
!strings.Contains(output, `"processInstanceId": "instance-dry-run"`) {
t.Fatalf("dry-run output = %q, want revoke preview", output)
}
}
func TestDocVersionRevertDryRunSkipsRemotePreflightAndEmitsPreview(t *testing.T) {
caller := &contractDefectCaller{dryRun: true}
output, err := executeContractDefectCommand(t, caller, newDocCommand,
"version", "revert", "--node", "node-dry-run", "--version", "7", "--dry-run")
if err != nil {
t.Fatalf("doc version revert 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, `"tool": "revert_doc_version"`) ||
!strings.Contains(output, `"version": 7`) {
t.Fatalf("dry-run output = %q, want version revert preview", output)
}
}
func TestRenameDocumentRemovesOnePreservedExtension(t *testing.T) {
tests := []struct {
name string
build func() *cobra.Command
args []string
}{
{
name: "doc",
build: newDocCommand,
args: []string{"rename", "--node", "node-1", "--name", "report.txt"},
},
{
name: "drive",
build: newDriveCommand,
args: []string{"rename", "--node", "node-1", "--name", "report.txt"},
},
}
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)
}
if len(caller.calls) != 1 {
t.Fatalf("tool calls = %#v, want one rename call", caller.calls)
}
if got := caller.calls[0].args["newName"]; got != "report" {
t.Fatalf("newName = %#v, want report", got)
}
})
}
if got := renameBaseName("release.v2"); got != "release.v2" {
t.Fatalf("unknown dotted suffix = %q, want release.v2", got)
}
if got := renameBaseName("REPORT.XLSX"); got != "REPORT" {
t.Fatalf("case-insensitive extension normalization = %q, want REPORT", got)
}
}
func TestDocInfoEnrichesMissingFileSizeFromDriveMetadata(t *testing.T) {
caller := &contractDefectCaller{
responses: map[string]string{
"doc/get_document_info": `{"result":{"title":"budget.xlsx","fileSize":null}}`,
"drive/get_file_info": `{"result":{"fileSize":93682}}`,
},
}
output, err := executeContractDefectCommand(t, caller, newDocCommand,
"info", "--node", "node-1")
if err != nil {
t.Fatalf("doc info returned error: %v", err)
}
wantCalls := []guardedMutationCall{
{productID: "doc", toolName: "get_document_info", args: map[string]any{"nodeId": "node-1"}},
{productID: "drive", toolName: "get_file_info", args: map[string]any{"fileId": "node-1"}},
}
if !reflect.DeepEqual(caller.calls, wantCalls) {
t.Fatalf("tool calls = %#v, want %#v", caller.calls, wantCalls)
}
var payload struct {
Result struct {
FileSize int64 `json:"fileSize"`
} `json:"result"`
}
if err := json.Unmarshal([]byte(output), &payload); err != nil {
t.Fatalf("decode doc info output %q: %v", output, err)
}
if payload.Result.FileSize != 93682 {
t.Fatalf("fileSize = %d, want 93682", payload.Result.FileSize)
}
}
func TestDocInfoFallsBackWhenDriveMetadataIsUnavailable(t *testing.T) {
caller := &contractDefectCaller{
responses: map[string]string{
"doc/get_document_info": `{"result":{"title":"online document"}}`,
},
errors: map[string]error{
"drive/get_file_info": fmt.Errorf("drive metadata unavailable"),
},
}
output, err := executeContractDefectCommand(t, caller, newDocCommand,
"info", "--node", "node-1")
if err != nil {
t.Fatalf("doc info fallback returned error: %v", err)
}
if !strings.Contains(output, `"title": "online document"`) {
t.Fatalf("fallback output = %q, want original document metadata", output)
}
}
func TestDocInfoDryRunUsesSingleStandardPreview(t *testing.T) {
caller := &contractDefectCaller{dryRun: true}
output, err := executeContractDefectCommand(t, caller, newDocCommand,
"info", "--node", "node-dry-run", "--dry-run")
if err != nil {
t.Fatalf("doc info 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, `"tool": "get_document_info"`) ||
!strings.Contains(output, `"nodeId": "node-dry-run"`) {
t.Fatalf("dry-run output = %q, want document info preview", output)
}
}
+73 -15
View File
@@ -61,6 +61,60 @@ func docVersionExists(ctx context.Context, nodeID string, version int) (bool, er
return false, nil
}
// docInfoWithDriveSize keeps the document metadata response authoritative and
// best-effort enriches its missing fileSize from drive/get_file_info. The
// document metadata endpoint does not expose a size for several uploaded file
// types even though the drive endpoint does.
func docInfoWithDriveSize(ctx context.Context, nodeID string) error {
docText, err := callMCPToolReturnTextOnServer(ctx, "doc", "get_document_info", map[string]any{
"nodeId": nodeID,
})
if err != nil {
return err
}
var docResponse map[string]any
if err := json.Unmarshal([]byte(docText), &docResponse); err != nil {
deps.Out.PrintRaw(docText)
return nil
}
docResult, wrapped := docResponse["result"].(map[string]any)
if !wrapped {
docResult = docResponse
}
if current, exists := docResult["fileSize"]; exists && current != nil {
return deps.Out.PrintJSON(docResponse)
}
fileID := nodeID
if extracted := extractNodeIDFromDocURL(nodeID); extracted != "" {
fileID = extracted
}
driveText, err := callMCPToolReturnTextOnServer(ctx, "drive", "get_file_info", map[string]any{
"fileId": fileID,
})
if err != nil {
return deps.Out.PrintJSON(docResponse)
}
var driveResponse map[string]any
if err := json.Unmarshal([]byte(driveText), &driveResponse); err != nil {
return deps.Out.PrintJSON(docResponse)
}
driveResult, ok := driveResponse["result"].(map[string]any)
if !ok {
driveResult = driveResponse
}
fileSize, exists := driveResult["fileSize"]
if !exists || fileSize == nil {
fileSize, exists = driveResult["size"]
}
if exists && fileSize != nil {
docResult["fileSize"] = fileSize
}
return deps.Out.PrintJSON(docResponse)
}
// docVersionNextCursor 从 list_doc_versions 响应中提取分页游标;没有下一页时返回 ""。
func docVersionNextCursor(v any) string {
switch val := v.(type) {
@@ -917,7 +971,8 @@ func newDocCommand() *cobra.Command {
infoCmd := &cobra.Command{
Use: "info",
Short: "获取文档元信息",
Long: `获取文档标题、类型、创建者、创建时间、权限等元信息 (不含内容)。`,
Long: `获取文档标题、类型、创建者、创建时间、权限等元信息 (不含内容)。
文档接口未返回 fileSize 时,会从钉盘元数据补齐文件大小。`,
Example: ` dws doc info --node DOC_ID
dws doc info --node "https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>"`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -925,9 +980,10 @@ func newDocCommand() *cobra.Command {
if err != nil {
return err
}
return callMCPTool("get_document_info", map[string]any{
"nodeId": nodeID,
})
if commandDryRun(cmd) {
return callMCPToolOnServer("doc", "get_document_info", map[string]any{"nodeId": nodeID})
}
return docInfoWithDriveSize(cmd.Context(), nodeID)
},
}
@@ -1550,7 +1606,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
}
return callMCPTool("rename_document", map[string]any{
"nodeId": nodeID,
"newName": flagOrFallback(cmd, "name", "title"),
"newName": renameBaseName(flagOrFallback(cmd, "name", "title")),
})
},
}
@@ -1740,7 +1796,7 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
// rename
renameCmd.Flags().String("node", "", "文档/文件 ID 或 URL (必填)")
renameCmd.Flags().String("name", "", "新名称 (必填)")
renameCmd.Flags().String("name", "", "新名称 (必填;服务端保留原扩展名,传入常见扩展名时 CLI 会自动去掉一层)")
// delete
deleteCmd.Flags().String("node", "", "文档/文件 ID 或 URL (必填)")
@@ -2709,15 +2765,17 @@ CLI 内部自动完成全部流程:
return fmt.Errorf("flag --version is required")
}
version, _ := cmd.Flags().GetInt("version")
exists, err := docVersionExists(cmd.Context(), nodeID, version)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("文档版本 %d 不存在,已停止回滚;请先执行 dws doc version list --node %s --format json 获取可回滚版本", version, nodeID)
}
if !confirmDangerousAction(cmd, fmt.Sprintf("revert document to version %d", version), nodeID) {
return nil
if !commandDryRun(cmd) {
exists, err := docVersionExists(cmd.Context(), nodeID, version)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("文档版本 %d 不存在,已停止回滚;请先执行 dws doc version list --node %s --format json 获取可回滚版本", version, nodeID)
}
if !confirmDangerousAction(cmd, fmt.Sprintf("revert document to version %d", version), nodeID) {
return nil
}
}
return callMCPToolOnServer("doc", "revert_doc_version", map[string]any{
"nodeId": nodeID,
+4 -3
View File
@@ -834,7 +834,8 @@ func newDriveCommand() *cobra.Command {
driveRenameCmd := &cobra.Command{
Use: "rename",
Short: "重命名文件/文档",
Long: `修改文档空间中文档或文件的名称。
Long: `修改文档空间中文档或文件的名称。服务端会保留节点的原扩展名;
如果 --name 带常见文件扩展名,CLI 会自动去掉一层,避免出现 report.txt.txt。
权限要求: 对文档有"编辑"权限。`,
Example: ` dws drive rename --node DOC_ID --name "新名称"`,
@@ -848,12 +849,12 @@ func newDriveCommand() *cobra.Command {
}
return callMCPToolOnServer("doc", "rename_document", map[string]any{
"nodeId": nodeID,
"newName": flagOrFallback(cmd, "name", "title"),
"newName": renameBaseName(flagOrFallback(cmd, "name", "title")),
})
},
}
driveRenameCmd.Flags().String("node", "", "文档/文件 ID 或 URL (必填)")
driveRenameCmd.Flags().String("name", "", "新名称 (必填)")
driveRenameCmd.Flags().String("name", "", "新名称 (必填;服务端保留原扩展名,传入常见扩展名时 CLI 会自动去掉一层)")
driveStatsCmd := &cobra.Command{
Use: "stats",
+1 -1
View File
@@ -129,7 +129,7 @@ func newOaCommand() *cobra.Command {
return err
}
instanceId := mustGetFlag(cmd, "instance-id")
if !confirmDelete("审批实例", instanceId) {
if !commandDryRun(cmd) && !confirmDelete("审批实例", instanceId) {
return nil
}
argsMap := map[string]any{
+13 -1
View File
@@ -282,6 +282,10 @@ func newTodoCommand() *cobra.Command {
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`,
@@ -427,6 +431,10 @@ func newTodoCommand() *cobra.Command {
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
@@ -473,6 +481,10 @@ func newTodoCommand() *cobra.Command {
todoTaskUpdateReminderCmd := &cobra.Command{
Use: "reset-reminder",
Short: "重置待办提醒",
Long: `整体替换待办提醒规则;不传 --reminder-rules 时清除提醒。
当前上游没有提醒规则查询接口,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
@@ -924,7 +936,7 @@ func rejectUnsupportedTodoReminderFlags(cmd *cobra.Command) error {
return &CLIError{
Code: CodeMissingParam,
Message: fmt.Sprintf("todo 当前不支持独立 reminder 参数: %s", strings.Join(unsupported, ", ")),
Suggestion: "请使用 --due 表示截止时间;如果用户要的是精确提醒时间,请明确说明该能力当前不支持,而不是把 --due 当作 reminder。",
Suggestion: "请先用 --due 设置截止时间;需要独立提醒时,再用 dws todo task add-reminder。当前上游不支持读取提醒规则,不能用 task get/list 验证写入结果。",
Operation: "todo.task.reminder",
}
}
+6 -6
View File
@@ -21,7 +21,7 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
// Remind: create a personal todo for YOURSELF with an optional due/reminder time,
// Remind: create a personal todo for YOURSELF with an optional due time,
// in one command. It resolves the current user and explicitly passes executorIds;
// the real todo backend does not reliably default a missing executor to "me".
//
@@ -35,13 +35,13 @@ var Remind = shortcut.Shortcut{
Service: "todo",
Command: "+remind",
Product: "todo",
Description: "给自己创建一条带截止/提醒时间的待办",
Intent: "当你想给自己记一件事、并(可选)设一个截止/提醒时间,又不想先查自己的 userId 时使用;" +
"内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 会按 ISO8601 解析为截止时间。会真实创建待办。",
Description: "给自己创建一条带可选截止时间的待办",
Intent: "当你想给自己记一件事、并(可选)设一个截止时间,又不想先查自己的 userId 时使用;" +
"内部先解析当前登录用户的 userId,再显式设置 executorIds,--at 只会按 ISO8601 写入截止时间 dueTime,不会创建独立提醒规则。会真实创建待办。",
Risk: shortcut.RiskWrite,
Flags: []shortcut.Flag{
{Name: "task", Type: shortcut.FlagString, Desc: "待办标题/内容", Required: true},
{Name: "at", Type: shortcut.FlagString, Desc: "截止/提醒时间(ISO8601,可选,如 2026-03-10T18:00:00+08:00)"},
{Name: "at", Type: shortcut.FlagString, Desc: "截止时间(ISO8601,可选,不是提醒时间;如 2026-03-10T18:00:00+08:00)"},
},
Tips: []string{`dws todo +remind --task "交周报" --at 2026-03-10T18:00:00+08:00`},
Execute: func(rt *shortcut.RuntimeContext) error {
@@ -59,7 +59,7 @@ var Remind = shortcut.Shortcut{
"executorIds": []string{userID},
}
// Optional due/reminder time. The todo helper feeds --due through
// Optional due time. The todo helper feeds --due through
// parseISOTimeToMillis and stores dueTime as epoch milliseconds (int64),
// so we do the same here rather than passing a raw string.
if rt.Changed("at") {
@@ -0,0 +1,20 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package smart
import (
"strings"
"testing"
)
func TestRemindShortcutDoesNotAdvertiseDueTimeAsReminder(t *testing.T) {
if strings.Contains(Remind.Description, "提醒时间") || strings.Contains(Remind.Intent, "截止/提醒") {
t.Fatalf("shortcut contract still conflates dueTime with a reminder: %#v", Remind)
}
for _, flag := range Remind.Flags {
if flag.Name == "at" && !strings.Contains(flag.Desc, "不是提醒时间") {
t.Fatalf("--at description = %q, want explicit dueTime boundary", flag.Desc)
}
}
}
+2 -2
View File
@@ -26,7 +26,7 @@
| "搜一下有没有叫XX的文件" | 全局搜索 | `drive search` | `wiki node search` | 未指定空间 → drive search 全局聚合搜索 |
| "在知识库里创建一个文档" | 创建空文件实体 | `wiki node create --type adoc` | `doc create` | 空间内创建节点归 wiki;doc create 是向已有文档写入内容,不是创建文件节点 |
| "帮我建一个明天下午的日程" | 日历日程 | `calendar` | — | 日历日程管理(可含参与者/会议室);视频会议(conference)当前开源 CLI 不支持 |
| "明早 9 点提醒我提交周报" | 创建个人待办,但需先声明 reminder 边界 | `todo` | `calendar` | todo 当前只支持 dueTime 截止时间,不支持独立精确 reminder |
| "明早 9 点提醒我提交周报" | 创建个人待办后添加提醒 | `todo task create` + `todo task add-reminder` | `calendar` | `+remind --at` 只写截止时间;提醒可写入,但上游不支持规则读回 |
| "帮我建一个项目群" | 创建群聊 | `chat group create` | — | 群聊管理,不是日历日程 |
| "把张三拉进群" | 添加群成员 | `chat group members add` | — | 先查 userId,再添加 |
| "通知群里的人都来开会" | 个人身份群发 | `chat message send` | `chat message send-by-bot` | 以个人身份向群发消息 |
@@ -330,7 +330,7 @@ dws chat message send --open-dingtalk-id <openDingTalkId> --msg-type file --file
**用 `todo` 的场景**:
- "记一下这周要做的事" — 个人任务管理
- "创建一个待办提醒" — 仍归 `todo`,但要先说明当前只有 dueTime 截止时间,没有独立 reminder schedule
- "创建一个待办提醒" — 仍归 `todo`:先创建待办,再用 `task add-reminder` 写入;需说明上游不支持读取 reminderRules,不能写后读回核验
**判断关键**:钉钉日志系统(日报/周报模版,含按模版创建汇报)→ `report`;文档/知识库长文→ `doc`;任务清单→ `todo`
+3 -2
View File
@@ -457,9 +457,10 @@ dws todo task list-sub --task-id <taskId> --format json
- 优先级值: 10=低, 20=普通, 30=较高, 40=紧急
- `--due` 是截止时间 dueTime,不是提醒时间;使用 ISO-8601 格式(如 2026-03-10T18:00:00+08:00)
- 当前不支持单独的 `reminder` / `remind-at` 精确提醒能力;不要把 `--due` 解释成“几点提醒”
- `todo +remind --at` 同样只写截止时间 dueTime,不会创建独立提醒规则;不要把它解释成“几点提醒”
- `--recurrence`:仅在与 `--due` 同时设置时有效;当前仅支持按天循环。字符串内需含换行,示例:`DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1`(DTSTART 表示首次截止时间,需与业务约定一致)
- 若用户的真实诉求是“到点提醒我”,需要先说明能力边界;当前 CLI 只能表达 deadline / recurrence,不能表达独立 reminder schedule
- 独立提醒可在待办创建后通过 `task add-reminder` 写入,或用 `task reset-reminder` 整体替换/清除
- 当前上游没有提醒规则查询接口,`task get/list` 均不返回 `reminderRules`;写命令成功响应只能作为写入回执,不能声称已读回核验
- `task list` 的 `--status` 对应 MCP `get_user_todos_in_current_org` 的 `todoStatus` 参数
- `task list --query-all` 查询当前用户跨组织的全部待办;不传时保持当前组织范围
- todo 是个人待办管理产品
+1 -1
View File
@@ -39,7 +39,7 @@ metadata:
| `dws todo +list-comment` | read | 查询待办评论列表 |
| `dws todo +list-sub` | read | 查询子待办列表 |
| `dws todo +overdue` | read | 列出我已过期未完成的待办 |
| `dws todo +remind` | write | 给自己创建一条带截止/提醒时间的待办 |
| `dws todo +remind` | write | 给自己创建一条带可选截止时间的待办(`--at` 只写 dueTime,不创建提醒规则) |
| `dws todo +todo-done` | write | 按标题关键词把我的某条待办标记完成(自动定位 taskId) |
<!-- VISIBLE_SHORTCUTS_END -->
@@ -455,9 +455,10 @@ dws todo task list-sub --task-id <taskId> --format json
- 优先级值: 10=低, 20=普通, 30=较高, 40=紧急
- `--due` 是截止时间 dueTime,不是提醒时间;使用 ISO-8601 格式(如 2026-03-10T18:00:00+08:00)
- 当前不支持单独的 `reminder` / `remind-at` 精确提醒能力;不要把 `--due` 解释成“几点提醒”
- `todo +remind --at` 同样只写截止时间 dueTime,不会创建独立提醒规则;不要把它解释成“几点提醒”
- `--recurrence`:仅在与 `--due` 同时设置时有效;当前仅支持按天循环。字符串内需含换行,示例:`DTSTART:20260320T020000Z\nRRULE:FREQ=DAILY;INTERVAL=1`(DTSTART 表示首次截止时间,需与业务约定一致)
- 若用户的真实诉求是“到点提醒我”,需要先说明能力边界;当前 CLI 只能表达 deadline / recurrence,不能表达独立 reminder schedule
- 独立提醒可在待办创建后通过 `task add-reminder` 写入,或用 `task reset-reminder` 整体替换/清除
- 当前上游没有提醒规则查询接口,`task get/list` 均不返回 `reminderRules`;写命令成功响应只能作为写入回执,不能声称已读回核验
- `task list` 的 `--status` 对应 MCP `get_user_todos_in_current_org` 的 `todoStatus` 参数
- `task list --query-all` 查询当前用户跨组织的全部待办;不传时保持当前组织范围
- todo 是个人待办管理产品