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
34 changes: 25 additions & 9 deletions shortcuts/doc/docs_fetch_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
return err
}
if _, err := parseDocumentRef(runtime.Str("doc")); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --doc: %v", err).WithParam("--doc")
}
if err := validateFetchDetail(runtime); err != nil {
return err
}
if err := validateReadModeFlags(runtime); err != nil {
Expand Down Expand Up @@ -71,6 +68,9 @@
if err != nil {
return err
}
if warning := addFetchDetailDowngradeWarning(runtime, data); warning != "" && runtime.Format == "pretty" {
fmt.Fprintf(runtime.IO().ErrOut, "warning: %s\n", warning)
}

runtime.OutFormatRaw(data, nil, func(w io.Writer) {
if doc, ok := data["document"].(map[string]interface{}); ok {
Expand All @@ -90,7 +90,7 @@
body["revision_id"] = v
}

detail := runtime.Str("detail")
detail := effectiveFetchDetail(runtime)
switch detail {
case "", "simple":
body["export_option"] = map[string]interface{}{
Expand Down Expand Up @@ -146,17 +146,33 @@
return ro
}

// validateFetchDetail 非 xml 格式(markdown)不承载 block_id 与样式属性,拒绝 with-ids/full。
func validateFetchDetail(runtime *common.RuntimeContext) error {
// effectiveFetchDetail degrades detail options that cannot be represented by
// non-XML exports. The original flag value is left intact so callers can still
// surface an explicit warning in execute output.
func effectiveFetchDetail(runtime *common.RuntimeContext) string {
format := strings.TrimSpace(runtime.Str("doc-format"))
detail := strings.TrimSpace(runtime.Str("detail"))
if format == "" || format == "xml" {
return nil
return detail
}
if detail == "with-ids" || detail == "full" {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--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).WithParam("--detail")
return "simple"
}
return nil
return detail

Check warning on line 161 in shortcuts/doc/docs_fetch_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_fetch_v2.go#L161

Added line #L161 was not covered by tests
}

func addFetchDetailDowngradeWarning(runtime *common.RuntimeContext, data map[string]interface{}) string {
format := strings.TrimSpace(runtime.Str("doc-format"))
detail := strings.TrimSpace(runtime.Str("detail"))
if format == "" || format == "xml" {
return ""

Check warning on line 168 in shortcuts/doc/docs_fetch_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_fetch_v2.go#L168

Added line #L168 was not covered by tests
}
if detail != "with-ids" && detail != "full" {
return ""

Check warning on line 171 in shortcuts/doc/docs_fetch_v2.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_fetch_v2.go#L171

Added line #L171 was not covered by tests
}
warning := fmt.Sprintf("--detail %s is only supported with --doc-format xml; returning %s output and ignoring the unsupported detail option", detail, format)
appendDocWarning(data, warning)
return warning
}

// validateReadModeFlags 客户端前置校验,服务端也会再校验一次。
Expand Down
123 changes: 123 additions & 0 deletions shortcuts/doc/docs_fetch_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ package doc

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

"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -96,6 +99,126 @@ func TestDocsFetchAPIVersionV1StillUsesV2Endpoint(t *testing.T) {
}
}

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

for _, detail := range []string{"with-ids", "full"} {
t.Run(detail, func(t *testing.T) {
t.Parallel()

runtime := newFetchShortcutTestRuntime(t, "", map[string]string{
"doc-format": "markdown",
"detail": detail,
})
if err := validateFetchV2(context.Background(), runtime); err != nil {
t.Fatalf("validateFetchV2() error = %v", err)
}

dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
exportOption, _ := dry.API[0].Body["export_option"].(map[string]interface{})
if exportOption == nil {
t.Fatalf("missing export_option: %#v", dry.API[0].Body)
}
if got := exportOption["export_block_id"]; got != false {
t.Fatalf("export_block_id = %#v, want false after markdown detail downgrade", got)
}
if got := exportOption["export_style_attrs"]; got != false {
t.Fatalf("export_style_attrs = %#v, want false after markdown detail downgrade", got)
}
if got := exportOption["export_cite_extra_data"]; got != false {
t.Fatalf("export_cite_extra_data = %#v, want false after markdown detail downgrade", got)
}
})
}
}

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

f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-detail-warning"))
reg.Register(&httpmock.Stub{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Method: "POST",
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchWarning/fetch",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcnFetchWarning",
"revision_id": float64(1),
"content": "# hello",
},
},
},
})

err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--doc", "doxcnFetchWarning",
"--doc-format", "markdown",
"--detail", "with-ids",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var envelope map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("decode output: %v\nraw=%s", err, stdout.String())
}
data, _ := envelope["data"].(map[string]interface{})
warnings, _ := data["warnings"].([]interface{})
if len(warnings) != 1 {
t.Fatalf("warnings = %#v, want one downgrade warning", data["warnings"])
}
if got, _ := warnings[0].(string); !strings.Contains(got, "returning markdown output") || !strings.Contains(got, "ignoring the unsupported detail option") {
t.Fatalf("unexpected warning: %q", got)
}
}

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

f, stdout, stderr, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-detail-pretty-warning"))
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchPrettyWarning/fetch",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"data": map[string]interface{}{
"document": map[string]interface{}{
"document_id": "doxcnFetchPrettyWarning",
"revision_id": float64(1),
"content": "# hello",
},
},
},
})

err := mountAndRunDocs(t, DocsFetch, []string{
"+fetch",
"--doc", "doxcnFetchPrettyWarning",
"--doc-format", "markdown",
"--detail", "full",
"--format", "pretty",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got := stdout.String(); got != "# hello\n" {
t.Fatalf("stdout = %q, want markdown content only", got)
}
if got := stderr.String(); !strings.Contains(got, "warning: --detail full is only supported with --doc-format xml") ||
!strings.Contains(got, "returning markdown output") ||
!strings.Contains(got, "ignoring the unsupported detail option") {
t.Fatalf("stderr missing downgrade warning: %q", got)
}
}

func TestDocsFetchRejectsLegacyFlags(t *testing.T) {
tests := []struct {
name string
Expand Down
19 changes: 19 additions & 0 deletions shortcuts/doc/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,22 @@ func buildDriveRouteExtra(docID string) (string, error) {
}
return string(extra), nil
}

func appendDocWarning(data map[string]interface{}, warning string) {
if data == nil {
return
}
if strings.TrimSpace(warning) == "" {
return
}
switch existing := data["warnings"].(type) {
case []interface{}:
data["warnings"] = append(existing, warning)
case []string:
data["warnings"] = append(existing, warning)
case nil:
data["warnings"] = []string{warning}
default:
data["warnings"] = []interface{}{existing, warning}
}
}
49 changes: 49 additions & 0 deletions shortcuts/doc/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package doc

import (
"reflect"
"strings"
"testing"
)
Expand Down Expand Up @@ -88,3 +89,51 @@ func TestBuildDriveRouteExtraEscapesJSON(t *testing.T) {
t.Fatalf("buildDriveRouteExtra() = %q, want %q", got, want)
}
}

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

appendDocWarning(nil, "ignored")

empty := map[string]interface{}{}
appendDocWarning(empty, " ")
if _, ok := empty["warnings"]; ok {
t.Fatalf("blank warning should be ignored: %#v", empty)
}

tests := []struct {
name string
data map[string]interface{}
want interface{}
}{
{
name: "missing warnings",
data: map[string]interface{}{},
want: []string{"new warning"},
},
{
name: "string slice warnings",
data: map[string]interface{}{"warnings": []string{"old warning"}},
want: []string{"old warning", "new warning"},
},
{
name: "interface slice warnings",
data: map[string]interface{}{"warnings": []interface{}{"old warning"}},
want: []interface{}{"old warning", "new warning"},
},
{
name: "scalar warning",
data: map[string]interface{}{"warnings": "old warning"},
want: []interface{}{"old warning", "new warning"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
appendDocWarning(tt.data, "new warning")
if got := tt.data["warnings"]; !reflect.DeepEqual(got, tt.want) {
t.Fatalf("warnings = %#v, want %#v", got, tt.want)
}
})
}
}
3 changes: 3 additions & 0 deletions skills/lark-doc/references/lark-doc-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ lark-cli docs +update --api-version v2 --doc "<doc_id>" --command block_replace
--content '<p>替换后的段落内容</p>'
```

> `block_replace` 由服务端执行整块替换,目标 block 的 ID 不保证在替换后继续可用。后续如果还要在替换后的块附近继续 `block_insert_after`、`range` 或其他 block 级操作,先重新 `docs +fetch --detail with-ids` 获取最新 block ID,不要复用旧 ID。

### block_delete — 删除指定 block

```bash
Expand Down Expand Up @@ -234,6 +236,7 @@ lark-cli docs +update --api-version v2 --doc "<doc_id>" --command str_replace \
- **保护不可重建的内容**:图片、画板、电子表格等以 token 形式存储,替换时避开这些 block
- **str_replace 的 replacement 支持富文本**:可以用行内标签 `<b>`、`<a>`、`<cite>`、`<latex>` 等替换普通文本为富文本
- **同一 block 只能被 replace 一次**:多次修改同一 block 请合并为一次 block_replace
- **block_replace 后重新获取 ID**:`block_replace` 成功后旧 block ID 不保证继续可用;继续做相邻块操作前,重新 `docs +fetch --detail with-ids`
- **block_delete 支持批量**:用逗号分隔多个 block_id 一次删除
- **复杂结构重组**:将多个段落转换为 grid / table 等复杂布局时,分步操作比 overwrite 更安全:
1. 用 `block_insert_after` 在目标位置插入新的富文本结构
Expand Down
4 changes: 4 additions & 0 deletions skills/lark-doc/references/lark-doc-xml.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
</ul>
```

## 代码块
- 代码块必须写成 `<pre lang="xxx" caption="可选说明"><code>代码内容</code></pre>`。
- 不要将代码文本直接放在 `<pre>` 下;应放在内层 `<code>` 中。


## 用户名写入规则

Expand Down
Loading