feat: add CommandCode Go support (JSON API via API Key)

- New CommandCodeGoRepository: calls api.commandcode.ai/alpha/billing/credits + /alpha/billing/subscriptions
- ServiceType: COMMANDCODE_GO added (reordered: OCGO → CCGO → DEEPSEEK)
- ServiceConfigProvider: REST_API config with $ unit
- NetworkModule: DI for CommandCodeGoRepository
- RefreshBalanceUseCase: COMMANDCODE_GO branch
- CredentialEditViewModel: inject CommandCodeGoRepository, allow testAndSave
- ServiceIcon: ⚡ emoji for CommandCode Go
- DashboardScreen: CCGO card with 3 windows (5h/weekly/monthly) like OCGO
- ServiceDetailScreen: CommandCodeGoUsageCard with 5h→weekly→monthly order
- Fixed resetAt: epoch millis → remaining seconds conversion
This commit is contained in:
WaterRain 2026-06-26 08:04:16 +00:00
parent d37589b89a
commit 0f26269b7e
No known key found for this signature in database
9 changed files with 460 additions and 5 deletions

View File

@ -0,0 +1,237 @@
package com.rainy.token.data.repository
import com.rainy.token.data.cache.BalanceCache
import com.rainy.token.domain.model.Credential
import com.rainy.token.domain.model.ServiceBalance
import com.rainy.token.domain.service.ServiceConfigProvider
import com.rainy.token.domain.service.ServiceType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.double
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import javax.inject.Singleton
/**
* CommandCode Go 余额仓库。
*
* 调 JSON API 获取月度配额余额 + 用量窗口信息:
* GET https://api.commandcode.ai/alpha/billing/credits
* Authorization: Bearer <API Key>
*
* 调 subscription 端点获取计划信息(用来算已用/总量百分比):
* GET https://api.commandcode.ai/alpha/billing/subscriptions
* Authorization: Bearer <API Key>
*/
@Singleton
class CommandCodeGoRepository(
private val okHttpClient: OkHttpClient,
private val credentialRepository: CredentialRepository,
private val balanceCache: BalanceCache
) {
private val json = Json { ignoreUnknownKeys = true }
private val apiBase = "https://api.commandcode.ai"
suspend fun fetchBalance(): Result<ServiceBalance> = withContext(Dispatchers.IO) {
val credential = credentialRepository.get(ServiceType.COMMANDCODE_GO)
?: return@withContext Result.failure(RepositoryError.InvalidCredential())
if (credential !is Credential.ApiKeyCredential) {
return@withContext Result.failure(RepositoryError.InvalidCredential())
}
val apiKey = credential.key
if (apiKey.isBlank()) {
return@withContext Result.failure(RepositoryError.InvalidCredential())
}
// 并行拉取 credits + subscriptions
val creditsResult = runCatching { fetchCredits(apiKey) }
val subResult = runCatching { fetchSubscription(apiKey) }
val creditsPayload = creditsResult.getOrElse { e ->
return@withContext Result.failure(
if (e is IOException) RepositoryError.Network(e)
else RepositoryError.Unknown(e)
)
}
// 从订阅信息拿计划名称,查 plan catalog 拿总量
val monthlyTotal = subResult.getOrNull()?.let { sub ->
PLANS[sub.planId.lowercase()]
}
val billingPeriodEndMillis = subResult.getOrNull()?.let { parseIsoToEpoch(it.currentPeriodEnd) }
val config = ServiceConfigProvider.get(ServiceType.COMMANDCODE_GO)
val used = monthlyTotal?.let { total ->
maxOf(0.0, total - creditsPayload.monthlyCredits)
}
val extras = buildMap {
put("monthlyRemaining", creditsPayload.monthlyCredits.toString())
put("purchasedCredits", creditsPayload.purchasedCredits.toString())
put("freeCredits", creditsPayload.freeCredits.toString())
monthlyTotal?.let { put("monthlyTotal", it.toString()) }
used?.let { put("monthlyUsed", it.toString()) }
creditsPayload.fiveHourUsed?.let { put("fiveHour.used", it.toString()) }
creditsPayload.fiveHourCap?.let { put("fiveHour.cap", it.toString()) }
creditsPayload.fiveHourResetAt?.let { put("fiveHour.resetInSec", epochToRemainingSec(it).toString()) }
creditsPayload.weeklyUsed?.let { put("weekly.used", it.toString()) }
creditsPayload.weeklyCap?.let { put("weekly.cap", it.toString()) }
creditsPayload.weeklyResetAt?.let { put("weekly.resetInSec", epochToRemainingSec(it).toString()) }
billingPeriodEndMillis?.let {
put("billingPeriodEnd", it.toString())
put("monthly.resetInSec", epochToRemainingSec(it).toString())
}
subResult.getOrNull()?.planId?.let { put("planId", it) }
subResult.getOrNull()?.planId?.let { put("planName", planDisplayName(it)) }
}
val balance = ServiceBalance(
service = ServiceType.COMMANDCODE_GO,
amount = creditsPayload.monthlyCredits,
unit = config.displayUnit,
isAvailable = true,
monthlySpent = used,
totalQuota = monthlyTotal,
nextResetAt = billingPeriodEndMillis,
extras = extras
)
balanceCache.put(ServiceType.COMMANDCODE_GO, balance)
credentialRepository.save(credential.copy(lastVerifiedAt = System.currentTimeMillis()))
Result.success(balance)
}
private fun fetchCredits(apiKey: String): CreditsPayload {
val request = Request.Builder()
.url("$apiBase/alpha/billing/credits")
.header("Authorization", "Bearer $apiKey")
.header("Accept", "application/json")
.get()
.build()
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) {
throw IOException("HTTP ${response.code}: ${response.body?.string().orEmpty()}")
}
val body = response.body?.string() ?: throw IOException("空响应体")
val root = json.parseToJsonElement(body).jsonObject
val credits = root["credits"]?.jsonObject ?: throw IOException("缺少 credits 字段")
fun getDouble(obj: JsonObject, key: String): Double =
obj[key]?.jsonPrimitive?.double ?: 0.0
fun getLong(obj: JsonObject, key: String): Long? =
obj[key]?.jsonPrimitive?.long
val fiveHour = root["windowLimits"]?.jsonObject?.let { limits ->
if (limits["limited"]?.jsonPrimitive?.content == "true") {
limits["fiveHour"]?.jsonObject
} else null
}
val weekly = root["windowLimits"]?.jsonObject?.let { limits ->
if (limits["limited"]?.jsonPrimitive?.content == "true") {
limits["weekly"]?.jsonObject
} else null
}
return CreditsPayload(
monthlyCredits = getDouble(credits, "monthlyCredits"),
purchasedCredits = getDouble(credits, "purchasedCredits"),
freeCredits = getDouble(credits, "freeCredits"),
fiveHourUsed = fiveHour?.let { getDouble(it, "used") },
fiveHourCap = fiveHour?.let { getDouble(it, "cap") },
fiveHourResetAt = fiveHour?.let { getLong(it, "resetAt") },
weeklyUsed = weekly?.let { getDouble(it, "used") },
weeklyCap = weekly?.let { getDouble(it, "cap") },
weeklyResetAt = weekly?.let { getLong(it, "resetAt") }
)
}
private fun fetchSubscription(apiKey: String): SubscriptionPayload? {
val request = Request.Builder()
.url("$apiBase/alpha/billing/subscriptions")
.header("Authorization", "Bearer $apiKey")
.header("Accept", "application/json")
.get()
.build()
val response = try {
okHttpClient.newCall(request).execute()
} catch (_: Exception) {
return null
}
if (!response.isSuccessful) return null
val body = response.body?.string() ?: return null
return try {
val root = json.parseToJsonElement(body).jsonObject
if (root["success"]?.jsonPrimitive?.content != "true") return null
val data = root["data"]?.jsonObject ?: return null
SubscriptionPayload(
planId = data["planId"]?.jsonPrimitive?.content.orEmpty(),
currentPeriodEnd = data["currentPeriodEnd"]?.jsonPrimitive?.content
)
} catch (_: Exception) { null }
}
private fun planDisplayName(planId: String): String =
PLAN_NAMES[planId.lowercase()] ?: planId
companion object {
private val PLANS = mapOf(
"individual-go" to 10.0,
"individual-pro" to 30.0,
"individual-max" to 150.0,
"individual-ultra" to 300.0
)
private val PLAN_NAMES = mapOf(
"individual-go" to "Go",
"individual-pro" to "Pro",
"individual-max" to "Max",
"individual-ultra" to "Ultra"
)
/** API 返回的是 epoch millis,转为距现在的剩余秒数 */
private fun epochToRemainingSec(epochMillis: Long): Long =
maxOf(0L, (epochMillis - System.currentTimeMillis()) / 1000)
private fun parseIsoToEpoch(isoStr: String?): Long? {
if (isoStr == null) return null
return try {
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.US)
sdf.timeZone = java.util.TimeZone.getTimeZone("UTC")
sdf.parse(isoStr.take(19))?.time
} catch (_: Exception) { null }
}
}
private data class CreditsPayload(
val monthlyCredits: Double,
val purchasedCredits: Double,
val freeCredits: Double,
val fiveHourUsed: Double?,
val fiveHourCap: Double?,
val fiveHourResetAt: Long?,
val weeklyUsed: Double?,
val weeklyCap: Double?,
val weeklyResetAt: Long?
)
private data class SubscriptionPayload(
val planId: String,
val currentPeriodEnd: String?
)
}

View File

@ -9,6 +9,7 @@ import com.rainy.token.data.local.UsageCache
import com.rainy.token.data.local.usageCacheDataStore
import com.rainy.token.data.repository.CredentialRepository
import com.rainy.token.data.repository.DeepSeekRepository
import com.rainy.token.data.repository.CommandCodeGoRepository
import com.rainy.token.data.repository.OpenCodeGoRepository
import com.rainy.token.data.repository.OpenCodeUsageRepository
import dagger.Module
@ -101,6 +102,17 @@ object NetworkModule {
balanceCache: BalanceCache
): OpenCodeGoRepository = OpenCodeGoRepository(okHttpClient, credentialRepository, balanceCache)
/**
* CommandCode Go 仓库:API Key 认证,调 JSON API。
*/
@Provides
@Singleton
fun provideCommandCodeGoRepository(
okHttpClient: OkHttpClient,
credentialRepository: CredentialRepository,
balanceCache: BalanceCache
): CommandCodeGoRepository = CommandCodeGoRepository(okHttpClient, credentialRepository, balanceCache)
/**
* DeepSeek 仓库:同上。注意即使 [DeepSeekRepository] 自己有 @Inject constructor,
* 在 3+ Repository 同模式时 KSP 2.x 仍会误报,这里统一改 @Provides 杜绝隐患。

View File

@ -48,6 +48,12 @@ object ServiceConfigProvider {
method = FetchMethod.WEBVIEW_SCRAPER,
loginUrl = "https://opencode.ai/auth",
displayUnit = "%"
),
ServiceType.COMMANDCODE_GO to ServiceConfig(
type = ServiceType.COMMANDCODE_GO,
method = FetchMethod.REST_API,
loginUrl = "", // API Key 模式不需要登录 URL
displayUnit = "$"
)
)

View File

@ -15,6 +15,7 @@ enum class ServiceType(
val storageKey: String
) {
OPENCODE_GO("OpenCode Go", "opencode_go"),
COMMANDCODE_GO("CommandCode Go", "commandcode_go"),
DEEPSEEK("DeepSeek", "deepseek");
companion object {

View File

@ -1,5 +1,6 @@
package com.rainy.token.domain.usecase
import com.rainy.token.data.repository.CommandCodeGoRepository
import com.rainy.token.data.repository.DeepSeekRepository
import com.rainy.token.data.repository.OpenCodeGoRepository
import com.rainy.token.domain.model.ServiceBalance
@ -10,7 +11,7 @@ import javax.inject.Provider
/**
* 唯一的 UseCase。计划架构补充说明:
* - 内部按 ServiceType 分发到对应 Repository
* - 现阶段实现:DeepSeek(REST API) + OpenCode Go(OkHttp 抓 dashboard)
* - 现阶段实现:DeepSeek(REST API) + OpenCode Go(OkHttp 抓 dashboard)+ CommandCode Go(JSON API)
* - **使用 Provider 注入**——规避 KSP 2.x 在多个 @Inject constructor Repository
* 注入同一 UseCase 时的"could not be resolved"误报(KSP 已知 issue)。
* Provider 让 Hilt 推迟创建 Repository 实例到第一次 .get() 时,KSP 不需要在
@ -18,10 +19,12 @@ import javax.inject.Provider
*/
class RefreshBalanceUseCase @Inject constructor(
private val deepSeekRepositoryProvider: Provider<DeepSeekRepository>,
private val openCodeGoRepositoryProvider: Provider<OpenCodeGoRepository>
private val openCodeGoRepositoryProvider: Provider<OpenCodeGoRepository>,
private val commandCodeGoRepositoryProvider: Provider<CommandCodeGoRepository>
) {
suspend operator fun invoke(service: ServiceType): Result<ServiceBalance> = when (service) {
ServiceType.DEEPSEEK -> deepSeekRepositoryProvider.get().fetchBalance()
ServiceType.OPENCODE_GO -> openCodeGoRepositoryProvider.get().fetchBalance()
ServiceType.COMMANDCODE_GO -> commandCodeGoRepositoryProvider.get().fetchBalance()
}
}

View File

@ -51,6 +51,21 @@ fun ServiceIcon(
)
}
}
ServiceType.COMMANDCODE_GO -> {
// CommandCode Go:暂无官方 logo,用 ⚡ emoji 占位
Box(
modifier = modifier
.size(size.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFF2D3748)),
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = "⚡",
fontSize = (size * 0.5).sp
)
}
}
ServiceType.DEEPSEEK -> {
// DeepSeek 官方无开放 logo 资源 —— 用蓝鲸 emoji 占位
Box(

View File

@ -297,6 +297,12 @@ private fun BalanceMainArea(card: DashboardCardUi) {
// 3 个窗口进度条
OpenCodeGoUsageWindows(balance)
}
card.service == ServiceType.COMMANDCODE_GO -> {
// CommandCode Go 卡片:跟 OCGO 一样平铺三档窗口
CommandCodeGoMainBalance(balance)
Spacer(modifier = Modifier.height(12.dp))
CommandCodeGoUsageWindows(balance)
}
else -> {
// DeepSeek 等:主数字 + 单位
Row(verticalAlignment = Alignment.Bottom) {
@ -358,7 +364,6 @@ private fun OpenCodeGoMainBalance(balance: com.rainy.token.domain.model.ServiceB
)
}
}
@Composable
private fun OpenCodeGoUsageWindows(balance: com.rainy.token.domain.model.ServiceBalance) {
val windows = listOf(
@ -371,7 +376,65 @@ private fun OpenCodeGoUsageWindows(balance: com.rainy.token.domain.model.Service
if (pct != null) {
CompactUsageRow(label = label, pct = pct, resetInSec = resetSec)
} else {
// 数据未拿到 —— 不隐藏整行,显示"—"占位
CompactUsageRowEmpty(label = label, resetInSec = resetSec)
}
}
}
}
@Composable
private fun CommandCodeGoMainBalance(balance: com.rainy.token.domain.model.ServiceBalance) {
val total = balance.totalQuota
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = formatAmount(balance.amount),
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "$",
style = MaterialTheme.typography.titleLarge,
color = inkMuted(),
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 6.dp, start = 2.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "剩余",
style = MaterialTheme.typography.titleMedium,
color = inkMuted(),
modifier = Modifier.padding(bottom = 6.dp)
)
if (total != null && total > 0) {
val used = total - balance.amount
Text(
text = " · 已用 ${formatAmount(used)} / 共 ${formatAmount(total)}",
style = MaterialTheme.typography.bodySmall,
color = inkMuted(),
modifier = Modifier.padding(bottom = 6.dp)
)
}
}
}
@Composable
private fun CommandCodeGoUsageWindows(balance: com.rainy.token.domain.model.ServiceBalance) {
val extras = balance.extras
fun calcPct(used: Double?, cap: Double?): Int? {
if (used == null || cap == null || cap <= 0) return null
return ((used / cap) * 100).toInt().coerceIn(0, 100)
}
val windows = listOf(
Triple("5 小时", calcPct(extras["fiveHour.used"]?.toDoubleOrNull(), extras["fiveHour.cap"]?.toDoubleOrNull()), extras["fiveHour.resetInSec"]?.toLongOrNull()),
Triple("本周", calcPct(extras["weekly.used"]?.toDoubleOrNull(), extras["weekly.cap"]?.toDoubleOrNull()), extras["weekly.resetInSec"]?.toLongOrNull()),
Triple("本月", calcPct(balance.monthlySpent, balance.totalQuota), extras["monthly.resetInSec"]?.toLongOrNull())
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
windows.forEach { (label, pct, resetSec) ->
if (pct != null) {
CompactUsageRow(label = label, pct = pct, resetInSec = resetSec)
} else {
CompactUsageRowEmpty(label = label, resetInSec = resetSec)
}
}
@ -501,6 +564,7 @@ private fun DashboardCardUi.statusBadgeStyle(): StatusStyle = when {
private fun secondaryLine(card: DashboardCardUi): String = when (card.service) {
ServiceType.DEEPSEEK -> "REST API · ¥"
ServiceType.OPENCODE_GO -> "WebView 抓取 · 5h 配额"
ServiceType.COMMANDCODE_GO -> "JSON API · \$"
}
private fun footerText(card: DashboardCardUi): String {

View File

@ -131,6 +131,9 @@ fun ServiceDetailScreen(
ServiceType.OPENCODE_GO -> {
item { OpenCodeGoWindowsCard(uiState.state) }
}
ServiceType.COMMANDCODE_GO -> {
item { CommandCodeGoUsageCard(uiState.state) }
}
}
// 错误信息(如有)
@ -161,6 +164,117 @@ fun ServiceDetailScreen(
}
}
/**
* CommandCode Go 专属:月度用量卡 + 窗口进度条。
*
* 数据从 balance.extras 中的 monthlyRemaining / monthlyTotal / fiveHour / weekly 取,
* 展示月度已用/总量 + 5h + 每周窗口进度条。
* 窗口顺序:5小时 → 本周 → 本月(最底部为月度总览)。
*/
@Composable
private fun CommandCodeGoUsageCard(state: State) {
val balance = when (state) {
is State.Fresh -> state.data
is State.Stale -> state.data
is State.Error -> state.cached
else -> null
}
val extras = balance?.extras ?: return
val monthlyRemaining = extras["monthlyRemaining"]?.toDoubleOrNull() ?: return
val monthlyTotal = extras["monthlyTotal"]?.toDoubleOrNull()
val purchased = extras["purchasedCredits"]?.toDoubleOrNull() ?: 0.0
val planName = extras["planName"].orEmpty()
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = "用量窗口${if (planName.isNotBlank()) " · $planName" else ""}",
style = MaterialTheme.typography.labelLarge,
color = inkMuted()
)
Spacer(modifier = Modifier.height(12.dp))
// 1. 5h 窗口(最上面)
val fiveHourUsed = extras["fiveHour.used"]?.toDoubleOrNull()
val fiveHourCap = extras["fiveHour.cap"]?.toDoubleOrNull()
if (fiveHourUsed != null && fiveHourCap != null && fiveHourCap > 0) {
val pct = ((fiveHourUsed / fiveHourCap) * 100).toInt().coerceIn(0, 100)
UsageWindowRow(
label = "5 小时滚动",
pct = pct,
resetInSec = extras["fiveHour.resetInSec"]?.toLongOrNull()
)
Spacer(modifier = Modifier.height(14.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(14.dp))
}
// 2. 每周窗口
val weeklyUsed = extras["weekly.used"]?.toDoubleOrNull()
val weeklyCap = extras["weekly.cap"]?.toDoubleOrNull()
if (weeklyUsed != null && weeklyCap != null && weeklyCap > 0) {
val pct = ((weeklyUsed / weeklyCap) * 100).toInt().coerceIn(0, 100)
UsageWindowRow(
label = "本周",
pct = pct,
resetInSec = extras["weekly.resetInSec"]?.toLongOrNull()
)
Spacer(modifier = Modifier.height(14.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(14.dp))
}
// 3. 本月(最下面)
if (monthlyTotal != null && monthlyTotal > 0) {
val used = monthlyTotal - monthlyRemaining
val pct = ((used / monthlyTotal) * 100).toInt().coerceIn(0, 100)
UsageWindowRow(
label = "本月",
pct = pct,
resetInSec = extras["billingPeriodEnd"]?.let { parseIsoDuration(it) }
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "已用 \$${formatAmount(used)} / 共 \$${formatAmount(monthlyTotal)}",
style = MaterialTheme.typography.bodySmall,
color = inkMuted()
)
} else {
Text(
text = "剩余 \$${formatAmount(monthlyRemaining)}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
// 额外充值
if (purchased > 0) {
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(8.dp))
BreakdownRow("额外充值", purchased, "$")
}
}
}
}
/**
* 尝试从 ISO8601 时间戳计算剩余秒数。
*/
private fun parseIsoDuration(isoStr: String): Long? {
return try {
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.US)
sdf.timeZone = java.util.TimeZone.getTimeZone("UTC")
val end = sdf.parse(isoStr.take(19))?.time ?: return null
maxOf(0L, (end - System.currentTimeMillis()) / 1000)
} catch (_: Exception) { null }
}
/**
* OpenCode Go 专属:3 个窗口(rolling 5h / weekly / monthly)独立用量卡。
*
@ -532,6 +646,7 @@ private fun ActionButtons(
private fun mainCardLabel(service: ServiceType): String = when (service) {
ServiceType.DEEPSEEK -> "当前余额"
ServiceType.OPENCODE_GO -> "5h 实时用量"
ServiceType.COMMANDCODE_GO -> "月度余额"
}
private fun stateToChip(state: State): StatusStyle = when (state) {

View File

@ -5,6 +5,7 @@ import android.content.ClipboardManager
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rainy.token.data.repository.CommandCodeGoRepository
import com.rainy.token.data.repository.CredentialRepository
import com.rainy.token.data.repository.OpenCodeGoRepository
import com.rainy.token.data.repository.RepositoryError
@ -34,6 +35,7 @@ import javax.inject.Provider
class CredentialEditViewModel @Inject constructor(
private val credentialRepository: CredentialRepository,
private val openCodeGoRepositoryProvider: Provider<OpenCodeGoRepository>,
private val commandCodeGoRepositoryProvider: Provider<CommandCodeGoRepository>,
private val refreshBalanceUseCaseProvider: Provider<RefreshBalanceUseCase>
) : ViewModel() {
@ -125,7 +127,7 @@ class CredentialEditViewModel @Inject constructor(
*/
fun testAndSaveApiKey() {
val type = serviceType ?: return
if (type != ServiceType.DEEPSEEK) {
if (type != ServiceType.DEEPSEEK && type != ServiceType.COMMANDCODE_GO) {
_uiState.update { it.copy(message = "暂不支持测试此服务") }
return
}