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.
- package/CHANGELOG.md +105 -0
- package/assets/web/assets/index-BRROK0vh.js +1 -1
- package/config/default_config/app.yaml +4 -6
- package/config/default_config/bilibili.yaml +1 -1
- package/config/default_config/config.json +2 -2
- package/lib/build-metadata.json +10 -0
- package/lib/compat/adapter-info.js +333 -0
- package/lib/compat/forward-collect.js +57 -4
- package/lib/compat/node-karin.js +25 -56
- package/lib/compat/sendError.js +27 -0
- package/lib/index.js +15 -8
- package/lib/karin/apps/help.js +23 -2
- package/lib/karin/module/utils/Base.js +30 -8
- package/lib/karin/module/utils/ErrorHandler/render.js +8 -2
- package/lib/karin/module/utils/InteractiveGraph.js +164 -0
- package/lib/karin/module/utils/InteractiveVideo.js +316 -0
- package/lib/karin/module/utils/Network/Downloader.js +159 -13
- package/lib/karin/module/utils/Network/Network.js +5 -1
- package/lib/karin/module/utils/ParseForward.js +12 -2
- package/lib/karin/module/utils/ParseSteps.js +1 -1
- package/lib/karin/module/utils/Render/index.js +10 -110
- package/lib/karin/module/utils/build-metadata.js +64 -19
- package/lib/karin/module/utils/runtime-report.js +16 -4
- package/lib/karin/platform/bilibili/bilibili.js +137 -11
- package/lib/karin/platform/bilibili/interactive-story.js +463 -0
- package/lib/karin/platform/bilibili/push.js +12 -5
- package/lib/ktr/registry.js +1 -0
- package/lib/ktr/template/bilibili/interactive/components/interactive.js +137 -0
- package/lib/ktr/template/bilibili/interactive/components/types.js +3 -0
- package/lib/ktr/template/bilibili/interactive/index.js +13 -0
- package/lib/ktr/template/other/handlerError/components/handlerError.js +1 -1
- package/lib/webui.js +40 -18
- package/package.json +1 -1
- package/scripts/build.mjs +36 -0
- package/scripts/patch-webui.mjs +126 -40
- package/scripts/probe-edge-cookie.cjs +50 -0
- package/scripts/probe-panel-options.cjs +15 -0
- package/scripts/probe-schema-options.cjs +12 -0
- package/scripts/smoke-adapter-info.cjs +149 -0
- package/scripts/smoke-config-schema.cjs +62 -0
- package/scripts/smoke-download-source-fallback.cjs +166 -0
- package/scripts/smoke-forward-chart.cjs +139 -0
- package/scripts/smoke-interactive-card.cjs +87 -0
- package/scripts/smoke-interactive-story.cjs +285 -0
- package/scripts/smoke-interactive.cjs +298 -0
- package/scripts/smoke-onebot-forward.cjs +10 -5
- package/scripts/smoke-panel-chart.cjs +92 -0
- package/scripts/smoke-parse-forward.cjs +1 -1
- package/scripts/smoke-render-path.cjs +44 -55
- package/src/compat/adapter-info.ts +346 -0
- package/src/compat/forward-collect.ts +69 -4
- package/src/compat/node-karin.ts +24 -54
- package/src/compat/sendError.ts +26 -0
- package/src/index.ts +15 -8
- package/src/karin/apps/help.ts +26 -3
- package/src/karin/module/utils/Base.ts +39 -9
- package/src/karin/module/utils/ErrorHandler/render.ts +8 -2
- package/src/karin/module/utils/InteractiveGraph.ts +199 -0
- package/src/karin/module/utils/InteractiveVideo.ts +400 -0
- package/src/karin/module/utils/Network/Downloader.ts +164 -14
- package/src/karin/module/utils/Network/Network.ts +6 -2
- package/src/karin/module/utils/ParseForward.ts +12 -3
- package/src/karin/module/utils/ParseSteps.ts +1 -1
- package/src/karin/module/utils/Render/index.ts +10 -113
- package/src/karin/module/utils/build-metadata.ts +61 -19
- package/src/karin/module/utils/runtime-report.ts +16 -4
- package/src/karin/platform/bilibili/bilibili.ts +149 -20
- package/src/karin/platform/bilibili/interactive-story.ts +519 -0
- package/src/karin/platform/bilibili/push.ts +12 -6
- package/src/karin/types/config/app.ts +7 -15
- package/src/karin/types/config/bilibili.ts +1 -1
- package/src/karin/types/netrorks/NetworksConfigType.ts +5 -0
- package/src/ktr/registry.ts +1 -0
- package/src/ktr/template/bilibili/interactive/components/interactive.tsx +202 -0
- package/src/ktr/template/bilibili/interactive/components/types.ts +37 -0
- package/src/ktr/template/bilibili/interactive/index.tsx +15 -0
- package/src/ktr/template/other/handlerError/components/handlerError.tsx +4 -1
- package/src/webui.ts +38 -16
- package/scripts/smoke-shotkit-compat.cjs +0 -102
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.interactiveChart = exports.renderInteractiveChart = exports.forgetChoiceMessage = exports.toCardGraph = exports.findNode = exports.runInteractiveStory = exports.sendStoryTip = exports.sendInteractiveChoices = exports.buildChoiceMessage = exports.RENDER_CHART_COMMAND = exports.CHOICE_WAIT_SECONDS = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* 互动视频的「发选项 → 等选择 → 播下一段」这一段流程。
|
|
39
|
+
*
|
|
40
|
+
* 数据与清洗逻辑在 module/utils/InteractiveVideo.ts,这里只管**怎么问、怎么等、怎么续**:
|
|
41
|
+
*
|
|
42
|
+
* · QQ(官方适配器)发 markdown,每个选项一个 `<qqbot-…-input>` 按钮,点一下等于发出字母;
|
|
43
|
+
* · 其它适配器发纯文字(A / B / C 各一行),用户回字母或数字;
|
|
44
|
+
* · 等回复用 karin.ctx(同频道同用户的下一句话),默认 180 秒,超时就体面收尾;
|
|
45
|
+
* · 每次只留最新一条选项消息(下一条发出前撤回上一条),不给群里刷屏。
|
|
46
|
+
*
|
|
47
|
+
* 播放下一段视频交给上层注入的 play 回调(走现成的解析/下载/发送链路),
|
|
48
|
+
* 这样这里完全不依赖 bilibili 平台代码,可以用假实现直接测。
|
|
49
|
+
*/
|
|
50
|
+
const node_karin_1 = __importStar(require("../../../compat/node-karin.js"));
|
|
51
|
+
const module_1 = require("../../module");
|
|
52
|
+
const InteractiveGraph_1 = require("../../module/utils/InteractiveGraph");
|
|
53
|
+
const QqPanel_1 = require("../../module/utils/QqPanel");
|
|
54
|
+
const runtime_1 = require("../../../compat/runtime");
|
|
55
|
+
const ErrorHandler_1 = require("../../module/utils/ErrorHandler");
|
|
56
|
+
const InteractiveVideo_1 = require("../../module/utils/InteractiveVideo");
|
|
57
|
+
/** 等用户选择的时间:太长会挂着等待,太短用户还没看完视频 */
|
|
58
|
+
exports.CHOICE_WAIT_SECONDS = 180;
|
|
59
|
+
/** 重新渲染流程图的指令(按钮点的就是它) */
|
|
60
|
+
exports.RENDER_CHART_COMMAND = '渲染流程图';
|
|
61
|
+
/** 每个频道最近一条「选项消息」,用于发下一条之前撤回 */
|
|
62
|
+
const lastChoiceMessages = new Map();
|
|
63
|
+
const platformOf = (e) => String(e?.bot?.bot?.platform ?? e?.bot?.platform ?? e?.platform ?? '');
|
|
64
|
+
const channelOf = (e) => String(e?.contact?.peer ?? e?.guildId ?? '');
|
|
65
|
+
/** 剧情会话 key(和 InteractiveVideo 里的口径一致) */
|
|
66
|
+
const sessionKeyOf = (e) => (0, InteractiveVideo_1.interactiveKey)(platformOf(e), channelOf(e));
|
|
67
|
+
/**
|
|
68
|
+
* 组装选项消息。
|
|
69
|
+
*
|
|
70
|
+
* 标题、进度、题目、B站 的提示语都会带上;QQ 走 markdown + 按钮,其它平台走纯文字。
|
|
71
|
+
*
|
|
72
|
+
* @param node 当前剧情节点
|
|
73
|
+
* @param options 标题 / 已走过的剧情 / 等待秒数 / 是否加按钮
|
|
74
|
+
* @returns 要发送的文本
|
|
75
|
+
*/
|
|
76
|
+
const buildChoiceMessage = (node, options) => {
|
|
77
|
+
const { title, path = [], waitSeconds = exports.CHOICE_WAIT_SECONDS, buttons, detailed = true } = options;
|
|
78
|
+
const lines = [];
|
|
79
|
+
/**
|
|
80
|
+
* **只有第一次带标题、题目和「怎么回复」的说明**。
|
|
81
|
+
*
|
|
82
|
+
* 用户要求:后面的每一次都只发选项本身 —— 一是群里不刷屏,
|
|
83
|
+
* 二是 QQ 官方适配器的被动回复窗口有限,正文越长越容易整条发不出去(40034128)。
|
|
84
|
+
*/
|
|
85
|
+
if (detailed) {
|
|
86
|
+
const heading = [title || node.title || '互动视频', path.length ? '剧情:' + (0, InteractiveVideo_1.formatInteractivePath)(path) : '']
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(' · ');
|
|
89
|
+
lines.push(buttons ? '**' + heading + '**' : '🎬 ' + heading);
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(node.question || '请选择接下来的剧情');
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
for (const choice of node.choices) {
|
|
95
|
+
if (!buttons) {
|
|
96
|
+
lines.push(choice.label + ' ' + choice.text);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 按钮文案也带字母:用户看到的和「回 A」是同一套编号,不会两套编号打架。
|
|
101
|
+
* 选项文字里的引号/括号由 cmdInput 自己编码。
|
|
102
|
+
*/
|
|
103
|
+
lines.push((0, QqPanel_1.cmdInput)(choice.label, choice.label + ' ' + choice.text));
|
|
104
|
+
}
|
|
105
|
+
/** 重新画一张流程图的按钮:走到图外的分支、或者想再看一眼时随手点 */
|
|
106
|
+
lines.push(buttons
|
|
107
|
+
? (0, QqPanel_1.cmdInput)((0, runtime_1.commandInvocation)(exports.RENDER_CHART_COMMAND), '渲染流程图')
|
|
108
|
+
: '回复「' + exports.RENDER_CHART_COMMAND + '」可以重新画一张流程图');
|
|
109
|
+
if (detailed) {
|
|
110
|
+
lines.push('');
|
|
111
|
+
if (node.notice)
|
|
112
|
+
lines.push(node.notice);
|
|
113
|
+
lines.push(buttons
|
|
114
|
+
? '点上面的按钮,或者直接回复字母(' + waitSeconds + ' 秒内有效)'
|
|
115
|
+
: '回复上面的字母或数字继续(' + waitSeconds + ' 秒内有效)');
|
|
116
|
+
}
|
|
117
|
+
return lines.join(String.fromCharCode(10));
|
|
118
|
+
};
|
|
119
|
+
exports.buildChoiceMessage = buildChoiceMessage;
|
|
120
|
+
/** 撤掉这个频道上一条选项消息(撤不掉就算了,绝不让它影响剧情) */
|
|
121
|
+
const recallLastChoices = async (e) => {
|
|
122
|
+
const key = sessionKeyOf(e);
|
|
123
|
+
const messageId = lastChoiceMessages.get(key);
|
|
124
|
+
if (!messageId)
|
|
125
|
+
return;
|
|
126
|
+
lastChoiceMessages.delete(key);
|
|
127
|
+
try {
|
|
128
|
+
await e.bot?.recallMsg?.(messageId, channelOf(e));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
node_karin_1.logger.debug('[互动视频] 撤回上一条选项消息失败: ' + String(error?.message ?? error));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* 发一条剧情选项消息。
|
|
136
|
+
*
|
|
137
|
+
* QQ 上先试 markdown(能出按钮),失败就退回纯文字 —— 按钮只是体验,剧情不能因为发不出去而中断。
|
|
138
|
+
* 纯文本会被「合并转发」收集,所以统一用 withoutForwardCollect 包起来。
|
|
139
|
+
*/
|
|
140
|
+
const sendInteractiveChoices = async (e, node, options = {}) => {
|
|
141
|
+
await recallLastChoices(e);
|
|
142
|
+
const useButtons = (0, QqPanel_1.isQqPlatform)(e) && node.choices.length > 0;
|
|
143
|
+
const send = async (content) => (0, node_karin_1.withoutForwardCollect)(() => e.reply(content));
|
|
144
|
+
let sent;
|
|
145
|
+
try {
|
|
146
|
+
sent = await send(useButtons
|
|
147
|
+
? node_karin_1.segment.markdown((0, exports.buildChoiceMessage)(node, { ...options, buttons: true }))
|
|
148
|
+
: (0, exports.buildChoiceMessage)(node, { ...options, buttons: false }));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!useButtons)
|
|
152
|
+
throw error;
|
|
153
|
+
node_karin_1.logger.warn('[互动视频] markdown 按钮消息发送失败(' + String(error?.message ?? error).slice(0, 120) + '),改用文字发送');
|
|
154
|
+
sent = await send((0, exports.buildChoiceMessage)(node, { ...options, buttons: false }));
|
|
155
|
+
}
|
|
156
|
+
const messageId = String(sent?.messageId ?? '');
|
|
157
|
+
if (messageId)
|
|
158
|
+
lastChoiceMessages.set(sessionKeyOf(e), messageId);
|
|
159
|
+
return messageId || undefined;
|
|
160
|
+
};
|
|
161
|
+
exports.sendInteractiveChoices = sendInteractiveChoices;
|
|
162
|
+
/** 发一条剧情提示(不参与合并转发、默认不撤回) */
|
|
163
|
+
const sendStoryTip = async (e, text) => {
|
|
164
|
+
try {
|
|
165
|
+
await (0, node_karin_1.withoutForwardCollect)(() => e.reply(text));
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
node_karin_1.logger.debug('[互动视频] 提示发送失败: ' + String(error?.message ?? error));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
exports.sendStoryTip = sendStoryTip;
|
|
172
|
+
/**
|
|
173
|
+
* 跑完一次互动剧情:把所有选项、等待、续播串起来。
|
|
174
|
+
*
|
|
175
|
+
* 这个函数是**长跑**的:它会一直等到用户选完或者超时,所以调用方应该放到后台跑
|
|
176
|
+
* (不要 await 在解析主流程里,否则解析结果要等到剧情结束才算完成)。
|
|
177
|
+
*
|
|
178
|
+
* @param options 见 {@link StoryOptions}
|
|
179
|
+
*/
|
|
180
|
+
const runInteractiveStory = async (options) => {
|
|
181
|
+
const { e, bvid, graphVersion, rootCid, title, notice, headers, request, play, onNode } = options;
|
|
182
|
+
const waitSeconds = options.waitSeconds ?? exports.CHOICE_WAIT_SECONDS;
|
|
183
|
+
const wait = options.wait ?? (async (seconds) => {
|
|
184
|
+
const context = await node_karin_1.default.ctx(e, { time: seconds * 1000, reply: true, throwOnTimeout: false });
|
|
185
|
+
return context ?? null;
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* 当前用来发消息的事件。
|
|
189
|
+
*
|
|
190
|
+
* 一开始是用户发链接那条消息;用户每回一次,就换成**那句回复** ——
|
|
191
|
+
* QQ 官方适配器只能在「用户刚说完话」的时间窗里被动回复,
|
|
192
|
+
* 一直拿最初那条链接消息去发,后面每一条都会撞 40034128(被动回复超限)然后退化成主动消息。
|
|
193
|
+
*/
|
|
194
|
+
let target = e;
|
|
195
|
+
/** 只有第一组选项带标题与说明 */
|
|
196
|
+
let firstChoices = true;
|
|
197
|
+
const key = sessionKeyOf(e);
|
|
198
|
+
const path = [];
|
|
199
|
+
let nodes = 0;
|
|
200
|
+
const finish = (ended) => {
|
|
201
|
+
(0, InteractiveVideo_1.clearInteractiveSession)(key);
|
|
202
|
+
return { ended, path, nodes };
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* 当前所在节点的视频 cid。
|
|
206
|
+
*
|
|
207
|
+
* 注意 edgeinfo_v2 的两个 cid 不是一回事:**请求**要带你现在这一段的 cid,
|
|
208
|
+
* 而落地那一段的 cid 是选项自己带的(choice.cid)—— 一开始这里搞混了,
|
|
209
|
+
* 结果每次「下一段」都重复播了当前这一段。
|
|
210
|
+
*/
|
|
211
|
+
let currentCid = rootCid;
|
|
212
|
+
/** 进入当前段的那条边(第一段没有):剧情图里选项 id 会重复,靠它才能定位用户在哪 */
|
|
213
|
+
let entry = null;
|
|
214
|
+
let failures = 0;
|
|
215
|
+
let node = await (0, InteractiveVideo_1.fetchInteractiveNode)({ bvid, graphVersion, cid: currentCid, headers, request });
|
|
216
|
+
if (!node) {
|
|
217
|
+
node_karin_1.logger.warn('[互动视频] 取不到剧情节点(graph_version=' + graphVersion + ' cid=' + rootCid + ')');
|
|
218
|
+
return finish('unavailable');
|
|
219
|
+
}
|
|
220
|
+
while (node) {
|
|
221
|
+
nodes++;
|
|
222
|
+
(0, InteractiveVideo_1.rememberInteractiveSession)(key, {
|
|
223
|
+
bvid,
|
|
224
|
+
graphVersion,
|
|
225
|
+
cid: node.cid,
|
|
226
|
+
title: node.title || title || '',
|
|
227
|
+
choices: node.choices,
|
|
228
|
+
path: [...path],
|
|
229
|
+
notice: node.notice,
|
|
230
|
+
entry: entry ?? undefined,
|
|
231
|
+
updatedAt: Date.now()
|
|
232
|
+
});
|
|
233
|
+
if (node.isLeaf || !node.choices.length) {
|
|
234
|
+
/**
|
|
235
|
+
* 结局同样给上层一次画图的机会:用户要求「走到最后一个选项也要在当前位置渲染」,
|
|
236
|
+
* 走到结尾时把完整路径画出来,比只丢一句「走到结局了」清楚得多。
|
|
237
|
+
*/
|
|
238
|
+
if (onNode) {
|
|
239
|
+
try {
|
|
240
|
+
await onNode({ ...node, isLeaf: true }, target, path, false, entry);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
node_karin_1.logger.warn('[互动视频] 结局剧情图渲染失败: ' + String(error?.message ?? error).slice(0, 160));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await recallLastChoices(target);
|
|
247
|
+
await (0, exports.sendStoryTip)(target, '🎬 互动视频走到结局了' + (path.length ? '\n剧情:' + (0, InteractiveVideo_1.formatInteractivePath)(path) : ''));
|
|
248
|
+
return finish('leaf');
|
|
249
|
+
}
|
|
250
|
+
/** B站 的提示语来自播放器接口,edgeinfo 里没有,所以在这里补进去 */
|
|
251
|
+
const currentNode = node.notice ? node : { ...node, notice: notice ?? '' };
|
|
252
|
+
/**
|
|
253
|
+
* 第一段额外渲染一张剧情图卡片(和作品信息卡同一套样式,页脚版本信息由 DefaultLayout 带)。
|
|
254
|
+
* 卡片发出去了,选项消息就只发选项本身;卡片没发出去,才用长文本兜底。
|
|
255
|
+
*/
|
|
256
|
+
let cardSent = false;
|
|
257
|
+
if (onNode) {
|
|
258
|
+
try {
|
|
259
|
+
cardSent = (await onNode(currentNode, target, path, firstChoices, entry)) === true;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
node_karin_1.logger.warn('[互动视频] 剧情图卡片渲染失败: ' + String(error?.message ?? error).slice(0, 160));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await (0, exports.sendInteractiveChoices)(target, currentNode, {
|
|
266
|
+
title: title || node.title,
|
|
267
|
+
path,
|
|
268
|
+
waitSeconds,
|
|
269
|
+
detailed: firstChoices && !cardSent
|
|
270
|
+
});
|
|
271
|
+
firstChoices = false;
|
|
272
|
+
/**
|
|
273
|
+
* 等用户选出这一段的走向。
|
|
274
|
+
*
|
|
275
|
+
* **认不出来就什么都不发,继续等**:群里别人正常聊天、或者用户自己随便说句话,
|
|
276
|
+
* 都不该被机器人插一句「没认出你的选择」—— 之前那样做,群一热闹就一直刷提示。
|
|
277
|
+
* 也不重发选项(选项就在上面那条消息里,重复发同样是在刷屏)。
|
|
278
|
+
*/
|
|
279
|
+
let choice = null;
|
|
280
|
+
while (!choice) {
|
|
281
|
+
const answer = await wait(waitSeconds);
|
|
282
|
+
/** 拿用户那句回复当新的发送目标(被动回复窗口就在它身上) */
|
|
283
|
+
const answerMessage = answer && typeof answer === 'object' ? answer : null;
|
|
284
|
+
const answerText = answerMessage ? String(answerMessage.msg ?? '') : String(answer ?? '');
|
|
285
|
+
if (answerMessage)
|
|
286
|
+
target = answerMessage;
|
|
287
|
+
if (!answerText.trim()) {
|
|
288
|
+
await recallLastChoices(target);
|
|
289
|
+
await (0, exports.sendStoryTip)(target, '互动视频先玩到这里~想接着玩就再发一次链接(等了 ' + waitSeconds + ' 秒没等到你的选择)');
|
|
290
|
+
return finish('timeout');
|
|
291
|
+
}
|
|
292
|
+
const index = (0, InteractiveVideo_1.parseChoiceInput)(answerText, node.choices);
|
|
293
|
+
if (index < 0)
|
|
294
|
+
continue;
|
|
295
|
+
choice = node.choices[index];
|
|
296
|
+
}
|
|
297
|
+
path.push(choice.text);
|
|
298
|
+
/** 用「当前节点 cid + 这条边」问下一组选项,返回的就是落地节点 */
|
|
299
|
+
const next = await (0, InteractiveVideo_1.fetchInteractiveNode)({ bvid, graphVersion, cid: currentCid, edgeId: choice.edgeId, headers, request });
|
|
300
|
+
if (!next) {
|
|
301
|
+
/**
|
|
302
|
+
* 取不到下一段时**先别结束**:实测多数是接口抖了一下。
|
|
303
|
+
* 把选项再发一次,等用户重新选(内部已经重试过一次请求)。
|
|
304
|
+
*/
|
|
305
|
+
failures++;
|
|
306
|
+
if (failures > 1) {
|
|
307
|
+
await (0, exports.sendStoryTip)(target, '剧情图一直取不到下一段(可能剧情被 UP 主改过了),这次就到这里');
|
|
308
|
+
return finish('unavailable');
|
|
309
|
+
}
|
|
310
|
+
await (0, exports.sendStoryTip)(target, '刚刚没取到下一段,再选一次试试~');
|
|
311
|
+
path.pop();
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
/** 落地那一段的视频就是选项带的 cid */
|
|
315
|
+
entry = { fromCid: currentCid, edgeId: choice.edgeId };
|
|
316
|
+
currentCid = choice.cid;
|
|
317
|
+
if (play) {
|
|
318
|
+
try {
|
|
319
|
+
await play(currentCid, target);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
node_karin_1.logger.warn('[互动视频] 播放下一段失败(cid=' + currentCid + '): ' + String(error?.message ?? error));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
node = { ...next, cid: currentCid };
|
|
326
|
+
}
|
|
327
|
+
return finish('leaf');
|
|
328
|
+
};
|
|
329
|
+
exports.runInteractiveStory = runInteractiveStory;
|
|
330
|
+
/**
|
|
331
|
+
* 在一张图里按「从哪条边进来」定位节点。
|
|
332
|
+
*
|
|
333
|
+
* 剧情图里选项 id / 落点会重复,所以优先用 fromCid:edgeId 这一对,
|
|
334
|
+
* 实在对不上再按 cid 兜(有重复落点时会挑第一个)。
|
|
335
|
+
*/
|
|
336
|
+
const findNode = (root, entry, cid) => {
|
|
337
|
+
let byCid = null;
|
|
338
|
+
const walk = (node, edgeId) => {
|
|
339
|
+
if (!node)
|
|
340
|
+
return null;
|
|
341
|
+
if (edgeId !== undefined && entry && Number(edgeId) === Number(entry.edgeId) && Number(node.cid) === Number(cid))
|
|
342
|
+
return node;
|
|
343
|
+
if (!byCid && cid !== undefined && Number(node.cid) === Number(cid))
|
|
344
|
+
byCid = node;
|
|
345
|
+
for (const choice of node.choices ?? []) {
|
|
346
|
+
for (const child of choice.children ?? []) {
|
|
347
|
+
const hit = walk(child, choice.edgeId);
|
|
348
|
+
if (hit)
|
|
349
|
+
return hit;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
const found = entry ? walk(root, undefined) : null;
|
|
355
|
+
return found ?? byCid ?? null;
|
|
356
|
+
};
|
|
357
|
+
exports.findNode = findNode;
|
|
358
|
+
/** 卡片需要的嵌套结构(带递归上限,防止剧情图里的环把渲染卡死) */
|
|
359
|
+
const toCardGraph = (node, depth = 0) => {
|
|
360
|
+
const choices = Array.isArray(node?.choices) ? node.choices : [];
|
|
361
|
+
return {
|
|
362
|
+
question: String(node?.question ?? ''),
|
|
363
|
+
isLeaf: Boolean(node?.isLeaf),
|
|
364
|
+
truncated: Boolean(node?.truncated),
|
|
365
|
+
choices: choices.map((choice) => ({
|
|
366
|
+
label: choice.label,
|
|
367
|
+
text: choice.text,
|
|
368
|
+
isDefault: choice.isDefault,
|
|
369
|
+
children: depth >= 6 || !Array.isArray(choice.children) || !choice.children.length
|
|
370
|
+
? undefined
|
|
371
|
+
: choice.children.map((child) => (0, exports.toCardGraph)(child, depth + 1))
|
|
372
|
+
}))
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
exports.toCardGraph = toCardGraph;
|
|
376
|
+
/** 清理某个频道的选项消息记录(解析失败等异常收尾时用) */
|
|
377
|
+
const forgetChoiceMessage = (e) => {
|
|
378
|
+
lastChoiceMessages.delete(sessionKeyOf(e));
|
|
379
|
+
};
|
|
380
|
+
exports.forgetChoiceMessage = forgetChoiceMessage;
|
|
381
|
+
/**
|
|
382
|
+
* 渲染并发送一张「剧情图」卡片。
|
|
383
|
+
*
|
|
384
|
+
* 三处会用到:第一段、走到图外分支时、以及用户点「渲染流程图」按钮时。
|
|
385
|
+
* crawlInteractiveGraph 的 rootEdge 决定「从哪一段开始画」——剧情图里选项 id 会重复,
|
|
386
|
+
* 只给 cid 不够,必须带上进入那条边。
|
|
387
|
+
*
|
|
388
|
+
* @returns 卡片是否真的发出去了
|
|
389
|
+
*/
|
|
390
|
+
const renderInteractiveChart = async (params) => {
|
|
391
|
+
const { e, bvid, graphVersion, cid, entry, title, path = [], notice, question, isEnding } = params;
|
|
392
|
+
/**
|
|
393
|
+
* 优先用缓存里的整张图:第一次解析就整张爬完了,
|
|
394
|
+
* 之后(结局、点渲染流程图)都是换个起点画同一份缓存,不再请求接口。
|
|
395
|
+
*/
|
|
396
|
+
let whole = (0, InteractiveGraph_1.peekCachedGraph)(bvid, graphVersion);
|
|
397
|
+
if (!whole) {
|
|
398
|
+
whole = await (0, InteractiveGraph_1.crawlInteractiveGraph)({
|
|
399
|
+
bvid,
|
|
400
|
+
graphVersion,
|
|
401
|
+
rootCid: cid,
|
|
402
|
+
rootEdge: entry,
|
|
403
|
+
headers: params.headers,
|
|
404
|
+
request: params.request ?? InteractiveVideo_1.interactiveRequest
|
|
405
|
+
}).catch(() => null);
|
|
406
|
+
}
|
|
407
|
+
if (!whole)
|
|
408
|
+
return { sent: false, complete: false };
|
|
409
|
+
/** 在整张图里定位「用户现在在哪一段」:先按进来的那条边找,找不到再按 cid 兜 */
|
|
410
|
+
const located = (0, exports.findNode)(whole.root, entry, cid) ?? whole.root;
|
|
411
|
+
const graph = { root: located, nodeCount: whole.nodeCount, truncated: whole.truncated, cids: whole.cids };
|
|
412
|
+
const img = await (0, module_1.Render)(e, 'bilibili/interactive', {
|
|
413
|
+
title: title || '互动视频',
|
|
414
|
+
step: path.length + 1,
|
|
415
|
+
rootLabel: params.full
|
|
416
|
+
? (question || '开场')
|
|
417
|
+
: (isEnding ? ('结局 · ' + (question || '完')) : ('当前 · ' + (question || '这一段'))),
|
|
418
|
+
path,
|
|
419
|
+
notice,
|
|
420
|
+
graph: (0, exports.toCardGraph)(graph.root),
|
|
421
|
+
truncated: graph.truncated
|
|
422
|
+
});
|
|
423
|
+
if (!img?.length)
|
|
424
|
+
return { sent: false, complete: !graph.truncated };
|
|
425
|
+
/**
|
|
426
|
+
* 剧情图**不是过程提示**,所以不能再用 withoutForwardCollect 把它挡在合并转发之外:
|
|
427
|
+
* 用 withForwardKind('chart', …) 标成「流程图」这一类,剩下的交给「合并转发内容」:
|
|
428
|
+
* - 勾了「流程图」→ 它进聊天记录;
|
|
429
|
+
* - 没勾 → 解析结束时单独直发(不会丢);
|
|
430
|
+
* - 根本就没开合并转发(或者剧情是解析完才在后台画的,袋子已经关掉了)→ 就是普通发送。
|
|
431
|
+
*/
|
|
432
|
+
await (0, node_karin_1.withForwardKind)('chart', () => e.reply(img));
|
|
433
|
+
return { sent: true, complete: !graph.truncated };
|
|
434
|
+
};
|
|
435
|
+
exports.renderInteractiveChart = renderInteractiveChart;
|
|
436
|
+
/**
|
|
437
|
+
* 「渲染流程图」按钮/指令:重新画一张**当前位置**的图。
|
|
438
|
+
*
|
|
439
|
+
* 关键:**不撤回选项面板** —— 用户还在选,撤了就没得点了。
|
|
440
|
+
*/
|
|
441
|
+
const handleRenderChart = (0, ErrorHandler_1.wrapWithErrorHandler)(async (e) => {
|
|
442
|
+
const session = (0, InteractiveVideo_1.getInteractiveSession)(sessionKeyOf(e));
|
|
443
|
+
if (!session) {
|
|
444
|
+
await (0, exports.sendStoryTip)(e, '现在没有正在进行的互动视频,发一条互动视频链接就能玩');
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
const sent = await (0, exports.renderInteractiveChart)({
|
|
448
|
+
e,
|
|
449
|
+
bvid: session.bvid,
|
|
450
|
+
graphVersion: session.graphVersion,
|
|
451
|
+
cid: session.cid,
|
|
452
|
+
entry: session.entry,
|
|
453
|
+
title: session.title,
|
|
454
|
+
path: session.path,
|
|
455
|
+
notice: session.notice
|
|
456
|
+
});
|
|
457
|
+
if (!sent.sent)
|
|
458
|
+
await (0, exports.sendStoryTip)(e, '流程图这次没画出来(接口没返回剧情数据),过一会儿再点一次试试');
|
|
459
|
+
return true;
|
|
460
|
+
}, { businessName: '互动视频流程图' });
|
|
461
|
+
exports.interactiveChart = node_karin_1.default.command(new RegExp('^#?' + exports.RENDER_CHART_COMMAND + '$'), handleRenderChart, {
|
|
462
|
+
name: 'kkk-渲染流程图'
|
|
463
|
+
});
|
|
@@ -757,14 +757,21 @@ class Bilibilipush extends module_1.Base {
|
|
|
757
757
|
const infoData = await this.amagi.bilibili.fetcher.fetchVideoInfo({
|
|
758
758
|
bvid: data[dynamicId].Dynamic_Data.modules.module_dynamic.major.archive.bvid
|
|
759
759
|
});
|
|
760
|
-
|
|
760
|
+
/** 直链候选:base_url(可能是 mcdn/PCDN 节点)+ backup_url 镜像,坏源由下载器自动换 */
|
|
761
|
+
const videoUrls = (0, bilibili_1.buildDashUrlCandidates)(playUrlData.data?.data?.dash?.video[0]);
|
|
762
|
+
const audioUrls = (0, bilibili_1.buildDashUrlCandidates)(playUrlData.data?.data?.dash?.audio?.[0]);
|
|
763
|
+
if (videoUrls.length === 0)
|
|
764
|
+
break;
|
|
765
|
+
const mp4File = await (0, module_1.downloadFile)(videoUrls[0], {
|
|
761
766
|
title: `Bil_V_${infoData.data.data.bvid}.mp4`,
|
|
762
|
-
headers: bilibiliBaseHeaders
|
|
767
|
+
headers: bilibiliBaseHeaders,
|
|
768
|
+
backupUrls: videoUrls.slice(1)
|
|
763
769
|
});
|
|
764
|
-
const mp3File =
|
|
765
|
-
? await (0, module_1.downloadFile)(
|
|
770
|
+
const mp3File = audioUrls.length > 0
|
|
771
|
+
? await (0, module_1.downloadFile)(audioUrls[0], {
|
|
766
772
|
title: `Bil_A_${infoData.data.data.bvid}.mp3`,
|
|
767
|
-
headers: bilibiliBaseHeaders
|
|
773
|
+
headers: bilibiliBaseHeaders,
|
|
774
|
+
backupUrls: audioUrls.slice(1)
|
|
768
775
|
})
|
|
769
776
|
: undefined;
|
|
770
777
|
if (mp4File.filepath && (!mp3File || mp3File.filepath)) {
|
package/lib/ktr/registry.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.templateRegistry = {
|
|
|
44
44
|
'bilibili/dynamic/DYNAMIC_TYPE_FORWARD': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/dynamic/DYNAMIC_TYPE_FORWARD'))),
|
|
45
45
|
'bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/dynamic/DYNAMIC_TYPE_LIVE_RCMD'))),
|
|
46
46
|
'bilibili/dynamic/DYNAMIC_TYPE_WORD': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/dynamic/DYNAMIC_TYPE_WORD'))),
|
|
47
|
+
'bilibili/interactive': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/interactive'))),
|
|
47
48
|
'bilibili/qrcodeImg': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/qrcodeImg'))),
|
|
48
49
|
'bilibili/userlist': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/userlist'))),
|
|
49
50
|
'bilibili/videoInfo': () => Promise.resolve().then(() => __importStar(require('./template/bilibili/videoInfo'))),
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BilibiliInteractive = void 0;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
/**
|
|
9
|
+
* 互动视频的「剧情图」思维导图卡片。
|
|
10
|
+
*
|
|
11
|
+
* 用户要的是**全部剧情流程**一张图看完(像 markmap 那种),不是只列眼前这几个选项。
|
|
12
|
+
* 所以这里把爬下来的整张图铺成思维导图:根节点在最左,右侧逐层展开分支,用贝塞尔曲线连起来,
|
|
13
|
+
* 顶层分支各自一种颜色;已经走过的选项加粗高亮。
|
|
14
|
+
*
|
|
15
|
+
* 样式全部写行内 / SVG 属性:卡片用的是预编译的 tailwind 产物,新模板的任意值类不存在。
|
|
16
|
+
*/
|
|
17
|
+
const react_1 = __importDefault(require("react"));
|
|
18
|
+
const DefaultLayout_1 = require("../../../components/DefaultLayout");
|
|
19
|
+
const BILI_BLUE = '#00aeec';
|
|
20
|
+
/** 顶层分支配色,和思维导图常见做法一致:一条主枝一个颜色 */
|
|
21
|
+
const BRANCH_COLORS = ['#ff7a45', '#3fbf6f', '#22a6b3', '#a259ff', '#f0b429', '#5a8dee', '#e8608d', '#7f8c8d'];
|
|
22
|
+
const COL_WIDTH = 260;
|
|
23
|
+
const ROW_HEIGHT = 62;
|
|
24
|
+
const PAD_LEFT = 8;
|
|
25
|
+
const PAD_TOP = 20;
|
|
26
|
+
/** 把剧情图摊平成「一行一项」的列表,并算好每个叶子占用的行号 */
|
|
27
|
+
const flatten = (root, path, rootLabel) => {
|
|
28
|
+
const items = [];
|
|
29
|
+
let cursor = 0;
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const walk = (node, depth, parent, color, id) => {
|
|
32
|
+
const label = node.question || (node.isLeaf ? '结局' : '继续剧情');
|
|
33
|
+
const self = { id, label, depth, row: 0, color, parent, kind: parent ? 'node' : 'root' };
|
|
34
|
+
items.push(self);
|
|
35
|
+
const choices = Array.isArray(node.choices) ? node.choices : [];
|
|
36
|
+
if (!choices.length) {
|
|
37
|
+
self.row = cursor++;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let firstRow = -1;
|
|
41
|
+
choices.forEach((choice, index) => {
|
|
42
|
+
const childId = id + '.' + index;
|
|
43
|
+
const choiceItem = {
|
|
44
|
+
id: childId,
|
|
45
|
+
label: choice.label + ' ' + choice.text,
|
|
46
|
+
depth: depth + 1,
|
|
47
|
+
row: 0,
|
|
48
|
+
color,
|
|
49
|
+
parent: id,
|
|
50
|
+
kind: 'choice',
|
|
51
|
+
isDefault: choice.isDefault,
|
|
52
|
+
walked: path.includes(choice.text)
|
|
53
|
+
};
|
|
54
|
+
items.push(choiceItem);
|
|
55
|
+
const children = Array.isArray(choice.children) ? choice.children : [];
|
|
56
|
+
if (!children.length) {
|
|
57
|
+
choiceItem.row = cursor++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
children.forEach((child, childIndex) => walk(child, depth + 2, childId, color, childId + '.' + childIndex));
|
|
61
|
+
choiceItem.row = choiceItem.row || cursor - 1;
|
|
62
|
+
}
|
|
63
|
+
if (firstRow < 0)
|
|
64
|
+
firstRow = choiceItem.row;
|
|
65
|
+
});
|
|
66
|
+
/** 父节点对齐到自己第一个分支那一行,和思维导图的观感一致 */
|
|
67
|
+
self.row = firstRow >= 0 ? firstRow : cursor++;
|
|
68
|
+
};
|
|
69
|
+
const topChoices = Array.isArray(root.choices) ? root.choices : [];
|
|
70
|
+
const rootItem = { id: 'root', label: rootLabel || root.question || '开场', depth: 0, row: 0, color: BILI_BLUE, kind: 'root' };
|
|
71
|
+
items.push(rootItem);
|
|
72
|
+
if (!topChoices.length) {
|
|
73
|
+
rootItem.row = cursor++;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
topChoices.forEach((choice, index) => {
|
|
77
|
+
const color = BRANCH_COLORS[index % BRANCH_COLORS.length];
|
|
78
|
+
const childId = 'root.' + index;
|
|
79
|
+
const choiceItem = {
|
|
80
|
+
id: childId,
|
|
81
|
+
label: choice.label + ' ' + choice.text,
|
|
82
|
+
depth: 1,
|
|
83
|
+
row: 0,
|
|
84
|
+
color,
|
|
85
|
+
parent: 'root',
|
|
86
|
+
kind: 'choice',
|
|
87
|
+
isDefault: choice.isDefault,
|
|
88
|
+
walked: path.includes(choice.text)
|
|
89
|
+
};
|
|
90
|
+
items.push(choiceItem);
|
|
91
|
+
const children = Array.isArray(choice.children) ? choice.children : [];
|
|
92
|
+
if (!children.length) {
|
|
93
|
+
choiceItem.row = cursor++;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
children.forEach((child, childIndex) => walk(child, 3, childId, color, childId + '.' + childIndex));
|
|
97
|
+
choiceItem.row = choiceItem.row || cursor - 1;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
rootItem.row = items[1]?.row ?? 0;
|
|
101
|
+
}
|
|
102
|
+
seen.clear();
|
|
103
|
+
return { items, rows: Math.max(cursor, 1) };
|
|
104
|
+
};
|
|
105
|
+
exports.BilibiliInteractive = react_1.default.memo((props) => {
|
|
106
|
+
const { data } = props;
|
|
107
|
+
const path = Array.isArray(data.path) ? data.path.filter(Boolean) : [];
|
|
108
|
+
const graph = data.graph;
|
|
109
|
+
const flat = graph ? flatten(graph, path, data.rootLabel) : null;
|
|
110
|
+
const depth = flat ? flat.items.reduce((max, item) => Math.max(max, item.depth), 0) : 0;
|
|
111
|
+
const width = PAD_LEFT * 2 + (depth + 1) * COL_WIDTH;
|
|
112
|
+
const height = PAD_TOP * 2 + (flat ? flat.rows : 1) * ROW_HEIGHT;
|
|
113
|
+
const byId = new Map();
|
|
114
|
+
if (flat)
|
|
115
|
+
for (const item of flat.items)
|
|
116
|
+
byId.set(item.id, item);
|
|
117
|
+
const rowY = (row) => PAD_TOP + row * ROW_HEIGHT + ROW_HEIGHT / 2;
|
|
118
|
+
const colX = (item) => PAD_LEFT + item.depth * COL_WIDTH;
|
|
119
|
+
return ((0, jsx_runtime_1.jsx)(DefaultLayout_1.DefaultLayout, { ...props, children: (0, jsx_runtime_1.jsxs)("div", { style: { position: 'relative', zIndex: 10, padding: '64px 56px 48px' }, children: [(0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: 24 }, children: [(0, jsx_runtime_1.jsx)("span", { style: { borderRadius: 999, padding: '12px 28px', fontSize: 26, fontWeight: 900, letterSpacing: '0.18em', background: 'rgba(0,174,236,0.14)', color: BILI_BLUE }, children: "\u4E92\u52A8\u89C6\u9891" }), (0, jsx_runtime_1.jsxs)("span", { style: { fontSize: 26, fontWeight: 700, opacity: 0.42 }, children: ["\u7B2C ", data.step, " \u6BB5 \u00B7 \u5168\u90E8\u5267\u60C5\u6D41\u7A0B"] })] }), (0, jsx_runtime_1.jsx)("h1", { style: { margin: '28px 0 0', fontSize: 48, fontWeight: 900, lineHeight: 1.3, wordBreak: 'break-word' }, children: data.title }), path.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { style: { marginTop: 18, fontSize: 27, fontWeight: 600, opacity: 0.5, wordBreak: 'break-word' }, children: ["\u5DF2\u9009\uFF1A", path.join(' → ')] })), flat && ((0, jsx_runtime_1.jsxs)("svg", { viewBox: '0 0 ' + width + ' ' + height, width: '100%', style: { marginTop: 26, display: 'block', overflow: 'visible' }, role: 'img', children: [flat.items.map((item) => {
|
|
120
|
+
if (!item.parent)
|
|
121
|
+
return null;
|
|
122
|
+
const parent = byId.get(item.parent);
|
|
123
|
+
if (!parent)
|
|
124
|
+
return null;
|
|
125
|
+
const x1 = colX(parent) + 8;
|
|
126
|
+
const x2 = colX(item) - 6;
|
|
127
|
+
const mid = (x1 + x2) / 2;
|
|
128
|
+
const d = 'M ' + x1 + ' ' + rowY(parent.row) + ' C ' + mid + ' ' + rowY(parent.row) + ', ' + mid + ' ' + rowY(item.row) + ', ' + x2 + ' ' + rowY(item.row);
|
|
129
|
+
return (0, jsx_runtime_1.jsx)("path", { d: d, fill: 'none', stroke: item.color, strokeWidth: item.walked ? 5 : 3, strokeLinecap: 'round', opacity: item.walked ? 1 : 0.75 }, 'edge-' + item.id);
|
|
130
|
+
}), flat.items.map((item) => {
|
|
131
|
+
const x = colX(item);
|
|
132
|
+
const y = rowY(item.row);
|
|
133
|
+
const isChoice = item.kind === 'choice';
|
|
134
|
+
return ((0, jsx_runtime_1.jsxs)("g", { children: [(0, jsx_runtime_1.jsx)("circle", { cx: x, cy: y, r: isChoice ? 9 : 13, fill: item.walked || item.kind === 'root' ? item.color : '#ffffff', stroke: item.color, strokeWidth: 4 }), (0, jsx_runtime_1.jsxs)("text", { x: x + (isChoice ? 22 : 26), y: y + 9, fontSize: isChoice ? 27 : 29, fontWeight: item.walked || item.kind === 'root' ? 900 : 600, fill: item.walked ? item.color : 'currentColor', opacity: item.walked || item.kind === 'root' ? 1 : 0.82, children: [item.label.length > 14 ? item.label.slice(0, 14) + '…' : item.label, item.isDefault ? '(默认)' : ''] })] }, 'node-' + item.id));
|
|
135
|
+
})] })), !flat && (data.choices ?? []).map((choice, index) => ((0, jsx_runtime_1.jsxs)("div", { style: { marginTop: index === 0 ? 30 : 18, fontSize: 36, fontWeight: 800 }, children: [choice.label, " ", choice.text, choice.isDefault ? '(默认)' : ''] }, choice.label + index))), (data.truncated || graph?.truncated) && ((0, jsx_runtime_1.jsx)("div", { style: { marginTop: 26, fontSize: 25, fontWeight: 600, opacity: 0.55 }, children: "\u5267\u60C5\u592A\u957F\uFF0C\u56FE\u4E0A\u53EA\u753B\u4E86\u524D\u534A\u90E8\u5206\u5206\u652F" })), (0, jsx_runtime_1.jsxs)("div", { style: { marginTop: 22, display: 'flex', alignItems: 'center', gap: 14, fontSize: 25, fontWeight: 600, opacity: 0.52 }, children: [(0, jsx_runtime_1.jsx)("span", { style: { display: 'inline-block', width: 26, height: 5, borderRadius: 999, background: BILI_BLUE } }), "\u9AD8\u4EAE\u7684\u662F\u4F60\u5DF2\u7ECF\u8D70\u8FC7\u7684\u8DEF"] }), data.notice && ((0, jsx_runtime_1.jsx)("div", { style: { marginTop: 18, borderRadius: 20, padding: '16px 28px', fontSize: 25, fontWeight: 600, opacity: 0.62, background: 'rgba(127,127,127,0.12)' }, children: data.notice }))] }) }));
|
|
136
|
+
});
|
|
137
|
+
exports.BilibiliInteractive.displayName = 'BilibiliInteractive';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const template_react_1 = require("../../../../compat/template-react.js");
|
|
4
|
+
const interactive_1 = require("./components/interactive");
|
|
5
|
+
exports.default = (0, template_react_1.defineTemplate)({
|
|
6
|
+
name: '互动视频剧情图',
|
|
7
|
+
description: 'B站互动视频的剧情分支与选项',
|
|
8
|
+
component: interactive_1.BilibiliInteractive,
|
|
9
|
+
validate: (data) => typeof data === 'object' &&
|
|
10
|
+
data !== null &&
|
|
11
|
+
typeof data.title === 'string' &&
|
|
12
|
+
(data.graph !== undefined || Array.isArray(data.choices))
|
|
13
|
+
});
|