- 显示:请求行/模型/messages数/stream、HTTP、事件帧数、模型、文本摘要、用量、错误 - 原始请求+响应仍完整保留,点“复制原始”一键复制 - 版本升至 1.7.4
This commit is contained in:
parent
7de7111b18
commit
fbbca4a03b
@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.rainy.token"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 23
|
||||
versionName = "1.7.3"
|
||||
versionCode = 24
|
||||
versionName = "1.7.4"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -104,6 +104,7 @@ fun SettingsScreen(
|
||||
val proxyApiKey by viewModel.proxyApiKey.collectAsStateWithLifecycle()
|
||||
val proxyError by viewModel.proxyError.collectAsStateWithLifecycle()
|
||||
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
||||
val proxyDebugRaw by viewModel.proxyDebugRaw.collectAsStateWithLifecycle()
|
||||
var showLanguageDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
@ -180,6 +181,7 @@ fun SettingsScreen(
|
||||
apiKey = proxyApiKey,
|
||||
error = proxyError,
|
||||
debug = proxyDebug,
|
||||
debugRaw = proxyDebugRaw,
|
||||
onToggle = viewModel::setProxyEnabled,
|
||||
onTestTrae = viewModel::testTraeStream,
|
||||
onTestWorkBuddy = viewModel::testWorkBuddyStream
|
||||
@ -213,6 +215,7 @@ private fun ProxyGatewayCard(
|
||||
apiKey: String,
|
||||
error: String?,
|
||||
debug: String?,
|
||||
debugRaw: String?,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
onTestTrae: () -> Unit,
|
||||
onTestWorkBuddy: () -> Unit
|
||||
@ -271,17 +274,17 @@ private fun ProxyGatewayCard(
|
||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "原始输出",
|
||||
text = "探针摘要",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = InkMuted,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(onClick = {
|
||||
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()
|
||||
}) {
|
||||
Text("复制全部")
|
||||
Text("复制原始")
|
||||
}
|
||||
}
|
||||
Box(
|
||||
|
||||
@ -20,6 +20,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
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
|
||||
|
||||
/**
|
||||
@ -59,46 +66,68 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** 代理调试输出(原始 SSE 帧)。 */
|
||||
/** 代理调试输出(结构化摘要)。 */
|
||||
private val _proxyDebug = MutableStateFlow<String?>(null)
|
||||
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() {
|
||||
viewModelScope.launch {
|
||||
_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 {
|
||||
val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||
if (stream == null) {
|
||||
_proxyDebug.value = "Trae CN 未配置或未登录"
|
||||
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) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
_proxyDebug.value = "Trae CN 异常:" + (e.message ?: "未知错误")
|
||||
_proxyDebugRaw.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 用 WorkBuddy CN 默认账号发一条 stream=true 测试,抓上游原始 SSE。 */
|
||||
/** 用 WorkBuddy CN 默认账号发一条 stream=true 测试。 */
|
||||
fun testWorkBuddyStream() {
|
||||
viewModelScope.launch {
|
||||
_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 {
|
||||
val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||
if (stream == null) {
|
||||
_proxyDebug.value = "WorkBuddy CN 未配置或未登录"
|
||||
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) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
_proxyDebug.value = "WorkBuddy CN 异常:" + (e.message ?: "未知错误")
|
||||
_proxyDebugRaw.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,24 +135,133 @@ class SettingsViewModel @Inject constructor(
|
||||
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")
|
||||
requestBody: String,
|
||||
requestLine: String,
|
||||
): StreamProbeResult = withContext(Dispatchers.IO) {
|
||||
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 {
|
||||
val reader = stream.input.bufferedReader()
|
||||
repeat(80) {
|
||||
repeat(120) {
|
||||
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 (sb.length > 6000) return@repeat
|
||||
if (raw.length > 12000) return@repeat
|
||||
}
|
||||
} finally {
|
||||
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 =
|
||||
"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(
|
||||
val loading: Boolean = false,
|
||||
val credentialStatuses: List<CredentialStatusLine> = emptyList()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user