当前浏览的是 v7 预览版文档(v7 尚未正式发布,API 可能随版本调整)。本页是 v7 新增内容,去 v6 正式版文档首页
@ikenxuan/amagi

自定义请求

用 client.<平台>.request 请求 amagi 尚未收录的接口,含双轨语义、amagi 子对象、反爬参数精调,以及 apiUrls 的返回形态

自定义请求

client.<平台>.request 是一个真正的 axios 实例,create、defaults、interceptors、params 序列化都是 axios 原生行为。区别只有一点,它把请求交给 amagi 的执行管线,签名、cookie、judge、退避重试、事件与 trace 全部自动附加。所以请求 amagi 尚未收录的接口时,这些处理不会缺席。

四个平台都有 request。静态面没有,amagi.douyin.request 不存在,因为静态面没有可绑定的实例 cookie。

import  from '@ikenxuan/amagi'
const  = ({ : { : '', : '', : '', : '' } })
..
..
..
..

两条轨

request 上挂着两条轨,结构相同,唯一的区别是失败时抛不抛异常。

request.<动词>request.axios.<动词>
返回AmagiResult<T> 信封AxiosResponse<AmagiResult<T>>
失败返回失败信封,不抛reject AxiosError
失败时错误在哪result.errorerr.response?.data.error
一次响应都没拿到失败信封(kind: 'network' / 'timeout')err.response 是 undefined

抛不抛只看信封,跟 axios 的状态校验无关,validateStatus 在这条路上不起作用。AxiosError 的 code 也来自 amagi 的错误码(比如 RISK_CONTROL),不是 axios 的 errno。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
const  = 'https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD'

// 信封轨:失败也是返回值
const  = await ...()
if (!.) .(.., ..)

// axios 轨:失败是异常,失败信封在 err.response.data 上
try {
  const  = await ....()
  .(.)
} catch () {
  .()
}

两条轨各有六个入口,五个动词 get / delete / post / put / patch,再加一个 request。动词是 (url, config?),地址单独传,其余写在 config 里;request(config) 把 method / url / data 一次给齐,适合摊开 apiUrls 返回的描述对象。两条轨共用同一个实例、defaults 和 interceptors。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })

// 动词形态:地址 + 配置
await ...('https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD')

// 配置对象形态:方法、地址、参数一次给齐
await ...({
  : 'GET',
  : 'https://api.bilibili.com/x/web-interface/view',
  : { : 'BV1xx411c7mD' }
})

用泛型标注返回类型

request 打的是裸接口,不跑 normalize,返回平台原始载荷,返回类型默认是 unknown。用泛型参数标上,收窄后的 data 就是它,两条轨都能用。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })

interface VideoView {
  : string
  : string
}

const  = await ...<VideoView>(
  'https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD'
)
if (.) {
  .. // 类型是 VideoView
}

请求体放进 config.data

五个动词统一是 (url, config),请求体走 config.data,这不是 axios 原生的 post(url, data, config)。这样五个动词长得一样,也跟 request({ url, method, data }) 一致。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
declare const : string
// 请求体在 config.data 里;其余选项照旧用 axios 自己的键
const  = await ...(, {
  : { : '3xqxxxx' },
  : 8000,
  : { : '/rest/wd/photo/info' }
})

第二参永远被当成配置。把业务字段直接当第二参传,字段不会进入请求体,请求照发、body 却是空的,返回往往是「成功加空结果」。顺手写进去的 params、timeout 这类同名键还会按配置生效,更难被发现。

纯 JavaScript 完全静默。TypeScript 能挡下大多数写法,但只要载荷里有一个键和配置同名就放行,下面这段就能编译通过。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
declare const : string
// 看着像「发这个 body」,实际是「拿它当配置」:photoId 丢掉,timeout 生效
const  = { : '3xqxxxx', : 8000 }
await ...(, )

用 amagi 子对象做单次控制

能用 axios 键表达的(timeout、responseType、params、headers、signal、proxy、baseURL 等)一律用 axios 自己的键。amagi 里只装 axios 装不下的四样。

字段类型作用
signSignDecl本次怎么签。注册器名(string)、false(不签),或一份 SignStep[] 反爬参数清单(见精细控制反爬参数)。不给按平台默认
cookiefalse本次不带 cookie
signPathstring签名用的接口路径。不给时从 URL 反推 pathname
dropHeadersreadonly string[]从平台基线里删掉的头(大小写不敏感),逃生舱

sign 不给时按平台默认。给注册名时写错,管线会抛 未注册的签名器 并收成失败信封,不是裸抛。

平台sign 不给时说明
抖音'a-bogus'另一个可选项是 'x-bogus';两者都会自动走 x-secsdk-web-signature 那一步,不用自己选
B站false(不签)27 条端点里只有 5 条签名。默认签会大面积静默签错,要签就显式给
快手'hxfalcon'live_api 与 H5 两套接口共用同一个签名器
小红书按 method 现算,POST 给 'xhs-post',其余给 'xhs-get''xhs-get-trace'(多一个 x-b3-traceid)推不出来,要用的显式传

裸请求的重试策略也按平台预置,语义和端点声明的 retryOn 一样。抖音是 ANTIBOT_PAGE(会重新签名换一套新令牌,重放同一套必被拦),B站是 RISK_CONTROL,快手是 PLATFORM_UNAVAILABLE,小红书没有默认重试。

实例上绑的 ck(createClient({ cookies: { <平台>: '...' } }))会自动带上。想「就这一次匿名」用 amagi.cookie: false。

import  from '@ikenxuan/amagi'
const  = ({ : { : 'SESSDATA=example' } })
// B站评论区会按账号改变热度池排序:这一次要的是未登录看到的那个顺序
const  = await ...('https://api.bilibili.com/x/v2/reply?type=1&oid=170001', {
  : { : false }
})

cookie: false 等价于 dropHeaders: ['cookie'],删头发生在所有 header 合并之后,你自己在 headers 里写的 Cookie 也会一并删掉。删头范围比 cookie 更大时才需要写 dropHeaders。

await ...(, { : { : ['origin', 'sec-fetch-site'] } })

signPath 何时要自己传

signPath 指定签名用的接口路径。不传时从 URL 反推 pathname,多数接口够用。签名路径和公开 URL 不一致时必须自己传,有两种。

  • 小红书有些接口的公开 URL 是 HTML 页地址(如 userProfile 的 https://www.xiaohongshu.com/user/profile/<user_id>),要签的是另一条 API 路径。
  • 快手如 userInfoById,URL 走 /rest/w 前缀,签名路径却是 /rest/k/user/info。
import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
declare const : string
await ...(, {
  : { : '3xqxxxx' },
  : { : '/rest/wd/photo/info' }
})

用 apiUrls 时它已经把 signPath 一起给出来了(小红书叫 apiPath,快手叫 signPath),照着填即可。

精细控制反爬参数

指定注册名(sign: 'a-bogus')就是选一套配好的签名,多数时候够用。需要换 msToken 长度、去掉某个收尾步骤、只签其中一项,或凑一套注册名没覆盖的组合时,从 @ikenxuan/amagi/sign-steps 引入原子 step 工厂,拼成一份清单交给 sign。step 工厂按平台分成四个命名空间对象(douyin、bilibili、kuaishou、xiaohongshu)。

import {  } from '@ikenxuan/amagi/sign-steps'

// 要 a_bogus,但把 msToken 换成 200 长度,且不追加 secsdk 收尾
await ...(, { : { : [.(200), .()] } })

清单里每个 step 对应一个反爬参数,自己知道怎么算、写进 query 还是 header。你只管挑哪些、传什么参数。step 不按你写的先后执行,runtime 先按 phase 稳定排序(prepare → token → sign → finalize)再依次跑,所以顺序写反也不会错。

命名空间stepphase作用
douyinmsToken(len)token本地随机令牌,写进 URL 的 msToken,len 是随机段长度
douyinaBogus()signa_bogus 主签名,内部先补 webid、把 verifyFp / fp 对齐到 cookie 的 s_v_web_id
douyinxBogus()signX-Bogus 主签名,内部先补 webid、把 verifyFp / fp 对齐到 cookie 的 s_v_web_id
douyinsecsdk()finalizex-secsdk-web-signature,改写整条 URL,仅对策略表内的 path 生效
douyindouyinBogus(len)组合预设 [msToken(len), aBogus(), secsdk()],主流接口用
douyindouyinXBogus(len)组合预设 [msToken(len), xBogus(), secsdk()],需要 X-Bogus 的接口用
xiaohongshuxs(method, protocol)sign一次注入 x-s / x-s-common / x-t / x-xray-traceid 四个头
xiaohongshutraceId()finalize补一个 x-b3-traceid 头
xiaohongshurap()finalize补一个 x-rap-param 头
bilibiliwbi()signURL 追加 wts / w_rid
bilibiliqtparam()sign视频流专属的整套签名
kuaishouhxfalcon()sign__NS_hxfalcon,live_api 与 H5 两套接口通用

默认的 'a-bogus' 其实就是 douyin.douyinBogus(184)。184 是抖音作品详情类接口的 msToken 长度,其余接口用 116。手挑的 step 和注册名、平台默认签名内部是同一批实现,签出来必然一致。两层结构的内部组织见开发文档的签名与反爬。

各平台挑 step 时要注意的点如下。

抖音这条链的顺序是 msToken → a_bogus / x_bogus → secsdk,msToken() 会覆盖 URL 里已有的 msToken(retryFresh 重试靠它换新令牌)。secsdk() 只对策略表内的 path 生效(musicInfo、作品详情等需要,emojiList / search 加了也是空操作)。三个 step 都要求绝对 URL,xBogus() 更严,pathname 至少 3 段且带查询串;不满足会得到 kind: 'internal' 的失败信封(一个请求都没发)。

想自己掌控 msToken(比如复用固定值),就不要把 douyin.msToken() 放进清单,只给 [douyin.aBogus(), douyin.secsdk()],URL 里自己拼的 msToken 会原样保留,代价是 retryFresh 重试时不换新令牌。

小红书的 xs(method, protocol) 一步注入四个头,method 选 'get' 签 query、'post' 签 body;protocol 传 'xys' —— 内置的 13 条端点全部用它,包括所有数据获取类接口。'xyw' 是另一套 AES-128-CBC 格式,目前没有端点在用、也没验证过能用,别照着猜选。签名只认路径,要通过 amagi.signPath 给。

import {  } from '@ikenxuan/amagi/sign-steps'

await ...({
  : 'POST',
  : 'https://edith.xiaohongshu.com/api/sns/web/v1/feed',
  : { : 'note-id' },
  : { : [.('post', 'xys')], : '/api/sns/web/v1/feed' }
})

小红书 GET 用 step 手挑时,query 签不进去。 GET 的 x-s 要把 query 一起签进去,而这份参数只有接口声明在 build 时才产出,裸请求路径产不出。所以小红书的 GET 数据接口优先走 fetcher,或用注册名 sign: 'xhs-get'(带 x-b3-traceid 的用 'xhs-get-trace');手挑清单适合 POST。traceId() / rap() 这两个只补头的 step 不受影响。

B站的 wbi() 给 URL 追加 wts / w_rid,qtparam() 是视频流专属,把登录判断、fnval 清晰度、wbi 签名整套封在一起,不能拆开自己拼。两者共用同一个签名器实例和 /nav 的 key 缓存,手挑和默认注册名不会多打一次 /nav。

快手的 live_api(/rest/k/*)和 H5(/rest/wd/*)共用 hxfalcon() 一个 step。它签名要 query、signPath 和 POST 的 body,其中 body 必须是普通对象(签名和 transport 各做一次 JSON.stringify,两边逐字一致才算签对)。先序列化成字符串再传会直接抛错,不会闷头签个错的。

可观测性

request 走的是和 fetcher 同一条执行管线,可观测性有两处。

  • 返回的信封带 meta,有 requestId(一次调用一个)、attempts(实际发出的底层请求数,含重试)、durationMs 等,和 fetcher 的信封同形。
  • 调用过程发到实例事件总线,api:success / api:error、http:request / http:response 等,和 fetcher 发的是同一批事件,挂在同一条 client.events 上。
import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })

.('http:request', () => {
  .(.., .., ..)
})

const  = await ...('https://www.douyin.com/aweme/v1/web/...')
if (.) {
  .(.., ..) // 与上面事件里的 requestId 一致
}

完整的事件清单和 meta 字段表见事件系统。

派生、默认值与拦截器

create / defaults / interceptors 就是底层那个 axios 实例的,按 axios 的习惯用。

import  from '@ikenxuan/amagi'
const  = ({ : { : 'ttwid=xxx' } })
// 派生一份带默认值的模块(axios 的 create)
const  = ...({ : 8000, : { : false } })

// 拦截器两条轨共用
.....(() => {
  ..('X-Trace', '1')
  return 
})

// defaults 可以就地改
.... = 8000

create 里的 amagi 是按字段分别合并的,从 create({ amagi: { cookie: false } }) 派生的模块再写 amagi: { sign: false } 不会把 cookie: false 冲掉。但 axios 的 create 不继承拦截器,原模块上 use 过的在派生模块上一个都没有,得重新 use。默认值继承、拦截器不继承,这个不对称是 axios 自己的语义。

与 fetcher 的分工

client.<平台>.fetcher.<方法>client.<平台>.request.<动词>
请求哪类接口注册表里声明过的端点任何 URL
参数端点声明的类型 + 运行时校验自己拼
返回端点声明的响应类型,已 decode / normalize平台原始载荷
翻页 / 多请求聚合端点声明负责一次一个请求,不管翻页
judge / 重试 / 事件 / trace同一套同一套

接口已经收录就用 fetcher,多-JSON({"a":1}{"b":2} 这种拼接响应)和 protobuf 都由端点的 decode 处理,request 给的是未解码的原始载荷。请求尚未收录的接口时,request 是唯一入口。

已知边界

  • 不替你拼业务参数。 签名器只补签名那一组,业务参数尽量从 client.<平台>.apiUrls 拿(抖音 21 个构造器里有 16 个带 verifyFp)。verifyFp / fp 的值不用操心:URL 上带了这两个参数时,aBogus() / xBogus() 会在签名前把它们改成 cookie 里的 s_v_web_id(浏览器里三者本就是同一个值),cookie 没有或形状不对就原样保留;但参数本身要不要带由你决定,签名器不会替你加。构造器盖不到的才自己拼(getGuestUserInfo 等四条免鉴权接口两者都没有)。抖音的签名器还会在命中 ttwid 缓存时注入 webid,冷启动时不动(不传安全、传错致命)。
  • signal 取消不走信封。 取消时两条轨都 reject CanceledError,这是「信封轨永不 reject」的例外,取消要自己 try 一次。
  • 相对 URL 会在签名前置校验处失败。 抖音的 a-bogus 要求绝对地址,传相对路径会得到 kind: 'internal' 的失败信封(一个请求都没发)。
  • adapter 传不了。 类型上被 Omit 掉,运行期还会再兜一次,传它等于绕开签名和信封。
  • responseType 只有 'json' / 'text' / 'arraybuffer'。 'blob' / 'stream' / 'document' 解析出来的东西装不进 amagi 的 RawResponse.body。
  • 要拿 HTML 必须写 responseType: 'text'。 不写时,非空字符串响应体一律判 ANTIBOT_PAGE —— 那条公共判据的前提是「打的是 JSON 接口,一张 HTML 回来必然出了事」,对打页面的用法不成立。写了 'text' 之后它对字符串不再发表意见,但平台自己的判据仍然先判:抖音的滑块中间页、VMP / WAF 挑战页、Argus 拦截文本,小红书的验证头,在 'text' 下照旧是失败信封。
  • 'text' 模式下小红书 / 快手 / B站的 WAF 页会被当成 success。 这三家没有 HTML 挑战页的锚点判据(抖音有:滑块认 TTGCaptcha / 验证码中间页,VMP 认 _$jsvmprt,WAF 认 _wafchallengeid / WAFJS),所以它们的拦截页在 'text' 下与一张正常页面分不开。这是显式声明 'text' 时接受的取舍,自己按内容再判一次。
  • 抖音打页面地址每次调用要发 2 个请求。 零 cookie 或缺挑战 cookie 时打 https://www.douyin.com/ 这类页面地址,第 1 发必定拿到 VMP 挑战页,管线纯算出 __ac_signature 并立即重放,第 2 发才是真页(2026-10-11 实测)。解出的挑战 cookie 只在本次调用内有效,不回写实例身份,所以下一次调用还是 2 发。
  • validateStatus 不生效。 失败判据只有信封。
  • 请求体不会被提前序列化。 你给对象就是对象,快手那种「body 参与签名」的接口签的和发的不会是两份东西。调用方显式给的 transformRequest 仍然生效。

与 client.<平台>.apiUrls 配对

client.<平台>.apiUrls 是 v7 的 URL 构造器,只构造,不发请求。v6 那份不在 v7 门面上,要它从包顶层拿(import { douyinApiUrls } from '@ikenxuan/amagi'),或走 @ikenxuan/amagi/compat。

返回形态每个平台不一样,别照抄。

平台构造器的返回
抖音URL 字符串(21 个构造器全部如此,query 已拼好)
B站以字符串为主(26 个里的 24 个);getCaptchaFromVoucher / validateCaptcha 返回 { Url, Body }
快手请求描述对象,共三种,KuaishouH5Request / KuaishouLiveApiRequest / KuaishouGraphqlRequest
小红书XhsRequestDescription,含 Url / Body? / apiPath

字符串那份直接就是请求地址。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
const  = await ...(...({ : '1234567890123456789' }))

B站那两条验证码接口返回 { Url, Body },Body 是普通对象,按 JSON 发就行,别自己加表单编码头。

import  from '@ikenxuan/amagi'
const  = ({ : { : 'SESSDATA=example' } })
declare const : string
const  = ...({ :  })
const  = await ...(., { : . })

小红书的 Url / Body / apiPath

三个大写键分别对应 url、config.data、amagi.signPath。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
const  = ...({ : 'note-id', : 'xsec_xxx' })
const  = await ...({
  : 'POST',
  : .,
  : .,
  : { : . }
})

GET 那几条(noteComments / userProfile / userNoteList / emojiList)没有 Body。userProfile 的 Url 是 HTML 页地址,要签的 apiPath 是另一条路径。userNoteList 还得显式给 sign: 'xhs-get-trace',四个 GET 里只有它走那个签名器。

快手的三种描述对象

返回类型谁携带什么
KuaishouH5RequestvideoWork / videoWorkFull / commentsmethod(恒 'POST')/ body / signPath / referer / requiresSign
KuaishouLiveApiRequestlive_api 那批(userInfoById / profilePublic / liveDetail …)method(缺省 'GET')/ body? / signPath? / requiresSign?
KuaishouGraphqlRequestemojiList / danmaku只有 type / url / body,没有 method

H5 那份几乎能直接摊进去,method 恒为 'POST',requiresSign 是必填布尔,referer 也在这份上。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
const  = ...({ : '3xqxxxx' })

const  = await ...({
  : .,
  : .,
  : .,
  // H5 接口按分享页来源校验 Referer,这份要自己带上
  : { : . },
  : {
    // requiresSign 是必填布尔:`ugH5App/*` 那几条为 false
    : . ? 'hxfalcon' : false,
    : .
  }
})

live_api 那份不能照抄上面这段。 它的 requiresSign 是可选的,缺席不等于免签,端点层和执行管线都不读它,真正决定签不签的是端点声明里的 sign。这批端点声明里给的恰恰是 sign: 'hxfalcon',连 requiresSign: false 的 liveDetail 也照签。所以别拿 requiresSign 反推,live_api 要么不给 sign(走平台默认),要么显式写 'hxfalcon'。它也没有 referer 字段,Referer 要按接口自己给。

import  from '@ikenxuan/amagi'
const  = ({ : { : '' } })
const  = ...({ : 'KPL704668133' })

const  = await ...({
  : ., // 'GET',query 已经拼在 url 里
  : .,
  // 不指定 sign 即平台默认的 'hxfalcon'
  : { : . }
})

GraphQL 那份要自己写 method: 'POST',body 交给 config.data。它完全免鉴权,amagi: { sign: false } 就够。

最后更新于 2026-10-17

On this page