Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion shortcuts/doc/docs_fetch_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)

const docsFetchExtraParam = `{"enable_user_cite_reference_map":true}`

// v2FetchFlags returns the flag definitions for the v2 (OpenAPI) fetch path.
func v2FetchFlags() []common.Flag {
return []common.Flag{
Expand Down Expand Up @@ -88,7 +90,8 @@ func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error {

func buildFetchBody(runtime *common.RuntimeContext) map[string]interface{} {
body := map[string]interface{}{
"format": effectiveFetchFormat(runtime),
"format": effectiveFetchFormat(runtime),
"extra_param": docsFetchExtraParam,
}
if v := runtime.Int("revision-id"); v > 0 {
body["revision_id"] = v
Expand Down
39 changes: 39 additions & 0 deletions shortcuts/doc/docs_fetch_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,44 @@ func TestAddFetchDetailDowngradeWarningNoops(t *testing.T) {
}
}

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

runtime := newFetchBodyTestRuntime(context.Background())

body := buildFetchBody(runtime)
extraParam, ok := body["extra_param"].(string)
if !ok || extraParam == "" {
t.Fatalf("extra_param = %#v, want JSON string", body["extra_param"])
}
var got map[string]bool
if err := json.Unmarshal([]byte(extraParam), &got); err != nil {
t.Fatalf("decode extra_param %q: %v", extraParam, err)
}
if got["enable_user_cite_reference_map"] != true {
t.Fatalf("enable_user_cite_reference_map = %#v, want true in %#v", got["enable_user_cite_reference_map"], got)
}
if _, ok := got["return_html5_block_data"]; ok {
t.Fatalf("extra_param should not request html5 block data: %#v", got)
}
if _, ok := got["reference_map_mode"]; ok {
t.Fatalf("extra_param should not use legacy reference_map_mode: %#v", got)
}
if len(got) != 1 {
t.Fatalf("extra_param should only contain fetch reference_map toggle: %#v", got)
}
}

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

for _, flag := range v2FetchFlags() {
if flag.Name == "reference-map" {
t.Fatal("fetch should not expose reference-map flag")
}
}
}

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

Expand Down Expand Up @@ -904,6 +942,7 @@ func newUpdateBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
cmd.Flags().String("command", "append", "")
cmd.Flags().Int("revision-id", 0, "")
cmd.Flags().String("content", "<p>hello</p>", "")
cmd.Flags().String("reference-map", "", "")
cmd.Flags().String("pattern", "", "")
cmd.Flags().String("block-id", "", "")
cmd.Flags().String("src-block-ids", "", "")
Expand Down
122 changes: 122 additions & 0 deletions shortcuts/doc/docs_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package doc

import (
"context"
"errors"
"strings"
"testing"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -61,6 +63,116 @@ func TestDocsUpdateDryRunIgnoresAPIVersionCompatFlag(t *testing.T) {
}
}

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

var flag common.Flag
for _, candidate := range v2UpdateFlags() {
if candidate.Name == "reference-map" {
flag = candidate
break
}
}
if flag.Name == "" {
t.Fatal("reference-map flag not found")
}
if flag.Hidden {
t.Fatal("reference-map flag should be public")
}
if flag.Type != "" {
t.Fatalf("reference-map flag Type = %q, want default string", flag.Type)
}
if !hasUpdateTestInput(flag, common.File) || !hasUpdateTestInput(flag, common.Stdin) {
t.Fatalf("reference-map Input = %#v, want file and stdin", flag.Input)
}
if flag.Desc != docsUpdateReferenceMapFlagDesc {
t.Fatalf("reference-map help = %q, want %q", flag.Desc, docsUpdateReferenceMapFlagDesc)
}
}

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

runtime := newUpdateShortcutTestRuntime(t, "", map[string]string{
"command": "append",
"content": `<p><widget data-ref="r1"></widget></p>`,
"reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`,
})
body := buildUpdateBody(runtime)

refMap, ok := body["reference_map"].(map[string]interface{})
if !ok {
t.Fatalf("reference_map = %#v, want object", body["reference_map"])
}
widget, _ := refMap["widget"].(map[string]interface{})
r1, _ := widget["r1"].(map[string]interface{})
if got := r1["label"]; got != "widget-ref-value" {
t.Fatalf("reference_map.widget.r1.label = %#v, want widget-ref-value; body=%#v", got, body)
}
if got, want := body["command"], "block_insert_after"; got != want {
t.Fatalf("command = %#v, want %q", got, want)
}
if got, want := body["block_id"], "-1"; got != want {
t.Fatalf("block_id = %#v, want %q", got, want)
}
}

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

tests := []struct {
name string
setFlags map[string]string
wantCause bool
}{
{
name: "invalid json",
setFlags: map[string]string{
"reference-map": "{",
},
wantCause: true,
},
{
name: "empty",
setFlags: map[string]string{
"reference-map": "",
},
},
{
name: "without content",
setFlags: map[string]string{
"content": "",
"reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`,
},
},
{
name: "unsupported command",
setFlags: map[string]string{
"command": "block_move_after",
"block-id": "blk_anchor",
"src-block-ids": "blk_src",
"reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

runtime := newUpdateShortcutTestRuntime(t, "", tt.setFlags)
err := validateUpdateV2(context.Background(), runtime)
if err == nil {
t.Fatal("validateUpdateV2() succeeded, want error")
}
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--reference-map")
if tt.wantCause && errors.Unwrap(err) == nil {
t.Fatal("validateUpdateV2() error lost underlying JSON cause")
}
})
}
}

func TestDocsUpdateRejectsLegacyFlags(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -103,6 +215,15 @@ func TestDocsUpdateRejectsLegacyFlags(t *testing.T) {
}
}

func hasUpdateTestInput(flag common.Flag, input string) bool {
for _, candidate := range flag.Input {
if candidate == input {
return true
}
}
return false
}

func newUpdateShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[string]string) *common.RuntimeContext {
t.Helper()

Expand All @@ -113,6 +234,7 @@ func newUpdateShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[
cmd.Flags().String("command", "append", "")
cmd.Flags().Int("revision-id", -1, "")
cmd.Flags().String("content", "<p>hello</p>", "")
cmd.Flags().String("reference-map", "", "")
cmd.Flags().String("pattern", "", "")
cmd.Flags().String("block-id", "", "")
cmd.Flags().String("src-block-ids", "", "")
Expand Down
70 changes: 68 additions & 2 deletions shortcuts/doc/docs_update_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
Expand All @@ -22,12 +24,15 @@
"append": true,
}

const docsUpdateReferenceMapFlagDesc = "结构化 `reference_map` JSON object;当 `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map`。"

// v2UpdateFlags returns the flag definitions for the v2 (OpenAPI) update path.
func v2UpdateFlags() []common.Flag {
return []common.Flag{
{Name: "command", Desc: "operation; requirements: str_replace(--pattern), block_delete(--block-id, comma-separated for batch), block_insert_after/block_replace(--block-id,--content), block_copy_insert_after/block_move_after(--block-id,--src-block-ids), overwrite/append(--content)", Enum: validCommandsV2Keys()},
{Name: "doc-format", Desc: "content format for --content; xml is default for precise rich edits, markdown for user-provided Markdown or plain append/overwrite", Default: "xml", Enum: []string{"xml", "markdown"}},
{Name: "content", Desc: "replacement or inserted content; XML by default or Markdown when --doc-format markdown; empty with str_replace deletes match. " + docsContentSkillHelp + "; use --help for the latest command flags", Input: []string{common.File, common.Stdin}},
{Name: "reference-map", Desc: docsUpdateReferenceMapFlagDesc, Input: []string{common.File, common.Stdin}},
{Name: "pattern", Desc: "str_replace match pattern; XML mode is inline text, Markdown mode can match multiline text"},
{Name: "block-id", Desc: "target block ID(s) for block operations (comma-separated for batch delete); -1 means document end where supported"},
{Name: "src-block-ids", Desc: "comma-separated source block ids for block_copy_insert_after and block_move_after"},
Expand All @@ -54,6 +59,9 @@
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --command %q, valid: str_replace | block_delete | block_insert_after | block_copy_insert_after | block_replace | block_move_after | overwrite | append", cmd).WithParam("--command")
}
content := runtime.Str("content")
if err := validateUpdateReferenceMap(runtime, cmd, content); err != nil {
return err
}
pattern := runtime.Str("pattern")
blockID := runtime.Str("block-id")
srcBlockIDs := runtime.Str("src-block-ids")
Expand Down Expand Up @@ -113,7 +121,7 @@
func dryRunUpdateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
// Validate has already accepted --doc; parseDocumentRef cannot fail here.
ref, _ := parseDocumentRef(runtime.Str("doc"))
body := buildUpdateBody(runtime)
body, _ := buildUpdateBodyWithReferenceMap(runtime)
apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token)
return common.NewDryRunAPI().
PUT(apiPath).
Expand All @@ -126,7 +134,10 @@
ref, _ := parseDocumentRef(runtime.Str("doc"))

apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token)
body := buildUpdateBody(runtime)
body, err := buildUpdateBodyWithReferenceMap(runtime)
if err != nil {
return err

Check warning on line 139 in shortcuts/doc/docs_update_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update_v2.go#L137-L139

Added lines #L137 - L139 were not covered by tests
}

data, err := doDocAPI(runtime, "PUT", apiPath, body)
if err != nil {
Expand All @@ -138,6 +149,24 @@
}

func buildUpdateBody(runtime *common.RuntimeContext) map[string]interface{} {
body, _ := buildUpdateBodyWithReferenceMap(runtime)
return body
}

func buildUpdateBodyWithReferenceMap(runtime *common.RuntimeContext) (map[string]interface{}, error) {
body := buildUpdateBodyBase(runtime)
if !runtime.Changed("reference-map") {
return body, nil
}
refMap, err := parseUpdateReferenceMap(runtime.Str("reference-map"))
if err != nil {
return body, err

Check warning on line 163 in shortcuts/doc/docs_update_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update_v2.go#L163

Added line #L163 was not covered by tests
}
body["reference_map"] = refMap
return body, nil
}

func buildUpdateBodyBase(runtime *common.RuntimeContext) map[string]interface{} {
cmd := runtime.Str("command")

// append is a shorthand for block_insert_after with block_id "-1" (end of document)
Expand Down Expand Up @@ -169,3 +198,40 @@
injectDocsScene(runtime, body)
return body
}

func validateUpdateReferenceMap(runtime *common.RuntimeContext, command string, content string) error {
if !runtime.Changed("reference-map") {
return nil
}
if !updateCommandAcceptsReferenceMap(command) {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map is only supported with update commands that send --content").WithParam("--reference-map")
}
if content == "" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map requires --content that uses matching sidecar refs").WithParam("--reference-map")
}
_, err := parseUpdateReferenceMap(runtime.Str("reference-map"))
return err
}

func updateCommandAcceptsReferenceMap(command string) bool {
switch command {
case "str_replace", "block_insert_after", "block_replace", "overwrite", "append":
return true
default:
return false
}
}

func parseUpdateReferenceMap(raw string) (map[string]interface{}, error) {
if strings.TrimSpace(raw) == "" {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a non-empty JSON object").WithParam("--reference-map")
}
var refMap map[string]interface{}
if err := json.Unmarshal([]byte(raw), &refMap); err != nil {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a valid JSON object: %v", err).WithParam("--reference-map").WithCause(err)
}
if refMap == nil {
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a JSON object, got null").WithParam("--reference-map")

Check warning on line 234 in shortcuts/doc/docs_update_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update_v2.go#L234

Added line #L234 was not covered by tests
}
return refMap, nil
}
12 changes: 12 additions & 0 deletions shortcuts/drive/drive_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ func TestDriveExportMarkdownWritesFile(t *testing.T) {
if reqBody["format"] != "markdown" {
t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown")
}
if _, ok := reqBody["extra_param"]; ok {
t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody)
}

data, err := os.ReadFile(filepath.Join(tmpDir, "Weekly Notes.md"))
if err != nil {
Expand Down Expand Up @@ -213,6 +216,9 @@ func TestDriveExportMarkdownUsesProvidedFileName(t *testing.T) {
if reqBody["format"] != "markdown" {
t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown")
}
if _, ok := reqBody["extra_param"]; ok {
t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody)
}

data, err := os.ReadFile(filepath.Join(tmpDir, "custom-notes.md"))
if err != nil {
Expand Down Expand Up @@ -283,6 +289,9 @@ func TestDriveExportDryRunIncludesLocalFileNameMetadata(t *testing.T) {
if !strings.Contains(out, `"output_dir": "./exports"`) {
t.Fatalf("stdout missing output_dir metadata: %s", out)
}
if tt.name == "markdown" && strings.Contains(out, `"extra_param"`) {
t.Fatalf("markdown dry-run must not enable docs fetch extra_param: %s", out)
}
})
}
}
Expand Down Expand Up @@ -333,6 +342,9 @@ func TestDriveExportMarkdownFallsBackToTokenWhenTitleLookupFails(t *testing.T) {
if reqBody["format"] != "markdown" {
t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown")
}
if _, ok := reqBody["extra_param"]; ok {
t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody)
}

data, err := os.ReadFile(filepath.Join(tmpDir, "docx123.md"))
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions skills/lark-doc/references/lark-doc-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
| `--command` | 是 | 操作指令(见下方指令速查表) |
| `--doc-format` | 否 | 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) |
| `--content` | 视指令 | 写入内容(`str_replace` 传空字符串可实现删除) |
| `--reference-map` | 否 | 结构化 `reference_map` JSON object;当 `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map`。 |
| `--pattern` | 视指令 | 匹配文本(str_replace) |
| `--block-id` | 视指令 | 目标 block ID(block_* 操作),逗号分隔可批量删除,-1 表示末尾 |
| `--src-block-ids` | 视指令 | 源 block ID(逗号分隔),用于 block_copy_insert_after / block_move_after |
Expand Down
Loading
Loading