koishi-plugin-best-cave 2.7.3 → 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/Utils.d.ts +5 -11
- package/lib/index.js +117 -158
- 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/Utils.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ import { Context, h, Logger, Session } from 'koishi';
|
|
|
2
2
|
import { CaveObject, Config, StoredElement } from './index';
|
|
3
3
|
import { FileManager } from './FileManager';
|
|
4
4
|
import { HashManager, CaveHashObject } from './HashManager';
|
|
5
|
-
import { PendManager } from './PendManager';
|
|
6
|
-
import { AIManager } from './AIManager';
|
|
7
5
|
/**
|
|
8
6
|
* @description 构建一条用于发送的完整回声洞消息,处理不同存储后端的资源链接。
|
|
9
7
|
* @param cave 回声洞对象。
|
|
@@ -69,25 +67,21 @@ export declare function performSimilarityChecks(ctx: Context, config: Config, ha
|
|
|
69
67
|
imageHashesToStore?: Omit<CaveHashObject, 'cave'>[];
|
|
70
68
|
}>;
|
|
71
69
|
/**
|
|
72
|
-
* @description
|
|
70
|
+
* @description 异步处理文件上传,并在成功后更新回声洞状态。
|
|
73
71
|
* @param ctx - Koishi 上下文。
|
|
74
72
|
* @param config - 插件配置。
|
|
75
73
|
* @param fileManager - FileManager 实例,用于保存文件。
|
|
76
74
|
* @param logger - 日志记录器实例。
|
|
77
|
-
* @param reviewManager - ReviewManager 实例,用于提交审核。
|
|
78
75
|
* @param cave - 刚刚在数据库中创建的 `preload` 状态的回声洞对象。
|
|
79
76
|
* @param downloadedMedia - 需要保存的媒体文件及其 Buffer。
|
|
80
77
|
* @param reusableIds - 可复用 ID 的内存缓存。
|
|
81
|
-
* @param session -
|
|
82
|
-
* @
|
|
83
|
-
* @param textHashesToStore - 已预先计算好的、待存入数据库的文本哈希对象数组。
|
|
84
|
-
* @param imageHashesToStore - 已预先计算好的、待存入数据库的图片哈希对象数组。
|
|
85
|
-
* @param aiManager - AIManager 实例,如果启用则用于 AI 分析。
|
|
78
|
+
* @param session - 触发此操作的用户会话。
|
|
79
|
+
* @returns 成功则返回最终状态 ('pending' or 'active'),失败则返回 'delete'。
|
|
86
80
|
*/
|
|
87
|
-
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger,
|
|
81
|
+
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, cave: CaveObject, downloadedMedia: {
|
|
88
82
|
fileName: string;
|
|
89
83
|
buffer: Buffer;
|
|
90
|
-
}[], reusableIds: Set<number>, session: Session
|
|
84
|
+
}[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active' | 'delete'>;
|
|
91
85
|
/**
|
|
92
86
|
* @description 校验会话是否来自指定的管理群组。
|
|
93
87
|
* @param session 当前会话。
|
package/lib/index.js
CHANGED
|
@@ -482,25 +482,18 @@ async function performSimilarityChecks(ctx, config, hashManager, finalElementsFo
|
|
|
482
482
|
return { duplicate: false, textHashesToStore, imageHashesToStore };
|
|
483
483
|
}
|
|
484
484
|
__name(performSimilarityChecks, "performSimilarityChecks");
|
|
485
|
-
async function handleFileUploads(ctx, config, fileManager, logger2,
|
|
485
|
+
async function handleFileUploads(ctx, config, fileManager, logger2, cave, downloadedMedia, reusableIds, session) {
|
|
486
486
|
try {
|
|
487
|
-
if (aiManager) await aiManager.analyzeAndStore(cave, downloadedMedia);
|
|
488
487
|
await Promise.all(downloadedMedia.map((item) => fileManager.saveFile(item.fileName, item.buffer)));
|
|
489
488
|
const needsReview = config.enablePend && session.channelId !== config.adminChannel?.split(":")[1];
|
|
490
489
|
const finalStatus = needsReview ? "pending" : "active";
|
|
491
490
|
await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
|
|
492
|
-
|
|
493
|
-
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h5) => ({ ...h5, cave: cave.id }));
|
|
494
|
-
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
495
|
-
}
|
|
496
|
-
if (finalStatus === "pending" && reviewManager) {
|
|
497
|
-
const [finalCave] = await ctx.database.get("cave", { id: cave.id });
|
|
498
|
-
if (finalCave) reviewManager.sendForPend(finalCave);
|
|
499
|
-
}
|
|
491
|
+
return finalStatus;
|
|
500
492
|
} catch (fileProcessingError) {
|
|
501
493
|
logger2.error(`回声洞(${cave.id})文件处理失败:`, fileProcessingError);
|
|
502
494
|
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
503
495
|
cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds);
|
|
496
|
+
return "delete";
|
|
504
497
|
}
|
|
505
498
|
}
|
|
506
499
|
__name(handleFileUploads, "handleFileUploads");
|
|
@@ -985,10 +978,7 @@ var path3 = __toESM(require("path"));
|
|
|
985
978
|
var AIManager = class {
|
|
986
979
|
/**
|
|
987
980
|
* @constructor
|
|
988
|
-
* @
|
|
989
|
-
* @param {Config} config - 插件的配置信息。
|
|
990
|
-
* @param {Logger} logger - 日志记录器实例。
|
|
991
|
-
* @param {FileManager} fileManager - 文件管理器实例。
|
|
981
|
+
* @description AIManager 类的构造函数,负责初始化依赖项,并向 Koishi 的数据库模型中注册 `cave_meta` 表。
|
|
992
982
|
*/
|
|
993
983
|
constructor(ctx, config, logger2, fileManager) {
|
|
994
984
|
this.ctx = ctx;
|
|
@@ -1010,30 +1000,37 @@ var AIManager = class {
|
|
|
1010
1000
|
}
|
|
1011
1001
|
http;
|
|
1012
1002
|
/**
|
|
1013
|
-
* @description
|
|
1014
|
-
* @param {any} cave - 主 `cave`
|
|
1003
|
+
* @description 注册所有与 AIManager 功能相关的 Koishi 命令。
|
|
1004
|
+
* @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
|
|
1015
1005
|
*/
|
|
1016
1006
|
registerCommands(cave) {
|
|
1017
1007
|
cave.subcommand(".ai", "分析回声洞", { hidden: true, authority: 4 }).usage("分析尚未分析的回声洞,补全回声洞记录。").action(async ({ session }) => {
|
|
1018
|
-
|
|
1019
|
-
if (adminError) return adminError;
|
|
1008
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
1020
1009
|
try {
|
|
1021
1010
|
const allCaves = await this.ctx.database.get("cave", { status: "active" });
|
|
1022
1011
|
const analyzedCaveIds = new Set((await this.ctx.database.get("cave_meta", {})).map((meta) => meta.cave));
|
|
1023
1012
|
const cavesToAnalyze = allCaves.filter((cave2) => !analyzedCaveIds.has(cave2.id));
|
|
1024
1013
|
if (cavesToAnalyze.length === 0) return "无需分析回声洞";
|
|
1025
1014
|
await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
|
|
1026
|
-
let
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
+
}
|
|
1032
1029
|
}
|
|
1033
|
-
return `已分析 ${
|
|
1030
|
+
return `已分析 ${successCount} 个回声洞`;
|
|
1034
1031
|
} catch (error) {
|
|
1035
|
-
this.logger.error("
|
|
1036
|
-
return
|
|
1032
|
+
this.logger.error("分析回声洞失败:", error);
|
|
1033
|
+
return `操作失败: ${error.message}`;
|
|
1037
1034
|
}
|
|
1038
1035
|
});
|
|
1039
1036
|
cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
|
|
@@ -1041,12 +1038,11 @@ var AIManager = class {
|
|
|
1041
1038
|
try {
|
|
1042
1039
|
const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
|
|
1043
1040
|
if (!meta) return `回声洞(${id})尚未分析`;
|
|
1044
|
-
const keywordsText = meta.keywords.join(", ");
|
|
1045
1041
|
const report = [
|
|
1046
1042
|
`回声洞(${id})分析结果:`,
|
|
1047
1043
|
`描述:${meta.description}`,
|
|
1048
|
-
`关键词:${
|
|
1049
|
-
|
|
1044
|
+
`关键词:${meta.keywords.join(", ")}`,
|
|
1045
|
+
`综合评分:${meta.rating}/100`
|
|
1050
1046
|
];
|
|
1051
1047
|
return import_koishi3.h.text(report.join("\n"));
|
|
1052
1048
|
} catch (error) {
|
|
@@ -1056,90 +1052,83 @@ var AIManager = class {
|
|
|
1056
1052
|
});
|
|
1057
1053
|
}
|
|
1058
1054
|
/**
|
|
1059
|
-
* @description
|
|
1060
|
-
* @param {StoredElement[]} newElements -
|
|
1061
|
-
* @param {{ sourceUrl: string, fileName: string }[]} newMediaToSave -
|
|
1062
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1063
|
-
* @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 }>} 一个包含查重结果的对象。
|
|
1064
1060
|
*/
|
|
1065
1061
|
async checkForDuplicates(newElements, newMediaToSave, mediaBuffers) {
|
|
1066
1062
|
try {
|
|
1067
1063
|
const newAnalysis = await this.getAnalysis(newElements, newMediaToSave, mediaBuffers);
|
|
1068
1064
|
if (!newAnalysis || newAnalysis.keywords.length === 0) return { duplicate: false };
|
|
1069
|
-
const newKeywords = new Set(newAnalysis.keywords);
|
|
1070
1065
|
const allMeta = await this.ctx.database.get("cave_meta", {});
|
|
1071
|
-
const potentialDuplicates =
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
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
|
+
}
|
|
1075
1075
|
if (similarity * 100 >= 80) {
|
|
1076
1076
|
const [cave] = await this.ctx.database.get("cave", { id: meta.cave });
|
|
1077
|
-
|
|
1077
|
+
return cave;
|
|
1078
1078
|
}
|
|
1079
|
-
}
|
|
1079
|
+
}))).filter(Boolean);
|
|
1080
1080
|
if (potentialDuplicates.length === 0) return { duplicate: false };
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
+
};
|
|
1085
1094
|
} catch (error) {
|
|
1086
1095
|
this.logger.error("查重回声洞出错:", error);
|
|
1087
1096
|
return { duplicate: false };
|
|
1088
1097
|
}
|
|
1089
1098
|
}
|
|
1090
1099
|
/**
|
|
1091
|
-
* @description
|
|
1092
|
-
* @param {CaveObject} cave -
|
|
1093
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1094
|
-
* @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} 如果在分析或数据库存储过程中发生错误,则会向上抛出异常。
|
|
1095
1105
|
*/
|
|
1096
1106
|
async analyzeAndStore(cave, mediaBuffers) {
|
|
1097
1107
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
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
|
+
}
|
|
1100
1116
|
} catch (error) {
|
|
1101
1117
|
this.logger.error(`分析回声洞(${cave.id})失败:`, error);
|
|
1102
1118
|
throw error;
|
|
1103
1119
|
}
|
|
1104
1120
|
}
|
|
1105
1121
|
/**
|
|
1106
|
-
* @description
|
|
1107
|
-
* @param {StoredElement[]} elements -
|
|
1108
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] -
|
|
1109
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] -
|
|
1110
|
-
* @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`。
|
|
1111
1127
|
*/
|
|
1112
1128
|
async getAnalysis(elements, mediaToSave, mediaBuffers) {
|
|
1113
|
-
const
|
|
1114
|
-
if (!payload.contents) return null;
|
|
1115
|
-
const fullUrl = `${this.config.aiEndpoint}/models/${this.config.aiModel}:generateContent?key=${this.config.aiApiKey}`;
|
|
1116
|
-
const response = await this.http.post(fullUrl, payload, { headers: { "Content-Type": "application/json" }, timeout: 6e4 });
|
|
1117
|
-
return this.parseAnalysisResponse(response);
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* @description 使用 Jaccard 相似度系数计算两组关键词的相似度。
|
|
1121
|
-
* @param {Set<string>} setA - 第一组关键词集合。
|
|
1122
|
-
* @param {Set<string>} setB - 第二组关键词集合。
|
|
1123
|
-
* @returns {number} - 返回 0 到 1 之间的相似度值。
|
|
1124
|
-
*/
|
|
1125
|
-
calculateKeywordSimilarity(setA, setB) {
|
|
1126
|
-
const intersection = new Set([...setA].filter((x) => setB.has(x)));
|
|
1127
|
-
const union = /* @__PURE__ */ new Set([...setA, ...setB]);
|
|
1128
|
-
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* @description 准备发送给 AI 模型的请求体(Payload)。
|
|
1132
|
-
* @param {string} prompt - 系统提示词。
|
|
1133
|
-
* @param {string} schemaString - JSON Schema 字符串。
|
|
1134
|
-
* @param {StoredElement[]} elements - 内容的元素数组。
|
|
1135
|
-
* @param {{ sourceUrl: string, fileName: string }[]} [mediaToSave] - (可选) 待保存的媒体文件信息。
|
|
1136
|
-
* @param {{ fileName: string; buffer: Buffer }[]} [mediaBuffers] - (可选) 已下载的媒体文件 Buffer。
|
|
1137
|
-
* @returns {Promise<{ payload: any }>} - 返回包含请求体的对象。
|
|
1138
|
-
*/
|
|
1139
|
-
async preparePayload(prompt, schemaString, elements, mediaToSave, mediaBuffers) {
|
|
1140
|
-
const parts = [{ text: prompt }];
|
|
1129
|
+
const userContent = [];
|
|
1141
1130
|
const combinedText = elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join("\n");
|
|
1142
|
-
if (combinedText)
|
|
1131
|
+
if (combinedText.trim()) userContent.push({ type: "text", text: combinedText });
|
|
1143
1132
|
const mediaMap = new Map(mediaBuffers?.map((m) => [m.fileName, m.buffer]));
|
|
1144
1133
|
const imageElements = elements.filter((el) => el.type === "image" && el.file);
|
|
1145
1134
|
for (const el of imageElements) {
|
|
@@ -1155,79 +1144,46 @@ var AIManager = class {
|
|
|
1155
1144
|
}
|
|
1156
1145
|
if (buffer) {
|
|
1157
1146
|
const mimeType = path3.extname(el.file).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
|
|
1158
|
-
|
|
1147
|
+
userContent.push({
|
|
1148
|
+
type: "image_url",
|
|
1149
|
+
image_url: { url: `data:${mimeType};base64,${buffer.toString("base64")}` }
|
|
1150
|
+
});
|
|
1159
1151
|
}
|
|
1160
1152
|
} catch (error) {
|
|
1161
1153
|
this.logger.warn(`分析内容(${el.file})失败:`, error);
|
|
1162
1154
|
}
|
|
1163
1155
|
}
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
return { payload: { contents: [{ parts }], generationConfig: { response_schema: schema } } };
|
|
1168
|
-
} catch (error) {
|
|
1169
|
-
this.logger.error("解析JSON Schema失败:", error);
|
|
1170
|
-
return { payload: {} };
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
/**
|
|
1174
|
-
* @description 准备用于 AI 精准查重的请求体(Payload)。
|
|
1175
|
-
* @param {StoredElement[]} newElements - 新内容的元素。
|
|
1176
|
-
* @param {CaveObject[]} existingCaves - 经过初筛的疑似重复的旧内容。
|
|
1177
|
-
* @returns {Promise<{ payload: any }>} - 返回适用于查重场景的请求体。
|
|
1178
|
-
*/
|
|
1179
|
-
async prepareDedupePayload(newElements, existingCaves) {
|
|
1180
|
-
const formatContent = /* @__PURE__ */ __name((elements) => elements.filter((el) => el.type === "text").map((el) => el.content).join(" "), "formatContent");
|
|
1181
|
-
const payloadContent = JSON.stringify({
|
|
1182
|
-
new_content: { text: formatContent(newElements) },
|
|
1183
|
-
existing_contents: existingCaves.map((cave) => ({ id: cave.id, text: formatContent(cave.elements) }))
|
|
1184
|
-
});
|
|
1185
|
-
const fullPrompt = `${this.config.aiCheckPrompt}
|
|
1186
|
-
|
|
1187
|
-
以下是需要处理的数据:
|
|
1188
|
-
${payloadContent}`;
|
|
1189
|
-
try {
|
|
1190
|
-
const schema = JSON.parse(this.config.aiCheckSchema);
|
|
1191
|
-
return { payload: { contents: [{ parts: [{ text: fullPrompt }] }], generationConfig: { response_schema: schema } } };
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
this.logger.error("解析查重JSON Schema失败:", error);
|
|
1194
|
-
return { payload: {} };
|
|
1195
|
-
}
|
|
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);
|
|
1196
1159
|
}
|
|
1197
1160
|
/**
|
|
1198
|
-
* @description
|
|
1199
|
-
* @param {any}
|
|
1200
|
-
* @
|
|
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 返回错误时,抛出异常。
|
|
1201
1167
|
*/
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
return { keywords: [], description: "解析失败", rating: 0 };
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* @description 解析 AI 返回的查重响应。
|
|
1219
|
-
* @param {any} response - AI 服务的原始响应对象。
|
|
1220
|
-
* @returns {{ duplicate: boolean; id?: number }} - 返回查重结果。
|
|
1221
|
-
*/
|
|
1222
|
-
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
|
+
};
|
|
1223
1180
|
try {
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
if (parsed.duplicate === true && parsed.id) return { duplicate: true, id: Number(parsed.id) };
|
|
1227
|
-
return { duplicate: false };
|
|
1181
|
+
const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
|
|
1182
|
+
return JSON.parse(response.choices[0].message.content);
|
|
1228
1183
|
} catch (error) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1184
|
+
const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
|
|
1185
|
+
this.logger.error(`请求 API 失败: ${errorMessage}`);
|
|
1186
|
+
throw error;
|
|
1231
1187
|
}
|
|
1232
1188
|
}
|
|
1233
1189
|
};
|
|
@@ -1264,7 +1220,7 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1264
1220
|
}).description("复核配置"),
|
|
1265
1221
|
import_koishi4.Schema.object({
|
|
1266
1222
|
enableAI: import_koishi4.Schema.boolean().default(false).description("启用 AI"),
|
|
1267
|
-
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"),
|
|
1268
1224
|
aiApiKey: import_koishi4.Schema.string().description("密钥 (Key)").role("secret"),
|
|
1269
1225
|
aiModel: import_koishi4.Schema.string().description("模型").default("gemini-2.5-flash"),
|
|
1270
1226
|
AnalysePrompt: import_koishi4.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析提示词 (Prompt)"),
|
|
@@ -1409,22 +1365,25 @@ function apply(ctx, config) {
|
|
|
1409
1365
|
}
|
|
1410
1366
|
const userName = (config.enableName ? await profileManager.getNickname(session.userId) : null) || session.username;
|
|
1411
1367
|
const needsReview = config.enablePend && session.channelId !== config.adminChannel?.split(":")[1];
|
|
1412
|
-
|
|
1368
|
+
let finalStatus = hasMedia ? "preload" : needsReview ? "pending" : "active";
|
|
1413
1369
|
const newCave = await ctx.database.create("cave", {
|
|
1414
1370
|
id: newId,
|
|
1415
1371
|
elements: finalElementsForDb,
|
|
1416
1372
|
channelId: session.channelId,
|
|
1417
1373
|
userId: session.userId,
|
|
1418
1374
|
userName,
|
|
1419
|
-
status:
|
|
1375
|
+
status: finalStatus,
|
|
1420
1376
|
time: creationTime
|
|
1421
1377
|
});
|
|
1422
|
-
if (hasMedia)
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
if (aiManager) await aiManager.analyzeAndStore(newCave);
|
|
1426
|
-
if (hashManager
|
|
1427
|
-
|
|
1378
|
+
if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
|
|
1379
|
+
if (finalStatus !== "preload" && finalStatus !== "delete") {
|
|
1380
|
+
newCave.status = finalStatus;
|
|
1381
|
+
if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
|
|
1382
|
+
if (hashManager) {
|
|
1383
|
+
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h5) => ({ ...h5, cave: newCave.id }));
|
|
1384
|
+
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
1385
|
+
}
|
|
1386
|
+
if (finalStatus === "pending" && reviewManager) reviewManager.sendForPend(newCave);
|
|
1428
1387
|
}
|
|
1429
1388
|
return needsReview ? `提交成功,序号为(${newCave.id})` : `添加成功,序号为(${newCave.id})`;
|
|
1430
1389
|
} catch (error) {
|