koishi-plugin-chat-analyse 0.2.4 → 0.2.6
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 +61 -36
- package/lib/Renderer.d.ts +24 -15
- package/lib/Stat.d.ts +56 -0
- package/lib/index.d.ts +21 -3
- package/lib/index.js +329 -210
- package/package.json +1 -1
- package/lib/CmdStat.d.ts +0 -38
package/lib/Collector.d.ts
CHANGED
|
@@ -1,71 +1,96 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './index';
|
|
2
3
|
declare module 'koishi' {
|
|
3
4
|
interface Tables {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
analyse_user: {
|
|
6
|
+
uid: number;
|
|
6
7
|
channelId: string;
|
|
7
8
|
userId: string;
|
|
9
|
+
channelName: string;
|
|
10
|
+
userName: string;
|
|
11
|
+
};
|
|
12
|
+
analyse_cmd: {
|
|
13
|
+
uid: number;
|
|
14
|
+
command: string;
|
|
15
|
+
count: number;
|
|
16
|
+
timestamp: Date;
|
|
17
|
+
};
|
|
18
|
+
analyse_msg: {
|
|
19
|
+
uid: number;
|
|
8
20
|
type: string;
|
|
9
|
-
|
|
21
|
+
count: number;
|
|
10
22
|
timestamp: Date;
|
|
11
23
|
};
|
|
12
|
-
|
|
24
|
+
analyse_cache: {
|
|
25
|
+
id: number;
|
|
13
26
|
channelId: string;
|
|
14
|
-
channelName: string;
|
|
15
27
|
userId: string;
|
|
16
|
-
|
|
28
|
+
content: string;
|
|
29
|
+
timestamp: Date;
|
|
17
30
|
};
|
|
18
31
|
}
|
|
19
32
|
}
|
|
20
33
|
/**
|
|
21
34
|
* @class Collector
|
|
22
|
-
* @description
|
|
35
|
+
* @description 核心数据收集器。根据插件配置,高效地监听、收集、缓冲并持久化聊天数据。
|
|
23
36
|
*/
|
|
24
37
|
export declare class Collector {
|
|
25
38
|
private ctx;
|
|
39
|
+
private config;
|
|
40
|
+
/** @const {number} FLUSH_INTERVAL - 内存缓存区自动刷新到数据库的时间间隔。 */
|
|
26
41
|
private static readonly FLUSH_INTERVAL;
|
|
42
|
+
/** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
|
|
27
43
|
private static readonly BUFFER_THRESHOLD;
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
44
|
+
/** @member {Omit<Tables['analyse_cache'], 'id'>[]} cacheBuffer - 用于暂存原始消息的内存缓冲区,以减少数据库写入频率。 */
|
|
45
|
+
private cacheBuffer;
|
|
46
|
+
/** @member {Map<string, number>} uidCache - 用户 uid 的内存缓存,避免重复查询数据库。*/
|
|
47
|
+
private uidCache;
|
|
48
|
+
/** @member {Map<string, Promise<number>>} pendingUidRequests - 用于处理并发获取 uid 的请求锁。*/
|
|
49
|
+
private pendingUidRequests;
|
|
31
50
|
private flushInterval;
|
|
32
51
|
/**
|
|
33
52
|
* @constructor
|
|
34
|
-
* @param
|
|
35
|
-
|
|
36
|
-
constructor(ctx: Context);
|
|
37
|
-
/**
|
|
38
|
-
* 核心消息处理器,对消息进行格式化并存入缓冲区。
|
|
39
|
-
* @param session {Session} 消息会话对象。
|
|
53
|
+
* @param {Context} ctx - Koishi 的插件上下文。
|
|
54
|
+
* @param {Config} config - 插件的配置对象。
|
|
40
55
|
*/
|
|
41
|
-
|
|
56
|
+
constructor(ctx: Context, config: Config);
|
|
42
57
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
* @
|
|
58
|
+
* @private
|
|
59
|
+
* @method defineModels
|
|
60
|
+
* @description 定义插件所需的所有数据表模型。
|
|
46
61
|
*/
|
|
47
|
-
private
|
|
62
|
+
private defineModels;
|
|
48
63
|
/**
|
|
49
|
-
*
|
|
50
|
-
* @
|
|
51
|
-
* @
|
|
64
|
+
* @private
|
|
65
|
+
* @async
|
|
66
|
+
* @method handleMessage
|
|
67
|
+
* @description 统一的消息和命令处理器。它会解析收到的消息,提取关键信息并更新相应的统计数据。
|
|
68
|
+
* @param {Session} session - Koishi 的会话对象,包含消息的全部信息。
|
|
52
69
|
*/
|
|
53
|
-
private
|
|
70
|
+
private handleMessage;
|
|
54
71
|
/**
|
|
55
|
-
*
|
|
72
|
+
* @private
|
|
73
|
+
* @async
|
|
74
|
+
* @method getOrCreateUser
|
|
75
|
+
* @description 高效地获取或创建用户的中央记录 (`analyse_user`)。
|
|
76
|
+
* @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
|
|
77
|
+
* @param {string} channelId - 消息所在的频道或群组 ID。
|
|
78
|
+
* @returns {Promise<number | null>} 返回用户的唯一 `uid`,如果操作失败则返回 `null`。
|
|
56
79
|
*/
|
|
57
|
-
private
|
|
80
|
+
private getOrCreateUser;
|
|
58
81
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
* @
|
|
82
|
+
* @private
|
|
83
|
+
* @method sanitizeContent
|
|
84
|
+
* @description 将 Koishi 的消息元素 (Element) 数组净化为纯文本字符串,以便存储和分析。
|
|
85
|
+
* @param {Element[]} elements - 消息元素的数组。
|
|
86
|
+
* @returns {string} 净化后的纯文本字符串。
|
|
62
87
|
*/
|
|
63
|
-
private
|
|
88
|
+
private sanitizeContent;
|
|
64
89
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @
|
|
67
|
-
* @
|
|
68
|
-
* @
|
|
90
|
+
* @private
|
|
91
|
+
* @async
|
|
92
|
+
* @method flushCacheBuffer
|
|
93
|
+
* @description 将内存中的消息缓存 (`cacheBuffer`) 批量写入数据库。
|
|
69
94
|
*/
|
|
70
|
-
private
|
|
95
|
+
private flushCacheBuffer;
|
|
71
96
|
}
|
package/lib/Renderer.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* @
|
|
3
|
+
* @typedef {Array<string | number | Date>} RenderListItem
|
|
4
|
+
* @description 定义了统计列表中单行数据的格式,它是一个由字符串、数字或日期组成的元组。
|
|
5
5
|
*/
|
|
6
6
|
export type RenderListItem = (string | number | Date)[];
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* @interface ListRenderData
|
|
9
|
+
* @description 定义了调用渲染器生成列表图片时所需的完整数据结构。
|
|
9
10
|
*/
|
|
10
11
|
export interface ListRenderData {
|
|
11
12
|
title: string;
|
|
@@ -15,33 +16,41 @@ export interface ListRenderData {
|
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* @class Renderer
|
|
18
|
-
* @description
|
|
19
|
+
* @description 一个通用的列表渲染器。它使用 Koishi 的 Puppeteer 服务将结构化的 `ListRenderData` 数据
|
|
20
|
+
* 渲染为一张包含精美表格的图片。
|
|
19
21
|
*/
|
|
20
22
|
export declare class Renderer {
|
|
21
23
|
private ctx;
|
|
22
24
|
/**
|
|
23
25
|
* @constructor
|
|
24
|
-
* @param
|
|
26
|
+
* @param {Context} ctx - Koishi 的插件上下文,用于访问 puppeteer 服务。
|
|
25
27
|
*/
|
|
26
28
|
constructor(ctx: Context);
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @
|
|
30
|
-
* @
|
|
31
|
-
* @
|
|
30
|
+
* @public
|
|
31
|
+
* @async
|
|
32
|
+
* @method renderList
|
|
33
|
+
* @description 将列表数据渲染为图片。
|
|
34
|
+
* @param {ListRenderData} data - 待渲染的完整列表数据。
|
|
35
|
+
* @param {string[]} [headers] - (可选) 表头文案数组。如果提供,将会在表格顶部渲染表头。
|
|
36
|
+
* @returns {Promise<string | Buffer>} 渲染成功时返回图片的 Buffer 数据;如果输入数据为空,则返回提示文本。
|
|
32
37
|
*/
|
|
33
38
|
renderList(data: ListRenderData, headers?: string[]): Promise<string | Buffer>;
|
|
34
39
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
40
|
+
* @private
|
|
41
|
+
* @method formatDate
|
|
42
|
+
* @description 智能格式化日期。对于近期的时间,提供更人性化的相对时间描述(如“刚刚”,“x 分钟前”);对于较早的时间,则显示标准的“年-月-日”格式。
|
|
43
|
+
* @param {Date} date - 待格式化的 Date 对象。
|
|
37
44
|
* @returns {string} 格式化后的日期字符串。
|
|
38
45
|
*/
|
|
39
46
|
private formatDate;
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
43
|
-
* @
|
|
44
|
-
* @
|
|
48
|
+
* @private
|
|
49
|
+
* @method generateListHtml
|
|
50
|
+
* @description 根据传入的结构化数据和表头,动态生成用于 Puppeteer 渲染的完整 HTML 字符串。
|
|
51
|
+
* @param {ListRenderData} data - 列表数据对象。
|
|
52
|
+
* @param {string[]} [headers] - (可选) 表头数组。
|
|
53
|
+
* @returns {string | null} 返回生成的 HTML 字符串。如果列表数据为空,则返回 `null`。
|
|
45
54
|
*/
|
|
46
55
|
private generateListHtml;
|
|
47
56
|
}
|
package/lib/Stat.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* @constructor
|
|
14
|
+
* @param {Context} ctx - Koishi 的插件上下文。
|
|
15
|
+
* @param {Config} config - 插件的配置对象。
|
|
16
|
+
*/
|
|
17
|
+
constructor(ctx: Context, config: Config);
|
|
18
|
+
/**
|
|
19
|
+
* @method registerCommands
|
|
20
|
+
* @description 根据插件配置,动态地将 `.command` 和 `.message` 子命令注册到主 `analyse` 命令下。
|
|
21
|
+
* @param {Command} analyse - 主 `analyse` 命令实例。
|
|
22
|
+
*/
|
|
23
|
+
registerCommands(analyse: Command): void;
|
|
24
|
+
/**
|
|
25
|
+
* @private
|
|
26
|
+
* @async
|
|
27
|
+
* @method _generateTitle
|
|
28
|
+
* @description 通用的标题生成器。根据查询参数 (guildId, userId) 和统计类型动态生成易于理解的图片标题。
|
|
29
|
+
* @param {Session} session - 当前会话,备用。
|
|
30
|
+
* @param {string} [guildId] - (可选) 查询的群组 ID。
|
|
31
|
+
* @param {string} [userId] - (可选) 查询的用户 ID。
|
|
32
|
+
* @param {'命令' | '消息'} type - 统计类型,用于嵌入标题文本中。
|
|
33
|
+
* @returns {Promise<string>} 生成的标题字符串。
|
|
34
|
+
*/
|
|
35
|
+
private _generateTitle;
|
|
36
|
+
/**
|
|
37
|
+
* @private
|
|
38
|
+
* @async
|
|
39
|
+
* @method getCommandStats
|
|
40
|
+
* @description 从数据库中获取并聚合命令使用统计数据。
|
|
41
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
42
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
43
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
44
|
+
*/
|
|
45
|
+
private getCommandStats;
|
|
46
|
+
/**
|
|
47
|
+
* @private
|
|
48
|
+
* @async
|
|
49
|
+
* @method getMessageStats
|
|
50
|
+
* @description 从数据库中获取并聚合消息类型统计数据。
|
|
51
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
52
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
53
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
54
|
+
*/
|
|
55
|
+
private getMessageStats;
|
|
56
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
|
+
/**
|
|
3
|
+
* @name 插件使用说明
|
|
4
|
+
* @description 在 Koishi 控制台中显示的插件介绍和帮助信息。
|
|
5
|
+
*/
|
|
2
6
|
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";
|
|
3
7
|
export declare const name = "chat-analyse";
|
|
4
8
|
export declare const using: string[];
|
|
9
|
+
/**
|
|
10
|
+
* @interface Config
|
|
11
|
+
* @description 定义插件的配置项结构。
|
|
12
|
+
*/
|
|
5
13
|
export interface Config {
|
|
14
|
+
enableListener: boolean;
|
|
15
|
+
enableCmdStat: boolean;
|
|
16
|
+
enableMsgStat: boolean;
|
|
17
|
+
enableAdvanced: boolean;
|
|
6
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* @const {Schema<Config>} Config
|
|
21
|
+
* @description 使用 Koishi 的 `Schema` 来定义配置项的类型、默认值和在控制台中的交互界面。
|
|
22
|
+
*/
|
|
7
23
|
export declare const Config: Schema<Config>;
|
|
8
24
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
25
|
+
* @function apply
|
|
26
|
+
* @description Koishi 插件的主入口函数。
|
|
27
|
+
* @param {Context} ctx - Koishi 的插件上下文,提供了访问核心 API 的能力。
|
|
28
|
+
* @param {Config} config - 用户在 `koishi.config.js` 或控制台中配置的对象。
|
|
11
29
|
*/
|
|
12
|
-
export declare function apply(ctx: Context): void;
|
|
30
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -27,171 +27,213 @@ __export(src_exports, {
|
|
|
27
27
|
using: () => using
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(src_exports);
|
|
30
|
-
var
|
|
30
|
+
var import_koishi4 = require("koishi");
|
|
31
31
|
|
|
32
32
|
// src/Collector.ts
|
|
33
|
+
var import_koishi = require("koishi");
|
|
33
34
|
var Collector = class _Collector {
|
|
34
35
|
/**
|
|
35
36
|
* @constructor
|
|
36
|
-
* @param
|
|
37
|
+
* @param {Context} ctx - Koishi 的插件上下文。
|
|
38
|
+
* @param {Config} config - 插件的配置对象。
|
|
37
39
|
*/
|
|
38
|
-
constructor(ctx) {
|
|
40
|
+
constructor(ctx, config) {
|
|
39
41
|
this.ctx = ctx;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
channelId: "string",
|
|
43
|
-
userId: "string",
|
|
44
|
-
type: "string",
|
|
45
|
-
content: "text",
|
|
46
|
-
timestamp: "timestamp"
|
|
47
|
-
}, { primary: "id", autoInc: true, indexes: ["timestamp", "channelId", "userId", "type"] });
|
|
48
|
-
ctx.model.extend("analyse_name", {
|
|
49
|
-
channelId: "string",
|
|
50
|
-
channelName: "string",
|
|
51
|
-
userId: "string",
|
|
52
|
-
userName: "string"
|
|
53
|
-
}, { primary: ["channelId", "userId"] });
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.defineModels();
|
|
54
44
|
ctx.on("message", (session) => this.handleMessage(session));
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
if (this.config.enableAdvanced) {
|
|
46
|
+
this.flushInterval = setInterval(() => this.flushCacheBuffer(), _Collector.FLUSH_INTERVAL);
|
|
47
|
+
ctx.on("dispose", () => {
|
|
48
|
+
clearInterval(this.flushInterval);
|
|
49
|
+
this.flushCacheBuffer();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
60
52
|
}
|
|
61
53
|
static {
|
|
62
54
|
__name(this, "Collector");
|
|
63
55
|
}
|
|
64
|
-
|
|
56
|
+
/** @const {number} FLUSH_INTERVAL - 内存缓存区自动刷新到数据库的时间间隔。 */
|
|
65
57
|
static FLUSH_INTERVAL = 60 * 1e3;
|
|
66
|
-
|
|
58
|
+
/** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
|
|
67
59
|
static BUFFER_THRESHOLD = 100;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
/** @member {Omit<Tables['analyse_cache'], 'id'>[]} cacheBuffer - 用于暂存原始消息的内存缓冲区,以减少数据库写入频率。 */
|
|
61
|
+
cacheBuffer = [];
|
|
62
|
+
/** @member {Map<string, number>} uidCache - 用户 uid 的内存缓存,避免重复查询数据库。*/
|
|
63
|
+
uidCache = /* @__PURE__ */ new Map();
|
|
64
|
+
/** @member {Map<string, Promise<number>>} pendingUidRequests - 用于处理并发获取 uid 的请求锁。*/
|
|
65
|
+
pendingUidRequests = /* @__PURE__ */ new Map();
|
|
73
66
|
flushInterval;
|
|
74
67
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @
|
|
68
|
+
* @private
|
|
69
|
+
* @method defineModels
|
|
70
|
+
* @description 定义插件所需的所有数据表模型。
|
|
77
71
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
72
|
+
defineModels() {
|
|
73
|
+
this.ctx.model.extend("analyse_user", {
|
|
74
|
+
uid: "unsigned",
|
|
75
|
+
channelId: "string",
|
|
76
|
+
userId: "string",
|
|
77
|
+
channelName: "string",
|
|
78
|
+
userName: "string"
|
|
79
|
+
}, { primary: "uid", autoInc: true, indexes: ["channelId", "userId"] });
|
|
80
|
+
this.ctx.model.extend("analyse_cmd", {
|
|
81
|
+
uid: "unsigned",
|
|
82
|
+
command: "string",
|
|
83
|
+
count: "unsigned",
|
|
84
|
+
timestamp: "timestamp"
|
|
85
|
+
}, { primary: ["uid", "command"] });
|
|
86
|
+
this.ctx.model.extend("analyse_msg", {
|
|
87
|
+
uid: "unsigned",
|
|
88
|
+
type: "string",
|
|
89
|
+
count: "unsigned",
|
|
90
|
+
timestamp: "timestamp"
|
|
91
|
+
}, { primary: ["uid", "type"] });
|
|
92
|
+
if (this.config.enableAdvanced) {
|
|
93
|
+
this.ctx.model.extend("analyse_cache", {
|
|
94
|
+
id: "unsigned",
|
|
95
|
+
channelId: "string",
|
|
96
|
+
userId: "string",
|
|
97
|
+
content: "text",
|
|
98
|
+
timestamp: "timestamp"
|
|
99
|
+
}, { primary: "id", autoInc: true });
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @
|
|
98
|
-
* @
|
|
103
|
+
* @private
|
|
104
|
+
* @async
|
|
105
|
+
* @method handleMessage
|
|
106
|
+
* @description 统一的消息和命令处理器。它会解析收到的消息,提取关键信息并更新相应的统计数据。
|
|
107
|
+
* @param {Session} session - Koishi 的会话对象,包含消息的全部信息。
|
|
99
108
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
async handleMessage(session) {
|
|
110
|
+
try {
|
|
111
|
+
const { userId, channelId, guildId, content, timestamp, argv, elements } = session;
|
|
112
|
+
const effectiveId = channelId || guildId;
|
|
113
|
+
if (!effectiveId || !userId || !timestamp || !content?.trim()) return;
|
|
114
|
+
const uid = await this.getOrCreateUser(session, effectiveId);
|
|
115
|
+
if (!uid) return;
|
|
116
|
+
const now = /* @__PURE__ */ new Date();
|
|
117
|
+
if (argv?.command) {
|
|
118
|
+
await this.ctx.database.upsert("analyse_cmd", (row) => [{
|
|
119
|
+
uid,
|
|
120
|
+
command: argv.command.name,
|
|
121
|
+
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, import_koishi.$.literal(0)), 1),
|
|
122
|
+
timestamp: now
|
|
123
|
+
}]);
|
|
124
|
+
}
|
|
125
|
+
const uniqueElementTypes = new Set(elements.map((e) => e.type));
|
|
126
|
+
for (const type of uniqueElementTypes) {
|
|
127
|
+
await this.ctx.database.upsert("analyse_msg", (row) => [{
|
|
128
|
+
uid,
|
|
129
|
+
type,
|
|
130
|
+
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, import_koishi.$.literal(0)), 1),
|
|
131
|
+
timestamp: now
|
|
132
|
+
}]);
|
|
133
|
+
}
|
|
134
|
+
if (this.config.enableAdvanced) {
|
|
135
|
+
this.cacheBuffer.push({
|
|
136
|
+
channelId: effectiveId,
|
|
137
|
+
userId,
|
|
138
|
+
content: this.sanitizeContent(elements),
|
|
139
|
+
timestamp: new Date(timestamp)
|
|
140
|
+
});
|
|
141
|
+
if (this.cacheBuffer.length >= _Collector.BUFFER_THRESHOLD) await this.flushCacheBuffer();
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.ctx.logger.warn("消息处理出错:", error);
|
|
145
|
+
}
|
|
103
146
|
}
|
|
104
147
|
/**
|
|
105
|
-
*
|
|
106
|
-
* @
|
|
107
|
-
* @
|
|
148
|
+
* @private
|
|
149
|
+
* @async
|
|
150
|
+
* @method getOrCreateUser
|
|
151
|
+
* @description 高效地获取或创建用户的中央记录 (`analyse_user`)。
|
|
152
|
+
* @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
|
|
153
|
+
* @param {string} channelId - 消息所在的频道或群组 ID。
|
|
154
|
+
* @returns {Promise<number | null>} 返回用户的唯一 `uid`,如果操作失败则返回 `null`。
|
|
108
155
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return
|
|
156
|
+
async getOrCreateUser(session, channelId) {
|
|
157
|
+
const { userId, bot, guildId } = session;
|
|
158
|
+
const cacheKey = `${channelId}:${userId}`;
|
|
159
|
+
if (this.uidCache.has(cacheKey)) return this.uidCache.get(cacheKey);
|
|
160
|
+
if (this.pendingUidRequests.has(cacheKey)) return this.pendingUidRequests.get(cacheKey);
|
|
161
|
+
const promise = (async () => {
|
|
162
|
+
try {
|
|
163
|
+
const existing = await this.ctx.database.get("analyse_user", { channelId, userId }, ["uid"]);
|
|
164
|
+
if (existing.length > 0) {
|
|
165
|
+
this.uidCache.set(cacheKey, existing[0].uid);
|
|
166
|
+
return existing[0].uid;
|
|
167
|
+
}
|
|
168
|
+
const [guild, member] = await Promise.all([
|
|
169
|
+
guildId ? bot.getGuild(guildId).catch(() => null) : Promise.resolve(null),
|
|
170
|
+
guildId ? bot.getGuildMember(guildId, userId).catch(() => null) : Promise.resolve(null)
|
|
171
|
+
]);
|
|
172
|
+
const user = !member ? await bot.getUser(userId).catch(() => null) : null;
|
|
173
|
+
const newUser = await this.ctx.database.create("analyse_user", {
|
|
174
|
+
channelId,
|
|
175
|
+
userId,
|
|
176
|
+
channelName: guild?.name || channelId,
|
|
177
|
+
userName: member?.nick || member?.name || user?.name || userId
|
|
178
|
+
});
|
|
179
|
+
this.uidCache.set(cacheKey, newUser.uid);
|
|
180
|
+
return newUser.uid;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.ctx.logger.error(`创建或获取用户(${cacheKey}) UID 失败:`, error);
|
|
183
|
+
return null;
|
|
184
|
+
} finally {
|
|
185
|
+
this.pendingUidRequests.delete(cacheKey);
|
|
120
186
|
}
|
|
121
|
-
})
|
|
187
|
+
})();
|
|
188
|
+
this.pendingUidRequests.set(cacheKey, promise);
|
|
189
|
+
return promise;
|
|
122
190
|
}
|
|
123
191
|
/**
|
|
124
|
-
*
|
|
192
|
+
* @private
|
|
193
|
+
* @method sanitizeContent
|
|
194
|
+
* @description 将 Koishi 的消息元素 (Element) 数组净化为纯文本字符串,以便存储和分析。
|
|
195
|
+
* @param {Element[]} elements - 消息元素的数组。
|
|
196
|
+
* @returns {string} 净化后的纯文本字符串。
|
|
125
197
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
198
|
+
sanitizeContent = /* @__PURE__ */ __name((elements) => elements.map((e) => {
|
|
199
|
+
switch (e.type) {
|
|
200
|
+
case "text":
|
|
201
|
+
return e.attrs.content;
|
|
202
|
+
case "img":
|
|
203
|
+
return e.attrs.summary === "[动画表情]" ? "[gif]" : "[img]";
|
|
204
|
+
case "at":
|
|
205
|
+
return `[at:${e.attrs.id}]`;
|
|
206
|
+
default:
|
|
207
|
+
return `[${e.type}]`;
|
|
135
208
|
}
|
|
136
|
-
}
|
|
209
|
+
}).join(""), "sanitizeContent");
|
|
137
210
|
/**
|
|
138
|
-
*
|
|
139
|
-
* @
|
|
140
|
-
* @
|
|
211
|
+
* @private
|
|
212
|
+
* @async
|
|
213
|
+
* @method flushCacheBuffer
|
|
214
|
+
* @description 将内存中的消息缓存 (`cacheBuffer`) 批量写入数据库。
|
|
141
215
|
*/
|
|
142
|
-
async
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const CACHE_EXPIRATION = 24 * 60 * 60 * 1e3;
|
|
147
|
-
if (this.pendingNameRequests.has(cacheKey)) return this.pendingNameRequests.get(cacheKey);
|
|
148
|
-
const cached = this.nameCache.get(cacheKey);
|
|
149
|
-
if (cached && Date.now() - cached.timestamp < CACHE_EXPIRATION) return;
|
|
150
|
-
const promise = this.fetchAndUpdateNames(session, effectiveId, cacheKey);
|
|
151
|
-
this.pendingNameRequests.set(cacheKey, promise);
|
|
152
|
-
promise.finally(() => this.pendingNameRequests.delete(cacheKey));
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* 异步获取用户和群组的最新名称,并更新到数据库和内存缓存。
|
|
156
|
-
* @param session {Session} 消息会话对象。
|
|
157
|
-
* @param effectiveId {string} 频道/群组ID。
|
|
158
|
-
* @param cacheKey {string} 用于缓存的键。
|
|
159
|
-
*/
|
|
160
|
-
async fetchAndUpdateNames(session, effectiveId, cacheKey) {
|
|
216
|
+
async flushCacheBuffer() {
|
|
217
|
+
if (this.cacheBuffer.length === 0) return;
|
|
218
|
+
const bufferToFlush = this.cacheBuffer;
|
|
219
|
+
this.cacheBuffer = [];
|
|
161
220
|
try {
|
|
162
|
-
|
|
163
|
-
const [guild, member] = await Promise.all([
|
|
164
|
-
guildId ? bot.getGuild(guildId).catch(() => null) : Promise.resolve(null),
|
|
165
|
-
guildId && userId ? bot.getGuildMember(guildId, userId).catch(() => null) : Promise.resolve(null)
|
|
166
|
-
]);
|
|
167
|
-
const channelName = guild?.name;
|
|
168
|
-
const userName = member?.nick || member?.name;
|
|
169
|
-
if (!channelName || !userName) {
|
|
170
|
-
this.nameCache.set(cacheKey, { name: null, timestamp: Date.now() });
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
await this.ctx.database.upsert("analyse_name", [{
|
|
174
|
-
channelId: effectiveId,
|
|
175
|
-
userId,
|
|
176
|
-
channelName,
|
|
177
|
-
userName
|
|
178
|
-
}]);
|
|
179
|
-
this.nameCache.set(cacheKey, { name: userName, timestamp: Date.now() });
|
|
221
|
+
await this.ctx.database.upsert("analyse_cache", bufferToFlush);
|
|
180
222
|
} catch (error) {
|
|
181
|
-
this.
|
|
223
|
+
this.ctx.logger.error("写入缓存出错:", error);
|
|
182
224
|
}
|
|
183
225
|
}
|
|
184
226
|
};
|
|
185
227
|
|
|
186
|
-
// src/
|
|
187
|
-
var
|
|
228
|
+
// src/Stat.ts
|
|
229
|
+
var import_koishi3 = require("koishi");
|
|
188
230
|
|
|
189
231
|
// src/Renderer.ts
|
|
190
|
-
var
|
|
232
|
+
var import_koishi2 = require("koishi");
|
|
191
233
|
var Renderer = class {
|
|
192
234
|
/**
|
|
193
235
|
* @constructor
|
|
194
|
-
* @param
|
|
236
|
+
* @param {Context} ctx - Koishi 的插件上下文,用于访问 puppeteer 服务。
|
|
195
237
|
*/
|
|
196
238
|
constructor(ctx) {
|
|
197
239
|
this.ctx = ctx;
|
|
@@ -200,10 +242,13 @@ var Renderer = class {
|
|
|
200
242
|
__name(this, "Renderer");
|
|
201
243
|
}
|
|
202
244
|
/**
|
|
203
|
-
*
|
|
204
|
-
* @
|
|
205
|
-
* @
|
|
206
|
-
* @
|
|
245
|
+
* @public
|
|
246
|
+
* @async
|
|
247
|
+
* @method renderList
|
|
248
|
+
* @description 将列表数据渲染为图片。
|
|
249
|
+
* @param {ListRenderData} data - 待渲染的完整列表数据。
|
|
250
|
+
* @param {string[]} [headers] - (可选) 表头文案数组。如果提供,将会在表格顶部渲染表头。
|
|
251
|
+
* @returns {Promise<string | Buffer>} 渲染成功时返回图片的 Buffer 数据;如果输入数据为空,则返回提示文本。
|
|
207
252
|
*/
|
|
208
253
|
async renderList(data, headers) {
|
|
209
254
|
const htmlContent = this.generateListHtml(data, headers);
|
|
@@ -211,23 +256,27 @@ var Renderer = class {
|
|
|
211
256
|
return this.ctx.puppeteer.render(htmlContent);
|
|
212
257
|
}
|
|
213
258
|
/**
|
|
214
|
-
*
|
|
215
|
-
* @
|
|
259
|
+
* @private
|
|
260
|
+
* @method formatDate
|
|
261
|
+
* @description 智能格式化日期。对于近期的时间,提供更人性化的相对时间描述(如“刚刚”,“x 分钟前”);对于较早的时间,则显示标准的“年-月-日”格式。
|
|
262
|
+
* @param {Date} date - 待格式化的 Date 对象。
|
|
216
263
|
* @returns {string} 格式化后的日期字符串。
|
|
217
264
|
*/
|
|
218
265
|
formatDate(date) {
|
|
219
266
|
if (!date) return "未知";
|
|
220
267
|
const diff = Date.now() - date.getTime();
|
|
221
|
-
if (diff <
|
|
222
|
-
if (diff <
|
|
223
|
-
if (diff <
|
|
268
|
+
if (diff < import_koishi2.Time.minute) return "刚刚";
|
|
269
|
+
if (diff < import_koishi2.Time.hour) return `${Math.floor(diff / import_koishi2.Time.minute)} 分钟前`;
|
|
270
|
+
if (diff < import_koishi2.Time.day) return `${Math.floor(diff / import_koishi2.Time.hour)} 小时前`;
|
|
224
271
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
225
272
|
}
|
|
226
273
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @
|
|
229
|
-
* @
|
|
230
|
-
* @
|
|
274
|
+
* @private
|
|
275
|
+
* @method generateListHtml
|
|
276
|
+
* @description 根据传入的结构化数据和表头,动态生成用于 Puppeteer 渲染的完整 HTML 字符串。
|
|
277
|
+
* @param {ListRenderData} data - 列表数据对象。
|
|
278
|
+
* @param {string[]} [headers] - (可选) 表头数组。
|
|
279
|
+
* @returns {string | null} 返回生成的 HTML 字符串。如果列表数据为空,则返回 `null`。
|
|
231
280
|
*/
|
|
232
281
|
generateListHtml(data, headers) {
|
|
233
282
|
const { title, time, total, list } = data;
|
|
@@ -292,102 +341,164 @@ var Renderer = class {
|
|
|
292
341
|
}
|
|
293
342
|
};
|
|
294
343
|
|
|
295
|
-
// src/
|
|
296
|
-
var
|
|
297
|
-
|
|
344
|
+
// src/Stat.ts
|
|
345
|
+
var Stat = class {
|
|
346
|
+
/**
|
|
347
|
+
* @constructor
|
|
348
|
+
* @param {Context} ctx - Koishi 的插件上下文。
|
|
349
|
+
* @param {Config} config - 插件的配置对象。
|
|
350
|
+
*/
|
|
351
|
+
constructor(ctx, config) {
|
|
298
352
|
this.ctx = ctx;
|
|
353
|
+
this.config = config;
|
|
299
354
|
this.renderer = new Renderer(ctx);
|
|
300
|
-
this.ctx.model.extend("analyse_cmd", {
|
|
301
|
-
channelId: "string",
|
|
302
|
-
userId: "string",
|
|
303
|
-
command: "string",
|
|
304
|
-
count: "unsigned",
|
|
305
|
-
timestamp: "timestamp"
|
|
306
|
-
}, { primary: ["channelId", "userId", "command"] });
|
|
307
|
-
this.ctx.on("command/before-execute", async ({ command, session }) => {
|
|
308
|
-
const { userId, guildId } = session;
|
|
309
|
-
if (!guildId || !userId) return;
|
|
310
|
-
const query = { channelId: guildId, userId, command: command.name };
|
|
311
|
-
await this.ctx.database.upsert("analyse_cmd", (row) => [{
|
|
312
|
-
...query,
|
|
313
|
-
count: import_koishi2.$.add(import_koishi2.$.ifNull(row.count, 0), 1),
|
|
314
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
315
|
-
}]);
|
|
316
|
-
});
|
|
317
355
|
}
|
|
318
356
|
static {
|
|
319
|
-
__name(this, "
|
|
357
|
+
__name(this, "Stat");
|
|
320
358
|
}
|
|
321
359
|
renderer;
|
|
322
360
|
/**
|
|
323
|
-
*
|
|
324
|
-
* @
|
|
361
|
+
* @method registerCommands
|
|
362
|
+
* @description 根据插件配置,动态地将 `.command` 和 `.message` 子命令注册到主 `analyse` 命令下。
|
|
363
|
+
* @param {Command} analyse - 主 `analyse` 命令实例。
|
|
325
364
|
*/
|
|
326
365
|
registerCommands(analyse) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
366
|
+
if (this.config.enableCmdStat) {
|
|
367
|
+
analyse.subcommand(".command", "命令使用统计").option("user", "-u [user:user] 查看指定用户的统计").option("guild", "-g [guildId:string] 查看指定群组的统计 (默认当前群)").usage("查询命令使用统计。支持按用户、按群组或组合查询。").action(async ({ session, options }) => {
|
|
368
|
+
const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
|
|
369
|
+
let guildId = options.guild;
|
|
370
|
+
if (options.guild === "" && !options.user) {
|
|
371
|
+
if (!session.guildId) return "私聊中请使用 -g <群组ID> 指定群组。";
|
|
372
|
+
guildId = session.guildId;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const stats = await this.getCommandStats(guildId, userId);
|
|
376
|
+
if (typeof stats === "string") return stats;
|
|
377
|
+
const title = await this._generateTitle(session, guildId, userId, "命令");
|
|
378
|
+
const renderData = {
|
|
379
|
+
title,
|
|
380
|
+
time: /* @__PURE__ */ new Date(),
|
|
381
|
+
total: stats.total,
|
|
382
|
+
list: stats.list
|
|
383
|
+
};
|
|
384
|
+
const headers = ["命令", "次数", "上次使用"];
|
|
385
|
+
const result = await this.renderer.renderList(renderData, headers);
|
|
386
|
+
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
this.ctx.logger.error("渲染命令统计图片失败:", error);
|
|
389
|
+
return "渲染命令统计图片失败";
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (this.config.enableMsgStat) {
|
|
394
|
+
analyse.subcommand(".message", "消息类型统计").option("user", "-u [user:user] 查看指定用户的统计").option("guild", "-g [guildId:string] 查看指定群组的统计 (默认当前群)").usage("查询消息类型统计。支持按用户、按群组或组合查询。").action(async ({ session, options }) => {
|
|
395
|
+
const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
|
|
396
|
+
let guildId = options.guild;
|
|
397
|
+
if (options.guild === "" && !options.user) {
|
|
398
|
+
if (!session.guildId) return "私聊中请使用 -g <群组ID> 指定群组。";
|
|
399
|
+
guildId = session.guildId;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const stats = await this.getMessageStats(guildId, userId);
|
|
403
|
+
if (typeof stats === "string") return stats;
|
|
404
|
+
const title = await this._generateTitle(session, guildId, userId, "消息");
|
|
405
|
+
const renderData = {
|
|
406
|
+
title,
|
|
407
|
+
time: /* @__PURE__ */ new Date(),
|
|
408
|
+
total: stats.total,
|
|
409
|
+
list: stats.list
|
|
410
|
+
};
|
|
411
|
+
const headers = ["消息类型", "条数", "上次发送"];
|
|
412
|
+
const result = await this.renderer.renderList(renderData, headers);
|
|
413
|
+
return Buffer.isBuffer(result) ? import_koishi3.Element.image(result, "image/png") : result;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
this.ctx.logger.error("渲染消息统计图片失败:", error);
|
|
416
|
+
return "渲染消息统计图片失败";
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
352
420
|
}
|
|
353
421
|
/**
|
|
354
|
-
*
|
|
422
|
+
* @private
|
|
423
|
+
* @async
|
|
424
|
+
* @method _generateTitle
|
|
425
|
+
* @description 通用的标题生成器。根据查询参数 (guildId, userId) 和统计类型动态生成易于理解的图片标题。
|
|
426
|
+
* @param {Session} session - 当前会话,备用。
|
|
427
|
+
* @param {string} [guildId] - (可选) 查询的群组 ID。
|
|
428
|
+
* @param {string} [userId] - (可选) 查询的用户 ID。
|
|
429
|
+
* @param {'命令' | '消息'} type - 统计类型,用于嵌入标题文本中。
|
|
430
|
+
* @returns {Promise<string>} 生成的标题字符串。
|
|
355
431
|
*/
|
|
356
|
-
async
|
|
432
|
+
async _generateTitle(session, guildId, userId, type) {
|
|
357
433
|
if (userId && guildId) {
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
434
|
+
const user = await this.ctx.database.get("analyse_user", { channelId: guildId, userId }, ["userName"]);
|
|
435
|
+
const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
|
|
436
|
+
const userName = user[0]?.userName || userId;
|
|
437
|
+
const guildName = guild[0]?.channelName || guildId;
|
|
438
|
+
return `${userName} 在 ${guildName} 的${type}统计`;
|
|
361
439
|
}
|
|
362
440
|
if (userId) {
|
|
363
|
-
const
|
|
364
|
-
|
|
441
|
+
const user = await this.ctx.database.get("analyse_user", { userId }, ["userName"]);
|
|
442
|
+
const userName = user[0]?.userName || userId;
|
|
443
|
+
return `${userName} 的全局${type}统计`;
|
|
365
444
|
}
|
|
366
445
|
if (guildId) {
|
|
367
|
-
const
|
|
368
|
-
|
|
446
|
+
const guild = await this.ctx.database.get("analyse_user", { channelId: guildId }, ["channelName"]);
|
|
447
|
+
const guildName = guild[0]?.channelName || guildId;
|
|
448
|
+
return `${guildName} 的${type}统计`;
|
|
369
449
|
}
|
|
370
|
-
return
|
|
450
|
+
return `全局${type}统计`;
|
|
371
451
|
}
|
|
372
452
|
/**
|
|
373
|
-
*
|
|
374
|
-
* @
|
|
375
|
-
* @
|
|
376
|
-
* @
|
|
453
|
+
* @private
|
|
454
|
+
* @async
|
|
455
|
+
* @method getCommandStats
|
|
456
|
+
* @description 从数据库中获取并聚合命令使用统计数据。
|
|
457
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
458
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
459
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
377
460
|
*/
|
|
378
461
|
async getCommandStats(guildId, userId) {
|
|
379
|
-
const
|
|
380
|
-
if (guildId)
|
|
381
|
-
if (userId)
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
462
|
+
const userQuery = {};
|
|
463
|
+
if (guildId) userQuery.channelId = guildId;
|
|
464
|
+
if (userId) userQuery.userId = userId;
|
|
465
|
+
const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
|
|
466
|
+
if (users.length === 0) return "暂无目标用户的统计数据";
|
|
467
|
+
const uids = users.map((u) => u.uid);
|
|
468
|
+
const aggregatedStats = await this.ctx.database.select("analyse_cmd").where({ uid: { $in: uids } }).groupBy(["command"], {
|
|
469
|
+
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
470
|
+
lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
|
|
385
471
|
}).orderBy("count", "desc").execute();
|
|
386
472
|
if (aggregatedStats.length === 0) return "暂无统计数据";
|
|
387
473
|
const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
|
|
388
474
|
const list = aggregatedStats.map((item) => [item.command, item.count, item.lastUsed]);
|
|
389
475
|
return { list, total: totalCount };
|
|
390
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* @private
|
|
479
|
+
* @async
|
|
480
|
+
* @method getMessageStats
|
|
481
|
+
* @description 从数据库中获取并聚合消息类型统计数据。
|
|
482
|
+
* @param {string} [guildId] - (可选) 若提供,则将范围限制在此群组。
|
|
483
|
+
* @param {string} [userId] - (可选) 若提供,则将范围限制在此用户。
|
|
484
|
+
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 返回一个包含列表和总数的对象,或在无数据时返回提示字符串。
|
|
485
|
+
*/
|
|
486
|
+
async getMessageStats(guildId, userId) {
|
|
487
|
+
const userQuery = {};
|
|
488
|
+
if (guildId) userQuery.channelId = guildId;
|
|
489
|
+
if (userId) userQuery.userId = userId;
|
|
490
|
+
const users = await this.ctx.database.get("analyse_user", userQuery, ["uid"]);
|
|
491
|
+
if (users.length === 0) return "暂无目标用户的统计数据";
|
|
492
|
+
const uids = users.map((u) => u.uid);
|
|
493
|
+
const aggregatedStats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids } }).groupBy(["type"], {
|
|
494
|
+
count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count"),
|
|
495
|
+
lastUsed: /* @__PURE__ */ __name((row) => import_koishi3.$.max(row.timestamp), "lastUsed")
|
|
496
|
+
}).orderBy("count", "desc").execute();
|
|
497
|
+
if (aggregatedStats.length === 0) return "暂无统计数据";
|
|
498
|
+
const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
|
|
499
|
+
const list = aggregatedStats.map((item) => [item.type, item.count, item.lastUsed]);
|
|
500
|
+
return { list, total: totalCount };
|
|
501
|
+
}
|
|
391
502
|
};
|
|
392
503
|
|
|
393
504
|
// src/index.ts
|
|
@@ -405,12 +516,20 @@ var usage = `
|
|
|
405
516
|
`;
|
|
406
517
|
var name = "chat-analyse";
|
|
407
518
|
var using = ["database", "puppeteer"];
|
|
408
|
-
var Config =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
519
|
+
var Config = import_koishi4.Schema.intersect([
|
|
520
|
+
import_koishi4.Schema.object({
|
|
521
|
+
enableListener: import_koishi4.Schema.boolean().default(true).description("开启监听")
|
|
522
|
+
}).description("基本设置"),
|
|
523
|
+
import_koishi4.Schema.object({
|
|
524
|
+
enableCmdStat: import_koishi4.Schema.boolean().default(true).description("启用命令统计"),
|
|
525
|
+
enableMsgStat: import_koishi4.Schema.boolean().default(true).description("启用消息统计"),
|
|
526
|
+
enableAdvanced: import_koishi4.Schema.boolean().default(true).description("启用原始记录")
|
|
527
|
+
}).description("功能开关")
|
|
528
|
+
]);
|
|
529
|
+
function apply(ctx, config) {
|
|
530
|
+
if (config.enableListener) new Collector(ctx, config);
|
|
412
531
|
const analyse = ctx.command("analyse", "聊天记录分析");
|
|
413
|
-
|
|
532
|
+
new Stat(ctx, config).registerCommands(analyse);
|
|
414
533
|
}
|
|
415
534
|
__name(apply, "apply");
|
|
416
535
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
package/lib/CmdStat.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { Context, Command } from 'koishi';
|
|
2
|
-
import { Renderer } from './Renderer';
|
|
3
|
-
declare module 'koishi' {
|
|
4
|
-
interface Tables {
|
|
5
|
-
analyse_cmd: {
|
|
6
|
-
channelId: string;
|
|
7
|
-
userId: string;
|
|
8
|
-
command: string;
|
|
9
|
-
count: number;
|
|
10
|
-
timestamp: Date;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* @class CmdStat
|
|
16
|
-
* @description 提供命令统计服务,处理用户查询并渲染结果。
|
|
17
|
-
*/
|
|
18
|
-
export declare class CmdStat {
|
|
19
|
-
private ctx;
|
|
20
|
-
renderer: Renderer;
|
|
21
|
-
constructor(ctx: Context);
|
|
22
|
-
/**
|
|
23
|
-
* 注册所有相关的子命令到主 `analyse` 命令下。
|
|
24
|
-
* @param analyse {Command} 主 `analyse` 命令实例。
|
|
25
|
-
*/
|
|
26
|
-
registerCommands(analyse: Command): void;
|
|
27
|
-
/**
|
|
28
|
-
* 根据查询参数动态生成图片标题。
|
|
29
|
-
*/
|
|
30
|
-
private generateTitle;
|
|
31
|
-
/**
|
|
32
|
-
* 从数据库获取并聚合命令统计数据。
|
|
33
|
-
* @param guildId {string} (可选) 群组ID。
|
|
34
|
-
* @param userId {string} (可选) 用户ID。
|
|
35
|
-
* @returns {Promise<{ list: RenderListItem[], total: number } | string>} 包含结果列表和总数的对象,或错误/提示信息。
|
|
36
|
-
*/
|
|
37
|
-
private getCommandStats;
|
|
38
|
-
}
|