Files
larksuite-cli/shortcuts/doc/docs_fetch_v2.go
SunPeiYang996 1135fc2767 fix: remove unsupported docs fetch text format (#1109)
Change-Id: I1241ba6feede813c5bfec3e6820bc0886e39dc68
2026-05-26 21:55:42 +08:00

198 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package doc
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"github.com/larksuite/cli/shortcuts/common"
)
// v2FetchFlags returns the flag definitions for the v2 (OpenAPI) fetch path.
func v2FetchFlags() []common.Flag {
return []common.Flag{
{Name: "doc-format", Desc: "content format", Hidden: true, Default: "xml", Enum: []string{"xml", "markdown"}},
{Name: "detail", Desc: "export detail level: simple (read-only) | with-ids (block IDs for cross-referencing) | full (all attrs for editing)", Hidden: true, Default: "simple", Enum: []string{"simple", "with-ids", "full"}},
{Name: "revision-id", Desc: "document revision (-1 = latest)", Hidden: true, Type: "int", Default: "-1"},
{Name: "scope", Desc: "partial read scope: outline | range | keyword | section (omit to read whole doc)", Default: "full", Enum: []string{"full", "outline", "range", "keyword", "section"}},
{Name: "start-block-id", Desc: "range/section mode: start (anchor) block id"},
{Name: "end-block-id", Desc: "range mode: end block id; \"-1\" = to end of document"},
{Name: "keyword", Desc: "keyword mode: substring + regex match (case-insensitive); use '|' for OR branches, e.g. 'foo|bar' or 'bug|缺陷'"},
{Name: "context-before", Desc: "range/keyword/section mode: sibling blocks before match", Type: "int", Default: "0"},
{Name: "context-after", Desc: "range/keyword/section mode: sibling blocks after match", Type: "int", Default: "0"},
{Name: "max-depth", Desc: "outline: heading level cap; range/keyword/section: block subtree depth (-1 = unlimited)", Type: "int", Default: "-1"},
}
}
// validateFetchV2 is the Validate hook for the v2 fetch path. It runs before
// --dry-run so that invalid input fails with a structured exit code (2) and
// JSON envelope instead of slipping through dry-run as a "success".
func validateFetchV2(_ context.Context, runtime *common.RuntimeContext) error {
if _, err := parseDocumentRef(runtime.Str("doc")); err != nil {
return common.FlagErrorf("invalid --doc: %v", err)
}
if err := validateFetchDetail(runtime); err != nil {
return err
}
if err := validateReadModeFlags(runtime); err != nil {
return err
}
return nil
}
func dryRunFetchV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
// Validate has already accepted --doc; parseDocumentRef cannot fail here.
ref, _ := parseDocumentRef(runtime.Str("doc"))
body := buildFetchBody(runtime)
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", ref.Token)
return common.NewDryRunAPI().
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"))
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s/fetch", ref.Token)
body := buildFetchBody(runtime)
data, err := doDocAPI(runtime, "POST", apiPath, body)
if err != nil {
return err
}
runtime.OutFormatRaw(data, nil, func(w io.Writer) {
if doc, ok := data["document"].(map[string]interface{}); ok {
if content, ok := doc["content"].(string); ok {
fmt.Fprintln(w, content)
}
}
})
return nil
}
func buildFetchBody(runtime *common.RuntimeContext) map[string]interface{} {
body := map[string]interface{}{
"format": runtime.Str("doc-format"),
}
if v := runtime.Int("revision-id"); v > 0 {
body["revision_id"] = v
}
detail := runtime.Str("detail")
switch detail {
case "", "simple":
body["export_option"] = map[string]interface{}{
"export_block_id": false,
"export_style_attrs": false,
"export_cite_extra_data": false,
}
case "with-ids":
body["export_option"] = map[string]interface{}{
"export_block_id": true,
}
case "full":
body["export_option"] = map[string]interface{}{
"export_block_id": true,
"export_style_attrs": true,
"export_cite_extra_data": true,
}
}
if ro := buildReadOption(runtime); ro != nil {
body["read_option"] = ro
}
injectDocsScene(runtime, body)
return body
}
// buildReadOption 拼装 read_option JSONfull/空模式返回 nil让服务端走默认全文路径。
func buildReadOption(runtime *common.RuntimeContext) map[string]interface{} {
mode := strings.TrimSpace(runtime.Str("scope"))
if mode == "" || mode == "full" {
return nil
}
ro := map[string]interface{}{"read_mode": mode}
if v := strings.TrimSpace(runtime.Str("start-block-id")); v != "" {
ro["start_block_id"] = v
}
if v := strings.TrimSpace(runtime.Str("end-block-id")); v != "" {
ro["end_block_id"] = v
}
if v := strings.TrimSpace(runtime.Str("keyword")); v != "" {
ro["keyword"] = v
}
if v := runtime.Int("context-before"); v > 0 {
ro["context_before"] = strconv.Itoa(v)
}
if v := runtime.Int("context-after"); v > 0 {
ro["context_after"] = strconv.Itoa(v)
}
if v := runtime.Int("max-depth"); v >= 0 {
ro["max_depth"] = strconv.Itoa(v)
}
return ro
}
// validateFetchDetail 非 xml 格式markdown不承载 block_id 与样式属性,拒绝 with-ids/full。
func validateFetchDetail(runtime *common.RuntimeContext) error {
format := strings.TrimSpace(runtime.Str("doc-format"))
detail := strings.TrimSpace(runtime.Str("detail"))
if format == "" || format == "xml" {
return nil
}
if detail == "with-ids" || detail == "full" {
return common.FlagErrorf("--detail %s is only supported with --doc-format xml; %s output has no block ids, use --detail simple or switch to --doc-format xml", detail, format)
}
return nil
}
// validateReadModeFlags 客户端前置校验,服务端也会再校验一次。
func validateReadModeFlags(runtime *common.RuntimeContext) error {
mode := strings.TrimSpace(runtime.Str("scope"))
if mode == "" || mode == "full" {
return nil
}
if v := runtime.Int("context-before"); v < 0 {
return common.FlagErrorf("--context-before must be >= 0, got %d", v)
}
if v := runtime.Int("context-after"); v < 0 {
return common.FlagErrorf("--context-after must be >= 0, got %d", v)
}
if v := runtime.Int("max-depth"); v < -1 {
return common.FlagErrorf("--max-depth must be >= -1, got %d", v)
}
switch mode {
case "outline":
return nil
case "range":
if strings.TrimSpace(runtime.Str("start-block-id")) == "" &&
strings.TrimSpace(runtime.Str("end-block-id")) == "" {
return common.FlagErrorf("range mode requires --start-block-id or --end-block-id")
}
return nil
case "keyword":
if strings.TrimSpace(runtime.Str("keyword")) == "" {
return common.FlagErrorf("keyword mode requires --keyword")
}
return nil
case "section":
if strings.TrimSpace(runtime.Str("start-block-id")) == "" {
return common.FlagErrorf("section mode requires --start-block-id")
}
return nil
default:
return common.FlagErrorf("invalid --scope %q", mode)
}
}