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 843a5be..6344a8a 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 @@ -93,6 +93,7 @@ fun SettingsScreen( val proxyRunning by viewModel.proxyRunning.collectAsStateWithLifecycle() val proxyApiKey by viewModel.proxyApiKey.collectAsStateWithLifecycle() val proxyError by viewModel.proxyError.collectAsStateWithLifecycle() + val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle() var showLanguageDialog by remember { mutableStateOf(false) } Scaffold( @@ -168,7 +169,10 @@ fun SettingsScreen( running = proxyRunning, apiKey = proxyApiKey, error = proxyError, - onToggle = viewModel::setProxyEnabled + debug = proxyDebug, + onToggle = viewModel::setProxyEnabled, + onTestTrae = viewModel::testTraeStream, + onTestWorkBuddy = viewModel::testWorkBuddyStream ) } item { @@ -198,7 +202,10 @@ private fun ProxyGatewayCard( running: Boolean, apiKey: String, error: String?, - onToggle: (Boolean) -> Unit + debug: String?, + onToggle: (Boolean) -> Unit, + onTestTrae: () -> Unit, + onTestWorkBuddy: () -> Unit ) { Card( modifier = Modifier.fillMaxWidth(), @@ -241,6 +248,22 @@ private fun ProxyGatewayCard( modifier = Modifier.padding(top = 2.dp) ) } + Row(modifier = Modifier.padding(top = 4.dp)) { + TextButton(onClick = onTestTrae) { + Text("测试 Trae 流") + } + TextButton(onClick = onTestWorkBuddy) { + Text("测试 WorkBuddy 流") + } + } + if (debug != null) { + Text( + text = debug, + style = MaterialTheme.typography.bodySmall, + color = InkMuted, + modifier = Modifier.padding(top = 4.dp) + ) + } } Switch( checked = running, 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 dbac1d3..335a600 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 @@ -3,7 +3,11 @@ package com.rainy.token.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.rainy.token.data.proxy.LocalProxyServer +import com.rainy.token.data.proxy.ProviderRegion +import com.rainy.token.data.proxy.ProxyUpstreamStream import com.rainy.token.data.proxy.ProxyServerConfig +import com.rainy.token.data.proxy.TraeChatProxy +import com.rainy.token.data.proxy.WorkBuddyChatProxy import com.rainy.token.data.repository.CredentialRepository import com.rainy.token.domain.model.CredentialStatus import com.rainy.token.domain.service.ServiceType @@ -12,7 +16,9 @@ import java.util.UUID import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,7 +29,9 @@ import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( private val credentialRepository: CredentialRepository, - private val localProxyServer: LocalProxyServer + private val localProxyServer: LocalProxyServer, + private val traeChatProxy: TraeChatProxy, + private val workBuddyChatProxy: WorkBuddyChatProxy ) : ViewModel() { private val _uiState = MutableStateFlow(SettingsUiState()) @@ -51,6 +59,72 @@ class SettingsViewModel @Inject constructor( } } + /** 代理调试输出(原始 SSE 帧)。 */ + private val _proxyDebug = MutableStateFlow(null) + val proxyDebug: StateFlow = _proxyDebug.asStateFlow() + + /** 用 Trae CN 默认账号发一条 stream=true 测试,抓上游原始 SSE。 */ + fun testTraeStream() { + viewModelScope.launch { + _proxyDebug.value = "正在请求 Trae CN 流..." + val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}""" + try { + val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN) + if (stream == null) { + _proxyDebug.value = "Trae CN 未配置或未登录" + return@launch + } + _proxyDebug.value = readStreamSample(stream, "Trae CN") + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (e: Exception) { + _proxyDebug.value = "Trae CN 异常:" + (e.message ?: "未知错误") + } + } + } + + /** 用 WorkBuddy CN 默认账号发一条 stream=true 测试,抓上游原始 SSE。 */ + fun testWorkBuddyStream() { + viewModelScope.launch { + _proxyDebug.value = "正在请求 WorkBuddy CN 流..." + val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}""" + try { + val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN) + if (stream == null) { + _proxyDebug.value = "WorkBuddy CN 未配置或未登录" + return@launch + } + _proxyDebug.value = readStreamSample(stream, "WorkBuddy CN") + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (e: Exception) { + _proxyDebug.value = "WorkBuddy CN 异常:" + (e.message ?: "未知错误") + } + } + } + + private suspend fun readStreamSample( + stream: ProxyUpstreamStream, + label: String, + ): String = withContext(Dispatchers.IO) { + val sb = StringBuilder() + sb.append(label).append(" HTTP ").append(stream.status) + sb.append(" Content-Type=").append(stream.contentType).append("\n") + try { + val reader = stream.input.bufferedReader() + repeat(80) { + val line = reader.readLine() ?: return@repeat + sb.append(line).append("\n") + if (line.trim() == "data: [DONE]") return@repeat + if (sb.length > 6000) return@repeat + } + } finally { + stream.close() + } + sb.toString() + } + + private fun generateProxyKey(): String = "rainy-" + UUID.randomUUID().toString().replace("-", "").take(16)