LaiTool_PRO/src/main/service/mj/mjApiService.ts
lq1405 7a16f02673 V 4.0.1(2025.09.21)
1. 修改场景推理,导入到场景预设时原创界面的自动更新分组错误
2. 文案处理,可单独设置API、密钥、推理设置,没有设置就默认使用推理设置
3. 修改MJ出图的代理模式(添加账号,修改账号,出图)
4. 优化剪映关键帧设置UI界面
5. 修复文案处理的单个清空和批量清空
6. 删除 MJ Video Extend 的尾帧链接
2025-09-21 13:05:02 +08:00

672 lines
24 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 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'
import { define } from '@/define/define'
import { t } from '@/i18n'
/**
* 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
videoUrl!: string
token!: string
constructor() {
super()
this.bootType = 'MID_JOURNEY'
}
//#region InitMJSetting
/**
* 初始化MJ设置
*/
async InitMJSetting(outputMode?: ImageGenerateMode): Promise<void> {
await this.GetMJGeneralSetting()
// 获取当前机器人类型
this.bootType =
this.mjGeneralSetting?.robot == MJRobotType.NIJI ? 'NIJI_JOURNEY' : 'MID_JOURNEY'
if (outputMode) {
this.mjGeneralSetting!.outputMode = outputMode
}
// 再 MJ API 模式下 获取对应的数据
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
await this.GetApiSetting()
if (
!this.mjApiSetting ||
isEmpty(this.mjApiSetting.apiUrl) ||
isEmpty(this.mjApiSetting.apiKey)
) {
throw new Error(t("没有找到对应的API的配置请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
let apiProvider = GetApiDefineDataById(this.mjApiSetting.apiUrl as string)
if (apiProvider.mj_url == null) {
throw new Error(t("当前API不支持MJ出图请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
this.imagineUrl = apiProvider.mj_url.imagine
this.describeUrl = apiProvider.mj_url.describe
this.videoUrl = apiProvider.mj_url.video ?? ''
this.fetchTaskUrl = apiProvider.mj_url.once_get_task
this.token = this.mjApiSetting.apiKey
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_PACKAGE) {
await this.GetMJPackageSetting()
if (
!this.mjPackageSetting ||
isEmpty(this.mjPackageSetting.selectPackage) ||
isEmpty(this.mjPackageSetting.packageToken)
) {
throw new Error(
t("没有找到对应的生图包的配置或配置有误,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
})
)
}
let mjProvider = GetApiDefineDataById(this.mjPackageSetting.selectPackage)
if (!mjProvider.isPackage) {
throw new Error(t("当前选择的包不支持,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
}))
}
if (mjProvider.mj_url == null) {
throw new Error(t("当前选择的包不支持,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
}))
}
this.imagineUrl = mjProvider.mj_url.imagine
this.describeUrl = mjProvider.mj_url.describe
this.videoUrl = mjProvider.mj_url.video ?? ''
this.fetchTaskUrl = mjProvider.mj_url.once_get_task
this.token = this.mjPackageSetting.packageToken
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.LOCAL_MJ) {
await this.GetMjLocalSetting()
if (
this.mjLocalSetting == null ||
isEmpty(this.mjLocalSetting.requestUrl) ||
isEmpty(this.mjLocalSetting.token)
) {
throw new Error(
t("本地代理模式的设置不完善或配置错误,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 本地代理模式')
})
)
}
this.mjLocalSetting.requestUrl.endsWith('/')
? this.mjLocalSetting.requestUrl.slice(0, -1)
: this.mjLocalSetting.requestUrl
this.imagineUrl = this.mjLocalSetting.requestUrl + '/mj/submit/imagine'
this.describeUrl = this.mjLocalSetting.requestUrl + '/mj/submit/describe'
this.videoUrl = this.mjLocalSetting.requestUrl + '/mj/submit/video'
this.fetchTaskUrl = this.mjLocalSetting.requestUrl + '/mj/task/${id}/fetch'
this.token = this.mjLocalSetting.token
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.REMOTE_MJ) {
await this.GetMjRemoteSetting()
this.imagineUrl = define.remotemj_api + 'mj/submit/imagine'
this.describeUrl = define.remotemj_api + 'mj/submit/describe'
this.videoUrl = ""
this.fetchTaskUrl = define.remotemj_api + 'mj/task/${id}/fetch'
this.token = define.remote_token
} else {
throw new Error(t("当前的MJ出图模式不支持请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
}
//#endregion
//#region 获取对应的任务通过ID
/**
* 通过ID获取MidJourney API任务的状态和结果
*
* 该方法向MidJourney API发送请求获取指定任务ID的状态信息包括进度、图像URL和错误信息等。
* 在获取成功后,会将结果格式化为标准响应对象。如果任务失败,则会相应地更新内部任务状态记录。
*
* @param {string} taskId - MidJourney API的任务ID用于查询API任务状态
* @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): Promise<MJ.MJResponseToFront> {
try {
await this.InitMJSetting()
let APIDescribeUrl = this.fetchTaskUrl.replace('${id}', taskId)
// 拼接headers
let headers = {
Authorization: this.token
}
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.LOCAL_MJ) {
headers['mj-api-secret'] = this.token
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.REMOTE_MJ) {
headers['mj-api-secret'] = this.token
}
// 开始请求
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,
imageUrls: resData.imageUrls
? resData.imageUrls
.filter((item) => item.url != null && !isEmpty(item.url))
.map((item) => item.url)
: [],
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(t('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.token
} else {
throw new Error('MJ出图的类型不支持')
}
return {
body: data,
config: config
}
}
/**
* 通过API提交MidJourney图像描述(反推)请求
*
* 该方法发送图像到MidJourney API进行描述(反推)分析将base64编码的图像数据发送到API
* 并处理返回结果。根据API响应状态码会相应地更新任务状态记录并返回结果。
*
* 支持以下特殊响应处理:
* - 队列已满(code=23): 更新任务状态为RECONNECT返回"23"
* - 请求失败: 更新任务状态为FAIL抛出错误
* - 请求成功: 更新任务状态为RUNNING返回结果ID
*
* @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:
case ImageGenerateMode.MJ_PACKAGE:
case ImageGenerateMode.REMOTE_MJ:
case ImageGenerateMode.LOCAL_MJ:
res = await this.SubmitMJImagineAPI(taskId, prompt)
break
default:
throw new Error(t('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'
}
}
let useTransfer = false
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
delete data.accountFilter.remark
delete data.accountFilter.instanceId
config.headers['Authorization'] = this.token
useTransfer = false
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_PACKAGE) {
delete data.accountFilter.remark
delete data.accountFilter.instanceId
delete data.accountFilter.modes
config.headers['Authorization'] = this.token
useTransfer = false
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.LOCAL_MJ) {
delete data.accountFilter.remark
delete data.accountFilter.modes
delete data.accountFilter.instanceId
config.headers['mj-api-secret'] = this.token
useTransfer = false
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.REMOTE_MJ) {
config.headers['mj-api-secret'] = this.token
delete data.accountFilter.modes
delete data.accountFilter.instanceId
useTransfer = this.mjRemoteSetting?.isForward ?? false
} else {
throw new Error(t('不支持的MJ出图类型'))
}
console.log('useTransfer', useTransfer)
return {
body: data,
config: config
}
}
/**
* 通过API提交MidJourney图像生成任务
*
* 该方法向MidJourney API提交图像生成请求并处理返回结果。在提交前会验证提示词是否包含非法链接
* 然后构造API请求体并发送。根据API响应会相应地更新任务状态并返回结果。
*
* 支持以下特殊响应处理:
* - 队列已满(code=23): 更新任务状态为RECONNECT返回"23"
* - 请求失败: 更新任务状态为FAIL抛出错误
* - 请求成功: 更新任务状态为RUNNING返回结果ID
*
* @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(t("提示词里面出现了 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 == ImageGenerateMode.MJ_PACKAGE) {
if (resData.code == -1 || resData.success == false) {
throw new Error(resData.message)
}
}
if (resData == null) {
throw new Error(t('返回的数据为空'))
}
// 某些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
}