doc

package
v1.0.61 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

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).

Index

Constants

This section is empty.

Variables

View Source
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)
	},
}
View Source
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")}})
	},
}
View Source
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()
		}
		return rt.Output(withDocWarnings(docEnvelope("doc.checkpoint_update", data, steps...), chunkPlan.Warnings()))
	},
}
View Source
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,
}
View Source
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,
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
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: "新文档名称", Required: true},
		{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 {
		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
			}
		}
		params := map[string]any{"name": rt.Str("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)}
			if len(contentChunks) > 1 {

				preview["chunkPlan"] = chunkPlan.Summary()
			}
			return rt.Output(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()
		}
		return rt.Output(withDocWarnings(docEnvelope("doc.create", data, steps...), chunkPlan.Warnings()))
	},
}
View Source
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"}))
	},
}
View Source
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,
}
View Source
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,
}
View Source
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"}))
	},
}
View Source
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: "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"}},
		{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 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") == "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" {
			params["scope"] = scope
		}
		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"] = 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 == "keyword" {
			content = projectKeywordMatches(data, rt.Str("keyword"), rt.Int("context-before"), rt.Int("context-after"))
		}
		return rt.Output(map[string]any{
			"contractVersion": "doc.content.v1",
			"status":          "success",
			"complete":        true,
			"target":          target.Selected,
			"content":         content,
		})
	},
}
View Source
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
	},
}
View Source
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...))
	},
}
View Source
var List = shortcut.Shortcut{
	Service:     "doc",
	Command:     "+list",
	Product:     productDoc,
	Description: "列出文件夹或知识库下的直接子节点",
	Intent:      "当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。",
	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{"当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。"},
			AvoidWhen:    []string{"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"},
			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)
	},
}
View Source
var MediaDownload = shortcut.Shortcut{
	Service: "doc", Command: "+media-download", Product: productDoc,
	Description: "安全下载文档正文附件到工作目录",
	Intent:      "当用户已从 +media-list 拿到真实 resourceId,要把正文附件保存到工作目录时使用;CLI 内部换取临时链接并原子下载,默认拒绝覆盖。",
	Risk:        shortcut.RiskRead,
	Safety:      contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"},
	Contract: docContract("+media-download", "安全下载文档正文附件到工作目录",
		"当用户已从 +media-list 拿到真实 resourceId,要把正文附件保存到工作目录时使用;CLI 内部换取临时链接并原子下载,默认拒绝覆盖。",
		[]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,
}
View Source
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: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
		{Name: "file", Type: shortcut.FlagString, Desc: "工作目录内已存在的相对文件路径", Required: true},
		{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:    func(rt *shortcut.RuntimeContext) error { return validateWorkspaceInputPath("file", rt.Str("file")) },
	Constraints: []shortcut.Constraint{{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 helpers.RunDocMediaInsertShortcut(rt.Command()) },
}
View Source
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}))
	},
}
View Source
var MediaPreview = shortcut.Shortcut{
	Service: "doc", Command: "+media-preview", Product: productDoc,
	Description: "下载正文媒体到受控临时目录并返回预览路径",
	Intent:      "当用户要临时查看文档附件或图片内容而不指定持久保存路径时使用;下载到独立临时目录并返回 artifact 路径。",
	Risk:        shortcut.RiskRead,
	Safety:      contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"},
	Contract: docContract("+media-preview", "下载正文媒体到受控临时目录并返回预览路径",
		"当用户要临时查看文档附件或图片内容而不指定持久保存路径时使用;下载到独立临时目录并返回 artifact 路径。",
		[]string{`dws doc +media-preview --node <DOC_ID> --resource-id <RESOURCE_ID>`}),
	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},
	},
	Validate:    func(rt *shortcut.RuntimeContext) error { return validateDocResourceID(rt.Str("resource-id")) },
	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.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}))
	},
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
var ResourceDownload = shortcut.Shortcut{
	Service: "doc", Command: "+resource-download", Product: productDoc,
	Description: "读取并安全下载当前文档封面",
	Intent:      "当用户要把当前文档封面保存到本地时使用;先读 style,必要时用 resourceId 换临时链接,再按安全本地下载策略保存。",
	Risk:        shortcut.RiskRead,
	Safety:      contract.SafetySpec{Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent"},
	Contract: docContract("+resource-download", "读取并安全下载当前文档封面",
		"当用户要把当前文档封面保存到本地时使用;先读 style,必要时用 resourceId 换临时链接,再按安全本地下载策略保存。",
		[]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,
}
View Source
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: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL", Required: true},
		{Name: "image", Type: shortcut.FlagString, Desc: "HTTPS 封面图片 URL"},
		{Name: "file", Type: shortcut.FlagString, Desc: "工作目录内已存在封面图片的相对路径"},
	},
	Validate: func(rt *shortcut.RuntimeContext) error {
		if rt.Str("file") == "" {
			return nil
		}
		return validateWorkspaceInputPath("file", rt.Str("file"))
	},
	Constraints: []shortcut.Constraint{
		{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"image", "file"}, Description: "--image 与 --file 必须且只能提供一个"},
		{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 helpers.RunDocResourceUpdateShortcut(rt.Command()) },
}
View Source
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})
	},
}
View Source
var Search = shortcut.Shortcut{
	Service:     "doc",
	Command:     "+search",
	Product:     productDoc,
	Description: "按关键词或过滤条件搜索有权限的文档;默认只读取一页",
	Intent:      "当你需要按标题、主题词或过滤条件查找文档时使用;默认只读取一页。要求全部匹配、完整候选、判断唯一或不存在时必须使用 --page-all;只要前 N 条匹配结果时使用 --limit N。最近访问或最近编辑列表改用 drive +recent。",
	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{"当你需要按标题、主题词或过滤条件查找文档时使用;默认只读取一页。要求全部匹配、完整候选、判断唯一或不存在时必须使用 --page-all;只要前 N 条匹配结果时使用 --limit N。最近访问或最近编辑列表改用 drive +recent。"},
			AvoidWhen: []string{
				"已经取得稳定 nodeId/URL 时直接使用目标读取或写入命令,不要再次按标题搜索",
				"最近访问或最近编辑列表使用 drive +recent,不要用无关键词搜索替代",
				"只读取前 N 条匹配结果时不要为了 Top-N 无条件翻完整个数据源",
			},
			Examples: []string{
				"dws doc +search --query \"会议纪要\" --page-all --max-pages 20",
				"dws doc +search --query \"周报\" --limit 10",
			},
		},
	},
	Flags: []shortcut.Flag{
		{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词;不传仍兼容返回默认结果页,最近访问/编辑应使用 drive +recent"},
		{Name: "extensions", Type: shortcut.FlagStringSlice, Desc: "按文件扩展名过滤 (如 adoc,axls,pdf)"},
		{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:    validateDocAutoPagination,
	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")
		}
		pageSize := rt.Int("limit")
		if pageSize == 0 {
			pageSize = 10
		}
		result, err := collectDocPages(rt, "search_documents", "documents", params, searchDocsProject, 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)
	},
}
View Source
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)
	},
}
View Source
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)
	},
}
View Source
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,
			},
		}))
	},
}
View Source
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: "doc", Property: "node"},
		contract.ParamDecl{Name: "text", Property: "content"}),
	Flags: []shortcut.Flag{
		{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;相关动作要求时不能为空"},
		{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 {
		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") == "" {
				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{"command", "content", "block-id", "after-block-id", "before-block-id", "old", "new"}, Description: "依 command 校验,所需文本或 block 参数不能为空"}},
	Execute:     executeUpdate,
}
View Source
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)
	},
}
View Source
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"),
		})
	},
}
View Source
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL