From 39e4fffe9120ab2318e3cd52ff1412874efe9e01 Mon Sep 17 00:00:00 2001 From: john Date: Thu, 2 Jul 2026 21:22:43 +0800 Subject: [PATCH] Record page drafts in conversation packages --- mindspace-pages.mjs | 64 +++++++++++++++++++++++++++++++++- mindspace-pages.test.mjs | 75 ++++++++++++++++++++++++++++++++++++++++ server.mjs | 23 ++++++++---- 3 files changed, 155 insertions(+), 7 deletions(-) diff --git a/mindspace-pages.mjs b/mindspace-pages.mjs index 9b661b8..6266544 100644 --- a/mindspace-pages.mjs +++ b/mindspace-pages.mjs @@ -257,6 +257,7 @@ export function createPageService(pool, options = {}) { const storageRoot = path.resolve(options.storageRoot ?? path.join(process.cwd(), 'data', 'mindspace')); const h5Root = options.h5Root ? path.resolve(options.h5Root) : null; const idFactory = options.idFactory ?? (() => crypto.randomUUID()); + const conversationPackageRegistry = options.conversationPackageRegistry ?? null; const resolveWorkspacePublishDir = async (userId) => { if (!h5Root) return null; @@ -553,7 +554,20 @@ export function createPageService(pool, options = {}) { }).catch(() => {}); }); } - return getPage(userId, pageId); + const page = await getPage(userId, pageId); + await registerPageArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + source, + page, + contentAssetId, + contentMimeType, + contentBytes: normalized.contentBytes, + storageKey: stored.storageKey, + versionNo: nextVersionNo, + now, + }); + return page; } catch (error) { await conn.rollback(); if (writtenPath) await fs.rm(writtenPath, { force: true }).catch(() => {}); @@ -1438,6 +1452,53 @@ export function createPageService(pool, options = {}) { }; } +async function registerPageArtifactForConversation({ + registry, + userId, + source, + page, + contentAssetId, + contentMimeType, + contentBytes, + storageKey, + versionNo, + now, +}) { + const sessionId = String(source?.sessionId ?? '').trim(); + if (!registry || !sessionId || !page?.id) return null; + try { + const packageRecord = await registry.ensurePackage({ + userId, + sessionId, + title: page.title, + now, + }); + const artifact = await registry.recordArtifact({ + id: `ca_${page.id}_${page.currentVersionId}`, + packageId: packageRecord.id, + artifactKind: 'page', + role: 'assistant', + assetId: contentAssetId, + pageId: page.id, + messageId: source.messageId ?? page.sourceMessageId ?? null, + displayName: page.title, + mimeType: contentMimeType, + sizeBytes: contentBytes, + storageKey, + sortOrder: Number.isFinite(Number(versionNo)) ? Number(versionNo) : 0, + now, + }); + await registry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn( + '[MindSpace] conversation package page artifact registration failed:', + error instanceof Error ? error.message : error, + ); + return null; + } +} + export const pageInternals = { escapeHtml, normalizePageInput, @@ -1449,6 +1510,7 @@ export const pageInternals = { previewContentSecurityPolicy, isHtmlPage, renderPublicationHtml, + registerPageArtifactForConversation, redactContent, buildPageDeleteSummary, }; diff --git a/mindspace-pages.test.mjs b/mindspace-pages.test.mjs index aeec147..6105751 100644 --- a/mindspace-pages.test.mjs +++ b/mindspace-pages.test.mjs @@ -132,3 +132,78 @@ test('normalizeWorkspaceRelativePath unifies bare html filenames under public/', assert.equal(normalizeWorkspaceRelativePath('public/ai-robot-report.html'), 'public/ai-robot-report.html'); assert.equal(normalizeWorkspaceRelativePath('/public/demo.html'), 'public/demo.html'); }); + +test('registerPageArtifactForConversation records page artifact and refreshes manifest', async () => { + const calls = []; + const registry = { + async ensurePackage(input) { + calls.push(['ensurePackage', input]); + return { id: 'cp_session-1' }; + }, + async recordArtifact(input) { + calls.push(['recordArtifact', input]); + return input; + }, + async writeManifestForSession(input) { + calls.push(['writeManifestForSession', input]); + return { storageKey: 'users/user-1/conversations/session-1/manifest.json' }; + }, + }; + + const result = await pageInternals.registerPageArtifactForConversation({ + registry, + userId: 'user-1', + source: { sessionId: 'session-1', messageId: 'message-1' }, + page: { + id: 'page-1', + currentVersionId: 'version-1', + title: '旅行计划', + sourceMessageId: 'message-fallback', + }, + contentAssetId: 'asset-1', + contentMimeType: 'text/html', + contentBytes: 128, + storageKey: 'users/user-1/pages/page-1/versions/v1.html', + versionNo: 1, + now: 1000, + }); + + assert.equal(result.id, 'ca_page-1_version-1'); + assert.deepEqual(calls[0], [ + 'ensurePackage', + { + userId: 'user-1', + sessionId: 'session-1', + title: '旅行计划', + now: 1000, + }, + ]); + assert.equal(calls[1][1].artifactKind, 'page'); + assert.equal(calls[1][1].messageId, 'message-1'); + assert.equal(calls[1][1].storageKey, 'users/user-1/pages/page-1/versions/v1.html'); + assert.deepEqual(calls[2], [ + 'writeManifestForSession', + { userId: 'user-1', sessionId: 'session-1' }, + ]); +}); + +test('registerPageArtifactForConversation skips missing registry or session safely', async () => { + assert.equal( + await pageInternals.registerPageArtifactForConversation({ + registry: null, + userId: 'user-1', + source: { sessionId: 'session-1' }, + page: { id: 'page-1' }, + }), + null, + ); + assert.equal( + await pageInternals.registerPageArtifactForConversation({ + registry: { ensurePackage: async () => assert.fail('should not be called') }, + userId: 'user-1', + source: {}, + page: { id: 'page-1' }, + }), + null, + ); +}); diff --git a/server.mjs b/server.mjs index 794840c..2722fd8 100644 --- a/server.mjs +++ b/server.mjs @@ -42,6 +42,10 @@ import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs' import { ensureMindSpaceConfig } from './mindspace-config.mjs'; import { createAssetService } from './mindspace-assets.mjs'; import { createPageService, pageInternals, inlinePrivateAssetsInHtml, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs'; +import { createConversationPackageRegistry } from './mindspace-conversation-package-registry.mjs'; +import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs'; +import { createMindSpaceServiceFacade } from './mindspace-service.mjs'; +import { createLocalMindSpaceStorageAdapter } from './mindspace-storage-adapter.mjs'; import { createPageLiveEditService } from './mindspace-page-live-edit.mjs'; import { createAssetAgentService } from './mindspace-asset-agent.mjs'; import { createPageEditSessionService } from './mindspace-page-edit-session.mjs'; @@ -297,16 +301,24 @@ async function bootstrapUserAuth() { monthlyViewLimit: Number(process.env.MINDSPACE_FREE_MONTHLY_VIEW_LIMIT ?? 1000), scheduleService, }); + const mindSpaceStorageRoot = + process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'); + const conversationPackageRegistry = createConversationPackageRegistry({ + store: createConversationPackageStore(pool), + service: createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(mindSpaceStorageRoot), + publicBaseUrl: resolvePublicBaseUrl(), + }), + }); mindSpaceAssets = createAssetService(pool, { h5Root: __dirname, - storageRoot: - process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'), + storageRoot: mindSpaceStorageRoot, maxFileBytes: Number(process.env.MINDSPACE_MAX_FILE_BYTES ?? DEFAULT_MAX_FILE_BYTES), }); mindSpacePages = createPageService(pool, { h5Root: __dirname, - storageRoot: - process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'), + storageRoot: mindSpaceStorageRoot, + conversationPackageRegistry, }); const resolveUserIdForAgentSession = async (sessionId) => { const [rows] = await pool.query( @@ -325,8 +337,7 @@ async function bootstrapUserAuth() { }); mindSpacePublications = createPublicationService(pool, { h5Root: __dirname, - storageRoot: - process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'), + storageRoot: mindSpaceStorageRoot, publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5), }); setInterval(async () => {