diff --git a/shortcuts/sheets/sheet_batch_set_style.go b/shortcuts/sheets/sheet_batch_set_style.go new file mode 100644 index 00000000..c9da372d --- /dev/null +++ b/shortcuts/sheets/sheet_batch_set_style.go @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var SheetBatchSetStyle = common.Shortcut{ + Service: "sheets", + Command: "+batch-set-style", + Description: "Batch set cell styles for multiple ranges", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "data", Desc: "JSON array of {ranges, style} objects", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return common.FlagErrorf("specify --url or --spreadsheet-token") + } + var data interface{} + if err := json.Unmarshal([]byte(runtime.Str("data")), &data); err != nil { + return common.FlagErrorf("--data must be valid JSON: %v", err) + } + arr, ok := data.([]interface{}) + if !ok || len(arr) == 0 { + return common.FlagErrorf("--data must be a non-empty JSON array") + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + var data interface{} + json.Unmarshal([]byte(runtime.Str("data")), &data) + return common.NewDryRunAPI(). + PUT("/open-apis/sheets/v2/spreadsheets/:token/styles_batch_update"). + Body(map[string]interface{}{ + "data": data, + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + + var data interface{} + if err := json.Unmarshal([]byte(runtime.Str("data")), &data); err != nil { + return common.FlagErrorf("--data must be valid JSON: %v", err) + } + + result, err := runtime.CallAPI("PUT", + fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/styles_batch_update", validate.EncodePathSegment(token)), + nil, + map[string]interface{}{ + "data": data, + }, + ) + if err != nil { + return err + } + runtime.Out(result, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_cell_ops_test.go b/shortcuts/sheets/sheet_cell_ops_test.go new file mode 100644 index 00000000..bf4af00e --- /dev/null +++ b/shortcuts/sheets/sheet_cell_ops_test.go @@ -0,0 +1,539 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/httpmock" +) + +// ── MergeCells ─────────────────────────────────────────────────────────────── + +func TestSheetMergeCellsValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "range": "sheet1!A1:B2", "sheet-id": "", "merge-type": "MERGE_ALL", + }, nil) + err := SheetMergeCells.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSheetMergeCellsValidateRelativeRangeWithoutSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "A1:B2", "sheet-id": "", "merge-type": "MERGE_ALL", + }, nil) + err := SheetMergeCells.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--sheet-id") { + t.Fatalf("expected sheet-id error, got: %v", err) + } +} + +func TestSheetMergeCellsValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", "merge-type": "MERGE_ROWS", + }, nil) + if err := SheetMergeCells.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetMergeCellsDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "range": "A1:B2", "sheet-id": "sheet1", "merge-type": "MERGE_ALL", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetMergeCells.DryRun(context.Background(), rt)) + if !strings.Contains(got, `merge_cells`) { + t.Fatalf("DryRun URL missing merge_cells: %s", got) + } + if !strings.Contains(got, `"range":"sheet1!A1:B2"`) { + t.Fatalf("DryRun range not normalized: %s", got) + } + if !strings.Contains(got, `"mergeType":"MERGE_ALL"`) { + t.Fatalf("DryRun missing mergeType: %s", got) + } +} + +func TestSheetMergeCellsExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/merge_cells", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{"spreadsheetToken": "shtTOKEN"}}, + }) + err := mountAndRunSheets(t, SheetMergeCells, []string{ + "+merge-cells", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--merge-type", "MERGE_ALL", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "spreadsheetToken") { + t.Fatalf("stdout missing spreadsheetToken: %s", stdout.String()) + } +} + +func TestSheetMergeCellsExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/merge_cells", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetMergeCells, []string{ + "+merge-cells", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--merge-type", "MERGE_ALL", "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +// ── UnmergeCells ───────────────────────────────────────────────────────────── + +func TestSheetUnmergeCellsValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "range": "sheet1!A1:B2", "sheet-id": "", + }, nil) + err := SheetUnmergeCells.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSheetUnmergeCellsValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + }, nil) + if err := SheetUnmergeCells.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetUnmergeCellsDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "range": "sheet1!A1:B2", "sheet-id": "", + }, nil) + got := mustMarshalSheetsDryRun(t, SheetUnmergeCells.DryRun(context.Background(), rt)) + if !strings.Contains(got, `unmerge_cells`) { + t.Fatalf("DryRun URL missing unmerge_cells: %s", got) + } + if !strings.Contains(got, `"range":"sheet1!A1:B2"`) { + t.Fatalf("DryRun missing range: %s", got) + } +} + +func TestSheetUnmergeCellsExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/unmerge_cells", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{"spreadsheetToken": "shtTOKEN"}}, + }) + err := mountAndRunSheets(t, SheetUnmergeCells, []string{ + "+unmerge-cells", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetUnmergeCellsExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/unmerge_cells", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetUnmergeCells, []string{ + "+unmerge-cells", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +// ── Replace ────────────────────────────────────────────────────────────────── + +func TestSheetReplaceValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "sheet-id": "s1", "find": "a", "replacement": "b", "range": "", + }, map[string]bool{"match-case": false, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + err := SheetReplace.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSheetReplaceValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "s1", "find": "hello", "replacement": "world", "range": "", + }, map[string]bool{"match-case": false, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + if err := SheetReplace.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetReplaceValidateMismatchedRangeSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "sheet1", "find": "a", "replacement": "b", + "range": "sheet2!A1:B2", + }, map[string]bool{"match-case": false, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + err := SheetReplace.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "does not match") { + t.Fatalf("expected mismatch error, got: %v", err) + } +} + +func TestSheetReplaceValidateMatchingRangeSheetID(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "sheet-id": "sheet1", "find": "a", "replacement": "b", + "range": "sheet1!A1:B2", + }, map[string]bool{"match-case": false, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + if err := SheetReplace.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetReplaceDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "find": "old", "replacement": "new", "range": "A1:C5", + }, map[string]bool{"match-case": true, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + got := mustMarshalSheetsDryRun(t, SheetReplace.DryRun(context.Background(), rt)) + if !strings.Contains(got, `replace`) { + t.Fatalf("DryRun URL missing replace: %s", got) + } + if !strings.Contains(got, `"find":"old"`) { + t.Fatalf("DryRun missing find: %s", got) + } + if !strings.Contains(got, `"replacement":"new"`) { + t.Fatalf("DryRun missing replacement: %s", got) + } + if !strings.Contains(got, `"match_case":true`) { + t.Fatalf("DryRun missing match_case: %s", got) + } +} + +func TestSheetReplaceDryRunNoRange(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "sheet-id": "sheet1", "find": "a", "replacement": "b", "range": "", + }, map[string]bool{"match-case": false, "match-entire-cell": false, "search-by-regex": false, "include-formulas": false}) + got := mustMarshalSheetsDryRun(t, SheetReplace.DryRun(context.Background(), rt)) + // When no range specified, range defaults to sheet-id + if !strings.Contains(got, `"range":"sheet1"`) { + t.Fatalf("DryRun range should default to sheet-id: %s", got) + } +} + +func TestSheetReplaceExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + stub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/replace", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "replace_result": map[string]interface{}{ + "matched_cells": []interface{}{"A1"}, "rows_count": float64(1), + }, + }}, + } + reg.Register(stub) + err := mountAndRunSheets(t, SheetReplace, []string{ + "+replace", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--find", "hello", "--replacement", "world", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "matched_cells") { + t.Fatalf("stdout missing matched_cells: %s", stdout.String()) + } + var body map[string]interface{} + if err := json.Unmarshal(stub.CapturedBody, &body); err != nil { + t.Fatalf("parse body: %v", err) + } + if body["find"] != "hello" || body["replacement"] != "world" { + t.Fatalf("unexpected body: %#v", body) + } +} + +func TestSheetReplaceExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", URL: "/open-apis/sheets/v3/spreadsheets/shtTOKEN/sheets/sheet1/replace", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetReplace, []string{ + "+replace", "--spreadsheet-token", "shtTOKEN", + "--sheet-id", "sheet1", "--find", "a", "--replacement", "b", "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +// ── SetStyle ───────────────────────────────────────────────────────────────── + +func TestSheetSetStyleValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `{"font":{"bold":true}}`, + }, nil) + err := SheetSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSheetSetStyleValidateInvalidJSON(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `{invalid}`, + }, nil) + err := SheetSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--style must be valid JSON") { + t.Fatalf("expected JSON error, got: %v", err) + } +} + +func TestSheetSetStyleValidateRejectsArray(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `[{"bold":true}]`, + }, nil) + err := SheetSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "JSON object") { + t.Fatalf("expected object error, got: %v", err) + } +} + +func TestSheetSetStyleValidateRejectsString(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `"bold"`, + }, nil) + err := SheetSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "JSON object") { + t.Fatalf("expected object error, got: %v", err) + } +} + +func TestSheetSetStyleValidateRejectsNull(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `null`, + }, nil) + err := SheetSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "JSON object") { + t.Fatalf("expected object error, got: %v", err) + } +} + +func TestSheetSetStyleValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "range": "sheet1!A1:B2", "sheet-id": "", + "style": `{"font":{"bold":true},"backColor":"#ff0000"}`, + }, nil) + if err := SheetSetStyle.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetSetStyleDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", "range": "A1:B2", "sheet-id": "sheet1", + "style": `{"font":{"bold":true}}`, + }, nil) + got := mustMarshalSheetsDryRun(t, SheetSetStyle.DryRun(context.Background(), rt)) + if !strings.Contains(got, `"method":"PUT"`) { + t.Fatalf("DryRun should use PUT: %s", got) + } + if !strings.Contains(got, `/style`) { + t.Fatalf("DryRun URL missing /style: %s", got) + } + if !strings.Contains(got, `"range":"sheet1!A1:B2"`) { + t.Fatalf("DryRun range not normalized: %s", got) + } + if !strings.Contains(got, `"bold":true`) { + t.Fatalf("DryRun missing style: %s", got) + } +} + +func TestSheetSetStyleExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + stub := &httpmock.Stub{ + Method: "PUT", + URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/style", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "updates": map[string]interface{}{"updatedCells": float64(4), "updatedRange": "sheet1!A1:B2"}, + }}, + } + reg.Register(stub) + err := mountAndRunSheets(t, SheetSetStyle, []string{ + "+set-style", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--style", `{"font":{"bold":true}}`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "updatedCells") { + t.Fatalf("stdout missing updatedCells: %s", stdout.String()) + } + var body map[string]interface{} + if err := json.Unmarshal(stub.CapturedBody, &body); err != nil { + t.Fatalf("parse body: %v", err) + } + appendStyle, _ := body["appendStyle"].(map[string]interface{}) + if appendStyle["range"] != "sheet1!A1:B2" { + t.Fatalf("unexpected range: %v", appendStyle["range"]) + } +} + +func TestSheetSetStyleExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/style", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetSetStyle, []string{ + "+set-style", "--spreadsheet-token", "shtTOKEN", + "--range", "sheet1!A1:B2", "--style", `{"font":{"bold":true}}`, "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} + +// ── BatchSetStyle ──────────────────────────────────────────────────────────── + +func TestSheetBatchSetStyleValidateMissingToken(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "", + "data": `[{"ranges":["sheet1!A1:B2"],"style":{"font":{"bold":true}}}]`, + }, nil) + err := SheetBatchSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--url or --spreadsheet-token") { + t.Fatalf("expected token error, got: %v", err) + } +} + +func TestSheetBatchSetStyleValidateInvalidJSON(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "data": `not-json`, + }, nil) + err := SheetBatchSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "--data must be valid JSON") { + t.Fatalf("expected JSON error, got: %v", err) + } +} + +func TestSheetBatchSetStyleValidateNotArray(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "data": `{"not":"array"}`, + }, nil) + err := SheetBatchSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "non-empty JSON array") { + t.Fatalf("expected array error, got: %v", err) + } +} + +func TestSheetBatchSetStyleValidateEmptyArray(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", "data": `[]`, + }, nil) + err := SheetBatchSetStyle.Validate(context.Background(), rt) + if err == nil || !strings.Contains(err.Error(), "non-empty JSON array") { + t.Fatalf("expected empty array error, got: %v", err) + } +} + +func TestSheetBatchSetStyleValidateSuccess(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht1", + "data": `[{"ranges":["sheet1!A1:B2"],"style":{"font":{"bold":true}}}]`, + }, nil) + if err := SheetBatchSetStyle.Validate(context.Background(), rt); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSheetBatchSetStyleDryRun(t *testing.T) { + t.Parallel() + rt := newSheetsTestRuntime(t, map[string]string{ + "url": "", "spreadsheet-token": "sht_test", + "data": `[{"ranges":["sheet1!A1:B2"],"style":{"backColor":"#ff0000"}}]`, + }, nil) + got := mustMarshalSheetsDryRun(t, SheetBatchSetStyle.DryRun(context.Background(), rt)) + if !strings.Contains(got, `styles_batch_update`) { + t.Fatalf("DryRun URL missing styles_batch_update: %s", got) + } + if !strings.Contains(got, `"method":"PUT"`) { + t.Fatalf("DryRun should use PUT: %s", got) + } +} + +func TestSheetBatchSetStyleExecuteSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", + URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/styles_batch_update", + Body: map[string]interface{}{"code": 0, "msg": "success", "data": map[string]interface{}{ + "totalUpdatedCells": float64(4), "revision": float64(90), + }}, + }) + err := mountAndRunSheets(t, SheetBatchSetStyle, []string{ + "+batch-set-style", "--spreadsheet-token", "shtTOKEN", + "--data", `[{"ranges":["sheet1!A1:B2"],"style":{"font":{"bold":true}}}]`, "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "totalUpdatedCells") { + t.Fatalf("stdout missing totalUpdatedCells: %s", stdout.String()) + } +} + +func TestSheetBatchSetStyleExecuteAPIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, sheetsTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "PUT", URL: "/open-apis/sheets/v2/spreadsheets/shtTOKEN/styles_batch_update", + Status: 400, Body: map[string]interface{}{"code": 90001, "msg": "invalid"}, + }) + err := mountAndRunSheets(t, SheetBatchSetStyle, []string{ + "+batch-set-style", "--spreadsheet-token", "shtTOKEN", + "--data", `[{"ranges":["sheet1!A1:B2"],"style":{}}]`, "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected error") + } +} diff --git a/shortcuts/sheets/sheet_merge_cells.go b/shortcuts/sheets/sheet_merge_cells.go new file mode 100644 index 00000000..6b471a5d --- /dev/null +++ b/shortcuts/sheets/sheet_merge_cells.go @@ -0,0 +1,77 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var SheetMergeCells = common.Shortcut{ + Service: "sheets", + Command: "+merge-cells", + Description: "Merge cells in a spreadsheet", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "range", Desc: "cell range (!A1:B2, or A1:B2 with --sheet-id)", Required: true}, + {Name: "sheet-id", Desc: "sheet ID (for relative range)"}, + {Name: "merge-type", Desc: "merge method", Required: true, Enum: []string{"MERGE_ALL", "MERGE_ROWS", "MERGE_COLUMNS"}}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return common.FlagErrorf("specify --url or --spreadsheet-token") + } + if err := validateSheetRangeInput(runtime.Str("sheet-id"), runtime.Str("range")); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v2/spreadsheets/:token/merge_cells"). + Body(map[string]interface{}{ + "range": r, + "mergeType": runtime.Str("merge-type"), + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + + data, err := runtime.CallAPI("POST", + fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/merge_cells", validate.EncodePathSegment(token)), + nil, + map[string]interface{}{ + "range": r, + "mergeType": runtime.Str("merge-type"), + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_replace.go b/shortcuts/sheets/sheet_replace.go new file mode 100644 index 00000000..a24fe168 --- /dev/null +++ b/shortcuts/sheets/sheet_replace.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var SheetReplace = common.Shortcut{ + Service: "sheets", + Command: "+replace", + Description: "Find and replace cell values in a spreadsheet", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "sheet-id", Desc: "sheet ID", Required: true}, + {Name: "find", Desc: "search text or regex pattern", Required: true}, + {Name: "replacement", Desc: "replacement text", Required: true}, + {Name: "range", Desc: "search range (!A1:D10, or A1:D10 with --sheet-id)"}, + {Name: "match-case", Type: "bool", Desc: "case-sensitive search"}, + {Name: "match-entire-cell", Type: "bool", Desc: "match entire cell content"}, + {Name: "search-by-regex", Type: "bool", Desc: "use regex search"}, + {Name: "include-formulas", Type: "bool", Desc: "search in formulas"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return common.FlagErrorf("specify --url or --spreadsheet-token") + } + if err := validateSheetRangeInput(runtime.Str("sheet-id"), runtime.Str("range")); err != nil { + return err + } + if r := runtime.Str("range"); r != "" { + if rangeSheetID, _, ok := splitSheetRange(r); ok && runtime.Str("sheet-id") != "" && rangeSheetID != runtime.Str("sheet-id") { + return common.FlagErrorf("--range sheet ID %q does not match --sheet-id %q", rangeSheetID, runtime.Str("sheet-id")) + } + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + sheetID := runtime.Str("sheet-id") + findCondition := map[string]interface{}{ + "range": sheetID, + "match_case": runtime.Bool("match-case"), + "match_entire_cell": runtime.Bool("match-entire-cell"), + "search_by_regex": runtime.Bool("search-by-regex"), + "include_formulas": runtime.Bool("include-formulas"), + } + if runtime.Str("range") != "" { + findCondition["range"] = normalizeSheetRange(sheetID, runtime.Str("range")) + } + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v3/spreadsheets/:token/sheets/:sheet_id/replace"). + Body(map[string]interface{}{ + "find_condition": findCondition, + "find": runtime.Str("find"), + "replacement": runtime.Str("replacement"), + }). + Set("token", token).Set("sheet_id", sheetID) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + + sheetID := runtime.Str("sheet-id") + findCondition := map[string]interface{}{ + "range": sheetID, + "match_case": runtime.Bool("match-case"), + "match_entire_cell": runtime.Bool("match-entire-cell"), + "search_by_regex": runtime.Bool("search-by-regex"), + "include_formulas": runtime.Bool("include-formulas"), + } + if runtime.Str("range") != "" { + findCondition["range"] = normalizeSheetRange(sheetID, runtime.Str("range")) + } + + data, err := runtime.CallAPI("POST", + fmt.Sprintf("/open-apis/sheets/v3/spreadsheets/%s/sheets/%s/replace", + validate.EncodePathSegment(token), + validate.EncodePathSegment(sheetID), + ), + nil, + map[string]interface{}{ + "find_condition": findCondition, + "find": runtime.Str("find"), + "replacement": runtime.Str("replacement"), + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_set_style.go b/shortcuts/sheets/sheet_set_style.go new file mode 100644 index 00000000..6da9976e --- /dev/null +++ b/shortcuts/sheets/sheet_set_style.go @@ -0,0 +1,95 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var SheetSetStyle = common.Shortcut{ + Service: "sheets", + Command: "+set-style", + Description: "Set cell style for a range", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "range", Desc: "cell range (!A1:B2, or A1:B2 with --sheet-id)", Required: true}, + {Name: "sheet-id", Desc: "sheet ID (for relative range)"}, + {Name: "style", Desc: "style JSON object (e.g. {\"font\":{\"bold\":true},\"backColor\":\"#ff0000\"})", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return common.FlagErrorf("specify --url or --spreadsheet-token") + } + var style interface{} + if err := json.Unmarshal([]byte(runtime.Str("style")), &style); err != nil { + return common.FlagErrorf("--style must be valid JSON: %v", err) + } + if _, ok := style.(map[string]interface{}); !ok { + return common.FlagErrorf("--style must be a JSON object, got %T", style) + } + if err := validateSheetRangeInput(runtime.Str("sheet-id"), runtime.Str("range")); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + var style interface{} + json.Unmarshal([]byte(runtime.Str("style")), &style) + return common.NewDryRunAPI(). + PUT("/open-apis/sheets/v2/spreadsheets/:token/style"). + Body(map[string]interface{}{ + "appendStyle": map[string]interface{}{ + "range": r, + "style": style, + }, + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + var style interface{} + if err := json.Unmarshal([]byte(runtime.Str("style")), &style); err != nil { + return common.FlagErrorf("--style must be valid JSON: %v", err) + } + + data, err := runtime.CallAPI("PUT", + fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/style", validate.EncodePathSegment(token)), + nil, + map[string]interface{}{ + "appendStyle": map[string]interface{}{ + "range": r, + "style": style, + }, + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/sheet_unmerge_cells.go b/shortcuts/sheets/sheet_unmerge_cells.go new file mode 100644 index 00000000..f79d2e5c --- /dev/null +++ b/shortcuts/sheets/sheet_unmerge_cells.go @@ -0,0 +1,74 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package sheets + +import ( + "context" + "fmt" + + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var SheetUnmergeCells = common.Shortcut{ + Service: "sheets", + Command: "+unmerge-cells", + Description: "Unmerge (split) cells in a spreadsheet", + Risk: "write", + Scopes: []string{"sheets:spreadsheet:write_only", "sheets:spreadsheet:read"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "url", Desc: "spreadsheet URL"}, + {Name: "spreadsheet-token", Desc: "spreadsheet token"}, + {Name: "range", Desc: "cell range (!A1:B2, or A1:B2 with --sheet-id)", Required: true}, + {Name: "sheet-id", Desc: "sheet ID (for relative range)"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + if token == "" { + return common.FlagErrorf("specify --url or --spreadsheet-token") + } + if err := validateSheetRangeInput(runtime.Str("sheet-id"), runtime.Str("range")); err != nil { + return err + } + return nil + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + return common.NewDryRunAPI(). + POST("/open-apis/sheets/v2/spreadsheets/:token/unmerge_cells"). + Body(map[string]interface{}{ + "range": r, + }). + Set("token", token) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + token := runtime.Str("spreadsheet-token") + if runtime.Str("url") != "" { + token = extractSpreadsheetToken(runtime.Str("url")) + } + + r := normalizeSheetRange(runtime.Str("sheet-id"), runtime.Str("range")) + + data, err := runtime.CallAPI("POST", + fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/unmerge_cells", validate.EncodePathSegment(token)), + nil, + map[string]interface{}{ + "range": r, + }, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil + }, +} diff --git a/shortcuts/sheets/shortcuts.go b/shortcuts/sheets/shortcuts.go index a86ba7b5..60fa5383 100644 --- a/shortcuts/sheets/shortcuts.go +++ b/shortcuts/sheets/shortcuts.go @@ -16,5 +16,10 @@ func Shortcuts() []common.Shortcut { SheetFind, SheetCreate, SheetExport, + SheetMergeCells, + SheetUnmergeCells, + SheetReplace, + SheetSetStyle, + SheetBatchSetStyle, } } diff --git a/skills/lark-sheets/SKILL.md b/skills/lark-sheets/SKILL.md index c3655f80..e2d23d79 100644 --- a/skills/lark-sheets/SKILL.md +++ b/skills/lark-sheets/SKILL.md @@ -155,6 +155,11 @@ Shortcut 是对常用操作的高级封装(`lark-cli sheets + [flags]` | [`+find`](references/lark-sheets-find.md) | Find cells in a spreadsheet | | [`+create`](references/lark-sheets-create.md) | Create a spreadsheet (optional header row and initial data) | | [`+export`](references/lark-sheets-export.md) | Export a spreadsheet (async task polling + optional download) | +| [`+merge-cells`](references/lark-sheets-merge-cells.md) | Merge cells in a spreadsheet | +| [`+unmerge-cells`](references/lark-sheets-unmerge-cells.md) | Unmerge (split) cells in a spreadsheet | +| [`+replace`](references/lark-sheets-replace.md) | Find and replace cell values | +| [`+set-style`](references/lark-sheets-set-style.md) | Set cell style for a range | +| [`+batch-set-style`](references/lark-sheets-batch-set-style.md) | Batch set cell styles for multiple ranges | ## API Resources diff --git a/skills/lark-sheets/references/lark-sheets-batch-set-style.md b/skills/lark-sheets/references/lark-sheets-batch-set-style.md new file mode 100644 index 00000000..832c3be9 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-batch-set-style.md @@ -0,0 +1,53 @@ + +# sheets +batch-set-style(批量设置单元格样式) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +batch-set-style`。 + +对多个范围批量设置不同的单元格样式,一次请求可包含多组范围和样式。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 对两组范围分别设置样式 +lark-cli sheets +batch-set-style --spreadsheet-token "shtxxxxxxxx" \ + --data '[{"ranges":["!A1:C3"],"style":{"font":{"bold":true},"backColor":"#21d11f"}},{"ranges":["!D1:F3"],"style":{"foreColor":"#ff0000"}}]' + +# 同一样式应用到多个范围 +lark-cli sheets +batch-set-style --spreadsheet-token "shtxxxxxxxx" \ + --data '[{"ranges":["!A1:B2","!D4:E5"],"style":{"hAlign":1,"font":{"bold":true}}}]' + +# 仅预览 +lark-cli sheets +batch-set-style --spreadsheet-token "shtxxxxxxxx" \ + --data '[{"ranges":["!A1:B2"],"style":{"backColor":"#0000ff"}}]' --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url ` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token ` | 否 | 表格 token(与 `--url` 二选一) | +| `--data ` | 是 | JSON 数组,每项包含 `ranges`(字符串数组)和 `style`(样式对象) | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +### style 对象字段 + +与 `+set-style` 相同,参见 [lark-sheets-set-style](lark-sheets-set-style.md)。 + +## 输出 + +JSON,包含: + +- `totalUpdatedRows/totalUpdatedColumns/totalUpdatedCells`:汇总更新量 +- `revision`:工作表版本号 +- `responses[]`:每个范围的更新详情 + +## 参考 + +- [lark-sheets-set-style](lark-sheets-set-style.md) — 单范围设置样式 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-sheets/references/lark-sheets-merge-cells.md b/skills/lark-sheets/references/lark-sheets-merge-cells.md new file mode 100644 index 00000000..92bf6eb4 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-merge-cells.md @@ -0,0 +1,47 @@ + +# sheets +merge-cells(合并单元格) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +merge-cells`。 + +合并指定范围的单元格,支持全合并、按行合并、按列合并三种模式。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 全合并 A1:B2 +lark-cli sheets +merge-cells --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:B2" --merge-type MERGE_ALL + +# 按行合并,配合 --sheet-id +lark-cli sheets +merge-cells --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "A1:D4" --merge-type MERGE_ROWS + +# 仅预览 +lark-cli sheets +merge-cells --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:B2" --merge-type MERGE_ALL --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url ` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token ` | 否 | 表格 token(与 `--url` 二选一) | +| `--range ` | 是 | 单元格范围(`!A1:B2`,或配合 `--sheet-id` 使用 `A1:B2`) | +| `--sheet-id ` | 否 | 工作表 ID(用于相对范围) | +| `--merge-type ` | 是 | 合并方式:`MERGE_ALL`(全合并)、`MERGE_ROWS`(按行)、`MERGE_COLUMNS`(按列) | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含 `spreadsheetToken`。 + +## 参考 + +- [lark-sheets-unmerge-cells](lark-sheets-unmerge-cells.md) — 拆分单元格 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-sheets/references/lark-sheets-replace.md b/skills/lark-sheets/references/lark-sheets-replace.md new file mode 100644 index 00000000..46b4a4d5 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-replace.md @@ -0,0 +1,62 @@ + +# sheets +replace(替换单元格) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +replace`。 + +在指定范围内查找并替换单元格内容,支持正则、大小写敏感、全单元格匹配等选项。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 简单替换 +lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --find "hello" --replacement "world" + +# 指定范围 + 大小写敏感 +lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "A1:C5" \ + --find "Hello" --replacement "World" --match-case + +# 正则替换 +lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --find "\\d{4}-\\d{2}-\\d{2}" \ + --replacement "DATE" --search-by-regex + +# 仅预览 +lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --find "old" --replacement "new" --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url ` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token ` | 否 | 表格 token(与 `--url` 二选一) | +| `--sheet-id ` | 是 | 工作表 ID | +| `--find ` | 是 | 搜索文本(启用 `--search-by-regex` 时为正则表达式) | +| `--replacement ` | 是 | 替换文本 | +| `--range ` | 否 | 搜索范围(不传则搜索整个工作表) | +| `--match-case` | 否 | 区分大小写 | +| `--match-entire-cell` | 否 | 匹配整个单元格 | +| `--search-by-regex` | 否 | 使用正则表达式搜索 | +| `--include-formulas` | 否 | 在公式中搜索 | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含 `replace_result`: + +- `matched_cells`:匹配的非公式单元格列表 +- `matched_formula_cells`:匹配的公式单元格列表 +- `rows_count`:包含匹配的行数 + +## 参考 + +- [lark-sheets-find](lark-sheets-find.md) — 查找单元格(只查不改) +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-sheets/references/lark-sheets-set-style.md b/skills/lark-sheets/references/lark-sheets-set-style.md new file mode 100644 index 00000000..4fb34710 --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-set-style.md @@ -0,0 +1,71 @@ + +# sheets +set-style(设置单元格样式) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +set-style`。 + +对指定范围的单元格设置样式(字体、颜色、对齐、边框等)。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 设置加粗 + 红色背景 +lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:C3" \ + --style '{"font":{"bold":true},"backColor":"#ff0000"}' + +# 配合 --sheet-id + 居中对齐 +lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "A1:D1" \ + --style '{"hAlign":1,"vAlign":1,"font":{"bold":true,"font_size":"12pt/1.5"}}' + +# 清除格式 +lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:Z100" --style '{"clean":true}' + +# 仅预览 +lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:B2" --style '{"foreColor":"#0000ff"}' --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url ` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token ` | 否 | 表格 token(与 `--url` 二选一) | +| `--range ` | 是 | 单元格范围(`!A1:B2`,或配合 `--sheet-id`) | +| `--sheet-id ` | 否 | 工作表 ID(用于相对范围) | +| `--style ` | 是 | 样式 JSON 对象 | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +### style JSON 字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `font.bold` | bool | 加粗 | +| `font.italic` | bool | 斜体 | +| `font.font_size` | string | 字号,如 `"12pt/1.5"` | +| `font.clean` | bool | 清除字体格式 | +| `textDecoration` | int | 0=无, 1=下划线, 2=删除线, 3=两者 | +| `formatter` | string | 数字格式 | +| `hAlign` | int | 水平对齐:0=左, 1=居中, 2=右 | +| `vAlign` | int | 垂直对齐:0=上, 1=居中, 2=下 | +| `foreColor` | string | 字体颜色(hex,如 `"#000000"`) | +| `backColor` | string | 背景色(hex) | +| `borderType` | string | 边框:FULL_BORDER, OUTER_BORDER, INNER_BORDER, NO_BORDER 等 | +| `borderColor` | string | 边框颜色(hex) | +| `clean` | bool | 清除所有格式 | + +## 输出 + +JSON,包含 `updates`(updatedRange, updatedRows, updatedColumns, updatedCells, revision)。 + +## 参考 + +- [lark-sheets-batch-set-style](lark-sheets-batch-set-style.md) — 批量设置样式 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数 diff --git a/skills/lark-sheets/references/lark-sheets-unmerge-cells.md b/skills/lark-sheets/references/lark-sheets-unmerge-cells.md new file mode 100644 index 00000000..60cb843f --- /dev/null +++ b/skills/lark-sheets/references/lark-sheets-unmerge-cells.md @@ -0,0 +1,46 @@ + +# sheets +unmerge-cells(拆分单元格) + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +本 skill 对应 shortcut:`lark-cli sheets +unmerge-cells`。 + +拆分指定范围内的合并单元格。 + +> [!CAUTION] +> 这是**写入操作** —— 执行前必须确认用户意图。可以先用 `--dry-run` 预览。 + +## 命令 + +```bash +# 拆分 A1:B2 范围的合并单元格 +lark-cli sheets +unmerge-cells --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:B2" + +# 配合 --sheet-id +lark-cli sheets +unmerge-cells --spreadsheet-token "shtxxxxxxxx" \ + --sheet-id "" --range "A1:B2" + +# 仅预览 +lark-cli sheets +unmerge-cells --spreadsheet-token "shtxxxxxxxx" \ + --range "!A1:B2" --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--url ` | 否 | 电子表格 URL(与 `--spreadsheet-token` 二选一) | +| `--spreadsheet-token ` | 否 | 表格 token(与 `--url` 二选一) | +| `--range ` | 是 | 单元格范围(`!A1:B2`,或配合 `--sheet-id` 使用 `A1:B2`) | +| `--sheet-id ` | 否 | 工作表 ID(用于相对范围) | +| `--dry-run` | 否 | 仅打印参数,不执行请求 | + +## 输出 + +JSON,包含 `spreadsheetToken`。 + +## 参考 + +- [lark-sheets-merge-cells](lark-sheets-merge-cells.md) — 合并单元格 +- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数