From a53899c6cf3c1f21c3b03b90afd5a8402cf490ea Mon Sep 17 00:00:00 2001 From: Liuxinyu176 <1041316040@qq.com> Date: Sat, 10 Oct 2026 14:23:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20=E6=98=BE=E7=A4=BA=E5=B9=B6?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E5=A4=8D=E5=88=B6=E5=B1=80=E5=9F=9F=E7=BD=91?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=EF=BC=8C=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E7=83=AD=E7=82=B9=20IP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../token/data/proxy/LocalNetworkInfo.kt | 41 +++++++++++++++++++ .../rainy/token/ui/settings/SettingsScreen.kt | 18 +++++++- .../token/ui/settings/SettingsViewModel.kt | 8 ++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/rainy/token/data/proxy/LocalNetworkInfo.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9538033..cc091e5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.rainy.token" minSdk = 31 targetSdk = 35 - versionCode = 48 - versionName = "1.7.28" + versionCode = 49 + versionName = "1.7.29" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/rainy/token/data/proxy/LocalNetworkInfo.kt b/app/src/main/java/com/rainy/token/data/proxy/LocalNetworkInfo.kt new file mode 100644 index 0000000..609124e --- /dev/null +++ b/app/src/main/java/com/rainy/token/data/proxy/LocalNetworkInfo.kt @@ -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() + val fallback = mutableListOf() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt index f19fc5e..c79a961 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt @@ -105,6 +105,7 @@ fun SettingsScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val proxyRunning by viewModel.proxyRunning.collectAsStateWithLifecycle() val proxyAllowLan by viewModel.proxyAllowLan.collectAsStateWithLifecycle() + val proxyLanIp by viewModel.proxyLanIp.collectAsStateWithLifecycle() val proxyApiKey by viewModel.proxyApiKey.collectAsStateWithLifecycle() val proxyError by viewModel.proxyError.collectAsStateWithLifecycle() val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle() @@ -186,6 +187,7 @@ fun SettingsScreen( ProxyGatewayCard( running = proxyRunning, allowLan = proxyAllowLan, + lanIp = proxyLanIp, apiKey = proxyApiKey, error = proxyError, debug = proxyDebug, @@ -229,6 +231,7 @@ fun SettingsScreen( private fun ProxyGatewayCard( running: Boolean, allowLan: Boolean, + lanIp: String?, apiKey: String, error: String?, debug: String?, @@ -272,7 +275,8 @@ private fun ProxyGatewayCard( Text( text = when { !running -> "已停止" - allowLan -> "运行中 · 局域网模式(其他设备填 http://手机热点IP:8787/v1)" + allowLan && lanIp != null -> "运行中 · http://$lanIp:8787/v1" + allowLan -> "运行中 · 局域网模式(未取到 IP,请看手机热点设置)" else -> "运行中 · http://127.0.0.1:8787/v1" }, style = MaterialTheme.typography.bodySmall, @@ -300,6 +304,18 @@ private fun ProxyGatewayCard( 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) { Text( text = error, diff --git a/app/src/main/java/com/rainy/token/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/rainy/token/ui/settings/SettingsViewModel.kt index 2e692d9..e981acb 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/SettingsViewModel.kt @@ -11,11 +11,13 @@ import com.rainy.token.data.proxy.TraeChatProxy import com.rainy.token.data.proxy.TraeModelProvider import com.rainy.token.data.proxy.WorkBuddyModelProvider 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.domain.model.CredentialStatus import com.rainy.token.domain.service.ServiceType import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import java.net.NetworkInterface import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -61,6 +63,10 @@ class SettingsViewModel @Inject constructor( ) val proxyAllowLan: StateFlow = _proxyAllowLan.asStateFlow() + /** 当前局域网/热点 IPv4(仅供显示,不要依赖固定值)。 */ + private val _proxyLanIp = MutableStateFlow(currentLanIpv4()) + val proxyLanIp: StateFlow = _proxyLanIp.asStateFlow() + /** 当前本机 API Key(每次进程随机生成,仅提示用)。 */ val proxyApiKey: StateFlow = MutableStateFlow(generateProxyKey()) @@ -71,6 +77,7 @@ class SettingsViewModel @Inject constructor( /** 启动/停止本地反代服务。 */ fun setProxyEnabled(enabled: Boolean) { if (enabled) { + _proxyLanIp.value = currentLanIpv4() val config = ProxyServerConfig( apiKey = proxyApiKey.value, allowLan = _proxyAllowLan.value @@ -89,6 +96,7 @@ class SettingsViewModel @Inject constructor( context.getSharedPreferences("proxy_settings", Context.MODE_PRIVATE) .edit().putBoolean("allow_lan", enabled).apply() _proxyAllowLan.value = enabled + _proxyLanIp.value = currentLanIpv4() if (proxyRunning.value) { localProxyServer.stop() setProxyEnabled(true)