#!/usr/bin/env node /** * Verify meinput temporal recall → chat injection for a logged-in user. * * Usage: * MEMIND_BASE_URL=http://127.0.0.1:8081 \ * MEMIND_USERNAME=john MEMIND_PASSWORD=981122tj \ * node scripts/verify-meinput-recall-chat.mjs */ import { loadH5Environment } from './load-env.mjs'; import { createAgentRun, createReporter, extractAssistantTexts, getSession, loginViaApi, resolvePortalBase, waitForAssistantGrowth, waitForRunTerminal, } from './scenario-test-lib.mjs'; loadH5Environment(import.meta.dirname); const QUERY = process.env.MEINPUT_VERIFY_QUERY ?? '最近我输入了什么?'; const baseUrl = (process.env.MEMIND_BASE_URL ?? resolvePortalBase(Number(process.env.H5_PORT ?? 8081))).replace(/\/$/, ''); const account = { username: process.env.MEMIND_USERNAME ?? process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john', password: process.env.MEMIND_PASSWORD ?? process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj', }; async function verifyTemporalRecallApi(cookie) { const headers = { 'content-type': 'application/json', cookie }; const planRes = await fetch(`${baseUrl}/api/v1/context/plan`, { method: 'POST', headers, body: JSON.stringify({ query: QUERY, now: new Date().toISOString() }), }); const planBody = await planRes.json(); if (!planRes.ok) throw new Error(`context/plan ${planRes.status}: ${JSON.stringify(planBody)}`); const recallRes = await fetch(`${baseUrl}/api/v1/temporal-recall/query`, { method: 'POST', headers, body: JSON.stringify({ plan: planBody.plan, limit: 20 }), }); const recallBody = await recallRes.json(); if (!recallRes.ok) throw new Error(`temporal-recall/query ${recallRes.status}: ${JSON.stringify(recallBody)}`); const meinputItems = []; for (const group of recallBody.groups ?? []) { for (const item of group.items ?? []) { if (item.source === 'meinput') meinputItems.push(item); } } for (const item of recallBody.items ?? []) { if (item.source === 'meinput') meinputItems.push(item); } return { plan: planBody.plan, stats: recallBody.stats, meinputItems, sampleTitles: meinputItems.slice(0, 5).map((item) => item.title?.slice(0, 40)), }; } function replyLooksGrounded(text, meinputSamples = []) { const normalized = String(text ?? ''); if (/meinput|\[meinput\]|输入记录|按键|键盘输入/u.test(normalized)) return true; const keywords = ['项目', '手机', '越狱', '安装', '2026-09-02', '19:4']; if (keywords.some((kw) => normalized.includes(kw))) return true; return meinputSamples.some((title) => title && normalized.includes(String(title).slice(0, 4))); } async function main() { const reporter = createReporter(); console.log(`==> meinput recall chat verify`); console.log(` Portal: ${baseUrl}`); console.log(` User: ${account.username}`); console.log(` Query: ${QUERY}\n`); const statusRes = await fetch(`${baseUrl}/auth/status`); if (!statusRes.ok) throw new Error(`Portal 未就绪: ${statusRes.status}`); const auth = await loginViaApi(baseUrl, account, reporter); const api = await verifyTemporalRecallApi(auth.cookie); reporter.pass( 'API plan 识别 temporal recall', `${api.plan?.query_type ?? 'unknown'} / meinput=${api.plan?.sources?.meinput ?? '?'}`, ); if (!(api.stats?.sources_queried ?? []).includes('meinput')) { reporter.fail('API 检索源', `未查询 meinput: ${JSON.stringify(api.stats?.sources_queried)}`); } else { reporter.pass('API 检索源', 'meinput 已参与查询'); } if ((api.stats?.raw_count ?? 0) <= 0) { reporter.fail('API raw_count', 'meinput/chat 未返回原始条目'); } else { reporter.pass('API raw_count', String(api.stats.raw_count)); } if ((api.stats?.returned_count ?? 0) <= 0) { reporter.fail('API returned_count', '排序后无条目可注入'); } else { reporter.pass('API returned_count', String(api.stats.returned_count)); } if (api.meinputItems.length <= 0) { reporter.fail('API meinput 条目', 'groups/items 中无 meinput 源数据'); } else { reporter.pass('API meinput 条目', `${api.meinputItems.length} 条,样例: ${api.sampleTitles.join(' | ')}`); } const run = await createAgentRun(baseUrl, auth.cookie, { message: QUERY }); reporter.pass('发起聊天 run', run.runId); const terminal = await waitForRunTerminal(baseUrl, auth.cookie, run.runId, 180000); if (terminal.status !== 'succeeded') { reporter.fail('聊天 run 终态', terminal.status ?? 'unknown'); } else { reporter.pass('聊天 run 终态', 'succeeded'); } const sessionId = run.sessionId ?? terminal.sessionId ?? terminal.agent_session_id; if (!sessionId) { reporter.fail('会话 ID', 'run 未返回 sessionId'); } else { const growth = await waitForAssistantGrowth(baseUrl, auth.cookie, sessionId, { minChars: 20, timeoutMs: 5000, }); const session = growth ? { ok: true, session: { conversation: [] } } : await getSession(baseUrl, auth.cookie, sessionId); const texts = growth?.texts ?? extractAssistantTexts(session.session ?? session.payload ?? session); const reply = (growth?.combined ?? texts.join('\n')).trim(); if (!reply) { reporter.fail('助手回复', '会话中无 assistant 文本'); } else { reporter.pass('助手回复长度', `${reply.length} 字符`); console.log('\n--- assistant preview ---'); console.log(reply.slice(0, 600)); console.log('--- end preview ---\n'); if (replyLooksGrounded(reply, api.sampleTitles)) { reporter.pass('回复 grounded', '包含 meinput/输入记录或样例关键词'); } else { reporter.fail( '回复 grounded', '未检测到 meinput 注入痕迹(项目/手机/输入记录等)', ); } if (/无法直接访问|不能确切知道|没有权限访问/u.test(reply) && !replyLooksGrounded(reply, api.sampleTitles)) { reporter.fail('空注入幻觉', '模型声称无法访问记录,但 API 明明有 meinput 数据'); } } } process.exit(reporter.summary()); } main().catch((err) => { console.error(err); process.exit(1); });