17 KiB
Tergent SSH 终端模块 — 详细设计说明
版本: v1.3.2
最后更新: 2026-05-25
基于: hm/tergent-android (Gitea)
目录
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 |
核心函数
| 函数 | 作用 | 参数 | 返回值 | 说明 |
|---|---|---|---|---|
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 | 排序:目录优先,按字母 |
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> ← 保存的连接列表
关键逻辑
openSession(config): 如果密码为空且非密钥模式 → 弹出对话框让用户输入密码;否则立即创建 Tab → 异步连接closeSession(index): 断开连接 → 移除 Tab → 调整选中 Tab- 密钥文件选择: 使用
ActivityResultContracts.OpenDocument()系统文件选择器 - 空密码回填: 如果保存的连接密码为空,自动弹出预填充的 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=#E6EDF3accent=#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 |
deleteConnection(context, host, port, username) |
删除连接 | 从列表中移除并回写 |
saveCustomCommands(context, cmds) |
保存快捷命令 | JSON 序列化(List) |
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 编译注意事项
- Gradle 版本: 推荐用
gradle assemblePlayDebug(系统 Gradle 9.4.1),wrapper 8.2 可能存在 bcprov 缓存 bug - 依赖: JSch 通过 OpenClaw libs.versions.toml 管理
- 编码: 所有 .kt 文件必须 UTF-8(已有文件曾经有编码损坏问题)
- 两个 flavor:
play和thirdParty,当前用playDebug thirdPartyflavor 有重复文件 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 |