diff --git a/shortcuts/im/convert_lib/helpers.go b/shortcuts/im/convert_lib/helpers.go index 75368f45f..927d91b35 100644 --- a/shortcuts/im/convert_lib/helpers.go +++ b/shortcuts/im/convert_lib/helpers.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strconv" "strings" "time" @@ -72,12 +73,14 @@ func formatTimestamp(ts string) string { return time.Unix(n, 0).Local().Format("2006-01-02 15:04:05") } -// ResolveSenderNames batch-resolves sender open_ids to display names. +// ResolveSenderNames batch-resolves sender IDs to display names. // The cache map is used to share already-resolved IDs across calls; newly resolved // names are written back into it. Pass an empty map if no prior cache exists. // // Step 1: extract names from message mentions (free, no API call). -// Step 2: for remaining unresolved IDs, call contact batch API (requires contact:user.base:readonly). +// Step 2: batch-resolve bot open_ids via bot basic info API. +// Step 3: resolve the current bot/app sender from bot metadata as fallback. +// Step 4: for remaining unresolved user IDs, call contact batch API (requires contact:user.base:readonly). // Silently returns partial results on API error. // // [#22] Changed from variadic `cache ...map[string]string` to a required parameter. @@ -113,6 +116,10 @@ func ResolveSenderNames(runtime *common.RuntimeContext, messages []map[string]in } } + resolveAppSenderNamesFromContent(messages, nameMap) + resolveBotSenderNames(runtime, messages, nameMap) + resolveCurrentBotSenderName(runtime, messages, nameMap) + // Collect sender IDs still missing a name seen := make(map[string]bool) var missingIDs []string @@ -136,7 +143,7 @@ func ResolveSenderNames(runtime *common.RuntimeContext, messages []map[string]in return nameMap } - // Step 2: batch resolve remaining via contact API. + // Step 4: batch resolve remaining users via contact API. // Use basic_batch for user identity (lighter permission requirement), // full batch for bot identity. if runtime.As().IsBot() { @@ -148,6 +155,147 @@ func ResolveSenderNames(runtime *common.RuntimeContext, messages []map[string]in return nameMap } +var appWelcomeNameRe = regexp.MustCompile(`我是\s+\*\*([^*\n]+)\*\*`) + +func resolveAppSenderNamesFromContent(messages []map[string]interface{}, nameMap map[string]string) { + for _, msg := range messages { + sender, ok := msg["sender"].(map[string]interface{}) + if !ok { + continue + } + senderType, _ := sender["sender_type"].(string) + idType, _ := sender["id_type"].(string) + id, _ := sender["id"].(string) + if !isBotSenderType(senderType) || idType != "app_id" || id == "" || nameMap[id] != "" || senderHasName(sender) { + continue + } + content, _ := msg["content"].(string) + name := extractAppWelcomeName(content) + if name != "" { + nameMap[id] = name + } + } +} + +func extractAppWelcomeName(content string) string { + match := appWelcomeNameRe.FindStringSubmatch(content) + if len(match) < 2 { + return "" + } + return strings.TrimSpace(match[1]) +} + +func resolveBotSenderNames(runtime *common.RuntimeContext, messages []map[string]interface{}, nameMap map[string]string) { + if runtime == nil { + return + } + + seen := make(map[string]bool) + var missingIDs []string + for _, msg := range messages { + sender, ok := msg["sender"].(map[string]interface{}) + if !ok { + continue + } + senderType, _ := sender["sender_type"].(string) + if !isBotSenderType(senderType) { + continue + } + id, _ := sender["id"].(string) + if id == "" || !strings.HasPrefix(id, "ou_") || seen[id] || nameMap[id] != "" || senderHasName(sender) { + continue + } + seen[id] = true + missingIDs = append(missingIDs, id) + } + if len(missingIDs) == 0 { + return + } + + batchResolveBots(runtime, missingIDs, nameMap) +} + +func resolveCurrentBotSenderName(runtime *common.RuntimeContext, messages []map[string]interface{}, nameMap map[string]string) { + if runtime == nil { + return + } + + seen := make(map[string]bool) + var missingIDs []string + for _, msg := range messages { + sender, ok := msg["sender"].(map[string]interface{}) + if !ok { + continue + } + senderType, _ := sender["sender_type"].(string) + if !isBotSenderType(senderType) { + continue + } + id, _ := sender["id"].(string) + if id == "" || seen[id] || nameMap[id] != "" || senderHasName(sender) { + continue + } + seen[id] = true + missingIDs = append(missingIDs, id) + } + if len(missingIDs) == 0 { + return + } + + info, err := runtime.BotInfo() + if err != nil || info == nil || strings.TrimSpace(info.AppName) == "" { + return + } + + currentBotIDs := make(map[string]bool, 2) + if info.OpenID != "" { + currentBotIDs[info.OpenID] = true + } + if runtime.Config != nil && runtime.Config.AppID != "" { + currentBotIDs[runtime.Config.AppID] = true + } + for _, id := range missingIDs { + if currentBotIDs[id] { + nameMap[id] = info.AppName + } + } +} + +func isBotSenderType(senderType string) bool { + return senderType == "bot" || senderType == "app" +} + +func senderHasName(sender map[string]interface{}) bool { + name, _ := sender["name"].(string) + return strings.TrimSpace(name) != "" +} + +func batchResolveBots(runtime *common.RuntimeContext, missingIDs []string, nameMap map[string]string) { + const batchSize = 10 + for i := 0; i < len(missingIDs); i += batchSize { + end := i + batchSize + if end > len(missingIDs) { + end = len(missingIDs) + } + batch := missingIDs[i:end] + + query := larkcore.QueryParams{"bot_ids": batch} + data, err := runtime.DoAPIJSON(http.MethodGet, "/open-apis/bot/v3/bots/basic_batch", query, nil) + if err != nil { + break + } + + bots, _ := data["bots"].(map[string]interface{}) + for requestedID, raw := range bots { + bot, _ := raw.(map[string]interface{}) + name, _ := bot["name"].(string) + if requestedID != "" && name != "" { + nameMap[requestedID] = name + } + } + } +} + // batchResolveByBasicContact resolves user names via POST /contact/v3/users/basic_batch. // This API has lighter permission requirements and works with user identity // even when the target user is not in the app's visible range. diff --git a/shortcuts/im/convert_lib/helpers_test.go b/shortcuts/im/convert_lib/helpers_test.go index 3220ad6c6..3ecc578cf 100644 --- a/shortcuts/im/convert_lib/helpers_test.go +++ b/shortcuts/im/convert_lib/helpers_test.go @@ -12,6 +12,8 @@ import ( "strings" "testing" "time" + + "github.com/larksuite/cli/shortcuts/common" ) func TestParseJSONObject(t *testing.T) { @@ -172,6 +174,114 @@ func TestResolveSenderNames(t *testing.T) { } } +func TestResolveSenderNamesAddsCurrentBotAppName(t *testing.T) { + runtime := newBotConvertlibRuntime(t, convertlibRoundTripFunc(func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + })) + runtime.Config.AppID = "cli_current" + botInfoCalls := 0 + setConvertlibRuntimeField(t, runtime, "botInfoFunc", func() (*common.BotInfo, error) { + botInfoCalls++ + return &common.BotInfo{OpenID: "ou_bot_current", AppName: "Release Bot"}, nil + }) + + messages := []map[string]interface{}{ + {"sender": map[string]interface{}{"sender_type": "app", "id": "cli_current"}}, + {"sender": map[string]interface{}{"sender_type": "bot", "id": "ou_bot_current"}}, + {"sender": map[string]interface{}{"sender_type": "app", "id": "cli_other"}}, + } + + nameMap := ResolveSenderNames(runtime, messages, nil) + AttachSenderNames(messages, nameMap) + + if botInfoCalls != 1 { + t.Fatalf("BotInfo() calls = %d, want 1", botInfoCalls) + } + for i := 0; i < 2; i++ { + sender := messages[i]["sender"].(map[string]interface{}) + if sender["name"] != "Release Bot" { + t.Fatalf("sender %d name = %#v, want %#v", i, sender["name"], "Release Bot") + } + } + otherSender := messages[2]["sender"].(map[string]interface{}) + if _, hasName := otherSender["name"]; hasName { + t.Fatalf("other bot sender should remain unresolved, got %#v", otherSender["name"]) + } +} + +func TestResolveSenderNamesBatchResolvesBotOpenIDs(t *testing.T) { + var requestedBotIDs []string + runtime := newBotConvertlibRuntime(t, convertlibRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/open-apis/bot/v3/bots/basic_batch"): + requestedBotIDs = append(requestedBotIDs, req.URL.Query()["bot_ids"]...) + return convertlibJSONResponse(200, map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "bots": map[string]interface{}{ + "ou_bot_alpha": map[string]interface{}{"bot_id": "ou_bot_alpha", "name": "Alpha Bot"}, + "ou_bot_beta": map[string]interface{}{"bot_id": "ou_bot_beta", "name": "Beta Bot"}, + }, + "failed_bots": map[string]interface{}{ + "ou_bot_missing": map[string]interface{}{"code": 20002, "reason": "bot not found"}, + }, + }, + }), nil + default: + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + } + })) + + messages := []map[string]interface{}{ + {"sender": map[string]interface{}{"sender_type": "bot", "id": "ou_bot_alpha"}}, + {"sender": map[string]interface{}{"sender_type": "app", "id": "ou_bot_beta"}}, + {"sender": map[string]interface{}{"sender_type": "bot", "id": "ou_bot_missing"}}, + {"sender": map[string]interface{}{"sender_type": "bot", "id": "ou_bot_alpha"}}, + } + + nameMap := ResolveSenderNames(runtime, messages, nil) + AttachSenderNames(messages, nameMap) + + if want := []string{"ou_bot_alpha", "ou_bot_beta", "ou_bot_missing"}; !reflect.DeepEqual(requestedBotIDs, want) { + t.Fatalf("bot_ids = %#v, want %#v", requestedBotIDs, want) + } + if got := messages[0]["sender"].(map[string]interface{})["name"]; got != "Alpha Bot" { + t.Fatalf("alpha sender name = %#v, want Alpha Bot", got) + } + if got := messages[1]["sender"].(map[string]interface{})["name"]; got != "Beta Bot" { + t.Fatalf("beta sender name = %#v, want Beta Bot", got) + } + missingSender := messages[2]["sender"].(map[string]interface{}) + if _, hasName := missingSender["name"]; hasName { + t.Fatalf("failed bot sender should remain unresolved, got %#v", missingSender["name"]) + } +} + +func TestResolveSenderNamesExtractsAppSenderNameFromWelcomeCard(t *testing.T) { + runtime := newBotConvertlibRuntime(t, convertlibRoundTripFunc(func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("unexpected request: %s", req.URL.String()) + })) + + messages := []map[string]interface{}{ + { + "content": "\n同学们好!我是 **生活bot**,由 @ou_401dd1d6257568b71f210c84ec3d98d1 邀请入群,并由 AIME 提供能力支持。\n", + "sender": map[string]interface{}{ + "id": "cli_a97a8cbfdf38dcbc", + "id_type": "app_id", + "sender_type": "app", + }, + }, + } + + nameMap := ResolveSenderNames(runtime, messages, nil) + AttachSenderNames(messages, nameMap) + + sender := messages[0]["sender"].(map[string]interface{}) + if sender["name"] != "生活bot" { + t.Fatalf("sender name = %#v, want 生活bot", sender["name"]) + } +} + func TestBatchResolveByBasicContactRespectsAPILimit(t *testing.T) { // basic_batch allows at most 10 user_ids per request. Given 25 missing IDs, // expect three requests with sizes 10 / 10 / 5. diff --git a/shortcuts/im/helpers.go b/shortcuts/im/helpers.go index 5b42c32b7..94bc30717 100644 --- a/shortcuts/im/helpers.go +++ b/shortcuts/im/helpers.go @@ -34,6 +34,8 @@ var mentionFixRe = regexp.MustCompile(`]+ var threadIDRe = regexp.MustCompile(`^omt_`) var messageIDRe = regexp.MustCompile(`^om_`) +const botBasicInfoReadScope = "application:bot.basic_info:read" + func flagMessageID(rt *common.RuntimeContext) (string, error) { id := strings.TrimSpace(rt.Str("message-id")) if id == "" { @@ -1480,9 +1482,12 @@ const ( var ( flagWriteLookupScopes = append([]string{flagWriteScope}, flagLookupScopes...) + // Feed-thread flag enrichment can expose nested messages sent by bots, so keep + // the dynamic scope check aligned with sender-name resolution's bot lookup. flagMessageReadScopes = []string{ "im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", + botBasicInfoReadScope, } flagLookupScopes = []string{ "im:message.group_msg:get_as_user", diff --git a/shortcuts/im/helpers_test.go b/shortcuts/im/helpers_test.go index 853d5e9f0..c28a721e3 100644 --- a/shortcuts/im/helpers_test.go +++ b/shortcuts/im/helpers_test.go @@ -11,6 +11,7 @@ import ( "fmt" "net/http" "reflect" + "slices" "strings" "testing" @@ -876,3 +877,26 @@ func TestShortcuts(t *testing.T) { t.Fatalf("Shortcuts() commands = %#v, want %#v", commands, want) } } + +func TestMessageReadShortcutsDeclareBotBasicInfoScope(t *testing.T) { + for _, tt := range []struct { + name string + shortcut common.Shortcut + identity string + }{ + {name: "chat messages user", shortcut: ImChatMessageList, identity: "user"}, + {name: "chat messages bot", shortcut: ImChatMessageList, identity: "bot"}, + {name: "messages mget user", shortcut: ImMessagesMGet, identity: "user"}, + {name: "messages mget bot", shortcut: ImMessagesMGet, identity: "bot"}, + {name: "thread messages user", shortcut: ImThreadsMessagesList, identity: "user"}, + {name: "thread messages bot", shortcut: ImThreadsMessagesList, identity: "bot"}, + {name: "messages search user", shortcut: ImMessagesSearch, identity: "user"}, + } { + t.Run(tt.name, func(t *testing.T) { + const wantScope = "application:bot.basic_info:read" + if !slices.Contains(tt.shortcut.ScopesForIdentity(tt.identity), wantScope) { + t.Fatalf("%s scopes = %#v, want %s", tt.name, tt.shortcut.ScopesForIdentity(tt.identity), wantScope) + } + }) + } +} diff --git a/shortcuts/im/im_chat_messages_list.go b/shortcuts/im/im_chat_messages_list.go index bb247a9b5..d1d3a0185 100644 --- a/shortcuts/im/im_chat_messages_list.go +++ b/shortcuts/im/im_chat_messages_list.go @@ -22,8 +22,8 @@ var ImChatMessageList = common.Shortcut{ Description: "List messages in a chat or P2P conversation; user/bot; accepts --chat-id or --user-id, resolves P2P chat_id, supports time range/sort/pagination", Risk: "read", Scopes: []string{"im:message:readonly"}, - UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.base:readonly"}, - BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly"}, + UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.base:readonly", botBasicInfoReadScope}, + BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", botBasicInfoReadScope}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ diff --git a/shortcuts/im/im_flag_list.go b/shortcuts/im/im_flag_list.go index 6599bd024..ab912930e 100644 --- a/shortcuts/im/im_flag_list.go +++ b/shortcuts/im/im_flag_list.go @@ -8,9 +8,11 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/shortcuts/common" + convertlib "github.com/larksuite/cli/shortcuts/im/convert_lib" larkcore "github.com/larksuite/oapi-sdk-go/v3/core" ) @@ -29,7 +31,7 @@ var ImFlagList = common.Shortcut{ {Name: "page-token", Desc: "pagination token for next page"}, {Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages"}, {Name: "page-limit", Type: "int", Default: "20", Desc: "max pages when auto-pagination is enabled (default 20, max 1000)"}, - {Name: "enrich-feed-thread", Type: "bool", Default: "true", Desc: "fetch message content for feed-type thread entries (default true; may call messages/mget and require im:message.group_msg:get_as_user/im:message.p2p_msg:get_as_user; use --enrich-feed-thread=false to avoid extra scopes)"}, + {Name: "enrich-feed-thread", Type: "bool", Default: "true", Desc: "fetch message content for feed-type thread entries (default true; may call messages/mget and bot basic info; use --enrich-feed-thread=false to avoid extra scopes)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateListOptions(runtime) @@ -45,7 +47,12 @@ var ImFlagList = common.Shortcut{ "page_token": runtime.Str("page-token"), }) if runtime.Bool("enrich-feed-thread") { - d.Desc("conditional enrichment: if feed/thread flag items are missing message content, execution may also call GET /open-apis/im/v1/messages/mget and requires scopes im:message.group_msg:get_as_user im:message.p2p_msg:get_as_user; pass --enrich-feed-thread=false to skip this extra call and extra scopes") + // Dry-run must name every conditional API because agents use this output + // to decide which scopes to request before executing the real command. + d.Desc(fmt.Sprintf( + "conditional enrichment: if feed/thread flag items are missing message content, execution may also call GET /open-apis/im/v1/messages/mget and GET /open-apis/bot/v3/bots/basic_batch; requires scopes %s; pass --enrich-feed-thread=false to skip these extra calls and extra scopes", + strings.Join(flagMessageReadScopes, " "), + )) } return d }, @@ -180,6 +187,9 @@ func enrichFeedThreadItems(rt *common.RuntimeContext, data map[string]any) error if len(byID) == 0 { return nil } + // Flag-list nests message payloads under each flag item, so enrich them before + // attachment to match the sender contract of other message read shortcuts. + enrichFlagMessageSenderNames(rt, byID) // Attach message payload to the matching list entries. for _, it := range items { m, _ := it.(map[string]any) @@ -201,6 +211,18 @@ func enrichFeedThreadItems(rt *common.RuntimeContext, data map[string]any) error return nil } +// enrichFlagMessageSenderNames applies message-read sender enrichment to the +// raw messages collected from inline flag data and mget fallback responses. +func enrichFlagMessageSenderNames(rt *common.RuntimeContext, byID map[string]map[string]any) { + messages := make([]map[string]interface{}, 0, len(byID)) + for _, msg := range byID { + messages = append(messages, msg) + } + nameCache := make(map[string]string) + convertlib.ResolveSenderNames(rt, messages, nameCache) + convertlib.AttachSenderNames(messages, nameCache) +} + // asString converts an arbitrary value to its string representation. // Handles string, float64, int, int64, and json.Number types; returns empty string for other types. func asString(v any) string { diff --git a/shortcuts/im/im_flag_test.go b/shortcuts/im/im_flag_test.go index dbf3ad832..0374398d2 100644 --- a/shortcuts/im/im_flag_test.go +++ b/shortcuts/im/im_flag_test.go @@ -982,6 +982,7 @@ func TestFlagListDryRunMentionsConditionalEnrichmentScopes(t *testing.T) { for _, want := range []string{ "im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", + botBasicInfoReadScope, "--enrich-feed-thread=false", } { if !strings.Contains(got, want) { @@ -1151,6 +1152,65 @@ func TestEnrichFeedThreadItems(t *testing.T) { } } +func TestEnrichFeedThreadItems_ResolvesFetchedBotSenderName(t *testing.T) { + rt := newBotShortcutRuntime(t, shortcutRoundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/open-apis/im/v1/messages/mget"): + // Regression coverage: flag-list's mget fallback returns raw nested + // messages, so bot sender names must be resolved before attachment. + return shortcutJSONResponse(200, map[string]any{ + "code": 0, + "data": map[string]any{ + "items": []any{ + map[string]any{ + "message_id": "omt_456", + "sender": map[string]any{ + "id": "ou_bot_456", + "sender_type": "bot", + }, + }, + }, + }, + }), nil + case strings.Contains(req.URL.Path, "/open-apis/bot/v3/bots/basic_batch"): + // Keep the mock shape aligned with the real bot basic_batch map form + // so the test covers the exact lookup path used in production. + return shortcutJSONResponse(200, map[string]any{ + "code": 0, + "data": map[string]any{ + "bots": map[string]any{ + "ou_bot_456": map[string]any{"name": "Release Bot"}, + }, + }, + }), nil + default: + return nil, fmt.Errorf("unexpected request: %s", req.URL.Path) + } + })) + setRuntimeScopes(t, rt, strings.Join(flagMessageReadScopes, " ")) + + data := map[string]any{ + "flag_items": []any{ + map[string]any{ + "item_id": "omt_456", + "item_type": "4", + "flag_type": "1", + }, + }, + } + + if err := enrichFeedThreadItems(rt, data); err != nil { + t.Fatalf("enrichFeedThreadItems() error = %v", err) + } + + item := data["flag_items"].([]any)[0].(map[string]any) + message := item["message"].(map[string]any) + sender := message["sender"].(map[string]any) + if got := sender["name"]; got != "Release Bot" { + t.Fatalf("sender name = %#v, want Release Bot", got) + } +} + func TestEnrichFeedThreadItems_BatchMGet(t *testing.T) { // Test that batched mget works when > 50 items var feedItems []any diff --git a/shortcuts/im/im_messages_mget.go b/shortcuts/im/im_messages_mget.go index a8d9ade72..ea7de7461 100644 --- a/shortcuts/im/im_messages_mget.go +++ b/shortcuts/im/im_messages_mget.go @@ -22,8 +22,8 @@ var ImMessagesMGet = common.Shortcut{ Description: "Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies", Risk: "read", Scopes: []string{"im:message:readonly"}, - UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly"}, - BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly"}, + UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly", botBasicInfoReadScope}, + BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly", botBasicInfoReadScope}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ diff --git a/shortcuts/im/im_messages_search.go b/shortcuts/im/im_messages_search.go index 48dbca06d..6269fd59a 100644 --- a/shortcuts/im/im_messages_search.go +++ b/shortcuts/im/im_messages_search.go @@ -30,7 +30,7 @@ var ImMessagesSearch = common.Shortcut{ Command: "+messages-search", Description: "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", Risk: "read", - Scopes: []string{"search:message", "contact:user.basic_profile:readonly"}, + Scopes: []string{"search:message", "contact:user.basic_profile:readonly", botBasicInfoReadScope}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ diff --git a/shortcuts/im/im_threads_messages_list.go b/shortcuts/im/im_threads_messages_list.go index 538117a94..3c7cee3e7 100644 --- a/shortcuts/im/im_threads_messages_list.go +++ b/shortcuts/im/im_threads_messages_list.go @@ -24,8 +24,8 @@ var ImThreadsMessagesList = common.Shortcut{ Description: "List messages in a thread; user/bot; accepts om_/omt_ input, resolves message IDs to thread_id, supports sort/pagination", Risk: "read", Scopes: []string{"im:message:readonly"}, - UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly"}, - BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly"}, + UserScopes: []string{"im:message.group_msg:get_as_user", "im:message.p2p_msg:get_as_user", "contact:user.basic_profile:readonly", botBasicInfoReadScope}, + BotScopes: []string{"im:message.group_msg", "im:message.p2p_msg:readonly", "contact:user.base:readonly", botBasicInfoReadScope}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ diff --git a/skills/lark-im/references/lark-im-flag-list.md b/skills/lark-im/references/lark-im-flag-list.md index 913a89056..52b14cdeb 100644 --- a/skills/lark-im/references/lark-im-flag-list.md +++ b/skills/lark-im/references/lark-im-flag-list.md @@ -43,7 +43,7 @@ lark-cli im +flag-list --as user --page-all --page-limit 10 | `--page-token ` | empty | Pagination token from previous page; empty string must still be provided | | `--page-all` | false | Auto-paginate to fetch all pages and merge results | | `--page-limit ` | 20 | Max pages in `--page-all` mode (max 1000) | -| `--enrich-feed-thread` | true | Auto-enrich feed-layer thread entries with message content (calls `im.messages.mget`) | +| `--enrich-feed-thread` | true | Auto-enrich feed-layer thread entries with message content and bot sender names (calls `im.messages.mget` and bot basic info when needed) | | `--as user` | Required | Currently only supports user identity | ## Response Structure @@ -58,7 +58,7 @@ The response has `data` as the main body, with fields described below: | `has_more` | boolean | Whether there's a next page | | `page_token` | string | Pagination token for the next page | -Note: `(thread, feed)` / `(msg_thread, feed)` entries are automatically enriched via `mget` by the shortcut, and written to the corresponding entry's `message` field. +Note: `(thread, feed)` / `(msg_thread, feed)` entries are automatically enriched via `mget` by the shortcut, with bot sender names resolved when possible, and written to the corresponding entry's `message` field. ## Limitations