Documentation
¶
Overview ¶
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Package doc declares the high-fidelity `dws doc +<command>` shortcuts. Tool names and parameter keys are lifted verbatim from internal/helpers/doc.go (the single source of truth for DingTalk MCP tools).
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Copyright 2026 Alibaba Group Licensed under the Apache License, Version 2.0
Index ¶
Constants ¶
This section is empty.
Variables ¶
var BackgroundDelete = shortcut.Shortcut{ Service: "doc", Command: "+background-delete", Product: productDoc, Description: "清除文档背景色", Intent: "当用户明确要恢复文档默认背景、移除当前背景色时使用;执行 background clear 并要求确认。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "idempotent"}, Contract: docContract("+background-delete", "清除文档背景色", "当用户明确要恢复文档默认背景、移除当前背景色时使用;执行 background clear 并要求确认。", []string{`dws doc +background-delete --node <DOC_ID>`}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}}, Tips: []string{`dws doc +background-delete --node <DOC_ID>`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node"), "background": map[string]any{"action": "clear"}} if rt.DryRun() { return rt.Output(docEnvelope("doc.background_delete", map[string]any{"executed": false, "params": params})) } return rt.CallMCP("update_document_style", params) }, }
var BackgroundUpdate = shortcut.Shortcut{ Service: "doc", Command: "+background-update", Product: productDoc, Description: "设置文档 #RRGGBB 背景纯色", Intent: "当用户要设置在线文档背景纯色时使用;只接受 #RRGGBB,不支持背景图片。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+background-update", "设置文档 #RRGGBB 背景纯色", "当用户要设置在线文档背景纯色时使用;只接受 #RRGGBB,不支持背景图片。", []string{`dws doc +background-update --node <DOC_ID> --color "#E8F2FE"`}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "color", Type: shortcut.FlagString, Desc: "#RRGGBB 背景色", Required: true}}, Tips: []string{`dws doc +background-update --node <DOC_ID> --color "#E8F2FE"`}, Validate: func(rt *shortcut.RuntimeContext) error { color := rt.Str("color") if len(color) != 7 || color[0] != '#' { return apperrors.NewValidation("--color 必须是 #RRGGBB") } for _, char := range color[1:] { if !strings.ContainsRune("0123456789abcdefABCDEF", char) { return apperrors.NewValidation("--color 必须是 #RRGGBB") } } return nil }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"color"}, Description: "#RRGGBB"}}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("update_document_style", map[string]any{"nodeId": rt.Str("node"), "background": map[string]any{"action": "set", "backgroundColor": rt.Str("color")}}) }, }
var CheckpointUpdate = shortcut.Shortcut{ Service: "doc", Command: "+checkpoint-update", Product: productDoc, Description: "先保存可回滚版本,再更新并读回验证", Intent: "当用户要进行重要追加或整篇覆盖,并希望自动创建恢复点、执行更新、再读回确认时使用;任一步失败都会返回已经完成的步骤。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docContract("+checkpoint-update", "先保存可回滚版本,再更新并读回验证", "当用户要进行重要追加或整篇覆盖,并希望自动创建恢复点、执行更新、再读回确认时使用;任一步失败都会返回已经完成的步骤。", []string{`dws doc +checkpoint-update --node <DOC_ID> --mode append --content @section.md`, `dws doc +checkpoint-update --node <DOC_ID> --mode overwrite --content @document.md`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "mode", Type: shortcut.FlagString, Default: "append", Desc: "更新模式", Enum: []string{"append", "overwrite"}}, {Name: "content", Type: shortcut.FlagString, Desc: docContentInputDescription, Required: true}, }, Tips: []string{`dws doc +checkpoint-update --node <DOC_ID> --mode append --content @section.md`, `dws doc +checkpoint-update --node <DOC_ID> --mode overwrite --content @document.md`}, Execute: func(rt *shortcut.RuntimeContext) error { content, err := readShortcutContent(rt, "content") if err != nil { return err } chunkPlan := helpers.SplitMarkdownForAppend(content, helpers.DefaultMarkdownChunkRunes) chunks := chunkPlan.Chunks expected := chunkPlan.ExpectedDocument() plan := map[string]any{"nodeId": rt.Str("node"), "mode": rt.Str("mode"), "contentBytes": len(content), "steps": []string{"save_doc_version", "update_document", "get_document_content"}} if len(chunks) > 1 { plan["chunkPlan"] = chunkPlan.Summary() } if rt.DryRun() { plan["executed"] = false return rt.Output(withDocWarnings(docEnvelope("doc.checkpoint_update", plan), chunkPlan.Warnings())) } steps := []map[string]any{} checkpoint, err := rt.CallMCPWriteData(productDoc, "save_doc_version", map[string]any{"nodeId": rt.Str("node")}) if err != nil { return err } steps = append(steps, map[string]any{"name": "checkpoint", "status": "success"}) for index, chunk := range chunks { mode := "append" if index == 0 { mode = rt.Str("mode") } stepName := "update" if len(chunks) > 1 { stepName = fmt.Sprintf("update_chunk_%d", index+1) } if _, err := rt.CallMCPWriteData(productDoc, "update_document", map[string]any{"nodeId": rt.Str("node"), "markdown": chunk, "mode": mode}); err != nil { return checkpointPartialWriteError(rt.Str("node"), checkpoint, stepName, "doc_checkpoint_update_failed", err, append(steps, map[string]any{"name": stepName, "status": "failed"}, map[string]any{"name": "verify", "status": "not_started"})) } steps = append(steps, map[string]any{"name": stepName, "status": "success"}) } verification, err := readDocVerification(rt, "get_document_content", map[string]any{"nodeId": rt.Str("node"), "format": "markdown"}, func(data map[string]any) bool { return verifyUpdatedDocumentContent(data, expected, rt.Str("mode"), "markdown") }) if err != nil { return checkpointPartialWriteError(rt.Str("node"), checkpoint, "verify", "doc_checkpoint_verification_failed", err, append(steps, map[string]any{"name": "verify", "status": "failed"})) } if !verifyUpdatedDocumentContent(verification, expected, rt.Str("mode"), "markdown") { return checkpointPartialWriteError(rt.Str("node"), checkpoint, "verify", "doc_checkpoint_verification_failed", fmt.Errorf("回读结果未匹配预期变更"), append(steps, map[string]any{"name": "verify", "status": "failed"})) } steps = append(steps, map[string]any{"name": "verify", "status": "success"}) verificationSummary := compactDocVerification(verification, content, rt.Str("mode"), "markdown", nil) data := map[string]any{"nodeId": rt.Str("node"), "verified": true, "verification": verificationSummary} if len(chunks) > 1 { data["chunksWritten"] = len(chunks) data["chunkPlan"] = chunkPlan.Summary() } annotateMentionVerificationScope(data, steps, content) return rt.Output(withDocWarnings(docEnvelope("doc.checkpoint_update", data, steps...), withMentionTargetWarning(chunkPlan.Warnings(), content))) }, }
var CommentCreate = shortcut.Shortcut{ Service: "doc", Command: "+comment-create", Product: productComment, Description: "创建全文评论,或按 selection 创建划词评论", Intent: "当用户要对整篇文档留言,或针对文档中唯一匹配的一段文字创建精确划词评论时使用;已知 block/start/end 时也可直接走高级通道。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: docContract("+comment-create", "创建全文评论,或按 selection 创建划词评论", "当用户要对整篇文档留言,或针对文档中唯一匹配的一段文字创建精确划词评论时使用;已知 block/start/end 时也可直接走高级通道。", []string{`dws doc +comment-create --node <DOC_ID> --content "请补充数据来源"`, `dws doc +comment-create --node <DOC_ID> --selection "计划下周发布" --content "请确认日期"`}, contract.ParamDecl{Name: "node", Property: "node"}, contract.ParamDecl{Name: "content", Property: "content"}, contract.ParamDecl{Name: "selection", Property: "selection"}, contract.ParamDecl{Name: "block-id", Property: "blockId"}, contract.ParamDecl{Name: "start", Property: "start"}, contract.ParamDecl{Name: "end", Property: "end"}, contract.ParamDecl{Name: "selected-text", Property: "selectedText"}, contract.ParamDecl{Name: "mention", Property: "mention"}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "评论文字内容", Required: true}, {Name: "selection", Type: shortcut.FlagString, Desc: "完整文字或 前缀...后缀;" + commentCreateTargetConstraint}, {Name: "block-id", Type: shortcut.FlagString, Desc: "高级通道 block ID;" + commentCreateTargetConstraint}, {Name: "start", Type: shortcut.FlagInt, Desc: "块内 UTF-16 起始偏移;" + commentCreateTargetConstraint}, {Name: "end", Type: shortcut.FlagInt, Desc: "块内 UTF-16 结束偏移;" + commentCreateTargetConstraint}, {Name: "selected-text", Type: shortcut.FlagString, Desc: "可选引用原文;CLI 会从 block 回读并交叉校验"}, {Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"selection", "block-id", "start", "end"}, Description: commentCreateTargetConstraint}}, Tips: []string{`dws doc +comment-create --node <DOC_ID> --content "请补充数据来源"`, `dws doc +comment-create --node <DOC_ID> --selection "计划下周发布" --content "请确认日期"`}, Validate: validateCommentCreate, Execute: executeCommentCreate, }
var CommentCreateInline = shortcut.Shortcut{ Service: "doc", Command: "+comment-create-inline", Product: productComment, Description: "兼容入口:按 block/start/end 创建划词评论", Intent: "仅兼容既有调用;新任务统一使用 +comment-create 的 selection 或 block/start/end 通道。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "评论文字内容 (纯文本)", Required: true}, {Name: "block-id", Type: shortcut.FlagString, Desc: "评论标记所在的块 ID (通过 +block-list 获取)", Required: true}, {Name: "start", Type: shortcut.FlagInt, Desc: "块内文本起始字符偏移量 (从 0 开始)", Required: true}, {Name: "end", Type: shortcut.FlagInt, Desc: "块内文本结束字符偏移量 (须大于 start)", Required: true}, {Name: "selected-text", Type: shortcut.FlagString, Desc: "选中文本内容 (展示引用原文)"}, {Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"block-id", "start", "end"}, Description: "block-id/start/end 必须一起提供,CLI 回读并校验 selectedText"}}, Tips: []string{`dws doc +comment-create --node DOC_ID --block-id BLOCK_ID --start 0 --end 10 --content "这里需要修改"`}, Validate: validateCommentCreate, Execute: executeCommentCreate, }
var CommentDelete = shortcut.Shortcut{ Service: "doc", Command: "+comment-delete", Product: productComment, Description: "永久删除指定文档评论", Intent: "当用户明确要求永久删除某条文档评论,且已核对 node 与 commentKey 时使用;不可用于标记 resolved。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docContract("+comment-delete", "永久删除指定文档评论", "当用户明确要求永久删除某条文档评论,且已核对 node 与 commentKey 时使用;不可用于标记 resolved。", []string{`dws doc +comment-delete --node <DOC_ID> --comment-key <COMMENT_KEY>`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "comment-key", Type: shortcut.FlagString, Desc: "评论 commentKey", Required: true}, }, Tips: []string{`dws doc +comment-delete --node <DOC_ID> --comment-key <COMMENT_KEY>`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node"), "commentKey": rt.Str("comment-key")} if rt.DryRun() { return rt.Output(docEnvelope("doc.comment_delete", map[string]any{"executed": false, "params": params})) } return rt.CallMCP("delete_comment", params) }, }
var CommentList = shortcut.Shortcut{ Service: "doc", Command: "+comment-list", Product: productComment, Description: "查询文档评论列表", Intent: "当你想查看某篇文档上已有的评论、了解有哪些反馈或待处理意见(可按全文/划词、已解决/未解决过滤)时使用;输入 node,可用 --limit/--cursor 分页,返回评论列表及其 commentKey 以便后续回复。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_comment_list", CanonicalPath: "doc.shortcut_comment_list", CLIPath: "doc +comment-list", PrimaryCLIPath: "doc +comment-list", }, Description: "查询文档评论列表", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查询文档评论列表", UseWhen: []string{"当你想查看某篇文档上已有的评论、了解有哪些反馈或待处理意见(可按全文/划词、已解决/未解决过滤)时使用;输入 node,可用 --limit/--cursor 分页,返回评论列表及其 commentKey 以便后续回复。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws doc +comment-list --node DOC_ID --limit 20", "dws doc +comment-list --node DOC_ID --limit 20 --cursor NEXT_TOKEN", }, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 50,最大 50)", Aliases: []string{"page-size"}}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上一页返回的 nextToken)"}, {Name: "type", Type: shortcut.FlagString, Desc: "评论类型: global (全文) / inline (划词)", Enum: []string{"global", "inline"}}, {Name: "resolve-status", Type: shortcut.FlagString, Desc: "解决状态: resolved / unresolved", Enum: []string{"resolved", "unresolved"}}, }, Tips: []string{`dws doc +comment-list --node DOC_ID --limit 20`, `dws doc +comment-list --node DOC_ID --limit 20 --cursor NEXT_TOKEN`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node")} if rt.Changed("limit") || rt.Changed("page-size") { params["pageSize"] = rt.IntFirst("limit", "page-size") } if v := rt.Str("cursor"); v != "" { params["nextToken"] = v } if v := rt.Str("type"); v != "" { params["commentType"] = v } if v := rt.Str("resolve-status"); v != "" { params["resolveStatus"] = v } return rt.CallMCP("list_comments", params) }, }
var CommentReply = shortcut.Shortcut{ Service: "doc", Command: "+comment-reply", Product: productComment, Description: "回复文档中的一条评论", Intent: "当你要针对某条已有评论进行回复、参与讨论或用表情贴图回应时使用;先从评论列表拿到 comment-key,再输入 node、comment-key 与 content(--emoji 则作为表情回复),会实际发布一条回复。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_comment_reply", CanonicalPath: "doc.shortcut_comment_reply", CLIPath: "doc +comment-reply", PrimaryCLIPath: "doc +comment-reply", }, Description: "回复文档中的一条评论", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "回复文档中的一条评论", UseWhen: []string{"当你要针对某条已有评论进行回复、参与讨论或用表情贴图回应时使用;先从评论列表拿到 comment-key,再输入 node、comment-key 与 content(--emoji 则作为表情回复),会实际发布一条回复。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +comment-reply --node DOC_ID --comment-key COMMENT_KEY --content \"同意\""}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "回复文字内容 (表情回复时填表情名称)", Required: true}, {Name: "comment-key", Type: shortcut.FlagString, Desc: "被回复评论的 commentKey (从 list/create 获取)", Required: true}, {Name: "emoji", Type: shortcut.FlagBool, Desc: "作为表情贴图回复"}, {Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"}, }, Tips: []string{`dws doc +comment-reply --node DOC_ID --comment-key COMMENT_KEY --content "同意"`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{ "nodeId": rt.Str("node"), "content": rt.Str("content"), "replyCommentKey": rt.Str("comment-key"), } if rt.Bool("emoji") { if err := commentreaction.Validate(rt.Str("content")); err != nil { return err } params["emoji"] = true } if rt.Changed("mention") { mentions, err := normalizeMentionUserIDs(rt.StrSlice("mention")) if err != nil { return err } params["mentionedUserIds"] = mentions } return rt.CallMCP("reply_comment", params) }, }
var CommentUpdate = shortcut.Shortcut{ Service: "doc", Command: "+comment-update", Product: productComment, Description: "更新指定文档评论正文和 mention", Intent: "当用户要修改一条已有评论的文字内容或 @ 用户列表,且已知 commentKey 时使用;不会创建回复或改变解决状态。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown"}, Contract: docContract("+comment-update", "更新指定文档评论正文和 mention", "当用户要修改一条已有评论的文字内容或 @ 用户列表,且已知 commentKey 时使用;不会创建回复或改变解决状态。", []string{`dws doc +comment-update --node <DOC_ID> --comment-key <COMMENT_KEY> --content "已按最新数据修正"`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "comment-key", Type: shortcut.FlagString, Desc: "评论 commentKey", Required: true}, {Name: "content", Type: shortcut.FlagString, Desc: "更新后的评论正文", Required: true}, {Name: "mention", Type: shortcut.FlagStringSlice, Desc: "被 @ 的用户 uid,多个值用逗号分隔;不要传 JSON 数组"}, }, Tips: []string{`dws doc +comment-update --node <DOC_ID> --comment-key <COMMENT_KEY> --content "已按最新数据修正"`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node"), "commentKey": rt.Str("comment-key"), "content": rt.Str("content")} if rt.Changed("mention") { mentions, err := normalizeMentionUserIDs(rt.StrSlice("mention")) if err != nil { return err } params["mentionedUserIds"] = mentions } return rt.CallMCP("update_comment", params) }, }
var Copy = shortcut.Shortcut{ Service: "doc", Command: "+copy", Product: productDoc, Description: "复制文档/文件到指定文件夹或知识库", Intent: "当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_copy", CanonicalPath: "doc.shortcut_copy", CLIPath: "doc +copy", PrimaryCLIPath: "doc +copy", }, Description: "复制文档/文件到指定文件夹或知识库", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "复制文档/文件到指定文件夹或知识库", UseWhen: []string{"当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档/文件 ID 或 URL", Required: true}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文档文件夹 nodeId 或 alidocs 文件夹 URL"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"}, }, Tips: []string{`dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node")} if rt.Changed("folder") { params["targetFolderId"] = rt.Str("folder") } if rt.Changed("workspace") { params["workspaceId"] = rt.Str("workspace") } return rt.CallMCP("copy_document", params) }, }
var Create = shortcut.Shortcut{ Service: "doc", Command: "+create", Product: productDoc, Description: "从 Markdown 或 JSONML 创建在线文字文档", Intent: "当用户要新建钉钉在线文字文档,并可同时写入 Markdown/JSONML 初始内容、指定文件夹或知识库位置时使用;不会用于普通文件上传或其他在线对象类型。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, Contract: docContract( "+create", "从 Markdown 或 JSONML 创建在线文字文档", "当用户要新建钉钉在线文字文档,并可同时写入 Markdown/JSONML 初始内容、指定文件夹或知识库位置时使用;不会用于普通文件上传或其他在线对象类型。", []string{`dws doc +create --name "项目周报" --content "# 本周进展"`, `dws doc +create --name "模板" --content @body.json --doc-format jsonml`}, contract.ParamDecl{Name: "folder", Property: "folderId"}, contract.ParamDecl{Name: "workspace", Property: "workspaceId"}, ), Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "新文档名称;省略时从Markdown首个一级标题提取,否则使用未命名文档"}, {Name: "content", Type: shortcut.FlagString, Desc: docContentInputDescription}, {Name: "doc-format", Type: shortcut.FlagString, Default: "markdown", Desc: "内容格式", Enum: []string{"markdown", "jsonml"}}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文档文件夹 ID"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"}, }, Tips: []string{`dws doc +create --name "项目周报" --content "# 本周进展"`, `dws doc +create --name "模板" --content @body.json --doc-format jsonml`}, Execute: func(rt *shortcut.RuntimeContext) error { mediaFiles, err := validateDocCreateMedia(rt) if err != nil { return err } content, err := readShortcutContent(rt, "content") if err != nil { return err } format := rt.Str("doc-format") if format == "jsonml" && content != "" { content, err = validateJSONMLBody(rt.Command(), content) if err != nil { return err } } name := rt.Str("name") if name == "" { name = docDefaultTitle(content, format) } params := map[string]any{"name": name} if rt.Str("folder") != "" { params["folderId"] = rt.Str("folder") } if rt.Str("workspace") != "" { params["workspaceId"] = rt.Str("workspace") } contentChunks := []string{content} expected := content var chunkPlan helpers.MarkdownChunkPlan if format == "markdown" && content != "" { chunkPlan = helpers.SplitMarkdownForAppend(content, helpers.DefaultMarkdownChunkRunes) contentChunks = chunkPlan.Chunks expected = chunkPlan.ExpectedDocument() params["markdown"] = contentChunks[0] } if rt.DryRun() { preview := map[string]any{"executed": false, "previewKind": "plan", "create": params, "docFormat": format, "contentBytes": len(content), "mediaFiles": mediaFiles, "mediaPlacement": "append_in_order"} if len(contentChunks) > 1 { preview["chunkPlan"] = chunkPlan.Summary() } return outputCreatedDoc(rt, withDocWarnings(docEnvelope("doc.create", preview), chunkPlan.Warnings())) } created, err := rt.CallMCPWriteData(productDoc, "create_document", params) if err != nil { return docUnknownWriteError("doc.create", "create_document", "", err) } nodeID := nestedString(created, "nodeId", "documentId", "id") steps := []map[string]any{{"name": "create_document", "status": "success"}} if nodeID == "" { return docPartialWriteError( "doc.create", "doc_create_missing_node_id", "resolve_created_document", "创建文档成功但响应缺少 nodeId;无法验证新文档,请先在钉钉中定位,不要直接重试", nil, map[string]any{"nodeId": "", "docFormat": format, "verified": false}, append(steps, map[string]any{"name": "verify", "status": "not_started"}), map[string]any{"available": false, "reason": "create_document did not return nodeId; locate the new document in DingTalk"}, ) } if format == "jsonml" && content != "" { if _, err := rt.CallMCPWriteData(productDoc, "update_document", map[string]any{"nodeId": nodeID, "format": "jsonml", "jsonml": content, "mode": "overwrite"}); err != nil { return docPartialWriteError( "doc.create", "doc_create_initial_content_failed", "write_jsonml", fmt.Sprintf("文档已创建但 JSONML 写入失败(nodeId=%s);不要直接重试创建", nodeID), err, map[string]any{"nodeId": nodeID, "docFormat": format}, append(steps, map[string]any{"name": "write_jsonml", "status": "failed"}), map[string]any{"available": true, "action": "delete_created_document", "nodeId": nodeID, "reason": "remove the empty document before retrying create"}, ) } steps = append(steps, map[string]any{"name": "write_jsonml", "status": "success"}) } if format == "markdown" && len(contentChunks) > 1 { for index, chunk := range contentChunks[1:] { stepName := fmt.Sprintf("append_chunk_%d", index+2) if _, err := rt.CallMCPWriteData(productDoc, "update_document", map[string]any{"nodeId": nodeID, "markdown": chunk, "mode": "append"}); err != nil { return docPartialWriteError( "doc.create", "doc_create_chunk_commit_unknown", stepName, fmt.Sprintf("文档已创建,但第 %d/%d 个内容分片失败或提交状态未知;请先回读,不要重试整个创建", index+2, len(contentChunks)), err, map[string]any{"nodeId": nodeID, "chunksWritten": index + 1, "chunksTotal": len(contentChunks), "verified": false, "degradations": chunkPlan.Degradations}, append(steps, map[string]any{"name": stepName, "status": "unknown"}), map[string]any{"available": false, "reason": "inspect the current document and resume only confirmed missing content"}, ) } steps = append(steps, map[string]any{"name": stepName, "status": "success"}) } } verifyTool := "get_document_info" verifyParams := map[string]any{"nodeId": nodeID} if content != "" { verifyTool = "get_document_content" verifyParams["format"] = format } verification, err := readDocVerification(rt, verifyTool, verifyParams, func(data map[string]any) bool { return content == "" || verifyUpdatedDocumentContent(data, expected, "overwrite", format) }) if err != nil { return docVerificationError("doc.create", "verify", nodeID, err, append(steps, map[string]any{"name": "verify", "status": "failed"})) } if content != "" && !verifyUpdatedDocumentContent(verification, expected, "overwrite", format) { return docVerificationError("doc.create", "verify", nodeID, fmt.Errorf("回读结果与完整初始内容不一致"), append(steps, map[string]any{"name": "verify", "status": "failed"})) } steps = append(steps, map[string]any{"name": "verify", "status": "success"}) verificationSummary := compactDocVerification(verification, content, "overwrite", format, nil) data := map[string]any{"nodeId": nodeID, "result": created, "verified": true, "verification": verificationSummary} if len(contentChunks) > 1 { data["chunkPlan"] = chunkPlan.Summary() } if len(mediaFiles) > 0 { receipts, mediaErr := insertCreatedDocMedia(rt, nodeID, mediaFiles) if mediaErr != nil { return mediaErr } data["media"] = receipts data["mediaPlacement"] = "append_in_order" } annotateMentionVerificationScope(data, steps, content) return outputCreatedDoc(rt, withDocWarnings(docEnvelope("doc.create", data, steps...), withMentionTargetWarning(chunkPlan.Warnings(), content))) }, }
var CreateFromTemplate = shortcut.Shortcut{ Service: "doc", Command: "+create-from-template", Product: productDoc, Description: "使用已选定的 templateId 创建文档", Intent: "当模板搜索已经唯一解析或用户明确提供 templateId 时使用;只创建一次并返回稳定 nodeId。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown"}, Contract: createFromTemplateContract(), Flags: []shortcut.Flag{ {Name: "template-id", Type: shortcut.FlagString, Desc: "模板 ID"}, {Name: "query", Type: shortcut.FlagString, Desc: "兼容入口:先搜索且仅唯一命中时创建;新的 Agent 流程应先调用 +template-search"}, {Name: "source", Type: shortcut.FlagString, Desc: "模板来源", Enum: []string{"MY", "PUBLIC"}}, {Name: "name", Type: shortcut.FlagString, Desc: "新文档名称"}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 ID"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"template-id", "query"}, Description: "--template-id 与 --query 必须且只能提供一个"}}, Tips: []string{`dws doc +create-from-template --template-id <TEMPLATE_ID> --name "我的周报"`}, Execute: func(rt *shortcut.RuntimeContext) error { templateID := rt.Str("template-id") if templateID == "" { candidates, err := searchTemplateCandidatesForCreate(rt, rt.Str("query"), rt.Str("source")) if err != nil { return err } if len(candidates) != 1 { status := "selection_required" message := fmt.Sprintf("模板搜索返回 %d 个候选,必须先选择唯一 templateId", len(candidates)) actions := []string{"运行 dws doc +template-search 查看结构化候选", "让用户选择后使用 --template-id 创建一次"} if len(candidates) == 0 { status = "not_found" message = "模板搜索没有命中;已停止创建" actions = []string{"检查模板来源或请用户提供更准确的模板名", "不要自动变换关键词循环搜索"} } return apperrors.NewValidation(message, apperrors.WithOperation("doc.create_from_template"), apperrors.WithReason("template_"+status), apperrors.WithExecutionStarted(false), apperrors.WithRetryable(false), apperrors.WithActions(actions...), apperrors.WithDetails(map[string]any{ "contractVersion": "doc.template-selection.v1", "status": status, "query": rt.Str("query"), "source": rt.Str("source"), "candidates": candidates, }), ) } templateID = candidates[0]["templateId"].(string) } params := map[string]any{"templateId": templateID} for flag, property := range map[string]string{"name": "name", "folder": "folderId", "workspace": "workspaceId"} { if value := rt.Str(flag); value != "" { params[property] = value } } if rt.DryRun() { return rt.Output(docEnvelope("doc.create_from_template", map[string]any{"executed": false, "params": params})) } result, err := rt.CallMCPWriteData(productDoc, "apply_doc_template", params) if err != nil { return docUnknownWriteError("doc.create_from_template", "apply_template", "", err) } return rt.Output(docEnvelope("doc.create_from_template", map[string]any{"templateId": templateID, "result": result}, map[string]any{"name": "apply_template", "status": "success"})) }, }
var CreateWithMedia = func() shortcut.Shortcut { s := Create s.Command = createWithMediaCommand s.Description = "确认后创建文档并按顺序上传本地图片和附件" s.Intent = "需要在新文档正文末尾追加本地图片或附件时使用;确认前不创建文档或上传文件,所有路径先校验;失败保留已创建文档和媒体进度。只创建正文使用+create,已有文档插入媒体使用+media-insert。" s.Safety.Confirmation = "user_required" s.Flags = append(append([]shortcut.Flag(nil), Create.Flags...), shortcut.Flag{Name: "media-files", Type: shortcut.FlagStringSlice, Required: true, Desc: "media-files必须为1到20个工作目录内非空普通文件的相对路径,不能重复;按顺序追加,不改写正文内相对链接"}) s.Constraints = []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"media-files"}, Description: "media-files必须为1到20个工作目录内非空普通文件的相对路径,不能重复"}} s.Validate = func(rt *shortcut.RuntimeContext) error { _, err := validateDocCreateMedia(rt) return err } s.Tips = []string{"dws doc +create-with-media --name 项目资料 --media-files ./report.pdf"} s.Contract = docContract(s.Command, s.Description, s.Intent, s.Tips, contract.ParamDecl{Name: "folder", Property: "folderId"}, contract.ParamDecl{Name: "workspace", Property: "workspaceId"}) s.Contract.DryRun = &contract.DryRunSpec{PreviewKind: contract.DryRunPreviewPlan, RemoteReads: false} s.Contract.Result = &contract.ResultSpec{Outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure}, DataSchema: json.RawMessage(`{"type":"object","properties":{"executed":{"type":"boolean","description":"是否执行创建和媒体上传"},"preview_kind":{"type":"string","description":"dry-run计划类型"},"nodeId":{"type":"string","description":"已创建文档ID"},"result":{"type":"object","description":"底层创建回执"},"verified":{"type":"boolean","description":"初始正文或文档已独立读回核对"},"verification":{"type":"object","description":"初始内容读回证据"},"media":{"type":"array","description":"按输入顺序排列的媒体插入回执","items":{"type":"object"}},"mediaPlacement":{"type":"string","description":"媒体位置,append_in_order表示按序追加"},"steps":{"type":"array","description":"已完成步骤","items":{"type":"object"}},"warnings":{"type":"array","description":"内容转换和验证边界说明","items":{"type":"string"}},"create":{"type":"object","description":"dry-run创建请求"},"docFormat":{"type":"string","description":"初始正文格式"},"contentBytes":{"type":"integer","description":"初始正文长度"},"mediaFiles":{"type":"array","description":"已校验的本地媒体路径","items":{"type":"string"}},"chunkPlan":{"type":"object","description":"长Markdown分片计划"}}}`), SensitivePaths: []string{"create.markdown"}} s.OutputRollout = output.RolloutUnifiedActive return s }()
var DownloadOverwrite = shortcut.Shortcut{ Service: "doc", Command: downloadOverwriteCommand, Product: productDoc, Description: "确认后下载正文媒体或封面,允许原子替换已有普通文件", Intent: docDownloadOverwriteIntent, Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "idempotent"}, OutputRollout: output.RolloutUnifiedActive, Contract: downloadOverwriteContract(), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Required: true, Desc: "文档ID或URL"}, {Name: "source", Type: shortcut.FlagString, Default: "media", Enum: []string{"media", "cover"}, Desc: "media下载正文媒体,cover下载当前封面;media必须提供附件UUID;cover不能提供resource-id"}, {Name: "resource-id", Type: shortcut.FlagString, Desc: "media必须提供附件UUID;cover不能提供resource-id"}, {Name: "output", Shorthand: "o", Type: shortcut.FlagString, Required: true, Desc: "output必须为工作目录内相对路径;不替换符号链接或非普通文件;允许原子覆盖普通文件"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"source", "resource-id"}, Description: "media必须提供附件UUID;cover不能提供resource-id"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "output必须为工作目录内相对路径;不替换符号链接或非普通文件"}, }, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Str("source") == "media" { if err := validateDocResourceID(rt.Str("resource-id")); err != nil { return err } } else if rt.Changed("resource-id") { return apperrors.NewValidation("source=cover不能提供resource-id") } if err := localio.ValidateOutput(rt.Str("output")); err != nil { return apperrors.NewValidation(err.Error()) } return nil }, Execute: func(rt *shortcut.RuntimeContext) error { if rt.DryRun() { return rt.Output(map[string]any{"executed": false, "preview_kind": "plan", "nodeId": rt.Str("node"), "source": rt.Str("source"), "localPath": rt.Str("output")}) } if rt.Str("source") == "cover" { return executeResourceDownload(rt) } return executeMediaDownload(rt) }, }
var Export = shortcut.Shortcut{ Service: "doc", Command: "+export", Product: productDoc, Description: "提交、轮询并安全下载在线文档导出文件", Intent: "当用户要把在线文档导出成 docx、markdown 或 PDF 并保存到工作目录时使用;自动完成 job 提交、轮询与 no-clobber 原子下载;失败后保留 jobId 通过 +export-get 恢复,不改用 curl 或本地生成。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+export", "提交、轮询并安全下载在线文档导出文件", "当用户要把在线文档导出成 docx、markdown 或 PDF 并保存到工作目录时使用;自动完成 job 提交、轮询与 no-clobber 原子下载;失败后保留 jobId 通过 +export-get 恢复,不改用 curl 或本地生成。", []string{`dws doc +export --node <DOC_ID> --export-format docx --output ./exports/`, `dws doc +export --node <DOC_ID> --export-format markdown --output ./document.md`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "export-format", Type: shortcut.FlagString, Default: "docx", Desc: "导出格式;省略时默认为 docx,不能用全局 --format 代替", Enum: []string{"docx", "markdown", "pdf"}}, {Name: "output", Type: shortcut.FlagString, Default: ".", Desc: "工作目录内相对路径(文件或目录)"}, {Name: "max-polls", Type: shortcut.FlagInt, Default: "30", Desc: "最大轮询次数"}, }, Tips: []string{`dws doc +export --node <DOC_ID> --export-format docx --output ./exports/`, `dws doc +export --node <DOC_ID> --export-format markdown --output ./document.md`}, Validate: func(rt *shortcut.RuntimeContext) error { return localio.ValidateOutput(rt.Str("output")) }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "--output 必须是工作目录内相对路径;默认 no-clobber"}}, Execute: executeExport, }
var ExportGet = shortcut.Shortcut{ Service: "doc", Command: "+export-get", Product: productDoc, Description: "根据 jobId 查询文档导出任务结果", Intent: "当 +export 已返回 jobId 但轮询、中断或下载失败时使用;复用同一 job 查询,给 output 时通过 CLI 安全下载,禁止重新提交或 curl 临时链接。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_export_get", CanonicalPath: "doc.shortcut_export_get", CLIPath: "doc +export-get", PrimaryCLIPath: "doc +export-get", }, Description: "根据 jobId 查询文档导出任务结果", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "根据 jobId 查询文档导出任务结果", UseWhen: []string{"当 +export 已返回 jobId 但轮询、中断或下载失败时使用;复用同一 job 查询,给 output 时通过 CLI 安全下载,禁止重新提交或 curl 临时链接。"}, AvoidWhen: []string{"尚未提交导出或没有真实 jobId 时不要使用;正常首次导出使用 doc +export"}, Examples: []string{"dws doc +export-get --job-id JOB_ID", "dws doc +export-get --job-id JOB_ID --output ./exports/"}, }, }, Flags: []shortcut.Flag{ {Name: "job-id", Type: shortcut.FlagString, Desc: "导出任务 ID", Required: true}, {Name: "output", Shorthand: "o", Type: shortcut.FlagString, Desc: "可选:任务完成后安全下载到工作目录内相对路径"}, }, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Str("output") == "" { return nil } return localio.ValidateOutput(rt.Str("output")) }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "提供 --output 时必须是工作目录内相对路径;默认 no-clobber"}}, Tips: []string{`dws doc +export-get --job-id JOB_ID`, `dws doc +export-get --job-id JOB_ID --output ./exports/`}, Execute: executeExportGet, }
var ExportSubmit = shortcut.Shortcut{ Service: "doc", Command: "+export-submit", Product: productDoc, Description: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId", Intent: "仅当用户明确要求手工接管异步导出 job,且不需要当前命令下载文件时使用;返回 jobId 后只能用 +export-get 恢复。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_export_submit", CanonicalPath: "doc.shortcut_export_submit", CLIPath: "doc +export-submit", PrimaryCLIPath: "doc +export-submit", }, Description: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "提交在线文档导出任务 (docx/markdown/pdf),返回 jobId", UseWhen: []string{"仅当用户明确要求手工接管异步导出 job,且不需要当前命令下载文件时使用;返回 jobId 后只能用 +export-get 恢复。"}, AvoidWhen: []string{"正常导出和保存本地文件使用 doc +export;不要手工编排 submit/get,也不要用它绕过 +export 的安全下载"}, Examples: []string{"dws doc +export-submit --node DOC_ID --export-format markdown"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "要导出的文档 ID 或 URL", Required: true}, {Name: "export-format", Type: shortcut.FlagString, Default: "docx", Desc: "导出格式;省略时默认为 docx", Enum: []string{"docx", "markdown", "pdf"}}, }, Tips: []string{`dws doc +export-submit --node DOC_ID --export-format markdown`}, Execute: func(rt *shortcut.RuntimeContext) error { result, err := rt.CallMCPWriteData(productDoc, "submit_export_job", map[string]any{ "nodeId": rt.Str("node"), "exportFormat": rt.Str("export-format"), }) if err != nil { return docUnknownWriteError("doc.export_submit", "submit_export_job", rt.Str("node"), err) } return rt.Output(docEnvelope("doc.export_submit", map[string]any{"nodeId": rt.Str("node"), "result": result}, map[string]any{"name": "submit_export_job", "status": "success"})) }, }
var Fetch = shortcut.Shortcut{ Service: "doc", Command: "+fetch", Product: productDoc, Description: "读取完整或局部文档内容,并按 detail 控制保真度", Intent: "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --version 指定版本号(0 表示初始版本)。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract( "+fetch", "读取完整或局部文档内容,并按 detail 控制保真度", "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --version 指定版本号(0 表示初始版本)。", []string{`dws doc +fetch --node <DOC_ID>`, `dws doc +fetch --query "项目周报" --scope keyword --keyword "结论"`}, contract.ParamDecl{Name: "node", Property: "nodeId"}, contract.ParamDecl{Name: "query", Property: "keyword"}, contract.ParamDecl{Name: "password", Property: "password"}, contract.ParamDecl{Name: "version", Property: "historyVersion"}, ), Flags: []shortcut.Flag{ {Name: "include-comments", Type: shortcut.FlagBool, Desc: "同时读取全文评论,自动翻页;评论范围始终是整篇文档,最多1000条,超限报不完整"}, {Name: "context-unit", Type: shortcut.FlagString, Default: "characters", Enum: []string{"characters", "blocks"}, Desc: "关键词上下文单位;默认保留字符模式,blocks按相邻顶层块"}, {Name: "regex", Type: shortcut.FlagBool, Desc: "keyword+blocks模式使用RE2正则;非法正则在请求前拒绝"}, {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL;" + fetchTargetConstraint}, {Name: "query", Type: shortcut.FlagString, Desc: "文档标题或关键词;跨页唯一解析后读取;" + fetchTargetConstraint}, {Name: "detail", Type: shortcut.FlagString, Default: "simple", Desc: "输出细节", Enum: []string{"simple", "with-ids", "full"}}, {Name: "scope", Type: shortcut.FlagString, Default: "full", Desc: "读取范围;keyword 时 --keyword 不能为空", Enum: []string{"full", "outline", "range", "section", "keyword", "tags", "chapter"}}, {Name: "start-block-id", Type: shortcut.FlagString, Desc: "range/section 起始块 ID"}, {Name: "end-block-id", Type: shortcut.FlagString, Desc: "range 结束块 ID"}, {Name: "keyword", Type: shortcut.FlagString, Desc: "keyword 范围搜索词,不能为空,支持 foo|bar"}, {Name: "tags", Type: shortcut.FlagStringSlice, Desc: "tags 范围的 JSONML tag"}, {Name: "context-before", Type: shortcut.FlagInt, Desc: "关键词命中前的上下文字符数"}, {Name: "context-after", Type: shortcut.FlagInt, Desc: "关键词命中后的上下文字符数"}, {Name: "max-depth", Type: shortcut.FlagInt, Desc: "outline/section 最大深度"}, {Name: "password", Type: shortcut.FlagString, Desc: "互联网公开文档开启密码保护时的访问密码;普通文档无需传入"}, {Name: "revision", Type: shortcut.FlagInt, Desc: "不支持;revision 是文档编辑版本号(JSONML 读取响应返回、供 +update --expected-revision 条件写使用),不是历史版本号"}, {Name: "version", Type: shortcut.FlagInt, Desc: "读取指定历史版本(版本号从 doc +version-list 获取, 0 表示初始版本, 需要文档编辑权限);缺省读最新版"}, }, Tips: []string{`dws doc +fetch --node <DOC_ID>`, `dws doc +fetch --query "项目周报" --scope keyword --keyword "结论"`}, Validate: func(rt *shortcut.RuntimeContext) error { if err := validateDocSelection(rt); err != nil { return err } if rt.Changed("revision") { return apperrors.NewValidation("--revision 不支持:revision 是文档编辑版本号(doc read --content-format jsonml 响应返回,供 doc +update --expected-revision 条件写使用),不是历史版本号;读历史版本请用 --version") } if rt.Changed("version") && rt.Int("version") < 0 { return apperrors.NewValidation("--version 必须为非负整数历史版本号(0 表示初始版本,从 doc +version-list 获取)") } if rt.Str("scope") == "tags" && len(stringSliceNonEmpty(rt.StrSlice("tags"))) == 0 { return apperrors.NewValidation("--scope tags 时必须提供非空 --tags") } if rt.Str("scope") == "keyword" && rt.Str("keyword") == "" { return apperrors.NewValidation("--scope keyword 时必须提供 --keyword") } return nil }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"node", "query"}, Description: fetchTargetConstraint}, {Kind: shortcut.ConstraintCustom, Flags: []string{"scope", "keyword"}, Description: "--scope keyword 时 --keyword 不能为空"}, }, Execute: func(rt *shortcut.RuntimeContext) error { target, err := docresolver.Resolve(rt, rt.Str("node"), rt.Str("query")) if err != nil { return err } format := "markdown" if rt.Str("detail") != "simple" || rt.Str("scope") != "full" { format = "jsonml" } params := map[string]any{"nodeId": target.Selected.CanonicalID, "format": format} scope := rt.Str("scope") if scope != "keyword" && scope != "full" && scope != "chapter" { params["scope"] = scope } if scope != "chapter" { if value := rt.Str("start-block-id"); value != "" { params["startBlockId"] = value } if value := rt.Str("end-block-id"); value != "" { params["endBlockId"] = value } if rt.Changed("tags") { params["tags"] = strings.Join(stringSliceNonEmpty(rt.StrSlice("tags")), ",") } } if rt.Changed("max-depth") { params["maxDepth"] = rt.Int("max-depth") } if rt.Changed("version") { params["historyVersion"] = rt.Int("version") } if value := rt.Str("password"); value != "" { params["password"] = value } data, err := rt.CallMCPData(productDoc, "get_document_content", params) if err != nil { return err } content := any(data) if scope == "chapter" { content, err = selectDocChapter(data, rt.Str("start-block-id"), rt.Int("context-before"), rt.Int("context-after")) if err != nil { return err } } else if scope == "keyword" { if rt.Str("context-unit") == "blocks" { content, err = projectKeywordBlocks(data, rt.Str("keyword"), rt.Bool("regex"), rt.Int("context-before"), rt.Int("context-after")) if err != nil { return err } } else { content = projectKeywordMatches(data, rt.Str("keyword"), rt.Int("context-before"), rt.Int("context-after")) } } result := map[string]any{"contractVersion": "doc.content.v1", "status": "success", "complete": true, "target": target.Selected, "content": content} if rt.Bool("include-comments") { comments, err := readCompleteDocComments(rt, target.Selected.CanonicalID) if err != nil { return err } if comments["complete"] != true { return apperrors.NewAPI("评论读取未完整,不能报告全部读取成功", apperrors.WithDetails(map[string]any{"body": result, "comments": comments})) } result["comments"] = comments result["commentScope"] = "document" } return rt.Output(result) }, }
var Import = shortcut.Shortcut{ Service: "doc", Command: "+import", Product: productDoc, Description: "上传本地文件并等待转换成在线文档对象;白名单外格式自动改走文件上传原样入库", Intent: "当用户要把工作目录内的 doc/docx/xls/xlsx/md/txt/xmind/mark 相对路径文件导入为钉钉在线对象,并可指定目标文件夹或知识库时使用;白名单外格式(html/pdf 等)自动按原文件上传入库,结果带 fallback=upload、converted=false 标记。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown"}, Contract: docContract("+import", "上传本地文件并等待转换成在线文档对象;白名单外格式自动改走文件上传原样入库", "当用户要把工作目录内的 doc/docx/xls/xlsx/md/txt/xmind/mark 相对路径文件导入为钉钉在线对象,并可指定目标文件夹或知识库时使用;白名单外格式(html/pdf 等)自动按原文件上传入库,结果带 fallback=upload、converted=false 标记。", []string{`dws doc +import --file ./report.docx`, `dws doc +import --file ./notes.md --workspace <WORKSPACE_ID> --name "会议纪要"`}), Flags: []shortcut.Flag{ {Name: "file", Type: shortcut.FlagString, Desc: "工作目录内已存在文件的相对路径", Required: true}, {Name: "folder", Type: shortcut.FlagString, Desc: "可选目标文件夹 ID;与 workspace 互斥;在线转换格式省略二者时解析当前组织唯一 orgSpace 根目录"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "可选目标知识库 ID;与 folder 互斥;在线转换格式省略二者时解析当前组织唯一 orgSpace 根目录"}, {Name: "name", Type: shortcut.FlagString, Desc: "导入后名称"}, }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"file"}, Description: "--file 必须是工作目录内已存在且不通过符号链接逃逸的相对路径"}, }, Tips: []string{`dws doc +import --file ./report.docx`, `dws doc +import --file ./notes.md --workspace <WORKSPACE_ID> --name "会议纪要"`}, Validate: func(rt *shortcut.RuntimeContext) error { return validateWorkspaceInputPath("file", rt.Str("file")) }, Execute: func(rt *shortcut.RuntimeContext) error { if err := helpers.RunDocImportShortcut(rt.Command()); err != nil { return docUnknownWriteError("doc.import", "import", "", err) } return nil }, }
var Inspect = shortcut.Shortcut{ Service: "doc", Command: "+inspect", Product: productDoc, Description: "聚合文档元信息,并按需附带样式、权限、历史、媒体和评论", Intent: "当用户需要在一次调用中了解文档类型、标题、链接和可选的协作/样式/历史/媒体/评论状态,而不是读取正文时使用。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+inspect", "聚合文档元信息,并按需附带样式、权限、历史、媒体和评论", "当用户需要在一次调用中了解文档类型、标题、链接和可选的协作/样式/历史/媒体/评论状态,而不是读取正文时使用。", []string{`dws doc +inspect --node <DOC_ID>`, `dws doc +inspect --node <DOC_ID> --include-style --include-permissions --include-comments`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "include-style", Type: shortcut.FlagBool, Desc: "附带封面和背景"}, {Name: "include-permissions", Type: shortcut.FlagBool, Desc: "附带权限列表"}, {Name: "include-history", Type: shortcut.FlagBool, Desc: "附带最近历史版本"}, {Name: "include-media", Type: shortcut.FlagBool, Desc: "附带正文媒体列表"}, {Name: "include-comments", Type: shortcut.FlagBool, Desc: "附带评论列表"}, }, Tips: []string{`dws doc +inspect --node <DOC_ID>`, `dws doc +inspect --node <DOC_ID> --include-style --include-permissions --include-comments`}, Execute: func(rt *shortcut.RuntimeContext) error { node := rt.Str("node") result := map[string]any{} info, err := rt.CallMCPData(productDoc, "get_document_info", map[string]any{"nodeId": node}) if err != nil { return err } result["document"] = info reads := []struct { flag, key, product, tool string params map[string]any }{ {"include-style", "style", productDoc, "get_document_style", map[string]any{"nodeId": node}}, {"include-permissions", "permissions", productDoc, "list_permission", map[string]any{"nodeId": node}}, {"include-history", "history", productDoc, "list_doc_versions", map[string]any{"nodeId": node}}, {"include-media", "media", productDoc, "list_document_blocks", map[string]any{"nodeId": node, "format": "jsonml"}}, {"include-comments", "comments", productComment, "list_comments", map[string]any{"nodeId": node}}, } steps := []map[string]any{{"name": "get_document_info", "status": "success"}} failures := []map[string]any{} for _, read := range reads { if !rt.Bool(read.flag) { continue } value, callErr := rt.CallMCPReadData(read.product, read.tool, read.params) if callErr != nil { failures = append(failures, map[string]any{"tool": read.tool, "error": callErr.Error()}) steps = append(steps, map[string]any{"name": read.tool, "status": "failed"}) continue } result[read.key] = value steps = append(steps, map[string]any{"name": read.tool, "status": "success"}) } if len(failures) > 0 { return apperrors.NewAPI( "文档聚合检查只完成了部分读取", apperrors.WithOperation("doc.inspect"), apperrors.WithReason("doc_inspect_partial"), apperrors.WithFailureStage("optional_reads"), apperrors.WithExecutionStarted(false), apperrors.WithRetryable(true), apperrors.WithDetails(map[string]any{ "contractVersion": "doc.operation.v1", "status": "partial_success", "complete": false, "data": result, "steps": steps, "failures": failures, }), ) } return rt.Output(docEnvelope("doc.inspect", result, steps...)) }, }
var List = shortcut.Shortcut{ Service: "doc", Command: "+list", Product: productDoc, Description: "列出文件夹或知识库下的直接子节点", Intent: "已知文档文件夹nodeId或知识库workspaceId,仅需浏览直接子节点时使用;不递归、不执行关键词检索。钉盘spaceId/dentryUuid目录使用 drive +list。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_list", CanonicalPath: "doc.shortcut_list", CLIPath: "doc +list", PrimaryCLIPath: "doc +list", }, Description: "列出文件夹或知识库下的直接子节点", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "列出文件夹或知识库下的直接子节点", UseWhen: []string{"已知文档文件夹nodeId或知识库workspaceId,仅需浏览直接子节点时使用;不递归、不执行关键词检索。钉盘spaceId/dentryUuid目录使用 drive +list。"}, AvoidWhen: []string{"已有关键词或属性条件且要限定文档文件夹使用 doc +search --folder;钉盘目录使用 drive +list;知识库层级管理使用 wiki,不重复搜索加列表做目录浏览"}, Examples: []string{ "dws doc +list --folder DOC_FOLDER_NODE_ID", "dws doc +list --workspace WS_ID --limit 20", }, }, }, Flags: []shortcut.Flag{ {Name: "folder", Type: shortcut.FlagString, Desc: "文档文件夹 nodeId 或 alidocs 文件夹 URL"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 50,最大 50)"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上次结果的 nextPageToken)"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "有界读取全部后续页;--max-pages/--max-items 仅在 --page-all 时生效且必须大于 0"}, {Name: "max-pages", Type: shortcut.FlagInt, Default: "20", Desc: "--max-pages 仅在 --page-all 时生效,且必须大于 0"}, {Name: "max-items", Type: shortcut.FlagInt, Default: "500", Desc: "--page-all 最多返回节点数,必须大于 0"}, }, Constraints: docAutoPaginationConstraints(), Tips: []string{`dws doc +list --folder DOC_FOLDER_NODE_ID`, `dws doc +list --workspace WS_ID --limit 20`}, Validate: validateDocAutoPagination, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{} if rt.Changed("folder") { params["folderId"] = rt.Str("folder") } if rt.Changed("workspace") { params["workspaceId"] = rt.Str("workspace") } pageSize := rt.Int("limit") if pageSize == 0 { pageSize = 50 } result, err := collectDocPages(rt, "list_nodes", "nodes", params, listNodesProject, docPageOptions{ PageAll: rt.Bool("page-all"), PageSize: pageSize, MaxPages: rt.Int("max-pages"), MaxItems: rt.Int("max-items"), Cursor: rt.Str("cursor"), }) if err != nil { return err } return rt.Output(result) }, }
var MediaDownload = shortcut.Shortcut{ Service: "doc", Command: "+media-download", Product: productDoc, Description: "安全下载文档正文附件到工作目录", Intent: "当用户已从 +media-list 拿到真实 resourceId,要把正文附件保存到工作目录时使用;CLI 内部换取临时链接并原子下载,默认拒绝覆盖;覆盖已有文件用 +download-overwrite。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+media-download", "安全下载文档正文附件到工作目录", "当用户已从 +media-list 拿到真实 resourceId,要把正文附件保存到工作目录时使用;CLI 内部换取临时链接并原子下载,默认拒绝覆盖;覆盖已有文件用 +download-overwrite。", []string{`dws doc +media-download --node <DOC_ID> --resource-id <RESOURCE_ID> --output ./downloads/`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "resource-id", Type: shortcut.FlagString, Desc: "附件 resourceId;--resource-id 必须是附件回执返回的 UUID", Required: true}, {Name: "output", Type: shortcut.FlagString, Default: ".", Desc: "工作目录内相对路径(文件或目录)"}, }, Validate: func(rt *shortcut.RuntimeContext) error { if err := validateDocResourceID(rt.Str("resource-id")); err != nil { return err } return localio.ValidateOutput(rt.Str("output")) }, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintCustom, Flags: []string{"resource-id"}, Description: "--resource-id 必须是附件回执返回的 UUID"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "--output 必须是工作目录内相对路径;默认 no-clobber"}, }, Tips: []string{`dws doc +media-download --node <DOC_ID> --resource-id <RESOURCE_ID> --output ./downloads/`}, Execute: executeMediaDownload, }
var MediaInsert = shortcut.Shortcut{ Service: "doc", Command: "+media-insert", Product: productDoc, Description: "上传本地图片或文件并插入文档正文", Intent: "当用户要把工作目录内的本地图片或附件作为正文 block 插入在线文档时使用;组合本地校验、上传凭证、OSS PUT 和插块,失败后保留稳定 ID,禁止改走手写 HTTP。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: withDryRun(docContract("+media-insert", "上传本地图片或文件并插入文档正文", "当用户要把工作目录内的本地图片或附件作为正文 block 插入在线文档时使用;组合本地校验、上传凭证、OSS PUT 和插块,失败后保留稳定 ID,禁止改走手写 HTTP。", []string{`dws doc +media-insert --node <DOC_ID> --file ./report.pdf`, `dws doc +media-insert --node <DOC_ID> --file ./image.png --ref-block <BLOCK_ID> --where after`}), contract.DryRunPreviewPlan, false), Flags: []shortcut.Flag{ {Name: "file-view", Type: shortcut.FlagString, Enum: []string{"preview", "summary"}, Desc: "附件展示方式;仅附件有效,图片拒绝;省略沿用原默认"}, {Name: "from-clipboard", Type: shortcut.FlagBool, Desc: "读取系统剪贴板PNG,与本地file及image互斥"}, {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "file", Type: shortcut.FlagString, Desc: "工作目录内已存在的相对文件路径", Required: false}, {Name: "name", Type: shortcut.FlagString, Desc: "显示名称"}, {Name: "mime-type", Type: shortcut.FlagString, Desc: "MIME 类型"}, {Name: "index", Type: shortcut.FlagInt, Desc: "顶层插入索引"}, {Name: "where", Type: shortcut.FlagString, Desc: "相对参考块的位置", Enum: []string{"before", "after"}}, {Name: "ref-block", Type: shortcut.FlagString, Desc: "参考 block ID"}, }, Validate: validateDocImageInput, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"file", "from-clipboard"}, Description: "必须且只能提供file或from-clipboard"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"file"}, Description: "--file 必须是工作目录内存在且不能经符号链接逃逸的相对文件"}}, Tips: []string{`dws doc +media-insert --node <DOC_ID> --file ./report.pdf`, `dws doc +media-insert --node <DOC_ID> --file ./image.png --ref-block <BLOCK_ID> --where after`}, Execute: func(rt *shortcut.RuntimeContext) error { return withDocClipboard(rt, func() error { return helpers.RunDocMediaInsertShortcut(rt.Command()) }) }, }
var MediaList = shortcut.Shortcut{ Service: "doc", Command: "+media-list", Product: productDoc, Description: "列出文档正文中的图片和附件资源", Intent: "当用户要发现文档内可下载或可定位的图片、附件及其 blockId/resourceId 时使用;先取得稳定 ID,再交给 +media-download,禁止提取临时 URL 后 curl。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+media-list", "列出文档正文中的图片和附件资源", "当用户要发现文档内可下载或可定位的图片、附件及其 blockId/resourceId 时使用;先取得稳定 ID,再交给 +media-download,禁止提取临时 URL 后 curl。", []string{`dws doc +media-list --node <DOC_ID>`}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}}, Tips: []string{`dws doc +media-list --node <DOC_ID>`}, Execute: func(rt *shortcut.RuntimeContext) error { data, err := rt.CallMCPData(productDoc, "list_document_blocks", map[string]any{"nodeId": rt.Str("node"), "format": "element"}) if err != nil { return err } items := collectMediaItems(data) return rt.Output(docEnvelope("doc.media_list", map[string]any{"nodeId": rt.Str("node"), "count": len(items), "media": items})) }, }
var MediaPreview = shortcut.Shortcut{ Service: "doc", Command: "+media-preview", Product: productDoc, Description: "下载正文媒体到受控临时目录并返回预览路径", Intent: "当用户要临时查看文档附件或图片内容而不指定持久保存路径时使用;下载到独立临时目录并返回 artifact 路径;默认拒绝覆盖,覆盖已有文件用 +download-overwrite。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+media-preview", "下载正文媒体到受控临时目录并返回预览路径", "当用户要临时查看文档附件或图片内容而不指定持久保存路径时使用;下载到独立临时目录并返回 artifact 路径;默认拒绝覆盖,覆盖已有文件用 +download-overwrite。", []string{`dws doc +media-preview --node <DOC_ID> --resource-id <RESOURCE_ID>`}), Flags: []shortcut.Flag{ {Name: "output", Shorthand: "o", Type: shortcut.FlagString, Desc: "可选:持久保存到工作目录内相对路径;不传使用临时目录"}, {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "resource-id", Type: shortcut.FlagString, Desc: "附件 resourceId;--resource-id 必须是附件回执返回的 UUID", Required: true}, }, Validate: func(rt *shortcut.RuntimeContext) error { if err := validateDocResourceID(rt.Str("resource-id")); err != nil { return err } if rt.Changed("output") { return localio.ValidateOutput(rt.Str("output")) } return nil }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"resource-id"}, Description: "--resource-id 必须是附件回执返回的 UUID"}}, Tips: []string{`dws doc +media-preview --node <DOC_ID> --resource-id <RESOURCE_ID>`}, Execute: func(rt *shortcut.RuntimeContext) error { if rt.Changed("output") { return executeMediaDownload(rt) } if rt.DryRun() { return rt.Output(docEnvelope("doc.media_preview", map[string]any{"executed": false, "nodeId": rt.Str("node"), "resourceId": rt.Str("resource-id"), "output": "managed_temp_dir"})) } data, err := rt.CallMCPData(productDoc, "download_doc_attachment", map[string]any{"nodeId": rt.Str("node"), "resourceId": rt.Str("resource-id")}) if err != nil { return docMediaRecoveryError("doc_media_resolve_failed", "resolve_download", rt.Str("node"), rt.Str("resource-id"), err) } dir, err := docMkdirTemp("", "dws-doc-preview-*") if err != nil { return err } result, err := downloadResolvedResource(rt, data, dir, ".") if err != nil { _ = docRemoveAll(dir) return docMediaRecoveryError("doc_media_preview_failed", "download", rt.Str("node"), rt.Str("resource-id"), err) } if result.SizeBytes <= 0 { _ = docRemoveAll(dir) return docMediaRecoveryError("doc_media_empty_download", "verify", rt.Str("node"), rt.Str("resource-id"), fmt.Errorf("下载结果为空")) } return rt.Output(docEnvelope("doc.media_preview", map[string]any{"nodeId": rt.Str("node"), "resourceId": rt.Str("resource-id"), "previewPath": result.AbsolutePath, "sizeBytes": result.SizeBytes, "verified": true})) }, }
var MediaUpload = shortcut.Shortcut{ Service: "doc", Command: "+media-upload", Product: productDoc, Description: "上传同文档可复用媒体并下载校验字节,不插入正文", Intent: docMediaUploadIntent, Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, OutputRollout: output.RolloutUnifiedActive, Contract: mediaUploadContract(), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Required: true, Desc: "资源所属文档ID或URL"}, {Name: "file", Type: shortcut.FlagString, Required: true, Desc: "file必须为工作目录内的相对文件路径"}, {Name: "name", Type: shortcut.FlagString, Desc: "可选资源文件名"}, {Name: "mime-type", Type: shortcut.FlagString, Desc: "可选MIME类型,默认按扩展名"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"file"}, Description: "file必须为工作目录内的相对文件路径"}}, Validate: func(rt *shortcut.RuntimeContext) error { return validateWorkspaceInputPath("file", rt.Str("file")) }, Execute: executeMediaUpload, }
var Move = shortcut.Shortcut{ Service: "doc", Command: "+move", Product: productDoc, Description: "移动文档/文件到指定文件夹或知识库", Intent: "当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_move", CanonicalPath: "doc.shortcut_move", CLIPath: "doc +move", PrimaryCLIPath: "doc +move", }, Description: "移动文档/文件到指定文件夹或知识库", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "移动文档/文件到指定文件夹或知识库", UseWhen: []string{"当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档/文件 ID 或 URL", Required: true}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文档文件夹 nodeId 或 alidocs 文件夹 URL"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "目标知识库 ID"}, }, Tips: []string{`dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node")} if rt.Changed("folder") { params["targetFolderId"] = rt.Str("folder") } if rt.Changed("workspace") { params["workspaceId"] = rt.Str("workspace") } return rt.CallMCP("move_document", params) }, }
var ResourceDelete = shortcut.Shortcut{ Service: "doc", Command: "+resource-delete", Product: productDoc, Description: "幂等清除文档封面", Intent: "当用户明确要移除文档当前封面时使用;发送 cover clear,重复执行保持无封面状态。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "idempotent"}, Contract: docContract("+resource-delete", "幂等清除文档封面", "当用户明确要移除文档当前封面时使用;发送 cover clear,重复执行保持无封面状态。", []string{`dws doc +resource-delete --node <DOC_ID>`}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}}, Tips: []string{`dws doc +resource-delete --node <DOC_ID>`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node"), "cover": map[string]any{"action": "clear"}} if rt.DryRun() { return rt.Output(docEnvelope("doc.resource_delete", map[string]any{"executed": false, "params": params})) } return rt.CallMCP("update_document_style", params) }, }
var ResourceDownload = shortcut.Shortcut{ Service: "doc", Command: "+resource-download", Product: productDoc, Description: "读取并安全下载当前文档封面", Intent: "当用户要把当前文档封面保存到本地时使用;先读 style,必要时用 resourceId 换临时链接,再按安全本地下载策略保存;默认拒绝覆盖,覆盖已有文件用 +download-overwrite。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+resource-download", "读取并安全下载当前文档封面", "当用户要把当前文档封面保存到本地时使用;先读 style,必要时用 resourceId 换临时链接,再按安全本地下载策略保存;默认拒绝覆盖,覆盖已有文件用 +download-overwrite。", []string{`dws doc +resource-download --node <DOC_ID> --output ./cover.png`}), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "output", Type: shortcut.FlagString, Default: ".", Desc: "工作目录内相对路径(文件或目录)"}, }, Validate: func(rt *shortcut.RuntimeContext) error { return localio.ValidateOutput(rt.Str("output")) }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"output"}, Description: "--output 必须是工作目录内相对路径;默认 no-clobber"}}, Tips: []string{`dws doc +resource-download --node <DOC_ID> --output ./cover.png`}, Execute: executeResourceDownload, }
var ResourceUpdate = shortcut.Shortcut{ Service: "doc", Command: "+resource-update", Product: productDoc, Description: "从本地图片或 HTTPS URL 设置文档封面", Intent: "当用户要设置或替换文档顶部封面图时使用;本地图片会先上传,HTTPS URL 由服务端转存。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "idempotent"}, Contract: withDryRun(docContract("+resource-update", "从本地图片或 HTTPS URL 设置文档封面", "当用户要设置或替换文档顶部封面图时使用;本地图片会先上传,HTTPS URL 由服务端转存。", []string{`dws doc +resource-update --node <DOC_ID> --image https://example.com/cover.png`, `dws doc +resource-update --node <DOC_ID> --file ./cover.png`}), contract.DryRunPreviewRequest, false), Flags: []shortcut.Flag{ {Name: "from-clipboard", Type: shortcut.FlagBool, Desc: "读取系统剪贴板PNG,与本地file及image互斥"}, {Name: "position", Type: shortcut.FlagString, Default: "0.5", Desc: "封面竖直位置[0,1],0顶部、1底部;沿用原子接口坐标"}, {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "image", Type: shortcut.FlagString, Desc: "HTTPS 封面图片 URL"}, {Name: "file", Type: shortcut.FlagString, Desc: "工作目录内已存在封面图片的相对路径"}, }, Validate: validateDocImageInput, Constraints: []shortcut.Constraint{ {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"image", "file", "from-clipboard"}, Description: "image/file/from-clipboard 必须且只能提供一个"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"file"}, Description: "提供 --file 时必须是工作目录内已存在且不通过符号链接逃逸的相对路径"}, }, Tips: []string{`dws doc +resource-update --node <DOC_ID> --image https://example.com/cover.png`, `dws doc +resource-update --node <DOC_ID> --file ./cover.png`}, Execute: func(rt *shortcut.RuntimeContext) error { return withDocClipboard(rt, func() error { return helpers.RunDocResourceUpdateShortcut(rt.Command()) }) }, }
var Review = shortcut.Shortcut{ Service: "doc", Command: "+review", Product: productComment, Description: "聚合未解决评论、引用原文和块上下文", Intent: "当用户要确定性查看一篇文档仍待处理的 review 意见时使用;聚合 unresolved 评论与 block 上下文,不调用模型生成总结。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"}, Contract: docContract("+review", "聚合未解决评论、引用原文和块上下文", "当用户要确定性查看一篇文档仍待处理的 review 意见时使用;聚合 unresolved 评论与 block 上下文,不调用模型生成总结。", []string{`dws doc +review --node <DOC_ID>`}), Flags: []shortcut.Flag{{Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}}, Tips: []string{`dws doc +review --node <DOC_ID>`}, Execute: func(rt *shortcut.RuntimeContext) error { node := rt.Str("node") comments, err := rt.CallMCPData(productComment, "list_comments", map[string]any{"nodeId": node, "resolveStatus": "unresolved"}) if err != nil { return err } blocks, err := rt.CallMCPData(productDoc, "list_document_blocks", map[string]any{"nodeId": node, "format": "element"}) if err != nil { return err } items := projectReviewComments(comments, blocks) global, inline := 0, 0 for _, item := range items { if item["blockId"] == "" { global++ } else { inline++ } } return rt.Output(map[string]any{"status": "unresolved", "counts": map[string]any{"total": len(items), "global": global, "inline": inline}, "comments": items}) }, }
var Script = shortcut.Shortcut{ Service: "doc", Command: "+script", Product: productDoc, Description: "初始化本地文档草稿,或检查Markdown/JSONML结构和字数", Intent: docScriptIntent, Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "low", Confirmation: "not_required", Idempotency: "unknown"}, OutputRollout: output.RolloutUnifiedActive, Contract: scriptContract(), Flags: []shortcut.Flag{ {Name: "command", Type: shortcut.FlagString, Required: true, Enum: []string{"init-draft", "parse"}, Desc: "本地动作;init-draft创建独立目录,parse只检查内容;parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}, {Name: "content", Type: shortcut.FlagString, Desc: docContentInputDescription + ";parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}, {Name: "node", Type: shortcut.FlagString, Desc: "parse可读取的文档ID/URL;与content互斥;parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}, {Name: "doc-format", Type: shortcut.FlagString, Default: "markdown", Enum: []string{"markdown", "jsonml"}, Desc: "草稿或解析内容格式;不支持DocxXML"}, {Name: "min-words", Type: shortcut.FlagInt, Desc: "最少字数,0不限制;汉字逐字、其他字母数字连续词计数;parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}, {Name: "max-words", Type: shortcut.FlagInt, Desc: "最多字数,0不限制;parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}, {Name: "required-blocks", Type: shortcut.FlagStringSlice, Desc: "要求至少出现一次的块类型,如heading,paragraph,table(Markdown)或h1,p,table(JSONML)"}, }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"command", "content", "node", "min-words", "max-words"}, Description: "parse恰好提供content或node;init-draft不接受二者;字数上下界非负且顺序正确"}}, Validate: validateDocScript, Execute: executeDocScript, }
var Search = shortcut.Shortcut{ Service: "doc", Command: "+search", Product: productDoc, Description: "按关键词或过滤条件搜索有权限的文档;默认只读取一页", Intent: "按标题、主题词或属性查找待阅读编辑的在线文档时使用,文字文档可指定 --extensions adoc。默认一页;完整候选、唯一或不存在判断使用 --page-all,前N条使用 --limit N。--folder仅在搜索条件之外限制文档文件夹直接成员,需完整读取搜索候选及目录两组分页,受各自上限约束;不是目录浏览入口。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_search", CanonicalPath: "doc.shortcut_search", CLIPath: "doc +search", PrimaryCLIPath: "doc +search", }, Description: "按关键词或过滤条件搜索有权限的文档;默认只读取一页", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "按关键词或过滤条件搜索有权限的文档;默认只读取一页", UseWhen: []string{"按标题、主题词或属性查找待阅读编辑的在线文档时使用,文字文档可指定 --extensions adoc。默认一页;完整候选、唯一或不存在判断使用 --page-all,前N条使用 --limit N。--folder仅在搜索条件之外限制文档文件夹直接成员,需完整读取搜索候选及目录两组分页,受各自上限约束;不是目录浏览入口。"}, AvoidWhen: []string{ "已经取得稳定 nodeId/URL 时直接使用目标读取或写入命令,不要再次按标题搜索", "最近访问或最近编辑列表使用 drive +recent,不要用无关键词搜索替代", "仅浏览文档文件夹直接子项使用 doc +list;钉盘目录使用 drive +list,钉盘文件搜索使用 drive +search;不要重复调用目录列表和带folder的搜索来完成同一次目录浏览", "只读取前 N 条匹配结果时不要为了 Top-N 无条件翻完整个数据源", }, Examples: []string{ "dws doc +search --query \"会议纪要\" --page-all --max-pages 20", "dws doc +search --query \"周报\" --limit 10", }, }, }, Flags: []shortcut.Flag{ {Name: "folder", Type: shortcut.FlagString, Desc: "在搜索条件外限制文档文件夹直接成员(不递归);要求page-all,两组分页分别读取完整搜索候选与目录;仅列目录用doc +list"}, {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词;不传仍兼容返回默认结果页,最近访问/编辑应使用 drive +recent"}, {Name: "extensions", Type: shortcut.FlagStringSlice, Desc: "按文件扩展名过滤 (如 adoc,axls,pdf)"}, {Name: "created-after", Type: shortcut.FlagString, Desc: "创建起点:RFC3339带时区或YYYY-MM-DD(UTC);与created-from互斥"}, {Name: "created-before", Type: shortcut.FlagString, Desc: "创建终点:RFC3339带时区或YYYY-MM-DD(UTC);与created-to互斥"}, {Name: "visited-after", Type: shortcut.FlagString, Desc: "访问起点:RFC3339带时区或YYYY-MM-DD(UTC);与visited-from互斥"}, {Name: "visited-before", Type: shortcut.FlagString, Desc: "访问终点:RFC3339带时区或YYYY-MM-DD(UTC);与visited-to互斥"}, {Name: "with-metadata", Type: shortcut.FlagBool, Desc: "在每条结果metadata中保留下游原始字段;不改变默认精简结果"}, {Name: "created-from", Type: shortcut.FlagInt, Desc: "创建时间起始 (毫秒时间戳)"}, {Name: "created-to", Type: shortcut.FlagInt, Desc: "创建时间截止 (毫秒时间戳)"}, {Name: "visited-from", Type: shortcut.FlagInt, Desc: "访问时间起始 (毫秒时间戳)"}, {Name: "visited-to", Type: shortcut.FlagInt, Desc: "访问时间截止 (毫秒时间戳)"}, {Name: "creator-uids", Type: shortcut.FlagStringSlice, Desc: "按创建者用户 ID 过滤"}, {Name: "editor-uids", Type: shortcut.FlagStringSlice, Desc: "按编辑者用户 ID 过滤"}, {Name: "mentioned-uids", Type: shortcut.FlagStringSlice, Desc: "按 @提及的用户 ID 过滤"}, {Name: "workspace-ids", Type: shortcut.FlagStringSlice, Desc: "按知识库 ID 过滤"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量 (默认 10,最大 30)"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标 (上次结果的 nextPageToken)"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "有界读取全部后续页;--max-pages/--max-items 仅在 --page-all 时生效且必须大于 0"}, {Name: "max-pages", Type: shortcut.FlagInt, Default: "20", Desc: "--max-pages 仅在 --page-all 时生效,且必须大于 0"}, {Name: "max-items", Type: shortcut.FlagInt, Default: "500", Desc: "--page-all 最多返回文档数,必须大于 0"}, }, Constraints: docAutoPaginationConstraints(), Tips: []string{`dws doc +search --query "会议纪要" --page-all --max-pages 20`, `dws doc +search --query "周报" --limit 10`}, Validate: validateDocSearch, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{} if v := rt.Str("query"); v != "" { params["keyword"] = v } if rt.Changed("extensions") { params["extensions"] = rt.StrSlice("extensions") } if rt.Changed("created-from") { params["createdTimeFrom"] = rt.Int("created-from") } if rt.Changed("created-to") { params["createdTimeTo"] = rt.Int("created-to") } if rt.Changed("visited-from") { params["visitedTimeFrom"] = rt.Int("visited-from") } if rt.Changed("visited-to") { params["visitedTimeTo"] = rt.Int("visited-to") } if rt.Changed("creator-uids") { params["creatorUserIds"] = rt.StrSlice("creator-uids") } if rt.Changed("editor-uids") { params["editorUserIds"] = rt.StrSlice("editor-uids") } if rt.Changed("mentioned-uids") { params["mentionedUserIds"] = rt.StrSlice("mentioned-uids") } if rt.Changed("workspace-ids") { params["workspaceIds"] = rt.StrSlice("workspace-ids") } project := searchDocsProject if rt.Bool("with-metadata") { project = searchDocsProjectWithMetadata } pageSize := rt.Int("limit") if pageSize == 0 { pageSize = 10 } result, err := collectDocPages(rt, "search_documents", "documents", params, project, docPageOptions{ PageAll: rt.Bool("page-all"), PageSize: pageSize, MaxPages: rt.Int("max-pages"), MaxItems: rt.Int("max-items"), Cursor: rt.Str("cursor"), }) if err != nil { return err } if rt.Changed("folder") { result, err = filterDocSearchFolder(rt, result) if err != nil { return err } } return rt.Output(result) }, }
var TemplateApply = shortcut.Shortcut{ Service: "doc", Command: "+template-apply", Product: productDoc, Description: "使用指定模板创建新文档", Intent: "当你已选定某个模板、想据此快速生成一篇带预设结构的新文档时使用;输入 template-id(可选 name/folder/workspace),会实际按模板创建一篇新文档并返回其 ID。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "template-id", Type: shortcut.FlagString, Desc: "模板 ID", Required: true}, {Name: "name", Type: shortcut.FlagString, Desc: "新文档名称 (可选)"}, {Name: "folder", Type: shortcut.FlagString, Desc: "目标文件夹 ID (可选)"}, {Name: "workspace", Type: shortcut.FlagString, Desc: "知识库 ID (可选)"}, }, Tips: []string{`dws doc +template-apply --template-id TPL_ID --name "我的周报"`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"templateId": rt.Str("template-id")} if v := rt.Str("name"); v != "" { params["name"] = v } if rt.Changed("folder") { params["folderId"] = rt.Str("folder") } if rt.Changed("workspace") { params["workspaceId"] = rt.Str("workspace") } return rt.CallMCP("apply_doc_template", params) }, }
var TemplateList = shortcut.Shortcut{ Service: "doc", Command: "+template-list", Product: productDoc, Description: "浏览可用文档模板;默认只读取一页", Intent: "当你没有明确模板名称或关键词、需要浏览自己的 MY 或公共 PUBLIC 模板并获取 templateId 时使用;默认只读取一页,要求全部模板或完整浏览时必须使用 --page-all。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_template_list", CanonicalPath: "doc.shortcut_template_list", CLIPath: "doc +template-list", PrimaryCLIPath: "doc +template-list", }, Description: "浏览可用文档模板;默认只读取一页", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "浏览 MY/PUBLIC 文档模板;完整浏览必须显式自动翻页", UseWhen: []string{"没有明确模板名称或关键词、需要浏览自己的或公开模板时使用;要求全部模板或完整模板库时使用 --page-all"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{ "dws doc +template-list --source PUBLIC", "dws doc +template-list --source PUBLIC --page-all --max-pages 20", }, }, }, Flags: []shortcut.Flag{ {Name: "source", Type: shortcut.FlagString, Desc: "模板来源: MY / PUBLIC (默认 MY)", Enum: []string{"MY", "PUBLIC"}}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页数量(默认 20)"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"}, {Name: "page-all", Type: shortcut.FlagBool, Desc: "有界读取全部后续页;--max-pages/--max-items 仅在 --page-all 时生效且必须大于 0"}, {Name: "max-pages", Type: shortcut.FlagInt, Default: "20", Desc: "--max-pages 仅在 --page-all 时生效,且必须大于 0"}, {Name: "max-items", Type: shortcut.FlagInt, Default: "500", Desc: "--page-all 最多返回模板数,必须大于 0"}, }, Constraints: docAutoPaginationConstraints(), Tips: []string{`dws doc +template-list --source PUBLIC`, `dws doc +template-list --source PUBLIC --page-all --max-pages 20`}, Validate: validateDocAutoPagination, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{} if v := rt.Str("source"); v != "" { params["templateSource"] = v } pageSize := rt.Int("limit") if pageSize <= 0 { pageSize = 20 } result, err := collectDocPages(rt, "list_doc_templates", "templates", params, func(data map[string]any) []map[string]any { return collectTemplateCandidates(data) }, docPageOptions{ PageAll: rt.Bool("page-all"), PageSize: pageSize, MaxPages: rt.Int("max-pages"), MaxItems: rt.Int("max-items"), Cursor: rt.Str("cursor"), PageSizeParam: "maxResults", CursorParam: "nextCursor", }) if err != nil { return err } return rt.Output(result) }, }
var TemplateSearch = shortcut.Shortcut{ Service: "doc", Command: "+template-search", Product: productDoc, Description: "根据关键词搜索文档模板", Intent: "当模板较多、你想按关键词(如“周报”“合同”)快速找到合适的模板并拿到 templateId 时使用;输入 query,返回匹配的模板列表,随后可配合 +template-apply 套用。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_template_search", CanonicalPath: "doc.shortcut_template_search", CLIPath: "doc +template-search", PrimaryCLIPath: "doc +template-search", }, Description: "根据关键词搜索文档模板", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "根据关键词搜索文档模板", UseWhen: []string{"当模板较多、你想按关键词(如“周报”“合同”)快速找到合适的模板并拿到 templateId 时使用;输入 query,返回匹配的模板列表,随后可配合 +template-apply 套用。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +template-search --query \"周报\""}, }, }, Flags: []shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词", Required: true}, {Name: "source", Type: shortcut.FlagString, Desc: "模板来源: MY / PUBLIC (默认 MY)", Enum: []string{"MY", "PUBLIC"}}, {Name: "limit", Type: shortcut.FlagInt, Desc: "返回数量上限"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"}, }, Tips: []string{`dws doc +template-search --query "周报"`}, Execute: func(rt *shortcut.RuntimeContext) error { pageSize := rt.Int("limit") if pageSize <= 0 { pageSize = 50 } params := map[string]any{"searchName": rt.Str("query"), "maxResults": pageSize} if v := rt.Str("source"); v != "" { params["templateSource"] = v } if v := rt.Str("cursor"); v != "" { params["nextCursor"] = v } found, err := rt.CallMCPData(productDoc, "search_doc_templates", params) if err != nil { return err } candidates := collectTemplateCandidates(found) hasMore, hasMoreKnown, nextCursor := docPageState(found) nextCursor = strings.TrimSpace(nextCursor) complete := hasMoreKnown && !hasMore if !hasMoreKnown && nextCursor == "" && len(candidates) < pageSize { complete = true } globalComplete := complete && rt.Str("cursor") == "" status := "selection_required" if globalComplete && len(candidates) == 0 { status = "not_found" } else if globalComplete && len(candidates) == 1 { status = "resolved" } selectedTemplateID := "" if status == "resolved" { selectedTemplateID = candidates[0]["templateId"].(string) } nextAction := map[string]string{"resolved": "create_once", "not_found": "stop", "selection_required": "ask_user"}[status] if !complete { nextAction = "continue_search" } return rt.Output(docEnvelope("doc.template_search", map[string]any{ "query": rt.Str("query"), "source": rt.Str("source"), "count": len(candidates), "candidates": candidates, "complete": complete, "hasMore": !complete, "nextCursor": nextCursor, "selection": map[string]any{ "status": status, "templateId": selectedTemplateID, "nextAction": nextAction, }, })) }, }
var Update = shortcut.Shortcut{ Service: "doc", Command: "+update", Product: productDoc, Description: "追加、覆盖或按 block 精确更新文档内容", Intent: "当用户要修改已有在线文字文档时使用;支持整篇 append/overwrite、在参考 block 前后插入段落或标题、block 替换/删除,以及受限的唯一纯文本 str_replace,所有模式统一经过静态确认门禁。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown"}, Contract: docContract("+update", "追加、覆盖或按 block 精确更新文档内容", "当用户要修改已有在线文字文档时使用;支持整篇 append/overwrite、在参考 block 前后插入段落或标题、block 替换/删除,以及受限的唯一纯文本 str_replace,所有模式统一经过静态确认门禁。", []string{`dws doc +update --node <DOC_ID> --command append --content "补充说明"`, `dws doc +update --node <DOC_ID> --command block_insert_before --before-block-id <BLOCK_ID> --content "发布说明" --heading-level 1`}, contract.ParamDecl{Name: "node", Property: "node"}, contract.ParamDecl{Name: "command", Property: "command"}, contract.ParamDecl{Name: "content", Property: "content"}, contract.ParamDecl{Name: "doc-format", Property: "docFormat"}, contract.ParamDecl{Name: "block-id", Property: "blockId"}, contract.ParamDecl{Name: "after-block-id", Property: "afterBlockId"}, contract.ParamDecl{Name: "before-block-id", Property: "beforeBlockId"}, contract.ParamDecl{Name: "heading-level", Property: "headingLevel"}, contract.ParamDecl{Name: "old", Property: "old"}, contract.ParamDecl{Name: "new", Property: "new"}, contract.ParamDecl{Name: "expected-revision", Property: "expectedRevision"}, contract.ParamDecl{Name: "start-block-id", Property: "startBlockId"}, contract.ParamDecl{Name: "end-block-id", Property: "endBlockId"}, contract.ParamDecl{Name: "src-block-ids", Property: "srcBlockIds"}, contract.ParamDecl{Name: "doc", Property: "node"}, contract.ParamDecl{Name: "text", Property: "content"}), Flags: []shortcut.Flag{ {Name: "src-block-ids", Type: shortcut.FlagString, Desc: "仅block_copy_insert_after可用的逗号分隔源ID;与block-id同时提供须相同"}, {Name: "start-block-id", Type: shortcut.FlagString, Desc: "block_delete/block_replace顶层范围起点;与end-block-id成对,0表示第一块"}, {Name: "end-block-id", Type: shortcut.FlagString, Desc: "block_delete/block_replace顶层范围终点(含),-1表示最后块;最多50块"}, {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true, Aliases: []string{"doc"}, AliasesVisible: true}, {Name: "command", Type: shortcut.FlagString, Desc: "更新动作;不能为空", Enum: []string{"append", "overwrite", "block_insert_before", "block_insert_after", "block_replace", "block_delete", "str_replace", "block_copy_insert_after"}}, {Name: "content", Type: shortcut.FlagString, Desc: docRequiredContentInputDescription, Aliases: []string{"text"}, AliasesVisible: true}, {Name: "doc-format", Type: shortcut.FlagString, Default: "markdown", Desc: "内容格式", Enum: []string{"markdown", "jsonml"}}, {Name: "block-id", Type: shortcut.FlagString, Desc: "目标或源 block ID;相关动作要求时不能为空;block_delete 支持逗号分隔批量 ID,单次最多 50 个;block_copy_insert_after支持同文档顶层有序多源ID,最多20个,不支持含资源引用的块"}, {Name: "after-block-id", Type: shortcut.FlagString, Desc: "插入位置参考 block ID;相关动作要求时不能为空"}, {Name: "before-block-id", Type: shortcut.FlagString, Desc: "向前插入时的位置参考 block ID;block_insert_before 要求不能为空"}, {Name: "heading-level", Type: shortcut.FlagInt, Desc: "将插入内容写为指定级别标题(1-6);仅支持 Markdown block_insert_before/block_insert_after"}, {Name: "old", Type: shortcut.FlagString, Desc: "str_replace 原文字,不能为空"}, {Name: "new", Type: shortcut.FlagString, Desc: "str_replace 新文字;--old 不能为空,新值可为空但参数必须显式提供"}, {Name: "expected-revision", Type: shortcut.FlagInt, Desc: "仅 overwrite+jsonml:传给服务端执行原子 revision 条件写"}, }, Tips: []string{`dws doc +update --node <DOC_ID> --command append --content "补充说明"`, `dws doc +update --node <DOC_ID> --command block_insert_before --before-block-id <BLOCK_ID> --content "发布说明" --heading-level 1`}, Validate: func(rt *shortcut.RuntimeContext) error { if rt.Changed("src-block-ids") { if rt.Str("command") != "block_copy_insert_after" || strings.TrimSpace(rt.Str("src-block-ids")) == "" { return apperrors.NewValidation("src-block-ids仅用于block_copy_insert_after且不能为空") } if rt.Changed("block-id") && rt.Str("block-id") != rt.Str("src-block-ids") { return apperrors.NewValidation("src-block-ids与block-id冲突") } if err := rt.Command().Flags().Set("block-id", rt.Str("src-block-ids")); err != nil { return err } } if err := validateDocBlockRange(rt); err != nil { return err } command := rt.Str("command") if rt.StrFirst("node", "doc") == "" { return apperrors.NewValidation("缺少 --node") } if command == "" { return apperrors.NewValidation("缺少 --command") } switch command { case "append", "overwrite", "block_insert_before", "block_insert_after", "block_replace": if rt.StrFirst("content", "text") == "" { return apperrors.NewValidation("该更新动作的 --content 不能为空") } } switch command { case "block_replace", "block_delete", "block_copy_insert_after": if rt.Str("block-id") == "" && !((command == "block_delete" || command == "block_replace") && rt.Str("start-block-id") != "") { return apperrors.NewValidation("该 block 操作必须提供 --block-id") } } switch command { case "block_insert_after", "block_copy_insert_after": if rt.Str("after-block-id") == "" { return apperrors.NewValidation("该 block 操作必须提供 --after-block-id") } } if command == "block_insert_before" && rt.Str("before-block-id") == "" { return apperrors.NewValidation("--command block_insert_before 必须提供 --before-block-id") } if rt.Changed("heading-level") { level := rt.Int("heading-level") if command != "block_insert_before" && command != "block_insert_after" { return apperrors.NewValidation("--heading-level 仅支持 block_insert_before/block_insert_after") } if rt.Str("doc-format") != "markdown" { return apperrors.NewValidation("--heading-level 仅支持 --doc-format markdown") } if level < 1 || level > 6 { return apperrors.NewValidation("--heading-level 必须在 1-6 之间") } } if command == "str_replace" && (rt.Str("old") == "" || !rt.Changed("new")) { return apperrors.NewValidation("--command str_replace 必须同时提供 --old 和 --new") } if command == "append" && rt.Str("doc-format") == "jsonml" { return apperrors.NewValidation("JSONML 当前不支持 append") } if rt.Changed("expected-revision") && (command != "overwrite" || rt.Str("doc-format") != "jsonml") { return apperrors.NewValidation("--expected-revision 仅支持 --command overwrite --doc-format jsonml;其他写入接口没有服务端原子 revision 契约") } return nil }, Constraints: []shortcut.Constraint{{Kind: shortcut.ConstraintCustom, Flags: []string{"src-block-ids"}, Description: "仅block_copy_insert_after可用的逗号分隔源ID;与block-id同时提供须相同"}, {Kind: shortcut.ConstraintCustom, Flags: []string{"command", "content", "block-id", "after-block-id", "before-block-id", "old", "new"}, Description: "依 command 校验,所需文本或 block 参数不能为空"}}, Execute: executeUpdate, }
var VersionList = shortcut.Shortcut{ Service: "doc", Command: "+version-list", Product: productDoc, Description: "查看文档历史版本列表", Intent: "当你想查看某篇文档有哪些历史版本、以便挑一个版本号用于回滚时使用;输入 node,返回历史版本列表及其版本号。", Risk: shortcut.RiskRead, Safety: contract.SafetySpec{ Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_version_list", CanonicalPath: "doc.shortcut_version_list", CLIPath: "doc +version-list", PrimaryCLIPath: "doc +version-list", }, Description: "查看文档历史版本列表", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "查看文档历史版本列表", UseWhen: []string{"当你想查看某篇文档有哪些历史版本、以便挑一个版本号用于回滚时使用;输入 node,返回历史版本列表及其版本号。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +version-list --node DOC_ID"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "limit", Type: shortcut.FlagInt, Desc: "返回版本数量上限"}, {Name: "cursor", Type: shortcut.FlagString, Desc: "分页游标"}, }, Tips: []string{`dws doc +version-list --node DOC_ID`}, Execute: func(rt *shortcut.RuntimeContext) error { params := map[string]any{"nodeId": rt.Str("node")} if rt.Changed("limit") { params["maxResults"] = rt.Int("limit") } if v := rt.Str("cursor"); v != "" { params["nextCursor"] = v } return rt.CallMCP("list_doc_versions", params) }, }
var VersionRevert = shortcut.Shortcut{ Service: "doc", Command: "+version-revert", Product: productDoc, Description: "回滚文档到指定历史版本", Intent: "当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。", Risk: shortcut.RiskHighWrite, Safety: contract.SafetySpec{ Effect: "destructive", Risk: "high", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_version_revert", CanonicalPath: "doc.shortcut_version_revert", CLIPath: "doc +version-revert", PrimaryCLIPath: "doc +version-revert", }, Description: "回滚文档到指定历史版本", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "回滚文档到指定历史版本", UseWhen: []string{"当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +version-revert --node DOC_ID --version 3"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, {Name: "version", Type: shortcut.FlagInt, Desc: "目标版本号 (从 +version-list 获取)", Required: true}, }, Tips: []string{`dws doc +version-revert --node DOC_ID --version 3 --yes`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("revert_doc_version", map[string]any{ "nodeId": rt.Str("node"), "version": rt.Int("version"), }) }, }
var VersionSave = shortcut.Shortcut{ Service: "doc", Command: "+version-save", Product: productDoc, Description: "手动保存文档版本快照", Intent: "当你在做重大改动前后、想手动打一个可回滚的版本存档点时使用;输入 node,会实际为该文档保存一个当前内容的历史版本快照。", Risk: shortcut.RiskWrite, Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", Confirmation: "user_required", Idempotency: "unknown", }, Contract: corecmd.ContractDecl{ Identity: contract.ToolIdentitySpec{ ProductID: "doc", Name: "shortcut_version_save", CanonicalPath: "doc.shortcut_version_save", CLIPath: "doc +version-save", PrimaryCLIPath: "doc +version-save", }, Description: "手动保存文档版本快照", Interface: &contract.InterfaceSpec{ Mode: "composite", Availability: "available", Reason: "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", }, Selection: contract.SelectionSpec{ AgentSummary: "手动保存文档版本快照", UseWhen: []string{"当你在做重大改动前后、想手动打一个可回滚的版本存档点时使用;输入 node,会实际为该文档保存一个当前内容的历史版本快照。"}, AvoidWhen: []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"}, Examples: []string{"dws doc +version-save --node DOC_ID"}, }, }, Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true}, }, Tips: []string{`dws doc +version-save --node DOC_ID`}, Execute: func(rt *shortcut.RuntimeContext) error { return rt.CallMCP("save_doc_version", map[string]any{"nodeId": rt.Str("node")}) }, }
Functions ¶
This section is empty.
Types ¶
This section is empty.