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"
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,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")
)
}
}