2026-05-08 23:06:17 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量视频生产编排器
|
|
|
|
|
|
*
|
|
|
|
|
|
* 职责:读 Excel/CSV → 创建 batch-manifest → 管理 batch 状态
|
|
|
|
|
|
* 不负责:分镜、生图、生视频(由 AI Worker 子 Agent 承担)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:
|
|
|
|
|
|
* node batch-pipeline.js init --file <xlsx/csv> [--account <账号>] [--mode <模式>]
|
|
|
|
|
|
* node batch-pipeline.js status --file <batch-manifest.json>
|
|
|
|
|
|
* node batch-pipeline.js mark --file <batch-manifest.json> --row <N> --status <状态> [--manifest-path <path>] [--error <msg>]
|
|
|
|
|
|
* node batch-pipeline.js retry-failed --file <batch-manifest.json>
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs')
|
|
|
|
|
|
const path = require('path')
|
2026-05-08 23:53:37 +08:00
|
|
|
|
const { SKILLS_DIR, ACCOUNTS_DIR, loadConfig, resolveVoice } = require('./lib/pipeline-utils')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
|
|
|
|
|
|
// output/ 在项目根的父级(美图/output/)
|
|
|
|
|
|
const OUTPUT_BASE = path.join(SKILLS_DIR, '..', '..', '..', 'output')
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// CLI 参数解析
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function parseArgs(argv) {
|
|
|
|
|
|
const args = {}
|
|
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
|
|
|
|
if (argv[i] === '--file' && argv[i + 1]) args.file = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--account' && argv[i + 1]) args.account = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--mode' && argv[i + 1]) args.mode = argv[++i]
|
2026-05-08 23:53:37 +08:00
|
|
|
|
else if (argv[i] === '--voice' && argv[i + 1]) args.voice = argv[++i]
|
2026-05-08 23:06:17 +08:00
|
|
|
|
else if (argv[i] === '--row' && argv[i + 1]) args.row = parseInt(argv[++i])
|
|
|
|
|
|
else if (argv[i] === '--status' && argv[i + 1]) args.status = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--manifest-path' && argv[i + 1]) args.manifestPath = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--error' && argv[i + 1]) args.error = argv[++i]
|
2026-05-14 23:16:12 +08:00
|
|
|
|
else if (argv[i] === '--topic' && argv[i + 1]) args.topic = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--topic-a' && argv[i + 1]) args.topicA = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--topic-b' && argv[i + 1]) args.topicB = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--draft-name' && argv[i + 1]) args.draftName = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--forward-copy' && argv[i + 1]) args.forwardCopy = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--hashtags' && argv[i + 1]) args.hashtags = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--format' && argv[i + 1]) args.format = argv[++i]
|
|
|
|
|
|
else if (argv[i] === '--draft-dir' && argv[i + 1]) args.draftDir = argv[++i]
|
2026-05-15 12:04:51 +08:00
|
|
|
|
else if (argv[i] === '--draft-url' && argv[i + 1]) args.draftUrl = argv[++i]
|
2026-05-14 23:16:12 +08:00
|
|
|
|
else if (argv[i] === '--with-script') args.withScript = true
|
2026-05-08 23:06:17 +08:00
|
|
|
|
else if (!args.command) args.command = argv[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
return args
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// init: 读 Excel/CSV → batch-manifest.json + 提取脚本文件
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdInit(args) {
|
|
|
|
|
|
const filePath = path.resolve(args.file)
|
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
|
console.error(`文件不存在: ${filePath}`)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ext = path.extname(filePath).toLowerCase()
|
|
|
|
|
|
let rows
|
|
|
|
|
|
|
|
|
|
|
|
if (ext === '.csv') {
|
|
|
|
|
|
rows = parseCsv(filePath)
|
|
|
|
|
|
} else if (ext === '.xlsx' || ext === '.xls') {
|
|
|
|
|
|
rows = parseExcel(filePath)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error(`不支持的格式: ${ext},仅支持 .xlsx .xls .csv`)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rows.length === 0) {
|
|
|
|
|
|
console.error('表格为空(无数据行)')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 batch 输出目录
|
|
|
|
|
|
const dateStr = formatDate(new Date())
|
|
|
|
|
|
let seq = 1
|
|
|
|
|
|
while (fs.existsSync(path.join(OUTPUT_BASE, `batch_${dateStr}_${String(seq).padStart(3, '0')}`))) {
|
|
|
|
|
|
seq++
|
|
|
|
|
|
}
|
|
|
|
|
|
const batchDir = path.join(OUTPUT_BASE, `batch_${dateStr}_${String(seq).padStart(3, '0')}`)
|
|
|
|
|
|
const scriptsDir = path.join(batchDir, 'scripts')
|
|
|
|
|
|
ensureDir(batchDir)
|
|
|
|
|
|
ensureDir(scriptsDir)
|
|
|
|
|
|
|
|
|
|
|
|
const defaultAccount = args.account || ''
|
|
|
|
|
|
const defaultMode = args.mode || 'single'
|
2026-05-08 23:53:37 +08:00
|
|
|
|
const defaultVoice = args.voice || ''
|
2026-05-08 23:06:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 构建 items + 提取脚本
|
|
|
|
|
|
const items = []
|
|
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
|
|
|
|
const row = rows[i]
|
|
|
|
|
|
const script = extractField(row, ['脚本', 'script', '文案', '旁白'])
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const title = extractField(row, ['选题', '标题', 'title', 'name']) || ''
|
2026-05-08 23:06:17 +08:00
|
|
|
|
const account = extractField(row, ['账号', 'account']) || defaultAccount
|
|
|
|
|
|
const mode = extractField(row, ['模式', 'mode']) || defaultMode
|
2026-05-08 23:53:37 +08:00
|
|
|
|
const voiceName = extractField(row, ['音色', 'voice']) || defaultVoice
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const forwardRaw = extractField(row, ['转发文案带话题', '转发文案', 'forwardCopy', '分享文案'])
|
2026-05-08 23:06:17 +08:00
|
|
|
|
|
|
|
|
|
|
if (!script || !script.trim()) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.warn(` ⚠ 第 ${i + 2} 行(${title || '无标题'})脚本为空,跳过`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scriptFile = path.join(scriptsDir, `row_${String(i + 1).padStart(3, '0')}.txt`)
|
|
|
|
|
|
fs.writeFileSync(scriptFile, script.trim(), 'utf-8')
|
|
|
|
|
|
|
2026-05-08 23:53:37 +08:00
|
|
|
|
// 解析音色名称 → ID
|
|
|
|
|
|
const resolvedVoice = voiceName ? resolveVoice(voiceName) : ''
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
// 解析转发文案带话题:分离出 hashtags 和转发文案
|
|
|
|
|
|
const { forwardCopy, hashtags } = parseForwardField(forwardRaw)
|
|
|
|
|
|
|
2026-05-08 23:06:17 +08:00
|
|
|
|
items.push({
|
|
|
|
|
|
row: i + 1,
|
2026-05-14 23:16:12 +08:00
|
|
|
|
title: title || '', // 原 Excel 选题(可为空,由 AI 后续填充)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
account: account || defaultAccount,
|
|
|
|
|
|
mode: mode || defaultMode,
|
2026-05-08 23:53:37 +08:00
|
|
|
|
voice: resolvedVoice,
|
2026-05-14 23:16:12 +08:00
|
|
|
|
forwardCopy: forwardCopy || '',
|
|
|
|
|
|
hashtags: hashtags || '',
|
|
|
|
|
|
topicA: '', // 方案A: 双句封面 ≤12字
|
|
|
|
|
|
topicB: '', // 方案B: ≤4字极致精简
|
|
|
|
|
|
draftName: '', // 草稿名称: 账号_月日_序号_方案B
|
2026-05-08 23:06:17 +08:00
|
|
|
|
scriptFile: `scripts/row_${String(i + 1).padStart(3, '0')}.txt`,
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
manifestPath: null,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
|
console.error('没有有效的脚本行')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 校验账号
|
|
|
|
|
|
validateAccounts(items)
|
|
|
|
|
|
|
|
|
|
|
|
// 写 batch-manifest
|
|
|
|
|
|
const batchManifest = {
|
|
|
|
|
|
source: path.basename(filePath),
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
2026-05-08 23:53:37 +08:00
|
|
|
|
defaults: { account: defaultAccount, mode: defaultMode, voice: defaultVoice ? resolveVoice(defaultVoice) : '' },
|
2026-05-08 23:06:17 +08:00
|
|
|
|
stats: calcStats(items),
|
|
|
|
|
|
items,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const manifestPath = path.join(batchDir, 'batch-manifest.json')
|
|
|
|
|
|
writeJson(manifestPath, batchManifest)
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n批量任务已创建: ${manifestPath}`)
|
|
|
|
|
|
console.log(` 来源: ${path.basename(filePath)}`)
|
|
|
|
|
|
console.log(` 总数: ${items.length}`)
|
|
|
|
|
|
console.log(` 默认账号: ${defaultAccount || '(未指定,需每行填写)'}`)
|
|
|
|
|
|
console.log(` 默认模式: ${defaultMode}`)
|
2026-05-08 23:53:37 +08:00
|
|
|
|
console.log(` 默认音色: ${defaultVoice || '(用账号配置)'}`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log(` 脚本目录: ${scriptsDir}/`)
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// status: 展示批量进度
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdStatus(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
const batchDir = path.dirname(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n批量任务: ${batch.source}`)
|
|
|
|
|
|
console.log(` 创建时间: ${batch.createdAt}`)
|
|
|
|
|
|
console.log(` 进度: ${batch.stats.completed}/${batch.stats.total} 完成`)
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
|
|
|
|
|
|
const grouped = { pending: [], processing: [], completed: [], failed: [] }
|
|
|
|
|
|
for (const item of batch.items) {
|
|
|
|
|
|
const list = grouped[item.status] || grouped.pending
|
|
|
|
|
|
list.push(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const displayTitle = (it) => it.topicA || it.topic || it.title || ''
|
|
|
|
|
|
|
2026-05-08 23:06:17 +08:00
|
|
|
|
if (grouped.completed.length > 0) {
|
|
|
|
|
|
console.log(` ✅ 完成 (${grouped.completed.length}):`)
|
|
|
|
|
|
for (const it of grouped.completed) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(` #${it.row} ${displayTitle(it)} → ${it.manifestPath || ''}`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (grouped.failed.length > 0) {
|
|
|
|
|
|
console.log(` ❌ 失败 (${grouped.failed.length}):`)
|
|
|
|
|
|
for (const it of grouped.failed) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(` #${it.row} ${displayTitle(it)} — ${it.error || '未知错误'}`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (grouped.processing.length > 0) {
|
|
|
|
|
|
console.log(` 🔄 进行中 (${grouped.processing.length}):`)
|
|
|
|
|
|
for (const it of grouped.processing) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(` #${it.row} ${displayTitle(it)}`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (grouped.pending.length > 0) {
|
|
|
|
|
|
console.log(` ⏳ 待处理 (${grouped.pending.length}):`)
|
|
|
|
|
|
for (const it of grouped.pending) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(` #${it.row} ${displayTitle(it)} (账号: ${it.account || '未指定'}, 模式: ${it.mode}, 音色: ${it.voice || '账号默认'})`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 输出下一个待处理的行号(方便 AI agent 消费)
|
|
|
|
|
|
const next = batch.items.find(it => it.status === 'pending')
|
|
|
|
|
|
if (next) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(`\n ▶ 下一条: #${next.row} ${displayTitle(next)} (账号: ${next.account}, 模式: ${next.mode}, 音色: ${next.voice || '账号默认'})`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log(` 脚本文件: ${path.resolve(batchDir, next.scriptFile)}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// mark: 标记某行状态
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdMark(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
const item = batch.items.find(it => it.row === args.row)
|
|
|
|
|
|
if (!item) {
|
|
|
|
|
|
console.error(`行 ${args.row} 不存在`)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validStatuses = ['pending', 'processing', 'completed', 'failed']
|
|
|
|
|
|
if (!validStatuses.includes(args.status)) {
|
|
|
|
|
|
console.error(`无效状态: ${args.status},可选: ${validStatuses.join(', ')}`)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const oldStatus = item.status
|
|
|
|
|
|
item.status = args.status
|
|
|
|
|
|
if (args.manifestPath) item.manifestPath = args.manifestPath
|
|
|
|
|
|
if (args.error) item.error = args.error
|
|
|
|
|
|
if (args.status !== 'failed') item.error = null
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
// 可选元数据更新
|
|
|
|
|
|
if (args.topic) item.topicA = args.topic // 向后兼容 --topic
|
|
|
|
|
|
if (args.topicA) item.topicA = args.topicA
|
|
|
|
|
|
if (args.topicB) item.topicB = args.topicB
|
|
|
|
|
|
if (args.draftName) item.draftName = args.draftName
|
|
|
|
|
|
if (args.forwardCopy) item.forwardCopy = args.forwardCopy
|
|
|
|
|
|
if (args.hashtags) item.hashtags = args.hashtags
|
2026-05-15 12:04:51 +08:00
|
|
|
|
if (args.draftUrl) item.draftUrl = args.draftUrl
|
2026-05-14 23:16:12 +08:00
|
|
|
|
|
2026-05-08 23:06:17 +08:00
|
|
|
|
batch.stats = calcStats(batch.items)
|
|
|
|
|
|
writeJson(manifestPath, batch)
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const label = item.topicA || item.title || ''
|
|
|
|
|
|
console.log(`#${item.row} ${label}: ${oldStatus} → ${args.status}`)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// retry-failed: 重置失败行
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdRetryFailed(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
for (const item of batch.items) {
|
|
|
|
|
|
if (item.status === 'failed') {
|
|
|
|
|
|
item.status = 'pending'
|
|
|
|
|
|
item.error = null
|
|
|
|
|
|
count++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
batch.stats = calcStats(batch.items)
|
|
|
|
|
|
writeJson(manifestPath, batch)
|
|
|
|
|
|
console.log(`已重置 ${count} 条失败记录为 pending`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// next: 输出下一条待处理的信息(机器友好格式)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdNext(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
const batchDir = path.dirname(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
const item = batch.items.find(it => it.status === 'pending')
|
|
|
|
|
|
if (!item) {
|
|
|
|
|
|
console.log(JSON.stringify({ done: true }))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const result = {
|
2026-05-08 23:06:17 +08:00
|
|
|
|
done: false,
|
|
|
|
|
|
row: item.row,
|
2026-05-14 23:16:12 +08:00
|
|
|
|
title: item.title || '',
|
2026-05-08 23:06:17 +08:00
|
|
|
|
account: item.account,
|
|
|
|
|
|
mode: item.mode,
|
2026-05-08 23:53:37 +08:00
|
|
|
|
voice: item.voice || '',
|
2026-05-14 23:16:12 +08:00
|
|
|
|
forwardCopy: item.forwardCopy || '',
|
|
|
|
|
|
hashtags: item.hashtags || '',
|
|
|
|
|
|
topicA: item.topicA || '',
|
|
|
|
|
|
topicB: item.topicB || '',
|
|
|
|
|
|
draftName: item.draftName || '',
|
2026-05-15 12:04:51 +08:00
|
|
|
|
draftUrl: item.draftUrl || '',
|
2026-05-08 23:06:17 +08:00
|
|
|
|
scriptFile: path.resolve(batchDir, item.scriptFile),
|
2026-05-14 23:16:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --with-script:附带脚本内容,方便 AI 直接基于脚本生成选题/转发文案
|
|
|
|
|
|
if (args.withScript) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
result.script = fs.readFileSync(path.resolve(batchDir, item.scriptFile), 'utf-8')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
result.script = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(JSON.stringify(result))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// export: 输出最终表格(含草稿名称列)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdExport(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
const batchDir = path.dirname(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
// 构建导出行
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
for (const item of batch.items) {
|
|
|
|
|
|
// 读取脚本文件
|
|
|
|
|
|
let script = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
script = fs.readFileSync(path.resolve(batchDir, item.scriptFile), 'utf-8').trim()
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
// 重组转发文案带话题(避免 hashtags 重复)
|
|
|
|
|
|
const htags = (item.hashtags || '').trim()
|
|
|
|
|
|
let forwardBody = (item.forwardCopy || '').trim()
|
|
|
|
|
|
// 如果 forwardCopy 已包含 hashtags,则剥离避免重复
|
|
|
|
|
|
if (htags && forwardBody.endsWith(htags)) {
|
|
|
|
|
|
forwardBody = forwardBody.slice(0, -htags.length).trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
const forwardFull = [forwardBody, htags].filter(Boolean).join('')
|
|
|
|
|
|
|
|
|
|
|
|
// 选题列:topicA(方案A)> 旧字段 topic > 原 title
|
|
|
|
|
|
const topicDisplay = item.topicA || item.topic || item.title || ''
|
|
|
|
|
|
|
|
|
|
|
|
rows.push({
|
|
|
|
|
|
row: item.row,
|
|
|
|
|
|
选题: topicDisplay,
|
|
|
|
|
|
脚本: script,
|
|
|
|
|
|
账号: item.account,
|
|
|
|
|
|
模式: item.mode,
|
|
|
|
|
|
音色: item.voice || '',
|
|
|
|
|
|
转发文案带话题: forwardFull,
|
|
|
|
|
|
草稿名称: item.draftName || '',
|
2026-05-15 12:04:51 +08:00
|
|
|
|
草稿地址: item.draftUrl || '',
|
2026-05-14 23:16:12 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按 row 排序
|
|
|
|
|
|
rows.sort((a, b) => a.row - b.row)
|
|
|
|
|
|
|
|
|
|
|
|
const format = args.format || 'csv'
|
|
|
|
|
|
const dateStr = formatDate(new Date())
|
|
|
|
|
|
const baseName = path.basename(manifestPath, '.json')
|
|
|
|
|
|
|
|
|
|
|
|
if (format === 'xlsx') {
|
|
|
|
|
|
exportXlsx(manifestPath, rows)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
exportCsv(manifestPath, rows)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exportCsv(manifestPath, rows) {
|
|
|
|
|
|
const outPath = manifestPath.replace('.json', '_export.csv')
|
2026-05-15 12:04:51 +08:00
|
|
|
|
const headers = ['选题', '脚本', '账号', '模式', '音色', '转发文案带话题', '草稿名称', '草稿地址']
|
2026-05-14 23:16:12 +08:00
|
|
|
|
|
|
|
|
|
|
const lines = [headers.join(',')]
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
const vals = headers.map(h => {
|
|
|
|
|
|
const v = String(r[h] || '')
|
|
|
|
|
|
// CSV 转义:含逗号、引号、换行的字段用引号包裹
|
|
|
|
|
|
if (v.includes(',') || v.includes('"') || v.includes('\n')) {
|
|
|
|
|
|
return `"${v.replace(/"/g, '""')}"`
|
|
|
|
|
|
}
|
|
|
|
|
|
return v
|
|
|
|
|
|
})
|
|
|
|
|
|
lines.push(vals.join(','))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(outPath, lines.join('\n'), 'utf-8')
|
|
|
|
|
|
console.log(`表格已导出: ${outPath}`)
|
|
|
|
|
|
console.log(` 共 ${rows.length} 条记录`)
|
|
|
|
|
|
|
|
|
|
|
|
// 同时打印到控制台
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
printTable(rows, headers)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exportXlsx(manifestPath, rows) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const XLSX = require('xlsx')
|
2026-05-15 12:04:51 +08:00
|
|
|
|
const headers = ['选题', '脚本', '账号', '模式', '音色', '转发文案带话题', '草稿名称', '草稿地址']
|
2026-05-14 23:16:12 +08:00
|
|
|
|
const data = rows.map(r => headers.map(h => r[h] || ''))
|
|
|
|
|
|
data.unshift(headers)
|
|
|
|
|
|
|
|
|
|
|
|
const ws = XLSX.utils.aoa_to_sheet(data)
|
|
|
|
|
|
const wb = XLSX.utils.book_new()
|
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, '视频清单')
|
|
|
|
|
|
|
|
|
|
|
|
const outPath = manifestPath.replace('.json', '_export.xlsx')
|
|
|
|
|
|
XLSX.writeFile(wb, outPath)
|
|
|
|
|
|
console.log(`表格已导出: ${outPath}`)
|
|
|
|
|
|
console.log(` 共 ${rows.length} 条记录`)
|
|
|
|
|
|
|
|
|
|
|
|
console.log()
|
|
|
|
|
|
printTable(rows, headers)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
|
|
|
|
console.warn('xlsx 模块未安装,改用 CSV 格式')
|
|
|
|
|
|
exportCsv(manifestPath, rows)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function printTable(rows, headers) {
|
|
|
|
|
|
// 计算每列最大宽度
|
|
|
|
|
|
const widths = headers.map(h => {
|
|
|
|
|
|
const maxData = rows.reduce((m, r) => Math.max(m, String(r[h] || '').length), 0)
|
|
|
|
|
|
return Math.min(Math.max(maxData, h.length), 40) // 单列最长 40 字符
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 分隔线
|
|
|
|
|
|
const sep = '|-' + widths.map(w => '-'.repeat(w)).join('-|-') + '-|'
|
|
|
|
|
|
|
|
|
|
|
|
// 表头
|
|
|
|
|
|
const headerLine = '| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |'
|
|
|
|
|
|
|
|
|
|
|
|
console.log(sep)
|
|
|
|
|
|
console.log(headerLine)
|
|
|
|
|
|
console.log(sep)
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
const line = '| ' + headers.map((h, i) => pad(String(r[h] || ''), widths[i])).join(' | ') + ' |'
|
|
|
|
|
|
console.log(line)
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(sep)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pad(s, width) {
|
|
|
|
|
|
// 中文字符占 2 个显示宽度
|
|
|
|
|
|
let displayLen = 0
|
|
|
|
|
|
for (const ch of s) {
|
|
|
|
|
|
displayLen += /[一-鿿-]/.test(ch) ? 2 : 1
|
|
|
|
|
|
}
|
|
|
|
|
|
const padding = Math.max(0, width - displayLen)
|
|
|
|
|
|
return s + ' '.repeat(padding)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// rename-drafts: 批量重命名剪映草稿箱文件夹
|
|
|
|
|
|
// Mac 版剪映草稿显示名 = 文件夹名,直接 mv 即可
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function cmdRenameDrafts(args) {
|
|
|
|
|
|
const manifestPath = path.resolve(args.file)
|
|
|
|
|
|
const batch = readJson(manifestPath)
|
|
|
|
|
|
const batchDir = path.dirname(manifestPath)
|
|
|
|
|
|
|
|
|
|
|
|
// 草稿目录:默认 Mac 版 JianyingPro 路径
|
|
|
|
|
|
const homeDir = require('os').homedir()
|
|
|
|
|
|
const draftDir = args.draftDir || path.join(homeDir, 'Movies', 'JianyingPro', 'User Data', 'Projects', 'com.lveditor.draft')
|
|
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(draftDir)) {
|
|
|
|
|
|
console.error(`草稿目录不存在: ${draftDir}`)
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建脚本指纹 → draftName 映射
|
|
|
|
|
|
const itemLookup = []
|
|
|
|
|
|
for (const it of batch.items) {
|
|
|
|
|
|
if (!it.draftName) continue
|
|
|
|
|
|
const sp = path.resolve(batchDir, it.scriptFile)
|
|
|
|
|
|
let s = ''
|
|
|
|
|
|
try { s = fs.readFileSync(sp, 'utf-8').trim().replace(/\s+/g, '') } catch {}
|
|
|
|
|
|
itemLookup.push({ row: it.row, draftName: it.draftName, sig: s.slice(0, 30) })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 扫描草稿目录
|
|
|
|
|
|
const allDrafts = fs.readdirSync(draftDir)
|
|
|
|
|
|
.filter(d => fs.statSync(path.join(draftDir, d)).isDirectory())
|
|
|
|
|
|
|
|
|
|
|
|
// 匹配
|
|
|
|
|
|
const matches = []
|
|
|
|
|
|
for (const folderId of allDrafts) {
|
|
|
|
|
|
const cp = path.join(draftDir, folderId, 'draft_content.json')
|
|
|
|
|
|
if (!fs.existsSync(cp)) continue
|
|
|
|
|
|
let content
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = fs.readFileSync(cp, 'utf-8')
|
|
|
|
|
|
if (!raw.startsWith('{')) continue
|
|
|
|
|
|
content = JSON.parse(raw)
|
|
|
|
|
|
} catch { continue }
|
|
|
|
|
|
|
|
|
|
|
|
const texts = (content.materials?.texts || [])
|
|
|
|
|
|
.filter(t => t.type === 'subtitle')
|
|
|
|
|
|
.map(t => { try { const c = JSON.parse(t.content); return c.text || '' } catch { return '' } })
|
|
|
|
|
|
const fullText = texts.join('').replace(/\s+/g, '')
|
|
|
|
|
|
if (fullText.length < 50) continue
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of itemLookup) {
|
|
|
|
|
|
if (fullText.includes(item.sig)) {
|
|
|
|
|
|
matches.push({ folderId, item, dateStr: folderId.slice(0, 14) })
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 按 row 分组,按日期排序(同 row 多个草稿用 _v2 区分)
|
|
|
|
|
|
const rowGroups = {}
|
|
|
|
|
|
for (const m of matches) {
|
|
|
|
|
|
if (!rowGroups[m.item.row]) rowGroups[m.item.row] = []
|
|
|
|
|
|
rowGroups[m.item.row].push(m)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有目标名称,用于跳过已改名的
|
|
|
|
|
|
const targetNames = new Set()
|
|
|
|
|
|
for (const [row, group] of Object.entries(rowGroups)) {
|
|
|
|
|
|
for (let i = 0; i < group.length; i++) {
|
|
|
|
|
|
targetNames.add(i === 0 ? group[i].item.draftName : group[i].item.draftName + '_v' + (i + 1))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let renamed = 0
|
|
|
|
|
|
for (const [row, group] of Object.entries(rowGroups)) {
|
|
|
|
|
|
group.sort((a, b) => a.dateStr.localeCompare(b.dateStr))
|
|
|
|
|
|
for (let i = 0; i < group.length; i++) {
|
|
|
|
|
|
const m = group[i]
|
|
|
|
|
|
let newName = m.item.draftName
|
|
|
|
|
|
if (i > 0) newName = m.item.draftName + '_v' + (i + 1)
|
|
|
|
|
|
|
|
|
|
|
|
// 跳过已是目标名称或已被其他行匹配占用的
|
|
|
|
|
|
if (m.folderId === newName || (targetNames.has(m.folderId) && m.folderId !== newName)) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const oldPath = path.join(draftDir, m.folderId)
|
|
|
|
|
|
const newPath = path.join(draftDir, newName)
|
|
|
|
|
|
if (oldPath === newPath) continue
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
fs.renameSync(oldPath, newPath)
|
|
|
|
|
|
console.log('#' + row + ' ' + m.folderId + ' → ' + newName)
|
|
|
|
|
|
renamed++
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('#' + row + ' ' + m.folderId + ' FAILED: ' + e.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const unmatched = itemLookup.filter(it => !matches.some(m => m.item.row === it.row))
|
|
|
|
|
|
console.log('\n改名: ' + renamed + ' 匹配: ' + matches.length + ' 未匹配: ' + unmatched.length)
|
|
|
|
|
|
if (unmatched.length > 0) {
|
|
|
|
|
|
console.log('未匹配:')
|
|
|
|
|
|
for (const u of unmatched) console.log(' #' + u.row + ' ' + u.draftName)
|
|
|
|
|
|
}
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function ensureDir(dir) {
|
|
|
|
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readJson(filePath) {
|
|
|
|
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function writeJson(filePath, data) {
|
|
|
|
|
|
const tmp = filePath + '.tmp'
|
|
|
|
|
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8')
|
|
|
|
|
|
fs.renameSync(tmp, filePath)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calcStats(items) {
|
|
|
|
|
|
const stats = { total: items.length, pending: 0, processing: 0, completed: 0, failed: 0 }
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
if (stats[item.status] !== undefined) stats[item.status]++
|
|
|
|
|
|
else stats.pending++
|
|
|
|
|
|
}
|
|
|
|
|
|
return stats
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(d) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
d.getFullYear(),
|
|
|
|
|
|
String(d.getMonth() + 1).padStart(2, '0'),
|
|
|
|
|
|
String(d.getDate()).padStart(2, '0'),
|
|
|
|
|
|
].join('')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractField(row, names) {
|
|
|
|
|
|
for (const name of names) {
|
|
|
|
|
|
if (row[name] != null && String(row[name]).trim()) return String(row[name]).trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function validateAccounts(items) {
|
|
|
|
|
|
const uniqueAccounts = [...new Set(items.map(it => it.account).filter(Boolean))]
|
|
|
|
|
|
const missing = uniqueAccounts.filter(acc => {
|
|
|
|
|
|
return !fs.existsSync(path.join(ACCOUNTS_DIR, acc, 'account.json'))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
|
console.warn(`\n ⚠ 以下账号不存在: ${missing.join(', ')}`)
|
|
|
|
|
|
const available = fs.readdirSync(ACCOUNTS_DIR).filter(d => {
|
|
|
|
|
|
if (d.startsWith('_')) return false
|
|
|
|
|
|
return fs.existsSync(path.join(ACCOUNTS_DIR, d, 'account.json'))
|
|
|
|
|
|
})
|
|
|
|
|
|
console.warn(' 可用账号:')
|
|
|
|
|
|
for (const acc of available) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const cfg = readJson(path.join(ACCOUNTS_DIR, acc, 'account.json'))
|
|
|
|
|
|
console.warn(` - ${acc} (${cfg.name})`)
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
console.warn()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Excel / CSV 解析
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function parseExcel(filePath) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const XLSX = require('xlsx')
|
|
|
|
|
|
const workbook = XLSX.readFile(filePath)
|
|
|
|
|
|
const sheetName = workbook.SheetNames[0]
|
|
|
|
|
|
const sheet = workbook.Sheets[sheetName]
|
|
|
|
|
|
return XLSX.utils.sheet_to_json(sheet)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
|
|
|
|
console.error('需要安装 xlsx: cd scripts && pnpm add xlsx')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
throw err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseCsv(filePath) {
|
|
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
|
|
|
|
const lines = content.split(/\r?\n/).filter(l => l.trim())
|
|
|
|
|
|
if (lines.length < 2) return []
|
|
|
|
|
|
|
|
|
|
|
|
const headers = parseCsvLine(lines[0])
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
|
|
|
|
const values = parseCsvLine(lines[i])
|
|
|
|
|
|
const row = {}
|
|
|
|
|
|
headers.forEach((h, j) => { row[h.trim()] = (values[j] || '').trim() })
|
|
|
|
|
|
rows.push(row)
|
|
|
|
|
|
}
|
|
|
|
|
|
return rows
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 解析「转发文案带话题」字段
|
|
|
|
|
|
* 输入: "孩子只要长大,就会开始清算父母。#反派人格#执黑先行"
|
|
|
|
|
|
* 输出: { forwardCopy: "孩子只要长大,就会开始清算父母。", hashtags: "#反派人格#执黑先行" }
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseForwardField(raw) {
|
|
|
|
|
|
if (!raw || !raw.trim()) return { forwardCopy: '', hashtags: '' }
|
|
|
|
|
|
|
|
|
|
|
|
// 提取所有 #xxx 格式的话题
|
|
|
|
|
|
const hashtagRe = /#[^\s#]+/g
|
|
|
|
|
|
const hashtagMatches = raw.match(hashtagRe) || []
|
|
|
|
|
|
const hashtags = hashtagMatches.join('')
|
|
|
|
|
|
|
|
|
|
|
|
// 移除所有话题后剩余的是转发文案
|
|
|
|
|
|
let forwardCopy = raw
|
|
|
|
|
|
for (const tag of hashtagMatches) {
|
|
|
|
|
|
forwardCopy = forwardCopy.replace(tag, '')
|
|
|
|
|
|
}
|
|
|
|
|
|
// 清理多余空白和标点周围的空格
|
|
|
|
|
|
forwardCopy = forwardCopy.replace(/\s+/g, ' ').trim()
|
|
|
|
|
|
|
|
|
|
|
|
return { forwardCopy, hashtags }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 23:06:17 +08:00
|
|
|
|
function parseCsvLine(line) {
|
|
|
|
|
|
const result = []
|
|
|
|
|
|
let current = ''
|
|
|
|
|
|
let inQuotes = false
|
|
|
|
|
|
for (const char of line) {
|
|
|
|
|
|
if (char === '"') { inQuotes = !inQuotes }
|
|
|
|
|
|
else if (char === ',' && !inQuotes) { result.push(current); current = '' }
|
|
|
|
|
|
else { current += char }
|
|
|
|
|
|
}
|
|
|
|
|
|
result.push(current)
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// CLI 入口
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
function main() {
|
|
|
|
|
|
const args = parseArgs(process.argv.slice(2))
|
|
|
|
|
|
const command = args.command
|
|
|
|
|
|
|
|
|
|
|
|
if (command === 'init') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js init --file <xlsx/csv> [--account <账号>] [--mode <模式>]')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdInit(args)
|
|
|
|
|
|
} else if (command === 'status') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js status --file <batch-manifest.json>')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdStatus(args)
|
|
|
|
|
|
} else if (command === 'mark') {
|
|
|
|
|
|
if (!args.file || !args.row || !args.status) {
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.error('用法: batch-pipeline.js mark --file <batch-manifest.json> --row <N> --status <pending|processing|completed|failed> [--manifest-path <path>] [--error <msg>] [--topic-a <方案A>] [--topic-b <方案B>] [--draft-name <草稿名称>] [--forward-copy <转发文案>] [--hashtags <话题>]')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdMark(args)
|
|
|
|
|
|
} else if (command === 'retry-failed') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js retry-failed --file <batch-manifest.json>')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdRetryFailed(args)
|
|
|
|
|
|
} else if (command === 'next') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js next --file <batch-manifest.json>')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdNext(args)
|
2026-05-14 23:16:12 +08:00
|
|
|
|
} else if (command === 'export') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js export --file <batch-manifest.json> [--format csv|xlsx]')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdExport(args)
|
|
|
|
|
|
} else if (command === 'rename-drafts') {
|
|
|
|
|
|
if (!args.file) {
|
|
|
|
|
|
console.error('用法: batch-pipeline.js rename-drafts --file <batch-manifest.json> [--draft-dir <草稿箱路径>]')
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
cmdRenameDrafts(args)
|
2026-05-08 23:06:17 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log('批量视频生产编排器')
|
|
|
|
|
|
console.log('')
|
|
|
|
|
|
console.log('用法:')
|
2026-05-08 23:53:37 +08:00
|
|
|
|
console.log(' batch-pipeline.js init --file <xlsx/csv> [--account <账号>] [--mode <single|framePair>] [--voice <音色>]')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log(' batch-pipeline.js status --file <batch-manifest.json>')
|
|
|
|
|
|
console.log(' batch-pipeline.js next --file <batch-manifest.json>')
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(' batch-pipeline.js mark --file <...> --row <N> --status <pending|processing|completed|failed> [--manifest-path <path>] [--error <msg>] [--topic-a <方案A>] [--topic-b <方案B>] [--draft-name <草稿名称>] [--forward-copy <转发文案>] [--hashtags <话题>]')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log(' batch-pipeline.js retry-failed --file <batch-manifest.json>')
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(' batch-pipeline.js export --file <batch-manifest.json> [--format csv|xlsx]')
|
|
|
|
|
|
console.log(' batch-pipeline.js rename-drafts --file <batch-manifest.json> [--draft-dir <路径>]')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log('')
|
|
|
|
|
|
console.log('Excel 格式:')
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(' 选题 | 脚本 | 账号 | 模式 | 音色 | 转发文案带话题')
|
|
|
|
|
|
console.log(' 选题/标题/title — 标题(可选,留空则由 AI 根据脚本自动生成)')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
console.log(' 脚本/文案/旁白 — 口播文案(必填)')
|
|
|
|
|
|
console.log(' 账号/account — 账号ID(可选,可由 --account 指定默认值)')
|
|
|
|
|
|
console.log(' 模式/mode — single|framePair(可选,可由 --mode 指定默认值)')
|
2026-05-08 23:53:37 +08:00
|
|
|
|
console.log(' 音色/voice — 音色名称或ID(可选,可由 --voice 指定默认值)')
|
2026-05-14 23:16:12 +08:00
|
|
|
|
console.log(' 转发文案带话题/转发文案/forwardCopy — 转发文案+#话题(可选,留空则由 AI 生成)')
|
2026-05-08 23:06:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
main()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 23:16:12 +08:00
|
|
|
|
module.exports = { cmdInit, cmdStatus, cmdMark, cmdRetryFailed, cmdNext, cmdExport, cmdRenameDrafts }
|