fix(proxy): INTL 401 自动 refresh token 重试;Trae 重复签到文案判定为已签

This commit is contained in:
Liuxinyu176 2026-10-10 12:18:02 +08:00
parent 18404d63e7
commit 83dbf9e6ad
5 changed files with 92 additions and 40 deletions

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.rainy.token"
minSdk = 31
targetSdk = 35
versionCode = 45
versionName = "1.7.25"
versionCode = 46
versionName = "1.7.26"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -1,6 +1,7 @@
package com.rainy.token.data.proxy
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.service.ServiceType
import java.util.UUID
@ -28,6 +29,7 @@ import kotlinx.serialization.json.contentOrNull
class WorkBuddyChatProxy @Inject constructor(
private val okHttpClient: OkHttpClient,
private val credentialRepository: CredentialRepository,
private val workBuddyRepository: WorkBuddyRepository,
) {
suspend fun forwardChat(
@ -53,8 +55,12 @@ class WorkBuddyChatProxy @Inject constructor(
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
val intl = effectiveRegion == ProviderRegion.INTL
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) {
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
var attempt = 0
while (true) {
val request = buildChatRequest(base, path, upstreamBody, token, intl)
val response = try {
okHttpClient.newCall(request).execute()
} catch (e: java.io.IOException) {
@ -64,13 +70,25 @@ class WorkBuddyChatProxy @Inject constructor(
// 国际站 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()
return@withContext ProxyUpstreamResponse(status, contentType, bytes)
}
}
return@withContext null
}
suspend fun openStreamingChat(
@ -95,8 +113,12 @@ class WorkBuddyChatProxy @Inject constructor(
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
val intl = effectiveRegion == ProviderRegion.INTL
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) {
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
var attempt = 0
while (true) {
val request = buildChatRequest(base, path, upstreamBody, token, intl)
val response = try {
okHttpClient.newCall(request).execute()
} catch (e: java.io.IOException) {
@ -104,8 +126,19 @@ class WorkBuddyChatProxy @Inject constructor(
}
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
@ -117,6 +150,7 @@ class WorkBuddyChatProxy @Inject constructor(
close = { response.close() },
)
}
}
return@withContext null
}

View File

@ -719,9 +719,11 @@ class TraeRepository constructor(
// 权威口径幂等兜底码是 10001(trae-checkin-helper 逆向);历史实现用 1001,
// 两者都认,避免把已签误判为 FAILED 硬错误。
val already = (code == 1001L || code == 10001L) || (!deviceBlock && (
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("重复签到") ||
msg.contains("重复") || msg.contains("不能重复") ||
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 && (

View File

@ -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? {
for (key in keys) {
this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it }

View File

@ -245,8 +245,9 @@ object NetworkModule {
@Singleton
fun provideWorkBuddyChatProxy(
okHttpClient: OkHttpClient,
credentialRepository: CredentialRepository
): WorkBuddyChatProxy = WorkBuddyChatProxy(okHttpClient, credentialRepository)
credentialRepository: CredentialRepository,
workBuddyRepository: WorkBuddyRepository,
): WorkBuddyChatProxy = WorkBuddyChatProxy(okHttpClient, credentialRepository, workBuddyRepository)
@Provides
@Singleton