Files

909 lines
62 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MiragenFlow 渠道、模型、分组与智能调度重构计划
状态:待审批。当前只完成计划评审和计划修订,未修改业务代码,未启动浏览器,未创建展示用渠道,未执行浏览器模拟。
本文件是审批稿。负责人批准前,任何代理不得执行下面的代码改动;批准后仍须按阶段完成和验收,不得把“写入计划”当成“功能完成”。
## 0. 评审结论
### 0.1 总结
现有计划的业务方向是清楚的:渠道负责接入和发现模型,模型负责公开能力与分辨率,分组负责模型和渠道的路由关系,健康负责运行状态,任务负责生命周期,价格和套餐负责计费约束,审计负责追溯。
但原计划不能直接执行,原因是它把目标状态和当前状态混在了一起,并遗漏了几个会阻断实施的前置关系:
1. 当前模型仍然承担价格、数量、并发和 `channelGroupId`;直接删除这些字段会破坏任务创建和价格链路。
2. 计划没有明确一个公开模型能否属于多个分组;没有这个基数规则,任务创建无法确定唯一路由。
3. 当前分组只保存全局渠道集合和顺序,计划却要求“某个模型下支持哪些渠道”,数据结构尚未表达模型和渠道的对应关系。
4. 计划要求智能调度、容量和半开状态,但当前 `ProviderChannel` 和 `ChannelGroup` 没有策略、容量或半开状态字段。
5. 当前健康接口从任务 attempts 临时聚合,既不是独立探活样本,也没有每个渠道固定 12 个样本。
6. OpenAI 图片 provider 当前明确不支持查询未知任务;自动 unknown 对账不能作为本阶段已实现功能。
7. 当前人工对账会用 `Math.min` 静默截断超额金额;当前测试也验证的是截断成功,与本计划的 422 规则相反。
8. 当前审计详情仍可能直接返回 `before`/`after`,列表分页也是前端本地分页;这些无法仅靠页面调整解决。
本次修订将每个目标拆成“现状、代码改法、目标行为、测试、完成条件”,并把相互依赖的改动放在正确顺序中。
### 0.2 已确认的代码事实
| 事实 | 代码依据 | 对计划的影响 |
| --- | --- | --- |
| 后台模型、渠道、分组、健康和价格路由都挂在同一个 `BusinessPage` | `admin/src/router/index.ts:54-60` | 先按业务职责拆分页面逻辑,再决定是否拆成独立文件;不重复引入 UI 库 |
| 模型仍包含价格、数量、并发和分组 ID | `packages/contracts/src/index.ts:127-146`、`server/src/store.ts:120-128` | 必须先建立价格规则、运行限制和模型分组关系,再移除旧字段 |
| 任务创建直接读取上述旧字段 | `server/src/app/http.ts:873-926` | P1 不能只改表单;必须同步改任务创建、快照和计费解析 |
| 渠道已有 `displayModelId/requestModelId` 映射类型,但仍保留旧的 `providerModelId` 直连路径 | `server/src/store.ts:10-11`、`server/src/adapters/provider.ts:14-20`、`server/src/app/http.ts:2320-2332` | 删除旧路径,所有 provider 请求必须经过渠道模型实体或显式映射 |
| 渠道 PATCH 会按 `priority` 回写所有所属分组顺序 | `server/src/app/http.ts:2427-2429` | 渠道属性和分组路由排序必须完全隔离 |
| 分组排序只检查全局渠道存在,不检查是否属于该分组 | `server/src/app/http.ts:2548-2554` | 排序接口必须检查成员集合完全一致、无遗漏、无越组成员 |
| 任务已有基础路由快照和 unknown 保留预留 | `server/src/app/http.ts:915-926`、`server/src/jobs/task-worker.ts:435-517` | 保留已有能力,但扩展快照、候选过滤和严格重试语义 |
| worker 当前只按启用和 `health !== open` 过滤 | `server/src/jobs/task-worker.ts:426-435` | 智能调度必须抽成可测试的候选过滤和排序纯函数 |
| 熔断当前冷却约 30 秒,直接回到 `degraded`,没有 `half-open` | `server/src/jobs/task-worker.ts:429-433`、`server/src/jobs/task-worker.ts:487` | 重新定义状态机、探测租约和冷却配置;不能只改展示文字 |
| 健康接口返回单个 `groupId`,并从任务 attempts 计算 | `server/src/app/http.ts:3053-3064` | 新增健康样本来源和 `groupIds` 数组;不得把前 12 个渠道误当成 12 个历史样本 |
| OpenAI 图片查询返回 `PROVIDER_QUERY_UNAVAILABLE` | `server/src/adapters/provider.ts:250-257` | 当前 unknown 只走人工对账;自动查询列为后续能力门槛 |
| 对账任务和账本在事务中,但审计调用在事务外 | `server/src/app/http.ts:2875-2934` | 明确审计原子性方案,不能在计划中笼统写“有事务和审计” |
| 持久化仓储主要保存 JSON snapshot | `server/migrations/0002_v1_domain.sql:61-118`、`server/src/infra/repository.ts:561-583` | 新结构要有 snapshot schema 版本和恢复策略;不凭空假设已有独立实体表 |
| 测试 fixture 会创建渠道和分组 | `server/src/store.ts:136-143` | 禁止展示 mock 数据,但允许隔离测试 fixture,并验收生产不注入 fixture |
### 0.3 本次明确的架构决策
1. 第一阶段目标是 8 个后台业务模块:渠道、模型、分组、健康、任务、价格、套餐、审计。
2. 人工对账是任务中心中的独立 Tab/筛选视图,不新造一个与任务生命周期脱节的页面;需要独立深链接时再增加 `/reconciliation`,但不是第一阶段前置条件。
3. 模型发现是渠道管理中的主动操作,不单独建“模型发现”页面。
4. 一个公开模型在第一阶段只能归属一个启用的路由分组;一个分组可以管理多个公开模型。这样保留当前任务按模型找到唯一分组的语义,避免一次任务同时命中多个未知策略。
5. 分组内部使用“模型 × 渠道”关系。一个渠道可以支持同一分组中的部分模型,不要求一个渠道覆盖分组内所有模型;每个模型有独立渠道池、独立顺序和独立重试预算。
6. 分组默认使用严格优先级。只有管理员明确选择“同优先级智能调度”时,才在最低可用优先级层内使用健康、容量、成功率和 P95 排序;智能指标不能让低优先级渠道越过高优先级渠道。
7. `publicModelId` 是平台公开 ID,`requestModelId` 是供应商请求 ID。两者在契约、存储、页面、路由和 provider 边界都必须保持分离。
8. 模型目录只负责公开身份、能力和分辨率。价格倍率归价格规则,数量硬上限归系统运行策略,套餐并发归套餐/租约;这些字段不回到模型弹窗。
9. 当前 OpenAI 图片 provider 没有可查询的异步任务协议,所以 unknown 结果本阶段不自动查询、不自动退款、不自动切换,只保留预留并进入人工对账。
10. 健康趋势固定显示 12 根条。绿、黄、红、灰、蓝只表示样本或探活阶段,不能替代当前调度资格。
## 1. 范围和禁止事项
### 1.1 本次范围
- 管理后台渠道、模型、分组、健康、任务、价格、套餐和审计的职责重构。
- `packages/contracts` 中公开模型、渠道模型、路由快照、价格快照、健康和管理分页契约。
- `server/src/store.ts`、`server/src/app/http.ts`、`server/src/adapters/provider.ts`、`server/src/jobs/task-worker.ts` 及持久化 snapshot 结构。
- 管理台请求封装、TDesign 表格/弹窗/Tab/下拉和现有本地 Lineicons 的业务接入。
- 代码级测试、类型检查、Lint、差异检查和必要的持久化/迁移回归。
- `CHANGELOG.md`、双语 `todo`、双语 `pending-test`;用户确认浏览器测试通过后再更新正式功能文档。
### 1.2 明确不做
- 不修改 `web/src/pages/canvas`、`web/src/components/canvas`、`web/src/stores/canvas`、`web/src/lib/canvas`,也不修改任何画布底层架构。
- 不把现有 TDesign 后台重写成 Ant Design、Lucide 或其他控件库;继续使用锁定版本的 TDesign 和本地 `admin/src/components/LineIcons.jsx`。
- 不使用 CDN、WebFont、原生 `<select>` 或新增第三方业务图标库。
- 不在开发环境自动创建展示用 mock 渠道,不用静态成功数据伪造供应商结果。测试 fixture 可以存在,但必须限制在测试模式。
- 不在渠道保存、打开编辑、切 Tab、刷新列表时隐式请求供应商;只有管理员点击“拉取模型”或“探活”才请求上游。
- 不在模型页面填写基础价格、数量上限、并发上限、渠道组 ID或逗号分隔能力。
- 不在公开 API、用户端、日志、导出、任务详情或普通管理列表泄露 API Key、Cookie、完整 Base URL、供应商请求模型 ID、供应商请求 ID、完整提示词或原始 JSON。只有具备管理权限的渠道编辑/探活上下文可以读取必要的完整 Base URL 和实际请求路径,API Key 永不回显。
- 本阶段不启动浏览器,不执行 Playwright、截图和浏览器模拟;浏览器验收由负责人自行完成。
### 1.3 测试 fixture 例外
`server/src/store.ts` 中的测试渠道、测试分组和 `fixtureFailure` 只允许用于隔离自动化测试。代码级验收必须覆盖:
- `NODE_ENV=production` 不创建 fixture 渠道、fixture 分组或模拟支付适配器。
- 管理页面首次进入不自动创建任何渠道。
- 测试 fixture 不出现在生产 snapshot、生产导出和真实供应商列表中。
## 2. 目标页面和控件规格
目标是 8 个后台业务模块。现有路由仍可先由一个入口分发,但每个模块必须有独立的字段定义、数据请求、错误处理和验收边界。
### 2.1 渠道管理 `/channels`
**职责**:连接供应商、保存凭证引用、主动拉取供应商模型、维护渠道模型清单和可选映射。
**不负责**:公开模型参数、价格、分组顺序、用户套餐、任务结算。
```text
+--------------------------------------------------------------------------------+
| 渠道管理 [搜索渠道________] [类型 v] [启用状态 v] [刷新] [+ 新增渠道] |
+--------------------------------------------------------------------------------+
| 渠道名称 | 类型 | 地址摘要 | 启用 | 渠道模型数 | 当前健康 | 最近拉取 | 操作 |
| 渠道 A | OpenAI 图片 | example... | 开 | 12 | 正常 | 10:20 | 编辑 探活 |
+--------------------------------------------------------------------------------+
```
新增/编辑弹窗使用三个 Tab,Tab 切换不丢草稿:
```text
+----------------------------------+
| 新增/编辑渠道 X |
+----------------------------------+
| [基础信息] [渠道模型] [模型映射] |
| 渠道名称 [___________________] |
| 接口类型 [OpenAI 图片端点 v] |
| 请求地址 [https://example.com/v1]|
| API Key [********************] |
| 请求超时 [________] 毫秒 |
| 启用状态 [开关] |
| [取消][保存] |
+----------------------------------+
```
字段含义和规则:
| 控件 | 填写内容 | 规则 |
| --- | --- | --- |
| 渠道名称 | 管理员可读名称 | 必填,去首尾空白,长度有限制 |
| 接口类型 | 当前选择 `OpenAI 图片端点` | 只展示已有真实 adapter;没有真实调用链的类型不得伪造可用 |
| 请求地址 | 完整 `https://example.com` 或 `https://example.com/v1` | 禁止账号密码;生产只允许 HTTPS 和公网上游;开发测试的本地地址按现有安全开关处理 |
| API Key | 供应商密钥 | 去首尾空白后只校验非空;编辑留空表示沿用原凭证;存储必须加密 |
| 请求超时 | 毫秒正整数 | 存入渠道配置,provider 请求使用该值;设置合理的最小/最大边界 |
| 启用状态 | 是否参与调度 | 停用立即失去调度资格,但不删除历史任务和健康记录 |
“渠道模型” Tab:
- 只有点击“从供应商拉取模型”才调用真实 `/v1/models`;显示独立 loading、实际请求路径、`requestAttempted`、拉取时间、候选数量和错误阶段。
- 错误至少区分未发起的 URL/SSRF 校验、DNS、网络连接、TLS、HTTP 401/403/404/429/5xx、JSON 无效和 `data` 为空。
- 拉取结果只作为候选。管理员勾选候选并点击“加入渠道模型”后才保存,不能自动创建公开模型、映射或上架。
- 手动补录请求模型 ID必须明确标记为手动,不能为空,不能与现有渠道模型重复。
- 动态行使用创建时稳定 React key,不能使用正在编辑的模型 ID作为 key。
“模型映射” Tab:
- 左侧是平台公开模型 ID的主题化下拉,右侧是供应商 `requestModelId` 输入框。
- 目标请求 ID必须存在于该渠道模型清单;平台显示 ID不得重复。
- 映射保持可选:只有平台公开 ID与供应商请求 ID不同时才保存映射;两者相同时,只有该 ID已经作为明确的渠道模型存在,才允许做同名精确匹配。不得回退到任意首个模型或旧 `providerModelId`。
- 映射只解决 ID转换,不改变分辨率、能力和价格规则。
弹窗和异步规则:保存成功自动关闭并刷新;失败保留草稿并显示中文错误;保存、取消、右上角关闭、遮罩点击和 ESC 都能关闭;探活和拉取模型不能复用保存按钮的 loading。
### 2.2 模型管理 `/model-products`
页面标题显示为“模型管理”,路由可暂时保留旧路径以减少无意义的地址变更。
**职责**:维护平台公开模型目录和用户可见能力。
**不负责**:供应商连接、供应商请求模型 ID、渠道优先级、价格、套餐并发。
```text
+--------------------------------------------------------------------------------+
| 模型管理 [搜索公开 ID/名称________] [状态 v] [能力 v] [刷新] [+ 新增模型] |
+--------------------------------------------------------------------------------+
| 公开模型 ID | 展示名称 | 档位 | 能力 | 分辨率 | 可用渠道 | 状态 | 版本 | 操作 |
| image-pro | 图像专业 | 旗舰 | 图像 | 1K/2K | 3 | 草稿 | v3 | 编辑 |
+--------------------------------------------------------------------------------+
```
编辑/新增字段:
```text
+----------------------------------+
| 新增/编辑公开模型 X |
+----------------------------------+
| 公开模型 ID [image-pro_______] |
| 展示名称 [________________] |
| 档位 [基础 v] |
| 能力 [x 图像] [ 文本]... |
| 能力参数 [结构化字段] |
| 发布状态 [草稿/已上架开关] |
| 分辨率 [+ 新增分辨率] |
| 稳定 ID [1K] 名称 [低] |
| 宽 [1024] 高 [1024] |
| 支持比例 [1:1] [16:9] ... |
| [取消][保存] |
+----------------------------------+
```
字段规则:
- `publicModelId` 创建必填且全局唯一;编辑只读,不允许用改名方式破坏公开 API。
- 展示名称、档位、能力使用结构化字段;能力是多选/复选框,不再使用逗号分隔字符串。
- 能力参数按已登记 schema 展示字段,不允许原始 JSON 文本框。
- 分辨率保留稳定 ID、名称、宽、高、支持比例;比例使用多选,不允许把 `1:116:99...` 这样的拼接字符串当作一个值。
- 分辨率的价格倍率不放在模型目录,价格由价格规则页面管理。
- 新模型保存默认为草稿;上架是单独的显式动作。上架前服务端检查合法能力、至少一个合法分辨率(需要分辨率的能力)、至少一个可用路由和版本条件。
- 列表不显示基础价格、数量上限、并发上限、渠道组 ID、供应商名称或供应商请求模型 ID。
- “从渠道候选导入”只生成公开模型草稿,必须由管理员补齐公开参数并保存;不能自动建立渠道映射。
### 2.3 分组管理 `/channel-groups`
页面标题显示为“分组管理”。分组是路由策略容器,不是模型产品的附属字段。
**职责**:确定哪些公开模型可通过哪些渠道执行,以及每个模型的渠道顺序、失败预算和调度策略。
```text
+--------------------------------------------------------------------------------+
| 分组管理 [搜索] [启用状态 v] [刷新] [+ 新增分组] |
+--------------------------------------------------------------------------------+
| 分组名称 | 公开模型数 | 渠道关系数 | 首选渠道摘要 | 策略 | 重试预算 | 版本 | 操作 |
| 图像默认 | 2 | 6 | A > B > C | 严格 | 4 | v5 | 配置 |
+--------------------------------------------------------------------------------+
```
分组编辑采用“模型路由矩阵”,而不是一个全局渠道池:
```text
+--------------------------------------------------------------------------------+
| 分组:图像默认 X |
+--------------------------------------------------------------------------------+
| 分组名称 [________________] 启用 [开关] |
| 公开模型 [x image-pro] [x image-lite] |
+--------------------------------------------------------------------------------+
| 模型 image-pro 调度 [严格优先级 v] 总重试 [3] |
| 渠道 | 是否支持 | 优先级层 | 层内顺序 | 失败预算 | 健康 | 容量 | 操作 |
| A | [x] | 1 | 1 | 1 | 正常 | 2/10 | ↑ ↓ 删除 |
| B | [x] | 1 | 2 | 1 | 降级 | 8/10 | ↑ ↓ 删除 |
| C | [ ] | - | - | - | - | - | 加入 |
+--------------------------------------------------------------------------------+
| 模型 image-lite 调度 [同优先级智能 v] 总重试 [2] |
| 渠道 ... |
| [预览故障切换] [取消] [保存] |
+--------------------------------------------------------------------------------+
```
字段含义和规则:
- 分组 ID由系统生成、只读;分组名称必填;启用状态单独控制。
- 一个公开模型第一阶段最多有一个启用分组;同一个分组可以包含多个公开模型。保存时拒绝重复归属,并给出中文冲突信息。
- 每个模型有自己的渠道成员、优先级层、层内顺序、总重试预算和渠道失败预算。一个渠道可以支持同一分组中的一个模型而不支持另一个模型。
- 渠道候选必须已启用,并且渠道模型清单中存在有效 `requestModelId` 或有效 `displayModelId -> requestModelId` 映射;不满足条件的渠道显示为禁用原因,不能静默加入。
- 优先级层是正整数,数字越小越先尝试;同一层允许多个渠道。严格模式按“优先级层、层内顺序”执行,智能模式只在同一优先级层内动态排序。
- 顺序保存拒绝重复、遗漏、未知渠道、不属于该模型路由的渠道,以及同一层内重复的顺序值;使用 `If-Match`,冲突返回 409。
- 默认策略是严格优先级;“同优先级智能调度”是显式开关,不是后台自动改变管理员顺序。
- “预览故障切换”是纯函数:输入当前模型路由、健康、容量和模拟失败集合,输出候选过滤理由和排序结果;不得请求上游、创建任务、扣金币或写健康状态。
- 渠道普通属性编辑不能修改任何分组顺序;排序只能在分组路由接口中修改。
### 2.4 渠道健康 `/channel-health`
**职责**:显示渠道运行历史和当前调度资格,提供单项/批量探活。
```text
+--------------------------------------------------------------------------------+
| 渠道健康 [分组 v] [状态 v] [全部探活] [刷新] |
+--------------------------------------------------------------------------------+
| 渠道 | 12 根趋势条 | 当前状态 | 成功率 | P95 | 容量 | 连续失败 | 熔断剩余 | 操作|
| A | |||||||||||||| | 健康 | 99.2% | 820ms | 2/10 | 0 | - | 探活 |
| B | |||||||||||||| | 降级 | 84.1% | 2.4s | 8/10 | 2 | 38s | 探活 |
| C | |||||||||||||| | 未探活 | - | - | - | - | - | 探活 |
+--------------------------------------------------------------------------------+
```
趋势条规则:
- 每个渠道固定渲染 12 根,数据不足从左侧补灰或按统一时间槽补灰;数组长度永远是 12,不因样本数量改变布局。
- 绿:探活/任务成功且延迟在正常阈值内。
- 黄:慢、负载高或结果为降级。
- 红:网络、超时、HTTP 5xx或供应商失败。
- 灰:无数据或该时间槽没有样本。
- 蓝:当前探活请求进行中;探活完成后按结果落为绿/黄/红/灰。
- 颜色只表达样本趋势;当前状态、启用状态、模型覆盖、容量和熔断资格必须单独展示。
API 必须返回 `groupIds: string[]`、当前状态、成功率、P95、容量、连续失败、`circuitResetAt`、最后错误、最后探活信息和固定长度 `samples`。`samples` 来自独立探活/运行样本记录,不再只从所有任务 attempts 临时聚合。
探活操作必须显示独立 loading、真实请求路径、`requestAttempted` 和中文阶段错误。批量探活不能让某一渠道的失败覆盖其他渠道的结果。
### 2.5 任务中心 `/tasks`
**职责**:查看任务生命周期、路由快照、尝试记录、错误、输出、取消和重试。
```text
+--------------------------------------------------------------------------------+
| 任务中心 [任务 ID/模型搜索____] [状态 v] [类型 v] [刷新] |
| [全部任务] [排队中] [运行中] [未知待对账] [已完成] [失败] |
+--------------------------------------------------------------------------------+
| 任务 ID | 公开模型 | 类型 | 状态 | 预留金币 | 尝试 | 当前渠道 | 更新时间 | 操作 |
+--------------------------------------------------------------------------------+
```
详情使用字段化组件:
- 公开模型 ID、分组、价格规则、套餐版本和渠道顺序的只读快照。
- 每次尝试的渠道、状态、开始/结束时间、可重试标记、中文错误分类。
- 输出数量、类型、MIME、尺寸和结算状态。
- 不显示 Base URL、API Key、供应商请求模型 ID、供应商请求 ID或原始 JSON。
- “未知待对账”是任务中心的独立 Tab,使用任务 ID深链接和状态筛选;成功/失败对账必须显示版本冲突、重复提交和余额影响。
### 2.6 价格规则 `/pricing`
**职责**:独立管理公开模型的计费规则,不把价格塞回模型目录。
```text
+--------------------------------------------------------------------------------+
| 价格规则 [公开模型 v] [分辨率 v] [状态 v] [刷新] [+ 新增规则] |
+--------------------------------------------------------------------------------+
| 模型 | 分辨率 | 数量阶梯 | 单位价格 | 倍率/计算方式 | 生效版本 | 状态 | 操作 |
| image-pro | 1K | 1-1/2-4 | 10 | 1.0 | v3 | 生效 | 编辑 |
+--------------------------------------------------------------------------------+
```
规则字段:公开模型 ID、可选分辨率 ID、数量阶梯、单位价格、价格计算方式、金币单位版本、生效时间/结束时间、启用状态和版本。保存时拒绝重叠生效区间和重叠数量阶梯;调价使用 `If-Match`,冲突返回 409。
任务创建只能调用统一 `resolvePricingRule`,计算出的规则 ID、版本、单位价格、分辨率和数量固化到 `pricingSnapshot`。历史任务不能因后续调价而改变。
### 2.7 套餐与运行额度 `/plans`
**职责**:管理用户权益,不与模型目录字段重复。
```text
+--------------------------------------------------------------------------------+
| 套餐管理 [状态 v] [刷新] [+ 新增套餐] |
+--------------------------------------------------------------------------------+
| 套餐 | 金币 | 并发上限 | 队列优先级 | 可用模型 | 可用分组 | 有效期 | 状态 | 操作 |
+--------------------------------------------------------------------------------+
```
套餐字段:金币额度、有效期、允许的公开模型、允许的路由分组、最大并发、队列优先级、存储保留策略和发布状态。任务创建时只读取套餐权益;不再从模型读取 `maxConcurrent`。数量安全硬上限属于系统运行策略(例如 `SystemSettings.generationMaxCount`),不出现在模型表单。
### 2.8 审计 `/audit`、`/audit-logs`
逻辑上是一个审计模块,可保留当前“日志大板”和“详细日志”两个入口。
- 大板:HTTP 成功、4xx/5xx、业务错误、渠道切换、熔断、对账和配置变更趋势。
- 详细日志:时间、中文分类、中文行为、操作人、对象、资源公开标识、结果状态和来源。
- 审计内部可保存不可公开的 `requestId` 用于排查,但页面不展示内部请求 ID;页面只展示审计记录 ID或短的安全追踪标识。
- HTTP 状态趋势来自独立的请求结果记录或统一响应完成钩子,必须保存真实 `statusCode`;`AuditRecord` 保存配置/业务动作及明确 `outcome`。两者可以通过安全追踪 ID关联,但不得根据 action 名称猜测 200/500。
- 详情必须经过白名单字段投影和递归脱敏,不能直接返回存储的 `before`/`after`。
- 禁止显示凭证、Cookie、完整提示词、供应商请求 ID、哈希链内部值和原始 JSON。
## 3. 目标数据模型和字段迁移
### 3.1 公开模型目录
目标结构:
```text
PublicModelProduct
internalId 管理内部稳定 ID
publicModelId 平台公开 ID,创建后不可变
name
tier
capabilities[] 结构化多选值
capabilityParameters 按 capability schema 保存
resolutions[] id/name/width/height/ratios[]
enabled 草稿或已上架
version
```
从模型目录移除:`basePrice`、`maxCount`、`maxConcurrent`、`channelGroupId`、分辨率 `priceMultiplier`、逗号能力字符串和供应商字段。
替代关系:
| 旧字段 | 新归属 | 迁移/代码要求 |
| --- | --- | --- |
| `basePrice` | `PricingRule.unitPrice` | 先建立价格解析器和价格规则存储,再删除产品读取 |
| `maxCount` | `SystemSettings.generationMaxCount` | 作为统一安全上限;套餐可进一步限制,不在模型页配置 |
| `maxConcurrent` | `PlanEntitlement.maxConcurrent`、系统队列和渠道租约 | 任务创建/worker统一读取额度和租约 |
| `channelGroupId` | `ChannelGroup.routes[publicModelId]` | 每个公开模型第一阶段只允许一个启用分组 |
| `capabilities: string` | `capabilities: string[]` | 契约和页面统一多选,不再 `split(',')` |
| `resolutions[].priceMultiplier` | `PricingRule` | 模型仍保留分辨率本身,但不保存价格语义 |
由于项目当前主要使用 JSON snapshot,不能只改 TypeScript 类型而不处理持久化。需要给 snapshot 增加结构版本;开发环境遇到旧结构时按项目未上线规则直接拒绝加载并给出清晰重置/重新初始化提示,不写隐式旧字段兼容分支。
### 3.2 渠道和模型映射
目标结构:
```text
ProviderChannel
id
label
providerType
baseUrl
secretRefEncrypted
timeoutMs
enabled
version
runtimeHealth
circuit
capacity
models[]
requestModelId
source: discovered | manual
enabled
lastSeenAt
modelMappings[]
displayModelId
requestModelId
```
`providerModelId`、`enabledModelIds`和`resolutionModelMap`不再作为独立旧路径继续参与请求。所有请求模型必须先在 `models[]` 中存在;如果公开模型 ID与请求 ID不同,必须存在显式映射;如果二者相同,可以对渠道模型清单做同名精确匹配。provider 不允许在渠道模型不存在时把公开 ID直接当上游 ID,也不允许回退到任意首个模型。
### 3.3 分组和模型路由
目标结构用模型维度保存路由:
```text
ChannelGroup
id
name
enabled
version
routes: [
{
publicModelId
channels: [
{ channelId, priorityTier, orderWithinTier, retryBudget }
]
strategy: strict | smart-within-priority
retryBudget
}
]
```
保存不允许同一个 `publicModelId` 在多个启用分组中出现;一个分组内同一个模型只能有一条 route。后续如需一个模型多套路由策略,应另立需求设计用户/套餐到分组的选择规则,不能在本阶段隐式放开多归属。
### 3.4 健康样本和熔断
健康展示与调度运行状态分离:
```text
ChannelHealthSample
channelId
source: probe | task
status: success | slow | network_error | http_error | provider_error | empty
latencyMs?
httpStatus?
requestPath?
requestAttempted
errorCode?
createdAt
```
渠道熔断至少需要:`closed/healthy`、`degraded`、`open`、`half-open`和独立 `enabled=false`。`half-open` 只能由一个探测租约持有者执行,成功回到 healthy,失败重新 open;默认冷却 60 秒,实际值可由系统策略配置并写入快照/审计。
### 3.5 任务快照
任务创建完成后保存不可变:
```text
routeSnapshot
groupId
groupVersion
publicModelId
routeVersion
orderedCandidates[]
channelId
priorityTier
orderWithinTier
requestModelId
channelRetryBudget
strategy
totalRetryBudget
modelSnapshot
publicModelId
modelVersion
capability
resolutionId
pricingSnapshot
ruleId
ruleVersion
unitPrice
quantity
multiplier
balanceUnitVersion
planSnapshot
planId
planVersion
maxConcurrent
queuePriority
```
任务入队后不重新读取当前模型、价格、分组、渠道映射或套餐策略。重试若是用户主动创建新任务,必须生成新的任务 ID和新的快照;不能修改原任务快照。
## 4. API 边界和响应规则
### 4.1 资源 API
| 模块 | API 目标 | 关键规则 |
| --- | --- | --- |
| 渠道 | `GET/POST/PATCH /api/v1/admin/channels` | 保存连接配置,不触发上游;凭证写入加密引用 |
| 渠道发现 | `POST /api/v1/admin/channels/:id/discover-models` | 只有显式点击触发;返回路径、是否发起、阶段错误和候选 |
| 渠道模型 | `POST /api/v1/admin/channels/:id/models`、映射更新 API | 候选加入和映射保存分开;请求 ID必须存在 |
| 模型 | `GET/POST/PATCH /api/v1/admin/model-products` | 只接收公开目录字段;未知旧字段拒绝或明确忽略,不能重新持久化 |
| 分组 | `GET/POST/PATCH /api/v1/admin/channel-groups` | 保存模型路由关系、成员资格、策略和版本 |
| 分组排序 | `PATCH /api/v1/admin/channel-groups/:id/routes/:modelId/order` | 完整集合校验、`If-Match`、审计、冲突 409 |
| 健康 | `GET /api/v1/admin/channel-health`、`POST /api/v1/admin/channels/:id/probe` | 返回完整 `groupIds` 和 12 样本;探活不改变分组顺序 |
| 任务 | `GET /api/v1/admin/tasks`、`GET /api/v1/admin/tasks/:id` | 服务端筛选/分页;详情只返回字段化安全投影 |
| 对账 | `POST /api/v1/admin/tasks/:id/reconcile` | 仅 unknown;金额、数量、文件、版本和幂等严格校验 |
| 价格 | `GET/POST/PATCH /api/v1/admin/pricing-rules` | 生效区间/数量阶梯不重叠;`If-Match` |
| 套餐 | 复用现有 `/api/v1/admin/membership-plans` 资源 | 只管理权益和队列额度,不写模型价格字段 |
| 审计 | `GET /api/v1/admin/audit`、`GET /api/v1/admin/audit-logs` | 分页、状态字段、白名单安全投影 |
### 4.2 写请求通用规则
- 所有管理写接口需要对应 scope;高风险写操作保留现有审批边界。
- 资源有 `version` 时必须支持 `If-Match`;旧版本返回 409,不静默覆盖。
- 成功保存、探活、模型发现和对账都写审计;审计内容经过脱敏投影。
- 重复提交使用已有管理幂等机制;同一幂等键不同 body 返回冲突。
- 同一个业务事务内完成资源、账本/任务状态和必要审计写入;若底层仓储不能在一个事务内完成,必须设计可靠 outbox,而不是接受“可能没有审计”。
- 422 表示字段、关系、金额、能力或输出校验失败;409表示版本、状态、幂等或资源关系冲突;503表示暂时不可用或没有可用路由。
### 4.3 列表分页和请求代次
所有管理列表统一使用 `AdminPage<T>`:
```text
GET /api/v1/admin/tasks?page=1&pageSize=20&status=unknown&query=...
{ items: [...], total: 42, page: 1, pageSize: 20 }
```
服务端负责筛选、排序和分页,前端不再对全量数组 `slice` 后假装分页。前端每个列表保存请求代次和 `AbortController`;筛选/分页快速变化时,旧响应不能回写新条件。空态、加载态、失败重试和最后一次成功数据要分别表达。
## 5. 核心运行逻辑
### 5.1 创建任务的候选过滤
任务创建先通过模型和价格校验,再为该模型解析唯一启用分组和 route。候选依次过滤:
1. 分组启用、模型 route 存在。
2. 渠道启用、provider 类型匹配。
3. 渠道清单中存在请求模型 ID;映射完整且不指向不存在的请求模型。
4. 渠道具备任务能力和分辨率覆盖。
5. 渠道不是 `open`;`half-open`只有探测租约或显式允许的一次验证请求。
6. 渠道、套餐、系统和当前任务租约有可用容量。
7. 当前任务已经失败过的渠道不重复尝试,除非是明确的新任务重试。
8. route 和任务剩余重试预算允许继续尝试。
每个被过滤的候选都保留机器可读原因,例如 `CHANNEL_DISABLED`、`MODEL_NOT_MAPPED`、`CAPACITY_FULL`、`CIRCUIT_OPEN`,管理详情可显示中文映射。没有候选时返回 `MODEL_GROUP_UNAVAILABLE`,不发送任何上游请求。
### 5.2 排序和智能调度
默认严格使用管理员保存的 `priorityTier` 和 `orderWithinTier`。
当 route 明确开启 `smart-within-priority` 时:
- 先取数值最小的可用 `priorityTier`;只在这个优先级层内智能排序。
- 健康等级优先:healthy 高于 half-open,高于 degraded;open 不参与普通排序。
- 同等级按可用容量降序、P95 延迟升序、成功率降序、稳定 channel ID升序排序。
- 数值更大的低优先级层永远不能越过仍有候选的高优先级层。
- 排序是纯函数,输入必须包含 routeSnapshot 和健康/容量快照;稳定 ID保证相同输入有相同结果。
- 本阶段不引入无法持久化的随机权重;若未来需要权重或平滑加权轮询,先增加独立数据字段、并发安全游标和测试。
### 5.3 熔断状态机
```text
disabled --管理员启用--> healthy 或 unknown
unknown --首次成功--> healthy
unknown --首次失败--> degraded
healthy --慢/可确认失败--> degraded
degraded --连续失败达到 3 次--> open
open --冷却 60 秒且取得探测租约--> half-open
half-open --探测成功--> healthy
half-open --探测失败--> open,并重新计算冷却时间
任意运行状态 --管理员停用--> disabled
```
状态转换和探测租约必须在事务/持久化租约边界内完成,防止多 worker 同时把同一渠道当成半开探测。页面显示“停用”来自 `enabled=false`,不能把它伪装成健康颜色。
### 5.4 故障切换
- 只有 provider 明确返回 `status=failed` 且 `retryable=true` 的可确认失败,才允许按 routeSnapshot 尝试下一个渠道。
- 认证失败、密钥不存在、模型未配置、请求可能已到达上游但响应丢失等不可确认错误不得盲目切换;根据具体原因进入失败或 unknown。
- transport timeout、AbortError或可能到达上游的响应丢失进入 unknown,停止切换和退款。
- 每一次尝试都消耗任务总重试预算和对应渠道失败预算,且使用任务创建时的快照值,不读取实时配置覆盖历史任务。
- 故障切换、过滤原因、最终状态和结算结果都写事件和审计,但不写供应商敏感字段。
### 5.5 Unknown 和人工对账
当前 OpenAI 图片 provider 没有查询接口,因此本阶段规则是:
- unknown 任务保留预留金币,不由普通预留过期回收释放。
- 不自动切换到下一个渠道,不自动退款,不假设上游失败。
- 人工成功对账必须校验:任务仍为 unknown、`If-Match`版本正确、实际扣除金额满足 `0 <= chargedAmount <= reservedCost`、上传文件数量等于任务 count、MIME允许、文件大小和图片尺寸符合模型/任务参数、文件内容可解析。
- 超过预留金额返回 422,不能 `Math.min` 静默截断。
- 人工成功/失败必须幂等;重复相同请求返回同一结果,冲突 body 返回 409。
- staging 文件在事务失败、版本冲突或账本失败时清理;已提交文件和任务状态必须有一致的生命周期。
- 账本、任务状态、输出对象引用和审计要么一起提交,要么通过可靠 outbox补偿;不能接受任务已完成而没有审计的窗口。
自动 unknown 查询不列入本阶段完成条件。只有未来新增 provider 的异步查询能力契约后,才可以启用;启用时必须逐项验证任务类型、count、输出数量、MIME、大小、尺寸、图片内容和 provider 状态,任何一项失败都保持 `unknown/manual_review`。
## 6. 逐阶段实施计划
### P0:冻结基线和关系设计
**目标**:在改代码前把字段、关系、兼容边界和测试入口写成可执行清单。
**需要做的事**:
- 读取并核对 `AGENTS.md`、`plan.md`、`docs/miragenflow-channel-model-page-wireframes.md`、`docs/miragenflow-channel-model-logic.html`。
- 在代码改动前建立旧字段到新字段的映射表,确认 `PublicModelProduct`、`InternalModelProduct`、`ChannelGroup`、`ProviderChannel`、任务快照和套餐引用的所有读取点。
- 明确 snapshot schema 版本、测试 fixture边界和本地旧 snapshot 处理方式。
- 明确本阶段“一模型一个启用分组”的规则,并为多分组未来需求留下错误码而不是隐式行为。
**代码范围**:本阶段只读,不修改业务代码;审批前只允许修改本计划。
**完成条件**:每个旧字段都有替代归属;每个任务创建读取点都有后续阶段负责人;没有“后面再决定”的模型/分组基数。
### P1:契约、存储和价格/分组基础模型
**目标**:先建立不会破坏任务创建的目标数据形状。
**代码改法**:
- 修改 `packages/contracts/src/index.ts`:收窄公开模型字段,增加价格规则、渠道模型、路由、健康样本和分页类型;更新任务快照类型。
- 修改 `server/src/store.ts`:增加 `pricingRules`、健康样本存储、渠道模型实体、按模型 route 的分组结构和 snapshot schema version;补充默认 fixture。
- 修改 `server/src/infra/repository.ts` 及必要 migration:让新字段能保存、恢复和校验;不保留旧字段双读。
- 修改 `server/src/app/http.ts` 的 `createTask` 前置解析:先调用统一的模型、路由和价格解析器,暂时保留旧字段只到 P1 完成的同一提交边界,不允许新写入继续产生旧结构。
- 增加 `SystemSettings.generationMaxCount` 或同等运行策略字段,替代模型级 `maxCount`;套餐仍负责 `maxConcurrent`。
**目标结果**:模型、价格、分组、套餐和任务快照的责任边界在类型层和保存层一致;服务端不再需要从模型目录读取价格/并发/分组 ID。
**测试**:契约构造测试、snapshot 保存恢复测试、价格规则版本测试、模型重复分组归属测试、旧字段拒绝/不写回测试。
**依赖**:P0完成后才能开始;P2、P3、P4都依赖本阶段的字段形状。
### P2:模型管理和价格规则 API/UI
**目标**:让模型页面只表达公开模型和分辨率,让价格页面真正拥有价格。
**代码改法**:
- 修改 `admin/src/pages/Business/index.tsx` 的模型列表、表单字段和请求 body:删除 `basePrice`、`maxCount`、`maxConcurrent`、`channelGroupId`、逗号能力;改用能力多选、结构化参数和分辨率数组。
- 将价格列表和编辑弹窗改为读取/写入 `pricingRules`,不再 PATCH 模型产品的 `basePrice`。
- 修改 `server/src/app/http.ts` 的 `POST/PATCH model-products`:只接受公开模型字段,创建默认为草稿,编辑公开 ID只读;上架前执行能力、分辨率和路由检查。
- 修改 `server/src/app/http.ts` 的 `pricing-rules`:实现规则实体、数量阶梯/生效区间非重叠校验、版本和 If-Match。
- 修改 `server/src/app/http.ts` 的 `createTask` 和计费 helper:统一调用 `resolvePricingRule`,生成新的 `pricingSnapshot`。
- 修改 `admin/src/services/platform.ts`:补齐模型/价格接口的 typed wrapper、中文错误转换和版本头传递。
**目标结果**:打开模型页面看不到无意义的价格、数量、并发和渠道组字段;价格改动只影响新任务,历史任务金额不变。
**测试**:模型请求 body 静态检查;模型上架缺路由失败;能力/分辨率非法值失败;价格规则重叠失败;价格版本冲突 409;任务快照使用规则版本而不是产品旧价格。
**依赖**:P1;P3 的分组页面只能选择已保存模型,P8 的 UI 拆分以本阶段字段为准。
### P3:渠道、渠道模型和映射
**目标**:把供应商接入和平台公开模型彻底分开。
**代码改法**:
- 修改 `server/src/store.ts` 和 contracts:引入渠道模型实体、超时和探活结构;移除旧 `providerModelId` 直连依赖。
- 修改 `server/src/app/http.ts` 的渠道创建/编辑:保存连接、加密凭证、超时和启用状态;保存过程不得调用上游;普通 PATCH不得写分组优先级。
- 修改发现接口:把 `/v1/models` 请求封装成显式 action,返回候选、路径、`requestAttempted`、错误阶段和时间。
- 修改 provider 的 `resolveChannelModel`:优先使用显式 `displayModelId -> requestModelId` 映射;无映射时只允许渠道模型清单中的同名精确匹配,删除旧 `providerModelId`、任意首模型等 fallback。
- 修改管理台渠道 Tab:候选勾选加入、映射独立保存、稳定 React key、独立 loading、失败保留草稿。
- 补齐 DNS/TLS/HTTP/JSON/data-empty 的结构化错误字段,前端只做中文显示映射,不从自由文本猜错误。
**目标结果**:保存渠道不会偷偷请求供应商;只有点击发现才访问 `/v1/models`;模型映射缺失在任务创建前就能被识别。
**测试**:保存/编辑不发起上游请求;发现成功和各阶段失败;映射目标不存在失败;未登记公开 ID不能作为请求 ID;已登记同名渠道模型可精确匹配;渠道 PATCH前后所有分组顺序完全相同;超时使用渠道配置。
**依赖**:P1;P4 分组成员资格依赖渠道模型实体。
### P4:分组模型路由和顺序隔离
**目标**:让分组真正表达“哪些模型使用哪些渠道、按什么顺序”。
**代码改法**:
- 修改 `ChannelGroup` 保存按 `publicModelId` 分组的 routes;每个成员保存 `priorityTier`、`orderWithinTier` 和失败预算;删除产品侧 `channelGroupId` 读取和写入。
- 修改分组创建/编辑 API:校验模型存在、启用分组唯一归属、渠道启用、协议兼容、模型覆盖和映射完整。
- 修改排序 API:按模型 route校验提交集合与原成员集合完全一致;拒绝重复、遗漏、未知和越组成员;`If-Match`后再更新版本。
- 删除渠道 PATCH 对分组 `channelPriorities` 的回写逻辑;顺序只能由 route/order 接口修改。
- 修改任务创建:按公开模型找到唯一 route,保存排序后的 routeSnapshot、请求模型 ID和每渠道预算。
- 修改管理台分组编辑器:模型选择、每模型渠道矩阵、上移/下移、失败预算、策略选择和纯函数故障切换预览。
**目标结果**:同一渠道可以进入多个分组,也可以在同一分组的不同模型 route中有不同顺序;渠道名称或凭证编辑不会改变任何路由顺序。
**测试**:一模型重复归属失败;同渠道多分组独立顺序;遗漏/越组排序失败;If-Match冲突;渠道 PATCH顺序不变;任务 routeSnapshot与创建时版本一致。
**依赖**:P2、P3;P6 调度过滤以本阶段 routeSnapshot为输入。
### P5:健康样本、容量和熔断
**目标**:建立可用于调度的真实运行状态和固定 12 根趋势数据。
**代码改法**:
- 在 `server/src/store.ts`/repository中增加健康样本记录和容量事实来源;任务租约是并发事实来源,不能只依赖进程内 `activeTaskIds`。
- 修改 `server/src/app/http.ts` 健康接口:返回完整 `groupIds`、容量、熔断剩余、最后错误、最近样本并统一补足 12 项。
- 修改探活处理:每次显式探活写入样本,保留真实路径和 `requestAttempted`;不要把任务 attempts 临时聚合结果冒充探活样本。
- 修改 `task-worker.ts`:实现 healthy/degraded/open/half-open状态、60 秒默认冷却、单探测租约、成功恢复和失败重开。
- 修改 `admin/src/pages/Business/components/Charts.tsx` 或新的健康私有组件:每一行固定 12 根竖条,颜色和当前状态分开渲染;不能使用 `safeRows.slice(0, 12)`代替样本序列。
**目标结果**:健康页能同时回答“过去 12 次/时间槽发生了什么”和“当前是否允许调度”;多个分组归属完整显示。
**测试**:空样本/1样本/12样本/超过12样本;groupIds完整;容量竞争;半开单租约;30秒旧逻辑不再生效;停用状态不参与调度;探活和任务样本来源区分。
**依赖**:P3、P4;P6 的候选排序需要健康和容量快照。
### P6:智能调度、严格切换和任务执行
**目标**:把“优先级”和“智能调度”变成可证明的纯逻辑。
**代码改法**:
- 在 `server/src/jobs` 或同目录新增纯函数,负责候选过滤、过滤原因和同优先级排序;输入使用任务 routeSnapshot、渠道快照、健康快照和租约容量。
- 修改 `task-worker.ts`:使用上述纯函数,不再直接遍历所有渠道后只过滤 `enabled/open`;候选为空时不发上游并返回 `MODEL_GROUP_UNAVAILABLE`。
- 明确 `ProviderResult.retryable` 的切换判断;移除对 `PROVIDER_AUTH`、`PROVIDER_SECRET_UNAVAILABLE`等不可重试错误的无条件切换。
- 保持 routeSnapshot优先,排除当前任务已失败渠道,按总预算和渠道预算控制 attempts。
- 把渠道、套餐、系统的有效并发统一到 durable lease;跨进程/跨实例不能只看内存 Set。
**目标结果**:严格模式按优先级层和层内顺序执行;智能模式只在同一优先级层内择优;低优先级不能越级;未知结果不会被错误当作可重试失败。
**测试**:健康/容量/映射/能力/熔断/已失败渠道过滤;同优先级稳定排序;低优先级越级保护;无候选不上游;retryable与non-retryable切换;并发竞争和租约过期;routeSnapshot不受配置后改影响。
**依赖**:P4、P5;P7 对账依赖任务状态和输出校验边界。
### P7:人工对账和输出安全校验
**目标**:保证未知结果不会重复扣费、误退款或接受不完整输出。
**代码改法**:
- 修改 `server/src/app/http.ts` 对账接口:超额金额 422;任务状态、版本、幂等和 count严格校验;人工成功支持与任务 count匹配的输出集合。
- 抽取 provider 输出校验 helper,覆盖任务类型、数量、MIME、大小、尺寸、图片内容和 staging引用;自动查询尚未有真实 provider能力时不启用。
- 修改事务边界:任务、账本、对象引用和审计在同一事务或可靠 outbox中完成;staging失败路径清理。
- 修改现有“clamps the charge”测试为“rejects over-reservation with 422”,并补 0、等于预留、负数、小数、重复请求和并发对账案例。
- 管理台任务中心增加未知 Tab、对账弹窗、预留金额提示和中文失败原因;失败保留草稿。
**目标结果**:unknown永远由人工确认或明确的未来 provider 查询能力结算;超额输入不会被静默改写;不完整输出不会变成成功任务。
**测试**:金额边界、count>1、错误 MIME/尺寸/内容、事务回滚、staging清理、审计原子性、并发对账和预留过期竞争。
**依赖**:P1、P6;OpenAI 图片的自动 query不作为本阶段完成条件。
### P8:后台模块化、分页、审计和文档收口
**目标**:让已经正确的后端逻辑在后台页面中清晰、可维护、可验收。
**代码改法**:
- 将 `admin/src/pages/Business/index.tsx` 中的巨型 `sections`、字段定义、请求和弹窗按职责拆到同目录私有模块;保留统一布局、TDesign Provider和现有 Lineicons,不做无关视觉重写。
- 每个模块建立独立的列表列定义、过滤参数、请求代次、loading/empty/error状态和弹窗草稿;避免通过无名 `FormItem`覆盖受控 value。
- 把管理 API 改为 `AdminPage` 服务端分页;前端保留筛选条件和请求代次,旧响应不得覆盖新页面。
- HTTP 请求结果增加真实状态字段,审计动作增加明确 outcome;详情经过安全白名单投影;大板只统计真实请求状态和业务结果,不按 action 猜测。
- 检查所有新增/编辑/确认/提示弹窗的取消、右上角、遮罩和 ESC;异步成功关闭,失败保留草稿。
- 更新 `CHANGELOG.md` 的 Unreleased;同步 `todo.mdx/todo.zh-CN.mdx` 和 `pending-test.mdx/pending-test.zh-CN.mdx`;用户浏览器确认后再从 pending-test迁移到 `features.mdx/features.zh-CN.mdx`。
**目标结果**:后台 8 个业务模块边界清晰,列表数据不会全量加载,敏感详情不会原样输出;文档准确区分已实现和待人工测试。
**测试**:admin typecheck、Lint、列表响应契约、分页/竞态、详情脱敏、弹窗状态静态检查;不执行浏览器。
**依赖**:P2-P7对应 API稳定后执行;文档状态不能早于实际代码状态。
## 7. 文件级修改清单
以下是批准后允许触碰的主要文件;实际执行仍以每阶段需要为准,不得顺手改无关文件。
### 服务端和契约
- `packages/contracts/src/index.ts`:公开模型、渠道模型、路由快照、价格快照、健康样本、管理分页和审计投影。
- `server/src/store.ts`:Store字段、默认 fixture、snapshot schema、健康/价格/路由结构。
- `server/src/app/http.ts`:管理 API、任务创建、价格解析、渠道属性隔离、健康、对账和审计响应。
- `server/src/adapters/provider.ts`:请求模型解析、超时、探活错误阶段、输出校验边界和 provider能力声明。
- `server/src/jobs/task-worker.ts`:候选过滤、智能排序、租约、熔断、严格切换和结算。
- `server/src/infra/repository.ts`:snapshot保存/恢复、事务或 outbox边界。
- `server/migrations/*.sql`:只有确实需要的 schema/snapshot版本变更;不创建与当前 JSON仓储无关的假实体表。
- `server/tests/*.test.ts`:每阶段的纯函数、契约、HTTP、事务、provider和 worker 回归。
### 管理后台
- `admin/src/pages/Business/index.tsx`:路由分发和业务模块组装,逐步移除巨型字段/弹窗逻辑。
- `admin/src/pages/Business/index.module.less`:必要的列表、12根趋势条、窄屏和状态布局样式。
- `admin/src/pages/Business/components/Charts.tsx`:健康趋势等图表/列表展示;不能复用渠道数量切片表达样本。
- `admin/src/services/platform.ts`:接口封装、错误分类、If-Match、幂等和分页参数。
- `admin/src/router/index.ts`:只有确实需要真实独立页面时才调整;现有 URL优先保持稳定。
- `admin/src/components/LineIcons.jsx`:只复用已有本地图标,不增加外部图标资源。
### 画布和用户端禁改集合
- `web/src/pages/canvas/**`
- `web/src/components/canvas/**`
- `web/src/stores/canvas/**`
- `web/src/lib/canvas/**`
## 8. 代码级验收
### 8.1 每阶段必须验收
- 契约能通过类型检查,旧字段没有新的写入点。
- 任务创建和 worker 的实际调用链与目标规则一致,不只检查页面文案。
- 管理写接口具备权限、版本、幂等、事务和审计边界。
- 敏感字段不出现在公开响应、管理详情、审计详情和错误日志中。
- 没有修改画布禁改集合,没有新增展示 mock 渠道。
### 8.2 推荐命令
批准并完成代码后,按仓库脚本执行:
```text
npm --prefix server run typecheck
npm --prefix server test
npm --prefix admin run typecheck
npm --prefix admin run lint
git diff --check
```
如修改共享契约、构建入口、分页组件或管理路由,再增加:
```text
npm --prefix web run typecheck
npm --prefix admin run build
npm --prefix web run build
npm run build:all
```
本阶段不执行 `npm run dev:all`、浏览器启动、Playwright、截图或模拟操作。
### 8.3 关键验收清单
- [ ] 模型页面和 body没有价格、数量上限、并发上限、渠道组 ID、逗号能力。
- [ ] 价格规则独立保存,任务快照包含规则 ID和版本。
- [ ] 一个公开模型最多一个启用分组;分组按模型保存渠道关系。
- [ ] `publicModelId`和`requestModelId`全链路分离;只有渠道模型清单中的同名精确匹配或显式映射才能得到上游请求 ID。
- [ ] 渠道保存/编辑/切 Tab/刷新不请求供应商;只有主动发现和探活发起上游请求。
- [ ] 渠道 PATCH不改变任何分组顺序;排序拒绝遗漏、重复、未知和越组成员,并使用 If-Match。
- [ ] 健康接口返回完整 `groupIds`、容量、熔断剩余和固定 12 个样本;页面固定显示 12 根彩色竖条。
- [ ] 熔断有 healthy/degraded/open/half-open/disabled语义和单探测租约。
- [ ] 严格优先级不会被智能调度越级;智能排序只发生在同一最低可用 `priorityTier`。
- [ ] 只有明确 retryable失败才切换;可能到达上游的未知结果停止切换和退款。
- [ ] unknown不被普通过期任务自动退款;OpenAI 图片自动查询不被误报为已实现。
- [ ] 人工对账超额金额返回 422,不静默截断;count、MIME、大小、尺寸和内容完整校验。
- [ ] 对账任务、账本、输出引用和审计具备原子事务或可靠 outbox;失败清理 staging。
- [ ] HTTP 趋势来自真实请求状态,审计动作有明确 outcome;详情使用白名单投影,不返回原始 `before/after`。
- [ ] 管理列表使用服务端分页和请求代次,旧响应不能覆盖新筛选。
- [ ] 测试 fixture只存在于测试模式,生产无展示 mock渠道。
- [ ] 画布禁改集合无 diff。
## 9. 审批后的阶段执行提示词
以下提示词只能在负责人明确批准本计划后使用,并且必须在当前任务中逐阶段执行;不得创建、派生或转交到新的用户任务。每完成一个阶段先报告代码级结果,确认没有跨阶段遗留破坏,再进入下一阶段。
### 9.1 P1 契约和基础模型
```text
在 /Users/qiu/Desktop/MiragenFlow 当前任务中执行 plan.md 的 P1,不创建新任务,不修改画布。先读 AGENTS.md、plan.md、packages/contracts/src/index.ts、server/src/store.ts、server/src/infra/repository.ts、server/src/app/http.ts 中 createTask 和现有持久化测试。先建立独立 PricingRule、渠道模型、按公开模型保存的 ChannelGroup route、健康样本和新版任务快照契约,再把 store/repository/createTask 调整到新结构。一个公开模型最多属于一个启用分组;项目未上线,不写旧字段双读兼容,但旧 snapshot 必须明确拒绝或按审批方案重新初始化。只改 P1 必需文件,补保存恢复、重复归属、价格版本和旧字段拒绝测试。完成后只做代码级验收并报告,不启动浏览器。
```
### 9.2 P2 模型和价格
```text
在当前任务执行 plan.md 的 P2。先读 P1 最终类型和测试,再读 admin/src/pages/Business/index.tsx、admin/src/services/platform.ts、server/src/app/http.ts 的 model-products、pricing-rules 和 createTask。模型管理只保留公开 ID、名称、档位、能力多选、结构化能力参数、分辨率和发布状态,删除价格、数量、并发、渠道组 ID、逗号能力和分辨率价格倍率。价格规则建立独立实体、数量阶梯、生效区间、版本和 If-Match;任务只能通过 resolvePricingRule 生成 pricingSnapshot。保留现有 TDesign 和 Lineicons,不改画布。补上架缺路由、价格重叠、版本冲突和历史任务快照测试,代码级验收后停止报告。
```
### 9.3 P3 渠道和模型映射
```text
在当前任务执行 plan.md 的 P3。先读渠道页面、platform service、ProviderChannel、渠道 API、provider adapter 和安全测试。渠道保存只处理连接、加密凭证、timeoutMs、启用、渠道模型和可选映射,不得隐式请求上游。模型发现只能由显式按钮触发 /v1/models,并返回实际路径、requestAttempted 和结构化错误阶段。拉取候选与模型映射必须独立;同名精确匹配只在渠道模型清单已登记该 ID 时允许,ID不同必须显式 displayModelId -> requestModelId,删除 providerModelId 和任意首模型 fallback。渠道普通 PATCH 不得改任何分组顺序。补未发起/DNS/TLS/HTTP/JSON/data空、映射和顺序隔离测试。只做代码级验收,不启动浏览器。
```
### 9.4 P4 分组模型路由
```text
在当前任务执行 plan.md 的 P4。将 ChannelGroup 改为按 publicModelId 保存 routes,每个渠道成员保存 priorityTier、orderWithinTier 和 retryBudget。一个公开模型最多属于一个启用分组;一个分组可以管理多个模型;同一渠道可在不同模型或不同分组中有独立优先级。分组保存校验模型、渠道启用、协议、模型覆盖和映射;排序 API 按模型 route 检查成员集合完全一致,拒绝重复、遗漏、未知、越组和层内顺序冲突,使用 If-Match。任务创建固化唯一 routeSnapshot。管理台使用模型路由矩阵和纯函数故障切换预览。补多分组独立顺序、渠道 PATCH 不影响顺序、版本冲突和快照不变测试,不改画布。
```
### 9.5 P5 健康和熔断
```text
在当前任务执行 plan.md 的 P5。建立独立 ChannelHealthSample 和 durable 容量/探测租约,健康接口返回完整 groupIds、当前状态、成功率、P95、容量、连续失败、熔断剩余、最后探活和固定 12 个 samples。每个渠道页面固定显示 12 根竖条:绿成功、黄慢/降级、红失败、灰无数据、蓝探活中;趋势与当前调度资格分开。实现 healthy/degraded/open/half-open,默认 60 秒冷却和单 half-open 探测租约;停用独立表达。不要把前 12 个渠道或所有历史 attempts 冒充 12 个探活样本。补样本补齐、groupIds、容量竞争、半开单租约和恢复测试,只做代码级验收。
```
### 9.6 P6 智能调度
```text
在当前任务执行 plan.md 的 P6。把候选过滤和排序抽成纯函数,输入 routeSnapshot、渠道/健康/容量快照和当前任务 attempts;输出有序候选与每个被过滤渠道的机器可读原因。严格模式按 priorityTier、orderWithinTier;智能模式只在数值最小的可用 priorityTier 内按健康、可用容量、P95、成功率和稳定 ID排序,低优先级不能越级。worker 只对 status=failed 且 retryable=true 的可确认失败切换;认证、密钥、模型未配置和可能到达上游的 transport unknown 不得盲目切换。统一 durable 租约并发事实。补所有过滤原因、稳定排序、越级保护、无候选不上游、重试预算和跨实例容量测试。
```
### 9.7 P7 人工对账
```text
在当前任务执行 plan.md 的 P7。OpenAI 图片查询能力仍标记不可用,unknown 保留预留,不自动查询、切换或退款。重写人工成功/失败对账:只接受 unknown 和匹配 If-Match 的任务;chargedAmount 必须在 0 到 reservedCost,超额返回 422;输出数量必须等于 count,并校验任务类型、MIME、大小、尺寸和图片内容。幂等相同请求复用结果,不同 body 冲突。任务、账本、输出引用和审计在同一事务或可靠 outbox 中完成,失败清理 staging。把现有 clamps 测试改为超额拒绝,并补多输出、事务回滚和并发对账测试。管理台在任务中心 unknown Tab 中提供对账弹窗,不另造无关系页面。
```
### 9.8 P8 后台收口
```text
在当前任务执行 plan.md 的 P8。只在 P2-P7 API 稳定后拆分 admin/src/pages/Business/index.tsx:各业务模块使用同目录私有组件/配置,保留现有 TDesign、共享 Provider 和本地 Lineicons,不引入新 UI 库。所有列表改用 AdminPage 服务端分页,前端使用 AbortController 和请求代次防旧响应回写;所有弹窗支持取消、右上角、遮罩和 ESC,失败保留草稿。HTTP 请求结果保存真实 statusCode,审计动作保存 outcome,详情使用白名单字段和递归脱敏。更新 CHANGELOG、双语 todo 和 pending-test;浏览器由负责人验收,未确认内容不写入正式 features。执行完整代码级命令并报告所有剩余风险。
```
## 10. 交付口径和审批后执行规则
每次阶段交付必须分别报告:
1. 已修改的文件、符号和接口。
2. 已实现且代码级验收通过的规则。
3. 已实现但尚未由负责人浏览器确认的交互。
4. 尚未实现、必须进入 `todo.mdx/todo.zh-CN.mdx` 的事项。
5. 实际执行的命令及结果,未执行的浏览器验收明确列出。
完成度百分比只能按“阶段目标和验收条件都通过”的交付项计算;计划、ASCII草图、架构图、文档和未人工测试页面不能算完成。
审批前停止在此,不修改代码,不启动服务,不打开浏览器,不另开线程。