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:
parent
0bed6d626a
commit
1791fa6436
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user