Add conversation package manifest download

This commit is contained in:
john
2026-07-02 21:58:14 +08:00
parent b4ae4a870c
commit 49127d8675
5 changed files with 127 additions and 1 deletions
+39
View File
@@ -50,6 +50,45 @@ export function createGetConversationPackageHandler({
};
}
export function createDownloadConversationPackageManifestHandler({
registry,
getRegistry,
userAuth,
getUserAuth,
ensureMindSpaceEnabled = () => true,
mindSpaceError = defaultRouteError,
} = {}) {
return async function downloadConversationPackageManifest(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: '对话包不存在' });
const filename = `${manifest.packageId || sessionId}-manifest.json`;
res.set('Content-Type', 'application/json; charset=utf-8');
res.set('Content-Disposition', encodeDownloadFilename(filename));
res.set('Cache-Control', 'private, no-store');
if (req.requestId) res.setHeader('X-Request-Id', req.requestId);
return res.send(`${JSON.stringify(manifest, null, 2)}\n`);
} catch (error) {
return mindSpaceError(res, req, error);
}
};
}
export function createDownloadConversationPackageArtifactHandler({
registry,
getRegistry,
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {
createDownloadConversationPackageArtifactHandler,
createDownloadConversationPackageManifestHandler,
createGetConversationPackageHandler,
} from './mindspace-conversation-package-routes.mjs';
@@ -125,6 +126,62 @@ test('GET conversation package respects MindSpace feature gates', async () => {
assert.deepEqual(res.body, { message: 'MindSpace 已关闭' });
});
test('GET conversation package manifest downloads owned package metadata', async () => {
const handler = createDownloadConversationPackageManifestHandler({
registry: {
async readManifestForSession(input) {
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' });
return { packageId: 'cp_session-1', sessionId: 'session-1', artifacts: [] };
},
},
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' }, requestId: 'req-1' },
res,
);
assert.equal(res.statusCode, 200);
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
assert.match(res.headers['content-disposition'], /cp_session-1-manifest\.json/);
assert.equal(res.headers['cache-control'], 'private, no-store');
assert.equal(res.headers['x-request-id'], 'req-1');
assert.deepEqual(JSON.parse(res.body), {
packageId: 'cp_session-1',
sessionId: 'session-1',
artifacts: [],
});
});
test('GET conversation package manifest blocks unauthorized sessions', async () => {
const handler = createDownloadConversationPackageManifestHandler({
registry: {
async readManifestForSession() {
assert.fail('registry should not be called for unauthorized manifest downloads');
},
},
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 artifact downloads owned private objects', async () => {
const handler = createDownloadConversationPackageArtifactHandler({
registry: {
+11
View File
@@ -45,6 +45,7 @@ import { createPageService, pageInternals, inlinePrivateAssetsInHtml, normalizeW
import { createConversationPackageRegistry } from './mindspace-conversation-package-registry.mjs';
import {
createDownloadConversationPackageArtifactHandler,
createDownloadConversationPackageManifestHandler,
createGetConversationPackageHandler,
} from './mindspace-conversation-package-routes.mjs';
import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs';
@@ -2499,6 +2500,16 @@ api.get('/mindspace/v1/conversation-packages/:sessionId', createGetConversationP
mindSpaceError,
}));
api.get(
'/mindspace/v1/conversation-packages/:sessionId/manifest.json',
createDownloadConversationPackageManifestHandler({
getRegistry: () => mindSpaceConversationPackageRegistry,
getUserAuth: () => userAuth,
ensureMindSpaceEnabled,
mindSpaceError,
}),
);
api.get(
'/mindspace/v1/conversation-packages/:sessionId/artifacts/:artifactId/download',
createDownloadConversationPackageArtifactHandler({
+4
View File
@@ -649,6 +649,10 @@ export async function getMindSpaceConversationPackage(
return result.data;
}
export function buildMindSpaceConversationPackageManifestDownloadUrl(sessionId: string): string {
return `${API}/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}/manifest.json`;
}
export async function ignoreMindSpaceScheduleReminder(reminderId: string) {
const result = await apiFetch<{ data: MindSpaceScheduleReminder }>(
`/mindspace/v1/schedule/reminders/${encodeURIComponent(reminderId)}/ignore`,
+16 -1
View File
@@ -4,7 +4,12 @@ import { openAvatarPicker } from '../utils/userAvatar';
import { CHAT_SKILL_OPTIONS, filterChatSkills } from '../utils/chatSkills';
import { getMessageSaveActions } from '../utils/messageSave';
import { getDisplayText } from '../utils/message';
import { ApiError, downloadChatMessageDocx, getMindSpaceConversationPackage } from '../api/client';
import {
ApiError,
buildMindSpaceConversationPackageManifestDownloadUrl,
downloadChatMessageDocx,
getMindSpaceConversationPackage,
} from '../api/client';
import {
CHAT_IMAGE_UPLOAD_MAX_COUNT,
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
@@ -822,6 +827,16 @@ export function ChatPanel({
<button type="button" className="ghost-btn" onClick={() => void loadConversationPackage()}>
</button>
{session?.id && conversationPackage && (
<a
className="ghost-btn conversation-package-download"
href={buildMindSpaceConversationPackageManifestDownloadUrl(session.id)}
target="_blank"
rel="noreferrer"
>
</a>
)}
</div>
</section>
</div>