Files
memind/session-stream-store.mjs
john 14a00774d9 feat(h5): web 联网能力、实时查询路由与 session Finish 对齐
- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:06:26 +08:00

125 lines
3.4 KiB
JavaScript

import crypto from 'node:crypto';
function safeJsonParse(value, fallback = null) {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
export function createSessionStreamStore({ pool }) {
if (!pool) {
throw new Error('createSessionStreamStore requires pool');
}
async function appendEvent({
userId,
sessionId,
payload,
id = crypto.randomUUID(),
upstreamEventId = null,
} = {}) {
if (!userId || !sessionId || !payload) {
throw new Error('appendEvent requires userId, sessionId, and payload');
}
const createdAt = Date.now();
const eventType = String(payload?.type ?? 'unknown').slice(0, 64);
await pool.query(
`INSERT INTO h5_session_stream_events
(id, user_id, agent_session_id, event_type, payload_json, upstream_event_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
id,
userId,
sessionId,
eventType,
JSON.stringify(payload),
upstreamEventId || null,
createdAt,
],
);
return {
id,
createdAt,
eventType,
payload,
};
}
async function listEventsForUser(userId, sessionId, { afterEventId = null, limit = 500 } = {}) {
if (!userId || !sessionId) return null;
let afterCreatedAt = null;
let cursorMiss = false;
if (afterEventId) {
const [cursorRows] = await pool.query(
`SELECT created_at
FROM h5_session_stream_events
WHERE id = ? AND agent_session_id = ? AND user_id = ?
LIMIT 1`,
[afterEventId, sessionId, userId],
);
if (!cursorRows[0]) {
cursorMiss = true;
} else {
afterCreatedAt = Number(cursorRows[0].created_at);
}
}
const [rows] = await pool.query(
afterCreatedAt == null
? `SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ?
ORDER BY created_at ASC, id ASC
LIMIT ?`
: `SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ? AND created_at > ?
ORDER BY created_at ASC, id ASC
LIMIT ?`,
afterCreatedAt == null
? [sessionId, userId, limit]
: [sessionId, userId, afterCreatedAt, limit],
);
return {
events: rows.map((row) => ({
id: row.id,
eventType: row.event_type,
payload: safeJsonParse(row.payload_json, null),
upstreamEventId: row.upstream_event_id ?? null,
createdAt: Number(row.created_at),
})),
cursorMiss,
};
}
async function getLatestEventForUser(userId, sessionId) {
const [rows] = await pool.query(
`SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 1`,
[sessionId, userId],
);
const row = rows[0];
if (!row) return null;
return {
id: row.id,
eventType: row.event_type,
payload: safeJsonParse(row.payload_json, null),
upstreamEventId: row.upstream_event_id ?? null,
createdAt: Number(row.created_at),
};
}
return {
appendEvent,
listEventsForUser,
getLatestEventForUser,
};
}