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