fix: persist detail generation archives during in-progress runs
This commit is contained in:
parent
87f645eb3b
commit
ac51a8c6f5
@ -103,7 +103,7 @@ export async function GET(
|
||||
eq(canvasDetailArchive.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(canvasDetailArchive.createdAt))
|
||||
.orderBy(desc(canvasDetailArchive.updatedAt))
|
||||
.limit(30);
|
||||
|
||||
return NextResponse.json({ success: true, data: archives });
|
||||
@ -161,3 +161,66 @@ export async function POST(
|
||||
return NextResponse.json({ success: false, error: "保存详情存档失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id: projectId } = await params;
|
||||
const ownsProject = await ensureProjectOwnership(projectId, session.user.id);
|
||||
if (!ownsProject) {
|
||||
return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const archiveId = String((body as any)?.archiveId || "").trim();
|
||||
const archiveRaw = ((body as any)?.archive || {}) as Record<string, unknown>;
|
||||
if (!archiveId) {
|
||||
return NextResponse.json({ success: false, error: "缺少存档ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
const title = String(archiveRaw.title || "").trim() || "AI详情项目";
|
||||
const prompt = String(archiveRaw.prompt || "").replace(/\s+/g, " ").trim();
|
||||
const summary = String(archiveRaw.summary || "").trim();
|
||||
const imageCount = Number(archiveRaw.imageCount);
|
||||
const coverUrl = String(archiveRaw.coverUrl || "").trim();
|
||||
const brief = sanitizeBrief(archiveRaw.brief);
|
||||
const slots = sanitizeSlots(archiveRaw.slots);
|
||||
|
||||
const [updated] = await db
|
||||
.update(canvasDetailArchive)
|
||||
.set({
|
||||
title,
|
||||
prompt,
|
||||
brief,
|
||||
slots,
|
||||
summary: summary || null,
|
||||
imageCount: Number.isFinite(imageCount) ? Math.max(0, Math.round(imageCount)) : null,
|
||||
coverUrl: coverUrl || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(canvasDetailArchive.id, archiveId),
|
||||
eq(canvasDetailArchive.projectId, projectId),
|
||||
eq(canvasDetailArchive.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json({ success: false, error: "存档不存在或无权限" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, data: updated });
|
||||
} catch (error) {
|
||||
console.error("[CanvasDetailArchive] PATCH failed:", error);
|
||||
return NextResponse.json({ success: false, error: "更新详情存档失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,6 +313,7 @@ type DetailArchive = {
|
||||
brief: DetailBriefForm;
|
||||
slots: Array<PickedObjectAttachment | null>;
|
||||
createdAt: number;
|
||||
updatedAt?: number;
|
||||
summary?: string;
|
||||
imageCount?: number;
|
||||
coverUrl?: string;
|
||||
@ -1789,11 +1790,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const timestamp = Date.parse(String(item.createdAt || ""));
|
||||
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
||||
})(),
|
||||
updatedAt:
|
||||
typeof (item as any).updatedAt === "number"
|
||||
? Number((item as any).updatedAt)
|
||||
: (() => {
|
||||
const timestamp = Date.parse(String((item as any).updatedAt || ""));
|
||||
return Number.isFinite(timestamp) ? timestamp : undefined;
|
||||
})(),
|
||||
summary: item.summary ? String(item.summary) : undefined,
|
||||
imageCount: Number(item.imageCount || 0) || undefined,
|
||||
coverUrl: item.coverUrl ? String(item.coverUrl) : undefined,
|
||||
}))
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.updatedAt || b.createdAt) - (a.updatedAt || a.createdAt)
|
||||
);
|
||||
|
||||
setDetailArchives(sanitized.slice(0, 30));
|
||||
} catch (error) {
|
||||
@ -2523,8 +2534,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
[updateDetailSlotAttachment]
|
||||
);
|
||||
|
||||
const saveDetailArchiveToDatabase = useCallback(
|
||||
async (archive: DetailArchive) => {
|
||||
const updateLocalDetailArchive = useCallback(
|
||||
(archiveId: string, patch: Partial<DetailArchive>) => {
|
||||
setDetailArchives((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === archiveId
|
||||
? { ...item, ...patch, updatedAt: patch.updatedAt || Date.now() }
|
||||
: item
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const createDetailArchiveInDatabase = useCallback(
|
||||
async (archive: DetailArchive): Promise<DetailArchive | null> => {
|
||||
try {
|
||||
const response = await fetch(`/api/canvas/${canvasId}/detail-archives`, {
|
||||
method: "POST",
|
||||
@ -2534,25 +2558,64 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
body: JSON.stringify({ archive }),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result?.success) {
|
||||
if (!response.ok || !result?.success || !result?.data?.id) {
|
||||
console.error("[CanvasChat] Failed to persist detail archive:", result?.error || response.statusText);
|
||||
return null;
|
||||
}
|
||||
const createdAtValue =
|
||||
typeof result.data.createdAt === "number"
|
||||
? result.data.createdAt
|
||||
: Date.parse(String(result.data.createdAt || ""));
|
||||
return {
|
||||
...archive,
|
||||
id: String(result.data.id),
|
||||
createdAt: Number.isFinite(createdAtValue) ? createdAtValue : archive.createdAt,
|
||||
updatedAt: Number.isFinite(createdAtValue) ? createdAtValue : archive.createdAt,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[CanvasChat] Failed to persist detail archive:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[canvasId]
|
||||
);
|
||||
|
||||
const patchDetailArchiveInDatabase = useCallback(
|
||||
async (archiveId: string, archive: DetailArchive) => {
|
||||
try {
|
||||
const response = await fetch(`/api/canvas/${canvasId}/detail-archives`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ archiveId, archive }),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result?.success) {
|
||||
console.error(
|
||||
"[CanvasChat] Failed to update detail archive:",
|
||||
result?.error || response.statusText
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[CanvasChat] Failed to update detail archive:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[canvasId]
|
||||
);
|
||||
|
||||
const saveDetailArchiveSnapshot = useCallback(
|
||||
(params: {
|
||||
async (params: {
|
||||
summary?: string;
|
||||
imageCount?: number;
|
||||
coverUrl?: string;
|
||||
prompt: string;
|
||||
brief: DetailBriefForm;
|
||||
slots: Array<PickedObjectAttachment | null>;
|
||||
}) => {
|
||||
}): Promise<DetailArchive | null> => {
|
||||
const archiveId = generateUUID();
|
||||
const promptText = params.prompt.replace(/\s+/g, " ").trim();
|
||||
const spuSeed = String(params.brief.productSpu || "").replace(/\s+/g, "").trim();
|
||||
@ -2583,15 +2646,24 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
: null;
|
||||
}),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
summary: params.summary,
|
||||
imageCount: params.imageCount,
|
||||
coverUrl: params.coverUrl,
|
||||
};
|
||||
|
||||
setDetailArchives((prev) => [archive, ...prev].slice(0, 30));
|
||||
void saveDetailArchiveToDatabase(archive);
|
||||
const saved = await createDetailArchiveInDatabase(archive);
|
||||
if (saved?.id) {
|
||||
setDetailArchives((prev) => {
|
||||
const next = [saved, ...prev.filter((item) => item.id !== archive.id && item.id !== saved.id)];
|
||||
return next.slice(0, 30);
|
||||
});
|
||||
return saved;
|
||||
}
|
||||
return archive;
|
||||
},
|
||||
[saveDetailArchiveToDatabase]
|
||||
[createDetailArchiveInDatabase]
|
||||
);
|
||||
|
||||
const loadDetailArchive = useCallback((archive: DetailArchive) => {
|
||||
@ -2727,6 +2799,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
const outputLanguage: "zh" | "en" = /[a-zA-Z]/.test(detailPrompt) ? "en" : "zh";
|
||||
let workingArchive: DetailArchive | null = null;
|
||||
let completedDetailImageCount = 0;
|
||||
const generatedImages: Array<{ originalUrl: string; thumbnailUrl: string }> = [];
|
||||
|
||||
setIsDetailGenerating(true);
|
||||
setDetailProgress({
|
||||
@ -2736,6 +2811,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
total: Math.max(1, detailCount + 1 + DETAIL_REVIEW_COUNT),
|
||||
});
|
||||
try {
|
||||
workingArchive = await saveDetailArchiveSnapshot({
|
||||
summary: `进行中:已完成 0/${detailCount} 张详情图`,
|
||||
imageCount: 0,
|
||||
coverUrl: undefined,
|
||||
prompt: archiveSnapshot.prompt,
|
||||
brief: archiveSnapshot.brief,
|
||||
slots: archiveSnapshot.slots,
|
||||
});
|
||||
|
||||
const detailReferenceAttachments = DETAIL_SLOT_CONFIGS.map((config, index) => {
|
||||
const current = detailSlots[index];
|
||||
if (!current?.url) return null;
|
||||
@ -2835,8 +2919,6 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
: siteSettings?.canvasAutoModeDefaultModelId ||
|
||||
siteSettings?.canvasDefaultModelId ||
|
||||
undefined;
|
||||
|
||||
const generatedImages: Array<{ originalUrl: string; thumbnailUrl: string }> = [];
|
||||
for (let index = 0; index < prompts.length; index += 1) {
|
||||
const screen = screenSpecs[index];
|
||||
const useModelForScene = Boolean(screen?.useModel);
|
||||
@ -2940,6 +3022,25 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
const generated = generationResult.data.images[0]!;
|
||||
generatedImages.push(generated);
|
||||
completedDetailImageCount = generatedImages.length;
|
||||
|
||||
if (workingArchive?.id) {
|
||||
const progressSummary = `进行中:已完成 ${completedDetailImageCount}/${prompts.length} 张详情图`;
|
||||
const progressCoverUrl = generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl;
|
||||
const nextArchive: DetailArchive = {
|
||||
...workingArchive,
|
||||
summary: progressSummary,
|
||||
imageCount: completedDetailImageCount,
|
||||
coverUrl: progressCoverUrl,
|
||||
};
|
||||
workingArchive = nextArchive;
|
||||
updateLocalDetailArchive(nextArchive.id, {
|
||||
summary: progressSummary,
|
||||
imageCount: completedDetailImageCount,
|
||||
coverUrl: progressCoverUrl,
|
||||
});
|
||||
await patchDetailArchiveInDatabase(nextArchive.id, nextArchive);
|
||||
}
|
||||
|
||||
const dimensions = await getImageDimensions(
|
||||
generated.originalUrl || generated.thumbnailUrl
|
||||
@ -3140,14 +3241,35 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
current: totalSteps,
|
||||
total: totalSteps,
|
||||
});
|
||||
saveDetailArchiveSnapshot({
|
||||
summary: planData.summary,
|
||||
imageCount: generatedImages.length,
|
||||
coverUrl: generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl,
|
||||
prompt: archiveSnapshot.prompt,
|
||||
brief: archiveSnapshot.brief,
|
||||
slots: archiveSnapshot.slots,
|
||||
});
|
||||
const finalSummary =
|
||||
planData.summary ||
|
||||
(reviewFailures.length > 0
|
||||
? `详情图完成 ${generatedImages.length} 张,评价晒图部分失败 ${reviewFailures.length} 条`
|
||||
: `已完成 ${generatedImages.length} 张详情图 + ${reviewGroups.length} 条评价晒图`);
|
||||
if (workingArchive?.id) {
|
||||
const finalArchive: DetailArchive = {
|
||||
...workingArchive,
|
||||
summary: finalSummary,
|
||||
imageCount: generatedImages.length,
|
||||
coverUrl: generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl,
|
||||
};
|
||||
workingArchive = finalArchive;
|
||||
updateLocalDetailArchive(finalArchive.id, {
|
||||
summary: finalArchive.summary,
|
||||
imageCount: finalArchive.imageCount,
|
||||
coverUrl: finalArchive.coverUrl,
|
||||
});
|
||||
await patchDetailArchiveInDatabase(finalArchive.id, finalArchive);
|
||||
} else {
|
||||
await saveDetailArchiveSnapshot({
|
||||
summary: finalSummary,
|
||||
imageCount: generatedImages.length,
|
||||
coverUrl: generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl,
|
||||
prompt: archiveSnapshot.prompt,
|
||||
brief: archiveSnapshot.brief,
|
||||
slots: archiveSnapshot.slots,
|
||||
});
|
||||
}
|
||||
|
||||
if (reviewFailures.length > 0) {
|
||||
toast.warning(
|
||||
@ -3160,6 +3282,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : "AI详情生成失败";
|
||||
if (workingArchive?.id) {
|
||||
const failedSummary = `异常中断:已完成 ${completedDetailImageCount} 张详情图,错误:${errMsg}`;
|
||||
const failedArchive: DetailArchive = {
|
||||
...workingArchive,
|
||||
summary: failedSummary,
|
||||
imageCount: completedDetailImageCount,
|
||||
coverUrl: generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl,
|
||||
};
|
||||
updateLocalDetailArchive(failedArchive.id, {
|
||||
summary: failedSummary,
|
||||
imageCount: completedDetailImageCount,
|
||||
coverUrl: failedArchive.coverUrl,
|
||||
});
|
||||
void patchDetailArchiveInDatabase(failedArchive.id, failedArchive);
|
||||
}
|
||||
setDetailProgress((prev) => ({
|
||||
status: "error",
|
||||
stage: errMsg,
|
||||
@ -3194,11 +3331,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
input,
|
||||
isDetailGenerating,
|
||||
normalizedDetailBrief,
|
||||
patchDetailArchiveInDatabase,
|
||||
selectedModelId,
|
||||
saveDetailArchiveSnapshot,
|
||||
setMessages,
|
||||
siteSettings?.canvasAutoModeDefaultModelId,
|
||||
siteSettings?.canvasDefaultModelId,
|
||||
updateLocalDetailArchive,
|
||||
]);
|
||||
|
||||
const handleGenerateMoreDetailReviews = useCallback(async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user