koishi-plugin-chat-patch 0.8.2 → 1.0.6
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/client/vue/index.vue +608 -22
- package/client/vue/style.css +355 -0
- package/dist/index.js +8 -8
- package/dist/style.css +1 -1
- package/lib/api-handlers.d.ts +17 -0
- package/lib/config.d.ts +13 -0
- package/lib/file-manager.d.ts +17 -0
- package/lib/index.d.ts +4 -13
- package/lib/index.js +857 -454
- package/lib/message-handler.d.ts +16 -0
- package/lib/types.d.ts +53 -0
- package/lib/utils.d.ts +7 -0
- package/package.json +3 -3
- package/src/api-handlers.ts +593 -0
- package/src/config.ts +45 -0
- package/src/file-manager.ts +162 -0
- package/src/index.ts +45 -753
- package/src/message-handler.ts +247 -0
- package/src/types.ts +58 -0
- package/src/utils.ts +43 -0
package/lib/index.js
CHANGED
|
@@ -1,480 +1,247 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
2
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
var name = "chat-patch";
|
|
9
|
-
var reusable = false;
|
|
10
|
-
var filter = false;
|
|
11
|
-
var inject = {
|
|
12
|
-
required: ["console"]
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
13
11
|
};
|
|
14
|
-
var
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Config: () => Config,
|
|
34
|
+
apply: () => apply,
|
|
35
|
+
filter: () => filter,
|
|
36
|
+
inject: () => inject,
|
|
37
|
+
name: () => name,
|
|
38
|
+
reusable: () => reusable,
|
|
39
|
+
usage: () => usage
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})).role("table").description("屏蔽的平台列表").default([
|
|
31
|
-
{
|
|
32
|
-
"platformName": "qq",
|
|
33
|
-
"exactMatch": true
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"platformName": "qqguild",
|
|
37
|
-
"exactMatch": true
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"platformName": "sandbox",
|
|
41
|
-
"exactMatch": false
|
|
42
|
-
}
|
|
43
|
-
])
|
|
44
|
-
}).description("基础设置"),
|
|
45
|
-
Schema.object({
|
|
46
|
-
chatContainerHeight: Schema.number().default(80).description("手机端使用的视口高度(防止文本输入框被挡住)").min(50).max(100),
|
|
47
|
-
loggerinfo: Schema.boolean().default(false).description("日志调试模式").experimental()
|
|
48
|
-
}).description("进阶设置")
|
|
49
|
-
]);
|
|
50
|
-
async function apply(ctx, config) {
|
|
51
|
-
const logger = ctx.logger("chat-patch");
|
|
52
|
-
const dataFilePath = path.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
|
|
53
|
-
const initialData = readChatDataFromFile();
|
|
54
|
-
const cleanedData = cleanExcessMessages(initialData);
|
|
55
|
-
const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
56
|
-
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
57
|
-
if (originalCount !== cleanedCount) {
|
|
58
|
-
writeChatDataToFile(cleanedData);
|
|
59
|
-
}
|
|
60
|
-
logInfo("插件加载完成,数据统计:", {
|
|
61
|
-
机器人数量: Object.keys(cleanedData.bots).length,
|
|
62
|
-
频道数量: Object.keys(cleanedData.channels).reduce((total, botId) => total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
|
|
63
|
-
消息频道数: Object.keys(cleanedData.messages).length,
|
|
64
|
-
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
65
|
-
});
|
|
66
|
-
ctx.on("message", async (session) => {
|
|
67
|
-
if (isPlatformBlocked(session.platform || "unknown")) {
|
|
68
|
-
logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
await broadcastMessageEvent(session);
|
|
72
|
-
});
|
|
73
|
-
ctx.on("ready", async () => {
|
|
74
|
-
logInfo("插件启动完成,开始监听消息");
|
|
75
|
-
setInterval(() => {
|
|
76
|
-
const data = readChatDataFromFile();
|
|
77
|
-
const cleanedData2 = cleanExcessMessages(data);
|
|
78
|
-
const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
79
|
-
const cleanedCount2 = Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
80
|
-
if (originalCount2 !== cleanedCount2) {
|
|
81
|
-
writeChatDataToFile(cleanedData2);
|
|
82
|
-
logInfo("定期清理完成,清理了", originalCount2 - cleanedCount2, "条超量消息");
|
|
83
|
-
}
|
|
84
|
-
}, 3e5);
|
|
85
|
-
});
|
|
86
|
-
ctx.console.addListener("get-chat-data", async () => {
|
|
87
|
-
try {
|
|
88
|
-
const data = readChatDataFromFile();
|
|
89
|
-
const cleanedData2 = cleanExcessMessages(data);
|
|
90
|
-
logInfo("获取聊天数据:", {
|
|
91
|
-
机器人数量: Object.keys(cleanedData2.bots).length,
|
|
92
|
-
频道数量: Object.keys(cleanedData2.channels).reduce((total, botId) => total + Object.keys(cleanedData2.channels[botId] || {}).length, 0),
|
|
93
|
-
消息频道数: Object.keys(cleanedData2.messages).length,
|
|
94
|
-
总消息数: Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
95
|
-
});
|
|
96
|
-
return {
|
|
97
|
-
success: true,
|
|
98
|
-
data: {
|
|
99
|
-
...cleanedData2,
|
|
100
|
-
pinnedBots: cleanedData2.pinnedBots,
|
|
101
|
-
// 包含置顶机器人
|
|
102
|
-
pinnedChannels: cleanedData2.pinnedChannels
|
|
103
|
-
// 包含置顶频道
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
logger.error("获取聊天数据失败:", error);
|
|
108
|
-
return { success: false, error: error?.message || String(error) };
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
ctx.console.addListener("get-history-messages", async (requestData) => {
|
|
112
|
-
try {
|
|
113
|
-
const data = readChatDataFromFile();
|
|
114
|
-
const channelKey = `${requestData.selfId}-${requestData.channelId}`;
|
|
115
|
-
const messages = data.messages[channelKey] || [];
|
|
116
|
-
const sortedMessages = messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
117
|
-
logInfo("获取历史消息:", channelKey, "共", sortedMessages.length, "条消息");
|
|
118
|
-
return {
|
|
119
|
-
success: true,
|
|
120
|
-
messages: sortedMessages
|
|
121
|
-
};
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logger.error("获取历史消息失败:", error);
|
|
124
|
-
return { success: false, error: error?.message || String(error), messages: [] };
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
ctx.console.addListener("get-all-channel-message-counts", async () => {
|
|
128
|
-
try {
|
|
129
|
-
const data = readChatDataFromFile();
|
|
130
|
-
const counts = {};
|
|
131
|
-
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
132
|
-
counts[channelKey] = messages.length;
|
|
133
|
-
}
|
|
134
|
-
logInfo("获取所有频道消息数量:", {
|
|
135
|
-
频道数: Object.keys(counts).length,
|
|
136
|
-
总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
|
|
137
|
-
});
|
|
138
|
-
return {
|
|
139
|
-
success: true,
|
|
140
|
-
counts
|
|
141
|
-
};
|
|
142
|
-
} catch (error) {
|
|
143
|
-
logger.error("获取频道消息数量失败:", error);
|
|
144
|
-
return { success: false, error: error?.message || String(error), counts: {} };
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
ctx.console.addListener("fetch-image", async (data) => {
|
|
148
|
-
try {
|
|
149
|
-
const response = await fetch(data.url, {
|
|
150
|
-
headers: {
|
|
151
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
152
|
-
"Referer": ""
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
157
|
-
}
|
|
158
|
-
const buffer = await response.arrayBuffer();
|
|
159
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
160
|
-
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
161
|
-
return {
|
|
162
|
-
success: true,
|
|
163
|
-
base64,
|
|
164
|
-
contentType,
|
|
165
|
-
dataUrl: `data:${contentType};base64,${base64}`
|
|
166
|
-
};
|
|
167
|
-
} catch (error) {
|
|
168
|
-
return { success: false, error: error?.message || String(error) };
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
ctx.console.addListener("clear-channel-history", async (data) => {
|
|
172
|
-
try {
|
|
173
|
-
logInfo("收到清理历史记录请求:", data);
|
|
174
|
-
const chatData = readChatDataFromFile();
|
|
175
|
-
const channelKey = `${data.selfId}-${data.channelId}`;
|
|
176
|
-
if (!chatData.messages[channelKey]) {
|
|
177
|
-
return { success: true, message: "频道没有历史消息" };
|
|
178
|
-
}
|
|
179
|
-
const messages = chatData.messages[channelKey];
|
|
180
|
-
const originalCount2 = messages.length;
|
|
181
|
-
const keepCount = data.keepCount || config.keepMessagesOnClear;
|
|
182
|
-
if (keepCount > 0 && originalCount2 <= keepCount) {
|
|
183
|
-
return { success: true, message: `消息数量(${originalCount2})未超过保留数量(${keepCount}),无需清理` };
|
|
184
|
-
}
|
|
185
|
-
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
186
|
-
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : [];
|
|
187
|
-
const clearedCount = originalCount2 - keptMessages.length;
|
|
188
|
-
chatData.messages[channelKey] = keptMessages;
|
|
189
|
-
writeChatDataToFile(chatData);
|
|
190
|
-
logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
|
|
191
|
-
原始消息数: originalCount2,
|
|
192
|
-
保留消息数: keptMessages.length,
|
|
193
|
-
清理消息数: clearedCount
|
|
194
|
-
});
|
|
195
|
-
return {
|
|
196
|
-
success: true,
|
|
197
|
-
message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
|
|
198
|
-
clearedCount,
|
|
199
|
-
keptCount: keptMessages.length
|
|
200
|
-
};
|
|
201
|
-
} catch (error) {
|
|
202
|
-
logger.error("清理频道历史记录失败:", error);
|
|
203
|
-
return { success: false, error: error?.message || String(error) };
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
ctx.console.addListener("send-message", async (data) => {
|
|
207
|
-
try {
|
|
208
|
-
logInfo("收到发送消息请求:", data);
|
|
209
|
-
const bot = ctx.bots.find((bot2) => bot2.selfId === data.selfId);
|
|
210
|
-
if (!bot) {
|
|
211
|
-
logger.error("未找到机器人:", data.selfId);
|
|
212
|
-
return { success: false, error: "未找到指定的机器人" };
|
|
213
|
-
}
|
|
214
|
-
const result = await bot.sendMessage(data.channelId, data.content);
|
|
215
|
-
logInfo("消息发送成功:", result);
|
|
216
|
-
const timestamp = Date.now();
|
|
217
|
-
const chatData = readChatDataFromFile();
|
|
218
|
-
const botInfo = {
|
|
219
|
-
selfId: bot.selfId,
|
|
220
|
-
platform: bot.platform || "unknown",
|
|
221
|
-
username: bot.user?.name || `Bot-${data.selfId}`,
|
|
222
|
-
avatar: bot.user?.avatar,
|
|
223
|
-
status: "online"
|
|
224
|
-
};
|
|
225
|
-
chatData.bots[bot.selfId] = botInfo;
|
|
226
|
-
writeChatDataToFile(chatData);
|
|
227
|
-
const botMessageInfo = {
|
|
228
|
-
id: Array.isArray(result) ? result[0] : result || `bot-msg-${timestamp}`,
|
|
229
|
-
content: data.content,
|
|
230
|
-
userId: bot.selfId,
|
|
231
|
-
username: bot.user?.name || `Bot-${data.selfId}`,
|
|
232
|
-
avatar: bot.user?.avatar,
|
|
233
|
-
timestamp,
|
|
234
|
-
channelId: data.channelId,
|
|
235
|
-
selfId: data.selfId,
|
|
236
|
-
type: "bot",
|
|
237
|
-
platform: bot.platform || "unknown"
|
|
238
|
-
};
|
|
239
|
-
addMessageToFile(botMessageInfo);
|
|
240
|
-
const sentMessageEvent = {
|
|
241
|
-
type: "bot-message-sent",
|
|
242
|
-
selfId: data.selfId,
|
|
243
|
-
channelId: data.channelId,
|
|
244
|
-
messageId: Array.isArray(result) ? result[0] : result,
|
|
245
|
-
content: data.content,
|
|
246
|
-
timestamp,
|
|
247
|
-
botUsername: bot.user?.name || `Bot-${data.selfId}`,
|
|
248
|
-
botAvatar: bot.user?.avatar
|
|
249
|
-
};
|
|
250
|
-
ctx.console.broadcast("bot-message-sent-event", sentMessageEvent);
|
|
251
|
-
return { success: true, messageId: Array.isArray(result) ? result[0] : result };
|
|
252
|
-
} catch (error) {
|
|
253
|
-
logger.error("发送消息失败:", error);
|
|
254
|
-
return { success: false, error: error?.message || String(error) };
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
ctx.console.addListener("delete-bot-data", async (data) => {
|
|
258
|
-
try {
|
|
259
|
-
logInfo("收到删除机器人数据请求:", data);
|
|
260
|
-
const chatData = readChatDataFromFile();
|
|
261
|
-
let deletedChannels = 0;
|
|
262
|
-
let deletedMessages = 0;
|
|
263
|
-
if (chatData.channels[data.selfId]) {
|
|
264
|
-
deletedChannels = Object.keys(chatData.channels[data.selfId]).length;
|
|
265
|
-
delete chatData.channels[data.selfId];
|
|
266
|
-
}
|
|
267
|
-
const channelsToDelete = Object.keys(chatData.messages).filter((key) => key.startsWith(`${data.selfId}-`));
|
|
268
|
-
for (const channelKey of channelsToDelete) {
|
|
269
|
-
deletedMessages += chatData.messages[channelKey].length;
|
|
270
|
-
delete chatData.messages[channelKey];
|
|
271
|
-
}
|
|
272
|
-
delete chatData.bots[data.selfId];
|
|
273
|
-
writeChatDataToFile(chatData);
|
|
274
|
-
logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
|
|
275
|
-
删除频道数: deletedChannels,
|
|
276
|
-
删除消息数: deletedMessages
|
|
277
|
-
});
|
|
278
|
-
return {
|
|
279
|
-
success: true,
|
|
280
|
-
message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
|
|
281
|
-
deletedChannels,
|
|
282
|
-
deletedMessages
|
|
283
|
-
};
|
|
284
|
-
} catch (error) {
|
|
285
|
-
logger.error("删除机器人数据失败:", error);
|
|
286
|
-
return { success: false, error: error?.message || String(error) };
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
ctx.console.addListener("delete-channel-data", async (data) => {
|
|
290
|
-
try {
|
|
291
|
-
logInfo("收到删除频道数据请求:", data);
|
|
292
|
-
const chatData = readChatDataFromFile();
|
|
293
|
-
const channelKey = `${data.selfId}-${data.channelId}`;
|
|
294
|
-
let deletedMessages = 0;
|
|
295
|
-
if (chatData.messages[channelKey]) {
|
|
296
|
-
deletedMessages = chatData.messages[channelKey].length;
|
|
297
|
-
delete chatData.messages[channelKey];
|
|
298
|
-
}
|
|
299
|
-
if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
|
|
300
|
-
delete chatData.channels[data.selfId][data.channelId];
|
|
301
|
-
}
|
|
302
|
-
writeChatDataToFile(chatData);
|
|
303
|
-
logInfo(`频道 ${channelKey} 数据删除完成:`, {
|
|
304
|
-
删除消息数: deletedMessages
|
|
305
|
-
});
|
|
306
|
-
return {
|
|
307
|
-
success: true,
|
|
308
|
-
message: `成功删除频道数据:${deletedMessages} 条消息`,
|
|
309
|
-
deletedMessages
|
|
310
|
-
};
|
|
311
|
-
} catch (error) {
|
|
312
|
-
logger.error("删除频道数据失败:", error);
|
|
313
|
-
return { success: false, error: error?.message || String(error) };
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
ctx.console.addListener("set-pinned-bots", async (data) => {
|
|
317
|
-
try {
|
|
318
|
-
logInfo("收到设置置顶机器人请求:", data.pinnedBots);
|
|
319
|
-
const chatData = readChatDataFromFile();
|
|
320
|
-
chatData.pinnedBots = data.pinnedBots;
|
|
321
|
-
writeChatDataToFile(chatData);
|
|
322
|
-
return { success: true };
|
|
323
|
-
} catch (error) {
|
|
324
|
-
logger.error("设置置顶机器人失败:", error);
|
|
325
|
-
return { success: false, error: error?.message || String(error) };
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
ctx.console.addListener("set-pinned-channels", async (data) => {
|
|
329
|
-
try {
|
|
330
|
-
logInfo("收到设置置顶频道请求:", data.pinnedChannels);
|
|
331
|
-
const chatData = readChatDataFromFile();
|
|
332
|
-
chatData.pinnedChannels = data.pinnedChannels;
|
|
333
|
-
writeChatDataToFile(chatData);
|
|
334
|
-
return { success: true };
|
|
335
|
-
} catch (error) {
|
|
336
|
-
logger.error("设置置顶频道失败:", error);
|
|
337
|
-
return { success: false, error: error?.message || String(error) };
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
ctx.console.addListener("get-plugin-config", async () => {
|
|
341
|
-
try {
|
|
342
|
-
return {
|
|
343
|
-
success: true,
|
|
344
|
-
config: {
|
|
345
|
-
maxMessagesPerChannel: config.maxMessagesPerChannel,
|
|
346
|
-
keepMessagesOnClear: config.keepMessagesOnClear,
|
|
347
|
-
loggerinfo: config.loggerinfo,
|
|
348
|
-
blockedPlatforms: config.blockedPlatforms || [],
|
|
349
|
-
chatContainerHeight: config.chatContainerHeight
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
} catch (error) {
|
|
353
|
-
logger.error("获取插件配置失败:", error);
|
|
354
|
-
return { success: false, error: error?.message || String(error) };
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
ctx.console.addEntry({
|
|
358
|
-
dev: path.resolve(__dirname, "../client/index.ts"),
|
|
359
|
-
prod: path.resolve(__dirname, "../dist")
|
|
360
|
-
});
|
|
361
|
-
function logInfo(...args) {
|
|
362
|
-
if (config.loggerinfo) {
|
|
363
|
-
logger.info(...args);
|
|
364
|
-
}
|
|
44
|
+
// src/file-manager.ts
|
|
45
|
+
var import_node_path = __toESM(require("node:path"));
|
|
46
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
47
|
+
var FileManager = class {
|
|
48
|
+
constructor(ctx, config) {
|
|
49
|
+
this.ctx = ctx;
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.dataFilePath = import_node_path.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
|
|
52
|
+
this.logger = ctx.logger("chat-patch");
|
|
365
53
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!config.blockedPlatforms || config.blockedPlatforms.length === 0) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
for (const blockedPlatform of config.blockedPlatforms) {
|
|
372
|
-
if (blockedPlatform.exactMatch) {
|
|
373
|
-
if (platform === blockedPlatform.platformName) {
|
|
374
|
-
logInfo(`平台 ${platform} 被屏蔽 (完全匹配: ${blockedPlatform.platformName})`);
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
} else {
|
|
378
|
-
if (platform.includes(blockedPlatform.platformName)) {
|
|
379
|
-
logInfo(`平台 ${platform} 被屏蔽 (包含匹配: ${blockedPlatform.platformName})`);
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return false;
|
|
54
|
+
static {
|
|
55
|
+
__name(this, "FileManager");
|
|
385
56
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
57
|
+
dataFilePath;
|
|
58
|
+
fileOperationLock = Promise.resolve();
|
|
59
|
+
logger;
|
|
60
|
+
// 确保目录存在
|
|
61
|
+
ensureDataDir() {
|
|
62
|
+
const dir = import_node_path.default.dirname(this.dataFilePath);
|
|
63
|
+
if (!import_node_fs.default.existsSync(dir)) {
|
|
64
|
+
import_node_fs.default.mkdirSync(dir, { recursive: true });
|
|
391
65
|
}
|
|
392
66
|
}
|
|
393
|
-
|
|
394
|
-
|
|
67
|
+
// 从JSON文件读取数据
|
|
68
|
+
readChatDataFromFile() {
|
|
395
69
|
try {
|
|
396
|
-
if (
|
|
397
|
-
const jsonData =
|
|
70
|
+
if (import_node_fs.default.existsSync(this.dataFilePath)) {
|
|
71
|
+
const jsonData = import_node_fs.default.readFileSync(this.dataFilePath, "utf8");
|
|
398
72
|
const data = JSON.parse(jsonData);
|
|
399
73
|
return {
|
|
400
74
|
bots: data.bots || {},
|
|
401
75
|
channels: data.channels || {},
|
|
402
76
|
messages: data.messages || {},
|
|
403
77
|
pinnedBots: data.pinnedBots || [],
|
|
404
|
-
// 读取置顶机器人
|
|
405
78
|
pinnedChannels: data.pinnedChannels || [],
|
|
406
|
-
// 读取置顶频道
|
|
407
79
|
lastSaveTime: data.lastSaveTime
|
|
408
80
|
};
|
|
409
81
|
}
|
|
410
82
|
} catch (error) {
|
|
411
|
-
logger.error("读取聊天数据失败:", error);
|
|
83
|
+
this.logger.error("读取聊天数据失败:", error);
|
|
412
84
|
}
|
|
413
85
|
return {
|
|
414
86
|
bots: {},
|
|
415
87
|
channels: {},
|
|
416
88
|
messages: {},
|
|
417
89
|
pinnedBots: [],
|
|
418
|
-
// 默认空数组
|
|
419
90
|
pinnedChannels: []
|
|
420
|
-
// 默认空数组
|
|
421
91
|
};
|
|
422
92
|
}
|
|
423
|
-
|
|
424
|
-
|
|
93
|
+
// 写入数据到JSON文件
|
|
94
|
+
writeChatDataToFile(data) {
|
|
425
95
|
try {
|
|
426
|
-
ensureDataDir();
|
|
96
|
+
this.ensureDataDir();
|
|
427
97
|
data.lastSaveTime = Date.now();
|
|
428
98
|
const jsonData = JSON.stringify(data, null, 2);
|
|
429
|
-
|
|
99
|
+
import_node_fs.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
|
|
430
100
|
} catch (error) {
|
|
431
|
-
logger.error("写入聊天数据失败:", error);
|
|
101
|
+
this.logger.error("写入聊天数据失败:", error);
|
|
432
102
|
}
|
|
433
103
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
let
|
|
104
|
+
// 清理超量消息
|
|
105
|
+
cleanExcessMessages(data) {
|
|
106
|
+
let cleanedCount = 0;
|
|
437
107
|
const cleanedMessages = {};
|
|
438
108
|
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
439
|
-
if (messages.length > config.maxMessagesPerChannel) {
|
|
109
|
+
if (messages.length > this.config.maxMessagesPerChannel) {
|
|
440
110
|
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
441
|
-
const keptMessages = sortedMessages.slice(-config.maxMessagesPerChannel);
|
|
442
|
-
|
|
111
|
+
const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel);
|
|
112
|
+
cleanedCount += messages.length - keptMessages.length;
|
|
443
113
|
cleanedMessages[channelKey] = keptMessages;
|
|
444
|
-
logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
|
|
114
|
+
this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
|
|
445
115
|
} else {
|
|
446
116
|
cleanedMessages[channelKey] = messages;
|
|
447
117
|
}
|
|
448
118
|
}
|
|
449
|
-
if (
|
|
450
|
-
logInfo("总共清理超量消息:",
|
|
119
|
+
if (cleanedCount > 0) {
|
|
120
|
+
this.logInfo("总共清理超量消息:", cleanedCount, "条");
|
|
451
121
|
}
|
|
452
122
|
return {
|
|
453
123
|
...data,
|
|
454
124
|
messages: cleanedMessages
|
|
455
125
|
};
|
|
456
126
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
data.messages[channelKey]
|
|
127
|
+
// 添加消息到JSON文件(使用锁机制防止并发冲突)
|
|
128
|
+
async addMessageToFile(messageInfo) {
|
|
129
|
+
this.fileOperationLock = this.fileOperationLock.then(async () => {
|
|
130
|
+
const data = this.readChatDataFromFile();
|
|
131
|
+
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
132
|
+
if (!data.messages[channelKey]) {
|
|
133
|
+
data.messages[channelKey] = [];
|
|
134
|
+
}
|
|
135
|
+
const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
|
|
136
|
+
if (existingMessage) {
|
|
137
|
+
this.logInfo("消息已存在,跳过保存:", {
|
|
138
|
+
channelKey,
|
|
139
|
+
messageId: messageInfo.id,
|
|
140
|
+
existingType: existingMessage.type,
|
|
141
|
+
existingContent: existingMessage.content,
|
|
142
|
+
newType: messageInfo.type,
|
|
143
|
+
newContent: messageInfo.content
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!messageInfo.timestamp) {
|
|
148
|
+
messageInfo.timestamp = Date.now();
|
|
149
|
+
}
|
|
150
|
+
const beforeCount = data.messages[channelKey].length;
|
|
151
|
+
data.messages[channelKey].push(messageInfo);
|
|
152
|
+
const afterCount = data.messages[channelKey].length;
|
|
153
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
154
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
155
|
+
const removedCount = data.messages[channelKey].length - this.config.maxMessagesPerChannel;
|
|
156
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
157
|
+
this.logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`);
|
|
158
|
+
}
|
|
159
|
+
this.writeChatDataToFile(data);
|
|
160
|
+
const isCommandMessage = messageInfo.content?.startsWith("++") || messageInfo.content?.startsWith(".");
|
|
161
|
+
this.logInfo("添加消息到文件:", {
|
|
162
|
+
channelKey,
|
|
163
|
+
messageId: messageInfo.id,
|
|
164
|
+
content: messageInfo.content,
|
|
165
|
+
type: messageInfo.type,
|
|
166
|
+
userId: messageInfo.userId,
|
|
167
|
+
username: messageInfo.username,
|
|
168
|
+
timestamp: messageInfo.timestamp,
|
|
169
|
+
isCommandMessage,
|
|
170
|
+
消息数变化: `${beforeCount} -> ${afterCount} -> ${data.messages[channelKey].length}`
|
|
171
|
+
});
|
|
172
|
+
}).catch((error) => {
|
|
173
|
+
this.logger.error("保存消息时发生错误:", error);
|
|
174
|
+
});
|
|
175
|
+
await this.fileOperationLock;
|
|
176
|
+
}
|
|
177
|
+
logInfo(...args) {
|
|
178
|
+
if (this.config.loggerinfo) {
|
|
179
|
+
this.logger.info(...args);
|
|
463
180
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/utils.ts
|
|
185
|
+
var Utils = class {
|
|
186
|
+
constructor(config) {
|
|
187
|
+
this.config = config;
|
|
188
|
+
}
|
|
189
|
+
static {
|
|
190
|
+
__name(this, "Utils");
|
|
191
|
+
}
|
|
192
|
+
// 检查平台是否被屏蔽
|
|
193
|
+
isPlatformBlocked(platform) {
|
|
194
|
+
if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
for (const blockedPlatform of this.config.blockedPlatforms) {
|
|
198
|
+
if (blockedPlatform.exactMatch) {
|
|
199
|
+
if (platform === blockedPlatform.platformName) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
if (platform.includes(blockedPlatform.platformName)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
471
207
|
}
|
|
472
|
-
|
|
473
|
-
logInfo("添加消息到文件:", channelKey, "当前消息数:", data.messages[channelKey].length);
|
|
208
|
+
return false;
|
|
474
209
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
210
|
+
// 递归提取所有文本内容的函数
|
|
211
|
+
extractTextContent(elements) {
|
|
212
|
+
let text = "";
|
|
213
|
+
for (const element of elements) {
|
|
214
|
+
if (element.type === "text") {
|
|
215
|
+
text += element.attrs?.content || "";
|
|
216
|
+
} else if (element.type === "p") {
|
|
217
|
+
if (element.children && element.children.length > 0) {
|
|
218
|
+
text += this.extractTextContent(element.children) + "\n";
|
|
219
|
+
}
|
|
220
|
+
} else if (element.children && element.children.length > 0) {
|
|
221
|
+
text += this.extractTextContent(element.children);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return text;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/message-handler.ts
|
|
229
|
+
var MessageHandler = class {
|
|
230
|
+
constructor(ctx, config, fileManager) {
|
|
231
|
+
this.ctx = ctx;
|
|
232
|
+
this.config = config;
|
|
233
|
+
this.fileManager = fileManager;
|
|
234
|
+
this.logger = ctx.logger("chat-patch");
|
|
235
|
+
this.utils = new Utils(config);
|
|
236
|
+
}
|
|
237
|
+
static {
|
|
238
|
+
__name(this, "MessageHandler");
|
|
239
|
+
}
|
|
240
|
+
logger;
|
|
241
|
+
utils;
|
|
242
|
+
// 更新机器人信息到JSON文件
|
|
243
|
+
async updateBotInfoToFile(session) {
|
|
244
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
478
245
|
const botInfo = {
|
|
479
246
|
selfId: session.selfId,
|
|
480
247
|
platform: session.platform || "unknown",
|
|
@@ -483,20 +250,20 @@ async function apply(ctx, config) {
|
|
|
483
250
|
status: "online"
|
|
484
251
|
};
|
|
485
252
|
data.bots[session.selfId] = botInfo;
|
|
486
|
-
writeChatDataToFile(data);
|
|
487
|
-
logInfo("更新机器人信息到文件:", botInfo.username);
|
|
253
|
+
this.fileManager.writeChatDataToFile(data);
|
|
254
|
+
this.logInfo("更新机器人信息到文件:", botInfo.username);
|
|
488
255
|
}
|
|
489
|
-
|
|
490
|
-
async
|
|
491
|
-
const data = readChatDataFromFile();
|
|
256
|
+
// 更新频道信息到JSON文件
|
|
257
|
+
async updateChannelInfoToFile(session) {
|
|
492
258
|
let guildName = session.channelId;
|
|
259
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
493
260
|
try {
|
|
494
261
|
if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
|
|
495
262
|
const guild = await session.bot.getGuild(session.guildId);
|
|
496
263
|
guildName = guild?.name || session.channelId;
|
|
497
264
|
}
|
|
498
265
|
} catch (guildError) {
|
|
499
|
-
logInfo("获取群组信息失败,使用频道ID作为备用:", guildError);
|
|
266
|
+
this.logInfo("获取群组信息失败,使用频道ID作为备用:", guildError);
|
|
500
267
|
guildName = session.channelId;
|
|
501
268
|
}
|
|
502
269
|
if (!data.channels[session.selfId]) {
|
|
@@ -510,15 +277,14 @@ async function apply(ctx, config) {
|
|
|
510
277
|
guildName
|
|
511
278
|
};
|
|
512
279
|
data.channels[session.selfId][session.channelId] = channelInfo;
|
|
513
|
-
writeChatDataToFile(data);
|
|
514
|
-
logInfo("更新频道信息到文件:", channelInfo.name);
|
|
280
|
+
this.fileManager.writeChatDataToFile(data);
|
|
281
|
+
this.logInfo("更新频道信息到文件:", channelInfo.name);
|
|
515
282
|
return guildName;
|
|
516
283
|
}
|
|
517
|
-
|
|
518
|
-
async function broadcastMessageEvent(session) {
|
|
284
|
+
async broadcastMessageEvent(session) {
|
|
519
285
|
try {
|
|
520
|
-
updateBotInfoToFile(session);
|
|
521
|
-
const guildName = await updateChannelInfoToFile(session);
|
|
286
|
+
await this.updateBotInfoToFile(session);
|
|
287
|
+
const guildName = await this.updateChannelInfoToFile(session);
|
|
522
288
|
const timestamp = Date.now();
|
|
523
289
|
let quoteInfo = void 0;
|
|
524
290
|
if (session.quote) {
|
|
@@ -537,53 +303,690 @@ async function apply(ctx, config) {
|
|
|
537
303
|
timestamp: session.quote.timestamp || Date.now()
|
|
538
304
|
};
|
|
539
305
|
}
|
|
306
|
+
let content = "";
|
|
307
|
+
let elements = [];
|
|
308
|
+
if (session.content) {
|
|
309
|
+
content = session.content;
|
|
310
|
+
} else if (session.stripped?.content) {
|
|
311
|
+
content = session.stripped.content;
|
|
312
|
+
}
|
|
313
|
+
if (session.elements) {
|
|
314
|
+
elements = session.elements;
|
|
315
|
+
if (!content) {
|
|
316
|
+
content = session.elements.filter((element) => element.type === "text").map((element) => element.attrs?.content || "").join("");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
540
319
|
const messageInfo = {
|
|
541
|
-
id: session.
|
|
542
|
-
content: session.content || "",
|
|
543
|
-
userId: session.userId || "unknown",
|
|
544
|
-
username: session.username || session.userId || "unknown",
|
|
545
|
-
avatar: session.
|
|
320
|
+
id: session.event?.message?.id || `msg-${timestamp}`,
|
|
321
|
+
content: content || session.content || "",
|
|
322
|
+
userId: session.userId || session.event?.user?.id || "unknown",
|
|
323
|
+
username: session.username || session.event?.user?.name || session.userId || "unknown",
|
|
324
|
+
avatar: session.event?.user?.avatar,
|
|
546
325
|
timestamp,
|
|
547
326
|
channelId: session.channelId,
|
|
548
327
|
selfId: session.selfId,
|
|
549
|
-
elements
|
|
328
|
+
elements,
|
|
550
329
|
type: "user",
|
|
551
330
|
guildId: session.guildId,
|
|
552
331
|
guildName,
|
|
553
332
|
platform: session.platform || "unknown",
|
|
554
333
|
quote: quoteInfo
|
|
555
334
|
};
|
|
556
|
-
addMessageToFile(messageInfo);
|
|
335
|
+
await this.fileManager.addMessageToFile(messageInfo);
|
|
557
336
|
const messageEvent = {
|
|
558
337
|
type: "message",
|
|
559
338
|
selfId: session.selfId,
|
|
560
339
|
platform: session.platform || "unknown",
|
|
561
340
|
channelId: session.channelId,
|
|
562
|
-
messageId: session.
|
|
563
|
-
content: session.content,
|
|
564
|
-
userId: session.userId || "unknown",
|
|
565
|
-
username: session.username || session.userId || "unknown",
|
|
566
|
-
avatar: session.
|
|
341
|
+
messageId: session.event?.message?.id || `msg-${timestamp}`,
|
|
342
|
+
content: content || session.content || "",
|
|
343
|
+
userId: session.userId || session.event?.user?.id || "unknown",
|
|
344
|
+
username: session.username || session.event?.user?.name || session.userId || "unknown",
|
|
345
|
+
avatar: session.event?.user?.avatar,
|
|
567
346
|
timestamp,
|
|
568
347
|
guildId: session.guildId,
|
|
569
348
|
guildName,
|
|
570
349
|
channelType: session.type || 0,
|
|
571
|
-
elements
|
|
350
|
+
elements,
|
|
572
351
|
quote: quoteInfo,
|
|
573
352
|
bot: {
|
|
574
353
|
avatar: session.bot.user?.avatar,
|
|
575
354
|
name: session.bot.user?.name
|
|
576
355
|
}
|
|
577
356
|
};
|
|
578
|
-
ctx.console.broadcast("chat-message-event", messageEvent);
|
|
357
|
+
this.ctx.console.broadcast("chat-message-event", messageEvent);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.logger.error("广播消息事件失败:", error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// 处理机器人发送的消息
|
|
363
|
+
async broadcastBotMessageEvent(session) {
|
|
364
|
+
try {
|
|
365
|
+
await this.updateBotInfoToFile(session);
|
|
366
|
+
const guildName = await this.updateChannelInfoToFile(session);
|
|
367
|
+
const timestamp = Date.now();
|
|
368
|
+
let content = "";
|
|
369
|
+
if (session.event?.message?.elements) {
|
|
370
|
+
content = this.utils.extractTextContent(session.event.message.elements).trim();
|
|
371
|
+
}
|
|
372
|
+
const messageInfo = {
|
|
373
|
+
id: `bot-msg-${timestamp}`,
|
|
374
|
+
content,
|
|
375
|
+
userId: session.selfId,
|
|
376
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
377
|
+
avatar: session.bot.user?.avatar,
|
|
378
|
+
timestamp,
|
|
379
|
+
channelId: session.event?.channel?.id || session.channelId,
|
|
380
|
+
selfId: session.selfId,
|
|
381
|
+
elements: session.event?.message?.elements,
|
|
382
|
+
type: "bot",
|
|
383
|
+
guildId: session.event?.guild?.id || session.guildId,
|
|
384
|
+
guildName,
|
|
385
|
+
platform: session.platform || "unknown"
|
|
386
|
+
};
|
|
387
|
+
await this.fileManager.addMessageToFile(messageInfo);
|
|
388
|
+
const messageEvent = {
|
|
389
|
+
type: "bot-message",
|
|
390
|
+
selfId: session.selfId,
|
|
391
|
+
platform: session.platform || "unknown",
|
|
392
|
+
channelId: session.event?.channel?.id || session.channelId,
|
|
393
|
+
messageId: `bot-msg-${timestamp}`,
|
|
394
|
+
content,
|
|
395
|
+
userId: session.selfId,
|
|
396
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
397
|
+
avatar: session.bot.user?.avatar,
|
|
398
|
+
timestamp,
|
|
399
|
+
guildId: session.event?.guild?.id || session.guildId,
|
|
400
|
+
guildName,
|
|
401
|
+
channelType: session.event?.channel?.type || session.type || 0,
|
|
402
|
+
elements: session.event?.message?.elements,
|
|
403
|
+
bot: {
|
|
404
|
+
avatar: session.bot.user?.avatar,
|
|
405
|
+
name: session.bot.user?.name
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const imageElements = session.event?.message?.elements?.filter(
|
|
409
|
+
(element) => element.type === "img" || element.type === "image" || element.type === "mface"
|
|
410
|
+
) || [];
|
|
411
|
+
this.logInfo("机器人发送消息 (before-send):", {
|
|
412
|
+
selfId: session.selfId,
|
|
413
|
+
channelId: messageEvent.channelId,
|
|
414
|
+
content,
|
|
415
|
+
platform: session.platform,
|
|
416
|
+
imageCount: imageElements.length,
|
|
417
|
+
imageUrls: imageElements.map((el) => el.attrs?.src || el.attrs?.url || el.attrs?.file)
|
|
418
|
+
});
|
|
419
|
+
this.ctx.console.broadcast("chat-bot-message-event", messageEvent);
|
|
579
420
|
} catch (error) {
|
|
580
|
-
logger.error("
|
|
421
|
+
this.logger.error("广播机器人消息事件失败:", error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
logInfo(...args) {
|
|
425
|
+
if (this.config.loggerinfo) {
|
|
426
|
+
this.logger.info(...args);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/api-handlers.ts
|
|
432
|
+
var import_koishi = require("koishi");
|
|
433
|
+
var import_node_url = require("node:url");
|
|
434
|
+
var ApiHandlers = class {
|
|
435
|
+
constructor(ctx, config, fileManager) {
|
|
436
|
+
this.ctx = ctx;
|
|
437
|
+
this.config = config;
|
|
438
|
+
this.fileManager = fileManager;
|
|
439
|
+
this.logger = ctx.logger("chat-patch");
|
|
440
|
+
}
|
|
441
|
+
static {
|
|
442
|
+
__name(this, "ApiHandlers");
|
|
443
|
+
}
|
|
444
|
+
logger;
|
|
445
|
+
registerApiHandlers() {
|
|
446
|
+
this.ctx.console.addListener("get-chat-data", async () => {
|
|
447
|
+
try {
|
|
448
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
449
|
+
const cleanedData = this.fileManager.cleanExcessMessages(data);
|
|
450
|
+
this.logInfo("获取聊天数据:", {
|
|
451
|
+
机器人数量: Object.keys(cleanedData.bots).length,
|
|
452
|
+
频道数量: Object.keys(cleanedData.channels).reduce((total, botId) => total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
|
|
453
|
+
消息频道数: Object.keys(cleanedData.messages).length,
|
|
454
|
+
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
success: true,
|
|
458
|
+
data: {
|
|
459
|
+
...cleanedData,
|
|
460
|
+
pinnedBots: cleanedData.pinnedBots,
|
|
461
|
+
pinnedChannels: cleanedData.pinnedChannels
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.logger.error("获取聊天数据失败:", error);
|
|
466
|
+
return { success: false, error: error?.message || String(error) };
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
this.ctx.console.addListener("get-history-messages", async (requestData) => {
|
|
470
|
+
try {
|
|
471
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
472
|
+
const channelKey = `${requestData.selfId}:${requestData.channelId}`;
|
|
473
|
+
const messages = data.messages[channelKey] || [];
|
|
474
|
+
const sortedMessages = messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
475
|
+
this.logInfo("获取历史消息:", channelKey, "共", sortedMessages.length, "条消息");
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
messages: sortedMessages
|
|
479
|
+
};
|
|
480
|
+
} catch (error) {
|
|
481
|
+
this.logger.error("获取历史消息失败:", error);
|
|
482
|
+
return { success: false, error: error?.message || String(error), messages: [] };
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
this.ctx.console.addListener("get-all-channel-message-counts", async () => {
|
|
486
|
+
try {
|
|
487
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
488
|
+
const counts = {};
|
|
489
|
+
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
490
|
+
counts[channelKey] = messages.length;
|
|
491
|
+
}
|
|
492
|
+
this.logInfo("获取所有频道消息数量:", {
|
|
493
|
+
频道数: Object.keys(counts).length,
|
|
494
|
+
总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
|
|
495
|
+
});
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
counts
|
|
499
|
+
};
|
|
500
|
+
} catch (error) {
|
|
501
|
+
this.logger.error("获取频道消息数量失败:", error);
|
|
502
|
+
return { success: false, error: error?.message || String(error), counts: {} };
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
this.ctx.console.addListener("fetch-image", async (data) => {
|
|
506
|
+
try {
|
|
507
|
+
if (this.isFileUrl(data.url)) {
|
|
508
|
+
this.logInfo("处理本地文件请求:", data.url);
|
|
509
|
+
return await this.handleLocalFileRequest(data.url);
|
|
510
|
+
}
|
|
511
|
+
const response = await fetch(data.url, {
|
|
512
|
+
headers: {
|
|
513
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
514
|
+
"Referer": ""
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
519
|
+
}
|
|
520
|
+
const buffer = await response.arrayBuffer();
|
|
521
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
522
|
+
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
base64,
|
|
526
|
+
contentType,
|
|
527
|
+
dataUrl: `data:${contentType};base64,${base64}`
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
return { success: false, error: error?.message || String(error) };
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
this.ctx.console.addListener("clear-channel-history", async (data) => {
|
|
534
|
+
try {
|
|
535
|
+
this.logInfo("收到清理历史记录请求:", data);
|
|
536
|
+
const chatData = this.fileManager.readChatDataFromFile();
|
|
537
|
+
const channelKey = `${data.selfId}:${data.channelId}`;
|
|
538
|
+
if (!chatData.messages[channelKey]) {
|
|
539
|
+
return { success: true, message: "频道没有历史消息" };
|
|
540
|
+
}
|
|
541
|
+
const messages = chatData.messages[channelKey];
|
|
542
|
+
const originalCount = messages.length;
|
|
543
|
+
const keepCount = data.keepCount || this.config.keepMessagesOnClear;
|
|
544
|
+
if (keepCount > 0 && originalCount <= keepCount) {
|
|
545
|
+
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` };
|
|
546
|
+
}
|
|
547
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
548
|
+
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : [];
|
|
549
|
+
const clearedCount = originalCount - keptMessages.length;
|
|
550
|
+
chatData.messages[channelKey] = keptMessages;
|
|
551
|
+
this.fileManager.writeChatDataToFile(chatData);
|
|
552
|
+
this.logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
|
|
553
|
+
原始消息数: originalCount,
|
|
554
|
+
保留消息数: keptMessages.length,
|
|
555
|
+
清理消息数: clearedCount
|
|
556
|
+
});
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
|
|
560
|
+
clearedCount,
|
|
561
|
+
keptCount: keptMessages.length
|
|
562
|
+
};
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.logger.error("清理频道历史记录失败:", error);
|
|
565
|
+
return { success: false, error: error?.message || String(error) };
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
this.ctx.console.addListener("send-message", async (data) => {
|
|
569
|
+
try {
|
|
570
|
+
this.logInfo("收到发送消息请求:", data);
|
|
571
|
+
const bot = this.ctx.bots.find((bot2) => bot2.selfId === data.selfId);
|
|
572
|
+
if (!bot) {
|
|
573
|
+
this.logger.error("未找到机器人:", data.selfId);
|
|
574
|
+
return { success: false, error: "未找到指定的机器人" };
|
|
575
|
+
}
|
|
576
|
+
let messageContent = data.content;
|
|
577
|
+
if (data.images && data.images.length > 0) {
|
|
578
|
+
const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
|
|
579
|
+
for (const image of data.images) {
|
|
580
|
+
const files = require("fs").readdirSync(tempDir).filter(
|
|
581
|
+
(file) => file.includes(`temp_${image.tempId}`)
|
|
582
|
+
);
|
|
583
|
+
if (files.length > 0) {
|
|
584
|
+
const imagePath = `${tempDir}/${files[0]}`;
|
|
585
|
+
const fileUrl = this.createFileUrl(imagePath);
|
|
586
|
+
messageContent += import_koishi.h.image(fileUrl).toString();
|
|
587
|
+
this.logInfo("添加图片到消息:", { imagePath, fileUrl });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const result = await bot.sendMessage(data.channelId, messageContent);
|
|
592
|
+
this.logInfo("消息发送成功:", result);
|
|
593
|
+
return {
|
|
594
|
+
success: true,
|
|
595
|
+
messageId: Array.isArray(result) ? result[0] : result,
|
|
596
|
+
tempImageIds: data.images?.map((img) => img.tempId) || []
|
|
597
|
+
};
|
|
598
|
+
} catch (error) {
|
|
599
|
+
this.logger.error("发送消息失败:", error);
|
|
600
|
+
return { success: false, error: error?.message || String(error) };
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
this.ctx.console.addListener("cleanup-temp-images", async (data) => {
|
|
604
|
+
try {
|
|
605
|
+
this.logInfo("收到清理临时图片请求:", data.tempImageIds);
|
|
606
|
+
this.logInfo("临时图片将由定时任务清理,保持文件可用性");
|
|
607
|
+
return { success: true, cleanedCount: 0 };
|
|
608
|
+
} catch (error) {
|
|
609
|
+
this.logger.error("清理临时图片失败:", error);
|
|
610
|
+
return { success: false, error: error?.message || String(error) };
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
this.ctx.console.addListener("delete-bot-data", async (data) => {
|
|
614
|
+
try {
|
|
615
|
+
this.logInfo("收到删除机器人数据请求:", data);
|
|
616
|
+
const chatData = this.fileManager.readChatDataFromFile();
|
|
617
|
+
let deletedChannels = 0;
|
|
618
|
+
let deletedMessages = 0;
|
|
619
|
+
if (chatData.channels[data.selfId]) {
|
|
620
|
+
deletedChannels = Object.keys(chatData.channels[data.selfId]).length;
|
|
621
|
+
delete chatData.channels[data.selfId];
|
|
622
|
+
}
|
|
623
|
+
const channelsToDelete = Object.keys(chatData.messages).filter((key) => key.startsWith(`${data.selfId}:`));
|
|
624
|
+
for (const channelKey of channelsToDelete) {
|
|
625
|
+
deletedMessages += chatData.messages[channelKey].length;
|
|
626
|
+
delete chatData.messages[channelKey];
|
|
627
|
+
}
|
|
628
|
+
delete chatData.bots[data.selfId];
|
|
629
|
+
this.fileManager.writeChatDataToFile(chatData);
|
|
630
|
+
this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
|
|
631
|
+
删除频道数: deletedChannels,
|
|
632
|
+
删除消息数: deletedMessages
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
success: true,
|
|
636
|
+
message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
|
|
637
|
+
deletedChannels,
|
|
638
|
+
deletedMessages
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
this.logger.error("删除机器人数据失败:", error);
|
|
642
|
+
return { success: false, error: error?.message || String(error) };
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
this.ctx.console.addListener("delete-channel-data", async (data) => {
|
|
646
|
+
try {
|
|
647
|
+
this.logInfo("收到删除频道数据请求:", data);
|
|
648
|
+
const chatData = this.fileManager.readChatDataFromFile();
|
|
649
|
+
const channelKey = `${data.selfId}:${data.channelId}`;
|
|
650
|
+
let deletedMessages = 0;
|
|
651
|
+
if (chatData.messages[channelKey]) {
|
|
652
|
+
deletedMessages = chatData.messages[channelKey].length;
|
|
653
|
+
delete chatData.messages[channelKey];
|
|
654
|
+
}
|
|
655
|
+
if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
|
|
656
|
+
delete chatData.channels[data.selfId][data.channelId];
|
|
657
|
+
}
|
|
658
|
+
this.fileManager.writeChatDataToFile(chatData);
|
|
659
|
+
this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
|
|
660
|
+
删除消息数: deletedMessages
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
success: true,
|
|
664
|
+
message: `成功删除频道数据:${deletedMessages} 条消息`,
|
|
665
|
+
deletedMessages
|
|
666
|
+
};
|
|
667
|
+
} catch (error) {
|
|
668
|
+
this.logger.error("删除频道数据失败:", error);
|
|
669
|
+
return { success: false, error: error?.message || String(error) };
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
this.ctx.console.addListener("set-pinned-bots", async (data) => {
|
|
673
|
+
try {
|
|
674
|
+
this.logInfo("收到设置置顶机器人请求:", data.pinnedBots);
|
|
675
|
+
const chatData = this.fileManager.readChatDataFromFile();
|
|
676
|
+
chatData.pinnedBots = data.pinnedBots;
|
|
677
|
+
this.fileManager.writeChatDataToFile(chatData);
|
|
678
|
+
return { success: true };
|
|
679
|
+
} catch (error) {
|
|
680
|
+
this.logger.error("设置置顶机器人失败:", error);
|
|
681
|
+
return { success: false, error: error?.message || String(error) };
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
this.ctx.console.addListener("set-pinned-channels", async (data) => {
|
|
685
|
+
try {
|
|
686
|
+
this.logInfo("收到设置置顶频道请求:", data.pinnedChannels);
|
|
687
|
+
const chatData = this.fileManager.readChatDataFromFile();
|
|
688
|
+
chatData.pinnedChannels = data.pinnedChannels;
|
|
689
|
+
this.fileManager.writeChatDataToFile(chatData);
|
|
690
|
+
return { success: true };
|
|
691
|
+
} catch (error) {
|
|
692
|
+
this.logger.error("设置置顶频道失败:", error);
|
|
693
|
+
return { success: false, error: error?.message || String(error) };
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
this.ctx.console.addListener("upload-image", async (data) => {
|
|
697
|
+
try {
|
|
698
|
+
this.logInfo("收到图片上传请求:", { filename: data.filename, mimeType: data.mimeType });
|
|
699
|
+
const base64Data = data.file.replace(/^data:image\/\w+;base64,/, "");
|
|
700
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
701
|
+
const tempId = Date.now() + "_" + Math.random().toString(36).substring(2, 11);
|
|
702
|
+
const extension = data.filename.split(".").pop() || "jpg";
|
|
703
|
+
const tempFilename = `temp_${tempId}.${extension}`;
|
|
704
|
+
const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
|
|
705
|
+
if (!require("fs").existsSync(tempDir)) {
|
|
706
|
+
require("fs").mkdirSync(tempDir, { recursive: true });
|
|
707
|
+
}
|
|
708
|
+
const tempPath = `${tempDir}/${tempFilename}`;
|
|
709
|
+
require("fs").writeFileSync(tempPath, buffer);
|
|
710
|
+
this.logInfo("图片上传成功:", { tempPath, size: buffer.length });
|
|
711
|
+
return {
|
|
712
|
+
success: true,
|
|
713
|
+
tempId,
|
|
714
|
+
tempPath,
|
|
715
|
+
filename: data.filename,
|
|
716
|
+
size: buffer.length
|
|
717
|
+
};
|
|
718
|
+
} catch (error) {
|
|
719
|
+
this.logger.error("图片上传失败:", error);
|
|
720
|
+
return { success: false, error: error?.message || String(error) };
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
this.ctx.console.addListener("delete-temp-image", async (data) => {
|
|
724
|
+
try {
|
|
725
|
+
const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
|
|
726
|
+
const files = require("fs").readdirSync(tempDir).filter(
|
|
727
|
+
(file) => file.includes(`temp_${data.tempId}`)
|
|
728
|
+
);
|
|
729
|
+
for (const file of files) {
|
|
730
|
+
const filePath = `${tempDir}/${file}`;
|
|
731
|
+
if (require("fs").existsSync(filePath)) {
|
|
732
|
+
require("fs").unlinkSync(filePath);
|
|
733
|
+
this.logInfo("删除临时图片:", filePath);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return { success: true };
|
|
737
|
+
} catch (error) {
|
|
738
|
+
this.logger.error("删除临时图片失败:", error);
|
|
739
|
+
return { success: false, error: error?.message || String(error) };
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
this.setupTempFileCleanup();
|
|
743
|
+
this.ctx.console.addListener("get-plugin-config", async () => {
|
|
744
|
+
try {
|
|
745
|
+
return {
|
|
746
|
+
success: true,
|
|
747
|
+
config: {
|
|
748
|
+
maxMessagesPerChannel: this.config.maxMessagesPerChannel,
|
|
749
|
+
keepMessagesOnClear: this.config.keepMessagesOnClear,
|
|
750
|
+
keepTempImages: this.config.keepTempImages,
|
|
751
|
+
loggerinfo: this.config.loggerinfo,
|
|
752
|
+
blockedPlatforms: this.config.blockedPlatforms || [],
|
|
753
|
+
chatContainerHeight: this.config.chatContainerHeight
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
} catch (error) {
|
|
757
|
+
this.logger.error("获取插件配置失败:", error);
|
|
758
|
+
return { success: false, error: error?.message || String(error) };
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
this.ctx.console.addListener("debug-get-raw-data", async () => {
|
|
762
|
+
try {
|
|
763
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
764
|
+
return {
|
|
765
|
+
success: true,
|
|
766
|
+
data
|
|
767
|
+
};
|
|
768
|
+
} catch (error) {
|
|
769
|
+
this.logger.error("获取原始数据失败:", error);
|
|
770
|
+
return { success: false, error: error?.message || String(error) };
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// 检查是否为文件 URL
|
|
775
|
+
isFileUrl(url) {
|
|
776
|
+
try {
|
|
777
|
+
const parsedUrl = new import_node_url.URL(url);
|
|
778
|
+
return parsedUrl.protocol === "file:";
|
|
779
|
+
} catch {
|
|
780
|
+
return false;
|
|
581
781
|
}
|
|
582
782
|
}
|
|
583
|
-
|
|
783
|
+
// 创建文件 URL
|
|
784
|
+
createFileUrl(filePath) {
|
|
785
|
+
try {
|
|
786
|
+
return (0, import_node_url.pathToFileURL)(filePath).href;
|
|
787
|
+
} catch (error) {
|
|
788
|
+
this.logger.error("创建文件URL失败:", { filePath, error });
|
|
789
|
+
return `file://${filePath}`;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// 处理本地文件请求
|
|
793
|
+
async handleLocalFileRequest(fileUrl) {
|
|
794
|
+
try {
|
|
795
|
+
const fileresponse = await this.ctx.http.file(fileUrl);
|
|
796
|
+
const fileresponsebase64 = Buffer.from(fileresponse.data).toString("base64");
|
|
797
|
+
let contentType = fileresponse.type;
|
|
798
|
+
this.logInfo("成功读取本地文件:", { fileUrl, contentType });
|
|
799
|
+
return {
|
|
800
|
+
success: true,
|
|
801
|
+
base64: fileresponsebase64,
|
|
802
|
+
contentType,
|
|
803
|
+
dataUrl: `data:${contentType};base64,${fileresponsebase64}`
|
|
804
|
+
};
|
|
805
|
+
} catch (error) {
|
|
806
|
+
this.logger.error("读取本地文件失败:", { fileUrl, error: error.message });
|
|
807
|
+
return {
|
|
808
|
+
success: false,
|
|
809
|
+
error: `读取本地文件失败: ${error.message}`
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// 设置定时清理临时文件
|
|
814
|
+
setupTempFileCleanup() {
|
|
815
|
+
setInterval(() => {
|
|
816
|
+
this.cleanupTempImagesByCount();
|
|
817
|
+
}, 5 * 60 * 1e3);
|
|
818
|
+
}
|
|
819
|
+
// 基于数量清理临时图片(保留最新的N张)
|
|
820
|
+
async cleanupTempImagesByCount() {
|
|
821
|
+
try {
|
|
822
|
+
const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
|
|
823
|
+
if (!require("fs").existsSync(tempDir)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const now = Date.now();
|
|
827
|
+
const protectionTime = 30 * 1e3;
|
|
828
|
+
const files = require("fs").readdirSync(tempDir).filter((file) => file.startsWith("temp_")).map((file) => {
|
|
829
|
+
const filePath = `${tempDir}/${file}`;
|
|
830
|
+
try {
|
|
831
|
+
const stats = require("fs").statSync(filePath);
|
|
832
|
+
const fileAge = now - stats.mtime.getTime();
|
|
833
|
+
return {
|
|
834
|
+
name: file,
|
|
835
|
+
path: filePath,
|
|
836
|
+
mtime: stats.mtime.getTime(),
|
|
837
|
+
age: fileAge,
|
|
838
|
+
protected: fileAge < protectionTime
|
|
839
|
+
// 是否在保护期内
|
|
840
|
+
};
|
|
841
|
+
} catch (error) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
}).filter((file) => file !== null).sort((a, b) => b.mtime - a.mtime);
|
|
845
|
+
const keepCount = this.config.keepTempImages;
|
|
846
|
+
let cleanedCount = 0;
|
|
847
|
+
let protectedCount = 0;
|
|
848
|
+
const protectedFiles = files.filter((file) => file.protected);
|
|
849
|
+
const deletableFiles = files.filter((file) => !file.protected);
|
|
850
|
+
protectedCount = protectedFiles.length;
|
|
851
|
+
if (deletableFiles.length > keepCount) {
|
|
852
|
+
const filesToDelete = deletableFiles.slice(keepCount);
|
|
853
|
+
for (const file of filesToDelete) {
|
|
854
|
+
try {
|
|
855
|
+
if (require("fs").existsSync(file.path)) {
|
|
856
|
+
require("fs").unlinkSync(file.path);
|
|
857
|
+
cleanedCount++;
|
|
858
|
+
this.logInfo("清理多余临时图片:", file.path);
|
|
859
|
+
}
|
|
860
|
+
} catch (fileError) {
|
|
861
|
+
this.logger.warn("删除临时文件失败:", { file: file.name, error: fileError });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (cleanedCount > 0 || protectedCount > 0) {
|
|
866
|
+
this.logInfo(`基于数量清理完成,保留最新 ${keepCount} 张图片,清理了 ${cleanedCount} 张多余图片,保护了 ${protectedCount} 张新上传图片`);
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
this.logger.warn("基于数量清理临时文件失败:", error);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
logInfo(...args) {
|
|
873
|
+
if (this.config.loggerinfo) {
|
|
874
|
+
this.logger.info(...args);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// src/config.ts
|
|
880
|
+
var import_koishi2 = require("koishi");
|
|
881
|
+
var Config = import_koishi2.Schema.intersect([
|
|
882
|
+
import_koishi2.Schema.object({
|
|
883
|
+
maxMessagesPerChannel: import_koishi2.Schema.number().default(1e3).description("每个群组最大保存消息数量").min(50).max(5e3),
|
|
884
|
+
keepMessagesOnClear: import_koishi2.Schema.number().default(50).description("手动清理历史记录时保留的消息数量").min(0).max(1e3),
|
|
885
|
+
keepTempImages: import_koishi2.Schema.number().default(50).description("发送消息保留的临时图片数量(最新的N张)").min(10).max(200),
|
|
886
|
+
blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
|
|
887
|
+
platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
|
|
888
|
+
exactMatch: import_koishi2.Schema.boolean().default(false).description("完全匹配?如果关闭,包含关键词即屏蔽").default(true)
|
|
889
|
+
})).role("table").description("屏蔽的平台列表").default(
|
|
890
|
+
[
|
|
891
|
+
{
|
|
892
|
+
"platformName": "qq",
|
|
893
|
+
"exactMatch": true
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
"platformName": "qqguild",
|
|
897
|
+
"exactMatch": true
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
"platformName": "sandbox",
|
|
901
|
+
"exactMatch": false
|
|
902
|
+
}
|
|
903
|
+
]
|
|
904
|
+
)
|
|
905
|
+
}).description("基础设置"),
|
|
906
|
+
import_koishi2.Schema.object({
|
|
907
|
+
chatContainerHeight: import_koishi2.Schema.number().default(80).description("手机端使用的视口高度(防止文本输入框被挡住)").min(50).max(100),
|
|
908
|
+
loggerinfo: import_koishi2.Schema.boolean().default(false).description("日志调试模式").experimental()
|
|
909
|
+
}).description("进阶设置")
|
|
910
|
+
]);
|
|
911
|
+
|
|
912
|
+
// src/index.ts
|
|
913
|
+
var name = "chat-patch";
|
|
914
|
+
var reusable = false;
|
|
915
|
+
var filter = false;
|
|
916
|
+
var inject = {
|
|
917
|
+
required: ["console"]
|
|
918
|
+
};
|
|
919
|
+
var usage = `
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
开启后,即可在koishi控制台操作机器人收发消息啦
|
|
924
|
+
|
|
925
|
+
暂时只支持接受图文消息 / 发送文字消息
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
`;
|
|
929
|
+
async function apply(ctx, config) {
|
|
930
|
+
const logger = ctx.logger("chat-patch");
|
|
931
|
+
const fileManager = new FileManager(ctx, config);
|
|
932
|
+
const messageHandler = new MessageHandler(ctx, config, fileManager);
|
|
933
|
+
const apiHandlers = new ApiHandlers(ctx, config, fileManager);
|
|
934
|
+
const utils = new Utils(config);
|
|
935
|
+
const initialData = fileManager.readChatDataFromFile();
|
|
936
|
+
const cleanedData = fileManager.cleanExcessMessages(initialData);
|
|
937
|
+
const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
938
|
+
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
939
|
+
if (originalCount !== cleanedCount) {
|
|
940
|
+
fileManager.writeChatDataToFile(cleanedData);
|
|
941
|
+
}
|
|
942
|
+
function logInfo(...args) {
|
|
943
|
+
if (config.loggerinfo) {
|
|
944
|
+
logger.info(...args);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
__name(logInfo, "logInfo");
|
|
948
|
+
logInfo("插件加载完成,数据统计:", {
|
|
949
|
+
机器人数量: Object.keys(cleanedData.bots).length,
|
|
950
|
+
频道数量: Object.keys(cleanedData.channels).reduce((total, botId) => total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
|
|
951
|
+
消息频道数: Object.keys(cleanedData.messages).length,
|
|
952
|
+
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
953
|
+
});
|
|
954
|
+
ctx.on("message", async (session) => {
|
|
955
|
+
if (utils.isPlatformBlocked(session.platform || "unknown")) {
|
|
956
|
+
logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
await messageHandler.broadcastMessageEvent(session);
|
|
960
|
+
});
|
|
961
|
+
ctx.on("before-send", async (session) => {
|
|
962
|
+
if (utils.isPlatformBlocked(session.platform || "unknown")) {
|
|
963
|
+
logInfo(`忽略来自被屏蔽平台的机器人消息: ${session.platform}`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
await messageHandler.broadcastBotMessageEvent(session);
|
|
967
|
+
});
|
|
968
|
+
ctx.on("ready", async () => {
|
|
969
|
+
logInfo("插件启动完成,开始监听消息");
|
|
970
|
+
setInterval(() => {
|
|
971
|
+
const data = fileManager.readChatDataFromFile();
|
|
972
|
+
const cleanedData2 = fileManager.cleanExcessMessages(data);
|
|
973
|
+
const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
974
|
+
const cleanedCount2 = Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
975
|
+
if (originalCount2 !== cleanedCount2) {
|
|
976
|
+
fileManager.writeChatDataToFile(cleanedData2);
|
|
977
|
+
logInfo("定期清理完成,清理了", originalCount2 - cleanedCount2, "条超量消息");
|
|
978
|
+
}
|
|
979
|
+
}, 3e5);
|
|
980
|
+
});
|
|
981
|
+
apiHandlers.registerApiHandlers();
|
|
982
|
+
ctx.console.addEntry({
|
|
983
|
+
dev: import_node_path2.default.resolve(__dirname, "../client/index.ts"),
|
|
984
|
+
prod: import_node_path2.default.resolve(__dirname, "../dist")
|
|
985
|
+
});
|
|
584
986
|
}
|
|
585
987
|
__name(apply, "apply");
|
|
586
|
-
export
|
|
988
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
989
|
+
0 && (module.exports = {
|
|
587
990
|
Config,
|
|
588
991
|
apply,
|
|
589
992
|
filter,
|
|
@@ -591,4 +994,4 @@ export {
|
|
|
591
994
|
name,
|
|
592
995
|
reusable,
|
|
593
996
|
usage
|
|
594
|
-
};
|
|
997
|
+
});
|