koishi-plugin-chat-analyse 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Collector.d.ts +7 -4
- package/lib/index.js +52 -39
- package/package.json +1 -1
package/lib/Collector.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Context } from 'koishi';
|
|
|
2
2
|
declare module 'koishi' {
|
|
3
3
|
interface Tables {
|
|
4
4
|
analyse_msg: {
|
|
5
|
+
id: number;
|
|
5
6
|
channelId: string;
|
|
6
7
|
userId: string;
|
|
7
8
|
type: string;
|
|
@@ -28,6 +29,7 @@ export declare class Collector {
|
|
|
28
29
|
private msgBuffer;
|
|
29
30
|
private flushInterval;
|
|
30
31
|
private nameCache;
|
|
32
|
+
private pendingNameRequests;
|
|
31
33
|
/**
|
|
32
34
|
* @constructor
|
|
33
35
|
* @param ctx {Context} Koishi 的上下文对象
|
|
@@ -40,9 +42,6 @@ export declare class Collector {
|
|
|
40
42
|
private handleMessage;
|
|
41
43
|
/**
|
|
42
44
|
* 从消息元素中提取并汇总类型。
|
|
43
|
-
* @example
|
|
44
|
-
* // returns "[text][img]"
|
|
45
|
-
* summarizeElementTypes([{type: 'text'}, {type: 'img'}])
|
|
46
45
|
*/
|
|
47
46
|
private summarizeElementTypes;
|
|
48
47
|
/**
|
|
@@ -55,7 +54,11 @@ export declare class Collector {
|
|
|
55
54
|
private flushBuffer;
|
|
56
55
|
/**
|
|
57
56
|
* 检查并更新用户和群组的名称信息。
|
|
58
|
-
* 如果名称不在缓存或缓存已过期 (超过24小时),则从 API 获取并更新到数据库。
|
|
59
57
|
*/
|
|
60
58
|
private updateNameIfNeeded;
|
|
59
|
+
/**
|
|
60
|
+
* @private
|
|
61
|
+
* 封装了实际获取和更新名称的异步逻辑。
|
|
62
|
+
*/
|
|
63
|
+
private fetchAndUpdateNames;
|
|
61
64
|
}
|
package/lib/index.js
CHANGED
|
@@ -38,14 +38,16 @@ var Collector = class _Collector {
|
|
|
38
38
|
constructor(ctx) {
|
|
39
39
|
this.ctx = ctx;
|
|
40
40
|
this.ctx.model.extend("analyse_msg", {
|
|
41
|
+
id: "unsigned",
|
|
41
42
|
channelId: "string",
|
|
42
43
|
userId: "string",
|
|
43
44
|
type: "string",
|
|
44
45
|
content: "text",
|
|
45
46
|
timestamp: "timestamp"
|
|
46
47
|
}, {
|
|
47
|
-
primary:
|
|
48
|
-
|
|
48
|
+
primary: "id",
|
|
49
|
+
autoInc: true,
|
|
50
|
+
indexes: ["timestamp", "channelId", "userId", "type"]
|
|
49
51
|
});
|
|
50
52
|
this.ctx.model.extend("analyse_name", {
|
|
51
53
|
channelId: "string",
|
|
@@ -67,16 +69,12 @@ var Collector = class _Collector {
|
|
|
67
69
|
static {
|
|
68
70
|
__name(this, "Collector");
|
|
69
71
|
}
|
|
70
|
-
// 每隔 1 分钟将缓冲区数据写入数据库
|
|
71
72
|
static FLUSH_INTERVAL = 60 * 1e3;
|
|
72
|
-
// 当缓冲区数据达到 100 条时,立即写入数据库
|
|
73
73
|
static BUFFER_THRESHOLD = 100;
|
|
74
|
-
// 消息数据写入缓冲区
|
|
75
74
|
msgBuffer = [];
|
|
76
|
-
// 定时器,用于周期性地清空缓冲区
|
|
77
75
|
flushInterval;
|
|
78
|
-
// 名称缓存,减少不必要的 API 请求 (key: 'channelId:userId')
|
|
79
76
|
nameCache = /* @__PURE__ */ new Map();
|
|
77
|
+
pendingNameRequests = /* @__PURE__ */ new Map();
|
|
80
78
|
/**
|
|
81
79
|
* 核心消息处理函数。
|
|
82
80
|
* @param session {Session} 消息会话对象
|
|
@@ -84,8 +82,8 @@ var Collector = class _Collector {
|
|
|
84
82
|
async handleMessage(session) {
|
|
85
83
|
const { userId, channelId, guildId, content, timestamp, argv, elements } = session;
|
|
86
84
|
const effectiveId = channelId || guildId;
|
|
87
|
-
if (!effectiveId || !userId) return;
|
|
88
|
-
this.updateNameIfNeeded(session, effectiveId);
|
|
85
|
+
if (!effectiveId || !userId || !timestamp) return;
|
|
86
|
+
await this.updateNameIfNeeded(session, effectiveId);
|
|
89
87
|
const isCommand = !!argv?.command;
|
|
90
88
|
const type = isCommand ? argv.command.name : this.summarizeElementTypes(elements);
|
|
91
89
|
const finalContent = isCommand ? content : this.sanitizeContent(elements);
|
|
@@ -103,9 +101,6 @@ var Collector = class _Collector {
|
|
|
103
101
|
}
|
|
104
102
|
/**
|
|
105
103
|
* 从消息元素中提取并汇总类型。
|
|
106
|
-
* @example
|
|
107
|
-
* // returns "[text][img]"
|
|
108
|
-
* summarizeElementTypes([{type: 'text'}, {type: 'img'}])
|
|
109
104
|
*/
|
|
110
105
|
summarizeElementTypes(elements) {
|
|
111
106
|
return [...new Set(elements.map((e) => `[${e.type}]`))].join("");
|
|
@@ -135,39 +130,57 @@ var Collector = class _Collector {
|
|
|
135
130
|
const bufferToFlush = this.msgBuffer;
|
|
136
131
|
this.msgBuffer = [];
|
|
137
132
|
try {
|
|
138
|
-
await this.ctx.database.
|
|
133
|
+
await this.ctx.database.upsert("analyse_msg", bufferToFlush);
|
|
139
134
|
} catch (error) {
|
|
140
|
-
this.ctx.logger.error("
|
|
135
|
+
this.ctx.logger.error("数据写入失败:", error);
|
|
141
136
|
this.msgBuffer.unshift(...bufferToFlush);
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
139
|
/**
|
|
145
140
|
* 检查并更新用户和群组的名称信息。
|
|
146
|
-
* 如果名称不在缓存或缓存已过期 (超过24小时),则从 API 获取并更新到数据库。
|
|
147
141
|
*/
|
|
148
142
|
async updateNameIfNeeded(session, effectiveId) {
|
|
149
|
-
const { userId
|
|
143
|
+
const { userId } = session;
|
|
144
|
+
if (!userId) return;
|
|
150
145
|
const cacheKey = `${effectiveId}:${userId}`;
|
|
146
|
+
if (this.pendingNameRequests.has(cacheKey)) return this.pendingNameRequests.get(cacheKey);
|
|
151
147
|
const cached = this.nameCache.get(cacheKey);
|
|
152
148
|
const CACHE_EXPIRATION = 24 * 60 * 60 * 1e3;
|
|
153
149
|
if (cached && Date.now() - cached.timestamp < CACHE_EXPIRATION) return;
|
|
150
|
+
const promise = this.fetchAndUpdateNames(session, effectiveId, cacheKey);
|
|
151
|
+
this.pendingNameRequests.set(cacheKey, promise);
|
|
154
152
|
try {
|
|
153
|
+
await promise;
|
|
154
|
+
} finally {
|
|
155
|
+
this.pendingNameRequests.delete(cacheKey);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @private
|
|
160
|
+
* 封装了实际获取和更新名称的异步逻辑。
|
|
161
|
+
*/
|
|
162
|
+
async fetchAndUpdateNames(session, effectiveId, cacheKey) {
|
|
163
|
+
try {
|
|
164
|
+
const { userId, guildId, bot } = session;
|
|
155
165
|
const [guild, member] = await Promise.all([
|
|
156
|
-
bot.getGuild(guildId),
|
|
157
|
-
bot.getGuildMember(guildId, userId)
|
|
166
|
+
guildId ? bot.getGuild(guildId).catch(() => null) : Promise.resolve(null),
|
|
167
|
+
guildId && userId ? bot.getGuildMember(guildId, userId).catch(() => null) : Promise.resolve(null)
|
|
158
168
|
]);
|
|
159
169
|
const channelName = guild?.name;
|
|
160
170
|
const userName = member?.nick || member?.name;
|
|
161
|
-
if (!channelName || !userName)
|
|
171
|
+
if (!channelName || !userName) {
|
|
172
|
+
this.nameCache.set(cacheKey, { name: null, timestamp: Date.now() });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
162
175
|
await this.ctx.database.upsert("analyse_name", [{
|
|
163
176
|
channelId: effectiveId,
|
|
164
|
-
channelName,
|
|
165
177
|
userId,
|
|
178
|
+
channelName,
|
|
166
179
|
userName
|
|
167
180
|
}]);
|
|
168
181
|
this.nameCache.set(cacheKey, { name: userName, timestamp: Date.now() });
|
|
169
182
|
} catch (error) {
|
|
170
|
-
this.
|
|
183
|
+
this.nameCache.set(cacheKey, { name: null, timestamp: Date.now() });
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
186
|
};
|
|
@@ -331,7 +344,7 @@ var CmdStat = class {
|
|
|
331
344
|
}, { primary: ["channelId", "userId", "command"] });
|
|
332
345
|
this.ctx.on("command/before-execute", async ({ command, session }) => {
|
|
333
346
|
const { userId, guildId } = session;
|
|
334
|
-
if (!guildId || !userId
|
|
347
|
+
if (!guildId || !userId) return;
|
|
335
348
|
const commandName = command.name;
|
|
336
349
|
const query = { channelId: guildId, userId, command: commandName };
|
|
337
350
|
await this.ctx.database.upsert("analyse_cmd", (row) => [{
|
|
@@ -398,25 +411,25 @@ var CmdStat = class {
|
|
|
398
411
|
const query = {};
|
|
399
412
|
if (guildId) query.channelId = guildId;
|
|
400
413
|
if (userId) query.userId = userId;
|
|
401
|
-
const records = await this.ctx.database.get("analyse_cmd", query);
|
|
402
|
-
if (records.length === 0) return "暂无统计数据";
|
|
403
|
-
const totalCount = records.reduce((sum, record) => sum + record.count, 0);
|
|
404
|
-
let sortedList;
|
|
405
414
|
if (userId) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (record.timestamp > existing.lastUsed) {
|
|
413
|
-
existing.lastUsed = record.timestamp;
|
|
414
|
-
}
|
|
415
|
-
commandMap.set(record.command, existing);
|
|
416
|
-
}
|
|
417
|
-
sortedList = Array.from(commandMap.entries()).map(([name2, data]) => ({ name: name2, ...data })).sort((a, b) => b.count - a.count);
|
|
415
|
+
const records = await this.ctx.database.get("analyse_cmd", query);
|
|
416
|
+
if (records.length === 0) return "暂无统计数据";
|
|
417
|
+
const totalCount2 = records.reduce((sum, record) => sum + record.count, 0);
|
|
418
|
+
const sortedList2 = records.map((r) => ({ name: r.command, count: r.count, lastUsed: r.timestamp })).sort((a, b) => b.count - a.count);
|
|
419
|
+
const list2 = sortedList2.map((item) => [item.name, item.count, item.lastUsed]);
|
|
420
|
+
return { list: list2, total: totalCount2 };
|
|
418
421
|
}
|
|
419
|
-
const
|
|
422
|
+
const aggregatedStats = await this.ctx.database.select("analyse_cmd", query).groupBy(
|
|
423
|
+
["command"],
|
|
424
|
+
{
|
|
425
|
+
count: /* @__PURE__ */ __name((row) => import_koishi2.$.sum(row.count), "count"),
|
|
426
|
+
lastUsed: /* @__PURE__ */ __name((row) => import_koishi2.$.max(row.timestamp), "lastUsed")
|
|
427
|
+
}
|
|
428
|
+
).execute();
|
|
429
|
+
if (aggregatedStats.length === 0) return "暂无统计数据";
|
|
430
|
+
const totalCount = aggregatedStats.reduce((sum, record) => sum + record.count, 0);
|
|
431
|
+
const sortedList = aggregatedStats.sort((a, b) => b.count - a.count);
|
|
432
|
+
const list = sortedList.map((item) => [item.command, item.count, item.lastUsed]);
|
|
420
433
|
return { list, total: totalCount };
|
|
421
434
|
}
|
|
422
435
|
};
|