koishi-plugin-best-cave 2.7.7 → 2.7.9
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/Utils.d.ts +2 -2
- package/lib/index.js +117 -59
- package/package.json +1 -1
package/lib/Utils.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function buildCaveMessage(cave: CaveObject, config: Config, fileM
|
|
|
20
20
|
* @param logger 日志记录器实例。
|
|
21
21
|
* @param reusableIds 可复用 ID 的内存缓存。
|
|
22
22
|
*/
|
|
23
|
-
export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger, reusableIds: Set<number>): Promise<void>;
|
|
23
|
+
export declare function cleanupPendingDeletions(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reusableIds: Set<number>): Promise<void>;
|
|
24
24
|
/**
|
|
25
25
|
* @description 根据配置和会话,生成数据库查询的范围条件。
|
|
26
26
|
* @param session 当前会话。
|
|
@@ -81,7 +81,7 @@ export declare function performSimilarityChecks(ctx: Context, config: Config, ha
|
|
|
81
81
|
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, cave: CaveObject, downloadedMedia: {
|
|
82
82
|
fileName: string;
|
|
83
83
|
buffer: Buffer;
|
|
84
|
-
}[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'
|
|
84
|
+
}[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'>;
|
|
85
85
|
/**
|
|
86
86
|
* @description 校验会话是否来自指定的管理群组。
|
|
87
87
|
* @param session 当前会话。
|
package/lib/index.js
CHANGED
|
@@ -321,7 +321,7 @@ async function buildCaveMessage(cave, config, fileManager, logger2, platform, pr
|
|
|
321
321
|
return [finalInitialMessage, ...followUpMessages].filter((msg) => msg.length > 0);
|
|
322
322
|
}
|
|
323
323
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
324
|
-
async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
324
|
+
async function cleanupPendingDeletions(ctx, config, fileManager, logger2, reusableIds) {
|
|
325
325
|
try {
|
|
326
326
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
327
327
|
if (!cavesToDelete.length) return;
|
|
@@ -331,7 +331,7 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
|
331
331
|
idsToDelete.forEach((id) => reusableIds.add(id));
|
|
332
332
|
await ctx.database.remove("cave", { id: { $in: idsToDelete } });
|
|
333
333
|
await ctx.database.remove("cave_hash", { cave: { $in: idsToDelete } });
|
|
334
|
-
await ctx.database.remove("cave_meta", { cave: { $in: idsToDelete } });
|
|
334
|
+
if (config.enableAI) await ctx.database.remove("cave_meta", { cave: { $in: idsToDelete } });
|
|
335
335
|
} catch (error) {
|
|
336
336
|
logger2.error("清理回声洞时发生错误:", error);
|
|
337
337
|
}
|
|
@@ -492,8 +492,8 @@ async function handleFileUploads(ctx, config, fileManager, logger2, cave, downlo
|
|
|
492
492
|
} catch (fileProcessingError) {
|
|
493
493
|
logger2.error(`回声洞(${cave.id})文件处理失败:`, fileProcessingError);
|
|
494
494
|
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
495
|
-
cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds);
|
|
496
|
-
|
|
495
|
+
cleanupPendingDeletions(ctx, config, fileManager, logger2, reusableIds);
|
|
496
|
+
throw fileProcessingError;
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
__name(handleFileUploads, "handleFileUploads");
|
|
@@ -655,7 +655,7 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
655
655
|
if (cavesToProcess.length === 0) return `回声洞(${idsToProcess.join("|")})无需审核或不存在`;
|
|
656
656
|
const processedIds = cavesToProcess.map((cave2) => cave2.id);
|
|
657
657
|
await this.ctx.database.upsert("cave", processedIds.map((id) => ({ id, status: targetStatus })));
|
|
658
|
-
if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
|
|
658
|
+
if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.config, this.fileManager, this.logger, this.reusableIds);
|
|
659
659
|
return `已${actionText}回声洞(${processedIds.join("|")})`;
|
|
660
660
|
} catch (error) {
|
|
661
661
|
this.logger.error(`审核操作失败:`, error);
|
|
@@ -722,42 +722,29 @@ var HashManager = class {
|
|
|
722
722
|
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
723
723
|
try {
|
|
724
724
|
const allCaves = await this.ctx.database.get("cave", { status: "active" });
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
725
|
+
const existingHashes = await this.ctx.database.get("cave_hash", {}, { fields: ["cave"] });
|
|
726
|
+
const hashedCaveIds = new Set(existingHashes.map((h4) => h4.cave));
|
|
727
|
+
const cavesToProcess = allCaves.filter((cave2) => !hashedCaveIds.has(cave2.id));
|
|
728
|
+
if (cavesToProcess.length === 0) return "无需补全回声洞哈希";
|
|
729
|
+
await session.send(`开始补全 ${cavesToProcess.length} 个回声洞的哈希...`);
|
|
730
|
+
const hashesToInsert = [];
|
|
730
731
|
let processedCaveCount = 0;
|
|
731
|
-
let totalHashesGenerated = 0;
|
|
732
732
|
let errorCount = 0;
|
|
733
|
-
const
|
|
734
|
-
if (hashesToInsert.length === 0) return;
|
|
735
|
-
await this.ctx.database.upsert("cave_hash", hashesToInsert);
|
|
736
|
-
totalHashesGenerated += hashesToInsert.length;
|
|
737
|
-
this.logger.info(`[${processedCaveCount}/${allCaves.length}] 正在导入 ${hashesToInsert.length} 条回声洞哈希...`);
|
|
738
|
-
hashesToInsert = [];
|
|
739
|
-
}, "flushBatch");
|
|
740
|
-
for (const cave2 of allCaves) {
|
|
733
|
+
for (const cave2 of cavesToProcess) {
|
|
741
734
|
processedCaveCount++;
|
|
742
735
|
try {
|
|
743
736
|
const newHashesForCave = await this.generateAllHashesForCave(cave2);
|
|
744
|
-
|
|
745
|
-
const uniqueKey = `${hashObj.cave}-${hashObj.hash}-${hashObj.type}`;
|
|
746
|
-
if (!existingHashSet.has(uniqueKey)) {
|
|
747
|
-
hashesToInsert.push(hashObj);
|
|
748
|
-
existingHashSet.add(uniqueKey);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (hashesToInsert.length >= 100) await flushBatch();
|
|
737
|
+
if (newHashesForCave.length > 0) hashesToInsert.push(...newHashesForCave);
|
|
752
738
|
} catch (error) {
|
|
753
739
|
errorCount++;
|
|
754
740
|
this.logger.warn(`补全回声洞(${cave2.id})哈希时出错: ${error.message}`);
|
|
755
741
|
}
|
|
756
742
|
}
|
|
757
|
-
await
|
|
758
|
-
|
|
743
|
+
if (hashesToInsert.length > 0) await this.ctx.database.upsert("cave_hash", hashesToInsert);
|
|
744
|
+
const successCount = processedCaveCount - errorCount;
|
|
745
|
+
return `已补全 ${successCount} 个回声洞的 ${hashesToInsert.length} 条哈希(失败 ${errorCount} 条)`;
|
|
759
746
|
} catch (error) {
|
|
760
|
-
this.logger.error("
|
|
747
|
+
this.logger.error("补全哈希失败:", error);
|
|
761
748
|
return `操作失败: ${error.message}`;
|
|
762
749
|
}
|
|
763
750
|
});
|
|
@@ -768,33 +755,41 @@ var HashManager = class {
|
|
|
768
755
|
const textThreshold = options.textThreshold ?? this.config.textThreshold;
|
|
769
756
|
const imageThreshold = options.imageThreshold ?? this.config.imageThreshold;
|
|
770
757
|
const allHashes = await this.ctx.database.get("cave_hash", {});
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
for (const
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
758
|
+
if (allHashes.length < 2) return "无可比较哈希";
|
|
759
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
760
|
+
const hashLookup = /* @__PURE__ */ new Map();
|
|
761
|
+
for (const hashObj of allHashes) {
|
|
762
|
+
if (!hashLookup.has(hashObj.cave)) hashLookup.set(hashObj.cave, {});
|
|
763
|
+
hashLookup.get(hashObj.cave)[hashObj.type] = hashObj.hash;
|
|
764
|
+
const binHash = BigInt("0x" + hashObj.hash).toString(2).padStart(64, "0");
|
|
765
|
+
for (let i = 0; i < 4; i++) {
|
|
766
|
+
const band = binHash.substring(i * 16, (i + 1) * 16);
|
|
767
|
+
const bucketKey = `${hashObj.type}:${i}:${band}`;
|
|
768
|
+
if (!buckets.has(bucketKey)) buckets.set(bucketKey, []);
|
|
769
|
+
buckets.get(bucketKey).push(hashObj.cave);
|
|
779
770
|
}
|
|
780
771
|
}
|
|
772
|
+
const candidatePairs = /* @__PURE__ */ new Set();
|
|
781
773
|
const similarPairs = { text: /* @__PURE__ */ new Set(), image: /* @__PURE__ */ new Set() };
|
|
782
|
-
for (
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (
|
|
774
|
+
for (const ids of buckets.values()) {
|
|
775
|
+
if (ids.length < 2) continue;
|
|
776
|
+
for (let i = 0; i < ids.length; i++) {
|
|
777
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
778
|
+
const id1 = ids[i];
|
|
779
|
+
const id2 = ids[j];
|
|
780
|
+
const pairKey = [id1, id2].sort((a, b) => a - b).join("-");
|
|
781
|
+
if (candidatePairs.has(pairKey)) continue;
|
|
782
|
+
candidatePairs.add(pairKey);
|
|
783
|
+
const cave1Hashes = hashLookup.get(id1);
|
|
784
|
+
const cave2Hashes = hashLookup.get(id2);
|
|
785
|
+
if (cave1Hashes?.text && cave2Hashes?.text) {
|
|
786
|
+
const similarity = this.calculateSimilarity(cave1Hashes.text, cave2Hashes.text);
|
|
787
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
788
|
+
}
|
|
789
|
+
if (cave1Hashes?.image && cave2Hashes?.image) {
|
|
790
|
+
const similarity = this.calculateSimilarity(cave1Hashes.image, cave2Hashes.image);
|
|
791
|
+
if (similarity >= imageThreshold) similarPairs.image.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
792
|
+
}
|
|
798
793
|
}
|
|
799
794
|
}
|
|
800
795
|
}
|
|
@@ -809,6 +804,68 @@ var HashManager = class {
|
|
|
809
804
|
return `检查失败: ${error.message}`;
|
|
810
805
|
}
|
|
811
806
|
});
|
|
807
|
+
cave.subcommand(".fix [...ids:posint]", "修复回声洞", { hidden: true, authority: 3 }).usage("扫描并修复回声洞中的图片,可指定一个或多个 ID。").action(async ({ session }, ...ids) => {
|
|
808
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
809
|
+
let cavesToProcess;
|
|
810
|
+
try {
|
|
811
|
+
await session.send("正在修复,请稍候...");
|
|
812
|
+
if (ids.length === 0) {
|
|
813
|
+
cavesToProcess = await this.ctx.database.get("cave", { status: "active" });
|
|
814
|
+
} else {
|
|
815
|
+
cavesToProcess = await this.ctx.database.get("cave", { id: { $in: ids }, status: "active" });
|
|
816
|
+
}
|
|
817
|
+
if (!cavesToProcess.length) return "无可修复的回声洞";
|
|
818
|
+
let fixedFiles = 0;
|
|
819
|
+
let errorCount = 0;
|
|
820
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
821
|
+
const JPEG_SIGNATURE = Buffer.from([255, 216]);
|
|
822
|
+
const GIF_SIGNATURE = Buffer.from("GIF");
|
|
823
|
+
for (const cave2 of cavesToProcess) {
|
|
824
|
+
const imageElements = cave2.elements.filter((el) => el.type === "image" && el.file);
|
|
825
|
+
for (const element of imageElements) {
|
|
826
|
+
try {
|
|
827
|
+
const originalBuffer = await this.fileManager.readFile(element.file);
|
|
828
|
+
let sanitizedBuffer = originalBuffer;
|
|
829
|
+
if (originalBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
|
|
830
|
+
const IEND_CHUNK = Buffer.from("IEND");
|
|
831
|
+
const iendIndex = originalBuffer.lastIndexOf(IEND_CHUNK);
|
|
832
|
+
if (iendIndex !== -1) {
|
|
833
|
+
const endOfPngData = iendIndex + 8;
|
|
834
|
+
if (originalBuffer.length > endOfPngData) sanitizedBuffer = originalBuffer.slice(0, endOfPngData);
|
|
835
|
+
}
|
|
836
|
+
} else if (originalBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
|
|
837
|
+
const EOI_MARKER = Buffer.from([255, 217]);
|
|
838
|
+
const eoiIndex = originalBuffer.lastIndexOf(EOI_MARKER);
|
|
839
|
+
if (eoiIndex !== -1) {
|
|
840
|
+
const endOfJpegData = eoiIndex + 2;
|
|
841
|
+
if (originalBuffer.length > endOfJpegData) sanitizedBuffer = originalBuffer.slice(0, endOfJpegData);
|
|
842
|
+
}
|
|
843
|
+
} else if (originalBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
|
|
844
|
+
const GIF_TERMINATOR = Buffer.from([59]);
|
|
845
|
+
const terminatorIndex = originalBuffer.lastIndexOf(GIF_TERMINATOR);
|
|
846
|
+
if (terminatorIndex !== -1) {
|
|
847
|
+
const endOfGifData = terminatorIndex + 1;
|
|
848
|
+
if (originalBuffer.length > endOfGifData) sanitizedBuffer = originalBuffer.slice(0, endOfGifData);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (!originalBuffer.equals(sanitizedBuffer)) {
|
|
852
|
+
await this.fileManager.saveFile(element.file, sanitizedBuffer);
|
|
853
|
+
fixedFiles++;
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (error.code !== "ENOENT" && error.name !== "NoSuchKey") {
|
|
857
|
+
this.logger.warn(`无法修复回声洞(${cave2.id})的图片(${element.file}):`, error);
|
|
858
|
+
errorCount++;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return `已修复 ${cavesToProcess.length} 个回声洞的 ${fixedFiles} 张图片(失败 ${errorCount} 条)`;
|
|
864
|
+
} catch (error) {
|
|
865
|
+
this.logger.error("修复图像文件时发生严重错误:", error);
|
|
866
|
+
return `操作失败: ${error.message}`;
|
|
867
|
+
}
|
|
868
|
+
});
|
|
812
869
|
}
|
|
813
870
|
/**
|
|
814
871
|
* @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
|
|
@@ -835,8 +892,9 @@ var HashManager = class {
|
|
|
835
892
|
const imageBuffer = await this.fileManager.readFile(el.file);
|
|
836
893
|
const imageHash = await this.generatePHash(imageBuffer);
|
|
837
894
|
addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
|
|
838
|
-
} catch (
|
|
839
|
-
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`,
|
|
895
|
+
} catch (error) {
|
|
896
|
+
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, error);
|
|
897
|
+
throw error;
|
|
840
898
|
}
|
|
841
899
|
}
|
|
842
900
|
return tempHashes;
|
|
@@ -1279,7 +1337,7 @@ function apply(ctx, config) {
|
|
|
1279
1337
|
if (staleCaves.length > 0) {
|
|
1280
1338
|
const idsToMark = staleCaves.map((c) => ({ id: c.id, status: "delete" }));
|
|
1281
1339
|
await ctx.database.upsert("cave", idsToMark);
|
|
1282
|
-
await cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1340
|
+
await cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1283
1341
|
}
|
|
1284
1342
|
} catch (error) {
|
|
1285
1343
|
logger.error("清理残留回声洞时发生错误:", error);
|
|
@@ -1353,7 +1411,7 @@ function apply(ctx, config) {
|
|
|
1353
1411
|
time: creationTime
|
|
1354
1412
|
});
|
|
1355
1413
|
if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
|
|
1356
|
-
if (finalStatus !== "preload"
|
|
1414
|
+
if (finalStatus !== "preload") {
|
|
1357
1415
|
newCave.status = finalStatus;
|
|
1358
1416
|
if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
|
|
1359
1417
|
if (hashManager) {
|
|
@@ -1390,8 +1448,8 @@ function apply(ctx, config) {
|
|
|
1390
1448
|
if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
|
|
1391
1449
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1392
1450
|
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1393
|
-
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1394
1451
|
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1452
|
+
cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1395
1453
|
} catch (error) {
|
|
1396
1454
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1397
1455
|
return "删除失败,请稍后再试";
|