koishi-plugin-best-cave 2.7.7 → 2.7.8
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 +105 -34
- 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);
|
|
@@ -768,33 +768,41 @@ var HashManager = class {
|
|
|
768
768
|
const textThreshold = options.textThreshold ?? this.config.textThreshold;
|
|
769
769
|
const imageThreshold = options.imageThreshold ?? this.config.imageThreshold;
|
|
770
770
|
const allHashes = await this.ctx.database.get("cave_hash", {});
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
for (const
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
771
|
+
if (allHashes.length < 2) return "无可比较哈希";
|
|
772
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
773
|
+
const hashLookup = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const hashObj of allHashes) {
|
|
775
|
+
if (!hashLookup.has(hashObj.cave)) hashLookup.set(hashObj.cave, {});
|
|
776
|
+
hashLookup.get(hashObj.cave)[hashObj.type] = hashObj.hash;
|
|
777
|
+
const binHash = BigInt("0x" + hashObj.hash).toString(2).padStart(64, "0");
|
|
778
|
+
for (let i = 0; i < 4; i++) {
|
|
779
|
+
const band = binHash.substring(i * 16, (i + 1) * 16);
|
|
780
|
+
const bucketKey = `${hashObj.type}:${i}:${band}`;
|
|
781
|
+
if (!buckets.has(bucketKey)) buckets.set(bucketKey, []);
|
|
782
|
+
buckets.get(bucketKey).push(hashObj.cave);
|
|
779
783
|
}
|
|
780
784
|
}
|
|
785
|
+
const candidatePairs = /* @__PURE__ */ new Set();
|
|
781
786
|
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 (
|
|
787
|
+
for (const ids of buckets.values()) {
|
|
788
|
+
if (ids.length < 2) continue;
|
|
789
|
+
for (let i = 0; i < ids.length; i++) {
|
|
790
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
791
|
+
const id1 = ids[i];
|
|
792
|
+
const id2 = ids[j];
|
|
793
|
+
const pairKey = [id1, id2].sort((a, b) => a - b).join("-");
|
|
794
|
+
if (candidatePairs.has(pairKey)) continue;
|
|
795
|
+
candidatePairs.add(pairKey);
|
|
796
|
+
const cave1Hashes = hashLookup.get(id1);
|
|
797
|
+
const cave2Hashes = hashLookup.get(id2);
|
|
798
|
+
if (cave1Hashes?.text && cave2Hashes?.text) {
|
|
799
|
+
const similarity = this.calculateSimilarity(cave1Hashes.text, cave2Hashes.text);
|
|
800
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
801
|
+
}
|
|
802
|
+
if (cave1Hashes?.image && cave2Hashes?.image) {
|
|
803
|
+
const similarity = this.calculateSimilarity(cave1Hashes.image, cave2Hashes.image);
|
|
804
|
+
if (similarity >= imageThreshold) similarPairs.image.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
805
|
+
}
|
|
798
806
|
}
|
|
799
807
|
}
|
|
800
808
|
}
|
|
@@ -809,6 +817,68 @@ var HashManager = class {
|
|
|
809
817
|
return `检查失败: ${error.message}`;
|
|
810
818
|
}
|
|
811
819
|
});
|
|
820
|
+
cave.subcommand(".fix [...ids:posint]", "修复回声洞", { hidden: true, authority: 3 }).usage("扫描并修复回声洞中的图片,可指定一个或多个 ID。").action(async ({ session }, ...ids) => {
|
|
821
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
822
|
+
let cavesToProcess;
|
|
823
|
+
try {
|
|
824
|
+
if (ids.length === 0) {
|
|
825
|
+
await session.send("正在修复,请稍候...");
|
|
826
|
+
cavesToProcess = await this.ctx.database.get("cave", { status: "active" });
|
|
827
|
+
} else {
|
|
828
|
+
cavesToProcess = await this.ctx.database.get("cave", { id: { $in: ids }, status: "active" });
|
|
829
|
+
}
|
|
830
|
+
if (!cavesToProcess.length) return "无可修复回声洞";
|
|
831
|
+
let fixedFiles = 0;
|
|
832
|
+
let errorCount = 0;
|
|
833
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
834
|
+
const JPEG_SIGNATURE = Buffer.from([255, 216]);
|
|
835
|
+
const GIF_SIGNATURE = Buffer.from("GIF");
|
|
836
|
+
for (const cave2 of cavesToProcess) {
|
|
837
|
+
const imageElements = cave2.elements.filter((el) => el.type === "image" && el.file);
|
|
838
|
+
for (const element of imageElements) {
|
|
839
|
+
try {
|
|
840
|
+
const originalBuffer = await this.fileManager.readFile(element.file);
|
|
841
|
+
let sanitizedBuffer = originalBuffer;
|
|
842
|
+
if (originalBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
|
|
843
|
+
const IEND_CHUNK = Buffer.from("IEND");
|
|
844
|
+
const iendIndex = originalBuffer.lastIndexOf(IEND_CHUNK);
|
|
845
|
+
if (iendIndex !== -1) {
|
|
846
|
+
const endOfPngData = iendIndex + 8;
|
|
847
|
+
if (originalBuffer.length > endOfPngData) sanitizedBuffer = originalBuffer.slice(0, endOfPngData);
|
|
848
|
+
}
|
|
849
|
+
} else if (originalBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
|
|
850
|
+
const EOI_MARKER = Buffer.from([255, 217]);
|
|
851
|
+
const eoiIndex = originalBuffer.lastIndexOf(EOI_MARKER);
|
|
852
|
+
if (eoiIndex !== -1) {
|
|
853
|
+
const endOfJpegData = eoiIndex + 2;
|
|
854
|
+
if (originalBuffer.length > endOfJpegData) sanitizedBuffer = originalBuffer.slice(0, endOfJpegData);
|
|
855
|
+
}
|
|
856
|
+
} else if (originalBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
|
|
857
|
+
const GIF_TERMINATOR = Buffer.from([59]);
|
|
858
|
+
const terminatorIndex = originalBuffer.lastIndexOf(GIF_TERMINATOR);
|
|
859
|
+
if (terminatorIndex !== -1) {
|
|
860
|
+
const endOfGifData = terminatorIndex + 1;
|
|
861
|
+
if (originalBuffer.length > endOfGifData) sanitizedBuffer = originalBuffer.slice(0, endOfGifData);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (!originalBuffer.equals(sanitizedBuffer)) {
|
|
865
|
+
await this.fileManager.saveFile(element.file, sanitizedBuffer);
|
|
866
|
+
fixedFiles++;
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error.code !== "ENOENT" && error.name !== "NoSuchKey") {
|
|
870
|
+
this.logger.warn(`无法修复回声洞(${cave2.id})的图片(${element.file}):`, error);
|
|
871
|
+
errorCount++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return `已修复 ${cavesToProcess.length} 个回声洞的 ${fixedFiles} 张图片(失败 ${errorCount} 条)`;
|
|
877
|
+
} catch (error) {
|
|
878
|
+
this.logger.error("修复图像文件时发生严重错误:", error);
|
|
879
|
+
return `操作失败: ${error.message}`;
|
|
880
|
+
}
|
|
881
|
+
});
|
|
812
882
|
}
|
|
813
883
|
/**
|
|
814
884
|
* @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
|
|
@@ -835,8 +905,9 @@ var HashManager = class {
|
|
|
835
905
|
const imageBuffer = await this.fileManager.readFile(el.file);
|
|
836
906
|
const imageHash = await this.generatePHash(imageBuffer);
|
|
837
907
|
addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
|
|
838
|
-
} catch (
|
|
839
|
-
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`,
|
|
908
|
+
} catch (error) {
|
|
909
|
+
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, error);
|
|
910
|
+
throw error;
|
|
840
911
|
}
|
|
841
912
|
}
|
|
842
913
|
return tempHashes;
|
|
@@ -1279,7 +1350,7 @@ function apply(ctx, config) {
|
|
|
1279
1350
|
if (staleCaves.length > 0) {
|
|
1280
1351
|
const idsToMark = staleCaves.map((c) => ({ id: c.id, status: "delete" }));
|
|
1281
1352
|
await ctx.database.upsert("cave", idsToMark);
|
|
1282
|
-
await cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1353
|
+
await cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1283
1354
|
}
|
|
1284
1355
|
} catch (error) {
|
|
1285
1356
|
logger.error("清理残留回声洞时发生错误:", error);
|
|
@@ -1353,7 +1424,7 @@ function apply(ctx, config) {
|
|
|
1353
1424
|
time: creationTime
|
|
1354
1425
|
});
|
|
1355
1426
|
if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
|
|
1356
|
-
if (finalStatus !== "preload"
|
|
1427
|
+
if (finalStatus !== "preload") {
|
|
1357
1428
|
newCave.status = finalStatus;
|
|
1358
1429
|
if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
|
|
1359
1430
|
if (hashManager) {
|
|
@@ -1390,8 +1461,8 @@ function apply(ctx, config) {
|
|
|
1390
1461
|
if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
|
|
1391
1462
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1392
1463
|
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1393
|
-
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1394
1464
|
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1465
|
+
cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1395
1466
|
} catch (error) {
|
|
1396
1467
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1397
1468
|
return "删除失败,请稍后再试";
|