Compare commits

...

26 Commits

Author SHA1 Message Date
zhengzhijie
910b0d4c40 docs(sheets): clarify chart labels field is presence-toggle, not value-toggle
Synced from sheet-skill-spec. Chart labels (plotArea.plot.labels and per-series
labels) are toggled by object existence — passing labels at all turns data
labels on, even when value/category/series/percentage are all false (server
falls back to showing value). Models repeatedly try `{ value: false, category:
false, series: false }` to disable, which silently shows the value fallback.
The reference doc now spells out both directions: pass labels to show, omit
the whole labels field to hide.

Also picks up earlier spec-side drift not yet propagated:
- pivot-table reference: +pivot-list info return + overlap validation
- flag-defs: cell-matrix fan-out cap default 200000 -> 50000 (#1578)
2026-07-02 16:13:17 +08:00
zhengzhijie
b3fdfc5538 chore(sheets): update chart flag schema 2026-07-02 14:30:28 +08:00
xiongyuanwen-byted
f41e6c4d74 docs(sheets): type by data nature, add pre-write reference column and chart/cond-format/filter rows
- SKILL.md quick-reference: add a "read before acting" column pointing each
  intent at its reference doc; add chart / cond-format / filter rows.
- Reframe number-vs-text decision to follow the data's nature (measure vs
  identifier), not whether the current task happens to sort/sum; a
  leaderboard/report "display only" use does not make a percentage text.
- write-cells reference: mirror the same rule and the +cells-set fallback
  for layouts +table-put cannot express.
2026-07-02 13:41:09 +08:00
xiongyuanwen-byted
77dda3ddaa docs(sheets): clarify number-vs-text typing and copy-to-range template guidance in references 2026-07-01 16:22:33 +08:00
xiongyuanwen-byted
4318f57c12 docs(sheets): bump lark-sheets skill version to 3.0.1 2026-07-01 13:01:14 +08:00
xiongyuanwen-byted
082625d2f1 docs(sheets): clarify workbook-import over read-then-recreate in skill 2026-07-01 10:42:32 +08:00
xiongyuanwen-byted
906826d4a1 fix(sheets): lower cells-set --max-cells default to 50000 2026-07-01 10:42:31 +08:00
wenzhuozhen
aa1a065802 Merge pull request #1653 from larksuite/feat/sheet-history-revert
feat(sheets): add +history-list / +history-revert / +history-revert-status shortcuts
2026-06-30 15:22:44 +08:00
wenzhuozhen
017d752ed9 docs(sheets): sync history skill reference required badges from spec
Companion to commit 9fa73312 (transaction-id) and 6ca35b06
(history-version-id): the two flag tables in
skills/lark-sheets/references/lark-sheets-history.md still showed
'optional' even though the canonical contract — and shortcuts/sheets/data/
flag-defs.json — already moved to 'required'. The earlier syncs only
picked up the data file from spec; the skill markdown drift slipped
through. Pull in the spec-side regenerated reference (ee/sheet-skill-spec
@9ca814d) so the human-readable doc matches the wire contract.
2026-06-30 14:42:43 +08:00
wenzhuozhen
909f78ed58 fix(sheets): make +history-revert-status --transaction-id cobra-required (match +history-revert)
Companion to commit 6ca35b06: same gating model now applies to both history
receipts.
- shortcuts/sheets/lark_sheet_history_revert.go: transactionIDFlag.Required=true.
  Validate keeps a trim/empty-string guard for '--transaction-id ""'.
- shortcuts/sheets/data/flag-defs.json: +history-revert-status --transaction-id
  required: optional -> required (synced from sheet-skill-spec @9ca814d).
- shortcuts/sheets/flag_defs_gen.go: regenerated.
- shortcuts/sheets/lark_sheet_history_test.go:
  TestHistoryRevert_MissingRequiredFlag/+history-revert-status moved to the
  cobra "required flag(s)" text contract (the test rig invokes the shortcut
  via cmd.Execute, which sees the raw cobra error directly without the
  dispatcher's typed wrap). Drop now-unused `errors` and `errs` imports.

Validation:
- go test ./shortcuts/sheets/... PASS (sheets + backward)
- TestFlagsFor_EveryRegisteredCommandHasDefs: PASS
- TestFlagDefsGen_MatchesJSON: PASS
- TestHistoryRevert_MissingRequiredFlag (both subtests): PASS
2026-06-30 14:42:43 +08:00
wenzhuozhen
047f0675ac fix(sheets): make +history-revert --history-version-id cobra-required + revert max-cells default drift
Two issues surfaced during MR !37 review:

1) +history-revert --history-version-id requiredness was set as
   "optional" in the spec table (BE-2 fix dc5fe0ea) so cobra wouldn't
   block before Validate. Per upstream review the flag should be
   required-by-cobra so the user gets the standard "required flag(s)"
   gate immediately and the runtime contract matches the JSON shape.
   - shortcuts/sheets/lark_sheet_history_revert.go: historyVersionIDFlag
     now sets Required: true. Validate keeps a trim/empty-string guard
     so '--history-version-id ""' still fails as a typed
     *errs.ValidationError (cobra accepts empty strings as "set").
   - shortcuts/sheets/data/flag-defs.json: +history-revert
     --history-version-id required: optional -> required.
   - shortcuts/sheets/flag_defs_gen.go: regenerated.
   - shortcuts/sheets/lark_sheet_history_test.go:
     TestHistoryRevert_MissingRequiredFlag split into per-shortcut
     subtests; +history-revert asserts cobra's "required flag(s)"
     contract (raw err — the test rig calls cmd.Execute directly so it
     doesn't see the cmd dispatcher's typed envelope wrap);
     +history-revert-status keeps the typed *errs.ValidationError
     contract (its --transaction-id stays cobra-optional + Validate-enforced).

2) max-cells safety cap was accidentally rewritten from 200000 to
   50000 by the last sync from sheet-skill-spec (the spec canonical
   side fell out of date — fixed separately on the spec MR follow-up).
   Restore desc: "Safety cap; default 200000" / default: "200000" so
   +cells-get / +csv-get keep the documented cap.

Validation:
- go test ./shortcuts/sheets/...                                     PASS
- TestHistoryRevert_MissingRequiredFlag (both subtests)              PASS
- TestHistoryShortcuts_DryRun (incl. +history-list pagination case)  PASS
- TestFlagsFor_EveryRegisteredCommandHasDefs                         PASS
- TestFlagDefsGen_MatchesJSON                                        PASS
2026-06-30 14:42:42 +08:00
wenzhuozhen
983c6e72ec feat(sheets): +history-list --end-version for backward pagination
Spec follow-up sheet-history-revert: thread the history_list pagination
contract through the +history-list shortcut.

- shortcuts/sheets/lark_sheet_history_list.go:
  + --end-version (int, optional). Mapped to the tool input's `end_version`
    only when explicitly set (so the server treats absence as
    "first page / latest"), via runtime.Changed / runtime.Int (matches the
    +formula-verify --max-locations precedent).
  + Tip: pass next_end_version from the response on the next call;
    capture exits the pagination loop when the server omits the field.

- shortcuts/sheets/lark_sheet_history_test.go: + dry-run case asserting
  --end-version 12345 lands as input.end_version=12345 (post-JSON
  unmarshal float64).

- skills/lark-sheets/references/lark-sheets-history.md: synced from
  ee/sheet-skill-spec (commit 39c6b61). Adds the "倒序分页" caveat row +
  --end-version flag + pagination Examples line. Drops the internal
  MajorHistory.Version implementation detail per spec follow-up.

- shortcuts/sheets/data/flag-defs.json: synced from spec (+history-list
  +--end-version int optional).

- shortcuts/sheets/flag_defs_gen.go: regenerated via
  `go generate ./shortcuts/sheets/...`.

Companion changes:
- ee/sheet-skill-spec MR !37: spec-tables + tool-schemas pagination
  contract (commits 09e8604, 39c6b61).
- ee/sheet-facade-agg MR !1028: history_list tool plumbs end_version,
  emits next_end_version + has_more (omitted at earliest page),
  defaults PageSize=20 to datarpc.

Validation:
- go build ./shortcuts/sheets/...                 PASS
- go test ./shortcuts/sheets/...                  PASS (sheets + backward)
- TestHistoryShortcuts_DryRun (5 cases incl. new --end-version case): PASS
- TestHistoryRevert_MissingRequiredFlag:           PASS
- TestFlagsFor_EveryRegisteredCommandHasDefs:      PASS
- TestFlagDefsGen_MatchesJSON:                     PASS
2026-06-30 14:42:42 +08:00
wenzhuozhen
41101e8dad chore(sheets): sync lark-sheets-history reference from spec (BE-2 transaction-id)
Mirror the upstream BE-2 fix in canonical-spec/references/lark_sheet_history/
cli-reference.md: +history-revert-status now uses --transaction-id (taken from
the async receipt returned by +history-revert), and +history-revert's
--history-version-id flips required→optional (Validate enforces requiredness
at runtime).

This file is the only history-only delta from the upstream sheet-skill-spec
sync; the rest of skills/lark-sheets/ stays on the cli's newer baseline
(/wiki/ URL support, +cells-set-image / +float-image-create, etc.) to match
commit 8ae516db's history-only mirror policy.

Spec source companion change: feat/sheet-history-revert in
ee/sheet-skill-spec, canonical-spec/{tool-shortcut-map.json,references/
lark_sheet_history/cli-reference.md}.
2026-06-30 14:42:42 +08:00
wenzhuozhen
66d4cf9b49 fix(sheets): align history flag-defs with inline shortcuts (green TestFlagsFor)
TestFlagsFor_EveryRegisteredCommandHasDefs was RED: generated flag-defs drifted
from the hand-written history shortcuts.
- +history-revert-status: flag-defs had --history-version-id; the BE-2 fix switched
  the shortcut to --transaction-id. Updated the entry to transaction-id.
- +history-revert / -status --history-version-id were marked required="required",
  but the inline flags are cobra-optional (requiredness enforced in Validate).
  Set required="optional" to match. Regenerated flag_defs_gen.go.

NOTE: canonical source is sheet-skill-spec (BE-3); apply the same change upstream
or the next sync:cli will regress this.
2026-06-30 14:42:41 +08:00
wenzhuozhen
d2c010bda6 fix(sheets): +history-revert-status keys on --transaction-id, not version id
BE-2 gap surfaced by PPE E2E: +history-revert-status sent history_version_id,
but the facade-agg history_revert_status tool keys on transaction_id (the async
receipt returned by +history-revert), so it returned "[40400] transaction_id is
required". Give the status shortcut its own --transaction-id flag + input
(excel_id + transaction_id); revert keeps --history-version-id. Tests updated.
2026-06-30 14:42:41 +08:00
wenzhuozhen
1eb300d6ab chore(sheets): sync lark_sheet_history skill + flag defs from sheet-skill-spec (BE-3)
Synced artifacts for the history shortcuts from ee/sheet-skill-spec (SSOT),
landed surgically (history-only) to avoid regressing this branch's newer
skills/lark-sheets content:
- skills/lark-sheets/references/lark-sheets-history.md (new, mirrored).
- skills/lark-sheets/SKILL.md: + Lark Sheet History references-table row only.
- shortcuts/sheets/data/flag-defs.json: + 3 history shortcuts (additive; no existing entries touched).
- shortcuts/sheets/flag_defs_gen.go: regenerated via go generate ./shortcuts/sheets/...
  (this also resolves the pre-existing flag-defs/gen drift — TestFlagDefsGen_MatchesJSON now passes).

NOT a full mirror: the rest of skills/lark-sheets/ + flag-schemas.json on this
branch (feat/lark-sheets-develop) are NEWER than the sheet-skill-spec worktree's
canonical (e.g. /wiki/ URL support, schema_version 3). A wholesale sync:cli would
have reverted them, so only the history delta is taken here. Full re-sync should
happen once sheet-skill-spec canonical is realigned with this branch.

Validation: go generate clean; go test ./shortcuts/sheets/
(TestFlagDefsGen_MatchesJSON, TestHistory*) PASS.

Spec source: active@2acd94a24ac3f835357a274a02344f78435bcc1c39ad0d695ce587f0cbddfb21
2026-06-30 14:42:41 +08:00
wenzhuozhen
69ebac97c7 feat(sheets): add +history-list / +history-revert / +history-revert-status shortcuts
BE-1 + BE-2 (larksuite/cli lark-sheets) for spec sheet-history-revert.
Three thin callTool wrappers over facade-agg history tools, following the
existing sheets Validate/DryRun/Execute + --url/--spreadsheet-token(/--token)
locator convention:
- +history-list (read, history_list): passes the tool output through verbatim;
  facade-agg already does the minor_histories/4-field/RFC3339 transform.
- +history-revert (write, history_revert): --history-version-id required,
  enforced at Validate stage with a typed *errs.ValidationError (no request on
  missing); returns the async receipt.
- +history-revert-status (read, history_revert_status): polls in-progress /
  success / failure.

Flags declared inline (not via *_gen.go) — flag_defs_gen.go / data/flag-defs.json
are synced from sheet-skill-spec (BE-3) and must not be hand-edited.

Notes:
- history_revert / history_revert_status depend on facade-agg's downstream RPC
  wiring, a DEFERRED follow-up; the tools return a "not wired yet" guard today.
  These CLI wrappers are correct and go live when the backend follow-up lands.
  +history-list is fully functional now.
- TestFlagDefsGen_MatchesJSON fails on baseline (pre-existing BE-3 gen/json
  drift); resolves once BE-3 sync:cli regenerates flag defs for these shortcuts.

Validation: go build ./shortcuts/sheets/... PASS; new tests
(TestHistoryShortcuts_DryRun, TestHistoryRevert_MissingVersionID) PASS.

Spec source: active@2acd94a24ac3f835357a274a02344f78435bcc1c39ad0d695ce587f0cbddfb21
2026-06-30 14:42:40 +08:00
anunwu-byted
5323e8e444 Merge pull request #1638 from larksuite/feat/pivot-info
feat(pivot): lark-sheets pivot reference 补 +pivot-list info 说明与落点覆盖校验
2026-06-29 17:35:13 +08:00
wenzhuozhen
4ace5ca4da Merge pull request #1626 from larksuite/feat/sheets-formula-verify
feat(sheets): add +formula-verify shortcut for verify_formula tool
2026-06-29 17:30:00 +08:00
wenzhuozhen
3e3f1bbf3b feat(sheets): add +formula-verify shortcut for verify_formula tool
Wraps the new verify_formula read tool in a CLI shortcut so AI agents
can run write-then-zero-error verification end-to-end:

  lark-cli sheets +formula-verify --url <url>

Scans formulas + cell error states across one or more sub-sheets and
returns a JSON status report (success / errors_found / partial).
Aggregates all 7 Excel error categories (#REF! / #DIV/0! / #VALUE! /
#NAME? / #NULL! / #NUM! / #N/A) plus compile failures into one
envelope; the tool always reports every error in the scan window —
callers needing a subset filter the returned error_summary
client-side. The internal scan cap is hidden from callers; when it
trips the response sets has_more=true and includes a warning_message
asking the caller to narrow --range / split --sheet-id and continue.

Flags follow the lark-sheets convention:
- --url / --spreadsheet-token (XOR public)
- --sheet-id / --sheet-name (repeat or comma-separate; mutually
  exclusive)
- --range (repeatable A1)
- --max-locations (default 20)
- --exit-on-error (CI gate: status='errors_found' → exit 2 with
  failed_precondition)

Generated artifacts (skills/lark-sheets/{SKILL.md, references/
lark-sheets-formula-verify.md}, shortcuts/sheets/data/flag-defs.json,
shortcuts/sheets/flag_defs_gen.go) are mirrored from sheet-skill-spec
generated/ via 'npm run sync:cli'. shortcuts.go registers
FormulaVerify alongside the other lark_sheet_formula_verify skill
shortcuts so +formula-verify is discoverable from
'lark-cli sheets --help'.

Tests cover the dry-run wire shape (excel_id + sheet_ids/sheet_names/
ranges/max_locations packing), the read scope (invoke_read URL), the
mutually-exclusive selector validation, the non-positive
--max-locations guard, and the --exit-on-error status matrix
(success/partial/errors_found/unknown).
2026-06-29 16:43:55 +08:00
wuyanchun.anunwu
9d15b70179 feat(pivot): lark-sheets pivot reference 补 +pivot-list info 说明与落点覆盖校验
+pivot-list 返回 info(page_range/content_range/error_state 等):
1) 判断目标单元格在透视表内(改配置 +pivot-update)还是区域外(改值 +cells-set);
2) 透视表展开后会覆盖已有数据,落点强烈优先默认自动新建子表;
3) 创建后用 info.error_state / content_range 校验有没有覆盖/冲突。
2026-06-29 14:13:21 +08:00
zhengzhijiej-tech
a179900d53 perf(sheets): cap fan-out cell-matrix materialization to prevent OOM (#1578)
* perf(sheets): cap fan-out cell-matrix materialization to prevent OOM

The +cells-set-style / +dropdown-set / +cells-batch-set-style /
+dropdown-update shortcuts expand a single A1 range into a rows×cols
matrix of per-cell maps client-side (the backing set_cell_range tool
takes an explicit cells matrix). rangeDimensions() had no upper bound,
so a tiny input like "A1:Z100000" balloons into ~2.6M heap maps (~900MB,
doubled again by json.Marshal) and can OOM the process before the
request is even sent.

Add a 50000-cell safety cap (checkStampMatrixBudget) gating every
fan-out materialization point, matching the documented but never-wired
--max-cells default. Oversized ranges now fail fast with a clear
validation error instead of allocating. Also preallocate the per-op
slices now that the range count is known up front.

Adds benchmarks + a boundary test as regression guards.

* perf(sheets): cap table-put/batch fan-out materialization (siblings of the cell-matrix cap)

The single-range fan-out cap (maxStampMatrixCells) left three sibling
ingress paths uncapped, each able to materialize an unbounded matrix or
op set in memory before the request leaves:

- +table-put / +workbook-create --sheets/--values: buildSheetMatrix
  builds the whole rows×cols matrix before slicing it into per-write
  batches; tablePutMaxCellsPerWrite only bounds the batch size, not the
  total input. Add tablePayload.checkCellBudget (1M-cell guardrail),
  enforced in validate() and in buildValuesPayload (the --values path
  bypasses validate()).

- batch fan-out (+cells-batch-set-style / +dropdown-update): per-range
  checkStampMatrixBudget can't stop many ranges from summing past the
  cap. Add an aggregate cell budget (checkBatchStampBudget) and a shared
  maxBatchRanges (100) count cap in validateDropdownRanges — covering
  all fan-out commands and replacing the now-redundant +dropdown-delete
  count check.

- +batch-update: cap --operations at maxBatchOperations (100) in
  translateBatchOperations.

Adds boundary regression tests for each cap. go vet + gofmt clean; full
shortcuts/sheets + backward suites green.

* test(sheets): measure table-put matrix materialization cost

Add BenchmarkBuildSheetMatrix_* and TestTablePutMatrixPeakMemory mirroring
the fan-out probes. Confirms the +table-put/+workbook-create ingress has the
same OOM profile as the single-range stamp: 2.6M cells → ~917 MB / 5.3M allocs
(+875 MB resident heap) materialized before the first write — now rejected up
front by checkCellBudget.
2026-06-29 14:03:12 +08:00
zhengzhijiej-tech
646304a1c7 feat(sheets): add --type bitable to +sheet-create for creating bitable sub-sheets (#1520) 2026-06-29 11:42:01 +08:00
xiongyuanwen-byted
1870348fc9 docs(sheets): inline editing rules into SKILL.md and clarify flag descriptions
- Move cross-cutting editing rules and execution notes into the root
  SKILL.md and drop the now-redundant core-operations reference
- Clarify flag descriptions: offset must be explicit inside +batch-update,
  range prefixes written bare (no quotes), chart requires a dim index,
  untyped --values lose date/number types, ungroup level semantics
- Sync the corresponding reference docs
2026-06-29 10:35:10 +08:00
xiongyuanwen-byted
d46e3ccad2 Merge remote-tracking branch 'origin/main' into feat/lark-sheets-develop 2026-06-27 22:43:31 +08:00
zhengzhijiej-tech
e3e5944c86 feat(sheets): support font_family in cell styles (#1549)
Add a font_family field to cell_styles so a cell's font name can be set
and read back through every style entry point:

- +cells-set (--cells JSON) and +cells-set-style / +cells-batch-set-style
  gain a font_family field / --font-family flat flag
- +workbook-create / +table-put --styles accept font_family in cell_styles
- +cells-get returns font_family

helpers.go buildCellStyleFromFlags reads the --font-family flag;
lark_sheet_workbook.go allows font_family in the --styles cell_styles
whitelist; data/ + skills/ are synced from sheet-skill-spec.
2026-06-25 11:33:53 +08:00
33 changed files with 3290 additions and 206 deletions

View File

@@ -332,11 +332,21 @@ func translateBatchOp(raw interface{}, token string, index int) (map[string]inte
}, nil
}
// maxBatchOperations caps how many sub-operations a single +batch-update may
// carry. Every translated op (with its own cells/properties payload) is held in
// the out slice at once before the whole batch is marshaled, so an unbounded
// operation count is the same unbounded-materialization hazard as the fan-out
// matrix, on the operations axis.
const maxBatchOperations = 100
// translateBatchOperations 翻译整个 ops 数组fail-fast遇错立即返回。
func translateBatchOperations(rawOps []interface{}, token string) ([]interface{}, error) {
if len(rawOps) == 0 {
return nil, sheetsValidationForFlag("operations", "--operations must be a non-empty JSON array")
}
if len(rawOps) > maxBatchOperations {
return nil, sheetsValidationForFlag("operations", "--operations accepts at most %d entries; got %d", maxBatchOperations, len(rawOps))
}
out := make([]interface{}, 0, len(rawOps))
for i, raw := range rawOps {
translated, err := translateBatchOp(raw, token, i)

View File

@@ -1,4 +1,59 @@
{
"+formula-verify": {
"risk": "read",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet token (XOR with `--url`)"
},
{
"name": "sheet-id",
"kind": "public",
"type": "string_slice",
"required": "optional",
"desc": "Sheet reference_id(s); repeat or comma-separate to scan multiple sheets. Omit to scan all visible sheets."
},
{
"name": "sheet-name",
"kind": "public",
"type": "string_slice",
"required": "optional",
"desc": "Sheet name(s); repeat or comma-separate to scan multiple sheets. Omit to scan all visible sheets."
},
{
"name": "range",
"kind": "own",
"type": "string_slice",
"required": "optional",
"desc": "Optional A1 ranges (e.g. `A1:Z200`); repeat or comma-separate for multiple ranges. Omit to scan each sheet's current_region."
},
{
"name": "max-locations",
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Max locations / samples per error type; default 20.",
"default": "20"
},
{
"name": "exit-on-error",
"kind": "own",
"type": "bool",
"required": "optional",
"desc": "When status=errors_found, exit non-zero. Useful for CI gate after batch formula writes."
}
]
},
"+workbook-info": {
"risk": "read",
"flags": [
@@ -73,6 +128,14 @@
"desc": "Initial column count (default 20, max 200)",
"default": "20"
},
{
"name": "type",
"kind": "own",
"type": "string",
"required": "optional",
"desc": "New sub-sheet type: sheet (spreadsheet) | bitable; default sheet. bitable creates an empty table only — edit its content via lark-base commands",
"default": "sheet"
},
{
"name": "dry-run",
"kind": "system",
@@ -219,7 +282,7 @@
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Source position (0-based); optional. If omitted, the CLI runtime derives it from the current workbook index of `--sheet-id` / `--sheet-name`",
"desc": "Source position (0-based); optional for standalone calls — if omitted, the CLI runtime derives it from the current workbook index of `--sheet-id` / `--sheet-name`. Inside `+batch-update` it must be passed explicitly, since batch cannot issue a structure query mid-run to derive it",
"default": "-1"
},
{
@@ -515,7 +578,7 @@
"kind": "own",
"type": "string",
"required": "optional",
"desc": "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected, through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes",
"desc": "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected (dates / numbers land as text — use --sheets to preserve types), through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes",
"input": [
"file",
"stdin"
@@ -1069,7 +1132,7 @@
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Group nesting level to ungroup; default 1 (outermost)",
"desc": "Group nesting level to ungroup; default 1 (1 = outermost, larger = deeper)",
"default": "1"
},
{
@@ -1711,6 +1774,13 @@
"required": "optional",
"desc": "Font color (hex, e.g. `#000000`)"
},
{
"name": "font-family",
"kind": "own",
"type": "string",
"required": "optional",
"desc": "Font family name (e.g. `Arial`, `Microsoft YaHei`)"
},
{
"name": "font-size",
"kind": "own",
@@ -2739,7 +2809,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A1:B2\",\"'Sheet2'!D1:D10\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id; ranges may target different sheets; the same style is applied to every range",
"desc": "Target ranges as a JSON array (e.g. `[\"Sheet1!A1:B2\",\"Sheet2!D1:D10\"]`, prefix written bare without quotes); each prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id; ranges may target different sheets; the same style is applied to every range",
"input": [
"file",
"stdin"
@@ -2759,6 +2829,13 @@
"required": "optional",
"desc": "Font color (hex, e.g. `#000000`)"
},
{
"name": "font-family",
"kind": "own",
"type": "string",
"required": "optional",
"desc": "Font family name (e.g. `Arial`, `Microsoft YaHei`)"
},
{
"name": "font-size",
"kind": "own",
@@ -2885,7 +2962,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A2:A100\",\"'Sheet1'!C2:C100\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id",
"desc": "Target ranges as a JSON array (e.g. `[\"Sheet1!A2:A100\",\"Sheet1!C2:C100\"]`, prefix written bare without quotes); each item must include a sheet prefix; the prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id",
"input": [
"file",
"stdin"
@@ -2965,7 +3042,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Target ranges as a JSON array (up to 100 items, e.g. `[\"'Sheet1'!E2:E6\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id",
"desc": "Target ranges as a JSON array (up to 100 items, e.g. `[\"Sheet1!E2:E6\"]`, prefix written bare without quotes); each item must include a sheet prefix; the prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id",
"input": [
"file",
"stdin"
@@ -3009,7 +3086,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A2:Z1000\",\"'Sheet2'!A2:Z1000\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id; ranges may target different sheets; the same scope is cleared from every range",
"desc": "Target ranges as a JSON array (e.g. `[\"Sheet1!A2:Z1000\",\"Sheet2!A2:Z1000\"]`, prefix written bare without quotes); each prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id; ranges may target different sheets; the same scope is cleared from every range",
"input": [
"file",
"stdin"
@@ -3127,7 +3204,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "Full chart config JSON. Top-level keys: `position` / `offset` / `size` / `snapshot` (no top-level `data`, no extra nested `properties`); chart data config lives under `snapshot.data` (`refs` / `headerMode` / `dim1` / `dim2`). Deeply nested — run `--print-schema --flag-name properties` for the full structure.",
"desc": "Full chart config JSON. Top-level keys: `position` / `offset` / `size` / `snapshot` (no top-level `data`, no extra nested `properties`); chart data config lives under `snapshot.data` (`refs` / `headerMode` / `dim1` / `dim2`); must include at least one of `snapshot.data.dim1.serie.index` or `dim2.series[].index`, otherwise the server rejects it. Deeply nested — run `--print-schema --flag-name properties` for the full structure.",
"input": [
"file",
"stdin"
@@ -4066,7 +4143,7 @@
"kind": "own",
"type": "string",
"required": "optional",
"desc": "Filter-view name; auto-assigned by the server when omitted on create, kept unchanged when omitted on update; takes precedence over the same-named field inside `--properties`"
"desc": "Filter-view name; auto-assigned by the server when omitted; takes precedence over the same-named field inside `--properties`"
},
{
"name": "dry-run",
@@ -4747,5 +4824,104 @@
"desc": ""
}
]
},
"+history-list": {
"risk": "read",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "end-version",
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Max version to query (descending pagination). Omit on the first call; pass next_end_version from the previous response."
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
},
"+history-revert": {
"risk": "write",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "history-version-id",
"kind": "own",
"type": "string",
"required": "required",
"desc": "History version to revert to (from +history-list)."
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
},
"+history-revert-status": {
"risk": "read",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet locator"
},
{
"name": "transaction-id",
"kind": "own",
"type": "string",
"required": "required",
"desc": "Async revert transaction id (from +history-revert)."
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ var flagDefs = map[string]commandDef{
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A2:Z1000\",\"'Sheet2'!A2:Z1000\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id; ranges may target different sheets; the same scope is cleared from every range", Input: []string{"file", "stdin"}},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"Sheet1!A2:Z1000\",\"Sheet2!A2:Z1000\"]`, prefix written bare without quotes); each prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id; ranges may target different sheets; the same scope is cleared from every range", Input: []string{"file", "stdin"}},
{Name: "scope", Kind: "own", Type: "string", Required: "optional", Desc: "Clear scope: `content` (default, values only) / `formats` (formats only) / `all` (values and formats)", Default: "content", Enum: []string{"content", "formats", "all"}},
{Name: "yes", Kind: "system", Type: "bool", Required: "required", Desc: "Confirm destructive write (exit code 10 without this flag); batch clear is irreversible"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
@@ -38,9 +38,10 @@ var flagDefs = map[string]commandDef{
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A1:B2\",\"'Sheet2'!D1:D10\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id; ranges may target different sheets; the same style is applied to every range", Input: []string{"file", "stdin"}},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"Sheet1!A1:B2\",\"Sheet2!D1:D10\"]`, prefix written bare without quotes); each prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id; ranges may target different sheets; the same style is applied to every range", Input: []string{"file", "stdin"}},
{Name: "background-color", Kind: "own", Type: "string", Required: "optional", Desc: "Background color (hex, e.g. `#ffffff`)"},
{Name: "font-color", Kind: "own", Type: "string", Required: "optional", Desc: "Font color (hex, e.g. `#000000`)"},
{Name: "font-family", Kind: "own", Type: "string", Required: "optional", Desc: "Font family name (e.g. `Arial`, `Microsoft YaHei`)"},
{Name: "font-size", Kind: "own", Type: "float64", Required: "optional", Desc: "Font size in px (e.g. 10, 12, 14)"},
{Name: "font-style", Kind: "own", Type: "string", Required: "optional", Desc: "Font style", Enum: []string{"normal", "italic"}},
{Name: "font-weight", Kind: "own", Type: "string", Required: "optional", Desc: "Font weight", Enum: []string{"normal", "bold"}},
@@ -165,6 +166,7 @@ var flagDefs = map[string]commandDef{
{Name: "range", Kind: "own", Type: "string", Required: "required", Desc: "Target range (A1 notation, e.g. `A1:B2`)"},
{Name: "background-color", Kind: "own", Type: "string", Required: "optional", Desc: "Background color (hex, e.g. `#ffffff`)"},
{Name: "font-color", Kind: "own", Type: "string", Required: "optional", Desc: "Font color (hex, e.g. `#000000`)"},
{Name: "font-family", Kind: "own", Type: "string", Required: "optional", Desc: "Font family name (e.g. `Arial`, `Microsoft YaHei`)"},
{Name: "font-size", Kind: "own", Type: "float64", Required: "optional", Desc: "Font size in px (e.g. 10, 12, 14)"},
{Name: "font-style", Kind: "own", Type: "string", Required: "optional", Desc: "Font style", Enum: []string{"normal", "italic"}},
{Name: "font-weight", Kind: "own", Type: "string", Required: "optional", Desc: "Font weight", Enum: []string{"normal", "bold"}},
@@ -195,7 +197,7 @@ var flagDefs = map[string]commandDef{
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
{Name: "properties", Kind: "own", Type: "string", Required: "required", Desc: "Full chart config JSON. Top-level keys: `position` / `offset` / `size` / `snapshot` (no top-level `data`, no extra nested `properties`); chart data config lives under `snapshot.data` (`refs` / `headerMode` / `dim1` / `dim2`). Deeply nested — run `--print-schema --flag-name properties` for the full structure.", Input: []string{"file", "stdin"}},
{Name: "properties", Kind: "own", Type: "string", Required: "required", Desc: "Full chart config JSON. Top-level keys: `position` / `offset` / `size` / `snapshot` (no top-level `data`, no extra nested `properties`); chart data config lives under `snapshot.data` (`refs` / `headerMode` / `dim1` / `dim2`); must include at least one of `snapshot.data.dim1.serie.index` or `dim2.series[].index`, otherwise the server rejects it. Deeply nested — run `--print-schema --flag-name properties` for the full structure.", Input: []string{"file", "stdin"}},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request template; no side effects"},
},
},
@@ -405,7 +407,7 @@ var flagDefs = map[string]commandDef{
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
{Name: "depth", Kind: "own", Type: "int", Required: "optional", Desc: "Group nesting level to ungroup; default 1 (outermost)", Default: "1"},
{Name: "depth", Kind: "own", Type: "int", Required: "optional", Desc: "Group nesting level to ungroup; default 1 (1 = outermost, larger = deeper)", Default: "1"},
{Name: "range", Kind: "own", Type: "string", Required: "required", Desc: "Row/column closed range to ungroup; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
@@ -426,7 +428,7 @@ var flagDefs = map[string]commandDef{
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (up to 100 items, e.g. `[\"'Sheet1'!E2:E6\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id", Input: []string{"file", "stdin"}},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (up to 100 items, e.g. `[\"Sheet1!E2:E6\"]`, prefix written bare without quotes); each item must include a sheet prefix; the prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id", Input: []string{"file", "stdin"}},
{Name: "yes", Kind: "system", Type: "bool", Required: "required", Desc: "Confirm high-risk write (exit code 10 without this flag)"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
@@ -463,7 +465,7 @@ var flagDefs = map[string]commandDef{
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"'Sheet1'!A2:A100\",\"'Sheet1'!C2:C100\"]`); each item must include a sheet prefix; the prefix must be the sheet display name (e.g. `Sheet1`), not the sheet reference_id", Input: []string{"file", "stdin"}},
{Name: "ranges", Kind: "own", Type: "string", Required: "required", Desc: "Target ranges as a JSON array (e.g. `[\"Sheet1!A2:A100\",\"Sheet1!C2:C100\"]`, prefix written bare without quotes); each item must include a sheet prefix; the prefix must exactly match the sheet display name (case-sensitive), not the sheet reference_id", Input: []string{"file", "stdin"}},
{Name: "options", Kind: "own", Type: "string", Required: "xor", Desc: "Options as a JSON array, e.g. `[\"opt1\",\"opt2\"]`. Server enforces no item-count cap and no per-item length cap; values containing commas are accepted (they are escape-encoded on the wire). For very large lists prefer `--source-range`.", Input: []string{"file", "stdin"}},
{Name: "colors", Kind: "own", Type: "string", Required: "optional", Desc: "Per-option pill colors, RGB hex array (e.g. `[\"#1FB6C1\",\"#F006C2\"]`). Length may be shorter than the source (`--options` items / `--source-range` cells) — extras cycle through a 10-color palette — but never longer (CLI Validate rejects: `--colors length (N) must not exceed dropdown source size (M)`). **Applies on its own**; ignored when `--highlight=false`.", Input: []string{"file", "stdin"}},
{Name: "multiple", Kind: "own", Type: "bool", Required: "optional", Desc: "Enable multi-select"},
@@ -526,7 +528,7 @@ var flagDefs = map[string]commandDef{
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
{Name: "properties", Kind: "own", Type: "string", Required: "required", Desc: "Filter-view rule JSON: `rules?` (per-column rule array), `filtered_columns?`. `range` and `view_name` are separate flags", Input: []string{"file", "stdin"}},
{Name: "range", Kind: "own", Type: "string", Required: "required", Desc: "Range the filter view applies to (A1 notation, e.g. `A1:F1000`); takes precedence over the same-named field inside `--properties`; required on create and must cover the header row"},
{Name: "view-name", Kind: "own", Type: "string", Required: "optional", Desc: "Filter-view name; auto-assigned by the server when omitted on create, kept unchanged when omitted on update; takes precedence over the same-named field inside `--properties`"},
{Name: "view-name", Kind: "own", Type: "string", Required: "optional", Desc: "Filter-view name; auto-assigned by the server when omitted; takes precedence over the same-named field inside `--properties`"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
@@ -632,6 +634,45 @@ var flagDefs = map[string]commandDef{
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+formula-verify": {
Risk: "read",
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "sheet-id", Kind: "public", Type: "string_slice", Required: "optional", Desc: "Sheet reference_id(s); repeat or comma-separate to scan multiple sheets. Omit to scan all visible sheets."},
{Name: "sheet-name", Kind: "public", Type: "string_slice", Required: "optional", Desc: "Sheet name(s); repeat or comma-separate to scan multiple sheets. Omit to scan all visible sheets."},
{Name: "range", Kind: "own", Type: "string_slice", Required: "optional", Desc: "Optional A1 ranges (e.g. `A1:Z200`); repeat or comma-separate for multiple ranges. Omit to scan each sheet's current_region."},
{Name: "max-locations", Kind: "own", Type: "int", Required: "optional", Desc: "Max locations / samples per error type; default 20.", Default: "20"},
{Name: "exit-on-error", Kind: "own", Type: "bool", Required: "optional", Desc: "When status=errors_found, exit non-zero. Useful for CI gate after batch formula writes."},
},
},
"+history-list": {
Risk: "read",
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "end-version", Kind: "own", Type: "int", Required: "optional", Desc: "Max version to query (descending pagination). Omit on the first call; pass next_end_version from the previous response."},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+history-revert": {
Risk: "write",
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "history-version-id", Kind: "own", Type: "string", Required: "required", Desc: "History version to revert to (from +history-list)."},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+history-revert-status": {
Risk: "read",
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"},
{Name: "transaction-id", Kind: "own", Type: "string", Required: "required", Desc: "Async revert transaction id (from +history-revert)."},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+pivot-create": {
Risk: "write",
Flags: []flagDef{
@@ -768,6 +809,7 @@ var flagDefs = map[string]commandDef{
{Name: "index", Kind: "own", Type: "int", Required: "optional", Desc: "Insert position (0-based); appended to the end when omitted", Default: "-1"},
{Name: "row-count", Kind: "own", Type: "int", Required: "optional", Desc: "Initial row count (default 200, max 50000)", Default: "200"},
{Name: "col-count", Kind: "own", Type: "int", Required: "optional", Desc: "Initial column count (default 20, max 200)", Default: "20"},
{Name: "type", Kind: "own", Type: "string", Required: "optional", Desc: "New sub-sheet type: sheet (spreadsheet) | bitable; default sheet. bitable creates an empty table only — edit its content via lark-base commands", Default: "sheet"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
@@ -822,7 +864,7 @@ var flagDefs = map[string]commandDef{
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
{Name: "index", Kind: "own", Type: "int", Required: "required", Desc: "Target position (0-based)"},
{Name: "source-index", Kind: "own", Type: "int", Required: "optional", Desc: "Source position (0-based); optional. If omitted, the CLI runtime derives it from the current workbook index of `--sheet-id` / `--sheet-name`", Default: "-1"},
{Name: "source-index", Kind: "own", Type: "int", Required: "optional", Desc: "Source position (0-based); optional for standalone calls — if omitted, the CLI runtime derives it from the current workbook index of `--sheet-id` / `--sheet-name`. Inside `+batch-update` it must be passed explicitly, since batch cannot issue a structure query mid-run to derive it", Default: "-1"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
@@ -941,7 +983,7 @@ var flagDefs = map[string]commandDef{
Flags: []flagDef{
{Name: "title", Kind: "own", Type: "string", Required: "required", Desc: "Spreadsheet title"},
{Name: "folder-token", Kind: "own", Type: "string", Required: "optional", Desc: "Target folder token; placed at the drive root when omitted"},
{Name: "values", Kind: "own", Type: "string", Required: "optional", Desc: "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected, through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes", Input: []string{"file", "stdin"}},
{Name: "values", Kind: "own", Type: "string", Required: "optional", Desc: "Untyped initial data as one 2D JSON array (`[[\"alice\",95]]`); values are written as-is with their type auto-detected (dates / numbers land as text — use --sheets to preserve types), through the same batched set_cell_range path as --sheets — pair with --styles for number formats, colors, merges, and row/col sizes", Input: []string{"file", "stdin"}},
{Name: "sheets", Kind: "own", Type: "string", Required: "optional", Desc: "Typed table payload as JSON (same shape as `+table-put`): top-level `{\"sheets\":[...]}`, with each array item a sub-sheet `{name, start_cell?, mode?, header?, allow_overwrite?, columns:[\"colA\",\"colB\",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}` — `name` and the outer `sheets` envelope are both required. Agents typically use `df_to_sheet(df, name)` from `scripts/sheets_df.py` to pack each DataFrame into one item, then wrap the list in `{\"sheets\":[...]}`. Mutually exclusive with --values. Creates the workbook, then writes typed type-faithful data (dates land as real dates, numbers keep precision).", Input: []string{"file", "stdin"}},
{Name: "styles", Kind: "own", Type: "string", Required: "optional", Desc: "Initial visual operations as JSON: top-level `{styles:[...]}`. Each item corresponds to one target sheet and must include `name`, plus at least one of `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges`. `cell_styles` entries use +cells-set-style fields with a cell range; row/col sizes use dimension ranges plus type/size; merges use cell ranges plus optional merge_type. With --sheets, styles array length/order/name must match --sheets.sheets. With --values, pass exactly one styles item for the initial sheet (its name is ignored).", Input: []string{"file", "stdin"}},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},

View File

@@ -440,7 +440,7 @@ func requireJSONArray(runtime flagView, name string) ([]interface{}, error) {
// ─── style flags (shared by +cells-set-style and +cells-batch-set-style) ─
// buildCellStyleFromFlags reads the 11 flat style flags and returns the
// buildCellStyleFromFlags reads the 12 flat style flags and returns the
// cell_styles map expected by set_cell_range. Skips any flag the user
// didn't set so partial styles work.
func buildCellStyleFromFlags(runtime flagView) map[string]interface{} {
@@ -451,6 +451,9 @@ func buildCellStyleFromFlags(runtime flagView) map[string]interface{} {
if v := runtime.Str("font-color"); v != "" {
style["font_color"] = v
}
if v := runtime.Str("font-family"); v != "" {
style["font_family"] = v
}
if runtime.Changed("font-size") && runtime.Float64("font-size") > 0 {
style["font_size"] = runtime.Float64("font-size")
}

View File

@@ -215,7 +215,8 @@ func cellsBatchSetStyleInput(runtime *common.RuntimeContext, token string) (map[
if borderStyles != nil {
prototype["border_styles"] = borderStyles
}
var ops []interface{}
ops := make([]interface{}, 0, len(ranges))
var totalCells int64
for _, rng := range ranges {
sheet, sub, err := splitSheetPrefixedRange(rng)
if err != nil {
@@ -225,6 +226,13 @@ func cellsBatchSetStyleInput(runtime *common.RuntimeContext, token string) (map[
if err != nil {
return nil, sheetsValidationForFlag("range", "range %q: %v", rng, err)
}
if err := checkStampMatrixBudget("ranges", rng, rows, cols); err != nil {
return nil, err
}
totalCells += int64(rows) * int64(cols)
if err := checkBatchStampBudget(totalCells); err != nil {
return nil, err
}
cells := fillCellsMatrix(rows, cols, prototype)
ops = append(ops, map[string]interface{}{
"tool_name": "set_cell_range",
@@ -299,7 +307,7 @@ func cellsBatchClearInput(runtime *common.RuntimeContext, token string) (map[str
return nil, err
}
clearType := normalizeClearType(runtime.Str("scope"))
var ops []interface{}
ops := make([]interface{}, 0, len(ranges))
for _, rng := range ranges {
sheet, sub, err := splitSheetPrefixedRange(rng)
if err != nil {
@@ -382,13 +390,10 @@ var DropdownDelete = common.Shortcut{
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
ranges, err := validateDropdownRanges(runtime)
if err != nil {
// validateDropdownRanges enforces the shared maxBatchRanges cap.
if _, err := validateDropdownRanges(runtime); err != nil {
return err
}
if len(ranges) > 100 {
return sheetsValidationForFlag("ranges", "--ranges accepts at most 100 entries; got %d", len(ranges))
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
@@ -432,7 +437,8 @@ func dropdownBatchInput(runtime *common.RuntimeContext, token string, clear bool
}
prototype = map[string]interface{}{"data_validation": validation}
}
var ops []interface{}
ops := make([]interface{}, 0, len(ranges))
var totalCells int64
for _, rng := range ranges {
sheet, sub, err := splitSheetPrefixedRange(rng)
if err != nil {
@@ -442,6 +448,13 @@ func dropdownBatchInput(runtime *common.RuntimeContext, token string, clear bool
if err != nil {
return nil, sheetsValidationForFlag("range", "range %q: %v", rng, err)
}
if err := checkStampMatrixBudget("ranges", rng, rows, cols); err != nil {
return nil, err
}
totalCells += int64(rows) * int64(cols)
if err := checkBatchStampBudget(totalCells); err != nil {
return nil, err
}
cells := fillCellsMatrix(rows, cols, prototype)
ops = append(ops, map[string]interface{}{
"tool_name": "set_cell_range",
@@ -461,6 +474,25 @@ func dropdownBatchInput(runtime *common.RuntimeContext, token string, clear bool
// ─── helpers resurrected from B3 (used here + future skills) ──────────
// maxBatchRanges caps how many ranges a fan-out batch (+cells-batch-set-style /
// +cells-batch-clear / +dropdown-update / +dropdown-delete) may carry, bounding
// the number of ops materialized into one batch_update.
const maxBatchRanges = 100
// checkBatchStampBudget rejects a fan-out batch whose ranges materialize more
// than maxStampMatrixCells cells in aggregate. A batch builds every range's
// cells matrix up front, so the SUM across ranges is the real peak-memory bound
// — the per-range checkStampMatrixBudget alone can't stop many ranges from
// summing past it. totalCells is int64 to stay overflow-safe.
func checkBatchStampBudget(totalCells int64) error {
if totalCells > maxStampMatrixCells {
return sheetsValidationForFlag("ranges",
"ranges expand to %d cells total, over the %d-cell safety cap; reduce the number or size of ranges",
totalCells, maxStampMatrixCells)
}
return nil
}
// validateDropdownRanges parses --ranges, requires every entry to carry a
// sheet prefix, and returns the parsed list.
func validateDropdownRanges(runtime *common.RuntimeContext) ([]string, error) {
@@ -490,6 +522,9 @@ func validateDropdownRanges(runtime *common.RuntimeContext) ([]string, error) {
}
out = append(out, s)
}
if len(out) > maxBatchRanges {
return nil, sheetsValidationForFlag("ranges", "--ranges accepts at most %d entries; got %d", maxBatchRanges, len(out))
}
return out, nil
}

View File

@@ -0,0 +1,167 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/util"
"github.com/larksuite/cli/shortcuts/common"
)
// ─── lark_sheet_formula_verify ───────────────────────────────────────
//
// Wraps verify_formula (read): scan formulas + cell error states across one
// or more sub-sheets and aggregate Excel errors (#REF! / #DIV/0! / #VALUE! /
// #NAME? / #NULL! / #NUM! / #N/A) plus compile failures (formula_errors)
// into a recalc.py-shaped JSON status report. The contract is the single
// AI self-check entry point for the R10 "write → verify zero-error"
// invariant — see canonical-spec/references/lark_sheet_formula_verify/.
// FormulaVerify wraps verify_formula. Sheet selection is optional (both
// --sheet-id and --sheet-name are repeatable); when omitted, the tool scans
// every visible sub-sheet's current_region.
var FormulaVerify = common.Shortcut{
Service: "sheets",
Command: "+formula-verify",
Description: "Scan formulas / cell errors and return a recalc.py-shaped status report (success / errors_found / partial).",
Risk: "read",
Scopes: []string{"sheets:spreadsheet:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+formula-verify"),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
if err := validateFormulaVerifySheetSelector(runtime); err != nil {
return err
}
return validateFormulaVerifyLimits(runtime)
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
return invokeToolDryRun(token, ToolKindRead, "verify_formula", formulaVerifyInput(runtime, token))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "verify_formula", formulaVerifyInput(runtime, token))
if err != nil {
return err
}
runtime.Out(out, nil)
if runtime.Bool("exit-on-error") {
return formulaVerifyExitOnError(out)
}
return nil
},
}
// validateFormulaVerifySheetSelector enforces XOR-like guarantees on the
// two multi-value selectors: at most one of --sheet-id / --sheet-name may be
// non-empty (passing both is the high-frequency reflex confusion when the
// caller cargo-cults the single-sheet shortcut signature). Both empty is the
// documented "scan every visible sub-sheet" path. Control-char checks reuse
// requireSheetSelector's logic on each item.
func validateFormulaVerifySheetSelector(runtime *common.RuntimeContext) error {
ids := nonEmptySliceItems(runtime.StrSlice("sheet-id"))
names := nonEmptySliceItems(runtime.StrSlice("sheet-name"))
if len(ids) > 0 && len(names) > 0 {
return common.ValidationErrorf("--sheet-id and --sheet-name are mutually exclusive; pick one selector to identify sub-sheets").
WithParams(
sheetsInvalidParam("sheet-id", "mutually exclusive"),
sheetsInvalidParam("sheet-name", "mutually exclusive"),
)
}
for _, id := range ids {
if err := requireSheetSelector(id, ""); err != nil {
return err
}
}
for _, name := range names {
if err := requireSheetSelector("", name); err != nil {
return err
}
}
return nil
}
// validateFormulaVerifyLimits rejects non-positive caps so a misplaced 0 or
// negative flag value can't silently degrade the scan (the server-side
// default would otherwise mask the typo).
func validateFormulaVerifyLimits(runtime *common.RuntimeContext) error {
if runtime.Changed("max-locations") && runtime.Int("max-locations") <= 0 {
return sheetsValidationForFlag("max-locations", "--max-locations must be > 0")
}
return nil
}
// nonEmptySliceItems trims and drops blanks from a repeated-flag value so
// `--sheet-id ""` doesn't masquerade as a real entry.
func nonEmptySliceItems(in []string) []string {
out := make([]string, 0, len(in))
for _, v := range in {
if trimmed := strings.TrimSpace(v); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
// formulaVerifyInput builds the verify_formula tool input map from CLI flags.
// excel_id is required; everything else is optional per the schema.
func formulaVerifyInput(runtime *common.RuntimeContext, token string) map[string]interface{} {
input := map[string]interface{}{
"excel_id": token,
}
if ids := nonEmptySliceItems(runtime.StrSlice("sheet-id")); len(ids) > 0 {
input["sheet_ids"] = ids
} else if names := nonEmptySliceItems(runtime.StrSlice("sheet-name")); len(names) > 0 {
// The verify_formula schema only declares sheet_ids; the facade
// accepts sheet_names as a parallel optional field so name-based
// selection works without forcing the caller to pre-resolve. Mirrors
// how the other read shortcuts pack both fields via
// sheetSelectorForToolInput.
input["sheet_names"] = names
}
if ranges := nonEmptySliceItems(runtime.StrSlice("range")); len(ranges) > 0 {
input["ranges"] = ranges
}
if runtime.Changed("max-locations") {
input["max_locations_per_error"] = runtime.Int("max-locations")
}
return input
}
// formulaVerifyExitOnError converts a verify_formula status into a non-zero
// CLI exit when the caller passed --exit-on-error. status="errors_found"
// is the only failure mode for this flag: "partial" means truncated but the
// scanned slice is clean, and "success" is obviously clean. A missing /
// unknown status is treated as a typed internal error because the tool's
// schema guarantees the field and we don't want a silent zero-exit.
func formulaVerifyExitOnError(out interface{}) error {
m, ok := out.(map[string]interface{})
if !ok {
return errs.NewInternalError(errs.SubtypeInvalidResponse,
"verify_formula: missing status field in tool output")
}
status, _ := m["status"].(string)
switch status {
case "success", "partial":
return nil
case "errors_found":
total, _ := util.ToFloat64(m["total_errors"])
return errs.NewValidationError(errs.SubtypeFailedPrecondition,
"verify_formula: %d formula error(s) detected; resolve and re-run", int(total)).
WithHint("inspect error_summary[*] / compile_errors[*] in the JSON output, fix or wrap with IFERROR, then re-run +formula-verify until status=success")
default:
return errs.NewInternalError(errs.SubtypeInvalidResponse,
"verify_formula: unexpected status %q", status)
}
}

View File

@@ -0,0 +1,213 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
)
// TestFormulaVerify_DryRun pins the wire shape verify_formula sends for the
// common input combinations: no selector (workbook-wide scan), explicit
// sheet_ids, explicit ranges, and the optional max_locations_per_error
// field. The test exercises the One-OpenAPI body
// directly so the schema field names stay locked to the canonical
// tool-schemas.json verify_formula node.
func TestFormulaVerify_DryRun(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
wantInput map[string]interface{}
}{
{
name: "no selector — workbook-wide scan defaults",
args: []string{"--url", testURL},
wantInput: map[string]interface{}{
"excel_id": testToken,
},
},
{
name: "sheet_ids multi via repeat",
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--sheet-id", testSheetID2},
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_ids": []interface{}{testSheetID, testSheetID2},
},
},
{
name: "sheet_names multi via comma",
args: []string{"--url", testURL, "--sheet-name", "Sheet1,Sheet2"},
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_names": []interface{}{"Sheet1", "Sheet2"},
},
},
{
name: "ranges + max_locations",
args: []string{
"--url", testURL,
"--range", "A1:Z200",
"--range", "AA1:AZ100",
"--max-locations", "5",
},
wantInput: map[string]interface{}{
"excel_id": testToken,
"ranges": []interface{}{"A1:Z200", "AA1:AZ100"},
"max_locations_per_error": float64(5),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
body := parseDryRunBody(t, FormulaVerify, tt.args)
got := decodeToolInput(t, body, "verify_formula")
assertInputEquals(t, got, tt.wantInput)
})
}
}
// TestFormulaVerify_DryRunInvokeReadPath confirms the request hits
// invoke_read (read scope) and not invoke_write — a scope mismatch here would
// surface as a 403 from the gateway.
func TestFormulaVerify_DryRunInvokeReadPath(t *testing.T) {
t.Parallel()
calls := parseDryRunAPI(t, FormulaVerify, []string{"--url", testURL})
if len(calls) == 0 {
t.Fatalf("dry-run produced no api calls")
}
call, _ := calls[0].(map[string]interface{})
url, _ := call["url"].(string)
if !strings.HasSuffix(url, "/tools/invoke_read") {
t.Errorf("verify_formula must hit invoke_read; got url=%q", url)
}
if want := "/open-apis/sheet_ai/v2/spreadsheets/" + testToken + "/tools/invoke_read"; url != want {
t.Errorf("url = %q, want %q", url, want)
}
}
// TestFormulaVerify_RejectsBothSelectors locks the "at most one selector"
// rule on the two multi-value flags. Both empty is the documented
// workbook-wide scan path, so we only reject the both-supplied case.
func TestFormulaVerify_RejectsBothSelectors(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, FormulaVerify, []string{
"--url", testURL,
"--sheet-id", testSheetID,
"--sheet-name", "Sheet1",
"--dry-run",
})
ve := requireValidation(t, err, "mutually exclusive")
gotParams := map[string]bool{}
for _, p := range ve.Params {
gotParams[p.Name] = true
}
if !gotParams["--sheet-id"] || !gotParams["--sheet-name"] {
t.Errorf("params = %#v, want both --sheet-id and --sheet-name flagged", ve.Params)
}
}
// TestFormulaVerify_RejectsNonPositiveLimits guards against typos like
// `--max-locations 0`, which would otherwise be silently swallowed by the
// "explicit value but unset" comparison in the input builder.
func TestFormulaVerify_RejectsNonPositiveLimits(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
want string
}{
{
name: "max-locations=0",
args: []string{"--url", testURL, "--max-locations", "0"},
want: "--max-locations must be > 0",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, FormulaVerify, append(c.args, "--dry-run"))
requireValidation(t, err, c.want)
})
}
}
// TestFormulaVerifyExitOnError_StatusMatrix locks the --exit-on-error
// contract: success/partial → no error; errors_found → typed validation
// error with SubtypeFailedPrecondition; missing or unknown status →
// typed internal error so a silent zero-exit can never happen.
func TestFormulaVerifyExitOnError_StatusMatrix(t *testing.T) {
t.Parallel()
t.Run("success returns no error", func(t *testing.T) {
t.Parallel()
if err := formulaVerifyExitOnError(map[string]interface{}{"status": "success"}); err != nil {
t.Fatalf("success path returned err: %v", err)
}
})
t.Run("partial returns no error", func(t *testing.T) {
t.Parallel()
if err := formulaVerifyExitOnError(map[string]interface{}{"status": "partial", "has_more": true}); err != nil {
t.Fatalf("partial path returned err: %v", err)
}
})
t.Run("errors_found yields failed_precondition with count", func(t *testing.T) {
t.Parallel()
err := formulaVerifyExitOnError(map[string]interface{}{
"status": "errors_found",
"total_errors": float64(7),
})
if err == nil {
t.Fatal("expected error, got nil")
}
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("error = %T %v, want *errs.ValidationError", err, err)
}
if ve.Subtype != errs.SubtypeFailedPrecondition {
t.Errorf("subtype = %q, want %q", ve.Subtype, errs.SubtypeFailedPrecondition)
}
if !strings.Contains(ve.Message, "7 formula error") {
t.Errorf("message %q must surface the error count", ve.Message)
}
if ve.Hint == "" {
t.Errorf("hint must be set so AI agents know to re-run after fixes")
}
})
t.Run("unknown status maps to internal/invalid_response", func(t *testing.T) {
t.Parallel()
err := formulaVerifyExitOnError(map[string]interface{}{"status": "weird"})
if err == nil {
t.Fatal("expected error, got nil")
}
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed problem, got %T %v", err, err)
}
if p.Category != errs.CategoryInternal || p.Subtype != errs.SubtypeInvalidResponse {
t.Errorf("category/subtype = %q/%q, want internal/invalid_response", p.Category, p.Subtype)
}
})
t.Run("non-object output maps to internal/invalid_response", func(t *testing.T) {
t.Parallel()
err := formulaVerifyExitOnError("oops")
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed problem, got %T %v", err, err)
}
if p.Category != errs.CategoryInternal || p.Subtype != errs.SubtypeInvalidResponse {
t.Errorf("category/subtype = %q/%q, want internal/invalid_response", p.Category, p.Subtype)
}
})
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"github.com/larksuite/cli/shortcuts/common"
)
// ─── lark_sheet_history (BE-1: +history-list) ─────────────────────────
//
// Wraps the facade-agg `history_list` tool (read) behind the One-OpenAPI
// invoke_read endpoint. The tool returns a sheet's version history. The
// facade-agg tool already performs the response transform (minor_histories
// trim / id → history_version_id / 4-field projection / RFC3339 create_time),
// so the CLI passes the tool output straight through and does NOT re-implement
// the transform client-side.
//
// History is workbook-level (no sheet selector), mirroring +workbook-info:
// the only locator is --url / --spreadsheet-token (XOR), with --token accepted
// as a parse-time alias for --spreadsheet-token via the shared PostMount hook.
//
// Flags are declared inline here rather than via flagsFor(): the generated
// flag_defs_gen.go / data/flag-defs.json are synced from sheet-skill-spec
// (BE-3) and must not be hand-edited, so this hand-written shortcut owns its
// own flag set. The two locator flags match +workbook-info's shape exactly.
// historyLocatorFlags is the --url / --spreadsheet-token XOR locator pair
// shared by the three history shortcuts. Mirrors +workbook-info's flag-defs
// entry; XOR is enforced in Validate via parseSpreadsheetRef, not by Required.
func historyLocatorFlags() []common.Flag {
return []common.Flag{
{Name: "url", Type: "string", Desc: "Spreadsheet locator (a /sheets/ or /wiki/ URL)."},
{Name: "spreadsheet-token", Type: "string", Desc: "Spreadsheet locator (raw spreadsheet token)."},
}
}
// HistoryList wraps the history_list tool: list a spreadsheet's history
// versions. Each item carries history_version_id / create_time / action /
// all_block_revision (projected server-side). An empty sheet yields an empty
// list and exit 0.
//
// Backward pagination: --end-version (optional int) maps to the tool's
// `end_version` parameter. Omit on the first call to fetch the latest page.
// On subsequent pages pass the previous response's next_end_version as
// --end-version. The tool returns next_end_version + has_more only when
// more history exists; both fields are absent at the earliest page.
var HistoryList = common.Shortcut{
Service: "sheets",
Command: "+history-list",
Description: "List a spreadsheet's edit history versions (history_version_id, create_time, action, all_block_revision).",
Risk: "read",
Scopes: []string{"sheets:spreadsheet:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: append(historyLocatorFlags(),
common.Flag{Name: "end-version", Type: "int", Desc: "Max version to query (descending pagination). Omit on the first call; pass the previous response's next_end_version on subsequent pages."},
),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := resolveSpreadsheetToken(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
return invokeToolDryRun(token, ToolKindRead, "history_list", historyListInput(runtime, token))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "history_list", historyListInput(runtime, token))
if err != nil {
return err
}
// Pass the tool output through verbatim — facade-agg already shaped it.
runtime.Out(out, nil)
return nil
},
Tips: []string{
"Capture a history_version_id from the result to feed +history-revert.",
"For older history, capture next_end_version from the response and pass it as --end-version on the next call (omitted by the server when the earliest page is reached).",
},
}
// historyListInput composes the history_list tool input. --end-version is
// optional: include it only when explicitly set so the server treats absence
// as "first page (latest)".
func historyListInput(runtime *common.RuntimeContext, token string) map[string]interface{} {
in := map[string]interface{}{"excel_id": token}
if runtime.Changed("end-version") {
in["end_version"] = runtime.Int("end-version")
}
return in
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"strings"
"github.com/larksuite/cli/shortcuts/common"
)
// ─── lark_sheet_history (BE-2: +history-revert / +history-revert-status) ──
//
// Two thin callTool wrappers over the facade-agg history tools:
// - +history-revert → history_revert (write) — async revert
// - +history-revert-status → history_revert_status (read) — poll outcome
//
// Both target a single history version via --history-version-id (the id
// surfaced by +history-list). Revert is asynchronous: it returns a receipt /
// transaction id that +history-revert-status then polls, distinguishing
// in-progress / success / failure from the tool output (passed through
// verbatim — no client-side shaping).
//
// ⚠️ Backend state: the facade-agg history_revert / history_revert_status
// tools are registered but their downstream RPC wiring is a DEFERRED
// follow-up; today they return a "not wired yet" guard error from the gateway,
// which surfaces here as a normal tool error. These CLI shortcuts are correct
// thin wrappers and will work end-to-end once the backend follow-up lands —
// this is NOT a CLI blocker. See self_check.md.
//
// Flags are declared inline (historyLocatorFlags + history-version-id) rather
// than via flagsFor(), because flag_defs_gen.go / data/flag-defs.json are
// synced from sheet-skill-spec (BE-3) and must not be hand-edited.
// historyVersionIDFlag is the target-version selector shared by +history-revert.
// Required at the cli surface (cobra MarkFlagRequired): a missing value yields
// cobra's standard "required flag(s) \"history-version-id\" not set" message
// before Validate runs. We still trim + reject control-chars in Validate to
// reject empty strings ("--history-version-id "" "), which cobra accepts.
func historyVersionIDFlag() common.Flag {
return common.Flag{
Name: "history-version-id",
Type: "string",
Required: true,
Desc: "History version to act on (from +history-list).",
}
}
func historyRevertFlags() []common.Flag {
return append(historyLocatorFlags(), historyVersionIDFlag())
}
// validateHistoryVersionID enforces the required, control-char-clean
// --history-version-id. Returns the trimmed value so callers reuse it.
func validateHistoryVersionID(runtime *common.RuntimeContext) (string, error) {
id := strings.TrimSpace(runtime.Str("history-version-id"))
if id == "" {
return "", sheetsValidationForFlag("history-version-id", "--history-version-id is required")
}
return id, nil
}
func historyRevertInput(token, versionID string) map[string]interface{} {
return map[string]interface{}{
"excel_id": token,
"history_version_id": versionID,
}
}
// transactionIDFlag is the async-revert receipt selector used by
// +history-revert-status: the transaction_id returned by +history-revert (NOT a
// history version id — the facade-agg status tool keys on transaction_id).
// Required at the cli surface (cobra MarkFlagRequired) — same gating model as
// historyVersionIDFlag. Validate still trims + rejects empty/control-char
// values to catch the "--transaction-id ''" cobra-accepts-but-empty case.
func transactionIDFlag() common.Flag {
return common.Flag{
Name: "transaction-id",
Type: "string",
Required: true,
Desc: "Async revert transaction id (from +history-revert).",
}
}
func historyRevertStatusFlags() []common.Flag {
return append(historyLocatorFlags(), transactionIDFlag())
}
// validateTransactionID enforces the required, trimmed --transaction-id and
// returns it for reuse.
func validateTransactionID(runtime *common.RuntimeContext) (string, error) {
id := strings.TrimSpace(runtime.Str("transaction-id"))
if id == "" {
return "", sheetsValidationForFlag("transaction-id", "--transaction-id is required")
}
return id, nil
}
func historyRevertStatusInput(token, transactionID string) map[string]interface{} {
return map[string]interface{}{
"excel_id": token,
"transaction_id": transactionID,
}
}
// HistoryRevert wraps the history_revert tool (write): asynchronously revert a
// spreadsheet to the given history version. --history-version-id is required
// at the cli surface (cobra MarkFlagRequired); a missing flag fails before
// Validate runs with cobra's standard "required flag(s)" error (which the
// dispatcher classifies as a typed *errs.ValidationError, exit 2). We still
// trim + reject empty / control-char values in Validate to catch the
// "--history-version-id ''" cobra-accepts-but-empty case.
var HistoryRevert = common.Shortcut{
Service: "sheets",
Command: "+history-revert",
Description: "Revert a spreadsheet to a given history version (asynchronous; poll with +history-revert-status).",
Risk: "write",
Scopes: []string{"sheets:spreadsheet:write_only"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: historyRevertFlags(),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
_, err := validateHistoryVersionID(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
versionID := strings.TrimSpace(runtime.Str("history-version-id"))
return invokeToolDryRun(token, ToolKindWrite, "history_revert", historyRevertInput(token, versionID))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
versionID, err := validateHistoryVersionID(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindWrite, "history_revert", historyRevertInput(token, versionID))
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
Tips: []string{
"Revert is asynchronous — pass the returned id to +history-revert-status to track in-progress / success / failure.",
},
}
// HistoryRevertStatus wraps the history_revert_status tool (read): poll the
// outcome of a prior +history-revert. The tool output distinguishes
// in-progress / success / failure and is passed through verbatim.
var HistoryRevertStatus = common.Shortcut{
Service: "sheets",
Command: "+history-revert-status",
Description: "Poll the status of a history revert (in-progress / success / failure).",
Risk: "read",
Scopes: []string{"sheets:spreadsheet:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: historyRevertStatusFlags(),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
_, err := validateTransactionID(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
txnID := strings.TrimSpace(runtime.Str("transaction-id"))
return invokeToolDryRun(token, ToolKindRead, "history_revert_status", historyRevertStatusInput(token, txnID))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
txnID, err := validateTransactionID(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "history_revert_status", historyRevertStatusInput(token, txnID))
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
}

View File

@@ -0,0 +1,167 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"strings"
"testing"
"github.com/larksuite/cli/shortcuts/common"
)
// TestHistoryShortcuts_DryRun asserts each history shortcut targets the right
// facade-agg tool, routes through the correct read/write invoke endpoint, and
// builds the expected tool input (excel_id always; history_version_id for the
// revert pair).
func TestHistoryShortcuts_DryRun(t *testing.T) {
t.Parallel()
const versionID = "histVER123"
const txnID = "txn-abc-123"
tests := []struct {
name string
sc common.Shortcut
args []string
toolName string
wantPath string // invoke_read | invoke_write suffix
wantInput map[string]interface{}
}{
{
name: "+history-list via --url",
sc: HistoryList,
args: []string{"--url", testURL},
toolName: "history_list",
wantPath: "invoke_read",
wantInput: map[string]interface{}{
"excel_id": testToken,
},
},
{
name: "+history-list via --spreadsheet-token",
sc: HistoryList,
args: []string{"--spreadsheet-token", testToken},
toolName: "history_list",
wantPath: "invoke_read",
wantInput: map[string]interface{}{
"excel_id": testToken,
},
},
{
name: "+history-list paginates with --end-version",
sc: HistoryList,
args: []string{"--url", testURL, "--end-version", "12345"},
toolName: "history_list",
wantPath: "invoke_read",
wantInput: map[string]interface{}{
"excel_id": testToken,
"end_version": float64(12345), // post-JSON-unmarshal numeric type
},
},
{
name: "+history-revert routes to invoke_write with version id",
sc: HistoryRevert,
args: []string{"--url", testURL, "--history-version-id", versionID},
toolName: "history_revert",
wantPath: "invoke_write",
wantInput: map[string]interface{}{
"excel_id": testToken,
"history_version_id": versionID,
},
},
{
name: "+history-revert-status routes to invoke_read with transaction id",
sc: HistoryRevertStatus,
args: []string{"--url", testURL, "--transaction-id", txnID},
toolName: "history_revert_status",
wantPath: "invoke_read",
wantInput: map[string]interface{}{
"excel_id": testToken,
"transaction_id": txnID,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
callURL := dryRunFirstCallURL(t, tt.sc, tt.args)
if !containsSuffix(callURL, tt.wantPath) {
t.Errorf("invoke url = %q, want suffix %q", callURL, tt.wantPath)
}
body := parseDryRunBody(t, tt.sc, tt.args)
got := decodeToolInput(t, body, tt.toolName)
assertInputEquals(t, got, tt.wantInput)
})
}
}
// TestHistoryRevert_MissingRequiredFlag asserts each shortcut rejects a
// missing required selector before any request is sent, with two distinct
// gates by design:
//
// - +history-revert: --history-version-id is cobra-required (Required=true
// in the flag def → MarkFlagRequired). cobra refuses the call before
// Validate runs with a plain "required flag(s)" error; the cmd dispatcher
// classifies it as a typed *errs.ValidationError (invalid_argument, exit 2).
// The test rig invokes the shortcut via cmd.Execute and observes the raw
// cobra error directly (no dispatcher wrap), so we assert the cobra text
// contract instead of the typed envelope.
//
// - +history-revert-status: --transaction-id is cobra-optional;
// requiredness is enforced inside Validate so we still get a typed,
// flag-tagged *errs.ValidationError with Param="--transaction-id".
func TestHistoryRevert_MissingRequiredFlag(t *testing.T) {
t.Parallel()
t.Run(HistoryRevert.Command, func(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, HistoryRevert, []string{"--url", testURL})
if err == nil {
t.Fatalf("%s: expected error for missing --history-version-id", HistoryRevert.Command)
}
msg := err.Error()
if !strings.Contains(msg, "required flag(s)") || !strings.Contains(msg, "history-version-id") {
t.Fatalf("%s: cobra error = %q, want substrings 'required flag(s)' and 'history-version-id'", HistoryRevert.Command, msg)
}
})
t.Run(HistoryRevertStatus.Command, func(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, HistoryRevertStatus, []string{"--url", testURL})
if err == nil {
t.Fatalf("%s: expected error for missing --transaction-id", HistoryRevertStatus.Command)
}
msg := err.Error()
if !strings.Contains(msg, "required flag(s)") || !strings.Contains(msg, "transaction-id") {
t.Fatalf("%s: cobra error = %q, want substrings 'required flag(s)' and 'transaction-id'", HistoryRevertStatus.Command, msg)
}
})
}
// dryRunFirstCallURL runs the shortcut in --dry-run and returns the first
// api call's url, so tests can assert read vs. write endpoint routing.
func dryRunFirstCallURL(t *testing.T, sc common.Shortcut, args []string) string {
t.Helper()
out, err := runShortcut(t, sc, append(args, "--dry-run"))
if err != nil {
t.Fatalf("dry-run failed: %v\noutput=%s", err, out)
}
dryRun := decodeDryRunRaw(t, out)
calls, ok := dryRun["api"].([]interface{})
if !ok || len(calls) == 0 {
t.Fatalf("dry-run api array empty or wrong shape: %#v", dryRun)
}
call, _ := calls[0].(map[string]interface{})
url, _ := call["url"].(string)
return url
}
func containsSuffix(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}

View File

@@ -382,6 +382,32 @@ func (p *tablePayload) validate() error {
return common.ValidationErrorf("--sheets[%d] %q: mode %q is invalid (want \"overwrite\" or \"append\")", i, s.Name, s.Mode)
}
}
return p.checkCellBudget()
}
// maxTablePutCells bounds how many cells a single +table-put / +workbook-create
// write may materialize. Unlike the fan-out stamp cap (maxStampMatrixCells),
// these cells come from the caller's own --sheets/--values payload rather than a
// range blow-up, so this is a generous OOM guardrail, not a usability limit:
// buildSheetMatrix builds the whole rows×cols matrix of per-cell maps in memory
// before slicing it into tablePutMaxCellsPerWrite-sized writes, so an unbounded
// payload (2.6M cells ≈ 900MB heap, doubled again by json.Marshal) OOMs the
// process before the first write leaves.
const maxTablePutCells = 1_000_000
// checkCellBudget rejects a payload whose total materialized cell count across
// all sheets exceeds maxTablePutCells. Counted in int64 to stay overflow-safe on
// pathological row/column counts.
func (p *tablePayload) checkCellBudget() error {
var total int64
for i := range p.Sheets {
total += int64(len(p.Sheets[i].Rows)) * int64(len(p.Sheets[i].Columns))
}
if total > maxTablePutCells {
return common.ValidationErrorf(
"--sheets/--values cover %d cells total, over the %d-cell safety cap; split the write across smaller payloads",
total, maxTablePutCells)
}
return nil
}

View File

@@ -123,6 +123,26 @@ func sheetCreateInput(runtime flagView, token string) (map[string]interface{}, e
if strings.TrimSpace(runtime.Str("title")) == "" {
return nil, common.ValidationErrorf("--title is required")
}
// --type bitable 建一张空白多维表格子表operation=create_bitable默认 sheet 为普通
// 电子表格子表。bitable 子表内容编辑走 lark-base 命令row-count/col-count 不适用。
sheetType := strings.TrimSpace(runtime.Str("type"))
if sheetType == "" {
sheetType = "sheet"
}
if sheetType != "sheet" && sheetType != "bitable" {
return nil, common.ValidationErrorf("--type must be 'sheet' or 'bitable'")
}
if sheetType == "bitable" {
input := map[string]interface{}{
"excel_id": token,
"operation": "create_bitable",
"sheet_name": strings.TrimSpace(runtime.Str("title")),
}
if runtime.Changed("index") {
input["target_index"] = runtime.Int("index")
}
return input, nil
}
if n := runtime.Int("row-count"); n < 0 || n > 50000 {
return nil, common.ValidationErrorf("--row-count must be between 0 and 50000")
}
@@ -836,13 +856,19 @@ func buildValuesPayload(runtime flagView, sheetStyles *workbookCreateSheetStyles
cols[i] = tableColumnSpec{Name: fmt.Sprintf("col%d", i+1)} // type-less
}
noHeader := false
return &tablePayload{Sheets: []tableSheetSpec{{
payload := &tablePayload{Sheets: []tableSheetSpec{{
Name: valuesSheetName,
Mode: "overwrite",
Header: &noHeader,
Columns: cols,
Rows: rows,
}}}, nil
}}}
// --values bypasses tablePayload.validate(), so enforce the cell budget here
// too — otherwise a giant --values array materializes unbounded.
if err := payload.checkCellBudget(); err != nil {
return nil, err
}
return payload, nil
}
// parseValuesRows decodes --values (JSON 2D array, with @file/stdin already
@@ -1246,7 +1272,7 @@ func normalizeWorkbookCreateStyleObject(in map[string]interface{}, path string)
func workbookCreateCellStyleField(name string) bool {
switch name {
case "font_color", "font_size", "font_weight", "font_style", "font_line",
case "font_color", "font_family", "font_size", "font_weight", "font_style", "font_line",
"background_color", "horizontal_alignment", "vertical_alignment",
"number_format", "word_wrap":
return true

View File

@@ -111,10 +111,10 @@ func cellsSetInput(runtime flagView, token, sheetID, sheetName string) (map[stri
// CellsSetStyle stamps a single style block across every cell in --range.
// Style is composed from a dozen flat flags (background-color, font-color,
// font-size, font-style, font-weight, font-line, horizontal-alignment,
// vertical-alignment, word-wrap, number-format) plus --border-styles for
// the only field that still needs a nested object. At least one flag must
// be set.
// font-family, font-size, font-style, font-weight, font-line,
// horizontal-alignment, vertical-alignment, word-wrap, number-format) plus
// --border-styles for the only field that still needs a nested object. At
// least one flag must be set.
var CellsSetStyle = common.Shortcut{
Service: "sheets",
Command: "+cells-set-style",
@@ -165,6 +165,9 @@ func cellsSetStyleInput(runtime flagView, token, sheetID, sheetName string) (map
if err != nil {
return nil, sheetsValidationForFlag("range", "--range %q: %v", rangeStr, err)
}
if err := checkStampMatrixBudget("range", rangeStr, rows, cols); err != nil {
return nil, err
}
if err := requireAnyStyleFlag(runtime); err != nil {
return nil, err
}
@@ -450,6 +453,9 @@ func dropdownSetInput(runtime flagView, token, sheetID, sheetName string) (map[s
if err != nil {
return nil, sheetsValidationForFlag("range", "--range %q: %v", rangeStr, err)
}
if err := checkStampMatrixBudget("range", rangeStr, rows, cols); err != nil {
return nil, err
}
validation, err := buildDropdownValidation(runtime)
if err != nil {
return nil, err
@@ -692,9 +698,30 @@ func letterToColumnIndex(letters string) int {
return n - 1
}
// maxStampMatrixCells bounds how many per-cell maps a fan-out / stamp shortcut
// will materialize from a single A1 range. The backing tools take an explicit
// cells matrix, so the CLI must expand a range like "A1:Z100000" into rows×cols
// maps before sending it — an unbounded blow-up (2.6M cells ≈ 900MB heap, then
// doubled again by json.Marshal) that OOMs the process before the request even
// leaves. 200000 matches the documented --max-cells safety cap.
const maxStampMatrixCells = 200000
// checkStampMatrixBudget rejects a range whose materialized cell count would
// exceed maxStampMatrixCells, before fillCellsMatrix allocates it. rows*cols is
// computed in int64 to stay safe against overflow on pathological ranges.
func checkStampMatrixBudget(flagName, rangeStr string, rows, cols int) error {
if total := int64(rows) * int64(cols); total > maxStampMatrixCells {
return sheetsValidationForFlag(flagName,
"range %q covers %d cells, over the %d-cell safety cap; narrow the range or split it across smaller ranges",
rangeStr, total, maxStampMatrixCells)
}
return 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.
// Callers MUST gate the dimensions through checkStampMatrixBudget first.
func fillCellsMatrix(rows, cols int, prototype map[string]interface{}) [][]interface{} {
cells := make([][]interface{}, rows)
for r := range cells {

View File

@@ -0,0 +1,272 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"bytes"
"encoding/json"
"io"
"runtime"
"strings"
"testing"
)
// These benchmarks back the memory review of the sheets fan-out / download
// paths. They measure two hot spots:
//
// 1. fillCellsMatrix — fan-out shortcuts (+cells-set-style, +dropdown-set,
// +cells-batch-set-style, +dropdown-update) expand one A1 range into a
// rows×cols matrix of per-cell maps. A tiny input string ("A1:Z100000")
// explodes into millions of heap maps with no upper bound.
//
// 2. the export-download reader — strings.NewReader(string(rawBody)) copies
// the whole downloaded file once more before saving it.
//
// Run: go test ./shortcuts/sheets -run XXX -bench 'FillCellsMatrix|DownloadReader' -benchmem
var styleProto = map[string]interface{}{
"cell_styles": map[string]interface{}{"bold": true, "fg_color": "#FF0000"},
"border_styles": map[string]interface{}{"top": map[string]interface{}{"style": "solid"}},
}
func benchFillCellsMatrix(b *testing.B, rows, cols int) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := fillCellsMatrix(rows, cols, styleProto)
if len(m) != rows {
b.Fatalf("bad matrix")
}
}
}
func BenchmarkFillCellsMatrix_100(b *testing.B) { benchFillCellsMatrix(b, 10, 10) } // A1:J10
func BenchmarkFillCellsMatrix_10K(b *testing.B) { benchFillCellsMatrix(b, 1000, 10) } // A1:J1000
func BenchmarkFillCellsMatrix_100K(b *testing.B) { benchFillCellsMatrix(b, 10000, 10) } // A1:J10000
func BenchmarkFillCellsMatrix_2600K(b *testing.B) { benchFillCellsMatrix(b, 100000, 26) } // A1:Z100000
// TestFanoutMatrixPeakMemory reports the concrete resident-heap delta of
// materializing a large fan-out matrix, so the review doc can quote real MB.
// Not an assertion — it prints numbers under `go test -v -run PeakMemory`.
func TestFanoutMatrixPeakMemory(t *testing.T) {
if testing.Short() {
t.Skip("skipping memory probe in -short")
}
cases := []struct {
name string
rows, cols int
}{
{"A1:Z10000 (260K cells)", 10000, 26},
{"A1:Z100000 (2.6M cells)", 100000, 26},
}
for _, c := range cases {
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
m := fillCellsMatrix(c.rows, c.cols, styleProto)
runtime.ReadMemStats(&after)
runtime.KeepAlive(m)
t.Logf("%-26s heap +%6.1f MB (%d total allocs)",
c.name,
float64(after.HeapAlloc-before.HeapAlloc)/(1024*1024),
after.Mallocs-before.Mallocs)
}
}
// --- +table-put / +workbook-create matrix materialization (sibling #1 path) ---
//
// buildSheetMatrix turns the caller's --sheets/--values into a rows×cols matrix
// of per-cell maps, the same unbounded blow-up as fillCellsMatrix but on the
// table-put ingress (tablePutMaxCellsPerWrite only slices the *write*, not this
// in-memory build). checkCellBudget rejects oversized payloads before this runs.
func makeTypelessSpec(rows, cols int) *tableSheetSpec {
c := make([]tableColumnSpec, cols)
r := make([][]interface{}, rows)
for i := range r {
row := make([]interface{}, cols)
for j := range row {
row[j] = "x"
}
r[i] = row
}
return &tableSheetSpec{Columns: c, Rows: r}
}
func benchBuildSheetMatrix(b *testing.B, rows, cols int) {
spec := makeTypelessSpec(rows, cols)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m, err := buildSheetMatrix(spec, true)
if err != nil || len(m) != rows+1 {
b.Fatalf("bad matrix")
}
}
}
func BenchmarkBuildSheetMatrix_100K(b *testing.B) { benchBuildSheetMatrix(b, 10000, 10) } // 100K cells
func BenchmarkBuildSheetMatrix_2600K(b *testing.B) { benchBuildSheetMatrix(b, 100000, 26) } // 2.6M cells
// TestTablePutMatrixPeakMemory reports the resident-heap delta of materializing
// a large table-put matrix (the cost checkCellBudget now prevents), so the
// review doc can quote real MB. Not an assertion — prints under -v -run PeakMemory.
func TestTablePutMatrixPeakMemory(t *testing.T) {
if testing.Short() {
t.Skip("skipping memory probe in -short")
}
for _, c := range []struct {
name string
rows, cols int
}{
{"100000×26 (2.6M cells)", 100000, 26},
} {
spec := makeTypelessSpec(c.rows, c.cols)
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
m, _ := buildSheetMatrix(spec, true)
runtime.ReadMemStats(&after)
runtime.KeepAlive(m)
t.Logf("%-24s buildSheetMatrix heap +%6.1f MB (%d total allocs)",
c.name,
float64(after.HeapAlloc-before.HeapAlloc)/(1024*1024),
after.Mallocs-before.Mallocs)
}
}
// --- export-download reader copy ---
func benchDownloadReader(b *testing.B, size int, useStringCopy bool) {
raw := bytes.Repeat([]byte("x"), size)
sink := make([]byte, 32*1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var r io.Reader
if useStringCopy {
r = strings.NewReader(string(raw)) // current code: extra full-size copy
} else {
r = bytes.NewReader(raw) // fix: no copy
}
for {
if _, err := r.Read(sink); err != nil {
break
}
}
}
}
// --- fan-out cell-budget cap (fix for the unbounded matrix blow-up) ---
func TestStampMatrixBudgetCap(t *testing.T) {
// 199992 cells (7692×26) sits just under the 200000 cap → allowed.
if err := checkStampMatrixBudget("range", "A1:Z7692", 7692, 26); err != nil {
t.Fatalf("199992 cells should pass, got: %v", err)
}
// Exactly at the cap → allowed.
if err := checkStampMatrixBudget("range", "A1:A200000", 200000, 1); err != nil {
t.Fatalf("200000 cells (== cap) should pass, got: %v", err)
}
// Just over the cap → rejected.
if err := checkStampMatrixBudget("range", "A1:A200001", 200001, 1); err == nil {
t.Fatal("200001 cells should be rejected")
}
// The pathological case from the review (2.6M cells) → rejected.
if err := checkStampMatrixBudget("ranges", "Sheet1!A1:Z100000", 100000, 26); err == nil {
t.Fatal("2.6M-cell fan-out should be rejected")
}
}
// --- sibling cap gaps: +table-put/+workbook-create payload, batch aggregate,
// batch-update operation count (follow-up to the single fan-out cap) ---
// TestTablePutCellBudgetCap covers the --sheets/--values materialization cap:
// buildSheetMatrix builds the whole matrix in memory, so the total cell count is
// bounded before that allocation, summed across all sheets.
func TestTablePutCellBudgetCap(t *testing.T) {
// 1000×1000 = 1,000,000 == cap → allowed.
atCap := &tablePayload{Sheets: []tableSheetSpec{{
Columns: make([]tableColumnSpec, 1000),
Rows: make([][]interface{}, 1000),
}}}
if err := atCap.checkCellBudget(); err != nil {
t.Fatalf("1,000,000 cells (== cap) should pass, got: %v", err)
}
// 1000×1001 = 1,001,000 > cap → rejected.
over := &tablePayload{Sheets: []tableSheetSpec{{
Columns: make([]tableColumnSpec, 1000),
Rows: make([][]interface{}, 1001),
}}}
if err := over.checkCellBudget(); err == nil {
t.Fatal("1,001,000 cells should be rejected")
}
// Budget is summed across sheets, not per-sheet: 600k + 600k = 1.2M > cap.
twoSheets := &tablePayload{Sheets: []tableSheetSpec{
{Columns: make([]tableColumnSpec, 1000), Rows: make([][]interface{}, 600)},
{Columns: make([]tableColumnSpec, 1000), Rows: make([][]interface{}, 600)},
}}
if err := twoSheets.checkCellBudget(); err == nil {
t.Fatal("1.2M cells across two sheets should be rejected")
}
}
// TestBatchStampAggregateCap covers the batch fan-out aggregate budget — the
// per-range cap can't stop many ranges from summing past the matrix ceiling.
func TestBatchStampAggregateCap(t *testing.T) {
if err := checkBatchStampBudget(maxStampMatrixCells); err != nil {
t.Fatalf("aggregate == cap should pass, got: %v", err)
}
if err := checkBatchStampBudget(maxStampMatrixCells + 1); err == nil {
t.Fatal("aggregate over cap should be rejected")
}
}
// TestBatchFanoutRangeCountCap drives a fan-out shortcut with > maxBatchRanges
// ranges and expects the shared validateDropdownRanges cap to reject it.
func TestBatchFanoutRangeCountCap(t *testing.T) {
ranges := make([]string, maxBatchRanges+1)
for i := range ranges {
ranges[i] = "sheet1!A1"
}
rangesJSON, _ := json.Marshal(ranges)
_, _, err := runShortcutCapturingErr(t, CellsBatchSetStyle, []string{
"--url", testURL,
"--ranges", string(rangesJSON),
"--font-weight", "bold",
"--dry-run",
})
requireValidation(t, err, "at most")
}
// TestBatchOperationsCountCap covers the +batch-update sub-operation count cap.
func TestBatchOperationsCountCap(t *testing.T) {
ops := make([]interface{}, maxBatchOperations+1)
for i := range ops {
ops[i] = map[string]interface{}{"shortcut": "+cells-set", "input": map[string]interface{}{}}
}
_, err := translateBatchOperations(ops, testURL)
if err == nil || !strings.Contains(err.Error(), "at most") {
t.Fatalf("expected operations count cap error, got: %v", err)
}
}
// BenchmarkStampBudget_RejectsOversized is the "after" side of the fix: the same
// A1:Z100000 input that BenchmarkFillCellsMatrix_2600K shows costing ~917MB /
// 5.3M allocs is now rejected up front, allocating only the error string.
func BenchmarkStampBudget_RejectsOversized(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := checkStampMatrixBudget("range", "A1:Z100000", 100000, 26); err == nil {
b.Fatal("expected rejection")
}
}
}
func BenchmarkDownloadReader_StringCopy_1MB(b *testing.B) { benchDownloadReader(b, 1<<20, true) }
func BenchmarkDownloadReader_BytesNoCopy_1MB(b *testing.B) { benchDownloadReader(b, 1<<20, false) }
func BenchmarkDownloadReader_StringCopy_16MB(b *testing.B) { benchDownloadReader(b, 16<<20, true) }
func BenchmarkDownloadReader_BytesNoCopy_16MB(b *testing.B) {
benchDownloadReader(b, 16<<20, false)
}

View File

@@ -105,6 +105,9 @@ func shortcutList() []common.Shortcut {
CellsSearch,
CellsReplace,
// lark_sheet_formula_verify
FormulaVerify,
// lark_sheet_write_cells
CellsSet,
CellsSetStyle,
@@ -148,5 +151,10 @@ func shortcutList() []common.Shortcut {
CellsBatchClear,
DropdownUpdate,
DropdownDelete,
// lark_sheet_history
HistoryList,
HistoryRevert,
HistoryRevertStatus,
}
}

View File

@@ -1,6 +1,6 @@
---
name: lark-sheets
version: 3.0.0
version: 3.0.1
description: "飞书电子表格:创建和操作电子表格。支持创建表格、管理工作表与行列结构(增删/合并/调整尺寸/隐藏/冻结)、读写单元格(值/公式/样式/批注/单元格图片)、查找替换、多操作原子批量更新,以及图表、透视表、条件格式、筛选器、迷你图、浮动图片等对象的创建与维护。当用户需要创建电子表格、管理工作表、批量读写或编辑数据、统计汇总与可视化、表格美化、公式计算(含 Excel 公式迁移)、金融/财务建模DCF、三张表、预算、Sensitivity 等)等任务时使用。若用户是想按名称或关键词搜索云空间(云盘/云存储)里的表格文件,请改用 lark-drive 的 drive +search 先定位资源。当用户给出 doubao.com 的 /sheets/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。"
metadata:
requires:
@@ -32,52 +32,116 @@ metadata:
| 透视表 pivot | `--pivot-table-id` | 迷你图(按组) | `--group-id` |
| 浮动图片 | `--float-image-id` | | |
## 飞书表格编辑准则(动手前必守,所有编辑类任务一律生效)
下列准则横切所有飞书表格任务,**动手前先过一遍**——即使你是被索引直接路由进某个工具参考也一律生效。每条只给一句话纲要,展开与边界见括注的 reference。
1. **最小改动**:除任务要改的单元格 / 列外原表其它单元格、行列结构、Sheet 名、合并区、格式 1:1 保持;中间结果放原数据右侧或新建空白 Sheet**禁止删 / 改名 / 隐藏 / 移动已存在 Sheet**;改写类任务精确圈定行列,不该转的原值 1:1 保留。
2. **真实写回 + 回读校验**:交付必须是对在线表格的真实写入,写完用 `+csv-get` / `+cells-get` / `+<对象>-list` 回读确认实际生效——**写操作返回 `ok` 只代表请求被接受、不代表结果符合预期**;写公式后查错误码、筛选 / 排序后核对前几行、删除 / 清空后确认已空。禁止只在文本里声称"已完成"。
3. **读全再写**:批量填充 / 补齐 / 修正类任务先确认真实数据末行再写,只探前 N 行会漏写表尾(确定末行流程见 `lark-sheets-read-data`)。
4. **公式优先于硬编码**:能用公式表达的计算(总计 / 占比 / 增长率 / 提取 / 查找)一律写公式而非静态值;**凡可由表内其它单元格推导的派生值默认就用公式,即使用户没说"联动 / 自动更新"**;写任何飞书公式前先读 `lark-sheets-formula-translation`
5. **续写 / 扩展继承样式**:续写、补齐、复制区块、新增行列时禁止只读值只写值,必须连带 `cell_styles` + `border_styles` + 合并 + 行高一起继承(清单见 `lark-sheets-write-cells`,四边框最易漏)。
6. **多步写入合并 `+batch-update`**:多个连续写入、或同一工具对多区域重复调用,合并为单次原子 `+batch-update`(语义见 `lark-sheets-batch-update`)。
7. **分组汇总用透视表**"按 X 统计 Y / 分组汇总 / 各类数量金额"用 `+pivot-{create|update|delete}`,禁止用 SUMIF / 本地脚本拼一张假透视表。
8. **拆成可验证 checklist**:落地前把指令拆成所有"独立可验证子要点",逐点 `assert` 全过才交付(多维排序每维一点、多目标每目标一点、范围类核起 / 末 / 边界);只做第一个要点属违规。
9. **全量处理前置断言条数**:翻译 / 打标 / 批量公式落地等逐条任务,先把预期条数硬编码再 `assert actual == expected`,禁止输出"已完成前 N 条,剩余继续"的半成品。
> 上述准则的实操展开——读取路径、原生工具优先级、脚本配合、易漏陷阱——见下方「执行要点」节;端到端工作流为:了解结构(`+workbook-info`)→ 读数据 → 理解语义 → 原生工具优先 → 写入 → 回读验证。
## 场景 → 命令速查(拿不准命令名先查这里,别按直觉拼)
把高频意图映射到**真实存在**的 shortcut / flag。agent 常从 Excel / Google Sheets / 飞书 OpenAPI 误迁移命令名或 flag先对照本表避免一次必然失败的试错。完整 shortcut 见各工具参考。
把高频意图映射到**真实存在**的 shortcut / flag。agent 常从 Excel / Google Sheets / 飞书 OpenAPI 误迁移命令名或 flag先对照本表避免一次必然失败的试错。完整 shortcut 见各工具参考。**选定命令后别急着写——先读「动手前读」列指向的 reference 再动手**:命令名对得上不代表用法对,写入 / 清除 / 透视类尤其容易漏掉 reference 里的防错、类型与样式继承规则。
| 你要做的事 | ✅ 正确写法 | ❌ 不存在(会被 cobra 拒) |
| --- | --- | --- |
| 读数据(纯值 / CSV | `+csv-get`(范围用 `--range` | `+get-range``+range-get``+cells-read` |
| 读值 + 公式 / 样式 / 批注 | `+cells-get --include value,formula,style,comment,data_validation` | `+get-cell``+cell-get``--with-styles``--with-merges``--include-merged-cells` |
| 写纯文本值(整块 CSV 平铺,列里没有需保留的数值 / 日期语义) | `+csv-put`(定位用 `--start-cell`,单个左上角锚点格;也接受 `--range` 别名,区间自动取左上角) | — |
| 写带类型的数据到**已有**表(列里有数字 / 金额 / 百分比 / 日期 / 计数,要可排序 / 求和 / 入图表 / 透视 | `+table-put --sheets` 完整 payload `{"sheets":[{...}]}`(列名走 `columns`、二维数据走 `data`、列 pandas dtype 走 `dtypes`、列展示格式走 `formats`;来源不限 DataFrame——Counter / dict / list 同理,详见 write-cells | 在本地把数字拼成 `"$1,234"` / `"30.5%"` 字符串再 `+csv-put`(会落成文本、丢失计算能力 |
| **新建**电子表格并写带类型的数据(类型保真需求同上,但目标表还不存在) | `+workbook-create --sheets`(协议与 `+table-put` 同构、一步建表 + typed 写入,无需先建空表再 `+table-put`date / number 不丢,详见 workbook | 用 `--values` 灌日期 / 数字(会落成文本、丢类型) |
| 写值 / 公式 / 样式 | `+cells-set`(定位用 `--range` | — |
| 插图:图片**绑定到某条记录**、随行走(凭证 / 证件照 / 商品图 / 头像 / 二维码 / 每行配图) | `+cells-set-image`(单格 `--range`,嵌入单元格内) | — |
| 插图:**自由摆放、不绑数据**的装饰 / 标识logo / 水印 / 封面大图 / banner | `+float-image-create`(浮动图片,自由定位 + 尺寸 + 层级) | — |
| 查找单元格 | `+cells-search`(关键字用 `--find` | `+cells-find``+find``--query` |
| 查找并替换 | `+cells-replace` | — |
| 看子表结构(合并 / 行高列宽 / 冻结 / 隐藏) | `+sheet-info` | `+sheet-get``+structure-get``+sheet-structure-get` |
| 看工作簿 / 子表清单 | `+workbook-info` | `+sheet-list``+workbook-get``+workbook-list` |
| 导 xlsx / 单表 csv | `+workbook-export` | — |
| 导入本地 xlsx/xls/csv 文件为飞书电子表格 | `+workbook-import --file ./x.xlsx`(本地表格文件 → 飞书电子表格的正解;仅要导成多维表格 bitable 时才用 `drive +import --type bitable` | `drive +import`(导电子表格时绕了 drive 通道、还要多给 `--type`,应直接用 `+workbook-import`)、把 .xlsx 在本地读成数据再 `+workbook-create` 重灌 |
| 清除内容 / 格式 | `+cells-clear`范围维度用 `--scope`,取值 content / formats / all | `--type` |
| 批量清除多区域 | `+cells-batch-clear``--scope` | `--target` |
| 调整列宽 / 行高 | `+cols-resize` / `+rows-resize`(行、列是两个独立命令) | `--dimension`(无此 flag |
| 分组汇总 / 透视 | `+pivot-create`(默认不传落点 flag → 自动新建子表,零覆盖) | 用 SUMIF / 本地脚本拼一张假透视表 |
| 你要做的事 | ✅ 正确写法 | 动手前读 | ❌ 不存在(会被 cobra 拒) |
| --- | --- | --- | --- |
| 读数据(纯值 / CSV | `+csv-get`(范围用 `--range` | `lark-sheets-read-data` | `+get-range``+range-get``+cells-read` |
| 读值 + 公式 / 样式 / 批注 | `+cells-get --include value,formula,style,comment,data_validation` | `lark-sheets-read-data` | `+get-cell``+cell-get``--with-styles``--with-merges``--include-merged-cells` |
| 写纯文本值(整块 CSV 平铺,列里没有需保留的数值 / 日期语义) | `+csv-put`(定位用 `--start-cell`,单个左上角锚点格;也接受 `--range` 别名,区间自动取左上角) | `lark-sheets-write-cells` | — |
| 写带类型的数据到**已有**表(列里有数字 / 金额 / 百分比 / 日期 / 计数等**本质是量值**的数据——不看当下要不要排序 / 求和,量值一律走这里 | `+table-put --sheets` 完整 payload `{"sheets":[{...}]}`(列名走 `columns`、二维数据走 `data`、列 pandas dtype 走 `dtypes`、列展示格式走 `formats`;来源不限 DataFrame——Counter / dict / list 同理;要同时美化加 `--styles` 一步带样式(区域底色 / 边框 / 列宽 / 行高 / 合并不必事后再刷payload 里不存在的 sheet 名会自动建子表,详见 write-cells | `lark-sheets-write-cells` | 在本地把数字拼成 `"$1,234"` / `"30.5%"` 字符串再 `+csv-put`(会落成文本、丢失计算能力);或以"只是 leaderboard / 报表展示、又不用算"为由把百分比写成 `"54%"`(展示用途不改变"百分比是数值"的事实 |
| **新建**电子表格并写带类型的数据(类型保真需求同上,但目标表还不存在) | `+workbook-create --sheets`(协议与 `+table-put` 同构、一步建表 + typed 写入,无需先建空表再 `+table-put`date / number 不丢`--styles` 同样可在建表同一步带全套样式,详见 workbook | `lark-sheets-workbook` | 用 `--values` 灌日期 / 数字(会落成文本、丢类型) |
| 写公式 / 富写入(样式 · 批注 · 图片 · 富文本),或需精确矩形定位的值 | `+cells-set`(定位用 `--range`;批注 / 图片 / 富文本只能用它,公式也可) | `lark-sheets-write-cells` | — |
| 插图:图片**绑定到某条记录**、随行走(凭证 / 证件照 / 商品图 / 头像 / 二维码 / 每行配图) | `+cells-set-image`(单格 `--range`,嵌入单元格内) | `lark-sheets-write-cells` | — |
| 插图:**自由摆放、不绑数据**的装饰 / 标识logo / 水印 / 封面大图 / banner | `+float-image-create`(浮动图片,自由定位 + 尺寸 + 层级) | `lark-sheets-float-image` | — |
| 查找 / 替换文本 | `+cells-search`找,关键字用 `--find``+cells-replace`(替换) | `lark-sheets-search-replace` | `+cells-find``+find``--query` |
| 看子表结构(合并 / 行高列宽 / 冻结 / 隐藏) | `+sheet-info` | `lark-sheets-sheet-structure` | `+sheet-get``+structure-get``+sheet-structure-get` |
| 看工作簿 / 子表清单 | `+workbook-info` | `lark-sheets-workbook` | `+sheet-list``+workbook-get``+workbook-list` |
| 导出 xlsx / 单表 csv | `+workbook-export` | `lark-sheets-workbook` | — |
| 导入本地 xlsx/xls/csv 文件为飞书电子表格 | `+workbook-import --file ./x.xlsx`(本地表格文件 → 飞书电子表格的正解;仅要导成多维表格 bitable 时才用 `drive +import --type bitable` | `lark-sheets-workbook` | `drive +import`(导电子表格时绕了 drive 通道、还要多给 `--type`,应直接用 `+workbook-import`)、把 .xlsx 在本地读成数据再 `+workbook-create` 重灌(多此一举,应直接 `+workbook-import` |
| 清除内容 / 格式 | `+cells-clear`(范围维度用 `--scope`,取值 content / formats / all | `lark-sheets-range-operations` | `--type` |
| 批量清除多区域 | `+cells-batch-clear``--scope` | `lark-sheets-batch-update` | `--target` |
| 调整列宽 / 行高 | `+cols-resize` / `+rows-resize`(行、列是两个独立命令) | `lark-sheets-range-operations` | `--dimension`(无此 flag |
| 分组汇总 / 透视 | `+pivot-create`(默认不传落点 flag → 自动新建子表,零覆盖) | `lark-sheets-pivot-table` | 用 SUMIF / 本地脚本拼一张假透视表 |
| 画图表 / 可视化(柱 / 折线 / 饼 / 条 / 散点 / 组合…) | `+chart-create` | `lark-sheets-chart` | matplotlib / 本地画图再贴图(原生图表可交互、随数据更新) |
| 条件高亮 / 数据条 / 色阶 / 重复值标记 | `+cond-format-create` | `lark-sheets-conditional-format` | `+highlight``+conditional-format`、逐格 `+cells-set-style` 硬凑 |
| 筛选 / 只看符合条件的行 | `+filter-create` | `lark-sheets-filter` | pandas filter 后覆盖写回(会毁原数据;要保存多份筛选状态用 `+filter-view-create` |
> ⚠️ **动手前的触发式必读(按动作判定,不看主场景)**:本次操作只要**涉及样式 / 美化**(底色 / 边框 / 字号 / 对齐 / 数字格式 / 汇总行 / 配色 / 列宽行高),动手前先读 `lark-sheets-visual-standards`;只要**要写飞书公式**,动手前先读 `lark-sheets-formula-translation`(飞书函数与 Excel 有差异,凭直觉迁移易错)。哪怕主任务是"建表 / 展开数据 / 录入",只要动作里含美化或写公式就适用——别因"这不算专门的美化 / 公式任务"而跳过。
> ⚠️ **两种图片别选错**:图若**绑定某条记录、要随行排序 / 筛选 / 增删**(凭证 / 证件照 / 每行配图,话里带「对应 / 每行 / 这列」等绑定词)→ 单元格图片 `+cells-set-image`只是自由摆放的装饰logo / 水印 / 封面)→ 浮动图片 `+float-image-create`。别因「浮动图更好控制 / 更熟」默认选浮动图。
> ⚠️ **纯文本还是数值语义**:要写的列里有数字 / 金额 / 百分比 / 日期 / 计数 `+table-put`写入已有表;外层 `{"sheets":[...]}` 包裹、列 pandas dtype 用 `dtypes`、展示格式用 `formats`,保留排序 / 求和 / 图表 / 透视能力;**目标表还不存在就用 `+workbook-create --sheets`**,同 typed 协议、一步建表 + 写入,别先建空表再 `+table-put`只有纯文本用 `+csv-put`。两者写完显示可以完全相同,但 `+csv-put` 落的是文本、不能参与计算——别把数值在本地拼成带 `$` / `%` 字符串再走 `+csv-put`。
> ⚠️ **纯文本还是数值语义(看数据本质,不看当下用途)**:要写的列里有金额 / 百分比 / 比率 / 计数 / 日期等**本质是量值**的数据 → 一律数值写入,**与"当下要不要排序 / 求和"无关**"只是 leaderboard / 报表展示、不用算"不是写成 `"54%"` 文本的理由——展示用途不改变百分比是数值的事实):常规二维表用 `+table-put`(外层 `{"sheets":[...]}` 包裹、列 pandas dtype 用 `dtypes`、展示格式用 `formats`,保留排序 / 求和 / 图表 / 透视能力;**目标表还不存在就用 `+workbook-create --sheets`**,同 typed 协议、一步建表 + 写入,别先建空表再 `+table-put`只有编号 / 身份证 / 单据号这类**本质是标识符**、要字面保真的才算纯文本用 `+csv-put` 平铺;别把数值在本地拼成带 `$` / `%` 的字符串再走 `+csv-put`(落成文本、不能参与计算)。**当版式 `+table-put` 装不下时**(多级 / 合并表头、每方向占两列的宽表 leaderboard或同批要混写公式 / 批注)——用 `+cells-set` 而非退回 `+csv-put` 字符串`value` 直接传数字(百分比传小数 `0.4`、金额传 `1234.5`+ `cell_styles.number_format``"0%"` / `"#,##0.00"`),照样显示 `40%` 且数值无损。**版式复杂只决定用哪个命令,绝不是拼 `"40%"` 字符串的理由。**
> ⚠️ **要新建子表 / 整表美化 → 别默认「`+csv-put` 写值再事后刷样式」**`+table-put` / `+workbook-create` 的 `--styles` 能在写数据的**同一步**带全套样式(区域底色 / 边框 / 列宽 / 行高 / 合并),且 `+table-put` 的 payload 里若 sheet 名不在工作簿中会自动新建子表——**纯文本表要新建子表 + 美化时同样走这里**`--styles` 与列是否 typed 无关),比「`+csv-put` 写值 + 多次 `+cells-batch-set-style` / `+*-resize` 刷样式」少好几次调用(冻结行列等 sheet 级属性仍需 `+dim-freeze` 单独一步)。
> ⚠️ **定位 flag**`+cells-get` / `+cells-set` / `+csv-get` 用 `--range``+csv-put` 规范用 `--start-cell`(单个左上角锚点格),也接受 `--range` 别名(区间自动取左上角),二者择一即可。
> ⚠️ **读取附加信息**一律走 `+cells-get --include …`**没有** `--with-styles` 这类 flag**看合并单元格**用 `+sheet-info` 的 `merged_cells`,不要在 `+cells-get` 里找 merge flag。
## 执行要点(读取 / 原生工具 / 陷阱)
准则的实操展开。端到端工作流:了解结构 → 读数据 → 理解语义 → 原生工具优先 → 写入 → 回读验证。
### 读取:按需求选路径(细则见 `lark-sheets-read-data`
| 用户需求 | 读取路径 |
|---|---|
| "完善 / 补齐 / 填空 / 修正所有 XX"、分析 / 清洗 / 大数据 | 原生优先(公式 / `+pivot` / `+filter`);表达不了再分批 `+csv-get` 导出 + 脚本处理 + 分批回写(默认覆盖所有对应数据行,不以用户选区为准) |
| "查一下 / 看看 / 统计 / 汇总"等只读 | `+csv-get` 读到上下文 |
| 需要公式 / 样式 / 批注 | `+cells-get` |
| 续写 / 扩展已有内容 | `+csv-get` 看结构 + `+cells-get` 读源区样式 + `+sheet-info --include row_heights,merges`(见准则 5 |
> "补齐 / 填空"类用只读路径探 10 行就写会漏写表尾——写入前先按 `lark-sheets-read-data` 确认真实数据末行(准则 3
### 计算:原生工具优先,代码兜底(强化准则 7
| 用户需求 | 用原生 | 禁止的替代 |
|---|---|---|
| 按 X 统计 Y、分组汇总 | `+pivot-{create\|update\|delete}` | pandas groupby → 写值 |
| 求和 / 计数 / 平均 / 占比 | 公式 | Python 算 → 写静态值 |
| 图表 / 可视化 | `+chart-*` | matplotlib |
| 条件高亮 / 色阶 | `+cond-format-*` | 逐格设样式 |
| 筛选 | `+filter-*` | pandas filter → 覆盖写入 |
| 文本提取 / 转换 / 查找 | 公式REGEXEXTRACT / TEXT / VLOOKUP 等) | Python → 写静态值 |
只有多步清洗、统计建模、公式试错 3 次仍失败时才用代码。
### 用脚本配合 CLI 时
- **只读 stdout**CLI 数据走 stdout、诊断走 stderr解析 JSON 别 `2>&1`(警告混入会解析失败),用管道或单独重定向 stdout。
- **喂 CLI 的 CSV / JSON 用 UTF-8 无 BOM**;临时文件放系统临时目录、勿落项目目录。
- **命令失败先读 stderr 再调整**,别原样重发。
- **回写纯单元格值**:剥离 `值(V-Align: bottom)` 这类"值(样式)"串与残留引号再写;排序优先 `+range-sort` 原生工具,别"读出本地排完再整列写回"。
### 易漏陷阱
- **`+dim-insert` 不继承行高**:只继承值 / 公式 / 边框,新行回落默认高度截断长文本;插行填长文本前读相邻行 `row_height`,用 `+batch-update``+rows-resize` 补齐。
- **公式容错**:日期 / 查找 / 数值转换公式用 `IFERROR` 包裹;写完读结果列首末各 5 行查 `#VALUE!` / `#REF!` / `#DIV/0!`;同一方案试错上限 3 次。
- **循环引用**:聚合公式引用范围不能含目标 cell 自身或其传递依赖。
- **隐藏行列**`+csv-get` 默认含隐藏行列;设 `--skip-hidden=true` 只看可见,但返回行序号与实际行号不再对应。
- **跨 sheet 对象**:图表 / 条件格式 / 透视表 / 浮动图片可能分布在多个子表,操作前先 `+workbook-info` 掌握全局。
- **NLP 任务分批**:语义理解 / 翻译 / 改写 / 分类等用 NLP 处理(代码只做分批 / 行号映射 / 写回);数据量大必须分批(通常 30 行 / 批),每批处理完即时写回,单批生成通常 ≤ 300 行,多批用 `+batch-update`
## References
本 skill 的 reference 分两组:先读**通用方法与规范**(横切所有任务的工作流、铁律、样式、公式规则,不含具体 shortcut它们规定了"怎么做对";再按操作对象进入**工具参考**查具体 shortcut 与调用细节。编辑类任务务必先过一遍通用方法与规范,其中的铁律对所有工具参考一律生效。
本 skill 的 reference 分两组:先读**通用方法与规范**(横切所有任务的样式、公式规则,不含具体 shortcut它们规定了"怎么做对";再按操作对象进入**工具参考**查具体 shortcut 与调用细节。编辑类任务务必先过一遍通用方法与规范,连同上方「飞书表格编辑准则」对所有工具参考一律生效。
### 通用方法与规范(先读,横切所有任务,不含具体 shortcut
| Reference | 描述 |
| --- | --- |
| [飞书表格核心操作:分析、编辑与可视化](references/lark-sheets-core-operations.md) | 飞书表格核心操作工作流。当用户需要对已有的飞书表格进行查看、分析、编辑或可视化时使用。适用场景:数据查询与统计、公式计算、表格美化、创建图表/透视表、筛选排序、批量修改数据、调整表格结构等。即使用户没有明确说"飞书表格",只要操作对象是已有的在线表格,都应触发此工作流。 |
| [飞书表格样式与配色规范](references/lark-sheets-visual-standards.md) | 飞书表格样式与配色规范:表头/数据区/汇总行的颜色、字号、对齐、边框等取值标准,以及新增汇总行、追加行列继承原表风格、已有区域美化等典型场景的决策流程与样式要点。工具调用参数细节请参考对应的 lark-sheets-write-cells / lark-sheets-range-operations / lark-sheets-batch-update。条件格式高亮、标红、数据条、色阶请使用 lark-sheets-conditional-format。 |
| [飞书表格样式与配色规范](references/lark-sheets-visual-standards.md) | 飞书表格样式与配色规范:表头/数据区/汇总行的颜色、字号、对齐、边框、数字格式等取值标准,以及从零新建表格的版式美化、新增汇总行、追加行列继承原表风格、已有区域美化等典型场景的决策流程与样式要点。工具调用参数细节请参考对应的 lark-sheets-write-cells / lark-sheets-range-operations / lark-sheets-batch-update。条件格式高亮、标红、数据条、色阶请使用 lark-sheets-conditional-format。 |
| [飞书表格公式生成规则](references/lark-sheets-formula-translation.md) | Excel 公式到飞书表格公式的迁移与生成规则。核心目标不是保留 Excel 原语法,而是按飞书表格可执行规则重写公式,并在结果上尽量对齐 Excel。当用户要求把 Excel 公式改写成飞书表格公式,或需要生成飞书公式(尤其涉及 ARRAYFORMULA、原生数组函数、INDEX/OFFSET、MAP/LAMBDA、日期差、多层范围结果与二次展开时使用。 |
### 按对象的工具参考(含 shortcut
| Reference | 描述 |
| --- | --- |
| [飞书表格公式自检](references/lark-sheets-formula-verify.md) | 公式写入后的自检入口。对指定子表(或整本工作簿)扫描公式与单元格值,聚合所有 Excel 错误(#REF! / #DIV/0! / #VALUE! / #NAME? / #NULL! / #NUM! / #N/A同时合并最近一次写入留下的编译失败formula_errors输出统一 JSON 让 AI 一次拿到完整健康度报告。任何批量公式 / 含公式列写入完成后调用 +formula-verify 确认 zero-errorstatus='errors_found' 时禁止把链路标为完成。 |
| [Lark Sheet Workbook](references/lark-sheets-workbook.md) | 管理飞书表格的工作簿结构(子表列表及元数据)。当用户提到"看看这个表格有什么"、"表格结构"、"有哪些 sheet"、"新建一个 sheet"、"删除这个工作表"、"重命名"、"复制一份"、"移动到前面"时使用。 |
| [Lark Sheet Sheet Structure](references/lark-sheets-sheet-structure.md) | 管理飞书表格的子表结构与布局。适用场景:查看行高、列宽、隐藏行列、合并单元格等布局信息,以及"插入一行"、"删除这列"、"隐藏行"、"冻结表头"、行列分组(大纲折叠/展开)等操作。行列大纲仅在用户明确提到"行分组"、"列分组"、"大纲"、"outline"时才触发,"按XXX分组"等数据分组场景请使用 lark-sheets-pivot-table。如需在表尾追加数据应先通过此 skill 插入行,再通过 lark-sheets-write-cells 写入。 |
| [Lark Sheet Read Data](references/lark-sheets-read-data.md) | 读取飞书表格中的单元格数据。当用户需要"看看数据"、"分析数据"、"统计/汇总"时使用;也适用于需要查看公式、样式、批注等详细信息的场景。 |
@@ -92,6 +156,7 @@ metadata:
| [Lark Sheet Filter View](references/lark-sheets-filter-view.md) | 管理飞书表格中的筛选视图filter view。当用户需要"建一个 XX 视图"、"保存这个筛选状态"、"切换不同筛选"、维护一个 sheet 上多份独立筛选配置时使用。视图与筛选器filter相互独立可在同一 sheet 共存;视图的隐藏行仅在用户进入该视图时本地生效,不影响其他协作者。 |
| [Lark Sheet Sparkline](references/lark-sheets-sparkline.md) | 管理飞书表格中的迷你图(折线迷你图、柱形迷你图、胜负迷你图)。当用户需要在单元格内嵌入小型图表来展示数据趋势时使用。也适用于"趋势线"、"单元格内图表"、"迷你图"等场景。注意:不等同于被禁用的 SPARKLINE() 公式函数。 |
| [Lark Sheet Float Image](references/lark-sheets-float-image.md) | 管理飞书表格中的浮动图片。当用户需要在表格中插入浮动图片、调整图片位置和大小、查看已有浮动图片、删除图片时使用。也适用于"插入图片"、"添加 logo"、"放一张图"等场景。注意:如果用户需要将图片嵌入到某个单元格内部(单元格图片),请阅读 lark-sheets-write-cells。 |
| [Lark Sheet History](references/lark-sheets-history.md) | 查询飞书表格的历史版本并回滚到指定版本。当用户需要查看一张表的编辑历史版本列表、回滚到某个历史版本、或查询回滚的异步状态(进行中/成功/失败)时使用。回滚为异步操作,发起后通过状态查询轮询结果。仅针对飞书表格。 |
## 公共 flag 速查

View File

@@ -51,9 +51,10 @@ _公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组,每项必须带 sheet 前缀(如 `["'Sheet1'!A1:B2","'Sheet2'!D1:D10"]`);前缀必须 sheet 显示名(如 `Sheet1`),不接受 sheet reference_id支持跨 sheet所有 range 应用同一组 style |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组,每项必须带 sheet 前缀(如 `["Sheet1!A1:B2","Sheet2!D1:D10"]`,前缀裸写不加引号);前缀必须 sheet 真实显示名完全一致(含大小写),不接受 sheet reference_id支持跨 sheet所有 range 应用同一组 style |
| `--background-color` | string | optional | 背景颜色(十六进制,如 `#ffffff` |
| `--font-color` | string | optional | 字体颜色(十六进制,如 `#000000` |
| `--font-family` | string | optional | 字体名称(如 `Arial``微软雅黑` |
| `--font-size` | float64 | optional | 字体大小px10、12、14 |
| `--font-style` | string | optional | 字体样式(可选值:`normal` / `italic` |
| `--font-weight` | string | optional | 字重(可选值:`normal` / `bold` |
@@ -70,7 +71,7 @@ _公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组(如 `["'Sheet1'!A2:A100","'Sheet1'!C2:C100"]`),每项必须带 sheet 前缀;前缀必须 sheet 显示名(如 `Sheet1`),不接受 sheet reference_id |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组(如 `["Sheet1!A2:A100","Sheet1!C2:C100"]`,前缀裸写不加引号),每项必须带 sheet 前缀;前缀必须 sheet 真实显示名完全一致(含大小写),不接受 sheet reference_id |
| `--options` | string + File + Stdin复合 JSON | xor | 下拉选项 JSON 数组,例如 `["opt1","opt2"]`。服务端不限制选项数量,也不限制单个选项长度;含逗号的选项可以接受(写入时会自动转义)。大量选项建议改用 `--source-range`。 |
| `--colors` | string + File + Stdin简单 JSON | optional | 下拉胶囊背景色RGB hex 数组(如 `["#1FB6C1","#F006C2"]`)。长度可短不可长——超长 Validate 拦截(`--colors length (N) must not exceed dropdown source size (M)`),未指定项按内置 10 色色板循环补色。**单独传即生效**`--highlight=false` 时被忽略。 |
| `--multiple` | bool | optional | 启用多选 |
@@ -83,7 +84,7 @@ _公共URL/token无 sheet 定位) · 系统:`--yes`、`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组(最多 100 个,如 `["'Sheet1'!E2:E6"]`),每项必须带 sheet 前缀;前缀必须 sheet 显示名(如 `Sheet1`),不接受 sheet reference_id |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组(最多 100 个,如 `["Sheet1!E2:E6"]`,前缀裸写不加引号),每项必须带 sheet 前缀;前缀必须 sheet 真实显示名完全一致(含大小写),不接受 sheet reference_id |
### `+cells-batch-clear`
@@ -91,7 +92,7 @@ _公共URL/token无 sheet 定位) · 系统:`--yes`、`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组,每项必须带 sheet 前缀(如 `["'Sheet1'!A2:Z1000","'Sheet2'!A2:Z1000"]`);前缀必须 sheet 显示名(如 `Sheet1`),不接受 sheet reference_id支持跨 sheet对所有 range 执行同一 scope 的清除 |
| `--ranges` | string + File + Stdin简单 JSON | required | 目标范围 JSON 数组,每项必须带 sheet 前缀(如 `["Sheet1!A2:Z1000","Sheet2!A2:Z1000"]`,前缀裸写不加引号);前缀必须 sheet 真实显示名完全一致(含大小写),不接受 sheet reference_id支持跨 sheet对所有 range 执行同一 scope 的清除 |
| `--scope` | string | optional | 清除范围 enum`content`(默认,仅清内容)/ `formats`(仅清格式)/ `all`(清内容 + 格式)(可选值:`content` / `formats` / `all` |
## Schemas
@@ -137,7 +138,7 @@ lark-cli sheets +batch-update --url "https://example.feishu.cn/sheets/shtXXX" --
# ops.json array<{shortcut, input}>shortcut 用 CLI 名):
# [
# {"shortcut": "+dim-insert", "input": {"sheet_id":"...","dimension":"row","start":10,"end":12}},
# {"shortcut": "+dim-insert", "input": {"sheet_id":"...","position":10,"count":3}},
# {"shortcut": "+cells-set", "input": {"sheet_id":"...","range":"A11:B12","cells":[[{"value":"a"},{"value":"b"}],[{"value":"c"},{"value":"d"}]]}}
# ]
```
@@ -145,7 +146,7 @@ lark-cli sheets +batch-update --url "https://example.feishu.cn/sheets/shtXXX" --
> ⚠️ **子操作定位规则**
> - spreadsheet 定位(`--url` / `--spreadsheet-token`**只在顶层给一次**`+batch-update` 顶层**没有** `--sheet-id` / `--sheet-name`,在顶层传不生效。
> - **每个子操作的子表定位 `sheet_id`(或 `sheet_name`)写进它自己的 `input`**(见上方 ops.json 每个 item
> - `input` 的键是该 shortcut 的 flag **展平**成 JSON`"range":"A11:B12"`、`"dimension":"row"`),不要把整组 `--operations` 再套一层嵌套 JSON。
> - `input` 的键是该 shortcut 的 flag **展平**成 JSON`"range":"A11:B12"`、`"position":11`),不要把整组 `--operations` 再套一层嵌套 JSON。
> **常见组合:插列 + 写表头 + 整列回填**——一次原子提交,不要拆成 N 次独立调用。批量回填同一列 **只需一次** `+cells-set`range 写整列范围、cells 写 N×1 矩阵),不需要逐行循环。
>
@@ -153,7 +154,7 @@ lark-cli sheets +batch-update --url "https://example.feishu.cn/sheets/shtXXX" --
> // 在 C 列前插入新列 → 写表头 C1 → 回填 C2:C100 共 99 行
> [
> {"shortcut": "+dim-insert",
> "input": {"sheet_id": "...", "dimension": "column", "start": 3, "end": 4}},
> "input": {"sheet_id": "...", "position": "C", "count": 1}},
> {"shortcut": "+cells-set",
> "input": {"sheet_id": "...", "range": "C1:C100",
> "cells": [[{"value":"score"}], [{"value":95}], [{"value":87}], /* ... 97 more rows ... */ ]}}

View File

@@ -31,7 +31,9 @@
**常见配置错误(必须注意)**
- **图表类型选择错误**:用户说"堆积柱形图/百分比堆积"时,应在 `properties.snapshot.plotArea.plot.extra.stack` 中配置堆叠;百分比堆叠需在该 stack 下设置 `percentage: true`。用户说"占比/比例"时,优先考虑饼图或百分比堆积图。注意区分 `column`(柱形图,纵向)与 `bar`(条形图,横向)是两个不同的 type 取值,"对比/各 XX" 类纵向柱默认用 `column`
- **数据标签缺失**用户需要看到具体数值时,需配置 `properties.snapshot.plotArea.plot.labels`(数据标签)相关字段
- **数据标签开关**`plotArea.plot.labels` 对象的**存在性即开关**——
- 用户需要看到具体数值/类别时:传入 `labels` 并配置 `value` / `category` / `series` / `percentage` 等显示位。
- 用户明确说"不要数据标签 / 关掉标签"时:**整个 `labels` 字段省略**。不要用 `labels: { value: false, category: false, series: false }` 这种"全部置 false"的写法关闭——只要传了 `labels`,系统就会显示数据标签(且默认兜底显示 value
- **数据源范围与系列名来源要对齐**
- **默认情况inline 模式)**`refs` 范围**应包含表头行**(首行/首列即系列名),且范围要精确覆盖目标数据,不要多选或少选。
- **合并标题行要跳过**:如果表格在表头上方存在合并的标题行(如"员工统计表"横跨多列的大标题),`refs` 必须跳过标题行、从真正的列标题行开始。例如表头在第 3 行、数据在第 4-20 行,则 `refs` 应为 `A3:G20` 而非 `A1:G20`。包含合并标题行会导致列名识别错误、表头被当作数据参与聚合计算。
@@ -122,7 +124,7 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--properties` | string + File + Stdin复合 JSON | required | 图表完整配置 JSON。顶层字段为 `position` / `offset` / `size` / `snapshot`(无顶层 `data`,也无再嵌一层 `properties`);图表数据配置在 `snapshot.data` 下(含 `refs` / `headerMode` / `dim1` / `dim2`)。结构嵌套深,完整结构跑 `--print-schema --flag-name properties` |
| `--properties` | string + File + Stdin复合 JSON | required | 图表完整配置 JSON。顶层字段为 `position` / `offset` / `size` / `snapshot`(无顶层 `data`,也无再嵌一层 `properties`);图表数据配置在 `snapshot.data` 下(含 `refs` / `headerMode` / `dim1` / `dim2`;必须至少含 `snapshot.data.dim1.serie.index``dim2.series[].index` 之一,否则 server 拒。结构嵌套深,完整结构跑 `--print-schema --flag-name properties` |
### `+chart-update`

View File

@@ -43,6 +43,8 @@
**正确做法(两步走)**
Step 1 的 `+cells-set``--copy-to-range` 等 flag 以 `lark-sheets-write-cells` 为准。
```
Step 1: `+cells-set` 在新列写判断公式(形成"是/否"或布尔辅助列)
range="H2", cells=[[{formula: "=IF(A2>B2, \"是\", \"否\")"}]], --copy-to-range="H2:H100"

View File

@@ -1,103 +0,0 @@
# 飞书表格核心操作:分析、编辑与可视化
## 概览
面向"已有飞书表格"的核心工作流,核心原则:**先了解,再分析或写入,最后验证**。本文是方法论总纲;具体工具的参数细节、边界陷阱在对应 reference本文用指针引到那里不重复展开。
**三份「通用方法与规范」如何分工**(都不含 shortcut按主题单一归属
- **本文core-operations= 流程与铁律**:端到端工作流 + 全局铁律 + 横切陷阱,是读取入口与枢纽。
- **`lark-sheets-visual-standards` = 样式知识**:配色 / 表头 / 数值格式 / 斑马纹 / 美化决策等"正确视觉输出"的全部标准。
- **`lark-sheets-formula-translation` = 公式知识**:飞书公式书写与 Excel 迁移的全部正确性规则(绝对引用、范围语法、数组语义、不支持函数等)。
> **下面的铁律对所有任务一律生效**,即使你是被索引直接路由进 visual 或 formula 而没经过本文——编辑类任务务必先回到这里过一遍铁律。
## 铁律(所有编辑类任务必须满足,各 reference 不得放宽)
1. **最小改动**:除用户明示要改的单元格 / 列外原表其它单元格、行列结构、Sheet 名、合并区、格式必须 1:1 保持。中间结果优先放原数据**右侧**;会与原数据混淆或要承载透视表 / 图表时才**新建空白 Sheet**。**禁止**擅自删 / 改名 / 隐藏 / 移动**已存在**的 Sheet新建允许节制使用。**改写 / 转换类任务要精确圈定适用行列**:只对任务真正要求的对象做变换,**不该转的行 / 列保持原值 1:1**(典型反例:要求"统一翻译"时把本就是中文、应原样保留的评论也重新翻译;要求"改写某列格式"时连原始测量值也一并改动 → 应保留的原文被篡改)。
2. **真实写回 + 回读校验**:交付必须是对在线表格的真实写入,并 `+csv-get` / `+cells-get` / `+<对象>-list` 回读校验。**严禁**只在文本里描述"已完成"、用普通公式 / 文本假装结构化对象、或只给占位而无真实写入。**收尾前必须确认产物文件真实存在 / 可导出**——别在没真正生成产物时只凭文本"已完成"就结束(反例:文本称已完成,实际没生成产物文件,等于没交付)。
3. **读全再写,禁止只探前 N 行**:批量填充 / 补齐 / 修正类任务必须先确认**真实数据末行**再写,否则会漏写表尾。完整的"按表格形态分流读取 + `current_region` / `has_more` 兜底 + 真实末行确认"流程见 `lark-sheets-read-data` 的「确定数据范围的正确流程」。
4. **公式优先于硬编码**:能用飞书公式表达的计算(总计 / 占比 / 增长率 / 提取 / 查找等)一律写公式而非静态值,源数据变化才能自动重算。用户口头的"分列 / 排序 / 求和 / 提取"也要落地为公式或原生工具SORT / `TEXTBEFORE` / `MID` / 透视表 等。Excel 公式迁移、数组语义、不支持函数清单一律以 `lark-sheets-formula-translation` 为唯一权威。**即使用户没说"联动 / 自动更新",凡是可由表内其它单元格推导的派生值(年龄=当年-出生年、占比=本类数/总数、达标=阈值判断、排名、各类分组汇总)默认就必须用公式**——用户默认期望派生列能随源数据重算,**离线 Python / 脚本算完写静态值,即便当前数值正确,改了源数据也不会自动更新,等于没满足"派生"的本意**(反例:年龄、月度汇总、占比、分组求和等派生列写死值,源数据一改结果就过时)。
5. **续写 / 扩展必须继承样式**:续写、补齐、复制区块、新增行列时,**禁止**只读值只写值。必须连带 `cell_styles` + `border_styles` + 合并 + 行高一起继承。完整继承清单与做法见 `lark-sheets-write-cells` 的「新增列 / 新增行的样式继承」(`border_styles` 四边最易漏)。
6. **多步写入优先 `+batch-update`**:多个连续写入、或同一工具对多个区域重复调用(多次 merge / resize / cells-set必须合并为单次原子 `+batch-update`。语义与不可嵌套的限制见 `lark-sheets-batch-update`
7. **分组汇总必须用透视表**"按 X 统计 Y / 分组汇总 / 各部门数量金额"必须用 `+pivot-{create|update|delete}`(推荐省略 sheet_id 自动新建子表),**禁止**用 SUMIF / COUNTIF 或本地脚本覆盖原表替代。
8. **任务拆成可验证 checklist**:落地前把指令拆成所有"独立可验证子要点",每点一个 `assert`,全部通过才交付:多维度操作(按部门一/二/三级排序)每维一个 assert多目标删 N 行每目标一个多格式兼容多种日期格式每种至少一个样本范围类A1:H11 加边框)起 / 末行 / 末列三边界都核。只完成第一个要点(只排一级、只删 1 行)属违规。**题面 / 表头里写明的格式规范也是子要点**:表头注明"需标注某字段"就必须给对应单元格加规定前缀并逐条 assert 前缀存在(反例:漏加规定前缀,该要点即不达标);"相同编号连续行合并"必须遍历所有相同编号组全部合并(反例:只合并了其中一部分组)。
9. **全量处理要前置断言条数**:翻译 / 打标 / 批量公式落地等逐条任务,落地前把"预期处理条数"硬编码进代码,处理完 `assert actual == expected`。**严禁**输出"已完成前 N 条,剩余将继续"的半成品。
## 推荐工作流程
1. **规划 reference 清单**:开工前一次性列出本任务要读的 reference避免读一个调一个本轮已读过的不重复读。本文 + `lark-sheets-workbook` 几乎每次都要。
2. **了解结构**:先 `+workbook-info` 拿子表列表 / 行列数 / 冻结位置(不可猜测,猜错会越界覆盖);涉及合并 / 隐藏 / 分组 / 行高列宽再用 `lark-sheets-sheet-structure``+sheet-info`
3. **读取数据(按任务类型选路径,细则见 `lark-sheets-read-data`**
| 用户需求语义 | 路径 |
|---|---|
| "完善 / 补齐 / 填空 / 修正所有 XX" / 数据分析 / 清洗 / 大数据集 | **A原生优先**(公式 / `+pivot` / `+filter`,见第 5 步);原生表达不了或更复杂时**分批 `+csv-get` 导出 + 本地脚本处理 + 分批回写**(默认覆盖所有对应数据行,不以用户选区为准;脚本与 CLI 配合见下方「CLI 配合要点」) |
| "查一下 / 看看 / 统计 / 汇总" 等只读 | B`+csv-get` 读到上下文 |
| 需要公式 / 样式 / 批注 | C`+cells-get` |
| 续写 / 扩展 / 完善已有内容 | D`+csv-get` 看结构 + `+cells-get` 读源区样式 + `+sheet-info --include row_heights,merges`(见铁律 5 |
**注意**:对"完善 / 补齐 / 填空"类任务用路径 B 探 10 行就写入,实测会漏写表尾多行。写入前必须按 `lark-sheets-read-data`「确定数据范围的正确流程」确认真实数据末行。按关键字定位区域用 `lark-sheets-search-replace``+cells-search`
4. **理解数据语义(写入前必做)**:读表头 + 3-5 行样本确认各列含义与格式(文本 / 数字 / 日期 / 混合);写公式前先分析样本值格式模式再选提取策略;建透视表前先列清"行字段=分组维度、值字段=聚合指标"。需求模糊时(如"加入加减乘除"未说逻辑)基于表头与已有公式推断,不确定就问用户,禁止臆造业务逻辑。
5. **分析与计算(原生工具优先,代码兜底)**:飞书原生能力能随数据自动更新,**必须优先**
| 用户需求 | 必须用的原生工具 | 禁止用代码替代 |
|---|---|---|
| 按 X 统计 Y、分组汇总 | `+pivot-{create\|update\|delete}` | pandas groupby → `+cells-set` |
| 求和 / 计数 / 平均 / 占比 | 公式SUM/COUNT/AVERAGE | Python 算 → 写静态值 |
| 画图表 / 可视化 | `+chart-{create\|update\|delete}` | matplotlib 画图 |
| 条件高亮 / 色阶 | `+cond-format-{create\|update\|delete}` | 逐单元格设样式 |
| 数据筛选 | `+filter-{create\|update\|delete}` | pandas filter → 覆盖写入 |
| 文本提取 / 转换 | 公式REGEXEXTRACT/TEXT/VALUE | Python 正则 → 写静态值 |
| 查找匹配 | 公式VLOOKUP/INDEX+MATCH | pandas merge → 写静态值 |
**只有以下才用代码**:多步清洗流水线、统计建模、公式试错 3 次仍失败的降级。代码结果回写:大块纯值用 `+csv-put`+ `--start-cell`,必要时自动扩容);少量或需公式 / 样式用 `+cells-set`;能用飞书公式表达的写飞书公式。
6. **写入与修改(细节见 `lark-sheets-write-cells`**`+cells-set``range` 必须落在已有行列范围内、`cells` 二维数组与 `range` 严格同维;表尾追加先用 `+dim-insert` 插行列再写;整列 / 整行同结构的值 / 公式 / 格式用模板单元格 + `--copy-to-range`,禁止逐行 `+cells-set`;多步写入合并为 `+batch-update`;改尺寸先读相邻可见行列当前尺寸再决定 `pixel` / `standard` / `auto`,不要猜数值。
7. **验证**:重新读取受影响区域确认值 / 公式 / 样式 / 批注符合预期;对象类(图表 / 透视表 / 条件格式 / 筛选 / 迷你图 / 浮动图片)重新读对象配置确认;出错先定位错误类型 / 受影响区域 / 根因再修复重验。
## 用本地代码 / 脚本时的 CLI 配合要点
复杂处理——多步清洗、统计建模、批量转换、语义任务的分批编排等——用代码(`python` / `node` 等)解决是完全正当的。原生能力(公式 / `+pivot` / `+filter`)能表达就优先用(可随源数据自动重算);原生表达不了或逻辑更复杂时,放手用代码。下面几条让脚本与 CLI 顺畅配合:
- **解析输出时只读 stdout**CLI 把数据 JSON 写到 stdout、把诊断与警告写到 stderr。解析 JSON 时**不要合并这两条流**(即不要 `2>&1`),否则警告行混进 JSON 会让解析失败。用管道(`lark-cli … | jq …`)或先把 stdout 单独重定向到文件再读;需要诊断信息时把 stderr 另导到一个文件。
- **喂给 CLI 的 CSV / JSON 用 UTF-8、不带 BOM**BOM 会污染首格的值或触发 `invalid character` 解析错;脚本读写文件时显式指定 `encoding='utf-8'`
- **临时文件交给运行时的标准库**:用 `tempfile.gettempdir()` / `os.tmpdir()` 等取临时目录,不要硬编码固定路径;放在用户项目目录之外。
- **命令失败先读错误再调整**:同一条命令失败后不要原样重发;先看 stderr 的报错(参数错误、缺依赖、解释器不可用等)定位原因,再决定换写法、补依赖或退回原生工具。
- **写回的必须是纯单元格值,禁止把"值+样式标注"串当值写回**:本地脚本或某些 xlsx 解析库会把单元格渲染成 `甲方支行(V-Align: bottom)` 这种"值(样式)"字符串CSV 字段还可能带包裹双引号。回写前必须**剥离括号样式标注、去掉残留引号**,只写原始值——否则样式描述会变成单元格的字面文本污染原数据(反例:排序后单元格值里被写进 `(V-Align: bottom)` 这类样式后缀文本,末尾还多一个双引号)。**排序本身优先用 `+range-sort` 原生工具**,不要"读出来本地排完再整列写回",从根上避免这类回写污染。
## 公式策略
- **公式优先于硬编码**(同铁律 4能用公式表达的计算一律写公式源数据变化才能自动重算。
- **写任何公式前先读 `lark-sheets-formula-translation`**:它是公式正确性的唯一权威,覆盖绝对引用(`$`)、飞书范围语法(`H:H` 与工具 A1 表示法的区别、ARRAYFORMULA / 数组语义、Excel 迁移、不支持函数清单等全部规则。本文不再单列这些细则。
## 常见陷阱(铁律已覆盖的不再重复,仅列易漏点)
- **合并单元格**:合并区只有左上角存数据,其余读为空是正常行为;写入只能写左上角,写其它位置会报 `cell ... is inside a merged region`。改合并区先取消再操作。安全操作 5 条与"批量取消用大 range 一次调用"见 `lark-sheets-range-operations`
- **`+dim-insert` 不继承行高**`--inherit-style before/after` 只继承值 / 公式 / 边框,不继承 `row_height`,新行会回落默认高度截断长文本;中间插行填文本前先读相邻行 `row_height`,用 `+batch-update``+rows-resize` 补齐。
- **公式容错**:日期 / 查找 / 数值转换公式用 `IFERROR` 包裹;写完读结果列首 5 + 末 5 行查 `#VALUE!` / `#NAME?` / `#REF!` / `#DIV/0!`;同一方案试错上限 3 次,超了改代码以值写入。
- **循环引用**聚合公式SUM/AVERAGE引用范围不能含目标 cell 自身或其传递依赖。
- **NaN / 空值 / 除零**:空值不直接参与运算;除法用 `IF` / `IFERROR` 防零。
- **排序 / 筛选混合文本列**:带货币符 / 单位 / 表达式的文本列直接排序 / 筛选会按字典序出错,先抽数值到辅助列再处理(细则见 `lark-sheets-range-operations` / `lark-sheets-filter`)。
- **隐藏行列**`+csv-get` 默认 `--skip-hidden=false`(含隐藏行列);设 `true` 只看可见数据,但返回行序号与实际行号不再对应。
- **行号一律取 `[row=N]` 前缀**`+csv-get` 的 CSV 中双引号内换行是单元格内换行不是新行;禁止数 `\n`、禁止用"序号列"当行号(细则见 `lark-sheets-read-data`)。
- **列字母取 `col_indices[j]`**:禁止手数表头逗号定位列(>10 列极易 off-by-one
- **跨 sheet 对象**:图表 / 条件格式 / 透视表 / 浮动图片可能分布在多个子表,操作前先 `+workbook-info` 掌握全局。
- **`+cells-search` 不是万能**:用户说"汇总金额"是操作动作(求和),不是搜索该文本;只在确需定位某文本位置时才用。
## 特殊场景
### 续写 / 复制已有区块格式
核心要求见铁律 5。机制带齐哪些样式字段、怎么采样写入`lark-sheets-write-cells` 的「新增列 / 新增行的样式继承」;样式标准(斑马纹奇偶 / 配色 / 边框层级)见 `lark-sheets-visual-standards` 场景二。本文不再展开。
### NLP 任务处理
任务涉及语义理解、翻译、改写、摘要、分类、抽取、多行聚合时,以 NLP 方式处理,不要用纯规则代码替代语义理解(但可用代码做分批、行号映射、结果拼装与写回)。数据量大时**必须**分批(通常 30 行一批),每批处理完立即写回,不要全处理完再一次写入;单批生成通常不超 300 行,超出时按性质抽样或分批并向用户说明范围;多批写入优先用 `+batch-update` 合并为原子提交。
### 格式处理优先公式
"去除多余零 / 提取数字 / 文本格式转换 / 日期格式化"等清洗,**必须优先用公式**`SUBSTITUTE` / `TEXT` / `VALUE` / `LEFT` / `RIGHT` / `MID` 等):写一个模板 + `--copy-to-range` 即可整列处理,远比逐行修改高效。

View File

@@ -50,7 +50,7 @@ _公共四件套 · 系统:`--dry-run`_
| --- | --- | --- | --- |
| `--properties` | string + File + Stdin复合 JSON | required | 筛选视图规则 JSON`rules?`(列级筛选规则数组)和 `filtered_columns?``range``view_name` 是独立 flag |
| `--range` | string | required | 筛选视图作用的单元格范围A1 表示法,如 `A1:F1000`);优先级高于 `--properties` 中同名字段create 必填,必须覆盖表头行 |
| `--view-name` | string | optional | 筛选视图名称;create 不传时系统自动分配update 不传时保留原名;优先级高于 `--properties` 中同名字段 |
| `--view-name` | string | optional | 筛选视图名称;不传时系统自动分配;优先级高于 `--properties` 中同名字段 |
### `+filter-view-update`

View File

@@ -29,9 +29,9 @@
- **`--image <本地路径>`(首选,最省事)**直接给本地图片文件路径PNG/JPEG/GIF/BMP/HEIC 等。CLI 会自动把它以 `parent_type=sheet_image` 上传,拿到 file_token 后创建浮动图,**不用你手动上传 / 取 token**。路径规则同其它本地文件 flag必须是当前工作目录内的相对路径绝对路径会被 Validate 拒,`--dry-run` 也会拦)。
- `--image-token`:复用**已存在**的图片 file_token。常见来源`+float-image-list` 返回的 `image_token`(适合"换皮不换位置"复用同一张图);② `+cells-set-image` 成功返回里的 `file_token`(它也是 `sheet_image` 上传句柄)。适合"同一张图复用到多处",省去重复上传。
- `--image-uri`:图片 reference_idimage URI由系统自动转 file_token。
- `--image-uri`:图片 URI上传链路返回的句柄**非**表内对象 reference_id由系统自动转 file_token。
> ⚠️ **`--image` 仅 `+float-image-create` 支持**。`+float-image-update` 换图仍只接受 `--image-token` / `--image-uri`,而且**图片源是 update 唯一可省的部分**——三者全不传则保留原图。但 `--image-name` / `--position-{row,col}` / `--size-{width,height}` 在 update 时和 create 一样**必填**`+float-image-update` 强制要求这套核心字段,且 `+float-image-list` 不回传 `image_name` 供 CLI 回填)。要在 update 里换一张本地新图,先用 `+cells-set-image` 上传到任意临时单元格、从返回取 `file_token`,再把它传给 update 的 `--image-token`。
> ⚠️ **`--image` 仅 `+float-image-create` 支持**。`+float-image-update` 换图仍只接受 `--image-token` / `--image-uri`,而且**图片源是 update 唯一可省的部分**——三者全不传则保留原图。但 `--image-name` / `--position-{row,col}` / `--size-{width,height}` 在 update 时和 create 一样**必填**`+float-image-update` 强制要求这套核心字段,且 `+float-image-list` 不回传 `image_name` 供 CLI 回填)。要在 update 里换一张本地新图,先用 `+cells-set-image` 上传到任意临时单元格、从返回取 `file_token`,再把它传给 update 的 `--image-token`;用完清除该临时单元格,避免残留多余图片
## Shortcuts
@@ -122,7 +122,7 @@ lark-cli sheets +float-image-create --url "..." --sheet-id "$SID" \
--image-name "logo.png" --image-token "$TOKEN" \
--position-row 0 --position-col A --size-width 200 --size-height 150
# 用 reference_id图片上传链路返回的 image reference_id与 --image-token 二选一)
# 用 image URI上传链路返回的句柄非表内对象 reference_id与 --image-token 二选一)
lark-cli sheets +float-image-create --url "..." --sheet-id "$SID" \
--image-name "logo.png" --image-uri "$IMAGE_URI" \
--position-row 2 --position-col B --size-width 300 --size-height 200 --z-index 1

View File

@@ -1,14 +1,14 @@
# 飞书表格公式生成规则
> **本文定位**:飞书公式正确性的**唯一权威**——书写任何飞书公式、或把 Excel 公式迁移到飞书前,先读本文。涵盖公式书写约定(绝对引用、范围语法)、投影 vs spill、ARRAYFORMULA / 数组语义、高风险引用函数、日期差、不支持函数清单。
> **边界**:本文只讲"公式怎么写对";公式**怎么写入表格**`+cells-set` / 模板单元格 + `--copy-to-range` / 容错回读)见 `lark-sheets-write-cells` 与 `lark-sheets-core-operations`。本文不含 shortcut铁律见 `lark-sheets-core-operations`
> **边界**:本文只讲"公式怎么写对";公式**怎么写入表格**`+cells-set` / 模板单元格 + `--copy-to-range` / 容错回读)见 `lark-sheets-write-cells`。本文不含 shortcut通用编辑准则见主 SKILL.md「飞书表格编辑准则」
**核心原则:飞书不像 Excel 365 那样默认 spill溢出展开。飞书普通公式遇到区域时默认"投影"(只取当前行/列对应的单个值),必须显式使用 `ARRAYFORMULA` 或原生数组函数才能逐项展开。**
## 公式书写约定(写任何公式都先满足)
- **绝对引用 `$`**:向下 / 向右填充前判断哪些引用要锁定——用户指定的固定 cell`$C$3`)、要固定的数据范围(`$A$2:$B$5`)、锁列不锁行(`$A2`)、锁行不锁列(`B$1`)。填充前检查是否需固定汇率 / 税率 / 查找表 / 权重表,以及同列 / 同行公式结构是否一致。
- **公式字符串用飞书范围语法**:写 `H:H``A2:B5`**禁止** `H2:H` / `2:2`这与 CLI 工具参数(如 `--range`)的 A1 表示法(`A1:D3``1:1`)写法不同,两者混淆会导致调用失败或公式报错。
- **公式字符串用飞书范围语法**:写 `H:H``A2:B5`**禁止** `H2:H` / `2:2`要在公式里引用整行,用显式范围(如 `$A2:$Z2`)替代禁用的 `2:2`。这与 CLI 工具参数(如 `--range` / `--copy-to-range`)的 A1 表示法写法不同:参数侧合法的 `D3:D``1:1``3:6` 在公式串里反而非法。**公式串 ≠ CLI 参数**,两套规则别互相照搬,混用会导致调用失败或公式报错。
## 翻译后必做:代码复现校验
@@ -224,7 +224,7 @@ Excel`{=A1:A10*B1:B10}`Ctrl+Shift+Enter 输入)
## 飞书不支持的函数
> 本段是"飞书不支持函数"的**唯一权威清单**`lark-sheets-core-operations` 不再单列,统一指向这里)。以下函数在飞书里不存在或被禁用,禁止主动使用;用户明确要求时应拒绝并提供替代方案:
> 本段是"飞书不支持函数"的**唯一权威清单**。以下函数在飞书里不存在或被禁用,禁止主动使用;用户明确要求时应拒绝并提供替代方案:
- `STOCKHISTORY` — 实时股票数据,飞书无等价函数,需手动导入数据
- `WEBSERVICE` — 外部 HTTP 请求,飞书无等价函数
@@ -234,6 +234,7 @@ Excel`{=A1:A10*B1:B10}`Ctrl+Shift+Enter 输入)
- `INFO``RTD` — 系统信息 / 实时数据函数,飞书不支持
- `PIVOT` — 用 `+pivot-{create|update|delete}` 透视表对象替代
- `AMORDEGRC``PHONETIC``DETECTLANGUAGE` — 飞书不支持
- `LET`、命名自定义函数(名称管理器里定义的 LAMBDA、独立调用的 `LAMBDA`(如 `=LAMBDA(x,x+1)(5)`)— 会报 `#NAME?`;改用嵌套 IF / 辅助列。**例外**`LAMBDA` 作为 `MAP` / `REDUCE` / `BYROW` / `BYCOL` / `SCAN` / `MAKEARRAY` 的内联参数时**支持**(见上方「飞书原生数组函数清单」)
## 代表性改写示例

View File

@@ -0,0 +1,76 @@
# 飞书表格公式自检(+formula-verify
> **本文定位**:飞书表格"公式写入后是否真的零错误"的自检入口。公式的书写规则与 Excel→飞书迁移的语义规则一律以 `lark-sheets-formula-translation` 为唯一权威,本文不重复;本文聚焦"写完了之后怎么用一次调用确认 zero-error"。
>
> **边界**:本文不讲公式怎么写(去 `lark-sheets-formula-translation`),也不讲公式怎么写入表格(去 `lark-sheets-write-cells` / `lark-sheets-batch-update`)。本文只讲一件事:**写完之后必须用 `+formula-verify` 自检到 zero-error 才能交付**。
## 为什么需要自检
飞书在线表格已经实时算好结果,但"算出来"和"算对了"是两件事。常见缺口:
- 公式编译失败 → 单元格落成文本(写入类 shortcut 返回的 `formula_errors[]` 是**编译失败**信号)。
- 公式编译成功但**运行时错误**`#REF!` / `#DIV/0!` / `#VALUE!` / `#NAME?` / `#NULL!` / `#NUM!` / `#N/A`——这一类只看 `formula_errors[]` 看不到,必须扫单元格值。
`+formula-verify` 把两路信号合并成一份统一 JSON一次调用聚合全表错误清单 + 编译失败清单 + 每类错误的定位与样本AI 一眼就能定位修复,链路也能据 `status` 强制收敛到 `success`
## 调用契约
最小调用形态:
| 入参 | 含义 |
|---|---|
| `--url` / `--spreadsheet-token` | 表格定位XOR 二选一,必填) |
| `--sheet-id` / `--sheet-name` | 限定子表mutually exclusive省略则扫全部可见子表 |
| `--range` | 限定 A1 范围;省略则用各 sheet 的 `current_region` |
| `--max-locations` | 每类错误样本上限,默认 20 |
| `--exit-on-error` | `status='errors_found'` 时返回非 0 退出码CI 网关用) |
返回核心字段:
- `status``success` / `errors_found` / `partial`——**唯一可机读的健康度判据**。
- `total_errors` / `total_formulas` / `scanned_cells`——本次扫描规模指标。
- `has_more`——为 true 表示扫描被内部上限截断(详见后文「截断与续读」),未覆盖完整范围。
- `error_summary[<错误类型>]`——每类错误的 `count` / `locations[]` / `samples[].{address,formula,depends_on}`
- `compile_errors[]`——合并最近一次写入留下的编译失败清单,与运行时错误并存时同时出现。
- `warning_message`——仅在 `has_more=true` 时出现,告知调用方需要缩小 `--range` / 拆 `--sheet-id` 续读。
## 写入收尾收敛规则
任何批量公式 / 含公式列写入完成后调用 `+formula-verify` 直到 `status='success'` 才能交付。触发场景:
- `+cells-set` / `+cells-csv-set`
- `+sandbox-import`
- `+batch-update` 中含写入子操作
- `+table-put`(任意列含公式时)
- `+workbook-import`(导入的 xlsx 含公式时)
收敛规则:
1. `status='success'` → 通过;可以把链路标完成。
2. `status='partial'` → 扫描被内部上限截断。先缩小 `--range` 或拆 `--sheet-id` 续扫,**不允许**把 `partial` 当作 `success`
3. `status='errors_found'``compile_errors[]` 非空 → **先解决编译失败**:根据 `compile_errors[].reason` 修正公式语法(飞书函数名 / 范围语法 / 引用样式),用 `+cells-set` 重写后再调一次 `+formula-verify`
4. `status='errors_found'` 且只剩运行时错误 → 按 `error_summary``samples[].formula` + `depends_on` 排查根因(零除?空值参与运算?引用越界?日期差写法?数组语义?),修复后重新自检。
5. 同一处错误连续修复 3 次仍未通过 → 改用 `IFERROR` 包裹兜底,或退回纯值写入;不要在 `errors_found` 状态下扩展 `+cells-set --copy-to-range`、追加批量写入。
注意:
-`status='errors_found'` 的状态下调用 `+cells-set --copy-to-range` 继续扩展会把错误复制放大。
- "编译失败但运行时无报错"不是 zero-error编译失败的单元格此刻是文本不是公式源数据一变就再也算不出值
- 跳过自检直接交付、靠肉眼读首末 5 行确认是不可靠的——表中段、隐藏行、合并区里的错误这样根本看不到。
## 截断与续读
后端有一个内部硬上限对总扫描单元格数做截断(不暴露给调用方),超过后立即返回 `has_more=true` + `warning_message``error_summary` / `compile_errors` 仅覆盖已扫描部分。处理路径:
- 把工作簿按 `--sheet-id` / `--sheet-name` 拆成多次调用。
- 同 sheet 内按 `--range` 切片(如先 `A1:Z200``AA1:AZ200`),逐块自检。
- 每块都跑到 `has_more=false``status='success'` 才算通过。
## 常见陷阱
| 坑 | 应对 |
|---|---|
| 错误字符串本地化 | 后端按内部 `error_kind` / `compute_status` 字段识别错误类别,不走字符串匹配;调用方拿到的 7 类英文错误代码由后端统一规范输出,与 locale 无关。 |
| `formatted_value` 可能隐藏错误 | 某些条件格式 / 自定义数字格式会把 `#DIV/0!` 显示成空白。后端直接读 cell `error_kind`,不依赖 `formatted_value`,绕开此类被遮蔽。 |
| 把 `partial``success` | `partial` 仅表示**已扫描部分**无错误,剩余区域未知。必须续扫直到 `has_more=false``status='success'` 才能算通过。 |
| 编译失败 vs 运行时错误 | 同一份报告里 `compile_errors[]``error_summary` 并存。语义层先解决 `compile_errors[]`、再做运行时自检。 |

View File

@@ -0,0 +1,93 @@
# Lark Sheet History
## 概念回顾
每张飞书电子表格保留一串历史版本(`minor_histories`)。每个版本由 `history_version_id` 标识,并附带创建时间(`create_time`)、动作(`action`)与块修订信息(`all_block_revision`)。历史是**工作簿级**的(针对整张电子表格,不针对单个子表)。
回滚revert把电子表格的当前内容覆盖回某个历史版本——这是一个**写入 / 不可逆**操作,且为**异步**:发起后立即返回受理标识,真正的回滚在后台进行,需通过状态查询轮询最终结果(进行中 / 成功 / 失败)。
`+history-list` 读取版本列表以挑选目标;`+history-revert` 发起回滚;`+history-revert-status` 轮询回滚结果。
## 使用场景
读取历史版本、发起回滚、查询回滚状态。本 reference 覆盖 3 个 shortcut
| 操作需求 | 使用工具 | 说明 |
|---------|---------|------|
| 查看历史版本列表 | `+history-list` | 返回 `minor_histories`,每条含 `history_version_id` / `create_time` / `action` / `all_block_revision` 四个字段;支持向前分页(可选 `--end-version` |
| 回滚到指定历史版本 | `+history-revert` | 传入 `--history-version-id`;异步受理,返回可查询标识 |
| 查询回滚状态 | `+history-revert-status` | 传入 `--transaction-id`(取自 `+history-revert` 的异步受理标识);轮询某次回滚的进行中 / 成功 / 失败状态 |
典型工作流:`+history-list` 拿到目标版本的 `history_version_id`(必要时翻页拉取更早历史)→ `+history-revert` 发起回滚并取回 `transaction_id``+history-revert-status --transaction-id <transaction_id>` 轮询直到成功或失败。
**注意事项(必须了解)**
- **回滚是写入 / 不可逆操作**:会用历史版本内容覆盖当前表格,发起前请确认目标 `history_version_id` 正确。
- **回滚是异步的**`+history-revert` 返回的是 `transaction_id`(受理标识),不代表回滚已完成;必须用 `+history-revert-status --transaction-id <transaction_id>` 确认最终结果。
- **`history_version_id``transaction_id` 不是同一个**`history_version_id` 用于 `+history-revert`(取自 `+history-list``transaction_id` 用于 `+history-revert-status`(取自 `+history-revert` 的输出)。
- **历史是工作簿级**:定位只需 `--url` / `--spreadsheet-token`XOR不需要子表选择器。
- **`+history-list` 倒序分页**:首次查省略 `--end-version`,返回最新一页;若响应里附带 `next_end_version``has_more=true`,把 `next_end_version` 作为下一次的 `--end-version` 即可继续向更早翻页;当响应**不包含**这两个字段时表示已到最早一页,不必再翻。
## Shortcuts
| Shortcut | Risk | 分组 |
| --- | --- | --- |
| `+history-list` | read | 历史版本 |
| `+history-revert` | write | 历史版本 |
| `+history-revert-status` | read | 历史版本 |
## Flags
### `+history-list`
_公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--end-version` | int | optional | 分页查询的最大版本(倒序);首次查询省略,下一页传上一页返回的 next_end_version。 |
### `+history-revert`
_公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--history-version-id` | string | required | 要回滚到的历史版本(取自 +history-list |
### `+history-revert-status`
_公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--transaction-id` | string | required | 异步回滚的受理标识(取自 +history-revert |
## Examples
公共定位:所有 shortcut 顶部排列 `--url` / `--spreadsheet-token`XOR二选一`+history-revert``--history-version-id`(取自 `+history-list``+history-revert-status``--transaction-id`(取自 `+history-revert` 的异步受理标识)。
### `+history-list`
```bash
# 列出某张电子表格的最新一页历史版本
lark-cli sheets +history-list --url "https://sample.feishu.cn/sheets/SHTxxxxxx"
# 用原始 spreadsheet token 定位
lark-cli sheets +history-list --spreadsheet-token "SHTxxxxxx"
# 翻到下一页:把上次响应里的 next_end_version 作为 --end-version 传入
lark-cli sheets +history-list --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --end-version 12345
```
### `+history-revert`
```bash
# 回滚到指定历史版本(异步受理)
lark-cli sheets +history-revert --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --history-version-id "<id-from-history-list>"
```
### `+history-revert-status`
```bash
# 查询某次回滚的当前状态(进行中 / 成功 / 失败)
lark-cli sheets +history-revert-status --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --transaction-id "<transaction-id-from-history-revert>"
```

View File

@@ -32,9 +32,10 @@
**常见配置错误(必须注意)**
- **数据源范围必须精确**:透视表的数据源范围必须包含表头行,且精确覆盖全部数据行列。范围过大(包含空行/空列)或过小(遗漏数据列)都会导致透视表结果错误
- **行列字段选择要匹配用户意图**:用户说"按商品统计金额"→ 行字段=商品,值字段=金额(`summarize_by: "sum"`)。不要把行列字段搞反
- **聚合类型要匹配**:用户说"统计数量"→ `summarize_by: "count"`"统计总额"→ `"sum"`"统计平均"→ `"average"`。完整合法值:`sum` / `count` / `average` / `max` / `min` / `product` / `countNums` / `stdDev` / `stdDevp` / `var` / `varp` / `distinct` / `median`默认不要 `count` `sum`
- **聚合类型要匹配**:用户说"统计数量"→ `summarize_by: "count"`"统计总额"→ `"sum"`"统计平均"→ `"average"`。完整合法值:`sum` / `count` / `average` / `max` / `min` / `product` / `countNums` / `stdDev` / `stdDevp` / `var` / `varp` / `distinct` / `median`按用户意图选聚合方式,不要 `count` `sum`
- **参数长度限制**:如果透视表配置 JSON 过长(数据源范围跨越大量行列),可能导致工具调用失败。此时应先确认数据范围的精确边界,避免传入过大的 range
- **创建后必须验证**:调用 `+pivot-list` 确认透视表结构正确
- **落点不能覆盖任何已有数据(不只是 `--source` 范围)**:透视表创建后会向右下**展开**,展开区域哪怕只盖到一个已有单元格(即便已避开源数据),也会报「目标位置不能与数据源重叠」并产生 `#REF!`。创建前无法精确预知展开尺寸,故**强烈优先默认策略**(不传 `--target-sheet-id/-name``--target-position`/`--range`,后端自动新建空白子表),零覆盖风险;非要落到已有子表,必须挑一片足够大的纯空白区
- **创建后必须校验(用 `info` 读取展开后的真实占用区域)**:创建后调用 `+pivot-list``info.error_state``info.content_range`/`page_range`——`error_state``None`(如 `Cover` 盖到其它内容 / `Shrink` 展不开)说明落点冲突,应删除后重建到空白区;`content_range`/`page_range` 是展开后**实际占用区域**,可用 `+csv-get` 抽查其边缘外有没有盖掉原有数据,确认结构正确
## Shortcuts
@@ -120,6 +121,10 @@ _创建/更新的透视表属性_
lark-cli sheets +pivot-list --url "..." --sheet-id "$SID"
```
> **返回值含 `info`(展开后的占用区域与状态)**:每个透视表对象除 `position` / `snapshot` 外,还返回 `info`,标明它在 sheet 上的平铺区域与状态——`info.page_range`(筛选/分页区 A1、`info.content_range`(主体数据区 A1、`info.span_range`(空表合并区 A1、`info.error_state`(错误状态,如 `None`/`Cover`/`Shrink`/`Loading`)、`info.is_empty` / `info.is_hidden`、`info.row`/`info.col`(锚点)等。
> **用途 1判断改值还是改配置**:当用户描述某个单元格要改动时,先 `+pivot-list` 拿到 `info`,判断该单元格是否落在 `page_range` / `content_range` 内——**落在区域内 = 属于透视表,应走 `+pivot-update` 改配置**(透视表单元格不能直接 `+cells-set` 改值);**落在区域外 = 普通单元格,正常 `+cells-set` 改值**。
> **用途 2创建后校验覆盖**:建完透视表用 `info.error_state` 判断有没有冲突(非 `None` 即落点/展开区与已有数据重叠或展不开),用 `info.content_range`/`page_range` 拿到展开后真实占用区域再核对是否盖到原有数据。
### `+pivot-create`
> 数据源 `--source` 必须从表头行开始;空行 / 汇总行会被当作数据参与聚合,需提前用 `+csv-get` 确认起止边界。`--source` 和 `--range` 是独立 flag不要再放 `--properties``rows` / `columns` / `values` 等数组字段走 `--properties`。

View File

@@ -22,6 +22,7 @@
注意:
- **`--range` 两种语法别混**`+cells-clear` / `+cells-{merge|unmerge}` / `+range-*` 用单元格 A1 矩形(如 `A2:A10``+rows-resize` / `+cols-resize` 用纯行 / 列区间(行 `2:10`、列 `A:C`),不要给 resize 传 `A2:A10`
- 用户说"这行 / 整行 / 首行"时,优先使用整行范围如 `1:1`"这列 / 整列"时使用 `J:J`。不要截断为局部矩形
- 合并后只保留左上角单元格的内容,其余清除。写入合并区域用 `+cells-set` 对左上角单元格操作
- 调整行高列宽时,先读取相邻行列尺寸再决定像素值,不要随意猜测
@@ -35,7 +36,7 @@
2. **判定阈值**:当前列宽(用 `+sheet-info --include row_heights,col_widths` 拿)≥ 最长字符数 × 字体宽度系数 + buffer 才算适配。默认列宽 11 通常只够 11 个半角字符或 5-6 个汉字,写长文本前必扩宽。
3. **修复二选一**
- **扩列宽**:用 `+rows-resize / +cols-resize` 把目标列宽设为 `max(表头字符数, 内容采样最长字符数) × 8 + 16` 像素(经验值)
- **自动换行**:在 `+cells-set` 时给单元格设置 `cell_styles.word_wrap="auto-wrap"`(可选值:`overflow` / `auto-wrap` / `word-clip`),并用 `+rows-resize / +cols-resize` 调高对应行的行高
- **自动换行**:在 `+cells-set` 时给单元格设置 `cell_styles.word_wrap="auto-wrap"`(可选值:`overflow` / `auto-wrap` / `word-clip``cell_styles` 字段见 `lark-sheets-write-cells`),并用 `+rows-resize / +cols-resize` 调高对应行的行高
4. **新增列默认列宽规则**:新增列宽度 ≥ `max(表头字符数, 内容采样最长字符数) × 8 + 16` 像素,**禁止**用默认 11 直接交付。
**典型反例**:默认列宽 11 但内容含 12+ 字符的中文 / 含单位的数值(如 `109.10μmol/L`/ 长数字未设 `number_format` 显示为科学计数法 —— 用户在结果表里看不到完整原值。

View File

@@ -2,7 +2,7 @@
## 列格式多样性预探(写公式 / 排序 / 筛选前必做)
> 对应 `lark-sheets-core-operations` 的 **R3 计算复现**——本节是 R3 在 read_data 工具层的具体落地。
> 本节给出"写公式 / 排序 / 筛选前先探清列格式多样性"的正确流程,是主 SKILL.md「飞书表格编辑准则」准则 3读全再写在 read_data 工具层的落地。
对参与后续**计算 / 排序 / 筛选 / 公式提取**的列,**必须**先 sample **至少 50 行**(小表则全量),识别该列所有值类型变体后再设计公式 / 条件。只看前 10 行不够,因为下列差异通常潜伏在表尾或中段:
@@ -22,7 +22,7 @@
| 读取目的 | 用这个 shortcut | 数据去向 | 说明 |
|---------|----------------|---------|------|
| 快速查看纯值数据、批量处理 | `+csv-get` | 对话上下文 | 返回 CSV 文本(每行带 `[row=N]` 前缀);大表请按 `--range` 行窗口分批读(截断时看 `has_more` |
| 按列类型结构化读出(喂 DataFrame / round-trip 回 `+table-put` | `+table-get` | 对话上下文 | 返回 typed 协议(`columns:[列名]` + `data` + `dtypes`/`formats` + `range`),输出形状对齐 pandas split可一行 `pd.DataFrame(sheet["data"], columns=sheet["columns"]).astype(sheet["dtypes"])` 还原 DataFrame或直接 round-trip 回 `+table-put`。不带 `--range` 时读**完整 used range**(跨过表中部空行 / 空列),每个子表回传实际读取范围 `range` 供完整性校验 |
| 按列类型结构化读出(喂 DataFrame / round-trip 回 `+table-put` | `+table-get` | 对话上下文 | 返回 typed 协议(`columns:[列名]` + `data` + `dtypes`/`formats` + `range`),输出形状对齐 pandas split可一行 `pd.DataFrame(sheet["data"], columns=sheet["columns"]).astype(sheet["dtypes"])` 还原 DataFrame或直接 round-trip 回 `+table-put`。不带 `--range` 时读**完整 used range**(跨过表中部空行 / 空列),每个子表回传实际读取范围 `range` 供完整性校验。注意这与下文 `current_region` "遇表中部空行截断"不矛盾:`+table-get` 读的是子表物理 used range飞书记录的已用矩形含中间空行`current_region` 是从锚点连通扩展、遇整行空行就断 |
| 查看公式、样式、批注、数据验证 | `+cells-get` | 对话上下文 | 返回单元格完整信息token 开销较大 |
| 查看某区域的下拉框(数据验证)选项 | `+dropdown-get` | 对话上下文 | 返回该 A1 范围已配置的下拉列表选项 |
@@ -42,7 +42,7 @@
注意:
- `+csv-get``+cells-get` 支持分页/截断,注意检查 `has_more` / `truncated` 标志;使用 `+cells-get` 时,在读取 `cells` 之前必须先 `warning_message`,并用每个 range 的 `actual_range` / `row_indices` / `col_indices` 判断真实位置
- `+csv-get``+cells-get` 支持分页/截断,注意检查 `has_more` / `truncated` 标志;两者在处理返回数据之前必须先 `warning_message`(上游 schema 要求先读它再用其它字段,内含定位与截断续读提示),`+cells-get` 还要用每个 range 的 `actual_range` / `row_indices` / `col_indices` 判断真实位置
- 隐藏行列默认包含在返回结果中(`--skip-hidden=false`),如需只看可见数据设为 `true`。读取原语本身不标注哪些行列被隐藏:若要识别隐藏区间(以决定是否过滤、或如何解读混入的隐藏数据),用 `+sheet-info --include hidden_rows,hidden_cols` 取隐藏行列集合,再结合 `+csv-get` / `+cells-get` 返回的 `row_indices` / `col_indices` 判断每行 / 每列是否隐藏
**常见配置错误(必须注意)**

View File

@@ -39,7 +39,7 @@
**常见配置错误(必须注意)**
- **插入列直接用字母**`+dim-insert``--position` 在列场景直接传字母(如 `C`),不要把列字母换算成 0-based 索引
- **插入后引用偏移**:插入行/列后,原有数据的行号 / 列字母会发生偏移。如果插入后还需要对原有区域执行写入操作,必须重新计算偏移后的位置
- **删除行列前先确认范围**:删除操作不可逆,执行前应确认 `--range` 精确无误。可先用 `+csv-get` 读取目标区域验证内容
- **删除行列前先确认范围**:删除操作不可逆,执行前应确认 `--range` 精确无误。可先用 `+csv-get` 读取目标区域验证内容`+csv-get` / `+cells-get``lark-sheets-read-data`
- **"在 D 列左侧新增一列"的正确写法**`--position D --count 1`(新列插在 D 列之前);要继承左侧列样式加 `--inherit-style before`
- **`+dim-move` 同维度约束**`--source-range` 是行区间时 `--target` 必须是行号(数字),是列区间时 `--target` 必须是列字母——不可一行一列混用
- **插入列后必须检查多行表头合并区域**:很多表格有 2-3 行的合并表头。插入列后,原有的合并区域不会自动扩展到新列。必须先用 `+sheet-info --include merges` 读取合并区域,插入后将跨越插入位置的合并区域重新设置(用 `+cells-{merge|unmerge}`),否则新列的表头会是空的、格式不连续
@@ -129,7 +129,7 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--depth` | int | optional | 要取消的分组层级,默认 1最外层) |
| `--depth` | int | optional | 要取消的分组层级,默认 11=最外层,数字越大越内层) |
| `--range` | string | required | 要取消分组的行/列闭区间;行如 `3:7`,列如 `C:F` |
### `+dim-move`

View File

@@ -1,7 +1,7 @@
# 飞书表格样式与配色规范
> **本文定位**:飞书表格"正确视觉输出"的取值标准与美化决策流——配色、表头、对齐、数值格式、斑马纹、列宽行高、图表展示,以及新增 / 继承 / 美化已有区域三类场景的做法。
> **边界**:本文只讲"样式长什么样、怎么决策"**怎么调用工具写入样式**`cell_styles` / `border_styles` 字段、合并、resize 等参数)见 `lark-sheets-write-cells` / `lark-sheets-range-operations` / `lark-sheets-batch-update`。**条件格式**(高亮 / 标红 / 数据条 / 色阶)见 `lark-sheets-conditional-format`。本文不含 shortcut铁律见 `lark-sheets-core-operations`
> **边界**:本文只讲"样式长什么样、怎么决策"**怎么调用工具写入样式**`cell_styles` / `border_styles` 字段、合并、resize 等参数)见 `lark-sheets-write-cells` / `lark-sheets-range-operations` / `lark-sheets-batch-update`。**条件格式**(高亮 / 标红 / 数据条 / 色阶)见 `lark-sheets-conditional-format`。本文不含 shortcut通用编辑准则见主 SKILL.md「飞书表格编辑准则」
## 最高优先级原则
@@ -64,7 +64,7 @@
- 若追加位置紧邻汇总行、说明区或空白分隔区,先判断真实数据区域边界再操作,避免破坏原有结构。
- **Zebra Stripes 维护**:插入或删除行后若影响后续行奇偶性,须从受影响行往后重建条纹(先清理再重设)。少量增删用局部重建,大量变动用全局清理+统一重建。
- 具体采样与复制流程见下方「场景二:从已有区域继承美化」。
- **列宽调整**(飞书 `+rows-resize / +cols-resize` 按 pixel 传值):
- **列宽 / 行高调整**(飞书 `+rows-resize / +cols-resize` 按 pixel 传值):
- 禁止硬编码固定列宽,须根据该列实际内容长度估算像素。
- 经验估算:中文每字约 15-18px英文/数字每字约 7-9px外加 10-16px padding。
- 上下限建议 80~400px超上限启用自动换行`word_wrap: auto-wrap`+ 调整行高,而非无限加宽。
@@ -82,7 +82,7 @@
- 包含必要元素:标题、图例、数据标签、坐标轴标题。
- 调整至合适大小,避免数据和标签过多堆叠。
- **图表放置防重叠**:新增图表前须计算放置区域,避免与已有图表重叠。具体步骤:
1. 调用 `+chart-list` 获取当前工作表所有已有图表的 `position`(锚点单元格:`row` 行索引、`col` 列索引如 "A"/"B")、`offset`(锚点内偏移:`row_offset``col_offset`,单位像素)以及 `size``width``height`,单位像素)。
1. 调用 `+chart-list` 获取当前工作表所有已有图表的 `position`(锚点单元格:`col` 是列字母如 "A"/"B"`row` 是 1-based 行号;以 `+chart-list` 实际返回字段为准)、`offset`(锚点内偏移:`row_offset``col_offset`,单位像素)以及 `size``width``height`,单位像素)。
2. 获取工作表的行高和列宽信息(像素)。
3. 根据每个图表的锚点 `position.row`/`position.col` + 偏移 `offset.row_offset`/`offset.col_offset` + 尺寸 `size.width`/`size.height`,结合行高列宽,计算出每个已有图表覆盖的像素矩形区域 `(x_min, y_min, x_max, y_max)`
4. 为新图表选定大小后,候选放置位置应避开所有已有矩形区域;若存在重叠则向下或向右偏移,直至找到无冲突位置。

View File

@@ -15,7 +15,11 @@
| 操作需求 | 使用工具 | 说明 |
|---------|---------|------|
| 查看工作簿结构 | `+workbook-info` | 获取子表列表、名称、行列数、冻结位置等元数据 |
| 新建工作簿(可预填数据) | `+workbook-create` | 从内存数据建一张新表(`--values` / `--sheets` typed |
| 导入本地文件为新表 | `+workbook-import` | 把本地 `.xlsx` / `.xls` / `.csv` 导入为新的飞书电子表格 |
| 导出工作簿到本地 | `+workbook-export` | 导出为本地 `.xlsx`(整簿)或单子表 `.csv` |
| 变更工作簿结构 | `+sheet-{create|delete|rename|move|copy|hide|unhide|set-tab-color}` | 新建/删除/移动/重命名/复制/隐藏子表、修改标签颜色 |
| 切换子表网格线显隐 | `+sheet-show-gridline` / `+sheet-hide-gridline` | 显示 / 隐藏单个子表的网格线 |
注意:
@@ -65,6 +69,7 @@ _公共URL/token无 sheet 定位) · 系统:`--dry-run`_
| `--index` | int | optional | 插入位置0-based省略时附加到末尾 |
| `--row-count` | int | optional | 初始行数(默认 200上限 50000 |
| `--col-count` | int | optional | 初始列数(默认 20上限 200 |
| `--type` | string | optional | 新子表类型sheet电子表格\| bitable多维表格默认 sheet。bitable 只建空表,内容编辑改用 lark-base 命令 |
### `+sheet-delete`
@@ -87,7 +92,7 @@ _公共四件套 · 系统:`--dry-run`_
| Flag | Type | 必填 | 说明 |
| --- | --- | --- | --- |
| `--index` | int | required | 目标位置0-based |
| `--source-index` | int | optional | 源位置0-based可选未传时由 CLI runtime 根据 `--sheet-id` / `--sheet-name` 当前在工作簿中的 index 自动派生 |
| `--source-index` | int | optional | 源位置0-basedstandalone 调用时可选,未传时由 CLI runtime 根据 `--sheet-id` / `--sheet-name` 当前在工作簿中的 index 自动派生。但在 `+batch-update` 内不可省须显式传——batch 中途无法发起结构查询自动派生 |
### `+sheet-copy`
@@ -138,7 +143,7 @@ _系统`--dry-run`_
| --- | --- | --- | --- |
| `--title` | string | required | 新 spreadsheet 标题 |
| `--folder-token` | string | optional | 目标文件夹 token省略时放在云空间根目录 |
| `--values` | string + File + Stdin简单 JSON | optional | untyped 初始数据,一个 JSON 二维数组(表头并入第一行):`[["列A","列B"],["alice",95]]`;值原样写入、类型由飞书自动识别,走与 --sheets 相同的分批 `+cells-set`;配 --styles 控制格式/颜色/合并/行列尺寸 |
| `--values` | string + File + Stdin简单 JSON | optional | untyped 初始数据,一个 JSON 二维数组(表头并入第一行):`[["列A","列B"],["alice",95]]`;值原样写入、类型由飞书自动识别(日期 / 数字会落成文本,需类型保真改用 --sheets,走与 --sheets 相同的分批 `+cells-set`;配 --styles 控制格式/颜色/合并/行列尺寸 |
| `--sheets` | string + File + Stdin复合 JSON | optional | 建表后写入的 typed 表格协议 JSON同 +table-put顶层 `{"sheets":[...]}`,每个数组项是一张子表 `{name, start_cell?, mode?, header?, allow_overwrite?, columns:["colA","colB",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}` —— `name` 与外层 `sheets` 数组都不可省。Agents 用 `scripts/sheets_df.py``df_to_sheet(df, name)` 把 DataFrame 转成一项再包 `{"sheets":[...]}`。与 --values 互斥;新表默认子表复用为第一个子表,日期/数字类型保真。 |
| `--styles` | string + File + Stdin复合 JSON | optional | 建表时同时写入的视觉处理操作 JSON顶层 `{styles:[...]}`,每项对应一个目标子表、含 `name`,并至少给 `cell_styles` / `row_sizes` / `col_sizes` / `cell_merges` 之一。`cell_styles` 用 A1 单元格 range + 扁平样式字段(字段同 +cells-set-style含 number_format / 颜色 / 对齐 / border_stylesrow/col sizes 用行/列范围 + type/sizemerges 用单元格 range + 可选 merge_type。与 --sheets 搭配时 styles 数组长度/顺序/name 必须与 --sheets.sheets 对应;与 --values 搭配时只给一个 styles 项(其 name 忽略)。完整 cell_styles 字段结构跑 `+workbook-create --print-schema --flag-name styles`。 |
@@ -184,7 +189,7 @@ _一个或多个子表的 typed 数据,每个数组元素写入一张子表;
**数组项**(类型 object
- `cell_merges` (array<object>?) — 单元格合并操作数组range 使用 A1 单元格范围merge_type 默认 all each: { merge_type?: enum, range: string }
- `cell_styles` (array<object>?) — 单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐 each: { background_color?: string, border_styles?: object, font_color?: string, font_line?: enum, font_size?: number, …共 12 项 }
- `cell_styles` (array<object>?) — 单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐 each: { background_color?: string, border_styles?: object, font_color?: string, font_family?: string, font_line?: enum, …共 13 项 }
- `col_sizes` (array<object>?) — 列宽操作数组range 使用列范围如 A:Ctype 为 pixel/standardpixel 需要 size each: { range: string, size?: number, type: enum }
- `name` (string) — 子表名
- `row_sizes` (array<object>?) — 行高操作数组range 使用行范围如 1:3type 为 pixel/standard/autopixel 需要 size each: { range: string, size?: number, type: enum }
@@ -195,7 +200,13 @@ _一个或多个子表的 typed 数据,每个数组元素写入一张子表;
### `+workbook-info`
输出契约:返回 `sheets[]`,每个含 `sheet_id` / `title`(工作表显示名;旧 payload 用 `sheet_name`,读取时优先取 `title`、缺失再回退 `sheet_name`/ `row_count` / `column_count` / `index` / `is_hidden`,以及计数字段 `merged_cells_count` / `chart_count` / `pivot_table_count` / `float_image_count`(无 `frozen_*` 字段,冻结信息请用 `+sheet-info` 读取)。是操作飞书表格的第一步——任何后续 sheet 级动作都需要先拿这里的 sheet_id。
输出契约:返回 `sheets[]`,每个含 `sheet_id` / `title`(工作表显示名;旧 payload 用 `sheet_name`,读取时优先取 `title`、缺失再回退 `sheet_name`/ `index` / `resource_type` / `row_count` / `column_count` / `is_hidden`,以及计数字段 `merged_cells_count` / `chart_count` / `pivot_table_count` / `float_image_count`(无 `frozen_*` 字段,冻结信息请用 `+sheet-info` 读取)。是操作飞书表格的第一步——任何后续 sheet 级动作都需要先拿这里的 sheet_id。
> **子表类型 `resource_type`**`sheet`(普通网格子表)/ `bitable`(内嵌的多维表格子表)/ `#UNSUPPORTED_TYPE`(其它暂不支持的嵌入子表)。
> - 网格类操作(读写单元格 / 区域 / 样式 / CSV / 筛选 / 透视 / 图表等)**仅适用于 `sheet`**。对 `bitable` / `#UNSUPPORTED_TYPE` 子表执行网格操作会被直接拒绝并返回明确报错,不再静默出错。
> - 要操作 `bitable` 子表里的数据:该子表条目会附带 `bitable_app_token` + `bitable_table_id` 两个字段,直接用多维表格命令操作,例如 `lark-cli base +record-list --base-token <bitable_app_token> --table-id <bitable_table_id>`(记录增删改查、字段、视图等整套 `lark-cli base` 命令均可用)。不要走 sheets 网格命令。
> - `bitable` / `#UNSUPPORTED_TYPE` 子表条目**只含** `sheet_id` / `sheet_name` / `index` / `resource_type`bitable 另加上述两个 token以及 `is_hidden` / `tab_color`**不输出** `row_count` / `column_count` / `merged_cells_count` / `chart_count` / `pivot_table_count` / `float_image_count` / `frozen_*` 等网格指标(对非网格子表无意义)。
> - tab 管理类操作(`+sheet-rename` / `+sheet-move` / `+sheet-delete` / `+sheet-hide` 等)对任意 `resource_type` 的子表都合法,不受此限制。
### `+workbook-create`
@@ -341,8 +352,17 @@ lark-cli sheets +sheet-create --url "https://example.feishu.cn/sheets/shtXXX" \
--title "汇总" --index 0
```
新建一张**多维表格bitable子表**:加 `--type bitable`(默认 `sheet`,即普通电子表格子表)。
```bash
lark-cli sheets +sheet-create --url "https://example.feishu.cn/sheets/shtXXX" \
--title "任务表" --type bitable
```
> 💡 `+sheet-create` 只建一张**空子表**。要在已有工作簿里建子表并一步写入 typed 数据和/或样式,用 `+table-put`payload 里命名的子表缺则自动新建)配合它的 `--sheets` / `--styles`,省掉先建表再 `+cells-set` / `+cells-set-style` 的二次往返。
> 💡 `--type bitable` 只建一张**空的多维表格子表**(默认表 + 网格视图 + 默认字段)。它的内容编辑(字段、记录、视图)走 `lark-cli base`:先用 `+workbook-info` 拿到该子表的 `bitable_app_token` + `bitable_table_id`,再用 `lark-cli base +record-list` / `+record-create` 等操作sheets 侧的网格类命令(`+cells-get` / `+cells-set` 等)对 bitable 子表会被拒。
### `+sheet-delete`
> ⚠️ 工作表删除不可逆;先 `--dry-run` 看输出 sheet_id + title 确认是要删的那张。

View File

@@ -5,7 +5,7 @@
1. **明确写入边界**:写入前必须能回答"目标 range 的起止行列号是多少?是否落在用户授权范围内?"。除用户明示要修改的区域外,禁止扩张到原数据列以外或新建 Sheet。
2. **完整性断言**:批量写入前先把"预期写入条数"硬编码到代码里(如要填 106 条翻译 → `expected = 106`),写完后回读断言 `actual == expected`。少于预期就继续写,禁止交付半成品。
3. **回读抽样校验**:写完关键值 / 公式后,用 `+csv-get``+cells-get` 重新读取写入区域,至少抽样 3-5 个代表性单元格(首 / 中 / 末),核对值与预期一致(与本地脚本计算的预期值对照)。公式特定的"先验证模板再 --copy-to-range / 修完再读回"细则见下方相关章节。
4. **护原表 · 派生产物落点(写排名 / 标记 / 汇总 / 改写列时易丢数据)**:派生结果一律写到**真实末列 +1 的全新空列**或新建子表,**禁止复用任何已有原数据列**——哪怕该列看起来"空",也要先 `+csv-get` 回读确认整列无原始数据再写。三条铁律:① 不把新公式 / 新值写进原数据列(典型反例:把新算的排名公式写进了原本存放另一份原始数据的列,整列原始数据被覆盖丢失);② 不改写、不合并原表头字段名(典型反例:把几个独立表头字段合并成一列,原字段名丢失);③ 慎用 `--allow-overwrite`:它一旦让写入区盖到相邻原始列 / 行就是不可逆数据丢失,加它之前必须用 `+sheet-info` / `+csv-get` 核清目标 range 不含任何原始数据。
4. **护原表 · 派生产物落点(写排名 / 标记 / 汇总 / 改写列时易丢数据)**:派生结果一律写到**真实末列 +1 的全新空列**或新建子表,**禁止复用任何已有原数据列**——哪怕该列看起来"空",也要先 `+csv-get` 回读确认整列无原始数据再写。三条准则:① 不把新公式 / 新值写进原数据列(典型反例:把新算的排名公式写进了原本存放另一份原始数据的列,整列原始数据被覆盖丢失);② 不改写、不合并原表头字段名(典型反例:把几个独立表头字段合并成一列,原字段名丢失);③ 慎用 `--allow-overwrite`:它一旦让写入区盖到相邻原始列 / 行就是不可逆数据丢失,加它之前必须用 `+sheet-info` / `+csv-get` 核清目标 range 不含任何原始数据。
## 新增列 / 新增行的样式继承(防止视觉风格不一致)
@@ -13,7 +13,7 @@
**完整继承清单**(写新列 / 新行时 cells 数组必须同时携带):
1. `cell_styles.font_size` / `cell_styles.font_weight` / `cell_styles.font_color` / `cell_styles.font_style`(字号 / 粗细 / 颜色 / 斜体等)
1. `cell_styles.font_family` / `cell_styles.font_size` / `cell_styles.font_weight` / `cell_styles.font_color` / `cell_styles.font_style`字体名称 / 字号 / 粗细 / 颜色 / 斜体等)
2. `cell_styles.horizontal_alignment` / `cell_styles.vertical_alignment`H-Align / V-Align—— 漏继承会导致新列对齐与原列不一致(常见)
3. `cell_styles.number_format`(小数位 / 千分位 / 百分比 / 日期格式)—— 漏继承会导致同列数值格式混乱
4. `cell_styles.background_color`(背景色)
@@ -43,6 +43,8 @@
**典型反例**:长数字列(如审批单号、流水号)未设 `number_format`,飞书显示为 `1.23E+15`,用户复制出来已经丢失精度。
> **数字还是文本,按"数据本质是量值还是标识符"二选一 —— 不看当下要不要计算**:金额 / 百分比 / 比率 / 计数 / 度量这类**本质是量值**的数据,一律以**数字类型**写入(百分比存小数 `0.54` 配 `number_format:"0%"`**不要**设 `@` 文本格式。**这与"用户当下是否要排序 / 求和"无关**——数据类型由数据本质决定、不由当下用途决定:表格数据几乎总会被后续排序 / 图表 / 二次计算复用,`"54%"` 文本与数值列混排本就破坏一致性,且数字 + `number_format` 显示效果与文本**完全相同**,没有任何理由选文本。**最常见的误判就是"这只是 leaderboard / 报表 / 看板展示,又不用算,写成 `54%` 字符串就行"——这是错的,展示用途不改变"百分比是数值"的事实。**`+table-put` 用 `dtypes` 声明 `int64` / `float64`;版式 `+table-put` 装不下时用 `+cells-set` 传数字 + `number_format`;都别在本地拼成带 `$` / `%` 的字符串走 `+csv-put`。)反过来,编号 `001`、规格 `3-1`、身份证 / 电话 / 单据号等**本质是标识符 / 标签**、要原样保留不被飞书自动解释的内容(否则 `001`→`1`、`3-1`→日期、长号→科学计数),才以**字符串类型**写入(`dtypes` 设 `object`)并把 `number_format` 设为 `"@"`(文本格式),字面保真。
## 使用场景
写入。向飞书表格的单元格区域写入值、公式、样式、批注、图片或下拉,也可批量写入 CSV / DataFrame。本 reference 覆盖 6 个 shortcut按数据来源 + 内容形态选:
@@ -54,19 +56,20 @@
| 写入含样式、批注、图片、数据校验等任意富写入 | `+cells-set` | 唯一支持完整富字段的 shortcut公式 `+csv-put` 也能写) |
| 只改已有 cell 的样式,不动 value/formula | `+cells-set-style` | 拍平 10 个样式字段为独立 flag不触发不必要的值写入 |
| 单 cell 嵌入图片 | `+cells-set-image` | 比 `+cells-set` 参数更简短 |
| 大量纯值 + 需要表头样式/边框 | 先用 `+csv-put` 写值,再用 `+cells-set-style` 补样式 | 分工配合,入参最短 |
| 在**已有区域**局部补表头样式/边框 | 先用 `+csv-put` 写值,再用 `+cells-set-style` 补样式 | 分工配合,入参最短 |
| **新建子表 / 整表成套美化**(哪怕全是纯文本) | `+table-put --sheets … --styles …` 一步带值 + 全套样式(区域底色 / 边框 / 列宽 / 行高 / 合并payload 里不存在的 sheet 名自动建子表) | `--styles` 与列是否 typed 无关,纯文本同样适用;比「写值 + 多次刷样式」少好几次调用 |
**优先级**:常规批量写入(纯值或公式)优先 `+csv-put`(最短入参,直接传 CSV 文本);含样式/批注/图片才用 `+cells-set`。⚠️ 这里"纯值"特指**已是文本、无需保留数值语义**的内容;只要列里是金额 / 百分比 / 日期 / 计数等有数值语义的数据,应优先 `+table-put`(用 typed 协议的 `dtypes` 声明列类型 + `formats` 设展示格式),而不是 `+csv-put`
⚠️ `+csv-put` 可写值或公式:以 `=` 开头的单元格会被当作公式计算(读回时 `formula` 字段保留、`value` 为计算结果)。**公式内部含逗号 / 引号 / 换行时必须按 RFC 4180 转义**——含逗号的字段整格用双引号包裹、字段内部的引号再翻倍:如 `=COUNTIF(D5:D22,"及格")` 必须写成 `"=COUNTIF(D5:D22,""及格"")"`(外层双引号包裹整格,内部 `"及格"` 的引号翻倍成 `""及格""`)。漏转义会被 CSV 解析器按逗号拆列、整块写入区域错位(如本该 `G4:H6` 错成 `G4:K4`),详见下方 `+csv-put` 示例。**因此含逗号 / 引号 / 换行的公式优先改用 `+cells-set`JSON 二维数组)写入——`cells[r][c].formula` 字段直接放公式串,零 CSV 转义负担,从根上避免拆列错位**`+table-put` 的 typed 协议只接受 `columns / data / dtypes / formats` 四件套、没有 `formula` 字段,公式写入只能走 `+cells-set` / `+csv-put`)。此外 `+csv-put` **不会**携带样式/批注/图片,也无法把 `=` 开头的内容当字面量文本写入;需要样式/批注/图片用 `+cells-set`(或"写值 + 补样式"两步法)。
⚠️ **别把本该是数值的列格式化成字符串用 `+csv-put` 写入**:金额 / 百分比 / 市值 / 计数等列,若在本地拼成带 `$` / `%` / 千分位的字符串(如 `"$1,234.50"` / `"+30.5%"`)再 `+csv-put` 灌进去,单元格会变成**文本**——丢失排序 / 求和 / 图表 / 透视能力,且与 `number` 列混排时无法参与计算。正解是 `+table-put --sheets` 完整 payload外层一定要带 `{"sheets":[...]}`、列名走 `columns`、二维数据走 `data`、列 pandas dtype 走 `dtypes`、列展示格式走 `formats`),数值列用 pandas dtype 串如 `dtypes:{"价格":"float64"}`(百分比同样存小数 `0.305`),并配 `formats:{"价格":"$#,##0.00","完成率":"0.0%"}` 做展示格式,**显示效果完全相同、数值无损**。判断信号:**当你准备把一个数字 format 成字符串再写时,几乎总该用 `+table-put` 而非 `+csv-put`**
⚠️ **别把本该是数值的列格式化成字符串用 `+csv-put` 写入**:金额 / 百分比 / 市值 / 计数等列(判定看数据本质、不看当下用途,见上方"数字还是文本"——"只是报表展示不用算"不改变它是数值),若在本地拼成带 `$` / `%` / 千分位的字符串(如 `"$1,234.50"` / `"+30.5%"`)再 `+csv-put` 灌进去,单元格会变成**文本**——丢失排序 / 求和 / 图表 / 透视能力,且与 `number` 列混排时无法参与计算。正解是 `+table-put --sheets` 完整 payload外层一定要带 `{"sheets":[...]}`、列名走 `columns`、二维数据走 `data`、列 pandas dtype 走 `dtypes`、列展示格式走 `formats`),数值列用 pandas dtype 串如 `dtypes:{"价格":"float64"}`(百分比同样存小数 `0.305`),并配 `formats:{"价格":"$#,##0.00","完成率":"0.0%"}` 做展示格式,**显示效果完全相同、数值无损**。判断信号:**当你准备把一个数字 format 成字符串再写时,就是走错了路——数值一律以数字本身写入 + 用 `number_format` 控制显示,绝不拼成 `"40%"` / `"$1,234"` 字符串。** 两条正路按版式二选一:① 常规二维表用 `+table-put`(如上,`dtypes` + `formats`);② **版式 `+table-put` 装不下时**(多级 / 合并表头、每方向占两列的宽表,或同一批要混写公式 / 批注 / 富文本)改用 `+cells-set`——`value` 直接传数字(百分比传小数 `0.4`、金额传 `1234.5`),再配 `cell_styles.number_format``"0%"` / `"#,##0.00"`)控制显示,同样显示 `40%` 而数值无损。**版式复杂只是决定用哪个命令,绝不是退回拼 `"40%"` 字符串灌 `+csv-put` 的理由**——`+cells-set` 能承载任意数据类型 + 任意版式
⚠️ 大数据回写走"`+csv-get``--range` 行窗口分批读到本地 + 本地脚本处理 + `+csv-put` 分批回写"。
## `+cells-set` 写入要点(常用模式 / 公式 / 样式)
> 以下是用 `+cells-set`(及 `+cells-set-style`)做富写入时的常用模式与铁律;选哪个 shortcut 见上方「使用场景」。
> 以下是用 `+cells-set`(及 `+cells-set-style`)做富写入时的常用模式与准则;选哪个 shortcut 见上方「使用场景」。
`+cells-set` 为一块区域设置值 / 公式 / 批注 / 样式,也支持 `rich_text``type: "embed-image"` 嵌入单元格图片。**关键:`cells` 二维数组的行列维度必须与 `range`(闭区间)严格一致,否则触发 `InvalidCellRangeError`**——维度计算示例见文末 `## Schemas``--cells`
@@ -80,6 +83,8 @@
- 用户说”这列 / 整列 / 这行 / 首行 / 向下复制”时,**必须**使用模板单元格 + `--copy-to-range`
- 多区域写入相同格式/公式结构时,优先写一个模板,再用 `--copy-to-range` 复制到所有目标区域
⚠️ **模板 `--range` 从数据行起算、别把表头圈进去**`--copy-to-range` 会把 `--range` 模板按目标区尺寸周期性平铺,模板里若含了表头行,表头会每隔几行重复铺进数据区。整列填充时模板只取一格数据样式(如 `H2`),不要取成 `H1:H2`
⚠️ **逐行写入公式是常见低效写法**:对每一行单独调用 `+cells-set` 写公式(如 26 次)既慢又易错,且不会自动平移公式引用。正确做法是 1 次模板写入 + 1 次 `--copy-to-range`(公式引用自动平移)。
💡 **写入公式前先按迁移规则改写**:如果公式来自 Excel 或包含数组场景,先读取并遵循 `lark-sheets-formula-translation` 的规则完成改写,再把最终公式写入 `formula` 字段。
@@ -227,7 +232,7 @@ lark-cli sheets +dropdown-set \
> ⚠️ **`--source-range` 必须带 sheet 前缀**(即使跟 `--range` 同 sheet。注意一个坑回读这种 listFromRange 下拉单元格时,`data_validation.range` 看起来不带 sheet 前缀(形如 `$T$1:$T$3`),如果要把读出来的 range 反过来写回 `--source-range`**必须自己重新补上 sheet 前缀**,否则会被拒。
>
> ⚠️ **sheet 前缀里的表名一律「裸写」,不要加引号**——这条对所有带 sheet 前缀的 range 入参通用(`--source-range`、`+cells-batch-set-style` / `+cells-batch-clear` / `+dropdown-update` 的 `--ranges` 等)。即使表名含点或空格(如 `2025.9`、`一月份 `也直接写 `2025.9!A1`**不要**按电子表格习惯写成 `'2025.9'!A1`——引号会被当成表名一部分,导致 `sheet "'2025.9'" not found`
> ⚠️ **`--ranges` 类批量 flag 的 sheet 前缀必须「裸写」**——`+cells-batch-set-style` / `+cells-batch-clear` / `+dropdown-update` / `+dropdown-delete` 的 `--ranges` 解析器不接受引号:表名含点或空格(如 `2025.9`、`一月份`)也直接写 `2025.9!A1`写成 `'2025.9'!A1` 会被当成表名一部分、报 `sheet not found`。**但 `--source-range`、透视表 `--source`、`--range` 走 A1 标准**sheet 名带单引号(如 `'Sheet1'!A1:B2`)是标准写法、裸写也接受,回读统一返回带引号形式——别把 `--ranges` 的裸写要求套到这些 flag 上
`+dropdown-update`(多 range 批量更新)的所有 flag 语义与 `+dropdown-set` 完全一致;只是目标 `--ranges` 由单值变成 JSON 数组(每项带 sheet 前缀),同一份选项 + 配色应用到所有 range。
@@ -265,6 +270,7 @@ _公共四件套 · 系统:`--dry-run`_
| `--range` | string | required | 目标范围A1 格式,如 `A1:B2` |
| `--background-color` | string | optional | 背景颜色(十六进制,如 `#ffffff` |
| `--font-color` | string | optional | 字体颜色(十六进制,如 `#000000` |
| `--font-family` | string | optional | 字体名称(如 `Arial``微软雅黑` |
| `--font-size` | float64 | optional | 字体大小px10、12、14 |
| `--font-style` | string | optional | 字体样式(可选值:`normal` / `italic` |
| `--font-weight` | string | optional | 字重(可选值:`normal` / `bold` |
@@ -330,7 +336,7 @@ _【维度】行列数必须与 range 完全一致:'A1:C2'→[[_,_,_],[_,_,_]]
- `value` (oneOf?) — 静态单元格值(文本、数字、布尔)
- `formula` (string?) — 以 '=' 开头的单元格公式(例如:'=SUM(A1:A10)'
- `note` (string?) — 单元格批注/备注
- `cell_styles` (object?) — 单元格样式属性,包括字体、颜色、对齐方式和数字格式 { font_color?: string, font_size?: number, font_weight?: enum, font_style?: enum, font_line?: enum, …共 10 项 }
- `cell_styles` (object?) — 单元格样式属性,包括字体、颜色、对齐方式和数字格式 { font_color?: string, font_family?: string, font_size?: number, font_weight?: enum, font_style?: enum, …共 11 项 }
- `border_styles` (object?) — 单元格边框配置,含 top/bottom/left/right 四个方向,每个方向的结构相同(见 top { top?: object, bottom?: object, left?: object, right?: object }
- `rich_text` (array<object>?) — 富文本内容 each: { type: enum, text: string, style?: object, link?: string, mention_token?: string, …共 17 项 }
- `multiple_values` (array<object>?) — 多值内容,用于支持多选的列表验证单元格 each: { value: oneOf, format?: string }
@@ -373,7 +379,7 @@ _一个或多个子表的 typed 数据,每个数组元素写入一张子表;
**数组项**(类型 object
- `cell_merges` (array<object>?) — 单元格合并操作数组range 使用 A1 单元格范围merge_type 默认 all each: { merge_type?: enum, range: string }
- `cell_styles` (array<object>?) — 单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐 each: { background_color?: string, border_styles?: object, font_color?: string, font_line?: enum, font_size?: number, …共 12 项 }
- `cell_styles` (array<object>?) — 单元格样式操作数组;每项用 A1 单元格 range 指定范围,字段名与 +cells-set-style 对齐 each: { background_color?: string, border_styles?: object, font_color?: string, font_family?: string, font_line?: enum, …共 13 项 }
- `col_sizes` (array<object>?) — 列宽操作数组range 使用列范围如 A:Ctype 为 pixel/standardpixel 需要 size each: { range: string, size?: number, type: enum }
- `name` (string) — 子表名
- `row_sizes` (array<object>?) — 行高操作数组range 使用行范围如 1:3type 为 pixel/standard/autopixel 需要 size each: { range: string, size?: number, type: enum }