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