Repository navigation
cli: OS_EMAIL_PROVIDER=resend/postmark 缺 apiKey 时静默降级为 LogTransport —— #5087 在 CLI 层遗留的同形缺口 #5132
Description
Activity
分诊(PM 循环,邮件线派生):auto-queue ——
pm:queue+domain:cli。这是 #5087 的同一条缺陷在 CLI 层的残留,而且插件层已经修好了(
makeTransport建不出就抛),所以现状是 CLI 单方面把一个本该响亮的失败按了下去。属 restore-invariant 类,不需要维护者拍板。PM 裁定:与
smtp分支对齐,缺 apiKey 直接抛,让 boot 失败。 不走「先升到 error、再按迁移窗口改抛」那条渐进路线,理由是你自己在建议里点出的那一条:log这个显式取值的存在,正是这条抛错成立的前提——想要「本环境不发信」的部署有一个说得出口的写法(OS_EMAIL_PROVIDER=log),不必靠「配了 provider 却不配 key」来表达。而渐进路线的代价是这段时间里服务器继续「每封都成功、一封都没发出」,那正是本单要消灭的状态。你提的兼容性顾虑(是否有 CI 环境只设
OS_EMAIL_PROVIDER而依赖降级启动)按这个口径处理:这不是需要事先普查的阻塞项,而是 changeset 里要写清楚的破坏性变更——迁移动作只有一句「把OS_EMAIL_PROVIDER=resend改成=log,或补上OS_EMAIL_API_KEY」,成本极低,而继续静默的代价是用户报「收不到验证码」才暴露。派发时会要求错误消息同时给出后果与修复,并把serve-email-capability.test.ts里钉当前降级行为的那条用例一并翻面。排队,不与 PR #5133 同批(同属邮件装配路径,且 #5133 正在改
plugin-email/transports/index.ts的 provider 集合,本单要读那个集合)。
Generated by Claude Code
认领:PM 循环第 4 轮
会话:session_017MCKJaEomEqg4tvz4SzdNd
分支:claude/issue-5132-cli-email-provider-loud
Worktree:objectstack-issue-5132
域:domain:cli
文件面:packages/cli/src/commands/serve.ts(resolveEmailCapabilityArg一带)、packages/cli/src/commands/serve-email-capability.test.ts(越界即停,报告说明)前置条件已满足:#5094 的 PR #5133 已合入 main(
8597a7d79),EMAIL_TRANSPORT_PROVIDERS现在是 provider 集合的单一真相,本单可以直接读它。裁定见上一条评论,不重复。在飞撞车检查:
domain:cli有 #5091 / #5092 在飞(#5040 executor 家族),但它们落在 runtime/hono 的端点派发面,与serve.ts的 capability 装配段不相交。仍要求你合并最新origin/main后再动手 —— 该文件今天已被多次改动。
Generated by Claude Code
复核结论:ACCEPT —— PR #5153,转 ready 并入合并队列。CI 在 head
420d0865(已合并最新 main)上 7 个工作流全绿。落地的正是裁定要求的形状
resolveEmailCapabilityArg缺 apiKey 不再改写 provider + warning 启动,而是与紧邻的smtp分支同形直接抛;错误消息同时给出后果(每封都记为 sent、一封不出门)与修复(设OS_EMAIL_API_KEY,或显式OS_EMAIL_PROVIDER=log)。docstring 也重写了,把「refusing is only defensible because『本环境不发信』有说得出口的写法」这条前提写进代码,而不是只留在 issue 里。dev 找到了我没看见的第二份隐含列表
我只要求「复用 #5133 的
EMAIL_TRANSPORT_PROVIDERS,不要在 CLI 里再手写 provider 字面量」。dev 照做了,并且发现还有一份:「哪些 provider 需要 API key」这个判断此前硬编码在 CLI 的provider !== 'log' && provider !== 'smtp'里 —— 同样是一份会漂移的隐含清单,只是它编码的是「谁要 key」而不是「谁存在」。它把这条提成 plugin-email 的
API_KEY_EMAIL_PROVIDERS/emailProviderRequiresApiKey,并让makeTransport的 resend/postmark 分支经由requireApiKey(provider: ApiKeyEmailProvider, …)取值 —— 删常量即编译不过,反方向由新增的api-key-providers.contract.test.ts钉住(断言它是EMAIL_TRANSPORT_PROVIDERS的子集、缺 key 时建构真的抛、且emailProviderRequiresApiKey('sendgrid') === false)。这是 #5094 那条「两份字面量可以靠改另一份『修好』」的教训被主动应用到了一个我没点名的地方。makeTransport对外文案逐字不变,所以这层重构没有把行为改动混进来。一处删除我特意核过
EmailCapabilityArg.warning整个通道被删了。它只承载过一条消息(「falling back to LogTransport」),而那条降级现在是抛错。我确认过serve.ts里已无.warning消费者 —— 是删干净,不是删一半留个悬空字段。changeset 按要求写清了 breaking:受影响人群 + 一行迁移;
environment-variables.mdx的OS_EMAIL_API_KEY也标了 Required。顺带确认的搜索先行
dev 路过又看到
EmailProviderSchema缺'smtp',查到 #5104 已在跟,没有开孪生单。#5104 仍在排队等 spec 车道(#5114 / #5011 / #4964 / #4001 四单在飞)。
Generated by Claude Code
- added a commit that references this issue
on Aug 4, 2026
做 #5094 时读
resolveEmailCapabilityArg路过发现,与该 issue 无关,单独记录。#5087 的 PR 已经在代码注释里点名了这一处「still does」,但看起来没有单独立单。现状
packages/cli/src/commands/serve.ts的resolveEmailCapabilityArg(约 2900 行):即:
OS_EMAIL_PROVIDER=resend(或postmark)而没有配OS_EMAIL_API_KEY时,CLI 把 provider 改写成log,打一条warning,然后正常启动。同一函数里紧邻的
smtp分支是相反的处理 ——provider='smtp'无 host 直接throw,让 boot 失败,并且函数自己的 docstring 明确写了为什么:影响
运维显式声明了要用 resend/postmark 投递,得到的却是一台「每次 send 都成功、
sys_email里全是sent、一封信也没出去」的服务器。启动横幅之后那条 warning 很容易被 CI 日志淹没,而后果要等到用户报「收不到验证码」才暴露 —— 正是 AGENTS.md degradation-log-level 一节说的「系统从外面看一切正常」那一类。插件层(
EmailServicePlugin.resolveTransport/makeTransport)在 #5087 之后已经是「建不出就抛」,所以这条降级是 CLI 单方面把一个本该响亮的失败按下去了。建议
与
smtp分支对齐:缺 apiKey 时抛,让 boot 失败,错误里同时给出后果与修复(设OS_EMAIL_API_KEY,或显式改成OS_EMAIL_PROVIDER=log以表明「本环境不发信」)。log这个显式取值的存在正是这条抛错的前提 —— 想要「不发信」的部署有一个说得出口的写法,不需要靠「配了 provider 但不配 key」来表达。需要一并确认(可能构成兼容性变更,因此没有直接顺手改):是否有既有部署/示例依赖这条降级来启动(例如只设了
OS_EMAIL_PROVIDER的 CI 环境)。若有,至少应把日志级别从warning提到error并补上后果与修复两段,再按迁移窗口改成抛。packages/cli/src/commands/serve-email-capability.test.ts已经有一条noKey = resolveEmailCapabilityArg({}, { OS_EMAIL_PROVIDER: 'postmark' })的用例在钉当前行为,改的时候要一起翻。