diff --git a/shortcuts/doc/docs_create.go b/shortcuts/doc/docs_create.go index b0067deed..cae1205c4 100644 --- a/shortcuts/doc/docs_create.go +++ b/shortcuts/doc/docs_create.go @@ -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"}}, diff --git a/shortcuts/doc/docs_fetch.go b/shortcuts/doc/docs_fetch.go index 7aa6ccc2c..f76ec52ad 100644 --- a/shortcuts/doc/docs_fetch.go +++ b/shortcuts/doc/docs_fetch.go @@ -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"}}, diff --git a/shortcuts/doc/docs_fetch_v2.go b/shortcuts/doc/docs_fetch_v2.go index e70f1c238..0b3eaee3f 100644 --- a/shortcuts/doc/docs_fetch_v2.go +++ b/shortcuts/doc/docs_fetch_v2.go @@ -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"}, diff --git a/shortcuts/doc/docs_update.go b/shortcuts/doc/docs_update.go index b12c3ab1f..9353e54dd 100644 --- a/shortcuts/doc/docs_update.go +++ b/shortcuts/doc/docs_update.go @@ -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"}}, diff --git a/shortcuts/doc/shortcuts.go b/shortcuts/doc/shortcuts.go index 30ba91e4d..aac3366ca 100644 --- a/shortcuts/doc/shortcuts.go +++ b/shortcuts/doc/shortcuts.go @@ -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) + } + }) +} diff --git a/shortcuts/doc/versioned_help.go b/shortcuts/doc/versioned_help.go index 2111829ce..053bb3f36 100644 --- a/shortcuts/doc/versioned_help.go +++ b/shortcuts/doc/versioned_help.go @@ -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()) - } }) } diff --git a/shortcuts/register.go b/shortcuts/register.go index 534163ec2..c6d95654b 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -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) diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 81d316fef..a05e1c839 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -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() { diff --git a/skills/lark-doc/SKILL.md b/skills/lark-doc/SKILL.md index 3bdad9d8f..7d70830ce 100644 --- a/skills/lark-doc/SKILL.md +++ b/skills/lark-doc/SKILL.md @@ -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) diff --git a/skills/lark-doc/references/lark-doc-create.md b/skills/lark-doc/references/lark-doc-create.md index 67d641fd0..cb8689d3e 100644 --- a/skills/lark-doc/references/lark-doc-create.md +++ b/skills/lark-doc/references/lark-doc-create.md @@ -74,7 +74,7 @@ lark-cli docs +create --api-version v2 --doc-format markdown --content $'# 项 ## 最佳实践 - 文档标题从内容中自动提取(XML `