1. 手机远程操控 AI Agent 的整体设计思路1.1 为什么会有这个需求先说一个我自己的真实场景。我平时主力开发机是一台放在家里的工作站跑着 Claude Code、Codex、OpenCode 这几个命令行 AI Agent白天在公司用笔记本晚上回家才碰得到那台机器。问题就来了白天写代码的时候突然想让 Agent 帮我重构一个模块或者跑一个批量任务人不在机器旁边怎么办传统做法无非几种远程桌面、SSH 客户端、或者干脆把任务攒到晚上一起跑。远程桌面在手机上操作体验极差SSH 客户端能敲命令但看不到 Agent 的交互式输出尤其是 Claude Code 这种带 TUI 界面的工具在手机终端里基本没法用。这就是手机远程操控 AI Agent这个需求最原始的出发点——不是炫技是真的有场景。这个方案能解决的核心问题有三个第一随时随地触发任务不用守着电脑第二实时查看 Agent 执行状态包括它调用了哪些工具、输出了什么、有没有卡住第三多 Agent 统一管理Claude Code、Codex、OpenCode 这些工具各有各的交互方式能不能用一个入口统一调度。适合谁来参考如果你已经在本地跑过至少一个命令行 AI Agent对 Node.js、TypeScript 有基本认知那这篇内容基本可以照着抄。如果你还没装过 Claude Code 或 Codex建议先把本地环境跑通再来看远程这块不然会同时踩两个坑。1.2 整体架构怎么搭核心思路其实很朴素在本地跑一个轻量服务把 Agent 的输入输出通过 WebSocket 暴露出来手机端用一个 Web 页面连接这个服务。听起来简单但中间有几个关键决策点需要想清楚。第一个决策Agent 进程怎么管理。Claude Code、Codex、OpenCode 都是交互式 CLI 工具它们不是设计成被程序调用的。你有两个选择——用child_process.spawn起一个伪终端pty或者用它们各自的 SDK。Claude Code 有 TypeScript SDKCodex 也有对应的接口OpenCode 相对开放一些。我的建议是优先用 SDKSDK 覆盖不到的功能再退回 pty。原因很简单pty 方案要处理 ANSI 转义序列、光标移动、终端尺寸变化在手机小屏幕上渲染出来是一团乱码而 SDK 返回的是结构化数据直接渲染成卡片式 UI 舒服得多。第二个决策通信协议。HTTP 轮询太浪费SSE 只能单向推送WebSocket 是唯一合理的选择。手机端发指令走 WSAgent 输出也走 WS 推回来双向对称实现起来反而最简单。第三个决策安全边界。这个必须重点说。你把一个能执行任意命令的服务暴露到公网等于把家门钥匙挂在门口。我的做法是只在内网或者通过可信的私有网络访问绝对不做公网端口映射。如果确实需要外网访问用一层带认证的反向代理并且给 Agent 的执行权限做白名单限制。这一点后面会单独展开。整体数据流是这样的手机浏览器加载一个静态页面页面通过 WebSocket 连到本地服务的/ws端点服务端维护一个 Agent 会话池每个会话对应一个正在运行的 Agent 进程或 SDK 实例。手机发来的消息经过路由分发到对应会话Agent 的输出经过格式化后推回手机。1.3 技术选型背后的取舍为什么用 TypeScript 而不是 Python因为 Claude Code 的 SDK 是 TypeScript 优先的Codex 的生态也是 Node 侧更完整OpenCode 本身就是 TS 写的。用同一套语言栈SDK 调用、类型定义、进程管理都在一个工程里不用跨语言调试。而且 Node 的ws库和node-pty库成熟度很高踩坑少。为什么不用现成的 Web 终端方案比如 ttyd 或者 gotty我试过它们确实能把终端搬到浏览器但问题是它们只做终端转发不理解 Agent 的语义。你看到的还是原始 ANSI 流手机上要横向滚动才能看清而且没法做任务列表历史记录一键重跑这些针对 Agent 的增强功能。自己写一层虽然多花两天但后面用起来舒服太多。前端为什么不用 React 或者 Vue 搞一套完整 SPA因为手机端要的是快。一个单 HTML 文件加原生 JS加载速度比打包出来的 SPA 快一个数量级而且不用处理构建工具链。对于这种工具型页面原生 JS 完全够用代码量也就几百行。2. 核心细节解析与实操要点2.1 Agent 进程的启动与生命周期管理先说 Claude Code 的接入。它提供了 TypeScript SDK安装方式是npm install anthropic-ai/claude-code具体包名以官方为准我这里说的是思路。SDK 的核心是一个query函数你传入 prompt 和配置它返回一个异步迭代器每次迭代产出一条消息。这种流式接口天然适合 WebSocket 推送。import { query } from anthropic-ai/claude-code; async function runClaude(prompt: string, onMessage: (msg: any) void) { const response query({ prompt, options: { cwd: /path/to/your/project, permissionMode: acceptEdits, }, }); for await (const message of response) { onMessage(message); } }这里有个关键点permissionMode的设置。默认情况下 Claude Code 每执行一个工具调用都要问用户确认在手机上你不可能每次都点确认。所以要么设成acceptEdits自动接受文件编辑要么设成更宽松的模式。但宽松模式意味着 Agent 可以执行任意命令风险很高一定要配合工作目录限制和命令白名单。Codex 的接入思路类似它也有对应的 SDK 或者可以通过codex exec这种非交互模式调用。OpenCode 相对特殊它的免费层有使用限制只能在 OpenCode 自己的环境里用如果你要远程调用需要确认你的套餐支持 API 访问。这一点在热词里也提到了 opencodes free tier can only be used from within opencode踩过这个坑的人不少。进程生命周期管理要注意几个点。第一会话要有超时一个 Agent 跑太久没输出要么是卡住了要么是任务太大超过阈值应该主动终止并通知手机端。第二进程要能被强制杀掉手机端要有一个停止按钮对应服务端的kill操作。第三输出要缓冲Agent 可能瞬间吐出大量内容直接推给手机会卡顿服务端做一层节流比如每 100ms 合并一次推送。2.2 WebSocket 通信层的设计WebSocket 服务用ws库就够了。核心是维护一个会话映射表import { WebSocketServer } from ws; interface Session { id: string; agentType: claude | codex | opencode; process?: ChildProcess; buffer: string[]; lastActive: number; } const sessions new Mapstring, Session(); const wss new WebSocketServer({ port: 8080, path: /ws }); wss.on(connection, (ws) { ws.on(message, async (raw) { const msg JSON.parse(raw.toString()); switch (msg.type) { case start: // 创建新会话 break; case input: // 向指定会话发送输入 break; case stop: // 终止会话 break; } }); });消息协议我建议用简单的 JSON字段包括type、sessionId、payload。不要过度设计什么 protobuf、MessagePack 在这个场景下都是过度工程JSON 的可读性在调试时价值巨大。有一个容易被忽略的点断线重连。手机网络切换WiFi 转 4G会导致 WebSocket 断开如果服务端不保留会话状态重连后你就丢失了所有正在跑的任务。我的做法是服务端会话独立于连接存在连接断开只是取消订阅会话继续跑重连后通过sessionId重新订阅输出。这样即使手机锁屏、切后台任务也不会中断。心跳也不能少。手机端每 30 秒发一个 ping服务端回 pong超过 90 秒没收到就认为连接死了清理掉对应的订阅关系。没有心跳的话很多中间设备会静默断开空闲连接你以为是连着的结果消息发不出去。2.3 手机端 UI 的关键设计手机屏幕小UI 设计要克制。我的页面结构是这样的顶部一个会话标签栏可以横向滑动切换不同 Agent中间是消息流Agent 的输出按类型渲染成不同样式——普通文本、代码块、工具调用卡片、错误提示底部是输入框和发送按钮外加一个停止按钮。代码块渲染是个重点。Agent 输出的代码要能横向滚动不能换行否则缩进全乱。用precode配合overflow-x: auto就行不需要引入 highlight.js 这种重库手机端性能优先。工具调用卡片是我觉得最有价值的设计。Claude Code 执行任务时会调用 Read、Edit、Bash 等工具这些调用如果只显示原始 JSON 很难看。我把它渲染成一张小卡片显示工具名、关键参数、执行结果摘要点击可以展开看完整内容。这样你一眼就能看出 Agent 在干什么而不是盯着一堆 JSON 发呆。输入框要支持多行因为有时候你要给 Agent 一段比较长的指令。用textarea配合自动高度调整最多长到屏幕的三分之一就内部滚动。发送按钮要防抖避免手抖连点导致重复提交。提示手机端一定要做输入草稿保存。你在输入框里写了一半的指令切出去接个电话回来发现没了那种体验非常糟糕。用 localStorage 存一下成本极低。3. 实操过程与核心环节实现3.1 从零搭建服务端的完整步骤第一步初始化工程。我习惯用 pnpm速度快、磁盘占用小。mkdir agent-remote cd agent-remote pnpm init pnpm add ws node-pty pnpm add -D typescript types/node types/ws tsx第二步配置 TypeScript。tsconfig.json里关键是module: ESNext和target: ES2022因为要用到顶层 await 和异步迭代器。{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: bundler, strict: true, esModuleInterop: true, outDir: dist }, include: [src/**/*.ts] }第三步写 Agent 适配层。我把它抽象成一个接口每种 Agent 实现自己的适配器interface AgentAdapter { start(prompt: string, cwd: string): AsyncIterableAgentMessage; stop(): Promisevoid; } interface AgentMessage { type: text | tool_call | tool_result | error | done; content: string; meta?: Recordstring, unknown; }Claude 适配器用 SDKCodex 适配器用它的 exec 模式加 stdout 解析OpenCode 适配器根据你的套餐情况选择 API 或 CLI。这样上层 WebSocket 服务不用关心底层是哪个 Agent统一处理AgentMessage流。第四步实现会话管理器。核心是三个方法createSession、sendInput、destroySession。会话对象里存 Agent 适配器实例、输出缓冲、订阅者列表。输出缓冲用环形数组只保留最近 N 条避免内存无限增长。第五步接 WebSocket。前面已经给了骨架补充一点每个连接要绑定一个用户标识即使是单人使用也要做因为将来你可能想从多个设备同时连。用连接建立时生成的一个随机 token 作为标识存在连接对象上。第六步加一层静态文件服务。手机端页面就一个 HTML 文件用 Node 内置的http模块起一个静态服务和 WebSocket 共用一个端口通过 upgrade 事件区分。这样你只需要暴露一个端口配置简单。3.2 参数计算与性能调优并发数怎么定这取决于你的机器配置和 Agent 类型。Claude Code 每个会话大概占用 100-200MB 内存Codex 类似OpenCode 轻一些。一台 16GB 内存的机器留 4GB 给系统理论上能跑 50 个以上会话。但实际瓶颈不在内存在API 速率限制。你同时跑 10 个 Claude 会话很可能触发上游的并发限制导致部分请求失败。我的经验值是同时活跃会话不超过 5 个超过的排队。排队逻辑很简单用一个队列存待启动的任务每完成一个就出队一个。这样既不会触发限流又能保证任务最终都执行。输出节流的参数怎么定我实测下来100ms 的合并窗口是个甜点。低于 50ms手机端渲染压力大尤其是输出大量日志的时候高于 200ms交互感明显变差你打字后要等一会儿才看到回应。100ms 刚好在感知阈值以下同时把高频小包合并成低频大包网络开销降低一个数量级。心跳间隔 30 秒超时 90 秒这是经过验证的稳妥值。有些资料建议 15 秒心跳但在移动网络下过于频繁的心跳反而增加耗电和断连概率。90 秒的超时给了足够的容错空间即使中间有一次心跳丢失也不会误判。缓冲区大小我设的是 500 条消息。超过就丢弃最旧的因为手机端也不太可能往回翻几百条。如果你需要完整历史应该落盘到 SQLite而不是全放内存。3.3 手机端页面的实现细节页面骨架很朴素!DOCTYPE html html head meta nameviewport contentwidthdevice-width, initial-scale1, maximum-scale1 titleAgent Remote/title style/* 样式 *//style /head body div idtabs/div div idmessages/div div idinput-bar textarea idinput/textarea button idsend发送/button button idstop停止/button /div script/* 逻辑 *//script /body /htmlviewport里的maximum-scale1很重要防止 iOS 在输入框聚焦时自动放大页面那个体验很糟糕。WebSocket 连接逻辑要处理重连let ws; let reconnectDelay 1000; function connect() { ws new WebSocket(ws://${location.host}/ws); ws.onopen () { reconnectDelay 1000; // 重新订阅已有会话 }; ws.onclose () { setTimeout(connect, reconnectDelay); reconnectDelay Math.min(reconnectDelay * 2, 30000); }; ws.onmessage (e) renderMessage(JSON.parse(e.data)); }指数退避重连是标配初始 1 秒每次翻倍上限 30 秒。这样网络恢复后能快速重连又不会在服务端挂掉时疯狂重试。消息渲染用DocumentFragment批量插入避免频繁操作 DOM。每条消息渲染完自动滚到底部但如果用户手动往上滚了就不要强制滚动否则用户想看历史记录时会被不断打断。判断方法是记录当前scrollTop和scrollHeight的关系接近底部才自动滚。注意手机端渲染代码块时一定要对内容做 HTML 转义。Agent 输出的内容里可能包含script之类的字符串直接 innerHTML 会有 XSS 风险。虽然是你自己的 Agent但 Agent 可能读取了不可信的文件内容安全边界不能省。4. 常见问题与排查技巧实录4.1 连接类问题速查现象可能原因排查方法解决方式手机连不上服务不在同一网络手机浏览器访问服务端 IP 的静态页确认 WiFi 一致或走私有网络连上后立即断开端口被占用或路径不对看服务端日志有无 upgrade 请求检查path配置和防火墙频繁重连心跳超时或网络抖动抓包看心跳包是否发出调整心跳间隔检查省电模式消息延迟高输出节流窗口过大观察消息时间戳把节流窗口降到 100ms连接问题里最坑的是手机省电模式。iOS 和 Android 在锁屏后都会限制后台网络活动你的 WebSocket 可能被系统挂起。表现就是锁屏几分钟后回来消息全断了。这个没法完全避免只能靠重连机制兜底。我的做法是在页面visibilitychange事件里主动检测连接状态页面重新可见时如果连接已断就立即重连不等退避计时。4.2 Agent 执行类问题Claude Code 报 your organization has disabled claude subscription access 这类错误通常是账号权限问题和远程方案本身无关但会让人误以为是服务端配置错了。排查时先确认本地直接跑 Claude Code 是否正常本地正常再查远程链路。Codex 接入第三方模型比如 DeepSeek时要注意 base URL 和模型名的配置。Codex 的配置文件里model_provider和model两个字段要匹配写错了会报 provider 错误。这个在热词里也有体现说明踩坑的人不少。OpenCode 的免费层限制是个硬门槛。如果你看到 opencodes free tier can only be used from within opencode 这个提示说明你的调用方式不被免费层允许。要么升级套餐要么换用其他 Agent。不要试图绕过浪费时间。Agent 卡住不动的情况也常见。判断方法是看最后一条输出的时间戳超过 5 分钟没有任何输出基本可以判定卡死。这时候服务端应该主动发一个探测如果 Agent 没响应就标记为异常通知手机端。手机端给一个强制重启按钮对应服务端的 kill 加重新创建会话。4.3 我踩过的几个坑第一个坑pty 方案的终端尺寸。一开始我用 node-pty 起 Claude Code结果输出全是乱码。原因是 pty 需要一个终端尺寸默认是 80x24但 Claude Code 的 TUI 会根据尺寸调整布局尺寸不对就渲染错乱。后来改成 SDK 方案才彻底解决。如果你非要用 pty记得在 spawn 时传cols和rows并且在手机端旋转屏幕时同步更新。第二个坑输出编码。Agent 输出的中文在某些情况下会乱码尤其是通过 pty 的时候。原因是编码没设对要确保env里LANG和LC_ALL设成en_US.UTF-8或zh_CN.UTF-8。这个坑排查了很久因为英文输出正常只有中文乱很容易误以为是前端渲染问题。第三个坑会话泄漏。早期版本我没有做会话清理手机端断开后服务端的 Agent 进程还在跑跑了一天下来机器上堆了几十个僵尸进程内存直接爆了。后来加了定时清理每 5 分钟扫一遍超过 30 分钟无活动的会话自动销毁。这个逻辑一定要有不然跑久了必出问题。第四个坑权限模式太宽松。有一次我把 permissionMode 设成了完全自动结果 Agent 在执行任务时删掉了一个不该删的目录。幸好是测试环境。从那以后我坚持两个原则工作目录必须限定在项目目录内危险命令rm -rf、git push --force 之类必须走确认流程即使是在手机上也要弹一个确认框。4.4 安全加固的几条硬规矩远程操控 Agent 本质上是远程执行代码安全等级要按最高标准来。我的几条硬规矩服务只监听内网地址不做公网映射。如果必须外网访问前面加一层带强认证的反向代理并且限制来源 IP。Agent 的工作目录用白名单只能访问指定的几个项目目录不能访问家目录、系统目录。危险命令拦截。在服务端加一层命令解析遇到rm -rf /、mkfs、dd这类命令直接拒绝执行并告警。这不是万无一失但能挡住大部分误操作。操作日志全量记录。每条指令、每次工具调用、每个执行结果都落盘出问题能追溯。日志本身也要注意脱敏别把密钥之类的敏感信息记进去。提示如果你只是自己用最简单的安全方案是只在家里内网用出门通过可信的私有网络接入。不要图省事把端口开到公网那是在给自己埋雷。5. 后续可以怎么扩展这套东西跑通之后能扩展的方向不少。我目前在做的一个是任务模板把常用的指令存成模板手机上一点就执行不用每次手打。比如跑一遍测试并修复失败用例检查依赖更新这种做成按钮很实用。另一个方向是多设备同步。现在是从手机连家里机器如果我在公司也想看同一个会话就需要服务端支持多订阅者。前面提到的连接绑定用户标识就是为这个准备的改造成本不高。还有一个我觉得挺有意思的Agent 之间的协作。让 Claude Code 负责写代码Codex 负责 reviewOpenCode 负责跑测试三个 Agent 串成一个流水线。这个需要服务端做一层编排把上一个 Agent 的输出作为下一个的输入。技术上不难难的是设计好中断和回滚机制毕竟 Agent 的输出不确定性很高。最后分享一个小技巧手机端加一个语音输入按钮调用浏览器的 Web Speech API把语音转成文字填进输入框。走路的时候想给 Agent 下个指令说一句话就行比打字快多了。这个 API 在移动端支持度已经不错成本几乎为零值得一试。