Files
memind/scripts/verify-chat-intent-followup.mjs
T
John a30b62b987
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(chat-intent): observe routing, keep discuss-first on chat, and escalate short follow-ups
Close the chat-session dead zone without flipping production flags: record 0.72 fallbacks, stop C-2 from treating「先聊聊」as execute, and promote「不对你去执行」after a completed direct turn on both h5direct and date sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 13:13:17 +08:00

196 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Local Stage D check: casual chat then a short "go do it" follow-up.
* Does not require Goose to finish; waits for intent_routed only.
*
* H5_PORT=8086 node scripts/verify-chat-intent-followup.mjs
*/
import crypto from 'node:crypto';
import { loadH5Environment } from './load-env.mjs';
import { createDbPool } from '../db.mjs';
import { createUserAuth, USER_COOKIE } from '../user-auth.mjs';
import { CHAT_INTENT_ROUTE } from '../chat-intent-router.mjs';
import { isDirectFollowupEscalateEnabledForUser, resolveDirectFollowupEscalatePolicy } from '../chat-task-intent-config.mjs';
loadH5Environment(import.meta.dirname);
const PORTAL = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
const USERNAME = process.env.MEMIND_E2E_USERNAME ?? 'john';
const PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '888888';
const DIRECT_MESSAGE = '我想了解车载冰箱,MPV 用,宽度不超过 60cm,先聊聊选购要点';
const FOLLOWUP_MESSAGE = '不对,你去执行';
const EVENT_WAIT_MS = 90_000;
const issues = [];
function pass(label, detail = '') {
console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`);
}
function fail(label, detail = '') {
issues.push({ label, detail });
console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`);
}
function parseEventData(raw) {
if (!raw) return null;
if (typeof raw === 'object') return raw;
try {
return JSON.parse(String(raw));
} catch {
return null;
}
}
async function loginViaApi() {
const response = await fetch(`${PORTAL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
});
const body = await response.json().catch(() => ({}));
if (response.ok && body?.authenticated) {
const setCookie = response.headers.getSetCookie?.() ?? [];
const cookieLine = setCookie.find((line) => line.startsWith(`${USER_COOKIE}=`))
?? response.headers.get('set-cookie');
const match = String(cookieLine ?? '').match(new RegExp(`${USER_COOKIE}=([^;]+)`));
if (match?.[1]) {
return { token: decodeURIComponent(match[1]), userId: body.user?.id ?? null };
}
}
const pool = await createDbPool();
const auth = createUserAuth(pool);
const result = await auth.login({ username: USERNAME, password: PASSWORD, ip: '127.0.0.1' });
await pool.end();
if (!result.ok || !result.token) {
throw new Error(`登录失败: ${result.message ?? 'unknown'}`);
}
return { token: result.token, userId: result.user?.id ?? null };
}
async function createRun(token, { message, sessionId = null }) {
const response = await fetch(`${PORTAL}/api/agent/runs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: `${USER_COOKIE}=${token}`,
},
body: JSON.stringify({
request_id: crypto.randomUUID(),
session_id: sessionId,
user_message: {
id: crypto.randomUUID(),
role: 'user',
content: [{ type: 'text', text: message }],
metadata: { displayText: message, userVisible: true },
},
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`POST /api/agent/runs ${response.status}: ${JSON.stringify(payload)}`);
}
const run = payload.run ?? payload;
return { runId: run.id, sessionId: run.sessionId ?? run.agent_session_id ?? sessionId ?? null };
}
async function getRun(token, runId) {
const response = await fetch(`${PORTAL}/api/agent/runs/${runId}`, {
headers: { Cookie: `${USER_COOKIE}=${token}` },
});
const payload = await response.json().catch(() => ({}));
return payload.run ?? payload;
}
async function readRunEvents(runId) {
const pool = await createDbPool();
const [rows] = await pool.query(
`SELECT event_type, data_json FROM h5_agent_run_events WHERE run_id = ? ORDER BY created_at ASC`,
[runId],
);
await pool.end();
return rows;
}
async function waitForRunEvents(token, runId, { requireCompleted = false } = {}) {
const started = Date.now();
while (Date.now() - started < EVENT_WAIT_MS) {
const run = await getRun(token, runId);
const events = await readRunEvents(runId);
const routed = events.find((row) => row.event_type === 'intent_routed');
const completed = events.find((row) => row.event_type === 'direct_chat_completed');
const sessionId = parseEventData(completed?.data_json)?.sessionId
?? run?.sessionId
?? run?.agent_session_id
?? null;
if (routed && sessionId && (!requireCompleted || completed || ['succeeded', 'failed'].includes(run?.status))) {
return {
run,
events,
routed: parseEventData(routed.data_json),
sessionId,
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`run ${runId} 未在 ${EVENT_WAIT_MS}ms 内拿到路由/session`);
}
async function main() {
const health = await fetch(`${PORTAL}/auth/status`).catch(() => null);
if (!health?.ok) throw new Error(`Portal 不可用: ${PORTAL}/auth/status`);
pass('Portal', PORTAL);
const { token, userId } = await loginViaApi();
pass('登录', `${USERNAME} (${userId ?? 'unknown'})`);
const policy = resolveDirectFollowupEscalatePolicy(process.env);
if (!isDirectFollowupEscalateEnabledForUser(userId, policy)) {
fail('阶段 D flag', `用户 ${userId} 未命中 DIRECT_FOLLOWUP_ESCALATE canary`);
} else {
pass('阶段 D flag', 'enabled for user');
}
const direct = await createRun(token, { message: DIRECT_MESSAGE });
pass('direct 提交', direct.runId);
const directResult = await waitForRunEvents(token, direct.runId, { requireCompleted: true });
if (directResult.routed?.route !== CHAT_INTENT_ROUTE.DIRECT_CHAT) {
fail('direct 路由', String(directResult.routed?.route ?? 'missing'));
} else {
pass('direct 路由', directResult.routed.reason ?? 'direct_chat');
}
const sessionId = directResult.sessionId;
if (!sessionId) fail('direct session', 'missing');
else pass('direct session', sessionId);
const followup = await createRun(token, { message: FOLLOWUP_MESSAGE, sessionId });
pass('followup 提交', followup.runId);
const followupResult = await waitForRunEvents(token, followup.runId);
const routed = followupResult.routed;
if (routed?.route !== CHAT_INTENT_ROUTE.AGENT) {
fail('阶段 D 路由', `期望 agent,实际 ${routed?.route ?? 'missing'} / ${routed?.reason ?? ''}`);
} else if (!String(routed.reason ?? '').includes('短句纠偏')) {
fail('阶段 D 原因', routed.reason ?? 'missing');
} else {
pass('阶段 D 路由', routed.reason);
}
const escalated = followupResult.events.some((row) =>
row.event_type === 'direct_session_escalated_to_deep_reasoning');
const isH5Direct = String(sessionId ?? '').startsWith('h5direct_');
if (escalated) pass('升级事件', 'direct_session_escalated_to_deep_reasoning');
else if (!isH5Direct) pass('升级事件', '日期 session 同会话升级,无需换 Goose session');
else fail('升级事件', '未看到 direct session 升级');
console.log('\n=== 汇总 ===');
if (issues.length) {
for (const item of issues) console.log(` - ${item.label}: ${item.detail}`);
process.exit(1);
}
console.log('chat intent followup 本地验收通过');
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});