从手机控制本地 Mac 上的 pi coding agent —— 仿 codex-mobile。
浏览器即 UI(PWA,可加到主屏幕)。后端复用 pi 的 SDK(RpcClient + SessionManager),
所以会话、模型、思考等级全部和你终端里的 pi 完全一致、同一份会话文件。
- 列出所有项目 / 会话(按项目分组,最近活跃在前)
- 打开任意历史会话,查看完整对话(含 thinking / 工具调用 / 工具结果)
- 发消息驱动 agent,流式显示回复
- 运行中「停止」(abort)
- 续聊直接写回原会话文件,和桌面端 pi 共享
手机浏览器 (PWA)
│ WebSocket (实时事件流) + HTTP (REST: 列表/历史)
▼
Mac 上的 pi-mobile server (Node, 复用 pi SDK)
├─ SessionManager.listAll() 列项目/会话
├─ SessionManager.open().buildSessionContext() 读历史
├─ RpcClient (每会话一个, 空闲回收) prompt/steer/abort/set_model...
│ └─ spawn `pi --mode rpc --session <file>`
└─ token 鉴权 (defence-in-depth)
src/sessions.js— 只读:列会话、读历史并扁平化成前端友好结构src/rpc-pool.js— 按会话路径管理RpcClient实例,多 viewer 共享,空闲超时回收src/ws-bridge.js— WebSocket ↔ RpcClient 命令/事件桥接src/server.js— HTTP(REST + 静态)+ WS 入口 + token + 加载/分发插件src/plugins/— 后端插件(index.js加载器 + 各插件目录),见「插件」web/— 零构建原生前端(index.html / app.js / manifest)web/plugins/— 前端插件(loader.js+ 各插件目录)
pi-mobile 不是独立的 agent,它是 pi 这个本地 coding agent 的远程外壳。 先把 pi 装好、能在终端正常跑起来,pi-mobile 才有东西可驱动。
npm install -g @earendil-works/pi-coding-agent
pi --version # 验证,应输出版本号(本项目基于 0.79.4)pi 支持几十种 provider(Anthropic / OpenAI / Gemini / DeepSeek / Moonshot / …), 通过环境变量提供 key。任选其一即可,例如:
export ANTHROPIC_API_KEY=sk-ant-... # Claude
# 或
export OPENAI_API_KEY=sk-... # GPT完整变量列表见 pi --help。也可以用 pi config 打开 TUI 启用/管理资源。
cd ~/your-project # 进到要让 agent 工作的目录
pi # 交互模式,发个 "hi" 确认能正常对话pi 的会话按工作目录存在
~/.pi/agent/sessions/--项目路径--/<时间戳>_<uuid>.jsonl。 pi-mobile 读写的就是这些文件 —— 所以手机和终端是同一份会话,两边可无缝切换。
| 操作 | 命令 |
|---|---|
| 交互模式 | pi |
| 带初始 prompt | pi "帮我重构这个函数" |
| 指定模型 | pi --model anthropic/claude-opus-4-7 或 pi --model sonnet:high |
| 继续上次会话 | pi -c |
| 选择历史会话恢复 | pi -r |
| 非交互、跑完即退 | pi -p "总结这个仓库" |
| 只读模式(禁文件修改) | pi --no-tools 或 -nt |
| 列出可用模型 | pi --list-models |
这些在 pi-mobile 里的对应:选会话 = -r,发消息 = 交互,切模型的后端命令已就绪(前端 UI 待接)。
npm install
npm start启动后会打印手机访问地址和二维码链接:
│ phone: http://192.168.x.x:4180/?token=xxxxxxxx
└─ QR: https://api.qrserver.com/.../?data=... # 浏览器打开即可扫码
手机连同一个 WiFi,扫码或直接输地址即可。token 持久化在 ~/.pi-mobile/token,重启不变。
⚠️ 这是个能在你 Mac 上跑命令的 agent 的远程入口。token 不要外泄;公共 WiFi 下优先用 Tailscale(见下)。
出门也能连、全程加密,比裸 token 暴露在公网安全得多:
- Mac 装 Tailscale:
brew install --cask tailscale,打开 App 登录你的账号。 - 手机装 Tailscale App,登录同一账号。
- 拿到 Mac 的 tailnet IP:
tailscale ip -4(形如100.x.x.x)。 - 手机浏览器访问
http://100.x.x.x:4180/?token=...。
server 本来就监听 0.0.0.0,无需改动。
bash deploy/install-launchd.sh会自动填好路径、加载 agent,崩溃/登录自动拉起。
tail -f ~/.pi-mobile/server.log # 日志
cat ~/.pi-mobile/token # 当前 token
launchctl unload ~/Library/LaunchAgents/com.pi-mobile.server.plist # 停| 变量 | 默认 | 说明 |
|---|---|---|
PI_MOBILE_PORT |
4180 |
监听端口 |
PI_MOBILE_HOST |
0.0.0.0 |
监听地址 |
PI_MOBILE_TOKEN |
持久化生成 | 覆盖 token |
PI_MOBILE_IDLE_MS |
600000 |
会话空闲多久回收 RpcClient 子进程 |
PI_MOBILE_PLUGINS |
workflow |
启用的插件,逗号分隔;设为空字符串则全部停用(见下「插件」) |
pi-mobile 的核心只做「列会话 / 看历史 / 驱动 agent」。其余功能(如工作流步骤条)都是 插件:自包含、可整目录删除、用环境变量开关,不改核心代码即可扩展。
用 PI_MOBILE_PLUGINS(逗号分隔,默认 workflow):
npm start # 默认启用 workflow
PI_MOBILE_PLUGINS= npm start # 全部停用
PI_MOBILE_PLUGINS=workflow,foo npm start # 启用多个启动日志会打印当前启用的插件;前端通过 GET /api/plugins 拿到清单后动态加载。
| 插件 | 作用 |
|---|---|
workflow |
顶部步骤条,可视化多 Agent 工作流进度(规划 › 规划评审 › 构建 › 代码评审 › 验证)。数据来自会话文件里 pi subagent 运行时持久化的 vstack-subagents:runtime-state 记录,前端每 ~3s 轮询刷新。 |
一个插件 = 后端目录 src/plugins/<name>/ + 前端目录 web/plugins/<name>/,名字一致。
后端 src/plugins/<name>/server.js(default export):
export default {
name: "<name>", // 必须与目录名一致
// 注册 HTTP 路由:pathname -> async(ctx) => { status, body } | undefined
routes: {
"/api/<name>": async ({ url, isSessionPath }) => {
const p = url.searchParams.get("path");
if (!p || !isSessionPath(p)) return { status: 400, body: { error: "bad path" } };
return { status: 200, body: { /* ... */ } };
},
},
// 可选:就地装饰 /api/session 响应
async decorateSession(session, sessionPath) {
session.myField = await compute(sessionPath);
},
};
ctx = { req, url, isSessionPath, send }由核心注入;鉴权(token)已在分发前统一完成。
前端 web/plugins/<name>/client.js(default export):
export default {
name: "<name>",
init(host) { /* 一次性:注入 style、挂载 UI */ },
onSessionOpen(sess, data) { /* data 是 /api/session 全量响应(含 decorateSession 注入的字段) */ },
onSessionClose() { /* 切/关会话:清理 UI、停定时器 */ },
onAgentEvent(ev) { /* 转发的 RpcClient 事件:turn_end / agent_end / ... */ },
};host(核心提供给插件的稳定能力,传入 init):
| 方法 | 说明 |
|---|---|
host.el(tag, cls?, text?) |
创建 DOM 元素 |
host.$(id) |
getElementById |
host.api(path) |
带 token 的 fetch → JSON |
host.getCurrentSession() |
当前会话 { path, cwd, name } 或 null |
host.slot(name) |
命名挂载点容器,目前有 "belowHeader"(顶栏正下方) |
host.showSheet(title, text) |
复用底部弹层显示一段文字 |
隔离性:
init与各生命周期钩子都被 try/catch 包裹,单个插件报错只打 warning,不影响核心。 静态资源由核心统一托管web/plugins/**,无需额外配置。
参考实现见 src/plugins/workflow/ 与 web/plugins/workflow/。
node scripts/verify-sdk.mjs # 阶段0: SDK 连通性(列会话 + 一次 prompt 往返)
node scripts/ws-test.mjs # 阶段2: WS 往返(需先在 4181 起 server)- ✅ 切换模型 / 切思考等级:底部 meta 栏点 model / 🧠 pill 弹出选择器即可切换。
- fork / 新建会话:后端命令已就绪(ws-bridge),前端 UI 尚未接入。
- 工具批准(approval):当前依赖 pi 默认行为,未做手机端逐次批准。
- 图片输入未接。
MIT © Ebispongebob — 见 LICENSE。