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