koishi-plugin-chat-analyse 0.3.1 → 0.3.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 +12 -13
- package/lib/index.js +90 -56
- package/package.json +1 -1
package/lib/Collector.d.ts
CHANGED
|
@@ -41,12 +41,11 @@ export declare class Collector {
|
|
|
41
41
|
private static readonly FLUSH_INTERVAL;
|
|
42
42
|
/** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
|
|
43
43
|
private static readonly BUFFER_THRESHOLD;
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
private pendingUidRequests;
|
|
44
|
+
private msgStatBuffer;
|
|
45
|
+
private cmdStatBuffer;
|
|
46
|
+
private oriCacheBuffer;
|
|
47
|
+
private userCache;
|
|
48
|
+
private pendingUserRequests;
|
|
50
49
|
private flushInterval;
|
|
51
50
|
/**
|
|
52
51
|
* @constructor
|
|
@@ -71,13 +70,13 @@ export declare class Collector {
|
|
|
71
70
|
/**
|
|
72
71
|
* @private
|
|
73
72
|
* @async
|
|
74
|
-
* @method
|
|
75
|
-
* @description
|
|
73
|
+
* @method getOrCreateCachedUser
|
|
74
|
+
* @description 高效地获取或创建用户的中央记录,并全面利用缓存。
|
|
76
75
|
* @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
|
|
77
76
|
* @param {string} channelId - 消息所在的频道或群组 ID。
|
|
78
|
-
* @returns {Promise<
|
|
77
|
+
* @returns {Promise<UserCache | null>} 返回用户的缓存对象,如果操作失败则返回 `null`。
|
|
79
78
|
*/
|
|
80
|
-
private
|
|
79
|
+
private getOrCreateCachedUser;
|
|
81
80
|
/**
|
|
82
81
|
* @private
|
|
83
82
|
* @method sanitizeContent
|
|
@@ -89,8 +88,8 @@ export declare class Collector {
|
|
|
89
88
|
/**
|
|
90
89
|
* @private
|
|
91
90
|
* @async
|
|
92
|
-
* @method
|
|
93
|
-
* @description
|
|
91
|
+
* @method flushBuffers
|
|
92
|
+
* @description 将所有内存中的缓冲区数据批量写入数据库。
|
|
94
93
|
*/
|
|
95
|
-
private
|
|
94
|
+
private flushBuffers;
|
|
96
95
|
}
|
package/lib/index.js
CHANGED
|
@@ -42,13 +42,11 @@ var Collector = class _Collector {
|
|
|
42
42
|
this.config = config;
|
|
43
43
|
this.defineModels();
|
|
44
44
|
ctx.on("message", (session) => this.handleMessage(session));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
}
|
|
45
|
+
this.flushInterval = setInterval(() => this.flushBuffers(), _Collector.FLUSH_INTERVAL);
|
|
46
|
+
ctx.on("dispose", () => {
|
|
47
|
+
clearInterval(this.flushInterval);
|
|
48
|
+
this.flushBuffers();
|
|
49
|
+
});
|
|
52
50
|
}
|
|
53
51
|
static {
|
|
54
52
|
__name(this, "Collector");
|
|
@@ -57,12 +55,13 @@ var Collector = class _Collector {
|
|
|
57
55
|
static FLUSH_INTERVAL = 60 * 1e3;
|
|
58
56
|
/** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
|
|
59
57
|
static BUFFER_THRESHOLD = 100;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
// 统一的缓冲区
|
|
59
|
+
msgStatBuffer = /* @__PURE__ */ new Map();
|
|
60
|
+
cmdStatBuffer = /* @__PURE__ */ new Map();
|
|
61
|
+
oriCacheBuffer = [];
|
|
62
|
+
// 用户缓存
|
|
63
|
+
userCache = /* @__PURE__ */ new Map();
|
|
64
|
+
pendingUserRequests = /* @__PURE__ */ new Map();
|
|
66
65
|
flushInterval;
|
|
67
66
|
/**
|
|
68
67
|
* @private
|
|
@@ -111,35 +110,39 @@ var Collector = class _Collector {
|
|
|
111
110
|
const { userId, channelId, guildId, content, timestamp, argv, elements } = session;
|
|
112
111
|
const effectiveId = channelId || guildId;
|
|
113
112
|
if (!effectiveId || !userId || !timestamp || !content?.trim()) return;
|
|
114
|
-
const
|
|
115
|
-
if (!
|
|
113
|
+
const user = await this.getOrCreateCachedUser(session, effectiveId);
|
|
114
|
+
if (!user) return;
|
|
115
|
+
const { uid } = user;
|
|
116
|
+
const messageTime = new Date(timestamp);
|
|
116
117
|
if (argv?.command) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
timestamp
|
|
122
|
-
}
|
|
118
|
+
const key = `${uid}:${argv.command.name}`;
|
|
119
|
+
const existing = this.cmdStatBuffer.get(key);
|
|
120
|
+
if (existing) {
|
|
121
|
+
existing.count++;
|
|
122
|
+
existing.timestamp = messageTime;
|
|
123
|
+
} else {
|
|
124
|
+
this.cmdStatBuffer.set(key, { uid, command: argv.command.name, count: 1, timestamp: messageTime });
|
|
125
|
+
}
|
|
123
126
|
}
|
|
124
|
-
const messageTime = new Date(timestamp);
|
|
125
127
|
const hourStart = new Date(messageTime.getFullYear(), messageTime.getMonth(), messageTime.getDate(), messageTime.getHours());
|
|
126
128
|
const uniqueElementTypes = new Set(elements.map((e) => e.type));
|
|
127
129
|
for (const type of uniqueElementTypes) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const key = `${uid}:${type}:${hourStart.toISOString()}`;
|
|
131
|
+
const existing = this.msgStatBuffer.get(key);
|
|
132
|
+
if (existing) {
|
|
133
|
+
existing.count++;
|
|
134
|
+
existing.timestamp = messageTime;
|
|
135
|
+
} else {
|
|
136
|
+
this.msgStatBuffer.set(key, { uid, type, hour: hourStart, count: 1, timestamp: messageTime });
|
|
137
|
+
}
|
|
135
138
|
}
|
|
136
139
|
if (this.config.enableOriRecord) {
|
|
137
|
-
this.
|
|
140
|
+
this.oriCacheBuffer.push({
|
|
138
141
|
uid,
|
|
139
142
|
content: this.sanitizeContent(elements),
|
|
140
|
-
timestamp:
|
|
143
|
+
timestamp: messageTime
|
|
141
144
|
});
|
|
142
|
-
if (this.
|
|
145
|
+
if (this.oriCacheBuffer.length >= _Collector.BUFFER_THRESHOLD) await this.flushBuffers();
|
|
143
146
|
}
|
|
144
147
|
} catch (error) {
|
|
145
148
|
this.ctx.logger.warn("消息处理出错:", error);
|
|
@@ -148,45 +151,50 @@ var Collector = class _Collector {
|
|
|
148
151
|
/**
|
|
149
152
|
* @private
|
|
150
153
|
* @async
|
|
151
|
-
* @method
|
|
152
|
-
* @description
|
|
154
|
+
* @method getOrCreateCachedUser
|
|
155
|
+
* @description 高效地获取或创建用户的中央记录,并全面利用缓存。
|
|
153
156
|
* @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
|
|
154
157
|
* @param {string} channelId - 消息所在的频道或群组 ID。
|
|
155
|
-
* @returns {Promise<
|
|
158
|
+
* @returns {Promise<UserCache | null>} 返回用户的缓存对象,如果操作失败则返回 `null`。
|
|
156
159
|
*/
|
|
157
|
-
async
|
|
160
|
+
async getOrCreateCachedUser(session, channelId) {
|
|
158
161
|
const { userId, bot, guildId } = session;
|
|
159
162
|
const cacheKey = `${channelId}:${userId}`;
|
|
160
|
-
if (this.
|
|
161
|
-
if (this.
|
|
163
|
+
if (this.userCache.has(cacheKey)) return this.userCache.get(cacheKey);
|
|
164
|
+
if (this.pendingUserRequests.has(cacheKey)) return this.pendingUserRequests.get(cacheKey);
|
|
162
165
|
const promise = (async () => {
|
|
163
166
|
try {
|
|
164
|
-
const existing = await this.ctx.database.get("analyse_user", { channelId, userId }
|
|
167
|
+
const existing = await this.ctx.database.get("analyse_user", { channelId, userId });
|
|
165
168
|
if (existing.length > 0) {
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
const { uid: uid2, userName: userName2, channelName: channelName2 } = existing[0];
|
|
170
|
+
const cachedUser2 = { uid: uid2, userName: userName2, channelName: channelName2 };
|
|
171
|
+
this.userCache.set(cacheKey, cachedUser2);
|
|
172
|
+
return cachedUser2;
|
|
168
173
|
}
|
|
169
174
|
const [guild, member] = await Promise.all([
|
|
170
175
|
guildId ? bot.getGuild(guildId).catch(() => null) : Promise.resolve(null),
|
|
171
176
|
guildId ? bot.getGuildMember(guildId, userId).catch(() => null) : Promise.resolve(null)
|
|
172
177
|
]);
|
|
173
178
|
const user = !member ? await bot.getUser(userId).catch(() => null) : null;
|
|
174
|
-
const
|
|
179
|
+
const newUserRecord = {
|
|
175
180
|
channelId,
|
|
176
181
|
userId,
|
|
177
182
|
channelName: guild?.name || channelId,
|
|
178
183
|
userName: member?.nick || member?.name || user?.name || userId
|
|
179
|
-
}
|
|
180
|
-
this.
|
|
181
|
-
|
|
184
|
+
};
|
|
185
|
+
const createdUser = await this.ctx.database.create("analyse_user", newUserRecord);
|
|
186
|
+
const { uid, userName, channelName } = createdUser;
|
|
187
|
+
const cachedUser = { uid, userName, channelName };
|
|
188
|
+
this.userCache.set(cacheKey, cachedUser);
|
|
189
|
+
return cachedUser;
|
|
182
190
|
} catch (error) {
|
|
183
|
-
this.ctx.logger.error(`创建或获取用户(${cacheKey})
|
|
191
|
+
this.ctx.logger.error(`创建或获取用户(${cacheKey})失败:`, error);
|
|
184
192
|
return null;
|
|
185
193
|
} finally {
|
|
186
|
-
this.
|
|
194
|
+
this.pendingUserRequests.delete(cacheKey);
|
|
187
195
|
}
|
|
188
196
|
})();
|
|
189
|
-
this.
|
|
197
|
+
this.pendingUserRequests.set(cacheKey, promise);
|
|
190
198
|
return promise;
|
|
191
199
|
}
|
|
192
200
|
/**
|
|
@@ -211,17 +219,43 @@ var Collector = class _Collector {
|
|
|
211
219
|
/**
|
|
212
220
|
* @private
|
|
213
221
|
* @async
|
|
214
|
-
* @method
|
|
215
|
-
* @description
|
|
222
|
+
* @method flushBuffers
|
|
223
|
+
* @description 将所有内存中的缓冲区数据批量写入数据库。
|
|
216
224
|
*/
|
|
217
|
-
async
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
225
|
+
async flushBuffers() {
|
|
226
|
+
const cmdBufferToFlush = Array.from(this.cmdStatBuffer.values());
|
|
227
|
+
const msgBufferToFlush = Array.from(this.msgStatBuffer.values());
|
|
228
|
+
const advancedBufferToFlush = this.oriCacheBuffer;
|
|
229
|
+
this.cmdStatBuffer.clear();
|
|
230
|
+
this.msgStatBuffer.clear();
|
|
231
|
+
this.oriCacheBuffer = [];
|
|
221
232
|
try {
|
|
222
|
-
|
|
233
|
+
if (cmdBufferToFlush.length > 0) {
|
|
234
|
+
await this.ctx.database.upsert(
|
|
235
|
+
"analyse_cmd",
|
|
236
|
+
(row) => cmdBufferToFlush.map((item) => ({
|
|
237
|
+
uid: item.uid,
|
|
238
|
+
command: item.command,
|
|
239
|
+
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), item.count),
|
|
240
|
+
timestamp: item.timestamp
|
|
241
|
+
}))
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (msgBufferToFlush.length > 0) {
|
|
245
|
+
await this.ctx.database.upsert(
|
|
246
|
+
"analyse_msg",
|
|
247
|
+
(row) => msgBufferToFlush.map((item) => ({
|
|
248
|
+
uid: item.uid,
|
|
249
|
+
type: item.type,
|
|
250
|
+
hour: item.hour,
|
|
251
|
+
count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), item.count),
|
|
252
|
+
timestamp: item.timestamp
|
|
253
|
+
}))
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (advancedBufferToFlush.length > 0) await this.ctx.database.upsert("analyse_cache", advancedBufferToFlush);
|
|
223
257
|
} catch (error) {
|
|
224
|
-
this.ctx.logger.error("
|
|
258
|
+
this.ctx.logger.error("写入数据出错:", error);
|
|
225
259
|
}
|
|
226
260
|
}
|
|
227
261
|
};
|