# v17 → v18 (/docs/migrate/v18-preparation)

<Callout type="warn">
Core v18 自 18.0.1 起可用，18.0.0 未发布镜像。v18 已移除非加密运行和旧格式写入，生产启动必须提供显式字段密钥与至少 32 字符的 JWT。源码 dev 可自动生成本地字段密钥，见下方说明。Core v17.1.0 起提供 `upgrade:check`、`upgrade:prepare` 与 `upgrade:rotate-jwt`，v17.0.5 及更早版本不包含它们。v18 构建保留 check 与 rotate-jwt，回填并入发布迁移。请从 v17.1.0 升级；SDK、CLI 和主题的最低版本会在实际发布验证后补齐。
</Callout>

## 计划改变什么

| 范围 | 当前 v17 | v18 计划 |
| --- | --- | --- |
| 凭据写入 | v17.0.4 可分阶段启用 V2，有临时明文逃生口 | **v18 已生效**：生产显式 hex64 字段密钥、dev 可自动生成本地密钥；JWT 至少 32 字符、固定 V2；旧关闭配置报错 |
| 普通摘要、精读 GET | 可能隐式生成 | 只读，无结果 `data: null`；生成显式触发 |
| 非流式执行入口 | 旧 GET 可用 | 备份、清缓存、测试邮件与速记表态用 POST；GET SSE 保留 |
| 配置与参数 | v17 保留旧配置、排序与别名兼容 | **v18 已生效**：发布前迁移旧配置，正常服务只认新结构，PATCH 拒绝未知声明字段；数字排序和评论别名移除仍在后续阶段 |
| AI 元数据 | 旧响应字段保留 | 统一 generationMetrics，后续才删除旧列与旧响应字段 |
| SDK | 5.x 兼容版先转换请求 | 协调 SDK 6.x；保留 legacy 入口与 CLI V2 响应适配 |

加密强制、旧配置迁移和配置严格校验已在 v18 实现；其余最终破坏仍分阶段推进。当前继续支持旧 GET 和 SDK 5.x 签名，不改变普通 AI GET 的既有行为，不删除物理列。API `/api/v3`、snake_case、成功/错误 envelope 和 SSE 帧协议保持。

## 准备顺序

1. 更早的实例先按历史指南升级到 v17.1.0，保存数据库、文件、原字段密钥、原 JWT 和备份私钥。
2. 取得包含工具的构建，先不要启动业务副本。独立工具读取 `PG_URL` / `PG_CONNECTION_STRING` 或 `PG_*`、字段密钥和 JWT；业务进程用 CLI/YAML 配置时，也要提供等效环境变量。先用 check 诊断现有配置。
3. JWT 不足 32 字符时先执行下方维护轮换。不要直接改环境变量，否则现有 OAuth token 可能无法解密。
4. 删除旧加密开关，保存原有效字段密钥与符合要求的 JWT。**先停止全部旧副本和后台写入者**，再使用本构建运行 `node migrate.mjs`（源码为 `pnpm -C apps/core migrate`）。配置转换是必跑的发布前数据迁移，无需额外开关；失败阻止新服务启动，成功才记录 `20261109-canonical-config`。这一步需要维护窗口，不能让旧版在迁移后继续写回旧结构。
5. 同一个 migrate 还会回填 AI 指标，并必跑 `20261109-required-secret-storage`，完成 Snippet/Webhook 加密、API Key 哈希和 OAuth 加密。迁移成功后运行 check，再启动新业务副本，验证原 API Key、OAuth、配置消费与 AI 指标。Compose 中 app 与 mx-migrate 必须使用同一构建与秘密。

### 启动配置与旧开关

- 生产环境的 `MX_ENCRYPT_KEY` 或 `ENCRYPT_KEY` 必须是显式的 64 位十六进制密钥；不再回退到机器 ID，也不再派生短密钥。只有从未加密的新实例才用 `openssl rand -hex 32` 生成首个字段密钥并持久保存。
- `JWT_SECRET`（别名 `JWTSECRET`）至少 32 字符；已有短 JWT 必须先按下方维护流程轮换。
- 旧开关的 `true` 值暂接受并提示删除；`false` 或其他无效值会阻止启动，check 报告 `DEPRECATED_ENCRYPTION_FLAG`。它们不再控制运行模式。
- 配置、Snippet 和 Webhook 新写入固定 AES-256-GCM，使用随机 96 位 nonce、128 位认证标签，并以 AAD 绑定所属字段；API Key 固定哈希，OAuth 固定加密。历史明文和默认 AES-256-ECB 仍可读取并迁移。
- **破坏性变更**：删除 `ENCRYPT_ALGORITHM`、命令行 `--encrypt_algorithm` 和 YAML `encrypt_algorithm`。保留旧项会报错，即使填旧默认值、`aes-256-gcm` 或空环境变量也不接受；应删除配置项。环境变量检查报告 `REMOVED_ENCRYPTION_OPTION`。
- app 与 mx-migrate 使用相同且持久保存的密钥。字段加密必填不影响 ZIP 备份和历史归档读取；备份加密仍由独立设置控制。

历史字段解密固定为旧默认 AES-256-ECB。曾使用其他旧算法的实例，必须先在旧版本中使用原密钥和原算法将相关凭据转换为 V2，并验证凭据可用后再升级。删除配置项不会转换自定义算法密文。原算法和密钥仍须随旧快照单独保管，以便在隔离的旧版本环境恢复。

### 源码开发环境

dev 未提供字段密钥时，首次启动自动生成随机密钥，以仅所有者可读写的 `0600` 权限保存到当前工作目录的 `tmp/dev-encryption.key`；重启和多个开发进程复用同一密钥。显式 CLI、YAML 或环境变量密钥始终优先。已有本地密钥损坏或显式密钥无效时会报错，不会自动覆盖。字段仍使用 AES-256-GCM 加密，JWT 长度要求不变。

本地密钥须和开发数据库一同保留；删除 `tmp` 也会删除它。生产构建不会生成或读取这个默认密钥，即使设置 `NODE_ENV=development`。独立升级检查仍读取环境配置；检查开发库或将它迁到生产前，须显式配置原本地密钥，不能重新生成。

源码工作区中的命令（需要 v18 代码和已完成 schema 迁移的数据库）：

```bash
pnpm -C apps/core upgrade:check --json
pnpm -C apps/core migrate
pnpm -C apps/core upgrade:check --json
```

构建产物中用 `node migrate.mjs` 运行正式迁移，用 `node upgrade.mjs check --json` 检查结果。在包含该入口的镜像中，可使用 `docker compose run --rm --no-deps app node upgrade.mjs check --json` 启动独立检查；数据库需已运行。该检查进程不启动 HTTP、业务定时任务或 Redis。Compose 会先校验 JWT 非空，字段密钥缺失由迁移进程报告；诊断缺失配置时使用源码命令或直接 `docker run`，避免先被 Compose 拦截。

`check` 退出码：`0` 为 ready，`2` 为 needs-action，`1` 为检查失败。它只读，输出计数和路径，不输出秘密。空库可识别为新装；历史实例须完成规范配置迁移并通过实际数据检查，没有单独准备标记。

所有回填都属于正式发布迁移，无需独立 prepare、dry-run 或凭据迁移开关。在 v17.1.0 上预先执行 `upgrade:prepare` 是可选的：已准备和未准备的实例由同一发布迁移处理，已转换的凭据不会被再次改写。配置迁移持有数据库锁，与正常配置 PATCH 串行；按原路径解密，再转换配置并按新路径加密。凭据迁移哈希旧 API Key、封装 OAuth 并加密 Snippet/Webhook，原 API Key 字符串保持有效。新的 v18 凭据迁移有独立账本 ID，历史 opt-in 凭据迁移记录不会使本次回填被跳过。

每条迁移成功后才记录完成，损坏密文、未知配置形状、错误字段密钥或 JWT 会使对应批次失败并回滚，阻断发布。先前成功的迁移保留账本，重试时自动跳过；重复执行不会重复哈希 API Key 或轮换已加密 token。迁移和检查完成后启动新副本，不清空所有 Redis 状态。

## 配置一次性迁移

发布迁移把 `githubToken` 转为 `github.token`，将 Dodo 凭据转为 `membership.apiKey` / `webhookSigningKey`，把独立图片生成和 TTS 配置并入 `ai.providers` 与功能配置，删除旧配置行。历史 SMTP 的 `smtp.options` 连接参数迁为 `smtp.host` / `port` / `secure`。凭据先按原字段路径解密，再按新路径加密，保留实际值。

普通启动、读取和 PATCH 不再转换旧结构，也不再返回独立 `imageGenerationOptions` / `ttsOptions` 分区。旧分区读写返回 `CONFIG_NOT_FOUND`；未知或旧字段返回 `CONFIG_VALIDATION_FAILED`，不会静默忽略。部分更新、空秘密保留原值，以及 record / 自由 JSON 参数仍有效。

长期运行的实例常残留已无任何 schema 声明的旧字段。发布迁移会删除这些字段，并在日志中按路径列出，不输出值；record 与自由 JSON 的动态键不受影响。需要保留这些值时，以升级前的数据库快照为准。

数据库缺少配置迁移记录时，新服务拒绝初始化配置并提示运行发布迁移。转换器只保留在发布迁移与只读检查中，供历史数据升级；旧归档仍按下方隔离恢复流程处理。

规范凭据为空时保留已有旧凭据；新旧值都非空且不同时，报告 `CONFIG_SECRET_CONFLICT` 与字段路径，配置迁移整批回滚。先在旧实例或隔离维护环境核对并明确解决冲突，再重跑迁移；不要尝试启动新服务后通过旧字段 PATCH 修复。错误密钥、损坏密文和不支持的配置形状同样阻塞迁移，不写完成记录。

## 维护窗口轮换 JWT

先停止全部业务副本、后台任务和其他写入者。将原 JWT 和至少 32 字符的新 JWT 放在两个仅操作者可读的文件中，不把秘密值放在命令行或聊天记录里。新 JWT 用随机源生成，下例产出 64 个字符；文件末尾的单个换行会被工具忽略，原 JWT 请用编辑器写入文件：

```bash
umask 077
openssl rand -hex 32 > /secure/new-jwt
```

随后执行轮换：

```bash
pnpm -C apps/core upgrade:rotate-jwt --maintenance-confirmed --old-jwt-file /secure/old-jwt --new-jwt-file /secure/new-jwt
```

构建产物对应 `node upgrade.mjs rotate-jwt` 加同样参数；容器以只读 volume 挂载这两个文件，再传容器内路径。工具只读取文件，不改写秘密文件或业务环境。

事务内用旧 JWT 解开 OAuth，再用新 JWT 重封装；保留 provider token，不联系服务商，不改字段密钥和 API Key。成功会清空登录会话，所有用户须重新登录。之后由操作者将 **全部副本** 配置为同一新 JWT，再启动。失败整批回滚；原 JWT 丢失时不得清空 token 伪装成功，应找回原秘密或重新授权。

历史 Better Auth 裸 hex 密文也会验证与转换。疑似该格式但验证失败的 token 会阻塞操作，需要核对原秘密或重新授权，不会当明文继续。

## 消费者与恢复边界

兼容 SDK 将速记表态改为 POST，并把数值排序转换成 `asc` / `desc`；已有 token 参数转为认证头。Admin 的备份、清缓存与测试邮件提前走 POST。尚未验证的客户端不能宣称兼容 v18。

回退最低版本须是经过实测的准备版，当前发布 tag 尚未确定。回退代码不会回退 schema；回到更老版本必须有旧快照和当时的秘密。

当前恢复工具没有 v18 隔离预检闭环。历史 ZIP 或 v1 加密归档应先在隔离维护环境中恢复，用归档当时的字段密钥、JWT、恢复私钥完成 migrate 和 check，再验证并安排未来 v18 切换；不要把未迁移数据直接交给 v18 业务。数据库恢复和文件恢复是两个阶段，保留恢复前快照。

配置字段类型已按新结构同步到 API Client 源码，将随 SDK 6 发布。旧 `githubToken` 和 `smtp.options` 调用须改为嵌套 GitHub 配置与平铺 SMTP 字段；这是配套大版本契约，不能将该类型删除作为 SDK 5.x patch 发布。