fix: 登录后余额不刷新 + 明细中英文本地化

- 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
This commit is contained in:
Liuxinyu176 2026-09-13 16:37:22 +08:00
parent 0bed6d626a
commit 1791fa6436
7 changed files with 130 additions and 13 deletions

View File

@ -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(

View File

@ -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(

View File

@ -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
}

View File

@ -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)
}

View File

@ -141,6 +141,19 @@
<string name="service_desc_workbuddy">OAuth · CodeBuddy 同体系积分</string>
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
<string name="detail_extras_title">明细</string>
<string name="extra_label_consumed_credits">已消耗</string>
<string name="extra_label_consumption_ratio">消耗比例</string>
<string name="extra_label_total_credits">总积分</string>
<string name="extra_label_pack_count">权益包</string>
<string name="extra_label_packages">套餐数</string>
<string name="extra_label_frozen">冻结</string>
<string name="extra_label_is_credits_billing">积分计费</string>
<string name="extra_label_is_dollar_usage_billing">美元计费</string>
<string name="extra_label_is_new_user">新用户</string>
<string name="extra_label_is_paid_user">付费用户</string>
<string name="extra_label_subscription">订阅</string>
<string name="extra_value_yes">是</string>
<string name="extra_value_no">否</string>
<string name="credential_title_trae">Trae 凭据</string>
<string name="credential_hint_trae">粘贴 Trae 登录态中的 Cloud-IDE-JWT(ey… 开头)。请求头按 Authorization: Cloud-IDE-JWT 发送。</string>
<string name="field_trae_jwt">Cloud-IDE-JWT</string>

View File

@ -141,6 +141,19 @@
<string name="service_desc_workbuddy">OAuth · CodeBuddy 同體系積分</string>
<string name="service_desc_sub2api">自托管面板 · 用戶級 sk- Key 餘額</string>
<string name="detail_extras_title">明細</string>
<string name="extra_label_consumed_credits">已消耗</string>
<string name="extra_label_consumption_ratio">消耗比例</string>
<string name="extra_label_total_credits">總積分</string>
<string name="extra_label_pack_count">權益包</string>
<string name="extra_label_packages">套餐數</string>
<string name="extra_label_frozen">凍結</string>
<string name="extra_label_is_credits_billing">積分計費</string>
<string name="extra_label_is_dollar_usage_billing">美元計費</string>
<string name="extra_label_is_new_user">新用戶</string>
<string name="extra_label_is_paid_user">付費用戶</string>
<string name="extra_label_subscription">訂閱</string>
<string name="extra_value_yes">是</string>
<string name="extra_value_no">否</string>
<string name="credential_title_trae">Trae 憑證</string>
<string name="credential_hint_trae">貼上 Trae 登入態中的 Cloud-IDE-JWT(ey… 開頭)。請求頭以 Authorization: Cloud-IDE-JWT 送出。</string>
<string name="field_trae_jwt">Cloud-IDE-JWT</string>

View File

@ -144,6 +144,19 @@
<string name="service_desc_workbuddy">OAuth · CodeBuddy 同体系积分</string>
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
<string name="detail_extras_title">Breakdown</string>
<string name="extra_label_consumed_credits">Consumed</string>
<string name="extra_label_consumption_ratio">Consumption ratio</string>
<string name="extra_label_total_credits">Total credits</string>
<string name="extra_label_pack_count">Entitlement packs</string>
<string name="extra_label_packages">Packages</string>
<string name="extra_label_frozen">Frozen</string>
<string name="extra_label_is_credits_billing">Credits billing</string>
<string name="extra_label_is_dollar_usage_billing">USD billing</string>
<string name="extra_label_is_new_user">New user</string>
<string name="extra_label_is_paid_user">Paid user</string>
<string name="extra_label_subscription">Subscription</string>
<string name="extra_value_yes">Yes</string>
<string name="extra_value_no">No</string>
<string name="credential_title_trae">Trae credentials</string>
<string name="credential_hint_trae">Paste the Cloud-IDE-JWT from your Trae account session (starts with ey…). Used as Authorization: Cloud-IDE-JWT header.</string>
<string name="field_trae_jwt">Cloud-IDE-JWT</string>