karin-plugin-kkk 2.45.2-beta.1 → 2.45.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -11791,6 +11791,8 @@ var DecorationCard = ({ data }) => {
11791
11791
  };
11792
11792
  //#endregion
11793
11793
  //#region ktr/template/bilibili/bangumi/components/bangumi.tsx
11794
+ /** 海报每页展示的集数,和 core 的 EPISODE_SELECTION_PAGE_SIZE 保持一致 */
11795
+ var DEFAULT_POSTER_PAGE_SIZE$1 = 15;
11794
11796
  /**
11795
11797
  * 格式化数字显示
11796
11798
  * @param num 要格式化的数字
@@ -11814,20 +11816,6 @@ var formatDateParts = (timestamp) => {
11814
11816
  };
11815
11817
  };
11816
11818
  /**
11817
- * 格式化完整日期时间
11818
- * @param timestamp 时间戳
11819
- * @returns 格式化的日期时间字符串
11820
- */
11821
- var formatDateTime = (timestamp) => {
11822
- return (/* @__PURE__ */ new Date(timestamp * 1e3)).toLocaleDateString("zh-CN", {
11823
- year: "numeric",
11824
- month: "short",
11825
- day: "numeric",
11826
- hour: "2-digit",
11827
- minute: "2-digit"
11828
- });
11829
- };
11830
- /**
11831
11819
  * B站番剧头部组件
11832
11820
  * @param props 头部组件属性
11833
11821
  */
@@ -11839,102 +11827,88 @@ var BangumiBilibiliHeader = (props) => {
11839
11827
  className: "flex relative z-10 gap-25 p-25",
11840
11828
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11841
11829
  className: "flex flex-col shrink-0 gap-20",
11842
- children: [
11843
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11844
- className: "overflow-hidden rounded-4xl w-120 h-160",
11845
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11846
- src: props.mainCover,
11847
- alt: props.title,
11848
- className: "object-cover w-full h-full select-text"
11849
- })
11850
- }),
11851
- props.upInfo && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11852
- className: "flex gap-12 items-center mt-15",
11853
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11854
- className: "relative",
11855
- children: [
11856
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11857
- className: "w-28 h-28 rounded-full select-text",
11858
- src: `https://images.weserv.nl/?url=${encodeURIComponent(props.upInfo.avatar)}`,
11859
- alt: props.upInfo.uname
11860
- }),
11861
- props.upInfo.avatar_subscript_url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11862
- className: "absolute -right-1 -bottom-1 w-8 h-8 select-text",
11863
- src: props.upInfo.avatar_subscript_url,
11864
- alt: "头像角标"
11865
- }),
11866
- props.upInfo.pendant?.image && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
11867
- className: "absolute inset-0 w-full h-full transform select-text scale-160",
11868
- src: props.upInfo.pendant.image,
11869
- alt: props.upInfo.pendant.name || "挂件"
11870
- })
11871
- ]
11872
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11873
- className: "flex flex-col gap-4",
11874
- children: [
11875
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11876
- className: "flex gap-3 items-center",
11877
- children: [
11878
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11879
- className: "text-4xl font-medium select-text",
11880
- style: { color: props.upInfo.vip_status === 1 ? props.upInfo.nickname_color || "#FB7299" : "#EDEDED" },
11881
- children: props.upInfo.uname
11882
- }),
11883
- props.upInfo.verify_type > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
11884
- className: "flex items-center",
11885
- children: props.upInfo.verify_type === 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Shield, {
11886
- width: 20,
11887
- className: "text-warning"
11888
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Crown, {
11889
- width: 20,
11890
- className: "text-accent"
11891
- })
11892
- }),
11893
- props.upInfo.vip_status === 1 && props.upInfo.vip_label?.text && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11894
- size: "sm",
11895
- className: "px-2 py-1 text-xs select-text",
11896
- style: {
11897
- backgroundColor: props.upInfo.vip_label.bg_color || "#FB7299",
11898
- color: props.upInfo.vip_label.text_color || "#FFFFFF",
11899
- borderColor: props.upInfo.vip_label.border_color || "transparent"
11900
- },
11901
- children: props.upInfo.vip_label.text
11902
- })
11903
- ]
11904
- }),
11905
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11906
- className: "flex gap-6 items-center text-3xl select-text text-foreground",
11907
- children: [
11908
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Users, { size: 30 }),
11909
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [formatNumber$2(props.upInfo.follower), "粉丝"] }),
11910
- props.upInfo.is_follow === 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
11911
- size: "sm",
11912
- color: "accent",
11913
- variant: "soft",
11914
- className: "text-xs select-text",
11915
- children: "已关注"
11916
- })
11917
- ]
11918
- }),
11919
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11920
- className: "flex gap-2 items-center text-2xl select-text text-foreground/70",
11921
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Hash, { size: 20 }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: ["UID: ", props.upInfo.mid] })]
11922
- })
11923
- ]
11924
- })]
11925
- }),
11926
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11927
- 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",
11928
11841
  children: [
11929
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "提示:请在120秒内发送" }),
11930
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
11931
- className: "inline-flex items-center rounded-xl bg-danger/12 px-4 py-1.5 text-[0.95em] font-semibold text-danger",
11932
- 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: "头像角标"
11933
11851
  }),
11934
- /* @__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
+ })
11935
11857
  ]
11936
- })
11937
- ]
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
+ })]
11938
11912
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
11939
11913
  className: "flex flex-col flex-1 justify-between text-foreground",
11940
11914
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
@@ -12057,171 +12031,132 @@ var BangumiBilibiliHeader = (props) => {
12057
12031
  })
12058
12032
  });
12059
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
+ });
12060
12039
  /**
12061
- * B站番剧剧集列表组件
12040
+ * B站番剧剧集列表组件。
12041
+ * 每页最多 15 集,3 列网格展示封面 + 标题;长番剧不会再生成超长截图。
12062
12042
  * @param props 剧集列表组件属性
12063
12043
  */
12064
12044
  var BangumiBilibiliEpisodes = (props) => {
12065
- const sortedEpisodes = [...props.Episodes].sort((a, b) => b.pub_time - a.pub_time);
12066
- const groupedEpisodes = sortedEpisodes.reduce((groups, episode) => {
12067
- const date = /* @__PURE__ */ new Date(episode.pub_time * 1e3);
12068
- const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
12069
- if (!groups[dateKey]) groups[dateKey] = [];
12070
- groups[dateKey].push(episode);
12071
- return groups;
12072
- }, {});
12073
- const dateKeys = Object.keys(groupedEpisodes).sort((a, b) => b.localeCompare(a));
12074
- const flattenedEpisodes = [];
12075
- dateKeys.forEach((dateIndex) => {
12076
- const episodesInDate = groupedEpisodes[dateIndex];
12077
- episodesInDate.forEach((episode, episodeIndex) => {
12078
- flattenedEpisodes.push({
12079
- episode,
12080
- isFirstOfDate: episodeIndex === 0,
12081
- isLastOfDate: episodeIndex === episodesInDate.length - 1,
12082
- isLastOfAll: false,
12083
- dateKey: dateIndex,
12084
- episodesInSameDate: episodesInDate.length
12085
- });
12086
- });
12087
- });
12088
- 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} 集`;
12089
12056
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12090
12057
  className: "px-10",
12091
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12092
- className: "flex gap-8 items-center mb-20 text-5xl font-bold select-text text-foreground",
12093
- children: [
12094
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Play, { size: 46 }),
12095
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "剧集列表" }),
12096
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Chip, {
12097
- size: "lg",
12098
- color: "danger",
12099
- variant: "soft",
12100
- 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",
12101
12085
  children: [
12102
- "共",
12103
- sortedEpisodes.length,
12104
- "集"
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: "选集" })
12105
12089
  ]
12106
- })
12107
- ]
12108
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: flattenedEpisodes.map((item) => {
12109
- const { episode, isFirstOfDate, isLastOfAll, episodesInSameDate, isLastOfDate } = item;
12110
- const { month, day } = formatDateParts(episode.pub_time);
12111
- const episodeNumber = sortedEpisodes.findIndex((e) => e.bvid === episode.bvid);
12112
- const actualEpisodeNumber = sortedEpisodes.length - episodeNumber;
12113
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12114
- className: "flex gap-15",
12115
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12116
- className: "flex flex-col shrink-0 items-center w-20",
12117
- children: isFirstOfDate ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
12118
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12119
- className: "text-4xl select-text text-foreground",
12120
- 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: "/"
12121
12098
  }),
12122
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12123
- className: "flex justify-center items-center text-7xl font-bold select-text text-foreground",
12124
- 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: "/"
12125
12103
  }),
12126
- !isLastOfAll && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("mt-8 w-1 bg-divider", episodesInSameDate > 1 ? "h-110" : "h-95") })
12127
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
12128
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-1 h-10 bg-divider" }),
12129
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "my-2 w-4 h-4 rounded-full bg-divider" }),
12130
- (!isLastOfAll || episodesInSameDate > 1) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("w-1 bg-divider", isLastOfDate ? "h-110" : "h-130") })
12131
- ] })
12132
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12133
- className: cn("flex-1 min-w-0", !isLastOfAll && isLastOfDate && "mb-20"),
12134
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12135
- 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",
12136
12116
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12137
- className: "flex shrink-0 gap-8 items-center",
12138
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12139
- className: "relative",
12140
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
12141
- className: "w-32 h-32 rounded-full select-text",
12142
- src: `https://images.weserv.nl/?url=${encodeURIComponent(props.UPInfo ? props.UPInfo.avatar : props.mainCover)}`,
12143
- 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
12144
12140
  })
12145
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12146
- className: "flex flex-col gap-6",
12147
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12148
- className: "text-4xl font-bold select-text text-foreground/80",
12149
- children: props.UPInfo ? props.UPInfo.uname : props.Title
12150
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12151
- className: "flex gap-4 items-center text-3xl select-text text-foreground/70",
12152
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Calendar, { size: 30 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发布了内容" })]
12153
- })]
12154
- })]
12155
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12156
- className: "shrink-0 pr-20",
12157
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12158
- 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",
12159
12149
  children: [
12160
- "第",
12161
- actualEpisodeNumber,
12162
- "集"
12150
+ month,
12151
+ day,
12152
+ "日"
12163
12153
  ]
12164
- })
12165
- })]
12166
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12167
- className: "overflow-hidden shadow-large bg-surface rounded-4xl",
12168
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12169
- className: "flex gap-12 p-12",
12170
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12171
- className: "relative shrink-0",
12172
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12173
- className: "overflow-hidden relative h-64 rounded-3xl w-md",
12174
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(EnhancedImage, {
12175
- src: episode.cover,
12176
- alt: `第${actualEpisodeNumber}集 ${episode.long_title}`,
12177
- className: "object-cover w-full h-full select-text"
12178
- }), episode.badge && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
12179
- size: "lg",
12180
- className: "absolute top-6 right-8 rounded-2xl py-1 text-lg font-medium select-text scale-150",
12181
- style: {
12182
- backgroundColor: episode.badge_info?.bg_color || "#FB7299",
12183
- color: "#FFFFFF"
12184
- },
12185
- children: episode.badge_info?.text || episode.badge
12186
- })]
12187
- })
12188
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12189
- className: "flex flex-col flex-1 justify-center h-64",
12190
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
12191
- className: "mb-8 text-5xl font-semibold select-text text-foreground line-clamp-2",
12192
- children: episode.long_title
12193
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12194
- className: "space-y-4 text-4xl",
12195
- children: [
12196
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12197
- className: "flex gap-6 items-center select-text text-foreground/70",
12198
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Hash, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12199
- className: "truncate",
12200
- children: episode.bvid
12201
- })]
12202
- }),
12203
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12204
- className: "flex gap-6 items-center select-text text-foreground/70",
12205
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Clock, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12206
- className: "whitespace-nowrap",
12207
- children: formatDateTime(episode.pub_time)
12208
- })]
12209
- }),
12210
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
12211
- className: "flex gap-6 items-center select-text text-foreground/70",
12212
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Share2, { size: 36 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
12213
- className: "truncate",
12214
- children: episode.link
12215
- })]
12216
- })
12217
- ]
12218
- })]
12219
12154
  })]
12220
- })
12221
- })]
12222
- })]
12223
- }, episode.bvid);
12224
- }) })]
12155
+ })]
12156
+ }, `${episodeNumber}-${episode.bvid}`);
12157
+ })
12158
+ })
12159
+ ]
12225
12160
  });
12226
12161
  };
12227
12162
  var BangumiBilibili = import_react.memo((props) => {
@@ -18600,6 +18535,8 @@ var live_default = defineTemplate({
18600
18535
  });
18601
18536
  //#endregion
18602
18537
  //#region ktr/template/douyin/mix/components/Mix.tsx
18538
+ /** 海报每页展示的集数,和 core 的 EPISODE_SELECTION_PAGE_SIZE 保持一致 */
18539
+ var DEFAULT_POSTER_PAGE_SIZE = 15;
18603
18540
  /** 万/亿 计数,数值与单位分开返回:数字用大号等宽,单位用小号 */
18604
18541
  var splitCount = (count) => {
18605
18542
  if (count >= 1e8) return {
@@ -18872,113 +18809,114 @@ var MixSynopsis = ({ data }) => {
18872
18809
  })]
18873
18810
  });
18874
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
+ });
18875
18817
  var MixEpisodes = ({ data }) => {
18876
- const { kind, episodes, total_episode, updated_to_episode, timeoutSeconds } = data;
18877
- const episodeMap = new Map(episodes.map((item) => [item.episode, item]));
18878
- const lastEpisode = episodes.reduce((max, item) => Math.max(max, item.episode), 0);
18879
- const cells = Array.from({ length: lastEpisode }, (_, index) => ({
18880
- episode: index + 1,
18881
- item: episodeMap.get(index + 1)
18882
- }));
18883
- const missingCount = cells.filter((cell) => cell.item === void 0).length;
18884
- const padLength = String(lastEpisode).length;
18885
- const listedRange = lastEpisode < updated_to_episode;
18886
- const unit = kind === "collection" ? "视频" : "剧集";
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} 集`;
18887
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);
18888
18833
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
18889
18834
  className: "mt-16",
18890
18835
  children: [
18891
18836
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle$1, {
18892
- extra: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
18837
+ extra: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18893
18838
  className: "text-[30px] font-semibold text-muted",
18894
- children: [
18895
- "已列出 ",
18896
- episodes.length,
18897
- " / ",
18898
- total_episode,
18899
- " ",
18900
- countUnit
18901
- ]
18839
+ children: selectionMode === "button" ? range : `第 ${page}/${pageCount} 页 · ${range}`
18902
18840
  }),
18903
18841
  children: "选集"
18904
18842
  }),
18905
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18906
- className: "mt-8 flex items-center justify-between gap-10 rounded-4xl border border-foreground/10 bg-foreground/6 px-10 py-8",
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",
18907
18845
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18908
- className: "flex min-w-0 flex-wrap items-center gap-x-4 gap-y-3 text-[36px] font-semibold leading-tight text-foreground",
18846
+ className: "flex flex-wrap gap-3 items-center",
18909
18847
  children: [
18910
18848
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
18911
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18912
- className: "rounded-2xl bg-foreground px-5 py-2 font-bold text-background",
18913
- children: "第?集"
18914
- }),
18915
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: ["或直接发送集数,解析对应", unit] })
18849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "第?集" }),
18850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "选集" })
18916
18851
  ]
18917
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18918
- className: "shrink-0 text-right",
18919
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18920
- className: "text-[64px] font-semibold leading-none text-foreground tabular-nums",
18921
- children: [timeoutSeconds, "s"]
18922
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18923
- className: "mt-2 text-[24px] font-semibold text-muted",
18924
- children: "内有效"
18925
- })]
18926
- })]
18927
- }),
18928
- kind === "collection" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18929
- className: "mt-8 grid grid-cols-2 gap-4",
18930
- children: cells.map((cell) => cell.item === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18931
- className: "flex items-center gap-6 rounded-3xl border-2 border-dashed border-foreground/20 px-7 py-6",
18932
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18933
- className: "shrink-0 text-[40px] font-semibold leading-none text-foreground/25 tabular-nums",
18934
- children: String(cell.episode).padStart(padLength, "0")
18935
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18936
- className: "text-[30px] font-semibold text-foreground/25",
18937
- children: "缺失"
18938
- })]
18939
- }, cell.episode) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18940
- className: "flex items-center gap-6 rounded-3xl border border-foreground/10 bg-foreground/6 px-7 py-6",
18852
+ }), pageCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18853
+ className: "flex flex-wrap gap-3 items-center",
18941
18854
  children: [
18855
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "发送" }),
18856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "上一页" }),
18942
18857
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18943
- className: "shrink-0 text-[40px] font-semibold leading-none text-foreground tabular-nums select-text",
18944
- children: String(cell.episode).padStart(padLength, "0")
18858
+ className: "text-foreground/30",
18859
+ children: "/"
18945
18860
  }),
18861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "下一页" }),
18946
18862
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18947
- className: "line-clamp-2 min-w-0 flex-1 text-[30px] font-semibold leading-[1.35] text-foreground select-text",
18948
- style: { overflowWrap: "break-word" },
18949
- children: cell.item.title || "无标题"
18863
+ className: "text-foreground/30",
18864
+ children: "/"
18950
18865
  }),
18951
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18952
- className: "shrink-0 text-[24px] text-muted tabular-nums",
18953
- children: formatDuration$4(cell.item.duration)
18954
- })
18866
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommandChip, { children: "第?页" }),
18867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "翻页" })
18955
18868
  ]
18956
- }, cell.episode))
18957
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
18958
- className: "mt-8 grid grid-cols-10 gap-4",
18959
- children: cells.map((cell) => cell.item === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18960
- className: "flex aspect-square flex-col items-center justify-center rounded-3xl border-2 border-dashed border-foreground/20",
18961
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18962
- className: "text-[44px] font-semibold leading-none text-foreground/25 tabular-nums",
18963
- children: String(cell.episode).padStart(padLength, "0")
18964
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18965
- className: "mt-2 text-[22px] font-semibold text-foreground/25",
18966
- children: "缺失"
18967
- })]
18968
- }, cell.episode) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18969
- className: "flex aspect-square flex-col items-center justify-center rounded-3xl border border-foreground/10 bg-foreground/6",
18970
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18971
- className: "text-[44px] font-semibold leading-none text-foreground tabular-nums select-text",
18972
- children: String(cell.episode).padStart(padLength, "0")
18973
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
18974
- className: "mt-2 text-[22px] text-muted tabular-nums",
18975
- children: formatDuration$4(cell.item.duration)
18976
- })]
18977
- }, cell.episode))
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
+ })
18978
18907
  }),
18979
18908
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
18980
- className: "mt-8 flex flex-wrap items-center justify-between gap-6 text-[26px] text-muted",
18981
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [missingCount > 0 && `虚线格为接口未返回的${unit}(${missingCount} ${kind === "collection" ? "个" : "集"}),暂不能解析。`, listedRange && `已列出第 1–${lastEpisode} 集,共更新 ${updated_to_episode} 集。`] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
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", {
18982
18920
  className: "select-text",
18983
18921
  children: ["图片生成于: ", format(/* @__PURE__ */ new Date(), "yyyy-MM-dd HH:mm:ss")]
18984
18922
  })]
@@ -30617,6 +30555,268 @@ var wrapWithErrorHandler = (fn, options) => {
30617
30555
  }
30618
30556
  };
30619
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
+ };
30620
30820
  //#endregion
30621
30821
  //#region src/platform/bilibili/danmaku.ts
30622
30822
  /**
@@ -31901,8 +32101,13 @@ var Bilibili = class extends Base {
31901
32101
  const videoInfo = await this.amagi.bilibili.fetcher.fetchBangumiInfo({ [iddata.isEpid ? "ep_id" : "season_id"]: iddata.realid });
31902
32102
  this.islogin = (await checkCk()).Status === "isLogin";
31903
32103
  this.isVIP = (await checkCk()).isVIP;
31904
- const Episodes = [];
31905
- 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,
31906
32111
  cover: item.cover,
31907
32112
  bvid: item.bvid,
31908
32113
  link: item.short_link,
@@ -31910,8 +32115,23 @@ var Bilibili = class extends Base {
31910
32115
  pub_time: item.pub_time,
31911
32116
  badge: item.badge === "" ? "限免" : item.badge,
31912
32117
  badge_info: item.badge_info
31913
- });
31914
- 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", {
31915
32135
  mainCover: videoInfo.data.result.cover,
31916
32136
  Actors: videoInfo.data.result.actors,
31917
32137
  Evaluate: videoInfo.data.result.evaluate,
@@ -31924,38 +32144,104 @@ var Bilibili = class extends Base {
31924
32144
  UPInfo: videoInfo.data.result.up_info,
31925
32145
  Copyright: videoInfo.data.result.rights.copyright,
31926
32146
  Stat: videoInfo.data.result.stat,
31927
- Episodes,
31928
- length: videoInfo.data.result.episodes.length
31929
- });
31930
- this.e.reply([...img$1, segment.text("请在120秒内输入 第?集 选择集数")]);
31931
- const context = await karin.ctx(this.e, {
31932
- reply: true,
31933
- throwOnTimeout: false
32147
+ Episodes: displayEpisodes,
32148
+ length: sourceEpisodes.length,
32149
+ selectionMode,
32150
+ posterPage,
32151
+ posterPageSize: 15
31934
32152
  });
31935
- if (!context) return true;
31936
- const regex = /第([一二三四五六七八九十百千万0-9]+)集/.exec(context.msg);
31937
- let Episode;
31938
- if (regex && regex[1]) {
31939
- Episode = regex[1];
31940
- if (/^[一二三四五六七八九十百千万]+$/.test(Episode)) Episode = Common.chineseToArabic(Episode).toString();
31941
- this.downloadfilename = videoInfo.data.result.episodes[Number(Episode) - 1].share_copy.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
31942
- 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;
31943
32175
  } else {
31944
- logger$1.debug(Episode);
31945
- 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("未找到该集,请重新发送番剧链接");
31946
32229
  return true;
31947
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正在下载中`);
31948
32234
  const bangumidataBASEURL = bilibiliApiUrls.getBangumiStream({
31949
- cid: videoInfo.data.result.episodes[Number(Episode) - 1].cid,
31950
- ep_id: videoInfo.data.result.episodes[Number(Episode) - 1].ep_id.toString()
32235
+ cid: episodeInfo.cid,
32236
+ ep_id: episodeInfo.ep_id.toString()
31951
32237
  });
31952
32238
  const Params = await genParams(bangumidataBASEURL);
31953
- if (!this.islogin) this.e.reply("B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck");
32239
+ if (!this.islogin) await this.e.reply("B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck");
31954
32240
  const playUrlData = await new Network({
31955
32241
  url: bangumidataBASEURL + Params,
31956
32242
  headers: this.headers
31957
32243
  }).getData();
31958
- if (videoInfo.data.result.episodes[Number(Episode) - 1].badge === "会员" && !this.isVIP) {
32244
+ if (episodeInfo.badge === "会员" && !this.isVIP) {
31959
32245
  logger$1.warn("该CK不是大会员,无法获取视频流");
31960
32246
  return true;
31961
32247
  }
@@ -35942,24 +36228,6 @@ async function burnDouyinDanmaku(videoPath, danmakuList, outputPath, options = {
35942
36228
  return result.status;
35943
36229
  }
35944
36230
  //#endregion
35945
- //#region src/platform/douyin/mix.ts
35946
- /** 句中的「第N集」,集数可以是阿拉伯数字或中文数字,数字两侧允许空格(模板里写的就是「第 N 集」) */
35947
- var EPISODE_IN_TEXT = /第\s*([0-9]+|[零一二三四五六七八九十百千]+)\s*集/;
35948
- /** 整条回复只有集数:「12」「12集」「十二」 */
35949
- var EPISODE_ONLY = /^([0-9]+|[零一二三四五六七八九十百千]+)\s*集?$/;
35950
- /**
35951
- * 从用户回复里解析要看的集数。
35952
- * @param msg 用户回复的文本
35953
- * @returns 集数(从 1 开始);没解析出来返回 null
35954
- */
35955
- var parseMixEpisode = (msg) => {
35956
- const text = msg.trim();
35957
- const raw = (EPISODE_IN_TEXT.exec(text) ?? EPISODE_ONLY.exec(text))?.[1];
35958
- if (!raw) return null;
35959
- const episode = /^[0-9]+$/.test(raw) ? Number(raw) : Common.chineseToArabic(raw);
35960
- return Number.isInteger(episode) && episode > 0 ? episode : null;
35961
- };
35962
- //#endregion
35963
36231
  //#region src/platform/douyin/videoQuality.ts
35964
36232
  /** 档位优先级,从高到低 */
35965
36233
  var QUALITY_PRIORITY = [
@@ -36789,6 +37057,26 @@ async function renderLiveImage(options) {
36789
37057
  });
36790
37058
  }
36791
37059
  //#endregion
37060
+ //#region src/platform/douyin/shortUrl.ts
37061
+ /**
37062
+ * 把分享链接换成抖音签发的 `v.douyin.com` 短链(二维码专用)。
37063
+ *
37064
+ * 短链由 amagi 的 `douyin.shortUrl` 端点签发:同一 target 恒得同一短码,
37065
+ * 内容比任何规范链接都短,能明显降低二维码密度。该端点免登录但仍要 `a_bogus` 签名,
37066
+ * 可能因签名或平台策略失败 —— 失败不阻塞推送,回退原链接即可。
37067
+ * @param target - 待缩短的分享链接(规范链接或视频直链均可)
37068
+ * @returns 短链;签发失败时原样返回 target
37069
+ */
37070
+ var toDouyinShortUrl = async (target) => {
37071
+ try {
37072
+ const shortUrl = (await douyinFetcher.fetchShortUrl({ target })).data?.data;
37073
+ return typeof shortUrl === "string" && shortUrl.length > 0 ? shortUrl : target;
37074
+ } catch (error) {
37075
+ logger$1.debug(`[抖音短链] 签发失败,二维码回退原链接:${error instanceof Error ? error.message : String(error)}`);
37076
+ return target;
37077
+ }
37078
+ };
37079
+ //#endregion
36792
37080
  //#region src/platform/douyin/types.ts
36793
37081
  /**
36794
37082
  * 把 amagi 返回的 aweme 对象装配成渲染/推送使用的作品详情。
@@ -37235,7 +37523,7 @@ var DouYin = class DouYin extends Base {
37235
37523
  } else {
37236
37524
  const aweme = VideoData.data.aweme_detail;
37237
37525
  const userProfile = await this.amagi.douyin.fetcher.fetchUserProfile({ sec_uid: aweme.author.sec_uid });
37238
- const shareLink = isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`;
37526
+ const shareLink = await toDouyinShortUrl(isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`);
37239
37527
  const workInfoImg = await renderWorkImage({
37240
37528
  e: this.e,
37241
37529
  Detail_Data: buildDouyinWorkDetail(aweme, { user_info: userProfile.data }),
@@ -37263,7 +37551,7 @@ var DouYin = class DouYin extends Base {
37263
37551
  Type: isArticle ? "文章" : isVideo ? "视频" : this.is_slides ? "合辑" : "图集",
37264
37552
  CommentsData: douyinCommentsRes.CommentsData,
37265
37553
  CommentLength: douyinCommentsRes.CommentsData.length ?? 0,
37266
- share_url: isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`,
37554
+ share_url: await toDouyinShortUrl(isVideo && selectedVideo ? buildDouyinPlayUrl(selectedVideo.play_addr) : `https://www.douyin.com/${isArticle ? "article" : "note"}/${aweme.aweme_id}`),
37267
37555
  VideoSize: mp4size,
37268
37556
  VideoFPS: FPS,
37269
37557
  ImageLength: imagenum,
@@ -37463,18 +37751,24 @@ var DouYin = class DouYin extends Base {
37463
37751
  const mixInfo = first.mix_info;
37464
37752
  const author = first.author;
37465
37753
  const isPlaylet = typeof mixInfo.statis.total_episode === "number";
37466
- const episodes = awemeList.map((aweme) => ({
37754
+ const totalEpisode = isPlaylet ? mixInfo.statis.total_episode : mixInfo.statis.updated_to_episode;
37755
+ const displayEpisodes = [...awemeList.map((aweme) => ({
37467
37756
  episode: aweme.mix_info.statis.current_episode,
37468
37757
  aweme_id: aweme.aweme_id,
37469
37758
  title: aweme.desc.replace(/#\S+/g, "").trim(),
37470
- duration: aweme.video.duration
37471
- }));
37472
- const img = await Render(this.e, "douyin/mix", {
37759
+ duration: aweme.video.duration,
37760
+ cover: aweme.video.cover?.url_list?.[0] ?? "",
37761
+ originCover: aweme.video.origin_cover?.url_list?.[0] ?? ""
37762
+ }))].sort((a, b) => b.episode - a.episode);
37763
+ const buttonSelection = isButtonSelectionScene(this.e);
37764
+ const selectionMode = buttonSelection ? "button" : "text";
37765
+ /** 按海报页码渲染:15 集一页,长合集不会撑爆单张 PNG */
37766
+ const renderPoster = (posterPage) => Render(this.e, "douyin/mix", {
37473
37767
  kind: isPlaylet ? "playlet" : "collection",
37474
37768
  mix_name: mixInfo.mix_name,
37475
37769
  image_url: mixInfo.cover_url.url_list[0],
37476
- desc: isPlaylet ? episodes[0].title : "",
37477
- total_episode: isPlaylet ? mixInfo.statis.total_episode : mixInfo.statis.updated_to_episode,
37770
+ desc: isPlaylet ? displayEpisodes[0]?.title ?? "" : "",
37771
+ total_episode: totalEpisode,
37478
37772
  updated_to_episode: mixInfo.statis.updated_to_episode,
37479
37773
  author: {
37480
37774
  nickname: author.nickname,
@@ -37488,29 +37782,92 @@ var DouYin = class DouYin extends Base {
37488
37782
  digg_count: awemeList.reduce((sum, aweme) => sum + aweme.statistics.digg_count, 0),
37489
37783
  collect_count: awemeList.reduce((sum, aweme) => sum + aweme.statistics.collect_count, 0)
37490
37784
  },
37491
- episodes,
37492
- timeoutSeconds
37493
- });
37494
- await this.e.reply([...img, segment.text(`请在${timeoutSeconds}秒内输入 第?集 选择集数`)]);
37495
- const context = await karin.ctx(this.e, {
37496
- reply: true,
37497
- throwOnTimeout: false,
37498
- time: timeoutSeconds
37785
+ episodes: displayEpisodes,
37786
+ timeoutSeconds,
37787
+ selectionMode,
37788
+ posterPage,
37789
+ posterPageSize: 15
37499
37790
  });
37500
- if (!context) return true;
37501
- const episode = parseMixEpisode(context.msg);
37502
- const target = episode === null ? void 0 : episodes.find((item) => item.episode === episode);
37503
- if (!target) {
37504
- await this.e.reply(episode === null ? "匹配内容失败,请重新发送链接再次解析" : `第${episode}集不在可解析的剧集里,请重新发送链接再次解析`);
37791
+ if (buttonSelection) {
37792
+ const initialPage = 1;
37793
+ const poster = await renderPoster(initialPage);
37794
+ const session = createEpisodeSelection(getEpisodeSelectionScope(this.e), {
37795
+ title: mixInfo.mix_name,
37796
+ platform: "douyin",
37797
+ scene: this.e.isFriend ? "friend" : "group",
37798
+ poster,
37799
+ posterPage: initialPage,
37800
+ renderPoster,
37801
+ episodes: displayEpisodes.map((item) => ({
37802
+ episode: item.episode,
37803
+ target: {
37804
+ platform: "douyin",
37805
+ awemeId: String(item.aweme_id)
37806
+ }
37807
+ })),
37808
+ forceBurnDanmaku: this.forceBurnDanmaku
37809
+ });
37810
+ markParseStatSkipped(this.e);
37811
+ const sentAt = Date.now();
37812
+ setEpisodeSelectionMessage(session, (await this.e.reply(buildEpisodeSelectionReply(session, initialPage, poster))).messageId, sentAt);
37813
+ return true;
37814
+ }
37815
+ const posterPageCount = Math.ceil(displayEpisodes.length / 15);
37816
+ let posterPage = 1;
37817
+ /** 上一页海报消息,翻页发送成功后撤回,避免聊天里堆一串过期海报 */
37818
+ let posterMessageId = "";
37819
+ let posterMessageSentAt = 0;
37820
+ for (;;) {
37821
+ const poster = await renderPoster(posterPage);
37822
+ const start = (posterPage - 1) * 15;
37823
+ const pageEpisodes = displayEpisodes.slice(start, start + 15);
37824
+ const firstEpisode = pageEpisodes[0]?.episode ?? 0;
37825
+ const lastEpisode = pageEpisodes[pageEpisodes.length - 1]?.episode ?? 0;
37826
+ const range = firstEpisode === lastEpisode ? `第 ${firstEpisode} 集` : `第 ${firstEpisode}–${lastEpisode} 集`;
37827
+ const sentAt = Date.now();
37828
+ const result = await this.e.reply([...poster, segment.text(`《${mixInfo.mix_name}》\n${range}(共 ${totalEpisode} 集,已列出 ${displayEpisodes.length} 集)\n第 ${posterPage}/${posterPageCount} 页\n发送 第?集 选集${posterPageCount > 1 ? ",发送 上一页 / 下一页 / 第?页 翻页" : ""}(${timeoutSeconds} 秒内有效)`)]);
37829
+ const previousMessageId = posterMessageId;
37830
+ const previousMessageSentAt = posterMessageSentAt;
37831
+ posterMessageId = result.messageId;
37832
+ posterMessageSentAt = result.messageId ? sentAt : 0;
37833
+ if (previousMessageId && canRecallEpisodeMessage(previousMessageId, previousMessageSentAt)) try {
37834
+ await this.e.bot.recallMsg(this.e.contact, previousMessageId);
37835
+ } catch (error) {
37836
+ logger.debug("[选集] 撤回上一页失败:", error);
37837
+ }
37838
+ const context = await karin.ctx(this.e, {
37839
+ reply: true,
37840
+ throwOnTimeout: false,
37841
+ time: timeoutSeconds
37842
+ });
37843
+ if (!context) return true;
37844
+ const replyText = context.msg.trim();
37845
+ if (/^[#/]/.test(replyText) || /(https?:\/\/|b23\.tv|bilibili\.com|douyin\.com|iesdouyin\.com)/i.test(replyText)) return true;
37846
+ const pageAction = parseEpisodePosterPageAction(context.msg);
37847
+ if (pageAction) {
37848
+ const nextPage = pageAction.type === "prev" ? posterPage - 1 : pageAction.type === "next" ? posterPage + 1 : pageAction.page;
37849
+ if (nextPage < 1 || nextPage > posterPageCount) {
37850
+ await this.e.reply(`页码超出范围,请发送 1-${posterPageCount} 之间的页码`);
37851
+ continue;
37852
+ }
37853
+ posterPage = nextPage;
37854
+ continue;
37855
+ }
37856
+ const episode = parseEpisodeNumber(context.msg);
37857
+ const target = episode === null ? void 0 : displayEpisodes.find((item) => item.episode === episode);
37858
+ if (!target) {
37859
+ await this.e.reply(episode === null ? "匹配内容失败,请发送 第?集 选择集数,或发送 上一页 / 下一页 / 第?页 翻页" : `第${episode}集不在可解析的剧集里,请重新选择`);
37860
+ continue;
37861
+ }
37862
+ const episodeTitle = (isPlaylet ? mixInfo.mix_name : target.title || mixInfo.mix_name).replace(/\s+/g, " ").trim();
37863
+ await this.e.reply(`收到请求,第${episode}集\n${episodeTitle}\n正在解析中`);
37864
+ const targetData = {
37865
+ type: "one_work",
37866
+ aweme_id: target.aweme_id
37867
+ };
37868
+ await new DouYin(context, targetData, { forceBurnDanmaku: this.forceBurnDanmaku }).DouyinHandler(targetData);
37505
37869
  return true;
37506
37870
  }
37507
- await this.e.reply(`收到请求,第${episode}集\n${mixInfo.mix_name}\n正在解析中`);
37508
- const targetData = {
37509
- type: "one_work",
37510
- aweme_id: target.aweme_id
37511
- };
37512
- await new DouYin(context, targetData, { forceBurnDanmaku: this.forceBurnDanmaku }).DouyinHandler(targetData);
37513
- return true;
37514
37871
  }
37515
37872
  case "music_work": {
37516
37873
  this.workType = "music";
@@ -37969,26 +38326,6 @@ async function processRecommendList(contentList, sec_uid, userinfo, item, target
37969
38326
  return result;
37970
38327
  }
37971
38328
  //#endregion
37972
- //#region src/platform/douyin/shortUrl.ts
37973
- /**
37974
- * 把分享链接换成抖音签发的 `v.douyin.com` 短链(二维码专用)。
37975
- *
37976
- * 短链由 amagi 的 `douyin.shortUrl` 端点签发:同一 target 恒得同一短码,
37977
- * 内容比任何规范链接都短,能明显降低二维码密度。该端点免登录但仍要 `a_bogus` 签名,
37978
- * 可能因签名或平台策略失败 —— 失败不阻塞推送,回退原链接即可。
37979
- * @param target - 待缩短的分享链接(规范链接或视频直链均可)
37980
- * @returns 短链;签发失败时原样返回 target
37981
- */
37982
- var toDouyinShortUrl = async (target) => {
37983
- try {
37984
- const shortUrl = (await douyinFetcher.fetchShortUrl({ target })).data?.data;
37985
- return typeof shortUrl === "string" && shortUrl.length > 0 ? shortUrl : target;
37986
- } catch (error) {
37987
- logger$1.debug(`[抖音短链] 签发失败,二维码回退原链接:${error instanceof Error ? error.message : String(error)}`);
37988
- return target;
37989
- }
37990
- };
37991
- //#endregion
37992
38329
  //#region src/platform/douyin/push.ts
37993
38330
  var douyinBaseHeaders = {
37994
38331
  ...BASE_HEADERS,
@@ -42024,7 +42361,7 @@ var handleDouyin = wrapWithErrorHandler(async (e, next) => {
42024
42361
  const iddata = await getDouyinID(e, url);
42025
42362
  const douyin = new DouYin(e, iddata, { forceBurnDanmaku });
42026
42363
  await douyin.DouyinHandler(iddata);
42027
- await recordParseStat(e, "douyin", {
42364
+ if (!consumeParseStatSkip(e)) await recordParseStat(e, "douyin", {
42028
42365
  workType: douyin.workType,
42029
42366
  durationMs: Date.now() - startedAt
42030
42367
  });
@@ -42049,7 +42386,7 @@ var handleBilibili = wrapWithErrorHandler(async (e, next) => {
42049
42386
  const iddata = await getBilibiliID(url);
42050
42387
  const bilibili = new Bilibili(e, iddata, { forceBurnDanmaku });
42051
42388
  await bilibili.BilibiliHandler(iddata);
42052
- await recordParseStat(e, "bilibili", {
42389
+ if (!consumeParseStatSkip(e)) await recordParseStat(e, "bilibili", {
42053
42390
  workType: bilibili.workType,
42054
42391
  durationMs: Date.now() - startedAt
42055
42392
  });
@@ -42120,6 +42457,75 @@ var xiaohongshu = karin.command(reg.xiaohongshu, handleXiaohongshu, {
42120
42457
  priority: Config.app.videoTool ? -Infinity : 800
42121
42458
  });
42122
42459
  var prefix = karin.command(/^#?(解析|kkk解析|弹幕解析)/, handlePrefix, { name: "kkk-视频功能-引用解析" });
42460
+ var handleEpisodeSelection = wrapWithErrorHandler(async (e, next) => {
42461
+ if (!isButtonSelectionScene(e)) return next();
42462
+ const match = /^#选集\s+([A-Za-z0-9_-]+)(?:\s+(.+?))?\s*$/.exec(e.msg);
42463
+ if (!match) return next();
42464
+ const [, token, actionText] = match;
42465
+ const session = getEpisodeSelection(token, getEpisodeSelectionScope(e));
42466
+ if (!session) {
42467
+ await e.reply("本次选集已结束,请重新发送原链接", { at: true });
42468
+ return true;
42469
+ }
42470
+ if (!actionText) {
42471
+ await e.reply("请点击下方按钮,或点击「跳转到第x页」输入页码");
42472
+ return true;
42473
+ }
42474
+ const action = parseEpisodeSelectionAction(actionText);
42475
+ if (!action) {
42476
+ await e.reply("集数或页码格式不正确,请重新选择");
42477
+ return true;
42478
+ }
42479
+ if (action.type === "page") {
42480
+ if (!getEpisodePage(session, action.page)) {
42481
+ await e.reply("页码超出范围,请重新选择");
42482
+ return true;
42483
+ }
42484
+ const poster = await getEpisodeSelectionPoster(session, action.page);
42485
+ const previousMessageId = session.currentMessageId;
42486
+ const sentAt = Date.now();
42487
+ const result = await e.reply(buildEpisodeSelectionReply(session, action.page, poster));
42488
+ const shouldRecall = canRecallEpisodeSelectionMessage(session);
42489
+ setEpisodeSelectionMessage(session, result.messageId, sentAt);
42490
+ if (previousMessageId && shouldRecall) await e.bot.recallMsg(e.contact, previousMessageId).catch((error) => {
42491
+ logger$1.debug("[选集] 撤回上一页失败:", error);
42492
+ });
42493
+ touchEpisodeSelection(token, session.scope);
42494
+ return true;
42495
+ }
42496
+ const target = resolveEpisodeTarget(session, action.episode);
42497
+ if (!target) {
42498
+ await e.reply(`第${action.episode}集不在可解析的剧集里,请重新选择`);
42499
+ return true;
42500
+ }
42501
+ if (target.platform === "douyin" && !Config.douyin.switch) {
42502
+ endEpisodeSelection(token, session.scope);
42503
+ await e.reply("抖音解析未开启");
42504
+ return true;
42505
+ }
42506
+ if (target.platform === "bilibili" && !Config.bilibili.switch) {
42507
+ endEpisodeSelection(token, session.scope);
42508
+ await e.reply("B站解析未开启");
42509
+ return true;
42510
+ }
42511
+ if (!endEpisodeSelection(token, session.scope)) {
42512
+ await e.reply("本次选集已结束,请重新发送原链接");
42513
+ return true;
42514
+ }
42515
+ const originalMsg = e.msg;
42516
+ e.msg = `${session.forceBurnDanmaku ? "#弹幕解析 " : "#解析"}${buildEpisodeTargetUrl(target)}`;
42517
+ try {
42518
+ if (target.platform === "douyin") await handleDouyin(e, () => {});
42519
+ else {
42520
+ markBilibiliEpisodeAutoSelect(e);
42521
+ await handleBilibili(e, () => {});
42522
+ }
42523
+ } finally {
42524
+ e.msg = originalMsg;
42525
+ }
42526
+ return true;
42527
+ }, { businessName: "KKK选集" });
42528
+ var episodeSelection = karin.command(/^#选集\s+/, handleEpisodeSelection, { name: "kkk-视频功能-选集" });
42123
42529
  var douyinAPP = Config.douyin.switch && douyin;
42124
42530
  var bilibiliAPP = Config.bilibili.switch && bilibili;
42125
42531
  var kuaishouAPP = Config.kuaishou.switch && kuaishou;
@@ -43658,4 +44064,4 @@ mkdirSync(`${karinPathBase}/${Root.pluginName}/data`);
43658
44064
  mkdirSync(Common.tempDri.images);
43659
44065
  mkdirSync(Common.tempDri.video);
43660
44066
  //#endregion
43661
- 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 };
44067
+ 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 };