koishi-plugin-best-cave 1.4.1 → 1.5.1

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: "删除回声洞", l: "查询投稿统计" }, pass: { description: "通过回声洞审核", usage: "通过指定ID的回声洞审核\ncave.pass <ID> - 通过审核\ncave.pass all - 通过所有待审核内容\n" }, reject: { description: "拒绝回声洞审核", usage: "拒绝指定ID的回声洞审核\ncave.reject <ID> - 拒绝审核\ncave.reject all - 拒绝所有待审核内容\n" }, 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", l: "Query submission statistics" }, pass: { description: "Approve cave submission", usage: "Approve cave submission with specific ID\ncave.pass <ID> - Approve submission\ncave.pass all - Approve all pending submissions\n" }, reject: { description: "Reject cave submission", usage: "Reject cave submission with specific ID\ncave.reject <ID> - Reject submission\ncave.reject all - Reject all pending submissions\n" }, 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();
717
+ } catch (error) {
718
+ logger3.error(`Failed to update content hashes (cave ${caveId}): ${error.message}`);
719
+ }
720
+ }
721
+ /**
722
+ * 更新所有回声洞的哈希值
723
+ * @param isInitialBuild 是否为初始构建
724
+ */
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`);
660
769
  } catch (error) {
661
- logger3.error(`Failed to update ${type} hash for cave ${caveId}: ${error.message}`);
770
+ logger3.error(`Full update failed: ${error.message}`);
771
+ throw error;
662
772
  }
663
773
  }
664
774
  /**
665
- * 查找重复项
666
- * @param type - 查找类型(图像或文本)
667
- * @param hashes - 要查找的哈希值数组
668
- * @param threshold - 相似度阈值,默认为1
669
- * @returns 匹配结果数组
775
+ * 查找重复的图片
776
+ * @param content 待查找的图片buffer数组
777
+ * @param thresholds 相似度阈值
778
+ * @returns 匹配结果数组,包含索引、回声洞ID和相似度
670
779
  */
671
- async findDuplicates(type, hashes, threshold = 1) {
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,38 +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(`Progress: ${processedCount}/${total} caves (${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
- const stats = this.getStorageStats();
720
- logger3.info(`Cave Hashes Initialized: ${stats.text} text hashes, ${stats.image} image hashes`);
928
+ await this.saveContentHashes();
929
+ logger3.success(`Build completed. Processed ${processedCount}/${totalCaves} caves`);
721
930
  }
722
931
  /**
723
932
  * 更新缺失的哈希值
@@ -725,132 +934,285 @@ var HashStorage = class _HashStorage {
725
934
  */
726
935
  async updateMissingHashes() {
727
936
  const caveData = await this.loadCaveData();
728
- const missingImageCaves = caveData.filter((cave) => !this.imageHashes.has(cave.cave_id));
729
- const missingTextCaves = caveData.filter((cave) => !this.textHashes.has(cave.cave_id));
730
- const total = missingImageCaves.length + missingTextCaves.length;
731
- if (total > 0) {
732
- for (const cave of missingImageCaves) {
733
- await this.processCaveHashes(cave);
734
- }
735
- for (const cave of missingTextCaves) {
736
- await this.processCaveTextHashes(cave);
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++;
957
+ }
958
+ } catch (error) {
959
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
737
960
  }
738
- await this.saveHashes();
739
- const stats = this.getStorageStats();
740
- logger3.info(`Hash storage updated: ${stats.text} text hashes, ${stats.image} image hashes`);
741
961
  }
742
962
  }
743
963
  /**
744
- * 处理单个回声洞的哈希值
745
- * @param cave - 回声洞数据
964
+ * 批量处理数组项
965
+ * @param items 待处理项数组
966
+ * @param processor 处理函数
967
+ * @param batchSize 批处理大小
746
968
  * @private
747
969
  */
748
- async processCaveHashes(cave) {
749
- const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
750
- if (imgElements.length === 0) return;
751
- try {
752
- const hashes = await Promise.all(imgElements.map(async (el) => {
753
- const filePath = path3.join(this.resourceDir, el.file);
754
- return fs3.existsSync(filePath) ? ImageHasher.calculateHash(await fs3.promises.readFile(filePath)) : null;
755
- }));
756
- const validHashes = hashes.filter(Boolean);
757
- if (validHashes.length) {
758
- this.imageHashes.set(cave.cave_id, validHashes);
759
- await this.saveHashes();
760
- }
761
- } catch (error) {
762
- logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
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
+ );
763
982
  }
764
983
  }
765
- async processCaveTextHashes(cave) {
766
- const textElements = cave.elements?.filter((el) => el.type === "text" && el.content) || [];
767
- if (textElements.length === 0) return;
768
- try {
769
- const hashes = textElements.map((el) => _HashStorage.hashText(el.content));
770
- if (hashes.length) {
771
- this.textHashes.set(cave.cave_id, hashes);
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++;
772
1107
  }
773
- } catch (error) {
774
- logger3.error(`Failed to process text hashes for cave ${cave.cave_id}: ${error.message}`);
1108
+ await FileHandler.writeJsonData(pendingFilePath, []);
775
1109
  }
1110
+ return this.sendMessage(session, "commands.cave.audit.batchAuditResult", [
1111
+ isApprove ? "通过" : "拒绝",
1112
+ processedCount,
1113
+ pendingData.length
1114
+ ], false);
776
1115
  }
777
- /**
778
- * 保存哈希数据到文件
779
- * @private
780
- */
781
- async saveHashes() {
782
- try {
783
- const data = {
784
- imageHashes: Object.fromEntries(this.imageHashes),
785
- textHashes: Object.fromEntries(this.textHashes),
786
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
787
- };
788
- await FileHandler.writeJsonData(this.filePath, [data]);
789
- } catch (error) {
790
- logger3.error(`Failed to save hash data: ${error.message}`);
791
- throw error;
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
+ }
1128
+ }
792
1129
  }
793
1130
  }
794
- /**
795
- * 加载回声洞数据
796
- * @returns 回声洞数据数组
797
- * @private
798
- */
799
- async loadCaveData() {
800
- const data = await FileHandler.readJsonData(this.caveFilePath);
801
- return Array.isArray(data) ? data.flat() : [];
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
+ }
1141
+ }
802
1142
  }
803
- /**
804
- * 计算文本的哈希值
805
- * @param text - 要计算哈希的文本
806
- * @returns MD5哈希值
807
- */
808
- static hashText(text) {
809
- return import_crypto.default.createHash("md5").update(text).digest("hex");
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;
810
1165
  }
811
- /**
812
- * 获取存储统计数据
813
- * @private
814
- */
815
- getStorageStats() {
816
- const textCount = Array.from(this.textHashes.values()).reduce((sum, arr) => sum + arr.length, 0);
817
- const imageCount = Array.from(this.imageHashes.values()).reduce((sum, arr) => sum + arr.length, 0);
818
- return {
819
- text: textCount,
820
- image: imageCount
821
- };
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 "";
822
1182
  }
823
1183
  };
824
1184
 
825
1185
  // src/index.ts
826
1186
  var name = "best-cave";
827
1187
  var inject = ["database"];
828
- var Config = import_koishi4.Schema.object({
829
- 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(),
830
1190
  // 管理员用户ID
831
- number: import_koishi4.Schema.number().default(60),
1191
+ number: import_koishi5.Schema.number().default(60),
832
1192
  // 冷却时间(秒)
833
- enableAudit: import_koishi4.Schema.boolean().default(false),
1193
+ enableAudit: import_koishi5.Schema.boolean().default(false),
834
1194
  // 启用审核
835
- 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),
836
1204
  // 图片大小限制(MB)
837
- enableMD5: import_koishi4.Schema.boolean().default(true),
838
- // 启用MD5查重
839
- enableDuplicate: import_koishi4.Schema.boolean().default(true),
840
- // 启用相似度查重
841
- duplicateThreshold: import_koishi4.Schema.number().default(0.8),
842
- // 相似度查重阈值(0-1)
843
- allowVideo: import_koishi4.Schema.boolean().default(true),
1205
+ allowVideo: import_koishi5.Schema.boolean().default(true),
844
1206
  // 允许视频
845
- videoMaxSize: import_koishi4.Schema.number().default(16),
1207
+ videoMaxSize: import_koishi5.Schema.number().default(16),
846
1208
  // 视频大小限制(MB)
847
- enablePagination: import_koishi4.Schema.boolean().default(false),
1209
+ enablePagination: import_koishi5.Schema.boolean().default(false),
848
1210
  // 启用分页
849
- itemsPerPage: import_koishi4.Schema.number().default(10),
1211
+ itemsPerPage: import_koishi5.Schema.number().default(10),
850
1212
  // 每页条数
851
- blacklist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([]),
1213
+ blacklist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([]),
852
1214
  // 黑名单
853
- whitelist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([])
1215
+ whitelist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([])
854
1216
  // 白名单
855
1217
  }).i18n({
856
1218
  "zh-CN": require_zh_CN()._config,
@@ -859,19 +1221,20 @@ var Config = import_koishi4.Schema.object({
859
1221
  async function apply(ctx, config) {
860
1222
  ctx.i18n.define("zh-CN", require_zh_CN());
861
1223
  ctx.i18n.define("en-US", require_en_US());
862
- const dataDir = path4.join(ctx.baseDir, "data");
863
- const caveDir = path4.join(dataDir, "cave");
1224
+ const dataDir = path5.join(ctx.baseDir, "data");
1225
+ const caveDir = path5.join(dataDir, "cave");
864
1226
  await FileHandler.ensureDirectory(dataDir);
865
1227
  await FileHandler.ensureDirectory(caveDir);
866
- await FileHandler.ensureDirectory(path4.join(caveDir, "resources"));
867
- await FileHandler.ensureJsonFile(path4.join(caveDir, "cave.json"));
868
- await FileHandler.ensureJsonFile(path4.join(caveDir, "pending.json"));
869
- 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"));
870
1232
  const idManager = new IdManager(ctx.baseDir);
871
- const hashStorage = new HashStorage(caveDir);
1233
+ const contentHashManager = new ContentHashManager(caveDir);
1234
+ const auditManager = new AuditManager(ctx, config, caveDir, idManager);
872
1235
  await Promise.all([
873
- idManager.initialize(path4.join(caveDir, "cave.json"), path4.join(caveDir, "pending.json")),
874
- hashStorage.initialize()
1236
+ idManager.initialize(path5.join(caveDir, "cave.json"), path5.join(caveDir, "pending.json")),
1237
+ contentHashManager.initialize()
875
1238
  ]);
876
1239
  const lastUsed = /* @__PURE__ */ new Map();
877
1240
  async function processList(session, config2, userId, pageNum = 1) {
@@ -900,13 +1263,13 @@ async function apply(ctx, config) {
900
1263
  const pendingData = await FileHandler.readJsonData(pendingFilePath);
901
1264
  const isApprove = Boolean(options.p);
902
1265
  if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
903
- return await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
1266
+ return await auditManager.processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
904
1267
  }
905
1268
  const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
906
1269
  if (isNaN(id)) {
907
1270
  return sendMessage(session, "commands.cave.error.invalidId", [], true);
908
1271
  }
909
- 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);
910
1273
  }
911
1274
  __name(processAudit, "processAudit");
912
1275
  async function processView(caveFilePath, resourceDir, session, options, content) {
@@ -949,14 +1312,15 @@ async function apply(ctx, config) {
949
1312
  }
950
1313
  const caveContent = await buildMessage(targetCave, resourceDir, session);
951
1314
  if (targetCave.elements) {
952
- const hashStorage2 = new HashStorage(caveDir);
953
- await hashStorage2.initialize();
954
- await hashStorage2.clearHashes(caveId);
1315
+ await contentHashManager.updateCaveContent(caveId, {
1316
+ images: void 0,
1317
+ texts: void 0
1318
+ });
955
1319
  for (const element of targetCave.elements) {
956
1320
  if ((element.type === "img" || element.type === "video") && element.file) {
957
- const fullPath = path4.join(resourceDir, element.file);
958
- if (fs4.existsSync(fullPath)) {
959
- await fs4.promises.unlink(fullPath);
1321
+ const fullPath = path5.join(resourceDir, element.file);
1322
+ if (fs5.existsSync(fullPath)) {
1323
+ await fs5.promises.unlink(fullPath);
960
1324
  }
961
1325
  }
962
1326
  }
@@ -976,36 +1340,24 @@ async function apply(ctx, config) {
976
1340
  }
977
1341
  __name(processDelete, "processDelete");
978
1342
  async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
1343
+ let caveId;
979
1344
  try {
980
1345
  const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
981
- await sendMessage(session, "commands.cave.add.noContent", [], true);
1346
+ await sendMessage(session, "commands.cave.add.noContent", [], true, 6e4);
982
1347
  const reply = await session.prompt({ timeout: 6e4 });
983
- if (!reply) session.text("commands.cave.add.operationTimeout");
1348
+ if (!reply) throw new Error(session.text("commands.cave.add.operationTimeout"));
984
1349
  return reply;
985
1350
  })();
986
- const caveId = await idManager.getNextId();
1351
+ caveId = await idManager.getNextId();
987
1352
  if (inputContent.includes("/app/.config/QQ/")) {
988
1353
  return sendMessage(session, "commands.cave.add.localFileNotAllowed", [], true);
989
1354
  }
990
1355
  const bypassAudit = config2.whitelist.includes(session.userId) || config2.whitelist.includes(session.guildId) || config2.whitelist.includes(session.channelId);
991
1356
  const { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(inputContent, config2, session);
992
- const pureText = textParts.filter((tp) => tp.type === "text").map((tp) => tp.content.trim()).join("\n").trim();
993
- if (config2.enableMD5 && pureText) {
994
- const textHash = HashStorage.hashText(pureText);
995
- const textDuplicates = await hashStorage.findDuplicates("text", [textHash]);
996
- if (textDuplicates[0]) {
997
- const data2 = await FileHandler.readJsonData(caveFilePath);
998
- const duplicateCave = data2.find((item) => item.cave_id === textDuplicates[0].caveId);
999
- if (duplicateCave) {
1000
- const message = session.text("commands.cave.error.exactDuplicateFound");
1001
- await session.send(message + await buildMessage(duplicateCave, resourceDir, session));
1002
- throw new Error("duplicate_found");
1003
- }
1004
- }
1005
- }
1006
1357
  if (videoUrls.length > 0 && !config2.allowVideo) {
1007
1358
  return sendMessage(session, "commands.cave.add.videoDisabled", [], true);
1008
1359
  }
1360
+ const imageBuffers = [];
1009
1361
  const [savedImages, savedVideos] = await Promise.all([
1010
1362
  imageUrls.length > 0 ? saveMedia(
1011
1363
  imageUrls,
@@ -1015,7 +1367,9 @@ async function apply(ctx, config) {
1015
1367
  "img",
1016
1368
  config2,
1017
1369
  ctx2,
1018
- session
1370
+ session,
1371
+ imageBuffers
1372
+ // 添加参数用于收集buffer
1019
1373
  ) : [],
1020
1374
  videoUrls.length > 0 ? saveMedia(
1021
1375
  videoUrls,
@@ -1038,7 +1392,7 @@ async function apply(ctx, config) {
1038
1392
  // 保持原始文本和图片的相对位置
1039
1393
  index: el.index
1040
1394
  }))
1041
- ].sort((a) => a.index - a.index),
1395
+ ].sort((a, b) => a.index - a.index),
1042
1396
  contributor_number: session.userId,
1043
1397
  contributor_name: session.username
1044
1398
  };
@@ -1049,19 +1403,24 @@ async function apply(ctx, config) {
1049
1403
  index: Number.MAX_SAFE_INTEGER
1050
1404
  });
1051
1405
  }
1052
- const existingData = await FileHandler.readJsonData(caveFilePath);
1053
- const hasImages = existingData.some(
1054
- (cave) => cave.elements?.some((element) => element.type === "img" && element.file)
1055
- );
1056
- if (hasImages) {
1057
- 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
+ }
1058
1417
  }
1059
1418
  if (config2.enableAudit && !bypassAudit) {
1060
1419
  const pendingData = await FileHandler.readJsonData(pendingFilePath);
1061
1420
  pendingData.push(newCave);
1062
1421
  await Promise.all([
1063
1422
  FileHandler.writeJsonData(pendingFilePath, pendingData),
1064
- sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir, session), session)
1423
+ auditManager.sendAuditMessage(newCave, await buildMessage(newCave, resourceDir, session), session)
1065
1424
  ]);
1066
1425
  return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
1067
1426
  }
@@ -1070,151 +1429,69 @@ async function apply(ctx, config) {
1070
1429
  ...newCave,
1071
1430
  elements: cleanElementsForSave(newCave.elements, false)
1072
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
+ }
1073
1463
  await Promise.all([
1074
1464
  FileHandler.writeJsonData(caveFilePath, data),
1075
- pureText && config2.enableMD5 ? hashStorage.updateHash(caveId, "text", pureText) : Promise.resolve(),
1076
- 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
+ })
1077
1469
  ]);
1078
1470
  await idManager.addStat(session.userId, caveId);
1079
- if (config2.enableMD5 && pureText) {
1080
- const textHash = HashStorage.hashText(pureText);
1081
- await hashStorage.updateHash(caveId, "text", textHash);
1082
- }
1083
1471
  return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
1084
1472
  } catch (error) {
1085
- logger4.error(`Failed to process add command: ${error.message}`);
1086
- }
1087
- }
1088
- __name(processAdd, "processAdd");
1089
- async function handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
1090
- if (pendingData.length === 0) {
1091
- return sendMessage(session, "commands.cave.audit.noPending", [], true);
1092
- }
1093
- if (typeof targetId === "number") {
1094
- const targetCave = pendingData.find((item) => item.cave_id === targetId);
1095
- if (!targetCave) {
1096
- return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
1473
+ if (error.message !== "duplicate_found") {
1474
+ await idManager.markDeleted(caveId);
1097
1475
  }
1098
- const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
1099
- if (isApprove) {
1100
- const oldCaveData = await FileHandler.readJsonData(caveFilePath);
1101
- const newCaveData = [...oldCaveData, {
1102
- ...targetCave,
1103
- cave_id: targetId,
1104
- elements: cleanElementsForSave(targetCave.elements, false)
1105
- }];
1106
- await FileHandler.withTransaction([
1107
- {
1108
- filePath: caveFilePath,
1109
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
1110
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
1111
- },
1112
- {
1113
- filePath: pendingFilePath,
1114
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
1115
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1116
- }
1117
- ]);
1118
- await idManager.addStat(targetCave.contributor_number, targetId);
1119
- } else {
1120
- await FileHandler.writeJsonData(pendingFilePath, newPendingData);
1121
- await idManager.markDeleted(targetId);
1122
- if (targetCave.elements) {
1123
- for (const element of targetCave.elements) {
1124
- if ((element.type === "img" || element.type === "video") && element.file) {
1125
- const fullPath = path4.join(resourceDir, element.file);
1126
- if (fs4.existsSync(fullPath)) {
1127
- await fs4.promises.unlink(fullPath);
1128
- }
1129
- }
1130
- }
1131
- }
1132
- }
1133
- const remainingCount = newPendingData.length;
1134
- if (remainingCount > 0) {
1135
- const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
1136
- const action = isApprove ? "auditPassed" : "auditRejected";
1137
- return sendMessage(session, "commands.cave.audit.pendingResult", [
1138
- session.text(`commands.cave.audit.${action}`),
1139
- remainingCount,
1140
- remainingIds
1141
- ], false);
1142
- }
1143
- return sendMessage(
1144
- session,
1145
- isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
1146
- [],
1147
- false
1148
- );
1149
- }
1150
- const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
1151
- let processedCount = 0;
1152
- if (isApprove && data) {
1153
- const oldData = [...data];
1154
- const newData = [...data];
1155
- await FileHandler.withTransaction([
1156
- {
1157
- filePath: caveFilePath,
1158
- operation: /* @__PURE__ */ __name(async () => {
1159
- for (const cave of pendingData) {
1160
- newData.push({
1161
- ...cave,
1162
- cave_id: cave.cave_id,
1163
- elements: cleanElementsForSave(cave.elements, false)
1164
- });
1165
- processedCount++;
1166
- await idManager.addStat(cave.contributor_number, cave.cave_id);
1167
- }
1168
- return FileHandler.writeJsonData(caveFilePath, newData);
1169
- }, "operation"),
1170
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
1171
- },
1172
- {
1173
- filePath: pendingFilePath,
1174
- operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
1175
- rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1176
- }
1177
- ]);
1178
- } else {
1179
- for (const cave of pendingData) {
1180
- await idManager.markDeleted(cave.cave_id);
1181
- }
1182
- await FileHandler.writeJsonData(pendingFilePath, []);
1183
- for (const cave of pendingData) {
1184
- if (cave.elements) {
1185
- for (const element of cave.elements) {
1186
- if ((element.type === "img" || element.type === "video") && element.file) {
1187
- const fullPath = path4.join(resourceDir, element.file);
1188
- if (fs4.existsSync(fullPath)) {
1189
- await fs4.promises.unlink(fullPath);
1190
- }
1191
- }
1192
- }
1193
- }
1194
- processedCount++;
1476
+ if (error.message === "duplicate_found") {
1477
+ return "";
1195
1478
  }
1479
+ logger4.error(`Failed to process add command: ${error.message}`);
1480
+ return sendMessage(session, "commands.cave.error.addFailed", [], true);
1196
1481
  }
1197
- return sendMessage(session, "commands.cave.audit.batchAuditResult", [
1198
- isApprove ? "通过" : "拒绝",
1199
- processedCount,
1200
- pendingData.length
1201
- ], false);
1202
1482
  }
1203
- __name(handleAudit, "handleAudit");
1204
- 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 }) => {
1483
+ __name(processAdd, "processAdd");
1484
+ const caveCommand = ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
1205
1485
  if (config.blacklist.includes(session.userId)) {
1206
1486
  return sendMessage(session, "commands.cave.message.blacklisted", [], true);
1207
1487
  }
1208
- if ((options.p || options.d) && !config.manager.includes(session.userId)) {
1209
- return sendMessage(session, "commands.cave.message.managerOnly", [], true);
1210
- }
1211
1488
  }).action(async ({ session, options }, ...content) => {
1212
- const dataDir2 = path4.join(ctx.baseDir, "data");
1213
- const caveDir2 = path4.join(dataDir2, "cave");
1214
- const caveFilePath = path4.join(caveDir2, "cave.json");
1215
- const resourceDir = path4.join(caveDir2, "resources");
1216
- const pendingFilePath = path4.join(caveDir2, "pending.json");
1217
- const needsCooldown = !options.l && !options.a && !options.p && !options.d;
1489
+ const dataDir2 = path5.join(ctx.baseDir, "data");
1490
+ const caveDir2 = path5.join(dataDir2, "cave");
1491
+ const caveFilePath = path5.join(caveDir2, "cave.json");
1492
+ const resourceDir = path5.join(caveDir2, "resources");
1493
+ const pendingFilePath = path5.join(caveDir2, "pending.json");
1494
+ const needsCooldown = !options.l && !options.a;
1218
1495
  if (needsCooldown) {
1219
1496
  const guildId = session.guildId;
1220
1497
  const now = Date.now();
@@ -1244,9 +1521,6 @@ async function apply(ctx, config) {
1244
1521
  return await processList(session, config, session.userId);
1245
1522
  }
1246
1523
  }
1247
- if (options.p || options.d) {
1248
- return await processAudit(pendingFilePath, caveFilePath, resourceDir, session, options, content);
1249
- }
1250
1524
  if (options.g) {
1251
1525
  return await processView(caveFilePath, resourceDir, session, options, content);
1252
1526
  }
@@ -1258,9 +1532,47 @@ async function apply(ctx, config) {
1258
1532
  }
1259
1533
  return await processRandom(caveFilePath, resourceDir, session);
1260
1534
  });
1535
+ caveCommand.subcommand(".pass <id:text>", "通过回声洞审核").before(async ({ session }) => {
1536
+ if (!config.manager.includes(session.userId)) {
1537
+ return sendMessage(session, "commands.cave.message.managerOnly", [], true);
1538
+ }
1539
+ }).action(async ({ session }, id) => {
1540
+ const dataDir2 = path5.join(ctx.baseDir, "data");
1541
+ const caveDir2 = path5.join(dataDir2, "cave");
1542
+ const caveFilePath = path5.join(caveDir2, "cave.json");
1543
+ const resourceDir = path5.join(caveDir2, "resources");
1544
+ const pendingFilePath = path5.join(caveDir2, "pending.json");
1545
+ return await processAudit(
1546
+ pendingFilePath,
1547
+ caveFilePath,
1548
+ resourceDir,
1549
+ session,
1550
+ { p: true },
1551
+ [id]
1552
+ );
1553
+ });
1554
+ caveCommand.subcommand(".reject <id:text>", "拒绝回声洞审核").before(async ({ session }) => {
1555
+ if (!config.manager.includes(session.userId)) {
1556
+ return sendMessage(session, "commands.cave.message.managerOnly", [], true);
1557
+ }
1558
+ }).action(async ({ session }, id) => {
1559
+ const dataDir2 = path5.join(ctx.baseDir, "data");
1560
+ const caveDir2 = path5.join(dataDir2, "cave");
1561
+ const caveFilePath = path5.join(caveDir2, "cave.json");
1562
+ const resourceDir = path5.join(caveDir2, "resources");
1563
+ const pendingFilePath = path5.join(caveDir2, "pending.json");
1564
+ return await processAudit(
1565
+ pendingFilePath,
1566
+ caveFilePath,
1567
+ resourceDir,
1568
+ session,
1569
+ { d: true },
1570
+ [id]
1571
+ );
1572
+ });
1261
1573
  }
1262
1574
  __name(apply, "apply");
1263
- var logger4 = new import_koishi4.Logger("cave");
1575
+ var logger4 = new import_koishi5.Logger("cave");
1264
1576
  async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
1265
1577
  try {
1266
1578
  const msg = await session.send(session.text(key, params));
@@ -1279,22 +1591,6 @@ async function sendMessage(session, key, params = [], isTemp = true, timeout = 1
1279
1591
  return "";
1280
1592
  }
1281
1593
  __name(sendMessage, "sendMessage");
1282
- async function sendAuditMessage(ctx, config, cave, content, session) {
1283
- const auditMessage = `${session.text("commands.cave.audit.title")}
1284
- ${content}
1285
- ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
1286
- for (const managerId of config.manager) {
1287
- const bot = ctx.bots[0];
1288
- if (bot) {
1289
- try {
1290
- await bot.sendPrivateMessage(managerId, auditMessage);
1291
- } catch (error) {
1292
- logger4.error(session.text("commands.cave.audit.sendFailed", [managerId]));
1293
- }
1294
- }
1295
- }
1296
- }
1297
- __name(sendAuditMessage, "sendAuditMessage");
1298
1594
  function cleanElementsForSave(elements, keepIndex = false) {
1299
1595
  if (!elements?.length) return [];
1300
1596
  const cleanedElements = elements.map((element) => {
@@ -1320,7 +1616,7 @@ function cleanElementsForSave(elements, keepIndex = false) {
1320
1616
  }
1321
1617
  __name(cleanElementsForSave, "cleanElementsForSave");
1322
1618
  async function processMediaFile(filePath, type) {
1323
- const data = await fs4.promises.readFile(filePath).catch(() => null);
1619
+ const data = await fs5.promises.readFile(filePath).catch(() => null);
1324
1620
  if (!data) return null;
1325
1621
  return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
1326
1622
  }
@@ -1337,10 +1633,10 @@ async function buildMessage(cave, resourceDir, session) {
1337
1633
  session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
1338
1634
  ].join("\n");
1339
1635
  await session?.send(basicInfo);
1340
- const filePath = path4.join(resourceDir, videoElement.file);
1636
+ const filePath = path5.join(resourceDir, videoElement.file);
1341
1637
  const base64Data = await processMediaFile(filePath, "video");
1342
1638
  if (base64Data && session) {
1343
- await session.send((0, import_koishi4.h)("video", { src: base64Data }));
1639
+ await session.send((0, import_koishi5.h)("video", { src: base64Data }));
1344
1640
  }
1345
1641
  return "";
1346
1642
  }
@@ -1349,10 +1645,10 @@ async function buildMessage(cave, resourceDir, session) {
1349
1645
  if (element.type === "text") {
1350
1646
  lines.push(element.content);
1351
1647
  } else if (element.type === "img" && element.file) {
1352
- const filePath = path4.join(resourceDir, element.file);
1648
+ const filePath = path5.join(resourceDir, element.file);
1353
1649
  const base64Data = await processMediaFile(filePath, "image");
1354
1650
  if (base64Data) {
1355
- lines.push((0, import_koishi4.h)("image", { src: base64Data }));
1651
+ lines.push((0, import_koishi5.h)("image", { src: base64Data }));
1356
1652
  }
1357
1653
  }
1358
1654
  }
@@ -1401,13 +1697,13 @@ async function extractMediaContent(originalContent, config, session) {
1401
1697
  return { imageUrls, imageElements, videoUrls, videoElements, textParts };
1402
1698
  }
1403
1699
  __name(extractMediaContent, "extractMediaContent");
1404
- async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session) {
1700
+ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session, buffers) {
1405
1701
  const accept = mediaType === "img" ? "image/*" : "video/*";
1406
- const hashStorage = new HashStorage(path4.join(ctx.baseDir, "data", "cave"));
1702
+ const hashStorage = new ContentHashManager(path5.join(ctx.baseDir, "data", "cave"));
1407
1703
  await hashStorage.initialize();
1408
1704
  const downloadTasks = urls.map(async (url, i) => {
1409
1705
  const fileName = fileNames[i];
1410
- const ext = path4.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
1706
+ const ext = path5.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
1411
1707
  try {
1412
1708
  const response = await ctx.http(decodeURIComponent(url).replace(/&amp;/g, "&"), {
1413
1709
  method: "GET",
@@ -1421,75 +1717,58 @@ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config
1421
1717
  });
1422
1718
  if (!response.data) throw new Error("empty_response");
1423
1719
  const buffer = Buffer.from(response.data);
1424
- if (mediaType === "img") {
1425
- const baseName = path4.basename(fileName || "md5", ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1426
- if (config.enableMD5) {
1427
- const files = await fs4.promises.readdir(resourceDir);
1428
- const duplicateFile = files.find((file) => file.startsWith(baseName + "_"));
1429
- if (duplicateFile) {
1430
- const duplicateCaveId = parseInt(duplicateFile.split("_")[1]);
1431
- if (!isNaN(duplicateCaveId)) {
1432
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1433
- const data = await FileHandler.readJsonData(caveFilePath);
1434
- const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1435
- if (originalCave) {
1436
- const message = session.text("commands.cave.error.exactDuplicateFound");
1437
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1438
- throw new Error("duplicate_found");
1439
- }
1440
- }
1720
+ if (buffers && mediaType === "img") {
1721
+ buffers.push(buffer);
1722
+ }
1723
+ const md5 = path5.basename(fileName || `${mediaType}`, ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1724
+ const files = await fs5.promises.readdir(resourceDir);
1725
+ const duplicateFile = files.find((file) => {
1726
+ const match = file.match(/^\d+_([^.]+)/);
1727
+ return match && match[1] === md5;
1728
+ });
1729
+ if (duplicateFile) {
1730
+ const duplicateCaveId = parseInt(duplicateFile.split("_")[0]);
1731
+ if (!isNaN(duplicateCaveId)) {
1732
+ const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
1733
+ const data = await FileHandler.readJsonData(caveFilePath);
1734
+ const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1735
+ if (originalCave) {
1736
+ const message = session.text("commands.cave.error.exactDuplicateFound");
1737
+ await session.send(message + await buildMessage(originalCave, resourceDir, session));
1738
+ throw new Error("duplicate_found");
1441
1739
  }
1442
1740
  }
1443
- if (config.enableDuplicate) {
1444
- const hashStorage2 = new HashStorage(path4.join(ctx.baseDir, "data", "cave"));
1445
- await hashStorage2.initialize();
1446
- const result = await hashStorage2.findDuplicates("image", [buffer.toString("base64")], config.duplicateThreshold);
1447
- if (result.length > 0 && result[0] !== null) {
1448
- const duplicate = result[0];
1449
- const similarity = duplicate.similarity;
1450
- if (similarity >= config.duplicateThreshold) {
1451
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1452
- const data = await FileHandler.readJsonData(caveFilePath);
1453
- const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
1454
- if (originalCave) {
1455
- const message = session.text(
1456
- "commands.cave.error.similarDuplicateFound",
1457
- [(similarity * 100).toFixed(1)]
1458
- );
1459
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1460
- throw new Error("duplicate_found");
1461
- }
1462
- }
1741
+ }
1742
+ if (mediaType === "img" && config.enableImageDuplicate) {
1743
+ const result = await hashStorage.findDuplicates(
1744
+ { images: [buffer] },
1745
+ {
1746
+ image: config.imageDuplicateThreshold,
1747
+ text: config.textDuplicateThreshold
1463
1748
  }
1464
- }
1465
- const finalFileName = `${caveId}_${baseName}${ext}`;
1466
- const filePath = path4.join(resourceDir, finalFileName);
1467
- await FileHandler.saveMediaFile(filePath, buffer);
1468
- return finalFileName;
1469
- } else {
1470
- const baseName = path4.basename(fileName || "video", ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1471
- if (config.enableMD5) {
1472
- const files = await fs4.promises.readdir(resourceDir);
1473
- const duplicateFile = files.find((file) => file.startsWith(baseName + "_"));
1474
- if (duplicateFile) {
1475
- const duplicateCaveId = parseInt(duplicateFile.split("_")[1]);
1476
- if (!isNaN(duplicateCaveId)) {
1477
- const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1478
- const data = await FileHandler.readJsonData(caveFilePath);
1479
- const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
1480
- if (originalCave) {
1481
- const message = session.text("commands.cave.error.exactDuplicateFound");
1482
- await session.send(message + await buildMessage(originalCave, resourceDir, session));
1483
- throw new Error("duplicate_found");
1484
- }
1749
+ );
1750
+ if (result.length > 0 && result[0] !== null) {
1751
+ const duplicate = result[0];
1752
+ const similarity = duplicate.similarity;
1753
+ if (similarity >= config.imageDuplicateThreshold) {
1754
+ const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
1755
+ const data = await FileHandler.readJsonData(caveFilePath);
1756
+ const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
1757
+ if (originalCave) {
1758
+ const message = session.text(
1759
+ "commands.cave.error.similarDuplicateFound",
1760
+ [(similarity * 100).toFixed(1)]
1761
+ );
1762
+ await session.send(message + await buildMessage(originalCave, resourceDir, session));
1763
+ throw new Error("duplicate_found");
1485
1764
  }
1486
1765
  }
1487
1766
  }
1488
- const finalFileName = `${caveId}_${baseName}${ext}`;
1489
- const filePath = path4.join(resourceDir, finalFileName);
1490
- await FileHandler.saveMediaFile(filePath, buffer);
1491
- return finalFileName;
1492
1767
  }
1768
+ const finalFileName = `${caveId}_${md5}${ext}`;
1769
+ const filePath = path5.join(resourceDir, finalFileName);
1770
+ await FileHandler.saveMediaFile(filePath, buffer);
1771
+ return finalFileName;
1493
1772
  } catch (error) {
1494
1773
  if (error.message === "duplicate_found") {
1495
1774
  throw error;