Ship duration control, hidden previews, job resume, and desktop file purge.
The slider never left this machine, so deploy the generate UI with separate input/output hide toggles, reconnect in-progress jobs after refresh, and delete Comfy input/output files after they are saved to the library.
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import http from 'node:http'
|
||||
import net from 'node:net'
|
||||
import { execFile, spawn } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { readdirSync, existsSync, rmSync } from 'node:fs'
|
||||
import { basename, join } from 'node:path'
|
||||
|
||||
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 comfyHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
||||
const defaultHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
||||
|
||||
function json(res, status, body) {
|
||||
const payload = JSON.stringify(body)
|
||||
@@ -19,15 +23,68 @@ function authorized(req) {
|
||||
return header === `Bearer ${token}`
|
||||
}
|
||||
|
||||
async function httpUp() {
|
||||
function candidatePorts() {
|
||||
const ports = new Set([8188, 8189])
|
||||
try {
|
||||
const res = await fetch(`${comfyHttp}/system_stats`, { signal: AbortSignal.timeout(2500) })
|
||||
const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`)
|
||||
if (url.port) ports.add(Number(url.port))
|
||||
} catch { /* ignore */ }
|
||||
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]))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return [...ports]
|
||||
}
|
||||
|
||||
async function probe(portNum) {
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${portNum}/system_stats`, { signal: AbortSignal.timeout(2500) })
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function findHealthyPort() {
|
||||
for (const next of candidatePorts()) {
|
||||
if (await probe(next)) return next
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
let proxyServer = null
|
||||
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
|
||||
}
|
||||
proxyServer = net.createServer((client) => {
|
||||
const upstream = net.connect(proxyTarget, '127.0.0.1')
|
||||
const fail = () => {
|
||||
try { client.destroy() } catch { /* ignore */ }
|
||||
try { upstream.destroy() } catch { /* ignore */ }
|
||||
}
|
||||
client.on('error', fail)
|
||||
upstream.on('error', fail)
|
||||
client.pipe(upstream)
|
||||
upstream.pipe(client)
|
||||
})
|
||||
proxyServer.on('error', (error) => {
|
||||
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 }))
|
||||
})
|
||||
}
|
||||
|
||||
async function processUp() {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('tasklist', ['/FO', 'CSV', '/NH'], { windowsHide: true, timeout: 4000 })
|
||||
@@ -58,21 +115,100 @@ function startComfy() {
|
||||
return { started: true, command }
|
||||
}
|
||||
|
||||
function sharedRoots() {
|
||||
const local = process.env.LOCALAPPDATA || ''
|
||||
return {
|
||||
input: process.env.COMFY_INPUT_DIR || join(local, 'Comfy-Desktop', 'ComfyUI-Shared', 'input'),
|
||||
output: process.env.COMFY_OUTPUT_DIR || join(local, 'Comfy-Desktop', 'ComfyUI-Shared', 'output')
|
||||
}
|
||||
}
|
||||
|
||||
function safeFile(root, subfolder, filename) {
|
||||
const name = basename(String(filename || ''))
|
||||
if (!name || name === '.' || name === '..') return null
|
||||
const target = join(root, String(subfolder || '').replace(/\.\./g, ''), name)
|
||||
if (!target.startsWith(root)) return null
|
||||
return target
|
||||
}
|
||||
|
||||
function removeFile(path) {
|
||||
if (!path || !existsSync(path)) return false
|
||||
rmSync(path, { force: true })
|
||||
return true
|
||||
}
|
||||
|
||||
function readJson(req) {
|
||||
return new Promise((resolve) => {
|
||||
const chunks = []
|
||||
req.on('data', (chunk) => chunks.push(chunk))
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
})
|
||||
req.on('error', () => resolve({}))
|
||||
})
|
||||
}
|
||||
|
||||
function purgeDesktopFiles(body) {
|
||||
const roots = sharedRoots()
|
||||
const deleted = []
|
||||
const imageName = String(body?.imageName || body?.filename || '')
|
||||
if (imageName) {
|
||||
const inputPath = safeFile(roots.input, '', imageName)
|
||||
if (removeFile(inputPath)) deleted.push(inputPath)
|
||||
}
|
||||
const video = body?.video || null
|
||||
const videoName = String(video?.filename || '')
|
||||
if (videoName) {
|
||||
const outputPath = safeFile(roots.output, video.subfolder || 'video', videoName)
|
||||
if (removeFile(outputPath)) deleted.push(outputPath)
|
||||
}
|
||||
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'purge', deleted: deleted.length, files: deleted.map((path) => basename(path)) }))
|
||||
return { ok: true, deleted }
|
||||
}
|
||||
|
||||
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 [httpReady, process] = await Promise.all([httpUp(), processUp()])
|
||||
return json(res, 200, { ok: true, http: httpReady, process })
|
||||
const [healthyPort, process] = await Promise.all([findHealthyPort(), processUp()])
|
||||
if (healthyPort) ensureProxy(healthyPort)
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
http: Boolean(healthyPort),
|
||||
process,
|
||||
port: healthyPort || null,
|
||||
proxyPort: healthyPort ? proxyPort : null
|
||||
})
|
||||
}
|
||||
if (req.method === 'POST' && url.pathname === '/start') {
|
||||
if (await httpUp()) return json(res, 200, { ok: true, started: false, already: true })
|
||||
const healthyPort = await findHealthyPort()
|
||||
if (healthyPort) {
|
||||
ensureProxy(healthyPort)
|
||||
return json(res, 200, { ok: true, started: false, already: true, port: healthyPort, proxyPort })
|
||||
}
|
||||
const launched = startComfy()
|
||||
return json(res, 200, { ok: true, ...launched })
|
||||
}
|
||||
if (req.method === 'POST' && url.pathname === '/purge') {
|
||||
const body = await readJson(req)
|
||||
return json(res, 200, purgeDesktopFiles(body))
|
||||
}
|
||||
json(res, 404, { ok: false, error: 'not found' })
|
||||
})
|
||||
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'listen', port, comfyHttp }))
|
||||
server.listen(port, '0.0.0.0', async () => {
|
||||
const healthyPort = await findHealthyPort()
|
||||
if (healthyPort) ensureProxy(healthyPort)
|
||||
console.log(JSON.stringify({
|
||||
src: 'comfy-host-agent',
|
||||
event: 'listen',
|
||||
port,
|
||||
proxyPort,
|
||||
comfyPort: healthyPort || null,
|
||||
candidates: candidatePorts()
|
||||
}))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user