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