Skip to content

[windows] 快速连续听写时 clipboard restore 用错上一次的 previous_text #167

@appergb

Description

@appergb

现象

Windows 上短时间内连续两次 dictation(间隔 < 750ms),第一次的 "恢复剪贴板" 后台 thread 还在 sleep 中,第二次 dictation 又起一个新的恢复 thread。两个 thread 各自持有自己的 `previous_text` snapshot,但都对同一个系统剪贴板写——后跑的可能用前一个 session 的"原剪贴板"覆盖刚刚听写的内容。

观察现象:

  1. 用户连听两段(间隔 ~500ms)
  2. 第二段刚粘贴成功
  3. 突然剪贴板内容变回某个很早之前用户复制的字(第一次 dictation 时的 previous_text snapshot)

复现

需要快速操作:

  1. 复制文本 A(剪贴板 = A)
  2. 听写一段(粘贴成功,剪贴板暂时 = 听写 1)
  3. 300ms 内再听写一段(粘贴成功,剪贴板暂时 = 听写 2)
  4. 等 1.5s
  5. 观察剪贴板:可能不是 A,而是 "听写 1" 或某个 stale 值

底层是 race,复现率不稳定但确实存在(Win 750ms restore 比 mac 长得多,更容易撞上)。

根因

insertion.rs:144 `schedule_clipboard_restore` 是无界 spawn:

```rust
fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
std::thread::spawn(move || restore_clipboard_after_delay(plan, CLIPBOARD_RESTORE_DELAY));
}
```

每次 dictation 都起一个新 thread,无去重 / 无队列。两个 plan 各自有自己的 `previous_text` snapshot,但都跑 `should_restore_clipboard()` + `clipboard.set_text(plan.previous_text)` 写同一个系统剪贴板。

如果 thread 1 在 thread 2 之后 fire(即第二次 dictation 因为 spawn 时机晚一点点反而后跑),最终剪贴板会是 thread 1 的 previous_text — 那个值是 dictation 1 之前的剪贴板(=A),看起来"对"。但若 thread 1 先 fire(更常见,毕竟早 spawn 早 sleep 完),剪贴板被恢复成 A,然后 thread 2 fire,写的是 dictation 1 的结果(thread 2 的 previous_text 是 dictation 1 完成时的剪贴板)。

影响

  • 平台:Windows(Linux 同样有,但 delay 短覆盖率低)
  • 频率:用户快速连听写场景;不常见但当出现时相当迷惑
  • 体感:剪贴板莫名其妙变回某个历史值

建议 fix

单线程 restore 队列 / 全局 state:

```rust
// 要么:取消上一次未跑的 restore
static RESTORE_PENDING: AtomicBool = AtomicBool::new(false);

fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
// 如果已有 pending,直接 drop 这次 plan 的 previous_text(用户已知前一段被忽略)
if RESTORE_PENDING.swap(true, Ordering::SeqCst) {
log::info!("[insertion] previous restore still pending, dropping new plan");
return;
}
std::thread::spawn(move || {
restore_clipboard_after_delay(plan, CLIPBOARD_RESTORE_DELAY);
RESTORE_PENDING.store(false, Ordering::SeqCst);
});
}
```

或者更精细:维护一个 `Mutex<Option>`,新 plan 来时先 join/cancel 旧的。但 std::thread 不能 cancel,可以用 oneshot channel 通知。

简单优先 — 上面那段够用了。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingwindowsWindows-specific issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions