feat(sheets): add +recover shortcut (方案B, full-document revision rollback)

Splits 方案B (recover) into its own MR, shipping ahead of 方案A (undo).
+recover rolls the whole spreadsheet back to a past revision via the facade
recover_to_revision write tool; only the recover shortcut and its flag live
here. undo (方案A) and the read/write transaction_id split stay in PR #1321.
This commit is contained in:
zhengzhijie
2026-06-11 21:55:06 +08:00
parent bed30c4ecb
commit c95ab1c28e
4 changed files with 133 additions and 3 deletions

View File

@@ -1,4 +1,37 @@
{
"+recover": {
"risk": "write",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet token (XOR with `--url`)"
},
{
"name": "to-revision",
"kind": "own",
"type": "int",
"required": "required",
"desc": "Restore the whole spreadsheet to this revision (a revision number returned by a prior write)"
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
},
"+workbook-info": {
"risk": "read",
"flags": [
@@ -54,7 +87,7 @@
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Insert position (0-based); appended to the end when omitted",
"desc": "Insert position; appended to the end when omitted",
"default": "-1"
},
{
@@ -2020,7 +2053,7 @@
"kind": "own",
"type": "string",
"required": "required",
"desc": "RFC 4180 CSV text; values or formulas (a leading = is evaluated as a formula); no styles / comments / images (use +cells-set for those).",
"desc": "RFC 4180 CSV text; plain values only (no formulas / styles / comments)",
"input": [
"file",
"stdin"

View File

@@ -319,7 +319,7 @@ var flagDefs = map[string]commandDef{
{Name: "sheet-id", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet reference_id (XOR with `--sheet-name`)"},
{Name: "sheet-name", Kind: "public", Type: "string", Required: "xor", Desc: "Sheet name (XOR with `--sheet-id`)"},
{Name: "start-cell", Kind: "own", Type: "string", Required: "required", Desc: "Top-left A1 anchor (e.g. `A1`, `B5`; no sheet prefix — use `--sheet-id` / `--sheet-name` to select the sheet); must be a single cell, range notation not accepted; the bottom-right is inferred from CSV row/column counts", Default: "A1"},
{Name: "csv", Kind: "own", Type: "string", Required: "required", Desc: "RFC 4180 CSV text; values or formulas (a leading = is evaluated as a formula); no styles / comments / images (use +cells-set for those).", Input: []string{"file", "stdin"}},
{Name: "csv", Kind: "own", Type: "string", Required: "required", Desc: "RFC 4180 CSV text; plain values only (no formulas / styles / comments)", Input: []string{"file", "stdin"}},
{Name: "allow-overwrite", Kind: "own", Type: "bool", Required: "optional", Desc: "Allow overwriting (default true); set false to error if any target cell is non-empty", Default: "true"},
{Name: "range", Kind: "own", Type: "string", Required: "optional", Desc: "alias for --start-cell (parity with +csv-get / +cells-set, which locate with --range); a range like A1:H17 collapses to its top-left cell", Hidden: true},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
@@ -734,6 +734,15 @@ var flagDefs = map[string]commandDef{
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+recover": {
Risk: "write",
Flags: []flagDef{
{Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet URL (XOR with `--spreadsheet-token`)"},
{Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet token (XOR with `--url`)"},
{Name: "to-revision", Kind: "own", Type: "int", Required: "required", Desc: "Restore the whole spreadsheet to this revision (a revision number returned by a prior write)"},
{Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"},
},
},
"+rows-resize": {
Risk: "write",
Flags: []flagDef{

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"github.com/larksuite/cli/shortcuts/common"
)
// ─── lark_sheet_recover ───────────────────────────────────────────────
//
// Wraps:
// - recover_to_revision (write) — powers +recover
//
// Rolls the WHOLE spreadsheet back to a past revision (the undo design doc's
// "方案 B"). Unlike +undo — which is precise, per-edit, and scoped to this CLI
// link — +recover is a full-document version restore. The facade gateway
// already owns this capability (the same revert-by-revision path the web
// "history" panel drives): it submits a single RECOVER changeset that reverts
// every sheet to the target revision and produces a new revision. The CLI only
// passes the target revision; all the work stays server-side.
//
// ⚠️ Full-table overwrite: +recover discards EVERY change made after
// --to-revision, including other collaborators' (and the web UI's) edits. Use
// it only on agent scratch spreadsheets, or when a whole-document rollback is
// acceptable. For precise, this-link-only undo, use +undo instead.
var Recover = common.Shortcut{
Service: "sheets",
Command: "+recover",
Description: "Roll the whole spreadsheet back to a past revision (full-document restore; discards all later edits).",
Risk: "write",
Scopes: []string{"sheets:spreadsheet:write_only"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+recover"),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
_, err = recoverInput(runtime, token)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
input, _ := recoverInput(runtime, token)
return invokeToolDryRun(token, ToolKindWrite, "recover_to_revision", input)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
input, err := recoverInput(runtime, token)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindWrite, "recover_to_revision", input)
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
Tips: []string{
"+recover is a FULL-DOCUMENT rollback — it discards every edit made after --to-revision, including other collaborators'. For precise, this-link-only undo, use +undo instead.",
"--to-revision takes a revision number returned by a prior write (the `revision` field in the response).",
"Use --dry-run to preview the recover request before running it.",
},
}
// recoverInput builds the recover_to_revision tool body. Network-free; shared
// by Validate, DryRun, and Execute.
func recoverInput(runtime flagView, token string) (map[string]interface{}, error) {
rev := runtime.Int("to-revision")
if rev < 1 {
return nil, common.FlagErrorf("--to-revision must be a positive revision number")
}
return map[string]interface{}{
"excel_id": token,
"to_revision": rev,
}, nil
}

View File

@@ -61,6 +61,9 @@ func shortcutList() []common.Shortcut {
DropdownGet,
TableGet,
// lark_sheet_recover
Recover,
// lark_sheet_search_replace
CellsSearch,
CellsReplace,