修复重启后重复新增工作流标签页

This commit is contained in:
magicwang1111
2026-10-28 13:35:53 +08:00
parent c552533755
commit b5b1230923
3 changed files with 95 additions and 14 deletions
+4 -2
View File
@@ -168,8 +168,10 @@ Existing browser pages must be manually refreshed once after installing this fea
Before refreshing, open workflows are backed up in that tab's `sessionStorage`.
Backup serializes the graph directly, without running API prompt serializers;
inactive tabs that have not loaded yet are loaded before backup.
After reload, drafts already restored by ComfyUI are reused; missing drafts are
opened as local recovery copies without overwriting saved files. Backups are bound
After reload, original workflow paths identify tabs already restored by ComfyUI;
view changes do not create duplicate tabs. Missing drafts and pending edits are
restored under their original paths and tab order without overwriting saved files.
Backups from older versions are matched by graph identity and content. Backups are bound
to the authenticated account. If backup or recovery fails, automatic refresh pauses
and displays a warning so the user can save manually. This protects graph edits;
it does not preserve transient dialogs or file uploads in progress.
+59 -2
View File
@@ -113,11 +113,14 @@ async function draftImages(page) {
const h = await harness(browser);
await openDraft(h.page, 'one', 'unsaved-one.png');
await openDraft(h.page, 'two', 'unsaved-two.png');
const originalPaths = await h.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.openWorkflows.map(item => item.path));
const before = h.navigations();
h.state.served = h.state.current = '200';
await Promise.all([h.page.waitForEvent('load'), reconnect(h.page)]);
await h.page.waitForFunction(key => sessionStorage.getItem(key) === null && window.comfyAPI?.app?.app?.isGraphReady && document.querySelector('body'), backupKey);
const images = await draftImages(h.page);
assert.deepEqual(await h.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.openWorkflows.map(item => item.path)), originalPaths,
'Reload added duplicate workflow tabs');
assert(images.includes('unsaved-one.png'), 'Inactive unsaved draft was lost');
assert(images.includes('unsaved-two.png'), 'Active unsaved draft was lost');
assert.equal(await h.page.evaluate(() => window.comfyAPI.app.app.rootGraph.serialize().nodes.find(n => n.type === 'LoadImage').widgets_values[0]), 'unsaved-two.png');
@@ -128,13 +131,67 @@ async function draftImages(page) {
await h.page.waitForTimeout(250);
}
assert.equal(h.navigations(), before + 1, 'Same/older workers caused a reload loop');
for (const revision of ['210', '220']) {
h.state.served = h.state.current = revision;
await Promise.all([h.page.waitForEvent('load'), reconnect(h.page)]);
await h.page.waitForFunction(key => sessionStorage.getItem(key) === null && window.comfyAPI?.app?.app?.isGraphReady, backupKey);
assert.deepEqual(await h.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.openWorkflows.map(item => item.path)), originalPaths,
'Repeated restarts added workflow tabs');
assert.deepEqual((await draftImages(h.page)).sort(), images.slice().sort());
}
h.state.served = h.state.current = '300';
await h.page.reload();
await h.page.waitForTimeout(700);
assert.equal(h.navigations(), before + 2, 'An already updated page refreshed again');
assert.equal(h.navigations(), before + 4, 'An already updated page refreshed again');
assert.deepEqual(h.errors, []);
await h.context.close();
console.log('PASS: unsaved tabs, reconnect, stale worker, once per revision, manual refresh');
console.log('PASS: repeated restarts preserve tab paths, order and drafts without duplicates');
const saved = await harness(browser);
await openDraft(saved.page, 'saved', 'on-disk.png');
const savedPath = await saved.page.evaluate(async () => {
const { app } = await import('/scripts/app.js');
const store = app.extensionManager.workflow;
store.activeWorkflow.changeTracker.prepareForSave();
await store.saveWorkflow(store.activeWorkflow);
// Simulate an edit immediately before reload, before draft persistence runs.
const nodeId = app.rootGraph.serialize().nodes.find(n => n.type === 'LoadImage').id;
app.rootGraph.getNodeById(nodeId).widgets.find(w => w.name === 'image').value = 'latest-edit.png';
return store.activeWorkflow.path;
});
saved.state.served = saved.state.current = '200';
await Promise.all([saved.page.waitForEvent('load'), reconnect(saved.page)]);
await saved.page.waitForFunction(key => sessionStorage.getItem(key) === null && window.comfyAPI?.app?.app?.isGraphReady, backupKey);
assert.equal(await saved.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.activeWorkflow.path), savedPath);
assert((await draftImages(saved.page)).includes('latest-edit.png'));
assert.equal(await saved.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.activeWorkflow.isModified), true);
const diskContent = await saved.page.evaluate(async path => {
const { api } = await import('/scripts/api.js');
return (await api.getUserData(path)).json();
}, savedPath);
assert.equal(diskContent.nodes.find(n => n.type === 'LoadImage').widgets_values[0], 'on-disk.png');
// Backups created by the previous plugin version have no path field.
const savedPaths = await saved.page.evaluate(key => {
const app = window.comfyAPI.app.app;
const data = app.rootGraph.serialize();
data.extra.ds = { scale: 0.5, offset: [123, 456] };
sessionStorage.setItem(key, JSON.stringify({ user_id: 'user-a', active: 0, workflows: [{ name: 'saved', data }] }));
return app.extensionManager.workflow.openWorkflows.map(item => item.path).sort();
}, backupKey);
await reconnect(saved.page);
await saved.page.waitForFunction(key => sessionStorage.getItem(key) === null, backupKey);
assert.deepEqual(await saved.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.openWorkflows.map(item => item.path).sort()), savedPaths);
await saved.page.evaluate(async () => {
const store = window.comfyAPI.app.app.extensionManager.workflow;
for (const tab of store.openWorkflows.slice()) if (tab !== store.activeWorkflow) await store.closeWorkflow(tab);
});
saved.state.served = saved.state.current = '300';
await Promise.all([saved.page.waitForEvent('load'), reconnect(saved.page)]);
await saved.page.waitForFunction(key => sessionStorage.getItem(key) === null && window.comfyAPI?.app?.app?.isGraphReady, backupKey);
assert.deepEqual(await saved.page.evaluate(() => window.comfyAPI.app.app.extensionManager.workflow.openWorkflows.map(item => item.path)), [savedPath]);
assert.deepEqual(saved.errors, []);
await saved.context.close();
console.log('PASS: saved workflows retain unsaved edits, legacy backups reuse tabs, closed tabs stay closed');
const serializers = await harness(browser);
await openDraft(serializers.page, 'api-draft', 'api-draft.png');
+32 -10
View File
@@ -1,5 +1,6 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { ChangeTracker } from "/scripts/changeTracker.js";
const BACKUP_KEY = "AccountManager.RefreshBackup";
const SEEN_KEY = "AccountManager.RefreshRevision";
@@ -35,6 +36,7 @@ async function snapshotWorkflows(info) {
const workflow = app.rootGraph.serialize();
const open = store.openWorkflows;
const workflows = open.map(item => ({
path: item.path,
name: item.filename,
data: item === active ? workflow : item.activeState,
}));
@@ -56,31 +58,51 @@ async function restoreWorkflows(info) {
return;
}
const store = app.extensionManager.workflow;
const activeGraph = app.rootGraph.serialize();
const active = store.activeWorkflow;
for (const item of store.openWorkflows) {
if (item !== store.activeWorkflow && !item.activeState) await item.load();
}
const restored = [];
for (const saved of backup.workflows) {
const serialized = JSON.stringify(saved.data);
const existing = store.openWorkflows.find(item =>
!restored.includes(item) && JSON.stringify(item === active ? activeGraph : item.activeState) === serialized);
if (existing) {
const available = store.openWorkflows.filter(item => !restored.includes(item));
const graphData = item => item === store.activeWorkflow ? app.rootGraph.serialize() : item.activeState;
const existing = saved.path
? store.getWorkflowByPath(saved.path)
: available.find(item => saved.data.id && graphData(item)?.id === saved.data.id)
|| available.find(item => ChangeTracker.graphEqual(graphData(item), saved.data));
if (existing && !existing.activeState) await existing.load();
if (existing && available.includes(existing) && ChangeTracker.graphEqual(graphData(existing), saved.data)) {
restored.push(existing);
continue;
}
// Recover a local copy; never overwrite a workflow saved on the server.
// Keep the original path and tab instead of creating a renamed duplicate.
const previous = store.activeWorkflow;
const result = await app.loadGraphData(saved.data, true, true, `${saved.name} (刷新恢复)`);
if (result === false || !store.activeWorkflow || store.activeWorkflow === previous) {
const initialState = existing?.changeTracker.initialState;
const target = existing || (saved.path?.startsWith("workflows/")
? store.createTemporary(saved.path.slice("workflows/".length), saved.data)
: `${saved.name} (刷新恢复)`);
const result = await app.loadGraphData(saved.data, true, true, target);
if (result === false || !store.activeWorkflow || (typeof target !== "string" ? store.activeWorkflow.path !== target.path : store.activeWorkflow === previous)) {
throw new Error("The recovered workflow could not be opened");
}
if (existing) {
existing.changeTracker.initialState = initialState;
existing.changeTracker.updateModified();
}
restored.push(store.activeWorkflow);
}
for (let index = 0; index < restored.length; index++) {
const current = store.openWorkflows.indexOf(restored[index]);
if (current !== index) store.reorderWorkflows(current, index);
}
const selected = restored[backup.active];
if (selected && selected !== store.activeWorkflow) {
const initialState = selected.changeTracker.initialState;
const result = await app.loadGraphData(selected.activeState, true, true, selected);
if (result === false || store.activeWorkflow !== selected) {
throw new Error("The active workflow could not be restored");
}
selected.changeTracker.initialState = initialState;
selected.changeTracker.updateModified();
}
sessionStorage.removeItem(BACKUP_KEY);
}
@@ -91,7 +113,7 @@ async function checkForUpdate() {
pending = true;
return;
}
if (!app.isGraphReady || app.extensionManager.workflow.isBusy || app.configuringGraph) {
if (!app.isGraphReady || app.extensionManager.workflow.isBusy || app.extensionManager.workflow.isSyncLoading || app.configuringGraph) {
clearTimeout(retryTimer);
retryTimer = setTimeout(checkForUpdate, 500);
return;