karin-plugin-kkk 2.42.4 → 2.42.5

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.
@@ -19,7 +19,6 @@ import { generateSync, scanSync } from "@ikenxuan/qrcode";
19
19
  import { Transform } from "node:stream";
20
20
  import { pipeline } from "node:stream/promises";
21
21
  import { inflateSync } from "node:zlib";
22
- import { embedWatermarkToPngBytes } from "@ikenxuan/watermark";
23
22
  import _ from "node-karin/lodash";
24
23
  //#region src/index.ts
25
24
  globalThis.__kkkLoadStart ??= process.hrtime.bigint();
@@ -750,6 +749,25 @@ var DouyinDanmakuParamsSchema = zod_default.object({
750
749
  error: "获取弹幕区间的开始时间必须小于结束时间",
751
750
  path: ["start_time"]
752
751
  });
752
+ /** 游客用户参数验证 */
753
+ var DouyinGuestUserParamsSchema = zod_default.object({
754
+ methodType: zod_default.literal("guestUserInfo", { error: "方法类型必须是\"guestUserInfo\"" }),
755
+ unique_id: zod_default.string({ error: "抖音号必须是字符串" }).min(1, { error: "抖音号不能为空" })
756
+ });
757
+ /** 游客原声参数验证 */
758
+ var DouyinGuestMusicParamsSchema = zod_default.object({
759
+ methodType: zod_default.literal("guestMusicInfo", { error: "方法类型必须是\"guestMusicInfo\"" }),
760
+ music_id: zod_default.string({ error: "音乐ID必须是字符串" }).min(1, { error: "音乐ID不能为空" })
761
+ });
762
+ /** 游客原声作品列表参数验证 */
763
+ var DouyinGuestMusicListParamsSchema = zod_default.object({
764
+ methodType: zod_default.literal("guestMusicAwemeList", { error: "方法类型必须是\"guestMusicAwemeList\"" }),
765
+ music_id: zod_default.string({ error: "音乐ID必须是字符串" }).min(1, { error: "音乐ID不能为空" }),
766
+ number: smartPositiveInteger("作品数量必须是正整数").optional().default(10),
767
+ cursor: zod_default.coerce.number({ error: "游标必须是数字" }).int({ error: "游标必须是整数" }).min(0, { error: "游标不能小于0" }).default(0).optional()
768
+ });
769
+ /** 表情资源包参数验证(该接口不接受任何参数) */
770
+ var DouyinEmojiResourceParamsSchema = zod_default.object({ methodType: zod_default.literal("emojiResourceMeta", { error: "方法类型必须是\"emojiResourceMeta\"" }) });
753
771
  /** 抖音参数验证模式映射 */
754
772
  var DouyinValidationSchemas = {
755
773
  textWork: DouyinWorkParamsSchema,
@@ -770,7 +788,11 @@ var DouyinValidationSchemas = {
770
788
  emojiList: DouyinEmojiListParamsSchema,
771
789
  dynamicEmojiList: DouyinEmojiProParamsSchema,
772
790
  commentReplies: DouyinCommentReplyParamsSchema,
773
- danmakuList: DouyinDanmakuParamsSchema
791
+ danmakuList: DouyinDanmakuParamsSchema,
792
+ guestUserInfo: DouyinGuestUserParamsSchema,
793
+ guestMusicInfo: DouyinGuestMusicParamsSchema,
794
+ guestMusicAwemeList: DouyinGuestMusicListParamsSchema,
795
+ emojiResourceMeta: DouyinEmojiResourceParamsSchema
774
796
  };
775
797
  /** 抖音方法路由映射 */
776
798
  var DouyinMethodRoutes = {
@@ -792,7 +814,11 @@ var DouyinMethodRoutes = {
792
814
  dynamicEmojiList: "/fetch_emoji_pro_list",
793
815
  liveRoomInfo: "/fetch_user_live_videos",
794
816
  danmakuList: "/fetch_work_danmaku",
795
- loginQrcode: "/fetch_login_qrcode"
817
+ loginQrcode: "/fetch_login_qrcode",
818
+ guestUserInfo: "/fetch_guest_user_info",
819
+ guestMusicInfo: "/fetch_guest_music_info",
820
+ guestMusicAwemeList: "/fetch_guest_music_aweme_list",
821
+ emojiResourceMeta: "/fetch_emoji_resource_meta"
796
822
  };
797
823
  //#endregion
798
824
  //#region ../amagi/packages/core/src/validation/kuaishou.ts
@@ -4532,6 +4558,229 @@ var a_bogus_default = (url, user_agent) => {
4532
4558
  return result_encrypt(generate_random_str() + generate_rc4_bb_str(new URLSearchParams(new URL(url).search).toString(), cleanedUserAgent, "1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32"), "s4") + "=";
4533
4559
  };
4534
4560
  //#endregion
4561
+ //#region ../amagi/packages/core/src/platform/douyin/sign/secsdkWebSign.ts
4562
+ /**
4563
+ * 抖音主站 `x-secsdk-web-signature` 纯算实现(TypeScript 移植)
4564
+ *
4565
+ * 移植自 cv-cat/DouYin_Spider `utils/secsdk_web_sign.py`,算法与常量按该实现逐条对齐。
4566
+ *
4567
+ * ## 算法
4568
+ *
4569
+ * ```text
4570
+ * plain = {uifid}_{timestamp}_{WEBSIGN_CONST}_{canonical_query}
4571
+ * signature = md5(plain) 的 32 位小写十六进制
4572
+ * ```
4573
+ *
4574
+ * `WEBSIGN_CONST` 是 secsdk VMP 常量池里的写死盐,与账号 / 会话无关。
4575
+ *
4576
+ * ## canonical_query 规范化规则
4577
+ *
4578
+ * - 保持原始参数顺序,**不排序**;重复参数原样保留。
4579
+ * - 每个 value 先解码(`+`→空格、`%XX`→字节)再用 `encodeURIComponent` 重编码。
4580
+ * - key **只解码、不重编码**。
4581
+ * - 无等号的裸参数补成 `k=`。
4582
+ * - `&timestamp={ts}` 追加到 query 末尾后再参与哈希。
4583
+ * - query 里没有 `uifid` 时,从 `UIFID` cookie 取值追加到末尾再签。
4584
+ *
4585
+ * ## 重要
4586
+ *
4587
+ * 签名算的是**规范化后的 query**,服务端也按收到的 query 校验,所以必须发送
4588
+ * {@link signSecsdkWebUrl} 返回的 URL,不能拿原始 URL 直接发。
4589
+ *
4590
+ * 签名绑 query、**不绑 path**(本项目实测:同一份 query 换 path 仍然 200)。
4591
+ *
4592
+ * @module platform/douyin/secsdkWebSign
4593
+ */
4594
+ /** secsdk VM 常量池里的固定盐(与账号 / 会话无关) */
4595
+ var WEBSIGN_CONST = "A96D855A08C0A9707F8BEF0D9A527E4E";
4596
+ /** 签名参数名 */
4597
+ var SECSDK_SIG_KEY = "x-secsdk-web-signature";
4598
+ /** 时间戳参数名 */
4599
+ var SECSDK_TS_KEY = "timestamp";
4600
+ /** 走 secsdk 签名的 GET 接口路径(来自 SDK 的 webSign 策略配置) */
4601
+ var PROTECTED_PATHS_GET = [
4602
+ "/aweme/v1/web/aweme/detail/",
4603
+ "/aweme/v1/web/aweme/post/",
4604
+ "/aweme/v1/web/aweme/favorite/",
4605
+ "/aweme/v1/web/aweme/listcollection/",
4606
+ "/aweme/v1/web/mix/aweme/",
4607
+ "/aweme/v1/web/tab/feed/",
4608
+ "/aweme/v1/web/mix/list/",
4609
+ "/aweme/v1/web/music/aweme/",
4610
+ "/aweme/v1/web/music/list/",
4611
+ "/aweme/v1/web/mix/detail/",
4612
+ "/aweme/v1/web/mix/listcollection/",
4613
+ "/aweme/v1/web/music/detail/",
4614
+ "/aweme/v1/web/collects/list/",
4615
+ "/aweme/v1/web/collects/video/list/"
4616
+ ];
4617
+ /** 走 secsdk 签名的 POST 接口路径 */
4618
+ var PROTECTED_PATHS_POST = [
4619
+ "/aweme/v1/web/aweme/detail/",
4620
+ "/aweme/v1/web/aweme/post/",
4621
+ "/aweme/v1/web/aweme/favorite/",
4622
+ "/aweme/v1/web/aweme/listcollection/",
4623
+ "/aweme/v1/web/mix/aweme/",
4624
+ "/aweme/v1/web/tab/feed/"
4625
+ ];
4626
+ var HEX_PAIR = /^[0-9A-Fa-f]{2}$/;
4627
+ /**
4628
+ * 宽容的百分号解码,等价于 Python `urllib.parse.unquote(..., errors='replace')`:
4629
+ * 合法的 `%XX` 解成字节,非法的 `%` 原样保留,最后按 UTF-8 解码(坏字节 → U+FFFD)。
4630
+ *
4631
+ * 不能直接用 `decodeURIComponent` —— 它遇到 `100%` 这种孤立百分号会抛异常,
4632
+ * 而 SDK 的行为是把它当字面量留下(`100%` → 签成 `100%25`)。
4633
+ *
4634
+ * @param input - 待解码字符串
4635
+ * @returns 解码后的字符串
4636
+ */
4637
+ var percentDecodeLoose = (input) => {
4638
+ if (!input.includes("%")) return input;
4639
+ const bytes = [];
4640
+ let i = 0;
4641
+ while (i < input.length) {
4642
+ if (input[i] === "%" && HEX_PAIR.test(input.slice(i + 1, i + 3))) {
4643
+ bytes.push(parseInt(input.slice(i + 1, i + 3), 16));
4644
+ i += 3;
4645
+ continue;
4646
+ }
4647
+ for (const byte of Buffer.from(input[i], "utf8")) bytes.push(byte);
4648
+ i += 1;
4649
+ }
4650
+ return Buffer.from(bytes).toString("utf8");
4651
+ };
4652
+ /**
4653
+ * 等价于 Python `unquote_plus`:先把 `+` 当空格,再做百分号解码。
4654
+ *
4655
+ * @param input - 待解码字符串
4656
+ * @returns 解码后的字符串
4657
+ */
4658
+ var unquotePlusLoose = (input) => percentDecodeLoose(input.includes("+") ? input.replace(/\+/g, " ") : input);
4659
+ /**
4660
+ * 按 SDK 规则规范化 query 字符串:顺序不变,value 解码后用 `encodeURIComponent` 重编码,
4661
+ * key 只解码不重编码,无等号的裸参数补成 `k=`。
4662
+ *
4663
+ * @param query - 原始 query 字符串(不含 `?`)
4664
+ * @returns 规范化后的 query 字符串
4665
+ */
4666
+ var canonicalQuery = (query) => {
4667
+ const parts = [];
4668
+ for (const pair of query.split("&")) {
4669
+ if (!pair) continue;
4670
+ const eq = pair.indexOf("=");
4671
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
4672
+ const rawValue = eq === -1 ? "" : pair.slice(eq + 1);
4673
+ parts.push(`${unquotePlusLoose(rawKey)}=${encodeURIComponent(unquotePlusLoose(rawValue))}`);
4674
+ }
4675
+ return parts.join("&");
4676
+ };
4677
+ /**
4678
+ * 拆成 `[base, query]`,并剥掉已有的 `timestamp` / 签名字段,保证可重复调用。
4679
+ *
4680
+ * @param url - 完整 URL
4681
+ * @returns `[base, query]` 二元组
4682
+ */
4683
+ var splitUrl = (url) => {
4684
+ const mark = url.indexOf("?");
4685
+ if (mark === -1) return [url, ""];
4686
+ return [url.slice(0, mark), url.slice(mark + 1).split("&").filter((pair) => {
4687
+ if (!pair) return false;
4688
+ const name = pair.split("=", 1)[0];
4689
+ return name !== "timestamp" && name !== "x-secsdk-web-signature";
4690
+ }).join("&")];
4691
+ };
4692
+ /**
4693
+ * 从 cookie 字符串里取 `UIFID`。
4694
+ *
4695
+ * 注意 cookie 里通常还有个 `UIFID_TEMP`,取值时必须锚定 `UIFID=` 前面是行首或分隔符,
4696
+ * 否则会取错。
4697
+ *
4698
+ * @param cookie - cookie 字符串
4699
+ * @returns `UIFID` 的值,取不到时返回空串
4700
+ */
4701
+ var extractUifidFromCookie = (cookie) => {
4702
+ if (!cookie) return "";
4703
+ const hit = /(?:^|;\s*)UIFID=([^;]*)/.exec(cookie);
4704
+ return hit ? hit[1].trim() : "";
4705
+ };
4706
+ /**
4707
+ * 判断该 path 是否需要 secsdk 加签(依据 SDK 的 webSign 策略配置)。
4708
+ *
4709
+ * @param path - URL 的 pathname
4710
+ * @param method - HTTP 方法,默认 `GET`
4711
+ * @returns 是否需要加签
4712
+ */
4713
+ var isSecsdkProtected = (path, method = "GET") => {
4714
+ return (String(method).toUpperCase() === "POST" ? PROTECTED_PATHS_POST : PROTECTED_PATHS_GET).includes(path);
4715
+ };
4716
+ /**
4717
+ * 计算 `x-secsdk-web-signature`。
4718
+ *
4719
+ * @param url - 完整 API URL(scheme+host+path+query)。已带 `timestamp` /
4720
+ * `x-secsdk-web-signature` 时会自动剥除,可重复调用。
4721
+ * @param options - 时间戳与 uifid 兜底值
4722
+ * @returns 时间戳、签名与**实际应当发送**的规范化 query
4723
+ */
4724
+ var signSecsdkWebQuery = (url, options = {}) => {
4725
+ const ts = Math.trunc(options.ts ?? Date.now() / 1e3);
4726
+ const [, rawQuery] = splitUrl(url);
4727
+ let canon = canonicalQuery(rawQuery);
4728
+ if (!canon.split("&").filter(Boolean).map((pair) => pair.split("=", 1)[0]).includes("uifid") && options.uifid) {
4729
+ const appended = `uifid=${encodeURIComponent(options.uifid)}`;
4730
+ canon = canon ? `${canon}&${appended}` : appended;
4731
+ }
4732
+ const signedQuery = canon ? `${canon}&${SECSDK_TS_KEY}=${ts}` : `${SECSDK_TS_KEY}=${ts}`;
4733
+ let uifidValue = "";
4734
+ for (const pair of signedQuery.split("&")) if (pair.startsWith("uifid=")) {
4735
+ uifidValue = unquotePlusLoose(pair.slice(6));
4736
+ break;
4737
+ }
4738
+ if (!uifidValue && options.uifid) uifidValue = options.uifid;
4739
+ const plain = `${uifidValue}_${ts}_${WEBSIGN_CONST}_${signedQuery}`;
4740
+ return {
4741
+ ts,
4742
+ signature: crypto.createHash("md5").update(plain, "utf8").digest("hex"),
4743
+ signedQuery
4744
+ };
4745
+ };
4746
+ /**
4747
+ * 返回可直接发送的完整 URL(规范化 query + timestamp + 签名)。
4748
+ *
4749
+ * 服务端按收到的 query 校验签名,所以必须发这个返回值,不能拿原始 URL 直接发。
4750
+ *
4751
+ * @param url - 完整 API URL
4752
+ * @param options - 时间戳与 uifid 兜底值
4753
+ * @returns 带签名的完整 URL
4754
+ */
4755
+ var signSecsdkWebUrl = (url, options = {}) => {
4756
+ const [base] = splitUrl(url);
4757
+ const { signature, signedQuery } = signSecsdkWebQuery(url, options);
4758
+ return `${base}?${signedQuery}&${SECSDK_SIG_KEY}=${signature}`;
4759
+ };
4760
+ /**
4761
+ * 对受保护接口补上 secsdk 签名;path 不在策略表里时原样返回。
4762
+ *
4763
+ * 这是给取数层用的唯一入口 —— 无条件套在 URL 上即可,不需要调用方判断端点。
4764
+ *
4765
+ * @param url - 完整 API URL(应当是**最后一步**,即 a_bogus 等参数都已拼好之后)
4766
+ * @param options - cookie / uifid / method / ts
4767
+ * @returns 需要加签的返回带签名的 URL,否则原样返回
4768
+ */
4769
+ var applySecsdkWebSign = (url, options = {}) => {
4770
+ let pathname;
4771
+ try {
4772
+ pathname = new URL(url).pathname;
4773
+ } catch {
4774
+ return url;
4775
+ }
4776
+ if (!isSecsdkProtected(pathname, options.method)) return url;
4777
+ const uifid = options.uifid || extractUifidFromCookie(options.cookie);
4778
+ return signSecsdkWebUrl(url, {
4779
+ ts: options.ts,
4780
+ uifid
4781
+ });
4782
+ };
4783
+ //#endregion
4535
4784
  //#region ../amagi/packages/core/src/platform/douyin/sign/x_bogus.ts
4536
4785
  /**
4537
4786
  * X-Bogus 生成工具(TikTok/Douyin 签名算法)- TypeScript 版本
@@ -4723,6 +4972,20 @@ var douyinSign = class {
4723
4972
  static XB(url, userAgent) {
4724
4973
  return new XBogus().getXBogus(url, userAgent ?? defaultUserAgent).xbogus;
4725
4974
  }
4975
+ /**
4976
+ * `x-secsdk-web-signature` 签名算法
4977
+ *
4978
+ * 与 `AB` / `XB` 不同,它改写整条 URL 而不是返回一个参数值:签名算的是规范化后的 query,
4979
+ * 服务端也按收到的 query 校验,所以必须发送返回的这条 URL。只对 SDK 策略表内的 path 生效,
4980
+ * 其余原样返回,因此可以无条件套用。必须是最后一步(webid → a_bogus → 本签名)。
4981
+ *
4982
+ * @param url 已拼好全部参数(含 a_bogus)的完整地址
4983
+ * @param options `uifid`(query 缺失时从 cookie 取)、`cookie`、`method`、`ts`
4984
+ * @returns 需要加签时返回带签名的完整 URL,否则原样返回
4985
+ */
4986
+ static SecSdk(url, options = {}) {
4987
+ return applySecsdkWebSign(url, options);
4988
+ }
4726
4989
  /** 生成一个唯一的验证字符串 */
4727
4990
  static VerifyFpManager() {
4728
4991
  const e = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
@@ -4767,6 +5030,10 @@ var buildQueryString = (params) => {
4767
5030
  return Object.entries(params).filter(([_, value]) => value !== void 0 && value !== null).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
4768
5031
  };
4769
5032
  var fp = douyinSign.VerifyFpManager();
5033
+ /** iesdouyin v2 游客接口前缀:不需要 cookie、不需要签名 */
5034
+ var GUEST_BASE = "https://www.iesdouyin.com/web/api/v2";
5035
+ /** 表情资源包元信息,走 App 接口,同样免鉴权(需要 Android UA,见 getdata.ts) */
5036
+ var EMOJI_RESOURCE_URL = "https://api.amemv.com/aweme/v1/im/resources/emoji/?device_platform=android&version_name=17.4.0";
4770
5037
  /**
4771
5038
  * 抖音 API URL 构建类
4772
5039
  *
@@ -4823,8 +5090,7 @@ var DouyinAPI = class {
4823
5090
  version_name: "19.5.0",
4824
5091
  screen_width: "2328",
4825
5092
  screen_height: "1310",
4826
- round_trip_time: "150",
4827
- webid: "7351848354471872041"
5093
+ round_trip_time: "150"
4828
5094
  })}`;
4829
5095
  }
4830
5096
  /** 获取评论数据 */
@@ -4883,7 +5149,6 @@ var DouyinAPI = class {
4883
5149
  downlink: "10",
4884
5150
  effective_type: "4g",
4885
5151
  round_trip_time: "50",
4886
- webid: "7487210762873685515",
4887
5152
  verifyFp: fp,
4888
5153
  fp
4889
5154
  })}`;
@@ -4903,7 +5168,31 @@ var DouyinAPI = class {
4903
5168
  }
4904
5169
  /** 获取表情数据 */
4905
5170
  getEmojiList() {
4906
- return "https://www.douyin.com/aweme/v1/web/emoji/list";
5171
+ return `https://www.douyin.com/aweme/v1/web/emoji/list?${buildQueryString({
5172
+ ...this.getBaseParams(),
5173
+ /** 缺少该参数只会返回常规表情(214 个),带上才含限时、联名、节日表情(371 个) */
5174
+ need_all: "true"
5175
+ })}`;
5176
+ }
5177
+ /** 抖音号(unique_id)转用户信息,唯一免签名的 sec_uid 获取途径 */
5178
+ getGuestUserInfo(data) {
5179
+ return `${GUEST_BASE}/user/info/?${buildQueryString({ unique_id: data.unique_id })}`;
5180
+ }
5181
+ /** 获取原声本体,响应中没有 play_url,mp3 需从源作品上取 */
5182
+ getGuestMusicInfo(data) {
5183
+ return `${GUEST_BASE}/music/info/?${buildQueryString({ music_id: data.music_id })}`;
5184
+ }
5185
+ /** 获取使用某原声的作品列表,每条的 music 字段被抖音裁空,只能取 aweme_id */
5186
+ getGuestMusicAwemeList(data) {
5187
+ return `${GUEST_BASE}/music/list/aweme/?${buildQueryString({
5188
+ music_id: data.music_id,
5189
+ count: data.number ?? 10,
5190
+ cursor: data.cursor ?? 0
5191
+ })}`;
5192
+ }
5193
+ /** 获取表情资源包元信息 `{ id, md5, resource_url, update_time }`,md5 同时是版本号 */
5194
+ getEmojiResourceMeta() {
5195
+ return EMOJI_RESOURCE_URL;
4907
5196
  }
4908
5197
  /** 获取用户主页视频数据 */
4909
5198
  getUserVideoList(data) {
@@ -4923,8 +5212,7 @@ var DouyinAPI = class {
4923
5212
  version_name: "17.4.0",
4924
5213
  screen_width: "1552",
4925
5214
  screen_height: "970",
4926
- round_trip_time: "50",
4927
- webid: "7338423850134226495"
5215
+ round_trip_time: "50"
4928
5216
  })}`;
4929
5217
  }
4930
5218
  /** 获取用户喜欢列表数据 */
@@ -4946,8 +5234,7 @@ var DouyinAPI = class {
4946
5234
  version_name: "17.4.0",
4947
5235
  screen_width: "2328",
4948
5236
  screen_height: "1310",
4949
- round_trip_time: "0",
4950
- webid: "7487210762873685515"
5237
+ round_trip_time: "0"
4951
5238
  })}`;
4952
5239
  }
4953
5240
  /** 获取用户推荐列表数据 */
@@ -4987,7 +5274,6 @@ var DouyinAPI = class {
4987
5274
  downlink: "10",
4988
5275
  effective_type: "4g",
4989
5276
  round_trip_time: "50",
4990
- webid: "7487210762873685515",
4991
5277
  msToken: douyinSign.Mstoken(184),
4992
5278
  verifyFp: fp,
4993
5279
  fp
@@ -5005,8 +5291,7 @@ var DouyinAPI = class {
5005
5291
  version_name: "17.4.0",
5006
5292
  screen_width: "1552",
5007
5293
  screen_height: "970",
5008
- round_trip_time: "0",
5009
- webid: "7327957959955580467"
5294
+ round_trip_time: "0"
5010
5295
  })}`;
5011
5296
  }
5012
5297
  /** 获取热点词数据 */
@@ -5020,8 +5305,7 @@ var DouyinAPI = class {
5020
5305
  version_name: "17.4.0",
5021
5306
  screen_width: "1552",
5022
5307
  screen_height: "970",
5023
- round_trip_time: "50",
5024
- webid: "7327957959955580467"
5308
+ round_trip_time: "50"
5025
5309
  })}`;
5026
5310
  }
5027
5311
  /** 获取搜索数据 */
@@ -5050,7 +5334,6 @@ var DouyinAPI = class {
5050
5334
  support_h265: "1",
5051
5335
  version_code: "170400",
5052
5336
  version_name: "17.4.0",
5053
- webid: "7521399115230610959",
5054
5337
  ...data.search_id && { search_id: data.search_id }
5055
5338
  })}`;
5056
5339
  else if (searchType === "video") return `https://www.douyin.com/aweme/v1/web/search/item/?${buildQueryString({
@@ -5076,7 +5359,6 @@ var DouyinAPI = class {
5076
5359
  support_h265: "1",
5077
5360
  version_code: "170400",
5078
5361
  version_name: "17.4.0",
5079
- webid: "7521399115230610959",
5080
5362
  ...data.search_id && { search_id: data.search_id }
5081
5363
  })}`;
5082
5364
  else return `https://www.douyin.com/aweme/v1/web/general/search/stream/?${buildQueryString({
@@ -5100,8 +5382,7 @@ var DouyinAPI = class {
5100
5382
  support_dash: "1",
5101
5383
  support_h265: "1",
5102
5384
  version_code: "190600",
5103
- version_name: "19.6.0",
5104
- webid: "7521399115230610959"
5385
+ version_name: "19.6.0"
5105
5386
  })}`;
5106
5387
  }
5107
5388
  /** 获取互动表情数据 */
@@ -5112,7 +5393,8 @@ var DouyinAPI = class {
5112
5393
  channel: "channel_pc_web",
5113
5394
  publish_video_strategy_type: "2",
5114
5395
  app_id: "1128",
5115
- scenes: "[%22interactive_resources%22]",
5396
+ /** 原始 JSON 字符串,buildQueryString 会编码一次;预先编码过会变成 %2522,接口回「参数不合法」 */
5397
+ scenes: "[\"interactive_resources\"]",
5116
5398
  pc_client_type: "1",
5117
5399
  version_code: "170400",
5118
5400
  version_name: "17.4.0",
@@ -5134,7 +5416,6 @@ var DouyinAPI = class {
5134
5416
  downlink: "1.5",
5135
5417
  effective_type: "4g",
5136
5418
  round_trip_time: "350",
5137
- webid: "7347329698282833447",
5138
5419
  msToken: douyinSign.Mstoken(116),
5139
5420
  verifyFp: fp,
5140
5421
  fp
@@ -5169,7 +5450,6 @@ var DouyinAPI = class {
5169
5450
  downlink: "1.5",
5170
5451
  effective_type: "4g",
5171
5452
  round_trip_time: "350",
5172
- webid: "7347329698282833447",
5173
5453
  msToken: douyinSign.Mstoken(116),
5174
5454
  verifyFp: fp,
5175
5455
  fp
@@ -5234,7 +5514,6 @@ var DouyinAPI = class {
5234
5514
  engine_version: "140.0.0.0",
5235
5515
  downlink: "1.55",
5236
5516
  round_trip_time: "200",
5237
- webid: "7487210762873685515",
5238
5517
  msToken: douyinSign.Mstoken(116),
5239
5518
  verifyFp: fp,
5240
5519
  fp
@@ -5253,6 +5532,70 @@ var createDouyinApiUrls = (userAgent) => {
5253
5532
  /** 默认的 DouyinAPI 实例(使用默认浏览器版本 125.0.0.0) */
5254
5533
  var douyinApiUrls = new DouyinAPI();
5255
5534
  //#endregion
5535
+ //#region ../amagi/packages/core/src/platform/douyin/webid.ts
5536
+ /**
5537
+ * 抖音 `webid` 的取得与缓存。
5538
+ *
5539
+ * `webid` 是服务端下发的访客 id,客户端算不出来 —— 服务端按 `ttwid` 算好之后,
5540
+ * 通过每个响应的 `cookie_ttwidinfo_webid` 头告知。一个 ttwid 对应一个稳定的 webid。
5541
+ *
5542
+ * 抖音会拿 query 里的 `webid` 与 cookie 的会话交叉校验,对不上就返回 HTTP 200 + 空 body
5543
+ * (不是 403、也不是业务码),最典型的受害者是 `aweme/favorite`。因此缓存必须按 ttwid 分键:
5544
+ * 换了 cookie,webid 就得跟着换。取不到时不传 —— 不传是安全的,传错才致命。
5545
+ *
5546
+ * @module platform/douyin/webid
5547
+ */
5548
+ /** ttwid → webid。webid 对同一 ttwid 是稳定的,进程内缓存即可 */
5549
+ var cache = /* @__PURE__ */ new Map();
5550
+ /** 抖音回传 webid 的响应头 */
5551
+ var WEBID_HEADER = "cookie_ttwidinfo_webid";
5552
+ /**
5553
+ * 从 cookie 串里取 `ttwid`,用作缓存键
5554
+ *
5555
+ * @param cookie - cookie 串
5556
+ * @returns `ttwid` 的值,取不到时返回空串
5557
+ */
5558
+ var ttwidOf = (cookie) => {
5559
+ if (!cookie) return "";
5560
+ const hit = /(?:^|;\s*)ttwid=([^;]*)/.exec(cookie);
5561
+ return hit ? hit[1].trim() : "";
5562
+ };
5563
+ /**
5564
+ * 看到响应头就把 webid 记下来,对没有这个头的响应是无操作
5565
+ *
5566
+ * @param requestHeaders - 请求头,用于取 cookie 里的 ttwid 作为键
5567
+ * @param responseHeaders - 响应头
5568
+ */
5569
+ var rememberDouyinWebid = (requestHeaders, responseHeaders) => {
5570
+ const webid = responseHeaders?.[WEBID_HEADER];
5571
+ if (typeof webid !== "string" || !/^\d{6,}$/.test(webid)) return;
5572
+ const key = ttwidOf(requestHeaders?.Cookie ?? requestHeaders?.cookie);
5573
+ if (key) cache.set(key, webid);
5574
+ };
5575
+ /**
5576
+ * 取这份 cookie 对应的 webid
5577
+ *
5578
+ * @param cookie - cookie 串
5579
+ * @returns 缓存里的 webid,没有时返回空串
5580
+ */
5581
+ var douyinWebidFor = (cookie) => cache.get(ttwidOf(cookie)) ?? "";
5582
+ /**
5583
+ * 给 URL 补上 `webid`
5584
+ *
5585
+ * 必须在算 a_bogus 之前调用:真实浏览器的顺序是「拼好 query(含 webid)→ a_bogus → secsdk 签名」,
5586
+ * 后两者都覆盖 query,顺序错了签名就不成立。URL 里已有 `webid` 或缓存为空时原样返回。
5587
+ *
5588
+ * @param url - 还没加签的 URL
5589
+ * @param cookie - 本次请求使用的 cookie
5590
+ * @returns 补好 `webid` 的 URL
5591
+ */
5592
+ var withDouyinWebid = (url, cookie) => {
5593
+ if (url.includes("webid=")) return url;
5594
+ const webid = douyinWebidFor(cookie);
5595
+ if (!webid) return url;
5596
+ return `${url}${url.includes("?") ? "&" : "?"}webid=${webid}`;
5597
+ };
5598
+ //#endregion
5256
5599
  //#region ../amagi/packages/core/src/platform/douyin/getdata.ts
5257
5600
  /**
5258
5601
  * 抖音数据获取模块
@@ -5297,7 +5640,7 @@ var getSignParamName = (signType = "a_bogus") => {
5297
5640
  * @param userAgent - 用户代理
5298
5641
  * @returns 带签名的完整 URL
5299
5642
  */
5300
- var buildSignedUrl = (url, signType = "a_bogus", userAgent) => {
5643
+ var buildSignedUrlBase = (url, signType = "a_bogus", userAgent) => {
5301
5644
  const signature = getSignature(url, signType, userAgent);
5302
5645
  return `${url}&${getSignParamName(signType)}=${signature}`;
5303
5646
  };
@@ -5323,13 +5666,38 @@ var DouyinData = async (data, cookie, requestConfig) => {
5323
5666
  const userAgent = baseRequestConfig.headers?.["User-Agent"];
5324
5667
  const douyinApiUrls = createDouyinApiUrls(userAgent);
5325
5668
  const signType = data.signType ?? "a_bogus";
5669
+ /**
5670
+ * 遮蔽外层的 `buildSignedUrlBase`,让下面所有调用点自动补上 webid 与 secsdk 签名。
5671
+ *
5672
+ * 顺序固定为 webid → a_bogus → secsdk:三者都覆盖 query,颠倒任意一步签名都不成立。
5673
+ * path 不在 SDK 策略表内时 `SecSdk` 原样返回,因此对其余接口是无操作。
5674
+ */
5675
+ const secsdkUifid = extractUifidFromCookie(baseRequestConfig.headers?.Cookie);
5676
+ const buildSignedUrl = (url, st = signType, ua = userAgent) => douyinSign.SecSdk(buildSignedUrlBase(withDouyinWebid(url, baseRequestConfig.headers?.Cookie), st, ua), {
5677
+ uifid: secsdkUifid,
5678
+ method: baseRequestConfig.method
5679
+ });
5680
+ /** iesdouyin v2 游客端点专用:不签名、不带 cookie(带上只会多一层与会话的交叉校验) */
5681
+ const guestConfig = (url) => {
5682
+ const headers = { ...baseRequestConfig.headers };
5683
+ delete headers.Cookie;
5684
+ return {
5685
+ ...baseRequestConfig,
5686
+ headers,
5687
+ url
5688
+ };
5689
+ };
5326
5690
  switch (data.methodType) {
5327
5691
  case "textWork":
5328
5692
  case "parseWork":
5329
5693
  case "videoWork":
5330
5694
  case "imageAlbumWork":
5331
5695
  case "slidesWork": {
5332
- const url = douyinApiUrls.getWorkDetail({ aweme_id: data.aweme_id });
5696
+ /**
5697
+ * 走 www-hj 边缘 + 视频页自带的两个参数:www.douyin.com 上该接口实测 9/18 被 Argus 拦,
5698
+ * 换成 www-hj 后 18/18 通过。只是降低拦截率而非消除,重试仍需保留。
5699
+ */
5700
+ const url = douyinApiUrls.getWorkDetail({ aweme_id: data.aweme_id }).replace("//www.douyin.com", "//www-hj.douyin.com") + "&request_source=600&origin_type=video_page";
5333
5701
  return await GlobalGetData$3(data.methodType, {
5334
5702
  ...baseRequestConfig,
5335
5703
  url: buildSignedUrl(url, signType, userAgent)
@@ -5641,6 +6009,24 @@ var DouyinData = async (data, cookie, requestConfig) => {
5641
6009
  url: buildSignedUrl(url, signType, userAgent)
5642
6010
  });
5643
6011
  }
6012
+ case "guestUserInfo": return await GlobalGetData$3(data.methodType, guestConfig(douyinApiUrls.getGuestUserInfo({ unique_id: data.unique_id })));
6013
+ case "guestMusicInfo": return await GlobalGetData$3(data.methodType, guestConfig(douyinApiUrls.getGuestMusicInfo({ music_id: data.music_id })));
6014
+ case "guestMusicAwemeList": return await GlobalGetData$3(data.methodType, guestConfig(douyinApiUrls.getGuestMusicAwemeList({
6015
+ music_id: data.music_id,
6016
+ number: data.number,
6017
+ cursor: data.cursor
6018
+ })));
6019
+ case "emojiResourceMeta":
6020
+ /** App 的资源包接口:免鉴权,但要 Android UA,且不能带 douyin.com 那套默认头 */
6021
+ return await GlobalGetData$3(data.methodType, {
6022
+ ...baseRequestConfig,
6023
+ headers: {
6024
+ Accept: "application/json, text/plain, */*",
6025
+ "User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-S908E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36"
6026
+ },
6027
+ timeout: 15e3,
6028
+ url: douyinApiUrls.getEmojiResourceMeta()
6029
+ });
5644
6030
  case "liveRoomInfo": {
5645
6031
  let url = douyinApiUrls.getLiveRoomInfo({
5646
6032
  room_id: data.room_id,
@@ -5765,6 +6151,13 @@ var fetchPaginatedData = async (config) => {
5765
6151
  let lastResponse = {};
5766
6152
  let isFirstRequest = true;
5767
6153
  const userAgent = requestConfig.headers?.["User-Agent"];
6154
+ /** 分页接口同样在 SDK 的 webSign 策略表里,此处在 `DouyinData` 之外,需要自己再套一层 */
6155
+ const cookie = requestConfig.headers?.Cookie;
6156
+ const secsdkUifid = extractUifidFromCookie(cookie);
6157
+ const buildSignedUrl = (url, st, ua) => douyinSign.SecSdk(buildSignedUrlBase(withDouyinWebid(url, cookie), st, ua), {
6158
+ uifid: secsdkUifid,
6159
+ method: requestConfig.method
6160
+ });
5768
6161
  const targetNumber = Number(params.number ?? maxPageSize);
5769
6162
  while (fetchedData.length < targetNumber) {
5770
6163
  const remaining = targetNumber - fetchedData.length;
@@ -5798,27 +6191,63 @@ var fetchPaginatedData = async (config) => {
5798
6191
  var GlobalGetData$3 = async (type, config) => {
5799
6192
  let warningMessage = "";
5800
6193
  try {
5801
- const result = await fetchData(config);
5802
- if (isNetworkErrorResult(result)) {
5803
- const networkError = new Error(result.error.amagiError.errorDescription);
6194
+ const response = await fetchResponse(config);
6195
+ if (isNetworkErrorResult(response)) {
6196
+ const networkError = new Error(response.error.amagiError.errorDescription);
5804
6197
  Object.assign(networkError, {
5805
- code: result.error.code,
6198
+ code: response.error.code,
5806
6199
  data: null,
5807
6200
  amagiError: {
5808
- ...result.error.amagiError,
6201
+ ...response.error.amagiError,
5809
6202
  requestType: type
5810
6203
  }
5811
6204
  });
5812
6205
  throw networkError;
5813
6206
  }
6207
+ /** 抖音在每个响应头里回本次会话的 webid,顺手缓存,下次请求就能带上 */
6208
+ rememberDouyinWebid(config.headers, response.headers);
6209
+ const result = response.data;
6210
+ /**
6211
+ * 纯文本 body 说明被风控拦了,不是数据。
6212
+ *
6213
+ * 判据不能只看「是不是字符串」:综合搜索的 `general/search/stream/` 本来就回
6214
+ * 「十六进制长度行 + JSON」的分块流,由下游 `parseDouyinMultiJson` 解析。
6215
+ * 所以只有命中 Argus 关键字、或者整段里连一个 JSON 对象都没有时才算失败。
6216
+ */
6217
+ if (typeof result === "string" && result.trim() !== "") {
6218
+ const snippet = result.trim().slice(0, 200);
6219
+ const isArgus = /ArgusSecurityPlugin|Blocked by/i.test(snippet);
6220
+ if (isArgus || !result.includes("{\"")) {
6221
+ const ErrA = {
6222
+ errorDescription: isArgus ? `抖音风控拦截(Argus):${snippet}` : `接口返回了非 JSON 内容:${snippet}`,
6223
+ requestType: type ?? "未知请求类型",
6224
+ requestUrl: config.url
6225
+ };
6226
+ warningMessage = `
6227
+ 获取响应数据失败!原因:${ErrA.errorDescription}
6228
+ 请求类型:「${type}」
6229
+ 请求URL:${config.url}
6230
+ `;
6231
+ emitLogWarn(warningMessage);
6232
+ const argusError = new Error(ErrA.errorDescription);
6233
+ Object.assign(argusError, {
6234
+ code: isArgus ? "ARGUS_BLOCKED" : "NON_JSON_RESPONSE",
6235
+ data: null,
6236
+ amagiError: ErrA,
6237
+ argusBody: snippet
6238
+ });
6239
+ throw argusError;
6240
+ }
6241
+ }
5814
6242
  if (!result || result === "") {
6243
+ const emptyReason = "接口返回内容为空,可能的原因:①请求的设备类参数与 cookie 会话不匹配(多为 webid,抖音会静默返回 0 字节);②目标数据本身不公开(例如对方隐藏了「喜欢」列表);③抖音ck已失效";
5815
6244
  const Err = {
5816
- errorDescription: "获取响应数据失败!接口返回内容为空,你的抖音ck可能已经失效!",
6245
+ errorDescription: `获取响应数据失败!${emptyReason}`,
5817
6246
  requestType: type ?? "未知请求类型",
5818
6247
  requestUrl: config.url
5819
6248
  };
5820
6249
  warningMessage = `
5821
- 获取响应数据失败!原因:接口返回内容为空,你的抖音ck可能已经失效!
6250
+ 获取响应数据失败!原因:${emptyReason}
5822
6251
  请求类型:「${type}」
5823
6252
  请求URL:${config.url}
5824
6253
  `;
@@ -5904,6 +6333,10 @@ var filterSearchResponses = (objs) => {
5904
6333
  * 抖音 Fetcher 内部通用逻辑
5905
6334
  * @module fetchers/douyin/internal
5906
6335
  */
6336
+ /** 被风控拦截时的最大重试次数 */
6337
+ var MAX_ARGUS_RETRIES = 5;
6338
+ /** 触发换参重试的错误码 */
6339
+ var RETRYABLE_CODES = ["ARGUS_BLOCKED", "NON_JSON_RESPONSE"];
5907
6340
  /**
5908
6341
  * 抖音 API 内部调用函数
5909
6342
  * @internal
@@ -5915,18 +6348,39 @@ var filterSearchResponses = (objs) => {
5915
6348
  async function fetchDouyinInternal(methodType, options, config) {
5916
6349
  const startTime = Date.now();
5917
6350
  try {
5918
- const rawData = await DouyinData({ ...validateDouyinParams(methodType, options) }, config.cookie, config.requestConfig);
6351
+ const apiParams = { ...validateDouyinParams(methodType, options) };
6352
+ /**
6353
+ * 被风控拦下就换一整套请求参数重来。
6354
+ *
6355
+ * `DouyinData` 每次调用都会重新生成 msToken / verifyFp / a_bogus,而 Argus 是按单次请求的
6356
+ * token 组判定、不锁账号,所以换一组即可通过 —— 失败之间互不关联。
6357
+ */
6358
+ let rawData;
6359
+ let argusRetries = 0;
6360
+ for (;;) {
6361
+ rawData = await DouyinData(apiParams, config.cookie, config.requestConfig);
6362
+ if (!RETRYABLE_CODES.includes(rawData?.code) || argusRetries >= MAX_ARGUS_RETRIES) break;
6363
+ argusRetries++;
6364
+ emitLogWarn(`抖音风控拦截,换一组请求参数重试(${argusRetries}/${MAX_ARGUS_RETRIES}):` + String(rawData.argusBody ?? "").slice(0, 90));
6365
+ await new Promise((resolve) => setTimeout(resolve, 400 * argusRetries));
6366
+ }
5919
6367
  const duration = Date.now() - startTime;
5920
6368
  if (rawData.data === "" || rawData.status_code !== 0) {
6369
+ /** 带上真实业务码与文案:原先不传第三个参数,抖音的任何失败都恒为 500 */
6370
+ const retriedNote = argusRetries > 0 ? `(已重试 ${argusRetries} 次仍被拦)` : "";
6371
+ const argusDetail = rawData.amagiError?.errorDescription;
6372
+ const failMessage = rawData.status_msg ?? (argusDetail ? argusDetail + retriedNote : "抖音数据获取失败");
6373
+ /** 信封的 code 必须是数字,风控那类字符串码只放进事件的 errorCode */
6374
+ const failCode = typeof rawData.status_code === "number" ? rawData.status_code : 500;
5921
6375
  emitApiError({
5922
6376
  platform: "douyin",
5923
6377
  methodType,
5924
- errorCode: rawData.status_code,
5925
- errorMessage: rawData.status_msg ?? "抖音数据获取失败",
5926
- url: void 0,
6378
+ errorCode: rawData.code ?? failCode,
6379
+ errorMessage: failMessage,
6380
+ url: rawData.amagiError?.requestUrl,
5927
6381
  duration
5928
6382
  });
5929
- return createErrorResponse(rawData.amagiError, rawData.status_msg ?? "抖音数据获取失败");
6383
+ return createErrorResponse(rawData.amagiError, failMessage, failCode, rawData);
5930
6384
  }
5931
6385
  const result = createSuccessResponse$1(rawData, "获取成功", 200);
5932
6386
  emitApiSuccess({
@@ -5997,6 +6451,87 @@ async function fetchCommentReplies(options, cookie, requestConfig) {
5997
6451
  });
5998
6452
  }
5999
6453
  //#endregion
6454
+ //#region ../amagi/packages/core/src/model/fetchers/douyin/guest.ts
6455
+ /**
6456
+ * 通过抖音号获取用户信息
6457
+ *
6458
+ * 唯一免签名的抖音号转 sec_uid 途径。`searchContent` 需要 a_bogus、会被风控按概率拦下,
6459
+ * 而且是模糊匹配。抖音号不存在时接口返回 `status_code: 5`,落在失败信封里。
6460
+ *
6461
+ * @param options - 请求选项
6462
+ * @param options.unique_id - 抖音号
6463
+ * @param cookie - 抖音 Cookie (该接口不需要,保留以与其他 fetcher 同形)
6464
+ * @param requestConfig - 请求配置 (可选)
6465
+ * @returns 用户信息
6466
+ * @example
6467
+ * ```typescript
6468
+ * const result = await fetchGuestUserInfo({ unique_id: 'ubb_up' })
6469
+ * console.log(result.data.user_info.sec_uid)
6470
+ * ```
6471
+ */
6472
+ async function fetchGuestUserInfo(options, cookie, requestConfig) {
6473
+ return fetchDouyinInternal("guestUserInfo", options, {
6474
+ cookie,
6475
+ requestConfig
6476
+ });
6477
+ }
6478
+ /**
6479
+ * 获取原声本体 (免鉴权)
6480
+ *
6481
+ * 返回的 `music_info` 没有 `play_url`,mp3 只能从源作品上取 ——
6482
+ * `extra.extract_item_id` 就是创建这条原声的那个作品。
6483
+ *
6484
+ * @param options - 请求选项
6485
+ * @param options.music_id - 原声 ID (mid)
6486
+ * @param cookie - 抖音 Cookie (该接口不需要)
6487
+ * @param requestConfig - 请求配置 (可选)
6488
+ * @returns 原声信息
6489
+ */
6490
+ async function fetchGuestMusicInfo(options, cookie, requestConfig) {
6491
+ return fetchDouyinInternal("guestMusicInfo", options, {
6492
+ cookie,
6493
+ requestConfig
6494
+ });
6495
+ }
6496
+ /**
6497
+ * 获取使用某条原声的作品列表 (免鉴权)
6498
+ *
6499
+ * 每条的 `music` 字段被抖音裁成空对象,所以这条接口只能用来拿 `aweme_id`。
6500
+ *
6501
+ * @param options - 请求选项
6502
+ * @param options.music_id - 原声 ID
6503
+ * @param options.number - 获取数量,默认 10
6504
+ * @param options.cursor - 游标,用于翻页
6505
+ * @param cookie - 抖音 Cookie (该接口不需要)
6506
+ * @param requestConfig - 请求配置 (可选)
6507
+ * @returns 作品列表
6508
+ */
6509
+ async function fetchGuestMusicAwemeList(options, cookie, requestConfig) {
6510
+ return fetchDouyinInternal("guestMusicAwemeList", options, {
6511
+ cookie,
6512
+ requestConfig
6513
+ });
6514
+ }
6515
+ /**
6516
+ * 获取表情资源包元信息 (免鉴权)
6517
+ *
6518
+ * 返回的 `android_emoji_resource` 形如 `{ id, md5, resource_url, update_time }`,
6519
+ * `md5` 同时是版本号,抖音 App 自己就按它做增量判断。下载、校验、解包属于业务逻辑,
6520
+ * 这里只回答「是哪个包、去哪下」。
6521
+ *
6522
+ * @param options - 请求选项 (可选)
6523
+ * @param options.typeMode - 类型模式: 'strict' 返回严格类型, 'loose' 返回 any
6524
+ * @param cookie - 抖音 Cookie (该接口不需要)
6525
+ * @param requestConfig - 请求配置 (可选)
6526
+ * @returns 资源包元信息
6527
+ */
6528
+ async function fetchEmojiResourceMeta(options, cookie, requestConfig) {
6529
+ return fetchDouyinInternal("emojiResourceMeta", {}, {
6530
+ cookie,
6531
+ requestConfig
6532
+ });
6533
+ }
6534
+ //#endregion
6000
6535
  //#region ../amagi/packages/core/src/model/fetchers/douyin/misc.ts
6001
6536
  /**
6002
6537
  * 获取抖音音乐数据
@@ -6376,7 +6911,11 @@ function createBoundDouyinFetcher(cookie, requestConfig) {
6376
6911
  fetchLiveRoomInfo: (options, override) => fetchLiveRoomInfo$1(options, ...resolveRequest(override)),
6377
6912
  requestLoginQrcode: (options, override) => requestLoginQrcode(options, ...resolveRequest(override)),
6378
6913
  fetchEmojiList: (options, override) => fetchEmojiList$2(options, ...resolveRequest(override)),
6379
- fetchDynamicEmojiList: (options, override) => fetchDynamicEmojiList(options, ...resolveRequest(override))
6914
+ fetchDynamicEmojiList: (options, override) => fetchDynamicEmojiList(options, ...resolveRequest(override)),
6915
+ fetchGuestUserInfo: (options, override) => fetchGuestUserInfo(options, ...resolveRequest(override)),
6916
+ fetchGuestMusicInfo: (options, override) => fetchGuestMusicInfo(options, ...resolveRequest(override)),
6917
+ fetchGuestMusicAwemeList: (options, override) => fetchGuestMusicAwemeList(options, ...resolveRequest(override)),
6918
+ fetchEmojiResourceMeta: (options, override) => fetchEmojiResourceMeta(options, ...resolveRequest(override))
6380
6919
  };
6381
6920
  }
6382
6921
  //#endregion
@@ -6418,7 +6957,11 @@ var douyinFetcher$1 = {
6418
6957
  fetchLiveRoomInfo: fetchLiveRoomInfo$1,
6419
6958
  requestLoginQrcode,
6420
6959
  fetchEmojiList: fetchEmojiList$2,
6421
- fetchDynamicEmojiList
6960
+ fetchDynamicEmojiList,
6961
+ fetchGuestUserInfo,
6962
+ fetchGuestMusicInfo,
6963
+ fetchGuestMusicAwemeList,
6964
+ fetchEmojiResourceMeta
6422
6965
  };
6423
6966
  //#endregion
6424
6967
  //#region ../amagi/packages/core/src/platform/kuaishou/API.ts
@@ -14162,34 +14705,6 @@ var resolveUseDarkTheme = async (path, data) => {
14162
14705
  return Common.useDarkTheme();
14163
14706
  };
14164
14707
  //#endregion
14165
- //#region src/module/utils/Render/wm.ts
14166
- /**
14167
- * 使用 exiftool 写入图片详细信息
14168
- *
14169
- * @param pngBuffer - PNG 图片二进制数据
14170
- * @returns 写入后的 PNG Buffer
14171
- */
14172
- /**
14173
- * 将隐水印嵌入到 PNG 图片中
14174
- * @param pngBytes - PNG 图片的 Buffer 或 Uint8Array
14175
- * @param watermarkText - 要嵌入的水印文本
14176
- * @returns 嵌入水印后的 PNG Buffer,失败返回 null
14177
- */
14178
- var embedWatermark = (pngBytes, watermarkText) => {
14179
- try {
14180
- const result = embedWatermarkToPngBytes(pngBytes instanceof Buffer ? pngBytes : Buffer.from(pngBytes), watermarkText);
14181
- return result instanceof Buffer ? result : Buffer.from(result.buffer);
14182
- } catch {
14183
- return null;
14184
- }
14185
- };
14186
- /**
14187
- * 从 PNG 图片中提取隐水印文本
14188
- *
14189
- * @param pngBytes - PNG 图片的 Buffer 或 Uint8Array
14190
- * @returns 提取出的水印文本,失败返回 null
14191
- */
14192
- //#endregion
14193
14708
  //#region src/module/utils/Render/index.ts
14194
14709
  var isDevRuntime = !fileURLToPath(import.meta.url).replace(/\\/g, "/").includes("/lib/");
14195
14710
  var renderTemplate = createTemplateRenderer(import.meta.url, {
@@ -14204,31 +14719,24 @@ var renderTemplate = createTemplateRenderer(import.meta.url, {
14204
14719
  * 渲染函数
14205
14720
  * 将指定路径的模板渲染为图片元素数组
14206
14721
  *
14207
- * @param event 消息事件对象,用于获取机器人账号信息
14722
+ * @param event 消息事件对象,用于按协议端能力决定是否分页截图
14208
14723
  * @template P 渲染路径,必须是有效的动态路径
14209
14724
  * @param path 渲染路径,格式为 "平台/组件ID" 或 "平台/分类/组件ID"
14210
14725
  * @param data 渲染数据,类型根据路径自动推断
14211
- * @options 渲染选项
14212
14726
  * @returns 渲染结果图片元素数组的 Promise
14213
14727
  */
14214
- var Render = async (event, path, data, options) => {
14728
+ var Render = async (event, path, data) => {
14215
14729
  const templateType = path.split("/")[0];
14216
14730
  let hasUpdate = false;
14217
- if (!Config.app.RemoveWatermark) try {
14731
+ try {
14218
14732
  const lockedVersion = await db.get("kkk:update:lock");
14219
14733
  if (typeof lockedVersion === "string" && lockedVersion.length > 0) hasUpdate = isSemverGreater(lockedVersion, Root.pluginVersion);
14220
14734
  } catch {}
14221
- const watermarkText = JSON.stringify({
14222
- a: Date.now(),
14223
- b: `Generated by karin-plugin-kkk v${Root.pluginVersion}, with source code open-sourced under the GPL-3.0 license`,
14224
- c: `Sent by ${event?.bot?.account.selfId ?? "unknown"}|${event?.bot?.account.name ?? "unknown"}`
14225
- });
14226
- const watermarkTextBitSize = Buffer.byteLength(watermarkText, "utf8") * 8;
14227
14735
  const useDarkTheme = await resolveUseDarkTheme(path, data);
14228
14736
  const result = await renderTemplate(path, data, {
14229
14737
  scale: Math.min(2, Math.max(.5, Number(Config.app.renderScale) / 100)),
14230
14738
  theme: { mode: useDarkTheme ? "dark" : "light" },
14231
- version: Config.app.RemoveWatermark ? void 0 : {
14739
+ version: {
14232
14740
  plugin: "karin-plugin",
14233
14741
  pluginName: "kkk",
14234
14742
  pluginVersion: Root.pluginVersion,
@@ -14237,7 +14745,6 @@ var Render = async (event, path, data, options) => {
14237
14745
  frameworkVersion: Root.karinVersion,
14238
14746
  hasUpdate
14239
14747
  },
14240
- watermarkTextBitSize,
14241
14748
  ambientCover: {
14242
14749
  coverOpacity: Config.app.ambientCover?.coverOpacity,
14243
14750
  overlayEdgeOpacity: Config.app.ambientCover?.overlayEdgeOpacity,
@@ -14268,18 +14775,15 @@ var Render = async (event, path, data, options) => {
14268
14775
  const imageStats = [];
14269
14776
  for (const image of images) {
14270
14777
  const imageBuffer = Buffer.from(image, "base64");
14271
- let finalImageBuffer;
14272
- if (options?.skipWatermark) finalImageBuffer = imageBuffer;
14273
- else finalImageBuffer = embedWatermark(imageBuffer, watermarkText) ?? imageBuffer;
14274
- const metadata = getImageMetadata(finalImageBuffer);
14778
+ const metadata = getImageMetadata(imageBuffer);
14275
14779
  const dimensions = metadata.width && metadata.height ? `${metadata.width}x${metadata.height}` : "unknown";
14276
14780
  imageStats.push({
14277
14781
  index: ret.length,
14278
14782
  type: "png",
14279
- sizeMb: (finalImageBuffer.length / (1024 * 1024)).toFixed(3),
14783
+ sizeMb: (imageBuffer.length / (1024 * 1024)).toFixed(3),
14280
14784
  dimensions
14281
14785
  });
14282
- ret.push(segment.image("base64://" + finalImageBuffer.toString("base64")));
14786
+ ret.push(segment.image("base64://" + image));
14283
14787
  }
14284
14788
  const statsText = imageStats.map((stat, i) => ` [${i + 1}] 类型: ${stat.type}, 大小: ${stat.sizeMb}MB, 尺寸: ${stat.dimensions}`).join("\n");
14285
14789
  logger.debug(`[Render] 图片处理完成,准备入队发送:\n共 ${imageStats.length} 张图片:\n${statsText}`);
@@ -14306,24 +14810,6 @@ var getImageMetadata = (buffer) => {
14306
14810
  };
14307
14811
  return {};
14308
14812
  };
14309
- /**
14310
- * 对已渲染的图片元素数组应用水印
14311
- * 用于推送场景:渲染一次,按目标群逐个嵌入不同 bot 的水印
14312
- */
14313
- var applyWatermarkToImages = (images, event) => {
14314
- if (Config.app.RemoveWatermark) return images;
14315
- const watermarkText = JSON.stringify({
14316
- a: Date.now(),
14317
- b: `Generated by karin-plugin-kkk v${Root.pluginVersion}, with source code open-sourced under the GPL-3.0 license`,
14318
- c: `Sent by ${event?.bot?.account.selfId ?? "unknown"}|${event?.bot?.account.name ?? "unknown"}`
14319
- });
14320
- return images.map((img) => {
14321
- const base64Data = img.file.replace(/^base64:\/\//, "");
14322
- const buffer = Buffer.from(base64Data, "base64");
14323
- const watermarked = embedWatermark(buffer, watermarkText) ?? buffer;
14324
- return segment.image("base64://" + watermarked.toString("base64"));
14325
- });
14326
- };
14327
14813
  //#endregion
14328
14814
  //#region src/module/db/bilibili.ts
14329
14815
  /** 数据库操作类 */
@@ -16864,6 +17350,12 @@ var createWebLinkNode = (text, jumpUrl) => ({
16864
17350
  text,
16865
17351
  jumpUrl
16866
17352
  });
17353
+ /** 创建站内图文链接节点。 */
17354
+ var createOpusLinkNode = (text, url) => ({
17355
+ type: "opusLink",
17356
+ text,
17357
+ url
17358
+ });
16867
17359
  /** 创建投票节点。 */
16868
17360
  var createVoteNode = (text) => ({
16869
17361
  type: "vote",
@@ -16969,6 +17461,7 @@ var extractRichTextPlainText = (document) => {
16969
17461
  case "at":
16970
17462
  case "lottery":
16971
17463
  case "webLink":
17464
+ case "opusLink":
16972
17465
  case "vote":
16973
17466
  case "viewPicture":
16974
17467
  case "hashtag":
@@ -17125,6 +17618,34 @@ var WebLinkIcon = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs
17125
17618
  fill: "currentColor"
17126
17619
  })]
17127
17620
  });
17621
+ /** 站内图文链接图标 */
17622
+ var OpusLinkIcon = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
17623
+ className: cn$1(className, "inline-block w-16 h-auto mb-2 mr-2 align-middle"),
17624
+ xmlns: "http://www.w3.org/2000/svg",
17625
+ viewBox: "0 0 20 20",
17626
+ children: [
17627
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
17628
+ d: "M10 3.124591666666667C8.164908333333335 3.124591666666667 6.791808333333333 3.15845 5.811916666666667 3.1982166666666667C5.002533333333334 3.231058333333334 4.375 3.8969416666666667 4.375 4.743841666666667L4.375 15.12575C4.375 16.01275 5.067016666666667 16.737333333333336 5.943891666666667 16.773000000000003C7.195191666666667 16.823833333333337 8.756833333333335 16.874583333333334 10 16.874583333333334C11.243166666666667 16.874583333333334 12.804833333333335 16.823833333333337 14.056125 16.773000000000003C14.932958333333335 16.737333333333336 15.625 16.01275 15.625 15.12575L15.625 4.743841666666667C15.625 3.8969416666666667 14.997458333333332 3.231058333333334 14.188083333333335 3.1982166666666667C13.208208333333333 3.15845 11.835125 3.124591666666667 10 3.124591666666667zM5.7612250000000005 1.9492416666666668C6.759566666666667 1.908725 8.149641666666668 1.8745916666666667 10 1.8745916666666667C11.850375 1.8745916666666667 13.240458333333335 1.908725 14.238791666666666 1.9492416666666668C15.754791666666668 2.010766666666667 16.875 3.263466666666667 16.875 4.743841666666667L16.875 15.12575C16.875 16.673958333333335 15.663166666666667 17.958708333333334 14.106874999999999 18.021958333333334C12.851916666666668 18.072958333333336 11.269541666666667 18.124583333333334 10 18.124583333333334C8.730458333333333 18.124583333333334 7.148116666666668 18.072958333333336 5.893133333333334 18.021958333333334C4.336833333333334 17.958708333333334 3.125 16.673958333333335 3.125 15.12575L3.125 4.743841666666667C3.125 3.263475 4.245233333333334 2.010766666666667 5.7612250000000005 1.9492416666666668z",
17629
+ fill: "currentColor"
17630
+ }),
17631
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
17632
+ d: "M6.385416666666667 6.2491666666666665C6.385416666666667 5.921250000000001 6.65125 5.655416666666667 6.979166666666667 5.655416666666667L9.479166666666668 5.655416666666667C9.807083333333333 5.655416666666667 10.072916666666668 5.921250000000001 10.072916666666668 6.2491666666666665C10.072916666666668 6.577091666666667 9.807083333333333 6.842908333333333 9.479166666666668 6.842908333333333L6.979166666666667 6.842908333333333C6.65125 6.842908333333333 6.385416666666667 6.577091666666667 6.385416666666667 6.2491666666666665z",
17633
+ fill: "currentColor"
17634
+ }),
17635
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
17636
+ d: "M6.28125 8.749166666666667C6.28125 8.421258333333334 6.547083333333333 8.155408333333334 6.875 8.155408333333334L13.125 8.155408333333334C13.452916666666667 8.155408333333334 13.71875 8.421258333333334 13.71875 8.749166666666667C13.71875 9.077083333333334 13.452916666666667 9.342916666666666 13.125 9.342916666666666L6.875 9.342916666666666C6.547083333333333 9.342916666666666 6.28125 9.077083333333334 6.28125 8.749166666666667z",
17637
+ fill: "currentColor"
17638
+ }),
17639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
17640
+ d: "M6.28125 11.249166666666667C6.28125 10.92125 6.547083333333333 10.655416666666667 6.875 10.655416666666667L13.125 10.655416666666667C13.452916666666667 10.655416666666667 13.71875 10.92125 13.71875 11.249166666666667C13.71875 11.577083333333334 13.452916666666667 11.842916666666666 13.125 11.842916666666666L6.875 11.842916666666666C6.547083333333333 11.842916666666666 6.28125 11.577083333333334 6.28125 11.249166666666667z",
17641
+ fill: "currentColor"
17642
+ }),
17643
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", {
17644
+ d: "M6.28125 13.749166666666666C6.28125 13.42125 6.547083333333333 13.155416666666667 6.875 13.155416666666667L11.458333333333334 13.155416666666667C11.78625 13.155416666666667 12.052083333333334 13.42125 12.052083333333334 13.749166666666666C12.052083333333334 14.077083333333333 11.78625 14.342916666666667 11.458333333333334 14.342916666666667L6.875 14.342916666666667C6.547083333333333 14.342916666666667 6.28125 14.077083333333333 6.28125 13.749166666666666z",
17645
+ fill: "currentColor"
17646
+ })
17647
+ ]
17648
+ });
17128
17649
  /** 投票图标 */
17129
17650
  var VoteIcon = ({ className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
17130
17651
  className: cn$1(className, "inline-block w-20 mb-2 align-middle"),
@@ -17300,6 +17821,19 @@ var renderNodeToReact = (node, index, options) => {
17300
17821
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WebLinkIcon, {})
17301
17822
  }), node.text]
17302
17823
  }, `weblink-${index}`);
17824
+ case "opusLink": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
17825
+ className: options.opusLink?.className,
17826
+ "data-richtext-node": "opusLink",
17827
+ "data-url": node.url,
17828
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
17829
+ style: options.iconScale && options.iconScale !== 1 ? {
17830
+ transform: `scale(${options.iconScale})`,
17831
+ transformOrigin: "left center",
17832
+ display: "inline-block"
17833
+ } : void 0,
17834
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OpusLinkIcon, {})
17835
+ }), node.text]
17836
+ }, `opuslink-${index}`);
17303
17837
  case "vote": return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
17304
17838
  className: options.vote?.className,
17305
17839
  "data-richtext-node": "vote",
@@ -18463,7 +18997,7 @@ var ViteLogo = ({ className = "w-auto h-12" }) => /* @__PURE__ */ (0, import_jsx
18463
18997
  */
18464
18998
  var DefaultLayout = ({ children, ctx, className = "", style = {} }) => {
18465
18999
  const useDarkTheme = isDark(ctx);
18466
- const { version, watermarkTextBitSize } = ctx;
19000
+ const { version } = ctx;
18467
19001
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18468
19002
  className: cn("relative w-360 shrink-0 overflow-hidden rounded-[5rem] bg-background bg-clip-padding text-foreground font-[HarmonyOSHans-Regular]", className),
18469
19003
  style: {
@@ -18472,9 +19006,9 @@ var DefaultLayout = ({ children, ctx, className = "", style = {} }) => {
18472
19006
  maxWidth: "1440px",
18473
19007
  ...style
18474
19008
  },
18475
- children: [children, version ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
19009
+ children: [children, version ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18476
19010
  className: "relative z-50 pt-32 pb-20 text-foreground/80",
18477
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
19011
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18478
19012
  className: "flex relative justify-center items-center space-x-8",
18479
19013
  children: [
18480
19014
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -18594,20 +19128,8 @@ var DefaultLayout = ({ children, ctx, className = "", style = {} }) => {
18594
19128
  })]
18595
19129
  })] })
18596
19130
  ]
18597
- }), typeof watermarkTextBitSize === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18598
- className: "flex justify-center",
18599
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18600
- className: "text-xs font-mono text-foreground/30",
18601
- children: ["Restore ID: ", watermarkTextBitSize]
18602
- })
18603
- })]
18604
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18605
- className: "flex items-center justify-center h-24",
18606
- children: typeof watermarkTextBitSize === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18607
- className: "text-xs font-mono text-foreground/30",
18608
- children: ["Restore ID: ", watermarkTextBitSize]
18609
19131
  })
18610
- })]
19132
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-24" })]
18611
19133
  });
18612
19134
  };
18613
19135
  //#endregion
@@ -20003,6 +20525,50 @@ var comment_default$3 = defineTemplate({
20003
20525
  validate: (data) => typeof data === "object" && data !== null && typeof data.share_url === "string"
20004
20526
  });
20005
20527
  //#endregion
20528
+ //#region ktr/utils/avatarProxy.ts
20529
+ /**
20530
+ * ktr 开发面板预览用的头像代理约定。
20531
+ *
20532
+ * 头像 CDN(尤其 `q*.qlogo.cn`)不返回 `Access-Control-Allow-Origin`,
20533
+ * 浏览器里直接 fetch 会被同源策略拦掉,面板预览的二维码就嵌不上头像。
20534
+ * 生产渲染走 Node SSR(服务端 fetch,无同源策略),不受影响也不需要这个代理。
20535
+ *
20536
+ * 代理端点由 `karin.template.ts` 的 avatarProxyPlugin 挂在 ktr dev 服务器上,仅 serve 态存在。
20537
+ */
20538
+ /** 代理端点路径,dev 服务器与模板侧共用这一处定义。 */
20539
+ var AVATAR_PROXY_PATH = "/__kkk/avatar-proxy";
20540
+ /**
20541
+ * 允许转发的头像 CDN 域名。
20542
+ *
20543
+ * 白名单是为了不让 dev 服务器变成任意地址的开放转发;
20544
+ * 其它适配器的头像域名如果也缺 CORS 头,在这里补一条即可(不在名单内的走直连)。
20545
+ */
20546
+ var ALLOWED_AVATAR_HOSTS = [
20547
+ /(?:^|\.)qlogo\.cn$/i,
20548
+ /(?:^|\.)qpic\.cn$/i,
20549
+ /(?:^|\.)hdslb\.com$/i
20550
+ ];
20551
+ /**
20552
+ * 判断某个头像 URL 是否允许经代理转发
20553
+ * @param url - 头像地址
20554
+ * @returns 命中白名单且是 http(s) 时为 true
20555
+ */
20556
+ var isProxyableAvatarUrl = (url) => {
20557
+ try {
20558
+ const parsed = new URL(url);
20559
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
20560
+ return ALLOWED_AVATAR_HOSTS.some((pattern) => pattern.test(parsed.hostname));
20561
+ } catch {
20562
+ return false;
20563
+ }
20564
+ };
20565
+ /**
20566
+ * 把头像地址改写成同源代理地址
20567
+ * @param url - 原始头像地址
20568
+ * @returns 代理端点地址
20569
+ */
20570
+ var toProxiedAvatarUrl = (url) => `${AVATAR_PROXY_PATH}?url=${encodeURIComponent(url)}`;
20571
+ //#endregion
20006
20572
  //#region ktr/utils/QRCodeAvatar.ts
20007
20573
  var MAX_QRCODE_AVATAR_BYTES = 2 * 1024 * 1024;
20008
20574
  var QRCODE_AVATAR_TIMEOUT_MS = 5e3;
@@ -20019,7 +20585,7 @@ var isSupportedImageBytes = (image) => {
20019
20585
  const isWebp = image.byteLength >= 12 && image[0] === 82 && image[1] === 73 && image[2] === 70 && image[3] === 70 && image[8] === 87 && image[9] === 69 && image[10] === 66 && image[11] === 80;
20020
20586
  return isPng || isJpeg || isWebp;
20021
20587
  };
20022
- var fetchQRCodeAvatar = async (url) => {
20588
+ var requestAvatarBytes = async (url) => {
20023
20589
  try {
20024
20590
  const response = await fetch(url, {
20025
20591
  headers: { accept: "image/png,image/jpeg,image/webp,image/*;q=0.8,*/*;q=0.5" },
@@ -20037,6 +20603,23 @@ var fetchQRCodeAvatar = async (url) => {
20037
20603
  }
20038
20604
  };
20039
20605
  /**
20606
+ * 依次尝试的取图地址。
20607
+ *
20608
+ * 浏览器(只有 ktr 开发面板会走到)里头像 CDN 多半缺 CORS 头,直连必被同源策略拦,
20609
+ * 所以优先走 dev 服务器的同源代理,代理不在(如面板未启动)时再退回直连。
20610
+ * Node SSR 侧没有同源策略,始终直连。
20611
+ */
20612
+ var avatarCandidates = (url) => {
20613
+ if (typeof window === "undefined" || !isProxyableAvatarUrl(url)) return [url];
20614
+ return [toProxiedAvatarUrl(url), url];
20615
+ };
20616
+ var fetchQRCodeAvatar = async (url) => {
20617
+ for (const candidate of avatarCandidates(url)) {
20618
+ const image = await requestAvatarBytes(candidate);
20619
+ if (image) return image;
20620
+ }
20621
+ };
20622
+ /**
20040
20623
  * 加载并缓存二维码头像。同一头像的并发渲染会复用请求,失败结果不会进入缓存。
20041
20624
  */
20042
20625
  var loadQRCodeAvatar = (url) => {
@@ -20162,6 +20745,7 @@ var BilibiliArticleContent = import_react.memo((props) => {
20162
20745
  at: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
20163
20746
  topic: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
20164
20747
  webLink: { className: "text-[#006A9E] dark:text-[#58B0D5] underline" },
20748
+ opusLink: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
20165
20749
  mention: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
20166
20750
  lottery: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
20167
20751
  vote: { className: "text-[#006A9E] dark:text-[#58B0D5]" },
@@ -20386,10 +20970,7 @@ var BilibiliArticleFooter = import_react.memo((props) => {
20386
20970
  */
20387
20971
  var BilibiliArticleDynamic = import_react.memo((props) => {
20388
20972
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultLayout, {
20389
- ctx: {
20390
- ...props.ctx,
20391
- watermarkTextBitSize: void 0
20392
- },
20973
+ ctx: props.ctx,
20393
20974
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
20394
20975
  className: "p-4",
20395
20976
  children: [
@@ -22039,7 +22620,6 @@ var DYNAMIC_TYPE_WORD_default = defineTemplate({
22039
22620
  */
22040
22621
  var BilibiliQrcodeImg = import_react.memo((props) => {
22041
22622
  const dark = isDark(props.ctx);
22042
- const qrCodeDataUrl = generateQRCode(props.data.share_url, dark);
22043
22623
  const theme = {
22044
22624
  bg: dark ? "#000000" : "#FFFFFF",
22045
22625
  text: dark ? "#FFFFFF" : "#000000",
@@ -22184,8 +22764,10 @@ var BilibiliQrcodeImg = import_react.memo((props) => {
22184
22764
  width: "800px",
22185
22765
  height: "800px"
22186
22766
  },
22187
- children: qrCodeDataUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
22188
- src: qrCodeDataUrl,
22767
+ children: props.data.share_url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QRCodeWithAvatar, {
22768
+ value: props.data.share_url,
22769
+ avatarUrl: props.data.avatarUrl,
22770
+ useDarkTheme: dark,
22189
22771
  alt: "QR Code",
22190
22772
  className: "w-full h-full object-contain"
22191
22773
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -25659,7 +26241,6 @@ var musicinfo_default = defineTemplate({
25659
26241
  */
25660
26242
  var DouyinQrcodeImg = import_react.memo((props) => {
25661
26243
  const dark = isDark(props.ctx);
25662
- const qrCodeImage = generateQRCode(props.data.share_url || "", dark);
25663
26244
  const theme = {
25664
26245
  bg: dark ? "#000000" : "#FFFFFF",
25665
26246
  text: dark ? "#FFFFFF" : "#000000",
@@ -25797,8 +26378,10 @@ var DouyinQrcodeImg = import_react.memo((props) => {
25797
26378
  width: "800px",
25798
26379
  height: "800px"
25799
26380
  },
25800
- children: qrCodeImage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
25801
- src: qrCodeImage,
26381
+ children: props.data.share_url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QRCodeWithAvatar, {
26382
+ value: props.data.share_url,
26383
+ avatarUrl: props.data.avatarUrl,
26384
+ useDarkTheme: dark,
25802
26385
  alt: "QR Code",
25803
26386
  className: "w-full h-full object-contain"
25804
26387
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
@@ -29950,7 +30533,6 @@ var live_photo_tip_default = defineTemplate({
29950
30533
  * @returns JSX元素
29951
30534
  */
29952
30535
  var QrLogin = import_react.memo((props) => {
29953
- const qrCodeDataUrl = props.data.qr_url ? generateQRCode(props.data.qr_url, isDark(props.ctx)) : "";
29954
30536
  const dark = isDark(props.ctx);
29955
30537
  const bgColor = dark ? "#0f0f1a" : "#f8f6ff";
29956
30538
  const secondaryColor = dark ? "#a78bfa" : "#8b5cf6";
@@ -30171,10 +30753,12 @@ var QrLogin = import_react.memo((props) => {
30171
30753
  }),
30172
30754
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
30173
30755
  className: "flex-1 flex flex-col justify-center items-center py-12",
30174
- children: [qrCodeDataUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
30756
+ children: [props.data.share_url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
30175
30757
  className: "flex justify-center items-center w-200 h-200",
30176
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
30177
- src: qrCodeDataUrl,
30758
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QRCodeWithAvatar, {
30759
+ value: props.data.share_url,
30760
+ avatarUrl: props.data.avatarUrl,
30761
+ useDarkTheme: dark,
30178
30762
  alt: "登录二维码",
30179
30763
  className: "object-contain w-full h-full"
30180
30764
  })
@@ -33375,6 +33959,25 @@ var resolveUsableBot = async (selfId) => {
33375
33959
  if (!fallbackBotId) return void 0;
33376
33960
  return karin.getBot(fallbackBotId);
33377
33961
  };
33962
+ /**
33963
+ * 取触发者头像 URL,用于嵌到登录二维码中心当 logo。
33964
+ *
33965
+ * 适配器(如 console)可能不实现 getAvatarUrl 或返回空串,模板侧 avatarUrl 缺省即退化为普通二维码,
33966
+ * 所以这里任何失败都只记 debug 日志并返回 undefined,不影响登录流程。
33967
+ *
33968
+ * @param e - 触发登录的消息事件
33969
+ * @returns 头像 URL,取不到时为 undefined
33970
+ */
33971
+ var resolveTriggerAvatarUrl = async (e) => {
33972
+ const userId = e.sender?.userId || e.userId;
33973
+ if (!userId) return void 0;
33974
+ try {
33975
+ return await e.bot.getAvatarUrl(userId) || void 0;
33976
+ } catch (error) {
33977
+ logger.debug("[karin-plugin-kkk] 获取触发者头像失败,二维码将不嵌入头像:", error);
33978
+ return;
33979
+ }
33980
+ };
33378
33981
  //#endregion
33379
33982
  //#region src/module/utils/ErrorHandler/utils.ts
33380
33983
  /**
@@ -34289,6 +34892,16 @@ var extractWordStyle = (style = {}) => {
34289
34892
  var parseOpusTextNodes = (nodes, useDarkTheme) => {
34290
34893
  const result = [];
34291
34894
  for (const node of nodes) {
34895
+ if (node.node_type === 4) {
34896
+ const link = node.link;
34897
+ if (!link?.show_text) {
34898
+ logger.error(`[bilibili] opus 富文本遇到 node_type=4 但缺少 link 数据: ${JSON.stringify(node).slice(0, 500)}`);
34899
+ continue;
34900
+ }
34901
+ if (link.link) result.push(createOpusLinkNode(link.show_text, link.link));
34902
+ else result.push(createTextNode(link.show_text));
34903
+ continue;
34904
+ }
34292
34905
  if (node.node_type !== 1 || !node.word) {
34293
34906
  if (node.node_type !== void 0 && node.node_type !== 1) logger.error(`[bilibili] opus 富文本遇到未适配的 node_type: ${node.node_type},节点数据: ${JSON.stringify(node).slice(0, 500)}`);
34294
34907
  continue;
@@ -34599,6 +35212,7 @@ var inlineRichTextNodeToText = (node) => {
34599
35212
  case "hashtag": return node.text;
34600
35213
  case "emoji": return node.name;
34601
35214
  case "webLink": return formatRichTextLink(node.text, node.jumpUrl);
35215
+ case "opusLink": return formatRichTextLink(node.text, node.url);
34602
35216
  case "lineBreak": return "\n";
34603
35217
  default: return "";
34604
35218
  }
@@ -36410,7 +37024,10 @@ var getBilibiliID = async (url) => {
36410
37024
  var bilibiliLogin = async (e) => {
36411
37025
  /** 申请二维码 */
36412
37026
  const qrcodeurl = await bilibiliFetcher.requestLoginQrcode({ typeMode: "strict" });
36413
- const qrimg = await Render(e, "bilibili/qrcodeImg", { share_url: qrcodeurl.data.data.url });
37027
+ const qrimg = await Render(e, "bilibili/qrcodeImg", {
37028
+ share_url: qrcodeurl.data.data.url,
37029
+ avatarUrl: await resolveTriggerAvatarUrl(e)
37030
+ });
36414
37031
  const base64Data = qrimg[0]?.file;
36415
37032
  if (!base64Data) throw new Error("生成二维码图片失败");
36416
37033
  const cleanBase64 = base64Data.replace(/^base64:\/\//, "");
@@ -36779,7 +37396,7 @@ var Bilibilipush = class extends Base {
36779
37396
  additional: parseAdditionalCard(data[dynamicId].Dynamic_Data.modules.module_dynamic.additional),
36780
37397
  dynamicTYPE: "图文动态推送",
36781
37398
  dynamic_id: dynamicId
36782
- }, { skipWatermark: true });
37399
+ });
36783
37400
  break;
36784
37401
  /** 处理纯文动态 */
36785
37402
  case DynamicType.WORD: {
@@ -36816,7 +37433,7 @@ var Bilibilipush = class extends Base {
36816
37433
  additional: parseAdditionalCard(data[dynamicId].Dynamic_Data.modules.module_dynamic.additional),
36817
37434
  dynamicTYPE: "纯文动态推送",
36818
37435
  dynamic_id: dynamicId
36819
- }, { skipWatermark: true });
37436
+ });
36820
37437
  break;
36821
37438
  }
36822
37439
  /** 处理视频动态 */
@@ -36889,7 +37506,7 @@ var Bilibilipush = class extends Base {
36889
37506
  dynamicTYPE: "视频动态推送",
36890
37507
  dynamic_id: dynamicId,
36891
37508
  staff
36892
- }, { skipWatermark: true });
37509
+ });
36893
37510
  }
36894
37511
  break;
36895
37512
  /** 处理直播动态 */
@@ -36907,7 +37524,7 @@ var Bilibilipush = class extends Base {
36907
37524
  now_time: TimeFormatter.now(),
36908
37525
  share_url: "https://live.bilibili.com/" + liveInfo.live_play_info.room_id,
36909
37526
  dynamicTYPE: "直播动态推送"
36910
- }, { skipWatermark: true });
37527
+ });
36911
37528
  break;
36912
37529
  }
36913
37530
  /** 处理转发动态 */
@@ -37051,7 +37668,7 @@ var Bilibilipush = class extends Base {
37051
37668
  original_content,
37052
37669
  imgList: imgList.length > 0 ? imgList : null,
37053
37670
  dynamic_id: dynamicId
37054
- }, { skipWatermark: true });
37671
+ });
37055
37672
  break;
37056
37673
  }
37057
37674
  /** 文章/专栏动态 */
@@ -37093,7 +37710,7 @@ var Bilibilipush = class extends Base {
37093
37710
  render_time: TimeFormatter.now(),
37094
37711
  share_url: shareUrl,
37095
37712
  dynamicTYPE: "专栏动态推送"
37096
- }, { skipWatermark: true });
37713
+ });
37097
37714
  break;
37098
37715
  }
37099
37716
  /** 未处理的动态类型 */
@@ -37114,7 +37731,7 @@ var Bilibilipush = class extends Base {
37114
37731
  const eventWithBot = this.e;
37115
37732
  eventWithBot.bot = bot;
37116
37733
  eventWithBot.selfId = botId;
37117
- const watermarkedImg = img ? applyWatermarkToImages(img, this.e) : [];
37734
+ const pushImg = img ?? [];
37118
37735
  const parseUrl = buildParseUrl(data[dynamicId]);
37119
37736
  const parseButton = bot?.adapter?.name === "QQ Official Bot" && parseUrl ? [segment.button([{
37120
37737
  text: "解析",
@@ -37125,7 +37742,7 @@ var Bilibilipush = class extends Base {
37125
37742
  callback: true,
37126
37743
  data: `#kkk帮助`
37127
37744
  }])] : [];
37128
- status = await karin.sendMsg(botId, Contact, [...watermarkedImg, ...parseButton]);
37745
+ status = await karin.sendMsg(botId, Contact, [...pushImg, ...parseButton]);
37129
37746
  if (Config.bilibili.push.parsedynamic && Config.bilibili.push.parseDynamicTypes.includes(data[dynamicId].dynamic_type) && status.messageId) switch (data[dynamicId].dynamic_type) {
37130
37747
  case "DYNAMIC_TYPE_AV":
37131
37748
  if (send_video) {
@@ -39509,7 +40126,7 @@ function getDefaultPushLabel(workTypeInfo) {
39509
40126
  * @returns 渲染后的图片元素数组
39510
40127
  */
39511
40128
  async function renderWorkImage(options) {
39512
- const { e, Detail_Data, create_time, shareLink, skipWatermark = false, videoSource } = options;
40129
+ const { e, Detail_Data, create_time, shareLink, videoSource } = options;
39513
40130
  const workTypeInfo = getWorkTypeInfo(Detail_Data);
39514
40131
  const dynamicTypeLabel = options.dynamicTypeLabel ?? getDefaultPushLabel(workTypeInfo);
39515
40132
  const coverUrl = getWorkCoverUrl(workTypeInfo, Detail_Data);
@@ -39521,7 +40138,6 @@ async function renderWorkImage(options) {
39521
40138
  const authorNickname = Detail_Data.author?.nickname ?? user.nickname;
39522
40139
  const cooperationInfo = buildCooperationInfo(Detail_Data);
39523
40140
  const mentionCache = /* @__PURE__ */ new Map();
39524
- const renderOpts = skipWatermark ? { skipWatermark: true } : void 0;
39525
40141
  switch (workTypeInfo.mainType) {
39526
40142
  case DouyinWorkMainType.ARTICLE: {
39527
40143
  const articleInfo = Detail_Data.article_info;
@@ -39545,7 +40161,7 @@ async function renderWorkImage(options) {
39545
40161
  关注: Count(user.following_count),
39546
40162
  粉丝: Count(user.follower_count),
39547
40163
  share_url: shareLink
39548
- }, renderOpts);
40164
+ });
39549
40165
  }
39550
40166
  case DouyinWorkMainType.VIDEO: return await Render(e, "douyin/video-work", {
39551
40167
  image_url: coverUrl,
@@ -39575,7 +40191,7 @@ async function renderWorkImage(options) {
39575
40191
  关注: Count(user.following_count),
39576
40192
  dynamicTYPE: dynamicTypeLabel,
39577
40193
  cooperation_info: cooperationInfo
39578
- }, renderOpts);
40194
+ });
39579
40195
  case DouyinWorkMainType.IMAGE: {
39580
40196
  const cover = Detail_Data.images?.[0]?.url_list[2] ?? Detail_Data.images?.[0]?.url_list[1] ?? coverUrl;
39581
40197
  const rawDesc = Detail_Data.desc ?? "";
@@ -39604,7 +40220,7 @@ async function renderWorkImage(options) {
39604
40220
  关注: Count(user.following_count),
39605
40221
  dynamicTYPE: dynamicTypeLabel,
39606
40222
  cooperation_info: cooperationInfo
39607
- }, renderOpts);
40223
+ });
39608
40224
  }
39609
40225
  default: return [];
39610
40226
  }
@@ -39616,7 +40232,7 @@ async function renderWorkImage(options) {
39616
40232
  * @returns 渲染后的图片元素数组
39617
40233
  */
39618
40234
  async function renderFavoriteImage(options) {
39619
- const { e, Detail_Data, create_time, shareLink, remark, skipWatermark = false } = options;
40235
+ const { e, Detail_Data, create_time, shareLink, remark } = options;
39620
40236
  const coverUrl = getWorkCoverUrl(getWorkTypeInfo(Detail_Data), Detail_Data);
39621
40237
  const authorUserInfo = Detail_Data.author_user_info;
39622
40238
  const subscriberUser = Detail_Data.user_info.data.user;
@@ -39636,7 +40252,7 @@ async function renderFavoriteImage(options) {
39636
40252
  author_avatar: authorUserInfo ? cdnAvatar(authorUserInfo.data.user.avatar_larger.uri) : Detail_Data.author.avatar_thumb.url_list[0],
39637
40253
  author_douyin_id: authorUserInfo ? douyinId(authorUserInfo.data.user) : douyinId(Detail_Data.author),
39638
40254
  share_url: shareLink
39639
- }, skipWatermark ? { skipWatermark: true } : void 0);
40255
+ });
39640
40256
  }
39641
40257
  /**
39642
40258
  * 渲染推荐列表推送图片
@@ -39645,7 +40261,7 @@ async function renderFavoriteImage(options) {
39645
40261
  * @returns 渲染后的图片元素数组
39646
40262
  */
39647
40263
  async function renderRecommendImage(options) {
39648
- const { e, Detail_Data, create_time, shareLink, remark, skipWatermark = false } = options;
40264
+ const { e, Detail_Data, create_time, shareLink, remark } = options;
39649
40265
  const coverUrl = getWorkCoverUrl(getWorkTypeInfo(Detail_Data), Detail_Data);
39650
40266
  const authorUserInfo = Detail_Data.author_user_info;
39651
40267
  const recommenderUser = Detail_Data.user_info.data.user;
@@ -39665,7 +40281,7 @@ async function renderRecommendImage(options) {
39665
40281
  author_avatar: authorUserInfo ? cdnAvatar(authorUserInfo.data.user.avatar_larger.uri) : Detail_Data.author.avatar_thumb.url_list[0],
39666
40282
  author_douyin_id: authorUserInfo ? douyinId(authorUserInfo.data.user) : douyinId(Detail_Data.author),
39667
40283
  share_url: shareLink
39668
- }, skipWatermark ? { skipWatermark: true } : void 0);
40284
+ });
39669
40285
  }
39670
40286
  /**
39671
40287
  * 渲染直播状态推送图片
@@ -39675,7 +40291,7 @@ async function renderRecommendImage(options) {
39675
40291
  * @returns 渲染后的图片元素数组
39676
40292
  */
39677
40293
  async function renderLiveImage(options) {
39678
- const { e, Detail_Data, skipWatermark = false } = options;
40294
+ const { e, Detail_Data } = options;
39679
40295
  const dynamicTypeLabel = options.dynamicTypeLabel ?? "直播动态推送";
39680
40296
  const user = Detail_Data.user_info.data.user;
39681
40297
  if (!Detail_Data.room_data || !Detail_Data.live_data) return [];
@@ -39704,7 +40320,7 @@ async function renderLiveImage(options) {
39704
40320
  following_count: Count(user.following_count || 0),
39705
40321
  total_favorited: Count(user.total_favorited || 0),
39706
40322
  has_commerce_goods: liveItem.has_commerce_goods || false
39707
- }, skipWatermark ? { skipWatermark: true } : void 0);
40323
+ });
39708
40324
  }
39709
40325
  //#endregion
39710
40326
  //#region src/platform/douyin/types.ts
@@ -40968,20 +41584,11 @@ var DouYinpush = class extends Base {
40968
41584
  this.injectBotToEventForRender(pushItem.targets);
40969
41585
  if (!skip) iddata = await getDouyinID(this.e, shareUrl, false);
40970
41586
  if (!skip) {
40971
- const realUrl = pushItem.pushType !== "live" && workTypeInfo.isVideo && Config.douyin.push.shareType === "web" && await new Network({
40972
- url: pushItem.Detail_Data.share_url,
40973
- headers: {
40974
- "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
40975
- Accept: "*/*",
40976
- "Accept-Encoding": "gzip, deflate, br",
40977
- Connection: "keep-alive"
40978
- }
40979
- }).getLocation();
40980
41587
  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];
40981
41588
  let workShareLink;
40982
41589
  if (pushItem.pushType !== "live") if (workTypeInfo.isArticle) workShareLink = `https://www.douyin.com/article/${actualAwemeId}`;
40983
41590
  else if (workTypeInfo.isImage) workShareLink = `https://www.douyin.com/note/${actualAwemeId}`;
40984
- else if (Config.douyin.push.shareType === "web") workShareLink = realUrl || `https://www.douyin.com/video/${actualAwemeId}`;
41591
+ else if (Config.douyin.push.shareType === "web") workShareLink = `https://www.douyin.com/video/${actualAwemeId}`;
40985
41592
  else {
40986
41593
  const playAddr = selectedVideo?.play_addr ?? pushItem.Detail_Data.video?.play_addr;
40987
41594
  workShareLink = playAddr ? buildDouyinPlayUrl(playAddr) : `https://www.douyin.com/video/${actualAwemeId}`;
@@ -40992,7 +41599,6 @@ var DouYinpush = class extends Base {
40992
41599
  img = await renderLiveImage({
40993
41600
  e: this.e,
40994
41601
  Detail_Data: pushItem.Detail_Data,
40995
- skipWatermark: true,
40996
41602
  dynamicTypeLabel: "直播动态推送"
40997
41603
  });
40998
41604
  break;
@@ -41002,8 +41608,7 @@ var DouYinpush = class extends Base {
41002
41608
  Detail_Data: pushItem.Detail_Data,
41003
41609
  create_time: pushItem.create_time,
41004
41610
  shareLink: workShareLink,
41005
- remark: pushItem.remark,
41006
- skipWatermark: true
41611
+ remark: pushItem.remark
41007
41612
  });
41008
41613
  break;
41009
41614
  case "recommend":
@@ -41012,8 +41617,7 @@ var DouYinpush = class extends Base {
41012
41617
  Detail_Data: pushItem.Detail_Data,
41013
41618
  create_time: pushItem.create_time,
41014
41619
  shareLink: workShareLink,
41015
- remark: pushItem.remark,
41016
- skipWatermark: true
41620
+ remark: pushItem.remark
41017
41621
  });
41018
41622
  break;
41019
41623
  default:
@@ -41022,8 +41626,7 @@ var DouYinpush = class extends Base {
41022
41626
  Detail_Data: pushItem.Detail_Data,
41023
41627
  create_time: pushItem.create_time,
41024
41628
  shareLink: workShareLink,
41025
- videoSource: selectedVideo,
41026
- skipWatermark: true
41629
+ videoSource: selectedVideo
41027
41630
  });
41028
41631
  break;
41029
41632
  }
@@ -41037,7 +41640,7 @@ var DouYinpush = class extends Base {
41037
41640
  const eventWithBot = this.e;
41038
41641
  eventWithBot.bot = bot;
41039
41642
  eventWithBot.selfId = botId;
41040
- const watermarkedImg = img ? applyWatermarkToImages(img, this.e) : [];
41643
+ const pushImg = img ?? [];
41041
41644
  const parseButton = bot?.adapter?.name === "QQ Official Bot" && pushItem.pushType !== "live" && pushItem.Detail_Data.share_url ? [segment.button([{
41042
41645
  text: "解析",
41043
41646
  callback: true,
@@ -41047,7 +41650,7 @@ var DouYinpush = class extends Base {
41047
41650
  callback: true,
41048
41651
  data: `#kkk帮助`
41049
41652
  }])] : [];
41050
- status = await karin.sendMsg(botId, Contact, [...watermarkedImg, ...parseButton]);
41653
+ status = await karin.sendMsg(botId, Contact, [...pushImg, ...parseButton]);
41051
41654
  if (pushItem.pushType === "live" && "room_data" in pushItem.Detail_Data && status.message_id) await douyinDBInstance.updateLiveStatus(pushItem.sec_uid, true);
41052
41655
  if (pushItem.pushType !== "live" && Config.douyin.push.parsedynamic && status.message_id) {
41053
41656
  const Detail_Data = pushItem.Detail_Data;
@@ -42624,7 +43227,10 @@ var douyinLogin = async (e) => {
42624
43227
  };
42625
43228
  const validFor = qrcode.data.expires_in;
42626
43229
  logger.mark(`[抖音登录] 二维码已获取,有效期 ${validFor} 秒`);
42627
- const rendered = await Render(e, "douyin/qrcodeImg", { share_url: qrcode.data.content });
43230
+ const rendered = await Render(e, "douyin/qrcodeImg", {
43231
+ share_url: qrcode.data.content,
43232
+ avatarUrl: await resolveTriggerAvatarUrl(e)
43233
+ });
42628
43234
  const base64Data = rendered[0]?.file;
42629
43235
  if (!base64Data) throw new Error("生成二维码图片失败");
42630
43236
  fs.writeFileSync(`${Common.tempDri.default}DouyinLoginQrcode.png`, Buffer.from(base64Data.replace(/^base64:\/\//, ""), "base64"));
@@ -43353,7 +43959,8 @@ var handleQrLogin = wrapWithErrorHandler(async (e) => {
43353
43959
  port,
43354
43960
  authKey
43355
43961
  }),
43356
- serverUrl: `${protocol}://${host}:${port}`
43962
+ serverUrl: `${protocol}://${host}:${port}`,
43963
+ avatarUrl: await resolveTriggerAvatarUrl(e)
43357
43964
  });
43358
43965
  await karin.sendMaster(e.selfId, userId, images);
43359
43966
  if (e.isGroup) await e.reply("登录二维码已私聊发送,请查收~");
@@ -44774,4 +45381,4 @@ mkdirSync(`${karinPathBase}/${Root.pluginName}/data`);
44774
45381
  mkdirSync(Common.tempDri.images);
44775
45382
  mkdirSync(Common.tempDri.video);
44776
45383
  //#endregion
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 };
45384
+ export { createViewPictureNode as $, KuaishouEmojiParamsSchema as $n, emitHttpResponse as $r, AdditionalType as $t, testWrapWithErrorHandler as A, douyinFetcher$1 as An, BilibiliBangumiInfoParamsSchema as Ar, DouyinApiRoutes as At, createImageNode as B, bilibiliFetcher$1 as Bn, BilibiliLoginParamsSchema as Br, BilibiliMethodToFetcher as Bt, changelog as C, isNetworkErrorResult as Cn, DouyinValidationSchemas as Cr, reloadAmagiConfig as Ct, dylogin as D, kuaishouFetcher$1 as Dn, BilibiliArticleInfoParamsSchema as Dr, amagi as Dt, biLogin as E, createBoundKuaishouFetcher as En, BilibiliArticleCardParamsSchema as Er, CreateApp as Et, createCodeBlockNode as F, isSmsCodeVerifyWay as Fn, BilibiliCommentReplyParamsSchema as Fr, XiaohongshuMethodMapping as Ft, createLotteryNode as G, validateDouyinParams as Gn, BilibiliValidateCaptchaParamsSchema as Gr, KuaishouInternalMethods as Gt, createLinkCardNode as H, createErrorResponse as Hn, BilibiliQrcodeParamsSchema as Hr, DouyinInternalMethods as Ht, createEmojiNode as I, requestPassportQrcode as In, BilibiliDanmakuParamsSchema as Ir, getApiRoute as It, createParagraphNode as J, XiaohongshuMethodRoutes as Jn, BilibiliVideoParamsSchema as Jr, XiaohongshuFetcherMethods as Jt, createMentionNode as K, validateKuaishouParams as Kn, BilibiliValidationSchemas as Kr, KuaishouMethodToFetcher as Kt, createHashtagNode as L, sendPassportVerifyCode as Ln, BilibiliDynamicParamsSchema as Lr, getEnglishMethodName as Lt, renderRichTextToReact as M, douyinApiUrls as Mn, BilibiliBv2AvParamsSchema as Mr, KuaishouApiRoutes as Mt, createAtNode as N, douyinSign as Nn, BilibiliColumnInfoParamsSchema as Nr, KuaishouMethodMapping as Nt, removeOldFiles as O, kuaishouSign as On, BilibiliArticleParamsSchema as Or, BilibiliApiRoutes as Ot, createBlockquoteNode as P, checkPassportQrcode as Pn, BilibiliCommentParamsSchema as Pr, XiaohongshuApiRoutes as Pt, createTopicNode as Q, KuaishouCommentParamsSchema as Qn, emitHttpRequest as Qr, DynamicType as Qt, createHeadingNode as R, validatePassportVerifyCode as Rn, BilibiliEmojiParamsSchema as Rr, BilibiliFetcherMethods as Rt, setdyPush as S, getHeadersAndData as Sn, DouyinUserParamsSchema as Sr, registerAmagiReloadListener as St, version as T, xiaohongshuFetcher$1 as Tn, BilibiliApplyCaptchaParamsSchema as Tr, xiaohongshuFetcher as Tt, createListItemNode as U, createSuccessResponse$1 as Un, BilibiliQrcodeStatusParamsSchema as Ur, DouyinMethodToFetcher as Ut, createLineBreakNode as V, createBoundBilibiliFetcher as Vn, BilibiliMethodRoutes as Vr, DouyinFetcherMethods as Vt, createListNode as W, validateBilibiliParams as Wn, BilibiliUserParamsSchema as Wr, KuaishouFetcherMethods as Wt, createSearchKeywordNode as X, xiaohongshuApiUrls as Xn, emitApiError as Xr, XiaohongshuMethodToFetcher as Xt, createRichTextDocument as Y, XiaohongshuValidationSchemas as Yn, amagiEvents as Yr, XiaohongshuInternalMethods as Yt, createTextNode as Z, xiaohongshuSign as Zn, emitApiSuccess as Zr, toFetcherMethod as Zt, douyinPush as _, av2bv as _n, DouyinMethodRoutes as _r, AmagiError as _t, bilibiliAPP as a, emitLogWarn as ai, kuaishouUtils as an, KuaishouVideoParamsSchema as ar, webConfig as at, globalIgnore as b, fetchData as bn, DouyinSearchParamsSchema as br, douyinFetcher as bt, prefix as c, bilibiliApiUrls as ci, createDouyinRoutes as cn, DouyinDanmakuParamsSchema as cr, douyinDBInstance as ct, globalStatistics as d, ApiError as dn, DouyinEmojiResourceParamsSchema as dr, getStatisticsDB as dt, emitLog as ei, MajorType as en, KuaishouLiveRoomInfoParamsSchema as er, createVoteNode as et, groupStatistics as f, ValidationError as fn, DouyinGuestMusicListParamsSchema as fr, initAllDatabases as ft, changeBotID as g, parseDmSegMobileReply as gn, DouyinLiveRoomParamsSchema as gr, AmagiBase as gt, bilibiliPushList as h, wbi_sign as hn, DouyinHotWordsParamsSchema as hr, BilibiliDBBase as ht, update as i, emitLogMark as ii, createXiaohongshuRoutes as in, KuaishouValidationSchemas as ir, renderVideoPreviewPage as it, templates as j, createBoundDouyinFetcher as jn, BilibiliBangumiStreamParamsSchema as jr, DouyinMethodMapping as jt, task as k, kuaishouApiUrls as kn, BilibiliAv2BvParamsSchema as kr, BilibiliMethodMapping as kt, xiaohongshuAPP as l, Root as li, bilibiliUtils as ln, DouyinEmojiListParamsSchema as lr, getBilibiliDB as lt, bilibiliPush as m, bilibiliErrorCodeMap as mn, DouyinGuestUserParamsSchema as mr, DouyinDBBase as mt, kkkUpdateCommand as n, emitLogError as ni, createAmagiClient as nn, KuaishouUserProfileParamsSchema as nr, extractRichTextPlainText as nt, douyinAPP as o, emitNetworkError as oi, createKuaishouRoutes as on, DouyinCommentParamsSchema as or, bilibiliDBInstance as ot, qrLogin as p, handleError as pn, DouyinGuestMusicParamsSchema as pr, StatisticsDBBase as pt, createOpusLinkNode as q, validateXiaohongshuParams as qn, BilibiliVideoDownloadParamsSchema as qr, MethodMaps as qt, kkkUpdateTest as r, emitLogInfo as ri, xiaohongshuUtils as rn, KuaishouUserWorkListParamsSchema as rr, normalizeRichTextNodes as rt, kuaishouAPP as s, emitNetworkRetry as si, douyinUtils as sn, DouyinCommentReplyParamsSchema as sr, cleanOldDynamicCache as st, kkkUpdate as t, emitLogDebug as ti, CommentType as tn, KuaishouMethodRoutes as tr, createWebLinkNode as tt, testPush as u, createBilibiliRoutes as un, DouyinEmojiProParamsSchema as ur, getDouyinDB as ut, douyinPushList as v, bv2av as vn, DouyinMusicParamsSchema as vr, SOFT_ERROR_CODES as vt, help as w, createBoundXiaohongshuFetcher as wn, DouyinWorkParamsSchema as wr, softFetch as wt, setbiliPush as x, fetchResponse as xn, DouyinUserListParamsSchema as xr, kuaishouFetcher as xt, forcePush as y, qtparam as yn, DouyinQrcodeParamsSchema as yr, bilibiliFetcher as yt, createHorizontalRuleNode as z, passport_exports as zn, BilibiliLiveParamsSchema as zr, BilibiliInternalMethods as zt };