feat(proxy): 探针结果结构化摘要,不再刷屏原始参数
Some checks are pending
Release / build (push) Waiting to run

- 显示:请求行/模型/messages数/stream、HTTP、事件帧数、模型、文本摘要、用量、错误
- 原始请求+响应仍完整保留,点“复制原始”一键复制
- 版本升至 1.7.4
This commit is contained in:
Liuxinyu176 2026-10-09 10:46:30 +08:00
parent 7de7111b18
commit fbbca4a03b
3 changed files with 166 additions and 20 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 = 23 versionCode = 24
versionName = "1.7.3" versionName = "1.7.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -104,6 +104,7 @@ fun SettingsScreen(
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()
val proxyDebugRaw by viewModel.proxyDebugRaw.collectAsStateWithLifecycle()
var showLanguageDialog by remember { mutableStateOf(false) } var showLanguageDialog by remember { mutableStateOf(false) }
Scaffold( Scaffold(
@ -180,6 +181,7 @@ fun SettingsScreen(
apiKey = proxyApiKey, apiKey = proxyApiKey,
error = proxyError, error = proxyError,
debug = proxyDebug, debug = proxyDebug,
debugRaw = proxyDebugRaw,
onToggle = viewModel::setProxyEnabled, onToggle = viewModel::setProxyEnabled,
onTestTrae = viewModel::testTraeStream, onTestTrae = viewModel::testTraeStream,
onTestWorkBuddy = viewModel::testWorkBuddyStream onTestWorkBuddy = viewModel::testWorkBuddyStream
@ -213,6 +215,7 @@ private fun ProxyGatewayCard(
apiKey: String, apiKey: String,
error: String?, error: String?,
debug: String?, debug: String?,
debugRaw: String?,
onToggle: (Boolean) -> Unit, onToggle: (Boolean) -> Unit,
onTestTrae: () -> Unit, onTestTrae: () -> Unit,
onTestWorkBuddy: () -> Unit onTestWorkBuddy: () -> Unit
@ -271,17 +274,17 @@ private fun ProxyGatewayCard(
Spacer(modifier = Modifier.padding(top = 8.dp)) Spacer(modifier = Modifier.padding(top = 8.dp))
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "原始输出", text = "探针摘要",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = InkMuted, color = InkMuted,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
TextButton(onClick = { TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("代理网关探针输出", debug)) clipboard.setPrimaryClip(ClipData.newPlainText("代理网关探针输出", debugRaw ?: debug))
Toast.makeText(context, "已复制全部原始输出", Toast.LENGTH_SHORT).show() Toast.makeText(context, "已复制全部原始输出", Toast.LENGTH_SHORT).show()
}) { }) {
Text("复制全部") Text("复制原始")
} }
} }
Box( Box(

View File

@ -20,6 +20,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.Json
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -59,46 +66,68 @@ class SettingsViewModel @Inject constructor(
} }
} }
/** 代理调试输出(原始 SSE 帧)。 */ /** 代理调试输出(结构化摘要)。 */
private val _proxyDebug = MutableStateFlow<String?>(null) private val _proxyDebug = MutableStateFlow<String?>(null)
val proxyDebug: StateFlow<String?> = _proxyDebug.asStateFlow() val proxyDebug: StateFlow<String?> = _proxyDebug.asStateFlow()
/** 用 Trae CN 默认账号发一条 stream=true 测试,抓上游原始 SSE。 */ /** 代理调试完整原始输出(请求 + 响应帧),供一键复制。 */
private val _proxyDebugRaw = MutableStateFlow<String?>(null)
val proxyDebugRaw: StateFlow<String?> = _proxyDebugRaw.asStateFlow()
/** 用 Trae CN 默认账号发一条 stream=true 测试。 */
fun testTraeStream() { fun testTraeStream() {
viewModelScope.launch { viewModelScope.launch {
_proxyDebug.value = "正在请求 Trae CN 流..." _proxyDebug.value = "正在请求 Trae CN 流..."
val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}""" _proxyDebugRaw.value = null
val body = """{\"model\":\"glm-5.2\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"只回复OK两个字\"}],\"max_tokens\":100}\"""
try { try {
val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN) val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
if (stream == null) { if (stream == null) {
_proxyDebug.value = "Trae CN 未配置或未登录" _proxyDebug.value = "Trae CN 未配置或未登录"
return@launch return@launch
} }
_proxyDebug.value = readStreamSample(stream, "Trae CN") val result = readStreamSample(
stream,
"Trae CN",
body,
"POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat"
)
_proxyDebug.value = result.summary
_proxyDebugRaw.value = result.raw
} catch (e: kotlinx.coroutines.CancellationException) { } catch (e: kotlinx.coroutines.CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
_proxyDebug.value = "Trae CN 异常:" + (e.message ?: "未知错误") _proxyDebug.value = "Trae CN 异常:" + (e.message ?: "未知错误")
_proxyDebugRaw.value = null
} }
} }
} }
/** 用 WorkBuddy CN 默认账号发一条 stream=true 测试,抓上游原始 SSE。 */ /** 用 WorkBuddy CN 默认账号发一条 stream=true 测试。 */
fun testWorkBuddyStream() { fun testWorkBuddyStream() {
viewModelScope.launch { viewModelScope.launch {
_proxyDebug.value = "正在请求 WorkBuddy CN 流..." _proxyDebug.value = "正在请求 WorkBuddy CN 流..."
val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}""" _proxyDebugRaw.value = null
val body = """{\"model\":\"glm-5.2\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"只回复OK两个字\"}],\"max_tokens\":100}\"""
try { try {
val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN) val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
if (stream == null) { if (stream == null) {
_proxyDebug.value = "WorkBuddy CN 未配置或未登录" _proxyDebug.value = "WorkBuddy CN 未配置或未登录"
return@launch return@launch
} }
_proxyDebug.value = readStreamSample(stream, "WorkBuddy CN") val result = readStreamSample(
stream,
"WorkBuddy CN",
body,
"POST https://copilot.tencent.com/v2/chat/completions"
)
_proxyDebug.value = result.summary
_proxyDebugRaw.value = result.raw
} catch (e: kotlinx.coroutines.CancellationException) { } catch (e: kotlinx.coroutines.CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
_proxyDebug.value = "WorkBuddy CN 异常:" + (e.message ?: "未知错误") _proxyDebug.value = "WorkBuddy CN 异常:" + (e.message ?: "未知错误")
_proxyDebugRaw.value = null
} }
} }
} }
@ -106,24 +135,133 @@ class SettingsViewModel @Inject constructor(
private suspend fun readStreamSample( private suspend fun readStreamSample(
stream: ProxyUpstreamStream, stream: ProxyUpstreamStream,
label: String, label: String,
): String = withContext(Dispatchers.IO) { requestBody: String,
val sb = StringBuilder() requestLine: String,
sb.append(label).append(" HTTP ").append(stream.status) ): StreamProbeResult = withContext(Dispatchers.IO) {
sb.append(" Content-Type=").append(stream.contentType).append("\n") val lines = mutableListOf<String>()
val raw = StringBuilder()
raw.append(label).append(" HTTP ").append(stream.status)
raw.append(" Content-Type=").append(stream.contentType).append("\n")
raw.append("请求: ").append(requestLine).append("\n")
raw.append("请求体: ").append(requestBody).append("\n")
raw.append("---- 上游响应原始帧 ----\n")
try { try {
val reader = stream.input.bufferedReader() val reader = stream.input.bufferedReader()
repeat(80) { repeat(120) {
val line = reader.readLine() ?: return@repeat val line = reader.readLine() ?: return@repeat
sb.append(line).append("\n") lines.add(line)
raw.append(line).append("\n")
if (line.trim() == "data: [DONE]") return@repeat if (line.trim() == "data: [DONE]") return@repeat
if (sb.length > 6000) return@repeat if (raw.length > 12000) return@repeat
} }
} finally { } finally {
stream.close() stream.close()
} }
sb.toString() StreamProbeResult(
summary = summarizeProbe(label, stream.status, stream.contentType, requestBody, requestLine, lines),
raw = raw.toString()
)
} }
private fun summarizeProbe(
label: String,
status: Int,
contentType: String,
requestBody: String,
requestLine: String,
lines: List<String>,
): String {
val sb = StringBuilder()
sb.append(label).append(" 探针摘要")
sb.append("\n请求: ").append(requestLine)
runCatching {
val body = json.parseToJsonElement(requestBody) as? JsonObject
if (body != null) {
val model = (body["model"] as? JsonPrimitive)?.contentOrNull ?: "?"
val count = (body["messages"] as? JsonArray)?.size ?: 0
val stream = (body["stream"] as? JsonPrimitive)?.contentOrNull ?: "?"
sb.append("\n 模型=").append(model)
sb.append(" · messages=").append(count)
sb.append(" · stream=").append(stream)
(body["tools"] as? JsonArray)?.let { sb.append(" · tools=").append(it.size) }
}
}
sb.append("\n回复: HTTP ").append(status).append(" · ").append(contentType)
var events = 0
val texts = mutableListOf<String>()
val models = linkedSetOf<String>()
val usage = mutableListOf<String>()
val errors = mutableListOf<String>()
for (line in lines) {
val trimmed = line.trim()
if (!trimmed.startsWith("data:")) continue
val payload = trimmed.removePrefix("data:").trim()
if (payload == "[DONE]") continue
val el = runCatching { json.parseToJsonElement(payload) as? JsonObject }.getOrNull() ?: continue
events++
collectProbeText(el, texts)
collectProbeModel(el, models)
collectProbeUsage(el, usage)
(el["error"] as? JsonObject)?.let { e ->
val msg = (e["message"] as? JsonPrimitive)?.contentOrNull ?: e.toString()
errors.add(msg)
}
}
if (events > 0) sb.append("\n事件帧: ").append(events)
if (models.isNotEmpty()) sb.append("\n模型: ").append(models.joinToString(" / "))
if (texts.isNotEmpty()) {
val joined = texts.joinToString("").trim().replace("\n", " ").take(200)
sb.append("\n文本(截断): ").append(if (joined.isEmpty()) "(空)" else joined)
}
if (usage.isNotEmpty()) sb.append("\n用量: ").append(usage.distinct().joinToString(" · "))
if (errors.isNotEmpty()) sb.append("\n错误: ").append(errors.take(2).joinToString(" | "))
return sb.toString()
}
private fun collectProbeText(el: JsonObject, out: MutableList<String>) {
fun add(v: JsonElement?) {
when (v) {
is JsonPrimitive -> if (v.isString) out.add(v.content)
is JsonObject -> {
(v["content"] as? JsonPrimitive)?.takeIf { it.isString }?.let { out.add(it.content) }
(v["text"] as? JsonPrimitive)?.takeIf { it.isString }?.let { out.add(it.content) }
(v["delta"] as? JsonObject)?.let { add(it) }
}
is JsonArray -> v.forEach { add(it) }
else -> Unit
}
}
(el["data"] as? JsonObject)?.let { add(it) }
(el["content"] as? JsonElement)?.let { add(it) }
(el["text"] as? JsonElement)?.let { add(it) }
(el["message"] as? JsonObject)?.let { add(it) }
(el["choices"] as? JsonArray)?.forEach { c ->
val co = c as? JsonObject ?: return@forEach
(co["delta"] as? JsonObject)?.let { add(it) }
(co["message"] as? JsonObject)?.let { add(it) }
}
}
private fun collectProbeModel(el: JsonObject, out: MutableSet<String>) {
for (key in listOf("model_name", "modelName", "config_name", "model", "provider_model_name", "modelProviderName")) {
(el[key] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() }?.let { out.add(it) }
}
(el["data"] as? JsonObject)?.let { collectProbeModel(it, out) }
(el["message"] as? JsonObject)?.let { collectProbeModel(it, out) }
}
private fun collectProbeUsage(el: JsonObject, out: MutableList<String>) {
val usage = (el["usage"] as? JsonObject) ?: (el["token_usage"] as? JsonObject) ?: return
fun num(key: String, label: String) {
(usage[key] as? JsonPrimitive)?.let { out.add(label + "=" + it.content) }
}
num("prompt_tokens", "输入")
num("completion_tokens", "输出")
num("total_tokens", "合计")
}
private val json = Json { ignoreUnknownKeys = true }
private fun generateProxyKey(): String = private fun generateProxyKey(): String =
"rainy-" + UUID.randomUUID().toString().replace("-", "").take(16) "rainy-" + UUID.randomUUID().toString().replace("-", "").take(16)
@ -151,6 +289,11 @@ class SettingsViewModel @Inject constructor(
} }
} }
data class StreamProbeResult(
val summary: String,
val raw: String,
)
data class SettingsUiState( data class SettingsUiState(
val loading: Boolean = false, val loading: Boolean = false,
val credentialStatuses: List<CredentialStatusLine> = emptyList() val credentialStatuses: List<CredentialStatusLine> = emptyList()