feat(h5-session): Session Broker、run SSE replay 与 Finish 竞态修复
落地 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>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { resolveSessionAccess } from './session-broker.mjs';
|
||||
|
||||
function defaultSendData(res, _req, data) {
|
||||
return res.json({ data });
|
||||
}
|
||||
@@ -10,6 +12,32 @@ 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)}`;
|
||||
@@ -28,6 +56,8 @@ export function createGetConversationPackageHandler({
|
||||
getRegistry,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
ensureMindSpaceEnabled = () => true,
|
||||
sendData = defaultSendData,
|
||||
mindSpaceError = defaultRouteError,
|
||||
@@ -39,14 +69,15 @@ export function createGetConversationPackageHandler({
|
||||
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' });
|
||||
const sessionId = await assertConversationPackageSessionOwned(req, res, {
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
});
|
||||
if (!sessionId) return undefined;
|
||||
|
||||
try {
|
||||
const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth);
|
||||
if (!(await activeUserAuth.ownsSession(req.currentUser.id, sessionId))) {
|
||||
return res.status(403).json({ message: '无权访问该会话' });
|
||||
}
|
||||
if (typeof beforeReadManifest === 'function') {
|
||||
await beforeReadManifest({ req, userId: req.currentUser.id, sessionId });
|
||||
}
|
||||
@@ -67,6 +98,8 @@ export function createDownloadConversationPackageManifestHandler({
|
||||
getRegistry,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
ensureMindSpaceEnabled = () => true,
|
||||
mindSpaceError = defaultRouteError,
|
||||
beforeReadManifest,
|
||||
@@ -77,14 +110,15 @@ export function createDownloadConversationPackageManifestHandler({
|
||||
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' });
|
||||
const sessionId = await assertConversationPackageSessionOwned(req, res, {
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
});
|
||||
if (!sessionId) return undefined;
|
||||
|
||||
try {
|
||||
const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth);
|
||||
if (!(await activeUserAuth.ownsSession(req.currentUser.id, sessionId))) {
|
||||
return res.status(403).json({ message: '无权访问该会话' });
|
||||
}
|
||||
if (typeof beforeReadManifest === 'function') {
|
||||
await beforeReadManifest({ req, userId: req.currentUser.id, sessionId });
|
||||
}
|
||||
@@ -114,6 +148,8 @@ export function createDownloadConversationPackageArtifactHandler({
|
||||
getRegistry,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
ensureMindSpaceEnabled = () => true,
|
||||
mindSpaceError = defaultRouteError,
|
||||
} = {}) {
|
||||
@@ -123,16 +159,17 @@ export function createDownloadConversationPackageArtifactHandler({
|
||||
if (res.headersSent) return undefined;
|
||||
return res.status(503).json({ message: 'MindSpace 对话包未启用' });
|
||||
}
|
||||
const sessionId = String(req.params?.sessionId ?? '').trim();
|
||||
const sessionId = await assertConversationPackageSessionOwned(req, res, {
|
||||
sessionAccess,
|
||||
getSessionAccess,
|
||||
userAuth,
|
||||
getUserAuth,
|
||||
});
|
||||
if (!sessionId) return undefined;
|
||||
const artifactId = String(req.params?.artifactId ?? '').trim();
|
||||
if (!sessionId) return res.status(400).json({ message: '缺少 sessionId' });
|
||||
if (!artifactId) return res.status(400).json({ message: '缺少 artifactId' });
|
||||
|
||||
try {
|
||||
const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth);
|
||||
if (!(await activeUserAuth.ownsSession(req.currentUser.id, sessionId))) {
|
||||
return res.status(403).json({ message: '无权访问该会话' });
|
||||
}
|
||||
const result = await activeRegistry.readArtifactObject({
|
||||
userId: req.currentUser.id,
|
||||
sessionId,
|
||||
|
||||
Reference in New Issue
Block a user