koishi-plugin-best-cave 2.7.11 → 2.7.12

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.
@@ -32,6 +32,12 @@ export declare class HashManager {
32
32
  * @param cave - 主 `cave` 命令实例。
33
33
  */
34
34
  registerCommands(cave: any): void;
35
+ /**
36
+ * @description 扫描并修复单个图片 Buffer,移除文件结束符之后的多余数据。
37
+ * @param imageBuffer - 原始的图片 Buffer。
38
+ * @returns 修复后的图片 Buffer。如果无需修复,则返回原始 Buffer。
39
+ */
40
+ sanitizeImageBuffer(imageBuffer: Buffer): Buffer;
35
41
  /**
36
42
  * @description 执行一维离散余弦变换 (DCT-II) 的方法。
37
43
  * @param input - 输入的数字数组。
package/lib/Utils.d.ts CHANGED
@@ -57,7 +57,7 @@ export declare function processMessageElements(sourceElements: h[], newId: numbe
57
57
  * @description 执行文本 (Simhash) 和图片 (pHash) 相似度查重。
58
58
  * @returns 一个对象,指示是否发现重复项;如果未发现,则返回生成的哈希。
59
59
  */
60
- export declare function performSimilarityChecks(ctx: Context, config: Config, hashManager: HashManager, finalElementsForDb: StoredElement[], downloadedMedia: {
60
+ export declare function performSimilarityChecks(ctx: Context, config: Config, hashManager: HashManager, logger: Logger, finalElementsForDb: StoredElement[], downloadedMedia: {
61
61
  fileName: string;
62
62
  buffer: Buffer;
63
63
  }[]): Promise<{
@@ -81,7 +81,7 @@ export declare function performSimilarityChecks(ctx: Context, config: Config, ha
81
81
  export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, cave: CaveObject, downloadedMedia: {
82
82
  fileName: string;
83
83
  buffer: Buffer;
84
- }[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'>;
84
+ }[], reusableIds: Set<number>, needsReview: boolean): Promise<'pending' | 'active'>;
85
85
  /**
86
86
  * @description 校验会话是否来自指定的管理群组。
87
87
  * @param session 当前会话。
package/lib/index.js CHANGED
@@ -450,42 +450,46 @@ async function processMessageElements(sourceElements, newId, session, creationTi
450
450
  return { finalElementsForDb, mediaToSave };
451
451
  }
452
452
  __name(processMessageElements, "processMessageElements");
453
- async function performSimilarityChecks(ctx, config, hashManager, finalElementsForDb, downloadedMedia) {
454
- const textHashesToStore = [];
455
- const imageHashesToStore = [];
456
- const combinedText = finalElementsForDb.filter((el) => el.type === "text" && typeof el.content === "string").map((el) => el.content).join(" ");
457
- if (combinedText) {
458
- const newSimhash = hashManager.generateTextSimhash(combinedText);
459
- if (newSimhash) {
460
- const existingTextHashes = await ctx.database.get("cave_hash", { type: "text" });
461
- for (const existing of existingTextHashes) {
462
- const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
463
- if (similarity >= config.textThreshold) return { duplicate: true, message: `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
453
+ async function performSimilarityChecks(ctx, config, hashManager, logger2, finalElementsForDb, downloadedMedia) {
454
+ try {
455
+ const textHashesToStore = [];
456
+ const imageHashesToStore = [];
457
+ const combinedText = finalElementsForDb.filter((el) => el.type === "text" && typeof el.content === "string").map((el) => el.content).join(" ");
458
+ if (combinedText) {
459
+ const newSimhash = hashManager.generateTextSimhash(combinedText);
460
+ if (newSimhash) {
461
+ const existingTextHashes = await ctx.database.get("cave_hash", { type: "text" });
462
+ for (const existing of existingTextHashes) {
463
+ const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
464
+ if (similarity >= config.textThreshold) return { duplicate: true, message: `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
465
+ }
466
+ textHashesToStore.push({ hash: newSimhash, type: "text" });
464
467
  }
465
- textHashesToStore.push({ hash: newSimhash, type: "text" });
466
468
  }
467
- }
468
- if (downloadedMedia.length > 0) {
469
- const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "image" });
470
- for (const media of downloadedMedia) {
471
- if ([".png", ".jpg", ".jpeg", ".webp"].includes(path2.extname(media.fileName).toLowerCase())) {
472
- const imageHash = await hashManager.generatePHash(media.buffer);
473
- for (const existing of allExistingImageHashes) {
474
- const similarity = hashManager.calculateSimilarity(imageHash, existing.hash);
475
- if (similarity >= config.imageThreshold) return { duplicate: true, message: `图片与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
469
+ if (downloadedMedia.length > 0) {
470
+ const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "image" });
471
+ for (const media of downloadedMedia) {
472
+ if ([".png", ".jpg", ".jpeg", ".webp"].includes(path2.extname(media.fileName).toLowerCase())) {
473
+ const imageHash = await hashManager.generatePHash(media.buffer);
474
+ for (const existing of allExistingImageHashes) {
475
+ const similarity = hashManager.calculateSimilarity(imageHash, existing.hash);
476
+ if (similarity >= config.imageThreshold) return { duplicate: true, message: `图片与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
477
+ }
478
+ imageHashesToStore.push({ hash: imageHash, type: "image" });
479
+ allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "image" });
476
480
  }
477
- imageHashesToStore.push({ hash: imageHash, type: "image" });
478
- allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "image" });
479
481
  }
480
482
  }
483
+ return { duplicate: false, textHashesToStore, imageHashesToStore };
484
+ } catch (error) {
485
+ logger2.warn("相似度比较失败:", error);
486
+ return { duplicate: false, textHashesToStore: [], imageHashesToStore: [] };
481
487
  }
482
- return { duplicate: false, textHashesToStore, imageHashesToStore };
483
488
  }
484
489
  __name(performSimilarityChecks, "performSimilarityChecks");
485
- async function handleFileUploads(ctx, config, fileManager, logger2, cave, downloadedMedia, reusableIds, session) {
490
+ async function handleFileUploads(ctx, config, fileManager, logger2, cave, downloadedMedia, reusableIds, needsReview) {
486
491
  try {
487
492
  await Promise.all(downloadedMedia.map((item) => fileManager.saveFile(item.fileName, item.buffer)));
488
- const needsReview = config.enablePend && session.channelId !== config.adminChannel?.split(":")[1];
489
493
  const finalStatus = needsReview ? "pending" : "active";
490
494
  await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
491
495
  return finalStatus;
@@ -845,37 +849,12 @@ var HashManager = class {
845
849
  if (!cavesToProcess.length) return "无可修复的回声洞";
846
850
  let fixedFiles = 0;
847
851
  let errorCount = 0;
848
- const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
849
- const JPEG_SIGNATURE = Buffer.from([255, 216]);
850
- const GIF_SIGNATURE = Buffer.from("GIF");
851
852
  for (const cave2 of cavesToProcess) {
852
853
  const imageElements = cave2.elements.filter((el) => el.type === "image" && el.file);
853
854
  for (const element of imageElements) {
854
855
  try {
855
856
  const originalBuffer = await this.fileManager.readFile(element.file);
856
- let sanitizedBuffer = originalBuffer;
857
- if (originalBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
858
- const IEND_CHUNK = Buffer.from("IEND");
859
- const iendIndex = originalBuffer.lastIndexOf(IEND_CHUNK);
860
- if (iendIndex !== -1) {
861
- const endOfPngData = iendIndex + 8;
862
- if (originalBuffer.length > endOfPngData) sanitizedBuffer = originalBuffer.slice(0, endOfPngData);
863
- }
864
- } else if (originalBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
865
- const EOI_MARKER = Buffer.from([255, 217]);
866
- const eoiIndex = originalBuffer.lastIndexOf(EOI_MARKER);
867
- if (eoiIndex !== -1) {
868
- const endOfJpegData = eoiIndex + 2;
869
- if (originalBuffer.length > endOfJpegData) sanitizedBuffer = originalBuffer.slice(0, endOfJpegData);
870
- }
871
- } else if (originalBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
872
- const GIF_TERMINATOR = Buffer.from([59]);
873
- const terminatorIndex = originalBuffer.lastIndexOf(GIF_TERMINATOR);
874
- if (terminatorIndex !== -1) {
875
- const endOfGifData = terminatorIndex + 1;
876
- if (originalBuffer.length > endOfGifData) sanitizedBuffer = originalBuffer.slice(0, endOfGifData);
877
- }
878
- }
857
+ const sanitizedBuffer = this.sanitizeImageBuffer(originalBuffer);
879
858
  if (!originalBuffer.equals(sanitizedBuffer)) {
880
859
  await this.fileManager.saveFile(element.file, sanitizedBuffer);
881
860
  fixedFiles++;
@@ -895,6 +874,40 @@ var HashManager = class {
895
874
  }
896
875
  });
897
876
  }
877
+ /**
878
+ * @description 扫描并修复单个图片 Buffer,移除文件结束符之后的多余数据。
879
+ * @param imageBuffer - 原始的图片 Buffer。
880
+ * @returns 修复后的图片 Buffer。如果无需修复,则返回原始 Buffer。
881
+ */
882
+ sanitizeImageBuffer(imageBuffer) {
883
+ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
884
+ const JPEG_SIGNATURE = Buffer.from([255, 216]);
885
+ const GIF_SIGNATURE = Buffer.from("GIF");
886
+ let sanitizedBuffer = imageBuffer;
887
+ if (imageBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
888
+ const IEND_CHUNK = Buffer.from("IEND");
889
+ const iendIndex = imageBuffer.lastIndexOf(IEND_CHUNK);
890
+ if (iendIndex !== -1) {
891
+ const endOfPngData = iendIndex + 8;
892
+ if (imageBuffer.length > endOfPngData) sanitizedBuffer = imageBuffer.slice(0, endOfPngData);
893
+ }
894
+ } else if (imageBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
895
+ const EOI_MARKER = Buffer.from([255, 217]);
896
+ const eoiIndex = imageBuffer.lastIndexOf(EOI_MARKER);
897
+ if (eoiIndex !== -1) {
898
+ const endOfJpegData = eoiIndex + 2;
899
+ if (imageBuffer.length > endOfJpegData) sanitizedBuffer = imageBuffer.slice(0, endOfJpegData);
900
+ }
901
+ } else if (imageBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
902
+ const GIF_TERMINATOR = Buffer.from([59]);
903
+ const terminatorIndex = imageBuffer.lastIndexOf(GIF_TERMINATOR);
904
+ if (terminatorIndex !== -1) {
905
+ const endOfGifData = terminatorIndex + 1;
906
+ if (imageBuffer.length > endOfGifData) sanitizedBuffer = imageBuffer.slice(0, endOfGifData);
907
+ }
908
+ }
909
+ return sanitizedBuffer;
910
+ }
898
911
  /**
899
912
  * @description 执行一维离散余弦变换 (DCT-II) 的方法。
900
913
  * @param input - 输入的数字数组。
@@ -1094,15 +1107,19 @@ var AIManager = class {
1094
1107
  * @returns {Promise<void>} 分析和存储操作完成后解析的 Promise。
1095
1108
  */
1096
1109
  async analyzeAndStore(cave, mediaBuffers) {
1097
- const mediaMap = mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0;
1098
- const [result] = await this.getAnalyses([cave], mediaMap);
1099
- if (result) {
1100
- await this.ctx.database.upsert("cave_meta", [{
1101
- cave: cave.id,
1102
- keywords: result.keywords || [],
1103
- description: result.description || "",
1104
- rating: Math.max(0, Math.min(100, result.rating || 0))
1105
- }]);
1110
+ try {
1111
+ const mediaMap = mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0;
1112
+ const [result] = await this.getAnalyses([cave], mediaMap);
1113
+ if (result) {
1114
+ await this.ctx.database.upsert("cave_meta", [{
1115
+ cave: cave.id,
1116
+ keywords: result.keywords || [],
1117
+ description: result.description || "",
1118
+ rating: Math.max(0, Math.min(100, result.rating || 0))
1119
+ }]);
1120
+ }
1121
+ } catch (error) {
1122
+ this.logger.error(`分析回声洞(${cave.id})出错:`, error);
1106
1123
  }
1107
1124
  }
1108
1125
  /**
@@ -1256,7 +1273,7 @@ var Config = import_koishi3.Schema.intersect([
1256
1273
  enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1257
1274
  aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1258
1275
  aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1259
- aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1276
+ aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-1.5-flash"),
1260
1277
  aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
1261
1278
  AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我以JSON格式提供的一组内容(每项包含ID、文本和图片),为每一项内容总结关键词、概括内容并评分。你需要返回一个包含所有分析结果的JSON对象。`).description("分析 Prompt"),
1262
1279
  aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
@@ -1403,7 +1420,8 @@ function apply(ctx, config) {
1403
1420
  let textHashesToStore = [];
1404
1421
  let imageHashesToStore = [];
1405
1422
  if (hashManager) {
1406
- const checkResult = await performSimilarityChecks(ctx, config, hashManager, finalElementsForDb, downloadedMedia);
1423
+ for (const media of downloadedMedia) media.buffer = hashManager.sanitizeImageBuffer(media.buffer);
1424
+ const checkResult = await performSimilarityChecks(ctx, config, hashManager, logger, finalElementsForDb, downloadedMedia);
1407
1425
  if (checkResult.duplicate) return checkResult.message;
1408
1426
  textHashesToStore = checkResult.textHashesToStore;
1409
1427
  imageHashesToStore = checkResult.imageHashesToStore;
@@ -1413,7 +1431,7 @@ function apply(ctx, config) {
1413
1431
  if (duplicateResult && duplicateResult.duplicate) return `内容与回声洞(${duplicateResult.id})重复`;
1414
1432
  }
1415
1433
  const userName = (config.enableName ? await profileManager.getNickname(session.userId) : null) || session.username;
1416
- const needsReview = config.enablePend && session.channelId !== config.adminChannel?.split(":")[1];
1434
+ const needsReview = config.enablePend && session.cid !== config.adminChannel;
1417
1435
  let finalStatus = hasMedia ? "preload" : needsReview ? "pending" : "active";
1418
1436
  const newCave = await ctx.database.create("cave", {
1419
1437
  id: newId,
@@ -1424,7 +1442,7 @@ function apply(ctx, config) {
1424
1442
  status: finalStatus,
1425
1443
  time: creationTime
1426
1444
  });
1427
- if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
1445
+ if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, needsReview);
1428
1446
  if (finalStatus !== "preload") {
1429
1447
  newCave.status = finalStatus;
1430
1448
  if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
@@ -1458,7 +1476,7 @@ function apply(ctx, config) {
1458
1476
  const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
1459
1477
  if (!targetCave) return `回声洞(${id})不存在`;
1460
1478
  const isAuthor = targetCave.userId === session.userId;
1461
- const isAdmin = session.channelId === config.adminChannel?.split(":")[1];
1479
+ const isAdmin = session.cid === config.adminChannel;
1462
1480
  if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
1463
1481
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
1464
1482
  const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
@@ -1471,8 +1489,8 @@ function apply(ctx, config) {
1471
1489
  });
1472
1490
  cave.subcommand(".list", "查询投稿统计").option("user", "-u <user:user> 指定用户").option("all", "-a 查看排行").action(async ({ session, options }) => {
1473
1491
  if (options.all) {
1474
- const adminChannelId = config.adminChannel?.split(":")[1];
1475
- if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
1492
+ const adminError = requireAdmin(session, config);
1493
+ if (adminError) return adminError;
1476
1494
  try {
1477
1495
  const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
1478
1496
  if (!aggregatedStats.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.7.11",
4
+ "version": "2.7.12",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],