From 357e26ab49879adfef023ea4d73931fa64dc4229 Mon Sep 17 00:00:00 2001 From: john Date: Wed, 16 Sep 2026 10:26:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(seo-geo):=20=E8=90=BD=E7=9B=98=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=A1=A5=E9=BD=90=E4=B8=8E=20143=20=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。 Co-authored-by: Cursor --- health-report-page.mjs | 6 +- mindspace-geo-backfill.mjs | 27 ++++--- mindspace-geo-meta.mjs | 62 +++++++++++++-- mindspace-geo-meta.test.mjs | 7 ++ mindspace-public-finish-sync.mjs | 6 +- mindspace-public-finish-sync.test.mjs | 14 ++-- mindspace-seo-audit.mjs | 31 ++++++++ mindspace-seo-geo-disk.mjs | 44 +++++++++++ mindspace-seo-geo-disk.test.mjs | 33 ++++++++ ...workspace-publication-delivery-service.mjs | 28 +++++++ ...pace-publication-delivery-service.test.mjs | 18 ++--- package.json | 3 +- scripts/backfill-mindspace-geo-prod.sh | 77 +++++++++++++++++++ scripts/backfill-mindspace-geo.mjs | 37 ++++++++- scripts/check-mindspace-cover.mjs | 17 +++- 15 files changed, 365 insertions(+), 45 deletions(-) create mode 100644 mindspace-seo-geo-disk.mjs create mode 100644 mindspace-seo-geo-disk.test.mjs create mode 100755 scripts/backfill-mindspace-geo-prod.sh 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(/]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? ''; } @@ -135,7 +135,7 @@ export function upsertMindspaceGeoMeta(html, geoPayload, { forceReplace = false const topic = String(rawH1FromHtml(html) || rawTitleFromHtml(html) || payload.summary).trim(); payload.faq = [{ q: `${topic}讲什么?`, a: payload.summary.slice(0, 220) }]; } - const metaTag = ``; + const metaTag = ``; const source = String(html ?? ''); if (hasMeta(source, 'mindspace-geo')) { @@ -342,8 +342,15 @@ export function backfillMindspaceSeoGeoHtml(html, options = {}) { next = upsertMindspaceCoverMeta(next, buildMinimalCoverMeta(next, { title })); } next = upsertDescriptionMeta(next, geoPayload.summary); - if (!hasMeta(next, 'mindspace-geo') || options.forceGeo) { - next = upsertMindspaceGeoMeta(next, geoPayload); + const existingGeo = parseMindspaceGeoMeta(next); + const needsGeoUpsert = + !hasMeta(next, 'mindspace-geo') || + options.forceGeo || + !String(existingGeo.summary ?? '').trim(); + if (needsGeoUpsert) { + next = upsertMindspaceGeoMeta(next, geoPayload, { + forceReplace: options.forceGeo || !String(existingGeo.summary ?? '').trim(), + }); } next = repairMindspaceGeoMeta(next); if (options.fixWarnings !== false) { diff --git a/mindspace-geo-meta.mjs b/mindspace-geo-meta.mjs index 43da1e4..8e62c6a 100644 --- a/mindspace-geo-meta.mjs +++ b/mindspace-geo-meta.mjs @@ -21,15 +21,65 @@ function sanitizeFaqItem(item) { return { q, a }; } +function decodeMetaAttribute(value) { + return String(value ?? '').replaceAll('"', '"').replaceAll('&', '&').replaceAll('<', '<'); +} + +function extractGeoMetaJsonRaw(html) { + const source = String(html ?? ''); + const marker = source.match(/]*name=["']mindspace-geo["'][^>]*content\s*=\s*/i); + if (!marker) return null; + let rest = source.slice(marker.index + marker[0].length); + if (rest[0] === '"') { + let raw = ''; + for (let i = 1; i < rest.length; i += 1) { + if (rest[i] === '&' && rest.slice(i, i + 6) === '"') { + raw += '"'; + i += 5; + continue; + } + if (rest[i] === '"') return raw; + raw += rest[i]; + } + return null; + } + const jsonStart = rest.indexOf('{'); + if (jsonStart < 0) return null; + let depth = 0; + let inString = false; + let escape = false; + for (let i = jsonStart; i < rest.length; i += 1) { + const ch = rest[i]; + if (inString) { + if (escape) { + escape = false; + continue; + } + if (ch === '\\') { + escape = true; + continue; + } + if (ch === '"') inString = false; + continue; + } + if (ch === '"') { + inString = true; + continue; + } + if (ch === '{') depth += 1; + else if (ch === '}') { + depth -= 1; + if (depth === 0) return rest.slice(jsonStart, i + 1); + } + } + return null; +} + export function parseMindspaceGeoMeta(html) { - const tag = String(html ?? '').match(/]*name=["']mindspace-geo["'][^>]*>/i)?.[0]; - if (!tag) return {}; - const contentMatch = - tag.match(/content=(["'])([\s\S]*?)\1/i) ?? tag.match(/content=["']([^"']+)["']/i); - const raw = contentMatch?.[2] ?? contentMatch?.[1]; + const raw = extractGeoMetaJsonRaw(html); if (!raw) return {}; try { - return JSON.parse(raw.replaceAll('"', '"')); + return JSON.parse(decodeMetaAttribute(raw)); } catch { return {}; } diff --git a/mindspace-geo-meta.test.mjs b/mindspace-geo-meta.test.mjs index 549129c..8ad3651 100644 --- a/mindspace-geo-meta.test.mjs +++ b/mindspace-geo-meta.test.mjs @@ -15,6 +15,13 @@ test('parseMindspaceGeoMeta reads author faq and keywords', () => { assert.equal(meta.faq.length, 1); }); +test('parseMindspaceGeoMeta reads legacy single-quoted json with apostrophes in faq', () => { + const html = ``; + const meta = parseMindspaceGeoMeta(html); + assert.equal(meta.summary, '业绩回顾'); + assert.equal(meta.faq[0].a, '单客户费比上限18%,超标须特批'); +}); + test('extractFaqFromHeadings builds faq from h2 and paragraph', () => { const html = `

最佳季节

春秋两季气候最舒适,适合徒步与摄影。

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'), - 'Summer', - ); + const saved = fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8'); + assert.match(saved, /Summer<\/title>/i); + assert.match(saved, /name="mindspace-geo"/); } finally { fs.rmSync(publishDir, { recursive: true, force: true }); } @@ -420,10 +419,9 @@ test('materializeMissingPublicHtmlWrites writes html from developer write tool c ]; const result = materializeMissingPublicHtmlWrites({ messages, publishDir }); assert.deepEqual(result.materialized, ['public/guizhou-guide.html']); - assert.equal( - fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8'), - '<!doctype html><title>Guizhou', - ); + const saved = fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8'); + assert.match(saved, /Guizhou<\/title>/i); + assert.match(saved, /name="mindspace-geo"/); } finally { fs.rmSync(publishDir, { recursive: true, force: true }); } diff --git a/mindspace-seo-audit.mjs b/mindspace-seo-audit.mjs index 346f0ce..82b8adb 100644 --- a/mindspace-seo-audit.mjs +++ b/mindspace-seo-audit.mjs @@ -163,6 +163,29 @@ export function collectMindspaceHtmlFiles(root, userId = null) { return files; } +export function collectStoragePublicationHtmlFiles(storageRoot, userId = null) { + const files = []; + const usersDir = path.join(storageRoot, 'users'); + if (!fs.existsSync(usersDir)) return files; + + const userDirs = userId + ? [path.join(usersDir, userId)] + : fs.readdirSync(usersDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')) + .map((entry) => path.join(usersDir, entry.name)); + + for (const userDir of userDirs) { + const publicationsDir = path.join(userDir, 'publications'); + if (!fs.existsSync(publicationsDir)) continue; + for (const publication of fs.readdirSync(publicationsDir, { withFileTypes: true })) { + if (!publication.isDirectory() || publication.name.startsWith('.')) continue; + const indexHtml = path.join(publicationsDir, publication.name, 'index.html'); + if (fs.existsSync(indexHtml)) files.push(indexHtml); + } + } + return files; +} + export function collectPlatformHtmlFiles(publicRoot) { const files = []; if (!fs.existsSync(publicRoot)) return files; @@ -182,17 +205,25 @@ export function collectHtmlTargets({ scope = 'mindspace', mindspaceRoot, publicRoot, + storageRoot = null, userId = null, file = null, } = {}) { if (file) return [path.resolve(file)]; const normalized = String(scope ?? 'mindspace').trim().toLowerCase(); + const storageFiles = storageRoot + ? collectStoragePublicationHtmlFiles(storageRoot, userId) + : []; if (normalized === 'platform') { return collectPlatformHtmlFiles(publicRoot); } + if (normalized === 'storage') { + return storageFiles; + } if (normalized === 'all') { return [ ...collectMindspaceHtmlFiles(mindspaceRoot, userId), + ...storageFiles, ...collectPlatformHtmlFiles(publicRoot), ]; } diff --git a/mindspace-seo-geo-disk.mjs b/mindspace-seo-geo-disk.mjs new file mode 100644 index 0000000..53def01 --- /dev/null +++ b/mindspace-seo-geo-disk.mjs @@ -0,0 +1,44 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { backfillMindspaceSeoGeoHtml } from './mindspace-geo-backfill.mjs'; + +function isPublicHtmlPath(value) { + const normalized = String(value ?? '').replace(/\\/g, '/'); + return /(?:^|\/)public\/[^/]+\.html$/i.test(normalized); +} + +export function prepareMindspacePublicHtmlForDisk(html, { htmlFilePath = '' } = {}) { + const source = String(html ?? ''); + const filePath = String(htmlFilePath ?? ''); + if (!source.trim()) return source; + if (filePath && !isPublicHtmlPath(filePath)) return source; + if (!filePath && !/<html[\s>]/i.test(source)) return source; + return backfillMindspaceSeoGeoHtml(source, { + htmlFilePath: filePath, + fixWarnings: true, + }); +} + +export function materializeMindspaceSeoGeoOnDisk( + absolutePath, + { + forceGeo = false, + fixWarnings = true, + } = {}, +) { + const filePath = path.resolve(String(absolutePath ?? '')); + if (!filePath || !/\.html$/i.test(filePath)) { + return { changed: false, html: '' }; + } + const before = fs.readFileSync(filePath, 'utf8'); + const after = backfillMindspaceSeoGeoHtml(before, { + htmlFilePath: filePath, + forceGeo, + fixWarnings, + }); + if (after === before) { + return { changed: false, html: before }; + } + fs.writeFileSync(filePath, after, 'utf8'); + return { changed: true, html: after }; +} diff --git a/mindspace-seo-geo-disk.test.mjs b/mindspace-seo-geo-disk.test.mjs new file mode 100644 index 0000000..bb3ec5d --- /dev/null +++ b/mindspace-seo-geo-disk.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { + materializeMindspaceSeoGeoOnDisk, + prepareMindspacePublicHtmlForDisk, +} from './mindspace-seo-geo-disk.mjs'; + +test('prepareMindspacePublicHtmlForDisk injects mindspace-geo for public html', () => { + const html = '<html><head><title>Demo

Demo

'; + const next = prepareMindspacePublicHtmlForDisk(html, { + htmlFilePath: '/tmp/user/public/demo.html', + }); + assert.match(next, /name="mindspace-geo"/); + assert.match(next, /name="description"/); +}); + +test('materializeMindspaceSeoGeoOnDisk writes backfilled html to disk', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seo-geo-disk-')); + const filePath = path.join(dir, 'public', 'page.html'); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync( + filePath, + 'Disk Demo

Disk Demo

', + 'utf8', + ); + const result = materializeMindspaceSeoGeoOnDisk(filePath); + assert.equal(result.changed, true); + const saved = fs.readFileSync(filePath, 'utf8'); + assert.match(saved, /name="mindspace-geo"/); +}); diff --git a/mindspace-workspace-publication-delivery-service.mjs b/mindspace-workspace-publication-delivery-service.mjs index 57c40a8..d188ce9 100644 --- a/mindspace-workspace-publication-delivery-service.mjs +++ b/mindspace-workspace-publication-delivery-service.mjs @@ -6,6 +6,7 @@ import { materializePrivateAssetsInWorkspaceHtml, resolveClosestHtmlRelativePath, } from './mindspace-chat-save.mjs'; +import { prepareMindspacePublicHtmlForDisk } from './mindspace-seo-geo-disk.mjs'; import { scanWorkspaceFilesForProhibitedBrowserStorage, } from './mindspace-browser-storage-policy.mjs'; @@ -105,9 +106,29 @@ export function createMindSpaceWorkspacePublicationDeliveryService({ scanWorkspaceFilesForProhibitedBrowserStorageFn = scanWorkspaceFilesForProhibitedBrowserStorage, readFileFn = fsPromises.readFile, + writeFileFn = fsPromises.writeFile, statFn = fsPromises.stat, existsSyncFn = fs.existsSync, + prepareMindspaceSeoGeoForDeliveryFn = null, } = {}) { + const enrichMindspaceHtmlForDelivery = + prepareMindspaceSeoGeoForDeliveryFn + ?? (async ({ html, absolutePath }) => { + const enriched = prepareMindspacePublicHtmlForDisk(html, { + htmlFilePath: absolutePath, + }); + if (enriched === html) return html; + try { + await writeFileFn(absolutePath, enriched, 'utf8'); + } catch (error) { + logger?.warn?.( + `[MindSpace] SEO/GEO disk backfill failed for ${absolutePath}: ${ + error?.message || error + }`, + ); + } + return enriched; + }); if (!pool || typeof pool.query !== 'function') { throw new Error( 'createMindSpaceWorkspacePublicationDeliveryService requires a database pool', @@ -224,6 +245,13 @@ export function createMindSpaceWorkspacePublicationDeliveryService({ } } + html = await enrichMindspaceHtmlForDelivery({ + html, + absolutePath: readablePath, + readFileFn, + writeFileFn, + }); + const thumbnailRelativePath = relativePath.replace( /\.html$/i, diff --git a/mindspace-workspace-publication-delivery-service.test.mjs b/mindspace-workspace-publication-delivery-service.test.mjs index 5bf78cc..e0b4fb5 100644 --- a/mindspace-workspace-publication-delivery-service.test.mjs +++ b/mindspace-workspace-publication-delivery-service.test.mjs @@ -144,18 +144,12 @@ test('resolves workspace HTML into a logical delivery without exposing a physica requestPath: '/user-1/public/page.html', }); - assert.deepEqual(result, { - action: 'deliver', - kind: 'html', - ownerId: 'user-1', - relativePath: 'public/page.html', - servedName: 'page.html', - canonicalPath: - '/MindSpace/user-1/public/page.html', - thumbnailName: 'page.thumbnail.png', - html: - '

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) {