@ikenxuan/amagi 7.0.0-beta.7 → 7.0.0-beta.8

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,12 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { C as wbi, S as qtparam$1, _ as douyinSign$1, a as createXiaohongshuGuestCookie$1, b as getCookieValue, c as sharedKuaishouSigner, d as douyinBogus, f as douyinXBogus, g as applySecsdkWebSign, h as xBogus, i as getSearchId, l as deriveKuaishouPureSignature, m as secsdk, n as traceId, o as createXiaohongshuCryptoConfig, p as msToken, r as xs, s as hxfalcon, t as rap, u as aBogus$1, v as XBogus, x as AmagiHeaders, y as observeDouyinWebid } from "./steps-DQlwODdh.mjs";
3
- import { A as a_bogus_default, M as buildBrowserInfo, d as genVerifyFp, m as ABogus, o as TTWID_REGISTER_URL, r as DOUYIN_TTWID } from "./tokens-BFqSOfhx.mjs";
2
+ import { n as bv2av, t as av2bv } from "./bv2av-DUm4x7as.mjs";
3
+ import { _ as AmagiHeaders, a as createXiaohongshuGuestCookie$1, c as sharedKuaishouSigner, d as douyinXBogus, f as msToken, g as observeDouyinWebid, h as douyinSign$1, i as getSearchId, l as aBogus$1, m as xBogus, n as traceId, o as createXiaohongshuCryptoConfig, p as secsdk, r as xs, s as hxfalcon, t as rap, u as douyinBogus, v as qtparam$1, y as wbi } from "./steps-bZrsb4W6.mjs";
4
+ import { A as genVerifyFp, Ct as getCookieValue, N as ABogus, Y as a_bogus_default, Z as buildBrowserInfo, _t as applySecsdkWebSign, i as deriveKuaishouPureSignature, o as XBogus, w as TTWID_REGISTER_URL, x as DOUYIN_TTWID, yt as extractUifidFromCookie } from "./he-q7589Kp7.mjs";
4
5
  import express, { Router } from "express";
5
6
  import zod from "zod";
6
- import crypto, { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import protobuf from "protobufjs";
8
8
  import axios, { AxiosError, AxiosHeaders, mergeConfig } from "axios";
9
9
  import { EventEmitter } from "node:events";
10
+ import crypto, { createCipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
11
+ import vm from "node:vm";
10
12
  import { FingerprintGenerator, Xhshow } from "@ikenxuan/xhshow-ts";
11
13
  //#region src/platforms/bilibili/api.ts
12
14
  /**
@@ -408,57 +410,6 @@ const articleListInfo = defineBilibiliEndpoint({
408
410
  response: type()
409
411
  });
410
412
  //#endregion
411
- //#region src/platforms/bilibili/sign/bv2av.ts
412
- const XOR_CODE = 23442827791579n;
413
- const MASK_CODE = 2251799813685247n;
414
- const MAX_AID = 1n << 51n;
415
- const BASE = 58n;
416
- const data = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf";
417
- /**
418
- * av号转bv号
419
- * @param aid av号
420
- * @returns
421
- */
422
- const av2bv = (aid) => {
423
- const bytes = [
424
- "B",
425
- "V",
426
- "1",
427
- "0",
428
- "0",
429
- "0",
430
- "0",
431
- "0",
432
- "0",
433
- "0",
434
- "0",
435
- "0"
436
- ];
437
- let bvIndex = bytes.length - 1;
438
- let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE;
439
- while (tmp > 0) {
440
- bytes[bvIndex] = data[Number(tmp % BigInt(BASE))];
441
- tmp = tmp / BASE;
442
- bvIndex -= 1;
443
- }
444
- [bytes[3], bytes[9]] = [bytes[9], bytes[3]];
445
- [bytes[4], bytes[7]] = [bytes[7], bytes[4]];
446
- return bytes.join("");
447
- };
448
- /**
449
- * bv号转av号
450
- * @param bvid bv号
451
- * @returns
452
- */
453
- const bv2av = (bvid) => {
454
- const bvidArr = Array.from(bvid);
455
- [bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]];
456
- [bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]];
457
- bvidArr.splice(0, 3);
458
- const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar)), 0n);
459
- return Number(tmp & MASK_CODE ^ XOR_CODE);
460
- };
461
- //#endregion
462
413
  //#region src/platforms/bilibili/endpoints/avToBv.ts
463
414
  /**
464
415
  * AV 号转 BV 号(纯本地计算,不发请求)。
@@ -2208,6 +2159,7 @@ const DEFAULT_ERROR_MESSAGES = {
2208
2159
  RATE_LIMITED: "请求过于频繁,请稍后再试",
2209
2160
  RISK_CONTROL: "触发平台风控",
2210
2161
  CAPTCHA_REQUIRED: "需要完成验证码",
2162
+ SIGNATURE_REFUSED: "平台拒绝了本次请求的签名(amagi 侧的签名问题,换 cookie 无用)",
2211
2163
  NOT_FOUND: "资源不存在",
2212
2164
  DELETED: "资源已删除或已下架",
2213
2165
  PRIVATE: "资源为私密状态",
@@ -2449,6 +2401,61 @@ const DOUYIN_ANDROID_UA = "Mozilla/5.0 (Linux; Android 13; SM-S908E) AppleWebKit
2449
2401
  */
2450
2402
  const isDouyinArgusBody = (raw) => typeof raw === "string" && /ArgusSecurityPlugin|Blocked by/i.test(raw);
2451
2403
  /**
2404
+ * Argus 拦截文本里点名「签名不对」的片段。
2405
+ *
2406
+ * 命中这些的 403 是 **amagi 自己的问题,不是这个账号的问题** —— 平台在说
2407
+ * 「你这个签名我不认」,而不是「你这个号被风控了」。两者处置方式完全相反:
2408
+ * 前者换 cookie 一万次也没用(得修签名),后者换个号就好了。
2409
+ *
2410
+ * 片段清单来自线上实测,全部是 `Blocked by ArgusSecurityPlugin <reason>` 形态。
2411
+ * 403 若一律判风控,「签名器少算了一个参数」就会表现成账号被风控,排查方向
2412
+ * 整个跑偏。
2413
+ *
2414
+ * `uifid not found` 这一条在本仓库有具体成因:`/aweme/v1/web/music/detail/` 这类
2415
+ * 受 secsdk 保护的端点要求访客 id,而扫码登录拿回的 cookie 里**没有** `UIFID`
2416
+ * (它由 secsdk 在真实浏览器里铸造,passport 链路不经过那一步)。见
2417
+ * `sign/secsdkWebSign.ts` 的 `applySecsdkWebSign`。
2418
+ */
2419
+ const SIGNATURE_REFUSAL_MARKERS = [
2420
+ "uifid not found",
2421
+ "signature not found",
2422
+ "sign invalid",
2423
+ "sign expired"
2424
+ ];
2425
+ /**
2426
+ * 从 Argus 拦截文本里认出「签名被拒」。
2427
+ *
2428
+ * 只在**已经确定是 Argus 文本**之后调用(`isDouyinArgusBody` 为真),所以这里
2429
+ * 直接按子串找,不必担心把正常响应里恰好出现的字眼当成拦截。
2430
+ * @param raw - decode 之后的响应体
2431
+ * @returns 命中的片段;不是签名类拒绝时返回 `undefined`
2432
+ */
2433
+ const douyinSignatureRefusal = (raw) => {
2434
+ if (typeof raw !== "string") return void 0;
2435
+ const text = raw.toLowerCase();
2436
+ return SIGNATURE_REFUSAL_MARKERS.find((marker) => text.includes(marker));
2437
+ };
2438
+ /**
2439
+ * 抖音风控页(verify_center)的判据字段。
2440
+ *
2441
+ * 实物形状(`status_code: 10000` + 这个字段,字段本身是**字符串形式的 JSON**):
2442
+ *
2443
+ * ```jsonc
2444
+ * { "status_code": 10000, "status_msg": "",
2445
+ * "verify_center_decision_conf": "{\"verify_center_decision\":\"verify_hit\",\"decision_conf\":{\"subtype\":\"slide\"}}" }
2446
+ * ```
2447
+ *
2448
+ * **按字段名判,不扫全文**。扫 `captcha` 这类子串会把「简介里恰好提到验证码」
2449
+ * 的正常作品判成风控,而每一次误判都会冤枉一个健康账号;字段名不会出现在用户
2450
+ * 文案里。
2451
+ *
2452
+ * 这一条只负责**定性**到「需要人机验证」。它不产出验证页地址 —— 目前掌握的
2453
+ * 这份响应里确实没有可跳转的 URL,`verify_center_decision_conf` 里只有
2454
+ * `verify_hit` 与 `subtype: slide` 这类决策信息。所以 amagi 不装
2455
+ * `ChallengeExtractor`(`error.challenge` 仍为空),不编造一个地址出来。
2456
+ */
2457
+ const VERIFY_CENTER_KEY = "verify_center_decision_conf";
2458
+ /**
2452
2459
  * 抖音平台默认响应判定。
2453
2460
  *
2454
2461
  * 判定规则:
@@ -2474,10 +2481,22 @@ const douyinJudge = (raw, http) => {
2474
2481
  code: "EMPTY_RESPONSE",
2475
2482
  retryable: false
2476
2483
  };
2484
+ if (douyinSignatureRefusal(raw)) return {
2485
+ ok: false,
2486
+ kind: "risk",
2487
+ code: "SIGNATURE_REFUSED",
2488
+ retryable: false
2489
+ };
2477
2490
  const nonJson = verdictFromNonJsonBody(raw);
2478
2491
  if (nonJson) return nonJson;
2479
2492
  if (typeof raw !== "object" || raw === null) return verdictFromHttpStatus(http.status) ?? { ok: true };
2480
2493
  const body = raw;
2494
+ if (body[VERIFY_CENTER_KEY] !== void 0 && body[VERIFY_CENTER_KEY] !== null && body[VERIFY_CENTER_KEY] !== "") return {
2495
+ ok: false,
2496
+ kind: "risk",
2497
+ code: "CAPTCHA_REQUIRED",
2498
+ retryable: false
2499
+ };
2481
2500
  const filterDetail = body.filter_detail;
2482
2501
  if (filterDetail && typeof filterDetail.filter_reason === "string" && filterDetail.filter_reason.length > 0) return {
2483
2502
  ok: false,
@@ -2495,6 +2514,282 @@ const douyinJudge = (raw, http) => {
2495
2514
  return verdictFromHttpStatus(http.status) ?? { ok: true };
2496
2515
  };
2497
2516
  //#endregion
2517
+ //#region src/platforms/douyin/mint.ts
2518
+ /**
2519
+ * 抖音访客 id(`UIFID_TEMP`)的铸造与 cookie 审计。
2520
+ *
2521
+ * 受 secsdk 保护的端点(`/aweme/v1/web/music/detail/` 等)要求 cookie 里带访客 id,
2522
+ * 而它不由登录颁发——passport 扫码链路只发身份 cookie。缺了它,受保护端点稳定回
2523
+ * `403 Uifid Not Found`(见 `judge.ts` 的 `SIGNATURE_REFUSED` 归因)。
2524
+ *
2525
+ * 铸造是纯 HTTP 的,三步:
2526
+ *
2527
+ * 1. `GET www.douyin.com` → 服务端发 `__ac_nonce`,响应体是一份挑战页(内联
2528
+ * byted_acrawler 本体 + 引导脚本 `init({aid: 99999999, dfp: 0})` 与
2529
+ * `sign("", nonce)`);
2530
+ * 2. 在 Node `vm` 里执行本体,离线算出 `__ac_signature`;
2531
+ * 3. 带 `__ac_nonce + __ac_signature` 再 GET 一次 → 服务端一次性 Set-Cookie:
2532
+ * `ttwid` + `web_sign_token` + `UIFID_TEMP`。
2533
+ *
2534
+ * acrawler 本体不进仓库——每次铸造从挑战页现取现执行,抖音更新算法时自动跟随。
2535
+ * 同一客户端上下文(IP + UA)重复铸造返回同一个 `UIFID_TEMP`(有效期约 500 天),
2536
+ * 进程内缓存一次即可。
2537
+ *
2538
+ * 接线:`PLATFORM_RUNTIME.douyin.ensureCookie` 在每次请求 prepare 之后审计
2539
+ * (prepare 可能整串换 cookie,审计要对最终生效的那份负责);`session/qrcode.ts`
2540
+ * 在登录完成时审计,让扫码登录直接产出带访客 id 的凭证。
2541
+ *
2542
+ * 铸造尽力而为:网络失败、挑战页形状变化、签名被拒——一律返回原 cookie,归因
2543
+ * 交给 `SIGNATURE_REFUSED`。不抛错:自愈失败不该把调用变成 `internal`。
2544
+ *
2545
+ * @module platform/douyin/mint
2546
+ */
2547
+ /** 无调用方 UA 时铸造请求的兜底 UA。与 amagi 默认基线同代 */
2548
+ const DEFAULT_MINT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
2549
+ /** 挑战页引导脚本里的 aid。实测固定为 99999999,从引导块里解析,解析不到用它兜底 */
2550
+ const AID_FALLBACK = 99999999;
2551
+ /** acrawler 本体的最小长度。挑战页里最大的内联块(实测 ~71KB),低于此视为形状变化 */
2552
+ const ACGRAWLER_MIN_LENGTH = 1e4;
2553
+ /**
2554
+ * 从挑战页 HTML 里解析 acrawler 本体与引导 aid。
2555
+ *
2556
+ * 规则刻意保守:取**最大的无 `src` 内联 script 块**当本体(实测 ~71KB,远大于页面
2557
+ * 上任何其它内联块),引导块按「提到 `byted_acrawler` 且含 `sign(`」认——两者都不
2558
+ * 合格就返回 `undefined`,宁可放弃铸造也不把来路不明的脚本执行一遍。
2559
+ * @param html - 挑战页 HTML(首次请求的响应体)
2560
+ * @returns 解析结果;形状不对时 `undefined`
2561
+ */
2562
+ const extractChallengeParts = (html) => {
2563
+ const blocks = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]);
2564
+ if (blocks.length === 0) return void 0;
2565
+ const acrawlerSource = blocks.reduce((a, b) => b.length > a.length ? b : a);
2566
+ if (acrawlerSource.length < ACGRAWLER_MIN_LENGTH) return void 0;
2567
+ const bootstrap = blocks.find((b) => b !== acrawlerSource && b.includes("byted_acrawler") && b.includes("sign("));
2568
+ if (!bootstrap) return void 0;
2569
+ const aidMatch = bootstrap.match(/aid:(\d+)/);
2570
+ return {
2571
+ acrawlerSource,
2572
+ aid: aidMatch ? Number(aidMatch[1]) : AID_FALLBACK
2573
+ };
2574
+ };
2575
+ /**
2576
+ * 执行 acrawler 本体,返回对任意 nonce 的离线签名函数。
2577
+ *
2578
+ * shim 只需要 SDK 引用到的环境面:`document.cookie`(nonce 从这里读回,给空串让
2579
+ * 签名只依赖入参)、location / navigator / screen / history 与基础内置对象。环境
2580
+ * 数据不参与服务端校验(实测假数据可通过),所以 shim 不追求逼真。
2581
+ * @param source - acrawler 本体源码
2582
+ * @param aid - 引导 aid
2583
+ * @param userAgent - 参与签名的 UA(与后续 API 请求保持一致)
2584
+ * @returns `sign('', nonce)` 的等价函数
2585
+ */
2586
+ const createAcrawlerSigner = (source, aid, userAgent) => {
2587
+ const doc = {};
2588
+ Object.defineProperty(doc, "cookie", {
2589
+ get: () => "",
2590
+ set: () => {}
2591
+ });
2592
+ const windowShim = {
2593
+ document: doc,
2594
+ location: {
2595
+ href: "https://www.douyin.com/",
2596
+ protocol: "https:",
2597
+ host: "www.douyin.com",
2598
+ hostname: "www.douyin.com",
2599
+ pathname: "/",
2600
+ search: "",
2601
+ hash: "",
2602
+ origin: "https://www.douyin.com",
2603
+ reload: () => {}
2604
+ },
2605
+ navigator: {
2606
+ userAgent,
2607
+ language: "zh-CN",
2608
+ platform: "Win32"
2609
+ },
2610
+ screen: {
2611
+ width: 2560,
2612
+ height: 1440
2613
+ },
2614
+ history: { length: 2 },
2615
+ setTimeout: () => 0,
2616
+ setInterval: () => 0,
2617
+ clearTimeout: () => {},
2618
+ clearInterval: () => {},
2619
+ console: {
2620
+ log: () => {},
2621
+ warn: () => {},
2622
+ error: () => {}
2623
+ },
2624
+ Object,
2625
+ Array,
2626
+ Symbol,
2627
+ Error,
2628
+ TypeError,
2629
+ JSON,
2630
+ Math,
2631
+ Date,
2632
+ Promise,
2633
+ Reflect,
2634
+ Map,
2635
+ Set,
2636
+ Number,
2637
+ String,
2638
+ Boolean,
2639
+ RegExp,
2640
+ Function,
2641
+ parseInt,
2642
+ parseFloat,
2643
+ isNaN,
2644
+ encodeURIComponent,
2645
+ decodeURIComponent,
2646
+ TextEncoder,
2647
+ URL,
2648
+ URLSearchParams,
2649
+ performance
2650
+ };
2651
+ windowShim.window = windowShim;
2652
+ windowShim.self = windowShim;
2653
+ windowShim.top = windowShim;
2654
+ windowShim.parent = windowShim;
2655
+ windowShim.globalThis = windowShim;
2656
+ const context = vm.createContext(windowShim);
2657
+ vm.runInContext(source, context, { timeout: 2e4 });
2658
+ const ac = windowShim.byted_acrawler;
2659
+ if (!ac?.sign) throw new Error("acrawler 本体未注册 byted_acrawler.sign");
2660
+ ac.init({
2661
+ aid,
2662
+ dfp: 0
2663
+ });
2664
+ return (nonce) => ac.sign("", nonce);
2665
+ };
2666
+ /**
2667
+ * 测试环境守卫:vitest 进程内默认出口**不做真实出网请求**(审计在测试里保持透明,
2668
+ * 断言 Cookie 透传的用例不该被一次真网铸造改写)。通过 {@link MintDeps.getHome}
2669
+ * 注入出口的用例不受影响——守卫只管默认出口。
2670
+ */
2671
+ const testEnvGuard = async () => {
2672
+ throw new Error("douyin visitor mint is disabled under vitest (inject MintDeps.getHome to test it)");
2673
+ };
2674
+ /**
2675
+ * 运营开关:`AMAGI_DISABLE_DOUYIN_MINT=1` 关闭自动铸造。
2676
+ *
2677
+ * 铸造是 amagi **自主发起**的出网请求(用户没有显式调用任何端点),出口受限、
2678
+ * 安全审计、或「cookie 由我自己全权管理」的环境需要一个明确的选择权。关闭后
2679
+ * cookie 保持原样,受保护端点按 `SIGNATURE_REFUSED` 的既有归因失败。注入
2680
+ * {@link MintDeps.getHome} 的用例不受影响——开关只管默认出口。
2681
+ */
2682
+ const mintDisabledByEnv = () => process.env.AMAGI_DISABLE_DOUYIN_MINT === "1";
2683
+ const defaultGetHome = async (cookie, userAgent) => {
2684
+ if (process.env.VITEST) return testEnvGuard();
2685
+ if (mintDisabledByEnv()) return testEnvGuard();
2686
+ const response = await axios.get("https://www.douyin.com/", {
2687
+ headers: {
2688
+ "User-Agent": userAgent,
2689
+ Accept: "text/html",
2690
+ ...cookie ? { Cookie: cookie } : {}
2691
+ },
2692
+ timeout: 15e3,
2693
+ maxRedirects: 5,
2694
+ responseType: "text"
2695
+ });
2696
+ const setCookies = response.headers["set-cookie"] ?? [];
2697
+ return {
2698
+ setCookies: Array.isArray(setCookies) ? setCookies : [setCookies],
2699
+ body: String(response.data ?? "")
2700
+ };
2701
+ };
2702
+ /** 从 Set-Cookie 行数组里取某个 cookie 的值 */
2703
+ const setCookieValue = (cookies, name) => {
2704
+ const line = cookies.find((c) => c.startsWith(name + "="));
2705
+ return line ? line.split(";")[0].split("=").slice(1).join("=") : void 0;
2706
+ };
2707
+ /**
2708
+ * 铸造一次访客 id(两次 GET + 离线签名)。不做缓存、不去重——那是
2709
+ * {@link ensureDouyinVisitorCookie} 的职责。
2710
+ * @param userAgent - 铸造请求的 UA(与后续 API 请求保持一致)
2711
+ * @param deps - 可注入的 HTTP 出口(测试用)
2712
+ * @returns 铸造结果;挑战页形状变化 / 签名被拒 / 网络失败时 `undefined`
2713
+ */
2714
+ const mintDouyinVisitorCookie = async (userAgent, deps) => {
2715
+ const ua = userAgent || DEFAULT_MINT_UA;
2716
+ const getHome = deps?.getHome ?? defaultGetHome;
2717
+ try {
2718
+ const first = await getHome(void 0, ua);
2719
+ const nonce = setCookieValue(first.setCookies, "__ac_nonce");
2720
+ if (!nonce) return void 0;
2721
+ const parts = extractChallengeParts(first.body);
2722
+ if (!parts) return void 0;
2723
+ const second = await getHome(`__ac_nonce=${nonce}; __ac_signature=${createAcrawlerSigner(parts.acrawlerSource, parts.aid, ua)(nonce)}`, ua);
2724
+ const ttwid = setCookieValue(second.setCookies, "ttwid");
2725
+ const uifidTemp = setCookieValue(second.setCookies, "UIFID_TEMP");
2726
+ if (!ttwid || !uifidTemp) return void 0;
2727
+ return {
2728
+ ttwid,
2729
+ uifidTemp
2730
+ };
2731
+ } catch {
2732
+ return;
2733
+ }
2734
+ };
2735
+ /**
2736
+ * 把铸造结果合并进现有 cookie。**只补缺失的名字**:`ttwid` 已有则不动(那是调用方
2737
+ * 的设备标识,铸造顺带产的那份与它会话无关),访客 id 已有任何拼写则原样返回。
2738
+ * @param cookie - 现有 cookie 串,可为空
2739
+ * @param minted - 铸造结果
2740
+ * @returns 合并后的 cookie 串;无可合并项时原样返回
2741
+ */
2742
+ const mergeVisitorCookie = (cookie, minted) => {
2743
+ const base = cookie?.trim() ? cookie.trim().replace(/;\s*$/, "") : "";
2744
+ const names = new Set(base.split(";").map((p) => p.split("=")[0].trim()).filter(Boolean));
2745
+ if (names.has("UIFID_TEMP") || names.has("UIFID") || names.has("uifid") || names.has("uifid_temp")) return base;
2746
+ const parts = [];
2747
+ if (!names.has("ttwid") && minted.ttwid) parts.push(`ttwid=${minted.ttwid}`);
2748
+ if (minted.uifidTemp) parts.push(`UIFID_TEMP=${minted.uifidTemp}`);
2749
+ if (parts.length === 0) return base;
2750
+ return base ? `${base}; ${parts.join("; ")}` : parts.join("; ");
2751
+ };
2752
+ /** 审计缓存:原始 cookie(+UA)→ 合并后 cookie。进程级,`UIFID_TEMP` 有效期约 500 天 */
2753
+ const cache = /* @__PURE__ */ new Map();
2754
+ /** in-flight 去重:并发调用共享同一次铸造(合并是按各自 cookie 进行的,结果正确) */
2755
+ let inflight;
2756
+ const CACHE_MAX_ENTRIES = 64;
2757
+ /**
2758
+ * cookie 审计:缺访客 id 时铸造并合并,其余原样返回。进程内缓存,同一 cookie 只铸一次。
2759
+ *
2760
+ * **永不为调用增加失败**:任何异常都吞掉并返回原 cookie,请求按原路径走——自愈失败
2761
+ * 的归因交给受保护端点自己的 `SIGNATURE_REFUSED`。
2762
+ * @param cookie - 本次调用的 cookie,可为空(全新部署连 cookie 都没配的场景)
2763
+ * @param userAgent - 本次调用的 UA(铸造请求用它,保证与后续 API 请求同源)
2764
+ * @param deps - 可注入的 HTTP 出口(测试用)
2765
+ * @returns 升级后的 cookie;无需升级或铸造失败时原样返回
2766
+ */
2767
+ const ensureDouyinVisitorCookie = async (cookie, userAgent, deps) => {
2768
+ if (cookie && extractUifidFromCookie(cookie)) return cookie;
2769
+ const cacheKey = `${cookie ?? ""} ${userAgent ?? ""}`;
2770
+ const hit = cache.get(cacheKey);
2771
+ if (hit !== void 0) return hit;
2772
+ if (inflight !== void 0) return inflight;
2773
+ inflight = (async () => {
2774
+ try {
2775
+ const minted = await mintDouyinVisitorCookie(userAgent, deps);
2776
+ if (!minted) return cookie;
2777
+ const merged = mergeVisitorCookie(cookie, minted);
2778
+ cache.set(cacheKey, merged);
2779
+ if (cache.size > CACHE_MAX_ENTRIES) {
2780
+ const oldest = cache.keys().next().value;
2781
+ if (oldest !== void 0) cache.delete(oldest);
2782
+ }
2783
+ return merged;
2784
+ } catch {
2785
+ return cookie;
2786
+ } finally {
2787
+ inflight = void 0;
2788
+ }
2789
+ })();
2790
+ return inflight;
2791
+ };
2792
+ //#endregion
2498
2793
  //#region src/platforms/douyin/sign/signers.ts
2499
2794
  /**
2500
2795
  * 抖音签名器表:**只做「注册名 → `SignStep` 清单」的映射,不含算法**。
@@ -2686,7 +2981,7 @@ const parseKuaishouCaptcha = (raw) => {
2686
2981
  /** 默认超时,与抖音 / B站 / 小红书一致 */
2687
2982
  const KUAISHOU_DEFAULT_TIMEOUT = 1e4;
2688
2983
  /** 从请求配置里取 UA:大小写不敏感,取不到用集中维护的默认值 */
2689
- const userAgentOf$1 = (requestConfig) => {
2984
+ const userAgentOf$2 = (requestConfig) => {
2690
2985
  return new AmagiHeaders(requestConfig?.headers).get("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";
2691
2986
  };
2692
2987
  /**
@@ -2698,7 +2993,7 @@ const userAgentOf$1 = (requestConfig) => {
2698
2993
  * @returns 默认 header 基线 + 请求配置
2699
2994
  */
2700
2995
  const createKuaishouConfig = (cookie, requestConfig) => {
2701
- const userAgent = userAgentOf$1(requestConfig);
2996
+ const userAgent = userAgentOf$2(requestConfig);
2702
2997
  const secChUa = generateSecChUa(userAgent);
2703
2998
  return {
2704
2999
  headers: new AmagiHeaders().set("accept", "application/json, text/plain, */*").set("accept-encoding", "gzip, deflate, br, zstd").set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6").set("content-type", "application/json").set("priority", "u=0, i").set("origin", "https://www.kuaishou.com").set("referer", "https://www.kuaishou.com/new-reco").set("sec-ch-ua", secChUa).set("sec-ch-ua-mobile", "?0").set("sec-ch-ua-platform", "\"Windows\"").set("sec-fetch-dest", "empty").set("sec-fetch-mode", "cors").set("sec-fetch-site", "same-site").set("user-agent", userAgent).merge(requestConfig?.headers),
@@ -2936,7 +3231,7 @@ const createKuaishouSigners = (signer = sharedKuaishouSigner) => ({ hxfalcon: st
2936
3231
  /** 默认超时,与抖音 / B站 / 快手一致 */
2937
3232
  const XIAOHONGSHU_DEFAULT_TIMEOUT = 1e4;
2938
3233
  /** 从请求配置里取 UA:大小写不敏感,取不到用集中维护的默认值 */
2939
- const userAgentOf = (requestConfig) => {
3234
+ const userAgentOf$1 = (requestConfig) => {
2940
3235
  return new AmagiHeaders(requestConfig?.headers).get("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";
2941
3236
  };
2942
3237
  /**
@@ -2946,7 +3241,7 @@ const userAgentOf = (requestConfig) => {
2946
3241
  * @returns 默认 header 基线 + 请求配置
2947
3242
  */
2948
3243
  const createXiaohongshuConfig = (cookie, requestConfig) => {
2949
- const userAgent = userAgentOf(requestConfig);
3244
+ const userAgent = userAgentOf$1(requestConfig);
2950
3245
  const secChUa = generateSecChUa(userAgent);
2951
3246
  return {
2952
3247
  headers: new AmagiHeaders().set("accept", "application/json, text/plain, */*").set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6").set("cache-control", "no-cache").set("content-type", "application/json;charset=UTF-8").set("pragma", "no-cache").set("priority", "u=1, i").set("referer", "https://www.xiaohongshu.com/").set("sec-ch-ua", secChUa).set("sec-ch-ua-mobile", "?0").set("sec-ch-ua-platform", "\"Windows\"").set("sec-fetch-dest", "empty").set("sec-fetch-mode", "cors").set("sec-fetch-site", "same-site").set("user-agent", userAgent).set("cookie", cookie?.trim() ?? "").merge(requestConfig?.headers),
@@ -3651,7 +3946,7 @@ var HttpClient = class {
3651
3946
  //#endregion
3652
3947
  //#region src/client/runtime.ts
3653
3948
  /**
3654
- * 平台运行期依赖表:签名器表 + 默认 judge + 可选的风控挑战提取器 / 响应旁观者。
3949
+ * 平台运行期依赖表:签名器表 + 默认 judge + 可选的风控挑战提取器 / 响应旁观者 / cookie 审计。
3655
3950
  *
3656
3951
  * 四个平台都必须 `signers` / `judge` 齐全 —— 少一项不会报编译错误,请求会
3657
3952
  * 不签名、或业务失败被判成成功。
@@ -3663,6 +3958,11 @@ var HttpClient = class {
3663
3958
  * `observe` 是**可选的第四项**:只有「服务端把状态写在响应头里」的平台才装
3664
3959
  * (目前只有抖音,用它回收 `webid`)。每次 send 之后调用一次,只读、不影响判定。
3665
3960
  *
3961
+ * `ensureCookie` 是**可选的第五项**:只有「cookie 里有平台自己才认得的必填身份」的
3962
+ * 平台才装(目前只有抖音,缺访客 id 时自动铸造并合并)。每次调用在 prepare 之后、
3963
+ * build/sign 之前执行一次,返回升级后的 cookie;失败/无需升级原样返回——审计
3964
+ * **永不为调用增加失败**。
3965
+ *
3666
3966
  * createClient 的 fetcher、四个 `createXxxRoutes` 与静态 fetcher 共用这张表,
3667
3967
  * 保证同一平台在任何入口下的签名 / 判定 / 风控行为一致。
3668
3968
  */
@@ -3679,7 +3979,8 @@ const PLATFORM_RUNTIME = {
3679
3979
  douyin: {
3680
3980
  signers: createDouyinSigners(),
3681
3981
  judge: douyinJudge,
3682
- observe: observeDouyinWebid
3982
+ observe: observeDouyinWebid,
3983
+ ensureCookie: ensureDouyinVisitorCookie
3683
3984
  },
3684
3985
  bilibili: {
3685
3986
  signers: (() => {
@@ -3744,6 +4045,7 @@ const makeClientCtx = (platform, cookie, requestConfig = {}, clientId = "shared"
3744
4045
  judge: runtime.judge,
3745
4046
  ...runtime.challenge === void 0 ? {} : { challenge: runtime.challenge },
3746
4047
  ...runtime.observe === void 0 ? {} : { observe: runtime.observe },
4048
+ ...runtime.ensureCookie === void 0 ? {} : { ensureCookie: runtime.ensureCookie },
3747
4049
  ...bus === void 0 ? {} : { bus },
3748
4050
  ...debug === void 0 ? {} : { debug },
3749
4051
  send: (spec, reason, perCall) => http.send(spec, reason, perCall),
@@ -4278,6 +4580,10 @@ const execute = async (def, input, options) => {
4278
4580
  ...baseCtx,
4279
4581
  send: boundSend
4280
4582
  };
4583
+ if (options.ensureCookie) {
4584
+ const upgraded = await options.ensureCookie(ctx.cookie, ctx.userAgent);
4585
+ if (upgraded !== void 0 && upgraded !== ctx.cookie) ctx.cookie = upgraded;
4586
+ }
4281
4587
  stage = "build";
4282
4588
  if (!def.build) throw new Error(`端点 ${def.name} 既没有 build 也没有 compute`);
4283
4589
  stage = "sign";
@@ -4601,6 +4907,7 @@ const callEndpoint = (def, ctx, options, requestConfig) => {
4601
4907
  judge: ctx.judge,
4602
4908
  challenge: ctx.challenge,
4603
4909
  observe: ctx.observe,
4910
+ ensureCookie: ctx.ensureCookie,
4604
4911
  bus: ctx.bus,
4605
4912
  trace: tracer,
4606
4913
  debug: ctx.debug,
@@ -7449,6 +7756,11 @@ const buildVerifyBody = (verify, verifyWay, tail) => ({
7449
7756
  });
7450
7757
  //#endregion
7451
7758
  //#region src/platforms/douyin/session/qrcode.ts
7759
+ /** 从请求配置的 headers 里尽量取出 UA(大小写不敏感;取不到返回 `undefined`,铸造用兜底 UA) */
7760
+ const userAgentOf = (headers) => {
7761
+ if (!headers || typeof headers !== "object") return void 0;
7762
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === "user-agent" && typeof value === "string") return value;
7763
+ };
7452
7764
  /**
7453
7765
  * 抖音扫码登录策略。
7454
7766
  *
@@ -7574,7 +7886,7 @@ const douyinQrcodeStrategy = {
7574
7886
  };
7575
7887
  case "confirmed": {
7576
7888
  if (result.redirectUrl) await client.followSsoRedirect(result.redirectUrl);
7577
- const cookie = client.cookies.toString();
7889
+ const cookie = await ensureDouyinVisitorCookie(client.cookies.toString(), userAgentOf(ctx.requestConfig?.headers)) ?? client.cookies.toString();
7578
7890
  return {
7579
7891
  ok: true,
7580
7892
  state: {
@@ -51623,7 +51935,7 @@ const buildOpenApiSpec = (options = {}) => {
51623
51935
  openapi: "3.1.0",
51624
51936
  info: {
51625
51937
  title: "amagi HTTP API",
51626
- version: options.version ?? "7.0.0-beta.7",
51938
+ version: options.version ?? "7.0.0-beta.8",
51627
51939
  description: [
51628
51940
  "由端点注册表派生的规范 —— 手写的那份已经漂移,这份不会(`gen-openapi --check` 进 CI)。",
51629
51941
  "",
@@ -54393,7 +54705,7 @@ let DynamicType = /* @__PURE__ */ function(DynamicType) {
54393
54705
  * 构建后使用 __VERSION__,开发环境从 package.json 读取
54394
54706
  */
54395
54707
  const getVersion = () => {
54396
- return "7.0.0-beta.7";
54708
+ return "7.0.0-beta.8";
54397
54709
  };
54398
54710
  const VERSION = getVersion();
54399
54711
  /**
@@ -54444,4 +54756,4 @@ const amagi = Client;
54444
54756
  * GPL-3.0 Licensed
54445
54757
  */
54446
54758
  //#endregion
54447
- export { unwrap as $, douyinFetcher as A, xiaohongshuSign as B, ValidationError as C, parseDmSegMobileReply as Ct, createBoundKuaishouFetcher as D, xiaohongshuFetcher as E, bilibiliFetcher as F, createDouyinRoutes as G, createKuaishouRoutes as H, createBoundBilibiliFetcher as I, bilibiliUtils as J, passport_exports as K, createClient as L, requestPassportQrcode as M, sendPassportVerifyCode as N, kuaishouFetcher as O, validatePassportVerifyCode as P, isSuccess as Q, xiaohongshuUtils as R, ApiError as S, emitNetworkRetry as St, createBoundXiaohongshuFetcher as T, bv2av as Tt, kuaishouSign as U, kuaishouUtils as V, douyinUtils as W, AmagiThrownError as X, createBilibiliRoutes as Y, isFailure as Z, validateBilibiliParams as _, emitLogError as _t, AdditionalType as a, wbi_sign as at, validateXiaohongshuParams as b, emitLogWarn as bt, createAmagiClient as c, fetchResponse as ct, assertValidBilibiliParams as d, emitApiError as dt, AMAGI_BUS_EVENT_NAMES as et, assertValidDouyinParams as f, emitApiSuccess as ft, createSuccessResponse as g, emitLogDebug as gt, createErrorResponse as h, emitLog as ht, DynamicType as i, qtparam as it, checkPassportQrcode as j, createBoundDouyinFetcher as k, kuaishouApiUrls as l, isNetworkErrorResult as lt, assertValidXiaohongshuParams as m, emitHttpResponse as mt, CreateApp as n, KUAISHOU_PC_CAPTCHA_RESULT as nt, MajorType as o, bilibiliApiUrls as ot, assertValidKuaishouParams as p, emitHttpRequest as pt, douyinSign as q, amagi as r, parseKuaishouCaptcha as rt, CommentType as s, fetchData as st, Client as t, KUAISHOU_H5_CAPTCHA_RESULT as tt, douyinApiUrls as u, amagiEvents as ut, validateDouyinParams as v, emitLogInfo as vt, handleError as w, av2bv as wt, xiaohongshuApiUrls as x, emitNetworkError as xt, validateKuaishouParams as y, emitLogMark as yt, createXiaohongshuRoutes as z };
54759
+ export { unwrap as $, douyinFetcher as A, xiaohongshuSign as B, ValidationError as C, parseDmSegMobileReply as Ct, createBoundKuaishouFetcher as D, xiaohongshuFetcher as E, bilibiliFetcher as F, createDouyinRoutes as G, createKuaishouRoutes as H, createBoundBilibiliFetcher as I, bilibiliUtils as J, passport_exports as K, createClient as L, requestPassportQrcode as M, sendPassportVerifyCode as N, kuaishouFetcher as O, validatePassportVerifyCode as P, isSuccess as Q, xiaohongshuUtils as R, ApiError as S, emitNetworkRetry as St, createBoundXiaohongshuFetcher as T, kuaishouSign as U, kuaishouUtils as V, douyinUtils as W, AmagiThrownError as X, createBilibiliRoutes as Y, isFailure as Z, validateBilibiliParams as _, emitLogError as _t, AdditionalType as a, wbi_sign as at, validateXiaohongshuParams as b, emitLogWarn as bt, createAmagiClient as c, fetchResponse as ct, assertValidBilibiliParams as d, emitApiError as dt, AMAGI_BUS_EVENT_NAMES as et, assertValidDouyinParams as f, emitApiSuccess as ft, createSuccessResponse as g, emitLogDebug as gt, createErrorResponse as h, emitLog as ht, DynamicType as i, qtparam as it, checkPassportQrcode as j, createBoundDouyinFetcher as k, kuaishouApiUrls as l, isNetworkErrorResult as lt, assertValidXiaohongshuParams as m, emitHttpResponse as mt, CreateApp as n, KUAISHOU_PC_CAPTCHA_RESULT as nt, MajorType as o, bilibiliApiUrls as ot, assertValidKuaishouParams as p, emitHttpRequest as pt, douyinSign as q, amagi as r, parseKuaishouCaptcha as rt, CommentType as s, fetchData as st, Client as t, KUAISHOU_H5_CAPTCHA_RESULT as tt, douyinApiUrls as u, amagiEvents as ut, validateDouyinParams as v, emitLogInfo as vt, handleError as w, xiaohongshuApiUrls as x, emitNetworkError as xt, validateKuaishouParams as y, emitLogMark as yt, createXiaohongshuRoutes as z };