Files
zyt/app/research/ai_prescription_audit_20260821.md
2026-08-22 08:51:35 +08:00

21 KiB
Raw Permalink Blame History

AI 对话/报告直接开方链路审计(2026-08-21)

结论

当前不能从 AI 的回答或 AI 报告“一键形成并提交处方”。 当前工作树实现的是另一条链路:医生在 AI 对话框输入一条明确的本地命令(如“开个处方”),桌面端不请求 AI,而是重新核对诊单/患者后打开通用处方编辑器;医生仍需人工填写药材、剂量、用法和手写签名,点击确认后才调用真实创建接口,服务端保存为 audit_status = 0 的待审核处方。

因此应区分三种能力:

能力 当前状态 判断
AI 输出诊断/风险/用药建议文本 已有 助手返回 answer 文本;结构化分析仅有诊断建议、风险和治疗建议
从 AI 对话入口手工新建并提交待审核处方 部分可用 输入特定命令可打开编辑器,人工完成后调用 tcm.prescription/add
把 AI 生成的药味、剂量、用法直接转换成处方草稿/一键提交 不存在 无处方草稿 schema、无 AI 结果到编辑器的字段映射、无“采用为处方”按钮,也无服务端 AI 开方接口
AI 直接生成已审核/生效处方 不存在,且不应建设成无人工复核链路 创建接口强制待审核;审核由独立权限和角色控制

审计范围与验证

  • 审计当前工作树中的桌面端 Python/PySide6、repository/API、PHP controller/logic/validate、权限及数据绑定。
  • 未修改生产代码或测试,只新增本报告。
  • 已运行:uv run pytest tests/test_ai_consult_ui.py tests/test_prescription_ui.py -q,67 项通过。
  • 已运行:DiagnosisAiAssistantContractTest.php、DiagnosisAiAssistantStreamContractTest.php、DiagnosisWorkspaceRowAuthorizationTest.php,均通过。
  • 仓库根目录没有 AGENTS.md 声明的 .trellis/workflow.md 和 .trellis/spec/,故无法应用缺失的 Trellis 分层规范;本报告按现有实现和测试取证。

端到端链路

1. AI 返回结构:只能给建议,不能形成处方 DTO

服务端病例助手的返回契约只有 answer/model_key/model_label/model_name/task,其中 answer 是清洗后的纯文本;没有 herbs、medicine_id、dosage、usage_* 或可执行 action。证据:

  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:276-307:助手调用上游并交给 formatAssistantResult()。
  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:419-437:最终响应只有 answer 和模型/任务元数据。
  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:64-104:prescription_review 仅定义为“分析处方/用药并提示复核重点”,不是生成处方。
  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:1084-1106:提示词要求“简洁、分点的专业回答”和执业医师复核,没有处方 JSON schema。

另一路结构化 aiAnalysis 也只能返回 diagnosis_advice、risk_assessment、treatment_advice:

  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:1035-1069:模型被要求输出的唯一 JSON schema 不包含处方字段。
  • server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:1281-1352:解析器严格只接受上述三类业务字段。

桌面端流式处理也只拼接 delta.text 并渲染答案,不解析处方动作或草稿:

  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5243-5282:start/delta/done 只更新文本和模型标签。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5284-5289:流式片段直接拼成 _stream_text。

2. 桌面 AI UI:有“命令开编辑器”,没有“AI 结果转处方”

AI 入口和患者选择已接通:

  • app/src/doctor_workstation/ui/shell.py:1103-1122,1551-1565:有全局“AI 助手/开始对话”入口,并先进入患者诊单选择器。
  • app/src/doctor_workstation/ui/dialogs/ai_consult_picker.py:57-119:选择对象分别保存 diagnosis_id 与 source_patient_id,不会把诊单主键误当患者主键,只保留掩码手机号。
  • app/src/doctor_workstation/ui/dialogs/ai_consult_picker.py:274-298,390-419:通过专用 repository 拉取权限范围内诊单,再携带诊单/患者上下文打开 AI 工作区。

所谓“开方”实际是本地意图拦截:

  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:2931-2967:仅识别不超过 24 字的明确命令;带“怎么/是否/建议/分析/复核/审核”等词时不会触发。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5171-5179:本地动作发生在 _compose_ai_prompt() 和 AI worker 之前,命中后直接返回,不会调用模型。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:4881-4919:本地检查 tcm.diagnosis/chufang 或 tcm.diagnosis/kaifang,再开始诊单核对。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:4921-5001:重新读取只读诊单详情,严格比对诊单 ID 和当前会话患者 ID。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5003-5044:仅从权威诊单构造患者/诊断种子,没有从 AI 答案提取药材。
  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5055-5105:打开通用 PrescriptionEditorDialog;只有对话框返回 Accepted 后才调用 create_prescription。

这意味着:

  • 输入“怎么开方更合理”会得到 AI 文本建议,但该回答没有“采用为处方”入口。
  • 输入“开个处方”不会让 AI 开方,只会打开人工编辑器。
  • 即便在命令里写药名和剂量,当前代码也不会解析或带入编辑器。
  • UI 没有可发现的“开方”快捷按钮;现有快捷指令都是病情总结、用药建议、检查建议等(app/src/doctor_workstation/ui/dialogs/ai_consult.py:762-783,3189-3215)。

3. 人工编辑与提交:接口已复用,但仍是完整人工处方流程

通用处方编辑器可复用程度较高:

  • app/src/doctor_workstation/ui/dialogs/prescription.py:2652-2765:完整新增/编辑处方表单。
  • app/src/doctor_workstation/ui/dialogs/prescription.py:2910-3007:患者、诊断、诊单提示等表单字段。
  • app/src/doctor_workstation/ui/dialogs/prescription.py:3015-3042:可从处方库或文本导入药材;这不是 AI 回答映射。
  • app/src/doctor_workstation/ui/dialogs/prescription.py:3205-3227:医师姓名和手写签名是必填 UI。
  • app/src/doctor_workstation/ui/dialogs/prescription.py:3770-3831:提交前校验患者、临床诊断、医师、手写签名、至少一味药材、药材主数据选择和正剂量。

repository/API 已接真实端点:

  • app/src/doctor_workstation/services/repository.py:1563-1572:create_prescription() POST tcm.prescription/add。
  • app/src/doctor_workstation/services/repository.py:1574-1593:update_prescription() POST tcm.prescription/edit。
  • app/src/doctor_workstation/services/repository.py:1643-1649:按诊单刷新 tcm.prescription/listByDiagnosis。
  • app/src/doctor_workstation/services/repository.py:3249-3279:repository 只做浅层 DTO 规范化,不承担患者/诊单一致性校验。

4. 服务端创建与审核:创建即待审,不等于审核通过

服务端创建流程已有一些正确的安全边界:

  • server/app/adminapi/controller/tcm/PrescriptionController.php:27-35:控制器忽略客户端 creator_id,以当前 $adminId 调用创建逻辑。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:220-245:限制同诊单、同开方人、同日只能有一张未作废处方。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:268-278:要求药材数组并通过药材主数据解析。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:332-340:忽略客户端审核状态,强制 audit_status = 0,创建人与诊单医助由服务端写入。

“提交审核”只是保存一条待审核记录;AI 链路不会调用审核接口。真正审核是独立动作:

  • server/app/adminapi/controller/tcm/PrescriptionController.php:102-129:审核需另行调用 audit。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:55-73,796-840:审核还需允许角色、对象可见性和待审状态;通过后才变为 audit_status = 1。

主要缺口与风险

P1 / 高:创建接口没有诊单写权限和行级数据域校验

PrescriptionLogic::add() 在有 diagnosis_id 时只做 Diagnosis::find() 存在性检查,没有复用 DiagnosisLogic::canManageDiagnosis() 或 canViewReadonlyDiagnosis(),controller 也没有传入 $adminInfo:

  • server/app/adminapi/controller/tcm/PrescriptionController.php:27-35
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:251-266
  • 可复用的写权限入口已经存在于 server/app/adminapi/logic/tcm/DiagnosisLogic.php:4458-4468。

桌面 AI 流程虽然会先走只读详情并做客户端 ID 比对,但这不是服务端写操作授权。调用者只要能到达 tcm.prescription/add,就可能对一个仅知道 ID、但不在其可管理范围内的诊单创建处方。医疗数据完整性和越权写入风险都应由服务端兜底。

建议: add() 接收 $adminInfo,在任何读取患者/预约信息和写入前调用 DiagnosisLogic::canManageDiagnosis($diagnosisId, $adminId, $adminInfo);不存在和越权统一报错,避免枚举。AI 页面可继续保留客户端核对作为 UX 防误操作,但不能代替服务端鉴权。

P1 / 高:诊单、预约、患者和患者快照没有权威一致性校验

创建逻辑只验证诊单存在,随后直接信任客户端提交的 appointment_id、patient_id、patient_name、phone 和病例快照:

  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:251-260:只查诊单是否存在。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:291-315:诊单、预约、患者 ID 和患者快照直接来自 $params。
  • server/database/migrations/create_tcm_prescription.sql:5-7,27-31:三个关系字段只有普通索引,没有外键约束。

因此直接 API 请求可构造“诊单 A + 预约 B + 患者 C + 姓名 D”的处方。即便 UI 正常使用,也存在下一项实际丢字段问题。

建议: 创建时仅接受 diagnosis_id 和处方临床字段;由服务端基于授权诊单解析并写入 appointment_id/patient_id/patient_name/phone/gender/age,对病例快照使用服务端当前诊单生成。若必须允许修正患者打印信息,应走现有独立 patchPatient 权限和审计链路。

P1 / 高:桌面处方编辑器会丢失 patient_id 和 phone

AI 入口的种子包含患者 ID 和电话:

  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5021-5030

但 PrescriptionEditorDialog.payload() 的保留字段没有 patient_id、appointment_id、phone、case_record,表单输出也没有这些字段:

  • app/src/doctor_workstation/ui/dialogs/prescription.py:3692-3758
  • 领域模型 Prescription 本身也没有 patient_id 字段:app/src/doctor_workstation/core/models.py:577-588。

AI 流程在确认后只强制补回 diagnosis_id、appointment_id 和 case_record,没有补回 patient_id 或 phone:

  • app/src/doctor_workstation/ui/dialogs/ai_consult.py:5078-5093

服务端对缺失值使用 patient_id = 0、phone = '':

  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:300-307

所以从 AI 对话新建的处方虽然能按 diagnosis_id 找到,但患者 ID/手机号快照会为空。诊单详情里的既有手工开方流程采用同样的 payload 回填方式,也有同类问题(app/src/doctor_workstation/ui/dialogs/diagnosis.py:3085-3104,3117-3127)。

现有 AI UI 测试只断言强制回填了诊单、预约和病例快照,没有断言 patient_id/phone:app/tests/test_ai_consult_ui.py:293-305。

建议: 短期在编辑器 DTO 中不可编辑地保留 patient_id/phone/appointment_id/case_record 并补测试;最终仍应由服务端从诊单权威派生,避免信任客户端快照。

P1 / 高:编辑接口允许关系漂移,且共享处方的对象级编辑边界过宽

服务端编辑时允许客户端提供新的 diagnosis_id,但没有验证新诊单存在、调用者能管理新诊单,也不会同步/校验 patient_id、appointment_id 和 phone:

  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:388-425:新诊单 ID 用于唯一性检查,但没有授权/存在性检查。
  • server/app/adminapi/logic/tcm/PrescriptionLogic.php:438-485:保存新 diagnosis_id,患者 ID、预约 ID、电话和病例快照不在更新集合中。

同时对象级编辑规则是“创建者或 is_shared = 1”,即任何能到达编辑端点的用户都可编辑共享处方:server/app/adminapi/logic/tcm/PrescriptionLogic.php:408-412。这会让处方既可能被重新绑定到其他诊单,又可能保留旧患者关系字段。

建议: 编辑禁止修改关系字段;若确需迁移,使用专用、强审计接口并同时校验新诊单写权限和重建全部患者快照。共享应只扩大读取范围,不应自动扩大编辑权。

P1 / 高(部署相关):桌面权限码与真实 API 路由没有统一的服务端别名契约

AI UI 以 tcm.diagnosis/chufang 或 tcm.diagnosis/kaifang 判断可开方(app/src/doctor_workstation/ui/dialogs/ai_consult.py:4891-4901),独立已开处方页则使用 cf.prescription/add|edit(app/src/doctor_workstation/ui/pages/prescriptions.py:558-565,676-683),但 repository 最终调用的是 tcm.prescription/add|edit。

AuthMiddleware 的通用规则是:如果真实路由不在全量菜单 URI 中就直接放行;若已注册则要求真实路由或显式别名:

  • server/app/adminapi/http/middleware/AuthMiddleware.php:70-92
  • 现有别名仅为“处方库列表导入”覆盖多套权限,不包含 tcm.prescription/add|edit 的开方别名:server/app/adminapi/http/middleware/AuthMiddleware.php:169-202。

本仓库没有找到为 tcm.prescription/add|edit 注册并分配权限的版本化 SQL,故实际安全性依赖部署数据库里是否已有旧菜单记录:

  • 若未注册,middleware 的第 77-82 行会 fail-open。
  • 若注册但未分配真实路由,拥有 chufang/kaifang/cf.prescription/add 的桌面用户可能被 403。

建议: 选择一套 canonical 权限,版本化注册真实路由,并在 middleware 对兼容码做双向、可测试的精确别名;未知业务写路由应 fail-closed,而不是因为未注册就绕过鉴权。

P2 / 中:AI “开方命令”不可发现,且会丢弃命令中的处方内容

界面没有开方快捷按钮或“采用为处方”CTA;只有文本意图正则。命中后清空输入并进入本地编辑流程(app/src/doctor_workstation/ui/dialogs/ai_consult.py:4881-4889),未保存原命令里的药味、剂量或 AI 建议。用户容易误解为 AI 已生成处方,实际看到的是诊单预填的空药材表单。

建议: AI 答案与本地命令分离。提供明确的“生成处方草稿”与“采用草稿”按钮,展示字段来源、缺失项和风险提示;任何药材/剂量进入正式表单前都需医生逐项确认。

P2 / 中:医师显示名和签名缺少服务端身份约束

编辑器预填当前用户姓名,但姓名仍可编辑,签名由客户端 data URL 提交(app/src/doctor_workstation/ui/dialogs/prescription.py:3205-3227,3349-3355,3740-3741);服务端虽然强制 creator_id = $adminId,却直接保存客户端 doctor_name/doctor_signature(server/app/adminapi/logic/tcm/PrescriptionLogic.php:327-339)。待审核机制降低了风险,但不能防止错误/冒用的签名快照进入系统。

建议: 医师显示名由 authenticated profile 派生;签名使用账号绑定的签名资产或至少保存签名来源、哈希、提交人、时间和确认事件,不接受 AI 生成签名。

P2 / 中:删除、作废的对象级服务端授权也不完整

虽不是 AI 新建的主路径,但同一处方生命周期中:

  • delete() 未接收当前管理员,也没有创建者/共享/可见性检查:server/app/adminapi/logic/tcm/PrescriptionLogic.php:636-675。
  • void() 接收管理员仅用于记录作废人,没有对象级授权:server/app/adminapi/logic/tcm/PrescriptionLogic.php:1137-1178。

如果路由权限配置漂移或范围过宽,可能修改他人处方。建议所有写操作统一通过同一个处方对象授权策略。

可直接复用的接口和组件

层 可复用能力 证据 / 用途
AI 患者选择 list_ai_patient_options / tcm.diagnosis/aiPatientOptions app/src/doctor_workstation/services/repository.py:2029-2044;用于只暴露数据域内、脱敏的诊单目标
诊单权威读取 get_diagnosis_detail(..., readonly=True) / readonlyDetail app/src/doctor_workstation/services/repository.py:2046-2060;服务端在 server/app/adminapi/logic/tcm/DiagnosisLogic.php:4362-4412 做行级只读授权
AI 问答 stream_diagnosis_ai / aiAssistantStream app/src/doctor_workstation/services/repository.py:1373-1405;适合继续提供解释,不应直接作为可执行处方 DTO
人工处方编辑 PrescriptionEditorDialog 已有主数据药材选择、剂量、用法、签名和本地校验,可作为 AI 草稿的人工复核容器
药材主数据 doctor.medicine/lists + RemoteMedicineComboBox app/src/doctor_workstation/ui/dialogs/prescription.py:1425-1536;AI 草稿必须解析为有效 medicine_id
处方提交 create_prescription / tcm.prescription/add 可复用,但应先补服务端诊单写授权和权威关系派生
处方刷新 list_prescriptions_by_diagnosis 创建成功后已能按当前诊单刷新并过滤归属
服务端药材校验 normalizeHerbIdentities() server/app/adminapi/logic/tcm/PrescriptionLogic.php:165-175;AI 草稿落表前必须复用
服务端重复控制 assertUniquePrescriptionPerDiagnosisDay() server/app/adminapi/logic/tcm/PrescriptionLogic.php:220-245
诊单写授权 DiagnosisLogic::canManageDiagnosis() server/app/adminapi/logic/tcm/DiagnosisLogic.php:4458-4468;应接入处方 add/edit
审核 PrescriptionLogic::audit() 保留独立人工审核,不与 AI 生成合并

推荐目标链路

不建议把“AI 可以直接给患者开方”实现为模型静默调用创建接口。更安全且可交付的目标是“AI 生成结构化草稿,医生确认并签名,服务端权威绑定,进入独立审核”。

  1. 新增只读草稿能力:AI 返回 prescription_draft,至少包含 clinical_diagnosis、主/辅方药材(medicine_id/name/dosage/formula_type)、剂数、用法、禁忌、生成依据、缺失信息和风险警示;不得包含可自行决定的患者/诊单/医生身份字段。
  2. 服务端严格解析草稿:使用药材主数据解析、剂量范围、重复药名、特殊人群/相互作用规则;无法解析的草稿只作为文本展示。
  3. UI 显示“采用为处方草稿”,而不是“直接提交”;逐字段标记“AI 建议/诊单原值/医生修改”,打开现有 PrescriptionEditorDialog。
  4. 医生必须人工复核、补齐必填项并手写/绑定签名;确认页明确显示“将创建待审核处方”。
  5. 创建接口仅接收授权 diagnosis_id 和临床处方字段;患者、预约、医生身份、病例快照全部由服务端权威派生,并在事务内校验诊单写权限与唯一性。
  6. 审核继续使用独立角色/权限;AI 生成标记、模型、prompt 版本、草稿哈希、采用人和修改差异写审计日志。

建议补充的最小测试集

  1. AI done 事件的合法/非法 prescription_draft schema、超量药材、无 medicine_id、负剂量和重复药名。
  2. “AI 建议 → 采用草稿 → 人工修改 → 签名 → 创建待审”端到端桌面测试。
  3. AI 创建 payload 必须包含当前诊单,并由服务端返回的处方断言 diagnosis_id/appointment_id/patient_id 一致。
  4. 任意其他诊单 ID、跨数据域诊单、错配预约/患者 ID 的创建请求必须失败。
  5. 编辑请求尝试修改 diagnosis_id/patient_id/appointment_id 必须失败。
  6. 只有读取权限、只有 AI 权限、只有 cf.prescription/add、只有 chufang/kaifang 的权限矩阵测试,并覆盖 middleware 菜单已注册/未注册两种状态。
  7. 创建后必须仍为待审核;没有审核权限的创建者不能把处方变为已通过。
  8. 共享处方仅扩大读取范围,非创建者不能编辑、删除或作废。

最终判断

  • 问:目前能否从 AI 对话/报告一键形成并提交处方?答:不能。
  • 问:能否在 AI 对话窗口里用一句“开方”命令进入处方流程,并在人工填写/签名后创建待审核处方?答:当前工作树可以。
  • 问:这条半自动链路是否已达到可安全上线的端到端闭环?答:尚未。 服务端诊单写授权、患者/预约权威绑定、权限 canonical 化和患者字段丢失问题应先修复;之后再建设“AI 结构化草稿 → 医生确认 → 待审核”的链路。