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' /** * 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, '‘设置-> 推理设置’' ) if ( isEmpty(aiReasonSetting.apiProvider) || isEmpty(aiReasonSetting.apiToken) || isEmpty(aiReasonSetting.inferenceModel) || isEmpty(aiReasonSetting.aiPromptValue) ) { throw new Error( '请检查 ‘设置-> 推理设置’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!' ) } 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('请检查推理模型是否存在!') } 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 } /** * 获取当前分镜的上下文数据 * @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}` } /** * 返回当前推理数据的请求体中的message * @param currentBookTaskDetail 当前推理的提示词数据 * @param contextData 上下文数据 * @param autoAnalyzeCharacter 自动分析的角色数据 * @returns */ GetGPTRequestMessage( currentBookTaskDetail: Book.SelectBookTaskDetail, contextData: string, autoAnalyzeCharacter: string, selectInferenceModel: AiInferenceModelModel ): any[] { let message: any = [] if (selectInferenceModel.hasExample) { // // 有返回案例的 // message = gptDefine.GetExamplePromptMessage(global.config.gpt_auto_inference) // // 加当前提问的 // message.push({ // role: 'user', // content: currentBookTaskDetail.afterGpt // }) } else { // 直接返回,没有案例的 message = [ { role: 'system', content: this.replaceObject(selectInferenceModel.systemContent, { textContent: contextData, characterContent: autoAnalyzeCharacter }) }, { role: 'user', content: this.replaceObject(selectInferenceModel.userContent, { contextContent: contextData, textContent: currentBookTaskDetail.afterGpt ?? '', characterContent: autoAnalyzeCharacter, wordCount: '40' }) } ] } return message } /** * 发起推理请求 * @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, autoAnalyzeCharacter: string ) { await this.GetAISetting() // 获取当前的推理模式信息 let selectInferenceModel = this.GetInferenceModelMessage() // 内置模式 let context = this.GetBookTaskDetailContextData( currentBookTaskDetail, bookTaskDetails, contextCount ) if (isEmpty(autoAnalyzeCharacter) && selectInferenceModel.mustCharacter) { throw new Error('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!') } let message = this.GetGPTRequestMessage( currentBookTaskDetail, context, autoAnalyzeCharacter, selectInferenceModel ) // 开始请求 let res = await this.FetchGpt(message) if (res) { // 处理返回的数据,删除部分数据 res = res .replace(/\)\s*\(/g, ', ') .replace(/^\(/, '') .replace(/\)$/, '') .replaceAll('*', '') .replaceAll('--', ' ') } return res } }