fix: stream agent replacement results incrementally
This commit is contained in:
parent
5fc5a5e47c
commit
29f94248f4
@ -5948,7 +5948,6 @@ export async function POST(request: Request) {
|
||||
(isDetailReplacementPreset || isSkuReplacementPreset)
|
||||
) {
|
||||
const replacementSlot = orderedSlots[0];
|
||||
const replacementAssistantMessageId = generateUUID();
|
||||
const replacementTemplateReferenceImageUrls = Array.from(
|
||||
new Set(
|
||||
(replacementSlot?.referenceAssets || [])
|
||||
@ -6000,12 +5999,6 @@ export async function POST(request: Request) {
|
||||
typeof mergedImageParams.imageSize === "string"
|
||||
? mergedImageParams.imageSize
|
||||
: effectiveImageParams?.imageSize || defaultImageQualityParams.imageSize;
|
||||
const replacementSections: string[] = [];
|
||||
const replacementResultImages: Array<{
|
||||
originalUrl: string;
|
||||
thumbnailUrl?: string | null;
|
||||
}> = [];
|
||||
let generatedReplacementCount = 0;
|
||||
let resolvedModelDisplayName =
|
||||
selectedAgentPreset.defaultModel?.displayName || selectedAgentPreset.name;
|
||||
const replacementVisionModels = await getEnabledAiModelsWithProvider().catch(
|
||||
@ -6022,6 +6015,103 @@ export async function POST(request: Request) {
|
||||
"agent-preset:replacement",
|
||||
12_000,
|
||||
);
|
||||
const emitReplacementResultMessage = async (params: {
|
||||
reference: (typeof replacementReferences)[number];
|
||||
bodyText?: string | null;
|
||||
image: {
|
||||
originalUrl: string;
|
||||
thumbnailUrl?: string | null;
|
||||
};
|
||||
modelDisplayName?: string | null;
|
||||
sourceImageUrls: string[];
|
||||
sourceType: "detail_replacement" | "sku_replacement";
|
||||
streamLabel: string;
|
||||
}) => {
|
||||
const assistantText = prependImageModelBadge(
|
||||
[
|
||||
`### ${params.reference.label}`,
|
||||
String(params.bodyText || "").trim(),
|
||||
buildAgentToolImagesMarkdown([
|
||||
{
|
||||
originalUrl: params.image.originalUrl,
|
||||
thumbnailUrl: params.image.thumbnailUrl,
|
||||
imageSize: resolvedImageSize,
|
||||
},
|
||||
]),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
params.modelDisplayName || null,
|
||||
);
|
||||
const assistantMessageId = generateUUID();
|
||||
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
id: assistantMessageId,
|
||||
chatId: id,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: assistantText }],
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
videoStorageStatus: null,
|
||||
temporaryVideoUrl: null,
|
||||
permanentVideoUrl: null,
|
||||
archivedAt: null,
|
||||
videoMetadata: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await saveGeneratedImages(
|
||||
assistantText,
|
||||
session.user.id,
|
||||
id,
|
||||
assistantMessageId,
|
||||
baseReplacementPrompt || selectedAgentPreset.name,
|
||||
resolvedModelRef,
|
||||
{
|
||||
sourceAssetIds,
|
||||
sourceImageUrls: params.sourceImageUrls,
|
||||
relationType: "edited_from",
|
||||
sourceType: params.sourceType,
|
||||
projectId: contextPayload?.projectId || null,
|
||||
userPrompt: requestTextContent,
|
||||
optimizedPrompt: baseReplacementPrompt || undefined,
|
||||
requestedImageCount: 1,
|
||||
aspectRatio: resolvedAspectRatio,
|
||||
imageSize: resolvedImageSize,
|
||||
},
|
||||
);
|
||||
|
||||
writeAssistantTextChunk(
|
||||
writer,
|
||||
assistantText,
|
||||
params.streamLabel,
|
||||
assistantMessageId,
|
||||
);
|
||||
|
||||
if (contextPayload?.source === "canvas") {
|
||||
writeCanvasMediaChunk(
|
||||
writer,
|
||||
{
|
||||
messageId: assistantMessageId,
|
||||
imageUrls: [params.image.originalUrl],
|
||||
prompt: baseReplacementPrompt || selectedAgentPreset.name,
|
||||
sourceImageUrls: params.sourceImageUrls,
|
||||
agentPresetId: selectedAgentPreset.id,
|
||||
agentPresetName: selectedAgentPreset.name,
|
||||
agentCategoryId:
|
||||
selectedAgentPreset.category?.id ||
|
||||
selectedAgentPreset.categoryId ||
|
||||
null,
|
||||
agentCategoryName: selectedAgentPreset.category?.name || null,
|
||||
agentExecutionSnapshotId: presetSnapshot.id,
|
||||
},
|
||||
`${params.streamLabel}:canvas-media`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
for (const [referenceIndex, reference] of replacementReferences.entries()) {
|
||||
@ -6042,23 +6132,32 @@ export async function POST(request: Request) {
|
||||
imageUrl: reference.imageUrl,
|
||||
models: replacementVisionModels,
|
||||
});
|
||||
const replacementSourceType = isDetailReplacementPreset
|
||||
? "detail_replacement"
|
||||
: "sku_replacement";
|
||||
const referenceSourceImageUrls = Array.from(
|
||||
new Set([
|
||||
...sharedUploadedImageUrls,
|
||||
...replacementTemplateReferenceImageUrls,
|
||||
reference.imageUrl,
|
||||
]),
|
||||
);
|
||||
|
||||
if (!referenceDecision.shouldReplace) {
|
||||
replacementResultImages.push({
|
||||
originalUrl: reference.imageUrl,
|
||||
thumbnailUrl: reference.imageUrl,
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
bodyText: referenceDecision.reason,
|
||||
image: {
|
||||
originalUrl: reference.imageUrl,
|
||||
thumbnailUrl: reference.imageUrl,
|
||||
},
|
||||
modelDisplayName: null,
|
||||
sourceImageUrls: referenceSourceImageUrls,
|
||||
sourceType: replacementSourceType,
|
||||
streamLabel: isDetailReplacementPreset
|
||||
? "agent-preset:image:detail-replacement:item"
|
||||
: "agent-preset:image:sku-replacement:item",
|
||||
});
|
||||
replacementSections.push(
|
||||
`### ${reference.label}\n\n${referenceDecision.reason}\n\n${buildAgentToolImagesMarkdown(
|
||||
[
|
||||
{
|
||||
originalUrl: reference.imageUrl,
|
||||
thumbnailUrl: reference.imageUrl,
|
||||
imageSize: resolvedImageSize,
|
||||
},
|
||||
],
|
||||
)}`,
|
||||
);
|
||||
successCount += 1;
|
||||
continue;
|
||||
}
|
||||
@ -6105,132 +6204,53 @@ export async function POST(request: Request) {
|
||||
if (!firstImage) {
|
||||
throw new Error("未返回结果图片");
|
||||
}
|
||||
replacementResultImages.push(firstImage);
|
||||
replacementSections.push(
|
||||
`### ${reference.label}\n\n${buildAgentToolImagesMarkdown([
|
||||
{
|
||||
originalUrl: firstImage.originalUrl,
|
||||
thumbnailUrl: firstImage.thumbnailUrl,
|
||||
imageSize: resolvedImageSize,
|
||||
},
|
||||
])}`,
|
||||
);
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
image: firstImage,
|
||||
modelDisplayName: resolvedModelDisplayName,
|
||||
sourceImageUrls: Array.from(
|
||||
new Set([
|
||||
...sharedUploadedImageUrls,
|
||||
...replacementTemplateReferenceImageUrls,
|
||||
...replacementReferences.map((item) => item.imageUrl),
|
||||
]),
|
||||
),
|
||||
sourceType: replacementSourceType,
|
||||
streamLabel: isDetailReplacementPreset
|
||||
? "agent-preset:image:detail-replacement:item"
|
||||
: "agent-preset:image:sku-replacement:item",
|
||||
});
|
||||
successCount += 1;
|
||||
generatedReplacementCount += 1;
|
||||
} catch (replacementItemError) {
|
||||
console.error("[Chat API] Agent replacement item execution failed:", {
|
||||
referenceIndex,
|
||||
referenceLabel: reference.label,
|
||||
error: replacementItemError,
|
||||
});
|
||||
replacementSections.push(
|
||||
`### ${reference.label}\n\n执行失败:${
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
bodyText: `生成失败,当前先沿用参考图:${
|
||||
replacementItemError instanceof Error
|
||||
? replacementItemError.message
|
||||
: "请稍后重试"
|
||||
}`,
|
||||
);
|
||||
image: {
|
||||
originalUrl: reference.imageUrl,
|
||||
thumbnailUrl: reference.imageUrl,
|
||||
},
|
||||
modelDisplayName: null,
|
||||
sourceImageUrls: referenceSourceImageUrls,
|
||||
sourceType: replacementSourceType,
|
||||
streamLabel: isDetailReplacementPreset
|
||||
? "agent-preset:image:detail-replacement:item-fallback"
|
||||
: "agent-preset:image:sku-replacement:item-fallback",
|
||||
});
|
||||
successCount += 1;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stopKeepalive();
|
||||
}
|
||||
|
||||
const assistantText = prependImageModelBadge(
|
||||
[
|
||||
`### ${selectedAgentPreset.name}`,
|
||||
...replacementSections,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
generatedReplacementCount > 0 ? resolvedModelDisplayName : null,
|
||||
);
|
||||
|
||||
await saveMessages({
|
||||
messages: [
|
||||
{
|
||||
id: replacementAssistantMessageId,
|
||||
chatId: id,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: assistantText }],
|
||||
attachments: [],
|
||||
createdAt: new Date(),
|
||||
videoStorageStatus: null,
|
||||
temporaryVideoUrl: null,
|
||||
permanentVideoUrl: null,
|
||||
archivedAt: null,
|
||||
videoMetadata: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (replacementResultImages.length > 0) {
|
||||
await saveGeneratedImages(
|
||||
assistantText,
|
||||
session.user.id,
|
||||
id,
|
||||
replacementAssistantMessageId,
|
||||
baseReplacementPrompt || selectedAgentPreset.name,
|
||||
resolvedModelRef,
|
||||
{
|
||||
sourceAssetIds,
|
||||
sourceImageUrls: Array.from(
|
||||
new Set([
|
||||
...sharedUploadedImageUrls,
|
||||
...replacementTemplateReferenceImageUrls,
|
||||
...replacementReferences.map((item) => item.imageUrl),
|
||||
]),
|
||||
),
|
||||
relationType: "edited_from",
|
||||
sourceType: isDetailReplacementPreset
|
||||
? "detail_replacement"
|
||||
: "sku_replacement",
|
||||
projectId: contextPayload?.projectId || null,
|
||||
userPrompt: requestTextContent,
|
||||
optimizedPrompt: baseReplacementPrompt || undefined,
|
||||
requestedImageCount: replacementResultImages.length,
|
||||
aspectRatio: resolvedAspectRatio,
|
||||
imageSize: resolvedImageSize,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
writeAssistantTextChunk(
|
||||
writer,
|
||||
assistantText,
|
||||
isDetailReplacementPreset
|
||||
? "agent-preset:image:detail-replacement"
|
||||
: "agent-preset:image:sku-replacement",
|
||||
replacementAssistantMessageId,
|
||||
);
|
||||
if (contextPayload?.source === "canvas" && replacementResultImages.length > 0) {
|
||||
writeCanvasMediaChunk(
|
||||
writer,
|
||||
{
|
||||
messageId: replacementAssistantMessageId,
|
||||
imageUrls: replacementResultImages.map((image) => image.originalUrl),
|
||||
prompt: baseReplacementPrompt || selectedAgentPreset.name,
|
||||
sourceImageUrls: Array.from(
|
||||
new Set([
|
||||
...sharedUploadedImageUrls,
|
||||
...replacementTemplateReferenceImageUrls,
|
||||
...replacementReferences.map((item) => item.imageUrl),
|
||||
]),
|
||||
),
|
||||
agentPresetId: selectedAgentPreset.id,
|
||||
agentPresetName: selectedAgentPreset.name,
|
||||
agentCategoryId:
|
||||
selectedAgentPreset.category?.id ||
|
||||
selectedAgentPreset.categoryId ||
|
||||
null,
|
||||
agentCategoryName: selectedAgentPreset.category?.name || null,
|
||||
agentExecutionSnapshotId: presetSnapshot.id,
|
||||
},
|
||||
isDetailReplacementPreset
|
||||
? "agent-preset:image:detail-replacement:canvas-media"
|
||||
: "agent-preset:image:sku-replacement:canvas-media",
|
||||
);
|
||||
}
|
||||
} else if (selectedAgentPreset.type === "image" && isSceneGenerationPreset) {
|
||||
const sceneSlot = orderedSlots[0];
|
||||
const sceneAssistantMessageId = generateUUID();
|
||||
|
||||
@ -834,6 +834,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
() => Array.from(new Set(generatedGroups.flatMap((group) => group.imageUrls))),
|
||||
[generatedGroups]
|
||||
);
|
||||
const replacementGeneratedGroupsAfterLastUser = useMemo(() => {
|
||||
const lastUserIndex = [...messages]
|
||||
.map((message) => message.role)
|
||||
.lastIndexOf("user");
|
||||
if (lastUserIndex < 0) {
|
||||
return [] as GeneratedImageGroup[];
|
||||
}
|
||||
|
||||
return messages
|
||||
.slice(lastUserIndex + 1)
|
||||
.filter((message) => message.role === "assistant")
|
||||
.map((message) => generatedGroups.find((group) => group.messageId === message.id) || null)
|
||||
.filter((group): group is GeneratedImageGroup => Boolean(group));
|
||||
}, [generatedGroups, messages]);
|
||||
const latestGeneratedGroup = useMemo(() => {
|
||||
const lastUserIndex = [...messages]
|
||||
.map((message) => message.role)
|
||||
@ -857,7 +871,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
return generatedGroups[generatedGroups.length - 1] || null;
|
||||
}, [generatedGroups, messages]);
|
||||
const latestReplacementResultUrls = latestGeneratedGroup?.imageUrls || [];
|
||||
const latestReplacementResultUrls = useMemo(
|
||||
() =>
|
||||
replacementGeneratedGroupsAfterLastUser.flatMap((group) => group.imageUrls),
|
||||
[replacementGeneratedGroupsAfterLastUser]
|
||||
);
|
||||
const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null;
|
||||
const isGenerating = status === "streaming" || status === "submitted";
|
||||
const detailResultCards = useMemo(
|
||||
@ -2240,10 +2258,21 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
)}
|
||||
|
||||
{selectedModelAttachment ? (
|
||||
<div className="rounded-[22px] border border-emerald-400/20 bg-emerald-500/[0.06] p-3">
|
||||
<div className="text-xs font-medium text-emerald-200">模特图</div>
|
||||
<div className="mt-1 truncate text-[11px] text-emerald-100/70">
|
||||
{selectedModelAttachment.name || "已选择模特图"}
|
||||
<div className="overflow-hidden rounded-[22px] border border-emerald-400/20 bg-emerald-500/[0.06]">
|
||||
<div className="relative aspect-[5/4] overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.12),transparent_58%),linear-gradient(180deg,#08110d_0%,#101916_100%)]">
|
||||
<img
|
||||
src={buildPreviewImageUrl(selectedModelAttachment.url, 720)}
|
||||
alt={selectedModelAttachment.name || "模特图"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute left-3 top-3 rounded-full border border-emerald-300/25 bg-black/45 px-2.5 py-1 text-[11px] text-emerald-100">
|
||||
模特图
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-emerald-400/15 px-3 py-2">
|
||||
<div className="truncate text-xs font-medium text-emerald-100">
|
||||
{selectedModelAttachment.name || "已选择模特图"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@ -2402,7 +2431,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div
|
||||
key={card.id}
|
||||
className={cn(
|
||||
"cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
"w-full cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
isActive
|
||||
? "border-emerald-400/40 shadow-[0_0_0_1px_rgba(52,211,153,0.15)]"
|
||||
: "border-white/10"
|
||||
@ -2481,7 +2510,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div
|
||||
key={`${card.id}-result`}
|
||||
className={cn(
|
||||
"cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
"w-full cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
isActive
|
||||
? "border-emerald-400/40 shadow-[0_0_0_1px_rgba(52,211,153,0.15)]"
|
||||
: "border-white/10"
|
||||
@ -2618,10 +2647,21 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
)}
|
||||
|
||||
{selectedModelAttachment ? (
|
||||
<div className="rounded-[22px] border border-emerald-400/20 bg-emerald-500/[0.06] p-3">
|
||||
<div className="text-xs font-medium text-emerald-200">模特图</div>
|
||||
<div className="mt-1 truncate text-[11px] text-emerald-100/70">
|
||||
{selectedModelAttachment.name || "已选择模特图"}
|
||||
<div className="overflow-hidden rounded-[22px] border border-emerald-400/20 bg-emerald-500/[0.06]">
|
||||
<div className="relative aspect-[5/4] overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.12),transparent_58%),linear-gradient(180deg,#08110d_0%,#101916_100%)]">
|
||||
<img
|
||||
src={buildPreviewImageUrl(selectedModelAttachment.url, 720)}
|
||||
alt={selectedModelAttachment.name || "模特图"}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute left-3 top-3 rounded-full border border-emerald-300/25 bg-black/45 px-2.5 py-1 text-[11px] text-emerald-100">
|
||||
模特图
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-emerald-400/15 px-3 py-2">
|
||||
<div className="truncate text-xs font-medium text-emerald-100">
|
||||
{selectedModelAttachment.name || "已选择模特图"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
@ -2799,7 +2839,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div
|
||||
key={card.id}
|
||||
className={cn(
|
||||
"cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
"w-full cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
activeSkuReferenceCardId === card.id
|
||||
? "border-emerald-400/40 shadow-[0_0_0_1px_rgba(52,211,153,0.15)]"
|
||||
: "border-white/10"
|
||||
@ -2920,7 +2960,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div
|
||||
key={`${card.id}-result`}
|
||||
className={cn(
|
||||
"cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
"w-full cursor-pointer overflow-hidden rounded-[22px] border bg-black/20 transition-colors",
|
||||
activeSkuReferenceCardId === card.id
|
||||
? "border-emerald-400/40 shadow-[0_0_0_1px_rgba(52,211,153,0.15)]"
|
||||
: "border-white/10"
|
||||
|
||||
@ -180,8 +180,8 @@ export function AgentPresetCardDark({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="relative h-[64px] w-[64px] shrink-0 overflow-hidden rounded-[18px] border border-white/10 bg-black/30">
|
||||
<div className="flex items-stretch gap-3">
|
||||
<div className="relative w-[88px] min-h-[112px] shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-black/30">
|
||||
{previewUrl ? (
|
||||
<img
|
||||
src={previewUrl}
|
||||
@ -199,7 +199,7 @@ export function AgentPresetCardDark({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 pr-7">
|
||||
<div className="flex min-h-[112px] min-w-0 flex-1 flex-col justify-between pr-7">
|
||||
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
|
||||
{preset.name}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user