LaiTool_PRO/src/main/service/mj/mjApiService.ts

552 lines
19 KiB
TypeScript
Raw Normal View History

2025-08-19 14:33:59 +08:00
import axios from 'axios'
import { MJBasic } from './mjBasic'
import { ImageGenerateMode, MJRobotType, MJSpeed } from '@/define/data/mjData'
import { MJRespoonseType } from '@/define/enum/mjEnum'
import { GetApiDefineDataById } from '@/define/data/apiData'
import { isEmpty } from 'lodash'
import { BookBackTaskStatus } from '@/define/enum/bookEnum'
import { MJ } from '@/define/model/mj'
/**
* MidJourney API
*
* MidJourney API请求中账户过滤相关的配置选项
* API请求中指定任务处理速度ID等账户级别的设置
*
* @interface AccountFilter
* @property {['FAST' | 'RELAX']?} modes - 'FAST'()'RELAX'()
* @property {string?} remark -
* @property {string?} instanceId - ID
*
* @example
* // 创建一个账户过滤器对象
* const filter: AccountFilter = {
* modes: ['FAST'],
* remark: '请求来源标识',
* instanceId: ''
* };
*/
interface AccountFilter {
modes?: ['FAST' | 'RELAX']
remark?: string // 添加问号使其成为可选属性
instanceId?: string // 添加问号使其成为可选属性
}
/**
* MidJourney API
*
* MidJourney API提交图像生成请求时所需的请求体结构
*
*
* @interface MJAPIImagineRequestBody
* @property {'MID_JOURNEY' | 'NIJI_JOURNEY'} botType - 使MidJourney或NijiJourney
* @property {string} prompt -
* @property {AccountFilter} [accountFilter] -
*
* @example
* // 创建一个图像生成请求体
* const requestBody: MJAPIImagineRequestBody = {
* botType: 'MID_JOURNEY',
* prompt: 'a beautiful sunset over mountains, photorealistic style',
* accountFilter: {
* modes: ['FAST']
* }
* };
*/
interface MJAPIImagineRequestBody {
botType: 'MID_JOURNEY' | 'NIJI_JOURNEY'
prompt: string
accountFilter?: AccountFilter
}
/**
* MidJourney API
*
* MidJourney API提交图像描述()
* base64编码的图像数据和可选的账户过滤器设置
*
* @interface MJAPIDescribeRequestBody
* @property {'MID_JOURNEY' | 'NIJI_JOURNEY'} botType - 使MidJourney或NijiJourney
* @property {string} base64 - base64编码的图像数据字符串/
* @property {AccountFilter} [accountFilter] -
*
* @example
* // 创建一个图像描述请求体
* const requestBody: MJAPIDescribeRequestBody = {
* botType: 'MID_JOURNEY',
* base64: 'data:image/png;base64,iVBORw0KGgo...',
* accountFilter: {
* modes: ['FAST']
* }
* };
*/
interface MJAPIDescribeRequestBody {
botType: 'MID_JOURNEY' | 'NIJI_JOURNEY'
base64: string
accountFilter?: AccountFilter
}
/**
* MidJourney API
*
* MidJourney API的所有交互()
* MJBasic基类MJ API的完整封装
* API请求
*
*
* - MidJourney API设置和配置
* - (imagine)
* - (describe/)
* -
* - API要求的请求体
* - API响应和错误
*
* 使
* - AI图像生成
* - ()
* -
*
* @class MJApiService
* @extends MJBasic
*
* @example
* // 初始化服务
* const mjApiService = new MJApiService();
*
* // 提交图像生成请求
* const taskId = await mjApiService.SubmitMJImagine(
* "local-task-id",
* "a futuristic cityscape at sunset, hyperrealistic style"
* );
*
* // 查询任务状态
* const taskStatus = await mjApiService.GetMJAPITaskById(taskId, "local-task-id");
*/
export class MJApiService extends MJBasic {
bootType: 'NIJI_JOURNEY' | 'MID_JOURNEY'
imagineUrl!: string
fetchTaskUrl!: string
describeUrl!: string
constructor() {
super()
this.bootType = 'MID_JOURNEY'
}
/**
* MJ设置
*/
async InitMJSetting(): Promise<void> {
await this.GetMJGeneralSetting()
await this.GetApiSetting()
if (this.mjApiSetting?.apiKey == null || this.mjApiSetting?.apiKey == '') {
throw new Error('没有找到对应的API的配置请检查 ‘设置 -> MJ设置 配置!')
}
this.bootType =
this.mjGeneralSetting?.robot == MJRobotType.NIJI ? 'NIJI_JOURNEY' : 'MID_JOURNEY'
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
if (!this.mjApiSetting || isEmpty(this.mjApiSetting.apiUrl)) {
throw new Error('没有找到对应的API的配置请检查 ‘设置 -> MJ设置 配置!')
}
let apiProvider = GetApiDefineDataById(this.mjApiSetting.apiUrl as string)
if (apiProvider.mj_url == null) {
throw new Error('当前API不支持MJ出图请检查 ‘设置 -> MJ设置 配置!')
}
this.imagineUrl = apiProvider.mj_url.imagine
this.describeUrl = apiProvider.mj_url.describe
this.fetchTaskUrl = apiProvider.mj_url.once_get_task
} else {
throw new Error('当前的MJ出图模式不支持请检查 ‘设置 -> MJ设置 配置!')
}
}
//#region 获取对应的任务通过ID
/**
* ID获取MidJourney API任务的状态和结果
*
* MidJourney API发送请求ID的状态信息URL和错误信息等
*
*
* @param {string} taskId - MidJourney API的任务IDAPI任务状态
* @param {string} backTaskId - ID
* @returns {Promise<MJ.MJResponseToFront>} URL等信息
* @throws {Error} API请求失败或返回不可解析的数据
*
* @example
* try {
* const taskStatus = await mjApiService.GetMJAPITaskById("task-123", "local-456");
* if (taskStatus.code === 1 && taskStatus.progress === 100) {
* console.log("任务完成图像URL:", taskStatus.imageShow);
* } else {
* console.log("任务进度:", taskStatus.progress, "%");
* }
* } catch (error) {
* console.error("获取任务状态失败:", error.message);
* }
*/
async GetMJAPITaskById(taskId: string, backTaskId: string) {
try {
await this.InitMJSetting()
let APIDescribeUrl = this.fetchTaskUrl.replace('${id}', taskId)
// 拼接headers
let headers = {
Authorization: this.mjApiSetting?.apiKey
}
// 开始请求
let res = await axios.get(APIDescribeUrl, {
headers: headers
})
let resData = res.data
let progress =
resData.progress && resData.progress.length > 0
? parseInt(resData.progress.slice(0, -1))
: 0
let status = resData.status.toLowerCase()
let code = status == 'failure' || status == 'cancel' ? 0 : 1
// 失败
if (code == 0) {
if (!isEmpty(backTaskId)) {
this.taskListService.UpdateTaskStatus({
id: backTaskId,
status: BookBackTaskStatus.FAIL,
errorMessage: resData.message
})
}
}
let resObj = {
type: MJRespoonseType.UPDATED,
progress: isNaN(progress) ? 0 : progress,
category: this.mjGeneralSetting?.outputMode,
imageClick: resData.imageUrl,
imageShow: resData.imageUrl,
imagePath: resData.imageUrl,
messageId: taskId,
status: status,
code: code,
prompt: resData.prompt == '' ? resData.promptEn : resData.prompt,
message: resData.failReason,
mjApiUrl: this.fetchTaskUrl
} as MJ.MJResponseToFront
return resObj
} catch (error) {
throw error
}
}
//#endregion
//#region MJ反推相关操作
/**
* MidJourney图像描述()
*
* MidJourney进行描述分析()
* API模式MJ设置
*
* @param {MJ.APIDescribeParams} param - ID和base64编码图像的参数对象
* @returns {Promise<string>} API任务结果ID"23"
* @throws {Error} API调用失败时抛出错误
*
* @example
* try {
* const params = {
* taskId: "task-123",
* image: "data:image/png;base64,iVBORw0KGgo..."
* };
* const resultId = await mjApiService.SubmitMJDescribe(params);
* console.log("提交成功任务ID:", resultId);
* } catch (error) {
* console.error("提交反推任务失败:", error.message);
* }
*/
async SubmitMJDescribe(param: MJ.APIDescribeParams): Promise<string> {
await this.InitMJSetting()
let res: string
switch (this.mjGeneralSetting?.outputMode) {
case ImageGenerateMode.MJ_API:
res = await this.SubmitMJDescribeAPI(param)
break
default:
throw new Error('MJ反推的类型不支持反推只支持API和代理模式')
}
return res
}
/**
* MidJourney描述请求的主体和配置
*
* base64图像和当前MJ设置MidJourney描述API的请求主体和HTTP配置
*
*
* @param {string} imageBase64 - base64编码的图像字符串/
* @returns {{body: MJAPIDescribeRequestBody, config: Object}}
* @throws {Error} MJ输出模式不支持
*
* @example
* const imageBase64 = "data:image/png;base64,iVBORw0KGgo...";
* const { body, config } = mjApiService.GenerateDescribeRequestBody(imageBase64);
* const response = await axios.post(describeUrl, body, config);
*/
GenerateDescribeRequestBody(imageBase64: string): {
body: MJAPIDescribeRequestBody
config: any
} {
// 提交API的反推
let data = {
botType: this.bootType,
base64: imageBase64,
accountFilter: {
modes: [this.mjApiSetting?.apiSpeed == MJSpeed.FAST ? 'FAST' : 'RELAX'],
remark: global.machineId,
instanceId: ''
} as AccountFilter
}
let config = {
headers: {
'Content-Type': 'application/json'
}
}
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
delete data.accountFilter.remark
delete data.accountFilter.instanceId
config.headers['Authorization'] = this.mjApiSetting?.apiKey
} else {
throw new Error('MJ出图的类型不支持')
}
return {
body: data,
config: config
}
}
/**
* API提交MidJourney图像描述()
*
* MidJourney API进行描述()base64编码的图像数据发送到API
* API响应状态码
*
*
* - (code=23): RECONNECT"23"
* - 请求失败: 更新任务状态为FAIL
* - 请求成功: 更新任务状态为RUNNINGID
*
* @param {MJ.APIDescribeParams} param - ID和base64编码图像数据的参数对象
* @returns {Promise<string>} API任务结果ID"23"
* @throws {Error} API返回失败状态码或错误描述
*
* @example
* try {
* const params = {
* taskId: "task-123",
* image: "data:image/png;base64,iVBORw0KGgo..."
* };
* const taskResultId = await mjApiService.SubmitMJDescribeAPI(params);
* if (taskResultId === "23") {
* // 队列已满,需要重试
* } else {
* // 任务提交成功可以用taskResultId查询结果
* }
* } catch (error) {
* console.error("图像描述请求失败:", error.message);
* }
*/
async SubmitMJDescribeAPI(param: MJ.APIDescribeParams): Promise<string> {
// 获取body和config
let { body, config } = this.GenerateDescribeRequestBody(param.image)
// 开始请求
let res = await axios.post(this.describeUrl, body, config)
// 某些API的返回的code为23表示队列已满需要重新请求
if (res.data.code == 23) {
this.taskListService.UpdateTaskStatus({
id: param.taskId,
status: BookBackTaskStatus.RECONNECT
})
return '23'
}
if (res.data.code != 1 && res.data.code != 22) {
this.taskListService.UpdateTaskStatus({
id: param.taskId,
status: BookBackTaskStatus.FAIL,
errorMessage: res.data.description
})
throw new Error(res.data.description)
}
this.taskListService.UpdateTaskStatus({
id: param.taskId,
status: BookBackTaskStatus.RUNNING
})
return res.data.result as string
}
//#endregion
//#region 提交MJ生图任务
/**
* MidJourney图像生成任务
*
* MidJourney进行图像生成
* API模式MJ设置
*
* @param {string} taskId - ID
* @param {string} prompt -
* @returns {Promise<string>} API任务结果ID"23"
* @throws {Error} API调用失败时抛出错误
*
* @example
* try {
* const resultId = await mjApiService.SubmitMJImagine("task-123", "a beautiful sunset in watercolor style");
* console.log("提交成功任务ID:", resultId);
* } catch (error) {
* console.error("提交生图任务失败:", error.message);
* }
*/
async SubmitMJImagine(taskId: string, prompt: string): Promise<string> {
await this.InitMJSetting()
let res: string
switch (this.mjGeneralSetting?.outputMode) {
case ImageGenerateMode.MJ_API:
res = await this.SubmitMJImagineAPI(taskId, prompt)
break
default:
throw new Error('MJ出图的类型不支持')
}
return res
}
/**
* MidJourney API的imagine请求体和配置
*
* MJ设置生成用于调用imagine API的请求主体和HTTP配置
*
*
* @param {string} prompt -
* @returns {{body: MJAPIImagineRequestBody, config: Object}}
* @throws {Error} MJ输出模式不支持
*
* @example
* const { body, config } = mjApiService.GenerateImagineRequestBody("a beautiful sunset");
* const response = await axios.post(imagineUrl, body, config);
*
* @note prompt参数GenerateImagineRequestBody(prompt: string)
*/
GenerateImagineRequestBody(prompt: string): {
body: MJAPIImagineRequestBody
config: any
} {
// 提交API的出图任务
let data = {
botType: this.bootType,
prompt: prompt,
accountFilter: {
modes: [this.mjApiSetting?.apiSpeed == MJSpeed.FAST ? 'FAST' : 'RELAX'],
remark: global.machineId ?? '',
instanceId: ''
} as AccountFilter
}
let config = {
headers: {
'Content-Type': 'application/json'
}
}
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
delete data.accountFilter.remark
delete data.accountFilter.instanceId
config.headers['Authorization'] = this.mjApiSetting?.apiKey
} else {
throw new Error('MJ出图的类型不支持')
}
return {
body: data,
config: config
}
}
/**
* API提交MidJourney图像生成任务
*
* MidJourney API提交图像生成请求
* API请求体并发送API响应
*
*
* - (code=23): RECONNECT"23"
* - 请求失败: 更新任务状态为FAIL
* - 请求成功: 更新任务状态为RUNNINGID
*
* @param {string} taskId - ID
* @param {string} prompt - MidJourney的图像生成提示词
* @returns {Promise<string>} API任务结果ID"23"
* @throws {Error} API返回错误或返回数据为空
*
* @example
* try {
* const taskResultId = await mjApiService.SubmitMJImagineAPI("task-123", "a beautiful sunset");
* if (taskResultId === "23") {
* // 队列已满,需要重试
* } else {
* // 任务提交成功可以用taskResultId查询结果
* }
* } catch (error) {
* console.error("提交任务失败:", error.message);
* }
*/
async SubmitMJImagineAPI(taskId: string, prompt: string): Promise<string> {
// 这边校验是不是在提示词包含不正确的链接
if (prompt.includes('feishu.cn')) {
throw new Error('提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接')
}
let { body, config } = this.GenerateImagineRequestBody(prompt)
// 开始请求
let res = await axios.post(this.imagineUrl, body, config)
let resData = res.data
// if (this.mjGeneralSetting.outputMode == MJImageType.PACKAGE_MJ) {
// if (resData.code == -1 || resData.success == false) {
// throw new Error(resData.message)
// }
// }
if (resData == null) {
throw new Error('返回的数据为空')
}
// 某些API的返回的code为23表示队列已满需要重新请求
if (resData.code == 23) {
this.taskListService.UpdateTaskStatus({
id: taskId,
status: BookBackTaskStatus.RECONNECT
})
return '23'
}
if (resData.code != 1 && resData.code != 22) {
this.taskListService.UpdateTaskStatus({
id: taskId,
status: BookBackTaskStatus.FAIL,
errorMessage: resData.description
})
throw new Error(resData.description)
}
this.taskListService.UpdateTaskStatus({
id: taskId,
status: BookBackTaskStatus.RUNNING
})
return resData.result as string
}
//#endregion
}