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.
- package/lib/AIManager.d.ts +34 -57
- package/lib/index.d.ts +1 -0
- package/lib/index.js +117 -157
- 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;
|
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
|
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 }>}
|
|
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] -
|
|
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
|
|
71
|
-
* @param {StoredElement[]} elements -
|
|
72
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] -
|
|
73
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
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
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
81
|
-
* @
|
|
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
|
|
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
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;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
this.logger.info(`[${
|
|
1023
|
-
|
|
1024
|
-
|
|
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 `已分析 ${
|
|
1029
|
+
return `已分析 ${successCount} 个回声洞(失败 ${failedCount} 个)`;
|
|
1027
1030
|
} catch (error) {
|
|
1028
|
-
this.logger.error("
|
|
1029
|
-
return
|
|
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
|
-
`关键词:${
|
|
1042
|
-
|
|
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
|
|
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 }>}
|
|
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
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
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
|
-
|
|
1076
|
+
return cave;
|
|
1071
1077
|
}
|
|
1072
|
-
}
|
|
1078
|
+
}))).filter(Boolean);
|
|
1073
1079
|
if (potentialDuplicates.length === 0) return { duplicate: false };
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
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] -
|
|
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
|
|
1092
|
-
if (
|
|
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
|
|
1100
|
-
* @param {StoredElement[]} elements -
|
|
1101
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] -
|
|
1102
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
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
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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
|
|
1176
|
-
* @param {
|
|
1177
|
-
* @param {
|
|
1178
|
-
* @
|
|
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
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1232
|
-
const
|
|
1233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
1302
|
-
aiCheckPrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("
|
|
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("
|
|
1278
|
+
).description("查重 JSON Schema")
|
|
1319
1279
|
}).description("模型配置"),
|
|
1320
1280
|
import_koishi4.Schema.object({
|
|
1321
1281
|
localPath: import_koishi4.Schema.string().description("文件映射路径"),
|