koishi-plugin-cfmrmod 1.1.3 → 1.1.5

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
@@ -19,6 +19,15 @@ Star就是我维护的动力🤤
19
19
  - `cnmc.mod/.data/.pack/.tutorial/.author/.user <关键词>`
20
20
  - `cf.help` / `mr.help` / `cnmc.help`
21
21
 
22
+ #### AI 自然语言查询(可选)
23
+ 开启 `nlu.enabled` 并配置 OpenAI 兼容接口后,只有 `@机器人` 的消息会进入自然语言理解;不 @ 机器人时仍只响应上面的显式命令。
24
+
25
+ 示例:
26
+ - `@机器人 查询钠模组`:默认在 MCMod 查询 Mod
27
+ - `@机器人 在 cf 查一下 jei`:在 CurseForge 查询 Mod
28
+ - `@机器人 在 mr 查一下 iris 光影`:在 Modrinth 查询 Shader
29
+ - `@机器人 在 cnmc 查一下 Create: EasyFilling`:在 MCMod 查询 Mod
30
+
22
31
  #### 更新通知(notify)
23
32
  - `notify.add <platform> <projectId>` 添加订阅
24
33
  - `notify.remove <platform> <projectId>` 删除订阅
@@ -42,6 +51,20 @@ Star就是我维护的动力🤤
42
51
  - `timeouts`: 搜索会话超时(毫秒)
43
52
  - `debug`: 调试日志开关
44
53
 
54
+ #### AI 自然语言理解(nlu)
55
+ - `nlu.enabled`: 是否启用 `@机器人` 自然语言查询
56
+ - `nlu.endpoint`: OpenAI 兼容 Chat Completions 接口地址,默认 `https://api.openai.com/v1/chat/completions`
57
+ - `nlu.apiKey`: API Key
58
+ - `nlu.model`: 模型名称,默认 `gpt-4o-mini`
59
+ - `nlu.timeout`: AI 请求超时(毫秒)
60
+ - `nlu.temperature`: AI 温度参数,默认 `0`
61
+
62
+ #### MCMod(mcmod)
63
+ - `mcmod.cookie`: 手动填写 mcmod.cn Cookie
64
+ - `mcmod.autoCookie`: 自动从 `cookie-manager` 获取 Cookie(存在该模块时生效)
65
+ - `mcmod.cookieCheckInterval`: Cookie / `MCMOD_SEED` 检查间隔(毫秒)
66
+ - 未配置 Cookie 时,插件会自动访问 MCMod 首页获取 `MCMOD_SEED`,用于通过站点的基础 Cookie 校验。
67
+
45
68
  #### 更新通知(notify)
46
69
  - `notify.enabled`: 是否开启更新通知
47
70
  - `notify.interval`: 全局轮询间隔(毫秒)
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ 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 nlu = __importStar(require("./nlu"));
41
42
  const notify = __importStar(require("./notify"));
42
43
  exports.name = 'minecraft-search';
43
44
  exports.inject = ['database'];
@@ -65,6 +66,7 @@ exports.Config = koishi_1.Schema.object({
65
66
  }).description('—— 更新通知 ——'),
66
67
  timeouts: koishi_1.Schema.number().default(60000).description('搜索会话超时时间(ms)'),
67
68
  debug: koishi_1.Schema.boolean().default(false).description('开启调试日志'),
69
+ nlu: nlu.Config,
68
70
  cfmr: cfmr.Config.description('CurseForge/Modrinth 搜索与图片卡片'),
69
71
  mcmod: mcmod.Config.description('MCMod.cn 搜索与图片卡片'),
70
72
  });
@@ -98,6 +100,8 @@ function apply(ctx, config) {
98
100
  cfmr.apply(ctx, { ...((config === null || config === void 0 ? void 0 : config.cfmr) || {}), ...shared });
99
101
  if (mcmod.apply)
100
102
  mcmod.apply(ctx, { ...((config === null || config === void 0 ? void 0 : config.mcmod) || {}), ...shared });
103
+ if (nlu.apply)
104
+ nlu.apply(ctx, (config === null || config === void 0 ? void 0 : config.nlu) || {}, shared);
101
105
  if (notify.apply && canvasAdapter)
102
106
  notify.apply(ctx, (config === null || config === void 0 ? void 0 : config.notify) || {}, { cfmr: (config === null || config === void 0 ? void 0 : config.cfmr) || {} });
103
107
  if (!canvasAdapter)
@@ -12,27 +12,26 @@ async function drawModCard(url) {
12
12
  const html = await (0, http_1.fetchMcmodText)(url, { headers: (0, http_1.getHeaders)(url) });
13
13
  const $ = cheerio.load(html);
14
14
  // --- 1. 数据抓取 (保持原逻辑,确保稳定性) ---
15
- const titleEl = $('.class-title').clone();
16
- titleEl.find('.class-official-group').remove();
17
- const titleHtml = titleEl.html() || '';
18
- const cleanTitleStr = titleHtml.replace(/<[^>]+>/g, '\n');
19
- const titleLines = cleanTitleStr.split('\n').map(s => s.trim()).filter(s => s);
20
- const title = titleLines[0] || (0, utils_1.cleanText)($('.class-title').text().replace(/开源|活跃|稳定|闭源|停更|弃坑|半弃坑|Beta/g, '').trim());
21
- const subTitle = titleLines.slice(1).join(' ');
22
- let coverUrl = (0, utils_1.fixUrl)($('.class-cover-image img').attr('src'));
23
- let iconUrl = (0, utils_1.fixUrl)($('.class-icon img').attr('src'));
24
- // 如果没有封面,用图标代替;如果没有图标,尝试用封面代替
25
- if (!coverUrl && iconUrl)
26
- coverUrl = iconUrl;
27
- if (!iconUrl && coverUrl)
28
- iconUrl = coverUrl;
15
+ const titleRoot = $('.class-title').first();
16
+ const title = (0, utils_1.cleanText)(titleRoot.find('h1,h2,h3').first().text()) ||
17
+ (0, utils_1.cleanText)(($('meta[property="og:title"]').attr('content') || $('title').text()).split('-')[0]);
18
+ const subTitle = titleRoot.find('h4,small,.sub-title,.subtitle').map((_, el) => (0, utils_1.cleanText)($(el).text())).get()
19
+ .filter(text => text && text !== title)
20
+ .join(' ');
21
+ const coverNode = $('.class-cover-image img, .class-banner img').first()[0];
22
+ const iconNode = $('.class-icon img, .class-logo img, .class-cover-icon img').first()[0];
23
+ const hasDedicatedIcon = !!iconNode;
24
+ let coverUrl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)(coverNode));
25
+ let iconUrl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)(iconNode)) || coverUrl;
29
26
  // 标签
30
27
  const tags = [];
31
28
  const officialTags = new Set();
32
- $('.class-official-group div').each((i, el) => {
29
+ const seenTags = new Set();
30
+ $('.class-title .class-status, .class-official-group div').each((i, el) => {
33
31
  const txt = (0, utils_1.cleanText)($(el).text());
34
- if (!txt || txt.length > 20)
32
+ if (!txt || txt.length > 20 || seenTags.has(txt))
35
33
  return;
34
+ seenTags.add(txt);
36
35
  officialTags.add(txt);
37
36
  let color = '#999', bg = '#eee';
38
37
  if (txt.includes('开源') || txt.includes('活跃') || txt.includes('稳定')) {
@@ -51,8 +50,9 @@ async function drawModCard(url) {
51
50
  });
52
51
  $('.class-label-list a').each((i, el) => {
53
52
  const labelText = (0, utils_1.cleanText)($(el).text());
54
- if (!labelText || officialTags.has(labelText))
53
+ if (!labelText || officialTags.has(labelText) || seenTags.has(labelText))
55
54
  return;
55
+ seenTags.add(labelText);
56
56
  const cls = $(el).attr('class') || '';
57
57
  let bg = '#e3f2fd', c = '#3498db';
58
58
  if (cls.includes('c_1')) {
@@ -112,10 +112,10 @@ async function drawModCard(url) {
112
112
  const subNum = getSocialNum('subscribe');
113
113
  // 作者
114
114
  const authors = [];
115
- $('.author-list li, .author li').each((i, el) => {
116
- const n = (0, utils_1.cleanText)($(el).find('.name').text());
115
+ $('.author li, .author-list li, .class-author-list li, .common-class-author li').each((i, el) => {
116
+ const n = (0, utils_1.cleanText)($(el).find('.name a, .name, .member a').first().text()) || (0, utils_1.cleanText)($(el).attr('title'));
117
117
  const r = (0, utils_1.cleanText)($(el).find('.position').text());
118
- const iurl = (0, utils_1.fixUrl)($(el).find('img').attr('src'));
118
+ const iurl = (0, utils_1.fixUrl)((0, utils_1.extractImageUrl)($(el).find('.avatar img, img').first()[0]));
119
119
  if (n)
120
120
  authors.push({ n, r, i: iurl });
121
121
  });
@@ -296,6 +296,29 @@ async function drawModCard(url) {
296
296
  if (metaDesc)
297
297
  descNodes.push({ type: 't', val: metaDesc, tag: 'p' });
298
298
  }
299
+ const loadOptionalImage = async (src) => {
300
+ if (!src)
301
+ return null;
302
+ try {
303
+ return await (0, rendering_1.loadImageWithHeaders)(src, url, 18000);
304
+ }
305
+ catch (e) {
306
+ try {
307
+ return await (0, rendering_1.loadImageWithHeaders)(src, constants_1.BASE_URL, 18000);
308
+ }
309
+ catch (e2) {
310
+ return null;
311
+ }
312
+ }
313
+ };
314
+ const coverImg = await loadOptionalImage(coverUrl);
315
+ let iconImg = await loadOptionalImage(iconUrl);
316
+ if (!iconImg && iconUrl === coverUrl && coverImg)
317
+ iconImg = coverImg;
318
+ const showCover = !!coverImg && (hasDedicatedIcon || coverUrl !== iconUrl);
319
+ await Promise.all(authors.slice(0, 3).map(async (author) => {
320
+ author.imgCache = await loadOptionalImage(author.i);
321
+ }));
299
322
  // --- 2. 布局计算 (macOS 风格) ---
300
323
  const width = 800;
301
324
  const font = rendering_1.GLOBAL_FONT_FAMILY;
@@ -307,17 +330,30 @@ async function drawModCard(url) {
307
330
  const dummy = dummyC.getContext('2d');
308
331
  dummy.font = `bold 32px "${font}"`;
309
332
  // 头部区域 (Header)
310
- let headerH = 100; // Icon(80) + padding
311
- const titleLinesNum = (0, rendering_1.wrapText)(dummy, title, 0, 0, contentW - 100, 40, 10, false) / 40;
312
- headerH = Math.max(headerH, 10 + titleLinesNum * 40 + (subTitle ? 25 : 0) + (authors.length ? 40 : 0));
333
+ const iconSize = 88;
334
+ const titleAreaW = contentW - iconSize - 24;
335
+ const titleLinesNum = (0, rendering_1.wrapText)(dummy, title, 0, 0, titleAreaW, 40, 10, false) / 40;
336
+ let headerH = Math.max(iconSize, titleLinesNum * 40 + (subTitle ? 26 : 0) + (authors.length ? 32 : 0) + 8);
313
337
  // 标签区域
314
338
  let tagsH = 0;
315
- if (tags.length)
316
- tagsH = 40;
339
+ if (tags.length) {
340
+ dummy.font = `12px "${font}"`;
341
+ let rowW = 0;
342
+ let rows = 1;
343
+ for (const tag of tags) {
344
+ const tagW = dummy.measureText(tag.t).width + 20;
345
+ if (rowW && rowW + tagW > contentW) {
346
+ rows++;
347
+ rowW = 0;
348
+ }
349
+ rowW += tagW + 10;
350
+ }
351
+ tagsH = rows * 28;
352
+ }
317
353
  // 封面图 (Cover)
318
354
  let coverH = 0;
319
- if (coverUrl)
320
- coverH = 300; // 固定封面显示高度
355
+ if (showCover)
356
+ coverH = 220;
321
357
  // 统计数据 (Stats Grid)
322
358
  // 布局:每行4个数据
323
359
  const statsItems = [
@@ -351,8 +387,20 @@ async function drawModCard(url) {
351
387
  extraH += lines * 20 + 10;
352
388
  });
353
389
  }
354
- if (links.length)
355
- extraH += 50;
390
+ if (links.length) {
391
+ dummy.font = `bold 12px "${font}"`;
392
+ let rowW = 0;
393
+ let rows = 1;
394
+ for (const link of links) {
395
+ const linkW = dummy.measureText(link).width + 20;
396
+ if (rowW && rowW + linkW > contentW) {
397
+ rows++;
398
+ rowW = 0;
399
+ }
400
+ rowW += linkW + 10;
401
+ }
402
+ extraH += rows * 30 + 12;
403
+ }
356
404
  // 简介 (Desc)
357
405
  let descH = 0;
358
406
  dummy.font = `16px "${font}"`;
@@ -421,8 +469,8 @@ async function drawModCard(url) {
421
469
  // 总高度
422
470
  let cursorY = margin + 40; // Top traffic lights area
423
471
  const components = [
424
- { h: tagsH, gap: 10 },
425
- { h: headerH, gap: 20 },
472
+ { h: headerH, gap: 16 },
473
+ { h: tagsH, gap: 20 },
426
474
  { h: coverH, gap: 25 },
427
475
  { h: statsH, gap: 25 },
428
476
  { h: propsH, gap: 25 },
@@ -432,7 +480,7 @@ async function drawModCard(url) {
432
480
  components.forEach(c => { if (c.h > 0)
433
481
  cursorY += c.h + c.gap; });
434
482
  const windowH = cursorY;
435
- const totalH = windowH + margin * 2;
483
+ const totalH = windowH + margin * 2 + 24;
436
484
  // --- 3. 开始绘制 ---
437
485
  const canvas = (0, rendering_1.createCanvas)(width, totalH);
438
486
  const ctx = canvas.getContext('2d');
@@ -479,49 +527,50 @@ async function drawModCard(url) {
479
527
  // --- 内容绘制 ---
480
528
  let dy = winY + 50;
481
529
  const cx = winX + winPadding;
482
- // 1. Tags
483
- if (tags.length) {
484
- let tx = cx;
485
- ctx.textBaseline = 'middle'; // Fix tag text centering
486
- tags.forEach(t => {
487
- ctx.font = `12px "${font}"`;
488
- const tw = ctx.measureText(t.t).width + 20;
489
- if (tx + tw < cx + contentW) {
490
- ctx.fillStyle = t.bg;
491
- (0, rendering_1.roundRect)(ctx, tx, dy, tw, 24, 6);
492
- ctx.fill();
493
- ctx.fillStyle = t.c;
494
- ctx.fillText(t.t, tx + 10, dy + 12);
495
- tx += tw + 10;
496
- }
497
- });
498
- ctx.textBaseline = 'alphabetic'; // reset
499
- dy += 35;
500
- }
501
- // 2. Header
502
- // Icon
503
- const iconSize = 80;
504
- if (iconUrl) {
505
- try {
506
- const img = await (0, rendering_1.loadImageWithHeaders)(iconUrl, constants_1.BASE_URL);
507
- ctx.save();
508
- (0, rendering_1.roundRect)(ctx, cx, dy, iconSize, iconSize, 12);
509
- ctx.clip();
510
- ctx.drawImage(img, cx, dy, iconSize, iconSize);
511
- ctx.restore();
512
- }
513
- catch (e) {
514
- ctx.fillStyle = '#ddd';
515
- (0, rendering_1.roundRect)(ctx, cx, dy, iconSize, iconSize, 12);
516
- ctx.fill();
517
- }
518
- }
530
+ const getInitials = (text) => {
531
+ var _a;
532
+ const value = (0, utils_1.cleanText)(text).replace(/^\[[^\]]+\]\s*/, '');
533
+ if (!value)
534
+ return 'MOD';
535
+ const ascii = (_a = value.match(/[A-Za-z0-9]+/g)) === null || _a === void 0 ? void 0 : _a.join('').slice(0, 2);
536
+ if (ascii)
537
+ return ascii.toUpperCase();
538
+ return Array.from(value).slice(0, 2).join('');
539
+ };
540
+ const drawImageCover = (img, x, y, w, h, radius) => {
541
+ const scale = Math.max(w / img.width, h / img.height);
542
+ ctx.save();
543
+ (0, rendering_1.roundRect)(ctx, x, y, w, h, radius);
544
+ ctx.clip();
545
+ ctx.drawImage(img, x + (w - img.width * scale) / 2, y + (h - img.height * scale) / 2, img.width * scale, img.height * scale);
546
+ ctx.restore();
547
+ };
548
+ const drawInitialTile = (x, y, w, h, label, radius = 12) => {
549
+ const grad = ctx.createLinearGradient(x, y, x + w, y + h);
550
+ grad.addColorStop(0, '#4b5563');
551
+ grad.addColorStop(1, '#111827');
552
+ ctx.fillStyle = grad;
553
+ (0, rendering_1.roundRect)(ctx, x, y, w, h, radius);
554
+ ctx.fill();
555
+ ctx.fillStyle = '#fff';
556
+ ctx.font = `bold ${Math.max(18, Math.floor(w * 0.32))}px "${font}"`;
557
+ ctx.textAlign = 'center';
558
+ ctx.textBaseline = 'middle';
559
+ ctx.fillText(getInitials(label), x + w / 2, y + h / 2);
560
+ ctx.textAlign = 'left';
561
+ ctx.textBaseline = 'alphabetic';
562
+ };
563
+ // 1. Header
564
+ if (iconImg)
565
+ drawImageCover(iconImg, cx, dy, iconSize, iconSize, 12);
566
+ else
567
+ drawInitialTile(cx, dy, iconSize, iconSize, title, 12);
519
568
  // Title
520
- const titleX = cx + iconSize + 20;
569
+ const titleX = cx + iconSize + 24;
521
570
  ctx.fillStyle = '#333';
522
571
  ctx.font = `bold 32px "${font}"`;
523
572
  ctx.textBaseline = 'top';
524
- const titleDrawnH = (0, rendering_1.wrapText)(ctx, title, titleX, dy - 5, contentW - iconSize - 20, 40, 3, true);
573
+ const titleDrawnH = (0, rendering_1.wrapText)(ctx, title, titleX, dy - 4, titleAreaW, 40, 3, true);
525
574
  // SubTitle
526
575
  let subY = titleDrawnH + 5;
527
576
  if (subTitle) {
@@ -538,44 +587,60 @@ async function drawModCard(url) {
538
587
  ctx.beginPath();
539
588
  ctx.arc(ax + 12, subY + 12, 12, 0, Math.PI * 2);
540
589
  ctx.clip();
541
- if (a.i) {
542
- try {
543
- const img = await (0, rendering_1.loadImageWithHeaders)(a.i, constants_1.BASE_URL);
544
- ctx.drawImage(img, ax, subY, 24, 24);
545
- }
546
- catch (e) {
547
- ctx.fillStyle = '#ccc';
548
- ctx.fill();
549
- }
550
- }
590
+ if (a.imgCache)
591
+ ctx.drawImage(a.imgCache, ax, subY, 24, 24);
551
592
  else {
552
- ctx.fillStyle = '#ccc';
553
- ctx.fill();
593
+ ctx.fillStyle = '#d1d5db';
594
+ ctx.fillRect(ax, subY, 24, 24);
554
595
  }
555
596
  ctx.restore();
597
+ if (!a.imgCache) {
598
+ ctx.fillStyle = '#6b7280';
599
+ ctx.font = `bold 11px "${font}"`;
600
+ ctx.textAlign = 'center';
601
+ ctx.textBaseline = 'middle';
602
+ ctx.fillText(getInitials(a.n).slice(0, 1), ax + 12, subY + 12);
603
+ ctx.textAlign = 'left';
604
+ ctx.textBaseline = 'alphabetic';
605
+ }
556
606
  ctx.fillStyle = '#666';
557
607
  ctx.font = `14px "${font}"`;
558
- ctx.fillText(a.n, ax + 30, subY + 5);
559
- ax += ctx.measureText(a.n).width + 45;
608
+ let name = a.n;
609
+ while (ctx.measureText(name).width > 130 && name.length > 2)
610
+ name = `${name.slice(0, -2)}...`;
611
+ if (ax + 30 + ctx.measureText(name).width > cx + contentW)
612
+ break;
613
+ ctx.fillText(name, ax + 30, subY + 5);
614
+ ax += ctx.measureText(name).width + 45;
560
615
  }
561
616
  }
562
- dy += Math.max(headerH, 100) + 20;
617
+ dy += headerH + 16;
618
+ // 2. Tags
619
+ if (tags.length) {
620
+ let tx = cx;
621
+ let ty = dy;
622
+ ctx.textBaseline = 'middle';
623
+ tags.forEach(t => {
624
+ ctx.font = `12px "${font}"`;
625
+ const tw = ctx.measureText(t.t).width + 20;
626
+ if (tx !== cx && tx + tw > cx + contentW) {
627
+ tx = cx;
628
+ ty += 28;
629
+ }
630
+ ctx.fillStyle = t.bg;
631
+ (0, rendering_1.roundRect)(ctx, tx, ty, tw, 24, 6);
632
+ ctx.fill();
633
+ ctx.fillStyle = t.c;
634
+ ctx.fillText(t.t, tx + 10, ty + 12);
635
+ tx += tw + 10;
636
+ });
637
+ ctx.textBaseline = 'alphabetic';
638
+ dy += tagsH + 20;
639
+ }
563
640
  // 3. Cover Image
564
- if (coverUrl) {
565
- try {
566
- const img = await (0, rendering_1.loadImageWithHeaders)(coverUrl, constants_1.BASE_URL);
567
- const coverW = contentW;
568
- const coverH_Actual = 280;
569
- // Crop fit
570
- const r = Math.max(coverW / img.width, coverH_Actual / img.height);
571
- ctx.save();
572
- (0, rendering_1.roundRect)(ctx, cx, dy, coverW, coverH_Actual, 12);
573
- ctx.clip();
574
- ctx.drawImage(img, (coverW - img.width * r) / 2 + cx, (coverH_Actual - img.height * r) / 2 + dy, img.width * r, img.height * r);
575
- ctx.restore();
576
- dy += coverH_Actual + 25;
577
- }
578
- catch (e) { }
641
+ if (showCover) {
642
+ drawImageCover(coverImg, cx, dy, contentW, coverH, 12);
643
+ dy += coverH + 25;
579
644
  }
580
645
  // 4. Stats Grid
581
646
  if (statsItems.length) {
@@ -644,19 +709,24 @@ async function drawModCard(url) {
644
709
  }
645
710
  if (links.length) {
646
711
  let lx = cx;
712
+ let ly = dy;
713
+ ctx.textBaseline = 'middle';
647
714
  links.forEach(l => {
648
715
  ctx.font = `bold 12px "${font}"`;
649
716
  const w = ctx.measureText(l).width + 20;
650
- if (lx + w < cx + contentW) {
651
- ctx.fillStyle = '#333';
652
- (0, rendering_1.roundRect)(ctx, lx, dy, w, 24, 12);
653
- ctx.fill();
654
- ctx.fillStyle = '#fff';
655
- ctx.fillText(l, lx + 10, dy + 6);
656
- lx += w + 10;
717
+ if (lx !== cx && lx + w > cx + contentW) {
718
+ lx = cx;
719
+ ly += 30;
657
720
  }
721
+ ctx.fillStyle = '#333';
722
+ (0, rendering_1.roundRect)(ctx, lx, ly, w, 24, 12);
723
+ ctx.fill();
724
+ ctx.fillStyle = '#fff';
725
+ ctx.fillText(l, lx + 10, ly + 12);
726
+ lx += w + 10;
658
727
  });
659
- dy += 45;
728
+ ctx.textBaseline = 'alphabetic';
729
+ dy += Math.ceil((ly - dy + 24) / 30) * 30 + 12;
660
730
  }
661
731
  // 7. Description
662
732
  if (descNodes.length) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setMcmodCookie = setMcmodCookie;
4
+ exports.configureMcmodCookie = configureMcmodCookie;
4
5
  exports.getMcmodCookie = getMcmodCookie;
5
6
  exports.loadManagedCookie = loadManagedCookie;
6
7
  exports.fetchWithTimeout = fetchWithTimeout;
@@ -20,7 +21,8 @@ catch (e) {
20
21
  }
21
22
  let globalCookie = '';
22
23
  let cookieLastCheck = 0;
23
- const COOKIE_CHECK_INTERVAL = 30 * 60 * 1000;
24
+ let cookieCheckInterval = 30 * 60 * 1000;
25
+ let useManagedCookie = false;
24
26
  function mergeCookie(name, value) {
25
27
  if (!name || !value)
26
28
  return;
@@ -66,12 +68,22 @@ function setMcmodCookie(cookie, checkedAt = Date.now()) {
66
68
  globalCookie = String(cookie || '');
67
69
  cookieLastCheck = checkedAt;
68
70
  }
71
+ function configureMcmodCookie(options = {}) {
72
+ useManagedCookie = !!options.autoCookie;
73
+ const interval = Number(options.checkInterval);
74
+ if (Number.isFinite(interval) && interval > 0)
75
+ cookieCheckInterval = interval;
76
+ if (options.cookie !== undefined) {
77
+ setMcmodCookie(options.cookie || '', options.cookie ? Date.now() : 0);
78
+ }
79
+ }
69
80
  function getMcmodCookie() {
70
81
  return globalCookie;
71
82
  }
72
83
  function loadManagedCookie(logger) {
73
84
  if (!cookieManager)
74
85
  return;
86
+ useManagedCookie = true;
75
87
  cookieManager.getCookie().then(cookie => {
76
88
  var _a;
77
89
  if (cookie) {
@@ -125,10 +137,10 @@ function getImageHeaders(url, referer = `${constants_1.BASE_URL}/`) {
125
137
  }
126
138
  async function ensureValidCookie() {
127
139
  const now = Date.now();
128
- if (hasCookie('MCMOD_SEED') && (now - cookieLastCheck) < COOKIE_CHECK_INTERVAL) {
140
+ if (hasCookie('MCMOD_SEED') && (now - cookieLastCheck) < cookieCheckInterval) {
129
141
  return;
130
142
  }
131
- if (cookieManager) {
143
+ if (useManagedCookie && cookieManager) {
132
144
  try {
133
145
  const cookie = await cookieManager.getCookie();
134
146
  if (cookie) {
@@ -16,6 +16,8 @@ exports.name = 'mcmod-search';
16
16
  exports.Config = Schema.object({
17
17
  sendLink: Schema.boolean().default(true).description('发送卡片后是否附带链接'),
18
18
  cookie: Schema.string().description('【可选】手动填写 mcmod.cn 的 Cookie'),
19
+ autoCookie: Schema.boolean().default(false).description('自动从 cookie-manager 获取 mcmod.cn Cookie(存在该模块时生效)'),
20
+ cookieCheckInterval: Schema.number().default(30 * 60 * 1000).description('Cookie/Seed 检查间隔(ms)'),
19
21
  fontPath: Schema.string().role('path').description('可选:自定义字体文件路径'),
20
22
  debug: Schema.boolean().default(false).description('输出渲染调试日志'),
21
23
  render: Schema.object({
@@ -34,9 +36,13 @@ function apply(ctx, config) {
34
36
  if (!(0, rendering_1.configureRenderer)(config === null || config === void 0 ? void 0 : config.canvas, config, logger)) {
35
37
  return;
36
38
  }
39
+ (0, http_1.configureMcmodCookie)({
40
+ cookie: config.cookie,
41
+ autoCookie: config.autoCookie,
42
+ checkInterval: config.cookieCheckInterval,
43
+ });
37
44
  // 初始化 Cookie
38
45
  if (config.cookie) {
39
- (0, http_1.setMcmodCookie)(config.cookie);
40
46
  logger.info('使用手动配置的 Cookie');
41
47
  }
42
48
  else if (config.autoCookie) {
package/dist/nlu.js ADDED
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Config = void 0;
4
+ exports.normalizeAiDecision = normalizeAiDecision;
5
+ exports.buildCommand = buildCommand;
6
+ exports.apply = apply;
7
+ const { Schema } = require('koishi');
8
+ const fetch = require('node-fetch');
9
+ const DEFAULT_ENDPOINT = 'https://api.openai.com/v1/chat/completions';
10
+ const DEFAULT_MODEL = 'gpt-4o-mini';
11
+ exports.Config = Schema.object({
12
+ enabled: Schema.boolean().default(false).description('启用 @机器人 自然语言查询入口'),
13
+ endpoint: Schema.string().default(DEFAULT_ENDPOINT).description('OpenAI 兼容 Chat Completions 接口地址'),
14
+ apiKey: Schema.string().role('secret').description('OpenAI 兼容接口 API Key'),
15
+ model: Schema.string().default(DEFAULT_MODEL).description('模型名称'),
16
+ timeout: Schema.number().default(15000).description('AI 请求超时(ms)'),
17
+ temperature: Schema.number().default(0).description('AI 温度参数'),
18
+ }).description('—— AI 自然语言理解 ——');
19
+ const PLATFORM_ALIASES = {
20
+ mcmod: 'mcmod', cnmc: 'mcmod', mc: 'mcmod', 'mcmod.cn': 'mcmod', 'mc百科': 'mcmod',
21
+ cf: 'cf', curseforge: 'cf', curse: 'cf',
22
+ mr: 'mr', modrinth: 'mr',
23
+ };
24
+ const TYPE_ALIASES = {
25
+ mod: 'mod', mods: 'mod', 模组: 'mod',
26
+ pack: 'pack', modpack: 'pack', 整合包: 'pack',
27
+ resource: 'resource', resourcepack: 'resource', 材质: 'resource', 资源包: 'resource', 材质包: 'resource',
28
+ shader: 'shader', 光影: 'shader',
29
+ plugin: 'plugin', 插件: 'plugin',
30
+ data: 'data', item: 'data', 资料: 'data', 物品: 'data',
31
+ tutorial: 'tutorial', post: 'tutorial', 教程: 'tutorial',
32
+ author: 'author', 作者: 'author',
33
+ user: 'user', 用户: 'user',
34
+ };
35
+ const PLATFORM_TYPES = {
36
+ mcmod: new Set(['mod', 'pack', 'data', 'tutorial', 'author', 'user']),
37
+ cf: new Set(['mod', 'pack', 'resource', 'shader', 'plugin']),
38
+ mr: new Set(['mod', 'pack', 'resource', 'shader', 'plugin']),
39
+ };
40
+ function normalizeEndpoint(endpoint) {
41
+ const value = String(endpoint || DEFAULT_ENDPOINT).trim().replace(/\/+$/, '');
42
+ if (/\/chat\/completions$/i.test(value))
43
+ return value;
44
+ if (/\/v\d+$/i.test(value))
45
+ return `${value}/chat/completions`;
46
+ return value;
47
+ }
48
+ function withTimeout(timeout) {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeout) || 15000));
51
+ return { controller, done: () => clearTimeout(timer) };
52
+ }
53
+ function extractJsonObject(text) {
54
+ const value = String(text || '').trim();
55
+ try {
56
+ return JSON.parse(value);
57
+ }
58
+ catch { }
59
+ const match = value.match(/\{[\s\S]*\}/);
60
+ if (!match)
61
+ throw new Error('AI 返回内容不是 JSON');
62
+ return JSON.parse(match[0]);
63
+ }
64
+ function normalizePlatform(value) {
65
+ const key = String(value || '').trim().toLowerCase();
66
+ return PLATFORM_ALIASES[key] || 'mcmod';
67
+ }
68
+ function normalizeType(value, platform) {
69
+ var _a;
70
+ const key = String(value || '').trim().toLowerCase();
71
+ const type = TYPE_ALIASES[key] || 'mod';
72
+ return ((_a = PLATFORM_TYPES[platform]) === null || _a === void 0 ? void 0 : _a.has(type)) ? type : 'mod';
73
+ }
74
+ function normalizeAiDecision(raw) {
75
+ const action = String((raw === null || raw === void 0 ? void 0 : raw.action) || '').trim().toLowerCase();
76
+ if (action && action !== 'search')
77
+ return { action: 'ignore' };
78
+ const query = String((raw === null || raw === void 0 ? void 0 : raw.query) || (raw === null || raw === void 0 ? void 0 : raw.keyword) || '').replace(/[\r\n]+/g, ' ').trim();
79
+ if (!query)
80
+ return { action: 'ignore' };
81
+ const platform = normalizePlatform(raw === null || raw === void 0 ? void 0 : raw.platform);
82
+ const type = normalizeType(raw === null || raw === void 0 ? void 0 : raw.type, platform);
83
+ return { action: 'search', platform, type, query };
84
+ }
85
+ async function requestAi(config, text) {
86
+ var _a, _b, _c, _d, _e, _f;
87
+ const endpoint = normalizeEndpoint(config === null || config === void 0 ? void 0 : config.endpoint);
88
+ const headers = { 'Content-Type': 'application/json' };
89
+ if (config === null || config === void 0 ? void 0 : config.apiKey)
90
+ headers['Authorization'] = `Bearer ${config.apiKey}`;
91
+ const body = {
92
+ model: (config === null || config === void 0 ? void 0 : config.model) || DEFAULT_MODEL,
93
+ temperature: Number((_a = config === null || config === void 0 ? void 0 : config.temperature) !== null && _a !== void 0 ? _a : 0) || 0,
94
+ response_format: { type: 'json_object' },
95
+ messages: [
96
+ {
97
+ role: 'system',
98
+ content: [
99
+ '你是 Minecraft 模组搜索意图解析器,只返回 JSON,不要解释。',
100
+ 'JSON 格式: {"action":"search|ignore","platform":"mcmod|cf|mr","type":"mod|pack|data|tutorial|author|user|resource|shader|plugin","query":"关键词"}',
101
+ '默认 platform 为 mcmod,默认 type 为 mod。',
102
+ 'cf/curseforge 表示 CurseForge,mr/modrinth 表示 Modrinth,cnmc/mcmod/MC百科 表示 mcmod.cn。',
103
+ '“查询/搜索/查一下/找一下/模组”等只是意图词,不要放进 query;保留具体模组名、英文名、ID 或关键词。',
104
+ '如果不是搜索请求或没有明确关键词,返回 {"action":"ignore"}。',
105
+ ].join('\n'),
106
+ },
107
+ { role: 'user', content: text },
108
+ ],
109
+ };
110
+ const run = async (payload) => {
111
+ const { controller, done } = withTimeout(config === null || config === void 0 ? void 0 : config.timeout);
112
+ try {
113
+ const res = await fetch(endpoint, {
114
+ method: 'POST',
115
+ headers,
116
+ body: JSON.stringify(payload),
117
+ signal: controller.signal,
118
+ });
119
+ const responseText = await res.text();
120
+ if (!res.ok) {
121
+ const err = new Error(`AI 请求失败: HTTP ${res.status} ${responseText.slice(0, 300)}`);
122
+ err.status = res.status;
123
+ throw err;
124
+ }
125
+ return JSON.parse(responseText);
126
+ }
127
+ finally {
128
+ done();
129
+ }
130
+ };
131
+ let json;
132
+ try {
133
+ json = await run(body);
134
+ }
135
+ catch (e) {
136
+ if ((e === null || e === void 0 ? void 0 : e.status) !== 400 || !body.response_format)
137
+ throw e;
138
+ const retryBody = { ...body };
139
+ delete retryBody.response_format;
140
+ json = await run(retryBody);
141
+ }
142
+ const content = ((_d = (_c = (_b = json === null || json === void 0 ? void 0 : json.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) || ((_f = (_e = json === null || json === void 0 ? void 0 : json.choices) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.text) || (json === null || json === void 0 ? void 0 : json.output_text);
143
+ if (!content)
144
+ throw new Error('AI 返回中没有 message.content');
145
+ return normalizeAiDecision(extractJsonObject(content));
146
+ }
147
+ function botIds(session) {
148
+ var _a, _b;
149
+ return [session === null || session === void 0 ? void 0 : session.selfId, (_a = session === null || session === void 0 ? void 0 : session.bot) === null || _a === void 0 ? void 0 : _a.selfId, (_b = session === null || session === void 0 ? void 0 : session.bot) === null || _b === void 0 ? void 0 : _b.userId]
150
+ .filter(Boolean)
151
+ .map(id => String(id));
152
+ }
153
+ function hasAtSelf(session) {
154
+ var _a, _b, _c, _d;
155
+ const ids = botIds(session);
156
+ const elements = Array.isArray(session === null || session === void 0 ? void 0 : session.elements) ? session.elements : [];
157
+ for (const element of elements) {
158
+ if ((element === null || element === void 0 ? void 0 : element.type) !== 'at')
159
+ continue;
160
+ const id = String(((_a = element === null || element === void 0 ? void 0 : element.attrs) === null || _a === void 0 ? void 0 : _a.id) || ((_b = element === null || element === void 0 ? void 0 : element.attrs) === null || _b === void 0 ? void 0 : _b.userId) || ((_c = element === null || element === void 0 ? void 0 : element.attrs) === null || _c === void 0 ? void 0 : _c.qq) || '');
161
+ if (id && (!ids.length || ids.includes(id)))
162
+ return true;
163
+ }
164
+ const content = String((session === null || session === void 0 ? void 0 : session.content) || '');
165
+ const atRegex = /<at\s+([^>]*?)\/?>(?:<\/at>)?/gi;
166
+ let match;
167
+ while ((match = atRegex.exec(content))) {
168
+ const attrs = match[1] || '';
169
+ const id = (_d = attrs.match(/(?:id|user-id|qq)=(['"]?)([^'"\s/>]+)\1/i)) === null || _d === void 0 ? void 0 : _d[2];
170
+ if (id && (!ids.length || ids.includes(String(id))))
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+ function stripAt(text) {
176
+ return String(text || '')
177
+ .replace(/<at\s+[^>]*\/?>(?:<\/at>)?/gi, ' ')
178
+ .replace(/\[CQ:at,[^\]]+\]/gi, ' ')
179
+ .replace(/\s+/g, ' ')
180
+ .trim();
181
+ }
182
+ function getMentionText(session) {
183
+ var _a;
184
+ const stripped = String(((_a = session === null || session === void 0 ? void 0 : session.stripped) === null || _a === void 0 ? void 0 : _a.content) || '').trim();
185
+ const raw = stripAt((session === null || session === void 0 ? void 0 : session.content) || '');
186
+ return (stripped && stripped !== String((session === null || session === void 0 ? void 0 : session.content) || '').trim()) ? stripped : raw;
187
+ }
188
+ function isExplicitCommand(text, prefixes) {
189
+ const value = String(text || '').trim().toLowerCase();
190
+ return Object.values(prefixes || {}).some(prefix => {
191
+ const p = String(prefix || '').trim().toLowerCase();
192
+ return p && (value === p || value.startsWith(`${p} `) || value.startsWith(`${p}.`));
193
+ });
194
+ }
195
+ function buildCommand(decision, prefixes) {
196
+ if (!decision || decision.action !== 'search')
197
+ return '';
198
+ const platform = normalizePlatform(decision.platform);
199
+ const type = normalizeType(decision.type, platform);
200
+ const query = String(decision.query || '').replace(/[\r\n]+/g, ' ').trim();
201
+ if (!query)
202
+ return '';
203
+ const prefix = platform === 'mcmod'
204
+ ? ((prefixes === null || prefixes === void 0 ? void 0 : prefixes.cnmc) || 'cnmc')
205
+ : platform === 'cf'
206
+ ? ((prefixes === null || prefixes === void 0 ? void 0 : prefixes.cf) || 'cf')
207
+ : ((prefixes === null || prefixes === void 0 ? void 0 : prefixes.mr) || 'mr');
208
+ return `${prefix}.${type} ${query}`;
209
+ }
210
+ function apply(ctx, config, shared = {}) {
211
+ if (!(config === null || config === void 0 ? void 0 : config.enabled))
212
+ return;
213
+ const logger = ctx.logger('minecraft-nlu');
214
+ const prefixes = (shared === null || shared === void 0 ? void 0 : shared.prefixes) || {};
215
+ ctx.middleware(async (session, next) => {
216
+ if (!hasAtSelf(session))
217
+ return next();
218
+ const text = getMentionText(session);
219
+ if (!text || isExplicitCommand(text, prefixes))
220
+ return next();
221
+ let decision;
222
+ try {
223
+ decision = await requestAi(config, text);
224
+ }
225
+ catch (e) {
226
+ logger.warn(`AI 自然语言解析失败: ${(e === null || e === void 0 ? void 0 : e.message) || e}`);
227
+ await session.send(`AI 自然语言解析失败: ${(e === null || e === void 0 ? void 0 : e.message) || e}`);
228
+ return;
229
+ }
230
+ const command = buildCommand(decision, prefixes);
231
+ if (!command)
232
+ return next();
233
+ if ((shared === null || shared === void 0 ? void 0 : shared.debug) || (config === null || config === void 0 ? void 0 : config.debug))
234
+ logger.info(`NLU: ${text} -> ${command}`);
235
+ const result = await session.execute(command);
236
+ if (result)
237
+ await session.send(result);
238
+ });
239
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-cfmrmod",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
5
5
  "main": "dist/index.js",
6
6
  "files": [