koishi-plugin-chat-analyse 0.3.0 → 0.3.2

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.
@@ -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
- /** @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;
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 getOrCreateUser
75
- * @description 高效地获取或创建用户的中央记录 (`analyse_user`)。
73
+ * @method getOrCreateCachedUser
74
+ * @description 高效地获取或创建用户的中央记录,并全面利用缓存。
76
75
  * @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
77
76
  * @param {string} channelId - 消息所在的频道或群组 ID。
78
- * @returns {Promise<number | null>} 返回用户的唯一 `uid`,如果操作失败则返回 `null`。
77
+ * @returns {Promise<UserCache | null>} 返回用户的缓存对象,如果操作失败则返回 `null`。
79
78
  */
80
- private getOrCreateUser;
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 flushCacheBuffer
93
- * @description 将内存中的消息缓存 (`cacheBuffer`) 批量写入数据库。
91
+ * @method flushBuffers
92
+ * @description 将所有内存中的缓冲区数据批量写入数据库。
94
93
  */
95
- private flushCacheBuffer;
94
+ private flushBuffers;
96
95
  }
package/lib/Stat.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare class Stat {
17
17
  constructor(ctx: Context, config: Config);
18
18
  /**
19
19
  * @method registerCommands
20
- * @description 根据插件配置,动态地将 `.command`, `.message`, `.rank` 子命令注册到主 `analyse` 命令下。
20
+ * @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
21
21
  * @param {Command} analyse - 主 `analyse` 命令实例。
22
22
  */
23
23
  registerCommands(analyse: Command): void;
@@ -28,7 +28,10 @@ export declare class Stat {
28
28
  * @description 通用的标题生成器。根据查询参数和类型选项动态生成易于理解的图片标题。
29
29
  * @param {string} [guildId] - (可选) 查询的群组 ID。
30
30
  * @param {string} [userId] - (可选) 查询的用户 ID。
31
- * @param {TitleOptions} options - 标题的配置选项。
31
+ * @param {object} options - 标题的配置选项。
32
+ * @param {'命令' | '消息' | '排行'} options.main - 标题主类型。
33
+ * @param {string} [options.subtype] - (可选) 消息类型的子类型。
34
+ * @param {number} [options.timeRange] - (可选) 排行的时间范围(小时)。
32
35
  * @returns {Promise<string>} 生成的标题字符串。
33
36
  */
34
37
  private generateTitle;
package/lib/index.d.ts CHANGED
@@ -14,7 +14,8 @@ export interface Config {
14
14
  enableListener: boolean;
15
15
  enableCmdStat: boolean;
16
16
  enableMsgStat: boolean;
17
- enableAdvanced: boolean;
17
+ enableRankStat: boolean;
18
+ enableOriRecord: boolean;
18
19
  }
19
20
  /**
20
21
  * @const {Schema<Config>} Config
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
- 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
- }
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
- /** @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();
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
@@ -90,7 +89,7 @@ var Collector = class _Collector {
90
89
  count: "unsigned",
91
90
  timestamp: "timestamp"
92
91
  }, { primary: ["uid", "type", "hour"] });
93
- if (this.config.enableAdvanced) {
92
+ if (this.config.enableOriRecord) {
94
93
  this.ctx.model.extend("analyse_cache", {
95
94
  id: "unsigned",
96
95
  uid: "unsigned",
@@ -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 uid = await this.getOrCreateUser(session, effectiveId);
115
- if (!uid) return;
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
- await this.ctx.database.upsert("analyse_cmd", (row) => [{
118
- uid,
119
- command: argv.command.name,
120
- count: import_koishi.$.add(import_koishi.$.ifNull(row.count, import_koishi.$.literal(0)), 1),
121
- timestamp: /* @__PURE__ */ new Date()
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
- await this.ctx.database.upsert("analyse_msg", (row) => [{
129
- uid,
130
- type,
131
- hour: hourStart,
132
- count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), 1),
133
- timestamp: messageTime
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
- if (this.config.enableAdvanced) {
137
- this.cacheBuffer.push({
139
+ if (this.config.enableOriRecord) {
140
+ this.oriCacheBuffer.push({
138
141
  uid,
139
142
  content: this.sanitizeContent(elements),
140
- timestamp: new Date(timestamp)
143
+ timestamp: messageTime
141
144
  });
142
- if (this.cacheBuffer.length >= _Collector.BUFFER_THRESHOLD) await this.flushCacheBuffer();
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 getOrCreateUser
152
- * @description 高效地获取或创建用户的中央记录 (`analyse_user`)。
154
+ * @method getOrCreateCachedUser
155
+ * @description 高效地获取或创建用户的中央记录,并全面利用缓存。
153
156
  * @param {Session} session - Koishi 会话对象,用于获取用户信息和 Bot 实例。
154
157
  * @param {string} channelId - 消息所在的频道或群组 ID。
155
- * @returns {Promise<number | null>} 返回用户的唯一 `uid`,如果操作失败则返回 `null`。
158
+ * @returns {Promise<UserCache | null>} 返回用户的缓存对象,如果操作失败则返回 `null`。
156
159
  */
157
- async getOrCreateUser(session, channelId) {
160
+ async getOrCreateCachedUser(session, channelId) {
158
161
  const { userId, bot, guildId } = session;
159
162
  const cacheKey = `${channelId}:${userId}`;
160
- if (this.uidCache.has(cacheKey)) return this.uidCache.get(cacheKey);
161
- if (this.pendingUidRequests.has(cacheKey)) return this.pendingUidRequests.get(cacheKey);
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 }, ["uid"]);
167
+ const existing = await this.ctx.database.get("analyse_user", { channelId, userId });
165
168
  if (existing.length > 0) {
166
- this.uidCache.set(cacheKey, existing[0].uid);
167
- return existing[0].uid;
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 newUser = await this.ctx.database.create("analyse_user", {
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.uidCache.set(cacheKey, newUser.uid);
181
- return newUser.uid;
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}) UID 失败:`, error);
191
+ this.ctx.logger.error(`创建或获取用户(${cacheKey})失败:`, error);
184
192
  return null;
185
193
  } finally {
186
- this.pendingUidRequests.delete(cacheKey);
194
+ this.pendingUserRequests.delete(cacheKey);
187
195
  }
188
196
  })();
189
- this.pendingUidRequests.set(cacheKey, promise);
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 flushCacheBuffer
215
- * @description 将内存中的消息缓存 (`cacheBuffer`) 批量写入数据库。
222
+ * @method flushBuffers
223
+ * @description 将所有内存中的缓冲区数据批量写入数据库。
216
224
  */
217
- async flushCacheBuffer() {
218
- if (this.cacheBuffer.length === 0) return;
219
- const bufferToFlush = this.cacheBuffer;
220
- this.cacheBuffer = [];
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
- await this.ctx.database.upsert("analyse_cache", bufferToFlush);
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.create("analyse_cache", advancedBufferToFlush);
223
257
  } catch (error) {
224
- this.ctx.logger.error("写入缓存出错:", error);
258
+ this.ctx.logger.error("写入数据出错:", error);
225
259
  }
226
260
  }
227
261
  };
@@ -360,12 +394,12 @@ var Stat = class {
360
394
  renderer;
361
395
  /**
362
396
  * @method registerCommands
363
- * @description 根据插件配置,动态地将 `.command`, `.message`, `.rank` 子命令注册到主 `analyse` 命令下。
397
+ * @description 根据插件配置,动态地将 `.cmd`, `.msg`, `.rank` 子命令注册到主 `analyse` 命令下。
364
398
  * @param {Command} analyse - 主 `analyse` 命令实例。
365
399
  */
366
400
  registerCommands(analyse) {
367
401
  if (this.config.enableCmdStat) {
368
- analyse.subcommand(".command", "命令使用统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").usage("查询用户或群组的命令使用统计,默认展示全局统计。").action(async ({ session, options }) => {
402
+ analyse.subcommand(".cmd", "命令使用统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").usage("查询用户或群组的命令使用统计,默认展示全局统计。").action(async ({ session, options }) => {
369
403
  const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
370
404
  let guildId = options.guild;
371
405
  if (!userId && !guildId && session.guildId) {
@@ -393,7 +427,7 @@ var Stat = class {
393
427
  });
394
428
  }
395
429
  if (this.config.enableMsgStat) {
396
- analyse.subcommand(".message", "消息发送统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("type", "-t <type:string> 指定类型").usage("查询用户或群组的消息发送统计,默认展示全局统计。").action(async ({ session, options }) => {
430
+ analyse.subcommand(".msg", "消息发送统计").option("user", "-u [user:user] 指定用户").option("guild", "-g [guildId:string] 指定群组").option("type", "-t <type:string> 指定类型").usage("查询用户或群组的消息发送统计,默认展示全局统计。").action(async ({ session, options }) => {
397
431
  const userId = options.user ? import_koishi3.h.select(options.user, "user")[0]?.attrs.id : void 0;
398
432
  let guildId = options.guild;
399
433
  if (!userId && !guildId && !options.type && session.guildId) {
@@ -434,7 +468,9 @@ var Stat = class {
434
468
  return "渲染消息统计图片失败";
435
469
  }
436
470
  });
437
- analyse.subcommand(".rank", "用户发言排行").option("guild", "-g [guildId:string] 指定群组").option("hours", "-h <hours:number> 查看过去 N 小时的排行", { fallback: 24 }).usage("查询用户或群组的用户发言排行,默认展示全局统计。").action(async ({ session, options }) => {
471
+ }
472
+ 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 }) => {
438
474
  let guildId = options.guild;
439
475
  if (!guildId && session.guildId) guildId = session.guildId;
440
476
  if (!guildId) return "请指定查询范围";
@@ -470,7 +506,10 @@ var Stat = class {
470
506
  * @description 通用的标题生成器。根据查询参数和类型选项动态生成易于理解的图片标题。
471
507
  * @param {string} [guildId] - (可选) 查询的群组 ID。
472
508
  * @param {string} [userId] - (可选) 查询的用户 ID。
473
- * @param {TitleOptions} options - 标题的配置选项。
509
+ * @param {object} options - 标题的配置选项。
510
+ * @param {'命令' | '消息' | '排行'} options.main - 标题主类型。
511
+ * @param {string} [options.subtype] - (可选) 消息类型的子类型。
512
+ * @param {number} [options.timeRange] - (可选) 排行的时间范围(小时)。
474
513
  * @returns {Promise<string>} 生成的标题字符串。
475
514
  */
476
515
  async generateTitle(guildId, userId, options) {
@@ -635,13 +674,14 @@ var name = "chat-analyse";
635
674
  var using = ["database", "puppeteer"];
636
675
  var Config = import_koishi4.Schema.intersect([
637
676
  import_koishi4.Schema.object({
638
- enableListener: import_koishi4.Schema.boolean().default(true).description("开启监听")
639
- }).description("基本设置"),
677
+ enableListener: import_koishi4.Schema.boolean().default(true).description("启用消息监听"),
678
+ enableOriRecord: import_koishi4.Schema.boolean().default(true).description("启用原始记录")
679
+ }).description("监听配置"),
640
680
  import_koishi4.Schema.object({
641
681
  enableCmdStat: import_koishi4.Schema.boolean().default(true).description("启用命令统计"),
642
682
  enableMsgStat: import_koishi4.Schema.boolean().default(true).description("启用消息统计"),
643
- enableAdvanced: import_koishi4.Schema.boolean().default(true).description("启用原始记录")
644
- }).description("功能开关")
683
+ enableRankStat: import_koishi4.Schema.boolean().default(true).description("启用发言排行")
684
+ }).description("命令配置")
645
685
  ]);
646
686
  function apply(ctx, config) {
647
687
  if (config.enableListener) new Collector(ctx, config);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],