Skip to content

Commit f8fc948

Browse files
committed
feat: add docs history shortcuts
Add docs +history-list, +history-revert, and +history-revert-status backed by docs_ai history OpenAPI endpoints. Document the safe history workflow and extend dry-run/live E2E coverage for the new shortcuts.
1 parent 39d60cb commit f8fc948

10 files changed

Lines changed: 897 additions & 16 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Thumbs.db
2727
# Go
2828
docs/ref
2929
docs/
30+
!tests/cli_e2e/docs/
31+
!tests/cli_e2e/docs/*.go
32+
!tests/cli_e2e/docs/*.md
3033
vendor/
3134

3235

shortcuts/doc/docs_history.go

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package doc
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/larksuite/cli/errs"
14+
"github.com/larksuite/cli/internal/validate"
15+
"github.com/larksuite/cli/shortcuts/common"
16+
)
17+
18+
type docsHistoryListSpec struct {
19+
Doc documentRef
20+
PageSize int
21+
PageToken string
22+
}
23+
24+
type docsHistoryRevertSpec struct {
25+
Doc documentRef
26+
HistoryVersionID string
27+
WaitTimeoutMs int
28+
}
29+
30+
type docsHistoryRevertStatusSpec struct {
31+
Doc documentRef
32+
TaskID string
33+
}
34+
35+
func parseDocsHistoryDocRef(raw, shortcut string) (documentRef, error) {
36+
ref, err := parseDocumentRef(raw)
37+
if err != nil {
38+
return documentRef{}, err
39+
}
40+
if ref.Kind == "doc" {
41+
return documentRef{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "docs %s only supports docx documents; use a docx token/URL or a wiki URL that resolves to docx", shortcut).WithParam("--doc")
42+
}
43+
return ref, nil
44+
}
45+
46+
func validateDocsHistoryPageSize(pageSize int) error {
47+
if pageSize < 1 || pageSize > 20 {
48+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --page-size %d: must be between 1 and 20", pageSize).WithParam("--page-size")
49+
}
50+
return nil
51+
}
52+
53+
func validateDocsHistoryVersionID(historyVersionID string) error {
54+
version, err := strconv.ParseInt(strings.TrimSpace(historyVersionID), 10, 64)
55+
if err != nil || version <= 0 {
56+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--history-version-id must be a positive integer string returned by docs +history-list").WithParam("--history-version-id")
57+
}
58+
return nil
59+
}
60+
61+
func validateDocsHistoryWaitTimeout(timeoutMs int) error {
62+
if timeoutMs < 0 || timeoutMs > 30000 {
63+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --wait-timeout-ms %d: must be between 0 and 30000", timeoutMs).WithParam("--wait-timeout-ms")
64+
}
65+
return nil
66+
}
67+
68+
func docsHistoryListParams(spec docsHistoryListSpec) map[string]interface{} {
69+
params := map[string]interface{}{
70+
"page_size": spec.PageSize,
71+
}
72+
if spec.PageToken != "" {
73+
params["page_token"] = spec.PageToken
74+
}
75+
return params
76+
}
77+
78+
func docsHistoryRevertBody(spec docsHistoryRevertSpec) map[string]interface{} {
79+
return map[string]interface{}{
80+
"history_version_id": spec.HistoryVersionID,
81+
"wait_timeout_ms": spec.WaitTimeoutMs,
82+
}
83+
}
84+
85+
func docsHistoryStatusParams(spec docsHistoryRevertStatusSpec) map[string]interface{} {
86+
return map[string]interface{}{
87+
"task_id": spec.TaskID,
88+
}
89+
}
90+
91+
func docsHistoryAPIPath(docToken, suffix string) string {
92+
return fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/%s", validate.EncodePathSegment(docToken), suffix)
93+
}
94+
95+
var DocsHistoryList = common.Shortcut{
96+
Service: "docs",
97+
Command: "+history-list",
98+
Description: "List Lark document history versions",
99+
Risk: "read",
100+
Scopes: []string{"docx:document:readonly"},
101+
AuthTypes: []string{"user", "bot"},
102+
PostMount: installDocsShortcutHelp("+history-list"),
103+
Flags: []common.Flag{
104+
{Name: "doc", Desc: "document URL or token", Required: true},
105+
{Name: "page-size", Type: "int", Default: "20", Desc: "history entries to return, range 1-20"},
106+
{Name: "page-token", Desc: "pagination token from the previous page's page_token"},
107+
},
108+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
109+
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list"); err != nil {
110+
return err
111+
}
112+
return validateDocsHistoryPageSize(runtime.Int("page-size"))
113+
},
114+
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
115+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list")
116+
spec := docsHistoryListSpec{
117+
Doc: ref,
118+
PageSize: runtime.Int("page-size"),
119+
PageToken: strings.TrimSpace(runtime.Str("page-token")),
120+
}
121+
return common.NewDryRunAPI().
122+
Desc("OpenAPI: list document history versions").
123+
GET("/open-apis/docs_ai/v1/documents/:document_id/histories").
124+
Set("document_id", spec.Doc.Token).
125+
Params(docsHistoryListParams(spec))
126+
},
127+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
128+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-list")
129+
spec := docsHistoryListSpec{
130+
Doc: ref,
131+
PageSize: runtime.Int("page-size"),
132+
PageToken: strings.TrimSpace(runtime.Str("page-token")),
133+
}
134+
135+
data, err := runtime.CallAPITyped(
136+
http.MethodGet,
137+
docsHistoryAPIPath(spec.Doc.Token, "histories"),
138+
docsHistoryListParams(spec),
139+
nil,
140+
)
141+
if err != nil {
142+
return err
143+
}
144+
runtime.OutRaw(data, nil)
145+
return nil
146+
},
147+
}
148+
149+
var DocsHistoryRevert = common.Shortcut{
150+
Service: "docs",
151+
Command: "+history-revert",
152+
Description: "Revert a Lark document to a historical version",
153+
Risk: "write",
154+
Scopes: []string{"docx:document:write_only", "docx:document:readonly"},
155+
AuthTypes: []string{"user", "bot"},
156+
PostMount: installDocsShortcutHelp("+history-revert"),
157+
Flags: []common.Flag{
158+
{Name: "doc", Desc: "document URL or token", Required: true},
159+
{Name: "history-version-id", Desc: "history_version_id from docs +history-list to revert to", Required: true},
160+
{Name: "wait-timeout-ms", Type: "int", Default: "30000", Desc: "milliseconds to wait for revert completion before returning, range 0-30000"},
161+
},
162+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
163+
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert"); err != nil {
164+
return err
165+
}
166+
if err := validateDocsHistoryVersionID(runtime.Str("history-version-id")); err != nil {
167+
return err
168+
}
169+
return validateDocsHistoryWaitTimeout(runtime.Int("wait-timeout-ms"))
170+
},
171+
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
172+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert")
173+
spec := docsHistoryRevertSpec{
174+
Doc: ref,
175+
HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")),
176+
WaitTimeoutMs: runtime.Int("wait-timeout-ms"),
177+
}
178+
return common.NewDryRunAPI().
179+
Desc("OpenAPI: revert document history").
180+
POST("/open-apis/docs_ai/v1/documents/:document_id/history/revert").
181+
Set("document_id", spec.Doc.Token).
182+
Body(docsHistoryRevertBody(spec))
183+
},
184+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
185+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert")
186+
spec := docsHistoryRevertSpec{
187+
Doc: ref,
188+
HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")),
189+
WaitTimeoutMs: runtime.Int("wait-timeout-ms"),
190+
}
191+
192+
data, err := runtime.CallAPITyped(
193+
http.MethodPost,
194+
docsHistoryAPIPath(spec.Doc.Token, "history/revert"),
195+
nil,
196+
docsHistoryRevertBody(spec),
197+
)
198+
if err != nil {
199+
return err
200+
}
201+
runtime.OutRaw(data, nil)
202+
return nil
203+
},
204+
}
205+
206+
var DocsHistoryRevertStatus = common.Shortcut{
207+
Service: "docs",
208+
Command: "+history-revert-status",
209+
Description: "Get Lark document history revert task status",
210+
Risk: "read",
211+
Scopes: []string{"docx:document:readonly"},
212+
AuthTypes: []string{"user", "bot"},
213+
PostMount: installDocsShortcutHelp("+history-revert-status"),
214+
Flags: []common.Flag{
215+
{Name: "doc", Desc: "document URL or token", Required: true},
216+
{Name: "task-id", Desc: "task_id returned by docs +history-revert", Required: true},
217+
},
218+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
219+
if _, err := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status"); err != nil {
220+
return err
221+
}
222+
if strings.TrimSpace(runtime.Str("task-id")) == "" {
223+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--task-id is required").WithParam("--task-id")
224+
}
225+
return nil
226+
},
227+
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
228+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status")
229+
spec := docsHistoryRevertStatusSpec{
230+
Doc: ref,
231+
TaskID: strings.TrimSpace(runtime.Str("task-id")),
232+
}
233+
return common.NewDryRunAPI().
234+
Desc("OpenAPI: get document history revert status").
235+
GET("/open-apis/docs_ai/v1/documents/:document_id/history/revert_status").
236+
Set("document_id", spec.Doc.Token).
237+
Params(docsHistoryStatusParams(spec))
238+
},
239+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
240+
ref, _ := parseDocsHistoryDocRef(runtime.Str("doc"), "+history-revert-status")
241+
spec := docsHistoryRevertStatusSpec{
242+
Doc: ref,
243+
TaskID: strings.TrimSpace(runtime.Str("task-id")),
244+
}
245+
246+
data, err := runtime.CallAPITyped(
247+
http.MethodGet,
248+
docsHistoryAPIPath(spec.Doc.Token, "history/revert_status"),
249+
docsHistoryStatusParams(spec),
250+
nil,
251+
)
252+
if err != nil {
253+
return err
254+
}
255+
runtime.OutRaw(data, nil)
256+
return nil
257+
},
258+
}

0 commit comments

Comments
 (0)