fix(agent): persist detail replacement tasks
This commit is contained in:
parent
9093b0370b
commit
87bbcf0ccd
@ -138,7 +138,11 @@ import {
|
|||||||
generatedImage,
|
generatedImage,
|
||||||
message as messageTable,
|
message as messageTable,
|
||||||
} from "@/lib/db/schema";
|
} from "@/lib/db/schema";
|
||||||
import { generateCanvasImage } from "@/lib/canvas/generate-image";
|
import {
|
||||||
|
generateCanvasImage,
|
||||||
|
type CanvasGenerateImageRequest,
|
||||||
|
} from "@/lib/canvas/generate-image";
|
||||||
|
import { generateCanvasImageWithPersistentQueue } from "@/lib/canvas/image-task-queue";
|
||||||
import {
|
import {
|
||||||
detectRequestedStoryboardGrid,
|
detectRequestedStoryboardGrid,
|
||||||
isGridConfirmationReply,
|
isGridConfirmationReply,
|
||||||
@ -5992,44 +5996,55 @@ export async function POST(request: Request) {
|
|||||||
let normalizeElapsedMs: number | null = null;
|
let normalizeElapsedMs: number | null = null;
|
||||||
try {
|
try {
|
||||||
generationStartedAt = Date.now();
|
generationStartedAt = Date.now();
|
||||||
const result = await generateCanvasImage({
|
const result = await generateCanvasImageWithPersistentQueue({
|
||||||
prompt: replacementPrompt,
|
userId: session.user.id,
|
||||||
negativePrompt: effectiveImageParams?.negativePrompt,
|
request: {
|
||||||
model: resolvedModelRef,
|
prompt: replacementPrompt,
|
||||||
ownerKey: session.user.id,
|
negativePrompt: effectiveImageParams?.negativePrompt,
|
||||||
aspectRatio: generationAspectRatio,
|
model: resolvedModelRef,
|
||||||
imageSize: resolvedImageSize,
|
ownerKey: session.user.id,
|
||||||
imageCount: 1,
|
aspectRatio: generationAspectRatio,
|
||||||
referenceImages: sourceImageUrls,
|
imageSize: resolvedImageSize,
|
||||||
debugLabel: "agent-detail-replacement",
|
imageCount: 1,
|
||||||
debugMeta: {
|
referenceImages: sourceImageUrls,
|
||||||
referenceLabel: reference.label,
|
debugLabel: "agent-detail-replacement",
|
||||||
referenceWidth: referenceGeometry?.width,
|
debugMeta: {
|
||||||
referenceHeight: referenceGeometry?.height,
|
operation: "agent-detail-replacement",
|
||||||
referenceAspectRatio: referenceGeometry?.aspectRatio,
|
chatId: id,
|
||||||
|
messageId: message.id,
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
referenceId: reference.id,
|
||||||
|
referenceLabel: reference.label,
|
||||||
|
referenceWidth: referenceGeometry?.width,
|
||||||
|
referenceHeight: referenceGeometry?.height,
|
||||||
|
referenceAspectRatio: referenceGeometry?.aspectRatio,
|
||||||
|
},
|
||||||
|
onQueueStatusChange:
|
||||||
|
contextPayload?.source === "canvas"
|
||||||
|
? async (queueStatus) => {
|
||||||
|
writeImageGenerationStatusChunk(
|
||||||
|
writer,
|
||||||
|
{
|
||||||
|
phase:
|
||||||
|
queueStatus.status === "running"
|
||||||
|
? "processing"
|
||||||
|
: "queued",
|
||||||
|
modelName:
|
||||||
|
queueStatus.modelName ||
|
||||||
|
resolvedModelDisplayName ||
|
||||||
|
resolvedModelRef,
|
||||||
|
imageCount: 1,
|
||||||
|
queuePosition: queueStatus.queuePosition,
|
||||||
|
sameModelRunningCount:
|
||||||
|
queueStatus.sameModelRunningCount,
|
||||||
|
concurrencyLimit: queueStatus.concurrencyLimit,
|
||||||
|
estimatedWaitMs: queueStatus.estimatedWaitMs,
|
||||||
|
},
|
||||||
|
`agent-mode:detail-replacement:queue:${reference.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
onQueueStatusChange:
|
|
||||||
contextPayload?.source === "canvas"
|
|
||||||
? async (queueStatus) => {
|
|
||||||
writeImageGenerationStatusChunk(
|
|
||||||
writer,
|
|
||||||
{
|
|
||||||
phase:
|
|
||||||
queueStatus.status === "running"
|
|
||||||
? "processing"
|
|
||||||
: "queued",
|
|
||||||
modelName:
|
|
||||||
queueStatus.modelName || resolvedModelDisplayName || resolvedModelRef,
|
|
||||||
imageCount: 1,
|
|
||||||
queuePosition: queueStatus.queuePosition,
|
|
||||||
sameModelRunningCount: queueStatus.sameModelRunningCount,
|
|
||||||
concurrencyLimit: queueStatus.concurrencyLimit,
|
|
||||||
estimatedWaitMs: queueStatus.estimatedWaitMs,
|
|
||||||
},
|
|
||||||
`agent-mode:detail-replacement:queue:${reference.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
generationElapsedMs = Date.now() - generationStartedAt;
|
generationElapsedMs = Date.now() - generationStartedAt;
|
||||||
resolvedModelDisplayName = result.model || resolvedModelDisplayName;
|
resolvedModelDisplayName = result.model || resolvedModelDisplayName;
|
||||||
@ -6879,7 +6894,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await generateCanvasImage({
|
const generationRequest: CanvasGenerateImageRequest = {
|
||||||
prompt: replacementPrompt,
|
prompt: replacementPrompt,
|
||||||
negativePrompt:
|
negativePrompt:
|
||||||
typeof mergedImageParams.negativePrompt === "string"
|
typeof mergedImageParams.negativePrompt === "string"
|
||||||
@ -6893,6 +6908,11 @@ export async function POST(request: Request) {
|
|||||||
referenceImages: sourceImageUrls,
|
referenceImages: sourceImageUrls,
|
||||||
debugLabel: `agent-${replacementSourceType}`,
|
debugLabel: `agent-${replacementSourceType}`,
|
||||||
debugMeta: {
|
debugMeta: {
|
||||||
|
operation: `agent-${replacementSourceType}`,
|
||||||
|
chatId: id,
|
||||||
|
messageId: message.id,
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
referenceId: reference.id,
|
||||||
referenceLabel: reference.label,
|
referenceLabel: reference.label,
|
||||||
referenceWidth: referenceGeometry?.width,
|
referenceWidth: referenceGeometry?.width,
|
||||||
referenceHeight: referenceGeometry?.height,
|
referenceHeight: referenceGeometry?.height,
|
||||||
@ -6921,8 +6941,14 @@ export async function POST(request: Request) {
|
|||||||
`agent-preset:replacement:queue:${reference.id}`,
|
`agent-preset:replacement:queue:${reference.id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
};
|
||||||
|
const result = isDetailReplacementPreset
|
||||||
|
? await generateCanvasImageWithPersistentQueue({
|
||||||
|
userId: session.user.id,
|
||||||
|
request: generationRequest,
|
||||||
|
})
|
||||||
|
: await generateCanvasImage(generationRequest);
|
||||||
resolvedModelDisplayName = result.model || resolvedModelDisplayName;
|
resolvedModelDisplayName = result.model || resolvedModelDisplayName;
|
||||||
const firstImage = result.images[0];
|
const firstImage = result.images[0];
|
||||||
if (!firstImage) {
|
if (!firstImage) {
|
||||||
|
|||||||
@ -109,6 +109,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
normalizeAgentPresetModeConfig,
|
normalizeAgentPresetModeConfig,
|
||||||
} from "@/lib/agents/preset-mode-config";
|
} from "@/lib/agents/preset-mode-config";
|
||||||
|
import {
|
||||||
|
AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS,
|
||||||
|
getAgentGenerationAcceptanceRemainingMs,
|
||||||
|
isAgentGenerationRequestAccepted,
|
||||||
|
} from "@/lib/agents/pending-generation-guard";
|
||||||
import {
|
import {
|
||||||
defaultAgentWorkflowBusinessModeSettings,
|
defaultAgentWorkflowBusinessModeSettings,
|
||||||
normalizeAgentWorkflowBusinessModeSettings,
|
normalizeAgentWorkflowBusinessModeSettings,
|
||||||
@ -2574,6 +2579,8 @@ interface LocalGenerationPendingState {
|
|||||||
modeType: AgentGenerationResultModeType;
|
modeType: AgentGenerationResultModeType;
|
||||||
expectedCount: number;
|
expectedCount: number;
|
||||||
userTurnCountAtStart: number;
|
userTurnCountAtStart: number;
|
||||||
|
serverUserTurnCountAtStart?: number;
|
||||||
|
requestMessageId?: string | null;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
requestedCardIds?: string[] | null;
|
requestedCardIds?: string[] | null;
|
||||||
}
|
}
|
||||||
@ -2628,6 +2635,15 @@ function getStoredAgentPendingGenerations(): LocalGenerationPendingState[] {
|
|||||||
modeType: String(record.modeType || "").trim() as AgentGenerationResultModeType,
|
modeType: String(record.modeType || "").trim() as AgentGenerationResultModeType,
|
||||||
expectedCount: Number(record.expectedCount),
|
expectedCount: Number(record.expectedCount),
|
||||||
userTurnCountAtStart: Number(record.userTurnCountAtStart),
|
userTurnCountAtStart: Number(record.userTurnCountAtStart),
|
||||||
|
serverUserTurnCountAtStart: Number.isFinite(
|
||||||
|
Number(record.serverUserTurnCountAtStart)
|
||||||
|
)
|
||||||
|
? Number(record.serverUserTurnCountAtStart)
|
||||||
|
: undefined,
|
||||||
|
requestMessageId:
|
||||||
|
typeof record.requestMessageId === "string"
|
||||||
|
? record.requestMessageId.trim() || null
|
||||||
|
: null,
|
||||||
timestamp: Number(record.timestamp),
|
timestamp: Number(record.timestamp),
|
||||||
requestedCardIds: Array.isArray(record.requestedCardIds)
|
requestedCardIds: Array.isArray(record.requestedCardIds)
|
||||||
? record.requestedCardIds
|
? record.requestedCardIds
|
||||||
@ -2660,16 +2676,6 @@ function saveStoredAgentPendingGenerations(items: LocalGenerationPendingState[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DETAIL_RESULT_PROCESSING_STAGES = [
|
|
||||||
"解码图像",
|
|
||||||
"定位选区",
|
|
||||||
"分离主体",
|
|
||||||
"重绘背景",
|
|
||||||
"融合边缘",
|
|
||||||
"检查细节",
|
|
||||||
"输出结果",
|
|
||||||
];
|
|
||||||
|
|
||||||
function DetailResultProcessingScanOverlay({
|
function DetailResultProcessingScanOverlay({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
label,
|
label,
|
||||||
@ -2677,29 +2683,6 @@ function DetailResultProcessingScanOverlay({
|
|||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
label: string;
|
label: string;
|
||||||
}) {
|
}) {
|
||||||
const [progress, setProgress] = useState(6);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
const durationMs = 240_000;
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
const elapsed = Date.now() - startedAt;
|
|
||||||
const linear = Math.min(1, elapsed / durationMs);
|
|
||||||
const eased = 1 - Math.pow(1 - linear, 1.8);
|
|
||||||
const drift = Math.sin(elapsed / 3200) * 1.2;
|
|
||||||
const next = Math.max(6, Math.min(96, Math.round(eased * 96 + drift)));
|
|
||||||
setProgress((current) => Math.max(current, next));
|
|
||||||
}, 720);
|
|
||||||
|
|
||||||
return () => window.clearInterval(timer);
|
|
||||||
}, [imageUrl, label]);
|
|
||||||
|
|
||||||
const stageIndex = Math.min(
|
|
||||||
DETAIL_RESULT_PROCESSING_STAGES.length - 1,
|
|
||||||
Math.floor((progress / 100) * DETAIL_RESULT_PROCESSING_STAGES.length)
|
|
||||||
);
|
|
||||||
const stageName = DETAIL_RESULT_PROCESSING_STAGES[stageIndex] || "处理中";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#07090f]">
|
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#07090f]">
|
||||||
<style>{`
|
<style>{`
|
||||||
@ -2718,6 +2701,11 @@ function DetailResultProcessingScanOverlay({
|
|||||||
70% { box-shadow: 0 0 0 8px rgba(125, 211, 252, 0); }
|
70% { box-shadow: 0 0 0 8px rgba(125, 211, 252, 0); }
|
||||||
100% { box-shadow: 0 0 0 0 rgba(125, 211, 252, 0); }
|
100% { box-shadow: 0 0 0 0 rgba(125, 211, 252, 0); }
|
||||||
}
|
}
|
||||||
|
@keyframes detailResultIndeterminate {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
55% { transform: translateX(80%); }
|
||||||
|
100% { transform: translateX(260%); }
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
{isHttpImageUrl(imageUrl) ? (
|
{isHttpImageUrl(imageUrl) ? (
|
||||||
<img
|
<img
|
||||||
@ -2790,24 +2778,13 @@ function DetailResultProcessingScanOverlay({
|
|||||||
style={{ animation: "detailResultScanPulseRing 1.8s ease-out infinite" }}
|
style={{ animation: "detailResultScanPulseRing 1.8s ease-out infinite" }}
|
||||||
/>
|
/>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span className="min-w-[34px] text-right tabular-nums text-slate-300">{progress}%</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-3 left-3 z-[70] isolate inline-flex items-center gap-2 rounded-full border border-white/14 bg-[#080b12]/95 px-2.5 py-1.5 text-[11px] font-semibold leading-none text-slate-100 shadow-[0_12px_28px_rgba(0,0,0,0.32)]"
|
|
||||||
style={{
|
|
||||||
WebkitFontSmoothing: "antialiased",
|
|
||||||
textShadow: "0 1px 1px rgba(0,0,0,0.72)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="tabular-nums text-sky-300">{String(stageIndex + 1).padStart(2, "0")}</span>
|
|
||||||
<span>{stageName}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-x-0 bottom-0 z-[60] h-0.5 bg-white/5">
|
<div className="absolute inset-x-0 bottom-0 z-[60] h-0.5 bg-white/5">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-r-full shadow-[0_0_8px_rgba(125,211,252,0.6)] transition-[width] duration-300 ease-linear"
|
className="h-full w-1/3 rounded-full shadow-[0_0_8px_rgba(125,211,252,0.6)]"
|
||||||
style={{
|
style={{
|
||||||
|
animation: "detailResultIndeterminate 1.8s ease-in-out infinite",
|
||||||
background: "linear-gradient(90deg, #7dd3fc, #a78bfa)",
|
background: "linear-gradient(90deg, #7dd3fc, #a78bfa)",
|
||||||
width: `${progress}%`,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -3419,6 +3396,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const historyMessages = useMemo(() => historyData?.messages || [], [historyData?.messages]);
|
const historyMessages = useMemo(() => historyData?.messages || [], [historyData?.messages]);
|
||||||
|
const serverUserMessageCount = useMemo(
|
||||||
|
() => historyMessages.filter((message) => message.role === "user").length,
|
||||||
|
[historyMessages]
|
||||||
|
);
|
||||||
const referenceImages = workflowContext?.referenceImages || [];
|
const referenceImages = workflowContext?.referenceImages || [];
|
||||||
const resolvedAgentModeState = agentModeState || localAgentModeState;
|
const resolvedAgentModeState = agentModeState || localAgentModeState;
|
||||||
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
|
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
|
||||||
@ -5422,6 +5403,63 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
clearError();
|
clearError();
|
||||||
}, [chatError, clearError, pendingGeneration]);
|
}, [chatError, clearError, pendingGeneration]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!pendingGeneration ||
|
||||||
|
(pendingGeneration.modeType !== "detail_replacement" &&
|
||||||
|
pendingGeneration.modeType !== "sku_replacement")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestAccepted = isAgentGenerationRequestAccepted({
|
||||||
|
pending: pendingGeneration,
|
||||||
|
serverMessages: historyMessages,
|
||||||
|
});
|
||||||
|
if (requestAccepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMs = getAgentGenerationAcceptanceRemainingMs({
|
||||||
|
startedAt: pendingGeneration.timestamp,
|
||||||
|
timeoutMs: AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const clearUnacceptedGeneration = () => {
|
||||||
|
generationRequestSnapshotRef.current = {
|
||||||
|
hiddenPrompt: null,
|
||||||
|
replacementExecutionCardIds: null,
|
||||||
|
replacementRerunReferenceImages: null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
stop();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"[AgentGenerationWorkspaceDarkV2] Failed to stop unaccepted generation request:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setPendingGeneration((current) => {
|
||||||
|
if (
|
||||||
|
!current ||
|
||||||
|
current.chatId !== pendingGeneration.chatId ||
|
||||||
|
current.timestamp !== pendingGeneration.timestamp
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
toast.error("生成请求未被服务器接收,已结束处理中状态,请重新提交。");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
clearUnacceptedGeneration();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerId = window.setTimeout(clearUnacceptedGeneration, remainingMs);
|
||||||
|
return () => window.clearTimeout(timerId);
|
||||||
|
}, [historyMessages, pendingGeneration, serverUserMessageCount, stop]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pendingGeneration) {
|
if (!pendingGeneration) {
|
||||||
return;
|
return;
|
||||||
@ -6353,6 +6391,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
);
|
);
|
||||||
setActiveResultModeType(currentResultModeType);
|
setActiveResultModeType(currentResultModeType);
|
||||||
setSelectedResultUrl(null);
|
setSelectedResultUrl(null);
|
||||||
|
const requestMessageId = generateUUID();
|
||||||
setPendingGeneration({
|
setPendingGeneration({
|
||||||
chatId,
|
chatId,
|
||||||
modeType: currentResultModeType,
|
modeType: currentResultModeType,
|
||||||
@ -6365,6 +6404,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
? detailReplacementReferenceCards.length
|
? detailReplacementReferenceCards.length
|
||||||
: skuReplacementReferenceCards.length,
|
: skuReplacementReferenceCards.length,
|
||||||
userTurnCountAtStart: userMessageCount,
|
userTurnCountAtStart: userMessageCount,
|
||||||
|
serverUserTurnCountAtStart: serverUserMessageCount,
|
||||||
|
requestMessageId,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
requestedCardIds:
|
requestedCardIds:
|
||||||
replacementModeType && replacementExecutionCards.length > 0
|
replacementModeType && replacementExecutionCards.length > 0
|
||||||
@ -6375,7 +6416,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
toast.success(`继续处理剩余 ${replacementExecutionCards.length} 张结果图`);
|
toast.success(`继续处理剩余 ${replacementExecutionCards.length} 张结果图`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
void Promise.resolve(sendMessage({ role: "user", parts })).catch((error) => {
|
void Promise.resolve(
|
||||||
|
sendMessage({ id: requestMessageId, role: "user", parts })
|
||||||
|
).catch((error) => {
|
||||||
generationRequestSnapshotRef.current = {
|
generationRequestSnapshotRef.current = {
|
||||||
hiddenPrompt: null,
|
hiddenPrompt: null,
|
||||||
replacementExecutionCardIds: null,
|
replacementExecutionCardIds: null,
|
||||||
@ -6421,6 +6464,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
selectedAgentPresetId,
|
selectedAgentPresetId,
|
||||||
selectedModelId,
|
selectedModelId,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
serverUserMessageCount,
|
||||||
setPendingGeneration,
|
setPendingGeneration,
|
||||||
skuReplacementReferenceCards.length,
|
skuReplacementReferenceCards.length,
|
||||||
skuResultCards,
|
skuResultCards,
|
||||||
|
|||||||
48
lib/agents/pending-generation-guard.ts
Normal file
48
lib/agents/pending-generation-guard.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export const AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS = 60 * 1000;
|
||||||
|
|
||||||
|
interface PendingGenerationAcceptanceState {
|
||||||
|
requestMessageId?: string | null;
|
||||||
|
serverUserTurnCountAtStart?: number;
|
||||||
|
userTurnCountAtStart: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerMessageIdentity {
|
||||||
|
id: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAgentGenerationRequestAccepted(params: {
|
||||||
|
pending: PendingGenerationAcceptanceState;
|
||||||
|
serverMessages: ServerMessageIdentity[];
|
||||||
|
}) {
|
||||||
|
const requestMessageId = String(
|
||||||
|
params.pending.requestMessageId || ""
|
||||||
|
).trim();
|
||||||
|
if (requestMessageId) {
|
||||||
|
return params.serverMessages.some(
|
||||||
|
(message) =>
|
||||||
|
message.role === "user" && message.id === requestMessageId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUserMessageCount = params.serverMessages.filter(
|
||||||
|
(message) => message.role === "user"
|
||||||
|
).length;
|
||||||
|
const baseline =
|
||||||
|
params.pending.serverUserTurnCountAtStart ??
|
||||||
|
params.pending.userTurnCountAtStart;
|
||||||
|
return serverUserMessageCount > baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentGenerationAcceptanceRemainingMs(params: {
|
||||||
|
startedAt: number;
|
||||||
|
now?: number;
|
||||||
|
timeoutMs?: number;
|
||||||
|
}) {
|
||||||
|
const now = params.now ?? Date.now();
|
||||||
|
const timeoutMs = Math.max(
|
||||||
|
0,
|
||||||
|
params.timeoutMs ?? AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS
|
||||||
|
);
|
||||||
|
return Math.max(0, timeoutMs - Math.max(0, now - params.startedAt));
|
||||||
|
}
|
||||||
@ -33,6 +33,13 @@ type CanvasImageTaskRequest =
|
|||||||
| CanvasLayoutReplaceTaskRequest;
|
| CanvasLayoutReplaceTaskRequest;
|
||||||
type CanvasImageTaskResult = CanvasGenerateImageResult | StyleProTaskResult;
|
type CanvasImageTaskResult = CanvasGenerateImageResult | StyleProTaskResult;
|
||||||
|
|
||||||
|
export type CanvasGenerateImageTaskUpdate = {
|
||||||
|
taskId: string;
|
||||||
|
status: CanvasImageTaskStatus;
|
||||||
|
queueStatus: CanvasImageTaskQueueStatus;
|
||||||
|
error?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type CanvasImageTaskQueueStatus = {
|
export type CanvasImageTaskQueueStatus = {
|
||||||
status: CanvasImageTaskStatus;
|
status: CanvasImageTaskStatus;
|
||||||
queuePosition: number;
|
queuePosition: number;
|
||||||
@ -578,6 +585,105 @@ export async function createCanvasImageTask(params: {
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCanvasGenerateImageResult(
|
||||||
|
value: unknown
|
||||||
|
): value is CanvasGenerateImageResult {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = value as Partial<CanvasGenerateImageResult>;
|
||||||
|
return (
|
||||||
|
Array.isArray(result.images) &&
|
||||||
|
result.images.length > 0 &&
|
||||||
|
result.images.every(
|
||||||
|
(image) =>
|
||||||
|
Boolean(image) &&
|
||||||
|
typeof image.originalUrl === "string" &&
|
||||||
|
typeof image.thumbnailUrl === "string"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateCanvasImageWithPersistentQueue(params: {
|
||||||
|
userId: string;
|
||||||
|
request: CanvasGenerateImageRequest;
|
||||||
|
timeoutMs?: number;
|
||||||
|
onTaskUpdate?: (
|
||||||
|
update: CanvasGenerateImageTaskUpdate
|
||||||
|
) => void | Promise<void>;
|
||||||
|
}): Promise<CanvasGenerateImageResult> {
|
||||||
|
const {
|
||||||
|
onQueueStatusChange,
|
||||||
|
ownerKey: _requestOwnerKey,
|
||||||
|
...serializableRequest
|
||||||
|
} = params.request;
|
||||||
|
const task = await createCanvasImageTask({
|
||||||
|
userId: params.userId,
|
||||||
|
request: serializableRequest as unknown as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
await enqueueCanvasImageTask(task.id);
|
||||||
|
|
||||||
|
const timeoutMs = Math.max(60_000, params.timeoutMs ?? 30 * 60 * 1000);
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let lastStatusSignature = "";
|
||||||
|
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const currentTask = await getCanvasImageTaskForUser({
|
||||||
|
taskId: task.id,
|
||||||
|
userId: params.userId,
|
||||||
|
});
|
||||||
|
if (!currentTask) {
|
||||||
|
throw new Error(`图片生成任务不存在(${task.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = currentTask.status as CanvasImageTaskStatus;
|
||||||
|
const queueStatus = currentTask.queueStatus;
|
||||||
|
const statusSignature = [
|
||||||
|
status,
|
||||||
|
queueStatus.queuePosition,
|
||||||
|
queueStatus.sameModelPendingCount,
|
||||||
|
queueStatus.sameModelRunningCount,
|
||||||
|
queueStatus.concurrencyLimit,
|
||||||
|
].join(":");
|
||||||
|
if (statusSignature !== lastStatusSignature) {
|
||||||
|
lastStatusSignature = statusSignature;
|
||||||
|
await params.onTaskUpdate?.({
|
||||||
|
taskId: task.id,
|
||||||
|
status,
|
||||||
|
queueStatus,
|
||||||
|
error: currentTask.error,
|
||||||
|
});
|
||||||
|
if (status === "pending" || status === "running") {
|
||||||
|
await onQueueStatusChange?.({
|
||||||
|
...queueStatus,
|
||||||
|
modelName: serializableRequest.model || null,
|
||||||
|
modelRef: serializableRequest.model || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "succeeded") {
|
||||||
|
if (!isCanvasGenerateImageResult(currentTask.result)) {
|
||||||
|
throw new Error(`图片生成任务已完成但结果无效(${task.id})`);
|
||||||
|
}
|
||||||
|
return currentTask.result;
|
||||||
|
}
|
||||||
|
if (status === "failed") {
|
||||||
|
throw new Error(currentTask.error || "图片生成任务失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedWaitMs = Number(queueStatus.estimatedWaitMs || 0);
|
||||||
|
const pollAfterMs =
|
||||||
|
status === "pending" && estimatedWaitMs > 0
|
||||||
|
? Math.min(4000, Math.max(1000, estimatedWaitMs))
|
||||||
|
: 1000;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollAfterMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`图片生成任务等待超时(${task.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
async function estimateModelQueueWaitMs(params: {
|
async function estimateModelQueueWaitMs(params: {
|
||||||
modelKey: string;
|
modelKey: string;
|
||||||
sameModelRunningCount: number;
|
sameModelRunningCount: number;
|
||||||
|
|||||||
66
scripts/test-agent-generation-pending-guard.ts
Normal file
66
scripts/test-agent-generation-pending-guard.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS,
|
||||||
|
getAgentGenerationAcceptanceRemainingMs,
|
||||||
|
isAgentGenerationRequestAccepted,
|
||||||
|
} from "@/lib/agents/pending-generation-guard";
|
||||||
|
|
||||||
|
function expect(condition: boolean, message: string) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = {
|
||||||
|
requestMessageId: "request-2",
|
||||||
|
serverUserTurnCountAtStart: 1,
|
||||||
|
userTurnCountAtStart: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
!isAgentGenerationRequestAccepted({
|
||||||
|
pending,
|
||||||
|
serverMessages: [{ id: "request-1", role: "user" }],
|
||||||
|
}),
|
||||||
|
"an unrelated persisted user message must not acknowledge the request"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isAgentGenerationRequestAccepted({
|
||||||
|
pending,
|
||||||
|
serverMessages: [
|
||||||
|
{ id: "request-1", role: "user" },
|
||||||
|
{ id: "request-2", role: "user" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"the exact persisted request message must acknowledge the request"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isAgentGenerationRequestAccepted({
|
||||||
|
pending: {
|
||||||
|
requestMessageId: null,
|
||||||
|
serverUserTurnCountAtStart: 1,
|
||||||
|
userTurnCountAtStart: 1,
|
||||||
|
},
|
||||||
|
serverMessages: [
|
||||||
|
{ id: "request-1", role: "user" },
|
||||||
|
{ id: "legacy-request", role: "user" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"legacy pending state must recover by persisted user message count"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getAgentGenerationAcceptanceRemainingMs({
|
||||||
|
startedAt: 1_000,
|
||||||
|
now: 31_000,
|
||||||
|
}) ===
|
||||||
|
AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS - 30_000,
|
||||||
|
"acceptance timeout must retain the expected remaining duration"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getAgentGenerationAcceptanceRemainingMs({
|
||||||
|
startedAt: 1_000,
|
||||||
|
now: 91_000,
|
||||||
|
}) === 0,
|
||||||
|
"expired acceptance timeout must clamp to zero"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("agent generation pending guard tests passed");
|
||||||
Loading…
x
Reference in New Issue
Block a user