Files
memind/server/portal-workspace-publication-delivery.mjs
T
john fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(mindspace): add SEO/GEO delivery, page template catalog, and admin hooks
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>
2026-08-10 08:06:12 +08:00

420 lines
10 KiB
JavaScript

import {
buildViewerAnalyticsIdentity,
injectMindSpaceAnalytics,
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
} from '../mindspace-analytics.mjs';
import {
injectMindSpaceRybbit,
} from '../mindspace-rybbit.mjs';
import {
buildPublishedHtmlViewContext,
resolveMindSpacePageDataContext,
} from '../mindspace-public-page-context.mjs';
import {
preparePublicHtmlAssetDelivery,
} from '../mindspace-public-asset-token.mjs';
import {
decorateMindSpacePublishedHtml,
} from '../mindspace-public-delivery.mjs';
import {
injectPublicFileShareButton,
} from '../mindspace-public-share-widget.mjs';
import {
allowPlazaEmbedFrame,
isPlazaEmbedRequest,
preparePublicationHtmlForEmbed,
} from '../plaza-embed.mjs';
import {
publishedPageCsp,
} from '../mindspace-published-page-csp.mjs';
import {
rewriteKnownCdnScriptSources,
} from '../mindspace-published-script-localize.mjs';
import {
injectOgTags,
injectWechatShareBridge,
} from '../mindspace-og-tags.mjs';
import {
isLongImageDownloadRequest,
} from '../mindspace-long-image.mjs';
import { isWechatUserAgent } from '../wechat-oauth.mjs';
import {
applySeoGeoResponseHeaders,
} from '../mindspace-seo-geo-delivery.mjs';
import {
loadMindSpaceConfigCached,
} from '../mindspace-config.mjs';
import {
resolvePublicationIndexSnapshot,
} from '../mindspace-seo-discovery-service.mjs';
function deliveryNotFoundMessage(reason) {
if (reason === 'missing_owner_dir' || reason === 'missing_owner') {
return '用户不存在';
}
if (reason === 'missing_directory_index') {
return '目录中没有 index.html';
}
return '文件不存在';
}
export function createPortalWorkspacePublicationDelivery({
internalAgentSecret,
analyticsConfig,
rybbitConfig,
getAuthPool = () => null,
getUserAuth = () => null,
getMindSpacePages = () => null,
getWorkspacePublicationDelivery = () =>
null,
resolveRequestOrigin,
removeQueryParam,
logger = console,
resolvePageDataContext =
resolveMindSpacePageDataContext,
getMindSpaceConfig = loadMindSpaceConfigCached,
resolvePublicationSnapshot = resolvePublicationIndexSnapshot,
} = {}) {
if (
typeof resolveRequestOrigin !==
'function' ||
typeof removeQueryParam !== 'function'
) {
throw new Error(
'createPortalWorkspacePublicationDelivery requires delivery dependencies',
);
}
function requireDeliveryService() {
const service =
getWorkspacePublicationDelivery();
if (
!service ||
typeof service.resolveWorkspaceRequest !==
'function'
) {
throw Object.assign(
new Error('MindSpace 交付服务未启用'),
{ code: 'delivery_service_unavailable' },
);
}
return service;
}
async function sendLongImageDownloadIfRequested(
req,
res,
) {
if (
!isLongImageDownloadRequest(req.query)
) {
return false;
}
const origin = resolveRequestOrigin(req);
const currentUrl = origin
? `${origin}${
req.originalUrl || req.url || ''
}`
: null;
const pageUrl = currentUrl
? removeQueryParam(
removeQueryParam(
currentUrl,
'download',
),
'export',
)
: null;
try {
const rendered =
await requireDeliveryService()
.renderLongImage({ pageUrl });
const image = Buffer.from(
rendered.bodyBase64,
'base64',
);
res.set('Content-Type', 'image/png');
res.set(
'Content-Disposition',
`attachment; filename="${
rendered.servedName ||
'mindspace-public-page.long.png'
}"`,
);
res.set('Cache-Control', 'no-store');
res.send(image);
} catch (error) {
res
.status(500)
.type('text/plain; charset=utf-8')
.send(
`长图生成失败:${
error?.message || '未知错误'
}`,
);
}
return true;
}
async function sendPublishDelivery(
req,
res,
delivery,
{ isOwner = true } = {},
) {
if (delivery.kind !== 'html') {
if (delivery.cacheControl) {
res.set(
'Cache-Control',
delivery.cacheControl,
);
}
res.type?.(
delivery.servedName ||
'application/octet-stream',
);
return res.send(
Buffer.from(
delivery.bodyBase64 || '',
'base64',
),
);
}
if (
await sendLongImageDownloadIfRequested(
req,
res,
)
) {
return;
}
let html =
rewriteKnownCdnScriptSources(
delivery.html,
).html;
html = preparePublicHtmlAssetDelivery(
html,
internalAgentSecret,
);
const embed =
isPlazaEmbedRequest(req.query);
if (embed) {
html =
preparePublicationHtmlForEmbed(html);
allowPlazaEmbedFrame(res);
res.set(
'Content-Security-Policy',
publishedPageCsp(html, { embed }),
);
}
const context =
buildPublishedHtmlViewContext({
origin: resolveRequestOrigin(req),
requestPath:
req.originalUrl || req.url || '',
servedName: delivery.servedName,
fallbackImageName:
delivery.thumbnailName,
});
const userAuth = getUserAuth();
const pageOwner =
delivery.ownerId && userAuth
? await userAuth
.getUserById(delivery.ownerId)
.catch(() => null)
: null;
let pageDataContext = null;
const mindSpacePages =
getMindSpacePages();
if (
delivery.ownerId &&
delivery.relativePath
) {
pageDataContext =
await resolvePageDataContext({
pool: getAuthPool(),
pageService: mindSpacePages,
userId: delivery.ownerId,
relativePath:
delivery.relativePath,
logger,
});
}
html = injectMindSpaceAnalytics(html, {
ownerId: delivery.ownerId ?? '',
ownerSegment:
resolveAnalyticsOwnerSegment(
pageOwner ?? {},
),
ownerLabel: resolveAnalyticsOwnerLabel(
pageOwner ?? {},
),
planType: resolveAnalyticsPlan(
pageOwner ?? {},
),
generatedAt:
pageDataContext?.generatedAt ?? '',
pageId: pageDataContext?.pageId ?? '',
publicationId:
pageDataContext?.publicationId ??
pageDataContext?.publication_id ??
'',
viewerIdentity: buildViewerAnalyticsIdentity(
req.currentUser ?? null,
analyticsConfig,
),
config: analyticsConfig,
});
html = injectMindSpaceRybbit(html, {
ownerId: delivery.ownerId ?? '',
ownerSegment:
resolveAnalyticsOwnerSegment(
pageOwner ?? {},
),
ownerLabel: resolveAnalyticsOwnerLabel(
pageOwner ?? {},
),
pageId: pageDataContext?.pageId ?? '',
publicationId:
pageDataContext?.publicationId ??
pageDataContext?.publication_id ??
'',
config: rybbitConfig,
});
const mindSpaceConfig = getAuthPool()
? await getMindSpaceConfig(getAuthPool()).catch(() => ({ seoGeo: { enabled: false } }))
: { seoGeo: { enabled: false } };
let publicationSnapshot = null;
if (mindSpaceConfig?.seoGeo?.enabled && getAuthPool()) {
publicationSnapshot = await resolvePublicationSnapshot(getAuthPool(), {
publicationId:
pageDataContext?.publicationId ??
pageDataContext?.publication_id ??
null,
pageId: pageDataContext?.pageId ?? null,
userId: delivery.ownerId ?? null,
}).catch(() => null);
}
const decorated =
decorateMindSpacePublishedHtml({
html,
embed,
isOwner,
pageDataContext,
context,
userAgent:
req.get('user-agent') || '',
preparePublicationHtmlForEmbed,
injectOgTags,
injectWechatShareBridge,
injectPublicFileShareButton,
publishedPageCsp,
isWechatUserAgent,
seoGeoConfig: mindSpaceConfig?.seoGeo ?? null,
publicationSnapshot,
});
html = decorated.html;
if (decorated.allowEmbedFrame) {
allowPlazaEmbedFrame(res);
}
applySeoGeoResponseHeaders(res, decorated.robotsHeader);
res.set(
'Content-Security-Policy',
decorated.csp,
);
res.set(
'Content-Type',
'text/html; charset=utf-8',
);
// REGRESSION GUARD: mindspace-public-owner-vs-visitor — this HTML varies by viewer.
res.set(
'Cache-Control',
'private, no-store',
);
return res.send(html);
}
async function serveUserPublishFile(
req,
res,
next,
) {
let result;
try {
result =
await requireDeliveryService()
.resolveWorkspaceRequest({
requestPath: req.path,
});
} catch (error) {
logger?.error?.(
'[MindSpace] workspace delivery failed',
error,
);
return res
.status(503)
.json({
message: 'MindSpace 交付服务不可用',
});
}
if (result.action === 'redirect') {
return res.redirect(
result.status ?? 301,
result.location,
);
}
if (result.action === 'forbidden') {
return res
.status(403)
.json({ message: '禁止访问' });
}
if (result.action === 'not_found') {
return res
.status(404)
.json({
message:
deliveryNotFoundMessage(
result.reason,
),
});
}
if (result.action === 'not_ready') {
return res
.status(409)
.type('text/plain; charset=utf-8')
.send(
'页面已生成,正在完成发布验证,请稍后重试。',
);
}
const userAuth = getUserAuth();
const viewer =
req.userSession && userAuth
? await userAuth
.getMe(req.userToken)
.catch(() => null)
: null;
const isOwner = Boolean(
viewer?.id &&
result.ownerId &&
String(viewer.id).toLowerCase() ===
String(result.ownerId).toLowerCase(),
);
return sendPublishDelivery(
req,
res,
result,
{ isOwner },
);
}
return {
sendPublishDelivery,
serveUserPublishFile,
};
}