From 4fd17b0bf58b989a67bc147f4faf6b5700663d4c Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Mon, 24 Feb 2025 21:18:42 +0900 Subject: [PATCH] improved: advanced missing node detection based on embedded info https://github.com/ltdrdata/ComfyUI-Manager/issues/1445 feat: Custom Nodes In Workflow https://github.com/ltdrdata/ComfyUI-Manager/issues/990 https://github.com/ltdrdata/ComfyUI-Manager/issues/127 improved: show version on main dialog modified: aux_id - use github_id if possible removed: `fetch updates` button --- glob/git_utils.py | 12 ++- glob/manager_core.py | 20 ++-- js/comfyui-manager.js | 25 +++-- js/custom-nodes-manager.js | 192 ++++++++++++++++++++++++++++++++----- js/workflow-metadata.js | 3 +- pyproject.toml | 2 +- 6 files changed, 212 insertions(+), 42 deletions(-) diff --git a/glob/git_utils.py b/glob/git_utils.py index 7313bab1..5228198e 100644 --- a/glob/git_utils.py +++ b/glob/git_utils.py @@ -52,6 +52,14 @@ def git_url(fullpath): def normalize_url(url) -> str: + github_id = normalize_to_github_id(url) + if github_id is not None: + url = f"https://github.com/{github_id}" + + return url + + +def normalize_to_github_id(url) -> str: if 'github' in url or (GITHUB_ENDPOINT is not None and GITHUB_ENDPOINT in url): author = os.path.basename(os.path.dirname(url)) @@ -62,9 +70,9 @@ def normalize_url(url) -> str: if repo_name.endswith('.git'): repo_name = repo_name[:-4] - url = f"https://github.com/{author}/{repo_name}" + return f"{author}/{repo_name}" - return url + return None def get_url_for_clone(url): diff --git a/glob/manager_core.py b/glob/manager_core.py index e56f0cdf..04f6ba0e 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -42,7 +42,7 @@ import manager_downloader from node_package import InstalledNodePackage -version_code = [3, 25, 1] +version_code = [3, 26] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') @@ -1472,7 +1472,7 @@ def identify_node_pack_from_path(fullpath): # cnr cnr = cnr_utils.read_cnr_info(fullpath) if cnr is not None: - return module_name, cnr['version'], cnr['id'] + return module_name, cnr['version'], cnr['id'], None return None else: @@ -1480,10 +1480,18 @@ def identify_node_pack_from_path(fullpath): cnr_id = cnr_utils.read_cnr_id(fullpath) commit_hash = git_utils.get_commit_hash(fullpath) + github_id = git_utils.normalize_to_github_id(repo_url) + if github_id is None: + try: + github_id = os.path.basename(repo_url) + except: + logging.warning(f"[ComfyUI-Manager] unexpected repo url: {repo_url}") + github_id = module_name + if cnr_id is not None: - return module_name, commit_hash, cnr_id + return module_name, commit_hash, cnr_id, github_id else: - return module_name, commit_hash, '' + return module_name, commit_hash, '', github_id def get_installed_node_packs(): @@ -1501,7 +1509,7 @@ def get_installed_node_packs(): is_disabled = not y.endswith('.disabled') - res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'enabled': is_disabled } + res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'aux_id': info[3], 'enabled': is_disabled } disabled_dirs = os.path.join(x, '.disabled') if os.path.exists(disabled_dirs): @@ -1514,7 +1522,7 @@ def get_installed_node_packs(): if info is None: continue - res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'enabled': False } + res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'aux_id': info[3], 'enabled': False } return res diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index 7d353e60..79a99b81 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -21,6 +21,8 @@ import { CustomNodesManager } from "./custom-nodes-manager.js"; import { ModelManager } from "./model-manager.js"; import { SnapshotManager } from "./snapshot.js"; +let manager_version = await getVersion(); + var docStyle = document.createElement('style'); docStyle.innerHTML = ` .comfy-toast { @@ -42,7 +44,7 @@ docStyle.innerHTML = ` #cm-manager-dialog { width: 1000px; - height: 450px; + height: 455px; box-sizing: content-box; z-index: 1000; overflow-y: auto; @@ -139,7 +141,7 @@ docStyle.innerHTML = ` .cm-notice-board { width: 290px; - height: 210px; + height: 230px; overflow: auto; color: var(--input-text); border: 1px solid var(--descrip-text); @@ -958,7 +960,19 @@ class ManagerMenuDialog extends ComfyDialog { } }), + $el("button.cm-button", { + type: "button", + textContent: "Custom Nodes In Workflow", + onclick: + () => { + if(!CustomNodesManager.instance) { + CustomNodesManager.instance = new CustomNodesManager(app, self); + } + CustomNodesManager.instance.show(CustomNodesManager.ShowMode.IN_WORKFLOW); + } + }), + $el("br", {}, []), $el("button.cm-button", { type: "button", textContent: "Model Manager", @@ -987,7 +1001,7 @@ class ManagerMenuDialog extends ComfyDialog { update_all_button, update_comfyui_button, switch_comfyui_button, - fetch_updates_button, + // fetch_updates_button, $el("br", {}, []), restart_stop_button, @@ -1318,7 +1332,7 @@ class ManagerMenuDialog extends ComfyDialog { $el("div.comfy-modal-content", [ $el("tr.cm-title", {}, [ - $el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])] + $el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])] ), $el("br", {}, []), $el("div.cm-menu-container", @@ -1460,13 +1474,12 @@ async function getVersion() { return await version.text(); } - app.registerExtension({ name: "Comfy.ManagerMenu", aboutPageBadges: [ { - label: `ComfyUI-Manager ${await getVersion()}`, + label: `ComfyUI-Manager ${manager_version}`, url: 'https://github.com/ltdrdata/ComfyUI-Manager', icon: 'pi pi-th-large' } diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index 6f8fdd02..4c90ec65 100644 --- a/js/custom-nodes-manager.js +++ b/js/custom-nodes-manager.js @@ -363,6 +363,7 @@ const pageHtml = `
+ @@ -374,7 +375,8 @@ const ShowMode = { UPDATE: "Update", MISSING: "Missing", FAVORITES: "Favorites", - ALTERNATIVES: "Alternatives" + ALTERNATIVES: "Alternatives", + IN_WORKFLOW: "In Workflow", }; export class CustomNodesManager { @@ -586,6 +588,10 @@ export class CustomNodesManager { label: "Update", value: ShowMode.UPDATE, hasData: false + }, { + label: "In Workflow", + value: ShowMode.IN_WORKFLOW, + hasData: false }, { label: "Missing", value: ShowMode.MISSING, @@ -726,7 +732,7 @@ export class CustomNodesManager { const value = e.target.value this.filter = value; const item = this.getFilterItem(value); - if (item && !item.hasData) { + if (item && (!item.hasData)) { this.loadData(value); return; } @@ -779,6 +785,14 @@ export class CustomNodesManager { } }, + ".cn-manager-used-in-workflow": { + click: (e) => { + e.target.classList.add("cn-btn-loading"); + this.setFilter(ShowMode.IN_WORKFLOW); + this.loadData(ShowMode.IN_WORKFLOW); + } + }, + ".cn-manager-check-update": { click: (e) => { e.target.classList.add("cn-btn-loading"); @@ -1529,7 +1543,110 @@ export class CustomNodesManager { return extension_mappings; } + getNodesInWorkflow() { + let usedGroupNodes = new Set(); + let allUsedNodes = {}; + + for(let k in app.graph._nodes) { + let node = app.graph._nodes[k]; + + if(node.type.startsWith('workflow>')) { + usedGroupNodes.add(node.type.slice(9)); + continue; + } + + allUsedNodes[node.type] = node; + } + + for(let k of usedGroupNodes) { + let subnodes = app.graph.extra.groupNodes[k]?.nodes; + + if(subnodes) { + for(let k2 in subnodes) { + let node = subnodes[k2]; + allUsedNodes[node.type] = node; + } + } + } + + return allUsedNodes; + } + async getMissingNodes() { + let unresolved_missing_nodes = new Set(); + let hashMap = {}; + let allUsedNodes = this.getNodesInWorkflow(); + + const registered_nodes = new Set(); + for (let i in LiteGraph.registered_node_types) { + registered_nodes.add(LiteGraph.registered_node_types[i].type); + } + + let unresolved_aux_ids = {}; + let outdated_comfyui = false; + + for(let k in allUsedNodes) { + let node = allUsedNodes[k]; + + if(!registered_nodes.has(node.type)) { + // missing node + if(node.properties.cnr_id) { + if(node.properties.cnr_id == 'comfy-core') { + outdated_comfyui = true; + } + + let item = this.custom_nodes[node.properties.cnr_id]; + hashMap[item.hash] = true; + } + else if(node.properties.aux_id) { + unresolved_aux_ids[node.properties.aux_id] = node.type; + } + else { + unresolved_missing_nodes.add(node.type); + } + } + } + + if(outdated_comfyui) { + customAlert('ComfyUI is outdated, so some built-in nodes cannot be used.'); + } + + if(Object.keys(unresolved_aux_ids).length > 0) { + // building aux_id to nodepack map + let aux_id_to_pack = {}; + for(let k in this.custom_nodes) { + let nodepack = this.custom_nodes[k]; + let aux_id; + if(nodepack.repository?.startsWith('https://github.com')) { + aux_id = nodepack.repository.split('/').slice(-2).join('/'); + aux_id_to_pack[aux_id] = nodepack; + } + else if(nodepack.repository) { + aux_id = nodepack.repository.split('/').slice(-1); + aux_id_to_pack[aux_id] = nodepack; + } + } + + // resolving aux_id + for(let k in unresolved_aux_ids) { + let nodepack = aux_id_to_pack[k]; + if(nodepack) { + hashMap[nodepack.hash] = true; + } + else { + unresolved_missing_nodes.add(unresolved_aux_ids[k]); + } + } + } + + if(unresolved_missing_nodes.size > 0) { + await this.getMissingNodesLegacy(hashMap, unresolved_missing_nodes, registered_nodes); + } + + return hashMap; + } + + async getMissingNodesLegacy(hashMap, missing_nodes, registered_nodes) { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading missing nodes (${mode}) ...`); const res = await fetchData(`/customnode/getmappings?mode=${mode}`); @@ -1568,23 +1685,8 @@ export class CustomNodesManager { } } - const registered_nodes = new Set(); - for (let i in LiteGraph.registered_node_types) { - registered_nodes.add(LiteGraph.registered_node_types[i].type); - } - - const missing_nodes = new Set(); - const workflow = app.graph.serialize(); - const group_nodes = workflow.extra && workflow.extra.groupNodes ? workflow.extra.groupNodes : []; - let nodes = workflow.nodes; - - for (let i in group_nodes) { - let group_node = group_nodes[i]; - nodes = nodes.concat(group_node.nodes); - } - - for (let i in nodes) { - const node_type = nodes[i].type; + let unresolved_missing_nodes = new Set(); + for (let node_type of missing_nodes) { if(node_type.startsWith('workflow/') || node_type.startsWith('workflow>')) continue; @@ -1592,26 +1694,25 @@ export class CustomNodesManager { const packs = name_to_packs[node_type.trim()]; if(packs) packs.forEach(url => { - missing_nodes.add(url); + unresolved_missing_nodes.add(url); }); else { for(let j in regex_to_pack) { if(regex_to_pack[j].regex.test(node_type)) { - missing_nodes.add(regex_to_pack[j].url); + unresolved_missing_nodes.add(regex_to_pack[j].url); } } } } } - const hashMap = {}; for(let k in this.custom_nodes) { let item = this.custom_nodes[k]; - if(missing_nodes.has(item.id)) { + if(unresolved_missing_nodes.has(item.id)) { hashMap[item.hash] = true; } - else if (item.files?.some(file => missing_nodes.has(file))) { + else if (item.files?.some(file => unresolved_missing_nodes.has(file))) { hashMap[item.hash] = true; } } @@ -1630,6 +1731,41 @@ export class CustomNodesManager { return hashMap; } + async getNodepackInWorkflow() { + let allUsedNodes = this.getNodesInWorkflow(); + + // building aux_id to nodepack map + let aux_id_to_pack = {}; + for(let k in this.custom_nodes) { + let nodepack = this.custom_nodes[k]; + let aux_id; + if(nodepack.repository?.startsWith('https://github.com')) { + aux_id = nodepack.repository.split('/').slice(-2).join('/'); + aux_id_to_pack[aux_id] = nodepack; + } + else if(nodepack.repository) { + aux_id = nodepack.repository.split('/').slice(-1); + aux_id_to_pack[aux_id] = nodepack; + } + } + + const hashMap = {}; + for(let k in allUsedNodes) { + var item; + if(allUsedNodes[k].properties.cnr_id) { + item = this.custom_nodes[allUsedNodes[k].properties.cnr_id]; + } + else if(allUsedNodes[k].properties.aux_id) { + item = aux_id_to_pack[allUsedNodes[k].properties.aux_id]; + } + + if(item) + hashMap[item.hash] = true; + } + + return hashMap; + } + async getAlternatives() { const mode = manager_instance.datasrc_combo.value; this.showStatus(`Loading alternatives (${mode}) ...`); @@ -1725,9 +1861,14 @@ export class CustomNodesManager { hashMap = await this.getAlternatives(); } else if(this.show_mode == ShowMode.FAVORITES) { hashMap = await this.getFavorites(); + } else if(this.show_mode == ShowMode.IN_WORKFLOW) { + hashMap = await this.getNodepackInWorkflow(); } filterItem.hashMap = hashMap; - filterItem.hasData = true; + + if(this.show_mode != ShowMode.IN_WORKFLOW) { + filterItem.hasData = true; + } } for(let k in node_packs) { @@ -1779,7 +1920,6 @@ export class CustomNodesManager { case "disabled": filterTypes.add("installed"); break; - case "not-installed": filterTypes.add("not-installed"); break; diff --git a/js/workflow-metadata.js b/js/workflow-metadata.js index 7d221ff6..9bbf690b 100644 --- a/js/workflow-metadata.js +++ b/js/workflow-metadata.js @@ -62,13 +62,14 @@ class WorkflowMetadataExtension { if (moduleType === "custom_nodes") { const nodePackageName = modules[1]; - const { cnr_id, ver } = + const { cnr_id, aux_id, ver } = this.installedNodes[nodePackageName] ?? this.installedNodes[nodePackageName.toLowerCase()] ?? {}; if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name if (cnr_id) nodeProperties.cnr_id = cnr_id; + else nodeProperties.aux_id = aux_id; if (ver) nodeProperties.ver = ver; } else if (["nodes", "comfy_extras"].includes(moduleType)) { nodeProperties.cnr_id = "comfy-core"; diff --git a/pyproject.toml b/pyproject.toml index 3a7c8ed2..ce9edd1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-manager" description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." -version = "3.25.1" +version = "3.26" license = { file = "LICENSE.txt" } dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]