Files
zyt/artifacts/desktop-ai-quick-tools-review.md
T
2026-10-17 16:08:24 +08:00

76 lines
7.1 KiB
Markdown
Raw 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.
# Desktop AI quick-tools lifecycle review
审查时间:2026-10-17。范围:`app/src/doctor_workstation/ui/dialogs/ai_consult.py` 的工具绑定、流状态与关闭路径;辅助读取 repository / API SSE 实现和现有 UI 测试。审查时业务文件及测试已有未提交改动,未编辑这些文件,也未调用生产或任何外部服务。
## 结论
### 1. 已确认:done 后仍由 worker.finished 持有忙碌门控
- 六个工具在 `ai_consult.py:4009-4012` 用 `_ClickCard.clicked = Signal()` 绑定各自的默认参数 `prompt`。信号不携带 bool 参数,因此未发现常见的 lambda 参数覆盖或循环变量晚绑定问题。
- `_ask()` 在 `5996` 遇到 `_asking` 就无提示返回;卡片本身仍保留可点击外观,忙碌期间的工具选择被直接丢弃。
- `_stream_event(done)` 在 `6130-6143` 标记完成、停止总超时与耗时计时、渲染最终内容,但不清除 `_asking`,也不启用发送按钮。
- `_asking` 只在 `_stream_finished()` 的 `6196-6197` 恢复。`_AiStreamWorker.run()` 在 `3185-3188` 对 done 仍然继续迭代,直到底层迭代器退出才会进入 `finally` 发出 finished。
- 因而“已显示最终答案”和“允许再次点击工具”不是同一个时点。done 后迭代器或连接清理阻塞时,工具无反应可以持续;同时总超时已经停止,手工调用 `_stream_timed_out()` 也会被 `_stream_completed` 门控直接返回。
- 当前 `RemoteDoctorRepository.stream_diagnosis_ai()` 在 yield done 后有 `return`(`repository.py:1438-1442`),通常会很快触发 finished,所以不能仅凭该缺陷断言真实部署每次都会永久卡住。确定的客户端缺陷是 UI 终态仍依赖传输退出,并且在此期间没有超时恢复。
### 2. 已确认:返回、侧栏 ×、Escape 的 reject 路径绕过流清理
- 返回与侧栏 × 绑定 `self.reject`(`3710`、`3966`),但唯一关闭清理在 `closeEvent()`(`6231-6237`);没有覆盖 `done()` / `reject()` 或连接 `finished` 作统一清理。
- Qt 的 `QDialog.reject()` 隐藏对话框并结束 exec,不触发此处的 closeEvent 清理。离线调用已证实:reject 后 `_asking=True`、worker 未取消、两个流计时器仍运行、generation 未变化。
- `open_for()` 会先 `_cancel_stream()` 并递增会话 generation(`3601-3608`),所以复用同一对话框且按标准 open_for 重开时能够隔离旧回调。`present_ai_consult()` 每次新建一个对话框,旧对话框带父对象时隐藏后仍可能被保留,造成后台 worker / timer 继续存在。
- 这是生命周期缺陷;它本身不能证明同一已显示窗口的其他工具永久失效,但应与本次门控修复一起覆盖。
### 3. 其他退出路径
- `_stream_failed()` 与 done 相同,停止计时并显示错误,却只靠稍后的 finished 解锁。实际 worker 的 except 紧接 finally,常规错误路径间隙很短;仍建议终态入口统一恢复 UI 状态。
- `_stream_finished()` 对 generation / stream_generation 作检查;`_cancel_stream()` 与 `open_for()` 的 generation 隔离正确,迟到旧事件不会覆盖新流。
- 取消仅设置线程 Event,无法中断正在阻塞的 next()/网络读取;因此 UI 必须先解除对旧 worker 的依赖,再由 worker 完成清理。
- 未发现六个 prompt 绑定错误。连续点击无反馈的直接入口是 `_ask()` 的无提示忙碌返回,而非按钮失去信号连接。
## 离线复现
使用 `app/.venv/Scripts/python.exe`、`QT_QPA_PLATFORM=offscreen`,以 stdin 临时脚本运行,未新增或修改业务 / 测试文件。
1. 将线程池 start 替换成仅记录 worker 的函数,点击第一张工具卡,直接投递 done,不投递 finished,再点第二张卡。
```json
{"case":"done_before_finished","asking":true,"send_enabled":false,"timeout_active":false,"completed":true,"second_click_started_worker":false}
{"case":"forced_timeout_after_done","asking":true}
```
补发 finished 后再次点第二张卡,第二个 worker 正常创建。
2. 使用真实 QThreadPool / QRunnable 与完全本地的假 repository。假生成器 yield done 后在 Event 上等待,模拟终态数据已到但迭代器未退出。UI 事件循环实际收到 done 后立即发起第二问:
```json
{"case":"real_qrunnable_done_then_open_iterator","done_delivered":true,"iterator_resumed_after_done":true,"asking":true,"timeout_active":false,"prompts":["first"]}
{"case":"iterator_released","asking":false,"send_enabled":true}
```
3. fake pool 下 show 对话框并开始一问,调用实际 `dialog.reject()`:
```json
{"case":"reject","visible":false,"asking":true,"worker_cancelled":false,"generation_unchanged":true,"stream_generation_unchanged":true,"elapsed_timer_active":true,"timeout_active":true}
{"case":"open_for_after_reject","worker_cancelled":true,"asking":false,"generation_changed":true}
```
所有复现脚本 exit code 0,未访问 API。
## 建议的最小修复
1. 将 UI 终态处理集中为幂等 helper,确保 done、error、silent finished 都能停止计时、冲刷待显示文本、清除 `_asking` 并恢复发送按钮;done 不等待网络或 worker 退出。保留已完成气泡 / 元数据 / 最终耗时,避免用会清空这些字段的 `_cancel_stream()` 直接处理 done。
2. worker 将有效 done 视为终止事件,发出后不再拉取下一项;在 finally 中安全关闭拥有 close() 的迭代器。即使底层关闭有延迟,UI 在步骤 1 已恢复。
3. 选定忙碌时点击工具的明确策略:若允许切换,则工具入口取消当前流并发起新的工具请求;若保持串行,则提供可见的忙碌反馈或禁用状态。当前静默吞点击不利于判断应用是否失效。不要误将旧 finished 用于解锁新请求,继续依赖 stream_generation 隔离。
4. 在 `done(result)` 或其他统一的对话框结束入口中执行 generation 失效与 `_cancel_stream()`,覆盖 reject / accept / Escape / 窗口关闭;确保和 closeEvent 重入时幂等。
## 建议回归情境
- 按截图六个工具依次真实点击(包含卡片文字和图标区域),每个 done 到达即点下一个,不提前补发 finished;每次 prompt 与 worker 都应正确创建。
- done 后迭代器保持打开 / cleanup 延迟:UI 必须允许下一问,旧 worker 的迟到 delta/error/finished 不得改动新气泡或错误解锁新请求。
- 正在首帧等待或生成中点击另一工具,验证所选忙碌策略且用户可理解当前状态。
- error 到达但 finished 延迟、无 done 的静默结束、总超时、切换患者后迟到信号,均应恢复交互且不串患者。
- 对返回按钮、侧栏 ×、Escape、原生窗口关闭分别验证 worker 已取消、计时器停止、旧 generation 无效;重开后新请求正常。
- 保留首字即时显示、40ms 后续合并、完成后结构化渲染、首字/总耗时固定、一次性全量上下文提示的现有行为。
现有 `test_answered_stream_replaces_the_pending_placeholder` 与耗时完成测试在 done 后立即手工调用 finished,掩盖终态间隙;现有关闭耗时测试用 `dialog.close()`,未覆盖 reject 路径。