/** * Protocol Export Service * 处理研究方案的生成和导出 * * @module agent/protocol/services/ProtocolExportService */ import axios from 'axios'; import { PrismaClient } from '@prisma/client'; import { logger } from '../../../../common/logging/index.js'; // Python 微服务地址 const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000'; // 方案章节配置 const PROTOCOL_SECTIONS = [ { key: 'title', name: '研究题目' }, { key: 'background', name: '研究背景与立题依据' }, { key: 'objectives', name: '研究目的' }, { key: 'design', name: '研究设计' }, { key: 'subjects', name: '研究对象(纳入/排除标准)' }, { key: 'sample_size', name: '样本量估算' }, { key: 'implementation', name: '研究实施步骤与技术路线' }, { key: 'endpoints', name: '观察指标' }, { key: 'data_management', name: '数据管理与质量控制' }, { key: 'safety', name: '安全性评价' }, { key: 'statistics', name: '统计分析计划' }, { key: 'ethics', name: '伦理与知情同意' }, { key: 'timeline', name: '研究时间表' }, { key: 'references', name: '参考文献' }, ]; interface ContextData { scientificQuestion?: { content?: string; summary?: string; original?: string; }; pico?: { population?: string; intervention?: string; comparison?: string; outcome?: string; }; studyDesign?: { studyType?: string; design?: string[]; }; sampleSize?: { sampleSize?: number; calculation?: { alpha?: number; power?: number; effectSize?: string; }; }; endpoints?: { outcomes?: { primary?: string[]; secondary?: string[]; safety?: string[]; }; confounders?: string[]; }; } export class ProtocolExportService { private prisma: PrismaClient; constructor(prisma: PrismaClient) { this.prisma = prisma; } /** * 检查 Pandoc 服务可用性 */ async checkPandocStatus(): Promise<{ available: boolean; version: string | null; message: string; }> { try { const response = await axios.get(`${EXTRACTION_SERVICE_URL}/api/pandoc/status`, { timeout: 5000, }); return response.data; } catch (error) { logger.error('[ProtocolExportService] Pandoc 状态检查失败:', error); return { available: false, version: null, message: `无法连接到文档服务: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * 根据上下文数据生成 Markdown 格式的研究方案 */ generateProtocolMarkdown(context: ContextData, title?: string): string { const parts: string[] = []; // 标题 const protocolTitle = title || this.generateTitle(context); parts.push(`# 临床研究方案\n\n`); // 1. 研究题目 parts.push(`## 1. 研究题目\n\n${protocolTitle}\n\n`); // 2. 研究背景(占位,可由 LLM 生成) parts.push(`## 2. 研究背景与立题依据\n\n`); parts.push(`(待 LLM 根据科学问题生成)\n\n`); // 3. 研究目的 parts.push(`## 3. 研究目的\n\n`); if (context.scientificQuestion?.content) { parts.push(`**主要目的**:${context.scientificQuestion.content}\n\n`); } // 4. 研究设计 parts.push(`## 4. 研究设计\n\n`); if (context.studyDesign) { if (context.studyDesign.studyType) { parts.push(`**研究类型**:${context.studyDesign.studyType}\n\n`); } if (context.studyDesign.design && context.studyDesign.design.length > 0) { parts.push(`**设计特征**:\n`); context.studyDesign.design.forEach(d => { parts.push(`- ${d}\n`); }); parts.push('\n'); } } // 5. 研究对象 parts.push(`## 5. 研究对象(纳入/排除标准)\n\n`); if (context.pico?.population) { parts.push(`**目标人群**:${context.pico.population}\n\n`); } parts.push(`### 纳入标准\n\n(待补充)\n\n`); parts.push(`### 排除标准\n\n(待补充)\n\n`); // 6. 样本量估算 parts.push(`## 6. 样本量估算\n\n`); if (context.sampleSize) { if (context.sampleSize.sampleSize) { parts.push(`**计划样本量**:${context.sampleSize.sampleSize} 例\n\n`); } if (context.sampleSize.calculation) { const calc = context.sampleSize.calculation; parts.push(`**计算依据**:\n`); if (calc.alpha) parts.push(`- α = ${calc.alpha}\n`); if (calc.power) parts.push(`- 1-β = ${calc.power}\n`); if (calc.effectSize) parts.push(`- 效应量 = ${calc.effectSize}\n`); parts.push('\n'); } } // 7. 研究实施步骤 parts.push(`## 7. 研究实施步骤与技术路线\n\n`); if (context.pico) { if (context.pico.intervention) { parts.push(`**干预措施**:${context.pico.intervention}\n\n`); } if (context.pico.comparison) { parts.push(`**对照措施**:${context.pico.comparison}\n\n`); } } parts.push(`(技术路线图待补充)\n\n`); // 8. 观察指标 parts.push(`## 8. 观察指标\n\n`); if (context.endpoints?.outcomes) { const outcomes = context.endpoints.outcomes; if (outcomes.primary && outcomes.primary.length > 0) { parts.push(`### 主要结局指标\n\n`); outcomes.primary.forEach(o => parts.push(`- ${o}\n`)); parts.push('\n'); } if (outcomes.secondary && outcomes.secondary.length > 0) { parts.push(`### 次要结局指标\n\n`); outcomes.secondary.forEach(o => parts.push(`- ${o}\n`)); parts.push('\n'); } if (outcomes.safety && outcomes.safety.length > 0) { parts.push(`### 安全性指标\n\n`); outcomes.safety.forEach(o => parts.push(`- ${o}\n`)); parts.push('\n'); } } if (context.endpoints?.confounders && context.endpoints.confounders.length > 0) { parts.push(`### 潜在混杂因素\n\n`); context.endpoints.confounders.forEach(c => parts.push(`- ${c}\n`)); parts.push('\n'); } // 9-14. 其他章节(占位) parts.push(`## 9. 数据管理与质量控制\n\n(待补充)\n\n`); parts.push(`## 10. 安全性评价\n\n(待补充)\n\n`); parts.push(`## 11. 统计分析计划\n\n(待补充)\n\n`); parts.push(`## 12. 伦理与知情同意\n\n本研究将遵循赫尔辛基宣言的伦理原则,并提交机构伦理委员会审批。\n\n`); parts.push(`## 13. 研究时间表\n\n(待补充)\n\n`); parts.push(`## 14. 参考文献\n\n(待补充)\n\n`); return parts.join(''); } /** * 根据上下文生成研究题目 */ private generateTitle(context: ContextData): string { const parts: string[] = []; // PICO 要素 if (context.pico) { if (context.pico.population) parts.push(context.pico.population); if (context.pico.intervention) parts.push(`使用${context.pico.intervention}`); if (context.pico.comparison) parts.push(`与${context.pico.comparison}对比`); if (context.pico.outcome) parts.push(`对${context.pico.outcome}影响`); } if (parts.length > 0) { // 研究类型后缀 if (context.studyDesign?.studyType) { parts.push(`的${context.studyDesign.studyType}`); } else { parts.push('的临床研究'); } return parts.join(''); } // 使用科学问题作为标题 if (context.scientificQuestion?.content) { return context.scientificQuestion.content; } return '临床研究方案'; } /** * 将 Markdown 转换为 Word 文档 */ async convertToDocx(markdown: string): Promise { try { logger.info('[ProtocolExportService] 开始转换 Markdown → Word'); const response = await axios.post( `${EXTRACTION_SERVICE_URL}/api/convert/docx`, { content: markdown, use_template: true, title: '临床研究方案', }, { responseType: 'arraybuffer', timeout: 30000, // 30秒超时 } ); logger.info(`[ProtocolExportService] Word 转换成功, 大小: ${response.data.length} bytes`); return Buffer.from(response.data); } catch (error) { logger.error('[ProtocolExportService] Word 转换失败:', error); throw new Error(`Word 转换失败: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * 导出研究方案为 Word */ async exportProtocol( conversationId: string, context: ContextData, title?: string ): Promise<{ buffer: Buffer; filename: string; contentType: string; }> { // 1. 生成 Markdown const markdown = this.generateProtocolMarkdown(context, title); // 2. 转换为 Word const buffer = await this.convertToDocx(markdown); // 3. 生成文件名 const timestamp = new Date().toISOString().slice(0, 10); const filename = `研究方案_${timestamp}.docx`; return { buffer, filename, contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }; } }