diff --git a/mindspace-conversation-package-routes.mjs b/mindspace-conversation-package-routes.mjs new file mode 100644 index 0000000..e5ee056 --- /dev/null +++ b/mindspace-conversation-package-routes.mjs @@ -0,0 +1,46 @@ +function defaultSendData(res, _req, data) { + return res.json({ data }); +} + +function defaultRouteError(res, _req, error) { + return res.status(500).json({ message: error?.message || 'MindSpace 服务异常' }); +} + +function resolveRegistry(input) { + return typeof input === 'function' ? input() : input; +} + +export function createGetConversationPackageHandler({ + registry, + getRegistry, + userAuth, + getUserAuth, + ensureMindSpaceEnabled = () => true, + sendData = defaultSendData, + mindSpaceError = defaultRouteError, +} = {}) { + return async function getConversationPackage(req, res) { + const activeRegistry = resolveRegistry(getRegistry ?? registry); + if (!activeRegistry || !ensureMindSpaceEnabled(res, req)) { + if (res.headersSent) return undefined; + return res.status(503).json({ message: 'MindSpace 对话包未启用' }); + } + const sessionId = String(req.params?.sessionId ?? '').trim(); + if (!sessionId) return res.status(400).json({ message: '缺少 sessionId' }); + + try { + const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth); + if (!(await activeUserAuth.ownsSession(req.currentUser.id, sessionId))) { + return res.status(403).json({ message: '无权访问该会话' }); + } + const manifest = await activeRegistry.readManifestForSession({ + userId: req.currentUser.id, + sessionId, + }); + if (!manifest) return res.status(404).json({ message: '对话包不存在' }); + return sendData(res, req, manifest); + } catch (error) { + return mindSpaceError(res, req, error); + } + }; +} diff --git a/mindspace-conversation-package-routes.test.mjs b/mindspace-conversation-package-routes.test.mjs new file mode 100644 index 0000000..d669140 --- /dev/null +++ b/mindspace-conversation-package-routes.test.mjs @@ -0,0 +1,109 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createGetConversationPackageHandler } from './mindspace-conversation-package-routes.mjs'; + +function createResponseRecorder() { + return { + statusCode: 200, + headersSent: false, + body: null, + status(code) { + this.statusCode = code; + return this; + }, + json(payload) { + this.body = payload; + this.headersSent = true; + return this; + }, + }; +} + +test('GET conversation package returns manifest for owned sessions', async () => { + const handler = createGetConversationPackageHandler({ + registry: { + async readManifestForSession(input) { + assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' }); + return { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] }; + }, + }, + userAuth: { + async ownsSession(userId, sessionId) { + assert.equal(userId, 'user-1'); + assert.equal(sessionId, 'session-1'); + return true; + }, + }, + }); + const res = createResponseRecorder(); + + await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } }, res); + + assert.equal(res.statusCode, 200); + assert.deepEqual(res.body, { + data: { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] }, + }); +}); + +test('GET conversation package blocks sessions owned by another user', async () => { + const handler = createGetConversationPackageHandler({ + registry: { + async readManifestForSession() { + assert.fail('registry should not be called for unauthorized sessions'); + }, + }, + userAuth: { + async ownsSession() { + return false; + }, + }, + }); + const res = createResponseRecorder(); + + await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-2' } }, res); + + assert.equal(res.statusCode, 403); + assert.deepEqual(res.body, { message: '无权访问该会话' }); +}); + +test('GET conversation package returns 404 when package has not been recorded', async () => { + const handler = createGetConversationPackageHandler({ + registry: { + async readManifestForSession() { + return null; + }, + }, + userAuth: { + async ownsSession() { + return true; + }, + }, + }); + const res = createResponseRecorder(); + + await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-empty' } }, res); + + assert.equal(res.statusCode, 404); + assert.deepEqual(res.body, { message: '对话包不存在' }); +}); + +test('GET conversation package respects MindSpace feature gates', async () => { + const handler = createGetConversationPackageHandler({ + registry: {}, + userAuth: { + async ownsSession() { + assert.fail('user ownership should not be checked when feature gate rejects'); + }, + }, + ensureMindSpaceEnabled(res) { + res.status(403).json({ message: 'MindSpace 已关闭' }); + return false; + }, + }); + const res = createResponseRecorder(); + + await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } }, res); + + assert.equal(res.statusCode, 403); + assert.deepEqual(res.body, { message: 'MindSpace 已关闭' }); +}); diff --git a/package.json b/package.json index e204001..de8a02b 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-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", + "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-routes.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", diff --git a/server.mjs b/server.mjs index 6a1f2e9..d5d8445 100644 --- a/server.mjs +++ b/server.mjs @@ -43,6 +43,7 @@ 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 { createGetConversationPackageHandler } from './mindspace-conversation-package-routes.mjs'; import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs'; import { createMindSpaceServiceFacade } from './mindspace-service.mjs'; import { createLocalMindSpaceStorageAdapter } from './mindspace-storage-adapter.mjs'; @@ -2487,27 +2488,13 @@ api.post('/internal/agent/jobs/:jobId/complete', async (req, res) => { } }); -api.get('/mindspace/v1/conversation-packages/:sessionId', async (req, res) => { - if (!mindSpaceConversationPackageRegistry || !ensureMindSpaceEnabled(res, req)) { - if (res.headersSent) return; - return res.status(503).json({ message: 'MindSpace 对话包未启用' }); - } - const sessionId = String(req.params.sessionId ?? '').trim(); - if (!sessionId) return res.status(400).json({ message: '缺少 sessionId' }); - if (!(await userAuth.ownsSession(req.currentUser.id, sessionId))) { - return res.status(403).json({ message: '无权访问该会话' }); - } - try { - const manifest = await mindSpaceConversationPackageRegistry.readManifestForSession({ - userId: req.currentUser.id, - sessionId, - }); - if (!manifest) return res.status(404).json({ message: '对话包不存在' }); - return sendData(res, req, manifest); - } catch (error) { - return mindSpaceError(res, req, error); - } -}); +api.get('/mindspace/v1/conversation-packages/:sessionId', createGetConversationPackageHandler({ + getRegistry: () => mindSpaceConversationPackageRegistry, + getUserAuth: () => userAuth, + ensureMindSpaceEnabled, + sendData, + mindSpaceError, +})); api.post('/mindspace/v1/uploads', async (req, res) => { if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return;