From 253ee870e7b90141a7355bcfe3f21c75ac29b92e Mon Sep 17 00:00:00 2001 From: john Date: Thu, 2 Jul 2026 21:06:03 +0800 Subject: [PATCH] Add MindSpace service facade skeleton --- docs/mindspace-service-contract.md | 1 + mindspace-service.mjs | 87 ++++++++++++++++++++++++++++ mindspace-service.test.mjs | 91 ++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 mindspace-service.mjs create mode 100644 mindspace-service.test.mjs diff --git a/docs/mindspace-service-contract.md b/docs/mindspace-service-contract.md index 7321125..7620277 100644 --- a/docs/mindspace-service-contract.md +++ b/docs/mindspace-service-contract.md @@ -94,6 +94,7 @@ validatePublicBackingObject({ publicationId }) - 新代码不得直接拼 `/MindSpace//...`。 - 当前分支新增的 `mindspace-canonical-url.mjs` 是 P1 facade 起点;后续旧 helper 迁移到它后,再接入 backing object 校验。 +- 当前分支新增的 `mindspace-service.mjs` 是组合门面起点;它先组合 package manifest、storage adapter、canonical URL,不接入现有运行链路。 - 旧 URL helper 暂时保留为 compatibility layer。 - public URL 必须能反查到 publication 或 asset。 - URL 返回前应校验 backing object 存在,或明确返回 pending 状态。 diff --git a/mindspace-service.mjs b/mindspace-service.mjs new file mode 100644 index 0000000..20f8982 --- /dev/null +++ b/mindspace-service.mjs @@ -0,0 +1,87 @@ +import { + buildConversationPackageManifest, + buildConversationStoragePrefix, + createConversationArtifact, + createConversationPackageRecord, +} from './mindspace-conversation-package.mjs'; +import { + createMindSpacePublicPageUrl, + createMindSpacePublicUrl, + normalizeMindSpacePublicRelativePath, +} from './mindspace-canonical-url.mjs'; +import { normalizeMindSpaceStorageKey } from './mindspace-storage-adapter.mjs'; + +function serviceError(message, code, details = {}) { + return Object.assign(new Error(message), { code, ...details }); +} + +function requireStorageAdapter(storageAdapter) { + if (!storageAdapter || typeof storageAdapter.putObject !== 'function') { + throw serviceError('MindSpace storage adapter is required', 'missing_mindspace_storage_adapter'); + } + return storageAdapter; +} + +function packageStorageKey(packageRecord, relativePath) { + const record = createConversationPackageRecord(packageRecord); + const relative = normalizeMindSpaceStorageKey(relativePath); + return `${record.storagePrefix}/${relative}`; +} + +export function createMindSpaceServiceFacade({ storageAdapter, publicBaseUrl } = {}) { + const storage = requireStorageAdapter(storageAdapter); + + return { + createConversationPackage(input) { + return createConversationPackageRecord(input); + }, + + createConversationArtifact(input) { + return createConversationArtifact(input); + }, + + buildConversationManifest(input) { + return buildConversationPackageManifest(input); + }, + + buildConversationStoragePrefix(input) { + return buildConversationStoragePrefix(input); + }, + + createPublicPageUrl({ ownerKey, filename }) { + return createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }); + }, + + createPublicObjectUrl({ ownerKey, relativePath }) { + return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }); + }, + + resolvePackageStorageKey({ packageRecord, relativePath }) { + return packageStorageKey(packageRecord, relativePath); + }, + + async putPackageObject({ packageRecord, relativePath, body }) { + return storage.putObject(packageStorageKey(packageRecord, relativePath), body); + }, + + async getPackageObject({ packageRecord, relativePath }) { + return storage.getObject(packageStorageKey(packageRecord, relativePath)); + }, + + async statPackageObject({ packageRecord, relativePath }) { + return storage.statObject(packageStorageKey(packageRecord, relativePath)); + }, + + async listPackageObjects({ packageRecord, prefix = '' }) { + const record = createConversationPackageRecord(packageRecord); + const cleanPrefix = prefix ? normalizeMindSpacePublicRelativePath(prefix) : ''; + const storagePrefix = cleanPrefix ? `${record.storagePrefix}/${cleanPrefix}` : record.storagePrefix; + return storage.listObjects(storagePrefix); + }, + }; +} + +export const mindspaceServiceInternals = { + serviceError, + packageStorageKey, +}; diff --git a/mindspace-service.test.mjs b/mindspace-service.test.mjs new file mode 100644 index 0000000..cfd8510 --- /dev/null +++ b/mindspace-service.test.mjs @@ -0,0 +1,91 @@ +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 { createMindSpaceServiceFacade } from './mindspace-service.mjs'; +import { createLocalMindSpaceStorageAdapter } from './mindspace-storage-adapter.mjs'; + +test('createMindSpaceServiceFacade requires a storage adapter', () => { + assert.throws( + () => createMindSpaceServiceFacade({ publicBaseUrl: 'https://m.tkmind.cn' }), + (error) => error.code === 'missing_mindspace_storage_adapter', + ); +}); + +test('MindSpace service facade coordinates package storage and public URLs without raw paths', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-service-')); + const service = createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(root), + publicBaseUrl: 'https://m.tkmind.cn', + }); + + const packageRecord = service.createConversationPackage({ + id: 'cp_session-1', + userId: 'user-1', + sessionId: 'session-1', + title: '项目周报', + now: 1000, + }); + + assert.equal(packageRecord.storagePrefix, 'users/user-1/conversations/session-1'); + assert.equal( + service.resolvePackageStorageKey({ packageRecord, relativePath: 'public/report.html' }), + 'users/user-1/conversations/session-1/public/report.html', + ); + + const writeResult = await service.putPackageObject({ + packageRecord, + relativePath: 'public/report.html', + body: '

Report

', + }); + assert.equal(writeResult.key, 'users/user-1/conversations/session-1/public/report.html'); + + const body = await service.getPackageObject({ packageRecord, relativePath: 'public/report.html' }); + assert.equal(body.toString('utf8'), '

Report

'); + + assert.deepEqual( + (await service.listPackageObjects({ packageRecord })).map((item) => item.key), + ['users/user-1/conversations/session-1/public/report.html'], + ); + + assert.equal( + service.createPublicPageUrl({ ownerKey: 'user-1', filename: 'report.html' }), + 'https://m.tkmind.cn/MindSpace/user-1/public/report.html', + ); +}); + +test('MindSpace service facade builds manifests from package artifacts', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-service-')); + const service = createMindSpaceServiceFacade({ + storageAdapter: createLocalMindSpaceStorageAdapter(root), + publicBaseUrl: 'https://m.tkmind.cn', + }); + const packageRecord = service.createConversationPackage({ + id: 'cp_session-1', + userId: 'user-1', + sessionId: 'session-1', + now: 1000, + }); + const artifact = service.createConversationArtifact({ + id: 'ca_1', + packageId: packageRecord.id, + artifactKind: 'public_html', + displayName: 'report.html', + storageKey: 'users/user-1/conversations/session-1/public/report.html', + canonicalUrl: service.createPublicPageUrl({ ownerKey: 'user-1', filename: 'report.html' }), + now: 2000, + }); + + const manifest = service.buildConversationManifest({ + packageRecord, + artifacts: [artifact], + }); + + assert.equal(manifest.packageId, 'cp_session-1'); + assert.equal(manifest.artifacts[0].kind, 'public_html'); + assert.equal( + manifest.artifacts[0].canonicalUrl, + 'https://m.tkmind.cn/MindSpace/user-1/public/report.html', + ); +}); diff --git a/package.json b/package.json index 4fdecc4..c550db1 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-schema.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-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",