feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
Memind CI / Test, build, and release guards (push) Has been cancelled

Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-10 08:06:12 +08:00
parent 7c65540366
commit fde6503bdf
158 changed files with 9194 additions and 270 deletions
+109
View File
@@ -1,7 +1,12 @@
import crypto from 'node:crypto';
const PUBLIC_PAGE_LIMIT_KEY = 'public_page_limit';
const SEO_GEO_CONFIG_KEY = 'seo_geo_config';
const ANALYTICS_ENABLED_KEY = 'analytics_enabled';
const CONFIG_CACHE_TTL_MS = 5_000;
let cachedMindSpaceConfig = null;
let cachedMindSpaceConfigAt = 0;
const ANALYTICS_WEBSITE_ID_KEY = 'analytics_website_id';
const ANALYTICS_URL_KEY = 'analytics_url';
const ANALYTICS_DOMAINS_KEY = 'analytics_domains';
@@ -35,6 +40,66 @@ function asPositiveInteger(value, fallback) {
return Math.floor(parsed);
}
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
return /^(1|true|yes|on)$/i.test(String(value));
}
function parseJsonObject(value, fallback = {}) {
if (value == null || value === '') return structuredClone?.(fallback) ?? JSON.parse(JSON.stringify(fallback));
if (typeof value === 'object') return structuredClone?.(value) ?? JSON.parse(JSON.stringify(value));
try {
const parsed = JSON.parse(String(value));
return typeof parsed === 'object' && parsed ? parsed : structuredClone?.(fallback) ?? JSON.parse(JSON.stringify(fallback));
} catch {
return structuredClone?.(fallback) ?? JSON.parse(JSON.stringify(fallback));
}
}
export function defaultSeoGeoConfig() {
return {
enabled: false,
seo: {
enabled: false,
canonical: true,
sitemap: false,
robotsTxt: false,
baiduPush: false,
},
geo: {
enabled: false,
jsonLd: false,
llmsTxt: false,
},
};
}
export function normalizeSeoGeoConfig(input = null) {
const defaults = defaultSeoGeoConfig();
const source = parseJsonObject(input, defaults);
return {
enabled: normalizeBoolean(source.enabled, defaults.enabled),
seo: {
enabled: normalizeBoolean(source.seo?.enabled, defaults.seo.enabled),
canonical: normalizeBoolean(source.seo?.canonical, defaults.seo.canonical),
sitemap: normalizeBoolean(source.seo?.sitemap, defaults.seo.sitemap),
robotsTxt: normalizeBoolean(source.seo?.robotsTxt, defaults.seo.robotsTxt),
baiduPush: normalizeBoolean(source.seo?.baiduPush, defaults.seo.baiduPush),
},
geo: {
enabled: normalizeBoolean(source.geo?.enabled, defaults.geo.enabled),
jsonLd: normalizeBoolean(source.geo?.jsonLd, defaults.geo.jsonLd),
llmsTxt: normalizeBoolean(source.geo?.llmsTxt, defaults.geo.llmsTxt),
},
};
}
export function invalidateMindSpaceConfigCache() {
cachedMindSpaceConfig = null;
cachedMindSpaceConfigAt = 0;
}
async function ensureConfigTable(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS mindspace_config (
@@ -49,6 +114,21 @@ async function ensureConfigTable(pool) {
export function defaultMindSpaceConfig(env = process.env) {
return {
publicPageLimit: asPositiveInteger(env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5, 5),
seoGeo: normalizeSeoGeoConfig({
enabled: env.MINDSPACE_SEO_GEO_ENABLED,
seo: {
enabled: env.MINDSPACE_SEO_ENABLED,
canonical: env.MINDSPACE_SEO_CANONICAL,
sitemap: env.MINDSPACE_SEO_SITEMAP,
robotsTxt: env.MINDSPACE_SEO_ROBOTS_TXT,
baiduPush: env.MINDSPACE_SEO_BAIDU_PUSH,
},
geo: {
enabled: env.MINDSPACE_GEO_ENABLED,
jsonLd: env.MINDSPACE_GEO_JSONLD,
llmsTxt: env.MINDSPACE_GEO_LLMSTXT,
},
}),
analytics: {
enabled: String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true',
websiteId: String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim(),
@@ -89,6 +169,9 @@ export async function loadMindSpaceConfig(pool, { env = process.env, includeAnal
if (row.key === PUBLIC_PAGE_LIMIT_KEY) {
config.publicPageLimit = asPositiveInteger(row.value, config.publicPageLimit);
}
if (row.key === SEO_GEO_CONFIG_KEY) {
config.seoGeo = normalizeSeoGeoConfig(row.value);
}
if (row.key === ANALYTICS_ENABLED_KEY) config.analytics.enabled = row.value === 'true';
if (row.key === ANALYTICS_WEBSITE_ID_KEY) config.analytics.websiteId = String(row.value ?? '');
if (row.key === ANALYTICS_URL_KEY) config.analytics.analyticsUrl = String(row.value ?? '');
@@ -106,6 +189,23 @@ export async function loadMindSpaceConfig(pool, { env = process.env, includeAnal
return config;
}
export async function loadMindSpaceConfigCached(pool, options = {}) {
const ttlMs = Number(options.cacheTtlMs ?? CONFIG_CACHE_TTL_MS);
const now = Date.now();
if (
cachedMindSpaceConfig &&
now - cachedMindSpaceConfigAt < ttlMs &&
options.includeAnalyticsSecret === cachedMindSpaceConfig.__includeAnalyticsSecret
) {
return cachedMindSpaceConfig;
}
const config = await loadMindSpaceConfig(pool, options);
cachedMindSpaceConfig = config;
cachedMindSpaceConfigAt = now;
cachedMindSpaceConfig.__includeAnalyticsSecret = Boolean(options.includeAnalyticsSecret);
return config;
}
export async function updateMindSpaceConfig(pool, patch, { env = process.env } = {}) {
await ensureConfigTable(pool);
const updates = [];
@@ -118,6 +218,13 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
}
updates.push([PUBLIC_PAGE_LIMIT_KEY, String(publicPageLimit), '公开页面数量上限']);
}
if (patch?.seoGeo !== undefined) {
updates.push([
SEO_GEO_CONFIG_KEY,
JSON.stringify(normalizeSeoGeoConfig(patch.seoGeo)),
'MindSpace SEO/GEO 配置',
]);
}
if (patch?.analytics) {
const analytics = patch.analytics;
if (analytics.enabled !== undefined) updates.push([ANALYTICS_ENABLED_KEY, String(Boolean(analytics.enabled)), '本地 Umami 分析开关']);
@@ -129,6 +236,7 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
if (updates.length === 0) return loadMindSpaceConfig(pool, { env });
invalidateMindSpaceConfigCache();
const now = Date.now();
for (const [key, value, description] of updates) {
await pool.query(
@@ -146,6 +254,7 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
export const mindspaceConfigInternals = {
PUBLIC_PAGE_LIMIT_KEY,
SEO_GEO_CONFIG_KEY,
ANALYTICS_ENABLED_KEY,
ANALYTICS_WEBSITE_ID_KEY,
ANALYTICS_URL_KEY,