koishi-plugin-chat-analyse 0.3.6 → 0.4.1

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.
@@ -18,6 +18,11 @@ declare module 'koishi' {
18
18
  analyse_msg: {
19
19
  uid: number;
20
20
  type: string;
21
+ count: number;
22
+ timestamp: Date;
23
+ };
24
+ analyse_rank: {
25
+ uid: number;
21
26
  hour: Date;
22
27
  count: number;
23
28
  timestamp: Date;
@@ -28,6 +33,12 @@ declare module 'koishi' {
28
33
  content: string;
29
34
  timestamp: Date;
30
35
  };
36
+ analyse_at: {
37
+ uid: number;
38
+ target: string;
39
+ content: string;
40
+ timestamp: Date;
41
+ };
31
42
  }
32
43
  }
33
44
  /**
@@ -42,8 +53,10 @@ export declare class Collector {
42
53
  /** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
43
54
  private static readonly BUFFER_THRESHOLD;
44
55
  private msgStatBuffer;
56
+ private rankStatBuffer;
45
57
  private cmdStatBuffer;
46
58
  private oriCacheBuffer;
59
+ private whoAtBuffer;
47
60
  private userCache;
48
61
  private pendingUserRequests;
49
62
  private flushInterval;
package/lib/WhoAt.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Context, Command } from 'koishi';
2
+ import { Config } from './index';
3
+ /**
4
+ * @class WhoAt
5
+ * @description
6
+ * 负责处理与“谁@我”相关的功能。
7
+ * 该类会注册一个 'whoatme' 子命令,允许用户查询在何时被谁提及。
8
+ * 查询结果将以合并转发的形式发送给用户。
9
+ * 此外,该类还包含一个定时任务,用于定期清理数据库中旧的@记录。
10
+ */
11
+ export declare class WhoAt {
12
+ private ctx;
13
+ private config;
14
+ /**
15
+ * WhoAt 类的构造函数。
16
+ * @param {Context} ctx - Koishi 的插件上下文,用于访问框架核心功能和数据库等服务。
17
+ * @param {Config} config - 插件的配置对象,包含如记录保留天数等设置。
18
+ */
19
+ constructor(ctx: Context, config: Config);
20
+ /**
21
+ * @private
22
+ * @method setupCleanupTask
23
+ * @description 设置一个定时清理任务。
24
+ * 此任务会根据配置中的 `retentionDays` 定期删除过期的@记录,以防止数据库膨胀。
25
+ */
26
+ private setupCleanupTask;
27
+ /**
28
+ * @public
29
+ * @method registerCommand
30
+ * @description 在主 `analyse` 命令下注册 `whoatme` 子命令。
31
+ * @param {Command} analyse - 用户传入的主 `analyse` 命令实例,`whoatme` 将作为其子命令。
32
+ */
33
+ registerCommand(analyse: Command): void;
34
+ }
package/lib/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface Config {
16
16
  enableMsgStat: boolean;
17
17
  enableRankStat: boolean;
18
18
  enableOriRecord: boolean;
19
+ enableWhoAt: boolean;
20
+ retentionDays: number;
19
21
  }
20
22
  /**
21
23
  * @const {Schema<Config>} Config
package/lib/index.js CHANGED
@@ -27,7 +27,7 @@ __export(src_exports, {
27
27
  using: () => using
28
28
  });
29
29
  module.exports = __toCommonJS(src_exports);
30
- var import_koishi4 = require("koishi");
30
+ var import_koishi5 = require("koishi");
31
31
 
32
32
  // src/Collector.ts
33
33
  var import_koishi = require("koishi");
@@ -55,10 +55,12 @@ var Collector = class _Collector {
55
55
  static FLUSH_INTERVAL = 60 * 1e3;
56
56
  /** @const {number} BUFFER_THRESHOLD - 内存缓存区触发自动刷新的消息数量阈值。 */
57
57
  static BUFFER_THRESHOLD = 100;
58
- // 统一的缓冲区
58
+ // 分离的缓冲区
59
59
  msgStatBuffer = /* @__PURE__ */ new Map();
60
+ rankStatBuffer = /* @__PURE__ */ new Map();
60
61
  cmdStatBuffer = /* @__PURE__ */ new Map();
61
62
  oriCacheBuffer = [];
63
+ whoAtBuffer = [];
62
64
  // 用户缓存
63
65
  userCache = /* @__PURE__ */ new Map();
64
66
  pendingUserRequests = /* @__PURE__ */ new Map();
@@ -85,10 +87,15 @@ var Collector = class _Collector {
85
87
  this.ctx.model.extend("analyse_msg", {
86
88
  uid: "unsigned",
87
89
  type: "string",
90
+ count: "unsigned",
91
+ timestamp: "timestamp"
92
+ }, { primary: ["uid", "type"] });
93
+ this.ctx.model.extend("analyse_rank", {
94
+ uid: "unsigned",
88
95
  hour: "timestamp",
89
96
  count: "unsigned",
90
97
  timestamp: "timestamp"
91
- }, { primary: ["uid", "type", "hour"] });
98
+ }, { primary: ["uid", "hour"] });
92
99
  if (this.config.enableOriRecord) {
93
100
  this.ctx.model.extend("analyse_cache", {
94
101
  id: "unsigned",
@@ -97,6 +104,14 @@ var Collector = class _Collector {
97
104
  timestamp: "timestamp"
98
105
  }, { primary: "id", autoInc: true, indexes: ["uid", "timestamp"] });
99
106
  }
107
+ if (this.config.enableWhoAt) {
108
+ this.ctx.model.extend("analyse_at", {
109
+ uid: "unsigned",
110
+ target: "string",
111
+ content: "text",
112
+ timestamp: "timestamp"
113
+ }, { indexes: ["target", "uid"] });
114
+ }
100
115
  }
101
116
  /**
102
117
  * @private
@@ -125,15 +140,33 @@ var Collector = class _Collector {
125
140
  }
126
141
  }
127
142
  const hourStart = new Date(messageTime.getFullYear(), messageTime.getMonth(), messageTime.getDate(), messageTime.getHours());
143
+ const rankKey = `${uid}:${hourStart.toISOString()}`;
144
+ const existingRank = this.rankStatBuffer.get(rankKey);
145
+ if (existingRank) {
146
+ existingRank.count++;
147
+ existingRank.timestamp = messageTime;
148
+ } else {
149
+ this.rankStatBuffer.set(rankKey, { uid, hour: hourStart, count: 1, timestamp: messageTime });
150
+ }
128
151
  const uniqueElementTypes = new Set(elements.map((e) => e.type));
129
152
  for (const type of uniqueElementTypes) {
130
- const key = `${uid}:${type}:${hourStart.toISOString()}`;
153
+ const key = `${uid}:${type}`;
131
154
  const existing = this.msgStatBuffer.get(key);
132
155
  if (existing) {
133
156
  existing.count++;
134
157
  existing.timestamp = messageTime;
135
158
  } else {
136
- this.msgStatBuffer.set(key, { uid, type, hour: hourStart, count: 1, timestamp: messageTime });
159
+ this.msgStatBuffer.set(key, { uid, type, count: 1, timestamp: messageTime });
160
+ }
161
+ }
162
+ if (this.config.enableWhoAt) {
163
+ const atElements = elements.filter((e) => e.type === "at");
164
+ if (atElements.length > 0) {
165
+ const sanitizedContent = this.sanitizeContent(elements);
166
+ for (const atElement of atElements) {
167
+ const targetId = atElement.attrs.id;
168
+ if (targetId && targetId !== userId) this.whoAtBuffer.push({ uid, target: targetId, content: sanitizedContent, timestamp: messageTime });
169
+ }
137
170
  }
138
171
  }
139
172
  if (this.config.enableOriRecord) {
@@ -225,10 +258,14 @@ var Collector = class _Collector {
225
258
  async flushBuffers() {
226
259
  const cmdBufferToFlush = Array.from(this.cmdStatBuffer.values());
227
260
  const msgBufferToFlush = Array.from(this.msgStatBuffer.values());
228
- const advancedBufferToFlush = this.oriCacheBuffer;
261
+ const rankBufferToFlush = Array.from(this.rankStatBuffer.values());
262
+ const oriCacheBufferToFlush = this.oriCacheBuffer;
263
+ const whoAtBufferToFlush = this.whoAtBuffer;
229
264
  this.cmdStatBuffer.clear();
230
265
  this.msgStatBuffer.clear();
266
+ this.rankStatBuffer.clear();
231
267
  this.oriCacheBuffer = [];
268
+ this.whoAtBuffer = [];
232
269
  try {
233
270
  if (cmdBufferToFlush.length > 0) {
234
271
  await this.ctx.database.upsert(
@@ -247,13 +284,24 @@ var Collector = class _Collector {
247
284
  (row) => msgBufferToFlush.map((item) => ({
248
285
  uid: item.uid,
249
286
  type: item.type,
287
+ count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), item.count),
288
+ timestamp: item.timestamp
289
+ }))
290
+ );
291
+ }
292
+ if (rankBufferToFlush.length > 0) {
293
+ await this.ctx.database.upsert(
294
+ "analyse_rank",
295
+ (row) => rankBufferToFlush.map((item) => ({
296
+ uid: item.uid,
250
297
  hour: item.hour,
251
298
  count: import_koishi.$.add(import_koishi.$.ifNull(row.count, 0), item.count),
252
299
  timestamp: item.timestamp
253
300
  }))
254
301
  );
255
302
  }
256
- if (advancedBufferToFlush.length > 0) await this.ctx.database.upsert("analyse_cache", advancedBufferToFlush);
303
+ if (whoAtBufferToFlush.length > 0) await this.ctx.database.upsert("analyse_at", whoAtBufferToFlush);
304
+ if (oriCacheBufferToFlush.length > 0) await this.ctx.database.upsert("analyse_cache", oriCacheBufferToFlush);
257
305
  } catch (error) {
258
306
  this.ctx.logger.error("写入数据出错:", error);
259
307
  }
@@ -423,16 +471,16 @@ var Renderer = class {
423
471
  const { title, time, list } = data;
424
472
  if (!list?.length) return "暂无数据可供渲染";
425
473
  let totalValueForPercent = 0;
426
- const countHeaderIndex = headers?.findIndex((h2) => ["总计发言", "条数", "次数", "数量"].includes(h2));
474
+ const countHeaderIndex = headers?.findIndex((h3) => ["总计发言", "条数", "次数", "数量"].includes(h3));
427
475
  if (countHeaderIndex > -1) {
428
476
  totalValueForPercent = list.reduce((sum, row) => sum + (Number(row[countHeaderIndex]) || 0), 0);
429
477
  }
430
478
  const totalCount = data.total || totalValueForPercent;
431
- const tableHeadHtml = headers?.length > 0 ? `<thead><tr><th class="rank-cell">#</th>${headers.map((h2, i) => {
479
+ const tableHeadHtml = headers?.length > 0 ? `<thead><tr><th class="rank-cell">#</th>${headers.map((h3, i) => {
432
480
  const firstCell = list[0]?.[i];
433
- const isRightAlign = typeof firstCell === "number" || firstCell instanceof Date || h2.includes("占比");
481
+ const isRightAlign = typeof firstCell === "number" || firstCell instanceof Date || h3.includes("占比");
434
482
  const alignClass = isRightAlign ? "header-right-align" : "name-header";
435
- return `<th class="${alignClass}">${h2}</th>`;
483
+ return `<th class="${alignClass}">${h3}</th>`;
436
484
  }).join("")}</tr></thead>` : "";
437
485
  const tableRowsHtml = list.map((row, index) => {
438
486
  const rank = index + 1;
@@ -731,13 +779,13 @@ var Stat = class {
731
779
  if (usersInGuild.length === 0) return "暂无统计数据";
732
780
  const uids = usersInGuild.map((u) => u.uid);
733
781
  const userNameMap = new Map(usersInGuild.map((u) => [u.uid, u.userName]));
734
- const stats = await this.ctx.database.select("analyse_msg").where({ uid: { $in: uids }, hour: { $gte: since } }).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").limit(100).execute();
782
+ const stats = await this.ctx.database.select("analyse_rank").where({ uid: { $in: uids }, hour: { $gte: since } }).groupBy("uid", { count: /* @__PURE__ */ __name((row) => import_koishi3.$.sum(row.count), "count") }).orderBy("count", "desc").limit(100).execute();
735
783
  if (stats.length === 0) return "暂无统计数据";
736
784
  const total = stats.reduce((sum, record) => sum + record.count, 0);
737
785
  const list = stats.map((item) => [userNameMap.get(item.uid) || `UID ${item.uid}`, item.count]);
738
786
  return { list, total };
739
787
  } else {
740
- const msgStats = await this.ctx.database.select("analyse_msg").where({ hour: { $gte: since } }).project(["uid", "count"]).execute();
788
+ const msgStats = await this.ctx.database.select("analyse_rank").where({ hour: { $gte: since } }).project(["uid", "count"]).execute();
741
789
  if (msgStats.length === 0) return "暂无统计数据";
742
790
  const allUsers = await this.ctx.database.get("analyse_user", {}, ["uid", "userId", "userName"]);
743
791
  const uidToUserMap = new Map(allUsers.map((u) => [u.uid, { userId: u.userId, userName: u.userName }]));
@@ -758,6 +806,73 @@ var Stat = class {
758
806
  }
759
807
  };
760
808
 
809
+ // src/WhoAt.ts
810
+ var import_koishi4 = require("koishi");
811
+ var WhoAt = class {
812
+ /**
813
+ * WhoAt 类的构造函数。
814
+ * @param {Context} ctx - Koishi 的插件上下文,用于访问框架核心功能和数据库等服务。
815
+ * @param {Config} config - 插件的配置对象,包含如记录保留天数等设置。
816
+ */
817
+ constructor(ctx, config) {
818
+ this.ctx = ctx;
819
+ this.config = config;
820
+ this.setupCleanupTask();
821
+ }
822
+ static {
823
+ __name(this, "WhoAt");
824
+ }
825
+ /**
826
+ * @private
827
+ * @method setupCleanupTask
828
+ * @description 设置一个定时清理任务。
829
+ * 此任务会根据配置中的 `retentionDays` 定期删除过期的@记录,以防止数据库膨胀。
830
+ */
831
+ setupCleanupTask() {
832
+ if (this.config.retentionDays > 0) {
833
+ this.ctx.cron("0 0 * * *", async () => {
834
+ try {
835
+ const cutoffDate = new Date(Date.now() - this.config.retentionDays * import_koishi4.Time.day);
836
+ await this.ctx.database.remove("analyse_at", { timestamp: { $lt: cutoffDate } });
837
+ } catch (error) {
838
+ this.ctx.logger.error("清理 @ 历史记录出错:", error);
839
+ }
840
+ });
841
+ }
842
+ }
843
+ /**
844
+ * @public
845
+ * @method registerCommand
846
+ * @description 在主 `analyse` 命令下注册 `whoatme` 子命令。
847
+ * @param {Command} analyse - 用户传入的主 `analyse` 命令实例,`whoatme` 将作为其子命令。
848
+ */
849
+ registerCommand(analyse) {
850
+ analyse.subcommand("whoatme", "谁 @ 我").action(async ({ session }) => {
851
+ if (!session.userId) return "无法获取用户信息";
852
+ try {
853
+ const records = await this.ctx.database.select("analyse_at").where({ target: session.userId }).orderBy("timestamp", "asc").limit(100).execute();
854
+ if (records.length === 0) return "暂无 @ 记录";
855
+ const uids = [...new Set(records.map((r) => r.uid))];
856
+ const users = await this.ctx.database.select("analyse_user", { uid: { $in: uids } }).project(["uid", "userName", "userId"]).execute();
857
+ const userInfoMap = new Map(users.map((u) => [u.uid, { name: u.userName, id: u.userId }]));
858
+ const messageElements = records.map((record) => {
859
+ const senderInfo = userInfoMap.get(record.uid);
860
+ const userId = senderInfo?.id;
861
+ const authorElement = (0, import_koishi4.h)("author", { userId, name: userId });
862
+ const contentElement = import_koishi4.h.text(record.content);
863
+ return (0, import_koishi4.h)("message", {}, [authorElement, contentElement]);
864
+ });
865
+ if (messageElements.length === 0) return "暂无有效 @ 记录";
866
+ const forwardMessage = (0, import_koishi4.h)("message", { forward: true }, messageElements);
867
+ await session.send(forwardMessage);
868
+ } catch (error) {
869
+ this.ctx.logger.error("查询 @ 记录时失败:", error);
870
+ return "查询失败,请稍后再试";
871
+ }
872
+ });
873
+ }
874
+ };
875
+
761
876
  // src/index.ts
762
877
  var usage = `
763
878
  <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);">
@@ -772,22 +887,27 @@ var usage = `
772
887
  </div>
773
888
  `;
774
889
  var name = "chat-analyse";
775
- var using = ["database", "puppeteer"];
776
- var Config = import_koishi4.Schema.intersect([
777
- import_koishi4.Schema.object({
778
- enableListener: import_koishi4.Schema.boolean().default(true).description("启用消息监听"),
779
- enableOriRecord: import_koishi4.Schema.boolean().default(true).description("启用原始记录")
890
+ var using = ["database", "puppeteer", "cron"];
891
+ var Config = import_koishi5.Schema.intersect([
892
+ import_koishi5.Schema.object({
893
+ enableListener: import_koishi5.Schema.boolean().default(true).description("启用消息监听"),
894
+ enableOriRecord: import_koishi5.Schema.boolean().default(true).description("启用原始记录")
780
895
  }).description("监听配置"),
781
- import_koishi4.Schema.object({
782
- enableCmdStat: import_koishi4.Schema.boolean().default(true).description("启用命令统计"),
783
- enableMsgStat: import_koishi4.Schema.boolean().default(true).description("启用消息统计"),
784
- enableRankStat: import_koishi4.Schema.boolean().default(true).description("启用发言排行")
785
- }).description("命令配置")
896
+ import_koishi5.Schema.object({
897
+ enableCmdStat: import_koishi5.Schema.boolean().default(true).description("启用命令统计"),
898
+ enableMsgStat: import_koishi5.Schema.boolean().default(true).description("启用消息统计"),
899
+ enableRankStat: import_koishi5.Schema.boolean().default(true).description("启用发言排行")
900
+ }).description("命令配置"),
901
+ import_koishi5.Schema.object({
902
+ enableWhoAt: import_koishi5.Schema.boolean().default(true).description("启用 @ 记录"),
903
+ retentionDays: import_koishi5.Schema.number().min(0).default(7).description("保留天数")
904
+ }).description("@ 记录配置")
786
905
  ]);
787
906
  function apply(ctx, config) {
788
907
  if (config.enableListener) new Collector(ctx, config);
789
908
  const analyse = ctx.command("analyse", "聊天记录分析");
790
909
  new Stat(ctx, config).registerCommands(analyse);
910
+ if (config.enableWhoAt) new WhoAt(ctx, config).registerCommand(analyse);
791
911
  }
792
912
  __name(apply, "apply");
793
913
  // Annotate the CommonJS export names for ESM import in node:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-analyse",
3
3
  "description": "聊天记录分析",
4
- "version": "0.3.6",
4
+ "version": "0.4.1",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],