Files
memind/scripts/seed-plaza-covers.mjs
T

227 lines
8.1 KiB
JavaScript

#!/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');