feat(seo-geo): 落盘自动补齐与 143 全量回填支持
Memind CI / Test, build, and release guards (pull_request) Failing after 7m12s
Memind CI / Test, build, and release guards (push) Has been cancelled

Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-16 10:26:02 +08:00
parent acca846856
commit 357e26ab49
15 changed files with 365 additions and 45 deletions
+77
View File
@@ -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"
+34 -3
View File
@@ -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;
+14 -3
View File
@@ -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) {