1. 初步接入 comfy UI 出图,设定工作流,导出API,直接添加到软件,选中对应的工作流调用出图
2. 文案处理模型改为选择,同步通用设置的模型列表和令牌,添加测试链接功能
3. 添加缓存区图片删除功能
4. 添加垫图/MJ提示词过滤,提示无效的垫图文件(在出图的时候和添加垫图链接的时候,主要过滤飞书链接)
5. 优化聚合推文选图逻辑,没有出过图也能拖拽
6. 优化处理软件所有的加载状态
7. 修复MJ设置初始化问题
This commit is contained in:
lq1405 2025-03-22 10:38:23 +08:00
parent 77ee38d302
commit 6336b36ead
51 changed files with 1900 additions and 133 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "laitool",
"version": "3.2.4",
"version": "3.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "laitool",
"version": "3.2.2",
"version": "3.3.1",
"hasInstallScript": true,
"dependencies": {
"@alicloud/alimt20181012": "^1.2.0",

View File

@ -1,6 +1,6 @@
{
"name": "laitool",
"version": "3.2.5",
"version": "3.3.1",
"description": "An AI tool for image processing, video processing, and other functions.",
"main": "./out/main/index.js",
"author": "laitool.cn",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -80,6 +80,11 @@ const BOOK = {
*/
SD_IMAGE_GENERATE_RETURN: 'SD_IMAGE_GENERATE_RETURN',
/**
* ComfyUI生图返回信息
*/
ComfyUI_IMAGE_GENERATE_RETURN: 'ComfyUI_IMAGE_GENERATE_RETURN',
/**
* D3
*/
@ -119,6 +124,12 @@ const BOOK = {
/** 保存缓存区的屠图片到小说主图或者是选图区 */
SAVE_CACHE_IMAGE_TO_DATA: "SAVE_CACHE_IMAGE_TO_DATA",
/** 删除缓存区中的图片 */
DELETE_CACHE_IMAGE: "DELETE_CACHE_IMAGE",
/** 移动指定图片链接到主图 */
MOVE_IMAGE_TO_MAIN_IMAGE: "MOVE_IMAGE_TO_MAIN_IMAGE",
//#endregion
COMPUTE_STORYBOARD: 'COMPUTE_STORYBOARD',

View File

@ -17,6 +17,8 @@ export enum BookImageCategory {
MJ = 'mj',
// SD
SD = 'sd',
// ComfyUI
ComfyUI = "comfyui",
// D3
D3 = 'd3',
// FLUX API
@ -75,6 +77,8 @@ export enum BookBackTaskType {
MJ_IMAGE = 'mj_image',
// SD 生成图片
SD_IMAGE = 'sd_image',
// ComfyUI 生成图片
ComfyUI_IMAGE = 'comfyui_image',
// flux forge 生成图片
FLUX_FORGE_IMAGE = 'flux_forge_image',
// flux api 生成图片

View File

@ -14,6 +14,8 @@ export enum MJImageType {
// 本地 SD
LOCAL_SD = 'local_sd',
// ComfyUI
ComfyUI = 'comfyui',
// flux-api
FLUX_API = 'flux-api',

View File

@ -56,5 +56,20 @@ export enum OptionKeyName {
FLUX_APIModelList = 'FLUX_APIModelList',
//#endregion
//#region SD/ComfyUI
/**
* ComfyUI
*/
ComfyUI_SimpleSetting = "ComfyUI_SimpleSetting",
/**
* ComfyUI
*/
ComfyUI_WorkFlowSetting = "ComfyUI_WorkFlowSetting"
//#endregion
}

View File

@ -64,6 +64,7 @@ export enum ResponseMessageType {
REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE',// 反推提示词翻译
GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译
MJ_IMAGE = 'MJ_IMAGE',// MJ 生成图片
ComfyUI_IMAGE = 'ComfyUI_IMAGE',// ComfyUI 生成图片
HD_IMAGE = 'HD_IMAGE',// HD 生成图片
RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频
LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频

View File

@ -42,13 +42,17 @@ export const ImageSetting = {
return {
code: 1,
data: [
{
label: 'MJ',
value: 'mj'
},
{
label: 'SD',
value: 'sd'
},
{
label: 'MJ',
value: 'mj'
label: 'ComfyUI',
value: 'comfyui'
},
{
label: 'D3',

View File

@ -299,6 +299,12 @@ export function BookIpc() {
/** 保存缓存区的屠图片到小说主图或者是选图区 */
ipcMain.handle(DEFINE_STRING.BOOK.SAVE_CACHE_IMAGE_TO_DATA, async (event, bookTaskDetailId: string, imageFile: string | string[], option: string) => await bookImage.SaveCacheImageToData(bookTaskDetailId, imageFile, option))
/** 删除缓存区的图片 */
ipcMain.handle(DEFINE_STRING.BOOK.DELETE_CACHE_IMAGE, async (event, bookTaskId: string, imageFile: string) => await bookImage.DeleteCacheImage(bookTaskId, imageFile))
/** 移动指定的图片链接到主图 */
ipcMain.handle(DEFINE_STRING.BOOK.MOVE_IMAGE_TO_MAIN_IMAGE, async (event, bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) => await bookImage.MoveImageToMainImage(bookTaskId, bookTaskDetailId, sourceImagePath))
//#endregion

View File

@ -324,6 +324,9 @@ export class BookImage {
} else if (element.imageCategory == BookImageCategory.SD) {
taskType = BookBackTaskType.SD_IMAGE;
responseMessageName = DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN;
} else if (element.imageCategory == BookImageCategory.ComfyUI) {
taskType = BookBackTaskType.ComfyUI_IMAGE;
responseMessageName = DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN;
} else if (element.imageCategory == BookImageCategory.D3) {
taskType = BookBackTaskType.D3_IMAGE;
responseMessageName = DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN;
@ -792,4 +795,108 @@ export class BookImage {
//#endregion
//#region 删除缓存区中的图片
/**
*
* @param bookTaskId ID
* @param imageFile
* @returns
*/
public async DeleteCacheImage(bookTaskId: string, imageFile: string) {
try {
imageFile = imageFile.split("?t=")[0];
imageFile = imageFile.split("?time=")[0];
if (imageFile.startsWith("file:/")) {
imageFile = imageFile.replace("file:///", "");
imageFile = imageFile.replace("file://", "");
imageFile = imageFile.replace("file:/", "");
}
if (!await CheckFileOrDirExist(imageFile)) {
throw new Error(`图片文件 ${imageFile} 不存在,请检查`)
}
let deleteBaseName = path.basename(imageFile).toLocaleLowerCase();
// 开始查找数据删除
let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId);
// 修改缓存区数据
let cacheImageList = bookTask.cacheImageList ?? [];
for (let i = 0; i < cacheImageList.length; i++) {
const element = cacheImageList[i];
let baseName = path.basename(element).toLocaleLowerCase();
if (deleteBaseName == baseName) {
cacheImageList.splice(i, 1);
break;
}
}
// 删除本地的图片
await fs.promises.unlink(imageFile);
// 修改
await this.bookServiceBasic.UpdetedBookTaskData(bookTaskId, {
cacheImageList: cacheImageList.map((item) => path.relative(define.project_path, item))
})
return successMessage(null, '删除选中缓存区中的图片成功', 'BookImage_DeleteCacheImage')
} catch (error) {
return errorMessage('删除选中缓存区中的图片失败,错误信息如下:' + error.message, 'BookImage_DeleteCacheImage')
}
}
//#endregion
//#region 将指定的图片放到主图中
/**
*
* @param bookTaskDetailId
* @param sourceImagePath
* @returns
*/
public async MoveImageToMainImage(bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) {
try {
let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskId);
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(bookTaskDetailId);
// 获取小说项目的地址
let imageFolder = bookTask.imageFolder;
if (imageFolder == null) {
throw new Error('没有找到对应的小说项目的图片地址,请检查')
}
let currentImagePath = path.join(imageFolder, `${bookTaskDetail.name}.png`);
// 判断地址对应的文件是不是存在
if (await CheckFileOrDirExist(currentImagePath)) {
await fs.promises.unlink(currentImagePath);
}
sourceImagePath = sourceImagePath.split("?t=")[0];
sourceImagePath = sourceImagePath.split("?time=")[0];
if (sourceImagePath.startsWith("file:/")) {
sourceImagePath = sourceImagePath.replace("file:///", "");
sourceImagePath = sourceImagePath.replace("file://", "");
sourceImagePath = sourceImagePath.replace("file:/", "");
}
sourceImagePath = path.resolve(sourceImagePath);
if (!await CheckFileOrDirExist(sourceImagePath)) {
throw new Error(`图片文件 ${sourceImagePath} 不存在,请检查`)
}
// 复制文件,判断父文件夹是不是存在
await CopyFileOrFolder(sourceImagePath, currentImagePath, true);
// 修改数据库数据
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetailId, {
outImagePath: path.relative(define.project_path, currentImagePath)
})
// 返回数据
return successMessage(currentImagePath + `?t=${Date.now()}`, '将指定的图片放到主图中成功', 'BookImage_MoveImageToMainImage')
} catch (error) {
return errorMessage('将指定的图片放到主图中失败,错误信息如下:' + error.message, 'BookImage_MoveImageToMainImage')
}
}
//#endregion
}

View File

@ -289,9 +289,13 @@ class MJApi {
* @param taskId
*/
async SubmitMJImagineAPI(taskId: string, prompt: string): Promise<string> {
let _bookBackTaskListService = await BookBackTaskListService.getInstance()
// 这边校验是不是在提示词包含不正确的链接
if (prompt.includes("feishu.cn")) {
throw new Error("提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接")
}
// 提交API的出图任务
let data = {
botType: this.bootType,
@ -322,7 +326,6 @@ class MJApi {
let resData: any = undefined;
if (useTransfer) {
let url = define.lms + "/lms/Forward/SimpleTransfer"
let transferConfig = {
method: 'post',

View File

@ -0,0 +1,526 @@
import { BookBackTaskListService } from "@/define/db/service/Book/bookBackTaskListService";
import { OptionRealmService } from "@/define/db/service/SoftWare/optionRealmService";
import { OptionKeyName } from "@/define/enum/option";
import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder } from "@/define/Tools/file";
import { ValidateJson } from "@/define/Tools/validate";
import { TaskModal } from "@/model/task";
import { BookServiceBasic } from "@/main/Service/ServiceBasic/bookServiceBasic";
import fs from "fs";
import axios from "axios";
import { isEmpty } from "lodash";
import { BookBackTaskStatus, BookTaskStatus, BookType, MJAction } from "@/define/enum/bookEnum";
import { GeneralResponse } from "@/model/generalResponse";
import { DEFINE_STRING } from "@/define/define_string";
import { ResponseMessageType } from "@/define/enum/softwareEnum";
import { MJImageType, MJRespoonseType } from "@/define/enum/mjEnum";
import { MJ } from "@/model/mj";
import { SendMessageToRenderer } from "../globalService";
import { Book } from "@/model/book/book";
import path from 'path'
import { Base64ToFile } from "@/define/Tools/image";
import { define } from "@/define/define"
export class ComfyUIOpt {
bookServiceBasic: BookServiceBasic
constructor() {
this.bookServiceBasic = new BookServiceBasic()
}
//#region 获取ComfyUI的设置
/**
* ComfyUI的设置
* @returns
*/
private async GetComfyUISetting(): Promise<ComfyUIModel.ComfyUISettingCollection> {
let result = {} as ComfyUIModel.ComfyUISettingCollection;
let optionRealmService = await OptionRealmService.getInstance()
let comfyuiSimpleSettingOption = optionRealmService.GetOptionByKey(OptionKeyName.ComfyUI_SimpleSetting)
if (comfyuiSimpleSettingOption == null) {
throw new Error("未找到ComfyUI的设置请检查是否正确设置");
}
if (!ValidateJson(comfyuiSimpleSettingOption.value)) {
throw new Error("ComfyUI的设置不是有效的JSON格式请检查是否正确设置");
}
result["comfyuiSimpleSetting"] = JSON.parse(comfyuiSimpleSettingOption.value);
let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey(OptionKeyName.ComfyUI_WorkFlowSetting)
if (comfyuiWorkFlowSettingOption == null) {
throw new Error("未找到ComfyUI的工作流设置请检查是否正确设置");
}
if (!ValidateJson(comfyuiWorkFlowSettingOption.value)) {
throw new Error("ComfyUI的工作流设置不是有效的JSON格式请检查是否正确设置");
}
let comfyuiWorkFlowList = JSON.parse(comfyuiWorkFlowSettingOption.value);
result["comfyuiWorkFlowSetting"] = comfyuiWorkFlowList;
if (comfyuiWorkFlowList.length <= 0) {
throw new Error("ComfyUI的工作流设置为空请检查是否正确设置");
}
// 获取选中的工作流
let selectedWorkflow = comfyuiWorkFlowList.find(item => item.id == result.comfyuiSimpleSetting.selectedWorkflow);
if (selectedWorkflow == null) {
throw new Error("未找到选中的工作流,请检查是否正确设置!!");
}
// 判断工作流对应的文件是不是存在
if (!await CheckFileOrDirExist(selectedWorkflow.workflowPath)) {
throw new Error("本地未找到选中的工作流文件地址,请检查是否正确设置!!");
}
result["comfyuiSelectedWorkflow"] = selectedWorkflow;
return result;
}
//#endregion
//#region 组合ComfyUI的请求体
/**
* ComfyUI的请求体
* @param prompt
* @param negativePrompt
* @param workflowPath
*/
private async GetComfyUIAPIBody(prompt: string, negativePrompt: string, workflowPath: string): Promise<string> {
let jsonContentString = await fs.promises.readFile(workflowPath, 'utf-8');
if (!ValidateJson(jsonContentString)) {
throw new Error("工作流文件内容不是有效的JSON格式请检查是否正确设置");
}
let jsonContent = JSON.parse(jsonContentString);
// 判断是否是对象
if (jsonContent !== null && typeof jsonContent === 'object' && !Array.isArray(jsonContent)) {
// 遍历对象属性
for (const key in jsonContent) {
let element = jsonContent[key];
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词') {
jsonContent[key].inputs.text = prompt;
}
if (element._meta?.title === '反向提示词') {
jsonContent[key].inputs.text = negativePrompt;
}
}
if (element && element.class_type === "KSampler") {
const crypto = require('crypto');
const buffer = crypto.randomBytes(8);
let seed = BigInt('0x' + buffer.toString('hex'));
jsonContent[key].inputs.seed = seed.toString();
} else if (element && element.class_type === "KSamplerAdvanced") {
const crypto = require('crypto');
const buffer = crypto.randomBytes(8);
let seed = BigInt('0x' + buffer.toString('hex'));
jsonContent[key].inputs.noise_seed = seed.toString();
}
}
} else {
throw new Error("工作流文件内容不是有效的JSON对象格式请检查是否正确设置");
}
let result = JSON.stringify({
prompt: jsonContent
});
return result;
}
//#endregion
//#region 提交ComfyUI生成图片任务
private async SubmitComfyUIImagine(body: string, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection): Promise<any> {
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1");
if (url.endsWith('/')) {
url = url + "api/prompt"
} else {
url = url + "/api/prompt"
}
var config = {
method: 'post',
url: url,
headers: {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
'Content-Type': 'application/json'
},
data: body
};
let res = await axios(config);
let resData = res.data;
// 判断是不是失败
if (resData.error) {
let errorNode = '';
if (resData.node_errors) {
for (const key in resData.node_errors) {
errorNode += key + ', ';
}
}
let msg = "错误信息:" + resData.error.message + "错误节点:" + errorNode;
throw new Error(msg);
}
// 没有错误 判断是不是成功
if (resData.prompt_id && !isEmpty(resData.prompt_id)) {
// 成功
return resData;
} else {
throw new Error("未知错误未获取到请求ID请检查是否正确设置");
}
}
//#endregion
//#region 获取comfyui出图任务
/**
* ComfyUI出图任务
* @param promptId
* @param comfyUISettingCollection
*/
private async GetComfyUIImageTask(promptId: string, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection): Promise<any> {
if (isEmpty(promptId)) {
throw new Error("未获取到请求ID请检查是否正确设置");
}
if (isEmpty(comfyUISettingCollection.comfyuiSimpleSetting.requestUrl)) {
throw new Error("未获取到ComfyUI的请求地址请检查是否正确设置");
}
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1");
if (url.endsWith('/')) {
url = url + "api/history"
} else {
url = url + "/api/history"
}
var config = {
method: 'get',
url: `${url}/${promptId}`,
headers: {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'
}
};
let res = await axios.request(config);
let resData = res.data;
// 判断状态是失败还是成功
let data = resData[promptId];
if (data == null) {
// 还在执行中 或者是任务不存在
return {
progress: 0,
status: "in_progress",
message: "任务正在执行中"
}
}
let completed = data.status?.completed;
let outputs = data.outputs;
if (completed && outputs) {
let imageNames = [];
for (const key in outputs) {
let outputNode = outputs[key];
if (outputNode && outputNode?.images && outputNode?.images.length > 0) {
for (let i = 0; i < outputNode?.images.length; i++) {
const element = outputNode?.images[i];
imageNames.push(element.filename);
}
}
}
return {
progress: 100,
status: "success",
imageNames: imageNames
}
} else {
return {
progress: 0,
status: "error",
message: "生图失败,详细失败信息看启动器控制台"
}
}
}
//#endregion
//#region 请求下载图片
/**
*
* @param url
* @param path
*/
private async DownloadFileUrl(imageNames: string[], comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection, book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail): Promise<{
outImagePath: string,
subImagePath: string[]
}> {
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace("localhost", "127.0.0.1");
if (url.endsWith('/')) {
url = url + "api/view"
} else {
url = url + "/api/view"
}
let outImagePath = "";
let subImagePath = [];
for (let i = 0; i < imageNames.length; i++) {
const imageName = imageNames[i];
var config = {
method: 'get',
url: `${url}?filename=${imageName}&nocache=${Date.now()}`,
headers: {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'
},
responseType: 'arraybuffer' as 'arraybuffer' // 明确指定类型
};
let res = await axios.request(config);
// 检查响应状态和类型
console.log(`图片下载状态: ${res.status}, 内容类型: ${res.headers['content-type']}`);
// 确保得到的是图片数据
if (!res.headers['content-type']?.includes('image/')) {
console.error(`响应不是图片: ${res.headers['content-type']}`);
continue;
}
let resData = res.data;
console.log(resData);
let SdOriginalImage = path.join(book.bookFolderPath, 'data/SdOriginalImage');
await CheckFolderExistsOrCreate(SdOriginalImage);
let outputFolder = bookTask.imageFolder;
await CheckFolderExistsOrCreate(outputFolder);
let inputFolder = path.join(book.bookFolderPath, 'tmp/input')
await CheckFolderExistsOrCreate(inputFolder);
// 包含info信息的图片地址
let infoImgPath = path.join(SdOriginalImage, `info_${bookTaskDetail.name}_${new Date().getTime()}_${i}.png`)
// 不包含info信息的图片地址
let imgPath = path.join(SdOriginalImage, `${bookTaskDetail.name}_${new Date().getTime()}_${i}.png`)
// 直接将二进制数据写入文件
await fs.promises.writeFile(infoImgPath, Buffer.from(resData));
// 这边去图片信息
// await DeleteFileExifData(path.join(define.package_path, 'exittool/exiftool.exe'), infoImgPath, imgPath);
if (i == 0) {
// 复制到对应的文件夹里面
let outPath = path.join(outputFolder, `${bookTaskDetail.name}.png`)
await CopyFileOrFolder(infoImgPath, outPath)
outImagePath = outPath
}
subImagePath.push(infoImgPath)
}
console.log(outImagePath);
console.log(subImagePath);
// 将获取的数据返回
return {
outImagePath: outImagePath,
subImagePath: subImagePath
}
}
//#endregion
//#region 获取出图任务
async FetchImageTask(task: TaskModal.Task, promptId: string, book: Book.SelectBook, bookTask: Book.SelectBookTask, bookTaskDetail: Book.SelectBookTaskDetail, comfyUISettingCollection: ComfyUIModel.ComfyUISettingCollection) {
while (true) {
try {
let resData = await this.GetComfyUIImageTask(promptId, comfyUISettingCollection);
// 判断他的状态是不是成功
if (resData.status == 'error') {
// 生图失败
await this.bookServiceBasic.UpdateBookTaskDetail(task.bookTaskDetailId, {
status: BookTaskStatus.IMAGE_FAIL,
});
let errorMsg = `MJ生成图片失败失败信息如下${resData.message}`
await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, {
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 100,
category: MJImageType.ComfyUI,
imageClick: "",
imageShow: "",
messageId: promptId,
action: MJAction.IMAGINE,
status: 'error',
message: errorMsg
})
await this.bookServiceBasic.UpdateTaskStatus({
id: task.id,
status: BookBackTaskStatus.FAIL,
errorMessage: errorMsg
});
global.newWindow[0].win.webContents.send(task.messageName, {
code: 0,
message: errorMsg,
data: {
status: 'error',
message: errorMsg,
id: task.bookTaskDetailId
}
})
return;
} else if (resData.status == 'in_progress') {
// 生图中
await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, {
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 0,
category: MJImageType.ComfyUI,
imageClick: "",
imageShow: "",
messageId: promptId,
action: MJAction.IMAGINE,
status: 'running',
message: "任务正在执行中"
})
global.newWindow[0].win.webContents.send(task.messageName, {
code: 1,
message: "running",
data: {
status: 'running',
message: "任务正在执行中",
id: task.bookTaskDetailId
}
})
} else {
let res = await this.DownloadFileUrl(resData.imageNames, comfyUISettingCollection, book, bookTask, bookTaskDetail);
console.log(res);
// 修改数据库数据
// 修改数据库
await this.bookServiceBasic.UpdateBookTaskDetail(bookTaskDetail.id, {
outImagePath: path.relative(define.project_path, res.outImagePath),
subImagePath: res.subImagePath.map((item) => path.relative(define.project_path, item))
})
await this.bookServiceBasic.UpdateTaskStatus({
id: task.id,
status: BookBackTaskStatus.DONE
});
await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, {
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
progress: 100,
category: MJImageType.ComfyUI,
imageClick: "",
imageShow: "",
messageId: promptId,
action: MJAction.IMAGINE,
status: 'success',
message: "ComfyUI 生成图片成功"
})
global.newWindow[0].win.webContents.send(task.messageName, {
code: 1,
message: "ComfyUI 生成图片成功",
data: {
status: 'success',
message: 'ComfyUI 生成图片成功',
id: task.bookTaskDetailId,
outImagePath: res.outImagePath + "?t=" + new Date().getTime(),
subImagePath: res.subImagePath.map((item) => item + "?t=" + new Date().getTime())
}
})
break;
}
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (error) {
throw error;
}
}
}
//#endregion
//#region ComfyUI 生成图片
/**
* ComfyUI
* @param task
*/
async ComfyUIImageGenerate(task: TaskModal.Task) {
try {
let comfyUISettingCollection = await this.GetComfyUISetting();
let bookTaskDetail = await this.bookServiceBasic.GetBookTaskDetailDataById(task.bookTaskDetailId);
let book = await this.bookServiceBasic.GetBookDataById(bookTaskDetail.bookId)
let bookTask = await this.bookServiceBasic.GetBookTaskDataById(bookTaskDetail.bookTaskId)
let prompt = bookTaskDetail.prompt;
let negativePrompt = comfyUISettingCollection.comfyuiSimpleSetting.negativePrompt;
// 开始组合请求体
let body = await this.GetComfyUIAPIBody(prompt, negativePrompt, comfyUISettingCollection.comfyuiSelectedWorkflow.workflowPath);
// 开始发送请求
let resData = await this.SubmitComfyUIImagine(body, comfyUISettingCollection);
// 修改任务状态
await this.bookServiceBasic.UpdateBookTaskDetail(task.bookTaskDetailId, {
status: BookTaskStatus.IMAGE
})
await this.bookServiceBasic.UpdateTaskStatus({
id: task.id,
status: BookBackTaskStatus.RUNNING
})
global.newWindow[0].win.webContents.send(task.messageName, {
code: 0,
message: "任务已提交",
data: {
status: 'submited',
message: '任务已提交',
id: task.bookTaskDetailId
}
})
await this.FetchImageTask(task, resData.prompt_id, book, bookTask, bookTaskDetail, comfyUISettingCollection);
} catch (error) {
let errorMsg = "ComfyUI 生图失败,失败信息如下:" + error.toString()
await this.bookServiceBasic.UpdateTaskStatus({
id: task.id,
status: BookBackTaskStatus.FAIL,
errorMessage: errorMsg
})
await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, {
mjApiUrl: "",
progress: 0,
category: MJImageType.ComfyUI,
imageClick: "",
imageShow: "",
messageId: "",
action: MJAction.IMAGINE,
status: "error",
message: errorMsg,
})
global.newWindow[0].win.webContents.send(task.messageName, {
code: 0,
message: errorMsg,
data: {
status: 'error',
message: errorMsg,
id: task.bookTaskDetailId
}
})
throw error
}
}
//#endregion
}

View File

@ -257,6 +257,10 @@ export class SDOpt {
} else {
url = url + "/sdapi/v1/txt2img"
}
// 替换url中的localhost为127.0.0.1
url = url.replace('localhost', '127.0.0.1');
if (!isEmpty(sdSetting.webui.prompt)) {
prompt = sdSetting.webui.prompt + ', ' + prompt
}
@ -345,8 +349,8 @@ export class SDOpt {
messageId: subImagePath.join(','),
action: MJAction.IMAGINE,
status: "success",
subImagePath: subImagePath,
outImagePath: outImagePath,
subImagePath: subImagePath + "?t=" + new Date().getTime(),
outImagePath: subImagePath.map((item) => item + "?t=" + new Date().getTime()),
message: "SD生成图片成功"
}
await this.bookServiceBasic.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId, resp)

View File

@ -9,6 +9,7 @@ import { Base64ToFile, GetImageBase64 } from "../../define/Tools/image";
import { BookBackTaskStatus } from "../../define/enum/bookEnum";
import { MJAction, MJImageType } from "../../define/enum/mjEnum";
import axios from "axios";
import { TaskModal } from "@/model/task";
export class D3Opt {
bookServiceBasic: BookServiceBasic
gptService: GptService

View File

@ -8,6 +8,7 @@ import { GeneralResponse } from '../../../model/generalResponse'
import { DEFINE_STRING } from '../../../define/define_string'
import { MJOpt } from '../MJ/mj'
import { SDOpt } from '../SD/sd'
import { ComfyUIOpt } from '../SD/comfyui'
import { D3Opt } from '../d3'
import { FluxOpt } from '../Flux/flux'
import { AsyncQueue } from '../../quene'
@ -39,6 +40,8 @@ export class TaskManager {
intervalId: any; // 用于存储 setInterval 的 ID
mjOpt: MJOpt
sdOpt: SDOpt
comfyUIOpt: ComfyUIOpt
d3Opt: D3Opt
fluxOpt: FluxOpt
@ -50,6 +53,7 @@ export class TaskManager {
this.reverseBook = new ReverseBook();
this.mjOpt = new MJOpt();
this.sdOpt = new SDOpt();
this.comfyUIOpt = new ComfyUIOpt();
this.d3Opt = new D3Opt()
this.softWareServiceBasic = new SoftWareServiceBasic();
this.fluxOpt = new FluxOpt()
@ -254,6 +258,17 @@ export class TaskManager {
}, `${batch}_${task.id}`, batch, `${batch}_${task.id}_${new Date().getTime()}`, this.bookServiceBasic.SetMessageNameTaskToFail)
}
/**
* Comfy UI生图任务添加到内存任务中
* @param task
*/
async AddComfyUIImage(task: TaskModal.Task) {
let batch = task.messageName;
global.requestQuene.enqueue(async () => {
await this.comfyUIOpt.ComfyUIImageGenerate(task);
}, `${batch}_${task.id}`, batch, `${batch}_${task.id}_${new Date().getTime()}`, this.bookServiceBasic.SetMessageNameTaskToFail)
}
/**
* D3图像生成任务
*
@ -350,6 +365,9 @@ export class TaskManager {
case BookBackTaskType.SD_IMAGE:
this.AddSDImage(task);
break;
case BookBackTaskType.ComfyUI_IMAGE:
this.AddComfyUIImage(task);
break;
case BookBackTaskType.D3_IMAGE:
this.AddD3Image(task);
break;

43
src/model/comfyui.d.ts vendored Normal file
View File

@ -0,0 +1,43 @@
declare namespace ComfyUIModel {
/** ComfyUI的基础设置的模型 */
interface ComfyUI_SimpleSettingModel {
/** 请求地址 */
requestUrl: string,
/** 选择的工作流 */
selectedWorkflow: string,
/** 反向提示词 */
negativePrompt: string,
}
/** ComfyUI 工作流设置的模型 */
interface ComfyUI_WorkFlowSettingModel {
/** 设置的ID */
id: string,
/** 自定义的名字 */
name: string,
/** 工作流的地址 */
workflowPath: string,
}
/**
* ComfyUI的设置集合
*/
interface ComfyUISettingCollection {
/**
* ComfyUI的基础设置
*/
comfyuiSimpleSetting: ComfyUI_SimpleSettingModel,
/**
* ComfyUI的工作流集合
*/
comfyuiWorkFlowSetting: Array<ComfyUI_WorkFlowSettingModel>,
/**
*
*/
comfyuiSelectedWorkflow: ComfyUI_WorkFlowSettingModel
}
}

View File

@ -109,9 +109,15 @@ declare namespace OptionModel {
/** 超时时间,单位 毫秒 */
timeOut: number
}
//#endregion
//#region SD/MJ
/** ComfyUI的基础设置的模型 */
interface ComfyUI_SimpleSettingModel extends ComfyUIModel.ComfyUI_SimpleSettingModel { };
/** ComfyUI 工作流设置的模型 */
interface ComfyUI_WorkFlowSettingModel extends ComfyUIModel.ComfyUI_WorkFlowSettingModel { };
//#endregion
}

View File

@ -194,6 +194,11 @@ const book = {
/** 保存缓存区的屠图片到小说主图或者是选图区 */
SaveCacheImageToData: async (bookTaskDetailId: string, imageFile: string | string[], option: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.SAVE_CACHE_IMAGE_TO_DATA, bookTaskDetailId, imageFile, option),
/** 删除缓存区中的图片 */
DeleteCacheImage: async (bookTaskId: string, imageFile: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.DELETE_CACHE_IMAGE, bookTaskId, imageFile),
/** 移动指定的图片链接到主图 */
MoveImageToMainImage: async (bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.MOVE_IMAGE_TO_MAIN_IMAGE, bookTaskId, bookTaskDetailId, sourceImagePath),
//#endregion
//#region 一键反推的单个任务

View File

@ -1,6 +1,4 @@
<template>
<n-spin style="z-index: 1000 !important" :show="softwareStore.spin.spinning">
<template #description> {{ softwareStore.spin.tip }} </template>
<n-config-provider
:hljs="hljs"
:theme="softwareStore.globalSetting.theme == 'dark' ? darkTheme : null"
@ -15,11 +13,30 @@
</n-modal-provider>
</n-message-provider>
</n-config-provider>
<n-modal
v-model:show="softwareStore.spin.spinning"
:mask-closable="false"
:close-on-esc="true"
transform-origin="center"
:show-icon="false"
style="background: transparent; box-shadow: none; width: auto"
content-style="padding: 0; background: transparent;"
:bordered="false"
:mask="false"
>
<n-spin :color="'#18a058'">
<template #description>
<span :style="{ color: '#18a058', fontSize: '16px' }">
{{ softwareStore.spin.tip }}
</span>
</template>
</n-spin>
</n-modal>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
import { defineComponent, onMounted } from 'vue'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import {
@ -29,7 +46,8 @@ import {
NModalProvider,
darkTheme,
NNotificationProvider,
NSpin
NSpin,
NModal
} from 'naive-ui'
import { useSoftwareStore } from '../../stores/software'
import { SoftColor } from '../../define/enum/softwareEnum'
@ -42,7 +60,8 @@ export default defineComponent({
NMessageProvider,
NNotificationProvider,
NModalProvider,
NSpin
NSpin,
NModal
},
setup() {
let softwareStore = useSoftwareStore()

View File

@ -2,6 +2,7 @@
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { useOptionStore } from '@/stores/option'
import SDFluxCommon from './SDFluxCommon'
import { OptionModel } from '@/model/option/option'
/**
*
@ -44,7 +45,6 @@ async function InitSpecialCharacters() {
* @returns
*/
async function InitTTSGlobalSetting() {
debugger
let optionStore = useOptionStore()
let initData = {
selectModel: "edge-tts",
@ -96,7 +96,6 @@ async function InitTTSGlobalSetting() {
* @returns
*/
async function InitFluxModelList(isMust: boolean = false): Promise<Array<{ label: string, value: string }>> {
debugger
let initData = []
if (!isMust) {
let FLUX_APIModelListData = await window.options.GetOptionByKey(OptionKeyName.FLUX_APIModelList);
@ -136,9 +135,78 @@ async function InitFluxModelList(isMust: boolean = false): Promise<Array<{ label
}
}
/**
* ComfyUI
* @returns
*/
async function InitComfyUISetting() {
let optionStore = useOptionStore()
let initSimpleSetting: OptionModel.ComfyUI_SimpleSettingModel = {
requestUrl: "",
selectedWorkflow: null,
negativePrompt: ""
};
let initWorkFlowSetting: Array<OptionModel.ComfyUI_WorkFlowSettingModel> = [];
// 初始化基础设置
let ComfyUI_SimpleSetting = await window.options.GetOptionByKey(
OptionKeyName.ComfyUI_SimpleSetting
)
if (ComfyUI_SimpleSetting.code == 0) {
// 获取失败
window.api.showGlobalMessageDialog(ComfyUI_SimpleSetting)
return
}
if (ComfyUI_SimpleSetting.data == null) {
// 没有数据初始化
let saveRes = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_SimpleSetting,
JSON.stringify(initSimpleSetting),
OptionType.JOSN
)
if (saveRes.code == 0) {
window.api.showGlobalMessageDialog(saveRes)
return
} else {
optionStore.ComfyUI_SimpleSetting = initSimpleSetting
}
} else {
optionStore.ComfyUI_SimpleSetting = JSON.parse(ComfyUI_SimpleSetting.data.value)
}
// 初始化工作流设置
let ComfyUI_WorkFlowSetting = await window.options.GetOptionByKey(
OptionKeyName.ComfyUI_WorkFlowSetting
)
if (ComfyUI_WorkFlowSetting.code == 0) {
// 获取失败
window.api.showGlobalMessageDialog(ComfyUI_WorkFlowSetting)
return
}
if (ComfyUI_WorkFlowSetting.data == null) {
// 没有数据初始化
let saveRes = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_WorkFlowSetting,
JSON.stringify(initWorkFlowSetting),
OptionType.JOSN
)
if (saveRes.code == 0) {
window.api.showGlobalMessageDialog(saveRes)
return
} else {
optionStore.ComfyUI_WorkFlowSetting = initWorkFlowSetting
}
} else {
optionStore.ComfyUI_WorkFlowSetting = JSON.parse(ComfyUI_WorkFlowSetting.data.value)
}
}
let InitCommon = {
InitSpecialCharacters,
InitTTSGlobalSetting,
InitFluxModelList
InitFluxModelList,
InitComfyUISetting
}
export default InitCommon

View File

@ -26,8 +26,10 @@
v-for="(image, index) in item.imagePaths"
:key="image.id"
:id="image.id"
:bookname="item.bookTaskName"
class="image-container"
@click="SelectImage(image, index)"
@contextmenu="handleContextMenu"
>
<n-image
:width="150"
@ -43,6 +45,16 @@
</n-image-group>
</n-collapse-item>
</n-collapse>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="x"
:y="y"
:options="options"
:show="showDropdown"
:on-clickoutside="onClickoutside"
@select="handleSelect"
/>
<div style="margin-top: 15px; display: flex; justify-content: flex-end">
<n-button type="info" @click="SaveImageToData">选择完成</n-button>
</div>
@ -51,12 +63,23 @@
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { NImageGroup, NImage, NSpin, NButton, useMessage, NCollapse, NCollapseItem } from 'naive-ui'
import { onMounted, ref, nextTick, h } from 'vue'
import {
NImageGroup,
NImage,
NSpin,
NButton,
useMessage,
NCollapse,
NCollapseItem,
NDropdown,
NIcon
} from 'naive-ui'
import { useReverseManageStore } from '@/stores/reverseManage'
import { v4 as uuidv4 } from 'uuid'
import { MJImageType } from '@/define/enum/mjEnum'
import { MJAction } from '@/define/enum/bookEnum'
import { TrashBinOutline } from '@vicons/ionicons5'
let reverseManageStore = useReverseManageStore()
let props = defineProps({
@ -71,6 +94,94 @@ let show = ref(true)
let defaultExpandedNames = ref([])
let selectImageIds = ref([])
let deleteImage = ref(null)
let showDropdown = ref(false)
let x = ref(0)
let y = ref(0)
const options = [
{
label: () => h('span', { style: { color: 'red' } }, `删除`),
key: 'delete',
icon: () => h(NIcon, { color: 'red' }, { default: () => h(TrashBinOutline) })
}
]
async function handleSelect(key) {
showDropdown.value = false
if (key == 'delete') {
if (deleteImage.value == null) {
message.error('未找到对应的被删除的图片数据,请检查!!')
return
}
//
let res = await window.book.DeleteCacheImage(
deleteImage.value.booktaskId,
deleteImage.value.imagePath
)
if (res.code != 1) {
message.error(res.message)
return
}
//
//
let currentBookIndex = data.value.findIndex((x) => x.bookTaskId == deleteImage.value.booktaskId)
if (currentBookIndex != -1) {
let currentBooks = data.value[currentBookIndex]
let findIndex = currentBooks.imagePaths.findIndex((x) => x.id == deleteImage.value.id)
if (findIndex != -1) {
currentBooks.imagePaths.splice(findIndex, 1)
}
if (currentBooks.imagePaths <= 0) {
data.value.splice(currentBookIndex, 1)
}
}
message.success(res.message)
} else {
message.error('未找到对应的操作,请检查')
}
}
function handleContextMenu(e) {
e.preventDefault()
showDropdown.value = false
debugger
let bookname = e.target.getAttribute('bookname')
if (bookname == null) {
message.error('未找到对应的批次数据名称,请检查')
return
}
let imageId = e.target.id
if (imageId == null) {
message.error('未找到对应的图片ID请检查')
return
}
let currentBooks = data.value.find((x) => x.bookTaskName == bookname)
if (currentBooks == null) {
message.error('未找到对应的批次数据,请检查')
return
}
let cacheImageList = currentBooks.imagePaths
if (cacheImageList.length <= 0) {
message.error('未找到对应的图片数据,请检查')
return
}
let imageIndex = cacheImageList.findIndex((x) => x.id == imageId)
if (imageIndex == -1) {
message.error('未找到对应的图片数据,请检查')
return
}
deleteImage.value = { ...cacheImageList[imageIndex], booktaskId: currentBooks.bookTaskId }
nextTick().then(() => {
showDropdown.value = true
x.value = e.clientX
y.value = e.clientY
})
}
function onClickoutside() {
showDropdown.value = false
}
onMounted(async () => {
try {
@ -80,7 +191,6 @@ onMounted(async () => {
message.error(res.message)
return
}
console.log('GetAllBookTaskImageCache', res.data)
// ID
const specifiedId = reverseManageStore.selectBookTask.id // ID
res.data.sort((a, b) =>
@ -215,7 +325,6 @@ async function SaveImageToData() {
selectImagePaths[0],
props.type
)
console.log('SaveCacheImageToData', res)
if (res.code == 0) {
message.error(res.message)
return

View File

@ -201,6 +201,21 @@ export default defineComponent({
? characterData.value.show_image.split('?t')[0]
: null
//
if (characterData.value.image_url != null && !isEmpty(characterData.value.image_url)) {
//
//
let trimmedUrl = characterData.value.image_url.trim()
if (trimmedUrl.startsWith('--cref')) {
message.error('风格垫图地址不能以 --cref 开头,请删除!!!')
return
}
if (trimmedUrl.includes('feishu.cn')) {
message.error('垫图链接不能包含 feishu.cn 飞书的链接,请正确的复制图片链接!!!')
return
}
}
await window.mj.SaveTagPropertyData(
[JSON.stringify(characterData.value), 'character_tags'],
(value) => {

View File

@ -36,7 +36,10 @@
</n-tooltip>
</div>
</n-form-item>
<div style="color: red;">注意风格提示词描述英文 才是实际合并的提示词数据请和出图的提示词语言保持一致中文或英文</div>
<div style="color: red">
注意风格提示词描述英文
才是实际合并的提示词数据请和出图的提示词语言保持一致中文或英文
</div>
<n-form-item label="风格提示词描述(英文)" path="prompt">
<n-input
type="textarea"
@ -89,7 +92,7 @@
</template>
<script>
import { ref, h, onMounted, defineComponent, toRaw, watch } from 'vue'
import { ref, onMounted, defineComponent, toRaw, watch } from 'vue'
import {
NImage,
useMessage,
@ -145,15 +148,32 @@ export default defineComponent({
async function SaveStyleTag(e) {
e.preventDefault()
formRef.value?.validate(async (errors) => {
debugger
if (errors) {
message.error('请检查必填字段')
return
}
styleData.value['type'] = 'style_main'
styleData.value['show_image'] = styleData.value.show_image
? styleData.value.show_image.split('?t')[0]
: null
//
//
if (styleData.value.image_url != null && !isEmpty(styleData.value.image_url)) {
//
//
let trimmedUrl = styleData.value.image_url.trim()
if (trimmedUrl.startsWith('--sref')) {
message.error('风格垫图地址不能以 --sref 开头,请删除!!!')
return
}
if (trimmedUrl.includes('feishu.cn')) {
message.error('垫图链接不能包含 feishu.cn 飞书的链接,请正确的复制图片链接!!!')
return
}
}
//
await window.mj.SaveTagPropertyData(
[JSON.stringify(styleData.value), 'style_tags'],
@ -248,7 +268,6 @@ export default defineComponent({
}
imageLoading.value = true
await window.sd.txt2img(JSON.stringify([d]), async (value) => {
if (value.code == 0) {
message.error(value.message)
imageLoading.value = false

View File

@ -292,12 +292,8 @@ async function imageDragStart(e) {
//
async function imageDragDrop(e) {
debugger
console.log('end', e.target, dragTarget)
//
// if (dragTarget == null) {
// message.error('')
// return
// }
//
if (e.target.getAttribute('mainImage') != '1') {
message.error('图片只能拖拽到主图片上')
@ -307,19 +303,33 @@ async function imageDragDrop(e) {
.split('?')[0]
.replace(/^file:\/\/\//, '')
.replace(/^\//, '')
let target_img = e.target.src
.split('?')[0]
.replace(/^file:\/\/\//, '')
.replace(/^\//, '')
// let target_img = e.target.src
// .split('?')[0]
// .replace(/^file:\/\/\//, '')
// .replace(/^\//, '')
//
await window.api.DownloadImageFile([source_img, 'replace', target_img], (value) => {
if (value.code == 0) {
message.error(value.message)
let res = await window.book.MoveImageToMainImage(
data.value.bookTaskId,
data.value.id,
decodeURI(source_img)
)
if (res.code == 0) {
message.error(res.message)
return
}
data.value.outImagePath =
e.target.src.split('?')[0].replaceAll('\\', '/') + '?time=' + new Date().getTime()
})
data.value.outImagePath = res.data
message.success(res.message)
// await window.api.DownloadImageFile([source_img, 'replace', target_img], (value) => {
// if (value.code == 0) {
// message.error(value.message)
// return
// }
// data.value.outImagePath =
// e.target.src.split('?')[0].replaceAll('\\', '/') + '?time=' + new Date().getTime()
// })
}
//
@ -542,6 +552,7 @@ function renderIcon(icon) {
)
}
}
let mainImageOptions = []
function GetMainImageOptions() {
let t = [

View File

@ -137,7 +137,7 @@ export default defineComponent({
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.SD) {
type = 'sd_merge'
} else {
type = 'mj_merge'
type = 'sd_merge'
}
}
//

View File

@ -82,7 +82,7 @@ async function MergePrompt(key) {
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.SD) {
type = 'sd_merge'
} else {
type = 'mj_merge'
type = 'sd_merge'
}
}
//
@ -108,6 +108,7 @@ async function MergePrompt(key) {
//
async function SingleGenerateImage() {
message.info('1')
let res = undefined
if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.MJ) {
res = await window.mj.AddMJGenerateImageTask(
@ -125,6 +126,16 @@ async function SingleGenerateImage() {
data.value.id,
DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN
)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
// SD
res = await window.task.AddBookBackTask(
reverseManageStore.selectBook.id,
BookBackTaskType.ComfyUI_IMAGE,
TaskExecuteType.AUTO,
reverseManageStore.selectBookTask.id,
data.value.id,
DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
res = await window.task.AddBookBackTask(
reverseManageStore.selectBook.id,
@ -226,6 +237,20 @@ async function UnderGenerateImage() {
})
}
res = await window.task.AddMultiBookBackTask(tasksList)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
let tasksList = []
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
tasksList.push({
bookId: reverseManageStore.selectBook.id,
type: BookBackTaskType.ComfyUI_IMAGE,
executeType: TaskExecuteType.AUTO,
bookTaskId: reverseManageStore.selectBookTask.id,
bookTaskDetailId: element.id,
messageName: DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
})
}
res = await window.task.AddMultiBookBackTask(tasksList)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
let tasksList = []
for (let i = 0; i < bookTaskDetails.length; i++) {

View File

@ -806,6 +806,9 @@ async function AddFluxImageAll(type, cover) {
} else if (type == BookImageCategory.D3) {
messageName = DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.D3_IMAGE
} else if (type == BookImageCategory.ComfyUI) {
messageName = DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.ComfyUI_IMAGE
} else if (type == BookImageCategory.SD) {
messageName = DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.SD_IMAGE
@ -899,6 +902,8 @@ async function GenerateImageAll(cover = true) {
await AddFluxImageAll(BookImageCategory.FLUX_API, cover)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
await AddFluxImageAll(BookImageCategory.D3, cover)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
await AddFluxImageAll(BookImageCategory.ComfyUI, cover)
} else {
message.error('不支持的生图类型')
return

View File

@ -29,7 +29,7 @@
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
import { useMessage, NTabPane, NTabs, NCard } from 'naive-ui'
import MJSettingHome from '../../Setting/MJSetting/MJSettingHome.vue'
import SDSetting from '../../Setting/SDSetting.vue'
import SDSetting from '../../Setting/SDSetting/SDSetting.vue'
import VideoGenerateSetting from '../../Setting/VideoGenerateSetting.vue'
import WatermarkSetting from '../../Setting/WatermarkSetting.vue'
import SubtitleSetting from '../../Setting/SubtitleSetting.vue'

View File

@ -73,6 +73,11 @@ onMounted(async () => {
OriginalSDImageResponseReturn(value)
})
// ComfyUI
window.api.setEventListen([DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN], (value) => {
OriginalSDImageResponseReturn(value)
})
// FLUX API
window.api.setEventListen([DEFINE_STRING.BOOK.FLUX_API_IMAGE_GENERATE_RETURN], (value) =>
OriginalSDImageResponseReturn(value)
@ -178,6 +183,7 @@ onUnmounted(() => {
window.api.removeEventListen(DEFINE_STRING.BOOK.REVERSE_PROMPT_TRANSLATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.FLUX_API_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.FLUX_FORGE_IMAGE_GENERATE_RETURN)
})

View File

@ -130,7 +130,7 @@ async function MergePrompt(type = null) {
let image_generate_category = reverseManageStore.selectBookTask.imageCategory
let mergeType = 'mj_merge'
if (!type) {
if (image_generate_category == 'sd') {
if (image_generate_category == 'sd' || image_generate_category == 'comfyui') {
mergeType = 'sd_merge'
} else if (image_generate_category == 'mj') {
mergeType = 'mj_merge'

View File

@ -137,6 +137,16 @@ async function SingleGenerateImage() {
row.value.id,
DEFINE_STRING.BOOK.SD_IMAGE_GENERATE_RETURN
)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
// SD
res = await window.task.AddBookBackTask(
reverseManageStore.selectBook.id,
BookBackTaskType.ComfyUI_IMAGE,
TaskExecuteType.AUTO,
reverseManageStore.selectBookTask.id,
row.value.id,
DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
res = await window.task.AddBookBackTask(
reverseManageStore.selectBook.id,
@ -238,6 +248,20 @@ async function NextGenerateImage() {
})
}
res = await window.task.AddMultiBookBackTask(tasksList)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
let tasksList = []
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
tasksList.push({
bookId: reverseManageStore.selectBook.id,
type: BookBackTaskType.ComfyUI_IMAGE,
executeType: TaskExecuteType.AUTO,
bookTaskId: reverseManageStore.selectBookTask.id,
bookTaskDetailId: element.id,
messageName: DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
})
}
res = await window.task.AddMultiBookBackTask(tasksList)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
let tasksList = []
for (let i = 0; i < bookTaskDetails.length; i++) {

View File

@ -387,6 +387,9 @@ async function AddFluxImageAll(type, cover) {
} else if (type == BookImageCategory.FLUX_API) {
messageName = DEFINE_STRING.BOOK.FLUX_API_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.FLUX_API_IMAGE
} else if (type == BookImageCategory.ComfyUI) {
messageName = DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.ComfyUI_IMAGE
} else if (type == BookImageCategory.D3) {
messageName = DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN
taskType = BookBackTaskType.D3_IMAGE
@ -473,6 +476,8 @@ async function GenerateImageAll(cover = true) {
await AddFluxImageAll(BookImageCategory.FLUX_API, cover)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.D3) {
await AddFluxImageAll(BookImageCategory.D3, cover)
} else if (reverseManageStore.selectBookTask.imageCategory == BookImageCategory.ComfyUI) {
await AddFluxImageAll(BookImageCategory.ComfyUI, cover)
} else {
message.error('不支持的生图类型')
return

View File

@ -78,7 +78,7 @@ function OriginalMJImageResponseReturn(value) {
// SD
function OriginalSDImageResponseReturn(value) {
console.log('OriginalSDImageResponseReturn', value)
console.log('OriginalSDImageResponseReturn111111', value)
//
if (value.data) {
let dataIndex = reverseManageStore.selectBookTaskDetail.findIndex(
@ -193,6 +193,10 @@ onMounted(async () => {
OriginalSDImageResponseReturn(value)
})
window.api.setEventListen([DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN], (value) => {
OriginalSDImageResponseReturn(value)
})
window.api.setEventListen([DEFINE_STRING.BOOK.RUNWAY_IMAGE_TO_VIDEO_RETURN], (value) => {
VideoImageToVideoReturn(value)
})
@ -205,6 +209,7 @@ onUnmounted(() => {
window.api.removeEventListen(DEFINE_STRING.BOOK.FLUX_FORGE_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.FLUX_API_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.D3_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.ComfyUI_IMAGE_GENERATE_RETURN)
window.api.removeEventListen(DEFINE_STRING.BOOK.RUNWAY_IMAGE_TO_VIDEO_RETURN)
})
</script>

View File

@ -1,42 +1,136 @@
<template>
<n-space vertical>
<n-spin :show="loading">
<n-card title="LAI API 设置">
<div style="display: flex">
<n-input
v-model:value="optionStore.CW_AISetting.laiapi.gpt_url"
style="margin-right: 10px"
style="margin-right: 10px; flex: 1"
type="text"
placeholder="请输入GPT URL"
/>
<n-input
v-model:value="optionStore.CW_AISetting.laiapi.api_key"
style="margin-right: 10px"
style="margin-right: 10px; flex: 1"
type="text"
placeholder="请输入API KEY"
/>
<n-input
<n-select
v-model:value="optionStore.CW_AISetting.laiapi.model"
type="text"
placeholder="请输入Model"
:options="gpt_model_options"
placeholder="请选择Model"
style="flex: 1"
/>
<n-button
type="info"
style="margin-left: 10px; white-space: nowrap"
@click="testConnection"
>
测试连接
</n-button>
</div>
</n-card>
<div style="display: flex; justify-content: flex-end; margin: 20px">
<n-button type="primary" @click="SaveAISetting">保存</n-button>
<n-button
type="info"
style="margin-right: 20px; white-space: nowrap"
@click="syncGeneralSettings"
>
同步通用设置Key
</n-button>
<n-button type="info" @click="SaveAISetting">保存</n-button>
</div>
</n-spin>
</n-space>
</template>
<script setup>
import { useMessage, useDialog, NCard, NSpace, NInput, NSelect, NButton } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useMessage, useDialog, NCard, NSpace, NInput, NSelect, NButton, NSpin } from 'naive-ui'
import { isEmpty } from 'lodash'
import { useOptionStore } from '@/stores/option'
import { OptionKeyName, OptionType } from '@/define/enum/option'
let gpt_model_options = ref([])
let loading = ref(false)
let optionStore = useOptionStore()
let message = useMessage()
let dialog = useDialog()
onMounted(async () => {
await window.api.getGptModelOption('all', (value) => {
if (value.code == 0) {
message.error(value.message)
return
}
gpt_model_options.value = value.data
console.log(gpt_model_options.value)
})
})
/**
* 测试GPT链接是不是可以成功
*/
async function testConnection() {
loading.value = true
//
await window.api.TestGPTConnection(
JSON.stringify({
gpt_business: optionStore.CW_AISetting.laiapi.gpt_url + '/v1/chat/completions',
gpt_key: optionStore.CW_AISetting.laiapi.api_key,
gpt_model: optionStore.CW_AISetting.laiapi.model
}),
(value) => {
loading.value = false
if (value.code == 0) {
window.api.showGlobalMessageDialog({
code: 0,
message: 'GPT链接失败错误信息 ' + value.message
})
return
}
window.api.showGlobalMessageDialog({
code: 1,
message: 'gpt链接测试成功请保存数据后使用'
})
}
)
}
/**
* 同步通用设置中的数据信息
*/
async function syncGeneralSettings() {
// Create confirmation dialog before sync
const syncDialog = dialog.warning({
title: '同步确认',
content: '确认要同步通用设置中的API Key和Model吗这将覆盖当前设置。',
positiveText: '确认',
negativeText: '取消',
onNegativeClick: () => {
message.info('已取消同步')
return true
},
onPositiveClick: async () => {
syncDialog.destroy()
// Get global settings
let globalSetting = window.config
if (!globalSetting) {
message.error('未找到全局通用设置,请检查通用设置!')
return
}
// Sync settings
optionStore.CW_AISetting.laiapi.api_key = globalSetting.gpt_key
optionStore.CW_AISetting.laiapi.model = globalSetting.gpt_model
window.api.showGlobalMessageDialog({
code: 1,
message: '已同步通用设置,请测试成功后保存后使用!'
})
}
})
}
async function SaveAISetting() {
let da = dialog.warning({
title: '提示',

View File

@ -110,6 +110,7 @@ export default defineComponent({
* 单句生图
*/
async function SingleGenerateImage() {
message.info("3")
await window.api.OriginalSDImageGenerate([JSON.stringify([row]), true, false], (value) => {
if (value.code == 0) {
message.error(value.message)

View File

@ -112,6 +112,7 @@ export default defineComponent({
*/
async function SingleGenerateImage() {
//
message.info("5")
if (row.value.imageLock) {
message.error('当前数据已经被锁定,无法生图')
return

View File

@ -160,6 +160,7 @@ import { v4 as uuidv4 } from 'uuid'
import MJDefine from '@/main/Service/MJ/mjDefine'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { ValidateJson } from '@/define/Tools/validate'
import { useOptionStore } from '@/stores/option'
export default defineComponent({
components: {
@ -196,6 +197,7 @@ export default defineComponent({
})
let request_model_options = ref([])
let mj_speed_options = ref([])
let optionStore = useOptionStore()
/**
* 初始化GPT的option
@ -238,6 +240,7 @@ export default defineComponent({
* 初始化MJ的option
*/
async function InitMjOptions() {
debugger
let mjSettingData = await window.options.GetOptionByKey(OptionKeyName.MJ_GlobalSetting)
if (mjSettingData.code == 0) {
throw new Error('加载MJ设置失败失败原因如下' + mjSettingData.message)
@ -250,6 +253,7 @@ export default defineComponent({
}
mj_globalSetting = JSON.parse(mjSettingData.data.value)
mj_globalSetting = Object.assign(optionStore.MJ_GlobalSetting, mj_globalSetting)
let mjSimpleSetting = mj_globalSetting.mj_simpleSetting
mjSetting.value = Object.assign(mjSetting.value, mjSimpleSetting)
@ -319,8 +323,6 @@ export default defineComponent({
throw new Error(value.message)
}
})
debugger
// MJ
let saveMjRes = await window.options.ModifyOptionByKey(
OptionKeyName.MJ_GlobalSetting,

View File

@ -66,7 +66,6 @@ async function InitMJSettingData() {
optionStore.MJ_GlobalSetting = JSON.parse(mjRes.data.value)
message.success('加载MJSetting信息成功')
}
await TimeDelay(1000)
} catch (error) {
window.api.showGlobalMessageDialog({
@ -82,8 +81,6 @@ async function InitMJSettingData() {
* 保存MJ配置
*/
async function SaveMjSetting() {
//
debugger
//
let request_model = optionStore.MJ_GlobalSetting.mj_simpleSetting.requestModel
if (request_model == MJImageType.API_MJ) {

View File

@ -0,0 +1,250 @@
<template>
<div class="add-comfy-ui-workflow">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="100"
require-mark-placement="right-hanging"
>
<n-form-item label="名称" path="name">
<n-input v-model:value="formValue.name" placeholder="输入工作流名称" />
</n-form-item>
<n-form-item label="工作流文件" path="jsonFile">
<n-input v-model:value="formValue.workflowPath" placeholder="选择工作流API文件" readonly />
<n-button @click="triggerFileSelect" type="primary" style="margin-left: 20px">
选择文件
</n-button>
</n-form-item>
</n-form>
<div class="button-group">
<n-space justify="end" align="center">
<n-button @click="checkWorkflowFile" :disabled="!jsonContent" secondary>
检查工作流文件
</n-button>
<n-button
type="primary"
@click="saveWorkflow"
:disabled="!formValue.name || !formValue.workflowPath"
>
{{ isEdit ? '更新' : '保存' }}
</n-button>
</n-space>
</div>
<input
type="file"
ref="fileInput"
accept=".json"
style="display: none"
@change="handleFileUpload"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { NForm, NFormItem, NInput, NButton, NSpace, useMessage } from 'naive-ui'
import { useOptionStore } from '@/stores/option'
import { OptionKeyName, OptionType } from '@/define/enum/option'
const fileInput = ref(null)
const jsonContent = ref(null)
const formRef = ref(null)
const message = useMessage()
const optionStore = useOptionStore()
const formValue = ref({
id: null,
name: '',
workflowPath: null
})
const rules = {
name: {
required: true,
message: '请输入工作流名称',
trigger: 'blur'
},
jsonFile: {
required: true,
message: '请选择选择工作流API文件',
trigger: 'change'
}
}
// props - workflowData
const props = defineProps({
workflowData: {
type: Object,
default: null
}
})
const isEdit = ref(false)
//
onMounted(() => {
if (props.workflowData) {
isEdit.value = true
formValue.value = {
id: props.workflowData.id,
name: props.workflowData.name,
workflowPath: props.workflowData.workflowPath
}
}
})
async function saveWorkflow() {
try {
//
const isDuplicate = optionStore.ComfyUI_WorkFlowSetting.some(
(item) =>
item.name === formValue.value.name && (!isEdit.value || item.id !== formValue.value.id)
)
if (isDuplicate) {
message.error('工作流名称已存在,请重新输入')
return
}
if (isEdit.value) {
//
const index = optionStore.ComfyUI_WorkFlowSetting.findIndex(
(item) => item.id === formValue.value.id
)
if (index !== -1) {
optionStore.ComfyUI_WorkFlowSetting[index] = {
id: formValue.value.id,
name: formValue.value.name,
workflowPath: formValue.value.workflowPath
}
}
} else {
//
optionStore.ComfyUI_WorkFlowSetting.push({
id: crypto.randomUUID(),
name: formValue.value.name,
workflowPath: formValue.value.workflowPath
})
}
//
let res = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_WorkFlowSetting,
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
OptionType.JOSN
)
if (res.code == 1) {
message.success(isEdit.value ? '更新成功' : '保存成功')
//
if (!isEdit.value) {
formValue.value = {
name: '',
workflowPath: null,
id: null
}
jsonContent.value = null
}
} else {
message.error((isEdit.value ? '更新' : '保存') + '失败,失败原因:' + res.message)
}
} catch (error) {
message.error((isEdit.value ? '更新' : '保存') + '失败,失败原因:' + error.message)
}
}
const triggerFileSelect = () => {
fileInput.value.click()
}
/**
* 选择并读取json文件
* @param event
*/
const handleFileUpload = (event) => {
const file = event.target.files[0]
if (!file) return
formValue.value.workflowPath = file.path
const reader = new FileReader()
reader.onload = (e) => {
try {
jsonContent.value = JSON.parse(e.target.result)
} catch (error) {
console.error('解析JSON失败:', error)
message.error('无法解析JSON文件请确保文件格式正确')
jsonContent.value = null
formValue.value.jsonFile = null
}
}
reader.readAsText(file)
}
/**
* 检查工作流文件是不是正确
*/
async function checkWorkflowFile() {
if (formValue.value.workflowPath == null) {
message.error('请先选择工作流文件')
return
}
if (!jsonContent.value) {
message.error('工作流文件内容为空,请选择工作流文件')
return
}
console.log(jsonContent.value)
let hasPositivePrompt = false
let hasNegativePrompt = false
//
let elements = []
// ComfyUI
if (Array.isArray(jsonContent.value)) {
message.error('工作流文件的格式不正确,请检查工作流文件')
} else if (typeof jsonContent.value === 'object') {
// ComfyUInodes
if (jsonContent.value.nodes) {
elements = Object.values(jsonContent.value.nodes)
} else {
elements = Object.values(jsonContent.value)
}
}
for (const element of elements) {
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词') {
hasPositivePrompt = true
}
if (element._meta?.title === '反向提示词') {
hasNegativePrompt = true
}
}
}
if (!hasPositivePrompt || !hasNegativePrompt) {
message.error(
'工作流文件缺少正向提示词或反向提示词,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词!!'
)
return
} else {
message.success('工作流文件检查成功通过')
}
}
</script>
<style scoped>
.add-comfy-ui-workflow {
padding: 20px;
max-width: 600px;
}
</style>

View File

@ -0,0 +1,313 @@
<template>
<div class="comfy-ui-setting">
<div class="form-section">
<h3 class="section-title">ComfyUI 基础设置</h3>
<n-form inline :model="optionStore.ComfyUI_SimpleSetting" class="inline-form">
<n-form-item label="请求地址" path="requestUrl">
<n-input
v-model:value="optionStore.ComfyUI_SimpleSetting.requestUrl"
placeholder="输入请求地址"
/>
</n-form-item>
<n-form-item label="当前工作流" path="selectedWorkflow">
<n-select
placeholder="选择工作流"
v-model:value="optionStore.ComfyUI_SimpleSetting.selectedWorkflow"
:options="
optionStore.ComfyUI_WorkFlowSetting.map((workflow) => ({
label: workflow.name,
value: workflow.id
}))
"
style="width: 200px"
/>
</n-form-item>
<n-form-item label="反向提示词" path="negativePrompt">
<n-input
v-model:value="optionStore.ComfyUI_SimpleSetting.negativePrompt"
style="width: 300px"
placeholder="输入反向提示词"
/>
</n-form-item>
<n-form-item>
<n-button type="info" style="margin-left: 8px" @click="SaveComfyUISimpleSetting">
保存设置
</n-button>
</n-form-item>
</n-form>
</div>
<div style="color: red">
<p>注意</p>
<p>1 Comfy UI的工作流中正向提示词和反向提示必须为 <strong>Clip文本编码</strong> 节点</p>
<p>2 标题必须对应 <strong>正向提示词和反向提示词</strong></p>
<p>
3 图像输出节点必须是 <strong>保存图像</strong> 节点<strong
>采样器只支持简单 K采样器和K采样器高级</strong
>
</p>
</div>
<div class="action-bar">
<n-button type="primary" @click="handleAdd">添加</n-button>
</div>
<div ref="tableContainer" class="table-container">
<n-data-table
:columns="columns"
:data="optionStore.ComfyUI_WorkFlowSetting"
:bordered="true"
:single-line="false"
:max-height="tableHeight"
/>
</div>
</div>
</template>
<script setup>
import { ref, h, onMounted, onUnmounted, nextTick } from 'vue'
import {
NDataTable,
NButton,
useDialog,
NForm,
NFormItem,
NInput,
useMessage,
NSelect,
NSpace,
NPopconfirm,
NIcon
} from 'naive-ui'
//
import { useSoftwareStore } from '../../../../../stores/software'
import { useOptionStore } from '../../../../../stores/option'
import InitCommon from '../../../common/initCommon'
import AddComfyUIWorkflow from './AddComfyUIWorkflow.vue'
import { OptionKeyName, OptionType } from '@/define/enum/option'
const softwareStore = useSoftwareStore()
const optionStore = useOptionStore()
const dialog = useDialog()
let message = useMessage()
onMounted(async () => {
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = '加载设置中...'
await InitCommon.InitComfyUISetting()
message.success('加载 ComfyUI 设置成功')
} catch (error) {
message.error('加载设置失败,失败原因:' + error.message)
} finally {
softwareStore.spin.spinning = false
}
//
updateTableHeight()
window.addEventListener('resize', updateTableHeight)
})
onUnmounted(() => {
//
window.removeEventListener('resize', updateTableHeight)
})
async function SaveComfyUISimpleSetting() {
try {
// http https
if (
!optionStore.ComfyUI_SimpleSetting.requestUrl.startsWith('http://') &&
!optionStore.ComfyUI_SimpleSetting.requestUrl.startsWith('https://')
) {
message.error('请求地址必须以 http 或者 https 开头')
return
}
// ID
if (
!optionStore.ComfyUI_WorkFlowSetting.some(
(workflow) => workflow.id === optionStore.ComfyUI_SimpleSetting.selectedWorkflow
)
) {
message.error('当前选中的工作流不存在,请重新选择')
return
}
//
let res = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_SimpleSetting,
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
OptionType.JOSN
)
if (res.code == 1) {
message.success('保存设置成功')
} else {
message.error('保存设置失败,失败原因:' + res.message)
}
} catch (error) {
message.error('保存ComfyUI通用设置失败失败原因' + error.message)
}
}
//
const tableContainer = ref(null)
const tableHeight = ref(300) //
const minTableHeight = 200 //
//
const updateTableHeight = () => {
nextTick(() => {
if (!tableContainer.value) return
//
const containerRect = tableContainer.value.getBoundingClientRect()
//
const existingContentHeight = containerRect.top
//
const viewportHeight = window.innerHeight
//
const bottomPadding = 60 //
const availableHeight = viewportHeight - existingContentHeight - bottomPadding
//
tableHeight.value = Math.max(availableHeight, minTableHeight)
})
}
// Table columns
const columns = [
{
title: '名称',
key: 'name'
},
{
title: '工作流路径',
key: 'workflowPath'
},
{
title: '操作',
key: 'actions',
render(row) {
return h(
'div',
{},
{
default: () => [
h(
NButton,
{
size: 'small',
type: 'info',
secondary: true,
onClick: () => handleEdit(row)
},
{
default: () => '编辑'
}
),
h(
NButton,
{
size: 'small',
type: 'error',
secondary: true,
onClick: () => handleRemove(row),
style: { marginLeft: '16px' }
},
{
default: () => '删除'
}
)
]
}
)
}
}
]
const handleAdd = () => {
dialog.info({
title: '添加工作流',
showIcon: false,
maskClosable: false,
style: { width: '600px' },
content: () => h(AddComfyUIWorkflow)
})
}
//
const handleEdit = (row) => {
dialog.info({
title: '编辑工作流',
showIcon: false,
maskClosable: false,
style: { width: '600px' },
content: () => h(AddComfyUIWorkflow, { workflowData: row })
})
}
//
const handleRemove = (row) => {
dialog.warning({
title: '确认删除',
content: `确定要删除工作流"${row.name}"吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
debugger
//
const index = optionStore.ComfyUI_WorkFlowSetting.findIndex((item) => item.id === row.id)
if (index == -1) {
message.error('删除失败: 未找到该工作流')
return
}
optionStore.ComfyUI_WorkFlowSetting.splice(index, 1)
//
if (optionStore.ComfyUI_SimpleSetting.selectedWorkflow === row.id) {
optionStore.ComfyUI_SimpleSetting.selectedWorkflow = ''
}
//
let res = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_WorkFlowSetting,
JSON.stringify(optionStore.ComfyUI_WorkFlowSetting),
OptionType.JOSN
)
if (res.code == 0) {
message.error('删除失败,失败原因:' + res.message)
return
}
res = await window.options.ModifyOptionByKey(
OptionKeyName.ComfyUI_SimpleSetting,
JSON.stringify(optionStore.ComfyUI_SimpleSetting),
OptionType.JOSN
)
if (res.code == 1) {
message.success('删除成功')
} else {
message.error('删除失败,失败原因:' + res.message)
}
} catch (error) {
message.error('删除失败: ' + error.message)
}
}
})
}
</script>
<style scoped>
.description {
margin-bottom: 16px;
font-size: large;
color: red;
}
.action-bar {
margin: 16px 0;
}
.table-container {
width: 100%;
}
</style>

View File

@ -62,14 +62,9 @@
v-model:value="formValue.denoising_strength"
placeholder="输入重绘幅度"
/>
<n-checkbox
style="margin-left: 30px"
size="large"
v-model:checked="formValue.adetailer"
label="是否开启修脸/修手"
/>
</n-form-item>
</div>
<div style="display: flex">
<n-form-item label="采样方式" path="sampler_name">
<n-select
style="width: 250px"
@ -78,8 +73,7 @@
placeholder="选择采样方式"
/>
</n-form-item>
<div style="display: flex">
<n-form-item label="迭代步数" path="steps">
<n-form-item label="迭代步数" path="steps" style="margin-left: 10px">
<n-input-number
style="width: 150px"
v-model:value="formValue.steps"
@ -98,19 +92,37 @@
/>
</n-form-item>
</div>
<div style="display: flex">
<n-form-item label="图片分辨率" path="resolution">
<n-input-number
:show-button="false"
v-model:value="formValue.width"
placeholder="输入迭代步数"
placeholder="宽"
style="width: 100px"
/>
<span style="margin: 0 10px"> * </span>
<n-input-number
:show-button="false"
v-model:value="formValue.height"
placeholder="输入迭代步数"
placeholder="高"
style="width: 100px"
/>
</n-form-item>
<n-form-item>
<n-checkbox
style="margin-left: 30px"
size="large"
v-model:checked="formValue.adetailer"
label="是否开启修脸/修手"
/>
<n-button style="margin-left: 10px" type="primary" @click="AdetailerSetting"
>修手/脸设置</n-button
>
</n-form-item>
</div>
<div style="display: flex">
<n-form-item
style="width: 300px"
@ -151,14 +163,14 @@
</n-form-item>
</n-form>
</n-tab-pane>
<n-tab-pane name="ADetailer setting" tab="ADetailer 设置">
<SDADetailerSetting />
<n-tab-pane name="ComfyUI API Setting" tab="ComfyUI 设置">
<ComfyUISetting />
</n-tab-pane>
</n-tabs>
</template>
<script setup>
import { defineComponent, ref, h, onMounted, toRaw } from 'vue'
import { ref, h, onMounted, toRaw } from 'vue'
import {
NForm,
NFormItem,
@ -169,10 +181,13 @@ import {
NSelect,
NTabs,
NTabPane,
NCheckbox
NCheckbox,
useDialog
} from 'naive-ui'
import SDADetailerSetting from '../Components/SDADetailerSetting.vue'
import InitCommon from '../../common/initCommon'
import SDADetailerSetting from './SDADetailerSetting.vue'
import InitCommon from '../../../common/initCommon'
import ComfyUISetting from './ComfyUISetting.vue'
let dialog = useDialog()
let flux_model_options = ref([])
@ -332,4 +347,14 @@ async function LoadSDServiceData() {
message.success('加载成功')
})
}
async function AdetailerSetting() {
dialog.info({
title: 'ADetailer模型设置',
showIcon: false,
maskClosable: false,
style: { width: '600px' },
content: () => h(SDADetailerSetting)
})
}
</script>

View File

@ -61,7 +61,7 @@ const routes = [
{
path: '/sd_setting',
name: 'sd_setting',
component: () => import('./components/Setting/SDSetting.vue')
component: () => import('./components/Setting/SDSetting/SDSetting.vue')
},
{
path: '/copywriting',

View File

@ -31,6 +31,16 @@ export type OptionStoreModel = {
/** MJ 基础设置 */
[OptionKeyName.MJ_GlobalSetting]: MJSettingModel.MJ_GlobalSettingModel;
//#endregion
//#region SD/ComfyUI
/** ComfyUI的基础设置 */
[OptionKeyName.ComfyUI_SimpleSetting]: OptionModel.ComfyUI_SimpleSettingModel;
/** COmfyUI的工作流设置 */
[OptionKeyName.ComfyUI_WorkFlowSetting]: Array<OptionModel.ComfyUI_WorkFlowSettingModel>;
//#endregion
}
export const useOptionStore = defineStore('option', {
@ -104,7 +114,13 @@ export const useOptionStore = defineStore('option', {
mj_remoteSimpleSetting: {
useTransfer: false
}
}
},
[OptionKeyName.ComfyUI_SimpleSetting]: {
requestUrl: "",
negativePrompt: "",
selectedWorkflow: null
},
[OptionKeyName.ComfyUI_WorkFlowSetting]: []
} as unknown as OptionStoreModel),
getters: {

View File

@ -14,6 +14,7 @@
},
"include": [
"src",
"./package.json"
, "src/renderer/src/components/Book/Components/.vue" ]
"./package.json",
"src/renderer/src/components/Book/Components/.vue"
]
}