import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { inlinePrivateAssetsInHtml } from '../mindspace-pages.mjs'; import { getQuickPlazaFromPublicHtmlStatus, quickPlazaFromChat, quickPlazaFromPublicHtml, } from '../mindspace-chat-plaza.mjs'; import { rewriteWorkspacePublicAssetReferences } from '../mindspace-publications.mjs'; import { buildMindSpacePublicUrlForUser, resolveMindSpaceRuntimeConfig, resolveMindSpaceUserPublishDir, } from '../mindspace-runtime-config.mjs'; import { PUBLIC_ZONE_DIR } from '../user-publish.mjs'; function assertRouter(api) { if ( !api || typeof api.get !== 'function' || typeof api.post !== 'function' ) { throw new Error( 'attachPortalMindSpaceChatShareRoutes requires an Express-compatible router', ); } } export function attachPortalMindSpaceChatShareRoutes( api, { h5Root = '', env = process.env, getMindSpacePages = () => null, getMindSpacePublications = () => null, getPlazaPosts = () => null, getAuthPool = () => null, resolveChatSaveBundle, resolvePlazaPostUrl, mapPlazaError = () => 500, sendData = (res, _req, data, status = 200) => res.status(status).json({ data }), sendError = (res, _req, status, code, message) => res.status(status).json({ error: { code, message } }), handlePlazaError = (_res, _req, error) => { throw error; }, handleMindSpaceError = (_res, _req, error) => { throw error; }, resolveMindSpaceRuntimeConfigFn = resolveMindSpaceRuntimeConfig, inlinePrivateAssetsInHtmlFn = inlinePrivateAssetsInHtml, resolveMindSpaceUserPublishDirFn = resolveMindSpaceUserPublishDir, rewriteWorkspacePublicAssetReferencesFn = rewriteWorkspacePublicAssetReferences, buildMindSpacePublicUrlForUserFn = buildMindSpacePublicUrlForUser, quickPlazaFromChatFn = quickPlazaFromChat, getQuickPlazaFromPublicHtmlStatusFn = getQuickPlazaFromPublicHtmlStatus, quickPlazaFromPublicHtmlFn = quickPlazaFromPublicHtml, mkdir = fs.mkdir, writeFile = fs.writeFile, randomUUID = crypto.randomUUID, now = Date.now, logger = console, } = {}, ) { assertRouter(api); if ( typeof resolveChatSaveBundle !== 'function' || typeof resolvePlazaPostUrl !== 'function' ) { throw new Error( 'attachPortalMindSpaceChatShareRoutes requires chat and Plaza resolvers', ); } api.post('/mindspace/v1/pages/quick-share-from-chat', async (req, res) => { if (!getMindSpacePages()) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const bundle = await resolveChatSaveBundle( req.currentUser, h5Root, req.body, ); logger.log( '[quick-share] bundle resolved, hasHtml:', Boolean(bundle.resolvedHtml), ); if (!bundle.resolvedHtml) { throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found', }); } const { storageRoot } = resolveMindSpaceRuntimeConfigFn( h5Root, env, ); const { html: localizedHtml } = await inlinePrivateAssetsInHtmlFn( getAuthPool(), storageRoot, req.currentUser.id, bundle.resolvedHtml.content, ); const publishDir = resolveMindSpaceUserPublishDirFn( h5Root, req.currentUser, ); const sharedDir = path.join( publishDir, PUBLIC_ZONE_DIR, 'shared', ); await mkdir(sharedDir, { recursive: true }); const basename = bundle.resolvedHtml.filename.replace( /\.html$/i, '', ); const filename = `${basename}-${randomUUID().slice(0, 8)}.html`; const sharedRelativePath = `${PUBLIC_ZONE_DIR}/shared/${filename}`; const sharedHtml = rewriteWorkspacePublicAssetReferencesFn( localizedHtml, sharedRelativePath, ); const destPath = path.join(sharedDir, filename); await writeFile(destPath, sharedHtml, 'utf8'); const publicUrl = buildMindSpacePublicUrlForUserFn({ h5Root, env, user: req.currentUser.id, relativePath: sharedRelativePath, }); return res.status(201).json({ data: { publicUrl, filename, }, }); } catch (error) { logger.error('[quick-share] error:', error); return handleMindSpaceError(res, req, error); } }); api.post('/mindspace/v1/pages/quick-plaza-from-chat', async (req, res) => { const pages = getMindSpacePages(); const publications = getMindSpacePublications(); const plazaPosts = getPlazaPosts(); if (!pages || !publications || !plazaPosts) { return sendError( res, req, 503, 'plaza_unavailable', 'Plaza 或 MindSpace 未启用', ); } const startedAt = now(); try { const bundle = await resolveChatSaveBundle( req.currentUser, h5Root, req.body, ); const publishDir = resolveMindSpaceUserPublishDirFn( h5Root, req.currentUser, ); const result = await quickPlazaFromChatFn({ user: req.currentUser, h5Root, bundle, body: req.body ?? {}, mindSpacePages: pages, mindSpacePublications: publications, plazaPosts, publishDir, }); logger.info('[quick-plaza] ok', { ms: now() - startedAt, pageId: result.pageId, publicationId: result.publicationId, postId: result.post?.id, }); return sendData(res, req, result, 201); } catch (error) { logger.error('[quick-plaza] error:', { ms: now() - startedAt, error, }); if (error?.code && mapPlazaError(error) !== 500) { return handlePlazaError(res, req, error); } return handleMindSpaceError(res, req, error); } }); api.get( '/mindspace/v1/pages/quick-plaza-from-public-html/status', async (req, res) => { const pages = getMindSpacePages(); const publications = getMindSpacePublications(); const plazaPosts = getPlazaPosts(); if (!pages || !publications || !plazaPosts) { return sendError( res, req, 503, 'plaza_unavailable', 'Plaza 或 MindSpace 未启用', ); } try { const relativePath = String( req.query?.relative_path ?? req.query?.relativePath ?? '', ).trim(); const result = await getQuickPlazaFromPublicHtmlStatusFn({ user: req.currentUser, relativePath, mindSpacePages: pages, mindSpacePublications: publications, plazaPosts, }); const postId = result.post?.id; return sendData(res, req, { ...result, plaza_url: postId ? resolvePlazaPostUrl(postId, req) : null, }); } catch (error) { if (error?.code && mapPlazaError(error) !== 500) { return handlePlazaError(res, req, error); } return handleMindSpaceError(res, req, error); } }, ); api.post( '/mindspace/v1/pages/quick-plaza-from-public-html', async (req, res) => { const pages = getMindSpacePages(); const publications = getMindSpacePublications(); const plazaPosts = getPlazaPosts(); if (!pages || !publications || !plazaPosts) { return sendError( res, req, 503, 'plaza_unavailable', 'Plaza 或 MindSpace 未启用', ); } const startedAt = now(); try { const relativePath = String( req.body?.relative_path ?? req.body?.relativePath ?? '', ).trim(); const publishDir = resolveMindSpaceUserPublishDirFn( h5Root, req.currentUser, ); const result = await quickPlazaFromPublicHtmlFn({ user: req.currentUser, relativePath, mindSpacePages: pages, mindSpacePublications: publications, plazaPosts, publishDir, }); const plazaUrl = resolvePlazaPostUrl( result.post.id, req, ); logger.info('[quick-plaza-public-html] ok', { ms: now() - startedAt, pageId: result.pageId, publicationId: result.publicationId, postId: result.post?.id, relativePath, }); return sendData( res, req, { ...result, plaza_url: plazaUrl, }, 201, ); } catch (error) { logger.error('[quick-plaza-public-html] error:', { ms: now() - startedAt, error, }); if (error?.code === 'ALREADY_PUBLISHED') { const postId = error?.details?.post_id ?? error?.details?.postId; if (postId) { error.details = { ...(error.details ?? {}), plaza_url: resolvePlazaPostUrl(postId, req), }; } } if (error?.code && mapPlazaError(error) !== 500) { return handlePlazaError(res, req, error); } return handleMindSpaceError(res, req, error); } }, ); }