613 lines
21 KiB
JavaScript
613 lines
21 KiB
JavaScript
// 语言配置
|
||
const translations = {
|
||
en: {
|
||
title: "ComfyUI Workflow Fixer",
|
||
description: {
|
||
namespace: "This tool helps fix old workflow JSON files by adding the PaintingCoder namespace to node types.",
|
||
path: "This tool helps convert path separators between Windows (\\) and Unix-style (Linux/Mac) (/) formats."
|
||
},
|
||
dragText: "Choose Workflow File or drag and drop file here",
|
||
fixButton: "Fix & Download",
|
||
selectedFile: "Selected file: ",
|
||
errorSelectFile: "Please select a file first.",
|
||
successMessage: "File fixed and saved as ",
|
||
switchLang: "中文",
|
||
fixTypeNamespace: "Fix PaintingCoder old version (<0.3.0) workflow",
|
||
fixTypePath: "Fix Path Separator",
|
||
pathOptions: {
|
||
auto: "Auto Detect",
|
||
toWindows: "To Windows Format (\\)",
|
||
toUnix: "To Unix Format (Linux/Mac) (/)"
|
||
},
|
||
errorJsonFile: "Please select a JSON file.",
|
||
selectedFiles: "Selected files:",
|
||
noFilesSelected: "No files selected",
|
||
removeFile: "Remove",
|
||
zipOption: "Download as ZIP",
|
||
processingFiles: "Processing files: {0}/{1}",
|
||
successMultipleFiles: "Successfully processed {0} files",
|
||
clearButton: "Clear All",
|
||
successSingleFile: "Successfully processed 1 file",
|
||
errorProcessing: "Error occurred while processing files"
|
||
},
|
||
zh: {
|
||
title: "ComfyUI 工作流修复工具",
|
||
description: {
|
||
namespace: "此工具可以通过添加 PaintingCoder 命名空间来修复旧的工作流 JSON 文件。",
|
||
path: "此工具可以在 Windows (\\) 和 Unix (Linux/Mac) (/) 格式之间转换路径分隔符。"
|
||
},
|
||
dragText: "选择工作流文件或将文件拖放到此处",
|
||
fixButton: "修复并下载",
|
||
selectedFile: "已选择文件:",
|
||
errorSelectFile: "请先选择一个文件。",
|
||
successMessage: "文件已修复并保存为 ",
|
||
switchLang: "English",
|
||
fixTypeNamespace: "修复PaintingCoder旧版本(<0.3.0)工作流",
|
||
fixTypePath: "修复路径分隔符",
|
||
pathOptions: {
|
||
auto: "自动检测",
|
||
toWindows: "转换为 Windows 格式 (\\)",
|
||
toUnix: "转换为 Unix 格式 (Linux/Mac) (/)"
|
||
},
|
||
errorJsonFile: "请选择JSON文件。",
|
||
selectedFiles: "已选择文件:",
|
||
noFilesSelected: "未选择文件",
|
||
removeFile: "移除",
|
||
zipOption: "以ZIP格式下载",
|
||
processingFiles: "正在处理文件:{0}/{1}",
|
||
successMultipleFiles: "成功处理{0}个文件",
|
||
clearButton: "清除全部",
|
||
successSingleFile: "成功处理1个文件",
|
||
errorProcessing: "处理文件时发生错误"
|
||
}
|
||
};
|
||
|
||
// 全局变量
|
||
let currentLanguage = 'en';
|
||
let currentTheme = localStorage.getItem('theme') || 'light';
|
||
let selectedFiles = [];
|
||
|
||
// 页面加载完成后的初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initializeApp();
|
||
setTheme(currentTheme);
|
||
setupEventListeners();
|
||
updateHeaderImage();
|
||
initializePathOptions();
|
||
|
||
// 初始化按钮文本
|
||
const t = translations[currentLanguage];
|
||
const fixButton = document.getElementById('fixButton');
|
||
if (fixButton) {
|
||
fixButton.textContent = t.fixButton;
|
||
}
|
||
});
|
||
|
||
// 添加路径选项初始化函数
|
||
function initializePathOptions() {
|
||
const t = translations[currentLanguage];
|
||
|
||
// 初始化自动检测选项
|
||
const autoLabel = document.getElementById('autoLabel');
|
||
if (autoLabel) {
|
||
autoLabel.textContent = t.pathOptions.auto;
|
||
}
|
||
|
||
// 初始化Windows格式选项
|
||
const toWindowsLabel = document.getElementById('toWindowsLabel');
|
||
if (toWindowsLabel) {
|
||
toWindowsLabel.textContent = t.pathOptions.toWindows;
|
||
}
|
||
|
||
// 初始化Unix格式选项
|
||
const toUnixLabel = document.getElementById('toUnixLabel');
|
||
if (toUnixLabel) {
|
||
toUnixLabel.textContent = t.pathOptions.toUnix;
|
||
}
|
||
}
|
||
|
||
// 初始化应用
|
||
function initializeApp() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const lang = urlParams.get('lang');
|
||
|
||
if (lang && (lang === 'zh' || lang === 'en')) {
|
||
currentLanguage = lang;
|
||
updateLanguage();
|
||
} else {
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set('lang', currentLanguage);
|
||
window.history.replaceState({}, '', url.toString());
|
||
}
|
||
}
|
||
|
||
// 设置事件监听器
|
||
function setupEventListeners() {
|
||
const dropZone = document.getElementById('dropZone');
|
||
const fileInput = document.getElementById('fileInput');
|
||
|
||
dropZone.addEventListener('dragover', handleDragOver);
|
||
dropZone.addEventListener('dragleave', handleDragLeave);
|
||
dropZone.addEventListener('drop', handleDrop);
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
}
|
||
|
||
// 主题切换相关函数
|
||
function setTheme(theme) {
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
localStorage.setItem('theme', theme);
|
||
|
||
const moonIcon = document.querySelector('.moon-icon');
|
||
const sunIcon = document.querySelector('.sun-icon');
|
||
|
||
if (theme === 'dark') {
|
||
moonIcon.classList.remove('active');
|
||
sunIcon.classList.add('active');
|
||
} else {
|
||
moonIcon.classList.add('active');
|
||
sunIcon.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
function toggleTheme() {
|
||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||
setTheme(currentTheme);
|
||
}
|
||
|
||
// 语言切换相关函数
|
||
function toggleLanguage() {
|
||
currentLanguage = currentLanguage === 'en' ? 'zh' : 'en';
|
||
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set('lang', currentLanguage);
|
||
window.history.replaceState({}, '', url.toString());
|
||
|
||
updateLanguage();
|
||
updateHeaderImage();
|
||
}
|
||
|
||
function updateLanguage() {
|
||
const t = translations[currentLanguage];
|
||
|
||
// 更新所有文本内容
|
||
document.getElementById('title').textContent = t.title;
|
||
document.getElementById('dragText').textContent = t.dragText;
|
||
|
||
// 确保更新 Fix & Download 按钮文本
|
||
const fixButton = document.getElementById('fixButton');
|
||
if (fixButton) {
|
||
fixButton.textContent = t.fixButton;
|
||
}
|
||
|
||
document.getElementById('langBtn').textContent = t.switchLang;
|
||
document.getElementById('clearButton').textContent = t.clearButton;
|
||
document.getElementById('zipLabel').textContent = t.zipOption;
|
||
|
||
// 更新下拉选项
|
||
const fixTypeSelect = document.getElementById('fixType');
|
||
if (fixTypeSelect) {
|
||
const options = fixTypeSelect.options;
|
||
options[0].textContent = t.fixTypeNamespace;
|
||
options[1].textContent = t.fixTypePath;
|
||
}
|
||
|
||
// 更新路径选项
|
||
initializePathOptions();
|
||
|
||
updateDescription();
|
||
updateFileList();
|
||
}
|
||
|
||
// 添加头图切换函数
|
||
function updateHeaderImage() {
|
||
const headerImg = document.querySelector('.header-image img');
|
||
if (headerImg) {
|
||
const imageName = currentLanguage === 'en' ?
|
||
'workflow_fix_top_banner01.jpg' :
|
||
'workflow_fix_top_banner02.jpg';
|
||
headerImg.src = `../images/${imageName}`;
|
||
}
|
||
}
|
||
|
||
// 文件处理相关函数
|
||
function handleDragOver(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dropZone').classList.add('dragover');
|
||
}
|
||
|
||
function handleDragLeave(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dropZone').classList.remove('dragover');
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
const dropZone = document.getElementById('dropZone');
|
||
dropZone.classList.remove('dragover');
|
||
|
||
const files = e.dataTransfer.files;
|
||
handleFiles(files);
|
||
}
|
||
|
||
function handleFileSelect(event) {
|
||
handleFiles(event.target.files);
|
||
event.target.value = '';
|
||
}
|
||
|
||
function handleFiles(files) {
|
||
for (let file of files) {
|
||
if (file.name.endsWith('.json')) {
|
||
if (!selectedFiles.some(f => f.name === file.name)) {
|
||
selectedFiles.push(file);
|
||
}
|
||
} else {
|
||
const t = translations[currentLanguage];
|
||
document.getElementById('error').textContent = t.errorJsonFile;
|
||
}
|
||
}
|
||
updateFileList();
|
||
}
|
||
|
||
function updateFileList() {
|
||
const filesContainer = document.getElementById('selectedFiles');
|
||
const t = translations[currentLanguage];
|
||
const clearButton = document.getElementById('clearButton');
|
||
|
||
if (selectedFiles.length === 0) {
|
||
filesContainer.innerHTML = `<div class="file-item"><span class="file-name">${t.noFilesSelected}</span></div>`;
|
||
clearButton.disabled = true;
|
||
document.getElementById('fixButton').disabled = true;
|
||
filesContainer.classList.add('no-scroll');
|
||
|
||
// 清除错误和成功消息
|
||
document.getElementById('error').textContent = '';
|
||
document.getElementById('success').textContent = '';
|
||
return;
|
||
}
|
||
|
||
filesContainer.innerHTML = selectedFiles.map((file, index) => `
|
||
<div class="file-item">
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="delete-btn" onclick="removeFile(${index})">×</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 根据文件数量控制滚动条
|
||
if (selectedFiles.length <= 10) {
|
||
filesContainer.classList.add('no-scroll');
|
||
} else {
|
||
filesContainer.classList.remove('no-scroll');
|
||
}
|
||
|
||
clearButton.disabled = false;
|
||
document.getElementById('fixButton').disabled = false;
|
||
}
|
||
|
||
function removeFile(index) {
|
||
selectedFiles.splice(index, 1);
|
||
updateFileList();
|
||
}
|
||
|
||
function clearFiles() {
|
||
selectedFiles = [];
|
||
updateFileList();
|
||
document.getElementById('fileInput').value = '';
|
||
|
||
// 清除错误和成功消息
|
||
document.getElementById('error').textContent = '';
|
||
document.getElementById('success').textContent = '';
|
||
|
||
// 禁用相关按钮
|
||
document.getElementById('clearButton').disabled = true;
|
||
document.getElementById('fixButton').disabled = true;
|
||
|
||
// 重置文件拖放区域的状态
|
||
const dropZone = document.getElementById('dropZone');
|
||
dropZone.classList.remove('dragover');
|
||
|
||
// 更新文件列表显示
|
||
const t = translations[currentLanguage];
|
||
const filesContainer = document.getElementById('selectedFiles');
|
||
filesContainer.innerHTML = `<div class="file-item"><span class="file-name">${t.noFilesSelected}</span></div>`;
|
||
filesContainer.classList.add('no-scroll');
|
||
}
|
||
|
||
// UI更新函数
|
||
function updateDescription() {
|
||
const fixType = document.getElementById('fixType').value;
|
||
const t = translations[currentLanguage];
|
||
document.getElementById('description').textContent = t.description[fixType];
|
||
}
|
||
|
||
function updateUI() {
|
||
const fixType = document.getElementById('fixType').value;
|
||
updateDescription();
|
||
document.getElementById('pathOptions').classList.toggle('show', fixType === 'path');
|
||
}
|
||
|
||
// 添加下载功能
|
||
async function fixAndDownload() {
|
||
const t = translations[currentLanguage];
|
||
const fixType = document.getElementById('fixType').value;
|
||
const useZip = document.getElementById('zipDownload').checked;
|
||
const error = document.getElementById('error');
|
||
const success = document.getElementById('success');
|
||
|
||
error.textContent = '';
|
||
success.textContent = '';
|
||
|
||
if (selectedFiles.length === 0) {
|
||
error.textContent = t.errorSelectFile;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (useZip) {
|
||
// ZIP下载逻辑保持不变
|
||
const zip = new JSZip();
|
||
|
||
for (let i = 0; i < selectedFiles.length; i++) {
|
||
const file = selectedFiles[i];
|
||
const content = await file.text();
|
||
const fixedContent = fixWorkflowContent(content, fixType);
|
||
const fileName = file.name.replace('.json', '_fixed.json');
|
||
zip.file(fileName, fixedContent);
|
||
}
|
||
|
||
const blob = await zip.generateAsync({type: 'blob'});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'fixed_workflows.zip';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
success.textContent = t.successMultipleFiles.replace('{0}', selectedFiles.length);
|
||
} else {
|
||
// 修改后的单文件处理逻辑
|
||
let processedCount = 0;
|
||
const totalFiles = selectedFiles.length;
|
||
const fixedFiles = [];
|
||
|
||
// 首先处理所有文件
|
||
for (const file of selectedFiles) {
|
||
try {
|
||
const content = await file.text();
|
||
const fixedContent = fixWorkflowContent(content, fixType);
|
||
const fileName = file.name.replace('.json', '_fixed.json');
|
||
fixedFiles.push({
|
||
name: fileName,
|
||
content: fixedContent
|
||
});
|
||
processedCount++;
|
||
success.textContent = t.processingFiles
|
||
.replace('{0}', processedCount)
|
||
.replace('{1}', totalFiles);
|
||
} catch (err) {
|
||
console.error(`Error processing ${file.name}:`, err);
|
||
}
|
||
}
|
||
|
||
// 然后逐个下载处理好的文件
|
||
for (let i = 0; i < fixedFiles.length; i++) {
|
||
const file = fixedFiles[i];
|
||
const blob = new Blob([file.content], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
await new Promise(resolve => {
|
||
setTimeout(() => {
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = file.name;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
resolve();
|
||
}, i * 800); // 增加间隔时间到800ms,确保文件能够正确下载
|
||
});
|
||
}
|
||
|
||
// 更新最终状态
|
||
if (processedCount > 0) {
|
||
success.textContent = processedCount === 1
|
||
? t.successSingleFile
|
||
: t.successMultipleFiles.replace('{0}', processedCount);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
error.textContent = t.errorProcessing;
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
// 定义需要进行路径转换的节点类型
|
||
const PATH_CONVERSION_NODES = new Set([
|
||
"CheckpointLoaderSimple",
|
||
"LoraLoader",
|
||
"ControlNetLoader",
|
||
"UNETLoader",
|
||
"VAELoader",
|
||
"CLIPLoader",
|
||
"DiffControlNetLoader",
|
||
"ModelLoader",
|
||
"LoadDiffusionModel",
|
||
"CheckpointLoader",
|
||
"DiffusersLoader",
|
||
"LoraLoaderModelOnly",
|
||
"VAEDecode",
|
||
"VAEEncode",
|
||
"CLIPVisionLoader",
|
||
"StyleModelLoader",
|
||
"UpscaleModelLoader",
|
||
"Load LoRA",
|
||
"Lora List",
|
||
"LoRA Stacker",
|
||
"GridLoras",
|
||
"CheckpointLoader|pysssss",
|
||
"LoraLoader|pysssss",
|
||
"LoraSave",
|
||
"DualCLIPLoader",
|
||
"DualUNETLoader",
|
||
"DualVAELoader",
|
||
"DualCLIPVisionLoader",
|
||
"DualStyleModelLoader",
|
||
"DualUpscaleModelLoader"
|
||
]);
|
||
|
||
function fixWorkflowContent(content, fixType) {
|
||
const workflow = JSON.parse(content);
|
||
|
||
if (fixType === 'namespace') {
|
||
// 遍历所有节点
|
||
for (const nodeId in workflow.nodes) {
|
||
const node = workflow.nodes[nodeId];
|
||
if (node && node.type) {
|
||
// 只修改 nodeMapping 中定义的节点
|
||
if (nodeMapping[node.type]) {
|
||
node.type = nodeMapping[node.type];
|
||
}
|
||
}
|
||
}
|
||
} else if (fixType === 'path') {
|
||
// 获取选中的路径类型
|
||
const pathType = document.querySelector('input[name="pathType"]:checked').value;
|
||
let conversionDirection = pathType;
|
||
|
||
// 处理自动检测
|
||
if (pathType === 'auto') {
|
||
const currentFormat = detectPathFormat(workflow);
|
||
conversionDirection = currentFormat === 'unix' ? 'toWindows' : 'toUnix';
|
||
}
|
||
|
||
// 遍历所有节点
|
||
for (const nodeId in workflow.nodes) {
|
||
const node = workflow.nodes[nodeId];
|
||
const nodeType = node.type || node.class_type;
|
||
|
||
// 如果是需要处理的节点类型
|
||
if (PATH_CONVERSION_NODES.has(nodeType) && node.widgets_values) {
|
||
node.widgets_values = node.widgets_values.map(value => {
|
||
if (typeof value === 'string' && !value.includes('base64')) {
|
||
// 转换路径分隔符
|
||
if (conversionDirection === 'toUnix') {
|
||
return value.replace(/\\/g, '/');
|
||
} else if (conversionDirection === 'toWindows') {
|
||
return value.replace(/\//g, '\\');
|
||
}
|
||
} else if (typeof value === 'object' && value?.content) {
|
||
// 处理特殊格式
|
||
if (!value.content.includes('base64')) {
|
||
const newContent = conversionDirection === 'toUnix'
|
||
? value.content.replace(/\\/g, '/')
|
||
: value.content.replace(/\//g, '\\');
|
||
return { ...value, content: newContent };
|
||
}
|
||
}
|
||
return value;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return JSON.stringify(workflow, null, 2);
|
||
}
|
||
|
||
// 检测路径格式
|
||
function detectPathFormat(workflow) {
|
||
let unixCount = 0;
|
||
let windowsCount = 0;
|
||
|
||
for (const nodeId in workflow.nodes) {
|
||
const node = workflow.nodes[nodeId];
|
||
const nodeType = node.type || node.class_type;
|
||
|
||
if (PATH_CONVERSION_NODES.has(nodeType) && node.widgets_values) {
|
||
node.widgets_values.forEach(value => {
|
||
if (typeof value === 'string' && !value.includes('base64')) {
|
||
const unixSlashes = (value.match(/\//g) || []).length;
|
||
const windowsSlashes = (value.match(/\\/g) || []).length;
|
||
|
||
if (unixSlashes > windowsSlashes) {
|
||
unixCount++;
|
||
} else if (windowsSlashes > unixSlashes) {
|
||
windowsCount++;
|
||
}
|
||
} else if (typeof value === 'object' && value?.content) {
|
||
if (!value.content.includes('base64')) {
|
||
const unixSlashes = (value.content.match(/\//g) || []).length;
|
||
const windowsSlashes = (value.content.match(/\\/g) || []).length;
|
||
|
||
if (unixSlashes > windowsSlashes) {
|
||
unixCount++;
|
||
} else if (windowsSlashes > unixSlashes) {
|
||
windowsCount++;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
return unixCount > windowsCount ? 'unix' : 'windows';
|
||
}
|
||
|
||
// 更新 nodeMapping 对象
|
||
const nodeMapping = {
|
||
"MaskPreview": "PaintingCoder::MaskPreview",
|
||
"ImageSizeCreator": "PaintingCoder::ImageSizeCreator",
|
||
"ImageToBase64": "PaintingCoder::ImageToBase64",
|
||
"ImageLatentCreator": "PaintingCoder::ImageLatentCreator",
|
||
"DynamicImageCombiner": "PaintingCoder::DynamicImageCombiner",
|
||
"DynamicMaskCombiner": "PaintingCoder::DynamicMaskCombiner",
|
||
"ImageResolutionAdjuster": "PaintingCoder::ImageResolutionAdjuster",
|
||
"TextCombiner": "PaintingCoder::TextCombiner",
|
||
"ShowTextPlus": "PaintingCoder::ShowTextPlus",
|
||
"SimpleTextInput": "PaintingCoder::SimpleTextInput",
|
||
"MultilineTextInput": "PaintingCoder::MultilineTextInput",
|
||
"RemoveEmptyLinesAndLeadingSpaces": "PaintingCoder::RemoveEmptyLinesAndLeadingSpaces",
|
||
"ImageSwitch": "PaintingCoder::ImageSwitch",
|
||
"TextSwitch": "PaintingCoder::TextSwitch",
|
||
"MaskSwitch": "PaintingCoder::MaskSwitch",
|
||
"LatentSwitch": "PaintingCoder::LatentSwitch",
|
||
"WebImageLoader": "PaintingCoder::WebImageLoader",
|
||
"MaskPreview+": "PaintingCoder::MaskPreview"
|
||
};
|
||
|
||
function isPaintingCoderNode(type) {
|
||
// PaintingCoder 节点的类名前缀列表
|
||
const paintingCoderPrefixes = [
|
||
'Prompt',
|
||
'ControlNet',
|
||
'Model',
|
||
'VAE',
|
||
'CLIP',
|
||
'Lora',
|
||
'Checkpoint',
|
||
'UNET',
|
||
'Save',
|
||
'Preview',
|
||
'KSampler',
|
||
'EmptyLatent',
|
||
'Conditioning',
|
||
'Latent',
|
||
'Image',
|
||
'Text',
|
||
'Mask',
|
||
'Dynamic',
|
||
'Simple',
|
||
'Show',
|
||
'Web'
|
||
];
|
||
|
||
// 检查类名是否以任一前缀开头
|
||
return paintingCoderPrefixes.some(prefix =>
|
||
type.startsWith(prefix) ||
|
||
type.startsWith('PaintingCoder::' + prefix)
|
||
);
|
||
}
|