86 lines
3.2 KiB
JavaScript
86 lines
3.2 KiB
JavaScript
|
|
/**
|
|||
|
|
* 共享视频轮询重试工具
|
|||
|
|
*
|
|||
|
|
* 提供 pollWithRetry 工厂函数,供 kling/veo/grok 三个视频生成器共用。
|
|||
|
|
* 两层重试:轮询级(同一 taskId,处理网络瞬断)→ 任务级(创建新 task + 优化提示词)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const TRANSIENT_RE = /timeout|ECONNRESET|ETIMEDOUT|network|socket/i
|
|||
|
|
|
|||
|
|
const POLL_RETRIES = 2 // 同一 task 轮询重试次数
|
|||
|
|
const POLL_RETRY_DELAY = 5000 // 轮询重试间隔 ms
|
|||
|
|
const TASK_RETRY_DELAY = 5000 // 任务级重试间隔 ms
|
|||
|
|
|
|||
|
|
function isTransientError(err) {
|
|||
|
|
return TRANSIENT_RE.test(err.message || '')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建 pollWithRetry 函数
|
|||
|
|
*
|
|||
|
|
* @param {object} opts
|
|||
|
|
* @param {object} opts.Api - 有 create() 和 poll() 方法的 API 对象
|
|||
|
|
* @param {string} opts.suffix - 输出文件后缀(如 '_kling')
|
|||
|
|
* @param {number} opts.duration - 视频时长(秒)
|
|||
|
|
* @param {number} [opts.maxRetries=3] - 任务级最大重试次数
|
|||
|
|
* @param {function} [opts.optimizePrompt] - 提示词优化函数 (prompt, failReason, attempt) => optimizedPrompt
|
|||
|
|
* @param {function} opts.buildCreateOpts - (item_options) => create() 的第三个参数
|
|||
|
|
* @returns {function} pollWithRetry(taskId, prompt, options)
|
|||
|
|
*/
|
|||
|
|
function makePollWithRetry({ Api, suffix, duration, maxRetries = 3, optimizePrompt, buildCreateOpts }) {
|
|||
|
|
return async function pollWithRetry(taskId, prompt, options = {}) {
|
|||
|
|
let currentTaskId = taskId
|
|||
|
|
let currentPrompt = prompt
|
|||
|
|
let lastError = null
|
|||
|
|
|
|||
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|||
|
|
if (attempt > 0) {
|
|||
|
|
if (optimizePrompt) {
|
|||
|
|
currentPrompt = optimizePrompt(prompt, lastError, attempt)
|
|||
|
|
}
|
|||
|
|
console.log(`\n 🔄 重试 (任务 ${currentTaskId.substring(0, 8)}...): ${currentPrompt.substring(0, 50)}`)
|
|||
|
|
const createOpts = buildCreateOpts(options)
|
|||
|
|
currentTaskId = await Api.create(options.imageUrl || '', currentPrompt, createOpts)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const outputDir = options.outputDir || './output'
|
|||
|
|
|
|||
|
|
for (let pollAttempt = 0; pollAttempt <= POLL_RETRIES; pollAttempt++) {
|
|||
|
|
try {
|
|||
|
|
const result = await Api.poll(currentTaskId)
|
|||
|
|
|
|||
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|||
|
|
const videoFile = path.join(outputDir, `${timestamp}${suffix}.mp4`)
|
|||
|
|
await download(result.videoUrl, videoFile)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
taskId: currentTaskId,
|
|||
|
|
prompt: currentPrompt,
|
|||
|
|
originalPrompt: prompt,
|
|||
|
|
attempts: attempt + 1,
|
|||
|
|
file: videoFile,
|
|||
|
|
files: [videoFile],
|
|||
|
|
duration,
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
lastError = err.message
|
|||
|
|
if (isTransientError(err) && pollAttempt < POLL_RETRIES) {
|
|||
|
|
console.log(` ⚠ 轮询瞬断 (${pollAttempt + 1}/${POLL_RETRIES}): ${err.message.slice(0, 60)}`)
|
|||
|
|
await new Promise(r => setTimeout(r, POLL_RETRY_DELAY))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (attempt < maxRetries) {
|
|||
|
|
await new Promise(r => setTimeout(r, TASK_RETRY_DELAY))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new Error(`重试 ${maxRetries} 次后仍失败: ${lastError}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = { makePollWithRetry, POLL_RETRIES, POLL_RETRY_DELAY, TASK_RETRY_DELAY }
|