koishi-plugin-ll-mc 3.0.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +176 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -184,7 +184,11 @@ var Config = import_koishi.Schema.intersect([
|
|
|
184
184
|
isTextToImageConversionEnabled: import_koishi.Schema.boolean().default(false).description(
|
|
185
185
|
"是否将文本排行榜转为 Markdown 图片(依赖 `markdownToImage` 服务)。"
|
|
186
186
|
),
|
|
187
|
-
isLeaderboardToHorizontalBarChartConversionEnabled: import_koishi.Schema.boolean().default(false).description("是否将排行榜渲染为水平柱状图(依赖 `puppeteer` 服务)。")
|
|
187
|
+
isLeaderboardToHorizontalBarChartConversionEnabled: import_koishi.Schema.boolean().default(false).description("是否将排行榜渲染为水平柱状图(依赖 `puppeteer` 服务)。"),
|
|
188
|
+
renderMode: import_koishi.Schema.union([
|
|
189
|
+
import_koishi.Schema.const("chart").description("原版画布图(默认,保持原有效果)"),
|
|
190
|
+
import_koishi.Schema.const("template").description("自定义 HTML 模板图(按模板设计渲染,需 puppeteer)"),
|
|
191
|
+
]).default("chart").description("排行图渲染方式"),
|
|
188
192
|
}).description("图片生成"),
|
|
189
193
|
// 仅在开启柱状图功能时显示以下详细选项
|
|
190
194
|
import_koishi.Schema.union([
|
|
@@ -384,6 +388,19 @@ async function apply(ctx, config) {
|
|
|
384
388
|
fontFile
|
|
385
389
|
);
|
|
386
390
|
}
|
|
391
|
+
// 官机补丁:强制覆盖 NotoEmoji 字体(纯 emoji 版),
|
|
392
|
+
// 防止旧版本(带数字字形)缓存在 data/messageCounter/fonts/ 里导致数字被加宽
|
|
393
|
+
try {
|
|
394
|
+
const emojiSrc = import_path.default.join(assetsDir, "fonts", "NotoEmoji.ttf");
|
|
395
|
+
const emojiDest = import_path.default.join(fontsPath, "NotoEmoji.ttf");
|
|
396
|
+
await fs.copyFile(emojiSrc, emojiDest);
|
|
397
|
+
// 删除旧版补丁字体(避免旧版带数字字形的 patched 文件被继续加载、家族名冲突)
|
|
398
|
+
const patched = import_path.default.join(fontsPath, "NotoEmoji-patched.ttf");
|
|
399
|
+
await fs.rm(patched, { force: true });
|
|
400
|
+
logger.info("[ll-mc] 已强制更新 NotoEmoji 字体为纯 emoji 版,并清理旧补丁字体。");
|
|
401
|
+
} catch (error) {
|
|
402
|
+
logger.warn("[ll-mc] 强制更新 NotoEmoji 字体失败:", error?.message);
|
|
403
|
+
}
|
|
387
404
|
const avatarCache = /* @__PURE__ */ new Map();
|
|
388
405
|
let iconCache = [];
|
|
389
406
|
let barBgImgCache = [];
|
|
@@ -2492,6 +2509,147 @@ ${targetUserRecord[0].username}
|
|
|
2492
2509
|
return number / total * 100;
|
|
2493
2510
|
}
|
|
2494
2511
|
__name(calculatePercentage, "calculatePercentage");
|
|
2512
|
+
function escapeHtml(value) {
|
|
2513
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, (ch) => ({
|
|
2514
|
+
"&": "&",
|
|
2515
|
+
"<": "<",
|
|
2516
|
+
">": ">",
|
|
2517
|
+
'"': """,
|
|
2518
|
+
"'": "'"
|
|
2519
|
+
}[ch]));
|
|
2520
|
+
}
|
|
2521
|
+
__name(escapeHtml, "escapeHtml");
|
|
2522
|
+
const LEADERBOARD_ROW_PALETTES = [
|
|
2523
|
+
{ bar: "#C8B8AE", fill: "#A26D51", text: "#5D3F2E" },
|
|
2524
|
+
{ bar: "#C4C4C4", fill: "#8E908D", text: "#4A4B49" },
|
|
2525
|
+
{ bar: "#F8BBD0", fill: "#F06292", text: "#374151" },
|
|
2526
|
+
{ bar: "#D1C4E9", fill: "#9575CD", text: "#374151" },
|
|
2527
|
+
{ bar: "#FFCDD2", fill: "#E57373", text: "#374151" },
|
|
2528
|
+
{ bar: "#CFD8DC", fill: "#90A4AE", text: "#374151" },
|
|
2529
|
+
{ bar: "#D7CCC8", fill: "#A1887F", text: "#374151" },
|
|
2530
|
+
{ bar: "#FFE0B2", fill: "#FFB74D", text: "#374151" },
|
|
2531
|
+
{ bar: "#BDBDBD", fill: "#757575", text: "#374151" },
|
|
2532
|
+
{ bar: "#C5E1A5", fill: "#AED581", text: "#374151" },
|
|
2533
|
+
{ bar: "#FFECB3", fill: "#FFD54F", text: "#374151" },
|
|
2534
|
+
{ bar: "#B2DFDB", fill: "#80CBC4", text: "#374151" },
|
|
2535
|
+
{ bar: "#BBDEFB", fill: "#90CAF9", text: "#374151" },
|
|
2536
|
+
{ bar: "#E1BEE7", fill: "#CE93D8", text: "#374151" },
|
|
2537
|
+
{ bar: "#FFCCBC", fill: "#FFAB91", text: "#374151" },
|
|
2538
|
+
{ bar: "#F0F4C3", fill: "#E6EE9C", text: "#374151" },
|
|
2539
|
+
{ bar: "#BCAAA4", fill: "#A1887F", text: "#374151" },
|
|
2540
|
+
{ bar: "#EEEEEE", fill: "#BDBDBD", text: "#374151" },
|
|
2541
|
+
{ bar: "#CFD8DC", fill: "#90A4AE", text: "#374151" }
|
|
2542
|
+
];
|
|
2543
|
+
function buildLeaderboardRow(item, index) {
|
|
2544
|
+
const pal = LEADERBOARD_ROW_PALETTES[index % LEADERBOARD_ROW_PALETTES.length];
|
|
2545
|
+
const isHero = index === 0;
|
|
2546
|
+
const width = item.count > 0 ? Math.max(item.percentage, 1) : 1;
|
|
2547
|
+
const avatar = escapeHtml(item.avatar || "");
|
|
2548
|
+
const name = escapeHtml(item.name || "");
|
|
2549
|
+
const count = item.count;
|
|
2550
|
+
const barBg = isHero ? "#F3F4F6" : pal.bar;
|
|
2551
|
+
const fill = isHero
|
|
2552
|
+
? 'linear-gradient(to right, #6366F1, #A855F7, #EC4899)'
|
|
2553
|
+
: pal.fill;
|
|
2554
|
+
const textColor = isHero ? "#FFFFFF" : pal.text;
|
|
2555
|
+
const star = isHero
|
|
2556
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="row-star"><path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z" clip-rule="evenodd"/></svg>`
|
|
2557
|
+
: "";
|
|
2558
|
+
return `<div class="row">
|
|
2559
|
+
<div class="avatar"><img src="${avatar}" alt="avatar"></div>
|
|
2560
|
+
<div class="bar" style="background:${barBg};">
|
|
2561
|
+
<div class="fill" style="width:${width}%;background:${fill};"></div>
|
|
2562
|
+
<div class="bar-content" style="color:${textColor};">
|
|
2563
|
+
<span class="row-name${isHero ? " is-hero" : ""}">${star}${name}</span>
|
|
2564
|
+
<span class="row-count">${count}</span>
|
|
2565
|
+
</div>
|
|
2566
|
+
</div>
|
|
2567
|
+
</div>`;
|
|
2568
|
+
}
|
|
2569
|
+
__name(buildLeaderboardRow, "buildLeaderboardRow");
|
|
2570
|
+
function buildLeaderboardHtml(title, date, items) {
|
|
2571
|
+
const rows = items.map(buildLeaderboardRow).join("\n");
|
|
2572
|
+
return `<!DOCTYPE html>
|
|
2573
|
+
<html lang="zh-CN">
|
|
2574
|
+
<head>
|
|
2575
|
+
<meta charset="UTF-8">
|
|
2576
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2577
|
+
<style>
|
|
2578
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2579
|
+
html, body {
|
|
2580
|
+
width: 900px;
|
|
2581
|
+
background-color: #F5F7FA;
|
|
2582
|
+
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
2583
|
+
-webkit-font-smoothing: antialiased;
|
|
2584
|
+
}
|
|
2585
|
+
body { padding: 16px; }
|
|
2586
|
+
.card {
|
|
2587
|
+
width: 100%;
|
|
2588
|
+
max-width: 868px;
|
|
2589
|
+
margin: 0 auto;
|
|
2590
|
+
background: #FFFFFF;
|
|
2591
|
+
border-radius: 12px;
|
|
2592
|
+
padding: 24px;
|
|
2593
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.06);
|
|
2594
|
+
border: 1px solid #F3F4F6;
|
|
2595
|
+
}
|
|
2596
|
+
.card-header { text-align: center; margin-bottom: 22px; }
|
|
2597
|
+
.card-title { font-size: 28px; font-weight: 700; letter-spacing: 0.02em; color: #000000; margin-bottom: 6px; }
|
|
2598
|
+
.card-date { font-size: 14px; font-weight: 500; color: #9CA3AF; }
|
|
2599
|
+
.rows { display: flex; flex-direction: column; gap: 8px; }
|
|
2600
|
+
.row { display: flex; align-items: center; gap: 12px; width: 100%; }
|
|
2601
|
+
.avatar { width: 40px; height: 40px; flex-shrink: 0; }
|
|
2602
|
+
.avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; box-shadow: 0 1px 2px rgba(0,0,0,0.08); background: #fff; padding: 2px; }
|
|
2603
|
+
.bar { position: relative; flex-grow: 1; height: 40px; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; }
|
|
2604
|
+
.bar::after { content: ''; position: absolute; inset: 0; background-image: linear-gradient(to right, transparent 99%, rgba(255,255,255,0.2) 100%); background-size: 10% 100%; pointer-events: none; }
|
|
2605
|
+
.fill { position: absolute; left: 0; top: 0; height: 100%; border-radius: 8px 0 0 8px; z-index: 0; }
|
|
2606
|
+
.bar-content { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; width: 100%; }
|
|
2607
|
+
.row-name { font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 5px; max-width: 78%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2608
|
+
.row-name.is-hero { color: #FFFFFF; text-shadow: 0 1px 2px rgba(0,0,0,0.2); }
|
|
2609
|
+
.row-star { width: 16px; height: 16px; color: #FCD34D; flex-shrink: 0; }
|
|
2610
|
+
.row-count { font-size: 18px; font-weight: 700; }
|
|
2611
|
+
</style>
|
|
2612
|
+
</head>
|
|
2613
|
+
<body>
|
|
2614
|
+
<div class="card">
|
|
2615
|
+
<div class="card-header">
|
|
2616
|
+
<h1 class="card-title">${escapeHtml(title)}</h1>
|
|
2617
|
+
<p class="card-date">${escapeHtml(date)}</p>
|
|
2618
|
+
</div>
|
|
2619
|
+
<div class="rows">
|
|
2620
|
+
${rows}
|
|
2621
|
+
</div>
|
|
2622
|
+
</div>
|
|
2623
|
+
</body>
|
|
2624
|
+
</html>`;
|
|
2625
|
+
}
|
|
2626
|
+
__name(buildLeaderboardHtml, "buildLeaderboardHtml");
|
|
2627
|
+
async function renderLeaderboardTemplate(ctx, title, date, items) {
|
|
2628
|
+
if (typeof ctx.puppeteer?.page !== "function") return null;
|
|
2629
|
+
let page;
|
|
2630
|
+
try {
|
|
2631
|
+
const html = buildLeaderboardHtml(title, date, items);
|
|
2632
|
+
page = await ctx.puppeteer.page();
|
|
2633
|
+
await page.setViewport({ width: 900, height: 400, deviceScaleFactor: 2 });
|
|
2634
|
+
try {
|
|
2635
|
+
await page.setContent(html, { waitUntil: "networkidle0", timeout: 20000 });
|
|
2636
|
+
} catch (e) {
|
|
2637
|
+
logger.warn("Leaderboard template wait timed out, screenshotting current content.");
|
|
2638
|
+
}
|
|
2639
|
+
await page.waitForSelector(".card").catch(() => {});
|
|
2640
|
+
const el = await page.$(".card");
|
|
2641
|
+
const buf = el
|
|
2642
|
+
? await el.screenshot({ type: "png" })
|
|
2643
|
+
: await page.screenshot({ type: "png", fullPage: true });
|
|
2644
|
+
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
logger.error("Failed to render leaderboard template:", error);
|
|
2647
|
+
return null;
|
|
2648
|
+
} finally {
|
|
2649
|
+
if (page) await page.close().catch(() => {});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
__name(renderLeaderboardTemplate, "renderLeaderboardTemplate");
|
|
2495
2653
|
function getCurrentBeijingTime() {
|
|
2496
2654
|
return (/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
|
|
2497
2655
|
}
|
|
@@ -2501,6 +2659,23 @@ ${targetUserRecord[0].username}
|
|
|
2501
2659
|
rankTitle,
|
|
2502
2660
|
rankingData
|
|
2503
2661
|
}) {
|
|
2662
|
+
if (config.renderMode === "template") {
|
|
2663
|
+
const templateItems = rankingData.map((item, index) => ({
|
|
2664
|
+
...item,
|
|
2665
|
+
rank: index + 1,
|
|
2666
|
+
name: item.name.replace(/^★/, "")
|
|
2667
|
+
}));
|
|
2668
|
+
const dataUri = await renderLeaderboardTemplate(
|
|
2669
|
+
ctx,
|
|
2670
|
+
rankTitle,
|
|
2671
|
+
rankTimeTitle,
|
|
2672
|
+
templateItems
|
|
2673
|
+
);
|
|
2674
|
+
if (dataUri) {
|
|
2675
|
+
return import_koishi.h.image(dataUri);
|
|
2676
|
+
}
|
|
2677
|
+
logger.warn("Leaderboard template render failed. Falling back to text.");
|
|
2678
|
+
}
|
|
2504
2679
|
if (config.isLeaderboardToHorizontalBarChartConversionEnabled) {
|
|
2505
2680
|
if (!ctx.puppeteer) {
|
|
2506
2681
|
logger.warn("Puppeteer service is not enabled. Falling back to text.");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-ll-mc",
|
|
3
3
|
"description": "消息数量统计插件(基于 koishi-plugin-message-counter 重做,官方QQ机器人兼容)",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"koishi": {
|
|
12
12
|
"description": {
|
|
13
|
-
"zh": "
|
|
13
|
+
"zh": "消息数量统计插件:发言排行榜、群排行榜,支持自定义 HTML 模板排行图,官方QQ机器人(官机)兼容补丁"
|
|
14
14
|
},
|
|
15
15
|
"service": {
|
|
16
16
|
"required": [
|