@ikenxuan/amagi 6.4.0 → 6.6.0
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/default/index.cjs +2006 -1155
- package/dist/default/index.d.ts +1093 -1007
- package/dist/default/index.mjs +1962 -1107
- package/dist/exports/axios.cjs +1 -1
- package/dist/exports/express.cjs +1 -1
- package/dist/{rolldown-runtime-D6vf50IK.cjs → rolldown-runtime-C0BPl7ul.cjs} +16 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +4 -4
package/dist/default/index.mjs
CHANGED
|
@@ -1,451 +1,13 @@
|
|
|
1
|
+
import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
|
|
1
2
|
import URL$1 from "node:url";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
4
|
import zod from "zod";
|
|
4
5
|
import { CryptoConfig, FingerprintGenerator, Xhshow } from "@ikenxuan/xhshow-ts";
|
|
5
6
|
import crypto, { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
7
|
import axios, { AxiosError } from "axios";
|
|
7
|
-
import { Chalk } from "chalk";
|
|
8
8
|
import protobuf from "protobufjs";
|
|
9
9
|
import express from "express";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 废弃 API 注册表
|
|
13
|
-
* 存储所有已注册的废弃 API 配置
|
|
14
|
-
*/
|
|
15
|
-
const deprecatedApis = /* @__PURE__ */ new Map();
|
|
16
|
-
/**
|
|
17
|
-
* 注册一个废弃的 API
|
|
18
|
-
*
|
|
19
|
-
* 将 API 添加到废弃注册表中,后续可通过 checkDeprecation 检查
|
|
20
|
-
*
|
|
21
|
-
* @param config - 废弃配置对象
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* registerDeprecatedApi({
|
|
26
|
-
* name: 'getDouyinData',
|
|
27
|
-
* deprecatedIn: '6.0.0',
|
|
28
|
-
* removedIn: '7.0.0',
|
|
29
|
-
* replacement: 'douyinFetcher',
|
|
30
|
-
* throwError: true
|
|
31
|
-
* })
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
function registerDeprecatedApi(config) {
|
|
35
|
-
deprecatedApis.set(config.name, config);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* 检查 API 是否已废弃并进行相应处理
|
|
39
|
-
*
|
|
40
|
-
* 如果 API 已注册为废弃,根据配置决定是打印警告还是抛出错误
|
|
41
|
-
*
|
|
42
|
-
* @param apiName - 要检查的 API 名称
|
|
43
|
-
* @throws {DeprecatedApiError} 如果 API 已废弃且配置为抛出错误
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```typescript
|
|
47
|
-
* // 在函数开头调用检查
|
|
48
|
-
* function getDouyinData(options) {
|
|
49
|
-
* checkDeprecation('getDouyinData')
|
|
50
|
-
* // ...
|
|
51
|
-
* }
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
function checkDeprecation(apiName) {
|
|
55
|
-
const config = deprecatedApis.get(apiName);
|
|
56
|
-
if (!config) return;
|
|
57
|
-
const message = buildDeprecationMessage(config);
|
|
58
|
-
if (config.throwError) throw new DeprecatedApiError(message, config);
|
|
59
|
-
else console.warn(message);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* 根据配置构建废弃提示消息
|
|
63
|
-
*
|
|
64
|
-
* @param config - 废弃配置
|
|
65
|
-
* @returns 格式化的废弃提示消息字符串
|
|
66
|
-
*/
|
|
67
|
-
function buildDeprecationMessage(config) {
|
|
68
|
-
const lines = [`[DEPRECATED] "${config.name}" 已在 v${config.deprecatedIn} 版本废弃。`];
|
|
69
|
-
if (config.replacement) lines.push(`请使用 "${config.replacement}" 替代。`);
|
|
70
|
-
else lines.push("此接口已被上游删除,无法继续使用,无可用替代方案。");
|
|
71
|
-
if (config.removedIn) lines.push(`此 API 将在 v${config.removedIn} 版本移除。`);
|
|
72
|
-
if (config.migrationGuide) lines.push(`迁移指南: ${config.migrationGuide}`);
|
|
73
|
-
return lines.join("\n");
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 废弃 API 调用错误类
|
|
77
|
-
*
|
|
78
|
-
* 当调用已废弃且配置为抛出错误的 API 时抛出此错误
|
|
79
|
-
* 包含完整的废弃配置信息,便于调试和迁移
|
|
80
|
-
*/
|
|
81
|
-
var DeprecatedApiError = class DeprecatedApiError extends Error {
|
|
82
|
-
/** 废弃配置信息,包含替代方案等详细信息 */
|
|
83
|
-
config;
|
|
84
|
-
/**
|
|
85
|
-
* 创建废弃 API 错误实例
|
|
86
|
-
*
|
|
87
|
-
* @param message - 错误消息
|
|
88
|
-
* @param config - 废弃配置对象
|
|
89
|
-
*/
|
|
90
|
-
constructor(message, config) {
|
|
91
|
-
super(message);
|
|
92
|
-
this.name = "DeprecatedApiError";
|
|
93
|
-
this.config = config;
|
|
94
|
-
Error.captureStackTrace?.(this, DeprecatedApiError);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
registerDeprecatedApi({
|
|
98
|
-
name: "getDouyinData",
|
|
99
|
-
deprecatedIn: "6.0.0",
|
|
100
|
-
removedIn: "7.0.0",
|
|
101
|
-
replacement: "douyinFetcher 或 client.douyin.fetcher",
|
|
102
|
-
migrationGuide: "https://github.com/ikenxuan/amagi/blob/main/packages/core/MIGRATION-v6.md",
|
|
103
|
-
throwError: true
|
|
104
|
-
});
|
|
105
|
-
registerDeprecatedApi({
|
|
106
|
-
name: "getBilibiliData",
|
|
107
|
-
deprecatedIn: "6.0.0",
|
|
108
|
-
removedIn: "7.0.0",
|
|
109
|
-
replacement: "bilibiliFetcher 或 client.bilibili.fetcher",
|
|
110
|
-
migrationGuide: "https://github.com/ikenxuan/amagi/blob/main/packages/core/MIGRATION-v6.md",
|
|
111
|
-
throwError: true
|
|
112
|
-
});
|
|
113
|
-
registerDeprecatedApi({
|
|
114
|
-
name: "getKuaishouData",
|
|
115
|
-
deprecatedIn: "6.0.0",
|
|
116
|
-
removedIn: "7.0.0",
|
|
117
|
-
replacement: "kuaishouFetcher 或 client.kuaishou.fetcher",
|
|
118
|
-
migrationGuide: "https://github.com/ikenxuan/amagi/blob/main/packages/core/MIGRATION-v6.md",
|
|
119
|
-
throwError: true
|
|
120
|
-
});
|
|
121
|
-
registerDeprecatedApi({
|
|
122
|
-
name: "getXiaohongshuData",
|
|
123
|
-
deprecatedIn: "6.0.0",
|
|
124
|
-
removedIn: "7.0.0",
|
|
125
|
-
replacement: "xiaohongshuFetcher 或 client.xiaohongshu.fetcher",
|
|
126
|
-
migrationGuide: "https://github.com/ikenxuan/amagi/blob/main/packages/core/MIGRATION-v6.md",
|
|
127
|
-
throwError: true
|
|
128
|
-
});
|
|
129
|
-
[
|
|
130
|
-
{
|
|
131
|
-
name: "单个视频作品数据",
|
|
132
|
-
replacement: "fetchVideoInfo"
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: "单个视频下载信息数据",
|
|
136
|
-
replacement: "fetchVideoStreamUrl"
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: "评论数据",
|
|
140
|
-
replacement: "fetchComments"
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: "指定评论的回复",
|
|
144
|
-
replacement: "fetchCommentReplies"
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: "用户主页数据",
|
|
148
|
-
replacement: "fetchUserCard"
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: "用户主页动态列表数据",
|
|
152
|
-
replacement: "fetchUserDynamicList"
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: "用户空间详细信息",
|
|
156
|
-
replacement: "fetchUserSpaceInfo"
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: "获取UP主总播放量",
|
|
160
|
-
replacement: "fetchUploaderTotalViews"
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: "Emoji数据",
|
|
164
|
-
replacement: "fetchEmojiList"
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: "番剧基本信息数据",
|
|
168
|
-
replacement: "fetchBangumiInfo"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: "番剧下载信息数据",
|
|
172
|
-
replacement: "fetchBangumiStreamUrl"
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
name: "动态详情数据",
|
|
176
|
-
replacement: "fetchDynamicDetail"
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
name: "直播间信息",
|
|
180
|
-
replacement: "fetchLiveRoomInfo"
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: "直播间初始化信息",
|
|
184
|
-
replacement: "fetchLiveRoomInitInfo"
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: "登录基本信息",
|
|
188
|
-
replacement: "fetchLoginStatus"
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "申请二维码",
|
|
192
|
-
replacement: "requestLoginQrcode"
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: "二维码状态",
|
|
196
|
-
replacement: "checkQrcodeStatus"
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
name: "AV转BV",
|
|
200
|
-
replacement: "convertAvToBv"
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: "BV转AV",
|
|
204
|
-
replacement: "convertBvToAv"
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
name: "专栏正文内容",
|
|
208
|
-
replacement: "fetchArticleContent"
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: "专栏显示卡片信息",
|
|
212
|
-
replacement: "fetchArticleCards"
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
name: "专栏文章基本信息",
|
|
216
|
-
replacement: "fetchArticleInfo"
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
name: "文集基本信息",
|
|
220
|
-
replacement: "fetchArticleListInfo"
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
name: "实时弹幕",
|
|
224
|
-
replacement: "fetchVideoDanmaku"
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
name: "从_v_voucher_申请_captcha",
|
|
228
|
-
replacement: "requestCaptchaFromVoucher"
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
name: "验证验证码结果",
|
|
232
|
-
replacement: "validateCaptchaResult"
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
name: "视频作品数据",
|
|
236
|
-
replacement: "fetchVideoWork"
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
name: "图集作品数据",
|
|
240
|
-
replacement: "fetchImageAlbumWork"
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
name: "合辑作品数据",
|
|
244
|
-
replacement: "fetchSlidesWork"
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "文字作品数据",
|
|
248
|
-
replacement: "fetchTextWork"
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "聚合解析",
|
|
252
|
-
replacement: "parseWork"
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: "指定评论回复数据",
|
|
256
|
-
replacement: "fetchCommentReplies"
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
name: "用户主页视频列表数据",
|
|
260
|
-
replacement: "fetchUserVideoList"
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: "热点词数据",
|
|
264
|
-
replacement: "fetchSuggestWords"
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: "搜索数据",
|
|
268
|
-
replacement: "searchContent"
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: "音乐数据",
|
|
272
|
-
replacement: "fetchMusicInfo"
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: "直播间信息数据",
|
|
276
|
-
replacement: "fetchLiveRoomInfo"
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
name: "申请二维码数据",
|
|
280
|
-
replacement: "requestLoginQrcode"
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: "动态表情数据",
|
|
284
|
-
replacement: "fetchDynamicEmojiList"
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
name: "弹幕数据",
|
|
288
|
-
replacement: "fetchDanmakuList"
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
name: "单个视频作品数据",
|
|
292
|
-
replacement: "fetchVideoWork"
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "首页推荐数据",
|
|
296
|
-
replacement: "fetchHomeFeed"
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: "单个笔记数据",
|
|
300
|
-
replacement: "fetchNoteDetail"
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
name: "用户数据",
|
|
304
|
-
replacement: "fetchUserProfile"
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
name: "用户笔记数据",
|
|
308
|
-
replacement: "fetchUserNoteList"
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
name: "表情列表",
|
|
312
|
-
replacement: "fetchEmojiList"
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
name: "搜索笔记",
|
|
316
|
-
replacement: "searchNotes"
|
|
317
|
-
}
|
|
318
|
-
].forEach(({ name, replacement }) => {
|
|
319
|
-
registerDeprecatedApi({
|
|
320
|
-
name: `methodType: '${name}'`,
|
|
321
|
-
deprecatedIn: "6.0.0",
|
|
322
|
-
removedIn: "7.0.0",
|
|
323
|
-
replacement: `fetcher.${replacement}()`,
|
|
324
|
-
throwError: true
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
registerDeprecatedApi({
|
|
328
|
-
name: `methodType: '动态卡片数据'`,
|
|
329
|
-
deprecatedIn: "6.0.0",
|
|
330
|
-
removedIn: "7.0.0",
|
|
331
|
-
migrationGuide: "https://amagi-docs.vercel.app/docs/changelog/6.1.3",
|
|
332
|
-
throwError: false
|
|
333
|
-
});
|
|
334
|
-
registerDeprecatedApi({
|
|
335
|
-
name: "fetchDynamicCard",
|
|
336
|
-
deprecatedIn: "6.1.3",
|
|
337
|
-
removedIn: "7.0.0",
|
|
338
|
-
migrationGuide: "https://amagi-docs.vercel.app/docs/changelog/6.1.3",
|
|
339
|
-
throwError: false
|
|
340
|
-
});
|
|
341
|
-
//#endregion
|
|
342
|
-
//#region src/model/DataFetchers.ts
|
|
343
|
-
/**
|
|
344
|
-
* 数据获取器模块 (已废弃)
|
|
345
|
-
*
|
|
346
|
-
* 此模块中的 getXXXData 函数已在 v6 版本废弃并移除
|
|
347
|
-
* 请使用新的 fetcher API 替代
|
|
348
|
-
*
|
|
349
|
-
* @module model/DataFetchers
|
|
350
|
-
* @deprecated v6 已废弃,请使用 fetcher API 替代
|
|
351
|
-
*/
|
|
352
|
-
/**
|
|
353
|
-
* 获取抖音数据
|
|
354
|
-
*
|
|
355
|
-
* @deprecated v6 已废弃,请使用 douyinFetcher 或 client.douyin.fetcher 替代
|
|
356
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
357
|
-
*
|
|
358
|
-
* @example
|
|
359
|
-
* ```typescript
|
|
360
|
-
* // 旧用法 (已废弃,会抛出错误)
|
|
361
|
-
* const data = await getDouyinData('videoWork', { aweme_id: '123' }, cookie)
|
|
362
|
-
*
|
|
363
|
-
* // 新用法
|
|
364
|
-
* import { douyinFetcher } from '@ikenxuan/amagi'
|
|
365
|
-
* const data = await douyinFetcher.fetchVideoWork({ aweme_id: '123' }, cookie)
|
|
366
|
-
*
|
|
367
|
-
* // 或使用客户端实例
|
|
368
|
-
* const client = createAmagiClient({ cookies: { douyin: cookie } })
|
|
369
|
-
* const data = await client.douyin.fetcher.fetchVideoWork({ aweme_id: '123' })
|
|
370
|
-
* ```
|
|
371
|
-
*/
|
|
372
|
-
function getDouyinData(..._args) {
|
|
373
|
-
checkDeprecation("getDouyinData");
|
|
374
|
-
throw new Error("getDouyinData 已废弃");
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* 获取B站数据
|
|
378
|
-
*
|
|
379
|
-
* @deprecated v6 已废弃,请使用 bilibiliFetcher 或 client.bilibili.fetcher 替代
|
|
380
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
381
|
-
*
|
|
382
|
-
* @example
|
|
383
|
-
* ```typescript
|
|
384
|
-
* // 旧用法 (已废弃,会抛出错误)
|
|
385
|
-
* const data = await getBilibiliData('videoInfo', { bvid: 'BV123' }, cookie)
|
|
386
|
-
*
|
|
387
|
-
* // 新用法
|
|
388
|
-
* import { bilibiliFetcher } from '@ikenxuan/amagi'
|
|
389
|
-
* const data = await bilibiliFetcher.fetchVideoInfo({ bvid: 'BV123' }, cookie)
|
|
390
|
-
*
|
|
391
|
-
* // 或使用客户端实例
|
|
392
|
-
* const client = createAmagiClient({ cookies: { bilibili: cookie } })
|
|
393
|
-
* const data = await client.bilibili.fetcher.fetchVideoInfo({ bvid: 'BV123' })
|
|
394
|
-
* ```
|
|
395
|
-
*/
|
|
396
|
-
function getBilibiliData(..._args) {
|
|
397
|
-
checkDeprecation("getBilibiliData");
|
|
398
|
-
throw new Error("getBilibiliData 已废弃");
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* 获取快手数据
|
|
402
|
-
*
|
|
403
|
-
* @deprecated v6 已废弃,请使用 kuaishouFetcher 或 client.kuaishou.fetcher 替代
|
|
404
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
405
|
-
*
|
|
406
|
-
* @example
|
|
407
|
-
* ```typescript
|
|
408
|
-
* // 旧用法 (已废弃,会抛出错误)
|
|
409
|
-
* const data = await getKuaishouData('videoWork', { photoId: '123' }, cookie)
|
|
410
|
-
*
|
|
411
|
-
* // 新用法
|
|
412
|
-
* import { kuaishouFetcher } from '@ikenxuan/amagi'
|
|
413
|
-
* const data = await kuaishouFetcher.fetchVideoWork({ photoId: '123' }, cookie)
|
|
414
|
-
*
|
|
415
|
-
* // 或使用客户端实例
|
|
416
|
-
* const client = createAmagiClient({ cookies: { kuaishou: cookie } })
|
|
417
|
-
* const data = await client.kuaishou.fetcher.fetchVideoWork({ photoId: '123' })
|
|
418
|
-
* ```
|
|
419
|
-
*/
|
|
420
|
-
function getKuaishouData(..._args) {
|
|
421
|
-
checkDeprecation("getKuaishouData");
|
|
422
|
-
throw new Error("getKuaishouData 已废弃");
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* 获取小红书数据
|
|
426
|
-
*
|
|
427
|
-
* @deprecated v6 已废弃,请使用 xiaohongshuFetcher 或 client.xiaohongshu.fetcher 替代
|
|
428
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
429
|
-
*
|
|
430
|
-
* @example
|
|
431
|
-
* ```typescript
|
|
432
|
-
* // 旧用法 (已废弃,会抛出错误)
|
|
433
|
-
* const data = await getXiaohongshuData('noteDetail', { note_id: '123' }, cookie)
|
|
434
|
-
*
|
|
435
|
-
* // 新用法
|
|
436
|
-
* import { xiaohongshuFetcher } from '@ikenxuan/amagi'
|
|
437
|
-
* const data = await xiaohongshuFetcher.fetchNoteDetail({ note_id: '123' }, cookie)
|
|
438
|
-
*
|
|
439
|
-
* // 或使用客户端实例
|
|
440
|
-
* const client = createAmagiClient({ cookies: { xiaohongshu: cookie } })
|
|
441
|
-
* const data = await client.xiaohongshu.fetcher.fetchNoteDetail({ note_id: '123' })
|
|
442
|
-
* ```
|
|
443
|
-
*/
|
|
444
|
-
function getXiaohongshuData(..._args) {
|
|
445
|
-
checkDeprecation("getXiaohongshuData");
|
|
446
|
-
throw new Error("getXiaohongshuData 已废弃");
|
|
447
|
-
}
|
|
448
|
-
//#endregion
|
|
10
|
+
import { Chalk } from "chalk";
|
|
449
11
|
//#region src/platform/bilibili/API.ts
|
|
450
12
|
/**
|
|
451
13
|
* B站 API URL 构建类
|
|
@@ -517,15 +79,6 @@ var BilibiliAPI = class {
|
|
|
517
79
|
getDynamicDetail(data) {
|
|
518
80
|
return `https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${data.dynamic_id}&features=itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme`;
|
|
519
81
|
}
|
|
520
|
-
/**
|
|
521
|
-
* 获取动态卡片信息
|
|
522
|
-
*
|
|
523
|
-
* @deprecated B站官方已于 `2025-08-09` 删除原 `dynamic_svr` 接口,该接口已停用。
|
|
524
|
-
* 调用将返回错误信息,请使用 {@link getDynamicDetail} 替代。
|
|
525
|
-
*/
|
|
526
|
-
getDynamicCard(data) {
|
|
527
|
-
return this.getDynamicDetail(data);
|
|
528
|
-
}
|
|
529
82
|
/** 获取用户名片信息 */
|
|
530
83
|
getUserCard(data) {
|
|
531
84
|
return `https://api.bilibili.com/x/web-interface/card?mid=${data.host_mid}&photo=true`;
|
|
@@ -617,86 +170,6 @@ var BilibiliAPI = class {
|
|
|
617
170
|
/** B站 API URL 构建器实例 */
|
|
618
171
|
const bilibiliApiUrls = new BilibiliAPI();
|
|
619
172
|
//#endregion
|
|
620
|
-
//#region src/platform/bilibili/BilibiliApi.ts
|
|
621
|
-
/**
|
|
622
|
-
* 创建废弃的 API 存根函数
|
|
623
|
-
*/
|
|
624
|
-
const createDeprecatedStub$3 = (methodName) => {
|
|
625
|
-
return (..._args) => {
|
|
626
|
-
checkDeprecation("getBilibiliData");
|
|
627
|
-
throw new Error(`bilibili.${methodName} 已废弃,请使用 bilibiliFetcher 替代`);
|
|
628
|
-
};
|
|
629
|
-
};
|
|
630
|
-
/**
|
|
631
|
-
* B站相关 API 的命名空间。
|
|
632
|
-
*
|
|
633
|
-
* @deprecated v6 已废弃,请使用 bilibiliFetcher 或 client.bilibili.fetcher 替代
|
|
634
|
-
*/
|
|
635
|
-
const bilibili = {
|
|
636
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchVideoInfo 替代 */
|
|
637
|
-
getVideoInfo: createDeprecatedStub$3("getVideoInfo"),
|
|
638
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchVideoStreamUrl 替代 */
|
|
639
|
-
getVideoStream: createDeprecatedStub$3("getVideoStream"),
|
|
640
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchComments 替代 */
|
|
641
|
-
getComments: createDeprecatedStub$3("getComments"),
|
|
642
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchCommentReplies 替代 */
|
|
643
|
-
getCommentReply: createDeprecatedStub$3("getCommentReply"),
|
|
644
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchUserCard 替代 */
|
|
645
|
-
getUserProfile: createDeprecatedStub$3("getUserProfile"),
|
|
646
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchUserDynamicList 替代 */
|
|
647
|
-
getUserDynamic: createDeprecatedStub$3("getUserDynamic"),
|
|
648
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchEmojiList 替代 */
|
|
649
|
-
getEmojiList: createDeprecatedStub$3("getEmojiList"),
|
|
650
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchBangumiInfo 替代 */
|
|
651
|
-
getBangumiInfo: createDeprecatedStub$3("getBangumiInfo"),
|
|
652
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchBangumiStreamUrl 替代 */
|
|
653
|
-
getBangumiStream: createDeprecatedStub$3("getBangumiStream"),
|
|
654
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchDynamicDetail 替代 */
|
|
655
|
-
getDynamicInfo: createDeprecatedStub$3("getDynamicInfo"),
|
|
656
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchDynamicCard 替代 */
|
|
657
|
-
getDynamicCard: createDeprecatedStub$3("getDynamicCard"),
|
|
658
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchLiveRoomInfo 替代 */
|
|
659
|
-
getLiveRoomDetail: createDeprecatedStub$3("getLiveRoomDetail"),
|
|
660
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchLiveRoomInitInfo 替代 */
|
|
661
|
-
getLiveRoomInitInfo: createDeprecatedStub$3("getLiveRoomInitInfo"),
|
|
662
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchLoginStatus 替代 */
|
|
663
|
-
getLoginBasicInfo: createDeprecatedStub$3("getLoginBasicInfo"),
|
|
664
|
-
/** @deprecated 请使用 bilibiliFetcher.requestLoginQrcode 替代 */
|
|
665
|
-
getLoginQrcode: createDeprecatedStub$3("getLoginQrcode"),
|
|
666
|
-
/** @deprecated 请使用 bilibiliFetcher.checkQrcodeStatus 替代 */
|
|
667
|
-
checkQrcodeStatus: createDeprecatedStub$3("checkQrcodeStatus"),
|
|
668
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchUploaderTotalViews 替代 */
|
|
669
|
-
getUserTotalPlayCount: createDeprecatedStub$3("getUserTotalPlayCount"),
|
|
670
|
-
/** @deprecated 请使用 bilibiliFetcher.convertAvToBv 替代 */
|
|
671
|
-
convertAvToBv: createDeprecatedStub$3("convertAvToBv"),
|
|
672
|
-
/** @deprecated 请使用 bilibiliFetcher.convertBvToAv 替代 */
|
|
673
|
-
convertBvToAv: createDeprecatedStub$3("convertBvToAv"),
|
|
674
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchArticleContent 替代 */
|
|
675
|
-
getArticleContent: createDeprecatedStub$3("getArticleContent"),
|
|
676
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchArticleCards 替代 */
|
|
677
|
-
getArticleCard: createDeprecatedStub$3("getArticleCard"),
|
|
678
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchArticleInfo 替代 */
|
|
679
|
-
getArticleInfo: createDeprecatedStub$3("getArticleInfo"),
|
|
680
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchArticleListInfo 替代 */
|
|
681
|
-
getColumnInfo: createDeprecatedStub$3("getColumnInfo"),
|
|
682
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchUserSpaceInfo 替代 */
|
|
683
|
-
getUserProfileDetail: createDeprecatedStub$3("getUserProfileDetail"),
|
|
684
|
-
/** @deprecated 请使用 bilibiliFetcher.requestCaptchaFromVoucher 替代 */
|
|
685
|
-
applyVoucherCaptcha: createDeprecatedStub$3("applyVoucherCaptcha"),
|
|
686
|
-
/** @deprecated 请使用 bilibiliFetcher.validateCaptchaResult 替代 */
|
|
687
|
-
validateCaptcha: createDeprecatedStub$3("validateCaptcha"),
|
|
688
|
-
/** @deprecated 请使用 bilibiliFetcher.fetchVideoDanmaku 替代 */
|
|
689
|
-
getDanmaku: createDeprecatedStub$3("getDanmaku")
|
|
690
|
-
};
|
|
691
|
-
/**
|
|
692
|
-
* 创建绑定了cookie的B站API对象
|
|
693
|
-
*
|
|
694
|
-
* @deprecated v6 已废弃,请使用 createBoundBilibiliFetcher 替代
|
|
695
|
-
*/
|
|
696
|
-
const createBoundBilibiliApi = (_cookie, _requestConfig) => {
|
|
697
|
-
return { ...bilibili };
|
|
698
|
-
};
|
|
699
|
-
//#endregion
|
|
700
173
|
//#region src/model/events.ts
|
|
701
174
|
/**
|
|
702
175
|
* Amagi 事件系统
|
|
@@ -1009,7 +482,7 @@ const BilibiliBangumiStreamParamsSchema = zod.object({
|
|
|
1009
482
|
});
|
|
1010
483
|
/** 动态参数验证 */
|
|
1011
484
|
const BilibiliDynamicParamsSchema = zod.object({
|
|
1012
|
-
methodType: zod.
|
|
485
|
+
methodType: zod.literal("dynamicDetail", { error: "方法类型必须是\"dynamicDetail\"" }),
|
|
1013
486
|
dynamic_id: zod.string({ error: "动态ID必须是字符串" }).min(1, { error: "动态ID不能为空" })
|
|
1014
487
|
});
|
|
1015
488
|
/** 直播间参数验证 */
|
|
@@ -1091,7 +564,6 @@ const BilibiliValidationSchemas = {
|
|
|
1091
564
|
bangumiInfo: BilibiliBangumiInfoParamsSchema,
|
|
1092
565
|
bangumiStream: BilibiliBangumiStreamParamsSchema,
|
|
1093
566
|
dynamicDetail: BilibiliDynamicParamsSchema,
|
|
1094
|
-
dynamicCard: BilibiliDynamicParamsSchema,
|
|
1095
567
|
liveRoomInfo: BilibiliLiveParamsSchema,
|
|
1096
568
|
liveRoomInit: BilibiliLiveParamsSchema,
|
|
1097
569
|
loginStatus: BilibiliLoginParamsSchema,
|
|
@@ -1122,7 +594,6 @@ const BilibiliMethodRoutes = {
|
|
|
1122
594
|
bangumiInfo: "/fetch_bangumi_video_info",
|
|
1123
595
|
bangumiStream: "/fetch_bangumi_video_playurl",
|
|
1124
596
|
dynamicDetail: "/fetch_dynamic_info",
|
|
1125
|
-
dynamicCard: "/fetch_dynamic_card",
|
|
1126
597
|
liveRoomInfo: "/fetch_live_room_detail",
|
|
1127
598
|
liveRoomInit: "/fetch_liveroom_def",
|
|
1128
599
|
loginStatus: "/login_basic_info",
|
|
@@ -1699,18 +1170,20 @@ const xiaohongshuApiUrls = {
|
|
|
1699
1170
|
* @returns 完整的接口URL
|
|
1700
1171
|
*/
|
|
1701
1172
|
noteComments(data) {
|
|
1173
|
+
const baseUrl = "https://edith.xiaohongshu.com/api/sns/web/v2/comment/page";
|
|
1174
|
+
const params = {
|
|
1175
|
+
note_id: data.note_id,
|
|
1176
|
+
cursor: data.cursor ?? "",
|
|
1177
|
+
image_formats: [
|
|
1178
|
+
"jpg",
|
|
1179
|
+
"webp",
|
|
1180
|
+
"avif"
|
|
1181
|
+
].join(","),
|
|
1182
|
+
xsec_token: data.xsec_token
|
|
1183
|
+
};
|
|
1702
1184
|
return {
|
|
1703
1185
|
apiPath: "/api/sns/web/v2/comment/page",
|
|
1704
|
-
Url:
|
|
1705
|
-
note_id: data.note_id,
|
|
1706
|
-
cursor: data.cursor ?? "",
|
|
1707
|
-
image_formats: [
|
|
1708
|
-
"jpg",
|
|
1709
|
-
"webp",
|
|
1710
|
-
"avif"
|
|
1711
|
-
].join(","),
|
|
1712
|
-
xsec_token: data.xsec_token
|
|
1713
|
-
})}`
|
|
1186
|
+
Url: `${baseUrl}?${buildQueryString$1(params)}`
|
|
1714
1187
|
};
|
|
1715
1188
|
},
|
|
1716
1189
|
/**
|
|
@@ -1730,19 +1203,21 @@ const xiaohongshuApiUrls = {
|
|
|
1730
1203
|
* @returns 完整的接口URL
|
|
1731
1204
|
*/
|
|
1732
1205
|
userNoteList(data) {
|
|
1206
|
+
const baseUrl = "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted";
|
|
1207
|
+
const params = {
|
|
1208
|
+
user_id: data.user_id,
|
|
1209
|
+
cursor: data.cursor ?? "",
|
|
1210
|
+
num: data.num ?? 30,
|
|
1211
|
+
image_formats: [
|
|
1212
|
+
"jpg",
|
|
1213
|
+
"webp",
|
|
1214
|
+
"avif"
|
|
1215
|
+
].join(","),
|
|
1216
|
+
xsec_source: "pc_feed"
|
|
1217
|
+
};
|
|
1733
1218
|
return {
|
|
1734
1219
|
apiPath: "/api/sns/web/v1/user_posted",
|
|
1735
|
-
Url:
|
|
1736
|
-
user_id: data.user_id,
|
|
1737
|
-
cursor: data.cursor ?? "",
|
|
1738
|
-
num: data.num ?? 30,
|
|
1739
|
-
image_formats: [
|
|
1740
|
-
"jpg",
|
|
1741
|
-
"webp",
|
|
1742
|
-
"avif"
|
|
1743
|
-
].join(","),
|
|
1744
|
-
xsec_source: "pc_feed"
|
|
1745
|
-
})}`
|
|
1220
|
+
Url: `${baseUrl}?${buildQueryString$1(params)}`
|
|
1746
1221
|
};
|
|
1747
1222
|
},
|
|
1748
1223
|
/**
|
|
@@ -1959,7 +1434,8 @@ const createErrorResponse = (error, message, code = 500, data) => {
|
|
|
1959
1434
|
async function fetchBilibiliInternal(methodType, options, config) {
|
|
1960
1435
|
const startTime = Date.now();
|
|
1961
1436
|
try {
|
|
1962
|
-
const
|
|
1437
|
+
const apiParams = { ...validateBilibiliParams(methodType, options) };
|
|
1438
|
+
const rawData = await fetchBilibili(apiParams, config.cookie, config.requestConfig);
|
|
1963
1439
|
const duration = Date.now() - startTime;
|
|
1964
1440
|
if (rawData.code !== 0) {
|
|
1965
1441
|
const errorMessage = rawData.message || "B站数据获取失败";
|
|
@@ -1971,11 +1447,12 @@ async function fetchBilibiliInternal(methodType, options, config) {
|
|
|
1971
1447
|
url: void 0,
|
|
1972
1448
|
duration
|
|
1973
1449
|
});
|
|
1974
|
-
|
|
1450
|
+
const amagiError = rawData.amagiError ?? {
|
|
1975
1451
|
errorDescription: errorMessage,
|
|
1976
1452
|
requestType: methodType,
|
|
1977
1453
|
requestUrl: void 0
|
|
1978
|
-
}
|
|
1454
|
+
};
|
|
1455
|
+
return createErrorResponse(amagiError, errorMessage, rawData.code, rawData);
|
|
1979
1456
|
}
|
|
1980
1457
|
const result = createSuccessResponse(rawData, "获取成功", 200);
|
|
1981
1458
|
emitApiSuccess({
|
|
@@ -2282,31 +1759,6 @@ async function fetchDynamicDetail(options, cookie, requestConfig) {
|
|
|
2282
1759
|
requestConfig
|
|
2283
1760
|
});
|
|
2284
1761
|
}
|
|
2285
|
-
/**
|
|
2286
|
-
* 获取B站动态卡片信息
|
|
2287
|
-
*
|
|
2288
|
-
* @deprecated v6.1.3 已废弃,B站官方已于 `2025-08-09` 删除原 `dynamic_svr` 接口。
|
|
2289
|
-
* 调用将返回错误信息
|
|
2290
|
-
* 计划于 v7.0.0 移除。
|
|
2291
|
-
*
|
|
2292
|
-
* @param options - 动态参数
|
|
2293
|
-
* @param options.dynamic_id - 动态 ID
|
|
2294
|
-
* @param cookie - B站 Cookie (可选)
|
|
2295
|
-
* @param requestConfig - 请求配置 (可选)
|
|
2296
|
-
* @returns 动态卡片数据(已停用,返回错误信息)
|
|
2297
|
-
* @example
|
|
2298
|
-
* ```typescript
|
|
2299
|
-
* const result = await fetchDynamicCard({ dynamic_id: '123456789' }, cookie)
|
|
2300
|
-
* // result.success === false,错误信息提示接口已停用
|
|
2301
|
-
* ```
|
|
2302
|
-
*/
|
|
2303
|
-
async function fetchDynamicCard(options, cookie, requestConfig) {
|
|
2304
|
-
checkDeprecation("fetchDynamicCard");
|
|
2305
|
-
return fetchBilibiliInternal("dynamicCard", options, {
|
|
2306
|
-
cookie,
|
|
2307
|
-
requestConfig
|
|
2308
|
-
});
|
|
2309
|
-
}
|
|
2310
1762
|
//#endregion
|
|
2311
1763
|
//#region src/model/fetchers/bilibili/live.ts
|
|
2312
1764
|
/**
|
|
@@ -2625,8 +2077,6 @@ function createBoundBilibiliFetcher(cookie, requestConfig) {
|
|
|
2625
2077
|
fetchUserSpaceInfo: (options, override) => fetchUserSpaceInfo(options, ...resolveRequest(override)),
|
|
2626
2078
|
fetchUploaderTotalViews: (options, override) => fetchUploaderTotalViews(options, ...resolveRequest(override)),
|
|
2627
2079
|
fetchDynamicDetail: (options, override) => fetchDynamicDetail(options, ...resolveRequest(override)),
|
|
2628
|
-
/** @deprecated v6.1.3 已废弃,调用将返回错误信息 */
|
|
2629
|
-
fetchDynamicCard: (options, override) => fetchDynamicCard(options, ...resolveRequest(override)),
|
|
2630
2080
|
fetchBangumiInfo: (options, override) => fetchBangumiInfo(options, ...resolveRequest(override)),
|
|
2631
2081
|
fetchBangumiStreamUrl: (options, override) => fetchBangumiStreamUrl(options, ...resolveRequest(override)),
|
|
2632
2082
|
fetchLiveRoomInfo: (options, override) => fetchLiveRoomInfo$2(options, ...resolveRequest(override)),
|
|
@@ -2646,51 +2096,1739 @@ function createBoundBilibiliFetcher(cookie, requestConfig) {
|
|
|
2646
2096
|
};
|
|
2647
2097
|
}
|
|
2648
2098
|
//#endregion
|
|
2649
|
-
//#region src/model/fetchers/bilibili/index.ts
|
|
2099
|
+
//#region src/model/fetchers/bilibili/index.ts
|
|
2100
|
+
/**
|
|
2101
|
+
* B站 Fetcher 模块入口
|
|
2102
|
+
* @module fetchers/bilibili
|
|
2103
|
+
*/
|
|
2104
|
+
/**
|
|
2105
|
+
* B站数据获取器
|
|
2106
|
+
* 包含所有 B站 API 方法,调用时需要传递 cookie
|
|
2107
|
+
* @example
|
|
2108
|
+
* ```typescript
|
|
2109
|
+
* import { bilibiliFetcher } from '@ikenxuan/amagi'
|
|
2110
|
+
*
|
|
2111
|
+
* const result = await bilibiliFetcher.fetchVideoInfo({ bvid: 'BV1xx411c7mD' }, cookie)
|
|
2112
|
+
* ```
|
|
2113
|
+
*/
|
|
2114
|
+
const bilibiliFetcher = {
|
|
2115
|
+
fetchVideoInfo,
|
|
2116
|
+
fetchVideoStreamUrl,
|
|
2117
|
+
fetchVideoDanmaku,
|
|
2118
|
+
fetchComments,
|
|
2119
|
+
fetchCommentReplies: fetchCommentReplies$1,
|
|
2120
|
+
fetchUserCard,
|
|
2121
|
+
fetchUserDynamicList,
|
|
2122
|
+
fetchUserLiveStatus,
|
|
2123
|
+
fetchUserSpaceInfo,
|
|
2124
|
+
fetchUploaderTotalViews,
|
|
2125
|
+
fetchDynamicDetail,
|
|
2126
|
+
fetchBangumiInfo,
|
|
2127
|
+
fetchBangumiStreamUrl,
|
|
2128
|
+
fetchLiveRoomInfo: fetchLiveRoomInfo$2,
|
|
2129
|
+
fetchLiveRoomInitInfo,
|
|
2130
|
+
fetchArticleContent,
|
|
2131
|
+
fetchArticleCards,
|
|
2132
|
+
fetchArticleInfo,
|
|
2133
|
+
fetchArticleListInfo,
|
|
2134
|
+
fetchLoginStatus,
|
|
2135
|
+
requestLoginQrcode: requestLoginQrcode$1,
|
|
2136
|
+
checkQrcodeStatus,
|
|
2137
|
+
requestCaptchaFromVoucher,
|
|
2138
|
+
validateCaptchaResult,
|
|
2139
|
+
convertAvToBv,
|
|
2140
|
+
convertBvToAv,
|
|
2141
|
+
fetchEmojiList: fetchEmojiList$3
|
|
2142
|
+
};
|
|
2143
|
+
//#endregion
|
|
2144
|
+
//#region src/platform/douyin/passport/sm3.ts
|
|
2145
|
+
/**
|
|
2146
|
+
* SM3 摘要(GM/T 0004-2012),抖音 bdms 签名链使用的变体
|
|
2147
|
+
*
|
|
2148
|
+
* 与标准实现的唯一差异:字符串按 `charCodeAt` 逐字符取字节(非 UTF-8 编码),
|
|
2149
|
+
* 与浏览器里 bdms 的 `strToBytes` 行为一致。签名输入均为 ASCII,实际不会踩到多字节分支,
|
|
2150
|
+
* 但仍保留该分支以保证与浏览器实现逐位一致。
|
|
2151
|
+
*/
|
|
2152
|
+
/** 循环左移 32 位 */
|
|
2153
|
+
const rotl = (x, n) => {
|
|
2154
|
+
const shift = n % 32;
|
|
2155
|
+
return (x << shift | x >>> 32 - shift) >>> 0;
|
|
2156
|
+
};
|
|
2157
|
+
/** 轮常量 Tj */
|
|
2158
|
+
const tj = (j) => j < 16 ? 2043430169 : 2055708042;
|
|
2159
|
+
/** 布尔函数 FFj */
|
|
2160
|
+
const ff = (j, x, y, z) => j < 16 ? (x ^ y ^ z) >>> 0 : (x & y | x & z | y & z) >>> 0;
|
|
2161
|
+
/** 布尔函数 GGj */
|
|
2162
|
+
const gg = (j, x, y, z) => j < 16 ? (x ^ y ^ z) >>> 0 : (x & y | ~x & z) >>> 0;
|
|
2163
|
+
/** 初始向量 IV */
|
|
2164
|
+
const IV = [
|
|
2165
|
+
1937774191,
|
|
2166
|
+
1226093241,
|
|
2167
|
+
388252375,
|
|
2168
|
+
3666478592,
|
|
2169
|
+
2842636476,
|
|
2170
|
+
372324522,
|
|
2171
|
+
3817729613,
|
|
2172
|
+
2969243214
|
|
2173
|
+
];
|
|
2174
|
+
/** 字符串转字节数组:逐字符 charCodeAt,大于一字节时按大端拆分 */
|
|
2175
|
+
const strToBytes = (input) => {
|
|
2176
|
+
const bytes = [];
|
|
2177
|
+
for (let i = 0; i < input.length; i++) {
|
|
2178
|
+
let code = input.charCodeAt(i);
|
|
2179
|
+
const chunk = [];
|
|
2180
|
+
do {
|
|
2181
|
+
chunk.push(code & 255);
|
|
2182
|
+
code >>= 8;
|
|
2183
|
+
} while (code);
|
|
2184
|
+
bytes.push(...chunk.reverse());
|
|
2185
|
+
}
|
|
2186
|
+
return bytes;
|
|
2187
|
+
};
|
|
2188
|
+
/** 消息扩展:生成 W[0..67] 与 W'[0..63](后者放在 68 之后) */
|
|
2189
|
+
const expand = (block) => {
|
|
2190
|
+
const w = new Array(132);
|
|
2191
|
+
for (let i = 0; i < 16; i++) w[i] = (block[i * 4] << 24 | block[i * 4 + 1] << 16 | block[i * 4 + 2] << 8 | block[i * 4 + 3]) >>> 0;
|
|
2192
|
+
for (let j = 16; j < 68; j++) {
|
|
2193
|
+
let x = w[j - 16] ^ w[j - 9] ^ rotl(w[j - 3], 15);
|
|
2194
|
+
x = x ^ rotl(x, 15) ^ rotl(x, 23);
|
|
2195
|
+
w[j] = (x ^ rotl(w[j - 13], 7) ^ w[j - 6]) >>> 0;
|
|
2196
|
+
}
|
|
2197
|
+
for (let j = 0; j < 64; j++) w[j + 68] = (w[j] ^ w[j + 4]) >>> 0;
|
|
2198
|
+
return w;
|
|
2199
|
+
};
|
|
2200
|
+
/** SM3 压缩函数:就地更新寄存器 */
|
|
2201
|
+
const compress = (reg, block) => {
|
|
2202
|
+
const w = expand(block);
|
|
2203
|
+
const r = reg.slice();
|
|
2204
|
+
for (let j = 0; j < 64; j++) {
|
|
2205
|
+
let ss1 = rotl(r[0], 12) + r[4] + rotl(tj(j), j) & 4294967295;
|
|
2206
|
+
ss1 = rotl(ss1 >>> 0, 7);
|
|
2207
|
+
const ss2 = (ss1 ^ rotl(r[0], 12)) >>> 0;
|
|
2208
|
+
const tt1 = ff(j, r[0], r[1], r[2]) + r[3] + ss2 + w[j + 68] >>> 0;
|
|
2209
|
+
const tt2 = gg(j, r[4], r[5], r[6]) + r[7] + ss1 + w[j] >>> 0;
|
|
2210
|
+
r[3] = r[2];
|
|
2211
|
+
r[2] = rotl(r[1], 9);
|
|
2212
|
+
r[1] = r[0];
|
|
2213
|
+
r[0] = tt1;
|
|
2214
|
+
r[7] = r[6];
|
|
2215
|
+
r[6] = rotl(r[5], 19);
|
|
2216
|
+
r[5] = r[4];
|
|
2217
|
+
r[4] = (tt2 ^ rotl(tt2, 9) ^ rotl(tt2, 17)) >>> 0;
|
|
2218
|
+
}
|
|
2219
|
+
for (let i = 0; i < 8; i++) reg[i] = (reg[i] ^ r[i]) >>> 0;
|
|
2220
|
+
};
|
|
2221
|
+
/** 按 SM3 规则填充消息(0x80 + 0 + 64 位比特长度) */
|
|
2222
|
+
const pad = (bytes) => {
|
|
2223
|
+
const padded = bytes.slice();
|
|
2224
|
+
const bitLength = bytes.length * 8;
|
|
2225
|
+
padded.push(128);
|
|
2226
|
+
while (padded.length % 64 !== 56) padded.push(0);
|
|
2227
|
+
const high = Math.floor(bitLength / 4294967296);
|
|
2228
|
+
for (let i = 0; i < 4; i++) padded.push(high >>> (3 - i) * 8 & 255);
|
|
2229
|
+
for (let i = 0; i < 4; i++) padded.push(bitLength >>> (3 - i) * 8 & 255);
|
|
2230
|
+
return padded;
|
|
2231
|
+
};
|
|
2232
|
+
/**
|
|
2233
|
+
* 计算 SM3 摘要
|
|
2234
|
+
* @param message 待摘要的字符串或字节数组
|
|
2235
|
+
* @returns 32 字节摘要
|
|
2236
|
+
*/
|
|
2237
|
+
const sm3 = (message) => {
|
|
2238
|
+
const bytes = typeof message === "string" ? strToBytes(message) : message;
|
|
2239
|
+
const reg = IV.slice();
|
|
2240
|
+
const padded = pad(bytes);
|
|
2241
|
+
for (let i = 0; i < padded.length; i += 64) compress(reg, padded.slice(i, i + 64));
|
|
2242
|
+
const digest = new Array(32);
|
|
2243
|
+
for (let i = 0; i < 8; i++) {
|
|
2244
|
+
digest[i * 4] = reg[i] >>> 24 & 255;
|
|
2245
|
+
digest[i * 4 + 1] = reg[i] >>> 16 & 255;
|
|
2246
|
+
digest[i * 4 + 2] = reg[i] >>> 8 & 255;
|
|
2247
|
+
digest[i * 4 + 3] = reg[i] & 255;
|
|
2248
|
+
}
|
|
2249
|
+
return digest;
|
|
2250
|
+
};
|
|
2251
|
+
/**
|
|
2252
|
+
* 连续两次 SM3(bdms 对 URL 与盐值的处理方式)
|
|
2253
|
+
* @param message 待摘要的字符串或字节数组
|
|
2254
|
+
* @returns 32 字节摘要
|
|
2255
|
+
*/
|
|
2256
|
+
const sm3Twice = (message) => sm3(sm3(message));
|
|
2257
|
+
/**
|
|
2258
|
+
* 十六进制摘要,仅用于测试与排查
|
|
2259
|
+
* @param message 待摘要的字符串或字节数组
|
|
2260
|
+
*/
|
|
2261
|
+
const sm3Hex = (message) => sm3(message).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/platform/douyin/passport/aBogus.ts
|
|
2264
|
+
/**
|
|
2265
|
+
* a_bogus 签名(bdms 1.0.1.19 形态,passport 登录接口使用)
|
|
2266
|
+
*
|
|
2267
|
+
* 抖音同时在线着多个 a_bogus 版本:`@ikenxuan/amagi` 里带的是 web 数据接口用的旧版
|
|
2268
|
+
* (盐值 `cus`、pageId 6241),而 login.douyin.com 的 passport SDK 用的是本文件实现的
|
|
2269
|
+
* 新版(盐值 `dhzx`、pageId 7571、sdkVersion 1.0.1.19-fix.01)。两者互不通用,
|
|
2270
|
+
* 所以这里单独实现一份,不复用 amagi 的签名。
|
|
2271
|
+
*
|
|
2272
|
+
* 算法为社区公开的逆向结论(见 PR 说明中的参考链接),此处按 kkk 的代码风格重写:
|
|
2273
|
+
* - SM3 为抖音变体(连续两次摘要),见 ./sm3
|
|
2274
|
+
* - RC4 为变体:S 盒递减初始化 + `j = (j * S[i] + j + K[i]) % 256`
|
|
2275
|
+
* - Base64 使用自定义字符表,UA 段用 s3、最终结果用 s4
|
|
2276
|
+
* - `random()` 保留了原实现里 `Math.random`(函数对象而非调用)参与位运算的行为,
|
|
2277
|
+
* 该表达式恒为 NaN → 位运算结果恒为 0,这里直接写成常量而不是照抄错误代码
|
|
2278
|
+
*/
|
|
2279
|
+
/** RC4 / S 盒长度 */
|
|
2280
|
+
const BOX_SIZE = 256;
|
|
2281
|
+
/** bdms 1.0.1.19 的盐值 */
|
|
2282
|
+
const SALT = "dhzx";
|
|
2283
|
+
/** bdms SDK 版本号,同时也是 passport 通用参数里的 p_bd */
|
|
2284
|
+
const BDMS_SDK_VERSION = "1.0.1.19-fix.01";
|
|
2285
|
+
/** 版本号基准时间戳(bdms 内部按 14 天为一档计数) */
|
|
2286
|
+
const VERSION_EPOCH = 17218368e5;
|
|
2287
|
+
/** 模块加载时刻,等价于浏览器里的「进入页面时间」 */
|
|
2288
|
+
const ENTER_PAGE_TS = Date.now();
|
|
2289
|
+
/** 自定义 Base64 字符表 */
|
|
2290
|
+
const BASE64_TABLES = {
|
|
2291
|
+
s0: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
|
2292
|
+
s1: "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
|
2293
|
+
s2: "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
|
|
2294
|
+
s3: "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe",
|
|
2295
|
+
s4: "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"
|
|
2296
|
+
};
|
|
2297
|
+
/**
|
|
2298
|
+
* 浏览器环境快照。服务器上没有真实窗口,这里给出一组常见的桌面分辨率组合;
|
|
2299
|
+
* 该值只影响指纹内容本身,不需要与任何真实设备对应。
|
|
2300
|
+
*/
|
|
2301
|
+
const BROWSER_ENV = {
|
|
2302
|
+
innerWidth: 2048,
|
|
2303
|
+
innerHeight: 960,
|
|
2304
|
+
outerWidth: 2554,
|
|
2305
|
+
outerHeight: 1386,
|
|
2306
|
+
availWidth: 2560,
|
|
2307
|
+
availHeight: 1392,
|
|
2308
|
+
sizeWidth: 2560,
|
|
2309
|
+
sizeHeight: 1440,
|
|
2310
|
+
platform: "Win32"
|
|
2311
|
+
};
|
|
2312
|
+
/** RC4 密钥:bdms 用 `[1 / 256, 1 % 256, 14 % 256]` 造出 "\x00\x01\x0e" */
|
|
2313
|
+
const rc4Key = String.fromCharCode(...[
|
|
2314
|
+
1 / BOX_SIZE,
|
|
2315
|
+
1 % BOX_SIZE,
|
|
2316
|
+
14 % BOX_SIZE
|
|
2317
|
+
]);
|
|
2318
|
+
/**
|
|
2319
|
+
* 变体 RC4
|
|
2320
|
+
* @param key 密钥
|
|
2321
|
+
* @param text 明文(按 charCode 处理)
|
|
2322
|
+
*/
|
|
2323
|
+
const rc4 = (key, text) => {
|
|
2324
|
+
const s = new Uint8Array(BOX_SIZE);
|
|
2325
|
+
const k = new Uint8Array(BOX_SIZE);
|
|
2326
|
+
for (let i = 0; i < BOX_SIZE; i++) {
|
|
2327
|
+
s[i] = 255 - i;
|
|
2328
|
+
k[i] = key.charCodeAt(i % key.length);
|
|
2329
|
+
}
|
|
2330
|
+
let j = 0;
|
|
2331
|
+
for (let i = 0; i < BOX_SIZE; i++) {
|
|
2332
|
+
j = (j * s[i] + j + k[i]) % BOX_SIZE;
|
|
2333
|
+
[s[i], s[j]] = [s[j], s[i]];
|
|
2334
|
+
}
|
|
2335
|
+
let x = 0;
|
|
2336
|
+
let y = 0;
|
|
2337
|
+
let cipher = "";
|
|
2338
|
+
for (let n = 0; n < text.length; n++) {
|
|
2339
|
+
x = (x + 1) % BOX_SIZE;
|
|
2340
|
+
y = (y + s[x]) % BOX_SIZE;
|
|
2341
|
+
[s[x], s[y]] = [s[y], s[x]];
|
|
2342
|
+
cipher += String.fromCharCode(text.charCodeAt(n) ^ s[(s[x] + s[y]) % BOX_SIZE]);
|
|
2343
|
+
}
|
|
2344
|
+
return cipher;
|
|
2345
|
+
};
|
|
2346
|
+
/**
|
|
2347
|
+
* 自定义字符表 Base64
|
|
2348
|
+
* @param input 明文(按 charCode 取低 8 位)
|
|
2349
|
+
* @param table 字符表名(s0 ~ s4)
|
|
2350
|
+
*/
|
|
2351
|
+
const base64 = (input, table) => {
|
|
2352
|
+
const alphabet = BASE64_TABLES[table];
|
|
2353
|
+
let output = "";
|
|
2354
|
+
let i = 0;
|
|
2355
|
+
while (i < input.length) {
|
|
2356
|
+
const c1 = input.charCodeAt(i++);
|
|
2357
|
+
const c2 = input.charCodeAt(i++);
|
|
2358
|
+
const c3 = input.charCodeAt(i++);
|
|
2359
|
+
const chunk = (c1 & 255) << 16 | (Number.isNaN(c2) ? 0 : (c2 & 255) << 8) | (Number.isNaN(c3) ? 0 : c3 & 255);
|
|
2360
|
+
output += alphabet.charAt(chunk >> 18 & 63);
|
|
2361
|
+
output += alphabet.charAt(chunk >> 12 & 63);
|
|
2362
|
+
output += Number.isNaN(c2) ? "=" : alphabet.charAt(chunk >> 6 & 63);
|
|
2363
|
+
output += Number.isNaN(c3) ? "=" : alphabet.charAt(chunk & 63);
|
|
2364
|
+
}
|
|
2365
|
+
return output;
|
|
2366
|
+
};
|
|
2367
|
+
/**
|
|
2368
|
+
* 生成 4 字节随机混淆段
|
|
2369
|
+
* @param seed 两字节的基准值
|
|
2370
|
+
* @param flag 0 = 完全随机;1 = 高位固定为 0;2 = 低位受限、高位固定为 178
|
|
2371
|
+
*/
|
|
2372
|
+
const mix = (seed, flag) => {
|
|
2373
|
+
const r = Math.random() * 65535 | 0;
|
|
2374
|
+
let low = r & 255;
|
|
2375
|
+
let high = r >> 8 & 255;
|
|
2376
|
+
if (flag === 1) high = 0;
|
|
2377
|
+
if (flag === 2) {
|
|
2378
|
+
low = Math.random() * 240 >> 0;
|
|
2379
|
+
if (low > 109) low += low % 2 + 1;
|
|
2380
|
+
high = 178;
|
|
2381
|
+
}
|
|
2382
|
+
return [
|
|
2383
|
+
low & 170 | seed[0] & 85,
|
|
2384
|
+
low & 85 | seed[0] & 170,
|
|
2385
|
+
high & 170 | seed[1] & 85,
|
|
2386
|
+
high & 85 | seed[1] & 170
|
|
2387
|
+
];
|
|
2388
|
+
};
|
|
2389
|
+
/** SDK 版本号拆成数字数组,非纯数字段(如 `19-fix`)取 0 */
|
|
2390
|
+
const versionSegments = (version) => version.split(".").map((segment) => ~~Number(segment));
|
|
2391
|
+
/** 每 3 字节扩成 4 字节,掺入随机位 */
|
|
2392
|
+
const spread = (bytes) => {
|
|
2393
|
+
const masks = [
|
|
2394
|
+
145,
|
|
2395
|
+
110,
|
|
2396
|
+
66,
|
|
2397
|
+
189,
|
|
2398
|
+
44,
|
|
2399
|
+
211
|
|
2400
|
+
];
|
|
2401
|
+
const out = [];
|
|
2402
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
2403
|
+
if (i + 2 >= bytes.length) {
|
|
2404
|
+
out.push(bytes[i]);
|
|
2405
|
+
if (bytes[i + 1] !== void 0) out.push(bytes[i + 1]);
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2408
|
+
const noise = Math.random() * 1e3 & 255;
|
|
2409
|
+
out.push(noise & masks[0] | bytes[i] & masks[1], noise & masks[2] | bytes[i + 1] & masks[3], noise & masks[4] | bytes[i + 2] & masks[5], bytes[i] & masks[0] | bytes[i + 1] & masks[2] | bytes[i + 2] & masks[4]);
|
|
2410
|
+
}
|
|
2411
|
+
return out;
|
|
2412
|
+
};
|
|
2413
|
+
/**
|
|
2414
|
+
* 生成 a_bogus
|
|
2415
|
+
* @param query 除 a_bogus 之外的完整查询串(未加 `?`,保持实际发送顺序)
|
|
2416
|
+
* @param userAgent 与请求头一致的 UA
|
|
2417
|
+
* @returns a_bogus 参数值(未做 URL 编码)
|
|
2418
|
+
*/
|
|
2419
|
+
const aBogus = (query, userAgent) => {
|
|
2420
|
+
const salted = query.endsWith(SALT) ? query : query + SALT;
|
|
2421
|
+
const queryDigest = sm3Twice(salted);
|
|
2422
|
+
const saltDigest = sm3Twice(SALT);
|
|
2423
|
+
const uaEncoded = base64(rc4(rc4Key, userAgent), "s3");
|
|
2424
|
+
const uaDigest = sm3(uaEncoded);
|
|
2425
|
+
const now = Date.now();
|
|
2426
|
+
const ink = now - 1;
|
|
2427
|
+
/** 指纹字节表,下标沿用 bdms 内部编号,便于与逆向资料对照 */
|
|
2428
|
+
const b = {};
|
|
2429
|
+
b[24] = 41;
|
|
2430
|
+
b[26] = (now - VERSION_EPOCH) / 1e3 / 60 / 60 / 24 / 14 >> 0;
|
|
2431
|
+
b[27] = 6;
|
|
2432
|
+
b[28] = now - ENTER_PAGE_TS + 3 & 255;
|
|
2433
|
+
b[29] = now & 255;
|
|
2434
|
+
b[30] = now >> 8 & 255;
|
|
2435
|
+
b[31] = now >> 16 & 255;
|
|
2436
|
+
b[32] = now >> 24 & 255;
|
|
2437
|
+
b[33] = now / 2 ** 32 & 255;
|
|
2438
|
+
b[34] = now / 2 ** 40 & 255;
|
|
2439
|
+
b[35] = 1;
|
|
2440
|
+
b[36] = 0;
|
|
2441
|
+
b[38] = 129;
|
|
2442
|
+
b[39] = 0;
|
|
2443
|
+
b[40] = 0;
|
|
2444
|
+
b[41] = 0;
|
|
2445
|
+
b[42] = 0;
|
|
2446
|
+
b[43] = 0;
|
|
2447
|
+
b[44] = 14;
|
|
2448
|
+
b[45] = 0;
|
|
2449
|
+
b[46] = 0;
|
|
2450
|
+
b[47] = 0;
|
|
2451
|
+
b[48] = queryDigest[9];
|
|
2452
|
+
b[49] = queryDigest[18];
|
|
2453
|
+
b[51] = queryDigest[3];
|
|
2454
|
+
b[52] = saltDigest[10];
|
|
2455
|
+
b[53] = saltDigest[19];
|
|
2456
|
+
b[55] = saltDigest[4];
|
|
2457
|
+
b[56] = uaDigest[11];
|
|
2458
|
+
b[57] = uaDigest[21];
|
|
2459
|
+
b[59] = uaDigest[5];
|
|
2460
|
+
b[60] = ink & 255;
|
|
2461
|
+
b[61] = ink >> 8 & 255;
|
|
2462
|
+
b[62] = ink >> 16 & 255;
|
|
2463
|
+
b[63] = ink >> 24 & 255;
|
|
2464
|
+
b[64] = ink / 2 ** 32 & 255;
|
|
2465
|
+
b[65] = ink / 2 ** 40 & 255;
|
|
2466
|
+
b[66] = 3;
|
|
2467
|
+
b[67] = 147;
|
|
2468
|
+
b[68] = 29;
|
|
2469
|
+
b[69] = 0;
|
|
2470
|
+
b[70] = 0;
|
|
2471
|
+
b[71] = 239;
|
|
2472
|
+
b[72] = 24;
|
|
2473
|
+
b[73] = 0;
|
|
2474
|
+
b[74] = 0;
|
|
2475
|
+
const envSnapshot = Object.values(BROWSER_ENV).join("|");
|
|
2476
|
+
const envBytes = Array.from(envSnapshot, (char) => char.charCodeAt(0));
|
|
2477
|
+
b[79] = envBytes.length & 255;
|
|
2478
|
+
b[80] = envBytes.length >> 8 & 255;
|
|
2479
|
+
const tail = `${now + 3 & 255},`;
|
|
2480
|
+
const tailBytes = Array.from(tail, (char) => char.charCodeAt(0));
|
|
2481
|
+
b[84] = tailBytes.length & 255;
|
|
2482
|
+
b[85] = tailBytes.length >> 8 & 255;
|
|
2483
|
+
const version = versionSegments(BDMS_SDK_VERSION);
|
|
2484
|
+
const noise = mix([version[0], version[1]], 0).concat(mix([version[0], version[1]], 2));
|
|
2485
|
+
const checksum = [
|
|
2486
|
+
24,
|
|
2487
|
+
26,
|
|
2488
|
+
27,
|
|
2489
|
+
28,
|
|
2490
|
+
29,
|
|
2491
|
+
30,
|
|
2492
|
+
31,
|
|
2493
|
+
32,
|
|
2494
|
+
33,
|
|
2495
|
+
34,
|
|
2496
|
+
35,
|
|
2497
|
+
36,
|
|
2498
|
+
38,
|
|
2499
|
+
39,
|
|
2500
|
+
40,
|
|
2501
|
+
41,
|
|
2502
|
+
42,
|
|
2503
|
+
43,
|
|
2504
|
+
44,
|
|
2505
|
+
45,
|
|
2506
|
+
46,
|
|
2507
|
+
47,
|
|
2508
|
+
48,
|
|
2509
|
+
49,
|
|
2510
|
+
51,
|
|
2511
|
+
52,
|
|
2512
|
+
53,
|
|
2513
|
+
55,
|
|
2514
|
+
56,
|
|
2515
|
+
57,
|
|
2516
|
+
59,
|
|
2517
|
+
60,
|
|
2518
|
+
61,
|
|
2519
|
+
62,
|
|
2520
|
+
63,
|
|
2521
|
+
64,
|
|
2522
|
+
65,
|
|
2523
|
+
66,
|
|
2524
|
+
67,
|
|
2525
|
+
68,
|
|
2526
|
+
69,
|
|
2527
|
+
70,
|
|
2528
|
+
71,
|
|
2529
|
+
72,
|
|
2530
|
+
73,
|
|
2531
|
+
74,
|
|
2532
|
+
79,
|
|
2533
|
+
80,
|
|
2534
|
+
84,
|
|
2535
|
+
85
|
|
2536
|
+
].reduce((acc, key) => acc ^ b[key], noise.reduce((acc, value) => acc ^ value, 0));
|
|
2537
|
+
/** 打乱后的字节顺序,与 bdms 内部的 tlist 一致 */
|
|
2538
|
+
const shuffled = [
|
|
2539
|
+
34,
|
|
2540
|
+
44,
|
|
2541
|
+
56,
|
|
2542
|
+
61,
|
|
2543
|
+
73,
|
|
2544
|
+
29,
|
|
2545
|
+
70,
|
|
2546
|
+
45,
|
|
2547
|
+
35,
|
|
2548
|
+
49,
|
|
2549
|
+
38,
|
|
2550
|
+
66,
|
|
2551
|
+
51,
|
|
2552
|
+
68,
|
|
2553
|
+
28,
|
|
2554
|
+
48,
|
|
2555
|
+
64,
|
|
2556
|
+
47,
|
|
2557
|
+
30,
|
|
2558
|
+
71,
|
|
2559
|
+
26,
|
|
2560
|
+
55,
|
|
2561
|
+
31,
|
|
2562
|
+
69,
|
|
2563
|
+
59,
|
|
2564
|
+
40,
|
|
2565
|
+
62,
|
|
2566
|
+
63,
|
|
2567
|
+
27,
|
|
2568
|
+
72,
|
|
2569
|
+
41,
|
|
2570
|
+
74,
|
|
2571
|
+
57,
|
|
2572
|
+
52,
|
|
2573
|
+
42,
|
|
2574
|
+
39,
|
|
2575
|
+
33,
|
|
2576
|
+
67,
|
|
2577
|
+
53,
|
|
2578
|
+
43,
|
|
2579
|
+
65,
|
|
2580
|
+
46,
|
|
2581
|
+
36,
|
|
2582
|
+
24,
|
|
2583
|
+
60,
|
|
2584
|
+
32,
|
|
2585
|
+
79,
|
|
2586
|
+
80,
|
|
2587
|
+
84,
|
|
2588
|
+
85
|
|
2589
|
+
].map((key) => b[key]);
|
|
2590
|
+
const payload = spread(shuffled.concat(envBytes, tailBytes, [checksum]));
|
|
2591
|
+
const prefix = String.fromCharCode(...mix([3, 82], 1));
|
|
2592
|
+
const encrypted = rc4(String.fromCharCode(211), String.fromCharCode(...noise.concat(payload)));
|
|
2593
|
+
return base64(prefix + encrypted, "s4");
|
|
2594
|
+
};
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region src/platform/douyin/passport/cookieJar.ts
|
|
2597
|
+
/**
|
|
2598
|
+
* 登录流程用的轻量 CookieJar
|
|
2599
|
+
*
|
|
2600
|
+
* 登录过程会跨 `www.douyin.com` / `login.douyin.com` / `ttwid.bytedance.com` 三个域,
|
|
2601
|
+
* 且同一个 cookie 名会被多次下发(例如 `ttwid` 在换取可信指纹后会被替换、
|
|
2602
|
+
* `sessionid` 在二次验证通过后会被升级)。这里只做一件事:**按下发顺序覆盖同名 cookie**,
|
|
2603
|
+
* 保证最终拿到的永远是最后一次下发的值,同时正确处理服务端的删除指令。
|
|
2604
|
+
*
|
|
2605
|
+
* 不做域/路径隔离:整个登录流程都在抖音自己的域下,隔离反而会漏掉跨子域下发的凭证。
|
|
2606
|
+
*
|
|
2607
|
+
* 另外承载一小部分**本地会话状态**(见 `INTERNAL_PREFIX`):passport 的几个接口对外是
|
|
2608
|
+
* 无状态的,会话全靠 cookie 串在调用之间传递,而 bd-ticket-guard 需要在多次调用之间
|
|
2609
|
+
* 记住自己生成的密钥与服务端签发的票据。这些条目以 `__amagi_` 开头,
|
|
2610
|
+
* `toString()` 不会把它们放进 Cookie 请求头,只有 `serialize()` 才会带上。
|
|
2611
|
+
*/
|
|
2612
|
+
/** 本地会话状态的 cookie 名前缀,这些条目永远不会发给服务端 */
|
|
2613
|
+
const INTERNAL_PREFIX = "__amagi_";
|
|
2614
|
+
/** 是否为本地会话状态条目 */
|
|
2615
|
+
const isInternal = (name) => name.startsWith(INTERNAL_PREFIX);
|
|
2616
|
+
/** 判断一条 Set-Cookie 是否表示「删除该 cookie」 */
|
|
2617
|
+
const isDeletion = (value, attributes) => {
|
|
2618
|
+
if (value === "") return true;
|
|
2619
|
+
for (const attribute of attributes) {
|
|
2620
|
+
const [rawName, ...rest] = attribute.split("=");
|
|
2621
|
+
const name = rawName.trim().toLowerCase();
|
|
2622
|
+
const rawValue = rest.join("=").trim();
|
|
2623
|
+
if (name === "max-age") {
|
|
2624
|
+
const maxAge = Number(rawValue);
|
|
2625
|
+
if (Number.isFinite(maxAge) && maxAge <= 0) return true;
|
|
2626
|
+
}
|
|
2627
|
+
if (name === "expires") {
|
|
2628
|
+
const expires = Date.parse(rawValue);
|
|
2629
|
+
if (Number.isFinite(expires) && expires <= Date.now()) return true;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
return false;
|
|
2633
|
+
};
|
|
2634
|
+
var CookieJar = class {
|
|
2635
|
+
/** Map 保留插入顺序,重复 set 只更新值、不改变位置 */
|
|
2636
|
+
cookies = /* @__PURE__ */ new Map();
|
|
2637
|
+
/**
|
|
2638
|
+
* @param initial 初始 cookie 串,形如 `a=1; b=2`
|
|
2639
|
+
*/
|
|
2640
|
+
constructor(initial) {
|
|
2641
|
+
if (initial) this.merge(initial);
|
|
2642
|
+
}
|
|
2643
|
+
/** 当前持有的 cookie 数量 */
|
|
2644
|
+
get size() {
|
|
2645
|
+
return this.cookies.size;
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* 写入一条 cookie
|
|
2649
|
+
* @param name cookie 名
|
|
2650
|
+
* @param value cookie 值
|
|
2651
|
+
*/
|
|
2652
|
+
set(name, value) {
|
|
2653
|
+
this.cookies.set(name, value);
|
|
2654
|
+
return this;
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* 读取一条 cookie
|
|
2658
|
+
* @param name cookie 名
|
|
2659
|
+
*/
|
|
2660
|
+
get(name) {
|
|
2661
|
+
return this.cookies.get(name);
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* 是否持有某条 cookie
|
|
2665
|
+
* @param name cookie 名
|
|
2666
|
+
*/
|
|
2667
|
+
has(name) {
|
|
2668
|
+
return this.cookies.has(name);
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* 合并一段 `name=value; name=value` 形式的 cookie 串
|
|
2672
|
+
* @param cookieString cookie 串,空值直接忽略
|
|
2673
|
+
*/
|
|
2674
|
+
merge(cookieString) {
|
|
2675
|
+
if (!cookieString) return this;
|
|
2676
|
+
for (const pair of cookieString.split(";")) {
|
|
2677
|
+
const index = pair.indexOf("=");
|
|
2678
|
+
if (index <= 0) continue;
|
|
2679
|
+
const name = pair.slice(0, index).trim();
|
|
2680
|
+
const value = pair.slice(index + 1).trim();
|
|
2681
|
+
if (name && value) this.cookies.set(name, value);
|
|
2682
|
+
}
|
|
2683
|
+
return this;
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* 应用响应的 Set-Cookie 头
|
|
2687
|
+
* @param setCookies 单条或多条 Set-Cookie 原始值
|
|
2688
|
+
*/
|
|
2689
|
+
applySetCookie(setCookies) {
|
|
2690
|
+
if (!setCookies) return this;
|
|
2691
|
+
for (const line of Array.isArray(setCookies) ? setCookies : [setCookies]) {
|
|
2692
|
+
const [pair, ...attributes] = line.split(";");
|
|
2693
|
+
const index = pair.indexOf("=");
|
|
2694
|
+
if (index <= 0) continue;
|
|
2695
|
+
const name = pair.slice(0, index).trim();
|
|
2696
|
+
const value = pair.slice(index + 1).trim();
|
|
2697
|
+
if (!name) continue;
|
|
2698
|
+
if (isDeletion(value, attributes)) {
|
|
2699
|
+
this.cookies.delete(name);
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
this.cookies.set(name, value);
|
|
2703
|
+
}
|
|
2704
|
+
return this;
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* 是否已经拿到登录态凭证(`ttwid` 是匿名设备指纹,不算登录)
|
|
2708
|
+
*/
|
|
2709
|
+
isLoggedIn() {
|
|
2710
|
+
return this.has("sessionid") || this.has("sessionid_ss") || this.has("sid_guard");
|
|
2711
|
+
}
|
|
2712
|
+
/** 序列化为可直接放进 Cookie 请求头的字符串,不含本地会话状态 */
|
|
2713
|
+
toString() {
|
|
2714
|
+
return [...this.cookies].filter(([name]) => !isInternal(name)).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* 序列化为在两次调用之间传递的会话串,包含本地会话状态
|
|
2718
|
+
*
|
|
2719
|
+
* 登录流程内部用这个;最终落库的登录凭证用 `toString()`,避免把本地密钥写进配置。
|
|
2720
|
+
*/
|
|
2721
|
+
serialize() {
|
|
2722
|
+
return [...this.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
2723
|
+
}
|
|
2724
|
+
/** 导出为普通对象,便于断言与日志 */
|
|
2725
|
+
toJSON() {
|
|
2726
|
+
return Object.fromEntries(this.cookies);
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
//#endregion
|
|
2730
|
+
//#region src/platform/douyin/passport/params.ts
|
|
2731
|
+
/**
|
|
2732
|
+
* passport 登录 SDK 的参数与签名构造
|
|
2733
|
+
*
|
|
2734
|
+
* login.douyin.com 的接口一共要过四道签名:
|
|
2735
|
+
* - `p_no`:SDK 版本相关参数排序后取 sha256
|
|
2736
|
+
* - `sign`:`排序后的前 10 个 query 参数 & 排序后的 body 参数 & app_key` 取 sha256
|
|
2737
|
+
* - `qs`:参与 sign 的参数名列表逐字节异或 5 后转十六进制
|
|
2738
|
+
* - `x-tt-passport-aid-sign` 请求头:以 appKey 为消息、当日 UTC 正午时间戳为密钥做 HMAC 派生
|
|
2739
|
+
*
|
|
2740
|
+
* 这些常量都是 SDK 自身的版本号与固定 app_key,属于协议的一部分,不含任何设备/账号信息。
|
|
2741
|
+
*/
|
|
2742
|
+
/** passport 登录 SDK 的 app_key */
|
|
2743
|
+
const APP_KEY = "163e7ce78d58971a41f5b969996d85c2";
|
|
2744
|
+
/** 抖音 web 的 aid */
|
|
2745
|
+
const PASSPORT_AID = "6383";
|
|
2746
|
+
/** 登录接口域名 */
|
|
2747
|
+
const LOGIN_HOST = "login.douyin.com";
|
|
2748
|
+
/** 抖音主站域名 */
|
|
2749
|
+
const WEB_HOST = "www.douyin.com";
|
|
2750
|
+
/** 登录 SDK(normal 形态)版本号 */
|
|
2751
|
+
const JSSDK_VERSION = "3.1.3";
|
|
2752
|
+
/** 验证页 SDK(lite 形态)版本号 */
|
|
2753
|
+
const LITE_JSSDK_VERSION = "5.1.2";
|
|
2754
|
+
/** 验证页使用的 authn SDK 版本号 */
|
|
2755
|
+
const LITE_AUTHN_VERSION = "1.0.0.420-web";
|
|
2756
|
+
/** 各子 SDK 版本号,参与 p_no 计算 */
|
|
2757
|
+
const SDK_VERSIONS = {
|
|
2758
|
+
pVer: "1.1.3",
|
|
2759
|
+
pZt: "3.3.14",
|
|
2760
|
+
pUi: "2.1.9-alpha.6",
|
|
2761
|
+
pCa: "4.0.17",
|
|
2762
|
+
pCaReal: "1.0.0.874"
|
|
2763
|
+
};
|
|
2764
|
+
/** 与 aBogus 中 BROWSER_ENV 对应的窗口尺寸,用于生成 account_sdk_source_info */
|
|
2765
|
+
const ENV_VIEWPORT = {
|
|
2766
|
+
innerWidth: 2048,
|
|
2767
|
+
innerHeight: 960,
|
|
2768
|
+
outerWidth: 2554,
|
|
2769
|
+
outerHeight: 1386
|
|
2770
|
+
};
|
|
2771
|
+
const sha256Hex = (input) => crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
2772
|
+
const hmacSha256 = (key, message) => crypto.createHmac("sha256", Buffer.from(key)).update(Buffer.from(message)).digest();
|
|
2773
|
+
const hexToBytes = (hex) => Uint8Array.from(hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []);
|
|
2774
|
+
/**
|
|
2775
|
+
* 逐字节异或 5 后转十六进制,SDK 用它编码参数名列表、验证码与密码
|
|
2776
|
+
* @param input 明文
|
|
2777
|
+
*/
|
|
2778
|
+
const xor5Hex = (input) => Array.from(Buffer.from(input, "utf8"), (byte) => (byte ^ 5).toString(16).padStart(2, "0")).join("");
|
|
2779
|
+
/** 随机十六进制串,用于 biz_trace_id 一类的追踪 ID */
|
|
2780
|
+
const randomHex = (length) => crypto.randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
2781
|
+
/** 当日 UTC 12:00 的秒级时间戳,aid-sign 以此为密钥基准 */
|
|
2782
|
+
const utcNoonTimestamp = (now = /* @__PURE__ */ new Date()) => Math.floor(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 12, 0, 0, 0) / 1e3);
|
|
2783
|
+
/**
|
|
2784
|
+
* 按 SDK 规则序列化参数:键名排序后拼成 `k=v&k=v`
|
|
2785
|
+
* @param params 参数对象
|
|
2786
|
+
* @param limit 大于等于 0 时只取排序后的前 limit 个键
|
|
2787
|
+
*/
|
|
2788
|
+
const serializeSorted = (params, limit = -1) => {
|
|
2789
|
+
const keys = Object.keys(params).sort();
|
|
2790
|
+
if (limit >= 0) keys.splice(limit);
|
|
2791
|
+
return {
|
|
2792
|
+
text: keys.map((key) => `${key}=${typeof params[key] === "object" ? JSON.stringify(params[key]) : params[key]}`).join("&"),
|
|
2793
|
+
keys
|
|
2794
|
+
};
|
|
2795
|
+
};
|
|
2796
|
+
/**
|
|
2797
|
+
* 计算 sign 与 qs
|
|
2798
|
+
* @param params query 参数(仅排序后的前 10 个参与签名)
|
|
2799
|
+
* @param data body 参数,GET 请求传空对象
|
|
2800
|
+
*/
|
|
2801
|
+
const makeSignAndQs = (params, data = {}) => {
|
|
2802
|
+
const { text: paramsText, keys } = serializeSorted(params, 10);
|
|
2803
|
+
const { text: dataText } = serializeSorted(data);
|
|
2804
|
+
return {
|
|
2805
|
+
sign: sha256Hex(`${paramsText}&${dataText}&app_key=${APP_KEY}`),
|
|
2806
|
+
qs: xor5Hex(keys.join(","))
|
|
2807
|
+
};
|
|
2808
|
+
};
|
|
2809
|
+
/**
|
|
2810
|
+
* 计算 p_no
|
|
2811
|
+
* @param pTs 毫秒时间戳,与 query 里的 p_ts 保持一致
|
|
2812
|
+
*/
|
|
2813
|
+
const makePNo = (pTs) => {
|
|
2814
|
+
const parts = {
|
|
2815
|
+
passport_jssdk_version: JSSDK_VERSION,
|
|
2816
|
+
p_bd: BDMS_SDK_VERSION,
|
|
2817
|
+
p_ca: SDK_VERSIONS.pCa,
|
|
2818
|
+
p_ts: pTs,
|
|
2819
|
+
p_ver: SDK_VERSIONS.pVer,
|
|
2820
|
+
p_zt: SDK_VERSIONS.pZt
|
|
2821
|
+
};
|
|
2822
|
+
return sha256Hex(Object.keys(parts).sort().map((key) => `${key}=${parts[key]}`).join("&"));
|
|
2823
|
+
};
|
|
2824
|
+
/**
|
|
2825
|
+
* HKDF 风格的密钥派生,aid-sign 内部使用
|
|
2826
|
+
* @param keyHex 初始密钥(十六进制)
|
|
2827
|
+
* @param length 输出长度
|
|
2828
|
+
*/
|
|
2829
|
+
const deriveKey = (keyHex, length) => {
|
|
2830
|
+
const output = [];
|
|
2831
|
+
let previous = "";
|
|
2832
|
+
let counter = 0;
|
|
2833
|
+
while (output.length < length) {
|
|
2834
|
+
counter++;
|
|
2835
|
+
const message = Uint8Array.from([...hexToBytes(previous), counter]);
|
|
2836
|
+
previous = hmacSha256(hexToBytes(keyHex), message).toString("hex");
|
|
2837
|
+
output.push(...hexToBytes(previous));
|
|
2838
|
+
}
|
|
2839
|
+
return Uint8Array.from(output.slice(0, length));
|
|
2840
|
+
};
|
|
2841
|
+
/**
|
|
2842
|
+
* 计算 `x-tt-passport-aid-sign` 请求头
|
|
2843
|
+
* @param urlPath 接口路径,如 `/passport/web/get_qrcode/`
|
|
2844
|
+
* @param timestamp 当日 UTC 正午时间戳(秒),默认取当前
|
|
2845
|
+
*/
|
|
2846
|
+
const makeAidSign = (urlPath, timestamp = utcNoonTimestamp()) => {
|
|
2847
|
+
const encoder = new TextEncoder();
|
|
2848
|
+
const ts = String(timestamp);
|
|
2849
|
+
const seed = hmacSha256(encoder.encode(ts), encoder.encode(APP_KEY)).toString("hex");
|
|
2850
|
+
const key = deriveKey(seed, 32);
|
|
2851
|
+
return hmacSha256(key, encoder.encode(`aid=${PASSPORT_AID}&path=${urlPath}&ts=${ts}`)).toString("hex");
|
|
2852
|
+
};
|
|
2853
|
+
/**
|
|
2854
|
+
* 生成 account_sdk_source_info:SDK 采集的浏览器环境快照,异或 5 后转十六进制。
|
|
2855
|
+
*
|
|
2856
|
+
* 上游参考实现内联的是作者本机抓包值(含显卡型号、堆内存占用、带 query 的个人主页 URL),
|
|
2857
|
+
* 不适合进仓库,这里换成一份等价形态的通用快照。
|
|
2858
|
+
*
|
|
2859
|
+
* 实测服务端在 `get_qrcode` 阶段不校验该字段内容(删掉、置空、填垃圾值都同样返回
|
|
2860
|
+
* `error_code: 0`),保留它只是为了与 SDK 的真实请求形态一致。
|
|
2861
|
+
*/
|
|
2862
|
+
const makeAccountSdkSourceInfo = () => xor5Hex(JSON.stringify({
|
|
2863
|
+
hardwareConcurrency: 8,
|
|
2864
|
+
webdriver: false,
|
|
2865
|
+
chromedriver: false,
|
|
2866
|
+
shelldriver: false,
|
|
2867
|
+
plugins: 5,
|
|
2868
|
+
innerHeight: ENV_VIEWPORT.innerHeight,
|
|
2869
|
+
innerWidth: ENV_VIEWPORT.innerWidth,
|
|
2870
|
+
outerHeight: ENV_VIEWPORT.outerHeight,
|
|
2871
|
+
outerWidth: ENV_VIEWPORT.outerWidth,
|
|
2872
|
+
webgl: {
|
|
2873
|
+
vendor: "Google Inc. (Intel)",
|
|
2874
|
+
renderer: "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)"
|
|
2875
|
+
},
|
|
2876
|
+
performance: {
|
|
2877
|
+
timeOrigin: Date.now(),
|
|
2878
|
+
navigationTiming: {
|
|
2879
|
+
entryType: "navigation",
|
|
2880
|
+
initiatorType: "navigation",
|
|
2881
|
+
name: `https://${WEB_HOST}/`,
|
|
2882
|
+
renderBlockingStatus: "non-blocking"
|
|
2883
|
+
}
|
|
2884
|
+
},
|
|
2885
|
+
browser: {
|
|
2886
|
+
bit_protocol: "false",
|
|
2887
|
+
bit_helper: false
|
|
2888
|
+
}
|
|
2889
|
+
}));
|
|
2890
|
+
/**
|
|
2891
|
+
* 构造 passport 登录 SDK 的通用 query 参数
|
|
2892
|
+
*
|
|
2893
|
+
* 参数顺序即实际发送顺序:SDK 各拦截器依次注入,`request_host` 在这里先编码一次,
|
|
2894
|
+
* 拼 URL 时会再编码一次,所以线上抓包看到的是双重编码。
|
|
2895
|
+
* @param extra 业务参数(GET 时并入 query)
|
|
2896
|
+
*/
|
|
2897
|
+
const makeCommonParams = (extra = {}) => {
|
|
2898
|
+
const pTs = String(Date.now());
|
|
2899
|
+
return {
|
|
2900
|
+
passport_jssdk_version: JSSDK_VERSION,
|
|
2901
|
+
passport_jssdk_type: "normal",
|
|
2902
|
+
is_from_ttaccountsdk: "1",
|
|
2903
|
+
aid: PASSPORT_AID,
|
|
2904
|
+
language: "zh",
|
|
2905
|
+
account_app_language: "zh-CN",
|
|
2906
|
+
ts: String(utcNoonTimestamp()),
|
|
2907
|
+
...Object.fromEntries(Object.entries(extra).map(([key, value]) => [key, String(value)])),
|
|
2908
|
+
is_from_iesaccountsaas: "1",
|
|
2909
|
+
p_ui: SDK_VERSIONS.pUi,
|
|
2910
|
+
p_ca: SDK_VERSIONS.pCa,
|
|
2911
|
+
p_ca_real: SDK_VERSIONS.pCaReal,
|
|
2912
|
+
account_sdk_source: "web",
|
|
2913
|
+
account_sdk_source_info: makeAccountSdkSourceInfo(),
|
|
2914
|
+
p_js_v: JSSDK_VERSION,
|
|
2915
|
+
p_js_t: "pro",
|
|
2916
|
+
p_zt: SDK_VERSIONS.pZt,
|
|
2917
|
+
p_ver: SDK_VERSIONS.pVer,
|
|
2918
|
+
p_ver_real: "0",
|
|
2919
|
+
request_host: encodeURIComponent(`https://${WEB_HOST}`),
|
|
2920
|
+
p_bd: BDMS_SDK_VERSION,
|
|
2921
|
+
p_ts: pTs,
|
|
2922
|
+
p_no: makePNo(pTs),
|
|
2923
|
+
biz_trace_id: randomHex(8),
|
|
2924
|
+
device_platform: "web_app"
|
|
2925
|
+
};
|
|
2926
|
+
};
|
|
2927
|
+
/**
|
|
2928
|
+
* 构造验证页 SDK(lite 形态)的固定 query 参数
|
|
2929
|
+
* @param bizTraceId 业务追踪 ID
|
|
2930
|
+
*/
|
|
2931
|
+
const makeLiteParams = (bizTraceId) => ({
|
|
2932
|
+
passport_jssdk_version: LITE_JSSDK_VERSION,
|
|
2933
|
+
passport_jssdk_type: "lite",
|
|
2934
|
+
is_from_ttaccountsdk: "1",
|
|
2935
|
+
aid: PASSPORT_AID,
|
|
2936
|
+
language: "zh",
|
|
2937
|
+
account_app_language: "zh-CN",
|
|
2938
|
+
new_authn_sdk_version: LITE_AUTHN_VERSION,
|
|
2939
|
+
biz_trace_id: bizTraceId
|
|
2940
|
+
});
|
|
2941
|
+
/**
|
|
2942
|
+
* 按插入顺序序列化为查询串(不排序,`request_host` 因此产生二次编码)
|
|
2943
|
+
* @param params 参数对象
|
|
2944
|
+
*/
|
|
2945
|
+
const serializeQuery = (params) => Object.entries(params).map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`).join("&");
|
|
2946
|
+
//#endregion
|
|
2947
|
+
//#region src/platform/douyin/passport/ticketGuard.ts
|
|
2948
|
+
/**
|
|
2949
|
+
* bd-ticket-guard 设备票据
|
|
2950
|
+
*
|
|
2951
|
+
* 抖音的设备真实性风控。常见的说法是必须从浏览器 localStorage 里导出一对密钥才能用,
|
|
2952
|
+
* 但那只适用于「已登录之后」的接口 —— **登录流程本身就是票据的签发入口**:
|
|
2953
|
+
*
|
|
2954
|
+
* 1. 本地生成一对 P-256 密钥
|
|
2955
|
+
* 2. 把公钥写进 `bd_ticket_guard_client_data` cookie,在取二维码**之前**交给服务端
|
|
2956
|
+
* 3. 服务端在后续响应的 `bd-ticket-guard-server-data` 头里签发 `{ticket, ts_sign, client_cert}`
|
|
2957
|
+
* 4. 之后每个请求用 `ECDH(自己的私钥, client_cert 里的服务端公钥)` 派生的密钥
|
|
2958
|
+
* 对 `ticket=…&path=…×tamp=…` 做 HMAC,放进 `bd-ticket-guard-client-data` 头
|
|
2959
|
+
*
|
|
2960
|
+
* 全程不需要浏览器,票据是服务端真实签发给我们自己这把密钥的,没有任何伪造数据。
|
|
2961
|
+
* 密钥与票据以 `__amagi_` 前缀存在 CookieJar 里,只在调用之间传递,不会发给服务端。
|
|
2962
|
+
*/
|
|
2963
|
+
/** 交给服务端的公钥所在的 cookie */
|
|
2964
|
+
const CLIENT_DATA_COOKIE = "bd_ticket_guard_client_data";
|
|
2965
|
+
/** 声明 web 域版本的 cookie */
|
|
2966
|
+
const CLIENT_WEB_DOMAIN_COOKIE = "bd_ticket_guard_client_web_domain";
|
|
2967
|
+
/** 服务端也可能把签发结果放在这条 cookie 里 */
|
|
2968
|
+
const SERVER_DATA_COOKIE = "bd_ticket_guard_server_data";
|
|
2969
|
+
/** 服务端签发结果所在的响应头 */
|
|
2970
|
+
const SERVER_DATA_HEADER = "bd-ticket-guard-server-data";
|
|
2971
|
+
/** 本地会话状态:PKCS#8 私钥(base64) */
|
|
2972
|
+
const KEY_ENTRY = `${INTERNAL_PREFIX}tg_key`;
|
|
2973
|
+
/** 本地会话状态:服务端签发的票据 */
|
|
2974
|
+
const TICKET_ENTRY = `${INTERNAL_PREFIX}tg_ticket`;
|
|
2975
|
+
/** 本地会话状态:票据的时间戳签名 */
|
|
2976
|
+
const TS_SIGN_ENTRY = `${INTERNAL_PREFIX}tg_ts_sign`;
|
|
2977
|
+
/** 本地会话状态:ECDH 派生出的 HMAC 密钥(hex) */
|
|
2978
|
+
const ECDH_ENTRY = `${INTERNAL_PREFIX}tg_ecdh`;
|
|
2979
|
+
/** SDK 声明的 ticket-guard 版本 */
|
|
2980
|
+
const GUARD_VERSION = "2";
|
|
2981
|
+
/** SDK 声明的迭代版本 */
|
|
2982
|
+
const ITERATION_VERSION = "1";
|
|
2983
|
+
/** P-256 公钥的 SubjectPublicKeyInfo 前缀,其后紧跟 65 字节未压缩点 */
|
|
2984
|
+
const P256_SPKI_PREFIX = Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex");
|
|
2985
|
+
/** 被签名内容的字段列表,需与实际拼接顺序一致 */
|
|
2986
|
+
const REQ_CONTENT = "ticket,path,timestamp";
|
|
2987
|
+
/** 紧凑 JSON,与浏览器的 `JSON.stringify` 一致 */
|
|
2988
|
+
const compactJson = (value) => JSON.stringify(value);
|
|
2989
|
+
/**
|
|
2990
|
+
* 从 PEM 证书或 `pub.<base64>` 里取出服务端公钥
|
|
2991
|
+
* @param clientCert 服务端下发的 client_cert
|
|
2992
|
+
*/
|
|
2993
|
+
const readServerPublicKey = (clientCert) => {
|
|
2994
|
+
if (clientCert.startsWith("pub.")) {
|
|
2995
|
+
const point = Buffer.from(clientCert.slice(4), "base64");
|
|
2996
|
+
const body = point.length === 65 ? point.subarray(1) : point;
|
|
2997
|
+
const spki = Buffer.concat([
|
|
2998
|
+
P256_SPKI_PREFIX,
|
|
2999
|
+
Buffer.from([4]),
|
|
3000
|
+
body
|
|
3001
|
+
]);
|
|
3002
|
+
return crypto.createPublicKey({
|
|
3003
|
+
key: spki,
|
|
3004
|
+
format: "der",
|
|
3005
|
+
type: "spki"
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
return new crypto.X509Certificate(clientCert).publicKey;
|
|
3009
|
+
};
|
|
3010
|
+
/**
|
|
3011
|
+
* bd-ticket-guard 会话
|
|
3012
|
+
*
|
|
3013
|
+
* 状态全部读写自传入的 CookieJar,因此与 passport 的无状态调用形态天然兼容。
|
|
3014
|
+
*/
|
|
3015
|
+
var TicketGuard = class {
|
|
3016
|
+
jar;
|
|
3017
|
+
/**
|
|
3018
|
+
* @param jar 当前会话 cookie
|
|
3019
|
+
*/
|
|
3020
|
+
constructor(jar) {
|
|
3021
|
+
this.jar = jar;
|
|
3022
|
+
}
|
|
3023
|
+
/** 本会话的私钥,缺失时生成一把并写回 CookieJar */
|
|
3024
|
+
get privateKey() {
|
|
3025
|
+
const stored = this.jar.get(KEY_ENTRY);
|
|
3026
|
+
if (stored) return crypto.createPrivateKey({
|
|
3027
|
+
key: Buffer.from(stored, "base64"),
|
|
3028
|
+
format: "der",
|
|
3029
|
+
type: "pkcs8"
|
|
3030
|
+
});
|
|
3031
|
+
const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
|
3032
|
+
const der = privateKey.export({
|
|
3033
|
+
format: "der",
|
|
3034
|
+
type: "pkcs8"
|
|
3035
|
+
});
|
|
3036
|
+
this.jar.set(KEY_ENTRY, der.toString("base64"));
|
|
3037
|
+
return privateKey;
|
|
3038
|
+
}
|
|
3039
|
+
/** 未压缩格式的公钥(base64),即 `bd-ticket-guard-ree-public-key` */
|
|
3040
|
+
get reePublicKey() {
|
|
3041
|
+
const spki = crypto.createPublicKey(this.privateKey).export({
|
|
3042
|
+
format: "der",
|
|
3043
|
+
type: "spki"
|
|
3044
|
+
});
|
|
3045
|
+
return spki.subarray(spki.length - 65).toString("base64");
|
|
3046
|
+
}
|
|
3047
|
+
/** 已签发的票据,未签发时为 undefined */
|
|
3048
|
+
get state() {
|
|
3049
|
+
const ticket = this.jar.get(TICKET_ENTRY);
|
|
3050
|
+
const tsSign = this.jar.get(TS_SIGN_ENTRY);
|
|
3051
|
+
const ecdh = this.jar.get(ECDH_ENTRY);
|
|
3052
|
+
if (!ticket || !tsSign || !ecdh) return void 0;
|
|
3053
|
+
return {
|
|
3054
|
+
ticket,
|
|
3055
|
+
tsSign,
|
|
3056
|
+
ecdhKey: Buffer.from(ecdh, "hex")
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* 在首个 passport 请求之前把公钥交给服务端
|
|
3061
|
+
*
|
|
3062
|
+
* 缺了这一步扫码依然能成功,但服务端不会签发票据,后续请求也就无从携带。
|
|
3063
|
+
*/
|
|
3064
|
+
publishPublicKey() {
|
|
3065
|
+
const payload = compactJson({
|
|
3066
|
+
"bd-ticket-guard-version": Number(GUARD_VERSION),
|
|
3067
|
+
"bd-ticket-guard-iteration-version": Number(ITERATION_VERSION),
|
|
3068
|
+
"bd-ticket-guard-ree-public-key": this.reePublicKey,
|
|
3069
|
+
"bd-ticket-guard-web-version": Number(GUARD_VERSION)
|
|
3070
|
+
});
|
|
3071
|
+
this.jar.set(CLIENT_DATA_COOKIE, encodeURIComponent(Buffer.from(payload, "utf8").toString("base64")));
|
|
3072
|
+
this.jar.set(CLIENT_WEB_DOMAIN_COOKIE, GUARD_VERSION);
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* 消化响应里可能带回的票据签发结果
|
|
3076
|
+
* @param headers 响应头
|
|
3077
|
+
* @returns 是否收到了新票据
|
|
3078
|
+
*/
|
|
3079
|
+
applyServerData(headers) {
|
|
3080
|
+
const fromHeader = headers[SERVER_DATA_HEADER];
|
|
3081
|
+
const raw = typeof fromHeader === "string" && fromHeader ? fromHeader : this.jar.get(SERVER_DATA_COOKIE);
|
|
3082
|
+
if (!raw) return false;
|
|
3083
|
+
let info;
|
|
3084
|
+
try {
|
|
3085
|
+
info = JSON.parse(Buffer.from(decodeURIComponent(raw), "base64").toString("utf8"));
|
|
3086
|
+
} catch {
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
if (!info.ticket || !info.ts_sign || !info.client_cert) return false;
|
|
3090
|
+
let ecdhKey;
|
|
3091
|
+
try {
|
|
3092
|
+
ecdhKey = this.deriveEcdhKey(info.client_cert);
|
|
3093
|
+
} catch {
|
|
3094
|
+
return false;
|
|
3095
|
+
}
|
|
3096
|
+
this.jar.set(TICKET_ENTRY, info.ticket);
|
|
3097
|
+
this.jar.set(TS_SIGN_ENTRY, info.ts_sign);
|
|
3098
|
+
this.jar.set(ECDH_ENTRY, ecdhKey.toString("hex"));
|
|
3099
|
+
return true;
|
|
3100
|
+
}
|
|
3101
|
+
/**
|
|
3102
|
+
* 生成本次请求的 bd-ticket-guard 请求头
|
|
3103
|
+
*
|
|
3104
|
+
* 尚未拿到票据时只声明公钥,让服务端有机会签发;拿到之后带完整签名。
|
|
3105
|
+
* @param path 请求路径,不含 query
|
|
3106
|
+
* @param timestamp 秒级时间戳,默认取当前
|
|
3107
|
+
*/
|
|
3108
|
+
headers(path, timestamp = Math.floor(Date.now() / 1e3)) {
|
|
3109
|
+
const base = {
|
|
3110
|
+
"bd-ticket-guard-version": GUARD_VERSION,
|
|
3111
|
+
"bd-ticket-guard-iteration-version": ITERATION_VERSION,
|
|
3112
|
+
"bd-ticket-guard-ree-public-key": this.reePublicKey
|
|
3113
|
+
};
|
|
3114
|
+
const state = this.state;
|
|
3115
|
+
if (!state) return base;
|
|
3116
|
+
const signed = `ticket=${state.ticket}&path=${path}×tamp=${timestamp}`;
|
|
3117
|
+
const clientData = compactJson({
|
|
3118
|
+
ts_sign: state.tsSign,
|
|
3119
|
+
req_content: REQ_CONTENT,
|
|
3120
|
+
req_sign: crypto.createHmac("sha256", state.ecdhKey).update(signed, "utf8").digest("base64"),
|
|
3121
|
+
timestamp
|
|
3122
|
+
});
|
|
3123
|
+
return {
|
|
3124
|
+
...base,
|
|
3125
|
+
"bd-ticket-guard-client-data": Buffer.from(clientData, "utf8").toString("base64"),
|
|
3126
|
+
"bd-ticket-guard-web-version": state.tsSign.startsWith("ts.1") ? "1" : GUARD_VERSION,
|
|
3127
|
+
"bd-ticket-guard-web-sign-type": "1"
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* ECDH + HKDF-SHA256 派生 HMAC 密钥
|
|
3132
|
+
* @param clientCert 服务端下发的证书或裸公钥
|
|
3133
|
+
*/
|
|
3134
|
+
deriveEcdhKey(clientCert) {
|
|
3135
|
+
const shared = crypto.diffieHellman({
|
|
3136
|
+
privateKey: this.privateKey,
|
|
3137
|
+
publicKey: readServerPublicKey(clientCert)
|
|
3138
|
+
});
|
|
3139
|
+
return Buffer.from(crypto.hkdfSync("sha256", shared, Buffer.alloc(32), Buffer.alloc(0), 32));
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
//#endregion
|
|
3143
|
+
//#region src/platform/douyin/passport/client.ts
|
|
3144
|
+
/**
|
|
3145
|
+
* passport 登录的 HTTP 客户端
|
|
3146
|
+
*
|
|
3147
|
+
* 走 amagi 自己的 `fetchResponse`(axios),因此代理、超时、重试与网络事件与其它接口一致。
|
|
3148
|
+
*
|
|
3149
|
+
* 客户端本身是无状态的:会话状态(`msToken`、`passport_csrf_token`)都以 cookie 形式
|
|
3150
|
+
* 随调用方传入的 cookie 串进出,`x-tt-passport-verify-portrait` 则由 `ttwid` 派生,
|
|
3151
|
+
* 因此同一份 cookie 在整个登录过程中会得到稳定的 portrait,调用方无需额外保存任何东西。
|
|
3152
|
+
*/
|
|
3153
|
+
/** 与签名里的浏览器环境保持一致的 UA */
|
|
3154
|
+
const PASSPORT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36";
|
|
3155
|
+
/** 单次请求默认超时 */
|
|
3156
|
+
const DEFAULT_TIMEOUT = 15e3;
|
|
3157
|
+
/** SSO 跳转最多跟随的次数 */
|
|
3158
|
+
const MAX_REDIRECT_HOPS = 5;
|
|
3159
|
+
/** 浏览器客户端提示头,服务端会与 UA 交叉校验 */
|
|
3160
|
+
const CLIENT_HINTS = {
|
|
3161
|
+
"sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Chromium\";v=\"142\"",
|
|
3162
|
+
"sec-ch-ua-mobile": "?0",
|
|
3163
|
+
"sec-ch-ua-platform": "\"Windows\""
|
|
3164
|
+
};
|
|
3165
|
+
/** 安全解析 JSON,失败返回空对象 */
|
|
3166
|
+
const parseJson = (text) => {
|
|
3167
|
+
try {
|
|
3168
|
+
return JSON.parse(text);
|
|
3169
|
+
} catch {
|
|
3170
|
+
return {};
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
/**
|
|
3174
|
+
* 由 ttwid 派生出稳定的 verify portrait
|
|
3175
|
+
*
|
|
3176
|
+
* 浏览器里这个值在一次登录页生命周期内固定不变。这里用 cookie 中已有的设备指纹推导,
|
|
3177
|
+
* 既保证同一会话内多次调用得到同一个值,又不需要调用方额外携带状态。
|
|
3178
|
+
* @param jar 当前会话 cookie
|
|
3179
|
+
*/
|
|
3180
|
+
const deriveVerifyPortrait = (jar) => {
|
|
3181
|
+
const seed = jar.get("ttwid") ?? jar.get("__ac_nonce") ?? "douyin-passport";
|
|
3182
|
+
const hex = crypto.createHash("sha256").update(seed).digest("hex");
|
|
3183
|
+
return `${[
|
|
3184
|
+
hex.slice(0, 8),
|
|
3185
|
+
hex.slice(8, 12),
|
|
3186
|
+
`4${hex.slice(13, 16)}`,
|
|
3187
|
+
`8${hex.slice(17, 20)}`,
|
|
3188
|
+
hex.slice(20, 32)
|
|
3189
|
+
].join("-")}.login`;
|
|
3190
|
+
};
|
|
3191
|
+
var DouyinPassportClient = class {
|
|
3192
|
+
requestConfig;
|
|
3193
|
+
/** 会话 cookie */
|
|
3194
|
+
cookies;
|
|
3195
|
+
/** bd-ticket-guard 设备票据,状态随 cookie 一起流转 */
|
|
3196
|
+
ticketGuard;
|
|
3197
|
+
/**
|
|
3198
|
+
* @param cookie 已有的会话 cookie 串
|
|
3199
|
+
* @param requestConfig amagi 的请求配置(代理、超时、额外请求头)
|
|
3200
|
+
*/
|
|
3201
|
+
constructor(cookie, requestConfig) {
|
|
3202
|
+
this.requestConfig = requestConfig;
|
|
3203
|
+
this.cookies = new CookieJar(cookie);
|
|
3204
|
+
this.ticketGuard = new TicketGuard(this.cookies);
|
|
3205
|
+
}
|
|
3206
|
+
/** CSRF token:优先用服务端下发的,缺失时本地生成并同步写进 cookie(双提交校验) */
|
|
3207
|
+
get csrfToken() {
|
|
3208
|
+
const fromCookie = this.cookies.get("passport_csrf_token");
|
|
3209
|
+
if (fromCookie) return fromCookie;
|
|
3210
|
+
const generated = randomHex(32);
|
|
3211
|
+
this.cookies.set("passport_csrf_token", generated);
|
|
3212
|
+
this.cookies.set("passport_csrf_token_default", generated);
|
|
3213
|
+
return generated;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* 初始化登录环境指纹
|
|
3217
|
+
*
|
|
3218
|
+
* 依次请求抖音首页拿 `__ac_nonce`、再向 ttwid 服务注册拿 `ttwid`。两步都是匿名的,
|
|
3219
|
+
* 任意机器、任意系统都能跑;失败不抛错,只会让后续更容易命中风控。
|
|
3220
|
+
*/
|
|
3221
|
+
async bootstrap() {
|
|
3222
|
+
this.ticketGuard.publishPublicKey();
|
|
3223
|
+
if (this.cookies.has("ttwid") && this.cookies.has("__ac_nonce")) return;
|
|
3224
|
+
await this.send({
|
|
3225
|
+
method: "GET",
|
|
3226
|
+
url: `https://${WEB_HOST}/`,
|
|
3227
|
+
headers: {
|
|
3228
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
3229
|
+
...CLIENT_HINTS
|
|
3230
|
+
}
|
|
3231
|
+
});
|
|
3232
|
+
await this.send({
|
|
3233
|
+
method: "POST",
|
|
3234
|
+
url: "https://ttwid.bytedance.com/ttwid/register/",
|
|
3235
|
+
headers: {
|
|
3236
|
+
"Content-Type": "application/json",
|
|
3237
|
+
Origin: `https://${WEB_HOST}`,
|
|
3238
|
+
Referer: `https://${WEB_HOST}/`
|
|
3239
|
+
},
|
|
3240
|
+
data: JSON.stringify({
|
|
3241
|
+
aid: 6383,
|
|
3242
|
+
service: WEB_HOST
|
|
3243
|
+
})
|
|
3244
|
+
});
|
|
3245
|
+
emitLogDebug(`[douyin passport] 环境指纹就绪: ttwid=${this.cookies.has("ttwid")}, ac_nonce=${this.cookies.has("__ac_nonce")}`);
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* 请求 login.douyin.com 的 passport 接口(四重签名 + a_bogus 形态)
|
|
3249
|
+
* @param path 接口路径,如 `/passport/web/get_qrcode/`
|
|
3250
|
+
* @param params 业务参数,并入 query
|
|
3251
|
+
*/
|
|
3252
|
+
async request(path, params = {}) {
|
|
3253
|
+
const common = makeCommonParams(params);
|
|
3254
|
+
const { sign, qs } = makeSignAndQs(common, {});
|
|
3255
|
+
const query = {
|
|
3256
|
+
...common,
|
|
3257
|
+
sign,
|
|
3258
|
+
qs
|
|
3259
|
+
};
|
|
3260
|
+
const msToken = this.cookies.get("msToken");
|
|
3261
|
+
if (msToken) query.msToken = msToken;
|
|
3262
|
+
const queryString = serializeQuery(query);
|
|
3263
|
+
const url = `https://${LOGIN_HOST}${path}?${queryString}&a_bogus=${encodeURIComponent(aBogus(queryString, PASSPORT_USER_AGENT))}`;
|
|
3264
|
+
return this.send({
|
|
3265
|
+
method: "GET",
|
|
3266
|
+
url,
|
|
3267
|
+
headers: {
|
|
3268
|
+
Accept: "application/json, text/javascript",
|
|
3269
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3270
|
+
Referer: `https://${WEB_HOST}/`,
|
|
3271
|
+
"x-tt-passport-aid-sign": makeAidSign(path),
|
|
3272
|
+
"x-tt-passport-csrf-token": this.csrfToken,
|
|
3273
|
+
"x-tt-passport-verify-portrait": deriveVerifyPortrait(this.cookies),
|
|
3274
|
+
"x-tt-passport-trace-id": String(common.biz_trace_id),
|
|
3275
|
+
...this.ticketGuard.headers(path),
|
|
3276
|
+
...CLIENT_HINTS
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* 请求 www.douyin.com 的验证页接口(lite 形态:固定 query + 表单 body,无签名)
|
|
3282
|
+
* @param path 接口路径,如 `/passport/web/send_code/`
|
|
3283
|
+
* @param params 业务参数,进 body
|
|
3284
|
+
* @param bizTraceId 业务追踪 ID,同一次验证流程内保持一致
|
|
3285
|
+
*/
|
|
3286
|
+
async liteRequest(path, params, bizTraceId) {
|
|
3287
|
+
return this.send({
|
|
3288
|
+
method: "POST",
|
|
3289
|
+
url: `https://${WEB_HOST}${path}?${serializeQuery(makeLiteParams(bizTraceId))}`,
|
|
3290
|
+
headers: {
|
|
3291
|
+
Accept: "application/json, text/javascript",
|
|
3292
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3293
|
+
Origin: `https://${WEB_HOST}`,
|
|
3294
|
+
Referer: `https://${WEB_HOST}/`,
|
|
3295
|
+
"x-tt-passport-aid-sign": makeAidSign(path),
|
|
3296
|
+
"x-tt-passport-csrf-token": this.csrfToken,
|
|
3297
|
+
"x-tt-passport-verify-portrait": deriveVerifyPortrait(this.cookies),
|
|
3298
|
+
"x-tt-passport-trace-id": bizTraceId,
|
|
3299
|
+
...this.ticketGuard.headers(path),
|
|
3300
|
+
...CLIENT_HINTS
|
|
3301
|
+
},
|
|
3302
|
+
data: serializeQuery(params)
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* 跟随扫码确认后下发的 SSO 跳转链,把最终的登录凭证收进 CookieJar
|
|
3307
|
+
* @param redirectUrl `check_qrconnect` 返回的 redirect_url
|
|
3308
|
+
* @returns 是否拿到登录态 cookie
|
|
3309
|
+
*/
|
|
3310
|
+
async followSsoRedirect(redirectUrl) {
|
|
3311
|
+
let current = redirectUrl;
|
|
3312
|
+
for (let hop = 0; hop < MAX_REDIRECT_HOPS; hop++) {
|
|
3313
|
+
const response = await this.send({
|
|
3314
|
+
method: "GET",
|
|
3315
|
+
url: current,
|
|
3316
|
+
maxRedirects: 0,
|
|
3317
|
+
headers: {
|
|
3318
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
3319
|
+
Referer: `https://${LOGIN_HOST}/`,
|
|
3320
|
+
...CLIENT_HINTS
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
const location = response.location;
|
|
3324
|
+
if (!location || response.status < 300 || response.status >= 400) break;
|
|
3325
|
+
current = new URL(location, current).toString();
|
|
3326
|
+
}
|
|
3327
|
+
return this.cookies.isLoggedIn();
|
|
3328
|
+
}
|
|
3329
|
+
/** 实际发请求:合并 cookie、消化 Set-Cookie 与 msToken */
|
|
3330
|
+
async send(config) {
|
|
3331
|
+
const cookie = this.cookies.toString();
|
|
3332
|
+
const response = await fetchResponse({
|
|
3333
|
+
timeout: this.requestConfig?.timeout ?? DEFAULT_TIMEOUT,
|
|
3334
|
+
proxy: this.requestConfig?.proxy,
|
|
3335
|
+
...config,
|
|
3336
|
+
responseType: "text",
|
|
3337
|
+
maxRedirects: config.maxRedirects ?? 5,
|
|
3338
|
+
headers: {
|
|
3339
|
+
"User-Agent": PASSPORT_USER_AGENT,
|
|
3340
|
+
...config.headers,
|
|
3341
|
+
...cookie ? { Cookie: cookie } : {}
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
if (isNetworkErrorResult(response)) throw new Error(response.error.amagiError.errorDescription);
|
|
3345
|
+
const axiosResponse = response;
|
|
3346
|
+
this.cookies.applySetCookie(axiosResponse.headers["set-cookie"]);
|
|
3347
|
+
const refreshed = axiosResponse.headers["x-ms-token"];
|
|
3348
|
+
if (typeof refreshed === "string" && refreshed) this.cookies.set("msToken", refreshed);
|
|
3349
|
+
if (this.ticketGuard.applyServerData(axiosResponse.headers)) emitLogDebug("[douyin passport] 已获得 bd-ticket-guard 票据");
|
|
3350
|
+
const raw = typeof axiosResponse.data === "string" ? axiosResponse.data : JSON.stringify(axiosResponse.data);
|
|
3351
|
+
return {
|
|
3352
|
+
status: axiosResponse.status,
|
|
3353
|
+
raw,
|
|
3354
|
+
body: parseJson(raw),
|
|
3355
|
+
cookie: this.cookies.serialize(),
|
|
3356
|
+
location: axiosResponse.headers.location
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
//#endregion
|
|
3361
|
+
//#region src/platform/douyin/passport/parser.ts
|
|
3362
|
+
/** 触发账号二次验证的错误码 */
|
|
3363
|
+
const ERROR_SECOND_VERIFY = 2046;
|
|
3364
|
+
/** 验证码错误 */
|
|
3365
|
+
const ERROR_WRONG_CODE = 1202;
|
|
3366
|
+
/** 发码过于频繁 */
|
|
3367
|
+
const ERROR_RATE_LIMITED = 1206;
|
|
3368
|
+
/** 命中风控 / 设备环境异常 */
|
|
3369
|
+
const RISK_ERROR_CODES = /* @__PURE__ */ new Set([2156, 4031]);
|
|
3370
|
+
/**
|
|
3371
|
+
* 轮询过于频繁
|
|
3372
|
+
*
|
|
3373
|
+
* 描述文案是「访问太频繁」,但设备指纹不完整时服务端也用这个码兜底,
|
|
3374
|
+
* 属于可重试的瞬时状态,退避后继续轮询即可,不能当致命错误。
|
|
3375
|
+
*/
|
|
3376
|
+
const ERROR_POLL_BUSY = 7;
|
|
3377
|
+
/** 命中限频后的退避倍率 */
|
|
3378
|
+
const BUSY_BACKOFF = 2;
|
|
3379
|
+
/** 轮询间隔下限与默认值,服务端偶尔会给 0 */
|
|
3380
|
+
const MIN_INTERVAL = 1e3;
|
|
3381
|
+
const DEFAULT_INTERVAL = 3e3;
|
|
3382
|
+
/** 验证会话票据字段,需原样透传 */
|
|
3383
|
+
const STD_KEYS = [
|
|
3384
|
+
"std_verify_flow_id",
|
|
3385
|
+
"std_verify_scene",
|
|
3386
|
+
"std_verify_template",
|
|
3387
|
+
"std_verify_token",
|
|
3388
|
+
"std_verify_type",
|
|
3389
|
+
"std_verify_way"
|
|
3390
|
+
];
|
|
3391
|
+
const asString = (value) => typeof value === "string" ? value : "";
|
|
3392
|
+
const asNumber = (value) => {
|
|
3393
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
3394
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3395
|
+
};
|
|
3396
|
+
/** 从响应体的多个可能位置取错误码 */
|
|
3397
|
+
const readErrorCode = (payload) => asNumber(payload.data?.error_code) ?? asNumber(payload.error_code);
|
|
3398
|
+
/** 取一段人类可读的错误描述 */
|
|
3399
|
+
const readMessage = (payload) => asString(payload.data?.description) || asString(payload.description) || asString(payload.message) || "";
|
|
3400
|
+
/**
|
|
3401
|
+
* 解析 `get_qrcode` 响应
|
|
3402
|
+
* @param payload 服务端响应体
|
|
3403
|
+
* @returns 二维码信息,缺少 token 时返回 null
|
|
3404
|
+
*/
|
|
3405
|
+
const parseQrcode = (payload) => {
|
|
3406
|
+
const data = payload.data ?? {};
|
|
3407
|
+
const token = asString(data.token);
|
|
3408
|
+
if (!token) return null;
|
|
3409
|
+
return {
|
|
3410
|
+
token,
|
|
3411
|
+
content: asString(data.qrcode_index_url) || token,
|
|
3412
|
+
expireTime: asNumber(data.expire_time) ?? 0
|
|
3413
|
+
};
|
|
3414
|
+
};
|
|
3415
|
+
/** 解析服务端下发的可选验证方式 */
|
|
3416
|
+
const parseVerifyWays = (raw) => {
|
|
3417
|
+
if (!Array.isArray(raw)) return [];
|
|
3418
|
+
return raw.map((item) => {
|
|
3419
|
+
const way = item;
|
|
3420
|
+
return {
|
|
3421
|
+
verifyWay: asString(way?.verify_way),
|
|
3422
|
+
mobile: asString(way?.mobile) || void 0
|
|
3423
|
+
};
|
|
3424
|
+
}).filter((way) => way.verifyWay !== "");
|
|
3425
|
+
};
|
|
3426
|
+
/**
|
|
3427
|
+
* 从轮询响应里提取二次验证上下文
|
|
3428
|
+
* @param data 轮询响应的 data 段
|
|
3429
|
+
*/
|
|
3430
|
+
const parseVerifyContext = (data) => {
|
|
3431
|
+
const stdParams = {};
|
|
3432
|
+
for (const key of STD_KEYS) {
|
|
3433
|
+
const value = asString(data[key]);
|
|
3434
|
+
if (value) stdParams[key] = value;
|
|
3435
|
+
}
|
|
3436
|
+
return {
|
|
3437
|
+
encryptUid: asString(data.encrypt_uid),
|
|
3438
|
+
verifyTicket: asString(data.verify_ticket),
|
|
3439
|
+
stdParams,
|
|
3440
|
+
copywritingKey: asString(data.copywriting_key) || "qr_connect",
|
|
3441
|
+
diversionTag: asString(data.ies_safety_diversion_tag) || "mfa",
|
|
3442
|
+
newVerifyFlow: asString(data.new_verify_flow),
|
|
3443
|
+
verifyWays: parseVerifyWays(data.verify_ways)
|
|
3444
|
+
};
|
|
3445
|
+
};
|
|
3446
|
+
/**
|
|
3447
|
+
* 解析 `check_qrconnect` 响应为状态机可消费的结果
|
|
3448
|
+
* @param payload 服务端响应体
|
|
3449
|
+
*/
|
|
3450
|
+
const parsePollResult = (payload) => {
|
|
3451
|
+
const data = payload.data ?? {};
|
|
3452
|
+
const rawInterval = asNumber(data.interval) ?? 0;
|
|
3453
|
+
const interval = rawInterval >= MIN_INTERVAL ? rawInterval : DEFAULT_INTERVAL;
|
|
3454
|
+
const status = asString(data.status);
|
|
3455
|
+
const errorCode = readErrorCode(payload);
|
|
3456
|
+
if (errorCode === ERROR_SECOND_VERIFY || asString(data.account_flow) === "verify") return {
|
|
3457
|
+
status: "verify",
|
|
3458
|
+
interval,
|
|
3459
|
+
verify: parseVerifyContext(data)
|
|
3460
|
+
};
|
|
3461
|
+
if (errorCode !== void 0 && RISK_ERROR_CODES.has(errorCode)) return {
|
|
3462
|
+
status: "risk",
|
|
3463
|
+
interval,
|
|
3464
|
+
message: readMessage(payload) || `error_code=${errorCode}`
|
|
3465
|
+
};
|
|
3466
|
+
if (errorCode === ERROR_POLL_BUSY) return {
|
|
3467
|
+
status: "busy",
|
|
3468
|
+
interval: interval * BUSY_BACKOFF,
|
|
3469
|
+
message: readMessage(payload) || "轮询过于频繁"
|
|
3470
|
+
};
|
|
3471
|
+
switch (status) {
|
|
3472
|
+
case "new":
|
|
3473
|
+
case "scanned":
|
|
3474
|
+
case "expired": return {
|
|
3475
|
+
status,
|
|
3476
|
+
interval
|
|
3477
|
+
};
|
|
3478
|
+
case "confirmed": return {
|
|
3479
|
+
status: "confirmed",
|
|
3480
|
+
interval,
|
|
3481
|
+
redirectUrl: asString(data.redirect_url) || (Array.isArray(data.redirect_urls) ? asString(data.redirect_urls[0]) : "")
|
|
3482
|
+
};
|
|
3483
|
+
default: return {
|
|
3484
|
+
status: "unknown",
|
|
3485
|
+
interval,
|
|
3486
|
+
message: readMessage(payload) || (status ? `status=${status}` : `error_code=${errorCode ?? "unknown"}`)
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
};
|
|
3490
|
+
/**
|
|
3491
|
+
* 解析 `send_code` 响应
|
|
3492
|
+
* @param payload 服务端响应体
|
|
3493
|
+
*/
|
|
3494
|
+
const parseSendCodeResult = (payload) => {
|
|
3495
|
+
const data = payload.data ?? {};
|
|
3496
|
+
const errorCode = readErrorCode(payload);
|
|
3497
|
+
const retryAfter = asNumber(data.retry_time) ?? 60;
|
|
3498
|
+
const mobile = asString(data.mobile);
|
|
3499
|
+
if (errorCode === 0 || errorCode === void 0 && payload.message === "success") return {
|
|
3500
|
+
ok: true,
|
|
3501
|
+
mobile,
|
|
3502
|
+
retryAfter,
|
|
3503
|
+
message: ""
|
|
3504
|
+
};
|
|
3505
|
+
return {
|
|
3506
|
+
ok: false,
|
|
3507
|
+
mobile,
|
|
3508
|
+
retryAfter,
|
|
3509
|
+
errorCode,
|
|
3510
|
+
message: readMessage(payload) || (errorCode === ERROR_RATE_LIMITED ? "短信发送过于频繁" : `发码失败 error_code=${errorCode ?? "unknown"}`)
|
|
3511
|
+
};
|
|
3512
|
+
};
|
|
3513
|
+
/**
|
|
3514
|
+
* 解析 `validate_code` 响应
|
|
3515
|
+
* @param payload 服务端响应体
|
|
3516
|
+
*/
|
|
3517
|
+
const parseValidateCodeResult = (payload) => {
|
|
3518
|
+
const data = payload.data ?? {};
|
|
3519
|
+
const errorCode = readErrorCode(payload);
|
|
3520
|
+
if (errorCode === 0 || asString(data.ticket) || errorCode === void 0 && payload.message === "success") return {
|
|
3521
|
+
ok: true,
|
|
3522
|
+
wrongCode: false,
|
|
3523
|
+
message: ""
|
|
3524
|
+
};
|
|
3525
|
+
return {
|
|
3526
|
+
ok: false,
|
|
3527
|
+
wrongCode: errorCode === ERROR_WRONG_CODE,
|
|
3528
|
+
errorCode,
|
|
3529
|
+
message: readMessage(payload) || (errorCode === ERROR_WRONG_CODE ? "验证码错误" : `验证失败 error_code=${errorCode ?? "unknown"}`)
|
|
3530
|
+
};
|
|
3531
|
+
};
|
|
3532
|
+
//#endregion
|
|
3533
|
+
//#region src/platform/douyin/passport/index.ts
|
|
3534
|
+
var passport_exports = /* @__PURE__ */ __exportAll({
|
|
3535
|
+
BDMS_SDK_VERSION: () => BDMS_SDK_VERSION,
|
|
3536
|
+
CookieJar: () => CookieJar,
|
|
3537
|
+
DouyinPassportClient: () => DouyinPassportClient,
|
|
3538
|
+
INTERNAL_PREFIX: () => INTERNAL_PREFIX,
|
|
3539
|
+
PASSPORT_USER_AGENT: () => PASSPORT_USER_AGENT,
|
|
3540
|
+
TicketGuard: () => TicketGuard,
|
|
3541
|
+
aBogus: () => aBogus,
|
|
3542
|
+
makeAidSign: () => makeAidSign,
|
|
3543
|
+
makeSignAndQs: () => makeSignAndQs,
|
|
3544
|
+
parsePollResult: () => parsePollResult,
|
|
3545
|
+
parseQrcode: () => parseQrcode,
|
|
3546
|
+
parseSendCodeResult: () => parseSendCodeResult,
|
|
3547
|
+
parseValidateCodeResult: () => parseValidateCodeResult,
|
|
3548
|
+
randomHex: () => randomHex,
|
|
3549
|
+
sm3: () => sm3,
|
|
3550
|
+
sm3Hex: () => sm3Hex,
|
|
3551
|
+
sm3Twice: () => sm3Twice,
|
|
3552
|
+
utcNoonTimestamp: () => utcNoonTimestamp,
|
|
3553
|
+
xor5Hex: () => xor5Hex
|
|
3554
|
+
});
|
|
3555
|
+
//#endregion
|
|
3556
|
+
//#region src/types/NetworksConfigType.ts
|
|
3557
|
+
/** 快手平台API错误码 */
|
|
3558
|
+
let kuaishouAPIErrorCode = /* @__PURE__ */ function(kuaishouAPIErrorCode) {
|
|
3559
|
+
/** Cookie无效或已过期 */
|
|
3560
|
+
kuaishouAPIErrorCode["COOKIE"] = "INVALID_COOKIE";
|
|
3561
|
+
/** 未知错误 */
|
|
3562
|
+
kuaishouAPIErrorCode["UNKNOWN"] = "UNKNOWN_ERROR";
|
|
3563
|
+
return kuaishouAPIErrorCode;
|
|
3564
|
+
}({});
|
|
3565
|
+
/** 小红书平台API错误码 */
|
|
3566
|
+
let xiaohongshuAPIErrorCode = /* @__PURE__ */ function(xiaohongshuAPIErrorCode) {
|
|
3567
|
+
/** Cookie无效或已过期 */
|
|
3568
|
+
xiaohongshuAPIErrorCode["COOKIE"] = "INVALID_COOKIE";
|
|
3569
|
+
/** 未知错误 */
|
|
3570
|
+
xiaohongshuAPIErrorCode["UNKNOWN"] = "UNKNOWN_ERROR";
|
|
3571
|
+
/** 非法请求 */
|
|
3572
|
+
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["ILLEGAL_REQUEST"] = 500] = "ILLEGAL_REQUEST";
|
|
3573
|
+
/** 检测到帐号异常,请稍后重试 */
|
|
3574
|
+
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["ACCOUNT_ABNORMAL"] = 300011] = "ACCOUNT_ABNORMAL";
|
|
3575
|
+
/** 网络连接异常,请检查网络设置后重试 */
|
|
3576
|
+
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["NETWORK_ERROR"] = 300012] = "NETWORK_ERROR";
|
|
3577
|
+
/** 访问频次异常,请勿频繁操作 */
|
|
3578
|
+
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["FREQUENCY_ERROR"] = 300013] = "FREQUENCY_ERROR";
|
|
3579
|
+
/** 浏览器异常,请尝试更换浏览器后重试 */
|
|
3580
|
+
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["BROWSER_ERROR"] = 300015] = "BROWSER_ERROR";
|
|
3581
|
+
return xiaohongshuAPIErrorCode;
|
|
3582
|
+
}({});
|
|
3583
|
+
//#endregion
|
|
3584
|
+
//#region src/model/fetchers/douyin/auth.ts
|
|
3585
|
+
/**
|
|
3586
|
+
* 抖音登录认证相关 API(passport 扫码登录)
|
|
3587
|
+
*
|
|
3588
|
+
* 与其它 fetcher 的差别:这几个接口不走 `DouyinData` 的 URL 拼装 + a_bogus 流水线,
|
|
3589
|
+
* 因为 passport 体系有自己的三重 query 签名、独立的 a_bogus 形态与双重编码规则。
|
|
3590
|
+
* 对外形态保持一致:同样是 `(options, cookie?, requestConfig?) => Result<T>`,
|
|
3591
|
+
* 同样发 `apiSuccess` / `apiError` 事件,同样复用 amagi 的代理、超时与重试。
|
|
3592
|
+
*
|
|
3593
|
+
* 这几个方法都是无状态的:会话状态全部装在 cookie 串里,调用方拿到返回的 `cookie`
|
|
3594
|
+
* 后在下一次调用时传回来即可。轮询循环由调用方维护。
|
|
3595
|
+
*
|
|
3596
|
+
* @module fetchers/douyin/auth
|
|
3597
|
+
*/
|
|
3598
|
+
/** 短信验证码的验证方式标识,服务端未给出可用方式时的兜底值 */
|
|
3599
|
+
const SMS_VERIFY_WAY = "mobile_sms_verify";
|
|
3600
|
+
/**
|
|
3601
|
+
* 可以用「收 6 位验证码」这套流程走完的验证方式
|
|
3602
|
+
*
|
|
3603
|
+
* 除官方常见的 `mobile_sms_verify`,账号被判定需要辅助验证时会给出
|
|
3604
|
+
* `assist_mobile_sms_verify`,两者都是下行短信收码,走同一对
|
|
3605
|
+
* `send_code` / `validate_code` 接口,区别只在 `std_verify_way` 的取值。
|
|
3606
|
+
* 上行短信(`*_up_sms_verify`)要求用户从手机发短信出去,是另一套接口,不在此列。
|
|
3607
|
+
*/
|
|
3608
|
+
const SMS_CODE_WAY_PATTERN = /^(assist_)?mobile_sms_verify$/;
|
|
3609
|
+
/**
|
|
3610
|
+
* 判断某个验证方式能否用短信验证码流程完成
|
|
3611
|
+
* @param verifyWay 服务端下发的 verify_way
|
|
3612
|
+
*/
|
|
3613
|
+
const isSmsCodeVerifyWay = (verifyWay) => SMS_CODE_WAY_PATTERN.test(verifyWay);
|
|
3614
|
+
/**
|
|
3615
|
+
* 选出本次要用的 std_verify_way
|
|
3616
|
+
*
|
|
3617
|
+
* 优先用调用方指定的;否则从服务端给出的可选方式里挑一个能收码的;都没有才回退到默认值。
|
|
3618
|
+
* 之前这里写死 `mobile_sms_verify`,遇到辅助验证的账号会因为 way 对不上而失败。
|
|
3619
|
+
* @param verify 轮询下发的验证上下文
|
|
3620
|
+
* @param requested 调用方显式指定的验证方式
|
|
3621
|
+
*/
|
|
3622
|
+
const resolveVerifyWay = (verify, requested) => requested ?? verify.verifyWays.find((way) => isSmsCodeVerifyWay(way.verifyWay))?.verifyWay ?? verify.stdParams.std_verify_way ?? SMS_VERIFY_WAY;
|
|
3623
|
+
/** 短信验证码的 act_type */
|
|
3624
|
+
const SMS_ACT_TYPE = "3737";
|
|
3625
|
+
/** 验证页 SDK 版本,随表单一起提交 */
|
|
3626
|
+
const AUTHN_VERSION = "1.0.0.420-web";
|
|
3627
|
+
/** 抖音 web 的 aid */
|
|
3628
|
+
const AID = "6383";
|
|
3629
|
+
/** 扫码成功后的跳转地址 */
|
|
3630
|
+
const NEXT_URL = "https://www.douyin.com";
|
|
3631
|
+
/**
|
|
3632
|
+
* 发码与验码共用的表单字段
|
|
3633
|
+
*
|
|
3634
|
+
* 字段顺序与「空值也要占位」的行为对齐官方验证页 SDK 的抓包形态:
|
|
3635
|
+
* `verify_ticket` / `new_verify_flow` / `std_verify_flow_id` / `std_verify_token`
|
|
3636
|
+
* 即使为空也必须出现,缺字段会被判为伪造请求。
|
|
3637
|
+
* @param verify 轮询下发的验证上下文
|
|
3638
|
+
* @param verifyWay 本次使用的验证方式,原样进 `std_verify_way`
|
|
3639
|
+
* @param tail 追加在 std_verify_way 之后的字段(发码是 is6Digits,验码是 code)
|
|
3640
|
+
*/
|
|
3641
|
+
const buildVerifyBody = (verify, verifyWay, tail) => ({
|
|
3642
|
+
mix_mode: "1",
|
|
3643
|
+
type: SMS_ACT_TYPE,
|
|
3644
|
+
encrypt_uid: verify.encryptUid,
|
|
3645
|
+
verify_ticket: verify.verifyTicket,
|
|
3646
|
+
copywriting_key: verify.copywritingKey,
|
|
3647
|
+
ies_safety_diversion_tag: verify.diversionTag,
|
|
3648
|
+
new_verify_flow: verify.newVerifyFlow,
|
|
3649
|
+
std_verify_flow_id: verify.stdParams.std_verify_flow_id ?? "",
|
|
3650
|
+
std_verify_scene: verify.stdParams.std_verify_scene ?? "account_login",
|
|
3651
|
+
std_verify_template: verify.stdParams.std_verify_template ?? "ato_web",
|
|
3652
|
+
std_verify_token: verify.stdParams.std_verify_token ?? "",
|
|
3653
|
+
std_verify_type: verify.stdParams.std_verify_type ?? "MFA",
|
|
3654
|
+
std_verify_way: verifyWay,
|
|
3655
|
+
...tail,
|
|
3656
|
+
aid: AID,
|
|
3657
|
+
new_authn_sdk_version: AUTHN_VERSION
|
|
3658
|
+
});
|
|
2650
3659
|
/**
|
|
2651
|
-
*
|
|
2652
|
-
* @
|
|
2653
|
-
|
|
3660
|
+
* 构造 passport 侧的业务错误响应
|
|
3661
|
+
* @param methodType 方法名,进 amagiError.requestType
|
|
3662
|
+
* @param message 错误描述
|
|
3663
|
+
*/
|
|
3664
|
+
const passportError = (methodType, message) => createErrorResponse({
|
|
3665
|
+
code: "UNKNOWN_ERROR",
|
|
3666
|
+
data: null,
|
|
3667
|
+
amagiError: {
|
|
3668
|
+
errorDescription: message,
|
|
3669
|
+
requestType: methodType,
|
|
3670
|
+
requestUrl: `https://login.douyin.com/passport/`
|
|
3671
|
+
},
|
|
3672
|
+
amagiMessage: message
|
|
3673
|
+
}, message);
|
|
3674
|
+
/** 统一包一层事件上报与异常兜底 */
|
|
3675
|
+
const run = async (methodType, task) => {
|
|
3676
|
+
const startTime = Date.now();
|
|
3677
|
+
try {
|
|
3678
|
+
const result = await task();
|
|
3679
|
+
const duration = Date.now() - startTime;
|
|
3680
|
+
if (result.code === 200) emitApiSuccess({
|
|
3681
|
+
platform: "douyin",
|
|
3682
|
+
methodType,
|
|
3683
|
+
response: result,
|
|
3684
|
+
statusCode: 200,
|
|
3685
|
+
duration
|
|
3686
|
+
});
|
|
3687
|
+
else emitApiError({
|
|
3688
|
+
platform: "douyin",
|
|
3689
|
+
methodType,
|
|
3690
|
+
errorCode: result.code,
|
|
3691
|
+
errorMessage: result.message,
|
|
3692
|
+
duration
|
|
3693
|
+
});
|
|
3694
|
+
return result;
|
|
3695
|
+
} catch (error) {
|
|
3696
|
+
const duration = Date.now() - startTime;
|
|
3697
|
+
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
|
3698
|
+
emitApiError({
|
|
3699
|
+
platform: "douyin",
|
|
3700
|
+
methodType,
|
|
3701
|
+
errorMessage,
|
|
3702
|
+
duration
|
|
3703
|
+
});
|
|
3704
|
+
throw new Error(`抖音登录请求失败: ${errorMessage}`);
|
|
3705
|
+
}
|
|
3706
|
+
};
|
|
2654
3707
|
/**
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
3708
|
+
* 申请抖音扫码登录二维码
|
|
3709
|
+
*
|
|
3710
|
+
* 首次调用会自动完成环境指纹初始化(`__ac_nonce` + `ttwid`),无需额外准备。
|
|
3711
|
+
* @param options - 请求选项 (可选)
|
|
3712
|
+
* @param cookie - 已有的会话 Cookie (可选,续用同一会话时传入)
|
|
3713
|
+
* @param requestConfig - 请求配置 (可选)
|
|
3714
|
+
* @returns 二维码令牌、内容与会话 cookie
|
|
2657
3715
|
* @example
|
|
2658
3716
|
* ```typescript
|
|
2659
|
-
*
|
|
3717
|
+
* const qrcode = await requestPassportQrcode()
|
|
3718
|
+
* console.log(qrcode.data.content) // 拿去生成二维码图片
|
|
3719
|
+
* ```
|
|
3720
|
+
*/
|
|
3721
|
+
async function requestPassportQrcode(options, cookie, requestConfig) {
|
|
3722
|
+
return run("passportQrcode", async () => {
|
|
3723
|
+
const client = new DouyinPassportClient(cookie, requestConfig);
|
|
3724
|
+
await client.bootstrap();
|
|
3725
|
+
const response = await client.request("/passport/web/get_qrcode/", {
|
|
3726
|
+
next: NEXT_URL,
|
|
3727
|
+
need_short_url: "true",
|
|
3728
|
+
need_logo: "false",
|
|
3729
|
+
is_new_login: "1"
|
|
3730
|
+
});
|
|
3731
|
+
const qrcode = parseQrcode(response.body);
|
|
3732
|
+
if (!qrcode) return passportError("passportQrcode", response.body.message || `获取二维码失败: ${response.raw.slice(0, 200)}`);
|
|
3733
|
+
return createSuccessResponse({
|
|
3734
|
+
token: qrcode.token,
|
|
3735
|
+
content: qrcode.content,
|
|
3736
|
+
expire_time: qrcode.expireTime,
|
|
3737
|
+
expires_in: Math.max(0, qrcode.expireTime - Math.floor(Date.now() / 1e3)),
|
|
3738
|
+
cookie: response.cookie
|
|
3739
|
+
}, "获取成功", 200);
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
/**
|
|
3743
|
+
* 查询抖音扫码登录二维码的状态
|
|
2660
3744
|
*
|
|
2661
|
-
*
|
|
3745
|
+
* 状态为 `confirmed` 时会自动跟随 SSO 跳转领取登录凭证,返回的 `cookie` 即完整登录态。
|
|
3746
|
+
* @param options - 二维码状态参数
|
|
3747
|
+
* @param options.token - `requestPassportQrcode` 返回的令牌
|
|
3748
|
+
* @param cookie - 会话 Cookie,必须是申请二维码时返回的那一份
|
|
3749
|
+
* @param requestConfig - 请求配置 (可选)
|
|
3750
|
+
* @returns 扫码状态与最新会话 cookie
|
|
3751
|
+
* @example
|
|
3752
|
+
* ```typescript
|
|
3753
|
+
* const status = await checkPassportQrcode({ token }, cookie)
|
|
3754
|
+
* // new 未扫码 / scanned 已扫待确认 / verify 需二次验证 / confirmed 登录成功 / expired 已过期
|
|
3755
|
+
* console.log(status.data.status)
|
|
2662
3756
|
* ```
|
|
2663
3757
|
*/
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3758
|
+
async function checkPassportQrcode(options, cookie, requestConfig) {
|
|
3759
|
+
return run("passportQrcodeStatus", async () => {
|
|
3760
|
+
if (!options?.token) return passportError("passportQrcodeStatus", "缺少 token 参数");
|
|
3761
|
+
const client = new DouyinPassportClient(cookie, requestConfig);
|
|
3762
|
+
const response = await client.request("/passport/web/check_qrconnect/", {
|
|
3763
|
+
next: NEXT_URL,
|
|
3764
|
+
need_logo: "false",
|
|
3765
|
+
is_frontier: "true",
|
|
3766
|
+
token: options.token,
|
|
3767
|
+
is_new_login: "1",
|
|
3768
|
+
need_short_url: "true"
|
|
3769
|
+
});
|
|
3770
|
+
const result = parsePollResult(response.body);
|
|
3771
|
+
if (result.status === "verify") emitLogDebug(`[douyin passport] 触发二次验证,服务端原始响应: ${response.raw.slice(0, 1e3)}`);
|
|
3772
|
+
if (result.status === "confirmed" && result.redirectUrl) await client.followSsoRedirect(result.redirectUrl);
|
|
3773
|
+
const sessionCookie = result.status === "confirmed" ? client.cookies.toString() : client.cookies.serialize();
|
|
3774
|
+
return createSuccessResponse({
|
|
3775
|
+
...result,
|
|
3776
|
+
cookie: sessionCookie,
|
|
3777
|
+
logged_in: client.cookies.isLoggedIn()
|
|
3778
|
+
}, "获取成功", 200);
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
/**
|
|
3782
|
+
* 向账号绑定手机发送二次验证短信验证码
|
|
3783
|
+
*
|
|
3784
|
+
* 用于轮询返回 `status: 'verify'`(即 `error_code=2046` / `account_flow=verify`)的场景。
|
|
3785
|
+
* @param options - 发码参数
|
|
3786
|
+
* @param options.verify - 轮询返回的验证上下文
|
|
3787
|
+
* @param options.biz_trace_id - 追踪 ID (可选,不传自动生成)
|
|
3788
|
+
* @param cookie - 会话 Cookie
|
|
3789
|
+
* @param requestConfig - 请求配置 (可选)
|
|
3790
|
+
* @returns 脱敏手机号、重发等待秒数与追踪 ID
|
|
3791
|
+
*/
|
|
3792
|
+
async function sendPassportVerifyCode(options, cookie, requestConfig) {
|
|
3793
|
+
return run("passportSendCode", async () => {
|
|
3794
|
+
if (!options?.verify?.encryptUid) return passportError("passportSendCode", "缺少 encrypt_uid,请从轮询响应中取得验证上下文");
|
|
3795
|
+
const bizTraceId = options.biz_trace_id ?? randomHex(8);
|
|
3796
|
+
const verifyWay = resolveVerifyWay(options.verify, options.verify_way);
|
|
3797
|
+
emitLogDebug(`[douyin passport] 发码使用的验证方式: ${verifyWay}`);
|
|
3798
|
+
const response = await new DouyinPassportClient(cookie, requestConfig).liteRequest("/passport/web/send_code/", buildVerifyBody(options.verify, verifyWay, { is6Digits: "1" }), bizTraceId);
|
|
3799
|
+
const result = parseSendCodeResult(response.body);
|
|
3800
|
+
if (!result.ok) emitLogDebug(`[douyin passport] 发码失败原文: ${response.raw.slice(0, 500)}`);
|
|
3801
|
+
return createSuccessResponse({
|
|
3802
|
+
...result,
|
|
3803
|
+
cookie: response.cookie,
|
|
3804
|
+
biz_trace_id: bizTraceId,
|
|
3805
|
+
verify_way: verifyWay
|
|
3806
|
+
}, "获取成功", 200);
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* 提交二次验证的短信验证码
|
|
3811
|
+
* @param options - 验码参数
|
|
3812
|
+
* @param options.verify - 轮询返回的验证上下文
|
|
3813
|
+
* @param options.code - 用户收到的 6 位验证码明文
|
|
3814
|
+
* @param options.biz_trace_id - 必须与发码时用的是同一个
|
|
3815
|
+
* @param cookie - 会话 Cookie
|
|
3816
|
+
* @param requestConfig - 请求配置 (可选)
|
|
3817
|
+
* @returns 验证结果;`wrongCode` 为 true 表示验证码填错,可以让用户重试
|
|
3818
|
+
*/
|
|
3819
|
+
async function validatePassportVerifyCode(options, cookie, requestConfig) {
|
|
3820
|
+
return run("passportValidateCode", async () => {
|
|
3821
|
+
if (!options?.verify?.encryptUid) return passportError("passportValidateCode", "缺少 encrypt_uid,请从轮询响应中取得验证上下文");
|
|
3822
|
+
if (!options.code) return passportError("passportValidateCode", "缺少 code,请填入收到的短信验证码");
|
|
3823
|
+
const response = await new DouyinPassportClient(cookie, requestConfig).liteRequest("/passport/web/validate_code/", buildVerifyBody(options.verify, resolveVerifyWay(options.verify, options.verify_way), { code: xor5Hex(options.code) }), options.biz_trace_id ?? randomHex(8));
|
|
3824
|
+
const result = parseValidateCodeResult(response.body);
|
|
3825
|
+
if (!result.ok) emitLogDebug(`[douyin passport] 验码失败原文: ${response.raw.slice(0, 500)}`);
|
|
3826
|
+
return createSuccessResponse({
|
|
3827
|
+
...result,
|
|
3828
|
+
cookie: response.cookie
|
|
3829
|
+
}, "获取成功", 200);
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
2694
3832
|
//#endregion
|
|
2695
3833
|
//#region src/platform/defaultConfigs.ts
|
|
2696
3834
|
/**
|
|
@@ -2826,34 +3964,6 @@ const getXiaohongshuDefaultConfig = (cookie) => {
|
|
|
2826
3964
|
} };
|
|
2827
3965
|
};
|
|
2828
3966
|
//#endregion
|
|
2829
|
-
//#region src/types/NetworksConfigType.ts
|
|
2830
|
-
/** 快手平台API错误码 */
|
|
2831
|
-
let kuaishouAPIErrorCode = /* @__PURE__ */ function(kuaishouAPIErrorCode) {
|
|
2832
|
-
/** Cookie无效或已过期 */
|
|
2833
|
-
kuaishouAPIErrorCode["COOKIE"] = "INVALID_COOKIE";
|
|
2834
|
-
/** 未知错误 */
|
|
2835
|
-
kuaishouAPIErrorCode["UNKNOWN"] = "UNKNOWN_ERROR";
|
|
2836
|
-
return kuaishouAPIErrorCode;
|
|
2837
|
-
}({});
|
|
2838
|
-
/** 小红书平台API错误码 */
|
|
2839
|
-
let xiaohongshuAPIErrorCode = /* @__PURE__ */ function(xiaohongshuAPIErrorCode) {
|
|
2840
|
-
/** Cookie无效或已过期 */
|
|
2841
|
-
xiaohongshuAPIErrorCode["COOKIE"] = "INVALID_COOKIE";
|
|
2842
|
-
/** 未知错误 */
|
|
2843
|
-
xiaohongshuAPIErrorCode["UNKNOWN"] = "UNKNOWN_ERROR";
|
|
2844
|
-
/** 非法请求 */
|
|
2845
|
-
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["ILLEGAL_REQUEST"] = 500] = "ILLEGAL_REQUEST";
|
|
2846
|
-
/** 检测到帐号异常,请稍后重试 */
|
|
2847
|
-
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["ACCOUNT_ABNORMAL"] = 300011] = "ACCOUNT_ABNORMAL";
|
|
2848
|
-
/** 网络连接异常,请检查网络设置后重试 */
|
|
2849
|
-
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["NETWORK_ERROR"] = 300012] = "NETWORK_ERROR";
|
|
2850
|
-
/** 访问频次异常,请勿频繁操作 */
|
|
2851
|
-
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["FREQUENCY_ERROR"] = 300013] = "FREQUENCY_ERROR";
|
|
2852
|
-
/** 浏览器异常,请尝试更换浏览器后重试 */
|
|
2853
|
-
xiaohongshuAPIErrorCode[xiaohongshuAPIErrorCode["BROWSER_ERROR"] = 300015] = "BROWSER_ERROR";
|
|
2854
|
-
return xiaohongshuAPIErrorCode;
|
|
2855
|
-
}({});
|
|
2856
|
-
//#endregion
|
|
2857
3967
|
//#region src/platform/douyin/sign/a_bogus.ts
|
|
2858
3968
|
var SM3 = class {
|
|
2859
3969
|
reg;
|
|
@@ -3055,8 +4165,6 @@ function result_encrypt(long_str, num) {
|
|
|
3055
4165
|
case 3:
|
|
3056
4166
|
temp_int = long_int & 63;
|
|
3057
4167
|
result += constant["str"].charAt(temp_int);
|
|
3058
|
-
break;
|
|
3059
|
-
default: break;
|
|
3060
4168
|
}
|
|
3061
4169
|
}
|
|
3062
4170
|
return result;
|
|
@@ -3558,7 +4666,8 @@ var DouyinAPI = class {
|
|
|
3558
4666
|
}
|
|
3559
4667
|
/** 获取视频或图集数据 */
|
|
3560
4668
|
getWorkDetail(data) {
|
|
3561
|
-
|
|
4669
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/aweme/detail/";
|
|
4670
|
+
const params = {
|
|
3562
4671
|
...this.getBaseParams(),
|
|
3563
4672
|
aweme_id: data.aweme_id,
|
|
3564
4673
|
update_version_code: "170400",
|
|
@@ -3568,11 +4677,13 @@ var DouyinAPI = class {
|
|
|
3568
4677
|
screen_height: "1310",
|
|
3569
4678
|
round_trip_time: "150",
|
|
3570
4679
|
webid: "7351848354471872041"
|
|
3571
|
-
}
|
|
4680
|
+
};
|
|
4681
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3572
4682
|
}
|
|
3573
4683
|
/** 获取评论数据 */
|
|
3574
4684
|
getComments(data) {
|
|
3575
|
-
|
|
4685
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/comment/list/";
|
|
4686
|
+
const params = {
|
|
3576
4687
|
...this.getBaseParams(),
|
|
3577
4688
|
aweme_id: data.aweme_id,
|
|
3578
4689
|
cursor: data.cursor ?? 0,
|
|
@@ -3587,11 +4698,13 @@ var DouyinAPI = class {
|
|
|
3587
4698
|
screen_width: "1552",
|
|
3588
4699
|
screen_height: "970",
|
|
3589
4700
|
round_trip_time: "50"
|
|
3590
|
-
}
|
|
4701
|
+
};
|
|
4702
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3591
4703
|
}
|
|
3592
4704
|
/** 获取二级评论数据 */
|
|
3593
4705
|
getCommentReplies(data) {
|
|
3594
|
-
|
|
4706
|
+
const baseUrl = "https://www-hj.douyin.com/aweme/v1/web/comment/list/reply/";
|
|
4707
|
+
const params = {
|
|
3595
4708
|
device_platform: "webapp",
|
|
3596
4709
|
aid: "6383",
|
|
3597
4710
|
channel: "channel_pc_web",
|
|
@@ -3629,11 +4742,13 @@ var DouyinAPI = class {
|
|
|
3629
4742
|
webid: "7487210762873685515",
|
|
3630
4743
|
verifyFp: fp,
|
|
3631
4744
|
fp
|
|
3632
|
-
}
|
|
4745
|
+
};
|
|
4746
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3633
4747
|
}
|
|
3634
4748
|
/** 获取动图数据 */
|
|
3635
4749
|
getSlidesInfo(data) {
|
|
3636
|
-
|
|
4750
|
+
const baseUrl = "https://www.iesdouyin.com/web/api/v2/aweme/slidesinfo/";
|
|
4751
|
+
const params = {
|
|
3637
4752
|
reflow_source: "reflow_page",
|
|
3638
4753
|
web_id: "7326472315356857893",
|
|
3639
4754
|
device_id: "7326472315356857893",
|
|
@@ -3642,7 +4757,8 @@ var DouyinAPI = class {
|
|
|
3642
4757
|
msToken: douyinSign.Mstoken(116),
|
|
3643
4758
|
verifyFp: fp,
|
|
3644
4759
|
fp
|
|
3645
|
-
}
|
|
4760
|
+
};
|
|
4761
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3646
4762
|
}
|
|
3647
4763
|
/** 获取表情数据 */
|
|
3648
4764
|
getEmojiList() {
|
|
@@ -3650,7 +4766,8 @@ var DouyinAPI = class {
|
|
|
3650
4766
|
}
|
|
3651
4767
|
/** 获取用户主页视频数据 */
|
|
3652
4768
|
getUserVideoList(data) {
|
|
3653
|
-
|
|
4769
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/aweme/post/";
|
|
4770
|
+
const params = {
|
|
3654
4771
|
...this.getBaseParams(),
|
|
3655
4772
|
sec_user_id: data.sec_uid,
|
|
3656
4773
|
max_cursor: data.max_cursor ?? "0",
|
|
@@ -3668,11 +4785,13 @@ var DouyinAPI = class {
|
|
|
3668
4785
|
screen_height: "970",
|
|
3669
4786
|
round_trip_time: "50",
|
|
3670
4787
|
webid: "7338423850134226495"
|
|
3671
|
-
}
|
|
4788
|
+
};
|
|
4789
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3672
4790
|
}
|
|
3673
4791
|
/** 获取用户喜欢列表数据 */
|
|
3674
4792
|
getUserFavoriteList(data) {
|
|
3675
|
-
|
|
4793
|
+
const baseUrl = "https://www-hj.douyin.com/aweme/v1/web/aweme/favorite/";
|
|
4794
|
+
const params = {
|
|
3676
4795
|
...this.getBaseParams(),
|
|
3677
4796
|
sec_user_id: data.sec_uid,
|
|
3678
4797
|
max_cursor: data.max_cursor ?? "0",
|
|
@@ -3691,11 +4810,13 @@ var DouyinAPI = class {
|
|
|
3691
4810
|
screen_height: "1310",
|
|
3692
4811
|
round_trip_time: "0",
|
|
3693
4812
|
webid: "7487210762873685515"
|
|
3694
|
-
}
|
|
4813
|
+
};
|
|
4814
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3695
4815
|
}
|
|
3696
4816
|
/** 获取用户推荐列表数据 */
|
|
3697
4817
|
getUserRecommendList(data) {
|
|
3698
|
-
|
|
4818
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/familiar/recommend/feed/";
|
|
4819
|
+
const params = {
|
|
3699
4820
|
device_platform: "",
|
|
3700
4821
|
aid: "6383",
|
|
3701
4822
|
channel: "channel_pc_web",
|
|
@@ -3734,11 +4855,13 @@ var DouyinAPI = class {
|
|
|
3734
4855
|
msToken: douyinSign.Mstoken(184),
|
|
3735
4856
|
verifyFp: fp,
|
|
3736
4857
|
fp
|
|
3737
|
-
}
|
|
4858
|
+
};
|
|
4859
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3738
4860
|
}
|
|
3739
4861
|
/** 获取用户主页信息 */
|
|
3740
4862
|
getUserProfile(data) {
|
|
3741
|
-
|
|
4863
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/user/profile/other/";
|
|
4864
|
+
const params = {
|
|
3742
4865
|
...this.getBaseParams(),
|
|
3743
4866
|
publish_video_strategy_type: "2",
|
|
3744
4867
|
source: "channel_pc_web",
|
|
@@ -3750,11 +4873,13 @@ var DouyinAPI = class {
|
|
|
3750
4873
|
screen_height: "970",
|
|
3751
4874
|
round_trip_time: "0",
|
|
3752
4875
|
webid: "7327957959955580467"
|
|
3753
|
-
}
|
|
4876
|
+
};
|
|
4877
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3754
4878
|
}
|
|
3755
4879
|
/** 获取热点词数据 */
|
|
3756
4880
|
getSuggestWords(data) {
|
|
3757
|
-
|
|
4881
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/api/suggest_words/";
|
|
4882
|
+
const params = {
|
|
3758
4883
|
...this.getBaseParams(),
|
|
3759
4884
|
query: data.query,
|
|
3760
4885
|
business_id: "30088",
|
|
@@ -3765,91 +4890,103 @@ var DouyinAPI = class {
|
|
|
3765
4890
|
screen_height: "970",
|
|
3766
4891
|
round_trip_time: "50",
|
|
3767
4892
|
webid: "7327957959955580467"
|
|
3768
|
-
}
|
|
4893
|
+
};
|
|
4894
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3769
4895
|
}
|
|
3770
4896
|
/** 获取搜索数据 */
|
|
3771
4897
|
search(data) {
|
|
3772
4898
|
const searchType = data.type ?? "general";
|
|
3773
4899
|
const { verifyFp, fp, ...baseParamsWithoutFp } = this.getBaseParams();
|
|
3774
|
-
if (searchType === "user")
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4900
|
+
if (searchType === "user") {
|
|
4901
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/discover/search/";
|
|
4902
|
+
const params = {
|
|
4903
|
+
...baseParamsWithoutFp,
|
|
4904
|
+
count: data.number ?? 10,
|
|
4905
|
+
disable_rs: "0",
|
|
4906
|
+
from_group_id: "",
|
|
4907
|
+
is_filter_search: "0",
|
|
4908
|
+
keyword: data.query,
|
|
4909
|
+
list_type: "single",
|
|
4910
|
+
need_filter_settings: "1",
|
|
4911
|
+
offset: "0",
|
|
4912
|
+
pc_libra_divert: "Windows",
|
|
4913
|
+
pc_search_top_1_params: "{\"enable_ai_search_top_1\":1}",
|
|
4914
|
+
query_correct_type: "1",
|
|
4915
|
+
round_trip_time: "250",
|
|
4916
|
+
screen_height: "1310",
|
|
4917
|
+
screen_width: "2328",
|
|
4918
|
+
search_channel: "aweme_user_web",
|
|
4919
|
+
search_source: "switch_tab",
|
|
4920
|
+
support_dash: "1",
|
|
4921
|
+
support_h265: "1",
|
|
4922
|
+
version_code: "170400",
|
|
4923
|
+
version_name: "17.4.0",
|
|
4924
|
+
webid: "7521399115230610959",
|
|
4925
|
+
...data.search_id && { search_id: data.search_id }
|
|
4926
|
+
};
|
|
4927
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
4928
|
+
} else if (searchType === "video") {
|
|
4929
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/search/item/";
|
|
4930
|
+
const params = {
|
|
4931
|
+
...baseParamsWithoutFp,
|
|
4932
|
+
count: data.number ?? 10,
|
|
4933
|
+
disable_rs: "0",
|
|
4934
|
+
enable_history: "1",
|
|
4935
|
+
from_group_id: "",
|
|
4936
|
+
is_filter_search: "0",
|
|
4937
|
+
keyword: data.query,
|
|
4938
|
+
list_type: "single",
|
|
4939
|
+
need_filter_settings: "1",
|
|
4940
|
+
offset: "0",
|
|
4941
|
+
pc_libra_divert: "Windows",
|
|
4942
|
+
pc_search_top_1_params: "{\"enable_ai_search_top_1\":1}",
|
|
4943
|
+
query_correct_type: "1",
|
|
4944
|
+
round_trip_time: "50",
|
|
4945
|
+
screen_height: "1310",
|
|
4946
|
+
screen_width: "2328",
|
|
4947
|
+
search_channel: "aweme_video_web",
|
|
4948
|
+
search_source: "switch_tab",
|
|
4949
|
+
support_dash: "1",
|
|
4950
|
+
support_h265: "1",
|
|
4951
|
+
version_code: "170400",
|
|
4952
|
+
version_name: "17.4.0",
|
|
4953
|
+
webid: "7521399115230610959",
|
|
4954
|
+
...data.search_id && { search_id: data.search_id }
|
|
4955
|
+
};
|
|
4956
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
4957
|
+
} else {
|
|
4958
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/general/search/stream/";
|
|
4959
|
+
const params = {
|
|
4960
|
+
...baseParamsWithoutFp,
|
|
4961
|
+
count: data.number ?? 10,
|
|
4962
|
+
disable_rs: "0",
|
|
4963
|
+
enable_history: "1",
|
|
4964
|
+
is_filter_search: "0",
|
|
4965
|
+
keyword: data.query,
|
|
4966
|
+
list_type: "",
|
|
4967
|
+
need_filter_settings: "1",
|
|
4968
|
+
offset: "0",
|
|
4969
|
+
pc_libra_divert: "Windows",
|
|
4970
|
+
pc_search_top_1_params: "{\"enable_ai_search_top_1\":1}",
|
|
4971
|
+
query_correct_type: "1",
|
|
4972
|
+
round_trip_time: "0",
|
|
4973
|
+
screen_height: "1310",
|
|
4974
|
+
screen_width: "2328",
|
|
4975
|
+
search_channel: "aweme_general",
|
|
4976
|
+
search_source: "normal_search",
|
|
4977
|
+
support_dash: "1",
|
|
4978
|
+
support_h265: "1",
|
|
4979
|
+
version_code: "190600",
|
|
4980
|
+
version_name: "19.6.0",
|
|
4981
|
+
webid: "7521399115230610959"
|
|
4982
|
+
};
|
|
4983
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
4984
|
+
}
|
|
3849
4985
|
}
|
|
3850
4986
|
/** 获取互动表情数据 */
|
|
3851
4987
|
getDynamicEmojiList() {
|
|
3852
|
-
|
|
4988
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/im/strategy/config";
|
|
4989
|
+
const params = {
|
|
3853
4990
|
device_platform: "webapp",
|
|
3854
4991
|
aid: "1128",
|
|
3855
4992
|
channel: "channel_pc_web",
|
|
@@ -3881,11 +5018,13 @@ var DouyinAPI = class {
|
|
|
3881
5018
|
msToken: douyinSign.Mstoken(116),
|
|
3882
5019
|
verifyFp: fp,
|
|
3883
5020
|
fp
|
|
3884
|
-
}
|
|
5021
|
+
};
|
|
5022
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3885
5023
|
}
|
|
3886
5024
|
/** 获取背景音乐数据 */
|
|
3887
5025
|
getMusicInfo(data) {
|
|
3888
|
-
|
|
5026
|
+
const baseUrl = "https://www.douyin.com/aweme/v1/web/music/detail/";
|
|
5027
|
+
const params = {
|
|
3889
5028
|
device_platform: "webapp",
|
|
3890
5029
|
aid: "6383",
|
|
3891
5030
|
channel: "channel_pc_web",
|
|
@@ -3916,11 +5055,13 @@ var DouyinAPI = class {
|
|
|
3916
5055
|
msToken: douyinSign.Mstoken(116),
|
|
3917
5056
|
verifyFp: fp,
|
|
3918
5057
|
fp
|
|
3919
|
-
}
|
|
5058
|
+
};
|
|
5059
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3920
5060
|
}
|
|
3921
5061
|
/** 获取直播间信息 */
|
|
3922
5062
|
getLiveRoomInfo(data) {
|
|
3923
|
-
|
|
5063
|
+
const baseUrl = "https://live.douyin.com/webcast/room/web/enter/";
|
|
5064
|
+
const params = {
|
|
3924
5065
|
aid: "6383",
|
|
3925
5066
|
app_name: "douyin_web",
|
|
3926
5067
|
live_id: "1",
|
|
@@ -3943,18 +5084,22 @@ var DouyinAPI = class {
|
|
|
3943
5084
|
msToken: douyinSign.Mstoken(116),
|
|
3944
5085
|
verifyFp: fp,
|
|
3945
5086
|
fp
|
|
3946
|
-
}
|
|
5087
|
+
};
|
|
5088
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3947
5089
|
}
|
|
3948
5090
|
/** 申请登录二维码 */
|
|
3949
5091
|
getLoginQrcode(data) {
|
|
3950
|
-
|
|
5092
|
+
const baseUrl = "https://sso.douyin.com/get_qrcode/";
|
|
5093
|
+
const params = {
|
|
3951
5094
|
verifyFp: data.verify_fp,
|
|
3952
5095
|
fp: data.verify_fp
|
|
3953
|
-
}
|
|
5096
|
+
};
|
|
5097
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3954
5098
|
}
|
|
3955
5099
|
/** 获取弹幕数据 */
|
|
3956
5100
|
getDanmakuList(data) {
|
|
3957
|
-
|
|
5101
|
+
const baseUrl = "https://www-hj.douyin.com/aweme/v1/web/danmaku/get_v2/";
|
|
5102
|
+
const params = {
|
|
3958
5103
|
...this.getBaseParams(),
|
|
3959
5104
|
app_name: "aweme",
|
|
3960
5105
|
format: "json",
|
|
@@ -3981,7 +5126,8 @@ var DouyinAPI = class {
|
|
|
3981
5126
|
msToken: douyinSign.Mstoken(116),
|
|
3982
5127
|
verifyFp: fp,
|
|
3983
5128
|
fp
|
|
3984
|
-
}
|
|
5129
|
+
};
|
|
5130
|
+
return `${baseUrl}?${buildQueryString(params)}`;
|
|
3985
5131
|
}
|
|
3986
5132
|
};
|
|
3987
5133
|
/**
|
|
@@ -4003,7 +5149,6 @@ const douyinApiUrls = new DouyinAPI();
|
|
|
4003
5149
|
* 提供抖音各类数据的获取功能,包括视频、评论、用户等
|
|
4004
5150
|
*
|
|
4005
5151
|
* 注意:为避免循环依赖,此文件直接从具体模块导入,而不是从平台 index 文件导入
|
|
4006
|
-
* 循环依赖链:DataFetchers → getdata → platform/douyin → DataFetchers
|
|
4007
5152
|
*
|
|
4008
5153
|
* @module platform/douyin/getdata
|
|
4009
5154
|
*/
|
|
@@ -4280,7 +5425,8 @@ const DouyinData = async (data, cookie, requestConfig) => {
|
|
|
4280
5425
|
signType: null,
|
|
4281
5426
|
processRawResponse: (raw) => {
|
|
4282
5427
|
if (!isUserSearch && !isVideoSearch) {
|
|
4283
|
-
const
|
|
5428
|
+
const chunks = typeof raw === "string" ? parseDouyinMultiJson(raw) : [raw];
|
|
5429
|
+
const responses = filterSearchResponses(chunks);
|
|
4284
5430
|
if (responses.length === 0) return raw;
|
|
4285
5431
|
const mergedData = [];
|
|
4286
5432
|
let lastValid = {};
|
|
@@ -4659,7 +5805,8 @@ const filterSearchResponses = (objs) => {
|
|
|
4659
5805
|
async function fetchDouyinInternal(methodType, options, config) {
|
|
4660
5806
|
const startTime = Date.now();
|
|
4661
5807
|
try {
|
|
4662
|
-
const
|
|
5808
|
+
const apiParams = { ...validateDouyinParams(methodType, options) };
|
|
5809
|
+
const rawData = await DouyinData(apiParams, config.cookie, config.requestConfig);
|
|
4663
5810
|
const duration = Date.now() - startTime;
|
|
4664
5811
|
if (rawData.data === "" || rawData.status_code !== 0) {
|
|
4665
5812
|
emitApiError({
|
|
@@ -5140,6 +6287,10 @@ function createBoundDouyinFetcher(cookie, requestConfig) {
|
|
|
5140
6287
|
* ```
|
|
5141
6288
|
*/
|
|
5142
6289
|
const douyinFetcher = {
|
|
6290
|
+
requestPassportQrcode,
|
|
6291
|
+
checkPassportQrcode,
|
|
6292
|
+
sendPassportVerifyCode,
|
|
6293
|
+
validatePassportVerifyCode,
|
|
5143
6294
|
fetchVideoWork: fetchVideoWork$1,
|
|
5144
6295
|
fetchImageAlbumWork,
|
|
5145
6296
|
fetchSlidesWork,
|
|
@@ -5267,11 +6418,13 @@ var API = class {
|
|
|
5267
6418
|
* @returns 请求配置
|
|
5268
6419
|
*/
|
|
5269
6420
|
profilePublic(data) {
|
|
6421
|
+
const count = "count" in data ? data.count ?? 12 : 12;
|
|
6422
|
+
const pcursor = "pcursor" in data ? data.pcursor ?? "" : "";
|
|
5270
6423
|
return createKuaishouLiveApiRequest("profilePublic", "/live_api/profile/public", {
|
|
5271
6424
|
caver: 2,
|
|
5272
|
-
count
|
|
6425
|
+
count,
|
|
5273
6426
|
hasMore: true,
|
|
5274
|
-
pcursor
|
|
6427
|
+
pcursor,
|
|
5275
6428
|
principalId: data.principalId,
|
|
5276
6429
|
privacy: "public"
|
|
5277
6430
|
}, { signPath: "/rest/k/feed/profile" });
|
|
@@ -5435,6 +6588,7 @@ var API = class {
|
|
|
5435
6588
|
* @returns 请求配置
|
|
5436
6589
|
*/
|
|
5437
6590
|
liveReco(gameId) {
|
|
6591
|
+
const normalizedGameId = Number(gameId) > 0 ? Number(gameId) : 1001;
|
|
5438
6592
|
return createKuaishouLiveApiRequest("liveReco", "/live_api/liveroom/reco", {}, {
|
|
5439
6593
|
method: "POST",
|
|
5440
6594
|
requiresSign: false,
|
|
@@ -5444,7 +6598,7 @@ var API = class {
|
|
|
5444
6598
|
followingWeight: 50
|
|
5445
6599
|
},
|
|
5446
6600
|
gameFavour: [{
|
|
5447
|
-
gameId:
|
|
6601
|
+
gameId: normalizedGameId,
|
|
5448
6602
|
totalStayLength: 100
|
|
5449
6603
|
}]
|
|
5450
6604
|
}
|
|
@@ -5677,8 +6831,10 @@ const maskKuaishouHudrPayload = (payload) => {
|
|
|
5677
6831
|
* @returns `HUDR_` 的完整结果及若干中间态,便于对拍与调试
|
|
5678
6832
|
*/
|
|
5679
6833
|
const deriveKuaishouHudrBody = (context) => {
|
|
5680
|
-
const
|
|
5681
|
-
const
|
|
6834
|
+
const payload = buildKuaishouHudrPayload(context);
|
|
6835
|
+
const maskedPayload = maskKuaishouHudrPayload(payload);
|
|
6836
|
+
const encrypted = new KuaishouChaChaCipher(KUAISHOU_HUDR_CHACHA_KEY, KUAISHOU_HUDR_CHACHA_NONCE).encrypt(maskedPayload);
|
|
6837
|
+
const body = encodeBase64Url(encrypted);
|
|
5682
6838
|
return {
|
|
5683
6839
|
body,
|
|
5684
6840
|
full: `${KUAISHOU_HUDR_PREFIX}${body}`,
|
|
@@ -6190,7 +7346,9 @@ const KUAISHOU_HE_RANDOM_MAX = 0xffffffffffff;
|
|
|
6190
7346
|
* @returns `$HE_` 载荷中的 4 字节 hash field hex
|
|
6191
7347
|
*/
|
|
6192
7348
|
const deriveKuaishouHeHashFieldHex = (signInput, hudrBody) => {
|
|
6193
|
-
|
|
7349
|
+
const hashInput = `${signInput}HUDR_${hudrBody}`;
|
|
7350
|
+
const digestHex = bytesToLowerHex(deriveKuaishouCts(deriveKuaishouB2sa(hashInput))).slice(0, 8);
|
|
7351
|
+
return bytesToLowerHex(xorByteArrays(hexToSignedBytes(digestHex), KUAISHOU_HE_INPUT_XOR_MASK));
|
|
6194
7352
|
};
|
|
6195
7353
|
/**
|
|
6196
7354
|
* 推导快手签名中的 `$HE_` 段。
|
|
@@ -6530,7 +7688,6 @@ var kuaishouSign = class {
|
|
|
6530
7688
|
* 快手数据获取模块
|
|
6531
7689
|
*
|
|
6532
7690
|
* 注意:为避免循环依赖,此文件直接从具体模块导入,而不是从平台 index 文件导入
|
|
6533
|
-
* 循环依赖链:DataFetchers → getdata → platform/kuaishou → DataFetchers
|
|
6534
7691
|
*/
|
|
6535
7692
|
const KUAISHOU_PROFILE_TAB_TYPE_MAP = {
|
|
6536
7693
|
public: "public",
|
|
@@ -7212,7 +8369,8 @@ const KuaishouData = async (data, cookie, requestConfig) => {
|
|
|
7212
8369
|
if (!liveDetailData) return liveRoomInfo;
|
|
7213
8370
|
const userInfo = isErrorDetailLike(userInfoPayload) ? void 0 : userInfoPayload?.data?.userInfo;
|
|
7214
8371
|
const sensitiveInfo = isErrorDetailLike(sensitivePayload) ? void 0 : sensitivePayload?.data?.sensitiveUserInfo;
|
|
7215
|
-
const
|
|
8372
|
+
const currentAuthor = mergeKuaishouLiveAuthor(liveDetailData?.author, userInfo, sensitiveInfo);
|
|
8373
|
+
const currentLiveRoomItem = mapLiveDetailToLiveRoomPlayItem(liveDetailData, currentAuthor);
|
|
7216
8374
|
const liveStreamId = currentLiveRoomItem.liveStream?.id ?? currentLiveRoomItem.config?.liveStreamId;
|
|
7217
8375
|
const currentGameId = liveDetailData?.gameInfo?.id ?? liveDetailData?.gameInfo?.gameId;
|
|
7218
8376
|
const liveDetailWebsocketMeta = resolveKuaishouLiveDetailWebsocketMeta(liveDetailData);
|
|
@@ -7230,7 +8388,8 @@ const KuaishouData = async (data, cookie, requestConfig) => {
|
|
|
7230
8388
|
shouldFetchRecommendList ? fetchKuaishouLiveApiPayload(data.methodType, kuaishouApiUrls.liveReco(currentGameId), refererPath, { allowResult2: true }) : Promise.resolve(null)
|
|
7231
8389
|
]);
|
|
7232
8390
|
const resolvedRecommendList = !isErrorDetailLike(recoPayload) && Array.isArray(recoPayload?.data?.list) ? recoPayload.data.list : liveDetailRecommendList;
|
|
7233
|
-
const
|
|
8391
|
+
const recoPlayList = Array.isArray(resolvedRecommendList) ? resolvedRecommendList.map((item) => mapRecoItemToLiveRoomPlayItem(item)) : [];
|
|
8392
|
+
const nextPlayList = dedupeLiveRoomPlayList([currentLiveRoomItem, ...recoPlayList]);
|
|
7234
8393
|
return {
|
|
7235
8394
|
...liveRoomInfo,
|
|
7236
8395
|
principalId: data.principalId,
|
|
@@ -7341,7 +8500,8 @@ const GlobalGetData$2 = async (type, options, config) => {
|
|
|
7341
8500
|
async function fetchKuaishouInternal(methodType, options, config) {
|
|
7342
8501
|
const startTime = Date.now();
|
|
7343
8502
|
try {
|
|
7344
|
-
const
|
|
8503
|
+
const apiParams = { ...validateKuaishouParams(methodType, options) };
|
|
8504
|
+
const rawData = await KuaishouData(apiParams, config.cookie, config.requestConfig);
|
|
7345
8505
|
const duration = Date.now() - startTime;
|
|
7346
8506
|
if (rawData.code && Object.values(kuaishouAPIErrorCode).includes(rawData.code)) {
|
|
7347
8507
|
emitApiError({
|
|
@@ -7600,18 +8760,19 @@ const XiaohongshuData = async (data, cookie, requestConfig) => {
|
|
|
7600
8760
|
...requestConfig?.headers ?? {}
|
|
7601
8761
|
}
|
|
7602
8762
|
};
|
|
8763
|
+
const userData = await GlobalGetData$1(data.methodType, {
|
|
8764
|
+
...baseRequestConfig,
|
|
8765
|
+
url: xiaohongshuApiUrls.userProfile(data).Url,
|
|
8766
|
+
headers: {
|
|
8767
|
+
...baseRequestConfig.headers,
|
|
8768
|
+
"x-s": xiaohongshuSign.generateXSGet(xiaohongshuApiUrls.userProfile(data).apiPath, xiaohongshuSign.extractA1FromCookie(requestCookie), "xhs-pc-web"),
|
|
8769
|
+
"x-s-common": xiaohongshuSign.generateXSCommon(requestCookie),
|
|
8770
|
+
"x-t": xiaohongshuSign.generateXT()
|
|
8771
|
+
}
|
|
8772
|
+
});
|
|
7603
8773
|
return {
|
|
7604
8774
|
code: 0,
|
|
7605
|
-
data: extractCreatorInfoFromHtml(
|
|
7606
|
-
...baseRequestConfig,
|
|
7607
|
-
url: xiaohongshuApiUrls.userProfile(data).Url,
|
|
7608
|
-
headers: {
|
|
7609
|
-
...baseRequestConfig.headers,
|
|
7610
|
-
"x-s": xiaohongshuSign.generateXSGet(xiaohongshuApiUrls.userProfile(data).apiPath, xiaohongshuSign.extractA1FromCookie(requestCookie), "xhs-pc-web"),
|
|
7611
|
-
"x-s-common": xiaohongshuSign.generateXSCommon(requestCookie),
|
|
7612
|
-
"x-t": xiaohongshuSign.generateXT()
|
|
7613
|
-
}
|
|
7614
|
-
})),
|
|
8775
|
+
data: extractCreatorInfoFromHtml(userData),
|
|
7615
8776
|
msg: "success"
|
|
7616
8777
|
};
|
|
7617
8778
|
}
|
|
@@ -7725,7 +8886,8 @@ const sortTypeMapping = {
|
|
|
7725
8886
|
async function fetchXiaohongshuInternal(methodType, options, config) {
|
|
7726
8887
|
const startTime = Date.now();
|
|
7727
8888
|
try {
|
|
7728
|
-
const
|
|
8889
|
+
const apiParams = { ...validateXiaohongshuParams(methodType, options) };
|
|
8890
|
+
const rawData = await XiaohongshuData(apiParams, config.cookie, config.requestConfig);
|
|
7729
8891
|
const duration = Date.now() - startTime;
|
|
7730
8892
|
if (rawData.code && Object.values(xiaohongshuAPIErrorCode).includes(rawData.code)) {
|
|
7731
8893
|
emitApiError({
|
|
@@ -8149,106 +9311,6 @@ const getHeadersAndData = async (config, maxRetries = DEFAULT_MAX_RETRIES) => {
|
|
|
8149
9311
|
};
|
|
8150
9312
|
};
|
|
8151
9313
|
//#endregion
|
|
8152
|
-
//#region src/model/logger.ts
|
|
8153
|
-
/**
|
|
8154
|
-
* @deprecated v6 已废弃日志模块,请使用事件系统替代
|
|
8155
|
-
* @see {@link ../events.ts} 使用 amagiEvents 监听日志事件
|
|
8156
|
-
*
|
|
8157
|
-
* 迁移示例:
|
|
8158
|
-
* ```typescript
|
|
8159
|
-
* import { amagiEvents } from '@ikenxuan/amagi'
|
|
8160
|
-
*
|
|
8161
|
-
* amagiEvents.on('log:info', (data) => console.log(data.message))
|
|
8162
|
-
* amagiEvents.on('log:error', (data) => console.error(data.message))
|
|
8163
|
-
* ```
|
|
8164
|
-
*/
|
|
8165
|
-
/**
|
|
8166
|
-
* @deprecated v6 已废弃,请使用事件系统替代
|
|
8167
|
-
* 初始化 logger 配置 - 此函数现在为空操作
|
|
8168
|
-
*/
|
|
8169
|
-
const initLogger = () => {};
|
|
8170
|
-
/**
|
|
8171
|
-
* @deprecated v6 已废弃,请使用事件系统替代
|
|
8172
|
-
* 简化的日志类,仅发射事件,不再依赖 log4js
|
|
8173
|
-
*/
|
|
8174
|
-
var SimpleLogger = class {
|
|
8175
|
-
chalk;
|
|
8176
|
-
red;
|
|
8177
|
-
green;
|
|
8178
|
-
yellow;
|
|
8179
|
-
blue;
|
|
8180
|
-
magenta;
|
|
8181
|
-
cyan;
|
|
8182
|
-
white;
|
|
8183
|
-
gray;
|
|
8184
|
-
constructor() {
|
|
8185
|
-
this.chalk = new Chalk();
|
|
8186
|
-
this.red = this.chalk.red;
|
|
8187
|
-
this.green = this.chalk.green;
|
|
8188
|
-
this.yellow = this.chalk.yellow;
|
|
8189
|
-
this.blue = this.chalk.blue;
|
|
8190
|
-
this.magenta = this.chalk.magenta;
|
|
8191
|
-
this.cyan = this.chalk.cyan;
|
|
8192
|
-
this.white = this.chalk.white;
|
|
8193
|
-
this.gray = this.chalk.gray;
|
|
8194
|
-
}
|
|
8195
|
-
info(message, ...args) {
|
|
8196
|
-
emitLog("info", String(message), ...args);
|
|
8197
|
-
}
|
|
8198
|
-
warn(message, ...args) {
|
|
8199
|
-
emitLog("warn", String(message), ...args);
|
|
8200
|
-
}
|
|
8201
|
-
error(message, ...args) {
|
|
8202
|
-
emitLog("error", String(message), ...args);
|
|
8203
|
-
}
|
|
8204
|
-
mark(message, ...args) {
|
|
8205
|
-
emitLog("mark", String(message), ...args);
|
|
8206
|
-
}
|
|
8207
|
-
debug(message, ...args) {
|
|
8208
|
-
emitLog("debug", String(message), ...args);
|
|
8209
|
-
}
|
|
8210
|
-
};
|
|
8211
|
-
/**
|
|
8212
|
-
* @deprecated v6 已废弃,请使用事件系统替代
|
|
8213
|
-
*/
|
|
8214
|
-
const logger = new SimpleLogger();
|
|
8215
|
-
/**
|
|
8216
|
-
* @deprecated v6 已废弃,请使用事件系统替代
|
|
8217
|
-
*/
|
|
8218
|
-
const httpLogger = new SimpleLogger();
|
|
8219
|
-
/**
|
|
8220
|
-
* @deprecated v6 已废弃,请使用事件系统监听 http:response 事件
|
|
8221
|
-
* 创建一个日志中间件,用于记录特定请求的详细信息
|
|
8222
|
-
* @param pathsToLog 指定需要记录日志的请求路径数组如果未提供,则记录所有请求的日志
|
|
8223
|
-
* @returns
|
|
8224
|
-
*/
|
|
8225
|
-
const logMiddleware = (pathsToLog) => {
|
|
8226
|
-
return (req, res, next) => {
|
|
8227
|
-
if (!pathsToLog || pathsToLog.some((path) => req.url.startsWith(path))) {
|
|
8228
|
-
const startTime = Date.now();
|
|
8229
|
-
const url = req.url;
|
|
8230
|
-
const method = req.method;
|
|
8231
|
-
const clientIP = req.headers["x-forwarded-for"] ?? req.socket.remoteAddress;
|
|
8232
|
-
res.on("finish", () => {
|
|
8233
|
-
const responseTime = Date.now() - startTime;
|
|
8234
|
-
const statusCode = res.statusCode;
|
|
8235
|
-
const requestSize = req.headers["content-length"] ?? "0";
|
|
8236
|
-
const responseSize = res.get("content-length") ?? "0";
|
|
8237
|
-
emitHttpResponse({
|
|
8238
|
-
method,
|
|
8239
|
-
url,
|
|
8240
|
-
statusCode,
|
|
8241
|
-
responseTime,
|
|
8242
|
-
clientIP: String(clientIP),
|
|
8243
|
-
requestSize: `${requestSize}B`,
|
|
8244
|
-
responseSize: `${responseSize}B`
|
|
8245
|
-
});
|
|
8246
|
-
});
|
|
8247
|
-
}
|
|
8248
|
-
next();
|
|
8249
|
-
};
|
|
8250
|
-
};
|
|
8251
|
-
//#endregion
|
|
8252
9314
|
//#region src/platform/bilibili/qtparam.ts
|
|
8253
9315
|
/**
|
|
8254
9316
|
* 生成B站视频流请求参数
|
|
@@ -8567,7 +9629,6 @@ const wbi_sign = async (BASEURL, cookie) => {
|
|
|
8567
9629
|
* 提供 B站各类数据的获取功能,包括视频、评论、用户、番剧等
|
|
8568
9630
|
*
|
|
8569
9631
|
* 注意:为避免循环依赖,此文件直接从具体模块导入,而不是从平台 index 文件导入
|
|
8570
|
-
* 循环依赖链:DataFetchers → getdata → platform/bilibili → DataFetchers
|
|
8571
9632
|
*
|
|
8572
9633
|
* @module platform/bilibili/getdata
|
|
8573
9634
|
*/
|
|
@@ -8587,10 +9648,11 @@ const fetchBilibili = async (data, cookie, requestConfig) => {
|
|
|
8587
9648
|
url: bilibiliApiUrls.getVideoInfo({ bvid: data.bvid })
|
|
8588
9649
|
});
|
|
8589
9650
|
case "videoStream": {
|
|
8590
|
-
const
|
|
9651
|
+
const baseUrl = bilibiliApiUrls.getVideoStream({
|
|
8591
9652
|
avid: data.avid,
|
|
8592
9653
|
cid: data.cid
|
|
8593
|
-
})
|
|
9654
|
+
});
|
|
9655
|
+
const sign = await qtparam(baseUrl, baseRequestConfig.headers?.Cookie);
|
|
8594
9656
|
return await GlobalGetData(data.methodType, {
|
|
8595
9657
|
...baseRequestConfig,
|
|
8596
9658
|
url: bilibiliApiUrls.getVideoStream({
|
|
@@ -8677,10 +9739,11 @@ const fetchBilibili = async (data, cookie, requestConfig) => {
|
|
|
8677
9739
|
});
|
|
8678
9740
|
}
|
|
8679
9741
|
case "bangumiStream": {
|
|
8680
|
-
const
|
|
9742
|
+
const baseUrl = bilibiliApiUrls.getBangumiStream({
|
|
8681
9743
|
cid: data.cid,
|
|
8682
9744
|
ep_id: data.ep_id.replace("ep", "")
|
|
8683
|
-
})
|
|
9745
|
+
});
|
|
9746
|
+
const sign = await qtparam(baseUrl, baseRequestConfig.headers?.cookie);
|
|
8684
9747
|
return await GlobalGetData(data.methodType, {
|
|
8685
9748
|
...baseRequestConfig,
|
|
8686
9749
|
url: bilibiliApiUrls.getBangumiStream({
|
|
@@ -8716,12 +9779,6 @@ const fetchBilibili = async (data, cookie, requestConfig) => {
|
|
|
8716
9779
|
url: bilibiliApiUrls.getDynamicDetail({ dynamic_id: data.dynamic_id })
|
|
8717
9780
|
});
|
|
8718
9781
|
}
|
|
8719
|
-
case "dynamicCard": return {
|
|
8720
|
-
code: -404,
|
|
8721
|
-
message: "接口已停用:B站官方已于 `2025-08-09` 删除 dynamic_svr 接口,fetchDynamicCard 方法已废弃,调用讲返回错误信息",
|
|
8722
|
-
ttl: 1,
|
|
8723
|
-
data: null
|
|
8724
|
-
};
|
|
8725
9782
|
case "userCard": {
|
|
8726
9783
|
const { host_mid } = data;
|
|
8727
9784
|
return await GlobalGetData(data.methodType, {
|
|
@@ -8734,7 +9791,8 @@ const fetchBilibili = async (data, cookie, requestConfig) => {
|
|
|
8734
9791
|
url: bilibiliApiUrls.getUserLiveStatus({ host_mid: data.host_mid })
|
|
8735
9792
|
});
|
|
8736
9793
|
case "userSpaceInfo": {
|
|
8737
|
-
const
|
|
9794
|
+
const baseUrl = bilibiliApiUrls.getUserSpaceInfo({ host_mid: data.host_mid });
|
|
9795
|
+
const wbiSignQuery = await wbi_sign(baseUrl, baseRequestConfig.headers?.cookie);
|
|
8738
9796
|
return await GlobalGetData(data.methodType, {
|
|
8739
9797
|
...baseRequestConfig,
|
|
8740
9798
|
url: bilibiliApiUrls.getUserSpaceInfo({ host_mid: data.host_mid }) + wbiSignQuery
|
|
@@ -9085,10 +10143,11 @@ var ValidationError = class ValidationError extends Error {
|
|
|
9085
10143
|
* @returns 验证错误实例
|
|
9086
10144
|
*/
|
|
9087
10145
|
static fromZodError(zodError, requestPath) {
|
|
9088
|
-
|
|
10146
|
+
const errors = zodError.issues.map((err) => ({
|
|
9089
10147
|
field: err.path.join("."),
|
|
9090
10148
|
message: err.message
|
|
9091
|
-
}))
|
|
10149
|
+
}));
|
|
10150
|
+
return new ValidationError("参数验证失败", errors, requestPath);
|
|
9092
10151
|
}
|
|
9093
10152
|
};
|
|
9094
10153
|
/**
|
|
@@ -9112,7 +10171,10 @@ const handleError = (error, requestPath) => {
|
|
|
9112
10171
|
platform: error.platform,
|
|
9113
10172
|
requestPath
|
|
9114
10173
|
};
|
|
9115
|
-
if (error instanceof zod.ZodError)
|
|
10174
|
+
if (error instanceof zod.ZodError) {
|
|
10175
|
+
const validationError = ValidationError.fromZodError(error, requestPath);
|
|
10176
|
+
return handleError(validationError, requestPath);
|
|
10177
|
+
}
|
|
9116
10178
|
return {
|
|
9117
10179
|
code: 500,
|
|
9118
10180
|
message: error instanceof Error ? error.message : "未知错误",
|
|
@@ -9223,71 +10285,7 @@ const bilibiliUtils = {
|
|
|
9223
10285
|
bv2av
|
|
9224
10286
|
},
|
|
9225
10287
|
danmaku: { parseDmSegMobileReply },
|
|
9226
|
-
bilibiliApiUrls
|
|
9227
|
-
api: bilibili
|
|
9228
|
-
};
|
|
9229
|
-
//#endregion
|
|
9230
|
-
//#region src/platform/douyin/DouyinApi.ts
|
|
9231
|
-
/**
|
|
9232
|
-
* 创建废弃的 API 存根函数
|
|
9233
|
-
*/
|
|
9234
|
-
const createDeprecatedStub$2 = (methodName) => {
|
|
9235
|
-
return (..._args) => {
|
|
9236
|
-
checkDeprecation("getDouyinData");
|
|
9237
|
-
throw new Error(`douyin.${methodName} 已废弃,请使用 douyinFetcher 替代`);
|
|
9238
|
-
};
|
|
9239
|
-
};
|
|
9240
|
-
/**
|
|
9241
|
-
* 封装了所有抖音相关的API请求,采用对象化的方式组织。
|
|
9242
|
-
*
|
|
9243
|
-
* @deprecated v6 已废弃,请使用 douyinFetcher 或 client.douyin.fetcher 替代
|
|
9244
|
-
*/
|
|
9245
|
-
const douyin = {
|
|
9246
|
-
/** @deprecated 请使用 douyinFetcher.fetchTextWork 替代 */
|
|
9247
|
-
getTextWorkInfo: createDeprecatedStub$2("getTextWorkInfo"),
|
|
9248
|
-
/** @deprecated 请使用 douyinFetcher.parseWork 替代 */
|
|
9249
|
-
getWorkInfo: createDeprecatedStub$2("getWorkInfo"),
|
|
9250
|
-
/** @deprecated 请使用 douyinFetcher.fetchVideoWork 替代 */
|
|
9251
|
-
getVideoWorkInfo: createDeprecatedStub$2("getVideoWorkInfo"),
|
|
9252
|
-
/** @deprecated 请使用 douyinFetcher.fetchImageAlbumWork 替代 */
|
|
9253
|
-
getImageAlbumWorkInfo: createDeprecatedStub$2("getImageAlbumWorkInfo"),
|
|
9254
|
-
/** @deprecated 请使用 douyinFetcher.fetchSlidesWork 替代 */
|
|
9255
|
-
getSlidesWorkInfo: createDeprecatedStub$2("getSlidesWorkInfo"),
|
|
9256
|
-
/** @deprecated 请使用 douyinFetcher.fetchComments 替代 */
|
|
9257
|
-
getComments: createDeprecatedStub$2("getComments"),
|
|
9258
|
-
/** @deprecated 请使用 douyinFetcher.fetchCommentReplies 替代 */
|
|
9259
|
-
getCommentReplies: createDeprecatedStub$2("getCommentReplies"),
|
|
9260
|
-
/** @deprecated 请使用 douyinFetcher.fetchUserProfile 替代 */
|
|
9261
|
-
getUserProfile: createDeprecatedStub$2("getUserProfile"),
|
|
9262
|
-
/** @deprecated 请使用 douyinFetcher.fetchEmojiList 替代 */
|
|
9263
|
-
getEmojiList: createDeprecatedStub$2("getEmojiList"),
|
|
9264
|
-
/** @deprecated 请使用 douyinFetcher.fetchDynamicEmojiList 替代 */
|
|
9265
|
-
getEmojiProList: createDeprecatedStub$2("getEmojiProList"),
|
|
9266
|
-
/** @deprecated 请使用 douyinFetcher.fetchUserVideoList 替代 */
|
|
9267
|
-
getUserVideos: createDeprecatedStub$2("getUserVideos"),
|
|
9268
|
-
/** @deprecated 请使用 douyinFetcher.fetchMusicInfo 替代 */
|
|
9269
|
-
getMusicInfo: createDeprecatedStub$2("getMusicInfo"),
|
|
9270
|
-
/** @deprecated 请使用 douyinFetcher.fetchSuggestWords 替代 */
|
|
9271
|
-
getSuggestWords: createDeprecatedStub$2("getSuggestWords"),
|
|
9272
|
-
/** @deprecated 请使用 douyinFetcher.searchContent 替代 */
|
|
9273
|
-
search: createDeprecatedStub$2("search"),
|
|
9274
|
-
/** @deprecated 请使用 douyinFetcher.fetchLiveRoomInfo 替代 */
|
|
9275
|
-
getLiveRoomInfo: createDeprecatedStub$2("getLiveRoomInfo"),
|
|
9276
|
-
/** @deprecated 请使用 douyinFetcher.fetchDanmakuList 替代 */
|
|
9277
|
-
getDanmaku: createDeprecatedStub$2("getDanmaku"),
|
|
9278
|
-
/** @deprecated 请使用 douyinFetcher 的具体方法替代 */
|
|
9279
|
-
invoke: createDeprecatedStub$2("invoke")
|
|
9280
|
-
};
|
|
9281
|
-
/**
|
|
9282
|
-
* 创建绑定了cookie的抖音API对象
|
|
9283
|
-
*
|
|
9284
|
-
* @deprecated v6 已废弃,请使用 createBoundDouyinFetcher 替代
|
|
9285
|
-
*/
|
|
9286
|
-
const createBoundDouyinApi = (_cookie, _requestConfig) => {
|
|
9287
|
-
return {
|
|
9288
|
-
...douyin,
|
|
9289
|
-
getSearchData: createDeprecatedStub$2("getSearchData")
|
|
9290
|
-
};
|
|
10288
|
+
bilibiliApiUrls
|
|
9291
10289
|
};
|
|
9292
10290
|
//#endregion
|
|
9293
10291
|
//#region src/platform/douyin/routes.ts
|
|
@@ -9341,46 +10339,8 @@ const createDouyinRoutes = (cookie, requestConfig = getDouyinDefaultConfig(cooki
|
|
|
9341
10339
|
/** 抖音相关功能模块 (工具集) */
|
|
9342
10340
|
const douyinUtils = {
|
|
9343
10341
|
sign: douyinSign,
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
};
|
|
9347
|
-
//#endregion
|
|
9348
|
-
//#region src/platform/kuaishou/KuaishouApi.ts
|
|
9349
|
-
/**
|
|
9350
|
-
* 创建废弃的 API 存根函数
|
|
9351
|
-
*/
|
|
9352
|
-
const createDeprecatedStub$1 = (methodName) => {
|
|
9353
|
-
return (..._args) => {
|
|
9354
|
-
checkDeprecation("getKuaishouData");
|
|
9355
|
-
throw new Error(`kuaishou.${methodName} 已废弃,请使用 kuaishouFetcher 替代`);
|
|
9356
|
-
};
|
|
9357
|
-
};
|
|
9358
|
-
/**
|
|
9359
|
-
* 快手相关 API 的命名空间。
|
|
9360
|
-
*
|
|
9361
|
-
* @deprecated v6 已废弃,请使用 kuaishouFetcher 或 client.kuaishou.fetcher 替代
|
|
9362
|
-
*/
|
|
9363
|
-
const kuaishou = {
|
|
9364
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchVideoWork 替代 */
|
|
9365
|
-
getWorkInfo: createDeprecatedStub$1("getWorkInfo"),
|
|
9366
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchWorkComments 替代 */
|
|
9367
|
-
getComments: createDeprecatedStub$1("getComments"),
|
|
9368
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchUserProfile 替代 */
|
|
9369
|
-
getUserProfile: createDeprecatedStub$1("getUserProfile"),
|
|
9370
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchUserWorkList 替代 */
|
|
9371
|
-
getUserWorkList: createDeprecatedStub$1("getUserWorkList"),
|
|
9372
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchLiveRoomInfo 替代 */
|
|
9373
|
-
getLiveRoomInfo: createDeprecatedStub$1("getLiveRoomInfo"),
|
|
9374
|
-
/** @deprecated 请使用 kuaishouFetcher.fetchEmojiList 替代 */
|
|
9375
|
-
getEmojiList: createDeprecatedStub$1("getEmojiList")
|
|
9376
|
-
};
|
|
9377
|
-
/**
|
|
9378
|
-
* 创建绑定了cookie的快手API对象
|
|
9379
|
-
*
|
|
9380
|
-
* @deprecated v6 已废弃,请使用 createBoundKuaishouFetcher 替代
|
|
9381
|
-
*/
|
|
9382
|
-
const createBoundKuaishouApi = (_cookie, _requestConfig) => {
|
|
9383
|
-
return { ...kuaishou };
|
|
10342
|
+
passport: passport_exports,
|
|
10343
|
+
douyinApiUrls
|
|
9384
10344
|
};
|
|
9385
10345
|
//#endregion
|
|
9386
10346
|
//#region src/platform/kuaishou/routes.ts
|
|
@@ -9434,48 +10394,7 @@ const createKuaishouRoutes = (cookie, requestConfig = getKuaishouDefaultConfig(c
|
|
|
9434
10394
|
/** 快手相关功能模块 (工具集) */
|
|
9435
10395
|
const kuaishouUtils = {
|
|
9436
10396
|
sign: kuaishouSign,
|
|
9437
|
-
kuaishouApiUrls
|
|
9438
|
-
api: kuaishou
|
|
9439
|
-
};
|
|
9440
|
-
//#endregion
|
|
9441
|
-
//#region src/platform/xiaohongshu/XiaohongshuApi.ts
|
|
9442
|
-
/**
|
|
9443
|
-
* 创建废弃的 API 存根函数
|
|
9444
|
-
*/
|
|
9445
|
-
const createDeprecatedStub = (methodName) => {
|
|
9446
|
-
return (..._args) => {
|
|
9447
|
-
checkDeprecation("getXiaohongshuData");
|
|
9448
|
-
throw new Error(`xiaohongshu.${methodName} 已废弃,请使用 xiaohongshuFetcher 替代`);
|
|
9449
|
-
};
|
|
9450
|
-
};
|
|
9451
|
-
/**
|
|
9452
|
-
* 封装了所有小红书相关的API请求,采用对象化的方式组织。
|
|
9453
|
-
*
|
|
9454
|
-
* @deprecated v6 已废弃,请使用 xiaohongshuFetcher 或 client.xiaohongshu.fetcher 替代
|
|
9455
|
-
*/
|
|
9456
|
-
const xiaohongshu = {
|
|
9457
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchHomeFeed 替代 */
|
|
9458
|
-
getHomeFeed: createDeprecatedStub("getHomeFeed"),
|
|
9459
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchNoteDetail 替代 */
|
|
9460
|
-
getNote: createDeprecatedStub("getNote"),
|
|
9461
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchNoteComments 替代 */
|
|
9462
|
-
getComments: createDeprecatedStub("getComments"),
|
|
9463
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchUserProfile 替代 */
|
|
9464
|
-
getUser: createDeprecatedStub("getUser"),
|
|
9465
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchUserNoteList 替代 */
|
|
9466
|
-
getUserNotes: createDeprecatedStub("getUserNotes"),
|
|
9467
|
-
/** @deprecated 请使用 xiaohongshuFetcher.searchNotes 替代 */
|
|
9468
|
-
getSearchNotes: createDeprecatedStub("getSearchNotes"),
|
|
9469
|
-
/** @deprecated 请使用 xiaohongshuFetcher.fetchEmojiList 替代 */
|
|
9470
|
-
getEmojiList: createDeprecatedStub("getEmojiList")
|
|
9471
|
-
};
|
|
9472
|
-
/**
|
|
9473
|
-
* 创建绑定了cookie的小红书API对象
|
|
9474
|
-
*
|
|
9475
|
-
* @deprecated v6 已废弃,请使用 createBoundXiaohongshuFetcher 替代
|
|
9476
|
-
*/
|
|
9477
|
-
const createBoundXiaohongshuApi = (_cookie, _requestConfig) => {
|
|
9478
|
-
return { ...xiaohongshu };
|
|
10397
|
+
kuaishouApiUrls
|
|
9479
10398
|
};
|
|
9480
10399
|
//#endregion
|
|
9481
10400
|
//#region src/platform/xiaohongshu/routes.ts
|
|
@@ -9529,8 +10448,7 @@ const createXiaohongshuRoutes = (cookie, requestConfig = getXiaohongshuDefaultCo
|
|
|
9529
10448
|
/** 小红书相关功能模块 (工具集) */
|
|
9530
10449
|
const xiaohongshuUtils = {
|
|
9531
10450
|
sign: xiaohongshuSign,
|
|
9532
|
-
xiaohongshuApiUrls
|
|
9533
|
-
api: xiaohongshu
|
|
10451
|
+
xiaohongshuApiUrls
|
|
9534
10452
|
};
|
|
9535
10453
|
//#endregion
|
|
9536
10454
|
//#region src/server/index.ts
|
|
@@ -9577,38 +10495,6 @@ const createAmagiClient = (options) => {
|
|
|
9577
10495
|
});
|
|
9578
10496
|
return app;
|
|
9579
10497
|
};
|
|
9580
|
-
/**
|
|
9581
|
-
* @deprecated v6 已废弃,请使用 douyin.fetcher 替代
|
|
9582
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
9583
|
-
*/
|
|
9584
|
-
const getDouyinData = (..._args) => {
|
|
9585
|
-
checkDeprecation("getDouyinData");
|
|
9586
|
-
throw new Error("getDouyinData 已废弃");
|
|
9587
|
-
};
|
|
9588
|
-
/**
|
|
9589
|
-
* @deprecated v6 已废弃,请使用 bilibili.fetcher 替代
|
|
9590
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
9591
|
-
*/
|
|
9592
|
-
const getBilibiliData = (..._args) => {
|
|
9593
|
-
checkDeprecation("getBilibiliData");
|
|
9594
|
-
throw new Error("getBilibiliData 已废弃");
|
|
9595
|
-
};
|
|
9596
|
-
/**
|
|
9597
|
-
* @deprecated v6 已废弃,请使用 kuaishou.fetcher 替代
|
|
9598
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
9599
|
-
*/
|
|
9600
|
-
const getKuaishouData = (..._args) => {
|
|
9601
|
-
checkDeprecation("getKuaishouData");
|
|
9602
|
-
throw new Error("getKuaishouData 已废弃");
|
|
9603
|
-
};
|
|
9604
|
-
/**
|
|
9605
|
-
* @deprecated v6 已废弃,请使用 xiaohongshu.fetcher 替代
|
|
9606
|
-
* @throws {DeprecatedApiError} 调用时抛出废弃错误
|
|
9607
|
-
*/
|
|
9608
|
-
const getXiaohongshuData = (..._args) => {
|
|
9609
|
-
checkDeprecation("getXiaohongshuData");
|
|
9610
|
-
throw new Error("getXiaohongshuData 已废弃");
|
|
9611
|
-
};
|
|
9612
10498
|
return {
|
|
9613
10499
|
/** 启动本地HTTP服务 */
|
|
9614
10500
|
startServer,
|
|
@@ -9626,39 +10512,23 @@ const createAmagiClient = (options) => {
|
|
|
9626
10512
|
* @param listener - 事件处理函数 (只触发一次)
|
|
9627
10513
|
*/
|
|
9628
10514
|
once: (event, listener) => amagiEvents.once(event, listener),
|
|
9629
|
-
/** @deprecated v6 已废弃,请使用 douyin.fetcher 替代 */
|
|
9630
|
-
getDouyinData,
|
|
9631
|
-
/** @deprecated v6 已废弃,请使用 bilibili.fetcher 替代 */
|
|
9632
|
-
getBilibiliData,
|
|
9633
|
-
/** @deprecated v6 已废弃,请使用 kuaishou.fetcher 替代 */
|
|
9634
|
-
getKuaishouData,
|
|
9635
|
-
/** @deprecated v6 已废弃,请使用 xiaohongshu.fetcher 替代 */
|
|
9636
|
-
getXiaohongshuData,
|
|
9637
10515
|
douyin: {
|
|
9638
10516
|
...douyinUtils,
|
|
9639
|
-
/** @deprecated 请使用 fetcher 替代 */
|
|
9640
|
-
api: createBoundDouyinApi(douyinCookie, requestConfig),
|
|
9641
10517
|
/** fetcher */
|
|
9642
10518
|
fetcher: createBoundDouyinFetcher(douyinCookie, requestConfig)
|
|
9643
10519
|
},
|
|
9644
10520
|
bilibili: {
|
|
9645
10521
|
...bilibiliUtils,
|
|
9646
|
-
/** @deprecated 请使用 fetcher 替代 */
|
|
9647
|
-
api: createBoundBilibiliApi(bilibiliCookie, requestConfig),
|
|
9648
10522
|
/** fetcher */
|
|
9649
10523
|
fetcher: createBoundBilibiliFetcher(bilibiliCookie, requestConfig)
|
|
9650
10524
|
},
|
|
9651
10525
|
kuaishou: {
|
|
9652
10526
|
...kuaishouUtils,
|
|
9653
|
-
/** @deprecated 请使用 fetcher 替代 */
|
|
9654
|
-
api: createBoundKuaishouApi(kuaishouCookie, requestConfig),
|
|
9655
10527
|
/** fetcher */
|
|
9656
10528
|
fetcher: createBoundKuaishouFetcher(kuaishouCookie, requestConfig)
|
|
9657
10529
|
},
|
|
9658
10530
|
xiaohongshu: {
|
|
9659
10531
|
...xiaohongshuUtils,
|
|
9660
|
-
/** @deprecated 请使用 fetcher 替代 */
|
|
9661
|
-
api: createBoundXiaohongshuApi(xiaohongshuCookie, requestConfig),
|
|
9662
10532
|
/** fetcher */
|
|
9663
10533
|
fetcher: createBoundXiaohongshuFetcher(xiaohongshuCookie, requestConfig)
|
|
9664
10534
|
}
|
|
@@ -9822,7 +10692,6 @@ const BilibiliInternalMethods = {
|
|
|
9822
10692
|
USER_SPACE_INFO: "用户空间详细信息",
|
|
9823
10693
|
USER_TOTAL_VIEWS: "获取UP主总播放量",
|
|
9824
10694
|
DYNAMIC_DETAIL: "动态详情数据",
|
|
9825
|
-
DYNAMIC_CARD: "动态卡片数据",
|
|
9826
10695
|
BANGUMI_INFO: "番剧基本信息数据",
|
|
9827
10696
|
BANGUMI_STREAM: "番剧下载信息数据",
|
|
9828
10697
|
LIVE_ROOM_INFO: "直播间信息",
|
|
@@ -9853,7 +10722,6 @@ const BilibiliFetcherMethods = {
|
|
|
9853
10722
|
USER_SPACE_INFO: "fetchUserSpaceInfo",
|
|
9854
10723
|
USER_TOTAL_VIEWS: "fetchUploaderTotalViews",
|
|
9855
10724
|
DYNAMIC_DETAIL: "fetchDynamicDetail",
|
|
9856
|
-
DYNAMIC_CARD: "fetchDynamicCard",
|
|
9857
10725
|
BANGUMI_INFO: "fetchBangumiInfo",
|
|
9858
10726
|
BANGUMI_STREAM: "fetchBangumiStreamUrl",
|
|
9859
10727
|
LIVE_ROOM_INFO: "fetchLiveRoomInfo",
|
|
@@ -9962,7 +10830,6 @@ const BilibiliMethodToFetcher = {
|
|
|
9962
10830
|
[BilibiliInternalMethods.USER_SPACE_INFO]: BilibiliFetcherMethods.USER_SPACE_INFO,
|
|
9963
10831
|
[BilibiliInternalMethods.USER_TOTAL_VIEWS]: BilibiliFetcherMethods.USER_TOTAL_VIEWS,
|
|
9964
10832
|
[BilibiliInternalMethods.DYNAMIC_DETAIL]: BilibiliFetcherMethods.DYNAMIC_DETAIL,
|
|
9965
|
-
[BilibiliInternalMethods.DYNAMIC_CARD]: BilibiliFetcherMethods.DYNAMIC_CARD,
|
|
9966
10833
|
[BilibiliInternalMethods.BANGUMI_INFO]: BilibiliFetcherMethods.BANGUMI_INFO,
|
|
9967
10834
|
[BilibiliInternalMethods.BANGUMI_STREAM]: BilibiliFetcherMethods.BANGUMI_STREAM,
|
|
9968
10835
|
[BilibiliInternalMethods.LIVE_ROOM_INFO]: BilibiliFetcherMethods.LIVE_ROOM_INFO,
|
|
@@ -10094,7 +10961,6 @@ const BilibiliMethodMapping = {
|
|
|
10094
10961
|
用户空间详细信息: "fetchUserSpaceInfo",
|
|
10095
10962
|
获取UP主总播放量: "fetchUploaderTotalViews",
|
|
10096
10963
|
动态详情数据: "fetchDynamicDetail",
|
|
10097
|
-
动态卡片数据: "fetchDynamicCard",
|
|
10098
10964
|
番剧基本信息数据: "fetchBangumiInfo",
|
|
10099
10965
|
番剧下载信息数据: "fetchBangumiStreamUrl",
|
|
10100
10966
|
直播间信息: "fetchLiveRoomInfo",
|
|
@@ -10177,7 +11043,6 @@ const BilibiliApiRoutes = {
|
|
|
10177
11043
|
userSpaceInfo: "/user/space",
|
|
10178
11044
|
uploaderTotalViews: "/user/total-views",
|
|
10179
11045
|
dynamicDetail: "/dynamic",
|
|
10180
|
-
dynamicCard: "/dynamic/card",
|
|
10181
11046
|
bangumiInfo: "/bangumi",
|
|
10182
11047
|
bangumiStream: "/bangumi/stream",
|
|
10183
11048
|
liveRoomInfo: "/live",
|
|
@@ -10247,14 +11112,10 @@ function getApiRoute(platform, methodType) {
|
|
|
10247
11112
|
* 构建后使用 __VERSION__,开发环境从 package.json 读取
|
|
10248
11113
|
*/
|
|
10249
11114
|
const getVersion = () => {
|
|
10250
|
-
return "6.
|
|
11115
|
+
return "6.6.0";
|
|
10251
11116
|
};
|
|
10252
11117
|
const VERSION = getVersion();
|
|
10253
11118
|
/**
|
|
10254
|
-
* @deprecated 请使用 createAmagiClient 替代
|
|
10255
|
-
*/
|
|
10256
|
-
const amagiClient = createAmagiClient;
|
|
10257
|
-
/**
|
|
10258
11119
|
* 创建一个新的 amagi 客户端实例
|
|
10259
11120
|
* 用于创建和初始化一个新的 amagi 客户端实例,支持通过 new 关键字或函数调用方式使用
|
|
10260
11121
|
* @param options - cookies 配置选项,用于设置客户端的 cookies 相关参数
|
|
@@ -10274,10 +11135,6 @@ CreateAmagiApp.douyin = douyinUtils;
|
|
|
10274
11135
|
CreateAmagiApp.bilibili = bilibiliUtils;
|
|
10275
11136
|
CreateAmagiApp.kuaishou = kuaishouUtils;
|
|
10276
11137
|
CreateAmagiApp.xiaohongshu = xiaohongshuUtils;
|
|
10277
|
-
CreateAmagiApp.getDouyinData = getDouyinData;
|
|
10278
|
-
CreateAmagiApp.getBilibiliData = getBilibiliData;
|
|
10279
|
-
CreateAmagiApp.getKuaishouData = getKuaishouData;
|
|
10280
|
-
CreateAmagiApp.getXiaohongshuData = getXiaohongshuData;
|
|
10281
11138
|
CreateAmagiApp.events = amagiEvents;
|
|
10282
11139
|
CreateAmagiApp.on = amagiEvents.on.bind(amagiEvents);
|
|
10283
11140
|
CreateAmagiApp.once = amagiEvents.once.bind(amagiEvents);
|
|
@@ -10300,6 +11157,4 @@ const amagi = Client;
|
|
|
10300
11157
|
* GPL-3.0 Licensed
|
|
10301
11158
|
*/
|
|
10302
11159
|
//#endregion
|
|
10303
|
-
export { AdditionalType, ApiError, BilibiliApiRoutes, BilibiliApplyCaptchaParamsSchema, BilibiliArticleCardParamsSchema, BilibiliArticleInfoParamsSchema, BilibiliArticleParamsSchema, BilibiliAv2BvParamsSchema, BilibiliBangumiInfoParamsSchema, BilibiliBangumiStreamParamsSchema, BilibiliBv2AvParamsSchema, BilibiliColumnInfoParamsSchema, BilibiliCommentParamsSchema, BilibiliCommentReplyParamsSchema, BilibiliDanmakuParamsSchema, BilibiliDynamicParamsSchema, BilibiliEmojiParamsSchema, BilibiliFetcherMethods, BilibiliInternalMethods, BilibiliLiveParamsSchema, BilibiliLoginParamsSchema, BilibiliMethodMapping, BilibiliMethodRoutes, BilibiliMethodToFetcher, BilibiliQrcodeParamsSchema, BilibiliQrcodeStatusParamsSchema, BilibiliUserParamsSchema, BilibiliValidateCaptchaParamsSchema, BilibiliValidationSchemas, BilibiliVideoDownloadParamsSchema, BilibiliVideoParamsSchema, CommentType, CreateApp, DouyinApiRoutes, DouyinCommentParamsSchema, DouyinCommentReplyParamsSchema, DouyinDanmakuParamsSchema, DouyinEmojiListParamsSchema, DouyinEmojiProParamsSchema, DouyinFetcherMethods, DouyinHotWordsParamsSchema, DouyinInternalMethods, DouyinLiveRoomParamsSchema, DouyinMethodMapping, DouyinMethodRoutes, DouyinMethodToFetcher, DouyinMusicParamsSchema, DouyinQrcodeParamsSchema, DouyinSearchParamsSchema, DouyinUserListParamsSchema, DouyinUserParamsSchema, DouyinValidationSchemas, DouyinWorkParamsSchema, DynamicType, KuaishouApiRoutes, KuaishouCommentParamsSchema, KuaishouEmojiParamsSchema, KuaishouFetcherMethods, KuaishouInternalMethods, KuaishouLiveRoomInfoParamsSchema, KuaishouMethodMapping, KuaishouMethodRoutes, KuaishouMethodToFetcher, KuaishouUserProfileParamsSchema, KuaishouUserWorkListParamsSchema, KuaishouValidationSchemas, KuaishouVideoParamsSchema, MajorType, MethodMaps, ValidationError, XiaohongshuApiRoutes, XiaohongshuFetcherMethods, XiaohongshuInternalMethods, XiaohongshuMethodMapping, XiaohongshuMethodRoutes, XiaohongshuMethodToFetcher, XiaohongshuValidationSchemas, amagi,
|
|
10304
|
-
|
|
10305
|
-
//# sourceMappingURL=index.mjs.map
|
|
11160
|
+
export { AdditionalType, ApiError, BilibiliApiRoutes, BilibiliApplyCaptchaParamsSchema, BilibiliArticleCardParamsSchema, BilibiliArticleInfoParamsSchema, BilibiliArticleParamsSchema, BilibiliAv2BvParamsSchema, BilibiliBangumiInfoParamsSchema, BilibiliBangumiStreamParamsSchema, BilibiliBv2AvParamsSchema, BilibiliColumnInfoParamsSchema, BilibiliCommentParamsSchema, BilibiliCommentReplyParamsSchema, BilibiliDanmakuParamsSchema, BilibiliDynamicParamsSchema, BilibiliEmojiParamsSchema, BilibiliFetcherMethods, BilibiliInternalMethods, BilibiliLiveParamsSchema, BilibiliLoginParamsSchema, BilibiliMethodMapping, BilibiliMethodRoutes, BilibiliMethodToFetcher, BilibiliQrcodeParamsSchema, BilibiliQrcodeStatusParamsSchema, BilibiliUserParamsSchema, BilibiliValidateCaptchaParamsSchema, BilibiliValidationSchemas, BilibiliVideoDownloadParamsSchema, BilibiliVideoParamsSchema, CommentType, CreateApp, DouyinApiRoutes, DouyinCommentParamsSchema, DouyinCommentReplyParamsSchema, DouyinDanmakuParamsSchema, DouyinEmojiListParamsSchema, DouyinEmojiProParamsSchema, DouyinFetcherMethods, DouyinHotWordsParamsSchema, DouyinInternalMethods, DouyinLiveRoomParamsSchema, DouyinMethodMapping, DouyinMethodRoutes, DouyinMethodToFetcher, DouyinMusicParamsSchema, DouyinQrcodeParamsSchema, DouyinSearchParamsSchema, DouyinUserListParamsSchema, DouyinUserParamsSchema, DouyinValidationSchemas, DouyinWorkParamsSchema, DynamicType, KuaishouApiRoutes, KuaishouCommentParamsSchema, KuaishouEmojiParamsSchema, KuaishouFetcherMethods, KuaishouInternalMethods, KuaishouLiveRoomInfoParamsSchema, KuaishouMethodMapping, KuaishouMethodRoutes, KuaishouMethodToFetcher, KuaishouUserProfileParamsSchema, KuaishouUserWorkListParamsSchema, KuaishouValidationSchemas, KuaishouVideoParamsSchema, MajorType, MethodMaps, ValidationError, XiaohongshuApiRoutes, XiaohongshuFetcherMethods, XiaohongshuInternalMethods, XiaohongshuMethodMapping, XiaohongshuMethodRoutes, XiaohongshuMethodToFetcher, XiaohongshuValidationSchemas, amagi, amagiEvents, av2bv, bilibiliApiUrls, bilibiliErrorCodeMap, bilibiliFetcher, bilibiliUtils, bv2av, checkPassportQrcode, createAmagiClient, createBilibiliRoutes, createBilibiliRoutes as registerBilibiliRoutes, createBoundBilibiliFetcher, createBoundDouyinFetcher, createBoundKuaishouFetcher, createBoundXiaohongshuFetcher, createDouyinRoutes, createDouyinRoutes as registerDouyinRoutes, createErrorResponse, createKuaishouRoutes, createKuaishouRoutes as registerKuaishouRoutes, createSuccessResponse, createXiaohongshuRoutes, createXiaohongshuRoutes as registerXiaohongshuRoutes, Client as default, douyinApiUrls, douyinFetcher, passport_exports as douyinPassport, douyinSign, douyinUtils, emitApiError, emitApiSuccess, emitHttpRequest, emitHttpResponse, emitLog, emitLogDebug, emitLogError, emitLogInfo, emitLogMark, emitLogWarn, emitNetworkError, emitNetworkRetry, fetchData, fetchResponse, getApiRoute, getEnglishMethodName, getHeadersAndData, handleError, isNetworkErrorResult, isSmsCodeVerifyWay, kuaishouApiUrls, kuaishouFetcher, kuaishouSign, kuaishouUtils, parseDmSegMobileReply, qtparam, requestPassportQrcode, sendPassportVerifyCode, toFetcherMethod, validateBilibiliParams, validateDouyinParams, validateKuaishouParams, validatePassportVerifyCode, validateXiaohongshuParams, wbi_sign, xiaohongshuApiUrls, xiaohongshuFetcher, xiaohongshuSign, xiaohongshuUtils };
|