fix: gate public page download links before Portal release

Add a MindSpace public HTML checker for missing companion docx/PDF paths, wire it into runtime packaging and release-portal-runtime-prod.sh, and document the workflow for agents and ops.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-01 12:40:59 +08:00
parent 16828a58f7
commit b4a2b2bdaf
10 changed files with 368 additions and 3 deletions
+17
View File
@@ -173,6 +173,19 @@ async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir, sourceNodeModul
await visit(runtimeNodeModulesDir);
}
async function copyMindspacePublicLinkTools() {
console.log('==> 拷贝 MindSpace 公开页链接检查工具');
await fs.copyFile(
path.join(root, 'mindspace-public-links.mjs'),
path.join(runtimeRoot, 'mindspace-public-links.mjs'),
);
await fs.mkdir(path.join(runtimeRoot, 'scripts'), { recursive: true });
await fs.copyFile(
path.join(root, 'scripts', 'check-mindspace-public-links.mjs'),
path.join(runtimeRoot, 'scripts', 'check-mindspace-public-links.mjs'),
);
}
async function writeMetadata() {
const packageJson = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
const runtimePackageJson = {
@@ -233,6 +246,9 @@ async function writeMetadata() {
'Bundled alongside server.mjs (required for sandbox-fs MCP):',
' mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)',
'',
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
' Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.',
'',
'Key runtime differences must stay in .env, not in the artifact:',
' DATABASE_URL / MYSQL_*',
' H5_PUBLIC_BASE_URL',
@@ -254,6 +270,7 @@ async function main() {
await bundleServer();
await bundleSandboxMcp();
await copyNodeModules();
await copyMindspacePublicLinkTools();
await writeMetadata();
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
console.log('');
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env node
/**
* Scan MindSpace public/*.html for broken relative links.
*
* Default (--downloads-only): attachment / download links only (release gate).
* --all-links: also check img/src and mindspace-cover assets.
*
* Usage:
* node scripts/check-mindspace-public-links.mjs
* node scripts/check-mindspace-public-links.mjs --user <uuid>
* node scripts/check-mindspace-public-links.mjs --root /path/to/MindSpace
* node scripts/check-mindspace-public-links.mjs --all-links
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { scanPublicHtmlLinks } from '../mindspace-public-links.mjs';
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const DEFAULT_PUBLISH_ROOT = 'MindSpace';
function parseArgs(argv) {
let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT);
let userId = null;
let allLinks = false;
for (let i = 2; i < argv.length; i += 1) {
if (argv[i] === '--root' && argv[i + 1]) {
root = path.resolve(argv[i + 1]);
i += 1;
} else if (argv[i] === '--user' && argv[i + 1]) {
userId = argv[i + 1];
i += 1;
} else if (argv[i] === '--all-links') {
allLinks = true;
} else if (argv[i] === '--downloads-only') {
allLinks = false;
} else if (argv[i] === '--help' || argv[i] === '-h') {
console.log(
`Usage: node scripts/check-mindspace-public-links.mjs [--root ${DEFAULT_PUBLISH_ROOT}] [--user <uuid>] [--downloads-only|--all-links]`,
);
process.exit(0);
}
}
return { root, userId, downloadsOnly: !allLinks };
}
const { root, userId, downloadsOnly } = parseArgs(process.argv);
const issues = scanPublicHtmlLinks(root, { userId, downloadsOnly });
const modeLabel = downloadsOnly ? 'download/attachment links' : 'all relative links';
if (issues.length === 0) {
console.log(`OK: no missing ${modeLabel} under ${root}${userId ? ` (user ${userId})` : ''}`);
process.exit(0);
}
console.error(`Found ${issues.length} missing ${modeLabel} under ${root}:`);
for (const issue of issues) {
const relHtml = path.relative(root, issue.htmlPath);
console.error(`- ${relHtml}: ${issue.ref} (${issue.source})`);
}
process.exit(1);
+38 -1
View File
@@ -139,7 +139,7 @@ fi
verify_runtime_artifact() {
local missing=0
for required in server.mjs mindspace-sandbox-mcp.mjs dist package.json scripts/run-memind-portal-prod.sh; do
for required in server.mjs mindspace-sandbox-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/check-mindspace-public-links.mjs; do
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
missing=1
@@ -153,6 +153,29 @@ verify_runtime_artifact() {
verify_runtime_artifact
verify_mindspace_public_links() {
local target_root="${1:-${ROOT}/MindSpace}"
local label="${2:-本地 MindSpace}"
if [[ "${ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES:-0}" == "1" ]]; then
say "跳过 ${label} 公开页链接检查(ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1"
return 0
fi
if [[ ! -d "${target_root}" ]]; then
say "跳过 ${label} 公开页链接检查(目录不存在: ${target_root}"
return 0
fi
say "检查 ${label} 公开页相对链接"
if ! node "${ROOT}/scripts/check-mindspace-public-links.mjs" --root "${target_root}" --downloads-only; then
echo "MindSpace 公开页存在缺失的相对下载/资源链接。" >&2
echo "修复 public/*.html 中的 href/src/cover 路径,或临时设置 ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1 跳过。" >&2
exit 1
fi
}
if [[ "${DRY_RUN}" -ne 1 ]]; then
verify_mindspace_public_links "${ROOT}/MindSpace" "本地"
fi
verify_remote_goosed_dependency() {
say "检查 103 goosed 依赖"
ssh -o BatchMode=yes "${HOST}" 'bash -s' <<'REMOTE'
@@ -435,10 +458,24 @@ if [[ "${portal_code}" != "200" ]]; then
exit 1
fi
if [[ "${ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES:-0}" != "1" && -d "${APP_DIR}/MindSpace" && -f "${APP_DIR}/scripts/check-mindspace-public-links.mjs" ]]; then
say "检查 MindSpace 公开页相对链接"
node_bin="/opt/homebrew/opt/node@24/bin/node"
if [[ ! -x "${node_bin}" ]]; then
node_bin="$(command -v node)"
fi
if ! "${node_bin}" "${APP_DIR}/scripts/check-mindspace-public-links.mjs" --root "${APP_DIR}/MindSpace" --downloads-only; then
echo "MindSpace public link check failed: broken relative download/asset links under public/*.html" >&2
echo "Set ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1 only if you accept shipping with known broken links." >&2
exit 1
fi
fi
say "检查 live 目录中不再保留源码树"
allowed_live_mjs=(
"${APP_DIR}/server.mjs"
"${APP_DIR}/mindspace-sandbox-mcp.mjs"
"${APP_DIR}/mindspace-public-links.mjs"
)
extra_files=""
while IFS= read -r file; do