/** * 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') const refDir = path.join(accountDir, 'references') const styles = config.styles || {} const hasStyleRefs = Object.values(styles).some(s => s.references && s.references.length > 0) const localRefs = fs.existsSync(refDir) ? fs.readdirSync(refDir).filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f)) : [] if (localRefs.length === 0 && !hasStyleRefs) { issues.push('无参考图(建议至少 1 张)') } const stylesDir = path.join(accountDir, 'styles') const styleFiles = fs.existsSync(stylesDir) ? fs.readdirSync(stylesDir).filter(f => f.endsWith('.md')) : [] if (styleFiles.length === 0) { issues.push('无风格文件(styles/ 下至少 1 个 .md)') } for (const [sName, sConf] of Object.entries(styles)) { for (const ref of (sConf.references || [])) { if (!ref.url) issues.push(`styles.${sName}: 参考图 ${ref.file} 缺少 url(未上传 OSS)`) } } if (issues.length === 0) { console.log(`✓ 账号校验通过: ${accountId}`) console.log(` ${config.name}, 模型: ${config.imageModel}+${config.videoModel || '(未指定)'}`) console.log(` 参考图: ${localRefs.length} 本地, 风格: ${styleFiles.length} 个`) } else { console.error(`✗ 发现 ${issues.length} 个问题:`) issues.forEach(i => console.error(` - ${i}`)) process.exit(1) } } module.exports = { validateManifest, validateAccount }