koishi-plugin-chat-analyse 0.5.1 → 0.5.3
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 +49 -0
- package/lib/Stat.d.ts +37 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +99 -40
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
/** 定义了渲染列表中单行数据的格式,是一个由字符串、数字或 `Date` 对象构成的数组。 */
|
|
3
|
+
export type RenderListItem = (string | number | Date)[];
|
|
4
|
+
/**
|
|
5
|
+
* @interface ListRenderData
|
|
6
|
+
* @description 定义了调用 `renderList` 方法所需的数据结构。
|
|
7
|
+
*/
|
|
8
|
+
export interface ListRenderData {
|
|
9
|
+
title: string;
|
|
10
|
+
time: Date;
|
|
11
|
+
total?: string | number;
|
|
12
|
+
list: RenderListItem[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @interface CircadianChartData
|
|
16
|
+
* @description 定义了调用 `renderCircadianChart` 方法所需的数据结构。
|
|
17
|
+
*/
|
|
18
|
+
export interface CircadianChartData {
|
|
19
|
+
title: string;
|
|
20
|
+
time: Date;
|
|
21
|
+
total: string | number;
|
|
22
|
+
data: number[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @class Renderer
|
|
26
|
+
* @description 负责将结构化的列表数据渲染为设计精美的 PNG 图片。
|
|
27
|
+
*/
|
|
28
|
+
export declare class Renderer {
|
|
29
|
+
private ctx;
|
|
30
|
+
/**
|
|
31
|
+
* @private @readonly
|
|
32
|
+
* @property COMMON_STYLE - 存储所有卡片共享的基础 CSS 样式。
|
|
33
|
+
*/
|
|
34
|
+
private readonly COMMON_STYLE;
|
|
35
|
+
constructor(ctx: Context);
|
|
36
|
+
/**
|
|
37
|
+
* @private
|
|
38
|
+
* @method generateFullHtml
|
|
39
|
+
* @description 将卡片内容和特定样式组合成一个完整的 HTML 文档。
|
|
40
|
+
* @param cardContent - 卡片部分的 HTML 字符串。
|
|
41
|
+
* @param specificStyles - 该卡片类型独有的 CSS 样式字符串。
|
|
42
|
+
* @returns 完整的 HTML 字符串。
|
|
43
|
+
*/
|
|
44
|
+
private generateFullHtml;
|
|
45
|
+
private htmlToImage;
|
|
46
|
+
private formatDate;
|
|
47
|
+
renderList(data: ListRenderData, headers?: string[]): Promise<string | Buffer[]>;
|
|
48
|
+
renderCircadianChart(data: CircadianChartData): Promise<string | Buffer[]>;
|
|
49
|
+
}
|
package/lib/Stat.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context, Command } from 'koishi';
|
|
2
|
+
import { Renderer } from './Renderer';
|
|
3
|
+
import { Config } from './index';
|
|
4
|
+
/**
|
|
5
|
+
* @class Stat
|
|
6
|
+
* @description 提供统一的统计查询服务。负责注册查询命令,从数据库获取数据,并调用渲染器生成图表。
|
|
7
|
+
*/
|
|
8
|
+
export declare class Stat {
|
|
9
|
+
private ctx;
|
|
10
|
+
private config;
|
|
11
|
+
renderer: Renderer;
|
|
12
|
+
/**
|
|
13
|
+
* @param ctx - Koishi 的插件上下文。
|
|
14
|
+
* @param config - 插件的配置对象。
|
|
15
|
+
*/
|
|
16
|
+
constructor(ctx: Context, config: Config);
|
|
17
|
+
/**
|
|
18
|
+
* @public @method registerCommands
|
|
19
|
+
* @description 根据配置,动态地将子命令注册到主 `analyse` 命令下。
|
|
20
|
+
* @param analyse - 主 `analyse` 命令实例。
|
|
21
|
+
*/
|
|
22
|
+
registerCommands(analyse: Command): void;
|
|
23
|
+
/**
|
|
24
|
+
* @private @method parseQueryScope
|
|
25
|
+
* @description 解析命令选项,转换为包含 UIDs 和描述性信息的统一查询范围对象。
|
|
26
|
+
* @param session - 当前会话对象。
|
|
27
|
+
* @param options - 命令选项。
|
|
28
|
+
* @returns 包含 uids、错误或范围描述的查询范围对象。
|
|
29
|
+
*/
|
|
30
|
+
private parseQueryScope;
|
|
31
|
+
/**
|
|
32
|
+
* @private @method generateTitle
|
|
33
|
+
* @description 根据查询范围和类型动态生成易于理解的图片标题。
|
|
34
|
+
* @returns 生成的标题字符串。
|
|
35
|
+
*/
|
|
36
|
+
private generateTitle;
|
|
37
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
/** @name 插件使用说明 */
|
|
3
|
+
export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
|
|
4
|
+
export declare const name = "chat-analyse";
|
|
5
|
+
export declare const using: string[];
|
|
6
|
+
/**
|
|
7
|
+
* @interface Config
|
|
8
|
+
* @description 定义插件的配置项结构。
|
|
9
|
+
*/
|
|
10
|
+
export interface Config {
|
|
11
|
+
enableListener: boolean;
|
|
12
|
+
enableCmdStat: boolean;
|
|
13
|
+
enableMsgStat: boolean;
|
|
14
|
+
enableRankStat: boolean;
|
|
15
|
+
enableActivityStat: boolean;
|
|
16
|
+
enableOriRecord: boolean;
|
|
17
|
+
enableWhoAt: boolean;
|
|
18
|
+
enableData: boolean;
|
|
19
|
+
atRetentionDays: number;
|
|
20
|
+
rankRetentionDays: number;
|
|
21
|
+
}
|
|
22
|
+
/** @description 插件的配置项定义,使用 Koishi Schema 构建。 */
|
|
23
|
+
export declare const Config: Schema<Config>;
|
|
24
|
+
/**
|
|
25
|
+
* @function apply
|
|
26
|
+
* @description Koishi 插件的主入口函数,负责初始化和注册所有功能模块。
|
|
27
|
+
* @param ctx - Koishi 的插件上下文。
|
|
28
|
+
* @param config - 用户配置对象。
|
|
29
|
+
*/
|
|
30
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -210,27 +210,42 @@ var import_koishi3 = require("koishi");
|
|
|
210
210
|
// src/Renderer.ts
|
|
211
211
|
var import_koishi2 = require("koishi");
|
|
212
212
|
var Renderer = class {
|
|
213
|
-
/**
|
|
214
|
-
* @param ctx - Koishi 的插件上下文,用于访问 `puppeteer` 等核心服务。
|
|
215
|
-
*/
|
|
216
213
|
constructor(ctx) {
|
|
217
214
|
this.ctx = ctx;
|
|
218
215
|
}
|
|
219
216
|
static {
|
|
220
217
|
__name(this, "Renderer");
|
|
221
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* @private @readonly
|
|
221
|
+
* @property COMMON_STYLE - 存储所有卡片共享的基础 CSS 样式。
|
|
222
|
+
*/
|
|
223
|
+
COMMON_STYLE = `:root{--card-bg:#fff;--text-color:#111827;--header-color:#111827;--sub-text-color:#6b7280;--border-color:#e5e7eb;--accent-color:#4a6ee0;--chip-bg:#f3f4f6;--stripe-bg:#f9fafb;--gold:#f59e0b;--silver:#9ca3af;--bronze:#a16207}body{display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:0 0;margin:0;padding:8px;-webkit-font-smoothing:antialiased}.container{display:inline-block;background:var(--card-bg);border-radius:12px;padding:0;overflow:hidden;box-shadow:0 2px 4px rgba(0,0,0,.05)}.header{padding:10px 14px}.header-table{border-collapse:collapse;width:100%}.header-table-left,.header-table-right{width:1%;white-space:nowrap}.header-table-left{text-align:left}.header-table-center{text-align:center}.header-table-right{text-align:right}.title-text{font-size:18px;font-weight:600;color:var(--header-color);margin:0}.stat-chip,.time-label{display:inline-flex;align-items:baseline;padding:4px 8px;border-radius:8px;background:var(--chip-bg);font-size:13px;color:var(--sub-text-color)}.stat-chip span{font-weight:600;color:var(--text-color);margin-left:4px}`;
|
|
222
224
|
/**
|
|
223
225
|
* @private
|
|
224
|
-
* @method
|
|
225
|
-
* @description
|
|
226
|
-
* @param
|
|
227
|
-
* @
|
|
226
|
+
* @method generateFullHtml
|
|
227
|
+
* @description 将卡片内容和特定样式组合成一个完整的 HTML 文档。
|
|
228
|
+
* @param cardContent - 卡片部分的 HTML 字符串。
|
|
229
|
+
* @param specificStyles - 该卡片类型独有的 CSS 样式字符串。
|
|
230
|
+
* @returns 完整的 HTML 字符串。
|
|
228
231
|
*/
|
|
229
|
-
|
|
232
|
+
generateFullHtml(cardContent, specificStyles) {
|
|
233
|
+
return `<!DOCTYPE html>
|
|
234
|
+
<html>
|
|
235
|
+
<head>
|
|
236
|
+
<meta charset="UTF-8">
|
|
237
|
+
<style>${this.COMMON_STYLE}${specificStyles}</style>
|
|
238
|
+
</head>
|
|
239
|
+
<body>
|
|
240
|
+
${cardContent}
|
|
241
|
+
</body>
|
|
242
|
+
</html>`;
|
|
243
|
+
}
|
|
244
|
+
async htmlToImage(fullHtmlContent) {
|
|
230
245
|
const page = await this.ctx.puppeteer.page();
|
|
231
246
|
try {
|
|
232
247
|
await page.setViewport({ width: 720, height: 1080, deviceScaleFactor: 2 });
|
|
233
|
-
await page.setContent(
|
|
248
|
+
await page.setContent(fullHtmlContent, { waitUntil: "networkidle0" });
|
|
234
249
|
const dimensions = await page.evaluate(() => ({ width: document.body.scrollWidth, height: document.body.scrollHeight }));
|
|
235
250
|
await page.setViewport({ ...dimensions, deviceScaleFactor: 2 });
|
|
236
251
|
return await page.screenshot({ type: "png", fullPage: true, omitBackground: true });
|
|
@@ -241,13 +256,6 @@ var Renderer = class {
|
|
|
241
256
|
if (page) await page.close().catch((e) => this.ctx.logger.error("关闭页面失败:", e));
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
|
-
/**
|
|
245
|
-
* @private
|
|
246
|
-
* @method formatDate
|
|
247
|
-
* @description 将 `Date` 对象格式化为易于理解的相对时间或绝对日期字符串。
|
|
248
|
-
* @param date - 需要格式化的日期对象。
|
|
249
|
-
* @returns 格式化后的时间字符串。
|
|
250
|
-
*/
|
|
251
259
|
formatDate(date) {
|
|
252
260
|
if (!date) return "未知";
|
|
253
261
|
const diff = Date.now() - date.getTime();
|
|
@@ -255,26 +263,18 @@ var Renderer = class {
|
|
|
255
263
|
if (diff > 365 * import_koishi2.Time.day) return date.toLocaleDateString("zh-CN").replace(/\//g, "-");
|
|
256
264
|
const timeUnits = [["月", 30 * import_koishi2.Time.day], ["天", import_koishi2.Time.day], ["时", import_koishi2.Time.hour], ["分", import_koishi2.Time.minute]];
|
|
257
265
|
for (const [unit, ms] of timeUnits) {
|
|
258
|
-
if (diff >= ms) {
|
|
259
|
-
return `${Math.floor(diff / ms)}${unit}前`;
|
|
260
|
-
}
|
|
266
|
+
if (diff >= ms) return `${Math.floor(diff / ms)}${unit}前`;
|
|
261
267
|
}
|
|
262
268
|
return "刚刚";
|
|
263
269
|
}
|
|
264
|
-
/**
|
|
265
|
-
* @public
|
|
266
|
-
* @method renderList
|
|
267
|
-
* @description 构建并渲染一个包含标题、统计信息和数据表格的 HTML 卡片为图片。
|
|
268
|
-
* @param data - 包含渲染所需全部信息的对象。
|
|
269
|
-
* @param headers - (可选) 表格的表头字符串数组。
|
|
270
|
-
* @returns 成功时返回包含 PNG 图片的 Buffer,若列表为空则返回提示字符串。
|
|
271
|
-
*/
|
|
272
270
|
async renderList(data, headers) {
|
|
273
271
|
const { title, time, list } = data;
|
|
274
272
|
if (!list?.length) return "暂无数据可供渲染";
|
|
273
|
+
const CHUNK_SIZE = 100;
|
|
274
|
+
const imageBuffers = [];
|
|
275
|
+
const totalItems = list.length;
|
|
275
276
|
const countHeaderIndex = headers?.findIndex((h4) => ["总计发言", "条数", "次数", "数量"].includes(h4)) ?? -1;
|
|
276
|
-
const
|
|
277
|
-
const totalCount = data.total || totalValue;
|
|
277
|
+
const totalCount = data.total || (countHeaderIndex > -1 ? list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0) : totalItems);
|
|
278
278
|
const renderCell = /* @__PURE__ */ __name((cell, i) => {
|
|
279
279
|
const headerText = headers?.[i] || "";
|
|
280
280
|
if (headerText.includes("占比")) {
|
|
@@ -285,14 +285,39 @@ var Renderer = class {
|
|
|
285
285
|
if (typeof cell === "number") return `<td class="count-cell">${cell.toLocaleString()}</td>`;
|
|
286
286
|
return `<td class="name-cell">${String(cell)}</td>`;
|
|
287
287
|
}, "renderCell");
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
288
|
+
const totalPages = Math.ceil(totalItems / CHUNK_SIZE);
|
|
289
|
+
const listStyles = `.table-container{border-top:1px solid var(--border-color)}.main-table{border-collapse:collapse;width:100%}.main-table th,.main-table td{padding:8px 14px;vertical-align:middle}.main-table th{font-size:12px;font-weight:500;color:var(--sub-text-color);text-transform:uppercase;letter-spacing:.05em;background:var(--stripe-bg)}.main-table td{font-size:14px;color:var(--text-color)}.main-table tbody tr:nth-child(even){background-color:var(--stripe-bg)}.main-table .name-cell,.main-table .name-header{text-align:left}.main-table .rank-cell,.main-table .count-cell,.main-table .date-cell,.main-table .percent-cell,.main-table .header-right-align{text-align:right;white-space:nowrap;width:1%;font-variant-numeric:tabular-nums}.name-cell{font-weight:500}.rank-cell{font-weight:500;color:var(--sub-text-color)}.count-cell{font-weight:600;color:var(--accent-color)}.date-cell{color:var(--sub-text-color)}.rank-gold,.rank-silver,.rank-bronze{font-weight:600}.rank-gold{color:var(--gold)!important}.rank-silver{color:var(--silver)!important}.rank-bronze{color:var(--bronze)!important}.percent-cell{position:relative}.percent-bar{position:absolute;top:0;right:0;height:100%;background-color:var(--accent-color);opacity:.15}.percent-text{position:relative;z-index:1}`;
|
|
290
|
+
for (let i = 0; i < totalItems; i += CHUNK_SIZE) {
|
|
291
|
+
const chunk = list.slice(i, i + CHUNK_SIZE);
|
|
292
|
+
const pageNum = Math.floor(i / CHUNK_SIZE) + 1;
|
|
293
|
+
const pageTitle = totalPages > 1 ? `${title} (第 ${pageNum}/${totalPages} 页)` : title;
|
|
294
|
+
const tableRowsHtml = chunk.map((row, index) => {
|
|
295
|
+
const rank = i + index + 1;
|
|
296
|
+
const rankClass = rank === 1 ? "rank-gold" : rank === 2 ? "rank-silver" : rank === 3 ? "rank-bronze" : "";
|
|
297
|
+
return `<tr><td class="rank-cell ${rankClass}">${rank}</td>${row.map(renderCell).join("")}</tr>`;
|
|
298
|
+
}).join("");
|
|
299
|
+
const tableHeadHtml = headers?.length ? `<thead><tr><th class="rank-cell">#</th>${headers.map((h4) => `<th class="${typeof list[0]?.[headers.indexOf(h4)] === "string" ? "name-header" : "header-right-align"}">${h4}</th>`).join("")}</tr></thead>` : "";
|
|
300
|
+
const cardHtml = `<div class="container"><div class="header"><table class="header-table"><tr><td class="header-table-left"><div class="stat-chip">总计: <span>${typeof totalCount === "number" ? totalCount.toLocaleString() : totalCount}</span></div></td><td class="header-table-center"><h1 class="title-text">${pageTitle}</h1></td><td class="header-table-right"><div class="time-label">${time.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-")}</div></td></tr></table></div><div class="table-container"><table class="main-table">${tableHeadHtml}<tbody>${tableRowsHtml}</tbody></table></div></div>`;
|
|
301
|
+
const fullHtml = this.generateFullHtml(cardHtml, listStyles);
|
|
302
|
+
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
303
|
+
if (imageBuffer) imageBuffers.push(imageBuffer);
|
|
304
|
+
}
|
|
305
|
+
return imageBuffers.length > 0 ? imageBuffers : "图片渲染失败";
|
|
306
|
+
}
|
|
307
|
+
async renderCircadianChart(data) {
|
|
308
|
+
const { title, time, total, data: hourlyCounts } = data;
|
|
309
|
+
if (!hourlyCounts || hourlyCounts.every((c) => c === 0)) return "暂无数据可供渲染";
|
|
310
|
+
const maxCount = Math.max(...hourlyCounts, 1);
|
|
311
|
+
const barsHtml = hourlyCounts.map((count, hour) => {
|
|
312
|
+
const barHeight = count / maxCount * 100;
|
|
313
|
+
const isPeak = count > 0 && count === maxCount;
|
|
314
|
+
return `<div class="bar-wrapper"><div class="bar-value ${count === 0 ? "hidden" : ""}">${count}</div><div class="bar-container"><div class="bar ${isPeak ? "peak" : ""}" style="height: ${barHeight}%;"></div></div><div class="bar-label">${hour}</div></div>`;
|
|
293
315
|
}).join("");
|
|
294
|
-
const cardHtml = `<div class="container"><div class="header"><table class="header-table"><tr><td class="header-table-left"><div class="stat-chip">总计: <span>${typeof
|
|
295
|
-
|
|
316
|
+
const cardHtml = `<div class="container"><div class="header"><table class="header-table"><tr><td class="header-table-left"><div class="stat-chip">总计: <span>${typeof total === "number" ? total.toLocaleString() : total}</span></div></td><td class="header-table-center"><h1 class="title-text">${title}</h1></td><td class="header-table-right"><div class="time-label">${time.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-")}</div></td></tr></table></div><div class="chart-container">${barsHtml}</div></div>`;
|
|
317
|
+
const chartStyles = `.chart-container{display:flex;align-items:flex-end;gap:4px;height:180px;padding:30px 15px 10px;border-top:1px solid var(--border-color)}.bar-wrapper{flex:1;text-align:center;display:flex;flex-direction:column;height:100%;justify-content:flex-end}.bar-value{font-size:11px;color:var(--sub-text-color);height:16px;line-height:16px;font-weight:500}.bar-value.hidden{visibility:hidden}.bar-container{flex-grow:1;display:flex;align-items:flex-end;width:100%}.bar{width:100%;background-color:var(--accent-color);opacity:.7;border-radius:3px 3px 0 0;transition:height .3s ease-out}.bar.peak{opacity:1;background-color:var(--gold)!important}.bar-label{font-size:10px;color:var(--sub-text-color);margin-top:4px;height:12px}`;
|
|
318
|
+
const fullHtml = this.generateFullHtml(cardHtml, chartStyles);
|
|
319
|
+
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
320
|
+
return imageBuffer ? [imageBuffer] : "图片渲染失败";
|
|
296
321
|
}
|
|
297
322
|
};
|
|
298
323
|
|
|
@@ -319,7 +344,7 @@ var Stat = class {
|
|
|
319
344
|
renderer;
|
|
320
345
|
/**
|
|
321
346
|
* @public @method registerCommands
|
|
322
|
-
* @description
|
|
347
|
+
* @description 根据配置,动态地将子命令注册到主 `analyse` 命令下。
|
|
323
348
|
* @param analyse - 主 `analyse` 命令实例。
|
|
324
349
|
*/
|
|
325
350
|
registerCommands(analyse) {
|
|
@@ -329,7 +354,13 @@ var Stat = class {
|
|
|
329
354
|
if (scope.error) return scope.error;
|
|
330
355
|
try {
|
|
331
356
|
const result = await handler(scope, options);
|
|
332
|
-
|
|
357
|
+
if (typeof result === "string") return result;
|
|
358
|
+
if (Array.isArray(result)) {
|
|
359
|
+
if (result.length === 0) return "图片渲染失败";
|
|
360
|
+
for (const buffer of result) await session.sendQueued(import_koishi3.h.image(buffer, "image/png"));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (Buffer.isBuffer(result)) return import_koishi3.h.image(result, "image/png");
|
|
333
364
|
} catch (error) {
|
|
334
365
|
this.ctx.logger.error("渲染统计图片失败:", error);
|
|
335
366
|
return "渲染统计图片失败";
|
|
@@ -377,7 +408,7 @@ var Stat = class {
|
|
|
377
408
|
const uidsInScope = guildId ? (await this.ctx.database.get("analyse_user", { channelId: guildId }, ["uid"])).map((u) => u.uid) : void 0;
|
|
378
409
|
if (guildId && uidsInScope.length === 0) return "暂无指定时段内发言记录";
|
|
379
410
|
if (uidsInScope) baseQuery.uid = { $in: uidsInScope };
|
|
380
|
-
const rankStats = await this.ctx.database.select("analyse_rank").where(baseQuery).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").
|
|
411
|
+
const rankStats = await this.ctx.database.select("analyse_rank").where(baseQuery).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").execute();
|
|
381
412
|
if (rankStats.length === 0) return "暂无指定时段内发言记录";
|
|
382
413
|
const uids = rankStats.map((s) => s.uid);
|
|
383
414
|
const users = await this.ctx.database.get("analyse_user", { uid: { $in: uids } }, ["uid", "userName"]);
|
|
@@ -387,12 +418,39 @@ var Stat = class {
|
|
|
387
418
|
const listWithPercentage = list.map((row) => [...row, total > 0 ? `${(row[1] / total * 100).toFixed(2)}%` : "0.00%"]);
|
|
388
419
|
const title = await this.generateTitle({ guildId }, { main: "发言排行", timeRange: hours, subtype: type });
|
|
389
420
|
const result = await this.renderer.renderList({ title, time: /* @__PURE__ */ new Date(), total, list: listWithPercentage }, ["用户", "总计发言", "占比"]);
|
|
390
|
-
|
|
421
|
+
if (typeof result === "string") return result;
|
|
422
|
+
if (Array.isArray(result)) {
|
|
423
|
+
if (result.length === 0) return "图片渲染失败";
|
|
424
|
+
for (const buffer of result) await session.sendQueued(import_koishi3.h.image(buffer, "image/png"));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
391
427
|
} catch (error) {
|
|
392
428
|
this.ctx.logger.error("渲染发言排行图片失败:", error);
|
|
393
429
|
return "渲染发言排行图片失败";
|
|
394
430
|
}
|
|
395
431
|
});
|
|
432
|
+
if (this.config.enableActivityStat) analyse.subcommand(".activity", "用户活跃分析").option("user", "-u <user:string> 指定用户").option("guild", "-g <guildId:string> 指定群组").option("all", "-a 全局").action(createHandler(async (scope) => {
|
|
433
|
+
const hourlyStats = await this.ctx.database.select("analyse_rank").where({ uid: { $in: scope.uids } }).groupBy(
|
|
434
|
+
["timestamp"],
|
|
435
|
+
{ count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }
|
|
436
|
+
).execute();
|
|
437
|
+
if (hourlyStats.length === 0) return "暂无消息数据";
|
|
438
|
+
const hourlyCounts = Array(24).fill(0);
|
|
439
|
+
let totalMessages = 0;
|
|
440
|
+
hourlyStats.forEach((stat) => {
|
|
441
|
+
const hour = stat.timestamp.getHours();
|
|
442
|
+
hourlyCounts[hour] = stat.count;
|
|
443
|
+
totalMessages += stat.count;
|
|
444
|
+
});
|
|
445
|
+
const title = await this.generateTitle(scope.scopeDesc, { main: "活跃分析" });
|
|
446
|
+
const result = await this.renderer.renderCircadianChart({
|
|
447
|
+
title,
|
|
448
|
+
time: /* @__PURE__ */ new Date(),
|
|
449
|
+
total: totalMessages,
|
|
450
|
+
data: hourlyCounts
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
}));
|
|
396
454
|
}
|
|
397
455
|
/**
|
|
398
456
|
* @private @method parseQueryScope
|
|
@@ -637,6 +695,7 @@ var Config = import_koishi6.Schema.intersect([
|
|
|
637
695
|
import_koishi6.Schema.object({
|
|
638
696
|
enableCmdStat: import_koishi6.Schema.boolean().default(true).description("启用命令统计"),
|
|
639
697
|
enableMsgStat: import_koishi6.Schema.boolean().default(true).description("启用消息统计"),
|
|
698
|
+
enableActivityStat: import_koishi6.Schema.boolean().default(true).description("启用活跃分析"),
|
|
640
699
|
enableOriRecord: import_koishi6.Schema.boolean().default(true).description("启用原始记录")
|
|
641
700
|
}).description("功能配置"),
|
|
642
701
|
import_koishi6.Schema.object({
|