Roam 源码深度分析: 通过 Cloudflare Worker 远程访问本机终端/文件/屏幕
> 来源: GitHub - valueriver/roam | V2EX 讨论
> 日期: 2026-05-08
> 作者: valueriver | 许可: MIT
> 语言: JavaScript + Vue 3 + Cloudflare Workers
> Star: 16 | 标签: remote-access, terminal, websocket, cloudflare-workers
一句话版本
Roam 让你在手机上打开一个网页,就能操控电脑的终端、文件、屏幕——电脑不暴露公网,通过 Cloudflare Worker 做中继,数据不经过任何人。
它解决了什么问题?
当你用 Claude Code / Codex / 其他终端 AI 编程工具时,离开电脑就没法继续对话了。Roam 就是解决这个问题的:把本机的终端、文件系统、屏幕截图带到任意设备的浏览器上。
架构总览
┌─────────────────────────┐ wss ┌─────────────────────┐ wss ┌──────────────────────┐
│ 本机 Roam Server │ ──────────────→ │ Cloudflare Worker │ ──────────────→ │ 远程浏览器 │
│ (Node.js + node-pty) │ 主动连接 │ (Durable Object) │ 连接 │ (Vue 3 SPA) │
│ │ │ 纯中继,不存数据 │ │ │
│ devices: 'desktop' │ │ │ │ devices: 'web' │
└─────────────────────────┘ └─────────────────────┘ └──────────────────────┘
↑ ↑
│ 终端 (node-pty) │ 终端页面 (xterm.js)
│ 文件 (fs/promises) │ 文件管理器
│ 截图 (screencapture) │ 屏幕截图
│ 系统状态 (os.cpus/os.totalmem) │ 状态面板
核心设计原则
1. 本机不暴露公网:Server 主动连接 Worker,不是被动等待连接
2. Worker 纯透传:不保存任何业务数据
3. 双向 WebSocket:数据流实时双向
源码模块详解
一、本机 Server (server/)
启动流程 (`server/index.js`)
async function boot() {
guard.bindOnGrant((clientId) => {
terminal.sendSnapshotTo(clientId); // 认证通过 → 推送终端快照
});
router.bindOnDevicesChanged((devices) => {
if (devices?.web === 'connected') {
terminal.sendSnapshotAll(); // 网页端接入 → 推送所有终端
guard.sendAuthMode(); // 推送认证模式
}
});
await terminal.ensureDefault(); // 自动创建默认终端
ws.init({...}); // 连接 Worker WebSocket
}
网络层 (`server/ws.js`)
function connect() {
const params = new URLSearchParams({ session: sessionId, device: 'desktop' });
const url = `${SERVER_URL}/ws?${params.toString()}`;
state.ws = new WebSocket(url);
// onOpen: 打印访问入口 URL
// onClose: 3 秒自动重连
}
// 三种发送模式
send(msg) → 发给 Worker(中继)
sendToClient(id, msg) → 发给指定 web 客户端
broadcast(type, data) → 发给所有 web 客户端
关键设计点:
- Server 作为
device: 'desktop'主动连接 Worker - 断开后 3 秒自动重连
- 可配置
SESSION_ID(固定)或随机生成
消息路由器 (`server/router.js`)
connection.ping → 心跳 (reply pong)
connection.devices → 设备状态变更通知
auth.* → 密码认证(挑战-响应协议)
terminal.create → 创建新终端 (node-pty)
terminal.activate → 切换活跃终端
terminal.close → 关闭终端
data.input → 向终端写入输入
system.init/resize → 终端初始化/尺寸调整
system.command → 系统命令执行
fs.list/read/... → 文件操作
screen.capture → 截取屏幕
status.request → 系统状态查询
终端服务 (`services/terminal/`)
使用 node-pty 创建真实的伪终端(PTY):
// sessions.js
function create(options) {
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols, rows, cwd, env: process.env,
});
ptyProcess.onData((data) => {
ws.broadcast('data.output', { terminalId, output: data });
});
terminals.set(id, terminal);
}
支持多终端:终端以 Map 管理,每个都有独立的 PTY 进程,支持切换活跃终端、自动创建默认终端、自动重启退出终端。
Shell 自动检测 (shell.js):
- Windows → powershell.exe
- macOS/Linux → $SHELL 或 bash
- 默认工作目录:Desktop(存在时)或 home
文件服务 (`services/files/`)
文件操作通过 WebSocket 消息透传执行:
| 操作 | 实现 | 说明 |
|---|---|---|
| `fs.list` | `fsp.readdir` + `fsp.stat` | 排序:目录优先,按名称 |
| `fs.read` | `fsp.readFile` + base64 | 带 maxSize 限制 |
| `fs.upload` | 分块写入临时文件 → 重命名 | `start/chunk/abort` 协议 |
| `fs.delete` | `fsp.rm` | 支持 recursive |
| `fs.rename` / `fs.mkdir` | 标准文件 API |
上传协议:分块上传,支持续传进度反馈。
屏幕截图 (`services/screen/`)
跨平台截图方案:
async function capturePng() {
// macOS: screencapture -x -t png
// Windows: PowerShell + System.Windows.Forms
// Linux: 依次尝试 gnome-screenshot / spectacle / scrot / import
}
截图结果 base64 编码后通过 WebSocket 传输到浏览器显示。
密码认证 (`services/guard/`)
采用挑战-响应协议,密码不在线路上传输:
1. 浏览器请求挑战 → Server 生成随机 nonce
2. Server 发送 nonce 到浏览器
3. 浏览器:HMAC-SHA256(password, nonce) → proof
4. Server:用已知密码验证 proof
5. 通过 → 颁发 authToken(30 天免登录)
失败次数限制(lockout.js)、nonce 一次性使用(nonces.js)。
系统状态 (`services/status/`)
- CPU:
os.cpus()采样 200ms 算使用率 - 内存:
os.totalmem()/os.freemem() - 磁盘:
df -k /命令输出解析 - 网络:
os.networkInterfaces()过滤非内部 IPv4
二、Cloudflare Worker (worker/)
Durable Object: TerminalSessionManager
这是整个系统的大脑——所有 WebSocket 连接和消息路由都在这里:
核心功能:
1. 连接管理:维护 desktop 和 web 两组 WebSocket 连接
2. 消息路由:根据 msg.to 字段决定转发目标(desktop / web / web:clientId / all)
3. 认证状态持久化:requiresPassword + authTokens 通过 Durable Object Storage 持久化
4. 单设备独占:新的 web 认证通过后,自动踢掉其他已认证的 web 连接
5. 免登录 Token:30 天有效 authToken
// 消息路由核心
route(msg) {
if (target === 'desktop') targets = this.ctx.getWebSockets('desktop');
else if (target === 'web') targets = this.ctx.getWebSockets('web').filter(auth);
else if (target.startsWith('web:')) targets = [findSocket(target.slice(4))];
// 广播
for (const ws of targets) ws.send(JSON.stringify(msg));
}
fetch handler 流程:
/ws?session=xxx&device=desktop → Durable Object (idFromName)
/ws?session=xxx&device=web&authToken=yyy → Durable Object
其他路径 → env.ASSETS.fetch(request) // 静态文件(Vue SPA)
三、前端 (worker/src/)
Vue 3 + Pinia + Vue Router 的单页应用。
核心 store (ws.js):
- WebSocket 连接生命周期管理
- 挑战-响应认证流程
- 自动重连(10 秒优雅降级窗口)
- 免登录 token 管理(localStorage)
- 设备状态追踪
页面:
GuardView.vue:密码输入TerminalView.vue:终端显示 + 输入(底部输入框 + 快捷键)FilesView.vue:文件管理器(列表/上传/预览/删除/重命名)ScreenView.vue:屏幕截图查看StatusView.vue:CPU/内存/磁盘/网络状态
技术亮点
1. 零信任架构
Roam 采用真正的零信任设计:
- Server 主动出站连接,不需要开放任何端口
- Worker 只做透传,不存储任何数据
- 密码经 HMAC 哈希后传输,原文永不在线
- 可选的
SESSION_PASSWORD二次验证
2. node-pty 的优雅使用
通过 node-pty 创建真实 PTY 而不是用 child_process.spawn 模拟,实现了:
- 完整的 ANSI 转义序列支持
- 终端尺寸动态调整(resize)
- 多终端切换
- 与 Claude Code / Codex 完全兼容
3. 跨平台截图方案
一套代码兼容 3 个操作系统,4 种 Linux 截图工具,fallback 链完整。
4. Cloudflare 免费套餐友好
- Durable Object 的免费额度足够个人使用
- Worker 静态文件托管(Vue SPA 免费部署)
- 不需要额外的服务器
安全问题(值得注意)
| 风险 | 严重程度 | 说明 |
|---|---|---|
| 配置泄露 | 高 | `config.js` 含 Worker URL,`.gitignore` 已排除但需注意 |
| 文件操作无权限限制 | 中 | Server 文件服务**没有路径合法性校验**,`fs.list('../../../etc')` 可能跨目录访问 |
| 终端全权限 | 中 | 远程终端 = 完整 shell 权限 |
| 上传文件覆盖 | 低 | overwrite 参数用户可控 |
| Worker 域名暴露 | 低 | 只要有 Worker URL + session ID 就能尝试连接 |
建议:始终设置 SESSION_PASSWORD,不要留空。
与类似方案的对比
| 特性 | Roam | Tailscale + SSH | ngrok | VS Code Remote |
|---|---|---|---|---|
| 需要公网端口 | ❌ | ❌ | ✅ | ❌ |
| 浏览器访问 | ✅ | ❌ | ✅ | ✅ (code-server) |
| 终端支持 | ✅ (原生 PTY) | ✅ | ❌ | ✅ |
| 文件管理 | ✅ | ❌ (需 scp) | ❌ | ✅ |
| 屏幕截图 | ✅ | ❌ | ❌ | ❌ |
| 自建中继 | ✅ (CF Worker) | ❌ (依赖 Tailnet) | ❌ | ❌ |
| 零信任架构 | ✅ 主动出站 | ✅ | ❌ 暴露端口 | ✅ |
| 部署复杂度 | 低 (npm install) | 中 | 低 | 高 |
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 代码质量 | ⭐⭐⭐⭐ | 代码干净、模块化清晰、ESM 模块 |
| 架构设计 | ⭐⭐⭐⭐⭐ | 零信任 + 主动出站,设计非常优雅 |
| 实用性 | ⭐⭐⭐⭐ | 解决真实痛点(远程连 AI agent) |
| 安全性 | ⭐⭐⭐ | 挑战-响应认证好,但文件操作无路径校验 |
| 可扩展性 | ⭐⭐⭐ | 目前功能直接,但架构便于扩展 |
| 部署简易度 | ⭐⭐⭐⭐ | npm install → 配置 → 部署 Worker → 启动 |
一句话再总结
> "Roam 让你在手机上打开一个网页就能操控电脑的终端——不暴露端口、数据不过第三方服务器、装好就能用。想继续跟 Claude Code 聊天?掏出手机就行。"
给你的建议
Roam 的设计思路(主动出站连接 + Worker 中继)和你已有的方案很契合:
- 你的 GitHub Actions + Pages 部署流程也可以用类似的 relay 模式
node-pty的使用方式值得参考- 如果你想加一个"远程管理 VPS 终端"的能力,Roam 的架构可以复用
报告生成时间: 2026-05-08 11:07 UTC
分析基于 valueriver/roam main branch 源码