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"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 36
|
versionCode = 37
|
||||||
versionName = "1.7.16"
|
versionName = "1.7.17"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -55,11 +55,7 @@ class TraeChatProxy @Inject constructor(
|
|||||||
val effectiveRegion = forcedRegion ?: runCatching {
|
val effectiveRegion = forcedRegion ?: runCatching {
|
||||||
ProviderRegion.valueOf(credential.region.uppercase())
|
ProviderRegion.valueOf(credential.region.uppercase())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
val base = if (effectiveRegion == ProviderRegion.INTL) {
|
val base = chatBase(effectiveRegion)
|
||||||
"https://a0ai-api-sg.byteintlapi.com"
|
|
||||||
} else {
|
|
||||||
"https://trae-api-cn.mchost.guru"
|
|
||||||
}
|
|
||||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||||
val requestId = UUID.randomUUID().toString()
|
val requestId = UUID.randomUUID().toString()
|
||||||
|
|
||||||
@ -94,11 +90,7 @@ class TraeChatProxy @Inject constructor(
|
|||||||
val effectiveRegion = forcedRegion ?: runCatching {
|
val effectiveRegion = forcedRegion ?: runCatching {
|
||||||
ProviderRegion.valueOf(credential.region.uppercase())
|
ProviderRegion.valueOf(credential.region.uppercase())
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
val base = if (effectiveRegion == ProviderRegion.INTL) {
|
val base = chatBase(effectiveRegion)
|
||||||
"https://a0ai-api-sg.byteintlapi.com"
|
|
||||||
} else {
|
|
||||||
"https://trae-api-cn.mchost.guru"
|
|
||||||
}
|
|
||||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||||
val requestId = UUID.randomUUID().toString()
|
val requestId = UUID.randomUUID().toString()
|
||||||
|
|
||||||
@ -237,6 +229,17 @@ class TraeChatProxy @Inject constructor(
|
|||||||
return MODEL_ALIASES[id.lowercase()] ?: id
|
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 {
|
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential, region: ProviderRegion?): String {
|
||||||
val src = try {
|
val src = try {
|
||||||
json.parseToJsonElement(raw) as? JsonObject
|
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 jwt = credential.jwt.trim().takeIf { it.isNotBlank() } ?: return@withContext false
|
||||||
|
|
||||||
val intl = effectiveRegion == ProviderRegion.INTL
|
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 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" +
|
val url = base + "/api/remote/v1/models" +
|
||||||
"?functions=solo_agent_remote%2Csolo_work_remote%2Csolo_design_remote" +
|
"?functions=solo_agent_remote%2Csolo_work_remote%2Csolo_design_remote" +
|
||||||
"&show_custom_model=true"
|
"&show_custom_model=true"
|
||||||
@ -88,7 +91,7 @@ class TraeModelProvider @Inject constructor(
|
|||||||
"Content-Type" to "application/json",
|
"Content-Type" to "application/json",
|
||||||
"X-Trae-Client-Type" to "web",
|
"X-Trae-Client-Type" to "web",
|
||||||
"X-Preferenced-Language" to "zh-CN",
|
"X-Preferenced-Language" to "zh-CN",
|
||||||
"x-user-region" to if (intl) "SG" else "CN",
|
"x-user-region" to userRegion,
|
||||||
"Origin" to origin,
|
"Origin" to origin,
|
||||||
"Referer" to origin + "/",
|
"Referer" to origin + "/",
|
||||||
"User-Agent" to (
|
"User-Agent" to (
|
||||||
|
|||||||
@ -66,9 +66,12 @@ class TraeRepository constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val regionKey = credential.region.uppercase().takeIf { it == "INTL" } ?: "CN"
|
val regionKey = credential.region.uppercase().takeIf { it == "INTL" } ?: "CN"
|
||||||
val url = when (regionKey) {
|
val url = if (regionKey == "INTL") {
|
||||||
"INTL" -> "https://grow-normal.trae.ai/trae/api/v1/pay/ide_user_ent_usage"
|
val host = credential.apiHost?.takeIf { it.startsWith("http") }?.trimEnd('/')
|
||||||
else -> "https://api.trae.cn/trae/api/v2/pay/ide_user_ent_usage"
|
?: "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()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
@ -124,6 +127,8 @@ class TraeRepository constructor(
|
|||||||
|
|
||||||
root["is_credits_billing"]?.jsonPrimitive?.contentOrNull?.let { extras["is_credits_billing"] = it }
|
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 }
|
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 }
|
root["is_pay_freshman"]?.jsonPrimitive?.contentOrNull?.let { extras["is_new_user"] = it }
|
||||||
|
|
||||||
// 1) 权威汇总
|
// 1) 权威汇总
|
||||||
@ -156,6 +161,26 @@ class TraeRepository constructor(
|
|||||||
?: base?.get("product_extra")?.jsonObject
|
?: base?.get("product_extra")?.jsonObject
|
||||||
?.get("package_extra")?.jsonObject?.getString("package_name")
|
?.get("package_extra")?.jsonObject?.getString("package_name")
|
||||||
?: "pack[$index]"
|
?: "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 limit = quota?.getNumber("credits_limit")?.toLong()
|
||||||
val used = pack.getNumber("usage", "credits_amount")
|
val used = pack.getNumber("usage", "credits_amount")
|
||||||
?: base?.get("usage")?.jsonObject?.get("credits_amount")?.safeDouble()
|
?: base?.get("usage")?.jsonObject?.get("credits_amount")?.safeDouble()
|
||||||
|
|||||||
@ -79,8 +79,10 @@ sealed class Credential {
|
|||||||
override val service: ServiceType,
|
override val service: ServiceType,
|
||||||
/** `Authorization: Cloud-IDE-JWT <jwt>` 的 JWT 值 */
|
/** `Authorization: Cloud-IDE-JWT <jwt>` 的 JWT 值 */
|
||||||
val jwt: String,
|
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",
|
val region: String = "CN",
|
||||||
|
/** 登录回调携带的账号 API 主机(如 https://api-us-east.trae.ai);null=旧凭据用区域默认 */
|
||||||
|
val apiHost: String? = null,
|
||||||
/** 登录会话绑定的设备 ID(App 持久化每机唯一),null=旧凭据未保存 */
|
/** 登录会话绑定的设备 ID(App 持久化每机唯一),null=旧凭据未保存 */
|
||||||
val deviceId: String? = null,
|
val deviceId: String? = null,
|
||||||
/** OAuth 用户 ID(回调 userInfo.userId),null=旧凭据未保存 */
|
/** OAuth 用户 ID(回调 userInfo.userId),null=旧凭据未保存 */
|
||||||
|
|||||||
@ -146,6 +146,10 @@ internal fun BalanceMainArea(card: DashboardCardUi) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TraeSplitMainBalance(balance: ServiceBalance) {
|
private fun TraeSplitMainBalance(balance: ServiceBalance) {
|
||||||
val extras = balance.extras
|
val extras = balance.extras
|
||||||
|
if (extras?.get("billing_mode") == "dollar") {
|
||||||
|
TraeDollarPlanSummary(extras)
|
||||||
|
return
|
||||||
|
}
|
||||||
val general = extras?.get("general_credits")?.toDoubleOrNull()
|
val general = extras?.get("general_credits")?.toDoubleOrNull()
|
||||||
val work = extras?.get("work_credits")?.toDoubleOrNull()
|
val work = extras?.get("work_credits")?.toDoubleOrNull()
|
||||||
if (general == null && work == null) {
|
if (general == null && work == null) {
|
||||||
@ -176,7 +180,7 @@ private fun TraeSplitMainBalance(balance: ServiceBalance) {
|
|||||||
|
|
||||||
/** 首页 Trae 分区一行:左侧分区名,右侧小计 + 单位。 */
|
/** 首页 Trae 分区一行:左侧分区名,右侧小计 + 单位。 */
|
||||||
@Composable
|
@Composable
|
||||||
private fun TraeSplitLine(label: String, value: Double?) {
|
private fun TraeSplitLine(label: String, value: Double?, unit: String = "Credits") {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
@ -196,7 +200,7 @@ private fun TraeSplitLine(label: String, value: Double?) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Credits",
|
text = unit,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = inkMuted(),
|
color = inkMuted(),
|
||||||
modifier = Modifier.padding(bottom = 2.dp)
|
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
|
@Composable
|
||||||
internal fun OpenCodeGoMainBalance(balance: ServiceBalance) {
|
internal fun OpenCodeGoMainBalance(balance: ServiceBalance) {
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
|
|||||||
@ -163,6 +163,21 @@ fun CredentialEditScreen(
|
|||||||
onRegionChange = viewModel::updateTraeRegion,
|
onRegionChange = viewModel::updateTraeRegion,
|
||||||
onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId,
|
onCheckinDeviceIdChange = viewModel::updateTraeCheckinDeviceId,
|
||||||
onStartInAppLogin = { onStartWebViewLogin(ServiceType.TRAE, accountId, uiState.traeRegion) },
|
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
|
onSave = viewModel::saveTraeCredential
|
||||||
)
|
)
|
||||||
} else if (service == ServiceType.WORKBUDDY) {
|
} else if (service == ServiceType.WORKBUDDY) {
|
||||||
@ -792,6 +807,7 @@ private fun TraeCredentialForm(
|
|||||||
onRegionChange: (String) -> Unit,
|
onRegionChange: (String) -> Unit,
|
||||||
onCheckinDeviceIdChange: (String) -> Unit,
|
onCheckinDeviceIdChange: (String) -> Unit,
|
||||||
onStartInAppLogin: () -> Unit,
|
onStartInAppLogin: () -> Unit,
|
||||||
|
onRecharge: () -> Unit,
|
||||||
onSave: () -> Unit
|
onSave: () -> Unit
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.credential_title_trae), style = MaterialTheme.typography.titleMedium)
|
Text(text = stringResource(R.string.credential_title_trae), style = MaterialTheme.typography.titleMedium)
|
||||||
@ -847,6 +863,12 @@ private fun TraeCredentialForm(
|
|||||||
onSelect = { onRegionChange("INTL") }
|
onSelect = { onRegionChange("INTL") }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onRecharge,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_trae_recharge))
|
||||||
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = checkinDeviceId,
|
value = checkinDeviceId,
|
||||||
onValueChange = onCheckinDeviceIdChange,
|
onValueChange = onCheckinDeviceIdChange,
|
||||||
|
|||||||
@ -318,14 +318,21 @@ class CredentialEditViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
traeJwt = parsed.first,
|
traeJwt = parsed.jwt,
|
||||||
traeRegion = parsed.second,
|
traeRegion = parsed.region,
|
||||||
|
traeApiHost = parsed.apiHost,
|
||||||
traeCallbackUrl = input
|
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 raw = input.trim()
|
||||||
val uri = runCatching { android.net.Uri.parse(raw) }.getOrNull() ?: return null
|
val uri = runCatching { android.net.Uri.parse(raw) }.getOrNull() ?: return null
|
||||||
fun query(name: String): String? {
|
fun query(name: String): String? {
|
||||||
@ -362,7 +369,12 @@ class CredentialEditViewModel @Inject constructor(
|
|||||||
val intl = userRegion?.let { it != "cn" } == true ||
|
val intl = userRegion?.let { it != "cn" } == true ||
|
||||||
userInfoRegion?.let { !it.equals("cn", true) && !it.equals("china", true) } == true ||
|
userInfoRegion?.let { !it.equals("cn", true) && !it.equals("china", true) } == true ||
|
||||||
query("userRegion")?.equals("us", 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) {
|
fun updateTraeRegion(value: String) {
|
||||||
@ -377,10 +389,10 @@ class CredentialEditViewModel @Inject constructor(
|
|||||||
val type = serviceType ?: return
|
val type = serviceType ?: return
|
||||||
val current = _uiState.value
|
val current = _uiState.value
|
||||||
var jwt = current.traeJwt.trim()
|
var jwt = current.traeJwt.trim()
|
||||||
if (jwt.isBlank() && current.traeCallbackUrl.isNotBlank()) {
|
val parsedCallback = if (jwt.isBlank() && current.traeCallbackUrl.isNotBlank()) {
|
||||||
val parsed = parseTraeCallback(current.traeCallbackUrl)
|
parseTraeCallback(current.traeCallbackUrl)
|
||||||
if (parsed != null) jwt = parsed.first
|
} else null
|
||||||
}
|
if (parsedCallback != null) jwt = parsedCallback.jwt
|
||||||
if (jwt.isBlank()) {
|
if (jwt.isBlank()) {
|
||||||
_uiState.update { it.copy(message = UiText.Resource(R.string.error_api_key_empty)) }
|
_uiState.update { it.copy(message = UiText.Resource(R.string.error_api_key_empty)) }
|
||||||
return
|
return
|
||||||
@ -396,6 +408,9 @@ class CredentialEditViewModel @Inject constructor(
|
|||||||
val effectiveId = credentialRepository.save(
|
val effectiveId = credentialRepository.save(
|
||||||
(existing ?: Credential.TraeCredential(service = type, jwt = jwt, region = "CN")).copy(
|
(existing ?: Credential.TraeCredential(service = type, jwt = jwt, region = "CN")).copy(
|
||||||
jwt = jwt,
|
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",
|
region = if (current.traeRegion.uppercase() == "INTL") "INTL" else "CN",
|
||||||
// 换账号(JWT 变化)时清空本地签到短路标记,避免新账号被误判「今日已签到」
|
// 换账号(JWT 变化)时清空本地签到短路标记,避免新账号被误判「今日已签到」
|
||||||
lastCheckinDate = if (existing != null && existing.jwt == jwt) existing.lastCheckinDate else null,
|
lastCheckinDate = if (existing != null && existing.jwt == jwt) existing.lastCheckinDate else null,
|
||||||
@ -1052,6 +1067,8 @@ data class CredentialEditUiState(
|
|||||||
val traeRegion: String = "CN",
|
val traeRegion: String = "CN",
|
||||||
/** Trae 签到设备 ID(官方客户端绑定 did;留空用内置默认) */
|
/** Trae 签到设备 ID(官方客户端绑定 did;留空用内置默认) */
|
||||||
val traeCheckinDeviceId: String = "",
|
val traeCheckinDeviceId: String = "",
|
||||||
|
/** Trae 登录回调携带的账号 API 主机(备用;从回调链接自动解析) */
|
||||||
|
val traeApiHost: String = "",
|
||||||
/** WorkBuddy:OAuth tokens + 区域 */
|
/** WorkBuddy:OAuth tokens + 区域 */
|
||||||
val workBuddyAccess: String = "",
|
val workBuddyAccess: String = "",
|
||||||
val workBuddyRefresh: String = "",
|
val workBuddyRefresh: String = "",
|
||||||
|
|||||||
@ -288,6 +288,7 @@ class WebViewLoginViewModel @Inject constructor(
|
|||||||
service = ServiceType.TRAE,
|
service = ServiceType.TRAE,
|
||||||
jwt = token,
|
jwt = token,
|
||||||
region = if (region == "INTL") "INTL" else "CN",
|
region = if (region == "INTL") "INTL" else "CN",
|
||||||
|
apiHost = param("host")?.takeIf { it.startsWith("http") }?.trimEnd('/'),
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
userId = parsedUserId?.takeIf { it.isNotBlank() },
|
userId = parsedUserId?.takeIf { it.isNotBlank() },
|
||||||
lastVerifiedAt = System.currentTimeMillis()
|
lastVerifiedAt = System.currentTimeMillis()
|
||||||
|
|||||||
@ -792,4 +792,10 @@
|
|||||||
<string name="update_install_permission_tip">需要开启「安装未知应用」权限</string>
|
<string name="update_install_permission_tip">需要开启「安装未知应用」权限</string>
|
||||||
<string name="update_enable_permission">去开启</string>
|
<string name="update_enable_permission">去开启</string>
|
||||||
<string name="update_failed">更新失败:%1$s</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>
|
</resources>
|
||||||
@ -792,4 +792,10 @@
|
|||||||
<string name="update_install_permission_tip">需要開啟「安裝未知應用」權限</string>
|
<string name="update_install_permission_tip">需要開啟「安裝未知應用」權限</string>
|
||||||
<string name="update_enable_permission">前往開啟</string>
|
<string name="update_enable_permission">前往開啟</string>
|
||||||
<string name="update_failed">更新失敗:%1$s</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>
|
</resources>
|
||||||
@ -797,4 +797,10 @@
|
|||||||
<string name="update_install_permission_tip">Please allow installing apps from unknown sources</string>
|
<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_enable_permission">Open settings</string>
|
||||||
<string name="update_failed">Update failed: %1$s</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>
|
</resources>
|
||||||
Loading…
Reference in New Issue
Block a user