a30b62b987
Memind CI / Test, build, and release guards (push) Has been cancelled
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>
196 lines
7.3 KiB
JavaScript
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);
|
|
});
|