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:
Liuxinyu176 2026-10-09 15:28:00 +08:00
parent e914633797
commit d6e4d34a42
10 changed files with 97 additions and 28 deletions

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.rainy.token" applicationId = "com.rainy.token"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 28 versionCode = 29
versionName = "1.7.8" versionName = "1.7.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -90,12 +90,15 @@ object Routes {
} }
const val ACCOUNTS = "accounts/{type}" const val ACCOUNTS = "accounts/{type}"
fun accounts(type: ServiceType) = "accounts/${type.name}" fun accounts(type: ServiceType) = "accounts/${type.name}"
const val WEBVIEW_LOGIN = "webview_login/{type}?account={account}" const val WEBVIEW_LOGIN = "webview_login/{type}?account={account}&region={region}"
fun webviewLogin(type: ServiceType, accountId: String? = null): String = fun webviewLogin(type: ServiceType, accountId: String? = null, region: String? = null): String = buildString {
if (accountId.isNullOrBlank()) { append("webview_login/").append(type.name)
"webview_login/${type.name}" if (!accountId.isNullOrBlank()) {
} else { append("?account=").append(Uri.encode(accountId))
"webview_login/${type.name}?account=${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}" const val WEBVIEW_PAGE = "webview_page?url={url}&title={title}"
fun webviewPage(url: String, title: String) = fun webviewPage(url: String, title: String) =
@ -356,7 +359,7 @@ private fun CompactNavHost() {
service = type, service = type,
accountId = accountId, accountId = accountId,
onBack = guardedPop, onBack = guardedPop,
onStartWebViewLogin = { svc, acc -> navController.navigate(Routes.webviewLogin(svc, acc)) }, onStartWebViewLogin = { svc, acc, region -> navController.navigate(Routes.webviewLogin(svc, acc, region)) },
onWebViewLoginSuccess = { }, onWebViewLoginSuccess = { },
onStartCodexOAuth = { navController.navigate(Routes.CODEX_OAUTH) } onStartCodexOAuth = { navController.navigate(Routes.CODEX_OAUTH) }
) )
@ -369,14 +372,21 @@ private fun CompactNavHost() {
type = NavType.StringType type = NavType.StringType
nullable = true nullable = true
defaultValue = null defaultValue = null
},
navArgument("region") {
type = NavType.StringType
nullable = true
defaultValue = null
} }
) )
) { backStackEntry -> ) { backStackEntry ->
val type = parseServiceType(backStackEntry.arguments?.getString("type")) val type = parseServiceType(backStackEntry.arguments?.getString("type"))
val accountId = backStackEntry.arguments?.getString("account") val accountId = backStackEntry.arguments?.getString("account")
val region = backStackEntry.arguments?.getString("region")
WebViewLoginScreen( WebViewLoginScreen(
service = type, service = type,
accountId = accountId, accountId = accountId,
region = region,
onBack = guardedPop, onBack = guardedPop,
onLoginSucceeded = { _, effectiveId -> onLoginSucceeded = { _, effectiveId ->
// 把实际写入的账号 ID 回传给上一层(凭据编辑页),新建账号模式用于回填 // 把实际写入的账号 ID 回传给上一层(凭据编辑页),新建账号模式用于回填
@ -417,7 +427,7 @@ private fun CompactNavHost() {
} }
}, },
onConfigureCredential = { svc -> navController.navigate(credentialEntry(svc)) }, 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 -> onOpenRecharge = { svc ->
ServiceConfigProvider.get(svc).externalUrl?.let { url -> ServiceConfigProvider.get(svc).externalUrl?.let { url ->
navController.navigate(Routes.webviewPage(url, rechargeLabel)) navController.navigate(Routes.webviewPage(url, rechargeLabel))
@ -543,7 +553,7 @@ private fun ExpandedDetailPane(
onBack = onClose, onBack = onClose,
onSwitchAccount = onSwitchAccount, onSwitchAccount = onSwitchAccount,
onConfigureCredential = { onClose() }, onConfigureCredential = { onClose() },
onStartWebViewLogin = { _, _ -> }, onStartWebViewLogin = { _, _, _ -> },
onOpenRecharge = { svc -> onOpenRecharge = { svc ->
ServiceConfigProvider.get(svc).externalUrl?.let { url -> ServiceConfigProvider.get(svc).externalUrl?.let { url ->
navController.navigate(Routes.webviewPage(url, rechargeLabel)) navController.navigate(Routes.webviewPage(url, rechargeLabel))
@ -692,8 +702,8 @@ private fun ExpandedDetailPane(
service = type, service = type,
accountId = accountId, accountId = accountId,
onBack = { apiNavController.popBackStack() }, onBack = { apiNavController.popBackStack() },
onStartWebViewLogin = { svc, acc -> onStartWebViewLogin = { svc, acc, region ->
apiNavController.navigate(Routes.webviewLogin(svc, acc)) apiNavController.navigate(Routes.webviewLogin(svc, acc, region))
}, },
onWebViewLoginSuccess = { }, onWebViewLoginSuccess = { },
onStartCodexOAuth = { apiNavController.navigate(Routes.CODEX_OAUTH) } onStartCodexOAuth = { apiNavController.navigate(Routes.CODEX_OAUTH) }
@ -707,14 +717,21 @@ private fun ExpandedDetailPane(
type = NavType.StringType type = NavType.StringType
nullable = true nullable = true
defaultValue = null defaultValue = null
},
navArgument("region") {
type = NavType.StringType
nullable = true
defaultValue = null
} }
) )
) { entry -> ) { entry ->
val type = parseServiceType(entry.arguments?.getString("type")) val type = parseServiceType(entry.arguments?.getString("type"))
val accountId = entry.arguments?.getString("account") val accountId = entry.arguments?.getString("account")
val region = entry.arguments?.getString("region")
WebViewLoginScreen( WebViewLoginScreen(
service = type, service = type,
accountId = accountId, accountId = accountId,
region = region,
onBack = { apiNavController.popBackStack() }, onBack = { apiNavController.popBackStack() },
onLoginSucceeded = { _, effectiveId -> onLoginSucceeded = { _, effectiveId ->
if (!effectiveId.isNullOrBlank()) { if (!effectiveId.isNullOrBlank()) {
@ -817,8 +834,8 @@ private fun ExpandedDetailPane(
service = type, service = type,
accountId = accountId, accountId = accountId,
onBack = { settingsNavController.popBackStack() }, onBack = { settingsNavController.popBackStack() },
onStartWebViewLogin = { svc, acc -> onStartWebViewLogin = { svc, acc, region ->
settingsNavController.navigate(Routes.webviewLogin(svc, acc)) settingsNavController.navigate(Routes.webviewLogin(svc, acc, region))
}, },
onWebViewLoginSuccess = { }, onWebViewLoginSuccess = { },
onStartCodexOAuth = { settingsNavController.navigate(Routes.CODEX_OAUTH) } onStartCodexOAuth = { settingsNavController.navigate(Routes.CODEX_OAUTH) }
@ -832,14 +849,21 @@ private fun ExpandedDetailPane(
type = NavType.StringType type = NavType.StringType
nullable = true nullable = true
defaultValue = null defaultValue = null
},
navArgument("region") {
type = NavType.StringType
nullable = true
defaultValue = null
} }
) )
) { entry -> ) { entry ->
val type = parseServiceType(entry.arguments?.getString("type")) val type = parseServiceType(entry.arguments?.getString("type"))
val accountId = entry.arguments?.getString("account") val accountId = entry.arguments?.getString("account")
val region = entry.arguments?.getString("region")
WebViewLoginScreen( WebViewLoginScreen(
service = type, service = type,
accountId = accountId, accountId = accountId,
region = region,
onBack = { settingsNavController.popBackStack() }, onBack = { settingsNavController.popBackStack() },
onLoginSucceeded = { _, effectiveId -> onLoginSucceeded = { _, effectiveId ->
if (!effectiveId.isNullOrBlank()) { if (!effectiveId.isNullOrBlank()) {

View File

@ -114,7 +114,7 @@ fun ServiceDetailScreen(
onBack: () -> Unit, onBack: () -> Unit,
onSwitchAccount: (ServiceType, String?) -> Unit = { _, _ -> }, onSwitchAccount: (ServiceType, String?) -> Unit = { _, _ -> },
onConfigureCredential: (ServiceType) -> Unit, onConfigureCredential: (ServiceType) -> Unit,
onStartWebViewLogin: (ServiceType, String?) -> Unit, onStartWebViewLogin: (ServiceType, String?, String?) -> Unit,
onOpenRecharge: (ServiceType) -> Unit = { }, onOpenRecharge: (ServiceType) -> Unit = { },
viewModel: ServiceDetailViewModel = hiltViewModel() viewModel: ServiceDetailViewModel = hiltViewModel()
) { ) {
@ -315,7 +315,7 @@ fun ServiceDetailScreen(
modelsLoading = modelsLoading, modelsLoading = modelsLoading,
onRefresh = { viewModel.refresh() }, onRefresh = { viewModel.refresh() },
onConfigureCredential = { onConfigureCredential(service) }, onConfigureCredential = { onConfigureCredential(service) },
onStartWebViewLogin = { onStartWebViewLogin(service, accountId) }, onStartWebViewLogin = { onStartWebViewLogin(service, accountId, uiState.region) },
onOpenRecharge = config.externalUrl?.let { { onOpenRecharge(service) } }, onOpenRecharge = config.externalUrl?.let { { onOpenRecharge(service) } },
onTriggerUsage = { viewModel.triggerUsage() }, onTriggerUsage = { viewModel.triggerUsage() },
onSelectModel = { viewModel.selectModel(it) }, onSelectModel = { viewModel.selectModel(it) },

View File

@ -454,6 +454,13 @@ class ServiceDetailViewModel @Inject constructor(
if (serviceGen != serviceGeneration || _serviceType.value != type) return@launch if (serviceGen != serviceGeneration || _serviceType.value != type) return@launch
currentCredentialFingerprint = newFingerprint 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) { when (change) {
CredentialChange.NONE_TO_NONE, CredentialChange.UNCHANGED -> { CredentialChange.NONE_TO_NONE, CredentialChange.UNCHANGED -> {
@ -1008,7 +1015,9 @@ sealed class State {
data class ServiceDetailUiState( data class ServiceDetailUiState(
val state: State = State.Loading, val state: State = State.Loading,
val hasCredential: Boolean = false, val hasCredential: Boolean = false,
val cached: CachedBalance? = null val cached: CachedBalance? = null,
/** 当前账号区域(Trae/WorkBuddy):CN / INTL;其他服务为 null。 */
val region: String? = null
) )
sealed class TriggerState { sealed class TriggerState {

View File

@ -82,7 +82,7 @@ fun CredentialEditScreen(
service: ServiceType, service: ServiceType,
accountId: String? = null, accountId: String? = null,
onBack: () -> Unit, onBack: () -> Unit,
onStartWebViewLogin: (ServiceType, String?) -> Unit, onStartWebViewLogin: (ServiceType, String?, String?) -> Unit,
onWebViewLoginSuccess: (ServiceType) -> Unit, onWebViewLoginSuccess: (ServiceType) -> Unit,
onStartCodexOAuth: () -> Unit = {}, onStartCodexOAuth: () -> Unit = {},
viewModel: CredentialEditViewModel = hiltViewModel() viewModel: CredentialEditViewModel = hiltViewModel()
@ -159,7 +159,7 @@ fun CredentialEditScreen(
onJwtChange = viewModel::updateTraeJwt, onJwtChange = viewModel::updateTraeJwt,
onRegionChange = viewModel::updateTraeRegion, onRegionChange = viewModel::updateTraeRegion,
onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId, onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId,
onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId) }, onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId, uiState.traeRegion) },
onSave = viewModel::saveTraeCredential onSave = viewModel::saveTraeCredential
) )
} else if (service == ServiceType.WORKBUDDY) { } else if (service == ServiceType.WORKBUDDY) {
@ -171,7 +171,7 @@ fun CredentialEditScreen(
onRefreshChange = viewModel::updateWorkBuddyRefresh, onRefreshChange = viewModel::updateWorkBuddyRefresh,
region = uiState.workBuddyRegion, region = uiState.workBuddyRegion,
onRegionChange = viewModel::updateWorkBuddyRegion, onRegionChange = viewModel::updateWorkBuddyRegion,
onStartInAppLogin = { onStartWebViewLogin(ServiceType.WORKBUDDY, accountId) }, onStartInAppLogin = { onStartWebViewLogin(ServiceType.WORKBUDDY, accountId, uiState.workBuddyRegion) },
onSave = viewModel::saveWorkBuddyCredential onSave = viewModel::saveWorkBuddyCredential
) )
} else if (service == ServiceType.SUB2API) { } else if (service == ServiceType.SUB2API) {
@ -269,7 +269,7 @@ fun CredentialEditScreen(
onImportFromClipboard = { viewModel.importFromClipboard(context) }, onImportFromClipboard = { viewModel.importFromClipboard(context) },
onCopyLoginUrl = { copyToClipboard(context, uiState.loginUrl) }, onCopyLoginUrl = { copyToClipboard(context, uiState.loginUrl) },
onOpenLoginUrl = { openInBrowser(context, uiState.loginUrl) }, onOpenLoginUrl = { openInBrowser(context, uiState.loginUrl) },
onStartInAppLogin = { onStartWebViewLogin(ServiceType.OPENCODE_GO, accountId) }, onStartInAppLogin = { onStartWebViewLogin(ServiceType.OPENCODE_GO, accountId, null) },
onShowHelp = { showGoHelp = true }, onShowHelp = { showGoHelp = true },
onApiKeyChange = viewModel::updateTriggerApiKey, onApiKeyChange = viewModel::updateTriggerApiKey,
onSaveApiKey = { viewModel.saveTriggerApiKey() } onSaveApiKey = { viewModel.saveTriggerApiKey() }

View File

@ -29,7 +29,10 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -54,6 +57,7 @@ import com.rainy.token.ui.components.asString
fun WebViewLoginScreen( fun WebViewLoginScreen(
service: ServiceType, service: ServiceType,
accountId: String? = null, accountId: String? = null,
region: String? = null,
onBack: () -> Unit, onBack: () -> Unit,
onLoginSucceeded: (ServiceType, String?) -> Unit, onLoginSucceeded: (ServiceType, String?) -> Unit,
/** 用户已保存的面板基址(Sub2API 的 loginUrl 为空,靠它提供登录入口与可信 host)。 */ /** 用户已保存的面板基址(Sub2API 的 loginUrl 为空,靠它提供登录入口与可信 host)。 */
@ -62,10 +66,11 @@ fun WebViewLoginScreen(
) { ) {
// configuredBaseUrl 可选:不传时 ViewModel 自行从凭据仓库读取(Sub2API 自托管实例) // configuredBaseUrl 可选:不传时 ViewModel 自行从凭据仓库读取(Sub2API 自托管实例)
LaunchedEffect(service, configuredBaseUrl, accountId) { LaunchedEffect(service, configuredBaseUrl, accountId) {
viewModel.bind(service, configuredBaseUrl, accountId) viewModel.bind(service, configuredBaseUrl, accountId, region)
} }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
var webViewGeneration by remember { mutableStateOf(0) }
// 登录成功自动回调(携带实际写入的账号 ID,供新建账号模式回填) // 登录成功自动回调(携带实际写入的账号 ID,供新建账号模式回填)
LaunchedEffect(uiState.loginSucceeded) { LaunchedEffect(uiState.loginSucceeded) {
@ -97,6 +102,14 @@ fun WebViewLoginScreen(
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back)) 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() .fillMaxSize()
.padding(innerPadding)) { .padding(innerPadding)) {
if (uiState.loginUrl.isNotEmpty()) { if (uiState.loginUrl.isNotEmpty()) {
key(webViewGeneration) {
AndroidView( AndroidView(
factory = { context -> factory = { context ->
WebView(context).apply { WebView(context).apply {
@ -180,6 +194,7 @@ fun WebViewLoginScreen(
}, },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
}
} else { } else {
// loginUrl 为空 = ViewModel 正在生成授权地址(Trae/WorkBuddy 需要片刻) // loginUrl 为空 = ViewModel 正在生成授权地址(Trae/WorkBuddy 需要片刻)
Box( Box(

View File

@ -57,14 +57,19 @@ class WebViewLoginViewModel @Inject constructor(
/** WorkBuddy 轮询协程,bind 新服务时取消。 */ /** WorkBuddy 轮询协程,bind 新服务时取消。 */
private var workBuddyPollingJob: Job? = null private var workBuddyPollingJob: Job? = null
/** WorkBuddy 当前基址(决定 auth state/token 用 CN 还是 INTL)。 */
private var workBuddyBase = "https://copilot.tencent.com"
/** 目标账号(多账号服务);null = 新建账号。 */ /** 目标账号(多账号服务);null = 新建账号。 */
private var accountId: String? = 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?.cancel()
workBuddyPollingJob = null workBuddyPollingJob = null
this.accountId = accountId?.takeIf { it.isNotBlank() } this.accountId = accountId?.takeIf { it.isNotBlank() }
this.region = region?.takeIf { it.isNotBlank() }?.uppercase()
val config = ServiceConfigProvider.get(service) val config = ServiceConfigProvider.get(service)
_uiState.update { _uiState.update {
@ -134,8 +139,9 @@ class WebViewLoginViewModel @Inject constructor(
// 也不可用 userJwt.ClientID(固定客户端标识,所有用户相同,必被按设备限流)。 // 也不可用 userJwt.ClientID(固定客户端标识,所有用户相同,必被按设备限流)。
val deviceId = traeDeviceId() val deviceId = traeDeviceId()
val callbackUrl = "http://127.0.0.1:18080/authorize" 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 { val authUrl = buildString {
append("https://www.trae.cn/authorization?") append(authRoot).append("?")
append("login_version=1&auth_from=solo&login_channel=native_ide&") append("login_version=1&auth_from=solo&login_channel=native_ide&")
append("auth_type=local&client_id=en1oxy7wnw8j9n&redirect=0&") append("auth_type=local&client_id=en1oxy7wnw8j9n&redirect=0&")
append("login_trace_id=$traceId&") append("login_trace_id=$traceId&")
@ -221,7 +227,7 @@ class WebViewLoginViewModel @Inject constructor(
val credential = Credential.TraeCredential( val credential = Credential.TraeCredential(
service = ServiceType.TRAE, service = ServiceType.TRAE,
jwt = token, jwt = token,
region = "CN", region = if (region == "INTL") "INTL" else "CN",
deviceId = deviceId, deviceId = deviceId,
userId = userId?.takeIf { it.isNotBlank() }, userId = userId?.takeIf { it.isNotBlank() },
lastVerifiedAt = System.currentTimeMillis() lastVerifiedAt = System.currentTimeMillis()
@ -239,8 +245,9 @@ class WebViewLoginViewModel @Inject constructor(
private fun bindWorkBuddy() { private fun bindWorkBuddy() {
viewModelScope.launch { viewModelScope.launch {
try { try {
workBuddyBase = if (region == "INTL") "https://www.workbuddy.ai" else "https://copilot.tencent.com"
val request = Request.Builder() 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("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json")
.post("{}".toRequestBody("application/json".toMediaTypeOrNull())) .post("{}".toRequestBody("application/json".toMediaTypeOrNull()))
@ -285,7 +292,7 @@ class WebViewLoginViewModel @Inject constructor(
if (_uiState.value.loginSucceeded) return if (_uiState.value.loginSucceeded) return
try { try {
val request = Request.Builder() 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") .addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
.get() .get()
.build() .build()
@ -312,6 +319,7 @@ class WebViewLoginViewModel @Inject constructor(
service = ServiceType.WORKBUDDY, service = ServiceType.WORKBUDDY,
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken ?: "", refreshToken = refreshToken ?: "",
region = if (region == "INTL") "INTL" else "CN",
expiresAt = expiresAt, expiresAt = expiresAt,
lastVerifiedAt = System.currentTimeMillis() 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 ──────────────────────────────────────────────────── // ─── Page lifecycle ────────────────────────────────────────────────────
fun onPageFinished(url: String) { fun onPageFinished(url: String) {

View File

@ -447,6 +447,7 @@
<string name="error_token_refresh_generic">token 刷新失败,请重新登录或重新导入 auth.json</string> <string name="error_token_refresh_generic">token 刷新失败,请重新登录或重新导入 auth.json</string>
<!-- ══════════ WebView 登录 ══════════ --> <!-- ══════════ WebView 登录 ══════════ -->
<string name="webview_clear_login">清除登录</string>
<string name="title_webview_login">登录 %1$s</string> <string name="title_webview_login">登录 %1$s</string>
<string name="dialog_login_done_title">完成登录了吗?</string> <string name="dialog_login_done_title">完成登录了吗?</string>
<string name="dialog_login_done_body">如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。</string> <string name="dialog_login_done_body">如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。</string>

View File

@ -447,6 +447,7 @@
<string name="error_token_refresh_generic">token 重新整理失敗,請重新登入或重新匯入 auth.json</string> <string name="error_token_refresh_generic">token 重新整理失敗,請重新登入或重新匯入 auth.json</string>
<!-- ══════════ WebView 登录 ══════════ --> <!-- ══════════ WebView 登录 ══════════ -->
<string name="webview_clear_login">清除登入</string>
<string name="title_webview_login">登入 %1$s</string> <string name="title_webview_login">登入 %1$s</string>
<string name="dialog_login_done_title">完成登入了嗎?</string> <string name="dialog_login_done_title">完成登入了嗎?</string>
<string name="dialog_login_done_body">如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。</string> <string name="dialog_login_done_body">如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。</string>

View File

@ -450,6 +450,7 @@
<string name="error_token_refresh_generic">Token refresh failed. Please log in again or import auth.json.</string> <string name="error_token_refresh_generic">Token refresh failed. Please log in again or import auth.json.</string>
<!-- ══════════ WebView 登录 ══════════ --> <!-- ══════════ WebView 登录 ══════════ -->
<string name="webview_clear_login">Clear login</string>
<string name="title_webview_login">Sign in to %1$s</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_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> <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>