Files
jerryandClaude Fable 5 1e94de7464 PlantAI-rag-new 首次提交:文献知识工作台
- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化
- Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab
- 四档响应式(桌面/笔记本/平板/手机)、深浅主题
- 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测
- 真实凭据已脱敏,运行时配置在 backend/.env(不入 git)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 20:47:52 +08:00

139 lines
6.2 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.
// deploy/m2_e2e.js —— M2 集成验收脚本(PM 使用,在远端运行)
// 真实栈端到端:浏览器 → Vite dev(5180) → 真实 BFF(8090) → 真实上游(7920/8000/Neo4j)
// 流程:新建会话 → 问答提问 → 流水线相位推进 → 答案揭示 → 来源 tab → 图谱 tab → 刷新恢复历史
// 运行:cd ~/5090/teamWorkspace/zh/PlantAI-rag-new && node deploy/m2_e2e.js
// 注意:QA 上游 LLM 当前 402 降级,答案内容断言只验「有内容」不断言自然语言质量。
const { chromium } = require('playwright');
const BASE = 'http://127.0.0.1:5180';
const QUESTION = '水稻SPL基因家族的功能研究有哪些关键进展?';
const OUT_DIR = `${__dirname}/../artifacts/m2`;
const results = [];
function check(name, ok, extra = '') {
results.push({ name, ok });
console.log(`${ok ? ' ✅' : ' ❌'} ${name}${extra ? ' —— ' + extra : ''}`);
}
async function waitFor(fn, timeoutMs, intervalMs = 800, label = '') {
const start = Date.now();
let lastErr = null;
while (Date.now() - start < timeoutMs) {
try {
const v = await fn();
if (v) return v;
} catch (e) {
lastErr = e;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`等待超时(${timeoutMs}ms): ${label}${lastErr ? ' last=' + lastErr.message : ''}`);
}
async function stepStates(page) {
return page.$$eval('.pipeline-card .p-step', (els) =>
els.map((el) => ({
name: el.querySelector('.p-name')?.textContent?.trim() || '',
state: el.className.includes('done') ? 'done' : el.className.includes('running') ? 'running' : 'waiting',
})),
);
}
(async () => {
const fs = require('fs');
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const errors = [];
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
page.on('console', (m) => {
if (m.type() === 'error') errors.push('console: ' + m.text());
});
try {
// 1. 加载
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await waitFor(() => page.locator('.app-shell').count(), 15000, 500, 'AppShell 渲染');
check('页面加载:AppShell 渲染', true);
await page.screenshot({ path: `${OUT_DIR}/01-loaded.png` });
// 2. 新建会话
await page.click('.new-task-btn');
await waitFor(() => page.locator('.composer textarea').count(), 15000, 500, '作曲器出现');
check('新建会话:作曲器出现', true);
// 3. 提问(问答模式为默认)
await page.fill('.composer textarea', QUESTION);
await page.click('.send-btn');
await waitFor(() => page.locator('.pipeline-card').count(), 20000, 500, '流水线卡出现');
check('提问:流水线卡出现', true);
// 4. 相位推进(真实 SSE,QA 上游约 40s 完成)
const statesSeen = [];
const t0 = Date.now();
let allDone = false;
while (Date.now() - t0 < 240000) {
const st = await stepStates(page);
const key = JSON.stringify(st);
if (!statesSeen.includes(key)) statesSeen.push(key);
if (st.length && st.every((s) => s.state === 'done')) { allDone = true; break; }
await new Promise((r) => setTimeout(r, 1500));
}
check('相位推进:观察到多次状态变化', statesSeen.length >= 3, `${statesSeen.length} 个不同状态快照`);
check('相位推进:全部完成', allDone, `${Math.round((Date.now() - t0) / 1000)}s`);
await page.screenshot({ path: `${OUT_DIR}/02-pipeline.png` });
// 5. 答案揭示(complete 后揭示动画,最长 8s 加速)
await waitFor(async () => {
const txt = await page.locator('.msg.assistant .msg-body').last().innerText().catch(() => '');
return txt.length > 30;
}, 60000, 1000, '助手答案出现');
const answerText = await page.locator('.msg.assistant .msg-body').last().innerText();
check('答案渲染:有内容', answerText.length > 30, `长度 ${answerText.length}`);
check('答案渲染:含文献证据(PMID/DOI 或降级提示)', /PMID|doi|evidence|证据/i.test(answerText));
await page.screenshot({ path: `${OUT_DIR}/03-answer.png` });
// 6. 文献来源 tab(真实 sources)
await page.click('.bench-tab:has-text("文献来源")');
await waitFor(async () => page.locator('.source-row').count(), 30000, 800, '来源条目出现');
const srcCount = await page.locator('.source-row').count();
check('文献来源:来源条目渲染', srcCount >= 1, `${srcCount} 条`);
await page.screenshot({ path: `${OUT_DIR}/04-sources.png` });
// 7. 知识图谱 tab(答案图谱)
await page.click('.bench-tab:has-text("知识图谱")');
await waitFor(async () => {
const n = await page.locator('.bench-body svg, .bench-body .kg-list, .bench-body .empty-note').count();
return n > 0;
}, 15000, 800, '图谱 tab 内容');
check('知识图谱:tab 有内容(图/列表/空态之一)', true);
await page.screenshot({ path: `${OUT_DIR}/05-kg.png` });
// 8. 刷新恢复(真实 BFF SQLite 持久化)
await page.reload({ waitUntil: 'domcontentloaded' });
await waitFor(() => page.locator('.session-item').count(), 15000, 500, '会话列表加载');
check('刷新:会话列表恢复(SQLite)', true);
await page.click('.session-item:has-text("水稻SPL")');
await waitFor(async () => {
const n = await page.locator('.msg.assistant').count();
return n > 0;
}, 20000, 800, '历史消息恢复');
check('刷新:助手消息历史恢复', true);
await page.screenshot({ path: `${OUT_DIR}/06-restored.png` });
// 9. 浏览器错误
const realErrors = errors.filter((e) => !e.includes('favicon'));
check('无页面级 JS 错误', realErrors.length === 0, realErrors.slice(0, 3).join(' | '));
} catch (e) {
check('流程异常中断', false, e.message);
await page.screenshot({ path: `${OUT_DIR}/99-failure.png` }).catch(() => {});
} finally {
await browser.close();
}
const passed = results.filter((r) => r.ok).length;
console.log(`\nM2 验收结果:${passed}/${results.length} 通过`);
process.exit(passed === results.length ? 0 : 1);
})();