在 Mac 终端里跑 pocket attach,iMessage 把链接推到 iPhone,
Safari 打开就能看自己的终端、给 Claude 发指令。
终端字节走 WebRTC P2P 直连——没有任何中间服务器看到你的内容。
终端用户不注册任何账号。下二进制,开终端,扫码。
默认无链接。pocket attach 才生效,pocket sleep 立即吊销。
WebRTC DataChannel 自带 DTLS 加密;信令服务器看不到任何终端字节。
纯 P2P,不占 iOS VPN 槽位。Astrill / 公司 VPN 可以并行运行。
在 Mac Terminal 里粘贴下面这条命令。脚本会引导你登录 你自己的 Cloudflare、部署到 你自己的 免费 Worker + Pages、选择 iMessage 接收方。
作者看不到你的会话;你的免费额度只服务你一人。
curl -fsSL https://raw.githubusercontent.com/cocohahaha/joran-pocket/main/scripts/setup.sh | bash
仅 macOS。Apple Silicon + Intel 都支持。需要 Cloudflare 免费账号 (无信用卡)。
任意 Mac Terminal 窗口里:
pocket attach
helper 立即激活、注册新链接、推 iMessage 到你 iPhone、当前 Terminal 自动接管 tmux。屏幕在 attach 期间不熄屏。
pocket sleep
链接立即失效。Mac 恢复正常熄屏。下次再 attach 会推送新链接。
┌──────────────────────┐ ┌──────────────────────┐ │ iPhone Safari │ │ Mac (你的电脑) │ │ (PWA Guest) │ │ │ │ │ │ ┌────────────────┐ │ │ React + xterm.js │ │ │ Terminal.app │ │ │ + WebRTC client │ │ │ pocket attach │ │ │ │ │ └───────┬────────┘ │ │ ▲ │ │ │ │ │ │ │ │ ▼ │ │ │ DataChannel│ │ ┌────────────────┐ │ │ │ (DTLS/SCTP)│ 1) 信令握手 (短) │ │ tmux server │ │ │ │ │ ◄──────────────────────► │ │ (-L pocket) │ │ │ │ │ │ │ pocket sess. │ │ │ │ 2) 直连 P2P│ │ │ ↑↑ pane │ │ │ │ (终端字节) │ │ └────┬─┬─────────┘ │ │ └────────────┼────────────────────────────┼───────┘ │ │ │ │ │ ▼ pty │ │ │ │ ┌────────────────┐ │ └──────────────────────┘ │ │ pocket helper │ │ ▲ │ │ (Go binary) │ │ │ │ │ - WebRTC peer │ │ │ /api/register │ │ - tmux client │ │ │ /api/pair/X/ws │ │ - caffeinate │ │ ┌───────────────────┴─────────────┐ │ └────────┬───────┘ │ │ Cloudflare Pages + Workers │ │ │ │ │ (joran-pocket.pages.dev) │ │ ▼ │ │ ┌──────────────────────────┐ │ │ ~/.pocket/active │ │ │ Pages Functions │ │ │ ~/.pocket/url.txt │ │ │ POST /api/register │ │ │ ~/.pocket/code.txt │ │ │ GET /api/pair/X/ws │ │ │ (state markers) │ │ └──────────┬───────────────┘ │ └──────────────────────┘ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ PairingSession (DO) │ │ │ │ - host slot + guest slot │ │ │ │ - relay offer/answer/ICE │ │ │ └──────────────────────────┘ │ └──────────────────────────────────┘
Mac 开机 → launchd → helper boot
│
│ 删 active / url.txt / code.txt
▼
┌─────────┐
│ IDLE │ ✗ 无 DO 注册
│ │ ✗ 无链接 (任何旧 URL 都死)
│ │ ✗ 无 caffeinate
└────┬────┘
│
user: pocket attach
│ (touch ~/.pocket/active)
│ helper 250ms 内检测到
▼
┌─────────┐
│ ACTIVE │ ✓ 注册新 6 位码
│ │ ✓ 写 url.txt
│ │ ✓ 启 caffeinate (不熄屏)
│ │ ✓ iMessage 推链接
│ │ ✓ Pair loop 跑 WebRTC
└────┬────┘
│
user: pocket sleep
│ (rm ~/.pocket/active)
│ helper 2s 内检测到
▼
┌─────────┐
│ IDLE │ ← 链接立即失效
└─────────┘
| 层 | 实现 | 职责 |
|---|---|---|
| Helper (Mac) | Go + pion/webrtc + creack/pty |
tmux 客户端 + PTY 桥;IDLE/ACTIVE 状态机;caffeinate;iMessage 推送 (osascript) |
| 信令 | Cloudflare Pages Functions + Durable Object |
SDP/ICE 中转; per-code DO; 60s GC alarm; 不持有任何终端字节 |
| PWA (手机) | React + Vite + xterm.js |
WebRTC peer (guest); DOM renderer (iOS Safari 兼容); unicode-11 (CJK 列宽); 三步强制 paint |
| 持久化 | ~/.pocket/* (本地文件) |
active / url.txt / code.txt / imessage-to.txt / tmux.conf |
| 开机自启 | launchd LaunchAgent |
com.joranpocket.helper.plist; KeepAlive=true; UTF-8 locale 注入 |
phone Safari Cloudflare DO Mac helper
──────────── ───────────── ──────────
打开 /p/CODE
│
│ WS /api/pair/CODE/ws?role=guest
├──────────────────────────────►│
│ ◄────hello role=guest──────────┤
│ ◄──peer-joined role=host───────┤ (host slot 已被 helper 占)
│ ├──peer-joined role=guest──►│
│ │ │
│ │ ◄──────offer SDP───────────│
│ ◄────offer SDP─────────────────┤ │
│ acceptOffer + createAnswer │ │
├──── answer SDP ──────────────►│ ────answer SDP───────────►│
├──ICE trickle────────────────►│ ──ICE trickle────────────►│
│ ◄──ICE trickle─────────────────┤ ◄──ICE trickle────────────│
│
╔════════════════════════════════════════════════════════╗
║ P2P DTLS 直连 — Cloudflare 不再参与 ║
╠════════════════════════════════════════════════════════╣
║ pty DC: 终端字节双向 (Mac 渲染 + phone 输入) ║
║ sidechannel DC: pane_size / windows / claude_state ║
╚════════════════════════════════════════════════════════╝
| 决策 | 原因 |
|---|---|
| DOM renderer (非 canvas/WebGL) | iOS Safari 在非整数 DPR 下 canvas 字宽算错 |
| @xterm/addon-unicode11 | 默认 v6 把中文当 1 列, 跟 tmux (2 列) 错位 |
| .xterm-rows white-space:pre | nowrap 会合并连续空格 → 终端缩进消失 |
| .xterm-viewport overflow:visible | 让外层 div 接管 scroll, iOS touch 不被吃掉 |
| will-change + contain 提示 | iOS Safari 单独 compositor 层, 避免 lazy-paint |
| 三步强制 paint (refresh + RAF×2 + transform) | resize 后立即可见, 不需要划屏 |
| 决策 | 原因 |
|---|---|
| 默认 IDLE 不注册 | 链接 on-demand, 泄露窗口窄 |
| LANG=en_US.UTF-8 注入 | LaunchAgent 默认 C locale → tmux 把 \t 替换成 _ |
| sticky code 失败 5 次自动旋转 | Cloudflare DO host 槽偶尔 ghost 占用 |
| refresh-client per TTY | resize 后强制每个 client 重绘 → 投屏即时 |
| pane_size 200ms 采样 | 比 500ms 响应快 2.5x, 几乎察觉不到延迟 |
| 决策 | 原因 |
|---|---|
| Pages Functions 而非独立 Worker | PWA 静态 + 信令 API 同源, 省一个 deploy + CORS 麻烦 |
| Durable Object per code | 天然 host/guest slot 状态隔离 |
| 不存终端字节 | 仅中转 SDP/ICE (~2KB), 隐私零保留 |
| 命令 | 作用 |
|---|---|
| pocket install | 装 LaunchAgent (开机自启 helper, 但默认 IDLE) |
| pocket uninstall | 卸 LaunchAgent |
| pocket attach | 激活 helper + 注册 URL + iMessage + tmux 接管 |
| pocket sleep | 立即吊销链接 (helper 转回 IDLE) |
| pocket url | 输出当前 URL (IDLE 时无) |
| pocket status | 查 LaunchAgent / 进程 / 状态 / URL |
git clone https://github.com/cocohahaha/joran-pocket cd joran-pocket # 一键引导部署 (Cloudflare Worker + Pages + iMessage 配置) bash scripts/setup.sh # 或手动: # 1) 信令 Worker cd signaling && npm i && npx wrangler deploy # 2) PWA 静态站 cd ../pwa && npm i && npm run build && npx wrangler pages deploy dist # 3) helper 二进制 cd ../helper && go build -o pocket . sudo ln -s "$(pwd)/pocket" /opt/homebrew/bin/pocket pocket install