# Rtty 知识图谱(Knowledge Graph) > 面向新会话的快速对接文档。本文件记录项目的**实体、关系、流程、协议与约束**; > 项目总览与目录导航见 [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md)。 > 新会话请先读本文件 + PROJECT_SUMMARY.md,即可直接对接代码库。 --- ## 1. 项目身份 | 项 | 值 | |---|---| | 名称 | Rtty(rtty-server) | | 定位 | 仿 [dinotty](https://github.com/xichan96/dinotty) 的分布式虚拟终端系统,**只保留「终端同步」核心能力** | | 技术栈 | 后端 Rust(Axum + portable-pty + alacritty_terminal);前端 Flutter(桌面 Windows + 移动 Android) | | 仓库 | `https://gitea.ngmao.com/CNWei/Rtty.git`,默认分支 `developer` | | 会话 ID | 形如 `session-0`;也接受客户端自定义 ID(按 ID 原子绑定) | ## 2. 架构总览 ```mermaid flowchart LR subgraph Server["rtty-server (Rust)"] PTY["portable-pty
Shell 子进程"] ENGINE["alacritty_terminal Term
真相源:网格/历史/ANSI 解析"] WS["Axum WebSocket
会话表 + 广播"] PTY -->|原始字节流| ENGINE -->|快照/重绘| WS end subgraph Desktop["desktop (Flutter Windows)"] DB["RttyPtyBackend
WebSocket 桥接"] DA["flutter_alacritty 引擎
渲染原始 ANSI"] end subgraph Mobile["mobile (Flutter Android)"] MC["RttyMobileClient
语义快照消费"] MU["快照列表渲染
颜色/光标/Keybar"] end WS -->|"二进制原始 ANSI
client=desktop"| DB --> DA WS -->|"JSON 语义快照
client=mobile"| MC --> MU ``` ## 3. 核心实体与关系 ### 3.1 服务端(`src/`) | 实体 | 文件 | 职责 | |---|---|---| | `AppState` | `main.rs` | 全局状态:`sessions: DashMap>` + 配置 | | `ServerConfig` | `config.rs` | 环境变量配置:`RTTY_HOST/PORT/TOKEN/SHELL/COLS/ROWS/MAX_SCROLLBACK/MAX_CLIENTS/IDLE_TIMEOUT` | | `PtySession` | `terminal/pty.rs` | portable-pty 封装:reader 独占交出、writer 多客户端共享(`Arc>>`)、resize/kill/try_wait | | `TerminalEngine` | `terminal/engine.rs` | alacritty `Term` 真相源;`feed()` 逐字节喂解析器;`snapshot()` 生成移动端快照;`redraw_ansi()` 生成全屏 ANSI 重绘;`SessionListener` 把 `Event::PtyWrite`(终端应答)写回 PTY | | `Session` | `ws/handler.rs` | 一个会话 = PTY + 引擎 + broadcast 输出通道 + 控制权 + 客户端计数(`clients`/`mobile_clients`) | | `ClientKind` | `ws/handler.rs` | 按 `?client=` 参数区分:`desktop`(原始 ANSI 二进制) / `mobile`(JSON 快照),默认 desktop | ### 3.2 桌面端(`desktop/lib/`) | 实体 | 文件 | 职责 | |---|---|---| | `RttyPtyBackend` | `src/rtty_pty_backend.dart` | 实现 flutter_alacritty 的 `PtyBackend`:二进制输入、指数退避自动重连、30s 心跳、token、`onReady(id, cols, rows, resumed)` | | `TerminalScreen` | `src/terminal_screen.dart` | TerminalEngine + TerminalView 双向接线、resize 防抖(80ms) | | `ConnectionBar` | `src/connection_bar.dart` | HOST/PORT/TOKEN/SESSION 输入 + 状态灯 + `SID · RESUMED/NEW` 徽章 | | `RttyTerminalConfig` | `src/terminal_config.dart` | 配色/字体映射到 `TerminalConfig` | ### 3.3 移动端(`mobile/lib/`) | 实体 | 文件 | 职责 | |---|---|---| | `RttyMobileClient` | `src/rtty_mobile_client.dart` | `?client=mobile` 连接、快照/分段解析、自动重连、心跳、`sendInput/sendResize/claimControl` | | `TerminalPage` | `main.dart` | 连接面板 + 快照列表渲染(颜色/光标块/光标可见滚动)+ 输入框 + Keybar(ESC/TAB/CTRL+C/CTRL+L/方向键/ENTER) | ## 4. 数据流 ### 4.1 输出路径(PTY → 客户端) ``` Shell 输出 → PtySession.reader(spawn_blocking 独占) → TerminalEngine.feed()(引擎锁内) → 引擎锁内 broadcast: Raw(chunk) → desktop 客户端收二进制 Snapshot(快照) → 仅当 mobile_clients>0 时生成 → mobile 客户端收 JSON → desktop 订阅时/每次 resize:redraw_ansi() 注入 broadcast(全屏重绘) ``` **顺序不变量**:Raw / Snapshot / 重绘帧全部在**引擎锁内**发出,保证与实时输出严格有序(桌面重连恢复依赖此不变量)。 ### 4.2 输入路径(客户端 → PTY) ``` desktop: 引擎输出字节 → backend.write() → WebSocket 二进制帧 → 服务端 Message::Binary → pty.write() mobile: 输入框/Keybar → sendInput() → JSON {type:"input",data} → 服务端 → pty.write() ``` ### 4.3 会话生命周期 ``` 连接(?session=X) → 鉴权(RTTY_TOKEN 时校验 ?token=) → get_or_create_session:按 ID 原子「查-建-插」;已存在 → resumed=true,否则新建 resumed=false → Ready{id,cols,rows,resumed} →(mobile 发初始快照)→ 订阅广播 →(desktop 注入全屏重绘) → 持续双向收发 断开/退出 → clients-- ;为 0 时启动 idle_timeout(默认 60s)清理 → 兜底:退出监控(100ms 轮询 try_wait,上限 5 分钟)+ 读 EOF(Unix)→ cleanup_session(幂等) ``` ## 5. WebSocket 协议 端点:`ws://host:port/ws?client=desktop|mobile&session=&token=` ### 5.1 客户端 → 服务端(JSON 文本帧) ```jsonc {"type":"input","data":"ls\r"} // 输入,写入 PTY {"type":"resize","cols":120,"rows":32} {"type":"claim_control"} // 声明控制权(无控制者时授予) {"type":"ping"} // 心跳 → 服务端回 Pong ``` 桌面端也可用**二进制帧**直接发原始输入字节(无损)。 ### 5.2 服务端 → 客户端 | type | 说明 | |---|---| | `ready` | `{id, cols, rows, resumed}`;resumed=true 表示恢复既有会话 | | `mobile_snapshot` | `{data: MobileSnapshot}`,仅发给 mobile | | `control_response` | `{granted, holder}` | | `pong` | 心跳应答 | | `error` | `{message}`;鉴权失败等(客户端据此停止自动重连) | | `session_closed` | 会话结束(客户端停止自动重连) | ### 5.3 MobileSnapshot 结构 ```jsonc { "cursor_x": 0, "cursor_y": 0, "cols": 120, "rows": 32, "display_offset": 0, "scrollback_lines": 0, "lines": ["..."], // 纯文本行(去尾空白) "segments": [[{ // 着色分段,与 lines 一一对应 "text": "...", "fg": null, "bg": null, "bold": false, "italic": false, "underline": false }]] } ``` 颜色编码:`null` = 默认色;`"n:k"` = 命名色 0-15;`"r,g,b"` = 具体 RGB(服务端已把 256 色索引按标准色表转成 RGB)。 ## 6. 关键设计决策与不变量 1. **服务端 alacritty 引擎是唯一真相源**:断线重连恢复、移动端快照、桌面端重绘全部源于引擎网格。 2. **桌面端重连恢复**:订阅时注入 `redraw_ansi()`(`ESC[2J ESC[H` + 逐行 SGR + 光标归位);重连后画面完整,无需客户端重拉。 3. **惰性快照**:仅当存在 mobile 订阅者(`mobile_clients>0`)才生成快照,纯桌面场景零额外开销。 4. **二进制输入**:桌面端输入走二进制帧,避免 UTF-8 有损转换。 5. **控制权强制**:有控制者时非控制者输入被拒;`claim_control` 仅无控制者时授予。 6. **会话 ID 绑定**:请求的 ID 不存在时按该 ID 新建(原子 entry),存在则复用(resumed=true),杜绝并发重复创建。 7. **最小鉴权**:`RTTY_TOKEN` 全局令牌(可选);未设置时仅适合内网。 8. **快照为文本+颜色语义**,不是像素级画面:适合查看与轻量交互,不适合全屏 TUI 深度操作(vim/claude 建议用桌面端)。 ## 7. 构建 / 运行 / 测试 ```bash # 服务端(仓库根) cargo run # 监听 ws://0.0.0.0:8080 $env:RTTY_TOKEN="xxx" # 可选:启用鉴权 # 桌面端 cd desktop && flutter run -d windows flutter build windows --release # 移动端 cd mobile && flutter build apk --debug adb install -r build/app/outputs/flutter-apk/app-debug.apk # 测试 cargo test # 服务端单测 flutter test # 桌面端(服务端离线时集成测试自动跳过) flutter test # 移动端 widget 测试 ``` ## 8. 环境与操作要点(重要,踩坑记录) 1. **⚠️ 服务端必须在无文件写入限制的环境运行**:若在沙盒/受限环境启动服务端,其子进程(claude 等 TUI 程序)将无法写用户状态文件(如 `~/.claude.json`),表现为:claude 信任确认页勾选后卡死/退出、后续画面消失。**修复:在沙盒外启动服务端**(本机当前正确运行方式:提权执行后启动 `target/debug/rtty-server.exe`)。 2. **本机 Flutter 环境**:Flutter 3.44.4(`D:\flutter`),`PUB_CACHE=C:\Users\NianJiu\AppData\Local\Pub\Cache`;flutter/git 命令需要提权(沙盒外)执行,因为要写 `D:\flutter\bin\cache` 与 `.git`。 3. **adb**:手机 `3060945816001X7`;`adb install -r` 可能触发手机安装确认弹窗,重试一次即可。 4. **手机连接地址**:用 PC 局域网 IP(如 `192.168.31.249:8080`),不要用 127.0.0.1。 5. **Android 明文 WebSocket**:Manifest 已开 `usesCleartextTraffic` + `INTERNET` 权限。 6. **会话空闲清理**:默认 60s 无客户端即回收会话(PTY 被杀);测试/抓快照要保证有客户端在线。 ## 9. 当前状态与路线图 ### 已实现 - 服务端 VTE 真相源(alacritty_terminal)、会话持久化 + `resumed` 语义、多端解耦广播 - 桌面端全屏重绘恢复 + 自动重连 + 二进制输入 + token + 心跳 - 移动端最小验证 App:快照渲染(颜色/光标)、自动重连、Keybar(含 ENTER)、token - 最小鉴权(`RTTY_TOKEN`)、Ping/Pong、并发会话创建竞态修复 - **claude 端到端可用**(claude 在服务端 PTY 运行,手机可看可发消息;前提:服务端非沙盒) ### 已知限制 - 移动端是"语义快照视图":文本+颜色,无完整终端渲染器;全屏 TUI(vim/claude 深度操作)体验有限 - 移动端无文本换行(Word Wrap)、无 Keybar 自定义、无粘贴支持 - 鉴权仅全局令牌,无细粒度权限/会话绑定 ### 规划 - 移动端 TUI 透传模式(全屏快照渲染 + 虚拟键盘) - 智能换行 / 卡片化命令交互 / Keybar 自定义 - 分布式横向扩展、服务端细粒度鉴权 ## 10. 常见问题速查 | 现象 | 原因/解法 | |---|---| | 手机连不上 | 用 LAN IP;确认服务端在跑(`Test-NetConnection ip -Port 8080`) | | 手机显示空白 | 会话被空闲回收(60s)→ 重新连接;或确认 `?client=mobile` 已带上 | | claude 勾选确认后画面消失 | **服务端运行在受限环境** → 改为沙盒外启动 | | 桌面端重连后画面空白 | 检查服务端为最新构建(含 `redraw_ansi`);旧二进制无重绘功能 | | `flutter` 命令卡住 | 本机 flutter 需提权运行;设置 `PUB_CACHE` 指向 NianJiu 的 Pub 缓存 | | 输入乱码/被吞 | 桌面端应走二进制帧;`\r` 进 JSON 必须转义(用 `ConvertTo-Json` 而非手拼) |