koishi-plugin-chat-patch 0.8.3 → 1.0.7

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/index.js CHANGED
@@ -39,480 +39,209 @@ __export(src_exports, {
39
39
  usage: () => usage
40
40
  });
41
41
  module.exports = __toCommonJS(src_exports);
42
- var import_koishi = require("koishi");
42
+ var import_node_path2 = __toESM(require("node:path"));
43
+
44
+ // src/file-manager.ts
43
45
  var import_node_path = __toESM(require("node:path"));
44
46
  var import_node_fs = __toESM(require("node:fs"));
45
- var name = "chat-patch";
46
- var reusable = false;
47
- var filter = false;
48
- var inject = {
49
- required: ["console"]
50
- };
51
- var usage = `
52
-
53
- ---
54
-
55
- 开启后,即可在koishi控制台操作机器人收发消息啦
56
-
57
- 暂时只支持接受图文消息 / 发送文字消息
58
-
59
- ---
60
- `;
61
- var Config = import_koishi.Schema.intersect([
62
- import_koishi.Schema.object({
63
- maxMessagesPerChannel: import_koishi.Schema.number().default(1e3).description("每个群组最大保存消息数量").min(50).max(5e3),
64
- keepMessagesOnClear: import_koishi.Schema.number().default(0).description("手动清理历史记录时保留的消息数量").min(0).max(1e3),
65
- blockedPlatforms: import_koishi.Schema.array(import_koishi.Schema.object({
66
- platformName: import_koishi.Schema.string().description("平台名称或关键词"),
67
- exactMatch: import_koishi.Schema.boolean().default(false).description("完全匹配?如果关闭,包含关键词即屏蔽").default(true)
68
- })).role("table").description("屏蔽的平台列表").default([
69
- {
70
- "platformName": "qq",
71
- "exactMatch": true
72
- },
73
- {
74
- "platformName": "qqguild",
75
- "exactMatch": true
76
- },
77
- {
78
- "platformName": "sandbox",
79
- "exactMatch": false
80
- }
81
- ])
82
- }).description("基础设置"),
83
- import_koishi.Schema.object({
84
- chatContainerHeight: import_koishi.Schema.number().default(80).description("手机端使用的视口高度(防止文本输入框被挡住)").min(50).max(100),
85
- loggerinfo: import_koishi.Schema.boolean().default(false).description("日志调试模式").experimental()
86
- }).description("进阶设置")
87
- ]);
88
- async function apply(ctx, config) {
89
- const logger = ctx.logger("chat-patch");
90
- const dataFilePath = import_node_path.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
91
- const initialData = readChatDataFromFile();
92
- const cleanedData = cleanExcessMessages(initialData);
93
- const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0);
94
- const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0);
95
- if (originalCount !== cleanedCount) {
96
- writeChatDataToFile(cleanedData);
97
- }
98
- logInfo("插件加载完成,数据统计:", {
99
- 机器人数量: Object.keys(cleanedData.bots).length,
100
- 频道数量: Object.keys(cleanedData.channels).reduce((total, botId) => total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
101
- 消息频道数: Object.keys(cleanedData.messages).length,
102
- 总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
103
- });
104
- ctx.on("message", async (session) => {
105
- if (isPlatformBlocked(session.platform || "unknown")) {
106
- logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`);
107
- return;
108
- }
109
- await broadcastMessageEvent(session);
110
- });
111
- ctx.on("ready", async () => {
112
- logInfo("插件启动完成,开始监听消息");
113
- setInterval(() => {
114
- const data = readChatDataFromFile();
115
- const cleanedData2 = cleanExcessMessages(data);
116
- const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
117
- const cleanedCount2 = Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0);
118
- if (originalCount2 !== cleanedCount2) {
119
- writeChatDataToFile(cleanedData2);
120
- logInfo("定期清理完成,清理了", originalCount2 - cleanedCount2, "条超量消息");
121
- }
122
- }, 3e5);
123
- });
124
- ctx.console.addListener("get-chat-data", async () => {
125
- try {
126
- const data = readChatDataFromFile();
127
- const cleanedData2 = cleanExcessMessages(data);
128
- logInfo("获取聊天数据:", {
129
- 机器人数量: Object.keys(cleanedData2.bots).length,
130
- 频道数量: Object.keys(cleanedData2.channels).reduce((total, botId) => total + Object.keys(cleanedData2.channels[botId] || {}).length, 0),
131
- 消息频道数: Object.keys(cleanedData2.messages).length,
132
- 总消息数: Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0)
133
- });
134
- return {
135
- success: true,
136
- data: {
137
- ...cleanedData2,
138
- pinnedBots: cleanedData2.pinnedBots,
139
- // 包含置顶机器人
140
- pinnedChannels: cleanedData2.pinnedChannels
141
- // 包含置顶频道
142
- }
143
- };
144
- } catch (error) {
145
- logger.error("获取聊天数据失败:", error);
146
- return { success: false, error: error?.message || String(error) };
147
- }
148
- });
149
- ctx.console.addListener("get-history-messages", async (requestData) => {
150
- try {
151
- const data = readChatDataFromFile();
152
- const channelKey = `${requestData.selfId}-${requestData.channelId}`;
153
- const messages = data.messages[channelKey] || [];
154
- const sortedMessages = messages.sort((a, b) => a.timestamp - b.timestamp);
155
- logInfo("获取历史消息:", channelKey, "共", sortedMessages.length, "条消息");
156
- return {
157
- success: true,
158
- messages: sortedMessages
159
- };
160
- } catch (error) {
161
- logger.error("获取历史消息失败:", error);
162
- return { success: false, error: error?.message || String(error), messages: [] };
163
- }
164
- });
165
- ctx.console.addListener("get-all-channel-message-counts", async () => {
166
- try {
167
- const data = readChatDataFromFile();
168
- const counts = {};
169
- for (const [channelKey, messages] of Object.entries(data.messages)) {
170
- counts[channelKey] = messages.length;
171
- }
172
- logInfo("获取所有频道消息数量:", {
173
- 频道数: Object.keys(counts).length,
174
- 总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
175
- });
176
- return {
177
- success: true,
178
- counts
179
- };
180
- } catch (error) {
181
- logger.error("获取频道消息数量失败:", error);
182
- return { success: false, error: error?.message || String(error), counts: {} };
183
- }
184
- });
185
- ctx.console.addListener("fetch-image", async (data) => {
186
- try {
187
- const response = await fetch(data.url, {
188
- headers: {
189
- "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",
190
- "Referer": ""
191
- }
192
- });
193
- if (!response.ok) {
194
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
195
- }
196
- const buffer = await response.arrayBuffer();
197
- const base64 = Buffer.from(buffer).toString("base64");
198
- const contentType = response.headers.get("content-type") || "image/jpeg";
199
- return {
200
- success: true,
201
- base64,
202
- contentType,
203
- dataUrl: `data:${contentType};base64,${base64}`
204
- };
205
- } catch (error) {
206
- return { success: false, error: error?.message || String(error) };
207
- }
208
- });
209
- ctx.console.addListener("clear-channel-history", async (data) => {
210
- try {
211
- logInfo("收到清理历史记录请求:", data);
212
- const chatData = readChatDataFromFile();
213
- const channelKey = `${data.selfId}-${data.channelId}`;
214
- if (!chatData.messages[channelKey]) {
215
- return { success: true, message: "频道没有历史消息" };
216
- }
217
- const messages = chatData.messages[channelKey];
218
- const originalCount2 = messages.length;
219
- const keepCount = data.keepCount || config.keepMessagesOnClear;
220
- if (keepCount > 0 && originalCount2 <= keepCount) {
221
- return { success: true, message: `消息数量(${originalCount2})未超过保留数量(${keepCount}),无需清理` };
222
- }
223
- const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
224
- const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : [];
225
- const clearedCount = originalCount2 - keptMessages.length;
226
- chatData.messages[channelKey] = keptMessages;
227
- writeChatDataToFile(chatData);
228
- logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
229
- 原始消息数: originalCount2,
230
- 保留消息数: keptMessages.length,
231
- 清理消息数: clearedCount
232
- });
233
- return {
234
- success: true,
235
- message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
236
- clearedCount,
237
- keptCount: keptMessages.length
238
- };
239
- } catch (error) {
240
- logger.error("清理频道历史记录失败:", error);
241
- return { success: false, error: error?.message || String(error) };
242
- }
243
- });
244
- ctx.console.addListener("send-message", async (data) => {
245
- try {
246
- logInfo("收到发送消息请求:", data);
247
- const bot = ctx.bots.find((bot2) => bot2.selfId === data.selfId);
248
- if (!bot) {
249
- logger.error("未找到机器人:", data.selfId);
250
- return { success: false, error: "未找到指定的机器人" };
251
- }
252
- const result = await bot.sendMessage(data.channelId, data.content);
253
- logInfo("消息发送成功:", result);
254
- const timestamp = Date.now();
255
- const chatData = readChatDataFromFile();
256
- const botInfo = {
257
- selfId: bot.selfId,
258
- platform: bot.platform || "unknown",
259
- username: bot.user?.name || `Bot-${data.selfId}`,
260
- avatar: bot.user?.avatar,
261
- status: "online"
262
- };
263
- chatData.bots[bot.selfId] = botInfo;
264
- writeChatDataToFile(chatData);
265
- const botMessageInfo = {
266
- id: Array.isArray(result) ? result[0] : result || `bot-msg-${timestamp}`,
267
- content: data.content,
268
- userId: bot.selfId,
269
- username: bot.user?.name || `Bot-${data.selfId}`,
270
- avatar: bot.user?.avatar,
271
- timestamp,
272
- channelId: data.channelId,
273
- selfId: data.selfId,
274
- type: "bot",
275
- platform: bot.platform || "unknown"
276
- };
277
- addMessageToFile(botMessageInfo);
278
- const sentMessageEvent = {
279
- type: "bot-message-sent",
280
- selfId: data.selfId,
281
- channelId: data.channelId,
282
- messageId: Array.isArray(result) ? result[0] : result,
283
- content: data.content,
284
- timestamp,
285
- botUsername: bot.user?.name || `Bot-${data.selfId}`,
286
- botAvatar: bot.user?.avatar
287
- };
288
- ctx.console.broadcast("bot-message-sent-event", sentMessageEvent);
289
- return { success: true, messageId: Array.isArray(result) ? result[0] : result };
290
- } catch (error) {
291
- logger.error("发送消息失败:", error);
292
- return { success: false, error: error?.message || String(error) };
293
- }
294
- });
295
- ctx.console.addListener("delete-bot-data", async (data) => {
296
- try {
297
- logInfo("收到删除机器人数据请求:", data);
298
- const chatData = readChatDataFromFile();
299
- let deletedChannels = 0;
300
- let deletedMessages = 0;
301
- if (chatData.channels[data.selfId]) {
302
- deletedChannels = Object.keys(chatData.channels[data.selfId]).length;
303
- delete chatData.channels[data.selfId];
304
- }
305
- const channelsToDelete = Object.keys(chatData.messages).filter((key) => key.startsWith(`${data.selfId}-`));
306
- for (const channelKey of channelsToDelete) {
307
- deletedMessages += chatData.messages[channelKey].length;
308
- delete chatData.messages[channelKey];
309
- }
310
- delete chatData.bots[data.selfId];
311
- writeChatDataToFile(chatData);
312
- logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
313
- 删除频道数: deletedChannels,
314
- 删除消息数: deletedMessages
315
- });
316
- return {
317
- success: true,
318
- message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
319
- deletedChannels,
320
- deletedMessages
321
- };
322
- } catch (error) {
323
- logger.error("删除机器人数据失败:", error);
324
- return { success: false, error: error?.message || String(error) };
325
- }
326
- });
327
- ctx.console.addListener("delete-channel-data", async (data) => {
328
- try {
329
- logInfo("收到删除频道数据请求:", data);
330
- const chatData = readChatDataFromFile();
331
- const channelKey = `${data.selfId}-${data.channelId}`;
332
- let deletedMessages = 0;
333
- if (chatData.messages[channelKey]) {
334
- deletedMessages = chatData.messages[channelKey].length;
335
- delete chatData.messages[channelKey];
336
- }
337
- if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
338
- delete chatData.channels[data.selfId][data.channelId];
339
- }
340
- writeChatDataToFile(chatData);
341
- logInfo(`频道 ${channelKey} 数据删除完成:`, {
342
- 删除消息数: deletedMessages
343
- });
344
- return {
345
- success: true,
346
- message: `成功删除频道数据:${deletedMessages} 条消息`,
347
- deletedMessages
348
- };
349
- } catch (error) {
350
- logger.error("删除频道数据失败:", error);
351
- return { success: false, error: error?.message || String(error) };
352
- }
353
- });
354
- ctx.console.addListener("set-pinned-bots", async (data) => {
355
- try {
356
- logInfo("收到设置置顶机器人请求:", data.pinnedBots);
357
- const chatData = readChatDataFromFile();
358
- chatData.pinnedBots = data.pinnedBots;
359
- writeChatDataToFile(chatData);
360
- return { success: true };
361
- } catch (error) {
362
- logger.error("设置置顶机器人失败:", error);
363
- return { success: false, error: error?.message || String(error) };
364
- }
365
- });
366
- ctx.console.addListener("set-pinned-channels", async (data) => {
367
- try {
368
- logInfo("收到设置置顶频道请求:", data.pinnedChannels);
369
- const chatData = readChatDataFromFile();
370
- chatData.pinnedChannels = data.pinnedChannels;
371
- writeChatDataToFile(chatData);
372
- return { success: true };
373
- } catch (error) {
374
- logger.error("设置置顶频道失败:", error);
375
- return { success: false, error: error?.message || String(error) };
376
- }
377
- });
378
- ctx.console.addListener("get-plugin-config", async () => {
379
- try {
380
- return {
381
- success: true,
382
- config: {
383
- maxMessagesPerChannel: config.maxMessagesPerChannel,
384
- keepMessagesOnClear: config.keepMessagesOnClear,
385
- loggerinfo: config.loggerinfo,
386
- blockedPlatforms: config.blockedPlatforms || [],
387
- chatContainerHeight: config.chatContainerHeight
388
- }
389
- };
390
- } catch (error) {
391
- logger.error("获取插件配置失败:", error);
392
- return { success: false, error: error?.message || String(error) };
393
- }
394
- });
395
- ctx.console.addEntry({
396
- dev: import_node_path.default.resolve(__dirname, "../client/index.ts"),
397
- prod: import_node_path.default.resolve(__dirname, "../dist")
398
- });
399
- function logInfo(...args) {
400
- if (config.loggerinfo) {
401
- logger.info(...args);
402
- }
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");
403
53
  }
404
- __name(logInfo, "logInfo");
405
- function isPlatformBlocked(platform) {
406
- if (!config.blockedPlatforms || config.blockedPlatforms.length === 0) {
407
- return false;
408
- }
409
- for (const blockedPlatform of config.blockedPlatforms) {
410
- if (blockedPlatform.exactMatch) {
411
- if (platform === blockedPlatform.platformName) {
412
- logInfo(`平台 ${platform} 被屏蔽 (完全匹配: ${blockedPlatform.platformName})`);
413
- return true;
414
- }
415
- } else {
416
- if (platform.includes(blockedPlatform.platformName)) {
417
- logInfo(`平台 ${platform} 被屏蔽 (包含匹配: ${blockedPlatform.platformName})`);
418
- return true;
419
- }
420
- }
421
- }
422
- return false;
54
+ static {
55
+ __name(this, "FileManager");
423
56
  }
424
- __name(isPlatformBlocked, "isPlatformBlocked");
425
- function ensureDataDir() {
426
- const dir = import_node_path.default.dirname(dataFilePath);
57
+ dataFilePath;
58
+ fileOperationLock = Promise.resolve();
59
+ logger;
60
+ // 确保目录存在
61
+ ensureDataDir() {
62
+ const dir = import_node_path.default.dirname(this.dataFilePath);
427
63
  if (!import_node_fs.default.existsSync(dir)) {
428
64
  import_node_fs.default.mkdirSync(dir, { recursive: true });
429
65
  }
430
66
  }
431
- __name(ensureDataDir, "ensureDataDir");
432
- function readChatDataFromFile() {
67
+ // 从JSON文件读取数据
68
+ readChatDataFromFile() {
433
69
  try {
434
- if (import_node_fs.default.existsSync(dataFilePath)) {
435
- const jsonData = import_node_fs.default.readFileSync(dataFilePath, "utf8");
70
+ if (import_node_fs.default.existsSync(this.dataFilePath)) {
71
+ const jsonData = import_node_fs.default.readFileSync(this.dataFilePath, "utf8");
436
72
  const data = JSON.parse(jsonData);
437
73
  return {
438
74
  bots: data.bots || {},
439
75
  channels: data.channels || {},
440
76
  messages: data.messages || {},
441
77
  pinnedBots: data.pinnedBots || [],
442
- // 读取置顶机器人
443
78
  pinnedChannels: data.pinnedChannels || [],
444
- // 读取置顶频道
445
79
  lastSaveTime: data.lastSaveTime
446
80
  };
447
81
  }
448
82
  } catch (error) {
449
- logger.error("读取聊天数据失败:", error);
83
+ this.logger.error("读取聊天数据失败:", error);
450
84
  }
451
85
  return {
452
86
  bots: {},
453
87
  channels: {},
454
88
  messages: {},
455
89
  pinnedBots: [],
456
- // 默认空数组
457
90
  pinnedChannels: []
458
- // 默认空数组
459
91
  };
460
92
  }
461
- __name(readChatDataFromFile, "readChatDataFromFile");
462
- function writeChatDataToFile(data) {
93
+ // 写入数据到JSON文件
94
+ writeChatDataToFile(data) {
463
95
  try {
464
- ensureDataDir();
96
+ this.ensureDataDir();
465
97
  data.lastSaveTime = Date.now();
466
98
  const jsonData = JSON.stringify(data, null, 2);
467
- import_node_fs.default.writeFileSync(dataFilePath, jsonData, "utf8");
99
+ import_node_fs.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
468
100
  } catch (error) {
469
- logger.error("写入聊天数据失败:", error);
101
+ this.logger.error("写入聊天数据失败:", error);
470
102
  }
471
103
  }
472
- __name(writeChatDataToFile, "writeChatDataToFile");
473
- function cleanExcessMessages(data) {
474
- let cleanedCount2 = 0;
104
+ // 清理超量消息
105
+ cleanExcessMessages(data) {
106
+ let cleanedCount = 0;
475
107
  const cleanedMessages = {};
476
108
  for (const [channelKey, messages] of Object.entries(data.messages)) {
477
- if (messages.length > config.maxMessagesPerChannel) {
109
+ if (messages.length > this.config.maxMessagesPerChannel) {
478
110
  const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
479
- const keptMessages = sortedMessages.slice(-config.maxMessagesPerChannel);
480
- cleanedCount2 += messages.length - keptMessages.length;
111
+ const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel);
112
+ cleanedCount += messages.length - keptMessages.length;
481
113
  cleanedMessages[channelKey] = keptMessages;
482
- logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
114
+ this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
483
115
  } else {
484
116
  cleanedMessages[channelKey] = messages;
485
117
  }
486
118
  }
487
- if (cleanedCount2 > 0) {
488
- logInfo("总共清理超量消息:", cleanedCount2, "条");
119
+ if (cleanedCount > 0) {
120
+ this.logInfo("总共清理超量消息:", cleanedCount, "条");
489
121
  }
490
122
  return {
491
123
  ...data,
492
124
  messages: cleanedMessages
493
125
  };
494
126
  }
495
- __name(cleanExcessMessages, "cleanExcessMessages");
496
- function addMessageToFile(messageInfo) {
497
- const data = readChatDataFromFile();
498
- const channelKey = `${messageInfo.selfId}-${messageInfo.channelId}`;
499
- if (!data.messages[channelKey]) {
500
- 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);
501
180
  }
502
- messageInfo.timestamp = Date.now();
503
- data.messages[channelKey].push(messageInfo);
504
- if (data.messages[channelKey].length > config.maxMessagesPerChannel) {
505
- data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
506
- const removedCount = data.messages[channelKey].length - config.maxMessagesPerChannel;
507
- data.messages[channelKey] = data.messages[channelKey].slice(-config.maxMessagesPerChannel);
508
- logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`);
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
+ }
509
207
  }
510
- writeChatDataToFile(data);
511
- logInfo("添加消息到文件:", channelKey, "当前消息数:", data.messages[channelKey].length);
208
+ return false;
209
+ }
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");
512
239
  }
513
- __name(addMessageToFile, "addMessageToFile");
514
- function updateBotInfoToFile(session) {
515
- const data = readChatDataFromFile();
240
+ logger;
241
+ utils;
242
+ // 更新机器人信息到JSON文件
243
+ async updateBotInfoToFile(session) {
244
+ const data = this.fileManager.readChatDataFromFile();
516
245
  const botInfo = {
517
246
  selfId: session.selfId,
518
247
  platform: session.platform || "unknown",
@@ -521,20 +250,20 @@ async function apply(ctx, config) {
521
250
  status: "online"
522
251
  };
523
252
  data.bots[session.selfId] = botInfo;
524
- writeChatDataToFile(data);
525
- logInfo("更新机器人信息到文件:", botInfo.username);
253
+ this.fileManager.writeChatDataToFile(data);
254
+ this.logInfo("更新机器人信息到文件:", botInfo.username);
526
255
  }
527
- __name(updateBotInfoToFile, "updateBotInfoToFile");
528
- async function updateChannelInfoToFile(session) {
529
- const data = readChatDataFromFile();
256
+ // 更新频道信息到JSON文件
257
+ async updateChannelInfoToFile(session) {
530
258
  let guildName = session.channelId;
259
+ const data = this.fileManager.readChatDataFromFile();
531
260
  try {
532
261
  if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
533
262
  const guild = await session.bot.getGuild(session.guildId);
534
263
  guildName = guild?.name || session.channelId;
535
264
  }
536
265
  } catch (guildError) {
537
- logInfo("获取群组信息失败,使用频道ID作为备用:", guildError);
266
+ this.logInfo("获取群组信息失败,使用频道ID作为备用:", guildError);
538
267
  guildName = session.channelId;
539
268
  }
540
269
  if (!data.channels[session.selfId]) {
@@ -548,15 +277,14 @@ async function apply(ctx, config) {
548
277
  guildName
549
278
  };
550
279
  data.channels[session.selfId][session.channelId] = channelInfo;
551
- writeChatDataToFile(data);
552
- logInfo("更新频道信息到文件:", channelInfo.name);
280
+ this.fileManager.writeChatDataToFile(data);
281
+ this.logInfo("更新频道信息到文件:", channelInfo.name);
553
282
  return guildName;
554
283
  }
555
- __name(updateChannelInfoToFile, "updateChannelInfoToFile");
556
- async function broadcastMessageEvent(session) {
284
+ async broadcastMessageEvent(session) {
557
285
  try {
558
- updateBotInfoToFile(session);
559
- const guildName = await updateChannelInfoToFile(session);
286
+ await this.updateBotInfoToFile(session);
287
+ const guildName = await this.updateChannelInfoToFile(session);
560
288
  const timestamp = Date.now();
561
289
  let quoteInfo = void 0;
562
290
  if (session.quote) {
@@ -575,50 +303,686 @@ async function apply(ctx, config) {
575
303
  timestamp: session.quote.timestamp || Date.now()
576
304
  };
577
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
+ }
578
319
  const messageInfo = {
579
- id: session.messageId || `msg-${timestamp}`,
580
- content: session.content || "",
581
- userId: session.userId || "unknown",
582
- username: session.username || session.userId || "unknown",
583
- avatar: session.author?.avatar,
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,
584
325
  timestamp,
585
326
  channelId: session.channelId,
586
327
  selfId: session.selfId,
587
- elements: session.elements,
328
+ elements,
588
329
  type: "user",
589
330
  guildId: session.guildId,
590
331
  guildName,
591
332
  platform: session.platform || "unknown",
592
333
  quote: quoteInfo
593
334
  };
594
- addMessageToFile(messageInfo);
335
+ await this.fileManager.addMessageToFile(messageInfo);
595
336
  const messageEvent = {
596
337
  type: "message",
597
338
  selfId: session.selfId,
598
339
  platform: session.platform || "unknown",
599
340
  channelId: session.channelId,
600
- messageId: session.messageId,
601
- content: session.content,
602
- userId: session.userId || "unknown",
603
- username: session.username || session.userId || "unknown",
604
- avatar: session.author?.avatar,
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,
605
346
  timestamp,
606
347
  guildId: session.guildId,
607
348
  guildName,
608
349
  channelType: session.type || 0,
609
- elements: session.elements,
350
+ elements,
610
351
  quote: quoteInfo,
611
352
  bot: {
612
353
  avatar: session.bot.user?.avatar,
613
354
  name: session.bot.user?.name
614
355
  }
615
356
  };
616
- 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);
420
+ } catch (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;
781
+ }
782
+ }
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
+ }
617
868
  } catch (error) {
618
- logger.error("广播消息事件失败:", error);
869
+ this.logger.warn("基于数量清理临时文件失败:", error);
870
+ }
871
+ }
872
+ logInfo(...args) {
873
+ if (this.config.loggerinfo) {
874
+ this.logger.info(...args);
619
875
  }
620
876
  }
621
- __name(broadcastMessageEvent, "broadcastMessageEvent");
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
+ });
622
986
  }
623
987
  __name(apply, "apply");
624
988
  // Annotate the CommonJS export names for ESM import in node: