- 设置页新增模型下拉选择,内置参考映射兜底 - TraeModelProvider 在线拉取账号可用模型(/api/remote/v1/models) - 选中模型持久化,作为反代缺省模型 - 常用 OpenAI/Claude 模型名自动映射到 Trae 内部模型 - 修复探针测试请求体混入反斜杠导致 JSON 无效的问题 - 版本升至 1.7.5
This commit is contained in:
parent
8a9d9432fe
commit
0241840f9c
@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.rainy.token"
|
applicationId = "com.rainy.token"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 24
|
versionCode = 25
|
||||||
versionName = "1.7.4"
|
versionName = "1.7.5"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
class TraeChatProxy @Inject constructor(
|
class TraeChatProxy @Inject constructor(
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val credentialRepository: CredentialRepository,
|
private val credentialRepository: CredentialRepository,
|
||||||
|
private val modelProvider: TraeModelProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
@ -228,6 +229,13 @@ class TraeChatProxy @Inject constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 外部模型名 -> 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
|
||||||
|
return MODEL_ALIASES[id.lowercase()] ?: id
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential): String {
|
private fun buildUpstreamBody(raw: String, credential: Credential.TraeCredential): String {
|
||||||
val src = try {
|
val src = try {
|
||||||
json.parseToJsonElement(raw) as? JsonObject
|
json.parseToJsonElement(raw) as? JsonObject
|
||||||
@ -235,8 +243,8 @@ class TraeChatProxy @Inject constructor(
|
|||||||
null
|
null
|
||||||
} ?: return raw
|
} ?: return raw
|
||||||
|
|
||||||
val model = (src["model"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() }
|
val rawModel = (src["model"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() }
|
||||||
?: "glm-5.2"
|
val model = normalizeModel(rawModel)
|
||||||
val messages = convertNativeMessages(src["messages"])
|
val messages = convertNativeMessages(src["messages"])
|
||||||
val sessionId = UUID.randomUUID().toString().replace("-", "")
|
val sessionId = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
|
||||||
@ -255,4 +263,50 @@ class TraeChatProxy @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.toString()
|
}.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** OpenAI/Claude 常用名 -> Trae CN 内部模型名(参考 trae2api-cn)。 */
|
||||||
|
private val MODEL_ALIASES = mapOf(
|
||||||
|
"auto" to "glm-5.2",
|
||||||
|
"gpt-4o" to "DeepSeek-V4-Pro",
|
||||||
|
"gpt-4o-latest" to "DeepSeek-V4-Pro",
|
||||||
|
"gpt-4.1" to "DeepSeek-V4-Pro",
|
||||||
|
"deepseek-v3" to "DeepSeek-V4-Pro",
|
||||||
|
"deepseek-r1" to "DeepSeek-V4-Pro",
|
||||||
|
"gpt-4o-mini" to "DeepSeek-V4-Flash",
|
||||||
|
"claude-opus-4-7" to "glm-5.2",
|
||||||
|
"claude-opus-4-6" to "glm-5.2",
|
||||||
|
"claude-opus-4-5" to "glm-5.2",
|
||||||
|
"claude-sonnet-4-6" to "glm-5.2",
|
||||||
|
"claude-sonnet-4-5" to "glm-5.2",
|
||||||
|
"claude-sonnet-4" to "glm-5.2",
|
||||||
|
"claude-3.7-sonnet" to "glm-5.2",
|
||||||
|
"claude-3-7-sonnet" to "glm-5.2",
|
||||||
|
"claude-3.5-sonnet" to "glm-5.2",
|
||||||
|
"claude-haiku-4-5" to "glm-5.1",
|
||||||
|
"deepseek-v4-flash-official" to "DeepSeek-V4-Flash-Official",
|
||||||
|
"deepseek-v4-pro-official" to "DeepSeek-V4-Pro-Official",
|
||||||
|
"doubao-seed-evolving" to "Doubao-Seed-Evolving",
|
||||||
|
"deepseek-v4-pro" to "DeepSeek-V4-Pro",
|
||||||
|
"deepseek-v4-flash" to "DeepSeek-V4-Flash",
|
||||||
|
"seed-2.1-pro" to "Doubao-Seed-2.1-Pro",
|
||||||
|
"seed-2.1-turbo" to "Doubao-Seed-2.1-Turbo",
|
||||||
|
"seed-code" to "Doubao-Seed-Code",
|
||||||
|
"seed-evolving" to "Doubao-Seed-Evolving",
|
||||||
|
"qwen3.7-plus" to "qwen-3.7-plus",
|
||||||
|
"kimi-k2.6" to "kimi-k2.6",
|
||||||
|
"kimi-k3" to "kimi-k3",
|
||||||
|
"kimi-k2.7-code" to "kimi-k2.7-code",
|
||||||
|
"kimi-k2" to "kimi-k2",
|
||||||
|
"kimi-k2.5" to "kimi-k2.5",
|
||||||
|
"minimax-m3" to "minimax-m3",
|
||||||
|
"minimax-m2.7" to "minimax-m2.7",
|
||||||
|
"minimax-m2.1" to "minimax-m2.1",
|
||||||
|
"mimo-v2.5" to "mimo-v2.5",
|
||||||
|
"mimo-v2.5-pro" to "mimo-v2.5-pro",
|
||||||
|
"minimax-m25" to "minimax-m25",
|
||||||
|
"qwen36-35b" to "qwen36-35b",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,161 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trae 模型列表:内置参考映射 + 在线拉取账号可用模型 + 记住用户选择。
|
||||||
|
* 选择结果同时供设置页测试和 [TraeChatProxy] 作为缺省模型使用。
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class TraeModelProvider @Inject constructor(
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
private val credentialRepository: CredentialRepository,
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class TraeModel(val id: String, val label: String? = null)
|
||||||
|
|
||||||
|
private val prefs: SharedPreferences =
|
||||||
|
context.getSharedPreferences("rainytoken_proxy", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (id.isBlank()) return
|
||||||
|
selectedModel = id
|
||||||
|
prefs.edit().putString(KEY_MODEL, id).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取账号在线模型列表;失败时保留内置列表并返回 false。 */
|
||||||
|
suspend fun refreshFor(forcedRegion: ProviderRegion?): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val credential = credentialRepository.get(ServiceType.TRAE, null)
|
||||||
|
?: return@withContext false
|
||||||
|
if (credential !is Credential.TraeCredential) return@withContext false
|
||||||
|
val jwt = credential.jwt.trim().takeIf { it.isNotBlank() } ?: return@withContext false
|
||||||
|
|
||||||
|
val effectiveRegion = forcedRegion ?: runCatching {
|
||||||
|
ProviderRegion.valueOf(credential.region.uppercase())
|
||||||
|
}.getOrNull()
|
||||||
|
val intl = effectiveRegion == ProviderRegion.INTL
|
||||||
|
val base = if (intl) "https://a0ai-api-sg.byteintlapi.com" else "https://trae-api-cn.mchost.guru"
|
||||||
|
val origin = if (intl) "https://solo.trae.ai" else "https://solo.trae.cn"
|
||||||
|
val url = base + "/api/remote/v1/models" +
|
||||||
|
"?functions=solo_agent_remote%2Csolo_work_remote%2Csolo_design_remote" +
|
||||||
|
"&show_custom_model=true"
|
||||||
|
|
||||||
|
val headers = linkedMapOf(
|
||||||
|
"Authorization" to "Cloud-IDE-JWT $jwt",
|
||||||
|
"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",
|
||||||
|
"Origin" to origin,
|
||||||
|
"Referer" to origin + "/",
|
||||||
|
"User-Agent" to (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||||
|
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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<TraeModel>? {
|
||||||
|
val root = runCatching { json.parseToJsonElement(body) as? JsonObject }.getOrNull()
|
||||||
|
?: return null
|
||||||
|
val list = (root["data"] as? JsonObject)?.get("list") as? JsonArray ?: return null
|
||||||
|
val seen = LinkedHashMap<String, TraeModel>()
|
||||||
|
for (groupEl in list) {
|
||||||
|
val group = groupEl as? JsonObject ?: continue
|
||||||
|
val models = group["models"] as? JsonArray ?: continue
|
||||||
|
for (rawEl in models) {
|
||||||
|
val raw = rawEl as? JsonObject ?: continue
|
||||||
|
val id = (raw["name"] as? JsonPrimitive)?.contentOrNull?.trim()
|
||||||
|
?.takeIf { it.isNotEmpty() } ?: continue
|
||||||
|
val label = (raw["display_name"] as? JsonPrimitive)?.contentOrNull
|
||||||
|
?: (raw["display_model_name"] as? JsonPrimitive)?.contentOrNull
|
||||||
|
?: id
|
||||||
|
seen[id] = TraeModel(id, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen.values.toList().ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_MODEL = "trae_default_model"
|
||||||
|
|
||||||
|
/** 内置兜底模型(参考 trae2api-cn 的模型映射 /v1/models 列表)。 */
|
||||||
|
fun defaultModels(): List<TraeModel> = listOf(
|
||||||
|
TraeModel("glm-5.2", "GLM-5.2"),
|
||||||
|
TraeModel("glm-5.3", "GLM-5.3"),
|
||||||
|
TraeModel("glm-5.1", "GLM-5.1"),
|
||||||
|
TraeModel("glm-5", "GLM-5"),
|
||||||
|
TraeModel("glm-4.7", "GLM-4.7"),
|
||||||
|
TraeModel("glm-4.6", "GLM-4.6"),
|
||||||
|
TraeModel("DeepSeek-V4-Pro", "DeepSeek-V4-Pro"),
|
||||||
|
TraeModel("DeepSeek-V4-Pro-Official", "DeepSeek-V4-Pro 正式版"),
|
||||||
|
TraeModel("DeepSeek-V4-Flash", "DeepSeek-V4-Flash"),
|
||||||
|
TraeModel("DeepSeek-V4-Flash-Official", "DeepSeek-V4-Flash 正式版"),
|
||||||
|
TraeModel("Doubao-Seed-2.1-Pro", "Doubao-Seed-2.1-Pro"),
|
||||||
|
TraeModel("Doubao-Seed-2.1-Turbo", "Doubao-Seed-2.1-Turbo"),
|
||||||
|
TraeModel("Doubao-Seed-Code", "Doubao-Seed-Code"),
|
||||||
|
TraeModel("Doubao-Seed-Evolving", "Doubao-Seed-Evolving"),
|
||||||
|
TraeModel("qwen-3.7-plus", "qwen-3.7-plus"),
|
||||||
|
TraeModel("qwen-3.6-plus", "qwen-3.6-plus"),
|
||||||
|
TraeModel("qwen3.8-max", "qwen3.8-max"),
|
||||||
|
TraeModel("qwen-3.5", "qwen-3.5"),
|
||||||
|
TraeModel("qwen3-coder", "qwen3-coder"),
|
||||||
|
TraeModel("kimi-k2.6", "kimi-k2.6"),
|
||||||
|
TraeModel("kimi-k3", "kimi-k3"),
|
||||||
|
TraeModel("kimi-k2.7-code", "kimi-k2.7-code"),
|
||||||
|
TraeModel("kimi-k2", "kimi-k2"),
|
||||||
|
TraeModel("kimi-k2.5", "kimi-k2.5"),
|
||||||
|
TraeModel("minimax-m3", "minimax-m3"),
|
||||||
|
TraeModel("minimax-m2.7", "minimax-m2.7"),
|
||||||
|
TraeModel("minimax-m2.1", "minimax-m2.1"),
|
||||||
|
TraeModel("mimo-v2.5-pro", "mimo-v2.5-pro"),
|
||||||
|
TraeModel("mimo-v2.5", "mimo-v2.5"),
|
||||||
|
TraeModel("minimax-m25", "minimax-m25"),
|
||||||
|
TraeModel("qwen36-35b", "qwen36-35b"),
|
||||||
|
TraeModel("work", "Work(200K 档)"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ import com.rainy.token.data.proxy.KtorLocalProxyServer
|
|||||||
import com.rainy.token.data.proxy.LocalProxyServer
|
import com.rainy.token.data.proxy.LocalProxyServer
|
||||||
import com.rainy.token.data.proxy.Sub2ApiChatProxy
|
import com.rainy.token.data.proxy.Sub2ApiChatProxy
|
||||||
import com.rainy.token.data.proxy.TraeChatProxy
|
import com.rainy.token.data.proxy.TraeChatProxy
|
||||||
|
import com.rainy.token.data.proxy.TraeModelProvider
|
||||||
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
||||||
import com.rainy.token.data.repository.WorkBuddyRepository
|
import com.rainy.token.data.repository.WorkBuddyRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@ -250,8 +251,9 @@ object NetworkModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideTraeChatProxy(
|
fun provideTraeChatProxy(
|
||||||
okHttpClient: OkHttpClient,
|
okHttpClient: OkHttpClient,
|
||||||
credentialRepository: CredentialRepository
|
credentialRepository: CredentialRepository,
|
||||||
): TraeChatProxy = TraeChatProxy(okHttpClient, credentialRepository)
|
traeModelProvider: TraeModelProvider,
|
||||||
|
): TraeChatProxy = TraeChatProxy(okHttpClient, credentialRepository, traeModelProvider)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
@ -33,6 +33,8 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@ -60,6 +62,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.rainy.token.domain.model.CredentialStatus
|
import com.rainy.token.domain.model.CredentialStatus
|
||||||
|
import com.rainy.token.data.proxy.TraeModelProvider
|
||||||
import com.rainy.token.domain.service.ServiceType
|
import com.rainy.token.domain.service.ServiceType
|
||||||
import com.rainy.token.ui.components.ServiceIcon
|
import com.rainy.token.ui.components.ServiceIcon
|
||||||
import com.rainy.token.ui.components.StatusChip
|
import com.rainy.token.ui.components.StatusChip
|
||||||
@ -105,6 +108,8 @@ fun SettingsScreen(
|
|||||||
val proxyError by viewModel.proxyError.collectAsStateWithLifecycle()
|
val proxyError by viewModel.proxyError.collectAsStateWithLifecycle()
|
||||||
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
val proxyDebug by viewModel.proxyDebug.collectAsStateWithLifecycle()
|
||||||
val proxyDebugRaw by viewModel.proxyDebugRaw.collectAsStateWithLifecycle()
|
val proxyDebugRaw by viewModel.proxyDebugRaw.collectAsStateWithLifecycle()
|
||||||
|
val proxyModels by viewModel.proxyModels.collectAsStateWithLifecycle()
|
||||||
|
val proxyModel by viewModel.proxyModel.collectAsStateWithLifecycle()
|
||||||
var showLanguageDialog by remember { mutableStateOf(false) }
|
var showLanguageDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -182,6 +187,10 @@ fun SettingsScreen(
|
|||||||
error = proxyError,
|
error = proxyError,
|
||||||
debug = proxyDebug,
|
debug = proxyDebug,
|
||||||
debugRaw = proxyDebugRaw,
|
debugRaw = proxyDebugRaw,
|
||||||
|
models = proxyModels,
|
||||||
|
selectedModel = proxyModel,
|
||||||
|
onSelectModel = viewModel::selectProxyModel,
|
||||||
|
onRefreshModels = viewModel::refreshProxyModels,
|
||||||
onToggle = viewModel::setProxyEnabled,
|
onToggle = viewModel::setProxyEnabled,
|
||||||
onTestTrae = viewModel::testTraeStream,
|
onTestTrae = viewModel::testTraeStream,
|
||||||
onTestWorkBuddy = viewModel::testWorkBuddyStream
|
onTestWorkBuddy = viewModel::testWorkBuddyStream
|
||||||
@ -216,11 +225,16 @@ private fun ProxyGatewayCard(
|
|||||||
error: String?,
|
error: String?,
|
||||||
debug: String?,
|
debug: String?,
|
||||||
debugRaw: String?,
|
debugRaw: String?,
|
||||||
|
models: List<TraeModelProvider.TraeModel>,
|
||||||
|
selectedModel: String,
|
||||||
|
onSelectModel: (String) -> Unit,
|
||||||
|
onRefreshModels: () -> Unit,
|
||||||
onToggle: (Boolean) -> Unit,
|
onToggle: (Boolean) -> Unit,
|
||||||
onTestTrae: () -> Unit,
|
onTestTrae: () -> Unit,
|
||||||
onTestWorkBuddy: () -> Unit
|
onTestWorkBuddy: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
var modelMenuExpanded by remember { mutableStateOf(false) }
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@ -270,6 +284,39 @@ private fun ProxyGatewayCard(
|
|||||||
Text("测试 WorkBuddy 流")
|
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 = { modelMenuExpanded = true }) {
|
||||||
|
Text(selectedModel + " ▾")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = modelMenuExpanded,
|
||||||
|
onDismissRequest = { modelMenuExpanded = false }
|
||||||
|
) {
|
||||||
|
models.forEach { m ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(m.label ?: m.id) },
|
||||||
|
onClick = {
|
||||||
|
onSelectModel(m.id)
|
||||||
|
modelMenuExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = onRefreshModels) {
|
||||||
|
Text("刷新")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (debug != null) {
|
if (debug != null) {
|
||||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import com.rainy.token.data.proxy.ProviderRegion
|
|||||||
import com.rainy.token.data.proxy.ProxyUpstreamStream
|
import com.rainy.token.data.proxy.ProxyUpstreamStream
|
||||||
import com.rainy.token.data.proxy.ProxyServerConfig
|
import com.rainy.token.data.proxy.ProxyServerConfig
|
||||||
import com.rainy.token.data.proxy.TraeChatProxy
|
import com.rainy.token.data.proxy.TraeChatProxy
|
||||||
|
import com.rainy.token.data.proxy.TraeModelProvider
|
||||||
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
import com.rainy.token.data.proxy.WorkBuddyChatProxy
|
||||||
import com.rainy.token.data.repository.CredentialRepository
|
import com.rainy.token.data.repository.CredentialRepository
|
||||||
import com.rainy.token.domain.model.CredentialStatus
|
import com.rainy.token.domain.model.CredentialStatus
|
||||||
@ -38,7 +39,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private val credentialRepository: CredentialRepository,
|
private val credentialRepository: CredentialRepository,
|
||||||
private val localProxyServer: LocalProxyServer,
|
private val localProxyServer: LocalProxyServer,
|
||||||
private val traeChatProxy: TraeChatProxy,
|
private val traeChatProxy: TraeChatProxy,
|
||||||
private val workBuddyChatProxy: WorkBuddyChatProxy
|
private val workBuddyChatProxy: WorkBuddyChatProxy,
|
||||||
|
private val traeModelProvider: TraeModelProvider,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||||
@ -74,12 +76,34 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private val _proxyDebugRaw = MutableStateFlow<String?>(null)
|
private val _proxyDebugRaw = MutableStateFlow<String?>(null)
|
||||||
val proxyDebugRaw: StateFlow<String?> = _proxyDebugRaw.asStateFlow()
|
val proxyDebugRaw: StateFlow<String?> = _proxyDebugRaw.asStateFlow()
|
||||||
|
|
||||||
|
/** Trae 可用模型列表(内置兜底 + 在线拉取账号模型)。 */
|
||||||
|
val proxyModels: StateFlow<List<TraeModelProvider.TraeModel>> = traeModelProvider.models
|
||||||
|
|
||||||
|
/** 当前选择的 Trae 默认模型。 */
|
||||||
|
private val _proxyModel = MutableStateFlow(traeModelProvider.selectedModel)
|
||||||
|
val proxyModel: StateFlow<String> = _proxyModel.asStateFlow()
|
||||||
|
|
||||||
|
/** 切换 Trae 默认模型并持久化。 */
|
||||||
|
fun selectProxyModel(id: String) {
|
||||||
|
traeModelProvider.setSelectedModel(id)
|
||||||
|
_proxyModel.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取当前账号可用的 Trae 模型列表。 */
|
||||||
|
fun refreshProxyModels() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val ok = traeModelProvider.refreshFor(ProviderRegion.CN)
|
||||||
|
if (!ok) _proxyDebug.value = "模型列表更新失败(当前显示内置列表)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 用 Trae CN 默认账号发一条 stream=true 测试。 */
|
/** 用 Trae CN 默认账号发一条 stream=true 测试。 */
|
||||||
fun testTraeStream() {
|
fun testTraeStream() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_proxyDebug.value = "正在请求 Trae CN 流..."
|
_proxyDebug.value = "正在请求 Trae CN 流..."
|
||||||
_proxyDebugRaw.value = null
|
_proxyDebugRaw.value = null
|
||||||
val body = """{\"model\":\"glm-5.2\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"只回复OK两个字\"}],\"max_tokens\":100}\"""
|
val model = traeModelProvider.selectedModel
|
||||||
|
val body = """{"model":"$model","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}"""
|
||||||
try {
|
try {
|
||||||
val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
val stream = traeChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
@ -108,7 +132,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_proxyDebug.value = "正在请求 WorkBuddy CN 流..."
|
_proxyDebug.value = "正在请求 WorkBuddy CN 流..."
|
||||||
_proxyDebugRaw.value = null
|
_proxyDebugRaw.value = null
|
||||||
val body = """{\"model\":\"glm-5.2\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"只回复OK两个字\"}],\"max_tokens\":100}\"""
|
val body = """{"model":"glm-5.2","stream":true,"messages":[{"role":"user","content":"只回复OK两个字"}],"max_tokens":100}"""
|
||||||
try {
|
try {
|
||||||
val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
val stream = workBuddyChatProxy.openStreamingChat(body, null, ProviderRegion.CN)
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user