feat(proxy): 设置页新增 Trae/WorkBuddy 流式探针

- 用默认账号直接请求 llm_utils_chat/v2 chat,抓取上游原始 SSE 帧
- 调试输出显示 HTTP 状态、Content-Type 与最多 80 行原始帧
- 为 StreamNormalizer 提供真实协议样本
This commit is contained in:
Liuxinyu176 2026-10-09 08:08:47 +08:00
parent 67e1f060c5
commit a4d854271f
2 changed files with 100 additions and 3 deletions

View File

@ -93,6 +93,7 @@ fun SettingsScreen(
val proxyRunning by viewModel.proxyRunning.collectAsStateWithLifecycle() val proxyRunning by viewModel.proxyRunning.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()
var showLanguageDialog by remember { mutableStateOf(false) } var showLanguageDialog by remember { mutableStateOf(false) }
Scaffold( Scaffold(
@ -168,7 +169,10 @@ fun SettingsScreen(
running = proxyRunning, running = proxyRunning,
apiKey = proxyApiKey, apiKey = proxyApiKey,
error = proxyError, error = proxyError,
onToggle = viewModel::setProxyEnabled debug = proxyDebug,
onToggle = viewModel::setProxyEnabled,
onTestTrae = viewModel::testTraeStream,
onTestWorkBuddy = viewModel::testWorkBuddyStream
) )
} }
item { item {
@ -198,7 +202,10 @@ private fun ProxyGatewayCard(
running: Boolean, running: Boolean,
apiKey: String, apiKey: String,
error: String?, error: String?,
onToggle: (Boolean) -> Unit debug: String?,
onToggle: (Boolean) -> Unit,
onTestTrae: () -> Unit,
onTestWorkBuddy: () -> Unit
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -241,6 +248,22 @@ private fun ProxyGatewayCard(
modifier = Modifier.padding(top = 2.dp) 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( Switch(
checked = running, checked = running,

View File

@ -3,7 +3,11 @@ package com.rainy.token.ui.settings
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
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.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.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
@ -12,7 +16,9 @@ import java.util.UUID
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -23,7 +29,9 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val credentialRepository: CredentialRepository, private val credentialRepository: CredentialRepository,
private val localProxyServer: LocalProxyServer private val localProxyServer: LocalProxyServer,
private val traeChatProxy: TraeChatProxy,
private val workBuddyChatProxy: WorkBuddyChatProxy
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState()) private val _uiState = MutableStateFlow(SettingsUiState())
@ -51,6 +59,72 @@ class SettingsViewModel @Inject constructor(
} }
} }
/** 代理调试输出(原始 SSE 帧)。 */
private val _proxyDebug = MutableStateFlow<String?>(null)
val proxyDebug: StateFlow<String?> = _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 = private fun generateProxyKey(): String =
"rainy-" + UUID.randomUUID().toString().replace("-", "").take(16) "rainy-" + UUID.randomUUID().toString().replace("-", "").take(16)