Add page data delivery and publication guards

This commit is contained in:
john
2026-07-08 21:10:47 +08:00
parent 8d629e7a4e
commit eea14c1855
66 changed files with 3035 additions and 413 deletions
+69 -9
View File
@@ -3,7 +3,8 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import { localizeGoogleFontsCss } from './mindspace-html-localize.mjs';
import { replacePrivateResourceReferences, scanContent } from './mindspace-content-scan.mjs';
import { pageInternals } from './mindspace-pages.mjs';
import { pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { readWorkspacePublishHtml } from './mindspace-workspace-path.mjs';
import { loadMindSpaceConfig } from './mindspace-config.mjs';
import { buildPublicUrl, resolvePublicBaseUrl, resolvePublishDir } from './user-publish.mjs';
import {
@@ -465,6 +466,7 @@ export function createPublicationService(pool, options = {}) {
const loadVersion = async (userId, pageId, pageVersionId) => {
const [rows] = await pool.query(
`SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id,
p.workspace_relative_path,
p.source_session_id, p.source_message_id,
p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no,
pv.bundle_asset_id, av.storage_key,
@@ -501,6 +503,19 @@ export function createPublicationService(pool, options = {}) {
const preparePublishContent = async (page, ownerSlug, urlSlug) => {
let publishContent = page.content;
if (page.page_type !== 'html') return publishContent;
const workspaceRelativePath = normalizeWorkspaceRelativePath(
page.workspace_relative_path ?? page.source_relative_path,
);
if (h5Root && workspaceRelativePath) {
const workspaceHtml = await readWorkspacePublishHtml(
h5Root,
page.user_id,
workspaceRelativePath,
);
if (workspaceHtml && workspaceHtml.length > String(publishContent ?? '').length) {
publishContent = workspaceHtml;
}
}
const publishDir = h5Root ? resolvePublishDir(h5Root, { id: page.user_id }) : null;
return prepareHtmlPublishContent({
pool,
@@ -969,7 +984,7 @@ export function createPublicationService(pool, options = {}) {
});
};
const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => {
const updatePublicationAccess = async (userId, publicationId, { accessMode, password, expiresAt } = {}) => {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
@@ -981,15 +996,30 @@ export function createPublicationService(pool, options = {}) {
const publication = rows[0];
if (!publication) throw publicationError('发布记录不存在', 'publication_not_found');
const normalizedMode = normalizeAccessMode(accessMode);
const normalizedExpiresAt = normalizeExpiresAt(expiresAt, normalizedMode === 'time_limited');
const normalizedMode =
accessMode != null ? normalizeAccessMode(accessMode) : publication.access_mode;
const normalizedExpiresAt =
expiresAt !== undefined
? normalizeExpiresAt(expiresAt, normalizedMode === 'time_limited')
: publication.expires_at;
let passwordHash = null;
if (normalizedMode === 'password') {
const explicit = String(password ?? '').trim();
if (explicit) {
passwordHash = hashPassword(normalizePassword(explicit, true));
} else if (publication.password_hash) {
passwordHash = publication.password_hash;
} else {
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
}
}
const now = Date.now();
await conn.query(
`UPDATE h5_publish_records
SET access_mode = ?, expires_at = ?, user_confirmed_at = ?, updated_at = ?
SET access_mode = ?, expires_at = ?, password_hash = ?, user_confirmed_at = ?, updated_at = ?
WHERE id = ? AND user_id = ?`,
[normalizedMode, normalizedExpiresAt, now, now, publicationId, userId],
[normalizedMode, normalizedExpiresAt, passwordHash, now, now, publicationId, userId],
);
await conn.commit();
@@ -997,6 +1027,7 @@ export function createPublicationService(pool, options = {}) {
...publication,
access_mode: normalizedMode,
expires_at: normalizedExpiresAt,
password_hash: passwordHash,
user_confirmed_at: now,
updated_at: now,
};
@@ -1009,6 +1040,10 @@ export function createPublicationService(pool, options = {}) {
}
};
const updatePublicationStatus = async (userId, publicationId, { accessMode, expiresAt }) => {
return updatePublicationAccess(userId, publicationId, { accessMode, expiresAt });
};
const offline = async (userId, publicationId) => {
const conn = await pool.getConnection();
try {
@@ -1097,7 +1132,7 @@ export function createPublicationService(pool, options = {}) {
now,
],
);
const html = await fs.readFile(await resolveReadableStoragePath(row.storage_key), 'utf8');
const html = await readPublicationHtmlWithWorkspaceFallback(row);
return {
html: await refreshPublishedHtmlDownloadLinks(row, html),
publication: publicationResponse({ ...row, view_count: Number(row.view_count) + 1 }),
@@ -1111,6 +1146,24 @@ export function createPublicationService(pool, options = {}) {
};
};
// REGRESSION GUARD: mindspace-page-sync-thumbnail — publication delivery must fall back to
// workspace HTML when publication storage snapshot is missing (isolated env / migration).
async function readPublicationHtmlWithWorkspaceFallback(row) {
try {
return await fs.readFile(await resolveReadableStoragePath(row.storage_key), 'utf8');
} catch (error) {
const storageMissing = error?.code === 'ENOENT' || error?.code === 'storage_not_found';
if (!storageMissing || !h5Root) throw error;
const workspaceRelativePath = normalizeWorkspaceRelativePath(
row.workspace_relative_path ?? row.source_relative_path,
);
if (!workspaceRelativePath) throw error;
const workspaceHtml = await readWorkspacePublishHtml(h5Root, row.owner_id, workspaceRelativePath);
if (!workspaceHtml) throw error;
return workspaceHtml;
}
}
const refreshPublishedHtmlDownloadLinks = async (row, html) => {
if (!h5Root || !row?.owner_id) return html;
const source = String(html ?? '');
@@ -1133,7 +1186,10 @@ export function createPublicationService(pool, options = {}) {
const resolvePublic = async (ownerSlug, urlSlug, viewerId, password, requestMeta) => {
const [rows] = await pool.query(
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id,
p.workspace_relative_path,
av.storage_key,
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
FROM h5_publish_records pr
JOIN h5_users u ON u.id = pr.user_id
JOIN h5_page_records p ON p.id = pr.page_id
@@ -1150,7 +1206,10 @@ export function createPublicationService(pool, options = {}) {
const resolvePrivateLink = async (token, viewerId, requestMeta) => {
const tokenHash = crypto.createHash('sha256').update(String(token)).digest('hex');
const [rows] = await pool.query(
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id, av.storage_key
`SELECT pr.*, u.id AS owner_id, p.title, p.source_session_id, p.source_message_id,
p.workspace_relative_path,
av.storage_key,
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
FROM h5_publish_records pr
JOIN h5_users u ON u.id = pr.user_id
JOIN h5_page_records p ON p.id = pr.page_id
@@ -1251,6 +1310,7 @@ export function createPublicationService(pool, options = {}) {
getPublicHomepage,
getStats,
offline,
updatePublicationAccess,
updatePublicationStatus,
resolvePublic,
resolvePrivateLink,