diff --git a/shortcuts/sheets/helpers.go b/shortcuts/sheets/helpers.go index 4fbcfb730..51874f4d6 100644 --- a/shortcuts/sheets/helpers.go +++ b/shortcuts/sheets/helpers.go @@ -8,6 +8,7 @@ package sheets import ( + "encoding/json" "strings" "github.com/larksuite/cli/internal/validate" @@ -118,3 +119,50 @@ func sheetSelectorPlaceholder(sheetID, sheetName string) string { } return "" } + +// 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. +func parseJSONFlag(runtime *common.RuntimeContext, name string) (interface{}, error) { + raw := strings.TrimSpace(runtime.Str(name)) + if raw == "" { + return nil, nil + } + var out interface{} + if err := json.Unmarshal([]byte(raw), &out); err != nil { + return nil, common.FlagErrorf("--%s: invalid JSON: %v", name, err) + } + return out, nil +} + +// requireJSONObject is parseJSONFlag + a type assertion to map[string]interface{}. +func requireJSONObject(runtime *common.RuntimeContext, name string) (map[string]interface{}, error) { + v, err := parseJSONFlag(runtime, name) + if err != nil { + return nil, err + } + if v == nil { + return nil, common.FlagErrorf("--%s is required", name) + } + m, ok := v.(map[string]interface{}) + if !ok { + return nil, common.FlagErrorf("--%s must be a JSON object", name) + } + return m, nil +} + +// requireJSONArray is parseJSONFlag + a type assertion to []interface{}. +func requireJSONArray(runtime *common.RuntimeContext, name string) ([]interface{}, error) { + v, err := parseJSONFlag(runtime, name) + if err != nil { + return nil, err + } + if v == nil { + return nil, common.FlagErrorf("--%s is required", name) + } + a, ok := v.([]interface{}) + if !ok { + return nil, common.FlagErrorf("--%s must be a JSON array", name) + } + return a, nil +} diff --git a/shortcuts/sheets/lark_sheet_read_data.go b/shortcuts/sheets/lark_sheet_read_data.go new file mode 100644 index 000000000..428c209ab --- /dev/null +++ b/shortcuts/sheets/lark_sheet_read_data.go @@ -0,0 +1,271 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "strings" + + "github.com/larksuite/cli/shortcuts/common" +) + +// ─── lark_sheet_read_data ───────────────────────────────────────────── +// +// Wraps: +// - get_cell_ranges (powers +cells-get and +dropdown-get) +// - get_range_as_csv (powers +csv-get) +// +// The sandbox tool (export_sheet_to_sandbox) is Sheet-Tool-only and has no +// CLI surface here. + +var cellsGetIncludeEnum = []string{"value", "formula", "style", "comment", "data_validation"} + +// CellsGet wraps get_cell_ranges: read multiple A1 ranges and return per-cell +// values, formulas, styles, and other metadata as requested via --include. +var CellsGet = common.Shortcut{ + Service: "sheets", + Command: "+cells-get", + Description: "Read one or more cell ranges with values, formulas, and optional styles / comments / data validation.", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "ranges", Type: "string_array", Required: true, Desc: "A1 ranges (repeat: --ranges A1:B2 --ranges D1:E5)"}, + common.Flag{Name: "include", Type: "string_slice", Enum: cellsGetIncludeEnum, Desc: "categories to include (default: value+style). value|formula|style|comment|data_validation"}, + common.Flag{Name: "skip-hidden", Type: "bool", Desc: "skip hidden rows/cols"}, + common.Flag{Name: "cell-limit", Type: "int", Default: "5000", Hidden: true, Desc: "anti-burst cell scan cap"}, + common.Flag{Name: "max-chars", Type: "int", Default: "200000", Hidden: true, Desc: "anti-burst response char cap"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + if len(runtime.StrArray("ranges")) == 0 { + return common.FlagErrorf("--ranges is required") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + return invokeToolDryRun(token, ToolKindRead, "get_cell_ranges", cellsGetInput(runtime, token, sheetID, sheetName)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindRead, "get_cell_ranges", cellsGetInput(runtime, token, sheetID, sheetName)) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func cellsGetInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} { + input := map[string]interface{}{ + "excel_id": token, + "ranges": runtime.StrArray("ranges"), + } + sheetSelectorForToolInput(input, sheetID, sheetName) + applyIncludeToCellsGet(input, runtime.StrSlice("include")) + if runtime.Bool("skip-hidden") { + input["skip_hidden"] = true + } + if n := runtime.Int("cell-limit"); n > 0 { + input["cell_limit"] = n + } + if n := runtime.Int("max-chars"); n > 0 { + input["max_chars"] = n + } + return input +} + +// applyIncludeToCellsGet maps the fine-grained --include vocabulary to the +// tool's two coarse switches: +// +// - include_styles (bool) — toggled by "style" presence +// - value_render_option (enum) — "formula" → formula; otherwise omitted +// +// "value", "comment", and "data_validation" are always returned by the tool +// per the schema; they have no dedicated knob today but are accepted in +// --include for forward-compat with finer-grained server support. +func applyIncludeToCellsGet(input map[string]interface{}, include []string) { + if len(include) == 0 { + return + } + want := map[string]bool{} + for _, v := range include { + want[v] = true + } + if want["style"] { + input["include_styles"] = true + } else { + input["include_styles"] = false + } + if want["formula"] { + input["value_render_option"] = "formula" + } +} + +// CsvGet wraps get_range_as_csv: pull one range as RFC 4180 CSV with optional +// [row=N] line prefix for easy row-number lookup. +var CsvGet = common.Shortcut{ + Service: "sheets", + Command: "+csv-get", + Description: "Read a range as CSV (with [row=N] line prefix by default).", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "range", Desc: "A1 range; omit for the sheet's current_region"}, + common.Flag{ + Name: "value-render-option", Enum: []string{"formatted_value", "raw_value", "formula"}, + Desc: "value rendering: formatted_value (default) / raw_value / formula", + }, + common.Flag{Name: "include-row-prefix", Type: "bool", Default: "true", Desc: "keep [row=N] line prefix; pass --include-row-prefix=false to strip"}, + common.Flag{Name: "skip-hidden", Type: "bool", Desc: "skip hidden rows/cols"}, + common.Flag{Name: "max-rows", Type: "int", Default: "100000", Hidden: true, Desc: "anti-burst row cap"}, + common.Flag{Name: "max-chars", Type: "int", Default: "200000", Hidden: true, Desc: "anti-burst char cap"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + _, _, err := resolveSheetSelector(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + return invokeToolDryRun(token, ToolKindRead, "get_range_as_csv", csvGetInput(runtime, token, sheetID, sheetName)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindRead, "get_range_as_csv", csvGetInput(runtime, token, sheetID, sheetName)) + if err != nil { + return err + } + if !runtime.Bool("include-row-prefix") { + out = stripRowPrefixFromCsvOutput(out) + } + runtime.Out(out, nil) + return nil + }, +} + +func csvGetInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} { + input := map[string]interface{}{"excel_id": token} + sheetSelectorForToolInput(input, sheetID, sheetName) + if r := strings.TrimSpace(runtime.Str("range")); r != "" { + input["range"] = r + } + if v := runtime.Str("value-render-option"); v != "" { + input["value_render_option"] = v + } + if runtime.Bool("skip-hidden") { + input["skip_hidden"] = true + } + if n := runtime.Int("max-rows"); n > 0 { + input["max_rows"] = n + } + if n := runtime.Int("max-chars"); n > 0 { + input["max_chars"] = n + } + return input +} + +// stripRowPrefixFromCsvOutput removes "[row=N]" line prefixes from the tool's +// annotated_csv field. Operates client-side because the tool only emits the +// annotated form. +func stripRowPrefixFromCsvOutput(out interface{}) interface{} { + m, ok := out.(map[string]interface{}) + if !ok { + return out + } + csv, ok := m["annotated_csv"].(string) + if !ok { + return out + } + lines := strings.Split(csv, "\n") + for i, line := range lines { + if idx := strings.Index(line, "]"); idx >= 0 && strings.HasPrefix(line, "[row=") { + rest := line[idx+1:] + lines[i] = strings.TrimPrefix(rest, ",") + } + } + m["annotated_csv"] = strings.Join(lines, "\n") + return m +} + +// 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. +var DropdownGet = common.Shortcut{ + Service: "sheets", + Command: "+dropdown-get", + Description: "Read the dropdown / data-validation configuration on a sheet-prefixed range.", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicTokenFlags(), + common.Flag{Name: "range", Required: true, Desc: "A1 range with sheet prefix (e.g. sheet1!A2:A100)"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if strings.TrimSpace(runtime.Str("range")) == "" { + return common.FlagErrorf("--range is required") + } + if !strings.Contains(runtime.Str("range"), "!") { + return common.FlagErrorf("--range must include a sheet prefix (e.g. sheet1!A2:A100)") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + return invokeToolDryRun(token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindRead, "get_cell_ranges", dropdownGetInput(runtime, token)) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func dropdownGetInput(runtime *common.RuntimeContext, token string) map[string]interface{} { + return map[string]interface{}{ + "excel_id": token, + "ranges": []string{strings.TrimSpace(runtime.Str("range"))}, + "include_styles": false, + "value_render_option": "formatted_value", + } +} diff --git a/shortcuts/sheets/lark_sheet_search_replace.go b/shortcuts/sheets/lark_sheet_search_replace.go new file mode 100644 index 000000000..a332b207f --- /dev/null +++ b/shortcuts/sheets/lark_sheet_search_replace.go @@ -0,0 +1,189 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "strings" + + "github.com/larksuite/cli/shortcuts/common" +) + +// ─── lark_sheet_search_replace ──────────────────────────────────────── +// +// Wraps search_data (read) and replace_data (write). Both tools take an +// `options` sub-object; the CLI flattens its common booleans +// (--match-case / --match-entire-cell / --regex / --include-formulas) into +// independent flags per the铁律. + +// CellsSearch wraps search_data: find cell coordinates matching --find, +// with optional case / regex / whole-cell / formula-text controls. +var CellsSearch = common.Shortcut{ + Service: "sheets", + Command: "+cells-search", + Description: "Find cells matching --find in a spreadsheet (case / regex / whole-cell / formula-text controls).", + Risk: "read", + Scopes: []string{"sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "find", Required: true, Desc: "text to search for (regex when --regex is set)"}, + common.Flag{Name: "range", Desc: "optional A1 range to scope the search"}, + common.Flag{Name: "match-case", Type: "bool", Desc: "case-sensitive match"}, + common.Flag{Name: "match-entire-cell", Type: "bool", Desc: "match the entire cell content only"}, + common.Flag{Name: "regex", Type: "bool", Desc: "treat --find as a regular expression"}, + common.Flag{Name: "include-formulas", Type: "bool", Desc: "also search inside formula text"}, + common.Flag{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset (use next_offset from previous page)"}, + common.Flag{Name: "max-matches", Type: "int", Default: "5000", Hidden: true, Desc: "anti-burst match cap"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + if strings.TrimSpace(runtime.Str("find")) == "" { + return common.FlagErrorf("--find is required") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + return invokeToolDryRun(token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName)) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func searchInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} { + input := map[string]interface{}{ + "excel_id": token, + "search_term": runtime.Str("find"), + } + sheetSelectorForToolInput(input, sheetID, sheetName) + if r := strings.TrimSpace(runtime.Str("range")); r != "" { + input["range"] = r + } + if runtime.Changed("offset") && runtime.Int("offset") > 0 { + input["offset"] = runtime.Int("offset") + } + if opts := searchReplaceOptions(runtime); len(opts) > 0 { + input["options"] = opts + } + if n := runtime.Int("max-matches"); n > 0 { + input["max_matches"] = n + } + return input +} + +// searchReplaceOptions packs the four shared boolean flags into the tool's +// `options` sub-object. Empty result → caller should omit the field. +func searchReplaceOptions(runtime *common.RuntimeContext) map[string]interface{} { + opts := map[string]interface{}{} + if runtime.Bool("match-case") { + opts["match_case"] = true + } + if runtime.Bool("match-entire-cell") { + opts["match_entire_cell"] = true + } + if runtime.Bool("regex") { + opts["regex"] = true + } + if runtime.Bool("include-formulas") { + opts["include_formulas"] = true + } + return opts +} + +// CellsReplace wraps replace_data: find and replace text across a +// spreadsheet, with the same option controls as +cells-search. +var CellsReplace = common.Shortcut{ + Service: "sheets", + Command: "+cells-replace", + Description: "Find and replace text in a spreadsheet (case / regex / whole-cell / formula-text controls).", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "find", Required: true, Desc: "text to find (regex when --regex is set)"}, + common.Flag{Name: "replace", Required: true, Desc: "replacement text (empty string deletes the match)"}, + common.Flag{Name: "range", Desc: "optional A1 range to scope the replace"}, + common.Flag{Name: "match-case", Type: "bool", Desc: "case-sensitive match"}, + common.Flag{Name: "match-entire-cell", Type: "bool", Desc: "match the entire cell content only"}, + common.Flag{Name: "regex", Type: "bool", Desc: "treat --find as a regular expression"}, + common.Flag{Name: "include-formulas", Type: "bool", Desc: "also replace inside formula text"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + if strings.TrimSpace(runtime.Str("find")) == "" { + return common.FlagErrorf("--find is required") + } + if !runtime.Changed("replace") { + return common.FlagErrorf("--replace is required (pass an empty string to delete matches)") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + return invokeToolDryRun(token, ToolKindWrite, "replace_data", replaceInput(runtime, token, sheetID, sheetName)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "replace_data", replaceInput(runtime, token, sheetID, sheetName)) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, + Tips: []string{ + "Always preview with --dry-run before running — replace can mutate every matching cell across the sheet.", + }, +} + +func replaceInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} { + input := map[string]interface{}{ + "excel_id": token, + "search_term": runtime.Str("find"), + "replace_term": runtime.Str("replace"), + } + sheetSelectorForToolInput(input, sheetID, sheetName) + if r := strings.TrimSpace(runtime.Str("range")); r != "" { + input["range"] = r + } + if opts := searchReplaceOptions(runtime); len(opts) > 0 { + input["options"] = opts + } + return input +} diff --git a/shortcuts/sheets/lark_sheet_write_cells.go b/shortcuts/sheets/lark_sheet_write_cells.go new file mode 100644 index 000000000..f757a9f4c --- /dev/null +++ b/shortcuts/sheets/lark_sheet_write_cells.go @@ -0,0 +1,750 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/larksuite/cli/shortcuts/common" +) + +// ─── lark_sheet_write_cells ─────────────────────────────────────────── +// +// Wraps: +// - set_cell_range (powers +cells-set / +cells-set-style / +// +dropdown-set / +dropdown-update / +dropdown-delete) +// - set_range_from_csv (powers +csv-put) +// +// +cells-set-image is a `cli_only_derivative` shortcut (needs a local file +// upload before calling set_cell_range); it lives in the cli-only batch +// where the upload helper is shared with +workbook-create / +dim-move / +// +workbook-export. +// +// All set_cell_range-backed shortcuts construct a cells matrix whose +// dimensions exactly match the target range — the tool errors on mismatch. + +// CellsSet wraps set_cell_range with raw --data: caller provides the cells +// matrix (and any optional copy_to_range / resize_* fields) as JSON. +var CellsSet = common.Shortcut{ + Service: "sheets", + Command: "+cells-set", + Description: "Write values / formulas / styles / comments / data validation / embed-image to a cell range.", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "range", Required: true, Desc: "target A1 range (e.g. A1:C10); cells dimensions must match"}, + common.Flag{Name: "data", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "JSON body: { \"cells\": [[{value|formula|cell_styles|...}, ...]], optional copy_to_range / resize_width / resize_height }"}, + common.Flag{Name: "allow-overwrite", Type: "bool", Default: "true", Desc: "allow overwriting non-empty cells (default true)"}, + common.Flag{Name: "max-cells", Type: "int", Default: "50000", Hidden: true, Desc: "anti-burst cells write cap"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + if strings.TrimSpace(runtime.Str("range")) == "" { + return common.FlagErrorf("--range is required") + } + body, err := requireJSONObject(runtime, "data") + if err != nil { + return err + } + if _, ok := body["cells"]; !ok { + return common.FlagErrorf("--data must include a \"cells\" field") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + input, _ := cellsSetInput(runtime, token, sheetID, sheetName) + return invokeToolDryRun(token, ToolKindWrite, "set_cell_range", input) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + input, err := cellsSetInput(runtime, token, sheetID, sheetName) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_cell_range", input) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func cellsSetInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) (map[string]interface{}, error) { + body, err := requireJSONObject(runtime, "data") + if err != nil { + return nil, err + } + input := map[string]interface{}{ + "excel_id": token, + "range": strings.TrimSpace(runtime.Str("range")), + } + sheetSelectorForToolInput(input, sheetID, sheetName) + // --data fields override any of these except the core selectors. + for k, v := range body { + switch k { + case "excel_id", "range", "sheet_id", "sheet_name": + // reserved for flat flags + default: + input[k] = v + } + } + if !runtime.Bool("allow-overwrite") { + input["allow_overwrite"] = false + } + return input, nil +} + +// CellsSetStyle wraps set_cell_range applied to a uniform style: parse +// --style once, fan it out to a (rows × cols) cells matrix, and let +// set_cell_range stamp every cell in the range with that style. +var CellsSetStyle = common.Shortcut{ + Service: "sheets", + Command: "+cells-set-style", + Description: "Apply a single style block to every cell in a range (values / formulas untouched).", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "range", Required: true, Desc: "target A1 range (e.g. A1:B2)"}, + common.Flag{Name: "style", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "style JSON: { font, backColor, horizontal_alignment, vertical_alignment, ... , optional border_styles }"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + r := strings.TrimSpace(runtime.Str("range")) + if r == "" { + return common.FlagErrorf("--range is required") + } + if _, _, err := rangeDimensions(r); err != nil { + return common.FlagErrorf("--range %q: %v", r, err) + } + if _, err := requireJSONObject(runtime, "style"); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + input, _ := cellsSetStyleInput(runtime, token, sheetID, sheetName) + return invokeToolDryRun(token, ToolKindWrite, "set_cell_range", input) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + input, err := cellsSetStyleInput(runtime, token, sheetID, sheetName) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_cell_range", input) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func cellsSetStyleInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) (map[string]interface{}, error) { + style, err := requireJSONObject(runtime, "style") + if err != nil { + return nil, err + } + rangeStr := strings.TrimSpace(runtime.Str("range")) + rows, cols, err := rangeDimensions(rangeStr) + if err != nil { + return nil, common.FlagErrorf("--range %q: %v", rangeStr, err) + } + // Split border_styles out of the style block since the tool models it + // as a sibling field of cell_styles. + cellStyle := map[string]interface{}{} + var borderStyles interface{} + for k, v := range style { + if k == "border_styles" { + borderStyles = v + continue + } + cellStyle[k] = v + } + cells := make([][]interface{}, rows) + for r := range cells { + row := make([]interface{}, cols) + for c := range row { + cell := map[string]interface{}{"cell_styles": cellStyle} + if borderStyles != nil { + cell["border_styles"] = borderStyles + } + row[c] = cell + } + cells[r] = row + } + input := map[string]interface{}{ + "excel_id": token, + "range": rangeStr, + "cells": cells, + } + sheetSelectorForToolInput(input, sheetID, sheetName) + return input, nil +} + +// CsvPut wraps set_range_from_csv: dump a CSV blob into a sheet, only writing +// plain values. Use +cells-set for anything richer (formula / style / note). +var CsvPut = common.Shortcut{ + Service: "sheets", + Command: "+csv-put", + Description: "Paste RFC-4180 CSV into a sheet at --start-cell (plain values only, auto-expands sheet if needed).", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "csv", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "CSV text (RFC 4180); supports @file or stdin via -"}, + common.Flag{Name: "start-cell", Default: "A1", Required: true, Desc: "single A1 anchor cell, e.g. A1 / B5"}, + common.Flag{Name: "allow-overwrite", Type: "bool", Default: "true", + Desc: "allow overwriting non-empty cells (default true); false errors if any target cell is non-empty"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + if strings.TrimSpace(runtime.Str("csv")) == "" { + return common.FlagErrorf("--csv is required") + } + anchor := strings.TrimSpace(runtime.Str("start-cell")) + if anchor == "" { + return common.FlagErrorf("--start-cell is required") + } + if _, _, ok := splitCellRef(anchor); !ok { + return common.FlagErrorf("--start-cell %q must be a single cell ref (e.g. A1)", anchor) + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + return invokeToolDryRun(token, ToolKindWrite, "set_range_from_csv", csvPutInput(runtime, token, sheetID, sheetName)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_range_from_csv", csvPutInput(runtime, token, sheetID, sheetName)) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func csvPutInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} { + input := map[string]interface{}{ + "excel_id": token, + "csv": runtime.Str("csv"), + "start_cell": strings.TrimSpace(runtime.Str("start-cell")), + } + sheetSelectorForToolInput(input, sheetID, sheetName) + if !runtime.Bool("allow-overwrite") { + input["allow_overwrite"] = false + } + return input +} + +// ─── +dropdown-* (set_cell_range via data_validation) ───────────────── +// +// All three dropdown shortcuts stamp a `data_validation` block on every cell +// of the target range(s). set / update / delete differ in (a) how many +// ranges they accept and (b) whether the block is populated or null. + +// DropdownSet places a single dropdown on one range. +var DropdownSet = common.Shortcut{ + Service: "sheets", + Command: "+dropdown-set", + Description: "Attach a dropdown / data-validation list to every cell in --range.", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicSheetFlags(), + common.Flag{Name: "range", Required: true, Desc: "target A1 range (e.g. A2:A100)"}, + common.Flag{Name: "options", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "options JSON array (e.g. [\"opt1\",\"opt2\"]); ≤500 items, ≤100 chars each, no commas"}, + common.Flag{Name: "colors", Input: []string{common.File, common.Stdin}, + Desc: "optional RGB hex array (e.g. [\"#1FB6C1\",\"#F006C2\"]); length must equal --options"}, + common.Flag{Name: "multiple", Type: "bool", Desc: "enable multi-select; default false"}, + common.Flag{Name: "highlight", Type: "bool", Desc: "color-highlight options; default false"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, _, err := resolveSheetSelector(runtime); err != nil { + return err + } + r := strings.TrimSpace(runtime.Str("range")) + if r == "" { + return common.FlagErrorf("--range is required") + } + if _, _, err := rangeDimensions(r); err != nil { + return common.FlagErrorf("--range %q: %v", r, err) + } + if _, err := validateDropdownOptionsColors(runtime); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + sheetID, sheetName, _ := resolveSheetSelector(runtime) + input, _ := dropdownSetInput(runtime, token, sheetID, sheetName) + return invokeToolDryRun(token, ToolKindWrite, "set_cell_range", input) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + sheetID, sheetName, err := resolveSheetSelector(runtime) + if err != nil { + return err + } + input, err := dropdownSetInput(runtime, token, sheetID, sheetName) + if err != nil { + return err + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_cell_range", input) + if err != nil { + return err + } + runtime.Out(out, nil) + return nil + }, +} + +func dropdownSetInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) (map[string]interface{}, error) { + validation, err := buildDropdownValidation(runtime) + if err != nil { + return nil, err + } + rangeStr := strings.TrimSpace(runtime.Str("range")) + rows, cols, err := rangeDimensions(rangeStr) + if err != nil { + return nil, common.FlagErrorf("--range %q: %v", rangeStr, err) + } + cells := fillCellsMatrix(rows, cols, map[string]interface{}{"data_validation": validation}) + input := map[string]interface{}{ + "excel_id": token, + "range": rangeStr, + "cells": cells, + } + sheetSelectorForToolInput(input, sheetID, sheetName) + return input, nil +} + +// DropdownUpdate replaces (or installs) dropdowns on multiple ranges via +// sequential set_cell_range calls. Sheet ids are derived from the per-range +// sheet prefix; the public --sheet-id / --sheet-name flags are not used here. +var DropdownUpdate = common.Shortcut{ + Service: "sheets", + Command: "+dropdown-update", + Description: "Update dropdown configuration across multiple sheet-prefixed ranges.", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicTokenFlags(), + common.Flag{Name: "ranges", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "JSON array of sheet-prefixed A1 ranges (e.g. [\"sheet1!A2:A100\"])"}, + common.Flag{Name: "options", Input: []string{common.File, common.Stdin}, Required: true, Desc: "options JSON array"}, + common.Flag{Name: "colors", Input: []string{common.File, common.Stdin}, Desc: "optional hex color array (must equal --options length)"}, + common.Flag{Name: "multiple", Type: "bool", Desc: "enable multi-select"}, + common.Flag{Name: "highlight", Type: "bool", Desc: "color-highlight options"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if _, err := validateDropdownRanges(runtime); err != nil { + return err + } + if _, err := validateDropdownOptionsColors(runtime); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + validation, _ := buildDropdownValidation(runtime) + ranges, _ := validateDropdownRanges(runtime) + dry := common.NewDryRunAPI() + for _, rng := range ranges { + sheet, sub, _ := splitSheetPrefixedRange(rng) + rows, cols, _ := rangeDimensions(sub) + cells := fillCellsMatrix(rows, cols, map[string]interface{}{"data_validation": validation}) + body, _ := buildToolBody("set_cell_range", map[string]interface{}{ + "excel_id": token, + "range": sub, + "sheet_name": sheet, + "cells": cells, + }) + dry.POST(toolInvokePath(token, ToolKindWrite)).Body(body) + } + return dry. + Set("spreadsheet_token", token). + Set("tool_name", "set_cell_range"). + Set("invocation_count", len(ranges)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + validation, err := buildDropdownValidation(runtime) + if err != nil { + return err + } + ranges, err := validateDropdownRanges(runtime) + if err != nil { + return err + } + results := make([]interface{}, 0, len(ranges)) + for _, rng := range ranges { + sheet, sub, err := splitSheetPrefixedRange(rng) + if err != nil { + return err + } + rows, cols, err := rangeDimensions(sub) + if err != nil { + return common.FlagErrorf("--ranges %q: %v", rng, err) + } + cells := fillCellsMatrix(rows, cols, map[string]interface{}{"data_validation": validation}) + input := map[string]interface{}{ + "excel_id": token, + "range": sub, + "sheet_name": sheet, + "cells": cells, + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_cell_range", input) + if err != nil { + return fmt.Errorf("range %q failed: %w (partial: %d/%d done)", rng, err, len(results), len(ranges)) + } + results = append(results, map[string]interface{}{"range": rng, "result": out}) + } + runtime.Out(map[string]interface{}{"results": results}, nil) + return nil + }, + Tips: []string{ + "Calls set_cell_range once per range. A mid-batch failure leaves earlier ranges already updated — use --dry-run first to confirm the list.", + }, +} + +// DropdownDelete clears dropdowns from one or more sheet-prefixed ranges. +// high-risk-write — irreversible removal of validation rules. +var DropdownDelete = common.Shortcut{ + Service: "sheets", + Command: "+dropdown-delete", + Description: "Remove dropdowns from one or more sheet-prefixed ranges (irreversible).", + Risk: "high-risk-write", + Scopes: []string{"sheets:spreadsheet:write_only"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: append(publicTokenFlags(), + common.Flag{Name: "ranges", Input: []string{common.File, common.Stdin}, Required: true, + Desc: "JSON array of sheet-prefixed A1 ranges (max 100)"}, + ), + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + ranges, err := validateDropdownRanges(runtime) + if err != nil { + return err + } + if len(ranges) > 100 { + return common.FlagErrorf("--ranges accepts at most 100 entries; got %d", len(ranges)) + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token, _ := resolveSpreadsheetToken(runtime) + ranges, _ := validateDropdownRanges(runtime) + dry := common.NewDryRunAPI() + for _, rng := range ranges { + sheet, sub, _ := splitSheetPrefixedRange(rng) + rows, cols, _ := rangeDimensions(sub) + cells := fillCellsMatrix(rows, cols, map[string]interface{}{"data_validation": nil}) + body, _ := buildToolBody("set_cell_range", map[string]interface{}{ + "excel_id": token, + "range": sub, + "sheet_name": sheet, + "cells": cells, + }) + dry.POST(toolInvokePath(token, ToolKindWrite)).Body(body) + } + return dry. + Set("spreadsheet_token", token). + Set("tool_name", "set_cell_range"). + Set("invocation_count", len(ranges)) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token, err := resolveSpreadsheetToken(runtime) + if err != nil { + return err + } + ranges, err := validateDropdownRanges(runtime) + if err != nil { + return err + } + results := make([]interface{}, 0, len(ranges)) + for _, rng := range ranges { + sheet, sub, err := splitSheetPrefixedRange(rng) + if err != nil { + return err + } + rows, cols, err := rangeDimensions(sub) + if err != nil { + return common.FlagErrorf("--ranges %q: %v", rng, err) + } + cells := fillCellsMatrix(rows, cols, map[string]interface{}{"data_validation": nil}) + input := map[string]interface{}{ + "excel_id": token, + "range": sub, + "sheet_name": sheet, + "cells": cells, + } + out, err := callTool(ctx, runtime, token, ToolKindWrite, "set_cell_range", input) + if err != nil { + return fmt.Errorf("range %q failed: %w (partial: %d/%d done)", rng, err, len(results), len(ranges)) + } + results = append(results, map[string]interface{}{"range": rng, "result": out}) + } + runtime.Out(map[string]interface{}{"results": results}, nil) + return nil + }, + Tips: []string{ + "Calls set_cell_range once per range. A mid-batch failure leaves earlier ranges already cleared.", + }, +} + +// ─── shared dropdown helpers ────────────────────────────────────────── + +// buildDropdownValidation packs --options / --colors / --multiple / --highlight +// into the data_validation block expected by set_cell_range. +func buildDropdownValidation(runtime *common.RuntimeContext) (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 + } + if runtime.Bool("multiple") { + dv["multiple_values"] = true + } + if runtime.Bool("highlight") { + dv["highlight_options"] = true + } + return dv, nil +} + +// validateDropdownRanges parses --ranges, requires every entry to carry a +// sheet prefix, and returns the parsed list. +func validateDropdownRanges(runtime *common.RuntimeContext) ([]string, error) { + raw, err := requireJSONArray(runtime, "ranges") + if err != nil { + return nil, err + } + out := make([]string, 0, len(raw)) + for i, v := range raw { + s, ok := v.(string) + if !ok { + return nil, common.FlagErrorf("--ranges[%d] must be a string", i) + } + s = strings.TrimSpace(s) + if !strings.Contains(s, "!") { + return nil, common.FlagErrorf("--ranges[%d] (%q) must include a sheet prefix", i, s) + } + out = append(out, s) + } + return out, nil +} + +// validateDropdownOptionsColors validates --options is a JSON array and that +// --colors (when set) has matching length. Used by +dropdown-set Validate. +func validateDropdownOptionsColors(runtime *common.RuntimeContext) (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 +} + +// ─── range parsing helpers ──────────────────────────────────────────── + +// rangeDimensions parses an A1 range like "A1:C5" / "A1" / "sheet1!B2:D10" +// and returns its row / column counts. Errors on non-rectangular forms like +// "A:C" (whole-column) or "3:6" (whole-row) — those need a row/col total +// from get_sheet_structure, outside the scope of pure local parsing. +func rangeDimensions(rangeStr string) (rows, cols int, err error) { + if idx := strings.Index(rangeStr, "!"); idx >= 0 { + rangeStr = rangeStr[idx+1:] + } + rangeStr = strings.TrimSpace(rangeStr) + if rangeStr == "" { + return 0, 0, fmt.Errorf("empty range") + } + parts := strings.SplitN(rangeStr, ":", 2) + if len(parts) == 1 { + // single cell, e.g. "A1" + if _, _, ok := splitCellRef(parts[0]); !ok { + return 0, 0, fmt.Errorf("invalid cell ref %q", parts[0]) + } + return 1, 1, nil + } + startCol, startRow, ok1 := splitCellRef(parts[0]) + endCol, endRow, ok2 := splitCellRef(parts[1]) + if !ok1 || !ok2 { + return 0, 0, fmt.Errorf("unsupported range form %q (need rectangular A1:B2)", rangeStr) + } + if endRow < startRow || endCol < startCol { + return 0, 0, fmt.Errorf("end %q must be at or after start %q", parts[1], parts[0]) + } + return endRow - startRow + 1, endCol - startCol + 1, nil +} + +// splitCellRef parses "A1" → (col=0, row=0, true). Returns false for any +// non-rectangular form (pure column "A", pure row "1", invalid chars). +func splitCellRef(s string) (col, row int, ok bool) { + s = strings.TrimSpace(s) + if s == "" { + return 0, 0, false + } + var colEnd int + for i, r := range s { + if r >= '0' && r <= '9' { + colEnd = i + break + } + colEnd = i + 1 + } + if colEnd == 0 || colEnd == len(s) { + return 0, 0, false + } + col = letterToColumnIndex(s[:colEnd]) + if col < 0 { + return 0, 0, false + } + n, err := strconv.Atoi(s[colEnd:]) + if err != nil || n < 1 { + return 0, 0, false + } + return col, n - 1, true +} + +// letterToColumnIndex converts spreadsheet letter notation to a 0-based +// column index ("A" → 0, "Z" → 25, "AA" → 26). Returns -1 on bad input. +func letterToColumnIndex(letters string) int { + letters = strings.ToUpper(strings.TrimSpace(letters)) + if letters == "" { + return -1 + } + n := 0 + for _, c := range letters { + if c < 'A' || c > 'Z' { + return -1 + } + n = n*26 + int(c-'A'+1) + } + return n - 1 +} + +// splitSheetPrefixedRange splits "sheet1!A2:A100" into ("sheet1", "A2:A100"). +func splitSheetPrefixedRange(rng string) (sheet, sub string, err error) { + idx := strings.Index(rng, "!") + if idx <= 0 || idx == len(rng)-1 { + return "", "", common.FlagErrorf("range %q must use sheet!range form", rng) + } + return strings.TrimSpace(rng[:idx]), strings.TrimSpace(rng[idx+1:]), nil +} + +// fillCellsMatrix returns a rows×cols matrix where every cell is the same +// (shallow-copied) prototype map. Use for fan-out shortcuts that stamp a +// single attribute (style / data_validation) across an entire range. +func fillCellsMatrix(rows, cols int, prototype map[string]interface{}) [][]interface{} { + cells := make([][]interface{}, rows) + for r := range cells { + row := make([]interface{}, cols) + for c := range row { + cell := make(map[string]interface{}, len(prototype)) + for k, v := range prototype { + cell[k] = v + } + row[c] = cell + } + cells[r] = row + } + return cells +} diff --git a/shortcuts/sheets/shortcuts.go b/shortcuts/sheets/shortcuts.go index 2ad6a4b5f..816f16caf 100644 --- a/shortcuts/sheets/shortcuts.go +++ b/shortcuts/sheets/shortcuts.go @@ -30,5 +30,22 @@ func Shortcuts() []common.Shortcut { DimFreeze, DimGroup, DimUngroup, + + // lark_sheet_read_data + CellsGet, + CsvGet, + DropdownGet, + + // lark_sheet_search_replace + CellsSearch, + CellsReplace, + + // lark_sheet_write_cells + CellsSet, + CellsSetStyle, + CsvPut, + DropdownSet, + DropdownUpdate, + DropdownDelete, } } diff --git a/skills/lark-sheets/references/lark-sheets-batch-update.md b/skills/lark-sheets/references/lark-sheets-batch-update.md index 853c8b6d5..71c2fd2ce 100644 --- a/skills/lark-sheets/references/lark-sheets-batch-update.md +++ b/skills/lark-sheets/references/lark-sheets-batch-update.md @@ -27,6 +27,7 @@ | MCP tool | CLI shortcut | Risk | 分组 | | --- | --- | --- | --- | | `batch_update` | `+batch-update` | high-risk-write | 批量 | +| | `+cells-batch-set-style` | write | 批量 | ## Flags @@ -42,6 +43,15 @@ | `--yes` | 系统 | bool | 是 | `high-risk-write`,必须二次确认(不带时退出码 10) | | `--dry-run` | 系统 | bool | 否 | 输出每个子操作的请求模板,零网络副作用 | +### `+cells-batch-set-style` + +| Flag | 分类 | Type | 必填 | 说明 | +| --- | --- | --- | --- | --- | +| `--url` | 公共 | string | XOR | spreadsheet URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 公共 | string | XOR | spreadsheet token(与 `--url` 二选一) | +| `--data` | 专有 | string + File + Stdin | 是 | JSON 数组 `[{"ranges":["sheet1!A1:B2"],"style":{...}}]`;每个 ranges 元素必须带 sheet 前缀 | +| `--dry-run` | 系统 | bool | 否 | | + ## Schemas > 复合 JSON flag(`--data` / `--style` / `--options` / `--sort-keys`)的字段速查:只列顶层字段 + 一层嵌套结构。深层结构看 `## Examples` 段的真实示例;要拿完整 JSON Schema 跑 `lark-cli sheets --print-schema --flag `(runtime introspection,待落地)。 @@ -54,6 +64,22 @@ _要批量执行的操作列表,按顺序依次执行_ - `input` (object) — 对应工具的入参,结构与单独调用该工具时完全一致 - `tool_name` (string) — 要执行的工具名称,如 "set_cell_range"、"clear_cell_range"、"modify_sheet_structure" 等 +### `+cells-batch-set-style` `--data` + +_单元格样式属性,包括字体、颜色、对齐方式和数字格式_ + +**顶层字段**: +- `background_color` (string?) — 背景颜色(十六进制,例如 "#ffffff") +- `font_color` (string?) — 字体颜色(十六进制,例如 "#000000") +- `font_line` (enum?) — 字体线条样式 [none / underline / line-through] +- `font_size` (number?) — 字体大小(单位:px/像素,例如 10、12、14) +- `font_style` (enum?) — 字体样式 [normal / italic] +- `font_weight` (enum?) — 字重 [normal / bold] +- `horizontal_alignment` (enum?) — 水平对齐方式 [left / center / right] +- `number_format` (string?) — 数字格式(例如:文本用 "@"、数字用 "0.00"、货币用 "$#,##0.00"、日期用 "mm/dd/yyyy") +- `vertical_alignment` (enum?) — 垂直对齐方式 [top / middle / bottom] +- `word_wrap` (enum?) — 是否自动换行,默认溢出,可选自动换行或裁剪 [overflow / auto-wrap / word-clip] + ## Examples > shortcut 拆分 / Risk / 分组 / flag 表都由 [`tool-shortcut-map.json`](../../tool-shortcut-map.json) 自动注入到上方 `## Shortcuts` / `## Flags` 段。本节只承载手维护补充:命令示例、Validate / DryRun / Execute 约束。 diff --git a/skills/lark-sheets/references/lark-sheets-read-data.md b/skills/lark-sheets/references/lark-sheets-read-data.md index 2a0366392..0791149a7 100644 --- a/skills/lark-sheets/references/lark-sheets-read-data.md +++ b/skills/lark-sheets/references/lark-sheets-read-data.md @@ -82,6 +82,7 @@ | --- | --- | --- | --- | | `export_sheet_to_sandbox` | _Sheet Tool 独有,CLI 不实现_ | — | — | | `get_cell_ranges` | `+cells-get` | read | 单元格 | +| | `+dropdown-get` | read | 对象 | | `get_range_as_csv` | `+csv-get` | read | 单元格 | ## Flags @@ -103,6 +104,15 @@ | `--skip-hidden` | 专有 | bool | 否 | 同上 | | `--dry-run` | 系统 | bool | 否 | | +### `+dropdown-get` + +| Flag | 分类 | Type | 必填 | 说明 | +| --- | --- | --- | --- | --- | +| `--url` | 公共 | string | XOR | spreadsheet URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token` | 公共 | string | XOR | spreadsheet token(与 `--url` 二选一) | +| `--range` | 专有 | string | 是 | 目标范围 A1 格式(含 sheet 前缀,如 `sheet1!A2:A100`) | +| `--dry-run` | 系统 | bool | 否 | | + ### `+csv-get` | Flag | 分类 | Type | 必填 | 说明 | diff --git a/skills/lark-sheets/references/lark-sheets-write-cells.md b/skills/lark-sheets/references/lark-sheets-write-cells.md index 40dfa1f71..8b3f6282c 100644 --- a/skills/lark-sheets/references/lark-sheets-write-cells.md +++ b/skills/lark-sheets/references/lark-sheets-write-cells.md @@ -173,10 +173,8 @@ set_cell_range — range="A11:H11", cells=[[ | --- | --- | --- | --- | | `set_cell_range` | `+cells-set` | write | 单元格 | | | `+cells-set-style` | write | 单元格 | -| | `+cells-batch-set-style` | write | 批量 | | | `+cells-set-image` | write | 单元格 | | | `+dropdown-set` | write | 对象 | -| | `+dropdown-get` | read | 对象 | | | `+dropdown-update` | write | 对象 | | | `+dropdown-delete` | high-risk-write | 对象 | | `set_range_from_csv` | `+csv-put` | write | 单元格 | @@ -212,15 +210,6 @@ set_cell_range — range="A11:H11", cells=[[ | `--style` | 专有 | string + File + Stdin | 是 | 样式 JSON:`{"font":{"bold":true},"backColor":"#fff","border_styles":{...}}`;只改样式,不动 value/formula(底层走 set_cell_range 的 cell_styles + border_styles 字段) | | `--dry-run` | 系统 | bool | 否 | | -### `+cells-batch-set-style` - -| Flag | 分类 | Type | 必填 | 说明 | -| --- | --- | --- | --- | --- | -| `--url` | 公共 | string | XOR | spreadsheet URL(与 `--spreadsheet-token` 二选一) | -| `--spreadsheet-token` | 公共 | string | XOR | spreadsheet token(与 `--url` 二选一) | -| `--data` | 专有 | string + File + Stdin | 是 | JSON 数组 `[{"ranges":["sheet1!A1:B2"],"style":{...}}]`;每个 ranges 元素必须带 sheet 前缀 | -| `--dry-run` | 系统 | bool | 否 | | - ### `+cells-set-image` | Flag | 分类 | Type | 必填 | 说明 | @@ -249,15 +238,6 @@ set_cell_range — range="A11:H11", cells=[[ | `--highlight` | 专有 | bool | 否 | 选项配色显示;默认 `false` | | `--dry-run` | 系统 | bool | 否 | | -### `+dropdown-get` - -| Flag | 分类 | Type | 必填 | 说明 | -| --- | --- | --- | --- | --- | -| `--url` | 公共 | string | XOR | spreadsheet URL(与 `--spreadsheet-token` 二选一) | -| `--spreadsheet-token` | 公共 | string | XOR | spreadsheet token(与 `--url` 二选一) | -| `--range` | 专有 | string | 是 | 目标范围 A1 格式(含 sheet 前缀,如 `sheet1!A2:A100`) | -| `--dry-run` | 系统 | bool | 否 | | - ### `+dropdown-update` | Flag | 分类 | Type | 必填 | 说明 | @@ -311,7 +291,7 @@ set_cell_range — range="A11:H11", cells=[[ - `rich_text` (array?) — 富文本内容 each: { attachment_name?: string, attachment_token?: string, attachment_uri?: string, file_size?: number, image_height?: number, …共 17 项 } - `value` (oneOf?) — 静态单元格值(文本、数字、布尔) -### `+cells-set-style` `--style` / `+cells-batch-set-style` `--data` +### `+cells-set-style` `--style` _单元格样式属性,包括字体、颜色、对齐方式和数字格式_