koishi-plugin-best-cave 2.7.10 → 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.
@@ -2,7 +2,6 @@ import { Context, Logger } from 'koishi';
2
2
  import { Config, CaveObject, StoredElement } from './index';
3
3
  import { FileManager } from './FileManager';
4
4
  /**
5
- * @interface CaveMetaObject
6
5
  * @description 定义了数据库 `cave_meta` 表的结构模型。
7
6
  * @property {number} cave - 关联的回声洞 `id`,作为外键和主键。
8
7
  * @property {string[]} keywords - AI 从回声洞内容中提取的核心关键词数组。
@@ -22,7 +21,7 @@ declare module 'koishi' {
22
21
  }
23
22
  /**
24
23
  * @class AIManager
25
- * @description AI 管理器,是连接 AI 服务与回声洞功能的核心模块。
24
+ * @description AI 管理器,连接 AI 服务与回声洞功能的核心模块。
26
25
  */
27
26
  export declare class AIManager {
28
27
  private ctx;
@@ -34,25 +33,24 @@ export declare class AIManager {
34
33
  private rateLimitResetTime;
35
34
  /**
36
35
  * @constructor
37
- * @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
36
+ * @param {Context} ctx - Koishi 的上下文对象,提供框架核心功能。
37
+ * @param {Config} config - 插件的配置对象。
38
+ * @param {Logger} logger - 日志记录器实例,用于输出日志。
39
+ * @param {FileManager} fileManager - 文件管理器实例,用于处理媒体文件。
38
40
  */
39
41
  constructor(ctx: Context, config: Config, logger: Logger, fileManager: FileManager);
40
42
  /**
41
43
  * @description 注册所有与 AIManager 功能相关的 Koishi 命令。
42
- * @param {any} cave - `cave` 命令的实例,用于在其下注册子命令。
44
+ * @param {any} cave - Koishi 命令实例,用于挂载子命令。
43
45
  */
44
46
  registerCommands(cave: any): void;
45
47
  /**
46
48
  * @description 对新提交的内容执行 AI 驱动的查重检查。
47
- * @param {StoredElement[]} newElements - 待检查的新内容的结构化数组(包含文本、图片等)。
48
- * @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 伴随新内容提交的、需要从 URL 下载的媒体文件列表。
49
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已经加载到内存中的媒体文件 Buffer,可用于优化性能。
50
- * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个包含查重结果的对象。
49
+ * @param {StoredElement[]} newElements - 新提交的内容元素数组。
50
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组。
51
+ * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID(如果存在)。
51
52
  */
52
- checkForDuplicates(newElements: StoredElement[], newMediaToSave: {
53
- sourceUrl: string;
54
- fileName: string;
55
- }[], mediaBuffers?: {
53
+ checkForDuplicates(newElements: StoredElement[], mediaBuffers?: {
56
54
  fileName: string;
57
55
  buffer: Buffer;
58
56
  }[]): Promise<{
@@ -61,30 +59,45 @@ export declare class AIManager {
61
59
  }>;
62
60
  /**
63
61
  * @description 对单个回声洞对象执行完整的分析和存储流程。
64
- * @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
65
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
66
- * @returns {Promise<void>} 操作完成后 resolve 的 Promise。
67
- * @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
62
+ * @param {CaveObject} cave - 要分析的回声洞对象。
63
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,用于新提交内容的分析。
64
+ * @returns {Promise<void>} 分析和存储操作完成后解析的 Promise。
68
65
  */
69
66
  analyzeAndStore(cave: CaveObject, mediaBuffers?: {
70
67
  fileName: string;
71
68
  buffer: Buffer;
72
69
  }[]): Promise<void>;
73
70
  /**
74
- * @description 准备并发送内容给 AI 模型以获取分析结果。
75
- * @param {StoredElement[]} elements - 内容的结构化元素数组。
76
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选)需要从网络下载的媒体文件信息。
77
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已存在于内存中的媒体文件 Buffer。
78
- * @returns {Promise<Omit<CaveMetaObject, 'cave'>>} 返回一个不含 `cave` 字段的分析结果对象。如果内容为空或无法处理,则返回 `null`。
71
+ * @description 对一批回声洞执行分析并存储结果。
72
+ * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
73
+ * @returns {Promise<number>} 一个 Promise,解析为成功分析和存储的条目数。
74
+ */
75
+ private analyzeAndStoreBatch;
76
+ /**
77
+ * @description 根据新内容的关键词,查找并返回可能重复的回声洞。
78
+ * @param {string[]} newKeywords - 新内容的关键词数组。
79
+ * @returns {Promise<CaveObject[]>} 一个 Promise,解析为可能重复的回声洞对象数组。
80
+ */
81
+ private findPotentialDuplicates;
82
+ /**
83
+ * @description 为一批回声洞准备内容,并向 AI 发送单个请求以获取所有分析结果。
84
+ * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
85
+ * @param {Map<string, Buffer>} [mediaBufferMap] - 可选的媒体文件名到其缓冲区的映射。
86
+ * @returns {Promise<any[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
87
+ */
88
+ private getAnalyses;
89
+ /**
90
+ * @description 确保请求不会超过设定的速率限制(RPM)。如果需要,会延迟执行。
91
+ * @returns {Promise<void>} 当可以继续发送请求时解析的 Promise。
79
92
  */
80
- private getAnalysis;
93
+ private ensureRateLimit;
81
94
  /**
82
95
  * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
83
- * @param {any[]} messages - 要发送给 AI 的消息数组,格式遵循 OpenAI API 规范。
84
- * @param {string} systemPrompt - 指导 AI 行为的系统级提示词。
85
- * @param {string} schemaString - 一个 JSON 字符串,定义了期望 AI 返回的 JSON 对象的结构。
86
- * @returns {Promise<any>} AI 返回的、经过 JSON 解析的响应体。
87
- * @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
96
+ * @param {any[]} messages - 发送给 AI 的消息数组,遵循 OpenAI 格式。
97
+ * @param {string} systemPrompt - 系统提示词,用于指导 AI 的行为。
98
+ * @param {string} schemaString - 定义期望响应格式的 JSON Schema 字符串。
99
+ * @returns {Promise<any>} 一个 Promise,解析为从 AI 接收到的、解析后的 JSON 对象。
100
+ * @throws {Error} 当 AI 返回空或无效内容时抛出错误。
88
101
  */
89
102
  private requestAI;
90
103
  }
@@ -1,5 +1,5 @@
1
1
  import { Context, Logger } from 'koishi';
2
- import { Config, CaveObject } from './index';
2
+ import { Config } from './index';
3
3
  import { FileManager } from './FileManager';
4
4
  /**
5
5
  * @description 数据库 `cave_hash` 表的完整对象模型。
@@ -33,11 +33,11 @@ export declare class HashManager {
33
33
  */
34
34
  registerCommands(cave: any): void;
35
35
  /**
36
- * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
37
- * @param cave - 回声洞对象。
38
- * @returns 生成的哈希对象数组。
36
+ * @description 扫描并修复单个图片 Buffer,移除文件结束符之后的多余数据。
37
+ * @param imageBuffer - 原始的图片 Buffer。
38
+ * @returns 修复后的图片 Buffer。如果无需修复,则返回原始 Buffer。
39
39
  */
40
- generateAllHashesForCave(cave: Pick<CaveObject, 'id' | 'elements'>): Promise<CaveHashObject[]>;
40
+ sanitizeImageBuffer(imageBuffer: Buffer): Buffer;
41
41
  /**
42
42
  * @description 执行一维离散余弦变换 (DCT-II) 的方法。
43
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;
@@ -741,10 +745,27 @@ var HashManager = class {
741
745
  for (const cave2 of cavesToProcess) {
742
746
  processedCaveCount++;
743
747
  try {
744
- const newHashesForCave = await this.generateAllHashesForCave(cave2);
745
- if (newHashesForCave.length > 0) {
746
- hashesToInsert.push(...newHashesForCave);
748
+ const tempHashes = [];
749
+ const uniqueHashTracker = /* @__PURE__ */ new Set();
750
+ const addUniqueHash = /* @__PURE__ */ __name((hashObj) => {
751
+ const key = `${hashObj.hash}-${hashObj.type}`;
752
+ if (!uniqueHashTracker.has(key)) {
753
+ tempHashes.push(hashObj);
754
+ uniqueHashTracker.add(key);
755
+ }
756
+ }, "addUniqueHash");
757
+ const combinedText = cave2.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
758
+ if (combinedText) {
759
+ const textHash = this.generateTextSimhash(combinedText);
760
+ if (textHash) addUniqueHash({ cave: cave2.id, hash: textHash, type: "text" });
761
+ }
762
+ for (const el of cave2.elements.filter((el2) => el2.type === "image" && el2.file)) {
763
+ const imageBuffer = await this.fileManager.readFile(el.file);
764
+ const imageHash = await this.generatePHash(imageBuffer);
765
+ addUniqueHash({ cave: cave2.id, hash: imageHash, type: "image" });
747
766
  }
767
+ const newHashesForCave = tempHashes;
768
+ if (newHashesForCave.length > 0) hashesToInsert.push(...newHashesForCave);
748
769
  if (hashesToInsert.length >= 100) await flushBatch();
749
770
  } catch (error) {
750
771
  errorCount++;
@@ -828,37 +849,12 @@ var HashManager = class {
828
849
  if (!cavesToProcess.length) return "无可修复的回声洞";
829
850
  let fixedFiles = 0;
830
851
  let errorCount = 0;
831
- const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
832
- const JPEG_SIGNATURE = Buffer.from([255, 216]);
833
- const GIF_SIGNATURE = Buffer.from("GIF");
834
852
  for (const cave2 of cavesToProcess) {
835
853
  const imageElements = cave2.elements.filter((el) => el.type === "image" && el.file);
836
854
  for (const element of imageElements) {
837
855
  try {
838
856
  const originalBuffer = await this.fileManager.readFile(element.file);
839
- let sanitizedBuffer = originalBuffer;
840
- if (originalBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
841
- const IEND_CHUNK = Buffer.from("IEND");
842
- const iendIndex = originalBuffer.lastIndexOf(IEND_CHUNK);
843
- if (iendIndex !== -1) {
844
- const endOfPngData = iendIndex + 8;
845
- if (originalBuffer.length > endOfPngData) sanitizedBuffer = originalBuffer.slice(0, endOfPngData);
846
- }
847
- } else if (originalBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
848
- const EOI_MARKER = Buffer.from([255, 217]);
849
- const eoiIndex = originalBuffer.lastIndexOf(EOI_MARKER);
850
- if (eoiIndex !== -1) {
851
- const endOfJpegData = eoiIndex + 2;
852
- if (originalBuffer.length > endOfJpegData) sanitizedBuffer = originalBuffer.slice(0, endOfJpegData);
853
- }
854
- } else if (originalBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
855
- const GIF_TERMINATOR = Buffer.from([59]);
856
- const terminatorIndex = originalBuffer.lastIndexOf(GIF_TERMINATOR);
857
- if (terminatorIndex !== -1) {
858
- const endOfGifData = terminatorIndex + 1;
859
- if (originalBuffer.length > endOfGifData) sanitizedBuffer = originalBuffer.slice(0, endOfGifData);
860
- }
861
- }
857
+ const sanitizedBuffer = this.sanitizeImageBuffer(originalBuffer);
862
858
  if (!originalBuffer.equals(sanitizedBuffer)) {
863
859
  await this.fileManager.saveFile(element.file, sanitizedBuffer);
864
860
  fixedFiles++;
@@ -879,36 +875,38 @@ var HashManager = class {
879
875
  });
880
876
  }
881
877
  /**
882
- * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
883
- * @param cave - 回声洞对象。
884
- * @returns 生成的哈希对象数组。
878
+ * @description 扫描并修复单个图片 Buffer,移除文件结束符之后的多余数据。
879
+ * @param imageBuffer - 原始的图片 Buffer。
880
+ * @returns 修复后的图片 Buffer。如果无需修复,则返回原始 Buffer。
885
881
  */
886
- async generateAllHashesForCave(cave) {
887
- const tempHashes = [];
888
- const uniqueHashTracker = /* @__PURE__ */ new Set();
889
- const addUniqueHash = /* @__PURE__ */ __name((hashObj) => {
890
- const key = `${hashObj.hash}-${hashObj.type}`;
891
- if (!uniqueHashTracker.has(key)) {
892
- tempHashes.push(hashObj);
893
- uniqueHashTracker.add(key);
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);
894
893
  }
895
- }, "addUniqueHash");
896
- const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
897
- if (combinedText) {
898
- const textHash = this.generateTextSimhash(combinedText);
899
- if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "text" });
900
- }
901
- for (const el of cave.elements.filter((el2) => el2.type === "image" && el2.file)) {
902
- try {
903
- const imageBuffer = await this.fileManager.readFile(el.file);
904
- const imageHash = await this.generatePHash(imageBuffer);
905
- addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
906
- } catch (error) {
907
- this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, error);
908
- throw error;
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);
909
907
  }
910
908
  }
911
- return tempHashes;
909
+ return sanitizedBuffer;
912
910
  }
913
911
  /**
914
912
  * @description 执行一维离散余弦变换 (DCT-II) 的方法。
@@ -1016,7 +1014,10 @@ var path3 = __toESM(require("path"));
1016
1014
  var AIManager = class {
1017
1015
  /**
1018
1016
  * @constructor
1019
- * @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
1017
+ * @param {Context} ctx - Koishi 的上下文对象,提供框架核心功能。
1018
+ * @param {Config} config - 插件的配置对象。
1019
+ * @param {Logger} logger - 日志记录器实例,用于输出日志。
1020
+ * @param {FileManager} fileManager - 文件管理器实例,用于处理媒体文件。
1020
1021
  */
1021
1022
  constructor(ctx, config, logger2, fileManager) {
1022
1023
  this.ctx = ctx;
@@ -1041,28 +1042,26 @@ var AIManager = class {
1041
1042
  rateLimitResetTime = 0;
1042
1043
  /**
1043
1044
  * @description 注册所有与 AIManager 功能相关的 Koishi 命令。
1044
- * @param {any} cave - `cave` 命令的实例,用于在其下注册子命令。
1045
+ * @param {any} cave - Koishi 命令实例,用于挂载子命令。
1045
1046
  */
1046
1047
  registerCommands(cave) {
1047
1048
  cave.subcommand(".ai", "分析回声洞", { hidden: true, authority: 4 }).usage("分析尚未分析的回声洞,补全回声洞记录。").action(async ({ session }) => {
1048
1049
  if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
1049
1050
  try {
1050
1051
  const allCaves = await this.ctx.database.get("cave", { status: "active" });
1051
- const analyzedCaveIds = new Set((await this.ctx.database.get("cave_meta", {})).map((meta) => meta.cave));
1052
+ const analyzedCaveIds = new Set((await this.ctx.database.get("cave_meta", {}, { fields: ["cave"] })).map((meta) => meta.cave));
1052
1053
  const cavesToAnalyze = allCaves.filter((cave2) => !analyzedCaveIds.has(cave2.id));
1053
1054
  if (cavesToAnalyze.length === 0) return "无需分析回声洞";
1054
1055
  await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
1055
- let successCount = 0;
1056
- for (const [index, cave2] of cavesToAnalyze.entries()) {
1057
- try {
1058
- this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
1059
- await this.analyzeAndStore(cave2);
1060
- successCount++;
1061
- } catch (error) {
1062
- return `分析回声洞(${cave2.id})时出错: ${error.message}`;
1063
- }
1056
+ let totalSuccessCount = 0;
1057
+ const batchSize = 10;
1058
+ for (let i = 0; i < cavesToAnalyze.length; i += batchSize) {
1059
+ const batch = cavesToAnalyze.slice(i, i + batchSize);
1060
+ this.logger.info(`[${i + 1}/${cavesToAnalyze.length}] 正在分析 ${batch.length} 条回声洞...`);
1061
+ const successCountInBatch = await this.analyzeAndStoreBatch(batch);
1062
+ totalSuccessCount += successCountInBatch;
1064
1063
  }
1065
- return `已分析 ${successCount} 个回声洞`;
1064
+ return `已分析 ${totalSuccessCount} 个回声洞`;
1066
1065
  } catch (error) {
1067
1066
  this.logger.error("分析回声洞失败:", error);
1068
1067
  return `操作失败: ${error.message}`;
@@ -1071,41 +1070,25 @@ var AIManager = class {
1071
1070
  }
1072
1071
  /**
1073
1072
  * @description 对新提交的内容执行 AI 驱动的查重检查。
1074
- * @param {StoredElement[]} newElements - 待检查的新内容的结构化数组(包含文本、图片等)。
1075
- * @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 伴随新内容提交的、需要从 URL 下载的媒体文件列表。
1076
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已经加载到内存中的媒体文件 Buffer,可用于优化性能。
1077
- * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个包含查重结果的对象。
1073
+ * @param {StoredElement[]} newElements - 新提交的内容元素数组。
1074
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组。
1075
+ * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID(如果存在)。
1078
1076
  */
1079
- async checkForDuplicates(newElements, newMediaToSave, mediaBuffers) {
1077
+ async checkForDuplicates(newElements, mediaBuffers) {
1080
1078
  try {
1081
- const newAnalysis = await this.getAnalysis(newElements, newMediaToSave, mediaBuffers);
1082
- if (!newAnalysis || newAnalysis.keywords.length === 0) return { duplicate: false };
1083
- const allMeta = await this.ctx.database.get("cave_meta", {});
1084
- const potentialDuplicates = (await Promise.all(allMeta.map(async (meta) => {
1085
- const setA = new Set(newAnalysis.keywords);
1086
- const setB = new Set(meta.keywords);
1087
- let similarity = 0;
1088
- if (setA.size > 0 && setB.size > 0) {
1089
- const intersection = new Set([...setA].filter((x) => setB.has(x)));
1090
- const union = /* @__PURE__ */ new Set([...setA, ...setB]);
1091
- similarity = intersection.size / union.size;
1092
- }
1093
- if (similarity * 100 >= 80) {
1094
- const [cave] = await this.ctx.database.get("cave", { id: meta.cave });
1095
- return cave;
1096
- }
1097
- }))).filter(Boolean);
1079
+ const dummyCave = { id: 0, elements: newElements, channelId: "", userId: "", userName: "", status: "preload", time: /* @__PURE__ */ new Date() };
1080
+ const mediaMap = mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0;
1081
+ const [newAnalysis] = await this.getAnalyses([dummyCave], mediaMap);
1082
+ if (!newAnalysis?.keywords?.length) return { duplicate: false };
1083
+ const potentialDuplicates = await this.findPotentialDuplicates(newAnalysis.keywords);
1098
1084
  if (potentialDuplicates.length === 0) return { duplicate: false };
1099
1085
  const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
1100
1086
  const userMessage = {
1101
1087
  role: "user",
1102
- content: [{
1103
- type: "input_text",
1104
- text: JSON.stringify({
1105
- new_content: { text: formatContent(newElements) },
1106
- existing_contents: potentialDuplicates.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
1107
- })
1108
- }]
1088
+ content: JSON.stringify({
1089
+ new_content: { text: formatContent(newElements) },
1090
+ existing_contents: potentialDuplicates.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
1091
+ })
1109
1092
  };
1110
1093
  const response = await this.requestAI([userMessage], this.config.aiCheckPrompt, this.config.aiCheckSchema);
1111
1094
  return {
@@ -1119,74 +1102,96 @@ var AIManager = class {
1119
1102
  }
1120
1103
  /**
1121
1104
  * @description 对单个回声洞对象执行完整的分析和存储流程。
1122
- * @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
1123
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
1124
- * @returns {Promise<void>} 操作完成后 resolve 的 Promise。
1125
- * @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
1105
+ * @param {CaveObject} cave - 要分析的回声洞对象。
1106
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,用于新提交内容的分析。
1107
+ * @returns {Promise<void>} 分析和存储操作完成后解析的 Promise。
1126
1108
  */
1127
1109
  async analyzeAndStore(cave, mediaBuffers) {
1128
1110
  try {
1129
- const result = await this.getAnalysis(cave.elements, void 0, mediaBuffers);
1111
+ const mediaMap = mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0;
1112
+ const [result] = await this.getAnalyses([cave], mediaMap);
1130
1113
  if (result) {
1131
1114
  await this.ctx.database.upsert("cave_meta", [{
1132
1115
  cave: cave.id,
1133
- ...result,
1116
+ keywords: result.keywords || [],
1117
+ description: result.description || "",
1134
1118
  rating: Math.max(0, Math.min(100, result.rating || 0))
1135
1119
  }]);
1136
1120
  }
1137
1121
  } catch (error) {
1138
- this.logger.error(`分析回声洞(${cave.id})失败:`, error);
1139
- throw error;
1122
+ this.logger.error(`分析回声洞(${cave.id})出错:`, error);
1140
1123
  }
1141
1124
  }
1142
1125
  /**
1143
- * @description 准备并发送内容给 AI 模型以获取分析结果。
1144
- * @param {StoredElement[]} elements - 内容的结构化元素数组。
1145
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选)需要从网络下载的媒体文件信息。
1146
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已存在于内存中的媒体文件 Buffer。
1147
- * @returns {Promise<Omit<CaveMetaObject, 'cave'>>} 返回一个不含 `cave` 字段的分析结果对象。如果内容为空或无法处理,则返回 `null`。
1126
+ * @description 对一批回声洞执行分析并存储结果。
1127
+ * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
1128
+ * @returns {Promise<number>} 一个 Promise,解析为成功分析和存储的条目数。
1148
1129
  */
1149
- async getAnalysis(elements, mediaToSave, mediaBuffers) {
1150
- const userContent = [];
1151
- const combinedText = elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
1152
- if (combinedText.trim()) userContent.push({ type: "input_text", text: combinedText });
1153
- const mediaMap = new Map(mediaBuffers?.map((m) => [m.fileName, m.buffer]));
1154
- const imageElements = elements.filter((el) => el.type === "image" && el.file);
1155
- for (const el of imageElements) {
1156
- try {
1157
- let buffer;
1158
- if (mediaMap.has(el.file)) {
1159
- buffer = mediaMap.get(el.file);
1160
- } else if (mediaToSave) {
1161
- const item = mediaToSave.find((m) => m.fileName === el.file);
1162
- if (item) buffer = Buffer.from(await this.ctx.http.get(item.sourceUrl, { responseType: "arraybuffer" }));
1163
- } else {
1164
- buffer = await this.fileManager.readFile(el.file);
1165
- }
1166
- if (buffer) {
1167
- const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
1168
- userContent.push({
1169
- type: "input_image",
1170
- image_url: `data:${mimeType};base64,${buffer.toString("base64")}`
1171
- });
1172
- }
1173
- } catch (error) {
1174
- this.logger.warn(`分析内容(${el.file})失败:`, error);
1175
- }
1176
- }
1177
- if (userContent.length === 0) return null;
1178
- const userMessage = { role: "user", content: userContent };
1179
- return await this.requestAI([userMessage], this.config.AnalysePrompt, this.config.aiAnalyseSchema);
1130
+ async analyzeAndStoreBatch(caves) {
1131
+ const results = await this.getAnalyses(caves);
1132
+ if (!results?.length) return 0;
1133
+ const caveMetaObjects = results.map((res) => ({
1134
+ cave: res.id,
1135
+ keywords: res.keywords || [],
1136
+ description: res.description || "",
1137
+ rating: Math.max(0, Math.min(100, res.rating || 0))
1138
+ }));
1139
+ await this.ctx.database.upsert("cave_meta", caveMetaObjects);
1140
+ return caveMetaObjects.length;
1180
1141
  }
1181
1142
  /**
1182
- * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
1183
- * @param {any[]} messages - 要发送给 AI 的消息数组,格式遵循 OpenAI API 规范。
1184
- * @param {string} systemPrompt - 指导 AI 行为的系统级提示词。
1185
- * @param {string} schemaString - 一个 JSON 字符串,定义了期望 AI 返回的 JSON 对象的结构。
1186
- * @returns {Promise<any>} AI 返回的、经过 JSON 解析的响应体。
1187
- * @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
1143
+ * @description 根据新内容的关键词,查找并返回可能重复的回声洞。
1144
+ * @param {string[]} newKeywords - 新内容的关键词数组。
1145
+ * @returns {Promise<CaveObject[]>} 一个 Promise,解析为可能重复的回声洞对象数组。
1188
1146
  */
1189
- async requestAI(messages, systemPrompt, schemaString) {
1147
+ async findPotentialDuplicates(newKeywords) {
1148
+ const allMeta = await this.ctx.database.get("cave_meta", {}, { fields: ["cave", "keywords"] });
1149
+ const newKeywordsSet = new Set(newKeywords);
1150
+ const similarCaveIds = allMeta.filter((meta) => {
1151
+ if (!meta.keywords?.length) return false;
1152
+ const existingKeywordsSet = new Set(meta.keywords);
1153
+ const intersection = new Set([...newKeywordsSet].filter((x) => existingKeywordsSet.has(x)));
1154
+ const union = /* @__PURE__ */ new Set([...newKeywordsSet, ...existingKeywordsSet]);
1155
+ const similarity = union.size > 0 ? intersection.size / union.size : 0;
1156
+ return similarity * 100 >= 80;
1157
+ }).map((meta) => meta.cave);
1158
+ if (similarCaveIds.length === 0) return [];
1159
+ return this.ctx.database.get("cave", { id: { $in: similarCaveIds } });
1160
+ }
1161
+ /**
1162
+ * @description 为一批回声洞准备内容,并向 AI 发送单个请求以获取所有分析结果。
1163
+ * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
1164
+ * @param {Map<string, Buffer>} [mediaBufferMap] - 可选的媒体文件名到其缓冲区的映射。
1165
+ * @returns {Promise<any[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
1166
+ */
1167
+ async getAnalyses(caves, mediaBufferMap) {
1168
+ const batchPayload = await Promise.all(caves.map(async (cave) => {
1169
+ const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
1170
+ const imagesBase64 = (await Promise.all(
1171
+ cave.elements.filter((el) => el.type === "image" && el.file).map(async (el) => {
1172
+ try {
1173
+ const buffer = mediaBufferMap?.get(el.file) ?? await this.fileManager.readFile(el.file);
1174
+ const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
1175
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
1176
+ } catch (error) {
1177
+ this.logger.warn(`读取文件(${el.file})失败:`, error);
1178
+ return null;
1179
+ }
1180
+ })
1181
+ )).filter(Boolean);
1182
+ return { id: cave.id, text: combinedText, images: imagesBase64 };
1183
+ }));
1184
+ const nonEmptyPayload = batchPayload.filter((p) => p.text.trim() || p.images.length > 0);
1185
+ if (nonEmptyPayload.length === 0) return [];
1186
+ const userMessage = { role: "user", content: JSON.stringify(nonEmptyPayload) };
1187
+ const response = await this.requestAI([userMessage], this.config.AnalysePrompt, this.config.aiAnalyseSchema);
1188
+ return response.analyses || [];
1189
+ }
1190
+ /**
1191
+ * @description 确保请求不会超过设定的速率限制(RPM)。如果需要,会延迟执行。
1192
+ * @returns {Promise<void>} 当可以继续发送请求时解析的 Promise。
1193
+ */
1194
+ async ensureRateLimit() {
1190
1195
  const now = Date.now();
1191
1196
  if (now > this.rateLimitResetTime) {
1192
1197
  this.rateLimitResetTime = now + 6e4;
@@ -1194,51 +1199,43 @@ var AIManager = class {
1194
1199
  }
1195
1200
  if (this.requestCount >= this.config.aiRPM) {
1196
1201
  const delay = this.rateLimitResetTime - now;
1197
- await new Promise((resolve) => setTimeout(resolve, delay));
1202
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
1198
1203
  this.rateLimitResetTime = Date.now() + 6e4;
1199
1204
  this.requestCount = 0;
1200
1205
  }
1201
- let schema;
1202
- try {
1203
- schema = JSON.parse(schemaString);
1204
- } catch (error) {
1205
- this.logger.error("解析 JSON Schema 失败:", error);
1206
- throw new Error("无效的 JSON Schema 配置");
1207
- }
1206
+ }
1207
+ /**
1208
+ * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
1209
+ * @param {any[]} messages - 发送给 AI 的消息数组,遵循 OpenAI 格式。
1210
+ * @param {string} systemPrompt - 系统提示词,用于指导 AI 的行为。
1211
+ * @param {string} schemaString - 定义期望响应格式的 JSON Schema 字符串。
1212
+ * @returns {Promise<any>} 一个 Promise,解析为从 AI 接收到的、解析后的 JSON 对象。
1213
+ * @throws {Error} 当 AI 返回空或无效内容时抛出错误。
1214
+ */
1215
+ async requestAI(messages, systemPrompt, schemaString) {
1216
+ await this.ensureRateLimit();
1208
1217
  const payload = {
1209
1218
  model: this.config.aiModel,
1210
- input: [
1211
- { role: "system", content: systemPrompt },
1212
- ...messages
1213
- ],
1214
- text: {
1215
- format: {
1216
- type: "json_schema",
1217
- name: "extracted_data",
1218
- schema
1219
+ messages: [{ role: "system", content: systemPrompt }, ...messages],
1220
+ response_format: {
1221
+ type: "json_schema",
1222
+ json_schema: {
1223
+ name: "extract_data",
1224
+ description: "根据提供的内容提取或分析信息。",
1225
+ schema: JSON.parse(schemaString)
1219
1226
  }
1220
1227
  }
1221
1228
  };
1222
- const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/responses`;
1229
+ const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1223
1230
  const headers = {
1224
1231
  "Content-Type": "application/json",
1225
1232
  "Authorization": `Bearer ${this.config.aiApiKey}`
1226
1233
  };
1227
- try {
1228
- this.requestCount++;
1229
- const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1230
- const responseText = response.output?.[0]?.content?.[0]?.text;
1231
- if (responseText) {
1232
- return JSON.parse(responseText);
1233
- } else {
1234
- this.logger.error("AI 响应格式不正确:", JSON.stringify(response));
1235
- throw new Error("AI 响应格式不正确");
1236
- }
1237
- } catch (error) {
1238
- const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
1239
- this.logger.error(`请求 API 失败: ${errorMessage}`);
1240
- throw error;
1241
- }
1234
+ this.requestCount++;
1235
+ const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1236
+ const content = response.choices?.[0]?.message?.content;
1237
+ if (typeof content === "string" && content.trim()) return JSON.parse(content);
1238
+ throw new Error("响应无效");
1242
1239
  }
1243
1240
  };
1244
1241
 
@@ -1276,30 +1273,44 @@ var Config = import_koishi3.Schema.intersect([
1276
1273
  enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1277
1274
  aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1278
1275
  aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1279
- 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"),
1280
1277
  aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
1281
- AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
1278
+ AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我以JSON格式提供的一组内容(每项包含ID、文本和图片),为每一项内容总结关键词、概括内容并评分。你需要返回一个包含所有分析结果的JSON对象。`).description("分析 Prompt"),
1282
1279
  aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
1283
1280
  `{
1284
1281
  "type": "object",
1285
1282
  "properties": {
1286
- "keywords": {
1283
+ "analyses": {
1287
1284
  "type": "array",
1288
- "items": { "type": "string" },
1289
- "description": "使用尽可能多的关键词准确形容内容"
1290
- },
1291
- "description": {
1292
- "type": "string",
1293
- "description": "概括或描述这部分内容"
1294
- },
1295
- "rating": {
1296
- "type": "integer",
1297
- "description": "对内容的综合质量进行评分",
1298
- "minimum": 0,
1299
- "maximum": 100
1285
+ "description": "分析结果的数组",
1286
+ "items": {
1287
+ "type": "object",
1288
+ "properties": {
1289
+ "id": {
1290
+ "type": "integer",
1291
+ "description": "内容的唯一ID"
1292
+ },
1293
+ "keywords": {
1294
+ "type": "array",
1295
+ "items": { "type": "string" },
1296
+ "description": "使用尽可能多的关键词准确形容内容"
1297
+ },
1298
+ "description": {
1299
+ "type": "string",
1300
+ "description": "概括或描述这部分内容"
1301
+ },
1302
+ "rating": {
1303
+ "type": "integer",
1304
+ "description": "对内容的综合质量进行评分",
1305
+ "minimum": 0,
1306
+ "maximum": 100
1307
+ }
1308
+ },
1309
+ "required": ["id", "keywords", "description", "rating"]
1310
+ }
1300
1311
  }
1301
1312
  },
1302
- "required": ["keywords", "description", "rating"]
1313
+ "required": ["analyses"]
1303
1314
  }`
1304
1315
  ).description("分析 JSON Schema"),
1305
1316
  aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
@@ -1409,17 +1420,18 @@ function apply(ctx, config) {
1409
1420
  let textHashesToStore = [];
1410
1421
  let imageHashesToStore = [];
1411
1422
  if (hashManager) {
1412
- 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);
1413
1425
  if (checkResult.duplicate) return checkResult.message;
1414
1426
  textHashesToStore = checkResult.textHashesToStore;
1415
1427
  imageHashesToStore = checkResult.imageHashesToStore;
1416
1428
  }
1417
1429
  if (aiManager) {
1418
- const duplicateResult = await aiManager.checkForDuplicates(finalElementsForDb, mediaToSave, downloadedMedia);
1430
+ const duplicateResult = await aiManager.checkForDuplicates(finalElementsForDb, downloadedMedia);
1419
1431
  if (duplicateResult && duplicateResult.duplicate) return `内容与回声洞(${duplicateResult.id})重复`;
1420
1432
  }
1421
1433
  const userName = (config.enableName ? await profileManager.getNickname(session.userId) : null) || session.username;
1422
- const needsReview = config.enablePend && session.channelId !== config.adminChannel?.split(":")[1];
1434
+ const needsReview = config.enablePend && session.cid !== config.adminChannel;
1423
1435
  let finalStatus = hasMedia ? "preload" : needsReview ? "pending" : "active";
1424
1436
  const newCave = await ctx.database.create("cave", {
1425
1437
  id: newId,
@@ -1430,7 +1442,7 @@ function apply(ctx, config) {
1430
1442
  status: finalStatus,
1431
1443
  time: creationTime
1432
1444
  });
1433
- 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);
1434
1446
  if (finalStatus !== "preload") {
1435
1447
  newCave.status = finalStatus;
1436
1448
  if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
@@ -1464,7 +1476,7 @@ function apply(ctx, config) {
1464
1476
  const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
1465
1477
  if (!targetCave) return `回声洞(${id})不存在`;
1466
1478
  const isAuthor = targetCave.userId === session.userId;
1467
- const isAdmin = session.channelId === config.adminChannel?.split(":")[1];
1479
+ const isAdmin = session.cid === config.adminChannel;
1468
1480
  if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
1469
1481
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
1470
1482
  const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
@@ -1477,8 +1489,8 @@ function apply(ctx, config) {
1477
1489
  });
1478
1490
  cave.subcommand(".list", "查询投稿统计").option("user", "-u <user:user> 指定用户").option("all", "-a 查看排行").action(async ({ session, options }) => {
1479
1491
  if (options.all) {
1480
- const adminChannelId = config.adminChannel?.split(":")[1];
1481
- if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
1492
+ const adminError = requireAdmin(session, config);
1493
+ if (adminError) return adminError;
1482
1494
  try {
1483
1495
  const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
1484
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.10",
4
+ "version": "2.7.12",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],