Add conversation package manifest download
This commit is contained in:
@@ -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
@@ -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({
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user