koishi-plugin-chat-patch 2.3.0 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/vue/composables/useChatData.ts +0 -1
- package/client/vue/composables/useImageCache.ts +162 -21
- package/client/vue/composables/useVideoCache.ts +16 -21
- package/client/vue/types.ts +0 -1
- package/dist/index.js +3 -3
- package/lib/config.d.ts +0 -1
- package/lib/file-manager.d.ts +12 -4
- package/lib/index.js +276 -115
- package/lib/message-handler.d.ts +1 -1
- package/package.json +1 -1
- package/src/api-handlers.ts +103 -65
- package/src/config.ts +0 -2
- package/src/file-manager.ts +214 -57
- package/src/message-handler.ts +12 -7
package/lib/config.d.ts
CHANGED
package/lib/file-manager.d.ts
CHANGED
|
@@ -4,16 +4,24 @@ import { Config } from './config';
|
|
|
4
4
|
export declare class FileManager {
|
|
5
5
|
private ctx;
|
|
6
6
|
private config;
|
|
7
|
-
private
|
|
8
|
-
private
|
|
7
|
+
private chatHistoryDir;
|
|
8
|
+
private metadataFilePath;
|
|
9
9
|
private logger;
|
|
10
10
|
private utils;
|
|
11
11
|
private memoryCache;
|
|
12
12
|
private pendingMessages;
|
|
13
|
-
private
|
|
13
|
+
private writeTimers;
|
|
14
14
|
private readonly WRITE_DEBOUNCE_MS;
|
|
15
15
|
constructor(ctx: Context, config: Config);
|
|
16
|
-
private
|
|
16
|
+
private cleanupOldDataFiles;
|
|
17
|
+
private ensureDir;
|
|
18
|
+
private getChannelFilePath;
|
|
19
|
+
readChannelMessages(selfId: string, channelId: string): MessageInfo[];
|
|
20
|
+
private writeChannelMessages;
|
|
21
|
+
private readMetadata;
|
|
22
|
+
readMetadataOnly(): Omit<ChatData, 'messages'>;
|
|
23
|
+
private writeMetadata;
|
|
24
|
+
private loadAllChannelMessages;
|
|
17
25
|
readChatDataFromFile(): ChatData;
|
|
18
26
|
writeChatDataToFile(data: ChatData): void;
|
|
19
27
|
private scheduleWrite;
|
package/lib/index.js
CHANGED
|
@@ -304,7 +304,8 @@ var MessageHandler = class {
|
|
|
304
304
|
}
|
|
305
305
|
async downloadAndCacheMedia(url, type) {
|
|
306
306
|
try {
|
|
307
|
-
if (!url || url.startsWith("data:")
|
|
307
|
+
if (!url || url.startsWith("data:")) return url;
|
|
308
|
+
if (url.includes("/vite/@fs/")) return url;
|
|
308
309
|
let folder = "media";
|
|
309
310
|
if (type === "image") folder = "images";
|
|
310
311
|
else if (type === "avatar") folder = "avatars";
|
|
@@ -317,13 +318,14 @@ var MessageHandler = class {
|
|
|
317
318
|
const ext = require("node:path").extname(new URL(url).pathname) || (type === "image" ? ".jpg" : ".mp4");
|
|
318
319
|
const filename = `${hash}${ext}`;
|
|
319
320
|
const filePath = require("node:path").join(dir, filename);
|
|
320
|
-
if (require("node:fs").existsSync(filePath)) {
|
|
321
|
-
|
|
321
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
322
|
+
const buffer = await this.ctx.http.get(url, { responseType: "arraybuffer" });
|
|
323
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
322
324
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
return require("node:url").pathToFileURL(filePath).href;
|
|
325
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
326
|
+
return `/vite/@fs/${normalizedPath}`;
|
|
326
327
|
} catch (e) {
|
|
328
|
+
this.logger.warn("下载并缓存媒体失败:", e);
|
|
327
329
|
return url;
|
|
328
330
|
}
|
|
329
331
|
}
|
|
@@ -540,27 +542,153 @@ var FileManager = class {
|
|
|
540
542
|
constructor(ctx, config) {
|
|
541
543
|
this.ctx = ctx;
|
|
542
544
|
this.config = config;
|
|
543
|
-
|
|
545
|
+
const baseDir = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch");
|
|
546
|
+
this.chatHistoryDir = import_node_path2.default.join(baseDir, "chat-history");
|
|
547
|
+
this.metadataFilePath = import_node_path2.default.join(baseDir, "metadata.json");
|
|
544
548
|
this.logger = ctx.logger("chat-patch");
|
|
545
549
|
this.utils = new Utils(config);
|
|
550
|
+
this.cleanupOldDataFiles(baseDir);
|
|
546
551
|
this.memoryCache = this.readChatDataFromFile();
|
|
547
552
|
}
|
|
548
553
|
static {
|
|
549
554
|
__name(this, "FileManager");
|
|
550
555
|
}
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
chatHistoryDir;
|
|
557
|
+
// 聊天记录根目录
|
|
558
|
+
metadataFilePath;
|
|
559
|
+
// 元数据文件路径(存储bots、channels、pinned等信息)
|
|
553
560
|
logger;
|
|
554
561
|
utils;
|
|
555
562
|
memoryCache = null;
|
|
556
|
-
pendingMessages =
|
|
557
|
-
|
|
563
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
564
|
+
// 按channelKey分组的待写入消息
|
|
565
|
+
writeTimers = /* @__PURE__ */ new Map();
|
|
566
|
+
// 每个频道独立的写入定时器
|
|
558
567
|
WRITE_DEBOUNCE_MS = 1e3;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
568
|
+
// 清理旧版本的JSON文件
|
|
569
|
+
cleanupOldDataFiles(baseDir) {
|
|
570
|
+
const oldFiles = ["chat-data.json", "data.json", "messages.json"];
|
|
571
|
+
for (const fileName of oldFiles) {
|
|
572
|
+
const filePath = import_node_path2.default.join(baseDir, fileName);
|
|
573
|
+
if (import_node_fs2.default.existsSync(filePath)) {
|
|
574
|
+
try {
|
|
575
|
+
import_node_fs2.default.unlinkSync(filePath);
|
|
576
|
+
this.logger.info(`已删除旧版本数据文件: ${fileName}`);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.logger.warn(`删除旧版本数据文件失败: ${fileName}`, error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// 确保目录存在
|
|
584
|
+
ensureDir(dirPath) {
|
|
585
|
+
if (!import_node_fs2.default.existsSync(dirPath)) {
|
|
586
|
+
import_node_fs2.default.mkdirSync(dirPath, { recursive: true });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// 获取频道消息文件路径
|
|
590
|
+
getChannelFilePath(selfId, channelId) {
|
|
591
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, selfId);
|
|
592
|
+
this.ensureDir(botDir);
|
|
593
|
+
return import_node_path2.default.join(botDir, `${channelId}.json`);
|
|
594
|
+
}
|
|
595
|
+
// 读取单个频道的消息(公共方法)
|
|
596
|
+
readChannelMessages(selfId, channelId) {
|
|
597
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
598
|
+
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const jsonData = import_node_fs2.default.readFileSync(filePath, "utf8");
|
|
603
|
+
const messages = JSON.parse(jsonData);
|
|
604
|
+
return Array.isArray(messages) ? messages : [];
|
|
605
|
+
} catch (error) {
|
|
606
|
+
this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// 写入单个频道的消息
|
|
611
|
+
writeChannelMessages(selfId, channelId, messages) {
|
|
612
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
613
|
+
try {
|
|
614
|
+
const jsonData = JSON.stringify(messages, null, 2);
|
|
615
|
+
import_node_fs2.default.writeFileSync(filePath, jsonData, "utf8");
|
|
616
|
+
} catch (error) {
|
|
617
|
+
this.logger.error(`写入频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// 读取元数据(bots、channels、pinned等)
|
|
621
|
+
readMetadata() {
|
|
622
|
+
if (!import_node_fs2.default.existsSync(this.metadataFilePath)) {
|
|
623
|
+
return {
|
|
624
|
+
bots: {},
|
|
625
|
+
channels: {},
|
|
626
|
+
pinnedBots: [],
|
|
627
|
+
pinnedChannels: []
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const jsonData = import_node_fs2.default.readFileSync(this.metadataFilePath, "utf8");
|
|
632
|
+
const data = JSON.parse(jsonData);
|
|
633
|
+
return {
|
|
634
|
+
bots: data.bots || {},
|
|
635
|
+
channels: data.channels || {},
|
|
636
|
+
pinnedBots: data.pinnedBots || [],
|
|
637
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
638
|
+
lastSaveTime: data.lastSaveTime
|
|
639
|
+
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
this.logger.error("读取元数据失败:", error);
|
|
642
|
+
return {
|
|
643
|
+
bots: {},
|
|
644
|
+
channels: {},
|
|
645
|
+
pinnedBots: [],
|
|
646
|
+
pinnedChannels: []
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// 只读取元数据,不加载消息(公共方法)
|
|
651
|
+
readMetadataOnly() {
|
|
652
|
+
return this.readMetadata();
|
|
653
|
+
}
|
|
654
|
+
// 写入元数据
|
|
655
|
+
writeMetadata(metadata) {
|
|
656
|
+
try {
|
|
657
|
+
this.ensureDir(import_node_path2.default.dirname(this.metadataFilePath));
|
|
658
|
+
const dataToWrite = {
|
|
659
|
+
...metadata,
|
|
660
|
+
lastSaveTime: Date.now()
|
|
661
|
+
};
|
|
662
|
+
const jsonData = JSON.stringify(dataToWrite, null, 2);
|
|
663
|
+
import_node_fs2.default.writeFileSync(this.metadataFilePath, jsonData, "utf8");
|
|
664
|
+
} catch (error) {
|
|
665
|
+
this.logger.error("写入元数据失败:", error);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// 扫描所有频道消息文件并加载到内存
|
|
669
|
+
loadAllChannelMessages() {
|
|
670
|
+
const messages = {};
|
|
671
|
+
if (!import_node_fs2.default.existsSync(this.chatHistoryDir)) {
|
|
672
|
+
return messages;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
const botDirs = import_node_fs2.default.readdirSync(this.chatHistoryDir);
|
|
676
|
+
for (const botId of botDirs) {
|
|
677
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, botId);
|
|
678
|
+
const stat = import_node_fs2.default.statSync(botDir);
|
|
679
|
+
if (!stat.isDirectory()) continue;
|
|
680
|
+
const channelFiles = import_node_fs2.default.readdirSync(botDir);
|
|
681
|
+
for (const fileName of channelFiles) {
|
|
682
|
+
if (!fileName.endsWith(".json")) continue;
|
|
683
|
+
const channelId = fileName.replace(".json", "");
|
|
684
|
+
const channelKey = `${botId}:${channelId}`;
|
|
685
|
+
messages[channelKey] = this.readChannelMessages(botId, channelId);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} catch (error) {
|
|
689
|
+
this.logger.error("加载频道消息失败:", error);
|
|
563
690
|
}
|
|
691
|
+
return messages;
|
|
564
692
|
}
|
|
565
693
|
readChatDataFromFile() {
|
|
566
694
|
if (this.memoryCache) {
|
|
@@ -568,18 +696,12 @@ var FileManager = class {
|
|
|
568
696
|
}
|
|
569
697
|
process.nextTick(() => {
|
|
570
698
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
messages: data.messages || {},
|
|
578
|
-
pinnedBots: data.pinnedBots || [],
|
|
579
|
-
pinnedChannels: data.pinnedChannels || [],
|
|
580
|
-
lastSaveTime: data.lastSaveTime
|
|
581
|
-
};
|
|
582
|
-
}
|
|
699
|
+
const metadata = this.readMetadata();
|
|
700
|
+
const messages = this.loadAllChannelMessages();
|
|
701
|
+
this.memoryCache = {
|
|
702
|
+
...metadata,
|
|
703
|
+
messages
|
|
704
|
+
};
|
|
583
705
|
} catch (error) {
|
|
584
706
|
this.logger.error("读取聊天数据失败:", error);
|
|
585
707
|
}
|
|
@@ -598,36 +720,45 @@ var FileManager = class {
|
|
|
598
720
|
this.memoryCache = data;
|
|
599
721
|
process.nextTick(() => {
|
|
600
722
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
723
|
+
const { messages, ...metadata } = data;
|
|
724
|
+
this.writeMetadata(metadata);
|
|
725
|
+
for (const [channelKey, channelMessages] of Object.entries(messages)) {
|
|
726
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
727
|
+
if (selfId && channelId) {
|
|
728
|
+
this.writeChannelMessages(selfId, channelId, channelMessages);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
604
731
|
} catch (error) {
|
|
605
732
|
this.logger.error("写入聊天数据失败:", error);
|
|
606
733
|
}
|
|
607
734
|
});
|
|
608
735
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
736
|
+
// 为特定频道安排写入
|
|
737
|
+
scheduleWrite(channelKey) {
|
|
738
|
+
const existingTimer = this.writeTimers.get(channelKey);
|
|
739
|
+
if (existingTimer) {
|
|
740
|
+
existingTimer();
|
|
613
741
|
}
|
|
614
|
-
|
|
742
|
+
const timer = this.ctx.setTimeout(() => {
|
|
615
743
|
process.nextTick(() => {
|
|
616
|
-
this.flushPendingMessages();
|
|
744
|
+
this.flushPendingMessages(channelKey);
|
|
617
745
|
});
|
|
618
|
-
this.
|
|
746
|
+
this.writeTimers.delete(channelKey);
|
|
619
747
|
}, this.WRITE_DEBOUNCE_MS);
|
|
748
|
+
this.writeTimers.set(channelKey, timer);
|
|
620
749
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const messagesToWrite =
|
|
624
|
-
|
|
750
|
+
// 刷新特定频道的待写入消息
|
|
751
|
+
flushPendingMessages(channelKey) {
|
|
752
|
+
const messagesToWrite = this.pendingMessages.get(channelKey);
|
|
753
|
+
if (!messagesToWrite || messagesToWrite.length === 0) return;
|
|
754
|
+
this.pendingMessages.delete(channelKey);
|
|
625
755
|
const data = this.memoryCache || this.readChatDataFromFile();
|
|
756
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
757
|
+
if (!selfId || !channelId) return;
|
|
758
|
+
if (!data.messages[channelKey]) {
|
|
759
|
+
data.messages[channelKey] = [];
|
|
760
|
+
}
|
|
626
761
|
for (const messageInfo of messagesToWrite) {
|
|
627
|
-
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
628
|
-
if (!data.messages[channelKey]) {
|
|
629
|
-
data.messages[channelKey] = [];
|
|
630
|
-
}
|
|
631
762
|
const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
|
|
632
763
|
if (existingMessage) {
|
|
633
764
|
continue;
|
|
@@ -637,13 +768,14 @@ var FileManager = class {
|
|
|
637
768
|
}
|
|
638
769
|
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
|
|
639
770
|
data.messages[channelKey].push(cleanedMessageInfo);
|
|
640
|
-
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
641
|
-
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
642
|
-
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
643
|
-
}
|
|
644
771
|
}
|
|
645
|
-
this.
|
|
646
|
-
|
|
772
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
773
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
774
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
775
|
+
}
|
|
776
|
+
this.writeChannelMessages(selfId, channelId, data.messages[channelKey]);
|
|
777
|
+
this.memoryCache = data;
|
|
778
|
+
this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`);
|
|
647
779
|
}
|
|
648
780
|
cleanExcessMessages(data) {
|
|
649
781
|
let cleanedCount = 0;
|
|
@@ -668,9 +800,12 @@ var FileManager = class {
|
|
|
668
800
|
};
|
|
669
801
|
}
|
|
670
802
|
async addMessageToFile(messageInfo) {
|
|
671
|
-
this.pendingMessages.push(messageInfo);
|
|
672
|
-
const data = this.memoryCache || this.readChatDataFromFile();
|
|
673
803
|
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
804
|
+
if (!this.pendingMessages.has(channelKey)) {
|
|
805
|
+
this.pendingMessages.set(channelKey, []);
|
|
806
|
+
}
|
|
807
|
+
this.pendingMessages.get(channelKey).push(messageInfo);
|
|
808
|
+
const data = this.memoryCache || this.readChatDataFromFile();
|
|
674
809
|
if (!data.messages[channelKey]) {
|
|
675
810
|
data.messages[channelKey] = [];
|
|
676
811
|
}
|
|
@@ -687,14 +822,14 @@ var FileManager = class {
|
|
|
687
822
|
}
|
|
688
823
|
this.memoryCache = data;
|
|
689
824
|
}
|
|
690
|
-
this.scheduleWrite();
|
|
825
|
+
this.scheduleWrite(channelKey);
|
|
691
826
|
}
|
|
692
827
|
dispose() {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.
|
|
828
|
+
for (const [channelKey, timer] of this.writeTimers.entries()) {
|
|
829
|
+
timer();
|
|
830
|
+
this.flushPendingMessages(channelKey);
|
|
696
831
|
}
|
|
697
|
-
this.
|
|
832
|
+
this.writeTimers.clear();
|
|
698
833
|
}
|
|
699
834
|
logInfo(...args) {
|
|
700
835
|
if (this.config.loggerinfo) {
|
|
@@ -733,8 +868,8 @@ var ApiHandlers = class {
|
|
|
733
868
|
});
|
|
734
869
|
this.ctx.console.addListener("get-chat-data", async () => {
|
|
735
870
|
try {
|
|
736
|
-
const data = this.fileManager.
|
|
737
|
-
this.logInfo("
|
|
871
|
+
const data = this.fileManager.readMetadataOnly();
|
|
872
|
+
this.logInfo("获取基础聊天数据(仅元数据)");
|
|
738
873
|
return {
|
|
739
874
|
success: true,
|
|
740
875
|
data: {
|
|
@@ -742,6 +877,7 @@ var ApiHandlers = class {
|
|
|
742
877
|
channels: data.channels || {},
|
|
743
878
|
pinnedBots: data.pinnedBots || [],
|
|
744
879
|
pinnedChannels: data.pinnedChannels || [],
|
|
880
|
+
// 不返回消息数据,由前端按需加载
|
|
745
881
|
messages: {}
|
|
746
882
|
}
|
|
747
883
|
};
|
|
@@ -752,9 +888,7 @@ var ApiHandlers = class {
|
|
|
752
888
|
});
|
|
753
889
|
this.ctx.console.addListener("get-history-messages", async (requestData) => {
|
|
754
890
|
try {
|
|
755
|
-
|
|
756
|
-
const channelKey = `${requestData.selfId}:${requestData.channelId}`;
|
|
757
|
-
let messages = data.messages[channelKey] || [];
|
|
891
|
+
let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId);
|
|
758
892
|
const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp);
|
|
759
893
|
if (requestData.limit !== void 0) {
|
|
760
894
|
const limit = requestData.limit;
|
|
@@ -764,7 +898,7 @@ var ApiHandlers = class {
|
|
|
764
898
|
} else {
|
|
765
899
|
messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
766
900
|
}
|
|
767
|
-
this.logInfo("获取历史消息:",
|
|
901
|
+
this.logInfo("获取历史消息:", `${requestData.selfId}:${requestData.channelId}`, "共", messages.length, "条消息");
|
|
768
902
|
return {
|
|
769
903
|
success: true,
|
|
770
904
|
messages,
|
|
@@ -797,61 +931,72 @@ var ApiHandlers = class {
|
|
|
797
931
|
});
|
|
798
932
|
this.ctx.console.addListener("fetch-image", async (data) => {
|
|
799
933
|
try {
|
|
934
|
+
if (data.url.includes("/vite/@fs/")) {
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
viteUrl: data.url
|
|
938
|
+
};
|
|
939
|
+
}
|
|
800
940
|
if (this.isFileUrl(data.url)) {
|
|
801
941
|
this.logInfo("处理本地文件请求:", data.url);
|
|
802
|
-
|
|
942
|
+
const filePath2 = require("node:url").fileURLToPath(data.url);
|
|
943
|
+
const normalizedPath2 = filePath2.replace(/\\/g, "/");
|
|
944
|
+
return {
|
|
945
|
+
success: true,
|
|
946
|
+
viteUrl: `/vite/@fs/${normalizedPath2}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "images");
|
|
950
|
+
if (!require("node:fs").existsSync(dir)) {
|
|
951
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
803
952
|
}
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
953
|
+
const crypto = require("node:crypto");
|
|
954
|
+
const hash = crypto.createHash("md5").update(data.url).digest("hex");
|
|
955
|
+
const ext = require("node:path").extname(new import_node_url2.URL(data.url).pathname) || ".jpg";
|
|
956
|
+
const filename = `${hash}${ext}`;
|
|
957
|
+
const filePath = require("node:path").join(dir, filename);
|
|
958
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
959
|
+
const response = await fetch(data.url, {
|
|
960
|
+
headers: {
|
|
961
|
+
"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",
|
|
962
|
+
"Referer": ""
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
if (!response.ok) {
|
|
966
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
808
967
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
968
|
+
const buffer = await response.arrayBuffer();
|
|
969
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
812
970
|
}
|
|
813
|
-
const
|
|
814
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
815
|
-
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
971
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
816
972
|
return {
|
|
817
973
|
success: true,
|
|
818
|
-
|
|
819
|
-
contentType,
|
|
820
|
-
dataUrl: `data:${contentType};base64,${base64}`
|
|
974
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
821
975
|
};
|
|
822
976
|
} catch (error) {
|
|
977
|
+
this.logger.error("获取图片失败:", error);
|
|
823
978
|
return { success: false, error: error?.message || String(error) };
|
|
824
979
|
}
|
|
825
980
|
});
|
|
826
981
|
this.ctx.console.addListener("clear-channel-history", async (data) => {
|
|
827
982
|
try {
|
|
828
|
-
this.logInfo("
|
|
983
|
+
this.logInfo("收到清理历史记录请求(已废弃,建议使用删除频道数据):", data);
|
|
829
984
|
const chatData = this.fileManager.readChatDataFromFile();
|
|
830
985
|
const channelKey = `${data.selfId}:${data.channelId}`;
|
|
831
986
|
if (!chatData.messages[channelKey]) {
|
|
832
987
|
return { success: true, message: "频道没有历史消息" };
|
|
833
988
|
}
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
const keepCount = data.keepCount || this.config.keepMessagesOnClear;
|
|
837
|
-
if (keepCount > 0 && originalCount <= keepCount) {
|
|
838
|
-
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` };
|
|
839
|
-
}
|
|
840
|
-
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
841
|
-
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : [];
|
|
842
|
-
const clearedCount = originalCount - keptMessages.length;
|
|
843
|
-
chatData.messages[channelKey] = keptMessages;
|
|
989
|
+
const originalCount = chatData.messages[channelKey].length;
|
|
990
|
+
delete chatData.messages[channelKey];
|
|
844
991
|
this.fileManager.writeChatDataToFile(chatData);
|
|
845
|
-
this.logInfo(`频道 ${channelKey}
|
|
846
|
-
|
|
847
|
-
保留消息数: keptMessages.length,
|
|
848
|
-
清理消息数: clearedCount
|
|
992
|
+
this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
|
|
993
|
+
清理消息数: originalCount
|
|
849
994
|
});
|
|
850
995
|
return {
|
|
851
996
|
success: true,
|
|
852
|
-
message: `成功清理 ${
|
|
853
|
-
clearedCount,
|
|
854
|
-
keptCount:
|
|
997
|
+
message: `成功清理 ${originalCount} 条历史消息`,
|
|
998
|
+
clearedCount: originalCount,
|
|
999
|
+
keptCount: 0
|
|
855
1000
|
};
|
|
856
1001
|
} catch (error) {
|
|
857
1002
|
this.logger.error("清理频道历史记录失败:", error);
|
|
@@ -1063,7 +1208,6 @@ var ApiHandlers = class {
|
|
|
1063
1208
|
success: true,
|
|
1064
1209
|
config: {
|
|
1065
1210
|
maxMessagesPerChannel: this.config.maxMessagesPerChannel,
|
|
1066
|
-
keepMessagesOnClear: this.config.keepMessagesOnClear,
|
|
1067
1211
|
maxPersistImages: this.config.maxPersistImages,
|
|
1068
1212
|
loggerinfo: this.config.loggerinfo,
|
|
1069
1213
|
blockedPlatforms: this.config.blockedPlatforms || [],
|
|
@@ -1148,29 +1292,47 @@ var ApiHandlers = class {
|
|
|
1148
1292
|
this.ctx.console.addListener("fetch-video-temp", async (data) => {
|
|
1149
1293
|
try {
|
|
1150
1294
|
this.logInfo("收到视频临时加载请求:", data.url);
|
|
1295
|
+
if (data.url.includes("/vite/@fs/")) {
|
|
1296
|
+
return {
|
|
1297
|
+
success: true,
|
|
1298
|
+
viteUrl: data.url
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1151
1301
|
if (this.isFileUrl(data.url)) {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1302
|
+
const filePath2 = require("node:url").fileURLToPath(data.url);
|
|
1303
|
+
const normalizedPath2 = filePath2.replace(/\\/g, "/");
|
|
1304
|
+
return {
|
|
1305
|
+
success: true,
|
|
1306
|
+
viteUrl: `/vite/@fs/${normalizedPath2}`
|
|
1307
|
+
};
|
|
1154
1308
|
}
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1309
|
+
const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "media");
|
|
1310
|
+
if (!require("node:fs").existsSync(dir)) {
|
|
1311
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
1312
|
+
}
|
|
1313
|
+
const crypto = require("node:crypto");
|
|
1314
|
+
const hash = crypto.createHash("md5").update(data.url).digest("hex");
|
|
1315
|
+
const ext = require("node:path").extname(new import_node_url2.URL(data.url).pathname) || ".mp4";
|
|
1316
|
+
const filename = `${hash}${ext}`;
|
|
1317
|
+
const filePath = require("node:path").join(dir, filename);
|
|
1318
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
1319
|
+
const response = await fetch(data.url, {
|
|
1320
|
+
headers: {
|
|
1321
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
1322
|
+
"Referer": ""
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
if (!response.ok) {
|
|
1326
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1159
1327
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1328
|
+
const buffer = await response.arrayBuffer();
|
|
1329
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
1330
|
+
this.logInfo("视频下载成功:", { size: buffer.byteLength, path: filePath });
|
|
1163
1331
|
}
|
|
1164
|
-
const
|
|
1165
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
1166
|
-
const contentType = response.headers.get("content-type") || "video/mp4";
|
|
1167
|
-
this.logInfo("视频下载成功:", { size: buffer.byteLength, contentType });
|
|
1332
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
1168
1333
|
return {
|
|
1169
1334
|
success: true,
|
|
1170
|
-
|
|
1171
|
-
contentType,
|
|
1172
|
-
dataUrl: `data:${contentType};base64,${base64}`,
|
|
1173
|
-
size: buffer.byteLength
|
|
1335
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
1174
1336
|
};
|
|
1175
1337
|
} catch (error) {
|
|
1176
1338
|
this.logger.error("视频临时加载失败:", error);
|
|
@@ -1255,7 +1417,6 @@ var import_koishi2 = require("koishi");
|
|
|
1255
1417
|
var Config = import_koishi2.Schema.intersect([
|
|
1256
1418
|
import_koishi2.Schema.object({
|
|
1257
1419
|
maxMessagesPerChannel: import_koishi2.Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(1500).step(1),
|
|
1258
|
-
keepMessagesOnClear: import_koishi2.Schema.number().default(50).description("手动清理历史记录时保留的消息数量").min(0).max(1e3).step(1),
|
|
1259
1420
|
maxPersistImages: import_koishi2.Schema.number().default(100).description("持久化存储的图片缓存数量").min(10).max(500).step(1),
|
|
1260
1421
|
blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
|
|
1261
1422
|
platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
|
package/lib/message-handler.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class MessageHandler {
|
|
|
15
15
|
getCorrectChannelId(selfId: string): string | undefined;
|
|
16
16
|
updateBotInfoToFile(session: Session): void;
|
|
17
17
|
updateChannelInfoToFile(session: Session): string;
|
|
18
|
-
downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar'): Promise<
|
|
18
|
+
downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar'): Promise<string>;
|
|
19
19
|
private processMediaElementsAsync;
|
|
20
20
|
private processUserMessage;
|
|
21
21
|
private processBotMessage;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-chat-patch",
|
|
3
3
|
"description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.4",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|