12 KiB
12 KiB
Tergent — Android SSH 终端客户端
版本: v0.3.0 | 最后更新: 2026-05-23 当前版本: v0.9.0 — 连接测试 仓库: hm/tergent-android
1. 产品定位
Tergent 是一个 Android 平台上的 SSH 终端客户端,专注于提供类 Linux 终端的手机操作体验。
核心目标
- 在手机上获得接近桌面终端的 SSH 操作体验
- 完整的键盘控制(方向键、Tab、Ctrl 组合键、功能键)
- 轻量、快速、不依赖任何外部服务
设计原则
| 原则 | 说明 |
|---|---|
| 终端优先 | 不是 IM 或远程控制 App,就是终端 |
| 纯客户端 | 不需要配对、不需要网关、不需要注册 |
| 手机原生 | 针对触摸屏优化了键盘工具栏 |
| 离线可用 | 不启动后台服务,不请求多余权限 |
2. 版本历史
| 版本 | 日期 | 说明 |
|---|---|---|
| v0.1.0 | 2026-05-22 | 基于 OpenClaw Android 源码改造,改名 Tergent,APK 可编译 |
| v0.1.1 | 2026-05-23 | UI 重写 + SSH 终端(依赖 OpenClaw 网关模式) |
| v0.3.0 | 2026-05-23 | 独立 SSH 客户端,移除 OpenClaw 依赖 |
v0.3.0 关键变更
- ❌ 移除 Onboarding 引导页(不再需要扫码配对)
- ❌ 移除 OpenClaw 网关连接/状态/配对逻辑
- ❌ 移除 NodeRuntime、MainViewModel 等服务层
- ❌ 移除 ForegroundService、通知监听等后台服务
- ✅ 启动直接进入 SSH 连接对话框
- ✅ SSH 连接管理(JSch 库,0.1.55)
- ✅ 虚拟键盘工具栏(方向键/Tab/Ctrl/功能键)
- ✅ 终端输出实时显示(ANSI 颜色剥离)
- ✅ 连接/断开/重连
3. 系统架构
┌─────────────────────────────────────────────────┐
│ MainActivity │
│ ┌─ setContent ──────────────────────────────┐ │
│ │ SshHost (Composable) │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ SshConnectDialog │ │ │
│ │ │ (主机/端口/用户名/密码输入框) │ │ │
│ │ └──────────────┬───────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ SshTerminalScreen │ │ │
│ │ │ ┌─ 终端输出区 ───────────────┐ │ │ │
│ │ │ │ LazyColumn │ │ │ │
│ │ │ │ ANSI 解析 + 等宽字体 │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ │ ┌─ 命令行输入 ───────────────┐ │ │ │
│ │ │ │ BasicTextField + Send │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ │ ┌─ 键盘工具栏(折叠) ──────┐ │ │ │
│ │ │ │ ↑↓←→ Tab Esc Del │ │ │ │
│ │ │ │ F1-F3 Home End PgUp/Dn │ │ │ │
│ │ │ │ Ctrl+C Ctrl+D Ctrl+Z │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ SshTerminalManager │
│ (JSch 会话管理) │
│ ┌─────────────────┐ │
│ │ Session │ │
│ │ ├─ ChannelShell│ │
│ │ │ ├─ InputStream │
│ │ │ ├─ OutputStream │
│ │ │ └─ Pty │
│ │ └─ │
│ └─────────────────┘ │
└─────────────────────┘
4. 技术栈
| 层 | 技术 | 版本 |
|---|---|---|
| 语言 | Kotlin | 2.3.21 |
| UI 框架 | Jetpack Compose | BOM 2026.04.01 |
| 最低 SDK | Android 12 (API 31) | — |
| 目标 SDK | Android 16 (API 36) | — |
| SSH 库 | JSch | 0.1.55 |
| 构建系统 | Gradle + AGP | 9.2.0 / 9.4.1 |
5. 目录结构
app/src/main/java/ai/openclaw/app/
├── MainActivity.kt # 入口 Activity(极简)
├── MainViewModel.kt # [保留旧代码,未使用]
├── NodeRuntime.kt # [保留旧代码,未使用]
├── SensitiveFeatureConfig.kt # [thirdParty flavor 移植]
├── node/
│ ├── CallLogHandler.kt # [thirdParty flavor 移植]
│ ├── SmsHandler.kt # [thirdParty flavor 移植]
│ └── SmsManager.kt # [thirdParty flavor 移植]
└── ui/
├── RootScreen.kt # 直接启动 SshHost
├── MobileUiTokens.kt # [保留旧代码,未使用]
├── OpenClawTheme.kt # [保留旧代码,未使用]
├── tergent/
│ ├── SshTerminalManager.kt # SSH 会话管理核心
│ ├── SshTerminalScreen.kt # 终端 UI(输出+输入+键盘)
│ ├── SshHost.kt # 宿主组件(对话框 + 终端切换)
│ ├── TergentTheme.kt # Tergent 主题配色
│ ├── TergentComponents.kt # 通用组件
│ ├── DevicesScreen.kt # 设备管理页 [预留]
│ ├── DiagnosticsScreen.kt # 诊断页 [预留]
│ └── NotificationsScreen.kt # 通知页 [预留]
└── ...(其他旧 UI 文件保留未修改)
6. 核心模块设计
6.1 SshTerminalManager
负责 SSH 连接全生命周期。
SshTerminalManager
├── connect(config: SshConfig): Boolean
├── sendCommand(cmd: String): Boolean
├── sendKey(key: String): Boolean ← 发送特殊按键
├── disconnect()
├── isConnected(): Boolean
└── connectionState: StateFlow<SshConnectionState>
├── connected: Boolean
├── host: String
├── lines: List<String> ← 终端输出行
└── error: String?
关键实现:
- 使用 JSch
ChannelShell进行交互式 shell 会话 - 设置
xterm-256colorPTY 实现完整终端兼容 readOutput()在 IO 协程中持续读取,自动剥离 ANSI 转义序列- 输出缓冲区限制 2000 行,防止内存溢出
- 特殊按键通过转义序列发送(ANSI 控制码 / Ctrl 字符)
6.2 SshTerminalScreen
Compose UI 构建的三段式布局:
- 头部 — 连接状态图标 + 主机名 + 断连按钮
- 终端输出区 —
LazyColumn+ 水平滚动- 行级
parseAnsi()函数将 ANSI 色码转为 ComposeColor - 等宽字体
FontFamily.Monospace - 自动滚动到底部
- 行级
- 键盘工具栏 — 可折叠的辅助键盘
- 方向键:↑↓←→(ANSI
[A/[B/[D/[C) - Tab(
\t)、Esc(\u001b) - Ctrl+C(
\u0003)、Ctrl+D(\u0004)、Ctrl+Z(\u001a) - Del(
\u007f)、Home([H)、End([F) - PgUp(
[5~)、PgDn([6~) - F1-F3(
OP/OQ/OR)
- 方向键:↑↓←→(ANSI
6.3 颜色与主题
深色终端风格(参考 GitHub Dark / 传统终端配色)
BG: #0D1117 ← 主背景
SURF: #161B22 ← 面板/头部
BDR: #30363D ← 边框
TEXT: #E6EDF3 ← 默认文字
DIM: #8B949E ← 次要文字
GREEN:#3FB950 ← 提示符、成功
BLUE: #58A6FF ← 高亮、链接
RED: #F85149 ← 错误、断开
ORNG: #D29922 ← Ctrl 键标签
7. 数据流
用户输入 ←──────────────────────────────────┐
│ │
▼ │
SshTerminalScreen │
├─ 命令行 BasicTextField │
│ ↓ sendCommand(text) │
├─ 键盘工具栏 KeyButton │
│ ↓ sendKey(escape_sequence) │
│ │
▼ │
SshTerminalManager │
├─ sendCommand() → OutputStream.write() │
├─ sendKey() → OutputStream.write() │
│ │
▼ │
ChannelShell (JSch) │
├─ OutputStream → SSH 服务端 │
└─ InputStream ← SSH 服务端 stdout/stderr │
↓ │
readOutput() (IO 协程循环) │
├─ byte[] → String │
├─ ANSI escape sequence 剥离 │
├─ 换行/退格/制表符 处理 │
├─ lineBuffer 追加 │
└─ connectionState.lines 更新 → UI 刷新 ──┘
8. 构建与安装
# 编译(需 JDK 17 + Android SDK 35+)
./gradlew assemblePlayDebug
# APK 产物
app/build/outputs/apk/play/debug/openclaw-*-play-debug.apk
# 安装
adb install -r app/build/outputs/apk/play/debug/openclaw-*-play-debug.apk
签名说明
- Debug 编译使用 Android Studio 默认 debug 签名
- Release 编译需配置
OPENCLAW_ANDROID_STORE_FILE等属性
9. 未来规划
| 优先级 | 功能 | 说明 |
|---|---|---|
| P0 | 连接保存 | 保存多个 SSH 连接配置,快速切换 |
| P0 | 中文输入优化 | 解决中文输入法下的字符发送问题 |
| P1 | 密钥认证 | 支持 SSH 私钥认证(而非密码) |
| P1 | 主题切换 | 亮色/暗色/自定义配色方案 |
| P1 | 复制粘贴 | 长按选中 + 复制终端内容 |
| P2 | SFTP 文件浏览 | 连接后浏览/上传/下载文件 |
| P2 | 终端回滚 | 向上滚动查看历史输出 |
| P2 | 终端分屏 | 同时连接多个主机 |
10. 设计决策记录 (ADR)
ADR-001: 为什么用 JSch 而不是其他 SSH 库
- JSch 0.1.55 是纯 Java、轻量、无依赖冲突
- Apache MINA SSHD 依赖更重,需要 Bouncy Castle
- Trilead SSH 已不再维护
- JSch 经过广泛验证,兼容性好
ADR-002: 为什么移除 OpenClaw 网关依赖
- 产品定位从「OpenClaw 手机配件」变为「独立 SSH 客户端」
- 网关配对流程复杂,用户体验差
- 去掉了 60% 的代码量(NodeRuntime、GatewaySession 等)
- 启动速度从 3s 降到 0.5s
ADR-003: 为什么不使用系统 WebView 做终端
- WebView 渲染终端需要 xterm.js,包体积大
- 原生 Compose 渲染性能更好
- 键盘事件处理更可控
- 没有跨域/安全策略问题
文档维护: 茂之钳 · 2026-05-23
ADR-004: 为什么密码不持久化存储
- EncryptedSharedPreferences 虽然加密,但 Android Keystore 在设备重启后可能丢失密钥
- 密码明文存储始终有风险
- 用户每次连接输入密码更符合安全直觉
- 私钥文件存储到
context.filesDir(App 私有目录,其他 App 无法访问)