diff --git a/mindspace-publications.mjs b/mindspace-publications.mjs index 2844732..fe9fa01 100644 --- a/mindspace-publications.mjs +++ b/mindspace-publications.mjs @@ -296,6 +296,7 @@ export function createPublicationService(pool, options = {}) { const h5Root = options.h5Root ?? null; const idFactory = options.idFactory ?? (() => crypto.randomUUID()); const publicPageLimitFallback = Number(options.publicPageLimit ?? 5); + const conversationPackageRegistry = options.conversationPackageRegistry ?? null; let imgproxySigner = null; if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) { @@ -362,6 +363,7 @@ export function createPublicationService(pool, options = {}) { const loadVersion = async (userId, pageId, pageVersionId) => { const [rows] = await pool.query( `SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id, + p.source_session_id, p.source_message_id, p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no, pv.bundle_asset_id, av.storage_key, JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path @@ -729,7 +731,7 @@ export function createPublicationService(pool, options = {}) { } await conn.commit(); - return { + const publication = { ...publicationResponse({ id: publishId, page_id: pageId, @@ -745,6 +747,17 @@ export function createPublicationService(pool, options = {}) { }), findings: result.findings, }; + await registerPublicationArtifactForConversation({ + registry: conversationPackageRegistry, + userId, + page, + publication, + bundleAssetId, + storageKey, + htmlBytes, + now, + }); + return publication; } catch (error) { await conn.rollback(); if (writtenPath) await fs.rm(writtenPath, { force: true }).catch(() => {}); @@ -1046,6 +1059,53 @@ export function createPublicationService(pool, options = {}) { }; } +async function registerPublicationArtifactForConversation({ + registry, + userId, + page, + publication, + bundleAssetId, + storageKey, + htmlBytes, + now, +}) { + const sessionId = String(page?.source_session_id ?? '').trim(); + if (!registry || !sessionId || !publication?.id) return null; + try { + const packageRecord = await registry.ensurePackage({ + userId, + sessionId, + title: page.title, + now, + }); + const artifact = await registry.recordArtifact({ + id: `ca_${publication.id}`, + packageId: packageRecord.id, + artifactKind: 'public_html', + role: 'assistant', + assetId: bundleAssetId, + pageId: page.page_id, + publicationId: publication.id, + messageId: page.source_message_id ?? null, + displayName: `${page.title}.html`, + mimeType: 'text/html', + sizeBytes: htmlBytes, + storageKey, + canonicalUrl: publication.publicUrl, + sortOrder: Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0, + now, + }); + await registry.writeManifestForSession({ userId, sessionId }); + return artifact; + } catch (error) { + console.warn( + '[MindSpace] conversation package publication artifact registration failed:', + error instanceof Error ? error.message : error, + ); + return null; + } +} + export const publicationInternals = { normalizeSlug, normalizeAccessMode, @@ -1062,5 +1122,6 @@ export const publicationInternals = { rewriteWorkspacePublicAssetReferences, localizePrivateImageReferences, prepareHtmlPublishContent, + registerPublicationArtifactForConversation, scanContent, // re-exported from mindspace-content-scan.mjs }; diff --git a/mindspace-publications.test.mjs b/mindspace-publications.test.mjs index ec0e3b6..222184d 100644 --- a/mindspace-publications.test.mjs +++ b/mindspace-publications.test.mjs @@ -219,6 +219,74 @@ test('prepareHtmlPublishContent rewrites imgproxy local image urls to public sta assert.doesNotMatch(prepared, /plain\/local:\/\//); }); +test('registerPublicationArtifactForConversation records public html artifact', 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 publicationInternals.registerPublicationArtifactForConversation({ + registry, + userId: 'user-1', + page: { + page_id: 'page-1', + title: '发布页', + source_session_id: 'session-1', + source_message_id: 'message-1', + }, + publication: { + id: 'pub-1', + publicUrl: 'https://m.tkmind.cn/u/john/pages/published', + publishedAt: 3000, + }, + bundleAssetId: 'asset-pub', + storageKey: 'users/user-1/publications/pub-1/index.html', + htmlBytes: 256, + now: 3000, + }); + + assert.equal(result.id, 'ca_pub-1'); + assert.deepEqual(calls[0], [ + 'ensurePackage', + { + userId: 'user-1', + sessionId: 'session-1', + title: '发布页', + now: 3000, + }, + ]); + assert.equal(calls[1][1].artifactKind, 'public_html'); + assert.equal(calls[1][1].publicationId, 'pub-1'); + assert.equal(calls[1][1].canonicalUrl, 'https://m.tkmind.cn/u/john/pages/published'); + assert.deepEqual(calls[2], [ + 'writeManifestForSession', + { userId: 'user-1', sessionId: 'session-1' }, + ]); +}); + +test('registerPublicationArtifactForConversation skips pages without source session', async () => { + assert.equal( + await publicationInternals.registerPublicationArtifactForConversation({ + registry: { ensurePackage: async () => assert.fail('should not be called') }, + userId: 'user-1', + page: { page_id: 'page-1', title: '发布页' }, + publication: { id: 'pub-1' }, + }), + null, + ); +}); + test('prepareHtmlPublishContent rewrites imgproxy urls in srcset and css url contexts', async () => { const prepared = await publicationInternals.prepareHtmlPublishContent({ pool: { diff --git a/server.mjs b/server.mjs index 2722fd8..ef5a9ff 100644 --- a/server.mjs +++ b/server.mjs @@ -339,6 +339,7 @@ async function bootstrapUserAuth() { h5Root: __dirname, storageRoot: mindSpaceStorageRoot, publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5), + conversationPackageRegistry, }); setInterval(async () => { try {