2026-04-30 21:18:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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}]`
|
2026-05-01 01:52:02 +08:00
|
|
|
|
if (!item.script && !item.text) issues.push(`${prefix} 缺少 script 或 text(中文文案)`)
|
2026-04-30 21:18:31 +08:00
|
|
|
|
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}`)
|
|
|
|
|
|
}
|
2026-05-02 00:14:40 +08:00
|
|
|
|
if (item.status === 'done' && !item.file && !item.video && !item.url) {
|
|
|
|
|
|
issues.push(`${prefix} status=done 但缺少 file/video/url(素材路径)`)
|
|
|
|
|
|
}
|
2026-04-30 21:18:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
2026-05-01 00:44:18 +08:00
|
|
|
|
// 检查 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}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 21:18:31 +08:00
|
|
|
|
const refDir = path.join(accountDir, 'references')
|
|
|
|
|
|
const localRefs = fs.existsSync(refDir)
|
|
|
|
|
|
? fs.readdirSync(refDir).filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f))
|
|
|
|
|
|
: []
|
2026-04-30 21:27:49 +08:00
|
|
|
|
const topRefs = config.references || []
|
|
|
|
|
|
if (localRefs.length === 0 && topRefs.length === 0) {
|
2026-04-30 21:18:31 +08:00
|
|
|
|
issues.push('无参考图(建议至少 1 张)')
|
|
|
|
|
|
}
|
2026-04-30 21:27:49 +08:00
|
|
|
|
for (const ref of topRefs) {
|
|
|
|
|
|
if (!ref.url) issues.push(`参考图 ${ref.file} 缺少 url(未上传 OSS)`)
|
2026-04-30 21:18:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
|
|
console.log(`✓ 账号校验通过: ${accountId}`)
|
|
|
|
|
|
console.log(` ${config.name}, 模型: ${config.imageModel}+${config.videoModel || '(未指定)'}`)
|
2026-04-30 21:27:49 +08:00
|
|
|
|
console.log(` 参考图: ${localRefs.length} 本地, ${topRefs.length} 已上传`)
|
2026-04-30 21:18:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.error(`✗ 发现 ${issues.length} 个问题:`)
|
|
|
|
|
|
issues.forEach(i => console.error(` - ${i}`))
|
|
|
|
|
|
process.exit(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { validateManifest, validateAccount }
|