Files
memind/scripts/enrich-plaza-posts.mjs
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

171 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Enrich plaza post pages with polished HTML + update summaries.
* Then run: pnpm sync:plaza-prod -- --include-offline
*/
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import fsSync from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createDbPool } from '../db.mjs';
import { getEnrichment } from './plaza-rich-content.mjs';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const storageRoot = path.resolve(process.env.MINDSPACE_STORAGE_ROOT ?? path.join(root, 'data', 'mindspace'));
function loadEnvFile(filePath) {
if (!fsSync.existsSync(filePath)) return;
for (const line of fsSync.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'));
function absStorageKey(storageKey) {
const resolved = path.resolve(storageRoot, storageKey);
if (resolved !== storageRoot && !resolved.startsWith(`${storageRoot}${path.sep}`)) {
throw new Error(`非法 storage_key: ${storageKey}`);
}
return resolved;
}
async function ensureBundle(conn, row, html) {
const now = Date.now();
const checksum = crypto.createHash('sha256').update(html).digest('hex');
const sizeBytes = Buffer.byteLength(html, 'utf8');
const storageKey = `users/${row.user_id}/publications/${row.publication_id}/index.html`;
const filePath = absStorageKey(storageKey);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, html, 'utf8');
let bundleAssetId = row.bundle_asset_id;
let assetVersionId = row.bundle_version_id;
if (!bundleAssetId) {
bundleAssetId = crypto.randomUUID();
assetVersionId = crypto.randomUUID();
await conn.query(
`INSERT INTO h5_assets
(id, user_id, space_id, category_id, asset_type, mime_type, original_filename,
display_name, current_version_id, size_bytes, checksum, risk_level, visibility,
status, source_type, created_at, updated_at)
VALUES (?, ?, ?, ?, 'html', 'text/html', 'index.html', ?, ?, ?, ?, 'none',
'public_candidate', 'ready', 'generated', ?, ?)`,
[
bundleAssetId,
row.user_id,
row.space_id,
row.category_id,
`${row.title} · 发布快照`,
assetVersionId,
sizeBytes,
checksum,
now,
now,
],
);
await conn.query(
`INSERT INTO h5_asset_versions
(id, asset_id, version_no, storage_key, size_bytes, checksum, mime_type,
created_by, change_note, scan_status, created_at)
VALUES (?, ?, 1, ?, ?, ?, 'text/html', ?, '广场内容完善', 'passed', ?)`,
[assetVersionId, bundleAssetId, storageKey, sizeBytes, checksum, row.user_id, now],
);
await conn.query(`UPDATE h5_page_versions SET bundle_asset_id = ? WHERE id = ?`, [
bundleAssetId,
row.page_version_id,
]);
} else {
await conn.query(
`UPDATE h5_asset_versions
SET storage_key = ?, size_bytes = ?, checksum = ?, scan_status = 'passed'
WHERE asset_id = ? AND version_no = 1`,
[storageKey, sizeBytes, checksum, bundleAssetId],
);
await conn.query(
`UPDATE h5_assets SET size_bytes = ?, checksum = ?, updated_at = ? WHERE id = ?`,
[sizeBytes, checksum, now, bundleAssetId],
);
}
return { storageKey, checksum, sizeBytes };
}
async function main() {
const pool = createDbPool();
const [rows] = await pool.query(
`SELECT pp.id AS post_id, pp.title,
pr.id AS publication_id, pr.page_id, pr.page_version_id, pr.user_id, pr.status AS pub_status,
p.space_id, p.category_id, p.title AS page_title,
pv.bundle_asset_id,
(SELECT av.id FROM h5_asset_versions av
WHERE av.asset_id = pv.bundle_asset_id AND av.version_no = 1 LIMIT 1) AS bundle_version_id
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
JOIN h5_page_versions pv ON pv.id = pr.page_version_id
WHERE pp.status = 'published'
ORDER BY pp.hot_score DESC`,
);
const conn = await pool.getConnection();
let updated = 0;
try {
await conn.beginTransaction();
for (const row of rows) {
const { summary, html, skipHtml } = getEnrichment(row.post_id, row.title);
const summaryOnly = skipHtml && row.bundle_asset_id;
if (!summaryOnly) {
await ensureBundle(conn, row, html);
}
await conn.query(
`UPDATE plaza_posts SET summary = ?, updated_at = ? WHERE id = ?`,
[summary.slice(0, 500), Date.now(), row.post_id],
);
await conn.query(
`UPDATE h5_page_records SET title = ?, summary = ?, updated_at = ? WHERE id = ?`,
[row.title, summary.slice(0, 1000), Date.now(), row.page_id],
);
if (row.pub_status !== 'online') {
await conn.query(
`UPDATE h5_publish_records SET status = 'online', offline_at = NULL, updated_at = ? WHERE id = ?`,
[Date.now(), row.publication_id],
);
}
if (row.post_id === '4c1bf255-d7fd-4065-9044-d347579f85e4') {
await conn.query(`UPDATE plaza_posts SET title = ?, updated_at = ? WHERE id = ?`, [
'MindSpace 公开发布指南',
Date.now(),
row.post_id,
]);
}
updated += 1;
console.log(`${row.title}`);
}
await conn.commit();
} catch (error) {
await conn.rollback();
throw error;
} finally {
conn.release();
await pool.end();
}
console.log(`\n完成:已完善 ${updated} 张卡片的内容与摘要。`);
console.log('同步到线上:pnpm sync:plaza-prod -- --include-offline');
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack : error);
process.exit(1);
});