跳转到正文

用 AgentBuddy 在手机上继续 AI 编程任务

介绍搭子 AgentBuddy 的多 Agent 接入、三端功能与使用方式,介绍Rust 共享核心、协议适配、流式回复、会话恢复和完成通知。

背景

国庆期间要出门,一个小问题让我很不愉快:人离开电脑,我的小皮鞭子就够不着正在干活的 AI 了。带着电脑出门太麻烦,少出门又不划算;手里的任务还分散在不同的 Agent 里,我希望手机能有一个统一入口,随时看看进度,再补上两句要求。(其实这个需求用 ChatGPT 的Remote Control 就可以解决,但是我用的 Claude,迫于 A\的淫威不敢登录手机端。

这篇文章介绍的 AgentBuddy,中文名「搭子」,就是围绕这个场景展开的项目。它包含 iOS、Android 和鸿蒙原生客户端,连接运行在自己 Mac 上的 AI 编程 Agent,让一部分原本需要回到电脑前完成的交互,可以在手机上继续。目前 AgentBuddy 支持 Codex、Claude Code、MyFlicker、Pi、Hermes等常见的 Agent。

电脑与手机端的P2P 连接与多种 Agent 的适配则建立在 alleycat 之上。

手机和 Mac 分别负责什么

AgentBuddy 的工作方式可以概括为:Mac 负责执行任务,手机负责查看和交互。

代码仓库、开发环境和实际运行的 Agent 都在 Mac 上。手机发出的指令会传到主机,Agent 使用主机上的项目目录和工具执行,再把进度与结果传回来。因此,在手机上继续对话时,任务仍然围绕主机上的工作目录展开。

Mac 端有一个菜单栏应用,用来管理后台服务、查看 Agent 配置、展示配对二维码和检查日志。后台服务作为 LaunchAgent 独立运行,退出桌面控制台后可以继续工作。主机仍然需要保持开机、唤醒和网络可达;关闭控制台与关闭电脑是两件不同的事。

手机端则围绕「任务」「项目」「主机」组织入口。对使用者来说,需要先明确任务在哪台机器、哪个项目目录里运行,再进入对应的会话。

目前能用它做什么

相比只把聊天窗口搬到手机上,我更在意任务能否被持续推进。现在的功能主要集中在这些环节:

场景 已有能力 使用边界
管理任务与项目 按主机、项目查找任务,置顶、隐藏、重命名、归档,打开历史会话 任务搜索主要针对列表摘要,不是对所有聊天记录做全文检索
继续工作 流式回复、停止执行、后续消息排队、向执行中的轮次插入要求 插入依赖 turn/steer;具体 Agent 不一定支持
提供上下文 文字、图片输入,查看 Markdown、代码、推理、工具调用与文件变更 输入类型及工具信息完整度由 Agent 和适配层决定
处理确认 命令或文件操作审批,多问题问答表单 只有主机实际发起请求时才会出现;权限策略可能绕过审批
调整任务 选择模型、思考强度,以及运行时允许的审批、沙箱设置 可选项来自运行时能力,不能假设所有 Agent 都接受同一套参数
管理工作过程 Codex 的计划确认、进度展示、目标与 token 预算、代码审查入口 属于相应运行时的能力;鸿蒙部分目标和审查流程仍待端到端验证
修改对话历史 从消息处分叉,或回退后把原输入恢复为草稿 编辑会影响后续历史;运行中、排队中或边界不明确时,共享 Rust 会拒绝操作
离开 App 后获得提醒 主机上报一轮任务的完成或失败,通知点击跳回任务 需要兼容主机、系统通知权限和推送服务;鸿蒙真实后台投递仍待验收

这里的“任务”主要对应一个 thread,一次输入和随后的执行对应一个 turn。一个任务可以有很多轮;收到“完成通知”通常只表示其中一轮结束,不一定表示整个开发目标已经实现。

三端共用核心,并不意味着平台能力已经齐平:

客户端 界面与桥接 当前范围
iOS / iPadOS SwiftUI + UniFFI Swift 远程任务主流程,以及实时语音、Ghostty 终端、Live Activity、Apple Watch 等平台扩展;终端默认关闭
Android Jetpack Compose + UniFFI Kotlin 远程任务主流程,以及实时语音、Ghostty 终端和系统小组件;终端默认关闭
HarmonyOS ArkUI / ArkTS + 生成的 N-API 适配层 已有配对、任务、流式消息、图片、审批和历史操作;语音、终端、非图片附件、宠物与小组件仍待移植
macOS Tauri v2 + React / TypeScript + Rust 菜单栏控制台,管理独立运行的主机服务、Agent 配置、配对和日志

鸿蒙当前构建面向 API 24 及以上的 arm64 设备。扫码、SSH 私钥登录等路径还有待补齐的真机验收项,不能把界面入口当成完整验证的证明;详细范围见鸿蒙说明与 QA 矩阵。

从配对到继续对话

以连接自己的 Mac 为例,基本流程是这样的:

  1. 在 Mac 上准备好要使用的 Agent,完成它需要的账号或 API 配置,确认项目的开发环境可用。
  2. 打开 AgentBuddy 桌面控制台,安装后台服务,在配对页面展示二维码。
  3. 使用手机 App 扫码,选择主机提供的 Agent,建立连接。
  4. 选择项目目录,新建任务或打开已有任务,发送指令并查看回复。

例如,可以在一个项目里发起这样的任务:

帮我检查登录页面的错误提示逻辑,先说明问题和修改思路,再修改代码并运行相关测试。

之后在手机里查看回复,继续补充要求:

网络超时和密码错误需要使用不同的提示文案,请把这两个情况分别处理。

这里的重点是让同一个任务可以继续推进。手机很适合阅读摘要、补充上下文和作出简短决定;涉及大段代码对比、复杂调试时,仍然可以回到电脑上处理。

Agent 的登录、API key 和开发环境仍按各自 CLI 的方式在主机上准备,AgentBuddy 不需要另注册一套产品账号。Mac 桌面包及源码入口可以从项目 Releases和开发文档查找,具体安装渠道以项目当时提供的版本为准。

App 也提供局域网发现、手动 WebSocket 和 SSH 等入口。SSH 路径会处理认证、主机指纹与任务服务连接;首次连接或主机密钥变化时,需要确认信任。不同入口的部署条件不同,下面的架构和完成通知主要围绕二维码配对的 alleycat 主机展开。

多种 Agent 共用一个手机入口

当前主机适配目录包含 Codex、Claude Code、Pi、OpenCode、Amp、Factory Droid、Hermes、Devin、Grok,以及 MyFlicker 的 mfcli,还提供 Shell 入口。手机会先读取主机返回的 Agent 列表及可用状态,因此一台没有安装对应 CLI 的 Mac,不会因为手机支持展示它就自动获得执行能力。

这些 Agent 在主机端有各自的运行方式。Codex 使用 app-server 协议;其他 Agent 通过对应的适配层,把消息和操作转换成手机端能够理解的形式。主机还会提供能力标记,例如能否走 SSH bridge、能否覆盖会话权限、是否支持完成通知,手机据此决定如何连接和展示选项。

这样,手机上的会话界面和连接管理可以复用。不过,每种 Agent 实际支持哪些操作,仍然取决于它本身和适配层。支持接入多种 Agent,也不代表所有功能在它们之间完全一致。

对使用者来说,这提供了一个统一的入口:根据任务选择已经配置好的 Agent,在手机上查看它的工作过程并继续交流。

以 MyFlicker 为例,当前走的是 alleycat 配对 → mfcli bridge → mfcli acp。它已经接入会话创建与恢复、模型和思考强度选择,但当前适配没有提供手机 SSH bridge、turn/steer、归档和回退能力;ACP 权限请求也还没有转成手机审批卡片。这些是适配边界,不能套用 Codex 的全部功能列表。

移动端最有用的几个交互

查看进度与结果

手机端支持流式消息、Markdown、代码块和图片内容。查看任务时,既能读最终回复,也能跟随会话了解当前发生了什么。工具调用、文件变更和计划进度有各自的展示,方便区分“Agent 在解释方案”和“工具已经实际执行”。

这类场景里,信息是否清楚比一次展示多少内容更重要。打开 App 后能找到刚才的任务,看清它正在执行、等待输入,还是已经结束,才方便决定接下来要做什么。

处理需要介入的请求

当 Agent 和主机配置启用了相应的审批流程,手机端可以展示请求并提交决定。这样,任务因确认操作而停下来时,就多了一个处理入口。

这里也有一个实际边界:是否发起审批由 Agent、适配层和主机权限配置共同决定。手机端有审批界面,并不意味着主机上的每个命令都会要求确认。

从实现上说,审批和问答是服务端发来的待处理请求。Rust 保存请求与所属任务,原生界面呈现命令、路径、选项等信息,再通过 respond_to_approval 或 respond_to_user_input 把决定送回。界面上的“提交中”“提交失败”属于本地交互状态;请求是否已处理,要与共享状态同步。

接收一轮任务结束的通知

项目已经接入由主机上报的完成通知。符合条件的任务结束后,主机把事件交给推送服务,再通过 iOS 或 Android 的系统通知通道提醒手机;点击通知后可以进入对应会话查看结果。

这项能力需要主机支持 push.v1、所选 Agent 支持被主机观察、手机允许通知,并且推送服务能够连通。当前实现适用于支持这些能力的 alleycat 配对主机,普通 SSH 连接不在这条通知链路内。通知适合作为提醒,任务的具体结果仍以重新打开会话后读到的内容为准。

整体架构与设备边界

这张图回答三个问题:手机把指令发给谁,谁在 Mac 上真正执行任务,执行进度又怎样回到手机。以扫码配对的连接方式为例,主要路径是:手机 App → Mac 后台服务 → 所选 Agent,进度和结果沿原路返回。

手机发指令,Mac 执行任务:手机通过加密连接访问后台服务,后台服务连接 Codex 或其他 Agent;Agent 使用 Mac 上的项目文件和工具,桌面控制台只负责管理服务

顺着图往下看:手机里的 Rust 代码管理连接、整理会话进度;Mac 后台服务接收手机指令,交给选中的 Agent,再把消息传回手机;Agent 才是实际读写项目代码、运行命令和测试的程序。图左侧的桌面控制台负责安装、启停、配置服务和展示配对码,退出控制台不会停止已经安装并运行的后台服务。

iOS 使用 SwiftUI,Android 使用 Jetpack Compose;两端通过 UniFFI 生成的 Swift / Kotlin 接口调用 Rust。UniFFI 负责把 Rust 的对象、记录、枚举和方法导出为其他语言可调用的接口。鸿蒙沿用同一套 Rust 公共接口,通过额外的 ArkTS 绑定生成器与 N-API 适配层接入,并没有另写一套 WebSocket 协议和 reducer。

这里的 alleycat 是开源的主机连接与 Agent 适配项目:它提供手机连接认证、连接本机 Agent,以及转换不同 Agent 请求和消息格式的能力。AgentBuddy 复用这些实现,把主机程序命名为 agentbuddy,并随 Mac 应用一起提供。它承担的是连接和转发工作,实际改代码、运行工具仍由所选 Agent 完成。

Mac 控制台使用 Tauri、React 和 Rust。点击“安装服务”“配对”等操作时,它会调用随应用打包的 agentbuddy 命令行程序,例如 install、pair 和 status --json。安装后的后台服务由 macOS 的 LaunchAgent 机制管理,独立于控制台运行。相关实现可以从 sidecar.rs 和主机入口看到。

把公共运行时放进 Rust

让多个平台共享运行时,解决的是状态行为的一致性。例如,一段流式回复怎样合并、同一条消息收到多次更新时怎样处理、断线后怎样重新加载会话,这些逻辑如果分别写在 Swift、Kotlin 和 ArkTS 里,就需要分别维护和修复。

项目里的几个主要角色可以这样理解:

模块 职责
AppClient 提供读取会话、列出任务等直接服务操作
AppStore 提供状态快照、更新订阅,以及发起一轮输入等复合动作
MobileClient 在内部管理会话、路由请求,并组织事件处理
AppStoreReducer 把内部事件合并到手机端的运行时状态,并产生类型化更新

AppClient 与 AppStore 是并列的调用入口,共用同一个 MobileClient。手机界面通过平台侧的 AppModel 观察更新,再映射成各自的原生界面。这里有两层状态归属:实际执行与会话记录以主机为准,手机内的运行时状态以 Rust Store 为准。 原生层保留导航、弹窗、输入草稿、平台权限等必要状态。

目前仍有一部分状态处理逻辑留在 Swift 和 Kotlin 层,更准确地说,这是一个逐步把公共行为收敛到 Rust 的过程。对应入口可以从 AppStore 和 AppClient 看起。

让不同 Agent 接入同一套会话协议

手机侧需要理解的是会话、消息、工具调用和运行状态。Codex 原生提供 app-server 协议,主机可以转发它的 JSON-RPC;其他 Agent 则通过 bridge 转换请求和事件。

配对二维码包含协议版本、主机节点标识、访问 token,以及可选的 relay 和主机名称。客户端先校验配对数据,再通过带 token 的 list_agents 获取可用 Agent 与主机能力,选择后建立会话连接。具体实现见 alleycat.rs。

在配对路径里,底层使用 Iroh / QUIC 建立连接,运行时再根据主机提供的 wire 类型接入 WebSocket 或 JSONL 流。连接负责传输数据,协议适配负责解释这些数据对应的会话操作。网络不能直连时,Iroh 可以通过 relay 转发,端点之间的数据仍保持加密;这与“任何网络环境都能纯直连”是不同的承诺。可参考 Iroh 的连接说明和安全说明。

协议适配图,展示统一会话请求分别经过 Codex 直通、专用 bridge 与 ACP bridge 到达不同 Agent

适配层把差异放在靠近 Agent 的位置处理,手机端就可以复用多数会话展示与状态逻辑。这个设计也解释了前面提到的功能差异:只有底层 Agent 和 bridge 都能表达某项能力,统一界面才能完整提供它。

MyFlicker 的接入比较能说明这件事:手机发出的 turn/start 被转换成 ACP 的 session/prompt;ACP 的文本片段、工具调用与结束原因,再被转换成手机理解的事件。bridge 还需要处理会话所在的 cwd、模型配置与历史恢复,单纯转发一段提示词并不够。

这里还有一个关键实现:ACP 的长时间执行被放到后台任务中,turn/start 可以先返回,回复再通过事件持续推送。 这样既保持了流式体验,也让主机能及时识别已经开始的轮次并处理推送订阅,不必等整个 session/prompt 执行完才确认“任务开始”。实现位于 ACP bridge。

一次输入怎样变成流式回复

以一个空闲会话中发起新一轮任务为例,点击发送之后,界面会调用 UniFFI 暴露的 AppStore.start_turn,再进入 MobileClient.start_turn。Rust 层会暂存一条本地消息预览,让用户立即看到已提交的内容,然后向主机发送 turn/start 请求。

AgentBuddy 消息时序图,区分 turn/start 请求响应与流式事件经过 Rust 状态层更新手机界面的过程

MobileClient 中发送请求的核心片段如下,前后的状态管理和错误处理已省略:

let response_result = self
    .request_typed_for_server::<upstream::TurnStartResponse>(
        server_id,
        upstream::ClientRequest::TurnStart {
            request_id: upstream::RequestId::Integer(crate::next_request_id()),
            params: direct_params,
        },
    )
    .await;

这里直接使用类型化的协议请求和响应。TurnStartResponse 返回之后,客户端可以确认这次调用、将本地消息预览关联到对应的 turn.id。请求成功,此时并不等于任务已经完成。

Agent 执行过程中产生的文本增量、工具事件和结束事件,通过另一条事件处理路径持续返回。Rust 中的事件处理器把它们转换成内部 UiEvent,交给 AppStoreReducer 合并;例如,文本增量会追加到已有消息中,再向订阅者发出条目更新或增量更新。

各端的 AppModel 通过 subscribeUpdates() 和 nextUpdate() 消费更新,再应用增量或读取快照来驱动界面。把请求响应和事件更新区分开之后,发送按钮的结果、消息的流式展示和任务的最终状态,就有了各自明确的数据来源。

跨语言调用也有成本。Rust 的订阅层会合并相邻、兼容的文本增量,减少逐字跨 FFI 更新;同时保留不同消息和工具事件之间的边界。如果订阅者落后到丢失了缓冲区里的更新,会收到 FullResync,重新读取快照,而不是继续把残缺的增量拼下去。

这张图只展开新一轮任务的主要路径。请求失败时需要撤回本地预览;已有任务仍在执行时,新的输入还可能进入后续消息队列,或通过 turn/steer 补充到正在执行的一轮中。

断线后怎样恢复同一个任务

手机会切换 Wi-Fi、移动网络,也会被系统挂起。连接重新建立,只解决了“数据能传过来”,还需要恢复当前任务的监听和历史状态。

会话恢复图,展示重连后恢复 thread 监听、按主机能力分页或读取历史、合并到 Rust Store 并继续订阅

图 4:重新连接、恢复监听和补齐历史分别解决不同的问题。点击图片可放大。

对于需要重新挂接监听的远程会话,Rust 会调用 thread/resume。这一点容易忽略:thread/read 可以读取历史,但不等价于让当前连接订阅后续的消息增量。源码里的 resume_thread_for_runtime 明确说明,如果没有会话监听,连接可能只能看到状态变化,却收不到完整的轮次和条目事件。

历史加载也不必一次搬完整个对话。支持分页的主机使用 thread/turns/list,先取最近五轮,再按游标加载更早内容;不支持该能力的旧主机走兼容路径。当前分页会请求完整条目,避免只拿到展示摘要后丢失工具调用、commentary 等上下文。返回内容经 hydration 转成类型化会话条目,再进入同一个 Store。

共享层的 reconcile.rs 负责将权威查询结果与运行时状态对齐。例如读取会话、恢复会话、分叉和回退返回的结果,都会走对应的状态合并;已有事件能完整表达的变化,则尽量由事件驱动。这样可以减少“请求明明成功,界面仍停在旧状态”的情况。

恢复过程也有边界:主机不可达、Agent 不支持恢复或历史接口返回不完整时,客户端需要报告失败或降级。手机显示的一段本地预览不能代替主机上的执行结果。

手机进入后台后怎样收到完成通知

如果只依靠手机持续监听会话事件,一旦 App 进入后台、连接中断,就容易错过任务结束的时刻。AgentBuddy 把完成事件的观察放到主机端,再通过系统推送提醒手机。

AgentBuddy 完成通知链路图,展示手机密封推送 token、主机订阅和上报、Worker 校验以及系统通知后重新读取会话

图 5:主机负责观察任务结束,通知负责提醒,打开 App 后再读取结果。点击图片可放大。

当符合条件的一轮任务开始时,手机侧的 PushManager 会准备设备授权,把系统推送 token 用 Worker 的公钥加密,再通过 Iroh 发送 push_subscribe。主机拿到的是密封后的推送目标,随后用带签名的 HTTPS 请求向 Worker 注册订阅。

主机观察到任务进入终态后,再上报结束事件。Cloudflare Worker 校验签名与授权,交给按主机组织的 HostChannel 处理订阅匹配、事件去重和重试,最后调用 APNs 或 FCM 发送可见通知。Worker 接收的是标识和终态,不接收对话正文。这部分实现集中在 push-proxy。

这里有两种不同的保护:推送目标使用 X25519、HKDF 和 AES-GCM 密封,主机只转发密文,Worker 才能打开并获取投递 token;设备授权与主机上报则通过签名验证来源和订阅范围。聊天传输的端到端加密与系统推送是两条链路,不能混成一个概念。

订阅成功后,完成提醒不要求手机 App 始终保持与主机的会话连接。 主机仍需正常运行并能访问推送服务,手机也需要网络和通知权限。

用户点击通知时,App 根据会话定位信息连接主机,加载对应任务并刷新实际状态。这个步骤很重要:通知可能延迟到达,点击它的时候任务也可能已经有了后续变化,因此应重新向主机读取结果。

鸿蒙也已接入共享的密封订阅流程,并增加 Huawei Push Kit v3 发送器。仓库最新 QA 记录显示,真机已能获取推送 token,通知路由和 Worker 配置也有进展,真实后台完成通知的端到端投递仍未验收。因此图中将这条通道单独标为待验收。

语音、终端和平台扩展怎样接入

实时语音:音频留在平台,任务交接交给共享层

iOS 和 Android 已有原生 WebRTC 语音实现。peer connection、麦克风权限和音频会话由平台层负责,回声消除、降噪等由 libwebrtc 的音频处理模块承担;Rust 管理信令、会话生命周期、转录与任务交接状态。这样既能使用各系统的音频能力,也能复用任务选择和交接规则。

语音需要相应的服务凭据与配置,不会因为 Mac 上某个 Agent 已登录就自动全部可用。鸿蒙的原生 WebRTC 语音仍在移植清单中。

终端:Ghostty 负责渲染,执行环境另有归属

项目中还集成了 Ghostty 终端。iOS 通过 UIKit / Metal 承载渲染,Android 通过 Compose、SurfaceView 和 JNI 接入;终端的输入、选择、滚动、尺寸变化与远端会话相连。它适合临时查看输出或补一条命令,两端入口目前都属于默认关闭的实验功能。

需要区分“有终端渲染器”和“手机里有完整本地运行环境”:当前 fork 禁用了 iOS 本地 iSH,因此不能把远程终端写成 iPhone 已支持本地 Alpine 开发。Android 保留本地运行环境的相关路径,鸿蒙的终端渲染与本地环境仍待适配。

除此之外,仓库还有宠物、消息内交互组件,以及 iOS 的 Live Activity、Apple Watch 和 CarPlay、Android 的系统小组件等扩展。这些能力围绕任务状态或语音提供额外入口,平台支持范围不同,也不改变主机执行、共享层管理状态的基本分工。

体验之前需要了解的边界

AgentBuddy 仍在持续开发,各端功能和真机验收也在继续对齐。本文介绍的是手机连接 Mac 的使用方式,主机睡眠、断网或 Agent 进程退出都会影响任务;退出桌面控制台则不等于停止 LaunchAgent 后台服务。

连接方面,项目使用 Iroh 提供的端到端加密 P2P 能力,也保留了 relay 中继路径;完成通知则另外依赖推送代理与系统推送服务。因此,不能把它理解成所有流量都只在手机和 Mac 之间直连。具体 Agent 使用云端模型时,还会按自身配置调用模型服务。

配对信息也需要认真保管:当前主机使用共享 bearer token,还没有按手机分别撤销的机制;执行 agentbuddy rotate 会使原有配对凭据一起失效。拿到二维码的设备可能通过 Shell 或放宽权限的 Agent 以 Mac 用户身份执行命令。主机默认配置中也存在跳过审批的选项,因此需要按实际用途检查桌面 Agents 页面中的权限配置。分享截图时,应避开二维码和完整配对内容。

如果想自己尝试,可以从项目仓库和开发文档开始。源码构建需要准备对应平台的工具链,以及主机上各 Agent 的运行环境。

想继续读实现,可以按这几个入口往下看:

想了解的部分 源码入口
手机公共接口、发送与恢复会话 codex-mobile-client/src:ffi/、mobile_client/、store/
配对和传输 alleycat.rs、同目录的 discovery.rs 与 ssh_bridge.rs
MyFlicker 和其他 Agent 的转换 AkaShark/alleycat:mfcli-bridge、acp-bridge 及各专用 bridge
Mac 控制台与守护进程封装 apps/desktop、services/kittylitter
推送订阅与投递 共享 PushManager、push-proxy
鸿蒙的原生接入与剩余工作 apps/harmony、任务清单

我更看重的使用场景

对我来说,AgentBuddy 值得继续打磨的场景很具体:一个任务已经在电脑上开始执行,人在离开电脑之后,仍然能够知道它进行到了哪里,并在需要时补充信息、作出决定。

随着更多工作可以交给 Agent 持续执行,这种短暂但关键的交互会更常见。把它做好,手机就能成为开发流程里一个实用的补充入口。

好了,我的十一假期结束了,这个没啥用了,我又可以坐着拿着小皮鞭子抽打 AI了 go work!!!

评论与讨论

欢迎交流、补充和纠错。登录 GitHub 后即可发表评论。

评论由 GitHub Discussions 提供。若加载不出来,可前往 GitHub 查看讨论。