From ab30ce56c5e50b6550323b5f81beffd25342a542 Mon Sep 17 00:00:00 2001 From: xcs Date: Wed, 26 Aug 2026 02:16:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92=E4=BF=AE:=E5=85=A5=E7=AB=99?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E5=8F=96=E6=A0=B9=E4=B8=8D=E5=8F=96=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E4=B8=80=E8=B7=B3,=E9=99=84=E4=BB=B6=E7=BC=BA=20uploa?= =?UTF-8?q?dRef=20=E4=B8=8D=E9=9D=99=E9=BB=98=E4=B8=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审逐跳走了一遍:我方出站登记的 threadId 恒是这一串的首封,而归属解析①级是精确等值。 入站若取 In-Reply-To(最近一跳),跑一个来回就漂 —— 我方 M1 → 客户回 M1(命中)→ 我方再回 (实际 M2、仍记在 M1 名下)→ 客户回 M2 → 库里从没记过 M2 ⇒ ①级落空,退到③地址+时间窗。 取 references[0](根)才稳定;客户端只给 In-Reply-To 时退回它。 同批:app 层丢掉没给 uploadRef 的附件却照回 queued:true —— 模块自己那一层是硬的(抛 MailSendInvalidError), app 层比它松,活动说「带上 q.pdf」而信发了附件没了、没人吭声。翻成 invalid。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr --- docs/superpowers/plans/2026-08-25-mail.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-08-25-mail.md b/docs/superpowers/plans/2026-08-25-mail.md index 235adbf..3e6ada0 100644 --- a/docs/superpowers/plans/2026-08-25-mail.md +++ b/docs/superpowers/plans/2026-08-25-mail.md @@ -5473,9 +5473,10 @@ function fakeSend(result: Partial = {}): { send: (input: MailSen } describe('toInboundMail', () => { - it('线程取 References 末项(没 In-Reply-To);只带存下来的附件;纯 html 剥成正文', () => { + it('线程取 References **首项**(即这一串的根);只带存下来的附件;纯 html 剥成正文', () => { const mail: InboundMail = toInboundMail(detail); - expect(mail.threadId).toBe(''); + // 根 = 我方出站登记的那个 threadId —— 取最近一跳会让①级归属跑一个来回就漂 + expect(mail.threadId).toBe(''); expect(mail.from).toBe('buyer@acme.com'); expect(mail.to).toEqual(['sales@we.com']); expect(mail.cc).toEqual(['ops@we.com']); @@ -5707,9 +5708,18 @@ import { } from '@modules/mail'; import type { ToolHandler } from '@modules/toolbox'; -/** mail 的入站详情 → channel 的 InboundMail:只带存下来的附件;线程 = In-Reply-To ⊃ References 末项 */ +/** + * mail 的入站详情 → channel 的 InboundMail:只带存下来的附件。 + * + * **线程取的是根,不是最近一跳**:我方出站登记的 threadId 恒是这一串的**首封** + * (`channelTransport` 回的是 `input.threadId ?? messageId`,channel 据此 `recordOutbound`), + * 而归属解析①级是精确等值比对。若这里取 `inReplyTo`(最近一跳),跑一个来回就漂: + * 我方 M1 → 客户回 M1(命中)→ 我方再回(实际 M2、仍记在 M1 名下)→ 客户回 M2 → + * 取到 M2,而库里从没记过 M2 ⇒ ①级落空,退到③地址+时间窗。 + * `references[0]` 才稳定:客户端续链时首项恒是首封;客户端只给 In-Reply-To 时退回它。 + */ export function toInboundMail(mail: MailInboundDetail): InboundMail { - const threadId = mail.inReplyTo ?? mail.references.at(-1); + const threadId = mail.references[0] ?? mail.inReplyTo ?? undefined; return { messageId: mail.messageId, ...(threadId === undefined ? {} : { threadId }), @@ -5767,7 +5777,8 @@ function attachmentsOf(value: unknown): MailSendInput['attachments'] { if (typeof v !== 'object' || v === null) return []; const a = v as Record; const uploadRef = typeof a['uploadRef'] === 'string' && a['uploadRef'] !== '' ? a['uploadRef'] : undefined; - if (uploadRef === undefined) return []; + // 没给 uploadRef 的附件**不静默丢掉**:活动说「带上 q.pdf」,信发了附件没了还回 queued:true,是界面撒谎 + if (uploadRef === undefined) throw new MailSendInvalidError([`附件 ${String(a['name'] ?? '(无名)')} 没给 uploadRef`]); const name = typeof a['name'] === 'string' && a['name'] !== '' ? a['name'] : uploadRef; return [{ name, contentType: 'application/octet-stream', uploadRef }]; });