跳到主要内容

数据截至 (上游 commit efde31963f6f)

dinotty — 架构与原理

30 秒导读: dinotty 是一台自托管的「终端服务器」:它在自己的机器上跑 shell(里面再跑 Claude Code、opencode 这类 coding agent),把终端屏幕状态解析并保存在服务端;手机、iPad、桌面浏览器只是「看屏幕 + 发按键」的客户端。所以电脑跑到一半,掏出手机能接着看、接着敲,断网刷新也回到原处。


1. 这是什么(零基础也能懂)

一句话定义: dinotty 是一个 Rust(axum + tokio)写的终端服务器,配 Vue3 网页前端、Tauri 桌面壳和 Android app,让任意设备通过浏览器连上同一批长期存活的终端会话。

解决什么问题 / 给谁用: 假设你在桌面终端里跑 Claude Code 改代码,跑到一半要出门。普通终端的会话绑死在那台机器那个窗口上——窗口一关,上下文就没了。dinotty 把「跑 agent 的进程」和「看终端的屏幕」拆开:进程跑在服务器上,屏幕也在服务器上解析好,任何设备打开浏览器就能接管。

它能做什么:

  • 多设备接力:桌面、手机、iPad 同时或先后连同一个会话,画面一致。
  • 断线续跑:断网、刷新、关浏览器,PTY 和服务端屏幕都活着,重连即恢复原画面。
  • 分屏工作台:终端、插件、文件浏览器、网页预览都是 pane,可拖拽分屏。
  • SSH 会话:内建 SSH 客户端,远程终端享受同一套同步机制。
  • 给程序用的接口:REST agent API(run/send/read)、MCP server、JS 插件、webhook。

用起来什么样:

# 在服务器(或你自己的电脑)上启动,默认端口 8999
dinotty-server --port 8999
# 手机浏览器打开 http://<服务器IP>:8999,登录后新建 tab,
# 里面就是一个跑在服务器上的 shell——在里面跑 claude 即可

一句话直觉/类比: 把传统终端想成「显存和显卡都在你显示器旁边」;dinotty 相当于把「显存」(屏幕内容的数据结构)搬到了服务端,客户端只是一个远程显示器。换显示器不影响显存里的画面。

本节不涉及实现细节;下面先看大盘。


2. 顶层全景(它大概怎么转)

一张顶层图(从左到右是「数据怎么流动」):

手机浏览器 iPad 桌面浏览器 / Tauri 壳
│ │ │
│ WebSocket(/ws 终端字节 / /ws/sync 状态同步)
▼ ▼ ▼
┌───────────────────────────────────────────────┐
│ dinotty-server (Rust / axum / tokio) │
│ │
│ ① PTY/SSH 读取 ──► ② VirtualScreen(VTE 解析) │
│ │ │ │
│ │ ├─► ③ 快照/滚动历史 │
│ ▼ │ (重连时发给客户端)│
│ ④ 原始字节 ──► broadcast ─┴─► 各客户端 mpsc │
│ │
│ ⑤ SessionManager:会话表 + 收割 + session.json │
└───────────────────────────────────────────────┘
│ fork/exec(本地 PTY) 或 russh(SSH)

bash / zsh ──► claude / opencode / codex …

部件一句话职责:

部件干什么在哪个文件
VirtualScreen服务端解析终端字节流,维护「屏幕现在长什么样」src/vt_screen/screen.rs:11
ScreenPerformer把 vte 解析出的 CSI/OSC/ESC 指令作用到屏幕缓冲src/vt_screen/performer.rs:11
Session一个终端会话:后端 + VirtualScreen + 客户端列表src/session/mod.rs:103
SessionManager会话注册表、生命周期锁、后台收割、落盘调度src/session/manager.rs:86
broadcast_task把 PTY 原始输出转发给所有挂着的客户端src/pty.rs:72
/ws 终端通道单个 pane 的输出/输入/resize/快照握手src/ws/terminal.rs:26
/ws/sync 通道tab/布局/工作区/通知等「状态」多端广播src/ws/sync.rs:28
agent API + MCP给程序(而非人)用的 run/send/read 与工具面src/agent.rs:203src/mcp/server.rs:19

主线走一遍(一次按键到回显):

  1. 手机浏览器把按键经 WebSocket 发到服务端(/ws 通道的 ClientMsg::Input,见 src/ws/types.rs:12)。
  2. 服务端写入 PTY(Session::write_input_async,src/session/mod.rs:509)。
  3. shell/agent 产生输出,PTY reader 线程读到原始字节。
  4. 同一份字节走两条路:喂给 VirtualScreen::feed 更新服务端屏幕(src/pty.rs:471);原样塞进 output_txbroadcast_task 转发给所有客户端(src/pty.rs:520)。
  5. 客户端 xterm.js 照常渲染裸字节;服务端那份屏幕则用于重连快照——新设备连上时收到滚动历史 + 整屏快照,直接画出当前画面。

关键认识:每个会话同时存在「裸字节流」和「服务端屏幕」两条数据通路。在线时客户端吃的是字节流(和普通 web 终端一样);服务端屏幕平时几乎不用,但它是断线重连、多端接力、agent API 读屏的根基——这是第 1 章的主题。


3. 阅读地图

由浅入深,建议按顺序读:

顺序章节你会学到
101-server-side-vte.md为什么屏幕要在服务端解析;VirtualScreen 怎么把字节流变成「屏幕状态」;快照怎么生成
202-session-lifecycle.md会话从创建到被收割的全过程;session.json 怎么原子落盘;重启后怎么两阶段恢复
303-multi-device-sync.md多端同步的线协议:ReplayBegin/ReplayEnd 握手、DEC 2026 同步输出、防重绘机制
404-agent-and-extensibility.md给 agent/程序用的面:REST run/send/read、MCP、插件、webhook;以及它做什么

给 AI agent 的快速路由:

  • 只想懂「断网为什么不丢」→ 读 01 + 03 的握手一节。
  • 只想懂「重启服务后会话怎么回来」→ 读 02。
  • 想把 dinotty 接进自己的 agent 程序 → 直接读 04。

4. 边界速览(详细见第 4 章)

  • dinotty 不是 coding agent 本身:它里面跑的是 claude/opencode 等真实程序,dinotty 只管终端托管与多端同步。
  • 服务端重启恢复的是「布局 + 新 shell」,原来 shell 里跑的进程回不来(恢复逻辑见 src/session/restore.rs:27);「进程不死」指的是网络断开时 PTY 活着,不是服务端进程重启后还在。
  • SSH tab 在重启恢复时被跳过,需要手动重连(src/session/restore.rs:28-29)。

5. 代码地图(导航索引)

主题文件路径符号名
服务端虚拟屏幕src/vt_screen/screen.rsVirtualScreenfeedsnapshot_for_replay
VTE 指令解析src/vt_screen/performer.rsScreenPerformercsi_dispatchosc_dispatch
会话数据结构src/session/mod.rsSessionSessionClientEventatomic_resize_and_snapshot_for_client
会话注册表/收割src/session/manager.rsSessionManagerstart_cleanup_taskclose_session
落盘与恢复src/session/snapshot.rssrc/session/restore.rsSessionSnapshotStorerestore_session
多端状态同步src/ws/sync.rssrc/session/types.rssync_handlerSyncMsg
终端通道src/ws/terminal.rsws_handlerhandle_socket
agent RESTsrc/agent.rssessions_runsessions_sendsessions_read
MCPsrc/mcp/server.rssrc/mcp/tools.rsMcpServerMcpTools::list_tools
插件src/plugin/manager.rsPluginManagerwatch_changes
webhooksrc/webhook.rsWebhookDispatcher::dispatch