koishi-plugin-best-cave 2.7.6 → 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.
@@ -7,7 +7,7 @@ import { FileManager } from './FileManager';
7
7
  export interface CaveHashObject {
8
8
  cave: number;
9
9
  hash: string;
10
- type: 'simhash' | 'phash';
10
+ type: 'text' | 'image';
11
11
  }
12
12
  /**
13
13
  * @class HashManager
@@ -32,11 +32,6 @@ export declare class HashManager {
32
32
  * @param cave - 主 `cave` 命令实例。
33
33
  */
34
34
  registerCommands(cave: any): void;
35
- /**
36
- * @description 检查数据库中所有回声洞,为没有哈希记录的历史数据生成哈希。
37
- * @returns 一个包含操作结果的报告字符串。
38
- */
39
- generateHashesForHistoricalCaves(): Promise<string>;
40
35
  /**
41
36
  * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
42
37
  * @param cave - 回声洞对象。
@@ -44,27 +39,24 @@ export declare class HashManager {
44
39
  */
45
40
  generateAllHashesForCave(cave: Pick<CaveObject, 'id' | 'elements'>): Promise<CaveHashObject[]>;
46
41
  /**
47
- * @description 对数据库中所有哈希进行两两比较,找出相似度过高的内容。
48
- * @param options 包含临时阈值的可选对象。
49
- * @returns 一个包含检查结果的报告字符串。
42
+ * @description 执行一维离散余弦变换 (DCT-II) 的方法。
43
+ * @param input - 输入的数字数组。
44
+ * @returns DCT 变换后的数组。
50
45
  */
51
- checkForSimilarCaves(options?: {
52
- textThreshold?: number;
53
- imageThreshold?: number;
54
- }): Promise<string>;
46
+ private dct1D;
55
47
  /**
56
- * @description 执行二维离散余弦变换 (DCT-II)
48
+ * @description 执行二维离散余弦变换 (DCT-II) 的方法。
49
+ * 通过对行和列分别应用一维 DCT 来实现。
57
50
  * @param matrix - 输入的 N x N 像素亮度矩阵。
58
- * @returns DCT变换后的 N x N 系数矩阵。
51
+ * @returns DCT 变换后的 N x N 系数矩阵。
59
52
  */
60
- private _dct2D;
53
+ private dct2D;
61
54
  /**
62
- * @description pHash 算法核心实现。
63
- * @param imageBuffer - 图片的Buffer。
64
- * @param size - 期望的哈希位数 (必须是完全平方数, 如 64 256)。
65
- * @returns 十六进制pHash字符串。
55
+ * @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
56
+ * @param imageBuffer - 图片的 Buffer。
57
+ * @returns 64位十六进制 pHash 字符串。
66
58
  */
67
- generatePHash(imageBuffer: Buffer, size: number): Promise<string>;
59
+ generatePHash(imageBuffer: Buffer): Promise<string>;
68
60
  /**
69
61
  * @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
70
62
  * @param hex1 - 第一个哈希。
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' | 'delete'>;
84
+ }[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'>;
85
85
  /**
86
86
  * @description 校验会话是否来自指定的管理群组。
87
87
  * @param session 当前会话。
package/lib/index.d.ts CHANGED
@@ -61,7 +61,7 @@ export interface Config {
61
61
  aiEndpoint?: string;
62
62
  aiApiKey?: string;
63
63
  aiModel?: string;
64
- aiTPM?: number;
64
+ aiRPM?: number;
65
65
  AnalysePrompt?: string;
66
66
  aiCheckPrompt?: string;
67
67
  aiAnalyseSchema?: string;
package/lib/index.js CHANGED
@@ -37,7 +37,7 @@ __export(src_exports, {
37
37
  usage: () => usage
38
38
  });
39
39
  module.exports = __toCommonJS(src_exports);
40
- var import_koishi4 = require("koishi");
40
+ var import_koishi3 = require("koishi");
41
41
 
42
42
  // src/FileManager.ts
43
43
  var import_client_s3 = require("@aws-sdk/client-s3");
@@ -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
  }
@@ -457,25 +457,25 @@ async function performSimilarityChecks(ctx, config, hashManager, finalElementsFo
457
457
  if (combinedText) {
458
458
  const newSimhash = hashManager.generateTextSimhash(combinedText);
459
459
  if (newSimhash) {
460
- const existingTextHashes = await ctx.database.get("cave_hash", { type: "simhash" });
460
+ const existingTextHashes = await ctx.database.get("cave_hash", { type: "text" });
461
461
  for (const existing of existingTextHashes) {
462
462
  const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
463
463
  if (similarity >= config.textThreshold) return { duplicate: true, message: `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
464
464
  }
465
- textHashesToStore.push({ hash: newSimhash, type: "simhash" });
465
+ textHashesToStore.push({ hash: newSimhash, type: "text" });
466
466
  }
467
467
  }
468
468
  if (downloadedMedia.length > 0) {
469
- const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "phash" });
469
+ const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "image" });
470
470
  for (const media of downloadedMedia) {
471
471
  if ([".png", ".jpg", ".jpeg", ".webp"].includes(path2.extname(media.fileName).toLowerCase())) {
472
- const imageHash = await hashManager.generatePHash(media.buffer, 256);
472
+ const imageHash = await hashManager.generatePHash(media.buffer);
473
473
  for (const existing of allExistingImageHashes) {
474
474
  const similarity = hashManager.calculateSimilarity(imageHash, existing.hash);
475
475
  if (similarity >= config.imageThreshold) return { duplicate: true, message: `图片与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
476
476
  }
477
- imageHashesToStore.push({ hash: imageHash, type: "phash" });
478
- allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "phash" });
477
+ imageHashesToStore.push({ hash: imageHash, type: "image" });
478
+ allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "image" });
479
479
  }
480
480
  }
481
481
  }
@@ -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
- return "delete";
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);
@@ -686,7 +686,7 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
686
686
  };
687
687
 
688
688
  // src/HashManager.ts
689
- var import_sharp = __toESM(require("sharp"));
689
+ var import_jimp = __toESM(require("jimp"));
690
690
  var crypto = __toESM(require("crypto"));
691
691
  var HashManager = class {
692
692
  /**
@@ -718,73 +718,167 @@ var HashManager = class {
718
718
  * @param cave - 主 `cave` 命令实例。
719
719
  */
720
720
  registerCommands(cave) {
721
- const adminCheck = /* @__PURE__ */ __name(({ session }) => {
722
- const adminChannelId = this.config.adminChannel?.split(":")[1];
723
- if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
724
- }, "adminCheck");
725
- cave.subcommand(".hash", "校验回声洞", { hidden: true, authority: 3 }).usage("校验缺失哈希的回声洞,补全哈希记录。").action(async (argv) => {
726
- const checkResult = adminCheck(argv);
727
- if (checkResult) return checkResult;
728
- await argv.session.send("正在处理,请稍候...");
721
+ cave.subcommand(".hash", "校验回声洞", { hidden: true, authority: 3 }).usage("校验缺失哈希的回声洞,补全哈希记录。").action(async ({ session }) => {
722
+ if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
729
723
  try {
730
- return await this.generateHashesForHistoricalCaves();
724
+ const allCaves = await this.ctx.database.get("cave", { status: "active" });
725
+ if (allCaves.length === 0) return "无需补全回声洞哈希";
726
+ await session.send(`开始补全 ${allCaves.length} 个回声洞的哈希...`);
727
+ const existingHashes = await this.ctx.database.get("cave_hash", {});
728
+ const existingHashSet = new Set(existingHashes.map((h4) => `${h4.cave}-${h4.hash}-${h4.type}`));
729
+ let hashesToInsert = [];
730
+ let processedCaveCount = 0;
731
+ let totalHashesGenerated = 0;
732
+ let errorCount = 0;
733
+ const flushBatch = /* @__PURE__ */ __name(async () => {
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) {
741
+ processedCaveCount++;
742
+ try {
743
+ const newHashesForCave = await this.generateAllHashesForCave(cave2);
744
+ for (const hashObj of newHashesForCave) {
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();
752
+ } catch (error) {
753
+ errorCount++;
754
+ this.logger.warn(`补全回声洞(${cave2.id})哈希时出错: ${error.message}`);
755
+ }
756
+ }
757
+ await flushBatch();
758
+ return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
731
759
  } catch (error) {
732
- this.logger.error("生成历史哈希失败:", error);
760
+ this.logger.error("生成哈希失败:", error);
733
761
  return `操作失败: ${error.message}`;
734
762
  }
735
763
  });
736
- cave.subcommand(".check", "检查相似度", { hidden: true }).usage("检查所有回声洞,找出相似度过高的内容。").option("textThreshold", "-t <threshold:number> 文本相似度阈值 (%)").option("imageThreshold", "-i <threshold:number> 图片相似度阈值 (%)").action(async (argv) => {
737
- const checkResult = adminCheck(argv);
738
- if (checkResult) return checkResult;
739
- await argv.session.send("正在检查,请稍候...");
764
+ cave.subcommand(".check", "检查相似度", { hidden: true }).usage("检查所有回声洞,找出相似度过高的内容。").option("textThreshold", "-t <threshold:number> 文本相似度阈值 (%)").option("imageThreshold", "-i <threshold:number> 图片相似度阈值 (%)").action(async ({ session, options }) => {
765
+ if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
766
+ await session.send("正在检查,请稍候...");
740
767
  try {
741
- return await this.checkForSimilarCaves(argv.options);
768
+ const textThreshold = options.textThreshold ?? this.config.textThreshold;
769
+ const imageThreshold = options.imageThreshold ?? this.config.imageThreshold;
770
+ const allHashes = await this.ctx.database.get("cave_hash", {});
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);
783
+ }
784
+ }
785
+ const candidatePairs = /* @__PURE__ */ new Set();
786
+ const similarPairs = { text: /* @__PURE__ */ new Set(), image: /* @__PURE__ */ new Set() };
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
+ }
806
+ }
807
+ }
808
+ }
809
+ const totalFindings = similarPairs.text.size + similarPairs.image.size;
810
+ if (totalFindings === 0) return "未发现高相似度的内容";
811
+ let report = `已发现 ${totalFindings} 组高相似度的内容:`;
812
+ if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
813
+ if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
814
+ return report.trim();
742
815
  } catch (error) {
743
816
  this.logger.error("检查相似度失败:", error);
744
817
  return `检查失败: ${error.message}`;
745
818
  }
746
819
  });
747
- }
748
- /**
749
- * @description 检查数据库中所有回声洞,为没有哈希记录的历史数据生成哈希。
750
- * @returns 一个包含操作结果的报告字符串。
751
- */
752
- async generateHashesForHistoricalCaves() {
753
- const allCaves = await this.ctx.database.get("cave", { status: "active" });
754
- const existingHashes = await this.ctx.database.get("cave_hash", {});
755
- const existingHashSet = new Set(existingHashes.map((h5) => `${h5.cave}-${h5.hash}-${h5.type}`));
756
- if (allCaves.length === 0) return "无需补全回声洞哈希";
757
- this.logger.info(`开始补全 ${allCaves.length} 个回声洞的哈希...`);
758
- let hashesToInsert = [];
759
- let processedCaveCount = 0;
760
- let totalHashesGenerated = 0;
761
- let errorCount = 0;
762
- const flushBatch = /* @__PURE__ */ __name(async () => {
763
- if (hashesToInsert.length === 0) return;
764
- await this.ctx.database.upsert("cave_hash", hashesToInsert);
765
- totalHashesGenerated += hashesToInsert.length;
766
- this.logger.info(`[${processedCaveCount}/${allCaves.length}] 正在导入 ${hashesToInsert.length} 条回声洞哈希...`);
767
- hashesToInsert = [];
768
- }, "flushBatch");
769
- for (const cave of allCaves) {
770
- processedCaveCount++;
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;
771
823
  try {
772
- const newHashesForCave = await this.generateAllHashesForCave(cave);
773
- for (const hashObj of newHashesForCave) {
774
- const uniqueKey = `${hashObj.cave}-${hashObj.hash}-${hashObj.type}`;
775
- if (!existingHashSet.has(uniqueKey)) {
776
- hashesToInsert.push(hashObj);
777
- existingHashSet.add(uniqueKey);
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
+ }
778
874
  }
779
875
  }
780
- if (hashesToInsert.length >= 100) await flushBatch();
876
+ return `已修复 ${cavesToProcess.length} 个回声洞的 ${fixedFiles} 张图片(失败 ${errorCount} 条)`;
781
877
  } catch (error) {
782
- errorCount++;
783
- this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
878
+ this.logger.error("修复图像文件时发生严重错误:", error);
879
+ return `操作失败: ${error.message}`;
784
880
  }
785
- }
786
- await flushBatch();
787
- return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
881
+ });
788
882
  }
789
883
  /**
790
884
  * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
@@ -804,115 +898,72 @@ var HashManager = class {
804
898
  const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
805
899
  if (combinedText) {
806
900
  const textHash = this.generateTextSimhash(combinedText);
807
- if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "simhash" });
901
+ if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "text" });
808
902
  }
809
903
  for (const el of cave.elements.filter((el2) => el2.type === "image" && el2.file)) {
810
904
  try {
811
905
  const imageBuffer = await this.fileManager.readFile(el.file);
812
- const imageHash = await this.generatePHash(imageBuffer, 256);
813
- addUniqueHash({ cave: cave.id, hash: imageHash, type: "phash" });
814
- } catch (e) {
815
- this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, e);
906
+ const imageHash = await this.generatePHash(imageBuffer);
907
+ addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
908
+ } catch (error) {
909
+ this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, error);
910
+ throw error;
816
911
  }
817
912
  }
818
913
  return tempHashes;
819
914
  }
820
915
  /**
821
- * @description 对数据库中所有哈希进行两两比较,找出相似度过高的内容。
822
- * @param options 包含临时阈值的可选对象。
823
- * @returns 一个包含检查结果的报告字符串。
916
+ * @description 执行一维离散余弦变换 (DCT-II) 的方法。
917
+ * @param input - 输入的数字数组。
918
+ * @returns DCT 变换后的数组。
824
919
  */
825
- async checkForSimilarCaves(options = {}) {
826
- const textThreshold = options.textThreshold ?? this.config.textThreshold;
827
- const imageThreshold = options.imageThreshold ?? this.config.imageThreshold;
828
- const allHashes = await this.ctx.database.get("cave_hash", {});
829
- const allCaveIds = [...new Set(allHashes.map((h5) => h5.cave))];
830
- const textHashes = /* @__PURE__ */ new Map();
831
- const imageHashes = /* @__PURE__ */ new Map();
832
- for (const hash of allHashes) {
833
- if (hash.type === "simhash") {
834
- textHashes.set(hash.cave, hash.hash);
835
- } else if (hash.type === "phash") {
836
- imageHashes.set(hash.cave, hash.hash);
837
- }
920
+ dct1D(input) {
921
+ const N = input.length;
922
+ const output = new Array(N).fill(0);
923
+ const c0 = 1 / Math.sqrt(2);
924
+ for (let k = 0; k < N; k++) {
925
+ let sum = 0;
926
+ for (let n = 0; n < N; n++) sum += input[n] * Math.cos(Math.PI * (2 * n + 1) * k / (2 * N));
927
+ const ck = k === 0 ? c0 : 1;
928
+ output[k] = Math.sqrt(2 / N) * ck * sum;
838
929
  }
839
- const similarPairs = {
840
- text: /* @__PURE__ */ new Set(),
841
- image: /* @__PURE__ */ new Set()
842
- };
843
- for (let i = 0; i < allCaveIds.length; i++) {
844
- for (let j = i + 1; j < allCaveIds.length; j++) {
845
- const id1 = allCaveIds[i];
846
- const id2 = allCaveIds[j];
847
- const pair = [id1, id2].sort((a, b) => a - b).join(" & ");
848
- const text1 = textHashes.get(id1);
849
- const text2 = textHashes.get(id2);
850
- if (text1 && text2) {
851
- const similarity = this.calculateSimilarity(text1, text2);
852
- if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
853
- }
854
- const image1 = imageHashes.get(id1);
855
- const image2 = imageHashes.get(id2);
856
- if (image1 && image2) {
857
- const similarity = this.calculateSimilarity(image1, image2);
858
- if (similarity >= imageThreshold) similarPairs.image.add(`${pair} = ${similarity.toFixed(2)}%`);
859
- }
860
- }
861
- }
862
- const totalFindings = similarPairs.text.size + similarPairs.image.size;
863
- if (totalFindings === 0) return "未发现高相似度的内容";
864
- let report = `已发现 ${totalFindings} 组高相似度的内容:`;
865
- if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
866
- if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
867
- return report.trim();
930
+ return output;
868
931
  }
869
932
  /**
870
- * @description 执行二维离散余弦变换 (DCT-II)
933
+ * @description 执行二维离散余弦变换 (DCT-II) 的方法。
934
+ * 通过对行和列分别应用一维 DCT 来实现。
871
935
  * @param matrix - 输入的 N x N 像素亮度矩阵。
872
- * @returns DCT变换后的 N x N 系数矩阵。
936
+ * @returns DCT 变换后的 N x N 系数矩阵。
873
937
  */
874
- _dct2D(matrix) {
938
+ dct2D(matrix) {
875
939
  const N = matrix.length;
876
940
  if (N === 0) return [];
877
- const cosines = Array.from(
878
- { length: N },
879
- (_, i) => Array.from({ length: N }, (_2, j) => Math.cos(Math.PI * (2 * i + 1) * j / (2 * N)))
880
- );
881
- const applyDct1D = /* @__PURE__ */ __name((input) => {
882
- const output = new Array(N).fill(0);
883
- const scale = Math.sqrt(2 / N);
884
- for (let k = 0; k < N; k++) {
885
- let sum = 0;
886
- for (let n = 0; n < N; n++) sum += input[n] * cosines[n][k];
887
- output[k] = scale * sum;
888
- }
889
- output[0] /= Math.sqrt(2);
890
- return output;
891
- }, "applyDct1D");
892
- const tempMatrix = matrix.map((row) => applyDct1D(row));
893
- const transposed = tempMatrix[0].map((_, col) => tempMatrix.map((row) => row[col]));
894
- const dctResult = transposed.map((row) => applyDct1D(row));
895
- return dctResult[0].map((_, col) => dctResult.map((row) => row[col]));
941
+ const tempMatrix = matrix.map((row) => this.dct1D(row));
942
+ const transposed = tempMatrix.map((_, colIndex) => tempMatrix.map((row) => row[colIndex]));
943
+ const dctResultTransposed = transposed.map((row) => this.dct1D(row));
944
+ const dctResult = dctResultTransposed.map((_, colIndex) => dctResultTransposed.map((row) => row[colIndex]));
945
+ return dctResult;
896
946
  }
897
947
  /**
898
- * @description pHash 算法核心实现。
899
- * @param imageBuffer - 图片的Buffer。
900
- * @param size - 期望的哈希位数 (必须是完全平方数, 如 64 256)。
901
- * @returns 十六进制pHash字符串。
948
+ * @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
949
+ * @param imageBuffer - 图片的 Buffer。
950
+ * @returns 64位十六进制 pHash 字符串。
902
951
  */
903
- async generatePHash(imageBuffer, size) {
904
- const dctSize = 32;
905
- const hashGridSize = Math.sqrt(size);
906
- if (!Number.isInteger(hashGridSize)) throw new Error("哈希位数必须是完全平方数");
907
- const pixels = await (0, import_sharp.default)(imageBuffer).grayscale().resize(dctSize, dctSize, { fit: "fill" }).raw().toBuffer();
908
- const matrix = [];
909
- for (let y = 0; y < dctSize; y++) matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
910
- const dctMatrix = this._dct2D(matrix);
952
+ async generatePHash(imageBuffer) {
953
+ const image = await import_jimp.default.read(imageBuffer);
954
+ image.resize(32, 32, import_jimp.default.RESIZE_BILINEAR).greyscale();
955
+ const matrix = Array.from({ length: 32 }, () => new Array(32).fill(0));
956
+ image.scan(0, 0, 32, 32, (x, y, idx) => {
957
+ matrix[y][x] = image.bitmap.data[idx];
958
+ });
959
+ const dctMatrix = this.dct2D(matrix);
911
960
  const coefficients = [];
912
- for (let y = 0; y < hashGridSize; y++) for (let x = 0; x < hashGridSize; x++) coefficients.push(dctMatrix[y][x]);
913
- const median = [...coefficients.slice(1)].sort((a, b) => a - b)[Math.floor((coefficients.length - 1) / 2)];
914
- const binaryHash = coefficients.map((val) => val > median ? "1" : "0").join("");
915
- return BigInt("0b" + binaryHash).toString(16).padStart(size / 4, "0");
961
+ for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) coefficients.push(dctMatrix[y][x]);
962
+ const acCoefficients = coefficients.slice(1);
963
+ const average = acCoefficients.reduce((sum, val) => sum + val, 0) / acCoefficients.length;
964
+ let binaryHash = "";
965
+ for (const val of coefficients) binaryHash += val > average ? "1" : "0";
966
+ return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
916
967
  }
917
968
  /**
918
969
  * @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
@@ -922,8 +973,10 @@ var HashManager = class {
922
973
  */
923
974
  calculateHammingDistance(hex1, hex2) {
924
975
  let distance = 0;
925
- const bin1 = hexToBinary(hex1);
926
- const bin2 = hexToBinary(hex2);
976
+ let bin1 = "";
977
+ for (const char of hex1) bin1 += parseInt(char, 16).toString(2).padStart(4, "0");
978
+ let bin2 = "";
979
+ for (const char of hex2) bin2 += parseInt(char, 16).toString(2).padStart(4, "0");
927
980
  const len = Math.min(bin1.length, bin2.length);
928
981
  for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
929
982
  return distance;
@@ -947,14 +1000,8 @@ var HashManager = class {
947
1000
  generateTextSimhash(text) {
948
1001
  const cleanText = (text || "").toLowerCase().replace(/\s+/g, "");
949
1002
  if (!cleanText) return "";
950
- const n = 2;
951
- const tokens = /* @__PURE__ */ new Set();
952
- if (cleanText.length < n) {
953
- tokens.add(cleanText);
954
- } else {
955
- for (let i = 0; i <= cleanText.length - n; i++) tokens.add(cleanText.substring(i, i + n));
956
- }
957
- const tokenArray = Array.from(tokens);
1003
+ const tokens = Array.from(cleanText);
1004
+ const tokenArray = Array.from(new Set(tokens));
958
1005
  if (tokenArray.length === 0) return "";
959
1006
  const vector = new Array(64).fill(0);
960
1007
  tokenArray.forEach((token) => {
@@ -965,15 +1012,8 @@ var HashManager = class {
965
1012
  return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
966
1013
  }
967
1014
  };
968
- function hexToBinary(hex) {
969
- let bin = "";
970
- for (const char of hex) bin += parseInt(char, 16).toString(2).padStart(4, "0");
971
- return bin;
972
- }
973
- __name(hexToBinary, "hexToBinary");
974
1015
 
975
1016
  // src/AIManager.ts
976
- var import_koishi3 = require("koishi");
977
1017
  var path3 = __toESM(require("path"));
978
1018
  var AIManager = class {
979
1019
  /**
@@ -1015,40 +1055,21 @@ var AIManager = class {
1015
1055
  if (cavesToAnalyze.length === 0) return "无需分析回声洞";
1016
1056
  await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
1017
1057
  let successCount = 0;
1018
- let failedCount = 0;
1019
1058
  for (const [index, cave2] of cavesToAnalyze.entries()) {
1020
- this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
1021
1059
  try {
1060
+ this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
1022
1061
  await this.analyzeAndStore(cave2);
1023
1062
  successCount++;
1024
1063
  } catch (error) {
1025
- failedCount++;
1026
- this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
1064
+ return `分析回声洞(${cave2.id})时出错: ${error.message}`;
1027
1065
  }
1028
1066
  }
1029
- return `已分析 ${successCount} 个回声洞(失败 ${failedCount} 个)`;
1067
+ return `已分析 ${successCount} 个回声洞`;
1030
1068
  } catch (error) {
1031
1069
  this.logger.error("分析回声洞失败:", error);
1032
1070
  return `操作失败: ${error.message}`;
1033
1071
  }
1034
1072
  });
1035
- cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
1036
- if (!id) return "请输入要查看的回声洞序号";
1037
- try {
1038
- const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
1039
- if (!meta) return `回声洞(${id})尚未分析`;
1040
- const report = [
1041
- `回声洞(${id})分析结果:`,
1042
- `描述:${meta.description}`,
1043
- `关键词:${meta.keywords.join(", ")}`,
1044
- `综合评分:${meta.rating}/100`
1045
- ];
1046
- return import_koishi3.h.text(report.join("\n"));
1047
- } catch (error) {
1048
- this.logger.error(`查询回声洞(${id})失败:`, error);
1049
- return "查询失败,请稍后再试";
1050
- }
1051
- });
1052
1073
  }
1053
1074
  /**
1054
1075
  * @description 对新提交的内容执行 AI 驱动的查重检查。
@@ -1170,17 +1191,26 @@ var AIManager = class {
1170
1191
  this.rateLimitResetTime = now + 6e4;
1171
1192
  this.requestCount = 0;
1172
1193
  }
1173
- if (this.requestCount >= this.config.aiTPM) {
1194
+ if (this.requestCount >= this.config.aiRPM) {
1174
1195
  const delay = this.rateLimitResetTime - now;
1175
1196
  await new Promise((resolve) => setTimeout(resolve, delay));
1176
1197
  this.rateLimitResetTime = Date.now() + 6e4;
1177
1198
  this.requestCount = 0;
1178
1199
  }
1179
1200
  let schema = JSON.parse(schemaString);
1201
+ const toolName = "extract_data";
1180
1202
  const payload = {
1181
1203
  model: this.config.aiModel,
1182
1204
  messages: [{ role: "system", content: systemPrompt }, ...messages],
1183
- response_format: { type: "json_schema", strict: true, schema }
1205
+ tools: [{
1206
+ type: "function",
1207
+ function: {
1208
+ name: toolName,
1209
+ description: "根据提供的内容提取或分析信息。",
1210
+ parameters: schema
1211
+ }
1212
+ }],
1213
+ tool_choice: { type: "function", function: { name: toolName } }
1184
1214
  };
1185
1215
  const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1186
1216
  const headers = {
@@ -1190,7 +1220,13 @@ var AIManager = class {
1190
1220
  try {
1191
1221
  this.requestCount++;
1192
1222
  const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1193
- return JSON.parse(response.choices[0].message.content);
1223
+ const toolCall = response.choices?.[0]?.message?.tool_calls?.[0];
1224
+ if (toolCall?.function?.arguments) {
1225
+ return JSON.parse(toolCall.function.arguments);
1226
+ } else {
1227
+ this.logger.error("AI 响应格式不正确:", JSON.stringify(response));
1228
+ throw new Error("AI 响应格式不正确");
1229
+ }
1194
1230
  } catch (error) {
1195
1231
  const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
1196
1232
  this.logger.error(`请求 API 失败: ${errorMessage}`);
@@ -1214,29 +1250,29 @@ var usage = `
1214
1250
  <p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
1215
1251
  </div>
1216
1252
  `;
1217
- var logger = new import_koishi4.Logger("best-cave");
1218
- var Config = import_koishi4.Schema.intersect([
1219
- import_koishi4.Schema.object({
1220
- perChannel: import_koishi4.Schema.boolean().default(false).description("启用分群模式"),
1221
- enableName: import_koishi4.Schema.boolean().default(false).description("启用自定义昵称"),
1222
- enableIO: import_koishi4.Schema.boolean().default(false).description("启用导入导出"),
1223
- adminChannel: import_koishi4.Schema.string().default("onebot:").description("管理群组 ID"),
1224
- caveFormat: import_koishi4.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
1253
+ var logger = new import_koishi3.Logger("best-cave");
1254
+ var Config = import_koishi3.Schema.intersect([
1255
+ import_koishi3.Schema.object({
1256
+ perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
1257
+ enableName: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
1258
+ enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
1259
+ adminChannel: import_koishi3.Schema.string().default("onebot:").description("管理群组 ID"),
1260
+ caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
1225
1261
  }).description("基础配置"),
1226
- import_koishi4.Schema.object({
1227
- enablePend: import_koishi4.Schema.boolean().default(false).description("启用审核"),
1228
- enableSimilarity: import_koishi4.Schema.boolean().default(false).description("启用查重"),
1229
- textThreshold: import_koishi4.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
1230
- imageThreshold: import_koishi4.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
1262
+ import_koishi3.Schema.object({
1263
+ enablePend: import_koishi3.Schema.boolean().default(false).description("启用审核"),
1264
+ enableSimilarity: import_koishi3.Schema.boolean().default(false).description("启用查重"),
1265
+ textThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
1266
+ imageThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
1231
1267
  }).description("复核配置"),
1232
- import_koishi4.Schema.object({
1233
- enableAI: import_koishi4.Schema.boolean().default(false).description("启用 AI"),
1234
- aiEndpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1235
- aiApiKey: import_koishi4.Schema.string().description("密钥 (Key)").role("secret"),
1236
- aiModel: import_koishi4.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1237
- aiTPM: import_koishi4.Schema.number().description("每分钟请求数 (TPM)").default(60),
1238
- AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
1239
- aiAnalyseSchema: import_koishi4.Schema.string().role("textarea").default(
1268
+ import_koishi3.Schema.object({
1269
+ enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1270
+ aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1271
+ aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1272
+ aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1273
+ aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
1274
+ AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
1275
+ aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
1240
1276
  `{
1241
1277
  "type": "object",
1242
1278
  "properties": {
@@ -1259,8 +1295,8 @@ var Config = import_koishi4.Schema.intersect([
1259
1295
  "required": ["keywords", "description", "rating"]
1260
1296
  }`
1261
1297
  ).description("分析 JSON Schema"),
1262
- aiCheckPrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
1263
- aiCheckSchema: import_koishi4.Schema.string().role("textarea").default(
1298
+ aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
1299
+ aiCheckSchema: import_koishi3.Schema.string().role("textarea").default(
1264
1300
  `{
1265
1301
  "type": "object",
1266
1302
  "properties": {
@@ -1277,15 +1313,15 @@ var Config = import_koishi4.Schema.intersect([
1277
1313
  }`
1278
1314
  ).description("查重 JSON Schema")
1279
1315
  }).description("模型配置"),
1280
- import_koishi4.Schema.object({
1281
- localPath: import_koishi4.Schema.string().description("文件映射路径"),
1282
- enableS3: import_koishi4.Schema.boolean().default(false).description("启用 S3 存储"),
1283
- publicUrl: import_koishi4.Schema.string().description("公共访问 URL").role("link"),
1284
- endpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link"),
1285
- bucket: import_koishi4.Schema.string().description("存储桶 (Bucket)"),
1286
- region: import_koishi4.Schema.string().default("auto").description("区域 (Region)"),
1287
- accessKeyId: import_koishi4.Schema.string().description("Access Key ID").role("secret"),
1288
- secretAccessKey: import_koishi4.Schema.string().description("Secret Access Key").role("secret")
1316
+ import_koishi3.Schema.object({
1317
+ localPath: import_koishi3.Schema.string().description("文件映射路径"),
1318
+ enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
1319
+ publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
1320
+ endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
1321
+ bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
1322
+ region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
1323
+ accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
1324
+ secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
1289
1325
  }).description("存储配置")
1290
1326
  ]);
1291
1327
  function apply(ctx, config) {
@@ -1314,7 +1350,7 @@ function apply(ctx, config) {
1314
1350
  if (staleCaves.length > 0) {
1315
1351
  const idsToMark = staleCaves.map((c) => ({ id: c.id, status: "delete" }));
1316
1352
  await ctx.database.upsert("cave", idsToMark);
1317
- await cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
1353
+ await cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
1318
1354
  }
1319
1355
  } catch (error) {
1320
1356
  logger.error("清理残留回声洞时发生错误:", error);
@@ -1332,7 +1368,7 @@ function apply(ctx, config) {
1332
1368
  const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
1333
1369
  const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
1334
1370
  const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
1335
- for (const message of messages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
1371
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1336
1372
  } catch (error) {
1337
1373
  logger.error("随机获取回声洞失败:", error);
1338
1374
  return "随机获取回声洞失败";
@@ -1344,12 +1380,12 @@ function apply(ctx, config) {
1344
1380
  if (session.quote?.elements) {
1345
1381
  sourceElements = session.quote.elements;
1346
1382
  } else if (content?.trim()) {
1347
- sourceElements = import_koishi4.h.parse(content);
1383
+ sourceElements = import_koishi3.h.parse(content);
1348
1384
  } else {
1349
1385
  await session.send("请在一分钟内发送你要添加的内容");
1350
1386
  const reply = await session.prompt(6e4);
1351
1387
  if (!reply) return "等待操作超时";
1352
- sourceElements = import_koishi4.h.parse(reply);
1388
+ sourceElements = import_koishi3.h.parse(reply);
1353
1389
  }
1354
1390
  const newId = await getNextCaveId(ctx, reusableIds);
1355
1391
  const creationTime = /* @__PURE__ */ new Date();
@@ -1388,11 +1424,11 @@ function apply(ctx, config) {
1388
1424
  time: creationTime
1389
1425
  });
1390
1426
  if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
1391
- if (finalStatus !== "preload" && finalStatus !== "delete") {
1427
+ if (finalStatus !== "preload") {
1392
1428
  newCave.status = finalStatus;
1393
1429
  if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
1394
1430
  if (hashManager) {
1395
- const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h5) => ({ ...h5, cave: newCave.id }));
1431
+ const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: newCave.id }));
1396
1432
  if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
1397
1433
  }
1398
1434
  if (finalStatus === "pending" && reviewManager) reviewManager.sendForPend(newCave);
@@ -1409,7 +1445,7 @@ function apply(ctx, config) {
1409
1445
  const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
1410
1446
  if (!targetCave) return `回声洞(${id})不存在`;
1411
1447
  const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
1412
- for (const message of messages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
1448
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1413
1449
  } catch (error) {
1414
1450
  logger.error(`查看回声洞(${id})失败:`, error);
1415
1451
  return "查看失败,请稍后再试";
@@ -1425,8 +1461,8 @@ function apply(ctx, config) {
1425
1461
  if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
1426
1462
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
1427
1463
  const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
1428
- cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
1429
- for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
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);
1430
1466
  } catch (error) {
1431
1467
  logger.error(`标记回声洞(${id})失败:`, error);
1432
1468
  return "删除失败,请稍后再试";
@@ -1437,7 +1473,7 @@ function apply(ctx, config) {
1437
1473
  const adminChannelId = config.adminChannel?.split(":")[1];
1438
1474
  if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
1439
1475
  try {
1440
- const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi4.$.count(row.id), "count") }).execute();
1476
+ const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
1441
1477
  if (!aggregatedStats.length) return "目前没有回声洞投稿";
1442
1478
  const userStats = /* @__PURE__ */ new Map();
1443
1479
  for (const stat of aggregatedStats) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "功能强大、高度可定制的回声洞。支持丰富的媒体类型、内容查重、人工审核、用户昵称、数据迁移以及本地/S3 双重文件存储后端。",
4
- "version": "2.7.6",
4
+ "version": "2.7.8",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
@@ -29,6 +29,6 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@aws-sdk/client-s3": "^3.800.0",
32
- "sharp": "^0.34.3"
32
+ "jimp": "^0.22.9"
33
33
  }
34
34
  }