feat(sheets): implement read_data / search_replace / write_cells shortcuts (B3)

Land 11 shortcuts across three canonical skills:

  - lark_sheet_read_data (3): +cells-get / +csv-get / +dropdown-get
  - lark_sheet_search_replace (2): +cells-search / +cells-replace
  - lark_sheet_write_cells (6): +cells-set / +cells-set-style / +csv-put
    / +dropdown-set / +dropdown-update / +dropdown-delete

+dropdown-get reads the data_validation field via get_cell_ranges with
the range carrying its own sheet prefix (no --sheet-id needed). The
fine-grained --include vocabulary (value / formula / style / comment /
data_validation) maps to the tool's coarse include_styles bool plus
value_render_option enum. +csv-get's --include-row-prefix=false strips
the [row=N] prefix client-side because the tool only emits the
annotated form.

+cells-search / +cells-replace flatten the tool's options sub-object
into four independent flags (--match-case / --match-entire-cell /
--regex / --include-formulas) per the flat-flag rule, then repack them on the way
in.

+cells-set takes a raw --data JSON body whose `cells` array must match
the --range dimensions. +cells-set-style fans a single --style block
out to every cell in the range via a new fillCellsMatrix helper; the
range parser (rangeDimensions / splitCellRef / letterToColumnIndex)
only accepts rectangular A1:B2 forms — whole-column / whole-row need
sheet totals and are deferred.

+dropdown-set fans the validation block out to one range; +dropdown-
update / +dropdown-delete iterate sheet-prefixed --ranges and call
set_cell_range sequentially (partial failure leaves earlier ranges
already mutated; the Tip calls this out). +dropdown-delete is
high-risk-write and requires --yes.

+cells-set-image stays deferred to the cli-only batch (needs the
shared local-file upload helper alongside +workbook-create / +dim-move
/ +workbook-export).
This commit is contained in:
xiongyuanwen-byted
2026-05-16 22:26:17 +08:00
parent ae728fe7ec
commit 8494534c8f
8 changed files with 1312 additions and 21 deletions

View File

@@ -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 "<resolve:" + sheetName + ">"
}
// 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
}

View File

@@ -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",
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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 <shortcut> --print-schema --flag <name>`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 约束。

View File

@@ -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 | 必填 | 说明 |

View File

@@ -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<object>?) — 富文本内容 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`
_单元格样式属性,包括字体、颜色、对齐方式和数字格式_