feat(proxy): 显示并一键复制局域网地址,自动检测热点 IP
This commit is contained in:
parent
218278f14e
commit
653ae1462a
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 48
|
versionCode = 49
|
||||||
versionName = "1.7.28"
|
versionName = "1.7.29"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取当前手机可用于局域网/热点的 IPv4 地址。
|
||||||
|
*
|
||||||
|
* 热点接口名各机型不同(ap0/rndis0/wlan0/swlan0…),这里优先常见热点接口,
|
||||||
|
* 其次回退到任意非回环 IPv4。热点开启时手机热点 IP 一般稳定(多数为
|
||||||
|
* 192.168.x.1),但跨机型/跨配置可能不同,所以运行时取一次最稳妥。
|
||||||
|
*/
|
||||||
|
fun currentLanIpv4(): String? {
|
||||||
|
return try {
|
||||||
|
val candidates = mutableListOf<String>()
|
||||||
|
val fallback = mutableListOf<String>()
|
||||||
|
NetworkInterface.getNetworkInterfaces()?.let { nis ->
|
||||||
|
for (ni in nis) {
|
||||||
|
if (!ni.isUp || ni.isLoopback) continue
|
||||||
|
val ip = ni.inetAddresses
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { it as? Inet4Address }
|
||||||
|
.mapNotNull { it.hostAddress }
|
||||||
|
.firstOrNull { addr ->
|
||||||
|
!addr.startsWith("127.") && !addr.startsWith("169.254.") &&
|
||||||
|
addr.count { it == '.' } == 3
|
||||||
|
} ?: continue
|
||||||
|
val name = ni.name.lowercase()
|
||||||
|
when {
|
||||||
|
name == "ap0" || name == "rndis0" -> candidates.add(0, ip)
|
||||||
|
name == "wlan0" || name.startsWith("wlan") || name.startsWith("swlan") ->
|
||||||
|
candidates.add(ip)
|
||||||
|
else -> fallback.add(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.firstOrNull() ?: fallback.firstOrNull()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -105,6 +105,7 @@ fun SettingsScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val proxyRunning by viewModel.proxyRunning.collectAsStateWithLifecycle()
|
val proxyRunning by viewModel.proxyRunning.collectAsStateWithLifecycle()
|
||||||
val proxyAllowLan by viewModel.proxyAllowLan.collectAsStateWithLifecycle()
|
val proxyAllowLan by viewModel.proxyAllowLan.collectAsStateWithLifecycle()
|
||||||
|
val proxyLanIp by viewModel.proxyLanIp.collectAsStateWithLifecycle()
|
||||||
val proxyApiKey by viewModel.proxyApiKey.collectAsStateWithLifecycle()
|
val proxyApiKey by viewModel.proxyApiKey.collectAsStateWithLifecycle()
|
||||||
val proxyError by viewModel.proxyError.collectAsStateWithLifecycle()
|
val proxyError by viewModel.proxyError.collectAsStateWithLifecycle()
|
||||||
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
||||||
@ -186,6 +187,7 @@ fun SettingsScreen(
|
|||||||
ProxyGatewayCard(
|
ProxyGatewayCard(
|
||||||
running = proxyRunning,
|
running = proxyRunning,
|
||||||
allowLan = proxyAllowLan,
|
allowLan = proxyAllowLan,
|
||||||
|
lanIp = proxyLanIp,
|
||||||
apiKey = proxyApiKey,
|
apiKey = proxyApiKey,
|
||||||
error = proxyError,
|
error = proxyError,
|
||||||
debug = proxyDebug,
|
debug = proxyDebug,
|
||||||
@ -229,6 +231,7 @@ fun SettingsScreen(
|
|||||||
private fun ProxyGatewayCard(
|
private fun ProxyGatewayCard(
|
||||||
running: Boolean,
|
running: Boolean,
|
||||||
allowLan: Boolean,
|
allowLan: Boolean,
|
||||||
|
lanIp: String?,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
error: String?,
|
error: String?,
|
||||||
debug: String?,
|
debug: String?,
|
||||||
@ -272,7 +275,8 @@ private fun ProxyGatewayCard(
|
|||||||
Text(
|
Text(
|
||||||
text = when {
|
text = when {
|
||||||
!running -> "已停止"
|
!running -> "已停止"
|
||||||
allowLan -> "运行中 · 局域网模式(其他设备填 http://手机热点IP:8787/v1)"
|
allowLan && lanIp != null -> "运行中 · http://$lanIp:8787/v1"
|
||||||
|
allowLan -> "运行中 · 局域网模式(未取到 IP,请看手机热点设置)"
|
||||||
else -> "运行中 · http://127.0.0.1:8787/v1"
|
else -> "运行中 · http://127.0.0.1:8787/v1"
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@ -300,6 +304,18 @@ private fun ProxyGatewayCard(
|
|||||||
onCheckedChange = onAllowLanChange
|
onCheckedChange = onAllowLanChange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (allowLan && lanIp != null) {
|
||||||
|
val address = "http://$lanIp:8787/v1"
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText("proxy", address))
|
||||||
|
Toast.makeText(context, "已复制 $address", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("复制地址:$address")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error,
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import com.rainy.token.data.proxy.TraeChatProxy
|
|||||||
import com.rainy.token.data.proxy.TraeModelProvider
|
import com.rainy.token.data.proxy.TraeModelProvider
|
||||||
import com.rainy.token.data.proxy.WorkBuddyModelProvider
|
import com.rainy.token.data.proxy.WorkBuddyModelProvider
|
||||||
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
||||||
|
import com.rainy.token.data.proxy.currentLanIpv4
|
||||||
import com.rainy.token.data.repository.CredentialRepository
|
import com.rainy.token.data.repository.CredentialRepository
|
||||||
import com.rainy.token.domain.model.CredentialStatus
|
import com.rainy.token.domain.model.CredentialStatus
|
||||||
import com.rainy.token.domain.service.ServiceType
|
import com.rainy.token.domain.service.ServiceType
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import java.net.NetworkInterface
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -61,6 +63,10 @@ class SettingsViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
val proxyAllowLan: StateFlow<Boolean> = _proxyAllowLan.asStateFlow()
|
val proxyAllowLan: StateFlow<Boolean> = _proxyAllowLan.asStateFlow()
|
||||||
|
|
||||||
|
/** 当前局域网/热点 IPv4(仅供显示,不要依赖固定值)。 */
|
||||||
|
private val _proxyLanIp = MutableStateFlow(currentLanIpv4())
|
||||||
|
val proxyLanIp: StateFlow<String?> = _proxyLanIp.asStateFlow()
|
||||||
|
|
||||||
/** 当前本机 API Key(每次进程随机生成,仅提示用)。 */
|
/** 当前本机 API Key(每次进程随机生成,仅提示用)。 */
|
||||||
val proxyApiKey: StateFlow<String> = MutableStateFlow(generateProxyKey())
|
val proxyApiKey: StateFlow<String> = MutableStateFlow(generateProxyKey())
|
||||||
|
|
||||||
@ -71,6 +77,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
/** 启动/停止本地反代服务。 */
|
/** 启动/停止本地反代服务。 */
|
||||||
fun setProxyEnabled(enabled: Boolean) {
|
fun setProxyEnabled(enabled: Boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
_proxyLanIp.value = currentLanIpv4()
|
||||||
val config = ProxyServerConfig(
|
val config = ProxyServerConfig(
|
||||||
apiKey = proxyApiKey.value,
|
apiKey = proxyApiKey.value,
|
||||||
allowLan = _proxyAllowLan.value
|
allowLan = _proxyAllowLan.value
|
||||||
@ -89,6 +96,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
context.getSharedPreferences("proxy_settings", Context.MODE_PRIVATE)
|
context.getSharedPreferences("proxy_settings", Context.MODE_PRIVATE)
|
||||||
.edit().putBoolean("allow_lan", enabled).apply()
|
.edit().putBoolean("allow_lan", enabled).apply()
|
||||||
_proxyAllowLan.value = enabled
|
_proxyAllowLan.value = enabled
|
||||||
|
_proxyLanIp.value = currentLanIpv4()
|
||||||
if (proxyRunning.value) {
|
if (proxyRunning.value) {
|
||||||
localProxyServer.stop()
|
localProxyServer.stop()
|
||||||
setProxyEnabled(true)
|
setProxyEnabled(true)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user