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
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
/**
|
||
* 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();
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|