Files
memind/scripts/repair-page-data-workspace-bindings.mjs
john 73b308af0a fix(page-data): enforce full workspace bind delivery and repair tooling
Prevent false-positive Finish Guard passes and sync-created fake bindings by assessing publication, policy, dataset, injection, and insert smoke before H5 delivery.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 15:30:52 +08:00

155 lines
5.1 KiB
JavaScript

#!/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 <uuid>
* 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 <uuid>] [--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);
});