离开电脑之后,继续同一项工作:Open Nua Remote Control 的工程实践
Open Nua Engineering
在电脑上启动一个 Agent 任务,然后带着手机出门。任务运行到需要确认的地方,可以在手机上处理;生成了一份报告,可以直接打开;想到新的问题,可以继续发出指令。回到电脑前,看到的仍然是同一个会话。
这是 Open Nua Remote Control 想交付的体验。
需求听起来很小,甚至容易被理解成“给桌面应用加一个手机页面”。真正实现之后,我们发现最难的部分是让网络、身份、会话状态和移动端交互在各种中断之后仍然对得上。
手机显示连接成功,只是整个故事的开始。
执行留在本地电脑,手机成为另一个入口
远程控制的对象是 Agent 会话。手机可以查看历史、创建会话、发送消息、取消执行、处理人工确认,以及预览产物。屏幕共享和远程鼠标键盘不在产品范围内。
这个边界决定了系统的结构:本地电脑上的 Host 继续管理会话与执行,手机读取 Host 发布的状态,并提交明确的操作请求。模型调用、工具执行、工作区和安全策略依然由桌面端管理。
手机连接中断,不应被解释成用户取消了任务。用户再次进入会话时,客户端需要恢复到 Host 的当前状态,而不是从上一次屏幕上剩下的内容猜测发生了什么。执行继续的前提是 本地电脑和 Host 仍然可用;电脑休眠或关机后,远程访问也会受到影响。
先保证能访问,再争取直连
家里或公司的本地电脑通常位于路由器后面,外面的手机可能连接另一张 Wi-Fi,也可能使用蜂窝网络。两端能否直接通信,取决于当时的地址映射、防火墙和网络限制。
因此我们没有把公网直连作为产品可用的前提。两端先通过可达的私有中继建立通信,并尝试发现直接路径。直连建立后使用直接路径;无法直连时继续通过中继传输。
这个选择带来一个很实际的收益:用户不需要先理解自己的网络属于哪一类,才能开始使用产品。连接路径可以作为状态展示出来,但路径优化失败不必等同于业务连接失败。
验证时也必须分清不同证据。同一个局域网里获得直连,只能证明局域网路径正常。要证明中继确实能够承载产品流量,需要让手机和本地电脑 处于不同公网网络,并在排除直接路径后,实际读取会话、发送消息、接收结果。
我们分别保留了直连和强制中继的验收记录。一次成功连接证明的是当时的网络组合,不能外推成所有网络都能访问。
网络接通之后,还要回答三个身份问题
远程访问把几个容易混在一起的问题放到了同一条链路上:手机连接的是不是预期的中继?中继应该接纳哪台设备?这台设备可以在桌面端做什么?
我们用三层控制分别回答。
首先,客户端固定中继服务器证书的摘要。建立加密连接时,实际证书必须与预先配置的摘要匹配。这个机制确认的是服务器身份,并不保证持有该证书的服务器永远不会被入侵。
其次,每个节点拥有独立的私钥和公钥。节点连接中继时提交公钥,并通过加密握手证明自己持有对应私钥。中继只接纳授权列表中的节点。知道服务器地址、证书摘要,或者复制某个设备的公钥,都不足以取得中继使用权。
最后,桌面 Host 校验应用层授权。网络连接成功后,客户端仍然只能使用被允许的会话能力。工作区、模型、人工确认选项和产物访问范围,都需要经过 Host 的校验。
业务数据在手机和本地电脑之间端到端加密。中继能够观察连接地址、时间和流量规模,也能够延迟或阻断连接,但仅凭中继权限不能读取会话明文。
配对是授权交付,撤销也需要分层
当前使用桌面端显示二维码、手机扫码的配对流程。桌面端为设备生成独立身份,通过配对二维码交付连接信息和应用授权;手机校验后将凭据保存到本机安全存储,桌面端在配对完成后删除临时设备私钥。
这意味着配对二维码本身是敏感凭据。二维码的导入时间窗口有限,但导入期限结束并不等于其中的节点私钥自动失效。已经交付的身份需要通过撤销授权或轮换密钥失效。
未来可以进一步改成手机本机生成私钥、只向桌面交付公钥,让设备私钥从生成开始就不离开手机。这属于下一阶段的配对增强。
撤销同样有两层含义。桌面端撤销设备,是停止它的业务访问权限;云端移除节点公钥,是停止它继续使用私有中继。
当前中继准入发生在建立连接时,更新授权列表不会自动驱逐已经建立的连接。我们通过受控脚本同步公钥列表,并重启单节点中继,让既有连接重新接受准入校验。代价是所有客户端会经历短暂重连。配对、撤销之后的云端同步目前仍是运维步骤,尚未自动化。
另外,中继的节点准入不覆盖独立的地址探测服务。它仍有公开网络入口的风险,需要单独考虑容量、防护和可用性。
“手机创建了,电脑却看不到”暴露了真正的状态边界
第一轮验收中,一个很有代表性的问题是:手机能创建新会话、发送消息,但回到桌面却找不到这个会话。
如果只测试手机上的发送和回复,这项功能看起来已经完成。但用户理解的“新会话”包含更完整的承诺:它应该进入同一份持久会话目录,桌面能够找到,关闭客户端后还能重新打开。
因此,远程创建必须进入 Host 的持久化会话流程。会话列表也不能只枚举当前活跃的内存对象,而要结合持久目录与实时状态;进入某个会话时,再恢复相应历史和订阅。
另一个问题出现在模型选择上。手机创建时选中了模型,退出 App 再进入,会话底部却回到了默认模型。
这说明“选择成功”只发生在客户端局部状态里。修复的关键是让 Host 接受并保存会话配置,恢复会话时再把配置返回给客户端。工作区、推理强度和个人知识库范围,也遵循同样的原则。
远程客户端提交选择意图,Host 发布可选项、验证选择并持久化结果。界面重开后,应该呈现服务端保存的选择。
手机界面需要自己的交互尺度
我们采用混合式客户端:原生层负责连接、凭据、扫码和应用生命周期,Web 界面负责会话列表、详情和输入交互。
这个划分既方便处理移动设备特有的能力,也让会话客户端有机会被后续入口复用。不过,共享客户端能力并不意味着把桌面页面缩小就能得到一个好用的手机界面。
验收很快把这些差异暴露出来:返回需要手势和过渡动画,键盘弹出不能遮挡输入,长代码块不能把整个页面撑出横向滚动条,系统切换暗黑模式时界面也要跟随。
输入框同样不能只提供文本发送。真正继续一项工作,需要选择已有工作区、模型与推理强度,调用命令或技能,并指定个人知识库范围。手机支持选择已创建的工作区,工作区创建仍留在桌面端。
产物展示也是完整会话的一部分。Markdown 要正常渲染,HTML 报告需要独立预览;预览只能访问当前会话授权的产物,并对脚本、外部连接和导航加以隔离。报告能够打开,不应扩大成任意本机文件访问。
人工确认的决定权仍在桌面
手机端必须能够处理任务执行中出现的人工确认,否则离开电脑后,任务仍然可能卡在桌面上的一个按钮前。
但手机展示确认界面,不代表手机拥有新的安全决策权。Host 发布当前请求和可选动作,客户端提交用户选择,Host 再验证请求是否仍然有效、选项是否存在、是否已经被其它客户端处理。
这个约束在多端同时在线时尤其重要:同一请求可能出现在桌面和手机上,但只能被有效处理一次。后到的响应不能触发第二次执行,重新连接也不能把已完成的确认恢复成待处理状态。
因此,人工确认既是 UI 能力,也是 Host 的并发与授权契约。
“已连接”需要持续的证据
接近交付时,我们增加了已授权设备的连接状态。随后就遇到了一个直接的问题:手机 App 已经被强制结束,桌面为什么还显示在线?
原因是客户端消失时,服务端不一定立刻收到明确的连接关闭通知。仅仅保留一个连接对象,不能证明它仍然可用。
我们为远程连接增加了轻量心跳,在连续多个检测周期没有收到响应后终止失效连接,更新设备状态。在线状态保持在内存中,最近一次成功连接的时间持久保存。重启 Host 后,旧的“在线”不能被当成当前事实恢复。
这里还有一个语义上的细节:“最近连接时间”记录的是最近一次建立有效会话连接的时刻,并不等于最后一次操作时间,也不等于精确离线时间。把含义讲清楚,状态才不会误导用户。
验收要覆盖等待、中断和恢复
移动端的一些问题只有离开局域网后才会变得明显。例如会话列表和详情需要加载一段时间,如果点击后没有反馈,用户会认为按钮没响应,继而重复点击。
我们为这些操作补上加载与忙碌状态,并使用可控延迟验证反馈是否及时出现。测试目标不仅是最终内容正确,还包括等待期间用户能否理解发生了什么。
验收分为几个互补层次:界面测试覆盖会话渲染、输入和选择;模拟器自动化覆盖原生外壳、页面集成、导航与生命周期;连接真实桌面 Host 的测试覆盖持久会话的可见性与恢复;真机验收负责跨网络、中继路径、切网和实际操作体验。
模拟器中通过的用例不能代替真实蜂窝网络,中继端口可达也不能代替一次完整的会话操作。每份证据只证明它实际经过的那段链路。
同一份工作,多个入口
在 iPhone 上,用户能找到桌面的会话,也能创建新的会话;能继续输入、处理确认、查看结果;中断后能恢复到 Host 的权威状态。
它仍然依赖 本地电脑在线,私有中继仍是单节点,云端设备授权同步仍需受控操作。后续移动平台和手机本机生成配对密钥,也还有独立的工作要做。
对我们来说,这次交付验证了此前会话架构的一项重要承诺:当执行、状态和客户端的职责足够清晰,增加一个手机入口,就可以围绕同一份工作提供新的交互方式。用户离开的是电脑前的位置,任务和上下文仍然在那里。