/** * Command: validate — 校验 manifest.json 完整性 * Command: validate-account — 校验账号目录完整性 */ const fs = require('fs') const path = require('path') const { loadManifest, ACCOUNTS_DIR } = require('./pipeline-utils') function validateManifest(manifestPath) { const issues = [] if (!fs.existsSync(manifestPath)) { console.error(`错误: manifest 不存在: ${manifestPath}`) process.exit(1) } let manifest try { manifest = loadManifest(manifestPath) } catch (e) { console.error(`错误: JSON 解析失败: ${e.message}`) process.exit(1) } if (!manifest.account) issues.push('缺少顶层 account') if (!manifest.imageModel) issues.push('缺少顶层 imageModel(可选: gemini, mj)') if (!manifest.format) issues.push('缺少顶层 format(如 9:16)') if (!manifest.items || !Array.isArray(manifest.items)) issues.push('缺少顶层 items 数组') if (!manifest.mode) issues.push('缺少顶层 mode(single 或 framePair)') if (manifest.items && Array.isArray(manifest.items)) { manifest.items.forEach((item, i) => { const prefix = `items[${i}]` if (!item.narration && !item.text) issues.push(`${prefix} 缺少 narration 或 text(中文旁白)`) if (!item.shotDesc) issues.push(`${prefix} 缺少 shotDesc(分镜描述)`) if (!item.imagePrompt) issues.push(`${prefix} 缺少 imagePrompt`) if (manifest.mode === 'framePair' && !item.lastFramePrompt) { issues.push(`${prefix} 首尾帧模式缺少 lastFramePrompt(imagePrompt 作为第一帧)`) } if (item.status && !['pending', 'generating', 'done', 'failed'].includes(item.status)) { issues.push(`${prefix} status 无效: ${item.status}`) } }) } if (issues.length === 0) { console.log(`✓ Manifest 校验通过: ${manifestPath}`) console.log(` ${manifest.items?.length || 0} items, account=${manifest.account}, mode=${manifest.mode}`) } else { console.error(`✗ 发现 ${issues.length} 个问题:`) issues.forEach(issue => console.error(` - ${issue}`)) process.exit(1) } } function validateAccount(accountId) { const issues = [] const accountDir = path.join(ACCOUNTS_DIR, accountId) if (!fs.existsSync(accountDir)) { console.error(`错误: 账号不存在: ${accountDir}`); process.exit(1) } const accountPath = path.join(accountDir, 'account.json') if (!fs.existsSync(accountPath)) { console.error('错误: 缺少 account.json'); process.exit(1) } let config try { config = JSON.parse(fs.readFileSync(accountPath, 'utf-8')) } catch (e) { console.error(`错误: JSON 解析失败: ${e.message}`); process.exit(1) } if (config.id !== accountId) issues.push(`id 不匹配: json="${config.id}" vs 目录="${accountId}"`) if (!config.name) issues.push('缺少 name') if (!config.imageModel) issues.push('缺少 imageModel') if (!config.defaultFormat) issues.push('缺少 defaultFormat') // 检查 prompts 文件 const promptFiles = [ { field: 'storyboardPrompt', label: '分镜' }, { field: 'imageStylePrompt', label: '图片提示词' }, { field: 'videoStylePrompt', label: '视频提示词' }, ] for (const { field, label } of promptFiles) { const relPath = config[field] if (!relPath) { issues.push(`缺少 ${field}(prompts 路径)`) } else { const absPath = path.join(accountDir, relPath) if (!fs.existsSync(absPath)) { issues.push(`${label}文件不存在: ${relPath}`) } } } const refDir = path.join(accountDir, 'references') const localRefs = fs.existsSync(refDir) ? fs.readdirSync(refDir).filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f)) : [] const topRefs = config.references || [] if (localRefs.length === 0 && topRefs.length === 0) { issues.push('无参考图(建议至少 1 张)') } for (const ref of topRefs) { if (!ref.url) issues.push(`参考图 ${ref.file} 缺少 url(未上传 OSS)`) } if (issues.length === 0) { console.log(`✓ 账号校验通过: ${accountId}`) console.log(` ${config.name}, 模型: ${config.imageModel}+${config.videoModel || '(未指定)'}`) console.log(` 参考图: ${localRefs.length} 本地, ${topRefs.length} 已上传`) } else { console.error(`✗ 发现 ${issues.length} 个问题:`) issues.forEach(i => console.error(` - ${i}`)) process.exit(1) } } module.exports = { validateManifest, validateAccount }