import { app } from "./app.js"; import { api } from "./api.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25"; const loadingIcon = ``; function sendEventToCD(event, data) { const message = { type: event, data: data, }; window.parent.postMessage(JSON.stringify(message), "*"); } function dispatchAPIEventData(data) { const msg = JSON.parse(data); // Custom parse error if (msg.error) { let message = msg.error.message; if (msg.error.details) message += ": " + msg.error.details; for (const [nodeID, nodeError] of Object.entries(msg.node_errors)) { message += "\n" + nodeError.class_type + ":"; for (const errorReason of nodeError.errors) { message += "\n - " + errorReason.message + ": " + errorReason.details; } } app.ui.dialog.show(message); if (msg.node_errors) { app.lastNodeErrors = msg.node_errors; app.canvas.draw(true, true); } } switch (msg.event) { case "error": break; case "status": if (msg.data.sid) { // this.clientId = msg.data.sid; // window.name = this.clientId; // use window name so it isnt reused when duplicating tabs // sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow } api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); break; case "progress": api.dispatchEvent(new CustomEvent("progress", { detail: msg.data })); break; case "executing": api.dispatchEvent( new CustomEvent("executing", { detail: msg.data.node }), ); break; case "executed": api.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); break; case "execution_start": api.dispatchEvent( new CustomEvent("execution_start", { detail: msg.data }), ); break; case "execution_error": api.dispatchEvent( new CustomEvent("execution_error", { detail: msg.data }), ); break; case "execution_cached": api.dispatchEvent( new CustomEvent("execution_cached", { detail: msg.data }), ); break; default: api.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data })); // default: // if (this.#registered.has(msg.type)) { // } else { // throw new Error(`Unknown message type ${msg.type}`); // } } } /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @type {ComfyExtension} */ const ext = { name: "BennyKok.ComfyUIDeploy", init(app) { addButton(); const queryParams = new URLSearchParams(window.location.search); const workflow_version_id = queryParams.get("workflow_version_id"); const auth_token = queryParams.get("auth_token"); const org_display = queryParams.get("org_display"); const origin = queryParams.get("origin"); const workspace_mode = queryParams.get("workspace_mode"); if (workspace_mode) { document.querySelector(".comfy-menu").style.display = "none"; sendEventToCD("cd_plugin_onInit"); app.queuePrompt = ((originalFunction) => async () => { // const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onQueuePromptTrigger"); })(app.queuePrompt); // // Intercept the onkeydown event // window.addEventListener( // "keydown", // (event) => { // // Check for specific keys if necessary // console.log("hi"); // if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { // event.preventDefault(); // event.stopImmediatePropagation(); // event.stopPropagation(); // sendEventToCD("cd_plugin_onQueuePrompt", prompt); // } // }, // true, // ); } const data = getData(); let endpoint = data.endpoint; let apiKey = data.apiKey; // If there is auth token override it if (auth_token) { apiKey = auth_token; endpoint = origin; saveData({ displayName: org_display, endpoint: origin, apiKey: auth_token, displayName: org_display, environment: "cloud", }); localStorage.setItem("comfy_deploy_env", "cloud"); } if (!workflow_version_id) { console.error("No workflow_version_id provided in query parameters."); } else { loadingDialog.showLoading( "Loading workflow from " + org_display, "Please wait...", ); fetch(endpoint + "/api/workflow-version/" + workflow_version_id, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }) .then(async (res) => { const data = await res.json(); const { workflow, workflow_id, error } = data; if (error) { infoDialog.showMessage("Unable to load this workflow", error); return; } // // Adding a delay to wait for the intial graph to load // await new Promise((resolve) => setTimeout(resolve, 2000)); workflow?.nodes.forEach((x) => { if (x?.type === "ComfyDeploy") { x.widgets_values[1] = workflow_id; // x.widgets_values[2] = workflow_version.version; } }); /** @type {LGraph} */ app.loadGraphData(workflow); }) .catch((e) => infoDialog.showMessage("Error", e.message)) .finally(() => { loadingDialog.close(); window.history.replaceState( {}, document.title, window.location.pathname, ); }); } }, registerCustomNodes() { /** @type {LGraphNode}*/ class ComfyDeploy { color = LGraphCanvas.node_colors.yellow.color; bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; constructor() { if (!this.properties) { this.properties = {}; this.properties.workflow_name = ""; this.properties.workflow_id = ""; this.properties.version = ""; } ComfyWidgets.STRING( this, "workflow_name", ["", { default: this.properties.workflow_name, multiline: false }], app, ); ComfyWidgets.STRING( this, "workflow_id", ["", { default: this.properties.workflow_id, multiline: false }], app, ); ComfyWidgets.STRING( this, "version", ["", { default: this.properties.version, multiline: false }], app, ); // this.widgets.forEach((w) => { // // w.computeSize = () => [200,10] // w.computedHeight = 2; // }) this.widgets_start_y = 10; this.setSize(this.computeSize()); // const config = { }; // console.log(this); this.serialize_widgets = true; this.isVirtualNode = true; } } // Load default visibility LiteGraph.registerNodeType( "ComfyDeploy", Object.assign(ComfyDeploy, { title_mode: LiteGraph.NORMAL_TITLE, title: "Comfy Deploy", collapsable: true, }), ); ComfyDeploy.category = "deploy"; }, async setup() { // const graphCanvas = document.getElementById("graph-canvas"); window.addEventListener("message", async (event) => { // console.log("message", event); try { const message = JSON.parse(event.data); if (message.type === "graph_load") { const comfyUIWorkflow = message.data; // console.log("recieved: ", comfyUIWorkflow); // Assuming there's a method to load the workflow data into the ComfyUI // This part of the code would depend on how the ComfyUI expects to receive and process the workflow data // For demonstration, let's assume there's a loadWorkflow method in the ComfyUI API if (comfyUIWorkflow && app && app.loadGraphData) { console.log("loadGraphData"); app.loadGraphData(comfyUIWorkflow); } } else if (message.type === "deploy") { // deployWorkflow(); const prompt = await app.graphToPrompt(); // api.handlePromptGenerated(prompt); sendEventToCD("cd_plugin_onDeployChanges", prompt); } else if (message.type === "queue_prompt") { const prompt = await app.graphToPrompt(); api.handlePromptGenerated(prompt); sendEventToCD("cd_plugin_onQueuePrompt", prompt); } else if (message.type === "get_prompt") { const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onGetPrompt", prompt); } else if (message.type === "event") { dispatchAPIEventData(message.data); } else if (message.type === "add_node") { console.log("add node", message.data); app.graph.beforeChange(); var node = LiteGraph.createNode(message.data.type); node.configure({ widgets_values: message.data.widgets_values, }); console.log("node", node); const graphMouse = app.canvas.graph_mouse; node.pos = [graphMouse[0], graphMouse[1]]; app.graph.add(node); app.graph.afterChange(); } // else if (message.type === "refresh") { // sendEventToCD("cd_plugin_onRefresh"); // } } catch (error) { // console.error("Error processing message:", error); } }); api.addEventListener("executed", (evt) => { const images = evt.detail?.output.images; // if (images?.length > 0 && images[0].type === "output") { // generatedImages[evt.detail.node] = images[0].filename; // } // if (evt.detail?.output.gltfFilename) { // } }); app.graph.onAfterChange = ((originalFunction) => async function () { const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onAfterChange", prompt); if (typeof originalFunction === "function") { originalFunction.apply(this, arguments); } })(app.graph.onAfterChange); sendEventToCD("cd_plugin_setup"); }, }; /** * @typedef {import('../../../web/types/litegraph.js').LGraph} LGraph * @typedef {import('../../../web/types/litegraph.js').LGraphNode} LGraphNode */ function showError(title, message) { infoDialog.show( `

${title}


${message} `, ); } function createDynamicUIHtml(data) { console.log(data); let html = '
'; const bgcolor = "var(--comfy-input-bg)"; const evenBg = "var(--border-color)"; const textColor = "var(--input-text)"; // Custom Nodes html += `
`; html += '

Custom Nodes

'; if (data.missing_nodes?.length > 0) { html += `

Missing Nodes

These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database

${data.missing_nodes .map((node) => { return `

${node}

`; }) .join("")}
`; } Object.values(data.custom_nodes).forEach((node) => { html += `
${node.name }

${node.hash}

${node.warning ? `

${node.warning}

` : "" }
`; }); html += "
"; // Models html += `
`; html += '

Models

'; Object.entries(data.models).forEach(([section, items]) => { html += `

${section.charAt(0).toUpperCase() + section.slice(1) }

`; items.forEach((item) => { html += `

${item.name}

`; }); html += "
"; }); html += "
"; // Models html += `
`; html += '

Files

'; Object.entries(data.files).forEach(([section, items]) => { html += `

${section.charAt(0).toUpperCase() + section.slice(1) }

`; items.forEach((item) => { html += `

${item.name}

`; }); html += "
"; }); html += "
"; html += "
"; return html; } async function deployWorkflow() { const deploy = document.getElementById("deploy-button"); /** @type {LGraph} */ const graph = app.graph; let { endpoint, apiKey, displayName } = getData(); if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { configDialog.show(); return; } let deployMeta = graph.findNodesByType("ComfyDeploy"); if (deployMeta.length == 0) { const text = await inputDialog.input( "Create your deployment", "Workflow name", ); if (!text) return; console.log(text); app.graph.beforeChange(); var node = LiteGraph.createNode("ComfyDeploy"); node.configure({ widgets_values: [text], }); node.pos = [0, 0]; app.graph.add(node); app.graph.afterChange(); deployMeta = [node]; } const deployMetaNode = deployMeta[0]; const workflow_name = deployMetaNode.widgets[0].value; const workflow_id = deployMetaNode.widgets[1].value; const ok = await confirmDialog.confirm( `Confirm deployment`, `
A new version of will be deployed, do you confirm?





`, ); if (!ok) return; const includeDeps = document.getElementById("include-deps").checked; const reuseHash = document.getElementById("reuse-hash").checked; if (endpoint.endsWith("/")) { endpoint = endpoint.slice(0, -1); } loadingDialog.showLoading("Generating snapshot"); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); // console.log(snapshot); loadingDialog.close(); if (!snapshot) { showError( "Error when deploying", "Unable to generate snapshot, please install ComfyUI Manager", ); return; } const title = deploy.querySelector("#button-title"); const prompt = await app.graphToPrompt(); let deps = undefined; if (includeDeps) { loadingDialog.showLoading("Fetching existing version"); const existing_workflow = await fetch( endpoint + "/api/workflow/" + workflow_id, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }, ) .then((x) => x.json()) .catch(() => { return {}; }); loadingDialog.close(); loadingDialog.showLoading("Generating dependency graph"); deps = await generateDependencyGraph({ workflow_api: prompt.output, snapshot: snapshot, computeFileHash: async (file) => { console.log(existing_workflow?.dependencies?.models); // Match previous hash for models if (reuseHash && existing_workflow?.dependencies?.models) { const previousModelHash = Object.entries( existing_workflow?.dependencies?.models, ).flatMap(([key, value]) => { return Object.values(value).map((x) => ({ ...x, name: "models/" + key + "/" + x.name, })); }); console.log(previousModelHash); const match = previousModelHash.find((x) => { console.log(file, x.name); return file == x.name; }); console.log(match); if (match && match.hash) { console.log("cached hash used"); return match.hash; } } console.log(file); loadingDialog.showLoading("Generating hash", file); const hash = await fetch( `/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`, ).then((x) => x.json()); loadingDialog.showLoading("Generating hash", file); console.log(hash); return hash.file_hash; }, handleFileUpload: async (file, hash, prevhash) => { console.log("Uploading ", file); loadingDialog.showLoading("Uploading file", file); try { const { download_url } = await fetch(`/comfyui-deploy/upload-file`, { method: "POST", body: JSON.stringify({ file_path: file, token: apiKey, url: endpoint + "/api/upload-url", }), }) .then((x) => x.json()) .catch(() => { loadingDialog.close(); confirmDialog.confirm("Error", "Unable to upload file " + file); }); loadingDialog.showLoading("Uploaded file", file); console.log(download_url); return download_url; } catch (error) { return undefined; } }, existingDependencies: existing_workflow.dependencies, }); // Need to find a way to include this if this is not included in comfyui-json level if ( !deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] && !deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy.git"] ) deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] = { url: "https://github.com/BennyKok/comfyui-deploy", install_type: "git-clone", hash: snapshot?.git_custom_nodes?.[ "https://github.com/BennyKok/comfyui-deploy" ]?.hash ?? "HEAD", name: "ComfyUI Deploy", }; loadingDialog.close(); const depsOk = await confirmDialog.confirm( "Check dependencies", // JSON.stringify(deps, null, 2), `
${loadingIcon}