#!/usr/bin/env node /** * 图片本地化处理脚本 * 在静态站点生成后运行,将API返回的图片下载到本地 */ import fs from 'fs' import path from 'path' import { createHash } from 'crypto' import { fileURLToPath } from 'url' // 获取当前文件的目录 const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const PROJECT_ROOT = path.resolve(__dirname, '..') /** * 下载并本地化图片 * @param imageUrl 原始图片URL * @param basePath 保存图片的基础路径 * @returns 本地化后的图片路径 */ async function localizeImage(imageUrl, basePath = 'public/images/remote') { // 检查URL是否有效 if (!imageUrl || !imageUrl.startsWith('http')) { return imageUrl } try { // 创建保存目录 const fullBasePath = path.resolve(PROJECT_ROOT, basePath) if (!fs.existsSync(fullBasePath)) { fs.mkdirSync(fullBasePath, { recursive: true }) } // 生成唯一文件名(使用URL的MD5哈希) const urlHash = createHash('md5').update(imageUrl).digest('hex') const fileExt = path.extname(new URL(imageUrl).pathname) || '.jpg' const fileName = `${urlHash}${fileExt}` const filePath = path.join(fullBasePath, fileName) // 如果文件已存在,直接返回路径 if (fs.existsSync(filePath)) { return `/images/remote/${fileName}` } // 下载图片 const response = await fetch(imageUrl) if (!response.ok) { throw new Error(`下载图片失败: ${response.status} ${response.statusText}`) } const buffer = Buffer.from(await response.arrayBuffer()) fs.writeFileSync(filePath, buffer) console.log(`✅ 下载图片成功: ${imageUrl} -> /images/remote/${fileName}`) // 返回本地URL(相对于public目录) return `/images/remote/${fileName}` } catch (error) { console.error(`❌ 本地化图片失败 ${imageUrl}:`, error) return imageUrl // 失败时返回原始URL } } /** * 递归处理对象中的所有图片URL * @param data 需要处理的数据对象或数组 * @param imageFields 指定哪些字段包含图片URL * @returns 处理后的数据 */ async function localizeImages( data, imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src'] ) { if (!data) return data // 处理数组 if (Array.isArray(data)) { return Promise.all(data.map(item => localizeImages(item, imageFields))) } // 处理对象 if (typeof data === 'object') { const result = { ...data } // 处理所有键 for (const [key, value] of Object.entries(result)) { // 如果是图片字段且值是字符串,本地化图片 if (imageFields.includes(key) && typeof value === 'string') { result[key] = await localizeImage(value) } // 递归处理嵌套对象或数组 else if (typeof value === 'object' && value !== null) { result[key] = await localizeImages(value, imageFields) } } return result } return data } /** * 从JSON文件提取图片URL并下载到本地 * @param jsonFilePath JSON文件路径 * @param imageFields 包含图片URL的字段名称数组 */ async function extractAndDownloadImages( jsonFilePath, imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src'] ) { // 检查文件是否存在 if (!fs.existsSync(jsonFilePath)) { console.error(`文件不存在: ${jsonFilePath}`) return } try { // 读取JSON文件 const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8')) // 递归查找所有图片URL const imageUrls = new Set() // 递归函数查找所有图片URL function findImageUrls(obj) { if (!obj) return if (Array.isArray(obj)) { obj.forEach(item => findImageUrls(item)) return } if (typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { if (imageFields.includes(key) && typeof value === 'string' && value.startsWith('http')) { imageUrls.add(value) } else if (typeof value === 'object' && value !== null) { findImageUrls(value) } } } } findImageUrls(jsonData) // 下载所有图片 console.log(`📋 在 ${jsonFilePath} 中找到 ${imageUrls.size} 个图片URL`) let downloadedCount = 0 for (const url of imageUrls) { try { const localUrl = await localizeImage(url) if (localUrl !== url) { downloadedCount++ } } catch (error) { console.error(`❌ 下载失败 ${url}:`, error) } } console.log(`📊 成功下载 ${downloadedCount}/${imageUrls.size} 个图片`) // 将本地化的图片URL写回到JSON文件 try { const localizedData = await localizeImages(jsonData) fs.writeFileSync(jsonFilePath, JSON.stringify(localizedData, null, 2)) console.log(`💾 已更新图片URL至本地路径: ${jsonFilePath}`) } catch (error) { console.error(`❌ 无法更新JSON文件 ${jsonFilePath}:`, error) } } catch (error) { console.error(`❌ 处理文件 ${jsonFilePath} 时出错:`, error) } } /** * 处理API响应文件中的图片 * @param outputDir 输出目录路径(通常是.output目录) */ async function processApiResponseImages(outputDir = '.output') { console.log('🚀 开始处理API响应文件中的图片...') // 检查输出目录是否存在 if (!fs.existsSync(outputDir)) { console.error(`❌ 输出目录不存在: ${outputDir}`) return } const serverDir = path.join(outputDir, 'server/api') if (!fs.existsSync(serverDir)) { console.error(`❌ 服务器API目录不存在: ${serverDir}`) return } console.log(`🔍 扫描API响应文件: ${serverDir}`) // 递归函数查找所有JSON文件 async function processDirectory(dir) { const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { await processDirectory(fullPath) } else if (entry.name.endsWith('.json')) { console.log(`📄 处理JSON文件: ${fullPath}`) await extractAndDownloadImages(fullPath) } } } await processDirectory(serverDir) console.log('✅ 所有API响应文件处理完成') } // 确保脚本能单独运行 async function main() { console.log('===== 🖼️ 开始图片本地化处理 =====') try { // 默认的输出目录是.output const outputDir = process.argv[2] || '.output' await processApiResponseImages(outputDir) console.log('===== ✅ 图片本地化处理完成 =====') process.exit(0) } catch (error) { console.error('❌ 图片本地化处理失败:', error) process.exit(1) } } main()