5.5 KiB
异步任务供应商路由经验
更新日期: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 / textproviderProfileIdproviderTypemodelIdmodelRefbinding的协议、请求/响应 schema、提交路径、轮询路径模板
不要保存 API Key。恢复查询时应通过 providerProfileId 找当前配置里的最新 key。
2. 恢复查询必须优先使用任务路由
恢复查询时优先级应是:
task.invocationRoute- 兼容旧任务的
task.params.modelRef - 最后才是
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。