koishi-plugin-chat-patch 2.3.0 → 2.4.0
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/dist/index.js +2 -2
- package/lib/file-manager.d.ts +11 -4
- package/lib/index.js +181 -52
- package/package.json +1 -1
- package/src/file-manager.ts +209 -57
package/lib/file-manager.d.ts
CHANGED
|
@@ -4,16 +4,23 @@ 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
|
+
private readChannelMessages;
|
|
20
|
+
private writeChannelMessages;
|
|
21
|
+
private readMetadata;
|
|
22
|
+
private writeMetadata;
|
|
23
|
+
private loadAllChannelMessages;
|
|
17
24
|
readChatDataFromFile(): ChatData;
|
|
18
25
|
writeChatDataToFile(data: ChatData): void;
|
|
19
26
|
private scheduleWrite;
|
package/lib/index.js
CHANGED
|
@@ -540,27 +540,149 @@ var FileManager = class {
|
|
|
540
540
|
constructor(ctx, config) {
|
|
541
541
|
this.ctx = ctx;
|
|
542
542
|
this.config = config;
|
|
543
|
-
|
|
543
|
+
const baseDir = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch");
|
|
544
|
+
this.chatHistoryDir = import_node_path2.default.join(baseDir, "chat-history");
|
|
545
|
+
this.metadataFilePath = import_node_path2.default.join(baseDir, "metadata.json");
|
|
544
546
|
this.logger = ctx.logger("chat-patch");
|
|
545
547
|
this.utils = new Utils(config);
|
|
548
|
+
this.cleanupOldDataFiles(baseDir);
|
|
546
549
|
this.memoryCache = this.readChatDataFromFile();
|
|
547
550
|
}
|
|
548
551
|
static {
|
|
549
552
|
__name(this, "FileManager");
|
|
550
553
|
}
|
|
551
|
-
|
|
552
|
-
|
|
554
|
+
chatHistoryDir;
|
|
555
|
+
// 聊天记录根目录
|
|
556
|
+
metadataFilePath;
|
|
557
|
+
// 元数据文件路径(存储bots、channels、pinned等信息)
|
|
553
558
|
logger;
|
|
554
559
|
utils;
|
|
555
560
|
memoryCache = null;
|
|
556
|
-
pendingMessages =
|
|
557
|
-
|
|
561
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
562
|
+
// 按channelKey分组的待写入消息
|
|
563
|
+
writeTimers = /* @__PURE__ */ new Map();
|
|
564
|
+
// 每个频道独立的写入定时器
|
|
558
565
|
WRITE_DEBOUNCE_MS = 1e3;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
566
|
+
// 清理旧版本的JSON文件
|
|
567
|
+
cleanupOldDataFiles(baseDir) {
|
|
568
|
+
const oldFiles = ["chat-data.json", "data.json", "messages.json"];
|
|
569
|
+
for (const fileName of oldFiles) {
|
|
570
|
+
const filePath = import_node_path2.default.join(baseDir, fileName);
|
|
571
|
+
if (import_node_fs2.default.existsSync(filePath)) {
|
|
572
|
+
try {
|
|
573
|
+
import_node_fs2.default.unlinkSync(filePath);
|
|
574
|
+
this.logger.info(`已删除旧版本数据文件: ${fileName}`);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
this.logger.warn(`删除旧版本数据文件失败: ${fileName}`, error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// 确保目录存在
|
|
582
|
+
ensureDir(dirPath) {
|
|
583
|
+
if (!import_node_fs2.default.existsSync(dirPath)) {
|
|
584
|
+
import_node_fs2.default.mkdirSync(dirPath, { recursive: true });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// 获取频道消息文件路径
|
|
588
|
+
getChannelFilePath(selfId, channelId) {
|
|
589
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, selfId);
|
|
590
|
+
this.ensureDir(botDir);
|
|
591
|
+
return import_node_path2.default.join(botDir, `${channelId}.json`);
|
|
592
|
+
}
|
|
593
|
+
// 读取单个频道的消息
|
|
594
|
+
readChannelMessages(selfId, channelId) {
|
|
595
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
596
|
+
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const jsonData = import_node_fs2.default.readFileSync(filePath, "utf8");
|
|
601
|
+
const messages = JSON.parse(jsonData);
|
|
602
|
+
return Array.isArray(messages) ? messages : [];
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// 写入单个频道的消息
|
|
609
|
+
writeChannelMessages(selfId, channelId, messages) {
|
|
610
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
611
|
+
try {
|
|
612
|
+
const jsonData = JSON.stringify(messages, null, 2);
|
|
613
|
+
import_node_fs2.default.writeFileSync(filePath, jsonData, "utf8");
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.logger.error(`写入频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// 读取元数据(bots、channels、pinned等)
|
|
619
|
+
readMetadata() {
|
|
620
|
+
if (!import_node_fs2.default.existsSync(this.metadataFilePath)) {
|
|
621
|
+
return {
|
|
622
|
+
bots: {},
|
|
623
|
+
channels: {},
|
|
624
|
+
pinnedBots: [],
|
|
625
|
+
pinnedChannels: []
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const jsonData = import_node_fs2.default.readFileSync(this.metadataFilePath, "utf8");
|
|
630
|
+
const data = JSON.parse(jsonData);
|
|
631
|
+
return {
|
|
632
|
+
bots: data.bots || {},
|
|
633
|
+
channels: data.channels || {},
|
|
634
|
+
pinnedBots: data.pinnedBots || [],
|
|
635
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
636
|
+
lastSaveTime: data.lastSaveTime
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.logger.error("读取元数据失败:", error);
|
|
640
|
+
return {
|
|
641
|
+
bots: {},
|
|
642
|
+
channels: {},
|
|
643
|
+
pinnedBots: [],
|
|
644
|
+
pinnedChannels: []
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// 写入元数据
|
|
649
|
+
writeMetadata(metadata) {
|
|
650
|
+
try {
|
|
651
|
+
this.ensureDir(import_node_path2.default.dirname(this.metadataFilePath));
|
|
652
|
+
const dataToWrite = {
|
|
653
|
+
...metadata,
|
|
654
|
+
lastSaveTime: Date.now()
|
|
655
|
+
};
|
|
656
|
+
const jsonData = JSON.stringify(dataToWrite, null, 2);
|
|
657
|
+
import_node_fs2.default.writeFileSync(this.metadataFilePath, jsonData, "utf8");
|
|
658
|
+
} catch (error) {
|
|
659
|
+
this.logger.error("写入元数据失败:", error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// 扫描所有频道消息文件并加载到内存
|
|
663
|
+
loadAllChannelMessages() {
|
|
664
|
+
const messages = {};
|
|
665
|
+
if (!import_node_fs2.default.existsSync(this.chatHistoryDir)) {
|
|
666
|
+
return messages;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const botDirs = import_node_fs2.default.readdirSync(this.chatHistoryDir);
|
|
670
|
+
for (const botId of botDirs) {
|
|
671
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, botId);
|
|
672
|
+
const stat = import_node_fs2.default.statSync(botDir);
|
|
673
|
+
if (!stat.isDirectory()) continue;
|
|
674
|
+
const channelFiles = import_node_fs2.default.readdirSync(botDir);
|
|
675
|
+
for (const fileName of channelFiles) {
|
|
676
|
+
if (!fileName.endsWith(".json")) continue;
|
|
677
|
+
const channelId = fileName.replace(".json", "");
|
|
678
|
+
const channelKey = `${botId}:${channelId}`;
|
|
679
|
+
messages[channelKey] = this.readChannelMessages(botId, channelId);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} catch (error) {
|
|
683
|
+
this.logger.error("加载频道消息失败:", error);
|
|
563
684
|
}
|
|
685
|
+
return messages;
|
|
564
686
|
}
|
|
565
687
|
readChatDataFromFile() {
|
|
566
688
|
if (this.memoryCache) {
|
|
@@ -568,18 +690,12 @@ var FileManager = class {
|
|
|
568
690
|
}
|
|
569
691
|
process.nextTick(() => {
|
|
570
692
|
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
|
-
}
|
|
693
|
+
const metadata = this.readMetadata();
|
|
694
|
+
const messages = this.loadAllChannelMessages();
|
|
695
|
+
this.memoryCache = {
|
|
696
|
+
...metadata,
|
|
697
|
+
messages
|
|
698
|
+
};
|
|
583
699
|
} catch (error) {
|
|
584
700
|
this.logger.error("读取聊天数据失败:", error);
|
|
585
701
|
}
|
|
@@ -598,36 +714,45 @@ var FileManager = class {
|
|
|
598
714
|
this.memoryCache = data;
|
|
599
715
|
process.nextTick(() => {
|
|
600
716
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
717
|
+
const { messages, ...metadata } = data;
|
|
718
|
+
this.writeMetadata(metadata);
|
|
719
|
+
for (const [channelKey, channelMessages] of Object.entries(messages)) {
|
|
720
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
721
|
+
if (selfId && channelId) {
|
|
722
|
+
this.writeChannelMessages(selfId, channelId, channelMessages);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
604
725
|
} catch (error) {
|
|
605
726
|
this.logger.error("写入聊天数据失败:", error);
|
|
606
727
|
}
|
|
607
728
|
});
|
|
608
729
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
730
|
+
// 为特定频道安排写入
|
|
731
|
+
scheduleWrite(channelKey) {
|
|
732
|
+
const existingTimer = this.writeTimers.get(channelKey);
|
|
733
|
+
if (existingTimer) {
|
|
734
|
+
existingTimer();
|
|
613
735
|
}
|
|
614
|
-
|
|
736
|
+
const timer = this.ctx.setTimeout(() => {
|
|
615
737
|
process.nextTick(() => {
|
|
616
|
-
this.flushPendingMessages();
|
|
738
|
+
this.flushPendingMessages(channelKey);
|
|
617
739
|
});
|
|
618
|
-
this.
|
|
740
|
+
this.writeTimers.delete(channelKey);
|
|
619
741
|
}, this.WRITE_DEBOUNCE_MS);
|
|
742
|
+
this.writeTimers.set(channelKey, timer);
|
|
620
743
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const messagesToWrite =
|
|
624
|
-
|
|
744
|
+
// 刷新特定频道的待写入消息
|
|
745
|
+
flushPendingMessages(channelKey) {
|
|
746
|
+
const messagesToWrite = this.pendingMessages.get(channelKey);
|
|
747
|
+
if (!messagesToWrite || messagesToWrite.length === 0) return;
|
|
748
|
+
this.pendingMessages.delete(channelKey);
|
|
625
749
|
const data = this.memoryCache || this.readChatDataFromFile();
|
|
750
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
751
|
+
if (!selfId || !channelId) return;
|
|
752
|
+
if (!data.messages[channelKey]) {
|
|
753
|
+
data.messages[channelKey] = [];
|
|
754
|
+
}
|
|
626
755
|
for (const messageInfo of messagesToWrite) {
|
|
627
|
-
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
628
|
-
if (!data.messages[channelKey]) {
|
|
629
|
-
data.messages[channelKey] = [];
|
|
630
|
-
}
|
|
631
756
|
const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
|
|
632
757
|
if (existingMessage) {
|
|
633
758
|
continue;
|
|
@@ -637,13 +762,14 @@ var FileManager = class {
|
|
|
637
762
|
}
|
|
638
763
|
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
|
|
639
764
|
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
765
|
}
|
|
645
|
-
this.
|
|
646
|
-
|
|
766
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
767
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
768
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
769
|
+
}
|
|
770
|
+
this.writeChannelMessages(selfId, channelId, data.messages[channelKey]);
|
|
771
|
+
this.memoryCache = data;
|
|
772
|
+
this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`);
|
|
647
773
|
}
|
|
648
774
|
cleanExcessMessages(data) {
|
|
649
775
|
let cleanedCount = 0;
|
|
@@ -668,9 +794,12 @@ var FileManager = class {
|
|
|
668
794
|
};
|
|
669
795
|
}
|
|
670
796
|
async addMessageToFile(messageInfo) {
|
|
671
|
-
this.pendingMessages.push(messageInfo);
|
|
672
|
-
const data = this.memoryCache || this.readChatDataFromFile();
|
|
673
797
|
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
798
|
+
if (!this.pendingMessages.has(channelKey)) {
|
|
799
|
+
this.pendingMessages.set(channelKey, []);
|
|
800
|
+
}
|
|
801
|
+
this.pendingMessages.get(channelKey).push(messageInfo);
|
|
802
|
+
const data = this.memoryCache || this.readChatDataFromFile();
|
|
674
803
|
if (!data.messages[channelKey]) {
|
|
675
804
|
data.messages[channelKey] = [];
|
|
676
805
|
}
|
|
@@ -687,14 +816,14 @@ var FileManager = class {
|
|
|
687
816
|
}
|
|
688
817
|
this.memoryCache = data;
|
|
689
818
|
}
|
|
690
|
-
this.scheduleWrite();
|
|
819
|
+
this.scheduleWrite(channelKey);
|
|
691
820
|
}
|
|
692
821
|
dispose() {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.
|
|
822
|
+
for (const [channelKey, timer] of this.writeTimers.entries()) {
|
|
823
|
+
timer();
|
|
824
|
+
this.flushPendingMessages(channelKey);
|
|
696
825
|
}
|
|
697
|
-
this.
|
|
826
|
+
this.writeTimers.clear();
|
|
698
827
|
}
|
|
699
828
|
logInfo(...args) {
|
|
700
829
|
if (this.config.loggerinfo) {
|
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.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|