Files
memind/server/portal-workspace-publication-delivery.mjs
T
2026-07-27 15:34:35 +08:00

387 lines
9.0 KiB
JavaScript

import {
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';
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,
} = {}) {
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 ??
'',
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 decorated =
decorateMindSpacePublishedHtml({
html,
embed,
isOwner,
pageDataContext,
context,
userAgent:
req.get('user-agent') || '',
preparePublicationHtmlForEmbed,
injectOgTags,
injectWechatShareBridge,
injectPublicFileShareButton,
publishedPageCsp,
isWechatUserAgent,
});
html = decorated.html;
if (decorated.allowEmbedFrame) {
allowPlazaEmbedFrame(res);
}
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,
};
}