Blocked-by: objectstack-ai/objectstack#5161(同包串行:#5160 → #5161 → 本单;#5161 在 email-plugin.ts,本单主体在 email-service.ts + platform-objects,文件面基本不相交,但同包不同批) 维护者两次拍板(2026-08-04,会话内): 1. #5172 按「headers 先行、附件走 storage 引用」拆分; 2. 追加:**邮件队列先不支持大附件** —— 小附件设上限内联进行,大附件维持退回内联投递;storage 引用降为二期(#5172,已移出派发队列)。 本单因此覆盖两块同形改动(同三个文件里的加列 + 重建 + 资格判定,拆开纯属自我串行): ## A. headers_json(终态方案) 1. `sys-email.object.ts` 加 `headers_json`(可空 JSON 文本;老行读回 `undefined` 必须安全); 2. `send()` 落行时持久化 `headers`(两种模式都落,兼有审计价值); 3. `rowToNormalized()` 重建 `headers`; 4. headers 不再是退回内联的理由。 ## B. attachments_json(一期:有界内联,schema 一次到位) 1. `sys-email.object.ts` 加 `attachments_json`,元素形状**现在就设计成二期可扩展**: ``` { filename, contentType, size, hash, inline?: string /* base64 */, storageKey?: string } ``` 一期只有 `inline` 有生产者;`storageKey` 是二期(#5172)的扩展点 —— 将来支持大附件是给已声明字段补生产者,不动 schema、不迁移数据。字段本身要有 TSDoc 说明 `storageKey` 现阶段无生产者、由 #5172 跟踪,免得被 liveness 扫描误判。 2. **上限:整封合计 256KB 原始字节(PM 默认,dev 可凭实测理由调整并在 PR 说明)**;写成导出常量,不做配置项——少一个旋钮少一处漂移,有实证需求再提升为设置。 3. 资格判定:`≤ 上限` → 入队(内容 base64 进 `inline`);`> 上限` → **维持现行为**(退回内联投递 + info 说明,理由文案更新为「附件合计超过 X」)。超限不是错误,最坏结果=今天的现状。 4. `rowToNormalized()` 重建附件(含 `cid`——检查 `EmailAttachment.cid` 是否也要进元素形状,应该要,内联图片依赖它); 5. `Buffer` 与 `string` 两种 `content` 形态都要正确编码/还原(契约是 `string | Buffer`)。 ## 不在本单 - 大附件 storage 引用 + 内容随终态回收 = #5172(二期,维护者定为暂缓,勿动); - ⛔ `packages/spec` 无需动(`headers`/`attachments` 都在契约上)也不许动;⛔ `content/docs/releases/`。 ## 验收 - `queueDelivery` 开 + `send({ headers })` → `queued`,worker 投递的消息含全部自定义 header; - `queueDelivery` 开 + 小附件(≤上限,含中文文件名、`cid` 内联图、Buffer 与 base64 两种 content)→ `queued`,worker 投递的消息附件完整还原(真 `DbQueueAdapter` + wire 测试断言字节一致); - 大附件(>上限)→ 退回内联 + info,文案说明超限,投递内容完整不剥附件; - 老行(两列皆无)读取安全;默认(内联)模式行为不变,既有断言零改动; - `sys_email` 行体积有界:附件列最坏 ~350KB(256KB 的 base64),有一条用例断言超限内容**不会**落列。
Blocked-by: #5161(同包串行:#5160 → #5161 → 本单;#5161 在 email-plugin.ts,本单主体在 email-service.ts + platform-objects,文件面基本不相交,但同包不同批)
维护者两次拍板(2026-08-04,会话内):
本单因此覆盖两块同形改动(同三个文件里的加列 + 重建 + 资格判定,拆开纯属自我串行):
A. headers_json(终态方案)
sys-email.object.ts加headers_json(可空 JSON 文本;老行读回undefined必须安全);send()落行时持久化headers(两种模式都落,兼有审计价值);rowToNormalized()重建headers;B. attachments_json(一期:有界内联,schema 一次到位)
sys-email.object.ts加attachments_json,元素形状现在就设计成二期可扩展:inline有生产者;storageKey是二期(plugin-email: sys_email 无附件/自定义 header 列,带附件的邮件拿不到队列投递的持久化保证(#5160 落地后) #5172)的扩展点 —— 将来支持大附件是给已声明字段补生产者,不动 schema、不迁移数据。字段本身要有 TSDoc 说明storageKey现阶段无生产者、由 plugin-email: sys_email 无附件/自定义 header 列,带附件的邮件拿不到队列投递的持久化保证(#5160 落地后) #5172 跟踪,免得被 liveness 扫描误判。≤ 上限→ 入队(内容 base64 进inline);> 上限→ 维持现行为(退回内联投递 + info 说明,理由文案更新为「附件合计超过 X」)。超限不是错误,最坏结果=今天的现状。rowToNormalized()重建附件(含cid——检查EmailAttachment.cid是否也要进元素形状,应该要,内联图片依赖它);Buffer与string两种content形态都要正确编码/还原(契约是string | Buffer)。不在本单
packages/spec无需动(headers/attachments都在契约上)也不许动;⛔content/docs/releases/。验收
queueDelivery开 +send({ headers })→queued,worker 投递的消息含全部自定义 header;queueDelivery开 + 小附件(≤上限,含中文文件名、cid内联图、Buffer 与 base64 两种 content)→queued,worker 投递的消息附件完整还原(真DbQueueAdapter+ wire 测试断言字节一致);sys_email行体积有界:附件列最坏 ~350KB(256KB 的 base64),有一条用例断言超限内容不会落列。