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.
- package/lib/Collector.d.ts +13 -0
- package/lib/WhoAt.d.ts +34 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +143 -23
- package/package.json +1 -1
package/lib/Collector.d.ts
CHANGED
|
@@ -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
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
|
|
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", "
|
|
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}
|
|
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,
|
|
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
|
|
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 (
|
|
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((
|
|
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((
|
|
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 ||
|
|
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}">${
|
|
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("
|
|
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("
|
|
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 =
|
|
777
|
-
|
|
778
|
-
enableListener:
|
|
779
|
-
enableOriRecord:
|
|
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
|
-
|
|
782
|
-
enableCmdStat:
|
|
783
|
-
enableMsgStat:
|
|
784
|
-
enableRankStat:
|
|
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:
|