# System Migration Development

> 为 FastGPT 新增、修改或审查自动系统升级脚本及其注册信息。涉及系统迁移、升级任务、启动迁移、migration registry、checkpoint、全量重跑、阻塞升级、进度或失败数据时使用；普通业务更新和未接入自动升级框架的一次性手工清洗不使用。

- Skill: `gabrielmoreira/system-migration-development` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/system-migration-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/system-migration-development/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/system-migration-development

---


# FastGPT 系统升级脚本开发规范

## 目标与边界

系统升级任务属于 App 部署生命周期能力。框架负责顺序、状态、lease、并发互斥、重试入口和最终状态；脚本只负责可重复执行的业务迁移、进度、checkpoint，以及与阻塞类型匹配的错误报告方式。

开始前先查看当前实现，不能凭本 Skill 猜测已经变化的 Context 或目录结构：

- `projects/app/src/migration/registry.ts`
- `projects/app/src/migration/runner.ts`
- `packages/global/migration/constants.ts`
- `packages/global/migration/schema.ts`
- `projects/app/src/migration/tasks/README.md`
- 注册表中最近一个任务及其测试

如果这些路径正在迁移，以仓库中 `systemMigrations` 和 `SystemMigrationContext` 的实际定义为准。迁移执行框架和任务实现收敛到 `projects/app/src/migration`；需要被 App 前后端共同使用的状态枚举、Zod Schema 和 API 类型放在 `packages/global/migration`。

## 编写前必须确认

先明确以下契约；存在会改变数据安全或启动行为的缺失信息时，向用户确认后再编码：

1. 永久稳定的任务 ID，格式为 `YYYYMMDD_short_semantic_name`，以及首次发布版本。
2. 源数据、目标数据、权威数据源和迁移完成条件。
3. 该任务为什么必须排在当前注册表末尾，以及它依赖哪些前置任务。
4. 是否阻塞启动，以及失败后应停止还是继续后续任务；只有相互独立的非阻塞任务才能使用继续策略。
5. 恢复策略是“分批断点续跑”还是“幂等全量重跑”。该策略发布后不能切换。
6. 重复执行的幂等依据：唯一键、确定性覆盖、`$setOnInsert`、compare-and-set 或事务性全量重建。
7. 数据规模、预期耗时、批次大小和内存上界。
8. 坏数据是立即终止，还是跳过、汇总后由管理员修复再重试。
9. 滚动升级期间，旧版本节点是否仍能安全读写迁移中的结构。
10. 成功校验、失败回滚方式，以及需要展示给管理员的进度和错误定位数据。

不要用“目标表存在”“目标表非空”或当前数据形态替代任务状态。是否需要执行只能由静态注册表和迁移状态决定。

## 基本要求

- 注册表只允许在数组尾部追加。已发布任务的 ID、顺序、`blockStartup`、`onFailure` 和函数语义不得修改、删除或复用；修复已发布迁移时追加新任务。
- 完整任务及其可能重放的最小单元必须幂等。Lease 只保证同一时刻的执行权，不提供 exactly-once。
- 默认追加或回填，不删除旧字段、旧集合、回滚依据或用户数据。破坏性清理应放到后续兼容窗口结束后的独立任务。
- 自动迁移必须兼容滚动升级；要求全站停机或会立即破坏旧版本的变更不能直接进入该注册表。
- 禁止不可重放的外部副作用，例如无幂等键的消息、扣费或第三方写入。
- 所有业务写入必须有确定性，不能根据不稳定排序、offset 分页或随机值决定结果。
- 修改目标前验证源数据；返回前校验完成条件。不能仅因函数没有抛错就假设迁移完成。
- 日志、进度、最终结果、checkpoint 和错误数据不得包含凭证、完整业务正文、超大对象或完整错误栈。
- 迁移专属实现不得放入业务 DDD 模块。业务模块可以被迁移单向依赖，但不能反向依赖迁移目录。

## 状态与 Context

### 状态所有权

| 状态 | 谁负责写入 | 触发条件 |
| --- | --- | --- |
| `pending` | 框架 | 首次初始化，或管理员将非阻塞失败任务恢复为待执行 |
| `running` | Runner | 原子获得 lease，并生成新的 `runId` |
| `failed` | Runner / `context.fail` | 脚本明确报告失败，或普通异常被 Runner 捕获 |
| `succeeded` | Runner | 脚本正常返回且当前 `runId` 仍持有有效 lease |

脚本禁止直接操作迁移状态表和错误数据表，也不得主动设置 `pending`、`running`、`failed` 或 `succeeded`。所有运行状态写入必须经过 Context，让框架统一执行 runId fencing 和输入校验。

### 脚本可使用的状态能力

- `context.reportProgress(...)`：按注册表声明的阶段 `key` 更新该阶段快照。进入阶段时上报 `running`，完成后上报 `succeeded`；分批任务按合理频率更新 `current/total`，不要每条数据写一次。脚本不得主动上报 `failed`。
- `context.getCheckpoint(schema)`：读取并用任务自有 Zod Schema 校验断点。仅分批任务使用。
- `context.getFailedRecords()`：仅供非阻塞任务读取上次失败留下的坏数据。若任务曾跳过坏数据并把 checkpoint 推进到其后，重试时必须先处理这些记录；阻塞任务调用会被 Context 拒绝。
- `context.reportFailedRecords(records)`：仅供非阻塞任务按批替换“当前完整未解决错误快照”。业务批次提交后应先上报错误快照，再推进 checkpoint；替换语义保证批次重放不会重复追加同一错误。阻塞任务调用会被 Context 拒绝。
- `context.saveCheckpoint(value)`：只在一个幂等批次的业务事务完整提交、错误快照成功持久化并校验后保存。禁止先保存 checkpoint 再写业务数据或上报该批错误。
- `context.assertActive()`：确认当前 `runId` 仍持有 lease。每个业务写入批次前必须调用；全量任务至少在关键事务前及事务后的后续阶段前调用。
- `context.fail(...)`：保存可预期的结构化错误及最终错误快照，并终止本次执行。主要供需要在管理页面排障和重试的非阻塞任务使用；不要在调用后继续业务逻辑。阻塞任务通过该方法携带 `failedRecords` 会被拒绝。
- `context.logger`：记录诊断信息，框架会附加 migrationId、runId 和 runnerId。日志不能替代进度或结构化失败。
- `context.signal`：长计算或可取消 I/O 应响应中止信号。

正常返回表示脚本认为业务迁移和完成校验均已成功，由 Runner 标记 `succeeded` 并清理该任务的错误明细。任务可以返回有限标量的业务参数对象；Runner 校验后把参数与 `succeeded` 原子写入，列表接口再从静态注册表取得 `resultKey` 组装展示结果。`nameKey`、`descriptionKey`、`resultKey` 和阶段 `labelKey` 必须在注册表中使用 `i18nT(...)` 声明，禁止把 i18n key 写入 Mongo。不要自行调用完成状态更新，也不要用最后一条 progress 代替最终结果。

普通 `throw` 也会进入 `failed`，Runner 会把错误归属到当前 `running` 阶段，并保留任务之前通过 `reportFailedRecords` 上报的最新快照。只有 `context.fail` 显式携带 `failedRecords`（包括空数组）时才替换或清空旧快照。非阻塞任务的已知数据问题应维护完整未解决错误快照：处理中按批调用 `context.reportFailedRecords`，扫描结束仍有异常时再调用 `context.fail` 写入相同最终快照。错误对象只保存原始 `message`，不得携带 i18n key 或模板参数；每条错误数据必须包含已声明的 `stageKey`，只保存必要 ID 和原因，不复制原文档。框架按阶段记录异常数量，管理员从对应阶段按需打开详情。阻塞任务失败时管理页面本身不可用，应通过 `context.logger` 输出必要诊断后抛出异常；Context 会拒绝其读写错误明细，Runner 只把多节点协调所需的最小 `lastError` 写入状态表。

## 选择恢复策略

任务一旦获得新 runId，无论入口是管理员重置还是过期 lease 接管，都使用同一恢复策略：保留 checkpoint、progress 和最近错误；非阻塞任务还会保留供修复使用的错误明细。但触发条件不同：`running` 的 lease 过期可自动接管，阻塞 `failed` 在 owner 重启、lease 过期后可接管，非阻塞 `failed` 必须由管理员重置为 `pending`。脚本如何恢复由发布时选定的策略决定。

### 分批断点续跑

适用于数据量较大、执行时间不可控、无法在一个短事务内完成，或需要跳过坏数据继续扫描的任务。

每次执行必须覆盖三种输入状态：

1. 无 checkpoint、无错误数据：从稳定排序的起点开始。
2. 有 checkpoint、无错误数据：从最后完整提交的批次之后继续。
3. 有 checkpoint、有错误数据：先重新处理已跳过的错误记录，再从 checkpoint 继续扫描新数据。

推荐循环：

```text
读取并校验 checkpoint
读取 failedRecords，建立完整未解决错误快照并优先重试
while 还有数据:
  assertActive
  按稳定游标读取下一批
  在业务事务中执行幂等写入
  校验该批结果并提交事务
  如果错误快照变化，reportFailedRecords(完整未解决错误快照)
  saveCheckpoint
  reportProgress
执行全局完成校验
如果仍有错误，fail(包含相同最终错误快照)
reportProgress(completed)
return
```

关键约束：

- 使用 `_id` 或其他不可变、唯一且有索引的游标；不要使用 offset。
- 进程可能在业务提交后、checkpoint 保存前退出，因此整个批次必须可重复执行。
- checkpoint 只记录恢复所需的最小游标，不保存业务正文或不断增长的数组。
- `reportFailedRecords` 接收的是完整快照而不是新增项；不得逐条 append，也不得只上报当前批次，否则会覆盖之前仍未解决的错误。
- 错误快照必须先于对应 checkpoint 持久化。若进程在两者之间退出，接管节点会从旧 checkpoint 重放该批，并能读取已经保存的错误；禁止反向调用造成坏数据被 checkpoint 永久跳过。
- 错误快照已上报后又发生未预期异常时，Runner 会保留该快照；任务不得为了上报新异常而传入不完整的错误数组。
- 扫描结束仍有失败项时调用 `context.fail({ failedRecords })`，其中 `failedRecords` 与最近一次上报的完整快照一致。
- 重试后再次失败会替换旧错误明细；完整成功后由框架删除。

### 幂等全量重跑

仅适用于数据量确定较小、资源消耗有上界、结果由权威源确定，且整次写入可以保持原子或安全幂等的任务。

推荐流程：

```text
reportProgress(started)
读取完整权威源
在修改目标前完成全部转换、去重和校验
assertActive
原子写入或确定性覆盖完整结果
assertActive
刷新缓存并执行完成校验
reportProgress(completed)
return
```

关键约束：

- 不保存没有恢复意义的伪 checkpoint。每次管理员重试或 lease 接管都重新执行完整流程。
- 如果需要清空目标，目标必须完全由权威源派生，不包含用户增量；权威源必须保留。
- 清空和完整重建必须在同一事务中，失败时不能暴露空表或半成品。
- 源数据必须在清空目标前完成校验；重复执行和源数据修复后再执行都应产生确定结果。
- 如果全量操作无法在 lease 内稳定完成，改用分批策略，而不是单纯调大 lease。

## 阻塞与非阻塞

`blockStartup` 只决定节点 readiness，`onFailure` 只决定失败后是否暂停后续队列；所有任务仍严格按注册表顺序单线程执行。

- 选择阻塞任务：目标 Schema、配置或数据是新版本处理流量的必要前提。任一阻塞任务未成功，节点不能 ready。
- 选择非阻塞任务：新旧数据均可被业务兼容读取，迁移可在节点 ready 后后台完成。
- 阻塞任务的阶段进度仍通过 `reportProgress` 保存最新快照并同步输出阶段日志；具体错误诊断写终端日志，不写 `failedRecords`。状态表中的最小 `lastError` 仅用于跨节点观察失败事实，不承担错误日志存储职责。
- 阻塞任务明确失败后，owner 持续持有 lease 并等待；修复后需要重启 owner，lease 过期后由一个节点接管。
- 非阻塞任务明确失败后不自动重试；管理员查看错误、修复数据后点击重试，任务重新竞争 lease。
- `onFailure: 'continue'` 只允许相互独立的非阻塞任务使用：当前任务保持 `failed` 且等待管理员重试，Runner 可以跳过它继续后续任务；`stop` 则暂停后续队列。
- 位于后续阻塞任务之前且使用 `onFailure: 'stop'` 的非阻塞任务仍是启动前置条件；`continue` 任务失败后允许后续阻塞任务推进，因此必须确认二者没有成功依赖。

## 文件位置

迁移执行框架、静态注册表、Mongo Schema、服务端执行逻辑和任务集中在 App；前后端公共契约位于 global：

```text
packages/global/migration/
├── constants.ts
└── schema.ts

projects/app/src/migration/
├── constants.ts
├── registry.ts
├── runner.ts
├── entity.ts
├── service.ts
├── mongoSchema.ts
├── utils.ts
└── tasks/
    ├── README.md
    └── <migration-id>/
        ├── index.ts
        ├── service.ts
        └── utils.ts
```

- `index.ts` 只负责任务编排、Context 调用和进度阶段。
- `packages/global/migration` 只保存前后端共享的状态枚举、有限输入 Schema 和 API 类型，不放任务实现、Mongo Model 或 Runner。
- 注册项必须按执行顺序声明完整的 `progressSteps: [{ key, labelKey }]`；`key` 是永久稳定的机器标识，`labelKey` 放在 client-only `system_migration` i18n namespace，不写入 Mongo。
- 该任务专属的数据访问、转换和工具函数全部放在同名目录，不要散落到 `packages/global` 或 `packages/service`。
- 真正被正常运行期业务复用的能力保留在业务模块；迁移通过单向依赖调用它。
- Next.js API 路由和页面受框架目录约束，可以保留在 `pages/api`、`pages/config`，但必须是调用 migration service 的薄入口，不承载迁移逻辑。
- i18n 文案按项目现有机制放入所有语言文件；任务注册项使用 `i18nT(...)` 保存稳定的 name、description、result 和 progress label key。
- i18n key 只属于静态注册表和 API 展示 DTO，禁止写入状态表或错误明细表。成功结果只持久化有限标量参数，错误只持久化原始 `message`。
- 测试放在 `projects/app/test/migration/` 下并镜像源码子路径。

## 测试与验证

新增任务至少验证与其恢复策略相关的真实不变量，不写只匹配文案的测试。

通用场景：

- 首次执行成功，完成条件真实成立。
- 重复执行不会产生重复、覆盖错误数据或改变不应变化的结果。
- 源数据无效时不会留下部分目标数据，并能给出可定位错误。
- 业务写入或完成校验失败时不会被标记成功。
- 进度阶段、失败记录和日志不包含敏感或无界数据。
- 每个阶段均按 `running -> succeeded` 上报；任务返回前所有声明阶段都已成功，阶段异常和错误数据使用正确的 `stageKey`。
- 最终结果只在任务成功后存在；Mongo 只含参数，列表接口从注册表补入 key 后能渲染出预期业务结果。
- 任务注册在数组尾部，ID 唯一，`blockStartup` 和 `onFailure` 符合数据依赖。

分批任务额外验证：

- 从 checkpoint 恢复，不重扫已完成范围。
- 业务提交后、checkpoint 前退出时，重复批次仍然正确。
- checkpoint 与错误记录同时存在时，错误记录会被重新处理。
- 错误快照会在每批后替换持久化，且崩溃发生在错误上报与 checkpoint 之间时不会丢失或重复追加坏数据。
- 错误快照之后发生普通异常时，已保存的坏数据不会被空数组覆盖。
- 中途丢失 lease 后不再开始新的业务批次或保存状态。
- 阻塞任务调用 `getFailedRecords`、`reportFailedRecords` 或通过 `fail` 携带 `failedRecords` 时会被 Context 拒绝。

全量任务额外验证：

- 目标已有旧数据时仍能完整覆盖为权威结果。
- 写入失败会回滚清空和部分写入。
- 修改权威源后再次运行会产生对应的新结果。
- 空源数据的行为经过明确设计和测试，不能默认为安全。

只运行覆盖改动范围的局部测试、App typecheck、相关 ESLint 和 `git diff --check`。用户最终验收前不主动运行全量测试。若修改 API 路由或入参，继续遵守 `api-development` Skill；若新增单元测试，继续遵守 `test-case` Skill。

## 交付前检查

- [ ] 已确认恢复策略，并在任务函数注释中说明选择原因。
- [ ] 已证明任务整体及重放单元幂等。
- [ ] 只在注册表末尾追加，未修改已发布任务语义。
- [ ] 脚本未直接操作迁移状态表或错误数据表。
- [ ] 必需进度、lease 检查、完成校验和与阻塞类型匹配的失败出口均已实现。
- [ ] 需要展示最终产出时已由任务返回有限的 i18n 结果，并由 Runner 随成功终态提交。
- [ ] checkpoint 保存时机正确，或全量任务明确不使用 checkpoint。
- [ ] 分批任务在 checkpoint 前及时替换完整错误快照，没有只在任务末尾一次性保存坏数据。
- [ ] 阻塞任务只写终端诊断和最小 `lastError`，没有调用错误明细能力。
- [ ] 启动阻塞、失败调度策略、滚动升级兼容性和破坏性操作均已审查。
- [ ] 任务代码与测试位于 App migration 目录，不污染业务模块。
- [ ] 局部测试、类型检查、lint 和差异检查通过。

