- 新增“代理”下拉:Trae CN/INTL、WorkBuddy CN/INTL - WorkBuddyModelProvider 在线拉取 /v2/enterprises/personal/models - Trae/WorkBuddy 各区域独立记住所选测试模型 - 真实代理转发仍按前缀自动路由,不受测试选择影响 - 版本升至 1.7.6
This commit is contained in:
parent
0283ce6bdc
commit
f13b38c02b
@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.rainy.token"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 25
|
||||
versionName = "1.7.5"
|
||||
versionCode = 26
|
||||
versionName = "1.7.6"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -60,7 +60,7 @@ class TraeChatProxy @Inject constructor(
|
||||
} else {
|
||||
"https://trae-api-cn.mchost.guru"
|
||||
}
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential)
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
|
||||
val builder = Request.Builder()
|
||||
@ -99,7 +99,7 @@ class TraeChatProxy @Inject constructor(
|
||||
} else {
|
||||
"https://trae-api-cn.mchost.guru"
|
||||
}
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential)
|
||||
val upstreamBody = buildUpstreamBody(requestBody, credential, effectiveRegion)
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
|
||||
val builder = Request.Builder()
|
||||
@ -230,13 +230,14 @@ class TraeChatProxy @Inject constructor(
|
||||
}
|
||||
|
||||
/** 外部模型名 -> Trae 内部模型名;空/auto 用设置页选择的默认模型。 */
|
||||
private fun normalizeModel(raw: String?): String {
|
||||
val id = raw?.trim()?.takeIf { it.isNotBlank() } ?: return modelProvider.selectedModel
|
||||
if (id.equals("auto", ignoreCase = true)) return modelProvider.selectedModel
|
||||
private fun normalizeModel(raw: String?, region: ProviderRegion?): String {
|
||||
val selected = modelProvider.selectedFor(region ?: ProviderRegion.CN)
|
||||
val id = raw?.trim()?.takeIf { it.isNotBlank() } ?: return selected
|
||||
if (id.equals("auto", ignoreCase = true)) return selected
|
||||
return MODEL_ALIASES[id.lowercase()] ?: id
|
||||
}
|
||||
|
||||
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential): String {
|
||||
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential, region: ProviderRegion?): String {
|
||||
val src = try {
|
||||
json.parseToJsonElement(raw) as? JsonObject
|
||||
} catch (_: Throwable) {
|
||||
@ -244,7 +245,7 @@ class TraeChatProxy @Inject constructor(
|
||||
} ?: return raw
|
||||
|
||||
val rawModel = (src["model"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() }
|
||||
val model = normalizeModel(rawModel)
|
||||
val model = normalizeModel(rawModel, region)
|
||||
val messages = convertNativeMessages(src["messages"])
|
||||
val sessionId = UUID.randomUUID().toString().replace("-", "")
|
||||
|
||||
|
||||
@ -41,16 +41,20 @@ class TraeModelProvider @Inject constructor(
|
||||
private val _models = MutableStateFlow(defaultModels())
|
||||
val models: StateFlow<List<TraeModel>> = _models.asStateFlow()
|
||||
|
||||
/** 当前默认模型(用户在设置页选择的 Trae 模型名)。 */
|
||||
var selectedModel: String = prefs.getString(KEY_MODEL, "glm-5.2") ?: "glm-5.2"
|
||||
private set
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun setSelectedModel(id: String) {
|
||||
/** 当前区域选择的默认模型(未设置时 CN 用 GLM-5.2)。 */
|
||||
fun selectedFor(region: ProviderRegion): String {
|
||||
if (region == ProviderRegion.INTL) {
|
||||
return prefs.getString(KEY_MODEL_INTL, DEFAULT_MODEL_INTL) ?: DEFAULT_MODEL_INTL
|
||||
}
|
||||
return prefs.getString(KEY_MODEL_CN, "glm-5.2") ?: "glm-5.2"
|
||||
}
|
||||
|
||||
fun setSelectedFor(region: ProviderRegion, id: String) {
|
||||
if (id.isBlank()) return
|
||||
selectedModel = id
|
||||
prefs.edit().putString(KEY_MODEL, id).apply()
|
||||
val key = if (region == ProviderRegion.INTL) KEY_MODEL_INTL else KEY_MODEL_CN
|
||||
prefs.edit().putString(key, id).apply()
|
||||
}
|
||||
|
||||
/** 拉取账号在线模型列表;失败时保留内置列表并返回 false。 */
|
||||
@ -120,7 +124,9 @@ class TraeModelProvider @Inject constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_MODEL = "trae_default_model"
|
||||
private const val KEY_MODEL_CN = "trae_default_model_cn"
|
||||
private const val KEY_MODEL_INTL = "trae_default_model_intl"
|
||||
private const val DEFAULT_MODEL_INTL = "glm-5.2"
|
||||
|
||||
/** 内置兜底模型(参考 trae2api-cn 的模型映射 /v1/models 列表)。 */
|
||||
fun defaultModels(): List<TraeModel> = listOf(
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
package com.rainy.token.data.proxy
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.rainy.token.data.repository.CredentialRepository
|
||||
import com.rainy.token.domain.model.Credential
|
||||
import com.rainy.token.domain.service.ServiceType
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
/**
|
||||
* WorkBuddy / CodeBuddy 模型列表:
|
||||
* 在线接口 GET {base}/v2/enterprises/personal/models + 内置兜底 + 按区域记住选择。
|
||||
*/
|
||||
@Singleton
|
||||
class WorkBuddyModelProvider @Inject constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val credentialRepository: CredentialRepository,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences("rainytoken_proxy", Context.MODE_PRIVATE)
|
||||
|
||||
private val _models = MutableStateFlow(defaultModels())
|
||||
val models: StateFlow<List<TraeModelProvider.TraeModel>> = _models.asStateFlow()
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun selectedFor(region: ProviderRegion): String {
|
||||
val key = if (region == ProviderRegion.INTL) KEY_MODEL_INTL else KEY_MODEL_CN
|
||||
return prefs.getString(key, DEFAULT_MODEL) ?: DEFAULT_MODEL
|
||||
}
|
||||
|
||||
fun setSelectedFor(region: ProviderRegion, id: String) {
|
||||
if (id.isBlank()) return
|
||||
val key = if (region == ProviderRegion.INTL) KEY_MODEL_INTL else KEY_MODEL_CN
|
||||
prefs.edit().putString(key, id).apply()
|
||||
}
|
||||
|
||||
suspend fun refreshFor(region: ProviderRegion): Boolean = withContext(Dispatchers.IO) {
|
||||
val credential = credentialRepository.get(ServiceType.WORKBUDDY, null)
|
||||
?: return@withContext false
|
||||
if (credential !is Credential.WorkBuddyCredential) return@withContext false
|
||||
val accessToken = credential.accessToken.trim().takeIf { it.isNotBlank() }
|
||||
?: return@withContext false
|
||||
val intl = region == ProviderRegion.INTL
|
||||
val base = if (intl) "https://www.workbuddy.ai" else "https://copilot.tencent.com"
|
||||
val url = base + "/v2/enterprises/personal/models"
|
||||
|
||||
val headers = linkedMapOf(
|
||||
"Authorization" to "Bearer $accessToken",
|
||||
"Content-Type" to "application/json",
|
||||
"Accept" to "application/json",
|
||||
"X-Client-ID" to "codebuddy-cli",
|
||||
"X-Client-Version" to "2.143.1",
|
||||
"X-Product" to "SaaS",
|
||||
"User-Agent" to "CLI/2.143.1 CodeBuddy/2.143.1",
|
||||
"Origin" to (if (intl) "https://www.workbuddy.ai" else "https://www.codebuddy.cn"),
|
||||
)
|
||||
val request = Request.Builder().url(url).headers(headers.toHeaders()).get().build()
|
||||
val parsed = try {
|
||||
okHttpClient.newCall(request).execute().use { resp ->
|
||||
if (!resp.isSuccessful) return@use null
|
||||
val body = resp.body?.string() ?: return@use null
|
||||
parseModelList(body)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (parsed.isNullOrEmpty()) return@withContext false
|
||||
_models.value = parsed
|
||||
true
|
||||
}
|
||||
|
||||
private fun parseModelList(body: String): List<TraeModelProvider.TraeModel>? {
|
||||
val root = runCatching { json.parseToJsonElement(body) as? JsonObject }.getOrNull()
|
||||
?: return null
|
||||
val data = root["data"] as? JsonObject ?: return null
|
||||
val models = data["models"] as? JsonArray ?: return null
|
||||
val seen = LinkedHashMap<String, TraeModelProvider.TraeModel>()
|
||||
for (rawEl in models) {
|
||||
val raw = rawEl as? JsonObject ?: continue
|
||||
val id = (raw["id"] as? JsonPrimitive)?.contentOrNull?.trim()
|
||||
?.takeIf { it.isNotEmpty() } ?: continue
|
||||
val label = (raw["name"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() } ?: id
|
||||
seen[id] = TraeModelProvider.TraeModel(id, label)
|
||||
}
|
||||
return seen.values.toList().ifEmpty { null }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_MODEL_CN = "wb_default_model_cn"
|
||||
private const val KEY_MODEL_INTL = "wb_default_model_intl"
|
||||
private const val DEFAULT_MODEL = "hy4-preview"
|
||||
|
||||
/** 内置兜底(参考 workbuddy-gateway 模型目录,在线列表成功后会被替换)。 */
|
||||
fun defaultModels(): List<TraeModelProvider.TraeModel> = listOf(
|
||||
TraeModelProvider.TraeModel("hy4-preview", "hy4-preview"),
|
||||
TraeModelProvider.TraeModel("hy3", "hy3"),
|
||||
TraeModelProvider.TraeModel("hy3-preview", "hy3-preview"),
|
||||
TraeModelProvider.TraeModel("deepseek-v4.1-flash", "deepseek-v4.1-flash"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -109,6 +109,7 @@ fun SettingsScreen(
|
||||
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
||||
val proxyDebugRaw by viewModel.proxyDebugRaw.collectAsStateWithLifecycle()
|
||||
val proxyModels by viewModel.proxyModels.collectAsStateWithLifecycle()
|
||||
val selectedProxy by viewModel.selectedProxy.collectAsStateWithLifecycle()
|
||||
val proxyModel by viewModel.proxyModel.collectAsStateWithLifecycle()
|
||||
var showLanguageDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@ -187,6 +188,9 @@ fun SettingsScreen(
|
||||
error = proxyError,
|
||||
debug = proxyDebug,
|
||||
debugRaw = proxyDebugRaw,
|
||||
proxyTargets = viewModel.proxyTargets,
|
||||
selectedProxy = selectedProxy,
|
||||
onSelectProxy = viewModel::selectProxy,
|
||||
models = proxyModels,
|
||||
selectedModel = proxyModel,
|
||||
onSelectModel = viewModel::selectProxyModel,
|
||||
@ -225,6 +229,9 @@ private fun ProxyGatewayCard(
|
||||
error: String?,
|
||||
debug: String?,
|
||||
debugRaw: String?,
|
||||
proxyTargets: List<ProxyTarget>,
|
||||
selectedProxy: ProxyTarget,
|
||||
onSelectProxy: (ProxyTarget) -> Unit,
|
||||
models: List<TraeModelProvider.TraeModel>,
|
||||
selectedModel: String,
|
||||
onSelectModel: (String) -> Unit,
|
||||
@ -234,6 +241,7 @@ private fun ProxyGatewayCard(
|
||||
onTestWorkBuddy: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var proxyMenuExpanded by remember { mutableStateOf(false) }
|
||||
var modelMenuExpanded by remember { mutableStateOf(false) }
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -284,6 +292,36 @@ private fun ProxyGatewayCard(
|
||||
Text("测试 WorkBuddy 流")
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "代理",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = InkMuted
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box {
|
||||
TextButton(onClick = { proxyMenuExpanded = true }) {
|
||||
Text(selectedProxy.label + " ▾")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = proxyMenuExpanded,
|
||||
onDismissRequest = { proxyMenuExpanded = false }
|
||||
) {
|
||||
proxyTargets.forEach { t ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(t.label) },
|
||||
onClick = {
|
||||
onSelectProxy(t)
|
||||
proxyMenuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@ -8,6 +8,7 @@ import com.rainy.token.data.proxy.ProxyUpstreamStream
|
||||
import com.rainy.token.data.proxy.ProxyServerConfig
|
||||
import com.rainy.token.data.proxy.TraeChatProxy
|
||||
import com.rainy.token.data.proxy.TraeModelProvider
|
||||
import com.rainy.token.data.proxy.WorkBuddyModelProvider
|
||||
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
||||
import com.rainy.token.data.repository.CredentialRepository
|
||||
import com.rainy.token.domain.model.CredentialStatus
|
||||
@ -41,6 +42,7 @@ class SettingsViewModel @Inject constructor(
|
||||
private val traeChatProxy: TraeChatProxy,
|
||||
private val workBuddyChatProxy: WorkBuddyChatProxy,
|
||||
private val traeModelProvider: TraeModelProvider,
|
||||
private val workBuddyModelProvider: WorkBuddyModelProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
@ -76,23 +78,56 @@ class SettingsViewModel @Inject constructor(
|
||||
private val _proxyDebugRaw = MutableStateFlow<String?>(null)
|
||||
val proxyDebugRaw: StateFlow<String?> = _proxyDebugRaw.asStateFlow()
|
||||
|
||||
/** Trae 可用模型列表(内置兜底 + 在线拉取账号模型)。 */
|
||||
val proxyModels: StateFlow<List<TraeModelProvider.TraeModel>> = traeModelProvider.models
|
||||
/** 代理目标(用于测试和模型选择;真实转发仍由 ProviderRouter 自动路由)。 */
|
||||
val proxyTargets: List<ProxyTarget> = ProxyTarget.entries
|
||||
|
||||
/** 当前选择的 Trae 默认模型。 */
|
||||
private val _proxyModel = MutableStateFlow(traeModelProvider.selectedModel)
|
||||
private val _selectedProxy = MutableStateFlow(ProxyTarget.TRAE_CN)
|
||||
val selectedProxy: StateFlow<ProxyTarget> = _selectedProxy.asStateFlow()
|
||||
|
||||
private val _proxyModels = MutableStateFlow<List<TraeModelProvider.TraeModel>>(traeModelProvider.models.value)
|
||||
val proxyModels: StateFlow<List<TraeModelProvider.TraeModel>> = _proxyModels.asStateFlow()
|
||||
|
||||
private val _proxyModel = MutableStateFlow(traeModelProvider.selectedFor(ProviderRegion.CN))
|
||||
val proxyModel: StateFlow<String> = _proxyModel.asStateFlow()
|
||||
|
||||
/** 切换 Trae 默认模型并持久化。 */
|
||||
/** 切换当前测试目标代理。 */
|
||||
fun selectProxy(target: ProxyTarget) {
|
||||
_selectedProxy.value = target
|
||||
val region = target.region()
|
||||
_proxyModels.value = when (target) {
|
||||
ProxyTarget.TRAE_CN, ProxyTarget.TRAE_INTL -> traeModelProvider.models.value
|
||||
else -> workBuddyModelProvider.models.value
|
||||
}
|
||||
_proxyModel.value = when (target) {
|
||||
ProxyTarget.TRAE_CN, ProxyTarget.TRAE_INTL -> traeModelProvider.selectedFor(region)
|
||||
else -> workBuddyModelProvider.selectedFor(region)
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换当前代理的测试模型并持久化。 */
|
||||
fun selectProxyModel(id: String) {
|
||||
traeModelProvider.setSelectedModel(id)
|
||||
val target = _selectedProxy.value
|
||||
val region = target.region()
|
||||
when (target) {
|
||||
ProxyTarget.TRAE_CN, ProxyTarget.TRAE_INTL -> traeModelProvider.setSelectedFor(region, id)
|
||||
else -> workBuddyModelProvider.setSelectedFor(region, id)
|
||||
}
|
||||
_proxyModel.value = id
|
||||
}
|
||||
|
||||
/** 拉取当前账号可用的 Trae 模型列表。 */
|
||||
/** 拉取当前代理可用模型;失败保留内置列表。 */
|
||||
fun refreshProxyModels() {
|
||||
val target = _selectedProxy.value
|
||||
val region = target.region()
|
||||
viewModelScope.launch {
|
||||
val ok = traeModelProvider.refreshFor(ProviderRegion.CN)
|
||||
val ok = when (target) {
|
||||
ProxyTarget.TRAE_CN, ProxyTarget.TRAE_INTL -> traeModelProvider.refreshFor(region)
|
||||
else -> workBuddyModelProvider.refreshFor(region)
|
||||
}
|
||||
_proxyModels.value = when (target) {
|
||||
ProxyTarget.TRAE_CN, ProxyTarget.TRAE_INTL -> traeModelProvider.models.value
|
||||
else -> workBuddyModelProvider.models.value
|
||||
}
|
||||
if (!ok) _proxyDebug.value = "模型列表更新失败(当前显示内置列表)"
|
||||
}
|
||||
}
|
||||
@ -102,7 +137,7 @@ class SettingsViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
_proxyDebug.value = "正在请求 Trae CN 流..."
|
||||
_proxyDebugRaw.value = null
|
||||
val model = traeModelProvider.selectedModel
|
||||
val model = traeModelProvider.selectedFor(ProviderRegion.CN)
|
||||
val body = """{"model":"$model","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}"""
|
||||
try {
|
||||
val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||
@ -132,7 +167,8 @@ class SettingsViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
_proxyDebug.value = "正在请求 WorkBuddy CN 流..."
|
||||
_proxyDebugRaw.value = null
|
||||
val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}"""
|
||||
val model = workBuddyModelProvider.selectedFor(ProviderRegion.CN)
|
||||
val body = """{"model":"$model","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}"""
|
||||
try {
|
||||
val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||
if (stream == null) {
|
||||
@ -313,6 +349,18 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
enum class ProxyTarget(val key: String, val label: String) {
|
||||
TRAE_CN("trae_cn", "Trae CN"),
|
||||
TRAE_INTL("trae_intl", "Trae INTL"),
|
||||
WORKBUDDY_CN("wb_cn", "WorkBuddy CN"),
|
||||
WORKBUDDY_INTL("wb_intl", "WorkBuddy INTL");
|
||||
|
||||
fun region(): ProviderRegion = when (this) {
|
||||
TRAE_CN, WORKBUDDY_CN -> ProviderRegion.CN
|
||||
else -> ProviderRegion.INTL
|
||||
}
|
||||
}
|
||||
|
||||
data class StreamProbeResult(
|
||||
val summary: String,
|
||||
val raw: String,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user