koishi-plugin-kkk 3.7.0 → 3.7.1
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 +15 -0
- package/lib/build-metadata.json +3 -3
- package/lib/karin/module/utils/Base.js +21 -4
- package/lib/karin/platform/bilibili/bilibili.js +15 -41
- package/lib/karin/platform/bilibili/interactive-story.js +46 -1
- package/package.json +1 -1
- package/scripts/smoke-interactive-story.cjs +27 -0
- package/src/karin/module/utils/Base.ts +26 -4
- package/src/karin/platform/bilibili/bilibili.ts +20 -39
- package/src/karin/platform/bilibili/interactive-story.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 3.7.1
|
|
4
|
+
|
|
5
|
+
> 修一个 3.7.0 改过头的地方:**视频直接发到群里的那种互动视频,照旧要在群里发带按钮的选择面板**;
|
|
6
|
+
> 而且点完选项之后,**上一段视频会撤回**,群里只留当前这一段。
|
|
7
|
+
|
|
8
|
+
### 互动视频:两种玩法各归各位
|
|
9
|
+
|
|
10
|
+
- **选了在线播放**(或者视频太大、自动转成在线播放):选项在网页播放器里贴出来,群里不再问一遍。
|
|
11
|
+
- **视频直接发到群里**(没有播放页):照旧在群里发**带按钮的选择面板** ——
|
|
12
|
+
QQ 官方机器人是按钮,其它平台是一行一个选项的文字,点一下接着播下一段。
|
|
13
|
+
- **点完选项会撤回上一段视频**:不然玩几段之后,群里会躺着一串视频。
|
|
14
|
+
顺序是先发新的、再撤旧的,中间不会出现「一段视频都没有」的空档;
|
|
15
|
+
选择面板照旧是发新的时候撤掉旧的。
|
|
16
|
+
- 剧情图(勾了「流程图」时)照常:第一段出一张,走到结局再出一张。
|
|
17
|
+
|
|
3
18
|
## 3.7.0
|
|
4
19
|
|
|
5
20
|
> 互动视频这一版基本重做了:**选剧情搬到了网页播放器里**(选项就贴在画面上、点完能看到下载进度),
|
package/lib/build-metadata.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.7.
|
|
3
|
-
"buildTime": "2026-09-25T18:
|
|
4
|
-
"buildTimestamp":
|
|
2
|
+
"version": "3.7.1",
|
|
3
|
+
"buildTime": "2026-09-25T18:27:55.201Z",
|
|
4
|
+
"buildTimestamp": 1790360875202,
|
|
5
5
|
"name": "koishi-plugin-kkk",
|
|
6
6
|
"description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
|
|
7
7
|
"homepage": "https://github.com/maimai993/koishi-plugin-kkk#readme",
|
|
@@ -51,6 +51,19 @@ const runtime_1 = require("../../../compat/runtime");
|
|
|
51
51
|
const player_1 = require("../../../player");
|
|
52
52
|
// 解析阶段(「下载进度」指令读的就是这里登记的状态)
|
|
53
53
|
const Downloader_1 = require("./Network/Downloader");
|
|
54
|
+
/** 发送结果里把消息 ID 抠出来(不同适配器返回的形状不一样:对象带 messageId、或者就是字符串数组) */
|
|
55
|
+
const reportSent = (options, sent) => {
|
|
56
|
+
if (!options?.onSent)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const id = typeof sent === 'string'
|
|
60
|
+
? sent
|
|
61
|
+
: (sent?.messageId ?? (Array.isArray(sent) ? sent[0] : ''));
|
|
62
|
+
if (id)
|
|
63
|
+
options.onSent(String(id));
|
|
64
|
+
}
|
|
65
|
+
catch { /* 拿不到 ID 不影响发送本身 */ }
|
|
66
|
+
};
|
|
54
67
|
/**
|
|
55
68
|
* 基础基类
|
|
56
69
|
* @remarks 提供事件上下文与通用请求头,继承自 `AmagiBase` 以复用 Amagi 客户端。
|
|
@@ -259,13 +272,15 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
259
272
|
// 是群文件
|
|
260
273
|
const bot = node_karin_1.default.getBot(String(options.activeOption?.uin));
|
|
261
274
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('主动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('bot.uploadFile')}回复...`);
|
|
262
|
-
await bot.uploadFile(contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`);
|
|
275
|
+
const sent = await bot.uploadFile(contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`);
|
|
276
|
+
reportSent(options, sent);
|
|
263
277
|
}
|
|
264
278
|
else {
|
|
265
279
|
// 不是群文件
|
|
266
280
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('主动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('karin.sendMsg')}回复...`);
|
|
267
281
|
// 兼容层已经把「没拿到消息 ID」当成失败抛出(见 compat/sendError),所以这里没有异常就是发出去了
|
|
268
|
-
await node_karin_1.default.sendMsg(selfId, contact, [node_karin_1.segment.video(File)], { retryCount: 0 });
|
|
282
|
+
const sent = await node_karin_1.default.sendMsg(selfId, contact, [node_karin_1.segment.video(File)], { retryCount: 0 });
|
|
283
|
+
reportSent(options, sent);
|
|
269
284
|
sendStatus = true;
|
|
270
285
|
}
|
|
271
286
|
}
|
|
@@ -274,13 +289,15 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
274
289
|
if (useGroupFile) {
|
|
275
290
|
// 是文件
|
|
276
291
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('被动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('e.bot.uploadFile')}回复...`);
|
|
277
|
-
await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`);
|
|
292
|
+
const sent = await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`);
|
|
293
|
+
reportSent(options, sent);
|
|
278
294
|
}
|
|
279
295
|
else {
|
|
280
296
|
// 不是文件
|
|
281
297
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('被动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('e.reply')}回复...`);
|
|
282
298
|
// 同上:异常才是失败(没拿到消息 ID 已经由兼容层抛出来了)
|
|
283
|
-
await event.reply(node_karin_1.segment.video(File) || videoUrl);
|
|
299
|
+
const sent = await event.reply(node_karin_1.segment.video(File) || videoUrl);
|
|
300
|
+
reportSent(options, sent);
|
|
284
301
|
sendStatus = true;
|
|
285
302
|
}
|
|
286
303
|
}
|
|
@@ -588,15 +588,14 @@ class Bilibili extends utils_1.Base {
|
|
|
588
588
|
await downloadTask;
|
|
589
589
|
await this.sendPreparedVideo();
|
|
590
590
|
/**
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* 用户至少能看到这张图里有几条分支。
|
|
591
|
+
* 互动视频的剧情怎么走,看这次的视频发到哪儿:
|
|
592
|
+
* · **走在线播放**(视频没发进群里,用户在看播放页)→ 选项在播放页上贴出来,
|
|
593
|
+
* 群里不再问一遍,免得两处各问一次(见 sendPreparedVideo 里的 story);
|
|
594
|
+
* · **直接发到群里**(没有播放页)→ 照旧在群里发**带按钮的选择面板**,
|
|
595
|
+
* 用户点一下播下一段,并撤回上一段视频(用户要求)。
|
|
597
596
|
*/
|
|
598
597
|
if (this.interactive && !this.publishedStory)
|
|
599
|
-
void this.
|
|
598
|
+
void this.startInteractiveStory();
|
|
600
599
|
});
|
|
601
600
|
}
|
|
602
601
|
}
|
|
@@ -1720,38 +1719,6 @@ class Bilibili extends utils_1.Base {
|
|
|
1720
1719
|
}
|
|
1721
1720
|
return this.preparedVideo !== null;
|
|
1722
1721
|
}
|
|
1723
|
-
/**
|
|
1724
|
-
* 互动视频发到群里时:只补一张**剧情图**,不问选项(用户要求:群里不要选择按钮)。
|
|
1725
|
-
*
|
|
1726
|
-
* 选项流程在播放页里(见 sendPreparedVideo 的 story);这里只把「有几条分支」画出来。
|
|
1727
|
-
* 失败只记日志 —— 出不了图不影响视频已经发出去这件事。
|
|
1728
|
-
*/
|
|
1729
|
-
async sendInteractiveChartOnly() {
|
|
1730
|
-
const interactive = this.interactive;
|
|
1731
|
-
if (!interactive)
|
|
1732
|
-
return;
|
|
1733
|
-
/** 没勾「流程图」就什么都不做(配置说了算) */
|
|
1734
|
-
if (!Config_1.Config.bilibili.sendContent.some((item) => item === 'chart'))
|
|
1735
|
-
return;
|
|
1736
|
-
try {
|
|
1737
|
-
const info = await (0, InteractiveVideo_1.fetchInteractiveInfo)({ bvid: interactive.bvid, cid: interactive.cid, headers: this.headers });
|
|
1738
|
-
if (!info)
|
|
1739
|
-
return;
|
|
1740
|
-
await (0, interactive_story_1.renderInteractiveChart)({
|
|
1741
|
-
e: this.e,
|
|
1742
|
-
bvid: interactive.bvid,
|
|
1743
|
-
graphVersion: info.graphVersion,
|
|
1744
|
-
cid: interactive.cid,
|
|
1745
|
-
title: this.workInfo?.title ?? '互动视频',
|
|
1746
|
-
notice: info.notice,
|
|
1747
|
-
headers: this.headers,
|
|
1748
|
-
full: true
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
node_karin_1.logger.debug('[互动视频] 群里补剧情图失败(不影响视频): ' + String(error?.message ?? error));
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
1722
|
/**
|
|
1756
1723
|
* 起一次互动剧情(后台长跑,不阻塞解析主流程)。
|
|
1757
1724
|
*
|
|
@@ -1923,11 +1890,18 @@ class Bilibili extends utils_1.Base {
|
|
|
1923
1890
|
}
|
|
1924
1891
|
node_karin_1.logger.warn('[在线播放] 播放会话登记失败,退回直接发送视频文件');
|
|
1925
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* 互动视频(发到群里的那种):把这条视频的消息 ID 记下来 ——
|
|
1895
|
+
* 用户点了下一个选项之后,这条要撤回,群里只留当前这一段。
|
|
1896
|
+
*/
|
|
1897
|
+
const sentOptions = this.interactive
|
|
1898
|
+
? { onSent: (messageId) => (0, interactive_story_1.rememberStoryVideo)(this.e, messageId) }
|
|
1899
|
+
: undefined;
|
|
1926
1900
|
if (totalBytes > Config_1.Config.app.groupfilevalue) {
|
|
1927
|
-
await (0, utils_1.uploadFile)(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', { useGroupFile: true });
|
|
1901
|
+
await (0, utils_1.uploadFile)(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', { useGroupFile: true, ...sentOptions });
|
|
1928
1902
|
}
|
|
1929
1903
|
else {
|
|
1930
|
-
await (0, utils_1.uploadFile)(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '');
|
|
1904
|
+
await (0, utils_1.uploadFile)(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', sentOptions);
|
|
1931
1905
|
}
|
|
1932
1906
|
return true;
|
|
1933
1907
|
}
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
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;
|
|
36
|
+
exports.interactiveChart = exports.renderInteractiveChart = exports.forgetChoiceMessage = exports.toCardGraph = exports.findNode = exports.runInteractiveStory = exports.sendStoryTip = exports.sendInteractiveChoices = exports.buildChoiceMessage = exports.rememberStoryVideo = exports.RENDER_CHART_COMMAND = exports.CHOICE_WAIT_SECONDS = void 0;
|
|
37
37
|
/**
|
|
38
38
|
* 互动视频的「发选项 → 等选择 → 播下一段」这一段流程。
|
|
39
39
|
*
|
|
@@ -60,6 +60,43 @@ exports.CHOICE_WAIT_SECONDS = 180;
|
|
|
60
60
|
exports.RENDER_CHART_COMMAND = '渲染流程图';
|
|
61
61
|
/** 每个频道最近一条「选项消息」,用于发下一条之前撤回 */
|
|
62
62
|
const lastChoiceMessages = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* 每个频道最近一条「剧情视频」的消息 ID。
|
|
65
|
+
*
|
|
66
|
+
* 用户要求:点一下选项,**上一段视频也要撤回** —— 不然玩几段之后群里躺着一串视频。
|
|
67
|
+
* 视频是解析链路发的(不是这里发的),所以由那边把它发出去的消息 ID 记进来。
|
|
68
|
+
*/
|
|
69
|
+
const lastStoryVideos = new Map();
|
|
70
|
+
/** 记下刚发出去的剧情视频(由解析链路在上传成功时调用) */
|
|
71
|
+
const rememberStoryVideo = (e, messageId) => {
|
|
72
|
+
if (!messageId)
|
|
73
|
+
return;
|
|
74
|
+
lastStoryVideos.set(sessionKeyOf(e), String(messageId));
|
|
75
|
+
};
|
|
76
|
+
exports.rememberStoryVideo = rememberStoryVideo;
|
|
77
|
+
/**
|
|
78
|
+
* 取出并清掉「上一条剧情视频」的消息 ID。
|
|
79
|
+
*
|
|
80
|
+
* 取出来就删:这条只该被撤回一次,撤回失败也不能反复去点。
|
|
81
|
+
*/
|
|
82
|
+
const takeStoryVideo = (e) => {
|
|
83
|
+
const key = sessionKeyOf(e);
|
|
84
|
+
const messageId = lastStoryVideos.get(key);
|
|
85
|
+
if (messageId)
|
|
86
|
+
lastStoryVideos.delete(key);
|
|
87
|
+
return messageId;
|
|
88
|
+
};
|
|
89
|
+
/** 撤回某条消息(失败只记日志,绝不影响剧情) */
|
|
90
|
+
const recallMessage = async (e, messageId) => {
|
|
91
|
+
if (!messageId)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
await e.bot?.recallMsg?.(messageId, channelOf(e));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
node_karin_1.logger.debug('[互动视频] 撤回消息失败: ' + String(error?.message ?? error));
|
|
98
|
+
}
|
|
99
|
+
};
|
|
63
100
|
const platformOf = (e) => String(e?.bot?.bot?.platform ?? e?.bot?.platform ?? e?.platform ?? '');
|
|
64
101
|
const channelOf = (e) => String(e?.contact?.peer ?? e?.guildId ?? '');
|
|
65
102
|
/** 剧情会话 key(和 InteractiveVideo 里的口径一致) */
|
|
@@ -315,12 +352,20 @@ const runInteractiveStory = async (options) => {
|
|
|
315
352
|
entry = { fromCid: currentCid, edgeId: choice.edgeId };
|
|
316
353
|
currentCid = choice.cid;
|
|
317
354
|
if (play) {
|
|
355
|
+
/**
|
|
356
|
+
* 用户要求:点一下选项,**上一段视频也要撤回**(群里只留当前这一段)。
|
|
357
|
+
*
|
|
358
|
+
* 顺序是「发新的 → 撤旧的」:先取走旧 ID(新的那条发出去时会被重新记上),
|
|
359
|
+
* 新的发完再撤旧的 —— 中间不会出现「一段视频都没有」的空档。
|
|
360
|
+
*/
|
|
361
|
+
const previousVideo = takeStoryVideo(target);
|
|
318
362
|
try {
|
|
319
363
|
await play(currentCid, target);
|
|
320
364
|
}
|
|
321
365
|
catch (error) {
|
|
322
366
|
node_karin_1.logger.warn('[互动视频] 播放下一段失败(cid=' + currentCid + '): ' + String(error?.message ?? error));
|
|
323
367
|
}
|
|
368
|
+
await recallMessage(target, previousVideo);
|
|
324
369
|
}
|
|
325
370
|
node = { ...next, cid: currentCid };
|
|
326
371
|
}
|
package/package.json
CHANGED
|
@@ -145,6 +145,33 @@ const main = async () => {
|
|
|
145
145
|
check('结局时撤回了上一条选项消息', state.recalled.length >= 1, JSON.stringify(state.recalled))
|
|
146
146
|
check('结局有提示', state.replies.some((item) => String(item).includes('结局')), JSON.stringify(state.replies).slice(0, 200))
|
|
147
147
|
|
|
148
|
+
console.log('== 点一下选项:上一段视频也要撤回(用户要求)==')
|
|
149
|
+
{
|
|
150
|
+
const { e, state } = makeEvent('qqguild')
|
|
151
|
+
/** 解析链路发第一段视频时记下的消息 ID */
|
|
152
|
+
story.rememberStoryVideo(e, 'vid-root')
|
|
153
|
+
const playedVideo = []
|
|
154
|
+
const r = await runInteractiveStory({
|
|
155
|
+
e,
|
|
156
|
+
bvid: 'BV1xDgL6SEzk',
|
|
157
|
+
graphVersion: 1722536,
|
|
158
|
+
rootCid: 41156151510,
|
|
159
|
+
title: '史蒂夫的选择',
|
|
160
|
+
request: requestFor({ root: rootPayload, 11: leafPayload, 12: middlePayload }),
|
|
161
|
+
wait: async () => 'A',
|
|
162
|
+
play: async (cid) => {
|
|
163
|
+
playedVideo.push(cid)
|
|
164
|
+
/** 模拟解析链路:新一段视频发出去后把它的消息 ID 记进来 */
|
|
165
|
+
story.rememberStoryVideo(e, 'vid-' + cid)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
check('选完播了下一段', playedVideo.join(',') === '101', playedVideo.join(','))
|
|
169
|
+
check('上一段视频被撤回了(群里只留当前这一段)', state.recalled.includes('vid-root'), JSON.stringify(state.recalled))
|
|
170
|
+
check('刚发出去的那一段没有被误撤', !state.recalled.includes('vid-101'), JSON.stringify(state.recalled))
|
|
171
|
+
check('同一条只撤一次(取走即清)', state.recalled.filter((id) => id === 'vid-root').length === 1, JSON.stringify(state.recalled))
|
|
172
|
+
check('剧情照常走完', r.ended === 'leaf', r.ended)
|
|
173
|
+
}
|
|
174
|
+
|
|
148
175
|
console.log('== 结局那一段也要给上层画图的机会 ==')
|
|
149
176
|
const endingHook = makeEvent('qqguild')
|
|
150
177
|
const hookCalls = []
|
|
@@ -24,11 +24,29 @@ import {
|
|
|
24
24
|
// 解析阶段(「下载进度」指令读的就是这里登记的状态)
|
|
25
25
|
import { DOWNLOAD_STAGES, clearParseStage, updateDownloadStage } from './Network/Downloader'
|
|
26
26
|
|
|
27
|
+
/** 发送结果里把消息 ID 抠出来(不同适配器返回的形状不一样:对象带 messageId、或者就是字符串数组) */
|
|
28
|
+
const reportSent = (options: uploadFileOptions | undefined, sent: any): void => {
|
|
29
|
+
if (!options?.onSent) return
|
|
30
|
+
try {
|
|
31
|
+
const id = typeof sent === 'string'
|
|
32
|
+
? sent
|
|
33
|
+
: (sent?.messageId ?? (Array.isArray(sent) ? sent[0] : ''))
|
|
34
|
+
if (id) options.onSent(String(id))
|
|
35
|
+
} catch { /* 拿不到 ID 不影响发送本身 */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
type uploadFileOptions = {
|
|
28
39
|
/** 是否使用群文件上传 */
|
|
29
40
|
useGroupFile?: boolean
|
|
30
41
|
/** 消息ID,如果有,则将使用该消息ID制作回复元素 */
|
|
31
42
|
message_id?: string
|
|
43
|
+
/**
|
|
44
|
+
* 发出去了把消息 ID 交回来(可选)。
|
|
45
|
+
*
|
|
46
|
+
* 互动视频用它:下一段播出来之后要**撤回上一段那条视频**,群里只留当前这一段
|
|
47
|
+
* (不然点几次剧情,群里就躺着一串视频)。
|
|
48
|
+
*/
|
|
49
|
+
onSent?: (messageId: string) => void
|
|
32
50
|
/** 是否为主动消息 */
|
|
33
51
|
active?: boolean
|
|
34
52
|
/** 主动消息参数 */
|
|
@@ -351,12 +369,14 @@ export const uploadFile = async (event: Message, file: fileInfo, videoUrl: strin
|
|
|
351
369
|
// 是群文件
|
|
352
370
|
const bot = karin.getBot(String(options.activeOption?.uin))!
|
|
353
371
|
logger.mark(`${logger.blue('主动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow('bot.uploadFile')}回复...`)
|
|
354
|
-
await bot.uploadFile(contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`)
|
|
372
|
+
const sent: any = await bot.uploadFile(contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`)
|
|
373
|
+
reportSent(options, sent)
|
|
355
374
|
} else {
|
|
356
375
|
// 不是群文件
|
|
357
376
|
logger.mark(`${logger.blue('主动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow('karin.sendMsg')}回复...`)
|
|
358
377
|
// 兼容层已经把「没拿到消息 ID」当成失败抛出(见 compat/sendError),所以这里没有异常就是发出去了
|
|
359
|
-
await karin.sendMsg(selfId, contact, [segment.video(File)], { retryCount: 0 })
|
|
378
|
+
const sent: any = await karin.sendMsg(selfId, contact, [segment.video(File)], { retryCount: 0 })
|
|
379
|
+
reportSent(options, sent)
|
|
360
380
|
sendStatus = true
|
|
361
381
|
}
|
|
362
382
|
} else {
|
|
@@ -364,12 +384,14 @@ export const uploadFile = async (event: Message, file: fileInfo, videoUrl: strin
|
|
|
364
384
|
if (useGroupFile) {
|
|
365
385
|
// 是文件
|
|
366
386
|
logger.mark(`${logger.blue('被动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow('e.bot.uploadFile')}回复...`)
|
|
367
|
-
await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`)
|
|
387
|
+
const sent: any = await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`)
|
|
388
|
+
reportSent(options, sent)
|
|
368
389
|
} else {
|
|
369
390
|
// 不是文件
|
|
370
391
|
logger.mark(`${logger.blue('被动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow('e.reply')}回复...`)
|
|
371
392
|
// 同上:异常才是失败(没拿到消息 ID 已经由兼容层抛出来了)
|
|
372
|
-
await event.reply(segment.video(File) || videoUrl)
|
|
393
|
+
const sent: any = await event.reply(segment.video(File) || videoUrl)
|
|
394
|
+
reportSent(options, sent)
|
|
373
395
|
sendStatus = true
|
|
374
396
|
}
|
|
375
397
|
}
|
|
@@ -85,7 +85,11 @@ import {
|
|
|
85
85
|
buildBilibiliVideoDescRichText,
|
|
86
86
|
getUsernameMetadata
|
|
87
87
|
} from '@/platform/bilibili/dynamic-text'
|
|
88
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
rememberStoryVideo,
|
|
90
|
+
renderInteractiveChart as renderChart,
|
|
91
|
+
runInteractiveStory
|
|
92
|
+
} from '@/platform/bilibili/interactive-story'
|
|
89
93
|
import { BilibiliDataTypes } from '@/types'
|
|
90
94
|
|
|
91
95
|
let img: ElementTypes[]
|
|
@@ -664,14 +668,13 @@ export class Bilibili extends Base {
|
|
|
664
668
|
await downloadTask
|
|
665
669
|
await this.sendPreparedVideo()
|
|
666
670
|
/**
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* 用户至少能看到这张图里有几条分支。
|
|
671
|
+
* 互动视频的剧情怎么走,看这次的视频发到哪儿:
|
|
672
|
+
* · **走在线播放**(视频没发进群里,用户在看播放页)→ 选项在播放页上贴出来,
|
|
673
|
+
* 群里不再问一遍,免得两处各问一次(见 sendPreparedVideo 里的 story);
|
|
674
|
+
* · **直接发到群里**(没有播放页)→ 照旧在群里发**带按钮的选择面板**,
|
|
675
|
+
* 用户点一下播下一段,并撤回上一段视频(用户要求)。
|
|
673
676
|
*/
|
|
674
|
-
if (this.interactive && !this.publishedStory) void this.
|
|
677
|
+
if (this.interactive && !this.publishedStory) void this.startInteractiveStory()
|
|
675
678
|
})
|
|
676
679
|
}
|
|
677
680
|
}
|
|
@@ -1935,35 +1938,6 @@ export class Bilibili extends Base {
|
|
|
1935
1938
|
return this.preparedVideo !== null
|
|
1936
1939
|
}
|
|
1937
1940
|
|
|
1938
|
-
/**
|
|
1939
|
-
* 互动视频发到群里时:只补一张**剧情图**,不问选项(用户要求:群里不要选择按钮)。
|
|
1940
|
-
*
|
|
1941
|
-
* 选项流程在播放页里(见 sendPreparedVideo 的 story);这里只把「有几条分支」画出来。
|
|
1942
|
-
* 失败只记日志 —— 出不了图不影响视频已经发出去这件事。
|
|
1943
|
-
*/
|
|
1944
|
-
private async sendInteractiveChartOnly (): Promise<void> {
|
|
1945
|
-
const interactive = this.interactive
|
|
1946
|
-
if (!interactive) return
|
|
1947
|
-
/** 没勾「流程图」就什么都不做(配置说了算) */
|
|
1948
|
-
if (!Config.bilibili.sendContent.some((item) => item === 'chart')) return
|
|
1949
|
-
try {
|
|
1950
|
-
const info = await fetchInteractiveInfo({ bvid: interactive.bvid, cid: interactive.cid, headers: this.headers })
|
|
1951
|
-
if (!info) return
|
|
1952
|
-
await renderChart({
|
|
1953
|
-
e: this.e,
|
|
1954
|
-
bvid: interactive.bvid,
|
|
1955
|
-
graphVersion: info.graphVersion,
|
|
1956
|
-
cid: interactive.cid,
|
|
1957
|
-
title: this.workInfo?.title ?? '互动视频',
|
|
1958
|
-
notice: info.notice,
|
|
1959
|
-
headers: this.headers,
|
|
1960
|
-
full: true
|
|
1961
|
-
})
|
|
1962
|
-
} catch (error: any) {
|
|
1963
|
-
logger.debug('[互动视频] 群里补剧情图失败(不影响视频): ' + String(error?.message ?? error))
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
1941
|
/**
|
|
1968
1942
|
* 起一次互动剧情(后台长跑,不阻塞解析主流程)。
|
|
1969
1943
|
*
|
|
@@ -2136,10 +2110,17 @@ export class Bilibili extends Base {
|
|
|
2136
2110
|
}
|
|
2137
2111
|
logger.warn('[在线播放] 播放会话登记失败,退回直接发送视频文件')
|
|
2138
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* 互动视频(发到群里的那种):把这条视频的消息 ID 记下来 ——
|
|
2115
|
+
* 用户点了下一个选项之后,这条要撤回,群里只留当前这一段。
|
|
2116
|
+
*/
|
|
2117
|
+
const sentOptions = this.interactive
|
|
2118
|
+
? { onSent: (messageId: string) => rememberStoryVideo(this.e, messageId) }
|
|
2119
|
+
: undefined
|
|
2139
2120
|
if (totalBytes > Config.app.groupfilevalue) {
|
|
2140
|
-
await uploadFile(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', { useGroupFile: true })
|
|
2121
|
+
await uploadFile(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', { useGroupFile: true, ...sentOptions })
|
|
2141
2122
|
} else {
|
|
2142
|
-
await uploadFile(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '')
|
|
2123
|
+
await uploadFile(this.e, { filepath, totalBytes, originTitle }, videoUrl ?? '', sentOptions)
|
|
2143
2124
|
}
|
|
2144
2125
|
return true
|
|
2145
2126
|
}
|
|
@@ -41,6 +41,42 @@ export const RENDER_CHART_COMMAND = '渲染流程图'
|
|
|
41
41
|
/** 每个频道最近一条「选项消息」,用于发下一条之前撤回 */
|
|
42
42
|
const lastChoiceMessages = new Map<string, string>()
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 每个频道最近一条「剧情视频」的消息 ID。
|
|
46
|
+
*
|
|
47
|
+
* 用户要求:点一下选项,**上一段视频也要撤回** —— 不然玩几段之后群里躺着一串视频。
|
|
48
|
+
* 视频是解析链路发的(不是这里发的),所以由那边把它发出去的消息 ID 记进来。
|
|
49
|
+
*/
|
|
50
|
+
const lastStoryVideos = new Map<string, string>()
|
|
51
|
+
|
|
52
|
+
/** 记下刚发出去的剧情视频(由解析链路在上传成功时调用) */
|
|
53
|
+
export const rememberStoryVideo = (e: Message, messageId: string | undefined): void => {
|
|
54
|
+
if (!messageId) return
|
|
55
|
+
lastStoryVideos.set(sessionKeyOf(e), String(messageId))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 取出并清掉「上一条剧情视频」的消息 ID。
|
|
60
|
+
*
|
|
61
|
+
* 取出来就删:这条只该被撤回一次,撤回失败也不能反复去点。
|
|
62
|
+
*/
|
|
63
|
+
const takeStoryVideo = (e: Message): string | undefined => {
|
|
64
|
+
const key = sessionKeyOf(e)
|
|
65
|
+
const messageId = lastStoryVideos.get(key)
|
|
66
|
+
if (messageId) lastStoryVideos.delete(key)
|
|
67
|
+
return messageId
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 撤回某条消息(失败只记日志,绝不影响剧情) */
|
|
71
|
+
const recallMessage = async (e: Message, messageId: string | undefined): Promise<void> => {
|
|
72
|
+
if (!messageId) return
|
|
73
|
+
try {
|
|
74
|
+
await (e.bot as any)?.recallMsg?.(messageId, channelOf(e))
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
logger.debug('[互动视频] 撤回消息失败: ' + String(error?.message ?? error))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
const platformOf = (e: any): string =>
|
|
45
81
|
String(e?.bot?.bot?.platform ?? e?.bot?.platform ?? e?.platform ?? '')
|
|
46
82
|
|
|
@@ -358,11 +394,19 @@ export const runInteractiveStory = async (options: StoryOptions): Promise<StoryR
|
|
|
358
394
|
currentCid = choice.cid
|
|
359
395
|
|
|
360
396
|
if (play) {
|
|
397
|
+
/**
|
|
398
|
+
* 用户要求:点一下选项,**上一段视频也要撤回**(群里只留当前这一段)。
|
|
399
|
+
*
|
|
400
|
+
* 顺序是「发新的 → 撤旧的」:先取走旧 ID(新的那条发出去时会被重新记上),
|
|
401
|
+
* 新的发完再撤旧的 —— 中间不会出现「一段视频都没有」的空档。
|
|
402
|
+
*/
|
|
403
|
+
const previousVideo = takeStoryVideo(target)
|
|
361
404
|
try {
|
|
362
405
|
await play(currentCid, target)
|
|
363
406
|
} catch (error: any) {
|
|
364
407
|
logger.warn('[互动视频] 播放下一段失败(cid=' + currentCid + '): ' + String(error?.message ?? error))
|
|
365
408
|
}
|
|
409
|
+
await recallMessage(target, previousVideo)
|
|
366
410
|
}
|
|
367
411
|
node = { ...next, cid: currentCid }
|
|
368
412
|
}
|