koishi-plugin-best-cave 2.3.15 → 2.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Utils.d.ts CHANGED
@@ -9,9 +9,11 @@ import { PendManager } from './PendManager';
9
9
  * @param config 插件配置。
10
10
  * @param fileManager 文件管理器实例。
11
11
  * @param logger 日志记录器实例。
12
- * @returns 包含 h() 元素和字符串的消息数组。
12
+ * @param platform 目标平台名称 (e.g., 'onebot')
13
+ * @param prefix 可选的消息前缀 (e.g., '已删除', '待审核')。
14
+ * @returns 包含多条消息的数组,每条消息是一个 (string | h)[] 数组。
13
15
  */
14
- export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger): Promise<(string | h)[]>;
16
+ export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger, platform?: string, prefix?: string): Promise<(string | h)[][]>;
15
17
  /**
16
18
  * @description 清理数据库中标记为 'delete' 状态的回声洞及其关联文件和哈希。
17
19
  * @param ctx Koishi 上下文。
@@ -61,6 +63,20 @@ export declare function processMessageElements(sourceElements: h[], newId: numbe
61
63
  fileName: string;
62
64
  }[];
63
65
  }>;
66
+ /**
67
+ * @description 异步处理文件上传、查重和状态更新的后台任务。
68
+ * @param ctx - Koishi 上下文。
69
+ * @param config - 插件配置。
70
+ * @param fileManager - FileManager 实例,用于保存文件。
71
+ * @param logger - 日志记录器实例。
72
+ * @param reviewManager - ReviewManager 实例,用于提交审核。
73
+ * @param cave - 刚刚在数据库中创建的 `preload` 状态的回声洞对象。
74
+ * @param mediaToSave - 需要下载和处理的媒体文件列表。
75
+ * @param reusableIds - 可复用 ID 的内存缓存。
76
+ * @param session - 触发此操作的用户会话,用于发送反馈。
77
+ * @param hashManager - HashManager 实例,如果启用则用于哈希计算和比较。
78
+ * @param textHashesToStore - 已预先计算好的、待存入数据库的文本哈希对象数组。
79
+ */
64
80
  export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reviewManager: PendManager, cave: CaveObject, mediaToToSave: {
65
81
  sourceUrl: string;
66
82
  fileName: string;
package/lib/index.js CHANGED
@@ -80,9 +80,7 @@ var FileManager = class {
80
80
  * @returns 异步操作的结果。
81
81
  */
82
82
  async withLock(fullPath, operation) {
83
- while (this.locks.has(fullPath)) {
84
- await this.locks.get(fullPath);
85
- }
83
+ while (this.locks.has(fullPath)) await this.locks.get(fullPath);
86
84
  const promise = operation().finally(() => {
87
85
  this.locks.delete(fullPath);
88
86
  });
@@ -232,9 +230,7 @@ var DataManager = class {
232
230
  */
233
231
  registerCommands(cave) {
234
232
  const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
235
- if (session.channelId !== this.config.adminChannel?.split(":")[1]) {
236
- return "此指令仅限在管理群组中使用";
237
- }
233
+ if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
238
234
  try {
239
235
  await session.send("正在处理,请稍候...");
240
236
  return await action();
@@ -267,9 +263,7 @@ var DataManager = class {
267
263
  try {
268
264
  const fileContent = await this.fileManager.readFile(fileName);
269
265
  importedCaves = JSON.parse(fileContent.toString("utf-8"));
270
- if (!Array.isArray(importedCaves) || !importedCaves.length) {
271
- throw new Error("导入文件格式无效或为空");
272
- }
266
+ if (!Array.isArray(importedCaves) || !importedCaves.length) throw new Error("导入文件格式无效或为空");
273
267
  } catch (error) {
274
268
  throw new Error(`读取导入文件失败: ${error.message}`);
275
269
  }
@@ -292,7 +286,7 @@ var import_koishi2 = require("koishi");
292
286
  var import_koishi = require("koishi");
293
287
  var path2 = __toESM(require("path"));
294
288
  var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
295
- async function buildCaveMessage(cave, config, fileManager, logger2) {
289
+ async function buildCaveMessage(cave, config, fileManager, logger2, platform, prefix) {
296
290
  async function transformToH(elements) {
297
291
  return Promise.all(elements.map(async (el) => {
298
292
  if (el.type === "text") return import_koishi.h.text(el.content);
@@ -315,12 +309,8 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
315
309
  if (["image", "video", "audio", "file"].includes(el.type)) {
316
310
  const fileName = el.file;
317
311
  if (!fileName) return (0, import_koishi.h)("p", {}, `[${el.type}]`);
318
- if (config.enableS3 && config.publicUrl) {
319
- return (0, import_koishi.h)(el.type, { ...el, src: new URL(fileName, config.publicUrl).href });
320
- }
321
- if (config.localPath) {
322
- return (0, import_koishi.h)(el.type, { ...el, src: `file://${path2.join(config.localPath, fileName)}` });
323
- }
312
+ if (config.enableS3 && config.publicUrl) return (0, import_koishi.h)(el.type, { ...el, src: new URL(fileName, config.publicUrl).href });
313
+ if (config.localPath) return (0, import_koishi.h)(el.type, { ...el, src: `file://${path2.join(config.localPath, fileName)}` });
324
314
  try {
325
315
  const data = await fileManager.readFile(fileName);
326
316
  const mimeType = mimeTypeMap[path2.extname(fileName).toLowerCase()] || "application/octet-stream";
@@ -336,12 +326,35 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
336
326
  __name(transformToH, "transformToH");
337
327
  const caveHElements = await transformToH(cave.elements);
338
328
  const replacements = { id: cave.id.toString(), name: cave.userName };
339
- const [header, footer] = config.caveFormat.split("|", 2).map((part) => part.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim());
340
- const finalMessage = [];
341
- if (header) finalMessage.push(header + "\n");
342
- finalMessage.push(...caveHElements);
343
- if (footer) finalMessage.push("\n" + footer);
344
- return finalMessage;
329
+ const [rawHeader, rawFooter] = config.caveFormat.split("|", 2);
330
+ let header = rawHeader ? rawHeader.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim() : "";
331
+ if (prefix) header = `${prefix}${header}`;
332
+ const footer = rawFooter ? rawFooter.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim() : "";
333
+ const problematicTypes = ["video", "audio", "file", "forward"];
334
+ const placeholderMap = { video: "[视频]", audio: "[音频]", file: "[文件]", forward: "[合并转发]" };
335
+ const containsProblematic = platform === "onebot" && caveHElements.some((el) => problematicTypes.includes(el.type));
336
+ if (!containsProblematic) {
337
+ const finalMessage = [];
338
+ if (header) finalMessage.push(header + "\n");
339
+ finalMessage.push(...caveHElements);
340
+ if (footer) finalMessage.push("\n" + footer);
341
+ return [finalMessage.length > 0 ? finalMessage : []];
342
+ }
343
+ const initialMessageContent = [];
344
+ const followUpMessages = [];
345
+ for (const el of caveHElements) {
346
+ if (problematicTypes.includes(el.type)) {
347
+ initialMessageContent.push(import_koishi.h.text(placeholderMap[el.type]));
348
+ followUpMessages.push([el]);
349
+ } else {
350
+ initialMessageContent.push(el);
351
+ }
352
+ }
353
+ const finalInitialMessage = [];
354
+ if (header) finalInitialMessage.push(header + "\n");
355
+ finalInitialMessage.push(...initialMessageContent);
356
+ if (footer) finalInitialMessage.push("\n" + footer);
357
+ return [finalInitialMessage, ...followUpMessages].filter((msg) => msg.length > 0);
345
358
  }
346
359
  __name(buildCaveMessage, "buildCaveMessage");
347
360
  async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
@@ -349,9 +362,7 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
349
362
  const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
350
363
  if (!cavesToDelete.length) return;
351
364
  const idsToDelete = cavesToDelete.map((c) => c.id);
352
- for (const cave of cavesToDelete) {
353
- await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
354
- }
365
+ for (const cave of cavesToDelete) await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
355
366
  reusableIds.delete(0);
356
367
  idsToDelete.forEach((id) => reusableIds.add(id));
357
368
  await ctx.database.remove("cave", { id: { $in: idsToDelete } });
@@ -384,30 +395,22 @@ async function getNextCaveId(ctx, query = {}, reusableIds) {
384
395
  const existingIds = new Set(allCaveIds);
385
396
  let newId = 1;
386
397
  while (existingIds.has(newId)) newId++;
387
- if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0)) {
388
- reusableIds.add(0);
389
- }
398
+ if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0)) reusableIds.add(0);
390
399
  return newId;
391
400
  }
392
401
  __name(getNextCaveId, "getNextCaveId");
393
402
  function checkCooldown(session, config, lastUsed) {
394
403
  const adminChannelId = config.adminChannel?.split(":")[1];
395
- if (adminChannelId && session.channelId === adminChannelId) {
396
- return null;
397
- }
404
+ if (adminChannelId && session.channelId === adminChannelId) return null;
398
405
  if (config.coolDown <= 0 || !session.channelId) return null;
399
406
  const lastTime = lastUsed.get(session.channelId) || 0;
400
407
  const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
401
- if (remainingTime > 0) {
402
- return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
403
- }
408
+ if (remainingTime > 0) return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
404
409
  return null;
405
410
  }
406
411
  __name(checkCooldown, "checkCooldown");
407
412
  function updateCooldownTimestamp(session, config, lastUsed) {
408
- if (config.coolDown > 0 && session.channelId) {
409
- lastUsed.set(session.channelId, Date.now());
410
- }
413
+ if (config.coolDown > 0 && session.channelId) lastUsed.set(session.channelId, Date.now());
411
414
  }
412
415
  __name(updateCooldownTimestamp, "updateCooldownTimestamp");
413
416
  async function processMessageElements(sourceElements, newId, session, config, logger2) {
@@ -453,13 +456,9 @@ async function processMessageElements(sourceElements, newId, session, config, lo
453
456
  return (0, import_koishi.h)(type2, attrs);
454
457
  });
455
458
  const contentElements = await transform(elementsToProcess);
456
- if (contentElements.length > 0) {
457
- forwardNodes.push({ userId, userName, elements: contentElements });
458
- }
459
- }
460
- if (forwardNodes.length > 0) {
461
- result.push({ type: "forward", content: forwardNodes });
459
+ if (contentElements.length > 0) forwardNodes.push({ userId, userName, elements: contentElements });
462
460
  }
461
+ if (forwardNodes.length > 0) result.push({ type: "forward", content: forwardNodes });
463
462
  } else if (["image", "video", "audio", "file"].includes(type) && el.attrs.src) {
464
463
  let fileIdentifier = el.attrs.src;
465
464
  if (fileIdentifier.startsWith("http")) {
@@ -511,9 +510,7 @@ async function handleFileUploads(ctx, config, fileManager, logger2, reviewManage
511
510
  await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
512
511
  if (hashManager) {
513
512
  const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: cave.id }));
514
- if (allHashesToInsert.length > 0) {
515
- await ctx.database.upsert("cave_hash", allHashesToInsert);
516
- }
513
+ if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
517
514
  }
518
515
  if (finalStatus === "pending" && reviewManager) {
519
516
  const [finalCave] = await ctx.database.get("cave", { id: cave.id });
@@ -552,9 +549,7 @@ var PendManager = class {
552
549
  */
553
550
  registerCommands(cave) {
554
551
  const requireAdmin = /* @__PURE__ */ __name((session) => {
555
- if (session.channelId !== this.config.adminChannel?.split(":")[1]) {
556
- return "此指令仅限在管理群组中使用";
557
- }
552
+ if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
558
553
  return null;
559
554
  }, "requireAdmin");
560
555
  const pend = cave.subcommand(".pend [id:posint]", "审核回声洞").action(async ({ session }, id) => {
@@ -564,7 +559,9 @@ var PendManager = class {
564
559
  const [targetCave] = await this.ctx.database.get("cave", { id });
565
560
  if (!targetCave) return `回声洞(${id})不存在`;
566
561
  if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
567
- return [`待审核`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
562
+ const caveMessages = await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger, session.platform, "待审核");
563
+ for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi2.h.normalize(message));
564
+ return;
568
565
  }
569
566
  const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
570
567
  if (!pendingCaves.length) return "当前没有需要审核的回声洞";
@@ -608,8 +605,9 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
608
605
  return;
609
606
  }
610
607
  try {
611
- const pendMessage = [`待审核`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
612
- await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(pendMessage));
608
+ const [platform] = this.config.adminChannel.split(":", 1);
609
+ const caveMessages = await buildCaveMessage(cave, this.config, this.fileManager, this.logger, platform, "待审核");
610
+ for (const message of caveMessages) if (message.length > 0) await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(message));
613
611
  } catch (error) {
614
612
  this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
615
613
  }
@@ -650,9 +648,7 @@ var HashManager = class {
650
648
  registerCommands(cave) {
651
649
  const adminCheck = /* @__PURE__ */ __name(({ session }) => {
652
650
  const adminChannelId = this.config.adminChannel?.split(":")[1];
653
- if (session.channelId !== adminChannelId) {
654
- return "此指令仅限在管理群组中使用";
655
- }
651
+ if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
656
652
  }, "adminCheck");
657
653
  cave.subcommand(".hash", "校验回声洞").usage("校验缺失哈希的回声洞,补全哈希记录。").action(async (argv) => {
658
654
  const checkResult = adminCheck(argv);
@@ -709,9 +705,7 @@ var HashManager = class {
709
705
  existingHashSet.add(uniqueKey);
710
706
  }
711
707
  }
712
- if (hashesToInsert.length >= 100) {
713
- await flushBatch();
714
- }
708
+ if (hashesToInsert.length >= 100) await flushBatch();
715
709
  } catch (error) {
716
710
  errorCount++;
717
711
  this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
@@ -794,17 +788,13 @@ var HashManager = class {
794
788
  const text2 = textHashes.get(id2);
795
789
  if (text1 && text2) {
796
790
  const similarity = this.calculateSimilarity(text1, text2);
797
- if (similarity >= textThreshold) {
798
- similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
799
- }
791
+ if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
800
792
  }
801
793
  const global1 = globalHashes.get(id1);
802
794
  const global2 = globalHashes.get(id2);
803
795
  if (global1 && global2) {
804
796
  const similarity = this.calculateSimilarity(global1, global2);
805
- if (similarity >= imageThreshold) {
806
- similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
807
- }
797
+ if (similarity >= imageThreshold) similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
808
798
  }
809
799
  }
810
800
  }
@@ -833,9 +823,7 @@ var HashManager = class {
833
823
  components.get(root).add(id);
834
824
  }
835
825
  const partialGroups = [];
836
- for (const component of components.values()) {
837
- if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
838
- }
826
+ for (const component of components.values()) if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
839
827
  const totalFindings = similarPairs.text.size + similarPairs.global.size + partialGroups.length;
840
828
  if (totalFindings === 0) return "未发现高相似度的内容";
841
829
  let report = `已发现 ${totalFindings} 组高相似度的内容:`;
@@ -884,9 +872,7 @@ var HashManager = class {
884
872
  const scale = Math.sqrt(2 / N);
885
873
  for (let k = 0; k < N; k++) {
886
874
  let sum = 0;
887
- for (let n = 0; n < N; n++) {
888
- sum += input[n] * cosines[n][k];
889
- }
875
+ for (let n = 0; n < N; n++) sum += input[n] * cosines[n][k];
890
876
  output[k] = scale * sum;
891
877
  }
892
878
  output[0] /= Math.sqrt(2);
@@ -909,16 +895,10 @@ var HashManager = class {
909
895
  if (!Number.isInteger(hashGridSize)) throw new Error("哈希位数必须是完全平方数");
910
896
  const pixels = await (0, import_sharp.default)(imageBuffer).grayscale().resize(dctSize, dctSize, { fit: "fill" }).raw().toBuffer();
911
897
  const matrix = [];
912
- for (let y = 0; y < dctSize; y++) {
913
- matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
914
- }
898
+ for (let y = 0; y < dctSize; y++) matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
915
899
  const dctMatrix = this._dct2D(matrix);
916
900
  const coefficients = [];
917
- for (let y = 0; y < hashGridSize; y++) {
918
- for (let x = 0; x < hashGridSize; x++) {
919
- coefficients.push(dctMatrix[y][x]);
920
- }
921
- }
901
+ for (let y = 0; y < hashGridSize; y++) for (let x = 0; x < hashGridSize; x++) coefficients.push(dctMatrix[y][x]);
922
902
  const median = [...coefficients.slice(1)].sort((a, b) => a - b)[Math.floor((coefficients.length - 1) / 2)];
923
903
  const binaryHash = coefficients.map((val) => val > median ? "1" : "0").join("");
924
904
  return BigInt("0b" + binaryHash).toString(16).padStart(size / 4, "0");
@@ -934,9 +914,7 @@ var HashManager = class {
934
914
  const bin1 = hexToBinary(hex1);
935
915
  const bin2 = hexToBinary(hex2);
936
916
  const len = Math.min(bin1.length, bin2.length);
937
- for (let i = 0; i < len; i++) {
938
- if (bin1[i] !== bin2[i]) distance++;
939
- }
917
+ for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
940
918
  return distance;
941
919
  }
942
920
  /**
@@ -963,18 +941,14 @@ var HashManager = class {
963
941
  if (cleanText.length < n) {
964
942
  tokens.add(cleanText);
965
943
  } else {
966
- for (let i = 0; i <= cleanText.length - n; i++) {
967
- tokens.add(cleanText.substring(i, i + n));
968
- }
944
+ for (let i = 0; i <= cleanText.length - n; i++) tokens.add(cleanText.substring(i, i + n));
969
945
  }
970
946
  const tokenArray = Array.from(tokens);
971
947
  if (tokenArray.length === 0) return "";
972
948
  const vector = new Array(64).fill(0);
973
949
  tokenArray.forEach((token) => {
974
950
  const hash = crypto.createHash("md5").update(token).digest();
975
- for (let i = 0; i < 64; i++) {
976
- vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
977
- }
951
+ for (let i = 0; i < 64; i++) vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
978
952
  });
979
953
  const binaryHash = vector.map((v) => v > 0 ? "1" : "0").join("");
980
954
  return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
@@ -982,9 +956,7 @@ var HashManager = class {
982
956
  };
983
957
  function hexToBinary(hex) {
984
958
  let bin = "";
985
- for (const char of hex) {
986
- bin += parseInt(char, 16).toString(2).padStart(4, "0");
987
- }
959
+ for (const char of hex) bin += parseInt(char, 16).toString(2).padStart(4, "0");
988
960
  return bin;
989
961
  }
990
962
  __name(hexToBinary, "hexToBinary");
@@ -1061,13 +1033,12 @@ function apply(ctx, config) {
1061
1033
  try {
1062
1034
  const query = getScopeQuery(session, config);
1063
1035
  const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
1064
- if (!candidates.length) {
1065
- return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
1066
- }
1036
+ if (!candidates.length) return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
1067
1037
  const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
1068
1038
  const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
1069
1039
  updateCooldownTimestamp(session, config, lastUsed);
1070
- return buildCaveMessage(randomCave, config, fileManager, logger);
1040
+ const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
1041
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1071
1042
  } catch (error) {
1072
1043
  logger.error("随机获取回声洞失败:", error);
1073
1044
  return "随机获取回声洞失败";
@@ -1094,10 +1065,8 @@ ${JSON.stringify(session, null, 2)}`);
1094
1065
  }
1095
1066
  const newId = await getNextCaveId(ctx, getScopeQuery(session, config, false), reusableIds);
1096
1067
  const { finalElementsForDb, mediaToSave } = await processMessageElements(sourceElements, newId, session, config, logger);
1097
- if (config.debug) {
1098
- logger.info(`数据库元素:
1068
+ if (config.debug) logger.info(`数据库元素:
1099
1069
  ${JSON.stringify(finalElementsForDb, null, 2)}`);
1100
- }
1101
1070
  if (finalElementsForDb.length === 0) return "无可添加内容";
1102
1071
  const textHashesToStore = [];
1103
1072
  if (hashManager) {
@@ -1108,9 +1077,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
1108
1077
  const existingTextHashes = await ctx.database.get("cave_hash", { type: "simhash" });
1109
1078
  for (const existing of existingTextHashes) {
1110
1079
  const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
1111
- if (similarity >= config.textThreshold) {
1112
- return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
1113
- }
1080
+ if (similarity >= config.textThreshold) return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
1114
1081
  }
1115
1082
  textHashesToStore.push({ hash: newSimhash, type: "simhash" });
1116
1083
  }
@@ -1131,12 +1098,8 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
1131
1098
  if (hasMedia) {
1132
1099
  handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave, reusableIds, session, hashManager, textHashesToStore);
1133
1100
  } else {
1134
- if (hashManager && textHashesToStore.length > 0) {
1135
- await ctx.database.upsert("cave_hash", textHashesToStore.map((h4) => ({ ...h4, cave: newCave.id })));
1136
- }
1137
- if (initialStatus === "pending") {
1138
- reviewManager.sendForPend(newCave);
1139
- }
1101
+ if (hashManager && textHashesToStore.length > 0) await ctx.database.upsert("cave_hash", textHashesToStore.map((h4) => ({ ...h4, cave: newCave.id })));
1102
+ if (initialStatus === "pending") reviewManager.sendForPend(newCave);
1140
1103
  }
1141
1104
  return initialStatus === "pending" || initialStatus === "preload" && config.enablePend ? `提交成功,序号为(${newCave.id})` : `添加成功,序号为(${newCave.id})`;
1142
1105
  } catch (error) {
@@ -1152,7 +1115,8 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
1152
1115
  const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
1153
1116
  if (!targetCave) return `回声洞(${id})不存在`;
1154
1117
  updateCooldownTimestamp(session, config, lastUsed);
1155
- return buildCaveMessage(targetCave, config, fileManager, logger);
1118
+ const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
1119
+ for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1156
1120
  } catch (error) {
1157
1121
  logger.error(`查看回声洞(${id})失败:`, error);
1158
1122
  return "查看失败,请稍后再试";
@@ -1167,9 +1131,9 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
1167
1131
  const isAdmin = session.channelId === config.adminChannel?.split(":")[1];
1168
1132
  if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
1169
1133
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
1170
- const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
1134
+ const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
1171
1135
  cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
1172
- return [`已删除`, ...caveMessage];
1136
+ for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1173
1137
  } catch (error) {
1174
1138
  logger.error(`标记回声洞(${id})失败:`, error);
1175
1139
  return "删除失败,请稍后再试";
@@ -1178,9 +1142,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
1178
1142
  cave.subcommand(".list", "查询投稿统计").option("user", "-u <user:user> 指定用户").option("all", "-a 查看排行").action(async ({ session, options }) => {
1179
1143
  if (options.all) {
1180
1144
  const adminChannelId = config.adminChannel?.split(":")[1];
1181
- if (session.channelId !== adminChannelId) {
1182
- return "此指令仅限在管理群组中使用";
1183
- }
1145
+ if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
1184
1146
  try {
1185
1147
  const allCaves = await ctx.database.get("cave", { status: "active" });
1186
1148
  if (!allCaves.length) return "目前没有任何回声洞投稿。";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "功能强大、高度可定制的回声洞。支持丰富的媒体类型、内容查重、人工审核、用户昵称、数据迁移以及本地/S3 双重文件存储后端。",
4
- "version": "2.3.15",
4
+ "version": "2.3.17",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],