#!/usr/bin/env node /** * 扫描并补绑工作区中未完整交付的 Page Data 问卷页(publication + policy + dataset)。 * * Usage: * node scripts/repair-page-data-workspace-bindings.mjs * node scripts/repair-page-data-workspace-bindings.mjs --user-id * node scripts/repair-page-data-workspace-bindings.mjs --dry-run */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadH5Environment } from './load-env.mjs'; import { createDbPool } from '../db.mjs'; import { buildPublicUrl, PUBLISH_ROOT_DIR, resolvePublicBaseUrl } from '../user-publish.mjs'; import { resolveMindSpaceStorageRoot } from '../mindspace-runtime-config.mjs'; import { createPageService } from '../mindspace-pages.mjs'; import { assessPageDataHtmlBinding } from '../page-data-delivery-assess.mjs'; import { collectPageDataPublicHtmlFiles, maybeAutoBindPageDataHtmlPages, } from '../mindspace-page-data-finish-guard.mjs'; const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); loadH5Environment(import.meta.dirname); function parseArgs(argv) { const options = { userId: null, dryRun: false }; for (let i = 2; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--dry-run') options.dryRun = true; else if (arg === '--user-id' && argv[i + 1]) options.userId = argv[++i]; else if (arg === '-h' || arg === '--help') { console.log('Usage: node scripts/repair-page-data-workspace-bindings.mjs [--user-id ] [--dry-run]'); process.exit(0); } else { throw new Error(`未知参数: ${arg}`); } } return options; } function listWorkspaceUserIds(mindspaceRoot) { if (!fs.existsSync(mindspaceRoot)) return []; return fs .readdirSync(mindspaceRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && /^[0-9a-f-]{36}$/i.test(entry.name)) .map((entry) => entry.name); } async function assessUserWorkspace({ userId, pool, h5Root, storageRoot, publicBaseUrl }) { const publishDir = path.join(h5Root, 'MindSpace', userId); if (!fs.existsSync(publishDir)) return []; const pageService = createPageService(pool, { h5Root, storageRoot }); const broken = []; for (const file of collectPageDataPublicHtmlFiles(publishDir)) { if (file.evaluation.usage?.size === 0) continue; const assessment = await assessPageDataHtmlBinding({ pool, userId, publishDir, relativePath: file.relativePath, html: file.content, findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService), }); if (!assessment.bound) { broken.push({ userId, relativePath: file.relativePath, url: buildPublicUrl(publicBaseUrl, userId, file.relativePath), reasons: assessment.reasons, pageId: assessment.pageId, }); } } return broken; } async function main() { const options = parseArgs(process.argv); const pool = createDbPool(); const h5Root = repoRoot; const storageRoot = resolveMindSpaceStorageRoot(h5Root); const publicBaseUrl = resolvePublicBaseUrl(); const userIds = options.userId ? [options.userId] : listWorkspaceUserIds(path.join(h5Root, 'MindSpace')); const allBroken = []; for (const userId of userIds) { const broken = await assessUserWorkspace({ userId, pool, h5Root, storageRoot, publicBaseUrl, }); allBroken.push(...broken); } if (!allBroken.length) { console.log('未发现需要补绑的 Page Data 页面。'); await pool.end(); return; } console.log(`发现 ${allBroken.length} 个未完整绑定的 Page Data 页面:`); for (const item of allBroken) { console.log(`- [${item.userId}] ${item.relativePath}`); console.log(` url: ${item.url}`); console.log(` reasons: ${item.reasons.join(', ')}`); } if (options.dryRun) { console.log('\n(dry-run) 未执行补绑。'); await pool.end(); return; } const byUser = new Map(); for (const item of allBroken) { const list = byUser.get(item.userId) ?? []; list.push(item.relativePath); byUser.set(item.userId, list); } for (const [userId, relativePaths] of byUser.entries()) { const publishDir = path.join(h5Root, 'MindSpace', userId); const pageService = createPageService(pool, { h5Root, storageRoot }); const result = await maybeAutoBindPageDataHtmlPages({ pool, userId, publishDir, h5Root, storageRoot, onlyRelativePaths: relativePaths, findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService), }); console.log( `\n[user ${userId}] bound=${result.bound.length} skipped=${result.skipped.length} errors=${result.errors.length}`, ); for (const item of result.bound) { console.log(` ✓ ${item.relativePath} -> ${item.pageId}`); } for (const item of result.errors) { console.log(` ✗ ${item.relativePath}: ${item.message}`); } } await pool.end(); } main().catch(async (error) => { console.error(error instanceof Error ? error.stack ?? error.message : error); process.exit(1); });