feat: refactor deployment code

This commit is contained in:
bennykok 2024-04-22 00:07:13 +08:00
parent 35fed9aa4d
commit d592a6ba12

View File

@ -5,6 +5,14 @@ import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.23";
const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`; const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
function sendEventToCD(event, data) {
const message = {
type: event,
data: data,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
const ext = { const ext = {
@ -169,9 +177,11 @@ const ext = {
if (comfyUIWorkflow && app && app.loadGraphData) { if (comfyUIWorkflow && app && app.loadGraphData) {
app.loadGraphData(comfyUIWorkflow); app.loadGraphData(comfyUIWorkflow);
} }
} else if (message.type === "deploy") {
deployWorkflow();
} }
} catch (error) { } catch (error) {
console.error("Error processing message:", error); // console.error("Error processing message:", error);
} }
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0) // if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
@ -189,10 +199,16 @@ const ext = {
// } // }
}); });
const message = { app.graph.onAfterChange = ((originalFunction) => async function () {
type: "cd_plugin_setup", const prompt = await app.graphToPrompt();
}; sendEventToCD("cd_plugin_onAfterChange", prompt);
window.parent.postMessage(JSON.stringify(message), "*");
if (typeof originalFunction === "function") {
originalFunction.apply(this, arguments);
}
})(app.graph.onAfterChange);
sendEventToCD("cd_plugin_setup");
}, },
}; };
@ -293,294 +309,301 @@ function createDynamicUIHtml(data) {
return html; return html;
} }
function addButton() { async function deployWorkflow() {
const menu = document.querySelector(".comfy-menu"); const deploy = document.getElementById("deploy-button");
const deploy = document.createElement("button"); /** @type {LGraph} */
deploy.style.position = "relative"; const graph = app.graph;
deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => {
/** @type {LGraph} */
const graph = app.graph;
let { endpoint, apiKey, displayName } = getData(); let { endpoint, apiKey, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show(); configDialog.show();
return; return;
} }
let deployMeta = graph.findNodesByType("ComfyDeploy"); let deployMeta = graph.findNodesByType("ComfyDeploy");
if (deployMeta.length == 0) { if (deployMeta.length == 0) {
const text = await inputDialog.input( const text = await inputDialog.input(
"Create your deployment", "Create your deployment",
"Workflow name", "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`,
`
<div>
A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
<br><br>
<button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
<br><br>
<label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
); );
if (!ok) return; 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 includeDeps = document.getElementById("include-deps").checked; const deployMetaNode = deployMeta[0];
const reuseHash = document.getElementById("reuse-hash").checked;
if (endpoint.endsWith("/")) { const workflow_name = deployMetaNode.widgets[0].value;
endpoint = endpoint.slice(0, -1); const workflow_id = deployMetaNode.widgets[1].value;
}
loadingDialog.showLoading("Generating snapshot");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); const ok = await confirmDialog.confirm(
// console.log(snapshot); `Confirm deployment`,
loadingDialog.close(); `
<div>
if (!snapshot) { A new version of <button style="font-size: 18px;">${workflow_name}</button> will be deployed, do you confirm?
showError( <br><br>
"Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager",
);
return;
}
const title = deploy.querySelector("#button-title"); <button style="font-size: 18px;">${displayName}</button>
<br>
<button style="font-size: 18px;">${endpoint}</button>
const prompt = await app.graphToPrompt(); <br><br>
let deps = undefined; <label>
<input id="include-deps" type="checkbox" checked>Include dependency</input>
</label>
<br>
<label>
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label>
</div>
`,
);
if (!ok) return;
if (includeDeps) { const includeDeps = document.getElementById("include-deps").checked;
loadingDialog.showLoading("Fetching existing version"); const reuseHash = document.getElementById("reuse-hash").checked;
const existing_workflow = await fetch( if (endpoint.endsWith("/")) {
endpoint + "/api/workflow/" + workflow_id, endpoint = endpoint.slice(0, -1);
{ }
method: "GET", loadingDialog.showLoading("Generating snapshot");
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
loadingDialog.close(); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
// console.log(snapshot);
loadingDialog.close();
loadingDialog.showLoading("Generating dependency graph"); if (!snapshot) {
deps = await generateDependencyGraph({ showError(
workflow_api: prompt.output, "Error when deploying",
snapshot: snapshot, "Unable to generate snapshot, please install ComfyUI Manager",
computeFileHash: async (file) => { );
console.log(existing_workflow?.dependencies?.models); return;
}
// Match previous hash for models const title = deploy.querySelector("#button-title");
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) => { const prompt = await app.graphToPrompt();
console.log(file, x.name); let deps = undefined;
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 (includeDeps) {
if ( loadingDialog.showLoading("Fetching existing version");
!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 existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
const depsOk = await confirmDialog.confirm( {
"Check dependencies", method: "GET",
// JSON.stringify(deps, null, 2),
`
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps),
)}" />`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + apiKey, Authorization: "Bearer " + apiKey,
}, },
},
)
.then((x) => x.json())
.catch(() => {
return {};
}); });
console.log(data); loadingDialog.close();
if (data.status !== 200) { loadingDialog.showLoading("Generating dependency graph");
throw new Error(await data.text()); deps = await generateDependencyGraph({
} else { workflow_api: prompt.output,
data = await data.json(); snapshot: snapshot,
} computeFileHash: async (file) => {
console.log(existing_workflow?.dependencies?.models);
loadingDialog.close(); // 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);
title.textContent = "Done"; const match = previousModelHash.find((x) => {
title.style.color = "green"; 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,
});
deployMetaNode.widgets[1].value = data.workflow_id; // Need to find a way to include this if this is not included in comfyui-json level
deployMetaNode.widgets[2].value = data.version; if (
graph.change(); !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",
};
infoDialog.show( loadingDialog.close();
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
);
setTimeout(() => { const depsOk = await confirmDialog.confirm(
title.textContent = "Deploy"; "Check dependencies",
title.style.color = "white"; // JSON.stringify(deps, null, 2),
}, 1000); `
} catch (e) { <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
loadingDialog.close(); <iframe
app.ui.dialog.show(e); style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
console.error(e); src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
title.textContent = "Error"; JSON.stringify(deps),
title.style.color = "red"; )}" />`,
setTimeout(() => { // createDynamicUIHtml(deps),
title.textContent = "Deploy"; );
title.style.color = "white"; if (!depsOk) return;
}, 1000);
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
});
console.log(data);
if (data.status !== 200) {
throw new Error(await data.text());
} else {
data = await data.json();
} }
loadingDialog.close();
title.textContent = "Done";
title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version;
graph.change();
infoDialog.show(
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
);
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
} catch (e) {
loadingDialog.close();
app.ui.dialog.show(e);
console.error(e);
title.textContent = "Error";
title.style.color = "red";
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
}, 1000);
}
}
function addButton() {
const menu = document.querySelector(".comfy-menu");
const deploy = document.createElement("button");
deploy.id = "deploy-button";
deploy.style.position = "relative";
deploy.style.display = "block";
deploy.innerHTML = "<div id='button-title'>Deploy</div>";
deploy.onclick = async () => {
await deployWorkflow()
}; };
const config = document.createElement("img"); const config = document.createElement("img");