fix: keep recovered generation jobs polling

This commit is contained in:
FengLee
2026-05-30 11:56:46 +08:00
parent d9c1583c1b
commit 7eacfe9220
3 changed files with 23 additions and 8 deletions

View File

@@ -110,7 +110,7 @@ Use this guide when the user reports behavior. Start from the symptom row, inspe
| Symptom | Check Files | What To Verify |
| --- | --- | --- |
| History missing after generation or login/account switch | `src/lib/creation-history-store.ts`, `src/app/api/creation-history/route.ts`, create panel component | History POST, `works` insert, URL not data URL except reverse prompt placeholder, and `miaojing_auth_updated` triggers a fresh server fetch. Create panels now also recover queued/running jobs from `/api/generation-jobs` so a refresh or re-login can reattach the live task before it finishes. |
| History missing after generation or login/account switch | `src/lib/creation-history-store.ts`, `src/app/api/creation-history/route.ts`, create panel component | History POST, `works` insert, URL not data URL except reverse prompt placeholder, and `miaojing_auth_updated` triggers a fresh server fetch. Create panels now also recover queued/running jobs from `/api/generation-jobs` so a refresh or re-login can reattach the live task before it finishes. If the task card reappears after refresh but never turns into a result/error, inspect `src/components/create/use-generation-job-recovery.ts`; active-task state updates must not be part of the polling effect dependency list, or the recovery poller can be cancelled immediately after reattaching a job. |
| Detail delete removes only local history, skips confirmation, or record reappears after refresh | `src/components/creation-detail-dialog.tsx`, `src/components/ui/alert-dialog.tsx`, `src/lib/creation-history-store.ts`, `src/app/api/creation-history/route.ts`, `src/components/profile/creation-history-tab.tsx` | The detail action is labeled `删除作品` and must open a confirmation dialog warning that deletion cannot be recovered. Logged-in deletion should call `DELETE /api/creation-history?id=...` first, then refresh local history from the server. Check bearer token availability and route ownership filter (`id` + `user_id`). |
| Published work not in gallery or share to gallery is slow | `src/lib/creation-history-store.ts`, `src/lib/gallery-publish-media.ts`, `src/app/api/gallery/publish/route.ts`, `src/app/api/gallery/route.ts`, `src/app/gallery/page.tsx` | `is_public = true`, `status = completed`, stable `/api/local-storage/...` `result_url`, media copied/reused into gallery storage, and current filters. New generated `/api/local-storage/...` image/video URLs should use the publish fast path in `gallery-publish-media` and must not synchronously copy object-backed originals during share; external URLs still need copying and should fail the publish request if media preparation fails. Also check whether the browser marked the work shared before `/api/gallery/publish` returned success; local `published=true` without `publishedAt` is stale and should not block retry. For older incidents, inspect server logs/API status for publish failures that the previous frontend swallowed. |
| Imported gallery images do not render after production data import | `src/app/api/admin/data-export/route.ts`, `src/app/api/admin/data-import/route.ts`, `src/lib/local-storage.ts`, `src/app/api/local-storage/[...path]/route.ts`, DB `works.result_url` | New exports should include `_media`; import should persist media through the active storage adapter. If using an older export without `_media`, DB rows alone cannot recreate missing `/api/local-storage/*` files. For object migration, run `pnpm run storage:sync-object -- --verify-only` before switching to `STORAGE_MODE=object`. |

View File

@@ -59,4 +59,14 @@ await runTest('create panels restore active jobs from the server after reload or
}
});
await runTest('recovered job polling is not cancelled by active task state updates', () => {
const source = read('src/components/create/use-generation-job-recovery.ts');
assert.match(source, /knownJobIdsRef/);
const effectMatches = [...source.matchAll(/useEffect\(\(\) => \{[\s\S]*?void recover\(\);[\s\S]*?\}, \[([^\]]*)\]\);/g)];
assert.ok(effectMatches.length > 0, 'expected to find the recovery polling effect');
const dependencies = effectMatches.at(-1)?.[1] || '';
assert.doesNotMatch(dependencies, /\btypes\b/);
assert.doesNotMatch(dependencies, /\bnormalizedKnownJobIds\b/);
});
if (process.exitCode) process.exit(process.exitCode);

View File

@@ -66,12 +66,17 @@ export function useGenerationJobRecovery({
const onTaskRecoveredRef = useRef(onTaskRecovered);
const onTaskFinishedRef = useRef(onTaskFinished);
const onTaskFailedRef = useRef(onTaskFailed);
const typesRef = useRef(types);
const knownJobIdsRef = useRef(new Set<string>());
const normalizedTypes = useMemo(() => types.slice().sort().join(','), [types]);
const normalizedKnownJobIds = useMemo(
() => new Set(knownJobIds.map(id => id.trim()).filter(Boolean)),
[knownJobIds],
);
const normalizedKnownJobIds = useMemo(() => knownJobIds.map(id => id.trim()).filter(Boolean).sort().join(','), [knownJobIds]);
useEffect(() => {
typesRef.current = types;
}, [normalizedTypes, types]);
useEffect(() => {
knownJobIdsRef.current = new Set(normalizedKnownJobIds ? normalizedKnownJobIds.split(',') : []);
}, [normalizedKnownJobIds]);
useEffect(() => {
onTaskRecoveredRef.current = onTaskRecovered;
}, [onTaskRecovered]);
@@ -92,11 +97,11 @@ export function useGenerationJobRecovery({
try {
await new Promise(resolve => window.setTimeout(resolve, 800));
if (cancelled) return;
const jobs = await fetchActiveGenerationJobs(types);
const jobs = await fetchActiveGenerationJobs(typesRef.current);
if (cancelled) return;
for (const job of jobs) {
const task = normalizeJobTask(job);
if (!task || activeJobIdsRef.current.has(task.id) || normalizedKnownJobIds.has(task.id)) continue;
if (!task || activeJobIdsRef.current.has(task.id) || knownJobIdsRef.current.has(task.id)) continue;
activeJobIdsRef.current.add(task.id);
onTaskRecoveredRef.current(task, job);
void (async () => {
@@ -154,5 +159,5 @@ export function useGenerationJobRecovery({
cancelled = true;
window.removeEventListener('miaojing_auth_updated', handleAuthUpdated);
};
}, [isEnabled, normalizedKnownJobIds, normalizedTypes, types]);
}, [isEnabled, normalizedTypes]);
}