Files
video-create/.claude/skills/video-from-script/scripts/lib/video-poll-utils.js
lc 8787d369d3 feat(video-from-script): 批量生产元数据增强 — 选题/转发文案/草稿命名/导出/草稿箱改名
- batch-pipeline.js: 新增 mark 元数据字段(topicA/B, draftName, forwardCopy, hashtags)
- batch-pipeline.js: 新增 export 命令导出 CSV/XLSX 最终表格
- batch-pipeline.js: 新增 rename-drafts 命令批量重命名剪映草稿(Mac 直接 mv 文件夹)
- batch-pipeline.js: 完善 displayTitle 向后兼容旧 topic 字段
- lib/phase-tts: 增强 TTS 生成稳定性
- lib/phase-videos: 视频生成优化
- lib/video-poll-utils: 提取轮询重试共享工具
- CLAUDE.md: 补充批量生产选题/转发文案/草稿命名/导出/草稿箱改名文档
- 执黑先行 account.json: 配置更新

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 23:16:12 +08:00

111 lines
4.0 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 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 }