From b6da950be3bb2b7dc80d045ec80d77cd5c8d28ad Mon Sep 17 00:00:00 2001 From: xiongyuanwen-byted Date: Mon, 22 Jun 2026 18:03:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(sheets):=20styles=20=E6=8E=A5=E5=8F=97=20h?= =?UTF-8?q?align/valign=20=E7=AD=89=E5=AF=B9=E9=BD=90=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=88=AB=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把模型常幻觉的 horizontal_align / halign / vertical_align / valign 映射到 规范字段 horizontal_alignment / vertical_alignment,覆盖 --styles 与 typed --cells;与规范字段冲突时报错而非静默择一。同步 lark-sheets skill 文档补 对齐字段说明 + --print-schema --flag-name styles 提示。 --- shortcuts/sheets/data/flag-defs.json | 2 +- shortcuts/sheets/helpers.go | 67 ++++++ .../sheets/lark_sheet_style_alias_test.go | 199 ++++++++++++++++++ shortcuts/sheets/lark_sheet_workbook.go | 3 + shortcuts/sheets/lark_sheet_write_cells.go | 3 + .../references/lark-sheets-workbook.md | 6 +- .../references/lark-sheets-write-cells.md | 4 +- 7 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 shortcuts/sheets/lark_sheet_style_alias_test.go diff --git a/shortcuts/sheets/data/flag-defs.json b/shortcuts/sheets/data/flag-defs.json index 88644a3ce..b56fe8b98 100644 --- a/shortcuts/sheets/data/flag-defs.json +++ b/shortcuts/sheets/data/flag-defs.json @@ -2095,7 +2095,7 @@ "kind": "own", "type": "string", "required": "optional", - "desc": "Visual operations applied after the typed write, as JSON: top-level `{styles:[...]}`. Each item corresponds to one written 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. The styles array length/order/name must match the written sheets: with --sheets, match --sheets.sheets; with --dataframe (single sheet named Sheet1), pass exactly one styles item with name `Sheet1`.", + "desc": "Visual operations applied after the typed write, as JSON: top-level `{styles:[...]}`. Each item corresponds to one written 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. The styles array length/order/name must match the written sheets: with --sheets, match --sheets.sheets; with --dataframe (single sheet named Sheet1), pass exactly one styles item with name `Sheet1`. Run `+table-put --print-schema --flag-name styles` for the full cell_styles field schema.", "input": [ "file", "stdin" diff --git a/shortcuts/sheets/helpers.go b/shortcuts/sheets/helpers.go index 0a09524e1..202bd81b6 100644 --- a/shortcuts/sheets/helpers.go +++ b/shortcuts/sheets/helpers.go @@ -10,6 +10,7 @@ package sheets import ( "context" "encoding/json" + "fmt" "strings" "github.com/larksuite/cli/errs" @@ -335,6 +336,72 @@ func buildCellStyleFromFlags(runtime flagView) map[string]interface{} { return style } +// cellStyleAliases maps shorthand cell_styles field names that models commonly +// hallucinate (Excel / openpyxl / CSS conventions) onto the canonical field +// names the backend expects. Only the unambiguous alignment shorthands are +// aliased — they are the high-frequency miss; ambiguous guesses (e.g. "color", +// "bg_color", "text_align") are intentionally left out so a wrong guess still +// surfaces as an error rather than being silently reinterpreted. +var cellStyleAliases = []struct{ alias, canonical string }{ + {"horizontal_align", "horizontal_alignment"}, + {"halign", "horizontal_alignment"}, + {"vertical_align", "vertical_alignment"}, + {"valign", "vertical_alignment"}, +} + +// normalizeCellStyleAliases renames known shorthand keys in a single +// cell_styles map to their canonical equivalents, in place, so a model that +// writes e.g. "horizontal_align" instead of "horizontal_alignment" still +// applies the style instead of hitting an "unsupported field" error (--styles) +// or having the field silently dropped by the backend (typed --cells). If both +// the shorthand and its canonical key are present it returns a validation error +// rather than picking one. path labels the map for the error message. +func normalizeCellStyleAliases(style map[string]interface{}, path string) error { + if len(style) == 0 { + return nil + } + for _, a := range cellStyleAliases { + v, ok := style[a.alias] + if !ok { + continue + } + if _, exists := style[a.canonical]; exists { + return common.ValidationErrorf("%s.%s conflicts with %s; pass only %s", path, a.alias, a.canonical, a.canonical) + } + style[a.canonical] = v + delete(style, a.alias) + } + return nil +} + +// normalizeTypedCellsStyleAliases walks a typed --cells 2D array and applies +// normalizeCellStyleAliases to every cell's inline cell_styles object, so the +// alignment shorthands are accepted on +cells-set the same as on --styles. +// Structure is checked leniently to match the pass-through contract: any +// element that isn't the expected shape is skipped, not rejected. +func normalizeTypedCellsStyleAliases(cells []interface{}, path string) error { + for r, rowRaw := range cells { + row, ok := rowRaw.([]interface{}) + if !ok { + continue + } + for c, cellRaw := range row { + cell, ok := cellRaw.(map[string]interface{}) + if !ok { + continue + } + st, ok := cell["cell_styles"].(map[string]interface{}) + if !ok { + continue + } + if err := normalizeCellStyleAliases(st, fmt.Sprintf("%s[%d][%d].cell_styles", path, r, c)); err != nil { + return err + } + } + } + return nil +} + // borderStylesFromFlag parses --border-styles as a JSON object (top/bottom/ // left/right with style sub-objects). Returns nil when the flag is empty. func borderStylesFromFlag(runtime flagView) (map[string]interface{}, error) { diff --git a/shortcuts/sheets/lark_sheet_style_alias_test.go b/shortcuts/sheets/lark_sheet_style_alias_test.go new file mode 100644 index 000000000..a9f473ee0 --- /dev/null +++ b/shortcuts/sheets/lark_sheet_style_alias_test.go @@ -0,0 +1,199 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "encoding/json" + "strings" + "testing" +) + +// TestNormalizeCellStyleAliases pins the shorthand → canonical renaming for a +// single cell_styles map: the alignment shorthands models commonly hallucinate +// are rewritten in place, values are preserved, and a shorthand colliding with +// its canonical key is a hard error rather than a silent pick. +func TestNormalizeCellStyleAliases(t *testing.T) { + t.Parallel() + + t.Run("renames *_align shorthands, keeps values and other fields", func(t *testing.T) { + t.Parallel() + style := map[string]interface{}{ + "horizontal_align": "center", + "vertical_align": "middle", + "font_weight": "bold", + } + if err := normalizeCellStyleAliases(style, "x"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if style["horizontal_alignment"] != "center" || style["vertical_alignment"] != "middle" { + t.Errorf("alignment not renamed: %#v", style) + } + if _, ok := style["horizontal_align"]; ok { + t.Errorf("shorthand horizontal_align should be removed: %#v", style) + } + if _, ok := style["vertical_align"]; ok { + t.Errorf("shorthand vertical_align should be removed: %#v", style) + } + if style["font_weight"] != "bold" { + t.Errorf("unrelated field font_weight dropped: %#v", style) + } + }) + + t.Run("renames halign/valign shorthands", func(t *testing.T) { + t.Parallel() + style := map[string]interface{}{"halign": "left", "valign": "top"} + if err := normalizeCellStyleAliases(style, "x"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if style["horizontal_alignment"] != "left" || style["vertical_alignment"] != "top" { + t.Errorf("halign/valign not renamed: %#v", style) + } + }) + + t.Run("shorthand colliding with canonical is an error", func(t *testing.T) { + t.Parallel() + style := map[string]interface{}{ + "horizontal_align": "center", + "horizontal_alignment": "left", + } + err := normalizeCellStyleAliases(style, "cell_styles[0]") + if err == nil { + t.Fatalf("expected conflict error, got nil") + } + if !strings.Contains(err.Error(), "conflicts with horizontal_alignment") { + t.Errorf("error should name the conflict: %v", err) + } + }) + + t.Run("no shorthand leaves the map untouched", func(t *testing.T) { + t.Parallel() + style := map[string]interface{}{"font_weight": "bold", "horizontal_alignment": "center"} + if err := normalizeCellStyleAliases(style, "x"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(style) != 2 || style["font_weight"] != "bold" || style["horizontal_alignment"] != "center" { + t.Errorf("map should be unchanged: %#v", style) + } + }) + + t.Run("empty map is a no-op", func(t *testing.T) { + t.Parallel() + if err := normalizeCellStyleAliases(map[string]interface{}{}, "x"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +// TestNormalizeTypedCellsStyleAliases pins the 2D --cells walk: every cell's +// inline cell_styles is normalized, malformed shapes are skipped (matching the +// pass-through contract) rather than rejected, and a conflict propagates. +func TestNormalizeTypedCellsStyleAliases(t *testing.T) { + t.Parallel() + + t.Run("normalizes inline cell_styles across the grid", func(t *testing.T) { + t.Parallel() + cells := []interface{}{ + []interface{}{ + map[string]interface{}{ + "value": "x", + "cell_styles": map[string]interface{}{"horizontal_align": "center"}, + }, + map[string]interface{}{"value": "y"}, // no cell_styles → untouched + }, + } + if err := normalizeTypedCellsStyleAliases(cells, "--cells"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + row := cells[0].([]interface{}) + st := row[0].(map[string]interface{})["cell_styles"].(map[string]interface{}) + if st["horizontal_alignment"] != "center" { + t.Errorf("cell_styles not normalized: %#v", st) + } + if _, ok := st["horizontal_align"]; ok { + t.Errorf("shorthand should be removed: %#v", st) + } + }) + + t.Run("malformed shapes are skipped, not rejected", func(t *testing.T) { + t.Parallel() + cells := []interface{}{ + "not-a-row", + []interface{}{ + "not-a-cell", + map[string]interface{}{"cell_styles": "not-a-map"}, + }, + } + if err := normalizeTypedCellsStyleAliases(cells, "--cells"); err != nil { + t.Fatalf("lenient walk should not error on odd shapes: %v", err) + } + }) + + t.Run("conflict inside a cell propagates", func(t *testing.T) { + t.Parallel() + cells := []interface{}{ + []interface{}{ + map[string]interface{}{ + "cell_styles": map[string]interface{}{ + "valign": "top", + "vertical_alignment": "middle", + }, + }, + }, + } + err := normalizeTypedCellsStyleAliases(cells, "--cells") + if err == nil { + t.Fatalf("expected conflict error, got nil") + } + if !strings.Contains(err.Error(), "--cells[0][0].cell_styles") { + t.Errorf("error should carry the cell path: %v", err) + } + }) +} + +// TestCellsSet_StyleAliasesNormalized is the end-to-end guard for +cells-set: +// a typed --cells payload using alignment shorthands reaches set_cell_range +// with canonical field names so the backend doesn't silently drop them. +func TestCellsSet_StyleAliasesNormalized(t *testing.T) { + t.Parallel() + cells := `[[{"value":"Header","cell_styles":{"horizontal_align":"center","vertical_align":"middle","font_weight":"bold"}}]]` + body := parseDryRunBody(t, CellsSet, []string{ + "--url", testURL, "--sheet-id", testSheetID, + "--range", "A1", "--cells", cells, + }) + input := decodeToolInput(t, body, "set_cell_range") + raw, _ := json.Marshal(input["cells"]) + s := string(raw) + if !strings.Contains(s, `"horizontal_alignment":"center"`) || !strings.Contains(s, `"vertical_alignment":"middle"`) { + t.Errorf("alignment shorthands not normalized in cells: %s", s) + } + if strings.Contains(s, `"horizontal_align":`) || strings.Contains(s, `"vertical_align":`) { + t.Errorf("shorthand keys leaked through to backend payload: %s", s) + } +} + +// TestWorkbookCreate_StyleAliasesNormalized is the end-to-end guard for +// +workbook-create --styles: alignment shorthands in a cell_styles op are +// accepted (no "unsupported style field" error) and emitted as canonical +// field names merged into the fill cells. +func TestWorkbookCreate_StyleAliasesNormalized(t *testing.T) { + t.Parallel() + calls := parseDryRunAPI(t, WorkbookCreate, []string{ + "--title", "Sales", + "--values", `[["Name","Score"],["alice",95]]`, + "--styles", `{"styles":[{"name":"Sheet1","cell_styles":[{"range":"A1:B2","horizontal_align":"center","vertical_align":"middle"}]}]}`, + }) + if len(calls) != 2 { + t.Fatalf("api calls = %d, want 2 (create + fill)", len(calls)) + } + body, _ := calls[1].(map[string]interface{})["body"].(map[string]interface{}) + input := decodeToolInput(t, body, "set_cell_range") + raw, _ := json.Marshal(input["cells"]) + s := string(raw) + if c := strings.Count(s, `"horizontal_alignment":"center"`); c != 4 { + t.Errorf("horizontal_alignment occurrences = %d, want 4 in 2x2 range; cells=%s", c, s) + } + if strings.Contains(s, `"horizontal_align":`) || strings.Contains(s, `"vertical_align":`) { + t.Errorf("shorthand keys leaked through after normalization: %s", s) + } +} diff --git a/shortcuts/sheets/lark_sheet_workbook.go b/shortcuts/sheets/lark_sheet_workbook.go index d2e26c9ef..1757ed23f 100644 --- a/shortcuts/sheets/lark_sheet_workbook.go +++ b/shortcuts/sheets/lark_sheet_workbook.go @@ -1192,6 +1192,9 @@ func normalizeWorkbookCreateStyleObject(in map[string]interface{}, path string) if len(in) == 0 { return nil, nil } + if err := normalizeCellStyleAliases(in, path); err != nil { + return nil, err + } out := map[string]interface{}{} cellStyle := map[string]interface{}{} for k, v := range in { diff --git a/shortcuts/sheets/lark_sheet_write_cells.go b/shortcuts/sheets/lark_sheet_write_cells.go index 1da305758..62f27f1e5 100644 --- a/shortcuts/sheets/lark_sheet_write_cells.go +++ b/shortcuts/sheets/lark_sheet_write_cells.go @@ -88,6 +88,9 @@ func cellsSetInput(runtime flagView, token, sheetID, sheetName string) (map[stri if err != nil { return nil, err } + if err := normalizeTypedCellsStyleAliases(cells, "--cells"); err != nil { + return nil, err + } input := map[string]interface{}{ "excel_id": token, "range": strings.TrimSpace(runtime.Str("range")), diff --git a/skills/lark-sheets/references/lark-sheets-workbook.md b/skills/lark-sheets/references/lark-sheets-workbook.md index 7db3df264..8da91dc33 100644 --- a/skills/lark-sheets/references/lark-sheets-workbook.md +++ b/skills/lark-sheets/references/lark-sheets-workbook.md @@ -140,7 +140,7 @@ _系统:`--dry-run`_ | `--folder-token` | string | optional | 目标文件夹 token;省略时放在云空间根目录 | | `--values` | string + File + Stdin(简单 JSON) | optional | untyped 初始数据,一个 JSON 二维数组(表头并入第一行):`[["列A","列B"],["alice",95]]`;值原样写入、类型由飞书自动识别,走与 --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, ...}}`。Agents 通常用 `{**json.loads(df.to_json(orient="split")), "dtypes": df.dtypes.astype(str).to_dict()}` 一行构造。与 --values、--dataframe 互斥;新表默认子表复用为第一个子表,日期/数字类型保真。 | -| `--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_styles);row/col sizes 用行/列范围 + type/size;merges 用单元格 range + 可选 merge_type。与 --sheets 搭配时 styles 数组长度/顺序/name 必须与 --sheets.sheets 对应;与 --values 搭配时只给一个 styles 项(其 name 忽略)。 | +| `--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_styles);row/col sizes 用行/列范围 + type/size;merges 用单元格 range + 可选 merge_type。与 --sheets 搭配时 styles 数组长度/顺序/name 必须与 --sheets.sheets 对应;与 --values 搭配时只给一个 styles 项(其 name 忽略)。完整 cell_styles 字段结构跑 `+workbook-create --print-schema --flag-name styles`。 | | `--dataframe` | string | optional | 单 sheet 类型保真表格的二进制入口,从一个 Arrow IPC 文件(Feather v2,pandas `df.to_feather()` 直接写出)读入,与 --values / --sheets 互斥。用 `@` 传文件或 `-` 读二进制 stdin(同其他输入 flag 的约定)。Arrow 字节按原样读 —— 不做 TrimSpace / BOM strip,IPC magic 字节完整保留(区别于文本类输入 flag)。列类型从 Arrow schema 推导;每列的 `number_format` 可写在 Arrow Field metadata 里。建表后写入默认子表(`Sheet1` —— 直接复用,不残留空 Sheet1)。要多子表或换落点,请改用 `--sheets`。 | ### `+workbook-export` @@ -231,7 +231,7 @@ python prepare.py | lark-cli sheets +workbook-create --title "交易" --datafram `--styles` 可在建表写入时同时写视觉处理。它和 `--sheets` 一样只有一种外层写法:顶层对象里放 `styles` 数组;数组每项对应一个子表,含 `name`,并按能力拆成四类可选数组: -- `cell_styles`:像 `+cells-set-style`,用 A1 单元格 `range` 加扁平样式字段(`font_weight` / `background_color` / `number_format` 等)和可选 `border_styles`;这些样式会随内容在同一次写入里一并应用。 +- `cell_styles`:像 `+cells-set-style`,用 A1 单元格 `range` 加扁平样式字段(`font_weight` / `background_color` / `horizontal_alignment` / `vertical_alignment` / `number_format` 等)和可选 `border_styles`;这些样式会随内容在同一次写入里一并应用。完整字段跑 `+workbook-create --print-schema --flag-name styles`。 - `cell_merges`:用 A1 单元格 `range` 设置合并,`merge_type` 默认为 `all`,可选 `rows` / `columns`。 - `row_sizes`:用行范围(如 `1:3`)设置行高,`type` 为 `pixel` / `standard` / `auto`;`pixel` 需要 `size`。 - `col_sizes`:用列范围(如 `A:C`)设置列宽,`type` 为 `pixel` / `standard`;`pixel` 需要 `size`。 @@ -245,7 +245,7 @@ lark-cli sheets +workbook-create --title "销售" \ --styles '{ "styles":[ {"name":"Sheet1","cell_styles":[ - {"range":"A1:B1","font_weight":"bold","background_color":"#f5f5f5"}, + {"range":"A1:B1","font_weight":"bold","background_color":"#f5f5f5","horizontal_alignment":"center","vertical_alignment":"middle"}, {"range":"B2:B3","number_format":"#,##0"} ]} ] diff --git a/skills/lark-sheets/references/lark-sheets-write-cells.md b/skills/lark-sheets/references/lark-sheets-write-cells.md index 81fb677e6..1c777088c 100644 --- a/skills/lark-sheets/references/lark-sheets-write-cells.md +++ b/skills/lark-sheets/references/lark-sheets-write-cells.md @@ -317,7 +317,7 @@ _公共:URL/token(无 sheet 定位) · 系统:`--dry-run`_ | --- | --- | --- | --- | | `--sheets` | string + File + Stdin(复合 JSON) | xor | Typed 表格协议(pandas-DataFrame-shaped)JSON,与 `--dataframe` 互斥:顶层 sheets 数组,每项 `{name, start_cell?, mode?, header?, allow_overwrite?, columns:["colA","colB",...], data:[[...]], dtypes?:{colA:pandasDtype, ...}, formats?:{colA:numberFormat, ...}}`。Agents 通常用 `{**json.loads(df.to_json(orient="split")), "dtypes": df.dtypes.astype(str).to_dict()}` 一行构造。`dtypes` 值是 pandas dtype 字符串(`int64`、`float64`、`Int64`、`bool`、`boolean`、`datetime64[ns]`、`object`、...),CLI 端映射成内部 string/number/date/bool —— 省略 `dtypes` 时该列按文本写入(适合原始 CSV-shaped 数据)。`formats[col]` 是 Excel number_format 字符串(如 `#,##0.00`、`0.0%`、`yyyy-mm`);缺省时 date 列用 `yyyy-mm-dd`,string 列用文本格式 `@`。 | | `--dataframe` | string | xor | 单 sheet 类型保真表格的二进制入口,从一个 Arrow IPC 文件(即 Feather v2,pandas `df.to_feather()` 直接写出)读入,与 `--sheets` 互斥。用 `@` 传文件或 `-` 读二进制 stdin(同其他输入 flag 的约定)。Arrow 字节按原样读 —— 不做 TrimSpace / BOM strip,IPC magic 字节完整保留(区别于文本类输入 flag)。列类型从 Arrow schema 推导(int*/uint*/float* → number,date32/date64/timestamp → date,utf8/large_utf8 → string,bool → bool);每列的 `number_format` 可写在 Arrow Field metadata 里(`pa.field("price", pa.float64(), metadata={b"number_format": b"$#,##0.00"})`)。子表走默认落点:名为 `Sheet1`(缺则新建),从 A1 起覆盖写并带表头。要换子表名 / 起始位置 / 写入方式,或要写多子表,请改用 `--sheets`。 | -| `--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_styles);row/col sizes 用行/列范围 + type/size;merges 用单元格 range + 可选 merge_type。styles 数组的长度/顺序/name 必须与被写入的子表对应:配 --sheets 时与 --sheets.sheets 对应;配 --dataframe(单子表,名为 Sheet1)时只给一个 name 为 `Sheet1` 的 styles 项。 | +| `--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_styles);row/col sizes 用行/列范围 + type/size;merges 用单元格 range + 可选 merge_type。styles 数组的长度/顺序/name 必须与被写入的子表对应:配 --sheets 时与 --sheets.sheets 对应;配 --dataframe(单子表,名为 Sheet1)时只给一个 name 为 `Sheet1` 的 styles 项。完整 cell_styles 字段结构跑 `+table-put --print-schema --flag-name styles`。 | ## Schemas @@ -592,7 +592,7 @@ subprocess.run(["lark-cli","sheets","+table-put","--url",URL,"--dataframe","-"], lark-cli sheets +table-put --url "<表URL>" \ --sheets '{"sheets":[{"name":"明细","columns":["日期","金额"],"dtypes":{"日期":"datetime64[ns]","金额":"float64"},"formats":{"金额":"#,##0.00"},"data":[["2024-01-15",1234.5]]}]}' \ --styles '{"styles":[{"name":"明细", - "cell_styles":[{"range":"A1:B1","font_weight":"bold","background_color":"#f5f5f5"}], + "cell_styles":[{"range":"A1:B1","font_weight":"bold","background_color":"#f5f5f5","horizontal_alignment":"center"}], "cell_merges":[{"range":"A1:B1"}], "col_sizes":[{"range":"A:B","type":"pixel","size":120}]}]}' ```