Files
2026-05-24 01:33:14 +00:00

12 KiB
Raw Permalink Blame History

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 源码改造,改名 TergentAPK 可编译
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-256color PTY 实现完整终端兼容
  • readOutput() 在 IO 协程中持续读取,自动剥离 ANSI 转义序列
  • 输出缓冲区限制 2000 行,防止内存溢出
  • 特殊按键通过转义序列发送(ANSI 控制码 / Ctrl 字符)

6.2 SshTerminalScreen

Compose UI 构建的三段式布局:

  1. 头部 — 连接状态图标 + 主机名 + 断连按钮
  2. 终端输出区LazyColumn + 水平滚动
    • 行级 parseAnsi() 函数将 ANSI 色码转为 Compose Color
    • 等宽字体 FontFamily.Monospace
    • 自动滚动到底部
  3. 键盘工具栏 — 可折叠的辅助键盘
    • 方向键:↑↓←→(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-F3OP/OQ/OR

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 无法访问)