21 KiB
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()POSTtcm.prescription/add。app/src/doctor_workstation/services/repository.py:1574-1593:update_prescription()POSTtcm.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-35server/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 生成结构化草稿,医生确认并签名,服务端权威绑定,进入独立审核”。
- 新增只读草稿能力:AI 返回
prescription_draft,至少包含clinical_diagnosis、主/辅方药材(medicine_id/name/dosage/formula_type)、剂数、用法、禁忌、生成依据、缺失信息和风险警示;不得包含可自行决定的患者/诊单/医生身份字段。 - 服务端严格解析草稿:使用药材主数据解析、剂量范围、重复药名、特殊人群/相互作用规则;无法解析的草稿只作为文本展示。
- UI 显示“采用为处方草稿”,而不是“直接提交”;逐字段标记“AI 建议/诊单原值/医生修改”,打开现有
PrescriptionEditorDialog。 - 医生必须人工复核、补齐必填项并手写/绑定签名;确认页明确显示“将创建待审核处方”。
- 创建接口仅接收授权
diagnosis_id和临床处方字段;患者、预约、医生身份、病例快照全部由服务端权威派生,并在事务内校验诊单写权限与唯一性。 - 审核继续使用独立角色/权限;AI 生成标记、模型、prompt 版本、草稿哈希、采用人和修改差异写审计日志。
建议补充的最小测试集
- AI
done事件的合法/非法prescription_draftschema、超量药材、无medicine_id、负剂量和重复药名。 - “AI 建议 → 采用草稿 → 人工修改 → 签名 → 创建待审”端到端桌面测试。
- AI 创建 payload 必须包含当前诊单,并由服务端返回的处方断言
diagnosis_id/appointment_id/patient_id一致。 - 任意其他诊单 ID、跨数据域诊单、错配预约/患者 ID 的创建请求必须失败。
- 编辑请求尝试修改
diagnosis_id/patient_id/appointment_id必须失败。 - 只有读取权限、只有 AI 权限、只有
cf.prescription/add、只有chufang/kaifang的权限矩阵测试,并覆盖 middleware 菜单已注册/未注册两种状态。 - 创建后必须仍为待审核;没有审核权限的创建者不能把处方变为已通过。
- 共享处方仅扩大读取范围,非创建者不能编辑、删除或作废。
最终判断
- 问:目前能否从 AI 对话/报告一键形成并提交处方?答:不能。
- 问:能否在 AI 对话窗口里用一句“开方”命令进入处方流程,并在人工填写/签名后创建待审核处方?答:当前工作树可以。
- 问:这条半自动链路是否已达到可安全上线的端到端闭环?答:尚未。 服务端诊单写授权、患者/预约权威绑定、权限 canonical 化和患者字段丢失问题应先修复;之后再建设“AI 结构化草稿 → 医生确认 → 待审核”的链路。