# 异步任务供应商路由经验 更新日期:2026-05-05 ## 背景 图片、视频、音频异步任务都有“提交任务 -> 保存远端任务 ID -> 后续查询状态”的链路。多供应商接入后,提交时使用的供应商不一定等于用户刷新页面后的默认供应商。 这次 HappyHorse 用户反馈的错误是: - 状态查询失败 - 接口返回“该令牌无权访问模型” - request id: `202605020707307592757188268d9d67zVDq7SC` ## 根因 异步任务只保存 `remoteId` 不够。 如果任务恢复查询时只依赖当前 `task.params.model`、当前默认供应商,或当前全局 API Key,就会出现: - 提交任务用的是供应商 A 的 key - 刷新后默认路由切到了供应商 B - 查询状态时拿供应商 B 的 key 去查供应商 A 的任务 - 服务端返回模型无权限、任务不存在或鉴权失败 这不是 HappyHorse 单模型问题,而是多供应商异步任务的通用一致性问题。 ## 修复规则 ### 1. 异步任务必须绑定创建时路由 任务进入异步轮询阶段时,必须保存轻量 `invocationRoute` 快照: - `operation`: image / video / audio / text - `providerProfileId` - `providerType` - `modelId` - `modelRef` - `binding` 的协议、请求/响应 schema、提交路径、轮询路径模板 不要保存 API Key。恢复查询时应通过 `providerProfileId` 找当前配置里的最新 key。 ### 2. 恢复查询必须优先使用任务路由 恢复查询时优先级应是: 1. `task.invocationRoute` 2. 兼容旧任务的 `task.params.modelRef` 3. 最后才是 `task.params.model` 或默认路由 只要任务上存在 `providerProfileId`,就进入严格模式,不允许静默 fallback 到默认供应商。 ### 3. 严格模式要显式失败 以下情况应直接给出配置错误,而不是换默认 key 继续查: - 原供应商配置已删除 - 原供应商被停用 - 原供应商 API Key 缺失 - 原供应商 Base URL 缺失 - 原模型 binding 已不可用 这样用户看到的是“原供应商配置不可用”,而不是误导性的“令牌无权访问模型”。 ### 4. 查询路径也属于供应商 binding 视频状态查询不能硬编码一种路径。不同供应商可能需要: - `/videos/{taskId}` - `/kling/v1/videos/{action}/{taskId}` - 其他自定义 poll path 轮询路径应来自 binding 的 `pollPathTemplate`,并支持用任务参数填充 `{taskId}`、`{action}` 等占位符。 ### 5. 同步和恢复必须 round-trip `invocationRoute` 必须覆盖这些路径: - IndexedDB 写入与读取 - 内存 task queue 同步 - 外部任务追踪 - fallback executor - 主线程 resume - GitHub sync compact / restore - SW channel 类型 如果某条链路漏掉字段,刷新或远程同步后仍会回到默认供应商查询。 ### 6. 异步图片恢复判定不能只依赖模型白名单 刷新恢复时,只要异步任务已经拿到远端任务 ID,就应该优先恢复轮询,而不是直接标记为“页面刷新中断”。 图片任务尤其容易踩坑:动态供应商模型不一定在内置 `ASYNC_IMAGE_MODEL_IDS` 白名单里,但任务创建时已经持久化了 `invocationRoute.binding`。因此恢复判定应满足: - 任务类型是 image - 已有 `remoteId` - 模型命中内置异步图片白名单,或 `invocationRoute.binding` 表明它是 `openai.async.media` / `openai.async.image.form` 这样页面刷新后,UI 会继续显示生成中并由执行器恢复轮询;只有真正的业务终态失败才进入失败界面。 不要为了隐藏错误去改 UI。失败界面只是任务状态的投影,根因应在任务恢复层修正。 ## 代码落点 - `task-invocation-route.ts`:创建、解析、严格校验任务路由快照。 - `core.types.ts`:任务模型增加 `invocationRoute`。 - `task-queue-service.ts`:创建任务、外部任务、storage sync 写入路由快照。 - `generation-api-service.ts` / `useTaskExecutor.ts`:image/audio/video 恢复查询优先用任务路由,并在严格模式下校验原供应商。 - `task-utils.ts`:集中判断异步图片任务是否可恢复,避免 storage 和 executor 使用不同条件。 - `fallback-executor.ts`:fallback 视频恢复查询使用原供应商。 - `video-binding-utils.ts`:统一视频状态查询路径模板。 - `github-sync`:远程同步保留 `invocationRoute`。 ## 验证清单 - 异步任务提交后,storage 中应有 `remoteId` 和 `invocationRoute`。 - 刷新页面后恢复 image/audio/video 任务时,请求应命中原 `providerProfileId` 对应的 key。 - 修改默认供应商后,旧异步任务仍应使用原供应商查询。 - 删除或停用原供应商后,任务应失败为明确配置错误,不应切到默认 key。 - 自定义 `pollPathTemplate` 的视频供应商应按模板查询状态。 - 动态异步图片模型即使不在内置白名单,只要保存了 async image binding 和 `remoteId`,刷新后也应继续轮询。 ## 经验规则 - 异步任务的远端 ID 只在原供应商上下文中有意义。 - 多供应商系统里,“模型名”不是稳定路由键;稳定键应包含供应商 ID 和模型 ID。 - 不要把 API Key 持久化进任务;持久化供应商 ID,查询时读取当前 key。 - 新增异步供应商时,必须同时补提交路径、轮询路径、恢复路径和同步路径测试。 - 任何状态恢复逻辑都要警惕静默 fallback,它会把配置问题伪装成模型权限问题。 - UI 失败态不应作为异步中断兜底;先确认任务是否已有可轮询的远端 ID。