From 7574c3c7fbfc8f2b0ed14e9b8c16698a5219a48e Mon Sep 17 00:00:00 2001 From: Liuxinyu176 <1041316040@qq.com> Date: Fri, 9 Oct 2026 16:32:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(login):=20=E9=A1=B5=E9=9D=A2=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=8F=8D=E9=A6=88=20+=20WB=20=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=AE=B9=E9=94=99=20+=20=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E7=99=BB=E5=BD=95=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 登录页新增加载进度条、加载失败提示和重试 - 顶部新增「在浏览器打开」:Trae 谷歌登录被 WebView 拦时可走系统浏览器(沿用本机 Chrome 的 Google 登录态) - WorkBuddy 轮询兼容 accessToken/access_token/token/authToken 等返回结构,失败时打印调试日志 --- app/build.gradle.kts | 4 +- .../token/ui/webview/WebViewLoginScreen.kt | 73 ++++++++++++++++++- .../token/ui/webview/WebViewLoginViewModel.kt | 43 ++++++++--- 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5bf2a5b..f9df029 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.rainy.token" minSdk = 31 targetSdk = 35 - versionCode = 30 - versionName = "1.7.10" + versionCode = 31 + versionName = "1.7.11" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 36aac71..c0ca551 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 @@ -1,5 +1,7 @@ package com.rainy.token.ui.webview +import android.content.Intent +import android.net.Uri import android.view.ViewGroup import android.webkit.CookieManager import android.webkit.WebChromeClient @@ -10,12 +12,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -34,6 +39,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView @@ -70,7 +76,10 @@ fun WebViewLoginScreen( } val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current var webViewGeneration by remember { mutableStateOf(0) } + var pageLoading by remember { mutableStateOf(true) } + var pageError by remember { mutableStateOf(null) } // 登录成功自动回调(携带实际写入的账号 ID,供新建账号模式回填) LaunchedEffect(uiState.loginSucceeded) { @@ -104,6 +113,18 @@ fun WebViewLoginScreen( } }, actions = { + TextButton(onClick = { + val url = uiState.loginUrl + if (url.isNotBlank()) { + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + } + }) { + Text(stringResource(R.string.action_open_login_entry)) + } TextButton(onClick = { viewModel.clearWebViewSession() webViewGeneration++ @@ -118,6 +139,9 @@ fun WebViewLoginScreen( .fillMaxSize() .padding(innerPadding)) { if (uiState.loginUrl.isNotEmpty()) { + if (pageLoading) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } key(webViewGeneration) { AndroidView( factory = { context -> @@ -176,6 +200,8 @@ fun WebViewLoginScreen( } override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { + pageLoading = true + pageError = null DebugLog.i("TraeLogin", "pageStarted: ${url?.toSafeLogUrl()}") super.onPageStarted(view, url, favicon) } @@ -187,14 +213,59 @@ fun WebViewLoginScreen( viewModel.onPageFinished(url) DebugLog.i("TraeLogin", "pageFinished: ${url.toSafeLogUrl()}") } + + override fun onReceivedError( + view: WebView?, + request: android.webkit.WebResourceRequest?, + error: android.webkit.WebResourceError? + ) { + pageLoading = false + pageError = error?.description?.toString() ?: request?.url?.toString().orEmpty() + DebugLog.w("TraeLogin", "pageError: ${request?.url?.toString()?.toSafeLogUrl()} ${error?.description}") + super.onReceivedError(view, request, error) + } + } + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + pageLoading = newProgress < 100 + super.onProgressChanged(view, newProgress) + } } - webChromeClient = WebChromeClient() loadUrl(uiState.loginUrl) } }, modifier = Modifier.fillMaxSize() ) } + pageError?.let { err -> + Card(modifier = Modifier.fillMaxWidth().padding(12.dp)) { + Column(Modifier.padding(12.dp)) { + Text( + text = stringResource(R.string.error_network) + ": " + err, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + Row(horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp)) { + TextButton(onClick = { + pageError = null + webViewGeneration++ + }) { + Text(stringResource(R.string.action_retry)) + } + TextButton(onClick = { + val url = uiState.loginUrl + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + }) { + Text(stringResource(R.string.action_open_login_entry)) + } + } + } + } + } } else { // loginUrl 为空 = ViewModel 正在生成授权地址(Trae/WorkBuddy 需要片刻) Box( 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 b5a1aae..c946738 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 @@ -273,6 +273,7 @@ class WebViewLoginViewModel @Inject constructor( val state = data?.get("state")?.jsonPrimitive?.contentOrNull val authURL = data?.get("authUrl")?.jsonPrimitive?.contentOrNull ?: data?.get("authURL")?.jsonPrimitive?.contentOrNull + DebugLog.i("WorkBuddyLogin", "bind: host=${workBuddyBase} bodyKeys=${json.keys} dataKeys=${data?.keys ?: emptySet()}") if (state.isNullOrBlank() || authURL.isNullOrBlank()) { _uiState.update { it.copy(error = UiText.Resource(R.string.error_network_generic)) @@ -307,16 +308,34 @@ class WebViewLoginViewModel @Inject constructor( resp.close() s } + DebugLog.i("WorkBuddyLogin", "poll: state=${state.take(6)}… bodyLen=${body.length}") val json = Json.parseToJsonElement(body).jsonObject - val code = json["code"]?.jsonPrimitive?.intOrNull ?: -1 - if (code == 0) { - val data = json["data"]?.jsonObject - 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 + val codeText = json["code"]?.jsonPrimitive?.contentOrNull + val code = codeText?.toIntOrNull() ?: json["code"]?.jsonPrimitive?.intOrNull ?: -1 + val rawData = json["data"]?.jsonObject ?: json.jsonObject + DebugLog.i( + "WorkBuddyLogin", + "poll: code=$codeText bodyKeys=${json.keys} dataKeys=${rawData.keys}" + ) + if (code == 0 || codeText == "0") { + // 兼容多种字段:accessToken/access_token/token/authToken,refreshToken/refresh_token,expiresIn/expires_at + val accessToken = rawData.let { obj -> + obj["accessToken"]?.jsonPrimitive?.contentOrNull + ?: obj["access_token"]?.jsonPrimitive?.contentOrNull + ?: obj["token"]?.jsonPrimitive?.contentOrNull + ?: obj["authToken"]?.jsonPrimitive?.contentOrNull + } + val refreshToken = rawData.let { obj -> + obj["refreshToken"]?.jsonPrimitive?.contentOrNull + ?: obj["refresh_token"]?.jsonPrimitive?.contentOrNull + ?: obj["refresh"]?.jsonPrimitive?.contentOrNull + } + val expiresIn = rawData.let { obj -> + obj["expiresIn"]?.jsonPrimitive?.longOrNull + ?: obj["expires_in"]?.jsonPrimitive?.longOrNull + ?: obj["expiresAt"]?.jsonPrimitive?.longOrNull + ?: 7200L + } if (!accessToken.isNullOrBlank()) { val expiresAt = System.currentTimeMillis() + expiresIn * 1000 // 刚成功登录的凭据视为已验证(见 Trae 同款注释) @@ -337,11 +356,13 @@ class WebViewLoginViewModel @Inject constructor( loginAccountId = effectiveId.ifBlank { null } ) } + DebugLog.i("WorkBuddyLogin", "poll: 凭证已保存,登录完成") return } + DebugLog.w("WorkBuddyLogin", "poll: code=0 但 data 无 accessToken(dataKeys=${rawData.keys})") } - } catch (_: Exception) { - // 网络异常时继续轮询,直到超时 + } catch (e: Exception) { + DebugLog.w("WorkBuddyLogin", "poll: 异常=${e.message ?: "unknown"}") } delay(2000) }