Swift输入法Lynsi

反控里打中文,先决定由哪一端组字

灵犀互联反控的中文输入经历了两种不同的实现:让手机输入法消费物理键位,或者让 Mac 输入法组好一段再发送。键盘身份、按键队列、候选词可见性和提交时机,各自都有不能省掉的边界。

给灵犀互联 Lynsi做手机反控时,中文输入暴露出的麻烦比「把敲下的字发过去」多得多。这篇回看的是 10 月的开发与排查记录:物理按键如何进入手机输入法,候选词为什么在手机上有、镜像里却没有,以及后来为什么保留了 Mac 本地组字的第二条路。

这里讨论实现和当时的验证边界,不把当前工作区直接当成商店发行包。

拼音是一段状态,不是几个独立字符

输入 nihao 时,用户尚未决定最终要提交什么。输入法持有拼音、候选词和选择状态;随后按下空格、数字、退格,都会改变这个状态。

如果 Mac 先解释了一遍按键,再把 NSEvent.characters 当作手机的输入,手机输入法就失去了完整的过程。Mac 的输入源、修饰键处理与手机的候选选择,混成了同一条不明确的路径。

最终拆开的第一条路是:Mac 只发送键位,由手机输入法负责组字。

RemoteKeyboard.swift 根据 Mac 虚拟键码映射手机键码,不取事件里的文字。例如 N I H A O 对应的手机键码是:

2030 → 2025 → 2024 → 2017 → 2031

空格接着以 2050 到达,交给手机输入法选词。Shift 作为组合键的一部分传递;单独按下再松开 Shift,则是另一种有状态的动作,用来切换中英文。它必须记住中间有没有别的按键或点击,不能把打大写字母时的 Shift 松开也认成一次切换。

编辑快捷键同样需要翻译。Mac 上的 ⌘C、⌘V 映射为手机的 Ctrl 组合,⌘Q、⌘W 等未映射的快捷键留给 Mac。按键位置、编辑习惯和输入法状态,分别在明确的地方处理。

蓝牙键盘提供身份,按键走另一条通道

这条路还有一个容易被误解的前提:手机得先认为自己接着一把实体键盘。

10 月的 Mate X5 实验确认,登记实体键盘后,小艺输入法会进入实体键盘模式;此后通过 UiTest 注入的键位,也能被它用于拼音组字。因此当时落地的方案是:Mac 发布蓝牙 HID 键盘服务,让手机获得键盘身份;实际按键仍经反控的 UiTest 通道发送。这份实现不通过 BLE HID 输入报告传递按键。

这不是把两条通道混着用就结束了。身份与数据分开后,它们的生命周期也会分开:投屏连接还活着,不代表手机上那把键盘仍然在。

一次短断的双端日志很有说明力。Mac 的蓝牙日志先记录 Disconnecting GATT for unused LE device,手机随后移除了键盘输入设备;局域网会话仍保持认证,投屏继续收帧。如果界面只看「反控在线」就允许打字,后半截拼音可能在键盘模式已经退出后继续进入手机。

实现于是把直接打字的门槛写成两个条件:反控就绪,且手机实际报告键盘在场。键盘丢失不终止鼠标控制,但立即停止接收直接打字。

字母和鼠标不能共用同一种丢弃策略

反控原本需要防止过时的操作堆积。用户已经换了页面,几秒前的点击再落下去,可能点到完全不同的按钮。所以普通待执行操作有数量和时效限制。

但把同一策略套到拼音上,结果会很糟:nihao 中间丢掉一个键,后续输入虽然按时执行,词却已经变了。输入法不认识「这一键因为太旧被优化掉了」。

RemoteControlSession 因此给按键单独的准入规则:

操作 排队时需要保住的语义
点击等带坐标操作 检查时效和屏幕是否变化,避免旧坐标误触
普通打字 按序执行,不因等待时间丢弃、不合并;待发按键达到 48 个时明确拒绝新键并提示
长按产生的重复键 已有按键等待时不继续积压,让退格等重复动作跟随手机处理节奏

键盘丢失时,队列中的待发按键会被清掉;恢复连接不会重播旧拼音。执行前还要再检查一次键盘状态,避免准入时有效、轮到执行时已经失效。

这里还加了一层键盘代次。假如守护进程拒绝一个组合键,准备改走 CLI,而这时键盘已经经历断开和恢复,旧请求不能因为「现在又连上了」就复活。连接状态重新变成 true,不等于它还是原来的输入上下文。

候选条不在镜像里,最初的解释错了

能够组字以后,用户反馈:手机上有小艺候选条,Mac 镜像里却看不到。

第一轮排查把它理解成采集接口不支持输入法窗口。查 SDK 也没有找到一个能让应用强行包含系统输入法窗口的开关。这些线索足以说明应用不能随意控制采集范围,却还不足以证明「系统永远不让采集」。

10 月 10 日的真机记录修正了这个结论:当时那台手机的投屏通知里有屏幕录制「隐私保护」开关。关闭后,小艺实体键盘模式的候选条进入镜像;重新开启,候选条消失。这个选择按投屏会话生效,下一次投屏还需要重新处理。

这条结论有明确范围:Mate X5 上当时的系统、输入法和投屏会话。记录没有验证所有设备,也没有验证实体键盘模式下本来就收起的软键盘能否显示。把一次设备观察扩大成全平台承诺,只会制造下一轮误判。

第二条路:在 Mac 上完成组字,再提交整段文字

候选词可见性依赖手机的系统设置,于是保留了另一条路:画面底部放一个原生 NSTextField,由 macOS 输入法组字。用户选好词、完成文字后,再将整段内容交给手机反控的文本输入通道。

这条路不依赖蓝牙键盘身份,因为手机收到的是已经完成的文字,不需要再解释一串拼音。它也有自己的代价:当时的 UiTest 文本路径借用手机剪贴板,界面需要提示剪贴板会被覆盖;同步侧按内容哈希暂时抑制回声,避免这次注入又被当成新的复制操作传回来。

回车的语义必须说清楚。在这个输入条里,它提交当前文字,不额外向手机注入一次 Enter。否则在聊天应用中,「把文字放进输入框」可能顺手变成「发出消息」。

输入条什么时候清空,也不能凭按钮已经按下决定。sendRemoteText 先检查反控在线、通道空闲、内容非空、不含 NUL 字符,且 UTF-8 不超过 4096 字节。返回已接受后,输入条才清空;忙碌或其他准入失败时,文字留下来。这里的返回值代表本地接受发送,不能被解释成手机端已经确认上屏。

10 月 10 日的 Mate X5 验收记录覆盖了 Mac 选词、回车整段上屏、切换模式保留草稿,以及通道忙碌时文字不丢。源码中的回归测试还分别约束:没有键盘时不能偷偷回退成直接字母输入;手机组字路径必须忽略 Mac 事件里的字符;Mac 组字提交不能附带手机 Enter。这些是已有记录与测试代码的证据,本次写作没有重新操作真机。

这轮开发最后留下的判断很具体:先决定由哪一端持有输入法状态,再决定跨设备传什么。传键位,就要保住顺序和键盘生命周期;传完成的文字,就要定义提交、剪贴板和失败保留语义。中文输入的难点,往往藏在这些状态归谁管理的地方。

✦

留言

  • 加载中…

留言先审后发,通过后公开显示;邮箱只有站主可见。