mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
feat(sheets): implement lark_sheet_workbook shortcuts (B1)
Land the 8 modify_workbook_structure shortcuts that round out the lark_sheet_workbook canonical skill alongside the existing +workbook-info: +sheet-create / +sheet-delete / +sheet-rename / +sheet-move / +sheet-copy / +sheet-hide / +sheet-unhide / +sheet-set-tab-color. All eight call modify_workbook_structure via the One-OpenAPI invoke_write endpoint, dispatched by the `operation` enum. Helpers in helpers.go grow publicSheetFlags() / resolveSheetSelector() / sheetSelectorForToolInput() / sheetSelectorPlaceholder() so future sheet-level shortcuts share the public --sheet-id / --sheet-name XOR treatment. +sheet-create intentionally drops the sheet selector pair since create has no existing-sheet anchor (matches the spec fix in tool-shortcut-map.json). +sheet-delete is the first high-risk-write shortcut in the canonical package; the framework requires --yes (exit code 10 otherwise). +sheet-move's tool requires source_index in addition to target_index. The CLI accepts an optional --source-index override and falls back to a single get_workbook_structure read to derive it (and to resolve sheet_id from --sheet-name). DryRun stays network-free by rendering <resolve> placeholders for any field that would need that read.
This commit is contained in:
@@ -65,3 +65,56 @@ func publicTokenFlags() []common.Flag {
|
||||
{Name: "spreadsheet-token", Desc: "spreadsheet token (XOR --url)"},
|
||||
}
|
||||
}
|
||||
|
||||
// publicSheetFlags extends publicTokenFlags with the sheet selector pair.
|
||||
// Use for any +sheet-* / +cells-* / +dim-* / object shortcut that operates
|
||||
// on an existing single sub-sheet.
|
||||
func publicSheetFlags() []common.Flag {
|
||||
return append(publicTokenFlags(),
|
||||
common.Flag{Name: "sheet-id", Desc: "sheet reference_id (XOR --sheet-name)"},
|
||||
common.Flag{Name: "sheet-name", Desc: "sheet title (XOR --sheet-id)"},
|
||||
)
|
||||
}
|
||||
|
||||
// resolveSheetSelector validates the --sheet-id / --sheet-name XOR and
|
||||
// returns whichever was supplied. Network-free.
|
||||
//
|
||||
// Returned tuple: (sheetID, sheetName). Exactly one is non-empty — callers
|
||||
// pass both through to the tool input; the server picks whichever fits.
|
||||
func resolveSheetSelector(runtime *common.RuntimeContext) (sheetID, sheetName string, err error) {
|
||||
if err := common.ExactlyOne(runtime, "sheet-id", "sheet-name"); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if id := strings.TrimSpace(runtime.Str("sheet-id")); id != "" {
|
||||
if err := validate.RejectControlChars(id, "sheet-id"); err != nil {
|
||||
return "", "", common.FlagErrorf("%v", err)
|
||||
}
|
||||
return id, "", nil
|
||||
}
|
||||
name := strings.TrimSpace(runtime.Str("sheet-name"))
|
||||
if err := validate.RejectControlChars(name, "sheet-name"); err != nil {
|
||||
return "", "", common.FlagErrorf("%v", err)
|
||||
}
|
||||
return "", name, nil
|
||||
}
|
||||
|
||||
// sheetSelectorForToolInput packs --sheet-id / --sheet-name into the tool
|
||||
// input map, omitting empty fields. Use after resolveSheetSelector returns.
|
||||
func sheetSelectorForToolInput(input map[string]interface{}, sheetID, sheetName string) {
|
||||
if sheetID != "" {
|
||||
input["sheet_id"] = sheetID
|
||||
}
|
||||
if sheetName != "" {
|
||||
input["sheet_name"] = sheetName
|
||||
}
|
||||
}
|
||||
|
||||
// sheetSelectorPlaceholder returns a human-readable identifier for the
|
||||
// selected sheet, suitable for DryRun output. Avoids leaking that --sheet-name
|
||||
// would be resolved server-side at execute time.
|
||||
func sheetSelectorPlaceholder(sheetID, sheetName string) string {
|
||||
if sheetID != "" {
|
||||
return sheetID
|
||||
}
|
||||
return "<resolve:" + sheetName + ">"
|
||||
}
|
||||
|
||||
@@ -5,14 +5,31 @@ package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/util"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// WorkbookInfo wraps the get_workbook_structure tool: list a workbook's
|
||||
// sub-sheets with their metadata (sheet_id, title, dimensions, freeze rows
|
||||
// and cols, index, hidden). This is the first step for every sheets task —
|
||||
// downstream sheet-level operations all depend on the sheet_id returned here.
|
||||
// ─── lark_sheet_workbook ──────────────────────────────────────────────
|
||||
//
|
||||
// Wraps two tools behind the One-OpenAPI: get_workbook_structure (read) and
|
||||
// modify_workbook_structure (write, dispatched by `operation` enum).
|
||||
//
|
||||
// CLI Risk tiers diverge intentionally from the tool's single endpoint:
|
||||
// - +sheet-delete is high-risk-write (irreversible)
|
||||
// - everything else is plain write
|
||||
//
|
||||
// +sheet-create only carries --url / --spreadsheet-token (no sheet selector):
|
||||
// the create tool path needs no existing-sheet anchor, so the public sheet
|
||||
// selector pair is dropped here to avoid a misleading XOR requirement.
|
||||
|
||||
// WorkbookInfo wraps get_workbook_structure: list a workbook's sub-sheets
|
||||
// with their metadata (sheet_id, title, dimensions, freeze rows and cols,
|
||||
// index, hidden). First step for every sheets task — downstream sheet-level
|
||||
// operations all depend on the sheet_id returned here.
|
||||
var WorkbookInfo = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+workbook-info",
|
||||
@@ -50,3 +67,491 @@ var WorkbookInfo = common.Shortcut{
|
||||
"First step for every sheets task — capture sheet_id from the result before doing any sheet-level operation.",
|
||||
},
|
||||
}
|
||||
|
||||
// SheetCreate creates a new sub-sheet. --title is the new sheet's name;
|
||||
// --index inserts at a specific position (omitted → appended). Default
|
||||
// dimensions match the canonical schema (rows=100, cols=26 when omitted —
|
||||
// tool's defaults differ but CLI surface stays predictable).
|
||||
var SheetCreate = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-create",
|
||||
Description: "Create a new sub-sheet with an optional position and initial dimensions.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: append(publicTokenFlags(),
|
||||
common.Flag{Name: "title", Desc: "new sheet title", Required: true},
|
||||
common.Flag{Name: "index", Type: "int", Default: "-1", Desc: "insertion position (0-based); omit to append"},
|
||||
common.Flag{Name: "row-count", Type: "int", Default: "0", Desc: "initial row count; omit for tool default (200)"},
|
||||
common.Flag{Name: "col-count", Type: "int", Default: "0", Desc: "initial column count; omit for tool default (20)"},
|
||||
),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(runtime.Str("title")) == "" {
|
||||
return common.FlagErrorf("--title is required")
|
||||
}
|
||||
if n := runtime.Int("row-count"); n < 0 || n > 50000 {
|
||||
return common.FlagErrorf("--row-count must be between 0 and 50000")
|
||||
}
|
||||
if n := runtime.Int("col-count"); n < 0 || n > 200 {
|
||||
return common.FlagErrorf("--col-count must be between 0 and 200")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", sheetCreateInput(runtime, token))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", sheetCreateInput(runtime, token))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func sheetCreateInput(runtime *common.RuntimeContext, token string) map[string]interface{} {
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "create",
|
||||
"sheet_name": strings.TrimSpace(runtime.Str("title")),
|
||||
}
|
||||
if runtime.Changed("index") {
|
||||
input["target_index"] = runtime.Int("index")
|
||||
}
|
||||
if n := runtime.Int("row-count"); n > 0 {
|
||||
input["rows"] = n
|
||||
}
|
||||
if n := runtime.Int("col-count"); n > 0 {
|
||||
input["columns"] = n
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// SheetDelete deletes a sub-sheet. high-risk-write — framework rejects
|
||||
// without --yes. Always preview with --dry-run first to confirm the target.
|
||||
var SheetDelete = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-delete",
|
||||
Description: "Delete a sub-sheet (irreversible).",
|
||||
Risk: "high-risk-write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: publicSheetFlags(),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := resolveSheetSelector(runtime)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input := map[string]interface{}{"excel_id": token, "operation": "delete"}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", 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 := map[string]interface{}{"excel_id": token, "operation": "delete"}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
Tips: []string{
|
||||
"Sheet deletion is irreversible. Always run with --dry-run first to verify the target sheet_id/sheet_name.",
|
||||
},
|
||||
}
|
||||
|
||||
// SheetRename renames a sub-sheet via --title (mapped to tool's new_name).
|
||||
var SheetRename = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-rename",
|
||||
Description: "Rename a sub-sheet.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: append(publicSheetFlags(),
|
||||
common.Flag{Name: "title", Desc: "new sheet title", Required: true},
|
||||
),
|
||||
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("title")) == "" {
|
||||
return common.FlagErrorf("--title is required")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "rename",
|
||||
"new_name": strings.TrimSpace(runtime.Str("title")),
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", 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 := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "rename",
|
||||
"new_name": strings.TrimSpace(runtime.Str("title")),
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// SheetMove moves a sub-sheet to a new index. The tool requires sheet_id
|
||||
// and source_index in addition to target_index. The CLI accepts:
|
||||
// - --sheet-id / --sheet-name to identify the sheet
|
||||
// - --source-index (optional) for explicit source position
|
||||
//
|
||||
// When --source-index is omitted, or when --sheet-name is used instead of
|
||||
// --sheet-id, Execute issues a single get_workbook_structure read to derive
|
||||
// the missing pieces. DryRun stays network-free: it uses <resolve> placeholders
|
||||
// for any field that would need that read.
|
||||
var SheetMove = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-move",
|
||||
Description: "Move a sub-sheet to a new position.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:read", "sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: append(publicSheetFlags(),
|
||||
common.Flag{Name: "index", Type: "int", Required: true, Desc: "target position (0-based)"},
|
||||
common.Flag{Name: "source-index", Type: "int", Default: "-1", Desc: "source position (0-based); omitted → auto-derived from --sheet-id/--sheet-name's current workbook position"},
|
||||
),
|
||||
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 !runtime.Changed("index") {
|
||||
return common.FlagErrorf("--index is required")
|
||||
}
|
||||
if runtime.Int("index") < 0 {
|
||||
return common.FlagErrorf("--index must be >= 0")
|
||||
}
|
||||
if runtime.Changed("source-index") && runtime.Int("source-index") < 0 {
|
||||
return common.FlagErrorf("--source-index must be >= 0")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "move",
|
||||
"sheet_id": sheetSelectorPlaceholder(sheetID, sheetName),
|
||||
"target_index": runtime.Int("index"),
|
||||
"source_index": sourceIndexOrPlaceholder(runtime),
|
||||
}
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", 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
|
||||
}
|
||||
|
||||
resolvedID := sheetID
|
||||
var sourceIndex int
|
||||
needIDLookup := sheetID == ""
|
||||
needIndexLookup := !runtime.Changed("source-index")
|
||||
if needIDLookup || needIndexLookup {
|
||||
lookedID, lookedIdx, err := lookupSheetIndex(ctx, runtime, token, sheetID, sheetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedID = lookedID
|
||||
sourceIndex = lookedIdx
|
||||
}
|
||||
if runtime.Changed("source-index") {
|
||||
sourceIndex = runtime.Int("source-index")
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "move",
|
||||
"sheet_id": resolvedID,
|
||||
"source_index": sourceIndex,
|
||||
"target_index": runtime.Int("index"),
|
||||
}
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
Tips: []string{
|
||||
"Pass --source-index when you already know it to avoid the extra read; otherwise CLI derives it from --sheet-id/--sheet-name.",
|
||||
},
|
||||
}
|
||||
|
||||
// sourceIndexOrPlaceholder returns the user-supplied source-index, or the
|
||||
// string "<resolve>" when DryRun should signal that Execute will derive it.
|
||||
func sourceIndexOrPlaceholder(runtime *common.RuntimeContext) interface{} {
|
||||
if runtime.Changed("source-index") {
|
||||
return runtime.Int("source-index")
|
||||
}
|
||||
return "<resolve>"
|
||||
}
|
||||
|
||||
// SheetCopy duplicates a sub-sheet. --title (optional) names the copy;
|
||||
// --index (optional) places it.
|
||||
var SheetCopy = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-copy",
|
||||
Description: "Duplicate a sub-sheet, optionally renaming and repositioning the copy.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: append(publicSheetFlags(),
|
||||
common.Flag{Name: "title", Desc: "title for the duplicated sheet (server-generated when omitted)"},
|
||||
common.Flag{Name: "index", Type: "int", Default: "-1", Desc: "insertion position for the copy (0-based); omit to append"},
|
||||
),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := resolveSheetSelector(runtime)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", sheetCopyInput(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, ToolKindWrite, "modify_workbook_structure", sheetCopyInput(runtime, token, sheetID, sheetName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func sheetCopyInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} {
|
||||
input := map[string]interface{}{"excel_id": token, "operation": "duplicate"}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
if t := strings.TrimSpace(runtime.Str("title")); t != "" {
|
||||
input["new_name"] = t
|
||||
}
|
||||
if runtime.Changed("index") {
|
||||
input["target_index"] = runtime.Int("index")
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// SheetHide / SheetUnhide toggle visibility. Visible bool semantics live in
|
||||
// the operation enum so callers don't need a --visible flag.
|
||||
var SheetHide = newSheetVisibilityShortcut(
|
||||
"+sheet-hide", "Hide a sub-sheet from the tabs bar.", "hide",
|
||||
)
|
||||
|
||||
var SheetUnhide = newSheetVisibilityShortcut(
|
||||
"+sheet-unhide", "Restore a hidden sub-sheet.", "unhide",
|
||||
)
|
||||
|
||||
func newSheetVisibilityShortcut(command, desc, op string) common.Shortcut {
|
||||
return common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: command,
|
||||
Description: desc,
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: publicSheetFlags(),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err := resolveSheetSelector(runtime)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input := map[string]interface{}{"excel_id": token, "operation": op}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", 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 := map[string]interface{}{"excel_id": token, "operation": op}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SheetSetTabColor sets the tab color of a sub-sheet. --color "" clears.
|
||||
var SheetSetTabColor = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+sheet-set-tab-color",
|
||||
Description: "Set or clear the tab color of a sub-sheet.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: append(publicSheetFlags(),
|
||||
common.Flag{Name: "color", Desc: "hex color like #FF0000; pass empty string to clear"},
|
||||
),
|
||||
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 !runtime.Changed("color") {
|
||||
return common.FlagErrorf("--color is required (empty string clears)")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "set_tab_color",
|
||||
"tab_color": runtime.Str("color"),
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
return invokeToolDryRun(token, ToolKindWrite, "modify_workbook_structure", 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 := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "set_tab_color",
|
||||
"tab_color": runtime.Str("color"),
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
out, err := callTool(ctx, runtime, token, ToolKindWrite, "modify_workbook_structure", input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(out, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// lookupSheetIndex finds a sub-sheet by id or name and returns its canonical
|
||||
// id + current 0-based index. Caller is responsible for ensuring at least one
|
||||
// of sheetID/sheetName is non-empty.
|
||||
func lookupSheetIndex(ctx context.Context, runtime *common.RuntimeContext, token, sheetID, sheetName string) (resolvedID string, index int, err error) {
|
||||
out, err := callTool(ctx, runtime, token, ToolKindRead, "get_workbook_structure", map[string]interface{}{
|
||||
"excel_id": token,
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
m, ok := out.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", 0, output.Errorf(output.ExitAPI, "tool_output", "get_workbook_structure returned non-object output")
|
||||
}
|
||||
sheets, _ := m["sheets"].([]interface{})
|
||||
for _, raw := range sheets {
|
||||
sm, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, _ := sm["sheet_id"].(string)
|
||||
name, _ := sm["sheet_name"].(string)
|
||||
if (sheetID != "" && id == sheetID) || (sheetName != "" && name == sheetName) {
|
||||
idx, ok := util.ToFloat64(sm["index"])
|
||||
if !ok {
|
||||
return "", 0, output.Errorf(output.ExitAPI, "tool_output", "sheet entry missing index field")
|
||||
}
|
||||
return id, int(idx), nil
|
||||
}
|
||||
}
|
||||
target := sheetID
|
||||
if target == "" {
|
||||
target = sheetName
|
||||
}
|
||||
return "", 0, output.Errorf(output.ExitAPI, "not_found", fmt.Sprintf("sheet %q not found in workbook", target))
|
||||
}
|
||||
|
||||
@@ -12,5 +12,13 @@ func Shortcuts() []common.Shortcut {
|
||||
return []common.Shortcut{
|
||||
// lark_sheet_workbook
|
||||
WorkbookInfo,
|
||||
SheetCreate,
|
||||
SheetDelete,
|
||||
SheetRename,
|
||||
SheetMove,
|
||||
SheetCopy,
|
||||
SheetHide,
|
||||
SheetUnhide,
|
||||
SheetSetTabColor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `--url` | 公共 | string | XOR | spreadsheet URL(与 `--spreadsheet-token` 二选一) |
|
||||
| `--spreadsheet-token` | 公共 | string | XOR | spreadsheet token(与 `--url` 二选一) |
|
||||
| `--sheet-id` | 公共 | string | XOR | 工作表 reference_id(与 `--sheet-name` 二选一) |
|
||||
| `--sheet-name` | 公共 | string | XOR | 工作表名称(与 `--sheet-id` 二选一) |
|
||||
| `--title` | 专有 | string | 是 | 新工作表名称 |
|
||||
| `--index` | 专有 | int | 否 | 插入位置;省略时附加到末尾 |
|
||||
| `--row-count` | 专有 | int | 否 | 初始行数,默认 100 |
|
||||
@@ -104,6 +102,7 @@
|
||||
| `--sheet-id` | 公共 | string | XOR | 工作表 reference_id(与 `--sheet-name` 二选一) |
|
||||
| `--sheet-name` | 公共 | string | XOR | 工作表名称(与 `--sheet-id` 二选一) |
|
||||
| `--index` | 专有 | int | 是 | 目标位置(0-based) |
|
||||
| `--source-index` | 专有 | int | 否 | 源位置(0-based);可选,未传时由 CLI runtime 根据 --sheet-id / --sheet-name 当前在工作簿中的 index 自动派生 |
|
||||
| `--dry-run` | 系统 | bool | 否 | |
|
||||
|
||||
### `+sheet-copy`
|
||||
|
||||
Reference in New Issue
Block a user