This commit is contained in:
2026-07-29 16:21:59 +03:00
parent b83a40f4bd
commit 6485f4ec26
41 changed files with 797 additions and 224 deletions
@@ -1,31 +1,40 @@
package ru.shadowsparky.chat.backend.data
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Single
import ru.shadowsparky.chat.backend.domain.ChatService
import ru.shadowsparky.chat.domain.Message
import ru.shadowsparky.http.domain.HttpClientFactory
import java.util.UUID
@Factory
@Single
class GigaChatService(
private val tokenManager: GigaChatTokenManager,
private val json: Json
private val httpClientFactory: HttpClientFactory
) : ChatService {
private val httpClient by lazy { createGigaChatHttpClient() }
private val httpClient by lazy {
httpClientFactory.create(
BearerAuthProvider(
refreshTokens = {
val newToken = tokenManager.forceRefreshToken()
BearerTokens(newToken, null)
},
loadTokens = {
BearerTokens(tokenManager.getToken(), null)
},
realm = null
)
)
}
override suspend fun getCompletion(userId: Long, messages: List<Message>): String {
val clientId = "vbox-$userId"
@@ -44,21 +53,6 @@ class GigaChatService(
?: error("unexpected response $response")
}
private fun createGigaChatHttpClient(): HttpClient {
return HttpClient {
install(ContentNegotiation) { json(json) }
install(Auth) {
bearer {
loadTokens { BearerTokens(tokenManager.getToken(), "") }
refreshTokens {
val newToken = tokenManager.forceRefreshToken()
BearerTokens(newToken, "")
}
}
}
}
}
@Serializable
private data class GigaChatRequest(
val model: String = "GigaChat-2",
@@ -1,13 +1,13 @@
package ru.shadowsparky.chat.backend.data
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.Parameters
import io.ktor.http.contentType
import kotlinx.coroutines.sync.Mutex
@@ -17,16 +17,35 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.koin.core.annotation.Single
import ru.shadowsparky.backend.data.EnvFetcher
import ru.shadowsparky.http.data.RefreshTokenRunner
import ru.shadowsparky.http.domain.BadRequestException
import ru.shadowsparky.http.domain.HttpClientFactory
import java.util.UUID
@Single
class GigaChatTokenManager(
private val authClient: HttpClient,
private val json: Json,
private val envFetcher: EnvFetcher
private val envFetcher: EnvFetcher,
private val httpClientFactory: HttpClientFactory,
private val refreshTokenRunner: RefreshTokenRunner
) {
private val scope: String = "GIGACHAT_API_PERS"
private val authClient by lazy {
httpClientFactory.create(
BearerAuthProvider(
refreshTokens = { null },
loadTokens = {
BearerTokens(
envFetcher.get("GIGA_CHAT_AUTH_TOKEN").ifEmpty { error("auth token not provided") },
null
)
},
realm = null
)
)
}
private val mutex = Mutex()
private var cachedToken: String? = null
@@ -34,14 +53,14 @@ class GigaChatTokenManager(
cachedToken ?: fetchNewToken()
}
suspend fun forceRefreshToken(): String = mutex.withLock {
fetchNewToken()
suspend fun forceRefreshToken(): String {
return refreshTokenRunner.runOrJoin {
BearerTokens(fetchNewToken(), null)
}?.accessToken ?: throw BadRequestException("Unable to refresh access token")
}
private suspend fun fetchNewToken(): String {
val auth = envFetcher.get("GIGA_CHAT_AUTH_TOKEN").ifEmpty { error("auth token not provided") }
val responseString: String = authClient.post("https://ngw.devices.sberbank.ru:9443/api/v2/oauth") {
header(HttpHeaders.Authorization, auth)
header("RqUID", UUID.randomUUID().toString())
contentType(ContentType.Application.FormUrlEncoded)
setBody(FormDataContent(Parameters.build {
@@ -3,6 +3,6 @@ package ru.shadowsparky.chat.backend.domain
import ru.shadowsparky.chat.domain.Message
interface MessageStorage {
suspend fun query(userId: Long, limit: Int): List<Message>
suspend fun query(userId: Long, afterId: Long?, limit: Int): List<Message>
suspend fun insert(userId: Long, message: Message): Long
}
@@ -10,7 +10,7 @@ class ProcessUserMessageUseCase(
private val chatService: ChatService
) {
suspend fun execute(userId: Long): Message {
val history = storage.query(userId, limit = 10).toList()
val history = storage.query(userId, null, limit = 10).toList()
val sortedHistory = history.sortedBy { it.timestamp }
val systemMessage = Message(
role = ChatRoles.SYSTEM,