feat(seo-geo): 落盘自动补齐与 143 全量回填支持
Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
+17
-10
@@ -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[^>]*>([^<]*)<\/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 = `<meta name="mindspace-geo" content='${JSON.stringify(payload).replace(/'/g, ''')}'>`;
|
||||
const metaTag = `<meta name="mindspace-geo" content="${escapeMetaAttribute(JSON.stringify(payload))}">`;
|
||||
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) {
|
||||
|
||||
+56
-6
@@ -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(/<meta[^>]*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(/<meta[^>]*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 {};
|
||||
}
|
||||
|
||||
@@ -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 = `<meta name="mindspace-geo" content='{"summary":"业绩回顾","keywords":["报告"],"faq":[{"q":"费比规则?","a":"单客户费比上限18%,超标须特批"}]}'>`;
|
||||
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 = `<body>
|
||||
<h2>最佳季节</h2><p>春秋两季气候最舒适,适合徒步与摄影。</p>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
'<!doctype html><title>Summer</title>',
|
||||
);
|
||||
const saved = fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8');
|
||||
assert.match(saved, /<!doctype html><title>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</title>',
|
||||
);
|
||||
const saved = fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8');
|
||||
assert.match(saved, /<!doctype html><title>Guizhou<\/title>/i);
|
||||
assert.match(saved, /name="mindspace-geo"/);
|
||||
} finally {
|
||||
fs.rmSync(publishDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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</title></head><body><h1>Demo</h1></body></html>';
|
||||
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,
|
||||
'<html><head><title>Disk Demo</title></head><body><h1>Disk Demo</h1></body></html>',
|
||||
'utf8',
|
||||
);
|
||||
const result = materializeMindspaceSeoGeoOnDisk(filePath);
|
||||
assert.equal(result.changed, true);
|
||||
const saved = fs.readFileSync(filePath, 'utf8');
|
||||
assert.match(saved, /name="mindspace-geo"/);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
'<!doctype html><p>localized</p>',
|
||||
});
|
||||
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,
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
Executable
+77
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user