工作微信在小米上, 我想用 iPhone 回一句

14 min

工作微信放在小米手机上, 平时更常拿在手里的却是 iPhone.

事情不大, 麻烦也很具体: 想知道工作微信有没有新消息, 就得再看一部手机. 要是只是回一句收到, 也要把它拿过来, 解锁, 找到会话, 输入, 再放回去.

两部手机各有各的用途, 我并不想把它们合成一部. 只是那些短短的消息, 如果能在手边先看见, 能回的顺手回掉, 应该会省心不少.

于是有了 WeChat Relay.

名字很直白, 就是一段中继. 工作微信仍然留在小米上, iPhone 上另有一个接收和回复的入口. 这里说的工作微信, 是我用来处理工作的普通个人微信, 不是企业微信; iPhone 上的入口也是独立的 PWA, 不会把这些消息写进 iPhone 自己的微信聊天列表.

这个区别从一开始就得说清楚. 我想解决的是手边能不能接住一条消息, 没打算再造一个完整的微信.

先让消息过来

最早做的事情, 是在 Android 上监听微信通知.

消息来了, Relay 取得系统通知里可以读取的内容, 在手机端加密, 经过自托管服务转交, 再由 iPhone 端解密显示. iPhone 的网页入口添加到主屏幕后, 像一个单独的小应用那样打开, 会话和消息都放在里面.

这条路不用 Root, 也不是把微信的整个聊天数据库搬到另一台手机上. 它从通知出发, 因而也带着通知本身的限制. 微信没有发出通知的内容, 不能指望这边凭空知道; 过去没有接到的历史, 也不会因为今天配对好了就自动补齐.

我给它的第一份期待很低: 来一条, 能看见一条, 知道是谁说的.

真正做起来才发现, 提醒响了, 和正文出现在眼前, 居然是两件事.

有一阵子, iPhone 已经收到提醒, 打开页面却不见新内容. 后来又碰到另一种情形: 人就停在会话详情里, 新消息仍然不往下追加, 得再点一次系统通知.

我当然不愿意一直这样用. 都已经打开聊天页了, 还要绕出去点个提醒, 总觉得差了一口气.

排查过程中, 有一次问题出在我们自己的限流上. 页面同步, Android 上传与回复查询, 几种正常请求挤进同一个过紧的限额里, 反而把自己挡住了. 后来把流量的用途分开, 配合前台会话的更新机制与重试处理, 提醒和正文才逐渐接上.

这些事在架构图里只占一根箭头. 在手机上, 却是一次次发消息, 等待, 点开, 再问一句: 这回到了没有?

看见以后, 总想顺手回一句

能看消息以后, 回复就成了最自然的下一步.

iPhone 上写下的文字同样先加密, 经过服务端交给小米. Android 优先借用原始微信通知仍然有效的快捷回复能力, 把内容送回微信. 原来的通知还在, 这条路就比较直接.

可手机日常不会永远停在最配合的状态. 通知可能失效, 屏幕可能锁着, 微信界面也可能与刚才不同. 代码里写了发送, 不代表对面的人真的收到了.

为了自己的这台小米, 我们又做过一段明确开启才会使用的辅助功能实验: 在限定条件下操作手机上的微信, 完成回复, 再把设备恢复到原来的锁屏状态.

这段花了不少工夫. 曾经卡在系统锁屏控件的识别上, 也曾经把文字写进输入框, 却没等界面状态跟上就急着往下走. 修这些问题, 靠的不是多点几次发送, 而是把每一步真正完成的条件弄清楚.

那次最终验收, 我们看了两次锁屏回复的过程, 也确认了实际收件. 消息出去以后, 手机重新锁回去, 屏幕熄下来. 到这里, 才觉得一句简单的回复终于走完了它该走的路.

但这条实验路径仍有我不愿省略的一处风险: 当时的微信版本没有暴露可用的会话标题文字, 标题复核未能保留, 因而存在误发到错误会话的可能. 我的设备上试通了, 不代表适合让别人不加判断地照搬, 更不适合把重要消息放心交给它盲发.

日常能用与到处都能用, 中间还隔着许多不同的手机和界面.

语音过来了, 可我想知道它说了什么

文字之后, 自然就遇到了语音.

如果 iPhone 只显示一个语音提示, 我仍然得去拿小米听. 于是又想往前挪一点: 能不能借微信自己已有的转文字, 把结果带过来?

我们最后走的就是这条路. 新语音进入队列, Android 找到对应的那条消息, 调用微信原生转文字. PWA 先收到语音提示, 有了结果以后, 再把文字补到原来的会话里. 传过来的是转写文本, 不是录音文件.

其中有一段很绕. 微信明明已经把字显示在屏幕上, 辅助功能却拿不到文字节点. 后来增加了受限的复制读取过程, 只接收这一次操作刚产生的文字, 不拿旧剪贴板里的东西凑结果.

可第一次能复制, 还没算结束. 转写如果被放进一个新会话, 我仍然要猜它来自谁; 同一条通知短时间重复上报, 就可能冒出两份消息; 列表一滚动, 还得确认正在处理的依然是原来那条语音.

于是又一项项收拾: 去重, 保住发送方和原任务的关系, 等列表稳定, 等长按完成, 再去找复制按钮. 文字很长, 就分段同步. 微信自己转文字失败, 就如实显示失败.

最后, 单条语音的这条路跑通了, 我也确认过可以使用. 连续很快地发多条, 仍然可能卡住, 这个限制没有被一句已经完成抹掉. 目前我接受它作为一个自用工具的不足, 没把它当成语音原样同步的替代品.

图片和聊天记录, 各有各的过法

图片也试过几条路.

最初能看到的是聊天气泡的截图预览, 清晰度不够. 后来尝试打开大图再截图传过去. 这里得到的仍然是手机画面的截图, 不能因为点过查看原图, 就把传出的截图叫作原始文件. 长图与不同页面状态, 也还有各自的限制.

旧预览在手机上看到了, 后来的大图方案还没走完从小米到 iPhone 的确认过程. 所以眼下想认真看一张图, 尤其是图里有小字的时候, 我还是会回到原来的微信里打开.

别人合并转发过来的聊天记录, 又是另一回事. 只收到聊天记录四个字, 和真正看见里面的内容, 差得很远.

最后这类卡片借用微信原生转发, 送到事先准备、接收端账号也在其中的中继群里. 普通消息继续在 PWA 看, 完整聊天记录去接收端微信的那个群里看. 这条路径已经有过真机成功反馈.

听起来不够统一, 但它保留了内容原本适合的查看方式. 我宁愿多一个明确的入口, 也不想在 PWA 里放一张看似完整、实际点不开的卡片.

消息经过服务器, 正文不留在那里

既然是工作消息, 我不愿意为了少拿一部手机, 就在服务器上多攒一份明文聊天.

消息和回复在两端用 AES-256-GCM 加密. 服务端负责保存密文, 必要的路由信息与推送订阅, 再把它们送到对应设备. 解密后用来阅读的缓存留在接收端, 这一点也要与服务端的密文存储分开理解.

做这种中继, 最重要的几件事都不太显眼: 这条消息属于哪个配对, 回复要回到哪一条原通知, 处理失败以后能不能再试, 结果不明时会不会重复发送.

尤其是最后一件. 对方没有回话, 并不能据此断言刚才的消息没发出去. 所以发送结果不明的任务, 不能为了追求一个绿色的完成标记就自动重发.

人每天使用聊天软件, 几乎不会专门想这些问题. 自己做一小段以后, 才知道一句话在两台设备之间来回, 有多少地方需要认真对待.

也给工作消息留一个暂停键

把工作微信接到 iPhone 上, 是为了少些来回操作. 我并不想因此把工作消息接进每一个时刻.

后来, 设置页加了手动暂停和每周重复的暂停时段, 按中国标准时间计算. 服务端负责最后的转发门禁, Android 收到规则后, 也会在入口停止相关处理.

这套设计里, 暂停期间的新消息不会在恢复后补发. 如果只是把它们攒起来, 到点再一股脑送过来, 那份清静也就只借来了一会儿. 原来的微信仍然在小米上, 真要查看, 可以回到那里.

这套规则已经部署, 不过 iPhone 上的手动切换, 以及暂停时真有消息进来会怎样, 还留着几项真机检查. 在它们逐一确认以前, 这个暂停键还算不上完全收工.

对我来说, 能够接过来, 和能够暂时不接, 都属于这件工具该有的分寸.

现在, 两部手机之间少了一点距离

项目已经开源: AuroraNest / wechat-relay.

它仍然是自托管原型, 配置和使用限制都在 README 里. 不同手机系统, 微信版本, 通知样式和锁屏状态会影响结果. 特别是辅助功能实验, 不能把我这一台设备上的成功当作通用保证.

但回到最初那件小事, 它已经带来了很具体的方便: 工作微信还放在小米上, 我却可以在手边的 iPhone 查看同步过来的文字, 在可用的路径里回复, 也能接住一部分语音转写.

两部手机没有变成一部. 只是原来横在中间的那些动作, 少了一些.

为了少拿几次手机, 我写了一个项目, 又花了好些时间处理通知, 队列, 锁屏和微信界面. 算起来大概不划算.

可当一条消息在 iPhone 上出现, 我顺手回完, 不用再去找另一部手机的时候, 还是会觉得, 嗯, 就是想要这个.

↑ 回到顶部