自定义请求
用 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.error | err.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 装不下的四样。
| 字段 | 类型 | 作用 |
|---|---|---|
sign | SignDecl | 本次怎么签。注册器名(string)、false(不签),或一份 SignStep[] 反爬参数清单(见精细控制反爬参数)。不给按平台默认 |
cookie | false | 本次不带 cookie |
signPath | string | 签名用的接口路径。不给时从 URL 反推 pathname |
dropHeaders | readonly 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,小红书没有默认重试。
cookie 与删头
实例上绑的 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)再依次跑,所以顺序写反也不会错。
| 命名空间 | step | phase | 作用 |
|---|---|---|---|
douyin | msToken(len) | token | 本地随机令牌,写进 URL 的 msToken,len 是随机段长度 |
douyin | aBogus() | sign | a_bogus 主签名,内部先补 webid、把 verifyFp / fp 对齐到 cookie 的 s_v_web_id |
douyin | xBogus() | sign | X-Bogus 主签名,内部先补 webid、把 verifyFp / fp 对齐到 cookie 的 s_v_web_id |
douyin | secsdk() | finalize | x-secsdk-web-signature,改写整条 URL,仅对策略表内的 path 生效 |
douyin | douyinBogus(len) | 组合 | 预设 [msToken(len), aBogus(), secsdk()],主流接口用 |
douyin | douyinXBogus(len) | 组合 | 预设 [msToken(len), xBogus(), secsdk()],需要 X-Bogus 的接口用 |
xiaohongshu | xs(method, protocol) | sign | 一次注入 x-s / x-s-common / x-t / x-xray-traceid 四个头 |
xiaohongshu | traceId() | finalize | 补一个 x-b3-traceid 头 |
xiaohongshu | rap() | finalize | 补一个 x-rap-param 头 |
bilibili | wbi() | sign | URL 追加 wts / w_rid |
bilibili | qtparam() | sign | 视频流专属的整套签名 |
kuaishou | hxfalcon() | 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 可以就地改
.... = 8000create 里的 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取消不走信封。 取消时两条轨都 rejectCanceledError,这是「信封轨永不 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 里只有它走那个签名器。
快手的三种描述对象
| 返回类型 | 谁 | 携带什么 |
|---|---|---|
KuaishouH5Request | videoWork / videoWorkFull / comments | method(恒 'POST')/ body / signPath / referer / requiresSign |
KuaishouLiveApiRequest | live_api 那批(userInfoById / profilePublic / liveDetail …) | method(缺省 'GET')/ body? / signPath? / requiresSign? |
KuaishouGraphqlRequest | emojiList / 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