Skip to content

Commit e58d61b

Browse files
committed
feat: support docs create title option
Change-Id: I6fd840fe813e5e664ea9ec680765fd41375cdebf
1 parent 824aa9e commit e58d61b

11 files changed

Lines changed: 109 additions & 19 deletions

File tree

shortcuts/doc/doc_errors_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ func TestValidateCreateV2Contract(t *testing.T) {
203203
}
204204
}
205205

206+
func TestValidateCreateV2AllowsTitleWithoutContent(t *testing.T) {
207+
rt := docValidateRuntime(t, map[string]string{"title": "Only Title"}, nil, nil)
208+
if err := validateCreateV2(context.Background(), rt); err != nil {
209+
t.Fatalf("validateCreateV2() error = %v, want nil", err)
210+
}
211+
}
212+
206213
func TestValidateFetchV2Contract(t *testing.T) {
207214
cases := []struct {
208215
name string

shortcuts/doc/docs_create_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,6 @@ func TestDocsCreateRejectsLegacyV1Flags(t *testing.T) {
282282
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
283283
err := runDocsCreateShortcut(t, f, stdout, []string{
284284
"+create",
285-
"--title", "项目计划",
286285
"--markdown", "## 目标",
287286
"--as", "user",
288287
})
@@ -292,8 +291,7 @@ func TestDocsCreateRejectsLegacyV1Flags(t *testing.T) {
292291
for _, want := range []string{
293292
"docs +create is v2-only",
294293
"the old v1 interface has been shut down",
295-
"legacy v1 flag(s) --title, --markdown are no longer supported",
296-
"--title -> put the title in --content",
294+
"legacy v1 flag(s) --markdown are no longer supported",
297295
"--markdown -> use --content with --doc-format markdown",
298296
"lark-cli skills read lark-doc references/lark-doc-create.md",
299297
"lark-cli skills read lark-doc references/lark-doc-xml.md",

shortcuts/doc/docs_create_v2.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package doc
55

66
import (
7+
"bytes"
78
"context"
9+
"encoding/xml"
810
"strings"
911

1012
"github.com/larksuite/cli/errs"
@@ -14,6 +16,7 @@ import (
1416
// v2CreateFlags returns the flag definitions for the v2 (OpenAPI) create path.
1517
func v2CreateFlags() []common.Flag {
1618
return []common.Flag{
19+
{Name: "title", Desc: "document title; when provided, the CLI prepends it to --content as <title>...</title> so the title wins over later content titles"},
1720
{Name: "content", Desc: "document body; XML by default or Markdown when --doc-format markdown. " + docsContentSkillHelp + "; use --help for the latest command flags", Input: []string{common.File, common.Stdin}},
1821
{Name: "doc-format", Desc: "content format; xml is default and supports richer DocxXML blocks, markdown imports plain Markdown", Default: "xml", Enum: []string{"xml", "markdown"}},
1922
{Name: "parent-token", Desc: "parent folder token or wiki node token; mutually exclusive with --parent-position"},
@@ -25,8 +28,12 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
2528
if err := validateDocsV2Only(runtime, "+create", docsCreateLegacyFlags()); err != nil {
2629
return err
2730
}
28-
if runtime.Str("content") == "" {
29-
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required").WithParam("--content")
31+
title := strings.TrimSpace(runtime.Str("title"))
32+
if runtime.Changed("title") && title == "" {
33+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--title must not be empty").WithParam("--title")
34+
}
35+
if runtime.Str("content") == "" && title == "" {
36+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required unless --title is provided").WithParam("--content")
3037
}
3138
if runtime.Str("parent-token") != "" && runtime.Str("parent-position") != "" {
3239
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parent-token and --parent-position are mutually exclusive").WithParams(
@@ -66,7 +73,7 @@ func executeCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
6673
func buildCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
6774
body := map[string]interface{}{
6875
"format": runtime.Str("doc-format"),
69-
"content": runtime.Str("content"),
76+
"content": buildCreateContent(runtime),
7077
}
7178
if v := runtime.Str("parent-token"); v != "" {
7279
body["parent_token"] = v
@@ -78,6 +85,26 @@ func buildCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
7885
return body
7986
}
8087

88+
func buildCreateContent(runtime *common.RuntimeContext) string {
89+
content := runtime.Str("content")
90+
title := strings.TrimSpace(runtime.Str("title"))
91+
if title == "" {
92+
return content
93+
}
94+
95+
titleTag := "<title>" + escapeDocTitleText(title) + "</title>"
96+
if content == "" {
97+
return titleTag
98+
}
99+
return titleTag + "\n" + content
100+
}
101+
102+
func escapeDocTitleText(title string) string {
103+
var buf bytes.Buffer
104+
_ = xml.EscapeText(&buf, []byte(title))
105+
return buf.String()
106+
}
107+
81108
// augmentDocsCreatePermission grants full_access to the current CLI user when
82109
// the document was created with bot identity.
83110
func augmentDocsCreatePermission(runtime *common.RuntimeContext, data map[string]interface{}) {

shortcuts/doc/docs_fetch_v2_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ func TestBuildCreateBodyIncludesSceneFromContext(t *testing.T) {
4040
}
4141
}
4242

43+
func TestBuildCreateBodyPrependsTitleToContent(t *testing.T) {
44+
t.Parallel()
45+
46+
runtime := newCreateBodyTestRuntime(context.Background())
47+
if err := runtime.Cmd.Flags().Set("title", "A & B <C>"); err != nil {
48+
t.Fatalf("set title: %v", err)
49+
}
50+
if err := runtime.Cmd.Flags().Set("content", "## Body"); err != nil {
51+
t.Fatalf("set content: %v", err)
52+
}
53+
54+
body := buildCreateBody(runtime)
55+
if got, want := body["content"], "<title>A &amp; B &lt;C&gt;</title>\n## Body"; got != want {
56+
t.Fatalf("content = %#v, want %q", got, want)
57+
}
58+
}
59+
4360
func TestBuildUpdateBodyIncludesSceneFromContext(t *testing.T) {
4461
t.Parallel()
4562

@@ -351,6 +368,7 @@ func newFetchShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[s
351368
func newCreateBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
352369
cmd := &cobra.Command{Use: "+create"}
353370
cmd.Flags().String("doc-format", "xml", "")
371+
cmd.Flags().String("title", "", "")
354372
cmd.Flags().String("content", "<title>hello</title>", "")
355373
cmd.Flags().String("parent-token", "", "")
356374
cmd.Flags().String("parent-position", "", "")

shortcuts/doc/v2_only.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ func docsAPIVersionCompatFlag() common.Flag {
2525

2626
func docsCreateLegacyFlags() []docsLegacyFlag {
2727
return []docsLegacyFlag{
28-
{Name: "title", Replacement: "put the title in --content, for example <title>Title</title>"},
2928
{Name: "markdown", Replacement: "use --content with --doc-format markdown"},
3029
{Name: "folder-token", Replacement: "use --parent-token"},
3130
{Name: "wiki-node", Replacement: "use --parent-token"},

skills/lark-doc/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ metadata:
1717
```bash
1818
# 常用示例
1919
lark-cli docs +fetch --api-version v2 --doc "文档URL或token"
20-
lark-cli docs +create --api-version v2 --content '<title>标题</title><p>内容</p>'
20+
lark-cli docs +create --api-version v2 --title "标题" --content '<p>内容</p>'
2121
lark-cli docs +update --api-version v2 --doc "文档URL或token" --command append --content '<p>内容</p>'
2222
```
2323

skills/lark-doc/references/lark-doc-create.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616

1717
```bash
1818
# 创建 XML 文档(默认格式,推荐)
19-
lark-cli docs +create --api-version v2 --content '<title>项目计划</title><h1>目标</h1><ul><li>目标 1</li><li>目标 2</li></ul>'
19+
lark-cli docs +create --api-version v2 --title "项目计划" --content '<h1>目标</h1><ul><li>目标 1</li><li>目标 2</li></ul>'
2020

2121
# 创建到指定文件夹(XML)
22-
lark-cli docs +create --api-version v2 --parent-token fldcnXXXX --content '<title>标题</title><p>首段内容</p>'
22+
lark-cli docs +create --api-version v2 --parent-token fldcnXXXX --title "标题" --content '<p>首段内容</p>'
2323

2424
# 创建到个人知识库(XML)
25-
lark-cli docs +create --api-version v2 --parent-position my_library --content '<title>标题</title><p>内容</p>'
25+
lark-cli docs +create --api-version v2 --parent-position my_library --title "标题" --content '<p>内容</p>'
2626

27-
# 仅当用户明确要求时才使用 Markdown;文档标题必须是开头唯一的一级标题,正文从二级标题开始
28-
lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项目计划\n\n## 目标\n\n- 目标 1\n- 目标 2'
27+
# 仅当用户明确要求时才使用 Markdown;文档标题用 --title,正文标题按内容自然组织
28+
lark-cli docs +create --api-version v2 --doc-format markdown --title "项目计划" --content $'## 目标\n\n- 目标 1\n- 目标 2'
2929
```
3030

3131
## 返回值
@@ -66,15 +66,18 @@ lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项
6666
| 参数 | 必填 | 说明 |
6767
| ------------------- | -- |---------------------------------------------|
6868
| `--api-version` || 固定传 `v2` |
69-
| `--content` || 文档内容(XML 或 Markdown 格式) |
69+
| `--title` || 文档标题;传入后 CLI 会在 `--content` 开头补 `<title>...</title>` |
70+
| `--content` | 视情况 | 文档内容(XML 或 Markdown 格式);不传 `--content` 时必须传 `--title` |
7071
| `--doc-format` || 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) |
7172
| `--parent-token` || 父文件夹或知识库节点 token(与 `--parent-position` 互斥) |
7273
| `--parent-position` || 父节点位置,如 `my_library`(与 `--parent-token` 互斥) |
7374

7475
## 最佳实践
7576

76-
- 文档标题从内容中自动提取:XML 使用 `<title>`;Markdown 使用文档开头唯一的一级标题(`# 标题`),正文从 `##` 开始。不要在内容开头重复写标题,也不要在 Markdown 正文中使用多个一级标题。
77-
- **创建较长的文档时只建骨架**`--content` 仅传标题 + 各级 heading + 简短占位摘要;正文留给后续 `block_insert_after --block-id <章节标题 block_id>` 分段追加。一次性塞超长 `--content` 既容易触发参数限制,调试也更难。
77+
- 文档标题优先用 `--title`。CLI 会把它补成内容开头的 `<title>`;如果内容里又出现 `<title>`,SDK 会保留第一个标题并过滤后续标题,同时在 `warnings` / `degrade_details` 中提示。
78+
- XML 内容也可以直接写 `<title>`;但不要同时传 `--title` 和手写另一个 `<title>`,除非你明确接受后者被过滤。
79+
- Markdown 创建时用 `--title` 指定文档标题,不再要求正文开头必须是唯一一级标题;正文 heading 按内容自然组织即可。
80+
- **创建较长的文档时只建骨架**:用 `--title` 传文档标题,`--content` 仅传各级 heading + 简短占位摘要;正文留给后续 `block_insert_after --block-id <章节标题 block_id>` 分段追加。一次性塞超长 `--content` 既容易触发参数限制,调试也更难。
7881
- **表达形式**:由用户目标和内容决定。需要结构化表达时可参考 [`lark-doc-style.md`](style/lark-doc-style.md),但不要默认套用固定开头、固定富 block 比例或固定图表
7982

8083
## 参考

skills/lark-doc/references/lark-doc-md.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
## 创建文档标题
66

7-
使用 `docs +create --doc-format markdown` 创建文档时,文档标题必须写成内容开头唯一的一级标题:`# 标题`。正文标题从 `##` 开始,不要使用多个一级标题;否则标题可能无法被提取并显示为 `Untitled`
7+
使用 `docs +create --doc-format markdown` 创建文档时,优先用 `--title` 指定文档标题:
8+
9+
```bash
10+
lark-cli docs +create --api-version v2 --doc-format markdown --title "项目计划" --content @content.md
11+
```
12+
13+
CLI 会把 `--title` 注入为内容开头的 `<title>...</title>`。正文 Markdown 不再需要为了文档标题强制使用唯一一级标题;可以按内容自然使用 `#` / `##` 等 heading。
14+
15+
如果 `--content` 中也包含 `<title>`,SDK 会以第一个 `<title>` 为准,过滤后续标题,并通过 `warnings` / `degrade_details` 告知有标题被过滤。
816

917
## 转义规则
1018

skills/lark-doc/references/lark-doc-xml.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
1212

1313
## 创建文档标题
1414

15-
使用 `docs +create` 创建 XML 文档时,文档标题必须写成 `<title>标题</title>`,且每篇文档只写一个 `<title>`
15+
使用 `docs +create` 创建 XML 文档时,文档标题可以通过 `--title "标题"` 指定,也可以在内容开头写 `<title>标题</title>`。每篇文档只保留一个文档标题;如果出现多个 `<title>`,SDK 以第一个为准,过滤后续标题,并通过 `warnings` / `degrade_details` 提示
1616

1717
## 容器标签
1818
|标签|说明|关键属性|

tests/cli_e2e/docs/coverage.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
- TestDocs_CreateAndFetchWorkflowAsUser: proves the same shortcut pair with UAT injection via `create as user` and `fetch as user`; creates its own Drive folder fixture first, then reads back the created doc by token.
1111
- TestDocs_UpdateWorkflow: proves `docs +update` via `update-title-and-content as bot`, then re-fetches the same doc in `verify as bot` to assert persisted title/content changes.
1212
- TestDocs_DryRunDefaultsToV2OpenAPI: proves `docs +create`, `docs +fetch`, and `docs +update` dry-run all emit `/open-apis/docs_ai/v1/...` requests without MCP or `--api-version` guidance.
13+
- TestDocs_CreateTitleDryRunPrependsContent: proves `docs +create --title` dry-run prepends an escaped `<title>...</title>` tag to request body `content`.
1314
- Setup note: docs workflows create a Drive folder through `drive files create_folder` in `helpers_test.go`; that helper is external to the docs domain and is not counted here.
1415
- Blocked area: media and search shortcuts still need deterministic fixtures and local file orchestration.
1516

1617
## Command Table
1718

1819
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
1920
| --- | --- | --- | --- | --- | --- |
20-
|| docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/create | `--parent-token`; `--doc-format markdown`; `--content` | helper asserts returned doc id from `data.document.document_id` |
21+
|| docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/create; docs_update_dryrun_test.go::TestDocs_CreateTitleDryRunPrependsContent | `--parent-token`; `--doc-format markdown`; `--content`; `--title` | helper asserts returned doc id from `data.document.document_id`; dry-run asserts title is prepended into request body content |
2122
|| docs +fetch | shortcut | docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflow/fetch as bot; docs_update_test.go::TestDocs_UpdateWorkflow/verify as bot; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/fetch as user; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/fetch | `--doc <docToken>`; `--doc-format markdown` | |
2223
|| docs +media-download | shortcut | | none | no media fixture workflow yet |
2324
|| docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions |

0 commit comments

Comments
 (0)