310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
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 { cloneDeep, 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<void>} - 返回一个 Promise 对象,表示初始化操作的完成状态
|
||
*/
|
||
async InitAiReasonCommon(): Promise<void> {
|
||
if (!this.optionRealmService) {
|
||
this.optionRealmService = await OptionRealmService.getInstance()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取推理设置
|
||
* @returns {Promise<void>} - 返回一个 Promise 对象,表示获取操作的完成状态
|
||
* @throws {Error} - 如果推理设置不完整,则抛出错误
|
||
*/
|
||
async GetAISetting(): Promise<void> {
|
||
await this.InitAiReasonCommon()
|
||
let res = this.optionRealmService.GetOptionByKey(OptionKeyName.InferenceAI.InferenceSetting)
|
||
|
||
let aiReasonSetting = optionSerialization<SettingModal.InferenceAISettings>(
|
||
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} - 如果推理模型不存在,则抛出错误
|
||
*/
|
||
async GetInferenceModelMessage(optionsData?: AiInferenceModelModel[]): Promise<AiInferenceModelModel> {
|
||
let selectInferenceModel = await GetAIPromptOptionByValue(this.aiReasonSetting.aiPromptValue, optionsData)
|
||
if (isEmpty(selectInferenceModel)) {
|
||
throw new Error(t('请检查推理模型是否存在!'))
|
||
}
|
||
return selectInferenceModel
|
||
}
|
||
|
||
/**
|
||
* 替换字符串中的占位符为指定值
|
||
*
|
||
* 此方法查找字符串中所有格式为 {key} 的占位符,
|
||
* 并用 replacements 对象中对应的值进行替换。
|
||
*
|
||
* @param {string} content - 包含占位符的原始字符串
|
||
* @param {Record<string, string>} replacements - 键值对对象,键是要替换的占位符,值是替换内容
|
||
* @returns {string} 完成所有占位符替换后的字符串
|
||
*
|
||
* @example
|
||
* // 返回 "你好,张三,今天是星期一"
|
||
* replaceObject("你好,{name},今天是{day}", { name: "张三", day: "星期一" })
|
||
*/
|
||
replaceObject(content: string, replacements: Record<string, string>): 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<string, string>} 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<string, string>
|
||
): 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<string>} - 返回一个 Promise 对象,表示获取操作的完成状态
|
||
* @throws {Error} - 如果推理设置不完整,则抛出错误
|
||
* @throws {Error} - 如果请求失败,则抛出错误
|
||
* @throws {Error} - 如果响应数据格式不正确,则抛出错误
|
||
*
|
||
*/
|
||
async FetchGpt(message: any, option: any = {}): Promise<string> {
|
||
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,
|
||
optionsData?: AiInferenceModelModel[]
|
||
) {
|
||
await this.GetAISetting()
|
||
console.log(currentBookTaskDetail.id, currentBookTaskDetail.afterGpt)
|
||
|
||
// 获取当前的推理模式信息
|
||
let selectInferenceModel = await this.GetInferenceModelMessage(optionsData)
|
||
|
||
// 内置模式
|
||
let context = this.GetBookTaskDetailContextData(
|
||
currentBookTaskDetail,
|
||
bookTaskDetails,
|
||
contextCount
|
||
)
|
||
|
||
if (isEmpty(characterString) && selectInferenceModel.mustCharacter) {
|
||
throw new Error(t('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!'))
|
||
}
|
||
|
||
let requestBody = cloneDeep(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
|
||
}
|
||
}
|