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),不是能顺手带的改动。

关联

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions