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>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user