fix(trae): 回调兼容 localhost/fragment,pageStarted 兜底拦截
- 识别任意 127.0.0.1/localhost:18080/authorize 回调(http/https、query/fragment) - shouldOverrideUrlLoading 未触发时在 onPageStarted 拦截并停止加载,避免卡 Authenticating
This commit is contained in:
parent
101e2bc240
commit
68cdbcf0b0
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 34
|
versionCode = 35
|
||||||
versionName = "1.7.14"
|
versionName = "1.7.15"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -190,8 +190,9 @@ fun WebViewLoginScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleOverrideUrl(url: String): Boolean {
|
fun handleOverrideUrl(url: String): Boolean {
|
||||||
// Trae OAuth 回调:拦截 127.0.0.1:18080/authorize,提取 userJwt
|
// Trae OAuth 回调:拦截 127.0.0.1/localhost:18080/authorize,提取 userJwt
|
||||||
if (url.startsWith("http://127.0.0.1:18080/authorize")) {
|
// (部分流程会从 https 跳到 http,或把参数放 fragment)
|
||||||
|
if (isTraeLoginCallback(url)) {
|
||||||
DebugLog.i("TraeLogin", "回调命中: ${url.toSafeLogUrl()}")
|
DebugLog.i("TraeLogin", "回调命中: ${url.toSafeLogUrl()}")
|
||||||
viewModel.onTraeCallback(url)
|
viewModel.onTraeCallback(url)
|
||||||
return true
|
return true
|
||||||
@ -199,10 +200,24 @@ fun WebViewLoginScreen(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isTraeLoginCallback(url: String): Boolean {
|
||||||
|
val u = runCatching { android.net.Uri.parse(url) }.getOrNull()
|
||||||
|
return u?.host in setOf("127.0.0.1", "localhost") &&
|
||||||
|
u?.port == 18080 &&
|
||||||
|
u?.path == "/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
|
||||||
pageLoading = true
|
pageLoading = true
|
||||||
pageError = null
|
pageError = null
|
||||||
DebugLog.i("TraeLogin", "pageStarted: ${url?.toSafeLogUrl()}")
|
DebugLog.i("TraeLogin", "pageStarted: ${url?.toSafeLogUrl()}")
|
||||||
|
// 保险:某些重定向不一定走 shouldOverrideUrlLoading,
|
||||||
|
// 在 pageStarted 也拦一次回调地址,避免页面停在 Authenticating。
|
||||||
|
if (url != null && isTraeLoginCallback(url)) {
|
||||||
|
viewModel.onTraeCallback(url)
|
||||||
|
view?.stopLoading()
|
||||||
|
return
|
||||||
|
}
|
||||||
super.onPageStarted(view, url, favicon)
|
super.onPageStarted(view, url, favicon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -191,17 +191,31 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
if (service != ServiceType.TRAE) return
|
if (service != ServiceType.TRAE) return
|
||||||
if (_uiState.value.loginSucceeded) return
|
if (_uiState.value.loginSucceeded) return
|
||||||
|
|
||||||
|
val callbackUri = android.net.Uri.parse(url)
|
||||||
|
// 参数同时支持 query 和 fragment(部分 OAuth 回调用 # 而非 ?)
|
||||||
|
fun param(name: String): String? {
|
||||||
|
callbackUri.getQueryParameter(name)?.let { return it }
|
||||||
|
val frag = callbackUri.fragment
|
||||||
|
if (!frag.isNullOrBlank()) {
|
||||||
|
frag.split("&").forEach { pair ->
|
||||||
|
val kv = pair.split("=", limit = 2)
|
||||||
|
if (kv.size == 2 && kv[0] == name) return android.net.Uri.decode(kv[1])
|
||||||
|
if (kv.size == 1 && kv[0] == name) return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
val parsed = url.toHttpUrlOrNull() ?: run {
|
val parsed = url.toHttpUrlOrNull() ?: run {
|
||||||
DebugLog.w("TraeLogin", "onTraeCallback: URL 解析失败")
|
DebugLog.w("TraeLogin", "onTraeCallback: URL 解析失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 只记录参数名(不含 token 值,避免把 token 明文落调试日志)
|
// 只记录参数名(不含 token 值,避免把 token 明文落调试日志);query+both
|
||||||
DebugLog.i("TraeLogin", "onTraeCallback: 收到回调,参数=${parsed.queryParameterNames}")
|
DebugLog.i("TraeLogin", "onTraeCallback: 收到回调,query=${callbackUri.query?.split("&")?.map { it.substringBefore("=") } ?: emptyList()} fragmentKeys=${callbackUri.fragment?.split("&")?.map { it.substringBefore("=") } ?: emptyList()}")
|
||||||
// Trae 回调可能走两套:
|
// Trae 回调可能走两套:
|
||||||
// 1) userJwt/user_jwt 直接带 Token+RefreshToken(正常流程)
|
// 1) userJwt/user_jwt 直接带 Token+RefreshToken(正常流程)
|
||||||
// 2) 只带 refreshToken(老流程),需要调用 ExchangeToken 兑换 Cloud-IDE-JWT
|
// 2) 只带 refreshToken(老流程),需要调用 ExchangeToken 兑换 Cloud-IDE-JWT
|
||||||
val userJwtRaw = parsed.queryParameter("userJwt")
|
val userJwtRaw = param("userJwt")
|
||||||
?: parsed.queryParameter("user_jwt")
|
?: param("user_jwt")
|
||||||
?: ""
|
?: ""
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val userJwt = runCatching {
|
val userJwt = runCatching {
|
||||||
@ -214,18 +228,18 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
?: userJwt["accessToken"]?.jsonPrimitive?.contentOrNull
|
?: userJwt["accessToken"]?.jsonPrimitive?.contentOrNull
|
||||||
var refresh = userJwt["RefreshToken"]?.jsonPrimitive?.contentOrNull
|
var refresh = userJwt["RefreshToken"]?.jsonPrimitive?.contentOrNull
|
||||||
?: userJwt["refreshToken"]?.jsonPrimitive?.contentOrNull
|
?: userJwt["refreshToken"]?.jsonPrimitive?.contentOrNull
|
||||||
?: parsed.queryParameter("refreshToken")
|
?: param("refreshToken")
|
||||||
?: parsed.queryParameter("refresh_token")
|
?: param("refresh_token")
|
||||||
?: parsed.queryParameter("data")
|
?: param("data")
|
||||||
val clientId = userJwt["ClientID"]?.jsonPrimitive?.contentOrNull
|
val clientId = userJwt["ClientID"]?.jsonPrimitive?.contentOrNull
|
||||||
?: userJwt["clientId"]?.jsonPrimitive?.contentOrNull
|
?: userJwt["clientId"]?.jsonPrimitive?.contentOrNull
|
||||||
?: parsed.queryParameter("clientId")
|
?: param("clientId")
|
||||||
?: parsed.queryParameter("client_id")
|
?: param("client_id")
|
||||||
?: "en1oxy7wnw8j9n"
|
?: "en1oxy7wnw8j9n"
|
||||||
|
|
||||||
// 老流程:回调只有 refreshToken,先兑换 Cloud-IDE-JWT
|
// 老流程:回调只有 refreshToken,先兑换 Cloud-IDE-JWT
|
||||||
if (token.isNullOrBlank() && !refresh.isNullOrBlank()) {
|
if (token.isNullOrBlank() && !refresh.isNullOrBlank()) {
|
||||||
val exchangeHost = parsed.queryParameter("host")?.takeIf { it.startsWith("http") }
|
val exchangeHost = param("host")?.takeIf { it.startsWith("http") }
|
||||||
?: if (region == "INTL") "https://a0ai-api-sg.byteintlapi.com" else "https://api.trae.cn"
|
?: if (region == "INTL") "https://a0ai-api-sg.byteintlapi.com" else "https://api.trae.cn"
|
||||||
val exchanged = exchangeTraeRefreshToken(refresh, clientId, exchangeHost)
|
val exchanged = exchangeTraeRefreshToken(refresh, clientId, exchangeHost)
|
||||||
if (exchanged != null) {
|
if (exchanged != null) {
|
||||||
@ -248,7 +262,7 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
// 设备绑定 ID:登录会话绑定的 App 持久化设备 ID(与授权 URL device_id 一致)。
|
// 设备绑定 ID:登录会话绑定的 App 持久化设备 ID(与授权 URL device_id 一致)。
|
||||||
val deviceId = traeDeviceId()
|
val deviceId = traeDeviceId()
|
||||||
// 用户 ID:从回调 userInfo 参数提取,签到按 trae-check 口径拼 ${deviceId}-${userId}。
|
// 用户 ID:从回调 userInfo 参数提取,签到按 trae-check 口径拼 ${deviceId}-${userId}。
|
||||||
val parsedUserId = parsed.queryParameter("userInfo")?.let { raw ->
|
val parsedUserId = param("userInfo")?.let { raw ->
|
||||||
runCatching {
|
runCatching {
|
||||||
val u = Json.parseToJsonElement(raw).jsonObject
|
val u = Json.parseToJsonElement(raw).jsonObject
|
||||||
u["userId"]?.jsonPrimitive?.contentOrNull
|
u["userId"]?.jsonPrimitive?.contentOrNull
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user