koishi-plugin-kkk 3.3.0-beta.2 → 3.3.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 +11 -2
- package/lib/index.js +21 -1
- package/lib/karin/module/utils/ParseForward.js +30 -3
- package/lib/player/index.js +15 -1
- package/lib/player/server.js +28 -3
- package/package.json +1 -1
- package/src/index.ts +21 -1
- package/src/karin/module/utils/ParseForward.ts +29 -3
- package/src/player/index.ts +18 -2
- package/src/player/server.ts +29 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
## 3.3.
|
|
3
|
+
## 3.3.1
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> 3.3.0-beta.1 ~ beta.3 是给先行测试的人用的预览版,内容全部并进这一版;从 3.2.2 直接升上来就行。
|
|
6
|
+
|
|
7
|
+
### 修了两个「设置根本没生效」的问题
|
|
8
|
+
|
|
9
|
+
- **「强制在线播放的适配器」这类设置以前是静默失效的**:运行时配置是一份**手写白名单**,新加的开关(强制在线播放的适配器、面板「在线看」列、按需切片、解析去重…)忘了往里补一行,运行时读到的就是 undefined —— 表现就是 B站私聊明明配了强制在线播放,**还是去下载并发视频文件**。现在改成「字段表里的默认值打底 + 归一化覆盖」,以后再加开关不会漏。
|
|
10
|
+
- **不支持的适配器不再尝试合并转发**:判断「这个适配器支不支持聊天记录」用的是**黑名单**(只排除 QQ 官方),于是任何自定义适配器都被当成支持 —— B站私聊适配器(platform 就是 bilibili)也被算进去了,整条转发发出去「没有拿到消息 ID」,连退回来的逐条发送也失败,用户什么都没收到。现在改成**白名单**(OneBot 系平台名,或适配器自带 sendGroupForwardMsg/sendPrivateForwardMsg),并会在日志里写明「适配器 xxx 不支持合并转发,本次逐条发送」。
|
|
11
|
+
|
|
12
|
+
### 播放链接显示「已过期」的原因说清楚了
|
|
13
|
+
|
|
14
|
+
- **端口被别的实例占掉,链接就会指向「另一台机器人」**:同机跑两台 Koishi(正式 + 测试)时只有一台能监听播放器端口。占不到端口的那台退回 Koishi 自己的端口,而域名反代的是独立端口 —— 用户点开链接访问的其实是**抢到端口的那台实例**,它的会话表里没有这个 token,页面就显示「链接已过期」。现在遇到这种情况会打一条**明确的警告**(写清「链接会显示已过期,请换端口或停掉占端口的那台实例」),不再是含糊的「启动失败」;生成链接时也会提醒一次。
|
|
6
15
|
|
|
7
16
|
### 控制台里的设置全部隐藏,面板补上「Koishi 设置」
|
|
8
17
|
|
package/lib/index.js
CHANGED
|
@@ -892,7 +892,16 @@ async function apply(ctx, rawConfig) {
|
|
|
892
892
|
if (raw[key] !== undefined)
|
|
893
893
|
legacy[key] = raw[key];
|
|
894
894
|
}
|
|
895
|
-
|
|
895
|
+
/**
|
|
896
|
+
* **每个 qq 字段都要有值**:不能指望 Koishi 的 schema 默认值一定被填上 ——
|
|
897
|
+
* 线上实测踩过(用户报「B站私聊还是发视频、没给在线播放链接」):
|
|
898
|
+
* 配置里没写过的字段,运行时读到的是 `undefined`,
|
|
899
|
+
* 于是「强制在线播放的适配器」(默认 bilibili)整个失效、面板列按钮的开关也失效。
|
|
900
|
+
*
|
|
901
|
+
* 所以这里用 `readQqOptions` 先铺一层**字段表里的默认值**(qqFields.json 是唯一出处),
|
|
902
|
+
* 再用用户显式配的值覆盖 —— 显式配置永远优先,缺的字段一律有默认。
|
|
903
|
+
*/
|
|
904
|
+
const groupQq = { ...(0, qqOptions_1.readQqOptions)(raw), ...(qq ?? {}) };
|
|
896
905
|
const groupNative = { ...(advanced ?? {}) };
|
|
897
906
|
for (const key of qqOptions_1.QQ_KEYS)
|
|
898
907
|
if (legacy[key] !== undefined)
|
|
@@ -952,7 +961,18 @@ async function apply(ctx, rawConfig) {
|
|
|
952
961
|
const dataRoot = node_path_1.default.isAbsolute(config.dataPath) ? config.dataPath : node_path_1.default.resolve(ctx.baseDir ?? process.cwd(), config.dataPath);
|
|
953
962
|
(0, runtime_1.bindRuntime)({
|
|
954
963
|
ctx,
|
|
964
|
+
/**
|
|
965
|
+
* ⚠️ 这份 config **不是**原封不动转发,而是运行时真正读的那一份(`tryGetRuntime().config`)。
|
|
966
|
+
*
|
|
967
|
+
* 以前它是一个**手写白名单** —— 于是每加一个新开关(例如「强制在线播放的适配器」
|
|
968
|
+
* `forceOnlinePlayer`),忘了往这里补一行,运行时就读到 undefined,功能整个静默失效:
|
|
969
|
+
* 线上真实故障就是「B站私聊该给在线播放链接,结果还是去发视频文件」。
|
|
970
|
+
*
|
|
971
|
+
* 现在改成 `...readQqOptions(config)` 打底(qqFields.json 里的字段一个不落,缺的用默认值),
|
|
972
|
+
* 下面那些需要**归一化**的字段(端口取整数、分钟数夹范围、布尔用 !== false 这种)再覆盖上去。
|
|
973
|
+
*/
|
|
955
974
|
config: {
|
|
975
|
+
...(0, qqOptions_1.readQqOptions)(config),
|
|
956
976
|
masters: config.masters ?? [],
|
|
957
977
|
debug: config.debug,
|
|
958
978
|
dataPath: config.dataPath,
|
|
@@ -41,9 +41,31 @@ function parseForwardPeer(e) {
|
|
|
41
41
|
function platformOf(e) {
|
|
42
42
|
return String(e?.bot?.bot?.platform ?? e?.bot?.platform ?? '');
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* OneBot 系适配器(NapCat / Lagrange / go-cqhttp / Chronocat…):它们真的有「聊天记录」这个概念。
|
|
46
|
+
*
|
|
47
|
+
* 注意这份名单是**白名单**:compat 那边的 isForwardSupported 是黑名单(只排除 QQ 官方),
|
|
48
|
+
* 于是**任何自定义适配器都会被当成支持** —— 线上真实故障就是这样:
|
|
49
|
+
* B站私聊适配器(koishi-plugin-adapter-bilibili-dm,platform 就是 bilibili)被认成支持合并转发,
|
|
50
|
+
* 结果整条转发发出去「没有拿到消息 ID」(适配器根本没有聊天记录这种东西),
|
|
51
|
+
* 连退回来的逐条发送也失败了 —— 用户什么都没收到。
|
|
52
|
+
*/
|
|
53
|
+
const ONEBOT_LIKE = /onebot|napcat|lagrange|go-?cqhttp|chronocat|mirai/i;
|
|
54
|
+
/** 这台部署的适配器支不支持合并转发(**只有明确支持才算支持**) */
|
|
45
55
|
function canForwardParseResult(e) {
|
|
46
|
-
|
|
56
|
+
const raw = e?.bot?.bot ?? e?.bot;
|
|
57
|
+
/**
|
|
58
|
+
* ① 适配器自己带合并转发 API(OneBot 系的 sendGroupForwardMsg / sendPrivateForwardMsg)
|
|
59
|
+
* —— 这条最可靠,不依赖平台名怎么写。
|
|
60
|
+
*/
|
|
61
|
+
const internal = raw?.internal ?? e?.bot?.internal;
|
|
62
|
+
if (internal && (typeof internal.sendGroupForwardMsg === 'function' || typeof internal.sendPrivateForwardMsg === 'function'))
|
|
63
|
+
return true;
|
|
64
|
+
/** ② 其余只认 OneBot 系的平台名 */
|
|
65
|
+
const platform = platformOf(e);
|
|
66
|
+
if (!platform)
|
|
67
|
+
return false;
|
|
68
|
+
return ONEBOT_LIKE.test(platform);
|
|
47
69
|
}
|
|
48
70
|
/** 平台名 → 配置段名(合并转发的平台开关就写在各自的平台段里) */
|
|
49
71
|
const PLATFORM_SECTIONS = {
|
|
@@ -311,8 +333,13 @@ function withParseForward(handler, platform) {
|
|
|
311
333
|
+ '.forward 都没打开),按逐条发送处理');
|
|
312
334
|
return await handler(e, next);
|
|
313
335
|
}
|
|
314
|
-
if (!parseForwardPeer(e)
|
|
336
|
+
if (!parseForwardPeer(e))
|
|
315
337
|
return await handler(e, next);
|
|
338
|
+
if (!canForwardParseResult(e)) {
|
|
339
|
+
// 说明白为什么没合并:以前这里静默退化,用户只看到「转发了但没收到」
|
|
340
|
+
node_karin_1.logger.mark('[合并转发] 适配器 ' + (platformOf(e) || '未知') + ' 不支持合并转发(不是 OneBot 系、也没有 forward API),本次逐条发送');
|
|
341
|
+
return await handler(e, next);
|
|
342
|
+
}
|
|
316
343
|
return await (0, node_karin_1.runWithForwardBag)(parseForwardPeer(e), async () => {
|
|
317
344
|
try {
|
|
318
345
|
return await handler(e, next);
|
package/lib/player/index.js
CHANGED
|
@@ -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) {
|
package/lib/player/server.js
CHANGED
|
@@ -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,25 @@ 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
|
+
const runtime_1 = require("../../compat/runtime");
|
|
67
68
|
const page_1 = require("./page");
|
|
68
69
|
const store_1 = require("./store");
|
|
69
70
|
/** 路由前缀 */
|
|
70
71
|
exports.PLAYER_ROUTE_PREFIX = '/kkk/player/';
|
|
72
|
+
/** 独立端口是否真的监听上了:没监听上时链接指向的公网域名很可能打不到本实例 */
|
|
73
|
+
let standaloneReady = false;
|
|
74
|
+
/** 查询独立端口是否监听上了(给「生成链接」那边做提示用) */
|
|
75
|
+
const isStandalonePlayerReady = () => standaloneReady;
|
|
76
|
+
exports.isStandalonePlayerReady = isStandalonePlayerReady;
|
|
77
|
+
/** 面板里配的「播放器公网地址」(没配就是空串) */
|
|
78
|
+
function publicBaseUrl() {
|
|
79
|
+
try {
|
|
80
|
+
return String((0, runtime_1.tryGetRuntime)()?.config?.playerBaseUrl ?? '').trim();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
const TEXT_TYPE = 'text/plain; charset=utf-8';
|
|
72
87
|
const HTML_TYPE = 'text/html; charset=utf-8';
|
|
73
88
|
/** 过期 / 无效令牌的统一回应:页面直接渲染「链接已过期」,接口退回纯文本 */
|
|
@@ -405,13 +420,23 @@ function registerPlayerRoutes({ ctx, port }) {
|
|
|
405
420
|
});
|
|
406
421
|
});
|
|
407
422
|
server.on('error', (error) => {
|
|
408
|
-
|
|
409
|
-
|
|
423
|
+
const code = String(error?.code ?? error?.message ?? error);
|
|
424
|
+
/**
|
|
425
|
+
* 端口被别的进程占了:**必须说清楚后果** —— 线上真实故障就是「用户点开播放链接看到『链接已过期』」。
|
|
426
|
+
*
|
|
427
|
+
* 成因:这台实例退回 Koishi 自己的端口,而另一台实例(比如同机的测试实例)抢到了这个端口;
|
|
428
|
+
* 域名反代到该端口 → 用户访问的其实是**另一台实例**,它当然没有这个会话。
|
|
429
|
+
*/
|
|
430
|
+
node_karin_1.logger.error('[在线播放] 播放器端口 ' + port + ' 被占用(' + code + '),本次退回 Koishi 自己的端口'
|
|
431
|
+
+ (publicBaseUrl() ? '。注意:域名 ' + publicBaseUrl() + ' 反代的就是 ' + port + ' 的话,用户点开链接会看到「链接已过期」'
|
|
432
|
+
+ ' —— 说明这台实例不是 8888 的占用者(同机另一个 Koishi 实例抢到了),换一个端口或先停掉那台实例' : ''));
|
|
433
|
+
standaloneReady = false;
|
|
410
434
|
// 端口被占用不能把插件带崩:退回 ctx.server 再挂一遍
|
|
411
435
|
registerOnKoishi(ctx, disposers);
|
|
412
436
|
});
|
|
413
437
|
try {
|
|
414
438
|
server.listen(port, () => {
|
|
439
|
+
standaloneReady = true;
|
|
415
440
|
node_karin_1.logger.info('[在线播放] 播放器已监听独立端口 ' + port);
|
|
416
441
|
});
|
|
417
442
|
disposers.push(() => { try {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -885,7 +885,16 @@ export async function apply (ctx: Context, rawConfig: Config) {
|
|
|
885
885
|
for (const key of [...NATIVE_KEYS, ...QQ_KEYS]) {
|
|
886
886
|
if (raw[key] !== undefined) legacy[key] = raw[key]
|
|
887
887
|
}
|
|
888
|
-
|
|
888
|
+
/**
|
|
889
|
+
* **每个 qq 字段都要有值**:不能指望 Koishi 的 schema 默认值一定被填上 ——
|
|
890
|
+
* 线上实测踩过(用户报「B站私聊还是发视频、没给在线播放链接」):
|
|
891
|
+
* 配置里没写过的字段,运行时读到的是 `undefined`,
|
|
892
|
+
* 于是「强制在线播放的适配器」(默认 bilibili)整个失效、面板列按钮的开关也失效。
|
|
893
|
+
*
|
|
894
|
+
* 所以这里用 `readQqOptions` 先铺一层**字段表里的默认值**(qqFields.json 是唯一出处),
|
|
895
|
+
* 再用用户显式配的值覆盖 —— 显式配置永远优先,缺的字段一律有默认。
|
|
896
|
+
*/
|
|
897
|
+
const groupQq: any = { ...readQqOptions(raw as any), ...(qq ?? {}) }
|
|
889
898
|
const groupNative: any = { ...(advanced ?? {}) }
|
|
890
899
|
for (const key of QQ_KEYS) if (legacy[key] !== undefined) groupQq[key] = legacy[key]
|
|
891
900
|
for (const key of NATIVE_KEYS) if (legacy[key] !== undefined) groupNative[key] = legacy[key]
|
|
@@ -940,7 +949,18 @@ export async function apply (ctx: Context, rawConfig: Config) {
|
|
|
940
949
|
|
|
941
950
|
bindRuntime({
|
|
942
951
|
ctx,
|
|
952
|
+
/**
|
|
953
|
+
* ⚠️ 这份 config **不是**原封不动转发,而是运行时真正读的那一份(`tryGetRuntime().config`)。
|
|
954
|
+
*
|
|
955
|
+
* 以前它是一个**手写白名单** —— 于是每加一个新开关(例如「强制在线播放的适配器」
|
|
956
|
+
* `forceOnlinePlayer`),忘了往这里补一行,运行时就读到 undefined,功能整个静默失效:
|
|
957
|
+
* 线上真实故障就是「B站私聊该给在线播放链接,结果还是去发视频文件」。
|
|
958
|
+
*
|
|
959
|
+
* 现在改成 `...readQqOptions(config)` 打底(qqFields.json 里的字段一个不落,缺的用默认值),
|
|
960
|
+
* 下面那些需要**归一化**的字段(端口取整数、分钟数夹范围、布尔用 !== false 这种)再覆盖上去。
|
|
961
|
+
*/
|
|
943
962
|
config: {
|
|
963
|
+
...readQqOptions(config),
|
|
944
964
|
masters: config.masters ?? [],
|
|
945
965
|
debug: config.debug,
|
|
946
966
|
dataPath: config.dataPath,
|
|
@@ -35,9 +35,30 @@ function platformOf (e: any): string {
|
|
|
35
35
|
return String(e?.bot?.bot?.platform ?? e?.bot?.platform ?? '')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* OneBot 系适配器(NapCat / Lagrange / go-cqhttp / Chronocat…):它们真的有「聊天记录」这个概念。
|
|
40
|
+
*
|
|
41
|
+
* 注意这份名单是**白名单**:compat 那边的 isForwardSupported 是黑名单(只排除 QQ 官方),
|
|
42
|
+
* 于是**任何自定义适配器都会被当成支持** —— 线上真实故障就是这样:
|
|
43
|
+
* B站私聊适配器(koishi-plugin-adapter-bilibili-dm,platform 就是 bilibili)被认成支持合并转发,
|
|
44
|
+
* 结果整条转发发出去「没有拿到消息 ID」(适配器根本没有聊天记录这种东西),
|
|
45
|
+
* 连退回来的逐条发送也失败了 —— 用户什么都没收到。
|
|
46
|
+
*/
|
|
47
|
+
const ONEBOT_LIKE = /onebot|napcat|lagrange|go-?cqhttp|chronocat|mirai/i
|
|
48
|
+
|
|
49
|
+
/** 这台部署的适配器支不支持合并转发(**只有明确支持才算支持**) */
|
|
39
50
|
export function canForwardParseResult (e: any): boolean {
|
|
40
|
-
|
|
51
|
+
const raw: any = e?.bot?.bot ?? e?.bot
|
|
52
|
+
/**
|
|
53
|
+
* ① 适配器自己带合并转发 API(OneBot 系的 sendGroupForwardMsg / sendPrivateForwardMsg)
|
|
54
|
+
* —— 这条最可靠,不依赖平台名怎么写。
|
|
55
|
+
*/
|
|
56
|
+
const internal: any = raw?.internal ?? e?.bot?.internal
|
|
57
|
+
if (internal && (typeof internal.sendGroupForwardMsg === 'function' || typeof internal.sendPrivateForwardMsg === 'function')) return true
|
|
58
|
+
/** ② 其余只认 OneBot 系的平台名 */
|
|
59
|
+
const platform = platformOf(e)
|
|
60
|
+
if (!platform) return false
|
|
61
|
+
return ONEBOT_LIKE.test(platform)
|
|
41
62
|
}
|
|
42
63
|
|
|
43
64
|
/** 平台名 → 配置段名(合并转发的平台开关就写在各自的平台段里) */
|
|
@@ -302,7 +323,12 @@ export function withParseForward<F extends (e: any, next?: any) => any> (handler
|
|
|
302
323
|
+ '.forward 都没打开),按逐条发送处理')
|
|
303
324
|
return await handler(e, next)
|
|
304
325
|
}
|
|
305
|
-
if (!parseForwardPeer(e)
|
|
326
|
+
if (!parseForwardPeer(e)) return await handler(e, next)
|
|
327
|
+
if (!canForwardParseResult(e)) {
|
|
328
|
+
// 说明白为什么没合并:以前这里静默退化,用户只看到「转发了但没收到」
|
|
329
|
+
logger.mark('[合并转发] 适配器 ' + (platformOf(e) || '未知') + ' 不支持合并转发(不是 OneBot 系、也没有 forward API),本次逐条发送')
|
|
330
|
+
return await handler(e, next)
|
|
331
|
+
}
|
|
306
332
|
return await runWithForwardBag(parseForwardPeer(e), async () => {
|
|
307
333
|
try {
|
|
308
334
|
return await handler(e, next)
|
package/src/player/index.ts
CHANGED
|
@@ -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)
|
|
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) {
|
package/src/player/server.ts
CHANGED
|
@@ -23,6 +23,8 @@ import path from 'node:path'
|
|
|
23
23
|
|
|
24
24
|
import { logger } from 'node-karin'
|
|
25
25
|
|
|
26
|
+
import { tryGetRuntime } from '../../compat/runtime'
|
|
27
|
+
|
|
26
28
|
import { renderExpiredPage, renderPlayerPage } from './page'
|
|
27
29
|
import {
|
|
28
30
|
getPlayerSession,
|
|
@@ -58,6 +60,21 @@ export interface PlayerHttpResponse {
|
|
|
58
60
|
/** 路由前缀 */
|
|
59
61
|
export const PLAYER_ROUTE_PREFIX = '/kkk/player/'
|
|
60
62
|
|
|
63
|
+
/** 独立端口是否真的监听上了:没监听上时链接指向的公网域名很可能打不到本实例 */
|
|
64
|
+
let standaloneReady = false
|
|
65
|
+
|
|
66
|
+
/** 查询独立端口是否监听上了(给「生成链接」那边做提示用) */
|
|
67
|
+
export const isStandalonePlayerReady = (): boolean => standaloneReady
|
|
68
|
+
|
|
69
|
+
/** 面板里配的「播放器公网地址」(没配就是空串) */
|
|
70
|
+
function publicBaseUrl (): string {
|
|
71
|
+
try {
|
|
72
|
+
return String((tryGetRuntime()?.config as any)?.playerBaseUrl ?? '').trim()
|
|
73
|
+
} catch {
|
|
74
|
+
return ''
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
const TEXT_TYPE = 'text/plain; charset=utf-8'
|
|
62
79
|
const HTML_TYPE = 'text/html; charset=utf-8'
|
|
63
80
|
|
|
@@ -387,13 +404,23 @@ export function registerPlayerRoutes ({ ctx, port }: { ctx: any, port: number })
|
|
|
387
404
|
})
|
|
388
405
|
})
|
|
389
406
|
server.on('error', (error: any) => {
|
|
390
|
-
|
|
391
|
-
|
|
407
|
+
const code = String(error?.code ?? error?.message ?? error)
|
|
408
|
+
/**
|
|
409
|
+
* 端口被别的进程占了:**必须说清楚后果** —— 线上真实故障就是「用户点开播放链接看到『链接已过期』」。
|
|
410
|
+
*
|
|
411
|
+
* 成因:这台实例退回 Koishi 自己的端口,而另一台实例(比如同机的测试实例)抢到了这个端口;
|
|
412
|
+
* 域名反代到该端口 → 用户访问的其实是**另一台实例**,它当然没有这个会话。
|
|
413
|
+
*/
|
|
414
|
+
logger.error('[在线播放] 播放器端口 ' + port + ' 被占用(' + code + '),本次退回 Koishi 自己的端口'
|
|
415
|
+
+ (publicBaseUrl() ? '。注意:域名 ' + publicBaseUrl() + ' 反代的就是 ' + port + ' 的话,用户点开链接会看到「链接已过期」'
|
|
416
|
+
+ ' —— 说明这台实例不是 8888 的占用者(同机另一个 Koishi 实例抢到了),换一个端口或先停掉那台实例' : ''))
|
|
417
|
+
standaloneReady = false
|
|
392
418
|
// 端口被占用不能把插件带崩:退回 ctx.server 再挂一遍
|
|
393
419
|
registerOnKoishi(ctx, disposers)
|
|
394
420
|
})
|
|
395
421
|
try {
|
|
396
422
|
server.listen(port, () => {
|
|
423
|
+
standaloneReady = true
|
|
397
424
|
logger.info('[在线播放] 播放器已监听独立端口 ' + port)
|
|
398
425
|
})
|
|
399
426
|
disposers.push(() => { try { server.close() } catch { /* 忽略 */ } })
|