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.
- package/lib/AIManager.d.ts +1 -0
- package/lib/index.d.ts +7 -3
- package/lib/index.js +67 -43
- package/package.json +3 -3
package/lib/AIManager.d.ts
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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:
|
|
1377
|
+
model: modelName,
|
|
1361
1378
|
messages: [{ role: "system", content: systemPrompt }, ...messages]
|
|
1362
1379
|
};
|
|
1363
|
-
const fullUrl = `${
|
|
1380
|
+
const fullUrl = `${endpointConfig.url.replace(/\/$/, "")}/chat/completions`;
|
|
1364
1381
|
const headers = {
|
|
1365
1382
|
"Content-Type": "application/json",
|
|
1366
|
-
"Authorization": `Bearer ${
|
|
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)])
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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.
|
|
4
|
+
"version": "2.7.30",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Yis_Rime <yis_rime@outlook.com>"
|
|
7
7
|
],
|
|
8
|
-
"homepage": "https://github.com/
|
|
8
|
+
"homepage": "https://github.com//Koishi-Plugin/best-cave",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
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",
|