错误处理与重试
SDK 永不 process.exit。错误只从三个口子出来:装配期 / 选项期以 TansrSdkError 抛出;运行期的模型与工具错误恒经事件流表达(turn.error / tool.failed / turn.aborted);平台网关的错误码则藏在这两者的载荷里。先判清是哪一类,再决定重不重试。
第一类:TansrSdkError(装配期 / 选项期)
Section titled “第一类:TansrSdkError(装配期 / 选项期)”这些都是同步或首次 next() / await createSession() 时抛出的编程错误或环境错误,重试无益,读消息改代码或改环境:
| code | 时点 | 含义与处置 |
|---|---|---|
invalid_options |
同步 | 选项组合非法(三档字段混用 / 词表外工具名 / 令牌档带本地配置 / 显式 adjudication 与 permission.mode 同现)。读消息改代码 |
assembly_failed |
装配期 | 托管 / 令牌装配失败(配置不可用 / 别名解析失败 / bundle 拉取失败);cause 保真底层错误 |
capability_disabled |
装配期 | 能力位关而被显式选择;消息含控制台修复指引 |
session_closed |
运行期 | close() 后仍调用会改状态的方法 |
session_not_found |
装配期 | resume 目标不在 store——list() 核对或新建 |
session_store_corrupted |
装配期 / 落卷 | 会话存储损坏——恒不静默;删会话目录重新开始 |
checkpoints_not_wired |
运行期 | 快照存储未接线却调了 checkpoint() / listCheckpoints() / deleteCheckpoint() 或显式 compact({ checkpoint: true }) |
checkpoint_store_corrupted |
运行期 | 快照文件损坏;deleteCheckpoint() 该 id 后继续 |
turn_running |
运行期 | 运行中调了空闲期专属方法;await session.idle() 后重试——这是唯一「等一下再来」的 SDK 错 |
checkpoint_not_found |
运行期 | fork() / exportCheckpoint() 指名的快照不存在 |
checkpoint_import_invalid |
运行期 | 导入字节不是合法导出体;篡改即拒,零落盘 |
session_store_not_wired |
运行期 | fork() 需要会话 store 而未给 |
platform_request_failed |
运行期 | 令牌档直连 session.platform.transcribe() / speak() 失败(PlatformRequestError);status / platformCode / platformMessage 携网关事实,status 0 = 未达平台。0.19.0 起 createPlatformNonchatClient 的非聊天操作失败也用此码,但抛 PlatformNonchatError(status / platformCode / outcomeUnknown / taskId?):outcomeUnknown: true 表示提交结果未知——保留原幂等键与 taskId,用 getTask() 查原任务,不要换键重提 |
lifecycle_failed |
收尾期 | SdkLifecycleError:query() 的收尾结算失败(会话级 drain() / closeAsync() 不抛,以 status: 'failed' 的结果表达);failures 逐项列阶段与码,cleanup 在场时可再调用继续回收,result 在场时是已完成的 QueryResult(见生命周期与清理) |
import { TansrSdkError, createSession } from '@tansr/sdk';
try { session = await createSession({ token, baseUrl, store, resume: { sessionId, store } });} catch (e) { if (e instanceof TansrSdkError && e.code === 'session_not_found') { session = await createSession({ token, baseUrl, store }); // 确实不在 store 才新建 } else { throw e; // session_store_corrupted 等恒上抛,不静默开空会话 }}第二类:事件流里的错误
Section titled “第二类:事件流里的错误”| 事件 | 含义 | 你该做什么 |
|---|---|---|
turn.error { scope, message, recoverable, errorKind?, detail? } |
一轮内的错误 | 看 recoverable:true 是咨询(持久化失败、压缩失败、平台提示),进 SessionView.notices,会话继续;false 是本轮失败,进 lastError |
turn.aborted { reason } |
一轮被中止 | reason 是终局词:用户 interrupt() 为 aborted_*;max_turns / budget_exceeded 是触顶;client_gone 只在会话服务形态出现 |
tool.failed { errorType } |
工具执行失败 | timeout / unknown_tool 等;模型会看到结构化失败结果并自行改道,通常不需要你介入 |
tool.completed { isError: true } |
工具跑完但结果是错误 | 媒体四工具的供应商失败与超时走这一形;data.errorCode 携平台码 |
tool.permission.decided { allow: false, decisionSource } |
权限拒绝 | policy = 没接 askUser 的 fail-closed 拒绝;接上 permission.askUser 或调整规则 |
turn.error.detail 是可选结构化位。SDK 0.13.0 已提供:平台套餐四码携 detail: { scope: 'plan' },与会话服务 HTTP 面的 error.detail.scope 同键同值,便于端上区分「本机限流」与「套餐配额」。
第三类:平台网关错误码
Section titled “第三类:平台网关错误码”令牌档下,平台的错误码会出现在 turn.error 文案首段、tool.completed.data.errorCode、PlatformRequestError.platformCode / PlatformNonchatError.platformCode,以及你服务端换发令牌的响应里。全部码的权威表在错误码表,这里只讲最常撞到的十几个与处置:
该重试,按 Retry-After 等
Section titled “该重试,按 Retry-After 等”| 码 | HTTP | 处置 |
|---|---|---|
rate_limited |
429 | 平台限流,恒携 Retry-After(整秒)。按其值等待后重试;不要更密地重发 |
upstream_error / upstream_disconnected |
502 | 上游模型侧故障;指数退避 + 抖动后重试 |
upstream_timeout 及 upstream_connect_timeout / upstream_ttfb_timeout / upstream_idle_timeout / upstream_total_timeout |
504 | 上游超时;同上退避 |
plan_gate_unavailable |
503 | 平台守门依赖瞬态不可用,Retry-After: 5;既有会话不受影响 |
计划用量类 429:零自动重试
Section titled “计划用量类 429:零自动重试”| 码 | HTTP | 处置 |
|---|---|---|
plan_concurrency_exceeded |
429 | 套餐并发硬帽,Retry-After: 30。只拒新会话,运行中的会话恒不断。 SDK 零重试——原样重发在窗内注定再拒,多会话宿主自动重试只会惊群。让用户 30 秒后手动再发;在你的监控上把 detail.upgradeUrl 亮出来,这是开发者的升档信号 |
plan_end_users_exceeded |
429 | 测试期月度终端用户数触顶,仅铸令牌径 |
quota_exceeded 与各面 *_quota_exceeded |
429 | 日配额触顶;等配额窗刷新,不要重试 |
不该重试,改配置或改代码
Section titled “不该重试,改配置或改代码”| 码 | HTTP | 处置 |
|---|---|---|
unauthorized / token_expired |
401 | 会话令牌过期或被吊销。这是唯一会让令牌失效的码——回你的服务端重新换发。端侧 SDK 经登录态重取令牌至多 2 次,仍 401 即进入终态,由宿主显式重连 |
insufficient_balance |
402 | 应用钱包余额不足;充值后自愈,重试无益 |
model_not_authorized |
403 | 模型引用名不在应用授权面;控制台开授权 |
forbidden |
403 | 环 3 能力位关(网关强制面),detail 携开位指引 |
plan_required / plan_tier_insufficient |
403 | 应用所属账号无有效套餐或档位不含该能力——开发者装配错误,不该到终端用户;detail.upgradeUrl 指向控制台套餐页 |
payload_too_large |
413 | 超体帽(如语音转文字原始音频 > 24 MiB,detail.maxRawBytes);切分或压缩,不可重试 |
validation_failed / bad_request |
400 | 载荷形状非法;读消息改请求 |
session_archived |
410 | 平台会话已归档(30 天不活跃租约到期后);这条会话不再能续,resume 会失败,请新建会话 |
context_window_exceeded |
400 | 请求超出模型上下文窗口;先 compact() 或换更大窗口的模型 |
内核已经帮你做的重试
Section titled “内核已经帮你做的重试”模型请求层的瞬态失败不需要你重试:fallback 链上每个条目自带重试(退避 500 ms → 30 s、总尝试 3 次、优先尊重 Retry-After),耗尽后切下一条目并签发 cost.provider.switched(from / to / reason / attempts / failures)。上下文真溢出(上游 413)时内核会强制压缩一次再重试。你的重试逻辑只需要覆盖上面两张「该重试」表里的会话级失败。
在会话服务形态怎么看
Section titled “在会话服务形态怎么看”用 @tansr/serve 时,同一批平台码经服务端映射后到达移动端:套餐并发帽映射为 429 rate_limited + Retry-After + detail: { scope: 'plan' };plan_required / plan_tier_insufficient 以 403 原码透传 + detail.scope: 'plan';上游限流 / 5xx / 网络错映射为 503 upstream_unavailable。端侧退避纪律不变:429 / 503 携 Retry-After 时按整秒值等待,5xx 无 Retry-After 时指数退避 + 抖动。详见v2 协议与 webhook。
上面说的是 /v2 直连面的错误体 { error: { code, message, detail? } },它的字节在 @tansr/serve 0.13.0 … 0.15.1 保持不变;0.15.0 起旧前缀响应只多出 Deprecation / Link 两个响应头(@tansr/api-client 0.3.x / 0.4.0 均忽略),体与状态码不变。
经 /api 时的统一错误信封
Section titled “经 /api 时的统一错误信封”@tansr/serve 0.13.0 起(2026-10-02 发布)新集成走统一调用面 /api,所有 ≥ 400 的 JSON 错误不再按族各出一形,而是同一个统一信封:
{ "contract": "unified-v1", "traceId": "<x-request-id>", "requestId": "<客户端幂等键或 null>", "code": "capacity_exceeded", "status": 429, "retryAction": "same-request", "message": "...", "detail": { "domainCode": "rate_limited", "domainStatus": 429 } }code是 19 个统一码之一(invalid_request/protocol_mismatch/unauthorized/forbidden/not_found/method_not_allowed/gone/conflict/stale_generation/gap/capability_unavailable/capacity_exceeded/payload_too_large/upstream_unavailable/result_unknown/rejected/internal_error,加门面自有的precondition_failed与not_canonical);原族码(如本页上文的rate_limited、session_ended、session_not_found)只保留在detail.domainCode,原 wire 状态在detail.domainStatus。一套switch (code)覆盖全部域,需要族级细分时再读domainCode。retryAction是 6 个动作之一,它决定你的下一步:none不可重试;same-request同键同请求重放(携Retry-After/detail.retryAfterMs时先等);query-status先查回执 / 状态;rebind重建绑定;refresh重读修订后再试;rediscover重新能力发现后再试。result_unknown/commit_unknown只允许query-status或rebind,禁止换一个键重发——那等于承认重复副作用。- 0.14.0 起三个请求头
Idempotency-Key/If-Match+ETag/deadline的拒绝也走这同一信封,不新增错误码,细因在detail.reason(如idempotency_key_reused、if_match_stale、deadline_exceeded);412 precondition_failed自此既可能是围栏陈旧(rediscover)也可能是资源版本陈旧(refresh),按detail.domainCode区分。 - 响应缺
tansr-contract头、或/api返回非 JSON / 代理页,一律按「合同不可用」上报,不能据此猜成旧版服务而切回/v2。
@tansr/api-client 0.3.0 起(2026-10-04 正式版;现行 0.4.0,2026-10-06,./api 子入口与根入口零变化)的 ./api 子入口把这套解码做成 ApiError(code / retryAction / domainCode / domainStatus / closureId 读法)与 retryAdvice / retrySameRequest 工具,result_unknown 恒降为 query-status。统一码全表、retryAction 来源映射与各族 domainCode 对照见 统一 API 的请求头与错误信封。
本文是否有帮助?
感谢反馈,我们会持续改进这篇文章。