当前浏览的是 v7 预览版文档(v7 尚未正式发布,API 可能随版本调整)。查看对应的 v6 正式版文档
@ikenxuan/amagi

响应类型

v7 的响应类型模型与三条逃生舱(typeMode 已移除)

响应类型

v7 移除了 typeMode。 v6 的 typeMode: 'strict' | 'loose' 是一个「写了 两万六千行类型、默认却返回 any」的中间态。v7 只有一种模式:响应类型始终是 端点声明里的具体类型,不需要任何参数开关。v6 代码里的 typeMode 参数可以直接 删除。

一种模式:具体类型 + 判别联合

success 是唯一判别键。收窄之后 data 才可读;失败分支没有 data,只有 error:

const  = await ...({
  : 'BV1xx411c7mD'
})

if (.) {
  .data
AmagiSuccess.data: VideoInfo_V0

端点声明的返回类型

} 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.View
'r.data' is possibly 'undefined'.

2. 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
newField: number
}

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.coderesult.error.platform?.code ?? result.error.code(失败分支)

最后更新于 2026-10-23

On this page