feat(proxy): 允许局域网/热点设备连接选项(绑定 0.0.0.0)
This commit is contained in:
parent
f35bc6d3fc
commit
a858c4bce0
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 47
|
versionCode = 48
|
||||||
versionName = "1.7.27"
|
versionName = "1.7.28"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
* 按模型前缀路由:wbcn- → WorkBuddy 国内版;traecn- → Trae CN;其余 → Sub2API 透传
|
* 按模型前缀路由:wbcn- → WorkBuddy 国内版;traecn- → Trae CN;其余 → Sub2API 透传
|
||||||
*
|
*
|
||||||
* 安全:
|
* 安全:
|
||||||
* - 只绑定 127.0.0.1
|
* - 默认只绑定 127.0.0.1;config.allowLan=true 时绑定 0.0.0.0(热点/局域网共享)
|
||||||
* - config.apiKey 非空时,所有 v1 业务路由要求 Bearer Key 一致,否则 401
|
* - config.apiKey 非空时,所有 v1 业务路由要求 Bearer Key 一致,否则 401
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -62,7 +62,8 @@ class KtorLocalProxyServer @Inject constructor(
|
|||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
if (_isRunning.value) return Result.success(Unit)
|
if (_isRunning.value) return Result.success(Unit)
|
||||||
return try {
|
return try {
|
||||||
val engine = embeddedServer(CIO, host = "127.0.0.1", port = config.port) {
|
val bindHost = if (config.allowLan) "0.0.0.0" else "127.0.0.1"
|
||||||
|
val engine = embeddedServer(CIO, host = bindHost, port = config.port) {
|
||||||
proxyModule(config.apiKey, sub2ApiChatProxy, workBuddyChatProxy, traeChatProxy, providerRouter, accountPool)
|
proxyModule(config.apiKey, sub2ApiChatProxy, workBuddyChatProxy, traeChatProxy, providerRouter, accountPool)
|
||||||
}
|
}
|
||||||
engine.start(wait = false)
|
engine.start(wait = false)
|
||||||
|
|||||||
@ -10,6 +10,8 @@ data class ProxyServerConfig(
|
|||||||
val port: Int = 8787,
|
val port: Int = 8787,
|
||||||
/** 客户端必须携带的 Bearer Key;null = 关闭鉴权(仅本机建议)。 */
|
/** 客户端必须携带的 Bearer Key;null = 关闭鉴权(仅本机建议)。 */
|
||||||
val apiKey: String? = null,
|
val apiKey: String? = null,
|
||||||
|
/** true = 绑定 0.0.0.0,允许热点/同一 WiFi 下其他设备连接(需配合 API Key 使用)。 */
|
||||||
|
val allowLan: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -104,6 +104,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 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()
|
||||||
@ -184,6 +185,7 @@ fun SettingsScreen(
|
|||||||
item {
|
item {
|
||||||
ProxyGatewayCard(
|
ProxyGatewayCard(
|
||||||
running = proxyRunning,
|
running = proxyRunning,
|
||||||
|
allowLan = proxyAllowLan,
|
||||||
apiKey = proxyApiKey,
|
apiKey = proxyApiKey,
|
||||||
error = proxyError,
|
error = proxyError,
|
||||||
debug = proxyDebug,
|
debug = proxyDebug,
|
||||||
@ -196,6 +198,7 @@ fun SettingsScreen(
|
|||||||
onSelectModel = viewModel::selectProxyModel,
|
onSelectModel = viewModel::selectProxyModel,
|
||||||
onRefreshModels = viewModel::refreshProxyModels,
|
onRefreshModels = viewModel::refreshProxyModels,
|
||||||
onToggle = viewModel::setProxyEnabled,
|
onToggle = viewModel::setProxyEnabled,
|
||||||
|
onAllowLanChange = viewModel::setProxyAllowLan,
|
||||||
onTestTrae = viewModel::testTraeStream,
|
onTestTrae = viewModel::testTraeStream,
|
||||||
onTestWorkBuddy = viewModel::testWorkBuddyStream
|
onTestWorkBuddy = viewModel::testWorkBuddyStream
|
||||||
)
|
)
|
||||||
@ -225,6 +228,7 @@ fun SettingsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ProxyGatewayCard(
|
private fun ProxyGatewayCard(
|
||||||
running: Boolean,
|
running: Boolean,
|
||||||
|
allowLan: Boolean,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
error: String?,
|
error: String?,
|
||||||
debug: String?,
|
debug: String?,
|
||||||
@ -237,6 +241,7 @@ private fun ProxyGatewayCard(
|
|||||||
onSelectModel: (String) -> Unit,
|
onSelectModel: (String) -> Unit,
|
||||||
onRefreshModels: () -> Unit,
|
onRefreshModels: () -> Unit,
|
||||||
onToggle: (Boolean) -> Unit,
|
onToggle: (Boolean) -> Unit,
|
||||||
|
onAllowLanChange: (Boolean) -> Unit,
|
||||||
onTestTrae: () -> Unit,
|
onTestTrae: () -> Unit,
|
||||||
onTestWorkBuddy: () -> Unit
|
onTestWorkBuddy: () -> Unit
|
||||||
) {
|
) {
|
||||||
@ -265,7 +270,11 @@ private fun ProxyGatewayCard(
|
|||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (running) "运行中 · http://127.0.0.1:8787/v1" else "已停止",
|
text = when {
|
||||||
|
!running -> "已停止"
|
||||||
|
allowLan -> "运行中 · 局域网模式(其他设备填 http://手机热点IP:8787/v1)"
|
||||||
|
else -> "运行中 · http://127.0.0.1:8787/v1"
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = InkMuted,
|
color = InkMuted,
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
@ -276,6 +285,21 @@ private fun ProxyGatewayCard(
|
|||||||
color = InkMuted,
|
color = InkMuted,
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "允许局域网/热点设备连接",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = InkMuted,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = allowLan,
|
||||||
|
onCheckedChange = onAllowLanChange
|
||||||
|
)
|
||||||
|
}
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.rainy.token.ui.settings
|
package com.rainy.token.ui.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rainy.token.data.proxy.LocalProxyServer
|
import com.rainy.token.data.proxy.LocalProxyServer
|
||||||
@ -14,6 +15,7 @@ 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 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
|
||||||
@ -37,6 +39,7 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(
|
class SettingsViewModel @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
private val credentialRepository: CredentialRepository,
|
private val credentialRepository: CredentialRepository,
|
||||||
private val localProxyServer: LocalProxyServer,
|
private val localProxyServer: LocalProxyServer,
|
||||||
private val traeChatProxy: TraeChatProxy,
|
private val traeChatProxy: TraeChatProxy,
|
||||||
@ -51,6 +54,13 @@ class SettingsViewModel @Inject constructor(
|
|||||||
/** 本地反代服务运行状态。 */
|
/** 本地反代服务运行状态。 */
|
||||||
val proxyRunning: StateFlow<Boolean> = localProxyServer.isRunning
|
val proxyRunning: StateFlow<Boolean> = localProxyServer.isRunning
|
||||||
|
|
||||||
|
/** 是否允许局域网/热点设备连接(绑定 0.0.0.0)。 */
|
||||||
|
private val _proxyAllowLan = MutableStateFlow(
|
||||||
|
context.getSharedPreferences("proxy_settings", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("allow_lan", false)
|
||||||
|
)
|
||||||
|
val proxyAllowLan: StateFlow<Boolean> = _proxyAllowLan.asStateFlow()
|
||||||
|
|
||||||
/** 当前本机 API Key(每次进程随机生成,仅提示用)。 */
|
/** 当前本机 API Key(每次进程随机生成,仅提示用)。 */
|
||||||
val proxyApiKey: StateFlow<String> = MutableStateFlow(generateProxyKey())
|
val proxyApiKey: StateFlow<String> = MutableStateFlow(generateProxyKey())
|
||||||
|
|
||||||
@ -61,7 +71,11 @@ class SettingsViewModel @Inject constructor(
|
|||||||
/** 启动/停止本地反代服务。 */
|
/** 启动/停止本地反代服务。 */
|
||||||
fun setProxyEnabled(enabled: Boolean) {
|
fun setProxyEnabled(enabled: Boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
localProxyServer.start(ProxyServerConfig(apiKey = proxyApiKey.value))
|
val config = ProxyServerConfig(
|
||||||
|
apiKey = proxyApiKey.value,
|
||||||
|
allowLan = _proxyAllowLan.value
|
||||||
|
)
|
||||||
|
localProxyServer.start(config)
|
||||||
.onSuccess { _proxyError.value = null }
|
.onSuccess { _proxyError.value = null }
|
||||||
.onFailure { e -> _proxyError.value = e.message ?: "代理网关启动失败" }
|
.onFailure { e -> _proxyError.value = e.message ?: "代理网关启动失败" }
|
||||||
} else {
|
} else {
|
||||||
@ -70,6 +84,17 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 切换「允许局域网/热点设备连接」;运行中会按新配置重启代理。 */
|
||||||
|
fun setProxyAllowLan(enabled: Boolean) {
|
||||||
|
context.getSharedPreferences("proxy_settings", Context.MODE_PRIVATE)
|
||||||
|
.edit().putBoolean("allow_lan", enabled).apply()
|
||||||
|
_proxyAllowLan.value = enabled
|
||||||
|
if (proxyRunning.value) {
|
||||||
|
localProxyServer.stop()
|
||||||
|
setProxyEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 代理调试输出(结构化摘要)。 */
|
/** 代理调试输出(结构化摘要)。 */
|
||||||
private val _proxyDebug = MutableStateFlow<String?>(null)
|
private val _proxyDebug = MutableStateFlow<String?>(null)
|
||||||
val proxyDebug: StateFlow<String?> = _proxyDebug.asStateFlow()
|
val proxyDebug: StateFlow<String?> = _proxyDebug.asStateFlow()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user