302 lines
9.6 KiB
TypeScript
302 lines
9.6 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 { 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<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,
|
|||
|
|
'‘设置-> 推理设置’'
|
|||
|
|
)
|
|||
|
|
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<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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前分镜的上下文数据
|
|||
|
|
* @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<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,
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|