From 09c50bc320d5ca1d5f721d12e9f0dca406791ac7 Mon Sep 17 00:00:00 2001 From: iammm0 Date: Fri, 23 Jan 2026 14:02:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E5=B1=82=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展ApiService添加新接口 - 优化AuthService认证服务 - 增强WebSocketClient连接和消息处理 - 更新AuthModels和ConversationModels数据模型 --- .../com/huaga/life_echo/network/ApiService.kt | 72 +++++++--- .../huaga/life_echo/network/AuthService.kt | 105 +++++++++++++- .../life_echo/network/WebSocketClient.kt | 133 +++++++++++++++--- .../life_echo/network/models/AuthModels.kt | 17 ++- .../network/models/ConversationModels.kt | 9 ++ 5 files changed, 293 insertions(+), 43 deletions(-) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt index a2a5b0b..8fd4f74 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt @@ -1,7 +1,6 @@ package com.huaga.life_echo.network 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.models.* import io.ktor.client.* @@ -44,6 +43,17 @@ class ApiService( // ==================== 对话相关API ==================== + suspend fun createConversation(): Result { + 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> { return try { val response = client.get("$BASE_URL/api/conversations") { @@ -51,8 +61,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - // 接口失败时返回Mock数据 - Result.success(MockDataProvider.getMockConversations()) + Result.failure(e) } } @@ -63,7 +72,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockConversationDetail(id)) + Result.failure(e) } } @@ -74,7 +83,33 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockMessages(conversationId)) + Result.failure(e) + } + } + + suspend fun deleteConversation(conversationId: String): Result { + 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 { + 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()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockBookInfo()) + Result.failure(e) } } @@ -98,7 +133,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockChapters()) + Result.failure(e) } } @@ -109,8 +144,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - val chapters = MockDataProvider.getMockChapters() - Result.success(chapters.find { it.id == id } ?: chapters[0]) + Result.failure(e) } } @@ -121,7 +155,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockChapterContent(id)) + Result.failure(e) } } @@ -133,8 +167,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - // 更新失败时返回Mock数据 - Result.success(MockDataProvider.getMockBookInfo()) + Result.failure(e) } } @@ -159,7 +192,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockPlans()) + Result.failure(e) } } @@ -170,7 +203,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockCurrentPlan()) + Result.failure(e) } } @@ -181,7 +214,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockQuotaCheck()) + Result.failure(e) } } @@ -204,7 +237,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockOrders()) + Result.failure(e) } } @@ -215,8 +248,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - val orders = MockDataProvider.getMockOrders() - Result.success(orders.find { it.id == orderId } ?: orders[0]) + Result.failure(e) } } @@ -229,7 +261,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockUserProfile()) + Result.failure(e) } } @@ -240,7 +272,7 @@ class ApiService( } Result.success(response.body()) } catch (e: Exception) { - Result.success(MockDataProvider.getMockFAQs()) + Result.failure(e) } } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt index 3b235a7..7be0251 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt @@ -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.TokenResponse 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.call.body import io.ktor.client.engine.android.Android import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.forms.* import io.ktor.client.request.get 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.HttpStatusCode import io.ktor.http.contentType +import io.ktor.http.Headers +import io.ktor.http.append import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import java.io.File /** * 认证服务 @@ -66,8 +72,29 @@ class AuthService { Result.success(tokenResponse) } HttpStatusCode.BadRequest -> { - val error = response.body() - Result.failure(Exception(error.detail)) + // 尝试解析简单错误响应 + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("注册失败: ${response.status}")) + } + } + HttpStatusCode.UnprocessableEntity -> { + // FastAPI验证错误(422)- 尝试解析验证错误详情 + try { + val validationError = response.body() + val errorMessages = validationError.detail.joinToString(", ") { it.msg } + Result.failure(Exception("数据验证失败: $errorMessages")) + } catch (e: Exception) { + // 如果无法解析为验证错误,尝试解析为简单错误 + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e2: Exception) { + Result.failure(Exception("请求格式错误: ${response.status}")) + } + } } else -> { Result.failure(Exception("注册失败: ${response.status}")) @@ -97,8 +124,29 @@ class AuthService { Result.failure(Exception("手机号或密码错误")) } HttpStatusCode.BadRequest -> { - val error = response.body() - Result.failure(Exception(error.detail)) + // 尝试解析简单错误响应 + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e: Exception) { + Result.failure(Exception("请求错误: ${response.status}")) + } + } + HttpStatusCode.UnprocessableEntity -> { + // FastAPI验证错误(422)- 尝试解析验证错误详情 + try { + val validationError = response.body() + val errorMessages = validationError.detail.joinToString(", ") { it.msg } + Result.failure(Exception("数据验证失败: $errorMessages")) + } catch (e: Exception) { + // 如果无法解析为验证错误,尝试解析为简单错误 + try { + val error = response.body() + Result.failure(Exception(error.detail)) + } catch (e2: Exception) { + Result.failure(Exception("请求格式错误: ${response.status}")) + } + } } else -> { Result.failure(Exception("登录失败: ${response.status}")) @@ -188,4 +236,53 @@ class AuthService { Result.failure(Exception("网络错误: ${e.message}", e)) } } + + /** + * 上传用户头像 + */ + suspend fun uploadAvatar(accessToken: String, imageFile: File): Result { + 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() + Result.success(userResponse) + } + HttpStatusCode.BadRequest -> { + try { + val error = response.body() + 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)) + } + } } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt index d0e1a4a..5c74473 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt @@ -1,7 +1,7 @@ package com.huaga.life_echo.network 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.contentnegotiation.* import io.ktor.client.plugins.logging.* @@ -15,9 +15,10 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject +import android.util.Log class WebSocketClient { - private val client = HttpClient(Android) { + private val client = HttpClient(OkHttp) { install(WebSockets) install(ContentNegotiation) { json(Json { @@ -39,8 +40,10 @@ class WebSocketClient { private var currentToken: String? = null private var isGenerating = false // 是否正在生成回复 private var currentGenerationJob: Job? = null // 当前生成任务 + private var onErrorCallback: ((String) -> Unit)? = null // 错误回调 companion object { + private const val TAG = "WebSocketClient" private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL private const val RECONNECT_DELAY_MS = 3000L private const val MAX_RECONNECT_ATTEMPTS = 5 @@ -49,8 +52,14 @@ class WebSocketClient { suspend fun connect( conversationId: String, token: String? = null, - onMessage: (WebSocketMessage) -> Unit + onMessage: (WebSocketMessage) -> Unit, + onError: ((String) -> Unit)? = null ) { + // 如果已连接,先断开 + if (isConnected) { + disconnect() + } + val baseUrl = "$BASE_URL/ws/conversation/$conversationId" val url = if (token != null) { "$baseUrl?token=$token" @@ -58,61 +67,141 @@ class WebSocketClient { baseUrl } + onErrorCallback = onError + + Log.d(TAG, "开始连接WebSocket: $url") + try { + // 建立WebSocket连接 session = client.webSocketSession { url { takeFrom(url) } } + Log.d(TAG, "WebSocket session创建成功") + currentConversationId = conversationId currentToken = token - isConnected = true - // 启动消息接收协程 - scope.launch { + // 启动消息接收协程(在设置isConnected之前) + val receiveJob = scope.launch { receiveMessages(onMessage) } - // 发送连接消息 - sendMessage(WebSocketMessage( - type = MessageType.connect, - conversation_id = conversationId, - data = buildJsonObject { put("status", JsonPrimitive("connected")) } - )) + // 等待一小段时间确保连接建立 + delay(100) + + // 检查session是否有效 + 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) { + Log.e(TAG, "WebSocket连接异常: ${e.message}", e) isConnected = false + session?.close() + session = null + onErrorCallback?.invoke("连接失败: ${e.message}") throw e } } private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) { try { - while (isConnected) { + // 持续接收消息,直到连接关闭 + while (true) { val frame = session?.incoming?.receive() as? Frame.Text ?: break - val message = Json.decodeFromString(frame.readText()) - onMessage(message) - messageFlow.emit(message) + try { + val text = frame.readText() + Log.d(TAG, "收到WebSocket消息: $text") + + val message = Json.decodeFromString(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 + val errorMsg = "连接异常: ${e.message}" + onErrorCallback?.invoke(errorMsg) + // 触发重连 reconnectJob?.cancel() reconnectJob = scope.launch { + Log.d(TAG, "开始重连...") reconnectWithBackoff(onMessage) } } } 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 { val json = Json.encodeToString(WebSocketMessage.serializer(), message) + Log.d(TAG, "发送消息: ${message.type}, 内容: $json") session?.send(Frame.Text(json)) + Log.d(TAG, "消息发送成功: ${message.type}") } catch (e: Exception) { - // 处理发送失败 + // 发送失败,可能连接已断开 + Log.e(TAG, "发送消息失败: ${e.message}", e) + isConnected = false + onErrorCallback?.invoke("发送消息失败: ${e.message}") throw e } } @@ -127,6 +216,12 @@ class WebSocketClient { } suspend fun sendTextMessage(text: String, conversationId: String) { + Log.d(TAG, "准备发送文本消息: $text") + if (!isConnected) { + Log.w(TAG, "WebSocket未连接,无法发送文本消息") + throw Exception("WebSocket未连接,请先建立连接") + } + sendMessage(WebSocketMessage( type = MessageType.text, conversation_id = conversationId, @@ -168,6 +263,7 @@ class WebSocketClient { } suspend fun disconnect() { + Log.d(TAG, "断开WebSocket连接") isConnected = false isGenerating = false reconnectJob?.cancel() @@ -176,6 +272,7 @@ class WebSocketClient { session = null currentConversationId = null currentToken = null + Log.d(TAG, "WebSocket连接已断开") } private suspend fun reconnectWithBackoff( diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt index 67a3120..7db3114 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/AuthModels.kt @@ -48,8 +48,23 @@ data class UserResponse( val created_at: String ) -// 错误响应 +// 错误响应 - 支持简单字符串格式 @Serializable data class ErrorResponse( val detail: String ) + +// FastAPI验证错误详情 +@Serializable +data class ValidationErrorDetail( + val type: String, + val loc: List, + val msg: String, + val input: String? = null +) + +// FastAPI验证错误响应(422) +@Serializable +data class ValidationErrorResponse( + val detail: List +) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/ConversationModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/ConversationModels.kt index c081611..410a09d 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/models/ConversationModels.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/ConversationModels.kt @@ -29,6 +29,15 @@ data class MessageDto( 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 @Serializable data class ConversationDetailDto(