import fs from 'node:fs'; import path from 'node:path'; import { slugFromPageTitle } from './mindspace-chat-plaza.mjs'; import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs'; import { htmlUsesPageDataApi } from './page-data-html-detect.mjs'; import { resolvePublishDir } from './user-publish.mjs'; function parseJsonColumn(value, fallback = {}) { if (value == null || value === '') return { ...fallback }; if (typeof value === 'object' && !Buffer.isBuffer(value)) return value; try { return JSON.parse(String(value)); } catch { return { ...fallback }; } } function isPublicWorkspaceHtmlPath(relativePath) { const normalized = normalizeWorkspaceRelativePath(relativePath); return Boolean(normalized?.startsWith('public/') && normalized.toLowerCase().endsWith('.html')); } function readWorkspaceHtmlContent(publishDir, relativePath) { const normalized = normalizeWorkspaceRelativePath(relativePath); if (!normalized || !publishDir) return ''; const absolutePath = path.join(publishDir, ...normalized.split('/')); try { return fs.readFileSync(absolutePath, 'utf8'); } catch { return ''; } } function isPageDataWorkspaceHtml(publishDir, relativePath) { const content = readWorkspaceHtmlContent(publishDir, relativePath); return htmlUsesPageDataApi(content); } export function createWorkspacePageDeliverService({ pool, pageService, publicationService, pageSyncService, pageDataEnsure = null, h5Root = null, storageRoot = null, findPageByRelativePath = null, logger = console, } = {}) { async function listUnpublishedAutoSyncedWorkspacePages(userId) { if (!pool || !userId) return []; const [rows] = await pool.query( `SELECT p.id AS page_id, p.title, p.current_version_id, p.workspace_relative_path, pv.source_snapshot_json FROM h5_page_records p JOIN h5_page_versions pv ON pv.id = p.current_version_id LEFT JOIN h5_publish_records pr ON pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online' WHERE p.user_id = ? AND p.status <> 'deleted' AND pr.id IS NULL`, [userId], ); return (rows ?? []).filter((row) => { const snapshot = parseJsonColumn(row.source_snapshot_json); const relativePath = row.workspace_relative_path ?? snapshot.relative_path ?? null; if (!isPublicWorkspaceHtmlPath(relativePath)) return false; return snapshot.auto_synced === true || snapshot.content_mode === 'static_html'; }); } async function listOnlineAutoSyncedWorkspacePages(userId) { if (!pool || !userId) return []; const [rows] = await pool.query( `SELECT p.id AS page_id, p.title, p.current_version_id, p.workspace_relative_path, pv.source_snapshot_json FROM h5_page_records p JOIN h5_page_versions pv ON pv.id = p.current_version_id JOIN h5_publish_records pr ON pr.page_id = p.id AND pr.user_id = p.user_id AND pr.status = 'online' WHERE p.user_id = ? AND p.status <> 'deleted'`, [userId], ); return (rows ?? []).filter((row) => { const snapshot = parseJsonColumn(row.source_snapshot_json); const relativePath = row.workspace_relative_path ?? snapshot.relative_path ?? null; if (!isPublicWorkspaceHtmlPath(relativePath)) return false; return snapshot.auto_synced === true || snapshot.content_mode === 'static_html'; }); } function resolveWorkspaceRoot(userId) { if (!h5Root || !userId) return null; return resolvePublishDir(h5Root, { id: userId }); } async function ensurePageDataBindings(userId, { onlyRelativePaths = null } = {}) { if (!pageDataEnsure?.ensurePageDataHtmlPagesBound || !pool || !userId) { return { bound: [], skipped: [], errors: [] }; } const workspaceRoot = resolveWorkspaceRoot(userId); if (!workspaceRoot) { return { bound: [], skipped: [], errors: [{ code: 'missing_workspace_root' }] }; } return pageDataEnsure.ensurePageDataHtmlPagesBound({ pool, h5Root, storageRoot, userId, workspaceRoot, findPageByRelativePath, onlyRelativePaths, logger, }); } async function refreshOnlineWorkspacePublications(userId) { if (!publicationService?.refreshOnlinePublicationHtml || !userId) { return { refreshed: 0, skipped: 0, errors: [] }; } const candidates = await listOnlineAutoSyncedWorkspacePages(userId); let refreshed = 0; let skipped = 0; const errors = []; for (const candidate of candidates) { try { const result = await publicationService.refreshOnlinePublicationHtml( userId, candidate.page_id, ); if (result) refreshed += 1; else skipped += 1; } catch (error) { errors.push({ pageId: candidate.page_id, message: error instanceof Error ? error.message : String(error), }); logger?.warn?.( `[MindSpace] refresh publication failed for page ${candidate.page_id}:`, error instanceof Error ? error.message : error, ); } } return { refreshed, skipped, errors }; } async function ensureWorkspaceHtmlPublications(userId) { if (!publicationService?.publish || !pageService?.getPage || !userId) { return { published: 0, skipped: 0, errors: [] }; } const candidates = await listUnpublishedAutoSyncedWorkspacePages(userId); const publishDir = resolveWorkspaceRoot(userId); let published = 0; let skipped = 0; const errors = []; for (const candidate of candidates) { try { const snapshot = parseJsonColumn(candidate.source_snapshot_json); const relativePath = candidate.workspace_relative_path ?? snapshot.relative_path ?? null; if (isPageDataWorkspaceHtml(publishDir, relativePath)) { skipped += 1; continue; } const page = await pageService.getPage(userId, candidate.page_id); const current = await publicationService.getCurrent?.(userId, candidate.page_id); if (current?.status === 'online') { skipped += 1; continue; } await publicationService.publish(userId, candidate.page_id, { pageVersionId: page.currentVersionId, accessMode: 'public', urlSlug: slugFromPageTitle(page.title, candidate.page_id), autoAcknowledgeFindings: true, }); published += 1; } catch (error) { errors.push({ pageId: candidate.page_id, message: error instanceof Error ? error.message : String(error), }); logger?.warn?.( `[MindSpace] auto publish failed for page ${candidate.page_id}:`, error instanceof Error ? error.message : error, ); } } return { published, skipped, errors }; } async function syncAndDeliver(userId, { pageDataRelativePaths = null } = {}) { let syncResult = { created: 0, updated: 0, skipped: 0 }; if (pageSyncService?.syncUserGeneratedPages) { syncResult = await pageSyncService.syncUserGeneratedPages(userId, { onlyRelativePaths: pageDataRelativePaths, }); } const pageDataBindResult = await ensurePageDataBindings(userId, { onlyRelativePaths: pageDataRelativePaths, }); const publishResult = await ensureWorkspaceHtmlPublications(userId); const refreshResult = await refreshOnlineWorkspacePublications(userId); return { pageDataRelativePaths: pageDataRelativePaths == null ? null : [...pageDataRelativePaths], sync: syncResult, pageDataBind: pageDataBindResult, publish: publishResult, refresh: refreshResult, }; } return { syncAndDeliver, ensurePageDataBindings, ensureWorkspaceHtmlPublications, refreshOnlineWorkspacePublications, }; }