From f7a6c50c0b2d9e816711da1a778844559fa9b7d8 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 2 Aug 2026 21:27:43 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=9B=BE=E8=B0=B1=E4=B8=8E=E6=96=B0=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E8=87=AA=E5=8A=A8=E5=8A=A0=E8=BD=BD=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KNOWLEDGE_GRAPH.md: 架构/实体关系/数据流/WebSocket 协议/设计不变量/构建测试/环境踩坑/路线图 - CLAUDE.md + AGENTS.md: claude 与 Codex 会话自动加载的对接入口 - 记录关键运维约束: 服务端不可在沙盒等受限环境运行(否则 claude 等 TUI 子进程无法写状态) --- AGENTS.md | 18 ++++ CLAUDE.md | 20 +++++ KNOWLEDGE_GRAPH.md | 218 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 KNOWLEDGE_GRAPH.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..545a628 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# Rtty 项目指引(Codex 会话自动加载) + +Rtty 是一个仿 dinotty 的分布式虚拟终端系统,**只保留终端同步能力**:Rust 服务端 +(Axum + portable-pty + alacritty_terminal 真相源)通过 WebSocket 同步到 +Flutter 桌面端(原始 ANSI 渲染)与移动端(语义快照)。 + +**对接前必读**: +- [KNOWLEDGE_GRAPH.md](./KNOWLEDGE_GRAPH.md) —— 知识图谱:架构、协议、数据流、不变量、踩坑记录 +- [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md) —— 项目总览与目录导航 + +**关键约束**: +- 服务端 alacritty 引擎是唯一真相源;Raw/快照/重绘帧都在引擎锁内广播(严格有序) +- 协议见 KNOWLEDGE_GRAPH.md §5;移动端颜色分段 `segments` 是扩展字段 +- 会话空闲 60s 回收;`RTTY_TOKEN` 可选鉴权;桌面端输入走二进制帧 +- 本机 flutter/git 命令需要提权(沙盒外)执行;`PUB_CACHE=C:\Users\NianJiu\AppData\Local\Pub\Cache` + +**⚠️ 最重要的一条**:服务端不能在文件写入受限的环境(沙盒)里运行,否则子进程 +(如 claude)无法写入用户状态文件,表现为 TUI 应用确认后卡死/退出。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1fbb45c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# Rtty 项目指引(Claude Code 会话自动加载) + +Rtty 是一个仿 dinotty 的分布式虚拟终端系统,**只保留终端同步能力**:Rust 服务端 +(Axum + portable-pty + alacritty_terminal 真相源)通过 WebSocket 同步到 +Flutter 桌面端(原始 ANSI 渲染)与移动端(语义快照)。 + +**对接前必读**: +- [KNOWLEDGE_GRAPH.md](./KNOWLEDGE_GRAPH.md) —— 知识图谱:架构、协议、数据流、不变量、踩坑记录 +- [PROJECT_SUMMARY.md](./PROJECT_SUMMARY.md) —— 项目总览与目录导航 + +**关键约束**: +- 服务端 alacritty 引擎是唯一真相源;Raw/快照/重绘帧都在引擎锁内广播(严格有序) +- 协议见 KNOWLEDGE_GRAPH.md §5;移动端颜色分段 `segments` 是扩展字段 +- 会话空闲 60s 回收;`RTTY_TOKEN` 可选鉴权;桌面端输入走二进制帧 + +**运行**:`cargo run`(监听 ws://0.0.0.0:8080);桌面端 `cd desktop && flutter run -d windows`; +移动端 `cd mobile && flutter build apk --debug`。 + +**⚠️ 最重要的一条**:服务端不能在文件写入受限的环境(沙盒)里运行,否则子进程 +(如 claude)无法写入用户状态文件,表现为 TUI 应用确认后卡死/退出。 diff --git a/KNOWLEDGE_GRAPH.md b/KNOWLEDGE_GRAPH.md new file mode 100644 index 0000000..3016009 --- /dev/null +++ b/KNOWLEDGE_GRAPH.md @@ -0,0 +1,218 @@ +# 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` 而非手拼) |