|
| 1 | +# v0.12.3:浮窗死锁修复 + 浮窗交互/架构升级 + 精修工作台契约修复 |
| 2 | + |
| 3 | +> 状态:代码已交付(2026-08-23,两项真机验收 P0 + 浮窗交互/架构升级同版交付) |
| 4 | +> 依据:用户对话 2026-08-23(① 采集中点「🗕 浮窗化」→ 额外出现空白界面 + 全应用不可交互,ASR 仍正常; |
| 5 | +> ② 精修工作台打开即崩:`Cannot read properties of undefined (reading 'split')`,栈 W2/E5 = renderMd/RefineWorkbench) |
| 6 | +> 前置:v0.12.1(OCR 回归修复)真机验收中;v0.12.2(笔记页信息架构重构)规划未启动,顺延为后续版本候选 |
| 7 | +
|
| 8 | +## 一句话 |
| 9 | + |
| 10 | +两个 **P0 真机验收雷**同版修复: |
| 11 | + |
| 12 | +- **浮窗/覆盖层窗口创建死锁(wry#583)**:同步 command 在 WebView2 IPC 回调(主线程)内执行 |
| 13 | + `WebviewWindowBuilder::build()`——`CreateCoreWebView2ControllerWithOptions` 完成回调依赖主线程派发, |
| 14 | + 而主线程正阻塞在回调内等待它(循环等待)。症状与"空白界面 + 全应用不可点击、ASR 照常"完全对应 |
| 15 | + (引擎跑独立线程池;Win32 窗口先创建后 webview 初始化卡死 → 空白)。**修复:窗口命令全部 async** |
| 16 | + (Tauri 官方文档要求的模式,官方注释原文:"On Windows, this function deadlocks when used in a |
| 17 | + synchronous command and event handlers … use `async` commands and separate threads")。 |
| 18 | +- **精修工作台契约漏网(serde camelCase)**:`WorkbenchData` 是 `commands_ai_refine.rs` 中唯一缺失 |
| 19 | + `#[serde(rename_all = "camelCase")]` 的结构体——Rust 输出 `rule_markdown`(snake_case),前端按 |
| 20 | + `ruleMarkdown` 读取恒为 `undefined` → `renderMd(undefined).split` 崩溃。**修复:回填 rename 契约 |
| 21 | + + Rust 序列化单测 + 前端防御(`?? ""` 兜底)**。 |
| 22 | + |
| 23 | +同时交付浮窗**交互层**(P1:拖拽+位置记忆+边缘吸附、面板/字幕条双形态、点击穿透锁定、 |
| 24 | +置顶开关、透明度、回主窗升级)与**架构层**(P2:常驻预创建、窗口状态收敛、capabilities 拆分)。 |
| 25 | + |
| 26 | +## 范围 |
| 27 | + |
| 28 | +| # | 项目 | 类型 | 落点 | |
| 29 | +|---|------|------|------| |
| 30 | +| 1 | 浮窗/覆盖层窗口创建死锁(wry#583) | **P0** 修复 | `commands_window.rs` / `commands_overlay.rs`——建窗/关窗命令 async 化 | |
| 31 | +| 2 | 精修工作台 undefined.split 崩溃 | **P0** 修复 | `commands_ai_refine.rs`(serde rename_all)+ `RefineWorkbench.tsx`(防御)+ 单测 | |
| 32 | +| 3 | 浮窗双形态 + 拖拽/吸附/持久化 | P1 交互 | `useFloatWindow.ts`(新 hook)+ `utils/floatWindow.ts`(纯函数)+ `CaptureFloatPanel.tsx` 重写 | |
| 33 | +| 4 | 点击穿透锁定 + 主窗解锁 + 置顶/透明度 | P1 交互 | 同上 + `ClassroomPage.tsx` 三态按钮(浮窗化 ⇄ 收起 ⇄ 解锁) | |
| 34 | +| 5 | 回主窗升级(show+focus 主窗,浮窗保留) | P1 交互 | `show_main_window` 命令 | |
| 35 | +| 6 | 浮窗常驻预创建 + 状态收敛 + capabilities 拆分 | P2 架构 | `app_setup.rs`(预创建)+ `AppState.float_ui` + `capabilities/{float,overlay}.json` | |
| 36 | +| 7 | 文档与治理 | 伴随 | 本版本文档 + CHANGELOG + 索引 + 版本号 0.12.3 | |
| 37 | + |
| 38 | +## 根因(M1:浮窗死锁) |
| 39 | + |
| 40 | +- **调用链**:前端 `invoke("open_capture_float")` → WebView2 `WebMessageReceived` **主线程**派发 → |
| 41 | + Tauri 在该回调内**同步执行** command(`commands_window.rs` 原为 `#[tauri::command] pub fn`)→ |
| 42 | + `WebviewWindowBuilder::build()` 需要创建第二个 WebView2 控件。 |
| 43 | +- **死锁点**:wry 0.55.1 `create_controller` 调用 `CreateCoreWebView2ControllerWithOptions` 后用 |
| 44 | + `webview2_com::wait_with_pump(rx)` **同步阻塞等待**完成回调;该回调需主线程派发,而主线程正阻塞 |
| 45 | + 在回调内等它 → 循环等待(wry#583:"deadlock on the create_controller function (the second closure |
| 46 | + is never called)";维护者结论:"document that it must use async or create thread to spawn another |
| 47 | + window on Windows")。 |
| 48 | +- **症状映射**:tao 窗口先创建并显示(空白)→ webview 初始化卡死 → 主线程永久阻塞 → 主窗按钮不可点 |
| 49 | + (Win32 消息/点击全走主线程);ASR/OCR 跑独立引擎线程池不受影响 → "ASR 仍正常"。 |
| 50 | +- **同源隐患**:`commands_overlay.rs` 的 M3 覆盖层窗口是同一种模式(同步 command + builder), |
| 51 | + 本版一并修复(v0.12.0 真机验收时已发现但未执行覆盖层路径,属同类雷)。 |
| 52 | +- 证据:真实运行包(release exe 内嵌 brotli asset)反编译核对 + tauri 2.11.5 源码文档警告 |
| 53 | + (`webview_window.rs` L56-59)+ wry#583 issue 原文。 |
| 54 | + |
| 55 | +## 根因(M2:精修工作台崩溃) |
| 56 | + |
| 57 | +- `refine_workbench` 返回的 `WorkbenchData` 顶层键为 snake_case(`rule_markdown`/`refined_markdown`), |
| 58 | + 前端 `WorkbenchData` 类型与读取均为 camelCase(`ruleMarkdown`)→ 运行时 `undefined`。 |
| 59 | +- `RefineWorkbench.tsx` 渲染入口 `renderMd(wb.ruleMarkdown)` 无防御 → `undefined.split("\n")` 崩。 |
| 60 | +- 漏检原因:同模块所有兄弟结构体(AiRefineResult/CostEstimate/RefineEstimateView)均带 |
| 61 | + `#[serde(rename_all = "camelCase")]`,唯独 WorkbenchData(v0.11.5 新增)遗漏;该面板未进 |
| 62 | + v0.11.5 真机验收路径 → "真机验收待执行" 的又一实证。 |
| 63 | +- 附带说明:嵌套 `SectionDiff` 保持 snake_case 是**刻意契约**(首次出现即定,前端类型注释"勿改"), |
| 64 | + 本次 rename 只作用于顶层键,不影响嵌套字段。 |
| 65 | + |
| 66 | +## 交付内容 |
| 67 | + |
| 68 | +### M3 · 浮窗交互层(P1) |
| 69 | + |
| 70 | +- **双形态**:面板(360×240 全功能)⇄ 字幕条(360×44 只读近况),Esc 或 ⤡/⤢ 切换; |
| 71 | + 形态联动程序化窗口尺寸(`resizable=false` 不影响 `setSize`)。 |
| 72 | +- **拖拽 + 位置记忆 + 边缘吸附**:顶部空白处拖拽(`startDragging`);移动结束 250ms 去抖 → |
| 73 | + 边缘吸附(左/右/上/下 ≤8px 贴边)+ 位置持久化(localStorage,浮窗/主窗同源共享免 IPC); |
| 74 | + 挂载时按当前工作区钳制坐标(多屏变化防窗口"丢失")。 |
| 75 | +- **点击穿透锁定**:🔓/🔒 切换 `set_ignore_cursor_events`——只读悬浮不拦截鼠标(看视频零遮挡); |
| 76 | + 锁定态浮窗自身不可点,**解锁路径在主窗**(按钮变「🔓 解锁浮窗」/ Ctrl+Shift+F 同语义)。 |
| 77 | +- **置顶开关**(📌/📍)+ **透明度滑杆**(35%–100%,钳制防越界)。 |
| 78 | +- **回主窗升级**:`show_main_window`——显示 + 还原 + 聚焦主窗,**浮窗保留**(不再销毁)。 |
| 79 | +- `ClassroomPage` 浮窗按钮三态语义(浮窗化 ⇄ 收起 ⇄ 解锁),状态经 `float_state` 查询 + |
| 80 | + `float:state` 事件订阅(Rust 单一来源)。 |
| 81 | + |
| 82 | +### M4 · 浮窗架构层(P2) |
| 83 | + |
| 84 | +- **常驻预创建**:setup 预创建隐藏浮窗(P2-10:打开秒显、点击期零建窗风险);失败幂等回落为 |
| 85 | + 打开时懒创建(open_capture_float 兜底)。close 语义从销毁改为**隐藏**(保留事件订阅)。 |
| 86 | +- **状态收敛**:浮窗 UI 状态(locked/topmost)收进 `AppState.float_ui`(`set_ignore_cursor_events` |
| 87 | + 无 getter 必须自存);全部窗口操作集中在 `commands_window.rs`(单文件职责:open/close/locked/ |
| 88 | + topmost/state/show_main)。 |
| 89 | +- **capabilities 拆分**:`default.json`(main 仅保留)→ `float.json` / `overlay.json` |
| 90 | + (各自最小权限、无 dialog)。 |
| 91 | + |
| 92 | +### M5 · 防御性编程(随 M2 一并) |
| 93 | + |
| 94 | +- `RefineWorkbench.tsx`:`ruleMarkdown ?? ""`、`refinedMarkdown ?? null`、`sections ?? []`、 |
| 95 | + `stats ?? {0,0,0}`——后端字段缺失/契约漂移时渲染不崩(AGENTS.md 维度 4)。 |
| 96 | + |
| 97 | +## 验证 |
| 98 | + |
| 99 | +- Rust:`workbench_data_serializes_camel_case_top_level` 单测(顶层键 camelCase + 嵌套 |
| 100 | + SectionDiff 保持 snake_case 的契约锚定);`cargo check --all-targets` 通过(0 error,既有 warning 不变)。 |
| 101 | +- 前端:`tsc --noEmit` 零错误;`vitest run` **81 通过**(新增 `floatWindow.test.ts` 14 项: |
| 102 | + 钳制/吸附边界/偏好往返/损坏回退/透明度钳制/部分字段合并)。 |
| 103 | +- 真机验收路径(待执行): |
| 104 | + - 浮窗:采集中 Ctrl+Shift+F → 浮窗显示内容、主窗按钮仍可点;拖拽移角落自动吸附;重启后位置记忆; |
| 105 | + Esc 切换字幕条;🔒 后主窗「解锁浮窗」恢复;滑杆透明度生效;停止采集浮窗隐藏;再开秒显。 |
| 106 | + - 覆盖层:图文采集框选全链路(与浮窗同因修复,一并验收)。 |
| 107 | + - 精修工作台:任意会话打开工作台 → 规则版/精修版双栏渲染正常;无精修时右栏"尚未精修"提示。 |
| 108 | + |
| 109 | +## 已知问题与后续 |
| 110 | + |
| 111 | +- **独占全屏限制**:Windows 独占全屏(DXGI exclusive)下 alwaysOnTop 不可见(系统级限制), |
| 112 | + 全屏观看需走无边框窗口化——浮窗异常态提示文案未做(记需求池,真机验收后按反馈门控)。 |
| 113 | +- **常驻浮窗内存**:预创建常驻一个 WebView2(约 40-80MB)换取秒开——P2-10 决策在文、可回退为懒创建。 |
| 114 | +- **事件双订阅**:主窗与浮窗各自订阅 live:*(两 webview 独立状态);评估后判定收益低 |
| 115 | + (事件低频 × 状态一致——同源同事件),未改共享管线;若未来出现漂移再收敛为 Rust 侧摘要转发。 |
| 116 | +- **v0.12.2 顺延**:笔记页信息架构重构(三栏+收件箱动线)规划未启动,作为 v0.12.4 候选。 |
| 117 | + |
| 118 | +## 关联 |
| 119 | + |
| 120 | +- 市场调研:浮窗方案谱系(独立置顶子窗口 ✅ / 点击穿透只读叠加层 / acrylic 材质 / 系统级 Overlay ❌ YAGNI / |
| 121 | + 托盘化 / 位置记忆吸边)——竞品对标:Granola(转录浮窗指示器)、OBS Fullscreen Projector |
| 122 | + Always-On-Top + 点击穿透实践、Fluent Screen Recorder(悬浮控制条)。 |
| 123 | +- 依据链路:用户浮窗 bug 报告 → 源码证据链(tauri-runtime-wry `send_user_message` 主线程内联路径 / |
| 124 | + wry `create_controller` wait_with_pump / wry#583 / tauri 官方文档警告)→ async 修复。 |
| 125 | +- ADR:无(行为修复 + 交互增强,架构方向未变;常驻浮窗为已裁决 P2-10 落地)。 |
0 commit comments