diff --git a/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt b/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt index 76b3b2f..22febfd 100644 --- a/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt @@ -13,6 +13,9 @@ import java.time.temporal.ChronoUnit import java.util.TimeZone import javax.inject.Singleton import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -526,6 +529,23 @@ class Sub2ApiRepository constructor( */ data class WindowModelStats(val stats: List, val allTime: Boolean = false) + /** + * 单个 Key 在所选窗口内的用量汇总(来源:面板「使用记录」页的 + * GET /api/v1/usage/stats?start_date=&end_date=&api_key_id=N,实测 v0.2.10)。 + * + * 未使用过的 Key 该端点返回 404 API_KEY_NOT_FOUND,故用 [unused] 表示 + * 「面板无该 Key 的记录」,UI 显示为 0 而不是报错。 + */ + data class KeyUsageStat( + val keyId: Long, + val name: String, + val groupName: String?, + val requests: Long, + val totalTokens: Long, + val cost: Double, + val unused: Boolean = false + ) + /** 可用分组(切换分组选择器数据)。 */ data class PanelGroup(val id: Long, val name: String, val rateMultiplier: Double, val description: String = "") @@ -663,6 +683,77 @@ class Sub2ApiRepository constructor( }.getOrNull() } + /** + * 按 Key 统计(面板「使用记录」页口径,v0.2.10 实测)。 + * + * 先取 Key 列表,再对每个 Key 调 + * GET /api/v1/usage/stats?start_date=&end_date=&api_key_id=N。 + * 未使用过的 Key 返回 404 API_KEY_NOT_FOUND —— 属「无记录」而非错误, + * 标记为 [KeyUsageStat.unused] 并计 0,不中断整个列表。 + * + * 说明:面板没有「一次拿到所有 Key 用量」的端点(dashboard 下 keys 相关路由均 404, + * /api/v1/keys 上的 usage_1d/5h/7d 实测恒为 0),所以必须逐个查询, + * 故用 coroutineScope + async 并行,避免 Key 多时串行等待过久。 + */ + suspend fun fetchKeyUsageStats(startDate: LocalDate, endDate: LocalDate): Result> = + withContext(Dispatchers.IO) { + val (auth, base) = panelAuthOrNull() + ?: return@withContext Result.failure(RepositoryError.InvalidCredential("请先登录面板")) + val keys = fetchPanelKeys().getOrElse { return@withContext Result.failure(it) } + if (keys.isEmpty()) return@withContext Result.success(emptyList()) + + val start = startDate.toString() + val end = endDate.toString() + val tz = tzParam() + // 分组名映射(展示「Key · 分组」),拉取失败不影响主流程 + val groups = runCatching { fetchPanelGroups().getOrNull().orEmpty() } + .getOrDefault(emptyList()) + .associateBy { it.id } + + val stats = coroutineScope { + keys.map { k -> + async(Dispatchers.IO) { + val resp = get( + base, + "/api/v1/usage/stats?start_date=$start&end_date=$end" + + "&api_key_id=${k.id}&timezone=$tz", + auth + ) + val unused = resp == null || resp.code == 404 + val usage = if (!unused && resp!!.code in 200..299) { + runCatching { parseKeyUsage(resp.body) }.getOrNull() + } else null + KeyUsageStat( + keyId = k.id, + name = k.name, + groupName = k.groupId?.let { groups[it]?.name }, + requests = usage?.first ?: 0L, + totalTokens = usage?.second ?: 0L, + cost = usage?.third ?: 0.0, + unused = unused + ) + } + }.awaitAll() + } + Result.success( + stats.sortedWith( + compareByDescending { it.cost } + .thenByDescending { it.requests } + .thenBy { it.name } + ) + ) + } + + /** 解析 usage/stats → (requests, totalTokens, actualCost)。 */ + private fun parseKeyUsage(body: String): Triple? { + val root = json.parseToJsonElement(body).jsonObject + val d = root["data"]?.jsonObject ?: root + val requests = d.longOr("total_requests") ?: return null + val tokens = d.longOr("total_tokens") ?: 0L + val cost = d.numberOr("total_actual_cost") ?: d.numberOr("total_cost") ?: 0.0 + return Triple(requests, tokens, cost) + } + /** * 新建 Key:POST /api/v1/keys {name, group_id?} → 返回明文 sk-key。 * 兼容多种响应形态:data 为对象(key / api_key / sk_key / token 字段)、data 为字符串, 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 ce32db1..2b11703 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 @@ -120,6 +120,7 @@ fun ServiceDetailScreen( // 面板管理(Key/分组/兑换)已移到首页 Sub2API 卡片的「面板管理」入口, // 详情页只保留用量明细所需的窗口统计状态。 val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle() + val panelKeyStats by viewModel.panelKeyStats.collectAsStateWithLifecycle() val config = ServiceConfigProvider.get(service) val isManualMode = config.method == FetchMethod.MANUAL @@ -205,7 +206,9 @@ fun ServiceDetailScreen( Sub2ApiUsageCard( state = uiState.state, windowStatsState = panelWindowStats, - onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) } + onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) }, + keyStatsState = panelKeyStats, + onLoadKeyStats = { from, to -> viewModel.loadPanelKeyStats(from, to) } ) } // 面板管理(Key 列表/新建/分组/兑换)不再在详情页展示: 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 b405da4..750da93 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 @@ -132,6 +132,36 @@ class ServiceDetailViewModel @Inject constructor( } } + /** 「按 Key」用量状态(面板使用记录口径,按当前时间周期)。 */ + private val _panelKeyStats = MutableStateFlow(PanelKeyStatsState.Idle) + val panelKeyStats: StateFlow = _panelKeyStats.asStateFlow() + + private var panelKeyStatsGeneration = 0 + + /** 按所选周期拉取「按 Key」用量。 */ + fun loadPanelKeyStats(from: LocalDate, to: LocalDate) { + val generation = ++panelKeyStatsGeneration + _panelKeyStats.value = PanelKeyStatsState.Loading + viewModelScope.launch { + val result = sub2ApiRepository.fetchKeyUsageStats(from, to) + if (generation != panelKeyStatsGeneration) return@launch + result.fold( + onSuccess = { list -> + _panelKeyStats.value = PanelKeyStatsState.Success( + list.map { + PanelKeyStatsState.KeyStat( + keyId = it.keyId, name = it.name, groupName = it.groupName, + requests = it.requests, totalTokens = it.totalTokens, + cost = it.cost, unused = it.unused + ) + } + ) + }, + onFailure = { e -> _panelKeyStats.value = PanelKeyStatsState.Error(e.message ?: "加载按 Key 用量失败") } + ) + } + } + /** 刷新面板 Key 列表(仅 SUB2API 且有面板 Token)。 */ fun loadPanelKeys() { viewModelScope.launch { @@ -777,6 +807,19 @@ sealed class PanelWindowStatsState { data class Error(val message: String) : PanelWindowStatsState() } +/** Sub2API「按 Key」用量状态(按当前时间周期查询面板使用记录)。 */ +sealed class PanelKeyStatsState { + data object Idle : PanelKeyStatsState() + data object Loading : PanelKeyStatsState() + data class Success(val stats: List) : PanelKeyStatsState() + data class Error(val message: String) : PanelKeyStatsState() + + data class KeyStat( + val keyId: Long, val name: String, val groupName: String?, + val requests: Long, val totalTokens: Long, val cost: Double, val unused: Boolean + ) +} + sealed class State { data object Loading : State() data class Fresh(val data: ServiceBalance) : State() diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiPanelManagement.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiPanelManagement.kt index b06908d..bf147db 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiPanelManagement.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiPanelManagement.kt @@ -1,479 +1,503 @@ -package com.rainy.token.ui.servicedetail - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.rainy.token.R -import com.rainy.token.data.repository.Sub2ApiRepository -import com.rainy.token.domain.model.ServiceBalance -import com.rainy.token.ui.theme.inkMuted -import java.util.Locale - -/** - * Sub2API 面板管理卡:Key 列表 / 新建 / 分组 / 兑换 / 低余额提醒。 - * - * 首页 Sub2API 卡片与 Sub2API 详情页共用(有面板 Token 时才能加载; - * API Key 模式只读余额,进入后提示先登录面板)。 - * - * 交互细节: - * - Key 列表只展示掩码(sk-…后缀),完整 Key 通过「复制」按钮复制并 Toast 反馈; - * - 新建 Key 成功后弹窗展示完整 Key,避免把明文直接铺在长消息里; - * - 兑换/切换分组等操作结果以 Toast + 行内文案双重反馈。 - */ -@Composable -internal fun Sub2ApiPanelManagement( - balance: ServiceBalance?, - keysState: PanelKeysState, - groups: List, - actionMessage: String?, - createdKey: String? = null, - onLoadKeys: () -> Unit, - onLoadGroups: () -> Unit, - onCreateKey: (String, Long?) -> Unit, - onSwitchGroup: (Long, Long) -> Unit, - onDeleteKey: (Long) -> Unit, - onRedeem: (String) -> Unit, - onConsumeMessage: () -> Unit, - onConsumeCreatedKey: () -> Unit = {}, - /** 弹窗场景由 AlertDialog 的 title 槽展示标题,可关闭内部标题避免重复 */ - showHeader: Boolean = true -) { - val context = LocalContext.current - var showCreate by remember { mutableStateOf(false) } - var showRedeem by remember { mutableStateOf(false) } - var switchForKeyId by remember { mutableStateOf(null) } - // 待删除的 Key:先弹确认框(删除不可逆,避免列表里误触) - var deleteForKeyId by remember { mutableStateOf(null) } - - val groupMap = remember(groups) { groups.associateBy { it.id } } - val copiedToast = stringResource(R.string.sub2_panel_copied) - - // 进入卡片即自动拉取一次 Key 列表与分组(Idle 态),省掉一次手动点击 - LaunchedEffect(Unit) { - if (keysState is PanelKeysState.Idle) { - onLoadKeys() - onLoadGroups() - } - } - - // 最近一次操作结果:用于行内保留展示(Toast 消费后消息会被置空,这里留一份快照) - var lastResult by remember { mutableStateOf(null) } - - // 操作结果(新建/兑换/切组)用 Toast 提醒:避免结果消息藏在长页面里看不见。 - // 消费后再置空,保证下一次「同样的结果」(如连续两次切换分组成功)仍能再次提示。 - LaunchedEffect(actionMessage) { - val msg = actionMessage ?: return@LaunchedEffect - lastResult = msg - Toast.makeText(context, msg, Toast.LENGTH_LONG).show() - onConsumeMessage() - } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) - ) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - if (showHeader) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.sub2_panel_manage_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = { onLoadKeys(); onLoadGroups() }) { - Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_refresh)) - } - } - } - // showHeader=false 时不渲染任何标题/刷新行: - // 弹窗场景由调用方把标题与刷新按钮放在同一行(见 DashboardScreen 的 title 槽), - // 这里再放一行会让刷新按钮掉到标题下面,视觉上多出一条空行。 - - (actionMessage ?: lastResult)?.let { msg -> - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = msg.take(160) + if (msg.length > 160) "…" else "", - style = MaterialTheme.typography.bodySmall, - color = if (msg.startsWith("已创建") || msg.startsWith("分组已") || msg.startsWith("兑换成功")) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.error - ) - } - - val amount = balance?.amount ?: 0.0 - if (amount > 0.0 && amount < 5.0) { - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource( - R.string.sub2_panel_low_balance, - String.format(Locale.getDefault(), "%.2f", amount) - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.35f)) - .padding(horizontal = 10.dp, vertical = 8.dp) - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - // 主要操作(新建 Key / 兑换卡密)固定在列表上方: - // Key 数量多时列表很长,放在底部会被埋在滚动区尽头,不易发现。 - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = { onLoadGroups(); showCreate = true }, modifier = Modifier.weight(1f)) { - Text(stringResource(R.string.sub2_panel_create)) - } - OutlinedButton(onClick = { showRedeem = true }, modifier = Modifier.weight(1f)) { - Text(stringResource(R.string.sub2_panel_redeem)) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = stringResource(R.string.sub2_panel_keys_header), - style = MaterialTheme.typography.labelLarge, - color = inkMuted() - ) - Spacer(modifier = Modifier.height(4.dp)) - - when (keysState) { - is PanelKeysState.Idle -> { - OutlinedButton(onClick = onLoadKeys, modifier = Modifier.fillMaxWidth()) { - Text(stringResource(R.string.sub2_panel_load_keys)) - } - } - is PanelKeysState.Loading -> { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { - CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.common_loading), style = MaterialTheme.typography.bodySmall, color = inkMuted()) - } - } - is PanelKeysState.Error -> { - Column { - Text(keysState.message, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error) - TextButton(onClick = onLoadKeys) { Text(stringResource(R.string.action_retry)) } - } - } - is PanelKeysState.Success -> { - if (keysState.keys.isEmpty()) { - Text(stringResource(R.string.sub2_panel_no_keys), style = MaterialTheme.typography.bodySmall, color = inkMuted()) - } else { - keysState.keys.forEach { key -> - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(R.string.sub2_panel_key_id, key.name, key.id), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - // API Key 只显示掩码,避免页面展示过长明文 - Text( - text = buildString { - append(maskKey(key.key)) - append(" · ") - append( - key.groupId?.let { groupMap[it]?.name } - ?: stringResource(R.string.sub2_panel_default_group) - ) - append(" · ") - append(key.status) - append(" · ") - append( - key.lastUsedAt?.let { stringResource(R.string.sub2_panel_last_used, it.take(10)) } - ?: stringResource(R.string.sub2_panel_never_used) - ) - }, - style = MaterialTheme.typography.labelSmall, - color = inkMuted(), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - TextButton(onClick = { - copyToClipboard(context, key.key, copiedToast) - }) { Text(stringResource(R.string.action_copy), style = MaterialTheme.typography.labelSmall) } - if (groups.isNotEmpty()) { - TextButton(onClick = { switchForKeyId = key.id }) { - Text(stringResource(R.string.sub2_panel_group_column), style = MaterialTheme.typography.labelSmall) - } - } - // 删除:用图标而非文字,避免一行挤下三个按钮在窄屏折行 - IconButton(onClick = { deleteForKeyId = key.id }) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = stringResource(R.string.action_delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(18.dp) - ) - } - } - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)) - } - } - } - } - - } - } - - // 删除确认弹窗:删除不可逆,二次确认后才发请求 - deleteForKeyId?.let { keyId -> - val targetName = (keysState as? PanelKeysState.Success) - ?.keys?.firstOrNull { it.id == keyId }?.name - AlertDialog( - onDismissRequest = { deleteForKeyId = null }, - title = { Text(stringResource(R.string.sub2_panel_delete_confirm_title), fontWeight = FontWeight.Bold) }, - text = { - Text( - stringResource( - R.string.sub2_panel_delete_confirm_body, - targetName ?: ("#" + keyId) - ) - ) - }, - confirmButton = { - Button( - onClick = { - onDeleteKey(keyId) - deleteForKeyId = null - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError - ) - ) { Text(stringResource(R.string.action_delete)) } - }, - dismissButton = { - TextButton(onClick = { deleteForKeyId = null }) { Text(stringResource(R.string.action_cancel)) } - } - ) - } - - // 新建 Key 弹窗 - if (showCreate) { - var name by remember { mutableStateOf("") } - var groupId by remember { mutableStateOf(null) } - AlertDialog( - onDismissRequest = { showCreate = false }, - title = { Text(stringResource(R.string.sub2_panel_create_title), fontWeight = FontWeight.Bold) }, - text = { - Column { - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text(stringResource(R.string.sub2_panel_key_name)) }, - singleLine = true, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource(R.string.sub2_panel_group_optional), - style = MaterialTheme.typography.labelMedium, - color = inkMuted() - ) - Spacer(modifier = Modifier.height(2.dp)) - if (groups.isEmpty()) { - Text( - text = stringResource(R.string.sub2_panel_no_groups), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() - ) - } else { - groups.forEach { g -> - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Text( - text = g.name + (if (g.rateMultiplier != 1.0) "(x" + String.format(Locale.getDefault(), "%.2f", g.rateMultiplier) + ")" else ""), - style = MaterialTheme.typography.bodySmall, - color = if (groupId == g.id) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f) - ) - TextButton(onClick = { groupId = if (groupId == g.id) null else g.id }) { - Text( - text = if (groupId == g.id) stringResource(R.string.action_cancel) else stringResource(R.string.sub2_panel_group_column), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - } - } - }, - confirmButton = { - Button( - onClick = { onCreateKey(name.trim(), groupId); showCreate = false }, - enabled = name.isNotBlank() - ) { Text(stringResource(R.string.sub2_panel_create)) } - }, - dismissButton = { - TextButton(onClick = { showCreate = false }) { Text(stringResource(R.string.action_cancel)) } - } - ) - } - - // 新建成功:弹窗展示完整 Key,方便一次性复制(避免明文铺在长消息里) - createdKey?.let { key -> - AlertDialog( - onDismissRequest = onConsumeCreatedKey, - title = { Text(stringResource(R.string.sub2_panel_create_success_title), fontWeight = FontWeight.Bold) }, - text = { - Column { - Text( - text = stringResource(R.string.sub2_panel_create_success_hint), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() - ) - Spacer(modifier = Modifier.height(8.dp)) - SelectionContainer { - Text( - text = key, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(10.dp) - ) - } - } - }, - confirmButton = { - Button(onClick = { copyToClipboard(context, key, copiedToast); onConsumeCreatedKey() }) { - Text(stringResource(R.string.sub2_panel_copy_and_close)) - } - }, - dismissButton = { - TextButton(onClick = onConsumeCreatedKey) { Text(stringResource(R.string.action_close)) } - } - ) - } - - // 兑换弹窗 - if (showRedeem) { - var code by remember { mutableStateOf("") } - AlertDialog( - onDismissRequest = { showRedeem = false }, - title = { Text(stringResource(R.string.sub2_panel_redeem), fontWeight = FontWeight.Bold) }, - text = { - OutlinedTextField( - value = code, - onValueChange = { code = it }, - label = { Text(stringResource(R.string.sub2_panel_redeem_code_label)) }, - singleLine = true, - modifier = Modifier.fillMaxWidth() - ) - }, - confirmButton = { - Button( - onClick = { onRedeem(code.trim()); showRedeem = false }, - enabled = code.isNotBlank() && code.length >= 6 - ) { Text(stringResource(R.string.sub2_panel_redeem)) } - }, - dismissButton = { - TextButton(onClick = { showRedeem = false }) { Text(stringResource(R.string.action_cancel)) } - } - ) - } - - // 切换分组弹窗 - switchForKeyId?.let { keyId -> - AlertDialog( - onDismissRequest = { switchForKeyId = null }, - title = { Text(stringResource(R.string.sub2_panel_switch_group), fontWeight = FontWeight.Bold) }, - text = { - Column { - if (groups.isEmpty()) { - Text( - text = stringResource(R.string.sub2_panel_no_groups), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() - ) - } else { - groups.forEach { g -> - TextButton(onClick = { onSwitchGroup(keyId, g.id); switchForKeyId = null }) { - Text( - text = g.name + (if (g.rateMultiplier != 1.0) "(x" + String.format(Locale.getDefault(), "%.2f", g.rateMultiplier) + ")" else ""), - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - } - }, - confirmButton = {}, - dismissButton = { - TextButton(onClick = { switchForKeyId = null }) { Text(stringResource(R.string.action_cancel)) } - } - ) - } -} - -/** - * API Key 展示掩码:与面板网页保持一致的观感(sk-xxxx...yyyy)。 - * 列表接口虽然返回完整 key(实测 67 字符),但页面上不该铺满明文, - * 复制按钮复制的仍是完整值(见 copyToClipboard(context, key.key, …))。 - */ -private fun maskKey(key: String): String = when { - key.length <= 12 -> key - else -> key.take(7) + "..." + key.takeLast(4) -} - -/** 复制到剪贴板并 Toast 反馈(用户感知「复制按钮」有效)。 */ -private fun copyToClipboard(context: Context, text: String, toast: String) { - val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("API Key", text)) - Toast.makeText(context, toast, Toast.LENGTH_SHORT).show() -} +package com.rainy.token.ui.servicedetail + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.rainy.token.R +import com.rainy.token.data.repository.Sub2ApiRepository +import com.rainy.token.domain.model.ServiceBalance +import com.rainy.token.ui.theme.inkMuted +import java.util.Locale + +/** + * Sub2API 面板管理卡:Key 列表 / 新建 / 分组 / 兑换 / 低余额提醒。 + * + * 首页 Sub2API 卡片与 Sub2API 详情页共用(有面板 Token 时才能加载; + * API Key 模式只读余额,进入后提示先登录面板)。 + * + * 交互细节: + * - Key 列表只展示掩码(sk-…后缀),完整 Key 通过「复制」按钮复制并 Toast 反馈; + * - 新建 Key 成功后弹窗展示完整 Key,避免把明文直接铺在长消息里; + * - 兑换/切换分组等操作结果以 Toast + 行内文案双重反馈。 + */ +@Composable +internal fun Sub2ApiPanelManagement( + balance: ServiceBalance?, + keysState: PanelKeysState, + groups: List, + actionMessage: String?, + createdKey: String? = null, + onLoadKeys: () -> Unit, + onLoadGroups: () -> Unit, + onCreateKey: (String, Long?) -> Unit, + onSwitchGroup: (Long, Long) -> Unit, + onDeleteKey: (Long) -> Unit, + onRedeem: (String) -> Unit, + onConsumeMessage: () -> Unit, + onConsumeCreatedKey: () -> Unit = {}, + /** 弹窗场景由 AlertDialog 的 title 槽展示标题,可关闭内部标题避免重复 */ + showHeader: Boolean = true +) { + val context = LocalContext.current + var showCreate by remember { mutableStateOf(false) } + var showRedeem by remember { mutableStateOf(false) } + var switchForKeyId by remember { mutableStateOf(null) } + // 待删除的 Key:先弹确认框(删除不可逆,避免列表里误触) + var deleteForKeyId by remember { mutableStateOf(null) } + + val groupMap = remember(groups) { groups.associateBy { it.id } } + val copiedToast = stringResource(R.string.sub2_panel_copied) + + // 进入卡片即自动拉取一次 Key 列表与分组(Idle 态),省掉一次手动点击 + LaunchedEffect(Unit) { + if (keysState is PanelKeysState.Idle) { + onLoadKeys() + onLoadGroups() + } + } + + // 最近一次操作结果:用于行内保留展示(Toast 消费后消息会被置空,这里留一份快照) + var lastResult by remember { mutableStateOf(null) } + + // 操作结果(新建/兑换/切组)用 Toast 提醒:避免结果消息藏在长页面里看不见。 + // 消费后再置空,保证下一次「同样的结果」(如连续两次切换分组成功)仍能再次提示。 + LaunchedEffect(actionMessage) { + val msg = actionMessage ?: return@LaunchedEffect + lastResult = msg + Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + onConsumeMessage() + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + ) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + if (showHeader) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.sub2_panel_manage_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onLoadKeys(); onLoadGroups() }) { + Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_refresh)) + } + } + } + // showHeader=false 时不渲染任何标题/刷新行: + // 弹窗场景由调用方把标题与刷新按钮放在同一行(见 DashboardScreen 的 title 槽), + // 这里再放一行会让刷新按钮掉到标题下面,视觉上多出一条空行。 + + (actionMessage ?: lastResult)?.let { msg -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = msg.take(160) + if (msg.length > 160) "…" else "", + style = MaterialTheme.typography.bodySmall, + color = if (msg.startsWith("已创建") || msg.startsWith("分组已") || msg.startsWith("兑换成功")) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.error + ) + } + + val amount = balance?.amount ?: 0.0 + if (amount > 0.0 && amount < 5.0) { + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource( + R.string.sub2_panel_low_balance, + String.format(Locale.getDefault(), "%.2f", amount) + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.35f)) + .padding(horizontal = 10.dp, vertical = 8.dp) + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + // 主要操作(新建 Key / 兑换卡密)固定在列表上方: + // Key 数量多时列表很长,放在底部会被埋在滚动区尽头,不易发现。 + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = { onLoadGroups(); showCreate = true }, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.sub2_panel_create)) + } + OutlinedButton(onClick = { showRedeem = true }, modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.sub2_panel_redeem)) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = stringResource(R.string.sub2_panel_keys_header), + style = MaterialTheme.typography.labelLarge, + color = inkMuted() + ) + Spacer(modifier = Modifier.height(4.dp)) + + when (keysState) { + is PanelKeysState.Idle -> { + OutlinedButton(onClick = onLoadKeys, modifier = Modifier.fillMaxWidth()) { + Text(stringResource(R.string.sub2_panel_load_keys)) + } + } + is PanelKeysState.Loading -> { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.common_loading), style = MaterialTheme.typography.bodySmall, color = inkMuted()) + } + } + is PanelKeysState.Error -> { + Column { + Text(keysState.message, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error) + TextButton(onClick = onLoadKeys) { Text(stringResource(R.string.action_retry)) } + } + } + is PanelKeysState.Success -> { + if (keysState.keys.isEmpty()) { + Text(stringResource(R.string.sub2_panel_no_keys), style = MaterialTheme.typography.bodySmall, color = inkMuted()) + } else { + keysState.keys.forEach { key -> + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.sub2_panel_key_id, key.name, key.id), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // API Key 只显示掩码,避免页面展示过长明文 + Text( + text = buildString { + append(maskKey(key.key)) + append(" · ") + append( + key.groupId?.let { groupMap[it]?.name } + ?: stringResource(R.string.sub2_panel_default_group) + ) + append(" · ") + append(key.status) + append(" · ") + append( + key.lastUsedAt?.let { stringResource(R.string.sub2_panel_last_used, it.take(10)) } + ?: stringResource(R.string.sub2_panel_never_used) + ) + }, + style = MaterialTheme.typography.labelSmall, + color = inkMuted(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + // 三个操作紧凑排布(用户反馈「间隔有点宽」)。 + // 注意:Material3 的 TextButton 会把 58dp 最小宽度与内边距加在 + // 其内层节点上,外层传 defaultMinSize(0)/contentPadding(0) 压不下去 + //(实测按钮可点区仍有 232px),所以这里直接用可点击 Text。 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(R.string.action_copy), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { copyToClipboard(context, key.key, copiedToast) } + .padding(horizontal = 2.dp, vertical = 4.dp) + ) + if (groups.isNotEmpty()) { + Text( + text = stringResource(R.string.sub2_panel_group_column), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { switchForKeyId = key.id } + .padding(horizontal = 2.dp, vertical = 4.dp) + ) + } + // 删除:用图标而非文字,避免一行挤下三个按钮在窄屏折行 + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(R.string.action_delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { deleteForKeyId = key.id } + .padding(2.dp) + .size(18.dp) + ) + } + } + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)) + } + } + } + } + + } + } + + // 删除确认弹窗:删除不可逆,二次确认后才发请求 + deleteForKeyId?.let { keyId -> + val targetName = (keysState as? PanelKeysState.Success) + ?.keys?.firstOrNull { it.id == keyId }?.name + AlertDialog( + onDismissRequest = { deleteForKeyId = null }, + title = { Text(stringResource(R.string.sub2_panel_delete_confirm_title), fontWeight = FontWeight.Bold) }, + text = { + Text( + stringResource( + R.string.sub2_panel_delete_confirm_body, + targetName ?: ("#" + keyId) + ) + ) + }, + confirmButton = { + Button( + onClick = { + onDeleteKey(keyId) + deleteForKeyId = null + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + ) { Text(stringResource(R.string.action_delete)) } + }, + dismissButton = { + TextButton(onClick = { deleteForKeyId = null }) { Text(stringResource(R.string.action_cancel)) } + } + ) + } + + // 新建 Key 弹窗 + if (showCreate) { + var name by remember { mutableStateOf("") } + var groupId by remember { mutableStateOf(null) } + AlertDialog( + onDismissRequest = { showCreate = false }, + title = { Text(stringResource(R.string.sub2_panel_create_title), fontWeight = FontWeight.Bold) }, + text = { + Column { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text(stringResource(R.string.sub2_panel_key_name)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.sub2_panel_group_optional), + style = MaterialTheme.typography.labelMedium, + color = inkMuted() + ) + Spacer(modifier = Modifier.height(2.dp)) + if (groups.isEmpty()) { + Text( + text = stringResource(R.string.sub2_panel_no_groups), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + } else { + groups.forEach { g -> + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Text( + text = g.name + (if (g.rateMultiplier != 1.0) "(x" + String.format(Locale.getDefault(), "%.2f", g.rateMultiplier) + ")" else ""), + style = MaterialTheme.typography.bodySmall, + color = if (groupId == g.id) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + TextButton(onClick = { groupId = if (groupId == g.id) null else g.id }) { + Text( + text = if (groupId == g.id) stringResource(R.string.action_cancel) else stringResource(R.string.sub2_panel_group_column), + style = MaterialTheme.typography.labelSmall + ) + } + } + } + } + } + }, + confirmButton = { + Button( + onClick = { onCreateKey(name.trim(), groupId); showCreate = false }, + enabled = name.isNotBlank() + ) { Text(stringResource(R.string.sub2_panel_create)) } + }, + dismissButton = { + TextButton(onClick = { showCreate = false }) { Text(stringResource(R.string.action_cancel)) } + } + ) + } + + // 新建成功:弹窗展示完整 Key,方便一次性复制(避免明文铺在长消息里) + createdKey?.let { key -> + AlertDialog( + onDismissRequest = onConsumeCreatedKey, + title = { Text(stringResource(R.string.sub2_panel_create_success_title), fontWeight = FontWeight.Bold) }, + text = { + Column { + Text( + text = stringResource(R.string.sub2_panel_create_success_hint), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + Spacer(modifier = Modifier.height(8.dp)) + SelectionContainer { + Text( + text = key, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .padding(10.dp) + ) + } + } + }, + confirmButton = { + Button(onClick = { copyToClipboard(context, key, copiedToast); onConsumeCreatedKey() }) { + Text(stringResource(R.string.sub2_panel_copy_and_close)) + } + }, + dismissButton = { + TextButton(onClick = onConsumeCreatedKey) { Text(stringResource(R.string.action_close)) } + } + ) + } + + // 兑换弹窗 + if (showRedeem) { + var code by remember { mutableStateOf("") } + AlertDialog( + onDismissRequest = { showRedeem = false }, + title = { Text(stringResource(R.string.sub2_panel_redeem), fontWeight = FontWeight.Bold) }, + text = { + OutlinedTextField( + value = code, + onValueChange = { code = it }, + label = { Text(stringResource(R.string.sub2_panel_redeem_code_label)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + }, + confirmButton = { + Button( + onClick = { onRedeem(code.trim()); showRedeem = false }, + enabled = code.isNotBlank() && code.length >= 6 + ) { Text(stringResource(R.string.sub2_panel_redeem)) } + }, + dismissButton = { + TextButton(onClick = { showRedeem = false }) { Text(stringResource(R.string.action_cancel)) } + } + ) + } + + // 切换分组弹窗 + switchForKeyId?.let { keyId -> + AlertDialog( + onDismissRequest = { switchForKeyId = null }, + title = { Text(stringResource(R.string.sub2_panel_switch_group), fontWeight = FontWeight.Bold) }, + text = { + Column { + if (groups.isEmpty()) { + Text( + text = stringResource(R.string.sub2_panel_no_groups), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + } else { + groups.forEach { g -> + TextButton(onClick = { onSwitchGroup(keyId, g.id); switchForKeyId = null }) { + Text( + text = g.name + (if (g.rateMultiplier != 1.0) "(x" + String.format(Locale.getDefault(), "%.2f", g.rateMultiplier) + ")" else ""), + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = { switchForKeyId = null }) { Text(stringResource(R.string.action_cancel)) } + } + ) + } +} + +/** + * API Key 展示掩码:与面板网页保持一致的观感(sk-xxxx...yyyy)。 + * 列表接口虽然返回完整 key(实测 67 字符),但页面上不该铺满明文, + * 复制按钮复制的仍是完整值(见 copyToClipboard(context, key.key, …))。 + */ +private fun maskKey(key: String): String = when { + key.length <= 12 -> key + else -> key.take(7) + "..." + key.takeLast(4) +} + +/** 复制到剪贴板并 Toast 反馈(用户感知「复制按钮」有效)。 */ +private fun copyToClipboard(context: Context, text: String, toast: String) { + val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cm.setPrimaryClip(ClipData.newPlainText("API Key", text)) + Toast.makeText(context, toast, Toast.LENGTH_SHORT).show() +} diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt index 3e69f25..b7452de 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.rainy.token.R import com.rainy.token.domain.model.Sub2DailyUsage @@ -65,7 +66,10 @@ internal fun Sub2ApiUsageCard( onLoadWindowModelStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null, /** 「今天」窗口的按小时趋势;非空时用小时柱替代按天柱(按天只有一根柱没有信息量)。 */ hourlyTrend: List = emptyList(), - onLoadHourlyTrend: (() -> Unit)? = null + onLoadHourlyTrend: (() -> Unit)? = null, + /** 「按 Key」tab 的数据状态(按当前时间周期查询面板使用记录)。 */ + keyStatsState: PanelKeyStatsState = PanelKeyStatsState.Idle, + onLoadKeyStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null ) { val balance = when (state) { is State.Fresh -> state.data @@ -104,6 +108,8 @@ internal fun Sub2ApiUsageCard( var window by remember { mutableStateOf(Sub2Window.TODAY) } // key=detail:刷新后模型列表变化时重置选中态,避免「每日区隐藏+无模型说明」的悬空状态 var selectedModel by remember(detail) { mutableStateOf(null) } + // 明细分区 tab:按模型 / 按 Key + var detailTab by remember(detail) { mutableStateOf(DetailTab.MODEL) } // ── 时间窗口选择 ── LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -140,6 +146,14 @@ internal fun Sub2ApiUsageCard( onLoadWindowModelStats(from, to) } } + + // 「按 Key」tab 打开或切换时间周期时,按当前周期拉取按 Key 用量 + LaunchedEffect(window, detailTab) { + if (onLoadKeyStats != null && detailTab == DetailTab.KEY) { + val (from, to) = window.range() + onLoadKeyStats(from, to) + } + } if (rangeLabel != null) { Spacer(modifier = Modifier.height(8.dp)) Text( @@ -270,7 +284,7 @@ internal fun Sub2ApiUsageCard( } } - // ── 按模型统计(整卡始终展示,数据随所选时间周期变化)── + // ── 明细分区:「按模型 / 按 Key」两个 tab(数据都随所选时间周期变化)── val statsLoading = window != Sub2Window.ALL && (windowStatsState is PanelWindowStatsState.Idle || windowStatsState is PanelWindowStatsState.Loading) val statsError = (windowStatsState as? PanelWindowStatsState.Error)?.message @@ -279,62 +293,90 @@ internal fun Sub2ApiUsageCard( Spacer(modifier = Modifier.height(16.dp)) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) Spacer(modifier = Modifier.height(14.dp)) - Text( - text = stringResource(R.string.sub2_models_header) + - if (windowStatsAllTime) stringResource(R.string.sub2_models_alltime_suffix) else "", - style = MaterialTheme.typography.labelLarge, - color = inkMuted() - ) - Spacer(modifier = Modifier.height(8.dp)) - // 模型筛选 chips(按当前周期内有数据的模型;「全部」时用网关全量) - LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - item { - FilterChip( - selected = selectedModel == null, - onClick = { selectedModel = null }, - label = { Text(stringResource(R.string.sub2_model_all)) }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.primary, - selectedLabelColor = MaterialTheme.colorScheme.onPrimary - ) + // tab 切换:按模型 / 按 Key + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FilterChip( + selected = detailTab == DetailTab.MODEL, + onClick = { detailTab = DetailTab.MODEL }, + label = { Text(stringResource(R.string.sub2_tab_by_model)) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary ) - } - items(windowModelStats) { stat -> - FilterChip( - selected = selectedModel == stat.model, - onClick = { selectedModel = stat.model }, - label = { Text(stat.model) } + ) + FilterChip( + selected = detailTab == DetailTab.KEY, + onClick = { detailTab = DetailTab.KEY }, + label = { Text(stringResource(R.string.sub2_tab_by_key)) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary ) - } + ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(12.dp)) - when { - statsLoading -> Text( - text = stringResource(R.string.common_loading), - style = MaterialTheme.typography.bodySmall, + if (detailTab == DetailTab.KEY) { + KeyUsageSection(keyStatsState) + } else { + Text( + text = stringResource(R.string.sub2_models_header) + + if (windowStatsAllTime) stringResource(R.string.sub2_models_alltime_suffix) else "", + style = MaterialTheme.typography.labelLarge, color = inkMuted() ) - statsError != null -> Text( - text = "✗ " + statsError, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - windowModelStats.isEmpty() -> Text( - text = stringResource(R.string.sub2_no_model_data), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() - ) - else -> { - val shown = windowModelStats.filter { selectedModel == null || it.model == selectedModel } - shown.forEachIndexed { index, stat -> - ModelRow(stat, highlighted = selectedModel == stat.model) - if (index != shown.lastIndex) { - HorizontalDivider( - modifier = Modifier.padding(vertical = 2.dp), - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + Spacer(modifier = Modifier.height(8.dp)) + + // 模型筛选 chips(按当前周期内有数据的模型;「全部」时用网关全量) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + item { + FilterChip( + selected = selectedModel == null, + onClick = { selectedModel = null }, + label = { Text(stringResource(R.string.sub2_model_all)) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary ) + ) + } + items(windowModelStats) { stat -> + FilterChip( + selected = selectedModel == stat.model, + onClick = { selectedModel = stat.model }, + label = { Text(stat.model) } + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + + when { + statsLoading -> Text( + text = stringResource(R.string.common_loading), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + statsError != null -> Text( + text = "✗ " + statsError, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + windowModelStats.isEmpty() -> Text( + text = stringResource(R.string.sub2_no_model_data), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + else -> { + val shown = windowModelStats.filter { selectedModel == null || it.model == selectedModel } + shown.forEachIndexed { index, stat -> + ModelRow(stat, highlighted = selectedModel == stat.model) + if (index != shown.lastIndex) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 2.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + } } } } @@ -344,6 +386,90 @@ internal fun Sub2ApiUsageCard( } } +/** 明细分区 tab:按模型 / 按 Key。 */ +private enum class DetailTab { MODEL, KEY } + +/** 按 Key 用量区域:加载中/错误/空态/列表。 */ +@Composable +private fun KeyUsageSection(state: PanelKeyStatsState) { + when (state) { + is PanelKeyStatsState.Idle, + is PanelKeyStatsState.Loading -> Text( + text = stringResource(R.string.common_loading), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + is PanelKeyStatsState.Error -> Text( + text = "✗ " + state.message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + is PanelKeyStatsState.Success -> { + if (state.stats.isEmpty()) { + Text( + text = stringResource(R.string.sub2_no_key_data), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + } else { + state.stats.forEachIndexed { index, stat -> + KeyUsageRow(stat) + if (index != state.stats.lastIndex) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 2.dp), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + } + } + } + } + } +} + +/** Key 一行:名称(+分组) + 请求/tokens + cost;未使用的 Key 灰显。 */ +@Composable +private fun KeyUsageRow(stat: PanelKeyStatsState.KeyStat) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stat.name.ifBlank { "#" + stat.keyId }, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = if (stat.unused) inkMuted() else MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = buildString { + stat.groupName?.takeIf { it.isNotBlank() }?.let { append(it).append(" · ") } + if (stat.unused) append(stringResource(R.string.sub2_key_unused)) + else append( + stringResource( + R.string.sub2_model_meta, + compactNum(stat.requests), + compactNum(stat.totalTokens) + ) + ) + }, + style = MaterialTheme.typography.bodySmall, + color = inkMuted(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = if (stat.unused) "-" else formatCost(stat.cost), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = if (stat.unused) inkMuted() else MaterialTheme.colorScheme.onSurface + ) + } +} + /** 时间窗口:按日期切片(近 N 天 = 今天及之前 N-1 天),不再按行数。 */ private enum class Sub2Window(val labelRes: Int) { TODAY(R.string.sub2_window_today), 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 5386cc7..f8cc7c0 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -1,680 +1,688 @@ - - - - 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 次 - 余额 + + + + 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 的客户端会立即失效。 + 此图为账号整体趋势(无单模型维度) \ 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 5494cab..7eeb549 100644 --- a/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/app/src/main/res/values-b+zh+Hant/strings.xml @@ -1,680 +1,688 @@ - - - - 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 次 - 餘額 + + + + 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 的用戶端會立即失效。 + 此圖為帳號整體趨勢(無單模型維度) \ 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 1f5a549..9ab91f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,685 +1,693 @@ - - - - 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 + + + + 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) \ No newline at end of file