feat(sheets): add cell operation shortcuts for merge, replace, and style (#412)

Add 5 new sheet shortcuts for cell operations:
- +merge-cells: merge cells with MERGE_ALL/MERGE_ROWS/MERGE_COLUMNS
- +unmerge-cells: split merged cells
- +replace: find and replace cell values
- +set-style: set cell style (font, color, alignment, border)
- +batch-set-style: batch set styles for multiple ranges

Includes unit tests (81-89% coverage) and skill reference docs.
This commit is contained in:
caojie0621
2026-04-11 16:45:14 +08:00
committed by GitHub
parent 368ec7e753
commit 3242ca6f7f
13 changed files with 1269 additions and 0 deletions

View File

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

View File

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

View File

@@ -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 (<sheetId>!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
},
}

View File

@@ -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 (<sheetId>!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
},
}

View File

@@ -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 (<sheetId>!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
},
}

View File

@@ -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 (<sheetId>!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
},
}

View File

@@ -16,5 +16,10 @@ func Shortcuts() []common.Shortcut {
SheetFind,
SheetCreate,
SheetExport,
SheetMergeCells,
SheetUnmergeCells,
SheetReplace,
SheetSetStyle,
SheetBatchSetStyle,
}
}

View File

@@ -155,6 +155,11 @@ Shortcut 是对常用操作的高级封装(`lark-cli sheets +<verb> [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

View File

@@ -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":["<sheetId>!A1:C3"],"style":{"font":{"bold":true},"backColor":"#21d11f"}},{"ranges":["<sheetId>!D1:F3"],"style":{"foreColor":"#ff0000"}}]'
# 同一样式应用到多个范围
lark-cli sheets +batch-set-style --spreadsheet-token "shtxxxxxxxx" \
--data '[{"ranges":["<sheetId>!A1:B2","<sheetId>!D4:E5"],"style":{"hAlign":1,"font":{"bold":true}}}]'
# 仅预览
lark-cli sheets +batch-set-style --spreadsheet-token "shtxxxxxxxx" \
--data '[{"ranges":["<sheetId>!A1:B2"],"style":{"backColor":"#0000ff"}}]' --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--url <url>` | 否 | 电子表格 URL`--spreadsheet-token` 二选一) |
| `--spreadsheet-token <token>` | 否 | 表格 token`--url` 二选一) |
| `--data <json>` | 是 | 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) — 认证和全局参数

View File

@@ -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 "<sheetId>!A1:B2" --merge-type MERGE_ALL
# 按行合并,配合 --sheet-id
lark-cli sheets +merge-cells --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --range "A1:D4" --merge-type MERGE_ROWS
# 仅预览
lark-cli sheets +merge-cells --spreadsheet-token "shtxxxxxxxx" \
--range "<sheetId>!A1:B2" --merge-type MERGE_ALL --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--url <url>` | 否 | 电子表格 URL`--spreadsheet-token` 二选一) |
| `--spreadsheet-token <token>` | 否 | 表格 token`--url` 二选一) |
| `--range <range>` | 是 | 单元格范围(`<sheetId>!A1:B2`,或配合 `--sheet-id` 使用 `A1:B2` |
| `--sheet-id <id>` | 否 | 工作表 ID用于相对范围 |
| `--merge-type <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) — 认证和全局参数

View File

@@ -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 "<sheetId>" --find "hello" --replacement "world"
# 指定范围 + 大小写敏感
lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --range "A1:C5" \
--find "Hello" --replacement "World" --match-case
# 正则替换
lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --find "\\d{4}-\\d{2}-\\d{2}" \
--replacement "DATE" --search-by-regex
# 仅预览
lark-cli sheets +replace --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --find "old" --replacement "new" --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--url <url>` | 否 | 电子表格 URL`--spreadsheet-token` 二选一) |
| `--spreadsheet-token <token>` | 否 | 表格 token`--url` 二选一) |
| `--sheet-id <id>` | 是 | 工作表 ID |
| `--find <text>` | 是 | 搜索文本(启用 `--search-by-regex` 时为正则表达式) |
| `--replacement <text>` | 是 | 替换文本 |
| `--range <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) — 认证和全局参数

View File

@@ -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 "<sheetId>!A1:C3" \
--style '{"font":{"bold":true},"backColor":"#ff0000"}'
# 配合 --sheet-id + 居中对齐
lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --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 "<sheetId>!A1:Z100" --style '{"clean":true}'
# 仅预览
lark-cli sheets +set-style --spreadsheet-token "shtxxxxxxxx" \
--range "<sheetId>!A1:B2" --style '{"foreColor":"#0000ff"}' --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--url <url>` | 否 | 电子表格 URL`--spreadsheet-token` 二选一) |
| `--spreadsheet-token <token>` | 否 | 表格 token`--url` 二选一) |
| `--range <range>` | 是 | 单元格范围(`<sheetId>!A1:B2`,或配合 `--sheet-id` |
| `--sheet-id <id>` | 否 | 工作表 ID用于相对范围 |
| `--style <json>` | 是 | 样式 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) — 认证和全局参数

View File

@@ -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 "<sheetId>!A1:B2"
# 配合 --sheet-id
lark-cli sheets +unmerge-cells --spreadsheet-token "shtxxxxxxxx" \
--sheet-id "<sheetId>" --range "A1:B2"
# 仅预览
lark-cli sheets +unmerge-cells --spreadsheet-token "shtxxxxxxxx" \
--range "<sheetId>!A1:B2" --dry-run
```
## 参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--url <url>` | 否 | 电子表格 URL`--spreadsheet-token` 二选一) |
| `--spreadsheet-token <token>` | 否 | 表格 token`--url` 二选一) |
| `--range <range>` | 是 | 单元格范围(`<sheetId>!A1:B2`,或配合 `--sheet-id` 使用 `A1:B2` |
| `--sheet-id <id>` | 否 | 工作表 ID用于相对范围 |
| `--dry-run` | 否 | 仅打印参数,不执行请求 |
## 输出
JSON包含 `spreadsheetToken`
## 参考
- [lark-sheets-merge-cells](lark-sheets-merge-cells.md) — 合并单元格
- [lark-shared](../../lark-shared/SKILL.md) — 认证和全局参数