08feae8bef
落地 H5 Session 架构 Patch 1–5(Broker 收口、Router decision、SSE taxonomy、goosed 边界检查), 并新增可选 MEMIND_RUN_STREAM_REPLAY run 事件回放与 H5 假交付 guard;修复 Finish 先于 agent-run gate 导致 UI 永久 loading 的竞态,接入 verify:h5-session-patches 回归脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
6.5 KiB
JavaScript
195 lines
6.5 KiB
JavaScript
import { resolveSessionAccess } from './session-broker.mjs';
|
|
|
|
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;
|
|
}
|
|
|
|
function resolveActiveSessionAccess({ sessionAccess, getSessionAccess, userAuth, getUserAuth }) {
|
|
const explicit = resolveRegistry(getSessionAccess ?? sessionAccess);
|
|
if (explicit) return explicit;
|
|
const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth);
|
|
if (!activeUserAuth) return null;
|
|
return resolveSessionAccess({ userAuth: activeUserAuth, enabled: false });
|
|
}
|
|
|
|
async function assertConversationPackageSessionOwned(req, res, options) {
|
|
const sessionId = String(req.params?.sessionId ?? '').trim();
|
|
if (!sessionId) {
|
|
res.status(400).json({ message: '缺少 sessionId' });
|
|
return null;
|
|
}
|
|
const access = resolveActiveSessionAccess(options);
|
|
if (!access) {
|
|
res.status(503).json({ message: 'MindSpace 对话包未启用' });
|
|
return null;
|
|
}
|
|
if (!(await access.validateOwnership(req.currentUser.id, sessionId))) {
|
|
res.status(403).json({ message: '无权访问该会话' });
|
|
return null;
|
|
}
|
|
return sessionId;
|
|
}
|
|
|
|
function encodeDownloadFilename(filename) {
|
|
const safe = String(filename ?? '').trim() || 'mindspace-artifact';
|
|
return `attachment; filename="${encodeURIComponent(safe)}"; filename*=UTF-8''${encodeURIComponent(safe)}`;
|
|
}
|
|
|
|
function sanitizeDownloadFilenameBase(value, fallback = 'mindspace-conversation-package') {
|
|
const normalized = String(value ?? '')
|
|
.replace(/[\\/:*?"<>|]+/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
return normalized || fallback;
|
|
}
|
|
|
|
export function createGetConversationPackageHandler({
|
|
registry,
|
|
getRegistry,
|
|
userAuth,
|
|
getUserAuth,
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
ensureMindSpaceEnabled = () => true,
|
|
sendData = defaultSendData,
|
|
mindSpaceError = defaultRouteError,
|
|
beforeReadManifest,
|
|
} = {}) {
|
|
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 = await assertConversationPackageSessionOwned(req, res, {
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
userAuth,
|
|
getUserAuth,
|
|
});
|
|
if (!sessionId) return undefined;
|
|
|
|
try {
|
|
if (typeof beforeReadManifest === 'function') {
|
|
await beforeReadManifest({ req, userId: req.currentUser.id, sessionId });
|
|
}
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function createDownloadConversationPackageManifestHandler({
|
|
registry,
|
|
getRegistry,
|
|
userAuth,
|
|
getUserAuth,
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
ensureMindSpaceEnabled = () => true,
|
|
mindSpaceError = defaultRouteError,
|
|
beforeReadManifest,
|
|
} = {}) {
|
|
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 = await assertConversationPackageSessionOwned(req, res, {
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
userAuth,
|
|
getUserAuth,
|
|
});
|
|
if (!sessionId) return undefined;
|
|
|
|
try {
|
|
if (typeof beforeReadManifest === 'function') {
|
|
await beforeReadManifest({ req, userId: req.currentUser.id, sessionId });
|
|
}
|
|
const manifest = await activeRegistry.readManifestForSession({
|
|
userId: req.currentUser.id,
|
|
sessionId,
|
|
});
|
|
if (!manifest) return res.status(404).json({ message: '对话包不存在' });
|
|
const filenameBase = sanitizeDownloadFilenameBase(
|
|
manifest.displayFolderName ?? manifest.title,
|
|
'mindspace-conversation-package',
|
|
);
|
|
const filename = `${filenameBase}-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,
|
|
userAuth,
|
|
getUserAuth,
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
ensureMindSpaceEnabled = () => true,
|
|
mindSpaceError = defaultRouteError,
|
|
} = {}) {
|
|
return async function downloadConversationPackageArtifact(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 = await assertConversationPackageSessionOwned(req, res, {
|
|
sessionAccess,
|
|
getSessionAccess,
|
|
userAuth,
|
|
getUserAuth,
|
|
});
|
|
if (!sessionId) return undefined;
|
|
const artifactId = String(req.params?.artifactId ?? '').trim();
|
|
if (!artifactId) return res.status(400).json({ message: '缺少 artifactId' });
|
|
|
|
try {
|
|
const result = await activeRegistry.readArtifactObject({
|
|
userId: req.currentUser.id,
|
|
sessionId,
|
|
artifactId,
|
|
});
|
|
if (!result) return res.status(404).json({ message: '文件不存在' });
|
|
const filename = result.artifact.displayName || result.artifact.id;
|
|
res.set('Content-Type', result.artifact.mimeType || 'application/octet-stream');
|
|
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(result.body);
|
|
} catch (error) {
|
|
return mindSpaceError(res, req, error);
|
|
}
|
|
};
|
|
}
|
|
|
|
export const conversationPackageRouteInternals = {
|
|
encodeDownloadFilename,
|
|
sanitizeDownloadFilenameBase,
|
|
};
|