diff --git a/.memory/worklog.json b/.memory/worklog.json index 8d56fc9..97e0bba 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -523,6 +523,25 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:fix: update OpenAI image response handling", "files_changed": 1 + }, + { + "ts": "2026-05-19T11:13:07+08:00", + "type": "commit", + "message": "auto-save 2026-05-19 11:13 (~2)", + "hash": "74148d0", + "files_changed": 2 + }, + { + "ts": "2026-05-19T03:20:00Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-19 11:13 (~2)", + "files_changed": 1 + }, + { + "ts": "2026-05-19T03:30:00Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 6 项未提交变更 · 最近提交:auto-save 2026-05-19 11:13 (~2)", + "files_changed": 6 } ] } diff --git a/src/app/api/packs/generate-all/route.ts b/src/app/api/packs/generate-all/route.ts index 3715e09..279569b 100644 --- a/src/app/api/packs/generate-all/route.ts +++ b/src/app/api/packs/generate-all/route.ts @@ -3,13 +3,22 @@ import { generateAssetPack } from '@/lib/packGenerator'; import { detectProvider } from '@/lib/providers'; import { loadSession, saveSession } from '@/lib/storage'; import { PACK_ORDER } from '@/lib/templates'; -import type { GenerateAllPacksRequest, GenerateAllPacksResponse } from '@/lib/types'; +import type { AssetPack, GenerateAllPacksRequest, GenerateAllPacksResponse, GenSession } from '@/lib/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; +async function persistPackProgress(session: GenSession, imageId: string, pack: AssetPack) { + session.characterSpec = pack.characterSpec; + session.packs = [ + ...(session.packs ?? []).filter(existing => !(existing.kind === pack.kind && existing.sourceImageId === imageId)), + { ...pack, assets: [...pack.assets] }, + ]; + await saveSession(session); +} + export async function POST(req: Request) { - const { sessionId, imageId } = (await req.json()) as GenerateAllPacksRequest; + const { sessionId, imageId, background = false } = (await req.json()) as GenerateAllPacksRequest; if (!sessionId || !imageId) { return NextResponse.json({ error: 'sessionId and imageId required' }, { status: 400 }); } @@ -23,13 +32,20 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'image must be selected before generating packs' }, { status: 400 }); } - try { + const baseSession = session; + const baseSourceImage = sourceImage; + async function run() { const packs = []; const manifests = []; - let workingSession = session; + let workingSession: GenSession = baseSession; for (const kind of PACK_ORDER) { - const generated = await generateAssetPack({ session: workingSession, sourceImage, kind }); + const generated = await generateAssetPack({ + session: workingSession, + sourceImage: baseSourceImage, + kind, + onProgress: progressPack => persistPackProgress(workingSession, imageId, progressPack), + }); packs.push(generated.pack); manifests.push(generated.manifest); workingSession = { @@ -48,11 +64,24 @@ export async function POST(req: Request) { await saveSession(workingSession); - return NextResponse.json({ + return { packs, manifests, provider: detectProvider(), - } satisfies GenerateAllPacksResponse); + } satisfies GenerateAllPacksResponse; + } + + if (background) { + void run().catch(error => console.error('[packs:all] background generation failed', error)); + return NextResponse.json({ + ok: true, + background: true, + provider: detectProvider(), + }, { status: 202 }); + } + + try { + return NextResponse.json(await run()); } catch (error) { return NextResponse.json({ error: String(error) }, { status: 500 }); } diff --git a/src/app/api/packs/generate/route.ts b/src/app/api/packs/generate/route.ts index 259b186..ee588d7 100644 --- a/src/app/api/packs/generate/route.ts +++ b/src/app/api/packs/generate/route.ts @@ -1,15 +1,25 @@ import { NextResponse } from 'next/server'; import { generateAssetPack } from '@/lib/packGenerator'; +import { detectProvider } from '@/lib/providers'; import { loadSession, saveSession } from '@/lib/storage'; -import type { GeneratePackRequest, GeneratePackResponse, PackKind } from '@/lib/types'; +import type { AssetPack, GeneratePackRequest, GeneratePackResponse, GenSession, PackKind } from '@/lib/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; const PACK_KINDS: PackKind[] = ['patent', 'accessories', 'production', 'marketing']; +async function persistPackProgress(session: GenSession, imageId: string, pack: AssetPack) { + session.characterSpec = pack.characterSpec; + session.packs = [ + ...(session.packs ?? []).filter(existing => !(existing.kind === pack.kind && existing.sourceImageId === imageId)), + { ...pack, assets: [...pack.assets] }, + ]; + await saveSession(session); +} + export async function POST(req: Request) { - const { sessionId, imageId, kind } = (await req.json()) as GeneratePackRequest; + const { sessionId, imageId, kind, background = false } = (await req.json()) as GeneratePackRequest; if (!sessionId || !imageId || !PACK_KINDS.includes(kind)) { return NextResponse.json({ error: 'sessionId, imageId and valid kind required' }, { status: 400 }); } @@ -23,8 +33,13 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'image must be selected before generating packs' }, { status: 400 }); } - try { - const { pack, manifest, provider } = await generateAssetPack({ session, sourceImage, kind }); + async function run() { + const { pack, manifest, provider } = await generateAssetPack({ + session, + sourceImage, + kind, + onProgress: progressPack => persistPackProgress(session, imageId, progressPack), + }); session.characterSpec = pack.characterSpec; session.packs = [ ...(session.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), @@ -35,8 +50,23 @@ export async function POST(req: Request) { manifest, ]; await saveSession(session); + return { pack, manifest, provider } satisfies GeneratePackResponse; + } - return NextResponse.json({ pack, manifest, provider } satisfies GeneratePackResponse); + if (background) { + void run().catch(error => console.error(`[pack:${kind}] background generation failed`, error)); + return NextResponse.json({ + ok: true, + background: true, + kind, + provider: detectProvider(), + }, { status: 202 }); + } + + try { + const response = await run(); + + return NextResponse.json(response); } catch (error) { return NextResponse.json({ error: String(error) }, { status: 500 }); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 9badae2..5a6b450 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -136,7 +136,7 @@ export default function Home() { const r = await fetch('/api/packs/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId: current.id, imageId: image.id, kind }), + body: JSON.stringify({ sessionId: current.id, imageId: image.id, kind, background: true }), }); if (!r.ok) { alert('素材包生成失败:' + (await r.text())); @@ -147,6 +147,7 @@ export default function Home() { const all = await refreshSessions(); const updated = all.find(x => x.id === current.id) ?? null; setCurrent(updated); + scheduleSessionRefresh(current.id); } finally { setLoadingKind(null); } @@ -159,6 +160,15 @@ export default function Home() { return updated; } + function scheduleSessionRefresh(sessionId: string, remaining = 90) { + if (remaining <= 0) return; + window.setTimeout(async () => { + const updated = await reloadCurrent(sessionId); + const hasDraftPack = updated?.packs?.some(pack => pack.status === 'draft') ?? false; + if (hasDraftPack || remaining > 84) scheduleSessionRefresh(sessionId, remaining - 1); + }, 5000); + } + async function handleLockCharacter(image: GenImage) { if (!current || characterLoading) return; setCharacterLoading(true); @@ -187,7 +197,7 @@ export default function Home() { const r = await fetch('/api/packs/generate-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId: current.id, imageId: image.id }), + body: JSON.stringify({ sessionId: current.id, imageId: image.id, background: true }), }); if (!r.ok) { alert('完整三包生成失败:' + (await r.text())); @@ -196,6 +206,7 @@ export default function Home() { const d: GenerateAllPacksResponse = await r.json(); setProvider(d.provider); await reloadCurrent(current.id); + scheduleSessionRefresh(current.id); } finally { setAllLoading(false); } diff --git a/src/lib/packGenerator.ts b/src/lib/packGenerator.ts index 5a7ff6f..19463eb 100644 --- a/src/lib/packGenerator.ts +++ b/src/lib/packGenerator.ts @@ -274,6 +274,7 @@ export async function generateAssetPack(opts: { session: GenSession; sourceImage: GenImage; kind: PackKind; + onProgress?: (pack: AssetPack) => Promise; }): Promise<{ pack: AssetPack; manifest: ExportManifest; provider: 'mock' | 'gpt' }> { const templates = sortTemplatesByAnchor(getPackTemplates(opts.kind)); const initialCharacterSpec = opts.session.characterSpec?.sourceImageId === opts.sourceImage.id @@ -291,6 +292,20 @@ export async function generateAssetPack(opts: { const provider = detectProvider(); const assets: ToyAsset[] = []; + const pack: AssetPack = { + id: packId, + kind: opts.kind, + sessionId: opts.session.id, + sourceImageId: opts.sourceImage.id, + sourceImageUrl: opts.sourceImage.url, + characterSpec, + assets, + manifestId: `manifest_${packId}`, + createdAt, + version, + status: 'draft', + }; + for (const template of templates) { const assetId = `${opts.kind}_${template.filenamePart}_${randomBytes(3).toString('hex')}`; const anchorAsset = template.anchorTemplateId @@ -332,6 +347,7 @@ export async function generateAssetPack(opts: { anchorTemplateId: template.anchorTemplateId, }, }); + await opts.onProgress?.(pack); continue; } const generated = await generateAssetImage({ @@ -366,25 +382,13 @@ export async function generateAssetPack(opts: { raw: generated.raw, }, }); + await opts.onProgress?.(pack); } - const manifestId = `manifest_${packId}`; - const pack: AssetPack = { - id: packId, - kind: opts.kind, - sessionId: opts.session.id, - sourceImageId: opts.sourceImage.id, - sourceImageUrl: opts.sourceImage.url, - characterSpec, - assets, - manifestId, - createdAt, - version, - status: 'complete', - }; + pack.status = 'complete'; const manifest: ExportManifest = { - id: manifestId, + id: pack.manifestId, sessionId: opts.session.id, packId, packKind: opts.kind, diff --git a/src/lib/types.ts b/src/lib/types.ts index cf2b286..f16e7c4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -190,6 +190,7 @@ export type GeneratePackRequest = { sessionId: string; imageId: string; kind: PackKind; + background?: boolean; }; export type GeneratePackResponse = { @@ -201,6 +202,7 @@ export type GeneratePackResponse = { export type GenerateAllPacksRequest = { sessionId: string; imageId: string; + background?: boolean; }; export type GenerateAllPacksResponse = {