Skip to content

cli: config.email.defaultTemplateContext.appName 压过 OS_APP_NAME —— 与「env 逐项覆盖」的声明相反 #5448

Description

@os-zhuang

实施 #5307 时实测 resolveEmailCapabilityArg 的模板上下文解析顺序,发现一处与文档声明相反的优先级。#5307 的 PR 只把实测到的现状写进 TSDoc 并用测试钉住,没有改行为——改不改是契约决定,故单独立单。

实测(origin/main @ ed0d2aa)

packages/cli/src/commands/serve.ts,resolveEmailCapabilityArg 内:

const defaultTemplateContext = {
  appName: env.OS_APP_NAME || cfgEmail.appName || configAppName || 'ObjectStack',
  ...(cfgEmail.defaultTemplateContext || {}),
};

appName 先算,defaultTemplateContext 再整体展开覆盖在它上面。于是:

来源 胜负
config.email.defaultTemplateContext.appName 赢
OS_APP_NAME(环境变量) 输
config.email.appName 输
顶层 config.appName 输

复现(无需起服务):

resolveEmailCapabilityArg(
  { appName: 'From The Key', defaultTemplateContext: { appName: 'From The Context' } },
  { OS_APP_NAME: 'From The Env' },
).options.defaultTemplateContext.appName
// => 'From The Context'

为什么是问题

EmailServiceConfigSchema 的头部 TSDoc(也是生成文档 content/docs/references/system/email-config.mdx 的开头)写的是:

Resolution order in serve.ts:
  1. config.email.* from objectstack.config.ts
  2. OS_EMAIL_* environment variables (override per setting)
  3. Default -> provider='log'

「环境变量逐项覆盖配置」是这个文件对所有键的承诺,apiKey / defaultFrom / retries / queueDelivery / SMTP 一族都照此执行。只有 appName 这一项,一旦作者同时写了 defaultTemplateContext.appName,env 就失效——而且是静默失效。

具体后果:同一份 objectstack.config.ts 部署到多环境,运维在生产设 OS_APP_NAME=Acme 覆盖仓库里写死的 defaultTemplateContext: { appName: 'Acme Dev' },发出去的品牌邮件仍然叫 Acme Dev。连带地,没有配 defaultFrom 时的兜底发件人也是从这个值 slug 化来的(no-reply@acme-dev.local),所以错的不止是正文。

两种读法(需要维护者定)

  • A:现状即正确 —— 「显式完整上下文」比「便捷单键 + env」更具体,应该赢。那就该在 TSDoc 里把 appName 标为 env 规则的显式例外(spec: EmailServiceConfigSchema 未声明 CLI 实读的 queueDelivery / appName / defaultTemplateContext(与 #5104 同族,不同键) #5307 的 PR 已按这个读法把现状写清楚了),不改代码。

  • B:env 必须赢 —— 与文件声明的解析顺序一致,也与其余所有键一致。改法是把 env/键的解析结果放在展开之后:

    const defaultTemplateContext = {
      ...(cfgEmail.defaultTemplateContext || {}),
      appName: env.OS_APP_NAME || cfgEmail.appName
        || (cfgEmail.defaultTemplateContext || {}).appName || configAppName || 'ObjectStack',
    };

    注意这里要显式把 defaultTemplateContext.appName 插进链条(排在两个专用来源之后、顶层 configAppName 之前),否则从 B 改过去会把「只写了 defaultTemplateContext.appName、没写 appName」的既有配置直接降级到 'ObjectStack' —— 修一个静默错值换来另一个,更糟。

倾向 B:一份配置多环境部署时 env 覆盖是运维的唯一手段,而当前行为让这个手段在一个键上无声失效;例外规则(A)要求作者读到 TSDoc 的那一段才知道,属于「必须记住才不会踩」的设计。但 B 会改变既有部署的可观察行为(user-visible,需要 changeset),不是能顺手带的改动。

关联

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    分诊(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,2026-08-05):入队 pm:queue,domain:cli。判级:真缺陷 —— 文件自述的解析顺序(「env 逐项覆盖」)对其余全部键成立,唯独 appName 静默失效,多环境部署时运维唯一手段无声作废。

    方向裁定(留否决窗口):取 B(env 必须赢) —— 判据不是口味而是既有契约:EmailServiceConfigSchema TSDoc 与生成文档把解析顺序写成承诺,其余所有键照此执行,B 是恢复该成文不变量;A 的「例外规则」属于必须读到 TSDoc 那段才不踩的设计(防 AI 轴反面)。已记录的接受代价:依赖 context-压-env 现状的既有部署行为会变(user-visible,changeset 写明)—— 维护者不接受此代价可否决改 A(届时只改 TSDoc 把例外标明)。执行细节照正文 B 的陷阱注记:defaultTemplateContext.appName 显式插入链条(两个专用来源之后、顶层 configAppName 之前),防止把「只写了 context.appName」的配置降级到 'ObjectStack';#5307 钉现状的那条 contract 测试连同 schema TSDoc 一起改。

    ⛔ 串行约束(step 3 记录):与 #5447 同函数,#5447(enforce persist,PII 优先)先行,本单待其 MERGED 后基于新 main 派发。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 8 轮(cli 车道)
    会话:session_016FNvXhtSdnEGEfLEsMmvxh
    分支:claude/issue-5448-appname-env-wins
    Worktree:objectstack-issue-5448
    域:domain:cli
    文件面:packages/cli/src/commands/serve.ts(resolveEmailCapabilityArg 的 defaultTemplateContext 解析)+ packages/cli 测试(含 #5307 分支的 contract 测试若已合入则同步改)。串行约束已解除:PR #5470(#5447)已 MERGED cd2efe62a。与在飞 #5224(rest/runtime 面)不同文件;#5462 继续缓派。

    执行口径沿 12:3xZ 分诊裁定:方向 B(env 必须赢),链条按正文陷阱注记显式插入 defaultTemplateContext.appName(两个专用来源之后、顶层 configAppName 之前),防「只写 context.appName」的配置降级;changeset 写明行为变更;#5307 钉现状的测试与 schema TSDoc 若在 main 则一并改。


    Generated by Claude Code

  4. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    验收(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5498。

    CI 核后转 ready 挂 auto-merge,跟到 MERGED。


    Generated by Claude Code

  5. added a commit that references this issue on Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions