koishi-plugin-ll-mc 3.0.9 → 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 +163 -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([
|
|
@@ -2505,6 +2509,147 @@ ${targetUserRecord[0].username}
|
|
|
2505
2509
|
return number / total * 100;
|
|
2506
2510
|
}
|
|
2507
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");
|
|
2508
2653
|
function getCurrentBeijingTime() {
|
|
2509
2654
|
return (/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
|
|
2510
2655
|
}
|
|
@@ -2514,6 +2659,23 @@ ${targetUserRecord[0].username}
|
|
|
2514
2659
|
rankTitle,
|
|
2515
2660
|
rankingData
|
|
2516
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
|
+
}
|
|
2517
2679
|
if (config.isLeaderboardToHorizontalBarChartConversionEnabled) {
|
|
2518
2680
|
if (!ctx.puppeteer) {
|
|
2519
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": [
|