feat: 扩展前端网络层服务
- 扩展ApiService添加新接口 - 优化AuthService认证服务 - 增强WebSocketClient连接和消息处理 - 更新AuthModels和ConversationModels数据模型
This commit is contained in:
@@ -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<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>> {
|
||||
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<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())
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 -> {
|
||||
Result.failure(Exception("注册失败: ${response.status}"))
|
||||
@@ -97,8 +124,29 @@ class AuthService {
|
||||
Result.failure(Exception("手机号或密码错误"))
|
||||
}
|
||||
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 -> {
|
||||
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<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
|
||||
|
||||
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<WebSocketMessage>(frame.readText())
|
||||
onMessage(message)
|
||||
messageFlow.emit(message)
|
||||
try {
|
||||
val text = frame.readText()
|
||||
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
|
||||
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(
|
||||
|
||||
@@ -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<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"
|
||||
)
|
||||
|
||||
// 创建对话响应DTO
|
||||
@Serializable
|
||||
data class CreateConversationResponse(
|
||||
val id: String,
|
||||
val user_id: String,
|
||||
val started_at: String,
|
||||
val status: String
|
||||
)
|
||||
|
||||
// 对话详情DTO
|
||||
@Serializable
|
||||
data class ConversationDetailDto(
|
||||
|
||||
Reference in New Issue
Block a user