Compare commits

...

5 Commits

Author SHA1 Message Date
茂之钳 04e5056aaa chore: local-build branch - simplified gradle for Android Studio
- Remove benchmark module
- Use AGP 8.7.3 + Kotlin 2.0.21 (stable)
- Remove ktlint plugin
- Simplify app/build.gradle.kts (remove flavor, sourceSet overrides)
- Update version catalog with compatible library versions
2026-05-25 13:54:02 +00:00
茂之钳 eb03204347 docs: add SSH module design document (Chinese) 2026-05-25 13:34:22 +00:00
茂之钳 9315476a40 v1.3.2: remove terminal display $ prompt
- Remove $ prefix from terminal input display line (item(key="prompt"))
- Change idle prompt from "terminal@tergent:~\$" to "terminal@tergent"
2026-05-25 09:34:41 +00:00
茂之钳 bc9d8602ad v1.3.1: add inputType and fix coroutine scope for keyboard send
- Add InputType.TYPE_CLASS_TEXT so IME shows Send key on all keyboards
- Replace kotlinx.coroutines.MainScope() with external coroutineScope
  to avoid lifecycle mismatch and potential scope cancellation
2026-05-25 08:41:21 +00:00
茂之钳 4c31834810 fix: re-add setOnEditorActionListener for soft keyboard send
v1.2.9 removed setOnEditorActionListener, relying only on TextWatcher
detecting \n character. Many soft keyboards (especially Chinese IMEs)
send IME_ACTION_SEND/GO/DONE instead of inserting \n, so commands were
never sent.

This re-adds EditorInfo.IME_ACTION_SEND/GO/DONE handling alongside the
existing TextWatcher newline detection.
2026-05-25 05:40:36 +00:00
13 changed files with 853 additions and 322 deletions
+4 -3
View File
@@ -1,6 +1,7 @@
.gradle/
**/build/
build/
*.apk
*.aab
local.properties
.idea/
**/*.iml
*.apk
*.iml
+23 -248
View File
@@ -1,115 +1,33 @@
import com.android.build.api.variant.impl.VariantOutputImpl
val dnsjavaInetAddressResolverService = "META-INF/services/java.net.spi.InetAddressResolverProvider"
val androidStoreFile = providers.gradleProperty("OPENCLAW_ANDROID_STORE_FILE").orNull?.takeIf { it.isNotBlank() }
val androidStorePassword = providers.gradleProperty("OPENCLAW_ANDROID_STORE_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val androidKeyAlias = providers.gradleProperty("OPENCLAW_ANDROID_KEY_ALIAS").orNull?.takeIf { it.isNotBlank() }
val androidKeyPassword = providers.gradleProperty("OPENCLAW_ANDROID_KEY_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val resolvedAndroidStoreFile =
androidStoreFile?.let { storeFilePath ->
if (storeFilePath.startsWith("~/")) {
"${System.getProperty("user.home")}/${storeFilePath.removePrefix("~/")}"
} else {
storeFilePath
}
}
val hasAndroidReleaseSigning =
listOf(resolvedAndroidStoreFile, androidStorePassword, androidKeyAlias, androidKeyPassword).all { it != null }
val wantsAndroidReleaseBuild =
gradle.startParameter.taskNames.any { taskName ->
taskName.contains("Release", ignoreCase = true) ||
Regex("""(^|:)(bundle|assemble)$""").containsMatchIn(taskName)
}
if (wantsAndroidReleaseBuild && !hasAndroidReleaseSigning) {
error(
"Missing Android release signing properties. Set OPENCLAW_ANDROID_STORE_FILE, " +
"OPENCLAW_ANDROID_STORE_PASSWORD, OPENCLAW_ANDROID_KEY_ALIAS, and " +
"OPENCLAW_ANDROID_KEY_PASSWORD in ~/.gradle/gradle.properties.",
)
}
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.ktlint)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "ai.openclaw.app"
compileSdk = 36
// Release signing is local-only; keep the keystore path and passwords out of the repo.
signingConfigs {
if (hasAndroidReleaseSigning) {
create("release") {
storeFile = project.file(checkNotNull(resolvedAndroidStoreFile))
storePassword = checkNotNull(androidStorePassword)
keyAlias = checkNotNull(androidKeyAlias)
keyPassword = checkNotNull(androidKeyPassword)
}
}
}
sourceSets {
getByName("main") {
assets.directories.add("../../shared/OpenClawKit/Sources/OpenClawKit/Resources")
}
}
compileSdk = 35
defaultConfig {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
targetSdk = 35
versionCode = 2026052100
versionName = "2026.5.21"
versionName = "1.3.2"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
flavorDimensions += "store"
productFlavors {
create("play") {
dimension = "store"
}
create("thirdParty") {
dimension = "store"
}
}
sourceSets {
getByName("play") {
java.srcDirs("src/thirdParty/java")
}
}
buildTypes {
release {
if (hasAndroidReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
isMinifyEnabled = false
}
}
buildFeatures {
compose = true
buildConfig = true
}
compileOptions {
@@ -117,171 +35,28 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources {
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/*.version",
"/META-INF/LICENSE*.txt",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties",
"org/bouncycastle/x509/CertPathReviewerMessages*.properties",
)
}
kotlin {
jvmToolchain(17)
}
lint {
lintConfig = file("lint.xml")
warningsAsErrors = true
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
androidComponents {
onVariants { variant ->
variant.outputs
.filterIsInstance<VariantOutputImpl>()
.forEach { output ->
val versionName = output.versionName.orNull ?: "0"
val buildType = variant.buildType
val flavorName = variant.flavorName?.takeIf { it.isNotBlank() }
val outputFileName =
if (flavorName == null) {
"openclaw-$versionName-$buildType.apk"
} else {
"openclaw-$versionName-$flavorName-$buildType.apk"
}
output.outputFileName = outputFileName
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
allWarningsAsErrors.set(true)
}
}
ktlint {
android.set(true)
ignoreFailures.set(false)
filter {
exclude("**/build/**")
buildFeatures {
compose = true
}
}
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.webkit)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
// material-icons-extended pulled in full icon set (~20 MB DEX). Only ~18 icons used.
// R8 will tree-shake unused icons when minify is enabled on release builds.
implementation(libs.androidx.compose.material.icons.extended)
debugImplementation(libs.androidx.compose.ui.tooling)
// Material Components (XML theme + resources)
implementation(libs.material)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.exifinterface)
implementation(libs.okhttp)
implementation(libs.bcprov)
implementation(libs.commonmark)
implementation(libs.commonmark.ext.autolink)
implementation(libs.commonmark.ext.gfm.strikethrough)
implementation(libs.commonmark.ext.gfm.tables)
implementation(libs.commonmark.ext.task.list.items)
// CameraX (for node.invoke camera.* parity)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.video)
implementation(libs.play.services.code.scanner)
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
implementation(libs.dnsjava)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.coroutines.core)
implementation(libs.coroutines.android)
implementation(libs.serialization.json)
implementation(libs.jsch)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.mockwebserver)
testImplementation(libs.robolectric)
testRuntimeOnly(libs.junit.vintage.engine)
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
androidComponents {
onVariants(selector().withBuildType("release")) { variant ->
val variantName = variant.name
val variantNameCapitalized = variantName.replaceFirstChar(Char::titlecase)
val stripTaskName = "strip${variantNameCapitalized}DnsjavaServiceDescriptor"
val mergeTaskName = "merge${variantNameCapitalized}JavaResource"
val minifyTaskName = "minify${variantNameCapitalized}WithR8"
val mergedJar =
layout.buildDirectory.file(
"intermediates/merged_java_res/$variantName/$mergeTaskName/base.jar",
)
val stripTask =
tasks.register(stripTaskName) {
inputs.file(mergedJar)
outputs.file(mergedJar)
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
}
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
}
}
tasks.matching { it.name == mergeTaskName }.configureEach {
finalizedBy(stripTask)
}
tasks.matching { it.name == minifyTaskName }.configureEach {
dependsOn(stripTask)
}
}
implementation(libs.material)
debugImplementation(libs.androidx.compose.ui.tooling)
}
@@ -522,7 +522,23 @@ fun SshTerminalScreen(
hint = if (state.connected) "type command..." else ""
setHintTextColor(android.graphics.Color.parseColor("#6E7681"))
imeOptions = EditorInfo.IME_ACTION_SEND
inputType = android.text.InputType.TYPE_CLASS_TEXT
setSingleLine()
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_ACTION_GO || actionId == EditorInfo.IME_ACTION_DONE) {
val cmd = text.toString().trim()
if (cmd.isNotEmpty()) {
coroutineScope.launch {
manager.sendCommand(cmd)
}
}
setText("")
currentInput = ""
true
} else {
false
}
}
addTextChangedListener(object : android.text.TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: android.text.Editable?) {}
@@ -687,7 +703,7 @@ fun SshTerminalScreen(
}
item(key = "prompt") {
Text(
text = "\$ $currentInputText",
text = currentInputText.ifEmpty { " " },
color = textPrimary,
fontSize = terminalFontSizeSp,
fontFamily = FontFamily.Monospace,
@@ -732,7 +748,7 @@ fun SshTerminalScreen(
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "terminal@tergent:~$",
text = "terminal@tergent",
color = textTertiary,
fontFamily = FontFamily.Monospace,
fontSize = 16.sp
+5
View File
@@ -0,0 +1,5 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
+435
View File
@@ -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 |
+1 -6
View File
@@ -1,9 +1,4 @@
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
org.gradle.warning.mode=all
android.useAndroidX=true
android.nonTransitiveRClass=true
android.enableR8.fullMode=true
android.uniquePackageNames=false
android.dependency.useConstraints=false
android.r8.strictFullModeForKeepRules=false
android.newDsl=true
+18 -61
View File
@@ -1,76 +1,33 @@
[versions]
agp = "9.2.0"
androidx-activity = "1.13.0"
androidx-benchmark = "1.4.1"
androidx-camera = "1.6.0"
androidx-compose-bom = "2026.04.01"
androidx-core = "1.18.0"
androidx-exifinterface = "1.4.2"
androidx-lifecycle = "2.10.0"
androidx-security = "1.1.0"
androidx-test-ext = "1.3.0"
androidx-uiautomator = "2.4.0-beta02"
androidx-webkit = "1.15.0"
bcprov = "1.84"
commonmark = "0.28.0"
coroutines = "1.10.2"
dnsjava = "3.6.4"
junit = "4.13.2"
junit-vintage = "6.0.3"
kotest = "6.1.11"
ktlint-gradle = "14.2.0"
kotlin = "2.3.21"
material = "1.13.0"
okhttp = "5.3.2"
play-services-code-scanner = "16.1.0"
robolectric = "4.16.1"
serialization-json = "1.11.0"
agp = "8.7.3"
kotlin = "2.0.21"
compose-bom = "2024.12.01"
core-ktx = "1.15.0"
lifecycle = "2.8.7"
activity-compose = "1.9.3"
coroutines = "1.9.0"
serialization-json = "1.7.3"
jsch = "0.1.55"
material = "1.12.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "androidx-camera" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidx-security" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext" }
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-uiautomator" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" }
commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" }
commonmark-ext-gfm-tables = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" }
commonmark-ext-task-list-items = { module = "org.commonmark:commonmark-ext-task-list-items", version.ref = "commonmark" }
dnsjava = { module = "dnsjava:dnsjava", version.ref = "dnsjava" }
junit = { module = "junit:junit", version.ref = "junit" }
junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit-vintage" }
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" }
material = { module = "com.google.android.material:material", version.ref = "material" }
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "play-services-code-scanner" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "1.79" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
jsch = { module = "com.jcraft:jsch", version.ref = "jsch" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" }
material = { module = "com.google.android.material:material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-test = { id = "com.android.test", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" }
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+93
View File
@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+1 -2
View File
@@ -14,6 +14,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "OpenClawNodeAndroid"
rootProject.name = "Tergent"
include(":app")
include(":benchmark")