feat(proxy): 显示并一键复制局域网地址,自动检测热点 IP
Some checks are pending
Release / build (push) Waiting to run

This commit is contained in:
Liuxinyu176 2026-10-10 14:23:14 +08:00
parent 5b39e42458
commit a53899c6cf
4 changed files with 68 additions and 3 deletions

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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,

View File

@ -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)