From 2778bef5bda10e13d9b7b9213874913f88872913 Mon Sep 17 00:00:00 2001 From: xiatian Date: Thu, 13 Aug 2026 11:46:58 +0800 Subject: [PATCH 1/8] feat(sheet): support source range dropdowns and read completion --- .changes/sheet-source-range-dropdowns.md | 6 + internal/helpers/sheet_batch.go | 42 +++- internal/helpers/sheet_batch_test.go | 10 +- internal/helpers/sheet_data.go | 12 +- internal/helpers/sheet_dimension.go | 112 ++++++++-- .../sheet_dropdown_source_range_test.go | 171 +++++++++++++++ internal/helpers/sheet_range_ops.go | 19 +- .../helpers/sheet_read_completion_test.go | 202 ++++++++++++++++++ internal/helpers/sheet_validate.go | 54 +++-- .../products/sheet/sheet-batch-operations.md | 4 +- .../products/sheet/sheet-dropdown.md | 37 ++-- .../products/sheet/sheet-read-data.md | 38 +++- .../products/sheet/sheet-write-data.md | 7 +- .../sheet/sheet-batch-operations.md | 4 +- .../references/sheet/sheet-dropdown.md | 37 ++-- .../references/sheet/sheet-read-data.md | 38 +++- .../references/sheet/sheet-write-data.md | 7 +- 17 files changed, 707 insertions(+), 93 deletions(-) create mode 100644 .changes/sheet-source-range-dropdowns.md create mode 100644 internal/helpers/sheet_dropdown_source_range_test.go create mode 100644 internal/helpers/sheet_read_completion_test.go diff --git a/.changes/sheet-source-range-dropdowns.md b/.changes/sheet-source-range-dropdowns.md new file mode 100644 index 00000000..55d58640 --- /dev/null +++ b/.changes/sheet-source-range-dropdowns.md @@ -0,0 +1,6 @@ +--- +category: Added +--- + +- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. +- **Sheet read completion metadata** — documents and preserves returned ranges, truncation reasons, and partial-read status for large range and CSV reads. diff --git a/internal/helpers/sheet_batch.go b/internal/helpers/sheet_batch.go index 218a88fd..664bc4ae 100644 --- a/internal/helpers/sheet_batch.go +++ b/internal/helpers/sheet_batch.go @@ -60,6 +60,11 @@ func translateBatchOp(op map[string]any) (map[string]any, error) { if !ok { return nil, fmt.Errorf("unsupported toolName %q: must be a CLI command name (e.g. \"range clear\", \"range update\", \"merge-cells\"). Run 'dws sheet batch-update --help' for the full list", toolName) } + if toolName == "set-dropdown" { + if err := validateBatchSetDropdownInput(input); err != nil { + return nil, err + } + } return map[string]any{ "toolName": mapping.mcpTool, @@ -67,6 +72,33 @@ func translateBatchOp(op map[string]any) (map[string]any, error) { }, nil } +func validateBatchSetDropdownInput(input map[string]any) error { + if _, exists := input["source-colors"]; exists { + return fmt.Errorf("set-dropdown: SourceRange 颜色写入暂不支持") + } + if _, exists := input["colors"]; exists { + return fmt.Errorf("set-dropdown: SourceRange 颜色写入暂不支持") + } + + options, hasOptions := input["options"] + hasOptions = hasOptions && options != nil + sourceRange := batchStr(input, "source-range") + sourceSheetID := batchStr(input, "source-sheet-id") + hasSourceRange := sourceRange != "" + if hasOptions == hasSourceRange { + return fmt.Errorf("set-dropdown: options 与 source-range 必须且只能指定一个") + } + if hasSourceRange != (sourceSheetID != "") { + return fmt.Errorf("set-dropdown: source-range 与 source-sheet-id 必须同时指定") + } + if hasSourceRange { + if err := validateDropdownSourceRangeNotation(sourceRange); err != nil { + return fmt.Errorf("set-dropdown: %w", err) + } + } + return nil +} + // ── BuildXxxArgs: CLI flag → MCP param 转换函数 ────────────────────────────────── // 每个函数接收 CLI flag 名(kebab-case)的 map,输出 MCP 参数名(camelCase)的 map。 // 目前集中放在此文件;后续拆分命令文件时可移到各命令所在文件。 @@ -215,7 +247,15 @@ func BuildSetDropdownArgs(input map[string]any) map[string]any { args := map[string]any{ "sheetId": batchStr(input, "sheet-id"), "range": batchStr(input, "range"), - "options": input["options"], + } + if options, ok := input["options"]; ok && options != nil { + args["options"] = options + } + if sourceRange := batchStr(input, "source-range"); sourceRange != "" { + args["sourceRange"] = map[string]any{ + "sheetId": batchStr(input, "source-sheet-id"), + "a1Notation": sourceRange, + } } if v, ok := input["multi-select"]; ok { args["enableMultiSelect"] = v diff --git a/internal/helpers/sheet_batch_test.go b/internal/helpers/sheet_batch_test.go index f7038d08..edb85887 100644 --- a/internal/helpers/sheet_batch_test.go +++ b/internal/helpers/sheet_batch_test.go @@ -23,7 +23,15 @@ func TestCrossPlatformCoverageSheetBatchOperationTranslationCoversEveryMapping(t "group-state": "fold", } for name, mapping := range batchOpDispatch { - got, err := translateBatchOp(map[string]any{"toolName": name, "input": input}) + opInput := input + if name == "set-dropdown" { + opInput = make(map[string]any, len(input)) + for key, value := range input { + opInput[key] = value + } + delete(opInput, "source-range") + } + got, err := translateBatchOp(map[string]any{"toolName": name, "input": opInput}) if err != nil { t.Errorf("translateBatchOp(%q): %v", name, err) continue diff --git a/internal/helpers/sheet_data.go b/internal/helpers/sheet_data.go index 3cdb77b0..be6e9d2d 100644 --- a/internal/helpers/sheet_data.go +++ b/internal/helpers/sheet_data.go @@ -388,7 +388,10 @@ range update 与合并区域冲突时返回 MERGED_CELLS_CONFLICT 的行为。 csv 带 [row=N] 行号前缀的 CSV 文本 colIndices 列字母映射数组(定位列用 colIndices[j],禁止手数逗号) rowIndices 行号映射数组 - hasMore 是否因 maxChars 截断 + hasMore 目标范围是否还有未返回数据 + truncationReasons 部分返回原因:max_cells / max_chars + resolvedRange 未传 --range 时底层解析出的完整目标范围 + returnedRange 本次实际完整返回的范围 取值模式(--value-render-option): formatted_value 格式化后的展示值(默认),如 ¥1,000.00、2025-06-01 @@ -398,9 +401,14 @@ range update 与合并区域冲突时返回 MERGED_CELLS_CONFLICT 的行为。 与 range read 的区别: - CSV 格式 token 消耗约为 JSON 的 1/3 - 支持选择取值模式 - - 自动防爆(max-chars 截断 + has_more 标志) + - 自动防爆(30,000 单元格 / max-chars 上限 + hasMore 标志) - [row=N] 前缀防止行号计算错误 +当 hasMore=true 时本命令不会自动续读;请结合目标范围和 +returnedRange 从下一行显式传 --range。max_cells 不能通过增大 +--max-chars 解决。收到 forbidden.document.sizeOverLimit 表示工作簿整体 +无法装载,应创建更小副本或拆分工作簿,缩小 --range 不能解决。 + 注意:csv-get 不返回合并单元格结构。查看合并范围请使用 dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 mergedRanges。`, Example: ` dws sheet csv-get --node NODE_ID diff --git a/internal/helpers/sheet_dimension.go b/internal/helpers/sheet_dimension.go index 4ce8d7ba..2b442f02 100644 --- a/internal/helpers/sheet_dimension.go +++ b/internal/helpers/sheet_dimension.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -18,6 +19,16 @@ func sizeTypeEnumHint(dimension string) string { return "pixel / standard" } +func validateDropdownSourceRangeNotation(sourceRange string) error { + if strings.Contains(sourceRange, "!") { + return fmt.Errorf("--source-range 不能包含工作表前缀;请通过 --source-sheet-id 指定来源工作表") + } + if strings.HasPrefix(sourceRange, "=") || strings.Contains(sourceRange, ",") { + return fmt.Errorf("--source-range 必须是单一连续区域,不能是公式、表达式或多区域引用") + } + return nil +} + // newDimensionCmds creates dimension-related commands: insert/delete/update/move/add-dimension, // merge-cells, unmerge-cells, and dropdown commands (set/get/delete-dropdown). func newDimensionCmds() []*cobra.Command { @@ -775,15 +786,23 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 Short: "设置下拉列表", Long: `在钉钉表格的指定单元格范围内设置下拉列表。 -设置后,用户可以在这些单元格中从预定义的选项列表中选择值。 -支持自定义每个选项的颜色和是否允许多选。 +Inline 模式通过 --options 提供静态选项,支持选项颜色。 +SourceRange 模式通过 --source-sheet-id + --source-range 引用同一工作簿内的来源区域, +支持跨工作表、整行和整列引用;读取配置时不会展开来源区域的当前值或颜色。 +两种模式都支持 --multi-select,并且 --options 与 --source-range 必须且只能指定一个。 如果目标范围已存在下拉列表,会被新的配置覆盖。 nodeId 支持传入文档链接 URL 或文档 ID(dentryUuid),系统自动识别。 sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获取。 --options 为 JSON 数组,每个元素包含 value(必填)和 color(可选)。 -选项值不能包含英文逗号。`, +选项值不能包含英文逗号。 + +--source-range 只接受不含工作表前缀的 A1 区域,例如 T1:T3、T:T 或 1:3; +来源工作表由 --source-sheet-id 单独指定。SourceRange 颜色写入暂不支持。 + +已知限制:服务端写入的 SourceRange 不会在工作表重命名或行列插入、删除、移动后 +自动更新;执行这些结构操作后应重新写入下拉来源。`, Example: ` # 设置单选下拉列表 dws sheet set-dropdown --node NODE_ID --sheet-id SHEET_ID --range "A2:A100" \ --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' @@ -793,32 +812,63 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ --multi-select + # 引用同一工作簿内另一工作表的区域作为候选项来源 + dws sheet set-dropdown --node NODE_ID --sheet-id TARGET_SHEET_ID --range "C2:C100" \ + --source-sheet-id SOURCE_SHEET_ID --source-range "T1:T3" + # 使用文档链接 URL dws sheet set-dropdown --node "https://alidocs.dingtalk.com/i/nodes/" \ --sheet-id SHEET_ID --range "C1:C10" --options '[{"value":"是"},{"value":"否"}]'`, RunE: func(cmd *cobra.Command, args []string) error { + if err := validateRequiredFlags(cmd, "node", "sheet-id", "range"); err != nil { + return err + } optionsStr := mustGetFlag(cmd, "options") - var options []map[string]any - if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { - return fmt.Errorf("--options JSON 解析失败: %w", err) + sourceSheetID := mustGetFlag(cmd, "source-sheet-id") + sourceRange := mustGetFlag(cmd, "source-range") + hasOptions := optionsStr != "" + hasSourceRange := sourceRange != "" + if hasOptions == hasSourceRange { + return fmt.Errorf("--options 与 --source-range 必须且只能指定一个") } - if len(options) == 0 { - return fmt.Errorf("--options 至少包含 1 个选项") + if hasSourceRange && sourceSheetID == "" { + return fmt.Errorf("使用 --source-range 时必须同时指定 --source-sheet-id") } - for i, opt := range options { - val, ok := opt["value"].(string) - if !ok || val == "" { - return fmt.Errorf("--options[%d] 缺少必填的 value 字段或 value 为空", i) - } - if strings.Contains(val, ",") { - return fmt.Errorf("--options[%d].value 不能包含英文逗号: %q", i, val) - } + if !hasSourceRange && sourceSheetID != "" { + return fmt.Errorf("使用 --source-sheet-id 时必须同时指定 --source-range") } + toolArgs := map[string]any{ "nodeId": mustGetFlag(cmd, "node"), "sheetId": mustGetFlag(cmd, "sheet-id"), "range": mustGetFlag(cmd, "range"), - "options": options, + } + if hasOptions { + var options []map[string]any + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return fmt.Errorf("--options JSON 解析失败: %w", err) + } + if len(options) == 0 { + return fmt.Errorf("--options 至少包含 1 个选项") + } + for i, opt := range options { + val, ok := opt["value"].(string) + if !ok || val == "" { + return fmt.Errorf("--options[%d] 缺少必填的 value 字段或 value 为空", i) + } + if strings.Contains(val, ",") { + return fmt.Errorf("--options[%d].value 不能包含英文逗号: %q", i, val) + } + } + toolArgs["options"] = options + } else { + if err := validateDropdownSourceRangeNotation(sourceRange); err != nil { + return err + } + toolArgs["sourceRange"] = map[string]any{ + "sheetId": sourceSheetID, + "a1Notation": sourceRange, + } } if multiSelect, _ := cmd.Flags().GetBool("multi-select"); multiSelect { toolArgs["enableMultiSelect"] = true @@ -839,36 +889,54 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 CLIPath: "sheet set-dropdown", PrimaryCLIPath: "sheet set-dropdown", }, - Description: "为指定范围设置下拉列表(可多选、可带颜色)。", + Description: "为指定范围设置 Inline 或 SourceRange 下拉列表(可多选;颜色仅 Inline 支持)。", Interface: &contract.InterfaceSpec{ Mode: "mcp", Availability: "available", Ref: &contract.InterfaceRefSpec{ProductID: "sheet", RPCName: "set_dropdown_lists"}, }, Selection: contract.SelectionSpec{ - AgentSummary: "为指定范围设置下拉列表(可多选、可带颜色)。", + AgentSummary: "为指定范围设置 Inline 或 SourceRange 下拉列表;SourceRange 可跨同一工作簿内的工作表。", UseWhen: []string{"需要给单元格配置可选值下拉约束时"}, AvoidWhen: []string{"查看已有下拉用 get-dropdown;移除下拉用 delete-dropdown"}, - Examples: []string{"dws sheet set-dropdown --node --sheet-id --range \"A2:A100\" --options '[{\"value\":\"选项1\"},{\"value\":\"选项2\"}]'"}, + Examples: []string{ + "dws sheet set-dropdown --node --sheet-id --range \"A2:A100\" --options '[{\"value\":\"选项1\"},{\"value\":\"选项2\"}]'", + "dws sheet set-dropdown --node --sheet-id --range \"B2:B100\" --source-sheet-id --source-range \"T1:T3\"", + }, }, Parameters: []contract.ParamDecl{ {Name: "multi-select", Property: "enableMultiSelect"}, {Name: "node", Property: "nodeId"}, + {Name: "options", Property: "options", Required: boolPtr(false), RequiredWhen: "exactly one of --options or --source-range must be provided"}, + {Name: "source-sheet-id", Property: "sourceRange.sheetId", Required: boolPtr(false), RequiredWhen: "--source-range is provided"}, + {Name: "source-range", Property: "sourceRange.a1Notation", Required: boolPtr(false), RequiredWhen: "exactly one of --options or --source-range must be provided"}, }, }, }) setDropdownCmd.Flags().String("node", "", "表格文档 ID 或 URL (必填)") setDropdownCmd.Flags().String("sheet-id", "", "工作表 ID 或名称 (必填)") setDropdownCmd.Flags().String("range", "", "目标单元格范围,A1 表示法,如 A2:A100 (必填)") - setDropdownCmd.Flags().String("options", "", `下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]'`) + setDropdownCmd.Flags().String("options", "", `Inline 下拉选项 JSON 数组,与 --source-range 二选一,如 '[{"value":"选项1","color":"#ff0000"}]'`) + setDropdownCmd.Flags().String("source-sheet-id", "", "SourceRange 来源工作表 ID;使用 --source-range 时必填") + setDropdownCmd.Flags().String("source-range", "", "SourceRange 来源区域,与 --options 二选一;不含工作表前缀,如 T1:T3、T:T 或 1:3") setDropdownCmd.Flags().Bool("multi-select", false, "是否允许多选(默认单选)") + cli.AnnotateRuntimeFlagFormat(setDropdownCmd, "source-range", "a1-range") + setDropdownCmd.MarkFlagsOneRequired("options", "source-range") + setDropdownCmd.MarkFlagsMutuallyExclusive("options", "source-range") + setDropdownCmd.MarkFlagsRequiredTogether("source-sheet-id", "source-range") + cli.AnnotateRuntimeConstraints(setDropdownCmd, cli.RuntimeSchemaConstraints{ + MutuallyExclusive: [][]string{{"options", "source-range"}}, + RequireOneOf: [][]string{{"options", "source-range"}}, + RequireTogether: [][]string{{"source-sheet-id", "source-range"}}, + }) getDropdownCmd := &cobra.Command{ Use: "get-dropdown", Short: "获取下拉列表配置", Long: `查询钉钉表格指定范围内的下拉列表配置。 -返回范围内所有单元格的下拉列表配置信息,包括选项值和颜色。 +Inline 配置返回 conditionValues 和 options;SourceRange 配置返回 +sourceType/sourceRange/sourceRangeStatus/enableMultiSelect,不展开来源区域的候选值或颜色。 如果范围内存在多个不同的下拉列表配置,会分别返回每组配置及其覆盖的单元格列表。 如果范围内没有设置下拉列表,返回空。 diff --git a/internal/helpers/sheet_dropdown_source_range_test.go b/internal/helpers/sheet_dropdown_source_range_test.go new file mode 100644 index 00000000..af7c1eeb --- /dev/null +++ b/internal/helpers/sheet_dropdown_source_range_test.go @@ -0,0 +1,171 @@ +package helpers + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" +) + +func TestCrossPlatformCoverageSetDropdownSourceRangeCommand(t *testing.T) { + caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"success":true}`}}} + installScriptedCaller(t, caller) + err := executeDimensionCoverage(t, "set-dropdown", + "--node", "node-1", + "--sheet-id", "target-sheet", + "--range", "B2:B100", + "--source-sheet-id", "source-sheet", + "--source-range", "$t$1:$t$3", + "--multi-select", + ) + if err != nil { + t.Fatalf("set SourceRange dropdown: %v", err) + } + if caller.tool != "set_dropdown_lists" { + t.Fatalf("tool = %q, want set_dropdown_lists", caller.tool) + } + if _, exists := caller.args["options"]; exists { + t.Fatalf("SourceRange request must omit options: %#v", caller.args) + } + source, ok := caller.args["sourceRange"].(map[string]any) + if !ok { + t.Fatalf("sourceRange = %#v", caller.args["sourceRange"]) + } + if source["sheetId"] != "source-sheet" || source["a1Notation"] != "$t$1:$t$3" { + t.Fatalf("sourceRange = %#v", source) + } + if caller.args["enableMultiSelect"] != true { + t.Fatalf("enableMultiSelect = %#v", caller.args["enableMultiSelect"]) + } +} + +func TestCrossPlatformCoverageSetDropdownInlineCommandStillOmitsSourceRange(t *testing.T) { + caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"success":true}`}}} + installScriptedCaller(t, caller) + err := executeDimensionCoverage(t, "set-dropdown", + "--node", "node-1", + "--sheet-id", "sheet-1", + "--range", "A1:A3", + "--options", `[{"value":"one","color":"#ff0000"}]`, + ) + if err != nil { + t.Fatalf("set inline dropdown: %v", err) + } + if _, exists := caller.args["sourceRange"]; exists { + t.Fatalf("inline request must omit sourceRange: %#v", caller.args) + } + options, ok := caller.args["options"].([]map[string]any) + if !ok || len(options) != 1 || options[0]["value"] != "one" { + t.Fatalf("options = %#v", caller.args["options"]) + } +} + +func TestCrossPlatformCoverageSetDropdownModeConstraints(t *testing.T) { + base := []string{"--node", "node", "--sheet-id", "sheet", "--range", "A1"} + tests := []struct { + name string + args []string + want string + }{ + {name: "neither", want: "one of the flags"}, + {name: "both", args: []string{"--options", `[{"value":"one"}]`, "--source-sheet-id", "source", "--source-range", "A1:A3"}, want: "none of the others"}, + {name: "source sheet without range", args: []string{"--options", `[{"value":"one"}]`, "--source-sheet-id", "source"}, want: "must all be set"}, + {name: "range without source sheet", args: []string{"--source-range", "A1:A3"}, want: "must all be set"}, + {name: "sheet prefix", args: []string{"--source-sheet-id", "source", "--source-range", "Sheet2!A1:A3"}, want: "不能包含工作表前缀"}, + {name: "formula", args: []string{"--source-sheet-id", "source", "--source-range", "=A1:A3"}, want: "必须是单一连续区域"}, + {name: "multi region", args: []string{"--source-sheet-id", "source", "--source-range", "A1:A3,C1:C3"}, want: "必须是单一连续区域"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + caller := &scriptedToolCaller{} + installScriptedCaller(t, caller) + args := append(append([]string{}, base...), tc.args...) + err := executeDimensionCoverage(t, "set-dropdown", args...) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want contains %q", err, tc.want) + } + if caller.calls != 0 { + t.Fatalf("calls = %d, validation must fail before request", caller.calls) + } + }) + } +} + +func TestCrossPlatformCoverageSetDropdownSchemaConstraints(t *testing.T) { + cmd := dimensionCoverageCommand(t, "set-dropdown") + constraints := runtimeannotate.CommandConstraints(cmd) + want := runtimeannotate.RuntimeSchemaConstraints{ + MutuallyExclusive: [][]string{{"options", "source-range"}}, + RequireOneOf: [][]string{{"options", "source-range"}}, + RequireTogether: [][]string{{"source-sheet-id", "source-range"}}, + } + gotJSON, _ := json.Marshal(constraints) + wantJSON, _ := json.Marshal(runtimeannotate.NormalizeConstraints(want)) + if string(gotJSON) != string(wantJSON) { + t.Fatalf("constraints = %s, want %s", gotJSON, wantJSON) + } +} + +func TestCrossPlatformCoverageBatchSetDropdownSourceRange(t *testing.T) { + got, err := translateBatchOp(map[string]any{ + "toolName": "set-dropdown", + "input": map[string]any{ + "sheet-id": "target-sheet", + "range": "B2:B100", + "source-sheet-id": "source-sheet", + "source-range": "T:T", + "multi-select": true, + }, + }) + if err != nil { + t.Fatalf("translate SourceRange batch op: %v", err) + } + input := got["input"].(map[string]any) + if _, exists := input["options"]; exists { + t.Fatalf("SourceRange batch input must omit options: %#v", input) + } + source := input["sourceRange"].(map[string]any) + if source["sheetId"] != "source-sheet" || source["a1Notation"] != "T:T" { + t.Fatalf("sourceRange = %#v", source) + } + + invalid := []map[string]any{ + {}, + {"options": []any{map[string]any{"value": "one"}}, "source-sheet-id": "source", "source-range": "A1:A3"}, + {"source-range": "A1:A3"}, + {"source-sheet-id": "source", "source-range": "A1:A3", "colors": []any{"#fff"}}, + {"source-sheet-id": "source", "source-range": "Sheet2!A1:A3"}, + } + for _, value := range invalid { + if _, err := translateBatchOp(map[string]any{"toolName": "set-dropdown", "input": value}); err == nil { + t.Errorf("invalid batch SourceRange input %#v returned nil", value) + } + } +} + +func TestCrossPlatformCoverageValidateSourceRangeDataValidation(t *testing.T) { + valid := []any{ + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}}, + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "missing-sheet", "a1Notation": "not-validated-locally"}, "enableMultiSelect": true}, + } + for _, value := range valid { + if err := validateDataValidation(value, "dv"); err != nil { + t.Errorf("valid SourceRange data validation %#v: %v", value, err) + } + } + + invalid := []any{ + map[string]any{"type": "dropdown", "options": []any{map[string]any{"value": "one"}}, "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}}, + map[string]any{"type": "dropdown", "sourceRange": "T1:T3"}, + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"a1Notation": "T1:T3"}}, + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source"}}, + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3", "colors": []any{"#fff"}}}, + map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}, "enableMultiSelect": "yes"}, + } + for _, value := range invalid { + if err := validateDataValidation(value, "dv"); err == nil { + t.Errorf("invalid SourceRange data validation %#v returned nil", value) + } + } +} diff --git a/internal/helpers/sheet_range_ops.go b/internal/helpers/sheet_range_ops.go index 5d6c8217..8bd07dc9 100644 --- a/internal/helpers/sheet_range_ops.go +++ b/internal/helpers/sheet_range_ops.go @@ -26,6 +26,16 @@ func newRangeCmd() *cobra.Command { value 单元格值(内容由 --value-render-option 决定) dataValidation 数据验证配置(下拉列表/复选框),无则为 null hyperlink 单元格级超链接(path/sheet/range),无则省略 +顶层还会返回 rowIndices / colIndices 与完成度字段: + hasMore 目标范围是否还有未返回数据 + truncationReasons 部分返回原因(max_cells) + resolvedRange 未传 --range 时底层解析出的完整目标范围 + returnedRange 本次实际完整返回的范围 + +单次最多返回 30,000 个单元格。hasMore=true 是部分成功,本命令 +不会自动续读;请结合目标范围和 returnedRange 从下一行显式传 +--range。收到 forbidden.document.sizeOverLimit 则表示工作簿整体无法装载, +应创建更小副本或拆分工作簿,缩小 --range 不能解决。 注意:range read/get 不返回合并单元格结构。查看合并范围请使用 dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 mergedRanges。 @@ -157,7 +167,12 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg 2) {"dataValidation":{"type":"none"}} → 显式清除该单元格 DV 3) {"dataValidation":{"type":"dropdown",...}} → 写新 dropdown(覆盖) {"dataValidation":{"type":"checkbox",...}} → 写新 checkbox(覆盖) - dropdown: {"dataValidation":{"type":"dropdown","options":[{"value":"选项1"}],"enableMultiSelect":false}} + Inline dropdown: + {"dataValidation":{"type":"dropdown","options":[{"value":"选项1"}],"enableMultiSelect":false}} + SourceRange dropdown(同一工作簿内可跨工作表;不展开来源 values,不支持 colors): + {"dataValidation":{"type":"dropdown","sourceRange":{"sheetId":"SOURCE_SHEET_ID","a1Notation":"T1:T3"},"enableMultiSelect":false}} + dropdown 的 options 与 sourceRange 必须且只能传一个。SourceRange 支持普通区域、整行和整列; + a1Notation 不带工作表前缀,来源工作表单独写在 sheetId。 checkbox: {"dataValidation":{"type":"checkbox","checked":true}} 可与 text/richText 共存,也可单独使用(如 {dataValidation:{type:"none"}} 仅清除 DV 不写值) @@ -169,6 +184,8 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg - 只设样式或批量刷整片区域样式请用 dws sheet range set-style;写值同时设置少量 cell 样式可用 cellStyles - 目标范围与已有合并区域冲突时,range update 会返回 MERGED_CELLS_CONFLICT;先用 sheet info 查看 mergedRanges,取消合并后写入,必要时再重新合并 - csv-put 的合并处理不同:目标区域含合并单元格时会打散合并并写入 CSV 值或公式 + - SourceRange 当前不会随工作表重命名或行列结构调整自动更新;执行结构操作后应重新写入 + - 同一 cell 的 value/style 已写入但 SourceRange 校验失败时,服务端可返回 success=true,并通过 message 明确下拉未创建;必须检查 message,必要时重新读取确认 - 清空整片区域请用 dws sheet range clear`, Example: ` # 写入文本 dws sheet range update --node NODE_ID --sheet-id SHEET_ID --range "A1:B2" \ diff --git a/internal/helpers/sheet_read_completion_test.go b/internal/helpers/sheet_read_completion_test.go new file mode 100644 index 00000000..dca4bfdc --- /dev/null +++ b/internal/helpers/sheet_read_completion_test.go @@ -0,0 +1,202 @@ +package helpers + +import ( + "bytes" + "encoding/json" + "errors" + "strings" + "testing" +) + +func executeSheetReadCompletionCommand(t *testing.T, caller *scriptedToolCaller, args ...string) (*bytes.Buffer, error) { + t.Helper() + installScriptedCaller(t, caller) + installSheetProductArgs(t) + + stdout := &bytes.Buffer{} + deps.Out.w = stdout + + cmd := newSheetCommand() + cmd.SilenceErrors = true + cmd.SilenceUsage = true + cmd.SetArgs(args) + return stdout, cmd.Execute() +} + +func decodeSheetReadOutput(t *testing.T, stdout *bytes.Buffer) map[string]any { + t.Helper() + var got map[string]any + if err := json.Unmarshal(stdout.Bytes(), &got); err != nil { + t.Fatalf("decode sheet read output %q: %v", stdout.String(), err) + } + return got +} + +func assertSheetReadCompletionMetadata(t *testing.T, got map[string]any) { + t.Helper() + if hasMore, ok := got["hasMore"].(bool); !ok || !hasMore { + t.Fatalf("hasMore = %#v, want true", got["hasMore"]) + } + if got["resolvedRange"] != "A1:A30001" { + t.Fatalf("resolvedRange = %#v", got["resolvedRange"]) + } + if got["returnedRange"] != "A1:A30000" { + t.Fatalf("returnedRange = %#v", got["returnedRange"]) + } + reasons, ok := got["truncationReasons"].([]any) + if !ok || len(reasons) == 0 || reasons[0] != "max_cells" { + t.Fatalf("truncationReasons = %#v", got["truncationReasons"]) + } +} + +// csv-get intentionally projects the MCP response rather than inventing a +// client-side page model. Keep all completion metadata visible and make one +// request only, even when the server reports a partial success. +func TestSheetCSVGetPreservesCompletionMetadataWithoutAutoPagination(t *testing.T) { + const response = `{"csv":"[row=1]1\n","colIndices":["A"],"rowIndices":[1],"hasMore":true,"truncationReasons":["max_cells","max_chars"],"resolvedRange":"A1:A30001","returnedRange":"A1:A30000","message":"partial"}` + caller := &scriptedToolCaller{ + format: "raw", + steps: []scriptedToolStep{{text: response}}, + } + + stdout, err := executeSheetReadCompletionCommand(t, caller, + "csv-get", "--node", "NODE") + if err != nil { + t.Fatalf("csv-get: %v", err) + } + if caller.calls != 1 || caller.tool != "get_range_as_csv" { + t.Fatalf("calls/tool = %d/%q, want one get_range_as_csv call", caller.calls, caller.tool) + } + if _, exists := caller.args["range"]; exists { + t.Fatalf("omitted range was unexpectedly synthesized by the CLI: %#v", caller.args["range"]) + } + if got := strings.TrimSpace(stdout.String()); got != response { + t.Fatalf("raw csv-get response changed:\n got: %s\nwant: %s", got, response) + } + + got := decodeSheetReadOutput(t, stdout) + assertSheetReadCompletionMetadata(t, got) + reasons := got["truncationReasons"].([]any) + if len(reasons) != 2 || reasons[1] != "max_chars" { + t.Fatalf("truncationReasons = %#v, want max_cells and max_chars", reasons) + } +} + +// range read parses the get_cell_infos envelope to remove empty per-cell +// schema shells. That cleanup must not discard new top-level completion fields. +func TestSheetRangeReadPreservesCompletionMetadataWithoutAutoPagination(t *testing.T) { + const response = `{"cells":[[{"value":"1","dataValidation":{"type":null},"hyperlink":{"url":null}}]],"colIndices":["A"],"rowIndices":[1],"hasMore":true,"truncationReasons":["max_cells"],"resolvedRange":"A1:A30001","returnedRange":"A1:A30000","message":"partial"}` + caller := &scriptedToolCaller{ + format: "json", + steps: []scriptedToolStep{{text: response}}, + } + + stdout, err := executeSheetReadCompletionCommand(t, caller, + "range", "read", "--node", "NODE") + if err != nil { + t.Fatalf("range read: %v", err) + } + if caller.calls != 1 || caller.tool != "get_cell_infos" { + t.Fatalf("calls/tool = %d/%q, want one get_cell_infos call", caller.calls, caller.tool) + } + if _, exists := caller.args["range"]; exists { + t.Fatalf("omitted range was unexpectedly synthesized by the CLI: %#v", caller.args["range"]) + } + + got := decodeSheetReadOutput(t, stdout) + assertSheetReadCompletionMetadata(t, got) + cells := got["cells"].([]any) + row := cells[0].([]any) + cell := row[0].(map[string]any) + if _, exists := cell["dataValidation"]; exists { + t.Fatalf("empty dataValidation shell was not removed: %#v", cell) + } + if _, exists := cell["hyperlink"]; exists { + t.Fatalf("empty hyperlink shell was not removed: %#v", cell) + } +} + +// sheet +read calls get_cell_infos through this direct passthrough instead of +// callMCPToolCellInfos. Lock that third read surface to the same raw envelope +// contract and to one request only. +func TestSheetCellInfosDirectPassthroughPreservesCompletionMetadata(t *testing.T) { + const response = `{"cells":[[{"value":"1"}]],"colIndices":["A"],"rowIndices":[1],"hasMore":true,"truncationReasons":["max_cells"],"resolvedRange":"A1:A30001","returnedRange":"A1:A30000","message":"partial"}` + caller := &scriptedToolCaller{ + format: "raw", + steps: []scriptedToolStep{{text: response}}, + } + installScriptedCaller(t, caller) + stdout := &bytes.Buffer{} + deps.Out.w = stdout + + if err := CallMCPToolOnServer("sheet", "get_cell_infos", map[string]any{"nodeId": "NODE"}); err != nil { + t.Fatalf("direct get_cell_infos: %v", err) + } + if caller.calls != 1 || caller.server != "sheet" || caller.tool != "get_cell_infos" { + t.Fatalf("calls/server/tool = %d/%q/%q", caller.calls, caller.server, caller.tool) + } + if got := strings.TrimSpace(stdout.String()); got != response { + t.Fatalf("raw get_cell_infos response changed:\n got: %s\nwant: %s", got, response) + } + assertSheetReadCompletionMetadata(t, decodeSheetReadOutput(t, stdout)) +} + +// Large-CP failure is not a pageable success. Both the raw csv-get path and +// both get_cell_infos paths must retain the backend code and safe user message +// exactly, matching the generic get_all_sheets error-classification path. +func TestSheetReadPreservesWorkbookSizeOverLimitError(t *testing.T) { + const response = `{"success":false,"errorCode":"forbidden.document.sizeOverLimit","errorMessage":"The workbook data is too large to process. Use a smaller copy or split the workbook, then try again."}` + tests := []struct { + name string + args []string + tool string + direct bool + }{ + {name: "csv-get", args: []string{"csv-get", "--node", "NODE"}, tool: "get_range_as_csv"}, + {name: "range-read", args: []string{"range", "read", "--node", "NODE"}, tool: "get_cell_infos"}, + {name: "cell-infos-direct", tool: "get_cell_infos", direct: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + caller := &scriptedToolCaller{ + format: "json", + steps: []scriptedToolStep{{text: response}}, + } + var stdout *bytes.Buffer + var err error + if test.direct { + installScriptedCaller(t, caller) + stdout = &bytes.Buffer{} + deps.Out.w = stdout + err = CallMCPToolOnServer("sheet", test.tool, map[string]any{"nodeId": "NODE"}) + } else { + stdout, err = executeSheetReadCompletionCommand(t, caller, test.args...) + } + if err == nil { + t.Fatal("large-CP response was accepted as success") + } + var cliErr *CLIError + if !errors.As(err, &cliErr) || cliErr.Code != CodeMCPToolError { + t.Fatalf("error = %T %v, want MCP tool error", err, err) + } + if cliErr.Message != response { + t.Fatalf("backend error payload changed:\n got: %s\nwant: %s", cliErr.Message, response) + } + for _, want := range []string{ + "forbidden.document.sizeOverLimit", + "The workbook data is too large to process.", + } { + if !strings.Contains(err.Error(), want) { + t.Errorf("error %q does not preserve %q", err, want) + } + } + if stdout.Len() != 0 { + t.Fatalf("failure wrote a success body: %q", stdout.String()) + } + if caller.calls != 1 || caller.tool != test.tool { + t.Fatalf("calls/tool = %d/%q, want one %s call", caller.calls, caller.tool, test.tool) + } + }) + } +} diff --git a/internal/helpers/sheet_validate.go b/internal/helpers/sheet_validate.go index e40a7cb3..f32e6836 100644 --- a/internal/helpers/sheet_validate.go +++ b/internal/helpers/sheet_validate.go @@ -239,22 +239,48 @@ func validateDataValidation(dvRaw any, path string) error { } switch dvType { case "dropdown": - optionsRaw, ok := dv["options"] - if !ok { - return fmt.Errorf("%s: type=dropdown 必须包含 options 数组", path) + optionsRaw, hasOptions := dv["options"] + sourceRangeRaw, hasSourceRange := dv["sourceRange"] + hasOptions = hasOptions && optionsRaw != nil + hasSourceRange = hasSourceRange && sourceRangeRaw != nil + if hasOptions == hasSourceRange { + return fmt.Errorf("%s: type=dropdown 的 options 与 sourceRange 必须且只能包含一个", path) } - options, ok := optionsRaw.([]any) - if !ok || len(options) == 0 { - return fmt.Errorf("%s.options 必须为非空数组", path) - } - for i, opt := range options { - optMap, ok := opt.(map[string]any) - if !ok { - return fmt.Errorf("%s.options[%d] 必须为 object(如 {\"value\":\"选项\"})", path, i) + if hasOptions { + options, ok := optionsRaw.([]any) + if !ok || len(options) == 0 { + return fmt.Errorf("%s.options 必须为非空数组", path) } - val, ok := optMap["value"].(string) - if !ok || val == "" { - return fmt.Errorf("%s.options[%d].value 必须为非空字符串", path, i) + for i, opt := range options { + optMap, ok := opt.(map[string]any) + if !ok { + return fmt.Errorf("%s.options[%d] 必须为 object(如 {\"value\":\"选项\"})", path, i) + } + val, ok := optMap["value"].(string) + if !ok || val == "" { + return fmt.Errorf("%s.options[%d].value 必须为非空字符串", path, i) + } + } + } else { + sourceRange, ok := sourceRangeRaw.(map[string]any) + if !ok { + return fmt.Errorf("%s.sourceRange 必须为 object", path) + } + if _, exists := sourceRange["colors"]; exists { + return fmt.Errorf("%s.sourceRange.colors 暂不支持", path) + } + sheetID, ok := sourceRange["sheetId"].(string) + if !ok || strings.TrimSpace(sheetID) == "" { + return fmt.Errorf("%s.sourceRange.sheetId 必须为非空字符串", path) + } + a1Notation, ok := sourceRange["a1Notation"].(string) + if !ok || strings.TrimSpace(a1Notation) == "" { + return fmt.Errorf("%s.sourceRange.a1Notation 必须为非空字符串", path) + } + } + if multi, exists := dv["enableMultiSelect"]; exists { + if _, ok := multi.(bool); !ok { + return fmt.Errorf("%s.enableMultiSelect 必须为 boolean", path) } } case "checkbox": diff --git a/skills/mono/references/products/sheet/sheet-batch-operations.md b/skills/mono/references/products/sheet/sheet-batch-operations.md index 8d922bec..4654840f 100644 --- a/skills/mono/references/products/sheet/sheet-batch-operations.md +++ b/skills/mono/references/products/sheet/sheet-batch-operations.md @@ -83,7 +83,8 @@ Example: {"toolName":"range update","input":{"sheet-id":"Sheet1","range":"A1","values":[[{"type":"text","text":"hello"}]]}}, {"toolName":"merge-cells","input":{"sheet-id":"Sheet1","range":"A1:B1","merge-type":"mergeAll"}}, {"toolName":"update-dimension","input":{"sheet-id":"Sheet1","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}}, - {"toolName":"group-dimension","input":{"sheet-id":"Sheet1","range":"3:7","group-state":"expand"}} + {"toolName":"group-dimension","input":{"sheet-id":"Sheet1","range":"3:7","group-state":"expand"}}, + {"toolName":"set-dropdown","input":{"sheet-id":"Sheet1","range":"C2:C100","source-sheet-id":"SourceSheet","source-range":"T1:T3"}} ]' dws sheet batch-update --node --continue-on-error --operations '[...]' Flags: @@ -101,6 +102,7 @@ Notes: - operations 最多 20 条 - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) - `csv-put` 子操作与独立命令语义一致:CSV 字段值以 `=` 开头时按公式解析;前加单引号时写入以 `=` 开头的字面文本 + - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。SourceRange 颜色写入暂不支持,且不会随 sheet 重命名或行列结构操作自动更新 - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 - `group-dimension` 在 batch 中只适合默认展开分组;需要 `--group-state fold` 时请使用独立 `dws sheet group-dimension` - `table-put` 不支持放进 batch-update;结构化 table 请用独立 `dws sheet table-put` diff --git a/skills/mono/references/products/sheet/sheet-dropdown.md b/skills/mono/references/products/sheet/sheet-dropdown.md index 0b135aa4..b6e2e658 100644 --- a/skills/mono/references/products/sheet/sheet-dropdown.md +++ b/skills/mono/references/products/sheet/sheet-dropdown.md @@ -7,6 +7,7 @@ 用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": - 设置下拉列表 → `set-dropdown` - 设置多选下拉 → `set-dropdown --multi-select` +- 引用单元格区域作为候选项 → `set-dropdown --source-sheet-id ... --source-range ...` 用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": - 获取下拉列表配置 → `get-dropdown` @@ -29,18 +30,25 @@ Example: dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ --multi-select + + # 引用同一工作簿内另一工作表的区域作为候选项来源 + dws sheet set-dropdown --node --sheet-id --range "C2:C100" \ + --source-sheet-id --source-range "T1:T3" Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) - --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' - --multi-select 是否允许多选(默认单选) + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) + --options string Inline 下拉选项 JSON 数组,与 --source-range 二选一 + --source-sheet-id string SourceRange 来源工作表 ID,与 --source-range 同时指定 + --source-range string SourceRange 来源区域,与 --options 二选一;不带工作表前缀 + --multi-select 是否允许多选(默认单选) ``` -在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 -- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 +在指定单元格范围内设置下拉列表。Inline 模式直接存储选项;SourceRange 模式引用同一工作簿内的来源区域,可跨工作表,并支持普通区域、整行和整列。 +- **用途**:为单元格配置静态选项或区域来源下拉,两种模式都支持多选;颜色仅 Inline 支持。 - **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 -- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 +- **注意**:`--options` 与 `--source-range` 必须且只能指定一个。`--source-range` 只写 `T1:T3`、`T:T`、`1:3` 这类 A1 区域,来源工作表通过 `--source-sheet-id` 单独指定;不接受工作表前缀、公式或多区域。SourceRange 颜色写入暂不支持。 +- **静态引用限制**:服务端写入的 SourceRange 不会随工作表重命名或行列插入、删除、移动自动更新;结构操作后应重新写入该下拉来源。 ### 获取下拉列表配置 ``` @@ -55,10 +63,10 @@ Flags: --range string 查询范围,A1 表示法,如 A1:A100 (必填) ``` -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +查询指定范围内的下拉列表配置信息。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`sourceRange:{sheetId,a1Notation}`、`enableMultiSelect` 和 `ranges`,不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 ### 删除下拉列表 ``` @@ -81,14 +89,15 @@ Flags: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | -| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | +| `set-dropdown` | `range` 实际设置范围、`enableMultiSelect` 是否多选;仅 Inline 模式返回 `optionCount` | 确认下拉列表设置成功 | +| `get-dropdown` | `hasDropdown`、`dataValidations`;按 `sourceType` 区分 Inline 与 SourceRange | 查看已有下拉配置 | | `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | ## 注意事项 - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `set-dropdown` 的 Inline 模式使用 `--options`,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB`);SourceRange 模式使用 `--source-sheet-id` + `--source-range`。两种模式均可用 `--multi-select`,并会覆盖目标范围已有下拉 +- SourceRange 当前是静态引用,不会随 sheet 重命名或行列结构操作自动调整;颜色写入暂不支持 +- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,不回退展开候选值 - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/mono/references/products/sheet/sheet-read-data.md b/skills/mono/references/products/sheet/sheet-read-data.md index 0e2ae054..4963fc2a 100644 --- a/skills/mono/references/products/sheet/sheet-read-data.md +++ b/skills/mono/references/products/sheet/sheet-read-data.md @@ -13,7 +13,7 @@ | 读取目的 | 推荐命令 | 说明 | |---------|---------|------| -| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 maxChars 防爆 | +| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 30,000 单元格和 maxChars 防爆 | | 按 table/dataframe 协议读取 | `table-get` | 返回 `columns` / `data` / `dtypes` / `formats`;默认首行为表头 | | 查看数据验证配置(下拉/复选框) | `range read` | 返回 per-cell 结构,含 dataValidation | | 查看单元格样式(背景色/字体/对齐等) | `range read` | 返回 per-cell 结构,含 cellStyles(仅显式设置的样式) | @@ -52,7 +52,10 @@ Flags: - `csv` — CSV 文本,每逻辑行前加 `[row=N]` 前缀标注真实表格行号。行号一律从此前缀读取,禁止手算 - `colIndices` — 列字母映射数组(如 `["A","B","C"]`)。定位列字母用 `colIndices[j]`,禁止手数逗号 - `rowIndices` — 行号映射数组(如 `[1,2,3]`) -- `hasMore` — 是否因 maxChars 截断。为 true 时需要调整 `--range` 继续分页读取 +- `hasMore` — 完整目标范围是否还有未返回数据。`true` 是部分成功,不能宣称已读完 +- `truncationReasons` — 部分返回原因。`max_cells` 表示命中单次 30,000 单元格上限;`max_chars` 表示命中 CSV 字符上限,两者可同时出现 +- `resolvedRange` — 仅在未传 `--range` 时返回,表示底层解析出的完整目标范围 +- `returnedRange` — 本次实际完整返回的精确范围;它不是服务端续读游标 `csv-get` 不返回合并单元格结构。若 CSV 中出现合并区域的非左上角单元格为空,不能据此判断该区域"无内容";需要先用 `dws sheet info --node --sheet-id --format json` 读取 `mergedRanges`,再结合左上角单元格理解合并区域语义。 @@ -63,10 +66,15 @@ Flags: | `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | | `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | -**大表分批读取**:当 `hasMore=true` 或数据量很大时,按行窗口分批: -- 先通过 `info` 获取 `nonEmptyRange.range`,或用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定 A1 边界 -- 分批读取:`--range "A1:J500"`、`--range "A501:J1000"` …… -- 单次建议 ≤5000 单元格 +**大表分批读取**:`csv-get` 不会自动发起后续请求。当 `hasMore=true` 时: +- 显式传了 `--range` 时,该范围是完整目标;未传时,以 `resolvedRange` 为完整目标 +- 根据 `returnedRange` 的结束行,从下一行开始显式构造新的 `--range`;例如目标是 `A1:A30001`、本次返回 `A1:A30000`,则继续读 `A30001:A30001` +- `max_cells` 不能通过增大 `--max-chars` 解决;`max_chars` 且没有 `returnedRange` 时,增大 `--max-chars` 或缩小列范围 +- 上述完成度协议覆盖未传 `--range` 和显式有限矩形范围;`A:A`、`1:1` 等非有限范围可能不返回完成度字段,建议先用 `sheet info` 的 `nonEmptyRange` 换算成有限范围再读 +- 即使未命中硬上限,单次仍建议控制在 5000 单元格以内,减少超时和 token 消耗 + +**工作簿过大失败与部分成功不同**:收到顶层 +`errorCode=forbidden.document.sizeOverLimit` 和对应 `errorMessage` 时,表示工作簿整体无法装载。应停止读取并提示创建更小副本或拆分工作簿;缩小 `--range` 不能解决这类错误。不要把它当成 `hasMore=true` 的可续读结果。 ### 以 table/dataframe 协议读取结构化数据 ``` @@ -130,6 +138,8 @@ Flags: ``` **返回字段说明**: +- `rowIndices` / `colIndices` — 本次返回的真实行号与列字母映射 +- `hasMore` / `truncationReasons` / `resolvedRange` / `returnedRange` — 语义与 `csv-get` 相同;`range read` 的 `truncationReasons` 主要为 `max_cells` - `cells` — 二维数组,第一维为行,第二维为列。每个元素为 per-cell 对象,字段如下: | 字段 | 类型 | 是否必有 | 说明 | @@ -146,10 +156,16 @@ Flags: | type | 字段 | 说明 | |------|------|------| -| `dropdown` | `options: [{value: string, color?: string}]` | 下拉选项列表 | -| `dropdown` | `enableMultiSelect: boolean` | 是否允许多选 | +| `dropdown` Inline | `sourceType: "inline"` | 静态选项模式 | +| `dropdown` Inline | `options: [{value: string, color?: string}]` | 下拉选项列表 | +| `dropdown` Inline | `enableMultiSelect: boolean` | 是否允许多选 | +| `dropdown` SourceRange | `sourceType: "sourceRange"` | 区域来源模式 | +| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 同一工作簿内的来源工作表和规范化 A1 区域 | +| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置 | | `checkbox` | `checked: boolean` | 当前勾选状态 | +SourceRange 模式只返回引用元数据,不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。 + **hyperlink 结构**: | type | 字段 | 说明 | @@ -216,7 +232,7 @@ Flags: **公式校验建议**:写公式后不要只看写入返回结果。先用 `formula` 模式确认公式文本已落表,再运行 `formula-verify` 聚合扫描错误;关键业务数值继续用 `raw_value` 抽样对账。详见 [sheet-formula](./sheet-formula.md)。 -**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: +**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分批读取策略,每次都必须检查 `hasMore`: - 先通过 `info` 获取 `nonEmptyRange.range`,或用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定 A1 边界 - 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… - 避免不传 `--range` 直接读取整个大工作表 @@ -232,7 +248,7 @@ dws sheet list --node --format json # 2. 查看工作表详情(行列数、最后非空位置、mergedRanges 等) dws sheet info --node --sheet-id --format json -# 3. 读取全部数据 +# 3. 读取数据(只有 hasMore=false 才代表目标范围已完整返回) dws sheet range read --node --sheet-id --format json # 4. 读取指定区域 @@ -251,6 +267,8 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 - `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 - `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) +- ★ `csv-get` 和 `range read` 都必须检查 `hasMore`;`true` 时结合目标范围与 `returnedRange` 显式分批,CLI 不会自动续读 +- ★ `forbidden.document.sizeOverLimit` 是工作簿装载失败,不是 30,000 单元格部分成功;缩小 `--range` 不能修复 - ★ `csv-get` / `range read` / `range get` 不返回合并单元格结构;查看合并范围必须用 `sheet info` 的 `mergedRanges` - `table-get` 返回 table/dataframe 结构,适合需要 `columns` / `data` / `dtypes` / `formats` 的场景;它不返回 per-cell 元数据,也不替代 `range read` - ★ 大整数和长数字标识符回读校验:精确 ID 应是字符串 / `object`;若看到 `float64`,说明它已经按数值路径处理,超过 `9007199254740991` 时可能已丢精度 diff --git a/skills/mono/references/products/sheet/sheet-write-data.md b/skills/mono/references/products/sheet/sheet-write-data.md index eaa5e695..d2e61e65 100644 --- a/skills/mono/references/products/sheet/sheet-write-data.md +++ b/skills/mono/references/products/sheet/sheet-write-data.md @@ -386,9 +386,14 @@ Flags: **dropdown(下拉列表)**: ```json { "type": "text", "text": "High", "dataValidation": { "type": "dropdown", "options": [{"value":"High","color":"#00ff00"},{"value":"Low","color":"#ff0000"}], "enableMultiSelect": false } } +{ "dataValidation": { "type": "dropdown", "sourceRange": {"sheetId":"SOURCE_SHEET_ID","a1Notation":"T1:T3"}, "enableMultiSelect": false } } ``` -- `options`:必填,`[{value, color?}]` 数组 +- `options` 与 `sourceRange` 必须且只能提供一个 +- Inline:`options` 为非空 `[{value, color?}]` 数组 +- SourceRange:`sourceRange` 为 `{sheetId,a1Notation}`;可引用同一工作簿内其他工作表,支持普通区域、整行和整列。`a1Notation` 不带工作表前缀,不接受公式或多区域;颜色写入暂不支持 - `enableMultiSelect`:可选,是否多选,默认 false +- SourceRange 当前不会随工作表重命名或行列插入、删除、移动自动更新,结构操作后应重新写入 +- 复合 cell 写入中,如果值或样式已成功落盘但 SourceRange 校验失败,服务端可返回 `success:true` 并通过 `message` 说明下拉未创建;必须检查 `message` 并按需回读 **checkbox(复选框)**: ```json diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md index 8d922bec..4654840f 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md @@ -83,7 +83,8 @@ Example: {"toolName":"range update","input":{"sheet-id":"Sheet1","range":"A1","values":[[{"type":"text","text":"hello"}]]}}, {"toolName":"merge-cells","input":{"sheet-id":"Sheet1","range":"A1:B1","merge-type":"mergeAll"}}, {"toolName":"update-dimension","input":{"sheet-id":"Sheet1","dimension":"ROWS","start-index":"1","length":1,"pixel-size":40}}, - {"toolName":"group-dimension","input":{"sheet-id":"Sheet1","range":"3:7","group-state":"expand"}} + {"toolName":"group-dimension","input":{"sheet-id":"Sheet1","range":"3:7","group-state":"expand"}}, + {"toolName":"set-dropdown","input":{"sheet-id":"Sheet1","range":"C2:C100","source-sheet-id":"SourceSheet","source-range":"T1:T3"}} ]' dws sheet batch-update --node --continue-on-error --operations '[...]' Flags: @@ -101,6 +102,7 @@ Notes: - operations 最多 20 条 - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) - `csv-put` 子操作与独立命令语义一致:CSV 字段值以 `=` 开头时按公式解析;前加单引号时写入以 `=` 开头的字面文本 + - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。SourceRange 颜色写入暂不支持,且不会随 sheet 重命名或行列结构操作自动更新 - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 - `group-dimension` 在 batch 中只适合默认展开分组;需要 `--group-state fold` 时请使用独立 `dws sheet group-dimension` - `table-put` 不支持放进 batch-update;结构化 table 请用独立 `dws sheet table-put` diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md b/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md index 0b135aa4..b6e2e658 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md @@ -7,6 +7,7 @@ 用户说"设置下拉列表/下拉选项/下拉菜单/添加下拉/配置下拉": - 设置下拉列表 → `set-dropdown` - 设置多选下拉 → `set-dropdown --multi-select` +- 引用单元格区域作为候选项 → `set-dropdown --source-sheet-id ... --source-range ...` 用户说"查看下拉列表/获取下拉配置/下拉列表有哪些选项": - 获取下拉列表配置 → `get-dropdown` @@ -29,18 +30,25 @@ Example: dws sheet set-dropdown --node --sheet-id --range "B2:B50" \ --options '[{"value":"高","color":"#ff0000"},{"value":"中","color":"#ffaa00"},{"value":"低","color":"#00ff00"}]' \ --multi-select + + # 引用同一工作簿内另一工作表的区域作为候选项来源 + dws sheet set-dropdown --node --sheet-id --range "C2:C100" \ + --source-sheet-id --source-range "T1:T3" Flags: - --node string 表格文档 ID 或 URL (必填) - --sheet-id string 工作表 ID 或名称 (必填) - --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) - --options string 下拉选项 JSON 数组 (必填),如 '[{"value":"选项1","color":"#ff0000"}]' - --multi-select 是否允许多选(默认单选) + --node string 表格文档 ID 或 URL (必填) + --sheet-id string 工作表 ID 或名称 (必填) + --range string 目标单元格范围,A1 表示法,如 A2:A100 (必填) + --options string Inline 下拉选项 JSON 数组,与 --source-range 二选一 + --source-sheet-id string SourceRange 来源工作表 ID,与 --source-range 同时指定 + --source-range string SourceRange 来源区域,与 --options 二选一;不带工作表前缀 + --multi-select 是否允许多选(默认单选) ``` -在指定单元格范围内设置下拉列表。设置后用户可从预定义选项中选择值。 -- **用途**:为单元格配置下拉列表,支持自定义选项颜色和多选。 +在指定单元格范围内设置下拉列表。Inline 模式直接存储选项;SourceRange 模式引用同一工作簿内的来源区域,可跨工作表,并支持普通区域、整行和整列。 +- **用途**:为单元格配置静态选项或区域来源下拉,两种模式都支持多选;颜色仅 Inline 支持。 - **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 -- **注意**:选项值不能包含英文逗号;如果目标范围已存在下拉列表,会被新配置覆盖。 +- **注意**:`--options` 与 `--source-range` 必须且只能指定一个。`--source-range` 只写 `T1:T3`、`T:T`、`1:3` 这类 A1 区域,来源工作表通过 `--source-sheet-id` 单独指定;不接受工作表前缀、公式或多区域。SourceRange 颜色写入暂不支持。 +- **静态引用限制**:服务端写入的 SourceRange 不会随工作表重命名或行列插入、删除、移动自动更新;结构操作后应重新写入该下拉来源。 ### 获取下拉列表配置 ``` @@ -55,10 +63,10 @@ Flags: --range string 查询范围,A1 表示法,如 A1:A100 (必填) ``` -查询指定范围内的下拉列表配置信息,包括选项值、颜色和是否多选。 +查询指定范围内的下拉列表配置信息。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组,相同选项的单元格聚合为一组,每组包含 `conditionValues`(选项值)、`ranges`(覆盖范围)、`options`(含 `enableMultiSelect` 和 `colorValueMap`)。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`sourceRange:{sheetId,a1Notation}`、`enableMultiSelect` 和 `ranges`,不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 ### 删除下拉列表 ``` @@ -81,14 +89,15 @@ Flags: | 操作 | 从返回中提取 | 用于 | |------|-------------|------| -| `set-dropdown` | `range` 实际设置范围、`optionCount` 选项数量、`enableMultiSelect` 是否多选 | 确认下拉列表设置成功 | -| `get-dropdown` | `hasDropdown` 是否存在下拉、`dataValidations` 下拉配置列表(含 `conditionValues`、`ranges`、`options`) | 查看已有下拉配置 | +| `set-dropdown` | `range` 实际设置范围、`enableMultiSelect` 是否多选;仅 Inline 模式返回 `optionCount` | 确认下拉列表设置成功 | +| `get-dropdown` | `hasDropdown`、`dataValidations`;按 `sourceType` 区分 Inline 与 SourceRange | 查看已有下拉配置 | | `delete-dropdown` | `range` 实际删除范围 | 确认下拉列表删除完成 | | `list` | 工作表的 `sheetId` | info / range read / range update / find 的 --sheet-id | ## 注意事项 - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) -- `set-dropdown` 在指定范围内设置下拉列表,`--options` 为 JSON 数组,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB` 格式)。选项值不能包含英文逗号。`--multi-select` 启用多选模式。如果目标范围已存在下拉列表,会被新配置覆盖 -- `get-dropdown` 查询指定范围内的下拉列表配置,返回 `dataValidations` 数组,相同选项的单元格聚合为一组。无下拉列表时 `hasDropdown` 为 false +- `set-dropdown` 的 Inline 模式使用 `--options`,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB`);SourceRange 模式使用 `--source-sheet-id` + `--source-range`。两种模式均可用 `--multi-select`,并会覆盖目标范围已有下拉 +- SourceRange 当前是静态引用,不会随 sheet 重命名或行列结构操作自动调整;颜色写入暂不支持 +- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,不回退展开候选值 - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md b/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md index 0e2ae054..4963fc2a 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md @@ -13,7 +13,7 @@ | 读取目的 | 推荐命令 | 说明 | |---------|---------|------| -| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 maxChars 防爆 | +| 快速查看纯值、数据分析、大表分批读取 | `csv-get` | CSV 格式,token 消耗约为 JSON 的 1/3,内置 30,000 单元格和 maxChars 防爆 | | 按 table/dataframe 协议读取 | `table-get` | 返回 `columns` / `data` / `dtypes` / `formats`;默认首行为表头 | | 查看数据验证配置(下拉/复选框) | `range read` | 返回 per-cell 结构,含 dataValidation | | 查看单元格样式(背景色/字体/对齐等) | `range read` | 返回 per-cell 结构,含 cellStyles(仅显式设置的样式) | @@ -52,7 +52,10 @@ Flags: - `csv` — CSV 文本,每逻辑行前加 `[row=N]` 前缀标注真实表格行号。行号一律从此前缀读取,禁止手算 - `colIndices` — 列字母映射数组(如 `["A","B","C"]`)。定位列字母用 `colIndices[j]`,禁止手数逗号 - `rowIndices` — 行号映射数组(如 `[1,2,3]`) -- `hasMore` — 是否因 maxChars 截断。为 true 时需要调整 `--range` 继续分页读取 +- `hasMore` — 完整目标范围是否还有未返回数据。`true` 是部分成功,不能宣称已读完 +- `truncationReasons` — 部分返回原因。`max_cells` 表示命中单次 30,000 单元格上限;`max_chars` 表示命中 CSV 字符上限,两者可同时出现 +- `resolvedRange` — 仅在未传 `--range` 时返回,表示底层解析出的完整目标范围 +- `returnedRange` — 本次实际完整返回的精确范围;它不是服务端续读游标 `csv-get` 不返回合并单元格结构。若 CSV 中出现合并区域的非左上角单元格为空,不能据此判断该区域"无内容";需要先用 `dws sheet info --node --sheet-id --format json` 读取 `mergedRanges`,再结合左上角单元格理解合并区域语义。 @@ -63,10 +66,15 @@ Flags: | `raw_value` | 原始值(如 1000、45808) | 数据处理、计算 | | `formula` | 公式文本(如 =SUM(A1:A10)),无公式时回退原始值 | 查看/复制公式 | -**大表分批读取**:当 `hasMore=true` 或数据量很大时,按行窗口分批: -- 先通过 `info` 获取 `nonEmptyRange.range`,或用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定 A1 边界 -- 分批读取:`--range "A1:J500"`、`--range "A501:J1000"` …… -- 单次建议 ≤5000 单元格 +**大表分批读取**:`csv-get` 不会自动发起后续请求。当 `hasMore=true` 时: +- 显式传了 `--range` 时,该范围是完整目标;未传时,以 `resolvedRange` 为完整目标 +- 根据 `returnedRange` 的结束行,从下一行开始显式构造新的 `--range`;例如目标是 `A1:A30001`、本次返回 `A1:A30000`,则继续读 `A30001:A30001` +- `max_cells` 不能通过增大 `--max-chars` 解决;`max_chars` 且没有 `returnedRange` 时,增大 `--max-chars` 或缩小列范围 +- 上述完成度协议覆盖未传 `--range` 和显式有限矩形范围;`A:A`、`1:1` 等非有限范围可能不返回完成度字段,建议先用 `sheet info` 的 `nonEmptyRange` 换算成有限范围再读 +- 即使未命中硬上限,单次仍建议控制在 5000 单元格以内,减少超时和 token 消耗 + +**工作簿过大失败与部分成功不同**:收到顶层 +`errorCode=forbidden.document.sizeOverLimit` 和对应 `errorMessage` 时,表示工作簿整体无法装载。应停止读取并提示创建更小副本或拆分工作簿;缩小 `--range` 不能解决这类错误。不要把它当成 `hasMore=true` 的可续读结果。 ### 以 table/dataframe 协议读取结构化数据 ``` @@ -130,6 +138,8 @@ Flags: ``` **返回字段说明**: +- `rowIndices` / `colIndices` — 本次返回的真实行号与列字母映射 +- `hasMore` / `truncationReasons` / `resolvedRange` / `returnedRange` — 语义与 `csv-get` 相同;`range read` 的 `truncationReasons` 主要为 `max_cells` - `cells` — 二维数组,第一维为行,第二维为列。每个元素为 per-cell 对象,字段如下: | 字段 | 类型 | 是否必有 | 说明 | @@ -146,10 +156,16 @@ Flags: | type | 字段 | 说明 | |------|------|------| -| `dropdown` | `options: [{value: string, color?: string}]` | 下拉选项列表 | -| `dropdown` | `enableMultiSelect: boolean` | 是否允许多选 | +| `dropdown` Inline | `sourceType: "inline"` | 静态选项模式 | +| `dropdown` Inline | `options: [{value: string, color?: string}]` | 下拉选项列表 | +| `dropdown` Inline | `enableMultiSelect: boolean` | 是否允许多选 | +| `dropdown` SourceRange | `sourceType: "sourceRange"` | 区域来源模式 | +| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 同一工作簿内的来源工作表和规范化 A1 区域 | +| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置 | | `checkbox` | `checked: boolean` | 当前勾选状态 | +SourceRange 模式只返回引用元数据,不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。 + **hyperlink 结构**: | type | 字段 | 说明 | @@ -216,7 +232,7 @@ Flags: **公式校验建议**:写公式后不要只看写入返回结果。先用 `formula` 模式确认公式文本已落表,再运行 `formula-verify` 聚合扫描错误;关键业务数值继续用 `raw_value` 抽样对账。详见 [sheet-formula](./sheet-formula.md)。 -**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分页读取策略: +**超时处理建议**:读取大范围数据时若出现超时或响应过慢,请主动缩小 `--range` 查询范围,**建议单次读取的单元格数量控制在 5000 个以内**(例如 50 行 × 100 列、100 行 × 50 列)。对于大表可采用分批读取策略,每次都必须检查 `hasMore`: - 先通过 `info` 获取 `nonEmptyRange.range`,或用 `nonEmptyRange.lastRow` / `nonEmptyRange.lastColumn` 确定 A1 边界 - 按行分批读取,如 `A1:J500`、`A501:J1000`、`A1001:J1500` …… - 避免不传 `--range` 直接读取整个大工作表 @@ -232,7 +248,7 @@ dws sheet list --node --format json # 2. 查看工作表详情(行列数、最后非空位置、mergedRanges 等) dws sheet info --node --sheet-id --format json -# 3. 读取全部数据 +# 3. 读取数据(只有 hasMore=false 才代表目标范围已完整返回) dws sheet range read --node --sheet-id --format json # 4. 读取指定区域 @@ -251,6 +267,8 @@ dws sheet range read --node --sheet-id --range "A1:D10" --f - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询真实的 `sheetId` / 工作表名称后再调用,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等);用户仅给出工作表名称时,也应通过 `list` 校验该名称是否存在,避免名称大小写或拼写不一致导致失败 - `range read` 不传 `--range` 时默认读取整个工作表的全部非空数据 - `range read` 的 `--range` 支持 `Sheet1!A1:D10` 格式直接指定工作表(此时忽略 `--sheet-id`) +- ★ `csv-get` 和 `range read` 都必须检查 `hasMore`;`true` 时结合目标范围与 `returnedRange` 显式分批,CLI 不会自动续读 +- ★ `forbidden.document.sizeOverLimit` 是工作簿装载失败,不是 30,000 单元格部分成功;缩小 `--range` 不能修复 - ★ `csv-get` / `range read` / `range get` 不返回合并单元格结构;查看合并范围必须用 `sheet info` 的 `mergedRanges` - `table-get` 返回 table/dataframe 结构,适合需要 `columns` / `data` / `dtypes` / `formats` 的场景;它不返回 per-cell 元数据,也不替代 `range read` - ★ 大整数和长数字标识符回读校验:精确 ID 应是字符串 / `object`;若看到 `float64`,说明它已经按数值路径处理,超过 `9007199254740991` 时可能已丢精度 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md b/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md index eaa5e695..d2e61e65 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md @@ -386,9 +386,14 @@ Flags: **dropdown(下拉列表)**: ```json { "type": "text", "text": "High", "dataValidation": { "type": "dropdown", "options": [{"value":"High","color":"#00ff00"},{"value":"Low","color":"#ff0000"}], "enableMultiSelect": false } } +{ "dataValidation": { "type": "dropdown", "sourceRange": {"sheetId":"SOURCE_SHEET_ID","a1Notation":"T1:T3"}, "enableMultiSelect": false } } ``` -- `options`:必填,`[{value, color?}]` 数组 +- `options` 与 `sourceRange` 必须且只能提供一个 +- Inline:`options` 为非空 `[{value, color?}]` 数组 +- SourceRange:`sourceRange` 为 `{sheetId,a1Notation}`;可引用同一工作簿内其他工作表,支持普通区域、整行和整列。`a1Notation` 不带工作表前缀,不接受公式或多区域;颜色写入暂不支持 - `enableMultiSelect`:可选,是否多选,默认 false +- SourceRange 当前不会随工作表重命名或行列插入、删除、移动自动更新,结构操作后应重新写入 +- 复合 cell 写入中,如果值或样式已成功落盘但 SourceRange 校验失败,服务端可返回 `success:true` 并通过 `message` 说明下拉未创建;必须检查 `message` 并按需回读 **checkbox(复选框)**: ```json From 6abffce4e5e8f8e79b87629df6c8334255f37019 Mon Sep 17 00:00:00 2001 From: xiatian Date: Thu, 13 Aug 2026 16:13:30 +0800 Subject: [PATCH 2/8] fix(sheet): preserve dropdown schema compatibility --- internal/helpers/sheet_dimension.go | 6 --- .../sheet_dropdown_source_range_test.go | 49 ++++++++++++++----- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/internal/helpers/sheet_dimension.go b/internal/helpers/sheet_dimension.go index 2b442f02..1b902cc6 100644 --- a/internal/helpers/sheet_dimension.go +++ b/internal/helpers/sheet_dimension.go @@ -907,7 +907,6 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 Parameters: []contract.ParamDecl{ {Name: "multi-select", Property: "enableMultiSelect"}, {Name: "node", Property: "nodeId"}, - {Name: "options", Property: "options", Required: boolPtr(false), RequiredWhen: "exactly one of --options or --source-range must be provided"}, {Name: "source-sheet-id", Property: "sourceRange.sheetId", Required: boolPtr(false), RequiredWhen: "--source-range is provided"}, {Name: "source-range", Property: "sourceRange.a1Notation", Required: boolPtr(false), RequiredWhen: "exactly one of --options or --source-range must be provided"}, }, @@ -924,11 +923,6 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 setDropdownCmd.MarkFlagsOneRequired("options", "source-range") setDropdownCmd.MarkFlagsMutuallyExclusive("options", "source-range") setDropdownCmd.MarkFlagsRequiredTogether("source-sheet-id", "source-range") - cli.AnnotateRuntimeConstraints(setDropdownCmd, cli.RuntimeSchemaConstraints{ - MutuallyExclusive: [][]string{{"options", "source-range"}}, - RequireOneOf: [][]string{{"options", "source-range"}}, - RequireTogether: [][]string{{"source-sheet-id", "source-range"}}, - }) getDropdownCmd := &cobra.Command{ Use: "get-dropdown", diff --git a/internal/helpers/sheet_dropdown_source_range_test.go b/internal/helpers/sheet_dropdown_source_range_test.go index af7c1eeb..071e8ba4 100644 --- a/internal/helpers/sheet_dropdown_source_range_test.go +++ b/internal/helpers/sheet_dropdown_source_range_test.go @@ -1,7 +1,6 @@ package helpers import ( - "encoding/json" "strings" "testing" @@ -92,18 +91,43 @@ func TestCrossPlatformCoverageSetDropdownModeConstraints(t *testing.T) { } } -func TestCrossPlatformCoverageSetDropdownSchemaConstraints(t *testing.T) { - cmd := dimensionCoverageCommand(t, "set-dropdown") - constraints := runtimeannotate.CommandConstraints(cmd) - want := runtimeannotate.RuntimeSchemaConstraints{ - MutuallyExclusive: [][]string{{"options", "source-range"}}, - RequireOneOf: [][]string{{"options", "source-range"}}, - RequireTogether: [][]string{{"source-sheet-id", "source-range"}}, +func TestCrossPlatformCoverageSetDropdownRunEValidation(t *testing.T) { + tests := []struct { + name string + options string + source string + rangeID string + want string + }{ + {name: "neither mode", want: "必须且只能指定一个"}, + {name: "source range without sheet", rangeID: "A1:A3", want: "必须同时指定 --source-sheet-id"}, + {name: "source sheet without range", options: `[{"value":"one"}]`, source: "source", want: "必须同时指定 --source-range"}, + {name: "invalid options JSON", options: "{", want: "JSON 解析失败"}, + {name: "missing option value", options: `[{}]`, want: "value 为空"}, } - gotJSON, _ := json.Marshal(constraints) - wantJSON, _ := json.Marshal(runtimeannotate.NormalizeConstraints(want)) - if string(gotJSON) != string(wantJSON) { - t.Fatalf("constraints = %s, want %s", gotJSON, wantJSON) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cmd := dimensionCoverageCommand(t, "set-dropdown") + for name, value := range map[string]string{ + "node": "node", "sheet-id": "sheet", "range": "A1", + "options": tc.options, "source-sheet-id": tc.source, "source-range": tc.rangeID, + } { + if err := cmd.Flags().Set(name, value); err != nil { + t.Fatalf("set --%s: %v", name, err) + } + } + err := cmd.RunE(cmd, nil) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want contains %q", err, tc.want) + } + }) + } +} + +func TestCrossPlatformCoverageSetDropdownPreservesSchemaConstraints(t *testing.T) { + cmd := dimensionCoverageCommand(t, "set-dropdown") + if constraints := runtimeannotate.CommandConstraints(cmd); !runtimeannotate.ConstraintsEmpty(constraints) { + t.Fatalf("constraints = %#v, want no new schema-level constraints", constraints) } } @@ -135,6 +159,7 @@ func TestCrossPlatformCoverageBatchSetDropdownSourceRange(t *testing.T) { {"options": []any{map[string]any{"value": "one"}}, "source-sheet-id": "source", "source-range": "A1:A3"}, {"source-range": "A1:A3"}, {"source-sheet-id": "source", "source-range": "A1:A3", "colors": []any{"#fff"}}, + {"source-sheet-id": "source", "source-range": "A1:A3", "source-colors": []any{"#fff"}}, {"source-sheet-id": "source", "source-range": "Sheet2!A1:A3"}, } for _, value := range invalid { From 4b8d94c24eab4c8fb7138c28fd0b83263f297887 Mon Sep 17 00:00:00 2001 From: xiatian Date: Thu, 13 Aug 2026 17:15:22 +0800 Subject: [PATCH 3/8] ci: retry interrupted app race shard From 92195a58a346fadbcfdf834e6b86245db39332b4 Mon Sep 17 00:00:00 2001 From: xiatian Date: Sat, 15 Aug 2026 13:40:47 +0800 Subject: [PATCH 4/8] fix(sheet): align SourceRange review contract --- .changes/sheet-source-range-dropdowns.md | 2 +- internal/helpers/sheet_batch.go | 10 ++-- internal/helpers/sheet_dimension.go | 19 +++++-- .../sheet_dropdown_source_range_test.go | 52 +++++++++++++++++++ internal/helpers/sheet_range_ops.go | 2 +- .../products/sheet/sheet-batch-operations.md | 4 +- .../products/sheet/sheet-dropdown.md | 8 +-- .../products/sheet/sheet-read-data.md | 6 +-- .../products/sheet/sheet-write-data.md | 2 +- .../sheet/sheet-batch-operations.md | 4 +- .../references/sheet/sheet-dropdown.md | 8 +-- .../references/sheet/sheet-read-data.md | 6 +-- .../references/sheet/sheet-write-data.md | 2 +- 13 files changed, 97 insertions(+), 28 deletions(-) diff --git a/.changes/sheet-source-range-dropdowns.md b/.changes/sheet-source-range-dropdowns.md index 55d58640..91159e61 100644 --- a/.changes/sheet-source-range-dropdowns.md +++ b/.changes/sheet-source-range-dropdowns.md @@ -2,5 +2,5 @@ category: Added --- -- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. +- **Sheet SourceRange dropdowns** — supports range-backed dropdowns across direct, cell, and batch write paths, with structured readback for valid and invalid references. Batch `set-dropdown` now rejects unsupported top-level `colors` / `source-colors`; Inline colors belong in `options[].color`, while SourceRange color writes remain unsupported. - **Sheet read completion metadata** — documents and preserves returned ranges, truncation reasons, and partial-read status for large range and CSV reads. diff --git a/internal/helpers/sheet_batch.go b/internal/helpers/sheet_batch.go index 664bc4ae..7ce6e17d 100644 --- a/internal/helpers/sheet_batch.go +++ b/internal/helpers/sheet_batch.go @@ -74,10 +74,10 @@ func translateBatchOp(op map[string]any) (map[string]any, error) { func validateBatchSetDropdownInput(input map[string]any) error { if _, exists := input["source-colors"]; exists { - return fmt.Errorf("set-dropdown: SourceRange 颜色写入暂不支持") + return fmt.Errorf("set-dropdown: 顶层 source-colors 不受支持;Inline 颜色请写入 options[].color,SourceRange 颜色写入暂不支持") } if _, exists := input["colors"]; exists { - return fmt.Errorf("set-dropdown: SourceRange 颜色写入暂不支持") + return fmt.Errorf("set-dropdown: 顶层 colors 不受支持;Inline 颜色请写入 options[].color,SourceRange 颜色写入暂不支持") } options, hasOptions := input["options"] @@ -92,7 +92,7 @@ func validateBatchSetDropdownInput(input map[string]any) error { return fmt.Errorf("set-dropdown: source-range 与 source-sheet-id 必须同时指定") } if hasSourceRange { - if err := validateDropdownSourceRangeNotation(sourceRange); err != nil { + if err := validateDropdownSourceRangeInput(sourceSheetID, sourceRange); err != nil { return fmt.Errorf("set-dropdown: %w", err) } } @@ -457,6 +457,8 @@ func newBatchUpdateCmd() *cobra.Command { toolName 使用 CLI 命令名(与原子命令一致),input 的键用 CLI flag 名去掉 --。 CLI 层自动翻译为 MCP toolName + 参数名,无需记忆 MCP 参数名。 +source-range 的语义按 toolName 隔离:set-dropdown 中表示下拉候选项来源, +range fill/copy-to/move-to 中表示待填充、复制或移动的数据源区域。 支持的 CLI 命令名: range clear / range update / merge-cells / unmerge-cells / update-dimension @@ -465,6 +467,8 @@ CLI 层自动翻译为 MCP toolName + 参数名,无需记忆 MCP 参数名。 set-dropdown / delete-dropdown / csv-put / delete-float-image 其中 csv-put 与独立命令语义一致:CSV 字段值以 = 开头时按公式解析, 前加单引号(例如 "'=1+1")时写入以 = 开头的字面文本。 +set-dropdown 不接受顶层 colors/source-colors;Inline 颜色应写在 options[].color, +SourceRange 颜色写入暂不支持。 注意:batch-update 中 group-dimension 适合默认展开分组;需要 --group-state fold 时请使用独立 dws sheet group-dimension 命令。 diff --git a/internal/helpers/sheet_dimension.go b/internal/helpers/sheet_dimension.go index 1b902cc6..4afaf750 100644 --- a/internal/helpers/sheet_dimension.go +++ b/internal/helpers/sheet_dimension.go @@ -19,7 +19,14 @@ func sizeTypeEnumHint(dimension string) string { return "pixel / standard" } -func validateDropdownSourceRangeNotation(sourceRange string) error { +func validateDropdownSourceRangeInput(sourceSheetID, sourceRange string) error { + if strings.TrimSpace(sourceSheetID) == "" { + return fmt.Errorf("--source-sheet-id 不能为空") + } + sourceRange = strings.TrimSpace(sourceRange) + if sourceRange == "" { + return fmt.Errorf("--source-range 不能为空") + } if strings.Contains(sourceRange, "!") { return fmt.Errorf("--source-range 不能包含工作表前缀;请通过 --source-sheet-id 指定来源工作表") } @@ -801,8 +808,9 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 --source-range 只接受不含工作表前缀的 A1 区域,例如 T1:T3、T:T 或 1:3; 来源工作表由 --source-sheet-id 单独指定。SourceRange 颜色写入暂不支持。 -已知限制:服务端写入的 SourceRange 不会在工作表重命名或行列插入、删除、移动后 -自动更新;执行这些结构操作后应重新写入下拉来源。`, +已验证行为:工作表重命名、在引用前插入行/列、删除引用前的行会自动调整引用并保持 valid; +已验证的 move-dimension 场景会使引用变为 invalid。列删除、删除整个来源区域或来源工作表等未覆盖场景不能预设结果; +结构操作后应先回读 sourceRangeStatus,只有 invalid 时才重新选择来源并写入。`, Example: ` # 设置单选下拉列表 dws sheet set-dropdown --node NODE_ID --sheet-id SHEET_ID --range "A2:A100" \ --options '[{"value":"选项1"},{"value":"选项2"},{"value":"选项3"}]' @@ -862,7 +870,7 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 } toolArgs["options"] = options } else { - if err := validateDropdownSourceRangeNotation(sourceRange); err != nil { + if err := validateDropdownSourceRangeInput(sourceSheetID, sourceRange); err != nil { return err } toolArgs["sourceRange"] = map[string]any{ @@ -930,7 +938,8 @@ sheetId 支持传入工作表 ID 或工作表名称,可通过 sheet list 获 Long: `查询钉钉表格指定范围内的下拉列表配置。 Inline 配置返回 conditionValues 和 options;SourceRange 配置返回 -sourceType/sourceRange/sourceRangeStatus/enableMultiSelect,不展开来源区域的候选值或颜色。 +sourceType/sourceRangeStatus/enableMultiSelect,且仅在 sourceRangeStatus="valid" 时返回 +sourceRange。invalid 结果仍保留配置组,但省略 sourceRange;两种状态都不展开来源区域的候选值或颜色。 如果范围内存在多个不同的下拉列表配置,会分别返回每组配置及其覆盖的单元格列表。 如果范围内没有设置下拉列表,返回空。 diff --git a/internal/helpers/sheet_dropdown_source_range_test.go b/internal/helpers/sheet_dropdown_source_range_test.go index 071e8ba4..086b7da6 100644 --- a/internal/helpers/sheet_dropdown_source_range_test.go +++ b/internal/helpers/sheet_dropdown_source_range_test.go @@ -74,6 +74,8 @@ func TestCrossPlatformCoverageSetDropdownModeConstraints(t *testing.T) { {name: "sheet prefix", args: []string{"--source-sheet-id", "source", "--source-range", "Sheet2!A1:A3"}, want: "不能包含工作表前缀"}, {name: "formula", args: []string{"--source-sheet-id", "source", "--source-range", "=A1:A3"}, want: "必须是单一连续区域"}, {name: "multi region", args: []string{"--source-sheet-id", "source", "--source-range", "A1:A3,C1:C3"}, want: "必须是单一连续区域"}, + {name: "blank source range", args: []string{"--source-sheet-id", "source", "--source-range", " \t "}, want: "--source-range 不能为空"}, + {name: "blank source sheet", args: []string{"--source-sheet-id", " \t ", "--source-range", "A1:A3"}, want: "--source-sheet-id 不能为空"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -169,6 +171,56 @@ func TestCrossPlatformCoverageBatchSetDropdownSourceRange(t *testing.T) { } } +func TestCrossPlatformCoverageBatchSetDropdownValidationGuidance(t *testing.T) { + tests := []struct { + name string + input map[string]any + want string + }{ + { + name: "inline top-level colors", + input: map[string]any{"options": []any{map[string]any{"value": "one"}}, "colors": []any{"#fff"}}, + want: "Inline 颜色请写入 options[].color", + }, + { + name: "source top-level colors", + input: map[string]any{"source-sheet-id": "source", "source-range": "A1:A3", "source-colors": []any{"#fff"}}, + want: "SourceRange 颜色写入暂不支持", + }, + { + name: "blank source range", + input: map[string]any{"source-sheet-id": "source", "source-range": " \t "}, + want: "--source-range 不能为空", + }, + { + name: "blank source sheet", + input: map[string]any{"source-sheet-id": " \t ", "source-range": "A1:A3"}, + want: "--source-sheet-id 不能为空", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := translateBatchOp(map[string]any{"toolName": "set-dropdown", "input": tc.input}) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("error = %v, want contains %q", err, tc.want) + } + }) + } +} + +func TestCrossPlatformCoverageDropdownHelpDescribesDynamicAndInvalidContracts(t *testing.T) { + setDropdown := dimensionCoverageCommand(t, "set-dropdown") + if !strings.Contains(setDropdown.Long, "会自动调整引用并保持 valid") || + !strings.Contains(setDropdown.Long, "只有 invalid 时才重新选择来源并写入") { + t.Fatalf("set-dropdown help does not describe verified structural behavior:\n%s", setDropdown.Long) + } + getDropdown := dimensionCoverageCommand(t, "get-dropdown") + if !strings.Contains(getDropdown.Long, `仅在 sourceRangeStatus="valid" 时返回`) || + !strings.Contains(getDropdown.Long, "invalid 结果仍保留配置组,但省略 sourceRange") { + t.Fatalf("get-dropdown help does not describe conditional sourceRange readback:\n%s", getDropdown.Long) + } +} + func TestCrossPlatformCoverageValidateSourceRangeDataValidation(t *testing.T) { valid := []any{ map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}}, diff --git a/internal/helpers/sheet_range_ops.go b/internal/helpers/sheet_range_ops.go index 8bd07dc9..1c3a8fb5 100644 --- a/internal/helpers/sheet_range_ops.go +++ b/internal/helpers/sheet_range_ops.go @@ -184,7 +184,7 @@ dws sheet info --node NODE_ID --sheet-id SHEET_ID --format json,并读取 merg - 只设样式或批量刷整片区域样式请用 dws sheet range set-style;写值同时设置少量 cell 样式可用 cellStyles - 目标范围与已有合并区域冲突时,range update 会返回 MERGED_CELLS_CONFLICT;先用 sheet info 查看 mergedRanges,取消合并后写入,必要时再重新合并 - csv-put 的合并处理不同:目标区域含合并单元格时会打散合并并写入 CSV 值或公式 - - SourceRange 当前不会随工作表重命名或行列结构调整自动更新;执行结构操作后应重新写入 + - SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;move-dimension 及其他未覆盖的删除/移动场景后先回读 sourceRangeStatus,仅 invalid 时重新选源写入 - 同一 cell 的 value/style 已写入但 SourceRange 校验失败时,服务端可返回 success=true,并通过 message 明确下拉未创建;必须检查 message,必要时重新读取确认 - 清空整片区域请用 dws sheet range clear`, Example: ` # 写入文本 diff --git a/skills/mono/references/products/sheet/sheet-batch-operations.md b/skills/mono/references/products/sheet/sheet-batch-operations.md index 4654840f..66fafc90 100644 --- a/skills/mono/references/products/sheet/sheet-batch-operations.md +++ b/skills/mono/references/products/sheet/sheet-batch-operations.md @@ -102,7 +102,9 @@ Notes: - operations 最多 20 条 - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) - `csv-put` 子操作与独立命令语义一致:CSV 字段值以 `=` 开头时按公式解析;前加单引号时写入以 `=` 开头的字面文本 - - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。SourceRange 颜色写入暂不支持,且不会随 sheet 重命名或行列结构操作自动更新 + - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。顶层 `colors` / `source-colors` 会被拒绝;Inline 颜色写在 `options[].color`,SourceRange 颜色写入暂不支持 + - `set-dropdown` SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读,仅 `invalid` 时重新选源写入 + - `source-range` 按 `toolName` 解释:`set-dropdown` 中是下拉候选项来源;`range fill` / `range copy-to` / `range move-to` 中是待填充、复制或移动的数据源区域 - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 - `group-dimension` 在 batch 中只适合默认展开分组;需要 `--group-state fold` 时请使用独立 `dws sheet group-dimension` - `table-put` 不支持放进 batch-update;结构化 table 请用独立 `dws sheet table-put` diff --git a/skills/mono/references/products/sheet/sheet-dropdown.md b/skills/mono/references/products/sheet/sheet-dropdown.md index b6e2e658..05b5f0c0 100644 --- a/skills/mono/references/products/sheet/sheet-dropdown.md +++ b/skills/mono/references/products/sheet/sheet-dropdown.md @@ -48,7 +48,7 @@ Flags: - **用途**:为单元格配置静态选项或区域来源下拉,两种模式都支持多选;颜色仅 Inline 支持。 - **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 - **注意**:`--options` 与 `--source-range` 必须且只能指定一个。`--source-range` 只写 `T1:T3`、`T:T`、`1:3` 这类 A1 区域,来源工作表通过 `--source-sheet-id` 单独指定;不接受工作表前缀、公式或多区域。SourceRange 颜色写入暂不支持。 -- **静态引用限制**:服务端写入的 SourceRange 不会随工作表重命名或行列插入、删除、移动自动更新;结构操作后应重新写入该下拉来源。 +- **结构操作行为**:已验证的工作表重命名、在引用前插入行/列、删除引用前的行会自动调整引用并保持 `valid`;已验证的 `move-dimension` 场景会使其变为 `invalid`。列删除、删除整个来源区域或来源工作表等场景未覆盖,不能预设结果;结构操作后先回读 `sourceRangeStatus`,仅在 `invalid` 时重新选择来源并写入。 ### 获取下拉列表配置 ``` @@ -66,7 +66,7 @@ Flags: 查询指定范围内的下拉列表配置信息。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`sourceRange:{sheetId,a1Notation}`、`enableMultiSelect` 和 `ranges`,不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组始终返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`enableMultiSelect` 和 `ranges`,仅在 `sourceRangeStatus:"valid"` 时返回 `sourceRange:{sheetId,a1Notation}`。`invalid` 时仍保留配置组,但省略 `sourceRange`,不得依赖旧坐标修复。SourceRange 不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 ### 删除下拉列表 ``` @@ -98,6 +98,6 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `set-dropdown` 的 Inline 模式使用 `--options`,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB`);SourceRange 模式使用 `--source-sheet-id` + `--source-range`。两种模式均可用 `--multi-select`,并会覆盖目标范围已有下拉 -- SourceRange 当前是静态引用,不会随 sheet 重命名或行列结构操作自动调整;颜色写入暂不支持 -- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,不回退展开候选值 +- SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读,仅 `invalid` 时重新选源写入;颜色写入暂不支持 +- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,但省略 `sourceRange`,不回退展开候选值 - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/mono/references/products/sheet/sheet-read-data.md b/skills/mono/references/products/sheet/sheet-read-data.md index 4963fc2a..f8008d0e 100644 --- a/skills/mono/references/products/sheet/sheet-read-data.md +++ b/skills/mono/references/products/sheet/sheet-read-data.md @@ -160,11 +160,11 @@ Flags: | `dropdown` Inline | `options: [{value: string, color?: string}]` | 下拉选项列表 | | `dropdown` Inline | `enableMultiSelect: boolean` | 是否允许多选 | | `dropdown` SourceRange | `sourceType: "sourceRange"` | 区域来源模式 | -| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 同一工作簿内的来源工作表和规范化 A1 区域 | -| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置 | +| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 仅 `sourceRangeStatus:"valid"` 时返回的来源工作表和规范化 A1 区域;`invalid` 时省略 | +| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置,但不返回 `sourceRange` | | `checkbox` | `checked: boolean` | 当前勾选状态 | -SourceRange 模式只返回引用元数据,不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。 +SourceRange 模式不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。`invalid` 时只能依赖 `sourceRangeStatus` 判定需要重新选源,不得假定仍能从 `sourceRange` 取回旧坐标。 **hyperlink 结构**: diff --git a/skills/mono/references/products/sheet/sheet-write-data.md b/skills/mono/references/products/sheet/sheet-write-data.md index d2e61e65..9050fb43 100644 --- a/skills/mono/references/products/sheet/sheet-write-data.md +++ b/skills/mono/references/products/sheet/sheet-write-data.md @@ -392,7 +392,7 @@ Flags: - Inline:`options` 为非空 `[{value, color?}]` 数组 - SourceRange:`sourceRange` 为 `{sheetId,a1Notation}`;可引用同一工作簿内其他工作表,支持普通区域、整行和整列。`a1Notation` 不带工作表前缀,不接受公式或多区域;颜色写入暂不支持 - `enableMultiSelect`:可选,是否多选,默认 false -- SourceRange 当前不会随工作表重命名或行列插入、删除、移动自动更新,结构操作后应重新写入 +- SourceRange 在已验证的工作表重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读 `sourceRangeStatus`,仅 `invalid` 时重新选源写入 - 复合 cell 写入中,如果值或样式已成功落盘但 SourceRange 校验失败,服务端可返回 `success:true` 并通过 `message` 说明下拉未创建;必须检查 `message` 并按需回读 **checkbox(复选框)**: diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md index 4654840f..66fafc90 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-batch-operations.md @@ -102,7 +102,9 @@ Notes: - operations 最多 20 条 - 当需要对多个区域执行相同清除时,优先使用 `range batch-clear`(更简洁) - `csv-put` 子操作与独立命令语义一致:CSV 字段值以 `=` 开头时按公式解析;前加单引号时写入以 `=` 开头的字面文本 - - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。SourceRange 颜色写入暂不支持,且不会随 sheet 重命名或行列结构操作自动更新 + - `set-dropdown` 的 `input` 中,Inline 使用 `options`;SourceRange 使用 `source-sheet-id` + `source-range`,两种模式必须且只能选一个。顶层 `colors` / `source-colors` 会被拒绝;Inline 颜色写在 `options[].color`,SourceRange 颜色写入暂不支持 + - `set-dropdown` SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读,仅 `invalid` 时重新选源写入 + - `source-range` 按 `toolName` 解释:`set-dropdown` 中是下拉候选项来源;`range fill` / `range copy-to` / `range move-to` 中是待填充、复制或移动的数据源区域 - 典型场景:先插入行列再写入数据、先清除再写入、批量合并+调整行高列宽 - `group-dimension` 在 batch 中只适合默认展开分组;需要 `--group-state fold` 时请使用独立 `dws sheet group-dimension` - `table-put` 不支持放进 batch-update;结构化 table 请用独立 `dws sheet table-put` diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md b/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md index b6e2e658..05b5f0c0 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-dropdown.md @@ -48,7 +48,7 @@ Flags: - **用途**:为单元格配置静态选项或区域来源下拉,两种模式都支持多选;颜色仅 Inline 支持。 - **场景**:规范数据输入,如状态选择(完成/进行中/待处理)、优先级(高/中/低)等。 - **注意**:`--options` 与 `--source-range` 必须且只能指定一个。`--source-range` 只写 `T1:T3`、`T:T`、`1:3` 这类 A1 区域,来源工作表通过 `--source-sheet-id` 单独指定;不接受工作表前缀、公式或多区域。SourceRange 颜色写入暂不支持。 -- **静态引用限制**:服务端写入的 SourceRange 不会随工作表重命名或行列插入、删除、移动自动更新;结构操作后应重新写入该下拉来源。 +- **结构操作行为**:已验证的工作表重命名、在引用前插入行/列、删除引用前的行会自动调整引用并保持 `valid`;已验证的 `move-dimension` 场景会使其变为 `invalid`。列删除、删除整个来源区域或来源工作表等场景未覆盖,不能预设结果;结构操作后先回读 `sourceRangeStatus`,仅在 `invalid` 时重新选择来源并写入。 ### 获取下拉列表配置 ``` @@ -66,7 +66,7 @@ Flags: 查询指定范围内的下拉列表配置信息。 - **用途**:查看单元格已设置的下拉列表选项和配置。 - **场景**:在修改下拉列表前先查询现有配置;确认下拉列表是否设置成功。 -- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`sourceRange:{sheetId,a1Notation}`、`enableMultiSelect` 和 `ranges`,不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 +- **返回**:`dataValidations` 数组由底层按配置聚合。Inline 组返回 `sourceType:"inline"`、`conditionValues`、`ranges` 和 `options`;SourceRange 组始终返回 `sourceType:"sourceRange"`、`sourceRangeStatus:"valid"/"invalid"`、`enableMultiSelect` 和 `ranges`,仅在 `sourceRangeStatus:"valid"` 时返回 `sourceRange:{sheetId,a1Notation}`。`invalid` 时仍保留配置组,但省略 `sourceRange`,不得依赖旧坐标修复。SourceRange 不会展开候选值,因此不返回 `conditionValues`、`options` 或颜色。范围内无下拉列表时 `hasDropdown` 为 false。 ### 删除下拉列表 ``` @@ -98,6 +98,6 @@ Flags: - ★ **`--sheet-id` 获取规范(强制)**:`sheetId` 未知时必须先通过 `dws sheet list --node --format json` 查询,禁止凭空编造(如臆测为 `Sheet1`、`sheet1`、`0`、`default` 等) - `set-dropdown` 的 Inline 模式使用 `--options`,每个元素包含 `value`(必填)和 `color`(可选,`#RRGGBB`);SourceRange 模式使用 `--source-sheet-id` + `--source-range`。两种模式均可用 `--multi-select`,并会覆盖目标范围已有下拉 -- SourceRange 当前是静态引用,不会随 sheet 重命名或行列结构操作自动调整;颜色写入暂不支持 -- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,不回退展开候选值 +- SourceRange 在已验证的重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读,仅 `invalid` 时重新选源写入;颜色写入暂不支持 +- `get-dropdown` 查询指定范围内的下拉配置,聚合发生在底层服务。SourceRange 即使无效也保留一组并以 `sourceRangeStatus:"invalid"` 表示,但省略 `sourceRange`,不回退展开候选值 - `delete-dropdown` 删除指定范围内的下拉列表配置,单元格恢复为普通文本格式。已填写的值不会被清除。目标范围不存在下拉列表时操作仍返回成功 diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md b/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md index 4963fc2a..f8008d0e 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-read-data.md @@ -160,11 +160,11 @@ Flags: | `dropdown` Inline | `options: [{value: string, color?: string}]` | 下拉选项列表 | | `dropdown` Inline | `enableMultiSelect: boolean` | 是否允许多选 | | `dropdown` SourceRange | `sourceType: "sourceRange"` | 区域来源模式 | -| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 同一工作簿内的来源工作表和规范化 A1 区域 | -| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置 | +| `dropdown` SourceRange | `sourceRange: {sheetId, a1Notation}` | 仅 `sourceRangeStatus:"valid"` 时返回的来源工作表和规范化 A1 区域;`invalid` 时省略 | +| `dropdown` SourceRange | `sourceRangeStatus: "valid" / "invalid"` | 底层引用是否可解析;无效引用仍返回配置,但不返回 `sourceRange` | | `checkbox` | `checked: boolean` | 当前勾选状态 | -SourceRange 模式只返回引用元数据,不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。 +SourceRange 模式不动态拉取或展开来源区域的候选值,因此不返回 `options` 或颜色。`invalid` 时只能依赖 `sourceRangeStatus` 判定需要重新选源,不得假定仍能从 `sourceRange` 取回旧坐标。 **hyperlink 结构**: diff --git a/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md b/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md index d2e61e65..9050fb43 100644 --- a/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md +++ b/skills/multi/dingtalk-misc/references/sheet/sheet-write-data.md @@ -392,7 +392,7 @@ Flags: - Inline:`options` 为非空 `[{value, color?}]` 数组 - SourceRange:`sourceRange` 为 `{sheetId,a1Notation}`;可引用同一工作簿内其他工作表,支持普通区域、整行和整列。`a1Notation` 不带工作表前缀,不接受公式或多区域;颜色写入暂不支持 - `enableMultiSelect`:可选,是否多选,默认 false -- SourceRange 当前不会随工作表重命名或行列插入、删除、移动自动更新,结构操作后应重新写入 +- SourceRange 在已验证的工作表重命名、引用前插入行/列、删除引用前行的场景会自动调整;已验证的 `move-dimension` 会使其变为 `invalid`。其他未覆盖删除/移动场景后先回读 `sourceRangeStatus`,仅 `invalid` 时重新选源写入 - 复合 cell 写入中,如果值或样式已成功落盘但 SourceRange 校验失败,服务端可返回 `success:true` 并通过 `message` 说明下拉未创建;必须检查 `message` 并按需回读 **checkbox(复选框)**: From 4a33e7e893bb838000c3bbba95fbe036222f5643 Mon Sep 17 00:00:00 2001 From: chichuan Date: Sun, 16 Aug 2026 22:17:14 +0800 Subject: [PATCH 5/8] fix(ci): drop race instrumentation from the app schema partition The schema partition's 52 tests assert structural Schema-to-Cobra contracts over a single goroutine: none of them call t.Parallel or start a goroutine, so the race detector has no concurrent access to observe there. The process-global lazy metadata that does need race coverage (schema_source_root's atomic.Value, the parameter-binding lazy loaders) is exercised by internal/cli's concurrent tests, which stay instrumented. The instrumentation was not free here. The partition shares a single sync.Once Catalog build whose work is allocation-heavy, and -race made it roughly 11x slower: 26s -> 291s locally, and 357s of the app shard's 780s in CI. Within that partition TestFinalSchemaToolsHaveExecutableBaseCommands alone accounted for 262s, not because the test is expensive but because it is the first caller to pay for the shared snapshot; its 1121 subtests together measure 0.00s. run_partition now takes the instrumentation mode explicitly and fails closed on an unrecognized value, so a typo cannot silently drop -race from a partition that is supposed to carry it. --- scripts/ci/run-app-race-tests.sh | 49 +++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/scripts/ci/run-app-race-tests.sh b/scripts/ci/run-app-race-tests.sh index 9922e9ab..cdfbe017 100755 --- a/scripts/ci/run-app-race-tests.sh +++ b/scripts/ci/run-app-race-tests.sh @@ -101,26 +101,49 @@ fi run_partition() { name="$1" - run_pattern="$2" - skip_pattern="${3:-}" + instrumentation="$2" + run_pattern="$3" + skip_pattern="${4:-}" - printf 'running internal/app race partition %s\n' "$name" + # Fail closed on an unrecognized mode: a typo must not silently drop race + # instrumentation from a partition that is supposed to carry it. + case "$instrumentation" in + race|no-race) ;; + *) + printf 'unknown instrumentation %s for app partition %s\n' \ + "$instrumentation" "$name" >&2 + exit 1 + ;; + esac + + printf 'running internal/app %s partition %s\n' "$instrumentation" "$name" + set -- -v -count=1 -timeout=15m -run "$run_pattern" if [ -n "$skip_pattern" ]; then - go test -v -race -count=1 -timeout=15m \ - -run "$run_pattern" -skip "$skip_pattern" "$app_package" - else - go test -v -race -count=1 -timeout=15m \ - -run "$run_pattern" "$app_package" + set -- "$@" -skip "$skip_pattern" fi + if [ "$instrumentation" = race ]; then + set -- -race "$@" + fi + go test "$@" "$app_package" } # Schema assembly has the largest transient memory footprint. Run those tests # in a fresh process, then keep each remaining name range in its own process so # command trees retained by process-global registries are released between # partitions. The complementary run/skip patterns preserve the full test set. +# +# The schema partition runs uninstrumented. Its tests assert structural +# Schema-to-Cobra contracts over a single goroutine: none of them call +# t.Parallel or start a goroutine, so the race detector has no concurrent access +# to observe here. The process-global lazy metadata that does need race coverage +# (schema_source_root's atomic.Value, the parameter-binding lazy loaders) is +# exercised by internal/cli's concurrent tests, which stay instrumented. The +# instrumentation is not free on this partition: its shared sync.Once Catalog +# build is allocation-heavy, and -race made the partition roughly 11x slower +# (26s -> 291s locally, 357s in CI) without being able to report anything. schema_pattern='^Test.*Schema' -run_partition schema "$schema_pattern" -run_partition a-b '^Test[A-B]' "$schema_pattern" -run_partition c '^TestC' "$schema_pattern" -run_partition d-r '^Test[D-R]' "$schema_pattern" -run_partition s-z-example-fuzz '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern" +run_partition schema no-race "$schema_pattern" +run_partition a-b race '^Test[A-B]' "$schema_pattern" +run_partition c race '^TestC' "$schema_pattern" +run_partition d-r race '^Test[D-R]' "$schema_pattern" +run_partition s-z-example-fuzz race '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern" From bfd0976b31897c63509853f8494f4cb9034db31d Mon Sep 17 00:00:00 2001 From: chichuan Date: Sun, 16 Aug 2026 22:18:04 +0800 Subject: [PATCH 6/8] fix(ci): run the app test partitions as parallel shards The five internal/app partitions ran end to end inside one job, so the app shard's wall clock was the sum of all five: 780s in CI, of which the schema partition owned 357s. Each partition is now its own matrix shard, so they run concurrently and the shard's wall clock is set by its slowest partition rather than by their total. Every partition shard still selects the same single internal/app package, so the impacted-package query maps the shard name back to app and the partition only chooses which tests run. The helper gains a partition argument and a list-partitions mode. APP_PARTITIONS is the single source of truth for the set, and the discovery pass still runs in every job, so each one independently verifies that the partition patterns cover every top-level test exactly once before running the one it was asked for. Two fail-closed checks guard the split, because the helper's own coverage check can no longer prove the whole package ran once the partitions are separate jobs: - The helper cross-checks APP_PARTITIONS against the coverage counters in both directions, so a counted partition that nothing dispatches and a dispatchable partition with no counter both fail instead of silently skipping tests. - TestCIAppRacePartitionMatrixMatchesHelper pins the workflow's app- shards to list-partitions output in both directions, so a partition cannot lose its job while every job stays green. The discovery loop variable is renamed from partition to spec: it would otherwise shadow the partition requested on the command line, which run mode reads after the discovery pass completes. --- .github/workflows/ci.yml | 85 +++++++++++++++----- scripts/ci/run-app-race-tests.sh | 97 ++++++++++++++++++++--- test/scripts/changelog_pr_gate_test.go | 36 +++++---- test/scripts/ci_test_package_plan_test.go | 68 ++++++++++++++++ 4 files changed, 236 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4d30325..dc437165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -523,12 +523,22 @@ jobs: # shards at full-suite scope; release-scripts is included because its # dedicated job only runs at full-suite or release-sensitive scope, and # dropping it here would stop testing test/scripts changes entirely. + # internal/app is carried by one shard per bounded partition rather than a + # single app shard: the partitions used to run end to end inside one job, + # where the Schema partition alone owned most of the wall clock. The + # app- names are pinned to the helper's partition set by + # TestCIAppRacePartitionMatrixMatchesHelper, so a partition can never lose + # its job silently. timeout-minutes: 20 strategy: fail-fast: false matrix: shard: - - app + - app-schema + - app-a-b + - app-c + - app-d-r + - app-s-z-example-fuzz - generators - helpers - cli @@ -570,9 +580,16 @@ jobs: TEST_SHARD: ${{ matrix.shard }} run: | set -euo pipefail + # Every app partition shard tests the same single internal/app + # package, so the impacted-package query uses the base shard name and + # the partition only selects which tests run. + package_shard="$TEST_SHARD" + case "$TEST_SHARD" in + app-*) package_shard=app ;; + esac package_output="$( ./scripts/ci/changed-test-packages.sh \ - list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF" + list-shard "$package_shard" "$TEST_BASE_REF" "$TEST_HEAD_REF" )" if [ -z "$package_output" ]; then echo "No buildable Go package in shard $TEST_SHARD is affected by this revision." \ @@ -612,14 +629,19 @@ jobs: exit 1 } done - if [ "$TEST_SHARD" = "app" ]; then - # A single long-lived app test process retains every constructed - # command tree in framework registries. Isolate Schema assembly and - # bounded name ranges so each process releases that state on exit. - test "${#packages[@]}" -eq 1 - ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" - exit 0 - fi + case "$TEST_SHARD" in + app-*) + # A single long-lived app test process retains every constructed + # command tree in framework registries. Each partition is its own + # job, so that state is released when the process exits and the + # partitions run concurrently instead of end to end. The helper + # still verifies that the partition patterns cover every top-level + # test exactly once before running the one it was asked for. + test "${#packages[@]}" -eq 1 + ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}" + exit 0 + ;; + esac if [ "$TEST_SHARD" = "release-scripts" ]; then # Mirror the dedicated release-contract job: these suites shell out # to archive tooling and are not race-instrumented there. @@ -640,14 +662,21 @@ jobs: needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true' }} runs-on: ubuntu-latest - # app runs several independently bounded processes; cli/smoke need headroom - # beyond go test -timeout for setup + assembly. + # internal/app is split across one shard per bounded partition so the + # partitions run concurrently and each releases its framework registries + # when the process exits; cli/smoke need headroom beyond go test -timeout for + # setup + assembly. The app- names are pinned to the helper's + # partition set by TestCIAppRacePartitionMatrixMatchesHelper. timeout-minutes: 20 strategy: fail-fast: false matrix: shard: - - app + - app-schema + - app-a-b + - app-c + - app-d-r + - app-s-z-example-fuzz - generators - helpers - cli @@ -673,18 +702,30 @@ jobs: TEST_SHARD: ${{ matrix.shard }} run: | set -euo pipefail - package_output="$(./scripts/ci/test-packages.sh list "$TEST_SHARD")" + # Every app partition shard tests the same single internal/app + # package, so the package query uses the base shard name and the + # partition only selects which tests run. + package_shard="$TEST_SHARD" + case "$TEST_SHARD" in + app-*) package_shard=app ;; + esac + package_output="$(./scripts/ci/test-packages.sh list "$package_shard")" test -n "$package_output" mapfile -t packages <<< "$package_output" test "${#packages[@]}" -gt 0 - if [ "$TEST_SHARD" = "app" ]; then - # A single long-lived app test process retains every constructed - # command tree in framework registries. Isolate Schema assembly and - # bounded name ranges so each process releases that state on exit. - test "${#packages[@]}" -eq 1 - ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" - exit 0 - fi + case "$TEST_SHARD" in + app-*) + # A single long-lived app test process retains every constructed + # command tree in framework registries. Each partition is its own + # job, so that state is released when the process exits and the + # partitions run concurrently instead of end to end. The helper + # still verifies that the partition patterns cover every top-level + # test exactly once before running the one it was asked for. + test "${#packages[@]}" -eq 1 + ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}" + exit 0 + ;; + esac # cli/smoke own heavy NewRootCommand / Schema assembly under -race; # give them a dedicated package timeout on slower hosted runners. timeout_budget=12m diff --git a/scripts/ci/run-app-race-tests.sh b/scripts/ci/run-app-race-tests.sh index cdfbe017..3f586dd0 100755 --- a/scripts/ci/run-app-race-tests.sh +++ b/scripts/ci/run-app-race-tests.sh @@ -6,16 +6,35 @@ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" usage() { printf '%s\n' \ "usage: $0 verify " \ - " $0 run " >&2 + " $0 run [partition]" \ + " $0 list-partitions" >&2 exit 2 } -[ "$#" -eq 2 ] || usage -mode="$1" -app_package="$2" +# Single source of truth for the partition set. CI runs one job per partition and +# pins its shard names to this list, so a name that appears here without a +# dispatch entry below fails closed rather than silently skipping tests. +APP_PARTITIONS='schema a-b c d-r s-z-example-fuzz' +mode="${1:-}" +partition="" case "$mode" in - verify|run) ;; + list-partitions) + [ "$#" -eq 1 ] || usage + for name in $APP_PARTITIONS; do + printf '%s\n' "$name" + done + exit 0 + ;; + verify) + [ "$#" -eq 2 ] || usage + app_package="$2" + ;; + run) + [ "$#" -eq 2 ] || [ "$#" -eq 3 ] || usage + app_package="$2" + partition="${3:-}" + ;; *) usage ;; esac @@ -70,21 +89,49 @@ if [ "$unmatched_count" -ne 0 ]; then exit 1 fi -for partition in \ +# The loop variable is deliberately not named "partition": that name holds the +# partition requested on the command line, and run mode still executes this +# discovery pass before dispatching. +classified='' +for spec in \ "schema:$schema_count" \ "a-b:$ab_count" \ "c:$c_count" \ "d-r:$dr_count" \ "s-z-example-fuzz:$sz_count" do - name="${partition%%:*}" - count="${partition#*:}" + name="${spec%%:*}" + count="${spec#*:}" + classified="$classified $name" if [ "$count" -eq 0 ]; then printf 'app race partition %s is empty\n' "$name" >&2 exit 1 fi done +# The counters above and APP_PARTITIONS must describe the same set in both +# directions. A counted partition missing from APP_PARTITIONS would never be +# dispatched by any CI job, and a dispatchable partition with no counter would +# escape the exact-coverage check above. +for name in $APP_PARTITIONS; do + case " $classified " in + *" $name "*) ;; + *) + printf 'app partition %s has no coverage counter\n' "$name" >&2 + exit 1 + ;; + esac +done +for name in $classified; do + case " $APP_PARTITIONS " in + *" $name "*) ;; + *) + printf 'counted partition %s is not a dispatchable app partition\n' "$name" >&2 + exit 1 + ;; + esac +done + total_count="$(wc -l < "$tests" | tr -d ' ')" assigned_count=$((schema_count + ab_count + c_count + dr_count + sz_count)) if [ "$assigned_count" -ne "$total_count" ]; then @@ -142,8 +189,32 @@ run_partition() { # build is allocation-heavy, and -race made the partition roughly 11x slower # (26s -> 291s locally, 357s in CI) without being able to report anything. schema_pattern='^Test.*Schema' -run_partition schema no-race "$schema_pattern" -run_partition a-b race '^Test[A-B]' "$schema_pattern" -run_partition c race '^TestC' "$schema_pattern" -run_partition d-r race '^Test[D-R]' "$schema_pattern" -run_partition s-z-example-fuzz race '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern" + +# Dispatch table for the partition set declared in APP_PARTITIONS. CI passes one +# partition per job so they run concurrently; running without a partition keeps +# the original end-to-end behaviour for local use and for any caller that wants +# the whole package in one invocation. +run_named_partition() { + case "$1" in + schema) run_partition schema no-race "$schema_pattern" ;; + a-b) run_partition a-b race '^Test[A-B]' "$schema_pattern" ;; + c) run_partition c race '^TestC' "$schema_pattern" ;; + d-r) run_partition d-r race '^Test[D-R]' "$schema_pattern" ;; + s-z-example-fuzz) + run_partition s-z-example-fuzz race '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern" + ;; + *) + printf 'unknown app partition: %s\n' "$1" >&2 + exit 1 + ;; + esac +} + +if [ -n "$partition" ]; then + run_named_partition "$partition" + exit 0 +fi + +for name in $APP_PARTITIONS; do + run_named_partition "$name" +done diff --git a/test/scripts/changelog_pr_gate_test.go b/test/scripts/changelog_pr_gate_test.go index 849d9191..5d8ef72b 100644 --- a/test/scripts/changelog_pr_gate_test.go +++ b/test/scripts/changelog_pr_gate_test.go @@ -1157,7 +1157,7 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { `test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA"`, `test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA"`, `echo "TEST_HEAD_REF=$(git rev-parse HEAD)"`, - `list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF"`, + `list-shard "$package_shard" "$TEST_BASE_REF" "$TEST_HEAD_REF"`, "needs.lint.outputs.full_suite != 'true'", `name: "Test (race: ${{ matrix.shard }})"`, "name: Test (workflow and release contracts)", @@ -1219,16 +1219,20 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { } // The focused path fans the impacted set across the same shards as test-race // and runs each shard the way test-race runs it, so no single job carries - // internal/app together with its reverse dependencies. internal/app keeps its - // package-level headroom through the process-isolating helper instead of one - // long -timeout, which is strictly stronger: every process releases the - // framework registries it populated. release-scripts is asserted because its - // dedicated job only runs at full-suite or release-sensitive scope, so losing - // it here would silently stop testing test/scripts changes. + // internal/app together with its reverse dependencies. internal/app is split + // further into one shard per bounded partition, which keeps its package-level + // headroom through the process-isolating helper instead of one long -timeout + // and is strictly stronger than a single app job: every partition process + // releases the framework registries it populated, and the partitions run + // concurrently rather than end to end. Each partition shard still selects the + // same single internal/app package, so the impacted-package query maps the + // shard name back to app. release-scripts is asserted because its dedicated + // job only runs at full-suite or release-sensitive scope, so losing it here + // would silently stop testing test/scripts changes. for _, want := range []string{ - `if [ "$TEST_SHARD" = "app" ]; then`, + `app-*) package_shard=app ;;`, `test "${#packages[@]}" -eq 1`, - `./scripts/ci/run-app-race-tests.sh run "${packages[0]}"`, + `./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"`, `if [ "$TEST_SHARD" = "release-scripts" ]; then`, `go test -v -count=1 -timeout=10m "${packages[@]}"`, "timeout_budget=12m", @@ -1249,14 +1253,16 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { t.Fatal("Code Admission workflow missing race test job boundaries") } raceJob := admission[raceStart:raceEnd] - // The app shard uses independently bounded test processes so process-global - // command registries are released before the Schema assembly peak. Other full - // race shards retain the dynamic package timeout: default/floor 12m, with - // cli/smoke raised to 15m on slower hosted runners. + // internal/app is carried by one shard per bounded partition, so process-global + // command registries are released with each partition process and the Schema + // assembly peak no longer sits in front of the other partitions. Each + // partition shard resolves back to the same single internal/app package. + // Other full race shards retain the dynamic package timeout: default/floor + // 12m, with cli/smoke raised to 15m on slower hosted runners. for _, want := range []string{ - `if [ "$TEST_SHARD" = "app" ]; then`, + `app-*) package_shard=app ;;`, `test "${#packages[@]}" -eq 1`, - `./scripts/ci/run-app-race-tests.sh run "${packages[0]}"`, + `./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"`, "timeout_budget=12m", `if [ "$TEST_SHARD" = "cli" ] ||`, `[ "$TEST_SHARD" = "smoke" ]; then`, diff --git a/test/scripts/ci_test_package_plan_test.go b/test/scripts/ci_test_package_plan_test.go index 0d7944f2..b8250c19 100644 --- a/test/scripts/ci_test_package_plan_test.go +++ b/test/scripts/ci_test_package_plan_test.go @@ -99,6 +99,74 @@ func TestCIAppRacePartitionsCoverTopLevelTestsExactlyOnce(t *testing.T) { } } +// TestCIAppRacePartitionMatrixMatchesHelper pins the workflow's app partition +// shards to the partition set the helper actually runs. The partitions are +// separate CI jobs now, so the helper's own "covered exactly once" check can no +// longer prove the whole package ran: a partition the helper knows about but no +// matrix shard dispatches would silently stop running while every job stays +// green. Both directions are asserted so a stale matrix shard fails too. +func TestCIAppRacePartitionMatrixMatchesHelper(t *testing.T) { + root := testPackagePlanRoot(t) + script := filepath.Join(root, "scripts", "ci", "run-app-race-tests.sh") + cmd := exec.Command("sh", script, "list-partitions") + cmd.Dir = root + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("%s list-partitions failed: %v\n%s", script, err, output) + } + partitions := strings.Fields(string(output)) + if len(partitions) == 0 { + t.Fatalf("list-partitions returned no partitions: %q", output) + } + + workflow, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "ci.yml")) + if err != nil { + t.Fatalf("read ci.yml: %v", err) + } + admission := string(workflow) + + for _, job := range []struct { + name string + startMark string + endMark string + }{ + {"test-focused", "\n test-focused:\n", "\n test-race:\n"}, + {"test-race", "\n test-race:\n", "\n test-release-scripts:\n"}, + } { + start := strings.Index(admission, job.startMark) + end := strings.Index(admission, job.endMark) + if start < 0 || end <= start { + t.Fatalf("ci.yml is missing %s job boundaries", job.name) + } + body := admission[start:end] + + for _, partition := range partitions { + want := "- app-" + partition + if !strings.Contains(body, want) { + t.Errorf("%s matrix is missing shard %q for a partition the helper runs", job.name, want) + } + } + + for _, line := range strings.Split(body, "\n") { + shard := strings.TrimSpace(line) + if !strings.HasPrefix(shard, "- app-") { + continue + } + name := strings.TrimPrefix(shard, "- app-") + matched := false + for _, partition := range partitions { + if partition == name { + matched = true + break + } + } + if !matched { + t.Errorf("%s matrix shard %q has no matching helper partition", job.name, shard) + } + } + } +} + func TestCITestPackagePlanFailsClosedWhenGoListFails(t *testing.T) { root := testPackagePlanRoot(t) fakeBin := t.TempDir() From 4f9537342073bf5d9390f104a7effd45d0d930b4 Mon Sep 17 00:00:00 2001 From: chichuan Date: Mon, 17 Aug 2026 10:59:11 +0800 Subject: [PATCH 7/8] docs: seal changelog for v1.0.59-beta.2 --- .../1.0.59-beta.2}/1009-clitrack-telemetry.md | 0 .../1019-help-feedback-entry.md | 0 .../1.0.59-beta.2}/954-chat-im-id-flags.md | 0 .../chat-reference-card-verification.md | 0 .../schema-catalog-json-projection.md | 0 .../1.0.59-beta.2}/wiki-shortcut-alignment.md | 0 CHANGELOG.md | 31 +++++++++++++++++++ 7 files changed, 31 insertions(+) rename .changes/{ => released/1.0.59-beta.2}/1009-clitrack-telemetry.md (100%) rename .changes/{ => released/1.0.59-beta.2}/1019-help-feedback-entry.md (100%) rename .changes/{ => released/1.0.59-beta.2}/954-chat-im-id-flags.md (100%) rename .changes/{ => released/1.0.59-beta.2}/chat-reference-card-verification.md (100%) rename .changes/{ => released/1.0.59-beta.2}/schema-catalog-json-projection.md (100%) rename .changes/{ => released/1.0.59-beta.2}/wiki-shortcut-alignment.md (100%) diff --git a/.changes/1009-clitrack-telemetry.md b/.changes/released/1.0.59-beta.2/1009-clitrack-telemetry.md similarity index 100% rename from .changes/1009-clitrack-telemetry.md rename to .changes/released/1.0.59-beta.2/1009-clitrack-telemetry.md diff --git a/.changes/1019-help-feedback-entry.md b/.changes/released/1.0.59-beta.2/1019-help-feedback-entry.md similarity index 100% rename from .changes/1019-help-feedback-entry.md rename to .changes/released/1.0.59-beta.2/1019-help-feedback-entry.md diff --git a/.changes/954-chat-im-id-flags.md b/.changes/released/1.0.59-beta.2/954-chat-im-id-flags.md similarity index 100% rename from .changes/954-chat-im-id-flags.md rename to .changes/released/1.0.59-beta.2/954-chat-im-id-flags.md diff --git a/.changes/chat-reference-card-verification.md b/.changes/released/1.0.59-beta.2/chat-reference-card-verification.md similarity index 100% rename from .changes/chat-reference-card-verification.md rename to .changes/released/1.0.59-beta.2/chat-reference-card-verification.md diff --git a/.changes/schema-catalog-json-projection.md b/.changes/released/1.0.59-beta.2/schema-catalog-json-projection.md similarity index 100% rename from .changes/schema-catalog-json-projection.md rename to .changes/released/1.0.59-beta.2/schema-catalog-json-projection.md diff --git a/.changes/wiki-shortcut-alignment.md b/.changes/released/1.0.59-beta.2/wiki-shortcut-alignment.md similarity index 100% rename from .changes/wiki-shortcut-alignment.md rename to .changes/released/1.0.59-beta.2/wiki-shortcut-alignment.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1f2dfc..9afe33b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,37 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th ## [Unreleased] +## [1.0.59-beta.2] - 2026-08-17 + +### Added + +- **Privacy-safe CLI telemetry** (#1009) — reports reviewed command outcomes and profile identity dimensions while excluding command arguments, output, paths, device fingerprints, and automatic system dimensions; `DO_NOT_TRACK=1` disables reporting. + +- **Feedback survey entry in root help** (#1019) — `dws --help` now closes with a Feedback section linking the user-experience survey form. + +- **Wiki Shortcut workflows** — publishes 20 reviewed space, member, node, and + activity shortcuts with strict collection validation, cursor handling, + write-terminal evidence, safe read-backs where the backend supports them, + task-oriented routing, and documented backend + boundaries. + +### Changed + +- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags. +- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces. +- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name. + +- **Faster Schema Catalog assembly** — projects typed values into payload JSON + without re-running a validation scan over documents `json.Marshal` has just + produced, cutting roughly a third of the projection work across the full tool + set. Untrusted JSON input keeps its existing validation. + +### Fixed + +- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write. +- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent. + + ## [1.0.59-beta.1] - 2026-08-14 ### Added From bfeb9f6af0af6e8f905fe1768540b192bcdb01e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:35:41 +0000 Subject: [PATCH 8/8] chore: update beta formula for v1.0.59-beta.2 [skip ci] --- Formula/dingtalk-workspace-cli-beta.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Formula/dingtalk-workspace-cli-beta.rb b/Formula/dingtalk-workspace-cli-beta.rb index 98fc206f..1d76aa1f 100644 --- a/Formula/dingtalk-workspace-cli-beta.rb +++ b/Formula/dingtalk-workspace-cli-beta.rb @@ -1,33 +1,33 @@ class DingtalkWorkspaceCliBeta < Formula desc "Automate DingTalk workspace tasks from the terminal (beta channel)" homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli" - version "1.0.59-beta.1" + version "1.0.59-beta.2" license "Apache-2.0" keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli" on_macos do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-darwin-arm64.tar.gz" - sha256 "36a30f3496e0f759c15c0b09f67dbd23b8ecdfff2eebe572f88125b26485830f" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.2/dws-darwin-arm64.tar.gz" + sha256 "7f11218d3222f3e93c3b1e94b3a004c061eb0a297b206447ea95fe6b2b1ec674" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-darwin-amd64.tar.gz" - sha256 "e7a04906380efd8da88cd112e6a512bb6470a3956dc370150037ed6e314db445" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.2/dws-darwin-amd64.tar.gz" + sha256 "72f06a334cf29d23123639fabe13bf2951765066136e47ccc6453667c382f8c2" end end on_linux do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-linux-arm64.tar.gz" - sha256 "f59ab055f3e841e4cebc964ae3ef969668475548abaaf8bede44afdca9a3e28d" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.2/dws-linux-arm64.tar.gz" + sha256 "3edbfabb7718b53914a2d9efe7b1152e9ebd70765ff0b6f19ad3125e4a2458ed" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-linux-amd64.tar.gz" - sha256 "2c8f919489d958c7d49262615e81faac70a9fbcae2d589ab54a0bb3c5700a057" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.2/dws-linux-amd64.tar.gz" + sha256 "e1c610070a9c1b3763656cbd53c818290f199097adc07cb9fe629ed765c5e1e0" end end resource "skills" do - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.1/dws-skills.zip" - sha256 "25f4a7e1d01fa4d771d79201b34b11ee8a24182bdcdc94bfb98d2bd5845bed3b" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.59-beta.2/dws-skills.zip" + sha256 "aa2854651eaa2c857b526aaec73fd1358d31b11999fe7fd3e99e5060b2522a1f" end def install