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/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
- // src/index.ts
5
- import { Schema } from "koishi";
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 usage = `
15
- ---
16
-
17
- 开启后,即可在koishi控制台操作机器人收发消息啦
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 Config = Schema.intersect([
24
- Schema.object({
25
- maxMessagesPerChannel: Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(5e3),
26
- keepMessagesOnClear: Schema.number().default(0).description("手动清理历史记录时保留的消息数量").min(0).max(1e3),
27
- blockedPlatforms: Schema.array(Schema.object({
28
- platformName: Schema.string().description("平台名称或关键词"),
29
- exactMatch: Schema.boolean().default(false).description("完全匹配?如果关闭,包含关键词即屏蔽").default(true)
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
- __name(logInfo, "logInfo");
367
- function isPlatformBlocked(platform) {
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
- __name(isPlatformBlocked, "isPlatformBlocked");
387
- function ensureDataDir() {
388
- const dir = path.dirname(dataFilePath);
389
- if (!fs.existsSync(dir)) {
390
- fs.mkdirSync(dir, { recursive: true });
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
- __name(ensureDataDir, "ensureDataDir");
394
- function readChatDataFromFile() {
67
+ // 从JSON文件读取数据
68
+ readChatDataFromFile() {
395
69
  try {
396
- if (fs.existsSync(dataFilePath)) {
397
- const jsonData = fs.readFileSync(dataFilePath, "utf8");
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
- __name(readChatDataFromFile, "readChatDataFromFile");
424
- function writeChatDataToFile(data) {
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
- fs.writeFileSync(dataFilePath, jsonData, "utf8");
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
- __name(writeChatDataToFile, "writeChatDataToFile");
435
- function cleanExcessMessages(data) {
436
- let cleanedCount2 = 0;
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
- cleanedCount2 += messages.length - keptMessages.length;
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 (cleanedCount2 > 0) {
450
- logInfo("总共清理超量消息:", cleanedCount2, "条");
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
- __name(cleanExcessMessages, "cleanExcessMessages");
458
- function addMessageToFile(messageInfo) {
459
- const data = readChatDataFromFile();
460
- const channelKey = `${messageInfo.selfId}-${messageInfo.channelId}`;
461
- if (!data.messages[channelKey]) {
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
- messageInfo.timestamp = Date.now();
465
- data.messages[channelKey].push(messageInfo);
466
- if (data.messages[channelKey].length > config.maxMessagesPerChannel) {
467
- data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
468
- const removedCount = data.messages[channelKey].length - config.maxMessagesPerChannel;
469
- data.messages[channelKey] = data.messages[channelKey].slice(-config.maxMessagesPerChannel);
470
- 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
+ }
471
207
  }
472
- writeChatDataToFile(data);
473
- logInfo("添加消息到文件:", channelKey, "当前消息数:", data.messages[channelKey].length);
208
+ return false;
474
209
  }
475
- __name(addMessageToFile, "addMessageToFile");
476
- function updateBotInfoToFile(session) {
477
- const data = readChatDataFromFile();
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
- __name(updateBotInfoToFile, "updateBotInfoToFile");
490
- async function updateChannelInfoToFile(session) {
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
- __name(updateChannelInfoToFile, "updateChannelInfoToFile");
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.messageId || `msg-${timestamp}`,
542
- content: session.content || "",
543
- userId: session.userId || "unknown",
544
- username: session.username || session.userId || "unknown",
545
- 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,
546
325
  timestamp,
547
326
  channelId: session.channelId,
548
327
  selfId: session.selfId,
549
- elements: session.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.messageId,
563
- content: session.content,
564
- userId: session.userId || "unknown",
565
- username: session.username || session.userId || "unknown",
566
- 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,
567
346
  timestamp,
568
347
  guildId: session.guildId,
569
348
  guildName,
570
349
  channelType: session.type || 0,
571
- elements: session.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("广播消息事件失败:", 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
- __name(broadcastMessageEvent, "broadcastMessageEvent");
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
+ });