docs: add SSH module design document (Chinese)

This commit is contained in:
茂之钳
2026-05-25 13:34:22 +00:00
parent 9315476a40
commit eb03204347
+435
View File
@@ -0,0 +1,435 @@
# Tergent SSH 终端模块 — 详细设计说明
> 版本: v1.3.2
> 最后更新: 2026-05-25
> 基于: hm/tergent-android (Gitea)
---
## 目录
1. [项目概述](#1-项目概述)
2. [包结构](#2-包结构)
3. [SSH 核心模块](#3-ssh-核心模块)
4. [UI 界面层](#4-ui-界面层)
5. [数据持久化](#5-数据持久化)
6. [辅助功能模块](#6-辅助功能模块)
7. [关键数据流](#7-关键数据流)
8. [修改指南](#8-修改指南)
---
## 1. 项目概述
Tergent 是基于 OpenClaw Android 源码的 SSH 终端 App,支持多会话、SFTP 文件传输、端口转发等企业级 SSH 功能。UI 采用 Jetpack Compose + Material3,深色终端风格。
**包路径**: `ai.openclaw.app.ui.tergent` — SSH 相关代码全部在此包下。
**核心框架**: Kotlin + Jetpack Compose + JSch (SSH 库) + kotlinx.serialization
---
## 2. 包结构
```
app/src/main/java/ai/openclaw/app/
├── ui/
│ ├── tergent/ # SSH 终端模块(你主要关心的)
│ │ ├── SshTerminalManager.kt # SSH 连接/通信核心引擎
│ │ ├── SshTerminalScreen.kt # 终端界面(Compose UI
│ │ ├── SshHost.kt # 主机列表 + 多会话管理
│ │ ├── SshConfigStorage.kt # 配置持久化(SharedPreferences
│ │ ├── SftpBrowserScreen.kt # SFTP 文件浏览界面
│ │ ├── SftpFileInfo.kt # SFTP 文件信息数据类
│ │ ├── PortForwardManager.kt # 端口转发管理
│ │ ├── ConnectionTester.kt # 连接测试工具
│ │ ├── QuickCommands.kt # 快捷命令
│ │ ├── DevicesScreen.kt # 设备管理页
│ │ ├── NotificationsScreen.kt # 通知历史页
│ │ ├── DiagnosticsScreen.kt # 诊断页
│ │ ├── TergentComponents.kt # 公用 UI 组件
│ │ └── TergentTheme.kt # 深色主题颜色定义
│ ├── ConnectTabScreen.kt # 连接 Tab(入口页)
│ ├── PostOnboardingTabs.kt # 主 Tab 导航
│ └── ...
├── node/ # 后台服务
│ ├── ConnectionManager.kt # 网关连接管理
│ ├── InvokeDispatcher.kt # 指令分发
│ └── ...
└── ...
```
---
## 3. SSH 核心模块
### 3.1 `SshTerminalManager.kt` — SSH 引擎
**文件**: `app/src/main/java/ai/openclaw/app/ui/tergent/SshTerminalManager.kt`
**依赖**: JSch (SSH 库) + Kotlin Coroutines
#### 数据类型
| 类型 | 用途 | 关键字段 |
|------|------|----------|
| `SshConfig` | SSH 连接配置数据类 | host, port(默认22), username, password, displayName, group(分组), authMode("password"/"key"), savedKeyPath |
| `SshConnectionState` | 连接状态的 StateFlow | connected, host, displayName, lines(终端输出行列表), error, authMode |
| `HostKeyInfo` | 主机密钥指纹 | host, type(算法), fingerprint, key |
| `KnownHostsStore` | known_hosts 存储 | 基于 SharedPreferences 的 `host|fingerprint` 集合 |
#### 核心函数
| 函数 | 作用 | 参数 | 返回值 | 说明 |
|------|------|------|--------|------|
| `connect(config)` | 密码方式 SSH 连接 | SshConfig | Boolean | 发起连接,启动输出读取协程。超时15s |
| `connectWithKey(config, keyPath, passphrase)` | 密钥方式 SSH 连接 | SshConfig + 私钥路径 + 密码 | Boolean | 加载私钥 → 连接 → 启动读取 |
| `sendCommand(command)` | 发送指令 | String | Boolean | 将指令 + `\n` 写入 OutputStream |
| `disconnect()` | 断开连接 | 无 | Unit | 清理通道、会话、流 |
| `reconnect()` | 使用上次配置重连 | 无 | Boolean | 自动判断密码/密钥模式 |
| `isConnected()` | 连接状态查询 | 无 | Boolean | 返回 _connectionState.value.connected |
| `openSftpChannel()` | 打开 SFTP 通道 | 无 | Boolean | 基于现有 SSH 会话开 SFTP |
| `closeSftpChannel()` | 关闭 SFTP 通道 | 无 | Unit | 清理 SFTP |
| `listFiles(path)` | SFTP 列出目录 | String (默认".") | List<SftpFileInfo> | 排序:目录优先,按字母 |
| `downloadFile(remote, local)` | SFTP 下载 | remote路径, local路径 | Boolean | |
| `uploadFile(local, remote)` | SFTP 上传 | local路径, remote路径 | Boolean | |
#### 内部函数
| 函数 | 作用 | 说明 |
|------|------|------|
| `readOutput()` | 循环读取终端输出(IO 线程) | 使用 ByteArray(4096) 缓冲区,过滤 ANSI 转义序列,行缓存上限 5000 行 |
| `filterAnsi(text)` | 过滤 ANSI 转义序列 | 使用正则匹配 CSI 序列 (`ESC[...`) 和 OSC 序列 (`ESC]...BEL`) |
#### 关键状态流
```
_connectionState: MutableStateFlow<SshConnectionState>
- connected: Boolean
- host: String
- lines: List<String> ← 终端输出行,随 readOutput() 不断更新
- error: String?
```
调用方通过 `connectionState: StateFlow` 收集实时输出。
---
### 3.2 `SshTerminalScreen.kt` — 终端 UI
**文件**: `app/src/main/java/ai/openclaw/app/ui/tergent/SshTerminalScreen.kt`
**技术**: Jetpack Compose + AndroidView(EditText)
#### Composable 函数
| 函数 | 作用 | 层级 | 说明 |
|------|------|------|------|
| `SshConnectDialog` | SSH 连接对话框 | 弹窗 | 支持密码/密钥两种认证方式,显示已保存连接列表 |
| `SshTerminalScreen` | 终端主界面 | 主要页面 | 终端输出 + 输入桥 + 工具栏 |
#### SshConnectDialog 的输入字段
- host / port / username / password
- 认证方式切换:密码 / 密钥
- 私钥路径选择 + 密码
- 已保存连接快速填充
- 所有字段预填支持(initialConfig 参数)
#### SshTerminalScreen 的布局结构
```
Column
├── 工具栏 (Row)
│ ├── 断开按钮(exit)
│ ├── SFTP 切换按钮
│ ├── 快捷命令列表
│ ├── 端口转发设置
│ ├── 字体大小按钮
│ └── 新建连接按钮
├── 输出区 (LazyColumn) ← 终端输出的主体
│ ├── items(state.lines) ← 每行一个 Text
│ └── item(key="prompt") ← 底部输入提示行(当前输入内容)
└── 输入桥 (Row + AndroidView)
└── EditText ← 软键盘输入入口
```
#### 输入逻辑
| 触发方式 | 位置 | 说明 |
|----------|------|------|
| EditText → setOnEditorActionListener | AndroidView → EditText | 输入法按 Send/Enter 时调用 sendCommand |
| EditText → TextWatcher.onTextChanged | AndroidView → EditText | 检测 `\n` 字符(部分输入法用此方式) |
| EditText → setOnKeyListener | AndroidView → EditText | 硬件键盘 Enter 键 |
| 终端区域点击 → requestEditTextFocus | Column.onClick | 点击输出区域重新唤起键盘 |
**当前输入缓存**: `currentInput` (remember { mutableStateOf("") })
**输入框显示**: 在 EditText 中显示输入内容,同时底部输出区最后一个 Text 也显示相同的 `currentInput`
**连接后自动焦点**: LaunchedEffect(state.connected) — 连接成功后延迟 200ms 自动弹出键盘
---
### 3.3 `SshHost.kt` — 主机管理 + 多会话
**文件**: `app/src/main/java/ai/openclaw/app/ui/tergent/SshHost.kt`
**作用**: 整个 SSH 功能的入口 Composable,管理所有活跃会话
#### Composable 函数
| 函数 | 作用 | 说明 |
|------|------|------|
| `SshHost` | 主入口 | 标题栏 + 搜索 + 会话 Tab + 内容区 |
| `ConnectionListView` | 连接列表(按组) | 搜索过滤 → 分组 → LazyColumn |
| `SavedConnectionItem` | 单个连接项 | 显示名称 + 地址 + 测试按钮 + 删除 |
#### 数据结构
```
activeSessions: mutableStateListOf<SshTerminalManager>() ← 所有活跃会话
selectedTabIndex: Int ← 当前选中的 Tab
savedConfigs: List<SshConfig> ← 保存的连接列表
```
#### 关键逻辑
1. **`openSession(config)`**: 如果密码为空且非密钥模式 → 弹出对话框让用户输入密码;否则立即创建 Tab → 异步连接
2. **`closeSession(index)`**: 断开连接 → 移除 Tab → 调整选中 Tab
3. **密钥文件选择**: 使用 `ActivityResultContracts.OpenDocument()` 系统文件选择器
4. **空密码回填**: 如果保存的连接密码为空,自动弹出预填充的 SshConnectDialog
---
## 4. UI 界面层
### 4.1 页面路由
```
PostOnboardingTabs
├── Tab: 连接 (ConnectTabScreen)
│ ├── SshHost ← SSH 功能(你最关注的部分)
│ │ ├── SshConnectDialog (模态弹窗)
│ │ ├── SshTerminalScreen (Tab 内容)
│ │ └── SftpBrowserScreen (Tab 内容)
│ └── DevicesScreen
├── Tab: 对话 (ChatSheet)
├── Tab: 设置 (SettingsSheet)
└── Tab: 引导 (OnboardingFlow)
```
### 4.2 设计主题 (`TergentTheme.kt`)
深色终端风格,定义在 `TergentTheme.kt` 中。所有 UI 颜色在文件顶部作为私有常量定义:
- `bg` = `#0D1117` (背景)
- `surface` = `#161B22` (卡片)
- `surfaceAlt` = `#1C2333` (卡片强调)
- `textPrimary` = `#E6EDF3`
- `accent` = `#3FB950` (绿色,连接成功)
- `accentBlue` = `#58A6FF` (蓝色,交互)
- `danger` = `#F85149` (红色,断开)
---
## 5. 数据持久化
### 5.1 `SshConfigStorage.kt` — 配置存储
**存储方式**: Android SharedPreferences
**Key 前缀**: `tergent_ssh_configs` (文件)
| 函数 | 作用 | 数据格式 |
|------|------|----------|
| `saveConnection(context, config)` | 保存连接配置 | JSON 序列化后入 List, 存为 JSON 数组字符串 |
| `loadConnections(context)` | 加载所有连接 | JSON 数组反序列化为 List<SshConfig> |
| `deleteConnection(context, host, port, username)` | 删除连接 | 从列表中移除并回写 |
| `saveCustomCommands(context, cmds)` | 保存快捷命令 | JSON 序列化(List<QuickCmd> |
| `loadCustomCommands(context)` | 加载快捷命令 | JSON 反序列化 |
| `saveThemeMode(context, mode)` | 保存主题 | 字符串 "dark"/"light" |
| `loadThemeMode(context)` | 加载主题 | 默认 "dark" |
| `saveFontSize(context, size)` | 保存字体 | "small"/"medium"/"large" |
| `loadFontSize(context)` | 加载字体 | 默认 "medium" |
| `fontSizeToSp(size)` | 转换字体大小 | "small"→10sp, "medium"→12sp, "large"→14sp |
### 5.2 `SshHost.kt` 中的密钥文件存储
私钥保存到 `context.filesDir/` 下,文件名 `ssh_key_{host}_{timestamp}.pem`
密钥路径列表存为 `tergent_ssh_configs` Preference 中用 `|` 分隔的字符串。
---
## 6. 辅助功能模块
### 6.1 `SftpBrowserScreen.kt` — SFTP 文件浏览器
| 函数/类型 | 作用 |
|-----------|------|
| `SftpBrowserScreen(manager, ...)` | 主界面,文件列表 |
| `loadDir(path)` | 加载远程目录,更新文件列表 |
| `formatSize(bytes)` | 格式化文件大小 (B/KB/MB) |
| 文件项点击 | 目录→进入,文件→下载提示 |
| `formatPermissions(attrs)` | 权限字符串 (rwxr-xr-x) |
| `SftpFileInfo` | 数据类: name, isDir, size, lastModified, permissions |
### 6.2 `PortForwardManager.kt` — 端口转发
| 函数/类型 | 作用 |
|-----------|------|
| `PortForward` | 数据类: localPort, remoteHost, remotePort, type(本地/远程) |
| `addForward(session, forward)` | 添加端口转发规则 |
| `removeForward(session, forward)` | 移除端口转发 |
| `listForwards(session)` | 列出当前转发 |
### 6.3 `ConnectionTester.kt` — 连接测试
| 函数/类型 | 作用 |
|-----------|------|
| `testConnection(host, port, timeoutMs)` | 异步测试 TCP 可达性 |
| `TestResult` | 数据类: reachable, host, port, latencyMs, rawOutput |
### 6.4 `QuickCommands.kt` — 快捷命令
| 函数/类型 | 作用 |
|-----------|------|
| `QuickCommand` | 数据类: name, command |
| `loadCustomCommands(context)` | 加载自定义快捷命令 |
| `saveCustomCommand(context, cmd)` | 保存快捷命令 |
| `deleteCustomCommand(context, name)` | 删除快捷命令 |
| `QuickCommandBar(...)` | Compose UI 组件:底部快捷命令栏 |
### 6.5 `TergentComponents.kt` — 公用 UI 组件
| 组件 | 作用 |
|------|------|
| `TergentStatusPill` | 状态胶囊 (连接/断开/错误) |
| `TergentPrimaryButton` | 主按钮 |
| `TergentOutlinedButton` | 描边按钮 |
| `TergentCard` | 卡片容器 |
| `TergentToggleChip` | 切换 Chip |
---
## 7. 关键数据流
### 7.1 连接流程
```
用户点击连接
openSession(config)
(密码为空?)→ 是 → 弹出 SshConnectDialog 预填 → 用户输入密码 → openSession
↓ 否
创建 SshTerminalManager → 加入 activeSessions → 选中 Tab
coroutineScope.launch { mgr.connect(config) }
JSch获取Session → 打开ChannelShell → 设置PTY → 连接
启动 readOutput() 协程(IO 线程)
_connectionState.value 不断更新 lines + connected=true
SshTerminalScreen 通过 collectAsState() 实时刷新 UI
```
### 7.2 指令发送流程
```
用户在 EditText 输入指令
按 Enter
[三个可能的触发路径之一]
├── setOnEditorActionListener (IME_ACTION_SEND/GO/DONE)
├── TextWatcher.onTextChanged (检测到 \n 字符)
└── setOnKeyListener (硬件 KEYCODE_ENTER)
manager.sendCommand(command)
(command + "\n").toByteArray(UTF-8) → OutputStream.write() → flush()
SSH 服务器执行指令,返回输出
readOutput() 读到新数据 → 更新 _connectionState.lines
UI 自动刷新最后几行
```
### 7.3 断开流程
```
用户点击断开 / Tab 关闭按钮
closeSession(index) 或 mgr.disconnect()
running.set(false) → readOutput() 退出循环
closeSftpChannel() → channel.disconnect() → session.disconnect()
_connectionState.value = SshConnectionState() (重置)
```
---
## 8. 修改指南
### 8.1 常见修改场景
#### 修改终端输入行为
**涉及文件**: `SshTerminalScreen.kt`
**位置**: `EditText` 的 AndroidView → factory block(约第 520-580 行)
**关键点**:
- `setOnEditorActionListener` — 处理输入法发送事件
- `TextWatcher` — 检测 `\n` 字符
- `setOnKeyListener` — 硬件键盘
- 三个事件处理器**都**需要触发 `sendCommand`,但要注意各处理器的 `coroutineScope` 生命周期
#### 调整终端显示样式
**涉及文件**: `SshTerminalScreen.kt`
**位置**: 文件顶部颜色常量和 `Text` 组件的 `style` 参数
**示例**: 修改字体、字号、行距、配色
#### 新增 SSH 认证方式
**涉及文件**:
- `SshTerminalManager.kt` — 添加 `connectWithCert(...)` 等方法
- `SshTerminalScreen.kt` — 在 `SshConnectDialog` 添加认证方式切换
- `SshConfig` — 添加认证方式字段
#### 修改连接保存逻辑
**涉及文件**: `SshConfigStorage.kt`
**存储位置**: `context.getSharedPreferences("tergent_ssh_configs", 0)`
**数据格式**: JSON 序列化的 `List<SshConfig>`
#### 增加新的 SSH 功能(如 SCP 传输)
**涉及文件**:
- `SshTerminalManager.kt` — 通过现有 Session 开 SCP 通道
- 新文件或集成到 `SftpBrowserScreen.kt`
### 8.2 编译注意事项
1. **Gradle 版本**: 推荐用 `gradle assemblePlayDebug`(系统 Gradle 9.4.1),wrapper 8.2 可能存在 bcprov 缓存 bug
2. **依赖**: JSch 通过 OpenClaw libs.versions.toml 管理
3. **编码**: 所有 .kt 文件必须 UTF-8(已有文件曾经有编码损坏问题)
4. **两个 flavor**: `play``thirdParty`,当前用 `playDebug`
5. **`thirdParty` flavor 有重复文件 Bug**: 如果改动了 main 中的文件可能需要在 thirdParty 中也同步改动,反之亦然
---
## 附录: 文件速查表
| 文件 | 行数(约) | 核心职责 | 主要修改点 |
|------|---------|---------|-----------|
| `SshTerminalManager.kt` | 420 | SSH 引擎, 连接/发送/断开/SFTP | 认证逻辑, 输出解析, SFTP 功能 |
| `SshTerminalScreen.kt` | 800+ | 终端 UI, 输入桥, 连接对话框 | 输入处理, 显示样式, 对话框字段 |
| `SshHost.kt` | 500+ | 主机列表, 多会话管理, Tab 导航 | 连接列表 UI, 会话生命周期 |
| `SshConfigStorage.kt` | 100 | 配置读写 (SharedPreferences) | 存储字段, 序列化格式 |
| `SftpBrowserScreen.kt` | 400 | SFTP 文件浏览器 | 文件操作, 上传/下载 UI |
| `PortForwardManager.kt` | 70 | 端口转发 | 添加/移除/列出规则 |
| `ConnectionTester.kt` | 60 | TCP 连接测试 | 超时, 延迟检测 |
| `QuickCommands.kt` | 140 | 快捷命令 | 命令列表, 按钮栏 |
| `DevicesScreen.kt` | 420 | 设备管理 | 设备列表, 状态, 连接 |
| `DiagnosticsScreen.kt` | 300 | 诊断信息 | 传感器, 系统信息, 日志 |
| `NotificationsScreen.kt` | 200 | 通知历史 | 通知列表, 清除 |
| `TergentTheme.kt` | 50 | 颜色和字体常量 | 全局颜色配置 |
| `TergentComponents.kt` | 200 | 通用 UI 组件 | 按钮, 卡片, Chip |