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"
|
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 {
|
||||||
|
|||||||
@ -90,13 +90,16 @@ 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}®ion={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) =
|
||||||
"webview_page?url=${Uri.encode(url)}&title=${Uri.encode(title)}"
|
"webview_page?url=${Uri.encode(url)}&title=${Uri.encode(title)}"
|
||||||
@ -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()) {
|
||||||
|
|||||||
@ -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) },
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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() }
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user