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"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 34
|
||||
versionName = "1.7.14"
|
||||
versionCode = 35
|
||||
versionName = "1.7.15"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -190,8 +190,9 @@ fun WebViewLoginScreen(
|
||||
}
|
||||
|
||||
fun handleOverrideUrl(url: String): Boolean {
|
||||
// Trae OAuth 回调:拦截 127.0.0.1:18080/authorize,提取 userJwt
|
||||
if (url.startsWith("http://127.0.0.1:18080/authorize")) {
|
||||
// Trae OAuth 回调:拦截 127.0.0.1/localhost:18080/authorize,提取 userJwt
|
||||
// (部分流程会从 https 跳到 http,或把参数放 fragment)
|
||||
if (isTraeLoginCallback(url)) {
|
||||
DebugLog.i("TraeLogin", "回调命中: ${url.toSafeLogUrl()}")
|
||||
viewModel.onTraeCallback(url)
|
||||
return true
|
||||
@ -199,10 +200,24 @@ fun WebViewLoginScreen(
|
||||
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?) {
|
||||
pageLoading = true
|
||||
pageError = null
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -191,17 +191,31 @@ class WebViewLoginViewModel @Inject constructor(
|
||||
if (service != ServiceType.TRAE) 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 {
|
||||
DebugLog.w("TraeLogin", "onTraeCallback: URL 解析失败")
|
||||
return
|
||||
}
|
||||
// 只记录参数名(不含 token 值,避免把 token 明文落调试日志)
|
||||
DebugLog.i("TraeLogin", "onTraeCallback: 收到回调,参数=${parsed.queryParameterNames}")
|
||||
// 只记录参数名(不含 token 值,避免把 token 明文落调试日志);query+both
|
||||
DebugLog.i("TraeLogin", "onTraeCallback: 收到回调,query=${callbackUri.query?.split("&")?.map { it.substringBefore("=") } ?: emptyList()} fragmentKeys=${callbackUri.fragment?.split("&")?.map { it.substringBefore("=") } ?: emptyList()}")
|
||||
// Trae 回调可能走两套:
|
||||
// 1) userJwt/user_jwt 直接带 Token+RefreshToken(正常流程)
|
||||
// 2) 只带 refreshToken(老流程),需要调用 ExchangeToken 兑换 Cloud-IDE-JWT
|
||||
val userJwtRaw = parsed.queryParameter("userJwt")
|
||||
?: parsed.queryParameter("user_jwt")
|
||||
val userJwtRaw = param("userJwt")
|
||||
?: param("user_jwt")
|
||||
?: ""
|
||||
viewModelScope.launch {
|
||||
val userJwt = runCatching {
|
||||
@ -214,18 +228,18 @@ class WebViewLoginViewModel @Inject constructor(
|
||||
?: userJwt["accessToken"]?.jsonPrimitive?.contentOrNull
|
||||
var refresh = userJwt["RefreshToken"]?.jsonPrimitive?.contentOrNull
|
||||
?: userJwt["refreshToken"]?.jsonPrimitive?.contentOrNull
|
||||
?: parsed.queryParameter("refreshToken")
|
||||
?: parsed.queryParameter("refresh_token")
|
||||
?: parsed.queryParameter("data")
|
||||
?: param("refreshToken")
|
||||
?: param("refresh_token")
|
||||
?: param("data")
|
||||
val clientId = userJwt["ClientID"]?.jsonPrimitive?.contentOrNull
|
||||
?: userJwt["clientId"]?.jsonPrimitive?.contentOrNull
|
||||
?: parsed.queryParameter("clientId")
|
||||
?: parsed.queryParameter("client_id")
|
||||
?: param("clientId")
|
||||
?: param("client_id")
|
||||
?: "en1oxy7wnw8j9n"
|
||||
|
||||
// 老流程:回调只有 refreshToken,先兑换 Cloud-IDE-JWT
|
||||
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"
|
||||
val exchanged = exchangeTraeRefreshToken(refresh, clientId, exchangeHost)
|
||||
if (exchanged != null) {
|
||||
@ -248,7 +262,7 @@ class WebViewLoginViewModel @Inject constructor(
|
||||
// 设备绑定 ID:登录会话绑定的 App 持久化设备 ID(与授权 URL device_id 一致)。
|
||||
val deviceId = traeDeviceId()
|
||||
// 用户 ID:从回调 userInfo 参数提取,签到按 trae-check 口径拼 ${deviceId}-${userId}。
|
||||
val parsedUserId = parsed.queryParameter("userInfo")?.let { raw ->
|
||||
val parsedUserId = param("userInfo")?.let { raw ->
|
||||
runCatching {
|
||||
val u = Json.parseToJsonElement(raw).jsonObject
|
||||
u["userId"]?.jsonPrimitive?.contentOrNull
|
||||
|
||||
Loading…
Reference in New Issue
Block a user