From 9798a6072832c9344fc8d3f80ff86b16156f3068 Mon Sep 17 00:00:00 2001 From: liyuan Date: Thu, 20 Aug 2026 17:13:46 +0800 Subject: [PATCH 1/4] feat(doc): read password-protected public docs and historical versions --- CHANGELOG.md | 11 ++ internal/cli/param_aliases_generated.go | 2 +- internal/cli/schema_catalog_test.go | 11 +- internal/helpers/doc.go | 58 ++++++++-- internal/helpers/doc_coverage_more_test.go | 2 +- internal/helpers/doc_read_scope_test.go | 100 +++++++++++++++++- internal/helpers/http_coverage_more_test.go | 6 +- internal/shortcut/doc/content_shortcuts.go | 19 +++- internal/shortcut/doc/doc_shortcuts_test.go | 23 +++- skills/mono/references/products/doc.md | 6 +- .../mono/references/products/doc/doc-read.md | 15 +++ skills/multi/dingtalk-doc/references/doc.md | 2 + .../dingtalk-doc/references/doc/doc-read.md | 9 ++ 13 files changed, 236 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afe33b3..540e113f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] +### Added + +- **Doc public-link and historical-version reads** — `dws doc read` forwards + the reviewed `password` (internet-public documents with password protection) + and `historyVersion` (read content as of a listed historical version; + `0` denotes the document's initial version) parameters on the markdown, + JSONML, and scope read paths via `--password` / + `--version`; `dws doc +fetch` gains `--password` and replaces its + previous hard rejection of `--revision` with validated forwarding to + `get_document_content.historyVersion`. + ## [1.0.59-beta.2] - 2026-08-17 ### Added diff --git a/internal/cli/param_aliases_generated.go b/internal/cli/param_aliases_generated.go index fc5b2840..f5dcc0c1 100644 --- a/internal/cli/param_aliases_generated.go +++ b/internal/cli/param_aliases_generated.go @@ -4383,7 +4383,7 @@ var generatedParamAliases = []ParamAliasEntry{ "output-path": "output", "save-path": "output", }, - Blocked: []string{"before-block-id", "comment-id", "comment-key", "content-file", "dentry-id", "file", "file-path", "folder", "folder-id", "job-id", "name", "parent-block-id", "parent-id", "ref-block-id", "reference-block-id", "revision", "role", "space-id", "task-id", "template-id", "version", "workspace", "workspace-id"}, + Blocked: []string{"before-block-id", "comment-id", "comment-key", "content-file", "dentry-id", "file", "file-path", "folder", "folder-id", "job-id", "name", "parent-block-id", "parent-id", "ref-block-id", "reference-block-id", "revision", "role", "space-id", "task-id", "template-id", "workspace", "workspace-id"}, Ambiguous: []string{"block-id"}, }, { diff --git a/internal/cli/schema_catalog_test.go b/internal/cli/schema_catalog_test.go index 16f7806f..3e80796c 100644 --- a/internal/cli/schema_catalog_test.go +++ b/internal/cli/schema_catalog_test.go @@ -301,7 +301,7 @@ func TestDeliveryCatalogDocReadParamDeclsMatchMergeBaseContract(t *testing.T) { t.Fatalf("doc read --content-format required = %#v, want false", contentFormat["required"]) } - for _, flagName := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id"} { + for _, flagName := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id", "version", "password"} { if parameters[flagName]["required"] != false { t.Fatalf("doc read --%s required = %#v, want false", flagName, parameters[flagName]["required"]) } @@ -315,6 +315,15 @@ func TestDeliveryCatalogDocReadParamDeclsMatchMergeBaseContract(t *testing.T) { if parameters["max-depth"]["type"] != "integer" { t.Fatalf("doc read --max-depth type = %#v, want integer", parameters["max-depth"]["type"]) } + if got := parameters["version"]["property"]; got != "historyVersion" { + t.Fatalf("doc read --version property = %#v, want historyVersion", got) + } + if parameters["version"]["type"] != "integer" { + t.Fatalf("doc read --version type = %#v, want integer", parameters["version"]["type"]) + } + if got := parameters["password"]["property"]; got != "password" { + t.Fatalf("doc read --password property = %#v, want password", got) + } } func TestDeliveryCatalogDocCommentParamDeclsMatchMergeBaseContract(t *testing.T) { diff --git a/internal/helpers/doc.go b/internal/helpers/doc.go index 19c0241e..e43c41df 100644 --- a/internal/helpers/doc.go +++ b/internal/helpers/doc.go @@ -1428,9 +1428,14 @@ func newDocCommand() *cobra.Command { readCmd := &cobra.Command{ Use: "read", Short: "读取文档内容 (Markdown)", - Long: `获取文档内容,以 Markdown 格式返回。支持传入文档 URL 或 ID。`, + Long: `获取文档内容,以 Markdown 格式返回。支持传入文档 URL 或 ID。 + +互联网公开文档(含开启密码保护的)可传入公开链接;设置了访问密码时通过 --password 提供。 +--version 读取指定历史版本内容(版本号从 dws doc version list 获取,0 表示文档初始版本,需要文档编辑权限);缺省读最新版。`, Example: ` dws doc read --node DOC_ID - dws doc read --node "https://alidocs.dingtalk.com/i/nodes/"`, + dws doc read --node "https://alidocs.dingtalk.com/i/nodes/" + dws doc read --node PUBLIC_URL --password + dws doc read --node DOC_ID --version 3`, RunE: func(cmd *cobra.Command, args []string) error { nodeID, err := mustFlagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") if err != nil { @@ -1440,6 +1445,15 @@ func newDocCommand() *cobra.Command { "dws doc read --node DOC_ID --content-format jsonml"); err != nil { return err } + password, _ := cmd.Flags().GetString("password") + historyVersion := 0 + historyVersionSet := cmd.Flags().Changed("version") + if historyVersionSet { + historyVersion, _ = cmd.Flags().GetInt("version") + if historyVersion < 0 { + return fmt.Errorf("--version 必须为非负整数历史版本号(0 表示初始版本,版本号从 dws doc version list 获取),当前值: %d", historyVersion) + } + } format, _ := cmd.Flags().GetString("content-format") scope, _ := cmd.Flags().GetString("scope") tags, _ := cmd.Flags().GetString("tags") @@ -1480,15 +1494,18 @@ func newDocCommand() *cobra.Command { startBlockID, endBlockID, outputPath, + password, + historyVersion, + historyVersionSet, ) } if format == "jsonml" { outputPath, _ := cmd.Flags().GetString("output") - return runDocReadJsonML(cmd, nodeID, outputPath) + return runDocReadJsonML(nodeID, outputPath, password, historyVersion, historyVersionSet) } - return callMCPTool("get_document_content", map[string]any{ - "nodeId": nodeID, - }) + toolArgs := map[string]any{"nodeId": nodeID} + applyDocReadAccessParams(toolArgs, password, historyVersion, historyVersionSet) + return callMCPTool("get_document_content", toolArgs) }, } DeclareLeafMetadata(readCmd, LeafSpec{ @@ -1515,6 +1532,8 @@ func newDocCommand() *cobra.Command { UseWhen: []string{ "用户要读取钉钉在线文字文档(adoc)正文(Markdown)时", "用户直接粘贴文档 URL 且无其他指令时(默认读内容)", + "互联网公开文档(含设置密码保护的公开链接)时配合 --password 提供访问密码", + "要读取指定历史版本内容时用 --version(版本号来自 doc version list,0 表示初始版本,需要编辑权限)", "只需标题大纲、指定块区间/单块或特定 JSONML tags 时使用 --content-format jsonml 与 --scope", }, AvoidWhen: []string{ @@ -1532,9 +1551,11 @@ func newDocCommand() *cobra.Command { {Name: "content-format", Property: "format", Required: boolPtr(false)}, {Name: "end-block-id", Required: boolPtr(false)}, {Name: "max-depth", Required: boolPtr(false), InterfaceType: "integer"}, + {Name: "password", Property: "password", Required: boolPtr(false)}, {Name: "scope", Required: boolPtr(false)}, {Name: "start-block-id", Required: boolPtr(false), RequiredWhen: "--scope=range or --scope=section"}, {Name: "tags", Required: boolPtr(false), RequiredWhen: "--scope=tags"}, + {Name: "version", Property: "historyVersion", Required: boolPtr(false), InterfaceType: "integer"}, }, }, }) @@ -2679,6 +2700,8 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。 readCmd.Flags().Int("max-depth", 0, "筛选遍历最大深度, 0 表示不限(仅 --scope 时生效)") readCmd.Flags().String("start-block-id", "", "range/section 起始块 ID(节点 uuid); scope=range/section 时必填") readCmd.Flags().String("end-block-id", "", "range 结束块 ID(节点 uuid); \"-1\"或空=到文档末尾(仅 scope=range 生效)") + readCmd.Flags().String("password", "", "互联网公开文档开启密码保护时的访问密码;普通文档无需传入") + readCmd.Flags().Int("version", 0, "读取指定历史版本内容(版本号从 doc version list 获取, 0 表示初始版本, 需要文档编辑权限);缺省读最新版") cli.AnnotateRuntimeFlagEnum(readCmd, "scope", "outline", "range", "section", "tags") cli.AnnotateRuntimeFlagRequiredWhen(readCmd, "tags", "--scope=tags") cli.AnnotateRuntimeFlagRequiredWhen(readCmd, "start-block-id", "--scope=range or --scope=section") @@ -4739,16 +4762,30 @@ func wrapDocDeprecatedToTarget(cmd *cobra.Command, targetCmd string) { } } +// applyDocReadAccessParams 把 doc read 的访问参数(互联网公开文档密码、历史版本号) +// 附加到 get_document_content 请求上;空密码或未显式设置版本时不发送对应字段, +// 显式 --version 0 表示读取文档初始版本。 +func applyDocReadAccessParams(args map[string]any, password string, historyVersion int, historyVersionSet bool) { + if password != "" { + args["password"] = password + } + if historyVersionSet && historyVersion >= 0 { + args["historyVersion"] = historyVersion + } +} + // resolveContentFromFlags 从 --content-file / --content-path / --content / --markdown 获取文档内容。 // 优先级:--content-file/--content-path > --content > --markdown(已弃用别名,向后兼容)。 -func runDocReadJsonML(_ *cobra.Command, nodeID string, outputPath string) error { +func runDocReadJsonML(nodeID, outputPath, password string, historyVersion int, historyVersionSet bool) error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() - resultText, err := callMCPToolReturnText(ctx, "get_document_content", map[string]any{ + toolArgs := map[string]any{ "nodeId": nodeID, "format": "jsonml", - }) + } + applyDocReadAccessParams(toolArgs, password, historyVersion, historyVersionSet) + resultText, err := callMCPToolReturnText(ctx, "get_document_content", toolArgs) if err != nil { return err } @@ -4800,7 +4837,7 @@ func runDocReadJsonML(_ *cobra.Command, nodeID string, outputPath string) error // runDocReadScope calls get_document_content with JSONML filtering parameters // and preserves the returned read-only fragment container. -func runDocReadScope(nodeID, scope, tags string, maxDepth int, maxDepthSet bool, startBlockID, endBlockID, outputPath string) error { +func runDocReadScope(nodeID, scope, tags string, maxDepth int, maxDepthSet bool, startBlockID, endBlockID, outputPath, password string, historyVersion int, historyVersionSet bool) error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() @@ -4820,6 +4857,7 @@ func runDocReadScope(nodeID, scope, tags string, maxDepth int, maxDepthSet bool, if endBlockID != "" && scope == "range" { args["endBlockId"] = endBlockID } + applyDocReadAccessParams(args, password, historyVersion, historyVersionSet) resultText, err := callMCPToolReturnTextOnServer(ctx, "doc", "get_document_content", args) if err != nil { diff --git a/internal/helpers/doc_coverage_more_test.go b/internal/helpers/doc_coverage_more_test.go index 674d74f1..bbff1a7d 100644 --- a/internal/helpers/doc_coverage_more_test.go +++ b/internal/helpers/doc_coverage_more_test.go @@ -144,7 +144,7 @@ func TestCrossPlatformCoverageRunDocReadJSONMLCoverage(t *testing.T) { t.Run(tc.name, func(t *testing.T) { caller := &scriptedToolCaller{steps: []scriptedToolStep{tc.step}} installScriptedCaller(t, caller) - err := runDocReadJsonML(&cobra.Command{}, "node", tc.output) + err := runDocReadJsonML("node", tc.output, "", 0, false) if tc.wantFail && err == nil { t.Fatal("expected failure") } diff --git a/internal/helpers/doc_read_scope_test.go b/internal/helpers/doc_read_scope_test.go index 3fe9e8d7..dd68fc1d 100644 --- a/internal/helpers/doc_read_scope_test.go +++ b/internal/helpers/doc_read_scope_test.go @@ -77,7 +77,7 @@ func TestCrossPlatformCoverageDocReadScopeFlagsAndValidation(t *testing.T) { if err != nil || len(remaining) != 0 { t.Fatalf("find read: remaining=%v err=%v", remaining, err) } - for _, name := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id"} { + for _, name := range []string{"scope", "tags", "max-depth", "start-block-id", "end-block-id", "password", "version"} { if read.Flags().Lookup(name) == nil { t.Fatalf("doc read missing --%s", name) } @@ -249,7 +249,7 @@ func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := installDocReadScopeCaller(t, tt.caller) - err := runDocReadScope("doc-1", "", "", 0, false, "", "ignored", tt.output) + err := runDocReadScope("doc-1", "", "", 0, false, "", "ignored", tt.output, "", 0, false) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %v, want %q", err, tt.wantErr) @@ -277,7 +277,7 @@ func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) { caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{}]"}`} stdout := installDocReadScopeCaller(t, caller) outputPath := filepath.Join(t.TempDir(), "fragment.json") - if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath); err != nil { + if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath, "", 0, false); err != nil { t.Fatalf("runDocReadScope: %v", err) } var receipt map[string]any @@ -292,7 +292,7 @@ func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) { t.Run("human output reports missing fragment", func(t *testing.T) { caller := &docReadScopeCaller{text: `{}`, format: "raw"} stdout := installDocReadScopeCaller(t, caller) - if err := runDocReadScope("doc-1", "", "", 0, false, "", "", ""); err != nil { + if err := runDocReadScope("doc-1", "", "", 0, false, "", "", "", "", 0, false); err != nil { t.Fatalf("runDocReadScope: %v", err) } if !strings.Contains(stdout.String(), "未匹配到节点") { @@ -304,7 +304,7 @@ func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) { caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{}]"}`, format: "raw"} stdout := installDocReadScopeCaller(t, caller) outputPath := filepath.Join(t.TempDir(), "fragment.json") - if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath); err != nil { + if err := runDocReadScope("doc-1", "outline", "", 0, false, "", "", outputPath, "", 0, false); err != nil { t.Fatalf("runDocReadScope: %v", err) } if !strings.Contains(stdout.String(), "JSONML fragment 已写入 "+outputPath) { @@ -312,3 +312,93 @@ func TestCrossPlatformCoverageRunDocReadScopeResponseBranches(t *testing.T) { } }) } + +// TestCrossPlatformCoverageDocReadAccessParamsForwarding 验证 doc read 三条读取路径 +// 都把 --password / --version 透传到 get_document_content,且非法版本号在 +// 本地校验阶段被拒绝、不产生远端调用。 +func TestCrossPlatformCoverageDocReadAccessParamsForwarding(t *testing.T) { + t.Run("markdown path forwards password and history version", func(t *testing.T) { + caller := &docReadScopeCaller{text: `{"markdown":"body"}`} + if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--password", "pw", "--version", "7"); err != nil { + t.Fatalf("execute: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(caller.calls)) + } + want := map[string]any{"nodeId": "doc-1", "password": "pw", "historyVersion": 7} + if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) { + t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want) + } + }) + + t.Run("jsonml path forwards password and history version", func(t *testing.T) { + caller := &docReadScopeCaller{text: `{"jsonml":"[\"root\",{}]","revision":7}`} + if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--content-format", "jsonml", "--password", "pw", "--version", "7"); err != nil { + t.Fatalf("execute: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(caller.calls)) + } + want := map[string]any{"nodeId": "doc-1", "format": "jsonml", "password": "pw", "historyVersion": 7} + if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) { + t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want) + } + }) + + t.Run("scope path forwards password and history version", func(t *testing.T) { + caller := &docReadScopeCaller{text: `{"jsonml":"[\"fragment\",{},[\"h1\",{},\"t\"]]"}`} + if err := executeDocReadScopeCommand(t, caller, + "read", "--node", "doc-1", "--content-format", "jsonml", "--scope", "outline", + "--password", "pw", "--version", "7"); err != nil { + t.Fatalf("execute: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(caller.calls)) + } + want := map[string]any{"nodeId": "doc-1", "format": "jsonml", "scope": "outline", "password": "pw", "historyVersion": 7} + if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) { + t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want) + } + }) + + t.Run("negative history version is rejected locally", func(t *testing.T) { + for _, raw := range []string{"-1", "-3"} { + caller := &docReadScopeCaller{} + err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--version", raw) + if err == nil || !strings.Contains(err.Error(), "--version 必须为非负整数") { + t.Fatalf("--version %s error = %v, want invalid --version", raw, err) + } + if len(caller.calls) != 0 { + t.Fatalf("--version %s produced %d remote calls, want 0", raw, len(caller.calls)) + } + } + }) + + t.Run("explicit zero history version forwards initial version", func(t *testing.T) { + caller := &docReadScopeCaller{text: `{"markdown":"body"}`} + if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1", "--version", "0"); err != nil { + t.Fatalf("execute: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(caller.calls)) + } + want := map[string]any{"nodeId": "doc-1", "historyVersion": 0} + if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) { + t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want) + } + }) + + t.Run("unset history version is not forwarded", func(t *testing.T) { + caller := &docReadScopeCaller{text: `{"markdown":"body"}`} + if err := executeDocReadScopeCommand(t, caller, "read", "--node", "doc-1"); err != nil { + t.Fatalf("execute: %v", err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %d, want 1", len(caller.calls)) + } + want := map[string]any{"nodeId": "doc-1"} + if caller.calls[0].tool != "get_document_content" || !reflect.DeepEqual(caller.calls[0].args, want) { + t.Fatalf("call = %#v, want get_document_content %#v", caller.calls[0], want) + } + }) +} diff --git a/internal/helpers/http_coverage_more_test.go b/internal/helpers/http_coverage_more_test.go index d7bf6fd4..2d4e01b5 100644 --- a/internal/helpers/http_coverage_more_test.go +++ b/internal/helpers/http_coverage_more_test.go @@ -147,9 +147,9 @@ func TestCrossPlatformCoverageRunDocReadJsonMLCoverage(t *testing.T) { `{"jsonml":"{\"type\":\"doc\"}","revision":"bad"}`, } { caller.result = &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}} - _ = runDocReadJsonML(nil, "node", "") + _ = runDocReadJsonML("node", "", "", 0, false) } caller.result = &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{"jsonml":"{\"type\":\"doc\"}"}`}}} - _ = runDocReadJsonML(nil, "node", filepath.Join(t.TempDir(), "out.json")) - _ = runDocReadJsonML(nil, "node", filepath.Join(t.TempDir(), "missing", "out.json")) + _ = runDocReadJsonML("node", filepath.Join(t.TempDir(), "out.json"), "", 0, false) + _ = runDocReadJsonML("node", filepath.Join(t.TempDir(), "missing", "out.json"), "pw", 3, true) } diff --git a/internal/shortcut/doc/content_shortcuts.go b/internal/shortcut/doc/content_shortcuts.go index df6fd28e..00c55b47 100644 --- a/internal/shortcut/doc/content_shortcuts.go +++ b/internal/shortcut/doc/content_shortcuts.go @@ -176,15 +176,17 @@ var Fetch = shortcut.Shortcut{ Command: "+fetch", Product: productDoc, Description: "读取完整或局部文档内容,并按 detail 控制保真度", - Intent: "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论。", + Intent: "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --revision 指定版本号(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 局部内容用于精确编辑和评论。", + "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --revision 指定版本号(0 表示初始版本)。", []string{`dws doc +fetch --node `, `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: "revision", Property: "historyVersion"}, ), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL;" + fetchTargetConstraint}, @@ -198,12 +200,13 @@ var Fetch = shortcut.Shortcut{ {Name: "context-before", Type: shortcut.FlagInt, Desc: "关键词命中前的上下文字符数"}, {Name: "context-after", Type: shortcut.FlagInt, Desc: "关键词命中后的上下文字符数"}, {Name: "max-depth", Type: shortcut.FlagInt, Desc: "outline/section 最大深度"}, - {Name: "revision", Type: shortcut.FlagInt, Desc: "只接受当前最新版;历史 revision 暂不支持"}, + {Name: "password", Type: shortcut.FlagString, Desc: "互联网公开文档开启密码保护时的访问密码;普通文档无需传入"}, + {Name: "revision", Type: shortcut.FlagInt, Desc: "读取指定历史版本(版本号从 doc +version-list 获取, 0 表示初始版本, 需要文档编辑权限);缺省读最新版"}, }, Tips: []string{`dws doc +fetch --node `, `dws doc +fetch --query "项目周报" --scope keyword --keyword "结论"`}, Validate: func(rt *shortcut.RuntimeContext) error { - if rt.Changed("revision") { - return apperrors.NewValidation("HISTORICAL_READ_UNSUPPORTED: 当前接口不能读取指定历史 revision") + if rt.Changed("revision") && rt.Int("revision") < 0 { + return apperrors.NewValidation("--revision 必须为非负整数历史版本号(0 表示初始版本,从 doc +version-list 获取)") } if rt.Str("scope") == "keyword" && rt.Str("keyword") == "" { return apperrors.NewValidation("--scope keyword 时必须提供 --keyword") @@ -240,6 +243,12 @@ var Fetch = shortcut.Shortcut{ if rt.Changed("max-depth") { params["maxDepth"] = rt.Int("max-depth") } + if rt.Changed("revision") { + params["historyVersion"] = rt.Int("revision") + } + if value := rt.Str("password"); value != "" { + params["password"] = value + } data, err := rt.CallMCPData(productDoc, "get_document_content", params) if err != nil { return err diff --git a/internal/shortcut/doc/doc_shortcuts_test.go b/internal/shortcut/doc/doc_shortcuts_test.go index 8b3da279..1206bd10 100644 --- a/internal/shortcut/doc/doc_shortcuts_test.go +++ b/internal/shortcut/doc/doc_shortcuts_test.go @@ -1072,7 +1072,7 @@ func TestCrossPlatformCoverageDocContentValidationAndPureHelpers(t *testing.T) { {Create, []string{"--name", "n", "--content", `{}`, "--doc-format", "jsonml"}}, {Create, []string{"--name", "n", "--content", `[]`, "--doc-format", "jsonml"}}, {Create, []string{"--name", "n", "--content", `[["p",{},"x"]]`, "--doc-format", "jsonml"}}, - {Fetch, []string{"--node", "n", "--revision", "1"}}, + {Fetch, []string{"--node", "n", "--revision", "-2"}}, {Fetch, []string{"--node", "n", "--scope", "keyword"}}, {Update, []string{"--node", "n"}}, {Update, []string{"--command", "append", "--content", "x", "--yes"}}, @@ -1103,6 +1103,27 @@ func TestCrossPlatformCoverageDocContentValidationAndPureHelpers(t *testing.T) { t.Fatalf("expected revision was not forwarded atomically: %#v", revision.history) } + fetchAccess := &docCoverageCaller{} + if err := runDocCoverage(t, Fetch, fetchAccess, "--node", "n", "--revision", "7", "--password", "pw"); err != nil { + t.Fatalf("fetch with access params failed: %v", err) + } + if len(fetchAccess.history) != 1 || + fetchAccess.history[0].tool != "get_document_content" || + fetchAccess.history[0].params["historyVersion"] != 7 || + fetchAccess.history[0].params["password"] != "pw" { + t.Fatalf("fetch access params were not forwarded: %#v", fetchAccess.history) + } + + fetchZero := &docCoverageCaller{} + if err := runDocCoverage(t, Fetch, fetchZero, "--node", "n", "--revision", "0"); err != nil { + t.Fatalf("fetch with zero revision failed: %v", err) + } + if len(fetchZero.history) != 1 || + fetchZero.history[0].tool != "get_document_content" || + fetchZero.history[0].params["historyVersion"] != 0 { + t.Fatalf("fetch zero revision (initial version) was not forwarded: %#v", fetchZero.history) + } + for _, value := range []any{ map[string]any{"revision": 2.5}, map[string]any{"revision": json.Number("bad")}, map[string]any{"revision": "bad"}, map[string]any{"data": []any{map[string]any{"versionNumber": "3"}}}, []any{map[string]any{"version": 4.0}}, "none", diff --git a/skills/mono/references/products/doc.md b/skills/mono/references/products/doc.md index f7d2769c..bd0e4e45 100644 --- a/skills/mono/references/products/doc.md +++ b/skills/mono/references/products/doc.md @@ -55,6 +55,8 @@ Example: dws doc read --node "https://alidocs.dingtalk.com/document/edit?dentryKey=" dws doc read --node "https://alidocs.dingtalk.com/document/preview?dentryKey=" dws doc read --node --content-format jsonml --scope outline + dws doc read --node --password + dws doc read --node --version 3 Flags: --node string 文档 ID 或 URL (必填) --content-format string 输出格式: markdown / jsonml @@ -63,9 +65,11 @@ Flags: --max-depth int 筛选遍历最大深度,0 表示不限 --start-block-id string range / section 起始块 UUID --end-block-id string range 结束块 UUID + --password string 互联网公开文档密码保护时的访问密码 + --version int 读取指定历史版本(版本号从 doc version list 获取,0 表示初始版本);缺省读最新版 ``` -`--scope` 只适用于 `--content-format jsonml`,用于按大纲、块区间、单块子树或自定义 tag 局部读取。筛选结果是虚拟 JSONML fragment,只能剥壳后消费 children,不得把 fragment 容器整体写回。完整规则见 [`doc-read.md`](./doc/doc-read.md)。 +`--scope` 只适用于 `--content-format jsonml`,用于按大纲、块区间、单块子树或自定义 tag 局部读取。筛选结果是虚拟 JSONML fragment,只能剥壳后消费 children,不得把 fragment 容器整体写回。互联网公开文档(含密码保护)用 `--password`;读取历史版本内容用 `--version`(版本号来自 `dws doc version list`,0 表示初始版本,需要编辑权限)。完整规则见 [`doc-read.md`](./doc/doc-read.md)。 ### 创建文档 ``` diff --git a/skills/mono/references/products/doc/doc-read.md b/skills/mono/references/products/doc/doc-read.md index 07132bef..2e1d6741 100644 --- a/skills/mono/references/products/doc/doc-read.md +++ b/skills/mono/references/products/doc/doc-read.md @@ -25,6 +25,8 @@ Flags: --max-depth int 筛选遍历最大深度,0 表示不限 --start-block-id string range / section 起始块 UUID --end-block-id string range 结束块 UUID;空或 "-1" 表示到文档末尾 + --password string 互联网公开文档开启密码保护时的访问密码;普通文档无需传入 + --version int 读取指定历史版本内容(版本号从 doc version list 获取,0 表示初始版本,需要文档编辑权限);缺省读最新版 ``` ## 关键说明 @@ -33,6 +35,13 @@ Flags: - 返回的 Markdown 中,附件以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/.ext?Expires=...`),**链接会过期**。链接过期后从 URL 路径中提取 ``(即 `/att/` 后、扩展名前的 UUID 部分),用 `media download --node --resource-id ` 重新获取下载链接。 - `--content-format jsonml` 返回完整 JSONML 结构(含 `revision`),用于无损读改写;可直接配合 [`doc-update.md`](./doc-update.md) 的 `--content-format jsonml --content-file` 写回。`revision` 仅在并发敏感场景下需要透传给 update 触发并发检查(详见下方)。 +## 互联网公开文档与历史版本 + +- **互联网公开文档**:公开链接直接传 `--node`;文档开启了密码保护时,通过 `--password ` 提供访问密码,普通文档无需传入。密码只进入读取请求,不会回显在读取结果里(`--dry-run` 预览会包含所传参数,注意输出环境)。 +- **历史版本**:`--version ` 读取指定历史版本内容(Markdown / JSONML / scope 均可组合);版本号从 `dws doc version list` 或 `dws doc +version-list` 获取,`0` 表示文档初始版本;需要文档编辑权限(EDITOR 及以上),缺省读最新版。 +- **跨组织文档**:非互联网公开的跨组织文档,`doc read` 整体被组织边界拦截(提示「不支持跨组织访问数据」),最新版与历史版本内容都读不到(`doc info` 仍可用);互联网公开(含密码)文档的 `doc read` 不受影响,但 `doc version list` / `+version-list` 与写操作仍会被拦截,版本号无法从列表获取。 +- 只要看历史内容、不打算恢复时用 `--version`;要把文档整体恢复到历史版本才用 `doc version revert`(危险操作,需确认)。 + ## content-format=jsonml 输出 输出 JSON 对象,包含 `revision`(版本号)和 `jsonml`(JSONML body 数组): @@ -92,6 +101,12 @@ dws doc read --node --format json # alidocs URL 直传 dws doc read --node "https://alidocs.dingtalk.com/i/nodes/" --format json +# 互联网公开文档(含密码保护) +dws doc read --node --password --format json + +# 读取历史版本(版本号来自 dws doc version list) +dws doc read --node --version 3 --format json + # JSONML 完整结构 → 文件(无损改写前置) dws doc read --node --content-format jsonml --output /tmp/doc.json # 之后修改 /tmp/doc.json 中的 jsonml 数组,再用: diff --git a/skills/multi/dingtalk-doc/references/doc.md b/skills/multi/dingtalk-doc/references/doc.md index 76570e83..9c7ed952 100644 --- a/skills/multi/dingtalk-doc/references/doc.md +++ b/skills/multi/dingtalk-doc/references/doc.md @@ -48,6 +48,8 @@ dws doc +version-revert --node --version --format json `+version-save/list/revert` 分别用于快照、浏览和恢复,命中后直接执行,不预读 Help。`+history-*` 仅兼容已有调用,不用于新的 Agent 选路。重要内容更新优先使用 `+checkpoint-update`,不要手工编排保存、写入和回读。回滚必须确认,以 leaf Schema 与 Runtime gate 为准。 +只读某个历史版本的内容而不回滚时,用 `dws doc +fetch --node --revision `(版本号同样来自 `+version-list`,`0` 表示初始版本,需要文档编辑权限);不要为了看历史内容而执行回滚。互联网公开文档(含密码保护)的读取见 [doc-read.md](doc/doc-read.md) 的 `--password`。 + ## 权限与分享 - 查询或聚合权限:`+inspect --include-permissions`。 diff --git a/skills/multi/dingtalk-doc/references/doc/doc-read.md b/skills/multi/dingtalk-doc/references/doc/doc-read.md index dc8dd710..2fe6b330 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-read.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-read.md @@ -5,6 +5,8 @@ ```bash dws doc +fetch --node --format json dws doc +fetch --query "项目周报" --scope keyword --keyword "结论" --format json +dws doc +fetch --node --password --format json +dws doc +fetch --node --revision --format json ``` - 已知 ID 或 URL:传 `--node`。 @@ -12,6 +14,13 @@ dws doc +fetch --query "项目周报" --scope keyword --keyword "结论" --forma - `--node` 与 `--query` 必须且只能提供一个。 - 默认 `--detail simple --scope full`,适合普通阅读,避免加载不必要的 JSONML。 +## 互联网公开文档与历史版本 + +- **互联网公开文档**:公开链接直接传 `--node`;文档开启了密码保护时,通过 `--password ` 提供访问密码,普通文档无需传入。密码只进入读取请求,不会回显在读取结果里(`--dry-run` 预览会包含所传参数,注意输出环境)。 +- **历史版本**:`--revision ` 读取指定历史版本内容;版本号从 `dws doc +version-list` 获取,`0` 表示文档初始版本,需要文档编辑权限(EDITOR 及以上);缺省读最新版。 +- **跨组织文档**:非互联网公开的跨组织文档,`+fetch` 整体被组织边界拦截(提示「不支持跨组织访问数据」),最新版与历史版本内容都读不到;互联网公开(含密码)文档的 `+fetch` 不受影响,但 `+version-list` 与写操作仍会被拦截,版本号无法从列表获取。 +- 只要看历史内容、不打算恢复时用 `--revision`;要把文档整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。 + ## 局部读取 ```bash From a74d96bb960bfd4a308953ab46176583142392a1 Mon Sep 17 00:00:00 2001 From: liyuan Date: Thu, 20 Aug 2026 17:32:11 +0800 Subject: [PATCH 2/4] feat(doc): read password-protected public docs and historical versions --- .changes/doc-read-public-and-history-version.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changes/doc-read-public-and-history-version.md diff --git a/.changes/doc-read-public-and-history-version.md b/.changes/doc-read-public-and-history-version.md new file mode 100644 index 00000000..95dec203 --- /dev/null +++ b/.changes/doc-read-public-and-history-version.md @@ -0,0 +1,11 @@ +--- +category: Added +--- + +- **Doc public-link and historical-version reads** — `dws doc read` forwards + the reviewed `password` (internet-public documents with password protection) + and `historyVersion` (read content as of a listed historical version; `0` + denotes the document's initial version) parameters on the markdown, JSONML, + and scope read paths via `--password` / `--version`; `dws doc +fetch` gains + `--password` and replaces its previous hard rejection of `--revision` with + validated forwarding to `get_document_content.historyVersion`. From 470aa42d7bc990075e80d810c9d763bb2829a774 Mon Sep 17 00:00:00 2001 From: liyuan Date: Thu, 20 Aug 2026 17:35:03 +0800 Subject: [PATCH 3/4] chore: keep release note in .changes fragment, restore CHANGELOG.md --- CHANGELOG.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540e113f..9afe33b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,6 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] -### Added - -- **Doc public-link and historical-version reads** — `dws doc read` forwards - the reviewed `password` (internet-public documents with password protection) - and `historyVersion` (read content as of a listed historical version; - `0` denotes the document's initial version) parameters on the markdown, - JSONML, and scope read paths via `--password` / - `--version`; `dws doc +fetch` gains `--password` and replaces its - previous hard rejection of `--revision` with validated forwarding to - `get_document_content.historyVersion`. - ## [1.0.59-beta.2] - 2026-08-17 ### Added From 49ab53ea0da025f5994d8da7ebb1e2277b37d892 Mon Sep 17 00:00:00 2001 From: liyuan Date: Fri, 21 Aug 2026 14:29:05 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E8=AF=84=E5=AE=A1=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc-read-public-and-history-version.md | 6 ++++-- internal/cli/param_aliases_generated.go | 2 +- internal/shortcut/doc/content_shortcuts.go | 20 +++++++++++-------- internal/shortcut/doc/doc_shortcuts_test.go | 11 +++++----- skills/multi/dingtalk-doc/references/doc.md | 2 +- .../dingtalk-doc/references/doc/doc-read.md | 6 +++--- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/.changes/doc-read-public-and-history-version.md b/.changes/doc-read-public-and-history-version.md index 95dec203..b04e979d 100644 --- a/.changes/doc-read-public-and-history-version.md +++ b/.changes/doc-read-public-and-history-version.md @@ -7,5 +7,7 @@ category: Added and `historyVersion` (read content as of a listed historical version; `0` denotes the document's initial version) parameters on the markdown, JSONML, and scope read paths via `--password` / `--version`; `dws doc +fetch` gains - `--password` and replaces its previous hard rejection of `--revision` with - validated forwarding to `get_document_content.historyVersion`. + `--password` and `--version` with the same `historyVersion` forwarding, while + `--revision` stays rejected with explicit guidance: revision is the document + edit revision returned by JSONML reads for `+update --expected-revision` + conditional writes, not a historical version number. diff --git a/internal/cli/param_aliases_generated.go b/internal/cli/param_aliases_generated.go index f5dcc0c1..4451679c 100644 --- a/internal/cli/param_aliases_generated.go +++ b/internal/cli/param_aliases_generated.go @@ -3765,7 +3765,7 @@ var generatedParamAliases = []ParamAliasEntry{ "start-block": "start-block-id", "url": "node", }, - Blocked: []string{"comment-id", "comment-key", "content-format", "dentry-id", "doc-format", "folder", "folder-id", "id", "job-id", "name", "parent-id", "range", "role", "space-id", "task-id", "template-id", "version", "workspace", "workspace-id"}, + Blocked: []string{"comment-id", "comment-key", "content-format", "dentry-id", "doc-format", "folder", "folder-id", "id", "job-id", "name", "parent-id", "range", "role", "space-id", "task-id", "template-id", "workspace", "workspace-id"}, Ambiguous: []string{"block-id"}, }, { diff --git a/internal/shortcut/doc/content_shortcuts.go b/internal/shortcut/doc/content_shortcuts.go index 00c55b47..72d071da 100644 --- a/internal/shortcut/doc/content_shortcuts.go +++ b/internal/shortcut/doc/content_shortcuts.go @@ -176,17 +176,17 @@ var Fetch = shortcut.Shortcut{ Command: "+fetch", Product: productDoc, Description: "读取完整或局部文档内容,并按 detail 控制保真度", - Intent: "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --revision 指定版本号(0 表示初始版本)。", + 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 提供访问密码,读历史版本用 --revision 指定版本号(0 表示初始版本)。", + "当用户要按 node/URL 直接读取在线文字文档,或只知道唯一标题并希望一次完成解析和读取时使用;支持 outline/range/section/keyword/tags 局部内容用于精确编辑和评论;互联网公开文档(含密码保护)用 --password 提供访问密码,读历史版本用 --version 指定版本号(0 表示初始版本)。", []string{`dws doc +fetch --node `, `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: "revision", Property: "historyVersion"}, + contract.ParamDecl{Name: "version", Property: "historyVersion"}, ), Flags: []shortcut.Flag{ {Name: "node", Type: shortcut.FlagString, Desc: "文档 ID 或 URL;" + fetchTargetConstraint}, @@ -201,12 +201,16 @@ var Fetch = shortcut.Shortcut{ {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: "读取指定历史版本(版本号从 doc +version-list 获取, 0 表示初始版本, 需要文档编辑权限);缺省读最新版"}, + {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 `, `dws doc +fetch --query "项目周报" --scope keyword --keyword "结论"`}, Validate: func(rt *shortcut.RuntimeContext) error { - if rt.Changed("revision") && rt.Int("revision") < 0 { - return apperrors.NewValidation("--revision 必须为非负整数历史版本号(0 表示初始版本,从 doc +version-list 获取)") + 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") @@ -243,8 +247,8 @@ var Fetch = shortcut.Shortcut{ if rt.Changed("max-depth") { params["maxDepth"] = rt.Int("max-depth") } - if rt.Changed("revision") { - params["historyVersion"] = rt.Int("revision") + if rt.Changed("version") { + params["historyVersion"] = rt.Int("version") } if value := rt.Str("password"); value != "" { params["password"] = value diff --git a/internal/shortcut/doc/doc_shortcuts_test.go b/internal/shortcut/doc/doc_shortcuts_test.go index 1206bd10..975ece10 100644 --- a/internal/shortcut/doc/doc_shortcuts_test.go +++ b/internal/shortcut/doc/doc_shortcuts_test.go @@ -1072,7 +1072,8 @@ func TestCrossPlatformCoverageDocContentValidationAndPureHelpers(t *testing.T) { {Create, []string{"--name", "n", "--content", `{}`, "--doc-format", "jsonml"}}, {Create, []string{"--name", "n", "--content", `[]`, "--doc-format", "jsonml"}}, {Create, []string{"--name", "n", "--content", `[["p",{},"x"]]`, "--doc-format", "jsonml"}}, - {Fetch, []string{"--node", "n", "--revision", "-2"}}, + {Fetch, []string{"--node", "n", "--revision", "7"}}, + {Fetch, []string{"--node", "n", "--version", "-2"}}, {Fetch, []string{"--node", "n", "--scope", "keyword"}}, {Update, []string{"--node", "n"}}, {Update, []string{"--command", "append", "--content", "x", "--yes"}}, @@ -1104,7 +1105,7 @@ func TestCrossPlatformCoverageDocContentValidationAndPureHelpers(t *testing.T) { } fetchAccess := &docCoverageCaller{} - if err := runDocCoverage(t, Fetch, fetchAccess, "--node", "n", "--revision", "7", "--password", "pw"); err != nil { + if err := runDocCoverage(t, Fetch, fetchAccess, "--node", "n", "--version", "7", "--password", "pw"); err != nil { t.Fatalf("fetch with access params failed: %v", err) } if len(fetchAccess.history) != 1 || @@ -1115,13 +1116,13 @@ func TestCrossPlatformCoverageDocContentValidationAndPureHelpers(t *testing.T) { } fetchZero := &docCoverageCaller{} - if err := runDocCoverage(t, Fetch, fetchZero, "--node", "n", "--revision", "0"); err != nil { - t.Fatalf("fetch with zero revision failed: %v", err) + if err := runDocCoverage(t, Fetch, fetchZero, "--node", "n", "--version", "0"); err != nil { + t.Fatalf("fetch with zero version failed: %v", err) } if len(fetchZero.history) != 1 || fetchZero.history[0].tool != "get_document_content" || fetchZero.history[0].params["historyVersion"] != 0 { - t.Fatalf("fetch zero revision (initial version) was not forwarded: %#v", fetchZero.history) + t.Fatalf("fetch zero version (initial version) was not forwarded: %#v", fetchZero.history) } for _, value := range []any{ diff --git a/skills/multi/dingtalk-doc/references/doc.md b/skills/multi/dingtalk-doc/references/doc.md index 9c7ed952..6ec6b8bf 100644 --- a/skills/multi/dingtalk-doc/references/doc.md +++ b/skills/multi/dingtalk-doc/references/doc.md @@ -48,7 +48,7 @@ dws doc +version-revert --node --version --format json `+version-save/list/revert` 分别用于快照、浏览和恢复,命中后直接执行,不预读 Help。`+history-*` 仅兼容已有调用,不用于新的 Agent 选路。重要内容更新优先使用 `+checkpoint-update`,不要手工编排保存、写入和回读。回滚必须确认,以 leaf Schema 与 Runtime gate 为准。 -只读某个历史版本的内容而不回滚时,用 `dws doc +fetch --node --revision `(版本号同样来自 `+version-list`,`0` 表示初始版本,需要文档编辑权限);不要为了看历史内容而执行回滚。互联网公开文档(含密码保护)的读取见 [doc-read.md](doc/doc-read.md) 的 `--password`。 +只读某个历史版本的内容时,用 `dws doc +fetch --node --version `(版本号同样来自 `+version-list`,`0` 表示初始版本,需要文档编辑权限);整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。互联网公开文档(含密码保护)的读取见 [doc-read.md](doc/doc-read.md) 的 `--password`。 ## 权限与分享 diff --git a/skills/multi/dingtalk-doc/references/doc/doc-read.md b/skills/multi/dingtalk-doc/references/doc/doc-read.md index 2fe6b330..93b9bae6 100644 --- a/skills/multi/dingtalk-doc/references/doc/doc-read.md +++ b/skills/multi/dingtalk-doc/references/doc/doc-read.md @@ -6,7 +6,7 @@ dws doc +fetch --node --format json dws doc +fetch --query "项目周报" --scope keyword --keyword "结论" --format json dws doc +fetch --node --password --format json -dws doc +fetch --node --revision --format json +dws doc +fetch --node --version --format json ``` - 已知 ID 或 URL:传 `--node`。 @@ -17,9 +17,9 @@ dws doc +fetch --node --revision --format json ## 互联网公开文档与历史版本 - **互联网公开文档**:公开链接直接传 `--node`;文档开启了密码保护时,通过 `--password ` 提供访问密码,普通文档无需传入。密码只进入读取请求,不会回显在读取结果里(`--dry-run` 预览会包含所传参数,注意输出环境)。 -- **历史版本**:`--revision ` 读取指定历史版本内容;版本号从 `dws doc +version-list` 获取,`0` 表示文档初始版本,需要文档编辑权限(EDITOR 及以上);缺省读最新版。 +- **历史版本**:`--version ` 读取指定历史版本内容;版本号从 `dws doc +version-list` 获取,`0` 表示文档初始版本,需要文档编辑权限(EDITOR 及以上);缺省读最新版。注意区分:`revision` 是文档编辑版本号(JSONML 读取响应返回、供 `+update --expected-revision` 条件写使用),不是历史版本号,`+fetch` 不支持 `--revision`。 - **跨组织文档**:非互联网公开的跨组织文档,`+fetch` 整体被组织边界拦截(提示「不支持跨组织访问数据」),最新版与历史版本内容都读不到;互联网公开(含密码)文档的 `+fetch` 不受影响,但 `+version-list` 与写操作仍会被拦截,版本号无法从列表获取。 -- 只要看历史内容、不打算恢复时用 `--revision`;要把文档整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。 +- 只要看历史内容、不打算恢复时用 `--version`;要把文档整体恢复到历史版本才用 `+version-revert`(危险操作,需确认)。 ## 局部读取