From 13d67af45e8c8c560e6073b17a0673b45efcf759 Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Mon, 13 Apr 2026 18:26:24 +0800 Subject: [PATCH 1/5] feat: mail support scheduled send --- shortcuts/mail/draft/service.go | 8 +++-- shortcuts/mail/mail_forward.go | 4 ++- shortcuts/mail/mail_reply.go | 4 ++- shortcuts/mail/mail_reply_all.go | 4 ++- shortcuts/mail/mail_send.go | 4 ++- skill-template/domains/mail.md | 26 +++++++++----- skills/lark-mail/SKILL.md | 28 ++++++++++----- .../lark-mail/references/lark-mail-forward.md | 36 ++++++++++++++++++- .../references/lark-mail-reply-all.md | 36 ++++++++++++++++++- .../lark-mail/references/lark-mail-reply.md | 36 ++++++++++++++++++- skills/lark-mail/references/lark-mail-send.md | 34 ++++++++++++++++++ 11 files changed, 195 insertions(+), 25 deletions(-) diff --git a/shortcuts/mail/draft/service.go b/shortcuts/mail/draft/service.go index e62ab680f..4af5eac0a 100644 --- a/shortcuts/mail/draft/service.go +++ b/shortcuts/mail/draft/service.go @@ -59,8 +59,12 @@ func UpdateWithRaw(runtime *common.RuntimeContext, mailboxID, draftID, rawEML st return err } -func Send(runtime *common.RuntimeContext, mailboxID, draftID string) (map[string]interface{}, error) { - return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, nil) +func Send(runtime *common.RuntimeContext, mailboxID, draftID, sendTime string) (map[string]interface{}, error) { + bodyParams := map[string]interface{}{} + if sendTime != "" { + bodyParams["send_time"] = sendTime + } + return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, bodyParams) } func extractDraftID(data map[string]interface{}) string { diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go index a270277b5..05b0d3159 100644 --- a/shortcuts/mail/mail_forward.go +++ b/shortcuts/mail/mail_forward.go @@ -34,6 +34,7 @@ var MailForward = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated, appended after original attachments (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the forward immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -76,6 +77,7 @@ var MailForward = common.Shortcut{ attachFlag := runtime.Str("attach") inlineFlag := runtime.Str("inline") confirmSend := runtime.Bool("confirm-send") + sendTime := runtime.Str("send-time") mailboxID := resolveComposeMailboxID(runtime) sourceMsg, err := fetchComposeSourceMessage(runtime, mailboxID, messageId) @@ -218,7 +220,7 @@ var MailForward = common.Shortcut{ hintSendDraft(runtime, mailboxID, draftID) return nil } - resData, err := draftpkg.Send(runtime, mailboxID, draftID) + resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime) if err != nil { return fmt.Errorf("failed to send forward (draft %s created but not sent): %w", draftID, err) } diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go index 53370d5f9..7090e0a14 100644 --- a/shortcuts/mail/mail_reply.go +++ b/shortcuts/mail/mail_reply.go @@ -32,6 +32,7 @@ var MailReply = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -68,6 +69,7 @@ var MailReply = common.Shortcut{ attachFlag := runtime.Str("attach") inlineFlag := runtime.Str("inline") confirmSend := runtime.Bool("confirm-send") + sendTime := runtime.Str("send-time") inlineSpecs, err := parseInlineSpecs(inlineFlag) if err != nil { @@ -181,7 +183,7 @@ var MailReply = common.Shortcut{ hintSendDraft(runtime, mailboxID, draftID) return nil } - resData, err := draftpkg.Send(runtime, mailboxID, draftID) + resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime) if err != nil { return fmt.Errorf("failed to send reply (draft %s created but not sent): %w", draftID, err) } diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go index f8d6a4529..f5cc91096 100644 --- a/shortcuts/mail/mail_reply_all.go +++ b/shortcuts/mail/mail_reply_all.go @@ -33,6 +33,7 @@ var MailReplyAll = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -70,6 +71,7 @@ var MailReplyAll = common.Shortcut{ attachFlag := runtime.Str("attach") inlineFlag := runtime.Str("inline") confirmSend := runtime.Bool("confirm-send") + sendTime := runtime.Str("send-time") inlineSpecs, err := parseInlineSpecs(inlineFlag) if err != nil { @@ -195,7 +197,7 @@ var MailReplyAll = common.Shortcut{ hintSendDraft(runtime, mailboxID, draftID) return nil } - resData, err := draftpkg.Send(runtime, mailboxID, draftID) + resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime) if err != nil { return fmt.Errorf("failed to send reply-all (draft %s created but not sent): %w", draftID, err) } diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index 6eb11e754..334892e09 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -32,6 +32,7 @@ var MailSend = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the email immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { to := runtime.Str("to") @@ -74,6 +75,7 @@ var MailSend = common.Shortcut{ attachFlag := runtime.Str("attach") inlineFlag := runtime.Str("inline") confirmSend := runtime.Bool("confirm-send") + sendTime := runtime.Str("send-time") senderEmail := resolveComposeSenderEmail(runtime) @@ -145,7 +147,7 @@ var MailSend = common.Shortcut{ hintSendDraft(runtime, mailboxID, draftID) return nil } - resData, err := draftpkg.Send(runtime, mailboxID, draftID) + resData, err := draftpkg.Send(runtime, mailboxID, draftID, sendTime) if err != nil { return fmt.Errorf("failed to send email (draft %s created but not sent): %w", draftID, err) } diff --git a/skill-template/domains/mail.md b/skill-template/domains/mail.md index caba12c87..8ff221549 100644 --- a/skill-template/domains/mail.md +++ b/skill-template/domains/mail.md @@ -42,7 +42,7 @@ 4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送) 5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送) 6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送 -7. **确认投递** — 发送后用 `send_status` 查询投递状态,向用户报告结果 +7. **确认投递** — 立即发送后用 `send_status` 查询投递状态,定时发送后在预定时间后再查询;取消定时发送用 `cancel_scheduled_send` 8. **编辑草稿** — `+draft-edit` 修改已有草稿。正文编辑通过 `--patch-file`:回复/转发草稿用 `set_reply_body` op 保留引用区,普通草稿用 `set_body` op ### CRITICAL — 首次使用任何命令前先查 `-h` @@ -62,15 +62,17 @@ lark-cli mail user_mailbox.messages -h ### 命令选择:先判断邮件类型,再决定草稿还是发送 -| 邮件类型 | 存草稿(不发送) | 直接发送 | -|----------|-----------------|---------| -| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | -| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | -| **转发** | `+forward` | `+forward --confirm-send` | +| 邮件类型 | 存草稿(不发送) | 直接发送 | 定时发送 | +|----------|-----------------|---------|----------| +| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | `+send --confirm-send --send-time ` | +| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | `+reply --confirm-send --send-time ` 或 `+reply-all --confirm-send --send-time ` | +| **转发** | `+forward` | `+forward --confirm-send` | `+forward --confirm-send --send-time ` | - 有原邮件上下文 → 用 `+reply` / `+reply-all` / `+forward`(默认即草稿),**不要用 `+draft-create`** - **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`** -- **发送后必须调用 `send_status` 确认投递状态**(详见下方说明) +- **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明) + +> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),如 `1744608000`,需至少为当前时间 + 5 分钟。 ### 使用公共邮箱或别名(send_as)发信 @@ -109,7 +111,7 @@ lark-cli mail +send --mailbox me --from alias@example.com \ ### 发送后确认投递状态 -邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告: +**立即发送(无 `--send-time`)**:邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告: ```bash lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}' @@ -117,6 +119,14 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 返回每个收件人的投递状态(`status`):1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告结果,如有异常状态(退信/审批拒绝)需重点提示。 +**定时发送(指定了 `--send-time`)**:定时发送不会立即产生 `message_id`,`send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。如需取消定时发送: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + ### 正文格式:优先使用 HTML 撰写邮件正文时,**默认使用 HTML 格式**(body 内容会被自动检测)。仅当用户明确要求纯文本时,才使用 `--plain-text` 标志强制纯文本模式。 diff --git a/skills/lark-mail/SKILL.md b/skills/lark-mail/SKILL.md index 0a42fcdb8..52017bce5 100644 --- a/skills/lark-mail/SKILL.md +++ b/skills/lark-mail/SKILL.md @@ -56,7 +56,7 @@ metadata: 4. **回复** — `+reply` / `+reply-all`(默认存草稿,加 `--confirm-send` 则立即发送) 5. **转发** — `+forward`(默认存草稿,加 `--confirm-send` 则立即发送) 6. **新邮件** — `+send` 存草稿(默认),加 `--confirm-send` 发送 -7. **确认投递** — 发送后用 `send_status` 查询投递状态,向用户报告结果 +7. **确认投递** — 立即发送后用 `send_status` 查询投递状态,定时发送后在预定时间后再查询;取消定时发送用 `cancel_scheduled_send` 8. **编辑草稿** — `+draft-edit` 修改已有草稿。正文编辑通过 `--patch-file`:回复/转发草稿用 `set_reply_body` op 保留引用区,普通草稿用 `set_body` op ### CRITICAL — 首次使用任何命令前先查 `-h` @@ -76,15 +76,17 @@ lark-cli mail user_mailbox.messages -h ### 命令选择:先判断邮件类型,再决定草稿还是发送 -| 邮件类型 | 存草稿(不发送) | 直接发送 | -|----------|-----------------|---------| -| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | -| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | -| **转发** | `+forward` | `+forward --confirm-send` | +| 邮件类型 | 存草稿(不发送) | 直接发送 | 定时发送 | +|----------|-----------------|---------|----------| +| **新邮件** | `+send` 或 `+draft-create` | `+send --confirm-send` | `+send --confirm-send --send-time ` | +| **回复** | `+reply` 或 `+reply-all` | `+reply --confirm-send` 或 `+reply-all --confirm-send` | `+reply --confirm-send --send-time ` 或 `+reply-all --confirm-send --send-time ` | +| **转发** | `+forward` | `+forward --confirm-send` | `+forward --confirm-send --send-time ` | - 有原邮件上下文 → 用 `+reply` / `+reply-all` / `+forward`(默认即草稿),**不要用 `+draft-create`** - **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`** -- **发送后必须调用 `send_status` 确认投递状态**(详见下方说明) +- **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明) + +> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),如 `1744608000`,需至少为当前时间 + 5 分钟。 ### 使用公共邮箱或别名(send_as)发信 @@ -123,7 +125,7 @@ lark-cli mail +send --mailbox me --from alias@example.com \ ### 发送后确认投递状态 -邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告: +**立即发送(无 `--send-time`)**:邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` API 查询投递状态并向用户报告: ```bash lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}' @@ -131,6 +133,14 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 返回每个收件人的投递状态(`status`):1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告结果,如有异常状态(退信/审批拒绝)需重点提示。 +**定时发送(指定了 `--send-time`)**:定时发送不会立即产生 `message_id`,`send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。如需取消定时发送: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + ### 正文格式:优先使用 HTML 撰写邮件正文时,**默认使用 HTML 格式**(body 内容会被自动检测)。仅当用户明确要求纯文本时,才使用 `--plain-text` 标志强制纯文本模式。 @@ -268,6 +278,7 @@ lark-cli mail [flags] # 调用 API ### user_mailbox.drafts + - `cancel_scheduled_send` — 取消定时发送 - `create` — 创建草稿 - `delete` — 删除指定邮箱账户下的单份邮件草稿。注意:对于草稿状态的邮件,只能使用本接口删除,禁止使用 trash_message;被删除的草稿数据无法恢复,请谨慎使用。 - `get` — 获取草稿详情 @@ -347,6 +358,7 @@ lark-cli mail [flags] # 调用 API | `user_mailboxes.accessible_mailboxes` | `mail:user_mailbox:readonly` | | `user_mailboxes.profile` | `mail:user_mailbox:readonly` | | `user_mailboxes.search` | `mail:user_mailbox.message:readonly` | +| `user_mailbox.drafts.cancel_scheduled_send` | `mail:user_mailbox.message:send` | | `user_mailbox.drafts.create` | `mail:user_mailbox.message:modify` | | `user_mailbox.drafts.delete` | `mail:user_mailbox.message:modify` | | `user_mailbox.drafts.get` | `mail:user_mailbox.message:readonly` | diff --git a/skills/lark-mail/references/lark-mail-forward.md b/skills/lark-mail/references/lark-mail-forward.md index 69165af6d..64599eada 100644 --- a/skills/lark-mail/references/lark-mail-forward.md +++ b/skills/lark-mail/references/lark-mail-forward.md @@ -67,6 +67,7 @@ lark-cli mail +forward --message-id <邮件ID> --to alice@example.com --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔,追加在原邮件附件之后。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送转发(默认只保存草稿)。仅在用户明确确认后使用 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -113,6 +114,25 @@ lark-cli mail +forward --message-id <邮件ID> --to bob@example.com --body '

F lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' ``` +### 场景 3:用户说"下午 3 点转发给 Bob"(定时发送) +```bash +# Step 1: 创建转发草稿 +lark-cli mail +forward --message-id <邮件ID> --to bob@example.com --body '

FYI,请查收。

' +# → 返回 draft_id + +# Step 2: 向用户确认 "转发草稿已创建:收件人 bob@example.com,定时 2026-04-14 15:00 发送。确认吗?" + +# Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +``` + +### 场景 4:用户说"等等,先不转发了"(取消定时发送) +```bash +# 取消定时发送(取消后邮件变回草稿) +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` +→ 取消成功后邮件恢复为草稿状态,用户可重新编辑或在之后重新发送。 + ## 转发整个会话 `+forward` 操作的是单封邮件(`--message-id`),但转发整个会话时应 forward **会话中最后一条消息**,因为邮件客户端会将完整的回复链嵌套在最新一条中。典型流程: @@ -138,7 +158,9 @@ lark-cli mail +forward --message-id <最后一条的message_id> --to recipient@e 转发发送成功后: -**1. 确认投递状态**(必须)— 用返回的 `message_id` 查询投递状态: +**1. 确认投递状态**(仅立即发送 — 无 `--send-time` 时必须) + +用返回的 `message_id` 查询投递状态: ```bash lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}' @@ -146,6 +168,18 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 状态码:1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告投递结果,异常状态需重点提示。 +**1b. 定时发送(指定了 `--send-time`)** + +定时发送不会立即产生 `message_id`,因此 `send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。 + +如需取消定时发送: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + **2. 标记已读**(可选)— 询问用户是否需要将原邮件标记为已读。如果用户同意: ```bash diff --git a/skills/lark-mail/references/lark-mail-reply-all.md b/skills/lark-mail/references/lark-mail-reply-all.md index c5f736519..71131538e 100644 --- a/skills/lark-mail/references/lark-mail-reply-all.md +++ b/skills/lark-mail/references/lark-mail-reply-all.md @@ -71,6 +71,7 @@ lark-cli mail +reply-all --message-id <邮件ID> --body '测试' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -117,6 +118,25 @@ lark-cli mail +reply-all --message-id <邮件ID> --body '

已确认。

' lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' ``` +### 场景 3:用户说"下午 3 点回复全部说已确认"(定时发送) +```bash +# Step 1: 创建回复全部草稿 +lark-cli mail +reply-all --message-id <邮件ID> --body '

已确认。

' +# → 返回 draft_id + +# Step 2: 向用户确认 "回复全部草稿已创建:收件人 alice@, bob@, carol@,内容「已确认。」定时 2026-04-14 15:00 发送。确认吗?" + +# Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +``` + +### 场景 4:用户说"等等,先不回复了"(取消定时发送) +```bash +# 取消定时发送(取消后邮件变回草稿) +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` +→ 取消成功后邮件恢复为草稿状态,用户可重新编辑或在之后重新发送。 + ## 实现说明 - 自动收件人规则:原发件人优先进入 To,原 To/Cc 进入 Cc。 @@ -128,7 +148,9 @@ lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_ 回复发送成功后: -**1. 确认投递状态**(必须)— 用返回的 `message_id` 查询投递状态: +**1. 确认投递状态**(仅立即发送 — 无 `--send-time` 时必须) + +用返回的 `message_id` 查询投递状态: ```bash lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}' @@ -136,6 +158,18 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 状态码:1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告投递结果,异常状态需重点提示。 +**1b. 定时发送(指定了 `--send-time`)** + +定时发送不会立即产生 `message_id`,因此 `send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。 + +如需取消定时发送: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + **2. 标记已读**(可选)— 询问用户是否需要将原邮件标记为已读。如果用户同意: ```bash diff --git a/skills/lark-mail/references/lark-mail-reply.md b/skills/lark-mail/references/lark-mail-reply.md index 2a0485e61..dc2c7b7b5 100644 --- a/skills/lark-mail/references/lark-mail-reply.md +++ b/skills/lark-mail/references/lark-mail-reply.md @@ -74,6 +74,7 @@ lark-cli mail +reply --message-id <邮件ID> --body '

测试

' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -120,6 +121,25 @@ lark-cli mail +reply --message-id <邮件ID> --body '

已处理,谢谢。

"}' ``` +### 场景 3:用户说"下午 3 点回复这封邮件说已处理"(定时发送) +```bash +# Step 1: 创建回复草稿 +lark-cli mail +reply --message-id <邮件ID> --body '

已处理,谢谢。

' +# → 返回 draft_id + +# Step 2: 向用户确认 "回复草稿已创建:回复给 alice@example.com,内容「已处理,谢谢。」定时 2026-04-14 15:00 发送。确认吗?" + +# Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +``` + +### 场景 4:用户说"等等,先不回复了"(取消定时发送) +```bash +# 取消定时发送(取消后邮件变回草稿) +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` +→ 取消成功后邮件恢复为草稿状态,用户可重新编辑或在之后重新发送。 + ## 实现说明 ### 会话维护 @@ -143,7 +163,9 @@ References: <原邮件references + smtp_message_id> 回复发送成功后: -**1. 确认投递状态**(必须)— 用返回的 `message_id` 查询投递状态: +**1. 确认投递状态**(仅立即发送 — 无 `--send-time` 时必须) + +用返回的 `message_id` 查询投递状态: ```bash lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me","message_id":"<发送返回的 message_id>"}' @@ -151,6 +173,18 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 状态码:1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告投递结果,异常状态需重点提示。 +**1b. 定时发送(指定了 `--send-time`)** + +定时发送不会立即产生 `message_id`,因此 `send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询。 + +如需取消定时发送: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + **2. 标记已读**(可选)— 询问用户是否需要将原邮件标记为已读。如果用户同意: ```bash diff --git a/skills/lark-mail/references/lark-mail-send.md b/skills/lark-mail/references/lark-mail-send.md index eca4d300f..94e667296 100644 --- a/skills/lark-mail/references/lark-mail-send.md +++ b/skills/lark-mail/references/lark-mail-send.md @@ -71,6 +71,7 @@ lark-cli mail +send --to alice@example.com --subject '测试' --body '

test

` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送邮件(默认只保存草稿)。仅在用户明确确认收件人和内容后使用 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -119,8 +120,29 @@ lark-cli mail +send --to alice@example.com --subject '收到' --body '

已收 lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' ``` +### 场景 3:用户说"下午 3 点给 Alice 发一封周报"(定时发送) +```bash +# Step 1: 创建草稿(定时发送也走草稿流程) +lark-cli mail +send --to alice@example.com --subject '周报' --body '

本周进展如下...

' +# → 返回 draft_id + +# Step 2: 向用户确认 "邮件草稿已创建:收件人 alice@example.com,主题「周报」,定时 2026-04-14 15:00 发送。确认吗?" + +# Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +``` + +### 场景 4:用户说"等等,先不发那封邮件了"(取消定时发送) +```bash +# 取消定时发送(取消后邮件变回草稿) +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` +→ 取消成功后邮件恢复为草稿状态,用户可重新编辑或在之后重新发送。 + ## 发送后跟进 +### 立即发送(无 `--send-time`) + 邮件发送成功后(收到 `message_id`),**必须**调用 `send_status` 查询投递状态: ```bash @@ -129,6 +151,18 @@ lark-cli mail user_mailbox.messages send_status --params '{"user_mailbox_id":"me 状态码:1=正在投递, 2=投递失败重试, 3=退信, 4=投递成功, 5=待审批, 6=审批拒绝。向用户简要报告各收件人投递结果,异常状态需重点提示。 +### 定时发送(指定了 `--send-time`) + +定时发送不会立即产生 `message_id`,因此 `send_status` 在定时发送成功后会返回"待发送"状态,**不建议在定时发送后立即查询**。可在预定发送时间后再查询投递状态。 + +如需取消定时发送,可在预定时间前调用取消接口: + +```bash +lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"me","draft_id":""}' +``` + +**取消后邮件会变回草稿**,可继续编辑或在之后重新发送。 + ## 实现说明 - 使用 EML 构建器生成完整 MIME 邮件并 base64url 编码后发送。 From c3c3198ea094877536dfbbae72128ebd55cb70ff Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Wed, 15 Apr 2026 11:25:09 +0800 Subject: [PATCH 2/5] fix(mail): add --send-time validation and fix stale timestamp examples - Pass nil body instead of empty map when sendTime is absent in draft.Send() - Add validateSendTime() to check timestamp format and >=now+5min constraint - Call validateSendTime in all 4 handler Validate funcs (send/reply/reply-all/forward) - Replace hardcoded 1744608000 with placeholder in all docs Co-Authored-By: AI --- shortcuts/mail/draft/service.go | 4 ++-- shortcuts/mail/helpers.go | 19 +++++++++++++++++++ shortcuts/mail/mail_forward.go | 5 ++++- shortcuts/mail/mail_reply.go | 5 ++++- shortcuts/mail/mail_reply_all.go | 5 ++++- shortcuts/mail/mail_send.go | 5 ++++- skill-template/domains/mail.md | 2 +- skills/lark-mail/SKILL.md | 2 +- .../lark-mail/references/lark-mail-forward.md | 4 ++-- .../references/lark-mail-reply-all.md | 4 ++-- .../lark-mail/references/lark-mail-reply.md | 4 ++-- skills/lark-mail/references/lark-mail-send.md | 4 ++-- 12 files changed, 47 insertions(+), 16 deletions(-) diff --git a/shortcuts/mail/draft/service.go b/shortcuts/mail/draft/service.go index 4af5eac0a..34e668fe4 100644 --- a/shortcuts/mail/draft/service.go +++ b/shortcuts/mail/draft/service.go @@ -60,9 +60,9 @@ func UpdateWithRaw(runtime *common.RuntimeContext, mailboxID, draftID, rawEML st } func Send(runtime *common.RuntimeContext, mailboxID, draftID, sendTime string) (map[string]interface{}, error) { - bodyParams := map[string]interface{}{} + var bodyParams map[string]interface{} if sendTime != "" { - bodyParams["send_time"] = sendTime + bodyParams = map[string]interface{}{"send_time": sendTime} } return runtime.CallAPI("POST", mailboxPath(mailboxID, "drafts", draftID, "send"), nil, bodyParams) } diff --git a/shortcuts/mail/helpers.go b/shortcuts/mail/helpers.go index c528554d9..dcee72e97 100644 --- a/shortcuts/mail/helpers.go +++ b/shortcuts/mail/helpers.go @@ -17,6 +17,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/internal/auth" @@ -1906,6 +1907,24 @@ func checkAttachmentSizeLimit(fio fileio.FileIO, filePaths []string, extraBytes return nil } +// validateSendTime checks that --send-time, if provided, is a valid Unix +// timestamp in seconds and is at least 5 minutes in the future. +func validateSendTime(runtime *common.RuntimeContext) error { + sendTime := runtime.Str("send-time") + if sendTime == "" { + return nil + } + ts, err := strconv.ParseInt(sendTime, 10, 64) + if err != nil { + return fmt.Errorf("--send-time must be a valid Unix timestamp in seconds, got %q", sendTime) + } + minTime := time.Now().Unix() + 5*60 + if ts < minTime { + return fmt.Errorf("--send-time must be at least 5 minutes in the future (minimum: %d, got: %d)", minTime, ts) + } + return nil +} + // validateConfirmSendScope checks that the user's token includes the // mail:user_mailbox.message:send scope when --confirm-send is set. // This scope is not declared in the shortcut's static Scopes (to keep the diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go index 05b0d3159..69250d08b 100644 --- a/shortcuts/mail/mail_forward.go +++ b/shortcuts/mail/mail_forward.go @@ -34,7 +34,7 @@ var MailForward = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated, appended after original attachments (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the forward immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, - {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -60,6 +60,9 @@ var MailForward = common.Shortcut{ if err := validateConfirmSendScope(runtime); err != nil { return err } + if err := validateSendTime(runtime); err != nil { + return err + } if runtime.Bool("confirm-send") { if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil { return err diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go index 7090e0a14..6634552fd 100644 --- a/shortcuts/mail/mail_reply.go +++ b/shortcuts/mail/mail_reply.go @@ -32,7 +32,7 @@ var MailReply = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, - {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -57,6 +57,9 @@ var MailReply = common.Shortcut{ if err := validateConfirmSendScope(runtime); err != nil { return err } + if err := validateSendTime(runtime); err != nil { + return err + } return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "") }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go index f5cc91096..20552ab84 100644 --- a/shortcuts/mail/mail_reply_all.go +++ b/shortcuts/mail/mail_reply_all.go @@ -33,7 +33,7 @@ var MailReplyAll = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the reply immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, - {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { messageId := runtime.Str("message-id") @@ -58,6 +58,9 @@ var MailReplyAll = common.Shortcut{ if err := validateConfirmSendScope(runtime); err != nil { return err } + if err := validateSendTime(runtime); err != nil { + return err + } return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), "") }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index 334892e09..3b7ea4bdf 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -32,7 +32,7 @@ var MailSend = common.Shortcut{ {Name: "attach", Desc: "Attachment file path(s), comma-separated (relative path only)"}, {Name: "inline", Desc: "Inline images as a JSON array. Each entry: {\"cid\":\"\",\"file_path\":\"\"}. All file_path values must be relative paths. Cannot be used with --plain-text. CID images are embedded via in the HTML body. CID is a unique identifier, e.g. a random hex string like \"a1b2c3d4e5f6a7b8c9d0\"."}, {Name: "confirm-send", Type: "bool", Desc: "Send the email immediately instead of saving as draft. Only use after the user has explicitly confirmed recipients and content."}, - {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds (e.g. 1744608000). Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, + {Name: "send-time", Desc: "Scheduled send time as a Unix timestamp in seconds. Must be at least 5 minutes in the future. Use with --confirm-send to schedule the email."}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { to := runtime.Str("to") @@ -63,6 +63,9 @@ var MailSend = common.Shortcut{ if err := validateComposeHasAtLeastOneRecipient(runtime.Str("to"), runtime.Str("cc"), runtime.Str("bcc")); err != nil { return err } + if err := validateSendTime(runtime); err != nil { + return err + } return validateComposeInlineAndAttachments(runtime.FileIO(), runtime.Str("attach"), runtime.Str("inline"), runtime.Bool("plain-text"), runtime.Str("body")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/skill-template/domains/mail.md b/skill-template/domains/mail.md index 8ff221549..c81a35b00 100644 --- a/skill-template/domains/mail.md +++ b/skill-template/domains/mail.md @@ -72,7 +72,7 @@ lark-cli mail user_mailbox.messages -h - **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`** - **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明) -> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),如 `1744608000`,需至少为当前时间 + 5 分钟。 +> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),需至少为当前时间 + 5 分钟。 ### 使用公共邮箱或别名(send_as)发信 diff --git a/skills/lark-mail/SKILL.md b/skills/lark-mail/SKILL.md index 52017bce5..ee367d371 100644 --- a/skills/lark-mail/SKILL.md +++ b/skills/lark-mail/SKILL.md @@ -86,7 +86,7 @@ lark-cli mail user_mailbox.messages -h - **发送前必须向用户确认收件人和内容,用户明确同意后才可加 `--confirm-send`** - **立即发送后必须调用 `send_status` 确认投递状态**;定时发送(`--send-time`)在预定发送时间后再查询,取消定时发送用 `cancel_scheduled_send`(详见下方说明) -> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),如 `1744608000`,需至少为当前时间 + 5 分钟。 +> **定时发送注意事项**:`--send-time` 必须与 `--confirm-send` 配合使用,不能单独使用。`send_time` 为 Unix 时间戳(秒),需至少为当前时间 + 5 分钟。 ### 使用公共邮箱或别名(send_as)发信 diff --git a/skills/lark-mail/references/lark-mail-forward.md b/skills/lark-mail/references/lark-mail-forward.md index 64599eada..c267193da 100644 --- a/skills/lark-mail/references/lark-mail-forward.md +++ b/skills/lark-mail/references/lark-mail-forward.md @@ -67,7 +67,7 @@ lark-cli mail +forward --message-id <邮件ID> --to alice@example.com --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔,追加在原邮件附件之后。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送转发(默认只保存草稿)。仅在用户明确确认后使用 | -| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -123,7 +123,7 @@ lark-cli mail +forward --message-id <邮件ID> --to bob@example.com --body '

F # Step 2: 向用户确认 "转发草稿已创建:收件人 bob@example.com,定时 2026-04-14 15:00 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) -lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' ``` ### 场景 4:用户说"等等,先不转发了"(取消定时发送) diff --git a/skills/lark-mail/references/lark-mail-reply-all.md b/skills/lark-mail/references/lark-mail-reply-all.md index 71131538e..4ad901bbd 100644 --- a/skills/lark-mail/references/lark-mail-reply-all.md +++ b/skills/lark-mail/references/lark-mail-reply-all.md @@ -71,7 +71,7 @@ lark-cli mail +reply-all --message-id <邮件ID> --body '测试' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | -| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -127,7 +127,7 @@ lark-cli mail +reply-all --message-id <邮件ID> --body '

已确认。

' # Step 2: 向用户确认 "回复全部草稿已创建:收件人 alice@, bob@, carol@,内容「已确认。」定时 2026-04-14 15:00 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) -lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' ``` ### 场景 4:用户说"等等,先不回复了"(取消定时发送) diff --git a/skills/lark-mail/references/lark-mail-reply.md b/skills/lark-mail/references/lark-mail-reply.md index dc2c7b7b5..96f0e1a01 100644 --- a/skills/lark-mail/references/lark-mail-reply.md +++ b/skills/lark-mail/references/lark-mail-reply.md @@ -74,7 +74,7 @@ lark-cli mail +reply --message-id <邮件ID> --body '

测试

' --dry-run | `--attach ` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送回复(默认只保存草稿)。仅在用户明确确认后使用 | -| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -130,7 +130,7 @@ lark-cli mail +reply --message-id <邮件ID> --body '

已处理,谢谢。

"}' --data '{"send_time":1744608000}' +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' ``` ### 场景 4:用户说"等等,先不回复了"(取消定时发送) diff --git a/skills/lark-mail/references/lark-mail-send.md b/skills/lark-mail/references/lark-mail-send.md index 94e667296..5b6b2fc18 100644 --- a/skills/lark-mail/references/lark-mail-send.md +++ b/skills/lark-mail/references/lark-mail-send.md @@ -71,7 +71,7 @@ lark-cli mail +send --to alice@example.com --subject '测试' --body '

test

` | 否 | 附件文件路径,多个用逗号分隔。相对路径 | | `--inline ` | 否 | 高级用法:手动指定内嵌图片 CID 映射。推荐直接在 `--body` 中使用 ``(自动解析)。仅在需要精确控制 CID 命名时使用此参数。格式:`'[{"cid":"mycid","file_path":"./logo.png"}]'`,在 body 中用 `` 引用。不可与 `--plain-text` 同时使用 | | `--confirm-send` | 否 | 确认发送邮件(默认只保存草稿)。仅在用户明确确认收件人和内容后使用 | -| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒),如 `1744608000`。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | +| `--send-time ` | 否 | 定时发送时间,Unix 时间戳(秒)。需至少为当前时间 + 5 分钟。配合 `--confirm-send` 使用可定时发送邮件 | | `--dry-run` | 否 | 仅打印请求,不执行 | ## 返回值 @@ -129,7 +129,7 @@ lark-cli mail +send --to alice@example.com --subject '周报' --body '

本周 # Step 2: 向用户确认 "邮件草稿已创建:收件人 alice@example.com,主题「周报」,定时 2026-04-14 15:00 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) -lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":1744608000}' +lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' ``` ### 场景 4:用户说"等等,先不发那封邮件了"(取消定时发送) From 6617580669d44901b9e83ce88ca5a1c6494cdcaa Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Wed, 15 Apr 2026 11:34:02 +0800 Subject: [PATCH 3/5] fix(mail): replace stale hardcoded schedule date in doc examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace "2026-04-14 15:00" with "<目标时间>" placeholder in all scheduled-send scenario examples to avoid stale dates. Co-Authored-By: AI --- skills/lark-mail/references/lark-mail-forward.md | 2 +- skills/lark-mail/references/lark-mail-reply-all.md | 2 +- skills/lark-mail/references/lark-mail-reply.md | 2 +- skills/lark-mail/references/lark-mail-send.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skills/lark-mail/references/lark-mail-forward.md b/skills/lark-mail/references/lark-mail-forward.md index c267193da..17a271d42 100644 --- a/skills/lark-mail/references/lark-mail-forward.md +++ b/skills/lark-mail/references/lark-mail-forward.md @@ -120,7 +120,7 @@ lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_ lark-cli mail +forward --message-id <邮件ID> --to bob@example.com --body '

FYI,请查收。

' # → 返回 draft_id -# Step 2: 向用户确认 "转发草稿已创建:收件人 bob@example.com,定时 2026-04-14 15:00 发送。确认吗?" +# Step 2: 向用户确认 "转发草稿已创建:收件人 bob@example.com,定时 <目标时间> 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' diff --git a/skills/lark-mail/references/lark-mail-reply-all.md b/skills/lark-mail/references/lark-mail-reply-all.md index 4ad901bbd..645f758a8 100644 --- a/skills/lark-mail/references/lark-mail-reply-all.md +++ b/skills/lark-mail/references/lark-mail-reply-all.md @@ -124,7 +124,7 @@ lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_ lark-cli mail +reply-all --message-id <邮件ID> --body '

已确认。

' # → 返回 draft_id -# Step 2: 向用户确认 "回复全部草稿已创建:收件人 alice@, bob@, carol@,内容「已确认。」定时 2026-04-14 15:00 发送。确认吗?" +# Step 2: 向用户确认 "回复全部草稿已创建:收件人 alice@, bob@, carol@,内容「已确认。」定时 <目标时间> 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' diff --git a/skills/lark-mail/references/lark-mail-reply.md b/skills/lark-mail/references/lark-mail-reply.md index 96f0e1a01..c75902de3 100644 --- a/skills/lark-mail/references/lark-mail-reply.md +++ b/skills/lark-mail/references/lark-mail-reply.md @@ -127,7 +127,7 @@ lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_ lark-cli mail +reply --message-id <邮件ID> --body '

已处理,谢谢。

' # → 返回 draft_id -# Step 2: 向用户确认 "回复草稿已创建:回复给 alice@example.com,内容「已处理,谢谢。」定时 2026-04-14 15:00 发送。确认吗?" +# Step 2: 向用户确认 "回复草稿已创建:回复给 alice@example.com,内容「已处理,谢谢。」定时 <目标时间> 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' diff --git a/skills/lark-mail/references/lark-mail-send.md b/skills/lark-mail/references/lark-mail-send.md index 5b6b2fc18..1b4a4053e 100644 --- a/skills/lark-mail/references/lark-mail-send.md +++ b/skills/lark-mail/references/lark-mail-send.md @@ -126,7 +126,7 @@ lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_ lark-cli mail +send --to alice@example.com --subject '周报' --body '

本周进展如下...

' # → 返回 draft_id -# Step 2: 向用户确认 "邮件草稿已创建:收件人 alice@example.com,主题「周报」,定时 2026-04-14 15:00 发送。确认吗?" +# Step 2: 向用户确认 "邮件草稿已创建:收件人 alice@example.com,主题「周报」,定时 <目标时间> 发送。确认吗?" # Step 3: 用户确认后定时发送(send_time 为 Unix 时间戳,需至少当前时间 + 5 分钟) lark-cli mail user_mailbox.drafts send --params '{"user_mailbox_id":"me","draft_id":""}' --data '{"send_time":""}' From a828e603f69bf59904c5105543187014137cebe9 Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Wed, 15 Apr 2026 11:47:07 +0800 Subject: [PATCH 4/5] fix(mail): enforce --confirm-send with --send-time and output draft_id for scheduled sends - validateSendTime now rejects --send-time without --confirm-send - Scheduled send output includes draft_id, scheduled_send_time, and cancel_tip so users can cancel via cancel_scheduled_send - Applied to all 4 handlers (send/reply/reply-all/forward) --- shortcuts/mail/helpers.go | 7 +++++-- shortcuts/mail/mail_forward.go | 12 ++++++++++-- shortcuts/mail/mail_reply.go | 12 ++++++++++-- shortcuts/mail/mail_reply_all.go | 12 ++++++++++-- shortcuts/mail/mail_send.go | 12 ++++++++++-- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/shortcuts/mail/helpers.go b/shortcuts/mail/helpers.go index dcee72e97..024340ee9 100644 --- a/shortcuts/mail/helpers.go +++ b/shortcuts/mail/helpers.go @@ -1907,13 +1907,16 @@ func checkAttachmentSizeLimit(fio fileio.FileIO, filePaths []string, extraBytes return nil } -// validateSendTime checks that --send-time, if provided, is a valid Unix -// timestamp in seconds and is at least 5 minutes in the future. +// validateSendTime checks that --send-time, if provided, requires --confirm-send, +// is a valid Unix timestamp in seconds, and is at least 5 minutes in the future. func validateSendTime(runtime *common.RuntimeContext) error { sendTime := runtime.Str("send-time") if sendTime == "" { return nil } + if !runtime.Bool("confirm-send") { + return fmt.Errorf("--send-time requires --confirm-send to be set") + } ts, err := strconv.ParseInt(sendTime, 10, 64) if err != nil { return fmt.Errorf("--send-time must be a valid Unix timestamp in seconds, got %q", sendTime) diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go index 69250d08b..e557a30e9 100644 --- a/shortcuts/mail/mail_forward.go +++ b/shortcuts/mail/mail_forward.go @@ -227,10 +227,18 @@ var MailForward = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send forward (draft %s created but not sent): %w", draftID, err) } - runtime.Out(map[string]interface{}{ + result := map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - }, nil) + } + if sendTime != "" { + result["draft_id"] = draftID + result["scheduled_send_time"] = sendTime + result["cancel_tip"] = fmt.Sprintf( + `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, + mailboxID, draftID) + } + runtime.Out(result, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go index 6634552fd..19c3f6e23 100644 --- a/shortcuts/mail/mail_reply.go +++ b/shortcuts/mail/mail_reply.go @@ -190,10 +190,18 @@ var MailReply = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send reply (draft %s created but not sent): %w", draftID, err) } - runtime.Out(map[string]interface{}{ + result := map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - }, nil) + } + if sendTime != "" { + result["draft_id"] = draftID + result["scheduled_send_time"] = sendTime + result["cancel_tip"] = fmt.Sprintf( + `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, + mailboxID, draftID) + } + runtime.Out(result, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go index 20552ab84..df56ee514 100644 --- a/shortcuts/mail/mail_reply_all.go +++ b/shortcuts/mail/mail_reply_all.go @@ -204,10 +204,18 @@ var MailReplyAll = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send reply-all (draft %s created but not sent): %w", draftID, err) } - runtime.Out(map[string]interface{}{ + result := map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - }, nil) + } + if sendTime != "" { + result["draft_id"] = draftID + result["scheduled_send_time"] = sendTime + result["cancel_tip"] = fmt.Sprintf( + `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, + mailboxID, draftID) + } + runtime.Out(result, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index 3b7ea4bdf..717459e93 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -154,10 +154,18 @@ var MailSend = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send email (draft %s created but not sent): %w", draftID, err) } - runtime.Out(map[string]interface{}{ + result := map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - }, nil) + } + if sendTime != "" { + result["draft_id"] = draftID + result["scheduled_send_time"] = sendTime + result["cancel_tip"] = fmt.Sprintf( + `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, + mailboxID, draftID) + } + runtime.Out(result, nil) return nil }, } From 9b248093280814593aa2111c9801ca90cbc954d8 Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Wed, 15 Apr 2026 11:49:34 +0800 Subject: [PATCH 5/5] fix(mail): revert unnecessary draft_id in scheduled send output Scheduled sends do return message_id; cancellation also uses message_id. The only limitation is send_status cannot be queried immediately. The extra draft_id/cancel_tip output was unnecessary. --- shortcuts/mail/mail_forward.go | 12 ++---------- shortcuts/mail/mail_reply.go | 12 ++---------- shortcuts/mail/mail_reply_all.go | 12 ++---------- shortcuts/mail/mail_send.go | 12 ++---------- 4 files changed, 8 insertions(+), 40 deletions(-) diff --git a/shortcuts/mail/mail_forward.go b/shortcuts/mail/mail_forward.go index e557a30e9..69250d08b 100644 --- a/shortcuts/mail/mail_forward.go +++ b/shortcuts/mail/mail_forward.go @@ -227,18 +227,10 @@ var MailForward = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send forward (draft %s created but not sent): %w", draftID, err) } - result := map[string]interface{}{ + runtime.Out(map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - } - if sendTime != "" { - result["draft_id"] = draftID - result["scheduled_send_time"] = sendTime - result["cancel_tip"] = fmt.Sprintf( - `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, - mailboxID, draftID) - } - runtime.Out(result, nil) + }, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_reply.go b/shortcuts/mail/mail_reply.go index 19c3f6e23..6634552fd 100644 --- a/shortcuts/mail/mail_reply.go +++ b/shortcuts/mail/mail_reply.go @@ -190,18 +190,10 @@ var MailReply = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send reply (draft %s created but not sent): %w", draftID, err) } - result := map[string]interface{}{ + runtime.Out(map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - } - if sendTime != "" { - result["draft_id"] = draftID - result["scheduled_send_time"] = sendTime - result["cancel_tip"] = fmt.Sprintf( - `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, - mailboxID, draftID) - } - runtime.Out(result, nil) + }, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_reply_all.go b/shortcuts/mail/mail_reply_all.go index df56ee514..20552ab84 100644 --- a/shortcuts/mail/mail_reply_all.go +++ b/shortcuts/mail/mail_reply_all.go @@ -204,18 +204,10 @@ var MailReplyAll = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send reply-all (draft %s created but not sent): %w", draftID, err) } - result := map[string]interface{}{ + runtime.Out(map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - } - if sendTime != "" { - result["draft_id"] = draftID - result["scheduled_send_time"] = sendTime - result["cancel_tip"] = fmt.Sprintf( - `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, - mailboxID, draftID) - } - runtime.Out(result, nil) + }, nil) hintMarkAsRead(runtime, mailboxID, messageId) return nil }, diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index 717459e93..3b7ea4bdf 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -154,18 +154,10 @@ var MailSend = common.Shortcut{ if err != nil { return fmt.Errorf("failed to send email (draft %s created but not sent): %w", draftID, err) } - result := map[string]interface{}{ + runtime.Out(map[string]interface{}{ "message_id": resData["message_id"], "thread_id": resData["thread_id"], - } - if sendTime != "" { - result["draft_id"] = draftID - result["scheduled_send_time"] = sendTime - result["cancel_tip"] = fmt.Sprintf( - `To cancel scheduled send: lark-cli mail user_mailbox.drafts cancel_scheduled_send --params '{"user_mailbox_id":"%s","draft_id":"%s"}'`, - mailboxID, draftID) - } - runtime.Out(result, nil) + }, nil) return nil }, }