/** * Phase: upload — OSS 上传 * * 将图片(含首尾帧)和视频上传到 OSS,回写 url / videoUrl */ const path = require('path') const fs = require('fs') const { saveManifest, log, getManifestDir } = require('./pipeline-utils') /** * 自动修复 item.file:如果当前 file 不存在,从 candidates 中匹配实际存在的文件。 * 用户手动换图时可能删除了非选中候选,导致 file 指向不存在的文件。 */ function autoFixFile(item, dir) { const currentPath = path.resolve(dir, item.file) if (fs.existsSync(currentPath)) return false const cands = item.candidates || [] const existing = cands.filter(c => fs.existsSync(path.resolve(dir, c))) if (existing.length === 0) { log('upload', ` ⚠ ${item.file} 不存在,candidates 中也没有文件`) return false } const oldFile = item.file const candMatch = oldFile.match(/_cand(\d+)\./) const targetCand = candMatch ? `_cand${candMatch[1]}.` : null const matched = targetCand ? (existing.find(c => c.includes(targetCand)) || existing[0]) : existing[0] item.file = matched log('upload', ` 🔧 ${path.basename(oldFile)} → ${path.basename(matched)} (${existing.length}/${cands.length} 候选存在)`) return true } async function phaseUpload(manifest, manifestPath) { const dir = getManifestDir(manifestPath) const { uploadFile } = require('../oss-upload') // 自动修复:用户手动换图后 file 可能指向已删除的候选 let fixedCount = 0 for (const item of manifest.items) { if (item.status === 'done' && item.file && !item.url) { if (autoFixFile(item, dir)) fixedCount++ } } if (fixedCount > 0) { saveManifest(manifestPath, manifest) log('upload', `已自动修复 ${fixedCount} 个 file 路径`) } // 图片(含首尾帧 first frame) const imageItems = manifest.items.filter(it => it.status === 'done' && it.file && !it.url ) // 视频 const videoItems = manifest.items.filter(it => it.status === 'done' && it.video && !it.videoUrl ) if (imageItems.length === 0 && videoItems.length === 0) { log('upload', '无待上传文件,跳过') return } // 上传图片 if (imageItems.length > 0) { log('upload', `图片: ${imageItems.length} 个`) for (let i = 0; i < imageItems.length; i++) { const item = imageItems[i] const filePath = path.resolve(dir, item.file) try { const { url } = await uploadFile(filePath) item.url = url log('upload', ` [${i + 1}/${imageItems.length}] ${item.file} → OK`) } catch (err) { item.error = `上传失败: ${err.message}` log('upload', ` [${i + 1}/${imageItems.length}] 失败: ${err.message}`) } // 首尾帧模式:上传 lastFrame if (item.url && item.lastFrame && !item.lastFrameUrl) { const lastPath = path.resolve(dir, item.lastFrame) try { const { url } = await uploadFile(lastPath) item.lastFrameUrl = url log('upload', ` [${i + 1}/${imageItems.length}] lastFrame → OK`) } catch (err) { log('upload', ` [${i + 1}/${imageItems.length}] lastFrame 上传失败: ${err.message}`) } } saveManifest(manifestPath, manifest) } } // 上传视频 if (videoItems.length > 0) { log('upload', `视频: ${videoItems.length} 个`) for (let i = 0; i < videoItems.length; i++) { const item = videoItems[i] const videoPath = path.resolve(dir, item.video) try { const { url } = await uploadFile(videoPath) item.videoUrl = url log('upload', ` [${i + 1}/${videoItems.length}] ${item.video} → OK`) } catch (err) { log('upload', ` [${i + 1}/${videoItems.length}] 失败: ${err.message}`) } saveManifest(manifestPath, manifest) } } } module.exports = { phaseUpload }