#!/usr/bin/env node /** * 强制重生成 Plaza 帖对应 workspace HTML 的缩略图(嵌入 hero 图)。 * * Usage (103): * node scripts/backfill-plaza-workspace-thumbnails.mjs --dry-run * node scripts/backfill-plaza-workspace-thumbnails.mjs --apply * node scripts/backfill-plaza-workspace-thumbnails.mjs --apply --batch-prefix plaza-batch- */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDbPool } from '../db.mjs'; import { resolveMindSpaceUserPublishDir } from '../mindspace-runtime-config.mjs'; import { ensureWorkspaceHtmlThumbnail } from '../mindspace-workspace-thumbnails.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); 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')); function parseArgs() { const args = process.argv.slice(2); let apply = false; let batchPrefix = ''; for (let i = 0; i < args.length; i += 1) { if (args[i] === '--apply') apply = true; else if (args[i] === '--dry-run') apply = false; else if (args[i] === '--batch-prefix' && args[i + 1]) batchPrefix = args[++i]; } return { apply, batchPrefix }; } const { apply, batchPrefix } = parseArgs(); const h5Root = process.env.H5_ROOT ?? path.join(root, 'users'); const pool = createDbPool(); const params = ['published']; let batchFilter = ''; if (batchPrefix) { batchFilter = ' AND p.workspace_relative_path LIKE ?'; params.push(`public/${batchPrefix}%`); } const [rows] = await pool.query( `SELECT pp.id AS post_id, pp.title, pp.user_id, p.workspace_relative_path FROM plaza_posts pp JOIN h5_publish_records pr ON pr.id = pp.publication_id JOIN h5_page_records p ON p.id = pr.page_id WHERE pp.status = ? AND p.workspace_relative_path LIKE 'public/%.html' ${batchFilter} ORDER BY pp.published_at DESC, pp.id DESC`, params, ); console.log(`${apply ? '写入模式' : 'Dry-run'}:命中 ${rows.length} 条 workspace HTML 帖子。`); let ok = 0; let skipped = 0; let failed = 0; for (const row of rows) { const relativePath = String(row.workspace_relative_path ?? '').trim(); if (!relativePath) { skipped += 1; continue; } const publishDir = resolveMindSpaceUserPublishDir(h5Root, { id: row.user_id }); const htmlPath = path.join(publishDir, relativePath); if (!fs.existsSync(htmlPath)) { console.warn(`- SKIP ${row.post_id} | ${row.title} | missing ${relativePath}`); skipped += 1; continue; } console.log(`- ${row.post_id} | ${row.title} | ${relativePath}`); if (!apply) continue; try { await ensureWorkspaceHtmlThumbnail(publishDir, relativePath, null, { title: row.title, force: true, }); ok += 1; } catch (error) { failed += 1; console.warn( ` FAIL ${row.post_id}:`, error instanceof Error ? error.message : error, ); } } console.log(`完成:regenerated=${ok} skipped=${skipped} failed=${failed}`); if (!apply) { console.log('Dry-run 未写入;正式执行: node scripts/backfill-plaza-workspace-thumbnails.mjs --apply'); } await pool.end();