Files
memind/server/portal-published-page-delivery.mjs
john c7718c683b
Memind CI / Test, build, and release guards (push) Successful in 1m36s
fix: inject Rybbit tracking on m.tkmind.cn publication pages
Publication routes and share shells were missing the Rybbit tracker, and
bootstrap now reuses the stored MindSpace analytics secret when Rybbit env
secret is absent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 22:10:07 +08:00

397 lines
9.7 KiB
JavaScript

import {
injectPublishedPageDataContext,
resolveMindSpacePageDataContext,
} from '../mindspace-public-page-context.mjs';
import {
collectInlineScriptHashes,
} from '../mindspace-public-delivery.mjs';
import {
injectMindSpaceAnalytics,
resolveAnalyticsOwnerLabel,
resolveAnalyticsOwnerSegment,
resolveAnalyticsPlan,
} from '../mindspace-analytics.mjs';
import {
injectMindSpaceRybbit,
} from '../mindspace-rybbit.mjs';
import {
rewriteBrokenMindSpacePublicImageUrls,
rewritePublicationCanonicalAssetUrls,
} from '../mindspace-publications.mjs';
import {
allowPlazaEmbedFrame,
preparePublicationHtmlForEmbed,
stripPublicationHtmlCspMeta,
} from '../plaza-embed.mjs';
import {
publishedPageCsp,
} from '../mindspace-published-page-csp.mjs';
import {
injectOgTags,
injectWechatShareBridge,
} from '../mindspace-og-tags.mjs';
import {
isLongImageDownloadRequest,
renderLongImageBuffer,
} from '../mindspace-long-image.mjs';
import { isWechatUserAgent } from '../wechat-oauth.mjs';
import {
appendQueryParam,
detectPublishedPageTitle,
extractOgImageUrl,
extractShareMetaFromPageHtml,
publishedPageShellHtml,
removeQueryParam,
resolveRequestOrigin,
} from './portal-publication-shell.mjs';
async function decoratePublicationHtmlAnalytics(
html,
{
result,
analyticsConfig,
rybbitConfig,
getAuthPool = () => null,
getUserAuth = () => null,
getMindSpacePages = () => null,
resolvePageDataContext =
resolveMindSpacePageDataContext,
logger = console,
} = {},
) {
const source = String(html ?? '');
const isFullHtml =
/^\s*<!doctype html/i.test(source) ||
/^\s*<html[\s>]/i.test(source);
if (!isFullHtml) return source;
const userAuth = getUserAuth?.();
const pageOwner =
result?.ownerId && userAuth
? await userAuth
.getUserById(result.ownerId)
.catch(() => null)
: null;
let pageDataContext = null;
const mindSpacePages = getMindSpacePages?.();
if (
result?.ownerId &&
result?.workspaceRelativePath &&
mindSpacePages
) {
pageDataContext = await resolvePageDataContext({
pool: getAuthPool?.(),
pageService: mindSpacePages,
userId: result.ownerId,
relativePath: result.workspaceRelativePath,
logger,
}).catch(() => null);
}
const ownerSegment = resolveAnalyticsOwnerSegment(
pageOwner ?? {},
);
const ownerLabel = resolveAnalyticsOwnerLabel(
pageOwner ?? {},
);
const pageId =
pageDataContext?.pageId ??
result?.pageSource?.pageId ??
'';
const publicationId =
pageDataContext?.publicationId ??
pageDataContext?.publication_id ??
result?.publication?.id ??
'';
let decorated = injectMindSpaceAnalytics(source, {
ownerId: result?.ownerId ?? '',
ownerSegment,
ownerLabel,
planType: resolveAnalyticsPlan(pageOwner ?? {}),
generatedAt: pageDataContext?.generatedAt ?? '',
pageId,
publicationId,
config: analyticsConfig,
});
decorated = injectMindSpaceRybbit(decorated, {
ownerId: result?.ownerId ?? '',
ownerSegment,
ownerLabel,
pageId,
publicationId,
channel: 'publication',
config: rybbitConfig,
});
return decorated;
}
export function createPortalPublishedPageDelivery({
registerLongImageArtifact,
renderLongImage =
renderLongImageBuffer,
analyticsConfig = { enabled: false },
rybbitConfig = { enabled: false },
getAuthPool = () => null,
getUserAuth = () => null,
getMindSpacePages = () => null,
logger = console,
} = {}) {
if (
typeof registerLongImageArtifact !==
'function'
) {
throw new Error(
'createPortalPublishedPageDelivery requires artifact registration',
);
}
return async function sendPublishedPage(
req,
res,
result,
{
embed = false,
raw = false,
ownerSlug = null,
} = {},
) {
let html =
rewriteBrokenMindSpacePublicImageUrls(
result.html,
);
if (ownerSlug) {
html =
rewritePublicationCanonicalAssetUrls(
html,
ownerSlug,
);
}
const origin = resolveRequestOrigin(req);
const originalPath =
req.originalUrl || req.url || '';
const sharePath = removeQueryParam(
removeQueryParam(
originalPath,
'download',
),
'export',
);
const pageUrl = originalPath
? new URL(
sharePath,
origin || 'http://localhost',
)
.toString()
.split('#')[0]
: '';
const pageDirUrl = pageUrl
? pageUrl.slice(
0,
pageUrl.lastIndexOf('/') + 1,
)
: '';
const wechatShare =
!embed &&
isWechatUserAgent(
req.get('user-agent') || '',
);
if (embed) {
html =
preparePublicationHtmlForEmbed(html);
allowPlazaEmbedFrame(res);
} else if (raw) {
html =
stripPublicationHtmlCspMeta(html);
}
html = injectPublishedPageDataContext(
html,
{
pageSource: result.pageSource,
publication: result.publication,
},
);
html = await decoratePublicationHtmlAnalytics(html, {
result,
analyticsConfig,
rybbitConfig,
getAuthPool,
getUserAuth,
getMindSpacePages,
logger,
});
if (!embed) {
try {
html = injectOgTags(html, {
origin,
pageUrl,
pageDirUrl,
});
if (wechatShare) {
html = injectWechatShareBridge(
html,
{ pageUrl },
);
}
} catch {
// Never block delivery on share metadata.
}
}
const isFullHtml =
/^\s*<!doctype html/i.test(html) ||
/^\s*<html[\s>]/i.test(html);
if (
!embed &&
!raw &&
isFullHtml &&
isLongImageDownloadRequest(req.query)
) {
try {
const rawUrl = new URL(
appendQueryParam(
sharePath || originalPath,
'view',
'raw',
),
origin || 'http://localhost',
);
const image =
await renderLongImage({
url: rawUrl.toString(),
});
const longImageUrl = new URL(
appendQueryParam(
sharePath || originalPath,
'download',
'long-image',
),
origin || 'http://localhost',
).toString();
await registerLongImageArtifact({
result,
image,
canonicalUrl: longImageUrl,
});
res.set('Content-Type', 'image/png');
res.set(
'Content-Disposition',
'attachment; filename="mindspace-public-page.long.png"',
);
res.set(
'Cache-Control',
'no-store',
);
return res.send(image);
} catch (error) {
return res
.status(500)
.type(
'text/plain; charset=utf-8',
)
.send(
`长图生成失败:${
error?.message || '未知错误'
}`,
);
}
}
const canWrapWithShell =
!embed &&
!raw &&
isFullHtml &&
result.publication?.accessMode !==
'password';
if (canWrapWithShell) {
const title =
detectPublishedPageTitle(html);
const rawUrl = appendQueryParam(
sharePath || originalPath,
'view',
'raw',
);
const longImageUrl = appendQueryParam(
sharePath || originalPath,
'download',
'long-image',
);
let shellHtml = publishedPageShellHtml({
iframeUrl: rawUrl,
shareUrl: pageUrl,
title,
longImageUrl,
});
try {
shellHtml = injectOgTags(shellHtml, {
origin,
pageUrl,
pageDirUrl,
fallbackImageUrl:
extractOgImageUrl(html),
meta:
extractShareMetaFromPageHtml(
html,
),
});
if (wechatShare) {
shellHtml =
injectWechatShareBridge(
shellHtml,
{ pageUrl },
);
}
} catch {
// Keep the share shell usable.
}
shellHtml = await decoratePublicationHtmlAnalytics(shellHtml, {
result,
analyticsConfig,
rybbitConfig,
getAuthPool,
getUserAuth,
getMindSpacePages,
logger,
});
res.set(
'Content-Type',
'text/html; charset=utf-8',
);
res.set(
'Content-Security-Policy',
wechatShare
? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
: "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'",
);
res.set(
'Cache-Control',
result.publication.accessMode ===
'public'
? 'public, max-age=60'
: 'private, no-store',
);
return res.send(shellHtml);
}
res.set(
'Content-Type',
'text/html; charset=utf-8',
);
res.set(
'Content-Security-Policy',
publishedPageCsp(html, {
embed,
raw,
wechatShare,
scriptHashes:
collectInlineScriptHashes(html),
}),
);
res.set(
'Cache-Control',
result.publication.accessMode ===
'public'
? 'public, max-age=60'
: 'private, no-store',
);
return res.send(html);
};
}