From 69bcdd9e350ea5d5027314ce34d3c4bbfeef3d3f Mon Sep 17 00:00:00 2001 From: YangJunzhou-01 Date: Mon, 30 Mar 2026 23:00:41 +0800 Subject: [PATCH] feat: add auto-pagination to messages search and update lark-im docs (#30) Change-Id: Ic50e891d2385c2e3ac902cd89d95c3db99f94050 --- shortcuts/im/builders_test.go | 50 +++ shortcuts/im/im_messages_search.go | 198 ++++++++++-- .../im/im_messages_search_execute_test.go | 285 ++++++++++++++++++ skills/lark-im/SKILL.md | 3 +- .../references/lark-im-messages-search.md | 40 ++- 5 files changed, 531 insertions(+), 45 deletions(-) create mode 100644 shortcuts/im/im_messages_search_execute_test.go diff --git a/shortcuts/im/builders_test.go b/shortcuts/im/builders_test.go index 36e06521d..a4a54de09 100644 --- a/shortcuts/im/builders_test.go +++ b/shortcuts/im/builders_test.go @@ -447,6 +447,17 @@ func TestShortcutValidateBranches(t *testing.T) { } }) + t.Run("ImMessagesSearch invalid page limit", func(t *testing.T) { + runtime := newTestRuntimeContext(t, map[string]string{ + "query": "incident", + "page-limit": "41", + }, nil) + err := ImMessagesSearch.Validate(context.Background(), runtime) + if err == nil || !strings.Contains(err.Error(), "--page-limit must be an integer between 1 and 40") { + t.Fatalf("ImMessagesSearch.Validate() error = %v", err) + } + }) + t.Run("ImMessagesSearch invalid sender id", func(t *testing.T) { runtime := newTestRuntimeContext(t, map[string]string{ "sender": "user_1", @@ -479,6 +490,45 @@ func TestShortcutValidateBranches(t *testing.T) { }) } +func TestMessagesSearchPaginationConfig(t *testing.T) { + t.Run("default single page", func(t *testing.T) { + runtime := newTestRuntimeContext(t, nil, nil) + autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime) + if autoPaginate { + t.Fatal("messagesSearchPaginationConfig() autoPaginate = true, want false") + } + if pageLimit != messagesSearchDefaultPageLimit { + t.Fatalf("messagesSearchPaginationConfig() pageLimit = %d, want %d", pageLimit, messagesSearchDefaultPageLimit) + } + }) + + t.Run("page all uses max limit", func(t *testing.T) { + runtime := newTestRuntimeContext(t, nil, map[string]bool{ + "page-all": true, + }) + autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime) + if !autoPaginate { + t.Fatal("messagesSearchPaginationConfig() autoPaginate = false, want true") + } + if pageLimit != messagesSearchMaxPageLimit { + t.Fatalf("messagesSearchPaginationConfig() pageLimit = %d, want %d", pageLimit, messagesSearchMaxPageLimit) + } + }) + + t.Run("explicit page limit enables auto pagination", func(t *testing.T) { + runtime := newTestRuntimeContext(t, map[string]string{ + "page-limit": "3", + }, nil) + autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime) + if !autoPaginate { + t.Fatal("messagesSearchPaginationConfig() autoPaginate = false, want true") + } + if pageLimit != 3 { + t.Fatalf("messagesSearchPaginationConfig() pageLimit = %d, want 3", pageLimit) + } + }) +} + func TestShortcutDryRunShapes(t *testing.T) { t.Run("ImChatCreate dry run includes params and body", func(t *testing.T) { runtime := newTestRuntimeContext(t, map[string]string{ diff --git a/shortcuts/im/im_messages_search.go b/shortcuts/im/im_messages_search.go index 3b0f7dfc4..d122cc23b 100644 --- a/shortcuts/im/im_messages_search.go +++ b/shortcuts/im/im_messages_search.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "strconv" + "strings" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" @@ -16,6 +17,15 @@ import ( larkcore "github.com/larksuite/oapi-sdk-go/v3/core" ) +const ( + messagesSearchDefaultPageSize = 20 + messagesSearchMaxPageSize = 50 + messagesSearchDefaultPageLimit = 20 + messagesSearchMaxPageLimit = 40 + messagesSearchMGetBatchSize = 50 + messagesSearchChatBatchSize = 50 +) + var ImMessagesSearch = common.Shortcut{ Service: "im", Command: "+messages-search", @@ -37,6 +47,8 @@ var ImMessagesSearch = common.Shortcut{ {Name: "end", Desc: "end time(ISO 8601) with local timezone offset (e.g. 2026-03-25T23:59:59+08:00)"}, {Name: "page-size", Default: "20", Desc: "page size (1-50)"}, {Name: "page-token", Desc: "page token"}, + {Name: "page-all", Type: "bool", Desc: "automatically paginate search results"}, + {Name: "page-limit", Type: "int", Default: "20", Desc: "max search pages when auto-pagination is enabled (default 20, max 40)"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { req, err := buildMessagesSearchRequest(runtime) @@ -49,8 +61,14 @@ var ImMessagesSearch = common.Shortcut{ dryParams[k] = vs[0] } } - return common.NewDryRunAPI(). - Desc("Step 1: search messages"). + autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime) + d := common.NewDryRunAPI() + if autoPaginate { + d = d.Desc(fmt.Sprintf("Step 1: search messages (auto-paginates up to %d page(s))", pageLimit)) + } else { + d = d.Desc("Step 1: search messages") + } + return d. POST("/open-apis/im/v1/messages/search"). Params(dryParams). Body(req.body). @@ -67,12 +85,10 @@ var ImMessagesSearch = common.Shortcut{ return err } - searchData, err := runtime.DoAPIJSON(http.MethodPost, "/open-apis/im/v1/messages/search", req.params, req.body) + rawItems, hasMore, nextPageToken, truncatedByLimit, pageLimit, err := searchMessages(runtime, req) if err != nil { return err } - rawItems, _ := searchData["items"].([]interface{}) - hasMore, nextPageToken := common.PaginationMeta(searchData) if len(rawItems) == 0 { outData := map[string]interface{}{ @@ -99,8 +115,7 @@ var ImMessagesSearch = common.Shortcut{ } // ── Step 2: Batch fetch message details (mget) ── - mgetURL := buildMGetURL(messageIds) - mgetData, err := runtime.DoAPIJSON(http.MethodGet, mgetURL, nil, nil) + msgItems, err := batchMGetMessages(runtime, messageIds) if err != nil { // Fallback when mget fails: return ID list only outData := map[string]interface{}{ @@ -118,37 +133,22 @@ var ImMessagesSearch = common.Shortcut{ }) return nil } - msgItems, _ := mgetData["items"].([]interface{}) // ── Step 3: Batch fetch chat info ── - chatIdSet := map[string]bool{} + chatIds := make([]string, 0, len(msgItems)) + chatSeen := make(map[string]bool) for _, item := range msgItems { m, _ := item.(map[string]interface{}) if chatId, _ := m["chat_id"].(string); chatId != "" { - chatIdSet[chatId] = true + if !chatSeen[chatId] { + chatSeen[chatId] = true + chatIds = append(chatIds, chatId) + } } } chatContexts := map[string]map[string]interface{}{} - if len(chatIdSet) > 0 { - chatIds := make([]string, 0, len(chatIdSet)) - for id := range chatIdSet { - chatIds = append(chatIds, id) - } - chatRes, chatErr := runtime.DoAPIJSON( - http.MethodPost, "/open-apis/im/v1/chats/batch_query", - larkcore.QueryParams{"user_id_type": []string{"open_id"}}, - map[string]interface{}{"chat_ids": chatIds}, - ) - if chatErr == nil { - if chatItems, ok := chatRes["items"].([]interface{}); ok { - for _, ci := range chatItems { - cm, _ := ci.(map[string]interface{}) - if cid, _ := cm["chat_id"].(string); cid != "" { - chatContexts[cid] = cm - } - } - } - } + if len(chatIds) > 0 { + chatContexts = batchQueryChatContexts(runtime, chatIds) } // ── Step 4: Format message content + attach chat context ── @@ -225,6 +225,9 @@ var ImMessagesSearch = common.Shortcut{ moreHint = " (more available, use --page-token to fetch next page)" } fmt.Fprintf(w, "\n%d search result(s)%s\n", len(enriched), moreHint) + if truncatedByLimit { + fmt.Fprintf(w, "warning: stopped after fetching %d page(s); use --page-limit, --page-all, or --page-token to continue\n", pageLimit) + } }) return nil }, @@ -247,6 +250,14 @@ func buildMessagesSearchRequest(runtime *common.RuntimeContext) (*messagesSearch endFlag := runtime.Str("end") pageSizeStr := runtime.Str("page-size") pageToken := runtime.Str("page-token") + pageLimitStr := strings.TrimSpace(runtime.Str("page-limit")) + + if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") { + pageLimit, err := strconv.Atoi(pageLimitStr) + if err != nil || pageLimit < 1 || pageLimit > messagesSearchMaxPageLimit { + return nil, output.ErrValidation("--page-limit must be an integer between 1 and 40") + } + } filter := map[string]interface{}{} timeRange := map[string]interface{}{} @@ -322,14 +333,14 @@ func buildMessagesSearchRequest(runtime *common.RuntimeContext) (*messagesSearch body["filter"] = filter } - pageSize := 20 + pageSize := messagesSearchDefaultPageSize if pageSizeStr != "" { n, err := strconv.Atoi(pageSizeStr) if err != nil || n < 1 { return nil, output.ErrValidation("--page-size must be an integer between 1 and 50") } - if n > 50 { - n = 50 + if n > messagesSearchMaxPageSize { + n = messagesSearchMaxPageSize } pageSize = n } @@ -346,3 +357,124 @@ func buildMessagesSearchRequest(runtime *common.RuntimeContext) (*messagesSearch body: body, }, nil } + +func messagesSearchPaginationConfig(runtime *common.RuntimeContext) (autoPaginate bool, pageLimit int) { + autoPaginate = runtime.Bool("page-all") + if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") { + autoPaginate = true + } + + pageLimit = messagesSearchDefaultPageLimit + if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") { + if n, err := strconv.Atoi(strings.TrimSpace(runtime.Str("page-limit"))); err == nil && n > 0 { + pageLimit = min(n, messagesSearchMaxPageLimit) + } + } else if runtime.Bool("page-all") { + pageLimit = messagesSearchMaxPageLimit + } + return autoPaginate, pageLimit +} + +func searchMessages(runtime *common.RuntimeContext, req *messagesSearchRequest) ([]interface{}, bool, string, bool, int, error) { + autoPaginate, pageLimit := messagesSearchPaginationConfig(runtime) + pageToken := "" + if tokens := req.params["page_token"]; len(tokens) > 0 { + pageToken = tokens[0] + } + + pageSize := strconv.Itoa(messagesSearchDefaultPageSize) + if sizes := req.params["page_size"]; len(sizes) > 0 { + pageSize = sizes[0] + } + + var ( + allItems []interface{} + lastHasMore bool + lastPageToken string + truncatedByLimit bool + pageCount int + ) + + for { + pageCount++ + params := larkcore.QueryParams{ + "page_size": []string{pageSize}, + } + if pageToken != "" { + params["page_token"] = []string{pageToken} + } + + searchData, err := runtime.DoAPIJSON(http.MethodPost, "/open-apis/im/v1/messages/search", params, req.body) + if err != nil { + return nil, false, "", false, pageLimit, err + } + + items, _ := searchData["items"].([]interface{}) + allItems = append(allItems, items...) + lastHasMore, lastPageToken = common.PaginationMeta(searchData) + + if !autoPaginate || !lastHasMore || lastPageToken == "" { + break + } + if pageCount >= pageLimit { + truncatedByLimit = true + break + } + + pageToken = lastPageToken + } + + return allItems, lastHasMore, lastPageToken, truncatedByLimit, pageLimit, nil +} + +func batchMGetMessages(runtime *common.RuntimeContext, messageIds []string) ([]interface{}, error) { + var items []interface{} + for _, batch := range chunkStrings(messageIds, messagesSearchMGetBatchSize) { + mgetData, err := runtime.DoAPIJSON(http.MethodGet, buildMGetURL(batch), nil, nil) + if err != nil { + return nil, err + } + batchItems, _ := mgetData["items"].([]interface{}) + items = append(items, batchItems...) + } + return items, nil +} + +func batchQueryChatContexts(runtime *common.RuntimeContext, chatIds []string) map[string]map[string]interface{} { + chatContexts := map[string]map[string]interface{}{} + for _, batch := range chunkStrings(chatIds, messagesSearchChatBatchSize) { + chatRes, chatErr := runtime.DoAPIJSON( + http.MethodPost, "/open-apis/im/v1/chats/batch_query", + larkcore.QueryParams{"user_id_type": []string{"open_id"}}, + map[string]interface{}{"chat_ids": batch}, + ) + if chatErr != nil { + continue + } + if chatItems, ok := chatRes["items"].([]interface{}); ok { + for _, ci := range chatItems { + cm, _ := ci.(map[string]interface{}) + if cid, _ := cm["chat_id"].(string); cid != "" { + chatContexts[cid] = cm + } + } + } + } + return chatContexts +} + +func chunkStrings(items []string, chunkSize int) [][]string { + if len(items) == 0 || chunkSize <= 0 { + return nil + } + + chunks := make([][]string, 0, (len(items)+chunkSize-1)/chunkSize) + for start := 0; start < len(items); start += chunkSize { + end := start + chunkSize + if end > len(items) { + end = len(items) + } + chunks = append(chunks, items[start:end]) + } + return chunks +} diff --git a/shortcuts/im/im_messages_search_execute_test.go b/shortcuts/im/im_messages_search_execute_test.go new file mode 100644 index 000000000..6cccd1841 --- /dev/null +++ b/shortcuts/im/im_messages_search_execute_test.go @@ -0,0 +1,285 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package im + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/larksuite/cli/shortcuts/common" + "github.com/spf13/cobra" +) + +func newMessagesSearchRuntime(t *testing.T, stringFlags map[string]string, boolFlags map[string]bool, rt http.RoundTripper) *common.RuntimeContext { + t.Helper() + + runtime := newBotShortcutRuntime(t, rt) + cmd := &cobra.Command{Use: "test"} + + stringFlagNames := []string{ + "query", + "page-size", + "page-token", + "page-limit", + } + for _, name := range stringFlagNames { + cmd.Flags().String(name, "", "") + } + boolFlagNames := []string{"page-all"} + for _, name := range boolFlagNames { + cmd.Flags().Bool(name, false, "") + } + if err := cmd.ParseFlags(nil); err != nil { + t.Fatalf("ParseFlags() error = %v", err) + } + for name, value := range stringFlags { + if err := cmd.Flags().Set(name, value); err != nil { + t.Fatalf("Flags().Set(%q) error = %v", name, err) + } + } + for name, value := range boolFlags { + if err := cmd.Flags().Set(name, map[bool]string{true: "true", false: "false"}[value]); err != nil { + t.Fatalf("Flags().Set(%q) error = %v", name, err) + } + } + runtime.Cmd = cmd + runtime.Format = "pretty" + return runtime +} + +func TestImMessagesSearchExecuteAutoPaginationBatches(t *testing.T) { + var ( + searchPageTokens []string + mgetBatchSizes []int + chatBatchSizes []int + ) + + runtime := newMessagesSearchRuntime(t, map[string]string{ + "query": "incident", + "page-limit": "2", + }, map[string]bool{ + "page-all": true, + }, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "tenant_access_token"): + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "tenant_access_token": "tenant-token", + "expire": 7200, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/search"): + pageToken := req.URL.Query().Get("page_token") + searchPageTokens = append(searchPageTokens, pageToken) + switch pageToken { + case "": + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildSearchResultItems(1, 50), + "has_more": true, + "page_token": "tok_p2", + }, + }), nil + case "tok_p2": + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildSearchResultItems(51, 55), + "has_more": true, + "page_token": "tok_p3", + }, + }), nil + default: + return nil, fmt.Errorf("unexpected search page_token: %q", pageToken) + } + case strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/mget"): + ids := req.URL.Query()["message_ids"] + mgetBatchSizes = append(mgetBatchSizes, len(ids)) + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildMessageDetails(ids), + }, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/im/v1/chats/batch_query"): + var body struct { + ChatIDs []string `json:"chat_ids"` + } + rawBody, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll() error = %v", err) + } + if err := json.Unmarshal(rawBody, &body); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + chatBatchSizes = append(chatBatchSizes, len(body.ChatIDs)) + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildChatContexts(body.ChatIDs), + }, + }), nil + default: + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + } + })) + + if err := ImMessagesSearch.Execute(context.Background(), runtime); err != nil { + t.Fatalf("ImMessagesSearch.Execute() error = %v", err) + } + + if !reflect.DeepEqual(searchPageTokens, []string{"", "tok_p2"}) { + t.Fatalf("search page tokens = %#v, want %#v", searchPageTokens, []string{"", "tok_p2"}) + } + if !reflect.DeepEqual(mgetBatchSizes, []int{50, 5}) { + t.Fatalf("mget batch sizes = %#v, want %#v", mgetBatchSizes, []int{50, 5}) + } + if !reflect.DeepEqual(chatBatchSizes, []int{50, 5}) { + t.Fatalf("chat batch sizes = %#v, want %#v", chatBatchSizes, []int{50, 5}) + } + + outBuf, _ := runtime.Factory.IOStreams.Out.(*bytes.Buffer) + if outBuf == nil { + t.Fatal("stdout buffer missing") + } + output := outBuf.String() + if !strings.Contains(output, "55 search result(s)") { + t.Fatalf("stdout = %q, want search results summary", output) + } + if !strings.Contains(output, "warning: stopped after fetching 2 page(s)") { + t.Fatalf("stdout = %q, want page limit warning", output) + } +} + +func TestImMessagesSearchExecuteExplicitPageLimitWithoutPageAll(t *testing.T) { + var searchCalls int + + runtime := newMessagesSearchRuntime(t, map[string]string{ + "query": "incident", + "page-limit": "2", + }, nil, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "tenant_access_token"): + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "tenant_access_token": "tenant-token", + "expire": 7200, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/search"): + searchCalls++ + pageToken := req.URL.Query().Get("page_token") + if searchCalls == 1 { + if pageToken != "" { + return nil, fmt.Errorf("unexpected first page token: %q", pageToken) + } + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildSearchResultItems(1, 1), + "has_more": true, + "page_token": "tok_p2", + }, + }), nil + } + if pageToken != "tok_p2" { + return nil, fmt.Errorf("unexpected second page token: %q", pageToken) + } + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildSearchResultItems(2, 2), + "has_more": false, + "page_token": "", + }, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/mget"): + ids := req.URL.Query()["message_ids"] + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildMessageDetails(ids), + }, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/im/v1/chats/batch_query"): + var body struct { + ChatIDs []string `json:"chat_ids"` + } + rawBody, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll() error = %v", err) + } + if err := json.Unmarshal(rawBody, &body); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + return shortcutJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "items": buildChatContexts(body.ChatIDs), + }, + }), nil + default: + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + } + })) + + if err := ImMessagesSearch.Execute(context.Background(), runtime); err != nil { + t.Fatalf("ImMessagesSearch.Execute() error = %v", err) + } + if searchCalls != 2 { + t.Fatalf("searchCalls = %d, want 2", searchCalls) + } +} + +func buildSearchResultItems(start, end int) []interface{} { + items := make([]interface{}, 0, end-start+1) + for i := start; i <= end; i++ { + items = append(items, map[string]interface{}{ + "meta_data": map[string]interface{}{ + "message_id": fmt.Sprintf("om_%03d", i), + }, + }) + } + return items +} + +func buildMessageDetails(ids []string) []interface{} { + items := make([]interface{}, 0, len(ids)) + for _, id := range ids { + suffix := strings.TrimPrefix(id, "om_") + items = append(items, map[string]interface{}{ + "message_id": id, + "msg_type": "text", + "create_time": "1710000000", + "chat_id": "oc_" + suffix, + "sender": map[string]interface{}{ + "id": "cli_bot", + "name": "Bot", + "sender_type": "bot", + }, + "body": map[string]interface{}{ + "content": fmt.Sprintf(`{"text":"message %s"}`, suffix), + }, + }) + } + return items +} + +func buildChatContexts(chatIDs []string) []interface{} { + items := make([]interface{}, 0, len(chatIDs)) + for _, chatID := range chatIDs { + items = append(items, map[string]interface{}{ + "chat_id": chatID, + "chat_mode": "group", + "name": "Chat " + strings.TrimPrefix(chatID, "oc_"), + }) + } + return items +} diff --git a/skills/lark-im/SKILL.md b/skills/lark-im/SKILL.md index e33ee5155..c7f86dd24 100644 --- a/skills/lark-im/SKILL.md +++ b/skills/lark-im/SKILL.md @@ -63,7 +63,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli im + [flags]`)。 | [`+messages-mget`](references/lark-im-messages-mget.md) | Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies | | [`+messages-reply`](references/lark-im-messages-reply.md) | Reply to a message (supports thread replies) with bot identity; bot-only; supports text/markdown/post/media replies, reply-in-thread, idempotency key | | [`+messages-resources-download`](references/lark-im-messages-resources-download.md) | Download images/files from a message; user/bot; downloads image/file resources by message-id and file-key to a safe relative output path | -| [`+messages-search`](references/lark-im-messages-search.md) | Search messages across chats (supports keyword, sender, time range filters) with user identity; user-only; filters by chat/sender/attachment/time, enriches results via mget and chats batch_query | +| [`+messages-search`](references/lark-im-messages-search.md) | Search messages across chats (supports keyword, sender, time range filters) with user identity; user-only; filters by chat/sender/attachment/time, supports auto-pagination via `--page-all` / `--page-limit`, enriches results via batched mget and chats batch_query | | [`+messages-send`](references/lark-im-messages-send.md) | Send a message to a chat or direct message with bot identity; bot-only; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key | | [`+threads-messages-list`](references/lark-im-threads-messages-list.md) | List messages in a thread; user/bot; accepts om_/omt_ input, resolves message IDs to thread_id, supports sort/pagination | @@ -136,4 +136,3 @@ lark-cli im [flags] # 调用 API | `pins.create` | `im:message.pins:write_only` | | `pins.delete` | `im:message.pins:write_only` | | `pins.list` | `im:message.pins:read` | - diff --git a/skills/lark-im/references/lark-im-messages-search.md b/skills/lark-im/references/lark-im-messages-search.md index 4cf3f2953..688583959 100644 --- a/skills/lark-im/references/lark-im-messages-search.md +++ b/skills/lark-im/references/lark-im-messages-search.md @@ -6,7 +6,7 @@ Search Feishu messages across conversations. This shortcut automatically perform > **User identity only** (`--as user`). Bot identity is not supported. -This skill maps to the shortcut: `lark-cli im +messages-search` (internally calls `POST /open-apis/im/v1/messages/search` + `GET /open-apis/im/v1/messages/mget`, then batch-fetches chat context). +This skill maps to the shortcut: `lark-cli im +messages-search` (internally calls `POST /open-apis/im/v1/messages/search` + batched `GET /open-apis/im/v1/messages/mget`, then batch-fetches chat context). ## Commands @@ -49,6 +49,12 @@ lark-cli im +messages-search --query "test" --format csv # Pagination lark-cli im +messages-search --query "test" --page-token +# Auto-pagination across multiple pages +lark-cli im +messages-search --query "test" --page-all --format json + +# Auto-pagination with an explicit page cap +lark-cli im +messages-search --query "test" --page-limit 5 --format json + # Preview the request without executing it lark-cli im +messages-search --query "test" --dry-run ``` @@ -69,6 +75,8 @@ lark-cli im +messages-search --query "test" --dry-run | `--end