auto-save 2026-05-19 11:29 (~6)

This commit is contained in:
2026-05-19 11:31:13 +08:00
parent 74148d0d05
commit 65a9080e02
6 changed files with 124 additions and 29 deletions

View File

@@ -523,6 +523,25 @@
"type": "session-heartbeat", "type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交fix: update OpenAI image response handling", "message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交fix: update OpenAI image response handling",
"files_changed": 1 "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
} }
] ]
} }

View File

@@ -3,13 +3,22 @@ import { generateAssetPack } from '@/lib/packGenerator';
import { detectProvider } from '@/lib/providers'; import { detectProvider } from '@/lib/providers';
import { loadSession, saveSession } from '@/lib/storage'; import { loadSession, saveSession } from '@/lib/storage';
import { PACK_ORDER } from '@/lib/templates'; 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 runtime = 'nodejs';
export const dynamic = 'force-dynamic'; 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) { 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) { if (!sessionId || !imageId) {
return NextResponse.json({ error: 'sessionId and imageId required' }, { status: 400 }); 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 }); 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 packs = [];
const manifests = []; const manifests = [];
let workingSession = session; let workingSession: GenSession = baseSession;
for (const kind of PACK_ORDER) { 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); packs.push(generated.pack);
manifests.push(generated.manifest); manifests.push(generated.manifest);
workingSession = { workingSession = {
@@ -48,11 +64,24 @@ export async function POST(req: Request) {
await saveSession(workingSession); await saveSession(workingSession);
return NextResponse.json({ return {
packs, packs,
manifests, manifests,
provider: detectProvider(), 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) { } catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });
} }

View File

@@ -1,15 +1,25 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { generateAssetPack } from '@/lib/packGenerator'; import { generateAssetPack } from '@/lib/packGenerator';
import { detectProvider } from '@/lib/providers';
import { loadSession, saveSession } from '@/lib/storage'; 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 runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
const PACK_KINDS: PackKind[] = ['patent', 'accessories', 'production', 'marketing']; 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) { 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)) { if (!sessionId || !imageId || !PACK_KINDS.includes(kind)) {
return NextResponse.json({ error: 'sessionId, imageId and valid kind required' }, { status: 400 }); 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 }); return NextResponse.json({ error: 'image must be selected before generating packs' }, { status: 400 });
} }
try { async function run() {
const { pack, manifest, provider } = await generateAssetPack({ session, sourceImage, kind }); const { pack, manifest, provider } = await generateAssetPack({
session,
sourceImage,
kind,
onProgress: progressPack => persistPackProgress(session, imageId, progressPack),
});
session.characterSpec = pack.characterSpec; session.characterSpec = pack.characterSpec;
session.packs = [ session.packs = [
...(session.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)), ...(session.packs ?? []).filter(existing => !(existing.kind === kind && existing.sourceImageId === imageId)),
@@ -35,8 +50,23 @@ export async function POST(req: Request) {
manifest, manifest,
]; ];
await saveSession(session); 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) { } catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });
} }

View File

@@ -136,7 +136,7 @@ export default function Home() {
const r = await fetch('/api/packs/generate', { const r = await fetch('/api/packs/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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) { if (!r.ok) {
alert('素材包生成失败:' + (await r.text())); alert('素材包生成失败:' + (await r.text()));
@@ -147,6 +147,7 @@ export default function Home() {
const all = await refreshSessions(); const all = await refreshSessions();
const updated = all.find(x => x.id === current.id) ?? null; const updated = all.find(x => x.id === current.id) ?? null;
setCurrent(updated); setCurrent(updated);
scheduleSessionRefresh(current.id);
} finally { } finally {
setLoadingKind(null); setLoadingKind(null);
} }
@@ -159,6 +160,15 @@ export default function Home() {
return updated; 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) { async function handleLockCharacter(image: GenImage) {
if (!current || characterLoading) return; if (!current || characterLoading) return;
setCharacterLoading(true); setCharacterLoading(true);
@@ -187,7 +197,7 @@ export default function Home() {
const r = await fetch('/api/packs/generate-all', { const r = await fetch('/api/packs/generate-all', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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) { if (!r.ok) {
alert('完整三包生成失败:' + (await r.text())); alert('完整三包生成失败:' + (await r.text()));
@@ -196,6 +206,7 @@ export default function Home() {
const d: GenerateAllPacksResponse = await r.json(); const d: GenerateAllPacksResponse = await r.json();
setProvider(d.provider); setProvider(d.provider);
await reloadCurrent(current.id); await reloadCurrent(current.id);
scheduleSessionRefresh(current.id);
} finally { } finally {
setAllLoading(false); setAllLoading(false);
} }

View File

@@ -274,6 +274,7 @@ export async function generateAssetPack(opts: {
session: GenSession; session: GenSession;
sourceImage: GenImage; sourceImage: GenImage;
kind: PackKind; kind: PackKind;
onProgress?: (pack: AssetPack) => Promise<void>;
}): Promise<{ pack: AssetPack; manifest: ExportManifest; provider: 'mock' | 'gpt' }> { }): Promise<{ pack: AssetPack; manifest: ExportManifest; provider: 'mock' | 'gpt' }> {
const templates = sortTemplatesByAnchor(getPackTemplates(opts.kind)); const templates = sortTemplatesByAnchor(getPackTemplates(opts.kind));
const initialCharacterSpec = opts.session.characterSpec?.sourceImageId === opts.sourceImage.id const initialCharacterSpec = opts.session.characterSpec?.sourceImageId === opts.sourceImage.id
@@ -291,6 +292,20 @@ export async function generateAssetPack(opts: {
const provider = detectProvider(); const provider = detectProvider();
const assets: ToyAsset[] = []; 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) { for (const template of templates) {
const assetId = `${opts.kind}_${template.filenamePart}_${randomBytes(3).toString('hex')}`; const assetId = `${opts.kind}_${template.filenamePart}_${randomBytes(3).toString('hex')}`;
const anchorAsset = template.anchorTemplateId const anchorAsset = template.anchorTemplateId
@@ -332,6 +347,7 @@ export async function generateAssetPack(opts: {
anchorTemplateId: template.anchorTemplateId, anchorTemplateId: template.anchorTemplateId,
}, },
}); });
await opts.onProgress?.(pack);
continue; continue;
} }
const generated = await generateAssetImage({ const generated = await generateAssetImage({
@@ -366,25 +382,13 @@ export async function generateAssetPack(opts: {
raw: generated.raw, raw: generated.raw,
}, },
}); });
await opts.onProgress?.(pack);
} }
const manifestId = `manifest_${packId}`; pack.status = 'complete';
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',
};
const manifest: ExportManifest = { const manifest: ExportManifest = {
id: manifestId, id: pack.manifestId,
sessionId: opts.session.id, sessionId: opts.session.id,
packId, packId,
packKind: opts.kind, packKind: opts.kind,

View File

@@ -190,6 +190,7 @@ export type GeneratePackRequest = {
sessionId: string; sessionId: string;
imageId: string; imageId: string;
kind: PackKind; kind: PackKind;
background?: boolean;
}; };
export type GeneratePackResponse = { export type GeneratePackResponse = {
@@ -201,6 +202,7 @@ export type GeneratePackResponse = {
export type GenerateAllPacksRequest = { export type GenerateAllPacksRequest = {
sessionId: string; sessionId: string;
imageId: string; imageId: string;
background?: boolean;
}; };
export type GenerateAllPacksResponse = { export type GenerateAllPacksResponse = {