- KNOWLEDGE_GRAPH.md: 架构/实体关系/数据流/WebSocket 协议/设计不变量/构建测试/环境踩坑/路线图 - CLAUDE.md + AGENTS.md: claude 与 Codex 会话自动加载的对接入口 - 记录关键运维约束: 服务端不可在沙盒等受限环境运行(否则 claude 等 TUI 子进程无法写状态)
11 KiB
11 KiB
Rtty 知识图谱(Knowledge Graph)
面向新会话的快速对接文档。本文件记录项目的实体、关系、流程、协议与约束; 项目总览与目录导航见 PROJECT_SUMMARY.md。 新会话请先读本文件 + PROJECT_SUMMARY.md,即可直接对接代码库。
1. 项目身份
| 项 | 值 |
|---|---|
| 名称 | Rtty(rtty-server) |
| 定位 | 仿 dinotty 的分布式虚拟终端系统,只保留「终端同步」核心能力 |
| 技术栈 | 后端 Rust(Axum + portable-pty + alacritty_terminal);前端 Flutter(桌面 Windows + 移动 Android) |
| 仓库 | https://gitea.ngmao.com/CNWei/Rtty.git,默认分支 developer |
| 会话 ID | 形如 session-0;也接受客户端自定义 ID(按 ID 原子绑定) |
2. 架构总览
flowchart LR
subgraph Server["rtty-server (Rust)"]
PTY["portable-pty<br/>Shell 子进程"]
ENGINE["alacritty_terminal Term<br/>真相源:网格/历史/ANSI 解析"]
WS["Axum WebSocket<br/>会话表 + 广播"]
PTY -->|原始字节流| ENGINE -->|快照/重绘| WS
end
subgraph Desktop["desktop (Flutter Windows)"]
DB["RttyPtyBackend<br/>WebSocket 桥接"]
DA["flutter_alacritty 引擎<br/>渲染原始 ANSI"]
end
subgraph Mobile["mobile (Flutter Android)"]
MC["RttyMobileClient<br/>语义快照消费"]
MU["快照列表渲染<br/>颜色/光标/Keybar"]
end
WS -->|"二进制原始 ANSI<br/>client=desktop"| DB --> DA
WS -->|"JSON 语义快照<br/>client=mobile"| MC --> MU
3. 核心实体与关系
3.1 服务端(src/)
| 实体 | 文件 | 职责 |
|---|---|---|
AppState |
main.rs |
全局状态:sessions: DashMap<id, Arc<Session>> + 配置 |
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<Mutex<Box<dyn Write>>>)、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=<id>&token=<token>
5.1 客户端 → 服务端(JSON 文本帧)
{"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 结构
{
"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. 关键设计决策与不变量
- 服务端 alacritty 引擎是唯一真相源:断线重连恢复、移动端快照、桌面端重绘全部源于引擎网格。
- 桌面端重连恢复:订阅时注入
redraw_ansi()(ESC[2J ESC[H+ 逐行 SGR + 光标归位);重连后画面完整,无需客户端重拉。 - 惰性快照:仅当存在 mobile 订阅者(
mobile_clients>0)才生成快照,纯桌面场景零额外开销。 - 二进制输入:桌面端输入走二进制帧,避免 UTF-8 有损转换。
- 控制权强制:有控制者时非控制者输入被拒;
claim_control仅无控制者时授予。 - 会话 ID 绑定:请求的 ID 不存在时按该 ID 新建(原子 entry),存在则复用(resumed=true),杜绝并发重复创建。
- 最小鉴权:
RTTY_TOKEN全局令牌(可选);未设置时仅适合内网。 - 快照为文本+颜色语义,不是像素级画面:适合查看与轻量交互,不适合全屏 TUI 深度操作(vim/claude 建议用桌面端)。
7. 构建 / 运行 / 测试
# 服务端(仓库根)
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. 环境与操作要点(重要,踩坑记录)
- ⚠️ 服务端必须在无文件写入限制的环境运行:若在沙盒/受限环境启动服务端,其子进程(claude 等 TUI 程序)将无法写用户状态文件(如
~/.claude.json),表现为:claude 信任确认页勾选后卡死/退出、后续画面消失。修复:在沙盒外启动服务端(本机当前正确运行方式:提权执行后启动target/debug/rtty-server.exe)。 - 本机 Flutter 环境:Flutter 3.44.4(
D:\flutter),PUB_CACHE=C:\Users\NianJiu\AppData\Local\Pub\Cache;flutter/git 命令需要提权(沙盒外)执行,因为要写D:\flutter\bin\cache与.git。 - adb:手机
3060945816001X7;adb install -r可能触发手机安装确认弹窗,重试一次即可。 - 手机连接地址:用 PC 局域网 IP(如
192.168.31.249:8080),不要用 127.0.0.1。 - Android 明文 WebSocket:Manifest 已开
usesCleartextTraffic+INTERNET权限。 - 会话空闲清理:默认 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 而非手拼) |