@ikenxuan/amagi 7.0.0-beta.5 → 7.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-BWAjDuTS.d.ts +802 -0
- package/dist/default/index.cjs +1 -1
- package/dist/default/index.d.ts +2 -1
- package/dist/default/index.mjs +1 -1
- package/dist/exports/compat.cjs +1 -1
- package/dist/exports/compat.d.ts +13 -7397
- package/dist/exports/compat.mjs +1 -1
- package/dist/exports/sign-steps.cjs +69 -0
- package/dist/exports/sign-steps.d.ts +202 -0
- package/dist/exports/sign-steps.mjs +65 -0
- package/dist/{index-TDTBTnNP.d.ts → index-NkyAecv9.d.ts} +34937 -38466
- package/dist/{src-blqLvYiW.mjs → src-BM1fZuVp.mjs} +345 -2809
- package/dist/{src-DixrwqqA.cjs → src-CNU3-zoM.cjs} +373 -2837
- package/dist/steps-BO8NYQHx.cjs +2872 -0
- package/dist/steps-DQlwODdh.mjs +2732 -0
- package/package.json +10 -5
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import zod from "zod";
|
|
2
|
+
import { AxiosRequestConfig } from "axios";
|
|
3
|
+
//#region src/contracts/request.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 请求 / 响应契约。
|
|
6
|
+
*
|
|
7
|
+
* `RequestConfig` 在本文件定义并对外导出。
|
|
8
|
+
*
|
|
9
|
+
* `contracts/` 是零依赖叶子层:本文件只 type-import 外部包 `axios`,
|
|
10
|
+
* 不 import 仓库内任何其他模块。
|
|
11
|
+
*/
|
|
12
|
+
/** HTTP 方法 */
|
|
13
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
14
|
+
/**
|
|
15
|
+
* 调用方可传的请求配置。
|
|
16
|
+
*
|
|
17
|
+
* 形状即 `Omit<AxiosRequestConfig, 'url' | 'method' | 'data'>`,
|
|
18
|
+
* `amagi({ request: { timeout: 8000, proxy } })` 这类写法直接可用。
|
|
19
|
+
*/
|
|
20
|
+
type RequestConfig = Omit<AxiosRequestConfig, 'url' | 'method' | 'data'>;
|
|
21
|
+
/** 可以用来初始化或合并 {@link AmagiHeaders} 的输入 */
|
|
22
|
+
type HeadersInput = AmagiHeaders | Record<string, string | number | undefined | null> | undefined | null;
|
|
23
|
+
/**
|
|
24
|
+
* 大小写不敏感的 header 容器。
|
|
25
|
+
*
|
|
26
|
+
* HTTP header 名本身大小写不敏感,用普通对象装就会出现「写了 `Cookie` 却读
|
|
27
|
+
* `cookie` 读不到」这类静默失配。本类把「大小写」这个变量彻底消掉。
|
|
28
|
+
*
|
|
29
|
+
* 语义:
|
|
30
|
+
* - 查找、判断、删除全部大小写不敏感。
|
|
31
|
+
* - 写入是「后写覆盖」:值覆盖,**显示用的大小写也跟着最后一次写入**,
|
|
32
|
+
* 所以 `set('User-Agent', a)` 之后 `set('user-agent', b)` 只会留下一个
|
|
33
|
+
* `user-agent: b`,不可能出现两条同名 header。
|
|
34
|
+
* - `undefined` / `null` 的值视为「不写这个 header」,方便直接摊入可选字段。
|
|
35
|
+
*/
|
|
36
|
+
declare class AmagiHeaders {
|
|
37
|
+
/** key 是小写化的 header 名,value 保留最后一次写入的原始大小写与值 */
|
|
38
|
+
private readonly entries;
|
|
39
|
+
/**
|
|
40
|
+
* @param init - 初始 header,可以是另一个 `AmagiHeaders` 或普通对象
|
|
41
|
+
*/
|
|
42
|
+
constructor(init?: HeadersInput);
|
|
43
|
+
/**
|
|
44
|
+
* 读取 header 值,大小写不敏感
|
|
45
|
+
* @param name - header 名,任意大小写
|
|
46
|
+
* @returns 值,不存在时返回 `undefined`
|
|
47
|
+
*/
|
|
48
|
+
get(name: string): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* 判断 header 是否存在,大小写不敏感
|
|
51
|
+
* @param name - header 名,任意大小写
|
|
52
|
+
* @returns 存在则返回 `true`
|
|
53
|
+
*/
|
|
54
|
+
has(name: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 写入 header。同名(忽略大小写)时覆盖值与显示大小写
|
|
57
|
+
* @param name - header 名
|
|
58
|
+
* @param value - 值。`undefined` / `null` 表示不写入
|
|
59
|
+
* @returns 自身,便于链式调用
|
|
60
|
+
*/
|
|
61
|
+
set(name: string, value: string | number | undefined | null): this;
|
|
62
|
+
/**
|
|
63
|
+
* 删除 header,大小写不敏感
|
|
64
|
+
* @param name - header 名
|
|
65
|
+
* @returns 原本存在则返回 `true`
|
|
66
|
+
*/
|
|
67
|
+
delete(name: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* 合并另一组 header,后来者覆盖
|
|
70
|
+
* @param input - 待合并的 header
|
|
71
|
+
* @returns 自身,便于链式调用
|
|
72
|
+
*/
|
|
73
|
+
merge(input?: HeadersInput): this;
|
|
74
|
+
/** header 条数 */
|
|
75
|
+
get size(): number;
|
|
76
|
+
/**
|
|
77
|
+
* 全部 header 名,保留最后一次写入的大小写
|
|
78
|
+
* @returns header 名数组
|
|
79
|
+
*/
|
|
80
|
+
keys(): string[];
|
|
81
|
+
/**
|
|
82
|
+
* 全部 `[名, 值]` 对,名保留最后一次写入的大小写
|
|
83
|
+
* @returns 键值对数组
|
|
84
|
+
*/
|
|
85
|
+
toEntries(): [string, string][];
|
|
86
|
+
/**
|
|
87
|
+
* 转成普通对象,用于交给 axios
|
|
88
|
+
* @returns 普通 header 对象,键保留最后一次写入的大小写
|
|
89
|
+
*/
|
|
90
|
+
toJSON(): Record<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* 深拷贝一份,避免下游改写上游的 header
|
|
93
|
+
* @returns 新的 `AmagiHeaders` 实例
|
|
94
|
+
*/
|
|
95
|
+
clone(): AmagiHeaders;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 一次底层 HTTP 请求的完整描述。
|
|
99
|
+
*
|
|
100
|
+
* 由端点的 `build` 产出,经 `sign` 加工,最后交给 transport 发送。
|
|
101
|
+
* 端点声明里返回数组即表示多请求聚合 / 分段并发。
|
|
102
|
+
*/
|
|
103
|
+
interface RequestSpec {
|
|
104
|
+
/** HTTP 方法 */
|
|
105
|
+
method: HttpMethod;
|
|
106
|
+
/** 完整 URL(含 query) */
|
|
107
|
+
url: string;
|
|
108
|
+
/** 请求头。缺省时由平台 `config.ts` 的基线补齐 */
|
|
109
|
+
headers?: HeadersInput;
|
|
110
|
+
/**
|
|
111
|
+
* 要从合并结果里**删掉**的头名(大小写不敏感),在所有 merge 之后执行。
|
|
112
|
+
*
|
|
113
|
+
* `headers` 只能覆盖同名头,给不出「这个端点不该发某个基线头」。快手 H5 端点
|
|
114
|
+
* 需要它:平台基线是照桌面 Chrome 攒的(`origin` / `sec-ch-ua*` / `sec-fetch-*`),
|
|
115
|
+
* 而 H5 端点用移动 UA,两者拼在一起是个自相矛盾的请求。
|
|
116
|
+
* 清单见 `platforms/kuaishou/config.ts` 的 `KUAISHOU_H5_DROP_HEADERS`。
|
|
117
|
+
*
|
|
118
|
+
* 删除发生在最后一步,所以**同时出现在 `headers` 与这里的头会被删掉** ——
|
|
119
|
+
* 端点自己要发的头别写进这个清单。
|
|
120
|
+
*/
|
|
121
|
+
dropHeaders?: readonly string[];
|
|
122
|
+
/** 请求体,`method === 'POST'` 时使用 */
|
|
123
|
+
body?: unknown;
|
|
124
|
+
/** 期望的响应形态。protobuf 端点用 `'arraybuffer'`,反爬页用 `'text'` */
|
|
125
|
+
responseType?: 'json' | 'text' | 'arraybuffer';
|
|
126
|
+
/** 签名器需要的接口路径(小红书 `x-s`、快手 hxfalcon 都要它,与 `url` 不同) */
|
|
127
|
+
signPath?: string;
|
|
128
|
+
/** 多请求聚合 / 分段并发时标识这一条是哪个部分,会进 trace */
|
|
129
|
+
tag?: string;
|
|
130
|
+
/** 端点自定义的附加信息,透传给 `sign` / `decode` */
|
|
131
|
+
extra?: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* transport 发出一次请求后拿到的原始响应。
|
|
135
|
+
*
|
|
136
|
+
* 放在 contracts 而不是 transport,是因为端点声明的 `decode(raw, res)` 需要它,
|
|
137
|
+
* 而依赖方向是 `contracts ← transport ← platforms`,contracts 不能反向依赖。
|
|
138
|
+
*/
|
|
139
|
+
interface RawResponse {
|
|
140
|
+
/** 平台返回的 HTTP 状态码。**原样带出,不再用 `validateStatus: () => true` 抹平** */
|
|
141
|
+
status: number;
|
|
142
|
+
/** 状态文案 */
|
|
143
|
+
statusText?: string;
|
|
144
|
+
/** 响应头,大小写不敏感 */
|
|
145
|
+
headers: AmagiHeaders;
|
|
146
|
+
/**
|
|
147
|
+
* 原始 `Set-Cookie` 头数组(一个响应里可能有多条)。
|
|
148
|
+
*
|
|
149
|
+
* `headers` 里的 `set-cookie` 是 join 成一条的字符串(多值会被 `'; '` 合并),
|
|
150
|
+
* 而 guest cookie 换身份、B站会话登录都需要**逐条**处理 Set-Cookie ——
|
|
151
|
+
* join 后无法还原成数组。所以这里单独保留原始数组。
|
|
152
|
+
*/
|
|
153
|
+
setCookie?: string[];
|
|
154
|
+
/** 未经端点 `decode` 的响应体:已解析的 JSON / 字符串 / `ArrayBuffer` */
|
|
155
|
+
body: unknown;
|
|
156
|
+
/** 这一次请求本身的耗时 */
|
|
157
|
+
durationMs: number;
|
|
158
|
+
/** 实际请求的最终 URL(含签名参数、跟随重定向后的地址) */
|
|
159
|
+
url: string;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/contracts/error.d.ts
|
|
163
|
+
/**
|
|
164
|
+
* 错误契约。
|
|
165
|
+
*
|
|
166
|
+
* 全仓唯一的错误载体:调用方写一段跨平台通用的错误处理代码,只需要认
|
|
167
|
+
* `AmagiError` 一种形状。
|
|
168
|
+
*
|
|
169
|
+
* `contracts/` 是零依赖叶子层,本文件不 import 仓库内任何其他模块。
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* 跨平台统一的错误大类,也是调用方唯一需要 `switch` 的判别键。
|
|
173
|
+
*
|
|
174
|
+
* 粗粒度归因用 `kind`,细粒度归因用 {@link AmagiErrorCode}。
|
|
175
|
+
*/
|
|
176
|
+
type ErrorKind =
|
|
177
|
+
/** 入参不合法,本地就能判定,没有发出请求 */
|
|
178
|
+
'validation' |
|
|
179
|
+
/** 需要登录 / cookie 失效 / 身份不足 */
|
|
180
|
+
'auth' |
|
|
181
|
+
/** 限频,退避后可重试 */
|
|
182
|
+
'rate_limit' |
|
|
183
|
+
/** 风控、验证码、需要人工介入 */
|
|
184
|
+
'risk' |
|
|
185
|
+
/** 资源不存在、已删除、已下架 */
|
|
186
|
+
'not_found' |
|
|
187
|
+
/** 有身份但无权限:地区限制、付费内容、隐私设置 */
|
|
188
|
+
'forbidden' |
|
|
189
|
+
/** 平台侧不可用:5xx、维护、过载 */
|
|
190
|
+
'unavailable' |
|
|
191
|
+
/** 传输层失败:连接重置、DNS、代理 */
|
|
192
|
+
'network' |
|
|
193
|
+
/** 超时 */
|
|
194
|
+
'timeout' |
|
|
195
|
+
/** 响应拿到了但解析不了:非 JSON、protobuf 损坏、反爬 HTML */
|
|
196
|
+
'parse' |
|
|
197
|
+
/** amagi 自身的 bug */
|
|
198
|
+
'internal' |
|
|
199
|
+
/** 平台返回了没见过的错误码 */
|
|
200
|
+
'unknown';
|
|
201
|
+
/**
|
|
202
|
+
* 稳定的字符串错误码,可用于 `switch` 与埋点。
|
|
203
|
+
*
|
|
204
|
+
* 用字符串字面量联合而不是 `enum`:`enum` 的 `Object.values()` 会带出反向映射键,
|
|
205
|
+
* 且拿平台返回的数字码去比时容易出现 `'-101'` 与 `-101` 对不上的静默错配。
|
|
206
|
+
*/
|
|
207
|
+
type AmagiErrorCode = 'PARAM_INVALID' | 'PARAM_MISSING' | 'COOKIE_MISSING' | 'COOKIE_EXPIRED' | 'LOGIN_REQUIRED' | 'RATE_LIMITED' | 'RISK_CONTROL' | 'CAPTCHA_REQUIRED' | 'NOT_FOUND' | 'DELETED' | 'PRIVATE' | 'GEO_RESTRICTED' | 'PAID_CONTENT' | 'PLATFORM_ERROR' | 'PLATFORM_UNAVAILABLE' | 'NETWORK_ERROR' | 'TIMEOUT' | 'EMPTY_RESPONSE' | 'DECODE_FAILED' | 'ANTIBOT_PAGE' | 'INTERNAL_ERROR' | 'UNKNOWN_ERROR';
|
|
208
|
+
/** `kind === 'validation'` 时的字段级错误 */
|
|
209
|
+
interface ValidationIssue {
|
|
210
|
+
/** 点号路径,如 `'verify.stdParams.token'` */
|
|
211
|
+
path: string;
|
|
212
|
+
/** 面向人的说明 */
|
|
213
|
+
message: string;
|
|
214
|
+
/** 收到的值,用于排查 */
|
|
215
|
+
received?: unknown;
|
|
216
|
+
}
|
|
217
|
+
/** 失败信封里唯一的错误载体,永不为 `undefined` */
|
|
218
|
+
interface AmagiError {
|
|
219
|
+
/** 判别键,跨平台统一的错误大类 */
|
|
220
|
+
kind: ErrorKind;
|
|
221
|
+
/** 稳定的字符串错误码,可用于 switch 与埋点 */
|
|
222
|
+
code: AmagiErrorCode;
|
|
223
|
+
/** 面向人的说明,取平台原文优先 */
|
|
224
|
+
message: string;
|
|
225
|
+
/** 是否值得重试。调用方据此决定退避还是放弃 */
|
|
226
|
+
retryable: boolean;
|
|
227
|
+
/** 平台原始错误码与文案,一个字都不丢 */
|
|
228
|
+
platform?: {
|
|
229
|
+
code: string | number;
|
|
230
|
+
message?: string;
|
|
231
|
+
};
|
|
232
|
+
/** 真实发生的 HTTP 状态(有请求才有) */
|
|
233
|
+
http?: {
|
|
234
|
+
status: number;
|
|
235
|
+
statusText?: string;
|
|
236
|
+
};
|
|
237
|
+
/** `kind === 'validation'` 时的字段级错误 */
|
|
238
|
+
issues?: ValidationIssue[];
|
|
239
|
+
/**
|
|
240
|
+
* 原始响应体。默认**连键都没有**,`createClient({ debug: true })` 且这次
|
|
241
|
+
* 确实**拿到了响应**时才填(网络中断 / 超时那类失败没有响应体可放)。
|
|
242
|
+
*/
|
|
243
|
+
raw?: unknown;
|
|
244
|
+
/**
|
|
245
|
+
* 风控挑战(验证页地址 + 票据)。`kind === 'risk'` 且平台认得出这份响应时才有。
|
|
246
|
+
*
|
|
247
|
+
* **不受 `debug` 开关影响** —— 它是「怎么过去」这条必要信息,见
|
|
248
|
+
* {@link RiskChallenge}。
|
|
249
|
+
*/
|
|
250
|
+
challenge?: RiskChallenge;
|
|
251
|
+
/** 底层 Error 对象,仅用于日志 */
|
|
252
|
+
cause?: unknown;
|
|
253
|
+
}
|
|
254
|
+
/** 平台判定的结论 */
|
|
255
|
+
interface JudgeVerdict {
|
|
256
|
+
/** 是否视为成功 */
|
|
257
|
+
ok: boolean;
|
|
258
|
+
/** 失败时的错误大类 */
|
|
259
|
+
kind?: ErrorKind;
|
|
260
|
+
/** 失败时的细粒度错误码 */
|
|
261
|
+
code?: AmagiErrorCode;
|
|
262
|
+
/** 覆盖 {@link isRetryableKind} 的默认推导 */
|
|
263
|
+
retryable?: boolean;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 平台响应判定函数。
|
|
267
|
+
*
|
|
268
|
+
* 每个平台一份纯函数,把原始响应映射为「成功」或一个错误分类。
|
|
269
|
+
* **这是全仓唯一判定成败的地方** —— 平台差异只体现在这一份实现里。
|
|
270
|
+
*/
|
|
271
|
+
type Judge = (raw: unknown, http: {
|
|
272
|
+
status: number;
|
|
273
|
+
}) => JudgeVerdict;
|
|
274
|
+
/**
|
|
275
|
+
* 风控挑战:撞验证页时交给调用方的「怎么过去」。
|
|
276
|
+
*
|
|
277
|
+
* 为什么不塞进 {@link JudgeVerdict}:judge 只管**分类**,四个槽位装不下一个
|
|
278
|
+
* URL。但 `kind: 'risk'` 光有分类是条死路 —— `CAPTCHA_REQUIRED` 只告诉调用方
|
|
279
|
+
* 「你被拦了」,不给出路。以前地址只能从 `error.raw` 里自己捞,而 `raw` 只在
|
|
280
|
+
* `createClient({ debug: true })` 时才有、HTTP 路由那一面**结构上拿不到**
|
|
281
|
+
* (`createXxxRoutes` 不接 `debug`)——最需要滑块地址的入口恰好是唯一产不出它的
|
|
282
|
+
* 入口。所以这一份**不受 `debug` 管**,只要 judge 判成 `risk` 就填。
|
|
283
|
+
*
|
|
284
|
+
* 只放地址与票据,不放原始响应体:它是「必要信息」而不是「排障明细」,
|
|
285
|
+
* 也没有大对象与凭证字段的顾虑。
|
|
286
|
+
*
|
|
287
|
+
* **只做中转,不做绕过** —— amagi 不引入任何识别、轨迹模拟或自动过验证的代码。
|
|
288
|
+
*/
|
|
289
|
+
interface RiskChallenge {
|
|
290
|
+
/** 验证页地址(已补协议) */
|
|
291
|
+
url: string;
|
|
292
|
+
/** 前端验证 SDK 地址(已补协议),自建验证页时要它 */
|
|
293
|
+
jsSdkUrl?: string;
|
|
294
|
+
/** 验证会话票据 */
|
|
295
|
+
session?: string;
|
|
296
|
+
/** 风控业务名 */
|
|
297
|
+
bizName?: string;
|
|
298
|
+
/** 平台命中的业务码 */
|
|
299
|
+
result?: string | number;
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/contracts/platform.d.ts
|
|
303
|
+
/**
|
|
304
|
+
* 平台契约。
|
|
305
|
+
*
|
|
306
|
+
* `contracts/` 是零依赖叶子层:本目录下的模块不 import 仓库内任何其他模块,
|
|
307
|
+
* 所以任何层都可以依赖它而不引入 import 环。
|
|
308
|
+
*/
|
|
309
|
+
/**
|
|
310
|
+
* amagi 支持的平台清单。
|
|
311
|
+
*
|
|
312
|
+
* `Platform` 联合类型由这个数组推导而来,两者不可能漂移 ——
|
|
313
|
+
* 新增平台只需在这里加一项。数组顺序即对外文档与遍历顺序。
|
|
314
|
+
*/
|
|
315
|
+
declare const PLATFORMS: readonly ['douyin', 'bilibili', 'kuaishou', 'xiaohongshu'];
|
|
316
|
+
/** amagi 支持的平台 */
|
|
317
|
+
type Platform = (typeof PLATFORMS)[number];
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/contracts/meta.d.ts
|
|
320
|
+
/**
|
|
321
|
+
* 可观测性契约。
|
|
322
|
+
*
|
|
323
|
+
* `AmagiMeta` 挂在每一个信封上(成功与失败都有),同时进事件负载。
|
|
324
|
+
* 它把几件「看不见」的事变成肉眼可见的数字:
|
|
325
|
+
* - `attempts`:一次调用实际打了多少个请求,含重试与分页的叠乘。
|
|
326
|
+
* - `requestId` / `clientId`:多实例并发时可归因。
|
|
327
|
+
* - 前置请求(换 guest cookie、取 wbi key)以 `reason: 'prepare'` 进 trace。
|
|
328
|
+
*/
|
|
329
|
+
/**
|
|
330
|
+
* 一次底层请求的发起原因。
|
|
331
|
+
*
|
|
332
|
+
* 区分「端点内重试」与「传输层重试」、「翻页」与「分段并发」,
|
|
333
|
+
* 是排查请求数叠乘的入口。
|
|
334
|
+
*/
|
|
335
|
+
type TraceReason =
|
|
336
|
+
/** 首次请求 */
|
|
337
|
+
'initial' |
|
|
338
|
+
/** 传输层或 `retryOn` 触发的重试 */
|
|
339
|
+
'retry' |
|
|
340
|
+
/** 声明式翻页的第 n 页 */
|
|
341
|
+
'page' |
|
|
342
|
+
/** 多请求聚合 / 分段并发里的一段 */
|
|
343
|
+
'segment' |
|
|
344
|
+
/** `prepare` 阶段的前置请求:换 guest cookie、取 wbi key */
|
|
345
|
+
'prepare';
|
|
346
|
+
/** 单次底层 HTTP 请求的明细 */
|
|
347
|
+
interface RequestTrace {
|
|
348
|
+
/** 实际请求的 URL(含签名参数) */
|
|
349
|
+
url: string;
|
|
350
|
+
/** HTTP 方法 */
|
|
351
|
+
method: string;
|
|
352
|
+
/** 平台返回的状态码,请求未发出(如 DNS 失败)时缺失 */
|
|
353
|
+
status?: number;
|
|
354
|
+
/** 这一次请求本身的耗时 */
|
|
355
|
+
durationMs: number;
|
|
356
|
+
/** 这次请求为什么会发出 */
|
|
357
|
+
reason: TraceReason;
|
|
358
|
+
/** `reason === 'retry'` 时,被重试的那次失败的错误码 */
|
|
359
|
+
retryOf?: AmagiErrorCode;
|
|
360
|
+
}
|
|
361
|
+
/** 挂在每个信封上的元信息 */
|
|
362
|
+
interface AmagiMeta {
|
|
363
|
+
/** 每次逻辑调用一个 id,贯穿事件、日志、trace */
|
|
364
|
+
requestId: string;
|
|
365
|
+
/** 发起调用的 client 实例 id;静态 fetcher 用 `'static'` */
|
|
366
|
+
clientId: string;
|
|
367
|
+
/** 平台 */
|
|
368
|
+
platform: Platform;
|
|
369
|
+
/** 端点全名,如 `'douyin.videoWork'` */
|
|
370
|
+
endpoint: string;
|
|
371
|
+
/** 从进入 fetcher 到返回信封的总耗时 */
|
|
372
|
+
durationMs: number;
|
|
373
|
+
/** 实际发出的 HTTP 请求数,含重试与分页。分页 3 页 + 1 次重试 = 4 */
|
|
374
|
+
attempts: number;
|
|
375
|
+
/**
|
|
376
|
+
* 每次底层请求的明细,按发出顺序。
|
|
377
|
+
*
|
|
378
|
+
* 默认不带:`createClient({ debug: true })` 时才填(同一个开关也给失败信封
|
|
379
|
+
* 填 `error.raw`,没有单独的 trace 开关)。不开时信封上**没有 `trace`
|
|
380
|
+
* 这个键**,而 `attempts` 照样准确 —— 计数始终发生,只有明细受开关控制。
|
|
381
|
+
*
|
|
382
|
+
* 静态 fetcher(`amagi.douyinFetcher.*`)与 HTTP 服务的平台路由没有这个开关。
|
|
383
|
+
* 要不受开关影响地逐条观测请求,监听 `http:request` / `http:response`
|
|
384
|
+
* 事件:它们的负载恒带 `trace`。URL 含签名参数,别在生产里无条件打印。
|
|
385
|
+
*/
|
|
386
|
+
trace?: RequestTrace[];
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/contracts/endpoint.d.ts
|
|
390
|
+
/**
|
|
391
|
+
* 端点声明契约。
|
|
392
|
+
*
|
|
393
|
+
* 核心:**一个端点一份声明,其余全部派生。** 参数类型、运行时校验、
|
|
394
|
+
* HTTP 路由、fetcher 方法、bound fetcher、方法名映射、文档与测试清单
|
|
395
|
+
* 全部从这份声明推出来,不再散在十几个文件里靠人工同步。
|
|
396
|
+
*
|
|
397
|
+
* `contracts/` 是零依赖叶子层:本文件只 type-import 外部包 `zod` 与同目录契约。
|
|
398
|
+
* 端点的钩子需要「发请求」的能力,但 contracts 不能反向依赖 transport,
|
|
399
|
+
* 所以 {@link EndpointCtx} 只声明 `send` 的**形状**,由 transport 去实现。
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* 只携带类型、不携带值的令牌。
|
|
403
|
+
*
|
|
404
|
+
* 用来把响应类型写进声明而不产生任何运行时开销:
|
|
405
|
+
* `response: type<DouyinReturnTypeMap['videoWork']>()`。
|
|
406
|
+
*
|
|
407
|
+
* `T` 取平台返回数据的实测快照类型(`XxxReturnTypeMap` 的键与端点短名
|
|
408
|
+
* 一一对应),快照自带的索引签名让「平台加字段」不算 breaking。
|
|
409
|
+
*/
|
|
410
|
+
interface TypeToken<T> {
|
|
411
|
+
/** 幻影字段,运行时永远是 `undefined`,只为让 TS 能推出 `T` */
|
|
412
|
+
readonly __type?: T;
|
|
413
|
+
}
|
|
414
|
+
/** 端点全名,形如 `'douyin.videoWork'` */
|
|
415
|
+
type EndpointName = `${Platform}.${string}`;
|
|
416
|
+
/**
|
|
417
|
+
* 端点钩子拿到的执行上下文。
|
|
418
|
+
*
|
|
419
|
+
* `send` 是依赖倒置点:contracts 只声明「能发一次请求并拿到 {@link RawResponse}」
|
|
420
|
+
* 这个形状,transport 提供实现。这样 `prepare` 里换 guest cookie、取 wbi key
|
|
421
|
+
* 都必须走 transport,用户配的 proxy / agent / 超时才对它生效。
|
|
422
|
+
*/
|
|
423
|
+
interface EndpointCtx {
|
|
424
|
+
/** 发起调用的 client 实例 id;静态 fetcher 用 `'static'` */
|
|
425
|
+
clientId: string;
|
|
426
|
+
/** 平台 */
|
|
427
|
+
platform: Platform;
|
|
428
|
+
/** 本次调用使用的 cookie */
|
|
429
|
+
cookie: string;
|
|
430
|
+
/** 本次调用使用的 User-Agent */
|
|
431
|
+
userAgent: string;
|
|
432
|
+
/** 调用方传入的请求配置 */
|
|
433
|
+
requestConfig: RequestConfig;
|
|
434
|
+
/**
|
|
435
|
+
* 发一次底层请求。由 transport 注入
|
|
436
|
+
* @param spec - 请求描述
|
|
437
|
+
* @param reason - 这次请求的来源,决定它在 trace 里的 `reason`
|
|
438
|
+
* @param requestConfig - 单次调用的请求配置(合并进本次请求)。缺省时
|
|
439
|
+
* 由 execute 把 ctx.requestConfig 当作默认值补上 —— 管线内任何内部请求
|
|
440
|
+
* (prepare 换 guest cookie、取 wbi key)都与主请求用同一份配置
|
|
441
|
+
* @returns 原始响应
|
|
442
|
+
*/
|
|
443
|
+
send: (spec: RequestSpec, reason?: TraceReason, requestConfig?: RequestConfig) => Promise<RawResponse>;
|
|
444
|
+
}
|
|
445
|
+
/** 自定义签名器:拿到请求描述与上下文,返回签好名的请求描述 */
|
|
446
|
+
type SignFn = (spec: RequestSpec, ctx: EndpointCtx) => RequestSpec | Promise<RequestSpec>;
|
|
447
|
+
/**
|
|
448
|
+
* 签名阶段:决定一个端点内多个反爬参数({@link SignStep})的执行先后。
|
|
449
|
+
*
|
|
450
|
+
* 端点作者只声明「要哪些参数」,不用手排顺序 —— runtime 按本枚举的固定次序排好再执行。
|
|
451
|
+
* 抖音那条链是活例:`webid → msToken → a_bogus/x_bogus → secsdk`,颠倒任意一步签名就
|
|
452
|
+
* 不成立,所以把「顺序」这个不变量收进机制层,而不是交给每个端点声明处去保证。
|
|
453
|
+
*
|
|
454
|
+
* - `prepare`:签名前的补参(如按 cookie 补 webid)。
|
|
455
|
+
* - `token`:本地随机令牌(如抖音 `msToken`),必须先于主签名进 URL。
|
|
456
|
+
* - `sign`:主签名(`a_bogus` / `x_bogus` / `wbi` / `x-s` / `hxfalcon` 等)。
|
|
457
|
+
* - `finalize`:收尾(如抖音 `secsdk` 重写整条 URL、小红书追加 `x-b3-traceid`)。
|
|
458
|
+
*/
|
|
459
|
+
type SignPhase = 'prepare' | 'token' | 'sign' | 'finalize';
|
|
460
|
+
/**
|
|
461
|
+
* 一个原子反爬参数。
|
|
462
|
+
*
|
|
463
|
+
* `apply` 复用 {@link SignFn}:「这个参数写进 query / header / 还是重写整条 URL」是它的
|
|
464
|
+
* 内部实现,端点作者不感知。`phase` 是唯一的顺序语义,同 phase 内按数组出现顺序执行。
|
|
465
|
+
*
|
|
466
|
+
* 端点用 `sign: [msToken(184), aBogus(), secsdk()]` 直接列出要哪些参数;增删数组元素
|
|
467
|
+
* 即可,不必为「要参数1不要参数2」另注册签名器名。
|
|
468
|
+
*/
|
|
469
|
+
interface SignStep {
|
|
470
|
+
/** 执行阶段,决定与同端点其他步骤的先后 */
|
|
471
|
+
phase: SignPhase;
|
|
472
|
+
/** 把这个反爬参数盖到请求上 */
|
|
473
|
+
apply: SignFn;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 签名声明。
|
|
477
|
+
*
|
|
478
|
+
* - {@link SignStep} / `SignStep[]`:**推荐**。原子反爬参数清单,runtime 按 {@link SignPhase}
|
|
479
|
+
* 排序后依次 `apply`;单个步骤可省数组(`sign: hxfalcon()`)。
|
|
480
|
+
* - 字符串:平台签名器表里的名字(如 `'a_bogus'` / `'xhs-post'`)。默认宽 `string`,
|
|
481
|
+
* 平台可以再包一层 `defineEndpoint`(传 `TSign` 为自己的签名器名联合)把它收窄。
|
|
482
|
+
* 与 SignStep 清单渐进共存,全平台迁移完成后再定去留。
|
|
483
|
+
* - `false`:显式声明这个端点不签名(抖音搜索、表情包接口)。
|
|
484
|
+
* - 函数:一次性的自定义签名。
|
|
485
|
+
*/
|
|
486
|
+
type SignDecl<TSign extends string = string> = TSign | false | SignFn | SignStep | SignStep[];
|
|
487
|
+
/** 多请求聚合 / 分段并发时,部分失败怎么处理 */
|
|
488
|
+
type PartialPolicy =
|
|
489
|
+
/** 缺失的部分留空,整体仍算成功 */
|
|
490
|
+
'tolerate' |
|
|
491
|
+
/** 任一部分失败即整体失败 */
|
|
492
|
+
'fail';
|
|
493
|
+
/**
|
|
494
|
+
* 一页 / 一段的默认类型,取最终返回 `TData`(一页 / 一段与返回同形)。
|
|
495
|
+
* `TData` 是 `unknown` / `any` / `never` 时退成 `Record<string, any>`(随便点,但挡住把它当函数调)。
|
|
496
|
+
*/
|
|
497
|
+
type PageOf<TData> = [TData] extends [never] ? Record<string, any> : unknown extends TData ? Record<string, any> : TData;
|
|
498
|
+
/**
|
|
499
|
+
* 翻页跑完交给 `merge` 的值。`lastPage` 恒有值(空跑由 {@link PaginateOutcome} 单独承载),
|
|
500
|
+
* 所以端点能直接 `...lastPage`。不保留每页完整体、只留累积条目,避免内存峰值。
|
|
501
|
+
*/
|
|
502
|
+
interface PaginatedValue<TPage = unknown, TItem = unknown> {
|
|
503
|
+
/** 最后一页 decode 之后的值 */
|
|
504
|
+
lastPage: TPage;
|
|
505
|
+
/** 按目标条数截断后的累积条目 */
|
|
506
|
+
items: TItem[];
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 声明式翻页:一页页往后翻,每页都走 ③build→④sign→⑤send→⑥decode→⑦judge,最后 `merge` 收尾。
|
|
510
|
+
* 页类型 `TPage` 缺省取 `PageOf<TData>`(一页与返回同形),所以 `items` / `hasMore` / `nextParams`
|
|
511
|
+
* 自动拿到 `response` 的形状、不必断言。详细机制见开发文档「翻页与会话」。
|
|
512
|
+
*/
|
|
513
|
+
interface PaginateDef<TParams, TData = unknown, TPage = unknown, TItem = unknown> {
|
|
514
|
+
/** 单页最多取多少条,用来把目标条数切成几次请求 */
|
|
515
|
+
maxPageSize: number;
|
|
516
|
+
/** 目标条数取自哪个参数,默认 `'number'`;该参数为 0 时一个请求都不发 */
|
|
517
|
+
limitParam?: keyof TParams & string;
|
|
518
|
+
/** 每页条数写回哪个参数,默认与 `limitParam` 相同 */
|
|
519
|
+
countParam?: keyof TParams & string;
|
|
520
|
+
/** 页形状与最终返回不同形时才写(覆盖 `TPage`);缺省取 `PageOf<TData>` */
|
|
521
|
+
page?: TypeToken<TPage>;
|
|
522
|
+
/** 从一页里取出本页条目(`page` 已是 `TPage`);返回空数组表示到底了 */
|
|
523
|
+
items: (page: TPage) => TItem[];
|
|
524
|
+
/** 还有没有下一页;返回 `false` 立刻停 */
|
|
525
|
+
hasMore: (page: TPage) => boolean;
|
|
526
|
+
/** 根据这一页产出下一次请求的参数(游标怎么带端点自己定) */
|
|
527
|
+
nextParams: (params: TParams, page: TPage) => TParams;
|
|
528
|
+
/** 把跨页累积的条目收成最终 data(可选):`value.lastPage` 恒有值、`value.items` 是累积条目 */
|
|
529
|
+
merge?: (value: PaginatedValue<TPage, TItem>, params: TParams) => NoInfer<TData>;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* 「同构分段并发」跑完交给 {@link EndpointDef.aggregate} 的值(与 {@link PaginatedValue} 一个套路)。
|
|
533
|
+
* `head` 恒有值(全失败时 execute 不进 aggregate),所以端点能直接 `...head`。段类型 `TPage`
|
|
534
|
+
* 缺省取 `PageOf<TData>`(段与返回同形),与 paginate 的 page 共用。详见「端点注册表」。
|
|
535
|
+
*/
|
|
536
|
+
interface AggregatedValue<TPage = unknown> {
|
|
537
|
+
/** 各段 decode 之后的值,顺序同 build 的分段;失败段(`partial: 'tolerate'`)为 undefined */
|
|
538
|
+
parts: ReadonlyArray<TPage | undefined>;
|
|
539
|
+
/** 第一段成功段(恒有值):元信息从它取,可直接 `...head` */
|
|
540
|
+
head: TPage;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* 端点的文档元数据 —— OpenAPI 规范里「面向人的那部分」的唯一出处。
|
|
544
|
+
*
|
|
545
|
+
* 规范从注册表派生,所以描述文案也只能长在声明里:写进文档站的 Markdown
|
|
546
|
+
* 就成了「手写第二遍」,必然漂移。
|
|
547
|
+
*
|
|
548
|
+
* `tags` 故意不在这里:**平台就是 tag**,由生成器从 {@link EndpointDef.name}
|
|
549
|
+
* 的平台段派生,同一个事实不写两遍。
|
|
550
|
+
*/
|
|
551
|
+
interface EndpointDoc {
|
|
552
|
+
/**
|
|
553
|
+
* OpenAPI 的 `summary`:一句话说清这个端点返回什么。
|
|
554
|
+
*
|
|
555
|
+
* 写法约定:**中文名词短语、不带句号、不超过 40 字**,例如 `'视频作品详细信息'`。
|
|
556
|
+
* 它会出现在 API 参考的端点卡片标题与侧边栏条目上,写成整句或超长都会被截断。
|
|
557
|
+
*/
|
|
558
|
+
summary: string;
|
|
559
|
+
/**
|
|
560
|
+
* OpenAPI 的 `description`:一句话讲不完的部分 —— 参数之间的约束、平台侧限制、
|
|
561
|
+
* 与相近端点的区别。支持 Markdown、可多行。没有要补充的就别写。
|
|
562
|
+
*/
|
|
563
|
+
description?: string;
|
|
564
|
+
/** 标为废弃:生成的 operation 带 `deprecated: true`,文档站会画删除线 */
|
|
565
|
+
deprecated?: boolean;
|
|
566
|
+
/** 指向平台官方文档(或仓库内的说明页) */
|
|
567
|
+
externalDocs?: {
|
|
568
|
+
/** 文档地址 */
|
|
569
|
+
url: string;
|
|
570
|
+
/** 链接文案,缺省由文档站决定 */
|
|
571
|
+
description?: string;
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 一个端点的完整声明。**声明一份,其余(方法 / 路由 / 类型 / 文档)全部派生。**
|
|
576
|
+
*
|
|
577
|
+
* 下面的字段大多对应「调一次接口」的流程步骤,注释里用 ①~⑧ 标出顺序:
|
|
578
|
+
* ① 校验参数 → ② prepare → ③ build → ④ sign → ⑤ 发请求 → ⑥ decode → ⑦ judge → ⑧ 整形。
|
|
579
|
+
* 完整字段清单、派生关系与设计取舍见开发文档「端点注册表」与「契约与信封」。
|
|
580
|
+
*/
|
|
581
|
+
interface EndpointDef<TParams extends zod.ZodType, TData, TSign extends string = string, TPage = PageOf<TData>, TItem = unknown> {
|
|
582
|
+
/** 端点全名,形如 `'douyin.videoWork'`(声明信息) */
|
|
583
|
+
name: EndpointName;
|
|
584
|
+
/** HTTP 路由路径,同平台内必须唯一(声明信息) */
|
|
585
|
+
route: string;
|
|
586
|
+
/** ① 参数 schema(zod):校验、类型推导、文档参数表都从它派生 */
|
|
587
|
+
params: TParams;
|
|
588
|
+
/** 文档元数据:`summary` 必填(API 参考的标题来源),`description` 可选 */
|
|
589
|
+
doc?: EndpointDoc;
|
|
590
|
+
/** ② 发请求前的准备(可选):换游客 cookie、取密钥等;返回的字段并入上下文 */
|
|
591
|
+
prepare?: (ctx: EndpointCtx) => Promise<Partial<EndpointCtx>>;
|
|
592
|
+
/** ③ 拼请求。返回数组 = 多个请求并发(分段并发 / 多请求聚合) */
|
|
593
|
+
build?: (params: zod.infer<TParams>, ctx: EndpointCtx) => RequestSpec | RequestSpec[];
|
|
594
|
+
/** ④ 签名(可选):{@link SignStep} 清单(推荐,可直接列出要哪些反爬参数)/ 签名器名字 / `false`(不签)/ 一次性函数 */
|
|
595
|
+
sign?: SignDecl<TSign>;
|
|
596
|
+
/** ⑥ 解码响应(可选,⑤ 是发请求):默认按 JSON;protobuf / 多段 JSON / HTML 在这里处理 */
|
|
597
|
+
decode?: (raw: unknown, res: RawResponse) => unknown;
|
|
598
|
+
/** 翻页(可选):把 ③~⑦ 包成一页页翻的循环,详见「端点注册表」 */
|
|
599
|
+
paginate?: PaginateDef<zod.infer<TParams>, TData, TPage, TItem>;
|
|
600
|
+
/** 多个请求时的部分失败策略:`'tolerate'`(缺的留空)/ `'fail'`(默认,一个失败即整体失败) */
|
|
601
|
+
partial?: PartialPolicy;
|
|
602
|
+
/** ⑦ 判定成功 / 失败(可选):缺省用所在平台的默认判定 */
|
|
603
|
+
judge?: Judge;
|
|
604
|
+
/**
|
|
605
|
+
* ⑧ 把响应整理成最终 data(可选)。`decoded` 是解码后的响应、什么形状都可能,所以类型是 unknown。
|
|
606
|
+
* 分段并发改用 {@link EndpointDef.aggregate}、翻页改用 {@link PaginateDef.merge}(都带类型、更好写)。
|
|
607
|
+
* 为什么是 unknown、`NoInfer` 的作用见「契约与信封」。
|
|
608
|
+
*/
|
|
609
|
+
normalize?: (decoded: unknown, params: zod.infer<TParams>) => NoInfer<TData>;
|
|
610
|
+
/**
|
|
611
|
+
* ⑧ 整理「同构分段并发」结果(可选,`normalize` 的带类型版本):`value.parts` 是各段结果、
|
|
612
|
+
* `value.head` 是第一段成功段(恒有值,可直接 `...head`)。声明了它就别再写 `normalize`(会被忽略)。
|
|
613
|
+
* 只适用各段同形;异构聚合(快手 `userProfile`)仍用 `normalize`。详见「端点注册表」。
|
|
614
|
+
*/
|
|
615
|
+
aggregate?: (value: AggregatedValue<TPage>, params: zod.infer<TParams>) => NoInfer<TData>;
|
|
616
|
+
/** 纯本地计算、不发请求(可选):声明了它就跳过 ②~⑧,直接算出 data(如 AV/BV 互转) */
|
|
617
|
+
compute?: (params: zod.infer<TParams>) => TData;
|
|
618
|
+
/** 响应类型令牌 `type<T>()`:**data 的类型只从这里来**。详见「契约与信封」 */
|
|
619
|
+
response?: TypeToken<TData>;
|
|
620
|
+
/** 命中这些错误码时端点级重试(如 B站 `-412`)。详见「端点注册表」 */
|
|
621
|
+
retryOn?: AmagiErrorCode[];
|
|
622
|
+
/** 重试时重新 build + 重新签名(而非原样重放);抖音 Argus 需要它。详见「端点注册表」 */
|
|
623
|
+
retryFresh?: boolean;
|
|
624
|
+
/** 预留槽位,当前恒为 `undefined`(跨平台统一视图,将来才用) */
|
|
625
|
+
toCanonical?: undefined;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 任意端点声明。
|
|
629
|
+
*
|
|
630
|
+
* `TParams` 出现在 `build` / `normalize` / `compute` 的形参位置,`TPage` / `TItem` 出现在
|
|
631
|
+
* `paginate.items` 等形参位置(都逆变),所以这里必须用 `any` 才能让具体端点赋值进来 ——
|
|
632
|
+
* 换成 `unknown` 会让 `EndpointDef<具体 schema, …>` 不可赋值给它。
|
|
633
|
+
*/
|
|
634
|
+
type AnyEndpointDef = EndpointDef<any, any, any, any, any>;
|
|
635
|
+
/** 一个平台的端点注册表:端点短名 → 声明 */
|
|
636
|
+
type Registry = Record<string, AnyEndpointDef>;
|
|
637
|
+
/** 取端点的参数 schema 类型 */
|
|
638
|
+
type ParamsSchemaOf<D> = D extends EndpointDef<infer P, unknown, any, any, any> ? P : never;
|
|
639
|
+
/** 取端点「校验后的参数」类型(对应 `zod.infer`)——build / merge / nextParams 取它 */
|
|
640
|
+
type ParsedOf<D> = D extends EndpointDef<infer P, unknown, any, any, any> ? zod.infer<P> : never;
|
|
641
|
+
/** 内部参数的 phantom 品牌键。只存在于类型里;运行时由 `.meta()` 携带同一标记 */
|
|
642
|
+
declare const InternalBrand: unique symbol;
|
|
643
|
+
/**
|
|
644
|
+
* 内部参数的 schema 类型:`internalParam()` 的返回。
|
|
645
|
+
*
|
|
646
|
+
* 交叉出来的品牌是 phantom —— schema 的运行时行为不变,`zod.infer` 读到的
|
|
647
|
+
* `output` / `input` 也不受影响(品牌键上没有这些属性,TS 从 S 侧解析)。
|
|
648
|
+
*/
|
|
649
|
+
type InternalParam<S extends zod.ZodType> = S & {
|
|
650
|
+
readonly [InternalBrand]: true;
|
|
651
|
+
};
|
|
652
|
+
/** 从 params schema 的 shape 里筛出内部参数的键名(类型层,读 phantom 品牌) */
|
|
653
|
+
type InternalKeysOf<S> = S extends zod.ZodObject<infer Shape> ? { [K in keyof Shape]-?: Shape[K] extends {
|
|
654
|
+
readonly [InternalBrand]: true;
|
|
655
|
+
} ? K : never; }[keyof Shape] : never;
|
|
656
|
+
/**
|
|
657
|
+
* 调用方能传的参数:校验后的形状挖掉内部参数。
|
|
658
|
+
*
|
|
659
|
+
* fetcher / 静态 fetcher 的 options 取它。`ParsedOf` 仍归 build / merge / nextParams
|
|
660
|
+
* —— 它们要读写内部参数(翻页游标这类),URL 构造器同样取完整形状。
|
|
661
|
+
*/
|
|
662
|
+
type PublicParamsOf<D> = Omit<ParsedOf<D>, InternalKeysOf<ParamsSchemaOf<D>>>;
|
|
663
|
+
/** 取端点的响应数据类型 */
|
|
664
|
+
type DataOf<D> = D extends EndpointDef<any, infer T, any, any, any> ? T : never;
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/platforms/kuaishou/api.d.ts
|
|
667
|
+
/**
|
|
668
|
+
* 快手 URL 构造(请求描述)。
|
|
669
|
+
*
|
|
670
|
+
* 参数类型在本文件本地定义,只负责返回请求描述对象,不发起网络请求。
|
|
671
|
+
*
|
|
672
|
+
* `videoWork` / `comments` 两条走 H5 命名空间 —— 见 {@link KUAISHOU_H5_HOST}。
|
|
673
|
+
*
|
|
674
|
+
* ## 几条**没有**实现的接口
|
|
675
|
+
*
|
|
676
|
+
* - **搜索 / 创作者搜索 / 热榜**(`/rest/v/search`、`/rest/v/feed/hot` 一类):
|
|
677
|
+
* 这三个要「浏览器激活过的真实 did」。真实 did 得先在浏览器里走
|
|
678
|
+
* `gdfp.gifshow.com/s/w/c` 完成设备指纹注册,**服务端有账本**,本地造不出来。
|
|
679
|
+
* 实测过 5 种组合(随机 did + 借来的完整风控指纹、服务端刚下发的新 did、
|
|
680
|
+
* 新 did 先走 `system/startup` 预热、Node 侧裸打 `gdfp` 注册……)全部被拒。
|
|
681
|
+
* amagi 的 did 是内部生成的,所以这条**绕不过去**。
|
|
682
|
+
* - **音乐标签页**:可用做法是抓分享页 HTML 解 `INIT_STATE`,而不是打 `tag/music/*`
|
|
683
|
+
* 接口。抓 HTML 解全局变量不属于「接口库」该干的事,不实现。
|
|
684
|
+
* - **相关推荐 `/rest/wd/ugH5App/slide/feed`、搜索热词 `/rest/wd/ugH5App/search/guess`**:
|
|
685
|
+
* 两条都免签、都能做,只是当前没有下游需要,按需再加。
|
|
686
|
+
*
|
|
687
|
+
* 上述实测结论来自 @OduckO 的 kuaishou-parser(GPL-3.0-only):
|
|
688
|
+
* https://github.com/OduckO
|
|
689
|
+
*/
|
|
690
|
+
/** `videoWork` 参数 */
|
|
691
|
+
interface VideoInfoParams {
|
|
692
|
+
/** 作品 ID */
|
|
693
|
+
photoId: string;
|
|
694
|
+
/** 业务标识,H5 分享页恒为 `NEBULA` */
|
|
695
|
+
kpn?: string;
|
|
696
|
+
/** 子业务,空串即可 */
|
|
697
|
+
subBiz?: string;
|
|
698
|
+
/** 以下 8 个 share 字段来自短链展开后的 URL query;直接用 photoId 调用时全传空串 */
|
|
699
|
+
fid?: string;
|
|
700
|
+
efid?: string;
|
|
701
|
+
shareToken?: string;
|
|
702
|
+
shareObjectId?: string;
|
|
703
|
+
shareMethod?: string;
|
|
704
|
+
shareId?: string;
|
|
705
|
+
shareResourceType?: string;
|
|
706
|
+
/** 分享渠道,取短链 query 里的 `cc` */
|
|
707
|
+
shareChannel?: string;
|
|
708
|
+
/** 分享页域名,恒为 `c.kuaishou.com` */
|
|
709
|
+
h5Domain?: string;
|
|
710
|
+
/** 是否长视频 */
|
|
711
|
+
isLongVideo?: boolean;
|
|
712
|
+
}
|
|
713
|
+
/** `comments` 参数 */
|
|
714
|
+
interface CommentParams {
|
|
715
|
+
/** 作品 ID */
|
|
716
|
+
photoId: string;
|
|
717
|
+
/** 分页游标;为空时请求首屏评论 */
|
|
718
|
+
pcursor?: string;
|
|
719
|
+
}
|
|
720
|
+
/** `userProfile` / `liveRoomInfo` 参数 */
|
|
721
|
+
interface UserProfileParams {
|
|
722
|
+
/** 用户主页 principalId,可直接取 profile 页 URL 末段 */
|
|
723
|
+
principalId: string;
|
|
724
|
+
}
|
|
725
|
+
/** `userWorkList` 参数 */
|
|
726
|
+
interface UserWorkListParams {
|
|
727
|
+
/** 用户主页 principalId */
|
|
728
|
+
principalId: string;
|
|
729
|
+
/** 分页游标;为空时请求首屏作品列表 */
|
|
730
|
+
pcursor?: string;
|
|
731
|
+
/** 每页数量,默认 12 */
|
|
732
|
+
count?: number;
|
|
733
|
+
}
|
|
734
|
+
/** `liveRoomInfo` 参数 */
|
|
735
|
+
interface LiveRoomInfoParams {
|
|
736
|
+
/** 直播间 principalId,可直接取 /u/{principalId} URL 末段 */
|
|
737
|
+
principalId: string;
|
|
738
|
+
}
|
|
739
|
+
/** `danmaku` 参数 */
|
|
740
|
+
interface DanmakuParams {
|
|
741
|
+
/** 作品 ID */
|
|
742
|
+
photoId: string;
|
|
743
|
+
/** 窗口起点(毫秒,**含**) */
|
|
744
|
+
positionFromInclude: number;
|
|
745
|
+
/**
|
|
746
|
+
* 窗口终点(毫秒,**排他**)。
|
|
747
|
+
*
|
|
748
|
+
* 与起点的差**必须 < 60000** —— 达到 60000 时接口回 `result: 1` 但
|
|
749
|
+
* `danmakus: []`,不报错、静默给空数组。窗口的收窄由端点负责,这里只描述请求。
|
|
750
|
+
*/
|
|
751
|
+
positionToExclude: number;
|
|
752
|
+
/** 分页游标。弹幕不靠游标翻页(实测恒回 `no_more`),传空串即可 */
|
|
753
|
+
pcursor?: string;
|
|
754
|
+
/** 请求时间戳,前端传 `Date.now()`;不传由构造器补 */
|
|
755
|
+
timestamp?: number;
|
|
756
|
+
}
|
|
757
|
+
type KuaishouBaseApiRequest = {
|
|
758
|
+
type: string;
|
|
759
|
+
url: string;
|
|
760
|
+
};
|
|
761
|
+
/**
|
|
762
|
+
* 快手 `live_api` 请求描述对象。
|
|
763
|
+
*
|
|
764
|
+
* 除了最终请求地址外,还可以携带 `signPath`,用于声明
|
|
765
|
+
* `__NS_hxfalcon` 实际参与签名的规范路径。
|
|
766
|
+
*/
|
|
767
|
+
type KuaishouLiveApiRequest = KuaishouBaseApiRequest & {
|
|
768
|
+
method: 'GET' | 'POST';
|
|
769
|
+
requiresSign?: boolean;
|
|
770
|
+
signPath?: string;
|
|
771
|
+
body?: Record<string, unknown>;
|
|
772
|
+
};
|
|
773
|
+
/**
|
|
774
|
+
* 快手 GraphQL 请求描述对象。
|
|
775
|
+
*
|
|
776
|
+
* 该结构只负责描述请求,不负责执行网络请求。
|
|
777
|
+
*/
|
|
778
|
+
type KuaishouGraphqlRequest = KuaishouBaseApiRequest & {
|
|
779
|
+
body: {
|
|
780
|
+
operationName: string;
|
|
781
|
+
variables: Record<string, unknown>;
|
|
782
|
+
query: string;
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
/**
|
|
786
|
+
* 快手 H5 请求描述对象。
|
|
787
|
+
*
|
|
788
|
+
* 与 `live_api` 的差别:H5 接口一律 POST + JSON body,参数**必须在 body 里**
|
|
789
|
+
* (放 query 会拿到 `result=1` 但 0 条数据),且 body 参与签名。
|
|
790
|
+
*/
|
|
791
|
+
type KuaishouH5Request = KuaishouBaseApiRequest & {
|
|
792
|
+
method: 'POST';
|
|
793
|
+
/** 是否需要 `__NS_hxfalcon`。`ugH5App/*` 那几个免签 */
|
|
794
|
+
requiresSign: boolean;
|
|
795
|
+
/** 规范签名路径(H5 接口与公开路径一致,显式给出便于端点直接透传) */
|
|
796
|
+
signPath: string;
|
|
797
|
+
body: Record<string, unknown>;
|
|
798
|
+
/** 分享页 Referer。H5 接口按分享页来源校验,桌面的 `/new-reco` 不适用 */
|
|
799
|
+
referer: string;
|
|
800
|
+
};
|
|
801
|
+
//#endregion
|
|
802
|
+
export { RawResponse as A, TraceReason as C, ErrorKind as D, AmagiErrorCode as E, RequestSpec as M, RiskChallenge as O, RequestTrace as S, AmagiError as T, SignDecl as _, KuaishouLiveApiRequest as a, SignStep as b, UserWorkListParams as c, DataOf as d, EndpointDef as f, Registry as g, PublicParamsOf as h, KuaishouH5Request as i, RequestConfig as j, ValidationIssue as k, VideoInfoParams as l, ParsedOf as m, DanmakuParams as n, LiveRoomInfoParams as o, InternalParam as p, KuaishouGraphqlRequest as r, UserProfileParams as s, CommentParams as t, AnyEndpointDef as u, SignFn as v, Platform as w, AmagiMeta as x, SignPhase as y };
|