387 lines
9.0 KiB
JavaScript
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,
|
|
};
|
|
}
|