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(
|
WebViewLoginScreen(
|
||||||
service = type,
|
service = type,
|
||||||
onBack = guardedPop,
|
onBack = guardedPop,
|
||||||
onLoginSucceeded = { guardedPop() }
|
onLoginSucceeded = {
|
||||||
|
// 登录成功后直接返回详情页(跳过凭据编辑页),让详情页 ON_RESUME 刷新余额
|
||||||
|
if (!navController.popBackStack(Routes.SERVICE_DETAIL, inclusive = false)) {
|
||||||
|
guardedPop()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
|
|||||||
@ -1212,6 +1212,9 @@ private fun CheckinNeedCredentialCard() {
|
|||||||
/**
|
/**
|
||||||
* 通用明细卡:把 balance.extras 中的友好键逐行展示(Trae 权益包 / WorkBuddy 资源 /
|
* 通用明细卡:把 balance.extras 中的友好键逐行展示(Trae 权益包 / WorkBuddy 资源 /
|
||||||
* Sub2API 账户等)。技术键(models / reset* / raw 等)一律过滤。
|
* Sub2API 账户等)。技术键(models / reset* / raw 等)一律过滤。
|
||||||
|
*
|
||||||
|
* 已知技术键(consumed_credits / pack_count / is_paid_user 等)映射为本地化标签;
|
||||||
|
* 未知键(权益包名等,由服务端按用户 locale 返回)原样展示。
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun GenericExtrasCard(state: State) {
|
internal fun GenericExtrasCard(state: State) {
|
||||||
@ -1232,6 +1235,38 @@ internal fun GenericExtrasCard(state: State) {
|
|||||||
!k.endsWith(".limit")
|
!k.endsWith(".limit")
|
||||||
}
|
}
|
||||||
if (friendly.isEmpty()) return
|
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(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@ -1247,9 +1282,11 @@ internal fun GenericExtrasCard(state: State) {
|
|||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold
|
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(
|
||||||
text = "$key: $value",
|
text = "$label: $formatted",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = inkMuted()
|
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
|
@Composable
|
||||||
private fun ErrorCard(message: String) {
|
private fun ErrorCard(message: String) {
|
||||||
Card(
|
Card(
|
||||||
|
|||||||
@ -117,13 +117,27 @@ fun WebViewLoginScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
webViewClient = object : WebViewClient() {
|
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(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
url: String?
|
url: String?
|
||||||
): Boolean {
|
): Boolean {
|
||||||
url ?: return false
|
url ?: return false
|
||||||
|
return handleOverrideUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleOverrideUrl(url: String): Boolean {
|
||||||
// Trae OAuth 回调:拦截 127.0.0.1:18080/authorize,提取 userJwt
|
// 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)
|
viewModel.onTraeCallback(url)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,7 +116,10 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
if (_uiState.value.loginSucceeded) return
|
if (_uiState.value.loginSucceeded) return
|
||||||
|
|
||||||
val parsed = url.toHttpUrlOrNull() ?: 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 {
|
val userJwt = try {
|
||||||
Json.parseToJsonElement(userJwtRaw).jsonObject
|
Json.parseToJsonElement(userJwtRaw).jsonObject
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@ -151,7 +154,8 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
.build()
|
.build()
|
||||||
val (body, ok) = withContext(Dispatchers.IO) {
|
val (body, ok) = withContext(Dispatchers.IO) {
|
||||||
val resp = okHttpClient.newCall(request).execute()
|
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()) {
|
if (!ok || body.isBlank()) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@ -162,7 +166,8 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
val json = Json.parseToJsonElement(body).jsonObject
|
val json = Json.parseToJsonElement(body).jsonObject
|
||||||
val data = json["data"]?.jsonObject
|
val data = json["data"]?.jsonObject
|
||||||
val state = data?.get("state")?.jsonPrimitive?.contentOrNull
|
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()) {
|
if (state.isNullOrBlank() || authURL.isNullOrBlank()) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(error = UiText.Resource(R.string.error_network_generic))
|
it.copy(error = UiText.Resource(R.string.error_network_generic))
|
||||||
@ -189,8 +194,7 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("https://copilot.tencent.com/v2/plugin/auth/token?state=$state")
|
.url("https://copilot.tencent.com/v2/plugin/auth/token?state=$state")
|
||||||
.addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
|
.addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
|
||||||
.addHeader("Content-Type", "application/json")
|
.get()
|
||||||
.post("{\"state\":\"$state\"}".toRequestBody("application/json".toMediaTypeOrNull()))
|
|
||||||
.build()
|
.build()
|
||||||
val body = withContext(Dispatchers.IO) {
|
val body = withContext(Dispatchers.IO) {
|
||||||
val resp = okHttpClient.newCall(request).execute()
|
val resp = okHttpClient.newCall(request).execute()
|
||||||
@ -202,9 +206,12 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
val code = json["code"]?.jsonPrimitive?.intOrNull ?: -1
|
val code = json["code"]?.jsonPrimitive?.intOrNull ?: -1
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
val data = json["data"]?.jsonObject
|
val data = json["data"]?.jsonObject
|
||||||
val accessToken = data?.get("access_token")?.jsonPrimitive?.contentOrNull
|
val accessToken = data?.get("accessToken")?.jsonPrimitive?.contentOrNull
|
||||||
val refreshToken = data?.get("refresh_token")?.jsonPrimitive?.contentOrNull
|
?: data?.get("access_token")?.jsonPrimitive?.contentOrNull
|
||||||
val expiresIn = data?.get("expires_in")?.jsonPrimitive?.longOrNull ?: 7200L
|
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()) {
|
if (!accessToken.isNullOrBlank()) {
|
||||||
val expiresAt = System.currentTimeMillis() + expiresIn * 1000
|
val expiresAt = System.currentTimeMillis() + expiresIn * 1000
|
||||||
val credential = Credential.WorkBuddyCredential(
|
val credential = Credential.WorkBuddyCredential(
|
||||||
@ -221,7 +228,7 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// 轮询网络错误忽略,下次重试
|
// 网络异常时继续轮询,直到超时
|
||||||
}
|
}
|
||||||
delay(2000)
|
delay(2000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,6 +141,19 @@
|
|||||||
<string name="service_desc_workbuddy">OAuth · CodeBuddy 同体系积分</string>
|
<string name="service_desc_workbuddy">OAuth · CodeBuddy 同体系积分</string>
|
||||||
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
|
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
|
||||||
<string name="detail_extras_title">明细</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_title_trae">Trae 凭据</string>
|
||||||
<string name="credential_hint_trae">粘贴 Trae 登录态中的 Cloud-IDE-JWT(ey… 开头)。请求头按 Authorization: Cloud-IDE-JWT 发送。</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>
|
<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_workbuddy">OAuth · CodeBuddy 同體系積分</string>
|
||||||
<string name="service_desc_sub2api">自托管面板 · 用戶級 sk- Key 餘額</string>
|
<string name="service_desc_sub2api">自托管面板 · 用戶級 sk- Key 餘額</string>
|
||||||
<string name="detail_extras_title">明細</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_title_trae">Trae 憑證</string>
|
||||||
<string name="credential_hint_trae">貼上 Trae 登入態中的 Cloud-IDE-JWT(ey… 開頭)。請求頭以 Authorization: Cloud-IDE-JWT 送出。</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>
|
<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_workbuddy">OAuth · CodeBuddy 同体系积分</string>
|
||||||
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
|
<string name="service_desc_sub2api">自托管面板 · 用户级 sk- Key 余额</string>
|
||||||
<string name="detail_extras_title">Breakdown</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_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="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>
|
<string name="field_trae_jwt">Cloud-IDE-JWT</string>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user