113 lines
5.3 KiB
JavaScript
113 lines
5.3 KiB
JavaScript
// Workflow ID — read ComfyUI's native workflow UUID.
|
|
// UUID dedup on Save-As is handled server-side by _dedup_workflow_uuid.
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
|
|
export function getWorkflowId() {
|
|
const graph = app.rootGraph ?? app.graph;
|
|
return graph?.id || null;
|
|
}
|
|
|
|
// ── workflow id drift healing ────────────────────────────────────
|
|
// litegraph regenerates the graph uuid whenever a configure() receives state
|
|
// without a top-level `id` (clear() zeroes it, _configureBase mints a fresh
|
|
// one) — change-tracker restores and undo hit such paths. The generation
|
|
// panel keys history by that uuid, so each silent regeneration "empties" the
|
|
// panel and the next file save persists the drifted id.
|
|
//
|
|
// Detection is anchored to the id INSIDE the workflow's own saved content,
|
|
// AND gated on the configure belonging to the active workflow. The gate is
|
|
// load-bearing: ComfyUI's afterConfigureGraph hook fires inside loadGraphData
|
|
// BEFORE afterLoadNewGraph switches activeWorkflow, so during ANY tab
|
|
// open/switch the live graph already wears the INCOMING workflow's id while
|
|
// activeWorkflow still points at the OUTGOING one. Comparing that pair is
|
|
// indistinguishable from drift by content alone — it's how Save-As cloned
|
|
// the source file's history into the new file (sf-test-7 incident) despite
|
|
// the file-id anchoring. Undo/redo/reload pass their own workflow to
|
|
// loadGraphData (changeTracker.ts), so same-target loads are the only
|
|
// legitimate drift candidates; loads targeting another workflow, a string
|
|
// tab name (graft tabs), or nothing (PNG drags, templates) never heal.
|
|
const wfMeta = new WeakMap(); // ComfyWorkflow → { content, fileId, lastGraphId }
|
|
|
|
let currentLoad = null; // { targetPath } while app.loadGraphData is on the stack
|
|
|
|
export function installLoadTargetClassifier() {
|
|
if (app.loadGraphData?.__pcrClassified) return;
|
|
const orig = app.loadGraphData.bind(app);
|
|
app.loadGraphData = async function (...args) {
|
|
const wfArg = args[3];
|
|
const prev = currentLoad;
|
|
currentLoad = { targetPath: (wfArg && typeof wfArg === "object" && wfArg.path) || null };
|
|
try {
|
|
return await orig(...args);
|
|
} finally {
|
|
currentLoad = prev;
|
|
}
|
|
};
|
|
app.loadGraphData.__pcrClassified = true;
|
|
}
|
|
|
|
export function healWorkflowIdDrift() {
|
|
try {
|
|
const graphId = getWorkflowId();
|
|
const wf = app.extensionManager?.workflow?.activeWorkflow;
|
|
if (!graphId || !wf) return;
|
|
// Heal only when this configure reloaded the ACTIVE workflow itself.
|
|
// Unknown provenance (classifier missed the load) skips too — a missed
|
|
// heal costs one stale panel until the next reconfigure; a wrong clone
|
|
// pollutes the DB and breaks the Save-As reset.
|
|
if (!currentLoad || currentLoad.targetPath !== wf.path) return;
|
|
let meta = wfMeta.get(wf);
|
|
if (!meta || meta.content !== wf.content) {
|
|
let fileId = null;
|
|
try { fileId = JSON.parse(wf.content || wf.originalContent || "null")?.id || null; } catch { /* unparseable content — treat as no persisted identity */ }
|
|
meta = { content: wf.content, fileId, lastGraphId: meta?.lastGraphId || null };
|
|
wfMeta.set(wf, meta);
|
|
}
|
|
if (!meta.fileId) return;
|
|
if (graphId === meta.fileId) { meta.lastGraphId = graphId; return; } // live matches the file — coherent
|
|
// Live graph re-minted away from the file's identity. Heal from the file
|
|
// id AND the previous drift segment (gens made between two drifts would
|
|
// otherwise fall through), both idempotent server-side.
|
|
const froms = [...new Set([meta.fileId, meta.lastGraphId])].filter((x) => x && x !== graphId);
|
|
console.warn(`[PromptChain] workflow id drift (${wf.path}): file ${meta.fileId} vs live ${graphId} — re-attaching generation history`);
|
|
for (const from of froms) {
|
|
api.fetchApi("/promptchain/workflow-clone", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ from_id: from, to_id: graphId, filepath: wf.path || "" }),
|
|
}).catch((e) => console.warn("[PromptChain] id-drift history clone failed", e));
|
|
}
|
|
meta.lastGraphId = graphId;
|
|
} catch (e) {
|
|
console.warn("[PromptChain] id drift check failed", e);
|
|
}
|
|
}
|
|
|
|
// Intercept workflow saves to detect new files (for sidebar refresh).
|
|
// NOTE — deliberate non-feature: Save-As to a NEW file mints a fresh uuid and
|
|
// the generation history intentionally RESETS with it (user spec: a new file
|
|
// starts a clean timeline). Only the SILENT id changes are healed, by
|
|
// healWorkflowIdDrift below — same tab path, regenerated uuid.
|
|
export function installSaveInterceptor() {
|
|
installLoadTargetClassifier();
|
|
const origFetch = api.fetchApi.bind(api);
|
|
|
|
api.fetchApi = async function(route, options) {
|
|
const result = await origFetch(route, options);
|
|
|
|
if (options?.method === "POST" && typeof route === "string") {
|
|
const decoded = decodeURIComponent(route);
|
|
if (decoded.includes("/userdata/workflows/") && decoded.split("?")[0].endsWith(".json")) {
|
|
const relPath = decoded.split("?")[0].split("/userdata/workflows/").pop();
|
|
window.dispatchEvent(new CustomEvent("promptchain:workflows-changed", {
|
|
detail: { path: relPath },
|
|
}));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|