koishi-plugin-chat-analyse 0.3.3 → 0.3.4

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 CHANGED
@@ -1,13 +1,5 @@
1
1
  import { Context } from 'koishi';
2
- /**
3
- * @typedef {Array<string | number | Date>} RenderListItem
4
- * @description 定义了统计列表中单行数据的格式,它是一个由字符串、数字或日期组成的元组。
5
- */
6
2
  export type RenderListItem = (string | number | Date)[];
7
- /**
8
- * @interface ListRenderData
9
- * @description 定义了调用渲染器生成列表图片时所需的完整数据结构。
10
- */
11
3
  export interface ListRenderData {
12
4
  title: string;
13
5
  time: Date;
@@ -16,15 +8,10 @@ export interface ListRenderData {
16
8
  }
17
9
  /**
18
10
  * @class Renderer
19
- * @description 一个通用的列表渲染器。它使用 Koishi 的 Puppeteer 服务将结构化的 `ListRenderData` 数据
20
- * 渲染为一张包含精美表格的图片。
11
+ * @description 通用列表渲染器,使用 Puppeteer 将结构化数据渲染为精美的、类似卡片的表格图片。
21
12
  */
22
13
  export declare class Renderer {
23
14
  private ctx;
24
- /**
25
- * @constructor
26
- * @param {Context} ctx - Koishi 的插件上下文,用于访问 puppeteer 服务。
27
- */
28
15
  constructor(ctx: Context);
29
16
  /**
30
17
  * @public
@@ -39,7 +26,7 @@ export declare class Renderer {
39
26
  /**
40
27
  * @private
41
28
  * @method formatDate
42
- * @description 智能格式化日期。对于近期的时间,提供更人性化的相对时间描述(如“刚刚”,“x 分钟前”);对于较早的时间,则显示标准的“年-月-日”格式。
29
+ * @description 将日期格式化为包含两个单位的相对时间字符串(如“21天14时前”),如果超过一年则显示绝对日期。
43
30
  * @param {Date} date - 待格式化的 Date 对象。
44
31
  * @returns {string} 格式化后的日期字符串。
45
32
  */
package/lib/Stat.d.ts CHANGED
@@ -3,7 +3,7 @@ import { Renderer } from './Renderer';
3
3
  import { Config } from './index';
4
4
  /**
5
5
  * @class Stat
6
- * @description 提供统一的统计查询服务。它负责注册查询命令,根据用户输入从数据库中获取数据,并调用渲染器生成统计图表。
6
+ * @description 提供统一的统计查询服务。负责注册查询命令,从数据库获取数据,并调用渲染器生成图表。
7
7
  */
8
8
  export declare class Stat {
9
9
  private ctx;
@@ -16,11 +16,31 @@ export declare class Stat {
16
16
  */
17
17
  constructor(ctx: Context, config: Config);
18
18
  /**
19
+ * @public
19
20
  * @method registerCommands
20
21
  * @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
21
22
  * @param {Command} analyse - 主 `analyse` 命令实例。
22
23
  */
23
24
  registerCommands(analyse: Command): void;
25
+ /**
26
+ * @private
27
+ * @method parseQueryScope
28
+ * @description 解析命令的选项,将其转换为统一的查询范围对象(userId 和 guildId)。
29
+ * @param {Session} session - 当前会话对象。
30
+ * @param {QueryScopeOptions} options - 命令传入的选项。
31
+ * @returns {QueryScopeResult} 包含 userId、guildId 或 error 信息的查询范围对象。
32
+ */
33
+ private parseQueryScope;
34
+ /**
35
+ * @private
36
+ * @async
37
+ * @method getUidsInScope
38
+ * @description 根据查询范围(guildId, userId)获取匹配用户的 UID 列表。
39
+ * @param {string} [guildId] - (可选) 群组 ID。
40
+ * @param {string} [userId] - (可选) 用户 ID。
41
+ * @returns {Promise<{ uids?: number[], error?: string }>} 包含 UID 数组或错误信息的对象。
42
+ */
43
+ private getUidsInScope;
24
44
  /**
25
45
  * @private
26
46
  * @async
@@ -63,17 +83,17 @@ export declare class Stat {
63
83
  * @param {string} type - 要查询的消息类型。
64
84
  * @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
65
85
  * @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
66
- * @returns {Promise<{ list: RenderListItem[], total: number } | string>}
86
+ * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
67
87
  */
68
88
  private getMessageStatsByType;
69
89
  /**
70
90
  * @private
71
91
  * @async
72
92
  * @method getActiveUserStats
73
- * @description 从数据库中获取并聚合活跃用户排行数据。
74
- * @param {string} guildId - 要查询的群组 ID。
93
+ * @description 从数据库中获取并聚合指定时间范围内的活跃用户排行数据。
75
94
  * @param {number} hours - 查询过去的小时数。
76
- * @returns {Promise<{ list: RenderListItem[], total: number } | string>}
95
+ * @param {string} [guildId] - (可选) 要查询的群组 ID。若不提供,则进行全局排行。
96
+ * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
77
97
  */
78
98
  private getActiveUserStats;
79
99
  }
package/lib/index.js CHANGED
@@ -266,10 +266,6 @@ var import_koishi3 = require("koishi");
266
266
  // src/Renderer.ts
267
267
  var import_koishi2 = require("koishi");
268
268
  var Renderer = class {
269
- /**
270
- * @constructor
271
- * @param {Context} ctx - Koishi 的插件上下文,用于访问 puppeteer 服务。
272
- */
273
269
  constructor(ctx) {
274
270
  this.ctx = ctx;
275
271
  }
@@ -293,7 +289,7 @@ var Renderer = class {
293
289
  /**
294
290
  * @private
295
291
  * @method formatDate
296
- * @description 智能格式化日期。对于近期的时间,提供更人性化的相对时间描述(如“刚刚”,“x 分钟前”);对于较早的时间,则显示标准的“年-月-日”格式。
292
+ * @description 将日期格式化为包含两个单位的相对时间字符串(如“21天14时前”),如果超过一年则显示绝对日期。
297
293
  * @param {Date} date - 待格式化的 Date 对象。
298
294
  * @returns {string} 格式化后的日期字符串。
299
295
  */
@@ -301,9 +297,26 @@ var Renderer = class {
301
297
  if (!date) return "未知";
302
298
  const diff = Date.now() - date.getTime();
303
299
  if (diff < import_koishi2.Time.minute) return "刚刚";
304
- if (diff < import_koishi2.Time.hour) return `${Math.floor(diff / import_koishi2.Time.minute)} 分钟前`;
305
- if (diff < import_koishi2.Time.day) return `${Math.floor(diff / import_koishi2.Time.hour)} 小时前`;
306
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
300
+ if (diff > 365 * import_koishi2.Time.day) {
301
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
302
+ }
303
+ const timeUnits = [
304
+ { unit: "月", ms: 30 * import_koishi2.Time.day },
305
+ { unit: "天", ms: import_koishi2.Time.day },
306
+ { unit: "时", ms: import_koishi2.Time.hour },
307
+ { unit: "分", ms: import_koishi2.Time.minute }
308
+ ];
309
+ let remainingDiff = diff;
310
+ const parts = [];
311
+ for (const { unit, ms } of timeUnits) {
312
+ if (remainingDiff >= ms) {
313
+ const value = Math.floor(remainingDiff / ms);
314
+ parts.push(`${value}${unit}`);
315
+ remainingDiff %= ms;
316
+ }
317
+ }
318
+ const result = parts.slice(0, 2).join("");
319
+ return result ? `${result}前` : "刚刚";
307
320
  }
308
321
  /**
309
322
  * @private
@@ -316,63 +329,45 @@ var Renderer = class {
316
329
  generateListHtml(data, headers) {
317
330
  const { title, time, total, list } = data;
318
331
  if (!list?.length) return null;
319
- const tableHeadHtml = headers?.length > 0 ? `<thead><tr><th class="rank-cell">排名</th>${headers.map((h2) => `<th>${h2}</th>`).join("")}</tr></thead>` : "";
332
+ const tableHeadHtml = headers?.length > 0 ? `<thead><tr><th class="rank-cell">排名</th>${headers.map((h2, i) => {
333
+ const firstCell = list[0]?.[i];
334
+ let headerClass = "";
335
+ if (i === 0) headerClass = "column-main-label";
336
+ if (typeof firstCell === "number" || firstCell instanceof Date) {
337
+ headerClass += " header-right-align";
338
+ }
339
+ return `<th class="${headerClass.trim()}">${h2}</th>`;
340
+ }).join("")}</tr></thead>` : "";
320
341
  const tableRowsHtml = list.map((row, index) => {
321
342
  const rank = index + 1;
322
343
  const rankClass = rank === 1 ? "rank-gold" : rank === 2 ? "rank-silver" : rank === 3 ? "rank-bronze" : "";
323
- const rankCell = `<td class="rank-cell"><span class="rank-badge ${rankClass}">${rank}</span></td>`;
324
- const dataCells = row.map((cell) => {
325
- if (cell instanceof Date) return `<td class="data-cell date-cell">${this.formatDate(cell)}</td>`;
326
- if (typeof cell === "number") return `<td class="data-cell count-cell">${cell}</td>`;
327
- return `<td class="data-cell name-cell">${String(cell)}</td>`;
344
+ const rankCell = `<td class="rank-cell ${rankClass}">${rank}</td>`;
345
+ const dataCells = row.map((cell, cellIndex) => {
346
+ let className = "data-cell";
347
+ let content;
348
+ if (cell instanceof Date) {
349
+ className += " date-cell";
350
+ content = this.formatDate(cell);
351
+ } else if (typeof cell === "number") {
352
+ className += " count-cell";
353
+ content = cell.toLocaleString();
354
+ } else {
355
+ className += " name-cell";
356
+ content = String(cell);
357
+ }
358
+ if (cellIndex === 0) className += " column-main-label";
359
+ return `<td class="${className}">${content}</td>`;
328
360
  }).join("");
329
361
  return `<tr>${rankCell}${dataCells}</tr>`;
330
362
  }).join("");
331
363
  const metaInfoHtml = `
332
364
  <div class="meta-group">
333
- ${total !== void 0 ? `<div class="total-count">总计: ${total}</div>` : ""}
365
+ ${total !== void 0 ? `<div class="total-count">总计: ${typeof total === "number" ? total.toLocaleString() : total}</div>` : ""}
334
366
  <div class="time-label">生成于 ${time.toLocaleString("zh-CN", { hour12: false })}</div>
335
367
  </div>
336
368
  `;
337
- const styles = `
338
- :root {
339
- --bg-color: #f7f8fa; --card-bg: #ffffff; --text-color: #333; --header-color: #1f2329;
340
- --sub-text-color: #646a73; --border-color: #e4e6eb; --accent-color: #4a6ee0;
341
- --gold: #ffc327; --silver: #a8b5c1; --bronze: #d69864;
342
- }
343
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--bg-color); margin: 0; padding: 20px; width: 700px; box-sizing: border-box; -webkit-font-smoothing: antialiased; }
344
- .container { background: var(--card-bg); border-radius: 12px; box-shadow: 0 6px 16px rgba(0,0,0,0.08); padding: 24px; }
345
- .header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 1px solid var(--border-color); padding-bottom: 16px; margin-bottom: 16px; }
346
- .title-group h1 { font-size: 24px; font-weight: 700; color: var(--header-color); margin: 0; }
347
- .meta-group { text-align: right; }
348
- .meta-group .total-count { font-size: 22px; font-weight: 700; color: var(--accent-color); }
349
- .meta-group .time-label { font-size: 13px; color: var(--sub-text-color); margin-top: 4px; }
350
- table { width: 100%; border-collapse: collapse; color: var(--text-color); }
351
- th, td { padding: 12px 8px; text-align: left; border-bottom: 1px solid var(--border-color); vertical-align: middle; }
352
- th { font-size: 13px; font-weight: 600; color: var(--sub-text-color); }
353
- td { font-size: 15px; }
354
- tr:last-child td { border-bottom: none; }
355
- .rank-cell { width: 50px; text-align: center; }
356
- .rank-badge { display: inline-block; width: 24px; height: 24px; line-height: 24px; border-radius: 50%; font-weight: 600; font-size: 14px; color: var(--header-color); background-color: #eef0f3; }
357
- .rank-gold, .rank-silver, .rank-bronze { color: #fff; }
358
- .rank-gold { background-color: var(--gold); } .rank-silver { background-color: var(--silver); } .rank-bronze { background-color: var(--bronze); }
359
- .data-cell { word-break: break-all; }
360
- .name-cell { font-weight: 600; color: var(--header-color); }
361
- .count-cell { text-align: right; font-weight: 600; color: var(--accent-color); }
362
- .date-cell { text-align: right; font-size: 13px; color: var(--sub-text-color); }
363
- `;
364
- return `
365
- <!DOCTYPE html><html lang="zh-CN">
366
- <head><meta charset="UTF-8"><title>${title}</title><style>${styles}</style></head>
367
- <body>
368
- <div class="container">
369
- <div class="header">
370
- <div class="title-group"><h1>${title}</h1></div>
371
- ${metaInfoHtml}
372
- </div>
373
- <table>${tableHeadHtml}<tbody>${tableRowsHtml}</tbody></table>
374
- </div>
375
- </body></html>`;
369
+ const styles = `:root{--bg-color:#f7f8fa;--card-bg:#fff;--text-color:#333;--header-color:#1f2329;--sub-text-color:#646a73;--border-color:#f0f0f0;--accent-color:#4a6ee0;--gold:#ffac33;--silver:#a8b5c1;--bronze:#d69864}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:var(--bg-color);margin:0;padding:20px;width:800px;box-sizing:border-box;-webkit-font-smoothing:antialiased}.container{background:var(--card-bg);border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:20px}.header{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:12px;margin-bottom:8px}.title-group h1{font-size:22px;font-weight:600;color:var(--header-color);margin:0}.meta-group{text-align:right;white-space:nowrap}.meta-group .total-count{font-size:18px;font-weight:600;color:var(--accent-color)}.meta-group .time-label{font-size:13px;color:var(--sub-text-color);margin-top:4px}table{width:100%;border-collapse:collapse;color:var(--text-color)}th,td{padding:9px 12px;text-align:left;border-bottom:1px solid var(--border-color);vertical-align:middle}thead tr:first-child th{border-top:1px solid var(--border-color)}th{font-size:13px;font-weight:500;color:var(--sub-text-color)}td{font-size:15px}tr:last-child td{border-bottom:none}.header-right-align{text-align:right}.rank-cell{width:45px;text-align:center;font-weight:600;color:var(--sub-text-color);padding-left:0;padding-right:0}.rank-gold{color:var(--gold) !important}.rank-silver{color:var(--silver) !important}.rank-bronze{color:var(--bronze) !important}.column-main-label{width:45%}.data-cell{word-break:break-all}.name-cell{font-weight:500;color:var(--header-color)}.count-cell,.date-cell{text-align:right}.count-cell{font-weight:600;color:var(--accent-color)}.date-cell{font-size:14px;color:var(--sub-text-color)}`;
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>`;
376
371
  }
377
372
  };
378
373
 
@@ -393,32 +388,22 @@ var Stat = class {
393
388
  }
394
389
  renderer;
395
390
  /**
391
+ * @public
396
392
  * @method registerCommands
397
393
  * @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
398
394
  * @param {Command} analyse - 主 `analyse` 命令实例。
399
395
  */
400
396
  registerCommands(analyse) {
401
397
  if (this.config.enableCmdStat) {
402
- analyse.subcommand(".cmd", "命令使用统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").usage("查询用户或群组的命令使用统计,默认展示全局统计。").action(async ({ session, options }) => {
403
- const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
404
- let guildId = options.guild;
405
- if (!userId && !guildId && session.guildId) {
406
- guildId = session.guildId;
407
- } else if (!userId && !guildId && !session.guildId) {
408
- return "请指定查询范围";
409
- }
398
+ analyse.subcommand(".cmd", "命令使用统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("all", "-a 展示全局统计").action(async ({ session, options }) => {
399
+ const scope = this.parseQueryScope(session, options);
400
+ if (scope.error) return scope.error;
410
401
  try {
411
- const stats = await this.getCommandStats(guildId, userId);
402
+ const stats = await this.getCommandStats(scope.guildId, scope.userId);
412
403
  if (typeof stats === "string") return stats;
413
- const title = await this.generateTitle(guildId, userId, { main: "命令" });
414
- const renderData = {
415
- title,
416
- time: /* @__PURE__ */ new Date(),
417
- total: stats.total,
418
- list: stats.list
419
- };
420
- const headers = ["命令", "次数", "最后使用"];
421
- const result = await this.renderer.renderList(renderData, headers);
404
+ const title = await this.generateTitle(scope.guildId, scope.userId, { main: "命令" });
405
+ const renderData = { title, time: /* @__PURE__ */ new Date(), total: stats.total, list: stats.list };
406
+ const result = await this.renderer.renderList(renderData, ["命令", "次数", "最后使用"]);
422
407
  return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
423
408
  } catch (error) {
424
409
  this.ctx.logger.error("渲染命令统计图片失败:", error);
@@ -427,40 +412,23 @@ var Stat = class {
427
412
  });
428
413
  }
429
414
  if (this.config.enableMsgStat) {
430
- analyse.subcommand(".msg", "消息发送统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("type", "-t <type:string> 指定类型").usage("查询用户或群组的消息发送统计,默认展示全局统计。").action(async ({ session, options }) => {
431
- const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
432
- let guildId = options.guild;
433
- if (!userId && !guildId && !options.type && session.guildId) {
434
- guildId = session.guildId;
435
- } else if (!userId && !guildId && !session.guildId) {
436
- return "请指定查询范围";
437
- }
415
+ analyse.subcommand(".msg", "消息发送统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("type", "-t <type:string> 指定类型").option("all", "-a 展示全局统计").action(async ({ session, options }) => {
416
+ const scope = this.parseQueryScope(session, options);
417
+ if (scope.error) return scope.error;
438
418
  try {
439
419
  if (options.type) {
440
- const stats = await this.getMessageStatsByType(options.type, guildId, userId);
420
+ const stats = await this.getMessageStatsByType(options.type, scope.guildId, scope.userId);
441
421
  if (typeof stats === "string") return stats;
442
- const title = await this.generateTitle(guildId, void 0, { main: "消息", subtype: options.type });
443
- const renderData = {
444
- title,
445
- time: /* @__PURE__ */ new Date(),
446
- total: stats.total,
447
- list: stats.list
448
- };
449
- const headers = ["用户", "条数", "最后发言"];
450
- const result = await this.renderer.renderList(renderData, headers);
422
+ const title = await this.generateTitle(scope.guildId, scope.userId, { main: "消息", subtype: options.type });
423
+ const renderData = { title, time: /* @__PURE__ */ new Date(), total: stats.total, list: stats.list };
424
+ const result = await this.renderer.renderList(renderData, ["用户", "条数", "最后发言"]);
451
425
  return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
452
426
  } else {
453
- const stats = await this.getMessageStats(guildId, userId);
427
+ const stats = await this.getMessageStats(scope.guildId, scope.userId);
454
428
  if (typeof stats === "string") return stats;
455
- const title = await this.generateTitle(guildId, userId, { main: "消息" });
456
- const renderData = {
457
- title,
458
- time: /* @__PURE__ */ new Date(),
459
- total: stats.total,
460
- list: stats.list
461
- };
462
- const headers = ["类型", "条数", "最后发言"];
463
- const result = await this.renderer.renderList(renderData, headers);
429
+ const title = await this.generateTitle(scope.guildId, scope.userId, { main: "消息" });
430
+ const renderData = { title, time: /* @__PURE__ */ new Date(), total: stats.total, list: stats.list };
431
+ const result = await this.renderer.renderList(renderData, ["类型", "条数", "最后发言"]);
464
432
  return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
465
433
  }
466
434
  } catch (error) {
@@ -470,27 +438,20 @@ var Stat = class {
470
438
  });
471
439
  }
472
440
  if (this.config.enableRankStat) {
473
- analyse.subcommand(".rank", "用户发言排行").option("guild", "-g [guildId:string] 指定群组").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).usage("查询用户或群组的用户发言排行,默认展示全局统计。").action(async ({ session, options }) => {
474
- let guildId = options.guild;
475
- if (!guildId && session.guildId) guildId = session.guildId;
476
- if (!guildId) return "请指定查询范围";
441
+ analyse.subcommand(".rank", "用户发言排行").option("guild", "-g [guildId:string] 指定群组").option("all", "-a 展示全局统计").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).action(async ({ session, options }) => {
442
+ let guildId = options.all ? void 0 : typeof options.guild === "string" ? options.guild : session.guildId;
443
+ if (!session.guildId) return "请指定群组 ID";
444
+ if (!guildId && !options.all) return "请提供查询范围";
477
445
  try {
478
- const stats = await this.getActiveUserStats(guildId, options.hours);
446
+ const stats = await this.getActiveUserStats(options.hours, guildId);
479
447
  if (typeof stats === "string") return stats;
480
- const listWithPercentage = stats.list.map((row) => {
481
- const count = row[1];
482
- const percentage = stats.total > 0 ? `${(count / stats.total * 100).toFixed(2)}%` : "0.00%";
483
- return [...row, percentage];
484
- });
448
+ const listWithPercentage = stats.list.map((row) => [
449
+ ...row,
450
+ stats.total > 0 ? `${(row[1] / stats.total * 100).toFixed(2)}%` : "0.00%"
451
+ ]);
485
452
  const title = await this.generateTitle(guildId, void 0, { main: "排行", timeRange: options.hours });
486
- const renderData = {
487
- title,
488
- time: /* @__PURE__ */ new Date(),
489
- total: stats.total,
490
- list: listWithPercentage
491
- };
492
- const headers = ["用户", "总计发言", "占比"];
493
- const result = await this.renderer.renderList(renderData, headers);
453
+ const renderData = { title, time: /* @__PURE__ */ new Date(), total: stats.total, list: listWithPercentage };
454
+ const result = await this.renderer.renderList(renderData, ["用户", "总计发言", "占比"]);
494
455
  return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
495
456
  } catch (error) {
496
457
  this.ctx.logger.error("渲染发言排行图片失败:", error);
@@ -499,6 +460,44 @@ var Stat = class {
499
460
  });
500
461
  }
501
462
  }
463
+ /**
464
+ * @private
465
+ * @method parseQueryScope
466
+ * @description 解析命令的选项,将其转换为统一的查询范围对象(userId 和 guildId)。
467
+ * @param {Session} session - 当前会话对象。
468
+ * @param {QueryScopeOptions} options - 命令传入的选项。
469
+ * @returns {QueryScopeResult} 包含 userId、guildId 或 error 信息的查询范围对象。
470
+ */
471
+ parseQueryScope(session, options) {
472
+ let userId, guildId;
473
+ if (typeof options.user === "string") userId = import_koishi3.h.select(options.user, "user")[0]?.attrs.id;
474
+ else if (options.user) userId = session.userId;
475
+ if (typeof options.guild === "string") guildId = options.guild;
476
+ else if (options.guild) {
477
+ if (!session.guildId) return { error: "请指定群组 ID" };
478
+ guildId = session.guildId;
479
+ }
480
+ if (options.all) return { userId, guildId: void 0 };
481
+ if (!guildId && !userId) return session.guildId ? { guildId: session.guildId } : { error: "请提供查询范围" };
482
+ return { userId, guildId };
483
+ }
484
+ /**
485
+ * @private
486
+ * @async
487
+ * @method getUidsInScope
488
+ * @description 根据查询范围(guildId, userId)获取匹配用户的 UID 列表。
489
+ * @param {string} [guildId] - (可选) 群组 ID。
490
+ * @param {string} [userId] - (可选) 用户 ID。
491
+ * @returns {Promise<{ uids?: number[], error?: string }>} 包含 UID 数组或错误信息的对象。
492
+ */
493
+ async getUidsInScope(guildId, userId) {
494
+ const query = {};
495
+ if (guildId) query.channelId = guildId;
496
+ if (userId) query.userId = userId;
497
+ const users = await this.ctx.database.get("analyse_user", query, ["uid"]);
498
+ if (users.length === 0) return { error: "暂无统计数据" };
499
+ return { uids: users.map((u) => u.uid) };
500
+ }
502
501
  /**
503
502
  * @private
504
503
  * @async
@@ -513,34 +512,21 @@ var Stat = class {
513
512
  * @returns {Promise<string>} 生成的标题字符串。
514
513
  */
515
514
  async generateTitle(guildId, userId, options) {
516
- let scopeText;
515
+ let scopeText = "全局";
517
516
  if (userId && guildId) {
518
517
  const user = await this.ctx.database.get("analyse_user", { channelId: guildId, userId }, ["userName"]);
519
518
  const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
520
- const userName = user[0]?.userName || userId;
521
- const guildName = guild[0]?.channelName || guildId;
522
- scopeText = `${userName} 在 ${guildName}`;
519
+ scopeText = `${user[0]?.userName || userId} 在 ${guild[0]?.channelName || guildId}`;
523
520
  } else if (userId) {
524
521
  const user = await this.ctx.database.get("analyse_user", { userId }, ["userName"]);
525
- const userName = user[0]?.userName || userId;
526
- scopeText = `${userName}的全局`;
522
+ scopeText = `${user[0]?.userName || userId}的全局`;
527
523
  } else if (guildId) {
528
524
  const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
529
525
  scopeText = guild[0]?.channelName || guildId;
530
- } else {
531
- scopeText = "全局";
532
- }
533
- switch (options.main) {
534
- case "命令":
535
- return `${scopeText}的命令统计`;
536
- case "消息":
537
- if (options.subtype) return `${scopeText}的"${options.subtype}"消息统计`;
538
- return `${scopeText}的消息统计`;
539
- case "排行":
540
- return `${scopeText}的${options.timeRange}小时消息排行`;
541
- default:
542
- return scopeText;
543
526
  }
527
+ if (options.main === "排行") return `${scopeText}的${options.timeRange}小时消息排行`;
528
+ if (options.main === "消息" && options.subtype) return `${scopeText}的"${options.subtype}"消息统计`;
529
+ return `${scopeText}的${options.main}统计`;
544
530
  }
545
531
  /**
546
532
  * @private
@@ -552,20 +538,13 @@ var Stat = class {
552
538
  * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
553
539
  */
554
540
  async getCommandStats(guildId, userId) {
555
- const userQuery = {};
556
- if (guildId) userQuery.channelId = guildId;
557
- if (userId) userQuery.userId = userId;
558
- const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
559
- if (users.length === 0) return "暂无目标用户统计数据";
560
- const uids = users.map((u) => u.uid);
561
- const aggregatedStats = await this.ctx.database.select("analyse_cmd").where({ uid: { $in: uids } }).groupBy(["command"], {
562
- count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
563
- lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
564
- }).orderBy("count", "desc").execute();
565
- if (aggregatedStats.length === 0) return "暂无统计数据";
566
- const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
567
- const list = aggregatedStats.map((item) => [item.command, item.count, item.lastUsed]);
568
- return { list, total: totalCount };
541
+ const { uids, error } = await this.getUidsInScope(guildId, userId);
542
+ if (error) return error;
543
+ const stats = await this.ctx.database.select("analyse_cmd").where({ uid: { $in: uids } }).groupBy("command", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).orderBy("count", "desc").execute();
544
+ if (stats.length === 0) return "暂无统计数据";
545
+ const total = stats.reduce((sum, record) => sum + record.count, 0);
546
+ const list = stats.map((item) => [item.command, item.count, item.lastUsed]);
547
+ return { list, total };
569
548
  }
570
549
  /**
571
550
  * @private
@@ -577,20 +556,13 @@ var Stat = class {
577
556
  * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
578
557
  */
579
558
  async getMessageStats(guildId, userId) {
580
- const userQuery = {};
581
- if (guildId) userQuery.channelId = guildId;
582
- if (userId) userQuery.userId = userId;
583
- const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
584
- if (users.length === 0) return "暂无目标用户统计数据";
585
- const uids = users.map((u) => u.uid);
586
- const aggregatedStats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids } }).groupBy(["type"], {
587
- count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
588
- lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
589
- }).orderBy("count", "desc").execute();
590
- if (aggregatedStats.length === 0) return "暂无统计数据";
591
- const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
592
- const list = aggregatedStats.map((item) => [item.type, item.count, item.lastUsed]);
593
- return { list, total: totalCount };
559
+ const { uids, error } = await this.getUidsInScope(guildId, userId);
560
+ if (error) return error;
561
+ const stats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids } }).groupBy("type", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).orderBy("count", "desc").execute();
562
+ if (stats.length === 0) return "暂无统计数据";
563
+ const total = stats.reduce((sum, record) => sum + record.count, 0);
564
+ const list = stats.map((item) => [item.type, item.count, item.lastUsed]);
565
+ return { list, total };
594
566
  }
595
567
  /**
596
568
  * @private
@@ -600,60 +572,62 @@ var Stat = class {
600
572
  * @param {string} type - 要查询的消息类型。
601
573
  * @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
602
574
  * @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
603
- * @returns {Promise<{ list: RenderListItem[], total: number } | string>}
575
+ * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
604
576
  */
605
577
  async getMessageStatsByType(type, guildId, userId) {
606
- const userQuery = {};
607
- if (guildId) userQuery.channelId = guildId;
608
- if (userId) userQuery.userId = userId;
609
- const users = await this.ctx.database.get("analyse_user", userQuery, ["uid", "userName"]);
610
- if (users.length === 0) return "暂无目标用户统计数据";
578
+ const query = {};
579
+ if (guildId) query.channelId = guildId;
580
+ if (userId) query.userId = userId;
581
+ const users = await this.ctx.database.get("analyse_user", query, ["uid", "userName"]);
582
+ if (users.length === 0) return "暂无统计数据";
611
583
  const uids = users.map((u) => u.uid);
612
584
  const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
613
- const aggregatedStats = await this.ctx.database.select("analyse_msg").where({
614
- uid: { $in: uids },
615
- type
616
- }).groupBy(["uid"], {
617
- count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
618
- lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
619
- }).orderBy("count", "desc").execute();
620
- if (aggregatedStats.length === 0) return `暂无统计数据`;
621
- const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
622
- const list = aggregatedStats.map((item) => [
623
- userNameMap.get(item.uid) || `UID ${item.uid}`,
624
- item.count,
625
- item.lastUsed
626
- ]);
627
- return { list, total: totalCount };
585
+ const stats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids }, type }).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"), lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed") }).orderBy("count", "desc").execute();
586
+ if (stats.length === 0) return `暂无统计数据`;
587
+ const total = stats.reduce((sum, record) => sum + record.count, 0);
588
+ const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count, item.lastUsed]);
589
+ return { list, total };
628
590
  }
629
591
  /**
630
592
  * @private
631
593
  * @async
632
594
  * @method getActiveUserStats
633
- * @description 从数据库中获取并聚合活跃用户排行数据。
634
- * @param {string} guildId - 要查询的群组 ID。
595
+ * @description 从数据库中获取并聚合指定时间范围内的活跃用户排行数据。
635
596
  * @param {number} hours - 查询过去的小时数。
636
- * @returns {Promise<{ list: RenderListItem[], total: number } | string>}
597
+ * @param {string} [guildId] - (可选) 要查询的群组 ID。若不提供,则进行全局排行。
598
+ * @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
637
599
  */
638
- async getActiveUserStats(guildId, hours) {
600
+ async getActiveUserStats(hours, guildId) {
639
601
  const since = new Date(Date.now() - hours * 3600 * 1e3);
640
- const usersInGuild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["uid", "userId", "userName"]);
641
- if (usersInGuild.length === 0) return "暂无用户统计数据";
642
- const uids = usersInGuild.map((u) => u.uid);
643
- const userNameMap = new Map(usersInGuild.map((u) => [u.uid, u.userName]));
644
- const aggregatedStats = await this.ctx.database.select("analyse_msg").where({
645
- uid: { $in: uids },
646
- hour: { $gte: since }
647
- }).groupBy(["uid"], {
648
- count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count")
649
- }).orderBy("count", "desc").limit(100).execute();
650
- if (aggregatedStats.length === 0) return "暂无统计数据";
651
- const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
652
- const list = aggregatedStats.map((item) => [
653
- userNameMap.get(item.uid) || `UID ${item.uid}`,
654
- item.count
655
- ]);
656
- return { list, total: totalCount };
602
+ if (guildId) {
603
+ const usersInGuild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["uid", "userName"]);
604
+ if (usersInGuild.length === 0) return "暂无统计数据";
605
+ const uids = usersInGuild.map((u) => u.uid);
606
+ const userNameMap = new Map(usersInGuild.map((u) => [u.uid, u.userName]));
607
+ const stats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids }, hour: { $gte: since } }).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").limit(100).execute();
608
+ if (stats.length === 0) return "暂无统计数据";
609
+ const total = stats.reduce((sum, record) => sum + record.count, 0);
610
+ const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count]);
611
+ return { list, total };
612
+ } else {
613
+ const msgStats = await this.ctx.database.select("analyse_msg").where({ hour: { $gte: since } }).project(["uid", "count"]).execute();
614
+ if (msgStats.length === 0) return "暂无统计数据";
615
+ const allUsers = await this.ctx.database.get("analyse_user", {}, ["uid", "userId", "userName"]);
616
+ const uidToUserMap = new Map(allUsers.map((u) => [u.uid, { userId: u.userId, userName: u.userName }]));
617
+ const userCounts = /* @__PURE__ */ new Map();
618
+ for (const msg of msgStats) {
619
+ const userInfo = uidToUserMap.get(msg.uid);
620
+ if (userInfo) {
621
+ const existing = userCounts.get(userInfo.userId);
622
+ userCounts.set(userInfo.userId, { count: (existing?.count || 0) + msg.count, name: userInfo.userName });
623
+ }
624
+ }
625
+ if (userCounts.size === 0) return "暂无统计数据";
626
+ const grandTotal = Array.from(userCounts.values()).reduce((sum, data) => sum + data.count, 0);
627
+ const sortedUsers = Array.from(userCounts.entries()).sort(([, a], [, b]) => b.count - a.count).slice(0, 100);
628
+ const list = sortedUsers.map(([userId, data]) => [data.name || userId, data.count]);
629
+ return { list, total: grandTotal };
630
+ }
657
631
  }
658
632
  };
659
633
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.3.3",
4
+ "version": "0.3.4",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],