feat: 扩展前端网络层服务
- 扩展ApiService添加新接口 - 优化AuthService认证服务 - 增强WebSocketClient连接和消息处理 - 更新AuthModels和ConversationModels数据模型
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package com.huaga.life_echo.network
|
package com.huaga.life_echo.network
|
||||||
|
|
||||||
import com.huaga.life_echo.data.auth.TokenManager
|
import com.huaga.life_echo.data.auth.TokenManager
|
||||||
import com.huaga.life_echo.data.mock.MockDataProvider
|
|
||||||
import com.huaga.life_echo.network.interceptors.AuthInterceptorPlugin
|
import com.huaga.life_echo.network.interceptors.AuthInterceptorPlugin
|
||||||
import com.huaga.life_echo.network.models.*
|
import com.huaga.life_echo.network.models.*
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
@@ -44,6 +43,17 @@ class ApiService(
|
|||||||
|
|
||||||
// ==================== 对话相关API ====================
|
// ==================== 对话相关API ====================
|
||||||
|
|
||||||
|
suspend fun createConversation(): Result<CreateConversationResponse> {
|
||||||
|
return try {
|
||||||
|
val response = client.post("$BASE_URL/api/conversations") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
Result.success(response.body())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
|
suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
|
||||||
return try {
|
return try {
|
||||||
val response = client.get("$BASE_URL/api/conversations") {
|
val response = client.get("$BASE_URL/api/conversations") {
|
||||||
@@ -51,8 +61,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 接口失败时返回Mock数据
|
Result.failure(e)
|
||||||
Result.success(MockDataProvider.getMockConversations())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +72,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockConversationDetail(id))
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +83,33 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockMessages(conversationId))
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteConversation(conversationId: String): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
val response = client.delete("$BASE_URL/api/conversations/$conversationId") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun organizeConversation(conversationId: String): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
val response = client.post("$BASE_URL/api/conversations/$conversationId/organize") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("整理失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +122,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockBookInfo())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +133,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockChapters())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +144,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val chapters = MockDataProvider.getMockChapters()
|
Result.failure(e)
|
||||||
Result.success(chapters.find { it.id == id } ?: chapters[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +155,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockChapterContent(id))
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +167,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 更新失败时返回Mock数据
|
Result.failure(e)
|
||||||
Result.success(MockDataProvider.getMockBookInfo())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +192,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockPlans())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +203,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockCurrentPlan())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +214,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockQuotaCheck())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +237,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockOrders())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,8 +248,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val orders = MockDataProvider.getMockOrders()
|
Result.failure(e)
|
||||||
Result.success(orders.find { it.id == orderId } ?: orders[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +261,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockUserProfile())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +272,7 @@ class ApiService(
|
|||||||
}
|
}
|
||||||
Result.success(response.body())
|
Result.success(response.body())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.success(MockDataProvider.getMockFAQs())
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,27 @@ import com.huaga.life_echo.network.models.RefreshTokenRequest
|
|||||||
import com.huaga.life_echo.network.models.RegisterRequest
|
import com.huaga.life_echo.network.models.RegisterRequest
|
||||||
import com.huaga.life_echo.network.models.TokenResponse
|
import com.huaga.life_echo.network.models.TokenResponse
|
||||||
import com.huaga.life_echo.network.models.UserResponse
|
import com.huaga.life_echo.network.models.UserResponse
|
||||||
|
import com.huaga.life_echo.network.models.ValidationErrorResponse
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.android.Android
|
import io.ktor.client.engine.android.Android
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.plugins.logging.LogLevel
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.client.plugins.logging.Logging
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
|
import io.ktor.http.Headers
|
||||||
|
import io.ktor.http.append
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证服务
|
* 认证服务
|
||||||
@@ -66,8 +72,29 @@ class AuthService {
|
|||||||
Result.success(tokenResponse)
|
Result.success(tokenResponse)
|
||||||
}
|
}
|
||||||
HttpStatusCode.BadRequest -> {
|
HttpStatusCode.BadRequest -> {
|
||||||
val error = response.body<ErrorResponse>()
|
// 尝试解析简单错误响应
|
||||||
Result.failure(Exception(error.detail))
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("注册失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpStatusCode.UnprocessableEntity -> {
|
||||||
|
// FastAPI验证错误(422)- 尝试解析验证错误详情
|
||||||
|
try {
|
||||||
|
val validationError = response.body<ValidationErrorResponse>()
|
||||||
|
val errorMessages = validationError.detail.joinToString(", ") { it.msg }
|
||||||
|
Result.failure(Exception("数据验证失败: $errorMessages"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果无法解析为验证错误,尝试解析为简单错误
|
||||||
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
Result.failure(Exception("请求格式错误: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Result.failure(Exception("注册失败: ${response.status}"))
|
Result.failure(Exception("注册失败: ${response.status}"))
|
||||||
@@ -97,8 +124,29 @@ class AuthService {
|
|||||||
Result.failure(Exception("手机号或密码错误"))
|
Result.failure(Exception("手机号或密码错误"))
|
||||||
}
|
}
|
||||||
HttpStatusCode.BadRequest -> {
|
HttpStatusCode.BadRequest -> {
|
||||||
val error = response.body<ErrorResponse>()
|
// 尝试解析简单错误响应
|
||||||
Result.failure(Exception(error.detail))
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("请求错误: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpStatusCode.UnprocessableEntity -> {
|
||||||
|
// FastAPI验证错误(422)- 尝试解析验证错误详情
|
||||||
|
try {
|
||||||
|
val validationError = response.body<ValidationErrorResponse>()
|
||||||
|
val errorMessages = validationError.detail.joinToString(", ") { it.msg }
|
||||||
|
Result.failure(Exception("数据验证失败: $errorMessages"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果无法解析为验证错误,尝试解析为简单错误
|
||||||
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e2: Exception) {
|
||||||
|
Result.failure(Exception("请求格式错误: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Result.failure(Exception("登录失败: ${response.status}"))
|
Result.failure(Exception("登录失败: ${response.status}"))
|
||||||
@@ -188,4 +236,53 @@ class AuthService {
|
|||||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传用户头像
|
||||||
|
*/
|
||||||
|
suspend fun uploadAvatar(accessToken: String, imageFile: File): Result<UserResponse> {
|
||||||
|
return try {
|
||||||
|
val response = client.post("$AUTH_BASE/me/avatar") {
|
||||||
|
header("Authorization", "Bearer $accessToken")
|
||||||
|
setBody(
|
||||||
|
MultiPartFormDataContent(
|
||||||
|
formData {
|
||||||
|
append(
|
||||||
|
key = "file",
|
||||||
|
headers = Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, "image/jpeg")
|
||||||
|
append(HttpHeaders.ContentDisposition, "form-data; name=\"file\"; filename=\"${imageFile.name}\"")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
imageFile.readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
val userResponse = response.body<UserResponse>()
|
||||||
|
Result.success(userResponse)
|
||||||
|
}
|
||||||
|
HttpStatusCode.BadRequest -> {
|
||||||
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("上传失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpStatusCode.Unauthorized -> {
|
||||||
|
Result.failure(Exception("未授权"))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Result.failure(Exception("上传头像失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.huaga.life_echo.network
|
package com.huaga.life_echo.network
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.android.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.plugins.websocket.*
|
import io.ktor.client.plugins.websocket.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.logging.*
|
||||||
@@ -15,9 +15,10 @@ import kotlinx.serialization.json.JsonPrimitive
|
|||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
class WebSocketClient {
|
class WebSocketClient {
|
||||||
private val client = HttpClient(Android) {
|
private val client = HttpClient(OkHttp) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
@@ -39,8 +40,10 @@ class WebSocketClient {
|
|||||||
private var currentToken: String? = null
|
private var currentToken: String? = null
|
||||||
private var isGenerating = false // 是否正在生成回复
|
private var isGenerating = false // 是否正在生成回复
|
||||||
private var currentGenerationJob: Job? = null // 当前生成任务
|
private var currentGenerationJob: Job? = null // 当前生成任务
|
||||||
|
private var onErrorCallback: ((String) -> Unit)? = null // 错误回调
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "WebSocketClient"
|
||||||
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
|
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
|
||||||
private const val RECONNECT_DELAY_MS = 3000L
|
private const val RECONNECT_DELAY_MS = 3000L
|
||||||
private const val MAX_RECONNECT_ATTEMPTS = 5
|
private const val MAX_RECONNECT_ATTEMPTS = 5
|
||||||
@@ -49,8 +52,14 @@ class WebSocketClient {
|
|||||||
suspend fun connect(
|
suspend fun connect(
|
||||||
conversationId: String,
|
conversationId: String,
|
||||||
token: String? = null,
|
token: String? = null,
|
||||||
onMessage: (WebSocketMessage) -> Unit
|
onMessage: (WebSocketMessage) -> Unit,
|
||||||
|
onError: ((String) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
// 如果已连接,先断开
|
||||||
|
if (isConnected) {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
val baseUrl = "$BASE_URL/ws/conversation/$conversationId"
|
val baseUrl = "$BASE_URL/ws/conversation/$conversationId"
|
||||||
val url = if (token != null) {
|
val url = if (token != null) {
|
||||||
"$baseUrl?token=$token"
|
"$baseUrl?token=$token"
|
||||||
@@ -58,61 +67,141 @@ class WebSocketClient {
|
|||||||
baseUrl
|
baseUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onErrorCallback = onError
|
||||||
|
|
||||||
|
Log.d(TAG, "开始连接WebSocket: $url")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 建立WebSocket连接
|
||||||
session = client.webSocketSession {
|
session = client.webSocketSession {
|
||||||
url {
|
url {
|
||||||
takeFrom(url)
|
takeFrom(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "WebSocket session创建成功")
|
||||||
|
|
||||||
currentConversationId = conversationId
|
currentConversationId = conversationId
|
||||||
currentToken = token
|
currentToken = token
|
||||||
isConnected = true
|
|
||||||
|
|
||||||
// 启动消息接收协程
|
// 启动消息接收协程(在设置isConnected之前)
|
||||||
scope.launch {
|
val receiveJob = scope.launch {
|
||||||
receiveMessages(onMessage)
|
receiveMessages(onMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送连接消息
|
// 等待一小段时间确保连接建立
|
||||||
sendMessage(WebSocketMessage(
|
delay(100)
|
||||||
type = MessageType.connect,
|
|
||||||
conversation_id = conversationId,
|
// 检查session是否有效
|
||||||
data = buildJsonObject { put("status", JsonPrimitive("connected")) }
|
if (session == null || session?.isActive != true) {
|
||||||
))
|
Log.e(TAG, "WebSocket连接失败:session无效")
|
||||||
|
throw Exception("WebSocket连接失败:session无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "WebSocket session有效,准备发送连接消息")
|
||||||
|
|
||||||
|
// 发送连接消息(不等待确认,由服务器返回connect消息时再设置isConnected)
|
||||||
|
try {
|
||||||
|
sendMessage(WebSocketMessage(
|
||||||
|
type = MessageType.connect,
|
||||||
|
conversation_id = conversationId,
|
||||||
|
data = buildJsonObject { put("status", JsonPrimitive("connected")) }
|
||||||
|
))
|
||||||
|
Log.d(TAG, "连接消息已发送,等待服务器确认")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 发送失败,连接可能有问题
|
||||||
|
Log.e(TAG, "发送连接消息失败: ${e.message}", e)
|
||||||
|
isConnected = false
|
||||||
|
receiveJob.cancel()
|
||||||
|
throw Exception("发送连接消息失败: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意:isConnected将在收到服务器的connect确认消息时设置为true
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "WebSocket连接异常: ${e.message}", e)
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
session?.close()
|
||||||
|
session = null
|
||||||
|
onErrorCallback?.invoke("连接失败: ${e.message}")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) {
|
private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) {
|
||||||
try {
|
try {
|
||||||
while (isConnected) {
|
// 持续接收消息,直到连接关闭
|
||||||
|
while (true) {
|
||||||
val frame = session?.incoming?.receive() as? Frame.Text
|
val frame = session?.incoming?.receive() as? Frame.Text
|
||||||
?: break
|
?: break
|
||||||
|
|
||||||
val message = Json.decodeFromString<WebSocketMessage>(frame.readText())
|
try {
|
||||||
onMessage(message)
|
val text = frame.readText()
|
||||||
messageFlow.emit(message)
|
Log.d(TAG, "收到WebSocket消息: $text")
|
||||||
|
|
||||||
|
val message = Json.decodeFromString<WebSocketMessage>(text)
|
||||||
|
|
||||||
|
// 如果收到connect消息,设置连接状态为已连接
|
||||||
|
if (message.type == MessageType.connect) {
|
||||||
|
isConnected = true
|
||||||
|
Log.d(TAG, "WebSocket连接已确认,状态设置为已连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果收到error消息,设置连接状态为未连接
|
||||||
|
if (message.type == MessageType.error) {
|
||||||
|
isConnected = false
|
||||||
|
val errorMsg = message.getString("message") ?: "未知错误"
|
||||||
|
Log.e(TAG, "收到错误消息: $errorMsg")
|
||||||
|
onErrorCallback?.invoke(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "处理消息类型: ${message.type}")
|
||||||
|
onMessage(message)
|
||||||
|
messageFlow.emit(message)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// JSON解析失败,记录错误但继续接收
|
||||||
|
Log.e(TAG, "消息解析失败: ${e.message}", e)
|
||||||
|
onErrorCallback?.invoke("消息解析失败: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// 连接异常
|
||||||
|
Log.e(TAG, "接收消息异常: ${e.message}", e)
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
val errorMsg = "连接异常: ${e.message}"
|
||||||
|
onErrorCallback?.invoke(errorMsg)
|
||||||
|
|
||||||
// 触发重连
|
// 触发重连
|
||||||
reconnectJob?.cancel()
|
reconnectJob?.cancel()
|
||||||
reconnectJob = scope.launch {
|
reconnectJob = scope.launch {
|
||||||
|
Log.d(TAG, "开始重连...")
|
||||||
reconnectWithBackoff(onMessage)
|
reconnectWithBackoff(onMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendMessage(message: WebSocketMessage) {
|
suspend fun sendMessage(message: WebSocketMessage) {
|
||||||
|
if (!isConnected && message.type != MessageType.connect) {
|
||||||
|
Log.w(TAG, "尝试发送消息但未连接: ${message.type}")
|
||||||
|
throw Exception("WebSocket未连接,无法发送消息")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session == null || session?.isActive != true) {
|
||||||
|
Log.w(TAG, "WebSocket会话无效")
|
||||||
|
isConnected = false
|
||||||
|
throw Exception("WebSocket会话无效")
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val json = Json.encodeToString(WebSocketMessage.serializer(), message)
|
val json = Json.encodeToString(WebSocketMessage.serializer(), message)
|
||||||
|
Log.d(TAG, "发送消息: ${message.type}, 内容: $json")
|
||||||
session?.send(Frame.Text(json))
|
session?.send(Frame.Text(json))
|
||||||
|
Log.d(TAG, "消息发送成功: ${message.type}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 处理发送失败
|
// 发送失败,可能连接已断开
|
||||||
|
Log.e(TAG, "发送消息失败: ${e.message}", e)
|
||||||
|
isConnected = false
|
||||||
|
onErrorCallback?.invoke("发送消息失败: ${e.message}")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +216,12 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendTextMessage(text: String, conversationId: String) {
|
suspend fun sendTextMessage(text: String, conversationId: String) {
|
||||||
|
Log.d(TAG, "准备发送文本消息: $text")
|
||||||
|
if (!isConnected) {
|
||||||
|
Log.w(TAG, "WebSocket未连接,无法发送文本消息")
|
||||||
|
throw Exception("WebSocket未连接,请先建立连接")
|
||||||
|
}
|
||||||
|
|
||||||
sendMessage(WebSocketMessage(
|
sendMessage(WebSocketMessage(
|
||||||
type = MessageType.text,
|
type = MessageType.text,
|
||||||
conversation_id = conversationId,
|
conversation_id = conversationId,
|
||||||
@@ -168,6 +263,7 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun disconnect() {
|
suspend fun disconnect() {
|
||||||
|
Log.d(TAG, "断开WebSocket连接")
|
||||||
isConnected = false
|
isConnected = false
|
||||||
isGenerating = false
|
isGenerating = false
|
||||||
reconnectJob?.cancel()
|
reconnectJob?.cancel()
|
||||||
@@ -176,6 +272,7 @@ class WebSocketClient {
|
|||||||
session = null
|
session = null
|
||||||
currentConversationId = null
|
currentConversationId = null
|
||||||
currentToken = null
|
currentToken = null
|
||||||
|
Log.d(TAG, "WebSocket连接已断开")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun reconnectWithBackoff(
|
private suspend fun reconnectWithBackoff(
|
||||||
|
|||||||
@@ -48,8 +48,23 @@ data class UserResponse(
|
|||||||
val created_at: String
|
val created_at: String
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误响应
|
// 错误响应 - 支持简单字符串格式
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ErrorResponse(
|
data class ErrorResponse(
|
||||||
val detail: String
|
val detail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FastAPI验证错误详情
|
||||||
|
@Serializable
|
||||||
|
data class ValidationErrorDetail(
|
||||||
|
val type: String,
|
||||||
|
val loc: List<String>,
|
||||||
|
val msg: String,
|
||||||
|
val input: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
// FastAPI验证错误响应(422)
|
||||||
|
@Serializable
|
||||||
|
data class ValidationErrorResponse(
|
||||||
|
val detail: List<ValidationErrorDetail>
|
||||||
|
)
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ data class MessageDto(
|
|||||||
val messageType: String = "text" // "text", "audio", "image"
|
val messageType: String = "text" // "text", "audio", "image"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 创建对话响应DTO
|
||||||
|
@Serializable
|
||||||
|
data class CreateConversationResponse(
|
||||||
|
val id: String,
|
||||||
|
val user_id: String,
|
||||||
|
val started_at: String,
|
||||||
|
val status: String
|
||||||
|
)
|
||||||
|
|
||||||
// 对话详情DTO
|
// 对话详情DTO
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ConversationDetailDto(
|
data class ConversationDetailDto(
|
||||||
|
|||||||
Reference in New Issue
Block a user