Files
Rtty/KNOWLEDGE_GRAPH.md
CNWei f7a6c50c0b docs: 新增项目知识图谱与新会话自动加载指引
- KNOWLEDGE_GRAPH.md: 架构/实体关系/数据流/WebSocket 协议/设计不变量/构建测试/环境踩坑/路线图
- CLAUDE.md + AGENTS.md: claude 与 Codex 会话自动加载的对接入口
- 记录关键运维约束: 服务端不可在沙盒等受限环境运行(否则 claude 等 TUI 子进程无法写状态)
2026-08-02 21:27:43 +08:00

219 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rtty 知识图谱Knowledge Graph
> 面向新会话的快速对接文档。本文件记录项目的**实体、关系、流程、协议与约束**
> 项目总览与目录导航见 [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md)。
> 新会话请先读本文件 + PROJECT_SUMMARY.md即可直接对接代码库。
---
## 1. 项目身份
| 项 | 值 |
|---|---|
| 名称 | Rttyrtty-server |
| 定位 | 仿 [dinotty](https://github.com/xichan96/dinotty) 的分布式虚拟终端系统,**只保留「终端同步」核心能力** |
| 技术栈 | 后端 RustAxum + 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<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` | 连接面板 + 快照列表渲染(颜色/光标块/光标可见滚动)+ 输入框 + KeybarESC/TAB/CTRL+C/CTRL+L/方向键/ENTER |
## 4. 数据流
### 4.1 输出路径PTY → 客户端)
```
Shell 输出
→ PtySession.readerspawn_blocking 独占)
→ TerminalEngine.feed()(引擎锁内)
→ 引擎锁内 broadcast
Raw(chunk) → desktop 客户端收二进制
Snapshot(快照) → 仅当 mobile_clients>0 时生成 → mobile 客户端收 JSON
→ desktop 订阅时/每次 resizeredraw_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 分钟)+ 读 EOFUnix→ cleanup_session幂等
```
## 5. WebSocket 协议
端点:`ws://host:port/ws?client=desktop|mobile&session=<id>&token=<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 运行,手机可看可发消息;前提:服务端非沙盒)
### 已知限制
- 移动端是"语义快照视图":文本+颜色,无完整终端渲染器;全屏 TUIvim/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` 而非手拼) |