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 { 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的任务ID,用于查询API任务状态 * @param {string} backTaskId - 内部系统的任务ID,用于更新本地任务状态记录 * @returns {Promise} 标准化的任务状态响应对象,包含进度、状态、图像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} 成功时返回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 { 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,抛出错误 * - 请求成功: 更新任务状态为RUNNING,返回结果ID * * @param {MJ.APIDescribeParams} param - 包含任务ID和base64编码图像数据的参数对象 * @returns {Promise} 成功时返回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 { // 获取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} 成功时返回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 { 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,抛出错误 * - 请求成功: 更新任务状态为RUNNING,返回结果ID * * @param {string} taskId - 任务ID,用于更新任务状态记录 * @param {string} prompt - 发送给MidJourney的图像生成提示词 * @returns {Promise} 成功时返回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 { // 这边校验是不是在提示词包含不正确的链接 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 }