diff --git a/internal/cli/schema_agent_metadata/doc.json b/internal/cli/schema_agent_metadata/doc.json index 27387e12..6df4c260 100644 --- a/internal/cli/schema_agent_metadata/doc.json +++ b/internal/cli/schema_agent_metadata/doc.json @@ -4713,7 +4713,8 @@ "availability": "available", "avoid_when": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "confirmation": "not_required", @@ -4765,7 +4766,8 @@ "avoid_when": { "value": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "source": "internal/cli/schema_hints/selection/doc.json", @@ -4776,7 +4778,8 @@ { "value": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "source": "internal/cli/schema_hints/selection/doc.json", @@ -4925,7 +4928,8 @@ }, "use_when": { "value": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -4934,7 +4938,8 @@ "candidates": [ { "value": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -4968,7 +4973,8 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#doc.insert_document_block" ], "use_when": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ] }, "doc block list": { @@ -5248,7 +5254,8 @@ "availability": "available", "avoid_when": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "confirmation": "not_required", "effect": "write", @@ -5298,7 +5305,8 @@ "avoid_when": { "value": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -5308,7 +5316,8 @@ { "value": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -5454,7 +5463,7 @@ }, "use_when": { "value": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -5463,7 +5472,7 @@ "candidates": [ { "value": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -5497,7 +5506,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#doc.update_document_block" ], "use_when": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ] }, "doc comment create": { @@ -6644,7 +6653,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "confirmation": "not_required", "effect": "write", @@ -6694,7 +6704,8 @@ }, "avoid_when": { "value": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -6703,7 +6714,8 @@ "candidates": [ { "value": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -6897,7 +6909,7 @@ }, "use_when": { "value": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -6906,7 +6918,7 @@ "candidates": [ { "value": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -6941,7 +6953,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#doc.reply_comment" ], "use_when": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ] }, "doc comment update": { @@ -7501,7 +7513,7 @@ "avoid_when": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "confirmation": "not_required", "effect": "write", @@ -7553,7 +7565,7 @@ "value": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -7564,7 +7576,7 @@ "value": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -7775,7 +7787,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -7826,7 +7838,7 @@ }, "avoid_when": { "value": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -7837,7 +7849,7 @@ "candidates": [ { "value": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -9095,7 +9107,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "confirmation": "not_required", "effect": "write", @@ -9138,7 +9150,7 @@ }, "avoid_when": { "value": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -9147,7 +9159,7 @@ "candidates": [ { "value": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -9337,7 +9349,7 @@ }, "use_when": { "value": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -9346,7 +9358,7 @@ "candidates": [ { "value": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -9373,7 +9385,7 @@ "structured-hint:internal/cli/schema_hints/products/doc.json" ], "use_when": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ] }, "doc info": { @@ -9919,7 +9931,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "confirmation": "not_required", "effect": "read", @@ -9968,7 +9981,8 @@ }, "avoid_when": { "value": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -9977,7 +9991,8 @@ "candidates": [ { "value": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -10123,7 +10138,7 @@ }, "use_when": { "value": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -10132,7 +10147,7 @@ "candidates": [ { "value": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -10164,7 +10179,7 @@ "structured-hint:internal/cli/schema_hints/selection-review.json#doc.download_doc_attachment" ], "use_when": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ] }, "doc media insert": { @@ -14670,7 +14685,8 @@ "availability": "available", "avoid_when": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "confirmation": "not_required", @@ -14722,7 +14738,8 @@ "avoid_when": { "value": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "source": "internal/cli/schema_hints/selection/doc.json", @@ -14733,7 +14750,8 @@ { "value": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "source": "internal/cli/schema_hints/selection/doc.json", @@ -14914,7 +14932,8 @@ "value": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -14925,7 +14944,8 @@ "value": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ], "source": "internal/cli/schema_hints/selection/doc.json", "precedence": "reviewed_explicit", @@ -14964,7 +14984,8 @@ "use_when": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ] }, "doc upload": { diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index fb72b4b4..ad64ad25 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:add7be57e9ed6b69783be5df6d321c83ebe3b9c71a934e9991d91ee41fea23f4", + "source_hash": "sha256:36b1189bb92154252ca0cfcac9922367d8f03e9534814119d19ea03abe382ae8", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "coverage": { "surface_products": 26, diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index f265adaf..f2b7b15b 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:add7be57e9ed6b69783be5df6d321c83ebe3b9c71a934e9991d91ee41fea23f4", + "source_hash": "sha256:36b1189bb92154252ca0cfcac9922367d8f03e9534814119d19ea03abe382ae8", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "source_files": 160, "hint_files": 54, diff --git a/internal/cli/schema_catalog/catalog.json b/internal/cli/schema_catalog/catalog.json index b5bc81cf..bacac292 100644 --- a/internal/cli/schema_catalog/catalog.json +++ b/internal/cli/schema_catalog/catalog.json @@ -1,12 +1,12 @@ { "version": 1, "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", - "source_hash": "sha256:2bdcd3ac9d59f625cd4ed1e97938357b68ac483ef213a9e739baac67e3a1db3b", + "source_hash": "sha256:b3c04a9c98aca29718dabe8c52464a1ffadf6cc2da04bed37701e1651150ec37", "catalog": { "agent_metadata": { "products_with_metadata": 26, "source": "embedded-skill-metadata", - "source_hash": "sha256:add7be57e9ed6b69783be5df6d321c83ebe3b9c71a934e9991d91ee41fea23f4", + "source_hash": "sha256:36b1189bb92154252ca0cfcac9922367d8f03e9534814119d19ea03abe382ae8", "surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7", "surface_products": 26, "surface_tools": 875, @@ -15383,7 +15383,7 @@ "avoid_when": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "canonical_path": "doc.create_document", "cli_name": "create", @@ -15536,7 +15536,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -15600,7 +15600,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "canonical_path": "doc.download_doc_attachment", "cli_name": "download", @@ -15622,7 +15623,7 @@ "risk": "low", "title": "下载文档附件", "use_when": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ] }, { @@ -15759,7 +15760,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "canonical_path": "doc.import_get", "cli_name": "get", @@ -15780,7 +15781,7 @@ "risk": "medium", "title": "查询导入任务结果(手动兜底)", "use_when": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ] }, { @@ -15790,7 +15791,8 @@ "availability": "available", "avoid_when": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "canonical_path": "doc.insert_document_block", @@ -15813,7 +15815,8 @@ "risk": "medium", "title": "插入块元素", "use_when": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ] }, { @@ -16105,7 +16108,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "canonical_path": "doc.reply_comment", "cli_name": "reply", @@ -16127,7 +16131,7 @@ "risk": "medium", "title": "回复评论", "use_when": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ] }, { @@ -16825,7 +16829,8 @@ "availability": "available", "avoid_when": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "canonical_path": "doc.update_document", @@ -16849,7 +16854,8 @@ "use_when": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ] }, { @@ -16859,7 +16865,8 @@ "availability": "available", "avoid_when": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "canonical_path": "doc.update_document_block", "cli_name": "update", @@ -16881,7 +16888,7 @@ "risk": "medium", "title": "更新块元素", "use_when": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ] }, { diff --git a/internal/cli/schema_catalog/tools/doc.json b/internal/cli/schema_catalog/tools/doc.json index 9397637f..05b2765f 100644 --- a/internal/cli/schema_catalog/tools/doc.json +++ b/internal/cli/schema_catalog/tools/doc.json @@ -2322,7 +2322,7 @@ "avoid_when": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "canonical_path": "doc.create_document", "cli_name": "create", @@ -2385,7 +2385,7 @@ "value": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ] } ], @@ -2396,7 +2396,7 @@ "value": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ] }, "canonical_path": { @@ -6450,7 +6450,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -6512,7 +6512,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ] @@ -6523,7 +6523,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ] @@ -7557,7 +7557,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "canonical_path": "doc.download_doc_attachment", "cli_name": "download", @@ -7617,7 +7618,8 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ] } ], @@ -7626,7 +7628,8 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ] }, "canonical_path": { @@ -7848,7 +7851,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ] } ], @@ -7857,7 +7860,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ] } }, @@ -8095,7 +8098,7 @@ "source": "reviewed_command_registry", "title": "下载文档附件", "use_when": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ] }, "doc.download_file": { @@ -10939,7 +10942,7 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "canonical_path": "doc.import_get", "cli_name": "get", @@ -10996,7 +10999,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ] } ], @@ -11005,7 +11008,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ] }, "canonical_path": { @@ -11261,7 +11264,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ] } ], @@ -11270,7 +11273,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ] } }, @@ -11389,7 +11392,7 @@ "source": "reviewed_command_registry", "title": "查询导入任务结果(手动兜底)", "use_when": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ] }, "doc.insert_document_block": { @@ -11414,7 +11417,8 @@ "availability": "available", "avoid_when": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "canonical_path": "doc.insert_document_block", @@ -11486,7 +11490,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ] } @@ -11497,7 +11502,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ] }, @@ -11722,7 +11728,8 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ] } ], @@ -11731,7 +11738,8 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ] } }, @@ -12873,7 +12881,8 @@ "source": "reviewed_command_registry", "title": "插入块元素", "use_when": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ] }, "doc.list_comments": { @@ -19702,7 +19711,8 @@ "agent_summary_source": "dws-agent-selection/doc", "availability": "available", "avoid_when": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "canonical_path": "doc.reply_comment", "cli_name": "reply", @@ -19763,7 +19773,8 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ] } ], @@ -19772,7 +19783,8 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ] }, "canonical_path": { @@ -20050,7 +20062,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ] } ], @@ -20059,7 +20071,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ] } }, @@ -20720,7 +20732,7 @@ "source": "reviewed_command_registry", "title": "回复评论", "use_when": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ] }, "doc.search_documents": { @@ -38103,7 +38115,8 @@ "availability": "available", "avoid_when": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "canonical_path": "doc.update_document", @@ -38180,7 +38193,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ] } @@ -38191,7 +38205,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ] }, @@ -38448,7 +38463,8 @@ "value": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ] } ], @@ -38459,7 +38475,8 @@ "value": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ] } }, @@ -39074,23 +39091,8 @@ "type": "integer" }, "mode": { - "cli_required": true, "description": "更新模式: overwrite=覆盖, append=追加 (必填)", "field_provenance": { - "cli_required": { - "candidates": [ - { - "precedence": "cobra_contract", - "selected": true, - "source": "cobra_hard_required", - "value": true - } - ], - "precedence": "cobra_contract", - "resolution": "highest_precedence", - "source": "cobra_hard_required", - "value": true - }, "description": { "candidates": [ { @@ -39133,15 +39135,9 @@ }, "required": { "candidates": [ - { - "precedence": "cobra_contract", - "selected": true, - "source": "cobra_hard_required", - "value": true - }, { "precedence": "inference", - "selected": false, + "selected": true, "source": "usage_required_inference", "value": true }, @@ -39152,9 +39148,9 @@ "value": false } ], - "precedence": "cobra_contract", + "precedence": "inference", "resolution": "highest_precedence", - "source": "cobra_hard_required", + "source": "usage_required_inference", "value": true }, "required_when": { @@ -39520,7 +39516,8 @@ "use_when": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ] }, "doc.update_document_block": { @@ -39545,7 +39542,8 @@ "availability": "available", "avoid_when": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "canonical_path": "doc.update_document_block", "cli_name": "update", @@ -39615,7 +39613,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ] } ], @@ -39625,7 +39624,8 @@ "source": "internal/cli/schema_hints/selection/doc.json", "value": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ] }, "canonical_path": { @@ -39847,7 +39847,7 @@ "selected": true, "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ] } ], @@ -39856,7 +39856,7 @@ "review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。", "source": "internal/cli/schema_hints/selection/doc.json", "value": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ] } }, @@ -40698,7 +40698,7 @@ "source": "reviewed_command_registry", "title": "更新块元素", "use_when": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ] }, "doc.update_permission": { diff --git a/internal/cli/schema_hints/selection/doc.json b/internal/cli/schema_hints/selection/doc.json index de160fe6..fda8f694 100644 --- a/internal/cli/schema_hints/selection/doc.json +++ b/internal/cli/schema_hints/selection/doc.json @@ -90,7 +90,7 @@ "avoid_when": [ "创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type (勿用 doc create)", "在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc", - "导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert" + "导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert" ], "examples": [ "dws doc create --name \"项目周报\" --format json", @@ -205,7 +205,7 @@ "用户明确要求用 doc delete 兼容入口将文档/文件移入回收站,且已确认目标时" ], "avoid_when": [ - "常规文件删除优先 dws drive delete;本入口仅为兼容", + "常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口", "用户未确认或目标不清时不要删", "删块用 doc block delete;删评论用 doc comment delete" ], @@ -250,10 +250,11 @@ "doc.download_doc_attachment": { "agent_summary": "获取文档附件的临时下载链接", "use_when": [ - "获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时" + "获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块" ], "avoid_when": [ - "下载钉盘普通文件用 drive download;导出在线文档用 doc export" + "下载钉盘普通文件用 drive download;导出在线文档用 doc export", + "不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl" ], "examples": [ "dws doc media download --node --resource-id --format json" @@ -350,10 +351,10 @@ "doc.import_get": { "agent_summary": "根据 taskId 查询文档导入任务的执行结果", "use_when": [ - "查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时" + "仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时" ], "avoid_when": [ - "发起导入用 doc import(若入口可用);不要用本命令代替导入" + "发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId" ], "examples": [ "dws doc import get --task-id --format json" @@ -371,11 +372,13 @@ "doc.insert_document_block": { "agent_summary": "向文档插入块元素", "use_when": [ - "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON" + "在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON", + "相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after" ], "avoid_when": [ "整篇追加 Markdown 优先 doc update --mode append", - "插入本地文件附件优先 doc media insert", + "插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON", + "整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert", "删块用 block delete;改已有块用 block update" ], "examples": [ @@ -606,10 +609,11 @@ "doc.reply_comment": { "agent_summary": "回复文档评论", "use_when": [ - "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create" + "回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回" ], "avoid_when": [ - "新建评论用 create/create-inline;删评论用 delete" + "新建评论用 create/create-inline;删评论用 delete", + "不要把 commentId、blockId 或示例占位符传给 --comment-key" ], "examples": [ "dws doc comment reply --node --comment-key --content \"同意\" --mentioned-open-conversation-id --format json", @@ -743,11 +747,13 @@ "use_when": [ "用户要向已有 adoc 追加内容时用 --mode append(更安全)", "用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)", - "append 且要插到第 N 个 block 前时加 --index N(先 block list)" + "append 且要插到第 N 个 block 前时加 --index N(先 block list)", + "长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏" ], "avoid_when": [ "目标不是 adoc 或只要改单个块时改用 doc block update", - "覆盖模式用户未确认前不要执行;可先 --dry-run 预览", + "覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes", + "--content 与 --content-file 二选一;--index 仅用于 mode=append", "创建新文档用 doc create,不要用 update 冒充创建" ], "examples": [ @@ -769,11 +775,12 @@ "doc.update_document_block": { "agent_summary": "更新文档中的指定块", "use_when": [ - "修改已有块的文本/标题/样式(已知 blockId)时" + "修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果" ], "avoid_when": [ "插入新块用 block insert;删除用 block delete", - "改文档显示名用 rename;整篇覆盖用 update overwrite" + "改文档显示名用 rename;整篇覆盖用 update overwrite", + "不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId" ], "examples": [ "dws doc block update --node --block-id --text \"新内容\" --format json" diff --git a/internal/errors/coverage_edges_test.go b/internal/errors/coverage_edges_test.go index cd14e084..bd3c9d12 100644 --- a/internal/errors/coverage_edges_test.go +++ b/internal/errors/coverage_edges_test.go @@ -363,6 +363,24 @@ func TestSuggestBusinessHintChatRecovery(t *testing.T) { } } +func TestSuggestBusinessHintDocRecovery(t *testing.T) { + cases := []struct{ message, want string }{ + {"nodeId not found", "drive search"}, + {"blockId 不存在", "block list"}, + {"commentKey invalid", "comment list/create"}, + {"resourceId not found", "attachment"}, + {"workspaceId invalid", "wiki space list"}, + {"folderId invalid", "dentryId"}, + {"templateId not found", "template search"}, + {"version 不存在", "version list"}, + } + for _, tc := range cases { + if got := SuggestBusinessHint(map[string]any{"message": tc.message}); !strings.Contains(got, tc.want) { + t.Errorf("SuggestBusinessHint(%q) = %q, want containing %q", tc.message, got, tc.want) + } + } +} + func TestCrossPlatformCoveragePATSerializationAndPolicyEdges(t *testing.T) { oldHost := hostControlProvider oldBrowser := patBrowserProvider diff --git a/internal/errors/pat.go b/internal/errors/pat.go index 511e97ff..3672e3c7 100644 --- a/internal/errors/pat.go +++ b/internal/errors/pat.go @@ -340,6 +340,22 @@ func suggestForBusinessErrorText(body map[string]any) string { switch { case strings.Contains(msg, "搜索内容不能为空"): return "请提供非空搜索关键词: dws doc search --query \"关键词\"" + case strings.Contains(msg, "nodeId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")): + return "目标文档 nodeId 不存在或当前账号不可见。请用 dws drive search 或 dws wiki node search 重新获取真实 nodeId;不要复用示例占位符。" + case strings.Contains(msg, "blockId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")): + return "目标 blockId 不存在或已变化。请重新执行 dws doc block list --node --format json,并从本次结果取 blockId。" + case strings.Contains(msg, "commentKey") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "commentKey 无效或评论已不存在。请从 dws doc comment list/create 的当前返回结果中提取 commentKey,不要使用 commentId 或占位符。" + case strings.Contains(msg, "resourceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "resourceId 无效。请用 dws doc block list 查找 attachment 块并读取真实 resourceId;不要把 blockId 当作 resourceId。" + case strings.Contains(msg, "workspaceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "workspaceId 无效或无权访问。请用 dws wiki space list --type myWikiSpace --format json 获取当前账号可用的 workspaceId。" + case strings.Contains(msg, "folderId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "folderId 无效。doc 的 --folder 需要文档文件夹 nodeId/URL,不是 drive 的纯数字 dentryId;请重新列出目标空间节点。" + case strings.Contains(msg, "templateId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "templateId 无效或模板不可见。请先用 dws doc template search --query <关键词> --format json 获取当前账号可用的真实 templateId。" + case strings.Contains(msg, "version") && (strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")): + return "目标版本不存在。请先执行 dws doc version list --node --format json,并从当前版本列表选择可回滚版本。" case strings.Contains(msg, "User has no permission to access this email"): return "请确认邮箱地址正确,查看可用邮箱: dws mail mailbox list" case strings.Contains(msg, "频率超限") || strings.Contains(msg, "rate limit"): diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index c5aede58..52facd7b 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -3776,8 +3776,8 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 return err } iconMediaID := strings.TrimSpace(mustGetFlag(cmd, "icon-media-id")) - if iconMediaID == "" { - return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令") + if err := ValidateChatMediaID(iconMediaID); err != nil { + return fmt.Errorf("invalid --icon-media-id: %w", err) } return callMCPToolOnServer("im", "update_group_icon", map[string]any{ "openConversationId": mustGetFlag(cmd, "group"), diff --git a/internal/helpers/chat_command_edges_test.go b/internal/helpers/chat_command_edges_test.go index 53ad7d8a..fffd3ede 100644 --- a/internal/helpers/chat_command_edges_test.go +++ b/internal/helpers/chat_command_edges_test.go @@ -85,6 +85,22 @@ func TestCrossPlatformCoverageChatGroupUpdateIconRejectsBlankMediaID(t *testing. } } +func TestCrossPlatformCoverageChatGroupUpdateIconRejectsLocalPathBeforeMCP(t *testing.T) { + previousDeps, previousArgs := deps, os.Args + os.Args = []string{"dws", "chat"} + t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs }) + + caller := &productExampleCaller{} + err := runChatCoverageCommand(t, caller, + "group", "update-icon", "--group=cid", "--icon-media-id=./logo.png") + if err == nil { + t.Fatal("update group icon with local path succeeded, want validation error") + } + if caller.calls != 0 { + t.Fatalf("tool calls = %d, want 0", caller.calls) + } +} + func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) { previousDeps, previousArgs := deps, os.Args os.Args = []string{"dws", "chat"} diff --git a/internal/helpers/chat_file_shortcut.go b/internal/helpers/chat_file_shortcut.go index 9d97c47e..60b80e4f 100644 --- a/internal/helpers/chat_file_shortcut.go +++ b/internal/helpers/chat_file_shortcut.go @@ -4,7 +4,12 @@ package helpers -import "context" +import ( + "context" + "fmt" + "path/filepath" + "strings" +) // ConversationLocalFileMeta exposes the already-reviewed native chat upload // metadata to built-in semantic Shortcuts. It remains an alias so the native @@ -41,3 +46,21 @@ func BuildConversationFileContent( ) (string, error) { return buildConversationFileContent(dentryID, spaceID, meta) } + +// ValidateChatMediaID rejects values that are deterministically not an +// uploaded DingTalk mediaId before an MCP request is dispatched. +func ValidateChatMediaID(value string) error { + value = strings.TrimSpace(value) + if value == "" { + return fmt.Errorf("mediaId 不能为空") + } + lower := strings.ToLower(value) + if strings.HasPrefix(lower, "file://") || filepath.IsAbs(value) || + strings.ContainsAny(value, `/\\`) || strings.HasPrefix(lower, "dentry") { + return fmt.Errorf("%q 是本地文件路径或文件标识,不是 mediaId;请使用可信上游返回的 mediaId,DWS CLI 当前不能把本地图片转换为群头像 mediaId", value) + } + if value[0] != '@' && value[0] != '$' { + return fmt.Errorf("%q 不是有效 mediaId:群头像 mediaId 应以 @ 或 $ 开头;不要传本地路径、dentryId 或 uploadKey", value) + } + return nil +} diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index ab2c0ee3..69cacfad 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -1652,7 +1652,8 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 updateCmd.Flags().String("markdown", "", "已弃用,请使用 --content 代替") _ = updateCmd.Flags().MarkHidden("markdown") updateCmd.Flags().String("mode", "", "更新模式: overwrite=覆盖, append=追加 (必填)") - _ = updateCmd.MarkFlagRequired("mode") + // Kept out of Cobra's generic required-flag validator so doc local + // preflight can return a structured, actionable error before MCP dispatch. updateCmd.Flags().Int("index", -1, "插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾") updateCmd.Flags().Bool("yes", false, "确认执行破坏性写入 (仅 --mode overwrite 需要)") updateCmd.Flags().Bool("dry-run", false, "预览覆盖写入差异,不调用远端 update") @@ -2902,6 +2903,7 @@ CLI 内部自动完成全部流程: permissionCmd.Hidden = true root.AddCommand(searchCmd, listCmd, infoCmd, readCmd, createCmd, updateCmd, uploadCmd, downloadCmd, copyCmd, moveCmd, renameCmd, deleteCmd, fileCmd, folderCmd, blockCmd, commentCmd, mediaCmd, permissionCmd, exportCmd, importCmd, versionCmd, templateCmd, newDocStyleCommand()) + attachDocLocalPreflight(root) return root } diff --git a/internal/helpers/doc_local_preflight.go b/internal/helpers/doc_local_preflight.go new file mode 100644 index 00000000..a1dc034f --- /dev/null +++ b/internal/helpers/doc_local_preflight.go @@ -0,0 +1,439 @@ +package helpers + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +// attachDocLocalPreflight wraps every doc leaf before its RunE reaches MCP. +// Only facts provable from local argv/filesystem state belong here; resource +// existence, permissions and business state remain server-owned. +func attachDocLocalPreflight(root *cobra.Command) { + var walk func(*cobra.Command) + walk = func(cmd *cobra.Command) { + for _, child := range cmd.Commands() { + walk(child) + } + if cmd.RunE == nil { + return + } + original := cmd.RunE + cmd.RunE = func(c *cobra.Command, args []string) error { + if err := validateDocLocalArgs(c); err != nil { + return err + } + return original(c, args) + } + } + walk(root) +} + +func docLocalError(cmd *cobra.Command, code, message, suggestion string) error { + return &CLIError{Code: code, Message: message, Suggestion: suggestion, Operation: strings.TrimPrefix(cmd.CommandPath(), "dws ")} +} + +func docRequire(cmd *cobra.Command, example string, names ...string) error { + missing := make([]string, 0, len(names)) + for _, name := range names { + flag := cmd.Flags().Lookup(name) + if flag == nil || strings.TrimSpace(flag.Value.String()) == "" { + missing = append(missing, "--"+name) + } + } + if len(missing) == 0 { + return nil + } + return docLocalError(cmd, CodeMissingParam, "缺少必填参数: "+strings.Join(missing, ", "), "示例: "+example) +} + +func docRequireNode(cmd *cobra.Command, example string) error { + if flagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") != "" { + return nil + } + return docLocalError(cmd, CodeMissingParam, "缺少目标文档 --node(可传文档 URL 或 nodeId)", "先用 dws drive search 或 dws wiki node search 获取 nodeId。示例: "+example) +} + +func docValidateLocalFile(cmd *cobra.Command, flagName, example string) error { + path, _ := cmd.Flags().GetString(flagName) + if strings.TrimSpace(path) == "" { + return docRequire(cmd, example, flagName) + } + info, err := os.Stat(path) + if err != nil { + return docLocalError(cmd, CodeFileNotFound, fmt.Sprintf("本地文件 %q 不可读取", path), "检查路径后重试。示例: "+example) + } + if info.IsDir() { + return docLocalError(cmd, CodeInvalidPath, fmt.Sprintf("%q 是目录,不是文件", path), "请传入具体文件路径。示例: "+example) + } + return nil +} + +func docValidateWhere(cmd *cobra.Command, example string) error { + where, _ := cmd.Flags().GetString("where") + ref, _ := cmd.Flags().GetString("ref-block") + if where != "" && where != "before" && where != "after" { + return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--where %q 无效,仅支持 before 或 after", where), "示例: "+example) + } + if where != "" && ref == "" { + return docLocalError(cmd, CodeMissingParam, "使用 --where 时必须同时提供 --ref-block", "先用 dws doc block list --node 获取 blockId。示例: "+example) + } + return nil +} + +func docValidateEnum(cmd *cobra.Command, name string, allowed []string, example string) error { + value, _ := cmd.Flags().GetString(name) + if value == "" { + return nil + } + for _, candidate := range allowed { + if strings.EqualFold(value, candidate) { + return nil + } + } + return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s %q 无效,仅支持 %s", name, value, strings.Join(allowed, "、")), "示例: "+example) +} + +func docValidateLimit(cmd *cobra.Command, max int, example string) error { + for _, name := range []string{"limit", "page-size", "max-results"} { + if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) { + value, err := cmd.Flags().GetInt(name) + if err == nil && (value < 1 || value > max) { + return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s 必须在 1 到 %d 之间", name, max), "示例: "+example) + } + } + } + return nil +} + +func docValidateBlockContent(cmd *cobra.Command, example string) error { + changed := make([]string, 0, 3) + for _, name := range []string{"text", "heading", "element"} { + if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) && strings.TrimSpace(flag.Value.String()) != "" { + changed = append(changed, "--"+name) + } + } + if len(changed) == 0 { + return docLocalError(cmd, CodeMissingParam, "必须提供一种块内容:--text、--heading 或 --element", "示例: "+example) + } + if len(changed) > 1 { + return docLocalError(cmd, CodeInvalidParam, "块内容参数不能同时使用: "+strings.Join(changed, ", "), "三选一。示例: "+example) + } + format, _ := cmd.Flags().GetString("content-format") + if format == "jsonml" && changed[0] != "--element" { + return docLocalError(cmd, CodeInvalidParam, "--content-format jsonml 必须通过 --element 提供 JSONML 节点", "示例: "+example) + } + return nil +} + +func docValidatePermissionUsers(cmd *cobra.Command, example string) error { + raw := flagOrFallback(cmd, "users", "user") + users := parseCommentMentionIds(raw) + if len(users) == 0 { + return docLocalError(cmd, CodeMissingParam, "缺少至少一个用户 ID:--users", "示例: "+example) + } + if len(users) > 30 { + return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("单次最多处理 30 个用户,当前为 %d 个", len(users)), "请拆分为多次调用") + } + return nil +} + +func validateDocLocalArgs(cmd *cobra.Command) error { + path := strings.TrimPrefix(cmd.CommandPath(), "dws ") + switch path { + case "doc info": + return docRequireNode(cmd, "dws "+path+" --node --format json") + case "doc search": + if err := docValidateLimit(cmd, 30, "dws doc search --query \"周报\" --limit 10"); err != nil { + return err + } + createdFrom, _ := cmd.Flags().GetInt64("created-from") + createdTo, _ := cmd.Flags().GetInt64("created-to") + visitedFrom, _ := cmd.Flags().GetInt64("visited-from") + visitedTo, _ := cmd.Flags().GetInt64("visited-to") + if createdFrom < 0 || createdTo < 0 || visitedFrom < 0 || visitedTo < 0 { + return docLocalError(cmd, CodeInvalidParam, "时间过滤值必须是非负毫秒时间戳", "示例: dws doc search --created-from 1700000000000 --created-to 1710000000000") + } + if cmd.Flags().Changed("created-from") && cmd.Flags().Changed("created-to") && createdFrom > createdTo { + return docLocalError(cmd, CodeInvalidParam, "--created-from 不能晚于 --created-to", "请交换起止时间") + } + if cmd.Flags().Changed("visited-from") && cmd.Flags().Changed("visited-to") && visitedFrom > visitedTo { + return docLocalError(cmd, CodeInvalidParam, "--visited-from 不能晚于 --visited-to", "请交换起止时间") + } + case "doc list": + return docValidateLimit(cmd, 50, "dws doc list --workspace --limit 50") + case "doc read": + if err := docRequireNode(cmd, "dws doc read --node --format json"); err != nil { + return err + } + if depth, _ := cmd.Flags().GetInt("max-depth"); depth < 0 { + return docLocalError(cmd, CodeInvalidParam, "--max-depth 不能为负数", "示例: dws doc read --node --content-format jsonml --scope outline --max-depth 3") + } + if scopeValue, _ := cmd.Flags().GetString("scope"); scopeValue != "" { + valid := false + for _, candidate := range []string{"outline", "range", "section", "tags"} { + if scopeValue == candidate { + valid = true + } + } + if !valid { + return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("invalid --scope %q: must be one of outline|range|section|tags", scopeValue), "示例: dws doc read --node --content-format jsonml --scope outline") + } + } + format, _ := cmd.Flags().GetString("content-format") + scope, _ := cmd.Flags().GetString("scope") + tags, _ := cmd.Flags().GetString("tags") + if (scope != "" || tags != "") && format != "jsonml" { + return docLocalError(cmd, CodeInvalidParam, "--scope/--tags requires --content-format jsonml", "示例: dws doc read --node --content-format jsonml --scope tags --tags h1,h2") + } + if scope == "tags" && strings.TrimSpace(tags) == "" { + return docLocalError(cmd, CodeMissingParam, "--tags is required when --scope=tags", "示例: dws doc read --node --content-format jsonml --scope tags --tags h1,h2") + } + if scope != "tags" && tags != "" { + if scope == "" { + return docLocalError(cmd, CodeInvalidParam, "--tags requires --scope tags", "移除 --tags 或改用 --scope tags") + } + return docLocalError(cmd, CodeInvalidParam, "--tags only works with --scope tags", "移除 --tags 或改用 --scope tags") + } + case "doc create": + if strings.TrimSpace(flagOrFallback(cmd, "name", "title")) == "" { + return docLocalError(cmd, CodeMissingParam, "缺少文档名称 --name", "示例: dws doc create --name \"项目周报\" --format json") + } + if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") { + return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "短文本用 --content;长文本或表格用 --content-file") + } + if cmd.Flags().Changed("content-file") { + return docValidateLocalFile(cmd, "content-file", "dws doc create --name \"周报\" --content-file ./weekly.md") + } + case "doc update": + if err := docRequireNode(cmd, "dws doc update --node --content \"追加内容\" --mode append"); err != nil { + return err + } + if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") { + return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "二选一;长内容优先 --content-file") + } + if cmd.Flags().Changed("content-file") { + if err := docValidateLocalFile(cmd, "content-file", "dws doc update --node --content-file ./body.md --mode append"); err != nil { + return err + } + } + mode, _ := cmd.Flags().GetString("mode") + if mode != "append" && mode != "overwrite" { + return docLocalError(cmd, CodeInvalidParam, "--mode 必须是 append 或 overwrite", "追加优先使用: dws doc update --node --content \"内容\" --mode append") + } + if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && (mode != "append" || idx < 0) { + return docLocalError(cmd, CodeInvalidParam, "--index 仅支持 mode=append 且必须大于等于 0", "示例: dws doc update --node --content \"内容\" --mode append --index 0") + } + content := flagOrFallback(cmd, "content", "markdown") + if strings.TrimSpace(content) == "" && !cmd.Flags().Changed("content-file") { + return docLocalError(cmd, CodeMissingParam, "必须通过 --content 或 --content-file 提供非空内容", "示例: dws doc update --node --content \"追加内容\" --mode append") + } + dryRun, _ := cmd.Flags().GetBool("dry-run") + yes, _ := cmd.Flags().GetBool("yes") + if dryRun && mode != "overwrite" { + return docLocalError(cmd, CodeInvalidParam, "--dry-run 仅用于预览 overwrite 覆盖写入", "append 本身不覆盖全文,请移除 --dry-run") + } + if yes && mode != "overwrite" { + return docLocalError(cmd, CodeInvalidParam, "--yes 仅用于确认 overwrite 覆盖写入", "append 无需 --yes,请移除该参数") + } + if mode == "overwrite" && !dryRun && !yes { + return docLocalError(cmd, CodeInvalidParam, "--mode overwrite 必须加 --yes 确认,或使用 --dry-run 预览", "示例: dws doc update --node --content-file ./body.md --mode overwrite --dry-run") + } + case "doc block list": + if err := docRequireNode(cmd, "dws doc block list --node --format json"); err != nil { + return err + } + start, _ := cmd.Flags().GetInt("start-index") + end, _ := cmd.Flags().GetInt("end-index") + if start < 0 || end < 0 || (cmd.Flags().Changed("end-index") && end < start) { + return docLocalError(cmd, CodeInvalidParam, "块索引必须非负,且 --end-index 不能小于 --start-index", "示例: dws doc block list --node --start-index 0 --end-index 5") + } + case "doc block insert": + if err := docRequireNode(cmd, "dws doc block insert --node --text \"内容\""); err != nil { + return err + } + if err := docValidateWhere(cmd, "dws doc block insert --node --text \"内容\" --ref-block --where after"); err != nil { + return err + } + if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 { + return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block insert --node --heading \"标题\" --level 2") + } + if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && idx < 0 { + return docLocalError(cmd, CodeInvalidParam, "--index 不能为负数", "示例: dws doc block insert --node --text \"内容\" --index 0") + } + return docValidateBlockContent(cmd, "dws doc block insert --node --text \"内容\"") + case "doc block update": + if err := docRequireNode(cmd, "dws doc block update --node --block-id --text \"新内容\""); err != nil { + return err + } + if err := docRequire(cmd, "dws doc block update --node --block-id --text \"新内容\"", "block-id"); err != nil { + return err + } + if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 { + return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block update --node --block-id --heading \"标题\" --level 2") + } + return docValidateBlockContent(cmd, "dws doc block update --node --block-id --text \"新内容\"") + case "doc block delete": + if err := docRequireNode(cmd, "dws doc block delete --node --block-id --yes"); err != nil { + return err + } + return docRequire(cmd, "dws doc block delete --node --block-id --yes", "block-id") + case "doc media download": + if err := docRequireNode(cmd, "dws doc media download --node --resource-id "); err != nil { + return err + } + return docRequire(cmd, "dws doc media download --node --resource-id ", "resource-id") + case "doc media insert": + if err := docRequireNode(cmd, "dws doc media insert --node --file ./report.pdf"); err != nil { + return err + } + if err := docValidateWhere(cmd, "dws doc media insert --node --file ./report.pdf --ref-block --where after"); err != nil { + return err + } + return docValidateLocalFile(cmd, "file", "dws doc media insert --node --file ./report.pdf") + case "doc comment list": + if err := docRequireNode(cmd, "dws doc comment list --node --format json"); err != nil { + return err + } + if err := docValidateLimit(cmd, 50, "dws doc comment list --node --limit 50"); err != nil { + return err + } + if err := docValidateEnum(cmd, "type", []string{"global", "inline"}, "dws doc comment list --node --type inline"); err != nil { + return err + } + return docValidateEnum(cmd, "resolve-status", []string{"resolved", "unresolved"}, "dws doc comment list --node --resolve-status unresolved") + case "doc comment create": + if err := docRequireNode(cmd, "dws doc comment create --node --content \"评论\""); err != nil { + return err + } + return docRequire(cmd, "dws doc comment create --node --content \"评论\"", "content") + case "doc comment reply", "doc comment update": + if err := docRequireNode(cmd, "dws "+path+" --node --comment-key --content \"内容\""); err != nil { + return err + } + if err := docRequire(cmd, "dws "+path+" --node --comment-key --content \"内容\"", "comment-key", "content"); err != nil { + return err + } + if path == "doc comment reply" { + emoji, _ := cmd.Flags().GetBool("emoji") + groups, groupErr := commentGroupMentionIDs(cmd) + if groupErr != nil { + return groupErr + } + if emoji && len(groups) > 0 { + return docLocalError(cmd, CodeInvalidParam, "--emoji cannot be used with --mentioned-open-conversation-id: emoji replies do not support group mentions", "表情回复请移除群 @;需要 @群时使用普通文字回复") + } + } + case "doc comment delete": + if err := docRequireNode(cmd, "dws doc comment delete --node --comment-key --yes"); err != nil { + return err + } + return docRequire(cmd, "dws doc comment delete --node --comment-key --yes", "comment-key") + case "doc comment create-inline": + if err := docRequireNode(cmd, "dws doc comment create-inline --node --block-id --start 0 --end 5 --content \"评论\""); err != nil { + return err + } + if err := docRequire(cmd, "dws doc comment create-inline --node --block-id --start 0 --end 5 --content \"评论\"", "block-id", "content"); err != nil { + return err + } + start, _ := cmd.Flags().GetInt("start") + end, _ := cmd.Flags().GetInt("end") + if !cmd.Flags().Changed("start") || !cmd.Flags().Changed("end") || start < 0 || end <= start { + return docLocalError(cmd, CodeInvalidParam, "划词范围必须显式提供 --start/--end,且满足 0 <= start < end", "先读取块文本确认字符偏移。示例: dws doc comment create-inline --node --block-id --start 0 --end 5 --content \"评论\"") + } + case "doc export": + if err := docRequireNode(cmd, "dws doc export --node --export-format pdf --output ./report.pdf"); err != nil { + return err + } + if err := docRequire(cmd, "dws doc export --node --export-format pdf --output ./report.pdf", "output"); err != nil { + return err + } + return docValidateEnum(cmd, "export-format", []string{"docx", "markdown", "md", "pdf"}, "dws doc export --node --export-format pdf --output ./report.pdf") + case "doc export get": + return docRequire(cmd, "dws doc export get --job-id ", "job-id") + case "doc import": + if err := docValidateLocalFile(cmd, "file", "dws doc import --file ./report.docx --workspace "); err != nil { + return err + } + if !deps.Caller.DryRun() && flagOrFallback(cmd, "folder", "folder-id") == "" && flagOrFallback(cmd, "workspace", "workspace-id") == "" { + return docLocalError(cmd, CodeMissingParam, "导入文档必须提供 --folder 或 --workspace 作为目标位置", "先用 dws wiki space list --type myWikiSpace --format json 获取 workspaceId") + } + case "doc import get": + return docRequire(cmd, "dws doc import get --task-id ", "task-id") + case "doc version save", "doc version list", "doc version revert": + if err := docRequireNode(cmd, "dws "+path+" --node --format json"); err != nil { + return err + } + if path == "doc version revert" && (!cmd.Flags().Changed("version")) { + return docLocalError(cmd, CodeMissingParam, "缺少目标版本 --version", "先用 dws doc version list --node 获取真实版本号") + } + if path == "doc version revert" { + version, _ := cmd.Flags().GetInt("version") + if version < 1 { + return docLocalError(cmd, CodeInvalidParam, "--version 必须是大于 0 的真实版本号", "先用 dws doc version list --node 获取版本号") + } + } + if path == "doc version list" { + return docValidateLimit(cmd, 50, "dws doc version list --node --limit 10") + } + case "doc permission add", "doc permission update": + if err := docRequireNode(cmd, "dws "+path+" --node --users uid1 --role EDITOR"); err != nil { + return err + } + if err := docValidatePermissionUsers(cmd, "dws "+path+" --node --users uid1 --role EDITOR"); err != nil { + return err + } + if err := docRequire(cmd, "dws "+path+" --node --users uid1 --role EDITOR", "role"); err != nil { + return err + } + return docValidateEnum(cmd, "role", []string{"MANAGER", "EDITOR", "DOWNLOADER", "READER"}, "dws "+path+" --node --users uid1 --role EDITOR") + case "doc permission remove": + if err := docRequireNode(cmd, "dws doc permission remove --node --users uid1"); err != nil { + return err + } + return docValidatePermissionUsers(cmd, "dws doc permission remove --node --users uid1") + case "doc permission list": + if err := docRequireNode(cmd, "dws doc permission list --node --limit 30"); err != nil { + return err + } + if err := docValidateLimit(cmd, 200, "dws doc permission list --node --limit 30"); err != nil { + return err + } + if roles, _ := cmd.Flags().GetString("filter-role"); roles != "" { + for _, role := range parseRoleList(roles) { + valid := false + for _, candidate := range []string{"OWNER", "MANAGER", "EDITOR", "DOWNLOADER", "READER"} { + if role == candidate { + valid = true + } + } + if !valid { + return docLocalError(cmd, CodeInvalidParam, "--filter-role 包含非法角色 "+role, "仅支持 OWNER、MANAGER、EDITOR、DOWNLOADER、READER") + } + } + } + case "doc template list": + if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template list --source MY"); err != nil { + return err + } + return docValidateLimit(cmd, 50, "dws doc template list --limit 20") + case "doc template search": + if flagOrFallback(cmd, "query", "keyword", "name") == "" { + return docLocalError(cmd, CodeMissingParam, "缺少非空模板关键词 --query", "示例: dws doc template search --query \"周报\" --format json") + } + if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template search --query \"周报\" --source PUBLIC"); err != nil { + return err + } + return docValidateLimit(cmd, 50, "dws doc template search --query \"周报\" --limit 20") + case "doc template apply": + if flagOrFallback(cmd, "template-id", "template", "tpl-id") == "" { + return docLocalError(cmd, CodeMissingParam, "缺少模板 ID --template-id", "先用 dws doc template search --query \"关键词\" 获取真实 templateId") + } + } + return nil +} + +func docLocalFileExtension(path string) string { return strings.ToLower(filepath.Ext(path)) } diff --git a/internal/helpers/doc_local_preflight_test.go b/internal/helpers/doc_local_preflight_test.go new file mode 100644 index 00000000..ca725e80 --- /dev/null +++ b/internal/helpers/doc_local_preflight_test.go @@ -0,0 +1,134 @@ +package helpers + +import ( + "errors" + "strings" + "testing" +) + +func TestDocLocalPreflightRejectsBeforeMCP(t *testing.T) { + cases := []struct { + name string + args []string + want string + }{ + {"info-node", []string{"info"}, "--node"}, + {"read-node", []string{"read"}, "--node"}, + {"create-name", []string{"create"}, "--name"}, + {"create-content-conflict", []string{"create", "--name", "x", "--content", "a", "--content-file", "b"}, "不能同时"}, + {"create-file-missing", []string{"create", "--name", "x", "--content-file", "/definitely/not/found.md"}, "不可读取"}, + {"update-node", []string{"update", "--content", "x", "--mode", "append"}, "--node"}, + {"update-content-conflict", []string{"update", "--node", "n", "--content", "a", "--content-file", "b", "--mode", "append"}, "不能同时"}, + {"update-mode-missing", []string{"update", "--node", "n", "--content", "a"}, "--mode"}, + {"update-mode-invalid", []string{"update", "--node", "n", "--content", "a", "--mode", "merge"}, "append 或 overwrite"}, + {"update-index-mode", []string{"update", "--node", "n", "--content", "a", "--mode", "overwrite", "--index", "1"}, "--index"}, + {"block-list-node", []string{"block", "list"}, "--node"}, + {"block-list-range", []string{"block", "list", "--node", "n", "--start-index", "4", "--end-index", "2"}, "不能小于"}, + {"block-insert-node", []string{"block", "insert", "--text", "x"}, "--node"}, + {"block-insert-where", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "middle"}, "before 或 after"}, + {"block-insert-ref", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "before"}, "--ref-block"}, + {"block-insert-level", []string{"block", "insert", "--node", "n", "--heading", "x", "--level", "7"}, "1 到 6"}, + {"block-update-node", []string{"block", "update", "--block-id", "b", "--text", "x"}, "--node"}, + {"block-update-id", []string{"block", "update", "--node", "n", "--text", "x"}, "--block-id"}, + {"block-delete-id", []string{"block", "delete", "--node", "n"}, "--block-id"}, + {"media-download-resource", []string{"media", "download", "--node", "n"}, "--resource-id"}, + {"media-insert-file", []string{"media", "insert", "--node", "n"}, "--file"}, + {"comment-create-content", []string{"comment", "create", "--node", "n"}, "--content"}, + {"comment-reply-key", []string{"comment", "reply", "--node", "n", "--content", "x"}, "--comment-key"}, + {"comment-update-content", []string{"comment", "update", "--node", "n", "--comment-key", "k"}, "--content"}, + {"comment-delete-key", []string{"comment", "delete", "--node", "n"}, "--comment-key"}, + {"inline-block", []string{"comment", "create-inline", "--node", "n", "--content", "x", "--start", "0", "--end", "1"}, "--block-id"}, + {"inline-range", []string{"comment", "create-inline", "--node", "n", "--block-id", "b", "--content", "x", "--start", "2", "--end", "1"}, "start < end"}, + {"export-output", []string{"export", "--node", "n"}, "--output"}, + {"export-job", []string{"export", "get"}, "--job-id"}, + {"import-file", []string{"import", "--workspace", "w"}, "--file"}, + {"import-target", []string{"import", "--file", "/definitely/not/found.docx"}, "不可读取"}, + {"import-task", []string{"import", "get"}, "--task-id"}, + {"version-node", []string{"version", "list"}, "--node"}, + {"version-number", []string{"version", "revert", "--node", "n"}, "--version"}, + {"template-query", []string{"template", "search"}, "--query"}, + {"template-id", []string{"template", "apply"}, "--template-id"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := newDocCommand() + cmd.SetArgs(tc.args) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want containing %q", err, tc.want) + } + var cliErr *CLIError + if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation { + t.Fatalf("error = %#v, want validation CLIError", err) + } + }) + } +} + +func TestDocLocalPreflightSecondBatchRejectsBeforeMCP(t *testing.T) { + users31 := strings.TrimSuffix(strings.Repeat("u,", 31), ",") + cases := []struct { + name string + args []string + want string + }{ + {"search-created-negative", []string{"search", "--created-from", "-1"}, "非负毫秒"}, + {"search-created-range", []string{"search", "--created-from", "20", "--created-to", "10"}, "created-from"}, + {"search-visited-negative", []string{"search", "--visited-to", "-1"}, "非负毫秒"}, + {"search-visited-range", []string{"search", "--visited-from", "20", "--visited-to", "10"}, "visited-from"}, + {"search-limit-zero", []string{"search", "--limit", "0"}, "1 到 30"}, + {"search-limit-large", []string{"search", "--limit", "31"}, "1 到 30"}, + {"list-limit-zero", []string{"list", "--limit", "0"}, "1 到 50"}, + {"list-limit-large", []string{"list", "--limit", "51"}, "1 到 50"}, + {"read-depth-negative", []string{"read", "--node", "n", "--max-depth", "-1"}, "不能为负"}, + {"read-scope-invalid", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "all"}, "outline"}, + {"read-scope-format", []string{"read", "--node", "n", "--scope", "outline"}, "content-format jsonml"}, + {"read-tags-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "tags"}, "--tags"}, + {"read-tags-wrong-scope", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "outline", "--tags", "h1"}, "--tags only works"}, + {"create-whitespace-name", []string{"create", "--name", " "}, "--name"}, + {"update-empty-content", []string{"update", "--node", "n", "--content", " ", "--mode", "append"}, "非空内容"}, + {"update-dry-run-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--dry-run"}, "仅用于预览 overwrite"}, + {"update-yes-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--yes"}, "仅用于确认 overwrite"}, + {"update-overwrite-confirm", []string{"update", "--node", "n", "--content", "x", "--mode", "overwrite"}, "必须加 --yes"}, + {"block-insert-content", []string{"block", "insert", "--node", "n"}, "必须提供一种块内容"}, + {"block-insert-text-heading", []string{"block", "insert", "--node", "n", "--text", "x", "--heading", "h"}, "不能同时"}, + {"block-insert-heading-element", []string{"block", "insert", "--node", "n", "--heading", "h", "--element", "{}"}, "不能同时"}, + {"block-insert-jsonml-text", []string{"block", "insert", "--node", "n", "--content-format", "jsonml", "--text", "x"}, "必须通过 --element"}, + {"block-insert-index", []string{"block", "insert", "--node", "n", "--text", "x", "--index", "-1"}, "--index 不能"}, + {"block-update-content", []string{"block", "update", "--node", "n", "--block-id", "b"}, "必须提供一种块内容"}, + {"block-update-text-element", []string{"block", "update", "--node", "n", "--block-id", "b", "--text", "x", "--element", "{}"}, "不能同时"}, + {"block-update-level", []string{"block", "update", "--node", "n", "--block-id", "b", "--heading", "h", "--level", "0"}, "1 到 6"}, + {"comment-limit-zero", []string{"comment", "list", "--node", "n", "--limit", "0"}, "1 到 50"}, + {"comment-limit-large", []string{"comment", "list", "--node", "n", "--limit", "51"}, "1 到 50"}, + {"comment-type", []string{"comment", "list", "--node", "n", "--type", "all"}, "global"}, + {"comment-status", []string{"comment", "list", "--node", "n", "--resolve-status", "open"}, "resolved"}, + {"reply-emoji-group", []string{"comment", "reply", "--node", "n", "--comment-key", "k", "--content", "x", "--emoji", "--mentioned-open-conversation-id", "g"}, "emoji replies do not support group mentions"}, + {"permission-users-missing", []string{"permission", "add", "--node", "n", "--role", "READER"}, "--users"}, + {"permission-users-large", []string{"permission", "add", "--node", "n", "--role", "READER", "--users", users31}, "最多处理 30"}, + {"permission-role", []string{"permission", "add", "--node", "n", "--role", "OWNER", "--users", "u"}, "MANAGER"}, + {"permission-filter-role", []string{"permission", "list", "--node", "n", "--filter-role", "ADMIN"}, "非法角色"}, + {"export-format", []string{"export", "--node", "n", "--output", "x", "--export-format", "html"}, "docx"}, + } + if len(cases) != 36 { + t.Fatalf("second batch has %d cases, want 36", len(cases)) + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := newDocCommand() + cmd.SetArgs(tc.args) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want containing %q", err, tc.want) + } + var cliErr *CLIError + if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation { + t.Fatalf("error = %#v, want validation CLIError", err) + } + }) + } +} diff --git a/internal/shortcut/chat/chat_group.go b/internal/shortcut/chat/chat_group.go index 0fd7adcb..b10a6bc8 100644 --- a/internal/shortcut/chat/chat_group.go +++ b/internal/shortcut/chat/chat_group.go @@ -18,6 +18,8 @@ import ( "strconv" "strings" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" ) @@ -82,6 +84,18 @@ var ChatMembersGet = shortcut.Shortcut{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"users", "open-dingtalk-ids"}}, }, Tips: []string{`dws chat +chat-members-get --id --users odid1,odid2`}, + Validate: func(rt *shortcut.RuntimeContext) error { + for _, value := range rt.StrSliceFirst("users", "open-dingtalk-ids") { + value = strings.TrimSpace(value) + if value == "" || !isOpenID(value) { + return apperrors.NewValidation(fmt.Sprintf( + "--users/--open-dingtalk-ids 只接受成员 openDingTalkId;%q 不是 openDingTalkId。请先查询人员并传返回的 openDingTalkId", + value, + )) + } + } + return nil + }, Execute: func(rt *shortcut.RuntimeContext) error { conversationID := rt.StrFirst("id", "group", "chat-id", "conversation-id", "open-conversation-id") return rt.CallMCP("list_group_member_by_ids", map[string]any{ @@ -186,6 +200,12 @@ var ChatUpdateIcon = shortcut.Shortcut{ {Name: "icon-media-id", Type: shortcut.FlagString, Desc: "群头像 mediaId(以 @ 开头)", Required: true}, }, Tips: []string{`dws chat +chat-update-icon --group --icon-media-id `}, + Validate: func(rt *shortcut.RuntimeContext) error { + if err := helpers.ValidateChatMediaID(rt.Str("icon-media-id")); err != nil { + return apperrors.NewValidation("invalid --icon-media-id: " + err.Error()) + } + return nil + }, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_group_icon", map[string]any{ "openConversationId": rt.Str("group"), @@ -207,7 +227,8 @@ var ChatUpdateSettings = shortcut.Shortcut{ {Name: "setting-key", Type: shortcut.FlagString, Desc: "群设置项 key,如 searchable / onlyAdminCanAtAll", Required: true}, {Name: "status", Type: shortcut.FlagInt, Desc: "设置值:0=关闭,1=开启", Required: true}, }, - Tips: []string{`dws chat +chat-update-settings --group --setting-key searchable --status 1`}, + Tips: []string{`dws chat +chat-update-settings --group --setting-key searchable --status 1`}, + Validate: validateChatUpdateSettings, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_group_settings", map[string]any{ "openConversationId": rt.Str("group"), @@ -217,6 +238,27 @@ var ChatUpdateSettings = shortcut.Shortcut{ }, } +var supportedChatSettingKeys = map[string]struct{}{ + "authority": {}, "joinValidation": {}, "onlyAdminCanAtAll": {}, "searchable": {}, + "addFriendForbidden": {}, "toolbarStatus": {}, "pluginCustomizeVerify": {}, + "onlyAdminCanDING": {}, "allMembersCanCreateMcsConf": {}, "onlyAdminCanSetMsgTop": {}, + "onlyAdminCanPinMsg": {}, "onlyAdminCanSendFile": {}, "allMembersCanCreateCalendar": {}, + "groupEmailDisabled": {}, "groupRedEnvelopeSwitch": {}, "groupLiveAuthority": {}, + "groupBillAuthority": {}, +} + +func validateChatUpdateSettings(rt *shortcut.RuntimeContext) error { + key := strings.TrimSpace(rt.Str("setting-key")) + if _, ok := supportedChatSettingKeys[key]; !ok { + return apperrors.NewValidation(fmt.Sprintf("不支持的 --setting-key %q;请使用 chat group update-settings --help 中列出的设置项", key)) + } + status := rt.Int("status") + if status != 0 && status != 1 { + return apperrors.NewValidation("--status 只允许 0(关闭)或 1(开启)") + } + return nil +} + // ChatDismiss dismisses (destroys) a group (dismiss_group, im). var ChatDismiss = shortcut.Shortcut{ Service: "chat", diff --git a/internal/shortcut/chat/gap_fill_test.go b/internal/shortcut/chat/gap_fill_test.go index 0af89941..614da896 100644 --- a/internal/shortcut/chat/gap_fill_test.go +++ b/internal/shortcut/chat/gap_fill_test.go @@ -54,6 +54,15 @@ func TestMessagesSendPublishesCompleteIdentityConstraintInputs(t *testing.T) { } } +func TestMessagesSendUploadTargetUsesUploadInterfaceFields(t *testing.T) { + if got := messagesSendUploadTarget("cid-1", ""); !reflect.DeepEqual(got, map[string]any{"openConversationId": "cid-1"}) { + t.Fatalf("group upload target = %#v", got) + } + if got := messagesSendUploadTarget("", "D-open-1"); !reflect.DeepEqual(got, map[string]any{"openDingTalkId": "D-open-1"}) { + t.Fatalf("direct upload target = %#v", got) + } +} + func TestCrossPlatformCoverageSafeResourceDownloadsStayReadOnly(t *testing.T) { for _, command := range []shortcut.Shortcut{MessagesMget, MessagesResourceDownload} { if command.Risk != shortcut.RiskRead { @@ -155,6 +164,9 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T) fake.calls[2].tool != "send_personal_message" { t.Fatalf("file flow calls = %#v", fake.calls) } + if fake.calls[0].args["openConversationId"] != "cid" || fake.calls[1].args["openConversationId"] != "cid" { + t.Fatalf("upload target args = %#v / %#v, want openConversationId", fake.calls[0].args, fake.calls[1].args) + } send := fake.calls[2] if send.args["msgType"] != "file" || send.args["openConversationId"] != "cid" || send.args["uuid"] != "file-key" { @@ -177,6 +189,32 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T) } } +func TestChatShortcutInvalidInputsStopBeforeMCP(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {name: "member numeric user id", args: []string{"chat", "+chat-members-get", "--id", "cid", "--users", "489149"}}, + {name: "icon local path", args: []string{"chat", "+chat-update-icon", "--group", "cid", "--icon-media-id", "./logo.png", "--yes"}}, + {name: "setting unknown key", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "unknown", "--status", "1", "--yes"}}, + {name: "setting invalid status", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "searchable", "--status", "2", "--yes"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fake := &larkAlignmentCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs(tc.args) + if err := root.Execute(); err == nil { + t.Fatal("invalid command succeeded, want validation error") + } + if len(fake.calls) != 0 { + t.Fatalf("MCP calls = %#v, want zero", fake.calls) + } + }) + } +} + func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileDryRunAndFailures(t *testing.T) { t.Chdir(t.TempDir()) if err := os.WriteFile("fixture.bin", []byte("x"), 0o600); err != nil { diff --git a/internal/shortcut/chat/unified_send.go b/internal/shortcut/chat/unified_send.go index d0024bf6..d0b21464 100644 --- a/internal/shortcut/chat/unified_send.go +++ b/internal/shortcut/chat/unified_send.go @@ -420,8 +420,7 @@ func executeMessagesSendUserFile( if err != nil { return err } - targetArgs := map[string]any{} - addMessagesSendUserTarget(targetArgs, group, openID) + targetArgs := messagesSendUploadTarget(group, openID) idempotencyKey := messagesSendIdempotencyKey(rt) if rt.DryRun() { return rt.Output(map[string]any{ @@ -490,6 +489,13 @@ func executeMessagesSendUserFile( }) } +func messagesSendUploadTarget(group, openID string) map[string]any { + if group != "" { + return map[string]any{"openConversationId": group} + } + return map[string]any{"openDingTalkId": openID} +} + func addMessagesSendUserTarget(params map[string]any, group, openID string) { if group != "" { params["openConversationId"] = group