import crypto from 'node:crypto'; import fsPromises from 'node:fs/promises'; import path from 'node:path'; import { collectOwnPublicHtmlArtifactRefs, normalizePublicHtmlRelativePath, } from './mindspace-public-finish-sync.mjs'; import { buildMindSpacePublicUrlForUser, resolveMindSpaceUserPublishDir, } from './mindspace-runtime-config.mjs'; export async function registerPublicHtmlArtifactsForConversationPackage({ conversationPackageRegistry, h5Root, env = process.env, user, publishDir, sessionId, title = null, relativePaths = [], artifactRefs = [], } = {}) { if ( !conversationPackageRegistry || !user?.id || !publishDir || !sessionId || ((!Array.isArray(relativePaths) || relativePaths.length === 0) && (!Array.isArray(artifactRefs) || artifactRefs.length === 0)) ) { return []; } try { const publishRoot = path.resolve(publishDir); const packageTitle = String(title ?? '').trim() || null; const existingManifest = await conversationPackageRegistry .readManifestForSession({ userId: user.id, sessionId }) .catch(() => null); const packageRecord = existingManifest?.packageId && existingManifest.title ? { id: existingManifest.packageId } : await conversationPackageRegistry.ensurePackage({ userId: user.id, sessionId, title: packageTitle ?? existingManifest?.title ?? null, }); const recorded = []; const now = Date.now(); const refs = Array.isArray(artifactRefs) && artifactRefs.length > 0 ? artifactRefs : relativePaths.map((relativePath) => ({ relativePath })); for (const ref of refs) { const relativePath = typeof ref === 'string' ? ref : ref?.relativePath; const normalized = normalizePublicHtmlRelativePath(relativePath); if (!normalized || !normalized.toLowerCase().endsWith('.html')) continue; const absolutePath = path.resolve(publishRoot, normalized); if (absolutePath !== publishRoot && !absolutePath.startsWith(`${publishRoot}${path.sep}`)) continue; let stat = null; try { stat = await fsPromises.stat(absolutePath); } catch { continue; } if (!stat.isFile()) continue; const hash = crypto .createHash('sha256') .update(`${user.id}:${sessionId}:${normalized}`) .digest('hex') .slice(0, 16); const artifactId = `ca_public_html_${hash}`; await conversationPackageRegistry.recordArtifact({ id: artifactId, packageId: packageRecord.id, artifactKind: 'public_html', role: 'assistant', messageId: typeof ref?.messageId === 'string' && ref.messageId.trim() ? ref.messageId.trim() : null, displayName: path.posix.basename(normalized), mimeType: 'text/html', sizeBytes: stat.size, canonicalUrl: buildMindSpacePublicUrlForUser({ h5Root, env, user, relativePath: normalized, }), sortOrder: Number.isFinite(stat.mtimeMs) ? Math.round(stat.mtimeMs) : now, now, }); recorded.push({ artifactId, relativePath: normalized }); } if (recorded.length > 0) { await conversationPackageRegistry.writeManifestForSession({ userId: user.id, sessionId, }); } return recorded; } catch (error) { console.warn('[MindSpace] failed to record public html artifacts:', error?.message ?? error); return []; } } export function createConversationPackagePublicHtmlHydrator({ h5Root, resolveSessionSnapshot, registerPublicHtmlArtifactsForConversation, } = {}) { return async function hydrateConversationPackagePublicHtmlArtifacts({ user, sessionId }) { if (!user?.id || !sessionId || typeof resolveSessionSnapshot !== 'function') return []; const snapshot = await resolveSessionSnapshot(sessionId).catch(() => null); const messages = Array.isArray(snapshot?.messages) ? snapshot.messages : []; const publishDir = resolveMindSpaceUserPublishDir(h5Root, { id: user.id }); const artifactRefs = collectOwnPublicHtmlArtifactRefs({ messages, currentUser: user, publishDir, }); return ( (await registerPublicHtmlArtifactsForConversation?.({ user, publishDir, sessionId, title: snapshot?.session?.name, artifactRefs, })) ?? [] ); }; }