Files
AIclinicalresearch/backend/src/index.ts
HaHafeng 5c5fec52c1 fix(aia,ssa,asl,infra): harden SSE transport and stabilize attachment context
Deliver SSE protocol hardening for SAE/HTTP2 paths, add graceful shutdown health behavior, and improve SSA retry UX for transient stream failures. For AIA, persist attachment extraction results in database with cache read-through fallback, plus production cache safety guard to prevent memory-cache drift in multi-instance deployments; also restore ASL SR page scrolling behavior.

Made-with: Cursor
2026-03-09 18:45:12 +08:00

386 lines
16 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.
import Fastify from 'fastify';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import { config, validateEnv } from './config/env.js';
import { testDatabaseConnection, prisma } from './config/database.js';
import { projectRoutes } from './legacy/routes/projects.js';
import { agentRoutes } from './legacy/routes/agents.js';
import { conversationRoutes } from './legacy/routes/conversations.js';
import knowledgeBaseRoutes from './legacy/routes/knowledgeBases.js';
import { chatRoutes } from './legacy/routes/chatRoutes.js';
import { batchRoutes } from './legacy/routes/batchRoutes.js';
import reviewRoutes from './legacy/routes/reviewRoutes.js';
import { rvwRoutes } from './modules/rvw/index.js';
import { aslRoutes } from './modules/asl/routes/index.js';
import { registerDCRoutes, initDCModule } from './modules/dc/index.js';
import pkbRoutes from './modules/pkb/routes/index.js';
import { aiaRoutes } from './modules/aia/index.js';
import { registerHealthRoutes, markShuttingDown } from './common/health/index.js';
import { logger } from './common/logging/index.js';
import { authRoutes, registerAuthPlugin } from './common/auth/index.js';
import { promptRoutes } from './common/prompt/index.js';
import { registerTestRoutes } from './test-platform-api.js';
import { registerScreeningWorkers } from './modules/asl/services/screeningWorker.js';
import { registerResearchWorker } from './modules/asl/workers/researchWorker.js';
import { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.js';
import { registerExtractionWorkers as registerAslExtractionWorkers } from './modules/asl/extraction/workers/index.js';
import { registerParseExcelWorker } from './modules/dc/tool-c/workers/parseExcelWorker.js';
import { registerReviewWorker } from './modules/rvw/workers/reviewWorker.js';
import { jobQueue } from './common/jobs/index.js';
// 全局处理BigInt序列化
(BigInt.prototype as any).toJSON = function() {
return Number(this);
};
// 生产环境使用JSON格式日志性能更好开发环境使用pino-pretty易读
const fastify = Fastify({
logger: config.nodeEnv === 'production'
? {
level: config.logLevel,
// 生产环境简单的JSON日志适合日志收集系统
}
: {
level: config.logLevel,
// 开发环境使用pino-pretty美化输出
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
},
});
// 注册CORS插件 - 完整配置
await fastify.register(cors, {
origin: true, // 开发环境允许所有来源
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'], // 明确允许的HTTP方法
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin'], // 允许的请求头
exposedHeaders: ['Content-Range', 'X-Content-Range'], // 暴露的响应头
maxAge: 600, // preflight请求缓存时间
preflightContinue: false, // Fastify处理preflight请求
});
console.log('✅ CORS已配置: 允许所有HTTP方法 (GET, POST, PUT, DELETE, PATCH, OPTIONS)');
// 注册文件上传插件
await fastify.register(multipart, {
limits: {
fileSize: 50 * 1024 * 1024, // 50MB与前端提示一致
},
});
console.log('✅ 文件上传插件已配置: 最大文件大小 10MB');
// ============================================
// 【平台基础设施】健康检查路由
// ============================================
// 注册健康检查路由(/health, /health/liveness, /health/readiness
await registerHealthRoutes(fastify);
logger.info('✅ 健康检查路由已注册');
// ============================================
// 【公开API】无需认证的公共接口
// ============================================
import { getTenantLoginConfig } from './modules/admin/controllers/tenantController.js';
fastify.get('/api/v1/public/tenant-config/:tenantCode', getTenantLoginConfig);
logger.info('✅ 公开API已注册: /api/v1/public/tenant-config/:tenantCode');
// ============================================
// 【平台基础设施】认证模块
// ============================================
await registerAuthPlugin(fastify);
await fastify.register(authRoutes, { prefix: '/api/v1/auth' });
logger.info('✅ 认证路由已注册: /api/v1/auth');
// ============================================
// 【运营管理】Prompt管理模块
// ============================================
await fastify.register(promptRoutes, { prefix: '/api/admin/prompts' });
logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
// ============================================
// 【运营管理】租户管理模块
// ============================================
import { tenantRoutes, moduleRoutes } from './modules/admin/routes/tenantRoutes.js';
import { userRoutes } from './modules/admin/routes/userRoutes.js';
import { statsRoutes, userOverviewRoute } from './modules/admin/routes/statsRoutes.js';
import { systemKbRoutes } from './modules/admin/system-kb/index.js';
import { iitProjectRoutes, iitQcRuleRoutes, iitUserMappingRoutes, iitBatchRoutes, iitQcCockpitRoutes, iitEqueryRoutes } from './modules/admin/iit-projects/index.js';
import { authenticate, requireRoles } from './common/auth/auth.middleware.js';
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
await fastify.register(userRoutes, { prefix: '/api/admin/users' });
await fastify.register(statsRoutes, { prefix: '/api/admin/stats' });
await fastify.register(userOverviewRoute, { prefix: '/api/admin/users' });
await fastify.register(systemKbRoutes, { prefix: '/api/v1/admin/system-kb' });
// IIT 项目管理路由 — 认证 + 角色守卫SUPER_ADMIN, PROMPT_ENGINEER, IIT_OPERATOR, PHARMA_ADMIN, HOSPITAL_ADMIN 可访问)
await fastify.register(async (scope) => {
scope.addHook('preHandler', authenticate);
scope.addHook('preHandler', requireRoles('SUPER_ADMIN', 'PROMPT_ENGINEER', 'IIT_OPERATOR', 'PHARMA_ADMIN', 'HOSPITAL_ADMIN'));
await scope.register(iitProjectRoutes);
await scope.register(iitQcRuleRoutes);
await scope.register(iitUserMappingRoutes);
await scope.register(iitBatchRoutes);
await scope.register(iitQcCockpitRoutes);
await scope.register(iitEqueryRoutes);
}, { prefix: '/api/v1/admin/iit-projects' });
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users, /api/admin/stats, /api/v1/admin/system-kb, /api/v1/admin/iit-projects (authenticated)');
// ============================================
// 【临时】平台基础设施测试API
// ============================================
await registerTestRoutes(fastify);
logger.info('✅ 测试API已注册: /test/platform');
// API路由前缀
fastify.get('/api/v1', async () => {
return {
message: 'AI Clinical Research Platform API',
version: '1.0.0',
environment: config.nodeEnv,
};
});
// 注册项目管理路由
await fastify.register(projectRoutes, { prefix: '/api/v1' });
// 注册智能体管理路由
await fastify.register(agentRoutes, { prefix: '/api/v1' });
// 注册对话管理路由
await fastify.register(conversationRoutes, { prefix: '/api/v1' });
// 注册知识库管理路由
await fastify.register(knowledgeBaseRoutes, { prefix: '/api/v1' });
// 注册通用对话路由
await fastify.register(chatRoutes, { prefix: '/api/v1' });
// Phase 3: 注册批处理路由
await fastify.register(batchRoutes, { prefix: '/api/v1' });
// 注册稿件审查路由(旧版,保留兼容)
await fastify.register(reviewRoutes, { prefix: '/api/v1' });
// ============================================
// 【业务模块】RVW - 稿件审查系统
// ============================================
await fastify.register(rvwRoutes, { prefix: '/api/v1/rvw' });
logger.info('✅ RVW稿件审查路由已注册: /api/v1/rvw');
// ============================================
// 【业务模块】PKB - 个人知识库
// ============================================
await fastify.register(pkbRoutes, { prefix: '/api/v1/pkb' });
logger.info('✅ PKB个人知识库路由已注册: /api/v1/pkb');
// ============================================
// 【业务模块】AIA - AI智能问答
// ============================================
await fastify.register(aiaRoutes, { prefix: '/api/v1/aia' });
logger.info('✅ AIA智能问答路由已注册: /api/v1/aia');
// ============================================
// 【业务模块】Protocol Agent - 研究方案制定Agent
// ============================================
import { protocolAgentRoutes } from './modules/agent/protocol/index.js';
await fastify.register((instance, opts, done) => {
protocolAgentRoutes(instance, { prisma, ...opts }).then(() => done()).catch(done);
}, { prefix: '/api/v1/aia/protocol-agent' });
logger.info('✅ Protocol Agent路由已注册: /api/v1/aia/protocol-agent');
// ============================================
// 【业务模块】ASL - AI智能文献筛选
// ============================================
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });
logger.info('✅ ASL智能文献筛选路由已注册: /api/v1/asl');
// ============================================
// 【业务模块】DC - 数据清洗整理
// ============================================
await registerDCRoutes(fastify);
logger.info('✅ DC数据清洗模块路由已注册: /api/v1/dc/tool-b');
// ============================================
// 【业务模块】IIT Manager Agent - IIT研究智能助手
// ============================================
import { registerIitRoutes, initIitManager } from './modules/iit-manager/index.js';
await registerIitRoutes(fastify);
logger.info('✅ IIT Manager Agent路由已注册: /api/v1/iit');
// ============================================
// 【业务模块】SSA - 智能统计分析
// ============================================
import { ssaRoutes } from './modules/ssa/index.js';
await fastify.register(ssaRoutes, { prefix: '/api/v1/ssa' });
logger.info('✅ SSA智能统计分析路由已注册: /api/v1/ssa');
// ============================================
// 【集成模块】Legacy Bridge - 旧系统集成桥接
// ============================================
import { legacyBridgeRoutes } from './modules/legacy-bridge/routes.js';
await fastify.register(legacyBridgeRoutes, { prefix: '/api/v1/legacy' });
logger.info('✅ Legacy Bridge路由已注册: /api/v1/legacy');
// 启动服务器
const start = async () => {
try {
// 验证环境变量
validateEnv();
// 测试数据库连接
console.log('🔍 正在测试数据库连接...');
const dbConnected = await testDatabaseConnection();
if (!dbConnected) {
console.error('❌ 数据库连接失败,无法启动服务器');
process.exit(1);
}
// ============================================
// 【Postgres-Only】启动队列和注册Workers
// ============================================
try {
logger.info('🚀 Starting Postgres-Only queue and workers...');
// 启动队列pg-boss
await jobQueue.start();
logger.info('✅ Queue started (pg-boss)');
// 注册ASL筛选Workers
registerScreeningWorkers();
logger.info('✅ ASL screening workers registered');
// 注册ASL智能文献检索Worker
registerResearchWorker();
logger.info('✅ ASL research worker registered');
// 注册DC提取Workers
registerExtractionWorkers();
logger.info('✅ DC extraction workers registered');
// 注册DC Tool C Excel解析Worker
registerParseExcelWorker();
logger.info('✅ DC Tool C parse excel worker registered');
// 注册ASL工具3全文提取Workers散装派发 + Aggregator
await registerAslExtractionWorkers();
logger.info('✅ ASL extraction workers registered (Tool 3)');
// 注册RVW审稿Worker包含启动时清理卡住任务
await registerReviewWorker();
logger.info('✅ RVW review worker registered');
// 注册IIT Manager Workers
await initIitManager();
logger.info('✅ IIT Manager workers registered');
// ⚠️ 等待3秒确保所有 Worker 异步注册到 pg-boss 完成
console.log('\n⏳ 等待 Workers 异步注册完成...');
await new Promise(resolve => setTimeout(resolve, 3000));
logger.info('✅ All workers registration completed (waited 3s)');
console.log('\n' + '='.repeat(60));
console.log('✅ Postgres-Only 架构已启动');
console.log('='.repeat(60));
console.log('📦 队列类型: pg-boss');
console.log('📦 缓存类型: PostgreSQL');
console.log('📦 注册的Workers:');
console.log(' - asl_screening_batch (文献筛选批次处理)');
console.log(' - dc_extraction_batch (数据提取批次处理)');
console.log(' - dc_toolc_parse_excel (Tool C Excel解析)');
console.log(' - rvw_review_task (稿件审查任务)');
console.log(' - iit_quality_check (IIT质控+企微推送)');
console.log(' - iit_redcap_poll (IIT REDCap轮询)');
console.log('='.repeat(60) + '\n');
} catch (error) {
logger.error('❌ Failed to start Postgres-Only architecture', { error });
console.error('❌ Postgres-Only 架构启动失败:', error);
process.exit(1);
}
// 初始化DC模块Seed预设模板
try {
await initDCModule();
logger.info('✅ DC模块初始化成功');
} catch (error) {
logger.warn('⚠️ DC模块初始化失败但不影响启动', { error });
}
// 启动Fastify服务器
await fastify.listen({
port: config.port,
host: config.host,
});
console.log('\n' + '='.repeat(60));
console.log('🚀 AI临床研究平台 - 后端服务器启动成功!');
console.log('='.repeat(60));
console.log(`📍 服务地址: http://${config.host === '0.0.0.0' ? 'localhost' : config.host}:${config.port}`);
console.log(`🔍 健康检查: http://localhost:${config.port}/health`);
console.log(`📡 API入口: http://localhost:${config.port}/api/v1`);
console.log(`🌍 运行环境: ${config.nodeEnv}`);
console.log('='.repeat(60) + '\n');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
// ============================================
// 🛡️ 优雅关闭处理Graceful Shutdown
// ============================================
const SHUTDOWN_TIMEOUT_MS = 30_000;
const gracefulShutdown = async (signal: string) => {
console.log(`\n⚠ 收到 ${signal} 信号,开始优雅关闭...`);
// 立即标记停机,健康检查返回 503CLB 不再派发新请求
markShuttingDown();
console.log('🚫 健康检查已切换为 503CLB 将停止路由新流量');
// 强制超时兜底:防止 SSE 长连接或死循环任务阻塞退出
const forceTimer = setTimeout(() => {
console.error(`❌ 优雅关闭超时 (${SHUTDOWN_TIMEOUT_MS / 1000}s),强制退出`);
process.exit(1);
}, SHUTDOWN_TIMEOUT_MS);
forceTimer.unref();
try {
// 1. 停止接收新请求(已有 SSE 连接继续跑完)
await fastify.close();
console.log('✅ HTTP 服务已停止');
// 2. 停止队列(等待当前任务完成)
await jobQueue.stop();
console.log('✅ 任务队列已停止');
// 3. 关闭数据库连接
await prisma.$disconnect();
console.log('✅ 数据库连接已关闭');
// 4. 关闭 Legacy MySQL 连接池
const { closeLegacyMysqlPool } = await import('./modules/legacy-bridge/mysql-pool.js');
await closeLegacyMysqlPool();
console.log('✅ Legacy MySQL连接池已关闭');
console.log('👋 优雅关闭完成,再见!');
process.exit(0);
} catch (error) {
console.error('❌ 优雅关闭失败:', error);
process.exit(1);
}
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));