From 827164ed706a46ccb2991ba46c79836286592450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 12:13:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BA=E5=9B=BE=E7=89=87=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E8=A1=A5=E5=9B=9E=E5=90=8C=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=9E=AC=E6=97=B6=E9=94=99=E8=AF=AF=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 两个脚本新增 fetchJsonWithRetry:对 408/429/5xx 按 1s/2s 退避重试同一 concrete model,最多 3 次,不引入跨模型回退 - requestImagePayload 改用带 requestId 的重试入口,单次 408/429/5xx 不再中断整批生成 - 删除只写不读的 providerBody / providerResponseParse 错误字段,解析失败与缺图改为把响应片段写进错误信息 --- .../generate-anthro-cat-illustrations.mjs | 46 +++++++++++++++---- .../scripts/generate-template-samples.mjs | 46 +++++++++++++++---- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs index 5c2bd1aae..da0310e08 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -223,6 +223,15 @@ function inferExtensionFromBytes(bytes) { return null; } +// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由 +// server-rs 负责,脚本不做任何模型替换。 +const transientRetryMaxAttempts = 3; +const transientRetryBaseDelayMs = 1000; + +function isTransientProviderStatus(status) { + return status === 408 || status === 429 || status >= 500; +} + async function fetchJson(url, options, timeoutMs) { const abortController = new AbortController(); const timer = setTimeout(() => abortController.abort(), timeoutMs); @@ -237,15 +246,14 @@ async function fetchJson(url, options, timeoutMs) { `Tiantoken ${response.status}: ${text.slice(0, 600)}`, ); error.providerStatus = response.status; - error.providerBody = text; throw error; } try { return JSON.parse(text); } catch (error) { - error.providerResponseParse = true; - error.providerBody = text; - throw error; + throw new Error( + `Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`, + ); } } catch (error) { if (error?.name === 'AbortError') { @@ -257,6 +265,26 @@ async function fetchJson(url, options, timeoutMs) { } } +async function fetchJsonWithRetry(url, options, timeoutMs, requestId) { + for (let attempt = 1; ; attempt += 1) { + try { + return await fetchJson(url, options, timeoutMs); + } catch (error) { + if ( + attempt >= transientRetryMaxAttempts || + !isTransientProviderStatus(error?.providerStatus) + ) { + throw error; + } + const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1); + console.warn( + `Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`, + ); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + async function requestImagePayload(env, entry) { const model = imageModel; const requestBody = { @@ -265,7 +293,7 @@ async function requestImagePayload(env, entry) { n: 1, size: '1024x1024', }; - const payload = await fetchJson( + const payload = await fetchJsonWithRetry( buildTiantokenImagesGenerationUrl(env.baseUrl), { method: 'POST', @@ -277,15 +305,15 @@ async function requestImagePayload(env, entry) { body: JSON.stringify(requestBody), }, env.timeoutMs, + entry.id, ); const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]); if (extractImageUrls(payload)[0] || base64Image) { return payload; } - const error = new Error(`Tiantoken returned no image for ${entry.id}`); - error.providerResponseParse = true; - error.providerBody = JSON.stringify(payload).slice(0, 600); - throw error; + throw new Error( + `Tiantoken returned no image for ${entry.id}:${JSON.stringify(payload).slice(0, 600)}`, + ); } async function downloadUrl(url, timeoutMs) { diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index 5b23bc70e..e37683cd8 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -193,6 +193,15 @@ function inferExtensionFromBytes(bytes) { return null; } +// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由 +// server-rs 负责,脚本不做任何模型替换。 +const transientRetryMaxAttempts = 3; +const transientRetryBaseDelayMs = 1000; + +function isTransientProviderStatus(status) { + return status === 408 || status === 429 || status >= 500; +} + async function fetchJson(url, options, timeoutMs) { const abortController = new AbortController(); const timer = setTimeout(() => abortController.abort(), timeoutMs); @@ -207,15 +216,14 @@ async function fetchJson(url, options, timeoutMs) { `Tiantoken ${response.status}: ${text.slice(0, 600)}`, ); error.providerStatus = response.status; - error.providerBody = text; throw error; } try { return JSON.parse(text); } catch (error) { - error.providerResponseParse = true; - error.providerBody = text; - throw error; + throw new Error( + `Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`, + ); } } catch (error) { if (error?.name === 'AbortError') { @@ -227,6 +235,26 @@ async function fetchJson(url, options, timeoutMs) { } } +async function fetchJsonWithRetry(url, options, timeoutMs, requestId) { + for (let attempt = 1; ; attempt += 1) { + try { + return await fetchJson(url, options, timeoutMs); + } catch (error) { + if ( + attempt >= transientRetryMaxAttempts || + !isTransientProviderStatus(error?.providerStatus) + ) { + throw error; + } + const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1); + console.warn( + `Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`, + ); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + async function requestImagePayload(env, template) { const model = imageModel; const requestBody = { @@ -235,7 +263,7 @@ async function requestImagePayload(env, template) { n: 1, size: '1024x1024', }; - const payload = await fetchJson( + const payload = await fetchJsonWithRetry( buildTiantokenImagesGenerationUrl(env.baseUrl), { method: 'POST', @@ -247,15 +275,15 @@ async function requestImagePayload(env, template) { body: JSON.stringify(requestBody), }, env.timeoutMs, + template.id, ); const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]); if (extractImageUrls(payload)[0] || base64Image) { return payload; } - const error = new Error(`Tiantoken returned no image for ${template.id}`); - error.providerResponseParse = true; - error.providerBody = JSON.stringify(payload).slice(0, 600); - throw error; + throw new Error( + `Tiantoken returned no image for ${template.id}:${JSON.stringify(payload).slice(0, 600)}`, + ); } async function downloadUrl(url, timeoutMs) {