b0f5d6a51c
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>
171 lines
6.0 KiB
JavaScript
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);
|
|
});
|