docs: add SSH module design document (Chinese)
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user