hulai-website/scripts/download-covers.mjs
刘涛 8832762e51 fix: 修复首页空白、小红书/游记封面图、定制页二维码
1. 首页大面积空白:useScrollReveal 添加 MutationObserver,
   确保异步子组件的 .reveal 元素也能触发入场动画

2. 小红书/游记封面图不显示:
   - 小红书 CDN 签名链接过期+防盗链,全部下载到本地
   - 新增 download-covers.mjs 脚本批量下载封面
   - 新增 useImageProxy composable 转换外部 URL
   - 页面 img 标签添加 referrerpolicy="no-referrer"
   - 管理端保存时自动下载外部封面图到本地
   - 种子数据更新为本地路径

3. 定制页微信号文字替换为二维码图片

4. prerender 路由添加 /xiaohongshu 和 /youji

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:37:58 +08:00

133 行
3.6 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 下载小红书/游记封面图片到本地
*
* 小红书 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)