From d6e4d34a423d27e950fdfb83dc6b65e2842eb3bb Mon Sep 17 00:00:00 2001 From: Liuxinyu176 <1041316040@qq.com> Date: Fri, 9 Oct 2026 15:28:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(login):=20INT=20L=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=B5=B0=E5=AF=B9=E5=BA=94=E5=9F=9F=E5=90=8D=20+=20=E5=8F=AF?= =?UTF-8?q?=E6=B8=85=E9=99=A4=20WebView=20=E7=99=BB=E5=BD=95=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trae INTL 登录改打开 trae.ai 授权页,不再误开 trae.cn - WorkBuddy auth state/token 按区域切换 copilot.tencent.com / workbuddy.ai - 登录页新增「清除登录」按钮:清 Cookie/WebStorage 后重开,避免沿用上一个号码的登录态 - 新建账号表单保持留空(确认逻辑已存在),登录结果回填新账号 - 版本升至 1.7.9 --- app/build.gradle.kts | 4 +- .../com/rainy/token/ui/RainyTokenNavHost.kt | 50 ++++++++++++++----- .../ui/servicedetail/ServiceDetailScreen.kt | 4 +- .../servicedetail/ServiceDetailViewModel.kt | 11 +++- .../token/ui/settings/CredentialEditScreen.kt | 8 +-- .../token/ui/webview/WebViewLoginScreen.kt | 17 ++++++- .../token/ui/webview/WebViewLoginViewModel.kt | 28 +++++++++-- app/src/main/res/values-b+zh+Hans/strings.xml | 1 + app/src/main/res/values-b+zh+Hant/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 97 insertions(+), 28 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fcac958..36c5e49 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 = 28 - versionName = "1.7.8" + versionCode = 29 + versionName = "1.7.9" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 b5a42a8..56a4de6 100644 --- a/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt +++ b/app/src/main/java/com/rainy/token/ui/RainyTokenNavHost.kt @@ -90,13 +90,16 @@ object Routes { } const val ACCOUNTS = "accounts/{type}" fun accounts(type: ServiceType) = "accounts/${type.name}" - const val WEBVIEW_LOGIN = "webview_login/{type}?account={account}" - fun webviewLogin(type: ServiceType, accountId: String? = null): String = - if (accountId.isNullOrBlank()) { - "webview_login/${type.name}" - } else { - "webview_login/${type.name}?account=${Uri.encode(accountId)}" + const val WEBVIEW_LOGIN = "webview_login/{type}?account={account}®ion={region}" + fun webviewLogin(type: ServiceType, accountId: String? = null, region: String? = null): String = buildString { + append("webview_login/").append(type.name) + if (!accountId.isNullOrBlank()) { + append("?account=").append(Uri.encode(accountId)) } + if (!region.isNullOrBlank()) { + append(if (accountId.isNullOrBlank()) "?" else "&").append("region=").append(Uri.encode(region)) + } + } const val WEBVIEW_PAGE = "webview_page?url={url}&title={title}" fun webviewPage(url: String, title: String) = "webview_page?url=${Uri.encode(url)}&title=${Uri.encode(title)}" @@ -356,7 +359,7 @@ private fun CompactNavHost() { service = type, accountId = accountId, onBack = guardedPop, - onStartWebViewLogin = { svc, acc -> navController.navigate(Routes.webviewLogin(svc, acc)) }, + onStartWebViewLogin = { svc, acc, region -> navController.navigate(Routes.webviewLogin(svc, acc, region)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { navController.navigate(Routes.CODEX_OAUTH) } ) @@ -369,14 +372,21 @@ private fun CompactNavHost() { type = NavType.StringType nullable = true defaultValue = null + }, + navArgument("region") { + type = NavType.StringType + nullable = true + defaultValue = null } ) ) { backStackEntry -> val type = parseServiceType(backStackEntry.arguments?.getString("type")) val accountId = backStackEntry.arguments?.getString("account") + val region = backStackEntry.arguments?.getString("region") WebViewLoginScreen( service = type, accountId = accountId, + region = region, onBack = guardedPop, onLoginSucceeded = { _, effectiveId -> // 把实际写入的账号 ID 回传给上一层(凭据编辑页),新建账号模式用于回填 @@ -417,7 +427,7 @@ private fun CompactNavHost() { } }, onConfigureCredential = { svc -> navController.navigate(credentialEntry(svc)) }, - onStartWebViewLogin = { svc, acc -> navController.navigate(Routes.webviewLogin(svc, acc)) }, + onStartWebViewLogin = { svc, acc, region -> navController.navigate(Routes.webviewLogin(svc, acc, region)) }, onOpenRecharge = { svc -> ServiceConfigProvider.get(svc).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) @@ -543,7 +553,7 @@ private fun ExpandedDetailPane( onBack = onClose, onSwitchAccount = onSwitchAccount, onConfigureCredential = { onClose() }, - onStartWebViewLogin = { _, _ -> }, + onStartWebViewLogin = { _, _, _ -> }, onOpenRecharge = { svc -> ServiceConfigProvider.get(svc).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) @@ -692,8 +702,8 @@ private fun ExpandedDetailPane( service = type, accountId = accountId, onBack = { apiNavController.popBackStack() }, - onStartWebViewLogin = { svc, acc -> - apiNavController.navigate(Routes.webviewLogin(svc, acc)) + onStartWebViewLogin = { svc, acc, region -> + apiNavController.navigate(Routes.webviewLogin(svc, acc, region)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { apiNavController.navigate(Routes.CODEX_OAUTH) } @@ -707,14 +717,21 @@ private fun ExpandedDetailPane( type = NavType.StringType nullable = true defaultValue = null + }, + navArgument("region") { + type = NavType.StringType + nullable = true + defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") + val region = entry.arguments?.getString("region") WebViewLoginScreen( service = type, accountId = accountId, + region = region, onBack = { apiNavController.popBackStack() }, onLoginSucceeded = { _, effectiveId -> if (!effectiveId.isNullOrBlank()) { @@ -817,8 +834,8 @@ private fun ExpandedDetailPane( service = type, accountId = accountId, onBack = { settingsNavController.popBackStack() }, - onStartWebViewLogin = { svc, acc -> - settingsNavController.navigate(Routes.webviewLogin(svc, acc)) + onStartWebViewLogin = { svc, acc, region -> + settingsNavController.navigate(Routes.webviewLogin(svc, acc, region)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { settingsNavController.navigate(Routes.CODEX_OAUTH) } @@ -832,14 +849,21 @@ private fun ExpandedDetailPane( type = NavType.StringType nullable = true defaultValue = null + }, + navArgument("region") { + type = NavType.StringType + nullable = true + defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") + val region = entry.arguments?.getString("region") WebViewLoginScreen( service = type, accountId = accountId, + region = region, onBack = { settingsNavController.popBackStack() }, onLoginSucceeded = { _, effectiveId -> if (!effectiveId.isNullOrBlank()) { 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 1a1d1d5..473674a 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 @@ -114,7 +114,7 @@ fun ServiceDetailScreen( onBack: () -> Unit, onSwitchAccount: (ServiceType, String?) -> Unit = { _, _ -> }, onConfigureCredential: (ServiceType) -> Unit, - onStartWebViewLogin: (ServiceType, String?) -> Unit, + onStartWebViewLogin: (ServiceType, String?, String?) -> Unit, onOpenRecharge: (ServiceType) -> Unit = { }, viewModel: ServiceDetailViewModel = hiltViewModel() ) { @@ -315,7 +315,7 @@ fun ServiceDetailScreen( modelsLoading = modelsLoading, onRefresh = { viewModel.refresh() }, onConfigureCredential = { onConfigureCredential(service) }, - onStartWebViewLogin = { onStartWebViewLogin(service, accountId) }, + onStartWebViewLogin = { onStartWebViewLogin(service, accountId, uiState.region) }, onOpenRecharge = config.externalUrl?.let { { onOpenRecharge(service) } }, onTriggerUsage = { viewModel.triggerUsage() }, onSelectModel = { viewModel.selectModel(it) }, diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt index 31fd4f4..a7aa4ae 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt @@ -454,6 +454,13 @@ class ServiceDetailViewModel @Inject constructor( if (serviceGen != serviceGeneration || _serviceType.value != type) return@launch currentCredentialFingerprint = newFingerprint + val currentCredential = credentialRepository.get(type, accountId) + val currentRegion = when (currentCredential) { + is Credential.TraeCredential -> currentCredential.region.uppercase() + is Credential.WorkBuddyCredential -> currentCredential.region.uppercase() + else -> null + } + _uiState.update { it.copy(region = currentRegion) } when (change) { CredentialChange.NONE_TO_NONE, CredentialChange.UNCHANGED -> { @@ -1008,7 +1015,9 @@ sealed class State { data class ServiceDetailUiState( val state: State = State.Loading, val hasCredential: Boolean = false, - val cached: CachedBalance? = null + val cached: CachedBalance? = null, + /** 当前账号区域(Trae/WorkBuddy):CN / INTL;其他服务为 null。 */ + val region: String? = null ) sealed class TriggerState { diff --git a/app/src/main/java/com/rainy/token/ui/settings/CredentialEditScreen.kt b/app/src/main/java/com/rainy/token/ui/settings/CredentialEditScreen.kt index f968da7..11f03be 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/CredentialEditScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/CredentialEditScreen.kt @@ -82,7 +82,7 @@ fun CredentialEditScreen( service: ServiceType, accountId: String? = null, onBack: () -> Unit, - onStartWebViewLogin: (ServiceType, String?) -> Unit, + onStartWebViewLogin: (ServiceType, String?, String?) -> Unit, onWebViewLoginSuccess: (ServiceType) -> Unit, onStartCodexOAuth: () -> Unit = {}, viewModel: CredentialEditViewModel = hiltViewModel() @@ -159,7 +159,7 @@ fun CredentialEditScreen( onJwtChange = viewModel::updateTraeJwt, onRegionChange = viewModel::updateTraeRegion, onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId, - onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId) }, + onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId, uiState.traeRegion) }, onSave = viewModel::saveTraeCredential ) } else if (service == ServiceType.WORKBUDDY) { @@ -171,7 +171,7 @@ fun CredentialEditScreen( onRefreshChange = viewModel::updateWorkBuddyRefresh, region = uiState.workBuddyRegion, onRegionChange = viewModel::updateWorkBuddyRegion, - onStartInAppLogin = { onStartWebViewLogin(ServiceType.WORKBUDDY, accountId) }, + onStartInAppLogin = { onStartWebViewLogin(ServiceType.WORKBUDDY, accountId, uiState.workBuddyRegion) }, onSave = viewModel::saveWorkBuddyCredential ) } else if (service == ServiceType.SUB2API) { @@ -269,7 +269,7 @@ fun CredentialEditScreen( onImportFromClipboard = { viewModel.importFromClipboard(context) }, onCopyLoginUrl = { copyToClipboard(context, uiState.loginUrl) }, onOpenLoginUrl = { openInBrowser(context, uiState.loginUrl) }, - onStartInAppLogin = { onStartWebViewLogin(ServiceType.OPENCODE_GO, accountId) }, + onStartInAppLogin = { onStartWebViewLogin(ServiceType.OPENCODE_GO, accountId, null) }, onShowHelp = { showGoHelp = true }, onApiKeyChange = viewModel::updateTriggerApiKey, onSaveApiKey = { viewModel.saveTriggerApiKey() } 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 52cd313..36aac71 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 @@ -29,7 +29,10 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.key import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -54,6 +57,7 @@ import com.rainy.token.ui.components.asString fun WebViewLoginScreen( service: ServiceType, accountId: String? = null, + region: String? = null, onBack: () -> Unit, onLoginSucceeded: (ServiceType, String?) -> Unit, /** 用户已保存的面板基址(Sub2API 的 loginUrl 为空,靠它提供登录入口与可信 host)。 */ @@ -62,10 +66,11 @@ fun WebViewLoginScreen( ) { // configuredBaseUrl 可选:不传时 ViewModel 自行从凭据仓库读取(Sub2API 自托管实例) LaunchedEffect(service, configuredBaseUrl, accountId) { - viewModel.bind(service, configuredBaseUrl, accountId) + viewModel.bind(service, configuredBaseUrl, accountId, region) } val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } + var webViewGeneration by remember { mutableStateOf(0) } // 登录成功自动回调(携带实际写入的账号 ID,供新建账号模式回填) LaunchedEffect(uiState.loginSucceeded) { @@ -97,6 +102,14 @@ fun WebViewLoginScreen( IconButton(onClick = onBack) { Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back)) } + }, + actions = { + TextButton(onClick = { + viewModel.clearWebViewSession() + webViewGeneration++ + }) { + Text(stringResource(R.string.webview_clear_login)) + } } ) } @@ -105,6 +118,7 @@ fun WebViewLoginScreen( .fillMaxSize() .padding(innerPadding)) { if (uiState.loginUrl.isNotEmpty()) { + key(webViewGeneration) { AndroidView( factory = { context -> WebView(context).apply { @@ -180,6 +194,7 @@ fun WebViewLoginScreen( }, modifier = Modifier.fillMaxSize() ) + } } 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 80534f0..b679aa4 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 @@ -57,14 +57,19 @@ class WebViewLoginViewModel @Inject constructor( /** WorkBuddy 轮询协程,bind 新服务时取消。 */ private var workBuddyPollingJob: Job? = null + /** WorkBuddy 当前基址(决定 auth state/token 用 CN 还是 INTL)。 */ + private var workBuddyBase = "https://copilot.tencent.com" /** 目标账号(多账号服务);null = 新建账号。 */ private var accountId: String? = null + /** 当前登录目标区域(Trae/WorkBuddy);null = 其他服务。 */ + private var region: String? = null - fun bind(service: ServiceType, configuredBaseUrl: String? = null, accountId: String? = null) { + fun bind(service: ServiceType, configuredBaseUrl: String? = null, accountId: String? = null, region: String? = null) { workBuddyPollingJob?.cancel() workBuddyPollingJob = null this.accountId = accountId?.takeIf { it.isNotBlank() } + this.region = region?.takeIf { it.isNotBlank() }?.uppercase() val config = ServiceConfigProvider.get(service) _uiState.update { @@ -134,8 +139,9 @@ class WebViewLoginViewModel @Inject constructor( // 也不可用 userJwt.ClientID(固定客户端标识,所有用户相同,必被按设备限流)。 val deviceId = traeDeviceId() val callbackUrl = "http://127.0.0.1:18080/authorize" + val authRoot = if (region == "INTL") "https://www.trae.ai/authorization" else "https://www.trae.cn/authorization" val authUrl = buildString { - append("https://www.trae.cn/authorization?") + append(authRoot).append("?") append("login_version=1&auth_from=solo&login_channel=native_ide&") append("auth_type=local&client_id=en1oxy7wnw8j9n&redirect=0&") append("login_trace_id=$traceId&") @@ -221,7 +227,7 @@ class WebViewLoginViewModel @Inject constructor( val credential = Credential.TraeCredential( service = ServiceType.TRAE, jwt = token, - region = "CN", + region = if (region == "INTL") "INTL" else "CN", deviceId = deviceId, userId = userId?.takeIf { it.isNotBlank() }, lastVerifiedAt = System.currentTimeMillis() @@ -239,8 +245,9 @@ class WebViewLoginViewModel @Inject constructor( private fun bindWorkBuddy() { viewModelScope.launch { try { + workBuddyBase = if (region == "INTL") "https://www.workbuddy.ai" else "https://copilot.tencent.com" val request = Request.Builder() - .url("https://copilot.tencent.com/v2/plugin/auth/state?platform=CLI") + .url(workBuddyBase + "/v2/plugin/auth/state?platform=CLI") .addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2") .addHeader("Content-Type", "application/json") .post("{}".toRequestBody("application/json".toMediaTypeOrNull())) @@ -285,7 +292,7 @@ class WebViewLoginViewModel @Inject constructor( if (_uiState.value.loginSucceeded) return try { val request = Request.Builder() - .url("https://copilot.tencent.com/v2/plugin/auth/token?state=$state") + .url(workBuddyBase + "/v2/plugin/auth/token?state=$state") .addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2") .get() .build() @@ -312,6 +319,7 @@ class WebViewLoginViewModel @Inject constructor( service = ServiceType.WORKBUDDY, accessToken = accessToken, refreshToken = refreshToken ?: "", + region = if (region == "INTL") "INTL" else "CN", expiresAt = expiresAt, lastVerifiedAt = System.currentTimeMillis() ) @@ -361,6 +369,16 @@ class WebViewLoginViewModel @Inject constructor( } } + /** 清除 WebView 登录缓存(Cookie/Storage),用于更换账号登录。 */ + fun clearWebViewSession() { + runCatching { + val cm = android.webkit.CookieManager.getInstance() + cm.removeAllCookies(null) + cm.removeSessionCookies(null) + android.webkit.WebStorage.getInstance().deleteAllData() + } + } + // ─── Page lifecycle ──────────────────────────────────────────────────── fun onPageFinished(url: String) { 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 97f00ea..d2d2811 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -447,6 +447,7 @@ token 刷新失败,请重新登录或重新导入 auth.json + 清除登录 登录 %1$s 完成登录了吗? 如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。 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 c99722d..cd8f83e 100644 --- a/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/app/src/main/res/values-b+zh+Hant/strings.xml @@ -447,6 +447,7 @@ token 重新整理失敗,請重新登入或重新匯入 auth.json + 清除登入 登入 %1$s 完成登入了嗎? 如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 15d0bee..bfed392 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -450,6 +450,7 @@ Token refresh failed. Please log in again or import auth.json. + Clear login Sign in to %1$s Done logging in? If you finished signing in but the page didn\'t redirect, tap the button below to grab the cookie and save your session.