# 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 | 排序:目录优先,按字母 | | `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 - connected: Boolean - host: String - lines: List ← 终端输出行,随 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() ← 所有活跃会话 selectedTabIndex: Int ← 当前选中的 Tab savedConfigs: List ← 保存的连接列表 ``` #### 关键逻辑 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 | | `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` #### 增加新的 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 |