mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
b07be60068
Emit structured validation, API, network, file, and internal error envelopes for Sheets shortcuts so users and agents can recover from failed spreadsheet workflows using stable type, subtype, param, and code fields. Add Sheets domain errscontract and golangci guards to prevent legacy envelope and common helper regressions.
173 lines
5.7 KiB
Go
173 lines
5.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package sheets
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// ─── lark_sheet_search_replace ────────────────────────────────────────
|
|
//
|
|
// Wraps search_data (read) and replace_data (write). Both tools take an
|
|
// `options` sub-object; the CLI flattens its common booleans
|
|
// (--match-case / --match-entire-cell / --regex / --include-formulas) into
|
|
// independent flags per the铁律.
|
|
|
|
// CellsSearch wraps search_data: find cell coordinates matching --find,
|
|
// with optional case / regex / whole-cell / formula-text controls.
|
|
var CellsSearch = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+cells-search",
|
|
Description: "Find cells matching --find in a spreadsheet (case / regex / whole-cell / formula-text controls).",
|
|
Risk: "read",
|
|
Scopes: []string{"sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
Flags: flagsFor("+cells-search"),
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
if _, _, err := resolveSheetSelector(runtime); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(runtime.Str("find")) == "" {
|
|
return sheetsValidationForFlag("find", "--find is required")
|
|
}
|
|
return nil
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := resolveSpreadsheetToken(runtime)
|
|
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
|
return invokeToolDryRun(token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
|
|
},
|
|
Execute: 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
|
|
}
|
|
out, err := callTool(ctx, runtime, token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func searchInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} {
|
|
input := map[string]interface{}{
|
|
"excel_id": token,
|
|
"search_term": runtime.Str("find"),
|
|
}
|
|
sheetSelectorForToolInput(input, sheetID, sheetName)
|
|
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
|
|
input["range"] = r
|
|
}
|
|
if runtime.Changed("offset") && runtime.Int("offset") > 0 {
|
|
input["offset"] = runtime.Int("offset")
|
|
}
|
|
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
|
|
input["options"] = opts
|
|
}
|
|
if n := runtime.Int("max-matches"); n > 0 {
|
|
input["max_matches"] = n
|
|
}
|
|
return input
|
|
}
|
|
|
|
// searchReplaceOptions packs the four shared boolean flags into the tool's
|
|
// `options` sub-object. Empty result → caller should omit the field.
|
|
func searchReplaceOptions(runtime flagView) map[string]interface{} {
|
|
opts := map[string]interface{}{}
|
|
if runtime.Bool("match-case") {
|
|
opts["match_case"] = true
|
|
}
|
|
if runtime.Bool("match-entire-cell") {
|
|
opts["match_entire_cell"] = true
|
|
}
|
|
if runtime.Bool("regex") {
|
|
opts["use_regex"] = true
|
|
}
|
|
if runtime.Bool("include-formulas") {
|
|
opts["match_formulas"] = true
|
|
}
|
|
return opts
|
|
}
|
|
|
|
// CellsReplace wraps replace_data: find and replace text across a
|
|
// spreadsheet, with the same option controls as +cells-search.
|
|
var CellsReplace = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+cells-replace",
|
|
Description: "Find and replace text in a spreadsheet (case / regex / whole-cell / formula-text controls).",
|
|
Risk: "write",
|
|
Scopes: []string{"sheets:spreadsheet:write_only"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
Flags: flagsFor("+cells-replace"),
|
|
Validate: validateViaInput(replaceInput),
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := resolveSpreadsheetToken(runtime)
|
|
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
|
input, _ := replaceInput(runtime, token, sheetID, sheetName)
|
|
return invokeToolDryRun(token, ToolKindWrite, "replace_data", input)
|
|
},
|
|
Execute: 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
|
|
}
|
|
input, err := replaceInput(runtime, token, sheetID, sheetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := callTool(ctx, runtime, token, ToolKindWrite, "replace_data", input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
Tips: []string{
|
|
"Always preview with --dry-run before running — replace can mutate every matching cell across the sheet.",
|
|
},
|
|
}
|
|
|
|
func replaceInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
|
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(runtime.Str("find")) == "" {
|
|
return nil, sheetsValidationForFlag("find", "--find is required")
|
|
}
|
|
if !runtime.Changed("replacement") {
|
|
return nil, sheetsValidationForFlag("replacement", "--replacement is required (pass an empty string to delete matches)")
|
|
}
|
|
input := map[string]interface{}{
|
|
"excel_id": token,
|
|
"search_term": runtime.Str("find"),
|
|
"replace_term": runtime.Str("replacement"),
|
|
}
|
|
sheetSelectorForToolInput(input, sheetID, sheetName)
|
|
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
|
|
input["range"] = r
|
|
}
|
|
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
|
|
input["options"] = opts
|
|
}
|
|
return input, nil
|
|
}
|