修复重启后重复新增工作流标签页
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user