koishi-plugin-kkk 4.0.0 → 4.0.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # 更新日志
2
2
 
3
+ ## 4.0.1
4
+
5
+ > 修一个 4.0.0 带出来的问题:开了「强制在线播放」之后,**番剧还是会下载完直接发文件**,
6
+ > 在发不出视频文件的机器人上(比如 B站私聊机器人)就卡在上传那一步报错。
7
+ > 普通视频不受影响 —— 它一直都是给链接的。
8
+
9
+ ### 修:番剧不走「强制在线播放」
10
+
11
+ - 「在线播放器设置」里的「强制在线播放的适配器」(默认填的是 `bilibili`)对**普通视频**
12
+ 一直是生效的:解析完给一条在线播放链接,不发视频文件。
13
+ - 但**番剧**走的是另一条分支,那一条漏了这一步 —— 于是照旧把视频下下来发文件,
14
+ 碰上发不出视频文件的机器人就卡在上传那一步报错。
15
+ - 现在两条分支一致了:**番剧也只给在线播放链接**,不再尝试发文件。
16
+
3
17
  ## 4.0.0
4
18
 
5
19
  > 这是 3.0 之后攒的一次大版本。下面把 **3.0 到 4.0 之间所有你用得出来的变化**
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "4.0.0",
3
- "buildTime": "2026-10-05T14:30:11.577Z",
4
- "buildTimestamp": 1791210611583,
2
+ "version": "4.0.1",
3
+ "buildTime": "2026-10-06T06:35:53.430Z",
4
+ "buildTimestamp": 1791268553468,
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",
8
- "commitHash": "d495df3b2707919944e94f53a90a94f1f79ffafa",
9
- "shortCommitHash": "d495df3"
8
+ "commitHash": "46d0341ac561e5273722748b928b2284fd83904f",
9
+ "shortCommitHash": "46d0341"
10
10
  }
@@ -661,6 +661,18 @@ class Bilibili extends utils_1.Base {
661
661
  break;
662
662
  }
663
663
  case 'bangumi_video_info': {
664
+ /**
665
+ * **强制在线播放**(通用 → 在线播放器设置 → 强制在线播放的适配器):
666
+ * 和 `one_video` 一样,**进分支第一件事**就调。
667
+ *
668
+ * ⚠️ 以前只有 `one_video` 调了这一句,番剧这条分支没有 ——
669
+ * 于是「B站私聊机器人(platform === 'bilibili')」这类发不出视频文件的适配器上,
670
+ * **普通视频走在线播放、番剧却照旧下载后发文件**,最后卡在上传那一步报错
671
+ * (用户反馈:「我不是设置了强制在线播放吗?为什么解析番剧还是会直接发文件」)。
672
+ * 标记之后下面 `getvideo → sendPreparedVideo` 读 `isOnlinePlayerRequest()`
673
+ * 会走播放页那条路,不再碰上传。
674
+ */
675
+ (0, player_1.applyForceOnlinePlayer)(this.e);
664
676
  const videoInfo = await this.amagi.bilibili.fetcher.fetchBangumiInfo({
665
677
  [iddata.isEpid ? 'ep_id' : 'season_id']: iddata.realid
666
678
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -0,0 +1,178 @@
1
+ /**
2
+ * 探针:「强制在线播放的适配器」有没有覆盖到**每一条会发视频文件的分支**。
3
+ *
4
+ * ## 这个 bug 是怎么来的
5
+ *
6
+ * 「强制在线播放」(通用 → 在线播放器设置,`forceOnlinePlayer`,默认 `bilibili`)
7
+ * 是靠平台 handler 一进来就调 `applyForceOnlinePlayer(e)` 生效的 —— 它把本次解析
8
+ * 标记成在线播放,后面 `sendPreparedVideo` 才会走播放页而不是上传文件。
9
+ *
10
+ * 但只有 `bilibili.ts` 的 `one_video` 这一支调了,**`bangumi_video_info`(番剧)没调** ——
11
+ * 于是 B站私聊机器人(platform === 'bilibili',发不出视频文件)上出现:
12
+ * 普通视频给链接、番剧照旧下载后发文件、最后卡在上传那一步报错。
13
+ * 用户原话:「我不是设置了强制在线播放吗?为什么解析番剧还是会直接发文件」。
14
+ *
15
+ * 这类漏调**没有任何编译期信号**(tsc 一条不报,构建也过),只能靠静态闸门盯住。
16
+ *
17
+ * ## 盯什么
18
+ *
19
+ * 1. 静态:四个平台 handler 里,凡是**会走到发送视频**的 `case` 分支,
20
+ * 体内必须出现 `applyForceOnlinePlayer(`;
21
+ * 2. 运行时:名单解析(逗号 / 数组 / 中文逗号 / 空)、
22
+ * 适配器在名单里 → 本次真的被标记成在线播放、不在名单里 → 不动。
23
+ */
24
+ const fs = require('node:fs')
25
+ const path = require('node:path')
26
+
27
+ const root = path.join(__dirname, '..')
28
+ const lib = path.join(root, 'lib')
29
+
30
+ let failed = 0
31
+ const check = (name, ok, detail) => {
32
+ console.log((ok ? ' \u2714 ' : ' \u2718 ') + name + (detail ? ' \u2192 ' + detail : ''))
33
+ if (!ok) failed++
34
+ }
35
+
36
+ /** 这四个平台 handler 里出现这些调用,说明这一支会真的把视频发出去 */
37
+ const SENDS_VIDEO = /getvideo\(|sendPreparedVideo\(|prepareVideo\(|downloadVideo\(|uploadFile\(/
38
+
39
+ /**
40
+ * 去掉注释再判 —— ⚠️ 不去掉会**误报**:
41
+ * 直播那一支的注释里写着「prepareVideo() 把视频下下来…sendPreparedVideo() 再上传」,
42
+ * 正文一行调用都没有,按正文判会把它算成「漏了」。
43
+ */
44
+ const stripComments = (s) =>
45
+ s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/[^\n]*/g, '$1')
46
+
47
+ /** 把源码按顶层 `case 'xxx': {` 切成一段一段(缩进 6 空格的那层 switch) */
48
+ const splitBranches = (src) => {
49
+ const marks = [...src.matchAll(/\n {6}case '([^']+)':\s*\{/g)]
50
+ return marks.map((m, i) => {
51
+ const start = m.index
52
+ /** 下一个 case 之前;同时**不能越过 switch 的收尾**(缩进 4 空格的 `}`),
53
+ * 否则最后一支会一路吃到后面的类方法(那些方法名本身就是 `prepareVideo`) */
54
+ const next = i + 1 < marks.length ? marks[i + 1].index : src.length
55
+ const close = src.indexOf('\n }', start)
56
+ const end = close > 0 && close < next ? close : next
57
+ return { name: m[1], start, end, body: stripComments(src.slice(start, end)) }
58
+ })
59
+ }
60
+
61
+ console.log('=== 1. 静态:会发视频的分支都调了 applyForceOnlinePlayer ===')
62
+
63
+ for (const file of ['bilibili/bilibili.ts', 'douyin/douyin.ts', 'kuaishou/kuaishou.ts', 'xiaohongshu/xiaohongshu.ts']) {
64
+ const src = fs.readFileSync(path.join(root, 'src', 'karin', 'platform', file), 'utf8')
65
+ const branches = splitBranches(src)
66
+ const offending = []
67
+ let videoBranches = 0
68
+ for (const branch of branches) {
69
+ if (!SENDS_VIDEO.test(branch.body)) continue
70
+ videoBranches++
71
+ if (!/applyForceOnlinePlayer\(/.test(branch.body)) offending.push(branch.name)
72
+ }
73
+ check(file + ':' + videoBranches + ' 个会发视频的分支都标记了强制在线播放',
74
+ offending.length === 0, offending.length ? '漏了 ' + offending.join(', ') : branches.map((b) => b.name).join(', '))
75
+ }
76
+
77
+ {
78
+ /**
79
+ * 单独点名两支(不依赖上面的正则判据,改坏了上面的闸门也不会把这两条一起放过去):
80
+ * B站**普通视频**和**番剧**是两条不同的分支,以前只有前者调了。
81
+ */
82
+ const src = fs.readFileSync(path.join(root, 'src', 'karin', 'platform', 'bilibili', 'bilibili.ts'), 'utf8')
83
+ for (const name of ['one_video', 'bangumi_video_info']) {
84
+ const branch = splitBranches(src).find((b) => b.name === name)
85
+ check(' B站 ' + name + ' 这一支调了 applyForceOnlinePlayer',
86
+ !!branch && /applyForceOnlinePlayer\(this\.e\)/.test(branch.body))
87
+ }
88
+ /** 顺序也不能反:必须在发视频之前标记 */
89
+ const bangumi = splitBranches(src).find((b) => b.name === 'bangumi_video_info').body
90
+ const firstSend = Math.min(
91
+ ...['getvideo(', 'sendPreparedVideo('].map((k) => {
92
+ const at = bangumi.indexOf(k)
93
+ return at < 0 ? Number.MAX_SAFE_INTEGER : at
94
+ }))
95
+ const firstMark = bangumi.indexOf('applyForceOnlinePlayer(')
96
+ check(' 番剧分支里标记发生在「发视频」之前(顺序不能反)',
97
+ firstMark >= 0 && firstMark < firstSend, '标记 @' + firstMark + ' / 发送 @' + firstSend)
98
+ }
99
+
100
+ console.log('=== 2. 运行时:名单解析 + 真的把本次标记成在线播放 ===')
101
+
102
+ const runtime = require(path.join(lib, 'compat/runtime.js'))
103
+ runtime.bindRuntime({
104
+ ctx: { config: { port: 5200, prefix: '' } },
105
+ config: { playerEnabled: true, forceOnlinePlayer: 'bilibili' },
106
+ dataRoot: fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'kkk-probe-force-'))
107
+ })
108
+ require(path.join(lib, 'compat/logger.js')).setLogger({ debug () {}, info () {}, mark () {}, warn () {}, error () {} })
109
+
110
+ const player = require(path.join(lib, 'player', 'index.js'))
111
+ const { runWithParseOverride, getParseOverride } = require(path.join(lib, 'karin', 'module', 'utils', 'ParseOverride.js'))
112
+
113
+ check('默认配置下 bilibili 在强制名单里', player.isForceOnlinePlayerAdapter('bilibili') === true)
114
+ check('qq 不在名单里(不强制)', player.isForceOnlinePlayerAdapter('qq') === false)
115
+ check('大小写不敏感(Bilibili 也算)', player.isForceOnlinePlayerAdapter('Bilibili') === true)
116
+ check('空 / 未配置不强制', player.isForceOnlinePlayerAdapter('') === false && player.isForceOnlinePlayerAdapter(undefined) === false)
117
+
118
+ /** 直接换一份配置验「逗号分隔 + 中文逗号 + 数组」三种写法 */
119
+ const withConfig = (value, fn) => {
120
+ runtime.bindRuntime({
121
+ ctx: { config: { port: 5200, prefix: '' } },
122
+ config: { playerEnabled: true, forceOnlinePlayer: value },
123
+ dataRoot: fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'kkk-probe-force-'))
124
+ })
125
+ try { return fn() } finally {
126
+ runtime.bindRuntime({
127
+ ctx: { config: { port: 5200, prefix: '' } },
128
+ config: { playerEnabled: true, forceOnlinePlayer: 'bilibili' },
129
+ dataRoot: fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'kkk-probe-force-'))
130
+ })
131
+ }
132
+ }
133
+ check('英文逗号分隔(bilibili,onebot)',
134
+ withConfig('bilibili,onebot', () => player.isForceOnlinePlayerAdapter('onebot') === true))
135
+ check('中文逗号分隔(bilibili,onebot)',
136
+ withConfig('bilibili,onebot', () => player.isForceOnlinePlayerAdapter('onebot') === true))
137
+ check('数组写法(["bilibili","onebot"])',
138
+ withConfig(['bilibili', 'onebot'], () => player.isForceOnlinePlayerAdapter('onebot') === true))
139
+ check('留空 = 不强制', withConfig('', () => player.isForceOnlinePlayerAdapter('bilibili') === false))
140
+
141
+ /** 真跑一遍:适配器在名单里 → 本次被标记;不在 → 不动 */
142
+ const run = async () => {
143
+ await runWithParseOverride({}, async () => {
144
+ const ok = player.applyForceOnlinePlayer({ bot: { bot: { platform: 'bilibili' } } })
145
+ check('名单里的适配器:applyForceOnlinePlayer 返回 true', ok === true, String(ok))
146
+ check(' → 本次真的被标记成在线播放', player.isOnlinePlayerRequest() === true, String(getParseOverride()?.onlinePlayer))
147
+ })
148
+ await runWithParseOverride({}, async () => {
149
+ const ok = player.applyForceOnlinePlayer({ bot: { bot: { platform: 'qq' } } })
150
+ check('名单外的适配器:不标记', ok === false && player.isOnlinePlayerRequest() === false)
151
+ })
152
+ /** 平台名可能挂在 e.platform 上(兼容层真实 Bot 在 bot.bot) */
153
+ await runWithParseOverride({}, async () => {
154
+ const ok = player.applyForceOnlinePlayer({ platform: 'bilibili' })
155
+ check(' → e.platform 这种挂法也认', ok === true, String(ok))
156
+ })
157
+ /**
158
+ * 播放器总开关关掉时不标记 —— 没有播放页可去,
159
+ * 标记了反而会让视频「下载完却什么都不发」。
160
+ */
161
+ runtime.bindRuntime({
162
+ ctx: { config: { port: 5200, prefix: '' } },
163
+ config: { playerEnabled: false, forceOnlinePlayer: 'bilibili' },
164
+ dataRoot: fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'kkk-probe-force-'))
165
+ })
166
+ await runWithParseOverride({}, async () => {
167
+ const ok = player.applyForceOnlinePlayer({ bot: { bot: { platform: 'bilibili' } } })
168
+ check('总开关关掉时即使平台在名单里也不标记', ok === false && player.isOnlinePlayerRequest() === false)
169
+ })
170
+ }
171
+
172
+ run().then(() => {
173
+ console.log(failed === 0 ? '\n✔ 全部通过' : '\n✘ 有 ' + failed + ' 项没通过')
174
+ process.exit(failed === 0 ? 0 : 1)
175
+ }).catch((error) => {
176
+ console.log('探针自己崩了: ' + String(error?.stack ?? error))
177
+ process.exit(1)
178
+ })
@@ -734,6 +734,18 @@ export class Bilibili extends Base {
734
734
  break
735
735
  }
736
736
  case 'bangumi_video_info': {
737
+ /**
738
+ * **强制在线播放**(通用 → 在线播放器设置 → 强制在线播放的适配器):
739
+ * 和 `one_video` 一样,**进分支第一件事**就调。
740
+ *
741
+ * ⚠️ 以前只有 `one_video` 调了这一句,番剧这条分支没有 ——
742
+ * 于是「B站私聊机器人(platform === 'bilibili')」这类发不出视频文件的适配器上,
743
+ * **普通视频走在线播放、番剧却照旧下载后发文件**,最后卡在上传那一步报错
744
+ * (用户反馈:「我不是设置了强制在线播放吗?为什么解析番剧还是会直接发文件」)。
745
+ * 标记之后下面 `getvideo → sendPreparedVideo` 读 `isOnlinePlayerRequest()`
746
+ * 会走播放页那条路,不再碰上传。
747
+ */
748
+ applyForceOnlinePlayer(this.e)
737
749
  const videoInfo = await this.amagi.bilibili.fetcher.fetchBangumiInfo({
738
750
  [iddata.isEpid ? 'ep_id' : 'season_id']: iddata.realid
739
751
  })