From e043ec865cde3cbcc2bb990dc47e8051c3d7b9c3 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 13:23:35 +0200 Subject: [PATCH] fix(manager): support ComfyUI core's bundled manager (v4 /v2 API) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- js/nodes_stats.js | 104 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/js/nodes_stats.js b/js/nodes_stats.js index e34f35c..1088db6 100644 --- a/js/nodes_stats.js +++ b/js/nodes_stats.js @@ -491,6 +491,43 @@ function buildTable(packages, status, withActions, managerInfo) { // 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: // { : { id, version, files, state }, ... } // We read the unified list (/customnode/getlist) rather than /customnode/installed @@ -502,8 +539,8 @@ function buildTable(packages, status, withActions, managerInfo) { // omitted entirely. async function fetchManagerInfo() { try { - const resp = await fetch("/customnode/getlist?mode=local&skip_update=true"); - if (!resp.ok) return null; + const resp = await mgrFetch("/customnode/getlist?mode=local&skip_update=true"); + if (!resp || !resp.ok) return null; const data = await resp.json(); const packs = data && data.node_packs; if (!packs || typeof packs !== "object") return null; @@ -589,8 +626,8 @@ async function ensureDisabledCatalog(forceRefresh = false) { if (!managerInfo) return null; // Manager absent let mappings = {}; try { - const r = await fetch("/customnode/getmappings?mode=local"); - if (r.ok) mappings = await r.json(); + const r = await mgrFetch("/customnode/getmappings?mode=local"); + if (r && r.ok) mappings = await r.json(); } catch { /* fall through -> empty catalog */ } _disabledCatalog = buildDisabledCatalog(mappings, managerInfo); return _disabledCatalog; @@ -634,10 +671,10 @@ async function classifyUnresolved(types) { let mappings = {}, managerInfo = null; try { 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}} ]); - if (mResp.ok) mappings = await mResp.json(); + if (mResp && mResp.ok) mappings = await mResp.json(); managerInfo = gi; } catch { /* manager absent */ } @@ -675,8 +712,8 @@ async function classifyUnresolved(types) { async function resolveInstallTarget(packKey) { let packs; try { - const r = await fetch("/customnode/getlist?mode=local&skip_update=true"); - if (!r.ok) return null; + const r = await mgrFetch("/customnode/getlist?mode=local&skip_update=true"); + if (!r || !r.ok) return null; packs = (await r.json()).node_packs; } catch { return null; } if (!packs) return null; @@ -706,8 +743,8 @@ function installPayload(entry, packKey) { async function findInstalledDir(entry) { let packs = null; try { - const r = await fetch("/customnode/getlist?mode=local&skip_update=true"); - if (r.ok) packs = (await r.json()).node_packs; + const r = await mgrFetch("/customnode/getlist?mode=local&skip_update=true"); + if (r && r.ok) packs = (await r.json()).node_packs; } catch { /* fall through to basename */ } if (packs) { 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); 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) { notify("ComfyUI Manager is busy. Please try again in a moment.", "warn"); 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 -// 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) { + 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" } }); for (const payload of payloads) { @@ -864,8 +924,8 @@ function enablePayload(dirName, info) { // the same way before calling runManagerDisable). async function managerIsBusy() { try { - const r = await fetch("/manager/queue/status"); - if (!r.ok) return false; + const r = await mgrFetch("/manager/queue/status"); + if (!r || !r.ok) return false; const st = await r.json(); return !!(st && st.is_processing); } 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) { + 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" } }); const r = await fetch("/manager/queue/install", { @@ -1328,8 +1398,8 @@ async function waitForQueue(timeoutMs = 60000) { while (Date.now() < deadline) { let st = null; try { - const r = await fetch("/manager/queue/status"); - if (r.ok) st = await r.json(); + const r = await mgrFetch("/manager/queue/status"); + if (r && r.ok) st = await r.json(); } catch { /* transient; retry */ } if (st && !st.is_processing && st.in_progress_count === 0) return; 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; notify("Restarting ComfyUI…", "info"); try { - await fetch("/manager/reboot", { method: "POST", headers: { "Content-Type": "application/json" } }); + await mgrFetch("/manager/reboot", { method: "POST", headers: { "Content-Type": "application/json" } }); } catch { // The reboot tears down the connection, so a network error here is expected. }