From 7a3761f944dbe8f15c4f600a36cd896bae29e3b5 Mon Sep 17 00:00:00 2001 From: bubbmon233 <272202079+bubbmon233@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:24:50 +0800 Subject: [PATCH 1/2] docs: clarify mail message shortcut guidance Clarify mail +message as single-email only and mail +messages as the multi-email path that chunks batch_get requests at 20 IDs and merges output while documenting the backend raw 50-ID validation limit. Update the mail +triage table tip and lark-mail docs to route one selected message to +message and multiple selected messages to +messages. Test: go test -count=1 ./shortcuts/mail Change-Type: ci-fix --- shortcuts/mail/mail_message.go | 10 +- shortcuts/mail/mail_message_help_test.go | 127 ++++++++++++++++++ shortcuts/mail/mail_messages.go | 6 +- shortcuts/mail/mail_messages_test.go | 16 ++- shortcuts/mail/mail_triage.go | 4 +- shortcuts/mail/mail_triage_test.go | 10 +- skill-template/domains/mail.md | 9 +- skills/lark-mail/SKILL.md | 14 +- .../lark-mail/references/lark-mail-message.md | 9 +- .../references/lark-mail-messages.md | 10 +- .../lark-mail/references/lark-mail-triage.md | 6 +- 11 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 shortcuts/mail/mail_message_help_test.go diff --git a/shortcuts/mail/mail_message.go b/shortcuts/mail/mail_message.go index 72dd7d52d..1a7610632 100644 --- a/shortcuts/mail/mail_message.go +++ b/shortcuts/mail/mail_message.go @@ -9,19 +9,19 @@ import ( "github.com/larksuite/cli/shortcuts/common" ) -// MailMessage is the `+message` shortcut: fetch full content of a single -// email by message ID (normalized body + attachments / inline metadata). +// MailMessage is the `+message` shortcut: fetch full content for one email by +// message ID (normalized body + attachments / inline metadata). var MailMessage = common.Shortcut{ Service: "mail", Command: "+message", - Description: "Use when reading full content for a single email by message ID. Returns normalized body content plus attachments metadata, including inline images.", + Description: "Use only when reading full content for a single email by message ID. For multiple message IDs, use mail +messages; do not loop mail +message. Returns normalized body content plus attachments metadata.", Risk: "read", Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "mailbox", Default: "me", Desc: "email address (default: me)"}, - {Name: "message-id", Desc: "Required. Email message ID", Required: true}, + {Name: "message-id", Desc: "Required. Single email message ID. For multiple IDs, use --message-ids with mail +messages; do not loop mail +message.", Required: true}, {Name: "html", Type: "bool", Default: "true", Desc: "Whether to return HTML body (false returns plain text only to save bandwidth)"}, {Name: "print-output-schema", Type: "bool", Desc: "Print output field reference (run this first to learn field names before parsing output)"}, }, @@ -32,7 +32,7 @@ var MailMessage = common.Shortcut{ mailboxID := resolveMailboxID(runtime) messageID := runtime.Str("message-id") return common.NewDryRunAPI(). - Desc("Fetch full email content and attachments metadata, including inline images"). + Desc("Fetch full content for one email only, including attachments metadata; for multiple IDs use mail +messages."). GET(mailboxPath(mailboxID, "messages", messageID)) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/mail/mail_message_help_test.go b/shortcuts/mail/mail_message_help_test.go new file mode 100644 index 000000000..4dbac8016 --- /dev/null +++ b/shortcuts/mail/mail_message_help_test.go @@ -0,0 +1,127 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package mail + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/shortcuts/common" +) + +func TestMailMessageHelpMentionsSingleMessageOnly(t *testing.T) { + help := strings.ToLower(mountedShortcutHelp(t, MailMessage)) + for _, want := range []string{ + "single email", + "mail +messages", + "do not loop mail +message", + "single email message id", + } { + if !strings.Contains(help, want) { + t.Errorf("mail +message help should mention %q; got:\n%s", want, help) + } + } +} + +func TestMailMessagesHelpMentionsBatchLimitAndAutoChunk(t *testing.T) { + help := strings.ToLower(mountedShortcutHelp(t, MailMessages)) + for _, want := range []string{ + "multiple emails", + "at most 20 ids per batch_get request", + "merges output", + "current backend raw request validation rejects more than 50 ids", + } { + if !strings.Contains(help, want) { + t.Errorf("mail +messages help should mention %q; got:\n%s", want, help) + } + } +} + +func TestMailMessageDryRunMentionsSingleMessageOnly(t *testing.T) { + runtime := runtimeForShortcutDryRun(t, MailMessage, map[string]string{ + "message-id": "msg_1", + }) + got := strings.ToLower(marshalDryRun(t, MailMessage.DryRun(context.Background(), runtime))) + for _, want := range []string{ + "one email only", + "for multiple ids use mail +messages", + } { + if !strings.Contains(got, want) { + t.Errorf("mail +message dry-run should mention %q; got:\n%s", want, got) + } + } +} + +func TestMailMessagesDryRunMentionsChunkAndBackendLimit(t *testing.T) { + runtime := runtimeForShortcutDryRun(t, MailMessages, map[string]string{ + "message-ids": "msg_1,msg_2", + }) + got := strings.ToLower(marshalDryRun(t, MailMessages.DryRun(context.Background(), runtime))) + for _, want := range []string{ + "auto-chunks at most 20 ids per request", + "merges output", + "backend raw request validation rejects more than 50 ids", + } { + if !strings.Contains(got, want) { + t.Errorf("mail +messages dry-run should mention %q; got:\n%s", want, got) + } + } +} + +func mountedShortcutHelp(t *testing.T, shortcut common.Shortcut) string { + t.Helper() + f, _, _, _ := mailShortcutTestFactory(t) + parent := &cobra.Command{Use: "mail"} + shortcut.Mount(parent, f) + cmd, _, err := parent.Find([]string{shortcut.Command}) + if err != nil { + t.Fatalf("find mounted shortcut %s: %v", shortcut.Command, err) + } + if cmd == parent { + t.Fatalf("shortcut %s was not mounted", shortcut.Command) + } + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + if err := cmd.Help(); err != nil { + t.Fatalf("help for %s: %v", shortcut.Command, err) + } + return cmd.Short + "\n" + out.String() +} + +func runtimeForShortcutDryRun(t *testing.T, shortcut common.Shortcut, values map[string]string) *common.RuntimeContext { + t.Helper() + cmd := &cobra.Command{Use: shortcut.Command} + for _, fl := range shortcut.Flags { + switch fl.Type { + case "bool": + cmd.Flags().Bool(fl.Name, fl.Default == "true", "") + default: + cmd.Flags().String(fl.Name, fl.Default, "") + } + } + if err := cmd.ParseFlags(nil); err != nil { + t.Fatalf("parse flags failed: %v", err) + } + for k, v := range values { + if err := cmd.Flags().Set(k, v); err != nil { + t.Fatalf("set flag --%s failed: %v", k, err) + } + } + return &common.RuntimeContext{Cmd: cmd} +} + +func marshalDryRun(t *testing.T, dry *common.DryRunAPI) string { + t.Helper() + raw, err := json.Marshal(dry) + if err != nil { + t.Fatalf("marshal dry-run: %v", err) + } + return string(raw) +} diff --git a/shortcuts/mail/mail_messages.go b/shortcuts/mail/mail_messages.go index 35d289abf..04338c696 100644 --- a/shortcuts/mail/mail_messages.go +++ b/shortcuts/mail/mail_messages.go @@ -23,14 +23,14 @@ type mailMessagesOutput struct { var MailMessages = common.Shortcut{ Service: "mail", Command: "+messages", - Description: "Use when reading full content for multiple emails by message ID. Prefer this shortcut over calling raw mail user_mailbox.messages batch_get directly, because it base64url-decodes body fields and returns normalized per-message output that is easier to consume.", + Description: "Use when reading full content for multiple emails by message ID. This shortcut sends at most 20 IDs per batch_get request and merges output; current backend raw request validation rejects more than 50 IDs.", Risk: "read", Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "mailbox", Default: "me", Desc: "email address (default: me)"}, - {Name: "message-ids", Desc: `Required. Comma-separated email message IDs. Example: "id1,id2,id3"`, Required: true}, + {Name: "message-ids", Desc: `Required. Comma-separated email message IDs. This shortcut sends at most 20 IDs per batch_get request and merges output; current backend raw request validation rejects more than 50 IDs. Example: "id1,id2,id3"`, Required: true}, {Name: "html", Type: "bool", Default: "true", Desc: "Whether to return HTML body (false returns plain text only to save bandwidth)"}, {Name: "print-output-schema", Type: "bool", Desc: "Print output field reference (run this first to learn field names before parsing output)"}, }, @@ -52,7 +52,7 @@ var MailMessages = common.Shortcut{ body["message_ids"] = messageIDs } return common.NewDryRunAPI(). - Desc("Fetch multiple emails via messages.batch_get (auto-chunked in batches of 20 IDs during execution)"). + Desc("Fetch multiple emails via messages.batch_get; execution auto-chunks at most 20 IDs per request and merges output. Current backend raw request validation rejects more than 50 IDs per raw request."). POST(mailboxPath(mailboxID, "messages", "batch_get")). Body(body) }, diff --git a/shortcuts/mail/mail_messages_test.go b/shortcuts/mail/mail_messages_test.go index 725880adc..193ff757b 100644 --- a/shortcuts/mail/mail_messages_test.go +++ b/shortcuts/mail/mail_messages_test.go @@ -14,9 +14,11 @@ import ( "github.com/larksuite/cli/internal/httpmock" ) -func TestMailMessagesExecuteChunksMoreThanTwentyIDs(t *testing.T) { +func TestMailMessagesExecuteChunksByTwentyAndMergesOutput(t *testing.T) { f, stdout, _, reg := mailShortcutTestFactory(t) - ids := make([]string, 21) + defer reg.Verify(t) + + ids := make([]string, 41) for i := range ids { ids[i] = base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("biz-%03d", i))) } @@ -30,8 +32,14 @@ func TestMailMessagesExecuteChunksMoreThanTwentyIDs(t *testing.T) { reg.Register(&httpmock.Stub{ Method: "POST", URL: "/user_mailboxes/me/messages/batch_get", - BodyFilter: requestMessageIDsEqual(ids[20:]), - Body: batchGetMessagesResponse(ids[20:]), + BodyFilter: requestMessageIDsEqual(ids[20:40]), + Body: batchGetMessagesResponse(ids[20:40]), + }) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/user_mailboxes/me/messages/batch_get", + BodyFilter: requestMessageIDsEqual(ids[40:]), + Body: batchGetMessagesResponse(ids[40:]), }) err := runMountedMailShortcut(t, MailMessages, []string{ diff --git a/shortcuts/mail/mail_triage.go b/shortcuts/mail/mail_triage.go index e6c1c2248..a21098db1 100644 --- a/shortcuts/mail/mail_triage.go +++ b/shortcuts/mail/mail_triage.go @@ -322,9 +322,9 @@ var MailTriage = common.Shortcut{ fmt.Fprintln(runtime.IO().ErrOut, hint.String()) } if mailbox != "me" { - fmt.Fprintln(runtime.IO().ErrOut, "tip: use mail +message --mailbox "+shellQuote(mailbox)+" --message-id to read full content") + fmt.Fprintln(runtime.IO().ErrOut, "tip: read full content: single message -> mail +message --mailbox "+shellQuote(mailbox)+" --message-id ; multiple messages -> mail +messages --mailbox "+shellQuote(mailbox)+" --message-ids ") } else { - fmt.Fprintln(runtime.IO().ErrOut, "tip: use mail +message --message-id to read full content") + fmt.Fprintln(runtime.IO().ErrOut, "tip: read full content: single message -> mail +message --message-id ; multiple messages -> mail +messages --message-ids ") } } return nil diff --git a/shortcuts/mail/mail_triage_test.go b/shortcuts/mail/mail_triage_test.go index c29794977..b6b642da4 100644 --- a/shortcuts/mail/mail_triage_test.go +++ b/shortcuts/mail/mail_triage_test.go @@ -1647,8 +1647,14 @@ func TestMailTriageTableOutputPreservesMailboxContext(t *testing.T) { if got := strings.Contains(errOut, "--mailbox "+quotedMailbox); got != tt.wantMailboxHint { t.Fatalf("mailbox hint presence mismatch: got %v, want %v\nstderr:\n%s", got, tt.wantMailboxHint, errOut) } - if !strings.Contains(errOut, "mail +message") { - t.Fatalf("stderr should contain mail +message tip, got:\n%s", errOut) + for _, want := range []string{"single message -> mail +message", "multiple messages -> mail +messages"} { + if !strings.Contains(errOut, want) { + t.Fatalf("stderr should contain %q tip, got:\n%s", want, errOut) + } + } + oldTip := "tip: use mail +message --message-id " + " to read full content" + if strings.Contains(errOut, oldTip) { + t.Fatalf("stderr should not contain old mail +message-only tip, got:\n%s", errOut) } }) } diff --git a/skill-template/domains/mail.md b/skill-template/domains/mail.md index a884f18a1..17f0f03e2 100644 --- a/skill-template/domains/mail.md +++ b/skill-template/domains/mail.md @@ -39,7 +39,7 @@ - ❌ 创建一个新对象代替查询不到的目标(找不到"工作"文件夹时,不得自行创建后再移动) - ❌ 用占位符(`example.com`、`alice@example.com`、`` 字面量)凑数 -所有"删除 X / 归档 X / 打标签 X / 取消定时发送 X"等操作,X 必须来自 `+triage` / `+message` / `drafts list` 等真实查询的返回结果。 +所有"删除 X / 归档 X / 打标签 X / 取消定时发送 X"等操作,X 必须来自 `+triage` / `+message` / `+messages` / `drafts list` 等真实查询的返回结果。 ### 2. 写操作前显式确认 @@ -81,7 +81,7 @@ 1. **确认身份** — 首次操作邮箱前先调用 `lark-cli mail user_mailboxes profile --params '{"user_mailbox_id":"me"}'` 获取当前用户的真实邮箱地址(`primary_email_address`),不要通过系统用户名猜测。后续判断"发件人是否为用户本人"时以此地址为准。 2. **浏览** — `+triage` 查看收件箱摘要,获取 `message_id` / `thread_id` -3. **阅读** — `+message` 读单封邮件,`+thread` 读整个会话 +3. **阅读** — 一个 `message_id` 用 `+message` 读单封邮件;多个 `message_id` 用 `+messages` 一次读取;`+thread` 读整个会话 4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送) 5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送) 6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送 @@ -377,6 +377,8 @@ lark-cli mail +reply --message-id --body '收到,谢谢' `+message`、`+messages`、`+thread` 默认返回 HTML 正文(`--html=true`)。仅需确认操作结果(如验证标记已读、移动文件夹是否成功)时,用 `--html=false` 跳过 HTML 正文,只返回纯文本,显著减少 token 消耗。 +从 `+triage` 结果读取详情时:只选中一封邮件用 `+message --message-id `;选中多封邮件用 `+messages --message-ids `,不要循环调用 `+message`。`+messages` 执行时每个后端 `batch_get` 请求最多发送 20 个 ID,超过 20 自动拆批并合并输出;当前后端 raw 请求校验上限是 50 个 ID。 + 输出默认为结构化 JSON,可直接读取,无需额外编码转换。 ```bash @@ -385,6 +387,9 @@ lark-cli mail +message --message-id --html=false # ✅ 需要阅读完整内容:保持默认 lark-cli mail +message --message-id + +# ✅ 多封邮件:一次调用 +messages,不要循环 +message +lark-cli mail +messages --message-ids ,, --html=false ``` ### 邮件模板(`+template-create` / `+template-update` / `--template-id`) diff --git a/skills/lark-mail/SKILL.md b/skills/lark-mail/SKILL.md index e045c6e27..4628c24ef 100644 --- a/skills/lark-mail/SKILL.md +++ b/skills/lark-mail/SKILL.md @@ -55,7 +55,7 @@ metadata: - ❌ 创建一个新对象代替查询不到的目标(找不到"工作"文件夹时,不得自行创建后再移动) - ❌ 用占位符(`example.com`、`alice@example.com`、`` 字面量)凑数 -所有"删除 X / 归档 X / 打标签 X / 取消定时发送 X"等操作,X 必须来自 `+triage` / `+message` / `drafts list` 等真实查询的返回结果。 +所有"删除 X / 归档 X / 打标签 X / 取消定时发送 X"等操作,X 必须来自 `+triage` / `+message` / `+messages` / `drafts list` 等真实查询的返回结果。 ### 2. 写操作前显式确认 @@ -97,7 +97,7 @@ metadata: 1. **确认身份** — 首次操作邮箱前先调用 `lark-cli mail user_mailboxes profile --params '{"user_mailbox_id":"me"}'` 获取当前用户的真实邮箱地址(`primary_email_address`),不要通过系统用户名猜测。后续判断"发件人是否为用户本人"时以此地址为准。 2. **浏览** — `+triage` 查看收件箱摘要,获取 `message_id` / `thread_id` -3. **阅读** — `+message` 读单封邮件,`+thread` 读整个会话 +3. **阅读** — 一个 `message_id` 用 `+message` 读单封邮件;多个 `message_id` 用 `+messages` 一次读取;`+thread` 读整个会话 4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送) 5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送) 6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送 @@ -349,6 +349,8 @@ lark-cli mail +reply --message-id --body '收到,谢谢' `+message`、`+messages`、`+thread` 默认返回 HTML 正文(`--html=true`)。仅需确认操作结果(如验证标记已读、移动文件夹是否成功)时,用 `--html=false` 跳过 HTML 正文,只返回纯文本,显著减少 token 消耗。 +从 `+triage` 结果读取详情时:只选中一封邮件用 `+message --message-id `;选中多封邮件用 `+messages --message-ids `,不要循环调用 `+message`。`+messages` 执行时每个后端 `batch_get` 请求最多发送 20 个 ID,超过 20 自动拆批并合并输出;当前后端 raw 请求校验上限是 50 个 ID。 + 输出默认为结构化 JSON,可直接读取,无需额外编码转换。 ```bash @@ -357,6 +359,9 @@ lark-cli mail +message --message-id --html=false # ✅ 需要阅读完整内容:保持默认 lark-cli mail +message --message-id + +# ✅ 多封邮件:一次调用 +messages,不要循环 +message +lark-cli mail +messages --message-ids ,, --html=false ``` ### 邮件模板(`+template-create` / `+template-update` / `--template-id`) @@ -466,8 +471,8 @@ Shortcut 是对常用操作的高级封装(`lark-cli mail + [flags]`) | Shortcut | 说明 | |----------|------| -| [`+message`](references/lark-mail-message.md) | Use when reading full content for a single email by message ID. Returns normalized body content plus attachments metadata, including inline images. | -| [`+messages`](references/lark-mail-messages.md) | Use when reading full content for multiple emails by message ID. Prefer this shortcut over calling raw mail user_mailbox.messages batch_get directly, because it base64url-decodes body fields and returns normalized per-message output that is easier to consume. | +| [`+message`](references/lark-mail-message.md) | Use only when reading full content for a single email by message ID. For multiple message IDs, use mail +messages; do not loop mail +message. Returns normalized body content plus attachments metadata. | +| [`+messages`](references/lark-mail-messages.md) | Use when reading full content for multiple emails by message ID. This shortcut sends at most 20 IDs per batch_get request and merges output; current backend raw request validation rejects more than 50 IDs. | | [`+thread`](references/lark-mail-thread.md) | Use when querying a full mail conversation/thread by thread ID. Returns all messages in chronological order, including replies and drafts, with body content and attachments metadata, including inline images. | | [`+triage`](references/lark-mail-triage.md) | List mail summaries (date/from/subject/message_id). Use --query for full-text search, --filter for exact-match conditions. | | [`+watch`](references/lark-mail-watch.md) | Watch for incoming mail events via WebSocket (requires scope mail:event and bot event mail.user_mailbox.event.message_received_v1 added). Run with --print-output-schema to see per-format field reference before parsing output. | @@ -657,4 +662,3 @@ lark-cli mail [flags] # 调用 API | `user_mailbox.threads.list` | `mail:user_mailbox.message:readonly` | | `user_mailbox.threads.modify` | `mail:user_mailbox.message:modify` | | `user_mailbox.threads.trash` | `mail:user_mailbox.message:modify` | - diff --git a/skills/lark-mail/references/lark-mail-message.md b/skills/lark-mail/references/lark-mail-message.md index c7a71474d..564dc0ef4 100644 --- a/skills/lark-mail/references/lark-mail-message.md +++ b/skills/lark-mail/references/lark-mail-message.md @@ -2,7 +2,9 @@ > **前置条件:** 先阅读 [`../../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -读取指定邮件的完整内容,包括邮件头、正文(纯文本 + 可选 HTML)以及统一的 `attachments` 列表(涵盖普通附件和内嵌图片)。 +读取单封指定邮件的完整内容,包括邮件头、正文(纯文本 + 可选 HTML)以及统一的 `attachments` 列表(涵盖普通附件和内嵌图片)。 + +`mail +message` 只适合单个 `message_id`。如果已经有多个 `message_id`,使用 [`mail +messages`](./lark-mail-messages.md) 一次性读取,不要用 shell loop 或多次调用 `mail +message` 逐封读取。 CLI 分两阶段构建最终 JSON: - 安全的邮件元数据字段直接透传 @@ -14,7 +16,7 @@ CLI 分两阶段构建最终 JSON: ## 命令 ```bash -# 读取一封邮件(默认包含 HTML 正文) +# 读取单封邮件(默认包含 HTML 正文) lark-cli mail +message --message-id # 仅纯文本正文(更小的负载,适合 AI 处理) @@ -34,7 +36,7 @@ lark-cli mail +message --message-id --dry-run | 参数 | 必填 | 默认值 | 说明 | |------|------|--------|------| -| `--message-id ` | 是 | — | 邮件 ID | +| `--message-id ` | 是 | — | 单封邮件 ID。多个 ID 请改用 `mail +messages --message-ids `,不要循环调用 `mail +message` | | `--mailbox ` | 否 | 当前用户 | 邮箱地址(`user_mailbox_id`) | | `--html` | 否 | true | 是否返回 HTML 正文(`false` 仅返回纯文本,减少带宽) | | `--format ` | 否 | json | 输出格式:`json`(默认)/ `pretty` / `table` / `ndjson` / `csv` | @@ -157,6 +159,7 @@ lark-cli mail +message --message-id --dry-run - **JSON 输出可直接使用** — 默认输出合法 UTF-8 JSON,可直接读取,无需额外编码转换。 - JSON 输出中 `body_html` 里的 `<` / `>` 可能显示为 `\u003c` / `\u003e`(JSON 安全转义,内容不变,`jq -r` 可还原)。 - `mail +message` 默认不再获取附件/图片下载 URL。这样可以保持邮件详情读取更轻量,调用方可按需单独请求 URL。 +- 多个 `message_id` 不要用 `for` 循环逐个调用 `mail +message`;改用 `mail +messages --message-ids `,由 shortcut 批量读取并合并输出。 - 查看原始 HTML: ```bash diff --git a/skills/lark-mail/references/lark-mail-messages.md b/skills/lark-mail/references/lark-mail-messages.md index 3b3c8da2c..1e8654200 100644 --- a/skills/lark-mail/references/lark-mail-messages.md +++ b/skills/lark-mail/references/lark-mail-messages.md @@ -6,14 +6,17 @@ 本 shortcut 是 `mail +message` 的批量版本。每个返回的 `messages[]` 项使用与 `+message` 相同的归一化结构:安全元数据字段直接透传,正文和辅助字段由 shortcut 派生。 +执行时 CLI 每次 `messages.batch_get` 请求最多发送 20 个 `message_id`;超过 20 个 ID 时自动拆成多次请求,并把结果按输入顺序合并输出。当前后端 raw `batch_get` 请求校验上限是 50 个 ID;CLI 的 20 是客户端分批阈值,不是 shortcut 输入上限。 + 优先使用本 shortcut 而非原生 `mail user_mailbox.messages batch_get` API,因为: - 正文字段已 base64url 解码 - 每条邮件的输出结构已归一化 - 不可用的 message ID 会被显式列出 本 skill 对应 shortcut `lark-cli mail +messages`,内部步骤: -1. `POST /open-apis/mail/v1/user_mailboxes/{mailbox}/messages/batch_get` — 批量获取邮件 +1. 按每批最多 20 个 `message_id` 调用 `POST /open-apis/mail/v1/user_mailboxes/{mailbox}/messages/batch_get` 2. 对每条返回的邮件使用与 `+message` 相同的规则归一化输出 +3. 按请求 ID 顺序合并多批结果,并把未返回的 ID 放入 `unavailable_message_ids` ## 命令 @@ -38,7 +41,7 @@ lark-cli mail +messages --message-ids , --dry-run | 参数 | 必填 | 默认值 | 说明 | |------|------|--------|------| -| `--message-ids ` | 是 | — | 逗号分隔的邮件 ID 列表 | +| `--message-ids ` | 是 | — | 逗号分隔的邮件 ID 列表。CLI 每次 `batch_get` 最多发送 20 个 ID 并合并输出;当前后端 raw 请求校验上限是 50 个 ID | | `--mailbox ` | 否 | 当前用户 | 邮箱地址(`user_mailbox_id`) | | `--html` | 否 | true | 是否返回 HTML 正文(`false` 仅返回纯文本,减少带宽) | | `--format ` | 否 | json | 输出格式:`json`(默认)/ `pretty` / `table` / `ndjson` / `csv` | @@ -74,7 +77,8 @@ lark-cli mail +messages --message-ids , --dry-run - **JSON 输出可直接使用**,可直接读取,无需额外编码转换。 - 只需读取一封邮件时请使用 `+message`。 -- `--message-ids` 无硬性上限;shortcut 内部会自动将大列表拆分为多次批量 API 调用。 +- `--message-ids` 无 shortcut 硬性上限;shortcut 内部会自动将大列表按 20 个 ID 一批拆分为多次批量 API 调用并合并输出。 +- 当前后端 raw `messages.batch_get` 请求校验上限是 50 个 ID;不要把这个上限当成 CLI 输入上限,因为 CLI 会先按 20 分批。 - JSON 输出中 `messages[].body_html` 里的 `<` / `>` 可能显示为 `\u003c` / `\u003e`(JSON 安全转义,内容不变,`jq -r` 可还原)。 - `mail +messages` 仅返回附件元数据。如后续步骤需要下载 URL,请针对特定的 `message_id` 和 `attachment_ids` 调用原生附件 URL API。 - 与 `+message` 一样,普通附件和内嵌图片都出现在 `messages[].attachments[]` 中,使用同一个 `user_mailbox.message.attachments download_url` API。 diff --git a/skills/lark-mail/references/lark-mail-triage.md b/skills/lark-mail/references/lark-mail-triage.md index 2f81167fa..c9ca6b88a 100644 --- a/skills/lark-mail/references/lark-mail-triage.md +++ b/skills/lark-mail/references/lark-mail-triage.md @@ -101,7 +101,7 @@ lark-cli mail +triage --page-size 10 } ``` -- `mailbox_id`:当前邮箱标识,用于传递给 `mail +message --mailbox` 以保持公共邮箱上下文 +- `mailbox_id`:当前邮箱标识,用于传递给 `mail +message --mailbox` 或 `mail +messages --mailbox` 以保持公共邮箱上下文 - `has_more`:是否还有下一页 - `page_token`:传入 `--page-token` 可获取下一页;为空字符串表示已到末尾 - token 前缀 `search:` / `list:` 标识来源 API 路径,不可混用 @@ -112,13 +112,13 @@ lark-cli mail +triage --page-size 10 ```text 15 message(s) next page: mail +triage --query '合同审批' --page-token 'search:abc123...' -tip: use mail +message --message-id to read full content +tip: read full content: single message -> mail +message --message-id ; multiple messages -> mail +messages --message-ids ``` 公共邮箱场景下,`--mailbox` 会自动出现在续页和 tip 中: ```text next page: mail +triage --mailbox 'shared@example.com' --query '合同审批' --page-token 'search:abc123...' -tip: use mail +message --mailbox 'shared@example.com' --message-id to read full content +tip: read full content: single message -> mail +message --mailbox 'shared@example.com' --message-id ; multiple messages -> mail +messages --mailbox 'shared@example.com' --message-ids ``` ### 搜索分页注意事项 From fcba9da295782a42c65deccdf0cb026a416461b5 Mon Sep 17 00:00:00 2001 From: bubbmon233 <272202079+bubbmon233@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:24:55 +0800 Subject: [PATCH 2/2] fix(im): use typed API calls for feed groups Change-Type: ci-fix --- shortcuts/im/im_feed_group_list.go | 4 ++-- shortcuts/im/im_feed_group_list_item.go | 4 ++-- shortcuts/im/im_feed_group_query_item.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/shortcuts/im/im_feed_group_list.go b/shortcuts/im/im_feed_group_list.go index c69354d9b..4d5ae5d50 100644 --- a/shortcuts/im/im_feed_group_list.go +++ b/shortcuts/im/im_feed_group_list.go @@ -57,7 +57,7 @@ var ImFeedGroupList = common.Shortcut{ return executeFeedGroupListGroupsAllPages(runtime) } - data, err := runtime.DoAPIJSON("GET", feedGroupListPath, feedGroupListGroupsQuery(runtime), nil) + data, err := runtime.DoAPIJSONTyped("GET", feedGroupListPath, feedGroupListGroupsQuery(runtime), nil) if err != nil { return err } @@ -158,7 +158,7 @@ func executeFeedGroupListGroupsAllPages(rt *common.RuntimeContext) error { params["end_time"] = []string{end} } - data, err := rt.DoAPIJSON("GET", feedGroupListPath, params, nil) + data, err := rt.DoAPIJSONTyped("GET", feedGroupListPath, params, nil) if err != nil { return err } diff --git a/shortcuts/im/im_feed_group_list_item.go b/shortcuts/im/im_feed_group_list_item.go index f4e0d8082..e86ea098b 100644 --- a/shortcuts/im/im_feed_group_list_item.go +++ b/shortcuts/im/im_feed_group_list_item.go @@ -54,7 +54,7 @@ var ImFeedGroupListItem = common.Shortcut{ return executeFeedGroupListAllPages(runtime) } - data, err := runtime.DoAPIJSON("GET", feedGroupListItemPath(runtime), feedGroupListQuery(runtime), nil) + data, err := runtime.DoAPIJSONTyped("GET", feedGroupListItemPath(runtime), feedGroupListQuery(runtime), nil) if err != nil { return err } @@ -163,7 +163,7 @@ func executeFeedGroupListAllPages(rt *common.RuntimeContext) error { params["end_time"] = []string{end} } - data, err := rt.DoAPIJSON("GET", feedGroupListItemPath(rt), params, nil) + data, err := rt.DoAPIJSONTyped("GET", feedGroupListItemPath(rt), params, nil) if err != nil { return err } diff --git a/shortcuts/im/im_feed_group_query_item.go b/shortcuts/im/im_feed_group_query_item.go index d35bebe7f..3bdef6b76 100644 --- a/shortcuts/im/im_feed_group_query_item.go +++ b/shortcuts/im/im_feed_group_query_item.go @@ -47,7 +47,7 @@ var ImFeedGroupQueryItem = common.Shortcut{ return err } - data, err := runtime.DoAPIJSON("POST", feedGroupQueryItemPath(runtime), nil, body) + data, err := runtime.DoAPIJSONTyped("POST", feedGroupQueryItemPath(runtime), nil, body) if err != nil { return err }