手机是怎么看到终端里的 Agent 的
结论:手机并没有显示终端。电脑替你看着每个终端,算出「它现在是什么状态、在问什么、说了什么」,再把这些结果加密传给手机。手机显示的是卡片和聊天。
1. 总览:一共五段
| 部分 | 它做什么 | 代码 |
|---|---|---|
| Agent | Claude、Codex 等,跑在 Mooring 开的真实终端(PTY)里。它们不知道手机存在。 | — |
mooring serve | 后台。持有所有终端,读屏幕和日志,算状态。App 关了它也还在。 | crates/mooring-host/src/host/ |
mooring remote --relay | 电脑主动连上中继并保持连接。每部手机一条会话,在这里解密、检查权限,再转给后台。 | host/src/relay.rs、remote.rs |
mooring-relay | 公网上的转发站。手机和电脑都连不到对方,就都连它。只存令牌的哈希,看不到内容。 | crates/mooring-relay/ |
| 手机 App | React Native(Expo),iOS 和安卓是同一份代码。管连接、加解密、显示。 | mobile/src/ |
为什么要中继:家里的电脑和外面的手机通常没法直接连。两边都主动往外连同一个公网地址,就接上了。
2. 手机看到的状态是怎么来的
这是整个设计的核心。Agent 不会主动告诉手机任何事,状态全靠电脑从几个来源拼出来,并且只在一个地方判定,所以电脑和手机显示的词永远一样。
| 来源 | 告诉我们什么 | 例子 |
|---|---|---|
| 日志 | 这一轮开始、结束,说了什么。聊天内容都从这里来。 | Claude 的 jsonl;OpenCode 的 SQLite |
| Hook | CLI 在关键时刻主动报告 | Claude 一轮结束时报后台还有哪些 shell |
| 屏幕 | 日志里没有的「此刻」:弹框正等着你 | OpenCode 的权限框 |
| 程序上报 | 程序通过终端转义序列报告自己的状态 | Codex 报 working |
| 进程 | 后台命令还活着吗,开了端口吗 | 测试算「在跑」,http.server 算「等后台」 |
3. 配对:只发生一次
关键:原始钥匙只走二维码,从不经过中继。中继只有哈希,就算被人拿到,也解不开内容,也算不出钥匙。每部手机有自己的钥匙,所以可以单独取消某一部。
4. 连接:每次打开都走一遍
5. 完整一趟:手机发一句话
这里能回答你最初的问题:手机上的字是怎么进终端的?就是电脑替你把字写进了那个终端,Agent 收到的和你在键盘上敲的一样。回复则走另一条路:Agent 写日志,电脑读日志,手机每 2 秒来取一次。
6. 答审批:为什么要再读一遍屏幕
OpenCode 走的是另一条路。它的权限框是横排按钮、没有编号,屏幕上也看不出光标在哪,靠读屏幕按键不安全:按错可能变成「永久允许」。所以 Mooring 改问 OpenCode 自己的本地接口:查到待答的问题,回答时再查一次确认还是这一条,然后直接调用回答接口,不模拟按键。手机上的按钮和确认流程和其他 CLI 一样。
| 情况 | 手机上 |
|---|---|
| 从 Mooring 启动的 OpenCode | 有「允许」「拒绝」「以后都允许」 |
在终端里手动敲 opencode | 能看到「等你回答」,但没有按钮:它没开接口,要回电脑上答 |
本地端口的安全:开了 HTTP 端口后,你浏览器里打开的任意网页都可能请求它,我实测跨站请求确实能打到。所以 Mooring 每次启动 OpenCode 都给一个随机密码,放在它的环境变量里,再由 Mooring 从该进程读出来用。没有密码的请求会收到 401。
7. 状态机
7.1 手机和一台电脑的连接
| 关闭码 | 意思 | 手机显示 | 之后 |
|---|---|---|---|
| 1012 | 电脑离开了中继 | offline | 退避重连 |
| 1013 | 手机太慢被中继断开 | offline | 退避重连 |
| 4400 | 手机发了协议不允许的内容 | offline | 退避重连 |
| 4401 | 没配对、钥匙不对或已取消 | refused | 不再重试,要重新扫码 |
| 4404 | 电脑换了配对码 | refused | 不再重试,要重新扫码 |
| 4410 | 电脑上点了断开这部手机 | offline | 退避重连,5 分钟后才能再连上 |
| 4411 | 权限改了 | offline | 立刻重连,拿到新权限 |
7.2 一个会话(电脑和手机共用)
手机的卡片还带一个更粗的 phase:在跑是 working,等你回答是 waiting,其余是 idle,没有 Agent 的普通终端是 shell。
7.3 加密通道
流量控制是每部手机单独算的,一部手机卡住只影响它自己,不会拖慢电脑或其他手机。
7.4 手机上「未发送」的一句话
每条写操作带一个一次性 id,电脑记得做过哪些。所以网络闪断后重发,也不会把同一句话发两遍。
8. 手机能做什么,由角色决定
| 角色 | 看会话和聊天 | 答审批 | 发消息、按键、开或停 Agent |
|---|---|---|---|
| 只看 view | 能 | 不能 | 不能 |
| 能答审批 answer | 能 | 能 | 不能 |
| 完全控制 full | 能 | 能 | 能 |
权限只在电脑上检查。手机隐藏按钮只是为了好看,不算安全措施,因为手机和中继都被当作不可信。
9. 现在没有的,以及要注意的
| 项目 | 现状 |
|---|---|
| 手机上的实时终端 | 没有,这是 11-02 定的方向:手机只做聊天。有一个只读的「终端最后几行」,只能看和复制。等你回答时,问题下面有解析出的选项按钮和一个 Esc 按钮;审批另有允许和拒绝按钮。除此之外手机不能按键。 |
| 推送通知 | 没接。需要 APNs / FCM 账号。App 在后台时连接可能断开,要回到前台才刷新,所以锁屏时不会收到「有会话在等你」。 |
| 电脑关机或后台没运行 | 手机只能显示上次的内容,并标为离线。写好的话留在「未发送」。 |
| 聊天页的新消息 | 靠每 2 秒轮询,不是推送,有 2 秒左右延迟。 |
| iOS | 和安卓同一份代码,模拟器验过;真机完整走查还在待办里。 |
依据 Mooring 的代码(2026-11-07 读):mooring-remote/src/{secure,wire,link,close,role,protocol}.rs、mooring-relay/src/server.rs、mooring-host/src/{relay,remote}.rs、mooring-core/src/status.rs、mobile/src/remote/{client,hub,close}.ts、mobile/src/store/outbox.ts、mobile/src/remote/pump.tsx、docs/phone-app/spec.md。