Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shortcuts/doc/doc_media_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ type docDryRunOutput struct {
Description string `json:"description"`
API []struct {
Desc string `json:"desc"`
Method string `json:"method"`
URL string `json:"url"`
Params map[string]interface{} `json:"params"`
Body map[string]interface{} `json:"body"`
Expand Down
21 changes: 18 additions & 3 deletions shortcuts/doc/docs_fetch_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,33 @@ func dryRunFetchV2(_ context.Context, runtime *common.RuntimeContext) *common.Dr
// Validate has already accepted --doc; parseDocumentRef cannot fail here.
ref, _ := parseDocumentRef(runtime.Str("doc"))
body := buildFetchBody(runtime)
dry := common.NewDryRunAPI()
if ref.Kind == "wiki" {
dry.Desc("2-step: resolve wiki node, then fetch document")
dry.GET(wikiGetNodePath).
Desc("[1] Resolve wiki node to underlying document").
Params(map[string]interface{}{"token": ref.Token})
dry.POST("/open-apis/docs_ai/v1/documents/<obj_token from step 1>/fetch").
Desc("[2] OpenAPI: fetch resolved document").
Body(body)
return dry
}

apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", ref.Token)
return common.NewDryRunAPI().
POST(apiPath).
return dry.POST(apiPath).
Desc("OpenAPI: fetch document").
Body(body).
Set("document_id", ref.Token)
}

func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error {
ref, _ := parseDocumentRef(runtime.Str("doc"))
documentID, err := resolveDocumentID(runtime, ref)
if err != nil {
return err
}

apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", ref.Token)
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", documentID)
body := buildFetchBody(runtime)

data, err := doDocAPI(runtime, "POST", apiPath, body)
Expand Down
118 changes: 118 additions & 0 deletions shortcuts/doc/docs_fetch_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"testing"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
Expand Down Expand Up @@ -124,6 +125,37 @@ func TestDocsFetchDryRunDefaultsToV2Endpoint(t *testing.T) {
}
}

func TestDocsFetchDryRunWikiAddsResolveStep(t *testing.T) {
t.Parallel()

runtime := newFetchShortcutTestRuntime(t, "", map[string]string{
"doc": "https://tenant.feishu.cn/wiki/wikcnABC?from=wiki",
})
if err := validateFetchV2(context.Background(), runtime); err != nil {
t.Fatalf("validateFetchV2() error = %v", err)
}

dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
if len(dry.API) != 2 {
t.Fatalf("expected 2 dry-run API calls, got %d", len(dry.API))
}
if got, want := dry.API[0].Method, "GET"; got != want {
t.Fatalf("resolve method = %q, want %q", got, want)
}
if got, want := dry.API[0].URL, "/open-apis/wiki/v2/spaces/get_node"; got != want {
t.Fatalf("resolve URL = %q, want %q", got, want)
}
if got, want := dry.API[0].Params["token"], "wikcnABC"; got != want {
t.Fatalf("resolve token = %#v, want %q", got, want)
}
if got := dry.API[1].URL; strings.Contains(got, "wikcnABC") {
t.Fatalf("fetch URL used raw wiki token: %q", got)
}
if got := dry.API[1].URL; !strings.Contains(got, "<obj_token from step 1>") {
t.Fatalf("fetch URL missing resolved token placeholder: %q", got)
}
}

func TestDocsFetchAPIVersionV1StillUsesV2Endpoint(t *testing.T) {
t.Parallel()

Expand All @@ -141,6 +173,92 @@ func TestDocsFetchAPIVersionV1StillUsesV2Endpoint(t *testing.T) {
}
}

func TestDocsFetchWikiUsesResolvedDocumentToken(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())

f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-wiki"))
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/wiki/v2/spaces/get_node",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"node": map[string]interface{}{
"obj_type": "docx",
"obj_token": "doxcnREAL",
},
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/docs_ai/v1/documents/doxcnREAL/fetch",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcnREAL",
"content": "<docx>resolved</docx>",
},
},
},
})

err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--doc", "https://tenant.feishu.cn/wiki/wikcnABC",
"--format", "pretty",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
reg.Verify(t)

if got := stdout.String(); got != "<docx>resolved</docx>\n" {
t.Fatalf("stdout = %q, want resolved document content", got)
}
}

func TestDocsFetchWikiRejectsNonDocumentNode(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())

f, _, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-wiki-sheet"))
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/wiki/v2/spaces/get_node",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"node": map[string]interface{}{
"obj_type": "sheet",
"obj_token": "shtcnREAL",
},
},
},
})

err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--doc", "https://tenant.feishu.cn/wiki/wikcnABC",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected non-document validation error, got nil")
}
reg.Verify(t)
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--doc")

for _, want := range []string{"sheet", "docs +fetch requires a doc/docx wiki node", "sheets", "drive +inspect"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error missing %q: %v", want, err)
}
}
}

func TestDocsFetchMarkdownDetailDowngradesToSimple(t *testing.T) {
t.Parallel()

Expand Down
47 changes: 47 additions & 0 deletions shortcuts/doc/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type documentRef struct {
Token string
}

const wikiGetNodePath = "/open-apis/wiki/v2/spaces/get_node"

func parseDocumentRef(input string) (documentRef, error) {
raw := strings.TrimSpace(input)
if raw == "" {
Expand Down Expand Up @@ -70,6 +72,51 @@ func doDocAPI(runtime *common.RuntimeContext, method, apiPath string, body inter
return runtime.CallAPITyped(method, apiPath, nil, body)
}

func resolveDocumentID(runtime *common.RuntimeContext, ref documentRef) (string, error) {
switch ref.Kind {
case "docx", "doc":
return ref.Token, nil
case "wiki":
data, err := runtime.CallAPITyped(
"GET",
wikiGetNodePath,
map[string]interface{}{"token": ref.Token},
nil,
)
if err != nil {
return "", err
}

node := common.GetMap(data, "node")
objType := common.GetString(node, "obj_type")
objToken := common.GetString(node, "obj_token")
if objType == "" || objToken == "" {
return "", errs.NewInternalError(errs.SubtypeInvalidResponse, "wiki get_node returned incomplete node data (obj_type=%q, obj_token=%q)", objType, objToken)
}
if objType != "docx" && objType != "doc" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "wiki resolved to %q, but docs +fetch requires a doc/docx wiki node; use the matching %s shortcut or run drive +inspect to inspect the underlying type", objType, docShortcutHintForWikiType(objType)).WithParam("--doc")
}
return objToken, nil
default:
return "", errs.NewInternalError(errs.SubtypeUnknown, "unsupported document ref kind %q", ref.Kind)
}
}

func docShortcutHintForWikiType(objType string) string {
switch objType {
case "sheet":
return "sheets"
case "bitable":
return "base"
case "slides":
return "slides"
case "mindnote":
return "mindnote"
default:
return "document"
}
}

func docsSceneFromContext(ctx context.Context) string {
if ctx == nil {
return ""
Expand Down
Loading