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/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):

文件服务 (`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/`)

二、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)

页面

技术亮点

1. 零信任架构

Roam 采用真正的零信任设计:

2. node-pty 的优雅使用

通过 node-pty 创建真实 PTY 而不是用 child_process.spawn 模拟,实现了:

3. 跨平台截图方案

一套代码兼容 3 个操作系统,4 种 Linux 截图工具,fallback 链完整。

4. Cloudflare 免费套餐友好

安全问题(值得注意)

风险严重程度说明
配置泄露`config.js` 含 Worker URL,`.gitignore` 已排除但需注意
文件操作无权限限制Server 文件服务**没有路径合法性校验**,`fs.list('../../../etc')` 可能跨目录访问
终端全权限远程终端 = 完整 shell 权限
上传文件覆盖overwrite 参数用户可控
Worker 域名暴露只要有 Worker URL + session ID 就能尝试连接

建议:始终设置 SESSION_PASSWORD,不要留空。

与类似方案的对比

特性RoamTailscale + SSHngrokVS Code Remote
需要公网端口
浏览器访问✅ (code-server)
终端支持✅ (原生 PTY)
文件管理❌ (需 scp)
屏幕截图
自建中继✅ (CF Worker)❌ (依赖 Tailnet)
零信任架构✅ 主动出站❌ 暴露端口
部署复杂度低 (npm install)

评分

维度分数说明
代码质量⭐⭐⭐⭐代码干净、模块化清晰、ESM 模块
架构设计⭐⭐⭐⭐⭐零信任 + 主动出站,设计非常优雅
实用性⭐⭐⭐⭐解决真实痛点(远程连 AI agent)
安全性⭐⭐⭐挑战-响应认证好,但文件操作无路径校验
可扩展性⭐⭐⭐目前功能直接,但架构便于扩展
部署简易度⭐⭐⭐⭐npm install → 配置 → 部署 Worker → 启动

一句话再总结

> "Roam 让你在手机上打开一个网页就能操控电脑的终端——不暴露端口、数据不过第三方服务器、装好就能用。想继续跟 Claude Code 聊天?掏出手机就行。"

给你的建议

Roam 的设计思路(主动出站连接 + Worker 中继)和你已有的方案很契合:

报告生成时间: 2026-05-08 11:07 UTC

分析基于 valueriver/roam main branch 源码