# EasyAI AI 网关中台设计文档

## 1. 建设目标

将现有 `easyai-server-main` / `easyai-integration-api` 中与 `integration-platform` 强相关的能力拆分为一个独立项目：

- 独立运行：后端 Go、复用 Agent memory 的 PostgreSQL 18（`easyai-pgvector`）、前端 React + TSX。
- 前端 UI：使用 `shadcn-ui` + Tailwind CSS + Radix UI + `lucide-react`，保持控制台形态与 EasyAI 运维后台一致。
- 独立部署：HTTP API、任务执行器、任务事件持久化与进度回调均由本服务承载；接入 EasyAI 主站时，业务前端实时推送继续复用 `server-main` 现有 WebSocket 网关。
- 身份双模式：支持 Gateway 本地账号注册登录，也支持沿用 `easyai-server-main` 的 JWT claim、角色权限和 OpenAPI `sk-*` 校验方式。
- 能力对接：作为 Chat、生图、生视频等模型能力网关，供 `easyai-server-main` 和前端直接调用。
- 渐进迁移：新服务稳定后，再让 `server-main` 的 `OpenaiService` / `IntegrationPlatformService` 切为远程调用门面。

## 2. 迁移边界

### 2.1 迁入 AI Gateway

| 能力 | 新服务职责 |
| --- | --- |
| 平台管理 | 平台 CRUD、启停、优先级、凭证配置、可用性检测、默认折扣系数 |
| 基准模型库 | 维护全量 provider、基准模型、模型能力、图像/视频/文本能力标签、基准定价和默认限流模板 |
| 模型管理 | 平台模型、模型类型、能力标签、上下文窗口、计费配置；支持从基准模型继承、按折扣继承或自由覆盖 |
| 模型路由 | 复用现有 `assignClientsByModelName` 的平台过滤、并发负载、优先级、队列等待语义 |
| 生成任务 | Chat、图片生成、图片编辑、视频生成、语音、Embedding 等任务入口 |
| 队列执行 | 按平台/模型/方法维度做并发控制、等待队列、超时、重试；队列状态必须持久化，服务异常重启后可恢复 |
| 测试模式 | 支持 simulation / dry-run，不向真实平台提交任务，仍完整走路由、队列、限流、重试、进度、结果归一流程 |
| 计费预估 | 在任务创建前或前端配置面提供 estimated billing，且必须使用与真实路由一致的有效模型价格 |
| 任务结果 | 保存任务请求、状态、结果摘要、billings，向主服务发送结算事件 |
| 推送 | 承接任务级进度事件，提供类似原 RxJS Observable 的中间过程返回能力；业务实时进度通过配置回调地址回调 `server-main` 内部任务进度接口，再由主服务复用原 WebSocket 网关推给业务前端 |

### 2.2 接入 server-main 时仍保留在 server-main

| 能力 | 原因 |
| --- | --- |
| 用户、组织、租户、角色 | EasyAI 主站模式下仍是主账号体系；Gateway 保存同步副本和策略快照 |
| 余额、资源包、扣费流水 | 需要复用现有账单锁、组织扣费、消费记录 |
| API Key 创建与撤销 | 接入模式下 `sk-*` 生命周期属于主服务用户体系；独立模式由 Gateway 本地 API Key 模块承接 |
| 文件上传 | 复用 `server-main` 已开放的文件上传接口，OSS/COS/S3 密钥和上传实现继续只落在主服务 |
| 业务 WebSocket 网关 | 现有前端已经订阅主服务 WebSocket 通道，Gateway 通过任务进度回调接入，不直接替换业务推送链路 |
| 对话、绘图历史、工作流历史 | 与产品域模型绑定，Gateway 只返回任务结果和结算载荷 |

## 3. 总体架构

```mermaid
flowchart LR
  subgraph client [调用方]
    FE[React / easyai-main-web]
    MAIN[easyai-server-main]
    OPENAPI[OpenAPI Client]
  end

  subgraph gw [API Gateway / Ingress]
    HTTP[HTTP Routes]
    PUSH[SSE / WebSocket Routes]
  end

  subgraph aigw [EasyAI AI Gateway]
    API[Go HTTP API]
    AUTH[Auth Middleware]
    ROUTER[Model Router]
    QUEUE[Queue + Runtime]
    CALLBACK[Task Progress Callback]
    VENDOR[Vendor Clients]
    ADMIN[React Admin Console]
  end

  subgraph data [Data]
    PG[(Agent memory PostgreSQL<br/>easyai-pgvector)]
    REDIS[(Redis later)]
  end

  subgraph servermain [server-main]
    USER[User / Org]
    APIKEY[API Key Verify]
    WSGW[WebSocket Gateway]
    BILL[Billing + Ledger]
    FILES[Open File Upload]
  end

  FE --> HTTP --> API
  FE <-->|business progress ws| WSGW
  ADMIN --> PUSH
  OPENAPI --> HTTP --> API
  MAIN -->|internal HTTP| API
  ADMIN --> API
  API --> AUTH
  AUTH -->|JWT local verify| API
  AUTH -->|sk-* delegate| APIKEY
  API --> ROUTER --> QUEUE --> VENDOR
  API --> PG
  QUEUE --> PG
  QUEUE --> CALLBACK -->|POST task progress callback to server-main| WSGW
  QUEUE -->|settlement event| BILL
  API -->|POST /v1/files/upload| FILES
```

## 4. Monorepo 方案

本脚手架采用 `Nx + pnpm + go.work`：

- `Nx` 负责任务编排、缓存、前后端统一命令：`dev`、`build`、`test`、`migrate`。
- `go.work` 管理 Go 模块，避免把 Go 项目硬塞进 Node 包管理。
- `pnpm-workspace.yaml` 管理 React 前端与 TypeScript contracts。
- 前端控制台使用 `shadcn-ui` 作为组件体系，配套 Tailwind CSS、Radix UI primitive、`lucide-react` 图标和 `class-variance-authority` 管理组件变体。

选择这个组合的原因：

- 与 EasyAI 现有 Nx 使用习惯接近，便于团队统一命令。
- Go 后端保持 Go 原生模块边界，后续可以拆 `apps/worker`、`libs/go/*`。
- 前端仍可复用 React 生态和 TSX 风格，contracts 可给前端、SDK、BFF 共用。
- `shadcn-ui` 代码是可拷贝进仓库的 TSX 组件，不绑定运行时 UI 框架，适合后续沉淀平台管理、队列监控、模型计费配置等复杂表单。

### 4.1 前端 UI 约束

- 基础组件统一从 `apps/web/src/components/ui/*` 引入，不直接散落自定义 button/input/table 样式。
- 首期需要引入并固化：`Button`、`Input`、`Textarea`、`Select`、`Switch`、`Checkbox`、`Tabs`、`Dialog`、`Sheet`、`Table`、`Badge`、`Alert`、`Toast/Sonner`、`Tooltip`、`Progress`、`DropdownMenu`、`Form`。
- 图标统一使用 `lucide-react`，按钮类动作优先用语义图标加 tooltip。
- 控制台不是营销页，布局以高密度管理视图为主：平台列表、模型配置、队列监控、任务详情、客户端运行状态、限流配置、重试策略。
- 主题 token 以 CSS variables 管理，后续可与 EasyAI 全局后台色板对齐。

## 5. 授权设计

### 5.1 身份模式与 JWT 用户授权

Gateway 支持三种身份模式：

- `standalone`：Gateway 本地账号注册登录，签发 Gateway JWT。
- `server-main`：只接受 `server-main` JWT / OpenAPI `sk-*` 校验结果，用户与租户从主服务同步。
- `hybrid`：同时接受 Gateway 本地账号和 `server-main` 身份，按 `source` 区分。

接入 `server-main` 时兼容主服务 JWT：

- secret：读取 `CONFIG_JWT_SECRET`，默认值与现有 `jwtConstants.secret` 保持一致。
- token 有效期：由 `server-main` 签发控制，当前 access token 为 `600s`，refresh token 为 `7d`。
- claim 兼容：
  - `sub`：用户 ID
  - `username`
  - `role`
  - `tenantId`
  - `gatewayTenantId`
  - `tenantKey`
  - `source`
  - `gatewayUserId`
  - `userGroupId` / `userGroupKey` / `userGroupKeys`
  - `sso_id`
  - API Key 场景扩展：`apiKeyId`、`apiKeySecret`、`apiKeyName`

Gateway 本地登录首期只签发短期 access token，不做 refresh token；接入 `server-main` 的刷新仍走主服务。

### 5.1.1 本地账号注册登录

首期普通账号只做用户名/邮箱 + 密码：

| Method | Path | Permission | 说明 |
| --- | --- | --- | --- |
| `POST` | `/api/v1/auth/register` | `public` | 注册 Gateway 本地账号，进入默认租户；邀请码仅记录邀请关系，返回 access token |
| `POST` | `/api/v1/auth/login` | `public` | 本地账号登录，返回 access token |

注册字段：

- `username` / `email`：至少一个必填。
- `password`：至少 8 位，落库为 bcrypt hash。
- `displayName`：可选。
- `invitationCode`：可选；填写后只校验并记录邀请关系，不改变租户、用户组或角色。

安全边界：

- 普通注册界面不展示、不提交租户 Key 和租户名称，单租户场景下后端创建或复用 `tenant_key=default` 的默认租户。
- 普通注册首期默认给 `user` 角色；首次初始化用户可作为本地闭环的全局管理员引导账号。
- 多租户加入后续通过域名校验、管理员审批或 `server-main` 同步等受控入口完成，不由普通注册邀请码决定。
- `server-main` 模式可关闭本地注册登录，只保留外部 token 入口。

### 5.2 权限等级

权限枚举与 `server-main` 保持一致：

| Permission | 用途 |
| --- | --- |
| `public` | 健康检查、公开模型列表等 |
| `basic` | 普通用户任务创建、任务查询 |
| `creat` | 创作者级能力 |
| `power` | 平台配置、模型配置、计费配置 |
| `manager` | 超级管理与危险操作 |

角色映射与 `server-main` 一致：`user`、`creator`、`operator`、`manager`、`admin`。

### 5.3 OpenAPI API Key

对于 `Authorization: Bearer sk-*` 或 `x-comfy-api-key`：

1. Gateway 不直接持有 API Key hash。
2. Gateway 调用 `server-main` 的内部校验接口。
3. `server-main` 返回标准用户 claim。
4. Gateway 将 claim 放入请求上下文，后续任务、结算事件都带 `apiKeyId` / `apiKeyName`。

建议在 `server-main` 增加内部接口：

```http
POST /internal/platform/auth/verify-api-key
Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN}
Content-Type: application/json

{ "apiKey": "sk-..." }
```

### 5.4 内部服务授权

`server-main` 调 Gateway 内部接口时使用服务令牌：

```http
Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN}
X-EasyAI-Actor: easyai-server-main
```

内部令牌只用于服务间调用，不替代用户上下文。用户发起的任务仍应透传用户 JWT 或 API Key claim。

## 6. 接口规范与兼容路由

### 6.1 兼容优先原则

AI Gateway 的外部接口必须以“兼容现有 EasyAI 路由、请求 DTO、响应结构、错误码、流式语义”为第一目标，不能要求前端或 `server-main` 因拆分服务而大面积改调用路径。

- **管理面兼容**：沿用原 `integration-platform`、`integration/platform-api`、`integration-platform/snapshots` 路由。
- **站内生成接口兼容**：沿用原 `OpenaiController` 根路径接口，例如 `/chat/completions`、`/images/generations`。
- **OpenAPI 兼容**：沿用 `/v1/*` 路由，例如 `/v1/chat/completions`、`/v1/images/generations`，API Key 语义与原来一致。
- **新增接口隔离**：`/api/v1/*` 只用于 Gateway 自身控制台、调试、内部 SDK 或新能力，不作为迁移期替代原路由。
- **响应包装**：对原来返回 `ResOp` 的管理接口继续返回同形态；对 OpenAI-compatible 接口保持原 JSON / stream chunk 结构；对任务类接口保持原 `taskId`、`status`、`url`、`billings` 等字段语义。
- **路径匹配顺序**：迁移 `integration-platform` 时必须保留静态路径优先于 `:id` / `:action/:id` 的规则，避免 `models/list/enabled`、`dynamic-priority/:id`、`scene-script/:id/partial` 被动态路由误吞。

### 6.2 平台管理兼容路由

| Method | Path | Permission | 说明 |
| --- | --- | --- | --- |
| `POST` | `/integration-platform` | `power` | 创建平台 |
| `POST` | `/integration-platform/preset` | `power` | 创建预置平台 |
| `GET` | `/integration-platform` | `power` | 平台列表 |
| `GET` | `/integration-platform/list/ops-compact` | `power` | 运维精简列表 |
| `GET` | `/integration-platform/queue-stats` | `power` | 队列统计 |
| `POST` | `/integration-platform/clear-queue/:platformId` | `power` | 清理平台队列 |
| `POST` | `/integration-platform/find-by-ids` | `power` | 按 ID 批量查平台 |
| `POST` | `/integration-platform/find-by-model` | `basic` | 按模型查候选平台 |
| `GET` | `/integration-platform/models/list/enabled` | `basic` | 启用模型列表 |
| `GET` | `/integration-platform/models/list/all` | `power` | 全量模型列表 |
| `PATCH` | `/integration-platform/dynamic-priority/:id` | `power` | 动态优先级 |
| `DELETE` | `/integration-platform/dynamic-priority/:id` | `power` | 删除动态优先级 |
| `POST` | `/integration-platform/disable/:id` | `power` | 禁用平台 |
| `POST` | `/integration-platform/enable-model/:id` | `power` | 启用平台模型 |
| `PATCH` | `/integration-platform/scene-script/:id/partial` | `power` | 局部更新场景脚本 |
| `PATCH` | `/integration-platform/:action/:id` | `power` | 兼容启停等 action |
| `PATCH` | `/integration-platform/:id` | `power` | 更新平台 |
| `DELETE` | `/integration-platform/:id` | `manager` | 删除平台 |
| `GET` | `/integration-platform/models/:type` | `basic` | 按类型查模型 |
| `GET` | `/integration-platform/models/strict/:type` | `basic` | 按类型严格查模型 |
| `POST` | `/integration-platform/models/estimatedBilling` | `basic` | 预估扣费 |
| `POST` | `/integration-platform/models/estimatedBilling/:workflowId` | `basic` | 工作流预估扣费 |
| `POST` | `/integration-platform/resetException/:id` | `power` | 重置异常状态 |
| `POST` | `/integration-platform/:id/copy` | `power` | 复制平台 |
| `GET` | `/integration-platform/:id` | `power` | 平台详情 |

兼容表中的旧权限用于保持原接口语义；在新 Gateway 第一阶段，所有写入 provider / platform 凭证、启停平台、删除平台、复制平台的接口都要额外要求全局管理员，即 `manager/admin` 角色。

### 6.3 平台 API 配置兼容路由

| Method | Path | Permission | 说明 |
| --- | --- | --- | --- |
| `POST` | `/integration/platform-api` | `power` | 创建 API 配置 |
| `GET` | `/integration/platform-api` | `power` | API 配置列表 |
| `GET` | `/integration/platform-api/:id` | `power` | API 配置详情 |
| `PATCH` | `/integration/platform-api/:id` | `power` | 更新 API 配置 |
| `DELETE` | `/integration/platform-api/:id` | `manager` | 删除 API 配置 |
| `POST` | `/integration/platform-api/execute` | `power` | 调试执行 |
| `GET` | `/integration-platform/snapshots/platform/:platformId` | `power` | 平台快照 |

### 6.4 站内生成兼容路由

| Method | Path | 说明 |
| --- | --- | --- |
| `GET` | `/models/list` | 站内模型列表 |
| `POST` | `/chat/completions` | 站内 Chat |
| `POST` | `/chat/completions/cancel/:requestId` | 取消 Chat |
| `POST` | `/chat/structured-output` | 结构化输出 |
| `POST` | `/images/generations` | 生图 |
| `POST` | `/images/edits` | 图片编辑 |
| `POST` | `/video/generations` | 生视频 |
| `POST` | `/embeddings` | Embeddings |
| `GET` | `/embeddings/models` | Embedding 模型 |
| `GET` | `/ai/result/:taskId` | 任务结果 |

### 6.5 OpenAPI 兼容路由

| Method | Path | 说明 |
| --- | --- | --- |
| `POST` | `/v1/chat/completions` | OpenAI-compatible Chat |
| `POST` | `/v1/chat/completions/cancel/:requestId` | 取消 Chat |
| `POST` | `/v1/images/generations` | 生图 |
| `POST` | `/v1/images/edits` | 图片编辑 |
| `POST` | `/v1/video/generations` | 生视频 |
| `GET` | `/v1/ai/result/:taskId` | 任务结果 |
| `GET` | `/v1/ai/cancel/:taskId` | 取消任务 |
| `POST` | `/v1/embeddings` | Embeddings |
| `GET` | `/v1/embeddings/models` | Embedding 模型 |
| `GET` | `/v1/models` | OpenAI-compatible 模型列表 |
| `GET` | `/v1/models/list` | EasyAI 模型列表 |

`/v1/balance`、`/v1/files/upload`、`/v1/creatToken`、`/v1/removeToken` 与用户余额、文件、API Key 生命周期强绑定，默认仍由 `server-main` 保留；若网关暴露同路径，只能做兼容代理或调用 `server-main` 内部接口。

### 6.6 Gateway 新增接口

| Method | Path | Permission | 说明 |
| --- | --- | --- | --- |
| `POST` | `/api/v1/auth/register` | `public` | Gateway 本地账号注册，`invitationCode` 可选 |
| `POST` | `/api/v1/auth/login` | `public` | Gateway 本地账号登录 |
| `GET` | `/api/v1/me` | `basic` | 网关当前身份调试 |
| `GET` | `/api/v1/public/catalog/providers` | `public` | 公开 provider 展示目录，供未登录模型页和 API 文档使用 |
| `GET` | `/api/v1/public/catalog/base-models` | `public` | 公开基准模型展示目录，不包含平台凭证 |
| `GET` | `/api/v1/catalog/providers` | `power` | 基准 provider 列表 |
| `GET` | `/api/v1/catalog/base-models` | `power` | 基准模型库列表 |
| `POST` | `/api/v1/catalog/base-models` | `manager` | 新增基准模型，支持写入能力、基准计费、默认限流和元数据 |
| `PATCH` | `/api/v1/catalog/base-models/:id` | `manager` | 修改基准模型 |
| `DELETE` | `/api/v1/catalog/base-models/:id` | `manager` | 删除基准模型 |
| `GET` | `/api/v1/tenants` | `power` | 网关租户列表，支持本地租户和 server-main 同步租户 |
| `GET` | `/api/v1/tenants/:id` | `power` | 租户详情、来源、策略和同步状态 |
| `POST` | `/api/v1/tenants/sync` | `power` | 从 `server-main` 拉取或接收租户同步 |
| `GET` | `/api/v1/invitations` | `power` | 本地注册邀请码列表 |
| `POST` | `/api/v1/invitations` | `power` | 创建本地注册邀请码 |
| `GET` | `/api/v1/users` | `power` | 网关用户列表，支持本地用户和 server-main 同步用户 |
| `GET` | `/api/v1/users/:id` | `power` | 用户详情、来源、角色、用户组和同步状态 |
| `POST` | `/api/v1/users/sync` | `power` | 从 `server-main` 拉取或接收用户同步 |
| `GET` | `/api/v1/user-groups` | `power` | 用户组策略列表 |
| `GET` | `/api/v1/user-groups/:id` | `power` | 用户组详情、成员与策略 |
| `POST` | `/api/v1/user-groups/:id/sync` | `power` | 同步用户组策略到 `server-main` |
| `GET` | `/api/v1/wallet/accounts` | `basic` | 独立模式本地余额账户 |
| `GET` | `/api/v1/wallet/transactions` | `basic` | 独立模式本地余额流水 |
| `GET` | `/api/v1/recharge/orders` | `basic` | 独立模式充值订单 |
| `POST` | `/api/v1/recharge/orders` | `basic` | 独立模式创建充值订单 |
| `GET` | `/api/v1/api-keys` | `basic` | 独立模式 API Key 列表 |
| `POST` | `/api/v1/api-keys` | `basic` | 独立模式创建 API Key |
| `PATCH` | `/api/v1/api-keys/:id/disable` | `basic` | 禁用本地 API Key |
| `POST` | `/api/v1/pricing/estimate` | `basic` | 使用 effective pricing resolver 做价格预估 |
| `GET` | `/api/v1/pricing/rules` | `power` | 定价规则列表 |
| `GET` | `/api/v1/pricing/rule-sets` | `power` | 定价规则集列表，包含规则集下的多条文本/图像/视频规则 |
| `POST` | `/api/v1/pricing/rule-sets` | `manager` | 新增定价规则集 |
| `PATCH` | `/api/v1/pricing/rule-sets/:id` | `manager` | 修改定价规则集和其下计价规则 |
| `DELETE` | `/api/v1/pricing/rule-sets/:id` | `manager` | 删除定价规则集；已绑定平台/模型需先迁移或按外键策略清空绑定 |
| `GET` | `/api/v1/tasks/:taskId` | `basic` | Gateway 任务详情 |
| `GET` | `/api/v1/tasks/:taskId/events` | `basic` | SSE 任务进度 |
| `GET` | `/api/v1/runtime/clients` | `power` | 客户端运行状态 |
| `GET` | `/api/v1/runtime/queues` | `power` | 队列与限流状态 |
| `GET` | `/api/v1/runtime/rate-limit-windows` | `power` | TPM/RPM 当前窗口与并发 lease 状态 |
| `POST` | `/api/v1/runtime/tasks/:taskId/replay` | `power` | 重放任务事件 |
| `POST` | `/api/v1/runtime/tasks/:taskId/callbacks/replay` | `power` | 重放任务进度回调 outbox |

### 6.7 内部接口

| Method | Path | 调用方 | 说明 |
| --- | --- | --- | --- |
| `POST` | `/internal/v1/settlements` | Gateway worker | 回调主服务结算失败时补偿 |
| `POST` | `${TASK_PROGRESS_CALLBACK_URL}` | Gateway worker | 任务进度回调到 `server-main` 内部任务进度接口，实际路径由配置决定 |
| `POST` | `/internal/platform/tenants/sync` | server-main | 同步租户增量、禁用状态和租户策略到 Gateway |
| `POST` | `/internal/platform/users/sync` | server-main | 同步用户增量、禁用状态、角色和用户组关系到 Gateway |
| `POST` | `/internal/platform/user-groups/sync` | server-main | 同步用户组、折扣和限流策略到 Gateway |
| `POST` | `/internal/v1/task-callbacks` | server-main | 迁移期主服务回写历史或任务绑定 |

## 7. 数据模型

数据模型按“基准模型库 -> 平台实例 -> 平台模型覆盖 -> 任务运行态”分层：

- 基准模型库保存 provider、canonical model、能力 schema、默认能力、基准价格、默认限流模板，是所有平台模型的 fallback。
- 创建平台时可以设置 `default_discount_factor`，平台模型默认按“基准价格 x 折扣系数”计算；没有任何平台侧配置时，能力和价格都 follow 基准模型。
- 平台模型可以覆盖能力、定价和限流。覆盖只作用于该平台模型，不能反向污染基准模型库。
- 租户是隔离域：独立模式下 Gateway 管理 `gateway_tenants`；接入模式下同步 `server-main` 租户/组织，任务、用户、平台可见性、限流与计费策略都需要带租户上下文。
- 用户是身份域：独立部署时 Gateway 自己维护 `gateway_users`、登录、API Key、余额、充值订单和钱包流水；接入 `server-main` 时 Gateway 保存用户同步副本和策略快照，认证、余额、充值、订单仍可由 `server-main` 作为事实源。
- 用户组是策略作用域：不同用户组可以绑定充值折扣、模型计费折扣、TPM/RPM/并发、队列优先级、API Key 配额等策略。独立模式由 Gateway 完整执行；`server-main` 模式下充值和余额执行归主服务，Gateway 执行模型调用侧策略。
- estimated billing、真实任务 billings、控制台价格预览必须走同一个 effective pricing resolver。

### 7.0 身份运行模式

Gateway 需要支持三种身份运行模式，默认配置为 `IDENTITY_MODE=hybrid`：

| 模式 | 配置 | 用户事实源 | API Key / 登录 | 用户组策略 | 适用场景 |
| --- | --- | --- | --- | --- | --- |
| Standalone | `IDENTITY_MODE=standalone` | Gateway `gateway_users` | Gateway 本地用户、密码/SSO、API Key | Gateway 维护并执行充值折扣、调用折扣、限流和并发 | 独立商业化或单独部署 |
| Server-main gateway | `IDENTITY_MODE=server-main` | `server-main` | JWT 复用主服务 secret，`sk-*` 调用主服务校验 | `server-main` 与 Gateway 同步；主服务执行充值/余额，Gateway 执行调用折扣和限流 | EasyAI 主站拆分后的 AI 网关 |
| Hybrid | `IDENTITY_MODE=hybrid` | Gateway + `server-main` | 两种方式并存，按 `source` 区分 | 按 `source + group_key` 合并，冲突以更高优先级策略为准 | 迁移灰度或私有化集成 |

本地注册规则：

- 普通注册默认开放，`invitationCode` 可选；不填邀请码时创建或复用 Gateway 本地默认租户 `tenant_key=default`，不允许普通用户在注册界面填写租户 Key 或租户名称。
- 填写邀请码时必须命中 `gateway_invitations` 的有效记录；邀请码只用于标识邀请关系、更新使用次数和写入用户注册 metadata，不参与租户、用户组或角色分配。
- 平台凭证、provider 凭证和全局模型配置暂时只允许全局管理员维护；租户侧先只做模型使用、用量查看和策略展示。

身份解析流程：

1. 从 JWT / API Key / 内部调用 token 解析出外部身份。
2. 根据 `source + external_user_id` 在 `gateway_users` 找到或创建同步副本。
3. 按用户、租户、API Key、组织命中 `gateway_user_group_memberships`。
4. 根据用户组优先级和策略合并规则得到 effective policy。
5. 创建任务时把 `gateway_user_id`、`user_source`、`user_group_id`、`user_group_key`、`user_group_policy_snapshot` 写入 `gateway_tasks`，后续重试和结算不受同步变更影响。

Auth Center OIDC 用户采用受控 JIT 本地投影：Token 完成全部安全校验和角色授权后，Gateway 才按 `source=oidc + external_user_id=sub` 解析或幂等创建 `gateway_users` 记录。首次创建、默认用户组、`resource` 钱包和脱敏审计在同一事务完成；不按邮箱或昵称关联历史用户，不自动创建租户，也不向认证中心 Claims 写入 Gateway 本地 ID。详细配置与错误语义见 `docs/oidc-jit-provisioning.md`。

Gateway Web Console 默认使用 HttpOnly OIDC Cookie 会话解决标签页隔离问题：Access Token 仍由 Auth Center 签发，Gateway 不二次签发 JWT；前端建立 Cookie 后不再持久化 OIDC Access Token。Cookie 共享到同域标签页，有效期不超过原 Token，并通过 SameSite、Secure、精确 CORS 和写请求 Origin 校验控制 CSRF 风险。

### 7.0.1 多租户模型

多租户支持不能只停留在 claim 的 `tenantId` 字符串，Gateway 需要有自己的租户表和执行上下文：

- `gateway_tenants` 保存租户事实或同步副本，使用 `source + external_tenant_id` 幂等同步。
- `gateway_users.gateway_tenant_id` 关联 Gateway 租户，`tenant_id` / `tenant_key` 保留与 `server-main` 兼容的外部标识。
- `gateway_tasks` 固化 `gateway_tenant_id`、`tenant_id`、`tenant_key`，确保任务恢复、重试、结算和审计不受后续租户变更影响。
- `integration_platforms.visibility_scope` 区分 `global`、`tenant`、`private`，租户专属平台只能被对应租户路由到。
- 限流、定价、用户组和 quota 都可以以租户作为 scope；同一请求会同时命中 global、tenant、user_group、user、api_key 等策略。
- 控制台数据查询默认按当前用户租户过滤；`power/manager` 可跨租户查看和管理。

### 7.1 `integration_platforms`

保存平台实例与凭证：

- `provider`：平台类型，如 `openai`、`runninghub`、`jimeng`。
- `name`：运营可识别名称。
- `base_url`
- `auth_type`
- `credentials`：加密后的凭证 JSON，后续应接入 KMS。
- `config`：限流、超时、重试、平台私有配置。
- `visibility_scope`：`global`、`tenant`、`private`，用于多租户平台可见性。
- `tenant_id` / `tenant_key`：租户专属平台的归属。
- `default_discount_factor`：平台默认折扣系数，基于基准模型价计算平台有效价。
- `priority`
- `status`

### 7.2 `platform_models`

保存平台模型配置：

- `platform_id`
- `base_model_id`：关联基准模型库，未配置能力/价格时 follow 基准模型。
- `model_name`
- `model_type`：`chat`、`image`、`video`、`audio` 等。
- `capabilities`：平台模型有效能力，来自基准模型能力与平台覆盖合并。
- `pricing_mode`：`inherit`、`inherit_discount`、`custom`。
- `discount_factor`：模型级折扣，未设置时使用平台默认折扣。
- `billing_config`：平台模型有效计费配置，来自基准价、折扣和自定义覆盖。
- `enabled`

### 7.3 `gateway_tasks`

保存任务请求与状态：

- `kind`：`chat.completions`、`images.generations`、`videos.generations`
- `user_id`：请求 claim 中的用户 ID，独立模式为 Gateway 用户 ID，接入模式为 `server-main` 用户 ID。
- `gateway_user_id`：Gateway 本地用户表 ID，用于关联同步副本和审计。
- `user_source`：`gateway`、`server-main`、`sync`。
- `tenant_id`
- `user_group_id` / `user_group_key`
- `user_group_policy_snapshot`：任务创建时解析出的用户组策略快照，用于审计和重试稳定性。
- `model`
- `request`
- `status`：`queued`、`running`、`succeeded`、`failed`、`cancelled`
- `queue_key`：限流队列 key，例如 `${platformKey}-${model}` 或 `${provider}-${methodName}`
- `priority`
- `idempotency_key`
- `remote_task_id`
- `remote_task_payload`
- `run_mode`：`production`、`simulation`
- `simulation_profile`
- `simulation_seed`
- `locked_by`
- `locked_at`
- `heartbeat_at`
- `next_run_at`
- `result`
- `billings`
- `error`

### 7.4 `gateway_task_attempts`

保存每一次客户端尝试，支持“上一个客户端失败，下一个客户端重试”的完整审计：

- `task_id`
- `attempt_no`
- `platform_id`
- `client_id`
- `model_id`
- `status`：`submitted`、`polling`、`succeeded`、`failed`、`skipped`
- `retryable`
- `error_code`
- `error_message`
- `remote_task_id`
- `simulated`
- `request_snapshot`
- `response_snapshot`
- `started_at`
- `finished_at`

### 7.5 `gateway_task_events`

保存任务执行过程中的事件，用于控制台 SSE / WebSocket 重放、进度回调 outbox 投递与服务重启后的进度恢复：

- `task_id`
- `seq`：单任务内递增序号
- `event_type`：`queue_status`、`node_status`、`progress`、`partial_result`、`completed`、`failed`
- `status`
- `progress`
- `message`
- `payload`
- `created_at`

客户端断线重连时可通过 `Last-Event-ID` 或 `afterSeq` 回放事件。

### 7.5.1 `gateway_task_callback_outbox`

保存投递给 `server-main` 内部任务进度接口的回调。Gateway 产生事件后先写 `gateway_task_events`，再写 callback outbox，由独立 worker 按配置的 `TASK_PROGRESS_CALLBACK_URL` 投递，失败后可重试。

- `task_id`
- `event_id` / `seq`：与 `gateway_task_events` 对应，用于幂等与顺序控制。
- `callback_url`：当前使用的回调地址快照，避免配置变化影响已排队事件。
- `payload`：回调给 server-main 的标准事件载荷。
- `status`：`pending`、`delivering`、`delivered`、`failed`、`skipped`
- `attempts`
- `next_attempt_at`
- `last_error`
- `delivered_at`

### 7.6 `runtime_client_states`

保存平台客户端的运行时状态：

- `client_id`
- `platform_id`
- `provider`
- `method_name`
- `queue_key`
- `running_count`
- `waiting_count`
- `limiter_ratio`
- `cooldown_until`
- `last_assigned_at`
- `last_error`
- `updated_at`

该表用于恢复与观测；实时并发计数仍可通过事务锁、`SKIP LOCKED`、Redis token bucket 或内存计数加 PG 校验组合实现，但 PG 是最终事实源。

### 7.7 `gateway_upload_assets`

保存网关任务引用的上传资产：

- `task_id`
- `source`：`server-main-open-upload`、`remote-url`、`multipart`、`simulation`
- `server_main_file_id`：主服务返回的文件 ID。
- `object_key`
- `url`
- `content_type`
- `size`
- `checksum`
- `metadata`

### 7.8 `settlement_outbox`

保存结算事件 outbox：

- `task_id`
- `event_type`
- `payload`
- `status`
- `attempts`
- `next_attempt_at`

结算事件必须按 `eventId` / `taskId` 幂等，不能因为 Gateway 重启、HTTP 回调失败或 MQ 抖动重复扣费。

### 7.9 表结构草案

以下为首期 PostgreSQL 表结构草案。字段类型以后续 migration 为准，但语义和索引边界应保持稳定。

#### 7.9.1 `model_catalog_providers`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 基准 provider ID |
| `provider_key` | `text` | unique, not null | Gateway 内部稳定 provider key，如 `openai`、`gemini`、`runninghub` |
| `provider_code` | `text` | unique, not null | 原 `server-main` `integration-platform.code`，如 `openai`、`google-gemini` |
| `display_name` | `text` | not null | 展示名称 |
| `provider_type` | `text` | not null | 对应原 `integration-platform.spec_type`，如 `openai`、`google-gemini`、`runninghub`、`jimeng` |
| `icon_path` | `text` | nullable | 原 `integration-platform.icon_path` 或 `Logos[code]` 解析后的图标地址 |
| `source` | `text` | not null | `gateway`、`server-main.integration-platform`、`sync` |
| `capability_schema` | `jsonb` | not null, default `{}` | provider 支持的能力字段定义 |
| `default_rate_limit_policy` | `jsonb` | not null, default `{}` | provider 默认 TPM/RPM/并发模板 |
| `status` | `text` | not null | `active`、`deprecated`、`hidden` |
| `metadata` | `jsonb` | not null, default `{}` | 原始同步字段、文档链接、排序等 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `uniq_model_catalog_provider_key(provider_key)`
- `idx_model_catalog_provider_code(provider_code)`
- `idx_model_catalog_provider_status(status)`

#### 7.9.2 `base_model_catalog`

保存全量基准模型。这个表不代表某个平台已经开通该模型，而是标准模型与价格来源。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 基准模型 ID |
| `provider_id` | `uuid` | FK | 所属基准 provider |
| `provider_key` | `text` | not null | 冗余 provider key，方便查询 |
| `canonical_model_key` | `text` | unique, not null | Gateway 内部标准模型 key |
| `provider_model_name` | `text` | not null | 供应商原始模型名 |
| `model_type` | `text` | not null | 原 `PlatformModelType` 主类型，如 `text_generate`、`image_generate`、`image_edit`、`video_generate`、`image_to_video`、`omni_video` 等 |
| `display_name` | `text` | not null | 展示名称 |
| `capabilities` | `jsonb` | not null, default `{}` | 上下文、多模态、参考图/视频/音频、尺寸、时长、质量等基准能力 |
| `base_billing_config` | `jsonb` | not null, default `{}` | 基准计费配置 |
| `default_rate_limit_policy` | `jsonb` | not null, default `{}` | 默认 TPM/RPM/并发限制模板 |
| `pricing_version` | `int` | not null, default `1` | 基准价格版本 |
| `status` | `text` | not null | `active`、`deprecated`、`hidden` |
| `metadata` | `jsonb` | not null, default `{}` | 供应商文档、排序、标签 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `uniq_base_model_catalog_key(canonical_model_key)`
- `idx_base_model_catalog_provider(provider_key, model_type, status)`
- `idx_base_model_catalog_capabilities` 使用 `GIN(capabilities)`

#### 7.9.3 `model_pricing_rule_sets` / `model_pricing_rules`

定价规则从“单条规则散落在模型或平台上”调整为“规则集 + 多条计价规则”。规则集是可绑定对象，规则是具体资源维度的计算条目。默认规则集必须覆盖文本、图像、视频，后续新增音频、音乐、数字人、3D 等能力时只新增规则，不改变平台和模型绑定关系。

`model_pricing_rule_sets` 保存规则集：

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 规则集 ID |
| `rule_set_key` | `text` | unique, not null | 稳定 key，如 `default-multimodal-v1` |
| `name` | `text` | not null | 展示名称 |
| `description` | `text` | nullable | 说明 |
| `category` | `text` | not null, default `custom` | `default`、`custom`、`provider`、`model` |
| `currency` | `text` | not null, default `resource` | 默认货币或资源单位 |
| `status` | `text` | not null, default `active` | `active`、`deprecated`、`hidden` |
| `metadata` | `jsonb` | not null, default `{}` | 来源、版本、同步信息、展示扩展 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

`model_pricing_rules` 保存规则集下的计价规则：

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 价格规则 ID |
| `rule_set_id` | `uuid` | FK nullable | 所属规则集；兼容旧数据时可为空 |
| `rule_key` | `text` | not null | 规则集内稳定 key，如 `text_input_tokens` |
| `display_name` | `text` | not null | 展示名称 |
| `scope_type` | `text` | not null | 默认 `rule_set`；兼容旧数据可为 `base_model`、`platform`、`platform_model` |
| `scope_id` | `uuid` | nullable | 旧 scope 兼容字段；新规则集模式通常为空 |
| `resource_type` | `text` | not null | `text_input`、`text_output`、`image`、`video`、`audio`、`music`、`digital_human`、`model` |
| `unit` | `text` | not null | `1k_tokens`、`image`、`5s`、`second`、`character_1k`、`item` |
| `base_price` | `numeric` | not null | 基准单价，单位使用 EasyAI resource / credit |
| `currency` | `text` | not null, default `resource` | `resource`、`credit`、`cny`、`usd` |
| `base_weight` | `jsonb` | not null, default `{}` | 固定权重，如默认倍率 |
| `dynamic_weight` | `jsonb` | not null, default `{}` | 分辨率、质量、时长、有无音频、有无参考视频等动态权重 |
| `calculator_type` | `text` | not null, default `weighted_unit` | `token_usage`、`weighted_unit`、`duration_weighted`、`formula` |
| `dimension_schema` | `jsonb` | not null, default `{}` | 本规则消费的维度定义，如 token、resolution、duration、hasAudio |
| `formula_config` | `jsonb` | not null, default `{}` | 计算公式配置、单位换算和缺省值 |
| `priority` | `int` | not null, default `100` | 规则执行顺序 |
| `status` | `text` | not null, default `active` | `active`、`deprecated`、`hidden` |
| `metadata` | `jsonb` | not null, default `{}` | provider 原始字段、价格版本、说明 |
| `effective_from` | `timestamptz` | nullable | 生效开始 |
| `effective_to` | `timestamptz` | nullable | 生效结束 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `idx_model_pricing_scope(scope_type, scope_id, resource_type)`
- `idx_model_pricing_rule_set(rule_set_id, resource_type)`
- `idx_model_pricing_rule_set_key(rule_set_id, rule_key)` 唯一
- `idx_model_pricing_effective(effective_from, effective_to)`

#### 7.9.4 `gateway_tenants`

保存 Gateway 可识别的租户。独立模式下它是租户事实表；接入 `server-main` 时它是租户/组织同步副本。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | Gateway 租户 ID |
| `tenant_key` | `text` | unique, not null | Gateway 内稳定租户 key |
| `source` | `text` | not null | `gateway`、`server-main`、`sync` |
| `external_tenant_id` | `text` | nullable | 外部租户/组织 ID |
| `name` | `text` | not null | 租户名称 |
| `description` | `text` | nullable | 说明 |
| `default_user_group_id` | `uuid` | FK nullable | 默认用户组 |
| `plan_key` | `text` | nullable | 套餐或商业计划 key |
| `billing_profile` | `jsonb` | not null, default `{}` | 独立模式账务资料或接入模式同步摘要 |
| `rate_limit_policy` | `jsonb` | not null, default `{}` | 租户级 TPM/RPM/并发/队列策略 |
| `auth_policy` | `jsonb` | not null, default `{}` | 注册、邀请、SSO、域名限制等认证策略 |
| `metadata` | `jsonb` | not null, default `{}` | 同步、运营、展示扩展信息 |
| `status` | `text` | not null | `active`、`disabled`、`locked`、`deleted` |
| `synced_at` | `timestamptz` | nullable | 最近同步时间 |
| `source_updated_at` | `timestamptz` | nullable | 外部源更新时间 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |
| `deleted_at` | `timestamptz` | nullable | 软删除时间 |

索引：

- `uniq_gateway_tenants_tenant_key(tenant_key)`
- `uniq_gateway_tenants_source_external(source, external_tenant_id)`
- `idx_gateway_tenants_status(status, created_at)`

#### 7.9.5 `gateway_users`

保存 Gateway 可识别的用户。独立模式下它是用户事实表，并与本地 API Key、钱包、充值订单形成闭环；接入 `server-main` 时它是用户同步副本和策略执行缓存，不迁入主服务余额、订单、充值流水。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | Gateway 用户 ID |
| `user_key` | `text` | unique, not null | Gateway 内稳定用户 key |
| `source` | `text` | not null | `gateway`、`server-main`、`sync` |
| `external_user_id` | `text` | nullable | 外部用户 ID，`server-main` 模式为主服务用户 ID |
| `username` | `text` | not null | 登录名或同步用户名 |
| `display_name` | `text` | nullable | 展示名 |
| `email` | `text` | nullable | 邮箱 |
| `phone` | `text` | nullable | 手机号 |
| `avatar_url` | `text` | nullable | 头像 |
| `password_hash` | `text` | nullable | 仅独立模式使用，接入主服务时为空 |
| `gateway_tenant_id` | `uuid` | FK nullable | Gateway 租户 ID |
| `tenant_id` | `text` | nullable | 外部租户 ID 或兼容 claim |
| `tenant_key` | `text` | nullable | Gateway 租户 key |
| `default_user_group_id` | `uuid` | FK nullable | 默认用户组 |
| `roles` | `jsonb` | not null, default `[]` | `user`、`creator`、`operator`、`admin` 等角色 |
| `auth_profile` | `jsonb` | not null, default `{}` | SSO、MFA、API Key 策略等认证资料 |
| `metadata` | `jsonb` | not null, default `{}` | 同步、运营、展示扩展信息 |
| `status` | `text` | not null | `active`、`disabled`、`locked`、`deleted` |
| `last_login_at` | `timestamptz` | nullable | 最近登录时间 |
| `synced_at` | `timestamptz` | nullable | 最近同步时间 |
| `source_updated_at` | `timestamptz` | nullable | 外部源更新时间 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |
| `deleted_at` | `timestamptz` | nullable | 软删除时间 |

索引：

- `uniq_gateway_users_user_key(user_key)`
- `uniq_gateway_users_source_external(source, external_user_id)`
- `idx_gateway_users_status(status, created_at)`
- `idx_gateway_users_tenant(tenant_id, tenant_key, status)`

同步规则：

- `source='gateway'`：用户由 Gateway 创建和禁用，可独立使用；充值、余额、API Key 由 Gateway 本地模块承接。
- `source='server-main'`：用户由主服务同步，Gateway 只保存执行模型调用所需字段；禁用、角色、用户组变更以主服务事件或同步任务为准。
- 同一个外部用户以 `source + external_user_id` 幂等 upsert，避免用户名变更造成重复用户。
- 用户被禁用后，新任务拒绝进入队列；已运行任务按任务策略快照继续或按管理员策略取消。

#### 7.9.6 `gateway_user_groups`

保存用户组及其策略。用户组可以由 Gateway 管理，也可以从 `server-main` 同步；独立模式下 Gateway 执行全部策略，接入模式下充值和余额仍以 `server-main` 为事实源。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 用户组 ID |
| `group_key` | `text` | unique, not null | 稳定用户组 key，如 `free`、`pro`、`enterprise` |
| `name` | `text` | not null | 展示名称 |
| `description` | `text` | nullable | 说明 |
| `source` | `text` | not null | `gateway`、`server-main`、`sync` |
| `priority` | `int` | not null, default `100` | 多组命中时优先级，越小越优先 |
| `recharge_discount_policy` | `jsonb` | not null, default `{}` | 充值折扣/赠送资源策略；独立模式由 Gateway 执行，接入模式由 `server-main` 执行 |
| `billing_discount_policy` | `jsonb` | not null, default `{}` | 模型调用计费折扣策略，用于 estimated billing 和 billings |
| `rate_limit_policy` | `jsonb` | not null, default `{}` | 用户组 TPM/RPM/并发/队列策略 |
| `quota_policy` | `jsonb` | not null, default `{}` | 日/月额度、API Key 额度、最大任务数 |
| `metadata` | `jsonb` | not null, default `{}` | 展示、运营、同步元数据 |
| `status` | `text` | not null | `active`、`disabled` |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

示例：

```json
{
  "recharge_discount_policy": {
    "type": "tiered_bonus",
    "tiers": [
      { "minAmount": 100, "bonusRatio": 0.05 },
      { "minAmount": 1000, "bonusRatio": 0.12 }
    ]
  },
  "billing_discount_policy": {
    "defaultDiscountFactor": 0.9,
    "modelTypeDiscounts": { "chat": 0.85, "image": 0.95 }
  },
  "rate_limit_policy": {
    "rules": [
      { "metric": "rpm", "limit": 1200, "windowSeconds": 60 },
      { "metric": "tpm_total", "limit": 300000, "windowSeconds": 60 },
      { "metric": "concurrent", "limit": 50, "leaseTtlSeconds": 900 }
    ]
  }
}
```

#### 7.9.7 `gateway_user_group_memberships`

保存用户组成员关系。一个用户、租户或 API Key 可命中多个组，运行时按组优先级和策略合并规则解析有效策略。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 成员关系 ID |
| `group_id` | `uuid` | FK | 用户组 ID |
| `principal_type` | `text` | not null | `user`、`tenant`、`api_key`、`organization` |
| `principal_id` | `text` | not null | 对应 Gateway 用户 ID、`server-main` 外部用户 ID、租户 ID、API Key ID 或组织 ID |
| `source` | `text` | not null | `gateway`、`server-main`、`sync` |
| `priority` | `int` | not null, default `100` | 关系级优先级 |
| `effective_from` | `timestamptz` | nullable | 生效开始 |
| `effective_to` | `timestamptz` | nullable | 生效结束 |
| `status` | `text` | not null | `active`、`disabled` |
| `metadata` | `jsonb` | not null, default `{}` | 同步和运营元数据 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `uniq_user_group_membership(group_id, principal_type, principal_id)`
- `idx_user_group_membership_principal(principal_type, principal_id, status)`
- `idx_user_group_membership_effective(effective_from, effective_to)`

成员解析规则：

- 独立模式优先使用 `principal_type='user' + gateway_users.id`。
- 接入 `server-main` 时可使用 `principal_type='user' + external_user_id`，并通过 `gateway_users.source='server-main'` 反查同步副本。
- 多个组同时命中时，先按 `gateway_user_group_memberships.priority`，再按 `gateway_user_groups.priority` 合并策略。

#### 7.9.8 `gateway_invitations`

本地注册邀请码。邀请码不是普通注册的必填项；填写后必须可校验，但当前阶段只标识邀请关系，不决定用户加入哪个租户、用户组或角色。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 邀请 ID |
| `invite_code` | `text` | unique, not null | 注册邀请码 |
| `max_uses` | `int` | nullable | 最大使用次数 |
| `used_count` | `int` | not null | 已使用次数 |
| `expires_at` | `timestamptz` | nullable | 过期时间 |
| `status` | `text` | not null | `active`、`disabled` |
| `metadata` | `jsonb` | not null | 渠道、备注、审批信息 |

#### 7.9.9 `gateway_api_keys`

独立模式本地 API Key 表。`server-main` 接入模式下，`sk-*` 仍调用主服务校验；Gateway 只记录执行快照和策略。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | API Key ID |
| `gateway_tenant_id` / `gateway_user_id` | `uuid` | FK | 所属租户与用户 |
| `key_prefix` | `text` | not null | 展示和快速定位前缀 |
| `key_hash` | `text` | unique, not null | secret hash，不保存明文 |
| `name` | `text` | not null | 用户命名 |
| `scopes` | `jsonb` | not null | 可调用能力范围 |
| `user_group_id` | `uuid` | nullable | API Key 级策略组 |
| `rate_limit_policy` / `quota_policy` | `jsonb` | not null | Key 级限流和额度覆盖 |
| `status` | `text` | not null | `active`、`disabled`、`revoked` |
| `expires_at` / `last_used_at` | `timestamptz` | nullable | 过期与最近使用 |

#### 7.9.10 `gateway_wallet_accounts`

独立模式本地钱包账户。Phase 1 先支持资源余额闭环，后续可扩展人民币、美元或企业额度。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 钱包账户 ID |
| `gateway_tenant_id` / `gateway_user_id` | `uuid` | FK | 所属租户与用户 |
| `currency` | `text` | not null | `resource`、`credit`、`cny`、`usd` |
| `balance` | `numeric` | not null | 可用余额 |
| `frozen_balance` | `numeric` | not null | 冻结余额 |
| `total_recharged` | `numeric` | not null | 累计充值 |
| `total_spent` | `numeric` | not null | 累计消费 |
| `status` | `text` | not null | `active`、`frozen`、`disabled` |

#### 7.9.11 `gateway_wallet_transactions`

独立模式钱包流水。任务扣费、充值到账、退款、人工调整都必须写流水，并使用 `idempotency_key` 防重。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 流水 ID |
| `account_id` | `uuid` | FK | 钱包账户 |
| `direction` | `text` | not null | `credit`、`debit`、`freeze`、`unfreeze` |
| `transaction_type` | `text` | not null | `recharge`、`billing`、`refund`、`adjustment` |
| `amount` | `numeric` | not null | 金额 |
| `balance_before` / `balance_after` | `numeric` | not null | 变动前后余额 |
| `idempotency_key` | `text` | nullable | 幂等 key |
| `reference_type` / `reference_id` | `text` | nullable | 关联任务、订单或人工工单 |

#### 7.9.12 `gateway_recharge_orders`

独立模式充值订单。第一阶段先闭合订单、折扣、入账和流水；支付渠道可以从手工确认开始，再接真实支付。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 充值订单 ID |
| `gateway_tenant_id` / `gateway_user_id` | `uuid` | FK | 所属租户与用户 |
| `amount` | `numeric` | not null | 入账基础额度 |
| `bonus_amount` | `numeric` | not null | 用户组充值折扣或赠送额度 |
| `payable_amount` | `numeric` | not null | 实际应付 |
| `currency` | `text` | not null | 币种或资源类型 |
| `channel` | `text` | not null | `manual`、`wechat`、`alipay`、`stripe` |
| `status` | `text` | not null | `created`、`pending`、`paid`、`closed`、`failed` |
| `external_order_id` | `text` | nullable | 第三方支付订单 |
| `idempotency_key` | `text` | nullable | 创建订单幂等 key |
| `paid_at` | `timestamptz` | nullable | 支付完成时间 |

#### 7.9.13 `integration_platforms`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 平台实例 ID |
| `provider` | `text` | not null | 平台类型，如 `openai`、`runninghub`、`jimeng` |
| `platform_key` | `text` | unique, not null | 稳定平台 key，用于队列和客户端注册 |
| `name` | `text` | not null | 展示名称 |
| `base_url` | `text` | nullable | 平台 API 地址 |
| `auth_type` | `text` | not null | `bearer`、`api_key`、`basic`、`custom` |
| `credentials` | `jsonb` | not null, default `{}` | 加密后的凭证 JSON |
| `config` | `jsonb` | not null, default `{}` | 超时、重试、限流、平台私有配置 |
| `visibility_scope` | `text` | not null, default `global` | `global`、`tenant`、`private` |
| `tenant_id` | `text` | nullable | 租户外部 ID 或兼容 claim |
| `tenant_key` | `text` | nullable | Gateway 租户 key |
| `default_pricing_mode` | `text` | not null, default `inherit_discount` | 平台默认价格模式：`inherit`、`inherit_discount`、`custom` |
| `default_discount_factor` | `numeric` | not null, default `1` | 平台默认折扣系数，创建平台时可统一基于基准模型打折 |
| `pricing_rule_set_id` | `uuid` | FK nullable | 平台默认绑定的定价规则集；为空时 follow 基准模型或全局默认规则集 |
| `retry_policy` | `jsonb` | not null, default `{}` | 平台级重试策略 |
| `rate_limit_policy` | `jsonb` | not null, default `{}` | 平台级限流策略 |
| `priority` | `int` | not null, default `100` | 静态优先级，越小越优先 |
| `dynamic_priority` | `int` | nullable | 动态优先级覆盖 |
| `status` | `text` | not null | `enabled`、`disabled`、`exception` |
| `disabled_reason` | `text` | nullable | 禁用原因 |
| `cooldown_until` | `timestamptz` | nullable | 熔断/冷却到期时间 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |
| `deleted_at` | `timestamptz` | nullable | 软删除 |

索引：

- `idx_integration_platforms_provider_status(provider, status)`
- `idx_integration_platforms_status_priority(status, priority, dynamic_priority)`
- `idx_integration_platforms_cooldown(cooldown_until)`
- `idx_integration_platforms_tenant_scope(visibility_scope, tenant_id, tenant_key, status)`

#### 7.9.14 `platform_models`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 模型配置 ID |
| `platform_id` | `uuid` | FK | 所属平台 |
| `base_model_id` | `uuid` | FK nullable | 关联基准模型；为空时表示纯自定义模型 |
| `model_name` | `text` | not null | 请求使用的模型名 |
| `model_alias` | `text` | nullable | 对外展示/兼容别名 |
| `model_type` | `text` | not null | `chat`、`image`、`video`、`audio`、`embedding` |
| `display_name` | `text` | not null | 展示名称 |
| `capability_override` | `jsonb` | not null, default `{}` | 相对基准模型的能力覆盖 |
| `capabilities` | `jsonb` | not null, default `{}` | 解析后的有效能力，用于路由和前端展示 |
| `pricing_mode` | `text` | not null, default `inherit_discount` | `inherit`、`inherit_discount`、`custom` |
| `discount_factor` | `numeric` | nullable | 模型级折扣，空值时继承平台默认折扣 |
| `pricing_rule_set_id` | `uuid` | FK nullable | 模型级定价规则集；优先级高于平台绑定规则集 |
| `billing_config_override` | `jsonb` | not null, default `{}` | 自定义计费覆盖 |
| `billing_config` | `jsonb` | not null, default `{}` | 解析后的有效计费配置，用于 estimated billing 和真实 billings |
| `permission_config` | `jsonb` | not null, default `{}` | 平台维度权限过滤配置 |
| `retry_policy` | `jsonb` | not null, default `{}` | 模型级重试策略 |
| `rate_limit_policy` | `jsonb` | not null, default `{}` | 模型级限流策略 |
| `enabled` | `boolean` | not null, default `true` | 是否启用 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引与约束：

- `uniq_platform_model_type(platform_id, model_name, model_type)`
- `idx_platform_models_base(base_model_id)`
- `idx_platform_models_lookup(model_type, model_name, enabled)`
- `idx_platform_models_alias(model_alias)`
- `idx_platform_models_capabilities` 使用 `GIN(capabilities)`

#### 7.9.15 `gateway_tasks`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | Gateway 任务 ID |
| `external_task_id` | `text` | nullable | 与 `server-main` / 前端兼容的外部任务 ID |
| `kind` | `text` | not null | `chat.completions`、`images.generations` 等 |
| `run_mode` | `text` | not null | `production`、`simulation` |
| `user_id` | `text` | not null | 请求 claim 中的用户 ID |
| `gateway_user_id` | `uuid` | nullable | Gateway 用户表 ID |
| `user_source` | `text` | not null, default `gateway` | `gateway`、`server-main`、`sync` |
| `gateway_tenant_id` | `uuid` | nullable | Gateway 租户 ID |
| `tenant_id` | `text` | nullable | 外部租户 ID 或兼容 claim |
| `tenant_key` | `text` | nullable | Gateway 租户 key |
| `api_key_id` | `text` | nullable | OpenAPI Key ID |
| `user_group_id` | `uuid` | nullable | 命中的用户组 ID |
| `user_group_key` | `text` | nullable | 命中的用户组 key |
| `user_group_policy_snapshot` | `jsonb` | not null, default `{}` | 用户组策略快照 |
| `model` | `text` | not null | 请求模型 |
| `model_type` | `text` | nullable | 模型类型 |
| `request` | `jsonb` | not null | 原始请求快照 |
| `normalized_request` | `jsonb` | not null, default `{}` | 参数处理后的请求 |
| `status` | `text` | not null | `queued`、`leased`、`running`、`polling`、`succeeded`、`failed_retryable`、`failed_final`、`cancelled` |
| `queue_key` | `text` | not null | 队列与限流 key |
| `priority` | `int` | not null, default `100` | 任务优先级 |
| `idempotency_key` | `text` | nullable | 幂等 key |
| `remote_task_id` | `text` | nullable | 供应商任务 ID |
| `remote_task_payload` | `jsonb` | nullable | 供应商任务载荷 |
| `simulation_profile` | `jsonb` | nullable | 模拟配置 |
| `simulation_seed` | `text` | nullable | 模拟种子 |
| `locked_by` | `text` | nullable | worker ID |
| `locked_at` | `timestamptz` | nullable | 租约时间 |
| `heartbeat_at` | `timestamptz` | nullable | worker 心跳 |
| `next_run_at` | `timestamptz` | not null | 下次可运行时间 |
| `attempt_count` | `int` | not null, default `0` | 已尝试次数 |
| `max_attempts` | `int` | not null, default `1` | 最大尝试次数 |
| `result` | `jsonb` | nullable | 归一化结果 |
| `billings` | `jsonb` | nullable | 计费结果 |
| `error_code` | `text` | nullable | 错误码 |
| `error_message` | `text` | nullable | 错误信息 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |
| `finished_at` | `timestamptz` | nullable | 完成时间 |

索引：

- `idx_gateway_tasks_queue(status, next_run_at, priority, created_at)`
- `idx_gateway_tasks_lease(status, heartbeat_at)`
- `idx_gateway_tasks_user_created(user_id, created_at desc)`
- `idx_gateway_tasks_external(external_task_id)`
- `uniq_gateway_tasks_idempotency(user_id, idempotency_key)` where `idempotency_key is not null`

#### 7.9.16 `gateway_task_attempts`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | attempt ID |
| `task_id` | `uuid` | FK | 所属任务 |
| `attempt_no` | `int` | not null | 第几次尝试 |
| `platform_id` | `uuid` | nullable | 尝试平台 |
| `platform_model_id` | `uuid` | nullable | 尝试模型配置 |
| `client_id` | `text` | nullable | 客户端实例 ID |
| `queue_key` | `text` | not null | 限流 key |
| `status` | `text` | not null | `submitted`、`polling`、`succeeded`、`failed`、`skipped` |
| `retryable` | `boolean` | not null, default `false` | 是否可重试 |
| `simulated` | `boolean` | not null, default `false` | 是否模拟 |
| `remote_task_id` | `text` | nullable | 供应商任务 ID |
| `request_snapshot` | `jsonb` | not null, default `{}` | 本次请求快照 |
| `response_snapshot` | `jsonb` | nullable | 本次响应快照 |
| `error_code` | `text` | nullable | 错误码 |
| `error_message` | `text` | nullable | 错误信息 |
| `started_at` | `timestamptz` | not null | 开始时间 |
| `finished_at` | `timestamptz` | nullable | 结束时间 |

索引：

- `uniq_gateway_attempt_no(task_id, attempt_no)`
- `idx_gateway_attempts_task(task_id)`
- `idx_gateway_attempts_client(client_id, started_at desc)`

#### 7.9.17 `gateway_task_events`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 事件 ID |
| `task_id` | `uuid` | FK | 所属任务 |
| `seq` | `bigint` | not null | 单任务递增序号 |
| `event_type` | `text` | not null | `queue_status`、`progress`、`partial_result`、`completed`、`failed` |
| `status` | `text` | nullable | 任务状态 |
| `phase` | `text` | nullable | `routing`、`queued`、`submit`、`polling`、`upload` |
| `progress` | `numeric` | nullable | 0 到 1 |
| `message` | `text` | nullable | 展示消息 |
| `payload` | `jsonb` | not null, default `{}` | 事件载荷 |
| `simulated` | `boolean` | not null, default `false` | 是否模拟 |
| `created_at` | `timestamptz` | not null | 创建时间 |

索引：

- `uniq_gateway_events_seq(task_id, seq)`
- `idx_gateway_events_task_created(task_id, created_at)`

#### 7.9.18 `gateway_task_callback_outbox`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 回调 outbox ID |
| `task_id` | `uuid` | FK | 任务 ID |
| `event_id` | `uuid` | FK nullable | 对应 `gateway_task_events.id` |
| `seq` | `bigint` | not null | 单任务事件序号 |
| `callback_url` | `text` | not null | 投递地址快照，来自 `TASK_PROGRESS_CALLBACK_URL` |
| `payload` | `jsonb` | not null | 回调载荷 |
| `status` | `text` | not null | `pending`、`delivering`、`delivered`、`failed`、`skipped` |
| `attempts` | `int` | not null, default `0` | 已投递次数 |
| `next_attempt_at` | `timestamptz` | not null | 下次投递时间 |
| `last_error` | `text` | nullable | 最近错误 |
| `delivered_at` | `timestamptz` | nullable | 投递成功时间 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `uniq_task_callback_seq(task_id, seq, callback_url)`
- `idx_task_callback_outbox_pending(status, next_attempt_at)`
- `idx_task_callback_outbox_task(task_id, seq)`

#### 7.9.19 `runtime_client_states`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `client_id` | `text` | PK | 客户端实例 ID |
| `platform_id` | `uuid` | nullable | 所属平台 |
| `provider` | `text` | not null | provider |
| `method_name` | `text` | not null | 方法名 |
| `queue_key` | `text` | not null | 队列 key |
| `running_count` | `int` | not null, default `0` | 当前运行数 |
| `waiting_count` | `int` | not null, default `0` | 当前等待数 |
| `limiter_ratio` | `numeric` | not null, default `0` | 负载比 |
| `cooldown_until` | `timestamptz` | nullable | 冷却截止 |
| `last_assigned_at` | `timestamptz` | nullable | 上次分配时间 |
| `last_error` | `text` | nullable | 最近错误 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

索引：

- `idx_runtime_client_queue(queue_key, cooldown_until)`
- `idx_runtime_client_platform(platform_id)`

#### 7.9.20 `gateway_upload_assets`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 资产 ID |
| `task_id` | `uuid` | FK nullable | 关联任务 |
| `source` | `text` | not null | 固定为 `server-main-open-upload` 或 `remote-url` / `simulation` |
| `server_main_file_id` | `text` | nullable | 主服务返回的 file id |
| `url` | `text` | not null | 主服务返回 URL |
| `object_key` | `text` | nullable | 主服务返回对象 key |
| `content_type` | `text` | nullable | MIME |
| `size` | `bigint` | nullable | 文件大小 |
| `checksum` | `text` | nullable | 校验和 |
| `metadata` | `jsonb` | not null, default `{}` | 上传响应与业务元数据 |
| `created_at` | `timestamptz` | not null | 创建时间 |

索引：

- `idx_gateway_upload_task(task_id)`
- `idx_gateway_upload_file(server_main_file_id)`

#### 7.9.21 `gateway_retry_policies`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 策略 ID |
| `scope_type` | `text` | not null | `global`、`provider`、`platform`、`model`、`method` |
| `scope_key` | `text` | not null | 作用域 key |
| `enabled` | `boolean` | not null | 是否启用重试 |
| `policy` | `jsonb` | not null | 重试策略 JSON |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

约束：

- `uniq_retry_policy_scope(scope_type, scope_key)`

#### 7.9.22 `gateway_rate_limit_policies`

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 策略 ID |
| `scope_type` | `text` | not null | `global`、`provider`、`platform`、`client`、`base_model`、`platform_model`、`method`、`tenant`、`user_group`、`user`、`api_key` |
| `scope_key` | `text` | not null | 作用域 key |
| `policy` | `jsonb` | not null | TPM、RPM、并发、队列长度、冷却策略 |
| `created_at` | `timestamptz` | not null | 创建时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

约束：

- `uniq_rate_limit_policy_scope(scope_type, scope_key)`

示例：

```json
{
  "rules": [
    { "metric": "tpm_total", "limit": 90000, "windowSeconds": 60, "consume": "reserve_then_reconcile" },
    { "metric": "rpm", "limit": 600, "windowSeconds": 60, "consume": "fixed_window" },
    { "metric": "concurrent", "limit": 20, "leaseTtlSeconds": 900 },
    { "metric": "queue_size", "limit": 1000 }
  ]
}
```

#### 7.9.23 `gateway_rate_limit_counters`

保存一分钟窗口内的 TPM/RPM 使用量。Redis 可做加速，但 PG counter 是重启恢复和审计的事实源。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `scope_type` | `text` | not null | 限流作用域 |
| `scope_key` | `text` | not null | 作用域 key |
| `metric` | `text` | not null | `tpm_total`、`tpm_input`、`tpm_output`、`rpm` |
| `window_start` | `timestamptz` | not null | 分钟窗口开始时间 |
| `limit_value` | `numeric` | not null | 当前窗口限制 |
| `used_value` | `numeric` | not null, default `0` | 已确认消耗 |
| `reserved_value` | `numeric` | not null, default `0` | 已预占但待回填消耗 |
| `reset_at` | `timestamptz` | not null | 窗口结束时间 |
| `updated_at` | `timestamptz` | not null | 更新时间 |

约束：

- `pk_rate_limit_counter(scope_type, scope_key, metric, window_start)`

#### 7.9.24 `gateway_concurrency_leases`

保存并发请求租约，服务异常重启后可释放过期并发。

| 字段 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `uuid` | PK | 并发租约 ID |
| `task_id` | `uuid` | FK | 任务 ID |
| `attempt_id` | `uuid` | FK nullable | attempt ID |
| `scope_type` | `text` | not null | 限流作用域 |
| `scope_key` | `text` | not null | 作用域 key |
| `lease_value` | `numeric` | not null, default `1` | 占用并发数 |
| `acquired_at` | `timestamptz` | not null | 获取时间 |
| `expires_at` | `timestamptz` | not null | 过期时间 |
| `released_at` | `timestamptz` | nullable | 释放时间 |

索引：

- `idx_concurrency_leases_active(scope_type, scope_key, released_at, expires_at)`
- `idx_concurrency_leases_task(task_id)`

## 8. 模型路由、限流与失败切换

迁移时不要做简单的 first-match。路由器需要复用现有语义：

1. 先按模型名、模型类型、平台启用状态筛候选。
2. 保留平台维度权限过滤，同名模型不能跨平台混用。
3. 按平台优先级排序。
4. 结合并发是否已满、limiter ratio、等待队列、lastAssignedAt 做负载均衡。
5. 对 app-style provider，例如 RunningHub 模板类能力，使用 `provider + methodName` 的队列 key。

一期脚手架只预留接口，迁移实现时把 `IntegrationModelFactory.assignClientsByModelName` 与 `assignClientsByProviderMethod` 的行为写成 Go 版单元测试再实现。

### 8.1 限流维度

限流策略需要支持以下维度组合：

- 全局：`global`
- Provider 级：`provider_key`
- 平台级：`platform_id`
- 客户端级：`client_id`
- 基准模型级：`base_model_id` / `canonical_model_key`
- 平台模型级：`platform_model_id` / `model_name` / `model_type`
- 方法级：`method_name`，例如 `runAIApp$`
- 租户/用户级：`gateway_tenant_id` / `tenant_id` / `tenant_key` / `user_id`
- 用户组级：`user_group_id` / `user_group_key`
- OpenAPI Key 级：`api_key_id`

同一请求可能命中多个策略，默认以最严格策略为准；如后续需要不同优先级，可在 policy 中增加 `mergeStrategy`。

### 8.1.1 一分钟窗口核心指标

限流必须把 TPM、RPM、并发作为一等指标，而不是只看 running count：

| 指标 | 含义 | 适用能力 | 执行方式 |
| --- | --- | --- | --- |
| `tpm_total` | 一分钟内输入 + 输出 token 总数 | Chat、Responses、Embedding、可 token 化文本任务 | 提交前按 prompt / max_tokens 预估并预占，完成后用实际 token 回填 |
| `tpm_input` | 一分钟内输入 token 数 | Chat、Responses、Embedding | 提交前按 tokenizer 或近似估算预占 |
| `tpm_output` | 一分钟内输出 token 数 | Chat、Responses | stream 场景可按 chunk 增量累计，完成后 reconcile |
| `rpm` | 一分钟内请求数 | 所有同步/异步模型提交 | 任务 attempt 提交前扣减；失败切换的新 attempt 也算一次 provider 请求 |
| `concurrent` | 同时运行或轮询中的请求数 | 所有 provider client | 通过 `gateway_concurrency_leases` 获取租约，完成、失败、取消或过期释放 |
| `queue_size` | 等待队列长度 | 所有队列 | 超过后按策略返回兼容错误或降级到 simulation / fallback |

TPM 的预占策略：

- 文本任务：优先使用模型 tokenizer；没有 tokenizer 时用字符数估算，并把估算方式写入 event payload。
- Chat stream：提交前按 `input_tokens + max_tokens` 预占，stream 中可累计输出 token，完成后用 usage 精确回填。
- 未提供 `max_tokens` 的请求：使用模型默认输出上限或策略中的 `defaultOutputReserveTokens`。
- 失败且未提交到供应商：释放预占 token；已提交但失败的 attempt 按供应商是否计费决定保留或部分释放。
- 生图/生视频通常不计 TPM，但如果 provider 对多模态 token 有限制，可在 `capabilities.tokenPolicy` 中启用对应估算器。

RPM 和并发的边界：

- RPM 以“向 provider 发起一次 submit / sync request”为计数点；排队但未提交不消耗 RPM。
- 失败切换到下一个客户端时，新客户端 attempt 会重新消耗该客户端、平台、provider 维度的 RPM。
- 并发租约从 submit 前获取，到同步响应完成、异步任务进入可安全释放阶段或任务终态时释放；长轮询 provider 可拆成 `submit_concurrent` 与 `poll_concurrent` 两类 scope。
- 服务重启后扫描过期 lease，结合 `gateway_tasks.heartbeat_at` 判断释放或续租，避免并发永久泄漏。

除核心指标外，还保留每日/月度配额、错误冷却、余额不足拦截等策略，但这些属于调度前置校验，不替代 TPM/RPM/并发。

### 8.2 候选客户端选择

候选客户端排序规则必须可测试、可复现：

1. 过滤禁用平台、禁用模型、不可用凭证、权限不满足的客户端。
2. 过滤与请求能力不匹配的客户端，例如不支持视频参考、音频参考、多图输入时不能作为候选。
3. 根据指定平台 / 指定客户端优先收窄候选。
4. 按 `priority`、是否满载、`limiter_ratio`、等待队列长度、`last_assigned_at`、轻微 jitter 排序。
5. 选中客户端后在同一事务内写入 `last_assigned_at` 和任务锁，避免并发请求都抢到同一个客户端。

### 8.3 失败切换与重试策略

一个模型请求进来，如果多个客户端满足要求，必须具备“上一个客户端失败，下一个客户端重试”的能力，且可配置是否允许：

```json
{
  "retry": {
    "enabled": true,
    "failoverClients": true,
    "maxAttempts": 3,
    "maxSameClientAttempts": 1,
    "backoff": { "type": "exponential", "baseMs": 500, "maxMs": 5000 },
    "retryableStatusCodes": [408, 409, 429, 500, 502, 503, 504],
    "retryableErrorCodes": ["timeout", "rate_limit", "temporary_unavailable"],
    "nonRetryableErrorCodes": ["invalid_request", "insufficient_quota", "content_policy"],
    "retryOnSubmitUnknown": false
  }
}
```

策略可挂在全局、provider、platform、model、method 四层，优先级为：请求显式配置 > 模型配置 > 平台配置 > provider 默认 > 全局默认。

重试要求：

- 每次尝试都写入 `gateway_task_attempts`，包括跳过原因和失败分类。
- 已明确提交成功但轮询失败的任务，优先继续轮询原 `remote_task_id`，不能盲目重复提交造成供应商侧重复任务。
- 提交状态未知时，根据 `retryOnSubmitUnknown` 决定是否切换客户端；默认保守处理，标记为 `unknown` 并进入人工/补偿检查。
- 非重试错误直接终止任务，不再切换客户端。
- 重试产生的新客户端必须仍满足同一请求的能力、权限和计费要求。

## 9. 基准模型库与定价体系

定价不直接散落在平台模型里。Gateway 先有一个基准模型库，基准模型库存所有 provider、模型、能力、默认限流模板和基准计价规则；平台创建和平台模型配置都通过“规则集绑定 + 折扣率”继承或覆盖。

新的定价核心是 `model_pricing_rule_sets`：

- 一个规则集可以包含多条计价规则，默认规则集覆盖文本、图像、视频。
- 平台可绑定一个规则集，并设置 `default_discount_factor` 做整体价格调节。
- 平台模型可绑定另一个规则集，并设置 `discount_factor` 做模型级整体调节。
- 未绑定模型规则集时继承平台规则集；平台也未绑定时 follow 基准模型或全局默认规则集。
- 折扣率只调整最终价格，不改变规则本身，便于后续对同一规则做不同平台、不同模型的商业策略。

Provider 目录从原 `easyai-server-main` 的 `integration-platform` 模块同步：`name` 映射为 `display_name`，`code` 映射为 `provider_code`，`icon_path` 优先使用平台配置，缺省时按原模块 `Logos[code]` 解析。Gateway 运行时仍保留 `provider_key` 作为内部路由 key，避免原平台 code 改名影响已有任务与平台模型配置。

### 9.1 基准模型库内容

每个基准模型至少包含：

- provider：如 `openai`、`runninghub`、`jimeng`。
- canonical model key：Gateway 内部稳定模型 key。
- provider model name：供应商真实请求模型名。
- model type：`chat`、`image`、`video`、`audio`、`embedding`、`music`、`digital_human`、`model_3d`。
- capabilities：上下文窗口、stream、多模态输入、参考图、参考视频、参考音频、支持尺寸、质量、时长、有无音频、最大 batch 等。
- base billing config：兼容旧配置的价格快照；新价格以定价规则集为准。
- default pricing rule set：推荐绑定的默认计价规则集，可覆盖文本、图像、视频等多种能力。
- default rate limit policy：provider 或模型默认 TPM/RPM/并发模板。

### 9.2 定价规则集与绑定

创建平台或平台模型时不直接编辑散落的单价，而是选择定价规则集：

| 绑定位置 | 字段 | 生效范围 | 说明 |
| --- | --- | --- | --- |
| 基准模型 | `base_model_catalog.base_billing_config` / metadata | fallback | 兼容旧价格快照，也可记录推荐默认规则集 key |
| 平台 | `integration_platforms.pricing_rule_set_id` | 平台下所有未覆盖模型 | 创建平台时选择规则集，并用 `default_discount_factor` 做整体折扣 |
| 平台模型 | `platform_models.pricing_rule_set_id` | 单个模型 | 模型单独选择规则集，并用 `discount_factor` 覆盖平台折扣 |

有效规则集解析顺序：

1. 平台模型 `pricing_rule_set_id`。
2. 平台 `pricing_rule_set_id`。
3. 基准模型推荐规则集或 `base_billing_config` 映射出的规则集。
4. 全局默认规则集 `default-multimodal-v1`。

有效折扣解析顺序：

1. 平台模型 `discount_factor`。
2. 平台 `default_discount_factor`。
3. 用户组 `billing_discount_policy`，用于用户组级模型调用折扣。
4. 租户、API Key 或促销策略的额外折扣，按策略优先级合并。

有效价计算：

```text
effective price = rule price(request dimensions) * platform/model discount * user/group policy discount
```

### 9.3 规则集模式

| 模式 | 配置 | 说明 |
| --- | --- | --- |
| `inherit` | 不填规则集，不填折扣 | follow 上一级规则集和能力 |
| `inherit_discount` | 继承规则集，只填平台或模型折扣 | 有效价 = 继承规则集价格 x 折扣系数；能力默认 follow 基准模型，可局部覆盖 |
| `custom` | 绑定自定义规则集 / `capability_override` | 使用自定义规则集和能力覆盖；未覆盖能力仍从基准模型补齐 |

有效配置解析顺序：

1. 平台模型 `capability_override`。
2. 平台模型 `pricing_rule_set_id` / `discount_factor`。
3. 平台 `pricing_rule_set_id` / `default_discount_factor`。
4. 用户组、租户、API Key 的折扣和配额策略。
5. 基准模型 `capabilities` / 默认规则集。

如果没有配置平台模型定价，必须 follow 上一级规则集；不能出现空价格导致预估扣费为 0 的隐式行为。

接入 `server-main` 时，充值折扣不在 Gateway 内直接变更余额。Gateway 只保存 `recharge_discount_policy` 并与主服务同步；充值订单、资源包发放、余额流水仍由 `server-main` 作为事实源执行。独立模式下，充值、余额、订单和钱包流水由 Gateway 本地账务模块闭环。

### 9.4 各能力计价模型

文本类：

- 按 `1k_tokens` 计价。
- 输入和输出分开计价：`text_input`、`text_output`。
- 如果模型只配置总价，可 fallback 到 `text_total`，但 resolver 对外仍展开成输入/输出明细。
- 预估时用输入 token + 输出 token 预估值；最终结算用 provider usage 或 Gateway tokenizer 回填。
- 规则维度：`input_tokens`、`output_tokens`、`cached_tokens`、`reasoning_tokens`、`unitScale`。

图像类：

- 基础单位为 `image`。
- 动态权重至少支持：分辨率、质量、张数、编辑/生成模式。
- 分辨率示例：`512x512`、`1024x1024`、`1K`、`2K`、`4K`、`8K`。
- 质量示例：`standard`、`hd`、`high`、`ultra`。
- 公式示例：`count * basePrice * resolutionWeight * qualityWeight * modeWeight`。
- 规则维度：`resolution`、`quality`、`count`、`mode`、`referenceImageCount`、`mask`。

视频类：

- 基础单位建议为 `5s` 或 `second`，与原 provider 配置保持可映射。
- 动态权重至少支持：时长、分辨率、是否包含音频、是否使用参考视频、是否指定声音/音色、生成数量。
- 分辨率示例：`480p`、`720p`、`1080p`、`2160p`。
- 公式示例：`count * (durationSeconds / unitSeconds) * basePrice * resolutionWeight * audioWeight * referenceWeight`。
- 规则维度：`durationSeconds`、`resolution`、`count`、`hasAudio`、`hasReferenceVideo`、`hasReferenceImage`、`voice`。

音频、音乐、数字人、3D 模型：

- TTS / audio 可按 `character_1k` 或秒计费。
- 音乐可按首数、时长、模型权重计费。
- 数字人可按时长、分辨率、音频驱动方式计费。
- 3D / model 类按任务数、模型复杂度或 provider 返回的计费单位映射。

### 9.5 estimated billing 一致性

`/integration-platform/models/estimatedBilling`、`/integration-platform/models/estimatedBilling/:workflowId` 和真实任务结算必须使用同一个 resolver：

```text
request -> normalize params -> resolve effective platform model
-> resolve effective capabilities -> resolve effective pricing
-> estimate billing -> route / submit -> final billing reconcile
```

要求：

- 预估扣费不能只按模型名查第一条，必须使用真实候选平台过滤、权限过滤和优先级排序后的有效平台模型。
- 同名模型跨平台时，平台权限和平台模型价格都不能串。
- 测试模式使用同样的预估逻辑，但 billings 标记 `simulated=true`。
- 基准模型价格变更时，平台模型可选择自动 follow 最新版本或 pin 到指定 `pricing_version`，避免历史价格被静默改变。

## 10. Base Client 与 Provider 实现架构

Gateway 需要保留类似原 `BaseModelClient` 的整体架构：公共基类定义通用生命周期，不同 provider/client 只实现平台差异。这样才能把参数预处理、限流、日志、进度、重试、计费和结果归一放在同一层处理。

### 10.1 Client 接口

Go 侧建议抽象为：

```go
type ModelClient interface {
  Provider() string
  MethodName() string
  BuildParams(ctx context.Context, task TaskContext) (VendorRequest, error)
  SubmitTask(ctx context.Context, request VendorRequest) (RemoteTask, error)
  PollResult(ctx context.Context, remote RemoteTask) (PollResult, error)
  NormalizeResult(ctx context.Context, result PollResult) (NormalizedResult, error)
  EstimateBilling(ctx context.Context, task TaskContext) ([]BillingItem, error)
  Cancel(ctx context.Context, remote RemoteTask) error
}
```

### 10.2 Base Client 公共职责

`BaseClient` / `BaseModelClient` 负责：

- 调用参数处理链：尺寸、画幅、时长、多模态内容过滤、默认值填充。
- 校验模型能力：图片参考、视频参考、音频参考、文件输入、stream 支持等。
- 发出进度事件：构建参数、排队、提交、轮询、下载、上传、完成。
- 执行通用 timeout、backoff、错误分类、重试和 failover 策略。
- 统一记录 `gateway_task_attempts`、`gateway_task_events`、运行指标。
- 统一提取 `billings` 与 `result`，交给 outbox 做结算事件。
- 统一处理文件资产：已上传文件引用、远程 URL 转存、provider 临时 URL 拉取后调用 `server-main` `/v1/files/upload`。

### 10.3 Provider Client 实现职责

具体 client 只负责供应商协议：

- OpenAI-compatible：同步/stream chat、responses、image generation。
- Gemini：Chat、多模态图片输入、生图、图像编辑优先进入第一阶段迁移。
- RunningHub：模板类 app 提交、轮询、结果提取，队列 key 使用 `${provider}-${methodName}`。
- Jimeng / Vidu / Kling / Hunyuan Video：视频任务提交与轮询。
- Suno / Speech / Digital Human：各自的提交和结果归一。
- SimulationClient：用于测试模式和 dry-run，完整模拟提交、轮询、进度、失败、结果，不参与真实生产候选。
- MockTest：用于 provider contract 回归和 loopback，不能和生产路由混在同一个职责里；应由独立 registry / loopback service 管理。

每个 provider 至少需要：

- contract test：固定输入输出 DTO。
- retry classification test：哪些错误可重试、哪些不可重试。
- progress event snapshot：确保前端进度面板兼容。
- billing snapshot：确保预估扣费和最终 billings 语义一致。

OpenAI-compatible 文本请求中的推理深度统一使用 `reasoning_effort` 表达。该字段是请求参数，不是响应中的推理内容；模型能力中用 `thinkingEffortLevels` 声明该模型支持的可选取值。`reasoning_effort` 必须按开放字符串处理，不在网关层写死枚举；实际可用集合必须以 provider 和模型能力为准。常见取值定义如下：

| 值 | 含义 |
| --- | --- |
| `none` | 不启用额外推理，适用于不需要思考链路的低延迟请求。 |
| `minimal` | 最小推理预算，优先降低延迟和成本。 |
| `low` | 较低推理预算，用于简单推理任务。 |
| `medium` | 默认/均衡推理深度，在质量、延迟和成本之间折中。 |
| `high` | 较高推理预算，用于复杂规划、代码和多步推理。 |
| `xhigh` | 最高推理预算，仅在模型和 provider 明确支持时使用，通常成本和延迟最高。 |
| `max` | 供应商自定义最高档示例，例如 DeepSeek V4 类模型可能使用该值；语义以 provider 文档为准。 |

除上表外，`thinkingEffortLevels` 可以保存任意供应商自定义值，例如 `max`、`ultra` 或后续模型新增档位。管理端只提供常见值作为快捷选项，不应阻止自定义输入；请求透传时按模型能力校验或直接交由上游 provider 返回错误。

`reasoning_content`、推理过程 delta 或思考摘要在 Chat Completions 中不是 OpenAI 标准必需字段；如需兼容 DeepSeek、Qwen 等供应商扩展，应在 adapter 层作为可选扩展透传，并避免把 hidden reasoning 默认暴露给普通兼容客户端。

## 11. 队列持久化、恢复与限流执行

### 11.1 持久化队列原则

队列必须以 PostgreSQL 为事实源，Redis / 内存只作为加速和通知层。服务异常重启后，任务状态必须可从 PG 恢复。

任务生命周期：

```text
created -> queued -> leased -> running -> polling -> succeeded
                                  |           |
                                  |           -> failed_retryable -> queued
                                  -> failed_final / cancelled
```

核心规则：

- 入队时写 `gateway_tasks`，状态为 `queued`，并写初始 `gateway_task_events`。
- worker 获取任务时使用事务、`FOR UPDATE SKIP LOCKED` 或 advisory lock，写入 `locked_by`、`locked_at`、`heartbeat_at`。
- worker 运行中定期刷新 `heartbeat_at`。
- 服务启动时扫描 `running/polling/leased` 且 heartbeat 超时的任务。
- 有 `remote_task_id` 的任务优先进入 reconciliation poller，继续取回供应商结果。
- 没有 `remote_task_id` 的任务按 retry 策略回到 `queued` 或进入 `failed_final`。
- 结算 outbox 独立重试，任务成功不等于结算成功。

### 11.2 恢复策略

| 重启前状态 | 恢复动作 |
| --- | --- |
| `queued` | 保持队列，等待 worker 重新租约 |
| `leased` 但无 heartbeat | 释放锁，回到 `queued` |
| `running` 且无 `remote_task_id` | 视为提交前失败，按 retry 策略重试 |
| `running/polling` 且有 `remote_task_id` | 进入 poller 继续取回结果 |
| `succeeded` 但 settlement pending | outbox 继续补偿 |
| `failed_retryable` 且未超最大次数 | 按 `next_run_at` 回队列 |
| `failed_final/cancelled` | 不自动恢复 |

### 11.3 限流执行

worker 租约任务前必须先拿到限流令牌：

- TPM 令牌：按 `gateway_rate_limit_counters` 的一分钟窗口预占 token，完成后 reconcile 实际 token。
- RPM 令牌：提交 provider 前扣减一分钟窗口请求数，失败切换的新 attempt 重新评估。
- 并发令牌：按 `queue_key` / `client_id` / `method_name` / `platform_model_id` 维度写 `gateway_concurrency_leases`。
- 排队令牌：超过 `max_waiting` 后直接返回兼容错误。
- 冷却令牌：客户端错误达到阈值后设置 `cooldown_until`。

如果 worker 拿不到令牌，任务保持 `queued` 并更新 `next_run_at`，不能占用 worker 长时间空等。

## 12. 进度流与 RxJS-like 中间结果

原系统里很多 provider 使用 RxJS Observable 在中间过程返回进度。Go 侧需要提供等价语义：

```go
type ProgressPublisher interface {
  Publish(ctx context.Context, event ProgressEvent) error
  Subscribe(ctx context.Context, taskID string, afterSeq int64) (<-chan ProgressEvent, error)
}
```

### 12.1 ProgressEvent 结构

```json
{
  "taskId": "uuid",
  "seq": 12,
  "event": "progress",
  "status": "running",
  "phase": "polling",
  "progress": 0.42,
  "message": "Generating video frames",
  "payload": {},
  "createdAt": "2026-05-09T12:00:00Z"
}
```

### 12.2 推送通道

- 业务实时进度主通道：Gateway 按 `TASK_PROGRESS_CALLBACK_URL` 回调 `server-main`，由 `server-main` 复用原 WebSocket 网关推送给业务前端。
- 控制台 SSE：`GET /api/v1/tasks/:taskId/events`，支持 `Last-Event-ID` 回放，用于 Gateway 控制台、调试和故障诊断。
- Gateway WebSocket：用于网关控制台高频队列监控、运行态监控；不替代业务前端现有 WebSocket 通道。
- OpenAI stream：`/chat/completions`、`/v1/chat/completions`、`/responses` 需要保持原 stream chunk 格式。

任务进度回调示例：

```http
POST ${TASK_PROGRESS_CALLBACK_URL}
Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN}
Content-Type: application/json
Idempotency-Key: ${taskId}:${seq}
X-EasyAI-Event-Type: task.progress
```

```json
{
  "eventId": "uuid",
  "taskId": "uuid",
  "externalTaskId": "server-main-task-id",
  "userId": "user-id",
  "tenantId": "tenant-id",
  "apiKeyId": "optional",
  "kind": "images.generations",
  "model": "gpt-image-1",
  "seq": 12,
  "event": "progress",
  "status": "running",
  "phase": "polling",
  "progress": 0.42,
  "message": "Generating video frames",
  "payload": {},
  "createdAt": "2026-05-09T12:00:00Z"
}
```

`server-main` 收到后不重新执行任务，只做三件事：

1. 按 `Idempotency-Key` 或 `taskId + seq` 幂等落库/去重。
2. 根据 `externalTaskId`、`taskId`、`userId`、`tenantId` 找到原业务会话或任务频道。
3. 通过原 WebSocket 网关推送给业务前端，保持前端订阅协议不大改。

### 12.3 进度持久化要求

- 所有对用户可见的状态变化都必须先写 `gateway_task_events`，再写 `gateway_task_callback_outbox`，最后广播/投递。
- 客户端断线后能从 `seq` 或 `Last-Event-ID` 补齐。
- 任务恢复后必须先重放最近状态，再继续发布新进度。
- 完成态事件必须包含足够的结果摘要，前端无需额外轮询才能更新卡片状态。
- 回调投递失败不能阻塞任务执行，但必须进入 outbox 重试，并在控制台显示滞留状态。
- 同一任务的回调按 `seq` 保序投递；允许不同任务并发投递。

## 13. 测试模式与全链路模拟

AI Gateway 需要内置测试模式，目标是在不触达真实供应商、不真实扣费、不调用主服务生产上传接口的前提下，把请求完整跑过路由、候选客户端选择、队列、限流、失败切换、进度流、结果归一、任务恢复等核心链路。

### 13.1 启用方式

支持三种启用粒度：

| 粒度 | 配置 | 说明 |
| --- | --- | --- |
| 全局 | `AI_GATEWAY_RUN_MODE=production|simulation` | 整个服务进入生产或模拟模式 |
| 请求级 | `X-EasyAI-Test-Mode: simulation` 或 body `test_mode=true` | 仅对单次请求模拟，默认只允许 `power/manager` 或内部服务使用 |
| 平台/模型级 | `platform.config.testMode`、`model.config.testMode` | 指定平台、模型在测试环境永远走模拟 |

安全要求：

- 生产环境默认禁止普通用户请求级 test mode；需要显式开启 `ALLOW_REQUEST_TEST_MODE=true`。
- OpenAPI `sk-*` 默认不能打开 test mode，除非 API Key 或租户配置允许。
- 进入 simulation 后必须在 task、attempt、event、response 中标记 `simulated=true`，避免和真实任务混淆。

### 13.2 模拟链路

测试模式仍走完整执行链：

```text
HTTP route -> auth -> route compatibility adapter -> model router
-> persistent queue -> rate limiter -> SimulationClient
-> progress events -> normalized result -> dry-run billing -> task completed
```

不会执行的动作：

- 不调用真实 provider 的 submit / poll / cancel API。
- 不生成真实供应商订单或任务。
- 不向 `server-main` 发真实扣费结算事件。
- 不调用 `server-main` 的生产文件上传，除非显式使用测试环境上传接口。
- 不消耗真实平台并发额度。

仍会执行的动作：

- 真实鉴权。
- 真实路由和候选客户端排序。
- 真实队列持久化、锁、heartbeat、恢复。
- 真实限流令牌计算。
- 真实进度事件持久化和推送。
- 真实任务记录、attempt 记录、dry-run outbox 记录。

### 13.3 Simulation Profile

SimulationClient 根据 `simulation_profile` 生成确定性行为：

```json
{
  "simulation_profile": {
    "latencyMs": [300, 800, 1200],
    "progressSteps": [0.1, 0.35, 0.7, 1],
    "resultKind": "image",
    "resultCount": 2,
    "failures": [
      { "attempt": 1, "errorCode": "timeout", "retryable": true },
      { "attempt": 2, "errorCode": "rate_limit", "retryable": true }
    ],
    "seed": "task-id-or-custom-seed"
  }
}
```

用途：

- 模拟成功：返回固定格式图片、视频、音频、文本或 embedding 结果。
- 模拟失败：按 attempt 注入可重试 / 不可重试错误。
- 模拟慢任务：验证进度、断线重连、任务恢复。
- 模拟限流：验证排队、等待上限、cooldown。
- 模拟未知提交态：验证 `retryOnSubmitUnknown` 和人工补偿路径。

### 13.4 失败切换测试

测试模式必须能验证“上一个客户端失败，下一个客户端重试”：

1. router 仍按真实规则选出多个候选客户端。
2. SimulationClient 为第一个候选返回 retryable error。
3. runtime 写入 failed attempt。
4. retry policy 判断可切换。
5. router 排除已失败且不可复用的 client，选下一个候选。
6. 第二个候选成功后，任务以 `succeeded` 完成，并记录完整 attempts。

禁用重试时，同样的失败应直接进入 `failed_final`，不能切换客户端。

### 13.5 Dry-run 计费与结算

- `EstimateBilling` 仍使用真实计费配置计算，结果标记 `simulated=true`。
- `settlement_outbox` 可写入 `dry_run` 事件，但默认状态为 `skipped`，不触发主服务扣费。
- 若需要联调主服务结算接口，只能调用 `server-main` 的 dry-run settlement endpoint，不能复用真实扣费 endpoint。

### 13.6 模拟结果资产

测试模式下的文件结果有三种来源：

- 内置静态占位 URL，例如 `/static/simulation/image.png`。
- data URL / inline metadata，仅用于接口契约测试。
- 测试文件上传接口：只有显式启用 `AI_GATEWAY_SIMULATION_UPLOAD=true` 时才允许调用 `server-main` 测试环境上传接口；默认使用内置静态占位资产。

结果必须带：

```json
{
  "simulated": true,
  "assetSource": "simulation",
  "provider": "simulation"
}
```

### 13.7 观测与审计

控制台需要能筛选 `run_mode=simulation` 的任务，展示：

- 使用的 simulation profile。
- 每次 attempt 的模拟错误和重试决策。
- 生成的 progress events。
- dry-run billing。
- 是否触发了恢复流程。

## 14. server-main 对接方式

### 14.1 短期：server-main 仍保留入口

`server-main` 对外接口不变：

- `/chat/completions`
- `/images/generations`
- `/video/generations`
- `/v1/chat/completions`
- `/v1/images/generations`
- `/v1/video/generations`

内部 `OpenaiService` 变成薄门面：

```mermaid
sequenceDiagram
  participant FE as Frontend
  participant MAIN as server-main
  participant GW as AI Gateway
  participant V as Vendor

  FE->>MAIN: POST /images/generations
  MAIN->>MAIN: create history / task id
  MAIN->>GW: POST /api/v1/images/generations
  GW->>V: submit task
  GW-->>MAIN: POST task progress callback
  MAIN-->>FE: WebSocket task progress
  V-->>GW: result
  GW-->>MAIN: POST task completed callback
  MAIN-->>FE: WebSocket task completed
  GW-->>MAIN: settlement event with billings
  MAIN->>MAIN: billing + history update
  GW-->>MAIN: task result
  MAIN-->>FE: response
```

### 14.2 实时任务进度回调

AI Gateway 不直接替换原业务前端的 WebSocket 订阅方式。Gateway 在配置中指定任务进度回调地址：

```env
TASK_PROGRESS_CALLBACK_URL=http://easyai-server-main:3000/internal/platform/task-progress-callbacks
TASK_PROGRESS_CALLBACK_ENABLED=true
TASK_PROGRESS_CALLBACK_TIMEOUT_MS=5000
TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS=10
```

运行时要求：

- `BaseClient` / runtime 每产生一个进度事件，先写 `gateway_task_events`，再写 `gateway_task_callback_outbox`。
- callback worker 使用 `SERVER_MAIN_INTERNAL_TOKEN` 调用 `TASK_PROGRESS_CALLBACK_URL`。
- `server-main` 收到事件后进入主服务内部推送流程，再由原 WebSocket 网关推送给业务前端。
- Gateway 控制台仍可通过 SSE 读 `gateway_task_events`，用于运维诊断，不影响业务前端推送通道。
- 如果 `server-main` 短暂不可用，Gateway 按 outbox 重试；超过最大次数后标记 `failed`，控制台可手动 replay。

### 14.3 中期：前端直接打 Gateway

前端配置 `VITE_GATEWAY_API_BASE_URL`，生成类请求直接走网关路由到 AI Gateway。`server-main` 只处理登录、刷新、余额、历史、开放文件上传、扣费。

即使生成请求改为前端直连 Gateway，业务实时进度默认仍回调 `server-main` 内部任务进度接口，除非某个新业务明确迁移到 Gateway 自身的事件通道。

### 14.4 结算事件

Gateway 任务成功后向 `server-main` 发送结算事件：

```json
{
  "eventId": "uuid",
  "taskId": "uuid",
  "userId": "user-id",
  "tenantId": "tenant-id",
  "apiKeyId": "optional",
  "kind": "images.generations",
  "model": "gpt-image-1",
  "billings": [
    {
      "resourceType": "image",
      "amount": 1,
      "unit": "item",
      "cost": 100
    }
  ],
  "resultRef": {
    "url": "https://..."
  }
}
```

要求：

- `server-main` 以 `eventId` / `taskId` 做幂等。
- Gateway 保留 outbox 或重试表，避免结算事件丢失。
- 扣费失败时任务结果仍可先保存，但前端展示需要明确异常状态。

## 15. 文件上传

文件上传策略收敛为：**AI Gateway 不维护自己的 OSS/COS/S3 配置，不做预签名，不直接写对象存储；所有需要上传或转存的文件，统一调用 `server-main` 已开放的文件上传接口**。这样可以复用主服务现有 OSS 配置、权限、文件记录、MIME 推断和后续审计。

### 15.1 ServerMainUploadClient 抽象

```go
type ServerMainUploadClient interface {
  UploadMultipart(ctx context.Context, input UploadMultipartInput) (UploadedAsset, error)
  UploadFromURL(ctx context.Context, input UploadFromURLInput) (UploadedAsset, error)
  ResolveAsset(ctx context.Context, ref AssetRef) (ResolvedAsset, error)
}
```

### 15.2 调用接口

默认调用 `server-main` 的开放上传接口：

| Method | Path | 说明 |
| --- | --- | --- |
| `POST` | `/v1/files/upload` | OpenAPI 兼容 multipart 文件上传 |

调用要求：

- 用户请求：透传用户 JWT 或 OpenAPI `sk-*`，保持与原主服务上传权限一致。
- Gateway 内部上传：使用服务令牌加原始用户上下文头，例如 `X-EasyAI-User-Id`、`X-EasyAI-Tenant-Id`、`X-EasyAI-Task-Id`，具体以 `server-main` 内部约定为准。
- 上传返回值以主服务响应为准，Gateway 只做字段归一并写入 `gateway_upload_assets`。
- 测试模式默认不调用上传接口，除非显式启用测试上传。

### 15.3 上传场景

| 场景 | 处理方式 |
| --- | --- |
| 前端已经通过主服务上传 | 请求中携带主服务返回的 URL / file id，Gateway 只校验和记录 |
| Gateway 需要转存远程 URL | Gateway 下载远程内容，作为 multipart 调 `/v1/files/upload` 上传到主服务 |
| Provider 返回临时 URL | Gateway 拉取结果，再调 `/v1/files/upload` 转存，最终结果使用主服务 URL |
| OpenAPI multipart 上传 | 继续由 `server-main` 的 `/v1/files/upload` 承接；Gateway 不新增并行上传入口 |
| base64 小文件 | Gateway 解码为 multipart，再调用 `/v1/files/upload` |

### 15.4 安全与可靠性

- 限制最大文件大小、content-type、扩展名。
- 远程 URL 下载必须禁止内网地址、metadata 地址、超长重定向链。
- 上传到 `server-main` 时带 `Idempotency-Key`，避免重启或 retry 造成重复文件记录。
- `gateway_upload_assets` 记录主服务返回的 file id、URL、object key、checksum、响应快照。
- 如果上传失败，任务按 provider retry 策略判断是否可重试；上传失败本身也要写 attempt/event。
- Gateway 不保存长期文件密钥，不暴露 OSS 配置页面。

## 16. 前端页面设计

前端使用 React + TypeScript + TSX + `shadcn-ui`，定位不是单纯后台控制台，而是“AI 能力门户 + 用户工作台 + 管理工作台 + API 文档中心”。页面根据 `IDENTITY_MODE` 切换身份来源：独立模式走 Gateway 本地登录 / API Key；接入 `server-main` 时走主服务 JWT / OpenAPI Key 授权。普通用户只能进入模型、用户工作台和 API 文档，管理员额外进入管理工作台。

### 16.1 一级导航

| 一级模块 | 路由 | 默认权限 | 定位 |
| --- | --- | --- | --- |
| 登录 | `/login`、`/register` | `public` | Gateway 本地账号登录注册、外部 token 入口 |
| 首页 | `/` | `public/basic` | 服务入口、模型能力概览、用量摘要、最近任务、快捷入口 |
| 模型 | `/models` | `public/basic` | 可用模型浏览、能力筛选、价格/限流展示、模型调用入口 |
| 用户工作台 | `/workspace` | `basic` | 个人中心、余额充值、API Key、任务记录 |
| 管理工作台 | `/admin` | `power/manager` | 用户与用户组、全局模型配置、平台管理、队列限流、结算与系统设置 |
| API 文档 | `/docs` | `public/basic` | 开放接口文档、鉴权说明、示例代码、在线调用测试 |

### 16.2 登录与注册

首期登录页先提供普通账号能力和可选邀请码注册，后续再接 SSO、验证码、MFA：

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 登录 | `/login` | `public` | 用户名/邮箱 + 密码登录 Gateway 本地账号 |
| 注册 | `/register` | `public` | 创建 Gateway 本地账号；可填写邀请码，租户字段不出现在普通注册界面 |
| 外部 Token 入口 | `/login?mode=external` | `public` | 粘贴 `server-main` access token，用于接入模式和开发调试 |

登录成功后统一获得 Gateway 可校验的 access token；本地账号 token 中包含 `source=gateway`、`gatewayUserId`、`gatewayTenantId`、`tenantKey`。外部 token 保留 `source=server-main` 或主服务返回的 claim。

### 16.3 首页

首页不是营销页，首屏应直接提供可操作入口和运行状态。

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 首页总览 | `/` | `public/basic` | 推荐模型、最近任务、余额摘要、API Key 状态、服务公告 |
| 能力概览 | `/capabilities` | `public/basic` | Chat、生图、生视频、音频、Embedding 等能力入口 |
| 服务状态 | `/status` | `public/basic` | 网关健康、模型可用性、平台异常公告、限流提示 |

首页组件：

- `HeroStatusBand`：显示 Gateway 健康、可用模型数、今日任务数、当前余额。
- `QuickStartActions`：Chat 调用、生图调用、生视频调用、创建 API Key、查看 API 文档。
- `ModelCapabilityGrid`：按文本、图像、视频、音频、Embedding 展示可用能力。
- `RecentTasks`：最近任务状态、进度、失败原因和重试入口。
- `UsageSnapshot`：本月调用量、token、图片/视频任务、费用趋势。

### 16.4 模型

模型页面面向普通用户和管理员，但展示内容按权限裁剪。

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 模型广场 | `/models` | `public/basic` | 按能力、provider、价格、上下文、多模态筛选模型 |
| 模型详情 | `/models/:modelKey` | `public/basic` | 能力、价格、限流、参数 schema、示例请求、在线试用 |
| 模型价格 | `/models/:modelKey/pricing` | `basic` | 文本输入/输出、图像分辨率/质量、视频时长/分辨率等价格 |
| 模型调用测试 | `/models/:modelKey/playground` | `basic` | 选择 API Key 或 JWT，在线测试 Chat/生图/生视频 |

模型页能力：

- 筛选：模型类型、provider、是否支持 stream、多模态、参考图/视频/音频、上下文窗口、价格区间。
- 展示：有效模型价、TPM/RPM/并发限制、是否支持测试模式、常见错误码。
- 管理员视角：展示平台模型来源、基准模型映射、平台折扣、能力覆盖和实际候选平台。

### 16.5 用户工作台

用户工作台关注“我能用什么、用了多少、怎么调用、历史任务在哪里”。

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 个人中心总览 | `/workspace/overview` | `basic` | 账号信息、身份来源、角色、用户组、余额、API Key 数、最近任务、用量摘要 |
| 余额与充值 | `/workspace/billing` | `basic` | 余额、资源包、充值入口、消费记录、用户组充值折扣、发票/订单状态 |
| API Key 管理 | `/workspace/api-keys` | `basic` | API Key 列表、创建、禁用、重置、权限范围、最近调用 |
| 任务记录 | `/workspace/tasks` | `basic` | Chat/生图/生视频等任务列表，按状态、模型、时间筛选 |
| 任务详情 | `/workspace/tasks/:id` | `basic` | 请求、进度事件、结果、计费、上传资产、失败原因 |

边界：

- 独立模式下，用户资料、API Key、余额、充值订单和钱包流水由 Gateway 本地模块闭环；接入 `server-main` 时，余额、充值、订单、API Key 生命周期仍归 `server-main`，Gateway 前端可以通过 `server-main` API 或兼容代理展示。
- 用户组影响充值折扣、调用折扣和并发/限流；工作台需要展示当前用户命中的用户组、折扣说明、TPM/RPM/并发限制。
- 任务记录以 Gateway 任务为执行事实源，但业务历史最终仍可由 `server-main` 汇总。
- API Key 创建时必须显示一次性 secret，后续只展示 key 名称、前缀、权限和最近使用时间。

### 16.6 管理工作台

管理工作台只对 `power/manager` 开放，负责全局模型、平台、限流、重试、运行态和系统集成。当前阶段平台凭证和 provider 凭证仅允许全局管理员配置，租户管理员暂不拥有自助配置入口。

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 管理总览 | `/admin` | `power` | 服务健康、任务吞吐、成功率、队列积压、异常平台、回调 outbox |
| 租户管理 | `/admin/tenants` | `power` | 本地租户、同步租户、租户策略、状态和用量隔离 |
| 用户管理 | `/admin/users` | `power` | 本地用户、同步用户、角色、状态、同步差异和用户组命中 |
| 用户组管理 | `/admin/user-groups` | `power` | 用户组、成员关系、充值折扣、调用折扣、并发/限流策略 |
| 全局模型配置 | `/admin/models/global` | `power` | 基准模型库、能力 schema、基准定价、默认限流模板 |
| 基准 Provider | `/admin/catalog/providers` | `power` | provider 列表、协议类型、能力 schema、启停状态 |
| 基准模型详情 | `/admin/catalog/base-models/:id` | `power` | 能力、价格版本、默认 TPM/RPM/并发、引用平台模型 |
| 定价规则 | `/admin/pricing/rules` | `power` | 定价规则集、文本/图像/视频规则、动态权重、平台/模型绑定引用 |
| 平台管理 | `/admin/platforms` | `power` | 平台 CRUD、启停、优先级、凭证状态、复制、异常重置 |
| 平台详情 | `/admin/platforms/:id` | `power` | 基础信息、凭证、模型、折扣、限流、重试、运行态、快照 |
| 平台模型 | `/admin/platform-models` | `power` | 平台模型列表、基准映射、能力覆盖、价格覆盖、权限过滤 |
| API 配置 | `/admin/platform-apis` | `power` | 兼容原 `integration/platform-api` 的 CRUD 与执行测试 |
| 队列监控 | `/admin/runtime/queues` | `power` | queue key、等待数、运行数、租约、恢复状态 |
| 客户端运行态 | `/admin/runtime/clients` | `power` | running、waiting、limiter ratio、cooldown、last error |
| 限流策略 | `/admin/policies/rate-limit` | `power` | TPM、RPM、并发、队列长度、冷却策略 |
| 重试策略 | `/admin/policies/retry` | `power` | global/provider/platform/model/method 重试策略 |
| 任务审计 | `/admin/tasks` | `power` | 全量任务、attempt、失败切换、用户/模型/平台筛选 |
| 上传记录 | `/admin/uploads` | `power` | 主服务上传记录映射、任务关联、失败重试 |
| 结算 outbox | `/admin/settlements` | `manager` | 结算事件、重试、跳过、幂等 key |
| 回调 outbox | `/admin/callbacks` | `power` | 任务进度回调状态、失败原因、手动 replay |
| 测试模式 | `/admin/simulation` | `power` | simulation profile、失败注入、慢任务、dry-run billing |
| 系统设置 | `/admin/settings` | `manager` | server-main 地址、内部令牌、运行模式、安全开关 |

关键管理页面：

- 全局模型配置：维护 `model_catalog_providers`、`base_model_catalog`、`model_pricing_rule_sets`、`model_pricing_rules`，支持从旧项目 `integration-platform` 同步 provider 名称、code、图标和全量模型基准配置。基准模型库在管理端以模型卡片呈现，可按厂商、能力、名称筛选，并通过弹窗新增、修改、删除模型。
- 租户管理：维护 `gateway_tenants`，支持本地租户 CRUD、从 `server-main` 增量同步、禁用状态同步、租户策略和用量隔离审计。
- 用户管理：维护 `gateway_users`，支持本地用户 CRUD、从 `server-main` 增量同步、禁用状态同步、角色同步和同步差异审计。
- 用户组管理：维护 `gateway_user_groups`、`gateway_user_group_memberships`，支持从 `server-main` 同步成员关系；独立模式执行全部策略，接入模式下充值折扣由 `server-main` 执行，Gateway 执行调用折扣和并发/限流。
- 平台管理：创建平台时选择基准 provider、默认定价规则集和默认折扣系数；平台模型可继承平台规则集，或单独绑定规则集并配置模型折扣率。
- 队列与限流：展示 TPM/RPM 窗口、预占值、并发 lease、cooldown、next run。
- 回调 outbox：展示 Gateway 到进度回调目标的投递结果，支持按 task replay。

### 16.7 API 文档与在线调用测试

API 文档面向开发者，需要能完成从鉴权到真实调用的闭环。

| 页面 | 路由 | 权限 | 说明 |
| --- | --- | --- | --- |
| 文档首页 | `/docs` | `public/basic` | 快速开始、鉴权方式、模型列表、错误码、限流说明 |
| 鉴权 | `/docs/auth` | `public/basic` | JWT、OpenAPI `sk-*`、Header、权限范围、API Key 安全 |
| Chat | `/docs/api/chat` | `public/basic` | `/v1/chat/completions`、stream、工具调用、示例代码 |
| Responses | `/docs/api/responses` | `public/basic` | `/v1/responses` 请求、取消、结构化输出 |
| 图片 | `/docs/api/images` | `public/basic` | `/v1/images/generations`、`/v1/images/edits`、文件输入 |
| 视频 | `/docs/api/videos` | `public/basic` | `/v1/video/generations`、进度查询、结果取回 |
| 音频/语音 | `/docs/api/audio` | `public/basic` | speech、music、digital human 等接口 |
| Embeddings | `/docs/api/embeddings` | `public/basic` | embedding 模型、批量输入、维度说明 |
| 文件上传 | `/docs/api/files` | `public/basic` | 说明仍调用 `server-main` `/v1/files/upload` |
| 任务结果 | `/docs/api/tasks` | `public/basic` | `/v1/ai/result/:taskId`、取消、状态、回调语义 |
| 错误码 | `/docs/errors` | `public/basic` | 兼容错误结构、retryable 分类、限流错误 |
| 在线调试 | `/docs/playground` | `basic` | 选择模型、API Key、请求模板，执行在线调用测试 |

在线调试器：

- 左侧选择接口类型：Chat、Responses、生图、图像编辑、生视频、Embedding、语音。
- 中间为参数表单 + JSON editor 双模式，表单字段来自模型能力 schema。
- 右侧展示 curl、JavaScript、Python 示例代码和实时响应。
- 支持 stream 结果面板、任务进度时间线、结果预览、usage / billings。
- 支持测试模式开关；开启后标记 `simulation`，不触达真实 provider、不真实扣费。
- 调试前展示当前 API Key 的权限、余额、TPM/RPM/并发限制。

### 16.8 前端模块目录

```text
apps/web/src/
  app/
    routes.tsx
    providers.tsx
  components/
    ui/                 # shadcn-ui generated components
    layout/
    data-table/
    json-viewer/
    status-badge/
  features/
    auth/
      login/
      register/
    home/
    models/
    workspace/
      overview/
      billing/
      api-keys/
      tasks/
    admin/
      dashboard/
      catalog/
      tenants/
      users/
      user-groups/
      pricing/
      platforms/
      platform-apis/
      runtime/
      policies/
      tasks/
      callbacks/
      uploads/
      settlements/
      simulation/
      settings/
    docs/
      api-reference/
      playground/
  lib/
    api-client.ts
    auth.ts
    format.ts
    query-keys.ts
  contracts/
    dto.ts              # 或从 packages/contracts 引入
```

### 16.9 前端数据流

- 使用 `@tanstack/react-query` 管理列表、详情、轮询和 mutation。
- 表格使用 `@tanstack/react-table`，配合 shadcn `Table`。
- 表单使用 `react-hook-form` + `zod`，校验策略与后端 DTO 对齐。
- 用户工作台在独立模式下调用 Gateway 本地身份/账务模块；接入 `server-main` 时余额、充值、API Key 生命周期优先调用 `server-main` API，Gateway 展示执行侧任务、模型能力、用户同步状态和用户组执行策略。
- Gateway 控制台事件统一封装为 `useTaskEvents(taskId)`；业务前端实时进度仍由原 WebSocket 网关推送。
- 所有 destructive action 使用 shadcn `Dialog` 二次确认。
- 页面级权限由路由 loader 或 wrapper 判断，不在每个按钮里重复散落判断。

## 17. 迁移计划

### Phase 0：脚手架与契约

- 建立 monorepo、Go API、React 控制台、PG migration。
- 前端接入 `shadcn-ui`，建立基础组件目录和主题 token。
- PG 目标版本为 PostgreSQL 18；复用 Agent memory 的 `easyai-pgvector` 实例，但使用独立数据库 `easyai_ai_gateway`，避免与 `easyai_memory` 的记忆表混库。
- 容器网络内默认连接串为 `postgresql://easyai:easyai2025@easyai-pgvector:5432/easyai_ai_gateway?schema=public`；宿主机直跑时必须改成宿主机可访问的 host/port，例如 `localhost` 或实际映射端口。
- 如果只提供 `MEMORY_DATABASE_URL`，Go 侧只借用其中的 host/user/password，并按 `AI_GATEWAY_DATABASE_NAME` 替换数据库名；同时会把 Prisma 风格的 `schema=public` 转成 PostgreSQL `search_path` 参数。
- 建立 JWT / API Key 授权中间件骨架和 `standalone` / `server-main` / `hybrid` 身份模式配置，默认 `hybrid`。
- 固化 API、事件、数据库设计。
- 建立 simulation / dry-run 模式配置与安全开关，默认禁止生产环境普通用户随意开启。
- 建立基准 provider、基准模型库、定价规则集、用户、用户组策略、邀请码、本地 API Key、钱包、充值订单和 TPM/RPM/并发限流表结构。

### Phase 1：模型库 + 首批生成能力

第一阶段只做可落地的核心闭环：模型库、大模型对话、生图、图像编辑。Client 只迁移 OpenAI 和 Gemini 两个，视频和其他 provider 后移。

- 默认导入原 `integration-platform` 的全量 provider 和基准模型，完整保留模型 `types`、`capabilities`、图标、计费和限流元数据；Phase 1 只迁移 OpenAI、Gemini 两个 Client 的真实调用实现。
- 建立全局模型配置：模型类型、上下文、多模态能力、图片输入/输出能力、stream 支持、定价规则集。
- 平台创建支持选择基准 provider、默认定价规则集、默认折扣系数；平台模型支持 follow 平台规则集、模型级规则集覆盖、折扣继承和自定义覆盖。
- 建立 `gateway_users` 同步副本和 `gateway_user_groups` 策略解析，支持本地用户与 `server-main` 用户在同一套策略链路里命中不同折扣和限流。
- 建立本地账号闭环：普通注册可选邀请码，独立模式支持本地 API Key、余额、充值订单和钱包流水。
- 迁移大模型对话路由：`/chat/completions`、`/v1/chat/completions`，并为 `/responses` / `/v1/responses` 保留兼容契约。
- 迁移生图和图像编辑路由：`/images/generations`、`/v1/images/generations`、`/images/edits`、`/v1/images/edits`。
- 建立 OpenAI Chat / Image Client 与 Gemini Chat / Image Client 的 Base Client 实现和 contract test。
- 支持文本输入/输出 token 计费，图像按分辨率、质量、数量、生成/编辑模式计费。
- 实现调用 `server-main` 开放上传接口的 `ServerMainUploadClient`，覆盖参考图、mask、provider 临时 URL 转存、base64 小文件上传。
- 实现 SimulationClient，支持 Chat、生图、图像编辑的成功、失败、慢任务、限流、未知提交态等 profile。

### Phase 2：路由、队列与稳定性补强

- 迁移 `IntegrationModelFactory.assignClientsByModelName` 行为。
- 建立 Base Client 生命周期：构建参数、提交任务、轮询结果、归一化结果、估算计费、取消任务。
- 实现客户端失败切换策略：上一个客户端失败后按配置切到下一个候选客户端。
- 补队列持久化、TPM/RPM/并发限流、重启恢复、重试、超时、任务事件。
- 实现任务进度 callback outbox，将 Gateway 进度回调给实时推送通道；同时保留 Gateway 控制台 SSE / WebSocket 和 OpenAI stream。
- 为 OpenAI、Gemini 增加 retry classification test、billing snapshot、progress event snapshot。

### Phase 3：server-main 切薄门面与灰度

- `OpenaiService` 内部先将 Chat、生图、图像编辑切到 Gateway HTTP SDK。
- 结算事件接入 `server-main` 扣费链路。
- 前端逐步改 `Gateway API Base URL`。
- 开启 shadow / dry-run，比对旧实现和 Gateway 的候选模型、预估扣费、参数预处理结果。

### Phase 4：视频与更多 provider

- 在 Phase 1 稳定后再迁移生视频、音频、Embedding、音乐、数字人等能力。
- 迁移 RunningHub、Jimeng、Vidu、Kling、Hunyuan Video、Suno 等 provider。
- 对 app-style provider 补 `assignClientsByProviderMethod` 和 `provider + methodName` 队列 key。
- 每个 provider 增加 contract test、retry classification test、billing snapshot。

### Phase 5：server-main 清理旧实现

- 删除或冻结 `server-main` 中重复的 runtime client。
- 保留必要 BFF、历史、账单、文件上传能力。

## 18. 验收标准

- `pnpm nx run api:migrate` 可在 Agent memory PostgreSQL 中初始化 AI Gateway 表。
- `pnpm nx run api:test` 通过。
- `pnpm nx run web:build` 通过。
- 控制台使用 `shadcn-ui` 组件，不出现散落的独立 UI 体系。
- 使用 `server-main` 签发的 JWT 能访问 `/api/v1/me`。
- OpenAPI `sk-*` 能委托 `server-main` 校验并获得用户 claim。
- 原 `integration-platform`、站内生成、`/v1/*` 核心路由可用，DTO 与响应结构兼容。
- 基准 provider、基准模型库、基准价格和默认限流模板可在控制台维护。
- 用户、用户组、成员关系、充值折扣、调用折扣、TPM/RPM/并发策略可在管理工作台维护；独立模式可本地闭环，接入模式下充值执行仍以 `server-main` 为事实源。
- 接入 `server-main` 时，用户、角色、禁用状态和用户组成员关系支持增量同步；任务保存用户和用户组策略快照。
- 平台创建可设置默认折扣系数；平台模型未配置时 follow 基准模型，配置折扣时按基准价折扣计算，自定义覆盖时覆盖价格和能力。
- 文本定价支持输入/输出 token 分开计费；图像定价支持分辨率和质量权重；视频定价支持时长、分辨率、有无音频等权重。
- estimated billing 与真实任务 billings 使用同一个 effective pricing resolver，不能出现预估和真实路由价格来源不一致。
- 队列任务在服务异常重启后可恢复：已提交供应商的任务继续 poll，未提交任务重新排队。
- 限流策略覆盖 TPM、RPM、并发，并支持平台、客户端、基准模型、平台模型、方法、租户、用户组、用户、API Key 维度。
- TPM/RPM 一分钟窗口和并发 lease 在服务重启后可恢复或释放，不产生永久占用。
- 多客户端候选下，一个客户端可重试失败后按策略切换下一个客户端；禁用重试时必须直接失败。
- Base Client 架构覆盖构建参数、提交任务、取回结果、归一化结果、计费估算、取消任务。
- 任务中间进度可通过 SSE / WebSocket / stream 返回，并支持断线重放。
- 业务前端实时进度通过 `TASK_PROGRESS_CALLBACK_URL` 回调到 `server-main` 内部任务进度接口，再由主服务复用原 WebSocket 网关推送；callback outbox 支持失败重试和手动 replay。
- 文件上传统一调用 `server-main` 开放上传接口，Gateway 不维护自己的 OSS 配置。
- 测试模式下不会向真实平台提交任务、不会真实扣费、不会调用主服务生产上传接口，但会完整经过路由、队列、限流、重试、进度和结果归一。
- 测试模式支持 profile 注入可重试错误，能验证客户端失败切换；禁用重试时能验证直接失败。
- Phase 1 至少完成 Chat / 生图 / 图像编辑各一个 provider 的端到端任务；生视频放到 Phase 4 后续迁移。
- 结算事件在 `server-main` 幂等扣费。
- 同名模型跨平台权限过滤与旧逻辑一致。

## 19. 本轮设计复核结论与已确认决策

本轮复核后已修正：

- 身份边界：不再写成“用户/租户只能留在 `server-main`”，改为 `standalone`、`server-main`、`hybrid` 三种模式。
- 多租户：补 `gateway_tenants`，并把用户、任务、平台可见性、限流和策略解析都接入租户上下文。
- 登录注册：补 Gateway 本地账号注册登录接口和前端登录页规划。
- 进度回调：统一表述为回调 `server-main` 内部任务进度接口，不直接回调 ws-gateway。
- Phase 1 范围：继续限定为模型库、Chat、生图、图像编辑、OpenAI 和 Gemini；视频迁移放到后续阶段。

已确认决策：

1. 默认身份模式就是 `hybrid`，用于同时支持本地账号和 `server-main` token / API Key。
2. 普通注册支持填写邀请码，但邀请码不是必填项；填写后只记录邀请关系，不决定租户、用户组或角色。
3. 先做本地闭环：独立模式下本地 API Key、余额、充值订单和钱包流水由 Gateway 承接。
4. 当前阶段仅允许全局管理员配置 provider / platform 凭证和全局模型，不开放租户管理员自助配置凭证。
