fix(proxy): INTL 401 自动 refresh token 重试;Trae 重复签到文案判定为已签
Some checks are pending
Release / build (push) Waiting to run
Some checks are pending
Release / build (push) Waiting to run
This commit is contained in:
parent
7ecb7b3246
commit
1a0bb2b3fd
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 45
|
versionCode = 46
|
||||||
versionName = "1.7.25"
|
versionName = "1.7.26"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.rainy.token.data.proxy
|
package com.rainy.token.data.proxy
|
||||||
|
|
||||||
import com.rainy.token.data.repository.CredentialRepository
|
import com.rainy.token.data.repository.CredentialRepository
|
||||||
|
import com.rainy.token.data.repository.WorkBuddyRepository
|
||||||
import com.rainy.token.domain.model.Credential
|
import com.rainy.token.domain.model.Credential
|
||||||
import com.rainy.token.domain.service.ServiceType
|
import com.rainy.token.domain.service.ServiceType
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -28,6 +29,7 @@ import kotlinx.serialization.json.contentOrNull
|
|||||||
class WorkBuddyChatProxy @Inject constructor(
|
class WorkBuddyChatProxy @Inject constructor(
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val credentialRepository: CredentialRepository,
|
private val credentialRepository: CredentialRepository,
|
||||||
|
private val workBuddyRepository: WorkBuddyRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun forwardChat(
|
suspend fun forwardChat(
|
||||||
@ -53,23 +55,39 @@ class WorkBuddyChatProxy @Inject constructor(
|
|||||||
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
||||||
val intl = effectiveRegion == ProviderRegion.INTL
|
val intl = effectiveRegion == ProviderRegion.INTL
|
||||||
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
||||||
|
var current = credential
|
||||||
|
var token = accessToken
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
|
var attempt = 0
|
||||||
val response = try {
|
while (true) {
|
||||||
okHttpClient.newCall(request).execute()
|
val request = buildChatRequest(base, path, upstreamBody, token, intl)
|
||||||
} catch (e: java.io.IOException) {
|
val response = try {
|
||||||
throw e
|
okHttpClient.newCall(request).execute()
|
||||||
}
|
} catch (e: java.io.IOException) {
|
||||||
val status = response.code
|
throw e
|
||||||
// 国际站 console 路由返回 404/405 时再退回共享 v2 路径
|
}
|
||||||
if (status == 404 || status == 405) {
|
val status = response.code
|
||||||
|
// 国际站 console 路由返回 404/405 时再退回共享 v2 路径
|
||||||
|
if (status == 404 || status == 405) {
|
||||||
|
response.close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ((status == 401 || status == 403) && attempt == 0) {
|
||||||
|
response.close()
|
||||||
|
val refreshed = workBuddyRepository.refreshCredential(current)
|
||||||
|
if (refreshed != null) {
|
||||||
|
credentialRepository.save(refreshed, accountId)
|
||||||
|
current = refreshed
|
||||||
|
token = refreshed.accessToken
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bytes = try { response.body?.bytes() ?: ByteArray(0) } catch (_: Throwable) { ByteArray(0) }
|
||||||
|
val contentType = response.header("Content-Type") ?: "application/json"
|
||||||
response.close()
|
response.close()
|
||||||
continue
|
return@withContext ProxyUpstreamResponse(status, contentType, bytes)
|
||||||
}
|
}
|
||||||
val bytes = try { response.body?.bytes() ?: ByteArray(0) } catch (_: Throwable) { ByteArray(0) }
|
|
||||||
val contentType = response.header("Content-Type") ?: "application/json"
|
|
||||||
response.close()
|
|
||||||
return@withContext ProxyUpstreamResponse(status, contentType, bytes)
|
|
||||||
}
|
}
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
@ -95,27 +113,43 @@ class WorkBuddyChatProxy @Inject constructor(
|
|||||||
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
||||||
val intl = effectiveRegion == ProviderRegion.INTL
|
val intl = effectiveRegion == ProviderRegion.INTL
|
||||||
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
||||||
|
var current = credential
|
||||||
|
var token = accessToken
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
|
var attempt = 0
|
||||||
val response = try {
|
while (true) {
|
||||||
okHttpClient.newCall(request).execute()
|
val request = buildChatRequest(base, path, upstreamBody, token, intl)
|
||||||
} catch (e: java.io.IOException) {
|
val response = try {
|
||||||
throw e
|
okHttpClient.newCall(request).execute()
|
||||||
|
} catch (e: java.io.IOException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (response.code == 404 || response.code == 405) {
|
||||||
|
response.close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ((response.code == 401 || response.code == 403) && attempt == 0) {
|
||||||
|
response.close()
|
||||||
|
val refreshed = workBuddyRepository.refreshCredential(current)
|
||||||
|
if (refreshed != null) {
|
||||||
|
credentialRepository.save(refreshed, accountId)
|
||||||
|
current = refreshed
|
||||||
|
token = refreshed.accessToken
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val input = response.body?.byteStream() ?: run {
|
||||||
|
response.close()
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
return@withContext ProxyUpstreamStream(
|
||||||
|
status = response.code,
|
||||||
|
contentType = response.header("Content-Type") ?: "text/event-stream",
|
||||||
|
input = input,
|
||||||
|
close = { response.close() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (response.code == 404 || response.code == 405) {
|
|
||||||
response.close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val input = response.body?.byteStream() ?: run {
|
|
||||||
response.close()
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
return@withContext ProxyUpstreamStream(
|
|
||||||
status = response.code,
|
|
||||||
contentType = response.header("Content-Type") ?: "text/event-stream",
|
|
||||||
input = input,
|
|
||||||
close = { response.close() },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -719,9 +719,11 @@ class TraeRepository constructor(
|
|||||||
// 权威口径幂等兜底码是 10001(trae-checkin-helper 逆向);历史实现用 1001,
|
// 权威口径幂等兜底码是 10001(trae-checkin-helper 逆向);历史实现用 1001,
|
||||||
// 两者都认,避免把已签误判为 FAILED 硬错误。
|
// 两者都认,避免把已签误判为 FAILED 硬错误。
|
||||||
val already = (code == 1001L || code == 10001L) || (!deviceBlock && (
|
val already = (code == 1001L || code == 10001L) || (!deviceBlock && (
|
||||||
msg.contains("已签到") || msg.contains("已经签到") || msg.contains("明日再来") ||
|
msg.contains("已签到") || msg.contains("已经签到") || msg.contains("重复签到") ||
|
||||||
msg.contains("今日已完成") || msg.contains("已领取") ||
|
msg.contains("重复") || msg.contains("不能重复") ||
|
||||||
lowerMsg.contains("already") || lowerMsg.contains("checked") || lowerMsg.contains("claimed")
|
msg.contains("明日再来") || msg.contains("今日已完成") || msg.contains("已领取") ||
|
||||||
|
lowerMsg.contains("already") || lowerMsg.contains("checked") || lowerMsg.contains("claimed") ||
|
||||||
|
lowerMsg.contains("duplicate") || lowerMsg.contains("repeat")
|
||||||
))
|
))
|
||||||
// 服务端临时限流/排队(账号、设备或活动人数限制):提示可稍后重试,不算硬错误。
|
// 服务端临时限流/排队(账号、设备或活动人数限制):提示可稍后重试,不算硬错误。
|
||||||
val retryable = !already && !deviceBlock && (
|
val retryable = !already && !deviceBlock && (
|
||||||
|
|||||||
@ -253,6 +253,21 @@ class WorkBuddyRepository constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 供 Chat 代理等非余额路径使用的 token 刷新入口:成功返回新凭据(由调用方落库)。 */
|
||||||
|
fun refreshCredential(credential: Credential.WorkBuddyCredential): Credential.WorkBuddyCredential? {
|
||||||
|
val refreshed = tryRefesh(credential, credential.refreshToken) ?: return null
|
||||||
|
return credential.copy(
|
||||||
|
accessToken = refreshed.accessToken,
|
||||||
|
refreshToken = refreshed.refreshToken,
|
||||||
|
expiresAt = if (refreshed.expiresIn > 0L) {
|
||||||
|
System.currentTimeMillis() + refreshed.expiresIn * 1000L
|
||||||
|
} else {
|
||||||
|
credential.expiresAt
|
||||||
|
},
|
||||||
|
lastVerifiedAt = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun JsonObject.readToken(vararg keys: String): String? {
|
private fun JsonObject.readToken(vararg keys: String): String? {
|
||||||
for (key in keys) {
|
for (key in keys) {
|
||||||
this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it }
|
this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
|||||||
@ -245,8 +245,9 @@ object NetworkModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideWorkBuddyChatProxy(
|
fun provideWorkBuddyChatProxy(
|
||||||
okHttpClient: OkHttpClient,
|
okHttpClient: OkHttpClient,
|
||||||
credentialRepository: CredentialRepository
|
credentialRepository: CredentialRepository,
|
||||||
): WorkBuddyChatProxy = WorkBuddyChatProxy(okHttpClient, credentialRepository)
|
workBuddyRepository: WorkBuddyRepository,
|
||||||
|
): WorkBuddyChatProxy = WorkBuddyChatProxy(okHttpClient, credentialRepository, workBuddyRepository)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user