koishi-plugin-cfmrmod 1.1.3 → 1.1.4

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.
@@ -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) {
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.4",
4
4
  "description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
5
5
  "main": "dist/index.js",
6
6
  "files": [