// 语言配置
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 = `
${t.noFilesSelected}
`;
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) => `
${file.name}
×
`).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 = `${t.noFilesSelected}
`;
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)
);
}