From f788d57798075fc24bc2f851c7d2ebf2b452624f Mon Sep 17 00:00:00 2001 From: WaterRain <109326062+CATMIAOZHI@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:02:58 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=8C=E6=AD=A5/=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=85=A8=E9=93=BE=E8=B7=AF=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=8C=96=EF=BC=88count=5Ftimes=20=E5=8D=95=E4=BD=8D=20+=20Unkn?= =?UTF-8?q?own/PartialSync/TriggerError=20=E7=BB=93=E6=9E=84=E5=8C=96?= =?UTF-8?q?=E6=98=A0=E5=B0=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../token/data/repository/CodexRepository.kt | 10 ++++- .../ui/dashboard/CommandCodeUsageStatsCard.kt | 3 +- .../token/ui/dashboard/DashboardViewModel.kt | 2 + .../token/ui/dashboard/UsageStatsCard.kt | 3 +- .../token/ui/dashboard/UsageViewModel.kt | 42 +++++++++++++++++-- .../servicedetail/ServiceDetailViewModel.kt | 13 +++--- .../ui/settings/CredentialEditViewModel.kt | 2 + app/src/main/res/values-b+zh+Hans/strings.xml | 3 +- app/src/main/res/values-b+zh+Hant/strings.xml | 3 +- app/src/main/res/values/strings.xml | 5 ++- 10 files changed, 68 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/rainy/token/data/repository/CodexRepository.kt b/app/src/main/java/com/rainy/token/data/repository/CodexRepository.kt index c775f51..a33847d 100644 --- a/app/src/main/java/com/rainy/token/data/repository/CodexRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/CodexRepository.kt @@ -252,7 +252,7 @@ class CodexRepository( } is RefreshResult.Failure -> { DebugLog.e(TAG, "triggerUsage: 二次刷新失败: ${r.reason}") - return@use Result.failure(TriggerError("token 刷新失败: ${r.reason}", "")) + return@use Result.failure(TriggerError("token 刷新失败: ${r.reason}", "", TriggerErrorReason.TOKEN_REFRESH)) } } } else { @@ -394,8 +394,14 @@ class CodexRepository( @Serializable data class OAuthRefreshResponse(@kotlinx.serialization.SerialName("access_token") val accessToken: String, @kotlinx.serialization.SerialName("refresh_token") val refreshToken: String? = null, @kotlinx.serialization.SerialName("expires_in") val expiresIn: Long = 3600, @kotlinx.serialization.SerialName("token_type") val tokenType: String? = null) } +/** TriggerError 的结构化原因(UI 层据此映射本地化文案;null = HTTP 错误,summary 为 ASCII 可直显) */ +enum class TriggerErrorReason { + /** token 刷新失败(summary 中为中文用户提示,不直接透传 UI) */ + TOKEN_REFRESH +} + /** 携带完整响应体的错误类,供 UI 展示服务端返回的详细信息 */ -class TriggerError(val summary: String, val responseBody: String) : Exception(summary) +class TriggerError(val summary: String, val responseBody: String, val reason: TriggerErrorReason? = null) : Exception(summary) /** * 将 SSE 流响应解析为简洁的文本摘要。 diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/CommandCodeUsageStatsCard.kt b/app/src/main/java/com/rainy/token/ui/dashboard/CommandCodeUsageStatsCard.kt index 22c2ffc..0c9ee7d 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/CommandCodeUsageStatsCard.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/CommandCodeUsageStatsCard.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.rainy.token.R +import com.rainy.token.ui.components.asString import com.rainy.token.data.local.OverviewStats import com.rainy.token.ui.theme.InkMuted import com.rainy.token.ui.theme.StrawberryPink @@ -164,7 +165,7 @@ fun CommandCodeUsageStatsCard( uiState.lastSyncError?.let { err -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = "✗ $err", + text = "✗ ${err.asString()}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardViewModel.kt index 4fe502b..530ac1b 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardViewModel.kt @@ -212,6 +212,8 @@ private fun errorToUiText(error: Throwable): UiText = when (error) { RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty) RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed) } + // Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案 + is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown) else -> error.message?.let { UiText.Dynamic(it) } ?: UiText.Resource(R.string.common_unknown) } diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/UsageStatsCard.kt b/app/src/main/java/com/rainy/token/ui/dashboard/UsageStatsCard.kt index 98a2436..cd088fc 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/UsageStatsCard.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/UsageStatsCard.kt @@ -31,6 +31,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.rainy.token.R import com.rainy.token.data.local.OverviewStats +import com.rainy.token.ui.components.asString import com.rainy.token.ui.theme.InkMuted import com.rainy.token.ui.theme.StrawberryPink import java.util.Locale @@ -163,7 +164,7 @@ fun UsageStatsCard( uiState.lastSyncError?.let { err -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = "✗ $err", + text = "✗ ${err.asString()}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/UsageViewModel.kt b/app/src/main/java/com/rainy/token/ui/dashboard/UsageViewModel.kt index 6cb1a70..aae4e15 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/UsageViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/UsageViewModel.kt @@ -9,10 +9,13 @@ import com.rainy.token.data.local.ModelStats import com.rainy.token.data.local.DailyStats import com.rainy.token.data.local.UsageCache import com.rainy.token.data.repository.CredentialRepository +import com.rainy.token.data.repository.RepositoryError import com.rainy.token.domain.model.Credential import com.rainy.token.domain.service.ServiceType import com.rainy.token.domain.usecase.SyncCommandCodeUsageUseCase +import com.rainy.token.domain.usecase.SyncError import com.rainy.token.domain.usecase.SyncUsageUseCase +import com.rainy.token.ui.components.UiText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -203,7 +206,7 @@ class UsageViewModel @Inject constructor( it.copy( syncing = false, lastSyncResult = result.getOrNull()?.inserted ?: 0, - lastSyncError = result.exceptionOrNull()?.message + lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) } ) } } @@ -224,7 +227,7 @@ class UsageViewModel @Inject constructor( it.copy( syncing = false, lastSyncResult = result.getOrNull()?.inserted ?: 0, - lastSyncError = result.exceptionOrNull()?.message + lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) } ) } } @@ -243,7 +246,7 @@ data class UsageUiState( val dailyStats: List = emptyList(), val recordCount: Int = 0, val lastSyncResult: Int = 0, - val lastSyncError: String? = null, + val lastSyncError: UiText? = null, val timeFilter: TimeFilter = TimeFilter.All, val dailyPage: Int = 1, val modelFilter: String? = null // null = 全部模型 @@ -255,4 +258,35 @@ private data class LoadResult( val modelStats: List, val dailyStats: List, val totalCount: Int -) \ No newline at end of file +) + +/** 把同步错误映射为本地化文案:SyncError / RepositoryError 全部走资源,不透传中文 message。 */ +private fun syncErrorToUiText(error: Throwable): UiText = when (error) { + is SyncError.PartialSync -> + UiText.Resource(R.string.sync_partial, listOf(error.inserted, error.errors.size)) + is RepositoryError.InvalidCredential -> + UiText.Resource(R.string.error_credential_invalid_reconfigure) + is RepositoryError.CredentialChanged -> + UiText.Resource(R.string.error_credential_changed_reload) + is RepositoryError.RateLimited -> UiText.Resource( + R.string.error_rate_limited_retry, + listOf( + error.retryAfterSeconds?.let { + UiText.Resource(R.string.error_rate_limited_retry_suffix, listOf(it)) + } ?: "" + ) + ) + is RepositoryError.Network -> UiText.Resource(R.string.error_network_check) + is RepositoryError.ServerError -> + UiText.Resource(R.string.error_server_http, listOf(error.code)) + is RepositoryError.ParseError -> when (error.reason) { + RepositoryError.ParseErrorReason.EMPTY_BODY -> UiText.Resource(R.string.error_parse_empty_body) + RepositoryError.ParseErrorReason.NOT_JSON_OBJECT -> UiText.Resource(R.string.error_parse_not_json) + RepositoryError.ParseErrorReason.NO_WINDOWS -> UiText.Resource(R.string.error_parse_no_windows) + RepositoryError.ParseErrorReason.NO_MODELS -> UiText.Resource(R.string.error_parse_no_models) + RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty) + RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed) + } + is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown) + else -> UiText.Resource(R.string.common_unknown) +} \ No newline at end of file diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt index 79f0da0..2c6ac2a 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt @@ -8,6 +8,7 @@ import com.rainy.token.data.debug.DebugLog import com.rainy.token.data.repository.CredentialRepository import com.rainy.token.data.repository.RepositoryError import com.rainy.token.data.repository.TriggerError +import com.rainy.token.data.repository.TriggerErrorReason import com.rainy.token.domain.model.Credential import com.rainy.token.domain.model.CredentialStatus import com.rainy.token.domain.model.ServiceBalance @@ -337,12 +338,10 @@ class ServiceDetailViewModel @Inject constructor( val responseBody: String? when (error) { is TriggerError -> { - val prefix = "token 刷新失败: " - message = if (error.summary.startsWith(prefix)) { - UiText.Resource( - R.string.error_token_refresh_failed, - listOf(error.summary.removePrefix(prefix)) - ) + // reason 结构化:token 刷新失败显示本地化文案(summary 中文提示只进日志); + // 其余(HTTP 错误)summary 为 ASCII 可直接展示 + message = if (error.reason == TriggerErrorReason.TOKEN_REFRESH) { + UiText.Resource(R.string.error_token_refresh_generic) } else { UiText.Dynamic(error.summary) } @@ -544,6 +543,8 @@ class ServiceDetailViewModel @Inject constructor( RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty) RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed) } + // Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案 + is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown) else -> error.message?.let { UiText.Dynamic(it) } ?: UiText.Resource(R.string.common_unknown) } diff --git a/app/src/main/java/com/rainy/token/ui/settings/CredentialEditViewModel.kt b/app/src/main/java/com/rainy/token/ui/settings/CredentialEditViewModel.kt index e0811ef..4e7fad4 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/CredentialEditViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/CredentialEditViewModel.kt @@ -185,6 +185,8 @@ class CredentialEditViewModel @Inject constructor( RepositoryError.ParseErrorReason.MODELS_EMPTY -> UiText.Resource(R.string.error_parse_models_empty) RepositoryError.ParseErrorReason.MALFORMED_RESPONSE -> UiText.Resource(R.string.error_parse_malformed) } + // Unknown 的 message 以硬编码中文"未知错误"开头,不能透传 UI,统一映射本地化文案 + is RepositoryError.Unknown -> UiText.Resource(R.string.common_unknown) else -> err?.message?.let { UiText.Dynamic(it) } ?: UiText.Resource(R.string.common_unknown) } diff --git a/app/src/main/res/values-b+zh+Hans/strings.xml b/app/src/main/res/values-b+zh+Hans/strings.xml index 52383f4..8579044 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -139,6 +139,7 @@ 缓存写入 %1$s 暂无数据,点击 🔄 同步 ✓ 新增 %1$d 条 + 部分同步完成:插入 %1$d条,%2$d页失败 点击切换 所有时间按 UTC+0 计算 已切换到 UTC+8,图表按北京时间显示 @@ -273,7 +274,7 @@ (响应解析失败,但请求已发出) 用量: prompt=%1$s completion=%2$s total=%3$s tokens 用量: input=%1$s output=%2$s tokens - token 刷新失败:%1$s + token 刷新失败,请重新登录或重新导入 auth.json 登录 %1$s diff --git a/app/src/main/res/values-b+zh+Hant/strings.xml b/app/src/main/res/values-b+zh+Hant/strings.xml index 12821b7..fcdde38 100644 --- a/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/app/src/main/res/values-b+zh+Hant/strings.xml @@ -139,6 +139,7 @@ 快取寫入 %1$s 暫無數據,點選 🔄 同步 ✓ 新增 %1$d 條 + 部分同步完成:插入 %1$d條,%2$d頁失敗 點選切換 所有時間按 UTC+0 計算 已切換到 UTC+8,圖表按北京時間顯示 @@ -273,7 +274,7 @@ (響應解析失敗,但請求已發出) 用量: prompt=%1$s completion=%2$s total=%3$s tokens 用量: input=%1$s output=%2$s tokens - token 重新整理失敗:%1$s + token 重新整理失敗,請重新登入或重新匯入 auth.json 登入 %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d82a0a..e32c4bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,7 @@ Cache write %1$s No data yet · tap 🔄 to sync ✓ %1$d new records + Partial sync: %1$d records inserted, %2$d pages failed tap to switch All times in UTC+0 Switched to UTC+8, charts shown in Beijing time @@ -276,7 +277,7 @@ (response parsing failed, but the request was sent) Usage: prompt=%1$s completion=%2$s total=%3$s tokens Usage: input=%1$s output=%2$s tokens - Token refresh failed: %1$s + Token refresh failed. Please log in again or import auth.json. Sign in to %1$s @@ -338,7 +339,7 @@ The widget refreshes balances when you swipe to it, or tap the ↻ button manually. - %1$d + %1$d calls Pick a date Pick a month (any day) Start date