karin-plugin-kkk 2.45.2-beta.0 → 2.45.2-beta.2

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.
@@ -7,7 +7,7 @@ import fs from "node:fs";
7
7
  import path, { resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import karin, { BOT_CONNECT, app, authMiddleware, checkPkgUpdate, common, config, createBadRequestResponse, createNotFoundResponse, createServerErrorResponse, createSuccessResponse, db, defineConfig, exec, ffmpeg, ffprobe, getPkgVersion, getRemotePkgVersion, hooks, isDocker, karin as karin$1, karinPathHtml, logger, logs, mkdirSync, parseChangelog, range, render, restart, segment, updatePkg, watch } from "node-karin";
10
- import crypto from "node:crypto";
10
+ import crypto, { randomBytes } from "node:crypto";
11
11
  import { deflateSync, inflateSync } from "node:zlib";
12
12
  import express from "node-karin/express";
13
13
  import axios, { AxiosError } from "node-karin/axios";
@@ -1113,6 +1113,7 @@ var Common = new class Tools {
1113
1113
  unit = 1;
1114
1114
  }
1115
1115
  }
1116
+ if (unit > 1) temp += unit;
1116
1117
  return result + temp;
1117
1118
  }
1118
1119
  /**
@@ -7644,6 +7645,7 @@ var templateFonts = (() => {
7644
7645
  var SMART_THEME_PATHS = /* @__PURE__ */ new Set([
7645
7646
  "douyin/video-work",
7646
7647
  "douyin/image-work",
7648
+ "douyin/mix",
7647
7649
  "bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD"
7648
7650
  ]);
7649
7651
  /**
@@ -7653,7 +7655,7 @@ var SMART_THEME_PATHS = /* @__PURE__ */ new Set([
7653
7655
  */
7654
7656
  var getCoverUrl$1 = (path, data) => {
7655
7657
  if (!data || typeof data !== "object") return "";
7656
- if (path === "douyin/video-work" || path === "bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD") {
7658
+ if (path === "douyin/video-work" || path === "douyin/mix" || path === "bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD") {
7657
7659
  const imageUrl = data.image_url;
7658
7660
  return typeof imageUrl === "string" ? imageUrl : "";
7659
7661
  }
@@ -11789,6 +11791,8 @@ var DecorationCard = ({ data }) => {
11789
11791
  };
11790
11792
  //#endregion
11791
11793
  //#region ktr/template/bilibili/bangumi/components/bangumi.tsx
11794
+ /** 海报每页展示的集数,和 core 的 EPISODE_SELECTION_PAGE_SIZE 保持一致 */
11795
+ var DEFAULT_POSTER_PAGE_SIZE$1 = 15;
11792
11796
  /**
11793
11797
  * 格式化数字显示
11794
11798
  * @param num 要格式化的数字
@@ -11812,20 +11816,6 @@ var formatDateParts = (timestamp) => {
11812
11816
  };
11813
11817
  };
11814
11818
  /**
11815
- * 格式化完整日期时间
11816
- * @param timestamp 时间戳
11817
- * @returns 格式化的日期时间字符串
11818
- */
11819
- var formatDateTime = (timestamp) => {
11820
- return (/* @__PURE__ */ new Date(timestamp * 1e3)).toLocaleDateString("zh-CN", {
11821
- year: "numeric",
11822
- month: "short",
11823
- day: "numeric",
11824
- hour: "2-digit",
11825
- minute: "2-digit"
11826
- });
11827
- };
11828
- /**
11829
11819
  * B站番剧头部组件
11830
11820
  * @param props 头部组件属性
11831
11821
  */
@@ -11837,102 +11827,88 @@ var BangumiBilibiliHeader = (props) => {
11837
11827
  className: "flex relative z-10 gap-25 p-25",
11838
11828
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11839
11829
  className: "flex flex-col shrink-0 gap-20",
11840
- children: [
11841
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11842
- className: "overflow-hidden rounded-4xl w-120 h-160",
11843
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11844
- src: props.mainCover,
11845
- alt: props.title,
11846
- className: "object-cover w-full h-full select-text"
11847
- })
11848
- }),
11849
- props.upInfo && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11850
- className: "flex gap-12 items-center mt-15",
11851
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11852
- className: "relative",
11853
- children: [
11854
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11855
- className: "w-28 h-28 rounded-full select-text",
11856
- src: `https://images.weserv.nl/?url=${encodeURIComponent(props.upInfo.avatar)}`,
11857
- alt: props.upInfo.uname
11858
- }),
11859
- props.upInfo.avatar_subscript_url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11860
- className: "absolute -right-1 -bottom-1 w-8 h-8 select-text",
11861
- src: props.upInfo.avatar_subscript_url,
11862
- alt: "头像角标"
11863
- }),
11864
- props.upInfo.pendant?.image && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11865
- className: "absolute inset-0 w-full h-full transform select-text scale-160",
11866
- src: props.upInfo.pendant.image,
11867
- alt: props.upInfo.pendant.name || "挂件"
11868
- })
11869
- ]
11870
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11871
- className: "flex flex-col gap-4",
11872
- children: [
11873
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11874
- className: "flex gap-3 items-center",
11875
- children: [
11876
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11877
- className: "text-4xl font-medium select-text",
11878
- style: { color: props.upInfo.vip_status === 1 ? props.upInfo.nickname_color || "#FB7299" : "#EDEDED" },
11879
- children: props.upInfo.uname
11880
- }),
11881
- props.upInfo.verify_type > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11882
- className: "flex items-center",
11883
- children: props.upInfo.verify_type === 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shield, {
11884
- width: 20,
11885
- className: "text-warning"
11886
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Crown, {
11887
- width: 20,
11888
- className: "text-accent"
11889
- })
11890
- }),
11891
- props.upInfo.vip_status === 1 && props.upInfo.vip_label?.text && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11892
- size: "sm",
11893
- className: "px-2 py-1 text-xs select-text",
11894
- style: {
11895
- backgroundColor: props.upInfo.vip_label.bg_color || "#FB7299",
11896
- color: props.upInfo.vip_label.text_color || "#FFFFFF",
11897
- borderColor: props.upInfo.vip_label.border_color || "transparent"
11898
- },
11899
- children: props.upInfo.vip_label.text
11900
- })
11901
- ]
11902
- }),
11903
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11904
- className: "flex gap-6 items-center text-3xl select-text text-foreground",
11905
- children: [
11906
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Users, { size: 30 }),
11907
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [formatNumber$2(props.upInfo.follower), "粉丝"] }),
11908
- props.upInfo.is_follow === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11909
- size: "sm",
11910
- color: "accent",
11911
- variant: "soft",
11912
- className: "text-xs select-text",
11913
- children: "已关注"
11914
- })
11915
- ]
11916
- }),
11917
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11918
- className: "flex gap-2 items-center text-2xl select-text text-foreground/70",
11919
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Hash, { size: 20 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: ["UID: ", props.upInfo.mid] })]
11920
- })
11921
- ]
11922
- })]
11923
- }),
11924
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11925
- className: "flex text-3xl select-text text-foreground",
11830
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11831
+ className: "overflow-hidden rounded-4xl w-120 h-160",
11832
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11833
+ src: props.mainCover,
11834
+ alt: props.title,
11835
+ className: "object-cover w-full h-full select-text"
11836
+ })
11837
+ }), props.upInfo && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11838
+ className: "flex gap-12 items-center mt-15",
11839
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11840
+ className: "relative",
11926
11841
  children: [
11927
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "提示:请在120秒内发送" }),
11928
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
11929
- className: "inline-flex items-center rounded-xl bg-danger/12 px-4 py-1.5 text-[0.95em] font-semibold text-danger",
11930
- children: "第 ? 集"
11842
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11843
+ className: "w-28 h-28 rounded-full select-text",
11844
+ src: `https://images.weserv.nl/?url=${encodeURIComponent(props.upInfo.avatar)}`,
11845
+ alt: props.upInfo.uname
11846
+ }),
11847
+ props.upInfo.avatar_subscript_url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11848
+ className: "absolute -right-1 -bottom-1 w-8 h-8 select-text",
11849
+ src: props.upInfo.avatar_subscript_url,
11850
+ alt: "头像角标"
11931
11851
  }),
11932
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "选择集数" })
11852
+ props.upInfo.pendant?.image && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11853
+ className: "absolute inset-0 w-full h-full transform select-text scale-160",
11854
+ src: props.upInfo.pendant.image,
11855
+ alt: props.upInfo.pendant.name || "挂件"
11856
+ })
11933
11857
  ]
11934
- })
11935
- ]
11858
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11859
+ className: "flex flex-col gap-4",
11860
+ children: [
11861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11862
+ className: "flex gap-3 items-center",
11863
+ children: [
11864
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11865
+ className: "text-4xl font-medium select-text",
11866
+ style: { color: props.upInfo.vip_status === 1 ? props.upInfo.nickname_color || "#FB7299" : "#EDEDED" },
11867
+ children: props.upInfo.uname
11868
+ }),
11869
+ props.upInfo.verify_type > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11870
+ className: "flex items-center",
11871
+ children: props.upInfo.verify_type === 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shield, {
11872
+ width: 20,
11873
+ className: "text-warning"
11874
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Crown, {
11875
+ width: 20,
11876
+ className: "text-accent"
11877
+ })
11878
+ }),
11879
+ props.upInfo.vip_status === 1 && props.upInfo.vip_label?.text && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11880
+ size: "sm",
11881
+ className: "px-2 py-1 text-xs select-text",
11882
+ style: {
11883
+ backgroundColor: props.upInfo.vip_label.bg_color || "#FB7299",
11884
+ color: props.upInfo.vip_label.text_color || "#FFFFFF",
11885
+ borderColor: props.upInfo.vip_label.border_color || "transparent"
11886
+ },
11887
+ children: props.upInfo.vip_label.text
11888
+ })
11889
+ ]
11890
+ }),
11891
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11892
+ className: "flex gap-6 items-center text-3xl select-text text-foreground",
11893
+ children: [
11894
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Users, { size: 30 }),
11895
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [formatNumber$2(props.upInfo.follower), "粉丝"] }),
11896
+ props.upInfo.is_follow === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11897
+ size: "sm",
11898
+ color: "accent",
11899
+ variant: "soft",
11900
+ className: "text-xs select-text",
11901
+ children: "已关注"
11902
+ })
11903
+ ]
11904
+ }),
11905
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11906
+ className: "flex gap-2 items-center text-2xl select-text text-foreground/70",
11907
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Hash, { size: 20 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: ["UID: ", props.upInfo.mid] })]
11908
+ })
11909
+ ]
11910
+ })]
11911
+ })]
11936
11912
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11937
11913
  className: "flex flex-col flex-1 justify-between text-foreground",
11938
11914
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
@@ -12055,171 +12031,132 @@ var BangumiBilibiliHeader = (props) => {
12055
12031
  })
12056
12032
  });
12057
12033
  };
12034
+ /** 文字选集提示里的命令高亮,和海报的强调色保持一致 */
12035
+ var CommandChip$1 = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12036
+ className: "inline-flex items-center rounded-xl bg-danger px-4 py-1.5 font-semibold text-danger-foreground",
12037
+ children
12038
+ });
12058
12039
  /**
12059
- * B站番剧剧集列表组件
12040
+ * B站番剧剧集列表组件。
12041
+ * 每页最多 15 集,3 列网格展示封面 + 标题;长番剧不会再生成超长截图。
12060
12042
  * @param props 剧集列表组件属性
12061
12043
  */
12062
12044
  var BangumiBilibiliEpisodes = (props) => {
12063
- const sortedEpisodes = [...props.Episodes].sort((a, b) => b.pub_time - a.pub_time);
12064
- const groupedEpisodes = sortedEpisodes.reduce((groups, episode) => {
12065
- const date = /* @__PURE__ */ new Date(episode.pub_time * 1e3);
12066
- const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
12067
- if (!groups[dateKey]) groups[dateKey] = [];
12068
- groups[dateKey].push(episode);
12069
- return groups;
12070
- }, {});
12071
- const dateKeys = Object.keys(groupedEpisodes).sort((a, b) => b.localeCompare(a));
12072
- const flattenedEpisodes = [];
12073
- dateKeys.forEach((dateIndex) => {
12074
- const episodesInDate = groupedEpisodes[dateIndex];
12075
- episodesInDate.forEach((episode, episodeIndex) => {
12076
- flattenedEpisodes.push({
12077
- episode,
12078
- isFirstOfDate: episodeIndex === 0,
12079
- isLastOfDate: episodeIndex === episodesInDate.length - 1,
12080
- isLastOfAll: false,
12081
- dateKey: dateIndex,
12082
- episodesInSameDate: episodesInDate.length
12083
- });
12084
- });
12085
- });
12086
- if (flattenedEpisodes.length > 0) flattenedEpisodes[flattenedEpisodes.length - 1].isLastOfAll = true;
12045
+ const totalEpisodes = props.length > 0 ? props.length : props.Episodes.length;
12046
+ const pageSize = Math.max(1, props.posterPageSize ?? DEFAULT_POSTER_PAGE_SIZE$1);
12047
+ const pageCount = Math.max(1, Math.ceil(totalEpisodes / pageSize));
12048
+ const page = Math.min(Math.max(1, props.posterPage ?? 1), pageCount);
12049
+ const start = (page - 1) * pageSize;
12050
+ const visibleEpisodes = props.Episodes.slice(start, start + pageSize);
12051
+ if (visibleEpisodes.length === 0) return null;
12052
+ const episodeNumbers = visibleEpisodes.map((episode, index) => episode.episode ?? start + index + 1);
12053
+ const firstEpisode = episodeNumbers[0];
12054
+ const lastEpisode = episodeNumbers[episodeNumbers.length - 1];
12055
+ const range = firstEpisode === lastEpisode ? `第 ${firstEpisode} 集` : `第 ${firstEpisode}–${lastEpisode} 集`;
12087
12056
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12088
12057
  className: "px-10",
12089
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12090
- className: "flex gap-8 items-center mb-20 text-5xl font-bold select-text text-foreground",
12091
- children: [
12092
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Play, { size: 46 }),
12093
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "剧集列表" }),
12094
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Chip, {
12095
- size: "lg",
12096
- color: "danger",
12097
- variant: "soft",
12098
- className: "h-18 px-4 py-2 text-4xl select-text",
12058
+ children: [
12059
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12060
+ className: "flex flex-wrap gap-6 items-center mb-16 text-5xl font-bold select-text text-foreground",
12061
+ children: [
12062
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Play, { size: 46 }),
12063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "剧集列表" }),
12064
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Chip, {
12065
+ size: "lg",
12066
+ color: "danger",
12067
+ variant: "soft",
12068
+ className: "h-18 px-4 py-2 text-4xl select-text",
12069
+ children: [
12070
+ "共",
12071
+ totalEpisodes,
12072
+ "集"
12073
+ ]
12074
+ }),
12075
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12076
+ className: "text-3xl font-semibold select-text text-muted",
12077
+ children: props.selectionMode === "button" ? range : `第 ${page}/${pageCount} 页 · ${range}`
12078
+ })
12079
+ ]
12080
+ }),
12081
+ props.selectionMode !== "button" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12082
+ className: "flex flex-col gap-4 mb-12 text-3xl select-text text-foreground/80",
12083
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12084
+ className: "flex flex-wrap gap-3 items-center",
12099
12085
  children: [
12100
- "共",
12101
- sortedEpisodes.length,
12102
- "集"
12086
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
12087
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip$1, { children: "第?集" }),
12088
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "选集" })
12103
12089
  ]
12104
- })
12105
- ]
12106
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: flattenedEpisodes.map((item) => {
12107
- const { episode, isFirstOfDate, isLastOfAll, episodesInSameDate, isLastOfDate } = item;
12108
- const { month, day } = formatDateParts(episode.pub_time);
12109
- const episodeNumber = sortedEpisodes.findIndex((e) => e.bvid === episode.bvid);
12110
- const actualEpisodeNumber = sortedEpisodes.length - episodeNumber;
12111
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12112
- className: "flex gap-15",
12113
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12114
- className: "flex flex-col shrink-0 items-center w-20",
12115
- children: isFirstOfDate ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
12116
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12117
- className: "text-4xl select-text text-foreground",
12118
- children: month
12090
+ }), pageCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12091
+ className: "flex flex-wrap gap-3 items-center",
12092
+ children: [
12093
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
12094
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip$1, { children: "上一页" }),
12095
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12096
+ className: "text-foreground/30",
12097
+ children: "/"
12119
12098
  }),
12120
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12121
- className: "flex justify-center items-center text-7xl font-bold select-text text-foreground",
12122
- children: day
12099
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip$1, { children: "下一页" }),
12100
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12101
+ className: "text-foreground/30",
12102
+ children: "/"
12123
12103
  }),
12124
- !isLastOfAll && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("mt-8 w-1 bg-divider", episodesInSameDate > 1 ? "h-110" : "h-95") })
12125
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
12126
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-1 h-10 bg-divider" }),
12127
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "my-2 w-4 h-4 rounded-full bg-divider" }),
12128
- (!isLastOfAll || episodesInSameDate > 1) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("w-1 bg-divider", isLastOfDate ? "h-110" : "h-130") })
12129
- ] })
12130
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12131
- className: cn("flex-1 min-w-0", !isLastOfAll && isLastOfDate && "mb-20"),
12132
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12133
- className: "flex justify-between items-center mb-10",
12104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip$1, { children: "第?页" }),
12105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "翻页" })
12106
+ ]
12107
+ })]
12108
+ }),
12109
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12110
+ className: "grid grid-cols-3 gap-8",
12111
+ children: visibleEpisodes.map((episode, index) => {
12112
+ const episodeNumber = episodeNumbers[index];
12113
+ const { month, day } = formatDateParts(episode.pub_time);
12114
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12115
+ className: "flex flex-col overflow-hidden bg-surface rounded-3xl shadow-large",
12134
12116
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12135
- className: "flex shrink-0 gap-8 items-center",
12136
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12137
- className: "relative",
12138
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
12139
- className: "w-32 h-32 rounded-full select-text",
12140
- src: `https://images.weserv.nl/?url=${encodeURIComponent(props.UPInfo ? props.UPInfo.avatar : props.mainCover)}`,
12141
- alt: ""
12117
+ className: "overflow-hidden relative aspect-video",
12118
+ children: [
12119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
12120
+ src: episode.cover,
12121
+ alt: `第${episodeNumber}集 ${episode.long_title}`,
12122
+ className: "object-cover w-full h-full select-text"
12123
+ }),
12124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12125
+ className: "absolute top-4 left-4 px-3 py-1 text-2xl font-bold text-white rounded-xl bg-black/65",
12126
+ children: [
12127
+ "第",
12128
+ episodeNumber,
12129
+ "集"
12130
+ ]
12131
+ }),
12132
+ episode.badge && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
12133
+ size: "sm",
12134
+ className: "absolute top-4 right-4 px-2 py-1 text-lg font-medium select-text",
12135
+ style: {
12136
+ backgroundColor: episode.badge_info?.bg_color || "#FB7299",
12137
+ color: "#FFFFFF"
12138
+ },
12139
+ children: episode.badge_info?.text || episode.badge
12142
12140
  })
12143
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12144
- className: "flex flex-col gap-6",
12145
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12146
- className: "text-4xl font-bold select-text text-foreground/80",
12147
- children: props.UPInfo ? props.UPInfo.uname : props.Title
12148
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12149
- className: "flex gap-4 items-center text-3xl select-text text-foreground/70",
12150
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Calendar, { size: 30 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发布了内容" })]
12151
- })]
12152
- })]
12153
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12154
- className: "shrink-0 pr-20",
12155
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12156
- className: "text-5xl font-semibold select-text text-foreground/70",
12141
+ ]
12142
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12143
+ className: "flex flex-col flex-1 gap-3 p-5",
12144
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12145
+ className: "text-3xl font-semibold leading-snug select-text text-foreground line-clamp-2",
12146
+ children: episode.long_title || `第${episodeNumber}集`
12147
+ }), episode.pub_time > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12148
+ className: "mt-auto text-2xl select-text text-foreground/60",
12157
12149
  children: [
12158
- "第",
12159
- actualEpisodeNumber,
12160
- "集"
12150
+ month,
12151
+ day,
12152
+ "日"
12161
12153
  ]
12162
- })
12163
- })]
12164
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12165
- className: "overflow-hidden shadow-large bg-surface rounded-4xl",
12166
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12167
- className: "flex gap-12 p-12",
12168
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12169
- className: "relative shrink-0",
12170
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12171
- className: "overflow-hidden relative h-64 rounded-3xl w-md",
12172
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
12173
- src: episode.cover,
12174
- alt: `第${actualEpisodeNumber}集 ${episode.long_title}`,
12175
- className: "object-cover w-full h-full select-text"
12176
- }), episode.badge && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
12177
- size: "lg",
12178
- className: "absolute top-6 right-8 rounded-2xl py-1 text-lg font-medium select-text scale-150",
12179
- style: {
12180
- backgroundColor: episode.badge_info?.bg_color || "#FB7299",
12181
- color: "#FFFFFF"
12182
- },
12183
- children: episode.badge_info?.text || episode.badge
12184
- })]
12185
- })
12186
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12187
- className: "flex flex-col flex-1 justify-center h-64",
12188
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12189
- className: "mb-8 text-5xl font-semibold select-text text-foreground line-clamp-2",
12190
- children: episode.long_title
12191
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12192
- className: "space-y-4 text-4xl",
12193
- children: [
12194
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12195
- className: "flex gap-6 items-center select-text text-foreground/70",
12196
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Hash, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12197
- className: "truncate",
12198
- children: episode.bvid
12199
- })]
12200
- }),
12201
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12202
- className: "flex gap-6 items-center select-text text-foreground/70",
12203
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Clock, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12204
- className: "whitespace-nowrap",
12205
- children: formatDateTime(episode.pub_time)
12206
- })]
12207
- }),
12208
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12209
- className: "flex gap-6 items-center select-text text-foreground/70",
12210
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Share2, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12211
- className: "truncate",
12212
- children: episode.link
12213
- })]
12214
- })
12215
- ]
12216
- })]
12217
12154
  })]
12218
- })
12219
- })]
12220
- })]
12221
- }, episode.bvid);
12222
- }) })]
12155
+ })]
12156
+ }, `${episodeNumber}-${episode.bvid}`);
12157
+ })
12158
+ })
12159
+ ]
12223
12160
  });
12224
12161
  };
12225
12162
  var BangumiBilibili = import_react.memo((props) => {
@@ -16112,7 +16049,7 @@ var DouyinMoreIcon = ({ size = 36, ...props }) => /* @__PURE__ */ (0, import_jsx
16112
16049
  });
16113
16050
  //#endregion
16114
16051
  //#region ktr/template/douyin/article-work/components/ArticleWork.tsx
16115
- var getTitleClassName$2 = (titleLength) => {
16052
+ var getTitleClassName$3 = (titleLength) => {
16116
16053
  if (titleLength > 96) return "text-[42px] leading-[1.34]";
16117
16054
  if (titleLength > 72) return "text-[48px] leading-[1.3]";
16118
16055
  if (titleLength > 48) return "text-[54px] leading-[1.26]";
@@ -16271,7 +16208,7 @@ var DouyinPosterHeader$2 = ({ data, ctx }) => {
16271
16208
  var DouyinArticleTitle = ({ data }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", {
16272
16209
  className: "mt-13",
16273
16210
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
16274
- className: `${getTitleClassName$2(data.title.length)} font-bold text-foreground select-text`,
16211
+ className: `${getTitleClassName$3(data.title.length)} font-bold text-foreground select-text`,
16275
16212
  style: {
16276
16213
  wordBreak: "break-word",
16277
16214
  overflowWrap: "break-word"
@@ -17749,7 +17686,7 @@ var imageMediaTypeMeta = {
17749
17686
  icon: o$3
17750
17687
  }
17751
17688
  };
17752
- var getTitleClassName$1 = (titleLength) => {
17689
+ var getTitleClassName$2 = (titleLength) => {
17753
17690
  if (titleLength > 96) return "text-[42px] leading-[1.34]";
17754
17691
  if (titleLength > 72) return "text-[48px] leading-[1.3]";
17755
17692
  if (titleLength > 48) return "text-[54px] leading-[1.26]";
@@ -17871,7 +17808,7 @@ var DouyinPosterTitle$1 = ({ data }) => {
17871
17808
  const { title, desc } = data;
17872
17809
  const hasTitle = Boolean(title?.nodes.length);
17873
17810
  const hasDesc = Boolean(desc?.nodes.length);
17874
- const titleClassName = getTitleClassName$1(title ? extractRichTextPlainText(title).length : 0);
17811
+ const titleClassName = getTitleClassName$2(title ? extractRichTextPlainText(title).length : 0);
17875
17812
  const richTextOptions = {
17876
17813
  hashtag: { className: cn("text-inherit opacity-60", !hasTitle && "text-6xl") },
17877
17814
  mention: { className: "text-inherit" }
@@ -18597,6 +18534,418 @@ var live_default = defineTemplate({
18597
18534
  validate: (data) => typeof data === "object" && data !== null && typeof data.share_url === "string"
18598
18535
  });
18599
18536
  //#endregion
18537
+ //#region ktr/template/douyin/mix/components/Mix.tsx
18538
+ /** 海报每页展示的集数,和 core 的 EPISODE_SELECTION_PAGE_SIZE 保持一致 */
18539
+ var DEFAULT_POSTER_PAGE_SIZE = 15;
18540
+ /** 万/亿 计数,数值与单位分开返回:数字用大号等宽,单位用小号 */
18541
+ var splitCount = (count) => {
18542
+ if (count >= 1e8) return {
18543
+ value: (count / 1e8).toFixed(1),
18544
+ unit: "亿"
18545
+ };
18546
+ if (count >= 1e4) return {
18547
+ value: (count / 1e4).toFixed(1),
18548
+ unit: "万"
18549
+ };
18550
+ return {
18551
+ value: count.toString(),
18552
+ unit: ""
18553
+ };
18554
+ };
18555
+ /** 总时长:不到一小时按分钟,否则按小时保留一位 */
18556
+ var splitRuntime = (milliseconds) => {
18557
+ const minutes = Math.round(milliseconds / 6e4);
18558
+ if (minutes < 60) return {
18559
+ value: minutes.toString(),
18560
+ unit: "分钟"
18561
+ };
18562
+ return {
18563
+ value: (minutes / 60).toFixed(1),
18564
+ unit: "小时"
18565
+ };
18566
+ };
18567
+ /** 单集时长 mm:ss */
18568
+ var formatDuration$4 = (milliseconds) => {
18569
+ const totalSeconds = Math.round(milliseconds / 1e3);
18570
+ const minutes = Math.floor(totalSeconds / 60);
18571
+ const seconds = totalSeconds % 60;
18572
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
18573
+ };
18574
+ /** 标题按字数分档:短标题撑满,长标题收小并最多三行 */
18575
+ var getTitleClassName$1 = (titleLength) => {
18576
+ if (titleLength <= 8) return "text-[112px] leading-[1.05]";
18577
+ if (titleLength <= 14) return "text-[96px] leading-[1.08]";
18578
+ if (titleLength <= 22) return "text-[76px] leading-[1.12]";
18579
+ return "text-[60px] leading-[1.18]";
18580
+ };
18581
+ /** 弥散背景:海报模糊铺底 + 单色离散杂色 */
18582
+ var MixBackground = ({ data, ctx }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18583
+ className: "pointer-events-none absolute inset-0 overflow-hidden select-none",
18584
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AmbientCover, {
18585
+ src: data.image_url,
18586
+ ctx
18587
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18588
+ className: "absolute inset-0 opacity-[0.35] mix-blend-overlay dark:mix-blend-soft-light",
18589
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
18590
+ className: "h-full w-full",
18591
+ xmlns: "http://www.w3.org/2000/svg",
18592
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("filter", {
18593
+ id: "douyinMixNoise",
18594
+ children: [
18595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feTurbulence", {
18596
+ type: "fractalNoise",
18597
+ baseFrequency: "0.8",
18598
+ numOctaves: "2",
18599
+ stitchTiles: "stitch"
18600
+ }),
18601
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feColorMatrix", {
18602
+ type: "saturate",
18603
+ values: "0"
18604
+ }),
18605
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("feComponentTransfer", { children: [
18606
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feFuncR", {
18607
+ type: "discrete",
18608
+ tableValues: "0 1"
18609
+ }),
18610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feFuncG", {
18611
+ type: "discrete",
18612
+ tableValues: "0 1"
18613
+ }),
18614
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feFuncB", {
18615
+ type: "discrete",
18616
+ tableValues: "0 1"
18617
+ })
18618
+ ] }),
18619
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feComponentTransfer", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feFuncA", {
18620
+ type: "linear",
18621
+ slope: "2",
18622
+ intercept: "-0.5"
18623
+ }) })
18624
+ ]
18625
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", {
18626
+ width: "100%",
18627
+ height: "100%",
18628
+ filter: "url(#douyinMixNoise)",
18629
+ fill: "white"
18630
+ })]
18631
+ })
18632
+ })]
18633
+ });
18634
+ var MixHeader = ({ ctx }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
18635
+ className: "flex items-center justify-between",
18636
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18637
+ className: "flex items-center gap-4",
18638
+ children: [
18639
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-4 w-4 rounded-full bg-foreground/80" }),
18640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18641
+ className: "text-[26px] font-bold uppercase tracking-[0.24em] text-muted",
18642
+ children: "Douyin Mix"
18643
+ }),
18644
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18645
+ className: "text-[26px] font-bold text-muted",
18646
+ children: "· 合集选集"
18647
+ })
18648
+ ]
18649
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
18650
+ src: isDark(ctx) ? "/image/douyin/dylogo-light.svg" : "/image/douyin/dylogo-dark.svg",
18651
+ alt: "抖音",
18652
+ className: "h-17 w-auto shrink-0 object-contain opacity-90"
18653
+ })]
18654
+ });
18655
+ var MixHero = ({ data }) => {
18656
+ const { kind, image_url, mix_name, author, statistics, episodes, total_episode, updated_to_episode } = data;
18657
+ const countUnit = kind === "collection" ? "个视频" : "集";
18658
+ const runtime = splitRuntime(episodes.reduce((sum, item) => sum + item.duration, 0));
18659
+ const stats = [
18660
+ {
18661
+ icon: a,
18662
+ label: "总播放",
18663
+ ...splitCount(statistics.play_count)
18664
+ },
18665
+ {
18666
+ icon: DouyinLikeIcon,
18667
+ label: "总点赞",
18668
+ ...splitCount(statistics.digg_count)
18669
+ },
18670
+ {
18671
+ icon: DouyinFavoriteIcon,
18672
+ label: "总收藏",
18673
+ ...splitCount(statistics.collect_count)
18674
+ },
18675
+ {
18676
+ icon: c,
18677
+ label: "总时长",
18678
+ ...runtime
18679
+ }
18680
+ ];
18681
+ const follower = splitCount(author.follower_count);
18682
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
18683
+ className: "mt-14 flex gap-14",
18684
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18685
+ className: `relative ${kind === "collection" ? "h-120" : "h-180"} w-120 shrink-0`,
18686
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
18687
+ src: image_url,
18688
+ alt: "",
18689
+ "aria-hidden": "true",
18690
+ className: "absolute inset-0 h-full w-full scale-[1.06] rounded-[40px] object-cover opacity-55 blur-[36px] saturate-[1.2]",
18691
+ referrerPolicy: "no-referrer",
18692
+ crossOrigin: "anonymous"
18693
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
18694
+ src: image_url,
18695
+ alt: "合集海报",
18696
+ className: "relative h-full w-full rounded-[40px] object-cover shadow-2xl",
18697
+ referrerPolicy: "no-referrer",
18698
+ crossOrigin: "anonymous"
18699
+ })]
18700
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18701
+ className: "flex min-w-0 flex-1 flex-col justify-between gap-10",
18702
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
18703
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
18704
+ className: `${getTitleClassName$1(mix_name.length)} line-clamp-3 font-bold text-balance text-foreground select-text`,
18705
+ style: {
18706
+ wordBreak: "break-word",
18707
+ overflowWrap: "break-word"
18708
+ },
18709
+ children: mix_name
18710
+ }),
18711
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18712
+ className: "mt-8 flex flex-wrap items-center gap-4",
18713
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18714
+ className: "rounded-full bg-foreground px-7 py-3 text-[30px] font-bold leading-none text-background",
18715
+ children: [
18716
+ "共 ",
18717
+ total_episode,
18718
+ " ",
18719
+ countUnit
18720
+ ]
18721
+ }), updated_to_episode < total_episode && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18722
+ className: "rounded-full border border-foreground/20 px-7 py-3 text-[30px] font-semibold leading-none text-foreground/80",
18723
+ children: [
18724
+ "更新至第 ",
18725
+ updated_to_episode,
18726
+ " 集"
18727
+ ]
18728
+ })]
18729
+ }),
18730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18731
+ className: "mt-10 flex min-w-0 items-center gap-5",
18732
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
18733
+ src: author.avatar,
18734
+ alt: "头像",
18735
+ className: "h-20 w-20 shrink-0 rounded-full object-cover shadow-xl",
18736
+ referrerPolicy: "no-referrer",
18737
+ crossOrigin: "anonymous"
18738
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18739
+ className: "min-w-0",
18740
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18741
+ className: "flex min-w-0 items-center gap-2",
18742
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18743
+ className: "truncate text-[38px] font-bold leading-tight text-foreground select-text",
18744
+ children: author.nickname
18745
+ }), author.verified && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RiVerifiedBadgeFill, {
18746
+ size: 34,
18747
+ className: "shrink-0 text-foreground/70"
18748
+ })]
18749
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18750
+ className: "mt-1 truncate text-[26px] text-muted select-text",
18751
+ children: [
18752
+ "抖音号 ",
18753
+ author.short_id,
18754
+ " · 粉丝 ",
18755
+ follower.value,
18756
+ follower.unit
18757
+ ]
18758
+ })]
18759
+ })]
18760
+ })
18761
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18762
+ className: "grid grid-cols-2 gap-x-10 gap-y-8",
18763
+ children: stats.map((stat) => {
18764
+ const Icon = stat.icon;
18765
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18766
+ className: "min-w-0",
18767
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18768
+ className: "flex items-center gap-3 text-[28px] font-semibold text-muted",
18769
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
18770
+ size: 32,
18771
+ weight: "fill",
18772
+ className: "text-foreground/80"
18773
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: stat.label })]
18774
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18775
+ className: "mt-2 flex items-baseline gap-2 text-foreground select-text",
18776
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18777
+ className: "text-[64px] font-semibold leading-none tabular-nums",
18778
+ children: stat.value
18779
+ }), stat.unit && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18780
+ className: "text-[28px] font-bold",
18781
+ children: stat.unit
18782
+ })]
18783
+ })]
18784
+ }, stat.label);
18785
+ })
18786
+ })]
18787
+ })]
18788
+ });
18789
+ };
18790
+ /** 区块标题:竖条 + 标题 */
18791
+ var SectionTitle$1 = ({ children, extra }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18792
+ className: "flex items-end justify-between gap-8",
18793
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18794
+ className: "flex items-center gap-4",
18795
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-10 w-2 rounded-full bg-foreground/80" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
18796
+ className: "text-[44px] font-bold leading-none text-foreground",
18797
+ children
18798
+ })]
18799
+ }), extra]
18800
+ });
18801
+ var MixSynopsis = ({ data }) => {
18802
+ if (!data.desc) return null;
18803
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
18804
+ className: "mt-16",
18805
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle$1, { children: "简介" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
18806
+ className: "mt-6 line-clamp-5 text-[40px] leading-[1.6] text-foreground/85 select-text",
18807
+ style: { overflowWrap: "break-word" },
18808
+ children: data.desc
18809
+ })]
18810
+ });
18811
+ };
18812
+ /** 文字选集提示里的命令高亮,和 B 站番剧模板保持一致 */
18813
+ var CommandChip = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18814
+ className: "inline-flex items-center rounded-xl bg-danger px-4 py-1.5 font-semibold text-danger-foreground",
18815
+ children
18816
+ });
18817
+ var MixEpisodes = ({ data }) => {
18818
+ const { kind, episodes, total_episode, updated_to_episode, selectionMode } = data;
18819
+ const pageSize = Math.max(1, data.posterPageSize ?? DEFAULT_POSTER_PAGE_SIZE);
18820
+ const pageCount = Math.max(1, Math.ceil(episodes.length / pageSize));
18821
+ const page = Math.min(Math.max(1, data.posterPage ?? 1), pageCount);
18822
+ const start = (page - 1) * pageSize;
18823
+ const visibleEpisodes = episodes.slice(start, start + pageSize);
18824
+ if (visibleEpisodes.length === 0) return null;
18825
+ const firstEpisode = visibleEpisodes[0].episode;
18826
+ const lastEpisode = visibleEpisodes[visibleEpisodes.length - 1].episode;
18827
+ const range = firstEpisode === lastEpisode ? `第 ${firstEpisode} 集` : `第 ${firstEpisode}–${lastEpisode} 集`;
18828
+ const countUnit = kind === "collection" ? "个视频" : "集";
18829
+ const maxEpisode = episodes.reduce((max, item) => Math.max(max, item.episode), 0);
18830
+ const episodeSet = new Set(episodes.map((item) => item.episode));
18831
+ const missingEpisodes = [];
18832
+ for (let episode = 1; episode <= Math.max(maxEpisode, updated_to_episode); episode++) if (!episodeSet.has(episode)) missingEpisodes.push(episode);
18833
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
18834
+ className: "mt-16",
18835
+ children: [
18836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle$1, {
18837
+ extra: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18838
+ className: "text-[30px] font-semibold text-muted",
18839
+ children: selectionMode === "button" ? range : `第 ${page}/${pageCount} 页 · ${range}`
18840
+ }),
18841
+ children: "选集"
18842
+ }),
18843
+ selectionMode !== "button" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18844
+ className: "flex flex-col gap-4 mt-8 text-[32px] select-text text-foreground/80",
18845
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18846
+ className: "flex flex-wrap gap-3 items-center",
18847
+ children: [
18848
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
18849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "第?集" }),
18850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "选集" })
18851
+ ]
18852
+ }), pageCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18853
+ className: "flex flex-wrap gap-3 items-center",
18854
+ children: [
18855
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
18856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "上一页" }),
18857
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18858
+ className: "text-foreground/30",
18859
+ children: "/"
18860
+ }),
18861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "下一页" }),
18862
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18863
+ className: "text-foreground/30",
18864
+ children: "/"
18865
+ }),
18866
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "第?页" }),
18867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "翻页" })
18868
+ ]
18869
+ })]
18870
+ }),
18871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18872
+ className: "grid grid-cols-3 gap-8 mt-8",
18873
+ children: visibleEpisodes.map((episode) => {
18874
+ const cover = kind === "collection" ? episode.cover || episode.originCover : episode.originCover || episode.cover;
18875
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18876
+ className: "flex flex-col overflow-hidden bg-surface rounded-3xl shadow-large",
18877
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18878
+ className: `relative overflow-hidden ${kind === "collection" ? "aspect-square" : "aspect-[3/4]"}`,
18879
+ children: [
18880
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
18881
+ src: cover || "",
18882
+ alt: `第${episode.episode}集 ${episode.title}`,
18883
+ className: "object-cover w-full h-full select-text"
18884
+ }),
18885
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18886
+ className: "absolute top-4 left-4 px-3 py-1 text-2xl font-bold text-white rounded-xl bg-black/65",
18887
+ children: [
18888
+ "第",
18889
+ episode.episode,
18890
+ "集"
18891
+ ]
18892
+ }),
18893
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18894
+ className: "absolute right-4 bottom-4 px-3 py-1 text-xl font-semibold text-white rounded-xl bg-black/65 tabular-nums",
18895
+ children: formatDuration$4(episode.duration)
18896
+ })
18897
+ ]
18898
+ }), kind === "collection" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18899
+ className: "flex flex-col flex-1 gap-3 p-5",
18900
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18901
+ className: "text-3xl font-semibold leading-snug select-text text-foreground line-clamp-2",
18902
+ children: episode.title || `第${episode.episode}集`
18903
+ })
18904
+ })]
18905
+ }, `${episode.episode}-${episode.aweme_id}`);
18906
+ })
18907
+ }),
18908
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18909
+ className: "flex flex-wrap items-center justify-between gap-6 mt-8 text-[26px] text-muted",
18910
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
18911
+ "已列出 ",
18912
+ episodes.length,
18913
+ " / ",
18914
+ total_episode,
18915
+ " ",
18916
+ countUnit,
18917
+ missingEpisodes.length > 0 && `;接口未返回第 ${missingEpisodes.join("、")} ${countUnit},暂不能解析。`,
18918
+ updated_to_episode < total_episode && `;已更新至第 ${updated_to_episode} ${countUnit}。`
18919
+ ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18920
+ className: "select-text",
18921
+ children: ["图片生成于: ", format(/* @__PURE__ */ new Date(), "yyyy-MM-dd HH:mm:ss")]
18922
+ })]
18923
+ })
18924
+ ]
18925
+ });
18926
+ };
18927
+ var DouyinMix = import_react.memo((props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DefaultLayout, {
18928
+ ...props,
18929
+ className: "relative overflow-hidden",
18930
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MixBackground, { ...props }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
18931
+ className: "relative z-10 px-20 pt-18",
18932
+ children: [
18933
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MixHeader, { ...props }),
18934
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MixHero, { ...props }),
18935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MixSynopsis, { ...props }),
18936
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MixEpisodes, { ...props })
18937
+ ]
18938
+ })]
18939
+ }));
18940
+ DouyinMix.displayName = "DouyinMix";
18941
+ //#endregion
18942
+ //#region ktr/template/douyin/mix/index.tsx
18943
+ var mix_default = defineTemplate({
18944
+ name: "合集选集",
18945
+ description: "抖音合集(短剧 / 普通合集)选集列表模板",
18946
+ component: DouyinMix
18947
+ });
18948
+ //#endregion
18600
18949
  //#region ktr/template/douyin/musicinfo/components/MusicInfo.tsx
18601
18950
  /**
18602
18951
  * 抖音Logo头部组件
@@ -29428,6 +29777,7 @@ var templates = {
29428
29777
  "douyin/favorite-list": favorite_list_default,
29429
29778
  "douyin/image-work": image_work_default,
29430
29779
  "douyin/live": live_default,
29780
+ "douyin/mix": mix_default,
29431
29781
  "douyin/musicinfo": musicinfo_default,
29432
29782
  "douyin/qrcodeImg": qrcodeImg_default$2,
29433
29783
  "douyin/recommend-list": recommend_list_default,
@@ -30205,6 +30555,268 @@ var wrapWithErrorHandler = (fn, options) => {
30205
30555
  }
30206
30556
  };
30207
30557
  };
30558
+ /** 每个会话最多缓存几张海报,避免长番剧来回翻页把图片全留在内存里 */
30559
+ var MAX_CACHED_POSTERS = 3;
30560
+ /** 会话表上限,防止异常流量无限堆积 */
30561
+ var MAX_SESSIONS = 500;
30562
+ /** 句中的「第N集」,集数可以是阿拉伯数字或中文数字,数字两侧允许空格 */
30563
+ var EPISODE_IN_TEXT = /第\s*([0-9]+|[零一二三四五六七八九十百千]+)\s*集/;
30564
+ /** 整条回复只有集数:「12」「12集」「十二」 */
30565
+ var EPISODE_ONLY = /^([0-9]+|[零一二三四五六七八九十百千]+)\s*集?$/;
30566
+ /**
30567
+ * 支持按钮 + 上下文交互选集的适配器。
30568
+ * 目前只有 QQBot;后续适配器确认支持 callback 按钮和指令按钮(输入框)后再加入。
30569
+ */
30570
+ var BUTTON_SELECTION_PROTOCOLS = /* @__PURE__ */ new Set(["qqbot"]);
30571
+ var sessions = /* @__PURE__ */ new Map();
30572
+ var parseStatSkipped = /* @__PURE__ */ new WeakSet();
30573
+ var bilibiliAutoSelectEvents = /* @__PURE__ */ new WeakSet();
30574
+ /**
30575
+ * 从用户输入中解析集数。
30576
+ * @param text 用户输入
30577
+ * @returns 集数(从 1 开始);没解析出来返回 null
30578
+ */
30579
+ var parseEpisodeNumber = (text) => {
30580
+ const normalized = text.trim();
30581
+ const raw = (EPISODE_IN_TEXT.exec(normalized) ?? EPISODE_ONLY.exec(normalized))?.[1];
30582
+ if (!raw) return null;
30583
+ const episode = /^[0-9]+$/.test(raw) ? Number(raw) : Common.chineseToArabic(raw);
30584
+ return Number.isInteger(episode) && episode > 0 ? episode : null;
30585
+ };
30586
+ /** 解析 `#选集` 命令的第二个参数,支持页码和集数。 */
30587
+ var parseEpisodeSelectionAction = (action) => {
30588
+ const pageMatch = /^page:\s*(\d+)$/.exec(action.trim());
30589
+ if (pageMatch) {
30590
+ const page = Number(pageMatch[1]);
30591
+ return Number.isInteger(page) && page > 0 ? {
30592
+ type: "page",
30593
+ page
30594
+ } : null;
30595
+ }
30596
+ const episode = parseEpisodeNumber(action);
30597
+ return episode === null ? null : {
30598
+ type: "episode",
30599
+ episode
30600
+ };
30601
+ };
30602
+ /**
30603
+ * 解析文字选集的翻页命令。
30604
+ * 支持「上一页」「下一页」「第12页」「十二页」「12页」「page:12」;
30605
+ * 单独的「12」仍然表示集数,避免和选集输入冲突。
30606
+ */
30607
+ var parseEpisodePosterPageAction = (text) => {
30608
+ const normalized = text.trim();
30609
+ if (/^(?:上一页|上页|上一頁)$/.test(normalized)) return { type: "prev" };
30610
+ if (/^(?:下一页|下页|下一頁)$/.test(normalized)) return { type: "next" };
30611
+ const rawPage = /^page:(\d+)$/.exec(normalized)?.[1] ?? /^(?:第\s*)?([0-9]+|[零一二三四五六七八九十百千]+)\s*页$/.exec(normalized)?.[1];
30612
+ if (!rawPage) return null;
30613
+ const page = /^[0-9]+$/.test(rawPage) ? Number(rawPage) : Common.chineseToArabic(rawPage);
30614
+ return Number.isInteger(page) && page > 0 ? {
30615
+ type: "page",
30616
+ page
30617
+ } : null;
30618
+ };
30619
+ /** 生成会话作用域;群聊内所有人共用,私聊只绑定当前用户。 */
30620
+ var getEpisodeSelectionScope = (event) => {
30621
+ const subPeer = event.contact.subPeer || "";
30622
+ return `${event.selfId}:${event.contact.peer}:${subPeer}`;
30623
+ };
30624
+ /** 当前适配器是否支持按钮 + 上下文交互选集(目前为 QQBot 群聊/单聊)。 */
30625
+ var isButtonSelectionScene = (event) => {
30626
+ return BUTTON_SELECTION_PROTOCOLS.has(event.bot.adapter.protocol) && (event.isGroup || event.isFriend);
30627
+ };
30628
+ /** 标记本次解析只负责发选集,不计入解析统计。 */
30629
+ var markParseStatSkipped = (event) => {
30630
+ parseStatSkipped.add(event);
30631
+ };
30632
+ /** 消费一次性统计跳过标记。 */
30633
+ var consumeParseStatSkip = (event) => {
30634
+ return parseStatSkipped.delete(event);
30635
+ };
30636
+ /** 标记本次 B 站 ep 解析来自选集按钮,需要直接选中对应集。 */
30637
+ var markBilibiliEpisodeAutoSelect = (event) => {
30638
+ bilibiliAutoSelectEvents.add(event);
30639
+ };
30640
+ /** 消费一次 B 站自动选集标记,用户分享的 ep 链接不会命中。 */
30641
+ var consumeBilibiliEpisodeAutoSelect = (event) => {
30642
+ return bilibiliAutoSelectEvents.delete(event);
30643
+ };
30644
+ var cleanupExpiredSessions = () => {
30645
+ const now = Date.now();
30646
+ for (const [token, session] of sessions) if (session.expiresAt <= now) sessions.delete(token);
30647
+ };
30648
+ var createToken = () => {
30649
+ for (;;) {
30650
+ const token = randomBytes(6).toString("base64url");
30651
+ if (!sessions.has(token)) return token;
30652
+ }
30653
+ };
30654
+ /**
30655
+ * 创建一次选集会话。
30656
+ * @param scope 由 {@link getEpisodeSelectionScope} 生成
30657
+ * @param options 海报、剧集和平台信息
30658
+ * @returns 可放进按钮 data 的短 token 会话
30659
+ */
30660
+ var createEpisodeSelection = (scope, options) => {
30661
+ cleanupExpiredSessions();
30662
+ if (sessions.size >= MAX_SESSIONS) {
30663
+ const oldest = sessions.keys().next().value;
30664
+ if (oldest) sessions.delete(oldest);
30665
+ }
30666
+ const session = {
30667
+ token: createToken(),
30668
+ scope,
30669
+ title: options.title,
30670
+ platform: options.platform,
30671
+ scene: options.scene,
30672
+ pageSize: Math.max(1, options.pageSize ?? 15),
30673
+ poster: options.poster,
30674
+ posterPages: /* @__PURE__ */ new Map([[options.posterPage ?? 1, options.poster]]),
30675
+ renderPoster: options.renderPoster,
30676
+ episodes: options.episodes,
30677
+ forceBurnDanmaku: options.forceBurnDanmaku ?? false,
30678
+ expiresAt: Date.now() + 12e4,
30679
+ currentMessageId: "",
30680
+ currentMessageSentAt: 0
30681
+ };
30682
+ sessions.set(session.token, session);
30683
+ return session;
30684
+ };
30685
+ /** 任务结束后立即关闭会话,旧按钮再次点击时只提示不再解析。 */
30686
+ var endEpisodeSelection = (token, scope) => {
30687
+ const session = sessions.get(token);
30688
+ if (!session || session.scope !== scope) return false;
30689
+ return sessions.delete(token);
30690
+ };
30691
+ /** 读取有效会话;token 不存在、scope 不匹配或已过期时返回 null。 */
30692
+ var getEpisodeSelection = (token, scope) => {
30693
+ const session = sessions.get(token);
30694
+ if (!session) return null;
30695
+ if (session.scope !== scope) return null;
30696
+ if (session.expiresAt <= Date.now()) {
30697
+ sessions.delete(token);
30698
+ return null;
30699
+ }
30700
+ return session;
30701
+ };
30702
+ /** 翻页成功后重新计算 120 秒有效期。 */
30703
+ var touchEpisodeSelection = (token, scope) => {
30704
+ const session = getEpisodeSelection(token, scope);
30705
+ if (!session) return false;
30706
+ session.expiresAt = Date.now() + 12e4;
30707
+ return true;
30708
+ };
30709
+ /** 记录当前选集消息,供下一次翻页撤回。 */
30710
+ var setEpisodeSelectionMessage = (session, messageId, sentAt = Date.now()) => {
30711
+ session.currentMessageId = messageId;
30712
+ session.currentMessageSentAt = messageId ? sentAt : 0;
30713
+ };
30714
+ /** 只有距离发送不超过 110 秒时才尝试撤回,避免顶格撞平台 120 秒窗口。 */
30715
+ var canRecallEpisodeMessage = (messageId, sentAt, now = Date.now()) => {
30716
+ if (!messageId || !sentAt) return false;
30717
+ const age = now - sentAt;
30718
+ return age >= 0 && age <= 11e4;
30719
+ };
30720
+ /** 会话里记录的当前消息是否还能撤回。 */
30721
+ var canRecallEpisodeSelectionMessage = (session, now = Date.now()) => {
30722
+ return canRecallEpisodeMessage(session.currentMessageId, session.currentMessageSentAt, now);
30723
+ };
30724
+ /** 总页数;海报和按钮共用。空选集返回 0。 */
30725
+ var getEpisodePageCount = (session) => {
30726
+ return Math.ceil(session.episodes.length / session.pageSize);
30727
+ };
30728
+ /** 取指定页;页码越界返回 null。 */
30729
+ var getEpisodePage = (session, page) => {
30730
+ if (!Number.isInteger(page) || page < 1) return null;
30731
+ const pageCount = getEpisodePageCount(session);
30732
+ if (page > pageCount) return null;
30733
+ const start = (page - 1) * session.pageSize;
30734
+ const items = session.episodes.slice(start, start + session.pageSize);
30735
+ if (!items.length) return null;
30736
+ return {
30737
+ page,
30738
+ pageCount,
30739
+ items,
30740
+ firstEpisode: items[0].episode,
30741
+ lastEpisode: items[items.length - 1].episode
30742
+ };
30743
+ };
30744
+ /** 取海报页(按需渲染并缓存);没有分页渲染器时始终返回初始海报。 */
30745
+ var getEpisodeSelectionPoster = async (session, posterPage) => {
30746
+ const cached = session.posterPages.get(posterPage);
30747
+ if (cached) return cached;
30748
+ if (!session.renderPoster) return session.poster;
30749
+ const poster = await session.renderPoster(posterPage);
30750
+ session.posterPages.set(posterPage, poster);
30751
+ while (session.posterPages.size > MAX_CACHED_POSTERS) {
30752
+ const oldest = session.posterPages.keys().next().value;
30753
+ if (oldest === void 0) break;
30754
+ session.posterPages.delete(oldest);
30755
+ }
30756
+ return poster;
30757
+ };
30758
+ /**
30759
+ * 生成 QQBot keyboard 行。
30760
+ * 跳转输入行在最上面,集数按钮每行 5 个,翻页按钮单独一行;
30761
+ * 15 集一页时正好 1 + 3 + 1 = 5 行,不会超过 QQBot 的上限。
30762
+ */
30763
+ var buildEpisodeKeyboardRows = (session, page) => {
30764
+ const data = getEpisodePage(session, page);
30765
+ if (!data) return [];
30766
+ const rows = [];
30767
+ if (data.pageCount > 1) rows.push([{
30768
+ text: "跳转到第x页",
30769
+ data: `#选集 ${session.token} page:`,
30770
+ enter: false,
30771
+ style: 1
30772
+ }]);
30773
+ for (let index = 0; index < data.items.length; index += 5) rows.push(data.items.slice(index, index + 5).map((item) => ({
30774
+ text: String(item.episode),
30775
+ show: String(item.episode),
30776
+ callback: true,
30777
+ data: `#选集 ${session.token} ${item.episode}`,
30778
+ style: 1
30779
+ })));
30780
+ const navigation = [];
30781
+ if (data.page > 1) navigation.push({
30782
+ text: "上一页",
30783
+ callback: true,
30784
+ data: `#选集 ${session.token} page:${data.page - 1}`
30785
+ });
30786
+ if (data.page < data.pageCount) navigation.push({
30787
+ text: "下一页",
30788
+ callback: true,
30789
+ data: `#选集 ${session.token} page:${data.page + 1}`
30790
+ });
30791
+ if (navigation.length) rows.push(navigation);
30792
+ return rows;
30793
+ };
30794
+ /** 生成选集提示文本。 */
30795
+ var buildEpisodeSelectionText = (session, page) => {
30796
+ const data = getEpisodePage(session, page);
30797
+ if (!data) return "";
30798
+ const range = data.firstEpisode === data.lastEpisode ? `第 ${data.firstEpisode} 集` : `第 ${data.firstEpisode}–${data.lastEpisode} 集`;
30799
+ return `《${session.title}》\n${range} · 第 ${data.page}/${data.pageCount} 页\n点击下方按钮选集(120 秒内有效)。`;
30800
+ };
30801
+ /** 按实际集号查找目标。 */
30802
+ var resolveEpisodeTarget = (session, episode) => {
30803
+ return session.episodes.find((item) => item.episode === episode)?.target ?? null;
30804
+ };
30805
+ /** 生成现有解析命令能识别的目标链接。 */
30806
+ var buildEpisodeTargetUrl = (target) => {
30807
+ return target.platform === "douyin" ? `https://www.douyin.com/video/${target.awemeId}` : `https://www.bilibili.com/bangumi/play/ep${target.epId}`;
30808
+ };
30809
+ /**
30810
+ * 组装 QQBot 选集消息:复用已渲染海报,并附当前页文本和 keyboard。
30811
+ * 海报按需传入(翻页跨 15 集区块时由调用方重新渲染),默认复用初始海报。
30812
+ */
30813
+ var buildEpisodeSelectionReply = (session, page, poster = session.poster) => {
30814
+ const text = buildEpisodeSelectionText(session, page);
30815
+ const rows = buildEpisodeKeyboardRows(session, page);
30816
+ const elements = [...poster, segment.text(text)];
30817
+ if (rows.length) elements.push(segment.keyboard(rows));
30818
+ return elements;
30819
+ };
30208
30820
  //#endregion
30209
30821
  //#region src/platform/bilibili/danmaku.ts
30210
30822
  /**
@@ -31489,8 +32101,13 @@ var Bilibili = class extends Base {
31489
32101
  const videoInfo = await this.amagi.bilibili.fetcher.fetchBangumiInfo({ [iddata.isEpid ? "ep_id" : "season_id"]: iddata.realid });
31490
32102
  this.islogin = (await checkCk()).Status === "isLogin";
31491
32103
  this.isVIP = (await checkCk()).isVIP;
31492
- const Episodes = [];
31493
- for (const item of videoInfo.data.result.episodes) Episodes.push({
32104
+ const sourceEpisodes = videoInfo.data.result.episodes ?? [];
32105
+ if (!sourceEpisodes.length) {
32106
+ await this.e.reply("未获取到可解析的剧集,请稍后重试");
32107
+ return true;
32108
+ }
32109
+ const displayEpisodes = sourceEpisodes.map((item, index) => ({
32110
+ episode: index + 1,
31494
32111
  cover: item.cover,
31495
32112
  bvid: item.bvid,
31496
32113
  link: item.short_link,
@@ -31498,8 +32115,23 @@ var Bilibili = class extends Base {
31498
32115
  pub_time: item.pub_time,
31499
32116
  badge: item.badge === "" ? "限免" : item.badge,
31500
32117
  badge_info: item.badge_info
31501
- });
31502
- img$1 = await Render(this.e, "bilibili/bangumi", {
32118
+ })).reverse();
32119
+ const episodeTargets = sourceEpisodes.map((item, index) => ({
32120
+ episode: index + 1,
32121
+ target: {
32122
+ platform: "bilibili",
32123
+ epId: String(item.ep_id)
32124
+ }
32125
+ })).reverse();
32126
+ const autoSelectEpisode = consumeBilibiliEpisodeAutoSelect(this.e);
32127
+ const linkedEpId = iddata.isEpid ? String(iddata.realid).replace(/^ep/i, "") : "";
32128
+ const linkedSourceIndex = linkedEpId ? sourceEpisodes.findIndex((item) => String(item.ep_id) === linkedEpId) : -1;
32129
+ const linkedDisplayIndex = linkedSourceIndex >= 0 ? sourceEpisodes.length - 1 - linkedSourceIndex : -1;
32130
+ let selectedIndex = autoSelectEpisode ? linkedSourceIndex : -1;
32131
+ const buttonSelection = isButtonSelectionScene(this.e);
32132
+ const selectionMode = buttonSelection ? "button" : "text";
32133
+ /** 按海报页码渲染:15 集一页,长番剧不会撑爆单张 PNG */
32134
+ const renderPoster = (posterPage) => Render(this.e, "bilibili/bangumi", {
31503
32135
  mainCover: videoInfo.data.result.cover,
31504
32136
  Actors: videoInfo.data.result.actors,
31505
32137
  Evaluate: videoInfo.data.result.evaluate,
@@ -31512,38 +32144,104 @@ var Bilibili = class extends Base {
31512
32144
  UPInfo: videoInfo.data.result.up_info,
31513
32145
  Copyright: videoInfo.data.result.rights.copyright,
31514
32146
  Stat: videoInfo.data.result.stat,
31515
- Episodes,
31516
- length: videoInfo.data.result.episodes.length
31517
- });
31518
- this.e.reply([...img$1, segment.text("请在120秒内输入 第?集 选择集数")]);
31519
- const context = await karin.ctx(this.e, {
31520
- reply: true,
31521
- throwOnTimeout: false
32147
+ Episodes: displayEpisodes,
32148
+ length: sourceEpisodes.length,
32149
+ selectionMode,
32150
+ posterPage,
32151
+ posterPageSize: 15
31522
32152
  });
31523
- if (!context) return true;
31524
- const regex = /第([一二三四五六七八九十百千万0-9]+)集/.exec(context.msg);
31525
- let Episode;
31526
- if (regex && regex[1]) {
31527
- Episode = regex[1];
31528
- if (/^[一二三四五六七八九十百千万]+$/.test(Episode)) Episode = Common.chineseToArabic(Episode).toString();
31529
- this.downloadfilename = videoInfo.data.result.episodes[Number(Episode) - 1].share_copy.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
31530
- this.e.reply(`收到请求,第${Episode}集\n${this.downloadfilename}\n正在下载中`);
32153
+ if (autoSelectEpisode) {
32154
+ if (linkedSourceIndex < 0) {
32155
+ await this.e.reply("未找到该集,请重新发送番剧链接");
32156
+ return true;
32157
+ }
32158
+ } else if (buttonSelection) {
32159
+ const initialPage = linkedDisplayIndex >= 0 ? Math.floor(linkedDisplayIndex / 15) + 1 : 1;
32160
+ const poster = await renderPoster(initialPage);
32161
+ const session = createEpisodeSelection(getEpisodeSelectionScope(this.e), {
32162
+ title: videoInfo.data.result.title,
32163
+ platform: "bilibili",
32164
+ scene: this.e.isFriend ? "friend" : "group",
32165
+ poster,
32166
+ posterPage: initialPage,
32167
+ renderPoster,
32168
+ episodes: episodeTargets,
32169
+ forceBurnDanmaku: this.forceBurnDanmaku
32170
+ });
32171
+ markParseStatSkipped(this.e);
32172
+ const sentAt = Date.now();
32173
+ setEpisodeSelectionMessage(session, (await this.e.reply(buildEpisodeSelectionReply(session, initialPage, poster))).messageId, sentAt);
32174
+ return true;
31531
32175
  } else {
31532
- logger$1.debug(Episode);
31533
- this.e.reply("匹配内容失败,请重新发送链接再次解析");
32176
+ const posterPageCount = Math.ceil(displayEpisodes.length / 15);
32177
+ let posterPage = linkedDisplayIndex >= 0 ? Math.floor(linkedDisplayIndex / 15) + 1 : 1;
32178
+ /** 上一页海报消息,翻页发送成功后撤回,避免聊天里堆一串过期海报 */
32179
+ let posterMessageId = "";
32180
+ let posterMessageSentAt = 0;
32181
+ for (;;) {
32182
+ const poster = await renderPoster(posterPage);
32183
+ const start = (posterPage - 1) * 15;
32184
+ const pageEpisodes = displayEpisodes.slice(start, start + 15);
32185
+ const firstEpisode = pageEpisodes[0]?.episode ?? 0;
32186
+ const lastEpisode = pageEpisodes[pageEpisodes.length - 1]?.episode ?? 0;
32187
+ const range = firstEpisode === lastEpisode ? `第 ${firstEpisode} 集` : `第 ${firstEpisode}–${lastEpisode} 集`;
32188
+ const sentAt = Date.now();
32189
+ const result = await this.e.reply([...poster, segment.text(`《${videoInfo.data.result.title}》\n${range}(共 ${displayEpisodes.length} 集)\n第 ${posterPage}/${posterPageCount} 页\n发送 第?集 选集${posterPageCount > 1 ? ",发送 上一页 / 下一页 / 第?页 翻页" : ""}(120 秒内有效)`)]);
32190
+ const previousMessageId = posterMessageId;
32191
+ const previousMessageSentAt = posterMessageSentAt;
32192
+ posterMessageId = result.messageId;
32193
+ posterMessageSentAt = result.messageId ? sentAt : 0;
32194
+ if (previousMessageId && canRecallEpisodeMessage(previousMessageId, previousMessageSentAt)) try {
32195
+ await this.e.bot.recallMsg(this.e.contact, previousMessageId);
32196
+ } catch (error) {
32197
+ logger$1.debug("[选集] 撤回上一页失败:", error);
32198
+ }
32199
+ const context = await karin.ctx(this.e, {
32200
+ reply: true,
32201
+ throwOnTimeout: false,
32202
+ time: 120
32203
+ });
32204
+ if (!context) return true;
32205
+ const replyText = context.msg.trim();
32206
+ if (/^[#/]/.test(replyText) || /(https?:\/\/|b23\.tv|bilibili\.com|douyin\.com|iesdouyin\.com)/i.test(replyText)) return true;
32207
+ const pageAction = parseEpisodePosterPageAction(context.msg);
32208
+ if (pageAction) {
32209
+ const nextPage = pageAction.type === "prev" ? posterPage - 1 : pageAction.type === "next" ? posterPage + 1 : pageAction.page;
32210
+ if (nextPage < 1 || nextPage > posterPageCount) {
32211
+ await this.e.reply(`页码超出范围,请发送 1-${posterPageCount} 之间的页码`);
32212
+ continue;
32213
+ }
32214
+ posterPage = nextPage;
32215
+ continue;
32216
+ }
32217
+ const parsedEpisode = parseEpisodeNumber(context.msg);
32218
+ if (parsedEpisode === null || parsedEpisode > sourceEpisodes.length) {
32219
+ await this.e.reply(parsedEpisode === null ? "匹配内容失败,请发送 第?集 选择集数,或发送 上一页 / 下一页 / 第?页 翻页" : `第${parsedEpisode}集不在可解析的剧集里,请重新选择`);
32220
+ continue;
32221
+ }
32222
+ selectedIndex = parsedEpisode - 1;
32223
+ break;
32224
+ }
32225
+ }
32226
+ const episodeInfo = sourceEpisodes[selectedIndex];
32227
+ if (!episodeInfo) {
32228
+ await this.e.reply("未找到该集,请重新发送番剧链接");
31534
32229
  return true;
31535
32230
  }
32231
+ const displayEpisode = selectedIndex + 1;
32232
+ this.downloadfilename = episodeInfo.share_copy.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
32233
+ await this.e.reply(`收到请求,第${displayEpisode}集\n${this.downloadfilename}\n正在下载中`);
31536
32234
  const bangumidataBASEURL = bilibiliApiUrls.getBangumiStream({
31537
- cid: videoInfo.data.result.episodes[Number(Episode) - 1].cid,
31538
- ep_id: videoInfo.data.result.episodes[Number(Episode) - 1].ep_id.toString()
32235
+ cid: episodeInfo.cid,
32236
+ ep_id: episodeInfo.ep_id.toString()
31539
32237
  });
31540
32238
  const Params = await genParams(bangumidataBASEURL);
31541
- if (!this.islogin) this.e.reply("B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck");
32239
+ if (!this.islogin) await this.e.reply("B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck");
31542
32240
  const playUrlData = await new Network({
31543
32241
  url: bangumidataBASEURL + Params,
31544
32242
  headers: this.headers
31545
32243
  }).getData();
31546
- if (videoInfo.data.result.episodes[Number(Episode) - 1].badge === "会员" && !this.isVIP) {
32244
+ if (episodeInfo.badge === "会员" && !this.isVIP) {
31547
32245
  logger$1.warn("该CK不是大会员,无法获取视频流");
31548
32246
  return true;
31549
32247
  }
@@ -37019,6 +37717,137 @@ var DouYin = class DouYin extends Base {
37019
37717
  }
37020
37718
  return true;
37021
37719
  }
37720
+ case "mix_work": {
37721
+ this.workType = "bangumi";
37722
+ /** 单次最多拉取的集数,超出部分不展示 */
37723
+ const episodeLimit = 200;
37724
+ const timeoutSeconds = 120;
37725
+ const awemeList = (await this.amagi.douyin.fetcher.fetchMixAweme({
37726
+ mix_id: data.mix_id,
37727
+ number: episodeLimit
37728
+ })).data.aweme_list;
37729
+ const first = awemeList[0];
37730
+ if (!first) throw new Error("获取合集作品列表失败,可能是因为该合集已被删除或设置为私密。");
37731
+ const mixInfo = first.mix_info;
37732
+ const author = first.author;
37733
+ const isPlaylet = typeof mixInfo.statis.total_episode === "number";
37734
+ const totalEpisode = isPlaylet ? mixInfo.statis.total_episode : mixInfo.statis.updated_to_episode;
37735
+ const displayEpisodes = [...awemeList.map((aweme) => ({
37736
+ episode: aweme.mix_info.statis.current_episode,
37737
+ aweme_id: aweme.aweme_id,
37738
+ title: aweme.desc.replace(/#\S+/g, "").trim(),
37739
+ duration: aweme.video.duration,
37740
+ cover: aweme.video.cover?.url_list?.[0] ?? "",
37741
+ originCover: aweme.video.origin_cover?.url_list?.[0] ?? ""
37742
+ }))].sort((a, b) => b.episode - a.episode);
37743
+ const buttonSelection = isButtonSelectionScene(this.e);
37744
+ const selectionMode = buttonSelection ? "button" : "text";
37745
+ /** 按海报页码渲染:15 集一页,长合集不会撑爆单张 PNG */
37746
+ const renderPoster = (posterPage) => Render(this.e, "douyin/mix", {
37747
+ kind: isPlaylet ? "playlet" : "collection",
37748
+ mix_name: mixInfo.mix_name,
37749
+ image_url: mixInfo.cover_url.url_list[0],
37750
+ desc: isPlaylet ? displayEpisodes[0]?.title ?? "" : "",
37751
+ total_episode: totalEpisode,
37752
+ updated_to_episode: mixInfo.statis.updated_to_episode,
37753
+ author: {
37754
+ nickname: author.nickname,
37755
+ avatar: author.avatar_thumb.url_list[0],
37756
+ short_id: author.unique_id === "" ? author.short_id : author.unique_id,
37757
+ follower_count: author.follower_count,
37758
+ verified: !!author.custom_verify || !!author.enterprise_verify_reason
37759
+ },
37760
+ statistics: {
37761
+ play_count: awemeList.reduce((sum, aweme) => sum + aweme.statistics.play_count, 0),
37762
+ digg_count: awemeList.reduce((sum, aweme) => sum + aweme.statistics.digg_count, 0),
37763
+ collect_count: awemeList.reduce((sum, aweme) => sum + aweme.statistics.collect_count, 0)
37764
+ },
37765
+ episodes: displayEpisodes,
37766
+ timeoutSeconds,
37767
+ selectionMode,
37768
+ posterPage,
37769
+ posterPageSize: 15
37770
+ });
37771
+ if (buttonSelection) {
37772
+ const initialPage = 1;
37773
+ const poster = await renderPoster(initialPage);
37774
+ const session = createEpisodeSelection(getEpisodeSelectionScope(this.e), {
37775
+ title: mixInfo.mix_name,
37776
+ platform: "douyin",
37777
+ scene: this.e.isFriend ? "friend" : "group",
37778
+ poster,
37779
+ posterPage: initialPage,
37780
+ renderPoster,
37781
+ episodes: displayEpisodes.map((item) => ({
37782
+ episode: item.episode,
37783
+ target: {
37784
+ platform: "douyin",
37785
+ awemeId: String(item.aweme_id)
37786
+ }
37787
+ })),
37788
+ forceBurnDanmaku: this.forceBurnDanmaku
37789
+ });
37790
+ markParseStatSkipped(this.e);
37791
+ const sentAt = Date.now();
37792
+ setEpisodeSelectionMessage(session, (await this.e.reply(buildEpisodeSelectionReply(session, initialPage, poster))).messageId, sentAt);
37793
+ return true;
37794
+ }
37795
+ const posterPageCount = Math.ceil(displayEpisodes.length / 15);
37796
+ let posterPage = 1;
37797
+ /** 上一页海报消息,翻页发送成功后撤回,避免聊天里堆一串过期海报 */
37798
+ let posterMessageId = "";
37799
+ let posterMessageSentAt = 0;
37800
+ for (;;) {
37801
+ const poster = await renderPoster(posterPage);
37802
+ const start = (posterPage - 1) * 15;
37803
+ const pageEpisodes = displayEpisodes.slice(start, start + 15);
37804
+ const firstEpisode = pageEpisodes[0]?.episode ?? 0;
37805
+ const lastEpisode = pageEpisodes[pageEpisodes.length - 1]?.episode ?? 0;
37806
+ const range = firstEpisode === lastEpisode ? `第 ${firstEpisode} 集` : `第 ${firstEpisode}–${lastEpisode} 集`;
37807
+ const sentAt = Date.now();
37808
+ const result = await this.e.reply([...poster, segment.text(`《${mixInfo.mix_name}》\n${range}(共 ${totalEpisode} 集,已列出 ${displayEpisodes.length} 集)\n第 ${posterPage}/${posterPageCount} 页\n发送 第?集 选集${posterPageCount > 1 ? ",发送 上一页 / 下一页 / 第?页 翻页" : ""}(${timeoutSeconds} 秒内有效)`)]);
37809
+ const previousMessageId = posterMessageId;
37810
+ const previousMessageSentAt = posterMessageSentAt;
37811
+ posterMessageId = result.messageId;
37812
+ posterMessageSentAt = result.messageId ? sentAt : 0;
37813
+ if (previousMessageId && canRecallEpisodeMessage(previousMessageId, previousMessageSentAt)) try {
37814
+ await this.e.bot.recallMsg(this.e.contact, previousMessageId);
37815
+ } catch (error) {
37816
+ logger.debug("[选集] 撤回上一页失败:", error);
37817
+ }
37818
+ const context = await karin.ctx(this.e, {
37819
+ reply: true,
37820
+ throwOnTimeout: false,
37821
+ time: timeoutSeconds
37822
+ });
37823
+ if (!context) return true;
37824
+ const replyText = context.msg.trim();
37825
+ if (/^[#/]/.test(replyText) || /(https?:\/\/|b23\.tv|bilibili\.com|douyin\.com|iesdouyin\.com)/i.test(replyText)) return true;
37826
+ const pageAction = parseEpisodePosterPageAction(context.msg);
37827
+ if (pageAction) {
37828
+ const nextPage = pageAction.type === "prev" ? posterPage - 1 : pageAction.type === "next" ? posterPage + 1 : pageAction.page;
37829
+ if (nextPage < 1 || nextPage > posterPageCount) {
37830
+ await this.e.reply(`页码超出范围,请发送 1-${posterPageCount} 之间的页码`);
37831
+ continue;
37832
+ }
37833
+ posterPage = nextPage;
37834
+ continue;
37835
+ }
37836
+ const episode = parseEpisodeNumber(context.msg);
37837
+ const target = episode === null ? void 0 : displayEpisodes.find((item) => item.episode === episode);
37838
+ if (!target) {
37839
+ await this.e.reply(episode === null ? "匹配内容失败,请发送 第?集 选择集数,或发送 上一页 / 下一页 / 第?页 翻页" : `第${episode}集不在可解析的剧集里,请重新选择`);
37840
+ continue;
37841
+ }
37842
+ await this.e.reply(`收到请求,第${episode}集\n${mixInfo.mix_name}\n正在解析中`);
37843
+ const targetData = {
37844
+ type: "one_work",
37845
+ aweme_id: target.aweme_id
37846
+ };
37847
+ await new DouYin(context, targetData, { forceBurnDanmaku: this.forceBurnDanmaku }).DouyinHandler(targetData);
37848
+ return true;
37849
+ }
37850
+ }
37022
37851
  case "music_work": {
37023
37852
  this.workType = "music";
37024
37853
  const musicInfo = (await this.amagi.douyin.fetcher.fetchMusicInfo({ music_id: data.music_id })).data.music_info;
@@ -37214,6 +38043,14 @@ var getDouyinID = async (event, url, log = true) => {
37214
38043
  room_id: longLink.split("/").pop()
37215
38044
  };
37216
38045
  break;
38046
+ case /\/(?:share\/(?:playlet|mix)\/detail|collection)\/(\d+)/.test(longLink): {
38047
+ const mixMatch = /\/(?:share\/(?:playlet|mix)\/detail|collection)\/(\d+)/.exec(longLink);
38048
+ result = {
38049
+ type: "mix_work",
38050
+ mix_id: mixMatch ? mixMatch[1] : void 0
38051
+ };
38052
+ break;
38053
+ }
37217
38054
  case /video\/(\d+)/.test(longLink):
37218
38055
  case /article\/(\d+)/.test(longLink):
37219
38056
  case /note\/(\d+)/.test(longLink): {
@@ -38164,23 +39001,10 @@ var DouYinpush = class extends Base {
38164
39001
  const botId = this.e.selfId;
38165
39002
  try {
38166
39003
  const inputDouyinId = this.e.msg.replace(/^#设置抖音推送/, "").trim();
38167
- /**
38168
- * 搜索结果按响应形态自述(判别式 `__search_type`,由 amagi 的 normalize 按响应结构 +
38169
- * 请求类型写入)。订阅只认「用户搜索」那一支:抖音把 user 请求回成别的形态时在这里说清楚,
38170
- * 而不是拿一个 undefined 去遍历。
38171
- */
38172
- if (data.__search_type !== "user") throw new Error(`抖音没有返回用户搜索结果(返回形态:${data.__search_type ?? "未知"})`);
38173
- let matchedUser = null;
38174
- for (const userItem of data.user_list) if ((userItem.user_info.unique_id === "" ? userItem.user_info.short_id : userItem.user_info.unique_id) === inputDouyinId) {
38175
- matchedUser = userItem.user_info;
38176
- break;
38177
- }
38178
- if (!matchedUser) throw new Error(`未找到抖音号为 ${inputDouyinId} 的用户`);
38179
- const sec_uid = matchedUser.sec_uid;
38180
- const UserInfoData = await this.amagi.douyin.fetcher.fetchUserProfile({ sec_uid });
38181
39004
  /** 处理抖音号 */
38182
- let user_shortid;
38183
- user_shortid = UserInfoData.data.user.unique_id === "" ? UserInfoData.data.user.short_id : UserInfoData.data.user.unique_id;
39005
+ const user_shortid = data.user_info.unique_id === "" ? data.user_info.short_id : data.user_info.unique_id;
39006
+ const sec_uid = data.user_info.sec_uid;
39007
+ if (inputDouyinId !== user_shortid) throw new Error(`未找到抖音号为 ${inputDouyinId} 的用户`);
38184
39008
  config.douyin ??= [];
38185
39009
  const existingItem = config.douyin.find((item) => item.sec_uid === sec_uid);
38186
39010
  const isSubscribed = await douyinDBInstance.isSubscribed(sec_uid, groupId);
@@ -38200,31 +39024,31 @@ var DouYinpush = class extends Base {
38200
39024
  config.douyin.splice(index, 1);
38201
39025
  }
38202
39026
  Config.Modify("pushlist", "douyin", config.douyin);
38203
- await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n删除成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}`);
38204
- logger$1.info(`\n删除成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}\nsec_uid${UserInfoData.data.user.sec_uid}`);
39027
+ await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n删除成功!${data.user_info.nickname}\n抖音号:${user_shortid}`);
39028
+ logger$1.info(`\n删除成功!${data.user_info.nickname}\n抖音号:${user_shortid}\nsec_uid${data.user_info.sec_uid}`);
38205
39029
  } else {
38206
39030
  existingItem.group_id.push(`${groupId}:${botId}`);
38207
39031
  if (!existingItem.pushTypes || existingItem.pushTypes.length === 0) existingItem.pushTypes = ["post", "live"];
38208
- if (!isSubscribed) await douyinDBInstance.subscribeDouyinUser(groupId, botId, sec_uid, user_shortid, UserInfoData.data.user.nickname);
39032
+ if (!isSubscribed) await douyinDBInstance.subscribeDouyinUser(groupId, botId, sec_uid, user_shortid, data.user_info.nickname);
38209
39033
  Config.Modify("pushlist", "douyin", config.douyin);
38210
- await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n添加成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}`);
39034
+ await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n添加成功!${data.user_info.nickname}\n抖音号:${user_shortid}`);
38211
39035
  if (Config.douyin.push.switch === false) await this.e.reply("请发送「#设置抖音推送开启」以进行推送");
38212
- logger$1.info(`\n设置成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}\nsec_uid${UserInfoData.data.user.sec_uid}`);
39036
+ logger$1.info(`\n设置成功!${data.user_info.nickname}\n抖音号:${user_shortid}\nsec_uid${data.user_info.sec_uid}`);
38213
39037
  }
38214
39038
  } else {
38215
39039
  config.douyin.push({
38216
39040
  switch: true,
38217
39041
  sec_uid,
38218
39042
  group_id: [`${groupId}:${botId}`],
38219
- remark: UserInfoData.data.user.nickname,
39043
+ remark: data.user_info.nickname,
38220
39044
  short_id: user_shortid,
38221
39045
  pushTypes: ["post", "live"]
38222
39046
  });
38223
- if (!isSubscribed) await douyinDBInstance.subscribeDouyinUser(groupId, botId, sec_uid, user_shortid, UserInfoData.data.user.nickname);
39047
+ if (!isSubscribed) await douyinDBInstance.subscribeDouyinUser(groupId, botId, sec_uid, user_shortid, data.user_info.nickname);
38224
39048
  Config.Modify("pushlist", "douyin", config.douyin);
38225
- await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n添加成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}`);
39049
+ await this.e.reply(`群:${groupInfo.groupName}(${groupId})\n添加成功!${data.user_info.nickname}\n抖音号:${user_shortid}`);
38226
39050
  if (Config.douyin.push.switch === false) await this.e.reply("请发送「#设置抖音推送开启」以进行推送");
38227
- logger$1.info(`\n设置成功!${UserInfoData.data.user.nickname}\n抖音号:${user_shortid}\nsec_uid${UserInfoData.data.user.sec_uid}`);
39051
+ logger$1.info(`\n设置成功!${data.user_info.nickname}\n抖音号:${user_shortid}\nsec_uid${data.user_info.sec_uid}`);
38228
39052
  }
38229
39053
  await this.renderPushList();
38230
39054
  } catch (error) {
@@ -40235,10 +41059,7 @@ var handleSetDouyinPush = wrapWithErrorHandler(async (e) => {
40235
41059
  logger$1.info(`抖音推送已${enable ? "开启" : "关闭"}`);
40236
41060
  return true;
40237
41061
  }
40238
- const data = await douyinFetcher.searchContent({
40239
- query,
40240
- type: "user"
40241
- });
41062
+ const data = await douyinFetcher.fetchGuestUserInfo({ unique_id: query });
40242
41063
  await new DouYinpush(e).setting(data.data);
40243
41064
  return true;
40244
41065
  }, { businessName: "设置抖音推送" });
@@ -41539,7 +42360,7 @@ var handleDouyin = wrapWithErrorHandler(async (e, next) => {
41539
42360
  const iddata = await getDouyinID(e, url);
41540
42361
  const douyin = new DouYin(e, iddata, { forceBurnDanmaku });
41541
42362
  await douyin.DouyinHandler(iddata);
41542
- await recordParseStat(e, "douyin", {
42363
+ if (!consumeParseStatSkip(e)) await recordParseStat(e, "douyin", {
41543
42364
  workType: douyin.workType,
41544
42365
  durationMs: Date.now() - startedAt
41545
42366
  });
@@ -41564,7 +42385,7 @@ var handleBilibili = wrapWithErrorHandler(async (e, next) => {
41564
42385
  const iddata = await getBilibiliID(url);
41565
42386
  const bilibili = new Bilibili(e, iddata, { forceBurnDanmaku });
41566
42387
  await bilibili.BilibiliHandler(iddata);
41567
- await recordParseStat(e, "bilibili", {
42388
+ if (!consumeParseStatSkip(e)) await recordParseStat(e, "bilibili", {
41568
42389
  workType: bilibili.workType,
41569
42390
  durationMs: Date.now() - startedAt
41570
42391
  });
@@ -41635,6 +42456,75 @@ var xiaohongshu = karin.command(reg.xiaohongshu, handleXiaohongshu, {
41635
42456
  priority: Config.app.videoTool ? -Infinity : 800
41636
42457
  });
41637
42458
  var prefix = karin.command(/^#?(解析|kkk解析|弹幕解析)/, handlePrefix, { name: "kkk-视频功能-引用解析" });
42459
+ var handleEpisodeSelection = wrapWithErrorHandler(async (e, next) => {
42460
+ if (!isButtonSelectionScene(e)) return next();
42461
+ const match = /^#选集\s+([A-Za-z0-9_-]+)(?:\s+(.+?))?\s*$/.exec(e.msg);
42462
+ if (!match) return next();
42463
+ const [, token, actionText] = match;
42464
+ const session = getEpisodeSelection(token, getEpisodeSelectionScope(e));
42465
+ if (!session) {
42466
+ await e.reply("本次选集已结束,请重新发送原链接", { at: true });
42467
+ return true;
42468
+ }
42469
+ if (!actionText) {
42470
+ await e.reply("请点击下方按钮,或点击「跳转到第x页」输入页码");
42471
+ return true;
42472
+ }
42473
+ const action = parseEpisodeSelectionAction(actionText);
42474
+ if (!action) {
42475
+ await e.reply("集数或页码格式不正确,请重新选择");
42476
+ return true;
42477
+ }
42478
+ if (action.type === "page") {
42479
+ if (!getEpisodePage(session, action.page)) {
42480
+ await e.reply("页码超出范围,请重新选择");
42481
+ return true;
42482
+ }
42483
+ const poster = await getEpisodeSelectionPoster(session, action.page);
42484
+ const previousMessageId = session.currentMessageId;
42485
+ const sentAt = Date.now();
42486
+ const result = await e.reply(buildEpisodeSelectionReply(session, action.page, poster));
42487
+ const shouldRecall = canRecallEpisodeSelectionMessage(session);
42488
+ setEpisodeSelectionMessage(session, result.messageId, sentAt);
42489
+ if (previousMessageId && shouldRecall) await e.bot.recallMsg(e.contact, previousMessageId).catch((error) => {
42490
+ logger$1.debug("[选集] 撤回上一页失败:", error);
42491
+ });
42492
+ touchEpisodeSelection(token, session.scope);
42493
+ return true;
42494
+ }
42495
+ const target = resolveEpisodeTarget(session, action.episode);
42496
+ if (!target) {
42497
+ await e.reply(`第${action.episode}集不在可解析的剧集里,请重新选择`);
42498
+ return true;
42499
+ }
42500
+ if (target.platform === "douyin" && !Config.douyin.switch) {
42501
+ endEpisodeSelection(token, session.scope);
42502
+ await e.reply("抖音解析未开启");
42503
+ return true;
42504
+ }
42505
+ if (target.platform === "bilibili" && !Config.bilibili.switch) {
42506
+ endEpisodeSelection(token, session.scope);
42507
+ await e.reply("B站解析未开启");
42508
+ return true;
42509
+ }
42510
+ if (!endEpisodeSelection(token, session.scope)) {
42511
+ await e.reply("本次选集已结束,请重新发送原链接");
42512
+ return true;
42513
+ }
42514
+ const originalMsg = e.msg;
42515
+ e.msg = `${session.forceBurnDanmaku ? "#弹幕解析 " : "#解析"}${buildEpisodeTargetUrl(target)}`;
42516
+ try {
42517
+ if (target.platform === "douyin") await handleDouyin(e, () => {});
42518
+ else {
42519
+ markBilibiliEpisodeAutoSelect(e);
42520
+ await handleBilibili(e, () => {});
42521
+ }
42522
+ } finally {
42523
+ e.msg = originalMsg;
42524
+ }
42525
+ return true;
42526
+ }, { businessName: "KKK选集" });
42527
+ var episodeSelection = karin.command(/^#选集\s+/, handleEpisodeSelection, { name: "kkk-视频功能-选集" });
41638
42528
  var douyinAPP = Config.douyin.switch && douyin;
41639
42529
  var bilibiliAPP = Config.bilibili.switch && bilibili;
41640
42530
  var kuaishouAPP = Config.kuaishou.switch && kuaishou;
@@ -43173,4 +44063,4 @@ mkdirSync(`${karinPathBase}/${Root.pluginName}/data`);
43173
44063
  mkdirSync(Common.tempDri.images);
43174
44064
  mkdirSync(Common.tempDri.video);
43175
44065
  //#endregion
43176
- export { douyinFetcher as $, task as A, getBilibiliDB as B, help as C, dylogin as D, biRefreshLogin as E, templates as F, StatisticsDBBase as G, getStatisticsDB as H, webConfig as I, BilibiliDBBase as J, resolveMetricBucket as K, bilibiliDBInstance as L, xhsLogin as M, handleRiskChallenge as N, ksLogin as O, runWithVerifyContext as P, bilibiliFetcher as Q, cleanOldDynamicCache as R, changelog as S, biLogin as T, initAllDatabases as U, getDouyinDB as V, METRIC_BUCKETS as W, AmagiError as X, AmagiBase as Y, SOFT_ERROR_CODES as Z, douyinPushList as _, bilibiliAPP as a, softFetch as at, setbiliPush as b, prefix as c, globalStatistics as d, getAmagiClient as et, groupStatistics as f, douyinPush as g, bilibiliPushList as h, update as i, reloadAmagiConfig as it, testWrapWithErrorHandler as j, removeOldFiles as k, xiaohongshuAPP as l, bilibiliPush as m, kkkUpdateCommand as n, kuaishouFetcher as nt, douyinAPP as o, xiaohongshuFetcher as ot, qrLogin as p, DouyinDBBase as q, kkkUpdateTest as r, registerAmagiReloadListener as rt, kuaishouAPP as s, Root as st, kkkUpdate as t, isSoftFailure as tt, testPush as u, forcePush as v, version as w, setdyPush as x, globalIgnore as y, douyinDBInstance as z };
44066
+ export { bilibiliFetcher as $, removeOldFiles as A, douyinDBInstance as B, changelog as C, biRefreshLogin as D, biLogin as E, runWithVerifyContext as F, METRIC_BUCKETS as G, getDouyinDB as H, templates as I, DouyinDBBase as J, StatisticsDBBase as K, webConfig as L, testWrapWithErrorHandler as M, xhsLogin as N, dylogin as O, handleRiskChallenge as P, SOFT_ERROR_CODES as Q, bilibiliDBInstance as R, setdyPush as S, version as T, getStatisticsDB as U, getBilibiliDB as V, initAllDatabases as W, AmagiBase as X, BilibiliDBBase as Y, AmagiError as Z, douyinPush as _, bilibiliAPP as a, reloadAmagiConfig as at, globalIgnore as b, kuaishouAPP as c, Root as ct, testPush as d, douyinFetcher as et, globalStatistics as f, bilibiliPushList as g, bilibiliPush as h, update as i, registerAmagiReloadListener as it, task as j, ksLogin as k, prefix as l, qrLogin as m, kkkUpdateCommand as n, isSoftFailure as nt, douyinAPP as o, softFetch as ot, groupStatistics as p, resolveMetricBucket as q, kkkUpdateTest as r, kuaishouFetcher as rt, episodeSelection as s, xiaohongshuFetcher as st, kkkUpdate as t, getAmagiClient as tt, xiaohongshuAPP as u, douyinPushList as v, help as w, setbiliPush as x, forcePush as y, cleanOldDynamicCache as z };