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.
@@ -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 htmlToImage
225
- * @description HTML 字符串转换为 PNG 图片 Buffer。
226
- * @param html - 要渲染的 HTML 主体内容。
227
- * @returns 返回一个包含 PNG 图片数据的 Buffer。
226
+ * @method generateFullHtml
227
+ * @description 将卡片内容和特定样式组合成一个完整的 HTML 文档。
228
+ * @param cardContent - 卡片部分的 HTML 字符串。
229
+ * @param specificStyles - 该卡片类型独有的 CSS 样式字符串。
230
+ * @returns 完整的 HTML 字符串。
228
231
  */
229
- async htmlToImage(html) {
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(`<!DOCTYPE html><html><head><meta charset="UTF-8"><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></head><body>${html}</body></html>`, { waitUntil: "networkidle0" });
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 totalValue = countHeaderIndex > -1 ? list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0) : 0;
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 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>` : "";
289
- const tableRowsHtml = list.map((row, index) => {
290
- const rank = index + 1;
291
- const rankClass = rank === 1 ? "rank-gold" : rank === 2 ? "rank-silver" : rank === 3 ? "rank-bronze" : "";
292
- return `<tr><td class="rank-cell ${rankClass}">${rank}</td>${row.map(renderCell).join("")}</tr>`;
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 totalCount === "number" ? totalCount.toLocaleString() : totalCount}</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="table-container"><table class="main-table">${tableHeadHtml}<tbody>${tableRowsHtml}</tbody></table></div></div>`;
295
- return this.htmlToImage(cardHtml);
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 根据配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
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
- return Buffer.isBuffer(result) ? import_koishi3.h.image(result, "image/png") : result;
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").limit(100).execute();
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
- return Buffer.isBuffer(result) ? import_koishi3.h.image(result, "image/png") : result;
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({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.5.1",
4
+ "version": "0.5.3",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],