CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载本篇指南完整解析 Webiny 开源仓库中packages/api-mailer邮件模块的一次纯删除型重构将 SMTP 密码加密从 mailer 自带的异步Encryption抽象切换为webiny/api-core/features/encryption中由平台统一注册的共享加密能力并彻底移除WEBINY_API_MAILER_*全部环境变量含加密密钥与 SMTP 传输回退。读者读完将掌握本次重构的架构动机、分 5 个任务推进的实施路径、每条关键代码改动的准确形态以及升级后运维侧必须执行的迁移动作。依据文档2026-04-19-mailer-encryption-removal.md配套设计说明2026-04-19-mailer-encryption-removal-design.md。一、背景与目标为什么移除 mailer 自己的加密层1.1 旧架构的问题在重构之前packages/api-mailer内部维护了一套独立于平台之外的加密体系由以下文件构成本次全部删除src/domain/Encryption/abstractions.ts—— mailer 本地Encryption抽象encrypt/decrypt为异步签名src/features/Encryption/PasswordEncryption.ts—— 基于 Nodecrypto的 AES-256-GCM 实现src/features/Encryption/feature.ts—— 在createMailerContext中的 DI 注册src/features/Encryption/utils/password.ts—— 密码加解密工具src/features/Encryption/utils/secret.ts—— 读取WEBINY_API_MAILER_PASSWORD_SECRET的密钥读取器。与此同时src/features/MailerService/TransportFactory.ts中的getDefaultSettingsFromEnv()会在 KV 存储没有持久化设置时从WEBINY_API_MAILER_HOST / PORT / USER / PASSWORD / REPLY_TO / FROM环境变量回退读取 SMTP 传输配置。由此暴露三个问题加密能力重复实现。平台层webiny/api-core/features/encryption已提供统一加解密mailer 却各自为政密钥来源WEBINY_API_MAILER_PASSWORD_SECRET与平台密钥体系Infra.Encryption.Key割裂配置来源分裂。同一套 SMTP 设置既可以来自 KV 持久化存储又可以来自环境变量回退行为难以预测也违背设置只存一处的原则职责越界。GraphQLgetSettings解析器中存在getSecret()短路逻辑在没有密钥时直接抛错与平台加密的无密钥即明文透传语义冲突。1.2 目标形态删除 mailer 本地Encryption抽象与实现SaveSettings/GetSettings两个仓库改为依赖webiny/api-core/features/encryption/index.js删除MailerService的环境变量回退只要 KV 中没有持久化设置sendMail立即以NoSettingsConfiguredError失败删除WEBINY_API_MAILER_*全部 6 个环境变量HOST、USER、PASSWORD、REPLY_TO、FROM、PASSWORD_SECRET的类型声明。架构定性纯删除 依赖替换。由于 api-core 的Encryption已由ApiCoreFeature注册且该 Feature 在createMailerContext之前运行mailer 侧不需要新增任何注册只需把依赖指向平台 token 即可。1.3 接受的风险Non-goals设计文档明确列出三项不做的内容理解这些边界有助于避免误解可插拔传输抽象不在本次范围MailTransportFactory形状保持不变无密钥时拒绝保存不在本次范围平台Encryption在EncryptionPassphrase未设置时是无操作透传见 commite693256292意味着未配置密钥的项目会把 SMTP 密码明文写入 KV 存储该问题由后续 PR 通过把Infra.Encryption.Key设为敏感设置持久化的硬性要求来兜底不做向后兼容解密旧密文使用不同的盐且环境变量来源已被移除升级后无法用新实现解密旧数据运维需手动重新保存一次 SMTP 凭据详见第五节迁移说明。二、前置条件与工作纪律实施前必须满足的条件依赖就绪每个任务开始前确保仓库根目录至少执行过一次yarn /dev/null 21保证依赖是最新的提交策略按仓库 CLAUDE.md 约定提交由用户执行而非 Agent。每个任务以checkpoint 结束——停下、运行验证命令、向用户呈现 diff 摘要不要运行git commitTDD 说明这是删除型重构而非新行为模式是改代码 → 跑既有测试套件 → 确认全绿。除重写一个依赖环境变量传输配置的测试外不引入新测试Task 2 与 Task 3 各删除一个原本就skip的测试。三、分任务实施详解Task 1将SaveSettingsRepository与GetSettingsRepository切换到 api-coreEncryption为什么先做这一步这两个仓库是 mailer 侧仅有的本地Encryption消费方。切换后本地PasswordEncryption即成为不可达的死代码而旧测试仍能通过是因为beforeEach中仍设置着WEBINY_API_MAILER_PASSWORD_SECRETapi-core 加密忽略该变量并按无操作透传运行——对测试断言而言仍是合法的往返round-trip。改动一src/features/SaveSettings/SaveSettingsRepository.ts导入行由import { Encryption } from ~/domain/Encryption/abstractions.js;改为import { Encryption } from webiny/api-core/features/encryption/index.js;同时删除execute()内两处awaitencrypt/decrypt由异步变为同步改后方法体类型不变为async execute(input: SaveSettingsInput): SaveSettingsRepository.Return { // Check if settings exist const existingResult await this.keyValueStore.getTransportSettings(MAILER_TRANSPORT_SETTINGS); const existingSettings existingResult.isOk() ? existingResult.value : null; const transportSettings: PartialTransportSettings existingSettings ?? {}; // If updating and no password provided, keep the existing password let passwordToStore input.password || ; if (!input.password existingSettings) { passwordToStore this.encryption.decrypt(transportSettings.password || ); } // Encrypt password const encryptedPassword this.encryption.encrypt(passwordToStore); // Prepare data const data { host: input.host ?? transportSettings.host, port: input.port ?? transportSettings.port ?? DEFAULT_PORT, user: input.user ?? transportSettings.user, password: encryptedPassword, from: input.from ?? transportSettings.from, replyTo: input.replyTo ?? transportSettings.replyTo }; // Save settings const result await this.keyValueStore.set(MAILER_TRANSPORT_SETTINGS, data); if (result.isFail()) { return Result.fail(new SettingsPersistenceError(result.error)); } // Return without encrypted password const returnSettings: TransportSettings { ...data, password: // Dont return password }; return Result.ok(returnSettings); }构造器与createImplementation调用保持不变——Encryptiontoken 的形态一致仅接口层面同步/异步不同依赖装配无需变动。改动二src/features/GetSettings/GetSettingsRepository.ts同样的导入替换并删除get()中this.encryption.decrypt(...)前的await改后方法体为async get(): PromiseResultTransportSettings | null { const result await this.keyValueStore.getTransportSettings(MAILER_TRANSPORT_SETTINGS); if (result.isFail()) { return Result.ok(null); } const settings result.value; if (!settings) { return Result.ok(null); } // Decrypt password if present const password settings.password ? this.encryption.decrypt(String(settings.password)) : ; const transportSettings: TransportSettings { host: String(settings.host || ), port: Number(settings.port || 25), user: String(settings.user || ), password, from: String(settings.from || ), replyTo: settings.replyTo ? String(settings.replyTo) : undefined }; return Result.ok(transportSettings); }验证Step 3–4从仓库根目录执行yarn build -p webiny/api-mailer 21 | tail -30 yarn test packages/api-mailer 21 | tail -50构建成功预期若 TypeScript 报Encryption类型不匹配说明旧的本地异步签名仍在别处泄漏——用grep搜索~/domain/Encryption/abstractions.js的导入确认只有 Save/Get 两个仓库引用过它。测试应保持与改动前相同的绿色数量旧PasswordEncryption虽仍注册但已无处注入。Checkpoint向用户呈现 diff 摘要建议提交信息refactor(api-mailer): switch Save/Get settings repositories to api-core Encryption。Task 2删除本地加密 Feature、移除 GraphQLgetSecret短路、清理 env-var 测试为什么紧随其后Task 1 落地后本地PasswordEncryption及其工具函数即为死代码。删除它们必须与 GraphQL 解析器清理仍引用getSecret放在同一任务内否则构建直接断裂设置WEBINY_API_MAILER_PASSWORD_SECRET的测试写入也要同步剔除因为环境变量读取已不存在。Step 1src/index.ts移除注册删除导入import { EncryptionFeature } from ~/features/Encryption/feature.js;与createMailerContext内的EncryptionFeature.register(context.container);一行改后文件必须与以下完全一致import { createContextPlugin } from webiny/api; import { GetSettingsFeature } from ~/features/GetSettings/feature.js; import { SaveSettingsFeature } from ~/features/SaveSettings/feature.js; import { DummyTransportFeature } from ~/features/DummyTransport/feature.js; import { SmtpTransportFeature } from ~/features/SmtpTransport/feature.js; import { MailerServiceFeature } from ~/features/MailerService/feature.js; import { SendMailFeature } from ~/features/SendMail/feature.js; import { createSettingsGraphQL } from ~/graphql/settings.js; export { MailerService } from ./domain/MailerService/abstractions.js; export type { IMailerService, IMailerServiceErrors } from ./domain/MailerService/abstractions.js; export const createMailerContext () { return createContextPlugin(context { // Register all features DummyTransportFeature.register(context.container); SmtpTransportFeature.register(context.container); GetSettingsFeature.register(context.container); SaveSettingsFeature.register(context.container); MailerServiceFeature.register(context.container); SendMailFeature.register(context.container); }); }; export const createMailerGraphQL () { return createSettingsGraphQL(); };Step 2graphql/settings.ts移除getSecret短路删除导入import { getSecret } from ~/features/Encryption/utils/secret.js;并删除MailerQuery.getSettings解析器内这段逻辑// First check of encryption key is set! // If not, this function will throw an error. // TODO: refactor this to make more sense. getSecret();改后解析器为getSettings: async (_, __, context) { try { const getSettings context.container.resolve(GetSettingsUseCase); const result await getSettings.execute(); const settings result.value; // Remove password from response if (settings?.password) { // eslint-disable-next-line typescript-eslint/no-unused-vars const { password, ...settingsWithoutPassword } settings; return new Response(settingsWithoutPassword); } return new Response(settings); } catch (ex) { return new ErrorResponse(ex); } }文件其余部分不动。Step 3删除本地加密文件夹rm -rf packages/api-mailer/src/domain/Encryption rm -rf packages/api-mailer/src/features/Encryption验证ls packages/api-mailer/src/domain 21 ls packages/api-mailer/src/features 21预期domain/仅含MailerService、MailTransport、errors.tsfeatures/仅含DummyTransport、GetSettings、MailerService、SaveSettings、SendMail、SmtpTransport。Step 4package.json清空adio.ignore.src由adio: { ignore: { src: [ node:crypto ] } }改为adio: { ignore: { src: [] } }保留空的adio块而非整体删除——其他包沿用同样模式保持一致性可避免yarn adio配置层面的意外。Step 5/6清理测试中的 env-var 写入__tests__/settings.crud.test.ts删除整个beforeEach块beforeEach(() { delete process.env[WEBINY_API_MAILER_PASSWORD_SECRET]; });若beforeEach导入因此不再使用从import { describe, it, expect, beforeEach, vi } from vitest;中一并剔除得到import { describe, it, expect, vi } from vitest;。删除已 skip 的测试it.skip(should not be possible to get or save settings without secret, ...)连同其前的// TODO: bruno - the \catch block is no longer triggered注释。删除其余所有形如process.env.WEBINY_API_MAILER_PASSWORD_SECRET really secret secret;的行共 6 处每处为测试体首行单行。编辑后用grep -n WEBINY_API_MAILER_PASSWORD_SECRET packages/api-mailer/tests/settings.crud.test.ts 验证预期无输出。__tests__/settings.graphql.test.ts删除整个beforeEach块beforeEach(() { process.env.WEBINY_API_MAILER_PASSWORD_SECRET really secret secret; });同步清理beforeEach导入删除已 skip 的测试it.skip(should not be possible to get or save settings when no secret is available, ...)含// TODO: bruno - this test is no longer failing注释最后验证grep -n WEBINY_API_MAILER packages/api-mailer/__tests__/settings.graphql.test.ts无输出。Step 7构建与测试yarn build -p webiny/api-mailer 21 | tail -30 yarn test packages/api-mailer --testPathPatternsettings 21 | tail -50若 Vitest 配置不接受--testPathPattern则跑完整套件yarn test packages/api-mailer——此时transporter.crud.test.ts仍依赖 env-var 传输配置是正常的因为getDefaultSettingsFromEnv()尚未被改动Task 3 才动MailerService。Checkpoint建议提交信息refactor(api-mailer): remove local Encryption feature and WEBINY_API_MAILER_PASSWORD_SECRET。Task 3移除MailerService的 env-var 传输回退重写transporter.crud.test.ts为什么留到最后处理Task 2 刻意保留getDefaultSettingsFromEnv()是为了让transporter.crud.test.ts::should send an email无需改动即可继续通过本任务才删除回退并把那个唯一依赖 env 配置的测试改为通过SaveSettingsUseCase持久化设置。Step 1MailerService.ts去除回退删除导入import { getDefaultSettingsFromEnv } from ./TransportFactory.js;并让sendMail在无持久化设置时快速失败改后完整文件为import { Result } from webiny/feature/api; import { MailerService as Abstraction } from ~/domain/MailerService/abstractions.js; import { NoTransportAvailableError, NoSettingsConfiguredError, TransportSendError } from ~/domain/MailerService/errors.js; import { MailTransport, MailTransportFactory } from ~/domain/MailTransport/abstractions.js; import { GetSettingsRepository } from ../GetSettings/abstractions.js; import type { TransportSettings, TransportSendData } from ~/types.js; class MailerServiceImpl implements Abstraction.Interface { constructor( private getSettingsRepository: GetSettingsRepository.Interface, private transportFactories: MailTransportFactory.Interface[] ) {} async sendMailT any(data: TransportSendData): Abstraction.ReturnT { const result await this.getSettingsRepository.get(); const settings result.value; if (!settings) { return Result.fail(new NoSettingsConfiguredError()); } const transport await this.getTransport(settings); if (!transport) { return Result.fail(new NoTransportAvailableError()); } try { const response await transport.send(data); if (response.error) { return Result.fail(new TransportSendError(response.error)); } return Result.ok(response); } catch (error) { return Result.fail(new TransportSendError(error)); } } private async getTransport( settings: TransportSettings ): PromiseMailTransport.Interface | null { if (this.transportFactories.length 0) { return null; } const factory this.transportFactories[this.transportFactories.length - 1]; return factory.createTransport(settings); } } export const MailerService Abstraction.createImplementation({ implementation: MailerServiceImpl, dependencies: [GetSettingsRepository, [MailTransportFactory, { multiple: true }]] });Step 2删除TransportFactory.tsrm packages/api-mailer/src/features/MailerService/TransportFactory.tsStep 3重写transporter.crud.test.ts完整改后文件为注意SaveSettingsUseCase的导入与新增的persistTransportSettings辅助函数import { describe, it, expect, vi } from vitest; import { createContextHandler } from ./contextHandler; import { SendMailUseCase } from ~/features/SendMail/abstractions.js; import { SaveSettingsUseCase } from ~/features/SaveSettings/abstractions.js; import { TransportSendData } from ~/types; vi.mock(nodemailer, () { return { default: { createTransport: () { return { sendMail: async (params: TransportSendData) { return { envelope: envelope, messageId: 123, accepted: [params.to], rejected: [], pending: [], response: ok }; } }; } } }; }); const to [todummy-host.webiny]; const cc [ccdummy-host.webiny]; const bcc [bccdummy-host.webiny]; const from fromdummy-host.webiny; const replyTo replyTodummy-host.webiny; const subject Some dummy subject; const text Some dummy body; const html pSome dummy body/p; const persistTransportSettings async ( context: AwaitedReturnTypeReturnTypetypeof createContextHandler[handle] ) { const saveSettings context.container.resolve(SaveSettingsUseCase); const result await saveSettings.execute({ host: dummy-host.webiny, user: user, password: password, from, replyTo }); if (result.isFail()) { throw new Error( Failed to persist mailer settings for test setup: ${result.error.message} ); } }; describe(Mailer Transporter CRUD, () { const { handle } createContextHandler(); it(should throw error before sending because of missing to, async () { const context await handle(); const params: TransportSendData { to: [], cc, bcc, from, replyTo, subject, text, html }; const sendMail context.container.resolve(SendMailUseCase); const result await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe(Mailer/SendMail/Validation); }); it(should throw error before sending because of missing from, async () { const context await handle(); const params: TransportSendData { to, cc, bcc, from: , replyTo, subject, text, html }; const sendMail context.container.resolve(SendMailUseCase); const result await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe(Mailer/SendMail/Validation); }); it(should throw error before sending because of missing subject, async () { const context await handle(); const params: TransportSendData { to, cc, bcc, from, replyTo, subject: , text, html }; const sendMail context.container.resolve(SendMailUseCase); const result await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe(Mailer/SendMail/Validation); }); it(should throw error before sending because of missing both text and html, async () { const context await handle(); const params: TransportSendData { to, cc, bcc, from, replyTo, subject, text: , html: }; const sendMail context.container.resolve(SendMailUseCase); const result await sendMail.execute(params); expect(result.isFail()).toBe(true); expect(result.error.code).toBe(Mailer/SendMail/Validation); }); it(should send an email, async () { const context await handle(); await persistTransportSettings(context); const params: TransportSendData { to, cc, bcc, from, replyTo, subject, text, html }; const sendMail context.container.resolve(SendMailUseCase); const result await sendMail.execute(params); expect(result.isOk()).toBe(true); expect(result.value).toEqual({ result: ok, error: null }); }); });与旧版的关键差异beforeEach的 env-var 写入整个移除钩子整体消失beforeEach从vitest导入中剔除顶部新增SaveSettingsUseCase导入新增persistTransportSettings辅助函数在发送测试前保存设置should send an email测试在调用SendMailUseCase前先调用该辅助函数。Step 4构建与测试yarn build -p webiny/api-mailer 21 | tail -30 yarn test packages/api-mailer 21 | tail -50预期全绿。should send an email现在完整走通SaveSettings → GetSettings → MailerService → mocked nodemailer的真实往返链路。Checkpoint建议提交信息refactor(api-mailer): drop env-var transport fallback, require persisted settings。Task 4删除WEBINY_API_MAILER_*环境变量类型声明为什么最后单独处理已无任何代码读取这些变量。删除类型声明可防止未来误用并能在构建期暴露任何遗漏的引用。编辑typings/env/index.d.ts删除整个api-mailer块next分支上位于 65–73 行若文件被编辑过行号可能漂移/** * api-mailer */ WEBINY_API_MAILER_HOST?: string; WEBINY_API_MAILER_USER?: string; WEBINY_API_MAILER_PASSWORD?: string; WEBINY_API_MAILER_REPLY_TO?: string; WEBINY_API_MAILER_FROM?: string; WEBINY_API_MAILER_PASSWORD_SECRET?: string;文件其余声明保持不动。随后从仓库根目录验证grep -rn WEBINY_API_MAILER packages typings 21 | head -20预期无任何匹配若有残留则先修复再继续。最后构建并跑完整测试套件yarn build -p webiny/api-mailer 21 | tail -30 yarn test packages/api-mailer 21 | tail -50Checkpoint建议提交信息chore(typings): remove WEBINY_API_MAILER_* environment variables。Task 5最终提交前的完整预检按仓库 CLAUDE.md 强制要求每次提交前必须按此序列执行一遍以捕捉所有删除之后的 TS 配置漂移、依赖声明、格式、lint 与工作区依赖同步问题。# 1. 暂存变更 git add . # 2. 确保 yarn.lock 最新 yarn /dev/null 21 # 3. 重新生成各包 tsconfig node scripts/generateTsConfigsInPackages.js # 4. adio 依赖声明检查 yarn adio 21 | tail -20 # 5. 格式化 yarn prettier:fix /dev/null 21 # 6. Lint y eslint 21 | tail -30 # 7. 工作区依赖同步 yarn webiny sync-dependencies 21 | tail -20 # 8. 重新暂存 git add .关于 Step 4 的重要提示如果yarn adio将webiny/api-core标记为api-mailer的缺失依赖请注意当前状态——webiny/api-core在packages/api-mailer/package.json中仅列于devDependencies。此前这可以被接受因为 mailer 只导入 token视为 peer。Task 1 新增的webiny/api-core/features/encryption/index.js导入与既有的EventPublisher、IdentityContext、KeyValueStore导入属于完全相同的运行时 token 模式不应改变依赖状态若yarn adio不认同以其建议为准它是权威工具。Step 9 最终 checkpoint向用户展示git status输出并汇总各任务的提交信息建议若用户选择 squash 可直接复用refactor(api-mailer): drop local encryption, rely on api-core Encryption persisted settings * Swap SaveSettings/GetSettings repositories to webiny/api-core/features/encryption (sync encrypt/decrypt). * Delete local Encryption feature, domain, and password-secret env var. * Remove env-var transport fallback; sendMail now requires persisted settings. * Remove all WEBINY_API_MAILER_* env typings. * Tests updated: the transporter send test persists settings via SaveSettingsUseCase. Operators must re-save their mailer SMTP settings after this upgrade — the previously stored ciphertext is no longer decryptable with the new (shared) encryption.等待用户提交。四、成功验收标准全部任务落地后逐项核对packages/api-mailer/src/domain/Encryption/与packages/api-mailer/src/features/Encryption/已不存在SaveSettingsRepository与GetSettingsRepository依赖webiny/api-core/features/encryption并使用同步encrypt/decryptMailerService无环境变量回退未持久化设置时sendMail以NoSettingsConfiguredError快速失败graphql/settings.ts无getSecret导入、无短路、无关于加密的 TODOpackages/api-mailer/**与typings/env/index.d.ts中无任何WEBINY_API_MAILER_*残留packages/api-mailer下所有__tests__/*.ts在不设置任何WEBINY_API_MAILER_*环境变量的情况下全部通过yarn build -p webiny/api-mailer成功yarn adio通过。五、运维迁移说明升级后必读升级后 KV 存储Mailer/Settings/Transport中已存在的邮件设置将无法再用新实现解密。运维人员必须通过管理后台或 GraphQLmailer.saveSettings变更重新保存一次 SMTP 设置。以下环境变量不再被读取应从项目配置中移除WEBINY_API_MAILER_PASSWORD_SECRET、WEBINY_API_MAILER_HOST、WEBINY_API_MAILER_PORT、WEBINY_API_MAILER_USER、WEBINY_API_MAILER_PASSWORD、WEBINY_API_MAILER_REPLY_TO、WEBINY_API_MAILER_FROM。六、仓库现状对照重构已在主线落地阅读本文时可直接对照当前仓库源码验证结果注意本次重构之后主线又在此基础上继续演进引入了CodeMailerSettings代码源设置、secure端口安全字段、按传输名解析等新能力但本文的核心结论——本地Encryption已删除、依赖已切换到 api-core、WEBINY_API_MAILER_*已全部移除——依然成立共享加密 token 定义位于 abstractions.tsIEncryption当前接口为encrypt(value: string): Promisestring/decrypt(value: string): Promisestring由createAbstractionIEncryption(Encryption)创建配套实现与注册见同一目录下的EncryptionService.ts、feature.ts与index.ts。设计文档描述的是同步接口形态而主线演进后接口再次改为异步——实施任何调用时务必以当前接口签名为准相应地在仓库源码中两个仓库的实现都保留了await this.encryption.*mailer 两个仓库的当前实现SaveSettingsRepository.ts 与 GetSettingsRepository.ts 均从webiny/api-core/features/encryption/index.js导入Encryption依赖注入数组为[KeyValueStore, Encryption]与[KeyValueStore, Encryption, CodeMailerSettings]不再引用任何~/domain/Encryption路径MailerService当前形态MailerService.ts 中已无getDefaultSettingsFromEnv与TransportFactory导入sendMail经由resolveTransport()基于工厂名逐项匹配可用传输错误路径返回NoTransportAvailableError/TransportSendError/TransportCreateErrorenv 变量零残留对typings/env/index.d.ts与packages/api-mailer全目录搜索WEBINY_API_MAILER均无任何匹配——Task 4 的验收标准在主线已满足测试套件现状packages/api-mailer/__tests__/下已无settings.graphql.test.ts被后续演进合并/重命名现存settings.crud.test.ts、transporter.crud.test.ts、saveSettingsEvents.test.ts、settings.codeSource.test.ts、codeSettings.test.ts等不再设置任何WEBINY_API_MAILER_*环境变量且transporter.crud.test.ts已采用通过SaveSettingsUseCase持久化设置后再发邮件的写法——与本文 Task 3 的落地形态一致。七、结语本次重构是 Webiny 平台能力收敛、删除重复实现 的典型样例mailer 从自带加密、自带配置来源收敛为完全依赖webiny/api-core的共享Encryption与 KV 持久化设置配置只存一处、密钥只认平台一套。对开发者而言最有价值的可复用经验是删除型重构也可以像新增功能一样分任务推进、以 checkpoint 控制节奏、用既有测试套件兜底对运维而言升级后重新保存一次 SMTP 凭据是唯一必须的手动动作。赞分享CMS后端前端【免费下载链接】webiny-jsOpen-source, self-hosted CMS platform on AWS serverless (Lambda, DynamoDB, S3). TypeScript framework with multi-tenancy, lifecycle hooks, GraphQL API, and AI-assisted development via MCP server. Built for developers at large organizations.项目地址https://gitcode.com/gh_mirrors/we/webiny-js点击查看免费下载相关推荐Webiny Headless CMS 存储层重构移除废弃的 register*StorageOperations 包装器全面落地 DI Feature 注册模式Webiny Headless CMS 存储层重构移除废弃的 register StorageOperations 包装器全面落地 DI Feature 注CMS后端前端electron-builder Toolset 体系重构ToolsetCustom 自定义工具包、环境变量移除与 PlatformPackager.info 弃用electron builder Toolset 体系重构ToolsetCustom 自定义工具包、环境变量移除与 PlatformPackager.info构建工具桌面应用开发工具Sphinx 废弃 API 完全指南废弃策略、RemovedInSphinxXXWarning 机制与全量迁移清单Sphinx 废弃 API 完全指南废弃策略、RemovedInSphinxXXWarning 机制与全量迁移清单 本文是 Sphinx 扩展开发者与深度用户文档开发工具上一篇ERCF v2 开源项目使用与启动指南下一篇ERCF v2 项目使用与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考