mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
7 Commits
feat/get-c
...
feat/chart
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c2e3950e | ||
|
|
080fb57ad0 | ||
|
|
87aa303ca1 | ||
|
|
c8f57c659e | ||
|
|
e81e42029f | ||
|
|
e303da4b5e | ||
|
|
f6bbd86303 |
@@ -223,6 +223,24 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) {
|
||||
args: []string{"--sheet-id", "sh1", "--chart-id", "c1"},
|
||||
subInput: `{"sheet-id":"sh1","chart-id":"c1"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+chart-create-basic",
|
||||
sc: ChartCreateBasic,
|
||||
args: []string{"--sheet-id", "sh1", "--chart-type", "column", "--data-range", "A1:C10", "--title", "Sales", "--data-labels", "value", "--anchor-cell", "F2"},
|
||||
subInput: `{"sheet-id":"sh1","chart-type":"column","data-range":"A1:C10","title":"Sales","data-labels":"value","anchor-cell":"F2"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+chart-config-update",
|
||||
sc: ChartConfigUpdate,
|
||||
args: []string{"--sheet-id", "sh1", "--chart-id", "c1", "--title", "Updated", "--data-labels", "category", "--data-label-position", "top"},
|
||||
subInput: `{"sheet-id":"sh1","chart-id":"c1","title":"Updated","data-labels":"category","data-label-position":"top"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+chart-data-update",
|
||||
sc: ChartDataUpdate,
|
||||
args: []string{"--sheet-id", "sh1", "--chart-id", "c1", "--data-range", "'Sheet1'!A1:M6", "--data-direction", "column", "--dim1-index", "1", "--dim2-indexes", "4,8"},
|
||||
subInput: `{"sheet-id":"sh1","chart-id":"c1","data-range":"'Sheet1'!A1:M6","data-direction":"column","dim1-index":1,"dim2-indexes":"4,8"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+pivot-create",
|
||||
sc: PivotCreate,
|
||||
@@ -424,6 +442,22 @@ func TestBatchOp_ErrorEquivalence(t *testing.T) {
|
||||
subInput: `{}`,
|
||||
wantContains: "specify at least one of --sheet-id or --sheet-name",
|
||||
},
|
||||
{
|
||||
name: "+chart-data-update invalid dim1 index",
|
||||
shortcut: ChartDataUpdate,
|
||||
args: []string{"--sheet-id", "sh1", "--chart-id", "c1", "--data-range", "A1:C4", "--dim1-index", "0"},
|
||||
subShortcut: "+chart-data-update",
|
||||
subInput: `{"sheet-id":"sh1","chart-id":"c1","data-range":"A1:C4","dim1-index":0}`,
|
||||
wantContains: "--dim1-index must be a positive 1-based index",
|
||||
},
|
||||
{
|
||||
name: "+chart-data-update dim1 and dim2 conflict",
|
||||
shortcut: ChartDataUpdate,
|
||||
args: []string{"--sheet-id", "sh1", "--chart-id", "c1", "--data-range", "A1:C4", "--dim1-index", "2", "--dim2-indexes", "2,3"},
|
||||
subShortcut: "+chart-data-update",
|
||||
subInput: `{"sheet-id":"sh1","chart-id":"c1","data-range":"A1:C4","dim1-index":2,"dim2-indexes":"2,3"}`,
|
||||
wantContains: "--dim2-indexes must not contain the dim1 index 2",
|
||||
},
|
||||
{
|
||||
name: "+float-image-create both image-token and image-uri",
|
||||
shortcut: FloatImageCreate,
|
||||
@@ -662,6 +696,18 @@ func TestBatchOp_RejectsBadSubOpInput(t *testing.T) {
|
||||
`{"sheet-id":"sh1","properties":{"title":"T"}}`,
|
||||
"--chart-id is required",
|
||||
},
|
||||
{
|
||||
"+chart-data-update missing --chart-id",
|
||||
"+chart-data-update",
|
||||
`{"sheet-id":"sh1","data-range":"A1:C4"}`,
|
||||
"--chart-id is required",
|
||||
},
|
||||
{
|
||||
"+chart-data-update missing --data-range",
|
||||
"+chart-data-update",
|
||||
`{"sheet-id":"sh1","chart-id":"c1"}`,
|
||||
"--data-range is required",
|
||||
},
|
||||
{
|
||||
"+filter-create missing --range",
|
||||
"+filter-create",
|
||||
|
||||
@@ -168,9 +168,12 @@ var batchOpDispatch = map[string]batchOpMapping{
|
||||
}},
|
||||
|
||||
// ─── 对象族 CRUD (manage_*_object, operation 区分) ─────────────
|
||||
"+chart-create": {"manage_chart_object", objCreateTranslate(chartSpec)},
|
||||
"+chart-update": {"manage_chart_object", objUpdateTranslate(chartSpec)},
|
||||
"+chart-delete": {"manage_chart_object", objDeleteTranslate(chartSpec)},
|
||||
"+chart-create": {"manage_chart_object", objCreateTranslate(chartSpec)},
|
||||
"+chart-update": {"manage_chart_object", objUpdateTranslate(chartSpec)},
|
||||
"+chart-delete": {"manage_chart_object", objDeleteTranslate(chartSpec)},
|
||||
"+chart-create-basic": {"manage_chart_object", chartCreateBasicInput},
|
||||
"+chart-config-update": {"manage_chart_object", chartConfigUpdateInput},
|
||||
"+chart-data-update": {"manage_chart_object", chartDataUpdateInput},
|
||||
|
||||
"+pivot-create": {"manage_pivot_table_object", objCreateTranslate(pivotSpec)},
|
||||
"+pivot-update": {"manage_pivot_table_object", objUpdateTranslate(pivotSpec)},
|
||||
|
||||
@@ -3249,6 +3249,559 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"+chart-create-basic": {
|
||||
"risk": "write",
|
||||
"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",
|
||||
"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": "chart-type",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Chart type",
|
||||
"enum": [
|
||||
"column",
|
||||
"bar",
|
||||
"line",
|
||||
"area",
|
||||
"pie",
|
||||
"scatter",
|
||||
"combo",
|
||||
"radar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "data-range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "One contiguous A1 range including headers, or comma-separated same-sheet ranges; aligned non-overlapping ranges stay independent, otherwise they merge to the smallest enclosing rectangle"
|
||||
},
|
||||
{
|
||||
"name": "data-direction",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data series direction; column uses the first column as categories, row uses the first row",
|
||||
"default": "column",
|
||||
"enum": [
|
||||
"column",
|
||||
"row"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Chart title"
|
||||
},
|
||||
{
|
||||
"name": "subtitle",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Chart subtitle"
|
||||
},
|
||||
{
|
||||
"name": "legend-position",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Legend position; hidden removes the legend",
|
||||
"enum": [
|
||||
"top",
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "x-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "X-axis title"
|
||||
},
|
||||
{
|
||||
"name": "y-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Left Y-axis title"
|
||||
},
|
||||
{
|
||||
"name": "secondary-y-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Right Y-axis title"
|
||||
},
|
||||
{
|
||||
"name": "x-axis-label-angle",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "X-axis label angle",
|
||||
"enum": [
|
||||
"-90",
|
||||
"-45",
|
||||
"0",
|
||||
"45",
|
||||
"90"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "y-axis-label-angle",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Left Y-axis label angle",
|
||||
"enum": [
|
||||
"-90",
|
||||
"-45",
|
||||
"0",
|
||||
"45",
|
||||
"90"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "data-labels",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data label content; none removes labels; category_percentage is normalized to value_percentage",
|
||||
"enum": [
|
||||
"none",
|
||||
"value",
|
||||
"percentage",
|
||||
"value_percentage",
|
||||
"category_percentage",
|
||||
"category",
|
||||
"series"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "data-label-position",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data label position",
|
||||
"enum": [
|
||||
"auto",
|
||||
"top",
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"center",
|
||||
"inside",
|
||||
"outside"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stack",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Stacking mode",
|
||||
"enum": [
|
||||
"none",
|
||||
"normal",
|
||||
"percent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stacked",
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Compatibility alias for --stack normal",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "smooth",
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Use smooth curves; accepts both --smooth=false and --smooth false"
|
||||
},
|
||||
{
|
||||
"name": "color-palette",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Preset chart-level color palette; mutually exclusive with --colors",
|
||||
"enum": [
|
||||
"brandColorSeries@v2",
|
||||
"rainbowColorSeries@v2",
|
||||
"complementaryColorSeries@v2",
|
||||
"converseColorSeries@v2",
|
||||
"primaryColorSeries@v2",
|
||||
"singleColorSeries-B-@v2",
|
||||
"singleColorSeries-W-@v2",
|
||||
"singleColorSeries-G-@v2",
|
||||
"singleColorSeries-Y-@v2",
|
||||
"singleColorSeries-O-@v2",
|
||||
"singleColorSeries-R-@v2",
|
||||
"singleColorSeries-D-@v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "colors",
|
||||
"kind": "own",
|
||||
"type": "string_slice",
|
||||
"required": "optional",
|
||||
"desc": "Custom chart-level series colors as a comma-separated list of at least two hex colors; mutually exclusive with --color-palette"
|
||||
},
|
||||
{
|
||||
"name": "anchor-cell",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Optional chart anchor cell such as F2; defaults to the right of the data range"
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Optional chart width; must be paired with --height"
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Optional chart height; must be paired with --width"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Print the request template; no side effects"
|
||||
}
|
||||
]
|
||||
},
|
||||
"+chart-config-update": {
|
||||
"risk": "write",
|
||||
"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",
|
||||
"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": "chart-id",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Target chart reference_id"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Chart title"
|
||||
},
|
||||
{
|
||||
"name": "subtitle",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Chart subtitle"
|
||||
},
|
||||
{
|
||||
"name": "legend-position",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Legend position; hidden removes the legend",
|
||||
"enum": [
|
||||
"top",
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "x-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "X-axis title"
|
||||
},
|
||||
{
|
||||
"name": "y-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Left Y-axis title"
|
||||
},
|
||||
{
|
||||
"name": "secondary-y-axis-title",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Right Y-axis title"
|
||||
},
|
||||
{
|
||||
"name": "x-axis-label-angle",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "X-axis label angle",
|
||||
"enum": [
|
||||
"-90",
|
||||
"-45",
|
||||
"0",
|
||||
"45",
|
||||
"90"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "y-axis-label-angle",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "Left Y-axis label angle",
|
||||
"enum": [
|
||||
"-90",
|
||||
"-45",
|
||||
"0",
|
||||
"45",
|
||||
"90"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "data-labels",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data label content; none removes labels; category_percentage is normalized to value_percentage",
|
||||
"enum": [
|
||||
"none",
|
||||
"value",
|
||||
"percentage",
|
||||
"value_percentage",
|
||||
"category_percentage",
|
||||
"category",
|
||||
"series"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "data-label-position",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data label position",
|
||||
"enum": [
|
||||
"auto",
|
||||
"top",
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"center",
|
||||
"inside",
|
||||
"outside"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stack",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Stacking mode",
|
||||
"enum": [
|
||||
"none",
|
||||
"normal",
|
||||
"percent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stacked",
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Compatibility alias for --stack normal",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"name": "smooth",
|
||||
"kind": "own",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Use smooth curves; accepts both --smooth=false and --smooth false"
|
||||
},
|
||||
{
|
||||
"name": "color-palette",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Preset chart-level color palette; mutually exclusive with --colors",
|
||||
"enum": [
|
||||
"brandColorSeries@v2",
|
||||
"rainbowColorSeries@v2",
|
||||
"complementaryColorSeries@v2",
|
||||
"converseColorSeries@v2",
|
||||
"primaryColorSeries@v2",
|
||||
"singleColorSeries-B-@v2",
|
||||
"singleColorSeries-W-@v2",
|
||||
"singleColorSeries-G-@v2",
|
||||
"singleColorSeries-Y-@v2",
|
||||
"singleColorSeries-O-@v2",
|
||||
"singleColorSeries-R-@v2",
|
||||
"singleColorSeries-D-@v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "colors",
|
||||
"kind": "own",
|
||||
"type": "string_slice",
|
||||
"required": "optional",
|
||||
"desc": "Custom chart-level series colors as a comma-separated list of at least two hex colors; mutually exclusive with --color-palette"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Print the request template; no side effects"
|
||||
}
|
||||
]
|
||||
},
|
||||
"+chart-data-update": {
|
||||
"risk": "write",
|
||||
"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",
|
||||
"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": "chart-id",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Target chart reference_id"
|
||||
},
|
||||
{
|
||||
"name": "data-range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "New data range including headers; accepts comma-separated same-sheet ranges and normalizes misaligned or overlapping ranges"
|
||||
},
|
||||
{
|
||||
"name": "data-direction",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Data series direction; defaults to the existing chart direction when omitted",
|
||||
"enum": [
|
||||
"column",
|
||||
"row"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dim1-index",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "optional",
|
||||
"desc": "1-based category/X-axis dimension index within the data range; defaults to the first dimension"
|
||||
},
|
||||
{
|
||||
"name": "dim2-indexes",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Comma-separated 1-based value/Y-axis series indexes within the data range; defaults to all dimensions except dim1"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
"type": "bool",
|
||||
"required": "optional",
|
||||
"desc": "Print the request template; no side effects"
|
||||
}
|
||||
]
|
||||
},
|
||||
"+chart-create": {
|
||||
"risk": "write",
|
||||
"flags": [
|
||||
@@ -3343,7 +3896,7 @@
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Full or sufficiently complete chart config JSON (read back with `+chart-list` first, then patch)",
|
||||
"desc": "Chart config patch JSON; send changed fields only by default; omitted fields are preserved, objects merge recursively, and arrays replace as a whole",
|
||||
"input": [
|
||||
"file",
|
||||
"stdin"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,32 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "end-revision", Kind: "own", Type: "int", Required: "optional", Desc: "End version (CS revision); defaults to the latest revision. Gap (end-start+1) must be <= 20", Default: "-1"},
|
||||
},
|
||||
},
|
||||
"+chart-config-update": {
|
||||
Risk: "write",
|
||||
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", 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: "chart-id", Kind: "own", Type: "string", Required: "required", Desc: "Target chart reference_id"},
|
||||
{Name: "title", Kind: "own", Type: "string", Required: "optional", Desc: "Chart title"},
|
||||
{Name: "subtitle", Kind: "own", Type: "string", Required: "optional", Desc: "Chart subtitle"},
|
||||
{Name: "legend-position", Kind: "own", Type: "string", Required: "optional", Desc: "Legend position; hidden removes the legend", Enum: []string{"top", "bottom", "left", "right", "hidden"}},
|
||||
{Name: "x-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "X-axis title"},
|
||||
{Name: "y-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "Left Y-axis title"},
|
||||
{Name: "secondary-y-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "Right Y-axis title"},
|
||||
{Name: "x-axis-label-angle", Kind: "own", Type: "int", Required: "optional", Desc: "X-axis label angle", Enum: []string{"-90", "-45", "0", "45", "90"}},
|
||||
{Name: "y-axis-label-angle", Kind: "own", Type: "int", Required: "optional", Desc: "Left Y-axis label angle", Enum: []string{"-90", "-45", "0", "45", "90"}},
|
||||
{Name: "data-labels", Kind: "own", Type: "string", Required: "optional", Desc: "Data label content; none removes labels; category_percentage is normalized to value_percentage", Enum: []string{"none", "value", "percentage", "value_percentage", "category_percentage", "category", "series"}},
|
||||
{Name: "data-label-position", Kind: "own", Type: "string", Required: "optional", Desc: "Data label position", Enum: []string{"auto", "top", "bottom", "left", "right", "center", "inside", "outside"}},
|
||||
{Name: "stack", Kind: "own", Type: "string", Required: "optional", Desc: "Stacking mode", Enum: []string{"none", "normal", "percent"}},
|
||||
{Name: "stacked", Kind: "own", Type: "bool", Required: "optional", Desc: "Compatibility alias for --stack normal", Hidden: true},
|
||||
{Name: "smooth", Kind: "own", Type: "bool", Required: "optional", Desc: "Use smooth curves; accepts both --smooth=false and --smooth false"},
|
||||
{Name: "color-palette", Kind: "own", Type: "string", Required: "optional", Desc: "Preset chart-level color palette; mutually exclusive with --colors", Enum: []string{"brandColorSeries@v2", "rainbowColorSeries@v2", "complementaryColorSeries@v2", "converseColorSeries@v2", "primaryColorSeries@v2", "singleColorSeries-B-@v2", "singleColorSeries-W-@v2", "singleColorSeries-G-@v2", "singleColorSeries-Y-@v2", "singleColorSeries-O-@v2", "singleColorSeries-R-@v2", "singleColorSeries-D-@v2"}},
|
||||
{Name: "colors", Kind: "own", Type: "string_slice", Required: "optional", Desc: "Custom chart-level series colors as a comma-separated list of at least two hex colors; mutually exclusive with --color-palette"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request template; no side effects"},
|
||||
},
|
||||
},
|
||||
"+chart-create": {
|
||||
Risk: "write",
|
||||
Flags: []flagDef{
|
||||
@@ -211,6 +237,52 @@ var flagDefs = map[string]commandDef{
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request template; no side effects"},
|
||||
},
|
||||
},
|
||||
"+chart-create-basic": {
|
||||
Risk: "write",
|
||||
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", 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: "chart-type", Kind: "own", Type: "string", Required: "required", Desc: "Chart type", Enum: []string{"column", "bar", "line", "area", "pie", "scatter", "combo", "radar"}},
|
||||
{Name: "data-range", Kind: "own", Type: "string", Required: "required", Desc: "One contiguous A1 range including headers, or comma-separated same-sheet ranges; aligned non-overlapping ranges stay independent, otherwise they merge to the smallest enclosing rectangle"},
|
||||
{Name: "data-direction", Kind: "own", Type: "string", Required: "optional", Desc: "Data series direction; column uses the first column as categories, row uses the first row", Default: "column", Enum: []string{"column", "row"}},
|
||||
{Name: "title", Kind: "own", Type: "string", Required: "optional", Desc: "Chart title"},
|
||||
{Name: "subtitle", Kind: "own", Type: "string", Required: "optional", Desc: "Chart subtitle"},
|
||||
{Name: "legend-position", Kind: "own", Type: "string", Required: "optional", Desc: "Legend position; hidden removes the legend", Enum: []string{"top", "bottom", "left", "right", "hidden"}},
|
||||
{Name: "x-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "X-axis title"},
|
||||
{Name: "y-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "Left Y-axis title"},
|
||||
{Name: "secondary-y-axis-title", Kind: "own", Type: "string", Required: "optional", Desc: "Right Y-axis title"},
|
||||
{Name: "x-axis-label-angle", Kind: "own", Type: "int", Required: "optional", Desc: "X-axis label angle", Enum: []string{"-90", "-45", "0", "45", "90"}},
|
||||
{Name: "y-axis-label-angle", Kind: "own", Type: "int", Required: "optional", Desc: "Left Y-axis label angle", Enum: []string{"-90", "-45", "0", "45", "90"}},
|
||||
{Name: "data-labels", Kind: "own", Type: "string", Required: "optional", Desc: "Data label content; none removes labels; category_percentage is normalized to value_percentage", Enum: []string{"none", "value", "percentage", "value_percentage", "category_percentage", "category", "series"}},
|
||||
{Name: "data-label-position", Kind: "own", Type: "string", Required: "optional", Desc: "Data label position", Enum: []string{"auto", "top", "bottom", "left", "right", "center", "inside", "outside"}},
|
||||
{Name: "stack", Kind: "own", Type: "string", Required: "optional", Desc: "Stacking mode", Enum: []string{"none", "normal", "percent"}},
|
||||
{Name: "stacked", Kind: "own", Type: "bool", Required: "optional", Desc: "Compatibility alias for --stack normal", Hidden: true},
|
||||
{Name: "smooth", Kind: "own", Type: "bool", Required: "optional", Desc: "Use smooth curves; accepts both --smooth=false and --smooth false"},
|
||||
{Name: "color-palette", Kind: "own", Type: "string", Required: "optional", Desc: "Preset chart-level color palette; mutually exclusive with --colors", Enum: []string{"brandColorSeries@v2", "rainbowColorSeries@v2", "complementaryColorSeries@v2", "converseColorSeries@v2", "primaryColorSeries@v2", "singleColorSeries-B-@v2", "singleColorSeries-W-@v2", "singleColorSeries-G-@v2", "singleColorSeries-Y-@v2", "singleColorSeries-O-@v2", "singleColorSeries-R-@v2", "singleColorSeries-D-@v2"}},
|
||||
{Name: "colors", Kind: "own", Type: "string_slice", Required: "optional", Desc: "Custom chart-level series colors as a comma-separated list of at least two hex colors; mutually exclusive with --color-palette"},
|
||||
{Name: "anchor-cell", Kind: "own", Type: "string", Required: "optional", Desc: "Optional chart anchor cell such as F2; defaults to the right of the data range"},
|
||||
{Name: "width", Kind: "own", Type: "int", Required: "optional", Desc: "Optional chart width; must be paired with --height"},
|
||||
{Name: "height", Kind: "own", Type: "int", Required: "optional", Desc: "Optional chart height; must be paired with --width"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request template; no side effects"},
|
||||
},
|
||||
},
|
||||
"+chart-data-update": {
|
||||
Risk: "write",
|
||||
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", 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: "chart-id", Kind: "own", Type: "string", Required: "required", Desc: "Target chart reference_id"},
|
||||
{Name: "data-range", Kind: "own", Type: "string", Required: "required", Desc: "New data range including headers; accepts comma-separated same-sheet ranges and normalizes misaligned or overlapping ranges"},
|
||||
{Name: "data-direction", Kind: "own", Type: "string", Required: "optional", Desc: "Data series direction; defaults to the existing chart direction when omitted", Enum: []string{"column", "row"}},
|
||||
{Name: "dim1-index", Kind: "own", Type: "int", Required: "optional", Desc: "1-based category/X-axis dimension index within the data range; defaults to the first dimension"},
|
||||
{Name: "dim2-indexes", Kind: "own", Type: "string", Required: "optional", Desc: "Comma-separated 1-based value/Y-axis series indexes within the data range; defaults to all dimensions except dim1"},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional", Desc: "Print the request template; no side effects"},
|
||||
},
|
||||
},
|
||||
"+chart-delete": {
|
||||
Risk: "high-risk-write",
|
||||
Flags: []flagDef{
|
||||
@@ -242,7 +314,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: "chart-id", Kind: "own", Type: "string", Required: "required", Desc: "Target chart reference_id"},
|
||||
{Name: "properties", Kind: "own", Type: "string", Required: "required", Desc: "Full or sufficiently complete chart config JSON (read back with `+chart-list` first, then patch)", Input: []string{"file", "stdin"}},
|
||||
{Name: "properties", Kind: "own", Type: "string", Required: "required", Desc: "Chart config patch JSON; send changed fields only by default; omitted fields are preserved, objects merge recursively, and arrays replace as a whole", Input: []string{"file", "stdin"}},
|
||||
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -159,7 +159,7 @@ func splitSchemaPath(flagName string) (string, []string) {
|
||||
// sliceSchemaByPath walks a decoded JSON Schema along dotted path segments.
|
||||
// Each segment matches a key under "properties"; array levels are descended
|
||||
// implicitly through "items" (an explicit "items" segment also works), and
|
||||
// oneOf branches are searched for the first one carrying the key. A miss
|
||||
// oneOf / anyOf branches are searched for the first one carrying the key. A miss
|
||||
// errors with the keys actually available at that level so the caller can
|
||||
// re-issue the path without a full dump.
|
||||
func sliceSchemaByPath(schema interface{}, flagName string, path []string) (interface{}, error) {
|
||||
@@ -179,7 +179,7 @@ func sliceSchemaByPath(schema interface{}, flagName string, path []string) (inte
|
||||
}
|
||||
|
||||
// schemaChild resolves one path segment against a schema node, descending
|
||||
// through items / oneOf wrappers as needed.
|
||||
// through items / oneOf / anyOf wrappers as needed.
|
||||
func schemaChild(node interface{}, seg string) (interface{}, bool) {
|
||||
for depth := 0; depth < 8; depth++ {
|
||||
m, ok := node.(map[string]interface{})
|
||||
@@ -207,13 +207,20 @@ func schemaChild(node interface{}, seg string) (interface{}, bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if branches, ok := m["anyOf"].([]interface{}); ok {
|
||||
for _, b := range branches {
|
||||
if child, ok := schemaChild(b, seg); ok {
|
||||
return child, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// schemaChildKeys lists the property keys reachable at a schema node (through
|
||||
// items / oneOf wrappers), for the path-miss error.
|
||||
// items / oneOf / anyOf wrappers), for the path-miss error.
|
||||
func schemaChildKeys(node interface{}) []string {
|
||||
seen := map[string]struct{}{}
|
||||
var collect func(n interface{}, depth int)
|
||||
@@ -240,6 +247,11 @@ func schemaChildKeys(node interface{}) []string {
|
||||
collect(b, depth+1)
|
||||
}
|
||||
}
|
||||
if branches, ok := m["anyOf"].([]interface{}); ok {
|
||||
for _, b := range branches {
|
||||
collect(b, depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
collect(node, 0)
|
||||
keys := make([]string, 0, len(seen))
|
||||
|
||||
605
shortcuts/sheets/lark_sheet_chart.go
Normal file
605
shortcuts/sheets/lark_sheet_chart.go
Normal file
@@ -0,0 +1,605 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var chartHexColorPattern = regexp.MustCompile(`^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$`)
|
||||
|
||||
var chartSemanticConfigFlags = []string{
|
||||
"title",
|
||||
"subtitle",
|
||||
"legend-position",
|
||||
"x-axis-title",
|
||||
"y-axis-title",
|
||||
"secondary-y-axis-title",
|
||||
"x-axis-label-angle",
|
||||
"y-axis-label-angle",
|
||||
"data-labels",
|
||||
"data-label-position",
|
||||
"stack",
|
||||
"color-palette",
|
||||
}
|
||||
|
||||
// ChartCreateBasic creates a complete server-side chart snapshot from a chart
|
||||
// type and a rectangular source range. The CLI only forwards semantic input;
|
||||
// it deliberately does not own or duplicate the full chart snapshot template.
|
||||
var ChartCreateBasic = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+chart-create-basic",
|
||||
Description: "Create a basic chart from a chart type and data range; the server builds and validates the full snapshot.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: flagsFor("+chart-create-basic"),
|
||||
PostMount: configureChartSemanticCommand,
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = chartCreateBasicInput(runtime, token, sheetID, sheetName)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input, _ := chartCreateBasicInput(runtime, token, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "manage_chart_object", input)
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetTokenExec(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input, err := chartCreateBasicInput(runtime, token, sheetID, sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "manage_chart_object", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// ChartConfigUpdate updates the common chart settings that repeatedly caused
|
||||
// full-snapshot retries in eval traces. Advanced per-series and marker styling
|
||||
// remains on +chart-update --properties.
|
||||
var ChartConfigUpdate = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+chart-config-update",
|
||||
Description: "Update common chart titles, axes, legend, labels, stacking, smoothing, or chart-level colors without sending a snapshot.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: flagsFor("+chart-config-update"),
|
||||
PostMount: configureChartSemanticCommand,
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = chartConfigUpdateInput(runtime, token, sheetID, sheetName)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input, _ := chartConfigUpdateInput(runtime, token, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "manage_chart_object", input)
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetTokenExec(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input, err := chartConfigUpdateInput(runtime, token, sheetID, sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "manage_chart_object", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// ChartDataUpdate rebinds an existing chart to a new source range. The server
|
||||
// reads the current snapshot, rebuilds its data mapping, and preserves the
|
||||
// chart's layout and visual configuration.
|
||||
var ChartDataUpdate = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+chart-data-update",
|
||||
Description: "Update an existing chart's data range or direction while preserving its layout and visual configuration.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: flagsFor("+chart-data-update"),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = chartDataUpdateInput(runtime, token, sheetID, sheetName)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input, _ := chartDataUpdateInput(runtime, token, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "manage_chart_object", input)
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetTokenExec(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input, err := chartDataUpdateInput(runtime, token, sheetID, sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "manage_chart_object", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func chartCreateBasicInput(rt flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartType := strings.TrimSpace(rt.Str("chart-type"))
|
||||
if chartType == "" {
|
||||
return nil, sheetsValidationForFlag("chart-type", "--chart-type is required")
|
||||
}
|
||||
dataRange := strings.TrimSpace(rt.Str("data-range"))
|
||||
if dataRange == "" {
|
||||
return nil, sheetsValidationForFlag("data-range", "--data-range is required")
|
||||
}
|
||||
direction := rt.Str("data-direction")
|
||||
if direction == "" {
|
||||
direction = "column"
|
||||
}
|
||||
normalizedDataRange, dimensionCount, dataPointCount, err := normalizeBasicChartDataRanges(dataRange, direction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dimensionCount < 2 || dataPointCount < 2 {
|
||||
return nil, sheetsValidationForFlag("data-range", "--data-range must provide at least 2 data points and 2 dimensions")
|
||||
}
|
||||
if chartType == "combo" && dimensionCount < 3 {
|
||||
return nil, sheetsValidationForFlag("data-range", "combo chart requires at least 3 rows or columns along --data-direction")
|
||||
}
|
||||
|
||||
basic := map[string]interface{}{
|
||||
"chart_type": chartType,
|
||||
"data_range": normalizedDataRange,
|
||||
}
|
||||
if rt.Changed("data-direction") {
|
||||
basic["data_direction"] = rt.Str("data-direction")
|
||||
}
|
||||
if err := validateChartColorFlags(rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateChartSemanticEnums(rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addChartSemanticConfig(rt, basic)
|
||||
|
||||
if rt.Changed("anchor-cell") {
|
||||
anchor := strings.TrimSpace(rt.Str("anchor-cell"))
|
||||
_, row, ok := splitCellRef(anchor)
|
||||
if !ok {
|
||||
return nil, sheetsValidationForFlag("anchor-cell", "--anchor-cell must be a single A1 cell such as F2")
|
||||
}
|
||||
colEnd := 0
|
||||
for colEnd < len(anchor) && ((anchor[colEnd] >= 'A' && anchor[colEnd] <= 'Z') || (anchor[colEnd] >= 'a' && anchor[colEnd] <= 'z')) {
|
||||
colEnd++
|
||||
}
|
||||
basic["position"] = map[string]interface{}{"row": row, "col": strings.ToUpper(anchor[:colEnd])}
|
||||
}
|
||||
widthChanged := rt.Changed("width")
|
||||
heightChanged := rt.Changed("height")
|
||||
if widthChanged != heightChanged {
|
||||
return nil, common.ValidationErrorf("--width and --height must be provided together").WithParams(
|
||||
sheetsInvalidParam("width", "must be paired with --height"),
|
||||
sheetsInvalidParam("height", "must be paired with --width"),
|
||||
)
|
||||
}
|
||||
if widthChanged {
|
||||
if rt.Int("width") < 10 || rt.Int("height") < 10 {
|
||||
return nil, common.ValidationErrorf("--width and --height must be at least 10")
|
||||
}
|
||||
basic["size"] = map[string]interface{}{"width": rt.Int("width"), "height": rt.Int("height")}
|
||||
}
|
||||
|
||||
input := map[string]interface{}{"excel_id": token, "operation": "create", "basic_chart": basic}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
if err := validateInputAgainstSchema(rt, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func chartConfigUpdateInput(rt flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartID := strings.TrimSpace(rt.Str("chart-id"))
|
||||
if chartID == "" {
|
||||
return nil, sheetsValidationForFlag("chart-id", "--chart-id is required")
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
if err := validateChartColorFlags(rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateChartSemanticEnums(rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addChartSemanticConfig(rt, updates)
|
||||
if len(updates) == 0 {
|
||||
return nil, common.ValidationErrorf("at least one chart configuration flag is required")
|
||||
}
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "update",
|
||||
"chart_id": chartID,
|
||||
"config_updates": updates,
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
if err := validateInputAgainstSchema(rt, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func chartDataUpdateInput(rt flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartID := strings.TrimSpace(rt.Str("chart-id"))
|
||||
if chartID == "" {
|
||||
return nil, sheetsValidationForFlag("chart-id", "--chart-id is required")
|
||||
}
|
||||
dataRange := strings.TrimSpace(rt.Str("data-range"))
|
||||
if dataRange == "" {
|
||||
return nil, sheetsValidationForFlag("data-range", "--data-range is required")
|
||||
}
|
||||
ranges, err := splitChartDataRanges(dataRange)
|
||||
if err != nil {
|
||||
return nil, sheetsValidationForFlag("data-range", "invalid --data-range %q: %v", dataRange, err)
|
||||
}
|
||||
explicitSheet := ""
|
||||
for _, value := range ranges {
|
||||
item, parseErr := parseChartDataRange(value)
|
||||
if parseErr != nil {
|
||||
return nil, sheetsValidationForFlag("data-range", "invalid --data-range item %q: %v", value, parseErr)
|
||||
}
|
||||
if item.sheet != "" {
|
||||
if explicitSheet != "" && item.sheet != explicitSheet {
|
||||
return nil, sheetsValidationForFlag("data-range", "all --data-range items must belong to the same sheet")
|
||||
}
|
||||
explicitSheet = item.sheet
|
||||
}
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{"data_range": dataRange}
|
||||
if rt.Changed("data-direction") {
|
||||
updates["data_direction"] = rt.Str("data-direction")
|
||||
}
|
||||
dim1Index := 1
|
||||
if rt.Changed("dim1-index") {
|
||||
dim1Index = rt.Int("dim1-index")
|
||||
if dim1Index < 1 {
|
||||
return nil, sheetsValidationForFlag("dim1-index", "--dim1-index must be a positive 1-based index")
|
||||
}
|
||||
updates["dim1_index"] = dim1Index
|
||||
}
|
||||
if rt.Changed("dim2-indexes") {
|
||||
dim2Indexes, parseErr := parseChartDim2Indexes(rt.Str("dim2-indexes"))
|
||||
if parseErr != nil {
|
||||
return nil, sheetsValidationForFlag("dim2-indexes", "%v", parseErr)
|
||||
}
|
||||
for _, index := range dim2Indexes {
|
||||
if index == dim1Index {
|
||||
return nil, sheetsValidationForFlag(
|
||||
"dim2-indexes",
|
||||
"--dim2-indexes must not contain the dim1 index %d",
|
||||
dim1Index,
|
||||
)
|
||||
}
|
||||
}
|
||||
updates["dim2_indexes"] = dim2Indexes
|
||||
}
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "update",
|
||||
"chart_id": chartID,
|
||||
"data_updates": updates,
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
if err := validateInputAgainstSchema(rt, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func parseChartDim2Indexes(raw string) ([]int, error) {
|
||||
parts := strings.Split(raw, ",")
|
||||
indexes := make([]int, 0, len(parts))
|
||||
seen := make(map[int]struct{}, len(parts))
|
||||
for _, part := range parts {
|
||||
value := strings.TrimSpace(part)
|
||||
if value == "" {
|
||||
return nil, common.ValidationErrorf("--dim2-indexes must be a comma-separated list of positive 1-based indexes")
|
||||
}
|
||||
index, err := strconv.Atoi(value)
|
||||
if err != nil || index < 1 {
|
||||
return nil, common.ValidationErrorf("--dim2-indexes must contain only positive 1-based indexes")
|
||||
}
|
||||
if _, exists := seen[index]; exists {
|
||||
return nil, common.ValidationErrorf("--dim2-indexes must not contain duplicate index %d", index)
|
||||
}
|
||||
seen[index] = struct{}{}
|
||||
indexes = append(indexes, index)
|
||||
}
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
type chartDataRange struct {
|
||||
sheet string
|
||||
row, col int
|
||||
rowCount, colCount int
|
||||
}
|
||||
|
||||
func normalizeBasicChartDataRanges(dataRange, direction string) (normalized string, dimensionCount, dataPointCount int, err error) {
|
||||
ranges, err := splitChartDataRanges(dataRange)
|
||||
if err != nil {
|
||||
return "", 0, 0, sheetsValidationForFlag("data-range", "invalid --data-range %q: %v", dataRange, err)
|
||||
}
|
||||
parsed := make([]chartDataRange, 0, len(ranges))
|
||||
for _, value := range ranges {
|
||||
item, parseErr := parseChartDataRange(value)
|
||||
if parseErr != nil {
|
||||
return "", 0, 0, sheetsValidationForFlag("data-range", "invalid --data-range item %q: %v", value, parseErr)
|
||||
}
|
||||
parsed = append(parsed, item)
|
||||
}
|
||||
first := parsed[0]
|
||||
explicitSheet := ""
|
||||
spans := make([][2]int, 0, len(parsed))
|
||||
aligned := true
|
||||
minRow, minCol := first.row, first.col
|
||||
maxRow, maxCol := first.row+first.rowCount, first.col+first.colCount
|
||||
for _, item := range parsed {
|
||||
if item.sheet != "" {
|
||||
if explicitSheet != "" && item.sheet != explicitSheet {
|
||||
return "", 0, 0, sheetsValidationForFlag("data-range", "all --data-range items must belong to the same sheet")
|
||||
}
|
||||
explicitSheet = item.sheet
|
||||
}
|
||||
if direction == "row" {
|
||||
if item.col != first.col || item.colCount != first.colCount {
|
||||
aligned = false
|
||||
}
|
||||
dimensionCount += item.rowCount
|
||||
spans = append(spans, [2]int{item.row, item.row + item.rowCount})
|
||||
} else {
|
||||
if item.row != first.row || item.rowCount != first.rowCount {
|
||||
aligned = false
|
||||
}
|
||||
dimensionCount += item.colCount
|
||||
spans = append(spans, [2]int{item.col, item.col + item.colCount})
|
||||
}
|
||||
minRow = min(minRow, item.row)
|
||||
minCol = min(minCol, item.col)
|
||||
maxRow = max(maxRow, item.row+item.rowCount)
|
||||
maxCol = max(maxCol, item.col+item.colCount)
|
||||
}
|
||||
overlapping := false
|
||||
for i, current := range spans {
|
||||
for j := 0; j < i; j++ {
|
||||
if current[0] < spans[j][1] && spans[j][0] < current[1] {
|
||||
overlapping = true
|
||||
}
|
||||
}
|
||||
}
|
||||
normalized = strings.Join(ranges, ",")
|
||||
if len(ranges) > 1 && (!aligned || overlapping) {
|
||||
prefix := ""
|
||||
if explicitSheet != "" {
|
||||
prefix = explicitSheet + "!"
|
||||
}
|
||||
normalized = prefix + columnIndexToLetter(minCol) + strconv.Itoa(minRow+1) + ":" + columnIndexToLetter(maxCol-1) + strconv.Itoa(maxRow)
|
||||
dimensionCount = maxCol - minCol
|
||||
dataPointCount = maxRow - minRow
|
||||
if direction == "row" {
|
||||
dimensionCount, dataPointCount = dataPointCount, dimensionCount
|
||||
}
|
||||
return normalized, dimensionCount, dataPointCount, nil
|
||||
}
|
||||
if direction == "row" {
|
||||
dataPointCount = first.colCount
|
||||
} else {
|
||||
dataPointCount = first.rowCount
|
||||
}
|
||||
return normalized, dimensionCount, dataPointCount, nil
|
||||
}
|
||||
|
||||
func splitChartDataRanges(value string) ([]string, error) {
|
||||
var ranges []string
|
||||
start := 0
|
||||
inQuote := false
|
||||
for i := 0; i <= len(value); i++ {
|
||||
if i < len(value) && value[i] == '\'' {
|
||||
if inQuote && i+1 < len(value) && value[i+1] == '\'' {
|
||||
i++
|
||||
} else {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
}
|
||||
if i == len(value) || (value[i] == ',' && !inQuote) {
|
||||
part := strings.TrimSpace(value[start:i])
|
||||
if part == "" {
|
||||
return nil, common.ValidationErrorf("range list contains an empty item")
|
||||
}
|
||||
ranges = append(ranges, part)
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if inQuote {
|
||||
return nil, common.ValidationErrorf("unterminated quoted sheet name")
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
func parseChartDataRange(value string) (chartDataRange, error) {
|
||||
item := chartDataRange{}
|
||||
ref := strings.TrimSpace(value)
|
||||
if bang := strings.LastIndex(ref, "!"); bang >= 0 {
|
||||
item.sheet = strings.TrimSpace(ref[:bang])
|
||||
ref = strings.TrimSpace(ref[bang+1:])
|
||||
}
|
||||
parts := strings.SplitN(ref, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return item, common.ValidationErrorf("expected a rectangular A1 range such as A1:C10")
|
||||
}
|
||||
startCol, startRow, startOK := splitCellRef(parts[0])
|
||||
endCol, endRow, endOK := splitCellRef(parts[1])
|
||||
if !startOK || !endOK || endCol < startCol || endRow < startRow {
|
||||
return item, common.ValidationErrorf("expected a rectangular A1 range such as A1:C10")
|
||||
}
|
||||
item.row, item.col = startRow, startCol
|
||||
item.rowCount, item.colCount = endRow-startRow+1, endCol-startCol+1
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func configureChartSemanticCommand(cmd *cobra.Command) {
|
||||
if cmd.Flags().Lookup("stacked") == nil {
|
||||
cmd.Flags().Bool("stacked", false, "compatibility alias for --stack normal")
|
||||
_ = cmd.Flags().MarkHidden("stacked")
|
||||
}
|
||||
originalArgs := cmd.Args
|
||||
cmd.Args = func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 1 && cmd.Flags().Changed("smooth") && (args[0] == "true" || args[0] == "false") {
|
||||
return cmd.Flags().Set("smooth", args[0])
|
||||
}
|
||||
return originalArgs(cmd, args)
|
||||
}
|
||||
cmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
|
||||
message := err.Error()
|
||||
if strings.Contains(message, "unknown flag: --stacked") {
|
||||
return sheetsValidationForFlag("stacked", "--stacked is not supported; use --stack normal (or --stack percent for 100%% stacking)")
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func addChartSemanticConfig(rt flagView, out map[string]interface{}) {
|
||||
for _, flag := range chartSemanticConfigFlags {
|
||||
if !rt.Changed(flag) {
|
||||
continue
|
||||
}
|
||||
key := strings.ReplaceAll(flag, "-", "_")
|
||||
if flag == "x-axis-label-angle" || flag == "y-axis-label-angle" {
|
||||
out[key] = rt.Int(flag)
|
||||
} else if flag == "data-labels" && rt.Str(flag) == "category_percentage" {
|
||||
out[key] = "value_percentage"
|
||||
} else {
|
||||
out[key] = rt.Str(flag)
|
||||
}
|
||||
}
|
||||
if rt.Changed("stacked") {
|
||||
out["stack"] = "normal"
|
||||
}
|
||||
if rt.Changed("smooth") {
|
||||
out["smooth"] = rt.Bool("smooth")
|
||||
}
|
||||
if rt.Changed("colors") {
|
||||
out["colors"] = normalizedChartColors(rt)
|
||||
}
|
||||
}
|
||||
|
||||
func validateChartSemanticEnums(rt flagView) error {
|
||||
if rt.Changed("stack") && rt.Changed("stacked") {
|
||||
return common.ValidationErrorf("--stack and --stacked are mutually exclusive").WithParams(
|
||||
sheetsInvalidParam("stack", "cannot be used with --stacked"),
|
||||
sheetsInvalidParam("stacked", "cannot be used with --stack"),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChartColorFlags(rt flagView) error {
|
||||
if rt.Changed("color-palette") && rt.Changed("colors") {
|
||||
return common.ValidationErrorf("--color-palette and --colors are mutually exclusive").WithParams(
|
||||
sheetsInvalidParam("color-palette", "cannot be used with --colors"),
|
||||
sheetsInvalidParam("colors", "cannot be used with --color-palette"),
|
||||
)
|
||||
}
|
||||
if rt.Changed("colors") {
|
||||
colors := normalizedChartColors(rt)
|
||||
if len(colors) < 2 {
|
||||
return sheetsValidationForFlag("colors", "--colors must contain at least two hex colors")
|
||||
}
|
||||
for _, color := range colors {
|
||||
if !chartHexColorPattern.MatchString(color) {
|
||||
return sheetsValidationForFlag("colors", "--colors contains invalid hex color %q", color)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedChartColors(rt flagView) []string {
|
||||
raw := rt.StrSlice("colors")
|
||||
colors := make([]string, len(raw))
|
||||
for i := range raw {
|
||||
colors[i] = strings.TrimSpace(raw[i])
|
||||
}
|
||||
return colors
|
||||
}
|
||||
346
shortcuts/sheets/lark_sheet_chart_test.go
Normal file
346
shortcuts/sheets/lark_sheet_chart_test.go
Normal file
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestChartCreateBasic_AllTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
types := []string{"column", "bar", "line", "area", "pie", "scatter", "combo", "radar"}
|
||||
for _, chartType := range types {
|
||||
chartType := chartType
|
||||
t.Run(chartType, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rangeValue := "A1:C4"
|
||||
if chartType == "combo" {
|
||||
rangeValue = "A1:D4"
|
||||
}
|
||||
body := parseDryRunBody(t, ChartCreateBasic, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-type", chartType,
|
||||
"--data-range", rangeValue,
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
if input["operation"] != "create" {
|
||||
t.Fatalf("operation = %v, want create", input["operation"])
|
||||
}
|
||||
if _, ok := input["properties"]; ok {
|
||||
t.Fatal("semantic create must not send properties")
|
||||
}
|
||||
basic, _ := input["basic_chart"].(map[string]interface{})
|
||||
if basic["chart_type"] != chartType || basic["data_range"] != rangeValue {
|
||||
t.Fatalf("basic_chart = %#v", basic)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartCreateBasic_ConfigAndPlacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartCreateBasic, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-type", "line",
|
||||
"--data-range", "A1:C4",
|
||||
"--anchor-cell", "f2",
|
||||
"--width", "640",
|
||||
"--height", "360",
|
||||
"--title", "Trend",
|
||||
"--legend-position", "bottom",
|
||||
"--smooth=false",
|
||||
"--data-direction", "row",
|
||||
"--color-palette", "brandColorSeries@v2",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
basic, _ := input["basic_chart"].(map[string]interface{})
|
||||
position, _ := basic["position"].(map[string]interface{})
|
||||
size, _ := basic["size"].(map[string]interface{})
|
||||
if position["col"] != "F" || position["row"] != float64(1) {
|
||||
t.Errorf("position = %#v, want F2 as zero-based row 1", position)
|
||||
}
|
||||
if size["width"] != float64(640) || size["height"] != float64(360) {
|
||||
t.Errorf("size = %#v", size)
|
||||
}
|
||||
if basic["title"] != "Trend" || basic["legend_position"] != "bottom" || basic["smooth"] != false ||
|
||||
basic["data_direction"] != "row" || basic["color_palette"] != "brandColorSeries@v2" {
|
||||
t.Errorf("semantic config = %#v", basic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartCreateBasic_MultipleAlignedRanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
rangeValue := "'Data, 2026'!A1:A10,'Data, 2026'!K1:L10"
|
||||
body := parseDryRunBody(t, ChartCreateBasic, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-type", "line",
|
||||
"--data-range", rangeValue,
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
basic := input["basic_chart"].(map[string]interface{})
|
||||
if basic["data_range"] != rangeValue {
|
||||
t.Fatalf("basic_chart.data_range = %v, want %q", basic["data_range"], rangeValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartCreateBasic_MergesMisalignedOrOverlappingRanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{name: "separated rows", input: "'Sheet1'!A1:M1,'Sheet1'!A3:M3", expected: "'Sheet1'!A1:M3"},
|
||||
{name: "overlapping columns", input: "A1:B10,B1:C10", expected: "A1:C10"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartCreateBasic, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-type", "line",
|
||||
"--data-range", tt.input,
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
basic := input["basic_chart"].(map[string]interface{})
|
||||
if basic["data_range"] != tt.expected {
|
||||
t.Fatalf("basic_chart.data_range = %v, want %q", basic["data_range"], tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartCreateBasic_RejectsCrossSheetRanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := runShortcutCapturingErr(t, ChartCreateBasic, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-type", "line",
|
||||
"--data-range", "'A'!A1:A10,'B'!C1:D10",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected cross-sheet ranges to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartSemanticShortcuts_InBatchUpdate(t *testing.T) {
|
||||
body := parseDryRunBody(t, BatchUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--operations", `[
|
||||
{"shortcut":"+chart-create-basic","input":{"sheet-id":"sh1","chart-type":"column","data-range":"A1:C10","title":"Sales"}},
|
||||
{"shortcut":"+chart-create-basic","input":{"sheet-id":"sh1","chart-type":"line","data-range":"E1:G10","title":"Trend"}}
|
||||
]`,
|
||||
"--yes",
|
||||
})
|
||||
input := decodeToolInput(t, body, "batch_update")
|
||||
ops := input["operations"].([]interface{})
|
||||
if len(ops) != 2 {
|
||||
t.Fatalf("operations len = %d, want 2", len(ops))
|
||||
}
|
||||
for i, op := range ops {
|
||||
item := op.(map[string]interface{})
|
||||
if item["tool_name"] != "manage_chart_object" {
|
||||
t.Fatalf("operations[%d].tool_name = %v", i, item["tool_name"])
|
||||
}
|
||||
chartInput := item["input"].(map[string]interface{})
|
||||
if chartInput["operation"] != "create" {
|
||||
t.Fatalf("operations[%d].input.operation = %v", i, chartInput["operation"])
|
||||
}
|
||||
if _, ok := chartInput["basic_chart"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("operations[%d].input.basic_chart = %#v", i, chartInput["basic_chart"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartConfigUpdate_PartialFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartConfigUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
"--y-axis-title", "Revenue",
|
||||
"--stack", "percent",
|
||||
"--smooth=false",
|
||||
"--colors", "#112233,#445566",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
if input["operation"] != "update" || input["chart_id"] != "chart-1" {
|
||||
t.Fatalf("input = %#v", input)
|
||||
}
|
||||
if _, ok := input["properties"]; ok {
|
||||
t.Fatal("semantic update must not send properties")
|
||||
}
|
||||
updates, _ := input["config_updates"].(map[string]interface{})
|
||||
if updates["y_axis_title"] != "Revenue" || updates["stack"] != "percent" || updates["smooth"] != false {
|
||||
t.Errorf("config_updates = %#v", updates)
|
||||
}
|
||||
colors, _ := updates["colors"].([]interface{})
|
||||
if len(colors) != 2 || colors[0] != "#112233" || colors[1] != "#445566" {
|
||||
t.Errorf("config_updates.colors = %#v", updates["colors"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartConfigUpdate_SpacedSmoothBool(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartConfigUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
"--smooth", "false",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
updates := input["config_updates"].(map[string]interface{})
|
||||
if updates["smooth"] != false {
|
||||
t.Fatalf("config_updates.smooth = %v, want false", updates["smooth"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartSemanticShortcuts_CompatibleAliases(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartConfigUpdate, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--stacked",
|
||||
})
|
||||
updates := decodeToolInput(t, body, "manage_chart_object")["config_updates"].(map[string]interface{})
|
||||
if updates["stack"] != "normal" {
|
||||
t.Fatalf("--stacked normalized stack = %v, want normal", updates["stack"])
|
||||
}
|
||||
body = parseDryRunBody(t, ChartConfigUpdate, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--data-labels", "category_percentage",
|
||||
})
|
||||
updates = decodeToolInput(t, body, "manage_chart_object")["config_updates"].(map[string]interface{})
|
||||
if updates["data_labels"] != "value_percentage" {
|
||||
t.Fatalf("data-labels normalized value = %v, want value_percentage", updates["data_labels"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartSemanticShortcuts_CompatibleAliasesInBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, BatchUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--operations", `[{"shortcut":"+chart-config-update","input":{"sheet_id":"sh1","chart_id":"chart-1","stacked":true,"data_labels":"category_percentage","smooth":false}}]`,
|
||||
"--yes",
|
||||
})
|
||||
input := decodeToolInput(t, body, "batch_update")
|
||||
ops := input["operations"].([]interface{})
|
||||
chartInput := ops[0].(map[string]interface{})["input"].(map[string]interface{})
|
||||
updates := chartInput["config_updates"].(map[string]interface{})
|
||||
if updates["stack"] != "normal" || updates["data_labels"] != "value_percentage" || updates["smooth"] != false {
|
||||
t.Fatalf("batch config_updates = %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartDataUpdate_PreservesSnapshotServerSide(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartDataUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
"--data-range", "'Sheet1'!A1:M6",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
if input["operation"] != "update" || input["chart_id"] != "chart-1" {
|
||||
t.Fatalf("input = %#v", input)
|
||||
}
|
||||
if _, ok := input["properties"]; ok {
|
||||
t.Fatal("semantic data update must not send properties")
|
||||
}
|
||||
updates, _ := input["data_updates"].(map[string]interface{})
|
||||
if updates["data_range"] != "'Sheet1'!A1:M6" {
|
||||
t.Errorf("data_updates = %#v", updates)
|
||||
}
|
||||
if _, ok := updates["data_direction"]; ok {
|
||||
t.Errorf("omitted --data-direction must preserve the server-side direction: %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartDataUpdate_ExplicitDirectionAndMultipleRanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartDataUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
"--data-range", "'Sheet1'!A1:A10,'Sheet1'!K1:L10",
|
||||
"--data-direction", "column",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
updates := input["data_updates"].(map[string]interface{})
|
||||
if updates["data_range"] != "'Sheet1'!A1:A10,'Sheet1'!K1:L10" || updates["data_direction"] != "column" {
|
||||
t.Errorf("data_updates = %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartDataUpdate_ExplicitSeriesIndexes(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, ChartDataUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
"--data-range", "'Sheet1'!A1:M6",
|
||||
"--dim1-index", "1",
|
||||
"--dim2-indexes", "4, 8",
|
||||
})
|
||||
input := decodeToolInput(t, body, "manage_chart_object")
|
||||
updates := input["data_updates"].(map[string]interface{})
|
||||
if updates["dim1_index"] != float64(1) {
|
||||
t.Errorf("data_updates.dim1_index = %#v", updates["dim1_index"])
|
||||
}
|
||||
indexes, _ := updates["dim2_indexes"].([]interface{})
|
||||
if len(indexes) != 2 || indexes[0] != float64(4) || indexes[1] != float64(8) {
|
||||
t.Errorf("data_updates.dim2_indexes = %#v", updates["dim2_indexes"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartSemanticShortcuts_Validation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{name: "unsupported type", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "donut", "--data-range", "A1:C4"}},
|
||||
{name: "invalid semantic enum", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:C4", "--legend-position", "diagonal"}},
|
||||
{name: "range too small", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:A4"}},
|
||||
{name: "combo needs two series", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "combo", "--data-range", "A1:B4"}},
|
||||
{name: "invalid direction", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:C4", "--data-direction", "horizontal"}},
|
||||
{name: "colors need two values", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:C4", "--colors", "#112233"}},
|
||||
{name: "palette and colors are exclusive", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:C4", "--color-palette", "brandColorSeries@v2", "--colors", "#112233,#445566"}},
|
||||
{name: "size must be paired", args: []string{"--url", testURL, "--sheet-id", testSheetID, "--chart-type", "line", "--data-range", "A1:C4", "--width", "640"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, _, err := runShortcutCapturingErr(t, ChartCreateBasic, tt.args)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_, _, err := runShortcutCapturingErr(t, ChartConfigUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--sheet-id", testSheetID,
|
||||
"--chart-id", "chart-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected config update with no changed field to fail")
|
||||
}
|
||||
|
||||
for _, args := range [][]string{
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--data-range", "A1:C4"},
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1"},
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--data-range", "A1:C4", "--data-direction", "horizontal"},
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--data-range", "A1:C4", "--dim1-index", "0"},
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--data-range", "A1:C4", "--dim2-indexes", "2,2"},
|
||||
{"--url", testURL, "--sheet-id", testSheetID, "--chart-id", "chart-1", "--data-range", "A1:C4", "--dim2-indexes", "1,2"},
|
||||
} {
|
||||
_, _, err = runShortcutCapturingErr(t, ChartDataUpdate, args)
|
||||
if err == nil {
|
||||
t.Fatalf("expected chart data update validation error for args %#v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ func shortcutList() []common.Shortcut {
|
||||
|
||||
// Object CRUD (3 per skill)
|
||||
ChartCreate, ChartUpdate, ChartDelete,
|
||||
ChartCreateBasic, ChartConfigUpdate, ChartDataUpdate,
|
||||
PivotCreate, PivotUpdate, PivotDelete,
|
||||
CondFormatCreate, CondFormatUpdate, CondFormatDelete,
|
||||
FilterCreate, FilterUpdate, FilterDelete,
|
||||
|
||||
@@ -68,7 +68,7 @@ metadata:
|
||||
| 批量清除多区域 | `+cells-batch-clear --yes`(需确认;`--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`(先 `+chart-create --print-example <column\|bar\|line\|pie\|combo…>` 本地拿最小可用 `--properties` 模板,改 refs / index 即可用) | `lark-sheets-chart` | matplotlib / 本地画图再贴图(原生图表可交互、随数据更新) |
|
||||
| 画图表 / 可视化(柱 / 折线 / 饼 / 条 / 散点 / 组合…) | 先读 `lark-sheets-chart`;普通图用 `+chart-create-basic`,多图用一次 `+batch-update --continue-on-error`,已有图的数据源用 `+chart-data-update`、常用配置用 `+chart-config-update`;只有单系列 / 单数据点 / 高级引擎字段才用完整 `+chart-create` / `+chart-update` snapshot | `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`) |
|
||||
|
||||
@@ -200,7 +200,7 @@ lark-cli sheets +csv-get --url "https://.../sheets/shtXXX" --sheet-name "<真实
|
||||
|
||||
> ⚠️ **high-risk-write 命令清单(首次调用就带 `--yes`,别等 exit 10 再补;或先 `--dry-run` 预览)**:`+batch-update`、`+cells-clear`、`+cells-batch-clear`、`+sheet-delete`、`+dim-delete`、`+dropdown-delete`,以及各对象删除 `+chart-delete` / `+pivot-delete` / `+cond-format-delete` / `+filter-delete` / `+filter-view-delete` / `+sparkline-delete` / `+float-image-delete`。
|
||||
|
||||
**Agent 使用提示**:写复合 JSON flag 前对结构不确定时,先 `--print-schema --flag-name <name>`(深层字段用点分路径切片)再构造 payload;图表直接 `+chart-create --print-example <type>` 拿最小可用模板改参。reference 的 `## Schemas` 段只给一层结构。
|
||||
**Agent 使用提示**:写复合 JSON flag 前对结构不确定时,先 `--print-schema --flag-name <name>`(深层字段用点分路径切片)再构造 payload。图表任务必须先读 `lark-sheets-chart`:能用 `+chart-create-basic` / `+chart-data-update` / `+chart-config-update` 的语义参数就不得探查 schema 或构造 snapshot,互不依赖的多图创建 / 更新用一次 `+batch-update --continue-on-error`;只有单系列、单数据点或高级引擎字段无语义参数时,才用 `+chart-create --print-example <type>` 或点分 schema 构造完整 snapshot。reference 的 `## Schemas` 段只给一层结构。
|
||||
|
||||
### flag 内容类型与输出约定(术语速记)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
`+batch-update` 把多次写入打包成单次请求,但每个子操作仍受编辑类任务硬性默认规则约束:
|
||||
|
||||
1. **目标 range 必须落在用户授权范围内**:除用户明示要修改的区域外,子操作禁止扩张到无关单元格 / 列 / Sheet。规划 range 时先确认每个子操作的边界。
|
||||
2. **批次完成后必须回读校验**:整个 `+batch-update` 执行成功后,用 `+csv-get` 或 `+cells-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末),与本地脚本预先计算的预期值对照。
|
||||
3. **预期条数前置断言**:涉及"批量填充 N 行"或"对 M 个区域分别写入"时,先把 N、M 硬编码进代码,回读后断言实际等于预期;不一致就再发一轮 `+batch-update` 补齐,禁止交付半成品。
|
||||
2. **批次完成后必须回读校验**:整个 `+batch-update` 执行成功后,单元格写入用 `+csv-get` 或 `+cells-get` 抽样回读受影响区域,至少校验 3-5 个代表性单元格(首 / 中 / 末);图表写入用一次 `+chart-list` 核对对象数量、类型、系列和范围。
|
||||
3. **预期条数前置断言**:涉及"批量填充 N 行"、"对 M 个区域分别写入"或“每个 / 每天 / 分别各建一张图”时,先从数据数出 N、M 并写进清单;图表场景要断言 operations 中的创建数 = 独立实体图数 + 汇总图数。回读后断言实际等于预期,禁止用一张多系列汇总图替代多张独立图,也禁止交付半成品。
|
||||
|
||||
若本次 `+batch-update` 的任一子操作写入了公式、复制了公式模板、或导入了含公式的数据块,**回读校验之后还必须继续执行 `+formula-verify`**。`+batch-update` 的原子提交只保证“写入动作都执行了”,不保证整批公式运行结果 zero-error。
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
- 需要对**多个**不同区域执行 `+cells-{merge|unmerge}` 时(如按分组合并多列相同内容)
|
||||
- 需要先插入行列再写入数据时(`+dim-{insert|delete|hide|unhide|freeze|group|ungroup}` + `+cells-set`)
|
||||
- 需要对多个区域执行不同写入操作时(多次 `+cells-set` + `+cells-clear` 等组合)
|
||||
- 需要创建多张基础图,或统一更新多张图的数据源、标题、坐标轴、图例、标签、堆叠和平滑配置时(多个 `+chart-create-basic` / `+chart-data-update` / `+chart-config-update`)
|
||||
|
||||
**行高列宽批量不走这里**:多行 / 多列不同尺寸直接用 `+rows-resize --heights` / `+cols-resize --widths` 的 map 形态(如 `--widths '{"A":100,"C:E":120}'`,见 `lark-sheets-range-operations`),一次调用原子完成;map 形态不可作为 `--operations` 子操作嵌入(子操作里仍可用单区间形态 `range` + `height`/`width`)。
|
||||
|
||||
当同一工具需要对多个区域重复调用时,**必须**改用 `+batch-update` 合并为单次请求——`+batch-update` 是原子提交(要么全成功要么整批回滚);逐个调用非原子,中途失败会留下半成品。
|
||||
当同一工具需要对多个区域重复调用时,**必须**改用 `+batch-update` 合并为单次请求。存在依赖关系的操作保持默认严格事务(要么全成功要么整批回滚);互不依赖的多图表创建/更新使用 `--continue-on-error`,保留成功图表并根据逐项错误只重试失败项。
|
||||
|
||||
**公式相关批处理的默认闭环**:
|
||||
- 写前:先读 `lark-sheets-formula-translation`,把公式改写成飞书可执行语义。
|
||||
@@ -112,7 +113,7 @@ _公共:URL/token(无 sheet 定位) · 系统:`--yes`、`--dry-run`_
|
||||
_要批量执行的 CLI shortcut 操作列表,按声明顺序串行执行;任一失败立即中断_
|
||||
|
||||
**数组项**(类型 object):
|
||||
- `shortcut` (enum) — CLI shortcut 名(不是底层 MCP tool 名) [+cells-set / +cells-set-style / +cells-clear / +cells-merge / +cells-unmerge / +cells-replace / +csv-put / +dropdown-set / +dim-insert / +dim-delete / +dim-hide / +dim-unhide / +dim-freeze / +dim-group / +dim-ungroup / +rows-resize / +cols-resize / +range-move / +range-copy / +range-fill / +range-sort / +sheet-create / +sheet-delete / +sheet-rename / +sheet-move / +sheet-copy / +sheet-hide / +sheet-unhide / +sheet-set-tab-color / +sheet-show-gridline / +sheet-hide-gridline / +chart-create / +chart-update / +chart-delete / +pivot-create / +pivot-update / +pivot-delete / +cond-format-create / +cond-format-update / +cond-format-delete / +filter-create / +filter-update / +filter-delete / +filter-view-create / +filter-view-update / +filter-view-delete / +sparkline-create / +sparkline-update / +sparkline-delete / +float-image-create / +float-image-update / +float-image-delete]
|
||||
- `shortcut` (enum) — CLI shortcut 名(不是底层 MCP tool 名) [+cells-set / +cells-set-style / +cells-clear / +cells-merge / +cells-unmerge / +cells-replace / +csv-put / +dropdown-set / +dim-insert / +dim-delete / +dim-hide / +dim-unhide / +dim-freeze / +dim-group / +dim-ungroup / +rows-resize / +cols-resize / +range-move / +range-copy / +range-fill / +range-sort / +sheet-create / +sheet-delete / +sheet-rename / +sheet-move / +sheet-copy / +sheet-hide / +sheet-unhide / +sheet-set-tab-color / +sheet-show-gridline / +sheet-hide-gridline / +chart-create / +chart-update / +chart-delete / +chart-create-basic / +chart-config-update / +chart-data-update / +pivot-create / +pivot-update / +pivot-delete / +cond-format-create / +cond-format-update / +cond-format-delete / +filter-create / +filter-update / +filter-delete / +filter-view-create / +filter-view-update / +filter-view-delete / +sparkline-create / +sparkline-update / +sparkline-delete / +float-image-create / +float-image-update / +float-image-delete]
|
||||
- `input` (object) — 该 shortcut 的入参集——含子表定位 sheet_id(或 sheet_name),但不含 spreadsheet token/url(后者只在顶层 …
|
||||
|
||||
### `+cells-batch-set-style` `--border-styles`
|
||||
@@ -169,6 +170,19 @@ lark-cli sheets +batch-update --url "https://example.feishu.cn/sheets/shtXXX" --
|
||||
> ]
|
||||
> ```
|
||||
|
||||
> **多图表组合**:先完成全部辅助数据,再把每张图的完整语义输入放进同一个批次,并在顶层传 `--continue-on-error`;每项同时记录精确表头范围、数据方向和预期系列数。批次完成后,每个受影响的 sheet 各调用一次 `+chart-list`,不要每创建一张图就读取、调整数据后再删除重建。若数据范围或系列数不符,用 `+chart-data-update` 修正已有图表,不要删除后重建。
|
||||
>
|
||||
> ```json
|
||||
> [
|
||||
> {"shortcut":"+chart-create-basic","input":{"sheet_name":"Sheet1","chart_type":"column","data_range":"'Sheet1'!A1:C10","title":"分类对比","anchor_cell":"F2"}},
|
||||
> {"shortcut":"+chart-create-basic","input":{"sheet_name":"Sheet1","chart_type":"line","data_range":"'Sheet1'!E1:G10","title":"趋势变化","anchor_cell":"F18"}}
|
||||
> ]
|
||||
> ```
|
||||
>
|
||||
> ```bash
|
||||
> lark-cli sheets +batch-update --url "..." --operations @ops.json --continue-on-error --yes
|
||||
> ```
|
||||
|
||||
### `+cells-batch-set-style`
|
||||
|
||||
多 range 应用同一组 style(服务端走 `+batch-update` 原子事务):
|
||||
|
||||
@@ -2,18 +2,30 @@
|
||||
|
||||
## 真对象硬约束
|
||||
|
||||
当用户要求"画个图 / 数据可视化 / 趋势图 / 对比图 / 占比图"时,**必须**通过 `+chart-{create|update|delete}` 创建真实的图表对象。**禁止**用本地脚本调 matplotlib / seaborn 生成图片再插入到表格代替——静态图片无法随源数据更新,且失去交互能力。判断标准:交付后 `+chart-list` 必须能返回该对象。
|
||||
当用户要求"画个图 / 数据可视化 / 趋势图 / 对比图 / 占比图"时,**必须**通过图表创建命令创建真实的图表对象。**禁止**用本地脚本调 matplotlib / seaborn 生成图片再插入到表格代替——静态图片无法随源数据更新,且失去交互能力。判断标准:交付后 `+chart-list` 必须能返回该对象。
|
||||
|
||||
## 使用场景
|
||||
|
||||
读写图表对象。本 reference 覆盖 4 个 shortcut:
|
||||
读写图表对象。基础创建和常用更新优先用语义 shortcut,只在高级配置时使用原始 snapshot:
|
||||
|
||||
| 操作需求 | 使用工具 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 查看已有图表 | `+chart-list` | 获取图表的类型、数据源和样式配置 |
|
||||
| 创建/更新/删除图表 | `+chart-{create|update|delete}` | 对图表对象执行写入操作 |
|
||||
| 按类型和范围创建基础图 | `+chart-create-basic` | 支持 column/bar/line/area/pie/scatter/combo/radar、行/列方向与整图配色;无需构造 snapshot |
|
||||
| 修正已有图表的数据范围或方向 | `+chart-data-update` | 服务端重建数据映射,保留标题、样式、位置和尺寸 |
|
||||
| 批量创建或更新多个独立图表 | `+batch-update --continue-on-error` | 保留成功图表,并逐项返回失败原因;只重试失败项 |
|
||||
| 更新标题、轴、图例、标签、堆叠、平滑或整图配色 | `+chart-config-update` | 无需回写 snapshot,未传字段保持不变 |
|
||||
| 高级创建/更新、删除图表 | `+chart-{create|update|delete}` | 按系列/数据点精细设置等高级需求才使用原始 properties |
|
||||
|
||||
典型工作流:先读取现有图表了解配置 → 执行创建/更新/删除 → 再次读取验证结果。
|
||||
典型工作流:先确认表头和精确数据范围,用 `+chart-create-basic` 一次创建并尽量在同次调用中带上已知标题/轴/标签要求;创建后用 `+chart-list` 验证。已有图表的数据范围或方向错误时用 `+chart-data-update`,常用配置修正用 `+chart-config-update`。只有用户要求单个系列、数据点或高级引擎字段时,才读取现有 snapshot 并调 `+chart-update --properties`。不要为了常用配置先输出整份 schema,也不要删除重建已经创建成功的图表。
|
||||
|
||||
**多图表工作流**:先完成所有辅助数据和表头,列出每张目标图的类型、精确数据范围、标题和落点;确认清单后,用一次 `+batch-update --continue-on-error` 批量执行 `+chart-create-basic`。图表之间独立时允许部分成功:按返回的逐项结果定位失败图表,只重试失败项,不要重复创建成功图表。批次后每个受影响的 sheet 各调用一次 `+chart-list`,验证数量、类型、系列和范围。数据尚未稳定时不要提前创建图表;已经成功创建的图表有数据源差异时批量使用 `+chart-data-update`,有配置差异时批量使用 `+chart-config-update`,不要删除重建。
|
||||
|
||||
**数量词必须展开**:用户说“每个 / 每天 / 分别 / 逐一 / 各一张图”时,先从数据中数出实体数 `N`,把这 `N` 张图逐项写进清单,再加上其它汇总图得到目标总数 `M`;一个包含全部实体的多系列图不能替代这 `N` 张独立图。批次前断言 operations 中恰有 `M` 个图表创建,批次后断言图表总数、逐图标题与实体集合一致。
|
||||
|
||||
**范围与系列前置校验**:清单中同时记录每张图的表头范围、纳入列、明确排除列、数据方向和预期系列数。表头在首列时用默认 `--data-direction column`;表头在首行、每行代表一个系列时用 `--data-direction row`。创建前根据实际表头确认边界,不凭字母猜范围;创建后范围、方向或系列数不符时,使用 `+chart-data-update` 修正,服务端会重建 `refs` / `dim1` / `dim2.series` 并保留其它配置,不要删除后重建。批次跨多个 sheet 时,每个受影响的 sheet 各调用一次 `+chart-list`。
|
||||
|
||||
**整图配色优先走语义参数**:只要求统一主题或一组系列颜色时,在创建时传 `--color-palette` 或 `--colors`,已有图表用 `+chart-config-update` 更新;二者互斥。只有指定某个系列或某个数据点的颜色时才使用原始 snapshot。
|
||||
|
||||
## 需求→图表类型映射(创建前必查)
|
||||
|
||||
@@ -29,6 +41,8 @@
|
||||
|
||||
**`--properties` 结构锚点(构造前必读)**:`--properties` 顶层只有 `position` / `offset` / `size` / `snapshot` 四个字段,**没有**顶层 `data`,也没有再嵌一层 `properties`。图表数据配置全部挂在 `snapshot.data` 下——下文及示例里出现的 `refs` / `headerMode` / `dim1` / `dim2` / `nameRef` 一律指 `snapshot.data.refs` / `snapshot.data.headerMode` / `snapshot.data.dim1` / `snapshot.data.dim2`(及其下的 `serie.nameRef` / `series[].nameRef`);样式 / 堆叠 / 数据标签等在 `snapshot.plotArea` 下。**构造起点优先用 `lark-cli sheets +chart-create --print-example <column|bar|line|area|pie|scatter|radar|combo>` 拿最小可用模板改参**(本地即时返回);查深层字段用点分路径切片 `--print-schema --flag-name properties.snapshot.plotArea.axes`,别整篇 dump 翻页。完整结构以 `--print-schema --flag-name properties` 为准。
|
||||
|
||||
**`+chart-update` 局部更新硬规则(更新前必读)**:默认只在 `--properties` 中传实际变化的字段,未传字段保持不变;不要复制并回写完整 snapshot。`snapshot` 内普通对象递归合并,`refs` / `axes` / `series` 等数组整体替换——修改数组中的一项时,应先读取当前数组、改好后只回写该完整数组,不需要携带 snapshot 的其它字段。`snapshot.data.isStaticData` 不能通过 update 改变;需要切换静态/非静态数据时删除后重建。
|
||||
|
||||
**常见配置错误(必须注意)**:
|
||||
- **图表类型选择错误**:用户说"堆积柱形图/百分比堆积"时,应在 `properties.snapshot.plotArea.plot.extra.stack` 中配置堆叠;百分比堆叠需在该 stack 下设置 `percentage: true`。用户说"占比/比例"时,优先考虑饼图或百分比堆积图。注意区分 `column`(柱形图,纵向)与 `bar`(条形图,横向)是两个不同的 type 取值,"对比/各 XX" 类纵向柱默认用 `column`
|
||||
- **数据标签开关**:`plotArea.plot.labels` 对象的**存在性即开关**——
|
||||
@@ -104,6 +118,9 @@
|
||||
| Shortcut | Risk | 分组 |
|
||||
| --- | --- | --- |
|
||||
| `+chart-list` | read | 对象 |
|
||||
| `+chart-create-basic` | write | 对象 |
|
||||
| `+chart-config-update` | write | 对象 |
|
||||
| `+chart-data-update` | write | 对象 |
|
||||
| `+chart-create` | write | 对象 |
|
||||
| `+chart-update` | write | 对象 |
|
||||
| `+chart-delete` | high-risk-write | 对象 |
|
||||
@@ -118,6 +135,69 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
| --- | --- | --- | --- |
|
||||
| `--chart-id` | string | optional | 指定单个图表 reference_id 过滤 |
|
||||
|
||||
### `+chart-create-basic`
|
||||
|
||||
_公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--chart-type` | string | required | 图表类型(可选值:`column` / `bar` / `line` / `area` / `pie` / `scatter` / `combo` / `radar`) |
|
||||
| `--data-range` | string | required | 含表头的一个连续 A1 范围,或逗号分隔的同表多范围;对齐且不重叠时保留独立引用,否则合并为最小包围矩形 |
|
||||
| `--data-direction` | string | optional | 数据系列方向;column 表示首列为类别,row 表示首行为类别(可选值:`column` / `row`)(默认 `column`) |
|
||||
| `--title` | string | optional | 图表标题 |
|
||||
| `--subtitle` | string | optional | 图表副标题 |
|
||||
| `--legend-position` | string | optional | 图例位置;hidden 隐藏图例(可选值:`top` / `bottom` / `left` / `right` / `hidden`) |
|
||||
| `--x-axis-title` | string | optional | X 轴标题 |
|
||||
| `--y-axis-title` | string | optional | 左 Y 轴标题 |
|
||||
| `--secondary-y-axis-title` | string | optional | 右 Y 轴标题 |
|
||||
| `--x-axis-label-angle` | int | optional | X 轴标签旋转角度(可选值:`-90` / `-45` / `0` / `45` / `90`) |
|
||||
| `--y-axis-label-angle` | int | optional | 左 Y 轴标签旋转角度(可选值:`-90` / `-45` / `0` / `45` / `90`) |
|
||||
| `--data-labels` | string | optional | 数据标签内容;none 隐藏标签;兼容 category_percentage 并自动按 value_percentage 处理(可选值:`none` / `value` / `percentage` / `value_percentage` / `category_percentage` / `category` / `series`) |
|
||||
| `--data-label-position` | string | optional | 数据标签位置(可选值:`auto` / `top` / `bottom` / `left` / `right` / `center` / `inside` / `outside`) |
|
||||
| `--stack` | string | optional | 堆叠模式(可选值:`none` / `normal` / `percent`) |
|
||||
| `--stacked` | bool | optional | 兼容别名;等价于 --stack normal(隐藏 flag:不在 `--help` 列出,但可正常传入) |
|
||||
| `--smooth` | bool | optional | 是否使用平滑曲线;支持 --smooth=false 和 --smooth false |
|
||||
| `--color-palette` | string | optional | 预设整图配色主题;与 --colors 互斥(可选值:`brandColorSeries@v2` / `rainbowColorSeries@v2` / `complementaryColorSeries@v2` / `converseColorSeries@v2` / `primaryColorSeries@v2` / `singleColorSeries-B-@v2` / `singleColorSeries-W-@v2` / `singleColorSeries-G-@v2` / `singleColorSeries-Y-@v2` / `singleColorSeries-O-@v2` / `singleColorSeries-R-@v2` / `singleColorSeries-D-@v2`) |
|
||||
| `--colors` | string_slice | optional | 自定义整图系列颜色,逗号分隔且至少 2 个十六进制色值;与 --color-palette 互斥 |
|
||||
| `--anchor-cell` | string | optional | 可选图表锚点单元格,如 F2;省略时放到数据范围右侧 |
|
||||
| `--width` | int | optional | 可选图表宽度;必须与 --height 同时传 |
|
||||
| `--height` | int | optional | 可选图表高度;必须与 --width 同时传 |
|
||||
|
||||
### `+chart-config-update`
|
||||
|
||||
_公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--chart-id` | string | required | 目标图表 reference_id |
|
||||
| `--title` | string | optional | 图表标题 |
|
||||
| `--subtitle` | string | optional | 图表副标题 |
|
||||
| `--legend-position` | string | optional | 图例位置;hidden 隐藏图例(可选值:`top` / `bottom` / `left` / `right` / `hidden`) |
|
||||
| `--x-axis-title` | string | optional | X 轴标题 |
|
||||
| `--y-axis-title` | string | optional | 左 Y 轴标题 |
|
||||
| `--secondary-y-axis-title` | string | optional | 右 Y 轴标题 |
|
||||
| `--x-axis-label-angle` | int | optional | X 轴标签旋转角度(可选值:`-90` / `-45` / `0` / `45` / `90`) |
|
||||
| `--y-axis-label-angle` | int | optional | 左 Y 轴标签旋转角度(可选值:`-90` / `-45` / `0` / `45` / `90`) |
|
||||
| `--data-labels` | string | optional | 数据标签内容;none 隐藏标签;兼容 category_percentage 并自动按 value_percentage 处理(可选值:`none` / `value` / `percentage` / `value_percentage` / `category_percentage` / `category` / `series`) |
|
||||
| `--data-label-position` | string | optional | 数据标签位置(可选值:`auto` / `top` / `bottom` / `left` / `right` / `center` / `inside` / `outside`) |
|
||||
| `--stack` | string | optional | 堆叠模式(可选值:`none` / `normal` / `percent`) |
|
||||
| `--stacked` | bool | optional | 兼容别名;等价于 --stack normal(隐藏 flag:不在 `--help` 列出,但可正常传入) |
|
||||
| `--smooth` | bool | optional | 是否使用平滑曲线;支持 --smooth=false 和 --smooth false |
|
||||
| `--color-palette` | string | optional | 预设整图配色主题;与 --colors 互斥(可选值:`brandColorSeries@v2` / `rainbowColorSeries@v2` / `complementaryColorSeries@v2` / `converseColorSeries@v2` / `primaryColorSeries@v2` / `singleColorSeries-B-@v2` / `singleColorSeries-W-@v2` / `singleColorSeries-G-@v2` / `singleColorSeries-Y-@v2` / `singleColorSeries-O-@v2` / `singleColorSeries-R-@v2` / `singleColorSeries-D-@v2`) |
|
||||
| `--colors` | string_slice | optional | 自定义整图系列颜色,逗号分隔且至少 2 个十六进制色值;与 --color-palette 互斥 |
|
||||
|
||||
### `+chart-data-update`
|
||||
|
||||
_公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--chart-id` | string | required | 目标图表 reference_id |
|
||||
| `--data-range` | string | required | 新的含表头数据范围;支持逗号分隔的同表多范围,错位或重叠时自动合并 |
|
||||
| `--data-direction` | string | optional | 数据系列方向;省略时沿用现有图表方向(可选值:`column` / `row`) |
|
||||
| `--dim1-index` | int | optional | 类别/X 轴维度在数据范围中的 1-based 索引;省略时使用第 1 个维度 |
|
||||
| `--dim2-indexes` | string | optional | 值/Y 轴系列在数据范围中的 1-based 索引,逗号分隔;省略时使用除 dim1 外的全部维度 |
|
||||
|
||||
### `+chart-create`
|
||||
|
||||
_公共四件套 · 系统:`--dry-run`_
|
||||
@@ -133,7 +213,7 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--chart-id` | string | required | 目标图表 reference_id |
|
||||
| `--properties` | string + File + Stdin(复合 JSON) | required | 完整或足够完整的图表配置 JSON(先 `+chart-list` 回读再 patch) |
|
||||
| `--properties` | string + File + Stdin(复合 JSON) | required | 图表配置补丁 JSON;默认只传变化字段,未传字段保持不变;普通对象递归合并,数组整体替换 |
|
||||
|
||||
### `+chart-delete`
|
||||
|
||||
@@ -155,7 +235,7 @@ _创建/更新的图表属性_
|
||||
- `position` (object?) — 必填 { row: number, col: string }
|
||||
- `offset` (object?) — 可选 { row_offset?: number, col_offset?: number }
|
||||
- `size` (object?) — 必填 { width: number, height: number }
|
||||
- `snapshot` (object?) — 图表快照配置 { title?: object, subTitle?: object, style?: object, legend?: oneOf, plotArea: object, …共 6 项 }
|
||||
- `snapshot` (oneOf?) — 图表快照配置
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -165,6 +245,86 @@ _创建/更新的图表属性_
|
||||
|
||||
输出契约:返回按工作表分组的图表列表,每个图表含 `chart_id` / `position` / `details.snapshot` 等。
|
||||
|
||||
### `+chart-create-basic`
|
||||
|
||||
首列自动作为维度,后续列作为数值系列。饼图只使用第二列数值;散点图以首列为 X、后续列为 Y;组合图以第二列为左轴柱,后续列为右轴折线。数据范围必须包含真实表头;如果类别列与数值列不连续,可以给 `--data-range` 传逗号分隔的多个范围,例如 `"'Sheet1'!A1:A10,'Sheet1'!K1:L10"`。对齐且不重叠的范围会保留为独立引用,不会把 B:J 的间隔列纳入图表;错行、错列或重叠的范围会自动合并为同一工作表内的最小包围矩形。跨工作表范围仍会拒绝。如果数据子集的表头在范围外,改用高级 `+chart-create` 的 detached 模式。
|
||||
|
||||
```bash
|
||||
# 柱形图:默认放在数据范围右侧
|
||||
lark-cli sheets +chart-create-basic --url "..." --sheet-name "Sheet1" \
|
||||
--chart-type column --data-range "'Sheet1'!A1:C10" \
|
||||
--title "销售额对比" --x-axis-title "品类" --y-axis-title "销售额" \
|
||||
--legend-position bottom --data-labels value --data-label-position top
|
||||
|
||||
# 双轴组合图:首个数值列为左轴柱,其余数值列为右轴折线
|
||||
lark-cli sheets +chart-create-basic --url "..." --sheet-name "Sheet1" \
|
||||
--chart-type combo --data-range "'Sheet1'!A1:D13" \
|
||||
--title "价格与效率" --y-axis-title "价格" --secondary-y-axis-title "效率" \
|
||||
--anchor-cell F2 --width 700 --height 400
|
||||
```
|
||||
|
||||
多张基础图一次创建。先把所有数据准备完成,再生成 `ops.json`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"shortcut": "+chart-create-basic",
|
||||
"input": {
|
||||
"sheet_name": "Sheet1",
|
||||
"chart_type": "column",
|
||||
"data_range": "'Sheet1'!A1:C10",
|
||||
"title": "分类对比",
|
||||
"anchor_cell": "F2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"shortcut": "+chart-create-basic",
|
||||
"input": {
|
||||
"sheet_name": "Sheet1",
|
||||
"chart_type": "line",
|
||||
"data_range": "'Sheet1'!E1:G10",
|
||||
"title": "趋势变化",
|
||||
"anchor_cell": "F18"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```bash
|
||||
lark-cli sheets +batch-update --url "..." --operations @ops.json --continue-on-error --yes
|
||||
lark-cli sheets +chart-list --url "..." --sheet-name "Sheet1"
|
||||
```
|
||||
|
||||
### `+chart-data-update`
|
||||
|
||||
当创建后发现漏列、范围过宽、辅助分类列发生变化、系列选择错误或数据方向错误时,只更新数据源。`--data-direction` 省略时沿用现有图表方向;新范围必须包含表头。默认使用第 1 个维度作为 dim1、其余维度作为 dim2;需要精确选择时,用 1-based 的 `--dim1-index` 和逗号分隔的 `--dim2-indexes`。工具返回实际采用的 `normalized_data_ranges`,随后用 `+chart-list` 验证范围和系列数。
|
||||
|
||||
```bash
|
||||
# 把遗漏的最后一列纳入原折线图,保留标题、配色、图例和落点
|
||||
lark-cli sheets +chart-data-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--data-range "'Sheet1'!A1:M6"
|
||||
|
||||
# 改用按行组织的数据源
|
||||
lark-cli sheets +chart-data-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--data-range "'Sheet1'!A1:M6" --data-direction row
|
||||
|
||||
# 第 1 列作为类别,只使用第 4、8 列作为数值系列
|
||||
lark-cli sheets +chart-data-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--data-range "'Sheet1'!A1:M6" --dim1-index 1 --dim2-indexes "4,8"
|
||||
```
|
||||
|
||||
### `+chart-config-update`
|
||||
|
||||
只传需要改的字段。`--data-labels none` 会删除数据标签;`--legend-position hidden` 会隐藏图例;`--smooth=false` 和 `--smooth false` 都可显式关闭平滑曲线。为减少参数重试,`--stacked` 自动按 `--stack normal` 处理,`--data-labels category_percentage` 自动按 `value_percentage` 处理;新调用仍优先使用规范参数。
|
||||
|
||||
```bash
|
||||
lark-cli sheets +chart-config-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--title "新标题" --x-axis-label-angle -45 --legend-position right
|
||||
|
||||
lark-cli sheets +chart-config-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--data-labels value_percentage --data-label-position outside --stack percent
|
||||
```
|
||||
|
||||
### `+chart-create`
|
||||
|
||||
> **`snapshot.data` 必填 `dim1.serie.index` 或 `dim2.series[].index` 之一**(1-based,对应 `refs.value` 范围内的列序)。schema 允许传空 `{}` 但 server 运行时强制:缺则被拒为 `snapshot.data.dim1.serie.index and dim2.series[].index are both missing; at least one must be set`,即便侥幸通过也只会渲染空图。
|
||||
@@ -292,22 +452,23 @@ JSON
|
||||
|
||||
### `+chart-update`
|
||||
|
||||
**Update 三步法**(缺一步会丢字段):
|
||||
|
||||
1. `+chart-list --chart-id <id>` 拿到完整 snapshot
|
||||
2. 在拿到的 snapshot 上**局部**修改要改的字段,其余保持不变
|
||||
3. 把**完整 snapshot** 整个回写到 `--properties.snapshot`
|
||||
默认提交**最小 patch**。例如只修改标题时,只传标题字段:
|
||||
|
||||
```bash
|
||||
lark-cli sheets +chart-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--properties '{
|
||||
"position":{"row":0,"col":"A"},
|
||||
"size":{"width":480,"height":320},
|
||||
"snapshot": <完整快照(由 +chart-list 取回后局部修改)>
|
||||
"snapshot":{"title":{"text":"新的图表标题"}}
|
||||
}'
|
||||
```
|
||||
|
||||
> 关键:**不能只提交局部 snapshot**,否则未传字段会被还原为默认值。`+chart-update` 的语义是 PUT(整体覆盖),不是 PATCH。
|
||||
只调整尺寸时,不需要传 `snapshot`:
|
||||
|
||||
```bash
|
||||
lark-cli sheets +chart-update --url "..." --sheet-id "$SID" --chart-id "chrXXX" \
|
||||
--properties '{"size":{"width":640,"height":360}}'
|
||||
```
|
||||
|
||||
> 数组采用整体替换语义。比如只修改一个坐标轴时,先用 `+chart-list --chart-id <id>` 取得当前 `snapshot.plotArea.axes`,修改目标项后,仅回写 `{"snapshot":{"plotArea":{"axes":[...]}}}`;不要同时回写标题、数据源、图例等未变化字段。
|
||||
|
||||
### `+chart-delete`
|
||||
|
||||
@@ -325,8 +486,8 @@ lark-cli sheets +chart-delete --url "https://example.feishu.cn/sheets/shtXXX" --
|
||||
|
||||
### Validate / DryRun / Execute 约束
|
||||
|
||||
- `Validate`:XOR 公共四件套;`+chart-create` / `+chart-update` 的 `--properties` 必须能解析为合法 JSON;`+chart-delete`(high-risk-write)校验 `--yes` 或 `--dry-run` 至少一个。
|
||||
- `DryRun`:`+chart-create` / `+chart-update` 输出"将要 POST 的 body 模板";`+chart-delete` 输出"将要删除的 chart_id 及隶属 sheet",零网络副作用。
|
||||
- `Validate`:XOR 公共四件套;`+chart-data-update` 要求 `--chart-id` 和 `--data-range`,并校验 `--dim1-index` / `--dim2-indexes` 是正整数索引;`+chart-create` / `+chart-update` 的 `--properties` 必须能解析为合法 JSON;`+chart-delete`(high-risk-write)校验 `--yes` 或 `--dry-run` 至少一个。
|
||||
- `DryRun`:`+chart-data-update` / `+chart-create` / `+chart-update` 输出"将要 POST 的 body 模板";`+chart-delete` 输出"将要删除的 chart_id 及隶属 sheet",零网络副作用。
|
||||
- `Execute`:写操作执行后不自动回读;如需确认,自行调用 `+chart-list` 比对结果。
|
||||
|
||||
> `+chart-create` / `+chart-update` 是 write 级别,按需可用 `--dry-run` 预览,不要求 `--yes`。只有 `+chart-delete`(high-risk-write)必须 `--yes`。
|
||||
|
||||
Reference in New Issue
Block a user