koishi-plugin-kkk 3.3.0-beta.3 → 3.3.2

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,14 +1,21 @@
1
1
  # 更新日志
2
2
 
3
- ## 3.3.0-beta.3
3
+ ## 3.3.2
4
+
5
+ > 修 3.3.1 的一个**致命笔误**:播放器模块里一行 import 多写了一层 `..`,导致插件在**加载时**就失败(`Cannot find module '../../compat/runtime'`),整个插件都起不来。构建链路只做转译不解析路径,所以当时没拦住 —— 现在补了一个「每个编译产物都 require 得动」的自测(scripts/smoke-require.cjs)。
6
+
7
+ ## 3.3.1
8
+
9
+ > 3.3.0-beta.1 ~ beta.3 是给先行测试的人用的预览版,内容全部并进这一版;从 3.2.2 直接升上来就行。
4
10
 
5
11
  ### 修了两个「设置根本没生效」的问题
6
12
 
7
13
  - **「强制在线播放的适配器」这类设置以前是静默失效的**:运行时配置是一份**手写白名单**,新加的开关(强制在线播放的适配器、面板「在线看」列、按需切片、解析去重…)忘了往里补一行,运行时读到的就是 undefined —— 表现就是 B站私聊明明配了强制在线播放,**还是去下载并发视频文件**。现在改成「字段表里的默认值打底 + 归一化覆盖」,以后再加开关不会漏。
8
14
  - **不支持的适配器不再尝试合并转发**:判断「这个适配器支不支持聊天记录」用的是**黑名单**(只排除 QQ 官方),于是任何自定义适配器都被当成支持 —— B站私聊适配器(platform 就是 bilibili)也被算进去了,整条转发发出去「没有拿到消息 ID」,连退回来的逐条发送也失败,用户什么都没收到。现在改成**白名单**(OneBot 系平台名,或适配器自带 sendGroupForwardMsg/sendPrivateForwardMsg),并会在日志里写明「适配器 xxx 不支持合并转发,本次逐条发送」。
9
- ## 3.3.0-beta.2
10
15
 
11
- > **预览版**:给愿意先试的人用。npm 上默认安装的(latest)还是 3.2.2,想装这个用 npm i koishi-plugin-kkk@beta(或者面板里的「检查更新」换成 beta 通道)。
16
+ ### 播放链接显示「已过期」的原因说清楚了
17
+
18
+ - **端口被别的实例占掉,链接就会指向「另一台机器人」**:同机跑两台 Koishi(正式 + 测试)时只有一台能监听播放器端口。占不到端口的那台退回 Koishi 自己的端口,而域名反代的是独立端口 —— 用户点开链接访问的其实是**抢到端口的那台实例**,它的会话表里没有这个 token,页面就显示「链接已过期」。现在遇到这种情况会打一条**明确的警告**(写清「链接会显示已过期,请换端口或停掉占端口的那台实例」),不再是含糊的「启动失败」;生成链接时也会提醒一次。
12
19
 
13
20
  ### 控制台里的设置全部隐藏,面板补上「Koishi 设置」
14
21
 
@@ -199,6 +199,8 @@ function forceOnlinePlayerAdapters() {
199
199
  }
200
200
  }
201
201
  /** 这个**适配器**是不是在「强制在线播放」名单里 */
202
+ /** 「本实例独立端口没监听上」这条只提醒一次,不然每条链接都刷一行 */
203
+ let warnedStandaloneMismatch = false;
202
204
  function isForceOnlinePlayerAdapter(platform) {
203
205
  if (!platform)
204
206
  return false;
@@ -366,8 +368,20 @@ let warnedLocalBase = false;
366
368
  function buildPlayerLink(token) {
367
369
  const config = (0, runtime_1.tryGetRuntime)()?.config ?? {};
368
370
  const base = String(config.playerBaseUrl ?? '').replace(/\/+$/, '');
369
- if (base)
371
+ if (base) {
372
+ /**
373
+ * 本实例**没监听上独立端口**,但链接指向公网域名 —— 那个域名反代的是独立端口,
374
+ * 于是用户点开访问的其实是**抢到端口的另一台实例**(它的会话表里没有这个 token),
375
+ * 页面就会显示「链接已过期」。线上真实故障,这里至少要说清楚一次。
376
+ */
377
+ if (!(0, server_1.isStandalonePlayerReady)() && !warnedStandaloneMismatch) {
378
+ warnedStandaloneMismatch = true;
379
+ node_karin_1.logger.warn('[在线播放] 本实例没有监听上独立端口(被别的进程占了),但链接指向 ' + base
380
+ + ' —— 如果那个域名反代到同一个端口,用户点开会看到「链接已过期」(其实连的是抢到端口的那台实例)。'
381
+ + '请换一个「播放器端口」,或者停掉同机另一台占着该端口的 Koishi 实例');
382
+ }
370
383
  return base + '/kkk/player/' + token;
384
+ }
371
385
  const port = Number(config.playerPort) > 0 ? Number(config.playerPort) : koishiPort();
372
386
  const link = 'http://' + localAddress() + ':' + port + '/kkk/player/' + token;
373
387
  if (!warnedLocalBase) {
@@ -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.PLAYER_ROUTE_PREFIX = void 0;
39
+ exports.isStandalonePlayerReady = exports.PLAYER_ROUTE_PREFIX = void 0;
40
40
  exports.sanitizeDownloadName = sanitizeDownloadName;
41
41
  exports.downloadDisposition = downloadDisposition;
42
42
  exports.handlePlayerRequest = handlePlayerRequest;
@@ -64,10 +64,26 @@ const node_fs_1 = __importDefault(require("node:fs"));
64
64
  const node_http_1 = __importDefault(require("node:http"));
65
65
  const node_path_1 = __importDefault(require("node:path"));
66
66
  const node_karin_1 = require("../compat/node-karin.js");
67
+ // 注意只有一层 .. :src/player/server.ts → src/compat/runtime(写两层会变成根目录下的 compat,直接 MODULE_NOT_FOUND)
68
+ const runtime_1 = require("../compat/runtime");
67
69
  const page_1 = require("./page");
68
70
  const store_1 = require("./store");
69
71
  /** 路由前缀 */
70
72
  exports.PLAYER_ROUTE_PREFIX = '/kkk/player/';
73
+ /** 独立端口是否真的监听上了:没监听上时链接指向的公网域名很可能打不到本实例 */
74
+ let standaloneReady = false;
75
+ /** 查询独立端口是否监听上了(给「生成链接」那边做提示用) */
76
+ const isStandalonePlayerReady = () => standaloneReady;
77
+ exports.isStandalonePlayerReady = isStandalonePlayerReady;
78
+ /** 面板里配的「播放器公网地址」(没配就是空串) */
79
+ function publicBaseUrl() {
80
+ try {
81
+ return String((0, runtime_1.tryGetRuntime)()?.config?.playerBaseUrl ?? '').trim();
82
+ }
83
+ catch {
84
+ return '';
85
+ }
86
+ }
71
87
  const TEXT_TYPE = 'text/plain; charset=utf-8';
72
88
  const HTML_TYPE = 'text/html; charset=utf-8';
73
89
  /** 过期 / 无效令牌的统一回应:页面直接渲染「链接已过期」,接口退回纯文本 */
@@ -405,13 +421,23 @@ function registerPlayerRoutes({ ctx, port }) {
405
421
  });
406
422
  });
407
423
  server.on('error', (error) => {
408
- node_karin_1.logger.warn('[在线播放] 播放器端口 ' + port + ' 启动失败(' + String(error?.code ?? error?.message ?? error)
409
- + '),已退回 Koishi 自己的端口;请在配置里换一个端口或留空');
424
+ const code = String(error?.code ?? error?.message ?? error);
425
+ /**
426
+ * 端口被别的进程占了:**必须说清楚后果** —— 线上真实故障就是「用户点开播放链接看到『链接已过期』」。
427
+ *
428
+ * 成因:这台实例退回 Koishi 自己的端口,而另一台实例(比如同机的测试实例)抢到了这个端口;
429
+ * 域名反代到该端口 → 用户访问的其实是**另一台实例**,它当然没有这个会话。
430
+ */
431
+ node_karin_1.logger.error('[在线播放] 播放器端口 ' + port + ' 被占用(' + code + '),本次退回 Koishi 自己的端口'
432
+ + (publicBaseUrl() ? '。注意:域名 ' + publicBaseUrl() + ' 反代的就是 ' + port + ' 的话,用户点开链接会看到「链接已过期」'
433
+ + ' —— 说明这台实例不是 8888 的占用者(同机另一个 Koishi 实例抢到了),换一个端口或先停掉那台实例' : ''));
434
+ standaloneReady = false;
410
435
  // 端口被占用不能把插件带崩:退回 ctx.server 再挂一遍
411
436
  registerOnKoishi(ctx, disposers);
412
437
  });
413
438
  try {
414
439
  server.listen(port, () => {
440
+ standaloneReady = true;
415
441
  node_karin_1.logger.info('[在线播放] 播放器已监听独立端口 ' + port);
416
442
  });
417
443
  disposers.push(() => { try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "3.3.0-beta.3",
3
+ "version": "3.3.2",
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,91 @@
1
+ /**
2
+ * 「每个编译产物都 require 得动」冒烟测试。
3
+ *
4
+ * 起因(线上真实故障):给 player/server.ts 加了一行
5
+ * `import { tryGetRuntime } from '../../compat/runtime'` —— 多写了一层 `..`,
6
+ * 编译出来变成 `require("../../compat/runtime")`(指向插件根目录下的 compat,不存在)。
7
+ * TypeScript 那条链路只做转译、不解析路径,构建时毫无提示;**插件在用户机器上直接加载失败**:
8
+ *
9
+ * [W] config failed to load kkk
10
+ * Error: Cannot find module '../../compat/runtime'
11
+ *
12
+ * 这个测试就是把 lib 下每个 .js 都 require 一遍 —— 路径写错、模块顶层就抛异常,全都跑不掉。
13
+ *
14
+ * 用法:node scripts/smoke-require.cjs
15
+ */
16
+ const fs = require('node:fs')
17
+ const path = require('node:path')
18
+ const { createRequire } = require('node:module')
19
+
20
+ const pluginRoot = path.resolve(__dirname, '..')
21
+ const libRoot = path.join(pluginRoot, 'lib')
22
+
23
+ /** 绑定最小运行时:很多模块在顶层就读配置/日志 */
24
+ const runtime = require(path.join(libRoot, 'compat', 'runtime.js'))
25
+ const noop = () => {}
26
+ runtime.bindRuntime({
27
+ ctx: {
28
+ get: () => undefined,
29
+ logger: () => ({ info: noop, warn: noop, error: noop, debug: noop, mark: noop }),
30
+ bots: [],
31
+ registry: new Map(),
32
+ on: noop,
33
+ middleware: noop
34
+ },
35
+ config: { app: {} },
36
+ dataRoot: path.join(pluginRoot, 'data-smoke-require'),
37
+ pluginRoot,
38
+ master: () => []
39
+ })
40
+
41
+ const walk = (dir) => {
42
+ const out = []
43
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
44
+ const full = path.join(dir, item.name)
45
+ if (item.isDirectory()) out.push(...walk(full))
46
+ else if (item.name.endsWith('.js')) out.push(full)
47
+ }
48
+ return out
49
+ }
50
+
51
+ const req = createRequire(path.join(libRoot, 'index.js'))
52
+ const files = walk(libRoot)
53
+ const failures = []
54
+ let ok = 0
55
+ let esm = 0
56
+ /** 缺第三方依赖(宿主没装)的模块:单独列出,不算失败 */
57
+ const optional = []
58
+ for (const file of files) {
59
+ try {
60
+ req(file)
61
+ ok++
62
+ } catch (error) {
63
+ const message = String(error?.message ?? error)
64
+ /** 模板类模块是 ESM(用 import 加载的),require 报这个是**预期**的,不算失败 */
65
+ const isEsm = error?.code === 'ERR_REQUIRE_ESM' || error?.code === 'ERR_REQUIRE_ASYNC_MODULE'
66
+ || /Cannot use import statement outside a module|require\(\) of ES Module/.test(message)
67
+ if (isEsm) { esm++; continue }
68
+ /**
69
+ * 缺**第三方包**(cors / node:sqlite 之类)不算这次要抓的问题:有些模块是动态 import 的可选路径,
70
+ * 依赖装没装取决于宿主环境。**相对路径**找不到才是真问题(就是这次线上的那种),照旧算失败。
71
+ */
72
+ const missing = /Cannot find module '([^']+)'/.exec(message)
73
+ if (missing && !missing[1].startsWith('.')) {
74
+ optional.push(path.relative(libRoot, file) + ' → 缺依赖 ' + missing[1])
75
+ continue
76
+ }
77
+ failures.push({ file: path.relative(libRoot, file), message })
78
+ }
79
+ }
80
+
81
+ console.log('检查了 ' + files.length + ' 个编译产物:require 成功 ' + ok + ' 个,ESM(预期跳过)' + esm + ' 个,缺第三方依赖(预期跳过)' + optional.length + ' 个')
82
+ for (const item of optional) console.log(' · ' + item)
83
+ if (failures.length === 0) {
84
+ console.log('\n=== 通过:每个模块都加载得动(没有写错的 import 路径 / 顶层异常) ===')
85
+ // 用 process.exit:require 这些模块时可能有别的东西把 exitCode 置成了非 0
86
+ process.exit(0)
87
+ } else {
88
+ for (const item of failures) console.log(' ❌ ' + item.file + ' → ' + item.message.split('\n')[0])
89
+ console.log('\n=== 失败:' + failures.length + ' 个模块加载不了 ===')
90
+ process.exit(1)
91
+ }
@@ -22,7 +22,7 @@ import { PLUGIN_DIR_NAME, tryGetRuntime } from '../compat/runtime'
22
22
  // 解析阶段(「下载进度」指令读的就是这里登记的状态)
23
23
  import { DOWNLOAD_STAGES, clearParseStage, updateDownloadStage } from '../karin/module/utils/Network/Downloader'
24
24
  import { getParseOverride } from '../karin/module/utils/ParseOverride'
25
- import { registerPlayerRoutes } from './server'
25
+ import { isStandalonePlayerReady, registerPlayerRoutes } from './server'
26
26
  import {
27
27
  normalizeExpireMinutes,
28
28
  registerPlayerSession,
@@ -151,6 +151,9 @@ export function forceOnlinePlayerAdapters (): string[] {
151
151
  }
152
152
 
153
153
  /** 这个**适配器**是不是在「强制在线播放」名单里 */
154
+ /** 「本实例独立端口没监听上」这条只提醒一次,不然每条链接都刷一行 */
155
+ let warnedStandaloneMismatch = false
156
+
154
157
  export function isForceOnlinePlayerAdapter (platform?: string): boolean {
155
158
  if (!platform) return false
156
159
  return forceOnlinePlayerAdapters().includes(String(platform).toLowerCase())
@@ -318,7 +321,20 @@ let warnedLocalBase = false
318
321
  export function buildPlayerLink (token: string): string {
319
322
  const config: any = tryGetRuntime()?.config ?? {}
320
323
  const base = String(config.playerBaseUrl ?? '').replace(/\/+$/, '')
321
- if (base) return base + '/kkk/player/' + token
324
+ if (base) {
325
+ /**
326
+ * 本实例**没监听上独立端口**,但链接指向公网域名 —— 那个域名反代的是独立端口,
327
+ * 于是用户点开访问的其实是**抢到端口的另一台实例**(它的会话表里没有这个 token),
328
+ * 页面就会显示「链接已过期」。线上真实故障,这里至少要说清楚一次。
329
+ */
330
+ if (!isStandalonePlayerReady() && !warnedStandaloneMismatch) {
331
+ warnedStandaloneMismatch = true
332
+ logger.warn('[在线播放] 本实例没有监听上独立端口(被别的进程占了),但链接指向 ' + base
333
+ + ' —— 如果那个域名反代到同一个端口,用户点开会看到「链接已过期」(其实连的是抢到端口的那台实例)。'
334
+ + '请换一个「播放器端口」,或者停掉同机另一台占着该端口的 Koishi 实例')
335
+ }
336
+ return base + '/kkk/player/' + token
337
+ }
322
338
  const port = Number(config.playerPort) > 0 ? Number(config.playerPort) : koishiPort()
323
339
  const link = 'http://' + localAddress() + ':' + port + '/kkk/player/' + token
324
340
  if (!warnedLocalBase) {
@@ -23,6 +23,9 @@ import path from 'node:path'
23
23
 
24
24
  import { logger } from 'node-karin'
25
25
 
26
+ // 注意只有一层 .. :src/player/server.ts → src/compat/runtime(写两层会变成根目录下的 compat,直接 MODULE_NOT_FOUND)
27
+ import { tryGetRuntime } from '../compat/runtime'
28
+
26
29
  import { renderExpiredPage, renderPlayerPage } from './page'
27
30
  import {
28
31
  getPlayerSession,
@@ -58,6 +61,21 @@ export interface PlayerHttpResponse {
58
61
  /** 路由前缀 */
59
62
  export const PLAYER_ROUTE_PREFIX = '/kkk/player/'
60
63
 
64
+ /** 独立端口是否真的监听上了:没监听上时链接指向的公网域名很可能打不到本实例 */
65
+ let standaloneReady = false
66
+
67
+ /** 查询独立端口是否监听上了(给「生成链接」那边做提示用) */
68
+ export const isStandalonePlayerReady = (): boolean => standaloneReady
69
+
70
+ /** 面板里配的「播放器公网地址」(没配就是空串) */
71
+ function publicBaseUrl (): string {
72
+ try {
73
+ return String((tryGetRuntime()?.config as any)?.playerBaseUrl ?? '').trim()
74
+ } catch {
75
+ return ''
76
+ }
77
+ }
78
+
61
79
  const TEXT_TYPE = 'text/plain; charset=utf-8'
62
80
  const HTML_TYPE = 'text/html; charset=utf-8'
63
81
 
@@ -387,13 +405,23 @@ export function registerPlayerRoutes ({ ctx, port }: { ctx: any, port: number })
387
405
  })
388
406
  })
389
407
  server.on('error', (error: any) => {
390
- logger.warn('[在线播放] 播放器端口 ' + port + ' 启动失败(' + String(error?.code ?? error?.message ?? error)
391
- + '),已退回 Koishi 自己的端口;请在配置里换一个端口或留空')
408
+ const code = String(error?.code ?? error?.message ?? error)
409
+ /**
410
+ * 端口被别的进程占了:**必须说清楚后果** —— 线上真实故障就是「用户点开播放链接看到『链接已过期』」。
411
+ *
412
+ * 成因:这台实例退回 Koishi 自己的端口,而另一台实例(比如同机的测试实例)抢到了这个端口;
413
+ * 域名反代到该端口 → 用户访问的其实是**另一台实例**,它当然没有这个会话。
414
+ */
415
+ logger.error('[在线播放] 播放器端口 ' + port + ' 被占用(' + code + '),本次退回 Koishi 自己的端口'
416
+ + (publicBaseUrl() ? '。注意:域名 ' + publicBaseUrl() + ' 反代的就是 ' + port + ' 的话,用户点开链接会看到「链接已过期」'
417
+ + ' —— 说明这台实例不是 8888 的占用者(同机另一个 Koishi 实例抢到了),换一个端口或先停掉那台实例' : ''))
418
+ standaloneReady = false
392
419
  // 端口被占用不能把插件带崩:退回 ctx.server 再挂一遍
393
420
  registerOnKoishi(ctx, disposers)
394
421
  })
395
422
  try {
396
423
  server.listen(port, () => {
424
+ standaloneReady = true
397
425
  logger.info('[在线播放] 播放器已监听独立端口 ' + port)
398
426
  })
399
427
  disposers.push(() => { try { server.close() } catch { /* 忽略 */ } })