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