fix(agent): persist detail replacement tasks
This commit is contained in:
parent
9093b0370b
commit
87bbcf0ccd
@ -138,7 +138,11 @@ import {
|
||||
generatedImage,
|
||||
message as messageTable,
|
||||
} 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 {
|
||||
detectRequestedStoryboardGrid,
|
||||
isGridConfirmationReply,
|
||||
@ -5992,44 +5996,55 @@ export async function POST(request: Request) {
|
||||
let normalizeElapsedMs: number | null = null;
|
||||
try {
|
||||
generationStartedAt = Date.now();
|
||||
const result = await generateCanvasImage({
|
||||
prompt: replacementPrompt,
|
||||
negativePrompt: effectiveImageParams?.negativePrompt,
|
||||
model: resolvedModelRef,
|
||||
ownerKey: session.user.id,
|
||||
aspectRatio: generationAspectRatio,
|
||||
imageSize: resolvedImageSize,
|
||||
imageCount: 1,
|
||||
referenceImages: sourceImageUrls,
|
||||
debugLabel: "agent-detail-replacement",
|
||||
debugMeta: {
|
||||
referenceLabel: reference.label,
|
||||
referenceWidth: referenceGeometry?.width,
|
||||
referenceHeight: referenceGeometry?.height,
|
||||
referenceAspectRatio: referenceGeometry?.aspectRatio,
|
||||
const result = await generateCanvasImageWithPersistentQueue({
|
||||
userId: session.user.id,
|
||||
request: {
|
||||
prompt: replacementPrompt,
|
||||
negativePrompt: effectiveImageParams?.negativePrompt,
|
||||
model: resolvedModelRef,
|
||||
ownerKey: session.user.id,
|
||||
aspectRatio: generationAspectRatio,
|
||||
imageSize: resolvedImageSize,
|
||||
imageCount: 1,
|
||||
referenceImages: sourceImageUrls,
|
||||
debugLabel: "agent-detail-replacement",
|
||||
debugMeta: {
|
||||
operation: "agent-detail-replacement",
|
||||
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;
|
||||
resolvedModelDisplayName = result.model || resolvedModelDisplayName;
|
||||
@ -6879,7 +6894,7 @@ export async function POST(request: Request) {
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await generateCanvasImage({
|
||||
const generationRequest: CanvasGenerateImageRequest = {
|
||||
prompt: replacementPrompt,
|
||||
negativePrompt:
|
||||
typeof mergedImageParams.negativePrompt === "string"
|
||||
@ -6893,6 +6908,11 @@ export async function POST(request: Request) {
|
||||
referenceImages: sourceImageUrls,
|
||||
debugLabel: `agent-${replacementSourceType}`,
|
||||
debugMeta: {
|
||||
operation: `agent-${replacementSourceType}`,
|
||||
chatId: id,
|
||||
messageId: message.id,
|
||||
projectId: contextPayload?.projectId || null,
|
||||
referenceId: reference.id,
|
||||
referenceLabel: reference.label,
|
||||
referenceWidth: referenceGeometry?.width,
|
||||
referenceHeight: referenceGeometry?.height,
|
||||
@ -6921,8 +6941,14 @@ export async function POST(request: Request) {
|
||||
`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;
|
||||
const firstImage = result.images[0];
|
||||
if (!firstImage) {
|
||||
|
||||
@ -109,6 +109,11 @@ import {
|
||||
import {
|
||||
normalizeAgentPresetModeConfig,
|
||||
} from "@/lib/agents/preset-mode-config";
|
||||
import {
|
||||
AGENT_PENDING_SERVER_ACCEPTANCE_TIMEOUT_MS,
|
||||
getAgentGenerationAcceptanceRemainingMs,
|
||||
isAgentGenerationRequestAccepted,
|
||||
} from "@/lib/agents/pending-generation-guard";
|
||||
import {
|
||||
defaultAgentWorkflowBusinessModeSettings,
|
||||
normalizeAgentWorkflowBusinessModeSettings,
|
||||
@ -2574,6 +2579,8 @@ interface LocalGenerationPendingState {
|
||||
modeType: AgentGenerationResultModeType;
|
||||
expectedCount: number;
|
||||
userTurnCountAtStart: number;
|
||||
serverUserTurnCountAtStart?: number;
|
||||
requestMessageId?: string | null;
|
||||
timestamp: number;
|
||||
requestedCardIds?: string[] | null;
|
||||
}
|
||||
@ -2628,6 +2635,15 @@ function getStoredAgentPendingGenerations(): LocalGenerationPendingState[] {
|
||||
modeType: String(record.modeType || "").trim() as AgentGenerationResultModeType,
|
||||
expectedCount: Number(record.expectedCount),
|
||||
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),
|
||||
requestedCardIds: Array.isArray(record.requestedCardIds)
|
||||
? record.requestedCardIds
|
||||
@ -2660,16 +2676,6 @@ function saveStoredAgentPendingGenerations(items: LocalGenerationPendingState[])
|
||||
}
|
||||
}
|
||||
|
||||
const DETAIL_RESULT_PROCESSING_STAGES = [
|
||||
"解码图像",
|
||||
"定位选区",
|
||||
"分离主体",
|
||||
"重绘背景",
|
||||
"融合边缘",
|
||||
"检查细节",
|
||||
"输出结果",
|
||||
];
|
||||
|
||||
function DetailResultProcessingScanOverlay({
|
||||
imageUrl,
|
||||
label,
|
||||
@ -2677,29 +2683,6 @@ function DetailResultProcessingScanOverlay({
|
||||
imageUrl?: 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 (
|
||||
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#07090f]">
|
||||
<style>{`
|
||||
@ -2718,6 +2701,11 @@ function DetailResultProcessingScanOverlay({
|
||||
70% { box-shadow: 0 0 0 8px 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>
|
||||
{isHttpImageUrl(imageUrl) ? (
|
||||
<img
|
||||
@ -2790,24 +2778,13 @@ function DetailResultProcessingScanOverlay({
|
||||
style={{ animation: "detailResultScanPulseRing 1.8s ease-out infinite" }}
|
||||
/>
|
||||
<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 className="absolute inset-x-0 bottom-0 z-[60] h-0.5 bg-white/5">
|
||||
<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={{
|
||||
animation: "detailResultIndeterminate 1.8s ease-in-out infinite",
|
||||
background: "linear-gradient(90deg, #7dd3fc, #a78bfa)",
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -3419,6 +3396,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
|
||||
const historyMessages = useMemo(() => historyData?.messages || [], [historyData?.messages]);
|
||||
const serverUserMessageCount = useMemo(
|
||||
() => historyMessages.filter((message) => message.role === "user").length,
|
||||
[historyMessages]
|
||||
);
|
||||
const referenceImages = workflowContext?.referenceImages || [];
|
||||
const resolvedAgentModeState = agentModeState || localAgentModeState;
|
||||
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
|
||||
@ -5422,6 +5403,63 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
clearError();
|
||||
}, [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(() => {
|
||||
if (!pendingGeneration) {
|
||||
return;
|
||||
@ -6353,6 +6391,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
setActiveResultModeType(currentResultModeType);
|
||||
setSelectedResultUrl(null);
|
||||
const requestMessageId = generateUUID();
|
||||
setPendingGeneration({
|
||||
chatId,
|
||||
modeType: currentResultModeType,
|
||||
@ -6365,6 +6404,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
? detailReplacementReferenceCards.length
|
||||
: skuReplacementReferenceCards.length,
|
||||
userTurnCountAtStart: userMessageCount,
|
||||
serverUserTurnCountAtStart: serverUserMessageCount,
|
||||
requestMessageId,
|
||||
timestamp: Date.now(),
|
||||
requestedCardIds:
|
||||
replacementModeType && replacementExecutionCards.length > 0
|
||||
@ -6375,7 +6416,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
toast.success(`继续处理剩余 ${replacementExecutionCards.length} 张结果图`);
|
||||
}
|
||||
try {
|
||||
void Promise.resolve(sendMessage({ role: "user", parts })).catch((error) => {
|
||||
void Promise.resolve(
|
||||
sendMessage({ id: requestMessageId, role: "user", parts })
|
||||
).catch((error) => {
|
||||
generationRequestSnapshotRef.current = {
|
||||
hiddenPrompt: null,
|
||||
replacementExecutionCardIds: null,
|
||||
@ -6421,6 +6464,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
selectedAgentPresetId,
|
||||
selectedModelId,
|
||||
sendMessage,
|
||||
serverUserMessageCount,
|
||||
setPendingGeneration,
|
||||
skuReplacementReferenceCards.length,
|
||||
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;
|
||||
type CanvasImageTaskResult = CanvasGenerateImageResult | StyleProTaskResult;
|
||||
|
||||
export type CanvasGenerateImageTaskUpdate = {
|
||||
taskId: string;
|
||||
status: CanvasImageTaskStatus;
|
||||
queueStatus: CanvasImageTaskQueueStatus;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export type CanvasImageTaskQueueStatus = {
|
||||
status: CanvasImageTaskStatus;
|
||||
queuePosition: number;
|
||||
@ -578,6 +585,105 @@ export async function createCanvasImageTask(params: {
|
||||
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: {
|
||||
modelKey: string;
|
||||
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