Skip to content

Commit e21ef41

Browse files
committed
feat: support docs create title option
Change-Id: I6fd840fe813e5e664ea9ec680765fd41375cdebf
1 parent 1c92ed8 commit e21ef41

11 files changed

Lines changed: 103 additions & 25 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
@@ -43,6 +43,23 @@ func TestBuildCreateBodyIncludesSceneFromContext(t *testing.T) {
4343
}
4444
}
4545

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

@@ -845,6 +862,7 @@ func newFetchShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[s
845862
func newCreateBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
846863
cmd := &cobra.Command{Use: "+create"}
847864
cmd.Flags().String("doc-format", "xml", "")
865+
cmd.Flags().String("title", "", "")
848866
cmd.Flags().String("content", "<title>hello</title>", "")
849867
cmd.Flags().String("parent-token", "", "")
850868
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: 15 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
## 返回值
@@ -61,20 +61,27 @@ lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项
6161
>
6262
> **不要擅自执行 owner 转移。** 如果用户需要把 owner 转给自己,必须单独确认。
6363
64+
## 文档标题
65+
66+
- 创建文档时优先用 `--title` 指定文档标题。CLI 会把它补成内容开头的 `<title>...</title>`
67+
- XML 内容也可以直接写 `<title>标题</title>`;每篇文档只应有一个文档标题。
68+
- 如果同时传 `--title``--content` 中也包含 `<title>`,SDK 会保留第一个标题并过滤后续标题,同时在 `warnings` / `degrade_details` 中提示。
69+
- Markdown 创建时用 `--title` 指定文档标题,不要求正文开头必须是唯一一级标题;正文 heading 按内容自然组织即可。
70+
6471
## 参数
6572

6673
| 参数 | 必填 | 说明 |
6774
| ------------------- | -- |---------------------------------------------|
6875
| `--api-version` || 固定传 `v2` |
69-
| `--content` || 文档内容(XML 或 Markdown 格式) |
76+
| `--title` || 文档标题;传入后 CLI 会在 `--content` 开头补 `<title>...</title>` |
77+
| `--content` | 视情况 | 文档内容(XML 或 Markdown 格式);不传 `--content` 时必须传 `--title` |
7078
| `--doc-format` || 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) |
7179
| `--parent-token` || 父文件夹或知识库节点 token(与 `--parent-position` 互斥) |
7280
| `--parent-position` || 父节点位置,如 `my_library`(与 `--parent-token` 互斥) |
7381

7482
## 最佳实践
7583

76-
- 文档标题从内容中自动提取:XML 使用 `<title>`;Markdown 使用文档开头唯一的一级标题(`# 标题`),正文从 `##` 开始。不要在内容开头重复写标题,也不要在 Markdown 正文中使用多个一级标题。
77-
- **较长文档**:参考 [`lark-doc-create-workflow.md`](style/lark-doc-create-workflow.md) 先建骨架再分段写入;短文档可一次写完整内容。
84+
- **较长文档**:用 `--title` 传文档标题,参考 [`lark-doc-create-workflow.md`](style/lark-doc-create-workflow.md) 先建骨架再分段写入;`--content` 仅传各级 heading + 简短占位摘要,短文档可一次写完整内容。
7885
- **表达形式**:由用户目标和内容决定。需要结构化表达时可参考 [`lark-doc-style.md`](style/lark-doc-style.md),但不要默认套用固定开头、固定富 block 比例或固定图表
7986

8087
## 参考

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
`docs +fetch --api-version v2` / `docs +create --api-version v2` / `docs +update --api-version v2` 使用 `--doc-format markdown` 时适用;fetch 的 `--doc-format im-markdown` 仅用于获取内容后在 `lark-im` 场景下使用,不作为 create/update 写入格式。
44

5-
## 创建文档标题
6-
7-
使用 `docs +create --doc-format markdown` 创建文档时,文档标题必须写成内容开头唯一的一级标题:`# 标题`。正文标题从 `##` 开始,不要使用多个一级标题;否则标题可能无法被提取并显示为 `Untitled`
8-
95
## 转义规则
106

117
> **⚠️ 当文本中包含以下字符且不想触发 Markdown 语法时**,需用 `\` 前缀转义。转义分为**无条件转义**(行内任意位置生效)和**位置敏感转义**(仅特定位置才需要)两类。

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
1010
| `<title>` | 文档标题(每篇唯一)| `align` |
1111
| `<checkbox>` | 待办项| `done="true"\|"false"` |
1212

13-
## 创建文档标题
14-
15-
使用 `docs +create` 创建 XML 文档时,文档标题必须写成 `<title>标题</title>`,且每篇文档只写一个 `<title>`
16-
1713
## 容器标签
1814
|标签|说明|关键属性|
1915
|-|-|-|

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)