Add MindSpace service facade skeleton
This commit is contained in:
@@ -94,6 +94,7 @@ validatePublicBackingObject({ publicationId })
|
|||||||
|
|
||||||
- 新代码不得直接拼 `/MindSpace/<userId>/...`。
|
- 新代码不得直接拼 `/MindSpace/<userId>/...`。
|
||||||
- 当前分支新增的 `mindspace-canonical-url.mjs` 是 P1 facade 起点;后续旧 helper 迁移到它后,再接入 backing object 校验。
|
- 当前分支新增的 `mindspace-canonical-url.mjs` 是 P1 facade 起点;后续旧 helper 迁移到它后,再接入 backing object 校验。
|
||||||
|
- 当前分支新增的 `mindspace-service.mjs` 是组合门面起点;它先组合 package manifest、storage adapter、canonical URL,不接入现有运行链路。
|
||||||
- 旧 URL helper 暂时保留为 compatibility layer。
|
- 旧 URL helper 暂时保留为 compatibility layer。
|
||||||
- public URL 必须能反查到 publication 或 asset。
|
- public URL 必须能反查到 publication 或 asset。
|
||||||
- URL 返回前应校验 backing object 存在,或明确返回 pending 状态。
|
- URL 返回前应校验 backing object 存在,或明确返回 pending 状态。
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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: '<h1>Report</h1>',
|
||||||
|
});
|
||||||
|
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'), '<h1>Report</h1>');
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
+1
-1
@@ -38,7 +38,7 @@
|
|||||||
"build:portal-runtime": "node scripts/build-portal-runtime.mjs",
|
"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": "node scripts/check-mindspace-public-links.mjs --downloads-only",
|
||||||
"check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links",
|
"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:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
||||||
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.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",
|
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user