import { writeFileSync, existsSync, mkdirSync } from 'fs' import { join } from 'path' import { requireAdmin } from '../../utils/auth.js' /** * 下载外部图片到本地 * POST /api/admin/download-image * Body: { url: "https://..." } * Returns: { localUrl: "/uploads/covers/xxx.webp" } */ const ALLOWED_HOSTS = [ 'sns-webpic-qc.xhscdn.com', 'ci.xiaohongshu.com', 'sns-img-bd.xhscdn.com', ] /** * 将签名 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}` } } } catch {} return url } export default defineEventHandler(async (event) => { requireAdmin(event) const { url } = await readBody(event) if (!url || !url.startsWith('http')) { throw createError({ statusCode: 400, message: '请提供有效的图片 URL' }) } // 验证域名白名单 let parsedUrl try { parsedUrl = new URL(url) } catch { throw createError({ statusCode: 400, message: '无效的 URL' }) } if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) { throw createError({ statusCode: 403, message: '该域名不支持自动下载,请手动上传图片' }) } // 生成本地文件名 const pathParts = parsedUrl.pathname.split('/') const imageId = pathParts[pathParts.length - 1].split('!')[0] || `img-${Date.now()}` const filename = `${imageId}.webp` const coversDir = join(process.cwd(), 'public', 'uploads', 'covers') if (!existsSync(coversDir)) { mkdirSync(coversDir, { recursive: true }) } const localPath = join(coversDir, filename) const localUrl = `/uploads/covers/${filename}` // 如果已下载过,直接返回 if (existsSync(localPath)) { return { localUrl } } // 下载图片 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) { throw createError({ statusCode: 502, message: `图片下载失败 (${res.status})` }) } const buffer = Buffer.from(await res.arrayBuffer()) writeFileSync(localPath, buffer) return { localUrl } } catch (err) { if (err.statusCode) throw err throw createError({ statusCode: 502, message: `图片下载出错: ${err.message}` }) } })