Compare commits

...

2 Commits

Author SHA1 Message Date
Ethanfel 2692bb1752 docs: note built-in manager (--enable-manager-legacy-ui) support; bump to 1.7.1
Publish to Comfy registry / Publish Custom Node to registry (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:24:30 +02:00
Ethanfel e043ec865c fix(manager): support ComfyUI core's bundled manager (v4 /v2 API)
ComfyUI core now bundles comfyui_manager v4 (enabled via --enable-manager),
which serves all Manager HTTP routes under a /v2 prefix and replaces the
per-item /manager/queue/install and /manager/queue/disable endpoints with a
single /v2/manager/queue/batch call. The extension called the old unprefixed
paths, so every request 404'd, fetchManagerInfo() returned null, and all
disable/enable/install/trial features went inert ("can't find manager").

Add detectManagerApi() to probe once (cheap queue/status) and cache which
generation answers: v3 (old git Manager, unprefixed, per-item) or v4-legacy
(/v2 + batch). Route every read through a prefix-aware mgrFetch(), and branch
install/disable on the batch transport via a shared runManagerBatch() helper.
Payload shapes are unchanged — v4's _install_custom_node/_disable_node read the
same fields. Verified against a live v4-legacy server on :8189.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:23:35 +02:00
3 changed files with 95 additions and 18 deletions
+7
View File
@@ -81,6 +81,13 @@ package, plus a **Disable all** button per section. Disabling:
If ComfyUI Manager is not installed, the disable buttons are hidden and stats work as before. If ComfyUI Manager is not installed, the disable buttons are hidden and stats work as before.
> **Manager compatibility:** works with both the standalone
> [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) custom node and
> ComfyUI core's built-in manager. For the built-in manager, launch ComfyUI with
> `--enable-manager-legacy-ui` (the extension detects the active manager and its
> API automatically). Without a reachable manager, all enable/disable/install
> actions are simply omitted.
### Workflow tab & temporary enable ### Workflow tab & temporary enable
Whenever you load a workflow, the extension scans for node types the running Whenever you load a workflow, the extension scans for node types the running
+87 -17
View File
@@ -491,6 +491,43 @@ function buildTable(packages, status, withActions, managerInfo) {
// ComfyUI Manager integration: disable unused node packages // ComfyUI Manager integration: disable unused node packages
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ComfyUI Manager ships in two HTTP API generations we support:
// • v3 — the standalone git custom_nodes/ComfyUI-Manager. Unprefixed paths
// (/customnode/..., /manager/...) and per-item queue endpoints
// (/manager/queue/install, /manager/queue/disable).
// • v4-legacy — the `comfyui_manager` package bundled with ComfyUI core, run
// with `--enable-manager-legacy-ui`. Same routes under a /v2 prefix, but the
// per-item install/disable endpoints are gone: everything is queued through
// one /v2/manager/queue/batch call ({install:[...], disable:[...]}) that
// also starts the worker.
// We probe once (cheap queue/status, present in both) and cache which generation
// answers, so every later call targets the right prefix + transport. null means
// no Manager is reachable — the disable/enable/install UI is then omitted.
let _managerApiProbe = null;
function detectManagerApi() {
if (!_managerApiProbe) {
_managerApiProbe = (async () => {
for (const prefix of ["/v2", ""]) { // prefer v4-legacy; fall back to v3
try {
const r = await fetch(`${prefix}/manager/queue/status`);
if (r.ok) return { prefix, batch: prefix === "/v2" };
} catch { /* try next generation */ }
}
return null;
})();
}
return _managerApiProbe;
}
// Fetch a Manager endpoint on whichever generation is active, prefixing the path
// as needed. Returns the Response, or null when no Manager is reachable (callers
// treat null exactly like a failed/absent Manager).
async function mgrFetch(path, opts) {
const api = await detectManagerApi();
if (!api) return null;
return fetch(api.prefix + path, opts);
}
// Map of installed packages from ComfyUI Manager, keyed by directory name: // Map of installed packages from ComfyUI Manager, keyed by directory name:
// { <dir name>: { id, version, files, state }, ... } // { <dir name>: { id, version, files, state }, ... }
// We read the unified list (/customnode/getlist) rather than /customnode/installed // We read the unified list (/customnode/getlist) rather than /customnode/installed
@@ -502,8 +539,8 @@ function buildTable(packages, status, withActions, managerInfo) {
// omitted entirely. // omitted entirely.
async function fetchManagerInfo() { async function fetchManagerInfo() {
try { try {
const resp = await fetch("/customnode/getlist?mode=local&skip_update=true"); const resp = await mgrFetch("/customnode/getlist?mode=local&skip_update=true");
if (!resp.ok) return null; if (!resp || !resp.ok) return null;
const data = await resp.json(); const data = await resp.json();
const packs = data && data.node_packs; const packs = data && data.node_packs;
if (!packs || typeof packs !== "object") return null; if (!packs || typeof packs !== "object") return null;
@@ -589,8 +626,8 @@ async function ensureDisabledCatalog(forceRefresh = false) {
if (!managerInfo) return null; // Manager absent if (!managerInfo) return null; // Manager absent
let mappings = {}; let mappings = {};
try { try {
const r = await fetch("/customnode/getmappings?mode=local"); const r = await mgrFetch("/customnode/getmappings?mode=local");
if (r.ok) mappings = await r.json(); if (r && r.ok) mappings = await r.json();
} catch { /* fall through -> empty catalog */ } } catch { /* fall through -> empty catalog */ }
_disabledCatalog = buildDisabledCatalog(mappings, managerInfo); _disabledCatalog = buildDisabledCatalog(mappings, managerInfo);
return _disabledCatalog; return _disabledCatalog;
@@ -634,10 +671,10 @@ async function classifyUnresolved(types) {
let mappings = {}, managerInfo = null; let mappings = {}, managerInfo = null;
try { try {
const [mResp, gi] = await Promise.all([ const [mResp, gi] = await Promise.all([
fetch("/customnode/getmappings?mode=local"), mgrFetch("/customnode/getmappings?mode=local"),
fetchManagerInfo(), // getlist -> {dir: {id, cnr_id, aux_id, version, files, state}} fetchManagerInfo(), // getlist -> {dir: {id, cnr_id, aux_id, version, files, state}}
]); ]);
if (mResp.ok) mappings = await mResp.json(); if (mResp && mResp.ok) mappings = await mResp.json();
managerInfo = gi; managerInfo = gi;
} catch { /* manager absent */ } } catch { /* manager absent */ }
@@ -675,8 +712,8 @@ async function classifyUnresolved(types) {
async function resolveInstallTarget(packKey) { async function resolveInstallTarget(packKey) {
let packs; let packs;
try { try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true"); const r = await mgrFetch("/customnode/getlist?mode=local&skip_update=true");
if (!r.ok) return null; if (!r || !r.ok) return null;
packs = (await r.json()).node_packs; packs = (await r.json()).node_packs;
} catch { return null; } } catch { return null; }
if (!packs) return null; if (!packs) return null;
@@ -706,8 +743,8 @@ function installPayload(entry, packKey) {
async function findInstalledDir(entry) { async function findInstalledDir(entry) {
let packs = null; let packs = null;
try { try {
const r = await fetch("/customnode/getlist?mode=local&skip_update=true"); const r = await mgrFetch("/customnode/getlist?mode=local&skip_update=true");
if (r.ok) packs = (await r.json()).node_packs; if (r && r.ok) packs = (await r.json()).node_packs;
} catch { /* fall through to basename */ } } catch { /* fall through to basename */ }
if (packs) { if (packs) {
const want = [entry.id, entry.repository, ...(entry.files || [])].filter(Boolean).map(normKey); const want = [entry.id, entry.repository, ...(entry.files || [])].filter(Boolean).map(normKey);
@@ -780,7 +817,7 @@ async function handleDisable(pkgNames, dialog, managerInfo) {
setDisableButtonsBusy(dialog, true); setDisableButtonsBusy(dialog, true);
try { try {
const pre = await fetch("/manager/queue/status").then((r) => (r.ok ? r.json() : null)).catch(() => null); const pre = await mgrFetch("/manager/queue/status").then((r) => (r && r.ok ? r.json() : null)).catch(() => null);
if (pre && pre.is_processing) { if (pre && pre.is_processing) {
notify("ComfyUI Manager is busy. Please try again in a moment.", "warn"); notify("ComfyUI Manager is busy. Please try again in a moment.", "warn");
setDisableButtonsBusy(dialog, false); setDisableButtonsBusy(dialog, false);
@@ -815,9 +852,32 @@ async function handleDisable(pkgNames, dialog, managerInfo) {
} }
} }
// v4-legacy: queue a batch of operations ({install:[...]}, {disable:[...]}, ...)
// through the single endpoint that also starts the worker, then wait for it to
// finish. A reset first clears any stale queue (harmless if empty).
async function runManagerBatch(api, body) {
await fetch(api.prefix + "/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } });
const r = await fetch(api.prefix + "/manager/queue/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`queue batch failed (HTTP ${r.status})`);
await waitForQueue();
}
// Queue the disable tasks and run them, then wait for the Manager worker to // Queue the disable tasks and run them, then wait for the Manager worker to
// finish. /manager/queue/start returns 201 if a worker is already running. // finish. v4-legacy batches every item through one /v2/manager/queue/batch call;
// v3 posts each item to /manager/queue/disable then /manager/queue/start (which
// returns 201 if a worker is already running).
async function runManagerDisable(payloads) { async function runManagerDisable(payloads) {
const api = await detectManagerApi();
if (!api) throw new Error("ComfyUI Manager not available");
if (api.batch) {
await runManagerBatch(api, { disable: payloads });
return;
}
await fetch("/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } }); await fetch("/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } });
for (const payload of payloads) { for (const payload of payloads) {
@@ -864,8 +924,8 @@ function enablePayload(dirName, info) {
// the same way before calling runManagerDisable). // the same way before calling runManagerDisable).
async function managerIsBusy() { async function managerIsBusy() {
try { try {
const r = await fetch("/manager/queue/status"); const r = await mgrFetch("/manager/queue/status");
if (!r.ok) return false; if (!r || !r.ok) return false;
const st = await r.json(); const st = await r.json();
return !!(st && st.is_processing); return !!(st && st.is_processing);
} catch { } catch {
@@ -873,7 +933,17 @@ async function managerIsBusy() {
} }
} }
// Install a pack or re-enable a disabled one (both go through Manager's install
// queue). v4-legacy batches it as {install:[payload]}; v3 posts the payload to
// /manager/queue/install then /manager/queue/start.
async function runManagerEnable(payload) { async function runManagerEnable(payload) {
const api = await detectManagerApi();
if (!api) throw new Error("ComfyUI Manager not available");
if (api.batch) {
await runManagerBatch(api, { install: [payload] });
return;
}
await fetch("/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } }); await fetch("/manager/queue/reset", { method: "POST", headers: { "Content-Type": "application/json" } });
const r = await fetch("/manager/queue/install", { const r = await fetch("/manager/queue/install", {
@@ -1328,8 +1398,8 @@ async function waitForQueue(timeoutMs = 60000) {
while (Date.now() < deadline) { while (Date.now() < deadline) {
let st = null; let st = null;
try { try {
const r = await fetch("/manager/queue/status"); const r = await mgrFetch("/manager/queue/status");
if (r.ok) st = await r.json(); if (r && r.ok) st = await r.json();
} catch { /* transient; retry */ } } catch { /* transient; retry */ }
if (st && !st.is_processing && st.in_progress_count === 0) return; if (st && !st.is_processing && st.in_progress_count === 0) return;
await sleep(500); await sleep(500);
@@ -1396,7 +1466,7 @@ async function rebootComfy() {
if (!confirm("Restart ComfyUI now? The server will go down briefly and the page will reconnect.")) return; if (!confirm("Restart ComfyUI now? The server will go down briefly and the page will reconnect.")) return;
notify("Restarting ComfyUI…", "info"); notify("Restarting ComfyUI…", "info");
try { try {
await fetch("/manager/reboot", { method: "POST", headers: { "Content-Type": "application/json" } }); await mgrFetch("/manager/reboot", { method: "POST", headers: { "Content-Type": "application/json" } });
} catch { } catch {
// The reboot tears down the connection, so a network error here is expected. // The reboot tears down the connection, so a network error here is expected.
} }
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-nodes-stats" name = "comfyui-nodes-stats"
description = "Track usage statistics for all ComfyUI nodes and packages" description = "Track usage statistics for all ComfyUI nodes and packages"
version = "1.7.0" version = "1.7.1"
license = "MIT" license = "MIT"
[project.urls] [project.urls]