feat(trae): INTL 美元计费展示 + apiHost + 充值入口 + INTL 网关修正
- Trae 凭据新增 apiHost(回调 host 自动保存,如 api-us-east.trae.ai) - INTL 余额改走官方 v1 计费接口(grow-normal/api-us-east),解析 Free plan 请求额度(Premium fast/slow、高级、自动补全) - 首页 INTL 不再显示空 0 Credits,改为按月配额展示 - Trae 凭据页新增「充值 / 升级」入口(CN→trae.cn/subscription,INTL→trae.ai/pricing) - INTL 模型/对话网关修正为 core-normal.trae.ai(实测接口有效)
This commit is contained in:
parent
4ae2aa01b6
commit
cf6f426a36
@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.rainy.token"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 36
|
||||
versionName = "1.7.16"
|
||||
versionCode = 37
|
||||
versionName = "1.7.17"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -55,11 +55,7 @@ class TraeChatProxy @Inject constructor(
|
||||
val effectiveRegion = forcedRegion ?: runCatching {
|
||||
ProviderRegion.valueOf(credential.region.uppercase())
|
||||
}.getOrNull()
|
||||
val base = if (effectiveRegion == ProviderRegion.INTL) {
|
||||
"https://a0ai-api-sg.byteintlapi.com"
|
||||
} else {
|
||||
"https://trae-api-cn.mchost.guru"
|
||||
}
|
||||
val base = chatBase(effectiveRegion)
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
|
||||
@ -94,11 +90,7 @@ class TraeChatProxy @Inject constructor(
|
||||
val effectiveRegion = forcedRegion ?: runCatching {
|
||||
ProviderRegion.valueOf(credential.region.uppercase())
|
||||
}.getOrNull()
|
||||
val base = if (effectiveRegion == ProviderRegion.INTL) {
|
||||
"https://a0ai-api-sg.byteintlapi.com"
|
||||
} else {
|
||||
"https://trae-api-cn.mchost.guru"
|
||||
}
|
||||
val base = chatBase(effectiveRegion)
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
|
||||
@ -237,6 +229,17 @@ class TraeChatProxy @Inject constructor(
|
||||
return MODEL_ALIASES[id.lowercase()] ?: id
|
||||
}
|
||||
|
||||
/**
|
||||
* INTL 统一走 Web 网关 core-normal.trae.ai(实测 api-us-east/a0ai 均不提供
|
||||
* llm_utils_chat 路由);CN 走 IDE 网关 trae-api-cn.mchost.guru。
|
||||
*/
|
||||
private fun chatBase(region: ProviderRegion?): String =
|
||||
if (region == ProviderRegion.INTL) {
|
||||
"https://core-normal.trae.ai"
|
||||
} else {
|
||||
"https://trae-api-cn.mchost.guru"
|
||||
}
|
||||
|
||||
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential, region: ProviderRegion?): String {
|
||||
val src = try {
|
||||
json.parseToJsonElement(raw) as? JsonObject
|
||||
|
||||
@ -77,8 +77,11 @@ class TraeModelProvider @Inject constructor(
|
||||
val jwt = credential.jwt.trim().takeIf { it.isNotBlank() } ?: return@withContext false
|
||||
|
||||
val intl = effectiveRegion == ProviderRegion.INTL
|
||||
val base = if (intl) "https://a0ai-api-sg.byteintlapi.com" else "https://trae-api-cn.mchost.guru"
|
||||
val base = if (intl) "https://core-normal.trae.ai" else "https://trae-api-cn.mchost.guru"
|
||||
val origin = if (intl) "https://solo.trae.ai" else "https://solo.trae.cn"
|
||||
val userRegion = if (intl) {
|
||||
if (credential.apiHost?.contains("us", ignoreCase = true) == true) "US" else "SG"
|
||||
} else "CN"
|
||||
val url = base + "/api/remote/v1/models" +
|
||||
"?functions=solo_agent_remote%2Csolo_work_remote%2Csolo_design_remote" +
|
||||
"&show_custom_model=true"
|
||||
@ -88,7 +91,7 @@ class TraeModelProvider @Inject constructor(
|
||||
"Content-Type" to "application/json",
|
||||
"X-Trae-Client-Type" to "web",
|
||||
"X-Preferenced-Language" to "zh-CN",
|
||||
"x-user-region" to if (intl) "SG" else "CN",
|
||||
"x-user-region" to userRegion,
|
||||
"Origin" to origin,
|
||||
"Referer" to origin + "/",
|
||||
"User-Agent" to (
|
||||
|
||||
@ -66,9 +66,12 @@ class TraeRepository constructor(
|
||||
}
|
||||
|
||||
val regionKey = credential.region.uppercase().takeIf { it == "INTL" } ?: "CN"
|
||||
val url = when (regionKey) {
|
||||
"INTL" -> "https://grow-normal.trae.ai/trae/api/v1/pay/ide_user_ent_usage"
|
||||
else -> "https://api.trae.cn/trae/api/v2/pay/ide_user_ent_usage"
|
||||
val url = if (regionKey == "INTL") {
|
||||
val host = credential.apiHost?.takeIf { it.startsWith("http") }?.trimEnd('/')
|
||||
?: "https://grow-normal.trae.ai"
|
||||
host + "/trae/api/v1/pay/ide_user_ent_usage"
|
||||
} else {
|
||||
"https://api.trae.cn/trae/api/v2/pay/ide_user_ent_usage"
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
@ -124,6 +127,8 @@ class TraeRepository constructor(
|
||||
|
||||
root["is_credits_billing"]?.jsonPrimitive?.contentOrNull?.let { extras["is_credits_billing"] = it }
|
||||
root["is_dollar_usage_billing"]?.jsonPrimitive?.contentOrNull?.let { extras["is_dollar_usage_billing"] = it }
|
||||
val isDollarBilling = root["is_dollar_usage_billing"]?.jsonPrimitive?.contentOrNull?.equals("true", true) == true
|
||||
if (isDollarBilling) extras["billing_mode"] = "dollar"
|
||||
root["is_pay_freshman"]?.jsonPrimitive?.contentOrNull?.let { extras["is_new_user"] = it }
|
||||
|
||||
// 1) 权威汇总
|
||||
@ -156,6 +161,26 @@ class TraeRepository constructor(
|
||||
?: base?.get("product_extra")?.jsonObject
|
||||
?.get("package_extra")?.jsonObject?.getString("package_name")
|
||||
?: "pack[$index]"
|
||||
if (isDollarBilling) {
|
||||
packCount++
|
||||
if (packShown == 0) {
|
||||
extras["intl.plan_name"] = name
|
||||
extras["intl.billing_version"] = root["billing_version"]?.jsonPrimitive?.contentOrNull ?: ""
|
||||
packShown++
|
||||
}
|
||||
quota?.let { q ->
|
||||
q.getNumber("premium_model_fast_request_limit")?.let { extras["intl.premium_fast_limit"] = trimNum(it) }
|
||||
q.getNumber("premium_model_slow_request_limit")?.let { extras["intl.premium_slow_limit"] = trimNum(it) }
|
||||
q.getNumber("advanced_model_request_limit")?.let { extras["intl.advanced_limit"] = trimNum(it) }
|
||||
q.getNumber("auto_completion_limit")?.let { extras["intl.auto_completion_limit"] = trimNum(it) }
|
||||
q.getNumber("basic_usage_limit")?.let { extras["intl.basic_usage_limit"] = trimNum(it) }
|
||||
q.getNumber("bonus_usage_limit")?.let { extras["intl.bonus_usage_limit"] = trimNum(it) }
|
||||
}
|
||||
pack.getNumber("usage", "basic_usage_amount")?.let { extras["intl.basic_used"] = trimNum(it) }
|
||||
pack.getNumber("usage", "bonus_usage_amount")?.let { extras["intl.bonus_used"] = trimNum(it) }
|
||||
pack.getNumber("usage", "credits_amount")?.let { extras["intl.credits_used"] = trimNum(it) }
|
||||
return@forEachIndexed
|
||||
}
|
||||
val limit = quota?.getNumber("credits_limit")?.toLong()
|
||||
val used = pack.getNumber("usage", "credits_amount")
|
||||
?: base?.get("usage")?.jsonObject?.get("credits_amount")?.safeDouble()
|
||||
|
||||
@ -79,8 +79,10 @@ sealed class Credential {
|
||||
override val service: ServiceType,
|
||||
/** `Authorization: Cloud-IDE-JWT <jwt>` 的 JWT 值 */
|
||||
val jwt: String,
|
||||
/** 区域:"CN"(api.trae.cn)/ "INTL"(grow-normal.trae.ai) */
|
||||
/** 区域:"CN"(api.trae.cn)/ "INTL"(grow-normal.trae.ai / api-us-east.trae.ai) */
|
||||
val region: String = "CN",
|
||||
/** 登录回调携带的账号 API 主机(如 https://api-us-east.trae.ai);null=旧凭据用区域默认 */
|
||||
val apiHost: String? = null,
|
||||
/** 登录会话绑定的设备 ID(App 持久化每机唯一),null=旧凭据未保存 */
|
||||
val deviceId: String? = null,
|
||||
/** OAuth 用户 ID(回调 userInfo.userId),null=旧凭据未保存 */
|
||||
|
||||
@ -146,6 +146,10 @@ internal fun BalanceMainArea(card: DashboardCardUi) {
|
||||
@Composable
|
||||
private fun TraeSplitMainBalance(balance: ServiceBalance) {
|
||||
val extras = balance.extras
|
||||
if (extras?.get("billing_mode") == "dollar") {
|
||||
TraeDollarPlanSummary(extras)
|
||||
return
|
||||
}
|
||||
val general = extras?.get("general_credits")?.toDoubleOrNull()
|
||||
val work = extras?.get("work_credits")?.toDoubleOrNull()
|
||||
if (general == null && work == null) {
|
||||
@ -176,7 +180,7 @@ private fun TraeSplitMainBalance(balance: ServiceBalance) {
|
||||
|
||||
/** 首页 Trae 分区一行:左侧分区名,右侧小计 + 单位。 */
|
||||
@Composable
|
||||
private fun TraeSplitLine(label: String, value: Double?) {
|
||||
private fun TraeSplitLine(label: String, value: Double?, unit: String = "Credits") {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
@ -196,7 +200,7 @@ private fun TraeSplitLine(label: String, value: Double?) {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Credits",
|
||||
text = unit,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = inkMuted(),
|
||||
modifier = Modifier.padding(bottom = 2.dp)
|
||||
@ -205,6 +209,21 @@ private fun TraeSplitLine(label: String, value: Double?) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TraeDollarPlanSummary(extras: Map<String, String>) {
|
||||
val planName = extras["intl.plan_name"] ?: "Free plan"
|
||||
Text(
|
||||
text = planName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = inkMuted()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TraeSplitLine(stringResource(R.string.trae_intl_premium_fast_title), extras["intl.premium_fast_limit"]?.toDoubleOrNull(), stringResource(R.string.trae_intl_quota_unit))
|
||||
TraeSplitLine(stringResource(R.string.trae_intl_premium_slow_title), extras["intl.premium_slow_limit"]?.toDoubleOrNull(), stringResource(R.string.trae_intl_quota_unit))
|
||||
TraeSplitLine(stringResource(R.string.trae_intl_advanced_title), extras["intl.advanced_limit"]?.toDoubleOrNull(), stringResource(R.string.trae_intl_quota_unit))
|
||||
TraeSplitLine(stringResource(R.string.trae_intl_auto_completion_title), extras["intl.auto_completion_limit"]?.toDoubleOrNull(), stringResource(R.string.trae_intl_quota_unit))
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun OpenCodeGoMainBalance(balance: ServiceBalance) {
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
|
||||
@ -163,6 +163,21 @@ fun CredentialEditScreen(
|
||||
onRegionChange = viewModel::updateTraeRegion,
|
||||
onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId,
|
||||
onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId, uiState.traeRegion) },
|
||||
onRecharge = {
|
||||
val url = if (uiState.traeRegion.uppercase() == "INTL") {
|
||||
"https://www.trae.ai/pricing"
|
||||
} else {
|
||||
"https://www.trae.cn/subscription"
|
||||
}
|
||||
runCatching {
|
||||
context.startActivity(
|
||||
android.content.Intent(
|
||||
android.content.Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse(url)
|
||||
).addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
},
|
||||
onSave = viewModel::saveTraeCredential
|
||||
)
|
||||
} else if (service == ServiceType.WORKBUDDY) {
|
||||
@ -792,6 +807,7 @@ private fun TraeCredentialForm(
|
||||
onRegionChange: (String) -> Unit,
|
||||
onCheckinDeviceIdChange: (String) -> Unit,
|
||||
onStartInAppLogin: () -> Unit,
|
||||
onRecharge: () -> Unit,
|
||||
onSave: () -> Unit
|
||||
) {
|
||||
Text(text = stringResource(R.string.credential_title_trae), style = MaterialTheme.typography.titleMedium)
|
||||
@ -847,6 +863,12 @@ private fun TraeCredentialForm(
|
||||
onSelect = { onRegionChange("INTL") }
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onRecharge,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.action_trae_recharge))
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = checkinDeviceId,
|
||||
onValueChange = onCheckinDeviceIdChange,
|
||||
|
||||
@ -318,14 +318,21 @@ class CredentialEditViewModel @Inject constructor(
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
traeJwt = parsed.first,
|
||||
traeRegion = parsed.second,
|
||||
traeJwt = parsed.jwt,
|
||||
traeRegion = parsed.region,
|
||||
traeApiHost = parsed.apiHost,
|
||||
traeCallbackUrl = input
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTraeCallback(input: String): Pair<String, String>? {
|
||||
private data class TraeParsedCallback(
|
||||
val jwt: String,
|
||||
val region: String,
|
||||
val apiHost: String
|
||||
)
|
||||
|
||||
private fun parseTraeCallback(input: String): TraeParsedCallback? {
|
||||
val raw = input.trim()
|
||||
val uri = runCatching { android.net.Uri.parse(raw) }.getOrNull() ?: return null
|
||||
fun query(name: String): String? {
|
||||
@ -362,7 +369,12 @@ class CredentialEditViewModel @Inject constructor(
|
||||
val intl = userRegion?.let { it != "cn" } == true ||
|
||||
userInfoRegion?.let { !it.equals("cn", true) && !it.equals("china", true) } == true ||
|
||||
query("userRegion")?.equals("us", true) == true
|
||||
return token to (if (intl) "INTL" else "CN")
|
||||
val apiHost = query("host")?.takeIf { it.startsWith("http") }?.trimEnd('/') ?: ""
|
||||
return TraeParsedCallback(
|
||||
jwt = token,
|
||||
region = if (intl) "INTL" else "CN",
|
||||
apiHost = apiHost
|
||||
)
|
||||
}
|
||||
|
||||
fun updateTraeRegion(value: String) {
|
||||
@ -377,10 +389,10 @@ class CredentialEditViewModel @Inject constructor(
|
||||
val type = serviceType ?: return
|
||||
val current = _uiState.value
|
||||
var jwt = current.traeJwt.trim()
|
||||
if (jwt.isBlank() && current.traeCallbackUrl.isNotBlank()) {
|
||||
val parsed = parseTraeCallback(current.traeCallbackUrl)
|
||||
if (parsed != null) jwt = parsed.first
|
||||
}
|
||||
val parsedCallback = if (jwt.isBlank() && current.traeCallbackUrl.isNotBlank()) {
|
||||
parseTraeCallback(current.traeCallbackUrl)
|
||||
} else null
|
||||
if (parsedCallback != null) jwt = parsedCallback.jwt
|
||||
if (jwt.isBlank()) {
|
||||
_uiState.update { it.copy(message = UiText.Resource(R.string.error_api_key_empty)) }
|
||||
return
|
||||
@ -396,6 +408,9 @@ class CredentialEditViewModel @Inject constructor(
|
||||
val effectiveId = credentialRepository.save(
|
||||
(existing ?: Credential.TraeCredential(service = type, jwt = jwt, region = "CN")).copy(
|
||||
jwt = jwt,
|
||||
apiHost = current.traeApiHost.takeIf { it.isNotBlank() }?.trimEnd('/')
|
||||
?: parsedCallback?.apiHost?.takeIf { it.isNotBlank() }?.trimEnd('/')
|
||||
?: existing?.apiHost,
|
||||
region = if (current.traeRegion.uppercase() == "INTL") "INTL" else "CN",
|
||||
// 换账号(JWT 变化)时清空本地签到短路标记,避免新账号被误判「今日已签到」
|
||||
lastCheckinDate = if (existing != null && existing.jwt == jwt) existing.lastCheckinDate else null,
|
||||
@ -1052,6 +1067,8 @@ data class CredentialEditUiState(
|
||||
val traeRegion: String = "CN",
|
||||
/** Trae 签到设备 ID(官方客户端绑定 did;留空用内置默认) */
|
||||
val traeCheckinDeviceId: String = "",
|
||||
/** Trae 登录回调携带的账号 API 主机(备用;从回调链接自动解析) */
|
||||
val traeApiHost: String = "",
|
||||
/** WorkBuddy:OAuth tokens + 区域 */
|
||||
val workBuddyAccess: String = "",
|
||||
val workBuddyRefresh: String = "",
|
||||
|
||||
@ -288,6 +288,7 @@ class WebViewLoginViewModel @Inject constructor(
|
||||
service = ServiceType.TRAE,
|
||||
jwt = token,
|
||||
region = if (region == "INTL") "INTL" else "CN",
|
||||
apiHost = param("host")?.takeIf { it.startsWith("http") }?.trimEnd('/'),
|
||||
deviceId = deviceId,
|
||||
userId = parsedUserId?.takeIf { it.isNotBlank() },
|
||||
lastVerifiedAt = System.currentTimeMillis()
|
||||
|
||||
@ -792,4 +792,10 @@
|
||||
<string name="update_install_permission_tip">需要开启「安装未知应用」权限</string>
|
||||
<string name="update_enable_permission">去开启</string>
|
||||
<string name="update_failed">更新失败:%1$s</string>
|
||||
<string name="trae_intl_premium_fast_title">Premium 快速</string>
|
||||
<string name="trae_intl_premium_slow_title">Premium 慢速</string>
|
||||
<string name="trae_intl_advanced_title">高级额度</string>
|
||||
<string name="trae_intl_auto_completion_title">自动补全</string>
|
||||
<string name="trae_intl_quota_unit">次/月</string>
|
||||
<string name="action_trae_recharge">充值 / 升级</string>
|
||||
</resources>
|
||||
@ -792,4 +792,10 @@
|
||||
<string name="update_install_permission_tip">需要開啟「安裝未知應用」權限</string>
|
||||
<string name="update_enable_permission">前往開啟</string>
|
||||
<string name="update_failed">更新失敗:%1$s</string>
|
||||
<string name="trae_intl_premium_fast_title">Premium 快速</string>
|
||||
<string name="trae_intl_premium_slow_title">Premium 慢速</string>
|
||||
<string name="trae_intl_advanced_title">進階額度</string>
|
||||
<string name="trae_intl_auto_completion_title">自動補全</string>
|
||||
<string name="trae_intl_quota_unit">次/月</string>
|
||||
<string name="action_trae_recharge">儲值 / 升級</string>
|
||||
</resources>
|
||||
@ -797,4 +797,10 @@
|
||||
<string name="update_install_permission_tip">Please allow installing apps from unknown sources</string>
|
||||
<string name="update_enable_permission">Open settings</string>
|
||||
<string name="update_failed">Update failed: %1$s</string>
|
||||
<string name="trae_intl_premium_fast_title">Premium fast</string>
|
||||
<string name="trae_intl_premium_slow_title">Premium slow</string>
|
||||
<string name="trae_intl_advanced_title">Advanced requests</string>
|
||||
<string name="trae_intl_auto_completion_title">Auto completion</string>
|
||||
<string name="trae_intl_quota_unit">/mo</string>
|
||||
<string name="action_trae_recharge">Upgrade / Top up</string>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user