diff --git a/shortcuts/sheets/batch_op_contract_test.go b/shortcuts/sheets/batch_op_contract_test.go index 3f178006f..58d1d7847 100644 --- a/shortcuts/sheets/batch_op_contract_test.go +++ b/shortcuts/sheets/batch_op_contract_test.go @@ -132,8 +132,8 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) { { shortcut: "+range-sort", sc: RangeSort, - args: []string{"--sheet-id", "sh1", "--range", "A1:D10", "--sort-keys", `[{"col":"B","order":"asc"}]`, "--has-header"}, - subInput: `{"sheet-id":"sh1","range":"A1:D10","sort-keys":[{"col":"B","order":"asc"}],"has-header":true}`, + args: []string{"--sheet-id", "sh1", "--range", "A1:D10", "--sort-keys", `[{"column":"B","ascending":true}]`, "--has-header"}, + subInput: `{"sheet-id":"sh1","range":"A1:D10","sort-keys":[{"column":"B","ascending":true}],"has-header":true}`, }, { shortcut: "+sheet-create", diff --git a/shortcuts/sheets/data/flag-defs.json b/shortcuts/sheets/data/flag-defs.json index d8687e227..b1000009e 100644 --- a/shortcuts/sheets/data/flag-defs.json +++ b/shortcuts/sheets/data/flag-defs.json @@ -1874,17 +1874,6 @@ "stdin" ] }, - { - "name": "colors", - "kind": "own", - "type": "string", - "required": "optional", - "desc": "RGB hex color array (e.g. `[\"#1FB6C1\",\"#F006C2\"]`); length must equal `--options`", - "input": [ - "file", - "stdin" - ] - }, { "name": "multiple", "kind": "own", @@ -1892,13 +1881,6 @@ "required": "optional", "desc": "Enable multi-select; default `false`" }, - { - "name": "highlight", - "kind": "own", - "type": "bool", - "required": "optional", - "desc": "Color-highlight options; default `false`" - }, { "name": "dry-run", "kind": "system", @@ -2802,17 +2784,6 @@ "stdin" ] }, - { - "name": "colors", - "kind": "own", - "type": "string", - "required": "optional", - "desc": "Color array (same length as `--options`)", - "input": [ - "file", - "stdin" - ] - }, { "name": "multiple", "kind": "own", @@ -2820,13 +2791,6 @@ "required": "optional", "desc": "Enable multi-select" }, - { - "name": "highlight", - "kind": "own", - "type": "bool", - "required": "optional", - "desc": "Color-highlight options" - }, { "name": "dry-run", "kind": "system", diff --git a/shortcuts/sheets/helpers.go b/shortcuts/sheets/helpers.go index 64206b9e2..79b7223a3 100644 --- a/shortcuts/sheets/helpers.go +++ b/shortcuts/sheets/helpers.go @@ -156,7 +156,7 @@ func sheetSelectorPlaceholder(sheetID, sheetName string) string { // parseJSONFlag parses a JSON string from a flag value. Returns nil when the // flag is empty (caller decides if that's acceptable). Used by --data / -// --style / --options / --ranges / --colors and friends. +// --style / --options / --ranges / --properties and friends. func parseJSONFlag(runtime flagView, name string) (interface{}, error) { raw := strings.TrimSpace(runtime.Str(name)) if raw == "" { diff --git a/shortcuts/sheets/lark_sheet_batch_update.go b/shortcuts/sheets/lark_sheet_batch_update.go index d6591bb52..abf51868c 100644 --- a/shortcuts/sheets/lark_sheet_batch_update.go +++ b/shortcuts/sheets/lark_sheet_batch_update.go @@ -332,7 +332,7 @@ var DropdownUpdate = common.Shortcut{ if _, err := validateDropdownRanges(runtime); err != nil { return err } - if _, err := validateDropdownOptionsColors(runtime); err != nil { + if _, err := validateDropdownOptions(runtime); err != nil { return err } return nil diff --git a/shortcuts/sheets/lark_sheet_batch_update_test.go b/shortcuts/sheets/lark_sheet_batch_update_test.go index 26220ff2a..e7885ab5c 100644 --- a/shortcuts/sheets/lark_sheet_batch_update_test.go +++ b/shortcuts/sheets/lark_sheet_batch_update_test.go @@ -215,8 +215,12 @@ func TestDropdownUpdate_BatchPayload(t *testing.T) { if dv == nil || dv["type"] != "list" { t.Errorf("op[%d] missing data_validation list: %#v", i, cell) } - if dv["multiple_values"] != true { - t.Errorf("op[%d] multiple_values = %v, want true", i, dv["multiple_values"]) + items, _ := dv["items"].([]interface{}) + if len(items) != 3 { + t.Errorf("op[%d] data_validation.items length = %d, want 3", i, len(items)) + } + if dv["support_multiple_values"] != true { + t.Errorf("op[%d] support_multiple_values = %v, want true", i, dv["support_multiple_values"]) } } } diff --git a/shortcuts/sheets/lark_sheet_range_operations.go b/shortcuts/sheets/lark_sheet_range_operations.go index e30b9666c..2e00c11d5 100644 --- a/shortcuts/sheets/lark_sheet_range_operations.go +++ b/shortcuts/sheets/lark_sheet_range_operations.go @@ -598,6 +598,23 @@ func rangeSortInput(runtime flagView, token, sheetID, sheetName string) (map[str if err != nil { return nil, err } + // transform_range.sort_conditions[i] requires both `column` (string) + // and `ascending` (bool); the server's own validation surfaces a + // terse "required property X is missing" with no per-item context. + // Pre-check here so the user sees which entry is malformed. + for i, raw := range keys { + item, ok := raw.(map[string]interface{}) + if !ok { + return nil, common.FlagErrorf("--sort-keys[%d] must be an object {column, ascending}; got %T", i, raw) + } + col, _ := item["column"].(string) + if strings.TrimSpace(col) == "" { + return nil, common.FlagErrorf("--sort-keys[%d] missing required string field `column` (the column letter to sort by, e.g. \"C\")", i) + } + if _, ok := item["ascending"].(bool); !ok { + return nil, common.FlagErrorf("--sort-keys[%d] missing required bool field `ascending`", i) + } + } input := map[string]interface{}{ "excel_id": token, "operation": "sort", diff --git a/shortcuts/sheets/lark_sheet_range_operations_test.go b/shortcuts/sheets/lark_sheet_range_operations_test.go index 73930f697..8bc3c171d 100644 --- a/shortcuts/sheets/lark_sheet_range_operations_test.go +++ b/shortcuts/sheets/lark_sheet_range_operations_test.go @@ -185,7 +185,7 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) { { name: "+range-sort multi-key with header", sc: RangeSort, - args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:E100", "--has-header", "--sort-keys", `[{"col":"B","order":"asc"},{"col":"D","order":"desc"}]`}, + args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:E100", "--has-header", "--sort-keys", `[{"column":"B","ascending":true},{"column":"D","ascending":false}]`}, toolName: "transform_range", wantInput: map[string]interface{}{ "excel_id": testToken, @@ -194,8 +194,8 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) { "range": "A1:E100", "has_header": true, "sort_conditions": []interface{}{ - map[string]interface{}{"col": "B", "order": "asc"}, - map[string]interface{}{"col": "D", "order": "desc"}, + map[string]interface{}{"column": "B", "ascending": true}, + map[string]interface{}{"column": "D", "ascending": false}, }, }, }, @@ -211,6 +211,41 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) { } } +// TestRangeSort_RejectsMalformedKeys verifies the pre-check that each +// --sort-keys entry has both `column` (string) and `ascending` (bool); +// previously the CLI passed any JSON through and the server bounced +// with a terse "required property X missing" that didn't name the bad +// entry. +func TestRangeSort_RejectsMalformedKeys(t *testing.T) { + t.Parallel() + cases := []struct { + name string + keys string + want string + }{ + {"missing column", `[{"ascending":true}]`, "missing required string field `column`"}, + {"missing ascending", `[{"column":"B"}]`, "missing required bool field `ascending`"}, + {"old vocab col/order", `[{"col":"B","order":"asc"}]`, "missing required string field `column`"}, + {"non-object item", `["B"]`, "must be an object"}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + stdout, stderr, err := runShortcutCapturingErr(t, RangeSort, []string{ + "--url", testURL, "--sheet-id", testSheetID, + "--range", "A1:E10", "--sort-keys", c.keys, "--dry-run", + }) + if err == nil { + t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr) + } + if !strings.Contains(stdout+stderr+err.Error(), c.want) { + t.Errorf("want substring %q in error; got stdout=%s stderr=%s err=%v", c.want, stdout, stderr, err) + } + }) + } +} + func TestResize_TypeAndSizeGuards(t *testing.T) { t.Parallel() cases := []struct { diff --git a/shortcuts/sheets/lark_sheet_read_data.go b/shortcuts/sheets/lark_sheet_read_data.go index 7c036ac3e..89a4b49db 100644 --- a/shortcuts/sheets/lark_sheet_read_data.go +++ b/shortcuts/sheets/lark_sheet_read_data.go @@ -215,8 +215,12 @@ func stripRowPrefixFromCsvOutput(out interface{}) interface{} { } // DropdownGet wraps get_cell_ranges scoped to data_validation: read the -// dropdown configuration on a range. The range carries its own sheet prefix -// (e.g. "sheet1!A2:A100"), so no separate --sheet-id / --sheet-name is needed. +// dropdown configuration on a range. The CLI accepts the range in the +// sheet-prefixed form (e.g. "sheet1!A2:A100") for convenience; the +// prefix is split client-side into sheet_name + bare A1 because the +// get_cell_ranges tool wants sheet selector and ranges as separate +// fields (ranges with the "sheet!" prefix gets the empty-sheet_id +// rejection from the server). var DropdownGet = common.Shortcut{ Service: "sheets", Command: "+dropdown-get", @@ -257,10 +261,15 @@ var DropdownGet = common.Shortcut{ } func dropdownGetInput(runtime *common.RuntimeContext, token string) map[string]interface{} { - return map[string]interface{}{ + // Validate already enforced the "Sheet!range" prefix, so the + // split error path can't be reached here in practice. + sheetName, bareRange, _ := splitSheetPrefixedRange(strings.TrimSpace(runtime.Str("range"))) + input := map[string]interface{}{ "excel_id": token, - "ranges": []string{strings.TrimSpace(runtime.Str("range"))}, + "ranges": []string{bareRange}, "include_styles": false, "value_render_option": "formatted_value", } + sheetSelectorForToolInput(input, "", sheetName) + return input } diff --git a/shortcuts/sheets/lark_sheet_read_data_test.go b/shortcuts/sheets/lark_sheet_read_data_test.go index 30a9a1a55..78233f795 100644 --- a/shortcuts/sheets/lark_sheet_read_data_test.go +++ b/shortcuts/sheets/lark_sheet_read_data_test.go @@ -54,7 +54,8 @@ func TestReadDataShortcuts_DryRun(t *testing.T) { toolName: "get_cell_ranges", wantInput: map[string]interface{}{ "excel_id": testToken, - "ranges": []interface{}{"sheet1!A2:A100"}, + "sheet_name": "sheet1", + "ranges": []interface{}{"A2:A100"}, "include_styles": false, "value_render_option": "formatted_value", }, diff --git a/shortcuts/sheets/lark_sheet_workbook_test.go b/shortcuts/sheets/lark_sheet_workbook_test.go index f7b257fc2..5298d41c0 100644 --- a/shortcuts/sheets/lark_sheet_workbook_test.go +++ b/shortcuts/sheets/lark_sheet_workbook_test.go @@ -317,6 +317,15 @@ func TestWorkbookCreate_DryRun(t *testing.T) { if input["range"] != "A1:B3" { t.Errorf("fill range = %v, want A1:B3 (1 header + 2 data rows × 2 cols)", input["range"]) } + // New workbook → fill targets the default sheet by name (no + // extra get_workbook_structure call is needed to learn the + // auto-generated sheet_id). + if input["sheet_name"] != "Sheet1" { + t.Errorf("fill sheet_name = %v, want Sheet1", input["sheet_name"]) + } + if _, hasID := input["sheet_id"]; hasID { + t.Errorf("fill sheet_id should be omitted (server rejects empty); got %v", input["sheet_id"]) + } }) } diff --git a/shortcuts/sheets/lark_sheet_write_cells.go b/shortcuts/sheets/lark_sheet_write_cells.go index 3693129de..671fd673d 100644 --- a/shortcuts/sheets/lark_sheet_write_cells.go +++ b/shortcuts/sheets/lark_sheet_write_cells.go @@ -331,52 +331,36 @@ func dropdownSetInput(runtime flagView, token, sheetID, sheetName string) (map[s // ─── shared dropdown helpers ────────────────────────────────────────── -// buildDropdownValidation packs --options / --colors / --multiple / --highlight -// into the data_validation block expected by set_cell_range. +// buildDropdownValidation packs --options / --multiple into the +// data_validation block expected by set_cell_range. Field names match +// the canonical schema: items (not values) for the option list, and +// support_multiple_values (not multiple_values) for multi-select. +// Earlier CLI builds also emitted `colors` and `highlight_options`, +// neither of which exists in the server schema for set_cell_range; both +// were rejected as "unexpected property". The --colors / --highlight +// flags have been removed; only --options + --multiple remain. func buildDropdownValidation(runtime flagView) (map[string]interface{}, error) { options, err := requireJSONArray(runtime, "options") if err != nil { return nil, err } dv := map[string]interface{}{ - "type": "list", - "values": options, - } - if runtime.Str("colors") != "" { - colors, err := requireJSONArray(runtime, "colors") - if err != nil { - return nil, err - } - if len(colors) != len(options) { - return nil, common.FlagErrorf("--colors length (%d) must equal --options length (%d)", len(colors), len(options)) - } - dv["colors"] = colors + "type": "list", + "items": options, } if runtime.Bool("multiple") { - dv["multiple_values"] = true - } - if runtime.Bool("highlight") { - dv["highlight_options"] = true + dv["support_multiple_values"] = true } return dv, nil } -// validateDropdownOptionsColors validates --options is a JSON array and that -// --colors (when set) has matching length. Used by +dropdown-update Validate. -func validateDropdownOptionsColors(runtime flagView) (int, error) { +// validateDropdownOptions checks that --options parses as a JSON array +// and returns its length so callers can size their cells matrix. +func validateDropdownOptions(runtime flagView) (int, error) { options, err := requireJSONArray(runtime, "options") if err != nil { return 0, err } - if runtime.Str("colors") != "" { - colors, err := requireJSONArray(runtime, "colors") - if err != nil { - return 0, err - } - if len(colors) != len(options) { - return 0, common.FlagErrorf("--colors length (%d) must equal --options length (%d)", len(colors), len(options)) - } - } return len(options), nil } diff --git a/shortcuts/sheets/lark_sheet_write_cells_test.go b/shortcuts/sheets/lark_sheet_write_cells_test.go index e9a0b289e..2e3543bb9 100644 --- a/shortcuts/sheets/lark_sheet_write_cells_test.go +++ b/shortcuts/sheets/lark_sheet_write_cells_test.go @@ -95,7 +95,7 @@ func TestWriteCellsShortcuts_DryRun(t *testing.T) { "--url", testURL, "--sheet-id", testSheetID, "--range", "A2:A4", "--options", `["a","b"]`, - "--multiple", "--highlight", + "--multiple", }, toolName: "set_cell_range", wantInput: map[string]interface{}{ @@ -143,8 +143,15 @@ func TestDropdownSet_CellsShape(t *testing.T) { if dv["type"] != "list" { t.Errorf("row %d data_validation.type = %v, want list", i, dv["type"]) } - if dv["multiple_values"] != true { - t.Errorf("row %d data_validation.multiple_values = %v, want true", i, dv["multiple_values"]) + items, _ := dv["items"].([]interface{}) + if len(items) != 2 || items[0] != "a" || items[1] != "b" { + t.Errorf("row %d data_validation.items = %#v, want [\"a\",\"b\"]", i, dv["items"]) + } + if dv["support_multiple_values"] != true { + t.Errorf("row %d data_validation.support_multiple_values = %v, want true", i, dv["support_multiple_values"]) + } + if _, hasLegacy := dv["multiple_values"]; hasLegacy { + t.Errorf("row %d data_validation should not emit legacy `multiple_values`", i) } } } diff --git a/skills/lark-sheets/references/lark-sheets-batch-update.md b/skills/lark-sheets/references/lark-sheets-batch-update.md index ba3160e0b..7b04905a1 100644 --- a/skills/lark-sheets/references/lark-sheets-batch-update.md +++ b/skills/lark-sheets/references/lark-sheets-batch-update.md @@ -70,9 +70,7 @@ _公共:URL/token(无 sheet 定位) · 系统:`--dry-run`_ | --- | --- | --- | --- | | `--ranges` | string + File + Stdin(简单 JSON) | required | 目标范围 JSON 数组(如 `["sheet1!A2:A100"]`),每项必须带 sheet 前缀 | | `--options` | string + File + Stdin(复合 JSON) | required | 选项 JSON 数组(如 `["opt1","opt2"]`) | -| `--colors` | string + File + Stdin(简单 JSON) | optional | 颜色数组(与 `--options` 等长) | | `--multiple` | bool | optional | 启用多选 | -| `--highlight` | bool | optional | 选项配色 | ### `+dropdown-delete` diff --git a/skills/lark-sheets/references/lark-sheets-write-cells.md b/skills/lark-sheets/references/lark-sheets-write-cells.md index 6e1887031..6ebe2b154 100644 --- a/skills/lark-sheets/references/lark-sheets-write-cells.md +++ b/skills/lark-sheets/references/lark-sheets-write-cells.md @@ -199,9 +199,7 @@ _公共四件套 · 系统:`--dry-run`_ | --- | --- | --- | --- | | `--range` | string | required | 目标范围(A1 格式,如 `A2:A100`) | | `--options` | string + File + Stdin(复合 JSON) | required | 选项 JSON 数组 `["opt1","opt2"]`;最多 500 项,每项 ≤100 字符,不含逗号 | -| `--colors` | string + File + Stdin(简单 JSON) | optional | RGB hex 颜色数组(如 `["#1FB6C1","#F006C2"]`),长度必须与 `--options` 一致 | | `--multiple` | bool | optional | 启用多选;默认 `false` | -| `--highlight` | bool | optional | 选项配色显示;默认 `false` | ### `+csv-put`