Files
video-create/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js
sion123 06f44ddafa refactor(video-from-script): 提取轮询重试逻辑为共享工具
提取三个视频生成器中重复的 `pollWithRetry` 函数到共享模块 `video-poll-utils`,消除代码重复。新增两层重试机制:轮询级(处理网络瞬断)和任务级(创建新任务 + 提示词优化)。同时优化 `phase-videos` 中视频状态管理和 manifest 保存逻辑。
2026-05-12 01:24:55 +08:00

86 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 共享视频轮询重试工具
*
* 提供 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 }