响应类型
v7 的响应类型模型与三条逃生舱(typeMode 已移除)
响应类型
v7 移除了 typeMode。 v6 的 typeMode: 'strict' | 'loose' 是一个「写了
两万六千行类型、默认却返回 any」的中间态。v7 只有一种模式:响应类型始终是
端点声明里的具体类型,不需要任何参数开关。v6 代码里的 typeMode 参数可以直接
删除。
一种模式:具体类型 + 判别联合
success 是唯一判别键。收窄之后 data 才可读;失败分支没有 data,只有 error:
const = await ...({
: 'BV1xx411c7mD'
})
if (.) {
.data} else {
// 失败分支:kind / code / message / platform 原文码都在 error 上
.(.., ..)
}四种读法
success 是唯一判别键,但「先判断再读」不是唯一写法。四种读法的区别只有一处:
得到的 data 是 T 还是 T | undefined。
1. 直接读(不收窄)
未收窄的信封上 data 与 error 都可读,类型各带 | undefined。这就是 v6 的读法,
而且比 v6 诚实 —— v6 两支都声明 data: T,掩盖了「这次可能是失败」:
const = await ...({ : 'BV1xx411c7mD' })
// 类型是 BiliOneWork_V0 | undefined
const = .
.(?.View)代价是访问其属性前需先处理 undefined,否则无法通过编译(页面上这段报的就是真实错误):
r.data.View2. if (r.success) 收窄
上一节那段就是这种写法:收窄之后 data 是 T(不带 | undefined),
else 分支里 error 是 AmagiError。日常首选。
3. filter(isSuccess)
数组回调里没有 if 可用 —— 这是 ?: undefined 解决不了的场景,
所以顶层导出了两个类型守卫:
const = await .([
...({ : 'BV1xx411c7mD' }),
...({ : 'BV1yy411c7mE' })
])
// BiliOneWork_V0[],不是 (BiliOneWork_V0 | undefined)[]
const = .().(() => .)
.(.)isFailure 与它对称,收窄之后可以直接按 error.kind 分流。
4. unwrap:失败即抛
愿意让失败沿调用栈往上冒的人用它 —— 返回类型是 T,不是 T | undefined:
const = (await ...({ : 'BV1xx411c7mD' }))
.(.)抛出的是 AmagiThrownError(Error 子类):kind / code / message / retryable
等字段全部平铺在实例上,catch (e) { (e as AmagiError).kind } 直接可用,
原始 error 对象在 e.error 上,error.cause 原样进 Error.cause。
管线本身永不 reject(参数校验失败、平台业务错误、网络错误、内部异常都映射为
success: false)。unwrap 是调用方显式把失败转成异常,不是管线行为变了。
要整体回到 v6 的语义(含顶层 code)请用 @ikenxuan/amagi/compat,
两者不重叠。
失败分支里的 error.challenge
失败信封的 error 上还有一个可选字段 challenge。平台判出 kind: 'risk' 且拿到验证信号时,它是 { url, ... }:url 是人能直接打开的验证页(已补协议);抖音从响应体通道识别时还带 jsSdkUrl / verifyData,可以自建验证页。
import , { } from '@ikenxuan/amagi'
const = ({ : { : '' } })
const = await ...({ : '7419965435527974176' })
if (!. && ..) {
const = (.., { : 'douyin' })
.(.)
}处理三步:把 url 交给真人打开(或把 renderVerifyPage 返回的 HTML 自己起路由返回);等真人过完;用同一份 cookie 重发原请求。amagi 不识别、不模拟轨迹、不自动过滑块。
覆盖范围:抖音 / 快手 / 小红书装了提取器,B站没有(error.challenge 恒为 undefined)。抖音的 WAF / VMP 这类纯工作量证明在管线内自动解掉并立即重放(trace.reason === 'challenge',不退避);滑块必须真人拖,判 CAPTCHA_REQUIRED 且不进重试名单。error.challenge 不受 debug 影响 —— client、HTTP 路由、静态 fetcher 三个入口都拿得到。
字段表
以下五张表由 packages/core 的 TypeScript 源码在构建期生成(<auto-type-table>),
字段名、类型、说明全部取自源码的 TSDoc。
AmagiSuccess
Prop
Type
AmagiFailure
Prop
Type
AmagiError
Prop
Type
AmagiMeta
Prop
Type
ClientOptions
Prop
Type
稳定性承诺
响应类型是实测快照 —— 按真实响应写下来的形状,不是平台的契约。因此:
- 平台新增字段不算破坏性变更(类型可能一时没有它)
- 平台删除/ 改名字段时,类型会随修复版本更新
- 每个响应类型都带顶层索引签名,读未声明字段返回
unknown而不是编译错误
三条逃生舱
当类型与真实响应不一致时,按代价从低到高选:
1. 索引签名(读未声明字段)
const = await ...({ : 'BV1xx411c7mD' })
if (.) {
// 未声明字段:类型是 unknown,自己收窄
const = .['newFieldFromPlatform']
if (typeof === 'string') .(.())
}2. 显式泛型覆盖返回类型
interface MyShape {
: { : string; : number }
}
const = await ...<MyShape>({
: 'BV1xx411c7mD'
})
if (.) {
...newField}3. 类型断言(最后手段)
const = await ...({ : 'BV1xx411c7mD' })
if (.) {
const = (. as <string, unknown>).newField
void
}从 v6 迁移
| v6 写法 | v7 写法 |
|---|---|
fetchX({ ..., typeMode: 'strict' }) | fetchX({ ... })(删参数,类型本就具体) |
fetchX({ ..., typeMode: 'loose' }) | fetchX({ ... }) + 需要时用上面的逃生舱 |
result.data(未收窄) | if (result.success) result.data |
result.code | result.error.platform?.code ?? result.error.code(失败分支) |
最后更新于 2026-10-23