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
1 change: 1 addition & 0 deletions shortcuts/doc/docs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
Expand Down
1 change: 1 addition & 0 deletions shortcuts/doc/docs_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/doc/docs_fetch_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
1 change: 1 addition & 0 deletions shortcuts/doc/docs_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
Expand Down
64 changes: 63 additions & 1 deletion shortcuts/doc/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -18,3 +35,48 @@
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

Check warning on line 45 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L43-L45

Added lines #L43 - L45 were not covered by tests
}
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
})

Check warning on line 53 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L47-L53

Added lines #L47 - L53 were not covered by tests
}

defaultHelp := cmd.HelpFunc()
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
Comment thread
SunPeiYang996 marked this conversation as resolved.
if cmd != serviceCmd {
defaultHelp(cmd, args)
return

Check warning on line 60 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L56-L60

Added lines #L56 - L60 were not covered by tests
}

apiVersion, _ := cmd.Flags().GetString("api-version")
previousLong := cmd.Long
if apiVersion == "v2" {
cmd.Long = strings.TrimSpace(docsServiceHelpV2)
} else {
cmd.Long = strings.TrimSpace(docsServiceHelpDefault)

Check warning on line 68 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L63-L68

Added lines #L63 - L68 were not covered by tests
}
defer func() {
cmd.Long = previousLong
}()

Check warning on line 72 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L70-L72

Added lines #L70 - L72 were not covered by tests

defaultHelp(cmd, args)
out := cmd.OutOrStdout()
fmt.Fprintln(out)
fmt.Fprintln(out, "Tips:")
for _, tip := range docsVersionSelectionTips {
fmt.Fprintf(out, " • %s\n", tip)

Check warning on line 79 in shortcuts/doc/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/shortcuts.go#L74-L79

Added lines #L74 - L79 were not covered by tests
}
Comment thread
SunPeiYang996 marked this conversation as resolved.
})
}
7 changes: 0 additions & 7 deletions shortcuts/doc/versioned_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
})
}

Expand Down
3 changes: 3 additions & 0 deletions shortcuts/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
205 changes: 202 additions & 3 deletions shortcuts/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -80,12 +109,182 @@ func TestRegisterShortcutsMountsDocsMediaPreview(t *testing.T) {
}
}

func TestRegisterShortcutsDocsHelpAddsVersionSelectorAndLegacyTips(t *testing.T) {
program := &cobra.Command{Use: "root"}
RegisterShortcuts(program, newRegisterTestFactory(t))

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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() {
Expand Down
4 changes: 2 additions & 2 deletions skills/lark-doc/SKILL.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Loading
Loading