Move library collections as a group and map v2 identity refs.
Collapsed collection actions now move, rename, delete, and rerun every part so chains stay together. Identity mode follows the v2u3 Ref2VA picture wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+166
-14
@@ -1,17 +1,22 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
import { getComfyHost } from '~/server/utils/comfy'
|
||||
|
||||
const imageHostAls = new AsyncLocalStorage<string>()
|
||||
let imageComfyHostOverride = ''
|
||||
|
||||
export type ImageEditBox = 'beast' | 'sidecar'
|
||||
|
||||
export function withImageComfyHost<T>(host: string, fn: () => Promise<T>) {
|
||||
return imageHostAls.run(String(host || '').replace(/\/$/, ''), fn)
|
||||
}
|
||||
|
||||
export function setImageComfyHostOverride(url: string) {
|
||||
imageComfyHostOverride = String(url || '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function getImageComfyHost() {
|
||||
if (imageComfyHostOverride) return imageComfyHostOverride
|
||||
const config = useRuntimeConfig()
|
||||
let host = String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim().replace(/\/$/, '')
|
||||
const port = String(config.imageComfyPort || process.env.IMAGE_COMFY_PORT || '').trim()
|
||||
if (!host) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'IMAGE_COMFY_HOST is not configured' })
|
||||
}
|
||||
function normalizeHost(raw: string, port = '') {
|
||||
let host = String(raw || '').trim().replace(/\/$/, '')
|
||||
if (!host) return ''
|
||||
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
|
||||
try {
|
||||
const url = new URL(host)
|
||||
@@ -22,13 +27,46 @@ export function getImageComfyHost() {
|
||||
}
|
||||
}
|
||||
|
||||
export function imageComfyConfigured() {
|
||||
export function getBeastImageHost() {
|
||||
const config = useRuntimeConfig()
|
||||
return Boolean(String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim())
|
||||
const explicit = String(config.imageComfyPrimaryHost || process.env.IMAGE_COMFY_PRIMARY_HOST || '').trim()
|
||||
if (explicit) return normalizeHost(explicit)
|
||||
try {
|
||||
return getComfyHost()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function getSidecarImageHost() {
|
||||
const config = useRuntimeConfig()
|
||||
const fallback = String(config.imageComfyFallbackHost || process.env.IMAGE_COMFY_FALLBACK_HOST || '').trim()
|
||||
if (fallback) return normalizeHost(fallback)
|
||||
const legacy = String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim()
|
||||
const port = String(config.imageComfyPort || process.env.IMAGE_COMFY_PORT || '').trim()
|
||||
return normalizeHost(legacy, port)
|
||||
}
|
||||
|
||||
export function getImageComfyHost() {
|
||||
const fromAls = imageHostAls.getStore()
|
||||
if (fromAls) return fromAls
|
||||
if (imageComfyHostOverride) return imageComfyHostOverride
|
||||
return getBeastImageHost() || getSidecarImageHost()
|
||||
}
|
||||
|
||||
export function imageComfyConfigured() {
|
||||
return Boolean(getBeastImageHost() || getSidecarImageHost())
|
||||
}
|
||||
|
||||
export function sameImageHost(a: string, b: string) {
|
||||
return Boolean(a && b && a.replace(/\/$/, '') === b.replace(/\/$/, ''))
|
||||
}
|
||||
|
||||
export async function imageComfyFetch(path: string, init?: RequestInit) {
|
||||
const host = getImageComfyHost()
|
||||
if (!host) {
|
||||
throw createError({ statusCode: 500, statusMessage: 'No image ComfyUI host is configured' })
|
||||
}
|
||||
try {
|
||||
return await fetch(`${host}${path}`, init)
|
||||
} catch (error) {
|
||||
@@ -88,10 +126,14 @@ export async function queueImagePrompt(graph: unknown, clientId: string) {
|
||||
return payload as { prompt_id: string; number?: number }
|
||||
}
|
||||
|
||||
export async function interruptImageComfy() {
|
||||
if (!imageComfyConfigured()) return false
|
||||
const res = await imageComfyFetch('/interrupt', { method: 'POST' })
|
||||
return res.ok
|
||||
export async function interruptImageComfy(host?: string) {
|
||||
if (!imageComfyConfigured() && !host) return false
|
||||
const run = async () => {
|
||||
const res = await imageComfyFetch('/interrupt', { method: 'POST' })
|
||||
return res.ok
|
||||
}
|
||||
if (host) return withImageComfyHost(host, run)
|
||||
return run()
|
||||
}
|
||||
|
||||
export async function fetchImageHistory(promptId: string) {
|
||||
@@ -253,3 +295,113 @@ export async function waitForImageEdit(opts: {
|
||||
})
|
||||
}
|
||||
|
||||
function purgeImageEnabled() {
|
||||
return useRuntimeConfig().purgeComfyOutputs !== false
|
||||
}
|
||||
|
||||
async function deleteSidecarFile(file: { filename: string; subfolder?: string; type?: string }) {
|
||||
const payload = {
|
||||
filename: file.filename,
|
||||
subfolder: file.subfolder || '',
|
||||
type: file.type || 'output'
|
||||
}
|
||||
for (const path of ['/delete', '/aigen/purge']) {
|
||||
try {
|
||||
const res = await imageComfyFetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(4000)
|
||||
})
|
||||
if (res.ok) return true
|
||||
} catch {
|
||||
/* Comfy may not expose this route */
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function purgeImageOnDesktop(opts: {
|
||||
inputName?: string
|
||||
inputSubfolder?: string
|
||||
output?: { filename: string; subfolder: string; type: string }
|
||||
}) {
|
||||
const config = useRuntimeConfig()
|
||||
const host = getImageComfyHost()
|
||||
const sidecar = getSidecarImageHost()
|
||||
const useSidecarAgent = sidecar && sameImageHost(host, sidecar)
|
||||
const controlUrl = String(
|
||||
useSidecarAgent
|
||||
? (config.imageComfyControlUrl || process.env.IMAGE_COMFY_CONTROL_URL || '')
|
||||
: (config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '')
|
||||
).replace(/\/$/, '')
|
||||
const token = String(
|
||||
useSidecarAgent
|
||||
? (config.imageComfyControlToken || process.env.IMAGE_COMFY_CONTROL_TOKEN || '')
|
||||
: (config.comfyControlToken || process.env.COMFY_CONTROL_TOKEN || '')
|
||||
)
|
||||
if (!controlUrl) return
|
||||
try {
|
||||
await fetch(`${controlUrl}/purge`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
imageName: opts.inputName || '',
|
||||
imageSubfolder: opts.inputSubfolder || '',
|
||||
output: opts.output || null
|
||||
}),
|
||||
signal: AbortSignal.timeout(4000)
|
||||
})
|
||||
} catch {
|
||||
/* Sidecar host agent is optional; Comfy HTTP delete is the primary path. */
|
||||
}
|
||||
}
|
||||
|
||||
export async function purgeImageComfyArtifacts(opts: {
|
||||
output?: { filename: string; subfolder: string; type: string }
|
||||
inputName?: string
|
||||
inputSubfolder?: string
|
||||
promptId?: string
|
||||
}) {
|
||||
if (!purgeImageEnabled()) return
|
||||
const files: { filename: string; subfolder: string; type: string }[] = []
|
||||
if (opts.output?.filename) {
|
||||
files.push({
|
||||
filename: opts.output.filename,
|
||||
subfolder: opts.output.subfolder || '',
|
||||
type: opts.output.type || 'output'
|
||||
})
|
||||
}
|
||||
if (opts.inputName) {
|
||||
files.push({
|
||||
filename: opts.inputName,
|
||||
subfolder: opts.inputSubfolder || '',
|
||||
type: 'input'
|
||||
})
|
||||
}
|
||||
for (const file of files) {
|
||||
await deleteSidecarFile(file)
|
||||
}
|
||||
await purgeImageOnDesktop({
|
||||
inputName: opts.inputName,
|
||||
inputSubfolder: opts.inputSubfolder,
|
||||
output: opts.output
|
||||
})
|
||||
if (opts.promptId) {
|
||||
try {
|
||||
await imageComfyFetch('/history', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ delete: [opts.promptId] }),
|
||||
signal: AbortSignal.timeout(4000)
|
||||
})
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user