-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: expand folder children via /files/:file_key/folder (clean) #2599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71458c3
70f3b62
33840b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package convertlib | ||
|
|
||
| // C4-C6:fetchFolderChildrenTree 单测(mock httpmock,不依赖真实 openapi) | ||
| // 覆盖 XML 一层输出(folder name+key+child_count / file name+key / 子文件夹 child_count / has_more) | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/larksuite/cli/internal/core" | ||
| "github.com/larksuite/cli/internal/httpmock" | ||
| "github.com/larksuite/cli/internal/cmdutil" | ||
| "github.com/larksuite/cli/shortcuts/common" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func folderTestRuntime(t *testing.T) (*common.RuntimeContext, *httpmock.Registry) { | ||
| t.Helper() | ||
| cfg := &core.CliConfig{Brand: core.BrandFeishu, AppID: "cli_x"} | ||
| f, _, _, reg := cmdutil.TestFactory(t, cfg) | ||
| rt := common.TestNewRuntimeContextForAPI(context.Background(), &cobra.Command{Use: "+x"}, cfg, f, core.AsUser) | ||
| return rt, reg | ||
| } | ||
|
|
||
| // C4:正常展开一层(文件 + 子文件夹 + child_count),无 has_more(items == all_count) | ||
| func TestFetchFolderChildrenTree_XMLOneLevel(t *testing.T) { | ||
| rt, reg := folderTestRuntime(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| Method: "GET", | ||
| URL: "/open-apis/im/v1/files/fld_root/folder", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, | ||
| "data": map[string]interface{}{ | ||
| "items": []interface{}{ | ||
| map[string]interface{}{"file_key": "f1", "name": "报告.pdf", "is_folder": false}, | ||
| map[string]interface{}{"file_key": "f2", "name": "文档.docx", "is_folder": false}, | ||
| map[string]interface{}{"file_key": "f3", "name": "子文件夹", "is_folder": true, "children_count": float64(3)}, | ||
| }, | ||
| "all_count": float64(3), | ||
| }, | ||
| }, | ||
| }) | ||
| got := fetchFolderChildrenTree(rt, "fld_root", "tmpavatra", "om_123") | ||
| want := `<folder name="tmpavatra" key="fld_root" child_count="3"><file name="报告.pdf" key="f1"/><file name="文档.docx" key="f2"/><folder name="子文件夹" key="f3" child_count="3"/></folder>` | ||
| if got != want { | ||
| t.Fatalf("fetchFolderChildrenTree() = %q, want %q", got, want) | ||
| } | ||
| } | ||
|
|
||
| // C4b:items < all_count 时根 folder 带 has_more="true" | ||
| func TestFetchFolderChildrenTree_HasMore(t *testing.T) { | ||
| rt, reg := folderTestRuntime(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| Method: "GET", | ||
| URL: "/open-apis/im/v1/files/fld_root/folder", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, | ||
| "data": map[string]interface{}{ | ||
| "items": []interface{}{ | ||
| map[string]interface{}{"file_key": "f1", "name": "a.pdf", "is_folder": false}, | ||
| }, | ||
| "all_count": float64(100), | ||
| }, | ||
| }, | ||
| }) | ||
| got := fetchFolderChildrenTree(rt, "fld_root", "big", "om_123") | ||
| want := `<folder name="big" key="fld_root" child_count="100" has_more="true"><file name="a.pdf" key="f1"/></folder>` | ||
| if got != want { | ||
| t.Fatalf("fetchFolderChildrenTree() = %q, want %q", got, want) | ||
| } | ||
| } | ||
|
|
||
| // C5:API 失败(error/nil)→ 返回空串(调用方降级旧输出) | ||
| func TestFetchFolderChildrenTree_APIFailure(t *testing.T) { | ||
| rt, reg := folderTestRuntime(t) | ||
| // 不注册 stub → httpmock 返回错误 | ||
| got := fetchFolderChildrenTree(rt, "fld_root", "x", "om_123") | ||
| if got != "" { | ||
| t.Fatalf("fetchFolderChildrenTree() on API failure = %q, want empty (caller downgrades)", got) | ||
| } | ||
| _ = reg | ||
| } | ||
|
|
||
| // C6:items 空 → 返回空串(降级) | ||
| func TestFetchFolderChildrenTree_EmptyItems(t *testing.T) { | ||
| rt, reg := folderTestRuntime(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| Method: "GET", | ||
| URL: "/open-apis/im/v1/files/fld_root/folder", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, | ||
| "data": map[string]interface{}{ | ||
| "items": []interface{}{}, | ||
| "all_count": float64(0), | ||
| }, | ||
| }, | ||
| }) | ||
| got := fetchFolderChildrenTree(rt, "fld_root", "x", "om_123") | ||
| if got != "" { | ||
| t.Fatalf("fetchFolderChildrenTree() empty items = %q, want empty", got) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,14 @@ | |
| package convertlib | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "regexp" | ||
| "strings" | ||
|
|
||
| "github.com/larksuite/cli/shortcuts/common" | ||
| larkcore "github.com/larksuite/oapi-sdk-go/v3/core" | ||
| ) | ||
|
|
||
| type stickerConverter struct{} | ||
|
|
@@ -72,12 +77,92 @@ func (folderConverter) Convert(ctx *ConvertContext) string { | |
| return "[Folder]" | ||
| } | ||
| name, _ := parsed["file_name"].(string) | ||
|
|
||
| // 展开一层:调 openapi children(recursive=false),输出第一层 + children_count + 深层提示 | ||
| // 需要 Runtime + MessageID(srctype=message&srcid=MessageID);不可用时降级为旧输出 | ||
| if ctx.Runtime != nil && ctx.MessageID != "" { | ||
| if tree := fetchFolderChildrenTree(ctx.Runtime, key, name, ctx.MessageID); tree != "" { | ||
| return tree | ||
| } | ||
| } | ||
| if name != "" { | ||
| return fmt.Sprintf(`<folder key="%s" name="%s"/>`, cardEscapeAttr(key), cardEscapeAttr(name)) | ||
| } | ||
| return fmt.Sprintf(`<folder key="%s"/>`, cardEscapeAttr(key)) | ||
| } | ||
|
|
||
| // fetchFolderChildrenTree 调 openapi 展开文件夹一层,返回树形文本(含 children_count 深层提示)。 | ||
| // 失败时返回空串,由调用方降级为旧输出。 | ||
| func fetchFolderChildrenTree(runtime *common.RuntimeContext, folderKey, folderName, messageID string) string { | ||
| data, err := runtime.DoAPIJSONTyped(http.MethodGet, "/open-apis/im/v1/files/"+folderKey+"/folder", | ||
| larkcore.QueryParams{ | ||
| "srctype": []string{"message"}, | ||
| "srcid": []string{messageID}, | ||
| "recursive": []string{"false"}, | ||
| }, nil) | ||
|
Comment on lines
+96
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C4 --glob '*.go' \
'DoAPIJSONTyped\(|/open-apis/im/v1/files/.*/folder|children_count|all_count' \
shortcuts internalRepository: larksuite/cli Length of output: 32126 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- repository conventions and learnings ---'
head -5 /tmp/coderabbit-repo-knowledge/larksuite-cli-f41f2344/*/*.md 2>/dev/null || true
printf '%s\n' '--- changed implementation ---'
cat -n shortcuts/im/convert_lib/misc.go | sed -n '90,150p'
printf '%s\n' '--- API boundary implementation ---'
cat -n shortcuts/common/runner.go | sed -n '560,625p'
printf '%s\n' '--- module and SDK references ---'
rg -n 'lark(o|core)|openapi|generated|files/.*/folder|Folder' go.mod go.sum shortcuts internal --glob '*.go' --glob 'go.mod' --glob 'go.sum' | head -250Repository: larksuite/cli Length of output: 40249 Project the folder-children response into typed structs.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| if err != nil || data == nil { | ||
| return "" | ||
| } | ||
| rawItems, _ := data["items"].([]interface{}) | ||
| if len(rawItems) == 0 { | ||
| return "" | ||
| } | ||
| // 只展开一层:file 用 <file name key/>;子文件夹用 <folder name key child_count/>(不递归,child_count 提示深层) | ||
| // 根 folder 带 child_count(=all_count 子项总数)+ has_more(items 数 < all_count 时标注还有更多未展示) | ||
| hasMore := false | ||
| var allCount int64 | ||
| if v, ok := data["all_count"]; ok { | ||
| allCount = numToInt64(v) | ||
| if allCount > int64(len(rawItems)) { | ||
| hasMore = true | ||
| } | ||
| } | ||
| var b strings.Builder | ||
| b.WriteString(`<folder name="` + cardEscapeAttr(folderName) + `" key="` + cardEscapeAttr(folderKey) + `"`) | ||
| if allCount > 0 { | ||
| fmt.Fprintf(&b, ` child_count="%d"`, allCount) | ||
| } | ||
| if hasMore { | ||
| b.WriteString(` has_more="true"`) | ||
| } | ||
| b.WriteString(`>`) | ||
| for _, raw := range rawItems { | ||
| item, _ := raw.(map[string]interface{}) | ||
| k, _ := item["file_key"].(string) | ||
| n, _ := item["name"].(string) | ||
| isFolder, _ := item["is_folder"].(bool) | ||
| if isFolder { | ||
| cc := numToInt64(item["children_count"]) | ||
| fmt.Fprintf(&b, `<folder name="%s" key="%s" child_count="%d"/>`, | ||
| cardEscapeAttr(n), cardEscapeAttr(k), cc) | ||
| } else { | ||
| fmt.Fprintf(&b, `<file name="%s" key="%s"/>`, cardEscapeAttr(n), cardEscapeAttr(k)) | ||
| } | ||
| } | ||
| b.WriteString("</folder>") | ||
| return b.String() | ||
| } | ||
|
|
||
|
|
||
| // numToInt64 兼容 JSON number(json.Number)/ float64 / int 的类型转换。 | ||
| func numToInt64(v interface{}) int64 { | ||
| switch n := v.(type) { | ||
| case json.Number: | ||
| if i, err := n.Int64(); err == nil { | ||
| return i | ||
| } | ||
| case float64: | ||
| return int64(n) | ||
| case float32: | ||
| return int64(n) | ||
| case int: | ||
| return int64(n) | ||
| case int64: | ||
| return n | ||
| } | ||
| return 0 | ||
| } | ||
|
|
||
| type calendarEventConverter struct{} | ||
|
|
||
| // Convert converts a share_calendar_event message content JSON to human-readable string. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Expand regression coverage for folder expansion.
Assert that the folder-children request sends
srctype=message, the expectedsrcid, andrecursive=false. Also add converter-level tests covering successful expansion and flat-folder fallback when runtime context is unavailable or the API call fails, so reverting the changed conversion branch makes the tests fail.📍 Affects 1 file
shortcuts/im/convert_lib/folder_test.go#L43-L47(this comment)shortcuts/im/convert_lib/folder_test.go#L30-L30🤖 Prompt for AI Agents
Source: Coding guidelines