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
This commit is contained in:
Dr.Lt.Data 2025-02-24 21:18:42 +09:00
parent 76d2206058
commit 4fd17b0bf5
6 changed files with 212 additions and 42 deletions

View File

@ -52,6 +52,14 @@ def git_url(fullpath):
def normalize_url(url) -> str: 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): if 'github' in url or (GITHUB_ENDPOINT is not None and GITHUB_ENDPOINT in url):
author = os.path.basename(os.path.dirname(url)) author = os.path.basename(os.path.dirname(url))
@ -62,9 +70,9 @@ def normalize_url(url) -> str:
if repo_name.endswith('.git'): if repo_name.endswith('.git'):
repo_name = repo_name[:-4] 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): def get_url_for_clone(url):

View File

@ -42,7 +42,7 @@ import manager_downloader
from node_package import InstalledNodePackage 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 '') 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 = cnr_utils.read_cnr_info(fullpath) cnr = cnr_utils.read_cnr_info(fullpath)
if cnr is not None: if cnr is not None:
return module_name, cnr['version'], cnr['id'] return module_name, cnr['version'], cnr['id'], None
return None return None
else: else:
@ -1480,10 +1480,18 @@ def identify_node_pack_from_path(fullpath):
cnr_id = cnr_utils.read_cnr_id(fullpath) cnr_id = cnr_utils.read_cnr_id(fullpath)
commit_hash = git_utils.get_commit_hash(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: if cnr_id is not None:
return module_name, commit_hash, cnr_id return module_name, commit_hash, cnr_id, github_id
else: else:
return module_name, commit_hash, '' return module_name, commit_hash, '', github_id
def get_installed_node_packs(): def get_installed_node_packs():
@ -1501,7 +1509,7 @@ def get_installed_node_packs():
is_disabled = not y.endswith('.disabled') 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') disabled_dirs = os.path.join(x, '.disabled')
if os.path.exists(disabled_dirs): if os.path.exists(disabled_dirs):
@ -1514,7 +1522,7 @@ def get_installed_node_packs():
if info is None: if info is None:
continue 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 return res

View File

@ -21,6 +21,8 @@ import { CustomNodesManager } from "./custom-nodes-manager.js";
import { ModelManager } from "./model-manager.js"; import { ModelManager } from "./model-manager.js";
import { SnapshotManager } from "./snapshot.js"; import { SnapshotManager } from "./snapshot.js";
let manager_version = await getVersion();
var docStyle = document.createElement('style'); var docStyle = document.createElement('style');
docStyle.innerHTML = ` docStyle.innerHTML = `
.comfy-toast { .comfy-toast {
@ -42,7 +44,7 @@ docStyle.innerHTML = `
#cm-manager-dialog { #cm-manager-dialog {
width: 1000px; width: 1000px;
height: 450px; height: 455px;
box-sizing: content-box; box-sizing: content-box;
z-index: 1000; z-index: 1000;
overflow-y: auto; overflow-y: auto;
@ -139,7 +141,7 @@ docStyle.innerHTML = `
.cm-notice-board { .cm-notice-board {
width: 290px; width: 290px;
height: 210px; height: 230px;
overflow: auto; overflow: auto;
color: var(--input-text); color: var(--input-text);
border: 1px solid var(--descrip-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", { $el("button.cm-button", {
type: "button", type: "button",
textContent: "Model Manager", textContent: "Model Manager",
@ -987,7 +1001,7 @@ class ManagerMenuDialog extends ComfyDialog {
update_all_button, update_all_button,
update_comfyui_button, update_comfyui_button,
switch_comfyui_button, switch_comfyui_button,
fetch_updates_button, // fetch_updates_button,
$el("br", {}, []), $el("br", {}, []),
restart_stop_button, restart_stop_button,
@ -1318,7 +1332,7 @@ class ManagerMenuDialog extends ComfyDialog {
$el("div.comfy-modal-content", $el("div.comfy-modal-content",
[ [
$el("tr.cm-title", {}, [ $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("br", {}, []),
$el("div.cm-menu-container", $el("div.cm-menu-container",
@ -1460,13 +1474,12 @@ async function getVersion() {
return await version.text(); return await version.text();
} }
app.registerExtension({ app.registerExtension({
name: "Comfy.ManagerMenu", name: "Comfy.ManagerMenu",
aboutPageBadges: [ aboutPageBadges: [
{ {
label: `ComfyUI-Manager ${await getVersion()}`, label: `ComfyUI-Manager ${manager_version}`,
url: 'https://github.com/ltdrdata/ComfyUI-Manager', url: 'https://github.com/ltdrdata/ComfyUI-Manager',
icon: 'pi pi-th-large' icon: 'pi pi-th-large'
} }

View File

@ -363,6 +363,7 @@ const pageHtml = `
<button class="cn-manager-restart">Restart</button> <button class="cn-manager-restart">Restart</button>
<button class="cn-manager-stop">Stop</button> <button class="cn-manager-stop">Stop</button>
<div class="cn-flex-auto"></div> <div class="cn-flex-auto"></div>
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
<button class="cn-manager-check-update">Check Update</button> <button class="cn-manager-check-update">Check Update</button>
<button class="cn-manager-check-missing">Check Missing</button> <button class="cn-manager-check-missing">Check Missing</button>
<button class="cn-manager-install-url">Install via Git URL</button> <button class="cn-manager-install-url">Install via Git URL</button>
@ -374,7 +375,8 @@ const ShowMode = {
UPDATE: "Update", UPDATE: "Update",
MISSING: "Missing", MISSING: "Missing",
FAVORITES: "Favorites", FAVORITES: "Favorites",
ALTERNATIVES: "Alternatives" ALTERNATIVES: "Alternatives",
IN_WORKFLOW: "In Workflow",
}; };
export class CustomNodesManager { export class CustomNodesManager {
@ -586,6 +588,10 @@ export class CustomNodesManager {
label: "Update", label: "Update",
value: ShowMode.UPDATE, value: ShowMode.UPDATE,
hasData: false hasData: false
}, {
label: "In Workflow",
value: ShowMode.IN_WORKFLOW,
hasData: false
}, { }, {
label: "Missing", label: "Missing",
value: ShowMode.MISSING, value: ShowMode.MISSING,
@ -726,7 +732,7 @@ export class CustomNodesManager {
const value = e.target.value const value = e.target.value
this.filter = value; this.filter = value;
const item = this.getFilterItem(value); const item = this.getFilterItem(value);
if (item && !item.hasData) { if (item && (!item.hasData)) {
this.loadData(value); this.loadData(value);
return; 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": { ".cn-manager-check-update": {
click: (e) => { click: (e) => {
e.target.classList.add("cn-btn-loading"); e.target.classList.add("cn-btn-loading");
@ -1529,7 +1543,110 @@ export class CustomNodesManager {
return extension_mappings; 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() { 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; const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading missing nodes (${mode}) ...`); this.showStatus(`Loading missing nodes (${mode}) ...`);
const res = await fetchData(`/customnode/getmappings?mode=${mode}`); const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
@ -1568,23 +1685,8 @@ export class CustomNodesManager {
} }
} }
const registered_nodes = new Set(); let unresolved_missing_nodes = new Set();
for (let i in LiteGraph.registered_node_types) { for (let node_type of missing_nodes) {
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;
if(node_type.startsWith('workflow/') || node_type.startsWith('workflow>')) if(node_type.startsWith('workflow/') || node_type.startsWith('workflow>'))
continue; continue;
@ -1592,26 +1694,25 @@ export class CustomNodesManager {
const packs = name_to_packs[node_type.trim()]; const packs = name_to_packs[node_type.trim()];
if(packs) if(packs)
packs.forEach(url => { packs.forEach(url => {
missing_nodes.add(url); unresolved_missing_nodes.add(url);
}); });
else { else {
for(let j in regex_to_pack) { for(let j in regex_to_pack) {
if(regex_to_pack[j].regex.test(node_type)) { 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) { for(let k in this.custom_nodes) {
let item = this.custom_nodes[k]; let item = this.custom_nodes[k];
if(missing_nodes.has(item.id)) { if(unresolved_missing_nodes.has(item.id)) {
hashMap[item.hash] = true; 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; hashMap[item.hash] = true;
} }
} }
@ -1630,6 +1731,41 @@ export class CustomNodesManager {
return hashMap; 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() { async getAlternatives() {
const mode = manager_instance.datasrc_combo.value; const mode = manager_instance.datasrc_combo.value;
this.showStatus(`Loading alternatives (${mode}) ...`); this.showStatus(`Loading alternatives (${mode}) ...`);
@ -1725,9 +1861,14 @@ export class CustomNodesManager {
hashMap = await this.getAlternatives(); hashMap = await this.getAlternatives();
} else if(this.show_mode == ShowMode.FAVORITES) { } else if(this.show_mode == ShowMode.FAVORITES) {
hashMap = await this.getFavorites(); hashMap = await this.getFavorites();
} else if(this.show_mode == ShowMode.IN_WORKFLOW) {
hashMap = await this.getNodepackInWorkflow();
} }
filterItem.hashMap = hashMap; filterItem.hashMap = hashMap;
filterItem.hasData = true;
if(this.show_mode != ShowMode.IN_WORKFLOW) {
filterItem.hasData = true;
}
} }
for(let k in node_packs) { for(let k in node_packs) {
@ -1779,7 +1920,6 @@ export class CustomNodesManager {
case "disabled": case "disabled":
filterTypes.add("installed"); filterTypes.add("installed");
break; break;
case "not-installed": case "not-installed":
filterTypes.add("not-installed"); filterTypes.add("not-installed");
break; break;

View File

@ -62,13 +62,14 @@ class WorkflowMetadataExtension {
if (moduleType === "custom_nodes") { if (moduleType === "custom_nodes") {
const nodePackageName = modules[1]; const nodePackageName = modules[1];
const { cnr_id, ver } = const { cnr_id, aux_id, ver } =
this.installedNodes[nodePackageName] ?? this.installedNodes[nodePackageName] ??
this.installedNodes[nodePackageName.toLowerCase()] ?? this.installedNodes[nodePackageName.toLowerCase()] ??
{}; {};
if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name
if (cnr_id) nodeProperties.cnr_id = cnr_id; if (cnr_id) nodeProperties.cnr_id = cnr_id;
else nodeProperties.aux_id = aux_id;
if (ver) nodeProperties.ver = ver; if (ver) nodeProperties.ver = ver;
} else if (["nodes", "comfy_extras"].includes(moduleType)) { } else if (["nodes", "comfy_extras"].includes(moduleType)) {
nodeProperties.cnr_id = "comfy-core"; nodeProperties.cnr_id = "comfy-core";

View File

@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-manager" 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." 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" } license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"] dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]