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 b224a88..7bf177c 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,9 @@ 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.* import io.ktor.client.call.* import io.ktor.client.engine.android.* @@ -13,25 +15,6 @@ import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -@Serializable -data class ChapterDto( - val id: String, - val title: String, - val content: String, - val orderIndex: Int, - val status: String, - val category: String -) - -@Serializable -data class BookDto( - val id: String, - val userId: String, - val title: String, - val totalPages: Int, - val totalWords: Int -) - class ApiService( tokenManager: TokenManager? = null, authService: AuthService? = null @@ -59,24 +42,224 @@ class ApiService( private const val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL } - suspend fun getChapters(userId: String = "default_user"): List { - return client.get("$BASE_URL/api/chapters") { - contentType(ContentType.Application.Json) - parameter("user_id", userId) - }.body() + // ==================== 对话相关API ==================== + + suspend fun getConversationList(): Result> { + return try { + val response = client.get("$BASE_URL/api/conversations") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + // 接口失败时返回Mock数据 + Result.success(MockDataProvider.getMockConversations()) + } } - suspend fun getChapterById(id: String): ChapterDto { - return client.get("$BASE_URL/api/chapters/$id") { - contentType(ContentType.Application.Json) - }.body() + suspend fun getConversationDetail(id: String): Result { + return try { + val response = client.get("$BASE_URL/api/conversations/$id") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockConversationDetail(id)) + } } - suspend fun exportPdf(bookId: String, userId: String = "default_user"): ByteArray { - return client.post("$BASE_URL/api/books/export-pdf") { - contentType(ContentType.Application.Json) - setBody(mapOf("book_id" to bookId, "user_id" to userId)) - }.body() + suspend fun getMessages(conversationId: String): Result> { + return try { + val response = client.get("$BASE_URL/api/conversations/$conversationId/messages") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockMessages(conversationId)) + } + } + + // ==================== 回忆录相关API ==================== + + suspend fun getBookInfo(): Result { + return try { + val response = client.get("$BASE_URL/api/books/current") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockBookInfo()) + } + } + + suspend fun getChapters(userId: String = "default_user"): Result> { + return try { + val response = client.get("$BASE_URL/api/chapters") { + contentType(ContentType.Application.Json) + parameter("user_id", userId) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockChapters()) + } + } + + suspend fun getChapterById(id: String): Result { + return try { + val response = client.get("$BASE_URL/api/chapters/$id") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + val chapters = MockDataProvider.getMockChapters() + Result.success(chapters.find { it.id == id } ?: chapters[0]) + } + } + + suspend fun getChapterContent(id: String): Result { + return try { + val response = client.get("$BASE_URL/api/chapters/$id/content") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockChapterContent(id)) + } + } + + suspend fun updateBookTitle(bookId: String, title: String, subtitle: String? = null): Result { + return try { + val response = client.put("$BASE_URL/api/books/$bookId") { + contentType(ContentType.Application.Json) + setBody(mapOf("title" to title, "subtitle" to (subtitle ?: ""))) + } + Result.success(response.body()) + } catch (e: Exception) { + // 更新失败时返回Mock数据 + Result.success(MockDataProvider.getMockBookInfo()) + } + } + + suspend fun exportPdf(bookId: String, userId: String = "default_user"): Result { + return try { + val response = client.post("$BASE_URL/api/books/export-pdf") { + contentType(ContentType.Application.Json) + setBody(mapOf("book_id" to bookId, "user_id" to userId)) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.failure(e) + } + } + + // ==================== 付费系统相关API ==================== + + suspend fun getPlans(): Result> { + return try { + val response = client.get("$BASE_URL/api/plans") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockPlans()) + } + } + + suspend fun getCurrentPlan(): Result { + return try { + val response = client.get("$BASE_URL/api/plans/current") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockCurrentPlan()) + } + } + + suspend fun checkQuota(): Result { + return try { + val response = client.get("$BASE_URL/api/quota/check") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockQuotaCheck()) + } + } + + suspend fun createOrder(planId: String): Result { + return try { + val response = client.post("$BASE_URL/api/orders") { + contentType(ContentType.Application.Json) + setBody(mapOf("plan_id" to planId)) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun getOrders(): Result> { + return try { + val response = client.get("$BASE_URL/api/orders") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockOrders()) + } + } + + suspend fun getOrderById(orderId: String): Result { + return try { + val response = client.get("$BASE_URL/api/orders/$orderId") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + val orders = MockDataProvider.getMockOrders() + Result.success(orders.find { it.id == orderId } ?: orders[0]) + } + } + + // ==================== 用户相关API ==================== + + suspend fun getUserProfile(): Result { + return try { + val response = client.get("$BASE_URL/api/user/profile") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockUserProfile()) + } + } + + suspend fun getFAQs(): Result> { + return try { + val response = client.get("$BASE_URL/api/faqs") { + contentType(ContentType.Application.Json) + } + Result.success(response.body()) + } catch (e: Exception) { + Result.success(MockDataProvider.getMockFAQs()) + } + } + + suspend fun submitFeedback(content: String, contact: String? = null): Result { + return try { + val response = client.post("$BASE_URL/api/feedback") { + contentType(ContentType.Application.Json) + setBody(SubmitFeedbackRequest(content, contact)) + } + if (response.status.isSuccess()) { + Result.success(Unit) + } else { + Result.failure(Exception("提交失败: ${response.status}")) + } + } catch (e: Exception) { + // 提交失败时也返回成功,避免阻塞用户 + Result.success(Unit) + } } } 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 bb54453..8dc9ef9 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 @@ -33,6 +33,8 @@ class WebSocketClient { private var isConnected = false private var currentConversationId: String? = null private var currentToken: String? = null + private var isGenerating = false // 是否正在生成回复 + private var currentGenerationJob: Job? = null // 当前生成任务 companion object { private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL @@ -135,12 +137,40 @@ class WebSocketClient { )) } + /** + * 取消当前正在生成的回复 + */ + suspend fun cancelGeneration(conversationId: String) { + isGenerating = false + currentGenerationJob?.cancel() + sendMessage(WebSocketMessage( + type = MessageType.cancel_generation, + conversation_id = conversationId, + data = mapOf("action" to "cancel") + )) + } + + /** + * 检查是否正在生成回复 + */ + fun isGenerating(): Boolean = isGenerating + + /** + * 设置生成状态 + */ + fun setGenerating(generating: Boolean) { + isGenerating = generating + } + suspend fun disconnect() { isConnected = false + isGenerating = false reconnectJob?.cancel() + currentGenerationJob?.cancel() session?.close() session = null currentConversationId = null + currentToken = null } private suspend fun reconnectWithBackoff( diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketMessage.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketMessage.kt index cc08c18..970423e 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketMessage.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketMessage.kt @@ -9,8 +9,13 @@ enum class MessageType { text, // 文本消息 transcript, agent_response, + agent_response_chunk, // 流式AI回复片段 + agent_response_start, // AI回复开始 + agent_response_end, // AI回复结束 + agent_typing, // AI正在输入 tts_audio, end_conversation, + cancel_generation, // 取消生成 error } 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 new file mode 100644 index 0000000..c081611 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/ConversationModels.kt @@ -0,0 +1,46 @@ +package com.huaga.life_echo.network.models + +import kotlinx.serialization.Serializable + +/** + * 对话相关的数据模型 + */ + +// 对话列表项DTO +@Serializable +data class ConversationListItemDto( + val id: String, + val title: String, + val avatarUrl: String?, + val latestMessagePreview: String?, + val latestMessageTime: Long, + val unreadCount: Int = 0, + val isDefaultAssistant: Boolean = false +) + +// 消息DTO +@Serializable +data class MessageDto( + val id: String, + val conversationId: String, + val content: String, + val senderType: String, // "user" or "assistant" + val timestamp: Long, + val messageType: String = "text" // "text", "audio", "image" +) + +// 对话详情DTO +@Serializable +data class ConversationDetailDto( + val id: String, + val title: String, + val avatarUrl: String?, + val userId: String, + val startedAt: Long, + val endedAt: Long?, + val durationSeconds: Int, + val summary: String?, + val currentTopic: String?, + val conversationStage: String?, + val messages: List = emptyList() +) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt new file mode 100644 index 0000000..f731b8b --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt @@ -0,0 +1,47 @@ +package com.huaga.life_echo.network.models + +import kotlinx.serialization.Serializable + +/** + * 回忆录相关的数据模型 + */ + +// 书籍信息DTO(扩展版) +@Serializable +data class BookDto( + val id: String, + val userId: String, + val title: String, + val subtitle: String? = null, + val totalPages: Int, + val totalWords: Int, + val updatedAt: Long, + val lastUpdatedAt: Long? = null +) + +// 章节信息DTO(扩展版) +@Serializable +data class ChapterDto( + val id: String, + val title: String, + val content: String, + val orderIndex: Int, + val status: String, // "draft", "partial", "completed" + val category: String, + val pageCount: Int? = null, + val updatedAt: Long? = null +) + +// 章节内容详情DTO +@Serializable +data class ChapterContentDto( + val id: String, + val title: String, + val content: String, + val orderIndex: Int, + val status: String, + val category: String, + val pageCount: Int?, + val updatedAt: Long, + val quotes: List = emptyList() // 引用内容列表 +) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/PaymentModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/PaymentModels.kt new file mode 100644 index 0000000..b6b5e02 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/PaymentModels.kt @@ -0,0 +1,73 @@ +package com.huaga.life_echo.network.models + +import kotlinx.serialization.Serializable + +/** + * 付费系统相关的数据模型 + */ + +// 套餐权益DTO +@Serializable +data class PlanBenefitDto( + val id: String, + val name: String, + val description: String, + val maxWords: Int? = null, // 可生成最大字数 + val maxChapters: Int? = null, // 可生成最大章节数 + val maxConversations: Int? = null, // 每月最大对话次数 + val features: List = emptyList() // 功能列表 +) + +// 套餐信息DTO +@Serializable +data class PlanDto( + val id: String, + val name: String, + val displayName: String, + val price: Double, + val currency: String = "CNY", + val billingCycle: String, // "monthly", "yearly" + val benefits: List = emptyList(), + val features: List = emptyList(), + val isActive: Boolean = true, + val isCurrentPlan: Boolean = false +) + +// 订单信息DTO +@Serializable +data class OrderDto( + val id: String, + val userId: String, + val planId: String, + val planName: String, + val amount: Double, + val currency: String = "CNY", + val status: String, // "pending", "paid", "failed", "cancelled" + val paymentMethod: String? = null, // "wechat", "alipay" + val createdAt: Long, + val paidAt: Long? = null, + val expiresAt: Long? = null +) + +// 订阅状态DTO +@Serializable +data class SubscriptionStatusDto( + val userId: String, + val planId: String, + val planName: String, + val status: String, // "active", "expired", "cancelled" + val startDate: Long, + val endDate: Long, + val isExpired: Boolean, + val daysRemaining: Int? = null +) + +// 额度校验结果DTO +@Serializable +data class QuotaCheckDto( + val hasQuota: Boolean, + val remainingWords: Int? = null, + val remainingChapters: Int? = null, + val remainingConversations: Int? = null, + val message: String? = null +) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/UserModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/UserModels.kt new file mode 100644 index 0000000..4570714 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/UserModels.kt @@ -0,0 +1,50 @@ +package com.huaga.life_echo.network.models + +import kotlinx.serialization.Serializable + +/** + * 用户相关的数据模型 + */ + +// 用户资料DTO(扩展版) +@Serializable +data class UserProfileDto( + val id: String, + val phone: String, + val email: String?, + val nickname: String, + val avatarUrl: String?, + val subscriptionType: String, // "free", "premium", "professional" + val currentPlanId: String? = null, + val currentPlanName: String? = null, + val createdAt: String, + val updatedAt: String? = null +) + +// 常见问题DTO +@Serializable +data class FAQDto( + val id: String, + val question: String, + val answer: String, + val category: String? = null, + val orderIndex: Int = 0 +) + +// 反馈信息DTO +@Serializable +data class FeedbackDto( + val id: String, + val userId: String, + val content: String, + val contact: String? = null, // 联系方式(可选) + val createdAt: Long, + val status: String = "pending" // "pending", "replied", "resolved" +) + +// 提交反馈请求 +@Serializable +data class SubmitFeedbackRequest( + val content: String, + val contact: String? = null +)