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!
Workflow ID: {1}
Version: {2}', zh: '工作流 "{0}" 部署成功!
工作流ID: {1}
版本: {2}' }, updateSuccess: { en: 'Workflow "{0}" updated successfully!
Workflow ID: {1}
Version: {2}', zh: '工作流 "{0}" 更新成功!
工作流ID: {1}
版本: {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 = `
${getText('deployDialogTitle')}

${getText('deployDescription')}

${getText('deployNodeInfo')}
${getText('configOptions')}
${getText('statusReady')}
${getText('docs')}
`; 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 = `
${getText('configTitle')}
`; 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 = `
${title}

${message}

`; 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 = `

${message}

`; 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 = `
${title}
${message}
`; 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 = `
${title}
`; 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 = `
${getText('createNewVersion')} ${workflowName}.
${getText('workflowId')}: ${workflowId}
${getText('currentVersion')}: ${version}
`; } else { confirmMessage = `
${getText('createNewWorkflow')} ${workflowName}.
`; } 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('
', '

') .replace('Workflow ID:', 'Workflow ID:') .replace('工作流ID:', '工作流ID:') .replace('Version:', 'Version:') .replace('版本:', '版本:'); 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 = ' '; 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); } });