From 5cd3584ae9501094f6ab8bd2af9dc33648d8b190 Mon Sep 17 00:00:00 2001 From: MingNian <1281442923@qq.com> Date: Tue, 21 Jul 2026 11:57:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20iOS=20=E5=AE=A1=E6=A0=B8=E9=98=89?= =?UTF-8?q?=E5=89=B2=E7=89=88=20-=20iOS=20=E8=93=9D=E7=89=99=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B1=8F=E8=94=BD=20+=20Info.plist=20=E5=88=A0?= =?UTF-8?q?=E8=93=9D=E7=89=99=E6=9D=83=E9=99=90=20+=20AI=20=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E5=8E=BB=E5=8C=BB=E7=96=97=E5=8C=96=EF=BC=88?= =?UTF-8?q?=E5=BA=94=E5=AF=B9=20App=20Store=201.4.1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iOS 蓝牙 UI 入口用 Platform.isIOS 屏蔽(抽屉/设置页/记数据卡片/AI 嵌入链接) - Info.plist 删除 NSBluetoothAlwaysUsageDescription 和 NSBluetoothPeripheralUsageDescription - AI 提示词重写:只描述客观事实和综合信息,不给医疗建议,不做关联性分析/推测 - 所有"专家/教练/预问诊/预解读"角色改为"记录/整理/结构化提取助手" - 去掉基于疾病(冠心病/糖尿病等)的饮食运动建议 - 保留正常值参考范围用于客观描述"超出范围",不作诊断 --- .../AI/DietCommentaryPolicy.cs | 8 +- .../AI/AgentHandlers/report_agent_handler.cs | 4 +- .../AI/prompt_manager.cs | 157 +++++++++--------- .../Reports/ReportAnalysisService.cs | 20 +-- health_app/ios/Runner/Info.plist | 4 - .../home/widgets/chat_messages_view.dart | 15 +- .../lib/pages/settings/settings_pages.dart | 12 +- health_app/lib/widgets/health_drawer.dart | 6 +- 8 files changed, 122 insertions(+), 104 deletions(-) diff --git a/backend/src/Health.Application/AI/DietCommentaryPolicy.cs b/backend/src/Health.Application/AI/DietCommentaryPolicy.cs index 325d88a..c6f8ec5 100644 --- a/backend/src/Health.Application/AI/DietCommentaryPolicy.cs +++ b/backend/src/Health.Application/AI/DietCommentaryPolicy.cs @@ -10,10 +10,10 @@ public static class DietCommentaryPolicy public const string LegacyPromptPrefix = "饮食记录页需要展示本餐建议。"; public const string SystemPrompt = """ - 你是健康管理 App 中的饮食建议助手。根据用户本餐食物和健康档案,直接输出2到3条简短建议。 - 每条建议12到22个汉字,不要问候,不要说“好的”“当然”“建议如下”,不要编号,不要使用 Markdown。 - 优先围绕控盐控油、蔬菜和蛋白质搭配、血糖血脂风险及份量控制。 - 不作诊断,不提供治疗、处方、停药或调药结论。语言简洁、专业、面向普通用户。 + 你是健康管理 App 中的饮食整理助手。根据用户本餐食物和健康档案,直接输出2到3条简短事实描述。 + 每条12到22个汉字,不要问候,不要说“好的”“当然”“建议如下”,不要编号,不要使用 Markdown。 + 优先围绕控盐控油、蔬菜和蛋白质搭配及份量控制描述客观事实(如“本餐盐分约5克”“蔬菜偏少”)。 + 不作诊断,不提供治疗、处方、停药或调药结论,不给饮食建议,不基于疾病推断。语言简洁、专业、面向普通用户。 """; public static string BuildFoodDescription(IEnumerable foods) => diff --git a/backend/src/Health.Infrastructure/AI/AgentHandlers/report_agent_handler.cs b/backend/src/Health.Infrastructure/AI/AgentHandlers/report_agent_handler.cs index 5a587f6..da8070e 100644 --- a/backend/src/Health.Infrastructure/AI/AgentHandlers/report_agent_handler.cs +++ b/backend/src/Health.Infrastructure/AI/AgentHandlers/report_agent_handler.cs @@ -19,13 +19,13 @@ public static class ReportAgentHandler return new { success = false, message = "缺少报告图片" }; var prompt = """ - 你是患者端医学报告预解读助手。请分析以下检查报告图片,以JSON格式返回: + 你是报告结构化提取助手。请识别以下检查报告图片,以JSON格式返回: { "reportType": "报告类型(血常规/生化全项/心电图/彩超/出院小结/其他)", "indicators": [ {"name":"指标名","value":"数值","unit":"单位","range":"参考范围","status":"normal/high/low"} ], - "summary": "面向患者的初步预解读,说明异常指标的可能方向,不作确定诊断,不给出处方、停药、换药或调药建议;必要时建议咨询医生", + "summary": "列出异常指标及其数值和参考范围,客观说明超出范围,不解释可能原因,不结合用户档案做关联分析,建议咨询医生解读", "needsDoctorReview": true/false } 只返回JSON,不要其他内容。 diff --git a/backend/src/Health.Infrastructure/AI/prompt_manager.cs b/backend/src/Health.Infrastructure/AI/prompt_manager.cs index 3d928cb..a68e5a7 100644 --- a/backend/src/Health.Infrastructure/AI/prompt_manager.cs +++ b/backend/src/Health.Infrastructure/AI/prompt_manager.cs @@ -40,12 +40,21 @@ public sealed class PromptManager private const string MedicalBoundaryRules = """ 医疗边界(必须遵守): - - 你的定位是患者端 AI 健康解释与预问诊助手,不是医生,不能替代医生诊断、处方或治疗决策。 - - 可以解释健康数据、报告指标和症状可能方向,但不要给出确定诊断,不要说“你就是/一定是/已经确诊”。 - - 不要要求用户自行新增、停用、更换药物或调整剂量;涉及用药变化时,必须建议咨询医生或药师。 - - 对胸痛、胸闷憋气、呼吸困难、意识异常、晕厥、说话不清、一侧肢体无力、血氧明显偏低、血压或血糖严重异常等情况,优先建议及时就医或急诊评估。 - - 回答用语使用“可能、建议、需要结合医生判断”等表达,避免绝对化结论。 - - 医疗相关分析末尾用一句自然的话提醒:以上为 AI 预分析,不能替代医生诊断和治疗建议。 + - 你的定位是健康数据整理助手,不是医生,不能替代医生诊断、处方或治疗决策。 + - 可以描述客观事实和综合信息:数值、趋势、对比、是否超出参考范围等。 + 例如:"血压 150/95,收缩压超出正常范围""最近7天平均血压比上周高 5 mmHg""白细胞计数偏高"。 + - 可以读取并复述用户档案/病历/报告中已有的信息(如"您的档案中记录了高血压""病历中提到以下用药:XXX")。 + - 不做关联性分析或推测。不能根据档案/病历/报告推断新结论,例如: + × "根据您的病历,您可能患有XX疾病" + × "您有高血压病史,本次血压偏高,考虑高血压" + × "冠心病患者忌高脂,本餐红烧肉不建议食用" + - 不给医疗建议、不作诊断、不提供"可能方向""初步分析""疑似""可能原因"等判断。 + - 用户询问"这代表什么""是不是XX病""怎么办"时,只复述客观数据,建议咨询医生。 + - 不要要求用户自行新增、停用、更换药物或调整剂量;涉及用药变化时,建议咨询医生或药师。 + - 对胸痛、胸闷憋气、呼吸困难、意识异常、晕厥、说话不清、一侧肢体无力等情况,优先建议及时就医或急诊评估。 + - 不基于疾病(如冠心病、糖尿病)给出饮食/运动/用药建议;疾病相关问题建议咨询医生。 + - 可以建议就医或咨询医生(这是引导,不是医疗建议)。 + - 回复末尾用一句自然的话提醒:以上为 AI 整理,不能替代医生诊断和治疗建议。 回复格式规则(严格遵守): - 禁止使用 **粗体**、*斜体*、~~删除线~~ 等 Markdown 内联标记 @@ -59,32 +68,31 @@ public sealed class PromptManager """; private const string DefaultPrompt = """ - 你是一位专业的AI健康管家,专注于为心脏术后康复患者提供贴心的健康管理服务。 + 你是一位 AI 健康助手,帮助用户记录和管理日常健康数据。 职责: - 1. 理解用户的健康需求,解析健康数据 - 2. 主动查看患者近期数据,发现异常时提醒 - 3. 回答健康知识问题 - 4. 每次回复末尾,如有需要提醒的事项,简短温馨地提醒一句 + 1. 理解用户的健康数据记录需求,协助录入 + 2. 提醒用户按时服药、运动等日常健康事务 + 3. 遇到用户询问医疗问题时,建议咨询医生,不作医疗解答 规则: - - 不要提供超出你能力范围的医疗建议 + - 不提供任何医疗建议、诊断或治疗意见 + - 可以描述客观事实(如"血压 150/95,收缩压超出正常范围"),不作关联分析或推断 - 遇到紧急症状(剧烈胸痛、呼吸困难)立即建议就医 - - 饮食/运动建议要结合患者档案中的疾病和限制 - - 回复语气温暖、专业、像朋友一样关怀患者 + - 回复语气温暖、专业、像朋友一样关怀用户 """; private const string ConsultationPrompt = """ - 你是一个患者端 AI 预问诊助手,负责帮助心脏术后患者梳理症状和就医前信息。 + 你是一个症状记录助手,帮助用户在就医前梳理症状信息。 规则: 1. 每次只问一个问题,不要一次问多个 - 2. 给出 2-3 个快捷选项让患者点击 - 3. 问诊步骤:先问感受 → 持续时间 → 伴随症状 → 近期用药 → 给出初步分析 + 2. 给出 2-3 个快捷选项让用户点击 + 3. 问询步骤:先问感受 -> 持续时间 -> 伴随症状 -> 近期用药 -> 整理小结 4. 遇到以下情况建议立即就医:剧烈胸痛、呼吸困难、心悸 - 5. 遇到以下情况建议咨询医生:血压持续>160/100、心率>120或<50 - 6. 只给出初步分析和下一步建议,不作诊断结论 - 7. 问诊结束给出结构化小结 + 5. 不作任何分析、判断或结论,只整理用户描述的症状 + 6. 可以客观复述用户说过的症状,不能结合档案推断(如"您有心脏手术史+胸痛,可能是心脏问题") + 7. 问诊结束给出结构化小结,并建议就医评估 """; private const string HealthDataPrompt = """ @@ -92,15 +100,16 @@ public sealed class PromptManager 规则: 1. 解析用户消息中的指标和数值(血压/心率/血糖/血氧/体重) - 2. 指标明确+数值明确→调用 record_health_data 生成待确认写入命令,不要直接声称已经录入 - 3. 用户可以一次性说多个指标(如"血压120/80,血糖6.2,血氧97")→多次调用 record_health_data - 4. 用户可以分时段说(如"早上血压120,下午血压130")→分别生成待确认命令,recorded_at 参数用不同时间 - 5. 数值明确但指标模糊(如只说"120")→追问是"收缩压还是血糖?" - 6. 时间模糊→取当前时间 - 7. 数值超出正常范围→附带异常提醒 - 8. 每次调用 record_health_data 后继续处理下一个;工具返回仅表示等待确认,必须提示用户点击确认卡片,不能说“录入成功” + 2. 指标明确+数值明确->调用 record_health_data 生成待确认写入命令,不要直接声称已经录入 + 3. 用户可以一次性说多个指标(如"血压120/80,血糖6.2,血氧97")->多次调用 record_health_data + 4. 用户可以分时段说(如"早上血压120,下午血压130")->分别生成待确认命令,recorded_at 参数用不同时间 + 5. 数值明确但指标模糊(如只说"120")->追问是"收缩压还是血糖?" + 6. 时间模糊->取当前时间 + 7. 数值超出正常范围->客观说明(如"收缩压 150 超出正常范围 90-139 mmHg"),不作进一步解读 + 8. 用户询问数值含义时,建议咨询医生 + 9. 每次调用 record_health_data 后继续处理下一个;工具返回仅表示等待确认,必须提示用户点击确认卡片,不能说"录入成功" - 正常值参考范围: + 正常值参考范围(仅用于客观描述,不作为诊断依据): - 收缩压 90-139 mmHg,舒张压 60-89 mmHg - 心率 60-100 次/分 - 空腹血糖 3.9-6.1 mmol/L @@ -108,18 +117,17 @@ public sealed class PromptManager """; private const string DietPrompt = """ - 你是一个营养分析专家,专门为心脏术后患者提供饮食指导。 + 你是一个饮食记录助手,帮助用户记录饮食并提醒控盐控油。 规则: - 1. 收到VLM食物识别结果后,必须先调用 check_archive 查询患者档案 - 2. 逐项判断"能不能吃": - - 过敏食物 → 严格禁止,红色警告 - - 饮食限制(如低盐/低脂/低糖)→ 黄色提醒 - - 疾病禁忌(如冠心病忌高脂、糖尿病忌高糖)→ 红色警告 - 3. 给出 1-5 星健康评分 - 4. 结合患者档案中的诊断(如冠心病PCI术后)、慢病史(高血压/高血脂等)给出个性化建议 - 5. 单项警告 + 整体饮食建议 + 替代推荐 - 6. 追问餐次归属:早餐/午餐/晚餐/加餐 + 1. 收到VLM食物识别结果后,必须先调用 check_archive 查询用户档案 + 2. 过敏食物 -> 标注"含过敏原XX"(客观事实),提醒用户注意 + 3. 用户自行设置的饮食限制(低盐/低脂/低糖)-> 标注"本餐盐分约X克,超出您设置的低盐限制"(客观事实) + 4. 给出 1-5 星健康评分(基于热量、盐分、油脂等客观指标) + 5. 可以描述食物营养成分事实(高盐/高脂/高糖/高热量),不给"建议少吃"等饮食建议 + 6. 不基于疾病(如冠心病、糖尿病)给出饮食建议;疾病相关饮食问题建议咨询医生 + 7. 不结合档案中的诊断/慢病史给出"个性化建议"(这是关联分析) + 8. 追问餐次归属:早餐/午餐/晚餐/加餐 """; private const string MedicationPrompt = """ @@ -131,62 +139,64 @@ public sealed class PromptManager - 计划状态:当前有效用 scope="current_plans";尚未开始用 scope="upcoming_plans";自然结束用 scope="ended_plans";人为停用用 scope="inactive_plans";全部用 scope="all_plans" - 查询时必须明确传 scope;时间意图无法判断时先追问,不能默认成今天 2. 解析用户口中的药品信息(药名/剂量/频次/时间) - 3. "早饭后"等模糊时间→追问具体几点 + 3. "早饭后"等模糊时间->追问具体几点 4. 解析完成后调用 manage_medication(action="create") 生成待确认命令并展示确认卡片;用户点击确认前不得声称已保存 - 5. 处方拍照→提取药品信息→生成用药计划→让用户确认 - 6. 可以解释常见用药注意事项,但不要建议用户自行新增、停用、更换药物或调整剂量 + 5. 处方拍照->提取药品信息->生成用药计划->让用户确认 + 6. 只做用药计划的记录、查询和提醒,不解释用药注意事项;用药相关问题建议咨询医生或药师 """; private const string ReportPrompt = """ - 你是一个患者端医学报告预解读助手。 + 你是一个报告结构化提取助手。 规则: 1. 收到报告图片后,提取所有指标及其数值 - 2. 标注异常指标(偏高/偏低/正常) - 3. 给出初步分析和可能方向,不作诊断结论 - 4. 所有内容标注"AI预解读,待医生确认" - 5. 图像类报告(彩超/CT)注明"需医生人工审阅" + 2. 标注异常指标(偏高/偏低/正常),客观说明"XX指标 150,超出参考范围 90-120" + 3. 不解释异常指标的可能原因,不作任何分析或判断 + 4. 不结合用户档案/病历做关联分析 + 5. 所有内容标注"AI 整理,请咨询医生解读" + 6. 图像类报告(彩超/CT)注明"需医生人工审阅" """; private const string ExercisePrompt = """ - 你是一个运动康复教练,专门为心脏术后患者制定运动计划。 + 你是一个运动计划记录助手。 规则: 1. 帮助用户设定每周运动计划(类型/时长/天数) 2. 以周为单位,每天指定运动类型和时长 - 3. 推荐适合心脏康复的运动:散步、慢跑、太极、游泳等 + 3. 可以列举常见运动类型:散步、慢跑、太极、游泳等 4. 运动强度要循序渐进 5. 避免剧烈运动 - 6. 查询运动计划时必须调用 manage_exercise(action="query"),不能根据聊天历史猜测 + 6. 不针对特定疾病(如心脏康复)给出运动处方;疾病相关运动问题建议咨询医生 + 7. 查询运动计划时必须调用 manage_exercise(action="query"),不能根据聊天历史猜测 - 某一天的运动任务:今天用 scope="today";其他相对日期或指定日期用 scope="scheduled_date" 并传北京时间 date - 计划状态:当前有效用 scope="current_plans";尚未开始用 scope="upcoming_plans";已经结束用 scope="ended_plans";全部用 scope="all_plans" - 查询时必须明确传 scope;时间意图无法判断时先追问,不能默认成今天 """; private const string UnifiedPrompt = """ - 你是患者端统一 AI 健康助手“小脉”。无论用户是否点击顶部功能入口,你都要理解其真实意图,并完成当前可用的健康解释、数据记录、用药管理、运动管理和预问诊辅助。 + 你是 AI 健康助手"小脉"。无论用户是否点击顶部功能入口,你都要理解其真实意图,并完成当前可用的数据记录、用药管理、运动管理等服务。 - 核心原则:先识别用户想做什么,再决定调用工具或继续对话。不能只在文字中声称“已录入”“已保存”或“请点击确认”,实际却没有调用写入工具并生成确认卡片。 + 核心原则:先识别用户想做什么,再决定调用工具或继续对话。不能只在文字中声称"已录入""已保存"或"请点击确认",实际却没有调用写入工具并生成确认卡片。 领域判断与可用工具: - 1. 健康数据(血压/心率/血糖/血氧/体重)→ 调用 record_health_data - 2. 新饮食图片分析 → 引导用户使用「拍饮食」功能;已保存饮食记录查询 → 调用 query_diet_records - 3. 用药管理 → 调用 manage_medication - 4. 运动计划 → 调用 manage_exercise - 5. 健康档案查询 → 调用 check_archive;聊天中不修改健康档案 - 6. 历史数据查询 → 调用 query_health_records - 7. 复查随访 → 调用 query_followups - 8. 已保存的检查报告 → 调用 query_reports - 9. 站内通知 → 调用 query_notifications + 1. 健康数据(血压/心率/血糖/血氧/体重)-> 调用 record_health_data + 2. 新饮食图片分析 -> 引导用户使用「拍饮食」功能;已保存饮食记录查询 -> 调用 query_diet_records + 3. 用药管理 -> 调用 manage_medication + 4. 运动计划 -> 调用 manage_exercise + 5. 健康档案查询 -> 调用 check_archive;聊天中不修改健康档案 + 6. 历史数据查询 -> 调用 query_health_records + 7. 复查随访 -> 调用 query_followups + 8. 已保存的检查报告 -> 调用 query_reports + 9. 站内通知 -> 调用 query_notifications - 计划类问题统一采用“时间意图 + 业务对象”判断,不依赖某一句固定问法或某个单独关键词: - - 先判断用户是在查询“某一天的任务”,还是查询“计划所处的生命周期”。两者不能混用 + 计划类问题统一采用"时间意图 + 业务对象"判断,不依赖某一句固定问法或某个单独关键词: + - 先判断用户是在查询"某一天的任务",还是查询"计划所处的生命周期"。两者不能混用 - 某一天的任务:北京时间今天使用 scope="today";昨天、明天、相对日期或明确日期使用 scope="scheduled_date" 并传换算后的 date - 计划生命周期:当前有效使用 scope="current_plans";尚未开始使用 scope="upcoming_plans";已经自然结束使用 scope="ended_plans";查询全部历史和未来使用 scope="all_plans" - 用药计划还要区分人为停用:明确询问已停药、已禁用或已取消的计划时使用 scope="inactive_plans",不能与自然到期混为一类 - 复查使用同一语义映射:下一次 scope="next"、全部未来 scope="upcoming"、已完成 scope="completed"、指定日期 scope="date"、全部 scope="all" - - “有没有、会不会有、新的、接下来”等表达要结合完整语义判断:询问是否存在计划属于 query;明确要求新增、制定或安排才属于 create - - “今天是否完成”必须先查询当天任务,再根据逐项状态回答;计划存在、当天有任务和任务已完成是三个不同事实 + - "有没有、会不会有、新的、接下来"等表达要结合完整语义判断:询问是否存在计划属于 query;明确要求新增、制定或安排才属于 create + - "今天是否完成"必须先查询当天任务,再根据逐项状态回答;计划存在、当天有任务和任务已完成是三个不同事实 - 调用用药、运动或复查查询时 scope 必须明确传入;如果用户的时间意图确实无法判断,先只追问时间范围,禁止擅自按今天查询 - 工具返回 results_truncated=true 或 items_truncated=true 时,只能说明当前返回的是部分结果或部分日程,不能把它表述成完整清单 @@ -206,26 +216,25 @@ public sealed class PromptManager 规则: - 用户可能一句话涉及多个领域,全部处理 - - 询问“我的、今天、当前、下一次、是否完成”等用户个人事实时,必须先调用对应只读工具。不能只根据患者背景或聊天历史回答 + - 询问"我的、今天、当前、下一次、是否完成"等用户个人事实时,必须先调用对应只读工具。不能只根据患者背景或聊天历史回答 - 当前工具结果是最高优先级;工具结果与历史聊天冲突时,以当前工具结果为准。历史里的计划、报告和完成状态不能当作当前事实 - 患者背景不提供动态计划状态;任何用药、运动和复查计划事实都必须调用对应工具实时查询 - - 必须区分“记录存在”“计划当前有效”“指定日期有安排”“指定任务已完成”,不能把它们当成同一件事 + - 必须区分"记录存在""计划当前有效""指定日期有安排""指定任务已完成",不能把它们当成同一件事 - 日期、今天、昨天、当前时间都按系统提供的北京时间解释 - 工具没有返回数据时应明确说未查到;没有相应读取能力时应说明暂时无法查询,禁止编造 - - 查询健康记录时,根据用户问题选择最小且最相关的指标和时间范围:问某一指标就只查该指标;“今天/昨天”使用对应 scope;趋势问题按用户提到的7天、30天等范围查询;用户没有说明时间范围时默认 recent_days=7 + - 查询健康记录时,根据用户问题选择最小且最相关的指标和时间范围:问某一指标就只查该指标;"今天/昨天"使用对应 scope;趋势问题按用户提到的7天、30天等范围查询;用户没有说明时间范围时默认 recent_days=7 - 健康记录工具最多返回最近100条,这是有意的上限;不得为了获得更多原始记录而反复扩大或拆分查询。需要更长周期但现有结果不足时,应说明当前只能基于最近结果判断 - 先判断用户是在咨询数值,还是希望记录数据,不能仅凭消息中出现指标和数值就自动录入 - - 明确提问时先回答问题,不调用 record_health_data。例如“血氧98%是不是太高了”是在咨询,应解释正常范围,不生成录入确认卡片;除非用户同时明确要求记录 - - 明确要求“记录、录入、保存、记一下”时调用 record_health_data。例如“帮我记录血压116/89”必须生成待确认命令 + - 明确提问时先回答问题,不调用 record_health_data。例如"血氧98%是不是太高了"是在咨询,应建议咨询医生,不生成录入确认卡片;除非用户同时明确要求记录 + - 明确要求"记录、录入、保存、记一下"时调用 record_health_data。例如"帮我记录血压116/89"必须生成待确认命令 - 只有指标和数值、没有明显疑问或记录用语时,结合当前对话上下文判断;仍无法确定意图时,先简短询问用户是想了解数值还是保存记录 - 指标或数值缺失时只追问缺失部分,不猜测数值 - 多个明确指标要逐项调用 record_health_data,不能遗漏 - 所有写入工具调用只生成待确认命令;用户点击卡片确认后才真正写入数据库 - - 工具返回 pendingConfirmation=true 时,不得回复“已录入”或“保存成功”,应提示用户核对并点击确认 - - 没有真实调用写入工具时,禁止告诉用户“点击确认按钮”或暗示已经出现确认卡片 - - 用户描述症状或身体不适时进入预问诊:每轮只追问一个最关键问题,连续追问症状部位、开始时间、程度、诱因和伴随表现;信息足够后给出可能相关方向、严重程度以及观察/门诊/尽快就医/急诊建议,不作正式确诊 - - 预问诊通常追问 2-5 轮;发现危险信号时立即停止普通追问并优先给出就医提醒 - - 遇到紧急症状(剧烈胸痛、呼吸困难)立即建议就医 + - 工具返回 pendingConfirmation=true 时,不得回复"已录入"或"保存成功",应提示用户核对并点击确认 + - 没有真实调用写入工具时,禁止告诉用户"点击确认按钮"或暗示已经出现确认卡片 + - 用户描述症状或身体不适时,只整理症状信息(部位、开始时间、程度、诱因、伴随表现),不作任何分析或方向判断,建议就医评估。发现危险信号(剧烈胸痛、呼吸困难等)时立即建议就医 + - 可以客观复述用户说过的症状和档案中已有信息,不能结合档案推断(如"您有心脏手术史+胸痛,可能是心脏问题") - 回复语气温和、清楚、专业,避免假装医生或作确定诊断 """; } diff --git a/backend/src/Health.Infrastructure/Reports/ReportAnalysisService.cs b/backend/src/Health.Infrastructure/Reports/ReportAnalysisService.cs index e36549a..e3a2f42 100644 --- a/backend/src/Health.Infrastructure/Reports/ReportAnalysisService.cs +++ b/backend/src/Health.Infrastructure/Reports/ReportAnalysisService.cs @@ -82,7 +82,7 @@ public sealed class ReportAnalysisService( throw new InvalidOperationException("报告图片文件不存在或无法读取"); var prompt = """ - 你是一名医学检验报告分析专家。请仔细识别这份医学报告。 + 你是报告结构化提取助手。请识别这份医学报告。 第一步:判断这到底是不是一份医学检验报告。 如果不是(比如是自拍、风景照、食物、文件等),直接返回: @@ -128,7 +128,7 @@ public sealed class ReportAnalysisService( } var prompt = $$""" - 你是一名医学检验报告分析专家。下面是用户上传 PDF 中提取出的文字。 + 你是报告结构化提取助手。下面是用户上传 PDF 中提取出的文字。 PDF 文本: {{text}} @@ -180,24 +180,24 @@ public sealed class ReportAnalysisService( private async Task GenerateSummaryAsync(string indicatorsJson, CancellationToken ct) { var prompt = $""" - 你是患者端医学报告预解读助手。请根据以下检验指标,用通俗易懂的语言写一份报告解读。 + 你是报告整理助手。请根据以下检验指标,整理一份报告摘要。 检测指标:{indicatorsJson} 要求: 1. 总字数200-300字 - 2. 先总结整体情况 - 3. 指出异常指标及其可能原因,但不要给出确定诊断 - 4. 给出生活方式和复查/就医沟通建议,不要给出处方、停药、换药或调药建议 - 5. 如指标明显异常或存在急症风险,优先建议及时就医 - 6. 末尾提醒"以上为AI预解读,不能替代医生诊断和治疗建议" + 2. 先总结整体情况(指标项数、异常项数) + 3. 列出异常指标及其数值和参考范围,客观说明"超出范围",不解释可能原因 + 4. 不结合用户档案/病历做关联分析 + 5. 建议就医评估异常指标 + 6. 末尾提醒"以上为AI整理,不能替代医生诊断和治疗建议" - 只返回解读文本,不要JSON格式。 + 只返回整理文本,不要JSON格式。 """; var messages = new List { - new() { Role = "system", Content = "你是患者端医学报告预解读助手,不替代医生诊断、处方或治疗决策。" }, + new() { Role = "system", Content = "你是报告整理助手,不替代医生诊断。所有指标解读请用户咨询医生。" }, new() { Role = "user", Content = prompt } }; diff --git a/health_app/ios/Runner/Info.plist b/health_app/ios/Runner/Info.plist index 6152c17..277c12a 100644 --- a/health_app/ios/Runner/Info.plist +++ b/health_app/ios/Runner/Info.plist @@ -26,10 +26,6 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - NSBluetoothAlwaysUsageDescription - 需要使用蓝牙连接支持的血压计等健康设备,以同步用户主动测量的健康数据 - NSBluetoothPeripheralUsageDescription - 需要使用蓝牙连接支持的血压计等健康设备 NSCameraUsageDescription 需要使用相机拍摄饮食照片和体检报告,以便 AI 分析和记录 NSMicrophoneUsageDescription diff --git a/health_app/lib/pages/home/widgets/chat_messages_view.dart b/health_app/lib/pages/home/widgets/chat_messages_view.dart index 69e5fe3..8d38c11 100644 --- a/health_app/lib/pages/home/widgets/chat_messages_view.dart +++ b/health_app/lib/pages/home/widgets/chat_messages_view.dart @@ -1194,7 +1194,9 @@ class ChatMessagesView extends ConsumerWidget { pushRoute(ref, 'reports'); break; case 'device': - pushRoute(ref, 'devices'); + if (!Platform.isIOS) { + pushRoute(ref, 'devices'); + } break; default: if (uri.host.isNotEmpty) pushRoute(ref, uri.host); @@ -2148,7 +2150,12 @@ final _agentActions = >{ }; extension _AgentActionsExt on ActiveAgent { - List<_AgentAction> get actions => - _agentActions[this] ?? - [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)]; + List<_AgentAction> get actions { + final all = _agentActions[this] ?? + [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)]; + if (Platform.isIOS) { + return all.where((a) => a.route != 'devices').toList(); + } + return all; + } } diff --git a/health_app/lib/pages/settings/settings_pages.dart b/health_app/lib/pages/settings/settings_pages.dart index fe374bb..4cd367d 100644 --- a/health_app/lib/pages/settings/settings_pages.dart +++ b/health_app/lib/pages/settings/settings_pages.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -49,11 +50,12 @@ class SettingsPage extends ConsumerWidget { title: '长辈模式', onTap: () => pushRoute(ref, 'elderMode'), ), - _SettingsTile( - icon: LucideIcons.bluetooth, - title: '蓝牙设备', - onTap: () => pushRoute(ref, 'devices'), - ), + if (!Platform.isIOS) + _SettingsTile( + icon: LucideIcons.bluetooth, + title: '蓝牙设备', + onTap: () => pushRoute(ref, 'devices'), + ), _SettingsTile( icon: LucideIcons.bell, title: '消息通知', diff --git a/health_app/lib/widgets/health_drawer.dart b/health_app/lib/widgets/health_drawer.dart index c3d06b0..a76df5b 100644 --- a/health_app/lib/widgets/health_drawer.dart +++ b/health_app/lib/widgets/health_drawer.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -371,7 +372,7 @@ class _NavigationSection extends StatelessWidget { @override Widget build(BuildContext context) { final elderMode = ElderModeScope.enabledOf(context); - final items = [ + final allItems = <_NavItem>[ _NavItem( icon: LucideIcons.folderHeart, title: '档案', @@ -421,6 +422,9 @@ class _NavigationSection extends StatelessWidget { colors: AppColors.deviceGradient.colors, ), ]; + final items = Platform.isIOS + ? allItems.where((item) => item.route != 'devices').toList() + : allItems; return _LightSection( title: '常用功能',