function assertRouter(api) { if ( !api || typeof api.get !== 'function' || typeof api.post !== 'function' || typeof api.put !== 'function' || typeof api.delete !== 'function' ) { throw new Error( 'attachPortalMindSpacePageCoreRoutes requires an Express-compatible router', ); } } const DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS = 30_000; export function createMindSpacePageListSyncScheduler({ syncUserGeneratedPages, logger = console, minIntervalMs = DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS, now = () => Date.now(), } = {}) { if (typeof syncUserGeneratedPages !== 'function') { throw new Error('createMindSpacePageListSyncScheduler requires syncUserGeneratedPages'); } const inFlightByUser = new Map(); const lastStartedAtByUser = new Map(); return function schedulePageListSync(userId) { if (!userId) return null; const inFlight = inFlightByUser.get(userId); if (inFlight) return inFlight; const currentTime = now(); const lastStartedAt = lastStartedAtByUser.get(userId); if ( Number.isFinite(minIntervalMs) && minIntervalMs > 0 && Number.isFinite(lastStartedAt) && currentTime - lastStartedAt < minIntervalMs ) { return null; } lastStartedAtByUser.set(userId, currentTime); const task = Promise.resolve() .then(() => syncUserGeneratedPages(userId)) .catch((error) => { logger?.warn?.( `[MindSpace] background page list sync failed for user ${userId}:`, error instanceof Error ? error.message : error, ); return null; }) .finally(() => { if (inFlightByUser.get(userId) === task) { inFlightByUser.delete(userId); } }); inFlightByUser.set(userId, task); return task; }; } export function attachPortalMindSpacePageCoreRoutes( api, { getMindSpacePages = () => null, getMindSpacePublications = () => null, getMindSpace = () => null, getMindSpaceAudit = () => null, getMindSpacePageLiveEdit = () => null, getMindSpacePageEditSession = () => null, getUserAuth = () => null, syncUserGeneratedPages, ownsAgentSession, ensureMindSpaceEnabled, sendData, sendError, handleMindSpaceError, logger = console, pageListSyncIntervalMs = DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS, } = {}, ) { assertRouter(api); if ( typeof syncUserGeneratedPages !== 'function' || typeof ownsAgentSession !== 'function' || typeof ensureMindSpaceEnabled !== 'function' || typeof sendData !== 'function' || typeof sendError !== 'function' || typeof handleMindSpaceError !== 'function' ) { throw new Error( 'attachPortalMindSpacePageCoreRoutes requires page route dependencies', ); } const schedulePageListSync = createMindSpacePageListSyncScheduler({ syncUserGeneratedPages, logger, minIntervalMs: pageListSyncIntervalMs, }); api.post('/mindspace/v1/pages', async (req, res) => { const pages = getMindSpacePages(); if (!pages) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const page = await pages.createPage(req.currentUser.id, { title: req.body?.title, summary: req.body?.summary, content: req.body?.content, contentFormat: req.body?.content_format === 'html' ? 'html' : undefined, templateId: req.body?.template_id, pageType: req.body?.page_type, categoryCode: req.body?.category_code, }); return res.status(201).json({ data: page }); } catch (error) { return handleMindSpaceError(res, req, error); } }); api.get('/mindspace/v1/pages', async (req, res) => { const pages = getMindSpacePages(); if (!pages) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { // Keep remote/public HTML discovery alive, but never put the full // workspace delivery pass on the latency path for listing pages. schedulePageListSync(req.currentUser.id); const limit = Number.parseInt(String(req.query.limit ?? ''), 10); const offset = Number.parseInt(String(req.query.offset ?? ''), 10); const result = await pages.listPages(req.currentUser.id, { status: typeof req.query.status === 'string' ? req.query.status : undefined, categoryCode: typeof req.query.category_code === 'string' ? req.query.category_code : undefined, ...(Number.isFinite(limit) ? { limit } : {}), ...(Number.isFinite(offset) ? { offset } : {}), }); return res.json({ data: result.items, page: { total: result.total, limit: result.limit, offset: result.offset, has_more: result.hasMore, next_cursor: null, }, }); } catch (error) { return handleMindSpaceError(res, req, error); } }); api.get('/mindspace/v1/pages/:pageId', async (req, res) => { const pages = getMindSpacePages(); if (!pages) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const [page, versions, publication] = await Promise.all([ pages.getPage(req.currentUser.id, req.params.pageId), pages.listVersions(req.currentUser.id, req.params.pageId), getMindSpacePublications()?.getCurrent( req.currentUser.id, req.params.pageId, ) ?? null, ]); return res.json({ data: { ...page, versions, publication, }, }); } catch (error) { return handleMindSpaceError(res, req, error); } }); api.delete('/mindspace/v1/pages/:pageId', async (req, res) => { const pages = getMindSpacePages(); if (!pages || !ensureMindSpaceEnabled(res, req)) return; try { const removeFromPlaza = String( req.query?.remove_from_plaza ?? req.body?.remove_from_plaza ?? '', ).toLowerCase() === 'true' || req.query?.remove_from_plaza === '1' || req.body?.remove_from_plaza === true; const result = await pages.deletePage( req.currentUser.id, req.params.pageId, { removeFromPlaza }, ); const quota = await getMindSpace().getQuota(req.currentUser.id); await getMindSpaceAudit()?.write({ userId: req.currentUser.id, action: 'page.delete', objectType: 'page', objectId: req.params.pageId, ip: req.ip, detail: { offlinedPublicationCount: result.offlinedPublicationCount, hiddenPlazaPostCount: result.hiddenPlazaPostCount, deletedAssetCount: result.deletedAssetCount, freedBytes: result.freedBytes, }, }); return sendData(res, req, { ...result, quota }); } catch (error) { return handleMindSpaceError(res, req, error); } }); api.get( '/mindspace/v1/pages/:pageId/delete-preview', async (req, res) => { const pages = getMindSpacePages(); if (!pages || !ensureMindSpaceEnabled(res, req)) return; try { return sendData( res, req, await pages.getDeletePreview( req.currentUser.id, req.params.pageId, ), ); } catch (error) { return handleMindSpaceError(res, req, error); } }, ); api.put('/mindspace/v1/pages/:pageId', async (req, res) => { const pages = getMindSpacePages(); if (!pages) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const page = await pages.updatePage( req.currentUser.id, req.params.pageId, { expectedVersion: req.body?.expected_version, title: req.body?.title, summary: req.body?.summary, content: req.body?.content, templateId: req.body?.template_id, pageType: req.body?.page_type, changeNote: req.body?.change_note, }, ); return res.json({ data: page }); } catch (error) { return handleMindSpaceError(res, req, error); } }); api.post( '/mindspace/v1/pages/:pageId/live-edit/bind', async (req, res) => { const pageLiveEdit = getMindSpacePageLiveEdit(); const pages = getMindSpacePages(); if (!pageLiveEdit || !pages) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const sessionId = String(req.body?.session_id ?? '').trim(); if (!sessionId) { return sendError( res, req, 400, 'invalid_request', '缺少 session_id', ); } const owns = await ownsAgentSession( req.currentUser.id, sessionId, ); if (!owns) { return sendError( res, req, 403, 'forbidden', '无权绑定该 Agent 会话', ); } await pages.getPage( req.currentUser.id, req.params.pageId, ); const parentSessionId = String(req.body?.parent_session_id ?? '').trim() || null; return sendData( res, req, pageLiveEdit.bindSession({ userId: req.currentUser.id, sessionId, pageId: req.params.pageId, parentSessionId, }), ); } catch (error) { return handleMindSpaceError(res, req, error); } }, ); api.post( '/mindspace/v1/pages/:pageId/live-edit/fork-session', async (req, res) => { const pageEditSession = getMindSpacePageEditSession(); if (!pageEditSession || !getMindSpacePages()) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const parentSessionId = String( req.body?.parent_session_id ?? '', ).trim(); if (!parentSessionId) { return sendError( res, req, 400, 'invalid_request', '缺少 parent_session_id', ); } const gate = await getUserAuth().canUseChat( req.currentUser.id, ); if (!gate.ok) { return sendError( res, req, 402, gate.code ?? 'insufficient_balance', gate.message, ); } return sendData( res, req, await pageEditSession.forkSession({ userId: req.currentUser.id, pageId: req.params.pageId, parentSessionId, h5ApiBase: String( req.body?.h5_api_base ?? req.body?.h5ApiBase ?? '', ).trim() || null, }), ); } catch (error) { if (error?.code === 'forbidden') { return sendError( res, req, 403, error.code, error.message, ); } if (error?.code === 'invalid_request') { return sendError( res, req, 400, error.code, error.message, ); } return handleMindSpaceError(res, req, error); } }, ); api.post( '/mindspace/v1/pages/:pageId/live-edit/close-session', async (req, res) => { const pageEditSession = getMindSpacePageEditSession(); if (!pageEditSession || !getMindSpacePages()) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { const sessionId = String(req.body?.session_id ?? '').trim(); if (!sessionId) { return sendError( res, req, 400, 'invalid_request', '缺少 session_id', ); } return sendData( res, req, await pageEditSession.closeSession({ userId: req.currentUser.id, pageId: req.params.pageId, sessionId, parentSessionId: String(req.body?.parent_session_id ?? '').trim() || null, summary: String(req.body?.summary ?? ''), }), ); } catch (error) { if ( error?.code === 'forbidden' || error?.code === 'page_binding_mismatch' ) { return sendError( res, req, 403, error.code, error.message, ); } if (error?.code === 'invalid_request') { return sendError( res, req, 400, error.code, error.message, ); } return handleMindSpaceError(res, req, error); } }, ); api.get( '/mindspace/v1/pages/:pageId/live-edit/revision', async (req, res) => { const pageLiveEdit = getMindSpacePageLiveEdit(); if (!pageLiveEdit || !getMindSpacePages()) { return res.status(503).json({ message: 'MindSpace 未启用' }); } try { return sendData( res, req, await pageLiveEdit.getRevisionSnapshot( req.currentUser.id, req.params.pageId, ), ); } catch (error) { return handleMindSpaceError(res, req, error); } }, ); }