From 1791fa6436edeea413a6879b1a6fa3e262fc0232 Mon Sep 17 00:00:00 2001
From: Liuxinyu176 <1041316040@qq.com>
Date: Sun, 13 Sep 2026 16:37:22 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E7=99=BB=E5=BD=95=E5=90=8E=E4=BD=99?=
=?UTF-8?q?=E9=A2=9D=E4=B8=8D=E5=88=B7=E6=96=B0=20+=20=E6=98=8E=E7=BB=86?=
=?UTF-8?q?=E4=B8=AD=E8=8B=B1=E6=96=87=E6=9C=AC=E5=9C=B0=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- WebViewLogin 登录成功后 popBackStack 到 SERVICE_DETAIL,让详情页
ON_RESUME 自动刷新余额;back stack 无 SERVICE_DETAIL 时 fallback
到 guardedPop() 避免卡住
- GenericExtrasCard 重写:11 个已知技术 key 映射为三语本地化标签,
布尔值格式化为是/否,consumption_ratio 格式化为百分比
- 三语 strings.xml 新增 13 个 extra_label_* / extra_value_* 字符串
- 移除 WebViewLoginViewModel/Screen 中的调试 Log
---
.../com/rainy/token/ui/RainyTokenNavHost.kt | 7 ++-
.../ui/servicedetail/ServiceDetailScreen.kt | 56 ++++++++++++++++++-
.../token/ui/webview/WebViewLoginScreen.kt | 16 +++++-
.../token/ui/webview/WebViewLoginViewModel.kt | 25 ++++++---
app/src/main/res/values-b+zh+Hans/strings.xml | 13 +++++
app/src/main/res/values-b+zh+Hant/strings.xml | 13 +++++
app/src/main/res/values/strings.xml | 13 +++++
7 files changed, 130 insertions(+), 13 deletions(-)
diff --git a/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt b/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt
index 4fdbe27..3b576b0 100644
--- a/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt
+++ b/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt
@@ -323,7 +323,12 @@ private fun CompactNavHost() {
WebViewLoginScreen(
service = type,
onBack = guardedPop,
- onLoginSucceeded = { guardedPop() }
+ onLoginSucceeded = {
+ // 登录成功后直接返回详情页(跳过凭据编辑页),让详情页 ON_RESUME 刷新余额
+ if (!navController.popBackStack(Routes.SERVICE_DETAIL, inclusive = false)) {
+ guardedPop()
+ }
+ }
)
}
composable(
diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt
index d000032..fdf0bf9 100644
--- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt
+++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt
@@ -1212,6 +1212,9 @@ private fun CheckinNeedCredentialCard() {
/**
* 通用明细卡:把 balance.extras 中的友好键逐行展示(Trae 权益包 / WorkBuddy 资源 /
* Sub2API 账户等)。技术键(models / reset* / raw 等)一律过滤。
+ *
+ * 已知技术键(consumed_credits / pack_count / is_paid_user 等)映射为本地化标签;
+ * 未知键(权益包名等,由服务端按用户 locale 返回)原样展示。
*/
@Composable
internal fun GenericExtrasCard(state: State) {
@@ -1232,6 +1235,38 @@ internal fun GenericExtrasCard(state: State) {
!k.endsWith(".limit")
}
if (friendly.isEmpty()) return
+
+ val yesLabel = stringResource(R.string.extra_value_yes)
+ val noLabel = stringResource(R.string.extra_value_no)
+ val labelMap = mapOf(
+ "total_credits" to stringResource(R.string.extra_label_total_credits),
+ "consumed_credits" to stringResource(R.string.extra_label_consumed_credits),
+ "consumption_ratio" to stringResource(R.string.extra_label_consumption_ratio),
+ "pack_count" to stringResource(R.string.extra_label_pack_count),
+ "is_credits_billing" to stringResource(R.string.extra_label_is_credits_billing),
+ "is_dollar_usage_billing" to stringResource(R.string.extra_label_is_dollar_usage_billing),
+ "is_new_user" to stringResource(R.string.extra_label_is_new_user),
+ "packages" to stringResource(R.string.extra_label_packages),
+ "frozen" to stringResource(R.string.extra_label_frozen),
+ "is_paid_user" to stringResource(R.string.extra_label_is_paid_user),
+ "subscription" to stringResource(R.string.extra_label_subscription)
+ )
+ val knownOrder = listOf(
+ "total_credits", "consumed_credits", "consumption_ratio", "pack_count",
+ "is_credits_billing", "is_dollar_usage_billing", "is_new_user",
+ "packages", "frozen", "is_paid_user", "subscription"
+ )
+ val sorted = friendly.entries.sortedWith { a, b ->
+ val ai = knownOrder.indexOf(a.key)
+ val bi = knownOrder.indexOf(b.key)
+ when {
+ ai >= 0 && bi < 0 -> -1
+ ai < 0 && bi >= 0 -> 1
+ ai >= 0 && bi >= 0 -> ai.compareTo(bi)
+ else -> a.key.compareTo(b.key)
+ }
+ }
+
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
@@ -1247,9 +1282,11 @@ internal fun GenericExtrasCard(state: State) {
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
- friendly.forEach { (key, value) ->
+ sorted.forEach { (key, value) ->
+ val label = labelMap[key] ?: key
+ val formatted = formatExtraValue(key, value, yesLabel, noLabel)
Text(
- text = "$key: $value",
+ text = "$label: $formatted",
style = MaterialTheme.typography.bodySmall,
color = inkMuted()
)
@@ -1258,6 +1295,21 @@ internal fun GenericExtrasCard(state: State) {
}
}
+/** 格式化 extras 值:布尔 → 是/否,比例 → 百分比,其余原样。 */
+private fun formatExtraValue(
+ key: String,
+ value: String,
+ yes: String,
+ no: String
+): String = when (key) {
+ "is_credits_billing", "is_dollar_usage_billing", "is_new_user",
+ "is_paid_user", "subscription" ->
+ if (value.equals("true", ignoreCase = true)) yes else no
+ "consumption_ratio" ->
+ value.toDoubleOrNull()?.let { "%.1f%%".format(it * 100) } ?: value
+ else -> value
+}
+
@Composable
private fun ErrorCard(message: String) {
Card(
diff --git a/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginScreen.kt b/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginScreen.kt
index 42da0ae..e828469 100644
--- a/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginScreen.kt
+++ b/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginScreen.kt
@@ -117,13 +117,27 @@ fun WebViewLoginScreen(
)
webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: android.webkit.WebResourceRequest?
+ ): Boolean {
+ val url = request?.url?.toString() ?: return false
+ return handleOverrideUrl(url)
+ }
+
+ @Deprecated("deprecated in API 24, kept for older devices")
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String?
): Boolean {
url ?: return false
+ return handleOverrideUrl(url)
+ }
+
+ fun handleOverrideUrl(url: String): Boolean {
// Trae OAuth 回调:拦截 127.0.0.1:18080/authorize,提取 userJwt
- if (url.startsWith("http://127.0.0.1:18080/authorize")) {
+ if (url.startsWith("http://127.0.0.1:18080/authorize") ||
+ url.startsWith("http://127.0.0.1:18080/authorize")) {
viewModel.onTraeCallback(url)
return true
}
diff --git a/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginViewModel.kt b/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginViewModel.kt
index ad8f003..ff858c8 100644
--- a/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginViewModel.kt
+++ b/app/src/main/java/com/rainy/token/ui/webview/WebViewLoginViewModel.kt
@@ -116,7 +116,10 @@ class WebViewLoginViewModel @Inject constructor(
if (_uiState.value.loginSucceeded) return
val parsed = url.toHttpUrlOrNull() ?: return
- val userJwtRaw = parsed.queryParameter("userJwt") ?: return
+ // Trae 回调中参数名可能为 userJwt 或 user_jwt,值是 JSON 字符串
+ val userJwtRaw = parsed.queryParameter("userJwt")
+ ?: parsed.queryParameter("user_jwt")
+ ?: return
val userJwt = try {
Json.parseToJsonElement(userJwtRaw).jsonObject
} catch (_: Exception) {
@@ -151,7 +154,8 @@ class WebViewLoginViewModel @Inject constructor(
.build()
val (body, ok) = withContext(Dispatchers.IO) {
val resp = okHttpClient.newCall(request).execute()
- resp.body?.string().orEmpty() to resp.isSuccessful
+ val s = resp.body?.string().orEmpty()
+ s to resp.isSuccessful
}
if (!ok || body.isBlank()) {
_uiState.update {
@@ -162,7 +166,8 @@ class WebViewLoginViewModel @Inject constructor(
val json = Json.parseToJsonElement(body).jsonObject
val data = json["data"]?.jsonObject
val state = data?.get("state")?.jsonPrimitive?.contentOrNull
- val authURL = data?.get("authURL")?.jsonPrimitive?.contentOrNull
+ val authURL = data?.get("authUrl")?.jsonPrimitive?.contentOrNull
+ ?: data?.get("authURL")?.jsonPrimitive?.contentOrNull
if (state.isNullOrBlank() || authURL.isNullOrBlank()) {
_uiState.update {
it.copy(error = UiText.Resource(R.string.error_network_generic))
@@ -189,8 +194,7 @@ class WebViewLoginViewModel @Inject constructor(
val request = Request.Builder()
.url("https://copilot.tencent.com/v2/plugin/auth/token?state=$state")
.addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
- .addHeader("Content-Type", "application/json")
- .post("{\"state\":\"$state\"}".toRequestBody("application/json".toMediaTypeOrNull()))
+ .get()
.build()
val body = withContext(Dispatchers.IO) {
val resp = okHttpClient.newCall(request).execute()
@@ -202,9 +206,12 @@ class WebViewLoginViewModel @Inject constructor(
val code = json["code"]?.jsonPrimitive?.intOrNull ?: -1
if (code == 0) {
val data = json["data"]?.jsonObject
- val accessToken = data?.get("access_token")?.jsonPrimitive?.contentOrNull
- val refreshToken = data?.get("refresh_token")?.jsonPrimitive?.contentOrNull
- val expiresIn = data?.get("expires_in")?.jsonPrimitive?.longOrNull ?: 7200L
+ val accessToken = data?.get("accessToken")?.jsonPrimitive?.contentOrNull
+ ?: data?.get("access_token")?.jsonPrimitive?.contentOrNull
+ val refreshToken = data?.get("refreshToken")?.jsonPrimitive?.contentOrNull
+ ?: data?.get("refresh_token")?.jsonPrimitive?.contentOrNull
+ val expiresIn = data?.get("expiresIn")?.jsonPrimitive?.longOrNull
+ ?: data?.get("expires_in")?.jsonPrimitive?.longOrNull ?: 7200L
if (!accessToken.isNullOrBlank()) {
val expiresAt = System.currentTimeMillis() + expiresIn * 1000
val credential = Credential.WorkBuddyCredential(
@@ -221,7 +228,7 @@ class WebViewLoginViewModel @Inject constructor(
}
}
} catch (_: Exception) {
- // 轮询网络错误忽略,下次重试
+ // 网络异常时继续轮询,直到超时
}
delay(2000)
}
diff --git a/app/src/main/res/values-b+zh+Hans/strings.xml b/app/src/main/res/values-b+zh+Hans/strings.xml
index ff60201..0d269bf 100644
--- a/app/src/main/res/values-b+zh+Hans/strings.xml
+++ b/app/src/main/res/values-b+zh+Hans/strings.xml
@@ -141,6 +141,19 @@
OAuth · CodeBuddy 同体系积分
自托管面板 · 用户级 sk- Key 余额
明细
+ 已消耗
+ 消耗比例
+ 总积分
+ 权益包
+ 套餐数
+ 冻结
+ 积分计费
+ 美元计费
+ 新用户
+ 付费用户
+ 订阅
+ 是
+ 否
Trae 凭据
粘贴 Trae 登录态中的 Cloud-IDE-JWT(ey… 开头)。请求头按 Authorization: Cloud-IDE-JWT 发送。
Cloud-IDE-JWT
diff --git a/app/src/main/res/values-b+zh+Hant/strings.xml b/app/src/main/res/values-b+zh+Hant/strings.xml
index b73d40e..8c5f0ba 100644
--- a/app/src/main/res/values-b+zh+Hant/strings.xml
+++ b/app/src/main/res/values-b+zh+Hant/strings.xml
@@ -141,6 +141,19 @@
OAuth · CodeBuddy 同體系積分
自托管面板 · 用戶級 sk- Key 餘額
明細
+ 已消耗
+ 消耗比例
+ 總積分
+ 權益包
+ 套餐數
+ 凍結
+ 積分計費
+ 美元計費
+ 新用戶
+ 付費用戶
+ 訂閱
+ 是
+ 否
Trae 憑證
貼上 Trae 登入態中的 Cloud-IDE-JWT(ey… 開頭)。請求頭以 Authorization: Cloud-IDE-JWT 送出。
Cloud-IDE-JWT
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7a7d006..016ba21 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -144,6 +144,19 @@
OAuth · CodeBuddy 同体系积分
自托管面板 · 用户级 sk- Key 余额
Breakdown
+ Consumed
+ Consumption ratio
+ Total credits
+ Entitlement packs
+ Packages
+ Frozen
+ Credits billing
+ USD billing
+ New user
+ Paid user
+ Subscription
+ Yes
+ No
Trae credentials
Paste the Cloud-IDE-JWT from your Trae account session (starts with ey…). Used as Authorization: Cloud-IDE-JWT header.
Cloud-IDE-JWT