fix(wb): 国际版反代走 console 路由并补齐请求头;模型目录改 /v3/config 与 CLI 名单过滤
This commit is contained in:
parent
f917e5bfd6
commit
027eb6185d
@ -12,12 +12,17 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WorkBuddy / CodeBuddy 上游 Chat 代理(国内版)。
|
* WorkBuddy / CodeBuddy 上游 Chat 代理(国内版)。
|
||||||
*
|
*
|
||||||
* 上游协议为 OpenAI 兼容的 POST {base}/v2/chat/completions,Bearer accessToken。
|
* 上游协议为 OpenAI 兼容的 POST {base}/v2/chat/completions,Bearer accessToken。
|
||||||
* 这里保留客户端原始 body(M1b 最小实现),并补齐桌面端请求头;完整归一化在后续迭代。
|
* 这里补齐桌面端请求头与上游 body 归一化,国际版先走 console 路由再退回 v2。
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class WorkBuddyChatProxy @Inject constructor(
|
class WorkBuddyChatProxy @Inject constructor(
|
||||||
@ -45,32 +50,28 @@ class WorkBuddyChatProxy @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
"https://copilot.tencent.com"
|
"https://copilot.tencent.com"
|
||||||
}
|
}
|
||||||
val requestId = UUID.randomUUID().toString()
|
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
||||||
val builder = Request.Builder()
|
val intl = effectiveRegion == ProviderRegion.INTL
|
||||||
.url(base + "/v2/chat/completions")
|
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
||||||
.addHeader("Content-Type", "application/json")
|
for (path in paths) {
|
||||||
.addHeader("Accept", "text/event-stream")
|
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
|
||||||
.addHeader("Authorization", "Bearer $accessToken")
|
val response = try {
|
||||||
.addHeader("User-Agent", "RainyToken/1.0")
|
okHttpClient.newCall(request).execute()
|
||||||
.addHeader("X-IDE-Type", "VSCode")
|
} catch (e: java.io.IOException) {
|
||||||
.addHeader("X-IDE-Name", "CodeBuddy")
|
throw e
|
||||||
.addHeader("X-IDE-Version", "3.0.0")
|
}
|
||||||
.addHeader("X-Product", "CodeBuddy")
|
val status = response.code
|
||||||
.addHeader("X-Agent-Intent", "craft")
|
// 国际站 console 路由返回 404/405 时再退回共享 v2 路径
|
||||||
.addHeader("X-Request-ID", requestId)
|
if (status == 404 || status == 405) {
|
||||||
.addHeader("X-Conv-Request-ID", requestId)
|
response.close()
|
||||||
.post(requestBody.toRequestBody("application/json".toMediaType()))
|
continue
|
||||||
|
}
|
||||||
val response = try {
|
val bytes = try { response.body?.bytes() ?: ByteArray(0) } catch (_: Throwable) { ByteArray(0) }
|
||||||
okHttpClient.newCall(builder.build()).execute()
|
val contentType = response.header("Content-Type") ?: "application/json"
|
||||||
} catch (e: java.io.IOException) {
|
response.close()
|
||||||
throw e
|
return@withContext ProxyUpstreamResponse(status, contentType, bytes)
|
||||||
}
|
}
|
||||||
val bytes = try { response.body?.bytes() ?: ByteArray(0) } catch (_: Throwable) { ByteArray(0) }
|
return@withContext null
|
||||||
val contentType = response.header("Content-Type") ?: "application/json"
|
|
||||||
val status = response.code
|
|
||||||
response.close()
|
|
||||||
ProxyUpstreamResponse(status, contentType, bytes)
|
|
||||||
}
|
}
|
||||||
suspend fun openStreamingChat(
|
suspend fun openStreamingChat(
|
||||||
requestBody: String,
|
requestBody: String,
|
||||||
@ -91,37 +92,161 @@ class WorkBuddyChatProxy @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
"https://copilot.tencent.com"
|
"https://copilot.tencent.com"
|
||||||
}
|
}
|
||||||
|
val upstreamBody = prepareChatBody(requestBody, effectiveRegion == ProviderRegion.INTL)
|
||||||
|
val intl = effectiveRegion == ProviderRegion.INTL
|
||||||
|
val paths = if (intl) listOf("/console/chat/completions", "/v2/chat/completions") else listOf("/v2/chat/completions")
|
||||||
|
for (path in paths) {
|
||||||
|
val request = buildChatRequest(base, path, upstreamBody, accessToken, intl)
|
||||||
|
val response = try {
|
||||||
|
okHttpClient.newCall(request).execute()
|
||||||
|
} catch (e: java.io.IOException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (response.code == 404 || response.code == 405) {
|
||||||
|
response.close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val input = response.body?.byteStream() ?: run {
|
||||||
|
response.close()
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
return@withContext ProxyUpstreamStream(
|
||||||
|
status = response.code,
|
||||||
|
contentType = response.header("Content-Type") ?: "text/event-stream",
|
||||||
|
input = input,
|
||||||
|
close = { response.close() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildChatRequest(
|
||||||
|
base: String,
|
||||||
|
path: String,
|
||||||
|
upstreamBody: String,
|
||||||
|
accessToken: String,
|
||||||
|
intl: Boolean,
|
||||||
|
): Request {
|
||||||
|
val origin = if (intl) "https://www.workbuddy.ai" else "https://www.codebuddy.cn"
|
||||||
val requestId = UUID.randomUUID().toString()
|
val requestId = UUID.randomUUID().toString()
|
||||||
val builder = Request.Builder()
|
val builder = Request.Builder()
|
||||||
.url(base + "/v2/chat/completions")
|
.url(base + path)
|
||||||
.addHeader("Content-Type", "application/json")
|
.addHeader("Content-Type", "application/json")
|
||||||
.addHeader("Accept", "text/event-stream")
|
.addHeader("Accept", "application/json, text/event-stream")
|
||||||
.addHeader("Authorization", "Bearer $accessToken")
|
.addHeader("Authorization", "Bearer $accessToken")
|
||||||
.addHeader("User-Agent", "RainyToken/1.0")
|
.addHeader("User-Agent", "CLI/2.63.2 CodeBuddy/2.63.2")
|
||||||
.addHeader("X-IDE-Type", "VSCode")
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
.addHeader("X-IDE-Name", "CodeBuddy")
|
.addHeader("Origin", origin)
|
||||||
.addHeader("X-IDE-Version", "3.0.0")
|
.addHeader("Referer", origin + "/")
|
||||||
.addHeader("X-Product", "CodeBuddy")
|
.addHeader("X-CodeBuddy-Request", "1")
|
||||||
.addHeader("X-Agent-Intent", "craft")
|
.addHeader("Accept-Language", if (intl) "en-US" else "zh-CN")
|
||||||
|
.addHeader("X-No-User-Id", "1")
|
||||||
|
.addHeader("X-Agent-Purpose", "conversation")
|
||||||
|
.addHeader("X-IDE-Name", "WorkBuddy")
|
||||||
|
.addHeader("X-IDE-Type", "WorkBuddy")
|
||||||
|
.addHeader("X-IDE-Version", "2.63.2")
|
||||||
|
.addHeader("X-Product", "WorkBuddy")
|
||||||
.addHeader("X-Request-ID", requestId)
|
.addHeader("X-Request-ID", requestId)
|
||||||
.addHeader("X-Conv-Request-ID", requestId)
|
.addHeader("X-Conv-Request-ID", requestId)
|
||||||
.post(requestBody.toRequestBody("application/json".toMediaType()))
|
if (intl) {
|
||||||
|
// 国际站强制声明个人客户端:不携带 enterprise,域固定为 www.workbuddy.ai
|
||||||
|
builder.addHeader("X-No-Enterprise-Id", "1")
|
||||||
|
builder.addHeader("X-Domain", "www.workbuddy.ai")
|
||||||
|
} else {
|
||||||
|
builder.addHeader("X-No-Enterprise-Id", "1")
|
||||||
|
builder.addHeader("X-No-Department-Info", "1")
|
||||||
|
}
|
||||||
|
return builder.post(upstreamBody.toRequestBody("application/json".toMediaType())).build()
|
||||||
|
}
|
||||||
|
|
||||||
val response = try {
|
/**
|
||||||
okHttpClient.newCall(builder.build()).execute()
|
* 对齐 dsh-workbuddy-connect 的上游归一化:
|
||||||
} catch (e: java.io.IOException) {
|
* - 上游拒绝非流式,强制 stream=true;
|
||||||
throw e
|
* - developer -> system(国内站直接拒绝 developer role);
|
||||||
|
* - 国际站首条必须 system,缺省则前置空 system(避免 11128);
|
||||||
|
* - max_completion_tokens 翻译成 max_tokens;
|
||||||
|
* - tool_choice 只接受字符串,对象形式拍平成 auto/工具名/none。
|
||||||
|
*/
|
||||||
|
private fun prepareChatBody(body: String, intl: Boolean): String {
|
||||||
|
val root = runCatching { Json.parseToJsonElement(body) as? JsonObject }.getOrNull() ?: return body
|
||||||
|
if (root.isEmpty()) return body
|
||||||
|
val out = LinkedHashMap(root)
|
||||||
|
out["stream"] = JsonPrimitive(true)
|
||||||
|
if (!out.containsKey("stream_options")) {
|
||||||
|
out["stream_options"] = JsonObject(
|
||||||
|
mapOf("include_usage" to JsonPrimitive(true))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val input = response.body?.byteStream() ?: run {
|
val maxCompletion = (root["max_completion_tokens"] as? JsonPrimitive)?.contentOrNull
|
||||||
response.close()
|
?.toLongOrNull()
|
||||||
return@withContext null
|
if (maxCompletion != null && !out.containsKey("max_tokens")) {
|
||||||
|
out["max_tokens"] = JsonPrimitive(maxCompletion)
|
||||||
}
|
}
|
||||||
ProxyUpstreamStream(
|
val messages = root["messages"] as? JsonArray
|
||||||
status = response.code,
|
if (messages != null) {
|
||||||
contentType = response.header("Content-Type") ?: "text/event-stream",
|
var list = messages.map { msg ->
|
||||||
input = input,
|
val obj = msg as? JsonObject ?: return@map msg
|
||||||
close = { response.close() },
|
if ((obj["role"] as? JsonPrimitive)?.contentOrNull == "developer") {
|
||||||
)
|
JsonObject(obj.entries.associate { (key, value) ->
|
||||||
|
key to (if (key == "role") JsonPrimitive("system") else value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
obj
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
if (intl) {
|
||||||
|
val firstRole = (list.firstOrNull() as? JsonObject)?.get("role") as? JsonPrimitive
|
||||||
|
if (firstRole?.contentOrNull != "system") {
|
||||||
|
val fallback = JsonObject(
|
||||||
|
mapOf(
|
||||||
|
"role" to JsonPrimitive("system"),
|
||||||
|
"content" to JsonPrimitive("You are a helpful assistant."),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
list.add(0, fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out["messages"] = JsonArray(list)
|
||||||
|
}
|
||||||
|
normalizeToolChoice(out)
|
||||||
|
return JsonObject(out).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeToolChoice(out: MutableMap<String, kotlinx.serialization.json.JsonElement>) {
|
||||||
|
val choice = out["tool_choice"] ?: return
|
||||||
|
val suppress = {
|
||||||
|
out.remove("tools")
|
||||||
|
out.remove("functions")
|
||||||
|
}
|
||||||
|
if (choice is JsonPrimitive) {
|
||||||
|
val s = choice.contentOrNull?.trim()?.lowercase()
|
||||||
|
if (s == "none") {
|
||||||
|
out.remove("tool_choice")
|
||||||
|
suppress()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val obj = choice as? JsonObject
|
||||||
|
if (obj != null) {
|
||||||
|
val type = (obj["type"] as? JsonPrimitive)?.contentOrNull?.trim()?.lowercase()
|
||||||
|
when (type) {
|
||||||
|
"none" -> {
|
||||||
|
out.remove("tool_choice")
|
||||||
|
suppress()
|
||||||
|
}
|
||||||
|
"auto", "required" -> out["tool_choice"] = JsonPrimitive(type)
|
||||||
|
"function" -> {
|
||||||
|
val fn = obj["function"] as? JsonObject
|
||||||
|
val name = ((fn?.get("name") as? JsonPrimitive)?.contentOrNull
|
||||||
|
?: (obj["name"] as? JsonPrimitive)?.contentOrNull
|
||||||
|
?: "").trim()
|
||||||
|
out["tool_choice"] = JsonPrimitive(if (name.isNotEmpty()) name else "auto")
|
||||||
|
}
|
||||||
|
else -> out.remove("tool_choice")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.remove("tool_choice")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,12 +37,12 @@ class WorkBuddyModelProvider @Inject constructor(
|
|||||||
context.getSharedPreferences("rainytoken_proxy", Context.MODE_PRIVATE)
|
context.getSharedPreferences("rainytoken_proxy", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
private val _modelsByRegion = MutableStateFlow(
|
private val _modelsByRegion = MutableStateFlow(
|
||||||
ProviderRegion.entries.associateWith { defaultModels() }
|
ProviderRegion.entries.associateWith { defaultModels(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 获取指定区域模型列表(在线拉取成功后该区域独立更新)。 */
|
/** 获取指定区域模型列表(在线拉取成功后该区域独立更新)。 */
|
||||||
fun modelsFor(region: ProviderRegion): List<TraeModelProvider.TraeModel> =
|
fun modelsFor(region: ProviderRegion): List<TraeModelProvider.TraeModel> =
|
||||||
_modelsByRegion.value[region] ?: defaultModels()
|
_modelsByRegion.value[region] ?: defaultModels(region)
|
||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
@ -70,17 +70,23 @@ class WorkBuddyModelProvider @Inject constructor(
|
|||||||
?: return@withContext false
|
?: return@withContext false
|
||||||
val intl = region == ProviderRegion.INTL
|
val intl = region == ProviderRegion.INTL
|
||||||
val base = if (intl) "https://www.workbuddy.ai" else "https://copilot.tencent.com"
|
val base = if (intl) "https://www.workbuddy.ai" else "https://copilot.tencent.com"
|
||||||
val url = base + "/v2/enterprises/personal/models"
|
// 国内站官方 CLI 吃 /console/enterprises/personal/models;国际站产品目录在 /v3/config,
|
||||||
|
// 且必须用 App 形态 UA,否则网关拒发(实测 code 12403)。
|
||||||
|
val url = base + if (intl) "/v3/config" else "/console/enterprises/personal/models"
|
||||||
|
val origin = if (intl) "https://www.workbuddy.ai" else "https://www.codebuddy.cn"
|
||||||
val headers = linkedMapOf(
|
val headers = linkedMapOf(
|
||||||
"Authorization" to "Bearer $accessToken",
|
"Authorization" to "Bearer $accessToken",
|
||||||
"Content-Type" to "application/json",
|
"Content-Type" to "application/json",
|
||||||
"Accept" to "application/json",
|
"Accept" to "application/json",
|
||||||
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
|
"Origin" to origin,
|
||||||
|
"Referer" to "$origin/",
|
||||||
|
"X-CodeBuddy-Request" to "1",
|
||||||
|
"Accept-Language" to (if (intl) "en-US" else "zh-CN"),
|
||||||
"X-Client-ID" to "codebuddy-cli",
|
"X-Client-ID" to "codebuddy-cli",
|
||||||
"X-Client-Version" to "2.143.1",
|
"X-Client-Version" to "2.63.2",
|
||||||
"X-Product" to "SaaS",
|
"X-Product" to (if (intl) "SaaS" else "WorkBuddy"),
|
||||||
"User-Agent" to "CLI/2.143.1 CodeBuddy/2.143.1",
|
"User-Agent" to (if (intl) "WorkBuddyAI/5.5.2" else "CLI/2.63.2 CodeBuddy/2.63.2"),
|
||||||
"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 request = Request.Builder().url(url).headers(headers.toHeaders()).get().build()
|
||||||
val parsed = try {
|
val parsed = try {
|
||||||
@ -112,13 +118,24 @@ class WorkBuddyModelProvider @Inject constructor(
|
|||||||
private fun parseModelList(body: String): List<TraeModelProvider.TraeModel>? {
|
private fun parseModelList(body: String): List<TraeModelProvider.TraeModel>? {
|
||||||
val root = runCatching { json.parseToJsonElement(body) as? JsonObject }.getOrNull()
|
val root = runCatching { json.parseToJsonElement(body) as? JsonObject }.getOrNull()
|
||||||
?: return null
|
?: return null
|
||||||
val data = root["data"] as? JsonObject ?: return null
|
// 国内站永远是 {code,msg,data} 包络;国际站 /v3/config 有时裸返回产品文档。
|
||||||
val models = data["models"] as? JsonArray ?: return null
|
val data = (root["data"] as? JsonObject) ?: root
|
||||||
|
val rawModels = data["models"] as? JsonArray ?: return null
|
||||||
|
val agents = data["agents"] as? JsonArray
|
||||||
|
// 只暴露 cli agent 可用的模型(国际站 App 目录里 agents[].name=="cli" 限定模型名单)
|
||||||
|
val cliIds = agents?.mapNotNull { it as? JsonObject }?.firstOrNull { agent ->
|
||||||
|
(agent["name"] as? JsonPrimitive)?.contentOrNull == "cli"
|
||||||
|
}?.get("models") as? JsonArray
|
||||||
|
// 有 agents 文档但找不到 cli 列表时,不猜模型,避免把 App 只读模型塞给 CLI 反代
|
||||||
|
if (agents != null && cliIds == null) return null
|
||||||
|
val allowed = cliIds?.mapNotNull { (it as? JsonPrimitive)?.contentOrNull }?.toSet()
|
||||||
val seen = LinkedHashMap<String, TraeModelProvider.TraeModel>()
|
val seen = LinkedHashMap<String, TraeModelProvider.TraeModel>()
|
||||||
for (rawEl in models) {
|
for (rawEl in rawModels) {
|
||||||
val raw = rawEl as? JsonObject ?: continue
|
val raw = rawEl as? JsonObject ?: continue
|
||||||
val id = (raw["id"] as? JsonPrimitive)?.contentOrNull?.trim()
|
val id = (raw["id"] as? JsonPrimitive)?.contentOrNull?.trim()
|
||||||
?.takeIf { it.isNotEmpty() } ?: continue
|
?.takeIf { it.isNotEmpty() } ?: continue
|
||||||
|
if (allowed != null && id !in allowed) continue
|
||||||
|
if (raw["disabled"] as? JsonPrimitive == JsonPrimitive(true)) continue
|
||||||
val label = (raw["name"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() } ?: id
|
val label = (raw["name"] as? JsonPrimitive)?.contentOrNull?.takeIf { it.isNotBlank() } ?: id
|
||||||
seen[id] = TraeModelProvider.TraeModel(id, label)
|
seen[id] = TraeModelProvider.TraeModel(id, label)
|
||||||
}
|
}
|
||||||
@ -130,12 +147,31 @@ class WorkBuddyModelProvider @Inject constructor(
|
|||||||
private const val KEY_MODEL_INTL = "wb_default_model_intl"
|
private const val KEY_MODEL_INTL = "wb_default_model_intl"
|
||||||
private const val DEFAULT_MODEL = "hy4-preview"
|
private const val DEFAULT_MODEL = "hy4-preview"
|
||||||
|
|
||||||
/** 内置兜底(参考 workbuddy-gateway 模型目录,在线列表成功后会被替换)。 */
|
/** 内置兜底(在线列表成功后会被替换)。 */
|
||||||
fun defaultModels(): List<TraeModelProvider.TraeModel> = listOf(
|
fun defaultModels(region: ProviderRegion): List<TraeModelProvider.TraeModel> =
|
||||||
|
if (region == ProviderRegion.INTL) intlDefaultModels() else cnDefaultModels()
|
||||||
|
|
||||||
|
/** 国内站 CLI 模型兜底(参考 workbuddy-gateway 模型目录)。 */
|
||||||
|
private fun cnDefaultModels(): List<TraeModelProvider.TraeModel> = listOf(
|
||||||
TraeModelProvider.TraeModel("hy4-preview", "hy4-preview"),
|
TraeModelProvider.TraeModel("hy4-preview", "hy4-preview"),
|
||||||
TraeModelProvider.TraeModel("hy3", "hy3"),
|
TraeModelProvider.TraeModel("hy3", "hy3"),
|
||||||
TraeModelProvider.TraeModel("hy3-preview", "hy3-preview"),
|
TraeModelProvider.TraeModel("hy3-preview", "hy3-preview"),
|
||||||
TraeModelProvider.TraeModel("deepseek-v4.1-flash", "deepseek-v4.1-flash"),
|
TraeModelProvider.TraeModel("deepseek-v4.1-flash", "deepseek-v4.1-flash"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 国际站 App 模型兜底(参考 cpa-multi-plugins 实测目录,避免刷新前误用国内模型名)。 */
|
||||||
|
private fun intlDefaultModels(): List<TraeModelProvider.TraeModel> = listOf(
|
||||||
|
TraeModelProvider.TraeModel("default-model", "Auto"),
|
||||||
|
TraeModelProvider.TraeModel("fast-model", "Fast"),
|
||||||
|
TraeModelProvider.TraeModel("balanced-model", "Balanced"),
|
||||||
|
TraeModelProvider.TraeModel("primary-model", "Primary"),
|
||||||
|
TraeModelProvider.TraeModel("deep-model", "Deep"),
|
||||||
|
TraeModelProvider.TraeModel("hy4-preview-f", "Hy4 preview"),
|
||||||
|
TraeModelProvider.TraeModel("hy3", "Hy3"),
|
||||||
|
TraeModelProvider.TraeModel("deepseek-v4.1-flash", "Deepseek-V4.1-Flash"),
|
||||||
|
TraeModelProvider.TraeModel("gpt-6-astra", "GPT-6-Astra"),
|
||||||
|
TraeModelProvider.TraeModel("gpt-5.6-luna", "GPT-5.6-Luna"),
|
||||||
|
TraeModelProvider.TraeModel("gemini-3.5-flash", "Gemini-3.5-Flash"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -214,6 +214,9 @@ class AutoCheckinManager @Inject constructor(
|
|||||||
ServiceResult(service, label, false, true, 0, "", false)
|
ServiceResult(service, label, false, true, 0, "", false)
|
||||||
WorkBuddyRepository.CheckinOutcome.QUERY ->
|
WorkBuddyRepository.CheckinOutcome.QUERY ->
|
||||||
ServiceResult(service, label, false, true, 0, "", false)
|
ServiceResult(service, label, false, true, 0, "", false)
|
||||||
|
WorkBuddyRepository.CheckinOutcome.NO_CAMPAIGN ->
|
||||||
|
// 国际站没有签到活动不是账号失败:静默跳过,不记失败日志
|
||||||
|
ServiceResult(service, label, false, false, 0, s.message, false)
|
||||||
WorkBuddyRepository.CheckinOutcome.FAILED -> {
|
WorkBuddyRepository.CheckinOutcome.FAILED -> {
|
||||||
recordFailure(service, account.id, CheckinStatusStore.STATUS_FAILED)
|
recordFailure(service, account.id, CheckinStatusStore.STATUS_FAILED)
|
||||||
ServiceResult(service, label, false, false, 0, s.message, failed = true)
|
ServiceResult(service, label, false, false, 0, s.message, failed = true)
|
||||||
|
|||||||
@ -184,6 +184,8 @@ class ServiceDetailViewModel @Inject constructor(
|
|||||||
if (s.message.isNotBlank()) UiText.Dynamic(s.message)
|
if (s.message.isNotBlank()) UiText.Dynamic(s.message)
|
||||||
else UiText.Resource(R.string.checkin_failed)
|
else UiText.Resource(R.string.checkin_failed)
|
||||||
)
|
)
|
||||||
|
WorkBuddyRepository.CheckinOutcome.NO_CAMPAIGN ->
|
||||||
|
CheckinState.Notice(UiText.Resource(R.string.checkin_wb_no_campaign))
|
||||||
// QUERY 不该出现在签到动作结果里,兜底为通用成功
|
// QUERY 不该出现在签到动作结果里,兜底为通用成功
|
||||||
WorkBuddyRepository.CheckinOutcome.QUERY ->
|
WorkBuddyRepository.CheckinOutcome.QUERY ->
|
||||||
CheckinState.Done(UiText.Resource(R.string.checkin_success))
|
CheckinState.Done(UiText.Resource(R.string.checkin_success))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user