fix(proxy): 根路由/无 v1 前缀兼容,访问裸地址不再 404

This commit is contained in:
Liuxinyu176 2026-10-10 14:39:04 +08:00
parent a53899c6cf
commit cf53d7c66f
2 changed files with 108 additions and 69 deletions

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.rainy.token" applicationId = "com.rainy.token"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 49 versionCode = 50
versionName = "1.7.29" versionName = "1.7.30"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -98,42 +98,83 @@ class KtorLocalProxyServer @Inject constructor(
json() json()
} }
routing { 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") { get("/health") {
call.respond(mapOf("status" to "ok")) call.respond(mapOf("status" to "ok"))
} }
get("/v1/models") { get("/v1/models") {
if (!authorized(call, apiKey)) { call.respondModels(apiKey, sub2Api)
call.respond(HttpStatusCode.Unauthorized, errorBody("未授权")) }
return@get // 部分客户端 Base URL 不带 /v1 时兼容
get("/models") {
call.respondModels(apiKey, sub2Api)
}
post("/v1/chat/completions") {
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() val result = sub2Api.forwardModels()
if (result == null) { if (result == null) {
call.respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key")) respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key"))
} else { } else {
call.respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, ""))
} }
} }
post("/v1/chat/completions") {
if (!authorized(call, apiKey)) { private suspend fun ApplicationCall.respondChatCompletion(
call.respond(HttpStatusCode.Unauthorized, errorBody("未授权")) apiKey: String?,
return@post router: ProviderRouter,
) {
if (!authorized(this, apiKey)) {
respond(HttpStatusCode.Unauthorized, errorBody("未授权"))
return
} }
val rawBody = call.receiveText() val rawBody = receiveText()
if (rawBody.length > MAX_REQUEST_BYTES) { if (rawBody.length > MAX_REQUEST_BYTES) {
call.respond(HttpStatusCode(413, ""), errorBody("请求体过大")) respond(HttpStatusCode(413, ""), errorBody("请求体过大"))
return@post return
} }
val conversationId = call.request.headers["X-Conversation-Id"] ?: extractUser(rawBody) val conversationId = request.headers["X-Conversation-Id"] ?: extractUser(rawBody)
try { try {
if (extractStream(rawBody)) { if (extractStream(rawBody)) {
val stream = router.openStreamingChat(rawBody, conversationId) val stream = router.openStreamingChat(rawBody, conversationId)
if (stream == null) { if (stream == null) {
call.respond( respond(
HttpStatusCode.BadRequest, HttpStatusCode.BadRequest,
errorBody("所有上游均不可用或未配置,请检查设置") errorBody("所有上游均不可用或未配置,请检查设置")
) )
} else { } else {
call.respondOutputStream( respondOutputStream(
contentType = contentTypeOf(stream.contentType), contentType = contentTypeOf(stream.contentType),
status = HttpStatusCode(stream.status, "") status = HttpStatusCode(stream.status, "")
) { ) {
@ -154,31 +195,29 @@ class KtorLocalProxyServer @Inject constructor(
} else { } else {
val result = router.forwardChat(rawBody, conversationId) val result = router.forwardChat(rawBody, conversationId)
if (result == null) { if (result == null) {
call.respond( respond(
HttpStatusCode.BadRequest, HttpStatusCode.BadRequest,
errorBody("所有上游均不可用或未配置,请检查设置") errorBody("所有上游均不可用或未配置,请检查设置")
) )
} else { } else {
call.respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, "")) respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, ""))
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
val detail = e.message ?: "未知错误" val detail = e.message ?: "未知错误"
call.respond( respond(
HttpStatusCode.BadGateway, HttpStatusCode.BadGateway,
errorBody("上游网络错误:$detail") errorBody("上游网络错误:$detail")
) )
} catch (e: Exception) { } catch (e: Exception) {
if (e is kotlinx.coroutines.CancellationException) throw e if (e is kotlinx.coroutines.CancellationException) throw e
val detail = e.message ?: "未知错误" val detail = e.message ?: "未知错误"
call.respond( respond(
HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError,
errorBody("网关内部错误:$detail") errorBody("网关内部错误:$detail")
) )
} }
} }
}
}
private fun authorized(call: ApplicationCall, apiKey: String?): Boolean { private fun authorized(call: ApplicationCall, apiKey: String?): Boolean {
if (apiKey.isNullOrBlank()) return true if (apiKey.isNullOrBlank()) return true