Skip to content

Latest commit

 

History

History
125 lines (101 loc) · 9.82 KB

File metadata and controls

125 lines (101 loc) · 9.82 KB

v0.12.3:浮窗死锁修复 + 浮窗交互/架构升级 + 精修工作台契约修复

状态:代码已交付(2026-08-23,两项真机验收 P0 + 浮窗交互/架构升级同版交付) 依据:用户对话 2026-08-23(① 采集中点「🗕 浮窗化」→ 额外出现空白界面 + 全应用不可交互,ASR 仍正常; ② 精修工作台打开即崩:Cannot read properties of undefined (reading 'split'),栈 W2/E5 = renderMd/RefineWorkbench) 前置:v0.12.1(OCR 回归修复)真机验收中;v0.12.2(笔记页信息架构重构)规划未启动,顺延为后续版本候选

一句话

两个 P0 真机验收雷同版修复:

  • 浮窗/覆盖层窗口创建死锁(wry#583):同步 command 在 WebView2 IPC 回调(主线程)内执行 WebviewWindowBuilder::build()——CreateCoreWebView2ControllerWithOptions 完成回调依赖主线程派发, 而主线程正阻塞在回调内等待它(循环等待)。症状与"空白界面 + 全应用不可点击、ASR 照常"完全对应 (引擎跑独立线程池;Win32 窗口先创建后 webview 初始化卡死 → 空白)。修复:窗口命令全部 async (Tauri 官方文档要求的模式,官方注释原文:"On Windows, this function deadlocks when used in a synchronous command and event handlers … use async commands and separate threads")。
  • 精修工作台契约漏网(serde camelCase):WorkbenchData 是 commands_ai_refine.rs 中唯一缺失 #[serde(rename_all = "camelCase")] 的结构体——Rust 输出 rule_markdown(snake_case),前端按 ruleMarkdown 读取恒为 undefined → renderMd(undefined).split 崩溃。**修复:回填 rename 契约
    • Rust 序列化单测 + 前端防御(?? "" 兜底)**。

同时交付浮窗交互层(P1:拖拽+位置记忆+边缘吸附、面板/字幕条双形态、点击穿透锁定、 置顶开关、透明度、回主窗升级)与架构层(P2:常驻预创建、窗口状态收敛、capabilities 拆分)。

范围

# 项目 类型 落点
1 浮窗/覆盖层窗口创建死锁(wry#583) P0 修复 commands_window.rs / commands_overlay.rs——建窗/关窗命令 async 化
2 精修工作台 undefined.split 崩溃 P0 修复 commands_ai_refine.rs(serde rename_all)+ RefineWorkbench.tsx(防御)+ 单测
3 浮窗双形态 + 拖拽/吸附/持久化 P1 交互 useFloatWindow.ts(新 hook)+ utils/floatWindow.ts(纯函数)+ CaptureFloatPanel.tsx 重写
4 点击穿透锁定 + 主窗解锁 + 置顶/透明度 P1 交互 同上 + ClassroomPage.tsx 三态按钮(浮窗化 ⇄ 收起 ⇄ 解锁)
5 回主窗升级(show+focus 主窗,浮窗保留) P1 交互 show_main_window 命令
6 浮窗常驻预创建 + 状态收敛 + capabilities 拆分 P2 架构 app_setup.rs(预创建)+ AppState.float_ui + capabilities/{float,overlay}.json
7 文档与治理 伴随 本版本文档 + CHANGELOG + 索引 + 版本号 0.12.3

根因(M1:浮窗死锁)

  • 调用链:前端 invoke("open_capture_float") → WebView2 WebMessageReceived 主线程派发 → Tauri 在该回调内同步执行 command(commands_window.rs 原为 #[tauri::command] pub fn)→ WebviewWindowBuilder::build() 需要创建第二个 WebView2 控件。
  • 死锁点:wry 0.55.1 create_controller 调用 CreateCoreWebView2ControllerWithOptions 后用 webview2_com::wait_with_pump(rx) 同步阻塞等待完成回调;该回调需主线程派发,而主线程正阻塞 在回调内等它 → 循环等待(wry#583:"deadlock on the create_controller function (the second closure is never called)";维护者结论:"document that it must use async or create thread to spawn another window on Windows")。
  • 症状映射:tao 窗口先创建并显示(空白)→ webview 初始化卡死 → 主线程永久阻塞 → 主窗按钮不可点 (Win32 消息/点击全走主线程);ASR/OCR 跑独立引擎线程池不受影响 → "ASR 仍正常"。
  • 同源隐患:commands_overlay.rs 的 M3 覆盖层窗口是同一种模式(同步 command + builder), 本版一并修复(v0.12.0 真机验收时已发现但未执行覆盖层路径,属同类雷)。
  • 证据:真实运行包(release exe 内嵌 brotli asset)反编译核对 + tauri 2.11.5 源码文档警告 (webview_window.rs L56-59)+ wry#583 issue 原文。

根因(M2:精修工作台崩溃)

  • refine_workbench 返回的 WorkbenchData 顶层键为 snake_case(rule_markdown/refined_markdown), 前端 WorkbenchData 类型与读取均为 camelCase(ruleMarkdown)→ 运行时 undefined。
  • RefineWorkbench.tsx 渲染入口 renderMd(wb.ruleMarkdown) 无防御 → undefined.split("\n") 崩。
  • 漏检原因:同模块所有兄弟结构体(AiRefineResult/CostEstimate/RefineEstimateView)均带 #[serde(rename_all = "camelCase")],唯独 WorkbenchData(v0.11.5 新增)遗漏;该面板未进 v0.11.5 真机验收路径 → "真机验收待执行" 的又一实证。
  • 附带说明:嵌套 SectionDiff 保持 snake_case 是刻意契约(首次出现即定,前端类型注释"勿改"), 本次 rename 只作用于顶层键,不影响嵌套字段。

交付内容

M3 · 浮窗交互层(P1)

  • 双形态:面板(360×240 全功能)⇄ 字幕条(360×44 只读近况),Esc 或 ⤡/⤢ 切换; 形态联动程序化窗口尺寸(resizable=false 不影响 setSize)。
  • 拖拽 + 位置记忆 + 边缘吸附:顶部空白处拖拽(startDragging);移动结束 250ms 去抖 → 边缘吸附(左/右/上/下 ≤8px 贴边)+ 位置持久化(localStorage,浮窗/主窗同源共享免 IPC); 挂载时按当前工作区钳制坐标(多屏变化防窗口"丢失")。
  • 点击穿透锁定:🔓/🔒 切换 set_ignore_cursor_events——只读悬浮不拦截鼠标(看视频零遮挡); 锁定态浮窗自身不可点,解锁路径在主窗(按钮变「🔓 解锁浮窗」/ Ctrl+Shift+F 同语义)。
  • 置顶开关(📌/📍)+ 透明度滑杆(35%–100%,钳制防越界)。
  • 回主窗升级:show_main_window——显示 + 还原 + 聚焦主窗,浮窗保留(不再销毁)。
  • ClassroomPage 浮窗按钮三态语义(浮窗化 ⇄ 收起 ⇄ 解锁),状态经 float_state 查询 + float:state 事件订阅(Rust 单一来源)。

M4 · 浮窗架构层(P2)

  • 常驻预创建:setup 预创建隐藏浮窗(P2-10:打开秒显、点击期零建窗风险);失败幂等回落为 打开时懒创建(open_capture_float 兜底)。close 语义从销毁改为隐藏(保留事件订阅)。
  • 状态收敛:浮窗 UI 状态(locked/topmost)收进 AppState.float_ui(set_ignore_cursor_events 无 getter 必须自存);全部窗口操作集中在 commands_window.rs(单文件职责:open/close/locked/ topmost/state/show_main)。
  • capabilities 拆分:default.json(main 仅保留)→ float.json / overlay.json (各自最小权限、无 dialog)。

M5 · 防御性编程(随 M2 一并)

  • RefineWorkbench.tsx:ruleMarkdown ?? ""、refinedMarkdown ?? null、sections ?? []、 stats ?? {0,0,0}——后端字段缺失/契约漂移时渲染不崩(AGENTS.md 维度 4)。

验证

  • Rust:workbench_data_serializes_camel_case_top_level 单测(顶层键 camelCase + 嵌套 SectionDiff 保持 snake_case 的契约锚定);cargo check --all-targets 通过(0 error,既有 warning 不变)。
  • 前端:tsc --noEmit 零错误;vitest run 81 通过(新增 floatWindow.test.ts 14 项: 钳制/吸附边界/偏好往返/损坏回退/透明度钳制/部分字段合并)。
  • 真机验收路径(待执行):
    • 浮窗:采集中 Ctrl+Shift+F → 浮窗显示内容、主窗按钮仍可点;拖拽移角落自动吸附;重启后位置记忆; Esc 切换字幕条;🔒 后主窗「解锁浮窗」恢复;滑杆透明度生效;停止采集浮窗隐藏;再开秒显。
    • 覆盖层:图文采集框选全链路(与浮窗同因修复,一并验收)。
    • 精修工作台:任意会话打开工作台 → 规则版/精修版双栏渲染正常;无精修时右栏"尚未精修"提示。

已知问题与后续

  • 独占全屏限制:Windows 独占全屏(DXGI exclusive)下 alwaysOnTop 不可见(系统级限制), 全屏观看需走无边框窗口化——浮窗异常态提示文案未做(记需求池,真机验收后按反馈门控)。
  • 常驻浮窗内存:预创建常驻一个 WebView2(约 40-80MB)换取秒开——P2-10 决策在文、可回退为懒创建。
  • 事件双订阅:主窗与浮窗各自订阅 live:*(两 webview 独立状态);评估后判定收益低 (事件低频 × 状态一致——同源同事件),未改共享管线;若未来出现漂移再收敛为 Rust 侧摘要转发。
  • v0.12.2 顺延:笔记页信息架构重构(三栏+收件箱动线)规划未启动,作为 v0.12.4 候选。

关联

  • 市场调研:浮窗方案谱系(独立置顶子窗口 ✅ / 点击穿透只读叠加层 / acrylic 材质 / 系统级 Overlay ❌ YAGNI / 托盘化 / 位置记忆吸边)——竞品对标:Granola(转录浮窗指示器)、OBS Fullscreen Projector Always-On-Top + 点击穿透实践、Fluent Screen Recorder(悬浮控制条)。
  • 依据链路:用户浮窗 bug 报告 → 源码证据链(tauri-runtime-wry send_user_message 主线程内联路径 / wry create_controller wait_with_pump / wry#583 / tauri 官方文档警告)→ async 修复。
  • ADR:无(行为修复 + 交互增强,架构方向未变;常驻浮窗为已裁决 P2-10 落地)。