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 CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  # koishi-plugin-maichuni-scorehelper
3
3
 
4
- 一个基础的 Koishi 插件,未来用于舞萌中二节奏分数辅助。
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.PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
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.PAGE_URL, {
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(this.API_URL);
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 (this.ctx.puppeteer && snippet.includes('<!DOCTYPE')) {
307
- let page = null;
308
- try {
309
- page = await this.ctx.puppeteer.page();
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-maichuni-scorehelper",
3
- "version": "0.0.15-test",
4
- "description": "一个基础的 Koishi 插件,未来用于舞萌中二节奏分数辅助。",
3
+ "version": "0.0.17-test",
4
+ "description": "【未完成】一些舞萌中二节奏功能",
5
5
  "keywords": [
6
6
  "koishi",
7
7
  "plugin",
@@ -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.PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 15000 })
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.PAGE_URL, {
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(this.API_URL)
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 (this.ctx.puppeteer && snippet.includes('<!DOCTYPE')) {
365
- let page: any = null
366
- try {
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 推送