import { collectOwnPublicHtmlArtifactRefs, materializePublicHtmlWritesFromSessionEvent, syncPublicHtmlAfterFinish, } from './mindspace-public-finish-sync.mjs'; import { buildMindSpacePublicUrlForUser, resolveMindSpaceUserPublishDir, } from './mindspace-runtime-config.mjs'; import { evaluateH5HtmlFinishGuard, } from './mindspace-h5-html-finish-guard.mjs'; import { buildPageDataDeliveryArtifactsFromBindResult, ensurePageDataDeliveryReady, maybeAutoBindPageDataHtmlPages, preparePageDataAfterFinish, resolvePageDataCollectOutcomeAsync, rewritePageDataDeliveryLinks, } from './mindspace-page-data-finish-guard.mjs'; import { createPageService, } from './mindspace-pages.mjs'; import { ensureWechatFreshPageThumbnailsAtWorkspace, prepareWechatHtmlDeliveryAtWorkspace, } from './mindspace-wechat-html-delivery.mjs'; function eventMessages(event, recentCount) { if (event?.type === 'Message' && event.message) { return [event.message]; } if ( event?.type === 'UpdateConversation' && Array.isArray(event.conversation) ) { return event.conversation.slice( -Math.max(1, recentCount), ); } return []; } function sanitizePageDataPreparation(value) { if (!value) return null; return JSON.parse( JSON.stringify(value, (key, item) => { if ( [ 'absolutePath', 'content', 'localPath', ].includes(key) ) { return undefined; } if (item instanceof Map) { return Object.fromEntries(item); } return item; }), ); } export function createMindSpacePublicFinishService({ pool, h5Root, storageRoot, env = process.env, syncWorkspaceAssets = null, conversationArtifactService, resolveMindSpaceUserPublishDirFn = resolveMindSpaceUserPublishDir, buildMindSpacePublicUrlForUserFn = buildMindSpacePublicUrlForUser, materializeSessionEventFn = materializePublicHtmlWritesFromSessionEvent, collectArtifactRefsFn = collectOwnPublicHtmlArtifactRefs, syncAfterFinishFn = syncPublicHtmlAfterFinish, evaluateH5HtmlFinishGuardFn = evaluateH5HtmlFinishGuard, preparePageDataAfterFinishFn = preparePageDataAfterFinish, resolvePageDataCollectOutcomeAsyncFn = resolvePageDataCollectOutcomeAsync, maybeAutoBindPageDataHtmlPagesFn = maybeAutoBindPageDataHtmlPages, buildPageDataDeliveryArtifactsFromBindResultFn = buildPageDataDeliveryArtifactsFromBindResult, ensurePageDataDeliveryReadyFn = ensurePageDataDeliveryReady, rewritePageDataDeliveryLinksFn = rewritePageDataDeliveryLinks, createPageServiceFn = createPageService, prepareWechatHtmlDeliveryAtWorkspaceFn = prepareWechatHtmlDeliveryAtWorkspace, ensureWechatFreshPageThumbnailsAtWorkspaceFn = ensureWechatFreshPageThumbnailsAtWorkspace, } = {}) { if ( !pool || typeof pool.query !== 'function' || !h5Root || !storageRoot ) { throw new Error( 'createMindSpacePublicFinishService requires pool, h5Root, and storageRoot', ); } if ( !conversationArtifactService || typeof conversationArtifactService .registerPublicHtmlArtifacts !== 'function' ) { throw new Error( 'createMindSpacePublicFinishService requires conversationArtifactService', ); } const resolveContext = (userId) => { const normalizedUserId = String(userId ?? '').trim(); if (!normalizedUserId) { throw Object.assign( new Error('MindSpace public finish requires userId'), { code: 'invalid_input' }, ); } const currentUser = { id: normalizedUserId }; return { currentUser, publishDir: resolveMindSpaceUserPublishDirFn( h5Root, currentUser, ), }; }; const attachCanonicalUrls = (result, currentUser) => { const artifactRefs = Array.isArray( result?.publicHtmlArtifactRefs, ) ? result.publicHtmlArtifactRefs : []; return { ...result, publicHtmlArtifacts: artifactRefs.map((ref) => ({ ...ref, canonicalUrl: buildMindSpacePublicUrlForUserFn({ h5Root, env, user: currentUser, relativePath: ref.relativePath, }), })), }; }; const buildCanonicalUrl = ( currentUser, relativePath, ) => buildMindSpacePublicUrlForUserFn({ h5Root, env, user: currentUser, relativePath, }); const syncAndRegisterWechatArtifacts = async ({ currentUser, sessionId, relativePaths, }) => { const normalizedRelativePaths = [ ...new Set( relativePaths .map((value) => String(value ?? '').trim(), ) .filter(Boolean), ), ]; if (normalizedRelativePaths.length === 0) { return; } if ( typeof syncWorkspaceAssets === 'function' ) { await syncWorkspaceAssets(currentUser.id, { categoryCode: 'public', sourceSessionId: String(sessionId ?? '').trim() || null, onlyRelativePaths: normalizedRelativePaths, }); } if (String(sessionId ?? '').trim()) { await conversationArtifactService .registerPublicHtmlArtifacts({ userId: currentUser.id, sessionId, relativePaths: normalizedRelativePaths.filter( (relativePath) => relativePath .toLowerCase() .endsWith('.html'), ), }); } }; const preparePageDataForUser = async ({ userId, messages, userText = '', } = {}) => { const { currentUser, publishDir } = resolveContext(userId); return sanitizePageDataPreparation( await preparePageDataAfterFinishFn({ userId: currentUser.id, publishDir, messages, pool, h5Root, storageRoot, userText, }), ); }; const prepareWechatPageDataForUser = async ({ userId, reply = null, intent = null, publicBaseUrl = null, requestStartedAt = 0, } = {}) => { const { currentUser, publishDir } = resolveContext(userId); const normalizedReply = { text: String(reply?.text ?? ''), messages: Array.isArray(reply?.messages) ? reply.messages : [], }; const normalizedIntent = { agentText: String(intent?.agentText ?? ''), displayText: String( intent?.displayText ?? '', ), }; let outcome = await resolvePageDataCollectOutcomeAsyncFn({ reply: normalizedReply, intent: normalizedIntent, publishDir, requestStartedAt, pool, userId: currentUser.id, findPageByRelativePath: null, apiBase: publicBaseUrl, }); let autoBind = null; if (outcome?.action === 'skip') { return sanitizePageDataPreparation({ outcome, autoBind, deliveryArtifacts: [], deliveryCheck: null, rewrittenText: normalizedReply.text, }); } const pageService = createPageServiceFn(pool, { h5Root, storageRoot, }); const findPageByRelativePath = pageService.findPageByRelativePath.bind( pageService, ); autoBind = await maybeAutoBindPageDataHtmlPagesFn({ pool, userId: currentUser.id, publishDir, h5Root, storageRoot, findPageByRelativePath, }); outcome = await resolvePageDataCollectOutcomeAsyncFn({ reply: normalizedReply, intent: normalizedIntent, publishDir, requestStartedAt, pool, userId: currentUser.id, findPageByRelativePath, apiBase: publicBaseUrl, }); let deliveryArtifacts = []; let deliveryCheck = null; let rewrittenText = normalizedReply.text; if (outcome?.action === 'send') { deliveryArtifacts = buildPageDataDeliveryArtifactsFromBindResultFn( autoBind, publishDir, { publicBaseUrl }, ); if (deliveryArtifacts.length > 0) { deliveryCheck = await ensurePageDataDeliveryReadyFn({ publishDir, userId: currentUser.id, pool, h5Root, storageRoot, apiBase: publicBaseUrl, artifacts: deliveryArtifacts, }); rewrittenText = rewritePageDataDeliveryLinksFn( normalizedReply.text, deliveryArtifacts, ); } } return sanitizePageDataPreparation({ outcome, autoBind, deliveryArtifacts, deliveryCheck, rewrittenText, }); }; const prepareWechatHtmlForUser = async ({ userId, sessionId = null, reply = null, intent = null, requestStartedAt = 0, allowRecentArtifacts = true, } = {}) => { const { currentUser, publishDir } = resolveContext(userId); const normalizedReply = { text: String(reply?.text ?? ''), messages: Array.isArray(reply?.messages) ? reply.messages : [], }; const normalizedIntent = { agentText: String( intent?.agentText ?? '', ), displayText: String( intent?.displayText ?? '', ), }; const prepared = prepareWechatHtmlDeliveryAtWorkspaceFn({ reply: normalizedReply, intent: normalizedIntent, publishDir, requestStartedAt, allowRecentArtifacts: allowRecentArtifacts === true, buildCanonicalUrl: (relativePath) => buildCanonicalUrl( currentUser, relativePath, ), }); await syncAfterFinishFn({ messages: normalizedReply.messages, currentUser, publishDir, sessionId, pool, storageRoot, h5Root, syncWorkspaceAssets, registerPublicHtmlArtifacts: (_registeredUserId, options) => conversationArtifactService .registerPublicHtmlArtifacts({ userId: currentUser.id, sessionId: options?.sessionId, relativePaths: options?.relativePaths, artifactRefs: options?.artifactRefs, }), }); const confirmedRelativePaths = prepared.confirmedArtifacts.map( (artifact) => artifact.relativePath, ); await syncAndRegisterWechatArtifacts({ currentUser, sessionId, relativePaths: confirmedRelativePaths, }); return prepared; }; const ensureWechatFreshPageThumbnailsForUser = async ({ userId, sessionId = null, artifacts = [], images = [], messages = [], repairEnabled = false, } = {}) => { const { currentUser, publishDir } = resolveContext(userId); const result = await ensureWechatFreshPageThumbnailsAtWorkspaceFn( { artifacts, images, messages: Array.isArray(messages) ? messages : [], publishDir, repairEnabled: repairEnabled === true, buildCanonicalUrl: (relativePath) => buildCanonicalUrl( currentUser, relativePath, ), }, ); if (result.ok) { await syncAndRegisterWechatArtifacts({ currentUser, sessionId, relativePaths: [ ...result.matchRelativePaths, ...result.thumbnailRelativePaths, ], }); } return result; }; return { async materializeSessionEvent({ userId, event, recentCount = 20, } = {}) { const { currentUser, publishDir } = resolveContext(userId); const result = materializeSessionEventFn(event, { publishDir, recentCount, }); const publicHtmlArtifactRefs = collectArtifactRefsFn({ messages: eventMessages(event, recentCount), currentUser, publishDir, materialized: result?.materialized, skipped: result?.skipped, }); return attachCanonicalUrls( { ...result, publicHtmlRelativePaths: publicHtmlArtifactRefs.map( (ref) => ref.relativePath, ), publicHtmlArtifactRefs, }, currentUser, ); }, async syncAfterFinish({ userId, sessionId, messages, currentUser: suppliedCurrentUser = null, } = {}) { const { currentUser, publishDir } = resolveContext(userId); const result = await syncAfterFinishFn({ messages, currentUser, publishDir, sessionId, pool, storageRoot, h5Root, syncWorkspaceAssets, registerPublicHtmlArtifacts: (_registeredUserId, options) => conversationArtifactService .registerPublicHtmlArtifacts({ userId: currentUser.id, sessionId: options?.sessionId, relativePaths: options?.relativePaths, artifactRefs: options?.artifactRefs, }), }); const evaluated = evaluateH5HtmlFinishGuardFn({ messages, currentUser: { ...suppliedCurrentUser, id: currentUser.id, }, publishDir, syncResult: result, }); const { linkExists: _linkExists, ...deliveryEvaluation } = evaluated ?? {}; return attachCanonicalUrls( { ...result, deliveryEvaluation, }, currentUser, ); }, preparePageDataAfterFinish: preparePageDataForUser, prepareWechatPageDataDelivery: prepareWechatPageDataForUser, prepareWechatHtmlDelivery: prepareWechatHtmlForUser, ensureWechatFreshPageThumbnails: ensureWechatFreshPageThumbnailsForUser, }; } export const mindSpacePublicFinishServiceInternals = { eventMessages, sanitizePageDataPreparation, };