mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(docs): add reference map flags (#1547)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", "", "")
|
||||
|
||||
@@ -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", "", "")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user