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:
john
2026-07-06 16:06:26 +08:00
parent 08feae8bef
commit 14a00774d9
41 changed files with 3501 additions and 126 deletions
+59
View File
@@ -284,6 +284,36 @@ test('agent run starts a session and marks submitted reply as succeeded', async
assert.equal(pool.runs.get(run.id).attempts, 1);
});
test('agent run awaits session Finish before succeeding when proxy supports it', async () => {
const pool = createFakePool();
const awaited = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-finish-1' };
},
async submitSessionReplyAndAwaitFinishForUser(userId, sessionId, requestId, userMessage) {
awaited.push({ userId, sessionId, requestId, userMessage });
return { ok: true, finishEvent: { type: 'Finish' }, tokenState: { totalTokens: 9 } };
},
},
retryDelaysMs: [],
});
const run = await gateway.createRun('user-1', {
requestId: 'req-finish-1',
userMessage: { role: 'user', content: [{ type: 'text', text: '世界杯现在赛况如何' }] },
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.equal(awaited.length, 1);
assert.equal(awaited[0].sessionId, 'session-finish-1');
const finishEvents = pool.events.filter((item) => item.eventType === 'session_finished');
assert.equal(finishEvents.length, 1);
});
test('agent run uses direct chat service for eligible chat messages', async () => {
const pool = createFakePool();
const directRuns = [];
@@ -1458,6 +1488,35 @@ test('listRunEventsForUser replays events after Last-Event-ID cursor', async ()
assert.equal(replay.events[0].data.run.agentSessionId, 'sess-1');
});
test('listRunEventsForUser accepts mysql2-parsed JSON objects in data_json', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {},
autoDispatch: false,
});
const run = await gateway.createRun('user-1', {
requestId: 'req-replay-json-object',
userMessage: { role: 'user', content: [] },
});
const queuedAt = Number(pool.events.find((event) => event.runId === run.id)?.createdAt ?? 1000);
pool.events.push({
id: 'evt-snapshot-object',
runId: run.id,
eventType: 'run_snapshot',
dataJson: { run: { ...run, status: 'running', agentSessionId: 'sess-mysql2' } },
createdAt: queuedAt + 1000,
});
const batch = await gateway.listRunEventsForUser('user-1', run.id);
const snapshotEvent = batch.events.find((event) => event.id === 'evt-snapshot-object');
assert.ok(snapshotEvent);
assert.equal(snapshotEvent.data.run.agentSessionId, 'sess-mysql2');
});
test('listRunEventsForUser reports cursorMiss for unknown Last-Event-ID', async () => {
const pool = createFakePool();
const gateway = createAgentRunGateway({