Files
AIclinicalresearch/backend/src/modules/asl/__tests__/deep-research-v2-smoke.ts
HaHafeng 8f06d4f929 feat(asl): Complete Deep Research V2.0 core development
Backend:
- Add SSE streaming client (unifuncsSseClient) replacing async polling
- Add paragraph-based reasoning parser with mergeConsecutiveThinking
- Add requirement expansion service (DeepSeek-V3 PICOS+MeSH)
- Add Word export service with Pandoc, inline hyperlinks, reference link expansion
- Add deep research V2 worker with 2s log flush and Chinese source prompt
- Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals)
- Add 4 API endpoints (generate-requirement/tasks/task-status/export-word)
- Update Prisma schema with 6 new V2.0 fields on AslResearchTask
- Add DB migration for V2.0 fields
- Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section)

Frontend:
- Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal)
- Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView
- Add react-markdown + remark-gfm for report rendering
- Add custom link component showing visible URLs after references
- Add useDeepResearchTask polling hook
- Add deep research TypeScript types

Tests:
- Add E2E test, smoke test, and Chinese data source test scripts

Docs:
- Update ASL module status (v2.0 - core features complete)
- Update system status (v6.1 - ASL V2.0 milestone)
- Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results)
- Update module auth specification (test script guidelines)
- Update V2.0 development plan

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 13:21:52 +08:00

162 lines
5.3 KiB
TypeScript
Raw 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.
/**
* Deep Research V2.0 — 冒烟测试Smoke Test
*
* 仅测试 API 接口连通性和基本参数校验,
* 不调用 LLM / Unifuncs无外部依赖几秒完成。
*
* 运行方式:
* npx tsx src/modules/asl/__tests__/deep-research-v2-smoke.ts
*
* 前置条件:
* - 后端服务运行在 localhost:3001
* - PostgreSQL 运行中
*/
const BASE_URL = process.env.TEST_BASE_URL || 'http://localhost:3001';
const API_PREFIX = `${BASE_URL}/api/v1`;
const TEST_PHONE = process.env.TEST_PHONE || '13800000001';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456';
let authToken = '';
let passed = 0;
let failed = 0;
async function api(path: string, options: RequestInit = {}): Promise<{ status: number; data: any }> {
const res = await fetch(`${API_PREFIX}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
...(options.headers || {}),
},
});
const contentType = res.headers.get('content-type') || '';
const data = contentType.includes('json') ? await res.json() : await res.text();
return { status: res.status, data };
}
function check(ok: boolean, label: string) {
if (ok) {
console.log(`${label}`);
passed++;
} else {
console.log(`${label}`);
failed++;
}
}
async function run() {
console.log('🔥 Deep Research V2.0 冒烟测试\n');
// ─── 1. 登录 ───
console.log('[1] 登录');
try {
const res = await api('/auth/login/password', {
method: 'POST',
body: JSON.stringify({ phone: TEST_PHONE, password: TEST_PASSWORD }),
});
const ok = res.status === 200 && res.data.success;
check(ok, `POST /auth/login/password → ${res.status}`);
if (ok) {
authToken = res.data.data.tokens?.accessToken || res.data.data.accessToken || res.data.data.token;
} else {
console.log(' ⚠️ 登录失败,后续需要认证的测试将跳过');
console.log(` 返回: ${JSON.stringify(res.data).slice(0, 200)}`);
}
} catch (e: any) {
check(false, `连接后端失败: ${e.message}`);
console.log('\n💡 请先启动后端: cd backend && npm run dev\n');
process.exit(1);
}
// ─── 2. GET /data-sources ───
console.log('\n[2] GET /asl/research/data-sources');
{
const res = await api('/asl/research/data-sources');
check(res.status === 200, `HTTP ${res.status} === 200`);
check(res.data.success === true, 'success=true');
check(Array.isArray(res.data.data) && res.data.data.length >= 3, `返回 ${res.data.data?.length} 个数据源`);
const hasPubmed = res.data.data?.some((s: any) => s.id === 'pubmed');
check(hasPubmed, 'PubMed 存在于数据源列表');
const defaultChecked = res.data.data?.filter((s: any) => s.defaultChecked);
check(defaultChecked?.length >= 1, `默认选中 ${defaultChecked?.length}`);
}
// ─── 3. POST /generate-requirement (参数校验) ───
console.log('\n[3] POST /asl/research/generate-requirement — 参数校验');
{
const res = await api('/asl/research/generate-requirement', {
method: 'POST',
body: JSON.stringify({ originalQuery: '' }),
});
check(res.status === 400, `空 query → HTTP ${res.status} === 400`);
}
{
const res = await api('/asl/research/generate-requirement', {
method: 'POST',
body: JSON.stringify({}),
});
check(res.status === 400, `缺少 query → HTTP ${res.status} === 400`);
}
// ─── 4. PUT /tasks/:taskId/execute (参数校验) ───
console.log('\n[4] PUT /asl/research/tasks/:taskId/execute — 参数校验');
{
const res = await api('/asl/research/tasks/nonexistent-id/execute', {
method: 'PUT',
body: JSON.stringify({ confirmedRequirement: 'test' }),
});
check(res.status === 404, `不存在的 taskId → HTTP ${res.status} === 404`);
}
{
const res = await api('/asl/research/tasks/some-id/execute', {
method: 'PUT',
body: JSON.stringify({ confirmedRequirement: '' }),
});
check(res.status === 400, `空 confirmedRequirement → HTTP ${res.status} === 400`);
}
// ─── 5. GET /tasks/:taskId (不存在) ───
console.log('\n[5] GET /asl/research/tasks/:taskId — 不存在');
{
const res = await api('/asl/research/tasks/nonexistent-id');
check(res.status === 404, `不存在 → HTTP ${res.status} === 404`);
}
// ─── 6. GET /tasks/:taskId/export-word (不存在) ───
console.log('\n[6] GET /asl/research/tasks/:taskId/export-word — 不存在');
{
const res = await api('/asl/research/tasks/nonexistent-id/export-word');
check(res.status === 500 || res.status === 404, `不存在 → HTTP ${res.status}`);
}
// ─── 7. 未认证访问 ───
console.log('\n[7] 未认证访问(无 Token');
{
const savedToken = authToken;
authToken = '';
const res = await api('/asl/research/data-sources');
check(res.status === 401, `无 Token → HTTP ${res.status} === 401`);
authToken = savedToken;
}
// ─── 结果汇总 ───
console.log(`\n${'═'.repeat(50)}`);
console.log(` 🏁 冒烟测试完成: ${passed} 通过, ${failed} 失败 (共 ${passed + failed})`);
console.log(`${'═'.repeat(50)}\n`);
if (failed > 0) {
process.exit(1);
}
}
run();