Files

965 lines
39 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
let deployDialog;
const DEFAULT_CONFIG = {
endpoint: "http://127.0.0.1:8080",
apiKey: "",
};
// Global config object
let deployConfig = {...DEFAULT_CONFIG};
function loadConfig() {
try {
const savedEndpoint = localStorage.getItem('comfy_api_deploy_endpoint');
const savedApiKey = localStorage.getItem('comfy_api_deploy_api_key');
if (savedEndpoint) {
deployConfig.endpoint = savedEndpoint;
}
if (savedApiKey) {
deployConfig.apiKey = savedApiKey;
}
console.log("comfy-deploy: Config loaded");
} catch (error) {
console.error("comfy-deploy: Failed to load config", error);
}
}
function saveConfig(config) {
try {
localStorage.setItem('comfy_api_deploy_endpoint', config.endpoint);
localStorage.setItem('comfy_api_deploy_api_key', config.apiKey);
// Update global config object
deployConfig = {...config};
console.log("comfy-deploy: Config saved");
return true;
} catch (error) {
console.error("comfy-deploy: Failed to save config", error);
return false;
}
}
function getUILanguage() {
try {
const locale = app.ui.settings.getSettingValue('Comfy.Locale');
return locale && locale.toLowerCase().startsWith('zh') ? 'zh' : 'en';
} catch (e) {
console.log('comfy-deploy: Failed to get UI language', e);
return 'en';
}
}
const i18n = {
deployDialogTitle: {
en: 'Comfy Deploy',
zh: 'Comfy Deploy'
},
deployDescription: {
en: 'Deploy the current workflow to ComfyDeploy, making it accessible via API. A new workflow version will be created after deployment.',
zh: '将当前工作流部署到ComfyDeploy,使其可通过API进行访问和运行。部署后将创建一个新的工作流版本。'
},
deployNodeInfo: {
en: 'Deployment will automatically add or update workflow nodes',
zh: '部署会自动添加或更新工作流节点'
},
deployButton: {
en: 'Deploy Workflow',
zh: '部署工作流'
},
configOptions: {
en: 'Configuration Options',
zh: '配置选项'
},
configApiButton: {
en: 'Configure API Endpoint & Key',
zh: '配置 API 端点与密钥'
},
statusReady: {
en: 'ComfyDeploy Ready',
zh: 'ComfyDeploy 已就绪'
},
statusConnected: {
en: 'ComfyDeploy Connected',
zh: 'ComfyDeploy 已连接'
},
statusFailed: {
en: 'Connection Failed',
zh: '连接失败'
},
configRequired: {
en: 'Please configure API endpoint and key',
zh: '请配置 API 端点和密钥'
},
docs: {
en: 'Docs',
zh: '文档'
},
// Input dialog related text
enterWorkflowName: {
en: 'Enter Workflow Name',
zh: '请输入工作流名称'
},
inputWorkflowName: {
en: 'Input workflow name',
zh: '输入工作流名称'
},
// Confirm dialog related text
confirmDeploy: {
en: 'Confirm Deployment',
zh: '确认部署'
},
createNewVersion: {
en: 'Will create a new version of workflow',
zh: '将创建工作流的新版本'
},
workflowId: {
en: 'Workflow ID',
zh: '工作流ID'
},
currentVersion: {
en: 'Current Version',
zh: '当前版本'
},
createNewWorkflow: {
en: 'Will create new workflow',
zh: '将创建新工作流'
},
// Config dialog related text
configTitle: {
en: 'Comfy Deploy Config',
zh: 'Comfy Deploy 配置'
},
endpointLabel: {
en: 'Endpoint',
zh: 'API端点'
},
apiKeyLabel: {
en: 'API Key',
zh: 'API 密钥'
},
endpointPlaceholder: {
en: 'Example: http://127.0.0.1:8080',
zh: '例如: http://127.0.0.1:8080'
},
apiKeyPlaceholder: {
en: 'API Key',
zh: 'API 密钥'
},
save: {
en: 'Save',
zh: '保存'
},
// Button text
cancel: {
en: 'Cancel',
zh: '取消'
},
confirm: {
en: 'Confirm',
zh: '确认'
},
ok: {
en: 'OK',
zh: '确定'
},
// Status message
deploying: {
en: 'Deploying workflow...',
zh: '正在部署工作流...'
},
success: {
en: 'Success',
zh: '成功'
},
error: {
en: 'Error',
zh: '错误'
},
deploySuccess: {
en: 'Workflow "{0}" deployed successfully!<br>Workflow ID: {1}<br>Version: {2}',
zh: '工作流 "{0}" 部署成功!<br>工作流ID: {1}<br>版本: {2}'
},
updateSuccess: {
en: 'Workflow "{0}" updated successfully!<br>Workflow ID: {1}<br>Version: {2}',
zh: '工作流 "{0}" 更新成功!<br>工作流ID: {1}<br>版本: {2}'
},
deployFailed: {
en: 'Failed to deploy workflow: {0}',
zh: '部署工作流失败: {0}'
},
configEmpty: {
en: 'API endpoint cannot be empty',
zh: 'API 端点不能为空'
}
};
function getText(key, ...args) {
const lang = getUILanguage();
let text = i18n[key][lang] || i18n[key]['en'] || key;
// If there are parameters, format the replacement {0}, {1}, {2}...
if (args && args.length > 0) {
args.forEach((arg, index) => {
text = text.replace(new RegExp('\\{' + index + '\\}', 'g'), arg);
});
}
return text;
}
// Create deploy dialog
function createDeployDialog() {
if (!deployDialog) {
deployDialog = document.createElement('dialog');
deployDialog.style.padding = '0';
deployDialog.style.border = 'none';
deployDialog.style.borderRadius = '12px';
deployDialog.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
const updateDialogContent = () => {
deployDialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:380px;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:18px 24px; font-size:20px; font-weight:bold; border-bottom: 1px solid #333;">
<div style="display: flex; align-items: center; gap: 10px;">
<div style="background: #28a745; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<i class="pi pi-upload" style="font-size: 16px;"></i>
</div>
${getText('deployDialogTitle')}
</div>
<button id="deploy-dialog-close" style="background:transparent; border:none; color:#bbb; padding:0; cursor:pointer; font-size:24px;">×</button>
</div>
<div style="padding:20px 24px 0;">
<div style="background: linear-gradient(45deg, #1a1a1a, #252525); border-radius: 8px; padding: 16px; margin-bottom: 20px; border: 1px solid #333;">
<p style="margin: 0 0 12px; font-size: 14px; color: #aaa; line-height: 1.4;">
${getText('deployDescription')}
</p>
<div style="display: flex; gap: 10px; align-items: center;">
<i class="pi pi-info-circle" style="color: #4a9eff; font-size: 14px;"></i>
<span style="font-size: 13px; color: #4a9eff;">${getText('deployNodeInfo')}</span>
</div>
</div>
<button id="deploy-dialog-deploy-btn" style="width:100%; background:#28a745; color:#fff; border:none; padding:14px; font-size:16px; border-radius:6px; display:flex; align-items:center; justify-content:center; gap:10px; margin-bottom:16px; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2); cursor: pointer; font-weight: 600;">
<i class="pi pi-cloud-upload" style="font-size: 18px;"></i> ${getText('deployButton')}
</button>
<div style="display: flex; align-items: center; margin: 16px 0; color: #666;">
<div style="flex-grow: 1; height: 1px; background: #333;"></div>
<div style="margin: 0 10px; font-size: 13px;">${getText('configOptions')}</div>
<div style="flex-grow: 1; height: 1px; background: #333;"></div>
</div>
<button id="deploy-dialog-config-btn" style="width:100%; background: linear-gradient(to bottom, #303030, #252525); color:#ddd; border:1px solid #444; padding:12px; font-size:15px; border-radius:6px; display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom: 24px; transition: all 0.2s; cursor: pointer;">
<i class="pi pi-cog" style="font-size: 16px;"></i> ${getText('configApiButton')}
</button>
</div>
<div style="background: #191919; padding: 16px 24px; border-top: 1px solid #333; border-radius: 0 0 12px 12px; font-size: 13px; color: #777; display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 8px;">
<div id="deploy-status-indicator" style="width: 8px; height: 8px; background: #28a745; border-radius: 50%;"></div>
<span id="deploy-status-text">${getText('statusReady')}</span>
</div>
<a href="https://github.com/ihmily/comfy-deploy-admin" target="_blank" style="color: #4a9eff; text-decoration: none; display: flex; align-items: center; gap: 5px;">
<i class="pi pi-github"></i>
${getText('docs')}
</a>
</div>
</div>
`;
deployDialog.querySelector('#deploy-dialog-close').onclick = () => deployDialog.close();
deployDialog.querySelector('#deploy-dialog-deploy-btn').onclick = () => deployWorkflow();
deployDialog.querySelector('#deploy-dialog-config-btn').onclick = () => openConfigDialog();
const deployBtn = deployDialog.querySelector('#deploy-dialog-deploy-btn');
deployBtn.addEventListener('mouseover', () => {
deployBtn.style.background = '#2dbc4e';
deployBtn.style.transform = 'translateY(-1px)';
});
deployBtn.addEventListener('mouseout', () => {
deployBtn.style.background = '#28a745';
deployBtn.style.transform = 'translateY(0)';
});
const configBtn = deployDialog.querySelector('#deploy-dialog-config-btn');
configBtn.addEventListener('mouseover', () => {
configBtn.style.background = '#353535';
});
configBtn.addEventListener('mouseout', () => {
configBtn.style.background = 'linear-gradient(to bottom, #303030, #252525)';
});
};
document.body.appendChild(deployDialog);
// Initialize dialog content
updateDialogContent();
// Listen to language changes, update dialog content
let lastLocale = app.ui.settings.getSettingValue('Comfy.Locale');
setInterval(() => {
try {
const currentLocale = app.ui.settings.getSettingValue('Comfy.Locale');
if (currentLocale !== lastLocale) {
lastLocale = currentLocale;
updateDialogContent();
}
} catch (e) {
console.log('comfydeploy: Failed to get ComfyUI language settings, using default English', e);
}
}, 1000);
}
updateDeployStatus();
return deployDialog;
}
function updateDeployStatus() {
if (!deployDialog) return;
const indicator = deployDialog.querySelector('#deploy-status-indicator');
const statusText = deployDialog.querySelector('#deploy-status-text');
if (!deployConfig.endpoint || !deployConfig.apiKey) {
indicator.style.background = '#ff9800'; // Yellow warning
statusText.textContent = getText('configRequired');
return;
}
// Test connection
fetch(deployConfig.endpoint + '/workflows/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${deployConfig.apiKey}`
}
})
.then(response => response.json())
.then(data => {
if (data && data.status === 'ok') {
indicator.style.background = '#28a745'; // Green success
statusText.textContent = getText('statusConnected');
} else {
indicator.style.background = '#ff4d4d'; // Red error
statusText.textContent = getText('statusFailed');
}
})
.catch(error => {
indicator.style.background = '#ff4d4d'; // Red error
statusText.textContent = getText('statusFailed');
});
}
// Open deploy dialog
function openDeployDialog() {
const dialog = createDeployDialog();
dialog.showModal();
}
function openConfigDialog() {
let dialog = document.getElementById('comfy-api-deploy-config-dialog');
if (!dialog) {
dialog = document.createElement('dialog');
dialog.id = 'comfy-api-deploy-config-dialog';
dialog.style.padding = '0';
dialog.style.border = 'none';
dialog.style.borderRadius = '12px';
dialog.style.boxShadow = 'none';
dialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 24px; font-size:18px; font-weight:bold;">
${getText('configTitle')}
<button id="comfy-api-deploy-config-close-x" style="background:transparent; border:none; color:#bbb; padding:0; cursor:pointer; font-size:24px;">×</button>
</div>
<div style="padding:0 24px 16px;">
<label style="display:block; margin-bottom:12px;">
${getText('endpointLabel')}
<input type="text" id="comfy-api-deploy-config-endpoint" placeholder="${getText('endpointPlaceholder')}" style="width:100%; padding:8px; margin-top:4px; border-radius:4px; border:1px solid #444; background:#2b2c2e; color:#fff;"/>
</label>
<label style="display:block; margin-bottom:12px;">
${getText('apiKeyLabel')}
<input type="text" id="comfy-api-deploy-config-api-key" placeholder="${getText('apiKeyPlaceholder')}" style="width:100%; padding:8px; margin-top:4px; border-radius:4px; border:1px solid #444; background:#2b2c2e; color:#fff;"/>
</label>
</div>
<div style="display:flex; justify-content:flex-end; gap:8px; padding:12px 24px; border-top:1px solid #333;">
<button id="comfy-api-deploy-config-close" style="background:transparent; border:none; color:#bbb; padding:8px 16px; cursor:pointer;">${getText('cancel')}</button>
<button id="comfy-api-deploy-config-save" style="background:#28a745; border:none; color:#fff; padding:8px 16px; cursor:pointer; border-radius:4px;">${getText('save')}</button>
</div>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('#comfy-api-deploy-config-close-x').onclick = () => dialog.close();
dialog.querySelector('#comfy-api-deploy-config-close').onclick = () => dialog.close();
dialog.querySelector('#comfy-api-deploy-config-save').onclick = () => {
const endpoint = document.getElementById('comfy-api-deploy-config-endpoint').value.trim();
const apiKey = document.getElementById('comfy-api-deploy-config-api-key').value.trim();
if (!endpoint) {
showMessage(getText('error'), getText('configEmpty'), true);
return;
}
// Save config
const success = saveConfig({ endpoint, apiKey });
if (success) {
dialog.close();
updateDeployStatus();
}
};
}
// Fill current config
dialog.querySelector('#comfy-api-deploy-config-endpoint').value = deployConfig.endpoint || '';
dialog.querySelector('#comfy-api-deploy-config-api-key').value = deployConfig.apiKey || '';
dialog.showModal();
}
function showMessage(title, message, isError = false) {
let dialog = document.createElement('dialog');
dialog.style.padding = '0';
dialog.style.border = 'none';
dialog.style.borderRadius = '12px';
dialog.style.boxShadow = 'none';
dialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 20px; font-size:18px; font-weight:bold; color:${isError ? '#ff4d4d' : '#fff'}">
${title}
<button class="dialog-close-x" style="background:transparent; border:none; color:#bbb; padding:0; cursor:pointer; font-size:24px;">×</button>
</div>
<div style="padding:5px 20px 16px;">
<p style="line-height:1.5; font-size:14px; margin:8px 0;">${message}</p>
</div>
<div style="display:flex; justify-content:flex-end; gap:8px; padding:12px 20px; border-top:1px solid #333;">
<button class="dialog-close" style="background:#28a745; border:none; color:#fff; padding:8px 20px; cursor:pointer; border-radius:4px; font-size:14px;">${getText('ok')}</button>
</div>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('.dialog-close-x').onclick = () => {
dialog.close();
dialog.remove();
};
dialog.querySelector('.dialog-close').onclick = () => {
dialog.close();
dialog.remove();
};
dialog.showModal();
}
function showLoading(message) {
let dialog = document.createElement('dialog');
dialog.id = 'comfy-api-deploy-loading';
dialog.style.padding = '0';
dialog.style.border = 'none';
dialog.style.borderRadius = '12px';
dialog.style.boxShadow = 'none';
dialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:400px; padding:24px; text-align:center;">
<div style="margin-bottom:16px;">
<i class="pi pi-spinner pi-spin" style="font-size:32px;"></i>
</div>
<p>${message}</p>
</div>
`;
document.body.appendChild(dialog);
dialog.showModal();
return dialog;
}
function showConfirmDialog(title, message) {
return new Promise((resolve) => {
let dialog = document.createElement('dialog');
dialog.style.padding = '0';
dialog.style.border = 'none';
dialog.style.borderRadius = '12px';
dialog.style.boxShadow = 'none';
dialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 20px; font-size:18px; font-weight:bold;">
${title}
<button class="dialog-close-x" style="background:transparent; border:none; color:#bbb; padding:0; cursor:pointer; font-size:24px;">×</button>
</div>
<div style="padding:5px 20px 16px;">
<div style="line-height:1.5; font-size:14px; margin:8px 0;">${message}</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px; padding:12px 20px; border-top:1px solid #333;">
<button class="dialog-cancel" style="background:#333; border:1px solid #555; color:#eee; padding:8px 16px; cursor:pointer; border-radius:4px; font-size:14px; transition:all 0.2s;">${getText('cancel')}</button>
<button class="dialog-confirm" style="background:#28a745; border:none; color:#fff; padding:8px 20px; cursor:pointer; border-radius:4px; font-size:14px; transition:all 0.2s;">${getText('confirm')}</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const cancelBtn = dialog.querySelector('.dialog-cancel');
cancelBtn.addEventListener('mouseover', () => {
cancelBtn.style.background = '#444';
});
cancelBtn.addEventListener('mouseout', () => {
cancelBtn.style.background = '#333';
});
const confirmBtn = dialog.querySelector('.dialog-confirm');
confirmBtn.addEventListener('mouseover', () => {
confirmBtn.style.background = '#2dbc4e';
confirmBtn.style.transform = 'translateY(-1px)';
});
confirmBtn.addEventListener('mouseout', () => {
confirmBtn.style.background = '#28a745';
confirmBtn.style.transform = 'translateY(0)';
});
dialog.querySelector('.dialog-close-x').onclick = () => {
dialog.close();
dialog.remove();
resolve(false);
};
dialog.querySelector('.dialog-cancel').onclick = () => {
dialog.close();
dialog.remove();
resolve(false);
};
dialog.querySelector('.dialog-confirm').onclick = () => {
dialog.close();
dialog.remove();
resolve(true);
};
dialog.showModal();
});
}
function showInputDialog(title, placeholder = '') {
return new Promise((resolve) => {
let dialog = document.createElement('dialog');
dialog.style.padding = '0';
dialog.style.border = 'none';
dialog.style.borderRadius = '12px';
dialog.style.boxShadow = 'none';
dialog.innerHTML = `
<div style="background:#1e1e1e; color:#fff; border-radius:12px; width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 20px; font-size:18px; font-weight:bold;">
${title}
<button class="dialog-close-x" style="background:transparent; border:none; color:#bbb; padding:0; cursor:pointer; font-size:24px;">×</button>
</div>
<div style="padding:5px 20px 16px;">
<input type="text" id="input-dialog-text" placeholder="${placeholder}"
style="width:100%; padding:10px; background:#2b2c2e; color:#fff; border:1px solid #444;
border-radius:4px; font-size:14px; margin-top:8px; box-sizing:border-box; box-shadow:0 2px 5px rgba(0,0,0,0.1) inset;">
</div>
<div style="display:flex; justify-content:flex-end; gap:10px; padding:12px 20px; border-top:1px solid #333;">
<button class="dialog-cancel" style="background:#333; border:1px solid #555; color:#eee; padding:8px 16px; cursor:pointer; border-radius:4px; font-size:14px; transition:all 0.2s;">${getText('cancel')}</button>
<button class="dialog-confirm" style="background:#28a745; border:none; color:#fff; padding:8px 20px; cursor:pointer; border-radius:4px; font-size:14px; transition:all 0.2s;">${getText('ok')}</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const inputElement = dialog.querySelector('#input-dialog-text');
dialog.addEventListener('click', () => {
inputElement.focus();
});
setTimeout(() => {
inputElement.focus();
}, 100);
inputElement.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const inputValue = inputElement.value.trim();
if (inputValue) {
dialog.close();
dialog.remove();
resolve(inputValue);
}
}
});
const cancelBtn = dialog.querySelector('.dialog-cancel');
cancelBtn.addEventListener('mouseover', () => {
cancelBtn.style.background = '#444';
});
cancelBtn.addEventListener('mouseout', () => {
cancelBtn.style.background = '#333';
});
const confirmBtn = dialog.querySelector('.dialog-confirm');
confirmBtn.addEventListener('mouseover', () => {
confirmBtn.style.background = '#2dbc4e';
confirmBtn.style.transform = 'translateY(-1px)';
});
confirmBtn.addEventListener('mouseout', () => {
confirmBtn.style.background = '#28a745';
confirmBtn.style.transform = 'translateY(0)';
});
dialog.querySelector('.dialog-close-x').onclick = () => {
dialog.close();
dialog.remove();
resolve(null);
};
dialog.querySelector('.dialog-cancel').onclick = () => {
dialog.close();
dialog.remove();
resolve(null);
};
dialog.querySelector('.dialog-confirm').onclick = () => {
const inputValue = inputElement.value.trim();
dialog.close();
dialog.remove();
resolve(inputValue);
};
dialog.showModal();
});
}
async function deployWorkflow() {
if (!deployConfig.endpoint || !deployConfig.apiKey) {
showMessage(getText('error'), getText('configRequired'), true);
openConfigDialog();
return;
}
// Find ComfyDeploy node in the workflow
let deployNodes = app.graph.findNodesByType("comfy-deploy/comfy-deploy");
let workflowName = "";
let workflowId = "";
let version = "";
// If node is found, use the values from the node
if (deployNodes.length > 0) {
const deployNode = deployNodes[0];
workflowName = deployNode.widgets[0].value || "";
workflowId = deployNode.widgets[1].value || "";
version = deployNode.widgets[2].value || "";
}
// If no workflow name is found, use custom dialog to prompt user to input
if (!workflowName) {
workflowName = await showInputDialog(getText('enterWorkflowName'), getText('inputWorkflowName'));
if (!workflowName) {
return;
}
}
let confirmMessage;
if (workflowId) {
confirmMessage = `
<div style="margin-bottom:12px">${getText('createNewVersion')} <span style="font-weight:bold;">${workflowName}</span>.</div>
<div style="margin-top:16px; margin-bottom:6px"><b>${getText('workflowId')}:</b> <span style="font-family:monospace; background:#252525; padding:2px 6px; border-radius:3px;">${workflowId}</span></div>
<div><b>${getText('currentVersion')}:</b> <span style="font-family:monospace; background:#252525; padding:2px 6px; border-radius:3px;">${version}</span></div>
`;
} else {
confirmMessage = `<div>${getText('createNewWorkflow')} <span style="font-weight:bold;">${workflowName}</span>.</div>`;
}
const confirmed = await showConfirmDialog(getText('confirmDeploy'), confirmMessage);
if (!confirmed) return;
if (deployDialog && deployDialog.open) {
deployDialog.close();
}
const loadingDialog = showLoading(getText('deploying'));
try {
const prompt = await app.graphToPrompt();
const requestData = {
// workflow: prompt.workflow, // Workflow graph data
prompt_template: JSON.stringify(prompt.output),
workflow_graph: JSON.stringify(prompt.workflow),
};
let url, method;
if (workflowId) {
// update workflow
url = `${deployConfig.endpoint}/workflows/${workflowId}`;
method = 'PUT';
} else {
// new workflow
url = `${deployConfig.endpoint}/workflows`;
method = 'POST';
requestData.workflow_name = workflowName;
}
// Create timeout controller (10 seconds)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
let response;
try {
response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${deployConfig.apiKey}`
},
body: JSON.stringify(requestData),
signal: controller.signal
});
clearTimeout(timeoutId);
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(getText('deployFailed', 'Request timed out (10 seconds)'));
}
throw error;
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || getText('deployFailed', ''));
}
const data = await response.json();
const responseWorkflowName = data.workflow_name || workflowName;
updateOrCreateDeployNode(responseWorkflowName, data.id, data.latest_version || "v1");
loadingDialog.close();
loadingDialog.remove();
const messageKey = workflowId ? 'updateSuccess' : 'deploySuccess';
const formattedMessage = getText(messageKey, responseWorkflowName, data.id, data.latest_version || "v1")
.replace('<br>', '<br><br>')
.replace('Workflow ID:', '<b>Workflow ID:</b>')
.replace('工作流ID:', '<b>工作流ID:</b>')
.replace('Version:', '<b>Version:</b>')
.replace('版本:', '<b>版本:</b>');
showMessage(getText('success'), formattedMessage);
} catch (error) {
loadingDialog.close();
loadingDialog.remove();
showMessage(getText('error'), getText('deployFailed', error.message), true);
}
}
function updateOrCreateDeployNode(workflowName, workflowId, version) {
let deployNodes = app.graph.findNodesByType("comfy-deploy/comfy-deploy");
if (deployNodes.length === 0) {
// If node does not exist, create a new node
app.graph.beforeChange();
const node = LiteGraph.createNode("comfy-deploy/comfy-deploy");
node.properties = {
workflow_name: workflowName,
workflow_id: workflowId,
version: version
};
node.widgets[0].value = workflowName;
node.widgets[1].value = workflowId;
node.widgets[2].value = version;
// Place node in the appropriate position in the canvas visible area
const canvasCenter = [
app.canvas.canvas.width / (2 * app.canvas.ds.scale) - app.canvas.ds.offset[0] / app.canvas.ds.scale,
app.canvas.canvas.height / (2 * app.canvas.ds.scale) - app.canvas.ds.offset[1] / app.canvas.ds.scale
];
node.pos = [canvasCenter[0] - 100, canvasCenter[1] - 50];
app.graph.add(node);
app.graph.afterChange();
app.graph.config_id = "last_saved_config_" + Date.now();
localStorage.setItem("litegraphDiagram", JSON.stringify(app.graph.serialize()));
} else {
const deployNode = deployNodes[0];
// Update properties
deployNode.properties.workflow_name = workflowName;
deployNode.properties.workflow_id = workflowId;
deployNode.properties.version = version;
// Update widgets with callback trigger for Vue compatibility
if (deployNode.widgets && deployNode.widgets.length >= 3) {
// Update widget values and trigger callbacks
const widget0 = deployNode.widgets[0];
const widget1 = deployNode.widgets[1];
const widget2 = deployNode.widgets[2];
widget0.value = workflowName;
if (widget0.callback) {
widget0.callback(workflowName);
}
widget1.value = workflowId;
if (widget1.callback) {
widget1.callback(workflowId);
}
widget2.value = version;
if (widget2.callback) {
widget2.callback(version);
}
}
// Use setProperty if available (for Vue nodes compatibility)
if (typeof deployNode.setProperty === 'function') {
deployNode.setProperty('workflow_name', workflowName);
deployNode.setProperty('workflow_id', workflowId);
deployNode.setProperty('version', version);
}
// Trigger node serialization to ensure changes are saved
if (typeof deployNode.onSerialize === 'function') {
const serialized = {};
deployNode.onSerialize(serialized);
}
app.graph.setDirtyCanvas(true, true);
app.graph.change();
app.graph.config_id = "last_saved_config_" + Date.now();
localStorage.setItem("litegraphDiagram", JSON.stringify(app.graph.serialize()));
}
}
app.registerExtension({
name: 'Comfy Deploy Panel:SidebarTab',
async setup() {
loadConfig();
// Compatible with old UI: insert into main menu (left side)
const interval = setInterval(() => {
const menu = document.querySelector('.comfy-menu');
if (!menu) return;
clearInterval(interval);
if (menu.querySelector('#comfy-api-deploy-menu-btn')) return;
const btn = document.createElement('button');
btn.id = 'comfy-api-deploy-menu-btn';
btn.className = 'comfy-menu-button';
btn.title = 'Comfy Deploy';
btn.innerText = 'ComfyDeploy';
btn.onclick = () => {
openDeployDialog();
};
menu.appendChild(btn);
}, 100);
const updateButtonText = (btn) => {
if (!btn) return;
const label = btn.querySelector('.side-bar-button-label');
if (!label) return;
// Use ComfyUI's settings API to get interface language
let buttonText = 'Deploy';
try {
const locale = app.ui.settings.getSettingValue('Comfy.Locale');
if (locale && locale.toLowerCase().startsWith('zh')) {
buttonText = '部署';
}
} catch (e) {
console.log('comfydeploy: Failed to get ComfyUI language settings, using default English', e);
}
label.textContent = buttonText;
};
// Compatible with new UI: insert into sidebar (right side)
const newUIInterval = setInterval(() => {
const sideToolBar = document.querySelector('.side-tool-bar-container');
if (!sideToolBar) return;
clearInterval(newUIInterval);
if (sideToolBar.querySelector('#comfy-deploy-sidebar-btn')) return;
const btn = document.createElement('button');
btn.id = 'comfy-deploy-sidebar-btn';
btn.className = 'p-button p-component p-button-icon-only p-button-text side-bar-button p-button-secondary';
btn.type = 'button';
btn.setAttribute('aria-label', 'ComfyDeploy');
btn.setAttribute('data-pc-name', 'button');
btn.setAttribute('data-p-disabled', 'false');
btn.setAttribute('data-pc-section', 'root');
btn.setAttribute('data-pd-tooltip', 'true');
btn.style.width = '64px';
btn.style.height = '72px';
btn.style.padding = '8px 0px';
btn.style.boxSizing = 'border-box';
const btnContent = document.createElement('div');
btnContent.className = 'side-bar-button-content';
btnContent.style.display = 'flex';
btnContent.style.flexDirection = 'column';
btnContent.style.alignItems = 'center';
btnContent.style.justifyContent = 'center';
btnContent.style.height = '100%';
// Create icon
const icon = document.createElement('i');
icon.className = 'pi pi-cloud-upload side-bar-button-icon';
icon.style.fontSize = '16px';
icon.style.marginBottom = '4px';
// Create label
const label = document.createElement('span');
label.className = 'side-bar-button-label';
label.style.fontSize = '10px';
btnContent.appendChild(icon);
btnContent.appendChild(label);
btn.appendChild(btnContent);
const buttonLabel = document.createElement('span');
buttonLabel.className = 'p-button-label';
buttonLabel.innerHTML = '&nbsp;';
buttonLabel.setAttribute('data-pc-section', 'label');
btn.appendChild(buttonLabel);
// Add click event
btn.onclick = () => {
openDeployDialog();
};
const templateButton = sideToolBar.querySelector('.templates-tab-button');
if (templateButton) {
templateButton.after(btn);
} else {
const firstButton = sideToolBar.querySelector('button');
if (firstButton) {
firstButton.after(btn);
} else {
sideToolBar.appendChild(btn);
}
}
updateButtonText(btn);
// Use timer to check if language settings change
let lastLocale = app.ui.settings.getSettingValue('Comfy.Locale');
setInterval(() => {
try {
const currentLocale = app.ui.settings.getSettingValue('Comfy.Locale');
if (currentLocale !== lastLocale) {
lastLocale = currentLocale;
updateButtonText(btn);
}
} catch (e) {
console.log('comfydeploy: Failed to get ComfyUI language settings, using default English', e);
}
}, 1000);
}, 100);
}
});