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

242 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
# 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 结构化草稿 → 医生确认 → 待审核”的链路。