import fs from 'node:fs/promises';
import path from 'node:path';
import { createPageService, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { createPublicationService, publicationInternals } from './mindspace-publications.mjs';
import { slugFromPageTitle } from './mindspace-chat-plaza.mjs';
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
import { normalizePageAccessPolicy } from './page-access-policy.mjs';
import { upsertPageDataPolicyIndex } from './page-data-policy-index.mjs';
import { syncPageDataPolicyAccessMode } from './page-data-publish-sync.mjs';
import { createMindSpacePublicUrl } from './mindspace-canonical-url.mjs';
import { resolvePublicBaseUrl } from './user-publish.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import {
assertPolicyMatchesHtmlDatasets,
buildPageDataPolicyDatasetsFromRegistry,
detectPageDataDatasetUsageFromHtml,
} from './page-data-html-detect.mjs';
export const DEFAULT_PAGE_DATA_ADMIN_PASSWORD = '88888888';
export function resolvePageDataBindAccess(accessMode = 'public') {
return publicationInternals.normalizeAccessMode(accessMode);
}
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD;<8 位抛错 */
export function resolvePageDataBindPassword(accessMode, password = null) {
const mode = resolvePageDataBindAccess(accessMode);
if (mode !== 'password') return null;
const explicit = String(password ?? '').trim();
if (explicit) {
return publicationInternals.normalizePassword(explicit, true);
}
return DEFAULT_PAGE_DATA_ADMIN_PASSWORD;
}
function titleFromPublicHtml(content, relativePath) {
const match = String(content ?? '').match(/
]*>([^<]+)<\/title>/i);
const fromTitle = match?.[1]?.replace(/\s+/g, ' ').trim();
if (fromTitle) return fromTitle;
const basename = path.basename(String(relativePath ?? ''), '.html');
return basename || 'MindSpace 页面';
}
function normalizePublicHtmlRelativePath(relativePath) {
const normalized = normalizeWorkspaceRelativePath(relativePath);
if (!normalized || !normalized.startsWith('public/') || !normalized.toLowerCase().endsWith('.html')) {
throw Object.assign(new Error('仅支持 workspace 公开 HTML 页面(public/*.html)'), {
code: 'invalid_public_html_path',
});
}
return normalized;
}
async function ensureWorkspaceHtmlPage({ userId, relativePath, content, title, mindSpacePages }) {
const normalized = normalizePublicHtmlRelativePath(relativePath);
let page = await mindSpacePages.findPageByRelativePath(userId, normalized);
const pageInput = {
title: title ?? page?.title ?? titleFromPublicHtml(content, normalized),
content,
contentFormat: 'html',
pageType: 'html',
categoryCode: 'draft',
};
if (page) {
return mindSpacePages.updatePage(
userId,
page.id,
{
...pageInput,
expectedVersion: page.versionNo,
},
{
snapshot: {
content_mode: 'static_html',
relative_path: normalized,
},
},
);
}
return mindSpacePages.createFromChat(userId, pageInput, {
snapshot: {
content_mode: 'static_html',
relative_path: normalized,
},
});
}
async function ensureWorkspaceHtmlPublished({
userId,
page,
mindSpacePublications,
accessMode,
password,
urlSlug,
}) {
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
const existing = await mindSpacePublications.getCurrent(userId, page.id);
if (existing?.id) {
const accessChanged = existing.accessMode !== normalizedAccessMode;
const needsPassword =
normalizedAccessMode === 'password' && Boolean(resolvedPassword);
if (accessChanged || needsPassword) {
const updated = await mindSpacePublications.updatePublicationAccess(userId, existing.id, {
accessMode: normalizedAccessMode,
password: resolvedPassword,
});
existing.accessMode = updated.accessMode;
existing.id = updated.id;
existing.publicUrl = updated.publicUrl;
}
if (page.pageType === 'html' && mindSpacePublications.refreshOnlinePublicationHtml) {
return (
(await mindSpacePublications.refreshOnlinePublicationHtml(userId, page.id)) ?? existing
);
}
return existing;
}
const preferredSlug = urlSlug || slugFromPageTitle(page.title, page.id);
return mindSpacePublications.publish(userId, page.id, {
pageVersionId: page.currentVersionId,
accessMode: normalizedAccessMode,
password: resolvedPassword,
urlSlug: preferredSlug,
autoAcknowledgeFindings: true,
});
}
function buildWorkspacePublicUrl(userId, relativePath) {
const publicBaseUrl = resolvePublicBaseUrl();
return createMindSpacePublicUrl({
publicBaseUrl,
ownerKey: userId,
relativePath,
});
}
export async function bindWorkspaceHtmlForPageData({
pool,
h5Root,
storageRoot,
userId,
workspaceRoot,
relativePath,
title = null,
accessMode = 'public',
password = null,
pageDataPolicy = null,
urlSlug = null,
publicPageLimit,
}) {
if (!pool) throw new Error('数据库未配置,无法绑定页面');
if (!userId) throw new Error('缺少 userId');
if (!workspaceRoot) throw new Error('缺少 workspaceRoot');
const normalized = normalizePublicHtmlRelativePath(relativePath);
const absoluteHtmlPath = path.join(workspaceRoot, ...normalized.split('/'));
const content = await fs.readFile(absoluteHtmlPath, 'utf8');
const normalizedAccessMode = resolvePageDataBindAccess(accessMode);
const resolvedPassword = resolvePageDataBindPassword(normalizedAccessMode, password);
const mindSpacePages = createPageService(pool, { h5Root, storageRoot });
const mindSpacePublications = createPublicationService(pool, {
h5Root,
storageRoot,
publicPageLimit,
});
const page = await ensureWorkspaceHtmlPage({
userId,
relativePath: normalized,
content,
title,
mindSpacePages,
});
const publication = await ensureWorkspaceHtmlPublished({
userId,
page,
mindSpacePublications,
accessMode: normalizedAccessMode,
password: resolvedPassword,
urlSlug,
});
let policy = null;
const htmlDatasetUsage = detectPageDataDatasetUsageFromHtml(content);
let resolvedPageDataPolicy = pageDataPolicy;
if (htmlDatasetUsage.size) {
if (pageDataPolicy?.datasets) {
assertPolicyMatchesHtmlDatasets(content, pageDataPolicy.datasets);
} else {
const userDataSpace = createUserDataSpaceService({ workspaceRoot });
const autoDatasets = buildPageDataPolicyDatasetsFromRegistry({
html: content,
registryDatasets: userDataSpace.listDatasets(),
usage: htmlDatasetUsage,
});
resolvedPageDataPolicy = {
ownerUserId: userId,
accessMode: normalizedAccessMode,
datasets: autoDatasets,
};
}
}
if (resolvedPageDataPolicy) {
const ownerUserId = String(resolvedPageDataPolicy.ownerUserId ?? userId).trim();
policy = normalizePageAccessPolicy(
{
pageId: page.id,
ownerUserId,
accessMode: resolvedPageDataPolicy.accessMode ?? normalizedAccessMode,
datasets: resolvedPageDataPolicy.datasets,
defaultVisitorRole: resolvedPageDataPolicy.defaultVisitorRole,
visitors: resolvedPageDataPolicy.visitors,
roles: resolvedPageDataPolicy.roles,
},
{ fallbackPageId: page.id, fallbackOwnerUserId: ownerUserId },
);
policy = writePageAccessPolicy(workspaceRoot, policy);
await upsertPageDataPolicyIndex(pool, policy).catch(() => null);
policy =
syncPageDataPolicyAccessMode(workspaceRoot, page.id, publication.accessMode, ownerUserId) ??
policy;
}
const workspaceUrl = buildWorkspacePublicUrl(userId, normalized);
return {
pageId: page.id,
pageTitle: page.title,
relativePath: normalized,
publicationId: publication.id,
publicationAccessMode: publication.accessMode,
publicationPasswordApplied: normalizedAccessMode === 'password',
publicationUrl: workspaceUrl,
workspaceUrl,
deliveryUrl: workspaceUrl,
deliveryHint:
'向用户交付静态页时优先使用 deliveryUrl(MindSpace /MindSpace/<用户ID>/public/... 路径),publicationUrl 仅作补充',
policy,
};
}