koishi-plugin-chat-analyse 0.5.2 → 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 +22 -22
- package/lib/Stat.d.ts +37 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +65 -39
- package/package.json +1 -1
package/lib/Renderer.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Context } from 'koishi';
|
|
|
3
3
|
export type RenderListItem = (string | number | Date)[];
|
|
4
4
|
/**
|
|
5
5
|
* @interface ListRenderData
|
|
6
|
-
* @description 定义了调用 `renderList`
|
|
6
|
+
* @description 定义了调用 `renderList` 方法所需的数据结构。
|
|
7
7
|
*/
|
|
8
8
|
export interface ListRenderData {
|
|
9
9
|
title: string;
|
|
@@ -11,39 +11,39 @@ export interface ListRenderData {
|
|
|
11
11
|
total?: string | number;
|
|
12
12
|
list: RenderListItem[];
|
|
13
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
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* @class Renderer
|
|
16
|
-
* @description 负责将结构化的列表数据渲染为设计精美的 PNG
|
|
26
|
+
* @description 负责将结构化的列表数据渲染为设计精美的 PNG 图片。
|
|
17
27
|
*/
|
|
18
28
|
export declare class Renderer {
|
|
19
29
|
private ctx;
|
|
20
30
|
/**
|
|
21
|
-
* @
|
|
31
|
+
* @private @readonly
|
|
32
|
+
* @property COMMON_STYLE - 存储所有卡片共享的基础 CSS 样式。
|
|
22
33
|
*/
|
|
34
|
+
private readonly COMMON_STYLE;
|
|
23
35
|
constructor(ctx: Context);
|
|
24
36
|
/**
|
|
25
37
|
* @private
|
|
26
|
-
* @method
|
|
27
|
-
* @description
|
|
28
|
-
* @param
|
|
29
|
-
* @
|
|
38
|
+
* @method generateFullHtml
|
|
39
|
+
* @description 将卡片内容和特定样式组合成一个完整的 HTML 文档。
|
|
40
|
+
* @param cardContent - 卡片部分的 HTML 字符串。
|
|
41
|
+
* @param specificStyles - 该卡片类型独有的 CSS 样式字符串。
|
|
42
|
+
* @returns 完整的 HTML 字符串。
|
|
30
43
|
*/
|
|
44
|
+
private generateFullHtml;
|
|
31
45
|
private htmlToImage;
|
|
32
|
-
/**
|
|
33
|
-
* @private
|
|
34
|
-
* @method formatDate
|
|
35
|
-
* @description 将 `Date` 对象格式化为易于理解的相对时间或绝对日期字符串。
|
|
36
|
-
* @param date - 需要格式化的日期对象。
|
|
37
|
-
* @returns 格式化后的时间字符串。
|
|
38
|
-
*/
|
|
39
46
|
private formatDate;
|
|
40
|
-
/**
|
|
41
|
-
* @public
|
|
42
|
-
* @method renderList
|
|
43
|
-
* @description 构建并渲染一个包含标题、统计信息和数据表格的 HTML 卡片为图片。如果数据过多,则会分片渲染成多张图片。
|
|
44
|
-
* @param data - 包含渲染所需全部信息的对象。
|
|
45
|
-
* @param headers - (可选) 表格的表头字符串数组。
|
|
46
|
-
* @returns 成功时返回包含 PNG 图片的 Buffer 数组,若列表为空则返回提示字符串。
|
|
47
|
-
*/
|
|
48
47
|
renderList(data: ListRenderData, headers?: string[]): Promise<string | Buffer[]>;
|
|
48
|
+
renderCircadianChart(data: CircadianChartData): Promise<string | Buffer[]>;
|
|
49
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,22 +210,37 @@ 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
|
*/
|
|
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
|
+
}
|
|
229
244
|
async htmlToImage(fullHtmlContent) {
|
|
230
245
|
const page = await this.ctx.puppeteer.page();
|
|
231
246
|
try {
|
|
@@ -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,20 +263,10 @@ 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 "暂无数据可供渲染";
|
|
@@ -288,6 +286,7 @@ var Renderer = class {
|
|
|
288
286
|
return `<td class="name-cell">${String(cell)}</td>`;
|
|
289
287
|
}, "renderCell");
|
|
290
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}`;
|
|
291
290
|
for (let i = 0; i < totalItems; i += CHUNK_SIZE) {
|
|
292
291
|
const chunk = list.slice(i, i + CHUNK_SIZE);
|
|
293
292
|
const pageNum = Math.floor(i / CHUNK_SIZE) + 1;
|
|
@@ -299,23 +298,27 @@ var Renderer = class {
|
|
|
299
298
|
}).join("");
|
|
300
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>` : "";
|
|
301
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>`;
|
|
302
|
-
const fullHtml =
|
|
303
|
-
<html>
|
|
304
|
-
<head>
|
|
305
|
-
<meta charset="UTF-8">
|
|
306
|
-
<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}.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}</style>
|
|
307
|
-
</head>
|
|
308
|
-
<body>
|
|
309
|
-
${cardHtml}
|
|
310
|
-
</body>
|
|
311
|
-
</html>`;
|
|
301
|
+
const fullHtml = this.generateFullHtml(cardHtml, listStyles);
|
|
312
302
|
const imageBuffer = await this.htmlToImage(fullHtml);
|
|
313
|
-
if (imageBuffer)
|
|
314
|
-
imageBuffers.push(imageBuffer);
|
|
315
|
-
}
|
|
303
|
+
if (imageBuffer) imageBuffers.push(imageBuffer);
|
|
316
304
|
}
|
|
317
305
|
return imageBuffers.length > 0 ? imageBuffers : "图片渲染失败";
|
|
318
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>`;
|
|
315
|
+
}).join("");
|
|
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] : "图片渲染失败";
|
|
321
|
+
}
|
|
319
322
|
};
|
|
320
323
|
|
|
321
324
|
// src/Stat.ts
|
|
@@ -341,7 +344,7 @@ var Stat = class {
|
|
|
341
344
|
renderer;
|
|
342
345
|
/**
|
|
343
346
|
* @public @method registerCommands
|
|
344
|
-
* @description
|
|
347
|
+
* @description 根据配置,动态地将子命令注册到主 `analyse` 命令下。
|
|
345
348
|
* @param analyse - 主 `analyse` 命令实例。
|
|
346
349
|
*/
|
|
347
350
|
registerCommands(analyse) {
|
|
@@ -426,6 +429,28 @@ var Stat = class {
|
|
|
426
429
|
return "渲染发言排行图片失败";
|
|
427
430
|
}
|
|
428
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
|
+
}));
|
|
429
454
|
}
|
|
430
455
|
/**
|
|
431
456
|
* @private @method parseQueryScope
|
|
@@ -670,6 +695,7 @@ var Config = import_koishi6.Schema.intersect([
|
|
|
670
695
|
import_koishi6.Schema.object({
|
|
671
696
|
enableCmdStat: import_koishi6.Schema.boolean().default(true).description("启用命令统计"),
|
|
672
697
|
enableMsgStat: import_koishi6.Schema.boolean().default(true).description("启用消息统计"),
|
|
698
|
+
enableActivityStat: import_koishi6.Schema.boolean().default(true).description("启用活跃分析"),
|
|
673
699
|
enableOriRecord: import_koishi6.Schema.boolean().default(true).description("启用原始记录")
|
|
674
700
|
}).description("功能配置"),
|
|
675
701
|
import_koishi6.Schema.object({
|