koishi-plugin-cfmrmod 1.0.1 → 1.0.3
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/cfmr/index.js +536 -38
- package/dist/index.js +20 -3
- package/dist/mcmod/index.js +38 -7
- package/dist/notify.js +529 -0
- package/package.json +6 -3
package/dist/cfmr/index.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Config = exports.name = void 0;
|
|
4
|
+
exports.drawProjectCard = drawProjectCard;
|
|
5
|
+
exports.drawProjectCardCF = drawProjectCardCF;
|
|
6
|
+
exports.drawProjectCardMRNotify = drawProjectCardMRNotify;
|
|
7
|
+
exports.drawProjectCardCFNotify = drawProjectCardCFNotify;
|
|
8
|
+
exports.fetchModrinthDetail = fetchModrinthDetail;
|
|
9
|
+
exports.fetchCurseForgeDetail = fetchCurseForgeDetail;
|
|
4
10
|
exports.apply = apply;
|
|
5
11
|
const { Schema, h } = require('koishi');
|
|
6
|
-
// 【修复】这里添加了 Path2D 的引入
|
|
7
|
-
const { createCanvas, loadImage, Path2D, GlobalFonts } = require('@napi-rs/canvas');
|
|
8
12
|
const fetch = require('node-fetch');
|
|
9
13
|
const cheerio = require('cheerio');
|
|
10
14
|
const { marked } = require('marked');
|
|
15
|
+
let createCanvas;
|
|
16
|
+
let loadImage;
|
|
17
|
+
let Path2DRef;
|
|
18
|
+
let registerFont;
|
|
19
|
+
async function toImageSrc(input) {
|
|
20
|
+
const value = (input && typeof input.then === 'function') ? await input : input;
|
|
21
|
+
if (!value)
|
|
22
|
+
return '';
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return value;
|
|
25
|
+
const buf = Buffer.isBuffer(value) ? value : (value instanceof Uint8Array ? Buffer.from(value) : null);
|
|
26
|
+
if (buf)
|
|
27
|
+
return `data:image/png;base64,${buf.toString('base64')}`;
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
11
30
|
const CF_LOADER_MAP = {
|
|
12
31
|
1: 'Forge',
|
|
13
32
|
2: 'Cauldron',
|
|
@@ -117,6 +136,14 @@ function formatNumber(num) {
|
|
|
117
136
|
return `${(n / 1e3).toFixed(1).replace('.0', '')}k`;
|
|
118
137
|
return String(n);
|
|
119
138
|
}
|
|
139
|
+
function formatFileSize(bytes) {
|
|
140
|
+
const n = Number(bytes) || 0;
|
|
141
|
+
if (n >= 1024 * 1024)
|
|
142
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MiB`;
|
|
143
|
+
if (n >= 1024)
|
|
144
|
+
return `${(n / 1024).toFixed(2)} KiB`;
|
|
145
|
+
return `${n} B`;
|
|
146
|
+
}
|
|
120
147
|
function parseCompactNumber(text) {
|
|
121
148
|
if (!text)
|
|
122
149
|
return null;
|
|
@@ -144,30 +171,45 @@ function fixUrl(url, base = '') {
|
|
|
144
171
|
async function loadImageSafe(url, timeout = 15000) {
|
|
145
172
|
if (!url)
|
|
146
173
|
return null;
|
|
147
|
-
|
|
148
|
-
if (url.includes('.webp')) {
|
|
149
|
-
tryUrls.push(url.replace('.webp', '.png'));
|
|
150
|
-
if (!url.includes('format='))
|
|
151
|
-
tryUrls.push(`${url}${url.includes('?') ? '&' : '?'}format=png`);
|
|
152
|
-
}
|
|
153
|
-
let lastErr;
|
|
154
|
-
for (const u of tryUrls) {
|
|
155
|
-
try {
|
|
156
|
-
return await loadImage(u);
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
lastErr = e;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
174
|
+
// 1. 尝试直接加载 (保留 User-Agent 以防万一,同时检查 res.ok)
|
|
162
175
|
try {
|
|
163
|
-
const res = await fetchWithTimeout(
|
|
176
|
+
const res = await fetchWithTimeout(url, {
|
|
177
|
+
headers: {
|
|
178
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
179
|
+
}
|
|
180
|
+
}, timeout);
|
|
181
|
+
// 如果返回 404/403 等非 2xx 状态,直接抛出异常进入 catch
|
|
182
|
+
if (!res.ok)
|
|
183
|
+
throw new Error(`HTTP ${res.status}`);
|
|
164
184
|
const buf = await res.buffer();
|
|
165
185
|
return await loadImage(buf);
|
|
166
186
|
}
|
|
167
|
-
catch (
|
|
168
|
-
|
|
187
|
+
catch (bufferErr) {
|
|
188
|
+
// 2. 加载失败 (如下载成功但 skia 无法解码 WebP),尝试备用链接
|
|
189
|
+
const tryUrls = []; // 清空之前的无效尝试
|
|
190
|
+
if (url.includes('.webp')) {
|
|
191
|
+
// 【关键修复】使用 wsrv.nl 代理将 WebP 转换为 PNG
|
|
192
|
+
tryUrls.push(`https://wsrv.nl/?url=${encodeURIComponent(url)}&output=png`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// 如果不是 webp 结尾,也尝试一下原链接 (应对网络波动)
|
|
196
|
+
tryUrls.push(url);
|
|
197
|
+
}
|
|
198
|
+
let lastErr = bufferErr;
|
|
199
|
+
for (const u of tryUrls) {
|
|
200
|
+
try {
|
|
201
|
+
// 备用链接也应当加 UA
|
|
202
|
+
// 注意:loadImage(string) 内部通常会自动处理 fetch,但为了稳妥这里利用 skia 的远程加载能力
|
|
203
|
+
// 或者也可以复用 fetchWithTimeout 下载 buffer,这里简化直接调 loadImage
|
|
204
|
+
return await loadImage(u);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
lastErr = e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// 如果都失败了,抛出异常(外部会捕获并绘制灰底)
|
|
211
|
+
throw lastErr;
|
|
169
212
|
}
|
|
170
|
-
throw lastErr;
|
|
171
213
|
}
|
|
172
214
|
// 简单的 Markdown 转 HTML 配置
|
|
173
215
|
marked.setOptions({ breaks: true, gfm: true });
|
|
@@ -560,6 +602,8 @@ async function drawProjectCard(data) {
|
|
|
560
602
|
// Stats & Tags Row
|
|
561
603
|
// Downloads Icon
|
|
562
604
|
const drawIcon = (path, x, y) => {
|
|
605
|
+
if (!Path2DRef)
|
|
606
|
+
return;
|
|
563
607
|
ctx.save();
|
|
564
608
|
ctx.translate(x, y);
|
|
565
609
|
ctx.scale(0.8, 0.8);
|
|
@@ -567,8 +611,7 @@ async function drawProjectCard(data) {
|
|
|
567
611
|
ctx.lineWidth = 2;
|
|
568
612
|
ctx.lineCap = 'round';
|
|
569
613
|
ctx.lineJoin = 'round';
|
|
570
|
-
|
|
571
|
-
const p = new Path2D(path);
|
|
614
|
+
const p = new Path2DRef(path);
|
|
572
615
|
ctx.stroke(p);
|
|
573
616
|
ctx.restore();
|
|
574
617
|
};
|
|
@@ -774,7 +817,7 @@ async function drawProjectCard(data) {
|
|
|
774
817
|
ctx.fillStyle = COLORS.textSec;
|
|
775
818
|
ctx.font = `12px "${font}"`;
|
|
776
819
|
ctx.textAlign = 'center';
|
|
777
|
-
ctx.fillText('Generated by Koishi | Powered by Modrinth & CurseForge', width / 2, footerY);
|
|
820
|
+
ctx.fillText('Generated by Koishi | Powered by Modrinth & CurseForge | Plugin By Mai_xiyu', width / 2, footerY);
|
|
778
821
|
footerY += 18;
|
|
779
822
|
// 3. 绘制要求的作者署名
|
|
780
823
|
ctx.fillText('Plugin By Mai_xiyu', width / 2, footerY);
|
|
@@ -1168,6 +1211,454 @@ async function drawProjectCardCF(data) {
|
|
|
1168
1211
|
}
|
|
1169
1212
|
return [canvas.toBuffer('image/png')];
|
|
1170
1213
|
}
|
|
1214
|
+
// ================= 更新通知卡片(Modrinth) =================
|
|
1215
|
+
// ================= 更新通知卡片(Modrinth 还原版) =================
|
|
1216
|
+
async function drawProjectCardMRNotify(data, latest) {
|
|
1217
|
+
const width = 1000;
|
|
1218
|
+
const margin = 30; // 稍微增大边距
|
|
1219
|
+
const gap = 30;
|
|
1220
|
+
const font = GLOBAL_FONT_FAMILY;
|
|
1221
|
+
// Modrinth Dark Theme Colors (参考截图取色)
|
|
1222
|
+
const C_BG = '#131516'; // 整体深色背景 (接近黑色)
|
|
1223
|
+
const C_CARD = '#1a1c1d'; // 左侧内容背景 (稍微亮一点)
|
|
1224
|
+
const C_TEXT_MAIN = '#ffffff'; // 主标题白色
|
|
1225
|
+
const C_TEXT_SEC = '#9ca5b5'; // 次要文本 (Label 颜色)
|
|
1226
|
+
const C_DIVIDER = '#252729'; // 分割线
|
|
1227
|
+
const C_GREEN = '#1bd96a'; // Release Green
|
|
1228
|
+
const C_CHIP_BG = '#2c2d30'; // Version ID 背景
|
|
1229
|
+
const C_LINK = '#3d83f7'; // 链接色(备用)
|
|
1230
|
+
const iconSize = 80;
|
|
1231
|
+
const rightW = 300; // 侧边栏宽度
|
|
1232
|
+
const cardW = width - margin * 2;
|
|
1233
|
+
const leftW = cardW - rightW - gap;
|
|
1234
|
+
// 1. 高度计算
|
|
1235
|
+
const dummyC = createCanvas(100, 100);
|
|
1236
|
+
const dummy = dummyC.getContext('2d');
|
|
1237
|
+
// Header 高度
|
|
1238
|
+
const headerTextW = cardW - iconSize - 24;
|
|
1239
|
+
dummy.font = `800 32px "${font}"`;
|
|
1240
|
+
const titleH = wrapText(dummy, data.name || '', 0, 0, headerTextW, 40, 2, false);
|
|
1241
|
+
const headerH = Math.max(iconSize, titleH + 30) + 20;
|
|
1242
|
+
// Changelog 高度
|
|
1243
|
+
const changelogText = ((latest === null || latest === void 0 ? void 0 : latest.changelog) || '').trim() || 'No changelog provided.';
|
|
1244
|
+
dummy.font = `15px "${font}"`;
|
|
1245
|
+
const changelogBodyH = wrapText(dummy, changelogText, 0, 0, leftW - 48, 26, 30, false);
|
|
1246
|
+
const mainContentH = 60 + changelogBodyH + 40;
|
|
1247
|
+
// 侧边栏高度计算 (Metadata)
|
|
1248
|
+
// 固定项目:Release(50) + Version(50) + Loaders(50) + GameVer(50) + Env(70) + DLs(50) + Date(50) + Pub(70) + ID(60)
|
|
1249
|
+
// 估算大约 550px,如果内容多会自动延伸
|
|
1250
|
+
let sidebarH = 600;
|
|
1251
|
+
const bodyH = Math.max(sidebarH, mainContentH);
|
|
1252
|
+
const totalH = margin + headerH + 20 + bodyH + margin + 30;
|
|
1253
|
+
// 2. 绘制
|
|
1254
|
+
const canvas = createCanvas(width, totalH);
|
|
1255
|
+
const ctx = canvas.getContext('2d');
|
|
1256
|
+
// 背景
|
|
1257
|
+
ctx.fillStyle = C_BG;
|
|
1258
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
1259
|
+
// === Header ===
|
|
1260
|
+
let cy = margin;
|
|
1261
|
+
if (data.icon) {
|
|
1262
|
+
try {
|
|
1263
|
+
const icon = await loadImageSafe(data.icon);
|
|
1264
|
+
ctx.save();
|
|
1265
|
+
roundRect(ctx, margin, cy, iconSize, iconSize, 16);
|
|
1266
|
+
ctx.clip();
|
|
1267
|
+
ctx.drawImage(icon, margin, cy, iconSize, iconSize);
|
|
1268
|
+
ctx.restore();
|
|
1269
|
+
}
|
|
1270
|
+
catch (e) {
|
|
1271
|
+
ctx.fillStyle = C_CARD;
|
|
1272
|
+
roundRect(ctx, margin, cy, iconSize, iconSize, 16);
|
|
1273
|
+
ctx.fill();
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const tx = margin + iconSize + 24;
|
|
1277
|
+
let ty = cy + 5;
|
|
1278
|
+
ctx.fillStyle = C_TEXT_MAIN;
|
|
1279
|
+
ctx.font = `800 32px "${font}"`;
|
|
1280
|
+
ty = wrapText(ctx, data.name || '', tx, ty, headerTextW, 40, 2, true) + 8;
|
|
1281
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1282
|
+
ctx.font = `16px "${font}"`;
|
|
1283
|
+
// Header 下方显示简单的 By 信息
|
|
1284
|
+
ctx.fillText(`By ${data.author || 'Unknown'}`, tx, ty);
|
|
1285
|
+
// 分割线
|
|
1286
|
+
cy += headerH + 10;
|
|
1287
|
+
ctx.strokeStyle = C_DIVIDER;
|
|
1288
|
+
ctx.lineWidth = 1;
|
|
1289
|
+
ctx.beginPath();
|
|
1290
|
+
ctx.moveTo(margin, cy);
|
|
1291
|
+
ctx.lineTo(width - margin, cy);
|
|
1292
|
+
ctx.stroke();
|
|
1293
|
+
// === Content Area ===
|
|
1294
|
+
cy += 30;
|
|
1295
|
+
const leftX = margin;
|
|
1296
|
+
const rightX = margin + leftW + gap;
|
|
1297
|
+
// --- Left: Changelog ---
|
|
1298
|
+
ctx.fillStyle = C_CARD;
|
|
1299
|
+
roundRect(ctx, leftX, cy, leftW, bodyH, 12);
|
|
1300
|
+
ctx.fill();
|
|
1301
|
+
ctx.fillStyle = C_TEXT_MAIN;
|
|
1302
|
+
ctx.font = `700 22px "${font}"`;
|
|
1303
|
+
ctx.fillText('Changelog', leftX + 24, cy + 40);
|
|
1304
|
+
ctx.strokeStyle = C_DIVIDER;
|
|
1305
|
+
ctx.beginPath();
|
|
1306
|
+
ctx.moveTo(leftX + 24, cy + 60);
|
|
1307
|
+
ctx.lineTo(leftX + leftW - 24, cy + 60);
|
|
1308
|
+
ctx.stroke();
|
|
1309
|
+
ctx.fillStyle = '#b4b4b4'; // Changelog 文本稍微亮一点的灰
|
|
1310
|
+
ctx.font = `15px "${font}"`;
|
|
1311
|
+
wrapText(ctx, changelogText, leftX + 24, cy + 90, leftW - 48, 26, 30, true);
|
|
1312
|
+
// --- Right: Metadata Sidebar (还原截图风格) ---
|
|
1313
|
+
let ry = cy;
|
|
1314
|
+
// Title
|
|
1315
|
+
ctx.fillStyle = C_TEXT_MAIN;
|
|
1316
|
+
ctx.font = `800 22px "${font}"`;
|
|
1317
|
+
ctx.fillText('Metadata', rightX, ry + 10);
|
|
1318
|
+
ry += 40;
|
|
1319
|
+
// Helper: Draw Section Label
|
|
1320
|
+
const drawLabel = (text) => {
|
|
1321
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1322
|
+
ctx.font = `700 14px "${font}"`;
|
|
1323
|
+
ctx.fillText(text, rightX, ry);
|
|
1324
|
+
ry += 24;
|
|
1325
|
+
};
|
|
1326
|
+
// Helper: Draw Value Text
|
|
1327
|
+
const drawValue = (text, color = C_TEXT_MAIN, isBold = false) => {
|
|
1328
|
+
ctx.fillStyle = color;
|
|
1329
|
+
ctx.font = `${isBold ? '700' : '500'} 16px "${font}"`;
|
|
1330
|
+
const h = wrapText(ctx, text, rightX, ry, rightW, 22, 2, true);
|
|
1331
|
+
ry = h + 16; // gap
|
|
1332
|
+
};
|
|
1333
|
+
// 1. Release channel
|
|
1334
|
+
drawLabel('Release channel');
|
|
1335
|
+
// Dot
|
|
1336
|
+
const channelType = (latest === null || latest === void 0 ? void 0 : latest.versionType) === 'beta' ? 'Beta' : ((latest === null || latest === void 0 ? void 0 : latest.versionType) === 'alpha' ? 'Alpha' : 'Release');
|
|
1337
|
+
const channelColor = (latest === null || latest === void 0 ? void 0 : latest.versionType) === 'beta' ? '#4695ee' : ((latest === null || latest === void 0 ? void 0 : latest.versionType) === 'alpha' ? '#f04747' : C_GREEN);
|
|
1338
|
+
ctx.beginPath();
|
|
1339
|
+
ctx.arc(rightX + 6, ry - 6, 5, 0, Math.PI * 2);
|
|
1340
|
+
ctx.fillStyle = channelColor;
|
|
1341
|
+
ctx.fill();
|
|
1342
|
+
ctx.fillStyle = channelColor;
|
|
1343
|
+
ctx.font = `700 16px "${font}"`;
|
|
1344
|
+
ctx.fillText(channelType, rightX + 18, ry);
|
|
1345
|
+
ry += 40;
|
|
1346
|
+
// 2. Version number
|
|
1347
|
+
drawLabel('Version number');
|
|
1348
|
+
drawValue((latest === null || latest === void 0 ? void 0 : latest.version) || (latest === null || latest === void 0 ? void 0 : latest.versionId) || '--');
|
|
1349
|
+
// 3. Loaders (With Icons)
|
|
1350
|
+
drawLabel('Loaders');
|
|
1351
|
+
const loaders = (latest === null || latest === void 0 ? void 0 : latest.loaders) || data.loaders || [];
|
|
1352
|
+
let lx = rightX;
|
|
1353
|
+
// 简易 Tag 图标 Path
|
|
1354
|
+
const tagPath = "M15.5 2H10.5C10.2 2 10 2.1 9.8 2.3L2.3 9.8C1.9 10.2 1.9 10.8 2.3 11.2L6.8 15.7C7.2 16.1 7.8 16.1 8.2 15.7L15.7 8.2C15.9 8 16 7.8 16 7.5V2.5C16 2.2 15.8 2 15.5 2ZM13.5 5C13.2 5 13 4.8 13 4.5C13 4.2 13.2 4 13.5 4C13.8 4 14 4.2 14 4.5C14 4.8 13.8 5 13.5 5Z";
|
|
1355
|
+
if (loaders.length === 0) {
|
|
1356
|
+
drawValue('--');
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
// 模拟 Flex 布局
|
|
1360
|
+
let loaderText = '';
|
|
1361
|
+
loaders.forEach((l, i) => {
|
|
1362
|
+
// 图标
|
|
1363
|
+
ctx.save();
|
|
1364
|
+
ctx.translate(lx, ry - 14);
|
|
1365
|
+
ctx.scale(0.9, 0.9);
|
|
1366
|
+
if (Path2DRef) {
|
|
1367
|
+
const p = new Path2DRef(tagPath);
|
|
1368
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1369
|
+
ctx.fill(p);
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
// Fallback circle
|
|
1373
|
+
ctx.beginPath();
|
|
1374
|
+
ctx.arc(8, 8, 4, 0, Math.PI * 2);
|
|
1375
|
+
ctx.fill();
|
|
1376
|
+
}
|
|
1377
|
+
ctx.restore();
|
|
1378
|
+
// 文本
|
|
1379
|
+
ctx.fillStyle = '#b4b4b4';
|
|
1380
|
+
ctx.font = `500 15px "${font}"`;
|
|
1381
|
+
const text = l.charAt(0).toUpperCase() + l.slice(1);
|
|
1382
|
+
ctx.fillText(text, lx + 20, ry);
|
|
1383
|
+
const itemW = 20 + ctx.measureText(text).width + 15;
|
|
1384
|
+
lx += itemW;
|
|
1385
|
+
// 简单的换行处理
|
|
1386
|
+
if (lx > rightX + rightW - 50) {
|
|
1387
|
+
lx = rightX;
|
|
1388
|
+
ry += 24;
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
ry += 40;
|
|
1392
|
+
}
|
|
1393
|
+
// 4. Game versions
|
|
1394
|
+
drawLabel('Game versions');
|
|
1395
|
+
const gv = ((latest === null || latest === void 0 ? void 0 : latest.gameVersions) || data.gameVersions || []).slice(0, 4).join(', ');
|
|
1396
|
+
drawValue(gv || 'All');
|
|
1397
|
+
// 5. Environment
|
|
1398
|
+
drawLabel('Environment');
|
|
1399
|
+
let envText = 'Client and server';
|
|
1400
|
+
if (data.clientSide === 'unsupported')
|
|
1401
|
+
envText = 'Server only';
|
|
1402
|
+
else if (data.serverSide === 'unsupported')
|
|
1403
|
+
envText = 'Client only';
|
|
1404
|
+
else if (data.clientSide === 'required' && data.serverSide === 'required')
|
|
1405
|
+
envText = 'Client and server, required on both';
|
|
1406
|
+
drawValue(envText);
|
|
1407
|
+
// 6. Downloads
|
|
1408
|
+
drawLabel('Downloads');
|
|
1409
|
+
drawValue(formatNumber((latest === null || latest === void 0 ? void 0 : latest.downloads) || 0));
|
|
1410
|
+
// 7. Publication date
|
|
1411
|
+
drawLabel('Publication date');
|
|
1412
|
+
const dateStr = (latest === null || latest === void 0 ? void 0 : latest.datePublished)
|
|
1413
|
+
? new Date(latest.datePublished).toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' })
|
|
1414
|
+
: '--';
|
|
1415
|
+
drawValue(dateStr);
|
|
1416
|
+
// 8. Publisher (Avatar + Name)
|
|
1417
|
+
drawLabel('Publisher');
|
|
1418
|
+
const authorName = data.author || 'Unknown';
|
|
1419
|
+
const memberRole = 'Member'; // 默认写 Member,API 没返回具体 Role
|
|
1420
|
+
// Avatar
|
|
1421
|
+
const avatarR = 20;
|
|
1422
|
+
ctx.save();
|
|
1423
|
+
ctx.beginPath();
|
|
1424
|
+
ctx.arc(rightX + avatarR, ry + avatarR - 10, avatarR, 0, Math.PI * 2);
|
|
1425
|
+
ctx.fillStyle = C_CHIP_BG;
|
|
1426
|
+
ctx.fill();
|
|
1427
|
+
ctx.clip();
|
|
1428
|
+
if (data.authorIcon) {
|
|
1429
|
+
try {
|
|
1430
|
+
const authorImg = await loadImageSafe(data.authorIcon);
|
|
1431
|
+
ctx.drawImage(authorImg, rightX, ry - 10, avatarR * 2, avatarR * 2);
|
|
1432
|
+
}
|
|
1433
|
+
catch (e) { }
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
// Draw Initials
|
|
1437
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1438
|
+
ctx.textAlign = 'center';
|
|
1439
|
+
ctx.textBaseline = 'middle';
|
|
1440
|
+
ctx.font = `bold 16px "${font}"`;
|
|
1441
|
+
ctx.fillText(authorName.charAt(0).toUpperCase(), rightX + avatarR, ry + avatarR - 10);
|
|
1442
|
+
}
|
|
1443
|
+
ctx.restore();
|
|
1444
|
+
// Name & Role
|
|
1445
|
+
ctx.textAlign = 'left';
|
|
1446
|
+
ctx.textBaseline = 'top';
|
|
1447
|
+
ctx.fillStyle = C_TEXT_MAIN;
|
|
1448
|
+
ctx.font = `700 15px "${font}"`;
|
|
1449
|
+
ctx.fillText(authorName, rightX + 50, ry - 5);
|
|
1450
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1451
|
+
ctx.font = `13px "${font}"`;
|
|
1452
|
+
ctx.fillText(memberRole, rightX + 50, ry + 15);
|
|
1453
|
+
ry += 60;
|
|
1454
|
+
// 9. Version ID (Chip)
|
|
1455
|
+
drawLabel('Version ID');
|
|
1456
|
+
const verId = (latest === null || latest === void 0 ? void 0 : latest.versionId) || '-------';
|
|
1457
|
+
const chipPadding = 10;
|
|
1458
|
+
ctx.font = `14px "${font}"`; // Monospace ideally, but sans is fine
|
|
1459
|
+
const idW = ctx.measureText(verId).width + 30; // + space for icon
|
|
1460
|
+
ctx.fillStyle = C_CHIP_BG;
|
|
1461
|
+
roundRect(ctx, rightX, ry - 5, idW + chipPadding * 2, 32, 8);
|
|
1462
|
+
ctx.fill();
|
|
1463
|
+
ctx.fillStyle = '#b4b4b4';
|
|
1464
|
+
ctx.fillText(verId, rightX + chipPadding, ry + 5);
|
|
1465
|
+
// Copy Icon (simulated)
|
|
1466
|
+
const iconX = rightX + chipPadding + ctx.measureText(verId).width + 10;
|
|
1467
|
+
ctx.strokeStyle = '#b4b4b4';
|
|
1468
|
+
ctx.lineWidth = 1.5;
|
|
1469
|
+
ctx.strokeRect(iconX, ry + 4, 10, 12);
|
|
1470
|
+
ctx.fillStyle = '#b4b4b4';
|
|
1471
|
+
ctx.fillRect(iconX + 3, ry + 2, 8, 2); // top bit
|
|
1472
|
+
ry += 50;
|
|
1473
|
+
// Footer
|
|
1474
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1475
|
+
ctx.font = `12px "${font}"`;
|
|
1476
|
+
ctx.textAlign = 'center';
|
|
1477
|
+
ctx.fillText('Powered by Modrinth | Generated by Koishi | Plugin By Mai_xiyu', width / 2, totalH - 15);
|
|
1478
|
+
return [canvas.toBuffer('image/png')];
|
|
1479
|
+
}
|
|
1480
|
+
// ================= 更新通知卡片(CurseForge) =================
|
|
1481
|
+
async function drawProjectCardCFNotify(data, latest) {
|
|
1482
|
+
const width = 1000;
|
|
1483
|
+
const margin = 24;
|
|
1484
|
+
const gap = 24;
|
|
1485
|
+
const font = GLOBAL_FONT_FAMILY;
|
|
1486
|
+
// CF Colors
|
|
1487
|
+
const C_BG = '#141414';
|
|
1488
|
+
const C_PANEL = '#1d1d1d';
|
|
1489
|
+
const C_TEXT = '#dee2e6';
|
|
1490
|
+
const C_TEXT_SEC = '#adb5bd';
|
|
1491
|
+
const C_ACCENT = '#f16436';
|
|
1492
|
+
const C_DIVIDER = '#2d2d2d';
|
|
1493
|
+
const iconSize = 80;
|
|
1494
|
+
const rightW = 320; // 加宽右侧
|
|
1495
|
+
const cardW = width - margin * 2;
|
|
1496
|
+
const leftW = cardW - rightW - gap;
|
|
1497
|
+
const dummyC = createCanvas(100, 100);
|
|
1498
|
+
const dummy = dummyC.getContext('2d');
|
|
1499
|
+
// --- 辅助:映射 Release Type ---
|
|
1500
|
+
const getReleaseTypeStr = (type) => {
|
|
1501
|
+
if (type === 1)
|
|
1502
|
+
return 'Release';
|
|
1503
|
+
if (type === 2)
|
|
1504
|
+
return 'Beta';
|
|
1505
|
+
if (type === 3)
|
|
1506
|
+
return 'Alpha';
|
|
1507
|
+
return String(type || 'Unknown');
|
|
1508
|
+
};
|
|
1509
|
+
const releaseTypeStr = getReleaseTypeStr(latest === null || latest === void 0 ? void 0 : latest.releaseType);
|
|
1510
|
+
// --- 1. 高度计算 ---
|
|
1511
|
+
const headerTextW = cardW - iconSize - 24;
|
|
1512
|
+
// Header: 标题自适应
|
|
1513
|
+
let titleFontSize = 32;
|
|
1514
|
+
dummy.font = `bold ${titleFontSize}px "${font}"`;
|
|
1515
|
+
while (dummy.measureText(data.name || '').width > headerTextW && titleFontSize > 22) {
|
|
1516
|
+
titleFontSize -= 2;
|
|
1517
|
+
dummy.font = `bold ${titleFontSize}px "${font}"`;
|
|
1518
|
+
}
|
|
1519
|
+
const titleH = wrapText(dummy, data.name || '', 0, 0, headerTextW, titleFontSize * 1.3, 2, false);
|
|
1520
|
+
dummy.font = `16px "${font}"`;
|
|
1521
|
+
const summaryH = wrapText(dummy, (data.summary || '').slice(0, 200), 0, 0, headerTextW, 24, 3, false);
|
|
1522
|
+
const headerH = Math.max(iconSize, titleH + summaryH + 15) + 20;
|
|
1523
|
+
// Sidebar Metadata
|
|
1524
|
+
const metaLines = [
|
|
1525
|
+
{ l: 'New Version', v: (latest === null || latest === void 0 ? void 0 : latest.version) || '--', hl: true }, // Highlight
|
|
1526
|
+
{ l: 'Downloads', v: formatNumber(latest === null || latest === void 0 ? void 0 : latest.downloads) },
|
|
1527
|
+
{ l: 'Game Ver', v: ((latest === null || latest === void 0 ? void 0 : latest.gameVersions) || data.gameVersions || []).slice(0, 5).join(', ') || '--' },
|
|
1528
|
+
{ l: 'Loaders', v: ((latest === null || latest === void 0 ? void 0 : latest.loaders) || data.loaders || []).slice(0, 4).join(', ') || '--' },
|
|
1529
|
+
{ l: 'Updated', v: (latest === null || latest === void 0 ? void 0 : latest.datePublished) ? new Date(latest.datePublished).toLocaleDateString() : '--' },
|
|
1530
|
+
{ l: 'Release Type', v: releaseTypeStr },
|
|
1531
|
+
{ l: 'File', v: (latest === null || latest === void 0 ? void 0 : latest.fileName) || '--' },
|
|
1532
|
+
{ l: 'Size', v: formatFileSize(latest === null || latest === void 0 ? void 0 : latest.fileSize) },
|
|
1533
|
+
{ l: 'Author', v: data.author || '--' },
|
|
1534
|
+
];
|
|
1535
|
+
// 计算 Sidebar 高度 (支持换行)
|
|
1536
|
+
let metaH = 60; // Title padding
|
|
1537
|
+
dummy.font = `15px "${font}"`;
|
|
1538
|
+
metaLines.forEach(item => {
|
|
1539
|
+
metaH += 20; // Label
|
|
1540
|
+
const valLines = wrapText(dummy, item.v, 0, 0, rightW - 40, 24, 5, false) / 24;
|
|
1541
|
+
metaH += valLines * 24 + 10; // Value + padding
|
|
1542
|
+
metaH += 10; // Divider
|
|
1543
|
+
});
|
|
1544
|
+
// Changelog Height
|
|
1545
|
+
const changelogText = ((latest === null || latest === void 0 ? void 0 : latest.changelog) || '').trim() || 'No changelog provided.';
|
|
1546
|
+
dummy.font = `15px "${font}"`;
|
|
1547
|
+
const changelogBodyH = wrapText(dummy, changelogText, 0, 0, leftW - 48, 26, 60, false);
|
|
1548
|
+
const contentH = 60 + changelogBodyH + 40;
|
|
1549
|
+
const bodyH = Math.max(metaH, contentH, 300);
|
|
1550
|
+
const totalH = margin + headerH + 20 + bodyH + margin + 30;
|
|
1551
|
+
// --- 2. 绘制 ---
|
|
1552
|
+
const canvas = createCanvas(width, totalH);
|
|
1553
|
+
const ctx = canvas.getContext('2d');
|
|
1554
|
+
ctx.fillStyle = C_BG;
|
|
1555
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
1556
|
+
// === Header ===
|
|
1557
|
+
let cy = margin;
|
|
1558
|
+
if (data.icon) {
|
|
1559
|
+
try {
|
|
1560
|
+
const icon = await loadImageSafe(data.icon);
|
|
1561
|
+
ctx.save();
|
|
1562
|
+
ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
|
1563
|
+
ctx.shadowBlur = 10;
|
|
1564
|
+
roundRect(ctx, margin, cy, iconSize, iconSize, 8);
|
|
1565
|
+
ctx.fill();
|
|
1566
|
+
ctx.shadowColor = 'transparent';
|
|
1567
|
+
roundRect(ctx, margin, cy, iconSize, iconSize, 8);
|
|
1568
|
+
ctx.clip();
|
|
1569
|
+
ctx.drawImage(icon, margin, cy, iconSize, iconSize);
|
|
1570
|
+
ctx.restore();
|
|
1571
|
+
}
|
|
1572
|
+
catch (e) {
|
|
1573
|
+
ctx.fillStyle = '#333';
|
|
1574
|
+
roundRect(ctx, margin, cy, iconSize, iconSize, 8);
|
|
1575
|
+
ctx.fill();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const tx = margin + iconSize + 24;
|
|
1579
|
+
let ty = cy + 5;
|
|
1580
|
+
ctx.fillStyle = C_TEXT;
|
|
1581
|
+
ctx.font = `bold ${titleFontSize}px "${font}"`;
|
|
1582
|
+
ty = wrapText(ctx, data.name || '', tx, ty, headerTextW, titleFontSize * 1.3, 2, true) + 8;
|
|
1583
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1584
|
+
ctx.font = `16px "${font}"`;
|
|
1585
|
+
wrapText(ctx, `By ${data.author || 'Unknown'}`, tx, ty, headerTextW, 24, 1, true);
|
|
1586
|
+
// Orange Tab Indicator
|
|
1587
|
+
cy += headerH + 10;
|
|
1588
|
+
ctx.fillStyle = C_ACCENT;
|
|
1589
|
+
ctx.fillRect(margin, cy, 100, 4); // 稍微长一点的指示条
|
|
1590
|
+
ctx.fillStyle = C_DIVIDER;
|
|
1591
|
+
ctx.fillRect(margin + 100, cy, width - margin * 2 - 100, 4);
|
|
1592
|
+
// === Body ===
|
|
1593
|
+
cy += 24;
|
|
1594
|
+
const leftX = margin;
|
|
1595
|
+
const rightX = margin + leftW + gap;
|
|
1596
|
+
// -- 左侧:Changelog --
|
|
1597
|
+
ctx.fillStyle = C_PANEL;
|
|
1598
|
+
roundRect(ctx, leftX, cy, leftW, bodyH, 8);
|
|
1599
|
+
ctx.fill();
|
|
1600
|
+
ctx.fillStyle = C_TEXT;
|
|
1601
|
+
ctx.font = `bold 22px "${font}"`;
|
|
1602
|
+
ctx.fillText('What\'s New', leftX + 24, cy + 40);
|
|
1603
|
+
ctx.fillStyle = C_DIVIDER;
|
|
1604
|
+
ctx.fillRect(leftX + 24, cy + 60, leftW - 48, 2);
|
|
1605
|
+
ctx.fillStyle = '#ced4da';
|
|
1606
|
+
ctx.font = `15px "${font}"`;
|
|
1607
|
+
wrapText(ctx, changelogText, leftX + 24, cy + 85, leftW - 48, 26, 60, true);
|
|
1608
|
+
// -- 右侧:Sidebar --
|
|
1609
|
+
ctx.fillStyle = C_PANEL;
|
|
1610
|
+
roundRect(ctx, rightX, cy, rightW, bodyH, 8);
|
|
1611
|
+
ctx.fill();
|
|
1612
|
+
let ry = cy + 40;
|
|
1613
|
+
const maxRy = cy + bodyH - 20;
|
|
1614
|
+
ctx.fillStyle = C_TEXT;
|
|
1615
|
+
ctx.font = `bold 18px "${font}"`;
|
|
1616
|
+
ctx.fillText('File Details', rightX + 20, ry);
|
|
1617
|
+
ry += 20;
|
|
1618
|
+
metaLines.forEach(item => {
|
|
1619
|
+
if (ry + 40 > maxRy) {
|
|
1620
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1621
|
+
ctx.font = `12px "${font}"`;
|
|
1622
|
+
ctx.fillText('...', rightX + 20, maxRy - 6);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
ry += 15;
|
|
1626
|
+
// Label
|
|
1627
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1628
|
+
ctx.font = `13px "${font}"`;
|
|
1629
|
+
ctx.fillText(item.l, rightX + 20, ry);
|
|
1630
|
+
// Value (支持换行)
|
|
1631
|
+
ry += 22;
|
|
1632
|
+
ctx.font = `15px "${font}"`;
|
|
1633
|
+
// 特殊颜色处理
|
|
1634
|
+
if (item.hl)
|
|
1635
|
+
ctx.fillStyle = C_ACCENT;
|
|
1636
|
+
else if (item.l === 'Release Type') {
|
|
1637
|
+
if (item.v === 'Release')
|
|
1638
|
+
ctx.fillStyle = '#1bd96a'; // Green
|
|
1639
|
+
else if (item.v === 'Beta')
|
|
1640
|
+
ctx.fillStyle = '#a020f0'; // Purple
|
|
1641
|
+
else
|
|
1642
|
+
ctx.fillStyle = C_TEXT;
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
ctx.fillStyle = C_TEXT;
|
|
1646
|
+
}
|
|
1647
|
+
// 自动换行绘制
|
|
1648
|
+
const nextY = wrapText(ctx, item.v, rightX + 20, ry, rightW - 40, 24, 5, true);
|
|
1649
|
+
ry = nextY + 10;
|
|
1650
|
+
if (ry + 6 > maxRy)
|
|
1651
|
+
return;
|
|
1652
|
+
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
|
1653
|
+
ctx.fillRect(rightX + 20, ry, rightW - 40, 1);
|
|
1654
|
+
});
|
|
1655
|
+
// Footer
|
|
1656
|
+
ctx.fillStyle = C_TEXT_SEC;
|
|
1657
|
+
ctx.font = `12px "${font}"`;
|
|
1658
|
+
ctx.textAlign = 'center';
|
|
1659
|
+
ctx.fillText('Powered by CurseForge | Generated by Koishi | Plugin By Mai_xiyu', width / 2, totalH - 15);
|
|
1660
|
+
return [canvas.toBuffer('image/png')];
|
|
1661
|
+
}
|
|
1171
1662
|
// ================= API 交互 =================
|
|
1172
1663
|
async function fetchModrinthDetail(id, timeout) {
|
|
1173
1664
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -1227,7 +1718,7 @@ async function fetchModrinthDetail(id, timeout) {
|
|
|
1227
1718
|
id: project.id,
|
|
1228
1719
|
name: project.title,
|
|
1229
1720
|
author,
|
|
1230
|
-
icon: (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.icon)
|
|
1721
|
+
icon: project.icon_url || (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.icon),
|
|
1231
1722
|
summary: project.description,
|
|
1232
1723
|
body,
|
|
1233
1724
|
bodyIsHtml,
|
|
@@ -1488,18 +1979,25 @@ async function searchCurseForge(query, type, apiKey, timeout, gameId = 432) {
|
|
|
1488
1979
|
function apply(ctx, config) {
|
|
1489
1980
|
var _a, _b;
|
|
1490
1981
|
const logger = ctx.logger('mc-search');
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
if (ok)
|
|
1495
|
-
GLOBAL_FONT_FAMILY = 'KoishiFont';
|
|
1496
|
-
else
|
|
1497
|
-
logger.warn('字体加载失败: registerFromPath 返回 false');
|
|
1498
|
-
}
|
|
1499
|
-
catch (e) {
|
|
1500
|
-
logger.warn('字体加载失败: ' + e.message);
|
|
1501
|
-
}
|
|
1982
|
+
const skia = ctx.skia;
|
|
1983
|
+
if (!(skia === null || skia === void 0 ? void 0 : skia.Canvas) || !(skia === null || skia === void 0 ? void 0 : skia.loadImage)) {
|
|
1984
|
+
throw new Error('缺少 skia 服务,请先启用 @ltxhhz/koishi-plugin-skia-canvas');
|
|
1502
1985
|
}
|
|
1986
|
+
createCanvas = (w, h) => {
|
|
1987
|
+
const c = new skia.Canvas(w || 0, h || 0);
|
|
1988
|
+
if (!c || typeof c.getContext !== 'function') {
|
|
1989
|
+
throw new Error('skia 服务异常:Canvas 无效,请确认使用 @ltxhhz/koishi-plugin-skia-canvas');
|
|
1990
|
+
}
|
|
1991
|
+
return c;
|
|
1992
|
+
};
|
|
1993
|
+
loadImage = skia.loadImage;
|
|
1994
|
+
registerFont = (path, options) => {
|
|
1995
|
+
var _a;
|
|
1996
|
+
if ((_a = skia.FontLibrary) === null || _a === void 0 ? void 0 : _a.use)
|
|
1997
|
+
skia.FontLibrary.use(path, options === null || options === void 0 ? void 0 : options.family);
|
|
1998
|
+
};
|
|
1999
|
+
Path2DRef = skia.Path2D || globalThis.Path2D;
|
|
2000
|
+
// 取消自定义字体配置,使用 skia 默认字体
|
|
1503
2001
|
const states = new Map();
|
|
1504
2002
|
const normalizeMessageIds = (res) => {
|
|
1505
2003
|
if (!res)
|
|
@@ -1564,7 +2062,7 @@ function apply(ctx, config) {
|
|
|
1564
2062
|
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
1565
2063
|
});
|
|
1566
2064
|
for (const buf of imgBufs) {
|
|
1567
|
-
await session.send(h.image(buf
|
|
2065
|
+
await session.send(h.image(await toImageSrc(buf)));
|
|
1568
2066
|
}
|
|
1569
2067
|
if (config.sendLink)
|
|
1570
2068
|
await session.send(`链接: ${detailData.url}`);
|
|
@@ -1625,7 +2123,7 @@ function apply(ctx, config) {
|
|
|
1625
2123
|
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
1626
2124
|
});
|
|
1627
2125
|
for (const buf of imgBufs) {
|
|
1628
|
-
await session.send(h.image(buf
|
|
2126
|
+
await session.send(h.image(await toImageSrc(buf)));
|
|
1629
2127
|
}
|
|
1630
2128
|
if (config.sendLink)
|
|
1631
2129
|
await session.send(`链接: ${detailData.url}`);
|
package/dist/index.js
CHANGED
|
@@ -33,19 +33,35 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.Config = exports.name = void 0;
|
|
36
|
+
exports.Config = exports.inject = exports.name = void 0;
|
|
37
37
|
exports.apply = apply;
|
|
38
38
|
const koishi_1 = require("koishi");
|
|
39
39
|
const cfmr = __importStar(require("./plugins/cfmr"));
|
|
40
40
|
const mcmod = __importStar(require("./plugins/mcmod"));
|
|
41
|
+
const notify = __importStar(require("./notify"));
|
|
41
42
|
exports.name = 'minecraft-search';
|
|
43
|
+
exports.inject = ['skia', 'database'];
|
|
42
44
|
exports.Config = koishi_1.Schema.object({
|
|
43
45
|
prefixes: koishi_1.Schema.object({
|
|
44
46
|
cf: koishi_1.Schema.string().default('cf'),
|
|
45
47
|
mr: koishi_1.Schema.string().default('mr'),
|
|
46
48
|
cnmc: koishi_1.Schema.string().default('cnmc'),
|
|
47
49
|
}).description('指令前缀设置'),
|
|
48
|
-
|
|
50
|
+
notify: koishi_1.Schema.object({
|
|
51
|
+
enabled: koishi_1.Schema.boolean().default(false).description('是否开启模组更新通知'),
|
|
52
|
+
interval: koishi_1.Schema.number().default(30 * 60 * 1000).description('轮询间隔(ms),默认 30 分钟'),
|
|
53
|
+
adminAuthority: koishi_1.Schema.number().default(3).description('机器人管理员权限等级(默认 3)'),
|
|
54
|
+
stateFile: koishi_1.Schema.string().default('data/cfmrmod_notify_state.json').description('状态存储 JSON 路径(数据库不可用时使用)'),
|
|
55
|
+
groups: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
56
|
+
channelId: koishi_1.Schema.string().description('群号/频道 ID'),
|
|
57
|
+
enabled: koishi_1.Schema.boolean().default(true).description('是否启用本群通知'),
|
|
58
|
+
subs: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
59
|
+
platform: koishi_1.Schema.union(['mr', 'cf']).description('平台:mr/cf'),
|
|
60
|
+
projectId: koishi_1.Schema.string().description('项目 ID'),
|
|
61
|
+
interval: koishi_1.Schema.number().default(30 * 60 * 1000).description('单独轮询间隔(ms),默认 30 分钟,<= 0 禁用该订阅'),
|
|
62
|
+
})).role('table').default([]).description('订阅列表'),
|
|
63
|
+
})).role('table').default([]).description('通知群与订阅列表'),
|
|
64
|
+
}).description('—— 更新通知 ——'),
|
|
49
65
|
timeouts: koishi_1.Schema.number().default(60000).description('搜索会话超时时间(ms)'),
|
|
50
66
|
debug: koishi_1.Schema.boolean().default(false).description('开启调试日志'),
|
|
51
67
|
cfmr: cfmr.Config.description('CurseForge/Modrinth 搜索与图片卡片'),
|
|
@@ -55,7 +71,6 @@ function apply(ctx, config) {
|
|
|
55
71
|
const prefixes = (config === null || config === void 0 ? void 0 : config.prefixes) || {};
|
|
56
72
|
const shared = {
|
|
57
73
|
prefixes,
|
|
58
|
-
fontPath: config === null || config === void 0 ? void 0 : config.fontPath,
|
|
59
74
|
timeouts: config === null || config === void 0 ? void 0 : config.timeouts,
|
|
60
75
|
debug: config === null || config === void 0 ? void 0 : config.debug,
|
|
61
76
|
};
|
|
@@ -63,4 +78,6 @@ function apply(ctx, config) {
|
|
|
63
78
|
cfmr.apply(ctx, { ...((config === null || config === void 0 ? void 0 : config.cfmr) || {}), ...shared });
|
|
64
79
|
if (mcmod.apply)
|
|
65
80
|
mcmod.apply(ctx, { ...((config === null || config === void 0 ? void 0 : config.mcmod) || {}), ...shared });
|
|
81
|
+
if (notify.apply)
|
|
82
|
+
notify.apply(ctx, (config === null || config === void 0 ? void 0 : config.notify) || {}, { cfmr: (config === null || config === void 0 ? void 0 : config.cfmr) || {} });
|
|
66
83
|
}
|
package/dist/mcmod/index.js
CHANGED
|
@@ -6,8 +6,21 @@ const fetch = require('node-fetch');
|
|
|
6
6
|
const cheerio = require('cheerio');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { createCanvas, loadImage, registerFont, GlobalFonts } = require('@napi-rs/canvas');
|
|
10
9
|
const { h, Schema } = require('koishi');
|
|
10
|
+
let createCanvas;
|
|
11
|
+
let loadImage;
|
|
12
|
+
let registerFont;
|
|
13
|
+
async function toImageSrc(input) {
|
|
14
|
+
const value = (input && typeof input.then === 'function') ? await input : input;
|
|
15
|
+
if (!value)
|
|
16
|
+
return '';
|
|
17
|
+
if (typeof value === 'string')
|
|
18
|
+
return value;
|
|
19
|
+
const buf = Buffer.isBuffer(value) ? value : (value instanceof Uint8Array ? Buffer.from(value) : null);
|
|
20
|
+
if (buf)
|
|
21
|
+
return `data:image/png;base64,${buf.toString('base64')}`;
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
11
24
|
// Cookie 管理器
|
|
12
25
|
let cookieManager = null;
|
|
13
26
|
try {
|
|
@@ -139,13 +152,14 @@ function wrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines = 1000, draw =
|
|
|
139
152
|
return currentY + lineHeight;
|
|
140
153
|
}
|
|
141
154
|
// ================= 字体注册 =================
|
|
142
|
-
function initFont(preferredPath, logger) {
|
|
155
|
+
function initFont(preferredPath, logger, registerFontFn) {
|
|
143
156
|
const fontName = 'MCModFont';
|
|
144
157
|
const tryRegister = (filePath, source) => {
|
|
145
158
|
if (!fs.existsSync(filePath))
|
|
146
159
|
return false;
|
|
147
160
|
try {
|
|
148
|
-
if (
|
|
161
|
+
if (registerFontFn) {
|
|
162
|
+
registerFontFn(filePath, { family: fontName });
|
|
149
163
|
GLOBAL_FONT_FAMILY = fontName;
|
|
150
164
|
logger.info(`[Font] 成功加载${source}: ${filePath}`);
|
|
151
165
|
return true;
|
|
@@ -2433,7 +2447,24 @@ exports.Config = Schema.object({
|
|
|
2433
2447
|
function apply(ctx, config) {
|
|
2434
2448
|
var _a;
|
|
2435
2449
|
const logger = ctx.logger('mcmod');
|
|
2436
|
-
|
|
2450
|
+
const skia = ctx.skia;
|
|
2451
|
+
if (!(skia === null || skia === void 0 ? void 0 : skia.Canvas) || !(skia === null || skia === void 0 ? void 0 : skia.loadImage)) {
|
|
2452
|
+
throw new Error('缺少 skia 服务,请先启用 @ltxhhz/koishi-plugin-skia-canvas');
|
|
2453
|
+
}
|
|
2454
|
+
createCanvas = (w, h) => {
|
|
2455
|
+
const c = new skia.Canvas(w || 0, h || 0);
|
|
2456
|
+
if (!c || typeof c.getContext !== 'function') {
|
|
2457
|
+
throw new Error('skia 服务异常:Canvas 无效,请确认使用 @ltxhhz/koishi-plugin-skia-canvas');
|
|
2458
|
+
}
|
|
2459
|
+
return c;
|
|
2460
|
+
};
|
|
2461
|
+
loadImage = skia.loadImage;
|
|
2462
|
+
registerFont = (path, options) => {
|
|
2463
|
+
var _a;
|
|
2464
|
+
if ((_a = skia.FontLibrary) === null || _a === void 0 ? void 0 : _a.use)
|
|
2465
|
+
skia.FontLibrary.use(path, options === null || options === void 0 ? void 0 : options.family);
|
|
2466
|
+
};
|
|
2467
|
+
// 取消自定义字体配置,使用 skia 默认字体
|
|
2437
2468
|
// 初始化 Cookie
|
|
2438
2469
|
if (config.cookie) {
|
|
2439
2470
|
globalCookie = config.cookie;
|
|
@@ -2563,7 +2594,7 @@ function apply(ctx, config) {
|
|
|
2563
2594
|
img = await drawTutorialCard(item.link);
|
|
2564
2595
|
else
|
|
2565
2596
|
img = await createInfoCard(item.link, type);
|
|
2566
|
-
await session.send(h.image(img
|
|
2597
|
+
await session.send(h.image(await toImageSrc(img)));
|
|
2567
2598
|
if (config.sendLink)
|
|
2568
2599
|
await session.send(`链接: ${item.link}`);
|
|
2569
2600
|
return;
|
|
@@ -2612,7 +2643,7 @@ function apply(ctx, config) {
|
|
|
2612
2643
|
const item = results[0];
|
|
2613
2644
|
await ensureValidCookie();
|
|
2614
2645
|
const img = await drawModCard(item.link);
|
|
2615
|
-
await session.send(h.image(img
|
|
2646
|
+
await session.send(h.image(await toImageSrc(img)));
|
|
2616
2647
|
if (config.sendLink)
|
|
2617
2648
|
await session.send(`链接: ${item.link}`);
|
|
2618
2649
|
return;
|
|
@@ -2716,7 +2747,7 @@ function apply(ctx, config) {
|
|
|
2716
2747
|
img = await drawTutorialCard(item.link);
|
|
2717
2748
|
else
|
|
2718
2749
|
img = await createInfoCard(item.link, currentState.type);
|
|
2719
|
-
await session.send(h.image(img
|
|
2750
|
+
await session.send(h.image(await toImageSrc(img)));
|
|
2720
2751
|
if (config.sendLink)
|
|
2721
2752
|
await session.send(`链接: ${item.link}`);
|
|
2722
2753
|
}
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.apply = apply;
|
|
7
|
+
const koishi_1 = require("koishi");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const cfmr_1 = require("./cfmr");
|
|
11
|
+
const fetch = require('node-fetch');
|
|
12
|
+
const MR_BASE = 'https://api.modrinth.com/v2';
|
|
13
|
+
const CF_MIRROR_BASE = 'https://api.curse.tools/v1/cf';
|
|
14
|
+
function normalizePlatform(platform) {
|
|
15
|
+
if (platform === 'mr' || platform === 'cf')
|
|
16
|
+
return platform;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async function toImageSrc(input) {
|
|
20
|
+
const value = (input && typeof input.then === 'function') ? await input : input;
|
|
21
|
+
if (!value)
|
|
22
|
+
return '';
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return value;
|
|
25
|
+
const buf = Buffer.isBuffer(value) ? value : (value instanceof Uint8Array ? Buffer.from(value) : null);
|
|
26
|
+
if (buf)
|
|
27
|
+
return `data:image/png;base64,${buf.toString('base64')}`;
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
async function fetchJson(url, timeout = 15000) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' }, signal: controller.signal });
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
throw new Error(`HTTP ${res.status}`);
|
|
37
|
+
return await res.json();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function apply(ctx, config, options) {
|
|
44
|
+
const logger = ctx.logger('cfmr-notify');
|
|
45
|
+
ctx.model.extend('cfmrmod_notify_sub', {
|
|
46
|
+
id: 'unsigned',
|
|
47
|
+
channelId: 'string',
|
|
48
|
+
platform: 'string',
|
|
49
|
+
projectId: 'string',
|
|
50
|
+
lastVersion: 'string',
|
|
51
|
+
lastNotifiedAt: 'timestamp',
|
|
52
|
+
}, { primary: 'id', autoInc: true });
|
|
53
|
+
const getRoleLevel = (session) => {
|
|
54
|
+
var _a, _b, _c, _d, _e;
|
|
55
|
+
const roles = new Set();
|
|
56
|
+
const list = (_a = session === null || session === void 0 ? void 0 : session.member) === null || _a === void 0 ? void 0 : _a.roles;
|
|
57
|
+
if (Array.isArray(list)) {
|
|
58
|
+
list.forEach((r) => {
|
|
59
|
+
if (typeof r === 'string')
|
|
60
|
+
roles.add(r);
|
|
61
|
+
else if (r && typeof r === 'object') {
|
|
62
|
+
if (typeof r.id === 'string')
|
|
63
|
+
roles.add(r.id);
|
|
64
|
+
if (typeof r.name === 'string')
|
|
65
|
+
roles.add(r.name);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const role = (_b = session === null || session === void 0 ? void 0 : session.member) === null || _b === void 0 ? void 0 : _b.role;
|
|
70
|
+
if (typeof role === 'string')
|
|
71
|
+
roles.add(role);
|
|
72
|
+
const onebotRole = (_d = (_c = session === null || session === void 0 ? void 0 : session.event) === null || _c === void 0 ? void 0 : _c.sender) === null || _d === void 0 ? void 0 : _d.role;
|
|
73
|
+
if (typeof onebotRole === 'string')
|
|
74
|
+
roles.add(onebotRole);
|
|
75
|
+
const eventMember = (_e = session === null || session === void 0 ? void 0 : session.event) === null || _e === void 0 ? void 0 : _e.member;
|
|
76
|
+
if ((eventMember === null || eventMember === void 0 ? void 0 : eventMember.role) && typeof eventMember.role === 'string')
|
|
77
|
+
roles.add(eventMember.role);
|
|
78
|
+
if (Array.isArray(eventMember === null || eventMember === void 0 ? void 0 : eventMember.roles)) {
|
|
79
|
+
eventMember.roles.forEach((r) => {
|
|
80
|
+
if (typeof r === 'string')
|
|
81
|
+
roles.add(r);
|
|
82
|
+
else if (r && typeof r === 'object') {
|
|
83
|
+
if (typeof r.id === 'string')
|
|
84
|
+
roles.add(r.id);
|
|
85
|
+
if (typeof r.name === 'string')
|
|
86
|
+
roles.add(r.name);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (roles.has('owner'))
|
|
91
|
+
return 3;
|
|
92
|
+
if (roles.has('admin'))
|
|
93
|
+
return 2;
|
|
94
|
+
if (roles.has('member'))
|
|
95
|
+
return 1;
|
|
96
|
+
return 0;
|
|
97
|
+
};
|
|
98
|
+
const isOwner = (session) => getRoleLevel(session) >= 3;
|
|
99
|
+
const isAdmin = (session) => getRoleLevel(session) >= 2;
|
|
100
|
+
const getRoleLevelAsync = async (session) => {
|
|
101
|
+
let level = getRoleLevel(session);
|
|
102
|
+
if (level > 0)
|
|
103
|
+
return level;
|
|
104
|
+
const bot = session === null || session === void 0 ? void 0 : session.bot;
|
|
105
|
+
if ((bot === null || bot === void 0 ? void 0 : bot.getGuildMember) && (session === null || session === void 0 ? void 0 : session.guildId) && (session === null || session === void 0 ? void 0 : session.userId)) {
|
|
106
|
+
try {
|
|
107
|
+
const member = await bot.getGuildMember(session.guildId, session.userId);
|
|
108
|
+
const roles = new Set();
|
|
109
|
+
if ((member === null || member === void 0 ? void 0 : member.role) && typeof member.role === 'string')
|
|
110
|
+
roles.add(member.role);
|
|
111
|
+
if (Array.isArray(member === null || member === void 0 ? void 0 : member.roles)) {
|
|
112
|
+
member.roles.forEach((r) => {
|
|
113
|
+
if (typeof r === 'string')
|
|
114
|
+
roles.add(r);
|
|
115
|
+
else if (r && typeof r === 'object') {
|
|
116
|
+
if (typeof r.id === 'string')
|
|
117
|
+
roles.add(r.id);
|
|
118
|
+
if (typeof r.name === 'string')
|
|
119
|
+
roles.add(r.name);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (roles.has('owner'))
|
|
124
|
+
level = 3;
|
|
125
|
+
else if (roles.has('admin'))
|
|
126
|
+
level = 2;
|
|
127
|
+
else if (roles.has('member'))
|
|
128
|
+
level = 1;
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
132
|
+
return level;
|
|
133
|
+
};
|
|
134
|
+
const requireManage = async (session, channelId) => {
|
|
135
|
+
var _a, _b, _c, _d, _e;
|
|
136
|
+
const level = Number((_a = config.adminAuthority) !== null && _a !== void 0 ? _a : 3);
|
|
137
|
+
if (level <= 1)
|
|
138
|
+
return true;
|
|
139
|
+
if (channelId && channelId !== session.channelId)
|
|
140
|
+
return false;
|
|
141
|
+
const roleLevel = await getRoleLevelAsync(session);
|
|
142
|
+
if (level <= 2)
|
|
143
|
+
return roleLevel >= 2;
|
|
144
|
+
const ok = roleLevel >= 3;
|
|
145
|
+
if (!ok) {
|
|
146
|
+
logger.info(`权限不足调试:level=${level}, role=${(_b = session === null || session === void 0 ? void 0 : session.member) === null || _b === void 0 ? void 0 : _b.role}, roles=${JSON.stringify((_c = session === null || session === void 0 ? void 0 : session.member) === null || _c === void 0 ? void 0 : _c.roles)}, onebotRole=${(_e = (_d = session === null || session === void 0 ? void 0 : session.event) === null || _d === void 0 ? void 0 : _d.sender) === null || _e === void 0 ? void 0 : _e.role}`);
|
|
147
|
+
}
|
|
148
|
+
return ok;
|
|
149
|
+
};
|
|
150
|
+
const parseChannelId = (channelId) => {
|
|
151
|
+
const idx = channelId.indexOf(':');
|
|
152
|
+
if (idx <= 0)
|
|
153
|
+
return null;
|
|
154
|
+
return { platform: channelId.slice(0, idx), id: channelId.slice(idx + 1) };
|
|
155
|
+
};
|
|
156
|
+
const sendToChannel = async (channelId, content) => {
|
|
157
|
+
const parsed = parseChannelId(channelId);
|
|
158
|
+
if (parsed) {
|
|
159
|
+
const bot = ctx.bots.find(b => b.platform === parsed.platform);
|
|
160
|
+
if (bot) {
|
|
161
|
+
await bot.sendMessage(parsed.id, content);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (ctx.bots.length === 1) {
|
|
166
|
+
await ctx.bots[0].sendMessage(channelId, content);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (ctx.bots.length) {
|
|
170
|
+
for (const bot of ctx.bots) {
|
|
171
|
+
try {
|
|
172
|
+
await bot.sendMessage(channelId, content);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
logger.warn(`无法发送到频道 ${channelId},请使用 platform:channelId 格式。`);
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const lastCheckMap = new Map();
|
|
182
|
+
const stateCache = new Map();
|
|
183
|
+
let stateLoaded = false;
|
|
184
|
+
let saving = false;
|
|
185
|
+
let dbWarned = false;
|
|
186
|
+
const getStateKey = (channelId, platform, projectId) => {
|
|
187
|
+
return `${channelId}|${platform}|${projectId}`;
|
|
188
|
+
};
|
|
189
|
+
const resolveStateFile = () => {
|
|
190
|
+
const p = String(config.stateFile || 'data/cfmrmod_notify_state.json');
|
|
191
|
+
return path_1.default.isAbsolute(p) ? p : path_1.default.resolve(process.cwd(), p);
|
|
192
|
+
};
|
|
193
|
+
const loadStateFromFile = async () => {
|
|
194
|
+
if (stateLoaded)
|
|
195
|
+
return;
|
|
196
|
+
stateLoaded = true;
|
|
197
|
+
try {
|
|
198
|
+
const filePath = resolveStateFile();
|
|
199
|
+
const content = await fs_1.promises.readFile(filePath, 'utf8');
|
|
200
|
+
const json = JSON.parse(content);
|
|
201
|
+
if (json && typeof json === 'object') {
|
|
202
|
+
Object.keys(json).forEach(key => {
|
|
203
|
+
const val = json[key];
|
|
204
|
+
if (val && typeof val === 'object')
|
|
205
|
+
stateCache.set(key, { lastVersion: val.lastVersion });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
};
|
|
211
|
+
const saveStateToFile = async () => {
|
|
212
|
+
if (saving)
|
|
213
|
+
return;
|
|
214
|
+
saving = true;
|
|
215
|
+
try {
|
|
216
|
+
const filePath = resolveStateFile();
|
|
217
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
218
|
+
const obj = {};
|
|
219
|
+
for (const [key, val] of stateCache.entries())
|
|
220
|
+
obj[key] = { lastVersion: val.lastVersion };
|
|
221
|
+
await fs_1.promises.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
logger.warn(`状态文件写入失败:${e.message}`);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
saving = false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const getState = async (channelId, platform, projectId) => {
|
|
231
|
+
await loadStateFromFile();
|
|
232
|
+
const key = getStateKey(channelId, platform, projectId);
|
|
233
|
+
return stateCache.get(key) || null;
|
|
234
|
+
};
|
|
235
|
+
const createState = async (channelId, platform, projectId, lastVersion) => {
|
|
236
|
+
await loadStateFromFile();
|
|
237
|
+
const key = getStateKey(channelId, platform, projectId);
|
|
238
|
+
stateCache.set(key, { lastVersion });
|
|
239
|
+
await saveStateToFile();
|
|
240
|
+
};
|
|
241
|
+
const updateState = async (channelId, platform, projectId, lastVersion) => {
|
|
242
|
+
await loadStateFromFile();
|
|
243
|
+
const key = getStateKey(channelId, platform, projectId);
|
|
244
|
+
stateCache.set(key, { lastVersion });
|
|
245
|
+
await saveStateToFile();
|
|
246
|
+
};
|
|
247
|
+
const getConfigGroups = () => Array.isArray(config.groups) ? config.groups : [];
|
|
248
|
+
const getConfigSubs = (channelId) => {
|
|
249
|
+
const groups = getConfigGroups();
|
|
250
|
+
const subs = [];
|
|
251
|
+
for (const group of groups) {
|
|
252
|
+
if (!(group === null || group === void 0 ? void 0 : group.channelId))
|
|
253
|
+
continue;
|
|
254
|
+
if (channelId && group.channelId !== channelId)
|
|
255
|
+
continue;
|
|
256
|
+
if (group.enabled === false)
|
|
257
|
+
continue;
|
|
258
|
+
const list = Array.isArray(group.subs) ? group.subs : [];
|
|
259
|
+
for (const sub of list) {
|
|
260
|
+
const platformKey = normalizePlatform(sub === null || sub === void 0 ? void 0 : sub.platform);
|
|
261
|
+
const projectId = String((sub === null || sub === void 0 ? void 0 : sub.projectId) || '').trim();
|
|
262
|
+
if (!platformKey || !projectId)
|
|
263
|
+
continue;
|
|
264
|
+
const rawInterval = Number(sub === null || sub === void 0 ? void 0 : sub.interval);
|
|
265
|
+
if (Number.isFinite(rawInterval) && rawInterval <= 0)
|
|
266
|
+
continue;
|
|
267
|
+
const interval = Math.max(60 * 1000, rawInterval || Number(config.interval) || 30 * 60 * 1000);
|
|
268
|
+
subs.push({ channelId: String(group.channelId), platform: platformKey, projectId, interval });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return subs;
|
|
272
|
+
};
|
|
273
|
+
const getConfigSubsOrdered = (channelId) => {
|
|
274
|
+
const groups = getConfigGroups();
|
|
275
|
+
const subs = [];
|
|
276
|
+
for (const group of groups) {
|
|
277
|
+
if (!(group === null || group === void 0 ? void 0 : group.channelId))
|
|
278
|
+
continue;
|
|
279
|
+
if (channelId && group.channelId !== channelId)
|
|
280
|
+
continue;
|
|
281
|
+
if (group.enabled === false)
|
|
282
|
+
continue;
|
|
283
|
+
const list = Array.isArray(group.subs) ? group.subs : [];
|
|
284
|
+
for (const sub of list) {
|
|
285
|
+
const platformKey = normalizePlatform(sub === null || sub === void 0 ? void 0 : sub.platform);
|
|
286
|
+
const projectId = String((sub === null || sub === void 0 ? void 0 : sub.projectId) || '').trim();
|
|
287
|
+
if (!platformKey || !projectId)
|
|
288
|
+
continue;
|
|
289
|
+
const rawInterval = Number(sub === null || sub === void 0 ? void 0 : sub.interval);
|
|
290
|
+
if (Number.isFinite(rawInterval) && rawInterval <= 0)
|
|
291
|
+
continue;
|
|
292
|
+
const interval = Math.max(60 * 1000, rawInterval || Number(config.interval) || 30 * 60 * 1000);
|
|
293
|
+
subs.push({ channelId: String(group.channelId), platform: platformKey, projectId, interval });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return subs;
|
|
297
|
+
};
|
|
298
|
+
async function getLatestModrinth(projectId, timeout) {
|
|
299
|
+
const versions = await fetchJson(`${MR_BASE}/project/${projectId}/version`, timeout);
|
|
300
|
+
const latest = Array.isArray(versions) ? versions[0] : null;
|
|
301
|
+
if (!latest)
|
|
302
|
+
return null;
|
|
303
|
+
const file = Array.isArray(latest.files) && latest.files.length ? latest.files[0] : null;
|
|
304
|
+
return {
|
|
305
|
+
versionId: latest.id,
|
|
306
|
+
version: latest.version_number || latest.name || latest.id,
|
|
307
|
+
changelog: latest.changelog || '',
|
|
308
|
+
downloads: latest.downloads,
|
|
309
|
+
datePublished: latest.date_published,
|
|
310
|
+
versionType: latest.version_type,
|
|
311
|
+
loaders: Array.isArray(latest.loaders) ? latest.loaders.map(String) : [],
|
|
312
|
+
gameVersions: Array.isArray(latest.game_versions) ? latest.game_versions.map(String) : [],
|
|
313
|
+
fileName: (file === null || file === void 0 ? void 0 : file.filename) || '',
|
|
314
|
+
fileSize: (file === null || file === void 0 ? void 0 : file.size) || 0,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function getLatestCurseForge(projectId, timeout) {
|
|
318
|
+
var _a;
|
|
319
|
+
const files = await fetchJson(`${CF_MIRROR_BASE}/mods/${projectId}/files?index=0&pageSize=1`, timeout);
|
|
320
|
+
const latest = (_a = files === null || files === void 0 ? void 0 : files.data) === null || _a === void 0 ? void 0 : _a[0];
|
|
321
|
+
if (!latest)
|
|
322
|
+
return null;
|
|
323
|
+
return {
|
|
324
|
+
versionId: String(latest.id),
|
|
325
|
+
version: latest.displayName || latest.fileName || String(latest.id),
|
|
326
|
+
changelog: latest.changelog || '',
|
|
327
|
+
downloads: latest.downloadCount,
|
|
328
|
+
datePublished: latest.fileDate || null,
|
|
329
|
+
releaseType: latest.releaseType,
|
|
330
|
+
loaders: Array.isArray(latest.gameVersions) ? latest.gameVersions.filter((v) => /forge|fabric|quilt|neoforge/i.test(String(v))) : [],
|
|
331
|
+
gameVersions: Array.isArray(latest.gameVersions) ? latest.gameVersions.filter((v) => /\d/.test(String(v))) : [],
|
|
332
|
+
fileName: latest.fileName || '',
|
|
333
|
+
fileSize: latest.fileLength || 0,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function sendUpdate(channelId, platform, projectId, latest) {
|
|
337
|
+
var _a, _b, _c;
|
|
338
|
+
try {
|
|
339
|
+
let detailData;
|
|
340
|
+
if (platform === 'mr')
|
|
341
|
+
detailData = await (0, cfmr_1.fetchModrinthDetail)(projectId, ((_a = options === null || options === void 0 ? void 0 : options.cfmr) === null || _a === void 0 ? void 0 : _a.requestTimeout) || 15000);
|
|
342
|
+
else
|
|
343
|
+
detailData = await (0, cfmr_1.fetchCurseForgeDetail)(projectId, (_b = options === null || options === void 0 ? void 0 : options.cfmr) === null || _b === void 0 ? void 0 : _b.curseforgeApiKey, ((_c = options === null || options === void 0 ? void 0 : options.cfmr) === null || _c === void 0 ? void 0 : _c.requestTimeout) || 15000, null);
|
|
344
|
+
detailData.type = 'mod';
|
|
345
|
+
const imgBufs = detailData.source === 'CurseForge'
|
|
346
|
+
? await (0, cfmr_1.drawProjectCardCFNotify)({ ...detailData }, latest)
|
|
347
|
+
: await (0, cfmr_1.drawProjectCardMRNotify)({ ...detailData }, latest);
|
|
348
|
+
for (const buf of imgBufs) {
|
|
349
|
+
const src = await toImageSrc(buf);
|
|
350
|
+
await sendToChannel(channelId, koishi_1.h.image(src));
|
|
351
|
+
}
|
|
352
|
+
// 仅发送卡片,不发送文字
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
logger.warn(`发送通知失败(${platform}:${projectId}): ${e.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function checkOnce(channelId, force = false) {
|
|
359
|
+
var _a;
|
|
360
|
+
if (!config.enabled)
|
|
361
|
+
return;
|
|
362
|
+
const subs = getConfigSubs(channelId);
|
|
363
|
+
const stats = { checked: 0, updated: 0, noChange: 0, skipped: 0, failed: 0 };
|
|
364
|
+
for (const sub of subs) {
|
|
365
|
+
try {
|
|
366
|
+
const key = `${sub.channelId}|${sub.platform}|${sub.projectId}`;
|
|
367
|
+
const lastCheck = lastCheckMap.get(key) || 0;
|
|
368
|
+
if (!force && Date.now() - lastCheck < sub.interval) {
|
|
369
|
+
stats.skipped += 1;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
lastCheckMap.set(key, Date.now());
|
|
373
|
+
stats.checked += 1;
|
|
374
|
+
const timeout = ((_a = options === null || options === void 0 ? void 0 : options.cfmr) === null || _a === void 0 ? void 0 : _a.requestTimeout) || 15000;
|
|
375
|
+
const latest = sub.platform === 'mr'
|
|
376
|
+
? await getLatestModrinth(sub.projectId, timeout)
|
|
377
|
+
: await getLatestCurseForge(sub.projectId, timeout);
|
|
378
|
+
if (!latest) {
|
|
379
|
+
stats.failed += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const state = await getState(sub.channelId, sub.platform, sub.projectId);
|
|
383
|
+
if (!state) {
|
|
384
|
+
await createState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
385
|
+
stats.noChange += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (!state.lastVersion) {
|
|
389
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version);
|
|
390
|
+
stats.noChange += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (latest.version === state.lastVersion) {
|
|
394
|
+
stats.noChange += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
await sendUpdate(sub.channelId, sub.platform, sub.projectId, latest);
|
|
398
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version);
|
|
399
|
+
stats.updated += 1;
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
logger.warn(`检查失败(${sub.platform}:${sub.projectId}): ${e.message}`);
|
|
403
|
+
stats.failed += 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return stats;
|
|
407
|
+
}
|
|
408
|
+
const checkOne = async (sub, forceSendAll) => {
|
|
409
|
+
var _a;
|
|
410
|
+
const timeout = ((_a = options === null || options === void 0 ? void 0 : options.cfmr) === null || _a === void 0 ? void 0 : _a.requestTimeout) || 15000;
|
|
411
|
+
const latest = sub.platform === 'mr'
|
|
412
|
+
? await getLatestModrinth(sub.projectId, timeout)
|
|
413
|
+
: await getLatestCurseForge(sub.projectId, timeout);
|
|
414
|
+
if (!latest)
|
|
415
|
+
return { sent: false, updated: false };
|
|
416
|
+
const state = await getState(sub.channelId, sub.platform, sub.projectId);
|
|
417
|
+
if (!state) {
|
|
418
|
+
await createState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
419
|
+
if (forceSendAll) {
|
|
420
|
+
await sendUpdate(sub.channelId, sub.platform, sub.projectId, latest);
|
|
421
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
422
|
+
return { sent: true, updated: true };
|
|
423
|
+
}
|
|
424
|
+
return { sent: false, updated: false };
|
|
425
|
+
}
|
|
426
|
+
if (forceSendAll) {
|
|
427
|
+
await sendUpdate(sub.channelId, sub.platform, sub.projectId, latest);
|
|
428
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
429
|
+
return { sent: true, updated: true };
|
|
430
|
+
}
|
|
431
|
+
if (!state.lastVersion) {
|
|
432
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
433
|
+
return { sent: false, updated: false };
|
|
434
|
+
}
|
|
435
|
+
if (latest.version === state.lastVersion)
|
|
436
|
+
return { sent: false, updated: false };
|
|
437
|
+
await sendUpdate(sub.channelId, sub.platform, sub.projectId, latest);
|
|
438
|
+
await updateState(sub.channelId, sub.platform, sub.projectId, latest.version || '');
|
|
439
|
+
return { sent: true, updated: true };
|
|
440
|
+
};
|
|
441
|
+
if (config.enabled) {
|
|
442
|
+
const tick = Math.max(60 * 1000, Number(config.interval) || 30 * 60 * 1000);
|
|
443
|
+
ctx.setInterval(() => checkOnce().catch(() => null), tick);
|
|
444
|
+
}
|
|
445
|
+
ctx.command('notify.add <platform> <projectId> [channelId]', '添加更新订阅')
|
|
446
|
+
.action(async ({ session }, platform, projectId, channelId) => {
|
|
447
|
+
if (!platform || !projectId)
|
|
448
|
+
return '参数不足。';
|
|
449
|
+
const targetChannel = channelId || session.channelId;
|
|
450
|
+
if (!targetChannel)
|
|
451
|
+
return '只能在群聊使用或指定 channelId。';
|
|
452
|
+
if (!await requireManage(session, channelId))
|
|
453
|
+
return '权限不足。';
|
|
454
|
+
return '请在配置页面的 notify.groups 中编辑订阅列表。';
|
|
455
|
+
});
|
|
456
|
+
ctx.command('notify.remove <platform> <projectId> [channelId]', '删除更新订阅')
|
|
457
|
+
.action(async ({ session }, platform, projectId, channelId) => {
|
|
458
|
+
if (!platform || !projectId)
|
|
459
|
+
return '参数不足。';
|
|
460
|
+
const targetChannel = channelId || session.channelId;
|
|
461
|
+
if (!targetChannel)
|
|
462
|
+
return '只能在群聊使用或指定 channelId。';
|
|
463
|
+
if (!await requireManage(session, channelId))
|
|
464
|
+
return '权限不足。';
|
|
465
|
+
return '请在配置页面的 notify.groups 中编辑订阅列表。';
|
|
466
|
+
});
|
|
467
|
+
ctx.command('notify.list [channelId]', '列出订阅')
|
|
468
|
+
.action(async ({ session }, channelId) => {
|
|
469
|
+
const targetChannel = channelId || session.channelId;
|
|
470
|
+
if (!targetChannel)
|
|
471
|
+
return '只能在群聊使用或指定 channelId。';
|
|
472
|
+
if (!await requireManage(session, channelId))
|
|
473
|
+
return '权限不足。';
|
|
474
|
+
const subs = getConfigSubs(targetChannel);
|
|
475
|
+
if (!subs.length)
|
|
476
|
+
return '暂无订阅。';
|
|
477
|
+
return subs.map(s => `- ${s.platform}:${s.projectId} (${Math.round(s.interval / 60000)} 分钟)`).join('\n');
|
|
478
|
+
});
|
|
479
|
+
ctx.command('notify.enable <onoff> [channelId]', '启用/禁用本群通知')
|
|
480
|
+
.action(async ({ session }, onoff, channelId) => {
|
|
481
|
+
const targetChannel = channelId || session.channelId;
|
|
482
|
+
if (!targetChannel)
|
|
483
|
+
return '只能在群聊使用或指定 channelId。';
|
|
484
|
+
if (!await requireManage(session, channelId))
|
|
485
|
+
return '权限不足。';
|
|
486
|
+
return '请在配置页面的 notify.groups 中编辑 enabled。';
|
|
487
|
+
});
|
|
488
|
+
ctx.command('notify.check [arg]', '手动检查更新')
|
|
489
|
+
.option('broadcast', '-b 直接发送最新版卡片(忽略是否更新)')
|
|
490
|
+
.action(async ({ session, options }, arg) => {
|
|
491
|
+
const targetChannel = session.channelId;
|
|
492
|
+
if (!targetChannel)
|
|
493
|
+
return '只能在群聊使用或指定 channelId。';
|
|
494
|
+
if (!await requireManage(session))
|
|
495
|
+
return '权限不足。';
|
|
496
|
+
const list = getConfigSubsOrdered(targetChannel);
|
|
497
|
+
if (!list.length)
|
|
498
|
+
return '暂无订阅。';
|
|
499
|
+
let targets = list;
|
|
500
|
+
if (arg) {
|
|
501
|
+
const idx = Number(arg);
|
|
502
|
+
if (Number.isFinite(idx) && idx > 0) {
|
|
503
|
+
const sub = list[idx - 1];
|
|
504
|
+
if (!sub)
|
|
505
|
+
return '未找到对应序号的订阅。';
|
|
506
|
+
targets = [sub];
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
const sub = list.find(s => s.projectId === String(arg));
|
|
510
|
+
if (!sub)
|
|
511
|
+
return '未找到对应项目 ID 的订阅。';
|
|
512
|
+
targets = [sub];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let sent = 0;
|
|
516
|
+
for (const sub of targets) {
|
|
517
|
+
try {
|
|
518
|
+
const res = await checkOne(sub, !!(options === null || options === void 0 ? void 0 : options.broadcast));
|
|
519
|
+
if (res.sent)
|
|
520
|
+
sent += 1;
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
logger.warn(`检查失败(${sub.platform}:${sub.projectId}): ${e.message}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!sent)
|
|
527
|
+
return '暂无更新。';
|
|
528
|
+
});
|
|
529
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cfmrmod",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"pub": "npm run build && npm publish --access public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"koishi": "^4.0.0"
|
|
42
|
+
"koishi": "^4.0.0",
|
|
43
|
+
"@ltxhhz/koishi-plugin-skia-canvas": "^0.0.10"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@napi-rs/canvas": "^0.1.55",
|
|
46
46
|
"cheerio": "^1.0.0-rc.12",
|
|
47
47
|
"marked": "^9.1.5",
|
|
48
48
|
"node-fetch": "^2.6.12"
|
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
"koishi": {
|
|
54
54
|
"description": {
|
|
55
55
|
"zh": "从 CurseForge/Modrinth/MCMod 搜索模组/整合包/光影等内容,并生成图片卡片。"
|
|
56
|
+
},
|
|
57
|
+
"service": {
|
|
58
|
+
"required": ["skia", "database"]
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|