feat(video-from-script): 区分视频生成超时与任务失败,保留 taskId 支持恢复
核心改动:将超时与任务失败明确区分,超时场景保留 taskId 和 pending 状态, 等待下次恢复,避免重复计费;仅 API 明确返回 failed 时才清理 taskId 并重试。 - phase-videos.js: 恢复失败分超时/失败处理,超时保留 taskId 置 pending - phase-videos.js: 轮询结果超时也保留 taskId 存 pending - video-poll-utils.js: 增加 isTaskFailed 判断;超时禁止创建新任务 - video-poll-utils.js: 提升轮询重试次数至 5 次以允许超时等待
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
* 共享视频轮询重试工具
|
||||
*
|
||||
* 提供 pollWithRetry 工厂函数,供 kling/veo/grok 三个视频生成器共用。
|
||||
* 两层重试:轮询级(同一 taskId,处理网络瞬断)→ 任务级(创建新 task + 优化提示词)
|
||||
* 两层重试:轮询级(同一 taskId,处理网络瞬断/超时)→ 任务级(仅 API 明确返回 failed 才创建新 task)
|
||||
*
|
||||
* 铁律:超时 ≠ 失败。超时说明服务端还在跑,创建新任务会重复计费。
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
@@ -10,9 +12,11 @@ const fs = require('fs')
|
||||
const https = require('https')
|
||||
const http = require('http')
|
||||
|
||||
const TRANSIENT_RE = /timeout|ECONNRESET|ETIMEDOUT|network|socket/i
|
||||
const TRANSIENT_RE = /timeout|ECONNRESET|ETIMEDOUT|network|socket|超时|processing|pending/i
|
||||
|
||||
const POLL_RETRIES = 2 // 同一 task 轮询重试次数
|
||||
const TASK_FAILED_RE = /\bfailed\b|失败/i
|
||||
|
||||
const POLL_RETRIES = 5 // 同一 task 轮询重试次数(含超时等待)
|
||||
const POLL_RETRY_DELAY = 5000 // 轮询重试间隔 ms
|
||||
const TASK_RETRY_DELAY = 5000 // 任务级重试间隔 ms
|
||||
|
||||
@@ -20,6 +24,10 @@ function isTransientError(err) {
|
||||
return TRANSIENT_RE.test(err.message || '')
|
||||
}
|
||||
|
||||
function isTaskFailed(err) {
|
||||
return TASK_FAILED_RE.test(err.message || '')
|
||||
}
|
||||
|
||||
async function download(url, outputPath) {
|
||||
const protocol = url.startsWith('https') ? https : http
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -57,9 +65,16 @@ function makePollWithRetry({ Api, suffix, duration, maxRetries = 3, optimizeProm
|
||||
let currentTaskId = taskId
|
||||
let currentPrompt = prompt
|
||||
let lastError = null
|
||||
let lastErrorWasTaskFailure = false
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
if (attempt > 0) {
|
||||
// 只有 API 明确返回 failed 才创建新任务。超时/网络问题禁止创建新任务(原任务仍在服务端运行,重复计费)
|
||||
if (!lastErrorWasTaskFailure) {
|
||||
const err = new Error(`视频生成超时,放弃等待: ${lastError}`)
|
||||
err.isTaskFailure = false
|
||||
throw err
|
||||
}
|
||||
if (optimizePrompt) {
|
||||
currentPrompt = optimizePrompt(prompt, lastError, attempt)
|
||||
}
|
||||
@@ -89,8 +104,10 @@ function makePollWithRetry({ Api, suffix, duration, maxRetries = 3, optimizeProm
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err.message
|
||||
lastErrorWasTaskFailure = isTaskFailed(err)
|
||||
if (isTransientError(err) && pollAttempt < POLL_RETRIES) {
|
||||
console.log(` ⚠ 轮询瞬断 (${pollAttempt + 1}/${POLL_RETRIES}): ${err.message.slice(0, 60)}`)
|
||||
const tag = isTaskFailed(err) ? '失败' : '超时/瞬断'
|
||||
console.log(` ⚠ 轮询${tag} (${pollAttempt + 1}/${POLL_RETRIES}): ${err.message.slice(0, 60)}`)
|
||||
await new Promise(r => setTimeout(r, POLL_RETRY_DELAY))
|
||||
continue
|
||||
}
|
||||
@@ -98,13 +115,15 @@ function makePollWithRetry({ Api, suffix, duration, maxRetries = 3, optimizeProm
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
if (attempt < maxRetries && lastErrorWasTaskFailure) {
|
||||
await new Promise(r => setTimeout(r, TASK_RETRY_DELAY))
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`重试 ${maxRetries} 次后仍失败: ${lastError}`)
|
||||
const finalErr = new Error(`重试 ${maxRetries} 次后仍失败: ${lastError}`)
|
||||
finalErr.isTaskFailure = true
|
||||
throw finalErr
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { makePollWithRetry, POLL_RETRIES, POLL_RETRY_DELAY, TASK_RETRY_DELAY }
|
||||
module.exports = { makePollWithRetry, isTaskFailed, POLL_RETRIES, POLL_RETRY_DELAY, TASK_RETRY_DELAY }
|
||||
|
||||
Reference in New Issue
Block a user