diff --git a/content/docs/configure/encryption.mdx b/content/docs/configure/encryption.mdx index c642243..25a355e 100644 --- a/content/docs/configure/encryption.mdx +++ b/content/docs/configure/encryption.mdx @@ -1,92 +1,50 @@ --- title: 数据加密 -description: 加密你的 Mix Space +description: v17 凭据加密配置、原密钥保留与 v18 准备 --- -Mix Space 提供了敏感配置的加密存储能力。默认为关。 +本文固定已发布的 Core v17.0.4。未发布的 v18 开发分支已删除关闭加密和旧格式写入,不能沿用下文的 v17 逃生口,详见 [v18 准备说明](/docs/migrate/v18-preparation)。 -为什么需要 Key 加密。 +## 三种秘密分别保管 -假设数据库被脱库。如果开启了 Key 加密,即便攻击者拿到了全部数据,也读不出被标记为加密的字段。同时,也需要谨慎开启此功能——开启后你需要记住加密密钥,否则你将永远丢失这些数据。 +| 秘密 | 用途 | 恢复时的要求 | +| --- | --- | --- | +| `MX_ENCRYPT_KEY` / `ENCRYPT_KEY` | 配置、Snippet、Webhook 的字段加密 | 保留产生这些密文的原密钥 | +| `JWT_SECRET` | 登录与 Better Auth OAuth token 加密 | 保留原值;轮换需要维护窗口 | +| 备份恢复私钥 | 打开 `.mxbackup` 归档 | 与备份分开保存;不能替代上面两种秘密 | -开启前请务必备份数据库。忘记密钥 = 这些数据永久无法恢复。 +已有实例不要为解决解密错误而生成新密钥。新密钥无法解开旧数据。先从部署配置、秘密管理器或历史快照找回原值;丢失原秘密时,需要重新授权或重新配置相应凭据。 -## 哪些字段会被加密 +## v17 如何开启 -加密范围不是全部配置项,而是由服务端 schema 上的 `encrypt` 元数据决定,**随版本变化**。 - -判定规则:服务端遍历配置 schema 时,会读每个字段的 `encrypt` 元数据;**父级被标记时,整棵子树都继承加密**。绝大多数密码类字段是通过 `field.password()` / `field.passwordHalfGrid()` 定义的,这两个 helper 会在构造 schema 时自动注入 `encrypt: true`,所以你在配置里看到的「密码」输入框,基本都属于加密范围。 - -当前版本(v14.15.2)覆盖的主要字段: - -| 区域 | 字段 | -| --- | --- | -| OAuth | 各服务商的 client secret、Apple 私钥 | -| 图床 / 对象存储 | S3、R2 的 `secretKey` | -| 邮件 | Resend API Key、SMTP 密码 | -| AI | 各 Provider 的 `apiKey` | -| Webhook | 签名密钥 | -| 搜索推送 | 百度推送 Token、Bing API key | -| 会员 | API Key | -| 其他 | 高德地图 Key、设备 Key、Personal Access Token 等 | - - -想确认当前版本的准确清单,可以看服务端 `apps/core/src/modules/configs/` 下的 `configs.schema.ts`:凡是写成 `field.password(...)` 或 `field.passwordHalfGrid(...)` 的字段都会被加密,另加显式标注 `{ encrypt: true }` 的字段。文档这里的说明可能落后于代码,以源码为准。 - - -## 如何开启 - -你可以附加 `--encrypt_enable` 来启动服务。如: +为从未加密过的新实例生成随机字段密钥,并保存到秘密管理器: ```bash -node out/main.mjs --encrypt_enable +openssl rand -hex 32 ``` - -**只要设置了加密密钥而没有显式关闭,加密就会被自动开启。** - -判定逻辑是`ENCRYPT_ENABLE` 未设置时,回落到「是否设置了密钥」这一项。也就是说,你只填了密钥、没管开关,加密就已经生效了。如果暂时只想把密钥填进配置、暂不加密,必须显式写 `ENCRYPT_ENABLE=false`。 - +将结果配置为 `MX_ENCRYPT_KEY`(优先)或 `ENCRYPT_KEY`,同时在 app 和 mx-migrate 的共享环境中启用 `MX_ENCRYPT_ENABLE=true`。开启时必须显式提供 **64 位十六进制**密钥;长度不足、非 hex、仅提供开启开关都会在启动时失败。不要依赖 machine-id 回退,也不要使用示例公共密钥。 -## 指定密钥 +设置密钥且未指定开关时会自动开启加密。命令行 `--encrypt_key` / `--encrypt_enable` 仍可使用;升级工具使用环境变量,不读取业务进程的 CLI/YAML 参数。 -可以通过 `--encrypt_key ` 指定,也可以用环境变量。环境变量有两名,`MX_ENCRYPT_KEY` 优先于 `ENCRYPT_KEY`: +## V2 写入与历史数据 -```bash -# 推荐 -export MX_ENCRYPT_KEY=<64 位密钥> -# 等价写法 -export ENCRYPT_KEY=<64 位密钥> -``` +`MX_SECRET_STORAGE_V2=true` 要求已开启字段加密,且 `JWT_SECRET` 至少 32 字符。它为配置、Snippet 和 Webhook 使用认证加密,为 API Key 使用哈希,为 OAuth access/refresh token 使用 Better Auth 加密。配置的密码字段由 schema 的 `encrypt` 元数据决定,数据库和 Redis 中均保留密文;空秘密 PATCH 保留旧值。 -密钥的校验分两层,容易搞混: +V2 字段密文绑定字段路径;不能把密文直接搬到另一个字段。旧明文和 ECB 保留读取与迁移能力。`ENCRYPT_ALGORITHM` 默认 `aes-256-ecb`,供旧格式使用;V2 固定使用 AES-256-GCM。 -| 情况 | 行为 | -|------|------| -| 提供的密钥**不是 64 位** | 启动即报错 | -| 提供的密钥**短于 64 位** | 不报错,服务端会用 `sha256(key + 'mx-encrypt-salt')` 派生并补齐到 64 位 | -| 提供的密钥**长于 64 位** | 启动即报错 | -| 提供的密钥**不是合法 hex 字符** | **启动时不校验**,直到真正加解密时才抛出 `Invalid key length` | +未发布的 v18 开发分支已删除 `ENCRYPT_ALGORITHM`、`--encrypt_algorithm` 和 YAML `encrypt_algorithm`,新写入固定 AES-256-GCM。旧项即使填默认值也会报错,应删除;历史读取固定为 AES-256-ECB。使用其他历史算法的实例须先在旧版本用原密钥和原算法转换为 V2 并验证,再升级;详见 [v18 准备说明](/docs/migrate/v18-preparation)。 -推荐用 `openssl rand -hex 32` 生成,产出的正是 64 位小写 hex,一次通过全部校验: - -```bash -openssl rand -hex 32 -``` +滚动部署时,先让全部副本具备新格式读取能力,再在全部副本启用 V2,最后回填旧数据。旧 `migrate:secrets` 完成记录仅代表该次凭据回填,不代表已完成未来 v18 的配置与指标准备。新准备工具尚未发布,见 [准备流程](/docs/migrate/v18-preparation)。 -加密算法可用 `ENCRYPT_ALGORITHM` 调整,默认 `aes-256-ecb`。 +## v17.0.4 临时明文逃生口 - - 不指定密钥时,服务端默认取机器的 - [machine-id](https://www.npmjs.com/package/node-machine-id) 并派生。 - +只有关闭字段加密且 `MX_SECRET_STORAGE_V2=false` 时,普通配置写入可继续保留明文。它不适用于已有密文,也不适用于明确要求加密的授权功能。关闭开关不会解密旧数据;遇到已有密文仍须开启加密并提供原密钥。 - - 关闭加密开关**不会**把已经写入的密文还原成明文。此前加密过的字段会继续以密文形式存储和返回,密文带有 `$${mx}$$` 标识前缀。如果你打算关闭加密,需要先准备好原密钥完成解密。 - +此逃生口仅适用于已发布的 v17.0.4。未发布的 v18 开发分支已经移除它,生产要求显式字段密钥、至少 32 字符的 JWT,并固定 V2 写入;旧关闭配置须删除。源码 dev 未配置字段密钥时可自动生成并保存在 `tmp/dev-encryption.key`,重启复用;开发库和该文件须一起保留,详见 [v18 开发环境说明](/docs/migrate/v18-preparation#源码开发环境)。 -## 疑难解答 +## 解密失败时 -如果出现 `Invalid key length`,说明你的密钥**不是合法的 hex 字符串**(长度问题在启动阶段就会被拦下,不会走到这里)。请用 `openssl rand -hex 32` 重新生成一个 64 位小写 hex 字符串。 +检查密钥来源、别名优先级、原 JWT 和密文所属路径。错误密钥、损坏密文或未知格式会失败,不会回落为明文成功。备份解封装成功也不表示归档内凭据能解密,详见 [备份与恢复](/docs/use/backup-restore)。 diff --git a/content/docs/configure/environment.mdx b/content/docs/configure/environment.mdx index 1d2c9af..a6e583c 100644 --- a/content/docs/configure/environment.mdx +++ b/content/docs/configure/environment.mdx @@ -7,6 +7,8 @@ description: 所有环境变量的完整说明 如果你不确定某个变量的作用,保持默认值即可。 +以下默认行为固定已发布的 Core v17.0.4。未发布的 v18 开发分支已强制字段加密和 V2 写入,生产环境须提供显式字段密钥,源码 dev 可自动生成并持久保存本地密钥;JWT 至少 32 字符,旧关闭配置须删除,见 [v18 准备说明](/docs/migrate/v18-preparation)。 + 以下环境变量适用于 Mix Space Core 后端服务。Docker 用户在 `docker-compose.yml` 顶部的 `x-mx-env` 锚点中设置;源码用户在 `.env` 或 `ecosystem.config.cjs` 中设置。 @@ -17,8 +19,8 @@ description: 所有环境变量的完整说明 | 变量名 | 说明 | 默认值 | 示例 | | --- | --- | --- | --- | -| `JWT_SECRET` | JWT 签名密钥。**运行时不做长度校验**,建议 16-32 个字符。未设置时 token 会在重启后全部失效 | - | `my-secret-key` | -| `JWTSECRET` | `JWT_SECRET` 的别名,两者都设置时以 `JWT_SECRET` 为准 | - | `my-secret-key` | +| `JWT_SECRET` | 登录与 OAuth token 加密秘密。启用 V2 时至少 32 字符;已有实例保留原值,短 JWT 用维护轮换处理 | - | `原实例秘密或新生成的随机值` | +| `JWTSECRET` | `JWT_SECRET` 的别名,两者都设置时以 `JWT_SECRET` 为准 | - | `原实例秘密或新生成的随机值` | | `JWT_EXPIRE` | 登录令牌有效期(天) | `14` | `30` | | `ALLOWED_ORIGINS` | 允许的跨域域名。**自建站必须显式设置**,否则你的域名不在白名单内 | 内置默认白名单(`innei.ren`、`localhost:*`、`*.dev`、`*.vercel.app` 等) | `example.com,www.example.com` | | `SNOWFLAKE_WORKER_ID` | 工作节点 ID,取值 `0-1023` 的整数。**生产环境不设置会直接启动失败**,单实例填 `1` | - | `1` | @@ -45,9 +47,9 @@ cluster 模式下可安全使用多实例:服务端会基于 `NODE_APP_INSTANC | 变量名 | 说明 | 默认值 | 示例 | | --- | --- | --- | --- | -| `REDIS_URL` | 完整连接串,**优先级最高**,会覆盖下面三项。支持 `redis://` / `rediss://`(`rediss` 自动启用 TLS),path 里的 `/0` 表示 db 序号 | - | `redis://:pass@host:6379/0` | -| `REDIS_CONNECTION` | `REDIS_URL` 的别名 | - | - | -| `REDIS_CONNECTION_STRING` | `REDIS_URL` 的别名,优先级最高 | - | - | +| `REDIS_URL` | 完整连接串,优先级低于 `REDIS_CONNECTION_STRING`、`REDIS_CONNECTION`。支持 `redis://` / `rediss://`(`rediss` 自动启用 TLS),path 里的 `/0` 表示 db 序号 | - | `redis://:pass@host:6379/0` | +| `REDIS_CONNECTION` | 完整连接串,优先于 `REDIS_URL` | - | - | +| `REDIS_CONNECTION_STRING` | 完整连接串,优先级最高 | - | - | | `REDIS_HOST` | 地址 | `localhost` | `redis` | | `REDIS_PORT` | 端口 | `6379` | `6379` | | `REDIS_PASSWORD` | 密码 | - | `secret` | @@ -57,16 +59,20 @@ cluster 模式下可安全使用多实例:服务端会基于 `NODE_APP_INSTANC | 变量名 | 说明 | 默认值 | 示例 | | --- | --- | --- | --- | | `ENCRYPT_ENABLE` | 启用加密 | `false`(⚠️ 只要设置了密钥就等价于 `true`)| `true` | -| `ENCRYPT_KEY` | 加密密钥,必须正好 64 位 | 自动取 machine-id 派生 | `openssl rand -hex 32` | +| `ENCRYPT_KEY` | 开启时必须显式提供 64 位十六进制字段密钥 | 无可用默认密钥 | `openssl rand -hex 32` | | `MX_ENCRYPT_KEY` | `ENCRYPT_KEY` 的别名,**优先于** `ENCRYPT_KEY` | 同上 | - | | `MX_ENCRYPT_ENABLE` | `ENCRYPT_ENABLE` 的别名,**优先于** `ENCRYPT_ENABLE` | 同上 | - | -| `ENCRYPT_ALGORITHM` | 加密算法 | `aes-256-ecb` | `aes-256-cbc` | +| `ENCRYPT_ALGORITHM` | v17 旧格式算法;V2 固定 AES-256-GCM。v18 已删除此项,保留会报错 | `aes-256-ecb`(仅 v17) | `aes-256-ecb`(仅 v17) | +| `MX_SECRET_STORAGE_V2` | V2 凭据写入;要求启用加密、JWT 至少 32 字符,全部副本兼容后才启用 | `false` | `true` | +| `MX_MIGRATE_SECRETS` | 旧凭据回填的一次性迁移开关,不放入常驻 app 环境;不等于 v18 准备完成 | 未开启 | `1` | | `ADMIN_UPDATE_S3_BASE_URL` | 后台更新包(`latest.json` + `admin-.zip`)的下载源 | `https://admin-r2.innei.dev` | - | `ENCRYPT_ENABLE` 的判定逻辑是「未设置时,回落到是否设置了密钥」。**只填密钥、不管开关,加密就已经开启。** 想暂不加密必须显式写 `ENCRYPT_ENABLE=false`。详见 [数据加密](/docs/configure/encryption)。 +当前 v17.0.4 保留关闭加密的临时明文逃生口,不能读取已有密文或绕过明确要求加密的功能。未发布的 v18 开发分支已实现强制加密,新准备工具也尚未发布,见 [v18 准备说明](/docs/migrate/v18-preparation)。 + ## 缓存与限流 | 变量名 | 说明 | 默认值 | 示例 | diff --git a/content/docs/deploy/agent.mdx b/content/docs/deploy/agent.mdx index bfe4931..c591926 100644 --- a/content/docs/deploy/agent.mdx +++ b/content/docs/deploy/agent.mdx @@ -156,9 +156,9 @@ Agent 可以帮你执行命令行操作和调用 API,但涉及敏感信息( 你是 Mix Space 部署专家。请 SSH 到我的服务器(IP: ___,用户: root), 帮我完成以下操作: 1. 安装 Docker 和 Docker Compose -2. 下载 Mix Space 的 docker-compose.yml -3. 设置 JWT_SECRET 为随机字符串,ALLOWED_ORIGINS 为我的域名 -4. 启动所有服务 +2. 固定同一个已发布版本的 docker-compose.yml 和 app/mx-migrate 镜像,先阅读大版本迁移说明 +3. 新装生成并保存至少 32 字符的 JWT;启用加密时保存 hex64 字段密钥,ALLOWED_ORIGINS 为我的域名;已有实例保留原秘密,短 JWT 需要维护轮换 +4. 为 app/mx-migrate 配置相同环境,发布前迁移成功后再启动业务副本;v18 准备工具尚未发布,不能在 v17.0.4 上调用 5. 配置 Nginx 反向代理,将 ___ 域名指向 localhost:2333 6. 用 Certbot 申请 SSL 证书 7. 验证 https://___ 可以正常访问 diff --git a/content/docs/deploy/docker.mdx b/content/docs/deploy/docker.mdx index f3b50b1..dda1c54 100644 --- a/content/docs/deploy/docker.mdx +++ b/content/docs/deploy/docker.mdx @@ -36,17 +36,19 @@ docker-compose -v ## 拉取编排文件 [step] -自 v12 起 Core 已从 MongoDB 迁移到 PostgreSQL,编排文件中不再有 mongo 服务。当前版本(v14)的 `docker-compose.yml` 由 `app`、`mx-migrate`、`postgres`、`redis` 四个服务组成。 +自 v12 起 Core 已从 MongoDB 迁移到 PostgreSQL,编排文件中不再有 mongo 服务。当前 v17 的 `docker-compose.yml` 由 `app`、`mx-migrate`、`postgres`、`redis` 四个服务组成。 ```bash cd && mkdir -p mx-space/core && cd $_ # 拉取 docker-compose.yml 文件(全球) -wget https://fastly.jsdelivr.net/gh/mx-space/core@master/docker-compose.yml +wget https://fastly.jsdelivr.net/gh/mx-space/core@v17.0.4/docker-compose.yml # 拉取 docker-compose.yml 文件(Gh-Proxy 全球) -wget "https://gh-proxy.org/https://github.com/mx-space/core/blob/master/docker-compose.yml" +wget "https://gh-proxy.org/https://github.com/mx-space/core/blob/v17.0.4/docker-compose.yml" ``` +本文固定已发布的 v17.0.4 编排。下载后把 app 和 mx-migrate 两处镜像都固定为 `innei/mx-server:17.0.4`,不要让一方使用 latest。未来准备工具尚未包含在该镜像,见 [v18 准备说明](/docs/migrate/v18-preparation)。 + ## 配置环境变量 [step] 我们需要正确配置环境变量,打开获取到的`docker-compose.yml`文件。 @@ -71,15 +73,15 @@ x-mx-env: &mx-env ``` -`SNOWFLAKE_WORKER_ID` 是必填项(`0-1023` 的整数),生产环境缺失会导致 mx-core 直接启动失败。官方文件里的默认值是 `'1'`。另外 `mx-migrate` 也会引用同一个 `*mx-env` 块并跑完整的 Nest 上下文,所以这里改了迁移步骤也会同步生效——**不要只给 `app` 服务单独加变量**。 +`SNOWFLAKE_WORKER_ID` 是必填项(`0-1023` 的整数),生产环境缺失会导致 mx-core 直接启动失败。官方文件里的默认值是 `'1'`。另外 `mx-migrate` 也会引用同一个 `*mx-env` 块执行独立发布前迁移,所以这里改了迁移步骤也会同步生效——**不要只给 `app` 服务单独加变量**。 ### 配置生成 -在下方的组件中填入配置,点击复制后粘贴到 `docker-compose.yml` 的 `x-mx-env` 块中,覆盖 `JWT_SECRET` 和 `ALLOWED_ORIGINS` 的值,如果需要开启加密功能,请添加并将 `ENCRYPT_ENABLE` 的值改为 `true`,并填入 `ENCRYPT_KEY`。 +在下方的组件中填入配置,点击复制后粘贴到 `docker-compose.yml` 的 `x-mx-env` 块中,覆盖 `JWT_SECRET` 和 `ALLOWED_ORIGINS` 的值,如果需要开启加密功能,请添加并将 `ENCRYPT_ENABLE` 的值改为 `true`,并填入原 `ENCRYPT_KEY`;从未加密的新实例才用 `openssl rand -hex 32` 生成并持久保存。复制内容是 YAML 映射,所有值按字符串引用,美元符号按 Compose 规则转义。 -注意:只要设置了 `ENCRYPT_KEY`(或 `MX_ENCRYPT_KEY`)而没有显式设 `ENCRYPT_ENABLE=false`,加密就会被**隐式开启**。如果暂时只想把密钥填进去而暂不加密,必须显式写 `ENCRYPT_ENABLE: false`。 +注意:只要设置了 `ENCRYPT_KEY`(或 `MX_ENCRYPT_KEY`)而没有显式设 `ENCRYPT_ENABLE=false`,加密就会被**隐式开启**。如果暂时只想把密钥填进去而暂不加密,必须显式写 `ENCRYPT_ENABLE: 'false'`。 @@ -112,7 +114,7 @@ x-mx-env: &mx-env type={{ 'JWT 密钥': { description: - '用于签发用户 JWT,务必保存好不要泄露。运行时不做长度校验,但建议使用 16-32 个字符。未设置时 token 会在服务重启后全部失效。', + '用于签发用户 JWT,务必保存好不要泄露。V2 要求至少 32 字符;已有 JWT 不要直接更换,短值需要维护轮换。', type: 'string', }, '被允许的域名': { @@ -127,12 +129,14 @@ x-mx-env: &mx-env }, '加密密钥': { description: - '开启加密后必填,必须正好 64 位,否则启动即报错。推荐用 openssl rand -hex 32 生成(产出 64 位小写 hex)。未提供时服务端会用 machine-id 派生。此操作不可逆,请谨慎。', + '开启加密后必填,必须正好 64 位,否则启动即报错。推荐用 openssl rand -hex 32 生成(产出 64 位小写 hex)。必须显式配置,不依赖 machine-id。已有实例保留原密钥,新实例随机生成并单独保存。', type: 'string', }, }} /> +开启 V2 前确认全部副本支持新格式,JWT 至少 32 字符,再在共享块设置 `MX_SECRET_STORAGE_V2: 'true'`。历史实例还需要凭据回填,不能只切开关。 + ### 启动服务 ```bash diff --git a/content/docs/deploy/source.mdx b/content/docs/deploy/source.mdx index acec278..b8f0e30 100644 --- a/content/docs/deploy/source.mdx +++ b/content/docs/deploy/source.mdx @@ -11,7 +11,7 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'; ## 要求 -- [Node.js](https://nodejs.org/zh-cn/) 22.12+(v14.7.0 起源码部署的最低要求) +- [Node.js](https://nodejs.org/zh-cn/) 26+(v15 起要求;Node.js 22、24 不再支持) - [PostgreSQL](https://www.postgresql.org/download/) 16+ - [Redis](https://redis.io/download) - [Git](https://git-scm.com/downloads)、[PNPM](https://pnpm.io/installation)、[PM2](https://pm2.keymetrics.io/docs/usage/quick-start/) @@ -22,8 +22,10 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'; ### 克隆并安装 ```bash -git clone https://github.com/mx-space/core.git --depth=1 +git clone https://github.com/mx-space/core.git --branch v17.0.4 --depth=1 cd core +npm install -g corepack +corepack enable pnpm i ``` @@ -33,7 +35,6 @@ pnpm i ```bash pnpm build -pnpm bundle ``` @@ -71,8 +72,8 @@ module.exports = { NODE_PATH: nodePath, // 必填:允许跨域的域名,多个用逗号分隔 ALLOWED_ORIGINS: 'your-domain.com', - // 必填:JWT 密钥。未设置时 token 会在重启后失效(建议 16-32 个字符,运行时不做长度校验) - JWT_SECRET: 'your-jwt-secret', + // 必填:JWT 密钥。未设置时 token 会在重启后失效(V2 要求至少 32 字符;已有实例保留原值) + JWT_SECRET: 'REPLACE_WITH_YOUR_SAVED_INSTANCE_SECRET', // 必填:Snowflake worker ID,整数 0-1023。生产环境不设置会直接启动失败。 // cluster 模式可安全使用多实例:core 会基于 NODE_APP_INSTANCE 为每个实例分配不重叠的 ID SNOWFLAKE_WORKER_ID: '1', @@ -87,7 +88,15 @@ module.exports = { -### 启动 +### 先迁移,再启动 + +先为独立迁移进程配置同样的数据库、Redis、字段密钥、JWT 等环境变量(例如 `apps/core/.env`),不要只放在 PM2 的 env 对象里。已有秘密保留,新装随机生成并持久保存。仓库根运行: + +```bash +pnpm -C apps/core migrate +``` + +确认迁移退出成功,再进入 `apps/core`: ```bash pm2 start ecosystem.config.cjs diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx index 8aca279..6515e9f 100644 --- a/content/docs/getting-started/quick-start.mdx +++ b/content/docs/getting-started/quick-start.mdx @@ -5,7 +5,7 @@ title: 5 分钟快速体验 import { Callout } from 'fumadocs-ui/components/callout' import { Step, Steps } from 'fumadocs-ui/components/steps' -本指南的目标是在 **5 分钟** 内让你在本地运行起一个完整的 Mix Space,**无需配置域名,无需填写环境变量**(全部使用默认值)。 +本指南的目标是在 **5 分钟** 内让你在本地运行起一个完整的 Mix Space,无需公网域名。本文以 v17.0.4 为本地体验基线;公开部署前需要保存自己的 JWT 与字段密钥,见 [Docker 部署](/docs/deploy/docker)。v18 尚未发布,未来会要求显式加密配置,见 [准备说明](/docs/migrate/v18-preparation)。 @@ -22,8 +22,9 @@ import { Step, Steps } from 'fumadocs-ui/components/steps' 执行以下命令克隆后端仓库并启动服务: ```bash - git clone https://github.com/mx-space/core.git --depth=1 + git clone https://github.com/mx-space/core.git --branch v17.0.4 --depth=1 cd core + # 将 compose 中 app / mx-migrate 两处镜像固定为 innei/mx-server:17.0.4 docker compose up -d ``` diff --git a/content/docs/migrate/index.mdx b/content/docs/migrate/index.mdx index f31f256..3a592d8 100644 --- a/content/docs/migrate/index.mdx +++ b/content/docs/migrate/index.mdx @@ -8,6 +8,11 @@ import { Card, Cards } from 'fumadocs-ui/components/card' import { ArrowUp, Globe } from 'lucide-react' + 准备工具、凭据转换和发布边界 + Agent 历史与 WebSocket 协议 + BCP 47 语言标签 + Node.js 26 运行时 + } href={'/docs/migrate/v13-to-v14'} diff --git a/content/docs/migrate/meta.json b/content/docs/migrate/meta.json index ba75587..dd7e129 100644 --- a/content/docs/migrate/meta.json +++ b/content/docs/migrate/meta.json @@ -4,6 +4,10 @@ "root": true, "pages": [ "index", + "v18-preparation", + "v16-to-v17", + "v15-to-v16", + "v14-to-v15", "v13-to-v14", "v12-to-v13", "v11-to-v12", diff --git a/content/docs/migrate/v14-to-v15.mdx b/content/docs/migrate/v14-to-v15.mdx new file mode 100644 index 0000000..02a0590 --- /dev/null +++ b/content/docs/migrate/v14-to-v15.mdx @@ -0,0 +1,21 @@ +--- +title: v14 → v15 +description: Node.js 26 与发布包运行时要求 +--- + +v15 将 Core、构建工具和发布包的最低运行时统一为 Node.js 26。此变更已经发布,不属于 v18 的新增破坏。 + +## 升级前 + +- 保存数据库、文件与原秘密的快照。 +- 官方 Docker 镜像已经包含 Node.js 26,主机无需另装 Node。 +- 源码、自定义镜像、Railway/nixpacks 及运行 CLI 的主机必须先升级到 Node.js 26;Node.js 22、24 不再受支持。 +- Node.js 26 不再内置 Corepack,需自行安装,再按仓库 `packageManager` 选择 pnpm。`@mx-space/api-client`、CLI、MongoDB 迁移 CLI 和 Webhook 包也声明 Node.js 26 要求。 + +固定目标版本的镜像和编排文件,运行发布前 `mx-migrate`,成功后启动 app。源码部署见 [部署步骤](/docs/deploy/source)。 + +## 验证与回退 + +确认 `node --version`、迁移退出状态、`/api/v3/ping` 和后台登录。需要回退时使用原版本代码和兼容的数据库;涉及数据恢复时同时恢复原文件和秘密,不能仅替换镜像。 + +[官方 v15.0.0 发布说明](https://github.com/mx-space/core/releases/tag/v15.0.0) diff --git a/content/docs/migrate/v15-to-v16.mdx b/content/docs/migrate/v15-to-v16.mdx new file mode 100644 index 0000000..41225b9 --- /dev/null +++ b/content/docs/migrate/v15-to-v16.mdx @@ -0,0 +1,22 @@ +--- +title: v15 → v16 +description: 语言代码切换为 BCP 47 标签 +--- + +v16 的存储、AI 设置与 API 输出统一使用 `zh-CN`、`zh-TW`、`en-US`、`ja-JP` 等 BCP 47 标签。繁体中文可以独立作为翻译目标。 + +## 受影响的调用 + +旧的 `zh`、`en`、`ja` 应迁为相应语言标签;`jp`、`cn`、`english` 等别名不再接受。`tw` 表示阿坎语,不能代替 `zh-TW`。客户端比较 `sourceLang`、`availableTranslations` 和 `targetLang` 时,应按语言与文字系统匹配,避免只比较旧的短代码。 + +`@mx-space/ai` 1.x 移除了 `normalizeLanguageCode`、`normalizeTargetLang`,改用 `canonicalizeLanguageTag`、`assertLanguageTag`、`matchLanguage`。Core 与适配后的 Yohaku 网页、移动端一起部署;其他主题先确认支持这些标签。 + +## 迁移与验证 + +备份后让发布前迁移 `0047_language_tag_bcp47` 完成,再切换业务副本。同一文章的新旧标签冲突时保留最近写入的记录;无法映射的值保持原样,并输出 `RAISE NOTICE`,需要检查处理。 + +验证翻译目标、语言列表、摘要、精读、搜索和朗读。`en-GB` 可匹配 `en-US`,`zh-HK` 可匹配 `zh-TW`。没有繁体摘要时不再用简体摘要替代。 + +迁移没有 down;只回退代码不会恢复旧标签或被合并的行。需要完整回退数据时使用升级前快照和原秘密。 + +[官方 v16.0.0 发布说明](https://github.com/mx-space/core/releases/tag/v16.0.0) diff --git a/content/docs/migrate/v16-to-v17.mdx b/content/docs/migrate/v16-to-v17.mdx new file mode 100644 index 0000000..b61eec0 --- /dev/null +++ b/content/docs/migrate/v16-to-v17.mdx @@ -0,0 +1,23 @@ +--- +title: v16 → v17 +description: 服务端 Agent 会话与新 WebSocket 协议 +--- + +v17 将编辑器 AI Agent 的完整 transcript 放到服务端,后台与服务端改用新的 WebSocket 消息形状。 + + +发布迁移会重建 Agent 会话表,清空升级前的 Agent 历史对话。旧对话无法带入新协议。此项属于 v17,不能在 v18 升级说明中再次当作新破坏。 + + +## 操作顺序 + +1. 备份数据库、文件与原秘密,确认可以接受旧 Agent 对话清空。 +2. 固定 v17 的镜像与匹配编排,让发布前 `mx-migrate` 完成。 +3. 一起部署 Core 和同版本内置 Admin,刷新原先打开的后台标签页;独立旧 Admin 不兼容新 Agent 协议。 +4. 验证登录、新建 Agent 对话、重新打开历史、工具调用和修改建议恢复。 + +文章、评论等业务内容不属于该会话表清空范围。回退到旧 Agent 协议需要升级前完整快照;只回退镜像不能找回已清空的对话。 + +v17.0.4 修复了关闭加密时普通配置写入失败的兼容问题;这是临时逃生口,见 [数据加密](/docs/configure/encryption)。下一阶段见 [v18 准备](/docs/migrate/v18-preparation)。 + +[官方 v17.0.0 发布说明](https://github.com/mx-space/core/releases/tag/v17.0.0) diff --git a/content/docs/migrate/v18-preparation.mdx b/content/docs/migrate/v18-preparation.mdx new file mode 100644 index 0000000..dc079db --- /dev/null +++ b/content/docs/migrate/v18-preparation.mdx @@ -0,0 +1,96 @@ +--- +title: v18 准备(未发布) +description: 本开发分支已强制加密,部署前检查凭据、配置和 AI 指标 +--- + + +v18 尚未发布。本开发分支已移除非加密运行和旧格式写入,生产启动必须提供显式字段密钥与至少 32 字符的 JWT。源码 dev 可自动生成本地字段密钥,见下方说明。本文的 `upgrade:*` 工具尚未发布,已发布的 v17.0.4 不包含它们。最低准备版 tag、SDK、CLI 和主题版本会在实际发布验证后补齐;不要直接在现有镜像上运行不存在的命令。 + + +## 计划改变什么 + +| 范围 | 当前 v17 | v18 计划 | +| --- | --- | --- | +| 凭据写入 | v17.0.4 可分阶段启用 V2,有临时明文逃生口 | **本开发分支已生效**:生产显式 hex64 字段密钥、dev 可自动生成本地密钥;JWT 至少 32 字符、固定 V2;旧关闭配置报错 | +| 普通摘要、精读 GET | 可能隐式生成 | 只读,无结果 `data: null`;生成显式触发 | +| 非流式执行入口 | 旧 GET 可用 | 备份、清缓存、测试邮件与速记表态用 POST;GET SSE 保留 | +| 配置与参数 | v17 保留旧配置、排序与别名兼容 | **本分支已生效**:发布前迁移旧配置,正常服务只认新结构,PATCH 拒绝未知声明字段;数字排序和评论别名移除仍在后续阶段 | +| AI 元数据 | 旧响应字段保留 | 统一 generationMetrics,后续才删除旧列与旧响应字段 | +| SDK | 5.x 兼容版先转换请求 | 协调 SDK 6.x;保留 legacy 入口与 CLI V2 响应适配 | + +加密强制、旧配置迁移和配置严格校验已在本分支实现;其余最终破坏仍分阶段推进。当前继续支持旧 GET 和 SDK 5.x 签名,不改变普通 AI GET 的既有行为,不删除物理列。API `/api/v3`、snake_case、成功/错误 envelope 和 SSE 帧协议保持。 + +## 准备顺序 + +1. 更早的实例先按历史指南升级到兼容的 v17,保存数据库、文件、原字段密钥、原 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`。独立升级检查仍读取环境配置;检查开发库或将它迁到生产前,须显式配置原本地密钥,不能重新生成。 + +源码工作区中的命令(需要本分支代码和已完成 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 或凭据迁移开关。配置迁移持有数据库锁,与正常配置 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 参数仍有效。 + +数据库缺少配置迁移记录时,新服务拒绝初始化配置并提示运行发布迁移。转换器只保留在发布迁移与只读检查中,供历史数据升级;旧归档仍按下方隔离恢复流程处理。 + +规范凭据为空时保留已有旧凭据;新旧值都非空且不同时,报告 `CONFIG_SECRET_CONFLICT` 与字段路径,配置迁移整批回滚。先在旧实例或隔离维护环境核对并明确解决冲突,再重跑迁移;不要尝试启动新服务后通过旧字段 PATCH 修复。错误密钥、损坏密文和不支持的配置形状同样阻塞迁移,不写完成记录。 + +## 维护窗口轮换 JWT + +先停止全部业务副本、后台任务和其他写入者。将原 JWT 和至少 32 字符的新 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。旧 `githubToken` 和 `smtp.options` 调用须改为嵌套 GitHub 配置与平铺 SMTP 字段;这是配套大版本契约,不能将该类型删除作为 SDK 5.x patch 发布。 diff --git a/content/docs/use/backup-restore.mdx b/content/docs/use/backup-restore.mdx index e25126a..276cc3e 100644 --- a/content/docs/use/backup-restore.mdx +++ b/content/docs/use/backup-restore.mdx @@ -1,64 +1,38 @@ --- title: 备份与恢复 -description: 保护你的 Mix Space 数据,学会备份与回滚 +description: ZIP、加密归档、原秘密与恢复边界 --- import { Callout } from 'fumadocs-ui/components/callout' -## 手动备份 +## 创建与保存 -在后台后台的「备份」页面中点击「立即备份」,系统会将数据打包为 zip 文件,保存到服务器的绝对目录: +后台「备份」页可以立即备份并下载。未启用归档加密时生成 ZIP;在「设置 → 存储 → 备份」启用备份加密后,新归档为 `.mxbackup`。定时备份、S3 上传和恢复前安全快照沿用该设置。字段加密与归档加密是两项独立设置,未来字段加密必填不等于所有备份必须加密。 -``` -~/.mx-space/backup/YYYY-MM-DD_HH:mm:ss/backup-YYYY-MM-DD_HH:mm:ss.zip -``` +启用时浏览器生成公私钥对,只把公钥提交给服务端;私有恢复密钥只展示一次,务必复制或下载后单独保管。需要 HTTPS 或 localhost 的 Web Crypto 环境。服务器配置中的公钥不能用来恢复,旧归档仍需要创建时对应的私钥。 - -数据目录固定为 `~/.mx-space`(开发模式下为项目下的 `./tmp`),不随部署路径变化。目录名与文件名的时间戳格式都是 `YYYY-MM-DD_HH:mm:ss`——注意其中**包含冒号**,这是 Linux/macOS 允许、Windows 不允许的。 + +备份私钥、字段加密密钥与 JWT 分别保存。归档打开后,其中的加密凭据仍需要当时的原字段密钥和 JWT。不要把恢复私钥与备份一起放在数据目录。 -建议定期手动下载备份包到本地,以防服务器故障导致数据丢失。 - -## 自动备份 - -目前该功能仅支持备份到支持 S3 协议的云服务,暂不支持其他类型的云服务。当然,你也可以通过设置计划任务等方法达到和此功能一样的效果。 - -设置入口:后台「设置 → 存储 → 备份」,填入云服务所提供的对应参数即可。 - ## 恢复 - -该操作涉及修改数据库,请多备份几个工作日的 backup,数据无价请谨慎操作! - -一般情况下回滚只会导致 analyze 数据页丢失 IP & PV 的数据。 - - -服务端提供了两条现成的恢复路径,**都不需要手工重命名备份包**: +上传已有 ZIP / `.mxbackup`,或选择服务器上的备份回滚,都无需手工改名。加密归档在恢复对话框粘贴私钥或导入 PEM 文件;它只随本次认证请求发送,不保存到服务器设置。旧 ZIP 不需要归档密钥,早期 v1 对称加密归档输入原 64 位十六进制密钥。 -1. **上传备份包恢复** —— 在后台后台的「备份」页面中上传你手上的 zip 备份包即可,服务端会先把它落到临时目录,再直接执行恢复。 -2. **按目录名回滚** —— 如果你想回到某次已有备份,直接指定该备份的目录名即可。 +服务端先验证整个加密归档,再解包和恢复数据库。缺少/错误私钥、损坏归档会失败;不要重新生成密钥冒充恢复。恢复需要 `pg_dump`、`pg_restore`、`psql`,官方镜像包含客户端工具。 -恢复操作开始前,服务端会**自动先做一次新备份**,所以回滚本身也是可逆的。回滚会导致 analyze 数据页丢失 IP 与 PV 数据,其余内容(文章、评论等)不受影响。操作前仍建议多备份几个工作日的包。 +恢复会覆盖数据库业务内容和数据文件。操作前服务端尝试创建安全快照,仍应额外保存可恢复的外部副本。会话、日志、搜索索引等运行时表按当前结构重建为空表。数据库替换在单个事务内,文件随后复制,二者不是同一个原子事务;文件复制失败时保留快照处理,不应报告整次恢复成功。 -替换完成后会提示「数据库有变动,将在 x 秒后重载页面」,重载后请先检查文章、评论等有没有丢失。 - -## 命令行备份(v12+) +恢复后核对文章、评论、登录、凭据消费及文件。跨版本恢复须遵循对应迁移指南。未来 v18 的历史恢复应走「隔离恢复 → migrate → check → 验证 → v18」路径,见 [v18 准备说明](/docs/migrate/v18-preparation)。当前没有自动隔离预检,不要恢复后直接开放未来 v18 业务。 -自 v12 起,Mix Space 使用 PostgreSQL 作为数据库。服务端自身的备份用的是 `pg_dump --format=custom`(custom 格式),你手工备份时建议保持一致: +## 手工数据库快照 -```bash -# 备份(custom 格式,dump 落在 mx-space/pg.dump) -pg_dump -Fc postgresql://mx:mx@localhost:5432/mx_core > pg.dump -``` - -恢复时用 `pg_restore`,并加上服务端同样使用的清理参数: +服务端使用 PostgreSQL custom 格式。手工备份可以使用: ```bash -pg_restore --clean --if-exists -d postgresql://mx:mx@localhost:5432/mx_core pg.dump +pg_dump -Fc "$PG_URL" > pg.dump ``` - -`--clean --if-exists` 会在恢复前删除已存在的同名对象。如果你要的是纯 SQL 文本备份,用 `pg_dump` 不加 `-Fc`,恢复时改用 `psql < backup.sql`。 - +该文件仅是数据库快照,不包含数据目录、秘密管理器或恢复私钥。请在隔离数据库中演练恢复;不要将生产库作为首次验证目标。 diff --git a/content/docs/use/update.mdx b/content/docs/use/update.mdx index 2d8d9a8..86e0211 100644 --- a/content/docs/use/update.mdx +++ b/content/docs/use/update.mdx @@ -28,10 +28,10 @@ docker compose pull && docker compose up -d ```bash pnpm i pnpm build -pnpm bundle +pnpm -C apps/core migrate ``` -随后进入 `./apps/core`,执行以下命令启动服务: +先确认迁移成功,并为迁移进程提供与 app 相同的数据库、字段密钥、JWT 等环境变量。随后进入 `./apps/core` 启动服务: ```bash pnpm prod @@ -50,6 +50,11 @@ pm2 start ecosystem.config.cjs + 准备工具、凭据转换和发布边界 + Agent 历史与 WebSocket 协议 + BCP 47 语言标签 + Node.js 26 运行时 + { - const lines = variableNames.map((v, i) => - format === 'yaml' ? `- ${v.key}=${values[i]}` : `${v.key}=${values[i]}`, - ); - navigator.clipboard.writeText(lines.join('\n')).then(() => { + navigator.clipboard.writeText(formatEnvVariables(variableNames, values, format)).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); diff --git a/src/components/env-variable-format.mts b/src/components/env-variable-format.mts new file mode 100644 index 0000000..8a81cca --- /dev/null +++ b/src/components/env-variable-format.mts @@ -0,0 +1,13 @@ +/** Compose environment mappings contain strings; dollar escaping prevents interpolation. */ +export function formatEnvVariables( + variables: ReadonlyArray<{ key: string }>, + values: readonly string[], + format: 'yaml' | 'env', +): string { + return variables.map((variable, index) => { + const value = values[index] ?? ''; + return format === 'yaml' + ? `${variable.key}: ${JSON.stringify(value.replaceAll('$', () => '$$'))}` + : `${variable.key}=${value}`; + }).join('\n'); +} diff --git a/tests/env-variable-format.test.mjs b/tests/env-variable-format.test.mjs new file mode 100644 index 0000000..cd4e67f --- /dev/null +++ b/tests/env-variable-format.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import test from 'node:test'; +import { parse } from 'yaml'; +import { formatEnvVariables } from '../src/components/env-variable-format.mts'; + +test('YAML output is a string-valued mapping with quoted special values', () => { + const values = ['false', '00123', 'a: b # value', 'quote" slash\\\nnext line']; + const variables = values.map((_, index) => ({ key: `KEY_${index}` })); + const result = parse(formatEnvVariables(variables, values, 'yaml')); + assert.equal(Array.isArray(result), false); + values.forEach((value, index) => assert.equal(result[`KEY_${index}`], value)); +}); + +test('Compose preserves literal dollar signs in copied secrets', { skip: spawnSync('docker', ['image', 'inspect', 'alpine:latest']).status !== 0 }, () => { + const environment = formatEnvVariables([{ key: 'JWT_SECRET' }], ['literal$secret${UNSET}'], 'yaml'); + const compose = `services:\n app:\n image: alpine:latest\n pull_policy: never\n network_mode: none\n environment:\n${environment.split('\n').map((line) => ` ${line}`).join('\n')}\n`; + const result = execFileSync('docker', ['compose', '-f', '-', 'run', '--rm', '--no-deps', 'app', 'printenv', 'JWT_SECRET'], { input: compose, encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, COMPOSE_PROJECT_NAME: `mx-docs-test-${process.pid}` } }); + assert.equal(result.trim(), 'literal$secret${UNSET}'); +}); + +test('existing env output and caller values remain unchanged', () => { + const values = Object.freeze(['test-value']); + assert.equal(formatEnvVariables([{ key: 'KEY' }], values, 'env'), 'KEY=test-value'); +});