koishi-plugin-cfmrmod 1.0.8 → 1.1.0
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/dist/notify.js +97 -9
- package/package.json +7 -4
package/dist/notify.js
CHANGED
|
@@ -11,6 +11,7 @@ const cfmr_1 = require("./cfmr");
|
|
|
11
11
|
const fetch = require('node-fetch');
|
|
12
12
|
const MR_BASE = 'https://api.modrinth.com/v2';
|
|
13
13
|
const CF_MIRROR_BASE = 'https://api.curse.tools/v1/cf';
|
|
14
|
+
const CF_OFFICIAL_BASE = 'https://api.curseforge.com/v1';
|
|
14
15
|
function normalizePlatform(platform) {
|
|
15
16
|
if (platform === 'mr' || platform === 'cf')
|
|
16
17
|
return platform;
|
|
@@ -388,9 +389,53 @@ function apply(ctx, config, options) {
|
|
|
388
389
|
};
|
|
389
390
|
}
|
|
390
391
|
async function getLatestCurseForge(projectId, timeout) {
|
|
391
|
-
var _a;
|
|
392
|
-
|
|
393
|
-
const
|
|
392
|
+
var _a, _b, _c;
|
|
393
|
+
// 优先使用官方 API(如果配置了 API Key),否则回退到镜像
|
|
394
|
+
const apiKey = (_a = options === null || options === void 0 ? void 0 : options.cfmr) === null || _a === void 0 ? void 0 : _a.curseforgeApiKey;
|
|
395
|
+
if (apiKey && String(apiKey).trim()) {
|
|
396
|
+
// 使用官方 API
|
|
397
|
+
try {
|
|
398
|
+
const controller = new AbortController();
|
|
399
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
400
|
+
try {
|
|
401
|
+
// 官方 API 可以使用分页参数
|
|
402
|
+
const res = await fetch(`${CF_OFFICIAL_BASE}/mods/${projectId}/files?index=0&pageSize=1`, {
|
|
403
|
+
headers: {
|
|
404
|
+
'Accept': 'application/json',
|
|
405
|
+
'x-api-key': String(apiKey).trim(),
|
|
406
|
+
},
|
|
407
|
+
signal: controller.signal,
|
|
408
|
+
});
|
|
409
|
+
if (!res.ok)
|
|
410
|
+
throw new Error(`HTTP ${res.status}`);
|
|
411
|
+
const files = await res.json();
|
|
412
|
+
const latest = (_b = files === null || files === void 0 ? void 0 : files.data) === null || _b === void 0 ? void 0 : _b[0];
|
|
413
|
+
if (!latest)
|
|
414
|
+
return null;
|
|
415
|
+
return {
|
|
416
|
+
versionId: String(latest.id),
|
|
417
|
+
version: latest.displayName || latest.fileName || String(latest.id),
|
|
418
|
+
changelog: latest.changelog || '',
|
|
419
|
+
downloads: latest.downloadCount,
|
|
420
|
+
datePublished: latest.fileDate || null,
|
|
421
|
+
releaseType: latest.releaseType,
|
|
422
|
+
loaders: Array.isArray(latest.gameVersions) ? latest.gameVersions.filter((v) => /forge|fabric|quilt|neoforge/i.test(String(v))) : [],
|
|
423
|
+
gameVersions: Array.isArray(latest.gameVersions) ? latest.gameVersions.filter((v) => /\d/.test(String(v))) : [],
|
|
424
|
+
fileName: latest.fileName || '',
|
|
425
|
+
fileSize: latest.fileLength || 0,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
clearTimeout(id);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
logger.warn(`[getLatestCurseForge] 官方 API 请求失败,回退到镜像: ${e.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// 回退到镜像 API - 不使用分页参数,避免缓存问题
|
|
437
|
+
const files = await fetchJson(`${CF_MIRROR_BASE}/mods/${projectId}/files`, timeout);
|
|
438
|
+
const latest = (_c = files === null || files === void 0 ? void 0 : files.data) === null || _c === void 0 ? void 0 : _c[0];
|
|
394
439
|
if (!latest)
|
|
395
440
|
return null;
|
|
396
441
|
return {
|
|
@@ -422,7 +467,6 @@ function apply(ctx, config, options) {
|
|
|
422
467
|
const src = await toImageSrc(buf);
|
|
423
468
|
await sendToChannel(channelId, koishi_1.h.image(src));
|
|
424
469
|
}
|
|
425
|
-
// 仅发送卡片,不发送文字
|
|
426
470
|
}
|
|
427
471
|
catch (e) {
|
|
428
472
|
logger.warn(`发送通知失败(${platform}:${projectId}): ${e.message}`);
|
|
@@ -439,7 +483,8 @@ function apply(ctx, config, options) {
|
|
|
439
483
|
try {
|
|
440
484
|
const key = `${sub.channelId}|${sub.platform}|${sub.projectId}`;
|
|
441
485
|
const lastCheck = lastCheckMap.get(key) || 0;
|
|
442
|
-
|
|
486
|
+
const elapsed = Date.now() - lastCheck;
|
|
487
|
+
if (!force && elapsed < sub.interval) {
|
|
443
488
|
stats.skipped += 1;
|
|
444
489
|
continue;
|
|
445
490
|
}
|
|
@@ -513,10 +558,53 @@ function apply(ctx, config, options) {
|
|
|
513
558
|
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
514
559
|
return { sent: true, updated: true };
|
|
515
560
|
};
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
561
|
+
// 自动检查更新定时器
|
|
562
|
+
const startAutoCheck = async () => {
|
|
563
|
+
// 首先加载配置文件
|
|
564
|
+
await loadConfigFromFile();
|
|
565
|
+
// 计算轮询间隔:取所有订阅中最小的 interval,最小不低于 1 分钟
|
|
566
|
+
const getMinInterval = () => {
|
|
567
|
+
const subs = getConfigSubs();
|
|
568
|
+
if (!subs.length)
|
|
569
|
+
return Number(config.interval) || 30 * 60 * 1000;
|
|
570
|
+
const intervals = subs.map(s => s.interval);
|
|
571
|
+
return Math.min(...intervals);
|
|
572
|
+
};
|
|
573
|
+
// 使用较短的基准轮询间隔(1分钟),让 checkOnce 内部判断每个订阅是否到期
|
|
574
|
+
// 这样可以支持每个订阅的独立 interval
|
|
575
|
+
const baseTick = 60 * 1000; // 1 分钟基准轮询
|
|
576
|
+
// 自动轮询函数:每次都检查 config.enabled
|
|
577
|
+
const autoCheckLoop = async () => {
|
|
578
|
+
try {
|
|
579
|
+
await loadConfigFromFile();
|
|
580
|
+
if (config.enabled) {
|
|
581
|
+
const subs = getConfigSubs();
|
|
582
|
+
if (subs.length > 0) {
|
|
583
|
+
logger.debug(`自动检查更新开始,共 ${subs.length} 个订阅...`);
|
|
584
|
+
const stats = await checkOnce();
|
|
585
|
+
if (stats) {
|
|
586
|
+
logger.debug(`自动检查完成: checked=${stats.checked}, updated=${stats.updated}, skipped=${stats.skipped}, failed=${stats.failed}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
logger.warn(`自动检查更新失败: ${e.message}`);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
// 启动时延迟执行一次初始检查(给 bot 连接时间)
|
|
596
|
+
ctx.setTimeout(async () => {
|
|
597
|
+
await autoCheckLoop();
|
|
598
|
+
}, 10 * 1000);
|
|
599
|
+
// 设置定时器,每分钟轮询一次,由 checkOnce 内部判断哪些订阅到期
|
|
600
|
+
ctx.setInterval(autoCheckLoop, baseTick);
|
|
601
|
+
const minInterval = getMinInterval();
|
|
602
|
+
logger.info(`自动更新检查已启动,基准轮询间隔: 1 分钟,最短订阅间隔: ${Math.round(minInterval / 60000)} 分钟`);
|
|
603
|
+
};
|
|
604
|
+
// 使用 ctx.on('ready') 确保在 Koishi 完全就绪后启动
|
|
605
|
+
ctx.on('ready', () => {
|
|
606
|
+
startAutoCheck().catch(e => logger.warn(`启动自动检查失败: ${e.message}`));
|
|
607
|
+
});
|
|
520
608
|
ctx.command('notify.add <platform> <projectId>', '添加更新订阅')
|
|
521
609
|
.action(async ({ session }, platform, projectId) => {
|
|
522
610
|
await loadConfigFromFile();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cfmrmod",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"pub": "npm run build && npm publish --access public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"koishi": "^
|
|
43
|
-
"
|
|
42
|
+
"@ltxhhz/koishi-plugin-skia-canvas": "^0.0.10",
|
|
43
|
+
"koishi": "^4.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"cheerio": "^1.0.0-rc.12",
|
|
@@ -55,7 +55,10 @@
|
|
|
55
55
|
"zh": "从 CurseForge/Modrinth/MCMod 搜索模组/整合包/光影等内容,并生成图片卡片。"
|
|
56
56
|
},
|
|
57
57
|
"service": {
|
|
58
|
-
"required": [
|
|
58
|
+
"required": [
|
|
59
|
+
"skia",
|
|
60
|
+
"database"
|
|
61
|
+
]
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
}
|