feat(docs): add reference map flags (#1547)

This commit is contained in:
SunPeiYang996
2026-06-30 12:07:18 +08:00
committed by GitHub
parent 31744f8cf9
commit 22108c3300
10 changed files with 282 additions and 12 deletions

View File

@@ -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{
@@ -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

View File

@@ -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()
@@ -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", "", "")

View File

@@ -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"
)
@@ -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
@@ -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()
@@ -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", "", "")

View File

@@ -5,7 +5,9 @@ package doc
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/shortcuts/common"
@@ -22,12 +24,15 @@ var validCommandsV2 = map[string]bool{
"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"},
@@ -54,6 +59,9 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
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")
@@ -113,7 +121,7 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
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).
@@ -126,7 +134,10 @@ func executeUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
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
}
data, err := doDocAPI(runtime, "PUT", apiPath, body)
if err != nil {
@@ -138,6 +149,24 @@ func executeUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
}
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
}
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)
@@ -169,3 +198,40 @@ func buildUpdateBody(runtime *common.RuntimeContext) map[string]interface{} {
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")
}
return refMap, nil
}