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" 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,85 +98,124 @@ 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") {
val result = sub2Api.forwardModels() call.respondModels(apiKey, sub2Api)
if (result == null) {
call.respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key"))
} else {
call.respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, ""))
}
} }
post("/v1/chat/completions") { post("/v1/chat/completions") {
if (!authorized(call, apiKey)) { call.respondChatCompletion(apiKey, router)
call.respond(HttpStatusCode.Unauthorized, errorBody("未授权")) }
return@post post("/chat/completions") {
} call.respondChatCompletion(apiKey, router)
val rawBody = call.receiveText() }
if (rawBody.length > MAX_REQUEST_BYTES) { }
call.respond(HttpStatusCode(413, ""), errorBody("请求体过大")) }
return@post
} private suspend fun ApplicationCall.respondModels(
val conversationId = call.request.headers["X-Conversation-Id"] ?: extractUser(rawBody) apiKey: String?,
try { sub2Api: Sub2ApiChatProxy,
if (extractStream(rawBody)) { ) {
val stream = router.openStreamingChat(rawBody, conversationId) if (!authorized(this, apiKey)) {
if (stream == null) { respond(HttpStatusCode.Unauthorized, errorBody("未授权"))
call.respond( return
HttpStatusCode.BadRequest, }
errorBody("所有上游均不可用或未配置,请检查设置") val result = sub2Api.forwardModels()
) if (result == null) {
} else { respond(HttpStatusCode.BadRequest, errorBody("Sub2API 未配置或未登录,请在设置中填写 API Key"))
call.respondOutputStream( } else {
contentType = contentTypeOf(stream.contentType), respondBytes(result.body, contentTypeOf(result.contentType), HttpStatusCode(result.status, ""))
status = HttpStatusCode(stream.status, "") }
) { }
try {
val buffer = ByteArray(8192) private suspend fun ApplicationCall.respondChatCompletion(
val input = stream.input apiKey: String?,
while (true) { router: ProviderRouter,
val read = input.read(buffer) ) {
if (read < 0) break if (!authorized(this, apiKey)) {
write(buffer, 0, read) respond(HttpStatusCode.Unauthorized, errorBody("未授权"))
flush() return
} }
} finally { val rawBody = receiveText()
stream.close() 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()
} }
} } finally {
} else { stream.close()
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, ""))
} }
} }
} catch (e: IOException) { }
val detail = e.message ?: "未知错误" } else {
call.respond( val result = router.forwardChat(rawBody, conversationId)
HttpStatusCode.BadGateway, if (result == null) {
errorBody("上游网络错误:$detail") respond(
) HttpStatusCode.BadRequest,
} catch (e: Exception) { errorBody("所有上游均不可用或未配置,请检查设置")
if (e is kotlinx.coroutines.CancellationException) throw e
val detail = e.message ?: "未知错误"
call.respond(
HttpStatusCode.InternalServerError,
errorBody("网关内部错误:$detail")
) )
} 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")
)
} }
} }