diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc091e5..b56c3a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.rainy.token" minSdk = 31 targetSdk = 35 - versionCode = 49 - versionName = "1.7.29" + versionCode = 50 + versionName = "1.7.30" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/rainy/token/data/proxy/KtorLocalProxyServer.kt b/app/src/main/java/com/rainy/token/data/proxy/KtorLocalProxyServer.kt index 907514e..55938a8 100644 --- a/app/src/main/java/com/rainy/token/data/proxy/KtorLocalProxyServer.kt +++ b/app/src/main/java/com/rainy/token/data/proxy/KtorLocalProxyServer.kt @@ -98,85 +98,124 @@ class KtorLocalProxyServer @Inject constructor( json() } routing { + get("/") { + call.respond( + mapOf( + "status" to "ok", + "endpoints" to listOf("GET /health", "GET /v1/models", "POST /v1/chat/completions"), + "base_url" to "http://<手机热点或局域网IP>:8787/v1" + ) + ) + } + get("/v1") { + call.respond( + mapOf( + "status" to "ok", + "endpoints" to listOf("GET /health", "GET /v1/models", "POST /v1/chat/completions"), + "base_url" to "http://<手机热点或局域网IP>:8787/v1" + ) + ) + } get("/health") { call.respond(mapOf("status" to "ok")) } get("/v1/models") { - if (!authorized(call, apiKey)) { - call.respond(HttpStatusCode.Unauthorized, errorBody("未授权")) - return@get - } - val result = sub2Api.forwardModels() - if (result == null) { - call.respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key")) - } else { - call.respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) - } + call.respondModels(apiKey, sub2Api) + } + // 部分客户端 Base URL 不带 /v1 时兼容 + get("/models") { + call.respondModels(apiKey, sub2Api) } post("/v1/chat/completions") { - if (!authorized(call, apiKey)) { - call.respond(HttpStatusCode.Unauthorized, errorBody("未授权")) - return@post - } - val rawBody = call.receiveText() - if (rawBody.length > MAX_REQUEST_BYTES) { - call.respond(HttpStatusCode(413, ""), errorBody("请求体过大")) - return@post - } - val conversationId = call.request.headers["X-Conversation-Id"] ?: extractUser(rawBody) - try { - if (extractStream(rawBody)) { - val stream = router.openStreamingChat(rawBody, conversationId) - if (stream == null) { - call.respond( - HttpStatusCode.BadRequest, - errorBody("所有上游均不可用或未配置,请检查设置") - ) - } else { - call.respondOutputStream( - contentType = contentTypeOf(stream.contentType), - status = HttpStatusCode(stream.status, "") - ) { - try { - val buffer = ByteArray(8192) - val input = stream.input - while (true) { - val read = input.read(buffer) - if (read < 0) break - write(buffer, 0, read) - flush() - } - } finally { - stream.close() - } + call.respondChatCompletion(apiKey, router) + } + post("/chat/completions") { + call.respondChatCompletion(apiKey, router) + } + } + } + + private suspend fun ApplicationCall.respondModels( + apiKey: String?, + sub2Api: Sub2ApiChatProxy, + ) { + if (!authorized(this, apiKey)) { + respond(HttpStatusCode.Unauthorized, errorBody("未授权")) + return + } + val result = sub2Api.forwardModels() + if (result == null) { + respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key")) + } else { + respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) + } + } + + private suspend fun ApplicationCall.respondChatCompletion( + apiKey: String?, + router: ProviderRouter, + ) { + if (!authorized(this, apiKey)) { + respond(HttpStatusCode.Unauthorized, errorBody("未授权")) + return + } + val rawBody = receiveText() + if (rawBody.length > MAX_REQUEST_BYTES) { + respond(HttpStatusCode(413, ""), errorBody("请求体过大")) + return + } + val conversationId = request.headers["X-Conversation-Id"] ?: extractUser(rawBody) + try { + if (extractStream(rawBody)) { + val stream = router.openStreamingChat(rawBody, conversationId) + if (stream == null) { + respond( + HttpStatusCode.BadRequest, + errorBody("所有上游均不可用或未配置,请检查设置") + ) + } else { + respondOutputStream( + contentType = contentTypeOf(stream.contentType), + status = HttpStatusCode(stream.status, "") + ) { + try { + val buffer = ByteArray(8192) + val input = stream.input + while (true) { + val read = input.read(buffer) + if (read < 0) break + write(buffer, 0, read) + flush() } - } - } else { - val result = router.forwardChat(rawBody, conversationId) - if (result == null) { - call.respond( - HttpStatusCode.BadRequest, - errorBody("所有上游均不可用或未配置,请检查设置") - ) - } else { - call.respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) + } finally { + stream.close() } } - } catch (e: IOException) { - val detail = e.message ?: "未知错误" - call.respond( - HttpStatusCode.BadGateway, - errorBody("上游网络错误:$detail") - ) - } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) throw e - val detail = e.message ?: "未知错误" - call.respond( - HttpStatusCode.InternalServerError, - errorBody("网关内部错误:$detail") + } + } else { + val result = router.forwardChat(rawBody, conversationId) + if (result == null) { + respond( + HttpStatusCode.BadRequest, + errorBody("所有上游均不可用或未配置,请检查设置") ) + } else { + respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) } } + } catch (e: IOException) { + val detail = e.message ?: "未知错误" + respond( + HttpStatusCode.BadGateway, + errorBody("上游网络错误:$detail") + ) + } catch (e: Exception) { + if (e is kotlinx.coroutines.CancellationException) throw e + val detail = e.message ?: "未知错误" + respond( + HttpStatusCode.InternalServerError, + errorBody("网关内部错误:$detail") + ) } }