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,151 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Local smoke: verify "世界杯现在赛况如何" routes to agent_orchestration.
|
||||
* Usage: node scripts/verify-world-cup-routing.mjs
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import { createDbPool } from '../db.mjs';
|
||||
import { createUserAuth } from '../user-auth.mjs';
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||
const USERNAME = 'john';
|
||||
const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888';
|
||||
const QUERY = '世界杯现在赛况如何';
|
||||
const MAX_WAIT_MS = 120_000;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const pool = await createDbPool();
|
||||
const auth = createUserAuth(pool);
|
||||
let result = await auth.login({ username: USERNAME, password: PASSWORD, ip: '127.0.0.1' });
|
||||
if (!result.ok) {
|
||||
throw new Error(`john 登录失败(请设置 JOHN_PASSWORD 环境变量): ${result.message ?? 'unknown'}`);
|
||||
}
|
||||
await pool.end();
|
||||
return result.token;
|
||||
}
|
||||
|
||||
async function createRun(token) {
|
||||
const requestId = crypto.randomUUID();
|
||||
const response = await fetch(`${PORTAL}/api/agent/runs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `tkmind_user_session=${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: QUERY }],
|
||||
metadata: { userVisible: true, displayText: QUERY },
|
||||
},
|
||||
}),
|
||||
});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(`POST /agent/runs ${response.status}: ${JSON.stringify(body)}`);
|
||||
}
|
||||
const run = body.run ?? body;
|
||||
return { runId: run.id, requestId, status: run.status };
|
||||
}
|
||||
|
||||
async function waitForRun(token, runId) {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < MAX_WAIT_MS) {
|
||||
const response = await fetch(`${PORTAL}/api/agent/runs/${runId}`, {
|
||||
headers: { Cookie: `tkmind_user_session=${token}` },
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const body = payload.run ?? payload;
|
||||
if (!response.ok) {
|
||||
throw new Error(`GET /agent/runs/${runId} ${response.status}: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
if (['succeeded', 'failed'].includes(body.status)) {
|
||||
return body;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
throw new Error(`run ${runId} 未在 ${MAX_WAIT_MS}ms 内完成`);
|
||||
}
|
||||
|
||||
async function loadRunEvents(runId) {
|
||||
const pool = await createDbPool();
|
||||
const [rows] = await pool.query(
|
||||
`SELECT event_type, data_json, created_at
|
||||
FROM h5_agent_run_events
|
||||
WHERE run_id = ?
|
||||
ORDER BY created_at ASC`,
|
||||
[runId],
|
||||
);
|
||||
await pool.end();
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function loadAssistantReply(sessionId) {
|
||||
const pool = await createDbPool();
|
||||
const [rows] = await pool.query(
|
||||
`SELECT text FROM h5_conversation_messages
|
||||
WHERE agent_session_id = ? AND role = 'assistant'
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
[sessionId],
|
||||
);
|
||||
await pool.end();
|
||||
return rows[0]?.text ?? '';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`==> Portal: ${PORTAL}`);
|
||||
const token = await login();
|
||||
console.log('==> john 登录成功');
|
||||
|
||||
const { runId } = await createRun(token);
|
||||
console.log(`==> run 已创建: ${runId}`);
|
||||
|
||||
const run = await waitForRun(token, runId);
|
||||
console.log(`==> run 终态: ${run.status} session=${run.sessionId ?? run.agent_session_id ?? 'n/a'}`);
|
||||
|
||||
const events = await loadRunEvents(runId);
|
||||
const routed = events.find((row) => row.event_type === 'intent_routed');
|
||||
const route = routed?.data_json?.route ?? null;
|
||||
const source = routed?.data_json?.source ?? null;
|
||||
const reason = routed?.data_json?.reason ?? null;
|
||||
|
||||
console.log('\n--- intent_routed ---');
|
||||
console.log(JSON.stringify({ route, source, reason, suggestedSkill: routed?.data_json?.suggestedSkill ?? null }, null, 2));
|
||||
|
||||
const sessionId = run.sessionId ?? run.agent_session_id;
|
||||
if (sessionId) {
|
||||
const reply = await loadAssistantReply(sessionId);
|
||||
console.log('\n--- assistant 回复摘要 ---');
|
||||
console.log(reply.slice(0, 500));
|
||||
if (/无法实时获取|无法获取.*最新赛况/i.test(reply)) {
|
||||
console.error('\nFAIL: 仍走 direct_chat 拒答路径');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (route !== 'agent_orchestration') {
|
||||
console.error(`\nFAIL: 期望 route=agent_orchestration,实际 ${route}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (run.status !== 'succeeded') {
|
||||
console.error(`\nFAIL: run 终态 ${run.status}: ${run.error ?? ''}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\nPASS: 世界杯查询已走 agent 路径');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err instanceof Error ? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user