2025-08-19 14:33:59 +08:00
|
|
|
|
import { OptionRealmService } from '@/define/db/service/optionService'
|
|
|
|
|
|
import { OptionKeyName } from '@/define/enum/option'
|
|
|
|
|
|
import { optionSerialization } from '../option/optionSerialization'
|
|
|
|
|
|
import { SettingModal } from '@/define/model/setting'
|
2025-09-15 16:58:09 +08:00
|
|
|
|
import { cloneDeep, isEmpty } from 'lodash'
|
2025-08-19 14:33:59 +08:00
|
|
|
|
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'
|
2025-09-12 14:52:28 +08:00
|
|
|
|
import { t } from '@/i18n'
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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,
|
2025-09-12 14:52:28 +08:00
|
|
|
|
t('设置 -> 推理设置')
|
2025-08-19 14:33:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
if (
|
|
|
|
|
|
isEmpty(aiReasonSetting.apiProvider) ||
|
|
|
|
|
|
isEmpty(aiReasonSetting.apiToken) ||
|
|
|
|
|
|
isEmpty(aiReasonSetting.inferenceModel) ||
|
|
|
|
|
|
isEmpty(aiReasonSetting.aiPromptValue)
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new Error(
|
2025-09-12 14:52:28 +08:00
|
|
|
|
t("请检查 ‘{path}’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!", {
|
|
|
|
|
|
path: t('设置 -> 推理设置')
|
|
|
|
|
|
})
|
2025-08-19 14:33:59 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.aiReasonSetting = aiReasonSetting
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前的API提供商信息
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
|
|
|
|
|
GetAPIProviderMessage() {
|
|
|
|
|
|
let apiProviders = GetApiDefineDataById(this.aiReasonSetting.apiProvider)
|
|
|
|
|
|
return apiProviders
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前的推理模型信息
|
|
|
|
|
|
* @returns {any} - 返回当前的推理模型信息
|
|
|
|
|
|
* @throws {Error} - 如果推理模型不存在,则抛出错误
|
|
|
|
|
|
*/
|
2025-09-15 16:58:09 +08:00
|
|
|
|
async GetInferenceModelMessage(optionsData?: AiInferenceModelModel[]): Promise<AiInferenceModelModel> {
|
|
|
|
|
|
let selectInferenceModel = await GetAIPromptOptionByValue(this.aiReasonSetting.aiPromptValue, optionsData)
|
2025-08-19 14:33:59 +08:00
|
|
|
|
if (isEmpty(selectInferenceModel)) {
|
2025-09-12 14:52:28 +08:00
|
|
|
|
throw new Error(t('请检查推理模型是否存在!'))
|
2025-08-19 14:33:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 16:58:42 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 替换消息对象数组中的占位符
|
|
|
|
|
|
*
|
|
|
|
|
|
* 此方法用于批量处理 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)
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-19 14:33:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前分镜的上下文数据
|
|
|
|
|
|
* @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,
|
2025-09-04 16:58:42 +08:00
|
|
|
|
characterString: string,
|
2025-09-15 16:58:09 +08:00
|
|
|
|
sceneString: string,
|
|
|
|
|
|
optionsData?: AiInferenceModelModel[]
|
2025-08-19 14:33:59 +08:00
|
|
|
|
) {
|
|
|
|
|
|
await this.GetAISetting()
|
2025-09-15 16:58:09 +08:00
|
|
|
|
console.log(currentBookTaskDetail.id, currentBookTaskDetail.afterGpt)
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取当前的推理模式信息
|
2025-09-15 16:58:09 +08:00
|
|
|
|
let selectInferenceModel = await this.GetInferenceModelMessage(optionsData)
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 内置模式
|
|
|
|
|
|
let context = this.GetBookTaskDetailContextData(
|
|
|
|
|
|
currentBookTaskDetail,
|
|
|
|
|
|
bookTaskDetails,
|
|
|
|
|
|
contextCount
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-04 16:58:42 +08:00
|
|
|
|
if (isEmpty(characterString) && selectInferenceModel.mustCharacter) {
|
2025-09-12 14:52:28 +08:00
|
|
|
|
throw new Error(t('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!'))
|
2025-08-19 14:33:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 16:58:09 +08:00
|
|
|
|
let requestBody = cloneDeep(selectInferenceModel.requestBody)
|
2025-09-04 16:58:42 +08:00
|
|
|
|
if (requestBody == null) {
|
2025-09-12 14:52:28 +08:00
|
|
|
|
throw new Error(t('未找到对应的分镜预设的请求数据,请检查'))
|
2025-09-04 16:58:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
requestBody.messages = this.replaceMessageObject(requestBody.messages, {
|
|
|
|
|
|
contextContent: context,
|
|
|
|
|
|
textContent: currentBookTaskDetail.afterGpt ?? '',
|
|
|
|
|
|
characterContent: characterString,
|
|
|
|
|
|
sceneContent: sceneString,
|
|
|
|
|
|
characterSceneContent: characterString + '\n' + sceneString,
|
|
|
|
|
|
wordCount: '40'
|
|
|
|
|
|
})
|
2025-08-19 14:33:59 +08:00
|
|
|
|
|
2025-09-04 16:58:42 +08:00
|
|
|
|
delete requestBody.model
|
2025-08-19 14:33:59 +08:00
|
|
|
|
// 开始请求
|
2025-09-04 16:58:42 +08:00
|
|
|
|
let res = await this.FetchGpt(requestBody.messages, requestBody)
|
2025-08-19 14:33:59 +08:00
|
|
|
|
if (res) {
|
|
|
|
|
|
// 处理返回的数据,删除部分数据
|
|
|
|
|
|
res = res
|
|
|
|
|
|
.replace(/\)\s*\(/g, ', ')
|
|
|
|
|
|
.replace(/^\(/, '')
|
|
|
|
|
|
.replace(/\)$/, '')
|
|
|
|
|
|
.replaceAll('*', '')
|
|
|
|
|
|
.replaceAll('--', ' ')
|
|
|
|
|
|
}
|
|
|
|
|
|
return res
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|