Files
dseditor-ComfyUI-ScheduledTask/web/scheduled_task.js
T
2025-06-19 14:16:31 +08:00

935 lines
31 KiB
JavaScript
Raw 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 globalEnabled = false;
let schedules = [];
let workflows = [];
let settingsContainer = null;
// Detect if we're in dark mode
function isDarkMode() {
// Check ComfyUI's theme
const body = document.body;
const computedStyle = getComputedStyle(body);
const bgColor = computedStyle.backgroundColor;
// If background is dark, we're in dark mode
if (bgColor) {
const rgb = bgColor.match(/\d+/g);
if (rgb && rgb.length >= 3) {
const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
return brightness < 128;
}
}
// Fallback: check for dark class or default to false
return body.classList.contains('dark') || body.classList.contains('dark-theme');
}
// Get theme colors
function getThemeColors() {
const darkMode = isDarkMode();
return {
background: darkMode ? '#2b2b2b' : '#ffffff',
surface: darkMode ? '#383838' : '#fafafa',
border: darkMode ? '#555555' : '#e0e0e0',
text: darkMode ? '#ffffff' : '#333333',
textSecondary: darkMode ? '#cccccc' : '#666666',
input: darkMode ? '#404040' : '#ffffff',
inputBorder: darkMode ? '#666666' : '#cccccc',
overlay: darkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.7)'
};
}
// Save workflow as task
async function saveWorkflowAsTask() {
try {
// Check if there are nodes in the current graph
if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) {
showNotification("No workflow to save!", "error");
return;
}
// Show naming dialog
const workflowName = await showNamingDialog();
if (!workflowName) {
return; // User cancelled
}
// Get pure API format (without _meta and other workflow metadata)
let apiWorkflow;
try {
// Use ComfyUI's graphToPrompt to get the API format
const promptData = await app.graphToPrompt();
// promptData.output contains the API format we need
if (promptData.output) {
apiWorkflow = promptData.output;
} else if (promptData.workflow) {
// If output is not available, clean the workflow format
apiWorkflow = cleanWorkflowToAPI(promptData.workflow);
} else {
throw new Error("No valid prompt data generated");
}
} catch (error) {
console.log("graphToPrompt failed, trying manual conversion:", error);
try {
// Manual conversion as fallback
const workflow = app.graph.serialize();
apiWorkflow = convertToAPIFormat(workflow);
} catch (conversionError) {
console.error("Manual conversion failed:", conversionError);
throw new Error("Failed to convert workflow to API format");
}
}
if (!apiWorkflow || Object.keys(apiWorkflow).length === 0) {
throw new Error("Generated API workflow is empty");
}
console.log("Generated API workflow:", apiWorkflow);
// Save workflow
const response = await fetch('/scheduledtask/save_workflow', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: workflowName,
workflow: apiWorkflow
})
});
if (response.ok) {
showNotification(`Workflow saved as "${workflowName}.json"`, "success");
// Refresh workflow list if settings is open
if (settingsContainer) {
workflows = await getWorkflowList();
// Re-render the settings if they're currently displayed
const newContent = await createScheduleSettings();
settingsContainer.innerHTML = '';
settingsContainer.appendChild(newContent);
}
} else {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP ${response.status}`);
}
} catch (error) {
console.error("Failed to save workflow:", error);
showNotification("Failed to save workflow: " + error.message, "error");
}
}
// Clean workflow format to pure API format
function cleanWorkflowToAPI(workflow) {
const apiWorkflow = {};
Object.keys(workflow).forEach(nodeId => {
const node = workflow[nodeId];
// Create clean API node (remove _meta and other metadata)
apiWorkflow[nodeId] = {
class_type: node.class_type,
inputs: { ...node.inputs } // Copy inputs without metadata
};
});
return apiWorkflow;
}
// Show naming dialog
function showNamingDialog() {
return new Promise((resolve) => {
const colors = getThemeColors();
// Create overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: ${colors.overlay};
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
`;
// Create dialog
const dialog = document.createElement('div');
dialog.style.cssText = `
background: ${colors.background};
color: ${colors.text};
border-radius: 8px;
padding: 25px;
min-width: 400px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid ${colors.border};
`;
// Title
const title = document.createElement('h3');
title.textContent = '💾 Save Workflow as Task';
title.style.cssText = `margin: 0 0 20px 0; color: ${colors.text}; text-align: center;`;
// Input container
const inputContainer = document.createElement('div');
inputContainer.style.cssText = 'margin-bottom: 20px;';
const label = document.createElement('label');
label.textContent = 'Workflow Name:';
label.style.cssText = `display: block; margin-bottom: 8px; font-weight: bold; color: ${colors.text};`;
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter workflow name (without .json extension)';
input.style.cssText = `
width: 100%;
padding: 10px;
border: 2px solid ${colors.inputBorder};
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
background: ${colors.input};
color: ${colors.text};
`;
// Auto-generate name based on current time
const now = new Date();
const defaultName = `workflow_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}_${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}`;
input.value = defaultName;
input.select();
inputContainer.appendChild(label);
inputContainer.appendChild(input);
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;';
// Save button
const saveButton = document.createElement('button');
saveButton.textContent = '💾 Save';
saveButton.style.cssText = `
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
`;
// Cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = '❌ Cancel';
cancelButton.style.cssText = `
padding: 10px 20px;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
// Event handlers
function handleSave() {
const name = input.value.trim();
if (!name) {
input.style.borderColor = '#f44336';
input.focus();
return;
}
// Validate filename
const invalidChars = /[<>:"/\\|?*]/g;
if (invalidChars.test(name)) {
showNotification("Invalid characters in filename!", "error");
input.style.borderColor = '#f44336';
input.focus();
return;
}
document.body.removeChild(overlay);
resolve(name);
}
function handleCancel() {
document.body.removeChild(overlay);
resolve(null);
}
saveButton.onclick = handleSave;
cancelButton.onclick = handleCancel;
// Enter key to save
input.onkeydown = (e) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
handleCancel();
}
};
// Click overlay to cancel
overlay.onclick = (e) => {
if (e.target === overlay) {
handleCancel();
}
};
buttonContainer.appendChild(saveButton);
buttonContainer.appendChild(cancelButton);
dialog.appendChild(title);
dialog.appendChild(inputContainer);
dialog.appendChild(buttonContainer);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Focus input
setTimeout(() => input.focus(), 100);
});
}
// Convert workflow to API format
function convertToAPIFormat(workflow) {
try {
const apiWorkflow = {};
// Convert nodes to API format
if (workflow.nodes) {
workflow.nodes.forEach(node => {
if (node.id !== undefined) {
apiWorkflow[node.id.toString()] = {
class_type: node.type,
inputs: {}
};
// Process node properties to get input values
const nodeInputs = apiWorkflow[node.id.toString()].inputs;
// Handle widgets (standalone values)
if (node.widgets_values && node.widgets_values.length > 0) {
// Get widget names from node definition
const nodeClass = LiteGraph.registered_node_types[node.type];
if (nodeClass && nodeClass.prototype) {
const tempNode = new nodeClass();
if (tempNode.widgets) {
tempNode.widgets.forEach((widget, index) => {
if (index < node.widgets_values.length) {
nodeInputs[widget.name] = node.widgets_values[index];
}
});
}
}
}
// Handle connections (linked inputs)
if (node.inputs && workflow.links) {
node.inputs.forEach((input, inputIndex) => {
if (input.link !== null && input.link !== undefined) {
// Find the link
const link = workflow.links.find(l => l && l[0] === input.link);
if (link) {
const sourceNodeId = link[1];
const sourceOutputIndex = link[2];
nodeInputs[input.name] = [
sourceNodeId.toString(),
sourceOutputIndex
];
}
}
});
}
// Fallback: try to get default values from properties
if (node.properties) {
Object.keys(node.properties).forEach(key => {
if (!(key in nodeInputs)) {
nodeInputs[key] = node.properties[key];
}
});
}
}
});
}
return apiWorkflow;
} catch (error) {
console.error("Error converting workflow to API format:", error);
// If conversion fails, try a simpler approach
try {
console.log("Trying alternative conversion method...");
// Use app.graphToPrompt if available
if (app && app.graphToPrompt) {
const prompt = app.graphToPrompt();
if (prompt && prompt.workflow) {
return prompt.workflow;
}
}
// Last resort: return the original workflow if it's already in API format
if (workflow && typeof workflow === 'object' && !workflow.nodes) {
console.log("Using original workflow (appears to be API format)");
return workflow;
}
throw new Error("All conversion methods failed");
} catch (fallbackError) {
console.error("Fallback conversion also failed:", fallbackError);
throw new Error("Failed to convert workflow format");
}
}
}
// Get workflow list
async function getWorkflowList() {
try {
const response = await fetch('/scheduledtask/get_workflows');
if (response.ok) {
const data = await response.json();
return data.workflows || [];
}
} catch (error) {
console.error("Failed to get workflow list:", error);
}
return [];
}
// Save schedule settings
async function saveSchedules() {
try {
// If globally disabled, clear all schedules
const finalSchedules = globalEnabled ? schedules.filter(s =>
s.time && s.workflow && s.enabled
) : [];
const response = await fetch('/scheduledtask/save_schedules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
schedules: finalSchedules,
globalEnabled: globalEnabled
})
});
if (response.ok) {
console.log("Schedule settings saved and applied");
showNotification("Schedule settings saved and applied!", "success");
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error("Save failed:", error);
showNotification("Save failed: " + error.message, "error");
}
return false;
}
// Load existing schedule settings
async function loadSchedules() {
try {
const response = await fetch('/scheduledtask/get_schedules');
if (response.ok) {
const data = await response.json();
globalEnabled = data.globalEnabled || false;
return data.schedules || [];
}
} catch (error) {
console.error("Failed to load schedule settings:", error);
}
return [];
}
// Show notification
function showNotification(message, type = "info") {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
font-weight: bold;
z-index: 10000;
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'};
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 3000);
}
// Create schedule settings interface
async function createScheduleSettings() {
const colors = getThemeColors();
// Load data
try {
const [loadedSchedules, loadedWorkflows] = await Promise.all([
loadSchedules(),
getWorkflowList()
]);
schedules = loadedSchedules;
workflows = loadedWorkflows;
// Ensure at least 3 schedule slots
while (schedules.length < 3) {
schedules.push({
time: '',
workflow: '',
enabled: false
});
}
} catch (error) {
console.error('Failed to load schedule data:', error);
schedules = [{time: '', workflow: '', enabled: false}, {time: '', workflow: '', enabled: false}, {time: '', workflow: '', enabled: false}];
workflows = [];
}
// Create container
const container = document.createElement('div');
container.style.cssText = `
padding: 15px;
border: 2px solid ${colors.border};
border-radius: 8px;
background: ${colors.surface};
color: ${colors.text};
margin: 8px 0;
max-width: 100%;
`;
// Description
const description = document.createElement('div');
description.innerHTML = `
<div style="margin-bottom: 15px; padding: 10px; background: ${isDarkMode() ? '#1a3a4a' : '#e8f4fd'}; border-left: 4px solid #2196F3; border-radius: 4px; font-size: 12px; color: ${colors.text};">
<strong>💡 Info:</strong> Workflow API files should be placed in <strong>ComfyUI-ScheduledTask/Workflow/</strong> folder.
</div>
`;
// Global switch container
const globalSwitchContainer = document.createElement('div');
globalSwitchContainer.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 12px;
background: ${colors.background};
border: 2px solid #4CAF50;
border-radius: 6px;
gap: 10px;
color: ${colors.text};
`;
const globalLabel = document.createElement('label');
globalLabel.innerHTML = '<strong>🔧 Enable Scheduler</strong>';
globalLabel.style.cssText = `font-size: 13px; color: ${colors.text};`;
const globalSwitch = document.createElement('input');
globalSwitch.type = 'checkbox';
globalSwitch.checked = globalEnabled;
globalSwitch.style.cssText = `
width: 16px;
height: 16px;
cursor: pointer;
`;
const globalStatus = document.createElement('span');
globalStatus.style.cssText = 'font-size: 11px; font-weight: bold;';
function updateGlobalStatus() {
globalStatus.textContent = globalEnabled ? '✅ System Active' : '❌ System Disabled';
globalStatus.style.color = globalEnabled ? '#4CAF50' : '#f44336';
}
updateGlobalStatus();
globalSwitch.onchange = (e) => {
globalEnabled = e.target.checked;
updateGlobalStatus();
saveSchedules(); // Auto save
};
globalSwitchContainer.appendChild(globalLabel);
globalSwitchContainer.appendChild(globalSwitch);
globalSwitchContainer.appendChild(globalStatus);
// Schedule list container
const schedulesContainer = document.createElement('div');
schedulesContainer.style.cssText = 'margin-bottom: 15px;';
function renderSchedules() {
schedulesContainer.innerHTML = '';
schedules.forEach((schedule, index) => {
const row = createScheduleRow(schedule, index);
schedulesContainer.appendChild(row);
});
}
// Warning message (if no workflows)
if (workflows.length === 0) {
const warning = document.createElement('div');
warning.innerHTML = `
<div style="padding: 10px; background: ${isDarkMode() ? '#4a3c1a' : '#fff3cd'}; border: 1px solid ${isDarkMode() ? '#665a2a' : '#ffeaa7'}; border-radius: 4px; margin-bottom: 12px; font-size: 12px; color: ${colors.text};">
<strong>⚠️ Warning:</strong> No workflow files found! Please place workflow API-JSON files in <code>ComfyUI-ScheduledTask/Workflow/</code> folder or rightclick save as task.
</div>
`;
container.appendChild(warning);
}
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 8px;
justify-content: flex-end;
border-top: 1px solid ${colors.border};
padding-top: 12px;
`;
// Add schedule button
const addButton = document.createElement('button');
addButton.textContent = '➕ Add Schedule';
addButton.style.cssText = `
padding: 6px 12px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
`;
addButton.onclick = () => {
schedules.push({
time: '',
workflow: '',
enabled: false
});
renderSchedules();
};
// Save button
const saveButton = document.createElement('button');
saveButton.textContent = '💾 Save Settings';
saveButton.style.cssText = `
padding: 6px 12px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
`;
saveButton.onclick = async () => {
saveButton.disabled = true;
saveButton.textContent = 'Saving...';
await saveSchedules();
saveButton.disabled = false;
saveButton.textContent = '💾 Save Settings';
};
buttonContainer.appendChild(addButton);
buttonContainer.appendChild(saveButton);
// Assemble interface
container.appendChild(description);
container.appendChild(globalSwitchContainer);
container.appendChild(schedulesContainer);
container.appendChild(buttonContainer);
renderSchedules();
return container;
}
// Create single schedule setting row
function createScheduleRow(schedule, index) {
const colors = getThemeColors();
const row = document.createElement('div');
row.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
padding: 12px;
border: 1px solid ${colors.border};
border-radius: 4px;
background: ${colors.background};
color: ${colors.text};
font-size: 12px;
flex-wrap: wrap;
min-height: 60px;
`;
// Index
const indexLabel = document.createElement('div');
indexLabel.textContent = `#${index + 1}`;
indexLabel.style.cssText = `
background: #666;
color: white;
padding: 4px 8px;
border-radius: 3px;
font-weight: bold;
min-width: 30px;
text-align: center;
font-size: 11px;
`;
// Enable switch
const enabledContainer = document.createElement('div');
enabledContainer.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 60px;';
const enabledLabel = document.createElement('label');
enabledLabel.textContent = 'Enable';
enabledLabel.style.cssText = `font-size: 10px; color: ${colors.textSecondary};`;
const enabledSwitch = document.createElement('input');
enabledSwitch.type = 'checkbox';
enabledSwitch.checked = schedule.enabled || false;
enabledSwitch.style.cssText = 'width: 16px; height: 16px; cursor: pointer;';
enabledSwitch.onchange = (e) => {
schedule.enabled = e.target.checked;
updateRowStyle();
};
function updateRowStyle() {
row.style.opacity = schedule.enabled ? '1' : '0.6';
row.style.background = schedule.enabled ? colors.background : colors.surface;
}
enabledContainer.appendChild(enabledLabel);
enabledContainer.appendChild(enabledSwitch);
// Time input
const timeContainer = document.createElement('div');
timeContainer.style.cssText = 'display: flex; flex-direction: column; gap: 2px; min-width: 110px;';
const timeLabel = document.createElement('label');
timeLabel.textContent = 'Time';
timeLabel.style.cssText = `font-size: 10px; color: ${colors.textSecondary}; font-weight: bold;`;
const timeInput = document.createElement('input');
timeInput.type = 'time';
timeInput.value = schedule.time || '';
timeInput.style.cssText = `
padding: 6px 8px;
border: 1px solid ${colors.inputBorder};
border-radius: 3px;
font-size: 12px;
width: 100%;
min-width: 100px;
background: ${colors.input};
color: ${colors.text};
box-sizing: border-box;
`;
timeInput.onchange = (e) => {
schedule.time = e.target.value;
};
timeContainer.appendChild(timeLabel);
timeContainer.appendChild(timeInput);
// Workflow selection
const workflowContainer = document.createElement('div');
workflowContainer.style.cssText = 'display: flex; flex-direction: column; gap: 2px; flex-grow: 1; min-width: 200px;';
const workflowLabel = document.createElement('label');
workflowLabel.textContent = 'Workflow File';
workflowLabel.style.cssText = `font-size: 10px; color: ${colors.textSecondary}; font-weight: bold;`;
const workflowSelect = document.createElement('select');
workflowSelect.style.cssText = `
padding: 6px 8px;
border: 1px solid ${colors.inputBorder};
border-radius: 3px;
font-size: 12px;
background: ${colors.input};
color: ${colors.text};
width: 100%;
box-sizing: border-box;
`;
// Add options
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = '-- Select Workflow File --';
workflowSelect.appendChild(defaultOption);
workflows.forEach(w => {
const option = document.createElement('option');
option.value = w.filename;
option.textContent = `${w.name} (${w.filename})`;
if (schedule.workflow === w.filename) {
option.selected = true;
}
workflowSelect.appendChild(option);
});
workflowSelect.onchange = (e) => {
schedule.workflow = e.target.value;
};
workflowContainer.appendChild(workflowLabel);
workflowContainer.appendChild(workflowSelect);
// Delete button
const deleteButton = document.createElement('button');
deleteButton.textContent = '❌';
deleteButton.title = 'Delete this schedule';
deleteButton.style.cssText = `
background: #f44336;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
padding: 8px 10px;
font-size: 12px;
min-width: 40px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
`;
deleteButton.onclick = () => {
if (confirm('Are you sure you want to delete this schedule?')) {
schedules.splice(index, 1);
// Re-render entire list
const container = row.parentElement;
container.innerHTML = '';
schedules.forEach((s, i) => {
const newRow = createScheduleRow(s, i);
container.appendChild(newRow);
});
}
};
// Assemble row
row.appendChild(indexLabel);
row.appendChild(enabledContainer);
row.appendChild(timeContainer);
row.appendChild(workflowContainer);
row.appendChild(deleteButton);
updateRowStyle();
return row;
}
// Check if ComfyUI is loaded
function waitForComfyUI() {
return new Promise((resolve) => {
if (app) {
resolve();
} else {
const checkInterval = setInterval(() => {
if (window.app) {
clearInterval(checkInterval);
resolve();
}
}, 100);
}
});
}
// Register extension
waitForComfyUI().then(() => {
console.log("🔧 Starting to register ComfyUI-ScheduledTask extension");
app.registerExtension({
name: "ComfyUI-ScheduledTask",
settings: [
{
id: "ScheduledTask.ScheduledTask",
name: "🎨",
type: () => {
console.log("🎨 Creating schedule settings interface");
if (!settingsContainer) {
// Create loading container
settingsContainer = document.createElement('div');
const colors = getThemeColors();
settingsContainer.style.cssText = `
padding: 15px;
text-align: center;
font-size: 12px;
color: ${colors.text};
background: ${colors.surface};
`;
settingsContainer.textContent = 'Loading schedule settings...';
// Load actual content asynchronously
createScheduleSettings().then(content => {
settingsContainer.innerHTML = '';
settingsContainer.appendChild(content);
console.log("✅ Schedule settings interface loaded successfully");
}).catch(error => {
console.error("❌ Failed to load schedule settings interface:", error);
const colors = getThemeColors();
settingsContainer.innerHTML = `<div style="padding: 15px; color: #f44336; font-size: 12px; background: ${colors.surface};">Loading failed: ${error.message}</div>`;
});
}
return settingsContainer;
},
defaultValue: true,
tooltip: "Configure and manage workflow scheduling tasks"
}
],
async setup() {
console.log("✅ ComfyUI-ScheduledTask extension loaded");
// Add context menu item
const origGetCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = origGetCanvasMenuOptions.apply(this, arguments);
// Add separator
options.push(null);
// Add our menu item
options.push({
content: "📋 Save as Task",
callback: () => {
saveWorkflowAsTask();
}
});
return options;
};
}
});
}).catch(error => {
console.error("❌ Failed to register ComfyUI-ScheduledTask extension:", error);
});