119 lines
5.5 KiB
TypeScript
119 lines
5.5 KiB
TypeScript
// Фоновое отслеживание незавершённых задач: результаты из ComfyUI записываются в базу,
|
|
// даже если ни одна страница не открыта.
|
|
import type { FileRef, Job, LiveStatus } from '../shared/api.ts';
|
|
import { comfyJson } from './comfy.ts';
|
|
import {
|
|
batchByJob, deleteJob, finishBatch, finishJob, finishProjectClean, finishProjectModel, pendingJobs, projectByCleanJob,
|
|
projectByModelJob, transaction,
|
|
} from './db.ts';
|
|
|
|
/** Сколько ждать задачу, которой нет ни в очереди, ни в истории ComfyUI (ComfyUI перезапускался). */
|
|
const LOST_AFTER_MS = 30_000;
|
|
|
|
const live = new Map<string, LiveStatus>();
|
|
const missingSince = new Map<string, number>();
|
|
|
|
/** Положение незавершённой задачи в очереди ComfyUI — для показа на страницах. */
|
|
export function liveStatus(id: string): LiveStatus | undefined {
|
|
return live.get(id);
|
|
}
|
|
|
|
interface HistoryEntry {
|
|
status?: { status_str?: string; completed?: boolean; messages?: [string, { exception_message?: string }][] };
|
|
outputs?: Record<string, Record<string, unknown>>;
|
|
}
|
|
|
|
/** Файлы из outputs истории: картинки (SaveImage) и GLB (SaveGLB кладёт их под другим ключом) — по расширению. */
|
|
function outputFiles(h: HistoryEntry) {
|
|
const images: FileRef[] = [];
|
|
const models: FileRef[] = [];
|
|
for (const node of Object.values(h.outputs ?? {})) {
|
|
for (const list of Object.values(node)) {
|
|
if (!Array.isArray(list)) continue;
|
|
for (const i of list as FileRef[]) {
|
|
if (typeof i?.filename !== 'string') continue;
|
|
if (/\.glb$/i.test(i.filename)) models.push(i);
|
|
else if (/\.(png|webp|jpe?g)$/i.test(i.filename)) images.push(i);
|
|
}
|
|
}
|
|
}
|
|
return { images, models };
|
|
}
|
|
|
|
/**
|
|
* Завершение задачи. Картинки text2img прикрепляются к её батчу (Images.batchId);
|
|
* результат очистки и модель проекта заменяют прежние в проекте, а сама задача удаляется — история не хранится.
|
|
*/
|
|
function finish(job: Job, state: 'done' | 'error', result: { error?: string; images?: FileRef[]; models?: FileRef[] }) {
|
|
transaction(() => {
|
|
if (job.type === 'cleanup') {
|
|
const projectId = job.params.projectId ?? projectByCleanJob(job.id);
|
|
if (projectId != null) finishProjectClean(projectId, state === 'done' ? result.images?.[0] ?? null : null, result.error);
|
|
deleteJob(job.id);
|
|
return;
|
|
}
|
|
// модель проекта: GLB и превью — в проект, задача удаляется (история не хранится)
|
|
const modelProject = job.type === 'model' ? job.params.projectId ?? projectByModelJob(job.id) : null;
|
|
if (modelProject != null) {
|
|
const done = state === 'done';
|
|
finishProjectModel(modelProject, done ? result.models?.[0] ?? null : null, done ? result.images?.[0] ?? null : null, result.error);
|
|
deleteJob(job.id);
|
|
return;
|
|
}
|
|
finishJob(job.id, state, result);
|
|
const batchId = job.type === 'text2img' ? job.params.batchId ?? batchByJob(job.id) : null;
|
|
if (batchId != null) finishBatch(batchId, state, result.images ?? [], result.error);
|
|
});
|
|
}
|
|
|
|
async function tick() {
|
|
const jobs = pendingJobs();
|
|
if (!jobs.length) {
|
|
live.clear();
|
|
missingSince.clear();
|
|
return;
|
|
}
|
|
// ComfyUI недоступен — задачи остаются незавершёнными до его возвращения
|
|
const q = await comfyJson<{ queue_running?: unknown[][]; queue_pending?: unknown[][] }>('/queue');
|
|
const running = new Set((q.queue_running ?? []).map(x => String(x[1])));
|
|
const queued = (q.queue_pending ?? []).map(x => String(x[1]));
|
|
|
|
for (const job of jobs) {
|
|
if (running.has(job.id)) {
|
|
live.set(job.id, { state: 'running' });
|
|
missingSince.delete(job.id);
|
|
continue;
|
|
}
|
|
const pos = queued.indexOf(job.id);
|
|
if (pos >= 0) {
|
|
live.set(job.id, { state: 'queued', position: pos + 1 });
|
|
missingSince.delete(job.id);
|
|
continue;
|
|
}
|
|
const h = (await comfyJson<Record<string, HistoryEntry>>('/history/' + encodeURIComponent(job.id)))[job.id];
|
|
if (h?.status?.status_str === 'error') {
|
|
const err = (h.status.messages ?? []).find(m => m[0] === 'execution_error' || m[0] === 'execution_interrupted');
|
|
finish(job, 'error', { error: err ? err[1].exception_message ?? 'прервано' : 'ошибка выполнения' });
|
|
} else if (h) {
|
|
finish(job, 'done', outputFiles(h));
|
|
} else {
|
|
// между постановкой в очередь и появлением в истории задачу может не быть видно — ждём
|
|
const since = missingSince.get(job.id) ?? Date.now();
|
|
missingSince.set(job.id, since);
|
|
if (Date.now() - since < LOST_AFTER_MS) continue;
|
|
finish(job, 'error', { error: 'задача не найдена в ComfyUI (перезапускался?)' });
|
|
}
|
|
live.delete(job.id);
|
|
missingSince.delete(job.id);
|
|
}
|
|
}
|
|
|
|
let busy = false;
|
|
export function startTracker(intervalMs = 1500) {
|
|
setInterval(async () => {
|
|
if (busy) return;
|
|
busy = true;
|
|
try { await tick(); } catch { /* ComfyUI недоступен — попробуем на следующем шаге */ } finally { busy = false; }
|
|
}, intervalMs);
|
|
}
|