diff --git a/health-report-page.mjs b/health-report-page.mjs index ce60956..09525a4 100644 --- a/health-report-page.mjs +++ b/health-report-page.mjs @@ -3,6 +3,7 @@ import path from 'node:path'; import { buildHealthAssessSummary } from './health-assess-summary.mjs'; import { buildHealthTimeline } from './health-timeline.mjs'; import { buildMindSpacePublicUrlForUser } from './mindspace-runtime-config.mjs'; +import { prepareMindspacePublicHtmlForDisk } from './mindspace-seo-geo-disk.mjs'; import { resolvePublishDir } from './user-publish.mjs'; import { looksLikeHealthReportPageRequest } from './health-channel-aliases.mjs'; @@ -49,7 +50,10 @@ export function writeHealthPublicHtmlPage({ const publishDir = resolvePublishDir(h5Root, { id: String(userId) }); const absolutePath = path.join(publishDir, clean); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); - fs.writeFileSync(absolutePath, html, 'utf8'); + const preparedHtml = prepareMindspacePublicHtmlForDisk(String(html ?? ''), { + htmlFilePath: absolutePath, + }); + fs.writeFileSync(absolutePath, preparedHtml, 'utf8'); if (!fs.existsSync(absolutePath) || fs.statSync(absolutePath).size < minSize) { const error = Object.assign(new Error('健康页写入失败'), { code: 'health_page_write_failed' }); throw error; diff --git a/mindspace-geo-backfill.mjs b/mindspace-geo-backfill.mjs index 2b60f91..c0bd8c7 100644 --- a/mindspace-geo-backfill.mjs +++ b/mindspace-geo-backfill.mjs @@ -22,13 +22,6 @@ import { hasRasterShareImageHint } from './wechat/verify/share-preview.mjs'; export const DEFAULT_RASTER_COVER_URL = 'https://m.tkmind.cn/brand/tkmind-icon.png'; -function stripHtml(value) { - return String(value ?? '') - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); -} - function escapeMetaAttribute(value) { return String(value ?? '') .replaceAll('&', '&') @@ -36,6 +29,13 @@ function escapeMetaAttribute(value) { .replaceAll('<', '<'); } +function stripHtml(value) { + return String(value ?? '') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + function rawTitleFromHtml(html) { return String(html ?? '').match(/
春秋两季气候最舒适,适合徒步与摄影。
diff --git a/mindspace-public-finish-sync.mjs b/mindspace-public-finish-sync.mjs index 30f168a..6704e58 100644 --- a/mindspace-public-finish-sync.mjs +++ b/mindspace-public-finish-sync.mjs @@ -4,6 +4,7 @@ import path from 'node:path'; import { extractStaticPageLinks, materializePrivateAssetsInPublicHtmlFiles } from './mindspace-chat-save.mjs'; import { DOWNLOADABLE_FILE_PATTERN } from './mindspace-html-download-links.mjs'; import { scheduleWorkspaceHtmlThumbnailSidecars } from './mindspace-workspace-thumbnails.mjs'; +import { prepareMindspacePublicHtmlForDisk } from './mindspace-seo-geo-disk.mjs'; import { repairSharePreviewHtml } from './wechat/verify/share-preview-repair.mjs'; import { verifySharePreviewMeta } from './wechat/verify/share-preview.mjs'; @@ -408,9 +409,12 @@ export function materializeMissingPublicHtmlWrites({ messages, publishDir }) { try { fs.mkdirSync(path.dirname(destination), { recursive: true }); const rawContent = String(artifact.content ?? ''); - const preparedContent = shouldRepairMaterializedPublicHtml(rawContent) + const repairedContent = shouldRepairMaterializedPublicHtml(rawContent) ? repairSharePreviewHtml(rawContent, {}) : rawContent; + const preparedContent = prepareMindspacePublicHtmlForDisk(repairedContent, { + htmlFilePath: destination, + }); fs.writeFileSync(destination, preparedContent, 'utf8'); materialized.push(artifact.relativePath); scheduleWorkspaceHtmlThumbnailSidecars(root, artifact.relativePath); diff --git a/mindspace-public-finish-sync.test.mjs b/mindspace-public-finish-sync.test.mjs index a3985c5..4290424 100644 --- a/mindspace-public-finish-sync.test.mjs +++ b/mindspace-public-finish-sync.test.mjs @@ -185,10 +185,9 @@ test('materializeMissingPublicHtmlWrites writes html from sandbox write_file too ]; const result = materializeMissingPublicHtmlWrites({ messages, publishDir }); assert.deepEqual(result.materialized, ['public/summer-essay.html']); - assert.equal( - fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8'), - 'localized
', - }); + assert.equal(result.action, 'deliver'); + assert.equal(result.kind, 'html'); + assert.equal(result.ownerId, 'user-1'); + assert.equal(result.relativePath, 'public/page.html'); + assert.match(result.html, /localized/); + assert.match(result.html, /name="mindspace-geo"/); assert.equal( Object.hasOwn(result, 'filePath'), false, diff --git a/package.json b/package.json index 210ee81..258c4fe 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "check:mindspace-cover": "node scripts/check-mindspace-cover.mjs --scope all", "check:mindspace-cover:seo": "node scripts/check-mindspace-cover.mjs --scope all", "backfill:mindspace-geo": "node scripts/backfill-mindspace-geo.mjs --scope all", + "backfill:mindspace-geo:143": "bash scripts/backfill-mindspace-geo-prod.sh", "demo:thumbnails": "node scripts/thumbnail-preview-demo.mjs", "audit:conversation-packages": "node scripts/audit-conversation-packages.mjs", "audit:memory-v2-shadow": "node scripts/audit-memory-v2-shadow.mjs", @@ -118,7 +119,7 @@ "verify:mindspace-wechat-mp": "node --test mindspace-wechat-mp-config.test.mjs mindspace-wechat-page-draft.test.mjs wechat-draft-publication-standard.test.mjs mindspace-chat-wechat-draft.test.mjs mindspace-public-share-widget.test.mjs server/portal-mindspace-wechat-routes.test.mjs server/portal-mindspace-chat-share-routes.test.mjs", "verify:portal-access-policy": "node scripts/verify-portal-access-policy.mjs", "verify:seo-discovery": "node scripts/verify-seo-discovery.mjs", - "verify:seo-geo": "node --test mindspace-index-policy.test.mjs mindspace-seo-tags.test.mjs mindspace-geo-meta.test.mjs mindspace-geo-backfill.test.mjs mindspace-geo-tags.test.mjs mindspace-seo-geo-delivery.test.mjs mindspace-seo-discovery-service.test.mjs mindspace-seo-notify.test.mjs mindspace-config.test.mjs server/portal-seo-discovery-routes.test.mjs plaza-public.test.mjs", + "verify:seo-geo": "node --test mindspace-index-policy.test.mjs mindspace-seo-tags.test.mjs mindspace-geo-meta.test.mjs mindspace-geo-backfill.test.mjs mindspace-geo-tags.test.mjs mindspace-seo-geo-disk.test.mjs mindspace-seo-geo-delivery.test.mjs mindspace-seo-discovery-service.test.mjs mindspace-seo-notify.test.mjs mindspace-config.test.mjs server/portal-seo-discovery-routes.test.mjs plaza-public.test.mjs", "verify:public-page-interaction": "node scripts/verify-public-page-interaction.mjs", "verify:page-data": "node --test mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.test.mjs page-data-acceptance.test.mjs page-data-integration.test.mjs page-data-public-service.test.mjs page-data-ops.test.mjs page-data-delivery-assess.test.mjs page-data-delivery-code-review.test.mjs mindspace-page-data-finish-guard.test.mjs mindspace-page-data-finish-guard.integration.test.mjs", "verify:excel-analyst": "node --test excel-analyst.test.mjs skills-registry.test.mjs skill-runtime-policy.test.mjs capabilities.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs session-reconcile.test.mjs tkmind-proxy-attachment.test.mjs", diff --git a/scripts/backfill-mindspace-geo-prod.sh b/scripts/backfill-mindspace-geo-prod.sh new file mode 100755 index 0000000..0cfb91e --- /dev/null +++ b/scripts/backfill-mindspace-geo-prod.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Batch backfill SEO/GEO for all MindSpace HTML on production host 143. +# 143 运行的是 release runtime 包(无 .git),因此从本机 rsync 工具脚本后再执行回填。 +# +# Usage (from laptop): +# bash scripts/backfill-mindspace-geo-prod.sh +# bash scripts/backfill-mindspace-geo-prod.sh --dry-run + +HOST="${MEMIND_PROD_HOST:-john@180.159.29.143}" +LOCAL_REPO="$(cd "$(dirname "$0")/.." && pwd)" +REMOTE_REPO="${MEMIND_PROD_REPO:-/Users/john/Project/Memind}" +REMOTE_MINDSPACE_ROOT="${MEMIND_PROD_MINDSPACE_ROOT:-${REMOTE_REPO}/MindSpace}" +REMOTE_STORAGE_ROOT="${MEMIND_PROD_STORAGE_ROOT:-/Users/john/MindSpace/data/mindspace}" +REMOTE_PUBLIC_ROOT="${MEMIND_PROD_PUBLIC_ROOT:-${REMOTE_REPO}/public}" +REMOTE_NODE="${MEMIND_PROD_NODE:-/opt/homebrew/Cellar/node@22/22.22.3/bin/node}" +EXTRA_ARGS=("$@") + +echo "==> 143 SEO/GEO backfill" +echo " host=${HOST}" +echo " local=${LOCAL_REPO}" +echo " remote=${REMOTE_REPO}" +echo " mindspace=${REMOTE_MINDSPACE_ROOT}" +echo " storage=${REMOTE_STORAGE_ROOT}" +echo " public=${REMOTE_PUBLIC_ROOT}" +echo " node=${REMOTE_NODE}" + +SEO_FILES=( + chat-image-materialize.mjs + chat-image-turn-scope.mjs + html-document-injection.mjs + mindspace-cover-meta.mjs + mindspace-geo-backfill.mjs + mindspace-geo-meta.mjs + mindspace-og-tags.mjs + mindspace-page-tag.mjs + mindspace-seo-audit.mjs + mindspace-seo-tags.mjs + mindspace-thumbnail-png.mjs + mindspace-thumbnails.mjs + mindspace-workspace-thumbnails.mjs + scripts/backfill-mindspace-geo.mjs + scripts/check-mindspace-cover.mjs + wechat/verify/page-artifact.mjs + wechat/verify/share-preview.mjs +) + +ssh "${HOST}" "mkdir -p '${REMOTE_REPO}/scripts' '${REMOTE_REPO}/wechat/verify'" +for rel in "${SEO_FILES[@]}"; do + rsync -az "${LOCAL_REPO}/${rel}" "${HOST}:${REMOTE_REPO}/${rel}" +done + +ssh "${HOST}" "set -eo pipefail + NODE_BIN='${REMOTE_NODE}' + if ! test -x \"\${NODE_BIN}\"; then + NODE_BIN=\$(ls /opt/homebrew/Cellar/node@22/*/bin/node 2>/dev/null | head -1) + fi + if ! test -x \"\${NODE_BIN}\"; then + NODE_BIN=\$(ls /Users/john/.local/share/fnm/node-versions/*/installation/bin/node 2>/dev/null | head -1) + fi + test -x \"\${NODE_BIN}\" || { echo 'node not found on 143' >&2; exit 127; } + cd '${REMOTE_REPO}' + \"\${NODE_BIN}\" scripts/backfill-mindspace-geo.mjs \\ + --scope all \\ + --root '${REMOTE_MINDSPACE_ROOT}' \\ + --storage-root '${REMOTE_STORAGE_ROOT}' \\ + --public-root '${REMOTE_PUBLIC_ROOT}' \\ + ${EXTRA_ARGS[*]:-} + \"\${NODE_BIN}\" scripts/check-mindspace-cover.mjs \\ + --scope all \\ + --root '${REMOTE_MINDSPACE_ROOT}' \\ + --storage-root '${REMOTE_STORAGE_ROOT}' \\ + --public-root '${REMOTE_PUBLIC_ROOT}' +" + +echo "==> done" diff --git a/scripts/backfill-mindspace-geo.mjs b/scripts/backfill-mindspace-geo.mjs index abcf9e9..70eae4c 100644 --- a/scripts/backfill-mindspace-geo.mjs +++ b/scripts/backfill-mindspace-geo.mjs @@ -20,6 +20,7 @@ function parseArgs(argv) { let scope = 'all'; let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT); let publicRoot = path.join(repoRoot, 'public'); + let storageRoot = null; let userId = null; let file = null; let dryRun = false; @@ -40,6 +41,9 @@ function parseArgs(argv) { } else if (arg === '--public-root' && argv[i + 1]) { publicRoot = path.resolve(argv[i + 1]); i += 1; + } else if (arg === '--storage-root' && argv[i + 1]) { + storageRoot = path.resolve(argv[i + 1]); + i += 1; } else if (arg === '--user' && argv[i + 1]) { userId = argv[i + 1]; i += 1; @@ -52,11 +56,38 @@ function parseArgs(argv) { } } - return { scope, root, publicRoot, userId, file, dryRun, forceGeo, fixWarnings }; + return { + scope, + root, + publicRoot, + storageRoot, + userId, + file, + dryRun, + forceGeo, + fixWarnings, + }; } -const { scope, root, publicRoot, userId, file, dryRun, forceGeo, fixWarnings } = parseArgs(process.argv); -const targets = collectHtmlTargets({ scope, mindspaceRoot: root, publicRoot, userId, file }); +const { + scope, + root, + publicRoot, + storageRoot, + userId, + file, + dryRun, + forceGeo, + fixWarnings, +} = parseArgs(process.argv); +const targets = collectHtmlTargets({ + scope, + mindspaceRoot: root, + publicRoot, + storageRoot, + userId, + file, +}); let changed = 0; let skipped = 0; diff --git a/scripts/check-mindspace-cover.mjs b/scripts/check-mindspace-cover.mjs index 1e4006a..1da6c77 100644 --- a/scripts/check-mindspace-cover.mjs +++ b/scripts/check-mindspace-cover.mjs @@ -23,6 +23,7 @@ function parseArgs(argv) { let scope = 'all'; let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT); let publicRoot = path.join(repoRoot, 'public'); + let storageRoot = null; let userId = null; let file = null; let seo = true; @@ -36,6 +37,9 @@ function parseArgs(argv) { } else if (argv[i] === '--public-root' && argv[i + 1]) { publicRoot = path.resolve(argv[i + 1]); i += 1; + } else if (argv[i] === '--storage-root' && argv[i + 1]) { + storageRoot = path.resolve(argv[i + 1]); + i += 1; } else if (argv[i] === '--user' && argv[i + 1]) { userId = argv[i + 1]; i += 1; @@ -51,7 +55,7 @@ function parseArgs(argv) { process.exit(0); } } - return { scope, root, publicRoot, userId, file, seo }; + return { scope, root, publicRoot, storageRoot, userId, file, seo }; } function isPlatformOnlyPath(htmlPath, publicRoot) { @@ -60,8 +64,15 @@ function isPlatformOnlyPath(htmlPath, publicRoot) { return resolved.startsWith(`${resolvedPublicRoot}${path.sep}`); } -const { scope, root, publicRoot, userId, file, seo } = parseArgs(process.argv); -const targets = collectHtmlTargets({ scope, mindspaceRoot: root, publicRoot, userId, file }); +const { scope, root, publicRoot, storageRoot, userId, file, seo } = parseArgs(process.argv); +const targets = collectHtmlTargets({ + scope, + mindspaceRoot: root, + publicRoot, + storageRoot, + userId, + file, +}); const results = []; for (const htmlPath of targets) {