import { OptionRealmService } from '@/define/db/service/optionService' import { OptionKeyName } from '@/define/enum/option' import { optionSerialization } from '../option/optionSerialization' import { SettingModal } from '@/define/model/setting' import { isEmpty } from 'lodash' import { GetOpenAISuccessResponse } from '@/define/response/openAIResponse' import { GetApiDefineDataById } from '@/define/data/apiData' import axios from 'axios' import { RetryWithBackoff } from '@/define/Tools/common' import { Book } from '@/define/model/book/book' import { AiInferenceModelModel, GetAIPromptOptionByValue } from '@/define/data/aiData/aiData' import { t } from '@/i18n' /** * AI推理通用工具类 * * 该类提供了与AI推理相关的各种通用功能,包括: * - 初始化和获取推理设置 * - 获取API提供商和模型信息 * - 文本中占位符替换 * - 获取上下文数据 * - 构建请求消息 * - 执行推理请求 * * 主要用于处理小说分镜任务的AI推理流程,负责构建请求、发送请求 * 和处理响应数据。 * * @class AiReasonCommon * @example * const aiReason = new AiReasonCommon(); * await aiReason.GetAISetting(); * const result = await aiReason.OriginalInferencePrompt(taskDetail, allDetails, 2, characterData); */ export class AiReasonCommon { optionRealmService!: OptionRealmService aiReasonSetting!: SettingModal.InferenceAISettings /** * * 初始化 AiReasonCommon 类的实例 * @returns {Promise} - 返回一个 Promise 对象,表示初始化操作的完成状态 */ async InitAiReasonCommon(): Promise { if (!this.optionRealmService) { this.optionRealmService = await OptionRealmService.getInstance() } } /** * 获取推理设置 * @returns {Promise} - 返回一个 Promise 对象,表示获取操作的完成状态 * @throws {Error} - 如果推理设置不完整,则抛出错误 */ async GetAISetting(): Promise { await this.InitAiReasonCommon() let res = this.optionRealmService.GetOptionByKey(OptionKeyName.InferenceAI.InferenceSetting) let aiReasonSetting = optionSerialization( res, t('设置 -> 推理设置') ) if ( isEmpty(aiReasonSetting.apiProvider) || isEmpty(aiReasonSetting.apiToken) || isEmpty(aiReasonSetting.inferenceModel) || isEmpty(aiReasonSetting.aiPromptValue) ) { throw new Error( t("请检查 ‘{path}’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!", { path: t('设置 -> 推理设置') }) ) } this.aiReasonSetting = aiReasonSetting } /** * 获取当前的API提供商信息 * @returns */ GetAPIProviderMessage() { let apiProviders = GetApiDefineDataById(this.aiReasonSetting.apiProvider) return apiProviders } /** * 获取当前的推理模型信息 * @returns {any} - 返回当前的推理模型信息 * @throws {Error} - 如果推理模型不存在,则抛出错误 */ GetInferenceModelMessage(): AiInferenceModelModel { let selectInferenceModel = GetAIPromptOptionByValue(this.aiReasonSetting.aiPromptValue) if (isEmpty(selectInferenceModel)) { throw new Error(t('请检查推理模型是否存在!')) } return selectInferenceModel } /** * 替换字符串中的占位符为指定值 * * 此方法查找字符串中所有格式为 {key} 的占位符, * 并用 replacements 对象中对应的值进行替换。 * * @param {string} content - 包含占位符的原始字符串 * @param {Record} replacements - 键值对对象,键是要替换的占位符,值是替换内容 * @returns {string} 完成所有占位符替换后的字符串 * * @example * // 返回 "你好,张三,今天是星期一" * replaceObject("你好,{name},今天是{day}", { name: "张三", day: "星期一" }) */ replaceObject(content: string, replacements: Record): string { let result = content for (let key in replacements) { result = result.replaceAll(`{${key}}`, replacements[key]) } return result } /** * 替换消息对象数组中的占位符 * * 此方法用于批量处理 OpenAI 请求消息数组,对每个消息对象的 content 字段 * 进行占位符替换。常用于在发送 AI 推理请求前,将消息模板中的占位符 * 替换为实际的动态内容。 * * @param {OpenAIRequest.RequestMessage[]} message - OpenAI 请求消息数组 * 每个消息对象包含 role (角色) 和 content (内容) 字段 * @param {Record} replacements - 键值对对象,键是要替换的占位符名,值是替换内容 * @returns {OpenAIRequest.RequestMessage[]} 完成占位符替换后的新消息数组 * * @example * const messages = [ * { role: 'system', content: '你是一个{role},擅长{skill}' }, * { role: 'user', content: '请帮我{task}' } * ]; * const replacements = { * role: '小说分析师', * skill: '情节分析', * task: '分析这段文字的情感色彩' * }; * // 返回替换后的消息数组 * const result = replaceMessageObject(messages, replacements); * * @see replaceObject - 单个字符串的占位符替换方法 */ replaceMessageObject( messages: OpenAIRequest.RequestMessage[], replacements: Record ): OpenAIRequest.RequestMessage[] { // 使用 map 方法遍历消息数组,对每个消息对象进行处理 return messages.map((item) => ({ // 保持原有的所有属性(使用扩展运算符) ...item, // 仅对 content 字段进行占位符替换处理 content: this.replaceObject(item.content, replacements) })) } /** * 获取当前分镜的上下文数据 * @param currentBookTaskDetail 当前分镜数据 * @param bookTaskDetails 所有的小说分镜数据 * @param contextCount 上下文行数 */ GetBookTaskDetailContextData( currentBookTaskDetail: Book.SelectBookTaskDetail, bookTaskDetails: Book.SelectBookTaskDetail[], contextCount: number ): string { let prefix = '' // 拼接一个word let i = (currentBookTaskDetail.no as number) - 1 if (i <= contextCount) { prefix = bookTaskDetails .filter((_item, index) => index < i) .map((item) => item.afterGpt) .join('\r\n') } else if (i > contextCount) { prefix = bookTaskDetails .filter((_item, index) => i - index <= contextCount && i - index > 0) .map((item) => item.afterGpt) .join('\r\n') } let suffix = '' let o_i = bookTaskDetails.length - i if (o_i <= contextCount) { suffix = bookTaskDetails .filter((_item, index) => index > i) .map((item) => item.afterGpt) .join('\r\n') } else if (o_i > contextCount) { suffix = bookTaskDetails .filter((_item, index) => index - i <= contextCount && index - i > 0) .map((item) => item.afterGpt) .join('\r\n') } return `${prefix}\r\n${currentBookTaskDetail.afterGpt}\r\n${suffix}` } /** * 发起推理请求 * @description 该方法用于发起推理请求,获取推理结果。包含重试机制和错误处理。 * * @param {OpenAISuccessResponse} message - 要发送的消息对象 * @returns {Promise} - 返回一个 Promise 对象,表示获取操作的完成状态 * @throws {Error} - 如果推理设置不完整,则抛出错误 * @throws {Error} - 如果请求失败,则抛出错误 * @throws {Error} - 如果响应数据格式不正确,则抛出错误 * */ async FetchGpt(message: any, option: any = {}): Promise { try { let data = { model: this.aiReasonSetting.inferenceModel, messages: message, ...option } let apiProvider = this.GetAPIProviderMessage() let config = { method: 'post', maxBodyLength: Infinity, url: apiProvider.gpt_url, headers: { Authorization: `Bearer ${this.aiReasonSetting.apiToken}`, 'Content-Type': 'application/json' }, data: JSON.stringify(data) } let res = await RetryWithBackoff( async () => { return await axios.request(config) }, 5, 2000 ) let content = GetOpenAISuccessResponse(res.data) // this.GetResponseContent(res, this.gptUrl) return content } catch (error) { throw error } } /** * 原创推理提示词数据 * @param currentBookTaskDetail 要推理的小说分镜任务 * @param bookTaskDetails 所有的小说分镜任务 * @param contextCount 上下文的数量 * @param autoAnalyzeCharacter 自动分析的角色数据字符串 */ async OriginalInferencePrompt( currentBookTaskDetail: Book.SelectBookTaskDetail, bookTaskDetails: Book.SelectBookTaskDetail[], contextCount: number, characterString: string, sceneString: string ) { await this.GetAISetting() // 获取当前的推理模式信息 let selectInferenceModel = this.GetInferenceModelMessage() // 内置模式 let context = this.GetBookTaskDetailContextData( currentBookTaskDetail, bookTaskDetails, contextCount ) if (isEmpty(characterString) && selectInferenceModel.mustCharacter) { throw new Error(t('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!')) } let requestBody = selectInferenceModel.requestBody if (requestBody == null) { throw new Error(t('未找到对应的分镜预设的请求数据,请检查')) } requestBody.messages = this.replaceMessageObject(requestBody.messages, { contextContent: context, textContent: currentBookTaskDetail.afterGpt ?? '', characterContent: characterString, sceneContent: sceneString, characterSceneContent: characterString + '\n' + sceneString, wordCount: '40' }) delete requestBody.model // 开始请求 let res = await this.FetchGpt(requestBody.messages, requestBody) if (res) { // 处理返回的数据,删除部分数据 res = res .replace(/\)\s*\(/g, ', ') .replace(/^\(/, '') .replace(/\)$/, '') .replaceAll('*', '') .replaceAll('--', ' ') } return res } }