koishi-plugin-best-cave 2.7.4 → 2.7.5
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 +32 -57
- package/lib/index.js +102 -154
- package/package.json +1 -1
package/lib/AIManager.d.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
21
|
-
* 通过与外部 AI 服务接口交互,实现对回声洞内容的深度分析和重复性检查。
|
|
25
|
+
* @description AI 管理器,是连接 AI 服务与回声洞功能的核心模块。
|
|
22
26
|
*/
|
|
23
27
|
export declare class AIManager {
|
|
24
28
|
private ctx;
|
|
@@ -28,23 +32,20 @@ export declare class AIManager {
|
|
|
28
32
|
private http;
|
|
29
33
|
/**
|
|
30
34
|
* @constructor
|
|
31
|
-
* @
|
|
32
|
-
* @param {Config} config - 插件的配置信息。
|
|
33
|
-
* @param {Logger} logger - 日志记录器实例。
|
|
34
|
-
* @param {FileManager} fileManager - 文件管理器实例。
|
|
35
|
+
* @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
|
|
35
36
|
*/
|
|
36
37
|
constructor(ctx: Context, config: Config, logger: Logger, fileManager: FileManager);
|
|
37
38
|
/**
|
|
38
|
-
* @description
|
|
39
|
-
* @param {any} cave - 主 `cave`
|
|
39
|
+
* @description 注册所有与 AIManager 功能相关的 Koishi 命令。
|
|
40
|
+
* @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
|
|
40
41
|
*/
|
|
41
42
|
registerCommands(cave: any): void;
|
|
42
43
|
/**
|
|
43
|
-
* @description
|
|
44
|
-
* @param {StoredElement[]} newElements -
|
|
45
|
-
* @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave -
|
|
46
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
47
|
-
* @returns {Promise<{ duplicate: boolean; id?: number }>}
|
|
44
|
+
* @description 对新提交的内容执行 AI 驱动的查重检查。
|
|
45
|
+
* @param {StoredElement[]} newElements - 待检查的新内容的结构化数组(包含文本、图片等)。
|
|
46
|
+
* @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 伴随新内容提交的、需要从 URL 下载的媒体文件列表。
|
|
47
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已经加载到内存中的媒体文件 Buffer,可用于优化性能。
|
|
48
|
+
* @returns {Promise<{ duplicate: boolean; id?: number }>} 一个包含查重结果的对象。
|
|
48
49
|
*/
|
|
49
50
|
checkForDuplicates(newElements: StoredElement[], newMediaToSave: {
|
|
50
51
|
sourceUrl: string;
|
|
@@ -57,57 +58,31 @@ export declare class AIManager {
|
|
|
57
58
|
id?: number;
|
|
58
59
|
}>;
|
|
59
60
|
/**
|
|
60
|
-
* @description
|
|
61
|
-
* @param {CaveObject} cave -
|
|
62
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
63
|
-
* @returns {Promise<void>}
|
|
61
|
+
* @description 对单个回声洞对象执行完整的分析和存储流程。
|
|
62
|
+
* @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
|
|
63
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
|
|
64
|
+
* @returns {Promise<void>} 操作完成后 resolve 的 Promise。
|
|
65
|
+
* @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
|
|
64
66
|
*/
|
|
65
67
|
analyzeAndStore(cave: CaveObject, mediaBuffers?: {
|
|
66
68
|
fileName: string;
|
|
67
69
|
buffer: Buffer;
|
|
68
70
|
}[]): Promise<void>;
|
|
69
71
|
/**
|
|
70
|
-
* @description
|
|
71
|
-
* @param {StoredElement[]} elements -
|
|
72
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] -
|
|
73
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
74
|
-
* @returns {Promise<Omit<CaveMetaObject, 'cave'>>}
|
|
72
|
+
* @description 准备并发送内容给 AI 模型以获取分析结果。
|
|
73
|
+
* @param {StoredElement[]} elements - 内容的结构化元素数组。
|
|
74
|
+
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选)需要从网络下载的媒体文件信息。
|
|
75
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已存在于内存中的媒体文件 Buffer。
|
|
76
|
+
* @returns {Promise<Omit<CaveMetaObject, 'cave'>>} 返回一个不含 `cave` 字段的分析结果对象。如果内容为空或无法处理,则返回 `null`。
|
|
75
77
|
*/
|
|
76
78
|
private getAnalysis;
|
|
77
79
|
/**
|
|
78
|
-
* @description
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
81
|
-
* @
|
|
80
|
+
* @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
|
|
81
|
+
* @param {any[]} messages - 要发送给 AI 的消息数组,格式遵循 OpenAI API 规范。
|
|
82
|
+
* @param {string} systemPrompt - 指导 AI 行为的系统级提示词。
|
|
83
|
+
* @param {string} schemaString - 一个 JSON 字符串,定义了期望 AI 返回的 JSON 对象的结构。
|
|
84
|
+
* @returns {Promise<any>} AI 返回的、经过 JSON 解析的响应体。
|
|
85
|
+
* @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
|
|
82
86
|
*/
|
|
83
|
-
private
|
|
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;
|
|
87
|
+
private requestAI;
|
|
113
88
|
}
|
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
|
-
* @
|
|
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;
|
|
@@ -1003,30 +1000,37 @@ var AIManager = class {
|
|
|
1003
1000
|
}
|
|
1004
1001
|
http;
|
|
1005
1002
|
/**
|
|
1006
|
-
* @description
|
|
1007
|
-
* @param {any} cave - 主 `cave`
|
|
1003
|
+
* @description 注册所有与 AIManager 功能相关的 Koishi 命令。
|
|
1004
|
+
* @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
|
|
1008
1005
|
*/
|
|
1009
1006
|
registerCommands(cave) {
|
|
1010
1007
|
cave.subcommand(".ai", "分析回声洞", { hidden: true, authority: 4 }).usage("分析尚未分析的回声洞,补全回声洞记录。").action(async ({ session }) => {
|
|
1011
|
-
|
|
1012
|
-
if (adminError) return adminError;
|
|
1008
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
1013
1009
|
try {
|
|
1014
1010
|
const allCaves = await this.ctx.database.get("cave", { status: "active" });
|
|
1015
1011
|
const analyzedCaveIds = new Set((await this.ctx.database.get("cave_meta", {})).map((meta) => meta.cave));
|
|
1016
1012
|
const cavesToAnalyze = allCaves.filter((cave2) => !analyzedCaveIds.has(cave2.id));
|
|
1017
1013
|
if (cavesToAnalyze.length === 0) return "无需分析回声洞";
|
|
1018
1014
|
await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
|
|
1019
|
-
let
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1015
|
+
let successCount = 0;
|
|
1016
|
+
const batchSize = 100;
|
|
1017
|
+
for (let i = 0; i < cavesToAnalyze.length; i += batchSize) {
|
|
1018
|
+
const batch = cavesToAnalyze.slice(i, i + batchSize);
|
|
1019
|
+
this.logger.info(`[${i}/${cavesToAnalyze.length}] 正在分析 ${batch.length} 条回声洞...`);
|
|
1020
|
+
for (const cave2 of batch) {
|
|
1021
|
+
try {
|
|
1022
|
+
await this.analyzeAndStore(cave2);
|
|
1023
|
+
successCount++;
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
|
|
1026
|
+
return `分析回声洞(${cave2.id})时出错: ${error.message}`;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1025
1029
|
}
|
|
1026
|
-
return `已分析 ${
|
|
1030
|
+
return `已分析 ${successCount} 个回声洞`;
|
|
1027
1031
|
} catch (error) {
|
|
1028
|
-
this.logger.error("
|
|
1029
|
-
return
|
|
1032
|
+
this.logger.error("分析回声洞失败:", error);
|
|
1033
|
+
return `操作失败: ${error.message}`;
|
|
1030
1034
|
}
|
|
1031
1035
|
});
|
|
1032
1036
|
cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
|
|
@@ -1034,12 +1038,11 @@ var AIManager = class {
|
|
|
1034
1038
|
try {
|
|
1035
1039
|
const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
|
|
1036
1040
|
if (!meta) return `回声洞(${id})尚未分析`;
|
|
1037
|
-
const keywordsText = meta.keywords.join(", ");
|
|
1038
1041
|
const report = [
|
|
1039
1042
|
`回声洞(${id})分析结果:`,
|
|
1040
1043
|
`描述:${meta.description}`,
|
|
1041
|
-
`关键词:${
|
|
1042
|
-
|
|
1044
|
+
`关键词:${meta.keywords.join(", ")}`,
|
|
1045
|
+
`综合评分:${meta.rating}/100`
|
|
1043
1046
|
];
|
|
1044
1047
|
return import_koishi3.h.text(report.join("\n"));
|
|
1045
1048
|
} catch (error) {
|
|
@@ -1049,90 +1052,83 @@ var AIManager = class {
|
|
|
1049
1052
|
});
|
|
1050
1053
|
}
|
|
1051
1054
|
/**
|
|
1052
|
-
* @description
|
|
1053
|
-
* @param {StoredElement[]} newElements -
|
|
1054
|
-
* @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave -
|
|
1055
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1056
|
-
* @returns {Promise<{ duplicate: boolean; id?: number }>}
|
|
1055
|
+
* @description 对新提交的内容执行 AI 驱动的查重检查。
|
|
1056
|
+
* @param {StoredElement[]} newElements - 待检查的新内容的结构化数组(包含文本、图片等)。
|
|
1057
|
+
* @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave - 伴随新内容提交的、需要从 URL 下载的媒体文件列表。
|
|
1058
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已经加载到内存中的媒体文件 Buffer,可用于优化性能。
|
|
1059
|
+
* @returns {Promise<{ duplicate: boolean; id?: number }>} 一个包含查重结果的对象。
|
|
1057
1060
|
*/
|
|
1058
1061
|
async checkForDuplicates(newElements, newMediaToSave, mediaBuffers) {
|
|
1059
1062
|
try {
|
|
1060
1063
|
const newAnalysis = await this.getAnalysis(newElements, newMediaToSave, mediaBuffers);
|
|
1061
1064
|
if (!newAnalysis || newAnalysis.keywords.length === 0) return { duplicate: false };
|
|
1062
|
-
const newKeywords = new Set(newAnalysis.keywords);
|
|
1063
1065
|
const allMeta = await this.ctx.database.get("cave_meta", {});
|
|
1064
|
-
const potentialDuplicates =
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1066
|
+
const potentialDuplicates = (await Promise.all(allMeta.map(async (meta) => {
|
|
1067
|
+
const setA = new Set(newAnalysis.keywords);
|
|
1068
|
+
const setB = new Set(meta.keywords);
|
|
1069
|
+
let similarity = 0;
|
|
1070
|
+
if (setA.size > 0 && setB.size > 0) {
|
|
1071
|
+
const intersection = new Set([...setA].filter((x) => setB.has(x)));
|
|
1072
|
+
const union = /* @__PURE__ */ new Set([...setA, ...setB]);
|
|
1073
|
+
similarity = intersection.size / union.size;
|
|
1074
|
+
}
|
|
1068
1075
|
if (similarity * 100 >= 80) {
|
|
1069
1076
|
const [cave] = await this.ctx.database.get("cave", { id: meta.cave });
|
|
1070
|
-
|
|
1077
|
+
return cave;
|
|
1071
1078
|
}
|
|
1072
|
-
}
|
|
1079
|
+
}))).filter(Boolean);
|
|
1073
1080
|
if (potentialDuplicates.length === 0) return { duplicate: false };
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1081
|
+
const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
|
|
1082
|
+
const userMessage = {
|
|
1083
|
+
role: "user",
|
|
1084
|
+
content: JSON.stringify({
|
|
1085
|
+
new_content: { text: formatContent(newElements) },
|
|
1086
|
+
existing_contents: potentialDuplicates.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
|
|
1087
|
+
})
|
|
1088
|
+
};
|
|
1089
|
+
const response = await this.requestAI([userMessage], this.config.aiCheckPrompt, this.config.aiCheckSchema);
|
|
1090
|
+
return {
|
|
1091
|
+
duplicate: response.duplicate || false,
|
|
1092
|
+
id: response.id ? Number(response.id) : void 0
|
|
1093
|
+
};
|
|
1078
1094
|
} catch (error) {
|
|
1079
1095
|
this.logger.error("查重回声洞出错:", error);
|
|
1080
1096
|
return { duplicate: false };
|
|
1081
1097
|
}
|
|
1082
1098
|
}
|
|
1083
1099
|
/**
|
|
1084
|
-
* @description
|
|
1085
|
-
* @param {CaveObject} cave -
|
|
1086
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1087
|
-
* @returns {Promise<void>}
|
|
1100
|
+
* @description 对单个回声洞对象执行完整的分析和存储流程。
|
|
1101
|
+
* @param {CaveObject} cave - 需要被分析的完整回声洞对象,包含 `id` 和 `elements`。
|
|
1102
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)与该回声洞相关的、已加载到内存的媒体文件 Buffer。
|
|
1103
|
+
* @returns {Promise<void>} 操作完成后 resolve 的 Promise。
|
|
1104
|
+
* @throws {Error} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
|
|
1088
1105
|
*/
|
|
1089
1106
|
async analyzeAndStore(cave, mediaBuffers) {
|
|
1090
1107
|
try {
|
|
1091
|
-
const
|
|
1092
|
-
if (
|
|
1108
|
+
const result = await this.getAnalysis(cave.elements, void 0, mediaBuffers);
|
|
1109
|
+
if (result) {
|
|
1110
|
+
await this.ctx.database.upsert("cave_meta", [{
|
|
1111
|
+
cave: cave.id,
|
|
1112
|
+
...result,
|
|
1113
|
+
rating: Math.max(0, Math.min(100, result.rating || 0))
|
|
1114
|
+
}]);
|
|
1115
|
+
}
|
|
1093
1116
|
} catch (error) {
|
|
1094
1117
|
this.logger.error(`分析回声洞(${cave.id})失败:`, error);
|
|
1095
1118
|
throw error;
|
|
1096
1119
|
}
|
|
1097
1120
|
}
|
|
1098
1121
|
/**
|
|
1099
|
-
* @description
|
|
1100
|
-
* @param {StoredElement[]} elements -
|
|
1101
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] -
|
|
1102
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1103
|
-
* @returns {Promise<Omit<CaveMetaObject, 'cave'>>}
|
|
1122
|
+
* @description 准备并发送内容给 AI 模型以获取分析结果。
|
|
1123
|
+
* @param {StoredElement[]} elements - 内容的结构化元素数组。
|
|
1124
|
+
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选)需要从网络下载的媒体文件信息。
|
|
1125
|
+
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选)已存在于内存中的媒体文件 Buffer。
|
|
1126
|
+
* @returns {Promise<Omit<CaveMetaObject, 'cave'>>} 返回一个不含 `cave` 字段的分析结果对象。如果内容为空或无法处理,则返回 `null`。
|
|
1104
1127
|
*/
|
|
1105
1128
|
async getAnalysis(elements, mediaToSave, mediaBuffers) {
|
|
1106
|
-
const
|
|
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 }];
|
|
1129
|
+
const userContent = [];
|
|
1134
1130
|
const combinedText = elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
|
|
1135
|
-
if (combinedText)
|
|
1131
|
+
if (combinedText.trim()) userContent.push({ type: "text", text: combinedText });
|
|
1136
1132
|
const mediaMap = new Map(mediaBuffers?.map((m) => [m.fileName, m.buffer]));
|
|
1137
1133
|
const imageElements = elements.filter((el) => el.type === "image" && el.file);
|
|
1138
1134
|
for (const el of imageElements) {
|
|
@@ -1148,93 +1144,45 @@ var AIManager = class {
|
|
|
1148
1144
|
}
|
|
1149
1145
|
if (buffer) {
|
|
1150
1146
|
const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
|
|
1151
|
-
|
|
1147
|
+
userContent.push({
|
|
1148
|
+
type: "image_url",
|
|
1149
|
+
image_url: { url: `data:${mimeType};base64,${buffer.toString("base64")}` }
|
|
1150
|
+
});
|
|
1152
1151
|
}
|
|
1153
1152
|
} catch (error) {
|
|
1154
1153
|
this.logger.warn(`分析内容(${el.file})失败:`, error);
|
|
1155
1154
|
}
|
|
1156
1155
|
}
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
}
|
|
1156
|
+
if (userContent.length === 0) return null;
|
|
1157
|
+
const userMessage = { role: "user", content: userContent };
|
|
1158
|
+
return await this.requestAI([userMessage], this.config.AnalysePrompt, this.config.aiAnalyseSchema);
|
|
1173
1159
|
}
|
|
1174
1160
|
/**
|
|
1175
|
-
* @description
|
|
1176
|
-
* @param {
|
|
1177
|
-
* @param {
|
|
1178
|
-
* @
|
|
1161
|
+
* @description 封装了向 OpenAI 兼容的 API 发送请求的底层逻辑。
|
|
1162
|
+
* @param {any[]} messages - 要发送给 AI 的消息数组,格式遵循 OpenAI API 规范。
|
|
1163
|
+
* @param {string} systemPrompt - 指导 AI 行为的系统级提示词。
|
|
1164
|
+
* @param {string} schemaString - 一个 JSON 字符串,定义了期望 AI 返回的 JSON 对象的结构。
|
|
1165
|
+
* @returns {Promise<any>} AI 返回的、经过 JSON 解析的响应体。
|
|
1166
|
+
* @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
|
|
1179
1167
|
*/
|
|
1180
|
-
async
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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: {} };
|
|
1201
|
-
}
|
|
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;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* @description 解析 AI 返回的查重响应。
|
|
1226
|
-
* @param {any} response - AI 服务的原始响应对象。
|
|
1227
|
-
* @returns {{ duplicate: boolean; id?: number }} - 返回查重结果。
|
|
1228
|
-
*/
|
|
1229
|
-
parseDedupeResponse(response) {
|
|
1168
|
+
async requestAI(messages, systemPrompt, schemaString) {
|
|
1169
|
+
let schema = JSON.parse(schemaString);
|
|
1170
|
+
const payload = {
|
|
1171
|
+
model: this.config.aiModel,
|
|
1172
|
+
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
1173
|
+
response_format: { type: "json_schema", strict: true, schema }
|
|
1174
|
+
};
|
|
1175
|
+
const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
|
|
1176
|
+
const headers = {
|
|
1177
|
+
"Content-Type": "application/json",
|
|
1178
|
+
"Authorization": `Bearer ${this.config.aiApiKey}`
|
|
1179
|
+
};
|
|
1230
1180
|
try {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const parsed = JSON.parse(content);
|
|
1234
|
-
if (parsed.duplicate === true && parsed.id) return { duplicate: true, id: Number(parsed.id) };
|
|
1235
|
-
return { duplicate: false };
|
|
1181
|
+
const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
|
|
1182
|
+
return JSON.parse(response.choices[0].message.content);
|
|
1236
1183
|
} catch (error) {
|
|
1237
|
-
|
|
1184
|
+
const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
|
|
1185
|
+
this.logger.error(`请求 API 失败: ${errorMessage}`);
|
|
1238
1186
|
throw error;
|
|
1239
1187
|
}
|
|
1240
1188
|
}
|
|
@@ -1272,7 +1220,7 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1272
1220
|
}).description("复核配置"),
|
|
1273
1221
|
import_koishi4.Schema.object({
|
|
1274
1222
|
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"),
|
|
1223
|
+
aiEndpoint: import_koishi4.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
|
|
1276
1224
|
aiApiKey: import_koishi4.Schema.string().description("密钥 (Key)").role("secret"),
|
|
1277
1225
|
aiModel: import_koishi4.Schema.string().description("模型").default("gemini-2.5-flash"),
|
|
1278
1226
|
AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析提示词 (Prompt)"),
|