32fb2cdeaf
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>
232 lines
8.3 KiB
JavaScript
232 lines
8.3 KiB
JavaScript
#!/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 run(VERIFY_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);
|
||
});
|