fix(canvas): preserve uncertain image submissions without paid failover

This commit is contained in:
134 2026-10-03 17:29:31 +08:00
parent c45a6a77a0
commit 96d67955cb
6 changed files with 23 additions and 4 deletions

View File

@ -1,6 +1,7 @@
import { mapVip98kVideoParameters } from "../vip98k-video-parameters";
import {
ProviderTaskFailedError,
ProviderTaskUncertainError,
type ProviderTaskLifecycle,
} from "@/lib/video-canvas/video-task-lifecycle";
import "server-only";
@ -1113,7 +1114,7 @@ async function submitTask(params: {
submissionTimeoutMs: params.submissionTimeoutMs,
message: error instanceof Error ? error.message : String(error),
});
throw error;
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${error instanceof Error ? error.message : String(error)}`);
}
const retryDelayMs = attempt * params.submissionRetryBaseDelayMs;
logger.warn("provider submission transport failed; retrying idempotently", {
@ -1192,6 +1193,10 @@ async function submitTask(params: {
await delay(reconciliationRetryDelayMs);
continue;
}
if ((response.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(errorMessage))
|| [502, 504].includes(response.status)
|| /pending\s+safe\s+reconciliation|request\s+outcome\s+(?:is\s+)?unknown|final\s+outcome\s+could\s+not\s+be\s+confirmed/i.test(errorMessage))
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${errorMessage} (HTTP ${response.status})`);
throw new Error(`${errorMessage} (HTTP ${response.status})`);
}

View File

@ -100,7 +100,8 @@ export function getVip98kSubmissionRetryDelay(params: {
maxAttempts: number;
retryAfterMs?: number | null;
}) {
const recoverableGatewayFailure = params.status === 502 || params.status === 504;
const inProgress = params.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(params.message);
const recoverableGatewayFailure = params.status === 502 || params.status === 504 || inProgress;
const recoverableSubmission =
/pending\s+safe\s+reconciliation/i.test(params.message) ||
/request\s+outcome\s+(?:is\s+)?unknown/i.test(params.message) ||

View File

@ -1,4 +1,5 @@
import "server-only";
import { ProviderTaskUncertainError } from "@/lib/video-canvas/video-task-lifecycle";
import { NextResponse } from "next/server";
import {
createCanvasImageTask,
@ -120,8 +121,9 @@ export async function executeCanvasImageResponse(body: GenerateImageRequest, use
} catch (error) {
console.error("[Canvas Generate Image] Error:", error);
return NextResponse.json(
{ success: false, error: getUserFacingImageGenerationErrorMessage(error) },
{ status: 500 },
{ success: false, error: getUserFacingImageGenerationErrorMessage(error),
...(error instanceof ProviderTaskUncertainError ? { code: "PROVIDER_TASK_UNCERTAIN" } : {}) },
{ status: error instanceof ProviderTaskUncertainError ? 503 : 500 },
);
}
}

View File

@ -1,4 +1,5 @@
import "server-only";
import { ProviderTaskUncertainError } from "@/lib/video-canvas/video-task-lifecycle";
import sharp from "sharp";
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
@ -5623,6 +5624,9 @@ export async function generateCanvasImage(
await slot.release();
}
// An accepted or uncertain request must not start another paid model/variant.
if (caughtError instanceof ProviderTaskUncertainError) throw caughtError;
const retryVariants = Array.isArray(request.retryVariants)
? request.retryVariants
: [];

View File

@ -159,6 +159,7 @@ async function execute(task: CanvasGenerationTask, userId: string) {
)
: await executeWorkflowAnalysisResponse(params);
const body = await response.json();
if (body.code === "PROVIDER_TASK_UNCERTAIN") throw new ProviderTaskUncertainError(body.error);
if (!response.ok || !body.success) throw Error(body.error || "任务执行失败");
if (
request.kind === "image" &&

View File

@ -232,6 +232,12 @@ test.describe("VIP98K reference-image routing", () => {
});
test("retries provider reconciliation without treating it as a media failure", () => {
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "A request with this idempotency key is still in progress.",
attempt: 1, maxAttempts: 3 })).toBe(15_000);
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "Conflicting request parameters",
attempt: 1, maxAttempts: 3 })).toBeNull();
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "A request with this idempotency key is still in progress.",
attempt: 3, maxAttempts: 3 })).toBeNull();
expect(
getVip98kSubmissionRetryDelay({
status: 503,