#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDbPool } from '../db.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const coversDir = path.join(root, 'public/plaza-covers'); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; const key = trimmed.slice(0, eq).trim(); const value = trimmed.slice(eq + 1).trim(); if (!process.env[key]) process.env[key] = value; } } loadEnvFile(path.join(root, '../../.env.local')); loadEnvFile(path.join(root, '.env')); const COVER_SPECS = [ { match: 'Q2 产品增长复盘', file: 'work-report-growth.jpg', url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: 'AI 客服落地', file: 'work-report-ai-service.jpg', url: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '供应链数字化周报', file: 'work-report-supply-chain.jpg', url: 'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: 'Rust 所有权', file: 'study-notes-rust.jpg', url: 'https://images.unsplash.com/photo-1516116216624-53e697fedbea?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: 'LLM Prompt', file: 'study-notes-llm.jpg', url: 'https://images.unsplash.com/photo-1676299082029-32c4f68d5b3d?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '赛博水墨城市', file: 'creative-cyber-city.jpg', url: 'https://images.unsplash.com/photo-1514565131521-0bd7e3b4d8f0?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '独立音乐人', file: 'creative-music-ep.jpg', url: 'https://images.unsplash.com/photo-1511379938545-c1f69419868d?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '社区咖啡店', file: 'business-coffee-shop.jpg', url: 'https://images.unsplash.com/photo-1495474472287-4d71b788c064?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '普拉提工作室', file: 'business-pilates.jpg', url: 'https://images.unsplash.com/photo-1518611012118-696072aa579a?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '马来西亚', file: 'travel-malaysia.jpg', url: 'https://images.unsplash.com/photo-1596422846544-e75a4f7f9d2c?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '京都红叶', file: 'travel-kyoto.jpg', url: 'https://images.unsplash.com/photo-1493976040374-85c8e9658e12?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '电商大促 GMV', file: 'data-analysis-gmv.jpg', url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1280&h=800&q=85&sat=-20', }, { match: '用户留存 Cohort', file: 'data-analysis-cohort.jpg', url: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '极简桌面改造', file: 'lifestyle-desk.jpg', url: 'https://images.unsplash.com/photo-1497215843264-67dbb8748b00?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '晨间写作', file: 'lifestyle-writing.jpg', url: 'https://images.unsplash.com/photo-1455390217141-c52aac34e192?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '2026 愿望清单', file: 'other-wishlist.jpg', url: 'https://images.unsplash.com/photo-1484480974693-6ca0a7823cce?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '开源副业', file: 'other-opensource.jpg', url: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '迪士尼城堡', file: 'work-report-disney.jpg', url: 'https://images.unsplash.com/photo-1568605114967-a6c355b6866c?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: '探索世界', file: 'work-report-explore.jpg', url: 'https://images.unsplash.com/photo-1488646953014-85cb44e25828?auto=format&fit=crop&w=1280&h=800&q=85', }, { match: 'Hello Test', file: 'work-report-hello.jpg', url: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&w=1280&h=800&q=85', }, ]; const CATEGORY_FALLBACKS = { 'work-report': 'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?auto=format&fit=crop&w=1280&h=800&q=85', 'study-notes': 'https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?auto=format&fit=crop&w=1280&h=800&q=85', creative: 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&w=1280&h=800&q=85', business: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=1280&h=800&q=85', travel: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1280&h=800&q=85', 'data-analysis': 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1280&h=800&q=85', lifestyle: 'https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?auto=format&fit=crop&w=1280&h=800&q=85', other: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1280&h=800&q=85', }; function slugify(value) { return String(value) .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, '-') .replace(/^-+|-+$/g, '') .slice(0, 48); } function resolveSpec(title, categorySlug) { const spec = COVER_SPECS.find((item) => title.includes(item.match)); if (spec) return spec; return { match: title, file: `${categorySlug}-${slugify(title)}.jpg`, url: CATEGORY_FALLBACKS[categorySlug] ?? CATEGORY_FALLBACKS.other, }; } function syncToPlazaPublic(fileName) { const source = path.join(coversDir, fileName); if (!fs.existsSync(source)) return; fs.mkdirSync(plazaPublicDir, { recursive: true }); fs.copyFileSync(source, path.join(plazaPublicDir, fileName)); } async function downloadCover(spec, fallbackUrl) { const target = path.join(coversDir, spec.file); if (fs.existsSync(target) && fs.statSync(target).size > 10_000) { syncToPlazaPublic(spec.file); return target; } for (const url of [spec.url, fallbackUrl].filter(Boolean)) { try { const response = await fetch(url, { headers: { Accept: 'image/*' }, redirect: 'follow', signal: AbortSignal.timeout(20_000), }); if (!response.ok) continue; const buffer = Buffer.from(await response.arrayBuffer()); if (buffer.length < 10_000) continue; fs.writeFileSync(target, buffer); syncToPlazaPublic(spec.file); return target; } catch { // try next url } } throw new Error(`下载失败 ${spec.file}`); } const coverBase = process.env.PLAZA_COVER_BASE ?? '/plaza-covers'; const plazaPublicDir = process.env.PLAZA_APP_DIR ? path.join(process.env.PLAZA_APP_DIR, 'public/plaza-covers') : path.join(root, '../memind_plaza/public/plaza-covers'); fs.mkdirSync(coversDir, { recursive: true }); const pool = createDbPool(); const [posts] = await pool.query( `SELECT pp.id, pp.title, pp.cover_url, c.slug AS category_slug FROM plaza_posts pp JOIN plaza_categories c ON c.id = pp.category_id WHERE pp.status = 'published' ORDER BY pp.published_at DESC`, ); let updated = 0; for (const post of posts) { const spec = resolveSpec(post.title, post.category_slug); await downloadCover(spec, CATEGORY_FALLBACKS[post.category_slug]); const coverUrl = `${coverBase}/${spec.file}`; if (post.cover_url === coverUrl) continue; await pool.query(`UPDATE plaza_posts SET cover_url = ?, updated_at = ? WHERE id = ?`, [ coverUrl, Date.now(), post.id, ]); updated += 1; console.log(`✓ ${post.title}`); } await pool.end(); console.log(`\n完成:更新 ${updated} 条封面,共 ${posts.length} 篇已发布内容。`); console.log(`封面目录:${coversDir}`); console.log('刷新查看:http://localhost:3001/plaza');