koishi-plugin-kkk 3.4.2 → 3.6.0

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/assets/web/assets/index-BRROK0vh.js +1 -1
  3. package/config/default_config/app.yaml +4 -6
  4. package/config/default_config/bilibili.yaml +1 -1
  5. package/config/default_config/config.json +2 -2
  6. package/lib/build-metadata.json +10 -0
  7. package/lib/compat/adapter-info.js +333 -0
  8. package/lib/compat/forward-collect.js +57 -4
  9. package/lib/compat/node-karin.js +25 -56
  10. package/lib/compat/sendError.js +27 -0
  11. package/lib/index.js +15 -8
  12. package/lib/karin/apps/help.js +23 -2
  13. package/lib/karin/module/utils/Base.js +30 -8
  14. package/lib/karin/module/utils/ErrorHandler/render.js +8 -2
  15. package/lib/karin/module/utils/InteractiveGraph.js +164 -0
  16. package/lib/karin/module/utils/InteractiveVideo.js +316 -0
  17. package/lib/karin/module/utils/Network/Downloader.js +159 -13
  18. package/lib/karin/module/utils/Network/Network.js +5 -1
  19. package/lib/karin/module/utils/ParseForward.js +12 -2
  20. package/lib/karin/module/utils/ParseSteps.js +1 -1
  21. package/lib/karin/module/utils/Render/index.js +10 -110
  22. package/lib/karin/module/utils/build-metadata.js +64 -19
  23. package/lib/karin/module/utils/runtime-report.js +16 -4
  24. package/lib/karin/platform/bilibili/bilibili.js +137 -11
  25. package/lib/karin/platform/bilibili/interactive-story.js +463 -0
  26. package/lib/karin/platform/bilibili/push.js +12 -5
  27. package/lib/ktr/registry.js +1 -0
  28. package/lib/ktr/template/bilibili/interactive/components/interactive.js +137 -0
  29. package/lib/ktr/template/bilibili/interactive/components/types.js +3 -0
  30. package/lib/ktr/template/bilibili/interactive/index.js +13 -0
  31. package/lib/ktr/template/other/handlerError/components/handlerError.js +1 -1
  32. package/lib/webui.js +40 -18
  33. package/package.json +1 -1
  34. package/scripts/build.mjs +36 -0
  35. package/scripts/patch-webui.mjs +126 -40
  36. package/scripts/probe-edge-cookie.cjs +50 -0
  37. package/scripts/probe-panel-options.cjs +15 -0
  38. package/scripts/probe-schema-options.cjs +12 -0
  39. package/scripts/smoke-adapter-info.cjs +149 -0
  40. package/scripts/smoke-config-schema.cjs +62 -0
  41. package/scripts/smoke-download-source-fallback.cjs +166 -0
  42. package/scripts/smoke-forward-chart.cjs +139 -0
  43. package/scripts/smoke-interactive-card.cjs +87 -0
  44. package/scripts/smoke-interactive-story.cjs +285 -0
  45. package/scripts/smoke-interactive.cjs +298 -0
  46. package/scripts/smoke-onebot-forward.cjs +10 -5
  47. package/scripts/smoke-panel-chart.cjs +92 -0
  48. package/scripts/smoke-parse-forward.cjs +1 -1
  49. package/scripts/smoke-render-path.cjs +44 -55
  50. package/src/compat/adapter-info.ts +346 -0
  51. package/src/compat/forward-collect.ts +69 -4
  52. package/src/compat/node-karin.ts +24 -54
  53. package/src/compat/sendError.ts +26 -0
  54. package/src/index.ts +15 -8
  55. package/src/karin/apps/help.ts +26 -3
  56. package/src/karin/module/utils/Base.ts +39 -9
  57. package/src/karin/module/utils/ErrorHandler/render.ts +8 -2
  58. package/src/karin/module/utils/InteractiveGraph.ts +199 -0
  59. package/src/karin/module/utils/InteractiveVideo.ts +400 -0
  60. package/src/karin/module/utils/Network/Downloader.ts +164 -14
  61. package/src/karin/module/utils/Network/Network.ts +6 -2
  62. package/src/karin/module/utils/ParseForward.ts +12 -3
  63. package/src/karin/module/utils/ParseSteps.ts +1 -1
  64. package/src/karin/module/utils/Render/index.ts +10 -113
  65. package/src/karin/module/utils/build-metadata.ts +61 -19
  66. package/src/karin/module/utils/runtime-report.ts +16 -4
  67. package/src/karin/platform/bilibili/bilibili.ts +149 -20
  68. package/src/karin/platform/bilibili/interactive-story.ts +519 -0
  69. package/src/karin/platform/bilibili/push.ts +12 -6
  70. package/src/karin/types/config/app.ts +7 -15
  71. package/src/karin/types/config/bilibili.ts +1 -1
  72. package/src/karin/types/netrorks/NetworksConfigType.ts +5 -0
  73. package/src/ktr/registry.ts +1 -0
  74. package/src/ktr/template/bilibili/interactive/components/interactive.tsx +202 -0
  75. package/src/ktr/template/bilibili/interactive/components/types.ts +37 -0
  76. package/src/ktr/template/bilibili/interactive/index.tsx +15 -0
  77. package/src/ktr/template/other/handlerError/components/handlerError.tsx +4 -1
  78. package/src/webui.ts +38 -16
  79. package/scripts/smoke-shotkit-compat.cjs +0 -102
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_karin_1 = require("../../../compat/node-karin.js");
11
+ const adapter_info_1 = require("../../../compat/adapter-info");
11
12
  const root_1 = require("../../root");
12
13
  const build_metadata_1 = require("./build-metadata");
13
14
  const Config_1 = require("./Config");
@@ -60,10 +61,16 @@ exports.getLocalChangelog = getLocalChangelog;
60
61
  * 不采集账号、主机名、用户目录、网络地址、环境变量内容、启动参数或适配器鉴权信息,
61
62
  * 保证该命令在群聊中触发时不会把机器身份和凭据写入图片。
62
63
  *
64
+ * 采集前会尝试问一句适配器的实现端版本(NapCat / Lagrange 这类),带 1.5 秒超时,
65
+ * 问不到就退化成「适配器插件版本」,绝不让海报卡住。
66
+ *
63
67
  * @param event 当前消息事件
64
68
  */
65
- const collectRuntimeReport = (event) => {
69
+ const collectRuntimeReport = async (event) => {
66
70
  const packageMetadata = root_1.Root.pkg;
71
+ // 兼容层里 e.bot 是包装对象,真实 Koishi Bot 在 .bot 上:问实现端版本要用真的那个
72
+ const koishiBot = event.bot?.bot ?? event.bot;
73
+ await (0, adapter_info_1.queryAdapterImplementation)(koishiBot);
67
74
  const adapter = event.bot.adapter;
68
75
  const cpus = node_os_1.default.cpus();
69
76
  const memory = process.memoryUsage();
@@ -72,7 +79,10 @@ const collectRuntimeReport = (event) => {
72
79
  const buildMetadata = (0, build_metadata_1.getBuildMetadata)();
73
80
  const currentChangelog = (0, exports.getLocalChangelog)(1);
74
81
  const renderScale = Math.min(2, Math.max(0.5, Number(Config_1.Config.app.renderScale) / 100));
75
- const buildState = !buildMetadata ? 'unavailable' : buildMetadata.version === root_1.Root.pluginVersion ? 'matched' : 'mismatched';
82
+ // 没有构建产物(老安装包)时依然会拿到一份 package.json 兜底元数据,所以用「有没有构建时间」判断
83
+ const buildState = !buildMetadata.buildTime
84
+ ? 'unavailable'
85
+ : buildMetadata.version === root_1.Root.pluginVersion ? 'matched' : 'mismatched';
76
86
  return {
77
87
  snapshotAt: new Intl.DateTimeFormat('zh-CN', {
78
88
  year: 'numeric',
@@ -109,13 +119,15 @@ const collectRuntimeReport = (event) => {
109
119
  processUptime: formatDuration(process.uptime())
110
120
  },
111
121
  adapter: {
112
- name: adapter.name || '未知',
122
+ // 卡片上要好看:NapCat / OneBot 这种;取不到就退回平台名
123
+ name: adapter.displayName || adapter.name || '未知',
113
124
  version: adapter.version || '未知',
114
125
  platform: String(adapter.platform || '未知'),
115
126
  protocol: String(adapter.protocol || '未知'),
116
127
  standard: String(adapter.standard || '未知'),
117
128
  communication: String(adapter.communication || '未知'),
118
- connectedFor: formatDuration(Math.max(0, (Date.now() - adapter.connectTime) / 1000))
129
+ // connectTime 为 0 表示「没记到上线时间」,此时别拿 1970 年去算天数
130
+ connectedFor: adapter.connectTime > 0 ? formatDuration(Math.max(0, (Date.now() - adapter.connectTime) / 1000)) : '未知'
119
131
  },
120
132
  renderer: {
121
133
  scale: `${renderScale.toFixed(2)}x`,
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.extractArticleImages = exports.getvideosize = exports.bilibiliProcessVideos = exports.preferAvcStream = exports.isAvcStream = exports.parseAdditionalCard = exports.generateDecorationCard = exports.cover = exports.TimeFormatter = exports.Bilibili = void 0;
39
+ exports.extractArticleImages = exports.getvideosize = exports.bilibiliProcessVideos = exports.preferAvcStream = exports.isAvcStream = exports.parseAdditionalCard = exports.generateDecorationCard = exports.cover = exports.TimeFormatter = exports.Bilibili = exports.buildDashUrlCandidates = void 0;
40
40
  const node_fs_1 = __importDefault(require("node:fs"));
41
41
  const ImageSlice_1 = require("../../module/utils/ImageSlice");
42
42
  const QqPanel_1 = require("../../module/utils/QqPanel");
@@ -55,6 +55,7 @@ const QqPanel_2 = require("../../module/utils/QqPanel");
55
55
  const utils_1 = require("../../module/utils");
56
56
  const amagiClient_1 = require("../../module/utils/amagiClient");
57
57
  const Config_1 = require("../../module/utils/Config");
58
+ const InteractiveVideo_1 = require("../../module/utils/InteractiveVideo");
58
59
  const ParseOverride_1 = require("../../module/utils/ParseOverride");
59
60
  // 解析阶段(「下载进度」指令读的就是这里登记的状态)
60
61
  const Downloader_1 = require("../../module/utils/Network/Downloader");
@@ -62,7 +63,44 @@ const parseTip_1 = require("../../module/utils/parseTip");
62
63
  const bilibili_1 = require("../../platform/bilibili");
63
64
  const danmaku_1 = require("../../platform/bilibili/danmaku");
64
65
  const dynamic_text_1 = require("../../platform/bilibili/dynamic-text");
66
+ const interactive_story_1 = require("../../platform/bilibili/interactive-story");
65
67
  let img;
68
+ /**
69
+ * 一个地址是不是 PCDN 边缘节点。
70
+ *
71
+ * `xy120x240x109x22xy.mcdn.bilivideo.cn:8082/v1/resource/upgcxcode/...` 这种就是:
72
+ * 域名带 mcdn、带非标准端口、路径 `/v1/resource/`。
73
+ * 它们对本机来说常常是「TCP 连得上、响应头也正常,就是不吐数据」(实测 118 KB / 60 秒),
74
+ * 而同一份数据在 `upos-*` 镜像上能跑满带宽。
75
+ */
76
+ const isPcdnUrl = (url) => /\.mcdn\.bilivideo\./i.test(url) || /\.pcdn\./i.test(url) || /\/v1\/resource\//i.test(url);
77
+ /**
78
+ * 把一条 dash 流的直链整理成**按优先级排序**的候选列表。
79
+ *
80
+ * 排序规则(尽量不改变原来的行为,只在必要时动):
81
+ * 1. `base_url` 不是 PCDN 时保持第一 —— 原来的行为不变;
82
+ * 2. `base_url` 是 PCDN 时,把非 PCDN 的备用地址提到前面;
83
+ * 3. 其余地址按接口给的顺序跟在后面。
84
+ *
85
+ * 下载器遇到「连得上但不吐数据」的源会自动换到下一个(见 Downloader 的坏源看门狗),
86
+ * 所以列表顺序就是换源顺序。
87
+ *
88
+ * @param stream dash 里的 video / audio 流对象
89
+ * @returns 去重且已排序的直链列表(可能为空)
90
+ */
91
+ const buildDashUrlCandidates = (stream) => {
92
+ const raw = [stream?.base_url ?? stream?.baseUrl, ...(stream?.backup_url ?? stream?.backupUrl ?? [])];
93
+ const seen = new Set();
94
+ const urls = [];
95
+ for (const url of raw) {
96
+ if (typeof url !== 'string' || !/^https?:\/\//i.test(url) || seen.has(url))
97
+ continue;
98
+ seen.add(url);
99
+ urls.push(url);
100
+ }
101
+ return [...urls.filter((url) => !isPcdnUrl(url)), ...urls.filter(isPcdnUrl)];
102
+ };
103
+ exports.buildDashUrlCandidates = buildDashUrlCandidates;
66
104
  /** 评论请求统一使用匿名态,避免账号 Cookie 改变评论热度池结果。 */
67
105
  const bilibiliAnonymousRequestConfig = {
68
106
  headers: {
@@ -90,6 +128,10 @@ class Bilibili extends utils_1.Base {
90
128
  downloadfilename;
91
129
  /** 强制烧录弹幕(用于 #弹幕解析 命令) */
92
130
  forceBurnDanmaku;
131
+ /** 只发这一段视频:互动剧情续播用,不发作品卡与评论区 */
132
+ storyOnly;
133
+ /** 本次是不是互动视频;是的话视频发完要发剧情选项 */
134
+ interactive;
93
135
  /**
94
136
  * 本次解析取到的弹幕。
95
137
  *
@@ -113,6 +155,7 @@ class Bilibili extends utils_1.Base {
113
155
  super(e);
114
156
  this.e = e;
115
157
  this.isVIP = false;
158
+ this.storyOnly = options?.storyOnly ?? false;
116
159
  this.Type = data?.type;
117
160
  this.workType = BILIBILI_WORK_TYPES[data?.type];
118
161
  this.islogin = data?.USER?.STATUS === 'isLogin';
@@ -134,7 +177,7 @@ class Bilibili extends utils_1.Base {
134
177
  // replyReplacing 会先撤掉上一条(也就是刚点的画质面板),群里只留这句提示
135
178
  await (0, QqPanel_2.replyReplacing)(this.e, (0, QqPanel_2.buildDownloadTip)(String(iddata.bvid ?? ''), '收到请求,开始下载'));
136
179
  }
137
- else if (Config_1.Config.app.parseTip) {
180
+ else if (!this.storyOnly && Config_1.Config.app.parseTip) {
138
181
  /**
139
182
  * 同样:发这句话时把上一条机器人消息撤掉。
140
183
  * 另外过一道「同一句提示 5 秒内只发一次」—— 同一条消息被投递多遍时
@@ -180,9 +223,18 @@ class Bilibili extends utils_1.Base {
180
223
  durationSeconds: Number(detail.duration) > 0 ? Number(detail.duration) : undefined
181
224
  };
182
225
  }
226
+ /**
227
+ * 本次要解析的目标 cid:普通视频按分 P / 主 cid;互动剧情的续播节点由上层直接指定 cid。
228
+ */
229
+ const targetCid = Number(iddata.cid ?? (iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid));
230
+ /** 互动视频(stein gate):视频发完要发剧情选项,续播的那几段不再重复识别 */
231
+ if (!this.storyOnly && Number(infoData.data.data.rights?.is_stein_gate) === 1) {
232
+ this.interactive = { bvid: String(iddata.bvid ?? infoData.data.data.bvid ?? ''), cid: targetCid };
233
+ node_karin_1.logger.info('[互动视频] 检测到互动视频(bv=' + this.interactive.bvid + ' cid=' + targetCid + ')');
234
+ }
183
235
  const playUrlData = await this.amagi.bilibili.fetcher.fetchVideoStreamUrl({
184
236
  avid: infoData.data.data.aid,
185
- cid: iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
237
+ cid: targetCid
186
238
  });
187
239
  // const playUrl = bilibiliApiUrls.视频流信息({ avid: infoData.data.aid, cid: infoData.data.cid })
188
240
  /**
@@ -209,7 +261,7 @@ class Bilibili extends utils_1.Base {
209
261
  const nockData = (await new utils_1.Networks({
210
262
  url: amagi_1.bilibiliApiUrls.getVideoStream({
211
263
  avid: infoData.data.data.aid,
212
- cid: iddata.p ? (infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid) : infoData.data.data.cid
264
+ cid: targetCid
213
265
  }) + '&platform=html5',
214
266
  headers: this.headers
215
267
  }).getData());
@@ -221,7 +273,7 @@ class Bilibili extends utils_1.Base {
221
273
  */
222
274
  // fromPanel:面板里已经发过这张卡片了,别再发一遍
223
275
  const renderInfoCard = async () => {
224
- if (fromPanel || !Config_1.Config.bilibili.sendContent.some((content) => content === 'info'))
276
+ if (this.storyOnly || fromPanel || !Config_1.Config.bilibili.sendContent.some((content) => content === 'info'))
225
277
  return;
226
278
  if (Config_1.Config.bilibili.videoInfoMode === 'text') {
227
279
  // 构建回复内容数组
@@ -419,7 +471,7 @@ class Bilibili extends utils_1.Base {
419
471
  sends.add('渲染作品信息卡', renderInfoCard);
420
472
  // 评论区同样只跳过自身
421
473
  sends.add('渲染评论区', async () => {
422
- if (!Config_1.Config.bilibili.sendContent.some((content) => content === 'comment'))
474
+ if (this.storyOnly || !Config_1.Config.bilibili.sendContent.some((content) => content === 'comment'))
423
475
  return;
424
476
  const commentsData = await (0, amagiClient_1.softFetch)(() => this.amagi.bilibili.fetcher.fetchComments({
425
477
  number: Config_1.Config.bilibili.numcomment,
@@ -506,6 +558,9 @@ class Bilibili extends utils_1.Base {
506
558
  sends.add('发送视频', async () => {
507
559
  await downloadTask;
508
560
  await this.sendPreparedVideo();
561
+ /** 互动视频:视频发完再发剧情(后台跑,不阻塞解析主流程) */
562
+ if (this.interactive)
563
+ void this.startInteractiveStory();
509
564
  });
510
565
  }
511
566
  }
@@ -1340,15 +1395,24 @@ class Bilibili extends utils_1.Base {
1340
1395
  this.danmakuList = danmakuList;
1341
1396
  switch (this.islogin) {
1342
1397
  case true: {
1343
- node_karin_1.logger.debug('视频 URL:', this.Type === 'one_video' ? playUrlData.data?.dash?.video[0].base_url : playUrlData.result.dash.video[0].base_url);
1398
+ const videoStream = this.Type === 'one_video' ? playUrlData.data?.dash?.video[0] : playUrlData.result.dash.video[0];
1399
+ node_karin_1.logger.debug('视频 URL:', videoStream?.base_url);
1344
1400
  // B站 CDN 需要正确的 Referer
1345
1401
  const downloadHeaders = {
1346
1402
  ...this.headers,
1347
1403
  Referer: 'https://www.bilibili.com'
1348
1404
  };
1349
- const bmp4Raw = await (0, utils_1.downloadFile)(this.Type === 'one_video' ? playUrlData.data?.dash?.video[0].base_url : playUrlData.result.dash.video[0].base_url, {
1405
+ /**
1406
+ * 直链候选:`base_url`(常是 mcdn/PCDN 节点)+ `backup_url` 镜像。
1407
+ * 排在前面的先试,坏源由下载器自动换掉 —— 不然一个死节点就能让整个解析卡死。
1408
+ */
1409
+ const videoUrls = (0, exports.buildDashUrlCandidates)(videoStream);
1410
+ if (videoUrls.length === 0)
1411
+ throw new Error('没有拿到视频流直链(playurl 返回里没有 base_url)');
1412
+ const bmp4Raw = await (0, utils_1.downloadFile)(videoUrls[0], {
1350
1413
  title: `Bil_V_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.m4s`,
1351
- headers: downloadHeaders
1414
+ headers: downloadHeaders,
1415
+ backupUrls: videoUrls.slice(1)
1352
1416
  });
1353
1417
  // 修复 m4s 文件为标准 MP4
1354
1418
  const videoPath = utils_1.Common.tempDri.video +
@@ -1360,14 +1424,18 @@ class Bilibili extends utils_1.Base {
1360
1424
  }
1361
1425
  // 删除原始 m4s 文件
1362
1426
  await utils_1.Common.removeFile(bmp4Raw.filepath, true);
1363
- const audioUrl = this.Type === 'one_video' ? playUrlData.data?.dash?.audio?.[0]?.base_url : playUrlData.result.dash.audio?.[0]?.base_url;
1427
+ const audioStream = this.Type === 'one_video' ? playUrlData.data?.dash?.audio?.[0] : playUrlData.result.dash.audio?.[0];
1428
+ /** 音频候选同样带镜像,理由见上面视频那段 */
1429
+ const audioUrls = (0, exports.buildDashUrlCandidates)(audioStream);
1430
+ const audioUrl = audioUrls[0];
1364
1431
  node_karin_1.logger.debug('音频 URL:', audioUrl);
1365
1432
  /** 没有音频流(如纯视频稿件)时为 undefined,此时无从合成,直接发视频流 */
1366
1433
  let bmp3;
1367
1434
  if (audioUrl) {
1368
1435
  const bmp3Raw = await (0, utils_1.downloadFile)(audioUrl, {
1369
1436
  title: `Bil_A_${this.Type === 'one_video' ? infoData && infoData.data.bvid : infoData && infoData.result.season_id}.m4s`,
1370
- headers: downloadHeaders
1437
+ headers: downloadHeaders,
1438
+ backupUrls: audioUrls.slice(1)
1371
1439
  });
1372
1440
  // 修复音频 m4s 文件为 m4a(AAC 音频不能直接转为 MP3 容器)
1373
1441
  const audioPath = utils_1.Common.tempDri.video +
@@ -1551,6 +1619,64 @@ class Bilibili extends utils_1.Base {
1551
1619
  }
1552
1620
  return this.preparedVideo !== null;
1553
1621
  }
1622
+ /**
1623
+ * 起一次互动剧情(后台长跑,不阻塞解析主流程)。
1624
+ *
1625
+ * 选项与等待都在 interactive-story 里;这里只负责:问一次剧情图版本号 → 跑剧情 → 续播时
1626
+ * 用**用户刚发的那条消息**重新走一遍解析链路(storyOnly:只发视频,不发卡片与评论区)。
1627
+ */
1628
+ async startInteractiveStory() {
1629
+ const interactive = this.interactive;
1630
+ if (!interactive)
1631
+ return;
1632
+ try {
1633
+ const info = await (0, InteractiveVideo_1.fetchInteractiveInfo)({ bvid: interactive.bvid, cid: interactive.cid, headers: this.headers });
1634
+ if (!info) {
1635
+ node_karin_1.logger.debug('[互动视频] 播放器接口没有 interaction.graph_version,按普通视频处理');
1636
+ return;
1637
+ }
1638
+ const result = await (0, interactive_story_1.runInteractiveStory)({
1639
+ e: this.e,
1640
+ bvid: interactive.bvid,
1641
+ graphVersion: info.graphVersion,
1642
+ rootCid: interactive.cid,
1643
+ title: this.workInfo?.title ?? '',
1644
+ notice: info.notice,
1645
+ headers: this.headers,
1646
+ onNode: async (node, event, path, isFirst, entry) => {
1647
+ /** 整张图第一次就爬完并缓存,之后不再判断「画没画过」:只有第一段和结局出图 */
1648
+ const isEnding = node.isLeaf || !node.choices.length;
1649
+ if (!isFirst && !isEnding)
1650
+ return false;
1651
+ /** 没勾「流程图」就只发选项面板,剧情照常能玩 */
1652
+ if (!Config_1.Config.bilibili.sendContent.some((item) => item === 'chart'))
1653
+ return false;
1654
+ const chart = await (0, interactive_story_1.renderInteractiveChart)({
1655
+ e: event,
1656
+ bvid: interactive.bvid,
1657
+ graphVersion: info.graphVersion,
1658
+ cid: node.cid,
1659
+ entry: entry ?? undefined,
1660
+ title: this.workInfo?.title ?? node.title ?? '互动视频',
1661
+ path,
1662
+ notice: node.notice,
1663
+ question: node.question,
1664
+ isEnding,
1665
+ headers: this.headers
1666
+ });
1667
+ return chart.sent;
1668
+ },
1669
+ play: async (cid, event) => {
1670
+ const story = new Bilibili(event, { type: 'one_video', bvid: interactive.bvid, cid }, { storyOnly: true });
1671
+ await story.BilibiliHandler({ type: 'one_video', bvid: interactive.bvid, cid });
1672
+ }
1673
+ });
1674
+ node_karin_1.logger.info('[互动视频] 剧情结束(' + result.ended + '),共 ' + result.nodes + ' 段:' + (result.path.join(' → ') || '无'));
1675
+ }
1676
+ catch (error) {
1677
+ node_karin_1.logger.warn('[互动视频] 剧情流程异常结束: ' + String(error?.message ?? error));
1678
+ }
1679
+ }
1554
1680
  /**
1555
1681
  * 把 {@link prepareVideo} 下好的视频发出去。
1556
1682
  *