From 97b648322ae8ad9292eb79f445941eb6be5e9c66 Mon Sep 17 00:00:00 2001 From: john Date: Thu, 2 Jul 2026 21:17:40 +0800 Subject: [PATCH] Add conversation package registry --- mindspace-conversation-package-registry.mjs | 39 +++++ ...ace-conversation-package-registry.test.mjs | 164 ++++++++++++++++++ mindspace-conversation-package.mjs | 9 +- mindspace-conversation-package.test.mjs | 11 ++ package.json | 2 +- 5 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 mindspace-conversation-package-registry.mjs create mode 100644 mindspace-conversation-package-registry.test.mjs diff --git a/mindspace-conversation-package-registry.mjs b/mindspace-conversation-package-registry.mjs new file mode 100644 index 0000000..e6f02dd --- /dev/null +++ b/mindspace-conversation-package-registry.mjs @@ -0,0 +1,39 @@ +function registryError(message, code) { + return Object.assign(new Error(message), { code }); +} + +function requireMethod(target, methodName, code) { + if (!target || typeof target[methodName] !== 'function') { + throw registryError(`MindSpace conversation package registry requires ${methodName}`, code); + } +} + +export function createConversationPackageRegistry({ store, service } = {}) { + requireMethod(store, 'upsertPackage', 'missing_conversation_package_store'); + requireMethod(store, 'upsertArtifact', 'missing_conversation_package_store'); + requireMethod(store, 'getPackageBySession', 'missing_conversation_package_store'); + requireMethod(store, 'listArtifacts', 'missing_conversation_package_store'); + requireMethod(service, 'putConversationManifest', 'missing_mindspace_service'); + + return { + async ensurePackage(input) { + return store.upsertPackage(input); + }, + + async recordArtifact(input) { + return store.upsertArtifact(input); + }, + + async writeManifestForSession({ userId, sessionId }) { + const packageRecord = await store.getPackageBySession(userId, sessionId); + if (!packageRecord) return null; + const artifacts = await store.listArtifacts(packageRecord.id); + return service.putConversationManifest({ packageRecord, artifacts }); + }, + }; +} + +export const conversationPackageRegistryInternals = { + registryError, + requireMethod, +}; diff --git a/mindspace-conversation-package-registry.test.mjs b/mindspace-conversation-package-registry.test.mjs new file mode 100644 index 0000000..e18fa76 --- /dev/null +++ b/mindspace-conversation-package-registry.test.mjs @@ -0,0 +1,164 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +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'; + +function createFakePool() { + const packages = new Map(); + const artifacts = new Map(); + return { + async query(sql, params = []) { + if (/INSERT INTO h5_conversation_packages/i.test(sql)) { + const [ + id, + user_id, + session_id, + title, + status, + storage_prefix, + manifest_asset_id, + created_at, + updated_at, + ] = params; + packages.set(`${user_id}:${session_id}`, { + id, + user_id, + session_id, + title, + status, + storage_prefix, + manifest_asset_id, + created_at, + updated_at, + }); + return [{ affectedRows: 1 }]; + } + if (/FROM h5_conversation_packages/i.test(sql)) { + return [[packages.get(`${params[0]}:${params[1]}`)].filter(Boolean)]; + } + if (/INSERT INTO h5_conversation_artifacts/i.test(sql)) { + const [ + id, + package_id, + artifact_kind, + role, + asset_id, + page_id, + publication_id, + agent_run_id, + message_id, + display_name, + mime_type, + size_bytes, + storage_key, + canonical_url, + sort_order, + created_at, + ] = params; + artifacts.set(id, { + id, + package_id, + artifact_kind, + role, + asset_id, + page_id, + publication_id, + agent_run_id, + message_id, + display_name, + mime_type, + size_bytes, + storage_key, + canonical_url, + sort_order, + created_at, + }); + return [{ affectedRows: 1 }]; + } + if (/FROM h5_conversation_artifacts/i.test(sql)) { + return [ + [...artifacts.values()] + .filter((row) => row.package_id === params[0]) + .sort( + (a, b) => + Number(a.sort_order) - Number(b.sort_order) || + Number(a.created_at) - Number(b.created_at) || + String(a.id).localeCompare(String(b.id)), + ), + ]; + } + throw new Error(`unexpected query: ${sql}`); + }, + }; +} + +test('createConversationPackageRegistry validates required collaborators', () => { + assert.throws( + () => createConversationPackageRegistry({ store: {}, service: {} }), + (error) => error.code === 'missing_conversation_package_store', + ); +}); + +test('conversation package registry records artifacts and writes manifest through storage facade', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-registry-')); + const service = createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(root), + publicBaseUrl: 'https://m.tkmind.cn', + }); + const store = createConversationPackageStore(createFakePool()); + const registry = createConversationPackageRegistry({ store, service }); + + const packageRecord = await registry.ensurePackage({ + userId: 'user-1', + sessionId: 'session-1', + title: '研究简报', + now: 1000, + }); + await registry.recordArtifact({ + id: 'ca_page', + packageId: packageRecord.id, + artifactKind: 'public_html', + displayName: 'brief.html', + canonicalUrl: 'https://m.tkmind.cn/MindSpace/user-1/public/brief.html', + sortOrder: 1, + now: 2000, + }); + + const result = await registry.writeManifestForSession({ + userId: 'user-1', + sessionId: 'session-1', + }); + + assert.equal(result.storageKey, 'users/user-1/conversations/session-1/manifest.json'); + const manifestJson = await service.getPackageObject({ packageRecord, relativePath: 'manifest.json' }); + const manifest = JSON.parse(manifestJson.toString('utf8')); + assert.equal(manifest.title, '研究简报'); + assert.deepEqual(manifest.artifacts, [ + { + artifactId: 'ca_page', + kind: 'public_html', + displayName: 'brief.html', + canonicalUrl: 'https://m.tkmind.cn/MindSpace/user-1/public/brief.html', + sortOrder: 1, + createdAt: 2000, + }, + ]); +}); + +test('conversation package registry skips manifest writes for unknown sessions', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-registry-')); + const registry = createConversationPackageRegistry({ + store: createConversationPackageStore(createFakePool()), + service: createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(root), + publicBaseUrl: 'https://m.tkmind.cn', + }), + }); + + assert.equal(await registry.writeManifestForSession({ userId: 'user-1', sessionId: 'missing' }), null); +}); diff --git a/mindspace-conversation-package.mjs b/mindspace-conversation-package.mjs index ecdf09f..2f16bff 100644 --- a/mindspace-conversation-package.mjs +++ b/mindspace-conversation-package.mjs @@ -32,6 +32,12 @@ function optionalString(value) { return normalized || null; } +function optionalNumber(value) { + if (value == null || value === '') return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; +} + function safeStorageSegment(value, field) { const normalized = requiredString(value, field); const encoded = encodeURIComponent(normalized); @@ -108,7 +114,7 @@ export function createConversationArtifact(input) { messageId: optionalString(input?.messageId), displayName: optionalString(input?.displayName), mimeType: optionalString(input?.mimeType), - sizeBytes: Number.isFinite(Number(input?.sizeBytes)) ? Number(input.sizeBytes) : null, + sizeBytes: optionalNumber(input?.sizeBytes), storageKey: optionalString(input?.storageKey), canonicalUrl: optionalString(input?.canonicalUrl), sortOrder: Number.isFinite(Number(input?.sortOrder)) ? Number(input.sortOrder) : 0, @@ -160,6 +166,7 @@ export function buildConversationPackageManifest({ packageRecord, artifacts = [] export const mindspaceConversationPackageInternals = { requiredString, + optionalNumber, optionalString, safeStorageSegment, }; diff --git a/mindspace-conversation-package.test.mjs b/mindspace-conversation-package.test.mjs index 65c1bed..d98ee2c 100644 --- a/mindspace-conversation-package.test.mjs +++ b/mindspace-conversation-package.test.mjs @@ -87,6 +87,17 @@ test('createConversationArtifact validates artifact kind and preserves provenanc assert.equal(artifact.messageId, 'msg-1'); }); +test('createConversationArtifact preserves unknown size as null', () => { + const artifact = createConversationArtifact({ + packageId: 'cp_1', + artifactKind: 'generated_file', + sizeBytes: null, + now: 1000, + }); + + assert.equal(artifact.sizeBytes, null); +}); + test('buildConversationPackageManifest sorts artifacts and emits stable public shape', () => { const manifest = buildConversationPackageManifest({ packageRecord: { diff --git a/package.json b/package.json index e9a0b20..e204001 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "build:portal-runtime": "node scripts/build-portal-runtime.mjs", "check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only", "check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links", - "test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs", + "test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs", "verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs", "verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs", "verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",