diff --git a/.env.example b/.env.example index 4cc7202..4cdce2d 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,16 @@ PORT=3000 HOST=0.0.0.0 -# ComfyUI on the Windows desktop. Desktop hops ports (8188/8189/8190/...). -# The host agent (scripts/comfy-host-agent.mjs) finds the live instance and proxies it on 8198. +# ComfyUI on the Windows desktop hops listen ports (8188/8189/8190/...). +# Coolify never talks to those. The host agent scans for the live instance +# and always fronts it on 8198. Point the site at that door (or just set +# COMFY_CONTROL_URL — the app rewrites Beast HTTP to 8198 automatically). COMFY_HOST=http://192.168.77.7:8198 COMFY_PORT=8198 # Optional: command used when this server runs on the Comfy host COMFY_LAUNCH_CMD= # Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely COMFY_CONTROL_URL=http://192.168.77.7:8199 +# COMFY_PROXY_PORT=8198 COMFY_CONTROL_TOKEN= # Optional. If set, MyMonitor must send it as X-Monitor-Token (or ?token=). # MONITOR_TOKEN= diff --git a/nuxt.config.ts b/nuxt.config.ts index c6f2f9a..7df42e1 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -32,6 +32,7 @@ export default defineNuxtConfig({ comfyLaunchCmd: process.env.COMFY_LAUNCH_CMD || '', comfyControlUrl: process.env.COMFY_CONTROL_URL || '', comfyControlToken: process.env.COMFY_CONTROL_TOKEN || '', + comfyProxyPort: process.env.COMFY_PROXY_PORT || '8198', monitorToken: process.env.MONITOR_TOKEN || '', imageComfyHost: process.env.IMAGE_COMFY_HOST || '', imageComfyPort: process.env.IMAGE_COMFY_PORT || '', diff --git a/scripts/comfy-host-agent.mjs b/scripts/comfy-host-agent.mjs index 00beec0..6f64bed 100644 --- a/scripts/comfy-host-agent.mjs +++ b/scripts/comfy-host-agent.mjs @@ -10,7 +10,7 @@ const execFileAsync = promisify(execFile) const port = Number(process.env.COMFY_CONTROL_PORT || 8199) const proxyPort = Number(process.env.COMFY_PROXY_PORT || 8198) const token = process.env.COMFY_CONTROL_TOKEN || '' -const defaultHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '') +const defaultHttp = (process.env.COMFY_LISTEN || 'http://127.0.0.1:8188').replace(/\/$/, '') const idleMs = Math.max(60_000, Number(process.env.COMFY_IDLE_MS || 30 * 60 * 1000) || 30 * 60 * 1000) const trainingUrl = String(process.env.TRAINING_CONTROL_URL || 'http://127.0.0.1:8200').replace(/\/$/, '') @@ -94,14 +94,20 @@ function candidatePorts() { return ordered } -async function probeStats(portNum, timeoutMs = 800) { +const probeSkipUntil = new Map() + +async function probeStats(portNum, timeoutMs = 400) { + if (Date.now() < (probeSkipUntil.get(portNum) || 0)) return null try { const res = await fetch(`http://127.0.0.1:${portNum}/system_stats`, { signal: AbortSignal.timeout(timeoutMs) }) if (!res.ok) return null const stats = await res.json() if (!stats?.system) return null + probeSkipUntil.delete(portNum) return stats - } catch { + } catch (error) { + const timedOut = error?.name === 'TimeoutError' || error?.name === 'AbortError' || /timeout/i.test(String(error?.message || '')) + if (timedOut) probeSkipUntil.set(portNum, Date.now() + 30_000) return null } } @@ -133,15 +139,15 @@ async function findHealthyPort() { let proxyServer = null let proxyTarget = 0 -function ensureProxy(targetPort) { - if (!targetPort) return - if (proxyTarget !== targetPort) { - proxyTarget = targetPort - console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-target', listen: proxyPort, target: proxyTarget })) - } +function ensureProxyListening() { if (proxyServer) return proxyServer = net.createServer((client) => { - const upstream = net.connect(proxyTarget, '127.0.0.1') + const target = proxyTarget + if (!target) { + client.destroy() + return + } + const upstream = net.connect(target, '127.0.0.1') const fail = () => { try { client.destroy() } catch { /* ignore */ } try { upstream.destroy() } catch { /* ignore */ } @@ -159,26 +165,35 @@ function ensureProxy(targetPort) { console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-error', error: String(error.message || error) })) }) proxyServer.listen(proxyPort, '0.0.0.0', () => { - console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy', listen: proxyPort, target: proxyTarget })) + console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy', listen: proxyPort, target: proxyTarget || null })) }) } +function setProxyTarget(targetPort) { + ensureProxyListening() + const next = Number(targetPort) || 0 + if (proxyTarget === next) return + proxyTarget = next + console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-target', listen: proxyPort, target: proxyTarget || null })) +} + function markAsleep() { stoppedByAgent = true lastQueueRunning = 0 lastHealthyPort = 0 lastProcessUp = false lastLaunchAt = 0 - proxyTarget = 0 + setProxyTarget(0) } async function syncProxy() { const healthyPort = await findHealthyPort() lastHealthyPort = healthyPort || 0 if (healthyPort) { - ensureProxy(healthyPort) + setProxyTarget(healthyPort) lastProcessUp = true } else { + setProxyTarget(0) lastProcessUp = false } return healthyPort @@ -211,7 +226,9 @@ function readJsonFile(path) { function desktopListenPort() { try { const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`) - return Number(url.port || 8188) || 8188 + const next = Number(url.port || 8188) || 8188 + if (next === port || next === proxyPort) return 8188 + return next } catch { return 8188 } @@ -641,7 +658,7 @@ const server = http.createServer(async (req, res) => { lastActivityAt: new Date(lastWorkAt).toISOString(), idleMs, port: healthyPort || null, - proxyPort: healthyPort ? proxyPort : null, + proxyPort, training: lastTraining }) } @@ -661,7 +678,7 @@ const server = http.createServer(async (req, res) => { return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort }) } if (lastLaunchAt && Date.now() - lastLaunchAt < 60_000) { - return json(res, 200, { ok: true, started: false, already: true, booting: true, asleep: false }) + return json(res, 200, { ok: true, started: false, already: true, booting: true, asleep: false, proxyPort }) } await taskkillImage('Comfy Desktop.exe') stoppedByAgent = false @@ -669,9 +686,9 @@ const server = http.createServer(async (req, res) => { const launched = await startComfy() if (launched.port) { lastProcessUp = true - ensureProxy(launched.port) + setProxyTarget(launched.port) } - return json(res, 200, { ok: true, asleep: false, ...launched }) + return json(res, 200, { ok: true, asleep: false, proxyPort, ...launched }) } if (req.method === 'POST' && url.pathname === '/stop') { const healthyPort = await syncProxy() @@ -697,6 +714,7 @@ const server = http.createServer(async (req, res) => { }) server.listen(port, '0.0.0.0', async () => { + ensureProxyListening() lastHealthyPort = await syncProxy() || 0 lastProcessUp = Boolean(lastHealthyPort) || await processUp().catch(() => false) if (lastHealthyPort) await noteQueue(lastHealthyPort) diff --git a/server/utils/comfy.ts b/server/utils/comfy.ts index 5da8f72..9d0c0d1 100644 --- a/server/utils/comfy.ts +++ b/server/utils/comfy.ts @@ -7,18 +7,58 @@ export function setComfyHostOverride(url: string) { comfyHostOverride = String(url || '').replace(/\/$/, '') } +function controlUrl() { + const config = useRuntimeConfig() + return String(config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '').trim() +} + +export function agentFrontDoorOrigin() { + const raw = controlUrl() + if (!raw) return '' + const config = useRuntimeConfig() + const proxyPort = String(config.comfyProxyPort || process.env.COMFY_PROXY_PORT || '8198').trim() || '8198' + try { + const control = new URL(raw.includes('://') ? raw : `http://${raw}`) + control.port = proxyPort + control.pathname = '' + control.search = '' + control.hash = '' + return control.origin + } catch { + return '' + } +} + +/** Beast HTTP goes through the host-agent proxy so hopped 8188/8189/8190 never reach Coolify. */ +export function viaAgentFrontDoor(host: string) { + const door = agentFrontDoorOrigin() + if (!door || !host) return host + try { + const url = new URL(host) + const front = new URL(door) + if (url.hostname !== front.hostname) return host + return front.origin + } catch { + return host + } +} + export function comfyConfigured() { if (comfyHostOverride) return true const config = useRuntimeConfig() - return Boolean(String(config.comfyHost || process.env.COMFY_HOST || '').trim()) + return Boolean( + String(config.comfyHost || process.env.COMFY_HOST || '').trim() + || controlUrl() + ) } -function comfyBase() { - if (comfyHostOverride) return comfyHostOverride +function configuredComfyBase() { const config = useRuntimeConfig() let host = String(process.env.COMFY_HOST || config.comfyHost || '').trim().replace(/\/$/, '') const port = String(process.env.COMFY_PORT || config.comfyPort || '').trim() + const door = agentFrontDoorOrigin() if (!host) { + if (door) return door throw createError({ statusCode: 500, statusMessage: 'COMFY_HOST is not configured' }) } if (!/^https?:\/\//i.test(host)) host = `http://${host}` @@ -32,21 +72,22 @@ function comfyBase() { } export function getComfyHost() { - return comfyBase() + if (comfyHostOverride) return viaAgentFrontDoor(comfyHostOverride) || comfyHostOverride + return viaAgentFrontDoor(configuredComfyBase()) || configuredComfyBase() } export function comfyWsUrl(clientId: string) { - return `${comfyBase().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}` + return `${getComfyHost().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}` } export async function comfyFetch(path: string, init?: RequestInit) { - const url = `${comfyBase()}${path}` + const url = `${getComfyHost()}${path}` try { return await fetch(url, init) } catch (error) { throw createError({ statusCode: 502, - statusMessage: `ComfyUI host unreachable (${comfyBase()})`, + statusMessage: `ComfyUI host unreachable (${getComfyHost()})`, data: { cause: error instanceof Error ? error.message : String(error) } }) } @@ -306,12 +347,12 @@ export function inspectHistory(history: Record | null, promptId export async function probeComfy() { try { - const res = await fetch(`${comfyBase()}/system_stats`, { signal: AbortSignal.timeout(2500) }) - if (!res.ok) return { ok: false, host: comfyBase() } + const res = await fetch(`${getComfyHost()}/system_stats`, { signal: AbortSignal.timeout(2500) }) + if (!res.ok) return { ok: false, host: getComfyHost() } const stats = await res.json() - return { ok: true, host: comfyBase(), stats } + return { ok: true, host: getComfyHost(), stats } } catch { - return { ok: false, host: comfyBase() } + return { ok: false, host: getComfyHost() } } } diff --git a/server/utils/imageComfy.ts b/server/utils/imageComfy.ts index abb3b39..361967a 100644 --- a/server/utils/imageComfy.ts +++ b/server/utils/imageComfy.ts @@ -1,5 +1,5 @@ import { AsyncLocalStorage } from 'node:async_hooks' -import { getComfyHost } from '~/server/utils/comfy' +import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy' import { comfyJobPrefix } from '~/utils/outputNames' const imageHostAls = new AsyncLocalStorage() @@ -31,7 +31,7 @@ function normalizeHost(raw: string, port = '') { export function getBeastImageHost() { const config = useRuntimeConfig() const explicit = String(config.imageComfyPrimaryHost || process.env.IMAGE_COMFY_PRIMARY_HOST || '').trim() - if (explicit) return normalizeHost(explicit) + if (explicit) return viaAgentFrontDoor(normalizeHost(explicit)) try { return getComfyHost() } catch { @@ -53,7 +53,7 @@ export function getSidecarImageHost() { ] for (const [raw, extraPort] of candidates) { if (!raw || isRetiredSidecarHost(raw)) continue - const host = normalizeHost(raw, extraPort) + const host = viaAgentFrontDoor(normalizeHost(raw, extraPort)) if (host && !isRetiredSidecarHost(host)) return host } return getBeastImageHost() @@ -61,8 +61,8 @@ export function getSidecarImageHost() { export function getImageComfyHost() { const fromAls = imageHostAls.getStore() - if (fromAls) return fromAls - if (imageComfyHostOverride) return imageComfyHostOverride + if (fromAls) return viaAgentFrontDoor(fromAls) || fromAls + if (imageComfyHostOverride) return viaAgentFrontDoor(imageComfyHostOverride) || imageComfyHostOverride return getBeastImageHost() || getSidecarImageHost() }