- 显示:请求行/模型/messages数/stream、HTTP、事件帧数、模型、文本摘要、用量、错误 - 原始请求+响应仍完整保留,点“复制原始”一键复制 - 版本升至 1.7.4
This commit is contained in:
parent
7de7111b18
commit
fbbca4a03b
@ -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 {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user