koishi-plugin-chat-analyse 0.2.6 → 0.3.1
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/Collector.d.ts +2 -2
- package/lib/Stat.d.ts +30 -7
- package/lib/index.d.ts +2 -1
- package/lib/index.js +172 -49
- package/package.json +1 -1
package/lib/Collector.d.ts
CHANGED
|
@@ -18,13 +18,13 @@ declare module 'koishi' {
|
|
|
18
18
|
analyse_msg: {
|
|
19
19
|
uid: number;
|
|
20
20
|
type: string;
|
|
21
|
+
hour: Date;
|
|
21
22
|
count: number;
|
|
22
23
|
timestamp: Date;
|
|
23
24
|
};
|
|
24
25
|
analyse_cache: {
|
|
25
26
|
id: number;
|
|
26
|
-
|
|
27
|
-
userId: string;
|
|
27
|
+
uid: number;
|
|
28
28
|
content: string;
|
|
29
29
|
timestamp: Date;
|
|
30
30
|
};
|
package/lib/Stat.d.ts
CHANGED
|
@@ -17,22 +17,24 @@ export declare class Stat {
|
|
|
17
17
|
constructor(ctx: Context, config: Config);
|
|
18
18
|
/**
|
|
19
19
|
* @method registerCommands
|
|
20
|
-
* @description 根据插件配置,动态地将 `.
|
|
20
|
+
* @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
|
|
21
21
|
* @param {Command} analyse - 主 `analyse` 命令实例。
|
|
22
22
|
*/
|
|
23
23
|
registerCommands(analyse: Command): void;
|
|
24
24
|
/**
|
|
25
25
|
* @private
|
|
26
26
|
* @async
|
|
27
|
-
* @method
|
|
28
|
-
* @description
|
|
29
|
-
* @param {Session} session - 当前会话,备用。
|
|
27
|
+
* @method generateTitle
|
|
28
|
+
* @description 通用的标题生成器。根据查询参数和类型选项动态生成易于理解的图片标题。
|
|
30
29
|
* @param {string} [guildId] - (可选) 查询的群组 ID。
|
|
31
30
|
* @param {string} [userId] - (可选) 查询的用户 ID。
|
|
32
|
-
* @param {
|
|
31
|
+
* @param {object} options - 标题的配置选项。
|
|
32
|
+
* @param {'命令' | '消息' | '排行'} options.main - 标题主类型。
|
|
33
|
+
* @param {string} [options.subtype] - (可选) 消息类型的子类型。
|
|
34
|
+
* @param {number} [options.timeRange] - (可选) 排行的时间范围(小时)。
|
|
33
35
|
* @returns {Promise<string>} 生成的标题字符串。
|
|
34
36
|
*/
|
|
35
|
-
private
|
|
37
|
+
private generateTitle;
|
|
36
38
|
/**
|
|
37
39
|
* @private
|
|
38
40
|
* @async
|
|
@@ -47,10 +49,31 @@ export declare class Stat {
|
|
|
47
49
|
* @private
|
|
48
50
|
* @async
|
|
49
51
|
* @method getMessageStats
|
|
50
|
-
* @description
|
|
52
|
+
* @description 从数据库中获取并聚合所有消息类型的统计数据。
|
|
51
53
|
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
52
54
|
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
53
55
|
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
54
56
|
*/
|
|
55
57
|
private getMessageStats;
|
|
58
|
+
/**
|
|
59
|
+
* @private
|
|
60
|
+
* @async
|
|
61
|
+
* @method getMessageStatsByType
|
|
62
|
+
* @description 按指定消息类型,从数据库中获取并聚合用户排行数据。
|
|
63
|
+
* @param {string} type - 要查询的消息类型。
|
|
64
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
65
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
66
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>}
|
|
67
|
+
*/
|
|
68
|
+
private getMessageStatsByType;
|
|
69
|
+
/**
|
|
70
|
+
* @private
|
|
71
|
+
* @async
|
|
72
|
+
* @method getActiveUserStats
|
|
73
|
+
* @description 从数据库中获取并聚合活跃用户排行数据。
|
|
74
|
+
* @param {string} guildId - 要查询的群组 ID。
|
|
75
|
+
* @param {number} hours - 查询过去的小时数。
|
|
76
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>}
|
|
77
|
+
*/
|
|
78
|
+
private getActiveUserStats;
|
|
56
79
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var Collector = class _Collector {
|
|
|
42
42
|
this.config = config;
|
|
43
43
|
this.defineModels();
|
|
44
44
|
ctx.on("message", (session) => this.handleMessage(session));
|
|
45
|
-
if (this.config.
|
|
45
|
+
if (this.config.enableOriRecord) {
|
|
46
46
|
this.flushInterval = setInterval(() => this.flushCacheBuffer(), _Collector.FLUSH_INTERVAL);
|
|
47
47
|
ctx.on("dispose", () => {
|
|
48
48
|
clearInterval(this.flushInterval);
|
|
@@ -86,17 +86,17 @@ var Collector = class _Collector {
|
|
|
86
86
|
this.ctx.model.extend("analyse_msg", {
|
|
87
87
|
uid: "unsigned",
|
|
88
88
|
type: "string",
|
|
89
|
+
hour: "timestamp",
|
|
89
90
|
count: "unsigned",
|
|
90
91
|
timestamp: "timestamp"
|
|
91
|
-
}, { primary: ["uid", "type"] });
|
|
92
|
-
if (this.config.
|
|
92
|
+
}, { primary: ["uid", "type", "hour"] });
|
|
93
|
+
if (this.config.enableOriRecord) {
|
|
93
94
|
this.ctx.model.extend("analyse_cache", {
|
|
94
95
|
id: "unsigned",
|
|
95
|
-
|
|
96
|
-
userId: "string",
|
|
96
|
+
uid: "unsigned",
|
|
97
97
|
content: "text",
|
|
98
98
|
timestamp: "timestamp"
|
|
99
|
-
}, { primary: "id", autoInc: true });
|
|
99
|
+
}, { primary: "id", autoInc: true, indexes: ["uid", "timestamp"] });
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
@@ -113,28 +113,29 @@ var Collector = class _Collector {
|
|
|
113
113
|
if (!effectiveId || !userId || !timestamp || !content?.trim()) return;
|
|
114
114
|
const uid = await this.getOrCreateUser(session, effectiveId);
|
|
115
115
|
if (!uid) return;
|
|
116
|
-
const now = /* @__PURE__ */ new Date();
|
|
117
116
|
if (argv?.command) {
|
|
118
117
|
await this.ctx.database.upsert("analyse_cmd", (row) => [{
|
|
119
118
|
uid,
|
|
120
119
|
command: argv.command.name,
|
|
121
120
|
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, import_koishi.$.literal(0)), 1),
|
|
122
|
-
timestamp:
|
|
121
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
123
122
|
}]);
|
|
124
123
|
}
|
|
124
|
+
const messageTime = new Date(timestamp);
|
|
125
|
+
const hourStart = new Date(messageTime.getFullYear(), messageTime.getMonth(), messageTime.getDate(), messageTime.getHours());
|
|
125
126
|
const uniqueElementTypes = new Set(elements.map((e) => e.type));
|
|
126
127
|
for (const type of uniqueElementTypes) {
|
|
127
128
|
await this.ctx.database.upsert("analyse_msg", (row) => [{
|
|
128
129
|
uid,
|
|
129
130
|
type,
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
hour: hourStart,
|
|
132
|
+
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), 1),
|
|
133
|
+
timestamp: messageTime
|
|
132
134
|
}]);
|
|
133
135
|
}
|
|
134
|
-
if (this.config.
|
|
136
|
+
if (this.config.enableOriRecord) {
|
|
135
137
|
this.cacheBuffer.push({
|
|
136
|
-
|
|
137
|
-
userId,
|
|
138
|
+
uid,
|
|
138
139
|
content: this.sanitizeContent(elements),
|
|
139
140
|
timestamp: new Date(timestamp)
|
|
140
141
|
});
|
|
@@ -359,29 +360,30 @@ var Stat = class {
|
|
|
359
360
|
renderer;
|
|
360
361
|
/**
|
|
361
362
|
* @method registerCommands
|
|
362
|
-
* @description 根据插件配置,动态地将 `.
|
|
363
|
+
* @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
|
|
363
364
|
* @param {Command} analyse - 主 `analyse` 命令实例。
|
|
364
365
|
*/
|
|
365
366
|
registerCommands(analyse) {
|
|
366
367
|
if (this.config.enableCmdStat) {
|
|
367
|
-
analyse.subcommand(".
|
|
368
|
+
analyse.subcommand(".cmd", "命令使用统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").usage("查询用户或群组的命令使用统计,默认展示全局统计。").action(async ({ session, options }) => {
|
|
368
369
|
const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
|
|
369
370
|
let guildId = options.guild;
|
|
370
|
-
if (
|
|
371
|
-
if (!session.guildId) return "私聊中请使用 -g <群组ID> 指定群组。";
|
|
371
|
+
if (!userId && !guildId && session.guildId) {
|
|
372
372
|
guildId = session.guildId;
|
|
373
|
+
} else if (!userId && !guildId && !session.guildId) {
|
|
374
|
+
return "请指定查询范围";
|
|
373
375
|
}
|
|
374
376
|
try {
|
|
375
377
|
const stats = await this.getCommandStats(guildId, userId);
|
|
376
378
|
if (typeof stats === "string") return stats;
|
|
377
|
-
const title = await this.
|
|
379
|
+
const title = await this.generateTitle(guildId, userId, { main: "命令" });
|
|
378
380
|
const renderData = {
|
|
379
381
|
title,
|
|
380
382
|
time: /* @__PURE__ */ new Date(),
|
|
381
383
|
total: stats.total,
|
|
382
384
|
list: stats.list
|
|
383
385
|
};
|
|
384
|
-
const headers = ["命令", "次数", "
|
|
386
|
+
const headers = ["命令", "次数", "最后使用"];
|
|
385
387
|
const result = await this.renderer.renderList(renderData, headers);
|
|
386
388
|
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
387
389
|
} catch (error) {
|
|
@@ -391,29 +393,74 @@ var Stat = class {
|
|
|
391
393
|
});
|
|
392
394
|
}
|
|
393
395
|
if (this.config.enableMsgStat) {
|
|
394
|
-
analyse.subcommand(".
|
|
396
|
+
analyse.subcommand(".msg", "消息发送统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("type", "-t <type:string> 指定类型").usage("查询用户或群组的消息发送统计,默认展示全局统计。").action(async ({ session, options }) => {
|
|
395
397
|
const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
|
|
396
398
|
let guildId = options.guild;
|
|
397
|
-
if (
|
|
398
|
-
if (!session.guildId) return "私聊中请使用 -g <群组ID> 指定群组。";
|
|
399
|
+
if (!userId && !guildId && !options.type && session.guildId) {
|
|
399
400
|
guildId = session.guildId;
|
|
401
|
+
} else if (!userId && !guildId && !session.guildId) {
|
|
402
|
+
return "请指定查询范围";
|
|
400
403
|
}
|
|
401
404
|
try {
|
|
402
|
-
|
|
405
|
+
if (options.type) {
|
|
406
|
+
const stats = await this.getMessageStatsByType(options.type, guildId, userId);
|
|
407
|
+
if (typeof stats === "string") return stats;
|
|
408
|
+
const title = await this.generateTitle(guildId, void 0, { main: "消息", subtype: options.type });
|
|
409
|
+
const renderData = {
|
|
410
|
+
title,
|
|
411
|
+
time: /* @__PURE__ */ new Date(),
|
|
412
|
+
total: stats.total,
|
|
413
|
+
list: stats.list
|
|
414
|
+
};
|
|
415
|
+
const headers = ["用户", "条数", "最后发言"];
|
|
416
|
+
const result = await this.renderer.renderList(renderData, headers);
|
|
417
|
+
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
418
|
+
} else {
|
|
419
|
+
const stats = await this.getMessageStats(guildId, userId);
|
|
420
|
+
if (typeof stats === "string") return stats;
|
|
421
|
+
const title = await this.generateTitle(guildId, userId, { main: "消息" });
|
|
422
|
+
const renderData = {
|
|
423
|
+
title,
|
|
424
|
+
time: /* @__PURE__ */ new Date(),
|
|
425
|
+
total: stats.total,
|
|
426
|
+
list: stats.list
|
|
427
|
+
};
|
|
428
|
+
const headers = ["类型", "条数", "最后发言"];
|
|
429
|
+
const result = await this.renderer.renderList(renderData, headers);
|
|
430
|
+
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.ctx.logger.error("渲染消息统计图片失败:", error);
|
|
434
|
+
return "渲染消息统计图片失败";
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (this.config.enableRankStat) {
|
|
439
|
+
analyse.subcommand(".rank", "用户发言排行").option("guild", "-g [guildId:string] 指定群组").option("hours", "-h <hours:number> 指定时长", { fallback: 24 }).usage("查询用户或群组的用户发言排行,默认展示全局统计。").action(async ({ session, options }) => {
|
|
440
|
+
let guildId = options.guild;
|
|
441
|
+
if (!guildId && session.guildId) guildId = session.guildId;
|
|
442
|
+
if (!guildId) return "请指定查询范围";
|
|
443
|
+
try {
|
|
444
|
+
const stats = await this.getActiveUserStats(guildId, options.hours);
|
|
403
445
|
if (typeof stats === "string") return stats;
|
|
404
|
-
const
|
|
446
|
+
const listWithPercentage = stats.list.map((row) => {
|
|
447
|
+
const count = row[1];
|
|
448
|
+
const percentage = stats.total > 0 ? `${(count / stats.total * 100).toFixed(2)}%` : "0.00%";
|
|
449
|
+
return [...row, percentage];
|
|
450
|
+
});
|
|
451
|
+
const title = await this.generateTitle(guildId, void 0, { main: "排行", timeRange: options.hours });
|
|
405
452
|
const renderData = {
|
|
406
453
|
title,
|
|
407
454
|
time: /* @__PURE__ */ new Date(),
|
|
408
455
|
total: stats.total,
|
|
409
|
-
list:
|
|
456
|
+
list: listWithPercentage
|
|
410
457
|
};
|
|
411
|
-
const headers = ["
|
|
458
|
+
const headers = ["用户", "总计发言", "占比"];
|
|
412
459
|
const result = await this.renderer.renderList(renderData, headers);
|
|
413
460
|
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
414
461
|
} catch (error) {
|
|
415
|
-
this.ctx.logger.error("
|
|
416
|
-
return "
|
|
462
|
+
this.ctx.logger.error("渲染发言排行图片失败:", error);
|
|
463
|
+
return "渲染发言排行图片失败";
|
|
417
464
|
}
|
|
418
465
|
});
|
|
419
466
|
}
|
|
@@ -421,33 +468,45 @@ var Stat = class {
|
|
|
421
468
|
/**
|
|
422
469
|
* @private
|
|
423
470
|
* @async
|
|
424
|
-
* @method
|
|
425
|
-
* @description
|
|
426
|
-
* @param {Session} session - 当前会话,备用。
|
|
471
|
+
* @method generateTitle
|
|
472
|
+
* @description 通用的标题生成器。根据查询参数和类型选项动态生成易于理解的图片标题。
|
|
427
473
|
* @param {string} [guildId] - (可选) 查询的群组 ID。
|
|
428
474
|
* @param {string} [userId] - (可选) 查询的用户 ID。
|
|
429
|
-
* @param {
|
|
475
|
+
* @param {object} options - 标题的配置选项。
|
|
476
|
+
* @param {'命令' | '消息' | '排行'} options.main - 标题主类型。
|
|
477
|
+
* @param {string} [options.subtype] - (可选) 消息类型的子类型。
|
|
478
|
+
* @param {number} [options.timeRange] - (可选) 排行的时间范围(小时)。
|
|
430
479
|
* @returns {Promise<string>} 生成的标题字符串。
|
|
431
480
|
*/
|
|
432
|
-
async
|
|
481
|
+
async generateTitle(guildId, userId, options) {
|
|
482
|
+
let scopeText;
|
|
433
483
|
if (userId && guildId) {
|
|
434
484
|
const user = await this.ctx.database.get("analyse_user", { channelId: guildId, userId }, ["userName"]);
|
|
435
485
|
const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
|
|
436
486
|
const userName = user[0]?.userName || userId;
|
|
437
487
|
const guildName = guild[0]?.channelName || guildId;
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (userId) {
|
|
488
|
+
scopeText = `${userName} 在 ${guildName}`;
|
|
489
|
+
} else if (userId) {
|
|
441
490
|
const user = await this.ctx.database.get("analyse_user", { userId }, ["userName"]);
|
|
442
491
|
const userName = user[0]?.userName || userId;
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
if (guildId) {
|
|
492
|
+
scopeText = `${userName}的全局`;
|
|
493
|
+
} else if (guildId) {
|
|
446
494
|
const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
|
|
447
|
-
|
|
448
|
-
|
|
495
|
+
scopeText = guild[0]?.channelName || guildId;
|
|
496
|
+
} else {
|
|
497
|
+
scopeText = "全局";
|
|
498
|
+
}
|
|
499
|
+
switch (options.main) {
|
|
500
|
+
case "命令":
|
|
501
|
+
return `${scopeText}的命令统计`;
|
|
502
|
+
case "消息":
|
|
503
|
+
if (options.subtype) return `${scopeText}的"${options.subtype}"消息统计`;
|
|
504
|
+
return `${scopeText}的消息统计`;
|
|
505
|
+
case "排行":
|
|
506
|
+
return `${scopeText}的${options.timeRange}小时消息排行`;
|
|
507
|
+
default:
|
|
508
|
+
return scopeText;
|
|
449
509
|
}
|
|
450
|
-
return `全局${type}统计`;
|
|
451
510
|
}
|
|
452
511
|
/**
|
|
453
512
|
* @private
|
|
@@ -463,7 +522,7 @@ var Stat = class {
|
|
|
463
522
|
if (guildId) userQuery.channelId = guildId;
|
|
464
523
|
if (userId) userQuery.userId = userId;
|
|
465
524
|
const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
|
|
466
|
-
if (users.length === 0) return "
|
|
525
|
+
if (users.length === 0) return "暂无目标用户统计数据";
|
|
467
526
|
const uids = users.map((u) => u.uid);
|
|
468
527
|
const aggregatedStats = await this.ctx.database.select("analyse_cmd").where({ uid: { $in: uids } }).groupBy(["command"], {
|
|
469
528
|
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
@@ -478,7 +537,7 @@ var Stat = class {
|
|
|
478
537
|
* @private
|
|
479
538
|
* @async
|
|
480
539
|
* @method getMessageStats
|
|
481
|
-
* @description
|
|
540
|
+
* @description 从数据库中获取并聚合所有消息类型的统计数据。
|
|
482
541
|
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
483
542
|
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
484
543
|
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
@@ -488,7 +547,7 @@ var Stat = class {
|
|
|
488
547
|
if (guildId) userQuery.channelId = guildId;
|
|
489
548
|
if (userId) userQuery.userId = userId;
|
|
490
549
|
const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
|
|
491
|
-
if (users.length === 0) return "
|
|
550
|
+
if (users.length === 0) return "暂无目标用户统计数据";
|
|
492
551
|
const uids = users.map((u) => u.uid);
|
|
493
552
|
const aggregatedStats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids } }).groupBy(["type"], {
|
|
494
553
|
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
@@ -499,6 +558,69 @@ var Stat = class {
|
|
|
499
558
|
const list = aggregatedStats.map((item) => [item.type, item.count, item.lastUsed]);
|
|
500
559
|
return { list, total: totalCount };
|
|
501
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* @private
|
|
563
|
+
* @async
|
|
564
|
+
* @method getMessageStatsByType
|
|
565
|
+
* @description 按指定消息类型,从数据库中获取并聚合用户排行数据。
|
|
566
|
+
* @param {string} type - 要查询的消息类型。
|
|
567
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
568
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
569
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>}
|
|
570
|
+
*/
|
|
571
|
+
async getMessageStatsByType(type, guildId, userId) {
|
|
572
|
+
const userQuery = {};
|
|
573
|
+
if (guildId) userQuery.channelId = guildId;
|
|
574
|
+
if (userId) userQuery.userId = userId;
|
|
575
|
+
const users = await this.ctx.database.get("analyse_user", userQuery, ["uid", "userName"]);
|
|
576
|
+
if (users.length === 0) return "暂无目标用户统计数据";
|
|
577
|
+
const uids = users.map((u) => u.uid);
|
|
578
|
+
const userNameMap = new Map(users.map((u) => [u.uid, u.userName]));
|
|
579
|
+
const aggregatedStats = await this.ctx.database.select("analyse_msg").where({
|
|
580
|
+
uid: { $in: uids },
|
|
581
|
+
type
|
|
582
|
+
}).groupBy(["uid"], {
|
|
583
|
+
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
584
|
+
lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
|
|
585
|
+
}).orderBy("count", "desc").execute();
|
|
586
|
+
if (aggregatedStats.length === 0) return `暂无统计数据`;
|
|
587
|
+
const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
|
|
588
|
+
const list = aggregatedStats.map((item) => [
|
|
589
|
+
userNameMap.get(item.uid) || `UID ${item.uid}`,
|
|
590
|
+
item.count,
|
|
591
|
+
item.lastUsed
|
|
592
|
+
]);
|
|
593
|
+
return { list, total: totalCount };
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* @private
|
|
597
|
+
* @async
|
|
598
|
+
* @method getActiveUserStats
|
|
599
|
+
* @description 从数据库中获取并聚合活跃用户排行数据。
|
|
600
|
+
* @param {string} guildId - 要查询的群组 ID。
|
|
601
|
+
* @param {number} hours - 查询过去的小时数。
|
|
602
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>}
|
|
603
|
+
*/
|
|
604
|
+
async getActiveUserStats(guildId, hours) {
|
|
605
|
+
const since = new Date(Date.now() - hours * 3600 * 1e3);
|
|
606
|
+
const usersInGuild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["uid", "userId", "userName"]);
|
|
607
|
+
if (usersInGuild.length === 0) return "暂无用户统计数据";
|
|
608
|
+
const uids = usersInGuild.map((u) => u.uid);
|
|
609
|
+
const userNameMap = new Map(usersInGuild.map((u) => [u.uid, u.userName]));
|
|
610
|
+
const aggregatedStats = await this.ctx.database.select("analyse_msg").where({
|
|
611
|
+
uid: { $in: uids },
|
|
612
|
+
hour: { $gte: since }
|
|
613
|
+
}).groupBy(["uid"], {
|
|
614
|
+
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count")
|
|
615
|
+
}).orderBy("count", "desc").limit(100).execute();
|
|
616
|
+
if (aggregatedStats.length === 0) return "暂无统计数据";
|
|
617
|
+
const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
|
|
618
|
+
const list = aggregatedStats.map((item) => [
|
|
619
|
+
userNameMap.get(item.uid) || `UID ${item.uid}`,
|
|
620
|
+
item.count
|
|
621
|
+
]);
|
|
622
|
+
return { list, total: totalCount };
|
|
623
|
+
}
|
|
502
624
|
};
|
|
503
625
|
|
|
504
626
|
// src/index.ts
|
|
@@ -518,13 +640,14 @@ var name = "chat-analyse";
|
|
|
518
640
|
var using = ["database", "puppeteer"];
|
|
519
641
|
var Config = import_koishi4.Schema.intersect([
|
|
520
642
|
import_koishi4.Schema.object({
|
|
521
|
-
enableListener: import_koishi4.Schema.boolean().default(true).description("
|
|
522
|
-
|
|
643
|
+
enableListener: import_koishi4.Schema.boolean().default(true).description("启用消息监听"),
|
|
644
|
+
enableOriRecord: import_koishi4.Schema.boolean().default(true).description("启用原始记录")
|
|
645
|
+
}).description("监听配置"),
|
|
523
646
|
import_koishi4.Schema.object({
|
|
524
647
|
enableCmdStat: import_koishi4.Schema.boolean().default(true).description("启用命令统计"),
|
|
525
648
|
enableMsgStat: import_koishi4.Schema.boolean().default(true).description("启用消息统计"),
|
|
526
|
-
|
|
527
|
-
}).description("
|
|
649
|
+
enableRankStat: import_koishi4.Schema.boolean().default(true).description("启用发言排行")
|
|
650
|
+
}).description("命令配置")
|
|
528
651
|
]);
|
|
529
652
|
function apply(ctx, config) {
|
|
530
653
|
if (config.enableListener) new Collector(ctx, config);
|