koishi-plugin-chat-analyse 0.3.4 → 0.3.6
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/Renderer.d.ts +36 -19
- package/lib/Stat.d.ts +3 -3
- package/lib/index.js +201 -74
- package/package.json +1 -1
package/lib/Renderer.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
+
/**
|
|
3
|
+
* 定义了渲染列表中单行数据的格式。它是一个由字符串、数字或 `Date` 对象组成的数组。
|
|
4
|
+
*/
|
|
2
5
|
export type RenderListItem = (string | number | Date)[];
|
|
6
|
+
/**
|
|
7
|
+
* @interface ListRenderData
|
|
8
|
+
* @description 定义了调用 `renderList` 方法所需的数据结构。
|
|
9
|
+
* 它包含了渲染一张完整列表图片所必需的所有信息。
|
|
10
|
+
*/
|
|
3
11
|
export interface ListRenderData {
|
|
4
12
|
title: string;
|
|
5
13
|
time: Date;
|
|
@@ -8,36 +16,45 @@ export interface ListRenderData {
|
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
18
|
* @class Renderer
|
|
11
|
-
* @
|
|
19
|
+
* @classdesc
|
|
20
|
+
* 负责将结构化的数据(特别是列表)转换为设计精美的PNG图片。
|
|
21
|
+
* 其核心特性是能够动态计算内容尺寸,生成布局紧凑、自适应的图片。
|
|
12
22
|
*/
|
|
13
23
|
export declare class Renderer {
|
|
14
24
|
private ctx;
|
|
25
|
+
/**
|
|
26
|
+
* @param {Context} ctx - Koishi 的插件上下文,用于访问核心服务如 `puppeteer` 和 `logger`。
|
|
27
|
+
*/
|
|
15
28
|
constructor(ctx: Context);
|
|
16
29
|
/**
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
* @
|
|
20
|
-
*
|
|
21
|
-
* @param {
|
|
22
|
-
* @
|
|
23
|
-
* @
|
|
30
|
+
* @private
|
|
31
|
+
* @method htmlToImage
|
|
32
|
+
* @description
|
|
33
|
+
* 负责将任意HTML字符串转换为PNG图片Buffer。
|
|
34
|
+
* @param {string} html - 要渲染的HTML主体内容(不包含 `<html>` 和 `<body>` 标签)。
|
|
35
|
+
* @returns {Promise<Buffer>} 返回一个包含PNG图片数据的 Buffer 对象。
|
|
36
|
+
* @throws {Error} 如果 Puppeteer 截图过程中发生错误,将抛出异常。
|
|
24
37
|
*/
|
|
25
|
-
|
|
38
|
+
private htmlToImage;
|
|
26
39
|
/**
|
|
27
40
|
* @private
|
|
28
41
|
* @method formatDate
|
|
29
|
-
* @description
|
|
30
|
-
*
|
|
31
|
-
* @
|
|
42
|
+
* @description
|
|
43
|
+
* 将 `Date` 对象格式化为人类友好的相对时间字符串。
|
|
44
|
+
* @param {Date} date - 需要格式化的日期对象。
|
|
45
|
+
* @returns {string} - 格式化后的时间字符串。
|
|
32
46
|
*/
|
|
33
47
|
private formatDate;
|
|
34
48
|
/**
|
|
35
|
-
* @
|
|
36
|
-
* @method
|
|
37
|
-
* @description
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @
|
|
49
|
+
* @public
|
|
50
|
+
* @method renderList
|
|
51
|
+
* @description
|
|
52
|
+
* 接收一个标准化的 `ListRenderData` 对象和可选的表头数组,
|
|
53
|
+
* 然后构建一个包含标题、统计信息和数据表格的完整HTML卡片。
|
|
54
|
+
* @param {ListRenderData} data - 包含渲染所需全部信息的对象。
|
|
55
|
+
* @param {string[]} [headers] - (可选) 表格的表头字符串数组。如果提供,将渲染表头。
|
|
56
|
+
* @returns {Promise<string | Buffer>}
|
|
57
|
+
* 如果成功,返回包含PNG图片的 Buffer。如果输入的数据列表为空,则返回一个提示性字符串。
|
|
41
58
|
*/
|
|
42
|
-
|
|
59
|
+
renderList(data: ListRenderData, headers?: string[]): Promise<string | Buffer>;
|
|
43
60
|
}
|
package/lib/Stat.d.ts
CHANGED
|
@@ -68,13 +68,13 @@ export declare class Stat {
|
|
|
68
68
|
/**
|
|
69
69
|
* @private
|
|
70
70
|
* @async
|
|
71
|
-
* @method
|
|
72
|
-
* @description
|
|
71
|
+
* @method getUserMessageStats
|
|
72
|
+
* @description 从数据库中获取并聚合每个用户的消息统计数据。
|
|
73
73
|
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
74
74
|
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
75
75
|
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
76
76
|
*/
|
|
77
|
-
private
|
|
77
|
+
private getUserMessageStats;
|
|
78
78
|
/**
|
|
79
79
|
* @private
|
|
80
80
|
* @async
|
package/lib/index.js
CHANGED
|
@@ -266,6 +266,9 @@ var import_koishi3 = require("koishi");
|
|
|
266
266
|
// src/Renderer.ts
|
|
267
267
|
var import_koishi2 = require("koishi");
|
|
268
268
|
var Renderer = class {
|
|
269
|
+
/**
|
|
270
|
+
* @param {Context} ctx - Koishi 的插件上下文,用于访问核心服务如 `puppeteer` 和 `logger`。
|
|
271
|
+
*/
|
|
269
272
|
constructor(ctx) {
|
|
270
273
|
this.ctx = ctx;
|
|
271
274
|
}
|
|
@@ -273,42 +276,129 @@ var Renderer = class {
|
|
|
273
276
|
__name(this, "Renderer");
|
|
274
277
|
}
|
|
275
278
|
/**
|
|
276
|
-
* @
|
|
277
|
-
* @
|
|
278
|
-
* @
|
|
279
|
-
*
|
|
280
|
-
* @param {
|
|
281
|
-
* @
|
|
282
|
-
* @
|
|
279
|
+
* @private
|
|
280
|
+
* @method htmlToImage
|
|
281
|
+
* @description
|
|
282
|
+
* 负责将任意HTML字符串转换为PNG图片Buffer。
|
|
283
|
+
* @param {string} html - 要渲染的HTML主体内容(不包含 `<html>` 和 `<body>` 标签)。
|
|
284
|
+
* @returns {Promise<Buffer>} 返回一个包含PNG图片数据的 Buffer 对象。
|
|
285
|
+
* @throws {Error} 如果 Puppeteer 截图过程中发生错误,将抛出异常。
|
|
283
286
|
*/
|
|
284
|
-
async
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
async htmlToImage(html) {
|
|
288
|
+
let page = null;
|
|
289
|
+
try {
|
|
290
|
+
page = await this.ctx.puppeteer.page();
|
|
291
|
+
await page.setViewport({ width: 720, height: 1080, deviceScaleFactor: 2 });
|
|
292
|
+
await page.setContent(`
|
|
293
|
+
<!DOCTYPE html>
|
|
294
|
+
<html>
|
|
295
|
+
<head>
|
|
296
|
+
<meta charset="UTF-8">
|
|
297
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
298
|
+
<style>
|
|
299
|
+
:root {
|
|
300
|
+
--card-bg: #ffffff; --text-color: #111827; --header-color: #111827;
|
|
301
|
+
--sub-text-color: #6b7280; --border-color: #e5e7eb; --accent-color: #4a6ee0;
|
|
302
|
+
--chip-bg: #f3f4f6; --stripe-bg: #f9fafb; --gold: #f59e0b; --silver: #9ca3af; --bronze: #a16207;
|
|
303
|
+
}
|
|
304
|
+
body {
|
|
305
|
+
display: inline-block; /* Crucial for shrink-wrapping */
|
|
306
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
307
|
+
background: transparent; margin: 0; padding: 8px;
|
|
308
|
+
-webkit-font-smoothing: antialiased;
|
|
309
|
+
}
|
|
310
|
+
.container {
|
|
311
|
+
display: inline-block; background: var(--card-bg);
|
|
312
|
+
border-radius: 12px; padding: 0; overflow: hidden;
|
|
313
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
314
|
+
}
|
|
315
|
+
.header { padding: 10px 14px; }
|
|
316
|
+
.header-table { border-collapse: collapse; width: 100%; }
|
|
317
|
+
.header-table-left, .header-table-right { width: 1%; white-space: nowrap; }
|
|
318
|
+
.header-table-left { text-align: left; }
|
|
319
|
+
.header-table-center { text-align: center; }
|
|
320
|
+
.header-table-right { text-align: right; }
|
|
321
|
+
.title-text { font-size: 18px; font-weight: 600; color: var(--header-color); margin: 0; }
|
|
322
|
+
.stat-chip, .time-label {
|
|
323
|
+
display: inline-flex; align-items: baseline; padding: 4px 8px; border-radius: 8px;
|
|
324
|
+
background: var(--chip-bg); font-size: 13px; color: var(--sub-text-color);
|
|
325
|
+
}
|
|
326
|
+
.stat-chip span { font-weight: 600; color: var(--text-color); margin-left: 4px; }
|
|
327
|
+
.table-container { border-top: 1px solid var(--border-color); }
|
|
328
|
+
.main-table { border-collapse: collapse; width: 100%; }
|
|
329
|
+
.main-table th, .main-table td {
|
|
330
|
+
padding: 8px 14px;
|
|
331
|
+
vertical-align: middle;
|
|
332
|
+
}
|
|
333
|
+
.main-table th {
|
|
334
|
+
font-size: 12px; font-weight: 500; color: var(--sub-text-color);
|
|
335
|
+
text-transform: uppercase; letter-spacing: 0.05em; background: var(--stripe-bg);
|
|
336
|
+
}
|
|
337
|
+
.main-table td { font-size: 14px; color: var(--text-color); }
|
|
338
|
+
.main-table tbody tr:nth-child(even) { background-color: var(--stripe-bg); }
|
|
339
|
+
.main-table .name-cell, .main-table .name-header { text-align: left; }
|
|
340
|
+
.main-table .rank-cell, .main-table .count-cell, .main-table .date-cell, .main-table .percent-cell, .main-table .header-right-align {
|
|
341
|
+
text-align: right;
|
|
342
|
+
white-space: nowrap;
|
|
343
|
+
width: 1%;
|
|
344
|
+
font-variant-numeric: tabular-nums;
|
|
345
|
+
}
|
|
346
|
+
.name-cell { font-weight: 500; }
|
|
347
|
+
.rank-cell { font-weight: 500; color: var(--sub-text-color); }
|
|
348
|
+
.count-cell { font-weight: 600; color: var(--accent-color); }
|
|
349
|
+
.date-cell { color: var(--sub-text-color); }
|
|
350
|
+
.rank-gold, .rank-silver, .rank-bronze { font-weight: 600; }
|
|
351
|
+
.rank-gold { color: var(--gold) !important; }
|
|
352
|
+
.rank-silver { color: var(--silver) !important; }
|
|
353
|
+
.rank-bronze { color: var(--bronze) !important; }
|
|
354
|
+
.percent-cell { position: relative; }
|
|
355
|
+
.percent-bar { position: absolute; top: 0; right: 0; height: 100%; background-color: var(--accent-color); opacity: 0.15; }
|
|
356
|
+
.percent-text { position: relative; z-index: 1; }
|
|
357
|
+
</style>
|
|
358
|
+
</head>
|
|
359
|
+
<body>${html}</body>
|
|
360
|
+
</html>
|
|
361
|
+
`, { waitUntil: "networkidle0" });
|
|
362
|
+
const dimensions = await page.evaluate(() => {
|
|
363
|
+
const el = document.body;
|
|
364
|
+
return {
|
|
365
|
+
width: el.scrollWidth,
|
|
366
|
+
height: el.scrollHeight
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
await page.setViewport({ ...dimensions, deviceScaleFactor: 2 });
|
|
370
|
+
return await page.screenshot({ type: "png", fullPage: true, omitBackground: true });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.ctx.logger.error("图片渲染出错:", error);
|
|
373
|
+
} finally {
|
|
374
|
+
if (page) await page.close().catch(() => {
|
|
375
|
+
});
|
|
376
|
+
}
|
|
288
377
|
}
|
|
289
378
|
/**
|
|
290
379
|
* @private
|
|
291
380
|
* @method formatDate
|
|
292
|
-
* @description
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
381
|
+
* @description
|
|
382
|
+
* 将 `Date` 对象格式化为人类友好的相对时间字符串。
|
|
383
|
+
* @param {Date} date - 需要格式化的日期对象。
|
|
384
|
+
* @returns {string} - 格式化后的时间字符串。
|
|
295
385
|
*/
|
|
296
386
|
formatDate(date) {
|
|
297
387
|
if (!date) return "未知";
|
|
298
388
|
const diff = Date.now() - date.getTime();
|
|
299
389
|
if (diff < import_koishi2.Time.minute) return "刚刚";
|
|
300
390
|
if (diff > 365 * import_koishi2.Time.day) {
|
|
301
|
-
return
|
|
391
|
+
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" }).replace(/\//g, "-");
|
|
302
392
|
}
|
|
303
393
|
const timeUnits = [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
394
|
+
["月", 30 * import_koishi2.Time.day],
|
|
395
|
+
["天", import_koishi2.Time.day],
|
|
396
|
+
["时", import_koishi2.Time.hour],
|
|
397
|
+
["分", import_koishi2.Time.minute]
|
|
308
398
|
];
|
|
309
399
|
let remainingDiff = diff;
|
|
310
400
|
const parts = [];
|
|
311
|
-
for (const
|
|
401
|
+
for (const [unit, ms] of timeUnits) {
|
|
312
402
|
if (remainingDiff >= ms) {
|
|
313
403
|
const value = Math.floor(remainingDiff / ms);
|
|
314
404
|
parts.push(`${value}${unit}`);
|
|
@@ -316,58 +406,83 @@ var Renderer = class {
|
|
|
316
406
|
}
|
|
317
407
|
}
|
|
318
408
|
const result = parts.slice(0, 2).join("");
|
|
319
|
-
return
|
|
409
|
+
return `${result}前`;
|
|
320
410
|
}
|
|
321
411
|
/**
|
|
322
|
-
* @
|
|
323
|
-
* @method
|
|
324
|
-
* @description
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* @
|
|
412
|
+
* @public
|
|
413
|
+
* @method renderList
|
|
414
|
+
* @description
|
|
415
|
+
* 接收一个标准化的 `ListRenderData` 对象和可选的表头数组,
|
|
416
|
+
* 然后构建一个包含标题、统计信息和数据表格的完整HTML卡片。
|
|
417
|
+
* @param {ListRenderData} data - 包含渲染所需全部信息的对象。
|
|
418
|
+
* @param {string[]} [headers] - (可选) 表格的表头字符串数组。如果提供,将渲染表头。
|
|
419
|
+
* @returns {Promise<string | Buffer>}
|
|
420
|
+
* 如果成功,返回包含PNG图片的 Buffer。如果输入的数据列表为空,则返回一个提示性字符串。
|
|
328
421
|
*/
|
|
329
|
-
|
|
330
|
-
const { title, time,
|
|
331
|
-
if (!list?.length) return
|
|
332
|
-
|
|
422
|
+
async renderList(data, headers) {
|
|
423
|
+
const { title, time, list } = data;
|
|
424
|
+
if (!list?.length) return "暂无数据可供渲染";
|
|
425
|
+
let totalValueForPercent = 0;
|
|
426
|
+
const countHeaderIndex = headers?.findIndex((h2) => ["总计发言", "条数", "次数", "数量"].includes(h2));
|
|
427
|
+
if (countHeaderIndex > -1) {
|
|
428
|
+
totalValueForPercent = list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0);
|
|
429
|
+
}
|
|
430
|
+
const totalCount = data.total || totalValueForPercent;
|
|
431
|
+
const tableHeadHtml = headers?.length > 0 ? `<thead><tr><th class="rank-cell">#</th>${headers.map((h2, i) => {
|
|
333
432
|
const firstCell = list[0]?.[i];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
headerClass += " header-right-align";
|
|
338
|
-
}
|
|
339
|
-
return `<th class="${headerClass.trim()}">${h2}</th>`;
|
|
433
|
+
const isRightAlign = typeof firstCell === "number" || firstCell instanceof Date || h2.includes("占比");
|
|
434
|
+
const alignClass = isRightAlign ? "header-right-align" : "name-header";
|
|
435
|
+
return `<th class="${alignClass}">${h2}</th>`;
|
|
340
436
|
}).join("")}</tr></thead>` : "";
|
|
341
437
|
const tableRowsHtml = list.map((row, index) => {
|
|
342
438
|
const rank = index + 1;
|
|
343
439
|
const rankClass = rank === 1 ? "rank-gold" : rank === 2 ? "rank-silver" : rank === 3 ? "rank-bronze" : "";
|
|
344
440
|
const rankCell = `<td class="rank-cell ${rankClass}">${rank}</td>`;
|
|
345
|
-
const dataCells = row.map((cell,
|
|
346
|
-
let className = "
|
|
441
|
+
const dataCells = row.map((cell, i) => {
|
|
442
|
+
let className = "";
|
|
347
443
|
let content;
|
|
348
|
-
|
|
349
|
-
|
|
444
|
+
const headerText = headers?.[i] || "";
|
|
445
|
+
if (headerText.includes("占比")) {
|
|
446
|
+
className = "percent-cell";
|
|
447
|
+
const percentValue = parseFloat(String(cell).replace("%", ""));
|
|
448
|
+
content = `<div class="percent-bar" style="width: ${percentValue}%;"></div><span class="percent-text">${cell}</span>`;
|
|
449
|
+
} else if (cell instanceof Date) {
|
|
450
|
+
className = "date-cell";
|
|
350
451
|
content = this.formatDate(cell);
|
|
351
452
|
} else if (typeof cell === "number") {
|
|
352
|
-
className
|
|
453
|
+
className = "count-cell";
|
|
353
454
|
content = cell.toLocaleString();
|
|
354
455
|
} else {
|
|
355
|
-
className
|
|
456
|
+
className = "name-cell";
|
|
356
457
|
content = String(cell);
|
|
357
458
|
}
|
|
358
|
-
if (cellIndex === 0) className += " column-main-label";
|
|
359
459
|
return `<td class="${className}">${content}</td>`;
|
|
360
460
|
}).join("");
|
|
361
461
|
return `<tr>${rankCell}${dataCells}</tr>`;
|
|
362
462
|
}).join("");
|
|
363
|
-
const
|
|
364
|
-
<div class="
|
|
365
|
-
|
|
366
|
-
|
|
463
|
+
const cardHtml = `
|
|
464
|
+
<div class="container">
|
|
465
|
+
<div class="header">
|
|
466
|
+
<table class="header-table">
|
|
467
|
+
<tr>
|
|
468
|
+
<td class="header-table-left">
|
|
469
|
+
<div class="stat-chip">总计: <span>${typeof totalCount === "number" ? totalCount.toLocaleString() : totalCount}</span></div>
|
|
470
|
+
</td>
|
|
471
|
+
<td class="header-table-center">
|
|
472
|
+
<h1 class="title-text">${title}</h1>
|
|
473
|
+
</td>
|
|
474
|
+
<td class="header-table-right">
|
|
475
|
+
<div class="time-label">${time.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-")}</div>
|
|
476
|
+
</td>
|
|
477
|
+
</tr>
|
|
478
|
+
</table>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="table-container">
|
|
481
|
+
<table class="main-table">${tableHeadHtml}<tbody>${tableRowsHtml}</tbody></table>
|
|
482
|
+
</div>
|
|
367
483
|
</div>
|
|
368
484
|
`;
|
|
369
|
-
|
|
370
|
-
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${title}</title><style>${styles}</style></head><body><div class="container"><div class="header"><div class="title-group"><h1>${title}</h1></div>${metaInfoHtml}</div><table>${tableHeadHtml}<tbody>${tableRowsHtml}</tbody></table></div></body></html>`;
|
|
485
|
+
return this.htmlToImage(cardHtml);
|
|
371
486
|
}
|
|
372
487
|
};
|
|
373
488
|
|
|
@@ -395,7 +510,7 @@ var Stat = class {
|
|
|
395
510
|
*/
|
|
396
511
|
registerCommands(analyse) {
|
|
397
512
|
if (this.config.enableCmdStat) {
|
|
398
|
-
analyse.subcommand(".cmd", "命令使用统计").option("user", "-u
|
|
513
|
+
analyse.subcommand(".cmd", "命令使用统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 展示全局统计").action(async ({ session, options }) => {
|
|
399
514
|
const scope = this.parseQueryScope(session, options);
|
|
400
515
|
if (scope.error) return scope.error;
|
|
401
516
|
try {
|
|
@@ -412,7 +527,7 @@ var Stat = class {
|
|
|
412
527
|
});
|
|
413
528
|
}
|
|
414
529
|
if (this.config.enableMsgStat) {
|
|
415
|
-
analyse.subcommand(".msg", "消息发送统计").option("user", "-u
|
|
530
|
+
analyse.subcommand(".msg", "消息发送统计").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("type", "-t <type:string> 指定类型").option("all", "-a 展示全局统计").action(async ({ session, options }) => {
|
|
416
531
|
const scope = this.parseQueryScope(session, options);
|
|
417
532
|
if (scope.error) return scope.error;
|
|
418
533
|
try {
|
|
@@ -424,11 +539,11 @@ var Stat = class {
|
|
|
424
539
|
const result = await this.renderer.renderList(renderData, ["用户", "条数", "最后发言"]);
|
|
425
540
|
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
426
541
|
} else {
|
|
427
|
-
const stats = await this.
|
|
542
|
+
const stats = await this.getUserMessageStats(scope.guildId, scope.userId);
|
|
428
543
|
if (typeof stats === "string") return stats;
|
|
429
544
|
const title = await this.generateTitle(scope.guildId, scope.userId, { main: "消息" });
|
|
430
545
|
const renderData = { title, time: /* @__PURE__ */ new Date(), total: stats.total, list: stats.list };
|
|
431
|
-
const result = await this.renderer.renderList(renderData, ["
|
|
546
|
+
const result = await this.renderer.renderList(renderData, ["用户", "总计发言", "最后发言"]);
|
|
432
547
|
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
433
548
|
}
|
|
434
549
|
} catch (error) {
|
|
@@ -438,9 +553,8 @@ var Stat = class {
|
|
|
438
553
|
});
|
|
439
554
|
}
|
|
440
555
|
if (this.config.enableRankStat) {
|
|
441
|
-
analyse.subcommand(".rank", "用户发言排行").option("guild", "-g
|
|
442
|
-
|
|
443
|
-
if (!session.guildId) return "请指定群组 ID";
|
|
556
|
+
analyse.subcommand(".rank", "用户发言排行").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 展示全局统计").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).action(async ({ session, options }) => {
|
|
557
|
+
const guildId = options.all ? void 0 : options.guild || session.guildId;
|
|
444
558
|
if (!guildId && !options.all) return "请提供查询范围";
|
|
445
559
|
try {
|
|
446
560
|
const stats = await this.getActiveUserStats(options.hours, guildId);
|
|
@@ -470,15 +584,20 @@ var Stat = class {
|
|
|
470
584
|
*/
|
|
471
585
|
parseQueryScope(session, options) {
|
|
472
586
|
let userId, guildId;
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
587
|
+
if (options.user) {
|
|
588
|
+
const atElements = import_koishi3.h.select(options.user, "at");
|
|
589
|
+
if (atElements.length > 0) {
|
|
590
|
+
userId = atElements[0].attrs.id;
|
|
591
|
+
} else {
|
|
592
|
+
userId = options.user.trim();
|
|
593
|
+
}
|
|
479
594
|
}
|
|
595
|
+
if (options.guild) guildId = options.guild;
|
|
480
596
|
if (options.all) return { userId, guildId: void 0 };
|
|
481
|
-
if (!
|
|
597
|
+
if (!userId && !guildId) {
|
|
598
|
+
if (session.guildId) return { guildId: session.guildId };
|
|
599
|
+
return { error: "请提供查询范围" };
|
|
600
|
+
}
|
|
482
601
|
return { userId, guildId };
|
|
483
602
|
}
|
|
484
603
|
/**
|
|
@@ -524,9 +643,9 @@ var Stat = class {
|
|
|
524
643
|
const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
|
|
525
644
|
scopeText = guild[0]?.channelName || guildId;
|
|
526
645
|
}
|
|
527
|
-
if (options.main === "排行") return `${scopeText}
|
|
528
|
-
if (options.main === "消息" && options.subtype) return `${scopeText}
|
|
529
|
-
return `${scopeText}
|
|
646
|
+
if (options.main === "排行") return `${scopeText}${options.timeRange}小时消息排行`;
|
|
647
|
+
if (options.main === "消息" && options.subtype) return `${scopeText}"${options.subtype}"消息统计`;
|
|
648
|
+
return `${scopeText}${options.main}统计`;
|
|
530
649
|
}
|
|
531
650
|
/**
|
|
532
651
|
* @private
|
|
@@ -549,19 +668,27 @@ var Stat = class {
|
|
|
549
668
|
/**
|
|
550
669
|
* @private
|
|
551
670
|
* @async
|
|
552
|
-
* @method
|
|
553
|
-
* @description
|
|
671
|
+
* @method getUserMessageStats
|
|
672
|
+
* @description 从数据库中获取并聚合每个用户的消息统计数据。
|
|
554
673
|
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
555
674
|
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
556
675
|
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
557
676
|
*/
|
|
558
|
-
async
|
|
559
|
-
const
|
|
560
|
-
if (
|
|
561
|
-
|
|
677
|
+
async getUserMessageStats(guildId, userId) {
|
|
678
|
+
const query = {};
|
|
679
|
+
if (guildId) query.channelId = guildId;
|
|
680
|
+
if (userId) query.userId = userId;
|
|
681
|
+
const users = await this.ctx.database.get("analyse_user", query, ["uid", "userName"]);
|
|
682
|
+
if (users.length === 0) return "暂无统计数据";
|
|
683
|
+
const uids = users.map((u) => u.uid);
|
|
684
|
+
const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
|
|
685
|
+
const stats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids } }).groupBy("uid", {
|
|
686
|
+
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
687
|
+
lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
|
|
688
|
+
}).orderBy("count", "desc").execute();
|
|
562
689
|
if (stats.length === 0) return "暂无统计数据";
|
|
563
690
|
const total = stats.reduce((sum, record) => sum + record.count, 0);
|
|
564
|
-
const list = stats.map((item) => [item.
|
|
691
|
+
const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count, item.lastUsed]);
|
|
565
692
|
return { list, total };
|
|
566
693
|
}
|
|
567
694
|
/**
|