koishi-plugin-best-cave 2.7.4 → 2.7.6

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,12 @@ import { Context, Logger } from 'koishi';
2
2
  import { Config, CaveObject, StoredElement } from './index';
3
3
  import { FileManager } from './FileManager';
4
4
  /**
5
- * @description 数据库 `cave_meta` 表的完整对象模型。
5
+ * @interface CaveMetaObject
6
+ * @description 定义了数据库 `cave_meta` 表的结构模型。
7
+ * @property {number} cave - 关联的回声洞 `id`,作为外键和主键。
8
+ * @property {string[]} keywords - AI 从回声洞内容中提取的核心关键词数组。
9
+ * @property {string} description - AI 生成的对回声洞内容的简洁摘要或描述。
10
+ * @property {number} rating - AI 对内容质量、趣味性或相关性的综合评分,范围为 0 到 100。
6
11
  */
7
12
  export interface CaveMetaObject {
8
13
  cave: number;
@@ -17,8 +22,7 @@ declare module 'koishi' {
17
22
  }
18
23
  /**
19
24
  * @class AIManager
20
- * @description 负责 AI 分析(描述、评分、关键词)和 AI 查重。
21
- * 通过与外部 AI 服务接口交互,实现对回声洞内容的深度分析和重复性检查。
25
+ * @description AI 管理器,是连接 AI 服务与回声洞功能的核心模块。
22
26
  */
23
27
  export declare class AIManager {
24
28
  private ctx;
@@ -26,25 +30,24 @@ export declare class AIManager {
26
30
  private logger;
27
31
  private fileManager;
28
32
  private http;
33
+ private requestCount;
34
+ private rateLimitResetTime;
29
35
  /**
30
36
  * @constructor
31
- * @param {Context} ctx - Koishi 的上下文对象。
32
- * @param {Config} config - 插件的配置信息。
33
- * @param {Logger} logger - 日志记录器实例。
34
- * @param {FileManager} fileManager - 文件管理器实例。
37
+ * @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
35
38
  */
36
39
  constructor(ctx: Context, config: Config, logger: Logger, fileManager: FileManager);
37
40
  /**
38
- * @description 注册与 AI 功能相关的 `.ai` 子命令。
39
- * @param {any} cave - 主 `cave` 命令实例。
41
+ * @description 注册所有与 AIManager 功能相关的 Koishi 命令。
42
+ * @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
40
43
  */
41
44
  registerCommands(cave: any): void;
42
45
  /**
43
- * @description 对新内容进行两阶段 AI 查重。
44
- * @param {StoredElement[]} newElements - 新内容的元素数组。
45
- * @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 新内容中待上传的媒体文件信息。
46
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer
47
- * @returns {Promise<{ duplicate: boolean; id?: number }>} - 返回 AI 判断结果。
46
+ * @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 }>} 一个包含查重结果的对象。
48
51
  */
49
52
  checkForDuplicates(newElements: StoredElement[], newMediaToSave: {
50
53
  sourceUrl: string;
@@ -57,57 +60,31 @@ export declare class AIManager {
57
60
  id?: number;
58
61
  }>;
59
62
  /**
60
- * @description 分析单个回声洞,并将分析结果存入数据库。
61
- * @param {CaveObject} cave - 需要分析的回声洞对象。
62
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
63
- * @returns {Promise<void>}
63
+ * @description 对单个回声洞对象执行完整的分析和存储流程。
64
+ * @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
65
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
66
+ * @returns {Promise<void>} 操作完成后 resolve 的 Promise。
67
+ * @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
64
68
  */
65
69
  analyzeAndStore(cave: CaveObject, mediaBuffers?: {
66
70
  fileName: string;
67
71
  buffer: Buffer;
68
72
  }[]): Promise<void>;
69
73
  /**
70
- * @description 调用 AI 模型获取内容的分析结果。
71
- * @param {StoredElement[]} elements - 内容的元素数组。
72
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选) 待保存的媒体文件信息。
73
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
74
- * @returns {Promise<Omit<CaveMetaObject, 'cave'>>} - 返回分析结果对象。
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`。
75
79
  */
76
80
  private getAnalysis;
77
81
  /**
78
- * @description 使用 Jaccard 相似度系数计算两组关键词的相似度。
79
- * @param {Set<string>} setA - 第一组关键词集合。
80
- * @param {Set<string>} setB - 第二组关键词集合。
81
- * @returns {number} - 返回 0 1 之间的相似度值。
82
+ * @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 返回错误时,抛出异常。
82
88
  */
83
- private calculateKeywordSimilarity;
84
- /**
85
- * @description 准备发送给 AI 模型的请求体(Payload)。
86
- * @param {string} prompt - 系统提示词。
87
- * @param {string} schemaString - JSON Schema 字符串。
88
- * @param {StoredElement[]} elements - 内容的元素数组。
89
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选) 待保存的媒体文件信息。
90
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
91
- * @returns {Promise<{ payload: any }>} - 返回包含请求体的对象。
92
- */
93
- private preparePayload;
94
- /**
95
- * @description 准备用于 AI 精准查重的请求体(Payload)。
96
- * @param {StoredElement[]} newElements - 新内容的元素。
97
- * @param {CaveObject[]} existingCaves - 经过初筛的疑似重复的旧内容。
98
- * @returns {Promise<{ payload: any }>} - 返回适用于查重场景的请求体。
99
- */
100
- private prepareDedupePayload;
101
- /**
102
- * @description 解析 AI 返回的分析响应。
103
- * @param {any} response - AI 服务的原始响应对象。
104
- * @returns {Omit<CaveMetaObject, 'cave'>} - 返回结构化的分析结果。
105
- */
106
- private parseAnalysisResponse;
107
- /**
108
- * @description 解析 AI 返回的查重响应。
109
- * @param {any} response - AI 服务的原始响应对象。
110
- * @returns {{ duplicate: boolean; id?: number }} - 返回查重结果。
111
- */
112
- private parseDedupeResponse;
89
+ private requestAI;
113
90
  }
package/lib/index.d.ts CHANGED
@@ -61,6 +61,7 @@ export interface Config {
61
61
  aiEndpoint?: string;
62
62
  aiApiKey?: string;
63
63
  aiModel?: string;
64
+ aiTPM?: number;
64
65
  AnalysePrompt?: string;
65
66
  aiCheckPrompt?: string;
66
67
  aiAnalyseSchema?: string;
package/lib/index.js CHANGED
@@ -978,10 +978,7 @@ var path3 = __toESM(require("path"));
978
978
  var AIManager = class {
979
979
  /**
980
980
  * @constructor
981
- * @param {Context} ctx - Koishi 的上下文对象。
982
- * @param {Config} config - 插件的配置信息。
983
- * @param {Logger} logger - 日志记录器实例。
984
- * @param {FileManager} fileManager - 文件管理器实例。
981
+ * @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
985
982
  */
986
983
  constructor(ctx, config, logger2, fileManager) {
987
984
  this.ctx = ctx;
@@ -1002,31 +999,37 @@ var AIManager = class {
1002
999
  __name(this, "AIManager");
1003
1000
  }
1004
1001
  http;
1002
+ requestCount = 0;
1003
+ rateLimitResetTime = 0;
1005
1004
  /**
1006
- * @description 注册与 AI 功能相关的 `.ai` 子命令。
1007
- * @param {any} cave - 主 `cave` 命令实例。
1005
+ * @description 注册所有与 AIManager 功能相关的 Koishi 命令。
1006
+ * @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
1008
1007
  */
1009
1008
  registerCommands(cave) {
1010
1009
  cave.subcommand(".ai", "分析回声洞", { hidden: true, authority: 4 }).usage("分析尚未分析的回声洞,补全回声洞记录。").action(async ({ session }) => {
1011
- const adminError = requireAdmin(session, this.config);
1012
- if (adminError) return adminError;
1010
+ if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
1013
1011
  try {
1014
1012
  const allCaves = await this.ctx.database.get("cave", { status: "active" });
1015
1013
  const analyzedCaveIds = new Set((await this.ctx.database.get("cave_meta", {})).map((meta) => meta.cave));
1016
1014
  const cavesToAnalyze = allCaves.filter((cave2) => !analyzedCaveIds.has(cave2.id));
1017
1015
  if (cavesToAnalyze.length === 0) return "无需分析回声洞";
1018
1016
  await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
1019
- let totalSuccessCount = 0;
1020
- for (let i = 0; i < cavesToAnalyze.length; i += 5) {
1021
- const batch = cavesToAnalyze.slice(i, i + 5);
1022
- this.logger.info(`[${totalSuccessCount}/${cavesToAnalyze.length}] 正在分析 ${batch.length} 条回声洞...`);
1023
- await Promise.all(batch.map((cave2) => this.analyzeAndStore(cave2)));
1024
- totalSuccessCount += batch.length;
1017
+ let successCount = 0;
1018
+ let failedCount = 0;
1019
+ for (const [index, cave2] of cavesToAnalyze.entries()) {
1020
+ this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
1021
+ try {
1022
+ await this.analyzeAndStore(cave2);
1023
+ successCount++;
1024
+ } catch (error) {
1025
+ failedCount++;
1026
+ this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
1027
+ }
1025
1028
  }
1026
- return `已分析 ${totalSuccessCount} 个回声洞`;
1029
+ return `已分析 ${successCount} 个回声洞(失败 ${failedCount} 个)`;
1027
1030
  } catch (error) {
1028
- this.logger.error("已中断分析回声洞:", error);
1029
- return `分析回声洞失败:${error.message}`;
1031
+ this.logger.error("分析回声洞失败:", error);
1032
+ return `操作失败: ${error.message}`;
1030
1033
  }
1031
1034
  });
1032
1035
  cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
@@ -1034,12 +1037,11 @@ var AIManager = class {
1034
1037
  try {
1035
1038
  const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
1036
1039
  if (!meta) return `回声洞(${id})尚未分析`;
1037
- const keywordsText = meta.keywords.join(", ");
1038
1040
  const report = [
1039
1041
  `回声洞(${id})分析结果:`,
1040
1042
  `描述:${meta.description}`,
1041
- `关键词:${keywordsText}`,
1042
- `评分:${meta.rating}/100`
1043
+ `关键词:${meta.keywords.join(", ")}`,
1044
+ `综合评分:${meta.rating}/100`
1043
1045
  ];
1044
1046
  return import_koishi3.h.text(report.join("\n"));
1045
1047
  } catch (error) {
@@ -1049,90 +1051,83 @@ var AIManager = class {
1049
1051
  });
1050
1052
  }
1051
1053
  /**
1052
- * @description 对新内容进行两阶段 AI 查重。
1053
- * @param {StoredElement[]} newElements - 新内容的元素数组。
1054
- * @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 新内容中待上传的媒体文件信息。
1055
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer
1056
- * @returns {Promise<{ duplicate: boolean; id?: number }>} - 返回 AI 判断结果。
1054
+ * @description 对新提交的内容执行 AI 驱动的查重检查。
1055
+ * @param {StoredElement[]} newElements - 待检查的新内容的结构化数组(包含文本、图片等)。
1056
+ * @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 伴随新内容提交的、需要从 URL 下载的媒体文件列表。
1057
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已经加载到内存中的媒体文件 Buffer,可用于优化性能。
1058
+ * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个包含查重结果的对象。
1057
1059
  */
1058
1060
  async checkForDuplicates(newElements, newMediaToSave, mediaBuffers) {
1059
1061
  try {
1060
1062
  const newAnalysis = await this.getAnalysis(newElements, newMediaToSave, mediaBuffers);
1061
1063
  if (!newAnalysis || newAnalysis.keywords.length === 0) return { duplicate: false };
1062
- const newKeywords = new Set(newAnalysis.keywords);
1063
1064
  const allMeta = await this.ctx.database.get("cave_meta", {});
1064
- const potentialDuplicates = [];
1065
- for (const meta of allMeta) {
1066
- const existingKeywords = new Set(meta.keywords);
1067
- const similarity = this.calculateKeywordSimilarity(newKeywords, existingKeywords);
1065
+ const potentialDuplicates = (await Promise.all(allMeta.map(async (meta) => {
1066
+ const setA = new Set(newAnalysis.keywords);
1067
+ const setB = new Set(meta.keywords);
1068
+ let similarity = 0;
1069
+ if (setA.size > 0 && setB.size > 0) {
1070
+ const intersection = new Set([...setA].filter((x) => setB.has(x)));
1071
+ const union = /* @__PURE__ */ new Set([...setA, ...setB]);
1072
+ similarity = intersection.size / union.size;
1073
+ }
1068
1074
  if (similarity * 100 >= 80) {
1069
1075
  const [cave] = await this.ctx.database.get("cave", { id: meta.cave });
1070
- if (cave) potentialDuplicates.push(cave);
1076
+ return cave;
1071
1077
  }
1072
- }
1078
+ }))).filter(Boolean);
1073
1079
  if (potentialDuplicates.length === 0) return { duplicate: false };
1074
- const { payload } = await this.prepareDedupePayload(newElements, potentialDuplicates);
1075
- const fullUrl = `${this.config.aiEndpoint}/models/${this.config.aiModel}:generateContent?key=${this.config.aiApiKey}`;
1076
- const response = await this.http.post(fullUrl, payload, { headers: { "Content-Type": "application/json" }, timeout: 9e4 });
1077
- return this.parseDedupeResponse(response);
1080
+ const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
1081
+ const userMessage = {
1082
+ role: "user",
1083
+ content: JSON.stringify({
1084
+ new_content: { text: formatContent(newElements) },
1085
+ existing_contents: potentialDuplicates.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
1086
+ })
1087
+ };
1088
+ const response = await this.requestAI([userMessage], this.config.aiCheckPrompt, this.config.aiCheckSchema);
1089
+ return {
1090
+ duplicate: response.duplicate || false,
1091
+ id: response.id ? Number(response.id) : void 0
1092
+ };
1078
1093
  } catch (error) {
1079
1094
  this.logger.error("查重回声洞出错:", error);
1080
1095
  return { duplicate: false };
1081
1096
  }
1082
1097
  }
1083
1098
  /**
1084
- * @description 分析单个回声洞,并将分析结果存入数据库。
1085
- * @param {CaveObject} cave - 需要分析的回声洞对象。
1086
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
1087
- * @returns {Promise<void>}
1099
+ * @description 对单个回声洞对象执行完整的分析和存储流程。
1100
+ * @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
1101
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
1102
+ * @returns {Promise<void>} 操作完成后 resolve 的 Promise。
1103
+ * @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
1088
1104
  */
1089
1105
  async analyzeAndStore(cave, mediaBuffers) {
1090
1106
  try {
1091
- const analysisResult = await this.getAnalysis(cave.elements, void 0, mediaBuffers);
1092
- if (analysisResult) await this.ctx.database.upsert("cave_meta", [{ cave: cave.id, ...analysisResult }]);
1107
+ const result = await this.getAnalysis(cave.elements, void 0, mediaBuffers);
1108
+ if (result) {
1109
+ await this.ctx.database.upsert("cave_meta", [{
1110
+ cave: cave.id,
1111
+ ...result,
1112
+ rating: Math.max(0, Math.min(100, result.rating || 0))
1113
+ }]);
1114
+ }
1093
1115
  } catch (error) {
1094
1116
  this.logger.error(`分析回声洞(${cave.id})失败:`, error);
1095
1117
  throw error;
1096
1118
  }
1097
1119
  }
1098
1120
  /**
1099
- * @description 调用 AI 模型获取内容的分析结果。
1100
- * @param {StoredElement[]} elements - 内容的元素数组。
1101
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选) 待保存的媒体文件信息。
1102
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
1103
- * @returns {Promise<Omit<CaveMetaObject, 'cave'>>} - 返回分析结果对象。
1121
+ * @description 准备并发送内容给 AI 模型以获取分析结果。
1122
+ * @param {StoredElement[]} elements - 内容的结构化元素数组。
1123
+ * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选)需要从网络下载的媒体文件信息。
1124
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已存在于内存中的媒体文件 Buffer。
1125
+ * @returns {Promise<Omit<CaveMetaObject, 'cave'>>} 返回一个不含 `cave` 字段的分析结果对象。如果内容为空或无法处理,则返回 `null`。
1104
1126
  */
1105
1127
  async getAnalysis(elements, mediaToSave, mediaBuffers) {
1106
- const { payload } = await this.preparePayload(this.config.AnalysePrompt, this.config.aiAnalyseSchema, elements, mediaToSave, mediaBuffers);
1107
- if (!payload.contents) return null;
1108
- const fullUrl = `${this.config.aiEndpoint}/models/${this.config.aiModel}:generateContent?key=${this.config.aiApiKey}`;
1109
- const response = await this.http.post(fullUrl, payload, { headers: { "Content-Type": "application/json" }, timeout: 6e4 });
1110
- return this.parseAnalysisResponse(response);
1111
- }
1112
- /**
1113
- * @description 使用 Jaccard 相似度系数计算两组关键词的相似度。
1114
- * @param {Set<string>} setA - 第一组关键词集合。
1115
- * @param {Set<string>} setB - 第二组关键词集合。
1116
- * @returns {number} - 返回 0 到 1 之间的相似度值。
1117
- */
1118
- calculateKeywordSimilarity(setA, setB) {
1119
- const intersection = new Set([...setA].filter((x) => setB.has(x)));
1120
- const union = /* @__PURE__ */ new Set([...setA, ...setB]);
1121
- return union.size === 0 ? 0 : intersection.size / union.size;
1122
- }
1123
- /**
1124
- * @description 准备发送给 AI 模型的请求体(Payload)。
1125
- * @param {string} prompt - 系统提示词。
1126
- * @param {string} schemaString - JSON Schema 字符串。
1127
- * @param {StoredElement[]} elements - 内容的元素数组。
1128
- * @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选) 待保存的媒体文件信息。
1129
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
1130
- * @returns {Promise<{ payload: any }>} - 返回包含请求体的对象。
1131
- */
1132
- async preparePayload(prompt, schemaString, elements, mediaToSave, mediaBuffers) {
1133
- const parts = [{ text: prompt }];
1128
+ const userContent = [];
1134
1129
  const combinedText = elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
1135
- if (combinedText) parts.push({ text: combinedText });
1130
+ if (combinedText.trim()) userContent.push({ type: "text", text: combinedText });
1136
1131
  const mediaMap = new Map(mediaBuffers?.map((m) => [m.fileName, m.buffer]));
1137
1132
  const imageElements = elements.filter((el) => el.type === "image" && el.file);
1138
1133
  for (const el of imageElements) {
@@ -1148,93 +1143,57 @@ var AIManager = class {
1148
1143
  }
1149
1144
  if (buffer) {
1150
1145
  const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
1151
- parts.push({ inline_data: { mime_type: mimeType, data: buffer.toString("base64") } });
1146
+ userContent.push({
1147
+ type: "image_url",
1148
+ image_url: { url: `data:${mimeType};base64,${buffer.toString("base64")}` }
1149
+ });
1152
1150
  }
1153
1151
  } catch (error) {
1154
1152
  this.logger.warn(`分析内容(${el.file})失败:`, error);
1155
1153
  }
1156
1154
  }
1157
- if (parts.length <= 1) return { payload: {} };
1158
- try {
1159
- const schema = JSON.parse(schemaString);
1160
- return {
1161
- payload: {
1162
- contents: [{ parts }],
1163
- generationConfig: {
1164
- responseMimeType: "application/json",
1165
- responseSchema: schema
1166
- }
1167
- }
1168
- };
1169
- } catch (error) {
1170
- this.logger.error("分析 JSON Schema 解析失败:", error);
1171
- return { payload: {} };
1172
- }
1155
+ if (userContent.length === 0) return null;
1156
+ const userMessage = { role: "user", content: userContent };
1157
+ return await this.requestAI([userMessage], this.config.AnalysePrompt, this.config.aiAnalyseSchema);
1173
1158
  }
1174
1159
  /**
1175
- * @description 准备用于 AI 精准查重的请求体(Payload)。
1176
- * @param {StoredElement[]} newElements - 新内容的元素。
1177
- * @param {CaveObject[]} existingCaves - 经过初筛的疑似重复的旧内容。
1178
- * @returns {Promise<{ payload: any }>} - 返回适用于查重场景的请求体。
1160
+ * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
1161
+ * @param {any[]} messages - 要发送给 AI 的消息数组,格式遵循 OpenAI API 规范。
1162
+ * @param {string} systemPrompt - 指导 AI 行为的系统级提示词。
1163
+ * @param {string} schemaString - 一个 JSON 字符串,定义了期望 AI 返回的 JSON 对象的结构。
1164
+ * @returns {Promise<any>} AI 返回的、经过 JSON 解析的响应体。
1165
+ * @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
1179
1166
  */
1180
- async prepareDedupePayload(newElements, existingCaves) {
1181
- const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
1182
- const payloadContent = JSON.stringify({
1183
- new_content: { text: formatContent(newElements) },
1184
- existing_contents: existingCaves.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
1185
- });
1186
- const parts = [{ text: this.config.aiCheckPrompt }, { text: payloadContent }];
1187
- try {
1188
- const schema = JSON.parse(this.config.aiCheckSchema);
1189
- return {
1190
- payload: {
1191
- contents: [{ parts }],
1192
- generationConfig: {
1193
- responseMimeType: "application/json",
1194
- responseSchema: schema
1195
- }
1196
- }
1197
- };
1198
- } catch (error) {
1199
- this.logger.error("查重 JSON Schema 解析失败:", error);
1200
- return { payload: {} };
1167
+ async requestAI(messages, systemPrompt, schemaString) {
1168
+ const now = Date.now();
1169
+ if (now > this.rateLimitResetTime) {
1170
+ this.rateLimitResetTime = now + 6e4;
1171
+ this.requestCount = 0;
1201
1172
  }
1202
- }
1203
- /**
1204
- * @description 解析 AI 返回的分析响应。
1205
- * @param {any} response - AI 服务的原始响应对象。
1206
- * @returns {Omit<CaveMetaObject, 'cave'>} - 返回结构化的分析结果。
1207
- */
1208
- parseAnalysisResponse(response) {
1209
- try {
1210
- if (!response?.candidates?.[0]?.content?.parts?.[0]?.text) throw new Error("分析响应解析失败");
1211
- const content = response.candidates[0].content.parts[0].text;
1212
- const parsed = JSON.parse(content);
1213
- const keywords = Array.isArray(parsed.keywords) ? parsed.keywords : [];
1214
- return {
1215
- keywords,
1216
- description: parsed.description || "无",
1217
- rating: Math.max(0, Math.min(100, parsed.rating || 0))
1218
- };
1219
- } catch (error) {
1220
- this.logger.error("分析响应解析失败:", error, "原始响应:", JSON.stringify(response));
1221
- throw error;
1173
+ if (this.requestCount >= this.config.aiTPM) {
1174
+ const delay = this.rateLimitResetTime - now;
1175
+ await new Promise((resolve) => setTimeout(resolve, delay));
1176
+ this.rateLimitResetTime = Date.now() + 6e4;
1177
+ this.requestCount = 0;
1222
1178
  }
1223
- }
1224
- /**
1225
- * @description 解析 AI 返回的查重响应。
1226
- * @param {any} response - AI 服务的原始响应对象。
1227
- * @returns {{ duplicate: boolean; id?: number }} - 返回查重结果。
1228
- */
1229
- parseDedupeResponse(response) {
1179
+ let schema = JSON.parse(schemaString);
1180
+ const payload = {
1181
+ model: this.config.aiModel,
1182
+ messages: [{ role: "system", content: systemPrompt }, ...messages],
1183
+ response_format: { type: "json_schema", strict: true, schema }
1184
+ };
1185
+ const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1186
+ const headers = {
1187
+ "Content-Type": "application/json",
1188
+ "Authorization": `Bearer ${this.config.aiApiKey}`
1189
+ };
1230
1190
  try {
1231
- if (!response?.candidates?.[0]?.content?.parts?.[0]?.text) throw new Error("查重响应解析失败");
1232
- const content = response.candidates[0].content.parts[0].text;
1233
- const parsed = JSON.parse(content);
1234
- if (parsed.duplicate === true && parsed.id) return { duplicate: true, id: Number(parsed.id) };
1235
- return { duplicate: false };
1191
+ this.requestCount++;
1192
+ const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1193
+ return JSON.parse(response.choices[0].message.content);
1236
1194
  } catch (error) {
1237
- this.logger.error("查重响应解析失败:", error, "原始响应:", JSON.stringify(response));
1195
+ const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
1196
+ this.logger.error(`请求 API 失败: ${errorMessage}`);
1238
1197
  throw error;
1239
1198
  }
1240
1199
  }
@@ -1272,10 +1231,11 @@ var Config = import_koishi4.Schema.intersect([
1272
1231
  }).description("复核配置"),
1273
1232
  import_koishi4.Schema.object({
1274
1233
  enableAI: import_koishi4.Schema.boolean().default(false).description("启用 AI"),
1275
- aiEndpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta"),
1234
+ aiEndpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1276
1235
  aiApiKey: import_koishi4.Schema.string().description("密钥 (Key)").role("secret"),
1277
- aiModel: import_koishi4.Schema.string().description("模型").default("gemini-2.5-flash"),
1278
- AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析提示词 (Prompt)"),
1236
+ aiModel: import_koishi4.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1237
+ aiTPM: import_koishi4.Schema.number().description("每分钟请求数 (TPM)").default(60),
1238
+ AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
1279
1239
  aiAnalyseSchema: import_koishi4.Schema.string().role("textarea").default(
1280
1240
  `{
1281
1241
  "type": "object",
@@ -1298,8 +1258,8 @@ var Config = import_koishi4.Schema.intersect([
1298
1258
  },
1299
1259
  "required": ["keywords", "description", "rating"]
1300
1260
  }`
1301
- ).description("分析输出模式 (JSON Schema)"),
1302
- aiCheckPrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重提示词 (Prompt)"),
1261
+ ).description("分析 JSON Schema"),
1262
+ aiCheckPrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
1303
1263
  aiCheckSchema: import_koishi4.Schema.string().role("textarea").default(
1304
1264
  `{
1305
1265
  "type": "object",
@@ -1315,7 +1275,7 @@ var Config = import_koishi4.Schema.intersect([
1315
1275
  },
1316
1276
  "required": ["duplicate"]
1317
1277
  }`
1318
- ).description("查重输出模式 (JSON Schema)")
1278
+ ).description("查重 JSON Schema")
1319
1279
  }).description("模型配置"),
1320
1280
  import_koishi4.Schema.object({
1321
1281
  localPath: import_koishi4.Schema.string().description("文件映射路径"),
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.4",
4
+ "version": "2.7.6",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],