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>
This commit is contained in:
2026-02-23 13:21:52 +08:00
parent b06daecacd
commit 8f06d4f929
39 changed files with 5605 additions and 417 deletions

View File

@@ -0,0 +1,179 @@
/**
* 中文数据源专项测试
*
* 只搜索 CNKI + 中华医学期刊网,验证 Unifuncs API 能否检索中文文献。
*
* 运行: npx tsx src/modules/asl/__tests__/deep-research-chinese-sources.ts
*/
const UNIFUNCS_BASE_URL = 'https://api.unifuncs.com/deepsearch/v1';
const UNIFUNCS_API_KEY = process.env.UNIFUNCS_API_KEY || 'sk-2fNwqUH73elGq0aDKJEM4ReqP7Ry0iqHo4OXyidDe2WpQ9XQ';
const CHINESE_SOURCES = [
'https://www.cnki.net/',
'https://medjournals.cn/',
];
const TEST_QUERIES = [
'2型糖尿病患者SGLT2抑制剂的肾脏保护作用',
'非小细胞肺癌免疫治疗的中国临床研究进展',
];
async function runTest(query: string, domainScope: string[]) {
console.log('\n' + '='.repeat(80));
console.log(`查询: ${query}`);
console.log(`数据源: ${domainScope.join(', ')}`);
console.log('='.repeat(80));
// --- Test 1: SSE 流式模式 ---
console.log('\n--- SSE 流式模式测试 ---');
try {
const payload = {
model: 's2',
messages: [{ role: 'user', content: query }],
stream: true,
introduction: '你是一名专业的中国临床研究文献检索专家,擅长从中国学术数据库中检索中文医学文献。请使用中文关键词进行检索。',
max_depth: 10,
domain_scope: domainScope,
reference_style: 'link',
output_prompt: '请使用中文输出。列出所有检索到的文献,包含标题、作者、期刊、年份和链接。如果文献是中文的,请保留中文信息。',
};
const response = await fetch(`${UNIFUNCS_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${UNIFUNCS_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
console.error(`❌ SSE 请求失败 HTTP ${response.status}: ${text}`);
return;
}
console.log(`✅ SSE 连接成功 (HTTP ${response.status})`);
const reader = (response.body as any).getReader() as ReadableStreamDefaultReader<Uint8Array>;
const decoder = new TextDecoder();
let buffer = '';
let reasoningContent = '';
let content = '';
let chunkCount = 0;
let reasoningChunks = 0;
let contentChunks = 0;
const startTime = Date.now();
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(':')) continue;
if (trimmed.startsWith('data: ')) {
const data = trimmed.slice(6);
if (data === '[DONE]') {
console.log('\n✅ 流式传输完成 [DONE]');
break;
}
try {
const json = JSON.parse(data);
const delta = json.choices?.[0]?.delta;
chunkCount++;
if (delta?.reasoning_content) {
reasoningContent += delta.reasoning_content;
reasoningChunks++;
if (reasoningChunks % 50 === 0) {
process.stdout.write(`\r reasoning chunks: ${reasoningChunks}, content chunks: ${contentChunks}`);
}
} else if (delta?.content) {
content += delta.content;
contentChunks++;
if (contentChunks % 20 === 0) {
process.stdout.write(`\r reasoning chunks: ${reasoningChunks}, content chunks: ${contentChunks}`);
}
}
} catch {
// Skip unparseable
}
}
}
}
reader.releaseLock();
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\n\n📊 统计:`);
console.log(` 耗时: ${elapsed}s`);
console.log(` 总 chunks: ${chunkCount}`);
console.log(` reasoning chunks: ${reasoningChunks} (${reasoningContent.length} chars)`);
console.log(` content chunks: ${contentChunks} (${content.length} chars)`);
// 分析结果
console.log(`\n📝 Reasoning 内容(前 500 字):`);
console.log(reasoningContent.slice(0, 500));
console.log(`\n📝 Content 结果(前 1000 字):`);
console.log(content.slice(0, 1000));
// 检查是否包含中文内容
const chineseCharCount = (content.match(/[\u4e00-\u9fa5]/g) || []).length;
const hasChineseLinks = content.includes('cnki.net') || content.includes('medjournals.cn');
const hasPubMedLinks = content.includes('pubmed.ncbi.nlm.nih.gov');
console.log(`\n🔍 中文分析:`);
console.log(` 中文字符数: ${chineseCharCount}`);
console.log(` 包含 CNKI 链接: ${hasChineseLinks ? '✅ 是' : '❌ 否'}`);
console.log(` 包含 PubMed 链接: ${hasPubMedLinks ? '⚠️ 是(非中文源)' : '✅ 否'}`);
// 统计所有 URL
const urls = content.match(/https?:\/\/[^\s)]+/g) || [];
console.log(` 结果中的链接 (共 ${urls.length} 个):`);
const domainCounts: Record<string, number> = {};
for (const url of urls) {
try {
const domain = new URL(url).hostname;
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
} catch { /* skip */ }
}
for (const [domain, count] of Object.entries(domainCounts).sort((a, b) => b[1] - a[1])) {
console.log(` ${domain}: ${count}`);
}
} catch (err: any) {
console.error(`❌ SSE 测试失败: ${err.message}`);
}
}
async function main() {
console.log('🔬 中文数据源专项测试');
console.log(`API Key: ${UNIFUNCS_API_KEY.slice(0, 10)}...`);
console.log(`测试数据源: ${CHINESE_SOURCES.join(', ')}`);
for (const query of TEST_QUERIES) {
await runTest(query, CHINESE_SOURCES);
}
// 额外测试: 同时包含中英文数据源
console.log('\n\n' + '🔬'.repeat(20));
console.log('附加测试: 中英文混合数据源');
await runTest(
'他汀类药物在心血管疾病一级预防中的最新RCT证据',
['https://pubmed.ncbi.nlm.nih.gov/', ...CHINESE_SOURCES]
);
console.log('\n\n✅ 所有测试完成');
}
main().catch(console.error);

View File

@@ -0,0 +1,345 @@
/**
* Deep Research V2.0 — 端到端集成测试
*
* 测试全流程:
* Step 1: 登录获取 Token
* Step 2: 获取数据源列表
* Step 3: 需求扩写LLM 调用)
* Step 4: 查看 draft 任务
* Step 5: HITL 确认 → 启动执行
* Step 6: 轮询任务直到 completed/failed
* Step 7: 验证结果完整性
* Step 8: Word 导出(可选,需要 Pandoc 微服务)
* Step 9: 清理测试数据
*
* 运行方式:
* npx tsx src/modules/asl/__tests__/deep-research-v2-e2e.ts
*
* 前置条件:
* - 后端服务运行在 localhost:3001
* - PostgreSQL 运行中
* - UNIFUNCS_API_KEY 环境变量已设置
* - 可选Python 微服务运行在 localhost:8000Word 导出)
*
* 预计耗时2-5 分钟(取决于 Unifuncs 搜索深度)
* 预计成本:约 ¥0.05-0.2LLM 需求扩写 + Unifuncs DeepSearch
*/
// ─── 配置 ───────────────────────────────────────
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';
const TEST_QUERY = '他汀类药物在心血管疾病一级预防中的最新 RCT 证据';
const MAX_POLL_ATTEMPTS = 60;
const POLL_INTERVAL_MS = 5000;
const SKIP_WORD_EXPORT = process.env.SKIP_WORD_EXPORT === 'true';
const SKIP_CLEANUP = process.env.SKIP_CLEANUP === 'true';
// ─── 工具函数 ───────────────────────────────────
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
let authToken = '';
async function api(
path: string,
options: RequestInit = {}
): Promise<{ status: number; data: any }> {
const url = `${API_PREFIX}${path}`;
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
...(options.headers || {}),
},
});
const contentType = res.headers.get('content-type') || '';
let data: any;
if (contentType.includes('application/json')) {
data = await res.json();
} else if (contentType.includes('application/vnd.openxmlformats')) {
const buf = await res.arrayBuffer();
data = { _binary: true, byteLength: buf.byteLength };
} else {
data = await res.text();
}
return { status: res.status, data };
}
function assert(condition: boolean, message: string) {
if (!condition) {
throw new Error(`❌ 断言失败: ${message}`);
}
console.log(`${message}`);
}
function printDivider(title: string) {
console.log(`\n${'═'.repeat(70)}`);
console.log(` ${title}`);
console.log(`${'═'.repeat(70)}\n`);
}
// ─── 测试流程 ───────────────────────────────────
async function runE2E() {
console.log('🧪 Deep Research V2.0 端到端集成测试');
console.log(`⏰ 时间: ${new Date().toLocaleString('zh-CN')}`);
console.log(`📍 后端: ${BASE_URL}`);
console.log(`📝 查询: ${TEST_QUERY}`);
console.log('');
let taskId: string | null = null;
const startTime = Date.now();
try {
// ═══════════ Step 1: 登录 ═══════════
printDivider('Step 1: 登录获取 Token');
const loginRes = await api('/auth/login/password', {
method: 'POST',
body: JSON.stringify({ phone: TEST_PHONE, password: TEST_PASSWORD }),
});
if (loginRes.status !== 200 || !loginRes.data.success) {
console.log('⚠️ 登录返回:', JSON.stringify(loginRes.data, null, 2));
throw new Error(
`登录失败 (HTTP ${loginRes.status}): ${loginRes.data.error || loginRes.data.message || '未知错误'}\n` +
`请确认测试账号存在phone=${TEST_PHONE}, password=${TEST_PASSWORD}\n` +
`或通过环境变量指定TEST_PHONE=xxx TEST_PASSWORD=yyy`
);
}
authToken = loginRes.data.data.tokens?.accessToken || loginRes.data.data.accessToken || loginRes.data.data.token;
assert(!!authToken, `获取到 Token (${authToken.slice(0, 20)}...)`);
// ═══════════ Step 2: 获取数据源列表 ═══════════
printDivider('Step 2: 获取数据源列表');
const sourcesRes = await api('/asl/research/data-sources');
assert(sourcesRes.status === 200, `HTTP 200 (实际: ${sourcesRes.status})`);
assert(sourcesRes.data.success === true, 'success=true');
const sources = sourcesRes.data.data;
assert(Array.isArray(sources), `返回数组 (${sources.length} 个数据源)`);
assert(sources.length >= 3, `至少 3 个数据源`);
console.log('\n 数据源列表:');
sources.forEach((s: any) => {
console.log(` ${s.defaultChecked ? '☑' : '☐'} [${s.category}] ${s.label}${s.domainScope}`);
});
const defaultIds = sources.filter((s: any) => s.defaultChecked).map((s: any) => s.domainScope);
console.log(`\n 默认选中: ${defaultIds.join(', ')}`);
// ═══════════ Step 3: 需求扩写 ═══════════
printDivider('Step 3: 需求扩写LLM 调用)');
console.log(' ⏳ 调用 LLM 生成检索指令书,请稍候...\n');
const expandStart = Date.now();
const expandRes = await api('/asl/research/generate-requirement', {
method: 'POST',
body: JSON.stringify({
originalQuery: TEST_QUERY,
targetSources: defaultIds,
filters: { yearRange: '近5年', targetCount: '约20篇' },
}),
});
const expandMs = Date.now() - expandStart;
assert(expandRes.status === 200, `HTTP 200 (实际: ${expandRes.status})`);
assert(expandRes.data.success === true, 'success=true');
const expandData = expandRes.data.data;
taskId = expandData.taskId;
assert(!!taskId, `创建 draft 任务: ${taskId}`);
assert(!!expandData.generatedRequirement, `生成检索指令书 (${expandData.generatedRequirement.length} 字)`);
assert(!!expandData.intentSummary, 'PICOS 结构化摘要已生成');
console.log(`\n ⏱️ 耗时: ${(expandMs / 1000).toFixed(1)}s`);
console.log(`\n 📋 PICOS 摘要:`);
const summary = expandData.intentSummary;
console.log(` 目标: ${summary.objective || '—'}`);
console.log(` P: ${summary.population || '—'}`);
console.log(` I: ${summary.intervention || '—'}`);
console.log(` C: ${summary.comparison || '—'}`);
console.log(` O: ${summary.outcome || '—'}`);
console.log(` 研究设计: ${(summary.studyDesign || []).join(', ')}`);
console.log(` MeSH: ${(summary.meshTerms || []).join(', ')}`);
console.log(`\n 📝 指令书预览 (前 200 字):`);
console.log(` ${expandData.generatedRequirement.slice(0, 200).replace(/\n/g, '\n ')}...`);
// ═══════════ Step 4: 查看 draft 任务 ═══════════
printDivider('Step 4: 查看 draft 状态任务');
const draftRes = await api(`/asl/research/tasks/${taskId}`);
assert(draftRes.status === 200, `HTTP 200`);
assert(draftRes.data.data.status === 'draft', `状态为 draft`);
assert(draftRes.data.data.query === TEST_QUERY, `query 匹配`);
// ═══════════ Step 5: HITL 确认 → 启动 ═══════════
printDivider('Step 5: HITL 确认 → 启动执行');
const confirmedReq = expandData.generatedRequirement;
const executeRes = await api(`/asl/research/tasks/${taskId}/execute`, {
method: 'PUT',
body: JSON.stringify({ confirmedRequirement: confirmedReq }),
});
assert(executeRes.status === 200, `HTTP 200 (实际: ${executeRes.status})`);
assert(executeRes.data.success === true, '任务已入队');
console.log(' 🚀 Deep Research 任务已启动!');
// ═══════════ Step 6: 轮询 ═══════════
printDivider('Step 6: 轮询任务进度');
let lastLogCount = 0;
let finalStatus = '';
for (let i = 1; i <= MAX_POLL_ATTEMPTS; i++) {
await sleep(POLL_INTERVAL_MS);
const pollRes = await api(`/asl/research/tasks/${taskId}`);
const task = pollRes.data.data;
const logs = task.executionLogs || [];
const newLogs = logs.slice(lastLogCount);
if (newLogs.length > 0) {
newLogs.forEach((log: any) => {
const icon: Record<string, string> = {
thinking: '💭', searching: '🔍', reading: '📖',
analyzing: '🧪', summary: '📊', info: '',
};
console.log(` ${icon[log.type] || '•'} [${log.title}] ${log.text.slice(0, 80)}`);
});
lastLogCount = logs.length;
}
console.log(` [${i}/${MAX_POLL_ATTEMPTS}] status=${task.status} | logs=${logs.length} | elapsed=${((Date.now() - startTime) / 1000).toFixed(0)}s`);
if (task.status === 'completed' || task.status === 'failed') {
finalStatus = task.status;
break;
}
}
assert(finalStatus === 'completed', `任务完成 (实际: ${finalStatus || 'timeout'})`);
// ═══════════ Step 7: 验证结果 ═══════════
printDivider('Step 7: 验证结果完整性');
const resultRes = await api(`/asl/research/tasks/${taskId}`);
const result = resultRes.data.data;
assert(result.status === 'completed', '状态: completed');
assert(!!result.synthesisReport, `AI 综合报告已生成 (${(result.synthesisReport || '').length} 字)`);
assert(!!result.completedAt, `completedAt 已设置`);
const hasResultList = Array.isArray(result.resultList) && result.resultList.length > 0;
if (hasResultList) {
console.log(` ✅ 结构化文献列表: ${result.resultList.length}`);
console.log('\n 📚 文献样例 (前 3 篇):');
result.resultList.slice(0, 3).forEach((item: any, i: number) => {
console.log(` ${i + 1}. ${item.title || '(无标题)'}`);
if (item.authors) console.log(` 作者: ${item.authors}`);
if (item.journal) console.log(` 期刊: ${item.journal}`);
if (item.pmid) console.log(` PMID: ${item.pmid}`);
});
} else {
console.log(' ⚠️ 结构化文献列表为空(降级到报告展示模式)');
}
const logs = result.executionLogs || [];
console.log(`\n 📊 执行日志统计: ${logs.length}`);
const typeCounts: Record<string, number> = {};
logs.forEach((l: any) => { typeCounts[l.type] = (typeCounts[l.type] || 0) + 1; });
Object.entries(typeCounts).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
console.log(`\n 📝 报告预览 (前 300 字):`);
console.log(` ${result.synthesisReport.slice(0, 300).replace(/\n/g, '\n ')}...`);
// ═══════════ Step 8: Word 导出 ═══════════
if (!SKIP_WORD_EXPORT) {
printDivider('Step 8: Word 导出(需要 Pandoc 微服务)');
try {
const exportRes = await api(`/asl/research/tasks/${taskId}/export-word`);
if (exportRes.status === 200 && exportRes.data._binary) {
assert(true, `Word 导出成功 (${exportRes.data.byteLength} bytes)`);
} else {
console.log(` ⚠️ Word 导出返回异常 (HTTP ${exportRes.status})`);
console.log(` 这通常是因为 Python 微服务Pandoc未运行可忽略。`);
}
} catch (e: any) {
console.log(` ⚠️ Word 导出跳过: ${e.message}`);
console.log(` 如需测试,请启动 Python 微服务: cd extraction-service && python app.py`);
}
} else {
console.log('\n ⏭️ 跳过 Word 导出测试 (SKIP_WORD_EXPORT=true)');
}
// ═══════════ Step 9: 清理 ═══════════
if (!SKIP_CLEANUP && taskId) {
printDivider('Step 9: 清理测试数据');
try {
const { PrismaClient } = await import('@prisma/client');
const prisma = new PrismaClient();
await prisma.aslResearchTask.delete({ where: { id: taskId } });
await prisma.$disconnect();
console.log(` 🗑️ 已删除测试任务: ${taskId}`);
} catch (e: any) {
console.log(` ⚠️ 清理失败: ${e.message} (可手动删除)`);
}
} else {
console.log(`\n ⏭️ 保留测试数据 taskId=${taskId}`);
}
// ═══════════ 总结 ═══════════
printDivider('🎉 测试通过!');
const totalMs = Date.now() - startTime;
console.log(` 总耗时: ${(totalMs / 1000).toFixed(1)}s`);
console.log(` 任务 ID: ${taskId}`);
console.log(` 综合报告: ${(result.synthesisReport || '').length}`);
console.log(` 文献数量: ${hasResultList ? result.resultList.length : 0}`);
console.log(` 执行日志: ${logs.length}`);
console.log('');
} catch (error: any) {
printDivider('💥 测试失败');
console.error(` 错误: ${error.message}`);
console.error(` 任务 ID: ${taskId || '未创建'}`);
console.error(` 耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
console.error('');
if (error.message.includes('登录失败')) {
console.error(' 💡 提示: 请检查测试账号配置');
console.error(' 可通过环境变量指定: TEST_PHONE=xxx TEST_PASSWORD=yyy npx tsx ...');
}
if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
console.error(' 💡 提示: 后端服务未启动,请先运行:');
console.error(' cd backend && npm run dev');
}
process.exit(1);
}
}
// ─── 入口 ───────────────────────────────────────
runE2E();

View File

@@ -0,0 +1,161 @@
/**
* 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();