diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43f4556..a92b19d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/rainy/token/data/proxy/WorkBuddyChatProxy.kt b/app/src/main/java/com/rainy/token/data/proxy/WorkBuddyChatProxy.kt index 70ba638..705c263 100644 --- a/app/src/main/java/com/rainy/token/data/proxy/WorkBuddyChatProxy.kt +++ b/app/src/main/java/com/rainy/token/data/proxy/WorkBuddyChatProxy.kt @@ -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,23 +55,39 @@ 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) - val response = try { - okHttpClient.newCall(request).execute() - } catch (e: java.io.IOException) { - throw e - } - val status = response.code - // 国际站 console 路由返回 404/405 时再退回共享 v2 路径 - if (status == 404 || status == 405) { + 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) { + throw e + } + 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() - 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 } @@ -95,27 +113,43 @@ 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) - val response = try { - okHttpClient.newCall(request).execute() - } catch (e: java.io.IOException) { - throw e + 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) { + 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 } diff --git a/app/src/main/java/com/rainy/token/data/repository/TraeRepository.kt b/app/src/main/java/com/rainy/token/data/repository/TraeRepository.kt index 9b6c909..ccdb8c4 100644 --- a/app/src/main/java/com/rainy/token/data/repository/TraeRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/TraeRepository.kt @@ -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 && ( diff --git a/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt b/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt index bd04cba..54ad4d6 100644 --- a/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/WorkBuddyRepository.kt @@ -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 } diff --git a/app/src/main/java/com/rainy/token/di/NetworkModule.kt b/app/src/main/java/com/rainy/token/di/NetworkModule.kt index 4f9219d..73aa42c 100644 --- a/app/src/main/java/com/rainy/token/di/NetworkModule.kt +++ b/app/src/main/java/com/rainy/token/di/NetworkModule.kt @@ -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