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.