LaiTool_PRO/src/main/service/aiReason/aiReasonCommon.ts
2025-09-04 16:58:42 +08:00

305 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
/**
* 替换消息对象数组中的占位符
*
* 此方法用于批量处理 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
) {
await this.GetAISetting()
// 获取当前的推理模式信息
let selectInferenceModel = this.GetInferenceModelMessage()
// 内置模式
let context = this.GetBookTaskDetailContextData(
currentBookTaskDetail,
bookTaskDetails,
contextCount
)
if (isEmpty(characterString) && selectInferenceModel.mustCharacter) {
throw new Error('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!')
}
let requestBody = selectInferenceModel.requestBody
if (requestBody == null) {
throw new Error('未找到对应的分镜预设的请求数据,请检查')
}
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
}
}