koishi-plugin-best-cave 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -33,14 +33,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/locales/zh-CN.yml
34
34
  var require_zh_CN = __commonJS({
35
35
  "src/locales/zh-CN.yml"(exports2, module2) {
36
- module2.exports = { _config: { manager: "管理员", number: "冷却时间(秒)", enableAudit: "启用审核", imageMaxSize: "图片最大大小(MB)", enableMD5: "启用MD5查重", enableDuplicate: "启用pHash查重", duplicateThreshold: "pHash相似度阈值(0-1)", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { noContent: "回声洞内容为空", getCave: "获取回声洞失败", noCave: "没有回声洞", invalidId: "请输入有效的回声洞ID", notFound: "未找到该回声洞", exactDuplicateFound: "发现完全相同的", similarDuplicateFound: "发现相似度为 {0}% 的" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
36
+ module2.exports = { _config: { manager: "管理员", number: "冷却时间(秒)", enableAudit: "启用审核", enableTextDuplicate: "启用文本查重", textDuplicateThreshold: "文本相似度阈值(0-1)", enableImageDuplicate: "启用图片查重", imageDuplicateThreshold: "图片相似度阈值(0-1)", imageMaxSize: "图片最大大小(MB)", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { noContent: "回声洞内容为空", getCave: "获取回声洞失败", noCave: "没有回声洞", invalidId: "请输入有效的回声洞ID", notFound: "未找到该回声洞", exactDuplicateFound: "发现完全相同的", similarDuplicateFound: "发现相似度为 {0}% 的", addFailed: "添加失败,请稍后重试。" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
37
37
  }
38
38
  });
39
39
 
40
40
  // src/locales/en-US.yml
41
41
  var require_en_US = __commonJS({
42
42
  "src/locales/en-US.yml"(exports2, module2) {
43
- module2.exports = { _config: { manager: "Administrator", number: "Cooldown time (seconds)", enableAudit: "Enable moderation", imageMaxSize: "Maximum image size (MB)", enableMD5: "Enable MD5 duplicate check", enableDuplicate: "Enable pHash duplicate check", duplicateThreshold: "pHash similarity threshold (0-1)", allowVideo: "Allow video upload", videoMaxSize: "Maximum video size (MB)", enablePagination: "Enable statistics pagination", itemsPerPage: "Items per page", blacklist: "Blacklist (users)", whitelist: "Moderation whitelist (users/groups/channels)" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing echo caves", examples: "Use cave to randomly draw an echo\nUse -a to add directly or add by reference\nUse -g to view specific echo\nUse -r to delete specific echo", options: { a: "Add echo", g: "View echo", r: "Delete echo", p: "Approve moderation (batch)", d: "Reject moderation (batch)", l: "Query submission statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timeout, addition cancelled", videoDisabled: "Video upload not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Added successfully, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You don't have permission to delete others' echos", deletePending: "Delete (pending review)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "Currently there are {0} echos:", totalItems: "User {0} has submitted {1} items:", idsLine: "{0}" }, audit: { noPending: "No pending echos for review", pendingNotFound: "Pending echo not found", pendingResult: "{0}, {1} pending echos remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} echos", title: "Pending echos:", from: "Submitted by:", sendFailed: "Failed to send moderation message, cannot contact administrator {0}" }, error: { noContent: "Echo content is empty", getCave: "Failed to get echo", noCave: "No echos available", invalidId: "Please enter a valid echo ID", notFound: "Echo not found", exactDuplicateFound: "Found exactly identical", similarDuplicateFound: "Found {0}% similar" }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is limited to administrators only", cooldown: "Group chat cooling down... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
43
+ module2.exports = { _config: { manager: "Administrator", number: "Cooldown time (seconds)", enableAudit: "Enable moderation", enableTextDuplicate: "Enable text duplicate check", textDuplicateThreshold: "Text similarity threshold (0-1)", enableImageDuplicate: "Enable image duplicate check", imageDuplicateThreshold: "Image similarity threshold (0-1)", imageMaxSize: "Maximum image size (MB)", allowVideo: "Allow video upload", videoMaxSize: "Maximum video size (MB)", enablePagination: "Enable statistics pagination", itemsPerPage: "Items per page", blacklist: "Blacklist (users)", whitelist: "Moderation whitelist (users/groups/channels)" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing echo caves", examples: "Use cave to randomly draw an echo\nUse -a to add directly or add by reference\nUse -g to view specific echo\nUse -r to delete specific echo", options: { a: "Add echo", g: "View echo", r: "Delete echo", p: "Approve moderation (batch)", d: "Reject moderation (batch)", l: "Query submission statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timeout, addition cancelled", videoDisabled: "Video upload not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Added successfully, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You don't have permission to delete others' echos", deletePending: "Delete (pending review)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "Currently there are {0} echos:", totalItems: "User {0} has submitted {1} items:", idsLine: "{0}" }, audit: { noPending: "No pending echos for review", pendingNotFound: "Pending echo not found", pendingResult: "{0}, {1} pending echos remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} echos", title: "Pending echos:", from: "Submitted by:", sendFailed: "Failed to send moderation message, cannot contact administrator {0}" }, error: { noContent: "Echo content is empty", getCave: "Failed to get echo", noCave: "No echos available", invalidId: "Please enter a valid echo ID", notFound: "Echo not found", exactDuplicateFound: "Found exactly identical", similarDuplicateFound: "Found {0}% similar", addFailed: "Add failed, please try again later." }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is limited to administrators only", cooldown: "Group chat cooling down... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
44
44
  }
45
45
  });
46
46
 
@@ -53,11 +53,11 @@ __export(src_exports, {
53
53
  name: () => name
54
54
  });
55
55
  module.exports = __toCommonJS(src_exports);
56
- var import_koishi4 = require("koishi");
57
- var fs4 = __toESM(require("fs"));
58
- var path4 = __toESM(require("path"));
56
+ var import_koishi5 = require("koishi");
57
+ var fs5 = __toESM(require("fs"));
58
+ var path5 = __toESM(require("path"));
59
59
 
60
- // src/utils/fileHandler.ts
60
+ // src/utils/FileHandle.ts
61
61
  var fs = __toESM(require("fs"));
62
62
  var path = __toESM(require("path"));
63
63
  var import_koishi = require("koishi");
@@ -216,11 +216,11 @@ var FileHandler = class {
216
216
  }
217
217
  };
218
218
 
219
- // src/utils/idManager.ts
219
+ // src/utils/IdManage.ts
220
220
  var fs2 = __toESM(require("fs"));
221
221
  var path2 = __toESM(require("path"));
222
222
  var import_koishi2 = require("koishi");
223
- var logger2 = new import_koishi2.Logger("idManager");
223
+ var logger2 = new import_koishi2.Logger("IdManager");
224
224
  var IdManager = class {
225
225
  static {
226
226
  __name(this, "IdManager");
@@ -288,11 +288,15 @@ var IdManager = class {
288
288
  ...status.deletedIds || [],
289
289
  0
290
290
  );
291
- this.deletedIds = new Set(
292
- status.deletedIds?.filter((id) => !this.usedIds.has(id)) || []
293
- );
291
+ this.deletedIds = new Set(status.deletedIds || []);
292
+ for (let i = 1; i <= this.maxId; i++) {
293
+ if (!this.usedIds.has(i)) {
294
+ this.deletedIds.add(i);
295
+ }
296
+ }
294
297
  await this.saveStatus();
295
298
  this.initialized = true;
299
+ logger2.success(`Cave ID Manager initialized with ${this.maxId}(-${this.deletedIds.size}) IDs`);
296
300
  } catch (error) {
297
301
  this.initialized = false;
298
302
  logger2.error(`ID Manager initialization failed: ${error.message}`);
@@ -430,16 +434,16 @@ var IdManager = class {
430
434
  }
431
435
  };
432
436
 
433
- // src/utils/HashStorage.ts
437
+ // src/utils/HashManage.ts
434
438
  var import_koishi3 = require("koishi");
435
439
  var fs3 = __toESM(require("fs"));
436
440
  var path3 = __toESM(require("path"));
437
441
 
438
- // src/utils/ImageHasher.ts
442
+ // src/utils/ContentHash.ts
439
443
  var import_sharp = __toESM(require("sharp"));
440
- var ImageHasher = class {
444
+ var ContentHasher = class {
441
445
  static {
442
- __name(this, "ImageHasher");
446
+ __name(this, "ContentHasher");
443
447
  }
444
448
  /**
445
449
  * 计算图片的感知哈希值
@@ -580,6 +584,21 @@ var ImageHasher = class {
580
584
  const distance = this.calculateDistance(hash1, hash2);
581
585
  return (64 - distance) / 64;
582
586
  }
587
+ /**
588
+ * 计算文本的哈希值
589
+ * @param text - 输入文本
590
+ * @returns 文本的哈希值(36进制字符串)
591
+ */
592
+ static calculateTextHash(text) {
593
+ const normalizedText = text.toLowerCase().trim().replace(/\s+/g, " ");
594
+ let hash = 0;
595
+ for (let i = 0; i < normalizedText.length; i++) {
596
+ const char = normalizedText.charCodeAt(i);
597
+ hash = (hash << 5) - hash + char;
598
+ hash = hash & hash;
599
+ }
600
+ return hash.toString(36);
601
+ }
583
602
  /**
584
603
  * 批量比较一个新哈希值与多个已存在哈希值的相似度
585
604
  * @param newHash - 新的哈希值
@@ -591,93 +610,199 @@ var ImageHasher = class {
591
610
  }
592
611
  };
593
612
 
594
- // src/utils/HashStorage.ts
595
- var import_crypto = __toESM(require("crypto"));
596
- var logger3 = new import_koishi3.Logger("HashStorage");
597
- var HashStorage = class _HashStorage {
613
+ // src/utils/HashManage.ts
614
+ var import_util = require("util");
615
+ var logger3 = new import_koishi3.Logger("HashManager");
616
+ var readFileAsync = (0, import_util.promisify)(fs3.readFile);
617
+ var ContentHashManager = class _ContentHashManager {
598
618
  /**
599
- * 创建哈希存储实例
600
- * @param caveDir - 回声洞数据目录路径
619
+ * 初始化HashManager实例
620
+ * @param caveDir 回声洞数据目录路径
601
621
  */
602
622
  constructor(caveDir) {
603
623
  this.caveDir = caveDir;
604
- this.filePath = path3.join(caveDir, "hash.json");
605
- this.resourceDir = path3.join(caveDir, "resources");
606
- this.caveFilePath = path3.join(caveDir, "cave.json");
607
624
  }
608
625
  static {
609
- __name(this, "HashStorage");
626
+ __name(this, "ContentHashManager");
610
627
  }
611
- filePath;
612
- resourceDir;
613
- caveFilePath;
628
+ // 哈希数据文件名
629
+ static HASH_FILE = "hash.json";
630
+ // 回声洞数据文件名
631
+ static CAVE_FILE = "cave.json";
632
+ // 批处理大小
633
+ static BATCH_SIZE = 50;
634
+ // 存储回声洞ID到图片哈希值的映射
614
635
  imageHashes = /* @__PURE__ */ new Map();
615
636
  textHashes = /* @__PURE__ */ new Map();
637
+ // 初始化状态标志
616
638
  initialized = false;
639
+ get filePath() {
640
+ return path3.join(this.caveDir, _ContentHashManager.HASH_FILE);
641
+ }
642
+ get resourceDir() {
643
+ return path3.join(this.caveDir, "resources");
644
+ }
645
+ get caveFilePath() {
646
+ return path3.join(this.caveDir, _ContentHashManager.CAVE_FILE);
647
+ }
617
648
  /**
618
649
  * 初始化哈希存储
650
+ * 读取现有哈希数据或重新构建哈希值
619
651
  * @throws 初始化失败时抛出错误
620
652
  */
621
653
  async initialize() {
622
654
  if (this.initialized) return;
623
655
  try {
624
656
  const hashData = await FileHandler.readJsonData(this.filePath).then((data) => data[0]).catch(() => null);
625
- if (!hashData?.imageHashes) {
657
+ if (!hashData?.imageHashes || !hashData?.textHashes || Object.keys(hashData.imageHashes).length === 0) {
658
+ this.imageHashes.clear();
659
+ this.textHashes.clear();
626
660
  await this.buildInitialHashes();
627
661
  } else {
628
- this.loadHashData(hashData);
662
+ this.imageHashes = new Map(
663
+ Object.entries(hashData.imageHashes).map(([k, v]) => [Number(k), v])
664
+ );
665
+ this.textHashes = new Map(
666
+ Object.entries(hashData.textHashes).map(([k, v]) => [Number(k), v])
667
+ );
629
668
  await this.updateMissingHashes();
630
669
  }
670
+ const totalCaves = (/* @__PURE__ */ new Set([...this.imageHashes.keys(), ...this.textHashes.keys()])).size;
631
671
  this.initialized = true;
672
+ logger3.success(`Cave Hash Manager initialized with ${totalCaves} hashes`);
632
673
  } catch (error) {
633
- logger3.error(`Hash storage initialization failed: ${error.message}`);
674
+ logger3.error(`Initialization failed: ${error.message}`);
675
+ this.initialized = false;
634
676
  throw error;
635
677
  }
636
678
  }
637
679
  /**
638
- * 加载哈希数据
639
- * @param data - 要加载的哈希数据
640
- * @private
680
+ * 获取当前哈希存储状态
681
+ * @returns 包含最后更新时间和所有条目的状态对象
641
682
  */
642
- loadHashData(data) {
643
- this.imageHashes = new Map(Object.entries(data.imageHashes).map(([k, v]) => [Number(k), v]));
644
- this.textHashes = new Map(Object.entries(data.textHashes || {}).map(([k, v]) => [Number(k), v]));
683
+ async getStatus() {
684
+ if (!this.initialized) await this.initialize();
685
+ return {
686
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
687
+ entries: Array.from(this.imageHashes.entries()).map(([caveId, imgHashes]) => ({
688
+ caveId,
689
+ imageHashes: imgHashes,
690
+ textHashes: this.textHashes.get(caveId) || []
691
+ }))
692
+ };
645
693
  }
646
694
  /**
647
- * 更新指定回声洞的哈希值
648
- * @param caveId - 回声洞ID
649
- * @param type - 哈希类型(图像或文本)
650
- * @param content - 要计算哈希的内容
695
+ * 更新指定回声洞的图片哈希值
696
+ * @param caveId 回声洞ID
697
+ * @param content 图片buffer数组
651
698
  */
652
- async updateHash(caveId, type, content) {
699
+ async updateCaveContent(caveId, content) {
653
700
  if (!this.initialized) await this.initialize();
654
701
  try {
655
- const hash = type === "image" ? await ImageHasher.calculateHash(content) : _HashStorage.hashText(content);
656
- const hashMap = type === "image" ? this.imageHashes : this.textHashes;
657
- const existingHashes = hashMap.get(caveId) || [];
658
- hashMap.set(caveId, [...existingHashes, hash]);
659
- await this.saveHashes();
702
+ if (content.images?.length) {
703
+ const imageHashes = await Promise.all(
704
+ content.images.map((buffer) => ContentHasher.calculateHash(buffer))
705
+ );
706
+ this.imageHashes.set(caveId, imageHashes);
707
+ }
708
+ if (content.texts?.length) {
709
+ const textHashes = content.texts.map((text) => ContentHasher.calculateTextHash(text));
710
+ this.textHashes.set(caveId, textHashes);
711
+ }
712
+ if (!content.images && !content.texts) {
713
+ this.imageHashes.delete(caveId);
714
+ this.textHashes.delete(caveId);
715
+ }
716
+ await this.saveContentHashes();
660
717
  } catch (error) {
661
- logger3.error(`Failed to update ${type} hash for cave ${caveId}: ${error.message}`);
718
+ logger3.error(`Failed to update content hashes (cave ${caveId}): ${error.message}`);
662
719
  }
663
720
  }
664
721
  /**
665
- * 查找重复项
666
- * @param type - 查找类型(图像或文本)
667
- * @param hashes - 要查找的哈希值数组
668
- * @param threshold - 相似度阈值,默认为1
669
- * @returns 匹配结果数组
722
+ * 更新所有回声洞的哈希值
723
+ * @param isInitialBuild 是否为初始构建
670
724
  */
671
- async findDuplicates(type, hashes, threshold = 1) {
725
+ async updateAllCaves(isInitialBuild = false) {
726
+ if (!this.initialized && !isInitialBuild) {
727
+ await this.initialize();
728
+ return;
729
+ }
730
+ try {
731
+ logger3.info("Starting full hash update...");
732
+ const caveData = await this.loadCaveData();
733
+ const cavesWithImages = caveData.filter(
734
+ (cave) => cave.elements?.some((el) => el.type === "img" && el.file)
735
+ );
736
+ this.imageHashes.clear();
737
+ let processedCount = 0;
738
+ const totalImages = cavesWithImages.length;
739
+ const processCave = /* @__PURE__ */ __name(async (cave) => {
740
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
741
+ if (imgElements.length === 0) return;
742
+ try {
743
+ const hashes = await Promise.all(
744
+ imgElements.map(async (imgElement) => {
745
+ const filePath = path3.join(this.resourceDir, imgElement.file);
746
+ if (!fs3.existsSync(filePath)) {
747
+ logger3.warn(`Image file not found: ${filePath}`);
748
+ return null;
749
+ }
750
+ const imgBuffer = await readFileAsync(filePath);
751
+ return await ContentHasher.calculateHash(imgBuffer);
752
+ })
753
+ );
754
+ const validHashes = hashes.filter((hash) => hash !== null);
755
+ if (validHashes.length > 0) {
756
+ this.imageHashes.set(cave.cave_id, validHashes);
757
+ processedCount++;
758
+ if (processedCount % 100 === 0) {
759
+ logger3.info(`Progress: ${processedCount}/${totalImages}`);
760
+ }
761
+ }
762
+ } catch (error) {
763
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
764
+ }
765
+ }, "processCave");
766
+ await this.processBatch(cavesWithImages, processCave);
767
+ await this.saveContentHashes();
768
+ logger3.success(`Update completed. Processed ${processedCount}/${totalImages} images`);
769
+ } catch (error) {
770
+ logger3.error(`Full update failed: ${error.message}`);
771
+ throw error;
772
+ }
773
+ }
774
+ /**
775
+ * 查找重复的图片
776
+ * @param content 待查找的图片buffer数组
777
+ * @param thresholds 相似度阈值
778
+ * @returns 匹配结果数组,包含索引、回声洞ID和相似度
779
+ */
780
+ async findDuplicates(content, thresholds) {
672
781
  if (!this.initialized) await this.initialize();
673
- const hashMap = type === "image" ? this.imageHashes : this.textHashes;
674
- const calculateSimilarity = type === "image" ? ImageHasher.calculateSimilarity : (a, b) => a === b ? 1 : 0;
675
- return hashes.map((hash, index) => {
782
+ const results = [];
783
+ if (content.images?.length) {
784
+ const imageResults = await this.findImageDuplicates(content.images, thresholds.image);
785
+ results.push(...imageResults.map(
786
+ (result) => result ? { ...result, type: "image" } : null
787
+ ));
788
+ }
789
+ if (content.texts?.length) {
790
+ const textResults = await this.findTextDuplicates(content.texts, thresholds.text);
791
+ results.push(...textResults.map(
792
+ (result) => result ? { ...result, type: "text" } : null
793
+ ));
794
+ }
795
+ return results;
796
+ }
797
+ async findTextDuplicates(texts, threshold) {
798
+ const inputHashes = texts.map((text) => ContentHasher.calculateTextHash(text));
799
+ const existingHashes = Array.from(this.textHashes.entries());
800
+ return inputHashes.map((hash, index) => {
676
801
  let maxSimilarity = 0;
677
802
  let matchedCaveId = null;
678
- for (const [caveId, existingHashes] of hashMap.entries()) {
679
- for (const existingHash of existingHashes) {
680
- const similarity = calculateSimilarity(hash, existingHash);
803
+ for (const [caveId, hashes] of existingHashes) {
804
+ for (const existingHash of hashes) {
805
+ const similarity = this.calculateTextSimilarity(hash, existingHash);
681
806
  if (similarity >= threshold && similarity > maxSimilarity) {
682
807
  maxSimilarity = similarity;
683
808
  matchedCaveId = caveId;
@@ -686,37 +811,122 @@ var HashStorage = class _HashStorage {
686
811
  }
687
812
  if (maxSimilarity === 1) break;
688
813
  }
689
- return matchedCaveId ? { index, caveId: matchedCaveId, similarity: maxSimilarity } : null;
814
+ return matchedCaveId ? {
815
+ index,
816
+ caveId: matchedCaveId,
817
+ similarity: maxSimilarity
818
+ } : null;
690
819
  });
691
820
  }
821
+ calculateTextSimilarity(hash1, hash2) {
822
+ if (hash1 === hash2) return 1;
823
+ const length = Math.max(hash1.length, hash2.length);
824
+ let matches = 0;
825
+ for (let i = 0; i < length; i++) {
826
+ if (hash1[i] === hash2[i]) matches++;
827
+ }
828
+ return matches / length;
829
+ }
830
+ // 重命名原有的图片哈希相关方法
831
+ async findImageDuplicates(images, threshold) {
832
+ if (!this.initialized) await this.initialize();
833
+ const inputHashes = await Promise.all(
834
+ images.map((buffer) => ContentHasher.calculateHash(buffer))
835
+ );
836
+ const existingHashes = Array.from(this.imageHashes.entries());
837
+ return Promise.all(
838
+ inputHashes.map(async (hash, index) => {
839
+ try {
840
+ let maxSimilarity = 0;
841
+ let matchedCaveId = null;
842
+ for (const [caveId, hashes] of existingHashes) {
843
+ for (const existingHash of hashes) {
844
+ const similarity = ContentHasher.calculateSimilarity(hash, existingHash);
845
+ if (similarity >= threshold && similarity > maxSimilarity) {
846
+ maxSimilarity = similarity;
847
+ matchedCaveId = caveId;
848
+ if (Math.abs(similarity - 1) < Number.EPSILON) break;
849
+ }
850
+ }
851
+ if (Math.abs(maxSimilarity - 1) < Number.EPSILON) break;
852
+ }
853
+ return matchedCaveId ? {
854
+ index,
855
+ caveId: matchedCaveId,
856
+ similarity: maxSimilarity
857
+ } : null;
858
+ } catch (error) {
859
+ logger3.warn(`处理图片 ${index} 失败: ${error.message}`);
860
+ return null;
861
+ }
862
+ })
863
+ );
864
+ }
692
865
  /**
693
- * 清除指定回声洞的所有哈希值
694
- * @param caveId - 回声洞ID
866
+ * 加载回声洞数据
867
+ * @returns 回声洞数据数组
868
+ * @private
695
869
  */
696
- async clearHashes(caveId) {
697
- if (!this.initialized) await this.initialize();
698
- this.imageHashes.delete(caveId);
699
- this.textHashes.delete(caveId);
700
- await this.saveHashes();
870
+ async loadCaveData() {
871
+ const data = await FileHandler.readJsonData(this.caveFilePath);
872
+ return Array.isArray(data) ? data.flat() : [];
873
+ }
874
+ /**
875
+ * 保存哈希数据到文件
876
+ * @private
877
+ */
878
+ async saveContentHashes() {
879
+ const data = {
880
+ imageHashes: Object.fromEntries(this.imageHashes),
881
+ textHashes: Object.fromEntries(this.textHashes),
882
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
883
+ };
884
+ await FileHandler.writeJsonData(this.filePath, [data]);
701
885
  }
702
886
  /**
703
- * 构建初始哈希值
887
+ * 构建初始哈希数据
704
888
  * @private
705
889
  */
706
890
  async buildInitialHashes() {
707
891
  const caveData = await this.loadCaveData();
708
892
  let processedCount = 0;
709
- const total = caveData.length;
893
+ const totalCaves = caveData.length;
894
+ logger3.info(`Building hash data for ${totalCaves} caves...`);
710
895
  for (const cave of caveData) {
711
- await this.processCaveHashes(cave);
712
- await this.processCaveTextHashes(cave);
713
- processedCount++;
714
- if (processedCount % 100 === 0 || processedCount === total) {
715
- logger3.info(`Processing caves: ${processedCount}/${total} (${Math.floor(processedCount / total * 100)}%)`);
896
+ try {
897
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
898
+ if (imgElements.length > 0) {
899
+ const hashes = await Promise.all(
900
+ imgElements.map(async (imgElement) => {
901
+ const filePath = path3.join(this.resourceDir, imgElement.file);
902
+ if (!fs3.existsSync(filePath)) {
903
+ logger3.warn(`Image not found: ${filePath}`);
904
+ return null;
905
+ }
906
+ const imgBuffer = await fs3.promises.readFile(filePath);
907
+ return await ContentHasher.calculateHash(imgBuffer);
908
+ })
909
+ );
910
+ const validHashes = hashes.filter((hash) => hash !== null);
911
+ if (validHashes.length > 0) {
912
+ this.imageHashes.set(cave.cave_id, validHashes);
913
+ }
914
+ }
915
+ const textElements = cave.elements?.filter((el) => el.type === "text" && el.content) || [];
916
+ if (textElements.length > 0) {
917
+ const textHashes = textElements.map((el) => ContentHasher.calculateTextHash(el.content));
918
+ this.textHashes.set(cave.cave_id, textHashes);
919
+ }
920
+ processedCount++;
921
+ if (processedCount % 100 === 0) {
922
+ logger3.info(`Progress: ${processedCount}/${totalCaves} caves`);
923
+ }
924
+ } catch (error) {
925
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
716
926
  }
717
927
  }
718
- await this.saveHashes();
719
- logger3.info(`Processed ${processedCount} caves with ${this.imageHashes.size} images and ${this.textHashes.size} texts`);
928
+ await this.saveContentHashes();
929
+ logger3.success(`Build completed. Processed ${processedCount}/${totalCaves} caves`);
720
930
  }
721
931
  /**
722
932
  * 更新缺失的哈希值
@@ -724,128 +934,285 @@ var HashStorage = class _HashStorage {
724
934
  */
725
935
  async updateMissingHashes() {
726
936
  const caveData = await this.loadCaveData();
727
- const missingImageCaves = caveData.filter((cave) => !this.imageHashes.has(cave.cave_id));
728
- const missingTextCaves = caveData.filter((cave) => !this.textHashes.has(cave.cave_id));
729
- const total = missingImageCaves.length + missingTextCaves.length;
730
- if (total > 0) {
731
- let processedCount = 0;
732
- for (const cave of missingImageCaves) {
733
- await this.processCaveHashes(cave);
734
- processedCount++;
735
- if (processedCount % 100 === 0 || processedCount === total) {
736
- logger3.info(`Updating missing hashes: ${processedCount}/${total} (${Math.floor(processedCount / total * 100)}%)`);
737
- }
738
- }
739
- for (const cave of missingTextCaves) {
740
- await this.processCaveTextHashes(cave);
741
- processedCount++;
742
- if (processedCount % 100 === 0 || processedCount === total) {
743
- logger3.info(`Updating missing hashes: ${processedCount}/${total} (${Math.floor(processedCount / total * 100)}%)`);
937
+ let updatedCount = 0;
938
+ for (const cave of caveData) {
939
+ if (this.imageHashes.has(cave.cave_id)) continue;
940
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
941
+ if (imgElements.length === 0) continue;
942
+ try {
943
+ const hashes = await Promise.all(
944
+ imgElements.map(async (imgElement) => {
945
+ const filePath = path3.join(this.resourceDir, imgElement.file);
946
+ if (!fs3.existsSync(filePath)) {
947
+ return null;
948
+ }
949
+ const imgBuffer = await fs3.promises.readFile(filePath);
950
+ return ContentHasher.calculateHash(imgBuffer);
951
+ })
952
+ );
953
+ const validHashes = hashes.filter((hash) => hash !== null);
954
+ if (validHashes.length > 0) {
955
+ this.imageHashes.set(cave.cave_id, validHashes);
956
+ updatedCount++;
744
957
  }
958
+ } catch (error) {
959
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
745
960
  }
746
- await this.saveHashes();
747
- logger3.info(`Updated ${missingImageCaves.length} missing images and ${missingTextCaves.length} missing texts`);
748
961
  }
749
962
  }
750
963
  /**
751
- * 处理单个回声洞的哈希值
752
- * @param cave - 回声洞数据
964
+ * 批量处理数组项
965
+ * @param items 待处理项数组
966
+ * @param processor 处理函数
967
+ * @param batchSize 批处理大小
753
968
  * @private
754
969
  */
755
- async processCaveHashes(cave) {
756
- const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
757
- if (imgElements.length === 0) return;
758
- try {
759
- const hashes = await Promise.all(imgElements.map(async (el) => {
760
- const filePath = path3.join(this.resourceDir, el.file);
761
- return fs3.existsSync(filePath) ? ImageHasher.calculateHash(await fs3.promises.readFile(filePath)) : null;
762
- }));
763
- const validHashes = hashes.filter(Boolean);
764
- if (validHashes.length) {
765
- this.imageHashes.set(cave.cave_id, validHashes);
766
- await this.saveHashes();
970
+ async processBatch(items, processor, batchSize = _ContentHashManager.BATCH_SIZE) {
971
+ for (let i = 0; i < items.length; i += batchSize) {
972
+ const batch = items.slice(i, i + batchSize);
973
+ await Promise.all(
974
+ batch.map(async (item) => {
975
+ try {
976
+ await processor(item);
977
+ } catch (error) {
978
+ logger3.error(`Batch processing error: ${error.message}`);
979
+ }
980
+ })
981
+ );
982
+ }
983
+ }
984
+ };
985
+
986
+ // src/utils/AuditManage.ts
987
+ var import_koishi4 = require("koishi");
988
+ var fs4 = __toESM(require("fs"));
989
+ var path4 = __toESM(require("path"));
990
+ var AuditManager = class {
991
+ constructor(ctx, config, caveDir, idManager) {
992
+ this.ctx = ctx;
993
+ this.config = config;
994
+ this.caveDir = caveDir;
995
+ this.idManager = idManager;
996
+ }
997
+ static {
998
+ __name(this, "AuditManager");
999
+ }
1000
+ logger = new import_koishi4.Logger("AuditManager");
1001
+ async processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
1002
+ if (pendingData.length === 0) {
1003
+ return this.sendMessage(session, "commands.cave.audit.noPending", [], true);
1004
+ }
1005
+ if (typeof targetId === "number") {
1006
+ return await this.handleSingleAudit(
1007
+ pendingData,
1008
+ isApprove,
1009
+ caveFilePath,
1010
+ resourceDir,
1011
+ pendingFilePath,
1012
+ targetId,
1013
+ session
1014
+ );
1015
+ }
1016
+ return await this.handleBatchAudit(
1017
+ pendingData,
1018
+ isApprove,
1019
+ caveFilePath,
1020
+ resourceDir,
1021
+ pendingFilePath,
1022
+ session
1023
+ );
1024
+ }
1025
+ async handleSingleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, targetId, session) {
1026
+ const targetCave = pendingData.find((item) => item.cave_id === targetId);
1027
+ if (!targetCave) {
1028
+ return this.sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
1029
+ }
1030
+ const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
1031
+ if (isApprove) {
1032
+ const oldCaveData = await FileHandler.readJsonData(caveFilePath);
1033
+ const newCaveData = [...oldCaveData, {
1034
+ ...targetCave,
1035
+ cave_id: targetId,
1036
+ elements: this.cleanElementsForSave(targetCave.elements, false)
1037
+ }];
1038
+ await FileHandler.withTransaction([
1039
+ {
1040
+ filePath: caveFilePath,
1041
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
1042
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
1043
+ },
1044
+ {
1045
+ filePath: pendingFilePath,
1046
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
1047
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1048
+ }
1049
+ ]);
1050
+ await this.idManager.addStat(targetCave.contributor_number, targetId);
1051
+ } else {
1052
+ await FileHandler.writeJsonData(pendingFilePath, newPendingData);
1053
+ await this.idManager.markDeleted(targetId);
1054
+ await this.deleteMediaFiles(targetCave, resourceDir);
1055
+ }
1056
+ const remainingCount = newPendingData.length;
1057
+ if (remainingCount > 0) {
1058
+ const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
1059
+ const action = isApprove ? "auditPassed" : "auditRejected";
1060
+ return this.sendMessage(session, "commands.cave.audit.pendingResult", [
1061
+ session.text(`commands.cave.audit.${action}`),
1062
+ remainingCount,
1063
+ remainingIds
1064
+ ], false);
1065
+ }
1066
+ return this.sendMessage(
1067
+ session,
1068
+ isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
1069
+ [],
1070
+ false
1071
+ );
1072
+ }
1073
+ async handleBatchAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session) {
1074
+ const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
1075
+ let processedCount = 0;
1076
+ if (isApprove && data) {
1077
+ const oldData = [...data];
1078
+ const newData = [...data];
1079
+ await FileHandler.withTransaction([
1080
+ {
1081
+ filePath: caveFilePath,
1082
+ operation: /* @__PURE__ */ __name(async () => {
1083
+ for (const cave of pendingData) {
1084
+ newData.push({
1085
+ ...cave,
1086
+ cave_id: cave.cave_id,
1087
+ elements: this.cleanElementsForSave(cave.elements, false)
1088
+ });
1089
+ processedCount++;
1090
+ await this.idManager.addStat(cave.contributor_number, cave.cave_id);
1091
+ }
1092
+ return FileHandler.writeJsonData(caveFilePath, newData);
1093
+ }, "operation"),
1094
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
1095
+ },
1096
+ {
1097
+ filePath: pendingFilePath,
1098
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
1099
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1100
+ }
1101
+ ]);
1102
+ } else {
1103
+ for (const cave of pendingData) {
1104
+ await this.idManager.markDeleted(cave.cave_id);
1105
+ await this.deleteMediaFiles(cave, resourceDir);
1106
+ processedCount++;
767
1107
  }
768
- } catch (error) {
769
- logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
1108
+ await FileHandler.writeJsonData(pendingFilePath, []);
770
1109
  }
1110
+ return this.sendMessage(session, "commands.cave.audit.batchAuditResult", [
1111
+ isApprove ? "通过" : "拒绝",
1112
+ processedCount,
1113
+ pendingData.length
1114
+ ], false);
771
1115
  }
772
- async processCaveTextHashes(cave) {
773
- const textElements = cave.elements?.filter((el) => el.type === "text" && el.content) || [];
774
- if (textElements.length === 0) return;
775
- try {
776
- const hashes = textElements.map((el) => _HashStorage.hashText(el.content));
777
- if (hashes.length) {
778
- this.textHashes.set(cave.cave_id, hashes);
1116
+ async sendAuditMessage(cave, content, session) {
1117
+ const auditMessage = `${session.text("commands.cave.audit.title")}
1118
+ ${content}
1119
+ ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
1120
+ for (const managerId of this.config.manager) {
1121
+ const bot = this.ctx.bots[0];
1122
+ if (bot) {
1123
+ try {
1124
+ await bot.sendPrivateMessage(managerId, auditMessage);
1125
+ } catch (error) {
1126
+ this.logger.error(session.text("commands.cave.audit.sendFailed", [managerId]));
1127
+ }
779
1128
  }
780
- } catch (error) {
781
- logger3.error(`Failed to process text hashes for cave ${cave.cave_id}: ${error.message}`);
782
1129
  }
783
1130
  }
784
- /**
785
- * 保存哈希数据到文件
786
- * @private
787
- */
788
- async saveHashes() {
789
- try {
790
- const data = {
791
- imageHashes: Object.fromEntries(this.imageHashes),
792
- textHashes: Object.fromEntries(this.textHashes),
793
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
794
- };
795
- await FileHandler.writeJsonData(this.filePath, [data]);
796
- } catch (error) {
797
- logger3.error(`Failed to save hash data: ${error.message}`);
798
- throw error;
1131
+ async deleteMediaFiles(cave, resourceDir) {
1132
+ if (cave.elements) {
1133
+ for (const element of cave.elements) {
1134
+ if ((element.type === "img" || element.type === "video") && element.file) {
1135
+ const fullPath = path4.join(resourceDir, element.file);
1136
+ if (fs4.existsSync(fullPath)) {
1137
+ await fs4.promises.unlink(fullPath);
1138
+ }
1139
+ }
1140
+ }
799
1141
  }
800
1142
  }
801
- /**
802
- * 加载回声洞数据
803
- * @returns 回声洞数据数组
804
- * @private
805
- */
806
- async loadCaveData() {
807
- const data = await FileHandler.readJsonData(this.caveFilePath);
808
- return Array.isArray(data) ? data.flat() : [];
1143
+ cleanElementsForSave(elements, keepIndex = false) {
1144
+ if (!elements?.length) return [];
1145
+ const cleanedElements = elements.map((element) => {
1146
+ if (element.type === "text") {
1147
+ const cleanedElement = {
1148
+ type: "text",
1149
+ content: element.content
1150
+ };
1151
+ if (keepIndex) cleanedElement.index = element.index;
1152
+ return cleanedElement;
1153
+ } else if (element.type === "img" || element.type === "video") {
1154
+ const mediaElement = element;
1155
+ const cleanedElement = {
1156
+ type: mediaElement.type
1157
+ };
1158
+ if (mediaElement.file) cleanedElement.file = mediaElement.file;
1159
+ if (keepIndex) cleanedElement.index = element.index;
1160
+ return cleanedElement;
1161
+ }
1162
+ return element;
1163
+ });
1164
+ return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
809
1165
  }
810
- /**
811
- * 计算文本的哈希值
812
- * @param text - 要计算哈希的文本
813
- * @returns MD5哈希值
814
- */
815
- static hashText(text) {
816
- return import_crypto.default.createHash("md5").update(text).digest("hex");
1166
+ async sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
1167
+ try {
1168
+ const msg = await session.send(session.text(key, params));
1169
+ if (isTemp && msg) {
1170
+ setTimeout(async () => {
1171
+ try {
1172
+ await session.bot.deleteMessage(session.channelId, msg);
1173
+ } catch (error) {
1174
+ this.logger.debug(`Failed to delete temporary message: ${error.message}`);
1175
+ }
1176
+ }, timeout);
1177
+ }
1178
+ } catch (error) {
1179
+ this.logger.error(`Failed to send message: ${error.message}`);
1180
+ }
1181
+ return "";
817
1182
  }
818
1183
  };
819
1184
 
820
1185
  // src/index.ts
821
1186
  var name = "best-cave";
822
1187
  var inject = ["database"];
823
- var Config = import_koishi4.Schema.object({
824
- manager: import_koishi4.Schema.array(import_koishi4.Schema.string()).required(),
1188
+ var Config = import_koishi5.Schema.object({
1189
+ manager: import_koishi5.Schema.array(import_koishi5.Schema.string()).required(),
825
1190
  // 管理员用户ID
826
- number: import_koishi4.Schema.number().default(60),
1191
+ number: import_koishi5.Schema.number().default(60),
827
1192
  // 冷却时间(秒)
828
- enableAudit: import_koishi4.Schema.boolean().default(false),
1193
+ enableAudit: import_koishi5.Schema.boolean().default(false),
829
1194
  // 启用审核
830
- imageMaxSize: import_koishi4.Schema.number().default(4),
1195
+ enableTextDuplicate: import_koishi5.Schema.boolean().default(true),
1196
+ // 启用文本查重
1197
+ textDuplicateThreshold: import_koishi5.Schema.number().default(0.9),
1198
+ // 文本查重阈值
1199
+ enableImageDuplicate: import_koishi5.Schema.boolean().default(true),
1200
+ // 开启图片查重
1201
+ imageDuplicateThreshold: import_koishi5.Schema.number().default(0.8),
1202
+ // 图片查重阈值
1203
+ imageMaxSize: import_koishi5.Schema.number().default(4),
831
1204
  // 图片大小限制(MB)
832
- enableMD5: import_koishi4.Schema.boolean().default(true),
833
- // 启用MD5查重
834
- enableDuplicate: import_koishi4.Schema.boolean().default(true),
835
- // 启用相似度查重
836
- duplicateThreshold: import_koishi4.Schema.number().default(0.8),
837
- // 相似度查重阈值(0-1)
838
- allowVideo: import_koishi4.Schema.boolean().default(true),
1205
+ allowVideo: import_koishi5.Schema.boolean().default(true),
839
1206
  // 允许视频
840
- videoMaxSize: import_koishi4.Schema.number().default(16),
1207
+ videoMaxSize: import_koishi5.Schema.number().default(16),
841
1208
  // 视频大小限制(MB)
842
- enablePagination: import_koishi4.Schema.boolean().default(false),
1209
+ enablePagination: import_koishi5.Schema.boolean().default(false),
843
1210
  // 启用分页
844
- itemsPerPage: import_koishi4.Schema.number().default(10),
1211
+ itemsPerPage: import_koishi5.Schema.number().default(10),
845
1212
  // 每页条数
846
- blacklist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([]),
1213
+ blacklist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([]),
847
1214
  // 黑名单
848
- whitelist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([])
1215
+ whitelist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([])
849
1216
  // 白名单
850
1217
  }).i18n({
851
1218
  "zh-CN": require_zh_CN()._config,
@@ -854,19 +1221,20 @@ var Config = import_koishi4.Schema.object({
854
1221
  async function apply(ctx, config) {
855
1222
  ctx.i18n.define("zh-CN", require_zh_CN());
856
1223
  ctx.i18n.define("en-US", require_en_US());
857
- const dataDir = path4.join(ctx.baseDir, "data");
858
- const caveDir = path4.join(dataDir, "cave");
1224
+ const dataDir = path5.join(ctx.baseDir, "data");
1225
+ const caveDir = path5.join(dataDir, "cave");
859
1226
  await FileHandler.ensureDirectory(dataDir);
860
1227
  await FileHandler.ensureDirectory(caveDir);
861
- await FileHandler.ensureDirectory(path4.join(caveDir, "resources"));
862
- await FileHandler.ensureJsonFile(path4.join(caveDir, "cave.json"));
863
- await FileHandler.ensureJsonFile(path4.join(caveDir, "pending.json"));
864
- await FileHandler.ensureJsonFile(path4.join(caveDir, "hash.json"));
1228
+ await FileHandler.ensureDirectory(path5.join(caveDir, "resources"));
1229
+ await FileHandler.ensureJsonFile(path5.join(caveDir, "cave.json"));
1230
+ await FileHandler.ensureJsonFile(path5.join(caveDir, "pending.json"));
1231
+ await FileHandler.ensureJsonFile(path5.join(caveDir, "hash.json"));
865
1232
  const idManager = new IdManager(ctx.baseDir);
866
- const hashStorage = new HashStorage(caveDir);
1233
+ const contentHashManager = new ContentHashManager(caveDir);
1234
+ const auditManager = new AuditManager(ctx, config, caveDir, idManager);
867
1235
  await Promise.all([
868
- idManager.initialize(path4.join(caveDir, "cave.json"), path4.join(caveDir, "pending.json")),
869
- hashStorage.initialize()
1236
+ idManager.initialize(path5.join(caveDir, "cave.json"), path5.join(caveDir, "pending.json")),
1237
+ contentHashManager.initialize()
870
1238
  ]);
871
1239
  const lastUsed = /* @__PURE__ */ new Map();
872
1240
  async function processList(session, config2, userId, pageNum = 1) {
@@ -895,13 +1263,13 @@ async function apply(ctx, config) {
895
1263
  const pendingData = await FileHandler.readJsonData(pendingFilePath);
896
1264
  const isApprove = Boolean(options.p);
897
1265
  if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
898
- return await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
1266
+ return await auditManager.processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
899
1267
  }
900
1268
  const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
901
1269
  if (isNaN(id)) {
902
1270
  return sendMessage(session, "commands.cave.error.invalidId", [], true);
903
1271
  }
904
- return sendMessage(session, await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, id), [], true);
1272
+ return await auditManager.processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, id);
905
1273
  }
906
1274
  __name(processAudit, "processAudit");
907
1275
  async function processView(caveFilePath, resourceDir, session, options, content) {
@@ -944,14 +1312,15 @@ async function apply(ctx, config) {
944
1312
  }
945
1313
  const caveContent = await buildMessage(targetCave, resourceDir, session);
946
1314
  if (targetCave.elements) {
947
- const hashStorage2 = new HashStorage(caveDir);
948
- await hashStorage2.initialize();
949
- await hashStorage2.clearHashes(caveId);
1315
+ await contentHashManager.updateCaveContent(caveId, {
1316
+ images: void 0,
1317
+ texts: void 0
1318
+ });
950
1319
  for (const element of targetCave.elements) {
951
1320
  if ((element.type === "img" || element.type === "video") && element.file) {
952
- const fullPath = path4.join(resourceDir, element.file);
953
- if (fs4.existsSync(fullPath)) {
954
- await fs4.promises.unlink(fullPath);
1321
+ const fullPath = path5.join(resourceDir, element.file);
1322
+ if (fs5.existsSync(fullPath)) {
1323
+ await fs5.promises.unlink(fullPath);
955
1324
  }
956
1325
  }
957
1326
  }
@@ -971,36 +1340,24 @@ async function apply(ctx, config) {
971
1340
  }
972
1341
  __name(processDelete, "processDelete");
973
1342
  async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
1343
+ let caveId;
974
1344
  try {
975
1345
  const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
976
1346
  await sendMessage(session, "commands.cave.add.noContent", [], true);
977
1347
  const reply = await session.prompt({ timeout: 6e4 });
978
- if (!reply) session.text("commands.cave.add.operationTimeout");
1348
+ if (!reply) throw new Error(session.text("commands.cave.add.operationTimeout"));
979
1349
  return reply;
980
1350
  })();
981
- const caveId = await idManager.getNextId();
1351
+ caveId = await idManager.getNextId();
982
1352
  if (inputContent.includes("/app/.config/QQ/")) {
983
1353
  return sendMessage(session, "commands.cave.add.localFileNotAllowed", [], true);
984
1354
  }
985
1355
  const bypassAudit = config2.whitelist.includes(session.userId) || config2.whitelist.includes(session.guildId) || config2.whitelist.includes(session.channelId);
986
1356
  const { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(inputContent, config2, session);
987
- const pureText = textParts.filter((tp) => tp.type === "text").map((tp) => tp.content.trim()).join("\n").trim();
988
- if (config2.enableMD5 && pureText) {
989
- const textHash = HashStorage.hashText(pureText);
990
- const textDuplicates = await hashStorage.findDuplicates("text", [textHash]);
991
- if (textDuplicates[0]) {
992
- const data2 = await FileHandler.readJsonData(caveFilePath);
993
- const duplicateCave = data2.find((item) => item.cave_id === textDuplicates[0].caveId);
994
- if (duplicateCave) {
995
- const message = session.text("commands.cave.error.exactDuplicateFound");
996
- await session.send(message + await buildMessage(duplicateCave, resourceDir, session));
997
- throw new Error("duplicate_found");
998
- }
999
- }
1000
- }
1001
1357
  if (videoUrls.length > 0 && !config2.allowVideo) {
1002
1358
  return sendMessage(session, "commands.cave.add.videoDisabled", [], true);
1003
1359
  }
1360
+ const imageBuffers = [];
1004
1361
  const [savedImages, savedVideos] = await Promise.all([
1005
1362
  imageUrls.length > 0 ? saveMedia(
1006
1363
  imageUrls,
@@ -1010,7 +1367,9 @@ async function apply(ctx, config) {
1010
1367
  "img",
1011
1368
  config2,
1012
1369
  ctx2,
1013
- session
1370
+ session,
1371
+ imageBuffers
1372
+ // 添加参数用于收集buffer
1014
1373
  ) : [],
1015
1374
  videoUrls.length > 0 ? saveMedia(
1016
1375
  videoUrls,
@@ -1033,7 +1392,7 @@ async function apply(ctx, config) {
1033
1392
  // 保持原始文本和图片的相对位置
1034
1393
  index: el.index
1035
1394
  }))
1036
- ].sort((a) => a.index - a.index),
1395
+ ].sort((a, b) => a.index - a.index),
1037
1396
  contributor_number: session.userId,
1038
1397
  contributor_name: session.username
1039
1398
  };
@@ -1044,19 +1403,24 @@ async function apply(ctx, config) {
1044
1403
  index: Number.MAX_SAFE_INTEGER
1045
1404
  });
1046
1405
  }
1047
- const existingData = await FileHandler.readJsonData(caveFilePath);
1048
- const hasImages = existingData.some(
1049
- (cave) => cave.elements?.some((element) => element.type === "img" && element.file)
1050
- );
1051
- if (hasImages) {
1052
- await hashStorage.initialize();
1406
+ const hashStorage = new ContentHashManager(path5.join(ctx2.baseDir, "data", "cave"));
1407
+ await hashStorage.initialize();
1408
+ const hashStatus = await hashStorage.getStatus();
1409
+ if (!hashStatus.lastUpdated || hashStatus.entries.length === 0) {
1410
+ const existingData = await FileHandler.readJsonData(caveFilePath);
1411
+ const hasImages = existingData.some(
1412
+ (cave) => cave.elements?.some((element) => element.type === "img" && element.file)
1413
+ );
1414
+ if (hasImages) {
1415
+ await hashStorage.updateAllCaves(true);
1416
+ }
1053
1417
  }
1054
1418
  if (config2.enableAudit && !bypassAudit) {
1055
1419
  const pendingData = await FileHandler.readJsonData(pendingFilePath);
1056
1420
  pendingData.push(newCave);
1057
1421
  await Promise.all([
1058
1422
  FileHandler.writeJsonData(pendingFilePath, pendingData),
1059
- sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir, session), session)
1423
+ auditManager.sendAuditMessage(newCave, await buildMessage(newCave, resourceDir, session), session)
1060
1424
  ]);
1061
1425
  return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
1062
1426
  }
@@ -1065,137 +1429,58 @@ async function apply(ctx, config) {
1065
1429
  ...newCave,
1066
1430
  elements: cleanElementsForSave(newCave.elements, false)
1067
1431
  });
1432
+ if (config2.enableImageDuplicate || config2.enableTextDuplicate) {
1433
+ try {
1434
+ const duplicateResults = await contentHashManager.findDuplicates({
1435
+ images: config2.enableImageDuplicate ? imageBuffers : void 0,
1436
+ texts: config2.enableTextDuplicate ? textParts.filter((p) => p.type === "text").map((p) => p.content) : void 0
1437
+ }, {
1438
+ image: config2.imageDuplicateThreshold,
1439
+ text: config2.textDuplicateThreshold
1440
+ });
1441
+ for (const result of duplicateResults) {
1442
+ if (!result) continue;
1443
+ const originalCave = data.find((item) => item.cave_id === result.caveId);
1444
+ if (!originalCave) continue;
1445
+ await idManager.markDeleted(caveId);
1446
+ const duplicateMessage = session.text(
1447
+ "commands.cave.error.similarDuplicateFound",
1448
+ [(result.similarity * 100).toFixed(1)]
1449
+ );
1450
+ await session.send(duplicateMessage + await buildMessage(originalCave, resourceDir, session));
1451
+ throw new Error("duplicate_found");
1452
+ }
1453
+ } catch (error) {
1454
+ if (error.message !== "duplicate_found") {
1455
+ await idManager.markDeleted(caveId);
1456
+ }
1457
+ if (error.message === "duplicate_found") {
1458
+ return "";
1459
+ }
1460
+ return sendMessage(session, "commands.cave.error.addFailed", [], true);
1461
+ }
1462
+ }
1068
1463
  await Promise.all([
1069
1464
  FileHandler.writeJsonData(caveFilePath, data),
1070
- pureText && config2.enableMD5 ? hashStorage.updateHash(caveId, "text", pureText) : Promise.resolve(),
1071
- savedImages?.length ? Promise.all(savedImages.map((buffer) => hashStorage.updateHash(caveId, "image", buffer))) : Promise.resolve()
1465
+ contentHashManager.updateCaveContent(caveId, {
1466
+ images: savedImages.length > 0 ? await Promise.all(savedImages.map((file) => fs5.promises.readFile(path5.join(resourceDir, file)))) : void 0,
1467
+ texts: textParts.filter((p) => p.type === "text").map((p) => p.content)
1468
+ })
1072
1469
  ]);
1073
1470
  await idManager.addStat(session.userId, caveId);
1074
- if (config2.enableMD5 && pureText) {
1075
- const textHash = HashStorage.hashText(pureText);
1076
- await hashStorage.updateHash(caveId, "text", textHash);
1077
- }
1078
1471
  return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
1079
1472
  } catch (error) {
1080
- logger4.error(`Failed to process add command: ${error.message}`);
1081
- }
1082
- }
1083
- __name(processAdd, "processAdd");
1084
- async function handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
1085
- if (pendingData.length === 0) {
1086
- return sendMessage(session, "commands.cave.audit.noPending", [], true);
1087
- }
1088
- if (typeof targetId === "number") {
1089
- const targetCave = pendingData.find((item) => item.cave_id === targetId);
1090
- if (!targetCave) {
1091
- return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
1473
+ if (error.message !== "duplicate_found") {
1474
+ await idManager.markDeleted(caveId);
1092
1475
  }
1093
- const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
1094
- if (isApprove) {
1095
- const oldCaveData = await FileHandler.readJsonData(caveFilePath);
1096
- const newCaveData = [...oldCaveData, {
1097
- ...targetCave,
1098
- cave_id: targetId,
1099
- elements: cleanElementsForSave(targetCave.elements, false)
1100
- }];
1101
- await FileHandler.withTransaction([
1102
- {
1103
- filePath: caveFilePath,
1104
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
1105
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
1106
- },
1107
- {
1108
- filePath: pendingFilePath,
1109
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
1110
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1111
- }
1112
- ]);
1113
- await idManager.addStat(targetCave.contributor_number, targetId);
1114
- } else {
1115
- await FileHandler.writeJsonData(pendingFilePath, newPendingData);
1116
- await idManager.markDeleted(targetId);
1117
- if (targetCave.elements) {
1118
- for (const element of targetCave.elements) {
1119
- if ((element.type === "img" || element.type === "video") && element.file) {
1120
- const fullPath = path4.join(resourceDir, element.file);
1121
- if (fs4.existsSync(fullPath)) {
1122
- await fs4.promises.unlink(fullPath);
1123
- }
1124
- }
1125
- }
1126
- }
1127
- }
1128
- const remainingCount = newPendingData.length;
1129
- if (remainingCount > 0) {
1130
- const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
1131
- const action = isApprove ? "auditPassed" : "auditRejected";
1132
- return sendMessage(session, "commands.cave.audit.pendingResult", [
1133
- session.text(`commands.cave.audit.${action}`),
1134
- remainingCount,
1135
- remainingIds
1136
- ], false);
1137
- }
1138
- return sendMessage(
1139
- session,
1140
- isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
1141
- [],
1142
- false
1143
- );
1144
- }
1145
- const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
1146
- let processedCount = 0;
1147
- if (isApprove && data) {
1148
- const oldData = [...data];
1149
- const newData = [...data];
1150
- await FileHandler.withTransaction([
1151
- {
1152
- filePath: caveFilePath,
1153
- operation: /* @__PURE__ */ __name(async () => {
1154
- for (const cave of pendingData) {
1155
- newData.push({
1156
- ...cave,
1157
- cave_id: cave.cave_id,
1158
- elements: cleanElementsForSave(cave.elements, false)
1159
- });
1160
- processedCount++;
1161
- await idManager.addStat(cave.contributor_number, cave.cave_id);
1162
- }
1163
- return FileHandler.writeJsonData(caveFilePath, newData);
1164
- }, "operation"),
1165
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
1166
- },
1167
- {
1168
- filePath: pendingFilePath,
1169
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
1170
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1171
- }
1172
- ]);
1173
- } else {
1174
- for (const cave of pendingData) {
1175
- await idManager.markDeleted(cave.cave_id);
1176
- }
1177
- await FileHandler.writeJsonData(pendingFilePath, []);
1178
- for (const cave of pendingData) {
1179
- if (cave.elements) {
1180
- for (const element of cave.elements) {
1181
- if ((element.type === "img" || element.type === "video") && element.file) {
1182
- const fullPath = path4.join(resourceDir, element.file);
1183
- if (fs4.existsSync(fullPath)) {
1184
- await fs4.promises.unlink(fullPath);
1185
- }
1186
- }
1187
- }
1188
- }
1189
- processedCount++;
1476
+ if (error.message === "duplicate_found") {
1477
+ return "";
1190
1478
  }
1479
+ logger4.error(`Failed to process add command: ${error.message}`);
1480
+ return sendMessage(session, "commands.cave.error.addFailed", [], true);
1191
1481
  }
1192
- return sendMessage(session, "commands.cave.audit.batchAuditResult", [
1193
- isApprove ? "通过" : "拒绝",
1194
- processedCount,
1195
- pendingData.length
1196
- ], false);
1197
1482
  }
1198
- __name(handleAudit, "handleAudit");
1483
+ __name(processAdd, "processAdd");
1199
1484
  ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
1200
1485
  if (config.blacklist.includes(session.userId)) {
1201
1486
  return sendMessage(session, "commands.cave.message.blacklisted", [], true);
@@ -1204,11 +1489,11 @@ async function apply(ctx, config) {
1204
1489
  return sendMessage(session, "commands.cave.message.managerOnly", [], true);
1205
1490
  }
1206
1491
  }).action(async ({ session, options }, ...content) => {
1207
- const dataDir2 = path4.join(ctx.baseDir, "data");
1208
- const caveDir2 = path4.join(dataDir2, "cave");
1209
- const caveFilePath = path4.join(caveDir2, "cave.json");
1210
- const resourceDir = path4.join(caveDir2, "resources");
1211
- const pendingFilePath = path4.join(caveDir2, "pending.json");
1492
+ const dataDir2 = path5.join(ctx.baseDir, "data");
1493
+ const caveDir2 = path5.join(dataDir2, "cave");
1494
+ const caveFilePath = path5.join(caveDir2, "cave.json");
1495
+ const resourceDir = path5.join(caveDir2, "resources");
1496
+ const pendingFilePath = path5.join(caveDir2, "pending.json");
1212
1497
  const needsCooldown = !options.l && !options.a && !options.p && !options.d;
1213
1498
  if (needsCooldown) {
1214
1499
  const guildId = session.guildId;
@@ -1255,7 +1540,7 @@ async function apply(ctx, config) {
1255
1540
  });
1256
1541
  }
1257
1542
  __name(apply, "apply");
1258
- var logger4 = new import_koishi4.Logger("cave");
1543
+ var logger4 = new import_koishi5.Logger("cave");
1259
1544
  async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
1260
1545
  try {
1261
1546
  const msg = await session.send(session.text(key, params));
@@ -1274,22 +1559,6 @@ async function sendMessage(session, key, params = [], isTemp = true, timeout = 1
1274
1559
  return "";
1275
1560
  }
1276
1561
  __name(sendMessage, "sendMessage");
1277
- async function sendAuditMessage(ctx, config, cave, content, session) {
1278
- const auditMessage = `${session.text("commands.cave.audit.title")}
1279
- ${content}
1280
- ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
1281
- for (const managerId of config.manager) {
1282
- const bot = ctx.bots[0];
1283
- if (bot) {
1284
- try {
1285
- await bot.sendPrivateMessage(managerId, auditMessage);
1286
- } catch (error) {
1287
- logger4.error(session.text("commands.cave.audit.sendFailed", [managerId]));
1288
- }
1289
- }
1290
- }
1291
- }
1292
- __name(sendAuditMessage, "sendAuditMessage");
1293
1562
  function cleanElementsForSave(elements, keepIndex = false) {
1294
1563
  if (!elements?.length) return [];
1295
1564
  const cleanedElements = elements.map((element) => {
@@ -1315,7 +1584,7 @@ function cleanElementsForSave(elements, keepIndex = false) {
1315
1584
  }
1316
1585
  __name(cleanElementsForSave, "cleanElementsForSave");
1317
1586
  async function processMediaFile(filePath, type) {
1318
- const data = await fs4.promises.readFile(filePath).catch(() => null);
1587
+ const data = await fs5.promises.readFile(filePath).catch(() => null);
1319
1588
  if (!data) return null;
1320
1589
  return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
1321
1590
  }
@@ -1332,10 +1601,10 @@ async function buildMessage(cave, resourceDir, session) {
1332
1601
  session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
1333
1602
  ].join("\n");
1334
1603
  await session?.send(basicInfo);
1335
- const filePath = path4.join(resourceDir, videoElement.file);
1604
+ const filePath = path5.join(resourceDir, videoElement.file);
1336
1605
  const base64Data = await processMediaFile(filePath, "video");
1337
1606
  if (base64Data && session) {
1338
- await session.send((0, import_koishi4.h)("video", { src: base64Data }));
1607
+ await session.send((0, import_koishi5.h)("video", { src: base64Data }));
1339
1608
  }
1340
1609
  return "";
1341
1610
  }
@@ -1344,10 +1613,10 @@ async function buildMessage(cave, resourceDir, session) {
1344
1613
  if (element.type === "text") {
1345
1614
  lines.push(element.content);
1346
1615
  } else if (element.type === "img" && element.file) {
1347
- const filePath = path4.join(resourceDir, element.file);
1616
+ const filePath = path5.join(resourceDir, element.file);
1348
1617
  const base64Data = await processMediaFile(filePath, "image");
1349
1618
  if (base64Data) {
1350
- lines.push((0, import_koishi4.h)("image", { src: base64Data }));
1619
+ lines.push((0, import_koishi5.h)("image", { src: base64Data }));
1351
1620
  }
1352
1621
  }
1353
1622
  }
@@ -1396,13 +1665,13 @@ async function extractMediaContent(originalContent, config, session) {
1396
1665
  return { imageUrls, imageElements, videoUrls, videoElements, textParts };
1397
1666
  }
1398
1667
  __name(extractMediaContent, "extractMediaContent");
1399
- async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session) {
1668
+ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session, buffers) {
1400
1669
  const accept = mediaType === "img" ? "image/*" : "video/*";
1401
- const hashStorage = new HashStorage(path4.join(ctx.baseDir, "data", "cave"));
1670
+ const hashStorage = new ContentHashManager(path5.join(ctx.baseDir, "data", "cave"));
1402
1671
  await hashStorage.initialize();
1403
1672
  const downloadTasks = urls.map(async (url, i) => {
1404
1673
  const fileName = fileNames[i];
1405
- const ext = path4.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
1674
+ const ext = path5.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
1406
1675
  try {
1407
1676
  const response = await ctx.http(decodeURIComponent(url).replace(/&amp;/g, "&"), {
1408
1677
  method: "GET",
@@ -1416,75 +1685,58 @@ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config
1416
1685
  });
1417
1686
  if (!response.data) throw new Error("empty_response");
1418
1687
  const buffer = Buffer.from(response.data);
1419
- if (mediaType === "img") {
1420
- const baseName = path4.basename(fileName || "md5", ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1421
- if (config.enableMD5) {
1422
- const files = await fs4.promises.readdir(resourceDir);
1423
- const duplicateFile = files.find((file) => file.startsWith(baseName + "_"));
1424
- if (duplicateFile) {
1425
- const duplicateCaveId = parseInt(duplicateFile.split("_")[1]);
1426
- if (!isNaN(duplicateCaveId)) {
1427
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1428
- const data = await FileHandler.readJsonData(caveFilePath);
1429
- const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1430
- if (originalCave) {
1431
- const message = session.text("commands.cave.error.exactDuplicateFound");
1432
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1433
- throw new Error("duplicate_found");
1434
- }
1435
- }
1688
+ if (buffers && mediaType === "img") {
1689
+ buffers.push(buffer);
1690
+ }
1691
+ const md5 = path5.basename(fileName || `${mediaType}`, ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1692
+ const files = await fs5.promises.readdir(resourceDir);
1693
+ const duplicateFile = files.find((file) => {
1694
+ const match = file.match(/^\d+_([^.]+)/);
1695
+ return match && match[1] === md5;
1696
+ });
1697
+ if (duplicateFile) {
1698
+ const duplicateCaveId = parseInt(duplicateFile.split("_")[0]);
1699
+ if (!isNaN(duplicateCaveId)) {
1700
+ const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
1701
+ const data = await FileHandler.readJsonData(caveFilePath);
1702
+ const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1703
+ if (originalCave) {
1704
+ const message = session.text("commands.cave.error.exactDuplicateFound");
1705
+ await session.send(message + await buildMessage(originalCave, resourceDir, session));
1706
+ throw new Error("duplicate_found");
1436
1707
  }
1437
1708
  }
1438
- if (config.enableDuplicate) {
1439
- const hashStorage2 = new HashStorage(path4.join(ctx.baseDir, "data", "cave"));
1440
- await hashStorage2.initialize();
1441
- const result = await hashStorage2.findDuplicates("image", [buffer.toString("base64")], config.duplicateThreshold);
1442
- if (result.length > 0 && result[0] !== null) {
1443
- const duplicate = result[0];
1444
- const similarity = duplicate.similarity;
1445
- if (similarity >= config.duplicateThreshold) {
1446
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1447
- const data = await FileHandler.readJsonData(caveFilePath);
1448
- const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
1449
- if (originalCave) {
1450
- const message = session.text(
1451
- "commands.cave.error.similarDuplicateFound",
1452
- [(similarity * 100).toFixed(1)]
1453
- );
1454
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1455
- throw new Error("duplicate_found");
1456
- }
1457
- }
1709
+ }
1710
+ if (mediaType === "img" && config.enableImageDuplicate) {
1711
+ const result = await hashStorage.findDuplicates(
1712
+ { images: [buffer] },
1713
+ {
1714
+ image: config.imageDuplicateThreshold,
1715
+ text: config.textDuplicateThreshold
1458
1716
  }
1459
- }
1460
- const finalFileName = `${caveId}_${baseName}${ext}`;
1461
- const filePath = path4.join(resourceDir, finalFileName);
1462
- await FileHandler.saveMediaFile(filePath, buffer);
1463
- return finalFileName;
1464
- } else {
1465
- const baseName = path4.basename(fileName || "video", ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1466
- if (config.enableMD5) {
1467
- const files = await fs4.promises.readdir(resourceDir);
1468
- const duplicateFile = files.find((file) => file.startsWith(baseName + "_"));
1469
- if (duplicateFile) {
1470
- const duplicateCaveId = parseInt(duplicateFile.split("_")[1]);
1471
- if (!isNaN(duplicateCaveId)) {
1472
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1473
- const data = await FileHandler.readJsonData(caveFilePath);
1474
- const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1475
- if (originalCave) {
1476
- const message = session.text("commands.cave.error.exactDuplicateFound");
1477
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1478
- throw new Error("duplicate_found");
1479
- }
1717
+ );
1718
+ if (result.length > 0 && result[0] !== null) {
1719
+ const duplicate = result[0];
1720
+ const similarity = duplicate.similarity;
1721
+ if (similarity >= config.imageDuplicateThreshold) {
1722
+ const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
1723
+ const data = await FileHandler.readJsonData(caveFilePath);
1724
+ const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
1725
+ if (originalCave) {
1726
+ const message = session.text(
1727
+ "commands.cave.error.similarDuplicateFound",
1728
+ [(similarity * 100).toFixed(1)]
1729
+ );
1730
+ await session.send(message + await buildMessage(originalCave, resourceDir, session));
1731
+ throw new Error("duplicate_found");
1480
1732
  }
1481
1733
  }
1482
1734
  }
1483
- const finalFileName = `${caveId}_${baseName}${ext}`;
1484
- const filePath = path4.join(resourceDir, finalFileName);
1485
- await FileHandler.saveMediaFile(filePath, buffer);
1486
- return finalFileName;
1487
1735
  }
1736
+ const finalFileName = `${caveId}_${md5}${ext}`;
1737
+ const filePath = path5.join(resourceDir, finalFileName);
1738
+ await FileHandler.saveMediaFile(filePath, buffer);
1739
+ return finalFileName;
1488
1740
  } catch (error) {
1489
1741
  if (error.message === "duplicate_found") {
1490
1742
  throw error;