diff --git a/scripts/backfill-plaza-cover-urls.mjs b/scripts/backfill-plaza-cover-urls.mjs index fdaabda..117e4ed 100644 --- a/scripts/backfill-plaza-cover-urls.mjs +++ b/scripts/backfill-plaza-cover-urls.mjs @@ -3,10 +3,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDbPool } from '../db.mjs'; -import { resolvePlazaCoverUrl } from '../plaza-posts.mjs'; +import { resolvePlazaPublicationCoverUrl } from '../plaza-posts.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const shouldWrite = process.argv.includes('--write'); +const batchPrefix = (() => { + const idx = process.argv.indexOf('--batch-prefix'); + return idx >= 0 ? String(process.argv[idx + 1] ?? '').trim() : ''; +})(); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) return; @@ -25,12 +29,23 @@ loadEnvFile(path.join(root, '../../.env.local')); loadEnvFile(path.join(root, '.env')); const pool = createDbPool(); +const params = []; +let batchFilter = ''; +if (batchPrefix) { + batchFilter = ' AND p.workspace_relative_path LIKE ?'; + params.push(`public/${batchPrefix}%`); +} + const [rows] = await pool.query( - `SELECT pp.id, pp.title, pp.cover_url, pp.status, pr.public_url + `SELECT pp.id, pp.title, pp.cover_url, pp.user_slug, pp.status, + pr.public_url, pr.url_slug 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 IN ('published', 'pending_review', 'rejected') + ${batchFilter} ORDER BY pp.published_at DESC, pp.id DESC`, + params, ); const updates = rows @@ -38,12 +53,19 @@ const updates = rows id: row.id, title: row.title, from: String(row.cover_url ?? ''), - to: resolvePlazaCoverUrl(row.cover_url, row.public_url), + to: resolvePlazaPublicationCoverUrl({ + inputCoverUrl: row.cover_url, + publicUrl: row.public_url, + publicationUrlSlug: row.url_slug, + userSlug: row.user_slug, + }), publicUrl: String(row.public_url ?? ''), })) .filter((row) => row.to && row.from !== row.to); -console.log(`${shouldWrite ? '写入模式' : 'Dry-run'}:共扫描 ${rows.length} 条帖子,命中 ${updates.length} 条待补封面。`); +console.log( + `${shouldWrite ? '写入模式' : 'Dry-run'}:共扫描 ${rows.length} 条帖子,命中 ${updates.length} 条待补封面。`, +); for (const row of updates.slice(0, 20)) { console.log(`- ${row.id} | ${row.title}`); diff --git a/scripts/backfill-plaza-workspace-thumbnails.mjs b/scripts/backfill-plaza-workspace-thumbnails.mjs new file mode 100644 index 0000000..84ee08d --- /dev/null +++ b/scripts/backfill-plaza-workspace-thumbnails.mjs @@ -0,0 +1,109 @@ +#!/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();