/** * 下载小红书/游记封面图片到本地 * * 小红书 CDN 签名 URL 会过期且有防盗链, * 此脚本将封面图下载到 public/uploads/covers/ 并更新数据库。 * * 运行: node scripts/download-covers.mjs */ import Database from 'better-sqlite3' import { writeFileSync, mkdirSync, existsSync } from 'fs' import { join, dirname } from 'path' import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) const ROOT = join(__dirname, '..') const DB_PATH = join(ROOT, 'server/database/hulai.db') const COVERS_DIR = join(ROOT, 'public/uploads/covers') // 确保目录存在 mkdirSync(COVERS_DIR, { recursive: true }) /** * 将签名 CDN URL 转为永久可下载的 URL */ function toPermanentUrl(url) { try { const parsed = new URL(url) if (parsed.hostname === 'sns-webpic-qc.xhscdn.com') { const parts = parsed.pathname.split('/') if (parts.length >= 4) { const imagePath = parts.slice(3).join('/') return `https://ci.xiaohongshu.com/${imagePath}` } } // 已经是永久链接 if (parsed.hostname === 'ci.xiaohongshu.com') { return url } } catch {} return url } /** * 下载图片到本地 */ async function downloadImage(url, filename) { const localPath = join(COVERS_DIR, filename) if (existsSync(localPath)) { console.log(` ⏭ 已存在: ${filename}`) return `/uploads/covers/${filename}` } const downloadUrl = toPermanentUrl(url) try { const res = await fetch(downloadUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', }, }) if (!res.ok) { console.log(` ✗ 下载失败 (${res.status}): ${filename} <- ${downloadUrl}`) return null } const buffer = Buffer.from(await res.arrayBuffer()) writeFileSync(localPath, buffer) console.log(` ✓ 已下载: ${filename} (${(buffer.length / 1024).toFixed(1)}KB)`) return `/uploads/covers/${filename}` } catch (err) { console.log(` ✗ 下载出错: ${filename} - ${err.message}`) return null } } /** * 从 URL 中提取文件扩展名 */ function getExtFromUrl(url) { if (url.includes('webp')) return '.webp' if (url.includes('.jpg') || url.includes('.jpeg')) return '.jpg' if (url.includes('.png')) return '.png' return '.webp' // 小红书默认 webp } async function main() { const db = new Database(DB_PATH) for (const key of ['xiaohongshu-wall', 'youji']) { console.log(`\n📦 处理: ${key}`) const row = db.prepare('SELECT data FROM site_content WHERE key = ?').get(key) if (!row) { console.log(` ⚠ 未找到数据: ${key}`) continue } const data = JSON.parse(row.data) const notes = data.notes || [] let updated = 0 for (const note of notes) { if (!note.cover) continue // 跳过已经是本地路径的 if (note.cover.startsWith('/')) { console.log(` ⏭ 已是本地: ${note.id}`) continue } const ext = getExtFromUrl(note.cover) const filename = `${key.replace('xiaohongshu-wall', 'xhs')}-${note.id}${ext}` const localUrl = await downloadImage(note.cover, filename) if (localUrl) { note.cover = localUrl updated++ } } if (updated > 0) { db.prepare('UPDATE site_content SET data = ? WHERE key = ?') .run(JSON.stringify(data), key) console.log(`\n ✅ 已更新 ${updated} 张封面到本地路径`) } else { console.log(`\n ℹ 无需更新`) } } db.close() console.log('\n🎉 完成!') } main().catch(console.error)