koishi-plugin-maichuni-scorehelper 0.0.15-test → 0.0.17-test
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/README.md +2 -3
- package/dist/services/maimai-status.d.ts +2 -2
- package/dist/services/maimai-status.js +61 -41
- package/package.json +2 -2
- package/src/services/maimai-status.ts +56 -34
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# koishi-plugin-maichuni-scorehelper
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
【未完成】用于舞萌中二节奏分数辅助。
|
|
5
5
|
|
|
6
6
|
## 使用方法
|
|
7
7
|
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
```
|
|
16
16
|
3. 在 Koishi 配置中引入本插件。
|
|
17
17
|
|
|
18
|
-
> 提示:舞萌状态监控默认使用反向代理域名 `maimaistatusreverseproxy.muxyang.com` 以绕过目标站点的 CC 防护。
|
|
19
18
|
|
|
20
19
|
## 目录结构
|
|
21
20
|
- src/ 插件源码
|
|
@@ -26,4 +25,4 @@
|
|
|
26
25
|
- typescript@^5.0.0
|
|
27
26
|
|
|
28
27
|
---
|
|
29
|
-
|
|
28
|
+
|
|
@@ -5,6 +5,8 @@ export interface Config {
|
|
|
5
5
|
targetChannelId?: string;
|
|
6
6
|
showRecovery: boolean;
|
|
7
7
|
mainServiceName: string;
|
|
8
|
+
statusPageUrl: string;
|
|
9
|
+
heartbeatApiUrl: string;
|
|
8
10
|
}
|
|
9
11
|
export declare const Config: Schema<Config>;
|
|
10
12
|
declare module 'koishi' {
|
|
@@ -18,8 +20,6 @@ export declare class MaimaiStatus extends Service {
|
|
|
18
20
|
private clientLogger;
|
|
19
21
|
private timer;
|
|
20
22
|
private groups;
|
|
21
|
-
private readonly PAGE_URL;
|
|
22
|
-
private readonly API_URL;
|
|
23
23
|
private readonly CACHE_FILE;
|
|
24
24
|
private readonly UA;
|
|
25
25
|
constructor(ctx: Context, config: Config);
|
|
@@ -7,12 +7,16 @@ exports.MaimaiStatus = exports.Config = exports.name = void 0;
|
|
|
7
7
|
const koishi_1 = require("koishi");
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const DEFAULT_STATUS_PAGE_URL = 'https://maimaistatusreverseproxy.muxyang.com/status/maimai';
|
|
11
|
+
const DEFAULT_HEARTBEAT_API_URL = 'https://maimaistatusreverseproxy.muxyang.com/api/status-page/heartbeat/maimai';
|
|
10
12
|
exports.name = 'maimai-status-monitor';
|
|
11
13
|
exports.Config = koishi_1.Schema.object({
|
|
12
14
|
interval: koishi_1.Schema.number().default(60000).description('轮询间隔 (毫秒),建议不低于 30000'),
|
|
13
15
|
targetChannelId: koishi_1.Schema.string().description('推送通知的目标群组/频道 ID (可选,留空则仅在控制台输出)'),
|
|
14
16
|
showRecovery: koishi_1.Schema.boolean().default(true).description('是否在服务恢复时也发送通知'),
|
|
15
|
-
mainServiceName: koishi_1.Schema.string().default('舞萌DX服务').description('主服务名称(去除了后缀的),当该服务下线时,屏蔽其他服务的通知')
|
|
17
|
+
mainServiceName: koishi_1.Schema.string().default('舞萌DX服务').description('主服务名称(去除了后缀的),当该服务下线时,屏蔽其他服务的通知'),
|
|
18
|
+
statusPageUrl: koishi_1.Schema.string().default(DEFAULT_STATUS_PAGE_URL).description('状态页 URL,默认使用内置反代,可自定义'),
|
|
19
|
+
heartbeatApiUrl: koishi_1.Schema.string().default(DEFAULT_HEARTBEAT_API_URL).description('心跳 API URL,默认使用内置反代,可自定义')
|
|
16
20
|
});
|
|
17
21
|
// 常量配置
|
|
18
22
|
const FLAP_WINDOW = 10 * 60 * 1000; // 10分钟
|
|
@@ -27,8 +31,6 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
27
31
|
// 内存中保存的分组状态映射: GroupName -> GroupData
|
|
28
32
|
this.groups = new Map();
|
|
29
33
|
// API 地址常量(已切换到反向代理域名以绕过 CC)
|
|
30
|
-
this.PAGE_URL = 'https://maimaistatusreverseproxy.muxyang.com/status/maimai';
|
|
31
|
-
this.API_URL = 'https://maimaistatusreverseproxy.muxyang.com/api/status-page/heartbeat/maimai';
|
|
32
34
|
this.CACHE_FILE = path_1.default.join(__dirname, '../../monitor_cache.json');
|
|
33
35
|
this.UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
34
36
|
this.clientLogger = ctx.logger('maimai-status');
|
|
@@ -192,7 +194,7 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
192
194
|
const page = await this.ctx.puppeteer.page();
|
|
193
195
|
try {
|
|
194
196
|
// 设置超时,防止卡死
|
|
195
|
-
await page.goto(this.
|
|
197
|
+
await page.goto(this.config.statusPageUrl || DEFAULT_STATUS_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
196
198
|
// 等待可能的 JS 跳转 (CC Protect)
|
|
197
199
|
try {
|
|
198
200
|
await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 5000 });
|
|
@@ -212,7 +214,7 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
212
214
|
// 如果 Puppeteer 失败或未启用,回退到 HTTP (可能无法通过 CC 验证)
|
|
213
215
|
if (!data) {
|
|
214
216
|
try {
|
|
215
|
-
const html = await this.ctx.http.get(this.
|
|
217
|
+
const html = await this.ctx.http.get(this.config.statusPageUrl || DEFAULT_STATUS_PAGE_URL, {
|
|
216
218
|
responseType: 'text',
|
|
217
219
|
headers: { 'User-Agent': this.UA }
|
|
218
220
|
});
|
|
@@ -292,8 +294,48 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
292
294
|
* 步骤3: 获取 API JSON 数据
|
|
293
295
|
*/
|
|
294
296
|
async fetchHeartbeats() {
|
|
297
|
+
const url = this.config.heartbeatApiUrl || DEFAULT_HEARTBEAT_API_URL;
|
|
298
|
+
// 使用 Puppeteer 绕过可能的 JS 质询 / CC 防护
|
|
299
|
+
const fetchViaPuppeteer = async () => {
|
|
300
|
+
if (!this.ctx.puppeteer)
|
|
301
|
+
return null;
|
|
302
|
+
let page = null;
|
|
303
|
+
try {
|
|
304
|
+
page = await this.ctx.puppeteer.page();
|
|
305
|
+
if (page.setUserAgent) {
|
|
306
|
+
await page.setUserAgent(this.UA);
|
|
307
|
+
}
|
|
308
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 20000 });
|
|
309
|
+
// 直接读取正文文本(可能被 JS 动态生成)
|
|
310
|
+
const body = await page.evaluate(() => document.body?.innerText || document.body?.textContent || '');
|
|
311
|
+
if (!body)
|
|
312
|
+
return null;
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(body);
|
|
315
|
+
}
|
|
316
|
+
catch (err2) {
|
|
317
|
+
this.clientLogger.warn(`Puppeteer heartbeat parse failed: ${err2.message}; body=${body.slice(0, 200)}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (pe) {
|
|
322
|
+
this.clientLogger.warn(`Puppeteer heartbeat fetch failed: ${pe.message}`);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
if (page) {
|
|
327
|
+
try {
|
|
328
|
+
await page.close();
|
|
329
|
+
}
|
|
330
|
+
catch { /* ignore */ }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
295
334
|
try {
|
|
296
|
-
let data = await this.ctx.http.get(
|
|
335
|
+
let data = await this.ctx.http.get(url, {
|
|
336
|
+
headers: { 'User-Agent': this.UA },
|
|
337
|
+
timeout: 15000
|
|
338
|
+
});
|
|
297
339
|
// 如果不是对象,尝试解析 JSON (因为 Object.keys 打印出了索引,说明是字符串)
|
|
298
340
|
if (typeof data === 'string') {
|
|
299
341
|
try {
|
|
@@ -303,48 +345,23 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
303
345
|
const snippet = data.slice(0, 200);
|
|
304
346
|
this.clientLogger.warn(`Heartbeat JSON parse failed: ${err.message}; snippet=${snippet}`);
|
|
305
347
|
// 如果返回的是 CC HTML,尝试使用 Puppeteer 绕过
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (page.setUserAgent) {
|
|
311
|
-
await page.setUserAgent(this.UA);
|
|
312
|
-
}
|
|
313
|
-
await page.goto(this.API_URL, { waitUntil: 'networkidle0', timeout: 15000 });
|
|
314
|
-
const body = await page.evaluate(() => {
|
|
315
|
-
const text = document.body?.innerText || document.body?.textContent || '';
|
|
316
|
-
return text;
|
|
317
|
-
});
|
|
318
|
-
try {
|
|
319
|
-
data = JSON.parse(body);
|
|
320
|
-
}
|
|
321
|
-
catch (err2) {
|
|
322
|
-
this.clientLogger.warn(`Puppeteer heartbeat parse failed: ${err2.message}; body=${body.slice(0, 200)}`);
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch (pe) {
|
|
327
|
-
this.clientLogger.warn(`Puppeteer heartbeat fetch failed: ${pe.message}`);
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
finally {
|
|
331
|
-
if (page) {
|
|
332
|
-
try {
|
|
333
|
-
await page.close();
|
|
334
|
-
}
|
|
335
|
-
catch { /* ignore */ }
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
return null;
|
|
348
|
+
if (snippet.includes('<!DOCTYPE')) {
|
|
349
|
+
const viaPuppeteer = await fetchViaPuppeteer();
|
|
350
|
+
if (viaPuppeteer)
|
|
351
|
+
return viaPuppeteer;
|
|
341
352
|
}
|
|
353
|
+
return null;
|
|
342
354
|
}
|
|
343
355
|
}
|
|
344
356
|
// 返回原始对象,包含 heartbeatList 和 uptimeList
|
|
345
357
|
return data || {};
|
|
346
358
|
}
|
|
347
359
|
catch (e) {
|
|
360
|
+
// HTTP 失败也尝试 Puppeteer(例如超时/质询)
|
|
361
|
+
this.clientLogger.warn(`HTTP heartbeat fetch failed, trying Puppeteer. reason=${e?.message || e}`);
|
|
362
|
+
const viaPuppeteer = await fetchViaPuppeteer();
|
|
363
|
+
if (viaPuppeteer)
|
|
364
|
+
return viaPuppeteer;
|
|
348
365
|
this.clientLogger.error('Failed to fetch heartbeat JSON', e);
|
|
349
366
|
return null;
|
|
350
367
|
}
|
|
@@ -542,6 +559,9 @@ class MaimaiStatus extends koishi_1.Service {
|
|
|
542
559
|
this.clientLogger.error('Failed to send notification:', e);
|
|
543
560
|
}
|
|
544
561
|
}
|
|
562
|
+
else {
|
|
563
|
+
this.clientLogger.debug('Notification skipped: targetChannelId not configured.');
|
|
564
|
+
}
|
|
545
565
|
// 示例:您也可以在这里添加 HTTP webhook 推送
|
|
546
566
|
// await this.ctx.http.post('YOUR_WEBHOOK_URL', { content: message })
|
|
547
567
|
}
|
package/package.json
CHANGED
|
@@ -2,6 +2,9 @@ import { Context, Schema, Logger, Service } from 'koishi'
|
|
|
2
2
|
import fs from 'fs/promises'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
|
|
5
|
+
const DEFAULT_STATUS_PAGE_URL = 'https://maimaistatusreverseproxy.muxyang.com/status/maimai'
|
|
6
|
+
const DEFAULT_HEARTBEAT_API_URL = 'https://maimaistatusreverseproxy.muxyang.com/api/status-page/heartbeat/maimai'
|
|
7
|
+
|
|
5
8
|
export const name = 'maimai-status-monitor'
|
|
6
9
|
|
|
7
10
|
export interface Config {
|
|
@@ -9,13 +12,17 @@ export interface Config {
|
|
|
9
12
|
targetChannelId?: string
|
|
10
13
|
showRecovery: boolean
|
|
11
14
|
mainServiceName: string
|
|
15
|
+
statusPageUrl: string
|
|
16
|
+
heartbeatApiUrl: string
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export const Config: Schema<Config> = Schema.object({
|
|
15
20
|
interval: Schema.number().default(60000).description('轮询间隔 (毫秒),建议不低于 30000'),
|
|
16
21
|
targetChannelId: Schema.string().description('推送通知的目标群组/频道 ID (可选,留空则仅在控制台输出)'),
|
|
17
22
|
showRecovery: Schema.boolean().default(true).description('是否在服务恢复时也发送通知'),
|
|
18
|
-
mainServiceName: Schema.string().default('舞萌DX服务').description('主服务名称(去除了后缀的),当该服务下线时,屏蔽其他服务的通知')
|
|
23
|
+
mainServiceName: Schema.string().default('舞萌DX服务').description('主服务名称(去除了后缀的),当该服务下线时,屏蔽其他服务的通知'),
|
|
24
|
+
statusPageUrl: Schema.string().default(DEFAULT_STATUS_PAGE_URL).description('状态页 URL,默认使用内置反代,可自定义'),
|
|
25
|
+
heartbeatApiUrl: Schema.string().default(DEFAULT_HEARTBEAT_API_URL).description('心跳 API URL,默认使用内置反代,可自定义')
|
|
19
26
|
})
|
|
20
27
|
|
|
21
28
|
// 定义服务状态枚举
|
|
@@ -61,8 +68,6 @@ export class MaimaiStatus extends Service {
|
|
|
61
68
|
private groups: Map<string, ServiceGroup> = new Map()
|
|
62
69
|
|
|
63
70
|
// API 地址常量(已切换到反向代理域名以绕过 CC)
|
|
64
|
-
private readonly PAGE_URL = 'https://maimaistatusreverseproxy.muxyang.com/status/maimai'
|
|
65
|
-
private readonly API_URL = 'https://maimaistatusreverseproxy.muxyang.com/api/status-page/heartbeat/maimai'
|
|
66
71
|
private readonly CACHE_FILE = path.join(__dirname, '../../monitor_cache.json')
|
|
67
72
|
private readonly UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
68
73
|
|
|
@@ -244,7 +249,7 @@ export class MaimaiStatus extends Service {
|
|
|
244
249
|
const page = await this.ctx.puppeteer.page()
|
|
245
250
|
try {
|
|
246
251
|
// 设置超时,防止卡死
|
|
247
|
-
await page.goto(this.
|
|
252
|
+
await page.goto(this.config.statusPageUrl || DEFAULT_STATUS_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 })
|
|
248
253
|
// 等待可能的 JS 跳转 (CC Protect)
|
|
249
254
|
try {
|
|
250
255
|
await page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 5000 })
|
|
@@ -263,7 +268,7 @@ export class MaimaiStatus extends Service {
|
|
|
263
268
|
// 如果 Puppeteer 失败或未启用,回退到 HTTP (可能无法通过 CC 验证)
|
|
264
269
|
if (!data) {
|
|
265
270
|
try {
|
|
266
|
-
const html = await this.ctx.http.get(this.
|
|
271
|
+
const html = await this.ctx.http.get(this.config.statusPageUrl || DEFAULT_STATUS_PAGE_URL, {
|
|
267
272
|
responseType: 'text',
|
|
268
273
|
headers: { 'User-Agent': this.UA }
|
|
269
274
|
})
|
|
@@ -350,8 +355,42 @@ export class MaimaiStatus extends Service {
|
|
|
350
355
|
* 步骤3: 获取 API JSON 数据
|
|
351
356
|
*/
|
|
352
357
|
private async fetchHeartbeats(): Promise<any> {
|
|
358
|
+
const url = this.config.heartbeatApiUrl || DEFAULT_HEARTBEAT_API_URL
|
|
359
|
+
|
|
360
|
+
// 使用 Puppeteer 绕过可能的 JS 质询 / CC 防护
|
|
361
|
+
const fetchViaPuppeteer = async (): Promise<any | null> => {
|
|
362
|
+
if (!this.ctx.puppeteer) return null
|
|
363
|
+
let page: any = null
|
|
364
|
+
try {
|
|
365
|
+
page = await this.ctx.puppeteer.page()
|
|
366
|
+
if (page.setUserAgent) {
|
|
367
|
+
await page.setUserAgent(this.UA)
|
|
368
|
+
}
|
|
369
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 20000 })
|
|
370
|
+
// 直接读取正文文本(可能被 JS 动态生成)
|
|
371
|
+
const body = await page.evaluate(() => document.body?.innerText || document.body?.textContent || '')
|
|
372
|
+
if (!body) return null
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(body)
|
|
375
|
+
} catch (err2) {
|
|
376
|
+
this.clientLogger.warn(`Puppeteer heartbeat parse failed: ${(err2 as any).message}; body=${body.slice(0, 200)}`)
|
|
377
|
+
return null
|
|
378
|
+
}
|
|
379
|
+
} catch (pe) {
|
|
380
|
+
this.clientLogger.warn(`Puppeteer heartbeat fetch failed: ${(pe as any).message}`)
|
|
381
|
+
return null
|
|
382
|
+
} finally {
|
|
383
|
+
if (page) {
|
|
384
|
+
try { await page.close() } catch { /* ignore */ }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
353
389
|
try {
|
|
354
|
-
let data = await this.ctx.http.get(
|
|
390
|
+
let data = await this.ctx.http.get(url, {
|
|
391
|
+
headers: { 'User-Agent': this.UA },
|
|
392
|
+
timeout: 15000
|
|
393
|
+
})
|
|
355
394
|
|
|
356
395
|
// 如果不是对象,尝试解析 JSON (因为 Object.keys 打印出了索引,说明是字符串)
|
|
357
396
|
if (typeof data === 'string') {
|
|
@@ -361,41 +400,22 @@ export class MaimaiStatus extends Service {
|
|
|
361
400
|
const snippet = data.slice(0, 200)
|
|
362
401
|
this.clientLogger.warn(`Heartbeat JSON parse failed: ${(err as any).message}; snippet=${snippet}`)
|
|
363
402
|
// 如果返回的是 CC HTML,尝试使用 Puppeteer 绕过
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
page = await this.ctx.puppeteer.page()
|
|
368
|
-
if (page.setUserAgent) {
|
|
369
|
-
await page.setUserAgent(this.UA)
|
|
370
|
-
}
|
|
371
|
-
await page.goto(this.API_URL, { waitUntil: 'networkidle0', timeout: 15000 })
|
|
372
|
-
const body = await page.evaluate(() => {
|
|
373
|
-
const text = document.body?.innerText || document.body?.textContent || ''
|
|
374
|
-
return text
|
|
375
|
-
})
|
|
376
|
-
try {
|
|
377
|
-
data = JSON.parse(body)
|
|
378
|
-
} catch (err2) {
|
|
379
|
-
this.clientLogger.warn(`Puppeteer heartbeat parse failed: ${(err2 as any).message}; body=${body.slice(0,200)}`)
|
|
380
|
-
return null
|
|
381
|
-
}
|
|
382
|
-
} catch (pe) {
|
|
383
|
-
this.clientLogger.warn(`Puppeteer heartbeat fetch failed: ${(pe as any).message}`)
|
|
384
|
-
return null
|
|
385
|
-
} finally {
|
|
386
|
-
if (page) {
|
|
387
|
-
try { await page.close() } catch { /* ignore */ }
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
return null
|
|
403
|
+
if (snippet.includes('<!DOCTYPE')) {
|
|
404
|
+
const viaPuppeteer = await fetchViaPuppeteer()
|
|
405
|
+
if (viaPuppeteer) return viaPuppeteer
|
|
392
406
|
}
|
|
407
|
+
return null
|
|
393
408
|
}
|
|
394
409
|
}
|
|
395
410
|
|
|
396
411
|
// 返回原始对象,包含 heartbeatList 和 uptimeList
|
|
397
412
|
return data || {}
|
|
398
413
|
} catch (e) {
|
|
414
|
+
// HTTP 失败也尝试 Puppeteer(例如超时/质询)
|
|
415
|
+
this.clientLogger.warn(`HTTP heartbeat fetch failed, trying Puppeteer. reason=${(e as any)?.message || e}`)
|
|
416
|
+
const viaPuppeteer = await fetchViaPuppeteer()
|
|
417
|
+
if (viaPuppeteer) return viaPuppeteer
|
|
418
|
+
|
|
399
419
|
this.clientLogger.error('Failed to fetch heartbeat JSON', e)
|
|
400
420
|
return null
|
|
401
421
|
}
|
|
@@ -614,6 +634,8 @@ export class MaimaiStatus extends Service {
|
|
|
614
634
|
} catch (e) {
|
|
615
635
|
this.clientLogger.error('Failed to send notification:', e)
|
|
616
636
|
}
|
|
637
|
+
} else {
|
|
638
|
+
this.clientLogger.debug('Notification skipped: targetChannelId not configured.')
|
|
617
639
|
}
|
|
618
640
|
|
|
619
641
|
// 示例:您也可以在这里添加 HTTP webhook 推送
|