import crypto from 'node:crypto'; import path from 'node:path'; import { registerChatDocxArtifactForConversation } from './mindspace-chat-docx-package.mjs'; import { registerPublicHtmlArtifactsForConversationPackage } from './mindspace-conversation-package-public-html.mjs'; import { resolveMindSpaceUserPublishDir } from './mindspace-runtime-config.mjs'; function normalizeBuffer(value) { if (Buffer.isBuffer(value)) return value; if (typeof value === 'string') { return Buffer.from(value, 'utf8'); } if (ArrayBuffer.isView(value)) { return Buffer.from(value.buffer, value.byteOffset, value.byteLength); } return null; } function generatedArtifactMetadata( relativePath, ) { const extension = path.posix .extname(relativePath) .toLowerCase(); const byExtension = { '.csv': [ 'generated_file', 'text/csv', ], '.doc': [ 'docx', 'application/msword', ], '.docx': [ 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ], '.gif': ['generated_image', 'image/gif'], '.jpeg': [ 'generated_image', 'image/jpeg', ], '.jpg': [ 'generated_image', 'image/jpeg', ], '.json': [ 'generated_file', 'application/json', ], '.md': [ 'generated_file', 'text/markdown', ], '.pdf': ['pdf', 'application/pdf'], '.png': [ 'generated_image', 'image/png', ], '.svg': [ 'generated_image', 'image/svg+xml', ], '.txt': [ 'generated_file', 'text/plain', ], '.webp': [ 'generated_image', 'image/webp', ], '.xlsx': [ 'generated_file', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ], }; const [artifactKind, mimeType] = byExtension[extension] ?? [ 'generated_file', 'application/octet-stream', ]; return { artifactKind, mimeType }; } export function createMindSpaceConversationArtifactService({ conversationPackageRegistry, h5Root, env = process.env, resolveMindSpaceUserPublishDirFn = resolveMindSpaceUserPublishDir, registerPublicHtmlArtifactsFn = registerPublicHtmlArtifactsForConversationPackage, registerChatDocxArtifactFn = registerChatDocxArtifactForConversation, } = {}) { if (!conversationPackageRegistry) { throw new Error( 'createMindSpaceConversationArtifactService requires a conversation package registry', ); } if (!h5Root) { throw new Error( 'createMindSpaceConversationArtifactService requires h5Root', ); } return { async registerPublicHtmlArtifacts({ userId, sessionId, title = null, relativePaths = [], artifactRefs = [], } = {}) { const normalizedUserId = String(userId ?? '').trim(); if ( !normalizedUserId || !String(sessionId ?? '').trim() ) { return []; } const user = { id: normalizedUserId }; const publishDir = resolveMindSpaceUserPublishDirFn( h5Root, user, ); return registerPublicHtmlArtifactsFn({ conversationPackageRegistry, h5Root, env, user, publishDir, sessionId, title, relativePaths, artifactRefs, }); }, async registerChatDocxArtifact({ userId, sessionId, messageId, selectedLinkIndex = 0, title = null, filename, body, } = {}) { const buffer = normalizeBuffer(body); if ( !String(userId ?? '').trim() || !String(sessionId ?? '').trim() || !String(messageId ?? '').trim() || !String(filename ?? '').trim() || !buffer ) { return null; } return registerChatDocxArtifactFn({ registry: conversationPackageRegistry, user: { id: String(userId).trim() }, bundle: { source: { session: { name: String(title ?? '').trim() || null, }, }, }, requestBody: { session_id: String(sessionId).trim(), message_id: String(messageId).trim(), selected_link_index: selectedLinkIndex, }, filename: String(filename).trim(), buffer, }); }, async registerPublishedLongImageArtifact({ ownerId, sessionId, messageId = null, publicationId, pageId = null, title = null, filename = null, body, canonicalUrl = null, } = {}) { const normalizedOwnerId = String(ownerId ?? '').trim(); const normalizedSessionId = String(sessionId ?? '').trim(); const normalizedPublicationId = String(publicationId ?? '').trim(); const buffer = normalizeBuffer(body); if ( !normalizedOwnerId || !normalizedSessionId || !normalizedPublicationId || !buffer ) { return null; } const hash = crypto .createHash('sha256') .update( `${normalizedOwnerId}:${normalizedSessionId}:${normalizedPublicationId}:long-image`, ) .digest('hex') .slice(0, 16); const artifactId = `ca_long_image_${hash}`; const displayName = String(filename ?? '').trim() || `${normalizedPublicationId}.long.png`; const relativePath = `artifacts/${artifactId}/${path.posix.basename(displayName)}`; const now = Date.now(); const { packageRecord, writeResult } = await conversationPackageRegistry.putObjectForSession({ userId: normalizedOwnerId, sessionId: normalizedSessionId, title: String(title ?? '').trim() || null, relativePath, body: buffer, }); const artifact = await conversationPackageRegistry.recordArtifact({ id: artifactId, packageId: packageRecord.id, artifactKind: 'long_image', role: 'assistant', pageId: String(pageId ?? '').trim() || null, publicationId: normalizedPublicationId, messageId: String(messageId ?? '').trim() || null, displayName: path.posix.basename(displayName), mimeType: 'image/png', sizeBytes: buffer.length, storageKey: writeResult.key, canonicalUrl: String(canonicalUrl ?? '').trim() || null, sortOrder: now, now, }); await conversationPackageRegistry.writeManifestForSession({ userId: normalizedOwnerId, sessionId: normalizedSessionId, }); return artifact; }, async registerWorkspaceFileArtifact({ userId, sessionId, messageId = null, title = null, relativePath, body, } = {}) { const normalizedUserId = String( userId ?? '', ).trim(); const normalizedSessionId = String( sessionId ?? '', ).trim(); const normalizedRelativePath = String( relativePath ?? '', ) .trim() .replace(/\\/g, '/'); const buffer = normalizeBuffer(body); if ( !normalizedUserId || !normalizedSessionId || !normalizedRelativePath || !buffer ) { return null; } const hash = crypto .createHash('sha256') .update( `${normalizedUserId}:${normalizedSessionId}:${normalizedRelativePath}`, ) .digest('hex') .slice(0, 20); const artifactId = `ca_workspace_write_${hash}`; const displayName = path.posix.basename( normalizedRelativePath, ); const packageRelativePath = `artifacts/${artifactId}/${displayName}`; const metadata = generatedArtifactMetadata( normalizedRelativePath, ); const now = Date.now(); const { packageRecord, writeResult } = await conversationPackageRegistry .putObjectForSession({ userId: normalizedUserId, sessionId: normalizedSessionId, title: String(title ?? '').trim() || null, relativePath: packageRelativePath, body: buffer, }); const artifact = await conversationPackageRegistry .recordArtifact({ id: artifactId, packageId: packageRecord.id, artifactKind: metadata.artifactKind, role: 'assistant', messageId: String(messageId ?? '').trim() || null, displayName, mimeType: metadata.mimeType, sizeBytes: buffer.length, storageKey: writeResult.key, canonicalUrl: null, sortOrder: now, now, }); await conversationPackageRegistry .writeManifestForSession({ userId: normalizedUserId, sessionId: normalizedSessionId, }); return artifact; }, }; } export const mindSpaceConversationArtifactServiceInternals = { normalizeBuffer, generatedArtifactMetadata, };