Files
AIclinicalresearch/backend/src/scripts/test-closeai.ts
HaHafeng 8eef9e0544 feat(asl): Complete Week 4 - Results display and Excel export with hybrid solution
Features:
- Backend statistics API (cloud-native Prisma aggregation)
- Results page with hybrid solution (AI consensus + human final decision)
- Excel export (frontend generation, zero disk write, cloud-native)
- PRISMA-style exclusion reason analysis with bar chart
- Batch selection and export (3 export methods)
- Fixed logic contradiction (inclusion does not show exclusion reason)
- Optimized table width (870px, no horizontal scroll)

Components:
- Backend: screeningController.ts - add getProjectStatistics API
- Frontend: ScreeningResults.tsx - complete results page (hybrid solution)
- Frontend: excelExport.ts - Excel export utility (40 columns full info)
- Frontend: ScreeningWorkbench.tsx - add navigation button
- Utils: get-test-projects.mjs - quick test tool

Architecture:
- Cloud-native: backend aggregation reduces network transfer
- Cloud-native: frontend Excel generation (zero file persistence)
- Reuse platform: global prisma instance, logger
- Performance: statistics API < 500ms, Excel export < 3s (1000 records)

Documentation:
- Update module status guide (add Week 4 features)
- Update task breakdown (mark Week 4 completed)
- Update API design spec (add statistics API)
- Update database design (add field usage notes)
- Create Week 4 development plan
- Create Week 4 completion report
- Create technical debt list

Test:
- End-to-end flow test passed
- All features verified
- Performance test passed
- Cloud-native compliance verified

Ref: Week 4 Development Plan
Scope: ASL Module MVP - Title Abstract Screening Results
Cloud-Native: Backend aggregation + Frontend Excel generation
2025-11-21 20:12:38 +08:00

365 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* CloseAI集成测试脚本
*
* 测试通过CloseAI代理访问GPT-5和Claude-4.5模型
*
* 运行方式:
* ```bash
* cd backend
* npx tsx src/scripts/test-closeai.ts
* ```
*
* 环境变量要求:
* - CLOSEAI_API_KEY: CloseAI API密钥
* - CLOSEAI_OPENAI_BASE_URL: OpenAI端点
* - CLOSEAI_CLAUDE_BASE_URL: Claude端点
*
* 参考文档docs/02-通用能力层/01-LLM大模型网关/03-CloseAI集成指南.md
*/
import { LLMFactory } from '../common/llm/adapters/LLMFactory.js';
import { config } from '../config/env.js';
/**
* 测试配置验证
*/
function validateConfig() {
console.log('🔍 验证环境配置...\n');
const checks = [
{
name: 'CLOSEAI_API_KEY',
value: config.closeaiApiKey,
required: true,
},
{
name: 'CLOSEAI_OPENAI_BASE_URL',
value: config.closeaiOpenaiBaseUrl,
required: true,
},
{
name: 'CLOSEAI_CLAUDE_BASE_URL',
value: config.closeaiClaudeBaseUrl,
required: true,
},
];
let allValid = true;
for (const check of checks) {
const status = check.value ? '✅' : '❌';
console.log(`${status} ${check.name}: ${check.value ? '已配置' : '未配置'}`);
if (check.required && !check.value) {
allValid = false;
}
}
console.log('');
if (!allValid) {
throw new Error('环境配置不完整,请检查 .env 文件');
}
console.log('✅ 环境配置验证通过\n');
}
/**
* 测试GPT-5-Pro
*/
async function testGPT5() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('1⃣ 测试 GPT-5-Pro');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
try {
const gpt5 = LLMFactory.getAdapter('gpt-5');
console.log('📤 发送测试请求...');
console.log('提示词: "你好,请用一句话介绍你自己。"\n');
const startTime = Date.now();
const response = await gpt5.chat([
{
role: 'user',
content: '你好,请用一句话介绍你自己。',
},
]);
const duration = Date.now() - startTime;
console.log('📥 收到响应:\n');
console.log(`模型: ${response.model}`);
console.log(`内容: ${response.content}`);
console.log(`耗时: ${duration}ms`);
if (response.usage) {
console.log(`Token使用: ${response.usage.totalTokens} (输入: ${response.usage.promptTokens}, 输出: ${response.usage.completionTokens})`);
}
console.log('\n✅ GPT-5测试通过\n');
return true;
} catch (error) {
console.error('\n❌ GPT-5测试失败:', error);
return false;
}
}
/**
* 测试Claude-4.5-Sonnet
*/
async function testClaude() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('2⃣ 测试 Claude-4.5-Sonnet');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
try {
const claude = LLMFactory.getAdapter('claude-4.5');
console.log('📤 发送测试请求...');
console.log('提示词: "你好,请用一句话介绍你自己。"\n');
const startTime = Date.now();
const response = await claude.chat([
{
role: 'user',
content: '你好,请用一句话介绍你自己。',
},
]);
const duration = Date.now() - startTime;
console.log('📥 收到响应:\n');
console.log(`模型: ${response.model}`);
console.log(`内容: ${response.content}`);
console.log(`耗时: ${duration}ms`);
if (response.usage) {
console.log(`Token使用: ${response.usage.totalTokens} (输入: ${response.usage.promptTokens}, 输出: ${response.usage.completionTokens})`);
}
console.log('\n✅ Claude测试通过\n');
return true;
} catch (error) {
console.error('\n❌ Claude测试失败:', error);
return false;
}
}
/**
* 测试文献筛选场景(实际应用)
*/
async function testLiteratureScreening() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('3⃣ 测试文献筛选场景(双模型对比)');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const testLiterature = {
title: 'Deep learning in medical imaging: A systematic review',
abstract: 'Background: Deep learning has shown remarkable performance in various medical imaging tasks. Methods: We systematically reviewed 150 studies on deep learning applications in radiology, pathology, and ophthalmology. Results: Deep learning models achieved high accuracy (>90%) in most tasks. Conclusion: Deep learning is a promising tool for medical image analysis.',
};
const picoPrompt = `
请根据以下PICO标准判断这篇文献是否应该纳入系统评价
**PICO标准**
- Population: 成年患者
- Intervention: 深度学习模型
- Comparison: 传统机器学习方法
- Outcome: 诊断准确率
**文献信息:**
标题:${testLiterature.title}
摘要:${testLiterature.abstract}
请输出JSON格式
{
"decision": "include/exclude/uncertain",
"reason": "判断理由",
"confidence": 0.0-1.0
}
`;
try {
console.log('📤 使用DeepSeek和GPT-5进行双模型对比筛选...\n');
// 并行调用两个模型
const [deepseekAdapter, gpt5Adapter] = [
LLMFactory.getAdapter('deepseek-v3'),
LLMFactory.getAdapter('gpt-5'),
];
const startTime = Date.now();
const [deepseekResponse, gpt5Response] = await Promise.all([
deepseekAdapter.chat([{ role: 'user', content: picoPrompt }]),
gpt5Adapter.chat([{ role: 'user', content: picoPrompt }]),
]);
const duration = Date.now() - startTime;
console.log('📥 DeepSeek响应:');
console.log(deepseekResponse.content);
console.log('');
console.log('📥 GPT-5响应:');
console.log(gpt5Response.content);
console.log('');
console.log(`⏱️ 总耗时: ${duration}ms并行`);
console.log(`💰 总Token: ${(deepseekResponse.usage?.totalTokens || 0) + (gpt5Response.usage?.totalTokens || 0)}`);
// 尝试解析JSON结果简单验证
try {
const deepseekDecision = JSON.parse(deepseekResponse.content);
const gpt5Decision = JSON.parse(gpt5Response.content);
console.log('\n✅ 双模型筛选结果:');
console.log(`DeepSeek决策: ${deepseekDecision.decision} (置信度: ${deepseekDecision.confidence})`);
console.log(`GPT-5决策: ${gpt5Decision.decision} (置信度: ${gpt5Decision.confidence})`);
if (deepseekDecision.decision === gpt5Decision.decision) {
console.log('✅ 两个模型一致,共识度高');
} else {
console.log('⚠️ 两个模型不一致建议人工复核或启用第三方仲裁Claude');
}
} catch (parseError) {
console.log('⚠️ JSON解析失败测试环境实际应用需要优化提示词');
}
console.log('\n✅ 文献筛选场景测试通过\n');
return true;
} catch (error) {
console.error('\n❌ 文献筛选场景测试失败:', error);
return false;
}
}
/**
* 测试流式调用(可选)
*/
async function testStreamMode() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('4⃣ 测试流式调用GPT-5');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
try {
const gpt5 = LLMFactory.getAdapter('gpt-5');
console.log('📤 发送流式请求...');
console.log('提示词: "请写一首关于人工智能的短诗4行"\n');
console.log('📥 流式响应:\n');
const startTime = Date.now();
let fullContent = '';
let chunkCount = 0;
for await (const chunk of gpt5.chatStream([
{
role: 'user',
content: '请写一首关于人工智能的短诗4行',
},
])) {
if (chunk.content) {
process.stdout.write(chunk.content);
fullContent += chunk.content;
chunkCount++;
}
if (chunk.done) {
const duration = Date.now() - startTime;
console.log('\n');
console.log(`\n⏱ 耗时: ${duration}ms`);
console.log(`📦 Chunk数: ${chunkCount}`);
console.log(`📝 总字符数: ${fullContent.length}`);
if (chunk.usage) {
console.log(`💰 Token使用: ${chunk.usage.totalTokens}`);
}
}
}
console.log('\n✅ 流式调用测试通过\n');
return true;
} catch (error) {
console.error('\n❌ 流式调用测试失败:', error);
return false;
}
}
/**
* 主测试函数
*/
async function main() {
console.log('╔═══════════════════════════════════════════════════╗');
console.log('║ 🧪 CloseAI集成测试 ║');
console.log('║ 测试GPT-5和Claude-4.5通过CloseAI代理访问 ║');
console.log('╚═══════════════════════════════════════════════════╝\n');
try {
// 验证配置
validateConfig();
// 测试结果
const results = {
gpt5: false,
claude: false,
literatureScreening: false,
stream: false,
};
// 1. 测试GPT-5
results.gpt5 = await testGPT5();
// 2. 测试Claude-4.5
results.claude = await testClaude();
// 3. 测试文献筛选场景
results.literatureScreening = await testLiteratureScreening();
// 4. 测试流式调用(可选)
results.stream = await testStreamMode();
// 总结
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 测试总结');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const allPassed = Object.values(results).every((r) => r === true);
console.log(`GPT-5测试: ${results.gpt5 ? '✅ 通过' : '❌ 失败'}`);
console.log(`Claude测试: ${results.claude ? '✅ 通过' : '❌ 失败'}`);
console.log(`文献筛选场景: ${results.literatureScreening ? '✅ 通过' : '❌ 失败'}`);
console.log(`流式调用测试: ${results.stream ? '✅ 通过' : '❌ 失败'}`);
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (allPassed) {
console.log('🎉 所有测试通过CloseAI集成成功');
console.log('\n✅ 可以在ASL模块中使用GPT-5和Claude-4.5进行双模型对比筛选');
console.log('✅ 支持三模型共识仲裁DeepSeek + GPT-5 + Claude');
console.log('✅ 支持流式调用,适用于实时响应场景\n');
process.exit(0);
} else {
console.error('⚠️ 部分测试失败,请检查配置和网络连接\n');
process.exit(1);
}
} catch (error) {
console.error('❌ 测试执行失败:', error);
process.exit(1);
}
}
// 运行测试
main();