Files
xuetang/app/research/integration_review.md
2026-09-08 11:40:15 +08:00

102 lines
11 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.
# 医生工作站集成审查
审查时间:2026-08-10。范围为当前工作区中的组合根、登录到主壳链路、Remote/Demo repository、权限门控、退出/视频生命周期及 PyInstaller 入口。审查只读进行;除本报告外未修改项目文件,也未发送网络请求。
## 结论
入口链 `packaging/doctor_workstation.spec -> doctor_workstation/__main__.py -> app.main()``build_repository(..., verify=...)` 的构造签名已经对齐,Remote/Demo 的主要 CRUD 与通话方法也具有兼容签名。当前仍有 4 个高严重度和 6 个中严重度集成问题;其中浏览器视频模式目前不能真正发起通话,退出时也存在通话窗口失管和 GUI 阻塞风险。
## P1(高)
### 1. 浏览器视频模式没有把一次性通话上下文交给伴随页
- `src/doctor_workstation/video/window.py:312-326` 在服务端 `start_call` 后仅执行 `webbrowser.open(location.url)``VideoCallRequest` 中的 diagnosis、目标用户和 UserSig 均未交给浏览器。
- `video_companion/src/main.ts:208-236` 只有显式调用 `window.doctorCall.start(config)` 才会初始化 SDK 并呼叫患者,页面自身不获取票据;`video_companion/src/main.ts:281-289` 只是暴露 API 和发送 ready。
- `README.md:70-77` 又把 browser 声明为正式降级路径,并要求用一次性业务票据传递上下文,和当前实现不一致。
系统浏览器不存在 Qt WebChannel,且静态 URL 连 diagnosis_id 都没有,因此页面会一直停在“等待桌面端发起”,而后端通话记录已经开始。浏览器标签关闭也无法回告 Python,`end_call` 只能等显式退出应用。应使用后端签发、单次消费且短有效期的浏览器 handoff ticketURL 中不能放 UserSig),或受认证的本机 IPC;伴随页确认接收后再调用 `start_call`,并建立可观测的结束回调。
### 2. 通话 start/bind/end 的同步 HTTP 被直接放在 Qt GUI 线程执行
- `src/doctor_workstation/video/window.py:206-290` 直接调用 repository 的 `start_call``bind_call_room``end_call`
- browser 在 `src/doctor_workstation/video/window.py:312-320` 同步 startembedded 在 loadFinished 回调 `src/doctor_workstation/video/window.py:437-456` 同步 start,在 bridge/close 回调 `src/doctor_workstation/video/window.py:485-522` 同步 bind/end。
- Remote 实现最终执行同步 `httpx` POST`src/doctor_workstation/services/repository.py:377-402`),而配置超时可达 120 秒。
- 登出和进程退出又在主线程逐个 `call.close()``src/doctor_workstation/app.py:298-315``src/doctor_workstation/app.py:409-415`)。
弱网时打开、挂断、退出都会冻结整个界面;`end_call` 抛错时 embedded 的 `closeEvent` 甚至到不了 `event.accept()`。应把生命周期写操作放入受控 worker,并使用状态机保证单次执行;退出阶段设置短上限、记录未完成结束动作,同时先关闭媒体/UI,不能让网络请求阻塞 Qt 关闭事件。
### 3. 同一 diagnosis 的重复发起会覆盖通话句柄,导致退出后仍可能保留旧视频窗口
- `src/doctor_workstation/app.py:343-356` 没有 pending/active 去重,用户可在票据请求完成前重复发起。
- `src/doctor_workstation/app.py:390-396` 以 diagnosis_id 为唯一 key 直接覆盖旧句柄;旧窗口的 destroyed 回调还会无条件 `pop` 同一个 key,可能把较新的句柄移除。
- 登出/退出只关闭字典当前仍持有的值(`src/doctor_workstation/app.py:298-305``src/doctor_workstation/app.py:409-412`)。
结果是第一个窗口可能在切回登录页后继续持有摄像头/麦克风;反向关闭旧窗口也会让新窗口失去托管。应对 diagnosis 建立 pending/active 单飞,拒绝或先可靠关闭旧通话;销毁回调必须做对象身份判断后再移除。
### 4. WebEngine 对任意来源自动授予音视频权限,且登出没有隔离/清理 profile
- `src/doctor_workstation/video/window.py:381-390` 使用 `QWebEngineView` 的共享默认 profile,没有为单次通话建立隔离 profile。
- `src/doctor_workstation/video/window.py:405-435` 在授权回调中不校验请求 origin、当前页面 URL 或通话状态,只要 feature 名含 audio/video 就 grant。
- 没有自定义 `acceptNavigationRequest`/origin allowlist;关闭时 `src/doctor_workstation/video/window.py:519-522` 仅挂断,不撤销权限或清理 Cookie、cache、local storage。
初始 URL 虽经过 HTTPS 校验,但页面后续导航不受限制;同一进程内切换账号时 WebEngine 状态也可能复用。应使用每通话或每会话的 off-the-record profile、精确 origin/path allowlist,只在活跃通话且当前主文档来源匹配时授权,并在挂断/登出时撤销权限和清理页面/profile。
## P2(中)
### 5. API code=-1 没有接入应用级会话失效处理
- `src/doctor_workstation/services/api_client.py:277-278` 会抛出 `AuthenticationExpiredError`
- worker 只把异常交给页面自己的通用 on_error(`src/doctor_workstation/ui/widgets.py:262-293`);组合根只有用户主动点击时才执行 `_logout``src/doctor_workstation/app.py:298-315`)。
- 项目中除异常定义/抛出外没有消费 `AuthenticationExpiredError` 的代码。
token 过期后主壳仍展示已加载的患者数据和旧权限快照,各页面只显示错误,用户必须手动退出。应在统一 API/worker 边界发出 session-expired 事件,由 Controller 原子地停止轮询和视频、清 token、销毁 ShellWindow 并回到 LoginWindow。
### 6. 页面和动作权限使用过宽的 OR 兜底,并忽略后端 menu
- `src/doctor_workstation/services/repository.py:111-127` 已解析 `Session.menu`,但 `src/doctor_workstation/ui/shell.py:248-260` 只按硬编码 `NAVIGATION.permissions` 注册页面,menu 从未参与判断;这与管理端“无有效 menu 即 403”的已确认行为(`research/admin_audit.md:103-109`)不一致。
- 接诊页可仅凭 `doctor.appointment/reception` 显示,但页面首先请求 `doctor.appointment/lists``src/doctor_workstation/ui/shell.py:40-47``src/doctor_workstation/ui/pages/reception.py:347-364`)。患者页可仅凭 readonlyDetail 显示,却始终请求 firstvisit lists;问诊页可仅凭 appointment lists 显示,却始终请求 diagnosis lists`src/doctor_workstation/ui/shell.py:62-75`)。
- 通知医助和视频按钮又把基础 lists 当动作权限兜底(`src/doctor_workstation/ui/pages/reception.py:321-332``src/doctor_workstation/ui/pages/consultations.py:88-96`),所以缺少 videoQr/notifyAssistant 的账号仍看到按钮。
服务端仍是最终边界,但当前 UI 会显示必然 403 的页面/动作,也可能绕过管理员对 desktop 页面入口的隐藏意图。页面应同时受后端 menu/capability 和实际列表接口权限约束;动作只接受对应动作码或经过确认的同义码,不能用 lists 兜底。
### 7. 已开处方审核状态筛选在 UI 适配层被改成 Remote 不识别的字段
- UI 传入 status`src/doctor_workstation/ui/widgets.py:205-208` 将其改写为 `audit_status`
- Remote 只在收到 `status` 时转换成后端需要的 `audit_filter=pending|passed|rejected``src/doctor_workstation/services/repository.py:284-303`),因此实际请求会原样携带数值 `audit_status`
- Demo 特意兼容了 `audit_status``src/doctor_workstation/services/mock_repository.py:374-399`),所以演示验收不会暴露生产差异。
结果是生产环境的“待审核/已通过/已驳回”筛选可能无效或被后端拒绝。应只在 repository 内做一次 UI 值到 API DTO 的映射,并给 Remote 增加 request-parameter 合同测试。
### 8. 登录 token 在 Session 校验成功前落盘,失败路径不回滚
- `src/doctor_workstation/services/repository.py:82-90` 在调用 `auth.admin/mySelf` 前就设置并持久化 token。
- mySelf 网络失败、协议错误或 code=10 时 LoginWindow 只显示错误(`src/doctor_workstation/ui/login.py:385-390`),不会调用 repository.logout。
这会在用户从未进入有效 Session 的情况下留下内存和 keyring/JSON token,后续登录还会带着该 token 请求 login/account。应先把 token 暂存在内存,完整构建并校验 Session 后再持久化;任何异常都清理 client/token store。
### 9. “记住账号”未控制 TokenStore 中的账号落盘,且持久 token 没有启动恢复闭环
- `src/doctor_workstation/services/repository.py:87-88` 每次成功账号密码登录都把 account 传给 TokenStore,与复选框无关。
- TokenStore 会把 account 写入 JSON 元数据(`src/doctor_workstation/services/token_store.py:83-108`),而 LoginWindow 的复选框只增删 QSettings`src/doctor_workstation/ui/login.py:374-379`)。
- `ApplicationController.start()` 始终显示登录页(`src/doctor_workstation/app.py:172-176`),未调用已实现的 `restore_session`;普通关闭只 close client,不清 token`src/doctor_workstation/app.py:409-415`)。
因此取消“记住账号”仍会在磁盘留下账号;成功 token 则持续保存但下次启动完全不使用。应让 remember-account 明确控制所有账号元数据,并二选一:启动时安全验证/恢复 token,或不持久化并在关闭时清除。
### 10. 当前冻结产物落后于最新 companion,构建脚本也没有执行入口 smoke test
- 最新 `video_companion/dist/index.html:8` 引用 `index-le5ZH3pL.js`(包含 roomId/bindCallRoom 桥接),现有 `dist/DoctorWorkstation/_internal/video_companion_dist/index.html:8` 仍引用旧的 `index-BrQGJzsD.js`
- 应用已经提供 `--smoke-test``src/doctor_workstation/app.py:436-451`),但 Windows 构建只检查 helper/pak/index 文件存在(`scripts/build_windows.ps1:30-43`),macOS 同样只做静态文件和 codesign 检查(`scripts/build_macos.sh:22-37`),都不启动冻结入口。
所以当前 `dist/DoctorWorkstation` 不包含刚合并的 room binding;未来即使入口 import/bootstrap 失败,构建也可能仍打印成功。交付前应重新执行 PyInstaller,并在隔离用户目录、无真实网络的环境下运行冻结程序 `--smoke-test`,同时校验退出码和日志中无未捕获异常。
## 已核对且未发现签名断点
- `ApiClient(base_url, ..., verify=...)``build_repository(..., verify=...)``app.py` 调用一致。
- Remote/Demo 的 login 均返回 `Session`;列表、接诊、处方库 CRUD、患者/问诊列表及 `get_call_ticket/start_call/bind_call_room/end_call` 的 UI 所需参数基本对齐。
- PyInstaller spec 的入口、`src` pathex、resources 和 `video_companion_dist` 目标路径与 `resources.py``_MEIPASS` 查找规则一致。
- 最新 source companion 已产生 roomId 并调用 `bind_call_room`;本报告未把此前已修复的“问诊页交换 appointment/diagnosis ID”或“未绑定 room”列为当前问题。
## 验证限制
本轮没有重新执行 Python 测试:工作区 `.venv/Scripts/python.exe` 指向当前机器上不存在的 uv Python 基础解释器;为保持只读审查,没有重建虚拟环境。现有 `research/ui_acceptance.md` 记录的最近一次完整测试为 46 passed,但上述多项是未被现有单元测试覆盖的跨层行为。