Add Image v2 as a sibling of Image edit, with separate Klein graphs.

Keep the v1 Image tab on its dual-branch template. The host agent follows Desktop's live port so Coolify stays on :8198.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 21:22:37 -05:00
co-authored by Cursor
parent 55f8cb1def
commit 21fe2412f9
18 changed files with 1678 additions and 67 deletions
+66 -24
View File
@@ -23,36 +23,73 @@ function authorized(req) {
return header === `Bearer ${token}`
}
function candidatePorts() {
const ports = new Set([8188, 8189])
try {
const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`)
if (url.port) ports.add(Number(url.port))
} catch { /* ignore */ }
function lockPorts() {
const ports = []
try {
const dir = join(process.env.APPDATA || '', 'Comfy Desktop', 'port-locks')
for (const name of readdirSync(dir)) {
const match = name.match(/^port-(\d+)\.json$/)
if (match) ports.add(Number(match[1]))
if (match) ports.push(Number(match[1]))
}
} catch { /* ignore */ }
return [...ports]
return ports
}
async function probe(portNum) {
function candidatePorts() {
const skip = new Set([port, proxyPort])
const ordered = []
const seen = new Set()
const add = (value) => {
const next = Number(value)
if (!Number.isInteger(next) || next < 1 || next > 65535) return
if (skip.has(next) || seen.has(next)) return
seen.add(next)
ordered.push(next)
}
for (const next of lockPorts()) add(next)
try {
const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`)
if (url.port) add(url.port)
} catch { /* ignore */ }
for (let next = 8188; next <= 8210; next++) add(next)
return ordered
}
async function probeStats(portNum) {
try {
const res = await fetch(`http://127.0.0.1:${portNum}/system_stats`, { signal: AbortSignal.timeout(2500) })
return res.ok
if (!res.ok) return null
const stats = await res.json()
if (!stats?.system) return null
return stats
} catch {
return false
return null
}
}
function advertisedPort(stats) {
const argv = stats?.system?.argv
if (!Array.isArray(argv)) return 0
const index = argv.findIndex(item => String(item) === '--port')
if (index >= 0) return Number(argv[index + 1]) || 0
const flag = argv.find(item => /^--port=\d+$/.test(String(item)))
if (flag) return Number(String(flag).split('=')[1]) || 0
return 0
}
async function findHealthyPort() {
const found = []
for (const next of candidatePorts()) {
if (await probe(next)) return next
const stats = await probeStats(next)
if (!stats) continue
found.push({ port: next, advertised: advertisedPort(stats) })
}
return 0
const native = found.find(item => item.advertised && item.advertised === item.port)
if (native) return native.port
for (const item of found) {
if (item.advertised && found.some(other => other.port === item.advertised)) return item.advertised
}
return found[0]?.port || 0
}
let proxyServer = null
@@ -60,12 +97,11 @@ let proxyTarget = 0
function ensureProxy(targetPort) {
if (!targetPort) return
if (proxyServer && proxyTarget === targetPort) return
proxyTarget = targetPort
if (proxyServer) {
try { proxyServer.close() } catch { /* ignore */ }
proxyServer = null
if (proxyTarget !== targetPort) {
proxyTarget = targetPort
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-target', listen: proxyPort, target: proxyTarget }))
}
if (proxyServer) return
proxyServer = net.createServer((client) => {
const upstream = net.connect(proxyTarget, '127.0.0.1')
const fail = () => {
@@ -85,6 +121,12 @@ function ensureProxy(targetPort) {
})
}
async function syncProxy() {
const healthyPort = await findHealthyPort()
if (healthyPort) ensureProxy(healthyPort)
return healthyPort
}
async function processUp() {
try {
const { stdout } = await execFileAsync('tasklist', ['/FO', 'CSV', '/NH'], { windowsHide: true, timeout: 4000 })
@@ -231,8 +273,7 @@ const server = http.createServer(async (req, res) => {
if (!authorized(req)) return json(res, 401, { ok: false, error: 'unauthorized' })
const url = new URL(req.url || '/', 'http://localhost')
if (req.method === 'GET' && url.pathname === '/status') {
const [healthyPort, process] = await Promise.all([findHealthyPort(), processUp()])
if (healthyPort) ensureProxy(healthyPort)
const [healthyPort, process] = await Promise.all([syncProxy(), processUp()])
return json(res, 200, {
ok: true,
http: Boolean(healthyPort),
@@ -242,9 +283,8 @@ const server = http.createServer(async (req, res) => {
})
}
if (req.method === 'POST' && url.pathname === '/start') {
const healthyPort = await findHealthyPort()
const healthyPort = await syncProxy()
if (healthyPort) {
ensureProxy(healthyPort)
return json(res, 200, { ok: true, started: false, already: true, port: healthyPort, proxyPort })
}
const launched = startComfy()
@@ -258,8 +298,7 @@ const server = http.createServer(async (req, res) => {
})
server.listen(port, '0.0.0.0', async () => {
const healthyPort = await findHealthyPort()
if (healthyPort) ensureProxy(healthyPort)
const healthyPort = await syncProxy()
console.log(JSON.stringify({
src: 'comfy-host-agent',
event: 'listen',
@@ -268,4 +307,7 @@ server.listen(port, '0.0.0.0', async () => {
comfyPort: healthyPort || null,
candidates: candidatePorts()
}))
setInterval(() => {
syncProxy().catch(() => {})
}, 3000)
})
+3
View File
@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0.."
node "%~dp0comfy-host-agent.mjs"