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.
@@ -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: ["channelId", "userId", "timestamp"],
48
- indexes: ["type"]
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.create("analyse_msg", bufferToFlush);
133
+ await this.ctx.database.upsert("analyse_msg", bufferToFlush);
139
134
  } catch (error) {
140
- this.ctx.logger.error("数据库写入失败:", 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, guildId, bot } = session;
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) return;
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.ctx.logger.warn("更新用户/群组名称失败:", error);
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 || command.name === "analyse" || command.parent?.name === "analyse") return;
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
- sortedList = records.map((r) => ({ name: r.command, count: r.count, lastUsed: r.timestamp })).sort((a, b) => b.count - a.count);
407
- } else {
408
- const commandMap = /* @__PURE__ */ new Map();
409
- for (const record of records) {
410
- const existing = commandMap.get(record.command) || { count: 0, lastUsed: /* @__PURE__ */ new Date(0) };
411
- existing.count += record.count;
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 list = sortedList.map((item) => [item.name, item.count, item.lastUsed]);
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
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],