karin-plugin-kkk 2.42.1 → 2.42.3

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.
@@ -1,17 +1,17 @@
1
1
  globalThis.__KTR_BUNDLED__ = true;
2
- import { i as __toESM } from "./rolldown-runtime.js";
2
+ import { i as __toESM, n as __exportAll } from "./rolldown-runtime.js";
3
3
  import { $ as RiTrophyFill, $n as Camera, $t as Share2, A as FaMusic, An as Gamepad2, At as format, B as RiGroupLine, Bn as Copy, Bt as Upload, C as react, Cn as LayoutTemplate, Cr as zod_default, Ct as a$1, D as MdLocationOn, Dn as Hash, Dt as parse, E as MdLightbulbOutline, En as Heart, Et as zhCN, F as AiFillPushpin, Fn as ExternalLink, Ft as Video, G as RiLiveLine, Gn as CircleQuestionMark, Gt as Star, H as RiHeart3Fill, Hn as Clock, Ht as Trash2, I as AiFillStar, In as Download, It as Users, J as RiShareForwardFill, Jn as CircleCheck, Jt as Sparkles, K as RiMessage3Fill, Kn as CircleFadingArrowUp, Kt as Square, L as AiOutlineVideoCamera, Ln as Crown, Lt as UsersRound, M as FaUserGroup, Mn as FilePlay, Mt as Zap, N as BiImage, Nn as Eye, Nt as X, O as MdSchedule, On as GitBranch, Ot as fromUnixTime, P as AiFillHeart, Pn as EyeOff, Pt as WandSparkles, Q as RiTiktokFill, Qn as ChartColumn, Qt as ShieldCheck, R as Markdown, Rn as Cpu, Rt as User, S as sqlite, Sn as Link, Sr as Xhshow, St as e, T as MdInfoOutline, Tn as Image$1, Tt as c, U as RiHeart3Line, Un as Clock3, Ut as Terminal, V as RiHashtag, Vn as Code, Vt as TriangleAlert, W as RiHeartLine, Wn as Clapperboard, Wt as Sun, X as RiStarLine, Xn as CircleAlert, Xt as ShoppingBag, Y as RiStarFill, Yn as CircleCheckBig, Yt as Smartphone, Z as RiThumbUpFill, Zn as Check, Zt as Shield, _ as webassembly, _n as MessageCircle, _r as defineTemplate, _t as o, an as RefreshCw, ar as BellRing, at as SiSamsung, b as tsdown, bn as MapPin, br as CryptoConfig, bt as a, c as require_heic_decode, cn as QrCode, cr as Button, ct as SiHuawei, d as VictoryLine, dn as Play, dr as require_jsx_runtime, dt as SiGithub, en as Settings2, er as Calendar, et as RiUserFollowLine, f as VictoryChart, fn as Pencil, fr as createTemplateRenderer, ft as SiBilibili, g as rehypeHighlight, gn as Monitor, gr as HtmlWrapper, gt as r, h as VictoryTheme, hn as Moon, hr as require_react, ht as o$3, in as RotateCcw, ir as BookOpenText, it as SiVivo, j as FaTiktok, jn as FileText, jt as differenceInSeconds, k as FaCommentDots, kn as Gift, kt as formatDistanceToNow, l as VictoryScatter, ln as Puzzle, lr as twMerge, lt as SiHonor, m as VictoryLabel, mn as Music, mr as require_server_node, mt as SiAnthropic, n as require_lib, nn as ScanLine, nr as Bot, nt as RiVideoLine, on as Radio, or as ArrowDownToLine, ot as SiOppo, p as VictoryAxis, pn as Palette, pr as resolveTemplateStyle, pt as SiApple, q as RiPieChart2Fill, qn as CircleEllipsis, qt as SquarePen, rn as Save, rr as Bookmark, rt as SiXiaomi, s as require_jpeg_js, sn as Quote, sr as Chip, st as SiOneplus, t as createProxyMiddleware, tn as Search, tr as Box, tt as RiVerifiedBadgeFill, u as VictoryPie, un as Plus, ur as clsx, ut as SiGooglephotos, v as vite, vn as Menu, vr as Chalk, vt as o$1, w as MdFitScreen, wn as Info, wt as o$2, x as tailwind_css, xn as LoaderCircle, xr as FingerprintGenerator, xt as n, y as typescript, yn as Maximize, yr as require_protobufjs, yt as m, z as RiArrowRightFill, zn as CornerDownLeft, zt as UserPlus } from "./vendor.js";
4
4
  import "node:module";
5
5
  import fs from "node:fs";
6
6
  import path, { resolve } from "node:path";
7
7
  import URL$2, { fileURLToPath } from "node:url";
8
- import karin, { BOT_CONNECT, app, authMiddleware, checkPkgUpdate, checkPort, common, config, createBadRequestResponse, createNotFoundResponse, createServerErrorResponse, createSuccessResponse, db, defineConfig, ffmpeg, ffprobe, hooks, isDocker, karin as karin$1, karinPathHtml, karinPathTemp, logger, logs, mkdirSync, parseChangelog, range, render, restart, segment, updatePkg, watch } from "node-karin";
8
+ import karin, { BOT_CONNECT, app, authMiddleware, checkPkgUpdate, checkPort, common, config, createBadRequestResponse, createNotFoundResponse, createServerErrorResponse, createSuccessResponse, db, defineConfig, ffmpeg, ffprobe, hooks, isDocker, karin as karin$1, karinPathHtml, logger, logs, mkdirSync, parseChangelog, range, render, restart, segment, updatePkg, watch } from "node-karin";
9
9
  import { EventEmitter } from "node:events";
10
10
  import crypto, { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
11
11
  import axios, { AxiosError } from "node-karin/axios";
12
12
  import express from "node-karin/express";
13
- import os, { platform } from "node:os";
14
- import { karinPathBase, karinPathTemp as karinPathTemp$1 } from "node-karin/root";
13
+ import os from "node:os";
14
+ import { karinPathBase, karinPathTemp } from "node-karin/root";
15
15
  import sqlite3 from "node-karin/sqlite3";
16
16
  import YAML from "node-karin/yaml";
17
17
  import util from "node:util";
@@ -21,8 +21,6 @@ import { pipeline } from "node:stream/promises";
21
21
  import { inflateSync } from "node:zlib";
22
22
  import { embedWatermarkToPngBytes } from "@ikenxuan/watermark";
23
23
  import _ from "node-karin/lodash";
24
- import { snapka } from "@karinjs/plugin-puppeteer";
25
- import { newInjectedPage } from "fingerprint-injector";
26
24
  //#region src/index.ts
27
25
  globalThis.__kkkLoadStart ??= process.hrtime.bigint();
28
26
  import("./setup.js");
@@ -2176,140 +2174,1410 @@ var bilibiliFetcher$1 = {
2176
2174
  fetchEmojiList: fetchEmojiList$3
2177
2175
  };
2178
2176
  //#endregion
2179
- //#region ../amagi/packages/core/src/platform/defaultConfigs.ts
2177
+ //#region ../amagi/packages/core/src/platform/douyin/passport/sm3.ts
2180
2178
  /**
2181
- * 根据User-Agent生成对应的Sec-Ch-Ua值
2182
- * @param userAgent - 用户代理字符串
2183
- * @returns 对应的Sec-Ch-Ua值
2184
- */
2185
- var generateSecChUa = (userAgent) => {
2186
- const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
2187
- const chromeVersion = chromeMatch ? chromeMatch[1] : "125";
2188
- return `"Not_A Brand";v="99", "Chromium";v="${chromeVersion}", "Google Chrome";v="${chromeVersion}"`;
2179
+ * SM3 摘要(GM/T 0004-2012),抖音 bdms 签名链使用的变体
2180
+ *
2181
+ * 与标准实现的唯一差异:字符串按 `charCodeAt` 逐字符取字节(非 UTF-8 编码),
2182
+ * 与浏览器里 bdms 的 `strToBytes` 行为一致。签名输入均为 ASCII,实际不会踩到多字节分支,
2183
+ * 但仍保留该分支以保证与浏览器实现逐位一致。
2184
+ */
2185
+ /** 循环左移 32 位 */
2186
+ var rotl = (x, n) => {
2187
+ const shift = n % 32;
2188
+ return (x << shift | x >>> 32 - shift) >>> 0;
2189
+ };
2190
+ /** 轮常量 Tj */
2191
+ var tj = (j) => j < 16 ? 2043430169 : 2055708042;
2192
+ /** 布尔函数 FFj */
2193
+ var ff = (j, x, y, z) => j < 16 ? (x ^ y ^ z) >>> 0 : (x & y | x & z | y & z) >>> 0;
2194
+ /** 布尔函数 GGj */
2195
+ var gg = (j, x, y, z) => j < 16 ? (x ^ y ^ z) >>> 0 : (x & y | ~x & z) >>> 0;
2196
+ /** 初始向量 IV */
2197
+ var IV = [
2198
+ 1937774191,
2199
+ 1226093241,
2200
+ 388252375,
2201
+ 3666478592,
2202
+ 2842636476,
2203
+ 372324522,
2204
+ 3817729613,
2205
+ 2969243214
2206
+ ];
2207
+ /** 字符串转字节数组:逐字符 charCodeAt,大于一字节时按大端拆分 */
2208
+ var strToBytes = (input) => {
2209
+ const bytes = [];
2210
+ for (let i = 0; i < input.length; i++) {
2211
+ let code = input.charCodeAt(i);
2212
+ const chunk = [];
2213
+ do {
2214
+ chunk.push(code & 255);
2215
+ code >>= 8;
2216
+ } while (code);
2217
+ bytes.push(...chunk.reverse());
2218
+ }
2219
+ return bytes;
2220
+ };
2221
+ /** 消息扩展:生成 W[0..67] 与 W'[0..63](后者放在 68 之后) */
2222
+ var expand = (block) => {
2223
+ const w = new Array(132);
2224
+ 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;
2225
+ for (let j = 16; j < 68; j++) {
2226
+ let x = w[j - 16] ^ w[j - 9] ^ rotl(w[j - 3], 15);
2227
+ x = x ^ rotl(x, 15) ^ rotl(x, 23);
2228
+ w[j] = (x ^ rotl(w[j - 13], 7) ^ w[j - 6]) >>> 0;
2229
+ }
2230
+ for (let j = 0; j < 64; j++) w[j + 68] = (w[j] ^ w[j + 4]) >>> 0;
2231
+ return w;
2232
+ };
2233
+ /** SM3 压缩函数:就地更新寄存器 */
2234
+ var compress = (reg, block) => {
2235
+ const w = expand(block);
2236
+ const r = reg.slice();
2237
+ for (let j = 0; j < 64; j++) {
2238
+ let ss1 = rotl(r[0], 12) + r[4] + rotl(tj(j), j) & 4294967295;
2239
+ ss1 = rotl(ss1 >>> 0, 7);
2240
+ const ss2 = (ss1 ^ rotl(r[0], 12)) >>> 0;
2241
+ const tt1 = ff(j, r[0], r[1], r[2]) + r[3] + ss2 + w[j + 68] >>> 0;
2242
+ const tt2 = gg(j, r[4], r[5], r[6]) + r[7] + ss1 + w[j] >>> 0;
2243
+ r[3] = r[2];
2244
+ r[2] = rotl(r[1], 9);
2245
+ r[1] = r[0];
2246
+ r[0] = tt1;
2247
+ r[7] = r[6];
2248
+ r[6] = rotl(r[5], 19);
2249
+ r[5] = r[4];
2250
+ r[4] = (tt2 ^ rotl(tt2, 9) ^ rotl(tt2, 17)) >>> 0;
2251
+ }
2252
+ for (let i = 0; i < 8; i++) reg[i] = (reg[i] ^ r[i]) >>> 0;
2253
+ };
2254
+ /** 按 SM3 规则填充消息(0x80 + 0 + 64 位比特长度) */
2255
+ var pad = (bytes) => {
2256
+ const padded = bytes.slice();
2257
+ const bitLength = bytes.length * 8;
2258
+ padded.push(128);
2259
+ while (padded.length % 64 !== 56) padded.push(0);
2260
+ const high = Math.floor(bitLength / 4294967296);
2261
+ for (let i = 0; i < 4; i++) padded.push(high >>> (3 - i) * 8 & 255);
2262
+ for (let i = 0; i < 4; i++) padded.push(bitLength >>> (3 - i) * 8 & 255);
2263
+ return padded;
2264
+ };
2265
+ /**
2266
+ * 计算 SM3 摘要
2267
+ * @param message 待摘要的字符串或字节数组
2268
+ * @returns 32 字节摘要
2269
+ */
2270
+ var sm3 = (message) => {
2271
+ const bytes = typeof message === "string" ? strToBytes(message) : message;
2272
+ const reg = IV.slice();
2273
+ const padded = pad(bytes);
2274
+ for (let i = 0; i < padded.length; i += 64) compress(reg, padded.slice(i, i + 64));
2275
+ const digest = new Array(32);
2276
+ for (let i = 0; i < 8; i++) {
2277
+ digest[i * 4] = reg[i] >>> 24 & 255;
2278
+ digest[i * 4 + 1] = reg[i] >>> 16 & 255;
2279
+ digest[i * 4 + 2] = reg[i] >>> 8 & 255;
2280
+ digest[i * 4 + 3] = reg[i] & 255;
2281
+ }
2282
+ return digest;
2189
2283
  };
2190
2284
  /**
2191
- * 抖音平台默认请求配置
2192
- * @param cookie - 用户Cookie
2193
- * @param requestConfig - 外部请求配置(优先级最高)
2194
- * @returns 合并后的请求配置
2285
+ * 连续两次 SM3(bdms 对 URL 与盐值的处理方式)
2286
+ * @param message 待摘要的字符串或字节数组
2287
+ * @returns 32 字节摘要
2195
2288
  */
2196
- var getDouyinDefaultConfig = (cookie, requestConfig) => {
2197
- let finalUserAgent = requestConfig?.headers?.["User-Agent"] ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
2198
- finalUserAgent = finalUserAgent.replace(/\s+Edg\/[\d.]+/g, "");
2199
- const defHeaders = {
2200
- Accept: "application/json, text/plain, */*",
2201
- "Accept-Encoding": "gzip, deflate, br, zstd",
2202
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
2203
- Cookie: cookie?.trim() ?? "",
2204
- Priority: "u=1, i",
2205
- Referer: "https://www.douyin.com/",
2206
- "Sec-Ch-Ua": generateSecChUa(finalUserAgent),
2207
- "Sec-Ch-Ua-Mobile": "?0",
2208
- "Sec-Ch-Ua-Platform": "\"Windows\"",
2209
- "Sec-Fetch-Dest": "empty",
2210
- "Sec-Fetch-Mode": "cors",
2211
- "Sec-Fetch-Site": "same-origin",
2212
- "User-Agent": finalUserAgent
2289
+ var sm3Twice = (message) => sm3(sm3(message));
2290
+ /**
2291
+ * 十六进制摘要,仅用于测试与排查
2292
+ * @param message 待摘要的字符串或字节数组
2293
+ */
2294
+ var sm3Hex = (message) => sm3(message).map((byte) => byte.toString(16).padStart(2, "0")).join("");
2295
+ //#endregion
2296
+ //#region ../amagi/packages/core/src/platform/douyin/passport/aBogus.ts
2297
+ /**
2298
+ * a_bogus 签名(bdms 1.0.1.19 形态,passport 登录接口使用)
2299
+ *
2300
+ * 抖音同时在线着多个 a_bogus 版本:`@ikenxuan/amagi` 里带的是 web 数据接口用的旧版
2301
+ * (盐值 `cus`、pageId 6241),而 login.douyin.com 的 passport SDK 用的是本文件实现的
2302
+ * 新版(盐值 `dhzx`、pageId 7571、sdkVersion 1.0.1.19-fix.01)。两者互不通用,
2303
+ * 所以这里单独实现一份,不复用 amagi 的签名。
2304
+ *
2305
+ * 算法为社区公开的逆向结论(见 PR 说明中的参考链接),此处按 kkk 的代码风格重写:
2306
+ * - SM3 为抖音变体(连续两次摘要),见 ./sm3
2307
+ * - RC4 为变体:S 盒递减初始化 + `j = (j * S[i] + j + K[i]) % 256`
2308
+ * - Base64 使用自定义字符表,UA 段用 s3、最终结果用 s4
2309
+ * - `random()` 保留了原实现里 `Math.random`(函数对象而非调用)参与位运算的行为,
2310
+ * 该表达式恒为 NaN → 位运算结果恒为 0,这里直接写成常量而不是照抄错误代码
2311
+ */
2312
+ /** RC4 / S 盒长度 */
2313
+ var BOX_SIZE = 256;
2314
+ /** bdms 1.0.1.19 的盐值 */
2315
+ var SALT = "dhzx";
2316
+ /** bdms SDK 版本号,同时也是 passport 通用参数里的 p_bd */
2317
+ var BDMS_SDK_VERSION = "1.0.1.19-fix.01";
2318
+ /** 版本号基准时间戳(bdms 内部按 14 天为一档计数) */
2319
+ var VERSION_EPOCH = 17218368e5;
2320
+ /** 模块加载时刻,等价于浏览器里的「进入页面时间」 */
2321
+ var ENTER_PAGE_TS = Date.now();
2322
+ /** 自定义 Base64 字符表 */
2323
+ var BASE64_TABLES = {
2324
+ s0: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
2325
+ s1: "Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
2326
+ s2: "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=",
2327
+ s3: "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe",
2328
+ s4: "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe"
2329
+ };
2330
+ /**
2331
+ * 浏览器环境快照。服务器上没有真实窗口,这里给出一组常见的桌面分辨率组合;
2332
+ * 该值只影响指纹内容本身,不需要与任何真实设备对应。
2333
+ */
2334
+ var BROWSER_ENV = {
2335
+ innerWidth: 2048,
2336
+ innerHeight: 960,
2337
+ outerWidth: 2554,
2338
+ outerHeight: 1386,
2339
+ availWidth: 2560,
2340
+ availHeight: 1392,
2341
+ sizeWidth: 2560,
2342
+ sizeHeight: 1440,
2343
+ platform: "Win32"
2344
+ };
2345
+ /** RC4 密钥:bdms 用 `[1 / 256, 1 % 256, 14 % 256]` 造出 "\x00\x01\x0e" */
2346
+ var rc4Key = String.fromCharCode(...[
2347
+ 1 / BOX_SIZE,
2348
+ 1 % BOX_SIZE,
2349
+ 14 % BOX_SIZE
2350
+ ]);
2351
+ /**
2352
+ * 变体 RC4
2353
+ * @param key 密钥
2354
+ * @param text 明文(按 charCode 处理)
2355
+ */
2356
+ var rc4 = (key, text) => {
2357
+ const s = new Uint8Array(BOX_SIZE);
2358
+ const k = new Uint8Array(BOX_SIZE);
2359
+ for (let i = 0; i < BOX_SIZE; i++) {
2360
+ s[i] = BOX_SIZE - 1 - i;
2361
+ k[i] = key.charCodeAt(i % key.length);
2362
+ }
2363
+ let j = 0;
2364
+ for (let i = 0; i < BOX_SIZE; i++) {
2365
+ j = (j * s[i] + j + k[i]) % BOX_SIZE;
2366
+ [s[i], s[j]] = [s[j], s[i]];
2367
+ }
2368
+ let x = 0;
2369
+ let y = 0;
2370
+ let cipher = "";
2371
+ for (let n = 0; n < text.length; n++) {
2372
+ x = (x + 1) % BOX_SIZE;
2373
+ y = (y + s[x]) % BOX_SIZE;
2374
+ [s[x], s[y]] = [s[y], s[x]];
2375
+ cipher += String.fromCharCode(text.charCodeAt(n) ^ s[(s[x] + s[y]) % BOX_SIZE]);
2376
+ }
2377
+ return cipher;
2378
+ };
2379
+ /**
2380
+ * 自定义字符表 Base64
2381
+ * @param input 明文(按 charCode 取低 8 位)
2382
+ * @param table 字符表名(s0 ~ s4)
2383
+ */
2384
+ var base64 = (input, table) => {
2385
+ const alphabet = BASE64_TABLES[table];
2386
+ let output = "";
2387
+ let i = 0;
2388
+ while (i < input.length) {
2389
+ const c1 = input.charCodeAt(i++);
2390
+ const c2 = input.charCodeAt(i++);
2391
+ const c3 = input.charCodeAt(i++);
2392
+ const chunk = (c1 & 255) << 16 | (Number.isNaN(c2) ? 0 : (c2 & 255) << 8) | (Number.isNaN(c3) ? 0 : c3 & 255);
2393
+ output += alphabet.charAt(chunk >> 18 & 63);
2394
+ output += alphabet.charAt(chunk >> 12 & 63);
2395
+ output += Number.isNaN(c2) ? "=" : alphabet.charAt(chunk >> 6 & 63);
2396
+ output += Number.isNaN(c3) ? "=" : alphabet.charAt(chunk & 63);
2397
+ }
2398
+ return output;
2399
+ };
2400
+ /**
2401
+ * 生成 4 字节随机混淆段
2402
+ * @param seed 两字节的基准值
2403
+ * @param flag 0 = 完全随机;1 = 高位固定为 0;2 = 低位受限、高位固定为 178
2404
+ */
2405
+ var mix = (seed, flag) => {
2406
+ const r = Math.random() * 65535 | 0;
2407
+ let low = r & 255;
2408
+ let high = r >> 8 & 255;
2409
+ if (flag === 1) high = 0;
2410
+ if (flag === 2) {
2411
+ low = Math.random() * 240 >> 0;
2412
+ if (low > 109) low += low % 2 + 1;
2413
+ high = 178;
2414
+ }
2415
+ return [
2416
+ low & 170 | seed[0] & 85,
2417
+ low & 85 | seed[0] & 170,
2418
+ high & 170 | seed[1] & 85,
2419
+ high & 85 | seed[1] & 170
2420
+ ];
2421
+ };
2422
+ /** SDK 版本号拆成数字数组,非纯数字段(如 `19-fix`)取 0 */
2423
+ var versionSegments = (version) => version.split(".").map((segment) => ~~Number(segment));
2424
+ /** 每 3 字节扩成 4 字节,掺入随机位 */
2425
+ var spread = (bytes) => {
2426
+ const masks = [
2427
+ 145,
2428
+ 110,
2429
+ 66,
2430
+ 189,
2431
+ 44,
2432
+ 211
2433
+ ];
2434
+ const out = [];
2435
+ for (let i = 0; i < bytes.length; i += 3) {
2436
+ if (i + 2 >= bytes.length) {
2437
+ out.push(bytes[i]);
2438
+ if (bytes[i + 1] !== void 0) out.push(bytes[i + 1]);
2439
+ continue;
2440
+ }
2441
+ const noise = Math.random() * 1e3 & 255;
2442
+ 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]);
2443
+ }
2444
+ return out;
2445
+ };
2446
+ /**
2447
+ * 生成 a_bogus
2448
+ * @param query 除 a_bogus 之外的完整查询串(未加 `?`,保持实际发送顺序)
2449
+ * @param userAgent 与请求头一致的 UA
2450
+ * @returns a_bogus 参数值(未做 URL 编码)
2451
+ */
2452
+ var aBogus = (query, userAgent) => {
2453
+ const queryDigest = sm3Twice(query.endsWith(SALT) ? query : query + SALT);
2454
+ const saltDigest = sm3Twice(SALT);
2455
+ const uaDigest = sm3(base64(rc4(rc4Key, userAgent), "s3"));
2456
+ const now = Date.now();
2457
+ const ink = now - 1;
2458
+ /** 指纹字节表,下标沿用 bdms 内部编号,便于与逆向资料对照 */
2459
+ const b = {};
2460
+ b[24] = 41;
2461
+ b[26] = (now - VERSION_EPOCH) / 1e3 / 60 / 60 / 24 / 14 >> 0;
2462
+ b[27] = 6;
2463
+ b[28] = now - ENTER_PAGE_TS + 3 & 255;
2464
+ b[29] = now & 255;
2465
+ b[30] = now >> 8 & 255;
2466
+ b[31] = now >> 16 & 255;
2467
+ b[32] = now >> 24 & 255;
2468
+ b[33] = now / 2 ** 32 & 255;
2469
+ b[34] = now / 2 ** 40 & 255;
2470
+ b[35] = 1;
2471
+ b[36] = 0;
2472
+ b[38] = 129;
2473
+ b[39] = 0;
2474
+ b[40] = 0;
2475
+ b[41] = 0;
2476
+ b[42] = 0;
2477
+ b[43] = 0;
2478
+ b[44] = 14;
2479
+ b[45] = 0;
2480
+ b[46] = 0;
2481
+ b[47] = 0;
2482
+ b[48] = queryDigest[9];
2483
+ b[49] = queryDigest[18];
2484
+ b[51] = queryDigest[3];
2485
+ b[52] = saltDigest[10];
2486
+ b[53] = saltDigest[19];
2487
+ b[55] = saltDigest[4];
2488
+ b[56] = uaDigest[11];
2489
+ b[57] = uaDigest[21];
2490
+ b[59] = uaDigest[5];
2491
+ b[60] = ink & 255;
2492
+ b[61] = ink >> 8 & 255;
2493
+ b[62] = ink >> 16 & 255;
2494
+ b[63] = ink >> 24 & 255;
2495
+ b[64] = ink / 2 ** 32 & 255;
2496
+ b[65] = ink / 2 ** 40 & 255;
2497
+ b[66] = 3;
2498
+ b[67] = 147;
2499
+ b[68] = 29;
2500
+ b[69] = 0;
2501
+ b[70] = 0;
2502
+ b[71] = 239;
2503
+ b[72] = 24;
2504
+ b[73] = 0;
2505
+ b[74] = 0;
2506
+ const envSnapshot = Object.values(BROWSER_ENV).join("|");
2507
+ const envBytes = Array.from(envSnapshot, (char) => char.charCodeAt(0));
2508
+ b[79] = envBytes.length & 255;
2509
+ b[80] = envBytes.length >> 8 & 255;
2510
+ const tail = `${now + 3 & 255},`;
2511
+ const tailBytes = Array.from(tail, (char) => char.charCodeAt(0));
2512
+ b[84] = tailBytes.length & 255;
2513
+ b[85] = tailBytes.length >> 8 & 255;
2514
+ const version = versionSegments(BDMS_SDK_VERSION);
2515
+ const noise = mix([version[0], version[1]], 0).concat(mix([version[0], version[1]], 2));
2516
+ const checksum = [
2517
+ 24,
2518
+ 26,
2519
+ 27,
2520
+ 28,
2521
+ 29,
2522
+ 30,
2523
+ 31,
2524
+ 32,
2525
+ 33,
2526
+ 34,
2527
+ 35,
2528
+ 36,
2529
+ 38,
2530
+ 39,
2531
+ 40,
2532
+ 41,
2533
+ 42,
2534
+ 43,
2535
+ 44,
2536
+ 45,
2537
+ 46,
2538
+ 47,
2539
+ 48,
2540
+ 49,
2541
+ 51,
2542
+ 52,
2543
+ 53,
2544
+ 55,
2545
+ 56,
2546
+ 57,
2547
+ 59,
2548
+ 60,
2549
+ 61,
2550
+ 62,
2551
+ 63,
2552
+ 64,
2553
+ 65,
2554
+ 66,
2555
+ 67,
2556
+ 68,
2557
+ 69,
2558
+ 70,
2559
+ 71,
2560
+ 72,
2561
+ 73,
2562
+ 74,
2563
+ 79,
2564
+ 80,
2565
+ 84,
2566
+ 85
2567
+ ].reduce((acc, key) => acc ^ b[key], noise.reduce((acc, value) => acc ^ value, 0));
2568
+ const payload = spread([
2569
+ 34,
2570
+ 44,
2571
+ 56,
2572
+ 61,
2573
+ 73,
2574
+ 29,
2575
+ 70,
2576
+ 45,
2577
+ 35,
2578
+ 49,
2579
+ 38,
2580
+ 66,
2581
+ 51,
2582
+ 68,
2583
+ 28,
2584
+ 48,
2585
+ 64,
2586
+ 47,
2587
+ 30,
2588
+ 71,
2589
+ 26,
2590
+ 55,
2591
+ 31,
2592
+ 69,
2593
+ 59,
2594
+ 40,
2595
+ 62,
2596
+ 63,
2597
+ 27,
2598
+ 72,
2599
+ 41,
2600
+ 74,
2601
+ 57,
2602
+ 52,
2603
+ 42,
2604
+ 39,
2605
+ 33,
2606
+ 67,
2607
+ 53,
2608
+ 43,
2609
+ 65,
2610
+ 46,
2611
+ 36,
2612
+ 24,
2613
+ 60,
2614
+ 32,
2615
+ 79,
2616
+ 80,
2617
+ 84,
2618
+ 85
2619
+ ].map((key) => b[key]).concat(envBytes, tailBytes, [checksum]));
2620
+ return base64(String.fromCharCode(...mix([3, 82], 1)) + rc4(String.fromCharCode(211), String.fromCharCode(...noise.concat(payload))), "s4");
2621
+ };
2622
+ //#endregion
2623
+ //#region ../amagi/packages/core/src/platform/douyin/passport/cookieJar.ts
2624
+ /**
2625
+ * 登录流程用的轻量 CookieJar
2626
+ *
2627
+ * 登录过程会跨 `www.douyin.com` / `login.douyin.com` / `ttwid.bytedance.com` 三个域,
2628
+ * 且同一个 cookie 名会被多次下发(例如 `ttwid` 在换取可信指纹后会被替换、
2629
+ * `sessionid` 在二次验证通过后会被升级)。这里只做一件事:**按下发顺序覆盖同名 cookie**,
2630
+ * 保证最终拿到的永远是最后一次下发的值,同时正确处理服务端的删除指令。
2631
+ *
2632
+ * 不做域/路径隔离:整个登录流程都在抖音自己的域下,隔离反而会漏掉跨子域下发的凭证。
2633
+ *
2634
+ * 另外承载一小部分**本地会话状态**(见 `INTERNAL_PREFIX`):passport 的几个接口对外是
2635
+ * 无状态的,会话全靠 cookie 串在调用之间传递,而 bd-ticket-guard 需要在多次调用之间
2636
+ * 记住自己生成的密钥与服务端签发的票据。这些条目以 `__amagi_` 开头,
2637
+ * `toString()` 不会把它们放进 Cookie 请求头,只有 `serialize()` 才会带上。
2638
+ */
2639
+ /** 本地会话状态的 cookie 名前缀,这些条目永远不会发给服务端 */
2640
+ var INTERNAL_PREFIX = "__amagi_";
2641
+ /** 是否为本地会话状态条目 */
2642
+ var isInternal = (name) => name.startsWith(INTERNAL_PREFIX);
2643
+ /** 判断一条 Set-Cookie 是否表示「删除该 cookie」 */
2644
+ var isDeletion = (value, attributes) => {
2645
+ if (value === "") return true;
2646
+ for (const attribute of attributes) {
2647
+ const [rawName, ...rest] = attribute.split("=");
2648
+ const name = rawName.trim().toLowerCase();
2649
+ const rawValue = rest.join("=").trim();
2650
+ if (name === "max-age") {
2651
+ const maxAge = Number(rawValue);
2652
+ if (Number.isFinite(maxAge) && maxAge <= 0) return true;
2653
+ }
2654
+ if (name === "expires") {
2655
+ const expires = Date.parse(rawValue);
2656
+ if (Number.isFinite(expires) && expires <= Date.now()) return true;
2657
+ }
2658
+ }
2659
+ return false;
2660
+ };
2661
+ var CookieJar = class {
2662
+ /** Map 保留插入顺序,重复 set 只更新值、不改变位置 */
2663
+ cookies = /* @__PURE__ */ new Map();
2664
+ /**
2665
+ * @param initial 初始 cookie 串,形如 `a=1; b=2`
2666
+ */
2667
+ constructor(initial) {
2668
+ if (initial) this.merge(initial);
2669
+ }
2670
+ /** 当前持有的 cookie 数量 */
2671
+ get size() {
2672
+ return this.cookies.size;
2673
+ }
2674
+ /**
2675
+ * 写入一条 cookie
2676
+ * @param name cookie 名
2677
+ * @param value cookie 值
2678
+ */
2679
+ set(name, value) {
2680
+ this.cookies.set(name, value);
2681
+ return this;
2682
+ }
2683
+ /**
2684
+ * 读取一条 cookie
2685
+ * @param name cookie 名
2686
+ */
2687
+ get(name) {
2688
+ return this.cookies.get(name);
2689
+ }
2690
+ /**
2691
+ * 是否持有某条 cookie
2692
+ * @param name cookie 名
2693
+ */
2694
+ has(name) {
2695
+ return this.cookies.has(name);
2696
+ }
2697
+ /**
2698
+ * 合并一段 `name=value; name=value` 形式的 cookie 串
2699
+ * @param cookieString cookie 串,空值直接忽略
2700
+ */
2701
+ merge(cookieString) {
2702
+ if (!cookieString) return this;
2703
+ for (const pair of cookieString.split(";")) {
2704
+ const index = pair.indexOf("=");
2705
+ if (index <= 0) continue;
2706
+ const name = pair.slice(0, index).trim();
2707
+ const value = pair.slice(index + 1).trim();
2708
+ if (name && value) this.cookies.set(name, value);
2709
+ }
2710
+ return this;
2711
+ }
2712
+ /**
2713
+ * 应用响应的 Set-Cookie 头
2714
+ * @param setCookies 单条或多条 Set-Cookie 原始值
2715
+ */
2716
+ applySetCookie(setCookies) {
2717
+ if (!setCookies) return this;
2718
+ for (const line of Array.isArray(setCookies) ? setCookies : [setCookies]) {
2719
+ const [pair, ...attributes] = line.split(";");
2720
+ const index = pair.indexOf("=");
2721
+ if (index <= 0) continue;
2722
+ const name = pair.slice(0, index).trim();
2723
+ const value = pair.slice(index + 1).trim();
2724
+ if (!name) continue;
2725
+ if (isDeletion(value, attributes)) {
2726
+ this.cookies.delete(name);
2727
+ continue;
2728
+ }
2729
+ this.cookies.set(name, value);
2730
+ }
2731
+ return this;
2732
+ }
2733
+ /**
2734
+ * 是否已经拿到登录态凭证(`ttwid` 是匿名设备指纹,不算登录)
2735
+ */
2736
+ isLoggedIn() {
2737
+ return this.has("sessionid") || this.has("sessionid_ss") || this.has("sid_guard");
2738
+ }
2739
+ /** 序列化为可直接放进 Cookie 请求头的字符串,不含本地会话状态 */
2740
+ toString() {
2741
+ return [...this.cookies].filter(([name]) => !isInternal(name)).map(([name, value]) => `${name}=${value}`).join("; ");
2742
+ }
2743
+ /**
2744
+ * 序列化为在两次调用之间传递的会话串,包含本地会话状态
2745
+ *
2746
+ * 登录流程内部用这个;最终落库的登录凭证用 `toString()`,避免把本地密钥写进配置。
2747
+ */
2748
+ serialize() {
2749
+ return [...this.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
2750
+ }
2751
+ /** 导出为普通对象,便于断言与日志 */
2752
+ toJSON() {
2753
+ return Object.fromEntries(this.cookies);
2754
+ }
2755
+ };
2756
+ //#endregion
2757
+ //#region ../amagi/packages/core/src/platform/douyin/passport/params.ts
2758
+ /**
2759
+ * passport 登录 SDK 的参数与签名构造
2760
+ *
2761
+ * login.douyin.com 的接口一共要过四道签名:
2762
+ * - `p_no`:SDK 版本相关参数排序后取 sha256
2763
+ * - `sign`:`排序后的前 10 个 query 参数 & 排序后的 body 参数 & app_key` 取 sha256
2764
+ * - `qs`:参与 sign 的参数名列表逐字节异或 5 后转十六进制
2765
+ * - `x-tt-passport-aid-sign` 请求头:以 appKey 为消息、当日 UTC 正午时间戳为密钥做 HMAC 派生
2766
+ *
2767
+ * 这些常量都是 SDK 自身的版本号与固定 app_key,属于协议的一部分,不含任何设备/账号信息。
2768
+ */
2769
+ /** passport 登录 SDK 的 app_key */
2770
+ var APP_KEY = "163e7ce78d58971a41f5b969996d85c2";
2771
+ /** 抖音 web 的 aid */
2772
+ var PASSPORT_AID = "6383";
2773
+ /** 登录接口域名 */
2774
+ var LOGIN_HOST = "login.douyin.com";
2775
+ /** 抖音主站域名 */
2776
+ var WEB_HOST = "www.douyin.com";
2777
+ /** 登录 SDK(normal 形态)版本号 */
2778
+ var JSSDK_VERSION = "3.1.3";
2779
+ /** 验证页 SDK(lite 形态)版本号 */
2780
+ var LITE_JSSDK_VERSION = "5.1.2";
2781
+ /** 验证页使用的 authn SDK 版本号 */
2782
+ var LITE_AUTHN_VERSION = "1.0.0.420-web";
2783
+ /** 各子 SDK 版本号,参与 p_no 计算 */
2784
+ var SDK_VERSIONS = {
2785
+ pVer: "1.1.3",
2786
+ pZt: "3.3.14",
2787
+ pUi: "2.1.9-alpha.6",
2788
+ pCa: "4.0.17",
2789
+ pCaReal: "1.0.0.874"
2790
+ };
2791
+ /** 与 aBogus 中 BROWSER_ENV 对应的窗口尺寸,用于生成 account_sdk_source_info */
2792
+ var ENV_VIEWPORT = {
2793
+ innerWidth: 2048,
2794
+ innerHeight: 960,
2795
+ outerWidth: 2554,
2796
+ outerHeight: 1386
2797
+ };
2798
+ var sha256Hex = (input) => crypto.createHash("sha256").update(input, "utf8").digest("hex");
2799
+ var hmacSha256 = (key, message) => crypto.createHmac("sha256", Buffer.from(key)).update(Buffer.from(message)).digest();
2800
+ var hexToBytes = (hex) => Uint8Array.from(hex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []);
2801
+ /**
2802
+ * 逐字节异或 5 后转十六进制,SDK 用它编码参数名列表、验证码与密码
2803
+ * @param input 明文
2804
+ */
2805
+ var xor5Hex = (input) => Array.from(Buffer.from(input, "utf8"), (byte) => (byte ^ 5).toString(16).padStart(2, "0")).join("");
2806
+ /** 随机十六进制串,用于 biz_trace_id 一类的追踪 ID */
2807
+ var randomHex = (length) => crypto.randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
2808
+ /** 当日 UTC 12:00 的秒级时间戳,aid-sign 以此为密钥基准 */
2809
+ var utcNoonTimestamp = (now = /* @__PURE__ */ new Date()) => Math.floor(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 12, 0, 0, 0) / 1e3);
2810
+ /**
2811
+ * 按 SDK 规则序列化参数:键名排序后拼成 `k=v&k=v`
2812
+ * @param params 参数对象
2813
+ * @param limit 大于等于 0 时只取排序后的前 limit 个键
2814
+ */
2815
+ var serializeSorted = (params, limit = -1) => {
2816
+ const keys = Object.keys(params).sort();
2817
+ if (limit >= 0) keys.splice(limit);
2818
+ return {
2819
+ text: keys.map((key) => `${key}=${typeof params[key] === "object" ? JSON.stringify(params[key]) : params[key]}`).join("&"),
2820
+ keys
2213
2821
  };
2822
+ };
2823
+ /**
2824
+ * 计算 sign 与 qs
2825
+ * @param params query 参数(仅排序后的前 10 个参与签名)
2826
+ * @param data body 参数,GET 请求传空对象
2827
+ */
2828
+ var makeSignAndQs = (params, data = {}) => {
2829
+ const { text: paramsText, keys } = serializeSorted(params, 10);
2830
+ const { text: dataText } = serializeSorted(data);
2214
2831
  return {
2215
- method: "GET",
2216
- timeout: 1e4,
2217
- ...requestConfig,
2218
- headers: {
2219
- ...defHeaders,
2220
- ...requestConfig?.headers ?? {}
2221
- }
2832
+ sign: sha256Hex(`${paramsText}&${dataText}&app_key=${APP_KEY}`),
2833
+ qs: xor5Hex(keys.join(","))
2222
2834
  };
2223
2835
  };
2224
2836
  /**
2225
- * B站平台默认请求配置
2226
- * @param cookie - 用户Cookie
2227
- * @param requestConfig - 外部请求配置(优先级最高)
2228
- * @returns 合并后的请求配置
2837
+ * 计算 p_no
2838
+ * @param pTs 毫秒时间戳,与 query 里的 p_ts 保持一致
2229
2839
  */
2230
- var getBilibiliDefaultConfig = (cookie, requestConfig) => {
2231
- let finalUserAgent = requestConfig?.headers?.["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";
2232
- finalUserAgent = finalUserAgent.replace(/\s+Edg\/[\d.]+/g, "");
2233
- const defHeaders = {
2234
- Accept: "application/json, text/plain, */*",
2235
- "Accept-Language": "zh-CN,zh;q=0.9",
2236
- "Accept-Encoding": "gzip, deflate, br, zstd",
2237
- "Cache-Control": "no-cache",
2238
- Pragma: "no-cache",
2239
- Referer: "https://www.bilibili.com/",
2240
- Priority: "u=1, i",
2241
- "Sec-Ch-Ua": generateSecChUa(finalUserAgent),
2242
- "Sec-Ch-Ua-Mobile": "?0",
2243
- "Sec-Ch-Ua-Platform": "\"Windows\"",
2244
- "Sec-Fetch-Dest": "empty",
2245
- "Sec-Fetch-Mode": "cors",
2246
- "Sec-Fetch-Site": "same-site",
2247
- "User-Agent": finalUserAgent,
2248
- Cookie: cookie?.trim() ?? ""
2840
+ var makePNo = (pTs) => {
2841
+ const parts = {
2842
+ passport_jssdk_version: JSSDK_VERSION,
2843
+ p_bd: BDMS_SDK_VERSION,
2844
+ p_ca: SDK_VERSIONS.pCa,
2845
+ p_ts: pTs,
2846
+ p_ver: SDK_VERSIONS.pVer,
2847
+ p_zt: SDK_VERSIONS.pZt
2249
2848
  };
2849
+ return sha256Hex(Object.keys(parts).sort().map((key) => `${key}=${parts[key]}`).join("&"));
2850
+ };
2851
+ /**
2852
+ * HKDF 风格的密钥派生,aid-sign 内部使用
2853
+ * @param keyHex 初始密钥(十六进制)
2854
+ * @param length 输出长度
2855
+ */
2856
+ var deriveKey = (keyHex, length) => {
2857
+ const output = [];
2858
+ let previous = "";
2859
+ let counter = 0;
2860
+ while (output.length < length) {
2861
+ counter++;
2862
+ const message = Uint8Array.from([...hexToBytes(previous), counter]);
2863
+ previous = hmacSha256(hexToBytes(keyHex), message).toString("hex");
2864
+ output.push(...hexToBytes(previous));
2865
+ }
2866
+ return Uint8Array.from(output.slice(0, length));
2867
+ };
2868
+ /**
2869
+ * 计算 `x-tt-passport-aid-sign` 请求头
2870
+ * @param urlPath 接口路径,如 `/passport/web/get_qrcode/`
2871
+ * @param timestamp 当日 UTC 正午时间戳(秒),默认取当前
2872
+ */
2873
+ var makeAidSign = (urlPath, timestamp = utcNoonTimestamp()) => {
2874
+ const encoder = new TextEncoder();
2875
+ const ts = String(timestamp);
2876
+ return hmacSha256(deriveKey(hmacSha256(encoder.encode(ts), encoder.encode(APP_KEY)).toString("hex"), 32), encoder.encode(`aid=${PASSPORT_AID}&path=${urlPath}&ts=${ts}`)).toString("hex");
2877
+ };
2878
+ /**
2879
+ * 生成 account_sdk_source_info:SDK 采集的浏览器环境快照,异或 5 后转十六进制。
2880
+ *
2881
+ * 上游参考实现内联的是作者本机抓包值(含显卡型号、堆内存占用、带 query 的个人主页 URL),
2882
+ * 不适合进仓库,这里换成一份等价形态的通用快照。
2883
+ *
2884
+ * 实测服务端在 `get_qrcode` 阶段不校验该字段内容(删掉、置空、填垃圾值都同样返回
2885
+ * `error_code: 0`),保留它只是为了与 SDK 的真实请求形态一致。
2886
+ */
2887
+ var makeAccountSdkSourceInfo = () => xor5Hex(JSON.stringify({
2888
+ hardwareConcurrency: 8,
2889
+ webdriver: false,
2890
+ chromedriver: false,
2891
+ shelldriver: false,
2892
+ plugins: 5,
2893
+ innerHeight: ENV_VIEWPORT.innerHeight,
2894
+ innerWidth: ENV_VIEWPORT.innerWidth,
2895
+ outerHeight: ENV_VIEWPORT.outerHeight,
2896
+ outerWidth: ENV_VIEWPORT.outerWidth,
2897
+ webgl: {
2898
+ vendor: "Google Inc. (Intel)",
2899
+ renderer: "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)"
2900
+ },
2901
+ performance: {
2902
+ timeOrigin: Date.now(),
2903
+ navigationTiming: {
2904
+ entryType: "navigation",
2905
+ initiatorType: "navigation",
2906
+ name: `https://${WEB_HOST}/`,
2907
+ renderBlockingStatus: "non-blocking"
2908
+ }
2909
+ },
2910
+ browser: {
2911
+ bit_protocol: "false",
2912
+ bit_helper: false
2913
+ }
2914
+ }));
2915
+ /**
2916
+ * 构造 passport 登录 SDK 的通用 query 参数
2917
+ *
2918
+ * 参数顺序即实际发送顺序:SDK 各拦截器依次注入,`request_host` 在这里先编码一次,
2919
+ * 拼 URL 时会再编码一次,所以线上抓包看到的是双重编码。
2920
+ * @param extra 业务参数(GET 时并入 query)
2921
+ */
2922
+ var makeCommonParams = (extra = {}) => {
2923
+ const pTs = String(Date.now());
2250
2924
  return {
2251
- method: "GET",
2252
- timeout: 1e4,
2253
- ...requestConfig,
2254
- headers: {
2255
- ...defHeaders,
2256
- ...requestConfig?.headers ?? {}
2925
+ passport_jssdk_version: JSSDK_VERSION,
2926
+ passport_jssdk_type: "normal",
2927
+ is_from_ttaccountsdk: "1",
2928
+ aid: PASSPORT_AID,
2929
+ language: "zh",
2930
+ account_app_language: "zh-CN",
2931
+ ts: String(utcNoonTimestamp()),
2932
+ ...Object.fromEntries(Object.entries(extra).map(([key, value]) => [key, String(value)])),
2933
+ is_from_iesaccountsaas: "1",
2934
+ p_ui: SDK_VERSIONS.pUi,
2935
+ p_ca: SDK_VERSIONS.pCa,
2936
+ p_ca_real: SDK_VERSIONS.pCaReal,
2937
+ account_sdk_source: "web",
2938
+ account_sdk_source_info: makeAccountSdkSourceInfo(),
2939
+ p_js_v: JSSDK_VERSION,
2940
+ p_js_t: "pro",
2941
+ p_zt: SDK_VERSIONS.pZt,
2942
+ p_ver: SDK_VERSIONS.pVer,
2943
+ p_ver_real: "0",
2944
+ request_host: encodeURIComponent(`https://${WEB_HOST}`),
2945
+ p_bd: BDMS_SDK_VERSION,
2946
+ p_ts: pTs,
2947
+ p_no: makePNo(pTs),
2948
+ biz_trace_id: randomHex(8),
2949
+ device_platform: "web_app"
2950
+ };
2951
+ };
2952
+ /**
2953
+ * 构造验证页 SDK(lite 形态)的固定 query 参数
2954
+ * @param bizTraceId 业务追踪 ID
2955
+ */
2956
+ var makeLiteParams = (bizTraceId) => ({
2957
+ passport_jssdk_version: LITE_JSSDK_VERSION,
2958
+ passport_jssdk_type: "lite",
2959
+ is_from_ttaccountsdk: "1",
2960
+ aid: PASSPORT_AID,
2961
+ language: "zh",
2962
+ account_app_language: "zh-CN",
2963
+ new_authn_sdk_version: LITE_AUTHN_VERSION,
2964
+ biz_trace_id: bizTraceId
2965
+ });
2966
+ /**
2967
+ * 按插入顺序序列化为查询串(不排序,`request_host` 因此产生二次编码)
2968
+ * @param params 参数对象
2969
+ */
2970
+ var serializeQuery = (params) => Object.entries(params).map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`).join("&");
2971
+ //#endregion
2972
+ //#region ../amagi/packages/core/src/platform/douyin/passport/ticketGuard.ts
2973
+ /**
2974
+ * bd-ticket-guard 设备票据
2975
+ *
2976
+ * 抖音的设备真实性风控。常见的说法是必须从浏览器 localStorage 里导出一对密钥才能用,
2977
+ * 但那只适用于「已登录之后」的接口 —— **登录流程本身就是票据的签发入口**:
2978
+ *
2979
+ * 1. 本地生成一对 P-256 密钥
2980
+ * 2. 把公钥写进 `bd_ticket_guard_client_data` cookie,在取二维码**之前**交给服务端
2981
+ * 3. 服务端在后续响应的 `bd-ticket-guard-server-data` 头里签发 `{ticket, ts_sign, client_cert}`
2982
+ * 4. 之后每个请求用 `ECDH(自己的私钥, client_cert 里的服务端公钥)` 派生的密钥
2983
+ * 对 `ticket=…&path=…&timestamp=…` 做 HMAC,放进 `bd-ticket-guard-client-data` 头
2984
+ *
2985
+ * 全程不需要浏览器,票据是服务端真实签发给我们自己这把密钥的,没有任何伪造数据。
2986
+ * 密钥与票据以 `__amagi_` 前缀存在 CookieJar 里,只在调用之间传递,不会发给服务端。
2987
+ */
2988
+ /** 交给服务端的公钥所在的 cookie */
2989
+ var CLIENT_DATA_COOKIE = "bd_ticket_guard_client_data";
2990
+ /** 声明 web 域版本的 cookie */
2991
+ var CLIENT_WEB_DOMAIN_COOKIE = "bd_ticket_guard_client_web_domain";
2992
+ /** 服务端也可能把签发结果放在这条 cookie 里 */
2993
+ var SERVER_DATA_COOKIE = "bd_ticket_guard_server_data";
2994
+ /** 服务端签发结果所在的响应头 */
2995
+ var SERVER_DATA_HEADER = "bd-ticket-guard-server-data";
2996
+ /** 本地会话状态:PKCS#8 私钥(base64) */
2997
+ var KEY_ENTRY = `${INTERNAL_PREFIX}tg_key`;
2998
+ /** 本地会话状态:服务端签发的票据 */
2999
+ var TICKET_ENTRY = `${INTERNAL_PREFIX}tg_ticket`;
3000
+ /** 本地会话状态:票据的时间戳签名 */
3001
+ var TS_SIGN_ENTRY = `${INTERNAL_PREFIX}tg_ts_sign`;
3002
+ /** 本地会话状态:ECDH 派生出的 HMAC 密钥(hex) */
3003
+ var ECDH_ENTRY = `${INTERNAL_PREFIX}tg_ecdh`;
3004
+ /** SDK 声明的 ticket-guard 版本 */
3005
+ var GUARD_VERSION = "2";
3006
+ /** SDK 声明的迭代版本 */
3007
+ var ITERATION_VERSION = "1";
3008
+ /** P-256 公钥的 SubjectPublicKeyInfo 前缀,其后紧跟 65 字节未压缩点 */
3009
+ var P256_SPKI_PREFIX = Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex");
3010
+ /** 被签名内容的字段列表,需与实际拼接顺序一致 */
3011
+ var REQ_CONTENT = "ticket,path,timestamp";
3012
+ /** 紧凑 JSON,与浏览器的 `JSON.stringify` 一致 */
3013
+ var compactJson = (value) => JSON.stringify(value);
3014
+ /**
3015
+ * 从 PEM 证书或 `pub.<base64>` 里取出服务端公钥
3016
+ * @param clientCert 服务端下发的 client_cert
3017
+ */
3018
+ var readServerPublicKey = (clientCert) => {
3019
+ if (clientCert.startsWith("pub.")) {
3020
+ const point = Buffer.from(clientCert.slice(4), "base64");
3021
+ const body = point.length === 65 ? point.subarray(1) : point;
3022
+ const spki = Buffer.concat([
3023
+ P256_SPKI_PREFIX,
3024
+ Buffer.from([4]),
3025
+ body
3026
+ ]);
3027
+ return crypto.createPublicKey({
3028
+ key: spki,
3029
+ format: "der",
3030
+ type: "spki"
3031
+ });
3032
+ }
3033
+ return new crypto.X509Certificate(clientCert).publicKey;
3034
+ };
3035
+ /**
3036
+ * bd-ticket-guard 会话
3037
+ *
3038
+ * 状态全部读写自传入的 CookieJar,因此与 passport 的无状态调用形态天然兼容。
3039
+ */
3040
+ var TicketGuard = class {
3041
+ jar;
3042
+ /**
3043
+ * @param jar 当前会话 cookie
3044
+ */
3045
+ constructor(jar) {
3046
+ this.jar = jar;
3047
+ }
3048
+ /** 本会话的私钥,缺失时生成一把并写回 CookieJar */
3049
+ get privateKey() {
3050
+ const stored = this.jar.get(KEY_ENTRY);
3051
+ if (stored) return crypto.createPrivateKey({
3052
+ key: Buffer.from(stored, "base64"),
3053
+ format: "der",
3054
+ type: "pkcs8"
3055
+ });
3056
+ const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "prime256v1" });
3057
+ const der = privateKey.export({
3058
+ format: "der",
3059
+ type: "pkcs8"
3060
+ });
3061
+ this.jar.set(KEY_ENTRY, der.toString("base64"));
3062
+ return privateKey;
3063
+ }
3064
+ /** 未压缩格式的公钥(base64),即 `bd-ticket-guard-ree-public-key` */
3065
+ get reePublicKey() {
3066
+ const spki = crypto.createPublicKey(this.privateKey).export({
3067
+ format: "der",
3068
+ type: "spki"
3069
+ });
3070
+ return spki.subarray(spki.length - 65).toString("base64");
3071
+ }
3072
+ /** 已签发的票据,未签发时为 undefined */
3073
+ get state() {
3074
+ const ticket = this.jar.get(TICKET_ENTRY);
3075
+ const tsSign = this.jar.get(TS_SIGN_ENTRY);
3076
+ const ecdh = this.jar.get(ECDH_ENTRY);
3077
+ if (!ticket || !tsSign || !ecdh) return void 0;
3078
+ return {
3079
+ ticket,
3080
+ tsSign,
3081
+ ecdhKey: Buffer.from(ecdh, "hex")
3082
+ };
3083
+ }
3084
+ /**
3085
+ * 在首个 passport 请求之前把公钥交给服务端
3086
+ *
3087
+ * 缺了这一步扫码依然能成功,但服务端不会签发票据,后续请求也就无从携带。
3088
+ */
3089
+ publishPublicKey() {
3090
+ const payload = compactJson({
3091
+ "bd-ticket-guard-version": Number(GUARD_VERSION),
3092
+ "bd-ticket-guard-iteration-version": Number(ITERATION_VERSION),
3093
+ "bd-ticket-guard-ree-public-key": this.reePublicKey,
3094
+ "bd-ticket-guard-web-version": Number(GUARD_VERSION)
3095
+ });
3096
+ this.jar.set(CLIENT_DATA_COOKIE, encodeURIComponent(Buffer.from(payload, "utf8").toString("base64")));
3097
+ this.jar.set(CLIENT_WEB_DOMAIN_COOKIE, GUARD_VERSION);
3098
+ }
3099
+ /**
3100
+ * 消化响应里可能带回的票据签发结果
3101
+ * @param headers 响应头
3102
+ * @returns 是否收到了新票据
3103
+ */
3104
+ applyServerData(headers) {
3105
+ const fromHeader = headers[SERVER_DATA_HEADER];
3106
+ const raw = typeof fromHeader === "string" && fromHeader ? fromHeader : this.jar.get(SERVER_DATA_COOKIE);
3107
+ if (!raw) return false;
3108
+ let info;
3109
+ try {
3110
+ info = JSON.parse(Buffer.from(decodeURIComponent(raw), "base64").toString("utf8"));
3111
+ } catch {
3112
+ return false;
2257
3113
  }
3114
+ if (!info.ticket || !info.ts_sign || !info.client_cert) return false;
3115
+ let ecdhKey;
3116
+ try {
3117
+ ecdhKey = this.deriveEcdhKey(info.client_cert);
3118
+ } catch {
3119
+ return false;
3120
+ }
3121
+ this.jar.set(TICKET_ENTRY, info.ticket);
3122
+ this.jar.set(TS_SIGN_ENTRY, info.ts_sign);
3123
+ this.jar.set(ECDH_ENTRY, ecdhKey.toString("hex"));
3124
+ return true;
3125
+ }
3126
+ /**
3127
+ * 生成本次请求的 bd-ticket-guard 请求头
3128
+ *
3129
+ * 尚未拿到票据时只声明公钥,让服务端有机会签发;拿到之后带完整签名。
3130
+ * @param path 请求路径,不含 query
3131
+ * @param timestamp 秒级时间戳,默认取当前
3132
+ */
3133
+ headers(path, timestamp = Math.floor(Date.now() / 1e3)) {
3134
+ const base = {
3135
+ "bd-ticket-guard-version": GUARD_VERSION,
3136
+ "bd-ticket-guard-iteration-version": ITERATION_VERSION,
3137
+ "bd-ticket-guard-ree-public-key": this.reePublicKey
3138
+ };
3139
+ const state = this.state;
3140
+ if (!state) return base;
3141
+ const signed = `ticket=${state.ticket}&path=${path}&timestamp=${timestamp}`;
3142
+ const clientData = compactJson({
3143
+ ts_sign: state.tsSign,
3144
+ req_content: REQ_CONTENT,
3145
+ req_sign: crypto.createHmac("sha256", state.ecdhKey).update(signed, "utf8").digest("base64"),
3146
+ timestamp
3147
+ });
3148
+ return {
3149
+ ...base,
3150
+ "bd-ticket-guard-client-data": Buffer.from(clientData, "utf8").toString("base64"),
3151
+ "bd-ticket-guard-web-version": state.tsSign.startsWith("ts.1") ? "1" : GUARD_VERSION,
3152
+ "bd-ticket-guard-web-sign-type": "1"
3153
+ };
3154
+ }
3155
+ /**
3156
+ * ECDH + HKDF-SHA256 派生 HMAC 密钥
3157
+ * @param clientCert 服务端下发的证书或裸公钥
3158
+ */
3159
+ deriveEcdhKey(clientCert) {
3160
+ const shared = crypto.diffieHellman({
3161
+ privateKey: this.privateKey,
3162
+ publicKey: readServerPublicKey(clientCert)
3163
+ });
3164
+ return Buffer.from(crypto.hkdfSync("sha256", shared, Buffer.alloc(32), Buffer.alloc(0), 32));
3165
+ }
3166
+ };
3167
+ //#endregion
3168
+ //#region ../amagi/packages/core/src/platform/douyin/passport/client.ts
3169
+ /**
3170
+ * passport 登录的 HTTP 客户端
3171
+ *
3172
+ * 走 amagi 自己的 `fetchResponse`(axios),因此代理、超时、重试与网络事件与其它接口一致。
3173
+ *
3174
+ * 客户端本身是无状态的:会话状态(`msToken`、`passport_csrf_token`)都以 cookie 形式
3175
+ * 随调用方传入的 cookie 串进出,`x-tt-passport-verify-portrait` 则由 `ttwid` 派生,
3176
+ * 因此同一份 cookie 在整个登录过程中会得到稳定的 portrait,调用方无需额外保存任何东西。
3177
+ */
3178
+ /** 与签名里的浏览器环境保持一致的 UA */
3179
+ var 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";
3180
+ /** 单次请求默认超时 */
3181
+ var DEFAULT_TIMEOUT = 15e3;
3182
+ /** SSO 跳转最多跟随的次数 */
3183
+ var MAX_REDIRECT_HOPS = 5;
3184
+ /** 浏览器客户端提示头,服务端会与 UA 交叉校验 */
3185
+ var CLIENT_HINTS = {
3186
+ "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Chromium\";v=\"142\"",
3187
+ "sec-ch-ua-mobile": "?0",
3188
+ "sec-ch-ua-platform": "\"Windows\""
3189
+ };
3190
+ /** 安全解析 JSON,失败返回空对象 */
3191
+ var parseJson = (text) => {
3192
+ try {
3193
+ return JSON.parse(text);
3194
+ } catch {
3195
+ return {};
3196
+ }
3197
+ };
3198
+ /**
3199
+ * 由 ttwid 派生出稳定的 verify portrait
3200
+ *
3201
+ * 浏览器里这个值在一次登录页生命周期内固定不变。这里用 cookie 中已有的设备指纹推导,
3202
+ * 既保证同一会话内多次调用得到同一个值,又不需要调用方额外携带状态。
3203
+ * @param jar 当前会话 cookie
3204
+ */
3205
+ var deriveVerifyPortrait = (jar) => {
3206
+ const seed = jar.get("ttwid") ?? jar.get("__ac_nonce") ?? "douyin-passport";
3207
+ const hex = crypto.createHash("sha256").update(seed).digest("hex");
3208
+ return `${[
3209
+ hex.slice(0, 8),
3210
+ hex.slice(8, 12),
3211
+ `4${hex.slice(13, 16)}`,
3212
+ `8${hex.slice(17, 20)}`,
3213
+ hex.slice(20, 32)
3214
+ ].join("-")}.login`;
3215
+ };
3216
+ var DouyinPassportClient = class {
3217
+ requestConfig;
3218
+ /** 会话 cookie */
3219
+ cookies;
3220
+ /** bd-ticket-guard 设备票据,状态随 cookie 一起流转 */
3221
+ ticketGuard;
3222
+ /**
3223
+ * @param cookie 已有的会话 cookie 串
3224
+ * @param requestConfig amagi 的请求配置(代理、超时、额外请求头)
3225
+ */
3226
+ constructor(cookie, requestConfig) {
3227
+ this.requestConfig = requestConfig;
3228
+ this.cookies = new CookieJar(cookie);
3229
+ this.ticketGuard = new TicketGuard(this.cookies);
3230
+ }
3231
+ /** CSRF token:优先用服务端下发的,缺失时本地生成并同步写进 cookie(双提交校验) */
3232
+ get csrfToken() {
3233
+ const fromCookie = this.cookies.get("passport_csrf_token");
3234
+ if (fromCookie) return fromCookie;
3235
+ const generated = randomHex(32);
3236
+ this.cookies.set("passport_csrf_token", generated);
3237
+ this.cookies.set("passport_csrf_token_default", generated);
3238
+ return generated;
3239
+ }
3240
+ /**
3241
+ * 初始化登录环境指纹
3242
+ *
3243
+ * 依次请求抖音首页拿 `__ac_nonce`、再向 ttwid 服务注册拿 `ttwid`。两步都是匿名的,
3244
+ * 任意机器、任意系统都能跑;失败不抛错,只会让后续更容易命中风控。
3245
+ */
3246
+ async bootstrap() {
3247
+ this.ticketGuard.publishPublicKey();
3248
+ if (this.cookies.has("ttwid") && this.cookies.has("__ac_nonce")) return;
3249
+ await this.send({
3250
+ method: "GET",
3251
+ url: `https://${WEB_HOST}/`,
3252
+ headers: {
3253
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
3254
+ ...CLIENT_HINTS
3255
+ }
3256
+ });
3257
+ await this.send({
3258
+ method: "POST",
3259
+ url: "https://ttwid.bytedance.com/ttwid/register/",
3260
+ headers: {
3261
+ "Content-Type": "application/json",
3262
+ Origin: `https://${WEB_HOST}`,
3263
+ Referer: `https://${WEB_HOST}/`
3264
+ },
3265
+ data: JSON.stringify({
3266
+ aid: 6383,
3267
+ service: WEB_HOST
3268
+ })
3269
+ });
3270
+ emitLogDebug(`[douyin passport] 环境指纹就绪: ttwid=${this.cookies.has("ttwid")}, ac_nonce=${this.cookies.has("__ac_nonce")}`);
3271
+ }
3272
+ /**
3273
+ * 请求 login.douyin.com 的 passport 接口(四重签名 + a_bogus 形态)
3274
+ * @param path 接口路径,如 `/passport/web/get_qrcode/`
3275
+ * @param params 业务参数,并入 query
3276
+ */
3277
+ async request(path, params = {}) {
3278
+ const common = makeCommonParams(params);
3279
+ const { sign, qs } = makeSignAndQs(common, {});
3280
+ const query = {
3281
+ ...common,
3282
+ sign,
3283
+ qs
3284
+ };
3285
+ const msToken = this.cookies.get("msToken");
3286
+ if (msToken) query.msToken = msToken;
3287
+ const queryString = serializeQuery(query);
3288
+ const url = `https://${LOGIN_HOST}${path}?${queryString}&a_bogus=${encodeURIComponent(aBogus(queryString, PASSPORT_USER_AGENT))}`;
3289
+ return this.send({
3290
+ method: "GET",
3291
+ url,
3292
+ headers: {
3293
+ Accept: "application/json, text/javascript",
3294
+ "Content-Type": "application/x-www-form-urlencoded",
3295
+ Referer: `https://${WEB_HOST}/`,
3296
+ "x-tt-passport-aid-sign": makeAidSign(path),
3297
+ "x-tt-passport-csrf-token": this.csrfToken,
3298
+ "x-tt-passport-verify-portrait": deriveVerifyPortrait(this.cookies),
3299
+ "x-tt-passport-trace-id": String(common.biz_trace_id),
3300
+ ...this.ticketGuard.headers(path),
3301
+ ...CLIENT_HINTS
3302
+ }
3303
+ });
3304
+ }
3305
+ /**
3306
+ * 请求 www.douyin.com 的验证页接口(lite 形态:固定 query + 表单 body,无签名)
3307
+ * @param path 接口路径,如 `/passport/web/send_code/`
3308
+ * @param params 业务参数,进 body
3309
+ * @param bizTraceId 业务追踪 ID,同一次验证流程内保持一致
3310
+ */
3311
+ async liteRequest(path, params, bizTraceId) {
3312
+ return this.send({
3313
+ method: "POST",
3314
+ url: `https://${WEB_HOST}${path}?${serializeQuery(makeLiteParams(bizTraceId))}`,
3315
+ headers: {
3316
+ Accept: "application/json, text/javascript",
3317
+ "Content-Type": "application/x-www-form-urlencoded",
3318
+ Origin: `https://${WEB_HOST}`,
3319
+ Referer: `https://${WEB_HOST}/`,
3320
+ "x-tt-passport-aid-sign": makeAidSign(path),
3321
+ "x-tt-passport-csrf-token": this.csrfToken,
3322
+ "x-tt-passport-verify-portrait": deriveVerifyPortrait(this.cookies),
3323
+ "x-tt-passport-trace-id": bizTraceId,
3324
+ ...this.ticketGuard.headers(path),
3325
+ ...CLIENT_HINTS
3326
+ },
3327
+ data: serializeQuery(params)
3328
+ });
3329
+ }
3330
+ /**
3331
+ * 跟随扫码确认后下发的 SSO 跳转链,把最终的登录凭证收进 CookieJar
3332
+ * @param redirectUrl `check_qrconnect` 返回的 redirect_url
3333
+ * @returns 是否拿到登录态 cookie
3334
+ */
3335
+ async followSsoRedirect(redirectUrl) {
3336
+ let current = redirectUrl;
3337
+ for (let hop = 0; hop < MAX_REDIRECT_HOPS; hop++) {
3338
+ const response = await this.send({
3339
+ method: "GET",
3340
+ url: current,
3341
+ maxRedirects: 0,
3342
+ headers: {
3343
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
3344
+ Referer: `https://${LOGIN_HOST}/`,
3345
+ ...CLIENT_HINTS
3346
+ }
3347
+ });
3348
+ const location = response.location;
3349
+ if (!location || response.status < 300 || response.status >= 400) break;
3350
+ current = new URL(location, current).toString();
3351
+ }
3352
+ return this.cookies.isLoggedIn();
3353
+ }
3354
+ /** 实际发请求:合并 cookie、消化 Set-Cookie 与 msToken */
3355
+ async send(config) {
3356
+ const cookie = this.cookies.toString();
3357
+ const response = await fetchResponse({
3358
+ timeout: this.requestConfig?.timeout ?? DEFAULT_TIMEOUT,
3359
+ proxy: this.requestConfig?.proxy,
3360
+ ...config,
3361
+ responseType: "text",
3362
+ maxRedirects: config.maxRedirects ?? 5,
3363
+ headers: {
3364
+ "User-Agent": PASSPORT_USER_AGENT,
3365
+ ...config.headers,
3366
+ ...cookie ? { Cookie: cookie } : {}
3367
+ }
3368
+ });
3369
+ if (isNetworkErrorResult(response)) throw new Error(response.error.amagiError.errorDescription);
3370
+ const axiosResponse = response;
3371
+ this.cookies.applySetCookie(axiosResponse.headers["set-cookie"]);
3372
+ const refreshed = axiosResponse.headers["x-ms-token"];
3373
+ if (typeof refreshed === "string" && refreshed) this.cookies.set("msToken", refreshed);
3374
+ if (this.ticketGuard.applyServerData(axiosResponse.headers)) emitLogDebug("[douyin passport] 已获得 bd-ticket-guard 票据");
3375
+ const raw = typeof axiosResponse.data === "string" ? axiosResponse.data : JSON.stringify(axiosResponse.data);
3376
+ return {
3377
+ status: axiosResponse.status,
3378
+ raw,
3379
+ body: parseJson(raw),
3380
+ cookie: this.cookies.serialize(),
3381
+ location: axiosResponse.headers.location
3382
+ };
3383
+ }
3384
+ };
3385
+ //#endregion
3386
+ //#region ../amagi/packages/core/src/platform/douyin/passport/parser.ts
3387
+ /** 触发账号二次验证的错误码 */
3388
+ var ERROR_SECOND_VERIFY = 2046;
3389
+ /** 验证码错误 */
3390
+ var ERROR_WRONG_CODE = 1202;
3391
+ /** 发码过于频繁 */
3392
+ var ERROR_RATE_LIMITED = 1206;
3393
+ /** 命中风控 / 设备环境异常 */
3394
+ var RISK_ERROR_CODES = /* @__PURE__ */ new Set([2156, 4031]);
3395
+ /**
3396
+ * 轮询过于频繁
3397
+ *
3398
+ * 描述文案是「访问太频繁」,但设备指纹不完整时服务端也用这个码兜底,
3399
+ * 属于可重试的瞬时状态,退避后继续轮询即可,不能当致命错误。
3400
+ */
3401
+ var ERROR_POLL_BUSY = 7;
3402
+ /** 命中限频后的退避倍率 */
3403
+ var BUSY_BACKOFF = 2;
3404
+ /** 轮询间隔下限与默认值,服务端偶尔会给 0 */
3405
+ var MIN_INTERVAL = 1e3;
3406
+ var DEFAULT_INTERVAL = 3e3;
3407
+ /** 验证会话票据字段,需原样透传 */
3408
+ var STD_KEYS = [
3409
+ "std_verify_flow_id",
3410
+ "std_verify_scene",
3411
+ "std_verify_template",
3412
+ "std_verify_token",
3413
+ "std_verify_type",
3414
+ "std_verify_way"
3415
+ ];
3416
+ var asString = (value) => typeof value === "string" ? value : "";
3417
+ var asNumber = (value) => {
3418
+ const parsed = typeof value === "number" ? value : Number(value);
3419
+ return Number.isFinite(parsed) ? parsed : void 0;
3420
+ };
3421
+ /** 从响应体的多个可能位置取错误码 */
3422
+ var readErrorCode = (payload) => asNumber(payload.data?.error_code) ?? asNumber(payload.error_code);
3423
+ /** 取一段人类可读的错误描述 */
3424
+ var readMessage = (payload) => asString(payload.data?.description) || asString(payload.description) || asString(payload.message) || "";
3425
+ /**
3426
+ * 解析 `get_qrcode` 响应
3427
+ * @param payload 服务端响应体
3428
+ * @returns 二维码信息,缺少 token 时返回 null
3429
+ */
3430
+ var parseQrcode = (payload) => {
3431
+ const data = payload.data ?? {};
3432
+ const token = asString(data.token);
3433
+ if (!token) return null;
3434
+ return {
3435
+ token,
3436
+ content: asString(data.qrcode_index_url) || token,
3437
+ expireTime: asNumber(data.expire_time) ?? 0
2258
3438
  };
2259
3439
  };
3440
+ /** 解析服务端下发的可选验证方式 */
3441
+ var parseVerifyWays = (raw) => {
3442
+ if (!Array.isArray(raw)) return [];
3443
+ return raw.map((item) => {
3444
+ const way = item;
3445
+ return {
3446
+ verifyWay: asString(way?.verify_way),
3447
+ mobile: asString(way?.mobile) || void 0
3448
+ };
3449
+ }).filter((way) => way.verifyWay !== "");
3450
+ };
2260
3451
  /**
2261
- * 快手平台默认请求配置
2262
- * @param cookie - 用户Cookie
2263
- * @param requestConfig - 外部请求配置(优先级最高)
2264
- * @returns 合并后的请求配置
3452
+ * 从轮询响应里提取二次验证上下文
3453
+ * @param data 轮询响应的 data 段
2265
3454
  */
2266
- var getKuaishouDefaultConfig = (cookie, requestConfig) => {
2267
- const defHeaders = {
2268
- Referer: "https://www.kuaishou.com/new-reco",
2269
- Origin: "https://www.kuaishou.com",
2270
- Accept: "application/json, text/plain, */*",
2271
- "Accept-Encoding": "gzip, deflate, br, zstd",
2272
- "Content-Type": "application/json",
2273
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
2274
- Priority: "u=0, i",
2275
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
2276
- Cookie: cookie?.trim() ?? ""
3455
+ var parseVerifyContext = (data) => {
3456
+ const stdParams = {};
3457
+ for (const key of STD_KEYS) {
3458
+ const value = asString(data[key]);
3459
+ if (value) stdParams[key] = value;
3460
+ }
3461
+ return {
3462
+ encryptUid: asString(data.encrypt_uid),
3463
+ verifyTicket: asString(data.verify_ticket),
3464
+ stdParams,
3465
+ copywritingKey: asString(data.copywriting_key) || "qr_connect",
3466
+ diversionTag: asString(data.ies_safety_diversion_tag) || "mfa",
3467
+ newVerifyFlow: asString(data.new_verify_flow),
3468
+ verifyWays: parseVerifyWays(data.verify_ways)
3469
+ };
3470
+ };
3471
+ /**
3472
+ * 解析 `check_qrconnect` 响应为状态机可消费的结果
3473
+ * @param payload 服务端响应体
3474
+ */
3475
+ var parsePollResult = (payload) => {
3476
+ const data = payload.data ?? {};
3477
+ const rawInterval = asNumber(data.interval) ?? 0;
3478
+ const interval = rawInterval >= MIN_INTERVAL ? rawInterval : DEFAULT_INTERVAL;
3479
+ const status = asString(data.status);
3480
+ const errorCode = readErrorCode(payload);
3481
+ if (errorCode === ERROR_SECOND_VERIFY || asString(data.account_flow) === "verify") return {
3482
+ status: "verify",
3483
+ interval,
3484
+ verify: parseVerifyContext(data)
3485
+ };
3486
+ if (errorCode !== void 0 && RISK_ERROR_CODES.has(errorCode)) return {
3487
+ status: "risk",
3488
+ interval,
3489
+ message: readMessage(payload) || `error_code=${errorCode}`
3490
+ };
3491
+ if (errorCode === ERROR_POLL_BUSY) return {
3492
+ status: "busy",
3493
+ interval: interval * BUSY_BACKOFF,
3494
+ message: readMessage(payload) || "轮询过于频繁"
3495
+ };
3496
+ switch (status) {
3497
+ case "new":
3498
+ case "scanned":
3499
+ case "expired": return {
3500
+ status,
3501
+ interval
3502
+ };
3503
+ case "confirmed": return {
3504
+ status: "confirmed",
3505
+ interval,
3506
+ redirectUrl: asString(data.redirect_url) || (Array.isArray(data.redirect_urls) ? asString(data.redirect_urls[0]) : "")
3507
+ };
3508
+ default: return {
3509
+ status: "unknown",
3510
+ interval,
3511
+ message: readMessage(payload) || (status ? `status=${status}` : `error_code=${errorCode ?? "unknown"}`)
3512
+ };
3513
+ }
3514
+ };
3515
+ /**
3516
+ * 解析 `send_code` 响应
3517
+ * @param payload 服务端响应体
3518
+ */
3519
+ var parseSendCodeResult = (payload) => {
3520
+ const data = payload.data ?? {};
3521
+ const errorCode = readErrorCode(payload);
3522
+ const retryAfter = asNumber(data.retry_time) ?? 60;
3523
+ const mobile = asString(data.mobile);
3524
+ if (errorCode === 0 || errorCode === void 0 && payload.message === "success") return {
3525
+ ok: true,
3526
+ mobile,
3527
+ retryAfter,
3528
+ message: ""
2277
3529
  };
2278
3530
  return {
2279
- method: "POST",
2280
- timeout: 1e4,
2281
- ...requestConfig,
2282
- headers: {
2283
- ...defHeaders,
2284
- ...requestConfig?.headers ?? {}
2285
- }
3531
+ ok: false,
3532
+ mobile,
3533
+ retryAfter,
3534
+ errorCode,
3535
+ message: readMessage(payload) || (errorCode === ERROR_RATE_LIMITED ? "短信发送过于频繁" : `发码失败 error_code=${errorCode ?? "unknown"}`)
2286
3536
  };
2287
3537
  };
2288
3538
  /**
2289
- * 获取小红书默认配置
2290
- * @param cookie - 用户Cookie
2291
- * @returns 小红书请求配置
3539
+ * 解析 `validate_code` 响应
3540
+ * @param payload 服务端响应体
2292
3541
  */
2293
- var getXiaohongshuDefaultConfig = (cookie) => {
2294
- return { headers: {
2295
- accept: "application/json, text/plain, */*",
2296
- "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
2297
- "cache-control": "no-cache",
2298
- "content-type": "application/json;charset=UTF-8",
2299
- pragma: "no-cache",
2300
- priority: "u=1, i",
2301
- referer: "https://www.xiaohongshu.com/",
2302
- "sec-ch-ua": "\"Microsoft Edge\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
2303
- "sec-ch-ua-mobile": "?0",
2304
- "sec-ch-ua-platform": "\"Windows\"",
2305
- "sec-fetch-dest": "empty",
2306
- "sec-fetch-mode": "cors",
2307
- "sec-fetch-site": "same-site",
2308
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
2309
- cookie: cookie ?? ""
2310
- } };
3542
+ var parseValidateCodeResult = (payload) => {
3543
+ const data = payload.data ?? {};
3544
+ const errorCode = readErrorCode(payload);
3545
+ if (errorCode === 0 || asString(data.ticket) || errorCode === void 0 && payload.message === "success") return {
3546
+ ok: true,
3547
+ wrongCode: false,
3548
+ message: ""
3549
+ };
3550
+ return {
3551
+ ok: false,
3552
+ wrongCode: errorCode === ERROR_WRONG_CODE,
3553
+ errorCode,
3554
+ message: readMessage(payload) || (errorCode === ERROR_WRONG_CODE ? "验证码错误" : `验证失败 error_code=${errorCode ?? "unknown"}`)
3555
+ };
2311
3556
  };
2312
3557
  //#endregion
3558
+ //#region ../amagi/packages/core/src/platform/douyin/passport/index.ts
3559
+ var passport_exports = /* @__PURE__ */ __exportAll({
3560
+ BDMS_SDK_VERSION: () => BDMS_SDK_VERSION,
3561
+ CookieJar: () => CookieJar,
3562
+ DouyinPassportClient: () => DouyinPassportClient,
3563
+ INTERNAL_PREFIX: () => INTERNAL_PREFIX,
3564
+ PASSPORT_USER_AGENT: () => PASSPORT_USER_AGENT,
3565
+ TicketGuard: () => TicketGuard,
3566
+ aBogus: () => aBogus,
3567
+ makeAidSign: () => makeAidSign,
3568
+ makeSignAndQs: () => makeSignAndQs,
3569
+ parsePollResult: () => parsePollResult,
3570
+ parseQrcode: () => parseQrcode,
3571
+ parseSendCodeResult: () => parseSendCodeResult,
3572
+ parseValidateCodeResult: () => parseValidateCodeResult,
3573
+ randomHex: () => randomHex,
3574
+ sm3: () => sm3,
3575
+ sm3Hex: () => sm3Hex,
3576
+ sm3Twice: () => sm3Twice,
3577
+ utcNoonTimestamp: () => utcNoonTimestamp,
3578
+ xor5Hex: () => xor5Hex
3579
+ });
3580
+ //#endregion
2313
3581
  //#region ../amagi/packages/core/src/types/NetworksConfigType.ts
2314
3582
  /** 未知错误 */
2315
3583
  var amagiAPIErrorCode = /* @__PURE__ */ function(amagiAPIErrorCode) {
@@ -2460,6 +3728,389 @@ var xiaohongshuAPIErrorCode = /* @__PURE__ */ function(xiaohongshuAPIErrorCode)
2460
3728
  return xiaohongshuAPIErrorCode;
2461
3729
  }({});
2462
3730
  //#endregion
3731
+ //#region ../amagi/packages/core/src/model/fetchers/douyin/auth.ts
3732
+ /**
3733
+ * 抖音登录认证相关 API(passport 扫码登录)
3734
+ *
3735
+ * 与其它 fetcher 的差别:这几个接口不走 `DouyinData` 的 URL 拼装 + a_bogus 流水线,
3736
+ * 因为 passport 体系有自己的三重 query 签名、独立的 a_bogus 形态与双重编码规则。
3737
+ * 对外形态保持一致:同样是 `(options, cookie?, requestConfig?) => Result<T>`,
3738
+ * 同样发 `apiSuccess` / `apiError` 事件,同样复用 amagi 的代理、超时与重试。
3739
+ *
3740
+ * 这几个方法都是无状态的:会话状态全部装在 cookie 串里,调用方拿到返回的 `cookie`
3741
+ * 后在下一次调用时传回来即可。轮询循环由调用方维护。
3742
+ *
3743
+ * @module fetchers/douyin/auth
3744
+ */
3745
+ /** 短信验证码的验证方式标识,服务端未给出可用方式时的兜底值 */
3746
+ var SMS_VERIFY_WAY = "mobile_sms_verify";
3747
+ /**
3748
+ * 可以用「收 6 位验证码」这套流程走完的验证方式
3749
+ *
3750
+ * 除官方常见的 `mobile_sms_verify`,账号被判定需要辅助验证时会给出
3751
+ * `assist_mobile_sms_verify`,两者都是下行短信收码,走同一对
3752
+ * `send_code` / `validate_code` 接口,区别只在 `std_verify_way` 的取值。
3753
+ * 上行短信(`*_up_sms_verify`)要求用户从手机发短信出去,是另一套接口,不在此列。
3754
+ */
3755
+ var SMS_CODE_WAY_PATTERN = /^(assist_)?mobile_sms_verify$/;
3756
+ /**
3757
+ * 判断某个验证方式能否用短信验证码流程完成
3758
+ * @param verifyWay 服务端下发的 verify_way
3759
+ */
3760
+ var isSmsCodeVerifyWay = (verifyWay) => SMS_CODE_WAY_PATTERN.test(verifyWay);
3761
+ /**
3762
+ * 选出本次要用的 std_verify_way
3763
+ *
3764
+ * 优先用调用方指定的;否则从服务端给出的可选方式里挑一个能收码的;都没有才回退到默认值。
3765
+ * 之前这里写死 `mobile_sms_verify`,遇到辅助验证的账号会因为 way 对不上而失败。
3766
+ * @param verify 轮询下发的验证上下文
3767
+ * @param requested 调用方显式指定的验证方式
3768
+ */
3769
+ var resolveVerifyWay = (verify, requested) => requested ?? verify.verifyWays.find((way) => isSmsCodeVerifyWay(way.verifyWay))?.verifyWay ?? verify.stdParams.std_verify_way ?? SMS_VERIFY_WAY;
3770
+ /** 短信验证码的 act_type */
3771
+ var SMS_ACT_TYPE = "3737";
3772
+ /** 验证页 SDK 版本,随表单一起提交 */
3773
+ var AUTHN_VERSION = "1.0.0.420-web";
3774
+ /** 抖音 web 的 aid */
3775
+ var AID = "6383";
3776
+ /** 扫码成功后的跳转地址 */
3777
+ var NEXT_URL = "https://www.douyin.com";
3778
+ /**
3779
+ * 发码与验码共用的表单字段
3780
+ *
3781
+ * 字段顺序与「空值也要占位」的行为对齐官方验证页 SDK 的抓包形态:
3782
+ * `verify_ticket` / `new_verify_flow` / `std_verify_flow_id` / `std_verify_token`
3783
+ * 即使为空也必须出现,缺字段会被判为伪造请求。
3784
+ * @param verify 轮询下发的验证上下文
3785
+ * @param verifyWay 本次使用的验证方式,原样进 `std_verify_way`
3786
+ * @param tail 追加在 std_verify_way 之后的字段(发码是 is6Digits,验码是 code)
3787
+ */
3788
+ var buildVerifyBody = (verify, verifyWay, tail) => ({
3789
+ mix_mode: "1",
3790
+ type: SMS_ACT_TYPE,
3791
+ encrypt_uid: verify.encryptUid,
3792
+ verify_ticket: verify.verifyTicket,
3793
+ copywriting_key: verify.copywritingKey,
3794
+ ies_safety_diversion_tag: verify.diversionTag,
3795
+ new_verify_flow: verify.newVerifyFlow,
3796
+ std_verify_flow_id: verify.stdParams.std_verify_flow_id ?? "",
3797
+ std_verify_scene: verify.stdParams.std_verify_scene ?? "account_login",
3798
+ std_verify_template: verify.stdParams.std_verify_template ?? "ato_web",
3799
+ std_verify_token: verify.stdParams.std_verify_token ?? "",
3800
+ std_verify_type: verify.stdParams.std_verify_type ?? "MFA",
3801
+ std_verify_way: verifyWay,
3802
+ ...tail,
3803
+ aid: AID,
3804
+ new_authn_sdk_version: AUTHN_VERSION
3805
+ });
3806
+ /**
3807
+ * 构造 passport 侧的业务错误响应
3808
+ * @param methodType 方法名,进 amagiError.requestType
3809
+ * @param message 错误描述
3810
+ */
3811
+ var passportError = (methodType, message) => createErrorResponse({
3812
+ code: amagiAPIErrorCode.UNKNOWN,
3813
+ data: null,
3814
+ amagiError: {
3815
+ errorDescription: message,
3816
+ requestType: methodType,
3817
+ requestUrl: `https://login.douyin.com/passport/`
3818
+ },
3819
+ amagiMessage: message
3820
+ }, message);
3821
+ /** 统一包一层事件上报与异常兜底 */
3822
+ var run = async (methodType, task) => {
3823
+ const startTime = Date.now();
3824
+ try {
3825
+ const result = await task();
3826
+ const duration = Date.now() - startTime;
3827
+ if (result.code === 200) emitApiSuccess({
3828
+ platform: "douyin",
3829
+ methodType,
3830
+ response: result,
3831
+ statusCode: 200,
3832
+ duration
3833
+ });
3834
+ else emitApiError({
3835
+ platform: "douyin",
3836
+ methodType,
3837
+ errorCode: result.code,
3838
+ errorMessage: result.message,
3839
+ duration
3840
+ });
3841
+ return result;
3842
+ } catch (error) {
3843
+ const duration = Date.now() - startTime;
3844
+ const errorMessage = error instanceof Error ? error.message : "未知错误";
3845
+ emitApiError({
3846
+ platform: "douyin",
3847
+ methodType,
3848
+ errorMessage,
3849
+ duration
3850
+ });
3851
+ throw new Error(`抖音登录请求失败: ${errorMessage}`);
3852
+ }
3853
+ };
3854
+ /**
3855
+ * 申请抖音扫码登录二维码
3856
+ *
3857
+ * 首次调用会自动完成环境指纹初始化(`__ac_nonce` + `ttwid`),无需额外准备。
3858
+ * @param options - 请求选项 (可选)
3859
+ * @param cookie - 已有的会话 Cookie (可选,续用同一会话时传入)
3860
+ * @param requestConfig - 请求配置 (可选)
3861
+ * @returns 二维码令牌、内容与会话 cookie
3862
+ * @example
3863
+ * ```typescript
3864
+ * const qrcode = await requestPassportQrcode()
3865
+ * console.log(qrcode.data.content) // 拿去生成二维码图片
3866
+ * ```
3867
+ */
3868
+ async function requestPassportQrcode(options, cookie, requestConfig) {
3869
+ return run("passportQrcode", async () => {
3870
+ const client = new DouyinPassportClient(cookie, requestConfig);
3871
+ await client.bootstrap();
3872
+ const response = await client.request("/passport/web/get_qrcode/", {
3873
+ next: NEXT_URL,
3874
+ need_short_url: "true",
3875
+ need_logo: "false",
3876
+ is_new_login: "1"
3877
+ });
3878
+ const qrcode = parseQrcode(response.body);
3879
+ if (!qrcode) return passportError("passportQrcode", response.body.message || `获取二维码失败: ${response.raw.slice(0, 200)}`);
3880
+ return createSuccessResponse$1({
3881
+ token: qrcode.token,
3882
+ content: qrcode.content,
3883
+ expire_time: qrcode.expireTime,
3884
+ expires_in: Math.max(0, qrcode.expireTime - Math.floor(Date.now() / 1e3)),
3885
+ cookie: response.cookie
3886
+ }, "获取成功", 200);
3887
+ });
3888
+ }
3889
+ /**
3890
+ * 查询抖音扫码登录二维码的状态
3891
+ *
3892
+ * 状态为 `confirmed` 时会自动跟随 SSO 跳转领取登录凭证,返回的 `cookie` 即完整登录态。
3893
+ * @param options - 二维码状态参数
3894
+ * @param options.token - `requestPassportQrcode` 返回的令牌
3895
+ * @param cookie - 会话 Cookie,必须是申请二维码时返回的那一份
3896
+ * @param requestConfig - 请求配置 (可选)
3897
+ * @returns 扫码状态与最新会话 cookie
3898
+ * @example
3899
+ * ```typescript
3900
+ * const status = await checkPassportQrcode({ token }, cookie)
3901
+ * // new 未扫码 / scanned 已扫待确认 / verify 需二次验证 / confirmed 登录成功 / expired 已过期
3902
+ * console.log(status.data.status)
3903
+ * ```
3904
+ */
3905
+ async function checkPassportQrcode(options, cookie, requestConfig) {
3906
+ return run("passportQrcodeStatus", async () => {
3907
+ if (!options?.token) return passportError("passportQrcodeStatus", "缺少 token 参数");
3908
+ const client = new DouyinPassportClient(cookie, requestConfig);
3909
+ const response = await client.request("/passport/web/check_qrconnect/", {
3910
+ next: NEXT_URL,
3911
+ need_logo: "false",
3912
+ is_frontier: "true",
3913
+ token: options.token,
3914
+ is_new_login: "1",
3915
+ need_short_url: "true"
3916
+ });
3917
+ const result = parsePollResult(response.body);
3918
+ if (result.status === "verify") emitLogDebug(`[douyin passport] 触发二次验证,服务端原始响应: ${response.raw.slice(0, 1e3)}`);
3919
+ if (result.status === "confirmed" && result.redirectUrl) await client.followSsoRedirect(result.redirectUrl);
3920
+ const sessionCookie = result.status === "confirmed" ? client.cookies.toString() : client.cookies.serialize();
3921
+ return createSuccessResponse$1({
3922
+ ...result,
3923
+ cookie: sessionCookie,
3924
+ logged_in: client.cookies.isLoggedIn()
3925
+ }, "获取成功", 200);
3926
+ });
3927
+ }
3928
+ /**
3929
+ * 向账号绑定手机发送二次验证短信验证码
3930
+ *
3931
+ * 用于轮询返回 `status: 'verify'`(即 `error_code=2046` / `account_flow=verify`)的场景。
3932
+ * @param options - 发码参数
3933
+ * @param options.verify - 轮询返回的验证上下文
3934
+ * @param options.biz_trace_id - 追踪 ID (可选,不传自动生成)
3935
+ * @param cookie - 会话 Cookie
3936
+ * @param requestConfig - 请求配置 (可选)
3937
+ * @returns 脱敏手机号、重发等待秒数与追踪 ID
3938
+ */
3939
+ async function sendPassportVerifyCode(options, cookie, requestConfig) {
3940
+ return run("passportSendCode", async () => {
3941
+ if (!options?.verify?.encryptUid) return passportError("passportSendCode", "缺少 encrypt_uid,请从轮询响应中取得验证上下文");
3942
+ const bizTraceId = options.biz_trace_id ?? randomHex(8);
3943
+ const verifyWay = resolveVerifyWay(options.verify, options.verify_way);
3944
+ emitLogDebug(`[douyin passport] 发码使用的验证方式: ${verifyWay}`);
3945
+ const response = await new DouyinPassportClient(cookie, requestConfig).liteRequest("/passport/web/send_code/", buildVerifyBody(options.verify, verifyWay, { is6Digits: "1" }), bizTraceId);
3946
+ const result = parseSendCodeResult(response.body);
3947
+ if (!result.ok) emitLogDebug(`[douyin passport] 发码失败原文: ${response.raw.slice(0, 500)}`);
3948
+ return createSuccessResponse$1({
3949
+ ...result,
3950
+ cookie: response.cookie,
3951
+ biz_trace_id: bizTraceId,
3952
+ verify_way: verifyWay
3953
+ }, "获取成功", 200);
3954
+ });
3955
+ }
3956
+ /**
3957
+ * 提交二次验证的短信验证码
3958
+ * @param options - 验码参数
3959
+ * @param options.verify - 轮询返回的验证上下文
3960
+ * @param options.code - 用户收到的 6 位验证码明文
3961
+ * @param options.biz_trace_id - 必须与发码时用的是同一个
3962
+ * @param cookie - 会话 Cookie
3963
+ * @param requestConfig - 请求配置 (可选)
3964
+ * @returns 验证结果;`wrongCode` 为 true 表示验证码填错,可以让用户重试
3965
+ */
3966
+ async function validatePassportVerifyCode(options, cookie, requestConfig) {
3967
+ return run("passportValidateCode", async () => {
3968
+ if (!options?.verify?.encryptUid) return passportError("passportValidateCode", "缺少 encrypt_uid,请从轮询响应中取得验证上下文");
3969
+ if (!options.code) return passportError("passportValidateCode", "缺少 code,请填入收到的短信验证码");
3970
+ 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));
3971
+ const result = parseValidateCodeResult(response.body);
3972
+ if (!result.ok) emitLogDebug(`[douyin passport] 验码失败原文: ${response.raw.slice(0, 500)}`);
3973
+ return createSuccessResponse$1({
3974
+ ...result,
3975
+ cookie: response.cookie
3976
+ }, "获取成功", 200);
3977
+ });
3978
+ }
3979
+ //#endregion
3980
+ //#region ../amagi/packages/core/src/platform/defaultConfigs.ts
3981
+ /**
3982
+ * 根据User-Agent生成对应的Sec-Ch-Ua值
3983
+ * @param userAgent - 用户代理字符串
3984
+ * @returns 对应的Sec-Ch-Ua值
3985
+ */
3986
+ var generateSecChUa = (userAgent) => {
3987
+ const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
3988
+ const chromeVersion = chromeMatch ? chromeMatch[1] : "125";
3989
+ return `"Not_A Brand";v="99", "Chromium";v="${chromeVersion}", "Google Chrome";v="${chromeVersion}"`;
3990
+ };
3991
+ /**
3992
+ * 抖音平台默认请求配置
3993
+ * @param cookie - 用户Cookie
3994
+ * @param requestConfig - 外部请求配置(优先级最高)
3995
+ * @returns 合并后的请求配置
3996
+ */
3997
+ var getDouyinDefaultConfig = (cookie, requestConfig) => {
3998
+ let finalUserAgent = requestConfig?.headers?.["User-Agent"] ?? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
3999
+ finalUserAgent = finalUserAgent.replace(/\s+Edg\/[\d.]+/g, "");
4000
+ const defHeaders = {
4001
+ Accept: "application/json, text/plain, */*",
4002
+ "Accept-Encoding": "gzip, deflate, br, zstd",
4003
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
4004
+ Cookie: cookie?.trim() ?? "",
4005
+ Priority: "u=1, i",
4006
+ Referer: "https://www.douyin.com/",
4007
+ "Sec-Ch-Ua": generateSecChUa(finalUserAgent),
4008
+ "Sec-Ch-Ua-Mobile": "?0",
4009
+ "Sec-Ch-Ua-Platform": "\"Windows\"",
4010
+ "Sec-Fetch-Dest": "empty",
4011
+ "Sec-Fetch-Mode": "cors",
4012
+ "Sec-Fetch-Site": "same-origin",
4013
+ "User-Agent": finalUserAgent
4014
+ };
4015
+ return {
4016
+ method: "GET",
4017
+ timeout: 1e4,
4018
+ ...requestConfig,
4019
+ headers: {
4020
+ ...defHeaders,
4021
+ ...requestConfig?.headers ?? {}
4022
+ }
4023
+ };
4024
+ };
4025
+ /**
4026
+ * B站平台默认请求配置
4027
+ * @param cookie - 用户Cookie
4028
+ * @param requestConfig - 外部请求配置(优先级最高)
4029
+ * @returns 合并后的请求配置
4030
+ */
4031
+ var getBilibiliDefaultConfig = (cookie, requestConfig) => {
4032
+ let finalUserAgent = requestConfig?.headers?.["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";
4033
+ finalUserAgent = finalUserAgent.replace(/\s+Edg\/[\d.]+/g, "");
4034
+ const defHeaders = {
4035
+ Accept: "application/json, text/plain, */*",
4036
+ "Accept-Language": "zh-CN,zh;q=0.9",
4037
+ "Accept-Encoding": "gzip, deflate, br, zstd",
4038
+ "Cache-Control": "no-cache",
4039
+ Pragma: "no-cache",
4040
+ Referer: "https://www.bilibili.com/",
4041
+ Priority: "u=1, i",
4042
+ "Sec-Ch-Ua": generateSecChUa(finalUserAgent),
4043
+ "Sec-Ch-Ua-Mobile": "?0",
4044
+ "Sec-Ch-Ua-Platform": "\"Windows\"",
4045
+ "Sec-Fetch-Dest": "empty",
4046
+ "Sec-Fetch-Mode": "cors",
4047
+ "Sec-Fetch-Site": "same-site",
4048
+ "User-Agent": finalUserAgent,
4049
+ Cookie: cookie?.trim() ?? ""
4050
+ };
4051
+ return {
4052
+ method: "GET",
4053
+ timeout: 1e4,
4054
+ ...requestConfig,
4055
+ headers: {
4056
+ ...defHeaders,
4057
+ ...requestConfig?.headers ?? {}
4058
+ }
4059
+ };
4060
+ };
4061
+ /**
4062
+ * 快手平台默认请求配置
4063
+ * @param cookie - 用户Cookie
4064
+ * @param requestConfig - 外部请求配置(优先级最高)
4065
+ * @returns 合并后的请求配置
4066
+ */
4067
+ var getKuaishouDefaultConfig = (cookie, requestConfig) => {
4068
+ const defHeaders = {
4069
+ Referer: "https://www.kuaishou.com/new-reco",
4070
+ Origin: "https://www.kuaishou.com",
4071
+ Accept: "application/json, text/plain, */*",
4072
+ "Accept-Encoding": "gzip, deflate, br, zstd",
4073
+ "Content-Type": "application/json",
4074
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
4075
+ Priority: "u=0, i",
4076
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
4077
+ Cookie: cookie?.trim() ?? ""
4078
+ };
4079
+ return {
4080
+ method: "POST",
4081
+ timeout: 1e4,
4082
+ ...requestConfig,
4083
+ headers: {
4084
+ ...defHeaders,
4085
+ ...requestConfig?.headers ?? {}
4086
+ }
4087
+ };
4088
+ };
4089
+ /**
4090
+ * 获取小红书默认配置
4091
+ * @param cookie - 用户Cookie
4092
+ * @returns 小红书请求配置
4093
+ */
4094
+ var getXiaohongshuDefaultConfig = (cookie) => {
4095
+ return { headers: {
4096
+ accept: "application/json, text/plain, */*",
4097
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
4098
+ "cache-control": "no-cache",
4099
+ "content-type": "application/json;charset=UTF-8",
4100
+ pragma: "no-cache",
4101
+ priority: "u=1, i",
4102
+ referer: "https://www.xiaohongshu.com/",
4103
+ "sec-ch-ua": "\"Microsoft Edge\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
4104
+ "sec-ch-ua-mobile": "?0",
4105
+ "sec-ch-ua-platform": "\"Windows\"",
4106
+ "sec-fetch-dest": "empty",
4107
+ "sec-fetch-mode": "cors",
4108
+ "sec-fetch-site": "same-site",
4109
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
4110
+ cookie: cookie ?? ""
4111
+ } };
4112
+ };
4113
+ //#endregion
2463
4114
  //#region ../amagi/packages/core/src/platform/douyin/sign/a_bogus.ts
2464
4115
  var SM3 = class {
2465
4116
  reg;
@@ -4745,6 +6396,10 @@ function createBoundDouyinFetcher(cookie, requestConfig) {
4745
6396
  * ```
4746
6397
  */
4747
6398
  var douyinFetcher$1 = {
6399
+ requestPassportQrcode,
6400
+ checkPassportQrcode,
6401
+ sendPassportVerifyCode,
6402
+ validatePassportVerifyCode,
4748
6403
  fetchVideoWork: fetchVideoWork$1,
4749
6404
  fetchImageAlbumWork,
4750
6405
  fetchSlidesWork,
@@ -8777,6 +10432,7 @@ var createDouyinRoutes = (cookie, requestConfig = getDouyinDefaultConfig(cookie)
8777
10432
  /** 抖音相关功能模块 (工具集) */
8778
10433
  var douyinUtils = {
8779
10434
  sign: douyinSign,
10435
+ passport: passport_exports,
8780
10436
  douyinApiUrls
8781
10437
  };
8782
10438
  //#endregion
@@ -9549,7 +11205,7 @@ function getApiRoute(platform, methodType) {
9549
11205
  * 构建后使用 __VERSION__,开发环境从 package.json 读取
9550
11206
  */
9551
11207
  var getVersion = () => {
9552
- return "6.5.0";
11208
+ return "6.6.0";
9553
11209
  };
9554
11210
  var VERSION = getVersion();
9555
11211
  /**
@@ -10341,11 +11997,11 @@ var Common = new class Tools {
10341
11997
  constructor() {
10342
11998
  this.tempDri = {
10343
11999
  /** 插件缓存目录 */
10344
- default: `${karinPathTemp$1}/${Root.pluginName}/`.replace(/\\/g, "/"),
12000
+ default: `${karinPathTemp}/${Root.pluginName}/`.replace(/\\/g, "/"),
10345
12001
  /** 视频缓存文件 */
10346
- video: `${karinPathTemp$1}/${Root.pluginName}/kkkdownload/video/`.replace(/\\/g, "/"),
12002
+ video: `${karinPathTemp}/${Root.pluginName}/kkkdownload/video/`.replace(/\\/g, "/"),
10347
12003
  /** 图片缓存文件 */
10348
- images: `${karinPathTemp$1}/${Root.pluginName}/kkkdownload/images/`.replace(/\\/g, "/")
12004
+ images: `${karinPathTemp}/${Root.pluginName}/kkkdownload/images/`.replace(/\\/g, "/")
10349
12005
  };
10350
12006
  this.videoPreviewState = /* @__PURE__ */ new Map();
10351
12007
  setInterval(() => {
@@ -25229,9 +26885,8 @@ var DouyinPosterHeader = ({ data, ctx }) => {
25229
26885
  })]
25230
26886
  });
25231
26887
  };
25232
- var DouyinVideoCover = ({ data }) => {
25233
- const { image_url, music, duration } = data;
25234
- const durationText = formatDuration$1(duration);
26888
+ var DouyinVideoCover = (props) => {
26889
+ const durationText = formatDuration$1(props.data.duration);
25235
26890
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
25236
26891
  className: "relative -mx-20 mt-12 overflow-visible",
25237
26892
  children: [
@@ -25242,7 +26897,7 @@ var DouyinVideoCover = ({ data }) => {
25242
26897
  WebkitMaskImage: ambientCoverMask
25243
26898
  },
25244
26899
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
25245
- src: image_url,
26900
+ src: props.data.image_url,
25246
26901
  alt: "",
25247
26902
  className: "h-full w-full scale-[1.16] object-cover opacity-48 blur-[72px] saturate-[1.2]",
25248
26903
  referrerPolicy: "no-referrer",
@@ -25250,7 +26905,7 @@ var DouyinVideoCover = ({ data }) => {
25250
26905
  })
25251
26906
  }),
25252
26907
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
25253
- src: image_url,
26908
+ src: props.data.image_url,
25254
26909
  alt: "视频封面",
25255
26910
  className: "relative z-10 block h-auto w-full drop-shadow-xl",
25256
26911
  style: {
@@ -25260,12 +26915,30 @@ var DouyinVideoCover = ({ data }) => {
25260
26915
  referrerPolicy: "no-referrer",
25261
26916
  crossOrigin: "anonymous"
25262
26917
  }),
25263
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
25264
- className: "absolute left-24 top-10 z-30 flex items-center gap-5 text-foreground/85",
25265
- children: durationText && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
25266
- className: "text-[34px] font-black tabular-nums",
26918
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
26919
+ className: "absolute inset-x-24 top-10 z-30 flex items-start justify-between text-foreground/85",
26920
+ children: [durationText && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
26921
+ className: "text-4xl font-black leading-none tabular-nums",
25267
26922
  children: ["时长: ", durationText]
25268
- })
26923
+ }), props.data.resolution && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
26924
+ className: "ml-auto flex flex-col items-end gap-1.5",
26925
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
26926
+ className: "text-3xl font-black leading-none tracking-[0.08em] select-text",
26927
+ children: [props.data.resolution.name, props.data.is_HDR && (isDark(props.ctx) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GlowText, {
26928
+ glowStrength: 1,
26929
+ blurRadius: 10,
26930
+ children: ` · HDR`
26931
+ }) : ` · HDR`)]
26932
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
26933
+ className: "text-2xl font-bold tracking-[0.08em] text-foreground/60 tabular-nums select-text",
26934
+ children: [
26935
+ props.data.resolution.width,
26936
+ " × ",
26937
+ props.data.resolution.height,
26938
+ " px"
26939
+ ]
26940
+ })]
26941
+ })]
25269
26942
  }),
25270
26943
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(a, {
25271
26944
  size: 104,
@@ -25273,15 +26946,15 @@ var DouyinVideoCover = ({ data }) => {
25273
26946
  "aria-label": "播放",
25274
26947
  className: "absolute bottom-12 right-24 z-30 text-white/50"
25275
26948
  }),
25276
- music && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
26949
+ props.data.music && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
25277
26950
  className: "absolute bottom-12 left-24 z-30 flex max-w-212.5 items-center gap-5 text-foreground/85",
25278
- children: [music.cover ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
26951
+ children: [props.data.music.cover ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
25279
26952
  className: "relative h-20 w-20 shrink-0",
25280
26953
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GlowImage, {
25281
26954
  glowStrength: 1,
25282
26955
  blurRadius: 20,
25283
26956
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
25284
- src: music.cover,
26957
+ src: props.data.music.cover,
25285
26958
  alt: "BGM封面",
25286
26959
  className: "relative z-10 h-full w-full rounded-2xl object-cover",
25287
26960
  referrerPolicy: "no-referrer",
@@ -25296,10 +26969,10 @@ var DouyinVideoCover = ({ data }) => {
25296
26969
  className: "min-w-0",
25297
26970
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
25298
26971
  className: "truncate text-[36px] font-black leading-tight select-text",
25299
- children: music.title
26972
+ children: props.data.music.title
25300
26973
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
25301
26974
  className: "truncate text-[27px] font-semibold text-foreground/60 select-text",
25302
- children: music.author
26975
+ children: props.data.music.author
25303
26976
  })]
25304
26977
  })]
25305
26978
  })
@@ -37211,6 +38884,196 @@ async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {
37211
38884
  return result.status;
37212
38885
  }
37213
38886
  //#endregion
38887
+ //#region src/platform/douyin/videoQuality.ts
38888
+ /** 档位优先级,从高到低 */
38889
+ var QUALITY_PRIORITY = [
38890
+ "4k",
38891
+ "2k",
38892
+ "1080p",
38893
+ "720p",
38894
+ "540p"
38895
+ ];
38896
+ /**
38897
+ * `video_extra.definition` → 内部档位名。
38898
+ * 抖音上游把 2K 记作 `1440p`,而配置项 `videoQuality` 用的是 `2k`,
38899
+ * 这张表是两套词表唯一的翻译点,配置域不需要跟着上游改。
38900
+ */
38901
+ var DEFINITION_TO_LEVEL = {
38902
+ "4k": "4k",
38903
+ "2k": "2k",
38904
+ "1440p": "2k",
38905
+ "1080p": "1080p",
38906
+ "720p": "720p",
38907
+ "540p": "540p"
38908
+ };
38909
+ /** 档位 → 展示标签,沿用抖音 Web 端播放器的说法 */
38910
+ var QUALITY_LABEL = {
38911
+ "4k": "超清4K",
38912
+ "2k": "超清2K",
38913
+ "1080p": "高清1080P",
38914
+ "720p": "高清720P",
38915
+ "540p": "标清540P"
38916
+ };
38917
+ /**
38918
+ * 安全解析 `video_extra`
38919
+ * @param extra - 原始 JSON 字符串
38920
+ * @returns 解析结果,为空或非法 JSON 时返回 null
38921
+ */
38922
+ var parseVideoExtra = (extra) => {
38923
+ if (!extra) return null;
38924
+ try {
38925
+ return JSON.parse(extra);
38926
+ } catch {
38927
+ return null;
38928
+ }
38929
+ };
38930
+ /**
38931
+ * 从 `gear_name` 猜测档位。
38932
+ * 只作为 `video_extra.definition` 缺失时的兜底:抖音随时可能新增 gear_name,
38933
+ * 认不出来就返回 undefined 交给调用方处理,不再默认落到 540p 污染分档。
38934
+ * @param gearName - 视频源的 gear_name
38935
+ * @returns 档位名,认不出时返回 undefined
38936
+ */
38937
+ var guessLevelFromGearName = (gearName) => {
38938
+ if (gearName.includes("lowest_4") || gearName.includes("2160")) return "4k";
38939
+ if (gearName.includes("1440") || gearName.includes("2k")) return "2k";
38940
+ if (gearName.includes("1080")) return "1080p";
38941
+ if (gearName.includes("720")) return "720p";
38942
+ if (gearName.includes("540")) return "540p";
38943
+ };
38944
+ /**
38945
+ * 判定单个视频源的清晰度档位。
38946
+ * `video_extra.definition` 是唯一可信来源:抖音视频比例不固定(存在 21:9、1:1 等),
38947
+ * 一个 3840x1608 的视频不满足 3840x2160 但官方仍定义为 4K,所以不能拿 play_addr 的宽高硬匹配。
38948
+ * @param video - 视频源对象
38949
+ * @returns 档位名,无法判定时返回 undefined
38950
+ */
38951
+ var getDouyinQualityLevel = (video) => {
38952
+ const definition = parseVideoExtra(video.video_extra)?.definition;
38953
+ return (definition ? DEFINITION_TO_LEVEL[definition] : void 0) ?? guessLevelFromGearName(video.gear_name ?? "");
38954
+ };
38955
+ /**
38956
+ * 同档位内的取源顺序:先 H.264,再按体积从大到小。
38957
+ * 1080p / 720p 的 H.265(ByteVC1)在部分协议端软解失败,同档位下 H.264 更稳;
38958
+ * 4K / 2K 上游通常只提供 H.265,此时全组同权重,自然退化为按体积排序。
38959
+ * @param a - 视频源 A
38960
+ * @param b - 视频源 B
38961
+ * @returns 排序比较值
38962
+ */
38963
+ var compareVideoPreference = (a, b) => {
38964
+ if (a.is_bytevc1 !== b.is_bytevc1) return a.is_bytevc1 - b.is_bytevc1;
38965
+ return b.play_addr.data_size - a.play_addr.data_size;
38966
+ };
38967
+ /**
38968
+ * 按档位给 mp4 视频源分组,组内已按 {@link compareVideoPreference} 排好序
38969
+ * @param videos - 视频源数组
38970
+ * @returns 档位 → 该档位下的视频源
38971
+ */
38972
+ var groupByQualityLevel = (videos) => {
38973
+ const grouped = /* @__PURE__ */ new Map();
38974
+ for (const video of videos) {
38975
+ const level = getDouyinQualityLevel(video);
38976
+ if (!level) {
38977
+ logger.debug(`无法判定清晰度档位,跳过该视频源:gear_name=${video.gear_name}`);
38978
+ continue;
38979
+ }
38980
+ const bucket = grouped.get(level);
38981
+ if (bucket) bucket.push(video);
38982
+ else grouped.set(level, [video]);
38983
+ }
38984
+ grouped.forEach((bucket) => bucket.sort(compareVideoPreference));
38985
+ return grouped;
38986
+ };
38987
+ /**
38988
+ * 构建抖音视频播放地址(二维码分享链接与下载共用)。
38989
+ * 上游 play 接口现在除 video_id 外还要求携带 file_id(从 play_addr.url_list[2] 的查询参数中提取),
38990
+ * 旧的 ratio/line 参数不再需要;file_id 提取失败时回退为仅带 video_id。
38991
+ * @param playAddr - 视频源的 play_addr 对象(bit_rate 项或 images 项的 play_addr_h264)
38992
+ * @returns 完整的播放地址
38993
+ */
38994
+ var buildDouyinPlayUrl = (playAddr) => {
38995
+ const fileId = (() => {
38996
+ try {
38997
+ const wrappedUrl = playAddr.url_list?.[2];
38998
+ return wrappedUrl ? new URL(wrappedUrl).searchParams.get("file_id") : null;
38999
+ } catch {
39000
+ return null;
39001
+ }
39002
+ })();
39003
+ return fileId ? `https://c/aweme/v1/play/?video_id=${playAddr.uri}&file_id=${fileId}` : `https://aweme.snssdk.com/aweme/v1/play/?video_id=${playAddr.uri}`;
39004
+ };
39005
+ /**
39006
+ * 把选中的视频源格式化成展示用的清晰度字符串。
39007
+ * 传入的必须是 {@link douyinProcessVideos} 选中的那一路源 —— 卡片上展示的清晰度要和实际下载的一致,
39008
+ * 否则会出现「卡片写 4K、实际下载 720p」的错位。
39009
+ * @param video - 选中的视频源
39010
+ * @returns 形如 `超清4K`;档位认不出时返回空字符串
39011
+ */
39012
+ var formatDouyinQualityLabel = (video) => {
39013
+ if (!video) return "";
39014
+ const level = getDouyinQualityLevel(video);
39015
+ if (!level) return "";
39016
+ return QUALITY_LABEL[level];
39017
+ };
39018
+ /**
39019
+ * 按画质偏好从抖音视频源列表里挑出唯一一路可下载的源
39020
+ * @param videos - `aweme_detail.video.bit_rate` 数组
39021
+ * @param videoQuality - 画质偏好,`adapt` 为按体积上限自动选择,其余为固定档位
39022
+ * @param maxAutoVideoSize - 自动模式下可接受的最大体积(MB),缺省回落到 `Config.app.filelimit`
39023
+ * @returns 长度为 1 的数组,元素为选中的视频源
39024
+ */
39025
+ var douyinProcessVideos = (videos, videoQuality, maxAutoVideoSize) => {
39026
+ const mp4Videos = videos.filter((video) => video.format === "mp4");
39027
+ if (mp4Videos.length === 0) {
39028
+ logger.warn("没有找到可用的 mp4 格式视频");
39029
+ return videos.slice(0, 1);
39030
+ }
39031
+ logger.debug(`过滤后剩余 ${mp4Videos.length} 个 mp4 格式视频`);
39032
+ const videosByQuality = groupByQualityLevel(mp4Videos);
39033
+ if (videoQuality === "adapt") {
39034
+ const sizeLimitBytes = (maxAutoVideoSize || Config.app.filelimit) * 1024 * 1024;
39035
+ for (const quality of QUALITY_PRIORITY) {
39036
+ const qualityVideos = videosByQuality.get(quality);
39037
+ if (qualityVideos && qualityVideos.length > 0) {
39038
+ const suitableVideo = qualityVideos.find((video) => video.play_addr.data_size <= sizeLimitBytes);
39039
+ if (suitableVideo) {
39040
+ logger.debug(`自动选择画质: ${quality}, 文件大小: ${(suitableVideo.play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
39041
+ return [suitableVideo];
39042
+ }
39043
+ }
39044
+ }
39045
+ let smallestVideo = mp4Videos[0];
39046
+ mp4Videos.forEach((video) => {
39047
+ if (video.play_addr.data_size < smallestVideo.play_addr.data_size) smallestVideo = video;
39048
+ });
39049
+ logger.debug(`未找到符合大小限制的视频,选择最小视频: ${(smallestVideo.play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
39050
+ return [smallestVideo];
39051
+ }
39052
+ const targetQuality = videoQuality;
39053
+ const targetVideos = videosByQuality.get(targetQuality);
39054
+ if (targetVideos && targetVideos.length > 0) {
39055
+ logger.debug(`选择固定画质: ${targetQuality}, 文件大小: ${(targetVideos[0].play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
39056
+ return [targetVideos[0]];
39057
+ }
39058
+ const targetIndex = QUALITY_PRIORITY.indexOf(targetQuality);
39059
+ for (let i = targetIndex + 1; i < QUALITY_PRIORITY.length; i++) {
39060
+ const fallbackVideos = videosByQuality.get(QUALITY_PRIORITY[i]);
39061
+ if (fallbackVideos && fallbackVideos.length > 0) {
39062
+ logger.debug(`目标画质 ${targetQuality} 不可用,降级到: ${QUALITY_PRIORITY[i]}`);
39063
+ return [fallbackVideos[0]];
39064
+ }
39065
+ }
39066
+ for (let i = targetIndex - 1; i >= 0; i--) {
39067
+ const fallbackVideos = videosByQuality.get(QUALITY_PRIORITY[i]);
39068
+ if (fallbackVideos && fallbackVideos.length > 0) {
39069
+ logger.debug(`目标画质 ${targetQuality} 不可用,升级到: ${QUALITY_PRIORITY[i]}`);
39070
+ return [fallbackVideos[0]];
39071
+ }
39072
+ }
39073
+ logger.warn("未找到任何匹配的画质,返回默认视频");
39074
+ return [mp4Videos[0]];
39075
+ };
39076
+ //#endregion
37214
39077
  //#region src/platform/douyin/workType.ts
37215
39078
  /**
37216
39079
  * 抖音作品类型工具模块
@@ -37337,7 +39200,7 @@ function getDouyinLiveImageSendPolicy(clipType, livePhotoMode = "video_and_livep
37337
39200
  * @returns 如果描述为空则返回默认提示,否则返回原文
37338
39201
  */
37339
39202
  function desc(Desc) {
37340
- return Desc === "" ? "该作品没有描述" : Desc;
39203
+ return !Desc ? "该作品没有描述" : Desc;
37341
39204
  }
37342
39205
  /**
37343
39206
  * 构建合作信息数据
@@ -37356,8 +39219,8 @@ function buildCooperationInfo(Detail_Data) {
37356
39219
  const co_creators = rawCreators.map((c) => {
37357
39220
  return {
37358
39221
  avatar_url: c.avatar_thumb?.url_list?.[0] ?? (c.avatar_thumb?.uri ? `https://p3.douyinpic.com/${c.avatar_thumb.uri}` : void 0),
37359
- nickname: c.nickname,
37360
- role_title: c.role_title
39222
+ nickname: c.nickname ?? "",
39223
+ role_title: c.role_title ?? ""
37361
39224
  };
37362
39225
  });
37363
39226
  if (Detail_Data.author && !rawCreators.some((c) => Detail_Data.author?.uid && c.uid && c.uid === Detail_Data.author.uid || Detail_Data.author?.sec_uid && c.sec_uid && c.sec_uid === Detail_Data.author.sec_uid || Detail_Data.author?.nickname && c.nickname && c.nickname === Detail_Data.author.nickname)) co_creators.unshift({
@@ -37646,7 +39509,7 @@ function getDefaultPushLabel(workTypeInfo) {
37646
39509
  * @returns 渲染后的图片元素数组
37647
39510
  */
37648
39511
  async function renderWorkImage(options) {
37649
- const { e, Detail_Data, create_time, shareLink, skipWatermark = false } = options;
39512
+ const { e, Detail_Data, create_time, shareLink, skipWatermark = false, videoSource } = options;
37650
39513
  const workTypeInfo = getWorkTypeInfo(Detail_Data);
37651
39514
  const dynamicTypeLabel = options.dynamicTypeLabel ?? getDefaultPushLabel(workTypeInfo);
37652
39515
  const coverUrl = getWorkCoverUrl(workTypeInfo, Detail_Data);
@@ -37661,10 +39524,12 @@ async function renderWorkImage(options) {
37661
39524
  const renderOpts = skipWatermark ? { skipWatermark: true } : void 0;
37662
39525
  switch (workTypeInfo.mainType) {
37663
39526
  case DouyinWorkMainType.ARTICLE: {
37664
- const content = JSON.parse(Detail_Data.article_info.article_content);
37665
- const fe_data = JSON.parse(Detail_Data.article_info.fe_data);
39527
+ const articleInfo = Detail_Data.article_info;
39528
+ if (!articleInfo) return [];
39529
+ const content = JSON.parse(articleInfo.article_content);
39530
+ const fe_data = JSON.parse(articleInfo.fe_data);
37666
39531
  return await Render(e, "douyin/article-work", {
37667
- title: Detail_Data.article_info.article_title,
39532
+ title: articleInfo.article_title,
37668
39533
  markdown: content.markdown,
37669
39534
  images: fe_data.image_list || [],
37670
39535
  read_time: fe_data.read_time || 0,
@@ -37690,6 +39555,12 @@ async function renderWorkImage(options) {
37690
39555
  suggest_word: extractSuggestWord(Detail_Data),
37691
39556
  music: buildMusicInfo(Detail_Data.music),
37692
39557
  duration: Detail_Data.duration,
39558
+ resolution: videoSource ? {
39559
+ name: formatDouyinQualityLabel(videoSource),
39560
+ width: videoSource.play_addr.width,
39561
+ height: videoSource.play_addr.height
39562
+ } : void 0,
39563
+ is_HDR: videoSource ? String(videoSource.HDR_type) === "1" : false,
37693
39564
  dianzan: Count(Detail_Data.statistics.digg_count),
37694
39565
  pinglun: Count(Detail_Data.statistics.comment_count),
37695
39566
  share: Count(Detail_Data.statistics.share_count),
@@ -37813,8 +39684,8 @@ async function renderLiveImage(options) {
37813
39684
  const streamExtra = liveItem.stream_url?.extra;
37814
39685
  const resolution = streamExtra ? `${streamExtra.width}x${streamExtra.height}` : liveItem.stream_url?.default_resolution;
37815
39686
  return await Render(e, "douyin/live", {
37816
- image_url: liveItem.cover?.url_list[0],
37817
- text: liveItem.title,
39687
+ image_url: liveItem.cover ? liveItem.cover?.url_list[0] : "",
39688
+ text: liveItem.title ?? "",
37818
39689
  partition_title: Detail_Data.live_data.data.data.partition_road_map?.partition?.title || "未知分区",
37819
39690
  room_id: room_data.owner.web_rid,
37820
39691
  online_viewers: Count(Number(liveItem.room_view_stats?.display_value)),
@@ -37825,8 +39696,8 @@ async function renderLiveImage(options) {
37825
39696
  share_url: "https://live.douyin.com/" + room_data.owner.web_rid,
37826
39697
  dynamicTYPE: dynamicTypeLabel,
37827
39698
  like_count: Count(Number(liveItem.like_count || 0)),
37828
- user_count_str: liveItem.user_count_str || "",
37829
- resolution,
39699
+ user_count_str: liveItem.user_count_str ?? "",
39700
+ resolution: resolution ?? "",
37830
39701
  signature: user.signature || "",
37831
39702
  city: user.city || "",
37832
39703
  aweme_count: Count(user.aweme_count || 0),
@@ -37836,6 +39707,20 @@ async function renderLiveImage(options) {
37836
39707
  }, skipWatermark ? { skipWatermark: true } : void 0);
37837
39708
  }
37838
39709
  //#endregion
39710
+ //#region src/platform/douyin/types.ts
39711
+ /**
39712
+ * 把 amagi 返回的 aweme 对象装配成渲染/推送使用的作品详情。
39713
+ * amagi 的生成类型在 text_extra(实为对象数组)、images(图文作品实为数组)等字段上
39714
+ * 与真实数据不符,统一在这一处边界断言,下游全部享受精确类型。
39715
+ * 返回类型会带上 extra 的实际形状,例如传入 user_info 后下游可把它当作必填字段使用。
39716
+ * @param aweme - amagi 返回的作品详情(aweme_detail 或作品列表项)
39717
+ * @param extra - 流程附加字段(作者/订阅者主页信息)
39718
+ */
39719
+ var buildDouyinWorkDetail = (aweme, extra = {}) => ({
39720
+ ...aweme,
39721
+ ...extra
39722
+ });
39723
+ //#endregion
37839
39724
  //#region src/platform/douyin/douyin.ts
37840
39725
  var mp4size = "";
37841
39726
  var img;
@@ -37923,7 +39808,7 @@ var DouYin = class DouYin extends Base {
37923
39808
  continue;
37924
39809
  }
37925
39810
  /** live 图 */
37926
- const liveimg = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${imageItem.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
39811
+ const liveimg = await downloadFile(buildDouyinPlayUrl(imageItem.video.play_addr_h264), {
37927
39812
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
37928
39813
  headers: this.headers
37929
39814
  });
@@ -38079,7 +39964,7 @@ var DouYin = class DouYin extends Base {
38079
39964
  continue;
38080
39965
  }
38081
39966
  /** 动图/短片 */
38082
- const livePhoto = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
39967
+ const livePhoto = await downloadFile(buildDouyinPlayUrl(item.video.play_addr_h264), {
38083
39968
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
38084
39969
  headers: this.headers
38085
39970
  });
@@ -38208,7 +40093,13 @@ var DouYin = class DouYin extends Base {
38208
40093
  }
38209
40094
  /** 视频 */
38210
40095
  let FPS;
40096
+ /**
40097
+ * 图文/文章作品的 video 字段不含 bit_rate,不能无条件初始化,
40098
+ * 否则会在这里直接抛 TypeError;仅在视频分支内赋值,其余场景保持 null。
40099
+ */
38211
40100
  let video = null;
40101
+ /** 按画质偏好选中、即将下载发送的那一路视频源 */
40102
+ let selectedVideo = null;
38212
40103
  if (isVideo) {
38213
40104
  video = VideoData.data.aweme_detail.video;
38214
40105
  FPS = video.bit_rate[0]?.FPS ?? "获取失败";
@@ -38217,10 +40108,11 @@ var DouYin = class DouYin extends Base {
38217
40108
  视频ID:${logger.green(VideoData.data.aweme_detail.aweme_id)}\n
38218
40109
  分享链接:${logger.green(VideoData.data.aweme_detail.share_url)}
38219
40110
  `);
38220
- video.bit_rate = douyinProcessVideos(video.bit_rate, Config.douyin.videoQuality, Config.douyin.maxAutoVideoSize);
38221
- g_video_url = video.bit_rate[0].play_addr.url_list[0] ?? video.bit_rate[0].play_addr.url_list[1] ?? video.bit_rate[0].play_addr.url_list[2];
40111
+ selectedVideo = douyinProcessVideos(video.bit_rate, Config.douyin.videoQuality, Config.douyin.maxAutoVideoSize)[0] ?? null;
40112
+ if (!selectedVideo) throw new Error(`未找到可用的视频源,aweme_id=${VideoData.data.aweme_detail.aweme_id}`);
40113
+ g_video_url = selectedVideo.play_addr.url_list[0] ?? selectedVideo.play_addr.url_list[1] ?? selectedVideo.play_addr.url_list[2];
38222
40114
  g_title = VideoData.data.aweme_detail.preview_title.substring(0, 80).replace(/[\\/:*?"<>|\r\n]/g, " ");
38223
- mp4size = (video.bit_rate[0].play_addr.data_size / (1024 * 1024)).toFixed(2);
40115
+ mp4size = (selectedVideo.play_addr.data_size / (1024 * 1024)).toFixed(2);
38224
40116
  }
38225
40117
  if (Config.douyin.sendContent.includes("info")) if (Config.douyin.videoInfoMode === "text") {
38226
40118
  const replyContent = [];
@@ -38247,13 +40139,11 @@ var DouYin = class DouYin extends Base {
38247
40139
  sec_uid: aweme.author.sec_uid,
38248
40140
  typeMode: "strict"
38249
40141
  });
38250
- const shareLink = isVideo ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${aweme.video.play_addr.uri}&ratio=1080p&line=0` : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`;
40142
+ const shareLink = isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`;
38251
40143
  const workInfoImg = await renderWorkImage({
38252
40144
  e: this.e,
38253
- Detail_Data: {
38254
- ...aweme,
38255
- user_info: userProfile
38256
- },
40145
+ Detail_Data: buildDouyinWorkDetail(aweme, { user_info: userProfile }),
40146
+ videoSource: selectedVideo,
38257
40147
  create_time: aweme.create_time,
38258
40148
  shareLink,
38259
40149
  dynamicTypeLabel: isArticle ? "文章作品" : isVideo ? "视频作品" : this.is_slides ? "合辑作品" : "图文作品"
@@ -38275,13 +40165,13 @@ var DouYin = class DouYin extends Base {
38275
40165
  Type: isArticle ? "文章" : isVideo ? "视频" : this.is_slides ? "合辑" : "图集",
38276
40166
  CommentsData: douyinCommentsRes.CommentsData,
38277
40167
  CommentLength: douyinCommentsRes.CommentsData.length ?? 0,
38278
- share_url: isVideo ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${aweme.video.play_addr.uri}&ratio=1080p&line=0` : aweme.share_url,
40168
+ share_url: isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : aweme.share_url,
38279
40169
  VideoSize: mp4size,
38280
40170
  VideoFPS: FPS,
38281
40171
  ImageLength: imagenum,
38282
40172
  Region: aweme.region,
38283
40173
  suggestWrod: suggest,
38284
- Resolution: isVideo && video ? `${video.bit_rate[0].play_addr.width} x ${video.bit_rate[0].play_addr.height}` : null,
40174
+ Resolution: selectedVideo ? `${selectedVideo.play_addr.width} x ${selectedVideo.play_addr.height}` : null,
38285
40175
  maxDepth: 6,
38286
40176
  Author: aweme.author.nickname,
38287
40177
  AuthorAvatar: aweme.author.avatar_thumb.url_list[0],
@@ -38560,86 +40450,6 @@ var DouYin = class DouYin extends Base {
38560
40450
  }
38561
40451
  }
38562
40452
  };
38563
- var douyinProcessVideos = (videos, videoQuality, maxAutoVideoSize) => {
38564
- const mp4Videos = videos.filter((video) => video.format !== "dash");
38565
- if (mp4Videos.length === 0) {
38566
- logger.warn("没有找到可用的 mp4 格式视频");
38567
- return videos.slice(0, 1);
38568
- }
38569
- logger.debug(`过滤后剩余 ${mp4Videos.length} 个 mp4 格式视频`);
38570
- const getQualityLevel = (gearName) => {
38571
- if (gearName.includes("lowest_4") || gearName.includes("2160")) return "4k";
38572
- if (gearName.includes("1440") || gearName.includes("2k")) return "2k";
38573
- if (gearName.includes("1080")) return "1080p";
38574
- if (gearName.includes("720")) return "720p";
38575
- if (gearName.includes("540")) return "540p";
38576
- return "540p";
38577
- };
38578
- const videosByQuality = /* @__PURE__ */ new Map();
38579
- mp4Videos.forEach((video) => {
38580
- const quality = getQualityLevel(video.gear_name);
38581
- if (!videosByQuality.has(quality)) videosByQuality.set(quality, []);
38582
- videosByQuality.get(quality).push(video);
38583
- });
38584
- videosByQuality.forEach((videos) => {
38585
- videos.sort((a, b) => b.play_addr.data_size - a.play_addr.data_size);
38586
- });
38587
- if (videoQuality === "adapt") {
38588
- const sizeLimitBytes = (maxAutoVideoSize || Config.app.filelimit) * 1024 * 1024;
38589
- for (const quality of [
38590
- "4k",
38591
- "2k",
38592
- "1080p",
38593
- "720p",
38594
- "540p"
38595
- ]) {
38596
- const qualityVideos = videosByQuality.get(quality);
38597
- if (qualityVideos && qualityVideos.length > 0) {
38598
- const suitableVideo = qualityVideos.find((video) => video.play_addr.data_size <= sizeLimitBytes);
38599
- if (suitableVideo) {
38600
- logger.debug(`自动选择画质: ${quality}, 文件大小: ${(suitableVideo.play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
38601
- return [suitableVideo];
38602
- }
38603
- }
38604
- }
38605
- let smallestVideo = mp4Videos[0];
38606
- mp4Videos.forEach((video) => {
38607
- if (video.play_addr.data_size < smallestVideo.play_addr.data_size) smallestVideo = video;
38608
- });
38609
- logger.debug(`未找到符合大小限制的视频,选择最小视频: ${(smallestVideo.play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
38610
- return [smallestVideo];
38611
- }
38612
- const targetQuality = videoQuality;
38613
- const targetVideos = videosByQuality.get(targetQuality);
38614
- if (targetVideos && targetVideos.length > 0) {
38615
- logger.debug(`选择固定画质: ${targetQuality}, 文件大小: ${(targetVideos[0].play_addr.data_size / (1024 * 1024)).toFixed(2)}MB`);
38616
- return [targetVideos[0]];
38617
- }
38618
- const qualityPriority = [
38619
- "4k",
38620
- "2k",
38621
- "1080p",
38622
- "720p",
38623
- "540p"
38624
- ];
38625
- const targetIndex = qualityPriority.indexOf(targetQuality);
38626
- for (let i = targetIndex + 1; i < qualityPriority.length; i++) {
38627
- const fallbackVideos = videosByQuality.get(qualityPriority[i]);
38628
- if (fallbackVideos && fallbackVideos.length > 0) {
38629
- logger.debug(`目标画质 ${targetQuality} 不可用,降级到: ${qualityPriority[i]}`);
38630
- return [fallbackVideos[0]];
38631
- }
38632
- }
38633
- for (let i = targetIndex - 1; i >= 0; i--) {
38634
- const fallbackVideos = videosByQuality.get(qualityPriority[i]);
38635
- if (fallbackVideos && fallbackVideos.length > 0) {
38636
- logger.debug(`目标画质 ${targetQuality} 不可用,升级到: ${qualityPriority[i]}`);
38637
- return [fallbackVideos[0]];
38638
- }
38639
- }
38640
- logger.warn("未找到任何匹配的画质,返回默认视频");
38641
- return [mp4Videos[0]];
38642
- };
38643
40453
  /**
38644
40454
  * 传递整数,返回x小时后的时间
38645
40455
  * @param {number} delay
@@ -38816,11 +40626,10 @@ async function processFavoriteList(contentList, sec_uid, userinfo, item, targets
38816
40626
  create_time: aweme.create_time,
38817
40627
  targets: validTargets,
38818
40628
  pushType,
38819
- Detail_Data: {
38820
- ...aweme,
40629
+ Detail_Data: buildDouyinWorkDetail(aweme, {
38821
40630
  user_info: userinfo,
38822
40631
  author_user_info: authorUserInfo
38823
- },
40632
+ }),
38824
40633
  avatar_img: "https://p3-pc.douyinpic.com/aweme/1080x1080/" + userinfo.data.user.avatar_larger.uri,
38825
40634
  living: false
38826
40635
  });
@@ -38837,7 +40646,6 @@ async function processFavoriteList(contentList, sec_uid, userinfo, item, targets
38837
40646
  * @returns 返回需要推送的直播项(如果有)
38838
40647
  */
38839
40648
  async function processLiveStream(sec_uid, userinfo, item, targets, amagi) {
38840
- const pushType = "live";
38841
40649
  const liveStatus = await douyinDBInstance.getLiveStatus(sec_uid);
38842
40650
  if (userinfo.data.user.live_status === 1) {
38843
40651
  const UserInfoData = await amagi.douyin.fetcher.fetchUserProfile({
@@ -38865,7 +40673,7 @@ async function processLiveStream(sec_uid, userinfo, item, targets, amagi) {
38865
40673
  sec_uid,
38866
40674
  create_time: Date.now(),
38867
40675
  targets,
38868
- pushType,
40676
+ pushType: "live",
38869
40677
  Detail_Data: {
38870
40678
  user_info: userinfo,
38871
40679
  room_data: JSON.parse(userinfo.data.user.room_data),
@@ -38924,10 +40732,7 @@ async function processPostList(contentList, sec_uid, userinfo, item, targets) {
38924
40732
  create_time: aweme.create_time,
38925
40733
  targets: validTargets,
38926
40734
  pushType,
38927
- Detail_Data: {
38928
- ...aweme,
38929
- user_info: userinfo
38930
- },
40735
+ Detail_Data: buildDouyinWorkDetail(aweme, { user_info: userinfo }),
38931
40736
  avatar_img: "https://p3-pc.douyinpic.com/aweme/1080x1080/" + userinfo.data.user.avatar_larger.uri,
38932
40737
  living: false
38933
40738
  });
@@ -38980,11 +40785,10 @@ async function processRecommendList(contentList, sec_uid, userinfo, item, target
38980
40785
  create_time: aweme.create_time,
38981
40786
  targets: validTargets,
38982
40787
  pushType,
38983
- Detail_Data: {
38984
- ...aweme,
40788
+ Detail_Data: buildDouyinWorkDetail(aweme, {
38985
40789
  user_info: userinfo,
38986
40790
  author_user_info: authorUserInfo
38987
- },
40791
+ }),
38988
40792
  avatar_img: "https://p3-pc.douyinpic.com/aweme/1080x1080/" + userinfo.data.user.avatar_larger.uri,
38989
40793
  living: false
38990
40794
  });
@@ -39129,7 +40933,7 @@ var DouYinpush = class extends Base {
39129
40933
  for (const awemeId in data) {
39130
40934
  const pushItem = data[awemeId];
39131
40935
  const actualAwemeId = awemeId.replace(/^(post|favorite|recommend|live)_/, "");
39132
- const shareUrl = pushItem.Detail_Data.share_url ?? `https://live.douyin.com/${pushItem.Detail_Data.room_data?.owner.web_rid}`;
40936
+ const shareUrl = pushItem.pushType === "live" ? `https://live.douyin.com/${pushItem.Detail_Data.room_data?.owner.web_rid}` : pushItem.Detail_Data.share_url ?? `https://www.douyin.com/video/${actualAwemeId}`;
39133
40937
  let pushTypeLabel;
39134
40938
  switch (pushItem.pushType) {
39135
40939
  case "post":
@@ -39159,11 +40963,13 @@ var DouYinpush = class extends Base {
39159
40963
  if (skip) logger.warn(`作品 https://www.douyin.com/video/${actualAwemeId} 已被处理,跳过`);
39160
40964
  let img = [];
39161
40965
  let iddata = { type: "one_work" };
40966
+ /** 按画质配置选中、即将下载发送的那一路视频源 */
40967
+ let selectedVideo = null;
39162
40968
  this.injectBotToEventForRender(pushItem.targets);
39163
- if (!skip) iddata = await getDouyinID(this.e, Detail_Data.share_url ?? "https://live.douyin.com/" + Detail_Data.room_data?.owner.web_rid, false);
40969
+ if (!skip) iddata = await getDouyinID(this.e, shareUrl, false);
39164
40970
  if (!skip) {
39165
40971
  const realUrl = pushItem.pushType !== "live" && workTypeInfo.isVideo && Config.douyin.push.shareType === "web" && await new Network({
39166
- url: Detail_Data.share_url,
40972
+ url: pushItem.Detail_Data.share_url,
39167
40973
  headers: {
39168
40974
  "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
39169
40975
  Accept: "*/*",
@@ -39171,17 +40977,21 @@ var DouYinpush = class extends Base {
39171
40977
  Connection: "keep-alive"
39172
40978
  }
39173
40979
  }).getLocation();
40980
+ if (pushItem.pushType !== "live" && workTypeInfo.isVideo && pushItem.Detail_Data.video?.bit_rate?.length) selectedVideo = douyinProcessVideos(pushItem.Detail_Data.video.bit_rate, Config.douyin.videoQuality)[0];
39174
40981
  let workShareLink;
39175
40982
  if (pushItem.pushType !== "live") if (workTypeInfo.isArticle) workShareLink = `https://www.douyin.com/article/${actualAwemeId}`;
39176
40983
  else if (workTypeInfo.isImage) workShareLink = `https://www.douyin.com/note/${actualAwemeId}`;
39177
40984
  else if (Config.douyin.push.shareType === "web") workShareLink = realUrl || `https://www.douyin.com/video/${actualAwemeId}`;
39178
- else workShareLink = Detail_Data.video?.play_addr?.uri ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${Detail_Data.video.play_addr.uri}&ratio=1080p&line=0` : `https://www.douyin.com/video/${actualAwemeId}`;
40985
+ else {
40986
+ const playAddr = selectedVideo?.play_addr ?? pushItem.Detail_Data.video?.play_addr;
40987
+ workShareLink = playAddr ? buildDouyinPlayUrl(playAddr) : `https://www.douyin.com/video/${actualAwemeId}`;
40988
+ }
39179
40989
  switch (pushItem.pushType) {
39180
40990
  case "live":
39181
- if (!("room_data" in pushItem.Detail_Data && Detail_Data.live_data)) break;
40991
+ if (!pushItem.Detail_Data.room_data || !pushItem.Detail_Data.live_data) break;
39182
40992
  img = await renderLiveImage({
39183
40993
  e: this.e,
39184
- Detail_Data,
40994
+ Detail_Data: pushItem.Detail_Data,
39185
40995
  skipWatermark: true,
39186
40996
  dynamicTypeLabel: "直播动态推送"
39187
40997
  });
@@ -39189,7 +40999,7 @@ var DouYinpush = class extends Base {
39189
40999
  case "favorite":
39190
41000
  img = await renderFavoriteImage({
39191
41001
  e: this.e,
39192
- Detail_Data,
41002
+ Detail_Data: pushItem.Detail_Data,
39193
41003
  create_time: pushItem.create_time,
39194
41004
  shareLink: workShareLink,
39195
41005
  remark: pushItem.remark,
@@ -39199,7 +41009,7 @@ var DouYinpush = class extends Base {
39199
41009
  case "recommend":
39200
41010
  img = await renderRecommendImage({
39201
41011
  e: this.e,
39202
- Detail_Data,
41012
+ Detail_Data: pushItem.Detail_Data,
39203
41013
  create_time: pushItem.create_time,
39204
41014
  shareLink: workShareLink,
39205
41015
  remark: pushItem.remark,
@@ -39209,9 +41019,10 @@ var DouYinpush = class extends Base {
39209
41019
  default:
39210
41020
  img = await renderWorkImage({
39211
41021
  e: this.e,
39212
- Detail_Data,
41022
+ Detail_Data: pushItem.Detail_Data,
39213
41023
  create_time: pushItem.create_time,
39214
41024
  shareLink: workShareLink,
41025
+ videoSource: selectedVideo,
39215
41026
  skipWatermark: true
39216
41027
  });
39217
41028
  break;
@@ -39227,10 +41038,10 @@ var DouYinpush = class extends Base {
39227
41038
  eventWithBot.bot = bot;
39228
41039
  eventWithBot.selfId = botId;
39229
41040
  const watermarkedImg = img ? applyWatermarkToImages(img, this.e) : [];
39230
- const parseButton = bot?.adapter?.name === "QQ Official Bot" && pushItem.pushType !== "live" && Detail_Data.share_url ? [segment.button([{
41041
+ const parseButton = bot?.adapter?.name === "QQ Official Bot" && pushItem.pushType !== "live" && pushItem.Detail_Data.share_url ? [segment.button([{
39231
41042
  text: "解析",
39232
41043
  callback: true,
39233
- data: `#解析${Detail_Data.share_url}`
41044
+ data: `#解析${pushItem.Detail_Data.share_url}`
39234
41045
  }, {
39235
41046
  text: "帮助",
39236
41047
  callback: true,
@@ -39238,20 +41049,21 @@ var DouYinpush = class extends Base {
39238
41049
  }])] : [];
39239
41050
  status = await karin.sendMsg(botId, Contact, [...watermarkedImg, ...parseButton]);
39240
41051
  if (pushItem.pushType === "live" && "room_data" in pushItem.Detail_Data && status.message_id) await douyinDBInstance.updateLiveStatus(pushItem.sec_uid, true);
39241
- if (Config.douyin.push.parsedynamic && status.message_id) {
41052
+ if (pushItem.pushType !== "live" && Config.douyin.push.parsedynamic && status.message_id) {
41053
+ const Detail_Data = pushItem.Detail_Data;
39242
41054
  logger.debug(`开始解析作品,类型为:${getWorkTypeDisplayName(workTypeInfo)}`);
39243
- if (workTypeInfo.isVideo) {
41055
+ if (workTypeInfo.isVideo && Detail_Data.video) {
39244
41056
  /** 默认视频下载地址 */
39245
- let downloadUrl = `https://aweme.snssdk.com/aweme/v1/play/?video_id=${Detail_Data.video.play_addr.uri}&ratio=1080p&line=0`;
41057
+ let downloadUrl = buildDouyinPlayUrl(Detail_Data.video.play_addr);
39246
41058
  logger.debug(`开始排除不符合条件的视频分辨率;\n
39247
41059
  共拥有${logger.yellow(Detail_Data.video.bit_rate.length)}个视频源\n
39248
41060
  视频ID:${logger.green(Detail_Data.aweme_id)}\n
39249
41061
  分享链接:${logger.green(Detail_Data.share_url)}
39250
41062
  `);
39251
- const videoObj = douyinProcessVideos(Detail_Data.video.bit_rate, Config.douyin.videoQuality);
41063
+ const videoObj = selectedVideo ?? douyinProcessVideos(Detail_Data.video.bit_rate, Config.douyin.videoQuality)[0];
39252
41064
  logger.debug("获取精确下载地址");
39253
41065
  downloadUrl = await new Network({
39254
- url: videoObj[0].play_addr.url_list[0],
41066
+ url: videoObj.play_addr.url_list[0],
39255
41067
  headers: douyinBaseHeaders
39256
41068
  }).getLongLink();
39257
41069
  await downloadVideo(this.e, {
@@ -39295,7 +41107,12 @@ var DouYinpush = class extends Base {
39295
41107
  continue;
39296
41108
  }
39297
41109
  /** 动图/短片 */
39298
- const liveimg = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
41110
+ const livePlayAddr = item.video?.play_addr_h264;
41111
+ if (!livePlayAddr?.uri) {
41112
+ logger.warn(`合辑第 ${index + 1} 个动态媒体缺少视频源,跳过`);
41113
+ continue;
41114
+ }
41115
+ const liveimg = await downloadFile(buildDouyinPlayUrl(livePlayAddr), {
39299
41116
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
39300
41117
  headers: douyinBaseHeaders
39301
41118
  });
@@ -39414,12 +41231,19 @@ var DouYinpush = class extends Base {
39414
41231
  }
39415
41232
  for (const [index, item] of Detail_Data.images.entries()) {
39416
41233
  if (item.clip_type === 2 || item.clip_type === void 0) {
39417
- const imageUrl = await processImageUrl(item.url_list[2] ?? item.url_list[1], Detail_Data.desc, index);
41234
+ const image_url = item.url_list[2] ?? item.url_list[1];
41235
+ if (!image_url) continue;
41236
+ const imageUrl = await processImageUrl(image_url, Detail_Data.desc, index);
39418
41237
  processedImages.push(segment.image(imageUrl));
39419
41238
  continue;
39420
41239
  }
39421
41240
  /** live 图 */
39422
- const liveimg = await downloadFile(`https://aweme.snssdk.com/aweme/v1/play/?video_id=${item.video.play_addr_h264.uri}&ratio=1080p&line=0`, {
41241
+ const livePlayAddr = item.video?.play_addr_h264;
41242
+ if (!livePlayAddr?.uri) {
41243
+ logger.warn(`图集第 ${index + 1} 个动态媒体缺少视频源,跳过`);
41244
+ continue;
41245
+ }
41246
+ const liveimg = await downloadFile(buildDouyinPlayUrl(livePlayAddr), {
39423
41247
  title: `Douyin_tmp_V_${Date.now()}.mp4`,
39424
41248
  headers: douyinBaseHeaders
39425
41249
  });
@@ -39523,11 +41347,12 @@ var DouYinpush = class extends Base {
39523
41347
  let image_url;
39524
41348
  for (const [index, item] of Detail_Data.images.entries()) {
39525
41349
  image_url = item.url_list[2] ?? item.url_list[1];
41350
+ if (!image_url) continue;
39526
41351
  const imageUrl = await processImageUrl(image_url, Detail_Data.desc, index);
39527
41352
  imageres.push(segment.image(imageUrl));
39528
41353
  }
39529
41354
  const bot = karin.getBot(botId);
39530
- if (imageres.length === 1) {
41355
+ if (imageres.length === 1 && image_url) {
39531
41356
  const imageUrl = await processImageUrl(image_url, Detail_Data.desc);
39532
41357
  await bot.sendMsg(Contact, [segment.image(imageUrl)]);
39533
41358
  } else {
@@ -39834,7 +41659,7 @@ var DouYinpush = class extends Base {
39834
41659
  * @returns 是否应该跳过推送
39835
41660
  */
39836
41661
  var skipDynamic = async (PushItem) => {
39837
- if ("liveStatus" in PushItem.Detail_Data) return false;
41662
+ if (PushItem.pushType === "live" || "liveStatus" in PushItem.Detail_Data) return false;
39838
41663
  const tags = [];
39839
41664
  if (PushItem.Detail_Data.text_extra) {
39840
41665
  for (const item of PushItem.Detail_Data.text_extra) if (item.hashtag_name) tags.push(item.hashtag_name);
@@ -40626,376 +42451,251 @@ var XiaohongshuEmoji = (data) => {
40626
42451
  };
40627
42452
  //#endregion
40628
42453
  //#region src/platform/douyin/login.ts
42454
+ /** 等待用户扫码的时限上限,与消息可撤回窗口(2 分钟)对齐;二维码本身更早失效时以它为准 */
42455
+ var SCAN_TIMEOUT = 12e4;
42456
+ /** 扫码后等待手机确认或完成二次验证的时限 */
42457
+ var CONFIRM_TIMEOUT = 18e4;
42458
+ /** 等待用户回填短信验证码的时限(秒) */
42459
+ var CODE_INPUT_TIMEOUT = 90;
42460
+ /** 短信验证码允许的重试次数 */
42461
+ var CODE_MAX_ATTEMPTS = 3;
42462
+ /** 6 位数字验证码 */
42463
+ var CODE_PATTERN = /^\d{6}$/;
42464
+ /** 登录凭证里需要确认下发的关键 cookie */
42465
+ var REQUIRED_COOKIES = [
42466
+ "sessionid",
42467
+ "sessionid_ss",
42468
+ "sid_guard",
42469
+ "uid_tt",
42470
+ "uid_tt_ss",
42471
+ "ttwid"
42472
+ ];
42473
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42474
+ /** 复用 amagi 配置里的代理与超时,让登录请求与其它抖音请求走同一条出口 */
42475
+ var requestConfig = () => {
42476
+ const amagi = Config.amagi;
42477
+ return {
42478
+ timeout: amagi.timeout,
42479
+ proxy: amagi.proxy?.switch ? amagi.proxy : false
42480
+ };
42481
+ };
40629
42482
  /**
40630
- * 截图函数
40631
- * @param page Puppeteer 页面对象
40632
- * @param screenshotPath 截图保存路径
42483
+ * 登录过程中发出的消息,全部登记在这里,结束时统一撤回,避免二维码留在群里
42484
+ * @param e 消息事件
40633
42485
  */
40634
- var safeScreenshot = async (page, screenshotPath) => {
40635
- try {
40636
- await page.screenshot({ path: screenshotPath });
40637
- logger.debug(`截图已保存: ${screenshotPath}`);
40638
- } catch (error) {
40639
- logger.warn("截图失败(已忽略):", error);
40640
- }
40641
- };
40642
- var douyinLogin = async (e) => {
40643
- const msg_id = [];
40644
- const puppeteer = await snapka.launch({
40645
- headless: "new",
40646
- protocolTimeout: 6e4,
40647
- args: [
40648
- "--disable-blink-features=AutomationControlled",
40649
- "--mute-audio",
40650
- "--window-size=800,600",
40651
- "--disable-gpu",
40652
- "--no-sandbox",
40653
- "--disable-setuid-sandbox",
40654
- "--no-zygote",
40655
- "--disable-extensions",
40656
- "--disable-dev-shm-usage",
40657
- "--disable-background-networking",
40658
- "--disable-sync",
40659
- "--disable-crash-reporter",
40660
- "--disable-translate",
40661
- "--disable-notifications",
40662
- "--disable-device-discovery-notifications",
40663
- "--disable-accelerated-2d-canvas",
40664
- "--autoplay-policy=user-gesture-required",
40665
- "--disable-web-security",
40666
- "--disable-features=IsolateOrigins,site-per-process",
40667
- "--disable-site-isolation-trials",
40668
- "--disable-features=VizDisplayCompositor",
40669
- "--js-flags=--max-old-space-size=128",
40670
- "--disable-software-rasterizer",
40671
- "--disable-webgl",
40672
- "--disable-webgl2",
40673
- "--disable-3d-apis",
40674
- "--disable-accelerated-video-decode",
40675
- "--disable-background-timer-throttling",
40676
- "--disable-backgrounding-occluded-windows",
40677
- "--disable-breakpad",
40678
- "--disable-component-extensions-with-background-pages",
40679
- "--disable-features=TranslateUI,BlinkGenPropertyTrees",
40680
- "--disable-ipc-flooding-protection",
40681
- "--disable-renderer-backgrounding"
40682
- ],
40683
- ignoreDefaultArgs: ["--enable-automation"]
40684
- });
40685
- /** 根据当前系统平台选择操作系统类型 */
40686
- const getOperatingSystem = () => {
40687
- const os = platform();
40688
- if (os === "win32") return "windows";
40689
- if (os === "darwin") return "macos";
40690
- return "linux";
40691
- };
40692
- const page = await newInjectedPage(puppeteer.browser, { fingerprintOptions: {
40693
- devices: ["desktop"],
40694
- operatingSystems: [getOperatingSystem()]
40695
- } });
40696
- await page.setRequestInterception(true);
40697
- page.on("request", async (request) => {
40698
- const resourceType = request.resourceType();
40699
- const url = request.url();
40700
- if (url.includes("passport") || url.includes("login") || url.includes("qrconnect") || url.includes("qrcode")) logger.debug(`[请求] ${resourceType}: ${url}`);
40701
- if (resourceType === "media" || resourceType === "font" || resourceType === "stylesheet" || /\.(mp4|webm|m3u8|flv|avi|mov|wmv|mkv)(\?|$)/i.test(url) || url.includes("/aweme/") || url.includes("/video/") || url.includes("v.douyin.com") || resourceType === "image" && !url.includes("qrcode") && !url.includes("data:image") && (url.includes(".jpg") || url.includes(".jpeg") || url.includes(".webp"))) {
40702
- if (url.includes("passport") || url.includes("login") || url.includes("qrconnect")) logger.warn(`[拦截] 登录相关请求被拦截: ${url}`);
40703
- request.abort();
40704
- } else request.continue();
40705
- });
40706
- await page.evaluateOnNewDocument(() => {
40707
- HTMLMediaElement.prototype.play = function() {
40708
- return Promise.reject(/* @__PURE__ */ new Error("Video playback blocked"));
40709
- };
40710
- if (window.MediaSource) window.MediaSource = void 0;
40711
- window.IntersectionObserver = class {
40712
- observe() {}
40713
- unobserve() {}
40714
- disconnect() {}
40715
- };
40716
- });
40717
- await page.goto("https://www.douyin.com", {
40718
- timeout: 12e4,
40719
- waitUntil: "domcontentloaded"
40720
- });
40721
- let qrCodeData;
40722
- try {
40723
- logger.mark("开始等待二维码加载...");
40724
- qrCodeData = await Promise.race([waitQrcode(page), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("QR_CODE_TIMEOUT")), 6e4))]);
40725
- logger.mark("二维码获取成功");
40726
- } catch (error) {
40727
- if (error.message === "QR_CODE_TIMEOUT") {
40728
- logger.warn("获取二维码超时");
40729
- await safeScreenshot(page, path.join(karinPathTemp, Root.pluginName, "DouyinLoginQrcodeError.png"));
40730
- await e.reply("获取二维码超时,请稍后重试", { reply: true });
40731
- } else {
40732
- logger.error("获取二维码失败:", error);
40733
- await e.reply("获取二维码失败,请查看控制台日志", { reply: true });
40734
- }
40735
- await puppeteer.browser.close();
40736
- return true;
40737
- }
40738
- let gcInterval;
40739
- try {
40740
- const loginQRcode = await Render(e, "douyin/qrcodeImg", qrCodeData.url ? { share_url: qrCodeData.url } : { share_url: qrCodeData.originalImage });
40741
- const base64Data = loginQRcode[0]?.file;
40742
- if (!base64Data) throw new Error("生成二维码图片失败");
40743
- const cleanBase64 = base64Data.replace(/^base64:\/\//, "");
40744
- const buffer = Buffer.from(cleanBase64, "base64");
40745
- fs.writeFileSync(`${Common.tempDri.default}DouyinLoginQrcode.png`, buffer);
40746
- const message2 = await e.reply(loginQRcode, { reply: true });
40747
- logger.debug("二维码图片消息ID:", message2.messageId);
40748
- msg_id.push(message2.messageId);
40749
- gcInterval = setInterval(async () => {
40750
- try {
40751
- await page.evaluate(() => {
40752
- if (window.gc) window.gc();
40753
- });
40754
- } catch {}
40755
- }, 1e4);
40756
- logger.mark("开始等待用户扫码登录...");
40757
- const loginResult = await Promise.race([new Promise((resolve) => {
40758
- const timer = setTimeout(async () => {
40759
- logger.warn("扫码登录超时(2分钟),撤回二维码消息");
40760
- await Promise.all(msg_id.map(async (id) => {
40761
- await e.bot.recallMsg(e.contact, id);
40762
- }));
40763
- resolve(false);
40764
- }, 120 * 1e3);
40765
- let secondVerifyHandled = false;
40766
- let scannedHandled = false;
40767
- let responseCount = 0;
40768
- page.on("response", async (response) => {
40769
- responseCount++;
40770
- const url = response.url();
40771
- if (responseCount % 10 === 0) logger.debug(`[心跳] 已收到 ${responseCount} 个响应`);
40772
- if (url.includes("passport") || url.includes("login") || url.includes("qrconnect") || url.includes("qrcode")) logger.debug(`[响应] 登录相关请求: ${url}, status: ${response.status()}`);
40773
- });
40774
- logger.mark("响应监听器已注册");
40775
- page.on("response", async (response) => {
42486
+ var createMessageTracker = (e) => {
42487
+ const messageIds = [];
42488
+ return {
42489
+ /**
42490
+ * 发送并登记一条消息
42491
+ * @param message 消息内容
42492
+ */
42493
+ async send(message) {
42494
+ const sent = await e.reply(message, { reply: true });
42495
+ if (sent.messageId) messageIds.push(sent.messageId);
42496
+ return sent;
42497
+ },
42498
+ /** 撤回目前登记的全部消息 */
42499
+ async recallAll() {
42500
+ const pending = messageIds.splice(0, messageIds.length);
42501
+ await Promise.all(pending.map(async (id) => {
40776
42502
  try {
40777
- if (response.url().includes("check_qrconnect")) {
40778
- logger.debug(`收到登录轮询响应: ${response.url()}`);
40779
- const hasSidGuard = (response.headers()["set-cookie"] || "").includes("sid_guard");
40780
- logger.debug(`响应头包含 sid_guard: ${hasSidGuard}`);
40781
- if (hasSidGuard) {
40782
- clearTimeout(timer);
40783
- logger.mark("检测到 sid_guard,登录成功");
40784
- logger.debug("开始获取 cookies...");
40785
- const cookies = await puppeteer.browser.cookies();
40786
- logger.debug(`获取到 ${cookies.length} 个 cookies`);
40787
- const cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
40788
- const hasSessionidSs = cookies.some((cookie) => cookie.name === "sessionid_ss");
40789
- const hasTtwid = cookies.some((cookie) => cookie.name === "ttwid");
40790
- logger.mark(`Cookie 参数检测: sessionid_ss=${hasSessionidSs}, ttwid=${hasTtwid}`);
40791
- if (!hasSessionidSs || !hasTtwid) {
40792
- logger.warn("警告:缺少关键 cookie 参数!");
40793
- if (!hasSessionidSs) logger.warn(" - 缺少 sessionid_ss");
40794
- if (!hasTtwid) logger.warn(" - 缺少 ttwid");
40795
- }
40796
- logger.debug("开始保存 cookies...");
40797
- await Config.Modify("amagi", "cookies.douyin", cookieString);
40798
- const reloaded = reloadAmagiConfig();
40799
- logger.debug(`cookies 保存完成,Amagi Client ${reloaded ? "已重载" : "配置未变化"}`);
40800
- await e.reply("登录成功!用户登录凭证已保存至配置", { reply: true });
40801
- await Promise.all(msg_id.map(async (id) => {
40802
- await e.bot.recallMsg(e.contact, id);
40803
- }));
40804
- logger.mark("关闭浏览器...");
40805
- await puppeteer.browser.close();
40806
- logger.mark("浏览器已关闭");
40807
- resolve(true);
40808
- return;
40809
- }
40810
- const responseBody = await response.text();
40811
- const jsonResponse = JSON.parse(responseBody);
40812
- logger.debug(`二维码状态:${jsonResponse.data?.status}, error_code: ${jsonResponse.data?.error_code}`);
40813
- if (jsonResponse.data?.status === "scanned" && !scannedHandled) {
40814
- scannedHandled = true;
40815
- logger.mark("检测到二维码已被扫描");
40816
- await Promise.all(msg_id.map(async (id) => {
40817
- await e.bot.recallMsg(e.contact, id);
40818
- }));
40819
- msg_id.length = 0;
40820
- const authMsg = await e.reply("此二维码已被扫描,请在手机上授权以登录", { reply: true });
40821
- msg_id.push(authMsg.messageId);
40822
- }
40823
- if (jsonResponse.data?.error_code === 2046 && !secondVerifyHandled) {
40824
- secondVerifyHandled = true;
40825
- logger.mark("检测到需要二次验证");
40826
- clearTimeout(timer);
40827
- (async () => {
40828
- try {
40829
- await page.waitForSelector("#uc-second-verify", { timeout: 5e3 });
40830
- if (!await page.evaluate(() => {
40831
- const element = document.evaluate("//text()[contains(., '接收短信验证码')]/ancestor::*[1]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
40832
- if (element) {
40833
- element.click();
40834
- return true;
40835
- }
40836
- return false;
40837
- })) logger.warn("未找到\"接收短信验证码\"按钮");
40838
- await new Promise((resolve) => setTimeout(resolve, 2e3));
40839
- const inputSelector = "#uc-second-verify input";
40840
- await page.waitForSelector(inputSelector, { timeout: 1e4 });
40841
- const tipMsg = await e.reply("此次验证需要进行 2FA\n6 位数的验证码已发送至扫码设备绑定的手机号\n请在 60 秒内发送此验证码以通过 2FA", { reply: true });
40842
- msg_id.push(tipMsg.messageId);
40843
- let verifyAttempts = 0;
40844
- const maxAttempts = 2;
40845
- let verifySuccess = false;
40846
- while (verifyAttempts < maxAttempts && !verifySuccess) {
40847
- verifyAttempts++;
40848
- logger.debug(`验证码输入尝试 ${verifyAttempts}/${maxAttempts}`);
40849
- const ctx = await Promise.race([karin$1.ctx(e, { reply: true }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("2FA_TIMEOUT")), 6e4))]).catch(async (error) => {
40850
- if (error.message === "2FA_TIMEOUT") {
40851
- logger.warn("2FA验证码输入超时");
40852
- clearTimeout(timer);
40853
- if (gcInterval) clearInterval(gcInterval);
40854
- await puppeteer.browser.close();
40855
- await Promise.all(msg_id.map(async (id) => {
40856
- await e.bot.recallMsg(e.contact, id);
40857
- }));
40858
- await e.reply("验证码验证码输入超时,登录失败", { reply: true });
40859
- resolve(true);
40860
- }
40861
- throw error;
40862
- });
40863
- if (!ctx) return;
40864
- await page.evaluate((selector) => {
40865
- const input = document.querySelector(selector);
40866
- if (input) input.value = "";
40867
- }, inputSelector);
40868
- await page.type(inputSelector, ctx.msg);
40869
- const validatePromise = new Promise((resolveValidate) => {
40870
- const validateHandler = async (resp) => {
40871
- if (resp.url().includes("validate_code")) try {
40872
- const validateBody = await resp.text();
40873
- const validateJson = JSON.parse(validateBody);
40874
- logger.debug("验证码验证响应:", validateJson);
40875
- if (validateJson.data?.error_code === 1202) {
40876
- logger.warn("验证码错误");
40877
- page.off("response", validateHandler);
40878
- resolveValidate(false);
40879
- } else if (validateJson.message === "success" || !validateJson.data?.error_code) {
40880
- logger.mark("验证码验证成功");
40881
- page.off("response", validateHandler);
40882
- resolveValidate(true);
40883
- }
40884
- } catch (err) {
40885
- logger.debug("解析验证响应失败:", err);
40886
- }
40887
- };
40888
- page.on("response", validateHandler);
40889
- setTimeout(() => {
40890
- page.off("response", validateHandler);
40891
- resolveValidate(false);
40892
- }, 5e3);
40893
- });
40894
- await page.evaluate(() => {
40895
- const verifyBtn = Array.from(document.querySelectorAll("*")).find((el) => el.textContent?.trim() === "验证");
40896
- if (verifyBtn) verifyBtn.click();
40897
- });
40898
- logger.mark("已提交验证码,等待验证结果...");
40899
- verifySuccess = await validatePromise;
40900
- if (!verifySuccess && verifyAttempts < maxAttempts) {
40901
- const retryMsg = await e.reply("验证码错误,请重新发送正确的 6 位数验证码(剩余机会:1次)", { reply: true });
40902
- msg_id.push(retryMsg.messageId);
40903
- } else if (!verifySuccess && verifyAttempts >= maxAttempts) {
40904
- logger.warn("验证码错误次数过多,登录失败");
40905
- clearTimeout(timer);
40906
- if (gcInterval) clearInterval(gcInterval);
40907
- await puppeteer.browser.close();
40908
- await Promise.all(msg_id.map(async (id) => {
40909
- await e.bot.recallMsg(e.contact, id);
40910
- }));
40911
- await e.reply("验证码错误,登录失败", { reply: true });
40912
- resolve(true);
40913
- return;
40914
- }
40915
- }
40916
- if (verifySuccess) logger.mark("2FA验证通过,等待最终登录确认...");
40917
- } catch (err) {
40918
- logger.error("二次验证处理失败:", err);
40919
- clearTimeout(timer);
40920
- if (gcInterval) clearInterval(gcInterval);
40921
- await puppeteer.browser.close();
40922
- await Promise.all(msg_id.map(async (id) => {
40923
- await e.bot.recallMsg(e.contact, id);
40924
- }));
40925
- await e.reply("二次验证处理失败,登录失败", { reply: true });
40926
- resolve(true);
40927
- }
40928
- })();
40929
- }
40930
- }
42503
+ await e.bot.recallMsg(e.contact, id);
40931
42504
  } catch (error) {
40932
- logger.error("处理响应时出错:", error);
42505
+ logger.debug("[抖音登录] 撤回消息失败:", error);
40933
42506
  }
40934
- });
40935
- })]);
40936
- if (gcInterval) clearInterval(gcInterval);
40937
- if (!loginResult) {
40938
- logger.warn("登录超时或失败");
40939
- await puppeteer.browser.close();
40940
- await e.reply("登录超时!二维码已失效!", { reply: true });
42507
+ }));
42508
+ }
42509
+ };
42510
+ };
42511
+ /**
42512
+ * 处理账号二次验证:发短信验证码 → 等用户回填 → 提交
42513
+ * @param e 消息事件
42514
+ * @param session 登录会话,验证过程中会刷新其中的 cookie
42515
+ * @param verify 轮询下发的验证上下文
42516
+ * @param tracker 消息登记器
42517
+ * @returns 验证是否通过
42518
+ */
42519
+ var handleSecondVerify = async (e, session, verify, tracker) => {
42520
+ if (!verify.encryptUid) {
42521
+ await tracker.send("账号触发了二次验证,但服务端未下发验证上下文,请稍后重试");
42522
+ return false;
42523
+ }
42524
+ const smsWay = verify.verifyWays.find((way) => isSmsCodeVerifyWay(way.verifyWay));
42525
+ if (verify.verifyWays.length > 0 && !smsWay) {
42526
+ const ways = verify.verifyWays.map((way) => way.verifyWay).join("、");
42527
+ logger.warn(`[抖音登录] 服务端给出的验证方式均不支持: ${ways}`);
42528
+ await tracker.send(`账号触发了二次验证,但当前仅支持短信验证码,服务端给出的方式为:${ways}`);
42529
+ return false;
42530
+ }
42531
+ const sent = await sendPassportVerifyCode({
42532
+ verify,
42533
+ verify_way: smsWay?.verifyWay,
42534
+ typeMode: "strict"
42535
+ }, session.cookie, requestConfig());
42536
+ if (!sent.success) {
42537
+ await tracker.send(`短信验证码发送失败:${sent.message}`);
42538
+ return false;
42539
+ }
42540
+ session.cookie = sent.data.cookie;
42541
+ if (!sent.data.ok) {
42542
+ await tracker.send(`短信验证码发送失败:${sent.data.message}`);
42543
+ return false;
42544
+ }
42545
+ const bizTraceId = sent.data.biz_trace_id;
42546
+ const verifyWay = sent.data.verify_way;
42547
+ logger.mark(`[抖音登录] 二次验证方式: ${verifyWay}`);
42548
+ const target = sent.data.mobile || smsWay?.mobile || "扫码设备绑定的手机号";
42549
+ await tracker.send(`此次登录需要二次验证\n6 位数验证码已发送至 ${target}\n请在 ${CODE_INPUT_TIMEOUT} 秒内直接回复该验证码`);
42550
+ for (let attempt = 1; attempt <= CODE_MAX_ATTEMPTS; attempt++) {
42551
+ const context = await karin$1.ctx(e, {
42552
+ time: CODE_INPUT_TIMEOUT,
42553
+ reply: false,
42554
+ throwOnTimeout: false
42555
+ });
42556
+ if (!context) {
42557
+ await tracker.send("等待验证码超时,登录已取消");
42558
+ return false;
42559
+ }
42560
+ const code = context.msg.trim();
42561
+ if (!CODE_PATTERN.test(code)) {
42562
+ if (attempt === CODE_MAX_ATTEMPTS) {
42563
+ await tracker.send("输入格式不正确,登录已取消");
42564
+ return false;
42565
+ }
42566
+ await tracker.send(`请只发送 6 位数字验证码(剩余 ${CODE_MAX_ATTEMPTS - attempt} 次机会)`);
42567
+ continue;
42568
+ }
42569
+ const checked = await validatePassportVerifyCode({
42570
+ verify,
42571
+ code,
42572
+ biz_trace_id: bizTraceId,
42573
+ verify_way: verifyWay,
42574
+ typeMode: "strict"
42575
+ }, session.cookie, requestConfig());
42576
+ if (!checked.success) {
42577
+ await tracker.send(`验证失败:${checked.message}`);
42578
+ return false;
42579
+ }
42580
+ session.cookie = checked.data.cookie;
42581
+ if (checked.data.ok) {
42582
+ logger.mark("[抖音登录] 二次验证通过");
42583
+ await tracker.send("验证通过,正在完成登录…");
40941
42584
  return true;
40942
42585
  }
40943
- } catch (err) {
40944
- logger.error("登录流程出错:", err);
40945
- if (gcInterval) clearInterval(gcInterval);
40946
- await puppeteer.browser.close();
40947
- await e.reply("登录过程出错,请查看控制台日志", { reply: true });
42586
+ if (!checked.data.wrongCode || attempt === CODE_MAX_ATTEMPTS) {
42587
+ await tracker.send(`验证失败:${checked.data.message}`);
42588
+ return false;
42589
+ }
42590
+ await tracker.send(`验证码错误,请重新发送(剩余 ${CODE_MAX_ATTEMPTS - attempt} 次机会)`);
40948
42591
  }
40949
- return true;
42592
+ return false;
40950
42593
  };
40951
42594
  /**
40952
- * 等待二维码出现并获取二维码信息
40953
- * @param page puppeteer的页面对象
40954
- * @returns 包含解码URL和原始图片的对象
42595
+ * 保存登录凭证并重载 Amagi 客户端
42596
+ * @param cookie 完整登录 cookie
40955
42597
  */
40956
- var waitQrcode = async (page) => {
40957
- const qrCodeSelector = "img[aria-label=\"二维码\"]";
40958
- try {
40959
- await page.waitForSelector(qrCodeSelector, { timeout: 6e4 });
40960
- } catch {
40961
- await safeScreenshot(page, path.join(karinPathTemp, Root.pluginName, "DouyinLoginQrcodeError.png"));
40962
- throw new Error("加载超时了,或者遇到验证码了。。。");
40963
- }
40964
- logger.debug("二维码加载完成");
40965
- await new Promise((resolve) => setTimeout(resolve, 1e3));
40966
- const qrCodeImages = (await page.$$eval("img", (imgs) => {
40967
- return imgs.map((img) => ({
40968
- src: img.src,
40969
- ariaLabel: img.getAttribute("aria-label")
40970
- }));
40971
- })).filter((img) => img.ariaLabel === "二维码");
40972
- if (qrCodeImages.length === 0) throw new Error("未找到二维码");
40973
- const originalImage = qrCodeImages[0].src;
42598
+ var persistCookie = async (cookie) => {
42599
+ const present = new Set(cookie.split(";").map((pair) => pair.split("=")[0].trim()));
42600
+ const missing = REQUIRED_COOKIES.filter((name) => !present.has(name));
42601
+ if (missing.length > 0) logger.warn(`[抖音登录] 以下 cookie 未在本次登录中下发:${missing.join(", ")}`);
42602
+ await Config.Modify("amagi", "cookies.douyin", cookie);
42603
+ const reloaded = reloadAmagiConfig();
42604
+ logger.mark(`[抖音登录] 登录凭证已保存,Amagi Client ${reloaded ? "已重载" : "配置未变化"}`);
42605
+ };
42606
+ /**
42607
+ * 抖音扫码登录
42608
+ *
42609
+ * 协议层在 `@ikenxuan/amagi` 的 passport 接口里,这里只负责与用户的交互和状态流转,
42610
+ * 全程不启动浏览器。
42611
+ * @param e 消息事件
42612
+ */
42613
+ var douyinLogin = async (e) => {
42614
+ const tracker = createMessageTracker(e);
40974
42615
  try {
40975
- let imageBuffer;
40976
- if (originalImage.startsWith("data:image")) {
40977
- const base64Data = originalImage.split(",")[1];
40978
- imageBuffer = Buffer.from(base64Data, "base64");
40979
- } else {
40980
- const response = await fetch(originalImage);
40981
- imageBuffer = Buffer.from(await response.arrayBuffer());
42616
+ const qrcode = await requestPassportQrcode({ typeMode: "strict" }, void 0, requestConfig());
42617
+ if (!qrcode.success) {
42618
+ await e.reply(`获取二维码失败:${qrcode.message}`, { reply: true });
42619
+ return true;
40982
42620
  }
40983
- const qrContent = scanSync(imageBuffer);
40984
- if (qrContent) {
40985
- logger.mark("二维码解码成功:", qrContent);
40986
- return {
40987
- url: qrContent,
40988
- originalImage
40989
- };
42621
+ const session = {
42622
+ cookie: qrcode.data.cookie,
42623
+ token: qrcode.data.token
42624
+ };
42625
+ const validFor = qrcode.data.expires_in;
42626
+ logger.mark(`[抖音登录] 二维码已获取,有效期 ${validFor} 秒`);
42627
+ const rendered = await Render(e, "douyin/qrcodeImg", { share_url: qrcode.data.content });
42628
+ const base64Data = rendered[0]?.file;
42629
+ if (!base64Data) throw new Error("生成二维码图片失败");
42630
+ fs.writeFileSync(`${Common.tempDri.default}DouyinLoginQrcode.png`, Buffer.from(base64Data.replace(/^base64:\/\//, ""), "base64"));
42631
+ await tracker.send(rendered);
42632
+ let deadline = Date.now() + (validFor > 0 ? Math.min(validFor * 1e3, SCAN_TIMEOUT) : SCAN_TIMEOUT);
42633
+ let scanned = false;
42634
+ while (Date.now() < deadline) {
42635
+ const polled = await checkPassportQrcode({
42636
+ token: session.token,
42637
+ typeMode: "strict"
42638
+ }, session.cookie, requestConfig());
42639
+ if (!polled.success) {
42640
+ await tracker.recallAll();
42641
+ await e.reply(`轮询二维码状态失败:${polled.message}`, { reply: true });
42642
+ return true;
42643
+ }
42644
+ const result = polled.data;
42645
+ session.cookie = result.cookie;
42646
+ switch (result.status) {
42647
+ case "new": break;
42648
+ case "scanned":
42649
+ if (!scanned) {
42650
+ scanned = true;
42651
+ deadline = Date.now() + CONFIRM_TIMEOUT;
42652
+ await tracker.recallAll();
42653
+ await tracker.send("二维码已扫描,请在手机上确认登录");
42654
+ }
42655
+ break;
42656
+ case "verify":
42657
+ if (!await handleSecondVerify(e, session, result.verify, tracker)) {
42658
+ await tracker.recallAll();
42659
+ return true;
42660
+ }
42661
+ deadline = Date.now() + CONFIRM_TIMEOUT;
42662
+ break;
42663
+ case "confirmed":
42664
+ if (!result.logged_in) {
42665
+ await tracker.recallAll();
42666
+ await e.reply("已确认登录,但服务端未下发登录凭证,请稍后重试", { reply: true });
42667
+ return true;
42668
+ }
42669
+ await persistCookie(result.cookie);
42670
+ await tracker.recallAll();
42671
+ await e.reply("登录成功!用户登录凭证已保存至配置", { reply: true });
42672
+ return true;
42673
+ case "expired":
42674
+ await tracker.recallAll();
42675
+ await e.reply("二维码已失效,请重新发起登录", { reply: true });
42676
+ return true;
42677
+ case "busy":
42678
+ logger.debug(`[抖音登录] 轮询被限频,${result.interval} ms 后重试: ${result.message}`);
42679
+ break;
42680
+ case "risk":
42681
+ logger.warn(`[抖音登录] 命中风控: ${result.message}`);
42682
+ await tracker.recallAll();
42683
+ await e.reply(`登录请求被抖音风控拦截:${result.message}\n请稍后再试`, { reply: true });
42684
+ return true;
42685
+ case "unknown":
42686
+ logger.warn(`[抖音登录] 未知的轮询状态: ${result.message}`);
42687
+ break;
42688
+ }
42689
+ await sleep(result.interval);
40990
42690
  }
42691
+ await tracker.recallAll();
42692
+ await e.reply(scanned ? "等待手机确认超时,登录已取消" : "登录超时!二维码已失效!", { reply: true });
40991
42693
  } catch (error) {
40992
- logger.warn("二维码解码失败:", error);
42694
+ logger.error("[抖音登录] 登录流程出错:", error);
42695
+ await tracker.recallAll();
42696
+ await e.reply("登录过程出错,请查看控制台日志", { reply: true });
40993
42697
  }
40994
- logger.warn("解码失败,将使用原始二维码图片");
40995
- return {
40996
- url: null,
40997
- originalImage
40998
- };
42698
+ return true;
40999
42699
  };
41000
42700
  //#endregion
41001
42701
  //#region src/apps/admin.ts
@@ -41746,11 +43446,12 @@ var globalStatistics = karin.command(/^#?kkk全局解析统计$/, handleGlobalSt
41746
43446
  //#endregion
41747
43447
  //#region src/apps/testPush.ts
41748
43448
  /** 构建与生产推送一致的作品二维码链接。 */
41749
- function buildWorkShareLink(aweme) {
43449
+ function buildWorkShareLink(aweme, selectedVideo) {
41750
43450
  const workTypeInfo = getWorkTypeInfo(aweme);
41751
43451
  if (workTypeInfo.isArticle) return `https://www.douyin.com/article/${aweme.aweme_id}`;
41752
43452
  if (workTypeInfo.isImage) return `https://www.douyin.com/note/${aweme.aweme_id}`;
41753
- return aweme.video?.play_addr?.uri ? `https://aweme.snssdk.com/aweme/v1/play/?video_id=${aweme.video.play_addr.uri}&ratio=1080p&line=0` : `https://www.douyin.com/video/${aweme.aweme_id}`;
43453
+ const playAddr = selectedVideo?.play_addr ?? aweme.video?.play_addr;
43454
+ return playAddr ? buildDouyinPlayUrl(playAddr) : `https://www.douyin.com/video/${aweme.aweme_id}`;
41754
43455
  }
41755
43456
  /**
41756
43457
  * 测试抖音推送命令处理器
@@ -41795,20 +43496,18 @@ var handleTestPush = wrapWithErrorHandler(async (e) => {
41795
43496
  return true;
41796
43497
  }
41797
43498
  const aweme = workData.data.aweme_detail;
41798
- const userinfo = await douyinFetcher.fetchUserProfile({
43499
+ const Detail_Data = buildDouyinWorkDetail(aweme, { user_info: await douyinFetcher.fetchUserProfile({
41799
43500
  sec_uid: aweme.author.sec_uid,
41800
43501
  typeMode: "strict"
41801
- });
41802
- const Detail_Data = {
41803
- ...aweme,
41804
- user_info: userinfo
41805
- };
41806
- const shareLink = buildWorkShareLink(Detail_Data);
43502
+ }) });
43503
+ const selectedVideo = aweme.video?.bit_rate?.length ? douyinProcessVideos(aweme.video.bit_rate, Config.douyin.videoQuality)[0] : null;
43504
+ const shareLink = buildWorkShareLink(Detail_Data, selectedVideo);
41807
43505
  images = await renderWorkImage({
41808
43506
  e,
41809
43507
  Detail_Data,
41810
43508
  create_time: aweme.create_time,
41811
- shareLink
43509
+ shareLink,
43510
+ videoSource: selectedVideo
41812
43511
  });
41813
43512
  if (images.length === 0) {
41814
43513
  e.reply("未能识别该作品类型,无法渲染推送图片");
@@ -41844,12 +43543,11 @@ var handleTestPush = wrapWithErrorHandler(async (e) => {
41844
43543
  typeMode: "strict"
41845
43544
  });
41846
43545
  } catch {}
41847
- const Detail_Data = {
41848
- ...aweme,
43546
+ const Detail_Data = buildDouyinWorkDetail(aweme, {
41849
43547
  user_info: userinfo,
41850
43548
  author_user_info: authorUserInfo
41851
- };
41852
- const shareLink = buildWorkShareLink(Detail_Data);
43549
+ });
43550
+ const shareLink = buildWorkShareLink(Detail_Data, aweme.video?.bit_rate?.length ? douyinProcessVideos(aweme.video.bit_rate, Config.douyin.videoQuality)[0] : null);
41853
43551
  images = await renderFavoriteImage({
41854
43552
  e,
41855
43553
  Detail_Data,
@@ -41891,12 +43589,11 @@ var handleTestPush = wrapWithErrorHandler(async (e) => {
41891
43589
  typeMode: "strict"
41892
43590
  });
41893
43591
  } catch {}
41894
- const Detail_Data = {
41895
- ...aweme,
43592
+ const Detail_Data = buildDouyinWorkDetail(aweme, {
41896
43593
  user_info: userinfo,
41897
43594
  author_user_info: authorUserInfo
41898
- };
41899
- const shareLink = buildWorkShareLink(Detail_Data);
43595
+ });
43596
+ const shareLink = buildWorkShareLink(Detail_Data, aweme.video?.bit_rate?.length ? douyinProcessVideos(aweme.video.bit_rate, Config.douyin.videoQuality)[0] : null);
41900
43597
  images = await renderRecommendImage({
41901
43598
  e,
41902
43599
  Detail_Data,
@@ -41969,8 +43666,9 @@ var reg = {
41969
43666
  kuaishou: /(快手.*快手|v\.kuaishou\.com|kuaishou\.com)/,
41970
43667
  xiaohongshu: /(xiaohongshu\.com|xhslink\.(?:com|cn))/
41971
43668
  };
43669
+ var passthroughCommandReg = /^#kkk推送全局忽略/;
41972
43670
  var handleDouyin = wrapWithErrorHandler(async (e, next) => {
41973
- if (e.msg.startsWith("#测试")) return next();
43671
+ if (e.msg.startsWith("#测试") || passthroughCommandReg.test(e.msg)) return next();
41974
43672
  const forceBurnDanmaku = /^#?弹幕解析/.test(e.msg);
41975
43673
  const urlMatch = e.msg.match(/(https?:\/\/[^\s]*\.(douyin|iesdouyin)\.com[^\s]*)/gi);
41976
43674
  if (!urlMatch) {
@@ -41988,6 +43686,7 @@ var handleDouyin = wrapWithErrorHandler(async (e, next) => {
41988
43686
  }
41989
43687
  }, { businessName: "抖音视频解析" });
41990
43688
  var handleBilibili = wrapWithErrorHandler(async (e, next) => {
43689
+ if (passthroughCommandReg.test(e.msg)) return next();
41991
43690
  e.msg = e.msg.replace(/\\/g, "");
41992
43691
  const forceBurnDanmaku = /^#?弹幕解析/.test(e.msg);
41993
43692
  const urlRegex = /(https?:\/\/(?:(?:www\.|m\.|t\.)?bilibili\.com|b23\.tv|bili2233\.cn)\/[a-zA-Z0-9_\-.~:/?#[\]@!$&'()*+,;=]+)/;
@@ -43075,4 +44774,4 @@ mkdirSync(`${karinPathBase}/${Root.pluginName}/data`);
43075
44774
  mkdirSync(Common.tempDri.images);
43076
44775
  mkdirSync(Common.tempDri.video);
43077
44776
  //#endregion
43078
- export { createVoteNode as $, DouyinCommentParamsSchema as $n, MajorType as $t, testWrapWithErrorHandler as A, createBoundDouyinFetcher as An, BilibiliMethodRoutes as Ar, DouyinMethodMapping as At, createImageNode as B, validateXiaohongshuParams as Bn, emitApiSuccess as Br, DouyinFetcherMethods as Bt, changelog as C, createBoundXiaohongshuFetcher as Cn, BilibiliCommentParamsSchema as Cr, softFetch as Ct, dylogin as D, kuaishouSign as Dn, BilibiliEmojiParamsSchema as Dr, BilibiliApiRoutes as Dt, biLogin as E, kuaishouFetcher$1 as En, BilibiliDynamicParamsSchema as Er, amagi as Et, createCodeBlockNode as F, createErrorResponse as Fn, BilibiliValidationSchemas as Fr, getApiRoute as Ft, createLotteryNode as G, KuaishouCommentParamsSchema as Gn, emitLogError as Gr, KuaishouMethodToFetcher as Gt, createLinkCardNode as H, XiaohongshuValidationSchemas as Hn, emitHttpResponse as Hr, DouyinMethodToFetcher as Ht, createEmojiNode as I, createSuccessResponse$1 as In, BilibiliVideoDownloadParamsSchema as Ir, getEnglishMethodName as It, createRichTextDocument as J, KuaishouMethodRoutes as Jn, emitLogWarn as Jr, XiaohongshuInternalMethods as Jt, createMentionNode as K, KuaishouEmojiParamsSchema as Kn, emitLogInfo as Kr, MethodMaps as Kt, createHashtagNode as L, validateBilibiliParams as Ln, BilibiliVideoParamsSchema as Lr, BilibiliFetcherMethods as Lt, renderRichTextToReact as M, douyinSign as Mn, BilibiliQrcodeStatusParamsSchema as Mr, KuaishouMethodMapping as Mt, createAtNode as N, bilibiliFetcher$1 as Nn, BilibiliUserParamsSchema as Nr, XiaohongshuApiRoutes as Nt, removeOldFiles as O, kuaishouApiUrls as On, BilibiliLiveParamsSchema as Or, BilibiliMethodMapping as Ot, createBlockquoteNode as P, createBoundBilibiliFetcher as Pn, BilibiliValidateCaptchaParamsSchema as Pr, XiaohongshuMethodMapping as Pt, createViewPictureNode as Q, KuaishouVideoParamsSchema as Qn, Root as Qr, AdditionalType as Qt, createHeadingNode as R, validateDouyinParams as Rn, amagiEvents as Rr, BilibiliInternalMethods as Rt, setdyPush as S, isNetworkErrorResult as Sn, BilibiliColumnInfoParamsSchema as Sr, reloadAmagiConfig as St, version as T, createBoundKuaishouFetcher as Tn, BilibiliDanmakuParamsSchema as Tr, CreateApp as Tt, createListItemNode as U, xiaohongshuApiUrls as Un, emitLog as Ur, KuaishouFetcherMethods as Ut, createLineBreakNode as V, XiaohongshuMethodRoutes as Vn, emitHttpRequest as Vr, DouyinInternalMethods as Vt, createListNode as W, xiaohongshuSign as Wn, emitLogDebug as Wr, KuaishouInternalMethods as Wt, createTextNode as X, KuaishouUserWorkListParamsSchema as Xn, emitNetworkRetry as Xr, toFetcherMethod as Xt, createSearchKeywordNode as Y, KuaishouUserProfileParamsSchema as Yn, emitNetworkError as Yr, XiaohongshuMethodToFetcher as Yt, createTopicNode as Z, KuaishouValidationSchemas as Zn, bilibiliApiUrls as Zr, DynamicType as Zt, douyinPush as _, bv2av as _n, BilibiliArticleParamsSchema as _r, SOFT_ERROR_CODES as _t, bilibiliAPP as a, createKuaishouRoutes as an, DouyinLiveRoomParamsSchema as ar, bilibiliDBInstance as at, globalIgnore as b, fetchResponse as bn, BilibiliBangumiStreamParamsSchema as br, kuaishouFetcher as bt, prefix as c, bilibiliUtils as cn, DouyinQrcodeParamsSchema as cr, getBilibiliDB as ct, globalStatistics as d, ValidationError as dn, DouyinUserParamsSchema as dr, initAllDatabases as dt, CommentType as en, DouyinCommentReplyParamsSchema as er, createWebLinkNode as et, groupStatistics as f, handleError as fn, DouyinValidationSchemas as fr, StatisticsDBBase as ft, changeBotID as g, av2bv as gn, BilibiliArticleInfoParamsSchema as gr, AmagiError as gt, bilibiliPushList as h, parseDmSegMobileReply as hn, BilibiliArticleCardParamsSchema as hr, AmagiBase as ht, update as i, kuaishouUtils as in, DouyinHotWordsParamsSchema as ir, webConfig as it, templates as j, douyinApiUrls as jn, BilibiliQrcodeParamsSchema as jr, KuaishouApiRoutes as jt, task as k, douyinFetcher$1 as kn, BilibiliLoginParamsSchema as kr, DouyinApiRoutes as kt, xiaohongshuAPP as l, createBilibiliRoutes as ln, DouyinSearchParamsSchema as lr, getDouyinDB as lt, bilibiliPush as m, wbi_sign as mn, BilibiliApplyCaptchaParamsSchema as mr, BilibiliDBBase as mt, kkkUpdateCommand as n, xiaohongshuUtils as nn, DouyinEmojiListParamsSchema as nr, normalizeRichTextNodes as nt, douyinAPP as o, douyinUtils as on, DouyinMethodRoutes as or, cleanOldDynamicCache as ot, qrLogin as p, bilibiliErrorCodeMap as pn, DouyinWorkParamsSchema as pr, DouyinDBBase as pt, createParagraphNode as q, KuaishouLiveRoomInfoParamsSchema as qn, emitLogMark as qr, XiaohongshuFetcherMethods as qt, kkkUpdateTest as r, createXiaohongshuRoutes as rn, DouyinEmojiProParamsSchema as rr, renderVideoPreviewPage as rt, kuaishouAPP as s, createDouyinRoutes as sn, DouyinMusicParamsSchema as sr, douyinDBInstance as st, kkkUpdate as t, createAmagiClient as tn, DouyinDanmakuParamsSchema as tr, extractRichTextPlainText as tt, testPush as u, ApiError as un, DouyinUserListParamsSchema as ur, getStatisticsDB as ut, douyinPushList as v, qtparam as vn, BilibiliAv2BvParamsSchema as vr, bilibiliFetcher as vt, help as w, xiaohongshuFetcher$1 as wn, BilibiliCommentReplyParamsSchema as wr, xiaohongshuFetcher as wt, setbiliPush as x, getHeadersAndData as xn, BilibiliBv2AvParamsSchema as xr, registerAmagiReloadListener as xt, forcePush as y, fetchData as yn, BilibiliBangumiInfoParamsSchema as yr, douyinFetcher as yt, createHorizontalRuleNode as z, validateKuaishouParams as zn, emitApiError as zr, BilibiliMethodToFetcher as zt };
44777
+ export { createVoteNode as $, KuaishouLiveRoomInfoParamsSchema as $n, emitLogMark as $r, MajorType as $t, testWrapWithErrorHandler as A, createBoundDouyinFetcher as An, BilibiliCommentReplyParamsSchema as Ar, DouyinMethodMapping as At, createImageNode as B, createBoundBilibiliFetcher as Bn, BilibiliValidateCaptchaParamsSchema as Br, DouyinFetcherMethods as Bt, changelog as C, createBoundXiaohongshuFetcher as Cn, BilibiliArticleParamsSchema as Cr, softFetch as Ct, dylogin as D, kuaishouSign as Dn, BilibiliBv2AvParamsSchema as Dr, BilibiliApiRoutes as Dt, biLogin as E, kuaishouFetcher$1 as En, BilibiliBangumiStreamParamsSchema as Er, amagi as Et, createCodeBlockNode as F, requestPassportQrcode as Fn, BilibiliLoginParamsSchema as Fr, getApiRoute as Ft, createLotteryNode as G, validateKuaishouParams as Gn, emitApiError as Gr, KuaishouMethodToFetcher as Gt, createLinkCardNode as H, createSuccessResponse$1 as Hn, BilibiliVideoDownloadParamsSchema as Hr, DouyinMethodToFetcher as Ht, createEmojiNode as I, sendPassportVerifyCode as In, BilibiliMethodRoutes as Ir, getEnglishMethodName as It, createRichTextDocument as J, XiaohongshuValidationSchemas as Jn, emitHttpResponse as Jr, XiaohongshuInternalMethods as Jt, createMentionNode as K, validateXiaohongshuParams as Kn, emitApiSuccess as Kr, MethodMaps as Kt, createHashtagNode as L, validatePassportVerifyCode as Ln, BilibiliQrcodeParamsSchema as Lr, BilibiliFetcherMethods as Lt, renderRichTextToReact as M, douyinSign as Mn, BilibiliDynamicParamsSchema as Mr, KuaishouMethodMapping as Mt, createAtNode as N, checkPassportQrcode as Nn, BilibiliEmojiParamsSchema as Nr, XiaohongshuApiRoutes as Nt, removeOldFiles as O, kuaishouApiUrls as On, BilibiliColumnInfoParamsSchema as Or, BilibiliMethodMapping as Ot, createBlockquoteNode as P, isSmsCodeVerifyWay as Pn, BilibiliLiveParamsSchema as Pr, XiaohongshuMethodMapping as Pt, createViewPictureNode as Q, KuaishouEmojiParamsSchema as Qn, emitLogInfo as Qr, AdditionalType as Qt, createHeadingNode as R, passport_exports as Rn, BilibiliQrcodeStatusParamsSchema as Rr, BilibiliInternalMethods as Rt, setdyPush as S, isNetworkErrorResult as Sn, BilibiliArticleInfoParamsSchema as Sr, reloadAmagiConfig as St, version as T, createBoundKuaishouFetcher as Tn, BilibiliBangumiInfoParamsSchema as Tr, CreateApp as Tt, createListItemNode as U, validateBilibiliParams as Un, BilibiliVideoParamsSchema as Ur, KuaishouFetcherMethods as Ut, createLineBreakNode as V, createErrorResponse as Vn, BilibiliValidationSchemas as Vr, DouyinInternalMethods as Vt, createListNode as W, validateDouyinParams as Wn, amagiEvents as Wr, KuaishouInternalMethods as Wt, createTextNode as X, xiaohongshuSign as Xn, emitLogDebug as Xr, toFetcherMethod as Xt, createSearchKeywordNode as Y, xiaohongshuApiUrls as Yn, emitLog as Yr, XiaohongshuMethodToFetcher as Yt, createTopicNode as Z, KuaishouCommentParamsSchema as Zn, emitLogError as Zr, DynamicType as Zt, douyinPush as _, bv2av as _n, DouyinUserParamsSchema as _r, SOFT_ERROR_CODES as _t, bilibiliAPP as a, createKuaishouRoutes as an, DouyinCommentParamsSchema as ar, bilibiliDBInstance as at, globalIgnore as b, fetchResponse as bn, BilibiliApplyCaptchaParamsSchema as br, kuaishouFetcher as bt, prefix as c, bilibiliUtils as cn, DouyinEmojiListParamsSchema as cr, getBilibiliDB as ct, globalStatistics as d, ValidationError as dn, DouyinLiveRoomParamsSchema as dr, initAllDatabases as dt, emitLogWarn as ei, CommentType as en, KuaishouMethodRoutes as er, createWebLinkNode as et, groupStatistics as f, handleError as fn, DouyinMethodRoutes as fr, StatisticsDBBase as ft, changeBotID as g, av2bv as gn, DouyinUserListParamsSchema as gr, AmagiError as gt, bilibiliPushList as h, parseDmSegMobileReply as hn, DouyinSearchParamsSchema as hr, AmagiBase as ht, update as i, Root as ii, kuaishouUtils as in, KuaishouVideoParamsSchema as ir, webConfig as it, templates as j, douyinApiUrls as jn, BilibiliDanmakuParamsSchema as jr, KuaishouApiRoutes as jt, task as k, douyinFetcher$1 as kn, BilibiliCommentParamsSchema as kr, DouyinApiRoutes as kt, xiaohongshuAPP as l, createBilibiliRoutes as ln, DouyinEmojiProParamsSchema as lr, getDouyinDB as lt, bilibiliPush as m, wbi_sign as mn, DouyinQrcodeParamsSchema as mr, BilibiliDBBase as mt, kkkUpdateCommand as n, emitNetworkRetry as ni, xiaohongshuUtils as nn, KuaishouUserWorkListParamsSchema as nr, normalizeRichTextNodes as nt, douyinAPP as o, douyinUtils as on, DouyinCommentReplyParamsSchema as or, cleanOldDynamicCache as ot, qrLogin as p, bilibiliErrorCodeMap as pn, DouyinMusicParamsSchema as pr, DouyinDBBase as pt, createParagraphNode as q, XiaohongshuMethodRoutes as qn, emitHttpRequest as qr, XiaohongshuFetcherMethods as qt, kkkUpdateTest as r, bilibiliApiUrls as ri, createXiaohongshuRoutes as rn, KuaishouValidationSchemas as rr, renderVideoPreviewPage as rt, kuaishouAPP as s, createDouyinRoutes as sn, DouyinDanmakuParamsSchema as sr, douyinDBInstance as st, kkkUpdate as t, emitNetworkError as ti, createAmagiClient as tn, KuaishouUserProfileParamsSchema as tr, extractRichTextPlainText as tt, testPush as u, ApiError as un, DouyinHotWordsParamsSchema as ur, getStatisticsDB as ut, douyinPushList as v, qtparam as vn, DouyinValidationSchemas as vr, bilibiliFetcher as vt, help as w, xiaohongshuFetcher$1 as wn, BilibiliAv2BvParamsSchema as wr, xiaohongshuFetcher as wt, setbiliPush as x, getHeadersAndData as xn, BilibiliArticleCardParamsSchema as xr, registerAmagiReloadListener as xt, forcePush as y, fetchData as yn, DouyinWorkParamsSchema as yr, douyinFetcher as yt, createHorizontalRuleNode as z, bilibiliFetcher$1 as zn, BilibiliUserParamsSchema as zr, BilibiliMethodToFetcher as zt };