feat: guide lark-doc v2 usage (#710)

## Summary
Add explicit guidance on the parent `docs` command so agents pick the right
lark-doc API version. Without this, agents that have an older lark-doc skill
installed can mistakenly mix v2 flags into a v1 flow.

## Changes
- Add `--api-version` help flag and a Tips section to `docs` so `lark docs --help`
  (and `--api-version v2`) explain when v2 should be used.
- Refresh the lark-doc skill references and `docs_fetch_v2` keyword flag
  description for clarity.
- Add `shortcuts/register_test.go` covering the new docs help wiring.

## Test Plan
- [x] Unit tests pass (`go test ./shortcuts/...`)
- [x] Manual local verification confirms the `lark docs --help` and
      `lark docs --help --api-version v2` commands work as expected

## Related Issues
- None

Change-Id: Id3b3196e6a069bb52f95a6fc679b8258313faf3d
This commit is contained in:
SunPeiYang996
2026-04-29 22:40:20 +08:00
committed by GitHub
parent b37adfd0ee
commit d7ee5b5769
13 changed files with 289 additions and 29 deletions

View File

@@ -43,6 +43,7 @@ var DocsCreate = common.Shortcut{
Risk: "write",
AuthTypes: []string{"user", "bot"},
Scopes: []string{"docx:document:create"},
Tips: docsVersionSelectionTips,
Flags: concatFlags(
[]common.Flag{
{Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}},

View File

@@ -49,6 +49,7 @@ var DocsFetch = common.Shortcut{
Scopes: []string{"docx:document:readonly"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Tips: docsVersionSelectionTips,
Flags: concatFlags(
[]common.Flag{
{Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}},

View File

@@ -22,7 +22,7 @@ func v2FetchFlags() []common.Flag {
{Name: "scope", Desc: "partial read scope: outline | range | keyword | section (omit to read whole doc)", Default: "full", Enum: []string{"full", "outline", "range", "keyword", "section"}},
{Name: "start-block-id", Desc: "range/section mode: start (anchor) block id"},
{Name: "end-block-id", Desc: "range mode: end block id; \"-1\" = to end of document"},
{Name: "keyword", Desc: "keyword mode: search string (case-insensitive); use '|' to match multiple keywords, e.g. 'foo|bar|baz'"},
{Name: "keyword", Desc: "keyword mode: substring + regex match (case-insensitive); use '|' for OR branches, e.g. 'foo|bar' or 'bug|缺陷'"},
{Name: "context-before", Desc: "range/keyword/section mode: sibling blocks before match", Type: "int", Default: "0"},
{Name: "context-after", Desc: "range/keyword/section mode: sibling blocks after match", Type: "int", Default: "0"},
{Name: "max-depth", Desc: "outline: heading level cap; range/keyword/section: block subtree depth (-1 = unlimited)", Type: "int", Default: "-1"},

View File

@@ -64,6 +64,7 @@ var DocsUpdate = common.Shortcut{
Risk: "write",
Scopes: []string{"docx:document:write_only", "docx:document:readonly"},
AuthTypes: []string{"user", "bot"},
Tips: docsVersionSelectionTips,
Flags: concatFlags(
[]common.Flag{
{Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}},

View File

@@ -3,7 +3,24 @@
package doc
import "github.com/larksuite/cli/shortcuts/common"
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/shortcuts/common"
)
const docsServiceHelpDefault = `Document and content operations.`
const docsServiceHelpV2 = `Document and content operations (v2).`
var docsVersionSelectionTips = []string{
"Agent version rule: use --api-version v2 only when the installed lark-doc skill explicitly instructs docs +create, docs +fetch, or docs +update to use v2; otherwise use the default v1 flags.",
"Do not mix versions: if the skill does not mention v2, follow its legacy v1 examples and flags.",
}
// Shortcuts returns all docs shortcuts.
func Shortcuts() []common.Shortcut {
@@ -18,3 +35,48 @@ func Shortcuts() []common.Shortcut {
DocMediaDownload,
}
}
// ConfigureServiceHelp adds docs-specific guidance to the parent `docs` command.
// The shortcut-level help remains compatible with legacy v1 skills; this parent
// help gives agents enough context to choose v2 only when their installed skill
// explicitly asks for `--api-version v2`.
func ConfigureServiceHelp(cmd *cobra.Command) {
if cmd == nil {
return
}
serviceCmd := cmd
cmd.Long = strings.TrimSpace(docsServiceHelpDefault)
if cmd.Flags().Lookup("api-version") == nil {
cmd.Flags().String("api-version", "", "show docs help for API version (v1|v2)")
cmdutil.RegisterFlagCompletion(cmd, "api-version", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"v1", "v2"}, cobra.ShellCompDirectiveNoFileComp
})
}
defaultHelp := cmd.HelpFunc()
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
if cmd != serviceCmd {
defaultHelp(cmd, args)
return
}
apiVersion, _ := cmd.Flags().GetString("api-version")
previousLong := cmd.Long
if apiVersion == "v2" {
cmd.Long = strings.TrimSpace(docsServiceHelpV2)
} else {
cmd.Long = strings.TrimSpace(docsServiceHelpDefault)
}
defer func() {
cmd.Long = previousLong
}()
defaultHelp(cmd, args)
out := cmd.OutOrStdout()
fmt.Fprintln(out)
fmt.Fprintln(out, "Tips:")
for _, tip := range docsVersionSelectionTips {
fmt.Fprintf(out, " • %s\n", tip)
}
})
}

View File

@@ -30,13 +30,6 @@ func installVersionedHelp(cmd *cobra.Command, defaultVersion string, flagVersion
}
})
origHelp(cmd, args)
if ver == "v1" {
fmt.Fprintf(cmd.OutOrStdout(),
"\n[NOTE] v1 API is deprecated and will be removed in a future release.\n"+
" Use --api-version v2 for the latest API:\n"+
" %s %s --api-version v2 --help\n",
cmd.Parent().Name(), cmd.Name())
}
})
}

View File

@@ -90,6 +90,9 @@ func RegisterShortcutsWithContext(ctx context.Context, program *cobra.Command, f
}
program.AddCommand(svc)
}
if service == "docs" {
doc.ConfigureServiceHelp(svc)
}
for _, shortcut := range shortcuts {
shortcut.MountWithContext(ctx, svc, f)

View File

@@ -4,16 +4,45 @@
package shortcuts
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/cobra"
)
func newRegisterTestFactory(t *testing.T) *cmdutil.Factory {
t.Helper()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{})
return f
}
func newRegisterTestProgramWithTipsHelp() *cobra.Command {
program := &cobra.Command{Use: "root"}
defaultHelp := program.HelpFunc()
program.SetHelpFunc(func(cmd *cobra.Command, args []string) {
defaultHelp(cmd, args)
tips := cmdutil.GetTips(cmd)
if len(tips) == 0 {
return
}
out := cmd.OutOrStdout()
fmt.Fprintln(out)
fmt.Fprintln(out, "Tips:")
for _, tip := range tips {
fmt.Fprintf(out, " • %s\n", tip)
}
})
return program
}
func TestAllShortcutsScopesNotNil(t *testing.T) {
for _, s := range allShortcuts {
hasScopes := s.Scopes != nil || s.UserScopes != nil || s.BotScopes != nil
@@ -48,7 +77,7 @@ func TestAllShortcutsReturnsCopyAndIncludesBase(t *testing.T) {
func TestRegisterShortcutsMountsBaseCommands(t *testing.T) {
program := &cobra.Command{Use: "root"}
RegisterShortcuts(program, &cmdutil.Factory{})
RegisterShortcuts(program, newRegisterTestFactory(t))
baseCmd, _, err := program.Find([]string{"base"})
if err != nil {
@@ -69,7 +98,7 @@ func TestRegisterShortcutsMountsBaseCommands(t *testing.T) {
func TestRegisterShortcutsMountsDocsMediaPreview(t *testing.T) {
program := &cobra.Command{Use: "root"}
RegisterShortcuts(program, &cmdutil.Factory{})
RegisterShortcuts(program, newRegisterTestFactory(t))
previewCmd, _, err := program.Find([]string{"docs", "+media-preview"})
if err != nil {
@@ -80,12 +109,182 @@ func TestRegisterShortcutsMountsDocsMediaPreview(t *testing.T) {
}
}
func TestRegisterShortcutsDocsHelpAddsVersionSelectorAndLegacyTips(t *testing.T) {
program := &cobra.Command{Use: "root"}
RegisterShortcuts(program, newRegisterTestFactory(t))
docsCmd, _, err := program.Find([]string{"docs"})
if err != nil {
t.Fatalf("find docs command: %v", err)
}
if docsCmd == nil || docsCmd.Name() != "docs" {
t.Fatalf("docs command not mounted: %#v", docsCmd)
}
if docsCmd.Flags().Lookup("api-version") == nil {
t.Fatal("docs command should expose --api-version for versioned help")
}
if !strings.Contains(docsCmd.Long, "Document and content operations.") {
t.Fatalf("docs long help missing default description:\n%s", docsCmd.Long)
}
var defaultHelp bytes.Buffer
docsCmd.SetOut(&defaultHelp)
if err := docsCmd.Help(); err != nil {
t.Fatalf("docs help failed: %v", err)
}
for _, want := range []string{
"Tips:",
"Agent version rule",
"use --api-version v2 only when the installed lark-doc skill explicitly instructs",
"otherwise use the default v1 flags",
"if the skill does not mention v2",
"legacy v1 examples and flags",
} {
if !strings.Contains(defaultHelp.String(), want) {
t.Fatalf("docs default help missing %q:\n%s", want, defaultHelp.String())
}
}
}
func TestRegisterShortcutsDocsV2HelpUsesV2Description(t *testing.T) {
program := &cobra.Command{Use: "root"}
RegisterShortcuts(program, newRegisterTestFactory(t))
docsCmd, _, err := program.Find([]string{"docs"})
if err != nil {
t.Fatalf("find docs command: %v", err)
}
if err := docsCmd.Flags().Set("api-version", "v2"); err != nil {
t.Fatalf("set docs api-version: %v", err)
}
var out bytes.Buffer
docsCmd.SetOut(&out)
if err := docsCmd.Help(); err != nil {
t.Fatalf("docs v2 help failed: %v", err)
}
for _, want := range []string{
"Document and content operations (v2).",
"Tips:",
"Agent version rule",
"otherwise use the default v1 flags",
"if the skill does not mention v2",
"legacy v1 examples and flags",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("docs v2 help missing %q:\n%s", want, out.String())
}
}
}
func TestRegisterShortcutsDocsVersionedShortcutHelpAddsVersionTips(t *testing.T) {
tests := []struct {
name string
shortcut string
apiVersion string
shortcutHelp string
versionedFlag string
}{
{
name: "create v1",
shortcut: "+create",
apiVersion: "v1",
shortcutHelp: "Create a Lark document",
versionedFlag: "--markdown",
},
{
name: "create v2",
shortcut: "+create",
apiVersion: "v2",
shortcutHelp: "Create a Lark document",
versionedFlag: "--content",
},
{
name: "fetch v1",
shortcut: "+fetch",
apiVersion: "v1",
shortcutHelp: "Fetch Lark document content",
versionedFlag: "--offset",
},
{
name: "fetch v2",
shortcut: "+fetch",
apiVersion: "v2",
shortcutHelp: "Fetch Lark document content",
versionedFlag: "partial read scope",
},
{
name: "update v1",
shortcut: "+update",
apiVersion: "v1",
shortcutHelp: "Update a Lark document",
versionedFlag: "--mode",
},
{
name: "update v2",
shortcut: "+update",
apiVersion: "v2",
shortcutHelp: "Update a Lark document",
versionedFlag: "--command",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
program := newRegisterTestProgramWithTipsHelp()
RegisterShortcuts(program, newRegisterTestFactory(t))
cmd, _, err := program.Find([]string{"docs", tt.shortcut})
if err != nil {
t.Fatalf("find docs %s command: %v", tt.shortcut, err)
}
if cmd == nil || cmd.Name() != tt.shortcut {
t.Fatalf("docs %s shortcut not mounted: %#v", tt.shortcut, cmd)
}
if err := cmd.Flags().Set("api-version", tt.apiVersion); err != nil {
t.Fatalf("set docs %s api-version: %v", tt.shortcut, err)
}
var out bytes.Buffer
cmd.SetOut(&out)
if err := cmd.Help(); err != nil {
t.Fatalf("docs %s help failed: %v", tt.shortcut, err)
}
for _, want := range []string{
tt.shortcutHelp,
tt.versionedFlag,
"Tips:",
"Agent version rule",
"use --api-version v2 only when the installed lark-doc skill explicitly instructs",
"otherwise use the default v1 flags",
"if the skill does not mention v2",
"legacy v1 examples and flags",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("docs %s %s help missing %q:\n%s", tt.shortcut, tt.apiVersion, want, out.String())
}
}
for _, unwanted := range []string{
"[NOTE]",
"Use --api-version v2 for the latest API",
} {
if strings.Contains(out.String(), unwanted) {
t.Fatalf("docs %s %s help should not include %q:\n%s", tt.shortcut, tt.apiVersion, unwanted, out.String())
}
}
})
}
}
func TestRegisterShortcutsReusesExistingServiceCommand(t *testing.T) {
program := &cobra.Command{Use: "root"}
existingBase := &cobra.Command{Use: "base", Short: "existing base service"}
program.AddCommand(existingBase)
RegisterShortcuts(program, &cmdutil.Factory{})
RegisterShortcuts(program, newRegisterTestFactory(t))
baseCount := 0
for _, command := range program.Commands() {

View File

@@ -1,11 +1,11 @@
---
name: lark-doc
version: 2.0.0
description: "飞书云文档:创建和编辑飞书文档。默认使用 DocxXML 格式(也支持 Markdown。创建文档、获取文档内容支持 simple/with-ids/full 三种导出详细度,以及 full/outline/range/keyword/section 五种局部读取模式可按目录、block id 区间、关键词或标题自动成节只拉部分内容以节省上下文、更新文档八种指令str_replace/block_insert_after/block_copy_insert_after/block_replace/block_delete/block_move_after/overwrite/append、上传和下载文档中的图片和文件、搜索云空间文档。当用户需要创建或编辑飞书文档、读取文档内容、在文档中插入图片、搜索云空间文档时使用如果用户是想按名称或关键词先定位电子表格、报表等云空间对象也优先使用本 skill 的 docs +search 做资源发现。"
description: "飞书云文档v2:创建和编辑飞书文档。使用本 skill 时docs +create、docs +fetch、docs +update 必须携带 --api-version v2默认使用 DocxXML 格式(也支持 Markdown。创建文档、获取文档内容支持 simple/with-ids/full 三种导出详细度,以及 full/outline/range/keyword/section 五种局部读取模式可按目录、block id 区间、关键词或标题自动成节只拉部分内容以节省上下文、更新文档八种指令str_replace/block_insert_after/block_copy_insert_after/block_replace/block_delete/block_move_after/overwrite/append、上传和下载文档中的图片和文件、搜索云空间文档。当用户需要创建或编辑飞书文档、读取文档内容、在文档中插入图片、搜索云空间文档时使用如果用户是想按名称或关键词先定位电子表格、报表等云空间对象也优先使用本 skill 的 docs +search 做资源发现。"
metadata:
requires:
bins: ["lark-cli"]
cliHelp: "lark-cli docs --help"
cliHelp: "lark-cli docs --api-version v2 --help; lark-cli docs +create --api-version v2 --help; lark-cli docs +fetch --api-version v2 --help; lark-cli docs +update --api-version v2 --help"
---
# docs (v2)

View File

@@ -74,7 +74,7 @@ lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项
## 最佳实践
- 文档标题从内容中自动提取XML `<title>` 或 Markdown `#`),不要在内容开头重复写标题
- 创建较长的文档时,先创建基础内容,再用 `docs +update --command block_insert_after` 分段追加
- **创建较长的文档时只建骨架**`--content` 仅传标题 + 各级 heading + 简短占位摘要;正文留给后续 `docs +update --command append``block_insert_after` 分段追加。一次性塞超长 `--content` 既容易触发参数限制,调试也更难。
- **视觉丰富度**:必须遵循 [`lark-doc-style.md`](style/lark-doc-style.md) 中的样式指南,主动使用结构化 block 丰富文档
## 参考

View File

@@ -49,21 +49,21 @@ lark-cli docs +fetch --api-version v2 --doc Z1Fj...tnAc \
| `outline` | 不知道结构,先看目录 | `--max-depth`(标题层级上限) | 扁平列出所有标题,**包括嵌在容器里的内嵌标题**(如 callout 里的 h3这些 id 可直接作后续 `section` / `range` 端点 |
| `section` | 读某个标题对应的整节 | `--start-block-id`(必填) | 顶层标题 → 展开到下一同级/更高级标题前;容器内节点(含内嵌标题) → 按"最小包容单元"返回容器/表格切片,不做 heading 扩展;顶层非标题块 → 仅该块 |
| `range` | 已知精确起止 | `--start-block-id` / `--end-block-id` 至少一个;`-1` = 读到末尾 | 两端同顶层 → 顶层序列切片;两端同一容器 → 容器整体;两端同一表格 → 瘦身切片;**跨顶层 → 端点所在顶层块整块输出,不做瘦身** |
| `keyword` | 只有模糊关键词 | `--keyword`不区分大小写、子串,`\|` 分隔多 OR | 每处命中按"最小包容单元"输出;**自动去重**(同容器多命中 → 单个容器,同表格多行命中 → 合并切片) |
| `keyword` | 只有模糊关键词 | `--keyword`**多级自动 fallback**:子串 → 归一化 → 分词形变 → RE2 正则;`\|` 分隔多分支 OR | 每处命中按"最小包容单元"输出;**自动去重**(同容器多命中 → 单个容器,同表格多行命中 → 合并切片) |
> 💡 **多关键词用 `\|` 拼接OR 语义,任一命中即返回)**:例 `"部署\|发布\|上线"`,三词任一命中都进结果,适合**同义词/别名/多业务术语**一次召回(如 `bug\|缺陷\|故障`)。
**设置 `--scope` 时共用** `--context-before` / `--context-after` / `--max-depth`
- `--max-depth``outline` = 标题层级上限3 = h1~h3其它模式 = 被选块的子树遍历深度(`-1` 不限,`0` 仅块自身)。
- `--context-before/--context-after`**只对整块顶层单元生效**;命中落在容器/表格内(返回容器或切片)时 before/after 被忽略,需要更大范围改用 `section` / `range` 显式指定。
**决策顺序**(核心原则:**局部获取优于全量获取**能精确到节/区间就绝不全量拉取;**任何文档的第一次读取都应从 `outline` 开始**
1. **第一次接触文档 / 不知道结构**`outline` 探测目录(**强制首步,无论文档是"目标"还是"引用源"**),再回到 2/3 精读
2. 改/读某个**标题对应的整节** → `section`(最省心,**首选精读方式**
3. 精确自定义起止 / 跨节连续区间 → `range`
4. 只有模糊关键词 → `keyword`
5. **兜底**确实需要整篇文档时才不传 `--scope`(默认整篇);**不要为省事读整篇**,局部模式上下文更省、响应更快
**推荐双步流程**`outline --max-depth 3` 拿目录 → `section --start-block-id <标题id> --detail with-ids` 精读该节。
**决策顺序**(核心原则:**局部获取优于全量获取**根据需求形态选起点,必要时多步组合收敛范围
1. 需求**直接给出待查的具体术语/错误码/标识** → 直接走 `keyword` 粗匹配(多级 fallback 自动覆盖形变),需要更大上下文时用返回的 `top-block-id``section` / `range`
2. 需求**指向某个章节/标题**"修改 XX 章"、"总结第 3 节"、"关于 xx 的内容")→ 先 `outline --max-depth 3` 拿目录 → `section --start-block-id <标题id>` 精读
3. 已知**精确起止 / 跨节连续区间**`range`
4. **结构未知且无明确关键词/章节线索**`outline` 探测,再回到 2/3
5. **兜底**仅在确需整篇时才省略 `--scope`不要为省事直接读整篇
## 局部读取的输出结构:`<fragment>` 与 `<excerpt>`
@@ -108,7 +108,7 @@ lark-cli docs +fetch --api-version v2 --doc Z1Fj...tnAc \
| `--scope` | 否 | `outline` \| `range` \| `keyword` \| `section`(省略 = 读整篇) |
| `--start-block-id` | 否 | `range`/`section` 起始/锚点 id`section` 必填) |
| `--end-block-id` | 否 | `range` 结束 id`-1` 表示读到末尾 |
| `--keyword` | 否 | `keyword` 模式关键词;`\|` 分隔多 OR |
| `--keyword` | 否 | `keyword` 模式关键词**4 层自动 fallback**(子串 → 归一化 → 分词形变 → RE2 正则)`\|` 分隔多分支 OR |
| `--context-before` | 否 | 命中前拉几个兄弟块(仅对顶层单元生效,默认 `0` |
| `--context-after` | 否 | 命中后拉几个兄弟块(仅对顶层单元生效,默认 `0` |
| `--max-depth` | 否 | `outline` = 标题层级上限;其它 = 子树深度(`-1` 不限,默认) |

View File

@@ -85,7 +85,7 @@ p, h1-h9, ul, ol, li, table, thead, tbody, tr, th, td, blockquote, pre, code, hr
- 合并单元格仅起始格输出 `colspan` / `rowspan`,被合并的格不出现
# 六、美化系统
- 颜色优先使用命名色,也可写 `rgb(r,g,b)` / `rgba(r,g,b,a)`。**基础色(6 色)**gray, red, orange, yellow, green, blue
- 颜色优先使用命名色,也可写 `rgb(r,g,b)` / `rgba(r,g,b,a)`。**基础色(7 色)**red, orange, yellow, green, blue, purple, gray
| 属性 | 支持的命名色 |
|-|-|
| 文字颜色 `<span text-color>` | 基础色 |

View File

@@ -20,7 +20,9 @@
1. 分析用户需求:受众、目的、范围
2. 设计大纲——每个 h1/h2 章节至少规划 1 个非文本 block
3. `docs +create --api-version v2` 创建文档:标题 + 开头 `<callout>` + 骨架(各级标题 + 简短占位摘要
3. `docs +create --api-version v2` **只建骨架**:标题 + 开头 `<callout>` + 各级标题 + 每节一句占位摘要
- ⚠️ **不要**一次性把完整章节内容塞进 `--content`。超长 `--content` 容易触发字符/参数限制。
- 完整内容留到第二波,由各 Agent 用 `docs +update --command append``block_insert_after` 分段写入。
### 第二波 — 内容撰写(并行 Agent
@@ -46,5 +48,3 @@
## Agent 子任务要求
Spawn Agent 时必须提供:文档 token、章节范围标题/block ID`lark-doc-xml.md``lark-doc-style.md` 路径、具体的 `docs +update` command 和 `--block-id`
章节较多时,先 `docs +create` 建骨架,再分段 `append` 追加,比一次性超长 `--content` 更可靠。