fix(login): INT L 登录走对应域名 + 可清除 WebView 登录态
- Trae INTL 登录改打开 trae.ai 授权页,不再误开 trae.cn - WorkBuddy auth state/token 按区域切换 copilot.tencent.com / workbuddy.ai - 登录页新增「清除登录」按钮:清 Cookie/WebStorage 后重开,避免沿用上一个号码的登录态 - 新建账号表单保持留空(确认逻辑已存在),登录结果回填新账号 - 版本升至 1.7.9
This commit is contained in:
parent
e914633797
commit
d6e4d34a42
@ -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 {
|
||||
|
||||
@ -90,12 +90,15 @@ 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) =
|
||||
@ -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()) {
|
||||
|
||||
@ -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) },
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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() }
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -447,6 +447,7 @@
|
||||
<string name="error_token_refresh_generic">token 刷新失败,请重新登录或重新导入 auth.json</string>
|
||||
|
||||
<!-- ══════════ WebView 登录 ══════════ -->
|
||||
<string name="webview_clear_login">清除登录</string>
|
||||
<string name="title_webview_login">登录 %1$s</string>
|
||||
<string name="dialog_login_done_title">完成登录了吗?</string>
|
||||
<string name="dialog_login_done_body">如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。</string>
|
||||
|
||||
@ -447,6 +447,7 @@
|
||||
<string name="error_token_refresh_generic">token 重新整理失敗,請重新登入或重新匯入 auth.json</string>
|
||||
|
||||
<!-- ══════════ WebView 登录 ══════════ -->
|
||||
<string name="webview_clear_login">清除登入</string>
|
||||
<string name="title_webview_login">登入 %1$s</string>
|
||||
<string name="dialog_login_done_title">完成登入了嗎?</string>
|
||||
<string name="dialog_login_done_body">如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。</string>
|
||||
|
||||
@ -450,6 +450,7 @@
|
||||
<string name="error_token_refresh_generic">Token refresh failed. Please log in again or import auth.json.</string>
|
||||
|
||||
<!-- ══════════ WebView 登录 ══════════ -->
|
||||
<string name="webview_clear_login">Clear login</string>
|
||||
<string name="title_webview_login">Sign in to %1$s</string>
|
||||
<string name="dialog_login_done_title">Done logging in?</string>
|
||||
<string name="dialog_login_done_body">If you finished signing in but the page didn\'t redirect, tap the button below to grab the cookie and save your session.</string>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user