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

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

View File

@ -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 {

View File

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