import fs from 'node:fs/promises'; import path from 'node:path'; import { normalizeWorkspaceRelativePath } from './mindspace-pages.mjs'; export function slugFromPageTitle(title, pageId) { const ascii = String(title ?? '') .normalize('NFKC') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, pageId ? 54 : 64); const base = ascii || 'page'; if (pageId) { return `${base}-${String(pageId).replace(/-/g, '').slice(0, 8)}`.slice(0, 64); } return base; } async function resolveExistingPage({ userId, sessionId, messageId, analysis, mindSpacePages, }) { const byMessage = await mindSpacePages .findPageBySourceMessage(userId, sessionId, messageId) .catch(() => null); if (byMessage) return byMessage; if (analysis.contentMode === 'static_html' && analysis.relativePath) { const byPath = await mindSpacePages .findPageByRelativePath(userId, analysis.relativePath) .catch(() => null); if (byPath) return byPath; } return null; } async function buildPageInput({ bundle, body = {} }) { const { source, analysis, resolvedHtml } = bundle; let pageInput = { title: body.title, summary: body.summary, templateId: body.template_id ?? 'editorial', pageType: body.page_type, categoryCode: 'draft', }; if (analysis.contentMode === 'static_html') { if (!resolvedHtml) { throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' }); } pageInput = { ...pageInput, title: body.title || resolvedHtml.suggestedTitle, summary: body.summary || resolvedHtml.suggestedSummary, content: resolvedHtml.content, contentFormat: 'html', pageType: 'html', }; } else { pageInput = { ...pageInput, title: body.title || 'AI 创作', content: source.content, contentFormat: 'markdown', }; } return pageInput; } export async function ensureChatPageForPlaza({ userId, bundle, mindSpacePages, ensureWorkspaceHtmlThumbnail, publishDir, body = {}, skipThumbnail = false, }) { const { source, analysis, resolvedHtml } = bundle; const sessionId = body.session_id ?? body.sessionId; const messageId = body.message_id ?? body.messageId; const snapshot = { session_name: source.session.name, message_created: source.message.created, role: source.message.role, content_mode: analysis.contentMode, public_url: analysis.previewUrl, relative_path: analysis.relativePath ?? null, }; const pageInput = await buildPageInput({ bundle, body }); const existingPage = await resolveExistingPage({ userId, sessionId, messageId, analysis, mindSpacePages, }); if ( !skipThumbnail && analysis.contentMode === 'static_html' && resolvedHtml?.content && analysis.relativePath && ensureWorkspaceHtmlThumbnail && publishDir ) { await ensureWorkspaceHtmlThumbnail(publishDir, analysis.relativePath, resolvedHtml.content, { title: pageInput.title, subtitle: pageInput.summary, }).catch(() => {}); } if (existingPage) { return mindSpacePages.updatePage(userId, existingPage.id, { ...pageInput, expectedVersion: existingPage.versionNo, }); } return mindSpacePages.createFromChat(userId, pageInput, { sessionId, messageId, snapshot, }); } export async function ensurePagePublicationForPlaza({ userId, page, mindSpacePublications, refreshIfOnline = true, }) { const existing = await mindSpacePublications.getCurrent(userId, page.id); if (existing?.id) { if (refreshIfOnline && page.pageType === 'html' && mindSpacePublications.refreshOnlinePublicationHtml) { return ( (await mindSpacePublications.refreshOnlinePublicationHtml(userId, page.id)) ?? existing ); } return existing; } const preferredSlug = slugFromPageTitle(page.title, page.id); return mindSpacePublications.publish(userId, page.id, { pageVersionId: page.currentVersionId, accessMode: 'public', urlSlug: preferredSlug, autoAcknowledgeFindings: true, }); } function throwAlreadyPublished(postId) { throw Object.assign(new Error('该内容已发布到广场'), { code: 'ALREADY_PUBLISHED', details: { post_id: postId }, }); } async function assertWorkspacePathNotAlreadyOnPlaza({ userId, relativePath, mindSpacePages }) { if (!mindSpacePages?.findPlazaContextByWorkspacePath) return null; const context = await mindSpacePages.findPlazaContextByWorkspacePath(userId, relativePath); if ( context?.postId && ['published', 'pending_review'].includes(String(context.postStatus ?? '')) ) { throwAlreadyPublished(context.postId); } return context; } async function ensureWorkspaceHtmlPageForPlaza({ userId, relativePath, content, title, mindSpacePages, }) { const normalized = normalizePublicHtmlRelativePath(relativePath); let page = await mindSpacePages.findPageByRelativePath(userId, normalized); const pageInput = { title: title ?? page?.title ?? 'MindSpace 页面', content, contentFormat: 'html', pageType: 'html', categoryCode: 'draft', }; if (page) { return mindSpacePages.updatePage(userId, page.id, { ...pageInput, expectedVersion: page.versionNo, }); } return mindSpacePages.createFromChat(userId, pageInput, { snapshot: { content_mode: 'static_html', relative_path: normalized, }, }); } async function publishPageToPlaza({ userId, page, mindSpacePublications, plazaPosts }) { const publication = await ensurePagePublicationForPlaza({ userId, page, mindSpacePublications, refreshIfOnline: true, }); const { post } = await ensurePlazaPostForPublication({ userId, publication, plazaPosts, }); return { pageId: page.id, publicationId: publication.id, publicUrl: publication.publicUrl ?? publication.public_url ?? null, post: { id: post.id, status: post.status, }, }; } export async function ensurePlazaPostForPublication({ userId, publication, plazaPosts, categorySlug = 'other', }) { const post = await plazaPosts.publishPostForPublication( userId, { publication_id: publication.id, category_slug: categorySlug, cover_url: '', allow_comment: true, }, { forcePublished: true, deferPostPublishedHooks: true }, ); return { post }; } export async function quickPlazaFromChat({ user, h5Root, bundle, body, mindSpacePages, mindSpacePublications, plazaPosts, publishDir, }) { if (!mindSpacePages || !mindSpacePublications || !plazaPosts) { throw Object.assign(new Error('Plaza 或 MindSpace 未启用'), { code: 'plaza_unavailable' }); } const { analysis } = bundle; const page = await ensureChatPageForPlaza({ userId: user.id, bundle, mindSpacePages, publishDir, body, skipThumbnail: true, }); if (analysis.contentMode === 'static_html' && analysis.relativePath) { await assertWorkspacePathNotAlreadyOnPlaza({ userId: user.id, relativePath: analysis.relativePath, mindSpacePages, }); } return publishPageToPlaza({ userId: user.id, page, mindSpacePublications, plazaPosts, }); } 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 页面'), { code: 'invalid_public_html_path' }); } return normalized; } export async function getQuickPlazaFromPublicHtmlStatus({ user, relativePath, mindSpacePages, mindSpacePublications, plazaPosts, }) { if (!mindSpacePages || !mindSpacePublications || !plazaPosts) { throw Object.assign(new Error('Plaza 或 MindSpace 未启用'), { code: 'plaza_unavailable' }); } const normalized = normalizePublicHtmlRelativePath(relativePath); const context = await mindSpacePages.findPlazaContextByWorkspacePath?.(user.id, normalized); if ( context?.postId && ['published', 'pending_review'].includes(String(context.postStatus ?? '')) ) { return { published: true, pageId: context.pageId, publicationId: context.publicationId, post: { id: context.postId, status: context.postStatus, }, }; } const page = context?.pageId ? { id: context.pageId } : await mindSpacePages.findPageByRelativePath(user.id, normalized); if (!page) { return { published: false }; } const publication = context?.publicationId != null ? { id: context.publicationId } : await mindSpacePublications.getCurrent(user.id, page.id); if (!publication?.id) { return { published: false, pageId: page.id }; } const post = await plazaPosts.findPostByPublicationId(publication.id, { userId: user.id }); if (!post || !['published', 'pending_review'].includes(post.status)) { return { published: false, pageId: page.id, publicationId: publication.id, }; } return { published: true, pageId: page.id, publicationId: publication.id, post: { id: post.id, status: post.status, }, }; } export async function quickPlazaFromPublicHtml({ user, relativePath, mindSpacePages, mindSpacePublications, plazaPosts, publishDir, }) { if (!mindSpacePages || !mindSpacePublications || !plazaPosts) { throw Object.assign(new Error('Plaza 或 MindSpace 未启用'), { code: 'plaza_unavailable' }); } const normalized = normalizePublicHtmlRelativePath(relativePath); await assertWorkspacePathNotAlreadyOnPlaza({ userId: user.id, relativePath: normalized, mindSpacePages, }); const resolvedRoot = path.resolve(publishDir); const filePath = path.resolve(publishDir, normalized); if (filePath !== resolvedRoot && !filePath.startsWith(`${resolvedRoot}${path.sep}`)) { throw Object.assign(new Error('路径越界'), { code: 'invalid_public_html_path' }); } let content; try { content = await fs.readFile(filePath, 'utf8'); } catch { throw Object.assign(new Error('页面文件不存在'), { code: 'static_page_not_found' }); } const page = await ensureWorkspaceHtmlPageForPlaza({ userId: user.id, relativePath: normalized, content, title: titleFromPublicHtml(content, normalized), mindSpacePages, }); return publishPageToPlaza({ userId: user.id, page, mindSpacePublications, plazaPosts, }); }