Skip to content

feat(dictation): 落字失败时把话交还给用户,换行不再劈开聊天消息 - #943

Open
bigsongeth wants to merge 1 commit into
Open-Less:betafrom
bigsongeth:fix/macos-streaming-newline-sends-message
Open

feat(dictation): 落字失败时把话交还给用户,换行不再劈开聊天消息#943
bigsongeth wants to merge 1 commit into
Open-Less:betafrom
bigsongeth:fix/macos-streaming-newline-sends-message

Conversation

@bigsongeth

@bigsongeth bigsongeth commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

两件事,本质是同一个:说出来的话没落到该去的地方。

一、换行会把聊天消息从中间劈开发出去

说一段话,润色把它分成了两段。逐字上屏到微信的时候,中间那个换行直接把上半句当成一条消息发了出去,下半句留在输入框里。只能撤回。

原因是 macOS 把 U+000A 当 Return 键,而聊天框里 Return 就是「发送」。

改成发真实的 Shift+Return——聊天框里是软换行不发送,编辑器、终端、网页输入框里就是普通换行。

做成设置而不是硬编码:风格市场里有靠换行把一段话拆成多条消息的风格包,那种效果要的正是真回车。默认 Shift+Return,需要的人在设置里切回 Return(只在 macOS + 开了流式插入时显示,结构对齐已有的 WindowsSendInputNewlineMode)。

二、落字失败时,那段话会悄无声息地消失

密码框挡着、辅助功能掉了权限、粘贴被拒——这些时候文本落不进去,而唯一的兜底是把它写进剪贴板。

问题是:这个兜底依赖一个默认可关的开关;而且就算开着,屏幕上也没有任何东西告诉你文本在剪贴板里。你看到的只是胶囊一闪而过,然后是个空输入框,话就这么没了。

现在屏幕底部会弹一张卡片,里面是完整的那段话,一键复制。20 秒后自己消失,鼠标停在上面就不走(你还在读的时候它不该跑)。

实现

技术细节

换行unicode_keystroke.rs):用 kVK_Return 虚拟键码 + shift flag,代替原来 CGEventKeyboardSetUnicodeString 送 U+000A 的做法。\r 吞掉不发键(CRLF 里它只是 LF 的前缀,发出去会变成两个换行),但仍计入 typed_chars——调用方拿它和 delta 的字符数比对,少一个就会判定「部分失败」并丢弃后续所有 delta。顺带把重复的 CGEvent 资源管理收口到一个 post_key_event

卡片触发InsertStatus::CopiedFallback | FailedPasteSent 是 Windows / Linux 上的成功语义,不能算失败,否则每次正常听写都会弹卡片。

逐字上屏打到一半断掉时,run_streaming_polish 把完整文本放进 Inner::insert_fallback_text,收尾据此把 status 从 Inserted 纠正为 CopiedFallback——此前那里一律报成功,history 里的 insertStatus 也跟着失真。

卡片内容是完整文本,不是屏幕上那半截。final_text 遵守「所见即所存」的约定(#412 确立),但用户要拿回的是整段。

卡片窗口复用词条建议卡片那一套(复用胶囊窗口、关穿透、缩尺寸、重定位),包括两个容易漏的缓存同步:capsule_cursor_passthrough 不同步,后续 emit_capsule 会以为「没变化」而跳过该调的那次,表现是胶囊之后一直挡着屏幕;capsule_layout 不清,下次录音时胶囊会留在卡片挪过去的位置。

时序:卡片在会话收尾那一刻弹出,而收尾自己安排了一次 idle 隐藏。照搬 selection_polish_capsule_active 已有的让路机制,否则卡片刚出现就被自己这轮会话的尾巴收掉。

复制走后端而不是 navigator.clipboard:卡片浮在别的 app 上,按钮刻意不抢焦点(抢了就把用户正在写的地方的光标弄没了),而未聚焦的文档调那个 API 会抛 Document is not focused

测试

cargo test --lib 1119 通过,tsc --noEmit 通过。

还没验证的

  • 换行修复没在真机上撞到:需要润色恰好产出换行才能复现,日常用着碰上就知道了
  • 卡片的窗口高度是估算的:按 360pt 宽、13px 字号推算行数。估矮了正文被裁在窗口外,估高了窗口会空出一块——而卡片显示期间窗口不穿透鼠标,那块空白会拦住底下的点击
  • Shift+Return 在编辑器类 app 里和 Return 的行为差异(例如 Notes 的列表项),这是默认值的已知取舍
  • Linux 流式路径走 enigo,是否有同样的换行问题没排查,不在本 PR 范围

@bigsongeth bigsongeth changed the title fix(streaming): macOS 逐字上屏的换行会把聊天消息提前发出去 fix(streaming): macOS 逐字上屏的换行会把聊天消息提前发出去 + 落字失败兜底卡片 Aug 11, 2026
@bigsongeth bigsongeth changed the title fix(streaming): macOS 逐字上屏的换行会把聊天消息提前发出去 + 落字失败兜底卡片 feat(dictation): 落字失败时不再默默丢字——兜底卡片交还整段话 Aug 11, 2026
@H-Chris233

H-Chris233 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

每次落字前确认焦点还在原来那个 app

我首先怀疑性能,确认焦点需要调用系统API,每个字调用一次会不会造成较大的性能开销?
其次是这个逻辑请确保只在落字的时候触发,因为已经观察到有些用户的使用习惯是说话同时进行其他操作,最终回到输入框进行落字,为了不影响这种使用习惯,请确保卡片逻辑只会在落字时触发
还有就是换行那个还需要考虑到风格市场里面的利用这种逻辑进行消息轰炸的风格包( @appergb 执迷于那玩意),可以给开关然后 @appergb 更改风格包说明使使用用户去设置里切换为需要的换行模式
(为什么我一个原作者comment全是手打的
(我真牛逼

@bigsongeth bigsongeth changed the title feat(dictation): 落字失败时不再默默丢字——兜底卡片交还整段话 feat(dictation): 落字失败时不再默默丢字 + 换行不再把聊天消息劈开发出去 Aug 12, 2026
两件事,本质是同一个:说出来的话没落到该去的地方。

**一、换行会把聊天消息从中间劈开发出去。**

macOS 把 U+000A 当 Return 键,而聊天框里 Return 就是「发送」。一条带空行
的两段话逐字上屏时,第一个换行直接把上半句发了出去,下半句留在输入框里。

改成发真实的 Shift+Return:聊天框里是软换行不发送,编辑器、终端、网页输入
框里就是普通换行。

做成设置而不是硬编码——风格市场里有靠换行把一段话拆成多条消息的风格包,
那种效果要的正是真回车。默认 Shift+Return,需要的人切回 Return。

**二、落字失败时,那段话会悄无声息地消失。**

密码框挡着(Secure Input)、辅助功能掉了权限、粘贴被拒——这些时候唯一的
兜底是把文本写进剪贴板。可它既依赖一个默认可关的开关,就算开着,屏幕上也
没有任何东西告诉你文本在那儿。用户看到的只是胶囊一闪而过,然后是个空输入框。

现在屏幕底部会弹一张卡片,里面是**完整**的那段话,一键复制。20 秒后自己
消失,鼠标停在上面就不走。

顺带修正一处数据失真:逐字上屏打到一半断掉时,此前一律报 InsertStatus::
Inserted(屏幕上明明只有半截),history 里的 insertStatus 因此是错的。
@bigsongeth
bigsongeth force-pushed the fix/macos-streaming-newline-sends-message branch from 4c5158e to fec1545 Compare August 12, 2026 10:44
@bigsongeth bigsongeth changed the title feat(dictation): 落字失败时不再默默丢字 + 换行不再把聊天消息劈开发出去 feat(dictation): 落字失败时把话交还给用户,换行不再劈开聊天消息 Aug 12, 2026
@bigsongeth

Copy link
Copy Markdown
Contributor Author

手打这么长也太牛了😂 三点都收到,PR 已经更新。

换行做成设置了

这个结论其实项目里早就有 —— Windows 侧的 WindowsSendInputNewlineMode 就是三档(Enter / Shift+Enter / CR+LF),设置文案还写着「聊天框通常选 Shift+Enter;记事本 / VS Code 等选 Enter」。macOS 一直没做这个分支,这次补上,结构照搬那边。

有个差异想请你定:Windows 默认是 Enter,我这边默认给了 Shift+Return。 理由是 macOS 这次是真出了事故——一条带空行的两段话,上半句直接飞出去成了一条微信消息,只能撤回。默认值想站在「不误发」这边;靠换行拆多条消息的风格包,让用户去设置里切 Return,风格包说明里写一句就行。如果你觉得两边默认值该保持一致,我改成 Enter 也可以——那样默认用户还是会遇到误发。

焦点守卫整个撤掉了

你说的那种使用习惯确实会被它干扰。顺着想下去发现更根本的问题是:它在猜用户的意图。拿「app 变了」当「字会打错地方」的信号,可技术上分不清「不小心切走了」和「故意切过去让它落在那」——同一个动作,意图相反。

撤掉之后卡片不受影响。它靠的是「落字确实失败了」这个事实:Secure Input、辅助功能掉权限、粘贴被拒。这些没有猜测成分。

性能那条也就不用担心了,守卫没了。

你提的「没有光标才弹卡片」我觉得方向是对的——判断「这儿能不能打字」是事实,比猜意图靠谱。不过要动的东西比较多,这个 PR 先不碰。

@H-Chris233 H-Chris233 self-assigned this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants