diff --git a/.memory/worklog.json b/.memory/worklog.json index e8c854c..99346cb 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -562,6 +562,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-19 11:37 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-19T11:46:08+08:00", + "type": "commit", + "message": "fix: handle board uploads and background pack generation", + "hash": "8e27d3b", + "files_changed": 1 + }, + { + "ts": "2026-05-19T03:50:00Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 3 项未提交变更 · 最近提交:fix: handle board uploads and background pack generation", + "files_changed": 3 } ] } diff --git a/src/app/api/packs/generate-all/route.ts b/src/app/api/packs/generate-all/route.ts index 279569b..a180a0e 100644 --- a/src/app/api/packs/generate-all/route.ts +++ b/src/app/api/packs/generate-all/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from 'next/server'; +import { startGenerationLock } from '@/lib/generationLocks'; import { generateAssetPack } from '@/lib/packGenerator'; import { detectProvider } from '@/lib/providers'; import { loadSession, saveSession } from '@/lib/storage'; -import { PACK_ORDER } from '@/lib/templates'; +import { getPackTemplates, PACK_ORDER } from '@/lib/templates'; import type { AssetPack, GenerateAllPacksRequest, GenerateAllPacksResponse, GenSession } from '@/lib/types'; export const runtime = 'nodejs'; @@ -17,6 +18,13 @@ async function persistPackProgress(session: GenSession, imageId: string, pack: A await saveSession(session); } +function isCompletePack(pack: AssetPack, imageId: string): boolean { + if (pack.sourceImageId !== imageId || pack.status !== 'complete') return false; + const expectedIds = new Set(getPackTemplates(pack.kind).map(template => template.id)); + const assetIds = new Set(pack.assets.map(asset => asset.templateId)); + return expectedIds.size > 0 && [...expectedIds].every(templateId => assetIds.has(templateId)); +} + export async function POST(req: Request) { const { sessionId, imageId, background = false } = (await req.json()) as GenerateAllPacksRequest; if (!sessionId || !imageId) { @@ -34,41 +42,75 @@ export async function POST(req: Request) { const baseSession = session; const baseSourceImage = sourceImage; + const releaseAllLock = startGenerationLock(`packs:all:${sessionId}:${imageId}`); + if (!releaseAllLock) { + return NextResponse.json({ + ok: true, + background: true, + running: true, + provider: detectProvider(), + }, { status: 202 }); + } + const releaseAll = releaseAllLock; + async function run() { - const packs = []; + const packs: AssetPack[] = []; const manifests = []; let workingSession: GenSession = baseSession; - for (const kind of PACK_ORDER) { - const generated = await generateAssetPack({ - session: workingSession, - sourceImage: baseSourceImage, - kind, - onProgress: progressPack => persistPackProgress(workingSession, imageId, progressPack), - }); - packs.push(generated.pack); - manifests.push(generated.manifest); - workingSession = { - ...workingSession, - characterSpec: generated.pack.characterSpec, - packs: [ - ...(workingSession.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), - generated.pack, - ], - exports: [ - ...(workingSession.exports ?? []).filter(existing => !(existing.packKind === kind && existing.source.sourceImageId === imageId)), - generated.manifest, - ], - }; + try { + for (const kind of PACK_ORDER) { + const existingPack = workingSession.packs?.find(pack => pack.kind === kind && isCompletePack(pack, imageId)); + if (existingPack) { + const existingManifest = workingSession.exports?.find(manifest => ( + manifest.packKind === kind && + manifest.source.sourceImageId === imageId && + manifest.packId === existingPack.id + )); + packs.push(existingPack); + if (existingManifest) manifests.push(existingManifest); + continue; + } + + const releasePackLock = startGenerationLock(`pack:${sessionId}:${imageId}:${kind}`); + if (!releasePackLock) continue; + + try { + const generated = await generateAssetPack({ + session: workingSession, + sourceImage: baseSourceImage, + kind, + onProgress: progressPack => persistPackProgress(workingSession, imageId, progressPack), + }); + packs.push(generated.pack); + manifests.push(generated.manifest); + workingSession = { + ...workingSession, + characterSpec: generated.pack.characterSpec, + packs: [ + ...(workingSession.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), + generated.pack, + ], + exports: [ + ...(workingSession.exports ?? []).filter(existing => !(existing.packKind === kind && existing.source.sourceImageId === imageId)), + generated.manifest, + ], + }; + } finally { + releasePackLock(); + } + } + + await saveSession(workingSession); + + return { + packs, + manifests, + provider: detectProvider(), + } satisfies GenerateAllPacksResponse; + } finally { + releaseAll(); } - - await saveSession(workingSession); - - return { - packs, - manifests, - provider: detectProvider(), - } satisfies GenerateAllPacksResponse; } if (background) { diff --git a/src/app/api/packs/generate/route.ts b/src/app/api/packs/generate/route.ts index 6592e67..d5dd1ac 100644 --- a/src/app/api/packs/generate/route.ts +++ b/src/app/api/packs/generate/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { startGenerationLock } from '@/lib/generationLocks'; import { generateAssetPack } from '@/lib/packGenerator'; import { detectProvider } from '@/lib/providers'; import { loadSession, saveSession } from '@/lib/storage'; @@ -35,24 +36,40 @@ export async function POST(req: Request) { const baseSession = session; const baseSourceImage = sourceImage; - async function run() { - const { pack, manifest, provider } = await generateAssetPack({ - session: baseSession, - sourceImage: baseSourceImage, + const releaseLock = startGenerationLock(`pack:${sessionId}:${imageId}:${kind}`); + if (!releaseLock) { + return NextResponse.json({ + ok: true, + background: true, + running: true, kind, - onProgress: progressPack => persistPackProgress(baseSession, imageId, progressPack), - }); - baseSession.characterSpec = pack.characterSpec; - baseSession.packs = [ - ...(baseSession.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), - pack, - ]; - baseSession.exports = [ - ...(baseSession.exports ?? []).filter(existing => !(existing.packKind === kind && existing.source.sourceImageId === imageId)), - manifest, - ]; - await saveSession(baseSession); - return { pack, manifest, provider } satisfies GeneratePackResponse; + provider: detectProvider(), + }, { status: 202 }); + } + const release = releaseLock; + + async function run() { + try { + const { pack, manifest, provider } = await generateAssetPack({ + session: baseSession, + sourceImage: baseSourceImage, + kind, + onProgress: progressPack => persistPackProgress(baseSession, imageId, progressPack), + }); + baseSession.characterSpec = pack.characterSpec; + baseSession.packs = [ + ...(baseSession.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), + pack, + ]; + baseSession.exports = [ + ...(baseSession.exports ?? []).filter(existing => !(existing.packKind === kind && existing.source.sourceImageId === imageId)), + manifest, + ]; + await saveSession(baseSession); + return { pack, manifest, provider } satisfies GeneratePackResponse; + } finally { + release(); + } } if (background) { diff --git a/src/lib/generationLocks.ts b/src/lib/generationLocks.ts new file mode 100644 index 0000000..a3f7ce0 --- /dev/null +++ b/src/lib/generationLocks.ts @@ -0,0 +1,17 @@ +const globalForGenerationLocks = globalThis as typeof globalThis & { + __toyGenerationLocks?: Set; +}; + +function getLocks(): Set { + globalForGenerationLocks.__toyGenerationLocks ??= new Set(); + return globalForGenerationLocks.__toyGenerationLocks; +} + +export function startGenerationLock(key: string): (() => void) | null { + const locks = getLocks(); + if (locks.has(key)) return null; + locks.add(key); + return () => { + locks.delete(key); + }; +}