Files
memind/scripts/verify-meinput-recall-chat.mjs
T
john be464a5b8d
Memind CI / Test, build, and release guards (push) Has been cancelled
Add Rain V0 for MeInput full-range chat analysis and delivery tooling.
Introduce rain-service orchestration, browser-safe chat skill filtering, MeInput
adapter helpers, and verify/deploy scripts so Rain mode can summarize recent input
without Memory V2 pollution.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 13:49:40 +08:00

169 lines
6.1 KiB
JavaScript

#!/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);
});