koishi-plugin-best-cave 2.7.12 → 2.7.14

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.
@@ -48,55 +48,44 @@ export declare class AIManager {
48
48
  * @description 对新提交的内容执行 AI 驱动的查重检查。
49
49
  * @param {StoredElement[]} newElements - 新提交的内容元素数组。
50
50
  * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组。
51
- * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID(如果存在)。
51
+ * @returns {Promise<{ duplicate: boolean; ids?: number[] }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID 数组(如果存在)。
52
52
  */
53
53
  checkForDuplicates(newElements: StoredElement[], mediaBuffers?: {
54
54
  fileName: string;
55
55
  buffer: Buffer;
56
56
  }[]): Promise<{
57
57
  duplicate: boolean;
58
- id?: number;
58
+ ids?: number[];
59
59
  }>;
60
60
  /**
61
- * @description 对单个回声洞对象执行完整的分析和存储流程。
62
- * @param {CaveObject} cave - 要分析的回声洞对象。
63
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,用于新提交内容的分析。
64
- * @returns {Promise<void>} 分析和存储操作完成后解析的 Promise。
65
- */
66
- analyzeAndStore(cave: CaveObject, mediaBuffers?: {
67
- fileName: string;
68
- buffer: Buffer;
69
- }[]): Promise<void>;
70
- /**
71
- * @description 对一批回声洞执行分析并存储结果。
61
+ * @description 对单个或批量回声洞执行完整的分析和存储流程。
72
62
  * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
63
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,仅在分析新内容时使用。
73
64
  * @returns {Promise<number>} 一个 Promise,解析为成功分析和存储的条目数。
74
65
  */
75
- private analyzeAndStoreBatch;
66
+ analyzeAndStore(caves: CaveObject[], mediaBuffers?: {
67
+ fileName: string;
68
+ buffer: Buffer;
69
+ }[]): Promise<number>;
76
70
  /**
77
- * @description 根据新内容的关键词,查找并返回可能重复的回声洞。
78
- * @param {string[]} newKeywords - 新内容的关键词数组。
79
- * @returns {Promise<CaveObject[]>} 一个 Promise,解析为可能重复的回声洞对象数组。
71
+ * @description 调用 AI 判断两个回声洞内容是否重复或高度相似。
72
+ * @param {CaveObject} caveA - 第一个回声洞对象。
73
+ * @param {CaveObject} caveB - 第二个回声洞对象。
74
+ * @returns {Promise<boolean>} 如果内容相似则返回 true,否则返回 false。
80
75
  */
81
- private findPotentialDuplicates;
76
+ private isContentDuplicateAI;
82
77
  /**
83
78
  * @description 为一批回声洞准备内容,并向 AI 发送单个请求以获取所有分析结果。
84
79
  * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
85
80
  * @param {Map<string, Buffer>} [mediaBufferMap] - 可选的媒体文件名到其缓冲区的映射。
86
- * @returns {Promise<any[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
81
+ * @returns {Promise<CaveMetaObject[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
87
82
  */
88
83
  private getAnalyses;
89
84
  /**
90
- * @description 确保请求不会超过设定的速率限制(RPM)。如果需要,会延迟执行。
91
- * @returns {Promise<void>} 当可以继续发送请求时解析的 Promise。
92
- */
93
- private ensureRateLimit;
94
- /**
95
- * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
85
+ * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑,并稳健地解析 JSON 响应。
96
86
  * @param {any[]} messages - 发送给 AI 的消息数组,遵循 OpenAI 格式。
97
87
  * @param {string} systemPrompt - 系统提示词,用于指导 AI 的行为。
98
- * @param {string} schemaString - 定义期望响应格式的 JSON Schema 字符串。
99
- * @returns {Promise<any>} 一个 Promise,解析为从 AI 接收到的、解析后的 JSON 对象。
88
+ * @returns {Promise<T>} 一个 Promise,解析为从 AI 接收到的、解析后的 JSON 对象。
100
89
  * @throws {Error} 当 AI 返回空或无效内容时抛出错误。
101
90
  */
102
91
  private requestAI;
package/lib/index.d.ts CHANGED
@@ -62,10 +62,6 @@ export interface Config {
62
62
  aiApiKey?: string;
63
63
  aiModel?: string;
64
64
  aiRPM?: number;
65
- AnalysePrompt?: string;
66
- aiCheckPrompt?: string;
67
- aiAnalyseSchema?: string;
68
- aiCheckSchema?: string;
69
65
  }
70
66
  export declare const Config: Schema<Config>;
71
67
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -1058,7 +1058,7 @@ var AIManager = class {
1058
1058
  for (let i = 0; i < cavesToAnalyze.length; i += batchSize) {
1059
1059
  const batch = cavesToAnalyze.slice(i, i + batchSize);
1060
1060
  this.logger.info(`[${i + 1}/${cavesToAnalyze.length}] 正在分析 ${batch.length} 条回声洞...`);
1061
- const successCountInBatch = await this.analyzeAndStoreBatch(batch);
1061
+ const successCountInBatch = await this.analyzeAndStore(batch);
1062
1062
  totalSuccessCount += successCountInBatch;
1063
1063
  }
1064
1064
  return `已分析 ${totalSuccessCount} 个回声洞`;
@@ -1067,102 +1067,130 @@ var AIManager = class {
1067
1067
  return `操作失败: ${error.message}`;
1068
1068
  }
1069
1069
  });
1070
+ cave.subcommand(".compare", "比较重复性", { hidden: true }).usage("检查回声洞,找出可能重复的内容。").action(async ({ session }) => {
1071
+ if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
1072
+ await session.send("正在检查,请稍候...");
1073
+ try {
1074
+ const allMeta = await this.ctx.database.get("cave_meta", {});
1075
+ if (allMeta.length < 2) return "无可比较数据";
1076
+ const allCaves = new Map((await this.ctx.database.get("cave", { status: "active" })).map((c) => [c.id, c]));
1077
+ const foundPairs = /* @__PURE__ */ new Set();
1078
+ const checkedPairs = /* @__PURE__ */ new Set();
1079
+ for (let i = 0; i < allMeta.length; i++) {
1080
+ for (let j = i + 1; j < allMeta.length; j++) {
1081
+ const meta1 = allMeta[i];
1082
+ const meta2 = allMeta[j];
1083
+ const pairKey = [meta1.cave, meta2.cave].sort((a, b) => a - b).join("-");
1084
+ if (checkedPairs.has(pairKey)) continue;
1085
+ const keywords1 = new Set(meta1.keywords);
1086
+ const keywords2 = new Set(meta2.keywords);
1087
+ const intersection = new Set([...keywords1].filter((x) => keywords2.has(x)));
1088
+ const union = /* @__PURE__ */ new Set([...keywords1, ...keywords2]);
1089
+ const similarity = union.size > 0 ? intersection.size / union.size : 0;
1090
+ if (similarity * 100 >= 80) {
1091
+ const cave1 = allCaves.get(meta1.cave);
1092
+ const cave2 = allCaves.get(meta2.cave);
1093
+ if (cave1 && cave2 && await this.isContentDuplicateAI(cave1, cave2)) foundPairs.add(`${cave1.id} & ${cave2.id}`);
1094
+ checkedPairs.add(pairKey);
1095
+ }
1096
+ }
1097
+ }
1098
+ if (foundPairs.size === 0) return "未发现高重复性的内容";
1099
+ let report = `已发现 ${foundPairs.size} 组高重复性的内容:
1100
+ `;
1101
+ report += [...foundPairs].join("\n");
1102
+ return report.trim();
1103
+ } catch (error) {
1104
+ this.logger.error("检查重复性失败:", error);
1105
+ return `检查失败: ${error.message}`;
1106
+ }
1107
+ });
1070
1108
  }
1071
1109
  /**
1072
1110
  * @description 对新提交的内容执行 AI 驱动的查重检查。
1073
1111
  * @param {StoredElement[]} newElements - 新提交的内容元素数组。
1074
1112
  * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组。
1075
- * @returns {Promise<{ duplicate: boolean; id?: number }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID(如果存在)。
1113
+ * @returns {Promise<{ duplicate: boolean; ids?: number[] }>} 一个 Promise,解析为一个对象,指示内容是否重复以及重复的回声洞 ID 数组(如果存在)。
1076
1114
  */
1077
1115
  async checkForDuplicates(newElements, mediaBuffers) {
1078
1116
  try {
1079
1117
  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);
1084
- if (potentialDuplicates.length === 0) return { duplicate: false };
1085
- const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
1086
- const userMessage = {
1087
- role: "user",
1088
- content: JSON.stringify({
1089
- new_content: { text: formatContent(newElements) },
1090
- existing_contents: potentialDuplicates.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
1091
- })
1092
- };
1093
- const response = await this.requestAI([userMessage], this.config.aiCheckPrompt, this.config.aiCheckSchema);
1094
- return {
1095
- duplicate: response.duplicate || false,
1096
- id: response.id ? Number(response.id) : void 0
1097
- };
1118
+ const [newAnalysis] = await this.getAnalyses([dummyCave], mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0);
1119
+ if (!newAnalysis?.keywords?.length) return { duplicate: false, ids: [] };
1120
+ const allMeta = await this.ctx.database.get("cave_meta", {}, { fields: ["cave", "keywords"] });
1121
+ const newKeywordsSet = new Set(newAnalysis.keywords);
1122
+ const similarCaveIds = allMeta.filter((meta) => {
1123
+ if (!meta.keywords?.length) return false;
1124
+ const existingKeywordsSet = new Set(meta.keywords);
1125
+ const intersection = new Set([...newKeywordsSet].filter((x) => existingKeywordsSet.has(x)));
1126
+ const union = /* @__PURE__ */ new Set([...newKeywordsSet, ...existingKeywordsSet]);
1127
+ const similarity = union.size > 0 ? intersection.size / union.size : 0;
1128
+ return similarity * 100 >= 80;
1129
+ }).map((meta) => meta.cave);
1130
+ if (similarCaveIds.length === 0) return { duplicate: false, ids: [] };
1131
+ const potentialDuplicates = await this.ctx.database.get("cave", { id: { $in: similarCaveIds } });
1132
+ const duplicateIds = [];
1133
+ for (const existingCave of potentialDuplicates) if (await this.isContentDuplicateAI(dummyCave, existingCave)) duplicateIds.push(existingCave.id);
1134
+ return { duplicate: duplicateIds.length > 0, ids: duplicateIds };
1098
1135
  } catch (error) {
1099
1136
  this.logger.error("查重回声洞出错:", error);
1100
- return { duplicate: false };
1137
+ return { duplicate: false, ids: [] };
1101
1138
  }
1102
1139
  }
1103
1140
  /**
1104
- * @description 对单个回声洞对象执行完整的分析和存储流程。
1105
- * @param {CaveObject} cave - 要分析的回声洞对象。
1106
- * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,用于新提交内容的分析。
1107
- * @returns {Promise<void>} 分析和存储操作完成后解析的 Promise
1141
+ * @description 对单个或批量回声洞执行完整的分析和存储流程。
1142
+ * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
1143
+ * @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - 可选的媒体文件缓冲区数组,仅在分析新内容时使用。
1144
+ * @returns {Promise<number>} 一个 Promise,解析为成功分析和存储的条目数。
1108
1145
  */
1109
- async analyzeAndStore(cave, mediaBuffers) {
1146
+ async analyzeAndStore(caves, mediaBuffers) {
1110
1147
  try {
1111
1148
  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
- }
1149
+ const results = await this.getAnalyses(caves, mediaMap);
1150
+ if (!results?.length) return 0;
1151
+ const caveMetaObjects = results.map((res) => ({
1152
+ cave: res.cave,
1153
+ keywords: res.keywords || [],
1154
+ description: res.description || "",
1155
+ rating: Math.max(0, Math.min(100, res.rating || 0))
1156
+ }));
1157
+ await this.ctx.database.upsert("cave_meta", caveMetaObjects);
1158
+ return caveMetaObjects.length;
1121
1159
  } catch (error) {
1122
- this.logger.error(`分析回声洞(${cave.id})出错:`, error);
1160
+ const caveIds = caves.map((c) => c.id).join(", ");
1161
+ this.logger.error(`分析回声洞 (${caveIds}) 出错:`, error);
1162
+ return 0;
1123
1163
  }
1124
1164
  }
1125
1165
  /**
1126
- * @description 对一批回声洞执行分析并存储结果。
1127
- * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
1128
- * @returns {Promise<number>} 一个 Promise,解析为成功分析和存储的条目数。
1129
- */
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;
1141
- }
1142
- /**
1143
- * @description 根据新内容的关键词,查找并返回可能重复的回声洞。
1144
- * @param {string[]} newKeywords - 新内容的关键词数组。
1145
- * @returns {Promise<CaveObject[]>} 一个 Promise,解析为可能重复的回声洞对象数组。
1166
+ * @description 调用 AI 判断两个回声洞内容是否重复或高度相似。
1167
+ * @param {CaveObject} caveA - 第一个回声洞对象。
1168
+ * @param {CaveObject} caveB - 第二个回声洞对象。
1169
+ * @returns {Promise<boolean>} 如果内容相似则返回 true,否则返回 false。
1146
1170
  */
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 } });
1171
+ async isContentDuplicateAI(caveA, caveB) {
1172
+ try {
1173
+ const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" "), "formatContent");
1174
+ const userMessage = {
1175
+ role: "user",
1176
+ content: JSON.stringify({
1177
+ content_a: { id: caveA.id, text: formatContent(caveA.elements) },
1178
+ content_b: { id: caveB.id, text: formatContent(caveB.elements) }
1179
+ })
1180
+ };
1181
+ const prompt = `你是一位内容查重专家。请判断 content_a 和 content_b 是否重复或高度相似。你的回复必须且只能是一个包裹在 \`\`\`json ... \`\`\` 代码块中的 JSON 对象,该对象仅包含一个键 "duplicate" (布尔值)。`;
1182
+ const response = await this.requestAI([userMessage], prompt);
1183
+ return response.duplicate || false;
1184
+ } catch (error) {
1185
+ this.logger.error(`比较回声洞(${caveA.id})与(${caveB.id})失败:`, error);
1186
+ return false;
1187
+ }
1160
1188
  }
1161
1189
  /**
1162
1190
  * @description 为一批回声洞准备内容,并向 AI 发送单个请求以获取所有分析结果。
1163
1191
  * @param {CaveObject[]} caves - 要分析的回声洞对象数组。
1164
1192
  * @param {Map<string, Buffer>} [mediaBufferMap] - 可选的媒体文件名到其缓冲区的映射。
1165
- * @returns {Promise<any[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
1193
+ * @returns {Promise<CaveMetaObject[]>} 一个 Promise,解析为 AI 返回的分析结果数组。
1166
1194
  */
1167
1195
  async getAnalyses(caves, mediaBufferMap) {
1168
1196
  const batchPayload = await Promise.all(caves.map(async (cave) => {
@@ -1184,14 +1212,23 @@ var AIManager = class {
1184
1212
  const nonEmptyPayload = batchPayload.filter((p) => p.text.trim() || p.images.length > 0);
1185
1213
  if (nonEmptyPayload.length === 0) return [];
1186
1214
  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 || [];
1215
+ const analysePrompt = `你是一位内容分析专家。请使用中文,分析我以JSON格式提供的一组内容,为每一项内容总结关键词、概括内容并评分。你的回复必须且只能是一个包裹在 \`\`\`json ... \`\`\` 代码块中的有效 JSON 对象。该JSON对象应有一个 "analyses" 键,其值为一个数组。数组中的每个对象都必须包含 "id" (整数), "keywords" (字符串数组), "description" (字符串), 和 "rating" (0-100的整数)。`;
1216
+ const response = await this.requestAI([userMessage], analysePrompt);
1217
+ return (response.analyses || []).map((res) => ({
1218
+ cave: res.id,
1219
+ keywords: res.keywords,
1220
+ description: res.description,
1221
+ rating: res.rating
1222
+ }));
1189
1223
  }
1190
1224
  /**
1191
- * @description 确保请求不会超过设定的速率限制(RPM)。如果需要,会延迟执行。
1192
- * @returns {Promise<void>} 当可以继续发送请求时解析的 Promise。
1225
+ * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑,并稳健地解析 JSON 响应。
1226
+ * @param {any[]} messages - 发送给 AI 的消息数组,遵循 OpenAI 格式。
1227
+ * @param {string} systemPrompt - 系统提示词,用于指导 AI 的行为。
1228
+ * @returns {Promise<T>} 一个 Promise,解析为从 AI 接收到的、解析后的 JSON 对象。
1229
+ * @throws {Error} 当 AI 返回空或无效内容时抛出错误。
1193
1230
  */
1194
- async ensureRateLimit() {
1231
+ async requestAI(messages, systemPrompt) {
1195
1232
  const now = Date.now();
1196
1233
  if (now > this.rateLimitResetTime) {
1197
1234
  this.rateLimitResetTime = now + 6e4;
@@ -1203,28 +1240,9 @@ var AIManager = class {
1203
1240
  this.rateLimitResetTime = Date.now() + 6e4;
1204
1241
  this.requestCount = 0;
1205
1242
  }
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();
1217
1243
  const payload = {
1218
1244
  model: this.config.aiModel,
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)
1226
- }
1227
- }
1245
+ messages: [{ role: "system", content: systemPrompt }, ...messages]
1228
1246
  };
1229
1247
  const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1230
1248
  const headers = {
@@ -1234,8 +1252,22 @@ var AIManager = class {
1234
1252
  this.requestCount++;
1235
1253
  const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
1236
1254
  const content = response.choices?.[0]?.message?.content;
1237
- if (typeof content === "string" && content.trim()) return JSON.parse(content);
1238
- throw new Error("响应无效");
1255
+ this.logger.info("原始响应:", content);
1256
+ try {
1257
+ const jsonRegex = /```json\s*([\s\S]*?)\s*```/;
1258
+ const match = content?.match(jsonRegex);
1259
+ let jsonString = "";
1260
+ if (match && match[1]) {
1261
+ jsonString = match[1];
1262
+ } else {
1263
+ jsonString = content;
1264
+ }
1265
+ return JSON.parse(jsonString);
1266
+ } catch (error) {
1267
+ this.logger.error("解析 JSON 失败:", error);
1268
+ this.logger.error("原始响应:", content);
1269
+ throw new Error("解析失败");
1270
+ }
1239
1271
  }
1240
1272
  };
1241
1273
 
@@ -1273,63 +1305,8 @@ var Config = import_koishi3.Schema.intersect([
1273
1305
  enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1274
1306
  aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
1275
1307
  aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1276
- aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-1.5-flash"),
1277
- aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
1278
- AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我以JSON格式提供的一组内容(每项包含ID、文本和图片),为每一项内容总结关键词、概括内容并评分。你需要返回一个包含所有分析结果的JSON对象。`).description("分析 Prompt"),
1279
- aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
1280
- `{
1281
- "type": "object",
1282
- "properties": {
1283
- "analyses": {
1284
- "type": "array",
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
- }
1311
- }
1312
- },
1313
- "required": ["analyses"]
1314
- }`
1315
- ).description("分析 JSON Schema"),
1316
- aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
1317
- aiCheckSchema: import_koishi3.Schema.string().role("textarea").default(
1318
- `{
1319
- "type": "object",
1320
- "properties": {
1321
- "duplicate": {
1322
- "type": "boolean",
1323
- "description": "新内容是否与已有内容重复"
1324
- },
1325
- "id": {
1326
- "type": "integer",
1327
- "description": "如果重复,此为第一个重复的已有内容的ID"
1328
- }
1329
- },
1330
- "required": ["duplicate"]
1331
- }`
1332
- ).description("查重 JSON Schema")
1308
+ aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
1309
+ aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60)
1333
1310
  }).description("模型配置"),
1334
1311
  import_koishi3.Schema.object({
1335
1312
  localPath: import_koishi3.Schema.string().description("文件映射路径"),
@@ -1428,7 +1405,7 @@ function apply(ctx, config) {
1428
1405
  }
1429
1406
  if (aiManager) {
1430
1407
  const duplicateResult = await aiManager.checkForDuplicates(finalElementsForDb, downloadedMedia);
1431
- if (duplicateResult && duplicateResult.duplicate) return `内容与回声洞(${duplicateResult.id})重复`;
1408
+ if (duplicateResult?.duplicate && duplicateResult.ids?.length > 0) return `内容与回声洞(${duplicateResult.ids.join("|")})重复`;
1432
1409
  }
1433
1410
  const userName = (config.enableName ? await profileManager.getNickname(session.userId) : null) || session.username;
1434
1411
  const needsReview = config.enablePend && session.cid !== config.adminChannel;
@@ -1445,7 +1422,7 @@ function apply(ctx, config) {
1445
1422
  if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, needsReview);
1446
1423
  if (finalStatus !== "preload") {
1447
1424
  newCave.status = finalStatus;
1448
- if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
1425
+ if (aiManager) await aiManager.analyzeAndStore([newCave], downloadedMedia);
1449
1426
  if (hashManager) {
1450
1427
  const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: newCave.id }));
1451
1428
  if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
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.12",
4
+ "version": "2.7.14",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],