import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectModel } from '@nestjs/sequelize'; import { OpenAI } from 'openai'; import { Readable } from 'stream'; import { MedicationsService } from '../medications.service'; import { Medication } from '../models/medication.model'; /** * 药品AI分析服务 * 使用 GLM-4.5V 大模型分析药品信息,提供专业的用药指导和健康建议 */ @Injectable() export class MedicationAnalysisService { private readonly logger = new Logger(MedicationAnalysisService.name); private readonly client: OpenAI; private readonly visionModel: string; private readonly model: string; constructor( private readonly configService: ConfigService, private readonly medicationsService: MedicationsService, @InjectModel(Medication) private readonly medicationModel: typeof Medication, ) { // GLM-4.5V Configuration const glmApiKey = this.configService.get('GLM_API_KEY'); const glmBaseURL = this.configService.get('GLM_BASE_URL') || 'https://open.bigmodel.cn/api/paas/v4'; this.client = new OpenAI({ apiKey: glmApiKey, baseURL: glmBaseURL, }); this.model = this.configService.get('GLM_MODEL') || 'glm-4-flash'; this.visionModel = this.configService.get('GLM_VISION_MODEL') || 'glm-4v-plus'; } /** * 分析药品信息并返回流式响应 * @param medicationId 药品ID * @param userId 用户ID * @returns 流式文本响应 */ async analyzeMedication(medicationId: string, userId: string): Promise { try { // 1. 获取药品信息 const medication = await this.medicationsService.findOne(medicationId, userId); // 2. 构建专业医药分析提示 const prompt = this.buildMedicationAnalysisPrompt(medication); // 3. 调用AI模型进行分析 if (medication.photoUrl) { // 有图片:使用视觉模型 return await this.analyzeWithVision(prompt, medication.photoUrl, medicationId, userId); } else { // 无图片:使用文本模型 return await this.analyzeWithText(prompt, medicationId, userId); } } catch (error) { this.logger.error(`药品分析失败: ${error instanceof Error ? error.message : String(error)}`); return this.createErrorStream('药品分析失败,请稍后重试。'); } } /** * 使用视觉模型分析药品(带图片) * @param prompt 分析提示 * @param imageUrl 药品图片URL * @param medicationId 药品ID * @param userId 用户ID * @returns 流式响应 */ private async analyzeWithVision(prompt: string, imageUrl: string, medicationId: string, userId: string): Promise { try { const stream = await this.client.chat.completions.create({ model: this.visionModel, temperature: 0.7, stream: true, messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', image_url: { url: imageUrl } } as any, ] as any, }, ], } as any); return this.createStreamFromAI(stream, medicationId, userId); } catch (error) { this.logger.error(`视觉模型调用失败: ${error instanceof Error ? error.message : String(error)}`); return this.createErrorStream('视觉分析失败,请稍后重试。'); } } /** * 使用文本模型分析药品(无图片) * @param prompt 分析提示 * @param medicationId 药品ID * @param userId 用户ID * @returns 流式响应 */ private async analyzeWithText(prompt: string, medicationId: string, userId: string): Promise { try { const stream = await this.client.chat.completions.create({ model: this.model, messages: [ { role: 'user', content: prompt } ], temperature: 0.7, stream: true, }); return this.createStreamFromAI(stream, medicationId, userId); } catch (error) { this.logger.error(`文本模型调用失败: ${error instanceof Error ? error.message : String(error)}`); return this.createErrorStream('文本分析失败,请稍后重试。'); } } /** * 从AI响应创建可读流 * @param aiStream AI模型流式响应 * @param medicationId 药品ID(用于保存分析结果) * @param userId 用户ID * @returns Readable stream */ private createStreamFromAI(aiStream: any, medicationId?: string, userId?: string): Readable { const readable = new Readable({ read() { } }); let fullContent = ''; // 收集完整的AI响应内容 (async () => { try { for await (const chunk of aiStream) { const delta = chunk.choices?.[0]?.delta?.content || ''; if (delta) { fullContent += delta; // 累积内容 readable.push(delta); } } // 流结束后保存完整的分析结果到数据库 if (medicationId && userId && fullContent) { await this.saveAnalysisResult(medicationId, userId, fullContent); } } catch (error) { this.logger.error(`流式响应错误: ${error instanceof Error ? error.message : String(error)}`); readable.push('\n\n[分析过程中发生错误,请稍后重试]'); } finally { readable.push(null); } })(); return readable; } /** * 保存AI分析结果到数据库 * @param medicationId 药品ID * @param userId 用户ID * @param analysisResult 分析结果 */ private async saveAnalysisResult(medicationId: string, userId: string, analysisResult: string): Promise { try { // 直接更新数据库,不通过 DTO await this.medicationModel.update( { aiAnalysis: analysisResult }, { where: { id: medicationId, userId: userId, deleted: false } } ); this.logger.log(`药品 ${medicationId} 的AI分析结果已保存到数据库`); } catch (error) { this.logger.error(`保存AI分析结果失败: ${error instanceof Error ? error.message : String(error)}`); // 不抛出错误,避免影响流式响应 } } /** * 创建错误流 * @param errorMessage 错误信息 * @returns Readable stream */ private createErrorStream(errorMessage: string): Readable { const readable = new Readable({ read() { } }); setTimeout(() => { readable.push(errorMessage); readable.push(null); }, 100); return readable; } /** * 构建专业医药分析提示 * @param medication 药品信息 * @returns 分析提示文本 */ private buildMedicationAnalysisPrompt(medication: Medication): string { const formName = this.getMedicationFormName(medication.form); const dosageInfo = `${medication.dosageValue}${medication.dosageUnit}`; return `你是一位拥有20年从业经验的资深药剂师和临床医学专家,同时也是一名充满关怀的健康顾问。 你的专业背景包括: - 药理学与临床药学 - 用药安全与药物相互作用 - 患者用药教育与健康管理 - 慢性病用药指导 - 中西医结合用药 请基于以下药品信息,为用户提供专业、详细、易懂的药品分析报告。 **药品信息**: - 药品名称:${medication.name} - 剂型:${formName} - 规格剂量:${dosageInfo} - 每日服用次数:${medication.timesPerDay}次 - 服药时间:${medication.medicationTimes.join('、')} ${medication.photoUrl ? '- 药品图片:已提供(请结合图片中的药品外观、包装、说明书等信息进行分析)' : ''} ${medication.note ? `- 用户备注:${medication.note}` : ''} **关键分析原则**: ⚠️ **药品识别度判断**: - 首先判断提供的药品名称是否是正规的、可识别的药品(包括通用名、商品名、中药名等) - 如果药品名称模糊、不规范、无法识别,或者明显是随意输入的内容(如"感冒药"、"止痛药"、"消炎药"、"xx片"等过于笼统的名称) - 如果有图片但图片信息不足以确认具体药品,结合药品名称综合判断 - 注意:没有图片不影响分析,只要药品名称明确即可 - 在无法准确识别药品的情况下,**不要进行药品分析**,而是友好地引导用户提供更多信息 **分析要求**: 1. **药品识别优先**:先判断是否能准确识别药品,无法识别时不要随意推测或给出建议 2. 使用温暖、专业、通俗易懂的语言 3. 以患者的健康和安全为首要考虑 4. 提供实用的用药指导和生活建议 5. 强调重要的注意事项和禁忌 6. 给予健康关怀和鼓励 7. 如果有图片,请结合图片信息提供更准确的分析 **输出格式要求**: **情况A:无法识别药品时**(药品名称不明确、过于笼统、随意输入、或缺少必要信息),请使用以下格式: ## 🤔 需要更多信息 很抱歉,根据您提供的信息,我无法准确识别这个药品。为了给您提供安全、专业的用药指导,我需要更多详细信息。 **当前信息不足的原因**: [说明为什么无法识别,例如: - 药品名称过于笼统(如"感冒药"包含多种不同成分的药物) - 药品名称不规范或无法在药品数据库中找到对应信息 - 药品名称疑似随意输入,无法对应到具体药品 - 提供的图片信息不足以确认具体药品(如有图片的话)] ## 💡 建议您这样做 为了给您提供安全、准确的用药指导,请选择以下任一方式: ### 方式一:补充药品完整名称 📝 在【备注】中添加药品的完整名称,例如: - "阿莫西林胶囊" - "布洛芬缓释胶囊" - "999感冒灵颗粒" 💡 **小贴士**:可以从药盒或说明书上找到完整的药品名称 ### 方式二:上传药品图片 📸 拍摄清晰的照片: - 药品外包装(带有药品名称的一面) - 或药品说明书 图片能帮助我更准确地识别和分析药品信息。 --- 补充信息后重新分析,我将为您提供专业的用药指导!💚 --- **情况B:能够识别药品时**,请严格按照以下Markdown结构输出(使用纯文本格式): ## 💊 药品基本信息 [简要说明药品的通用名称、主要成分、剂型等基本信息。如果有图片,描述药品外观特征] ## 🎯 主要用途与适应症 [详细说明药品的适应症和治疗目的,让患者了解为什么要服用这个药] ## 📋 用法用量指导 [根据药品信息给出标准用法用量指导,包括: - 推荐服用时间(饭前/饭后/空腹等) - 服用方法(吞服/咀嚼/含服等) - 是否需要用水送服 - 特殊注意事项] ## ⚠️ 重要注意事项 [列出关键的注意事项,包括: - 禁忌症(哪些人不能用) - 特殊人群用药注意(孕妇、哺乳期、儿童、老年人等) - 可能的药物相互作用 - 用药期间需要避免的食物或行为] ## 🌡️ 可能的副作用 [说明常见和严重的副作用: - 常见副作用及应对方法 - 需要立即就医的严重反应 - 如何减轻副作用的建议] ## 🏠 储存与保管 [正确的储存方法: - 储存温度和环境要求 - 有效期提醒 - 儿童接触预防] ## 💚 健康关怀建议 [个性化的健康建议: - 配合药物治疗的生活方式建议 - 饮食营养建议 - 运动和作息建议 - 心理调适建议] ## ⏰ 用药依从性提醒 [帮助患者坚持用药的实用技巧: - 如何记住服药时间 - 漏服后的处理方法 - 定期复查的重要性 - 与医生沟通的建议] --- **⚠️ 重要提醒**: 本分析基于药品的一般信息提供参考,不能替代专业医疗建议。每个人的情况不同,请: - 严格遵医嘱服药 - 如有疑问或不适,及时咨询医生或药师 - 定期复查,根据病情调整用药 - 不要自行增减剂量或停药 祝您早日康复,保持健康!💪`; } /** * 获取药品剂型的中文名称 * @param form 剂型枚举 * @returns 中文名称 */ private getMedicationFormName(form: string): string { const formNames: Record = { 'tablet': '片剂', 'capsule': '胶囊', 'syrup': '糖浆', 'injection': '注射剂', 'ointment': '软膏', 'drops': '滴剂', 'powder': '散剂', 'granules': '颗粒剂', 'other': '其他' }; return formNames[form] || form; } }