Files
video-create/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js

111 lines
4.0 KiB
JavaScript
Raw Normal View History

/**
* 共享视频轮询重试工具
*
* 提供 pollWithRetry 工厂函数 kling/veo/grok 三个视频生成器共用
* 两层重试轮询级同一 taskId处理网络瞬断 任务级创建新 task + 优化提示词
*/
const path = require('path')
const fs = require('fs')
const https = require('https')
const http = require('http')
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 || '')
}
async function download(url, outputPath) {
const protocol = url.startsWith('https') ? https : http
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(outputPath)
protocol.get(url, (response) => {
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
file.close()
fs.unlinkSync(outputPath)
return download(response.headers.location, outputPath).then(resolve).catch(reject)
}
response.pipe(file)
file.on('finish', () => { file.close(); resolve(outputPath) })
}).on('error', (err) => {
file.close()
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)
reject(err)
})
})
}
/**
* 创建 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 }