koishi-plugin-best-cave 2.7.28 → 2.7.30

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.
@@ -84,6 +84,7 @@ export declare class AIManager {
84
84
  /**
85
85
  * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
86
86
  * @template T - 期望从 AI 响应的 JSON 中解析出的数据类型。
87
+ * @param {string} modelIdentifier - 模型的完整标识符,格式为 `端点名称/模型名称`。
87
88
  * @param {any[]} messages - 发送给 AI 的消息数组,通常包含用户消息。
88
89
  * @param {string} systemPrompt - 指导 AI 行为的系统级指令。
89
90
  * @returns {Promise<T>} 一个 Promise,解析为从 AI 响应中提取并解析的 JSON 对象。
package/lib/index.d.ts CHANGED
@@ -58,9 +58,13 @@ export interface Config {
58
58
  bucket?: string;
59
59
  publicUrl?: string;
60
60
  enableAI: boolean;
61
- aiEndpoint?: string;
62
- aiApiKey?: string;
63
- aiModel?: string;
61
+ endpoints?: {
62
+ name: string;
63
+ url: string;
64
+ key: string;
65
+ }[];
66
+ analysisModel?: string;
67
+ duplicateCheckModel?: string;
64
68
  }
65
69
  export declare const Config: Schema<Config>;
66
70
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -1270,40 +1270,45 @@ var AIManager = class {
1270
1270
  async analyze(caves, mediaBuffers) {
1271
1271
  const mediaMap = mediaBuffers ? new Map(mediaBuffers.map((m) => [m.fileName, m.buffer])) : void 0;
1272
1272
  const analysisPromises = caves.map(async (cave) => {
1273
- const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
1274
- const imageElements = await Promise.all(
1275
- cave.elements.filter((el) => el.type === "image" && el.file).map(async (el) => {
1276
- try {
1277
- const buffer = mediaMap?.get(el.file) ?? await this.fileManager.readFile(el.file);
1278
- const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
1279
- return {
1280
- type: "image_url",
1281
- image_url: { url: `data:${mimeType};base64,${buffer.toString("base64")}` }
1282
- };
1283
- } catch (error) {
1284
- this.logger.warn(`读取文件(${el.file})失败:`, error);
1285
- return null;
1286
- }
1287
- })
1288
- );
1289
- const images = imageElements.filter(Boolean);
1290
- if (!combinedText.trim() && images.length === 0) return null;
1291
- const contentForAI = [];
1292
- if (combinedText.trim()) contentForAI.push({ type: "text", text: `请分析以下内容:
1273
+ try {
1274
+ const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
1275
+ const imageElements = await Promise.all(
1276
+ cave.elements.filter((el) => el.type === "image" && el.file).map(async (el) => {
1277
+ try {
1278
+ const buffer = mediaMap?.get(el.file) ?? await this.fileManager.readFile(el.file);
1279
+ const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
1280
+ return {
1281
+ type: "image_url",
1282
+ image_url: { url: `data:${mimeType};base64,${buffer.toString("base64")}` }
1283
+ };
1284
+ } catch (error) {
1285
+ this.logger.warn(`读取文件(${el.file})失败:`, error);
1286
+ return null;
1287
+ }
1288
+ })
1289
+ );
1290
+ const images = imageElements.filter(Boolean);
1291
+ if (!combinedText.trim() && images.length === 0) return null;
1292
+ const contentForAI = [];
1293
+ if (combinedText.trim()) contentForAI.push({ type: "text", text: `请分析以下内容:
1293
1294
 
1294
1295
  ${combinedText}` });
1295
- contentForAI.push(...images);
1296
- const userMessage = { role: "user", content: contentForAI };
1297
- const response = await this.requestAI([userMessage], this.ANALYSIS_SYSTEM_PROMPT);
1298
- if (response) {
1299
- return {
1300
- cave: cave.id,
1301
- keywords: response.keywords || [],
1302
- description: response.description || "",
1303
- rating: Math.max(0, Math.min(100, response.rating || 0))
1304
- };
1296
+ contentForAI.push(...images);
1297
+ const userMessage = { role: "user", content: contentForAI };
1298
+ const response = await this.requestAI(this.config.analysisModel, [userMessage], this.ANALYSIS_SYSTEM_PROMPT);
1299
+ if (response) {
1300
+ return {
1301
+ cave: cave.id,
1302
+ keywords: response.keywords || [],
1303
+ description: response.description || "",
1304
+ rating: Math.max(0, Math.min(100, response.rating || 0))
1305
+ };
1306
+ }
1307
+ return null;
1308
+ } catch (error) {
1309
+ this.logger.error(`分析回声洞(${cave.id})失败:`, error);
1310
+ return null;
1305
1311
  }
1306
- return null;
1307
1312
  });
1308
1313
  const results = await Promise.all(analysisPromises);
1309
1314
  return results.filter((result) => !!result);
@@ -1323,7 +1328,7 @@ ${combinedText}` });
1323
1328
  content_b: { id: caveB.id, text: formatContent(caveB.elements) }
1324
1329
  };
1325
1330
  const userMessage = { role: "user", content: JSON.stringify(userMessageContent) };
1326
- const response = await this.requestAI([userMessage], this.DUPLICATE_CHECK_SYSTEM_PROMPT);
1331
+ const response = await this.requestAI(this.config.duplicateCheckModel, [userMessage], this.DUPLICATE_CHECK_SYSTEM_PROMPT);
1327
1332
  return response?.duplicate || false;
1328
1333
  } catch (error) {
1329
1334
  this.logger.error(`比较回声洞(${caveA.id})与(${caveB.id})失败:`, error);
@@ -1349,25 +1354,37 @@ ${combinedText}` });
1349
1354
  /**
1350
1355
  * @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
1351
1356
  * @template T - 期望从 AI 响应的 JSON 中解析出的数据类型。
1357
+ * @param {string} modelIdentifier - 模型的完整标识符,格式为 `端点名称/模型名称`。
1352
1358
  * @param {any[]} messages - 发送给 AI 的消息数组,通常包含用户消息。
1353
1359
  * @param {string} systemPrompt - 指导 AI 行为的系统级指令。
1354
1360
  * @returns {Promise<T>} 一个 Promise,解析为从 AI 响应中提取并解析的 JSON 对象。
1355
1361
  * @throws {Error} 当网络请求失败、AI 未返回有效内容或 JSON 解析失败时抛出。
1356
1362
  * @private
1357
1363
  */
1358
- async requestAI(messages, systemPrompt) {
1364
+ async requestAI(modelIdentifier, messages, systemPrompt) {
1365
+ if (!modelIdentifier || !modelIdentifier.includes("/")) {
1366
+ throw new Error(`无效的模型标识符: ${modelIdentifier}。格式应为 '端点名称/模型名称'`);
1367
+ }
1368
+ const [name2, modelName] = modelIdentifier.split("/");
1369
+ if (!name2 || !modelName) {
1370
+ throw new Error(`无效的模型标识符: ${modelIdentifier}。格式应为 '端点名称/模型名称'`);
1371
+ }
1372
+ const endpointConfig = this.config.endpoints?.find((e) => e.name === name2);
1373
+ if (!endpointConfig) {
1374
+ throw new Error(`未在配置中找到名为 '${name2}' 的端点`);
1375
+ }
1359
1376
  const payload = {
1360
- model: this.config.aiModel,
1377
+ model: modelName,
1361
1378
  messages: [{ role: "system", content: systemPrompt }, ...messages]
1362
1379
  };
1363
- const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
1380
+ const fullUrl = `${endpointConfig.url.replace(/\/$/, "")}/chat/completions`;
1364
1381
  const headers = {
1365
1382
  "Content-Type": "application/json",
1366
- "Authorization": `Bearer ${this.config.aiApiKey}`
1383
+ "Authorization": `Bearer ${endpointConfig.key}`
1367
1384
  };
1368
1385
  const response = await this.http.post(fullUrl, payload, { headers, timeout: 6e5 });
1369
1386
  const content = response?.choices?.[0]?.message?.content;
1370
- if (!content?.trim()) throw new Error();
1387
+ if (!content?.trim()) throw new Error("AI 响应内容为空");
1371
1388
  const candidates = [];
1372
1389
  const jsonBlockMatch = content.match(/```json\s*([\s\S]*?)\s*```/i);
1373
1390
  if (jsonBlockMatch && jsonBlockMatch[1]) candidates.push(jsonBlockMatch[1]);
@@ -1375,11 +1392,14 @@ ${combinedText}` });
1375
1392
  const firstBrace = content.indexOf("{");
1376
1393
  const lastBrace = content.lastIndexOf("}");
1377
1394
  if (firstBrace !== -1 && lastBrace > firstBrace) candidates.push(content.substring(firstBrace, lastBrace + 1));
1378
- for (const candidate of [...new Set(candidates)]) try {
1379
- return JSON.parse(candidate);
1380
- } catch (parseError) {
1395
+ for (const candidate of [...new Set(candidates)]) {
1396
+ try {
1397
+ return JSON.parse(candidate);
1398
+ } catch (parseError) {
1399
+ }
1381
1400
  }
1382
1401
  this.logger.error("原始响应:", JSON.stringify(response, null, 2));
1402
+ throw new Error("无法从 AI 响应中解析出有效的 JSON");
1383
1403
  }
1384
1404
  };
1385
1405
 
@@ -1415,9 +1435,13 @@ var Config = import_koishi3.Schema.intersect([
1415
1435
  }).description("复核配置"),
1416
1436
  import_koishi3.Schema.object({
1417
1437
  enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
1418
- aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://api.siliconflow.cn/v1"),
1419
- aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
1420
- aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("THUDM/GLM-4.1V-9B-Thinking")
1438
+ endpoints: import_koishi3.Schema.array(import_koishi3.Schema.object({
1439
+ name: import_koishi3.Schema.string().description("名称").required(),
1440
+ url: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").required(),
1441
+ key: import_koishi3.Schema.string().description("密钥 (API Key)").role("secret").required()
1442
+ })).description("端点列表"),
1443
+ analysisModel: import_koishi3.Schema.string().description("分析模型"),
1444
+ duplicateCheckModel: import_koishi3.Schema.string().description("查重模型")
1421
1445
  }).description("模型配置"),
1422
1446
  import_koishi3.Schema.object({
1423
1447
  localPath: import_koishi3.Schema.string().description("文件映射路径"),
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "功能强大、高度可定制的回声洞插件。支持丰富的媒体类型、内容查重、AI分析、人工审核、用户昵称、数据迁移以及本地/S3 双重文件存储后端。",
4
- "version": "2.7.28",
4
+ "version": "2.7.30",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
8
- "homepage": "https://github.com/YisRime/koishi-plugin-best-cave",
8
+ "homepage": "https://github.com//Koishi-Plugin/best-cave",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/YisRime/koishi-plugin-best-cave.git"
11
+ "url": "git+https://github.com//Koishi-Plugin/best-cave.git"
12
12
  },
13
13
  "main": "lib/index.js",
14
14
  "typings": "lib/index.d.ts",