diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index f8e9383ef..9d6acf874 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -850,9 +850,21 @@ export default function CanvasModel3DPage() { ); const pollAttemptsRef = useRef(0); + const pollErrorCountRef = useRef(0); const multiViewSlotsRef = useRef([]); const batchImageSlotsRef = useRef([]); + const MAX_RETRYABLE_TASK_POLL_FAILURES = 12; + + const isRetryableTaskPollFailure = (message: string) => { + const normalized = String(message || "").toLowerCase(); + return ( + /http[^0-9]*5\d\d/.test(normalized) || + normalized.includes("超时") || + normalized.includes("timeout") + ); + }; + // Fetch site settings for Logo const { data: siteSettingsData } = useSWR( "/api/site-settings?scope=branding", @@ -1434,6 +1446,33 @@ export default function CanvasModel3DPage() { }); if (!result.success || !result.data) { const failMessage = result.error || "任务查询失败"; + + if (isRetryableTaskPollFailure(failMessage)) { + pollErrorCountRef.current += 1; + if (pollErrorCountRef.current === 1) { + console.warn("[Model3DPage] 任务查询临时异常,继续重试", { + taskId: currentTask.taskId, + provider: currentTask.provider, + error: failMessage, + attempt: pollAttemptsRef.current, + }); + toast.info(`任务查询异常,正在重试...`); + } + + if (pollErrorCountRef.current >= MAX_RETRYABLE_TASK_POLL_FAILURES) { + const failedRecord: TaskRecord = { + ...currentTask, + status: "failed", + error: `${failMessage}(重试仍异常)`, + }; + setCurrentTask(failedRecord); + upsertTaskHistory(failedRecord); + } + + return; + } + + pollErrorCountRef.current = 0; const failedRecord: TaskRecord = { ...currentTask, status: "failed", @@ -1479,6 +1518,8 @@ export default function CanvasModel3DPage() { error: status === "failed" ? result.data.errorMessage || "任务失败" : null, }; + + pollErrorCountRef.current = 0; if (taskRecordChanged(currentTask, nextRecord)) { setCurrentTask(nextRecord); upsertTaskHistory(nextRecord);