diff --git a/shortcuts/sheets/data/flag-defs.json b/shortcuts/sheets/data/flag-defs.json index c123aae2..5115b6eb 100644 --- a/shortcuts/sheets/data/flag-defs.json +++ b/shortcuts/sheets/data/flag-defs.json @@ -80,6 +80,32 @@ } ] }, + "+revision-get": { + "risk": "read", + "flags": [ + { + "name": "url", + "kind": "public", + "type": "string", + "required": "xor", + "desc": "Spreadsheet locator" + }, + { + "name": "spreadsheet-token", + "kind": "public", + "type": "string", + "required": "xor", + "desc": "Spreadsheet locator" + }, + { + "name": "dry-run", + "kind": "system", + "type": "bool", + "required": "optional", + "desc": "" + } + ] + }, "+sheet-create": { "risk": "write", "flags": [ @@ -4941,6 +4967,14 @@ "required": "xor", "desc": "Spreadsheet locator" }, + { + "name": "count", + "kind": "own", + "type": "int", + "required": "optional", + "desc": "Number of user undo stack entries to undo sequentially. Defaults to 1, maximum 20. Entries are undone from newest to oldest; the actual count is returned in undone.", + "default": "1" + }, { "name": "dry-run", "kind": "system", @@ -4949,5 +4983,39 @@ "desc": "" } ] + }, + "+changeset-get": { + "risk": "read", + "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": "start-revision", + "kind": "own", + "type": "int", + "required": "required", + "desc": "Start version (CS revision); the before baseline for review (must be >= 1)" + }, + { + "name": "end-revision", + "kind": "own", + "type": "int", + "required": "optional", + "desc": "End version (CS revision); defaults to the latest revision. Gap (end-start+1) must be <= 20", + "default": "-1" + } + ] } } diff --git a/shortcuts/sheets/flag_defs_gen.go b/shortcuts/sheets/flag_defs_gen.go index a3e9ff91..4e705c0e 100644 --- a/shortcuts/sheets/flag_defs_gen.go +++ b/shortcuts/sheets/flag_defs_gen.go @@ -1000,6 +1000,7 @@ var flagDefs = map[string]commandDef{ Flags: []flagDef{ {Name: "url", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"}, {Name: "spreadsheet-token", Kind: "public", Type: "string", Required: "xor", Desc: "Spreadsheet locator"}, + {Name: "count", Kind: "own", Type: "int", Required: "optional", Desc: "Number of user undo stack entries to undo sequentially. Defaults to 1, maximum 20. Entries are undone from newest to oldest; the actual count is returned in undone.", Default: "1"}, {Name: "dry-run", Kind: "system", Type: "bool", Required: "optional"}, }, }, diff --git a/shortcuts/sheets/lark_sheet_changeset.go b/shortcuts/sheets/lark_sheet_changeset.go index 455e2fb8..27553309 100644 --- a/shortcuts/sheets/lark_sheet_changeset.go +++ b/shortcuts/sheets/lark_sheet_changeset.go @@ -75,14 +75,14 @@ func changesetRevisions(runtime flagView) (start int, end int, err error) { start = runtime.Int("start-revision") end = runtime.Int("end-revision") if start < 1 { - return 0, 0, common.FlagErrorf("--start-revision must be >= 1") + return 0, 0, sheetsValidationForFlag("start-revision", "--start-revision must be >= 1") } if end > 0 { if end < start { - return 0, 0, common.FlagErrorf("--end-revision (%d) must be >= --start-revision (%d)", end, start) + return 0, 0, sheetsValidationForFlag("end-revision", "--end-revision (%d) must be >= --start-revision (%d)", end, start) } if end-start+1 > changesetMaxRevGap { - return 0, 0, common.FlagErrorf("version gap exceeds limit %d (start=%d, end=%d)", changesetMaxRevGap, start, end) + return 0, 0, sheetsValidationForFlag("end-revision", "version gap exceeds limit %d (start=%d, end=%d)", changesetMaxRevGap, start, end) } } return start, end, nil diff --git a/shortcuts/sheets/lark_sheet_undo.go b/shortcuts/sheets/lark_sheet_undo.go index 8327578e..a768a1a9 100644 --- a/shortcuts/sheets/lark_sheet_undo.go +++ b/shortcuts/sheets/lark_sheet_undo.go @@ -17,21 +17,30 @@ var Undo = common.Shortcut{ Scopes: []string{"sheets:spreadsheet:write_only"}, AuthTypes: []string{"user"}, HasFormat: true, - Flags: historyLocatorFlags(), + // History shortcuts keep locator flags hand-written because they share + // revision/revert helpers; keep --count here in sync with sheet-skill-spec. + Flags: append(historyLocatorFlags(), + common.Flag{Name: "count", Type: "int", Default: "1", Desc: "Number of user undo stack entries to undo sequentially (1-20)."}, + ), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { - _, err := resolveSpreadsheetToken(runtime) - return err + if _, err := resolveSpreadsheetToken(runtime); err != nil { + return err + } + if runtime.Int("count") < 1 || runtime.Int("count") > 20 { + return sheetsValidationForFlag("count", "--count must be between 1 and 20") + } + return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { token, _ := resolveSpreadsheetToken(runtime) - return invokeToolDryRun(token, ToolKindWrite, "undo_last", undoInput(token)) + return invokeToolDryRun(token, ToolKindWrite, "undo_last", undoInput(runtime, token)) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { token, err := resolveSpreadsheetTokenExec(runtime) if err != nil { return err } - out, err := callTool(ctx, runtime, token, ToolKindWrite, "undo_last", undoInput(token)) + out, err := callTool(ctx, runtime, token, ToolKindWrite, "undo_last", undoInput(runtime, token)) if err != nil { return err } @@ -40,8 +49,11 @@ var Undo = common.Shortcut{ }, } -func undoInput(token string) map[string]interface{} { +func undoInput(runtime *common.RuntimeContext, token string) map[string]interface{} { + // Always send count, including the default 1, so dry-run mirrors the exact + // request body sent by Execute. return map[string]interface{}{ "excel_id": token, + "count": runtime.Int("count"), } } diff --git a/shortcuts/sheets/lark_sheet_undo_test.go b/shortcuts/sheets/lark_sheet_undo_test.go index 86ea1fc3..e796de73 100644 --- a/shortcuts/sheets/lark_sheet_undo_test.go +++ b/shortcuts/sheets/lark_sheet_undo_test.go @@ -3,7 +3,10 @@ package sheets -import "testing" +import ( + "strings" + "testing" +) func TestUndo_DryRun(t *testing.T) { t.Parallel() @@ -18,14 +21,15 @@ func TestUndo_DryRun(t *testing.T) { got := decodeToolInput(t, body, "undo_last") assertInputEquals(t, got, map[string]interface{}{ "excel_id": testToken, + "count": float64(1), }) } func TestExecute_Undo(t *testing.T) { t.Parallel() - stub := toolOutputStub(testToken, "write", `{"undone":1,"op_id":"op-1","top_doc_revision":2,"new_revision":3}`) - out, err := runShortcutWithStubs(t, Undo, []string{"--url", testURL, "--as", "user"}, stub) + stub := toolOutputStub(testToken, "write", `{"undone":3,"op_ids":["op-3","op-2","op-1"],"results":[{"op_id":"op-3"},{"op_id":"op-2"},{"op_id":"op-1"}]}`) + out, err := runShortcutWithStubs(t, Undo, []string{"--url", testURL, "--count", "3", "--as", "user"}, stub) if err != nil { t.Fatalf("execute failed: %v\nout=%s", err, out) } @@ -34,10 +38,23 @@ func TestExecute_Undo(t *testing.T) { input := decodeToolInput(t, body, "undo_last") assertInputEquals(t, input, map[string]interface{}{ "excel_id": testToken, + "count": float64(3), }) data := decodeEnvelopeData(t, out) - if data["undone"].(float64) != 1 || data["op_id"] != "op-1" { + if data["undone"].(float64) != 3 { t.Fatalf("unexpected output data: %#v", data) } } + +func TestUndo_ValidateCount(t *testing.T) { + t.Parallel() + + _, err := runShortcutWithStubs(t, Undo, []string{"--url", testURL, "--count", "21", "--as", "user"}) + if err == nil { + t.Fatal("expected count validation error") + } + if !strings.Contains(err.Error(), "--count must be between 1 and 20") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/skills/lark-sheets/SKILL.md b/skills/lark-sheets/SKILL.md index 1ddc8688..3cfe059d 100644 --- a/skills/lark-sheets/SKILL.md +++ b/skills/lark-sheets/SKILL.md @@ -65,6 +65,8 @@ metadata: | 查找 / 替换文本 | `+cells-search`(找,关键字用 `--find`)、`+cells-replace`(替换) | `lark-sheets-search-replace` | `+cells-find`、`+find`、`--query` | | 看子表结构(合并 / 行高列宽 / 冻结 / 隐藏) | `+sheet-info` | `lark-sheets-sheet-structure` | `+sheet-get`、`+structure-get`、`+sheet-structure-get` | | 看工作簿 / 子表清单 | `+workbook-info` | `lark-sheets-workbook` | `+sheet-list`、`+workbook-get`、`+workbook-list` | +| 复核某次(AI)编辑改了什么 / 取两个版本间的变更 | `+changeset-get --start-revision <编辑前版本>`(省略 `--end-revision` 取到最新;版本差 ≤ 20) | — | +| 取当前文档 revision(版本号) | `+revision-get` | `lark-sheets-workbook` | — | | 导出 xlsx / 单表 csv | `+workbook-export` | `lark-sheets-workbook` | — | | 导入本地 xlsx/xls/csv 文件为飞书电子表格 | `+workbook-import --file ./x.xlsx`(本地表格文件 → 飞书电子表格的正解;仅要导成多维表格 bitable 时才用 `drive +import --type bitable`) | `lark-sheets-workbook` | `drive +import`(导电子表格时绕了 drive 通道、还要多给 `--type`,应直接用 `+workbook-import`)、把 .xlsx 在本地读成数据再 `+workbook-create` 重灌(多此一举,应直接 `+workbook-import`) | | 清除内容 / 格式 | `+cells-clear`(范围维度用 `--scope`,取值 content / formats / all) | `lark-sheets-range-operations` | `--type` | @@ -157,6 +159,7 @@ metadata: | [Lark Sheet Sparkline](references/lark-sheets-sparkline.md) | 管理飞书表格中的迷你图(折线迷你图、柱形迷你图、胜负迷你图)。当用户需要在单元格内嵌入小型图表来展示数据趋势时使用。也适用于"趋势线"、"单元格内图表"、"迷你图"等场景。注意:不等同于被禁用的 SPARKLINE() 公式函数。 | | [Lark Sheet Float Image](references/lark-sheets-float-image.md) | 管理飞书表格中的浮动图片。当用户需要在表格中插入浮动图片、调整图片位置和大小、查看已有浮动图片、删除图片时使用。也适用于"插入图片"、"添加 logo"、"放一张图"等场景。注意:如果用户需要将图片嵌入到某个单元格内部(单元格图片),请阅读 lark-sheets-write-cells。 | | [Lark Sheet History](references/lark-sheets-history.md) | 查询飞书表格的历史版本、回滚到指定版本,或撤销当前用户最近一次 AI 工具写入。当用户需要查看编辑历史、回滚历史版本、查询回滚状态,或说“撤销我刚才的修改 / undo 上一步 AI 写入”时使用。历史回滚为异步全表恢复;用户维度 undo 只撤当前用户自己的 undo 栈顶。仅针对飞书表格。 | +| [Lark Sheet Changeset](references/lark-sheets-changeset.md) | 读取两个版本(CS revision)之间的 changeset(原始变更操作清单),用于复核某次编辑——尤其是 AI 编辑——是否真实满足用户诉求。传入起始版本(编辑前基线),可选结束版本(省略取最新),版本差上限 20;返回里最外层带当前表格最新版本号。当用户需要"看看这次改了什么"、"核对 AI 改动"、"对比两个版本的变更"时使用。 | ## 公共 flag 速查 diff --git a/skills/lark-sheets/references/lark-sheets-history.md b/skills/lark-sheets/references/lark-sheets-history.md index b8b3a870..14b6c32c 100644 --- a/skills/lark-sheets/references/lark-sheets-history.md +++ b/skills/lark-sheets/references/lark-sheets-history.md @@ -6,9 +6,9 @@ 回滚(revert)把电子表格的当前内容覆盖回某个历史版本——这是一个**写入 / 不可逆**操作,且为**异步**:发起后立即返回受理标识,真正的回滚在后台进行,需通过状态查询轮询最终结果(进行中 / 成功 / 失败)。 -`+undo` 是另一类撤销:撤销当前 CLI 用户在该电子表格里最近一次由 AI 工具产生、且尚未撤销的写入。每个用户有独立 undo 栈;多人编辑同一篇文档时,只撤当前用户的栈顶,不撤其他用户的操作。它和历史版本回滚不同,不需要选择 `history_version_id`,也不是把整表恢复到某个历史快照。 +`+undo` 是另一类撤销:撤销当前 CLI 用户在该电子表格里最近一次或最近多次由 AI 工具产生、且尚未撤销的写入。每个用户有独立 undo 栈;多人编辑同一篇文档时,只撤当前用户自己的 undo 栈,不撤其他用户的操作。它和历史版本回滚不同,不需要选择 `history_version_id`,也不是把整表恢复到某个历史快照。 -`+history-list` 读取版本列表以挑选目标;`+history-revert` 发起回滚;`+history-revert-status` 轮询回滚结果;`+undo` 撤销当前用户的最近一次 AI 工具写入。 +`+history-list` 读取版本列表以挑选目标;`+history-revert` 发起回滚;`+history-revert-status` 轮询回滚结果;`+undo` 撤销当前用户最近的 AI 工具写入。若只是想拿**当前文档版本号(revision)**当作 recover / undo / `+changeset-get` 的起点锚点,直接用 `+revision-get` 更轻量。 ## 使用场景 @@ -19,11 +19,11 @@ | 查看历史版本列表 | `+history-list` | 返回 `minor_histories`,每条含 `history_version_id` / `create_time` / `action` / `all_block_revision` 四个字段;支持向前分页(可选 `--end-version`) | | 回滚到指定历史版本 | `+history-revert` | 传入 `--history-version-id`;异步受理,返回可查询标识 | | 查询回滚状态 | `+history-revert-status` | 传入 `--transaction-id`(取自 `+history-revert` 的异步受理标识);轮询某次回滚的进行中 / 成功 / 失败状态 | -| 撤销自己最近一次 AI 写入 | `+undo` | 只需 spreadsheet 定位;撤当前用户 undo 栈顶;支持普通单元格/结构写入,以及图表、透视表 update 的反向操作 | +| 撤销自己最近的 AI 写入 | `+undo` | 只需 spreadsheet 定位;默认撤当前用户 undo 栈顶;可用 `--count` 连续撤销多步;支持普通单元格/结构写入,以及图表、透视表 update 的反向操作 | 典型工作流:`+history-list` 拿到目标版本的 `history_version_id`(必要时翻页拉取更早历史)→ `+history-revert` 发起回滚并取回 `transaction_id` → `+history-revert-status --transaction-id ` 轮询直到成功或失败。 -`+undo` 的典型工作流更短:执行某个 AI 写入工具后,如果用户要求撤销刚才自己的修改,直接运行 `+undo`。返回 `undone=1` 表示已撤销一条当前用户栈顶;返回 `undone=0` 且 `reason=undo_stack_empty` 表示当前用户没有可撤销项。 +`+undo` 的典型工作流更短:执行某个 AI 写入工具后,如果用户要求撤销刚才自己的修改,直接运行 `+undo`。返回 `undone=1` 表示已撤销一条当前用户栈顶;传 `--count N` 时会按当前用户栈顶从新到旧连续撤销,返回的 `undone` 是实际撤销数量。返回 `undone=0` 且 `reason=undo_stack_empty` 表示当前用户没有可撤销项。 **注意事项(必须了解)**: - **回滚是写入 / 不可逆操作**:会用历史版本内容覆盖当前表格,发起前请确认目标 `history_version_id` 正确。 @@ -32,6 +32,7 @@ - **历史是工作簿级**:定位只需 `--url` / `--spreadsheet-token`(XOR),不需要子表选择器。 - **`+history-list` 倒序分页**:首次查省略 `--end-version`,返回最新一页;若响应里附带 `next_end_version` 与 `has_more=true`,把 `next_end_version` 作为下一次的 `--end-version` 即可继续向更早翻页;当响应**不包含**这两个字段时表示已到最早一页,不必再翻。 - **`+undo` 是用户维度**:只撤当前登录用户的 undo 栈顶;同一文档里其他用户的写入不会被撤销。 +- **`+undo --count` 是顺序多步撤销**:从当前用户最新栈项开始逐条撤销,最多 20 步;如果可撤销项不足或中途遇到不可撤销项,会停止并返回实际 `undone` 数量与 `reason`。 - **`+undo` 不区分 session / agent**:同一用户的不同 CLI 会话或 agent 共用同一个用户 undo 栈。 - **`+undo` 不进入 `+batch-update`**:撤销本身依赖用户栈状态,不能作为批量子操作嵌套执行。 @@ -74,11 +75,13 @@ _公共:URL/token(无 sheet 定位) · 系统:`--dry-run`_ _公共:URL/token(无 sheet 定位) · 系统:`--dry-run`_ -_仅含公共 / 系统 flag。_ +| Flag | Type | 必填 | 说明 | +| --- | --- | --- | --- | +| `--count` | int | optional | 连续撤销的用户 undo 栈项数量,默认 1,最大 20。按当前用户栈顶从新到旧顺序撤销,实际撤销数量以返回的 undone 为准。 | ## Examples -公共定位:所有 shortcut 顶部排列 `--url` / `--spreadsheet-token`(XOR,二选一)。`+history-revert` 用 `--history-version-id`(取自 `+history-list`);`+history-revert-status` 用 `--transaction-id`(取自 `+history-revert` 的异步受理标识)。`+undo` 不需要 sheet-id,也不需要 history version。 +公共定位:所有 shortcut 顶部排列 `--url` / `--spreadsheet-token`(XOR,二选一)。`+history-revert` 用 `--history-version-id`(取自 `+history-list`);`+history-revert-status` 用 `--transaction-id`(取自 `+history-revert` 的异步受理标识)。`+undo` 不需要 sheet-id,也不需要 history version;连续撤销多步时传 `--count`。 ### `+history-list` @@ -113,6 +116,9 @@ lark-cli sheets +history-revert-status --url "https://sample.feishu.cn/sheets/SH # 撤销当前用户最近一次 AI 工具写入 lark-cli sheets +undo --url "https://sample.feishu.cn/sheets/SHTxxxxxx" +# 连续撤销当前用户最近 3 次 AI 工具写入;实际撤销数量以返回的 undone 为准 +lark-cli sheets +undo --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --count 3 + # 先预览将调用的底层 undo_last 请求 -lark-cli sheets +undo --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --dry-run +lark-cli sheets +undo --url "https://sample.feishu.cn/sheets/SHTxxxxxx" --count 3 --dry-run ```