koishi-plugin-best-cave 2.7.5 → 2.7.7

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.
@@ -30,6 +30,8 @@ export declare class AIManager {
30
30
  private logger;
31
31
  private fileManager;
32
32
  private http;
33
+ private requestCount;
34
+ private rateLimitResetTime;
33
35
  /**
34
36
  * @constructor
35
37
  * @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
@@ -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/index.d.ts CHANGED
@@ -61,6 +61,7 @@ export interface Config {
61
61
  aiEndpoint?: string;
62
62
  aiApiKey?: string;
63
63
  aiModel?: string;
64
+ aiRPM?: number;
64
65
  AnalysePrompt?: string;
65
66
  aiCheckPrompt?: string;
66
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");
@@ -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
  }
@@ -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,74 +718,98 @@ 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
+ const allCaveIds = [...new Set(allHashes.map((h4) => h4.cave))];
772
+ const textHashes = /* @__PURE__ */ new Map();
773
+ const imageHashes = /* @__PURE__ */ new Map();
774
+ for (const hash of allHashes) {
775
+ if (hash.type === "text") {
776
+ textHashes.set(hash.cave, hash.hash);
777
+ } else if (hash.type === "image") {
778
+ imageHashes.set(hash.cave, hash.hash);
779
+ }
780
+ }
781
+ const similarPairs = { text: /* @__PURE__ */ new Set(), image: /* @__PURE__ */ new Set() };
782
+ for (let i = 0; i < allCaveIds.length; i++) {
783
+ for (let j = i + 1; j < allCaveIds.length; j++) {
784
+ const id1 = allCaveIds[i];
785
+ const id2 = allCaveIds[j];
786
+ const pair = [id1, id2].sort((a, b) => a - b).join(" & ");
787
+ const text1 = textHashes.get(id1);
788
+ const text2 = textHashes.get(id2);
789
+ if (text1 && text2) {
790
+ const similarity = this.calculateSimilarity(text1, text2);
791
+ if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
792
+ }
793
+ const image1 = imageHashes.get(id1);
794
+ const image2 = imageHashes.get(id2);
795
+ if (image1 && image2) {
796
+ const similarity = this.calculateSimilarity(image1, image2);
797
+ if (similarity >= imageThreshold) similarPairs.image.add(`${pair} = ${similarity.toFixed(2)}%`);
798
+ }
799
+ }
800
+ }
801
+ const totalFindings = similarPairs.text.size + similarPairs.image.size;
802
+ if (totalFindings === 0) return "未发现高相似度的内容";
803
+ let report = `已发现 ${totalFindings} 组高相似度的内容:`;
804
+ if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
805
+ if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
806
+ return report.trim();
742
807
  } catch (error) {
743
808
  this.logger.error("检查相似度失败:", error);
744
809
  return `检查失败: ${error.message}`;
745
810
  }
746
811
  });
747
812
  }
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++;
771
- 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);
778
- }
779
- }
780
- if (hashesToInsert.length >= 100) await flushBatch();
781
- } catch (error) {
782
- errorCount++;
783
- this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
784
- }
785
- }
786
- await flushBatch();
787
- return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
788
- }
789
813
  /**
790
814
  * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
791
815
  * @param cave - 回声洞对象。
@@ -804,13 +828,13 @@ var HashManager = class {
804
828
  const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
805
829
  if (combinedText) {
806
830
  const textHash = this.generateTextSimhash(combinedText);
807
- if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "simhash" });
831
+ if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "text" });
808
832
  }
809
833
  for (const el of cave.elements.filter((el2) => el2.type === "image" && el2.file)) {
810
834
  try {
811
835
  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" });
836
+ const imageHash = await this.generatePHash(imageBuffer);
837
+ addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
814
838
  } catch (e) {
815
839
  this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, e);
816
840
  }
@@ -818,101 +842,57 @@ var HashManager = class {
818
842
  return tempHashes;
819
843
  }
820
844
  /**
821
- * @description 对数据库中所有哈希进行两两比较,找出相似度过高的内容。
822
- * @param options 包含临时阈值的可选对象。
823
- * @returns 一个包含检查结果的报告字符串。
845
+ * @description 执行一维离散余弦变换 (DCT-II) 的方法。
846
+ * @param input - 输入的数字数组。
847
+ * @returns DCT 变换后的数组。
824
848
  */
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
- }
838
- }
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
- }
849
+ dct1D(input) {
850
+ const N = input.length;
851
+ const output = new Array(N).fill(0);
852
+ const c0 = 1 / Math.sqrt(2);
853
+ for (let k = 0; k < N; k++) {
854
+ let sum = 0;
855
+ for (let n = 0; n < N; n++) sum += input[n] * Math.cos(Math.PI * (2 * n + 1) * k / (2 * N));
856
+ const ck = k === 0 ? c0 : 1;
857
+ output[k] = Math.sqrt(2 / N) * ck * sum;
861
858
  }
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();
859
+ return output;
868
860
  }
869
861
  /**
870
- * @description 执行二维离散余弦变换 (DCT-II)
862
+ * @description 执行二维离散余弦变换 (DCT-II) 的方法。
863
+ * 通过对行和列分别应用一维 DCT 来实现。
871
864
  * @param matrix - 输入的 N x N 像素亮度矩阵。
872
- * @returns DCT变换后的 N x N 系数矩阵。
865
+ * @returns DCT 变换后的 N x N 系数矩阵。
873
866
  */
874
- _dct2D(matrix) {
867
+ dct2D(matrix) {
875
868
  const N = matrix.length;
876
869
  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]));
870
+ const tempMatrix = matrix.map((row) => this.dct1D(row));
871
+ const transposed = tempMatrix.map((_, colIndex) => tempMatrix.map((row) => row[colIndex]));
872
+ const dctResultTransposed = transposed.map((row) => this.dct1D(row));
873
+ const dctResult = dctResultTransposed.map((_, colIndex) => dctResultTransposed.map((row) => row[colIndex]));
874
+ return dctResult;
896
875
  }
897
876
  /**
898
- * @description pHash 算法核心实现。
899
- * @param imageBuffer - 图片的Buffer。
900
- * @param size - 期望的哈希位数 (必须是完全平方数, 如 64 256)。
901
- * @returns 十六进制pHash字符串。
877
+ * @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
878
+ * @param imageBuffer - 图片的 Buffer。
879
+ * @returns 64位十六进制 pHash 字符串。
902
880
  */
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);
881
+ async generatePHash(imageBuffer) {
882
+ const image = await import_jimp.default.read(imageBuffer);
883
+ image.resize(32, 32, import_jimp.default.RESIZE_BILINEAR).greyscale();
884
+ const matrix = Array.from({ length: 32 }, () => new Array(32).fill(0));
885
+ image.scan(0, 0, 32, 32, (x, y, idx) => {
886
+ matrix[y][x] = image.bitmap.data[idx];
887
+ });
888
+ const dctMatrix = this.dct2D(matrix);
911
889
  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");
890
+ for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) coefficients.push(dctMatrix[y][x]);
891
+ const acCoefficients = coefficients.slice(1);
892
+ const average = acCoefficients.reduce((sum, val) => sum + val, 0) / acCoefficients.length;
893
+ let binaryHash = "";
894
+ for (const val of coefficients) binaryHash += val > average ? "1" : "0";
895
+ return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
916
896
  }
917
897
  /**
918
898
  * @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
@@ -922,8 +902,10 @@ var HashManager = class {
922
902
  */
923
903
  calculateHammingDistance(hex1, hex2) {
924
904
  let distance = 0;
925
- const bin1 = hexToBinary(hex1);
926
- const bin2 = hexToBinary(hex2);
905
+ let bin1 = "";
906
+ for (const char of hex1) bin1 += parseInt(char, 16).toString(2).padStart(4, "0");
907
+ let bin2 = "";
908
+ for (const char of hex2) bin2 += parseInt(char, 16).toString(2).padStart(4, "0");
927
909
  const len = Math.min(bin1.length, bin2.length);
928
910
  for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
929
911
  return distance;
@@ -947,14 +929,8 @@ var HashManager = class {
947
929
  generateTextSimhash(text) {
948
930
  const cleanText = (text || "").toLowerCase().replace(/\s+/g, "");
949
931
  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);
932
+ const tokens = Array.from(cleanText);
933
+ const tokenArray = Array.from(new Set(tokens));
958
934
  if (tokenArray.length === 0) return "";
959
935
  const vector = new Array(64).fill(0);
960
936
  tokenArray.forEach((token) => {
@@ -965,15 +941,8 @@ var HashManager = class {
965
941
  return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
966
942
  }
967
943
  };
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
944
 
975
945
  // src/AIManager.ts
976
- var import_koishi3 = require("koishi");
977
946
  var path3 = __toESM(require("path"));
978
947
  var AIManager = class {
979
948
  /**
@@ -999,6 +968,8 @@ var AIManager = class {
999
968
  __name(this, "AIManager");
1000
969
  }
1001
970
  http;
971
+ requestCount = 0;
972
+ rateLimitResetTime = 0;
1002
973
  /**
1003
974
  * @description 注册所有与 AIManager 功能相关的 Koishi 命令。
1004
975
  * @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
@@ -1013,18 +984,13 @@ var AIManager = class {
1013
984
  if (cavesToAnalyze.length === 0) return "无需分析回声洞";
1014
985
  await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
1015
986
  let successCount = 0;
1016
- const batchSize = 100;
1017
- for (let i = 0; i < cavesToAnalyze.length; i += batchSize) {
1018
- const batch = cavesToAnalyze.slice(i, i + batchSize);
1019
- this.logger.info(`[${i}/${cavesToAnalyze.length}] 正在分析 ${batch.length} 条回声洞...`);
1020
- for (const cave2 of batch) {
1021
- try {
1022
- await this.analyzeAndStore(cave2);
1023
- successCount++;
1024
- } catch (error) {
1025
- this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
1026
- return `分析回声洞(${cave2.id})时出错: ${error.message}`;
1027
- }
987
+ for (const [index, cave2] of cavesToAnalyze.entries()) {
988
+ try {
989
+ this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
990
+ await this.analyzeAndStore(cave2);
991
+ successCount++;
992
+ } catch (error) {
993
+ return `分析回声洞(${cave2.id})时出错: ${error.message}`;
1028
994
  }
1029
995
  }
1030
996
  return `已分析 ${successCount} 个回声洞`;
@@ -1033,23 +999,6 @@ var AIManager = class {
1033
999
  return `操作失败: ${error.message}`;
1034
1000
  }
1035
1001
  });
1036
- cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
1037
- if (!id) return "请输入要查看的回声洞序号";
1038
- try {
1039
- const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
1040
- if (!meta) return `回声洞(${id})尚未分析`;
1041
- const report = [
1042
- `回声洞(${id})分析结果:`,
1043
- `描述:${meta.description}`,
1044
- `关键词:${meta.keywords.join(", ")}`,
1045
- `综合评分:${meta.rating}/100`
1046
- ];
1047
- return import_koishi3.h.text(report.join("\n"));
1048
- } catch (error) {
1049
- this.logger.error(`查询回声洞(${id})失败:`, error);
1050
- return "查询失败,请稍后再试";
1051
- }
1052
- });
1053
1002
  }
1054
1003
  /**
1055
1004
  * @description 对新提交的内容执行 AI 驱动的查重检查。
@@ -1166,11 +1115,31 @@ var AIManager = class {
1166
1115
  * @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
1167
1116
  */
1168
1117
  async requestAI(messages, systemPrompt, schemaString) {
1118
+ const now = Date.now();
1119
+ if (now > this.rateLimitResetTime) {
1120
+ this.rateLimitResetTime = now + 6e4;
1121
+ this.requestCount = 0;
1122
+ }
1123
+ if (this.requestCount >= this.config.aiRPM) {
1124
+ const delay = this.rateLimitResetTime - now;
1125
+ await new Promise((resolve) => setTimeout(resolve, delay));
1126
+ this.rateLimitResetTime = Date.now() + 6e4;
1127
+ this.requestCount = 0;
1128
+ }
1169
1129
  let schema = JSON.parse(schemaString);
1130
+ const toolName = "extract_data";
1170
1131
  const payload = {
1171
1132
  model: this.config.aiModel,
1172
1133
  messages: [{ role: "system", content: systemPrompt }, ...messages],
1173
- response_format: { type: "json_schema", strict: true, schema }
1134
+ tools: [{
1135
+ type: "function",
1136
+ function: {
1137
+ name: toolName,
1138
+ description: "根据提供的内容提取或分析信息。",
1139
+ parameters: schema
1140
+ }
1141
+ }],
1142
+ tool_choice: { type: "function", function: { name: toolName } }
1174
1143
  };
1175
1144
  const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1176
1145
  const headers = {
@@ -1178,8 +1147,15 @@ var AIManager = class {
1178
1147
  "Authorization": `Bearer ${this.config.aiApiKey}`
1179
1148
  };
1180
1149
  try {
1150
+ this.requestCount++;
1181
1151
  const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1182
- return JSON.parse(response.choices[0].message.content);
1152
+ const toolCall = response.choices?.[0]?.message?.tool_calls?.[0];
1153
+ if (toolCall?.function?.arguments) {
1154
+ return JSON.parse(toolCall.function.arguments);
1155
+ } else {
1156
+ this.logger.error("AI 响应格式不正确:", JSON.stringify(response));
1157
+ throw new Error("AI 响应格式不正确");
1158
+ }
1183
1159
  } catch (error) {
1184
1160
  const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
1185
1161
  this.logger.error(`请求 API 失败: ${errorMessage}`);
@@ -1203,28 +1179,29 @@ var usage = `
1203
1179
  <p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
1204
1180
  </div>
1205
1181
  `;
1206
- var logger = new import_koishi4.Logger("best-cave");
1207
- var Config = import_koishi4.Schema.intersect([
1208
- import_koishi4.Schema.object({
1209
- perChannel: import_koishi4.Schema.boolean().default(false).description("启用分群模式"),
1210
- enableName: import_koishi4.Schema.boolean().default(false).description("启用自定义昵称"),
1211
- enableIO: import_koishi4.Schema.boolean().default(false).description("启用导入导出"),
1212
- adminChannel: import_koishi4.Schema.string().default("onebot:").description("管理群组 ID"),
1213
- caveFormat: import_koishi4.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
1182
+ var logger = new import_koishi3.Logger("best-cave");
1183
+ var Config = import_koishi3.Schema.intersect([
1184
+ import_koishi3.Schema.object({
1185
+ perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
1186
+ enableName: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
1187
+ enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
1188
+ adminChannel: import_koishi3.Schema.string().default("onebot:").description("管理群组 ID"),
1189
+ caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
1214
1190
  }).description("基础配置"),
1215
- import_koishi4.Schema.object({
1216
- enablePend: import_koishi4.Schema.boolean().default(false).description("启用审核"),
1217
- enableSimilarity: import_koishi4.Schema.boolean().default(false).description("启用查重"),
1218
- textThreshold: import_koishi4.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
1219
- imageThreshold: import_koishi4.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
1191
+ import_koishi3.Schema.object({
1192
+ enablePend: import_koishi3.Schema.boolean().default(false).description("启用审核"),
1193
+ enableSimilarity: import_koishi3.Schema.boolean().default(false).description("启用查重"),
1194
+ textThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
1195
+ imageThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
1220
1196
  }).description("复核配置"),
1221
- import_koishi4.Schema.object({
1222
- enableAI: import_koishi4.Schema.boolean().default(false).description("启用 AI"),
1223
- aiEndpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1224
- aiApiKey: import_koishi4.Schema.string().description("密钥 (Key)").role("secret"),
1225
- aiModel: import_koishi4.Schema.string().description("模型").default("gemini-2.5-flash"),
1226
- AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析提示词 (Prompt)"),
1227
- aiAnalyseSchema: import_koishi4.Schema.string().role("textarea").default(
1197
+ import_koishi3.Schema.object({
1198
+ enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1199
+ aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1200
+ aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1201
+ aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1202
+ aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
1203
+ AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
1204
+ aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
1228
1205
  `{
1229
1206
  "type": "object",
1230
1207
  "properties": {
@@ -1246,9 +1223,9 @@ var Config = import_koishi4.Schema.intersect([
1246
1223
  },
1247
1224
  "required": ["keywords", "description", "rating"]
1248
1225
  }`
1249
- ).description("分析输出模式 (JSON Schema)"),
1250
- aiCheckPrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重提示词 (Prompt)"),
1251
- aiCheckSchema: import_koishi4.Schema.string().role("textarea").default(
1226
+ ).description("分析 JSON Schema"),
1227
+ aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
1228
+ aiCheckSchema: import_koishi3.Schema.string().role("textarea").default(
1252
1229
  `{
1253
1230
  "type": "object",
1254
1231
  "properties": {
@@ -1263,17 +1240,17 @@ var Config = import_koishi4.Schema.intersect([
1263
1240
  },
1264
1241
  "required": ["duplicate"]
1265
1242
  }`
1266
- ).description("查重输出模式 (JSON Schema)")
1243
+ ).description("查重 JSON Schema")
1267
1244
  }).description("模型配置"),
1268
- import_koishi4.Schema.object({
1269
- localPath: import_koishi4.Schema.string().description("文件映射路径"),
1270
- enableS3: import_koishi4.Schema.boolean().default(false).description("启用 S3 存储"),
1271
- publicUrl: import_koishi4.Schema.string().description("公共访问 URL").role("link"),
1272
- endpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link"),
1273
- bucket: import_koishi4.Schema.string().description("存储桶 (Bucket)"),
1274
- region: import_koishi4.Schema.string().default("auto").description("区域 (Region)"),
1275
- accessKeyId: import_koishi4.Schema.string().description("Access Key ID").role("secret"),
1276
- secretAccessKey: import_koishi4.Schema.string().description("Secret Access Key").role("secret")
1245
+ import_koishi3.Schema.object({
1246
+ localPath: import_koishi3.Schema.string().description("文件映射路径"),
1247
+ enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
1248
+ publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
1249
+ endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
1250
+ bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
1251
+ region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
1252
+ accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
1253
+ secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
1277
1254
  }).description("存储配置")
1278
1255
  ]);
1279
1256
  function apply(ctx, config) {
@@ -1320,7 +1297,7 @@ function apply(ctx, config) {
1320
1297
  const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
1321
1298
  const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
1322
1299
  const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
1323
- for (const message of messages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
1300
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1324
1301
  } catch (error) {
1325
1302
  logger.error("随机获取回声洞失败:", error);
1326
1303
  return "随机获取回声洞失败";
@@ -1332,12 +1309,12 @@ function apply(ctx, config) {
1332
1309
  if (session.quote?.elements) {
1333
1310
  sourceElements = session.quote.elements;
1334
1311
  } else if (content?.trim()) {
1335
- sourceElements = import_koishi4.h.parse(content);
1312
+ sourceElements = import_koishi3.h.parse(content);
1336
1313
  } else {
1337
1314
  await session.send("请在一分钟内发送你要添加的内容");
1338
1315
  const reply = await session.prompt(6e4);
1339
1316
  if (!reply) return "等待操作超时";
1340
- sourceElements = import_koishi4.h.parse(reply);
1317
+ sourceElements = import_koishi3.h.parse(reply);
1341
1318
  }
1342
1319
  const newId = await getNextCaveId(ctx, reusableIds);
1343
1320
  const creationTime = /* @__PURE__ */ new Date();
@@ -1380,7 +1357,7 @@ function apply(ctx, config) {
1380
1357
  newCave.status = finalStatus;
1381
1358
  if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
1382
1359
  if (hashManager) {
1383
- const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h5) => ({ ...h5, cave: newCave.id }));
1360
+ const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: newCave.id }));
1384
1361
  if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
1385
1362
  }
1386
1363
  if (finalStatus === "pending" && reviewManager) reviewManager.sendForPend(newCave);
@@ -1397,7 +1374,7 @@ function apply(ctx, config) {
1397
1374
  const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
1398
1375
  if (!targetCave) return `回声洞(${id})不存在`;
1399
1376
  const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
1400
- for (const message of messages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
1377
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1401
1378
  } catch (error) {
1402
1379
  logger.error(`查看回声洞(${id})失败:`, error);
1403
1380
  return "查看失败,请稍后再试";
@@ -1414,7 +1391,7 @@ function apply(ctx, config) {
1414
1391
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
1415
1392
  const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
1416
1393
  cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
1417
- for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi4.h.normalize(message));
1394
+ for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1418
1395
  } catch (error) {
1419
1396
  logger.error(`标记回声洞(${id})失败:`, error);
1420
1397
  return "删除失败,请稍后再试";
@@ -1425,7 +1402,7 @@ function apply(ctx, config) {
1425
1402
  const adminChannelId = config.adminChannel?.split(":")[1];
1426
1403
  if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
1427
1404
  try {
1428
- const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi4.$.count(row.id), "count") }).execute();
1405
+ const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
1429
1406
  if (!aggregatedStats.length) return "目前没有回声洞投稿";
1430
1407
  const userStats = /* @__PURE__ */ new Map();
1431
1408
  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.5",
4
+ "version": "2.7.7",
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
  }