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 37a5ede..973ef7e 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 @@ -195,13 +195,32 @@ class TraeRepository constructor( // ---------- 每日签到(checkin_credits)---------- + /** 签到动作语义结果(UI 层据此本地化,不直接展示 Repository 中文串)。 */ + enum class CheckinOutcome { + /** 签到成功 */ + SUCCESS, + /** 今日已签到(账号级,幂等) */ + ALREADY, + /** 设备级拦截(提示而非报错) */ + DEVICE_BLOCKED, + /** INTL 区签到接口未实测,暂不支持 */ + INTL_UNSUPPORTED, + /** 其他失败(服务端给了原因/错误码) */ + FAILED + } + /** 签到结果(来源:trae-check 实测字段与语义)。 */ data class CheckinResult( val success: Boolean, /** 今日已签(账号级,非设备拦截) */ val alreadyChecked: Boolean, val points: Long, - val message: String + /** 服务端原始提示文案(仅作 [outcome] 之外的补充透传) */ + val message: String, + /** 动作语义结果:UI 按此本地化,不依赖 [message] 语言 */ + val outcome: CheckinOutcome, + /** 服务端错误码(outcome=FAILED 时可能有;-1 表示未提供) */ + val code: Long = -1L ) /** @@ -222,7 +241,13 @@ class TraeRepository constructor( val regionKey = credential.region.uppercase().takeIf { it == "INTL" } ?: "CN" if (regionKey == "INTL") { return@withContext Result.success( - CheckinResult(false, false, 0, "INTL 区签到接口暂未支持,请用 CN 区账号") + CheckinResult( + success = false, + alreadyChecked = false, + points = 0, + message = "", + outcome = CheckinOutcome.INTL_UNSUPPORTED + ) ) } val request = Request.Builder() @@ -251,35 +276,16 @@ class TraeRepository constructor( return@withContext Result.failure(RepositoryError.Unknown(e)) } - parseCheckin(body) - } - - /** 解析 claim 响应(code/message/points 多候选;语义判断见 checkIn 注释)。 */ - private fun parseCheckin(body: String): Result = runCatching { - val root = json.parseToJsonElement(body).jsonObject - val data = root["data"]?.jsonObject ?: root - val code = data.numberOr("code")?.toLong() ?: root.numberOr("code")?.toLong() ?: -1L - val msg = data.scalarOr("message", "msg") ?: root.scalarOr("message", "msg") ?: "" - val points = (data.numberOr("points") ?: root.numberOr("points"))?.toLong() ?: 0L - - val succeeded = code == 0L || code == 200L - val lowerMsg = msg.lowercase() - val deviceBlock = msg.contains("设备") || lowerMsg.contains("device") || lowerMsg.contains("machine") - val already = code == 1001L || (!deviceBlock && ( - msg.contains("已签到") || msg.contains("已经签到") || msg.contains("明日再来") || - msg.contains("今日已完成") || msg.contains("已领取") || - lowerMsg.contains("already") || lowerMsg.contains("checked") || lowerMsg.contains("claimed") - )) - - when { - succeeded -> Result.success( - CheckinResult(true, false, points.takeIf { it > 0 } ?: 200L, msg.ifBlank { "签到成功" }) - ) - already -> Result.success(CheckinResult(true, true, 0L, "今日已签到")) - deviceBlock -> Result.success(CheckinResult(false, false, 0L, msg.ifBlank { "该设备今日签到次数已达上限" })) - else -> Result.success(CheckinResult(false, false, 0L, msg.ifBlank { "签到失败(code=" + code + ")" })) + // 仅「凭据被服务端接受」时才算已验证:SUCCESS/ALREADY/DEVICE_BLOCKED 都说明 + // JWT 有效(401/403 已在上面提前返回);FAILED(含 HTTP 400 业务失败)不标记, + // 避免把无效凭据误标为已验证。 + val parsed = parseCheckin(body) + val outcome = parsed.getOrNull()?.outcome + if (outcome != null && outcome != CheckinOutcome.FAILED) { + credentialRepository.save(credential.copy(lastVerifiedAt = System.currentTimeMillis())) } - }.getOrElse { Result.failure(RepositoryError.Unknown(it)) } + parsed + } /** 由 JWT 派生稳定伪 deviceId:同账号恒定、不同账号不同(防 claim 设备级去重误伤)。 */ private fun stableDeviceId(seed: String): String = runCatching { @@ -288,13 +294,6 @@ class TraeRepository constructor( digest.take(12).joinToString("") { "%02x".format(it) } }.getOrDefault("rainytoken-device") - private fun JsonObject.scalarOr(vararg keys: String): String? { - for (key in keys) { - this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it } - } - return null - } - private fun mapHttpError(code: Int): RepositoryError = when (code) { 401, 403 -> RepositoryError.InvalidCredential() 429 -> RepositoryError.RateLimited() @@ -315,4 +314,75 @@ class TraeRepository constructor( val totalQuota: Double?, val extras: Map ) + + companion object { + /** 解析专用 Json(companion 内独立持有,使 parseCheckin 可静态调用/单测)。 */ + private val parseJson = Json { ignoreUnknownKeys = true } + + /** + * 解析 claim 响应(code/message/points 多候选)。 + * + * 语义判定(实测口径): + * - code 0/200 → SUCCESS;code 1001 或文案含「已签到/already/claimed」→ ALREADY; + * - 文案含「设备/device/machine」→ DEVICE_BLOCKED(设备级拦截,优先级高于 already,避免误报已签); + * - 其余 → FAILED 并回传 code。 + * + * 放在 companion 内以便单测直接调用(不依赖 OkHttp/凭据)。 + */ + internal fun parseCheckin(body: String): Result = runCatching { + val root = parseJson.parseToJsonElement(body).jsonObject + val data = root["data"]?.jsonObject ?: root + val code = data.numberOr("code")?.toLong() ?: root.numberOr("code")?.toLong() ?: -1L + val msg = data.scalarOr("message", "msg") ?: root.scalarOr("message", "msg") ?: "" + val points = (data.numberOr("points") ?: root.numberOr("points"))?.toLong() ?: 0L + + val succeeded = code == 0L || code == 200L + val lowerMsg = msg.lowercase() + val deviceBlock = msg.contains("设备") || lowerMsg.contains("device") || lowerMsg.contains("machine") + val already = code == 1001L || (!deviceBlock && ( + msg.contains("已签到") || msg.contains("已经签到") || msg.contains("明日再来") || + msg.contains("今日已完成") || msg.contains("已领取") || + lowerMsg.contains("already") || lowerMsg.contains("checked") || lowerMsg.contains("claimed") + )) + + when { + succeeded -> Result.success( + CheckinResult( + success = true, + alreadyChecked = false, + points = points.takeIf { it > 0 } ?: 200L, + message = msg, + outcome = CheckinOutcome.SUCCESS + ) + ) + already -> Result.success( + CheckinResult(true, true, 0L, msg, CheckinOutcome.ALREADY) + ) + deviceBlock -> Result.success( + CheckinResult(false, false, 0L, msg, CheckinOutcome.DEVICE_BLOCKED) + ) + else -> Result.success( + CheckinResult(false, false, 0L, msg, CheckinOutcome.FAILED, code) + ) + } + }.getOrElse { Result.failure(RepositoryError.Unknown(it)) } + + private fun JsonObject.scalarOr(vararg keys: String): String? { + for (key in keys) { + this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it } + } + return null + } + + /** 平铺多候选数值扫描。 */ + private fun JsonObject.numberOr(vararg keys: String): Double? { + for (key in keys) { + this[key]?.let { el -> + (el as? JsonPrimitive)?.doubleOrNull?.let { return it } + (el as? JsonPrimitive)?.contentOrNull?.toDoubleOrNull()?.let { return it } + } + } + return null + } + } } 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 a9ff1a9..f425ebb 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 @@ -282,6 +282,18 @@ class WorkBuddyRepository constructor( // ---------- 每日签到(Buddy 加油站 / codebuddy.cn)---------- + /** 签到动作语义结果(UI 层据此本地化,不直接展示 Repository 中文串)。 */ + enum class CheckinOutcome { + /** 查询模式:只读状态,无动作结果 */ + QUERY, + /** 签到成功 */ + SUCCESS, + /** 今日已签到(幂等) */ + ALREADY, + /** 签到失败(服务端给了原因,见 message) */ + FAILED + } + /** * 签到状态摘要(来源:cpa-multi-plugins workbuddy 插件实测字段)。 * active=活动进行中;todayCheckedIn=今日已签;streakDays=连签;daily/todayCredit=每日/今日积分。 @@ -293,8 +305,10 @@ class WorkBuddyRepository constructor( val dailyCredit: Long, val todayCredit: Long, val activityName: String, - /** 签到动作后的提示(签到成功/今日已签);查询模式为空串 */ - val message: String = "" + /** 服务端原始提示文案(可能是中文/英文;仅作 [outcome] 之外的补充透传) */ + val message: String = "", + /** 动作语义结果:UI 按此本地化,不依赖 [message] 语言 */ + val outcome: CheckinOutcome = CheckinOutcome.QUERY ) /** 查询今日签到状态。uid 未知时省略 X-User-Id 头(源码确认该头可选)。 */ @@ -305,9 +319,12 @@ class WorkBuddyRepository constructor( return@withContext Result.failure(RepositoryError.InvalidCredential()) } // 两条候选路径 fallback(cpa-plugin 实测:activity-status 优先,旧路径 checkin-status 兜底) - val body = authedPost(credential, "/v2/billing/meter/checkin-activity-status") - ?: authedPost(credential, "/v2/billing/meter/checkin-status") - ?: return@withContext Result.failure(RepositoryError.Unknown()) + // 共享轮换状态:前一条路径轮换出的新 token 会用于下一条,且失败也先落库。 + val body = authedPostFirstSuccessful( + credential, + "/v2/billing/meter/checkin-activity-status", + "/v2/billing/meter/checkin-status" + ) ?: return@withContext Result.failure(RepositoryError.Unknown()) runCatching { parseCheckin(body) }.fold( onSuccess = { Result.success(it) }, onFailure = { @@ -331,8 +348,20 @@ class WorkBuddyRepository constructor( if (credential !is Credential.WorkBuddyCredential) { return@withContext Result.failure(RepositoryError.InvalidCredential()) } - val body = authedPost(credential, "/v2/billing/meter/daily-checkin") - ?: return@withContext Result.failure(RepositoryError.Unknown()) + val body = when (val r = authedPost(credential, "/v2/billing/meter/daily-checkin")) { + is AuthedPostResult.Ok -> { + persistCheckinOutcome(credential, r.rotatedCredential) + r.body + } + is AuthedPostResult.Failed -> { + // 即便签到请求失败,也必须先落库轮换出的新 token,否则账号卡在死 token 上; + // 但未发生轮换(纯网络失败)时不写入——避免把失败请求误标为「刚验证通过」。 + if (r.rotatedCredential != null) { + persistCheckinOutcome(credential, r.rotatedCredential) + } + return@withContext Result.failure(RepositoryError.Unknown()) + } + } val code = runCatching { val root = json.parseToJsonElement(body).jsonObject val obj = root["data"]?.jsonObject ?: root @@ -341,26 +370,60 @@ class WorkBuddyRepository constructor( val status = fetchCheckinStatus().getOrNull() when { code == 0L -> Result.success( - status?.copy(message = "签到成功 +" + (status.todayCredit.takeIf { it > 0 } ?: status.dailyCredit) + " 积分") - ?: CheckinStatus(true, true, 1, 0, 0, "", "签到成功") + (status ?: CheckinStatus(true, true, 1, 0, 0, "")).copy( + outcome = CheckinOutcome.SUCCESS + ) + ) + code == 10001L -> Result.success( + (status ?: CheckinStatus(true, true, 0, 0, 0, "")).copy( + outcome = CheckinOutcome.ALREADY + ) ) - code == 10001L -> Result.success(status?.copy(message = "今日已签到") ?: CheckinStatus(true, true, 0, 0, 0, "", "今日已签到")) else -> { val msg = runCatching { val root = json.parseToJsonElement(body).jsonObject val obj = root["data"]?.jsonObject ?: root obj.scalar("msg", "message", "Message") ?: "" }.getOrDefault("") - if (status != null) Result.success(status.copy(message = if (msg.isBlank()) "签到失败" else msg)) - else Result.failure(RepositoryError.Unknown()) + if (status != null) { + Result.success(status.copy(message = msg, outcome = CheckinOutcome.FAILED)) + } else { + Result.failure(RepositoryError.Unknown()) + } } } } - /** 带自动 refresh 一次的 POST(Bearer);返回响应体,失败 null。 */ - private fun authedPost(credential: Credential.WorkBuddyCredential, path: String): String? { + /** + * [authedPost] 的结果。 + * + * [rotatedCredential] 一旦非空,表示服务端已作废旧 accessToken(新的 access/refresh + * 已轮换),调用方**必须**先落库再决定后续动作——即使本次请求最终失败,也不能丢弃它, + * 否则旧 refreshToken 单次消费后失效,账号会卡在死 token 上(永久 401)。 + */ + private sealed interface AuthedPostResult { + val rotatedCredential: Credential.WorkBuddyCredential? + + /** 请求成功(2xx),[body] 为响应体。 */ + data class Ok( + val body: String, + override val rotatedCredential: Credential.WorkBuddyCredential? + ) : AuthedPostResult + + /** 请求失败,但过程中发生过 token 轮换(新凭据必须落库)。 */ + data class Failed( + override val rotatedCredential: Credential.WorkBuddyCredential? + ) : AuthedPostResult + } + + /** + * 带自动 refresh 一次的 POST(Bearer)。 + * 失败时若已发生轮换,会以 [AuthedPostResult.Failed] 回传新凭据(不丢轮换结果)。 + */ + private fun authedPost(credential: Credential.WorkBuddyCredential, path: String): AuthedPostResult { var access = credential.accessToken var refresh = credential.refreshToken + var rotated: Credential.WorkBuddyCredential? = null repeat(2) { attempt -> val request = Request.Builder() .url("https://www.codebuddy.cn" + path) @@ -372,44 +435,72 @@ class WorkBuddyRepository constructor( val resp = try { okHttpClient.newCall(request).execute() } catch (_: Throwable) { - return null + return AuthedPostResult.Failed(rotated) } val code = resp.code val body = runCatching { resp.body?.string().orEmpty() }.getOrDefault("") resp.close() when { - code in 200..299 -> return body + code in 200..299 -> return AuthedPostResult.Ok(body, rotated) (code == 401 || code == 403) && attempt == 0 -> { - val refreshed = tryRefesh(refresh) ?: return null - // 轮换后的凭据由调用方主流程 save(此处仅内存内继续),避免半途改库 + val refreshed = tryRefesh(refresh) ?: return AuthedPostResult.Failed(rotated) + val expiresAt = if (refreshed.expiresIn > 0) { + System.currentTimeMillis() + refreshed.expiresIn * 1000 + } else { + credential.expiresAt.coerceAtLeast(System.currentTimeMillis()) + } + rotated = credential.copy( + accessToken = refreshed.accessToken, + refreshToken = refreshed.refreshToken, + expiresAt = expiresAt + ) access = refreshed.accessToken refresh = refreshed.refreshToken } - else -> return null + else -> return AuthedPostResult.Failed(rotated) + } + } + return AuthedPostResult.Failed(rotated) + } + + /** + * 候选路径 fallback 专用:依次尝试各路径,但**同一次查询内共享轮换状态**—— + * 若前一条路径已轮换 token,后续路径必须用新 accessToken 请求,并把轮换结果 + * 落库后再返回,避免旧 token 重试直接失败。 + */ + private suspend fun authedPostFirstSuccessful( + credential: Credential.WorkBuddyCredential, + vararg paths: String + ): String? { + var current = credential + for (path in paths) { + when (val r = authedPost(current, path)) { + is AuthedPostResult.Ok -> { + persistCheckinOutcome(credential, r.rotatedCredential) + return r.body + } + is AuthedPostResult.Failed -> { + // 即便本路径失败,也要先把轮换出的新 token 落库并继续用新 token 试下一条 + if (r.rotatedCredential != null) { + persistCheckinOutcome(credential, r.rotatedCredential) + current = r.rotatedCredential + } + } } } return null } - /** 解析 checkin-activity-status 响应(data 平铺或 data 包裹,防御式多候选)。 */ - private fun parseCheckin(body: String): CheckinStatus { - val root = json.parseToJsonElement(body).jsonObject - val obj = root["data"]?.jsonObject ?: root - return CheckinStatus( - active = obj.scalar("active", "Active")?.toBoolean() ?: true, - todayCheckedIn = obj.scalar("today_checked_in", "todayCheckedIn")?.toBoolean() ?: false, - streakDays = obj.longOr("streak_days", "streakDays") ?: 0, - dailyCredit = obj.longOr("daily_credit", "dailyCredit") ?: 0, - todayCredit = obj.longOr("today_credit", "todayCredit") ?: 0, - activityName = obj.scalar("activity_name", "activityName", "theme_name") ?: "" - ) - } - - private fun JsonObject.longOr(vararg keys: String): Long? { - for (key in keys) { - this[key]?.jsonPrimitive?.contentOrNull?.toLongOrNull()?.let { return it } - } - return null + /** + * 落库本次签到/状态查询产出:轮换后的 token(若有)+ lastVerifiedAt。 + * 与 [fetchBalance] 同款语义——一次成功请求即视为凭据已验证。 + */ + private suspend fun persistCheckinOutcome( + credential: Credential.WorkBuddyCredential, + rotated: Credential.WorkBuddyCredential? + ) { + val base = rotated ?: credential + credentialRepository.save(base.copy(lastVerifiedAt = System.currentTimeMillis())) } private fun mapHttpError(code: Int, body: String): RepositoryError = when (code) { @@ -433,4 +524,40 @@ class WorkBuddyRepository constructor( val unit: String?, val extras: Map ) + + companion object { + /** 解析专用 Json(companion 内独立持有,使 parseCheckin 可静态调用/单测)。 */ + private val parseJson = Json { ignoreUnknownKeys = true } + + /** + * 解析 checkin 响应(data 平铺或 data 包裹,防御式多候选)。 + * 放在 companion 内以便单测直接调用(不依赖 OkHttp/凭据)。 + */ + internal fun parseCheckin(body: String): CheckinStatus { + val root = parseJson.parseToJsonElement(body).jsonObject + val obj = root["data"]?.jsonObject ?: root + return CheckinStatus( + active = obj.scalar("active", "Active")?.toBoolean() ?: true, + todayCheckedIn = obj.scalar("today_checked_in", "todayCheckedIn")?.toBoolean() ?: false, + streakDays = obj.longOr("streak_days", "streakDays") ?: 0, + dailyCredit = obj.longOr("daily_credit", "dailyCredit") ?: 0, + todayCredit = obj.longOr("today_credit", "todayCredit") ?: 0, + activityName = obj.scalar("activity_name", "activityName", "theme_name") ?: "" + ) + } + + private fun JsonObject.scalar(vararg keys: String): String? { + for (key in keys) { + this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }?.let { return it } + } + return null + } + + private fun JsonObject.longOr(vararg keys: String): Long? { + for (key in keys) { + this[key]?.jsonPrimitive?.contentOrNull?.toLongOrNull()?.let { return it } + } + return null + } + } } diff --git a/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt b/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt index 54366b4..6aa5810 100644 --- a/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt +++ b/app/src/main/java/com/rainy/token/domain/usecase/RefreshBalanceUseCase.kt @@ -92,6 +92,29 @@ class RefreshBalanceUseCase @Inject constructor( ollamaRepositoryProvider.get().triggerUsage(model) } + /** + * 执行每日签到(TRAE / WORKBUDDY),返回各自的语义结果类型。 + * + * 必须走 [withCredentialSession]:签到会触发 OAuth 轮换(WorkBuddy 401/403 时刷新 + * access/refresh),轮换结果需按凭据快照校验后提交,避免并发改凭据被旧快照覆盖; + * 同时会话内重试才能读到已轮换的新 token(否则旧 refreshToken 单次消费后直接失效)。 + */ + suspend fun checkInWorkBuddy(): Result = + withCredentialSession(ServiceType.WORKBUDDY) { + workBuddyRepositoryProvider.get().checkIn() + } + + suspend fun checkInTrae(): Result = + withCredentialSession(ServiceType.TRAE) { + traeRepositoryProvider.get().checkIn() + } + + /** 查询今日签到状态(仅 WORKBUDDY 有该接口)。走会话以保证状态查询的凭据写入不覆盖并发编辑。 */ + suspend fun fetchCheckinStatus(): Result = + withCredentialSession(ServiceType.WORKBUDDY) { + workBuddyRepositoryProvider.get().fetchCheckinStatus() + } + private suspend fun withCredentialSession( service: ServiceType, block: suspend () -> Result diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt index 9cc8b97..d000032 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt @@ -117,6 +117,8 @@ fun ServiceDetailScreen( val selectedModel by viewModel.selectedModel.collectAsStateWithLifecycle() val modelsLoading by viewModel.modelsLoading.collectAsStateWithLifecycle() val checkinState by viewModel.checkinState.collectAsStateWithLifecycle() + val checkinInfo by viewModel.checkinInfo.collectAsStateWithLifecycle() + // 面板管理(Key/分组/兑换)已移到首页 Sub2API 卡片的「面板管理」入口, // 详情页只保留用量明细所需的窗口统计状态。 val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle() @@ -193,11 +195,18 @@ fun ServiceDetailScreen( ServiceType.TRAE, ServiceType.WORKBUDDY -> { // 每日签到卡(WorkBuddy/Trae 共用);下方仍展示通用 extras + // 未配置凭据时展示引导卡,避免暴露必然失败的签到按钮 item { - CheckinCard( - state = checkinState, - onCheckin = { viewModel.checkin() } - ) + if (uiState.hasCredential) { + CheckinCard( + state = checkinState, + todayCheckedIn = checkinInfo?.todayCheckedIn == true, + streakDays = checkinInfo?.streakDays ?: 0L, + onCheckin = { viewModel.checkin() } + ) + } else { + CheckinNeedCredentialCard() + } } item { GenericExtrasCard(uiState.state) } } @@ -1098,7 +1107,13 @@ private fun BreakdownRow(label: String, value: Double, unit: String) { * 结果消息直接采用官方响应语义(签到成功/今日已签到/设备拦截/失败原因)。 */ @Composable -private fun CheckinCard(state: CheckinState, onCheckin: () -> Unit) { +private fun CheckinCard( + state: CheckinState, + todayCheckedIn: Boolean, + streakDays: Long, + onCheckin: () -> Unit +) { + val actionDone = todayCheckedIn || state is CheckinState.Done Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), @@ -1120,27 +1135,48 @@ private fun CheckinCard(state: CheckinState, onCheckin: () -> Unit) { Spacer(modifier = Modifier.height(12.dp)) when (state) { is CheckinState.Done -> Text( - text = state.message, + text = state.message.asString(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary ) is CheckinState.Error -> Text( - text = state.message, + text = state.message.asString(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) - else -> Unit + // 提示态(设备级拦截等):中性色,且不置灰按钮 + is CheckinState.Notice -> Text( + text = state.message.asString(), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + // 尚未点过按钮但预取到「今日已签」:给出连签提示 + else -> if (todayCheckedIn) { + Text( + text = if (streakDays > 0) { + stringResource(R.string.checkin_already_streak, streakDays) + } else { + stringResource(R.string.checkin_already) + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } } Spacer(modifier = Modifier.height(12.dp)) Button( onClick = onCheckin, - enabled = state !is CheckinState.Loading, + enabled = state !is CheckinState.Loading && !actionDone, modifier = Modifier.fillMaxWidth().height(44.dp), shape = RoundedCornerShape(12.dp) ) { Text( text = stringResource( - if (state is CheckinState.Loading) R.string.checkin_loading else R.string.checkin_action + when { + state is CheckinState.Loading -> R.string.checkin_loading + actionDone -> R.string.checkin_action_done + else -> R.string.checkin_action + } ) ) } @@ -1148,6 +1184,31 @@ private fun CheckinCard(state: CheckinState, onCheckin: () -> Unit) { } } +/** 未配置凭据时的签到占位卡:引导去配置,而不是直接暴露必然失败的签到按钮。 */ +@Composable +private fun CheckinNeedCredentialCard() { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + Text( + text = stringResource(R.string.checkin_title), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = stringResource(R.string.checkin_need_credential), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + } + } +} + /** * 通用明细卡:把 balance.extras 中的友好键逐行展示(Trae 权益包 / WorkBuddy 资源 / * Sub2API 账户等)。技术键(models / reset* / raw 等)一律过滤。 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 94225b5..4f56805 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 @@ -7,6 +7,8 @@ import com.rainy.token.data.cache.CachedBalance 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.TraeRepository +import com.rainy.token.data.repository.WorkBuddyRepository import com.rainy.token.data.repository.TriggerError import com.rainy.token.data.repository.TriggerErrorReason import com.rainy.token.domain.model.Credential @@ -33,8 +35,6 @@ class ServiceDetailViewModel @Inject constructor( private val credentialRepository: CredentialRepository, private val balanceCache: BalanceCache, private val refreshBalanceUseCase: RefreshBalanceUseCase, - private val workBuddyRepository: com.rainy.token.data.repository.WorkBuddyRepository, - private val traeRepository: com.rainy.token.data.repository.TraeRepository, private val sub2ApiRepository: com.rainy.token.data.repository.Sub2ApiRepository ) : ViewModel() { @@ -58,7 +58,33 @@ class ServiceDetailViewModel @Inject constructor( private val _checkinState = MutableStateFlow(CheckinState.Idle) val checkinState: StateFlow = _checkinState.asStateFlow() - /** 触发每日签到:仅 WORKBUDDY / TRAE 有效;结果消息来自官方响应语义。 */ + /** + * 进入详情页时预取的「今日签到信息」(WorkBuddy)。 + * null=未知/未加载;用于展示「今日已签到(连签 N 天)」并禁用按钮,避免重复点击。 + */ + private val _checkinInfo = MutableStateFlow(null) + val checkinInfo: StateFlow = _checkinInfo.asStateFlow() + + /** 今日签到信息摘要(仅字段已确认的才填)。 */ + data class CheckinInfo(val todayCheckedIn: Boolean, val streakDays: Long) + + /** 进入详情页时预取签到状态:仅 WORKBUDDY 支持状态查询接口,TRAE 无该接口。 */ + private fun preloadCheckinInfo(service: ServiceType) { + _checkinInfo.value = null + if (service != ServiceType.WORKBUDDY) return + val generation = serviceGeneration + viewModelScope.launch { + // 走 RefreshBalanceUseCase 会话:状态查询也会触发 OAuth 轮换,需按快照校验提交 + val status = refreshBalanceUseCase.fetchCheckinStatus().getOrNull() ?: return@launch + if (generation != serviceGeneration || _serviceType.value != service) return@launch + _checkinInfo.value = CheckinInfo( + todayCheckedIn = status.todayCheckedIn, + streakDays = status.streakDays + ) + } + } + + /** 触发每日签到:仅 WORKBUDDY / TRAE 有效;结果按语义本地化(不直接显示 Repository 中文串)。 */ fun checkin() { val service = _serviceType.value if (service != ServiceType.WORKBUDDY && service != ServiceType.TRAE) return @@ -67,23 +93,75 @@ class ServiceDetailViewModel @Inject constructor( val generation = serviceGeneration viewModelScope.launch { val result = when (service) { - ServiceType.WORKBUDDY -> workBuddyRepository.checkIn().fold( + ServiceType.WORKBUDDY -> refreshBalanceUseCase.checkInWorkBuddy().fold( onSuccess = { s -> - CheckinState.Done( - s.message.ifBlank { - if (s.todayCheckedIn) "今日已签到(连签 " + s.streakDays + " 天)" else "签到成功" + when (s.outcome) { + WorkBuddyRepository.CheckinOutcome.ALREADY -> + // 已签时带上连签天数(接口已回传,别浪费) + CheckinState.Done( + if (s.streakDays > 0) { + UiText.Resource(R.string.checkin_already_streak, listOf(s.streakDays)) + } else { + UiText.Resource(R.string.checkin_already) + } + ) + WorkBuddyRepository.CheckinOutcome.SUCCESS -> { + val credits = s.todayCredit.takeIf { it > 0 } ?: s.dailyCredit + CheckinState.Done( + if (credits > 0) { + UiText.Resource(R.string.checkin_wb_success_credits, listOf(credits)) + } else { + UiText.Resource(R.string.checkin_success) + } + ) } - ) + WorkBuddyRepository.CheckinOutcome.FAILED -> + // 失败原因优先透传服务端原文(多为官方中文提示),缺省用通用失败文案 + CheckinState.Error( + if (s.message.isNotBlank()) UiText.Dynamic(s.message) + else UiText.Resource(R.string.checkin_failed) + ) + // QUERY 不该出现在签到动作结果里,兜底为通用成功 + WorkBuddyRepository.CheckinOutcome.QUERY -> + CheckinState.Done(UiText.Resource(R.string.checkin_success)) + } }, - onFailure = { e -> CheckinState.Error(e.message ?: "签到失败,请检查凭据") } + // RepositoryError.message 是硬编码中文,必须走 errorMessage() 本地化映射 + onFailure = { e -> CheckinState.Error(errorMessage(e)) } ) - ServiceType.TRAE -> traeRepository.checkIn().fold( - onSuccess = { r -> CheckinState.Done(r.message) }, - onFailure = { e -> CheckinState.Error(e.message ?: "签到失败,请检查凭据") } + ServiceType.TRAE -> refreshBalanceUseCase.checkInTrae().fold( + onSuccess = { r -> + when (r.outcome) { + TraeRepository.CheckinOutcome.SUCCESS -> + CheckinState.Done(UiText.Resource(R.string.checkin_success)) + TraeRepository.CheckinOutcome.ALREADY -> + CheckinState.Done(UiText.Resource(R.string.checkin_already)) + // 设备级拦截不是「账号已签」:用 Notice 展示,按钮保持可点(用户可换网/稍后重试) + TraeRepository.CheckinOutcome.DEVICE_BLOCKED -> + CheckinState.Notice(UiText.Resource(R.string.checkin_trae_device_blocked)) + TraeRepository.CheckinOutcome.INTL_UNSUPPORTED -> + CheckinState.Error(UiText.Resource(R.string.checkin_trae_intl)) + TraeRepository.CheckinOutcome.FAILED -> CheckinState.Error( + // 服务端原始原因优先;缺省用带 code 的失败文案 + if (r.message.isNotBlank()) { + UiText.Dynamic(r.message) + } else if (r.code >= 0) { + UiText.Resource(R.string.checkin_trae_failed_code, listOf(r.code)) + } else { + UiText.Resource(R.string.checkin_failed) + } + ) + } + }, + onFailure = { e -> CheckinState.Error(errorMessage(e)) } ) - else -> CheckinState.Error("该服务不支持签到") + else -> CheckinState.Error(UiText.Resource(R.string.checkin_unsupported)) + } + if (generation == serviceGeneration) { + _checkinState.value = result + // 签到成功后刷新「今日已签到/连签」摘要,让按钮态即时更新 + if (result is CheckinState.Done) preloadCheckinInfo(service) } - if (generation == serviceGeneration) _checkinState.value = result } } @@ -294,11 +372,14 @@ class ServiceDetailViewModel @Inject constructor( _selectedModel.value = null _modelsLoading.value = false _triggerState.value = TriggerState.Idle + // 切换服务时重置签到状态,避免上一服务的 Done/Error 残留(按钮态/文案串台) + _checkinState.value = CheckinState.Idle if (service == ServiceType.CODEX || service == ServiceType.OPENCODE_GO || service == ServiceType.OLLAMA) { loadSelectedModel(service)?.let { _selectedModel.value = it } loadModelsCache(service).takeIf { it.isNotEmpty() }?.let { _models.value = it } } + preloadCheckinInfo(service) loadFromCache() } @@ -796,11 +877,18 @@ class ServiceDetailViewModel @Inject constructor( } /** 每日签到 UI 状态。 */ +/** 每日签到 UI 状态。消息用 [UiText] 承载,由 UI 层按当前 Locale 解析(支持多语言)。 */ sealed class CheckinState { data object Idle : CheckinState() data object Loading : CheckinState() - data class Done(val message: String) : CheckinState() - data class Error(val message: String) : CheckinState() + data class Done(val message: UiText) : CheckinState() + data class Error(val message: UiText) : CheckinState() + + /** + * 提示态(非错误、也非完成):例如 Trae 设备级拦截「该设备今日签到次数已达上限」。 + * 与 [Done] 的区别是**不置灰按钮**——账号本身尚未签到,换网络/稍后可重试。 + */ + data class Notice(val message: UiText) : CheckinState() } /** Sub2API 面板 Key 列表状态。 */ 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 f8cc7c0..cc1353e 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -1,688 +1,699 @@ - - - - AI用量面板 - OpenCode Go 用量 + DeepSeek 余额,自动刷新 - 雨晴喵 - 雨晴Token - - - - - 返回 - 取消 - 确认添加 - 我知道了 - 刷新 - 设置 - 删除 - 清除 - 保存 - 跳转 - ◀ 上一页 - 下一页 ▶ - 添加小组件到桌面 - 语言 - 选择语言 - 跟随系统 - 简体中文 - 繁體中文 - English - 查看详情 - 查看用量详情 - 详细数据 - 同步用量数据 - 我已登录,保存 - 继续等待 - - - 加载中… - 未知错误 - 全部模型 - - - 👈 选择服务查看详情 - 点击左侧卡片即可 - 设置 - API 管理 - 配置 - 编辑 - 测试 - 删除凭据? - 将删除 %1$s 已保存的凭据。 - 未配置 - 最后验证:%1$s - 缓存余额:%1$s %2$s - 用量统计 - 暂无已配置服务 - %1$d 项刷新失败 - 尚未获取 - 更新于 %1$s - 本月消费 - 总额度 - 下次重置 - ✅ %1$s 连接成功:%2$s %3$s - ❌ %1$s 连接失败:%2$s - 凭据无效 - 凭据已变更 - 请求过于频繁 - 网络异常 - 服务端异常(HTTP %1$d) - 解析失败 - 凭据管理 - 关于 RainyToken - 使用小技巧 - 调试日志 - 用量详情 - 总统计 - 详细数据 - 统计图表 - - - 未配置 - 已配置 - 已过期 - 需重登 - 刷新失败 - 未获取 - 待获取 - 正常 - 未配置 · 点击添加 - 已配置 · 凭据有效 - 已过期 · 点击重新配置 - 需要重新验证 - 手动输入模式 - 凭据已配置 - 未配置凭据 - 加载中 - 最新 - 缓存 - 失败 - 待输入 - - - AI 余额一览 - 还没有配置任何服务 - 首页仅显示已配置凭据的服务 - 去设置添加 - 从未获取 - 刚刚更新 - %1$d 分钟前更新 - %1$d 小时前更新 - %1$s 更新 - 💡 长按卡片可拖拽排序 - ⚠ %1$s - - 本地加密存储,不上传任何数据 - 添加桌面小组件? - 确认后会请求桌面启动器添加雨晴Token小组件。部分系统仍会弹出系统确认窗口。 - 桌面快捷方式权限已被拒绝,请在系统设置中允许后重试 - 桌面未接受添加请求,请检查启动器权限 - 当前桌面不支持应用内添加,请在桌面长按 → 小组件 → 查找雨晴Token - - - 点击配置凭据 - 下拉刷新 - 服务当前不可用 - 本月已用 %1$s%2$s - 剩余 - · 已用 %1$s / 共 %2$s - %1$s 已用 - 5h 已用 - · %1$s - %1$s 后重置 - REST API · ¥ - WebView 抓取 · 5h 配额 - JSON API · $ - ChatGPT · Codex 额度 - Cookie 抓取 · Cloud 配额 - Cloud-IDE-JWT · IDE 额度 - OAuth · CodeBuddy 同体系积分 - 自托管面板 · 用户级 sk- Key 余额 - 明细 - Trae 凭据 - 粘贴 Trae 登录态中的 Cloud-IDE-JWT(ey… 开头)。请求头按 Authorization: Cloud-IDE-JWT 发送。 - Cloud-IDE-JWT - 区域 - CN - INTL - WorkBuddy 凭据 - 粘贴腾讯 WorkBuddy(CodeBuddy 同体系)会话的 access & refresh token。refresh token 每次使用会轮换。 - Access Token - Refresh Token - Access 与 Refresh Token 必填 - Sub2API 实例 - 该实例的任何用户:直接填分给你的 sk-… API Key 即可查余额(无需管理员)。没有 Key 也可用面板账号邮箱+密码登录。 - 实例基址 - https://api.example.com - 认证方式 - API Key(sk-…) - 或面板账号 - 邮箱 - 密码 - 实例基址与(API Key 或 邮箱+密码)必填 - - - 用量明细 - 来自 Sub2API 网关的每日与分模型用量 - 今天 - 近7天 - 近30天 - 近90天 - 全部 - 请求 - Tokens - 花费 - 每日用量 - 按模型 - 全部模型 - %1$s 次请求 · %2$s tokens - 该时间范围内暂无每日用量数据 - 该时间范围内无花费 - 模型:%1$s · 当前周期累计 - 该周期暂无模型数据 - - - 每日签到 - 每天签到领积分,连签有奖励 - 签到 - 签到中… - - - 在打开的页面中正常登录,App 自动读取登录态;也可继续手动填 API Key - 已获取面板 Token,点击保存生效 - ✓ 已获取面板 Token - - - 天 - 小时 - 分 - - - 5 小时 - 5h - 5h 用量 - 5 小时滚动 - 本周 - 本月 - 每月 - 用量 - 用量窗口%1$s - 用量窗口 - 模型用量 - 配额 $%1$s - - - OpenCode 用量 - CommandCode 用量 - %1$d 条记录 - 点击同步 - 输入 Token - 输出 Token - 推理 Token - 总花费 - 缓存读取 %1$s - 缓存写入 %1$s - 暂无数据,点击 🔄 同步 - ✓ 新增 %1$d 条 - 部分同步完成:插入 %1$d条,%2$d页失败 - 点击切换 - 所有时间按 UTC+0 计算 - 已切换到 UTC+8,图表按北京时间显示 - 已切换到 UTC+0,图表按世界协调时显示 - 已用 $%1$s / 共 $%2$s - 剩余 $%1$s - 额外充值 - 按模型统计 - 按天统计(UTC+0) - %1$d 个模型 - %1$d 个 - 开始时间 - 结束时间 - - - 消耗金额 (USD) - API 请求次数 - Token 消耗 - 输出 - 输入(未命中) - 命中缓存 - - - 时间 - 模型 - 输入(缓存) - 输出 - 成本 - - - 全部 - 最近5小时 - 最近12小时 - 最近24小时 - 今天 - 昨天 - 最近7天 - 最近30天 - 当月 - 自定义 - 自定义天 - 自定义月 - 自定义时间 - - - 统一查询 AI 余额的小工具。\n本地加密存储凭据,不上传任何数据。 - 查看全部 %1$d 条隐藏操作 - 查看 token 刷新、网络请求等调试记录 - - - ✓ 已配置登录态(%1$d 个 Cookie) - 如何获取 Cookie? - 1. 在桌面 Chrome 打开下方登录入口 - 2. 用 GitHub / Google 登录(不会被拦截) - 3. 登录后按 F12 → Application → Cookies - 4. 复制该站点下的所有 Cookie 粘贴到上面输入框 - 格式:name1=value1; name2=value2 - 如何获取 OpenCode Go 凭据? - 1. 桌面 Chrome 打开 opencode.ai/auth - 2. 用 GitHub / Google 登录(Google 不会拦截 Chrome) - 3. 登录后跳转到 dashboard,URL 形如: - https://opencode.ai/workspace/{id}/go - 4. 复制 URL 中的 {id} 段 → workspaceId - 5. F12 → Application → Cookies - 6. 找到域名 opencode.ai 下名为 auth 的 Cookie - 7. 复制它的 Value 字段 → auth cookie - 注意:只填 auth 一个 Cookie 即可,不要整个 Cookie 字符串 - 如何获取 CommandCode 凭据? - 1. 打开 https://commandcode.ai 并登录 GitHub - ▸ 获取 API Key(必填): - 右上角头像 → Settings → API Keys → 创建一个 Key - ▸ 获取全部 Cookie(用于用量记录): - F12 → Application → Cookies → commandcode.ai - 选中所有 Cookie,复制整个 Cookie 字符串 - 粘贴到下方输入框即可,无需逐个挑选 - 提示:不填 Cookie 也能正常查看余额,只是用量记录为空。 - 如何获取 Codex auth.json? - 1. 在终端运行 codex-oauth-proxy 或 - codex-proxy,完成 ChatGPT 登录 - 2. 凭证文件保存在: - - - 已保存 - 已保存凭据 - 连接成功!余额: %1$s %2$s - 连接成功,凭据已保存 - API Key 不能为空 - JSON 格式错误 - 服务拒绝该凭据 (401/403) - 测试期间凭据已变更 - 请求过于频繁 (429) - 服务端错误 (%1$d) - 网络错误:%1$s - ,已恢复原凭据 - ,检测到凭据已变化,未执行回滚 - 测试失败。%1$s%2$s - 暂不支持测试此服务 - auth cookie 和 workspaceId 都需要填写 - Cookie 不能为空 - 凭据未配置 - 凭据未配置或已过期 - 凭据无效,请在设置中重新配置 - 凭据无效,请重新登录 - 凭据已变更,正在重新加载 - 凭据已变更,请重新操作 - 网络异常,请检查网络 - 网络异常 - 服务端异常 (HTTP %1$d) - 请求过于频繁%1$s - ,请 %1$d 秒后重试 - 不支持激活用量 - 请先选择模型 - 未抓到 Cookie,请确认已登录 - 未配置登录 URL - URL 应以 %1$s 开头 - 授权失败: %1$s - State 不匹配,请重试 - 回调缺少授权码 - Token 交换失败,请查看调试日志 - - - ✓ 用量已激活 - ✓ 激活成功 - 请求结果 - 错误: %1$s - 响应内容:\n%1$s - 模型: %1$s - 响应 ID: %1$s - 回复: - (空) - (无回复内容) - (响应解析失败,但请求已发出) - 用量: prompt=%1$s completion=%2$s total=%3$s tokens - 用量: input=%1$s output=%2$s tokens - token 刷新失败,请重新登录或重新导入 auth.json - - - 登录 %1$s - 完成登录了吗? - 如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。 - - - 暂无日志记录\n刷新 Codex 用量后这里会出现调试信息 - - - OpenCode Go · 用量 - DeepSeek 余额 - 自动刷新 - %1$s · 额度 - %1$s · %2$s - 更新 %1$s - 暂无数据 - 刷新中… - - - 长按卡片拖拽排序 - 长按首页卡片可以拖拽排序 - 在首页长按任意服务卡片即可拖拽到想要的位置,排序会自动保存。 - 小组件点左上角进 APP - 小组件左上角标识可打开 APP - 桌面小组件左上角 RainyToken › 标识可以点击打开应用,其它区域点击切换服务。 - 图表点击查看详情 - 点击图表柱子可查看时段详情 - 在用量图表上点击或滑动手指,可查看对应时段的详细数值。 - 图表标题可点击 - 点击图表标题弹出按模型明细 - 图表卡片标题(如「消耗金额」)点击后会弹出按模型拆分的明细。 - 切换图表时区 - 用量详情页可切换 UTC+0 / UTC+8 - 用量详情页顶部可切换 UTC+0 / UTC+8,图表标签会跟随切换。 - CCGO 清除并重新同步 - CCGO 详情页可清除缓存重新同步 - CCGO 用量详情页右上角「清除」可删除本地缓存并重新拉取全部数据。 - 查看用量详情 - 服务卡片底部可进入用量图表 - 有凭据的服务卡片底部会出现「查看用量详情」入口,点击进入图表统计。 - 原始数据浏览 - 用量详情页右上角可查看原始记录 - 用量详情页右上角「详细数据」可查看每条原始调用记录,支持分页跳转。 - 下拉刷新联动用量 - 下拉刷新会同步余额和用量 - 首页下拉刷新不仅刷新余额,还会自动同步 OCGO / CCGO 用量记录。 - 凭据获取教程 - 凭据页底部有获取教程 - 凭据编辑页底部的「如何获取」按钮有针对每个服务的详细操作步骤。 - 剪贴板自动导入 - Codex 支持从剪贴板一键导入 - Codex 凭据页支持「从剪贴板自动导入」,粘贴 auth.json 后一键填入。 - 详细数据页码跳转 - 原始数据页可输入页码直跳 - 原始数据页底部可输入页码直接跳转,数据量大时不必逐页翻。 - 小组件自动刷新 - 划到小组件会自动刷新余额 - 桌面小组件划到负一屏时会自动刷新余额,也可手动点 ↻ 按钮刷新。 - - - %1$d次 - 选择日期 - 选择月份(任意一天) - 日期范围 - 开始日期 - 结束日期 - 消费明细 - 调用次数明细 - Token 明细 - 清除数据 - 此操作将清除所有 CommandCode 用量本地缓存并重新同步,需要几秒钟完成。 - 是否继续? - 确认(%1$ds) - 确认清除 - 关闭 - 从 %1$s - 至 %1$s - 应用自定义范围 - 确定 - 结束日期不能早于开始日期 %1$s - 时间 %1$s:%2$s - 暂无数据 - 合计: %1$s - 总计 - 记录数 - 缓存读取 - 缓存写入 - 缓存 Token 细分 - ※ 缓存读取已计入「输入 Token」 - %1$d 次调用 - MM月dd日 EEEE - %1$d 次 · $%2$s - 原始数据 - 缓存写入(5m) - 缓存写入(1h) - 成本(USD) - 成本(Raw) - - - ~/.config/codex-oauth-proxy/auth.json - 3. 用文本编辑器打开该文件, - 复制全部内容粘贴到上方输入框 - JSON 格式示例: - {"tokens": { - "access_token": "...", - "refresh_token": "...", - "expiresAt": 1783727108252 - }} - 提示:导入后 APP 会自动刷新 token, - 不需要手动更新。 - Ollama 凭据 - 需要从浏览器复制完整 Cookie。登录 ollama.com/settings 后,在 DevTools → Network → 请求头里找到 Cookie 行复制。 - 更新凭据 - 保存凭据 - 测试并保存 - 在浏览器中打开 Ollama Settings - 复制登录 URL 到剪贴板 - 一键激活用量 API Key(可选) - 填入 Ollama Cloud API Key 后,可在详情页一键发送请求激活用量统计。从 ollama.com/settings → API Keys 获取。 - ollama-xxx 或 sk-xxx - 保存 API Key - OpenCode Go 凭据 - 需要 auth cookie + workspaceId 两个值。APP 会用 OkHttp 抓 dashboard 并自动解析 5h/周/月 三档用量。 - 从剪贴板自动导入 - dashboard URL 中的 {id} 段 - auth Cookie 值 - F12 → Cookies → opencode.ai → auth 的 Value - 如何获取这两个值? - 在浏览器中打开登录入口 - 填入 OpenCode Zen API Key 后,可在详情页一键发送请求激活用量统计。从 opencode.ai/settings → API Keys 获取。 - CommandCode 凭据 - 需要 API Key(拉余额)。Session Cookie(可选)在浏览器 DevTools 复制,用于拉用量记录。不填也可正常查看余额。 - 从 Settings → API Keys 生成 - Session Cookie(可选) - F12 → Cookies → commandcode.ai → 全选复制粘贴即可 - 如何获取 API Key 和 Cookie? - 请输入 API Key - 凭据会使用 Android Keystore (AES-256 GCM) 加密后存储到本机 DataStore。 - 更新 - Codex / ChatGPT 凭据 - 推荐使用 OAuth 登录自动获取凭据。也可手动粘贴 auth.json 内容。 - 🔐 OAuth 登录(推荐) - ── 或手动导入 ── - auth.json 完整内容 - 粘贴 {"tokens": {...}} 完整 JSON - 导入并保存 - 如何获取 auth.json? - 粘贴登录态 - Google OAuth 会拒绝 APP 内置 WebView,所以请用浏览器登录后把 Cookie 粘到这里。 - Cookie 字符串 - 保存 Cookie - - - 响应体为空 - 响应根节点不是 JSON 对象 - 未找到用量窗口数据 - 未找到模型列表 - 模型列表为空 - 响应格式异常 - 每周 - Spark - 模型调用次数 - 缓存数据 · %1$s - 显示的是上次成功获取的余额 - 本月消费 %1$s%2$s - 余额构成 - 赠送余额 - 自费充值 - 加载模型列表... - 选择模型 - 刷新模型列表 - 激活中... - 重试激活用量 - ⚡ 一键激活用量 - 刷新余额 - 重新登录 - 配置凭据 - 充值 - 当前余额 - 5h 实时用量 - 月度余额 - Session 用量 - 请选择模型 - 错误详情: - API 响应内容: - 复制 - *** (长度 %1$d) *** - %1$s***%2$s (长度 %3$d) - Cookie 格式错误,应为 name1=value1; name2=value2 - 已保存 %1$d 个 Cookie - 请粘贴 auth.json 内容 - auth.json 缺少 access_token 或 refresh_token - 已保存 Codex 凭据,token 到期后会自动刷新 - 解析失败:%1$s - 连接成功!余额: $%1$s - API Key 已保存 - 连接成功!Session: %1$s%% · %2$s - 凭据已删除 - 剪贴板为空 - 已识别 workspaceId 和 auth cookie - 已识别 workspaceId,请再粘贴 auth cookie - 已识别 auth cookie,请再粘贴 workspaceId - 未识别到有效凭据 - - - Codex OAuth 登录 - 正在交换 Token... - 重试 - 无头模式登录 - 1. 点击「在浏览器中打开」或复制下方链接到浏览器\n2. 在 OpenAI 页面完成登录和授权\n3. 浏览器会跳转到一个 localhost 地址(页面打不开是正常的)\n4. 复制浏览器地址栏的完整 URL 粘贴到下方输入框\n5. 点击「提交回调 URL」 - 授权链接 - 在浏览器中打开 - 复制链接 - ── 登录后粘贴回调 URL ── - 回调 URL - http://localhost:1455/auth/callback?code=... - 提交回调 URL - 重新生成授权链接 - 正在准备授权... - - - Token 活动 - 每日 - 每周 - 累计 - OCGO Token 活动 - OCGO 每日 Token 使用热力图 - Token 活动 - 暂无数据 - 少 - 多 - Token 活动热力图,%1$d 天数据 - Token 活动热力图,%1$d 周数据 - 选择年份 - 最近 365 天 - 累计 token - 峰值 token - 当前连续 - 最长连续 - - %1$d 天 - - %1$s使用了%2$s token - %1$s使用了%2$s token - - 个人资料 - 昵称 - 邮箱 - 未设置 - 编辑 - 编辑个人资料 - 更换头像 - 从相册选择 - 移除头像 - 保存 - 取消 - - 活动洞察 - 总请求次数 - - %1$d 次 - - 最多请求时段 - %1$d~%2$d点 - 一月 - 二月 - 三月 - 四月 - 五月 - 六月 - 七月 - 八月 - 九月 - 十月 - 十一月 - 十二月 - API Key - Workspace ID - 正在识别站点… - API Key:仅统计该 Key 的用量 - 面板登录:全账号消费统计,并解锁 Key 管理/分组/兑换 - 账号一键登录(推荐) - 正在登录… - 网页登录(备用,如开了二次验证) - 账号密码 - API Key - 二次验证码(选填) - 暂无用量数据,下拉刷新 - 面板管理 - 加载 API Key 列表 - 暂无 API Key - 新建 Key - 兑换卡密 - 新建 API Key - Key 名称 - 分组(可选,不选为默认) - 暂无可用分组 - ✓ 新建 Key 成功 - 以下 Key 仅显示这一次,请立即复制保存: - 复制并关闭 - 兑换码 - 切换分组 - 分组 - 默认分组 - 未使用 - 最近 %1$s - %1$s(ID %2$d) - API Key 已复制 - ⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密 - (累计口径,未按所选周期过滤) - API Key 列表 - 邮箱登录 - API 登录 - 消耗 %1$d 次 - 余额 - 按模型 - 按 Key - 该周期内没有 API Key 用量 - 本周期未使用 - 消耗 $%1$.2f - 删除 API Key - 确定删除「%1$s」吗?删除后无法恢复,正在使用该 Key 的客户端会立即失效。 - 此图为账号整体趋势(无单模型维度) + + + + AI用量面板 + OpenCode Go 用量 + DeepSeek 余额,自动刷新 + 雨晴喵 + 雨晴Token + + + + + 返回 + 取消 + 确认添加 + 我知道了 + 刷新 + 设置 + 删除 + 清除 + 保存 + 跳转 + ◀ 上一页 + 下一页 ▶ + 添加小组件到桌面 + 语言 + 选择语言 + 跟随系统 + 简体中文 + 繁體中文 + English + 查看详情 + 查看用量详情 + 详细数据 + 同步用量数据 + 我已登录,保存 + 继续等待 + + + 加载中… + 未知错误 + 全部模型 + + + 👈 选择服务查看详情 + 点击左侧卡片即可 + 设置 + API 管理 + 配置 + 编辑 + 测试 + 删除凭据? + 将删除 %1$s 已保存的凭据。 + 未配置 + 最后验证:%1$s + 缓存余额:%1$s %2$s + 用量统计 + 暂无已配置服务 + %1$d 项刷新失败 + 尚未获取 + 更新于 %1$s + 本月消费 + 总额度 + 下次重置 + ✅ %1$s 连接成功:%2$s %3$s + ❌ %1$s 连接失败:%2$s + 凭据无效 + 凭据已变更 + 请求过于频繁 + 网络异常 + 服务端异常(HTTP %1$d) + 解析失败 + 凭据管理 + 关于 RainyToken + 使用小技巧 + 调试日志 + 用量详情 + 总统计 + 详细数据 + 统计图表 + + + 未配置 + 已配置 + 已过期 + 需重登 + 刷新失败 + 未获取 + 待获取 + 正常 + 未配置 · 点击添加 + 已配置 · 凭据有效 + 已过期 · 点击重新配置 + 需要重新验证 + 手动输入模式 + 凭据已配置 + 未配置凭据 + 加载中 + 最新 + 缓存 + 失败 + 待输入 + + + AI 余额一览 + 还没有配置任何服务 + 首页仅显示已配置凭据的服务 + 去设置添加 + 从未获取 + 刚刚更新 + %1$d 分钟前更新 + %1$d 小时前更新 + %1$s 更新 + 💡 长按卡片可拖拽排序 + ⚠ %1$s + + 本地加密存储,不上传任何数据 + 添加桌面小组件? + 确认后会请求桌面启动器添加雨晴Token小组件。部分系统仍会弹出系统确认窗口。 + 桌面快捷方式权限已被拒绝,请在系统设置中允许后重试 + 桌面未接受添加请求,请检查启动器权限 + 当前桌面不支持应用内添加,请在桌面长按 → 小组件 → 查找雨晴Token + + + 点击配置凭据 + 下拉刷新 + 服务当前不可用 + 本月已用 %1$s%2$s + 剩余 + · 已用 %1$s / 共 %2$s + %1$s 已用 + 5h 已用 + · %1$s + %1$s 后重置 + REST API · ¥ + WebView 抓取 · 5h 配额 + JSON API · $ + ChatGPT · Codex 额度 + Cookie 抓取 · Cloud 配额 + Cloud-IDE-JWT · IDE 额度 + OAuth · CodeBuddy 同体系积分 + 自托管面板 · 用户级 sk- Key 余额 + 明细 + Trae 凭据 + 粘贴 Trae 登录态中的 Cloud-IDE-JWT(ey… 开头)。请求头按 Authorization: Cloud-IDE-JWT 发送。 + Cloud-IDE-JWT + 区域 + CN + INTL + WorkBuddy 凭据 + 粘贴腾讯 WorkBuddy(CodeBuddy 同体系)会话的 access & refresh token。refresh token 每次使用会轮换。 + Access Token + Refresh Token + Access 与 Refresh Token 必填 + Sub2API 实例 + 该实例的任何用户:直接填分给你的 sk-… API Key 即可查余额(无需管理员)。没有 Key 也可用面板账号邮箱+密码登录。 + 实例基址 + https://api.example.com + 认证方式 + API Key(sk-…) + 或面板账号 + 邮箱 + 密码 + 实例基址与(API Key 或 邮箱+密码)必填 + + + 用量明细 + 来自 Sub2API 网关的每日与分模型用量 + 今天 + 近7天 + 近30天 + 近90天 + 全部 + 请求 + Tokens + 花费 + 每日用量 + 按模型 + 全部模型 + %1$s 次请求 · %2$s tokens + 该时间范围内暂无每日用量数据 + 该时间范围内无花费 + 模型:%1$s · 当前周期累计 + 该周期暂无模型数据 + + + 每日签到 + 每天签到领积分,连签有奖励 + 签到 + 签到中… + 签到成功 + 今日已签到 + 今日已签到(连签 %1$d 天) + 签到失败,请检查凭据 + 该服务不支持签到 + 今日已签到 + 请先添加凭据后再签到。 + INTL 区签到接口暂未支持,请用 CN 区账号 + 该设备今日签到次数已达上限 + 签到失败(code=%1$d) + 签到成功 +%1$d 积分 + + + 在打开的页面中正常登录,App 自动读取登录态;也可继续手动填 API Key + 已获取面板 Token,点击保存生效 + ✓ 已获取面板 Token + + + 天 + 小时 + 分 + + + 5 小时 + 5h + 5h 用量 + 5 小时滚动 + 本周 + 本月 + 每月 + 用量 + 用量窗口%1$s + 用量窗口 + 模型用量 + 配额 $%1$s + + + OpenCode 用量 + CommandCode 用量 + %1$d 条记录 + 点击同步 + 输入 Token + 输出 Token + 推理 Token + 总花费 + 缓存读取 %1$s + 缓存写入 %1$s + 暂无数据,点击 🔄 同步 + ✓ 新增 %1$d 条 + 部分同步完成:插入 %1$d条,%2$d页失败 + 点击切换 + 所有时间按 UTC+0 计算 + 已切换到 UTC+8,图表按北京时间显示 + 已切换到 UTC+0,图表按世界协调时显示 + 已用 $%1$s / 共 $%2$s + 剩余 $%1$s + 额外充值 + 按模型统计 + 按天统计(UTC+0) + %1$d 个模型 + %1$d 个 + 开始时间 + 结束时间 + + + 消耗金额 (USD) + API 请求次数 + Token 消耗 + 输出 + 输入(未命中) + 命中缓存 + + + 时间 + 模型 + 输入(缓存) + 输出 + 成本 + + + 全部 + 最近5小时 + 最近12小时 + 最近24小时 + 今天 + 昨天 + 最近7天 + 最近30天 + 当月 + 自定义 + 自定义天 + 自定义月 + 自定义时间 + + + 统一查询 AI 余额的小工具。\n本地加密存储凭据,不上传任何数据。 + 查看全部 %1$d 条隐藏操作 + 查看 token 刷新、网络请求等调试记录 + + + ✓ 已配置登录态(%1$d 个 Cookie) + 如何获取 Cookie? + 1. 在桌面 Chrome 打开下方登录入口 + 2. 用 GitHub / Google 登录(不会被拦截) + 3. 登录后按 F12 → Application → Cookies + 4. 复制该站点下的所有 Cookie 粘贴到上面输入框 + 格式:name1=value1; name2=value2 + 如何获取 OpenCode Go 凭据? + 1. 桌面 Chrome 打开 opencode.ai/auth + 2. 用 GitHub / Google 登录(Google 不会拦截 Chrome) + 3. 登录后跳转到 dashboard,URL 形如: + https://opencode.ai/workspace/{id}/go + 4. 复制 URL 中的 {id} 段 → workspaceId + 5. F12 → Application → Cookies + 6. 找到域名 opencode.ai 下名为 auth 的 Cookie + 7. 复制它的 Value 字段 → auth cookie + 注意:只填 auth 一个 Cookie 即可,不要整个 Cookie 字符串 + 如何获取 CommandCode 凭据? + 1. 打开 https://commandcode.ai 并登录 GitHub + ▸ 获取 API Key(必填): + 右上角头像 → Settings → API Keys → 创建一个 Key + ▸ 获取全部 Cookie(用于用量记录): + F12 → Application → Cookies → commandcode.ai + 选中所有 Cookie,复制整个 Cookie 字符串 + 粘贴到下方输入框即可,无需逐个挑选 + 提示:不填 Cookie 也能正常查看余额,只是用量记录为空。 + 如何获取 Codex auth.json? + 1. 在终端运行 codex-oauth-proxy 或 + codex-proxy,完成 ChatGPT 登录 + 2. 凭证文件保存在: + + + 已保存 + 已保存凭据 + 连接成功!余额: %1$s %2$s + 连接成功,凭据已保存 + API Key 不能为空 + JSON 格式错误 + 服务拒绝该凭据 (401/403) + 测试期间凭据已变更 + 请求过于频繁 (429) + 服务端错误 (%1$d) + 网络错误:%1$s + ,已恢复原凭据 + ,检测到凭据已变化,未执行回滚 + 测试失败。%1$s%2$s + 暂不支持测试此服务 + auth cookie 和 workspaceId 都需要填写 + Cookie 不能为空 + 凭据未配置 + 凭据未配置或已过期 + 凭据无效,请在设置中重新配置 + 凭据无效,请重新登录 + 凭据已变更,正在重新加载 + 凭据已变更,请重新操作 + 网络异常,请检查网络 + 网络异常 + 服务端异常 (HTTP %1$d) + 请求过于频繁%1$s + ,请 %1$d 秒后重试 + 不支持激活用量 + 请先选择模型 + 未抓到 Cookie,请确认已登录 + 未配置登录 URL + URL 应以 %1$s 开头 + 授权失败: %1$s + State 不匹配,请重试 + 回调缺少授权码 + Token 交换失败,请查看调试日志 + + + ✓ 用量已激活 + ✓ 激活成功 + 请求结果 + 错误: %1$s + 响应内容:\n%1$s + 模型: %1$s + 响应 ID: %1$s + 回复: + (空) + (无回复内容) + (响应解析失败,但请求已发出) + 用量: prompt=%1$s completion=%2$s total=%3$s tokens + 用量: input=%1$s output=%2$s tokens + token 刷新失败,请重新登录或重新导入 auth.json + + + 登录 %1$s + 完成登录了吗? + 如果你已经在页面上完成登录但页面没有跳转,可点击下方按钮让我抓取 Cookie 并保存登录态。 + + + 暂无日志记录\n刷新 Codex 用量后这里会出现调试信息 + + + OpenCode Go · 用量 + DeepSeek 余额 + 自动刷新 + %1$s · 额度 + %1$s · %2$s + 更新 %1$s + 暂无数据 + 刷新中… + + + 长按卡片拖拽排序 + 长按首页卡片可以拖拽排序 + 在首页长按任意服务卡片即可拖拽到想要的位置,排序会自动保存。 + 小组件点左上角进 APP + 小组件左上角标识可打开 APP + 桌面小组件左上角 RainyToken › 标识可以点击打开应用,其它区域点击切换服务。 + 图表点击查看详情 + 点击图表柱子可查看时段详情 + 在用量图表上点击或滑动手指,可查看对应时段的详细数值。 + 图表标题可点击 + 点击图表标题弹出按模型明细 + 图表卡片标题(如「消耗金额」)点击后会弹出按模型拆分的明细。 + 切换图表时区 + 用量详情页可切换 UTC+0 / UTC+8 + 用量详情页顶部可切换 UTC+0 / UTC+8,图表标签会跟随切换。 + CCGO 清除并重新同步 + CCGO 详情页可清除缓存重新同步 + CCGO 用量详情页右上角「清除」可删除本地缓存并重新拉取全部数据。 + 查看用量详情 + 服务卡片底部可进入用量图表 + 有凭据的服务卡片底部会出现「查看用量详情」入口,点击进入图表统计。 + 原始数据浏览 + 用量详情页右上角可查看原始记录 + 用量详情页右上角「详细数据」可查看每条原始调用记录,支持分页跳转。 + 下拉刷新联动用量 + 下拉刷新会同步余额和用量 + 首页下拉刷新不仅刷新余额,还会自动同步 OCGO / CCGO 用量记录。 + 凭据获取教程 + 凭据页底部有获取教程 + 凭据编辑页底部的「如何获取」按钮有针对每个服务的详细操作步骤。 + 剪贴板自动导入 + Codex 支持从剪贴板一键导入 + Codex 凭据页支持「从剪贴板自动导入」,粘贴 auth.json 后一键填入。 + 详细数据页码跳转 + 原始数据页可输入页码直跳 + 原始数据页底部可输入页码直接跳转,数据量大时不必逐页翻。 + 小组件自动刷新 + 划到小组件会自动刷新余额 + 桌面小组件划到负一屏时会自动刷新余额,也可手动点 ↻ 按钮刷新。 + + + %1$d次 + 选择日期 + 选择月份(任意一天) + 日期范围 + 开始日期 + 结束日期 + 消费明细 + 调用次数明细 + Token 明细 + 清除数据 + 此操作将清除所有 CommandCode 用量本地缓存并重新同步,需要几秒钟完成。 + 是否继续? + 确认(%1$ds) + 确认清除 + 关闭 + 从 %1$s + 至 %1$s + 应用自定义范围 + 确定 + 结束日期不能早于开始日期 %1$s + 时间 %1$s:%2$s + 暂无数据 + 合计: %1$s + 总计 + 记录数 + 缓存读取 + 缓存写入 + 缓存 Token 细分 + ※ 缓存读取已计入「输入 Token」 + %1$d 次调用 + MM月dd日 EEEE + %1$d 次 · $%2$s + 原始数据 + 缓存写入(5m) + 缓存写入(1h) + 成本(USD) + 成本(Raw) + + + ~/.config/codex-oauth-proxy/auth.json + 3. 用文本编辑器打开该文件, + 复制全部内容粘贴到上方输入框 + JSON 格式示例: + {"tokens": { + "access_token": "...", + "refresh_token": "...", + "expiresAt": 1783727108252 + }} + 提示:导入后 APP 会自动刷新 token, + 不需要手动更新。 + Ollama 凭据 + 需要从浏览器复制完整 Cookie。登录 ollama.com/settings 后,在 DevTools → Network → 请求头里找到 Cookie 行复制。 + 更新凭据 + 保存凭据 + 测试并保存 + 在浏览器中打开 Ollama Settings + 复制登录 URL 到剪贴板 + 一键激活用量 API Key(可选) + 填入 Ollama Cloud API Key 后,可在详情页一键发送请求激活用量统计。从 ollama.com/settings → API Keys 获取。 + ollama-xxx 或 sk-xxx + 保存 API Key + OpenCode Go 凭据 + 需要 auth cookie + workspaceId 两个值。APP 会用 OkHttp 抓 dashboard 并自动解析 5h/周/月 三档用量。 + 从剪贴板自动导入 + dashboard URL 中的 {id} 段 + auth Cookie 值 + F12 → Cookies → opencode.ai → auth 的 Value + 如何获取这两个值? + 在浏览器中打开登录入口 + 填入 OpenCode Zen API Key 后,可在详情页一键发送请求激活用量统计。从 opencode.ai/settings → API Keys 获取。 + CommandCode 凭据 + 需要 API Key(拉余额)。Session Cookie(可选)在浏览器 DevTools 复制,用于拉用量记录。不填也可正常查看余额。 + 从 Settings → API Keys 生成 + Session Cookie(可选) + F12 → Cookies → commandcode.ai → 全选复制粘贴即可 + 如何获取 API Key 和 Cookie? + 请输入 API Key + 凭据会使用 Android Keystore (AES-256 GCM) 加密后存储到本机 DataStore。 + 更新 + Codex / ChatGPT 凭据 + 推荐使用 OAuth 登录自动获取凭据。也可手动粘贴 auth.json 内容。 + 🔐 OAuth 登录(推荐) + ── 或手动导入 ── + auth.json 完整内容 + 粘贴 {"tokens": {...}} 完整 JSON + 导入并保存 + 如何获取 auth.json? + 粘贴登录态 + Google OAuth 会拒绝 APP 内置 WebView,所以请用浏览器登录后把 Cookie 粘到这里。 + Cookie 字符串 + 保存 Cookie + + + 响应体为空 + 响应根节点不是 JSON 对象 + 未找到用量窗口数据 + 未找到模型列表 + 模型列表为空 + 响应格式异常 + 每周 + Spark + 模型调用次数 + 缓存数据 · %1$s + 显示的是上次成功获取的余额 + 本月消费 %1$s%2$s + 余额构成 + 赠送余额 + 自费充值 + 加载模型列表... + 选择模型 + 刷新模型列表 + 激活中... + 重试激活用量 + ⚡ 一键激活用量 + 刷新余额 + 重新登录 + 配置凭据 + 充值 + 当前余额 + 5h 实时用量 + 月度余额 + Session 用量 + 请选择模型 + 错误详情: + API 响应内容: + 复制 + *** (长度 %1$d) *** + %1$s***%2$s (长度 %3$d) + Cookie 格式错误,应为 name1=value1; name2=value2 + 已保存 %1$d 个 Cookie + 请粘贴 auth.json 内容 + auth.json 缺少 access_token 或 refresh_token + 已保存 Codex 凭据,token 到期后会自动刷新 + 解析失败:%1$s + 连接成功!余额: $%1$s + API Key 已保存 + 连接成功!Session: %1$s%% · %2$s + 凭据已删除 + 剪贴板为空 + 已识别 workspaceId 和 auth cookie + 已识别 workspaceId,请再粘贴 auth cookie + 已识别 auth cookie,请再粘贴 workspaceId + 未识别到有效凭据 + + + Codex OAuth 登录 + 正在交换 Token... + 重试 + 无头模式登录 + 1. 点击「在浏览器中打开」或复制下方链接到浏览器\n2. 在 OpenAI 页面完成登录和授权\n3. 浏览器会跳转到一个 localhost 地址(页面打不开是正常的)\n4. 复制浏览器地址栏的完整 URL 粘贴到下方输入框\n5. 点击「提交回调 URL」 + 授权链接 + 在浏览器中打开 + 复制链接 + ── 登录后粘贴回调 URL ── + 回调 URL + http://localhost:1455/auth/callback?code=... + 提交回调 URL + 重新生成授权链接 + 正在准备授权... + + + Token 活动 + 每日 + 每周 + 累计 + OCGO Token 活动 + OCGO 每日 Token 使用热力图 + Token 活动 + 暂无数据 + 少 + 多 + Token 活动热力图,%1$d 天数据 + Token 活动热力图,%1$d 周数据 + 选择年份 + 最近 365 天 + 累计 token + 峰值 token + 当前连续 + 最长连续 + + %1$d 天 + + %1$s使用了%2$s token + %1$s使用了%2$s token + + 个人资料 + 昵称 + 邮箱 + 未设置 + 编辑 + 编辑个人资料 + 更换头像 + 从相册选择 + 移除头像 + 保存 + 取消 + + 活动洞察 + 总请求次数 + + %1$d 次 + + 最多请求时段 + %1$d~%2$d点 + 一月 + 二月 + 三月 + 四月 + 五月 + 六月 + 七月 + 八月 + 九月 + 十月 + 十一月 + 十二月 + API Key + Workspace ID + 正在识别站点… + API Key:仅统计该 Key 的用量 + 面板登录:全账号消费统计,并解锁 Key 管理/分组/兑换 + 账号一键登录(推荐) + 正在登录… + 网页登录(备用,如开了二次验证) + 账号密码 + API Key + 二次验证码(选填) + 暂无用量数据,下拉刷新 + 面板管理 + 加载 API Key 列表 + 暂无 API Key + 新建 Key + 兑换卡密 + 新建 API Key + Key 名称 + 分组(可选,不选为默认) + 暂无可用分组 + ✓ 新建 Key 成功 + 以下 Key 仅显示这一次,请立即复制保存: + 复制并关闭 + 兑换码 + 切换分组 + 分组 + 默认分组 + 未使用 + 最近 %1$s + %1$s(ID %2$d) + API Key 已复制 + ⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密 + (累计口径,未按所选周期过滤) + API Key 列表 + 邮箱登录 + API 登录 + 消耗 %1$d 次 + 余额 + 按模型 + 按 Key + 该周期内没有 API Key 用量 + 本周期未使用 + 消耗 $%1$.2f + 删除 API Key + 确定删除「%1$s」吗?删除后无法恢复,正在使用该 Key 的客户端会立即失效。 + 此图为账号整体趋势(无单模型维度) \ No newline at end of file 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 7eeb549..c36fa5b 100644 --- a/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/app/src/main/res/values-b+zh+Hant/strings.xml @@ -1,688 +1,699 @@ - - - - AI用量面板 - OpenCode Go 用量 + DeepSeek 餘額,自動重新整理 - 雨晴喵 - 雨晴Token - - - - - 返回 - 取消 - 確認新增 - 我知道了 - 重新整理 - 設定 - 刪除 - 清除 - 儲存 - 跳轉 - ◀ 上一頁 - 下一頁 ▶ - 新增小工具到桌面 - 語言 - 選擇語言 - 跟隨系統 - 簡體中文 - 繁體中文 - English - 檢視詳情 - 檢視用量詳情 - 詳細數據 - 同步用量數據 - 我已登入,儲存 - 繼續等待 - - - 載入中… - 未知錯誤 - 全部模型 - - - 👈 選擇服務檢視詳情 - 點選左側卡片即可 - 設定 - API 管理 - 設定 - 編輯 - 測試 - 刪除憑證? - 將刪除 %1$s 已儲存的憑證。 - 未設定 - 最後驗證:%1$s - 快取餘額:%1$s %2$s - 用量統計 - 尚無已設定服務 - %1$d 項刷新失敗 - 尚未取得 - 更新於 %1$s - 本月消費 - 總額度 - 下次重置 - ✅ %1$s 連線成功:%2$s %3$s - ❌ %1$s 連線失敗:%2$s - 憑證無效 - 憑證已變更 - 請求過於頻繁 - 網路異常 - 伺服器異常(HTTP %1$d) - 解析失敗 - 憑據管理 - 關於 RainyToken - 使用小技巧 - 除錯日誌 - 用量詳情 - 總統計 - 詳細數據 - 統計圖表 - - - 未配置 - 已配置 - 已過期 - 需重登 - 重新整理失敗 - 未獲取 - 待獲取 - 正常 - 未配置 · 點選新增 - 已配置 · 憑據有效 - 已過期 · 點選重新配置 - 需要重新驗證 - 手動輸入模式 - 憑據已配置 - 未配置憑據 - 載入中 - 最新 - 快取 - 失敗 - 待輸入 - - - AI 餘額一覽 - 尚未設定任何服務 - 首頁僅顯示已設定憑證的服務 - 前往設定新增 - 從未獲取 - 剛剛更新 - %1$d 分鐘前更新 - %1$d 小時前更新 - %1$s 更新 - 💡 長按卡片可拖曳排序 - ⚠ %1$s - - 本地加密儲存,不上傳任何數據 - 新增桌面小工具? - 確認後會請求桌面啟動器新增雨晴Token小工具。部分系統仍會彈出系統確認視窗。 - 桌面快捷方式權限已被拒絕,請在系統設定中允許後重試 - 桌面未接受新增請求,請檢查啟動器權限 - 當前桌面不支援應用內新增,請在桌面長按 → 小工具 → 查詢雨晴Token - - - 點選配置憑據 - 下拉重新整理 - 服務當前不可用 - 本月已用 %1$s%2$s - 剩餘 - · 已用 %1$s / 共 %2$s - %1$s 已用 - 5h 已用 - · %1$s - %1$s 後重置 - REST API · ¥ - WebView 抓取 · 5h 配額 - JSON API · $ - ChatGPT · Codex 額度 - Cookie 抓取 · Cloud 配額 - Cloud-IDE-JWT · IDE 額度 - OAuth · CodeBuddy 同體系積分 - 自托管面板 · 用戶級 sk- Key 餘額 - 明細 - Trae 憑證 - 貼上 Trae 登入態中的 Cloud-IDE-JWT(ey… 開頭)。請求頭以 Authorization: Cloud-IDE-JWT 送出。 - Cloud-IDE-JWT - 區域 - CN - INTL - WorkBuddy 憑證 - 貼上騰訊 WorkBuddy(CodeBuddy 同體系)工作階段的 access & refresh token。refresh token 每次使用會輪換。 - Access Token - Refresh Token - Access 與 Refresh Token 必填 - Sub2API 實例 - 該實例的任何使用者:直接填分配給你的 sk-… API Key 即可查餘額(無需管理員)。沒有 Key 也可用面板帳號信箱+密碼登入。 - 實例基址 - https://api.example.com - 認證方式 - API Key(sk-…) - 或面板帳號 - 信箱 - 密碼 - 實例基址與(API Key 或 信箱+密碼)必填 - - - 用量明細 - 來自 Sub2API 閘道的每日與分模型用量 - 今天 - 近7天 - 近30天 - 近90天 - 全部 - 請求 - Tokens - 花費 - 每日用量 - 按模型 - 全部模型 - %1$s 次請求 · %2$s tokens - 該時間範圍內暫無每日用量數據 - 該時間範圍內無花費 - 模型:%1$s · 當前週期累計 - 該週期暫無模型數據 - - - 每日簽到 - 每天簽到領積分,連簽有獎勵 - 簽到 - 簽到中… - - - 在開啟的頁面中正常登入,App 自動讀取登入態;也可繼續手動填 API Key - 已擷取面板 Token,點擊儲存生效 - ✓ 已擷取面板 Token - - - 天 - 小時 - 分 - - - 5 小時 - 5h - 5h 用量 - 5 小時滾動 - 本週 - 本月 - 每月 - 用量 - 用量視窗%1$s - 用量視窗 - 模型用量 - 配額 $%1$s - - - OpenCode 用量 - CommandCode 用量 - %1$d 條記錄 - 點選同步 - 輸入 Token - 輸出 Token - 推理 Token - 總花費 - 快取讀取 %1$s - 快取寫入 %1$s - 暫無數據,點選 🔄 同步 - ✓ 新增 %1$d 條 - 部分同步完成:插入 %1$d條,%2$d頁失敗 - 點選切換 - 所有時間按 UTC+0 計算 - 已切換到 UTC+8,圖表按北京時間顯示 - 已切換到 UTC+0,圖表按世界協調時顯示 - 已用 $%1$s / 共 $%2$s - 剩餘 $%1$s - 額外充值 - 按模型統計 - 按天統計(UTC+0) - %1$d 個模型 - %1$d 個 - 開始時間 - 結束時間 - - - 消耗金額 (USD) - API 請求次數 - Token 消耗 - 輸出 - 輸入(未命中) - 命中快取 - - - 時間 - 模型 - 輸入(快取) - 輸出 - 成本 - - - 全部 - 最近5小時 - 最近12小時 - 最近24小時 - 今天 - 昨天 - 最近7天 - 最近30天 - 當月 - 自定義 - 自定義天 - 自定義月 - 自定義時間 - - - 統一查詢 AI 餘額的小工具。\n本地加密儲存憑據,不上傳任何數據。 - 檢視全部 %1$d 條隱藏操作 - 檢視 token 重新整理、網路請求等除錯記錄 - - - ✓ 已配置登入態(%1$d 個 Cookie) - 如何獲取 Cookie? - 1. 在桌面 Chrome 開啟下方登入入口 - 2. 用 GitHub / Google 登入(不會被攔截) - 3. 登入後按 F12 → Application → Cookies - 4. 複製該站點下的所有 Cookie 貼上到上面輸入框 - 格式:name1=value1; name2=value2 - 如何獲取 OpenCode Go 憑據? - 1. 桌面 Chrome 開啟 opencode.ai/auth - 2. 用 GitHub / Google 登入(Google 不會攔截 Chrome) - 3. 登入後跳轉到 dashboard,URL 形如: - https://opencode.ai/workspace/{id}/go - 4. 複製 URL 中的 {id} 段 → workspaceId - 5. F12 → Application → Cookies - 6. 找到域名 opencode.ai 下名為 auth 的 Cookie - 7. 複製它的 Value 欄位 → auth cookie - 注意:只填 auth 一個 Cookie 即可,不要整個 Cookie 字串 - 如何獲取 CommandCode 憑據? - 1. 開啟 https://commandcode.ai 並登入 GitHub - ▸ 獲取 API Key(必填): - 右上角頭像 → Settings → API Keys → 建立一個 Key - ▸ 獲取全部 Cookie(用於用量記錄): - F12 → Application → Cookies → commandcode.ai - 選中所有 Cookie,複製整個 Cookie 字串 - 貼上到下方輸入框即可,無需逐個挑選 - 提示:不填 Cookie 也能正常檢視餘額,只是用量記錄為空。 - 如何獲取 Codex auth.json? - 1. 在終端執行 codex-oauth-proxy 或 - codex-proxy,完成 ChatGPT 登入 - 2. 憑證檔案儲存在: - - - 已儲存 - 已儲存憑據 - 連線成功!餘額: %1$s %2$s - 連線成功,憑據已儲存 - API Key 不能為空 - JSON 格式錯誤 - 服務拒絕該憑據 (401/403) - 測試期間憑據已變更 - 請求過於頻繁 (429) - 服務端錯誤 (%1$d) - 網路錯誤:%1$s - ,已恢復原憑據 - ,檢測到憑據已變化,未執行回滾 - 測試失敗。%1$s%2$s - 暫不支援測試此服務 - auth cookie 和 workspaceId 都需要填寫 - Cookie 不能為空 - 憑據未配置 - 憑據未配置或已過期 - 憑據無效,請在設定中重新配置 - 憑據無效,請重新登入 - 憑據已變更,正在重新載入 - 憑據已變更,請重新操作 - 網路異常,請檢查網路 - 網路異常 - 服務端異常 (HTTP %1$d) - 請求過於頻繁%1$s - ,請 %1$d 秒後重試 - 不支援啟用用量 - 請先選擇模型 - 未抓到 Cookie,請確認已登入 - 未配置登入 URL - URL 應以 %1$s 開頭 - 授權失敗: %1$s - State 不匹配,請重試 - 回撥缺少授權碼 - Token 交換失敗,請檢視除錯日誌 - - - ✓ 用量已啟用 - ✓ 啟用成功 - 請求結果 - 錯誤: %1$s - 響應內容:\n%1$s - 模型: %1$s - 響應 ID: %1$s - 回覆: - (空) - (無回覆內容) - (響應解析失敗,但請求已發出) - 用量: prompt=%1$s completion=%2$s total=%3$s tokens - 用量: input=%1$s output=%2$s tokens - token 重新整理失敗,請重新登入或重新匯入 auth.json - - - 登入 %1$s - 完成登入了嗎? - 如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。 - - - 暫無日誌記錄\n重新整理 Codex 用量後這裡會出現除錯資訊 - - - OpenCode Go · 用量 - DeepSeek 餘額 - 自動重新整理 - %1$s · 額度 - %1$s · %2$s - 更新 %1$s - 暫無數據 - 重新整理中… - - - 長按卡片拖曳排序 - 長按首頁卡片可以拖曳排序 - 在首頁長按任意服務卡片即可拖曳到想要的位置,排序會自動儲存。 - 小工具點左上角進 APP - 小工具左上角標識可開啟 APP - 桌面小工具左上角 RainyToken › 標識可以點選開啟應用,其它區域點選切換服務。 - 圖表點選檢視詳情 - 點選圖表柱子可檢視時段詳情 - 在用量圖表上點選或滑動手指,可檢視對應時段的詳細數值。 - 圖表標題可點選 - 點選圖表標題彈出按模型明細 - 圖表卡片標題(如「消耗金額」)點選後會彈出按模型拆分的明細。 - 切換圖表時區 - 用量詳情頁可切換 UTC+0 / UTC+8 - 用量詳情頁頂部可切換 UTC+0 / UTC+8,圖表標籤會跟隨切換。 - CCGO 清除並重新同步 - CCGO 詳情頁可清除快取重新同步 - CCGO 用量詳情頁右上角「清除」可刪除本地快取並重新拉取全部數據。 - 檢視用量詳情 - 服務卡片底部可進入用量圖表 - 有憑據的服務卡片底部會出現「檢視用量詳情」入口,點選進入圖表統計。 - 原始數據瀏覽 - 用量詳情頁右上角可檢視原始記錄 - 用量詳情頁右上角「詳細數據」可檢視每條原始呼叫記錄,支援分頁跳轉。 - 下拉重新整理聯動用量 - 下拉重新整理會同步餘額和用量 - 首頁下拉重新整理不僅重新整理餘額,還會自動同步 OCGO / CCGO 用量記錄。 - 憑據獲取教程 - 憑據頁底部有獲取教程 - 憑據編輯頁底部的「如何獲取」按鈕有針對每個服務的詳細操作步驟。 - 剪貼簿自動匯入 - Codex 支援從剪貼簿一鍵匯入 - Codex 憑據頁支援「從剪貼簿自動匯入」,貼上 auth.json 後一鍵填入。 - 詳細數據頁碼跳轉 - 原始數據頁可輸入頁碼直跳 - 原始數據頁底部可輸入頁碼直接跳轉,數據量大時不必逐頁翻。 - 小工具自動重新整理 - 劃到小工具會自動重新整理餘額 - 桌面小工具劃到負一屏時會自動重新整理餘額,也可手動點 ↻ 按鈕重新整理。 - - - %1$d次 - 選擇日期 - 選擇月份(任意一天) - 日期範圍 - 開始日期 - 結束日期 - 消費明細 - 呼叫次數明細 - Token 明細 - 清除數據 - 此操作將清除所有 CommandCode 用量本地快取並重新同步,需要幾秒鐘完成。 - 是否繼續? - 確認(%1$ds) - 確認清除 - 關閉 - 從 %1$s - 至 %1$s - 應用自定義範圍 - 確定 - 結束日期不能早於開始日期 %1$s - 時間 %1$s:%2$s - 暫無數據 - 合計: %1$s - 總計 - 記錄數 - 快取讀取 - 快取寫入 - 快取 Token 細分 - ※ 快取讀取已計入「輸入 Token」 - %1$d 次呼叫 - MM月dd日 EEEE - %1$d 次 · $%2$s - 原始數據 - 快取寫入(5m) - 快取寫入(1h) - 成本(USD) - 成本(Raw) - - - ~/.config/codex-oauth-proxy/auth.json - 3. 用文字編輯器開啟該檔案, - 複製全部內容貼上到上方輸入框 - JSON 格式示例: - {"tokens": { - "access_token": "...", - "refresh_token": "...", - "expiresAt": 1783727108252 - }} - 提示:匯入後 APP 會自動重新整理 token, - 不需要手動更新。 - Ollama 憑據 - 需要從瀏覽器複製完整 Cookie。登入 ollama.com/settings 後,在 DevTools → Network → 請求頭裡找到 Cookie 行複製。 - 更新憑據 - 儲存憑據 - 測試並儲存 - 在瀏覽器中開啟 Ollama Settings - 複製登入 URL 到剪貼簿 - 一鍵啟用用量 API Key(可選) - 填入 Ollama Cloud API Key 後,可在詳情頁一鍵傳送請求啟用用量統計。從 ollama.com/settings → API Keys 獲取。 - ollama-xxx 或 sk-xxx - 儲存 API Key - OpenCode Go 憑據 - 需要 auth cookie + workspaceId 兩個值。APP 會用 OkHttp 抓 dashboard 並自動解析 5h/周/月 三檔用量。 - 從剪貼簿自動匯入 - dashboard URL 中的 {id} 段 - auth Cookie 值 - F12 → Cookies → opencode.ai → auth 的 Value - 如何獲取這兩個值? - 在瀏覽器中開啟登入入口 - 填入 OpenCode Zen API Key 後,可在詳情頁一鍵傳送請求啟用用量統計。從 opencode.ai/settings → API Keys 獲取。 - CommandCode 憑據 - 需要 API Key(拉餘額)。Session Cookie(可選)在瀏覽器 DevTools 複製,用於拉用量記錄。不填也可正常檢視餘額。 - 從 Settings → API Keys 生成 - Session Cookie(可選) - F12 → Cookies → commandcode.ai → 全選複製貼上即可 - 如何獲取 API Key 和 Cookie? - 請輸入 API Key - 憑據會使用 Android Keystore (AES-256 GCM) 加密後儲存到本機 DataStore。 - 更新 - Codex / ChatGPT 憑據 - 推薦使用 OAuth 登入自動獲取憑據。也可手動貼上 auth.json 內容。 - 🔐 OAuth 登入(推薦) - ── 或手動匯入 ── - auth.json 完整內容 - 貼上 {"tokens": {...}} 完整 JSON - 匯入並儲存 - 如何獲取 auth.json? - 貼上登入態 - Google OAuth 會拒絕 APP 內建 WebView,所以請用瀏覽器登入後把 Cookie 粘到這裡。 - Cookie 字串 - 儲存 Cookie - - - 回應體為空 - 回應根節點不是 JSON 物件 - 未找到用量視窗資料 - 未找到模型清單 - 模型清單為空 - 回應格式異常 - 每週 - Spark - 模型呼叫次數 - 快取數據 · %1$s - 顯示的是上次成功獲取的餘額 - 本月消費 %1$s%2$s - 餘額構成 - 贈送餘額 - 自費充值 - 載入模型列表... - 選擇模型 - 重新整理模型列表 - 啟用中... - 重試啟用用量 - ⚡ 一鍵啟用用量 - 重新整理餘額 - 重新登入 - 配置憑據 - 儲值 - 當前餘額 - 5h 實時用量 - 月度餘額 - Session 用量 - 請選擇模型 - 錯誤詳情: - API 響應內容: - 複製 - *** (長度 %1$d) *** - %1$s***%2$s (長度 %3$d) - Cookie 格式錯誤,應為 name1=value1; name2=value2 - 已儲存 %1$d 個 Cookie - 請貼上 auth.json 內容 - auth.json 缺少 access_token 或 refresh_token - 已儲存 Codex 憑據,token 到期後會自動重新整理 - 解析失敗:%1$s - 連線成功!餘額: $%1$s - API Key 已儲存 - 連線成功!Session: %1$s%% · %2$s - 憑據已刪除 - 剪貼簿為空 - 已識別 workspaceId 和 auth cookie - 已識別 workspaceId,請再貼上 auth cookie - 已識別 auth cookie,請再貼上 workspaceId - 未識別到有效憑據 - - - Codex OAuth 登入 - 正在交換 Token... - 重試 - 無頭模式登入 - 1. 點選「在瀏覽器中開啟」或複製下方連結到瀏覽器\n2. 在 OpenAI 頁面完成登入和授權\n3. 瀏覽器會跳轉到一個 localhost 地址(頁面打不開是正常的)\n4. 複製瀏覽器位址列的完整 URL 貼上到下方輸入框\n5. 點選「提交回撥 URL」 - 授權連結 - 在瀏覽器中開啟 - 複製連結 - ── 登入後貼上回撥 URL ── - 回撥 URL - http://localhost:1455/auth/callback?code=... - 提交回撥 URL - 重新生成授權連結 - 正在準備授權... - - - Token 活動 - 每日 - 每週 - 累計 - OCGO Token 活動 - OCGO 每日 Token 使用熱力圖 - Token 活動 - 暫無資料 - 少 - 多 - Token 活動熱力圖,%1$d 天資料 - Token 活動熱力圖,%1$d 週資料 - 選擇年份 - 最近 365 天 - 累計 Token - 峰值 Token - 當前連續 - 最長連續 - - %1$d 天 - - %1$s使用了%2$s Token - %1$s使用了%2$s Token - - 個人資料 - 暱稱 - 郵箱 - 未設定 - 編輯 - 編輯個人資料 - 更換頭像 - 從相冊選擇 - 移除頭像 - 儲存 - 取消 - - 活動洞察 - 總請求次數 - - %1$d 次 - - 最多請求時段 - %1$d~%2$d點 - 一月 - 二月 - 三月 - 四月 - 五月 - 六月 - 七月 - 八月 - 九月 - 十月 - 十一月 - 十二月 - API Key - Workspace ID - 正在識別站點… - API Key:僅統計該 Key 的用量 - 面板登錄:全帳號消費統計,並解鎖 Key 管理/分組/兌換 - 帳號一鍵登錄(推薦) - 正在登錄… - 網頁登錄(備用,如開了二次驗證) - 帳號密碼 - API Key - 二次驗證碼(選填) - 暫無用量數據,下拉刷新 - 面板管理 - 載入 API Key 列表 - 暫無 API Key - 新建 Key - 兌換卡密 - 新建 API Key - Key 名稱 - 分組(可選,不選為預設) - 暫無可用分組 - ✓ 新建 Key 成功 - 以下 Key 僅顯示這一次,請立即複製保存: - 複製並關閉 - 兌換碼 - 切換分組 - 分組 - 預設分組 - 未使用 - 最近 %1$s - %1$s(ID %2$d) - API Key 已複製 - ⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密 - (累計口徑,未按所選週期過濾) - API Key 列表 - 信箱登錄 - API 登錄 - 消耗 %1$d 次 - 餘額 - 按模型 - 按 Key - 該週期內沒有 API Key 用量 - 本週期未使用 - 消耗 $%1$.2f - 刪除 API Key - 確定刪除「%1$s」嗎?刪除後無法復原,正在使用該 Key 的用戶端會立即失效。 - 此圖為帳號整體趨勢(無單模型維度) + + + + AI用量面板 + OpenCode Go 用量 + DeepSeek 餘額,自動重新整理 + 雨晴喵 + 雨晴Token + + + + + 返回 + 取消 + 確認新增 + 我知道了 + 重新整理 + 設定 + 刪除 + 清除 + 儲存 + 跳轉 + ◀ 上一頁 + 下一頁 ▶ + 新增小工具到桌面 + 語言 + 選擇語言 + 跟隨系統 + 簡體中文 + 繁體中文 + English + 檢視詳情 + 檢視用量詳情 + 詳細數據 + 同步用量數據 + 我已登入,儲存 + 繼續等待 + + + 載入中… + 未知錯誤 + 全部模型 + + + 👈 選擇服務檢視詳情 + 點選左側卡片即可 + 設定 + API 管理 + 設定 + 編輯 + 測試 + 刪除憑證? + 將刪除 %1$s 已儲存的憑證。 + 未設定 + 最後驗證:%1$s + 快取餘額:%1$s %2$s + 用量統計 + 尚無已設定服務 + %1$d 項刷新失敗 + 尚未取得 + 更新於 %1$s + 本月消費 + 總額度 + 下次重置 + ✅ %1$s 連線成功:%2$s %3$s + ❌ %1$s 連線失敗:%2$s + 憑證無效 + 憑證已變更 + 請求過於頻繁 + 網路異常 + 伺服器異常(HTTP %1$d) + 解析失敗 + 憑據管理 + 關於 RainyToken + 使用小技巧 + 除錯日誌 + 用量詳情 + 總統計 + 詳細數據 + 統計圖表 + + + 未配置 + 已配置 + 已過期 + 需重登 + 重新整理失敗 + 未獲取 + 待獲取 + 正常 + 未配置 · 點選新增 + 已配置 · 憑據有效 + 已過期 · 點選重新配置 + 需要重新驗證 + 手動輸入模式 + 憑據已配置 + 未配置憑據 + 載入中 + 最新 + 快取 + 失敗 + 待輸入 + + + AI 餘額一覽 + 尚未設定任何服務 + 首頁僅顯示已設定憑證的服務 + 前往設定新增 + 從未獲取 + 剛剛更新 + %1$d 分鐘前更新 + %1$d 小時前更新 + %1$s 更新 + 💡 長按卡片可拖曳排序 + ⚠ %1$s + + 本地加密儲存,不上傳任何數據 + 新增桌面小工具? + 確認後會請求桌面啟動器新增雨晴Token小工具。部分系統仍會彈出系統確認視窗。 + 桌面快捷方式權限已被拒絕,請在系統設定中允許後重試 + 桌面未接受新增請求,請檢查啟動器權限 + 當前桌面不支援應用內新增,請在桌面長按 → 小工具 → 查詢雨晴Token + + + 點選配置憑據 + 下拉重新整理 + 服務當前不可用 + 本月已用 %1$s%2$s + 剩餘 + · 已用 %1$s / 共 %2$s + %1$s 已用 + 5h 已用 + · %1$s + %1$s 後重置 + REST API · ¥ + WebView 抓取 · 5h 配額 + JSON API · $ + ChatGPT · Codex 額度 + Cookie 抓取 · Cloud 配額 + Cloud-IDE-JWT · IDE 額度 + OAuth · CodeBuddy 同體系積分 + 自托管面板 · 用戶級 sk- Key 餘額 + 明細 + Trae 憑證 + 貼上 Trae 登入態中的 Cloud-IDE-JWT(ey… 開頭)。請求頭以 Authorization: Cloud-IDE-JWT 送出。 + Cloud-IDE-JWT + 區域 + CN + INTL + WorkBuddy 憑證 + 貼上騰訊 WorkBuddy(CodeBuddy 同體系)工作階段的 access & refresh token。refresh token 每次使用會輪換。 + Access Token + Refresh Token + Access 與 Refresh Token 必填 + Sub2API 實例 + 該實例的任何使用者:直接填分配給你的 sk-… API Key 即可查餘額(無需管理員)。沒有 Key 也可用面板帳號信箱+密碼登入。 + 實例基址 + https://api.example.com + 認證方式 + API Key(sk-…) + 或面板帳號 + 信箱 + 密碼 + 實例基址與(API Key 或 信箱+密碼)必填 + + + 用量明細 + 來自 Sub2API 閘道的每日與分模型用量 + 今天 + 近7天 + 近30天 + 近90天 + 全部 + 請求 + Tokens + 花費 + 每日用量 + 按模型 + 全部模型 + %1$s 次請求 · %2$s tokens + 該時間範圍內暫無每日用量數據 + 該時間範圍內無花費 + 模型:%1$s · 當前週期累計 + 該週期暫無模型數據 + + + 每日簽到 + 每天簽到領積分,連簽有獎勵 + 簽到 + 簽到中… + 簽到成功 + 今日已簽到 + 今日已簽到(連簽 %1$d 天) + 簽到失敗,請檢查憑證 + 該服務不支援簽到 + 今日已簽到 + 請先新增憑證後再簽到。 + INTL 區簽到介面暫未支援,請用 CN 區帳號 + 該裝置今日簽到次數已達上限 + 簽到失敗(code=%1$d) + 簽到成功 +%1$d 積分 + + + 在開啟的頁面中正常登入,App 自動讀取登入態;也可繼續手動填 API Key + 已擷取面板 Token,點擊儲存生效 + ✓ 已擷取面板 Token + + + 天 + 小時 + 分 + + + 5 小時 + 5h + 5h 用量 + 5 小時滾動 + 本週 + 本月 + 每月 + 用量 + 用量視窗%1$s + 用量視窗 + 模型用量 + 配額 $%1$s + + + OpenCode 用量 + CommandCode 用量 + %1$d 條記錄 + 點選同步 + 輸入 Token + 輸出 Token + 推理 Token + 總花費 + 快取讀取 %1$s + 快取寫入 %1$s + 暫無數據,點選 🔄 同步 + ✓ 新增 %1$d 條 + 部分同步完成:插入 %1$d條,%2$d頁失敗 + 點選切換 + 所有時間按 UTC+0 計算 + 已切換到 UTC+8,圖表按北京時間顯示 + 已切換到 UTC+0,圖表按世界協調時顯示 + 已用 $%1$s / 共 $%2$s + 剩餘 $%1$s + 額外充值 + 按模型統計 + 按天統計(UTC+0) + %1$d 個模型 + %1$d 個 + 開始時間 + 結束時間 + + + 消耗金額 (USD) + API 請求次數 + Token 消耗 + 輸出 + 輸入(未命中) + 命中快取 + + + 時間 + 模型 + 輸入(快取) + 輸出 + 成本 + + + 全部 + 最近5小時 + 最近12小時 + 最近24小時 + 今天 + 昨天 + 最近7天 + 最近30天 + 當月 + 自定義 + 自定義天 + 自定義月 + 自定義時間 + + + 統一查詢 AI 餘額的小工具。\n本地加密儲存憑據,不上傳任何數據。 + 檢視全部 %1$d 條隱藏操作 + 檢視 token 重新整理、網路請求等除錯記錄 + + + ✓ 已配置登入態(%1$d 個 Cookie) + 如何獲取 Cookie? + 1. 在桌面 Chrome 開啟下方登入入口 + 2. 用 GitHub / Google 登入(不會被攔截) + 3. 登入後按 F12 → Application → Cookies + 4. 複製該站點下的所有 Cookie 貼上到上面輸入框 + 格式:name1=value1; name2=value2 + 如何獲取 OpenCode Go 憑據? + 1. 桌面 Chrome 開啟 opencode.ai/auth + 2. 用 GitHub / Google 登入(Google 不會攔截 Chrome) + 3. 登入後跳轉到 dashboard,URL 形如: + https://opencode.ai/workspace/{id}/go + 4. 複製 URL 中的 {id} 段 → workspaceId + 5. F12 → Application → Cookies + 6. 找到域名 opencode.ai 下名為 auth 的 Cookie + 7. 複製它的 Value 欄位 → auth cookie + 注意:只填 auth 一個 Cookie 即可,不要整個 Cookie 字串 + 如何獲取 CommandCode 憑據? + 1. 開啟 https://commandcode.ai 並登入 GitHub + ▸ 獲取 API Key(必填): + 右上角頭像 → Settings → API Keys → 建立一個 Key + ▸ 獲取全部 Cookie(用於用量記錄): + F12 → Application → Cookies → commandcode.ai + 選中所有 Cookie,複製整個 Cookie 字串 + 貼上到下方輸入框即可,無需逐個挑選 + 提示:不填 Cookie 也能正常檢視餘額,只是用量記錄為空。 + 如何獲取 Codex auth.json? + 1. 在終端執行 codex-oauth-proxy 或 + codex-proxy,完成 ChatGPT 登入 + 2. 憑證檔案儲存在: + + + 已儲存 + 已儲存憑據 + 連線成功!餘額: %1$s %2$s + 連線成功,憑據已儲存 + API Key 不能為空 + JSON 格式錯誤 + 服務拒絕該憑據 (401/403) + 測試期間憑據已變更 + 請求過於頻繁 (429) + 服務端錯誤 (%1$d) + 網路錯誤:%1$s + ,已恢復原憑據 + ,檢測到憑據已變化,未執行回滾 + 測試失敗。%1$s%2$s + 暫不支援測試此服務 + auth cookie 和 workspaceId 都需要填寫 + Cookie 不能為空 + 憑據未配置 + 憑據未配置或已過期 + 憑據無效,請在設定中重新配置 + 憑據無效,請重新登入 + 憑據已變更,正在重新載入 + 憑據已變更,請重新操作 + 網路異常,請檢查網路 + 網路異常 + 服務端異常 (HTTP %1$d) + 請求過於頻繁%1$s + ,請 %1$d 秒後重試 + 不支援啟用用量 + 請先選擇模型 + 未抓到 Cookie,請確認已登入 + 未配置登入 URL + URL 應以 %1$s 開頭 + 授權失敗: %1$s + State 不匹配,請重試 + 回撥缺少授權碼 + Token 交換失敗,請檢視除錯日誌 + + + ✓ 用量已啟用 + ✓ 啟用成功 + 請求結果 + 錯誤: %1$s + 響應內容:\n%1$s + 模型: %1$s + 響應 ID: %1$s + 回覆: + (空) + (無回覆內容) + (響應解析失敗,但請求已發出) + 用量: prompt=%1$s completion=%2$s total=%3$s tokens + 用量: input=%1$s output=%2$s tokens + token 重新整理失敗,請重新登入或重新匯入 auth.json + + + 登入 %1$s + 完成登入了嗎? + 如果你已經在頁面上完成登入但頁面沒有跳轉,可點選下方按鈕讓我抓取 Cookie 並儲存登入態。 + + + 暫無日誌記錄\n重新整理 Codex 用量後這裡會出現除錯資訊 + + + OpenCode Go · 用量 + DeepSeek 餘額 + 自動重新整理 + %1$s · 額度 + %1$s · %2$s + 更新 %1$s + 暫無數據 + 重新整理中… + + + 長按卡片拖曳排序 + 長按首頁卡片可以拖曳排序 + 在首頁長按任意服務卡片即可拖曳到想要的位置,排序會自動儲存。 + 小工具點左上角進 APP + 小工具左上角標識可開啟 APP + 桌面小工具左上角 RainyToken › 標識可以點選開啟應用,其它區域點選切換服務。 + 圖表點選檢視詳情 + 點選圖表柱子可檢視時段詳情 + 在用量圖表上點選或滑動手指,可檢視對應時段的詳細數值。 + 圖表標題可點選 + 點選圖表標題彈出按模型明細 + 圖表卡片標題(如「消耗金額」)點選後會彈出按模型拆分的明細。 + 切換圖表時區 + 用量詳情頁可切換 UTC+0 / UTC+8 + 用量詳情頁頂部可切換 UTC+0 / UTC+8,圖表標籤會跟隨切換。 + CCGO 清除並重新同步 + CCGO 詳情頁可清除快取重新同步 + CCGO 用量詳情頁右上角「清除」可刪除本地快取並重新拉取全部數據。 + 檢視用量詳情 + 服務卡片底部可進入用量圖表 + 有憑據的服務卡片底部會出現「檢視用量詳情」入口,點選進入圖表統計。 + 原始數據瀏覽 + 用量詳情頁右上角可檢視原始記錄 + 用量詳情頁右上角「詳細數據」可檢視每條原始呼叫記錄,支援分頁跳轉。 + 下拉重新整理聯動用量 + 下拉重新整理會同步餘額和用量 + 首頁下拉重新整理不僅重新整理餘額,還會自動同步 OCGO / CCGO 用量記錄。 + 憑據獲取教程 + 憑據頁底部有獲取教程 + 憑據編輯頁底部的「如何獲取」按鈕有針對每個服務的詳細操作步驟。 + 剪貼簿自動匯入 + Codex 支援從剪貼簿一鍵匯入 + Codex 憑據頁支援「從剪貼簿自動匯入」,貼上 auth.json 後一鍵填入。 + 詳細數據頁碼跳轉 + 原始數據頁可輸入頁碼直跳 + 原始數據頁底部可輸入頁碼直接跳轉,數據量大時不必逐頁翻。 + 小工具自動重新整理 + 劃到小工具會自動重新整理餘額 + 桌面小工具劃到負一屏時會自動重新整理餘額,也可手動點 ↻ 按鈕重新整理。 + + + %1$d次 + 選擇日期 + 選擇月份(任意一天) + 日期範圍 + 開始日期 + 結束日期 + 消費明細 + 呼叫次數明細 + Token 明細 + 清除數據 + 此操作將清除所有 CommandCode 用量本地快取並重新同步,需要幾秒鐘完成。 + 是否繼續? + 確認(%1$ds) + 確認清除 + 關閉 + 從 %1$s + 至 %1$s + 應用自定義範圍 + 確定 + 結束日期不能早於開始日期 %1$s + 時間 %1$s:%2$s + 暫無數據 + 合計: %1$s + 總計 + 記錄數 + 快取讀取 + 快取寫入 + 快取 Token 細分 + ※ 快取讀取已計入「輸入 Token」 + %1$d 次呼叫 + MM月dd日 EEEE + %1$d 次 · $%2$s + 原始數據 + 快取寫入(5m) + 快取寫入(1h) + 成本(USD) + 成本(Raw) + + + ~/.config/codex-oauth-proxy/auth.json + 3. 用文字編輯器開啟該檔案, + 複製全部內容貼上到上方輸入框 + JSON 格式示例: + {"tokens": { + "access_token": "...", + "refresh_token": "...", + "expiresAt": 1783727108252 + }} + 提示:匯入後 APP 會自動重新整理 token, + 不需要手動更新。 + Ollama 憑據 + 需要從瀏覽器複製完整 Cookie。登入 ollama.com/settings 後,在 DevTools → Network → 請求頭裡找到 Cookie 行複製。 + 更新憑據 + 儲存憑據 + 測試並儲存 + 在瀏覽器中開啟 Ollama Settings + 複製登入 URL 到剪貼簿 + 一鍵啟用用量 API Key(可選) + 填入 Ollama Cloud API Key 後,可在詳情頁一鍵傳送請求啟用用量統計。從 ollama.com/settings → API Keys 獲取。 + ollama-xxx 或 sk-xxx + 儲存 API Key + OpenCode Go 憑據 + 需要 auth cookie + workspaceId 兩個值。APP 會用 OkHttp 抓 dashboard 並自動解析 5h/周/月 三檔用量。 + 從剪貼簿自動匯入 + dashboard URL 中的 {id} 段 + auth Cookie 值 + F12 → Cookies → opencode.ai → auth 的 Value + 如何獲取這兩個值? + 在瀏覽器中開啟登入入口 + 填入 OpenCode Zen API Key 後,可在詳情頁一鍵傳送請求啟用用量統計。從 opencode.ai/settings → API Keys 獲取。 + CommandCode 憑據 + 需要 API Key(拉餘額)。Session Cookie(可選)在瀏覽器 DevTools 複製,用於拉用量記錄。不填也可正常檢視餘額。 + 從 Settings → API Keys 生成 + Session Cookie(可選) + F12 → Cookies → commandcode.ai → 全選複製貼上即可 + 如何獲取 API Key 和 Cookie? + 請輸入 API Key + 憑據會使用 Android Keystore (AES-256 GCM) 加密後儲存到本機 DataStore。 + 更新 + Codex / ChatGPT 憑據 + 推薦使用 OAuth 登入自動獲取憑據。也可手動貼上 auth.json 內容。 + 🔐 OAuth 登入(推薦) + ── 或手動匯入 ── + auth.json 完整內容 + 貼上 {"tokens": {...}} 完整 JSON + 匯入並儲存 + 如何獲取 auth.json? + 貼上登入態 + Google OAuth 會拒絕 APP 內建 WebView,所以請用瀏覽器登入後把 Cookie 粘到這裡。 + Cookie 字串 + 儲存 Cookie + + + 回應體為空 + 回應根節點不是 JSON 物件 + 未找到用量視窗資料 + 未找到模型清單 + 模型清單為空 + 回應格式異常 + 每週 + Spark + 模型呼叫次數 + 快取數據 · %1$s + 顯示的是上次成功獲取的餘額 + 本月消費 %1$s%2$s + 餘額構成 + 贈送餘額 + 自費充值 + 載入模型列表... + 選擇模型 + 重新整理模型列表 + 啟用中... + 重試啟用用量 + ⚡ 一鍵啟用用量 + 重新整理餘額 + 重新登入 + 配置憑據 + 儲值 + 當前餘額 + 5h 實時用量 + 月度餘額 + Session 用量 + 請選擇模型 + 錯誤詳情: + API 響應內容: + 複製 + *** (長度 %1$d) *** + %1$s***%2$s (長度 %3$d) + Cookie 格式錯誤,應為 name1=value1; name2=value2 + 已儲存 %1$d 個 Cookie + 請貼上 auth.json 內容 + auth.json 缺少 access_token 或 refresh_token + 已儲存 Codex 憑據,token 到期後會自動重新整理 + 解析失敗:%1$s + 連線成功!餘額: $%1$s + API Key 已儲存 + 連線成功!Session: %1$s%% · %2$s + 憑據已刪除 + 剪貼簿為空 + 已識別 workspaceId 和 auth cookie + 已識別 workspaceId,請再貼上 auth cookie + 已識別 auth cookie,請再貼上 workspaceId + 未識別到有效憑據 + + + Codex OAuth 登入 + 正在交換 Token... + 重試 + 無頭模式登入 + 1. 點選「在瀏覽器中開啟」或複製下方連結到瀏覽器\n2. 在 OpenAI 頁面完成登入和授權\n3. 瀏覽器會跳轉到一個 localhost 地址(頁面打不開是正常的)\n4. 複製瀏覽器位址列的完整 URL 貼上到下方輸入框\n5. 點選「提交回撥 URL」 + 授權連結 + 在瀏覽器中開啟 + 複製連結 + ── 登入後貼上回撥 URL ── + 回撥 URL + http://localhost:1455/auth/callback?code=... + 提交回撥 URL + 重新生成授權連結 + 正在準備授權... + + + Token 活動 + 每日 + 每週 + 累計 + OCGO Token 活動 + OCGO 每日 Token 使用熱力圖 + Token 活動 + 暫無資料 + 少 + 多 + Token 活動熱力圖,%1$d 天資料 + Token 活動熱力圖,%1$d 週資料 + 選擇年份 + 最近 365 天 + 累計 Token + 峰值 Token + 當前連續 + 最長連續 + + %1$d 天 + + %1$s使用了%2$s Token + %1$s使用了%2$s Token + + 個人資料 + 暱稱 + 郵箱 + 未設定 + 編輯 + 編輯個人資料 + 更換頭像 + 從相冊選擇 + 移除頭像 + 儲存 + 取消 + + 活動洞察 + 總請求次數 + + %1$d 次 + + 最多請求時段 + %1$d~%2$d點 + 一月 + 二月 + 三月 + 四月 + 五月 + 六月 + 七月 + 八月 + 九月 + 十月 + 十一月 + 十二月 + API Key + Workspace ID + 正在識別站點… + API Key:僅統計該 Key 的用量 + 面板登錄:全帳號消費統計,並解鎖 Key 管理/分組/兌換 + 帳號一鍵登錄(推薦) + 正在登錄… + 網頁登錄(備用,如開了二次驗證) + 帳號密碼 + API Key + 二次驗證碼(選填) + 暫無用量數據,下拉刷新 + 面板管理 + 載入 API Key 列表 + 暫無 API Key + 新建 Key + 兌換卡密 + 新建 API Key + Key 名稱 + 分組(可選,不選為預設) + 暫無可用分組 + ✓ 新建 Key 成功 + 以下 Key 僅顯示這一次,請立即複製保存: + 複製並關閉 + 兌換碼 + 切換分組 + 分組 + 預設分組 + 未使用 + 最近 %1$s + %1$s(ID %2$d) + API Key 已複製 + ⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密 + (累計口徑,未按所選週期過濾) + API Key 列表 + 信箱登錄 + API 登錄 + 消耗 %1$d 次 + 餘額 + 按模型 + 按 Key + 該週期內沒有 API Key 用量 + 本週期未使用 + 消耗 $%1$.2f + 刪除 API Key + 確定刪除「%1$s」嗎?刪除後無法復原,正在使用該 Key 的用戶端會立即失效。 + 此圖為帳號整體趨勢(無單模型維度) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ab91f2..5966bb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,693 +1,704 @@ - - - - RainyToken - RainyToken · v%1$s - - - RainyToken - - - AI Usage Panel - OpenCode Go usage + DeepSeek balance, auto-refresh - Rainy - - - Back - Cancel - Add Widget - Got it - Refresh - Settings - Delete - Clear - Save - Go - ◀ Prev - Next ▶ - Add widget to home screen - Language - Select language - Follow system - 简体中文 - 繁體中文 - English - View details - View usage details - Raw data - Sync usage data - I\'m logged in, save - Keep waiting - - - Loading… - Unknown error - All models - - - 👈 Select a service to view details - Tap a card on the left - Settings - API Management - Configure - Edit - Test - Delete credential? - This will remove the saved credential for %1$s. - Not configured - Last verified: %1$s - Cached: %1$s %2$s - Usage Stats - No configured services yet - %1$d refresh failed - Not fetched yet - Updated %1$s - Monthly spent - Total quota - Next reset - ✅ %1$s connected: %2$s %3$s - ❌ %1$s failed: %2$s - Invalid credential - Credential changed - Too many requests - Network error - Server error (HTTP %1$d) - Parse failed - Credentials - About RainyToken - Tips - Debug log - Usage details - Overview - Raw data - Charts - - - Not set up - Configured - Expired - Re-login - Refresh failed - No data - Pending - OK - Not set up · Tap to add - Configured · Valid - Expired · Tap to reconfigure - Needs re-verification - Manual entry mode - Credential set - No credential - Loading - Latest - Cached - Failed - Awaiting input - - - AI Balance Overview - No services configured yet - Only services with credentials are shown on the home page. - Go to Settings - Never fetched - Updated just now - Updated %1$d min ago - Updated %1$d h ago - Updated %1$s - 💡 Long-press a card to reorder - ⚠ %1$s - Stored locally & encrypted, nothing is uploaded - Add home screen widget? - This will ask the launcher to add the RainyToken widget. Some systems may still show their own confirmation dialog. - Shortcut permission denied. Please allow it in system settings and try again. - The launcher rejected the request. Please check launcher permissions. - This launcher does not support in-app adding. Long-press home → Widgets → find RainyToken. - - - Tap to configure credentials - Pull to refresh - Service currently unavailable - %1$s%2$s used this month - Remaining - · %1$s used / %2$s total - %1$s used - 5h used - · %1$s - Resets in %1$s - REST API · ¥ - WebView fetch · 5h quota - JSON API · $ - ChatGPT · Codex quota - Cookie fetch · Cloud quota - Cloud-IDE-JWT · IDE 额度 - OAuth · CodeBuddy 同体系积分 - 自托管面板 · 用户级 sk- Key 余额 - Breakdown - Trae credentials - Paste the Cloud-IDE-JWT from your Trae account session (starts with ey…). Used as Authorization: Cloud-IDE-JWT header. - Cloud-IDE-JWT - Region - CN - INTL - WorkBuddy credentials - Paste the access & refresh tokens from your Tencent WorkBuddy (CodeBuddy) session. Refresh token is rotated once per use. - Access Token - Refresh Token - Access and refresh tokens are required - Sub2API instance - Any user of the instance: paste the sk-… API key issued to you to see your balance (no admin needed). No key? Use the panel login email & password. - Instance base URL - https://api.example.com - Authentication - API Key (sk-…) - Or panel account - Email - Password - Base URL and (API Key or email+password) are required - - - Usage breakdown - Daily and per-model usage from your Sub2API gateway - Today - 7 days - 30 days - 90 days - All - Requests - Tokens - Cost - Daily usage - By model - All models - %1$s requests · %2$s tokens - No daily usage data in this range - No spend in this range - Model: %1$s · in current period - No model data in this range - - - Daily check-in - Check in every day for credits; streaks earn bonuses - Check in - Checking… - - - Sign in normally on the opened page; the app reads the session automatically. Manual API Key still works. - Panel token captured — tap Save to apply - ✓ Panel token captured - - - d - h - m - - - 5 hours - 5h - 5h usage - 5-hour rolling - Week - Month - Monthly - Usage - Usage window%1$s - Usage window - Model usage - Quota $%1$s - - - OpenCode Usage - CommandCode Usage - %1$d records - Tap to sync - Input tokens - Output tokens - Reasoning tokens - Total cost - Cache read %1$s - 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 - Switched to UTC+0, charts shown in UTC - $%1$s spent / $%2$s total - $%1$s remaining - Extra credits - By model - By day (UTC+0) - %1$d models - %1$d - Start time - End time - - - Spending (USD) - API requests - Token usage - Output - Input (miss) - Cache hit - - - Time - Model - Input(cache) - Output - Cost - - - All - Last 5 hours - Last 12 hours - Last 24 hours - Today - Yesterday - Last 7 days - Last 30 days - This month - Custom - Custom day - Custom month - Custom range - - - A tiny tool to check AI balances.\nCredentials are encrypted locally — nothing is uploaded. - View all %1$d hidden tips - Inspect token refresh, network requests and other debug records - - - ✓ Login state configured (%1$d cookies) - How to get the Cookie? - 1. Open the login page below in desktop Chrome - 2. Sign in with GitHub / Google (won\'t be blocked) - 3. After login, press F12 → Application → Cookies - 4. Copy all cookies of the site and paste them above - Format: name1=value1; name2=value2 - How to get OpenCode Go credentials? - 1. Open opencode.ai/auth in desktop Chrome - 2. Sign in with GitHub / Google (Google won\'t block Chrome) - 3. You\'ll land on the dashboard, URL looks like: - https://opencode.ai/workspace/{id}/go - 4. Copy the {id} segment → workspaceId - 5. F12 → Application → Cookies - 6. Find the cookie named auth under opencode.ai - 7. Copy its Value field → auth cookie - Note: only the auth cookie is needed, not the whole cookie string - How to get CommandCode credentials? - 1. Open https://commandcode.ai and sign in with GitHub - ▸ Get API Key (required): - Avatar (top right) → Settings → API Keys → Create a Key - ▸ Get all cookies (for usage records): - F12 → Application → Cookies → commandcode.ai - Select all cookies and copy the whole string - Paste it below — no need to pick them one by one - Tip: balance works without cookies, usage records will just be empty. - How to get Codex auth.json? - 1. Run codex-oauth-proxy or - codex-proxy in a terminal to sign in to ChatGPT - 2. The credential file is saved at: - - - Saved - Credentials saved - Connected! Balance: %1$s %2$s - Connected, credentials saved - API Key cannot be empty - Invalid JSON format - Service rejected this credential (401/403) - Credentials changed during test - Too many requests (429) - Server error (%1$d) - Network error: %1$s - , previous credential restored - , credential changed, rollback skipped - Test failed. %1$s%2$s - Testing is not supported for this service - Both auth cookie and workspaceId are required - Cookie cannot be empty - Credential not configured - Credential missing or expired - Credential invalid, please reconfigure in Settings - Credential invalid, please log in again - Credentials changed, reloading - Credentials changed, please retry - Network error, please check your connection - Network error - Server error (HTTP %1$d) - Too many requests%1$s - , retry in %1$d seconds - Usage activation is not supported - Please select a model first - No cookie captured, please make sure you\'re logged in - No login URL configured - URL should start with %1$s - Authorization failed: %1$s - State mismatch, please retry - Callback missing authorization code - Token exchange failed, check the debug log - - - ✓ Usage activated - ✓ Activation successful - Request result - Error: %1$s - Response body:\n%1$s - Model: %1$s - Response ID: %1$s - Reply: - (empty) - (no reply content) - (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. Please log in again or import auth.json. - - - Sign in to %1$s - Done logging in? - If you finished signing in but the page didn\'t redirect, tap the button below to grab the cookie and save your session. - - - No log entries yet\nRefresh Codex usage and debug info will show up here - - - OpenCode Go · Usage - DeepSeek balance - Auto refresh - %1$s · Quota - %1$s · %2$s - Updated %1$s - No data - Refreshing… - - - Reorder cards by long-press - Long-press a card to reorder it - Long-press any service card on the home screen and drag it wherever you like. The order is saved automatically. - Widget top-left opens the app - Tap the widget\'s top-left mark to open the app - The RainyToken › mark at the top-left of the widget opens the app. Tapping elsewhere switches services. - Tap charts for details - Tap a chart bar to see that time slot - Tap or slide on the usage charts to inspect the exact values of a time slot. - Chart titles are clickable - Tap a chart title for a per-model breakdown - Tapping a chart card title (e.g. "Spending") shows a per-model breakdown dialog. - Switch chart timezone - Usage details can toggle UTC+0 / UTC+8 - The usage details page can toggle between UTC+0 and UTC+8; chart labels follow along. - CCGO clear & resync - CCGO details can clear cache and resync - The "Clear" button on the CCGO usage details page deletes local cache and re-fetches everything. - View usage details - Service cards link to usage charts - Configured service cards show a "View usage details" entry at the bottom that opens the charts. - Browse raw data - Top-right of usage details opens raw records - "Raw data" at the top-right of usage details lists every record with pagination. - Pull-to-refresh syncs usage - Pull to refresh syncs balances and usage - Pull-to-refresh on the home screen refreshes balances and also syncs OCGO / CCGO usage records. - Credential guides - Credential pages include step-by-step guides - The "How to get" button at the bottom of credential pages walks through every service step by step. - Clipboard auto-import - Codex supports one-tap import from clipboard - The Codex credential page supports "Auto-import from clipboard": paste auth.json and it fills in with one tap. - Jump to a page number - Raw data page accepts a page number - The raw data page lets you enter a page number to jump directly — no need to flip through pages. - Widget auto-refresh - Swipe to the widget and it auto-refreshes - The widget refreshes balances when you swipe to it, or tap the ↻ button manually. - - - %1$d calls - Pick a date - Pick a month (any day) - Date range - Start date - End date - Spending breakdown - Request breakdown - Token breakdown - Clear data - This clears all cached CommandCode usage and re-syncs. It takes a few seconds. - Continue? - Confirm (%1$ds) - Yes, clear - Close - From %1$s - To %1$s - Apply custom range - OK - End date cannot be earlier than start date %1$s - Time %1$s:%2$s - No data - Total: %1$s - Total - Records - Cache read - Cache write - Cache token breakdown - ※ Cache reads are included in "Input tokens" - %1$d calls - MMM d, EEEE - %1$d calls · $%2$s - Raw data - Cache write (5m) - Cache write (1h) - Cost (USD) - Cost (raw) - - - ~/.config/codex-oauth-proxy/auth.json - 3. Open it with a text editor, - copy everything and paste it above - JSON example: - {"tokens": { - "access_token": "...", - "refresh_token": "...", - "expiresAt": 1783727108252 - }} - Tip: the app auto-refreshes the token after import, - no manual updates needed. - Ollama credentials - Copy the full cookie from your browser. After logging into ollama.com/settings, find the Cookie header in DevTools → Network and copy it. - Update credentials - Save credentials - Test & save - Open Ollama Settings in browser - Copy login URL to clipboard - One-tap usage API Key (optional) - With an Ollama Cloud API Key, the details page can send one request to activate usage stats. Get it at ollama.com/settings → API Keys. - ollama-xxx or sk-xxx - Save API Key - OpenCode Go credentials - Needs both auth cookie and workspaceId. The app fetches the dashboard via OkHttp and parses 5h/week/month usage. - Auto-import from clipboard - The {id} segment of the dashboard URL - auth Cookie value - F12 → Cookies → opencode.ai → Value of auth - How to get these two? - Open login page in browser - With an OpenCode Zen API Key, the details page can send one request to activate usage stats. Get it at opencode.ai/settings → API Keys. - CommandCode credentials - Needs an API Key (for balance). Session Cookie (optional) for usage records, copied from browser DevTools. Balance works without it. - Generate at Settings → API Keys - Session Cookie (optional) - F12 → Cookies → commandcode.ai → select all & paste - How to get API Key and Cookie? - Enter API Key - Credentials are encrypted with Android Keystore (AES-256 GCM) and stored in local DataStore. - Update - Codex / ChatGPT credentials - OAuth login fetches credentials automatically. You can also paste auth.json manually. - 🔐 OAuth login (recommended) - ── or import manually ── - Full auth.json content - Paste the full {"tokens": {...}} JSON - Import & save - How to get auth.json? - Paste session - Google OAuth blocks the in-app WebView, so log in with a browser and paste the cookie here. - Cookie string - Save cookie - - - Empty response body - Response root is not a JSON object - No usage window data found - Model list not found - Model list is empty - Response format is abnormal - Weekly - Spark - Model call count - Cached data · %1$s - Showing the last successfully fetched balance - %1$s%2$s spent this month - Balance breakdown - Granted balance - Top-ups - Loading model list... - Select model - Refresh model list - Activating... - Retry usage activation - ⚡ One-tap usage activation - Refresh balance - Log in again - Configure credential - Top up / Recharge - Current balance - 5h live usage - Monthly balance - Session usage - Please select a model - Error details: - API response: - Copy - *** (length %1$d) *** - %1$s***%2$s (length %3$d) - Bad cookie format, expected name1=value1; name2=value2 - %1$d cookies saved - Please paste the auth.json content - auth.json is missing access_token or refresh_token - Codex credential saved; token auto-refreshes when expiring - Parse failed: %1$s - Connected! Balance: $%1$s - API Key saved - Connected! Session: %1$s%% · %2$s - Credential deleted - Clipboard is empty - Recognized workspaceId and auth cookie - Recognized workspaceId, please paste the auth cookie too - Recognized auth cookie, please paste the workspaceId too - No valid credential recognized - - - Codex OAuth Login - Exchanging token... - Retry - Headless login - 1. Tap "Open in browser" or copy the link below to a browser\n2. Complete login & authorization on the OpenAI page\n3. The browser redirects to a localhost address (page not loading is normal)\n4. Copy the full URL from the address bar and paste it below\n5. Tap "Submit callback URL" - Authorization link - Open in browser - Copy link - ── paste the callback URL after login ── - Callback URL - http://localhost:1455/auth/callback?code=... - Submit callback URL - Regenerate auth link - Preparing authorization... - - - Token Activity - Daily - Weekly - Cumulative - OCGO Token Activity - OCGO daily Token usage heatmap - Token Activity - No data yet - Less - More - Token activity heatmap, %1$d days - Token activity heatmap, %1$d weeks - Select year - Recent 365 days - Total tokens - Peak tokens - Current streak - Longest streak - - %1$d day - %1$d days - - %1$s used %2$s tokens - %1$s used %2$s tokens - - Profile - Nickname - Email - Not set - Edit - Edit Profile - Change Avatar - Choose from gallery - Remove avatar - Save - Cancel - - Activity Insights - Total requests - - %1$d request - %1$d requests - - Peak hours - %1$d–%2$d - Jan - Feb - Mar - Apr - May - Jun - Jul - Aug - Sep - Oct - Nov - Dec - API Key - Workspace ID - Detecting site… - API Key: tracks usage for this key only - Panel login: full-account usage stats; unlocks key management, grouping and redemption - One-click account login (recommended) - Logging in… - Web login (fallback, e.g. if 2FA is enabled) - Email login - API key login - 2FA code (optional) - No usage data yet — pull to refresh - Panel management - Load API Key list - No API Key yet - New Key - Redeem code - New API Key - Key name - Group (optional, default if unselected) - No group available - ✓ API Key created - This Key is shown only once — copy it now: - Copy & close - Redeem code - Switch group - Group - Default group - never used - last %1$s - %1$s (ID %2$d) - API Key copied - ⚠ Low balance: $%1$s left — top up or redeem a code - (all-time, not limited to the selected period) - API keys - Email login - API key login - %1$d calls - Balance - By model - By API key - No API key usage in this period - Not used in this period - Spend $%1$.2f - Delete API key - Delete "%1$s"? This cannot be undone and any client using this key will stop working. - Chart shows the whole account (no per-model breakdown) + + + + RainyToken + RainyToken · v%1$s + + + RainyToken + + + AI Usage Panel + OpenCode Go usage + DeepSeek balance, auto-refresh + Rainy + + + Back + Cancel + Add Widget + Got it + Refresh + Settings + Delete + Clear + Save + Go + ◀ Prev + Next ▶ + Add widget to home screen + Language + Select language + Follow system + 简体中文 + 繁體中文 + English + View details + View usage details + Raw data + Sync usage data + I\'m logged in, save + Keep waiting + + + Loading… + Unknown error + All models + + + 👈 Select a service to view details + Tap a card on the left + Settings + API Management + Configure + Edit + Test + Delete credential? + This will remove the saved credential for %1$s. + Not configured + Last verified: %1$s + Cached: %1$s %2$s + Usage Stats + No configured services yet + %1$d refresh failed + Not fetched yet + Updated %1$s + Monthly spent + Total quota + Next reset + ✅ %1$s connected: %2$s %3$s + ❌ %1$s failed: %2$s + Invalid credential + Credential changed + Too many requests + Network error + Server error (HTTP %1$d) + Parse failed + Credentials + About RainyToken + Tips + Debug log + Usage details + Overview + Raw data + Charts + + + Not set up + Configured + Expired + Re-login + Refresh failed + No data + Pending + OK + Not set up · Tap to add + Configured · Valid + Expired · Tap to reconfigure + Needs re-verification + Manual entry mode + Credential set + No credential + Loading + Latest + Cached + Failed + Awaiting input + + + AI Balance Overview + No services configured yet + Only services with credentials are shown on the home page. + Go to Settings + Never fetched + Updated just now + Updated %1$d min ago + Updated %1$d h ago + Updated %1$s + 💡 Long-press a card to reorder + ⚠ %1$s + Stored locally & encrypted, nothing is uploaded + Add home screen widget? + This will ask the launcher to add the RainyToken widget. Some systems may still show their own confirmation dialog. + Shortcut permission denied. Please allow it in system settings and try again. + The launcher rejected the request. Please check launcher permissions. + This launcher does not support in-app adding. Long-press home → Widgets → find RainyToken. + + + Tap to configure credentials + Pull to refresh + Service currently unavailable + %1$s%2$s used this month + Remaining + · %1$s used / %2$s total + %1$s used + 5h used + · %1$s + Resets in %1$s + REST API · ¥ + WebView fetch · 5h quota + JSON API · $ + ChatGPT · Codex quota + Cookie fetch · Cloud quota + Cloud-IDE-JWT · IDE 额度 + OAuth · CodeBuddy 同体系积分 + 自托管面板 · 用户级 sk- Key 余额 + Breakdown + Trae credentials + Paste the Cloud-IDE-JWT from your Trae account session (starts with ey…). Used as Authorization: Cloud-IDE-JWT header. + Cloud-IDE-JWT + Region + CN + INTL + WorkBuddy credentials + Paste the access & refresh tokens from your Tencent WorkBuddy (CodeBuddy) session. Refresh token is rotated once per use. + Access Token + Refresh Token + Access and refresh tokens are required + Sub2API instance + Any user of the instance: paste the sk-… API key issued to you to see your balance (no admin needed). No key? Use the panel login email & password. + Instance base URL + https://api.example.com + Authentication + API Key (sk-…) + Or panel account + Email + Password + Base URL and (API Key or email+password) are required + + + Usage breakdown + Daily and per-model usage from your Sub2API gateway + Today + 7 days + 30 days + 90 days + All + Requests + Tokens + Cost + Daily usage + By model + All models + %1$s requests · %2$s tokens + No daily usage data in this range + No spend in this range + Model: %1$s · in current period + No model data in this range + + + Daily check-in + Check in every day for credits; streaks earn bonuses + Check in + Checking… + Check-in successful + Already checked in today + Already checked in (streak %1$d days) + Check-in failed, please check credentials + This service does not support check-in + Checked in today + Add your credentials first to check in. + INTL check-in not yet supported, please use a CN account + Device check-in limit reached today + Check-in failed (code=%1$d) + Check-in successful +%1$d credits + + + Sign in normally on the opened page; the app reads the session automatically. Manual API Key still works. + Panel token captured — tap Save to apply + ✓ Panel token captured + + + d + h + m + + + 5 hours + 5h + 5h usage + 5-hour rolling + Week + Month + Monthly + Usage + Usage window%1$s + Usage window + Model usage + Quota $%1$s + + + OpenCode Usage + CommandCode Usage + %1$d records + Tap to sync + Input tokens + Output tokens + Reasoning tokens + Total cost + Cache read %1$s + 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 + Switched to UTC+0, charts shown in UTC + $%1$s spent / $%2$s total + $%1$s remaining + Extra credits + By model + By day (UTC+0) + %1$d models + %1$d + Start time + End time + + + Spending (USD) + API requests + Token usage + Output + Input (miss) + Cache hit + + + Time + Model + Input(cache) + Output + Cost + + + All + Last 5 hours + Last 12 hours + Last 24 hours + Today + Yesterday + Last 7 days + Last 30 days + This month + Custom + Custom day + Custom month + Custom range + + + A tiny tool to check AI balances.\nCredentials are encrypted locally — nothing is uploaded. + View all %1$d hidden tips + Inspect token refresh, network requests and other debug records + + + ✓ Login state configured (%1$d cookies) + How to get the Cookie? + 1. Open the login page below in desktop Chrome + 2. Sign in with GitHub / Google (won\'t be blocked) + 3. After login, press F12 → Application → Cookies + 4. Copy all cookies of the site and paste them above + Format: name1=value1; name2=value2 + How to get OpenCode Go credentials? + 1. Open opencode.ai/auth in desktop Chrome + 2. Sign in with GitHub / Google (Google won\'t block Chrome) + 3. You\'ll land on the dashboard, URL looks like: + https://opencode.ai/workspace/{id}/go + 4. Copy the {id} segment → workspaceId + 5. F12 → Application → Cookies + 6. Find the cookie named auth under opencode.ai + 7. Copy its Value field → auth cookie + Note: only the auth cookie is needed, not the whole cookie string + How to get CommandCode credentials? + 1. Open https://commandcode.ai and sign in with GitHub + ▸ Get API Key (required): + Avatar (top right) → Settings → API Keys → Create a Key + ▸ Get all cookies (for usage records): + F12 → Application → Cookies → commandcode.ai + Select all cookies and copy the whole string + Paste it below — no need to pick them one by one + Tip: balance works without cookies, usage records will just be empty. + How to get Codex auth.json? + 1. Run codex-oauth-proxy or + codex-proxy in a terminal to sign in to ChatGPT + 2. The credential file is saved at: + + + Saved + Credentials saved + Connected! Balance: %1$s %2$s + Connected, credentials saved + API Key cannot be empty + Invalid JSON format + Service rejected this credential (401/403) + Credentials changed during test + Too many requests (429) + Server error (%1$d) + Network error: %1$s + , previous credential restored + , credential changed, rollback skipped + Test failed. %1$s%2$s + Testing is not supported for this service + Both auth cookie and workspaceId are required + Cookie cannot be empty + Credential not configured + Credential missing or expired + Credential invalid, please reconfigure in Settings + Credential invalid, please log in again + Credentials changed, reloading + Credentials changed, please retry + Network error, please check your connection + Network error + Server error (HTTP %1$d) + Too many requests%1$s + , retry in %1$d seconds + Usage activation is not supported + Please select a model first + No cookie captured, please make sure you\'re logged in + No login URL configured + URL should start with %1$s + Authorization failed: %1$s + State mismatch, please retry + Callback missing authorization code + Token exchange failed, check the debug log + + + ✓ Usage activated + ✓ Activation successful + Request result + Error: %1$s + Response body:\n%1$s + Model: %1$s + Response ID: %1$s + Reply: + (empty) + (no reply content) + (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. Please log in again or import auth.json. + + + Sign in to %1$s + Done logging in? + If you finished signing in but the page didn\'t redirect, tap the button below to grab the cookie and save your session. + + + No log entries yet\nRefresh Codex usage and debug info will show up here + + + OpenCode Go · Usage + DeepSeek balance + Auto refresh + %1$s · Quota + %1$s · %2$s + Updated %1$s + No data + Refreshing… + + + Reorder cards by long-press + Long-press a card to reorder it + Long-press any service card on the home screen and drag it wherever you like. The order is saved automatically. + Widget top-left opens the app + Tap the widget\'s top-left mark to open the app + The RainyToken › mark at the top-left of the widget opens the app. Tapping elsewhere switches services. + Tap charts for details + Tap a chart bar to see that time slot + Tap or slide on the usage charts to inspect the exact values of a time slot. + Chart titles are clickable + Tap a chart title for a per-model breakdown + Tapping a chart card title (e.g. "Spending") shows a per-model breakdown dialog. + Switch chart timezone + Usage details can toggle UTC+0 / UTC+8 + The usage details page can toggle between UTC+0 and UTC+8; chart labels follow along. + CCGO clear & resync + CCGO details can clear cache and resync + The "Clear" button on the CCGO usage details page deletes local cache and re-fetches everything. + View usage details + Service cards link to usage charts + Configured service cards show a "View usage details" entry at the bottom that opens the charts. + Browse raw data + Top-right of usage details opens raw records + "Raw data" at the top-right of usage details lists every record with pagination. + Pull-to-refresh syncs usage + Pull to refresh syncs balances and usage + Pull-to-refresh on the home screen refreshes balances and also syncs OCGO / CCGO usage records. + Credential guides + Credential pages include step-by-step guides + The "How to get" button at the bottom of credential pages walks through every service step by step. + Clipboard auto-import + Codex supports one-tap import from clipboard + The Codex credential page supports "Auto-import from clipboard": paste auth.json and it fills in with one tap. + Jump to a page number + Raw data page accepts a page number + The raw data page lets you enter a page number to jump directly — no need to flip through pages. + Widget auto-refresh + Swipe to the widget and it auto-refreshes + The widget refreshes balances when you swipe to it, or tap the ↻ button manually. + + + %1$d calls + Pick a date + Pick a month (any day) + Date range + Start date + End date + Spending breakdown + Request breakdown + Token breakdown + Clear data + This clears all cached CommandCode usage and re-syncs. It takes a few seconds. + Continue? + Confirm (%1$ds) + Yes, clear + Close + From %1$s + To %1$s + Apply custom range + OK + End date cannot be earlier than start date %1$s + Time %1$s:%2$s + No data + Total: %1$s + Total + Records + Cache read + Cache write + Cache token breakdown + ※ Cache reads are included in "Input tokens" + %1$d calls + MMM d, EEEE + %1$d calls · $%2$s + Raw data + Cache write (5m) + Cache write (1h) + Cost (USD) + Cost (raw) + + + ~/.config/codex-oauth-proxy/auth.json + 3. Open it with a text editor, + copy everything and paste it above + JSON example: + {"tokens": { + "access_token": "...", + "refresh_token": "...", + "expiresAt": 1783727108252 + }} + Tip: the app auto-refreshes the token after import, + no manual updates needed. + Ollama credentials + Copy the full cookie from your browser. After logging into ollama.com/settings, find the Cookie header in DevTools → Network and copy it. + Update credentials + Save credentials + Test & save + Open Ollama Settings in browser + Copy login URL to clipboard + One-tap usage API Key (optional) + With an Ollama Cloud API Key, the details page can send one request to activate usage stats. Get it at ollama.com/settings → API Keys. + ollama-xxx or sk-xxx + Save API Key + OpenCode Go credentials + Needs both auth cookie and workspaceId. The app fetches the dashboard via OkHttp and parses 5h/week/month usage. + Auto-import from clipboard + The {id} segment of the dashboard URL + auth Cookie value + F12 → Cookies → opencode.ai → Value of auth + How to get these two? + Open login page in browser + With an OpenCode Zen API Key, the details page can send one request to activate usage stats. Get it at opencode.ai/settings → API Keys. + CommandCode credentials + Needs an API Key (for balance). Session Cookie (optional) for usage records, copied from browser DevTools. Balance works without it. + Generate at Settings → API Keys + Session Cookie (optional) + F12 → Cookies → commandcode.ai → select all & paste + How to get API Key and Cookie? + Enter API Key + Credentials are encrypted with Android Keystore (AES-256 GCM) and stored in local DataStore. + Update + Codex / ChatGPT credentials + OAuth login fetches credentials automatically. You can also paste auth.json manually. + 🔐 OAuth login (recommended) + ── or import manually ── + Full auth.json content + Paste the full {"tokens": {...}} JSON + Import & save + How to get auth.json? + Paste session + Google OAuth blocks the in-app WebView, so log in with a browser and paste the cookie here. + Cookie string + Save cookie + + + Empty response body + Response root is not a JSON object + No usage window data found + Model list not found + Model list is empty + Response format is abnormal + Weekly + Spark + Model call count + Cached data · %1$s + Showing the last successfully fetched balance + %1$s%2$s spent this month + Balance breakdown + Granted balance + Top-ups + Loading model list... + Select model + Refresh model list + Activating... + Retry usage activation + ⚡ One-tap usage activation + Refresh balance + Log in again + Configure credential + Top up / Recharge + Current balance + 5h live usage + Monthly balance + Session usage + Please select a model + Error details: + API response: + Copy + *** (length %1$d) *** + %1$s***%2$s (length %3$d) + Bad cookie format, expected name1=value1; name2=value2 + %1$d cookies saved + Please paste the auth.json content + auth.json is missing access_token or refresh_token + Codex credential saved; token auto-refreshes when expiring + Parse failed: %1$s + Connected! Balance: $%1$s + API Key saved + Connected! Session: %1$s%% · %2$s + Credential deleted + Clipboard is empty + Recognized workspaceId and auth cookie + Recognized workspaceId, please paste the auth cookie too + Recognized auth cookie, please paste the workspaceId too + No valid credential recognized + + + Codex OAuth Login + Exchanging token... + Retry + Headless login + 1. Tap "Open in browser" or copy the link below to a browser\n2. Complete login & authorization on the OpenAI page\n3. The browser redirects to a localhost address (page not loading is normal)\n4. Copy the full URL from the address bar and paste it below\n5. Tap "Submit callback URL" + Authorization link + Open in browser + Copy link + ── paste the callback URL after login ── + Callback URL + http://localhost:1455/auth/callback?code=... + Submit callback URL + Regenerate auth link + Preparing authorization... + + + Token Activity + Daily + Weekly + Cumulative + OCGO Token Activity + OCGO daily Token usage heatmap + Token Activity + No data yet + Less + More + Token activity heatmap, %1$d days + Token activity heatmap, %1$d weeks + Select year + Recent 365 days + Total tokens + Peak tokens + Current streak + Longest streak + + %1$d day + %1$d days + + %1$s used %2$s tokens + %1$s used %2$s tokens + + Profile + Nickname + Email + Not set + Edit + Edit Profile + Change Avatar + Choose from gallery + Remove avatar + Save + Cancel + + Activity Insights + Total requests + + %1$d request + %1$d requests + + Peak hours + %1$d–%2$d + Jan + Feb + Mar + Apr + May + Jun + Jul + Aug + Sep + Oct + Nov + Dec + API Key + Workspace ID + Detecting site… + API Key: tracks usage for this key only + Panel login: full-account usage stats; unlocks key management, grouping and redemption + One-click account login (recommended) + Logging in… + Web login (fallback, e.g. if 2FA is enabled) + Email login + API key login + 2FA code (optional) + No usage data yet — pull to refresh + Panel management + Load API Key list + No API Key yet + New Key + Redeem code + New API Key + Key name + Group (optional, default if unselected) + No group available + ✓ API Key created + This Key is shown only once — copy it now: + Copy & close + Redeem code + Switch group + Group + Default group + never used + last %1$s + %1$s (ID %2$d) + API Key copied + ⚠ Low balance: $%1$s left — top up or redeem a code + (all-time, not limited to the selected period) + API keys + Email login + API key login + %1$d calls + Balance + By model + By API key + No API key usage in this period + Not used in this period + Spend $%1$.2f + Delete API key + Delete "%1$s"? This cannot be undone and any client using this key will stop working. + Chart shows the whole account (no per-model breakdown) \ No newline at end of file diff --git a/app/src/test/java/com/rainy/token/data/repository/TraeRepositoryTest.kt b/app/src/test/java/com/rainy/token/data/repository/TraeRepositoryTest.kt new file mode 100644 index 0000000..31551ce --- /dev/null +++ b/app/src/test/java/com/rainy/token/data/repository/TraeRepositoryTest.kt @@ -0,0 +1,110 @@ +package com.rainy.token.data.repository + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for [TraeRepository.parseCheckin](每日签到 claim 响应 → 语义结果)。 + * + * 覆盖真实实测口径:code 0/200 成功、code 1001 已签、消息含「已签到」等账号级已签、 + * 消息含「设备」为设备级拦截(提示而非报错)、其它 code 走 FAILED 且带回错误码。 + * 全部纯 JVM、无网络(parseCheckin 位于 companion,静态调用,不构造 Repository)。 + */ +class TraeRepositoryTest { + + private fun parse(body: String) = TraeRepository.parseCheckin(body) + + @Test + fun success_parsesPointsAndOutcome() { + val body = """{"code":0,"message":"签到成功","data":{"points":200}}""" + val r = parse(body).getOrThrow() + + assertTrue(r.success) + assertFalse(r.alreadyChecked) + assertEquals(200L, r.points) + assertEquals(TraeRepository.CheckinOutcome.SUCCESS, r.outcome) + } + + @Test + fun success_withoutPoints_fallsBackTo200() { + // 服务端未回 points 时按 200 兜底(实测默认奖励) + val r = parse("""{"code":200,"message":"ok"}""").getOrThrow() + + assertTrue(r.success) + assertEquals(200L, r.points) + assertEquals(TraeRepository.CheckinOutcome.SUCCESS, r.outcome) + } + + @Test + fun code1001_marksAlreadyChecked() { + val r = parse("""{"code":1001,"message":"今日已签到"}""").getOrThrow() + + assertTrue(r.alreadyChecked) + assertEquals(TraeRepository.CheckinOutcome.ALREADY, r.outcome) + // 已签不应再报成功奖励 + assertEquals(0L, r.points) + } + + @Test + fun alreadyKeywordWithoutCode_marksAlreadyChecked() { + // 服务端换 code 但文案不变时仍识别为「账号级已签」 + val r = parse("""{"code":5,"message":"您今天已经签到过了"}""").getOrThrow() + + assertTrue(r.alreadyChecked) + assertEquals(TraeRepository.CheckinOutcome.ALREADY, r.outcome) + } + + @Test + fun englishAlreadyKeyword_marksAlreadyChecked() { + val r = parse("""{"code":7,"message":"Already claimed today"}""").getOrThrow() + + assertEquals(TraeRepository.CheckinOutcome.ALREADY, r.outcome) + } + + @Test + fun deviceBlock_isNotSuccessAndNotAlready() { + // 设备级拦截:既不是成功,也不能误判成「账号已签」(否则用户以为签过了) + val body = """{"code":9074,"message":"该设备今日签到次数已达上限"}""" + val r = parse(body).getOrThrow() + + assertFalse(r.success) + assertFalse(r.alreadyChecked) + assertEquals(TraeRepository.CheckinOutcome.DEVICE_BLOCKED, r.outcome) + } + + @Test + fun deviceBlock_englishWording_alsoDetected() { + val r = parse("""{"code":9074,"message":"device limit reached"}""").getOrThrow() + + assertEquals(TraeRepository.CheckinOutcome.DEVICE_BLOCKED, r.outcome) + } + + @Test + fun unknownCode_isFailedAndKeepsCode() { + val r = parse("""{"code":500,"message":"服务异常"}""").getOrThrow() + + assertFalse(r.success) + assertFalse(r.alreadyChecked) + assertEquals(TraeRepository.CheckinOutcome.FAILED, r.outcome) + assertEquals(500L, r.code) + } + + @Test + fun dataWrappedResponse_isUnwrapped() { + // data 包裹结构(服务端两种布局都出现过) + val body = """{"data":{"code":0,"points":150},"trace":"x"}""" + val r = parse(body).getOrThrow() + + assertTrue(r.success) + assertEquals(150L, r.points) + } + + @Test + fun malformedJson_returnsFailureNotCrash() { + val result = parse("not-json") + + assertTrue(result.isFailure) + } +} diff --git a/app/src/test/java/com/rainy/token/data/repository/WorkBuddyRepositoryTest.kt b/app/src/test/java/com/rainy/token/data/repository/WorkBuddyRepositoryTest.kt new file mode 100644 index 0000000..3355a3f --- /dev/null +++ b/app/src/test/java/com/rainy/token/data/repository/WorkBuddyRepositoryTest.kt @@ -0,0 +1,75 @@ +package com.rainy.token.data.repository + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Unit tests for [WorkBuddyRepository.parseCheckin](checkin-activity-status 响应 → 签到状态)。 + * + * 覆盖实测字段口径:data 平铺 / {data:{...}} 包裹、snake_case 与 camelCase 双候选、 + * 数值字段字符串化容错、字段缺失兜底。全部纯 JVM、无网络 + * (parseCheckin 位于 companion,静态调用,不构造 Repository)。 + */ +class WorkBuddyRepositoryTest { + + private fun parse(body: String) = WorkBuddyRepository.parseCheckin(body) + + @Test + fun flatResponse_parsesAllFields() { + val body = """ + {"active":true,"today_checked_in":true,"streak_days":5, + "daily_credit":10,"today_credit":10,"activity_name":"Buddy 加油站"} + """.trimIndent() + val s = parse(body) + + assertTrue(s.active) + assertTrue(s.todayCheckedIn) + assertEquals(5L, s.streakDays) + assertEquals(10L, s.dailyCredit) + assertEquals(10L, s.todayCredit) + assertEquals("Buddy 加油站", s.activityName) + // 查询模式不带动作语义 + assertEquals(WorkBuddyRepository.CheckinOutcome.QUERY, s.outcome) + } + + @Test + fun dataWrappedResponse_isUnwrapped() { + val body = """{"code":0,"data":{"active":true,"todayCheckedIn":true,"streakDays":"3"}}""" + val s = parse(body) + + assertTrue(s.todayCheckedIn) + // camelCase + 字符串数字都要能解析 + assertEquals(3L, s.streakDays) + } + + @Test + fun notCheckedInToday_isFalse() { + val s = parse("""{"today_checked_in":false,"streak_days":0}""") + + assertFalse(s.todayCheckedIn) + assertEquals(0L, s.streakDays) + } + + @Test + fun missingFields_fallBackToDefaults() { + // 字段缺失时:active 默认 true(活动视为进行中)、签到相关默认 0/false + val s = parse("{}") + + assertTrue(s.active) + assertFalse(s.todayCheckedIn) + assertEquals(0L, s.streakDays) + assertEquals(0L, s.dailyCredit) + assertEquals(0L, s.todayCredit) + assertEquals("", s.activityName) + } + + @Test + fun themeName_isAcceptedAsActivityNameFallback() { + // 部分响应只有 theme_name + val s = parse("""{"theme_name":"每日签到"}""") + + assertEquals("每日签到", s.activityName) + } +}