Files
memind/scripts/verify-oa-excel-analysis.mjs
john 32fb2cdeaf feat: chat uploads, vision turn isolation, and MindSpace agent improvements
Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 00:23:01 +08:00

232 lines
8.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Verify OA selected Excel analysis chain (john4 面条区域销售数据_示例.xlsx).
* Checks injected context + optional live agent run.
*/
import crypto from 'node:crypto';
import {
buildContextPrefix,
buildMindSpaceChatContext,
} from '../mindspace-chat-context.mjs';
import { USER_COOKIE } from '../user-auth.mjs';
import {
createReporter,
resolvePortalBase,
waitForAssistantGrowth,
waitForRunTerminal,
} from './scenario-test-lib.mjs';
const BASE = resolvePortalBase(process.env.PORTAL_PORT ?? process.env.PORT ?? 8081);
const ASSET_ID = process.env.OA_EXCEL_ASSET_ID ?? '597c0177-becd-4b89-be72-1b5fd3496f17';
const LIVE_RUN = process.env.VERIFY_OA_EXCEL_LIVE !== '0';
const TIMEOUT_MS = Number(process.env.VERIFY_OA_EXCEL_TIMEOUT_MS ?? 120000);
function buildUserAddressPrefix(username) {
return `[用户身份]
- 当前登录用户称呼:${username}
- 你是 TKMind 助手;与用户对话时用「${username}」称呼对方(如「${username},你好」),禁止把用户叫作 TKMind。
`;
}
async function login(reporter) {
const password = process.env.JOHN4_PASSWORD ?? '888888';
const response = await fetch(`${BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'john4', password }),
});
const body = await response.json().catch(() => ({}));
if (!response.ok || !body?.authenticated) {
throw new Error(`登录失败: ${response.status} ${JSON.stringify(body)}`);
}
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}=([^;]+)`));
const token = match?.[1] ? decodeURIComponent(match[1]) : null;
if (!token) throw new Error('未解析到会话 Cookie');
reporter.pass('登录', `john4 (${body.user?.id ?? 'unknown'})`);
return { cookie: `${USER_COOKIE}=${token}`, user: body.user };
}
async function apiFetch(cookie, path) {
const response = await fetch(`${BASE}${path}`, { headers: { Cookie: cookie } });
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(`${path} ${response.status}: ${JSON.stringify(payload)}`);
}
return payload;
}
function countProbeNarration(text) {
const patterns = [
/Let me first look/i,
/没有 [`']?oa\/?[`']? 文件夹/i,
/文件不在当前工作区/i,
/确认工作目录/i,
/工作目录跳转/i,
/list_dir oa/i,
];
return patterns.filter((re) => re.test(text)).length;
}
async function main() {
const reporter = createReporter();
const { cookie, user } = await login(reporter);
const spacePayload = await apiFetch(cookie, '/api/mindspace/v1/space');
const space = spacePayload.data;
const oaCategory = space.categories.find((item) => item.code === 'oa');
if (!oaCategory) {
reporter.fail('OA 分类', 'space 中未找到 oa 分类');
process.exit(reporter.summary());
}
const assetsPayload = await apiFetch(cookie, '/api/mindspace/v1/assets?category=oa');
const assets = assetsPayload.data ?? [];
const asset = assets.find((item) => item.id === ASSET_ID)
?? assets.find((item) => /面条区域销售数据/.test(item.displayName ?? ''));
if (!asset) {
reporter.fail('Excel 资料', `未找到 asset ${ASSET_ID}`);
process.exit(reporter.summary());
}
reporter.pass('Excel 资料', `${asset.displayName} (${asset.id})`);
if (asset.workspaceRelativePath) {
reporter.pass('workspaceRelativePath', asset.workspaceRelativePath);
} else {
reporter.fail('workspaceRelativePath', 'API 未返回落盘路径');
}
const staleCategory = { ...oaCategory, itemCount: 0 };
const visibleAssets = [asset];
const context = buildMindSpaceChatContext({
space: { ...space, categories: space.categories.map((c) => (c.code === 'oa' ? staleCategory : c)) },
ownerUsername: user.username ?? 'john4',
selectedCategory: staleCategory,
selectedPageId: null,
pages: [],
assets: visibleAssets,
selectedAssets: [asset],
route: '/space?category=oa',
agentSessionId: 'verify-session',
h5ApiBase: BASE,
});
if (context.category?.itemCount === 1) {
reporter.pass('itemCount 兜底', 'stale 0 → effective 1');
} else {
reporter.fail('itemCount 兜底', `expected 1 got ${context.category?.itemCount}`);
}
const prefix = buildContextPrefix(context);
const prefixChecks = [
...(asset.workspaceRelativePath
? [
['已落盘路径', /已落盘路径/],
['read_file 路径', new RegExp(`read_file ${asset.workspaceRelativePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
['禁止 list_dir', /禁止.*list_dir/],
]
: [
['优先 list_dir oa', /优先.*list_dir oa/],
]),
['数据表汇总校验', /数据表汇总校验/],
['共 1 项', /共 1 项/],
];
for (const [label, pattern] of prefixChecks) {
if (pattern.test(prefix)) reporter.pass(`上下文 · ${label}`, 'ok');
else reporter.fail(`上下文 · ${label}`, 'prefix 缺少预期片段');
}
if (!LIVE_RUN) {
console.log('\n跳过 live runVERIFY_OA_EXCEL_LIVE=0');
process.exit(reporter.summary());
}
const userText = '帮我分析一下这个数据吧';
const agentText = `${buildUserAddressPrefix(user.username ?? 'john4')}${prefix}${userText}`;
const requestId = crypto.randomUUID();
const runResponse = await fetch(`${BASE}/api/agent/runs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: cookie,
},
body: JSON.stringify({
request_id: requestId,
user_message: {
id: crypto.randomUUID(),
role: 'user',
content: [{ type: 'text', text: agentText }],
metadata: { userVisible: true, displayText: userText },
},
selected_asset_ids: [asset.id],
}),
});
const runPayload = await runResponse.json().catch(() => ({}));
if (!runResponse.ok) {
reporter.fail('创建 run', `${runResponse.status} ${JSON.stringify(runPayload)}`);
process.exit(reporter.summary());
}
const run = runPayload.run ?? runPayload;
const runId = run.id;
let resolvedSessionId = run.sessionId ?? run.agent_session_id ?? run.agentSessionId ?? null;
reporter.pass('创建 run', `${runId.slice(0, 8)}${resolvedSessionId ? ` session=${resolvedSessionId}` : ''}`);
const terminal = await waitForRunTerminal(BASE, cookie, runId, TIMEOUT_MS);
resolvedSessionId = resolvedSessionId
?? terminal.agent_session_id
?? terminal.agentSessionId
?? terminal.sessionId
?? null;
if (!resolvedSessionId) {
reporter.fail('sessionId', 'run 完成后仍无法解析 sessionId');
process.exit(reporter.summary());
}
reporter.pass('run 终态', `${terminal.status} · session=${resolvedSessionId}`);
const growth = await waitForAssistantGrowth(BASE, cookie, resolvedSessionId, {
previousCount: 0,
minChars: 80,
timeoutMs: 5000,
});
if (!growth?.combined) {
reporter.fail('assistant 回复', '未拿到可见回复');
process.exit(reporter.summary());
}
const combined = growth.combined;
reporter.pass('assistant 回复长度', `${combined.length} chars / ${growth.count} msgs`);
const probeHits = countProbeNarration(combined);
if (probeHits === 0) {
reporter.pass('路径探测旁白', '未检测到典型 probe 句式');
} else {
reporter.fail('路径探测旁白', `仍检测到 ${probeHits} 类 probe 句式`);
}
if (/4[,.]?872[,.]?000|4872000|487\.2\s*万/.test(combined)) {
reporter.pass('总销售额', '包含正确总计 ~487.2万');
} else if (/4[,.]?470[,.]?500|4470500/.test(combined)) {
reporter.fail('总销售额', '仍输出错误的 447.05 万');
} else {
reporter.fail('总销售额', '回复中未识别到明确总计(需人工查看)');
}
if (/54[,.]?200/.test(combined) && !/55[,.]?400|55400/.test(combined)) {
reporter.fail('总销量', '仍输出错误的 54200 箱');
} else if (/55[,.]?400|55400/.test(combined)) {
reporter.pass('总销量', '包含正确总计 55400 箱');
}
console.log('\n--- assistant 摘要 ---');
console.log(combined.slice(0, 1200));
process.exit(reporter.summary());
}
main().catch((err) => {
console.error(err);
process.exit(1);
});