feat: 扩展ViewModel功能

- 扩展AuthViewModel认证逻辑
- 扩展ConversationListViewModel对话列表逻辑
- 扩展CreateMemoryViewModel创建回忆功能
- 扩展MyMemoirViewModel回忆录功能
- 更新ViewModelFactory支持新功能
This commit is contained in:
iammm0
2026-01-23 14:02:53 +08:00
parent 2592143789
commit b0c9f3dc15
5 changed files with 413 additions and 13 deletions

View File

@@ -261,4 +261,34 @@ class AuthViewModel(private val context: Context) : ViewModel() {
fun clearOperationResult() {
_operationResult.value = null
}
/**
* 上传头像
*/
fun uploadAvatar(imageFile: java.io.File) {
viewModelScope.launch {
_isLoading.value = true
_errorMessage.value = null
val accessToken = TokenManager.getAccessToken()
if (accessToken.isNullOrBlank()) {
_errorMessage.value = "未登录"
_isLoading.value = false
return@launch
}
val result = authService.uploadAvatar(accessToken, imageFile)
result.fold(
onSuccess = { userResponse ->
_currentUser.value = userResponse
_successMessage.value = "头像上传成功"
_isLoading.value = false
},
onFailure = { exception ->
_errorMessage.value = exception.message ?: "上传失败"
_isLoading.value = false
}
)
}
}
}

View File

@@ -54,4 +54,60 @@ class ConversationListViewModel(
}
}
}
/**
* 创建新对话
*/
suspend fun createConversation(): Result<String> {
_isLoading.value = true
_error.value = null
return try {
val result = conversationRepository.createConversation()
result.fold(
onSuccess = { conversationId ->
_isLoading.value = false
// 刷新对话列表
refreshConversations()
Result.success(conversationId)
},
onFailure = { exception ->
_error.value = exception.message
_isLoading.value = false
Result.failure(exception)
}
)
} catch (e: Exception) {
_error.value = e.message
_isLoading.value = false
Result.failure(e)
}
}
/**
* 删除对话
*/
suspend fun deleteConversation(conversationId: String): Result<Unit> {
_isLoading.value = true
_error.value = null
return try {
val result = conversationRepository.deleteConversation(conversationId)
result.fold(
onSuccess = {
_isLoading.value = false
// 刷新对话列表
refreshConversations()
Result.success(Unit)
},
onFailure = { exception ->
_error.value = exception.message
_isLoading.value = false
Result.failure(exception)
}
)
} catch (e: Exception) {
_error.value = e.message
_isLoading.value = false
Result.failure(e)
}
}
}

View File

@@ -1,28 +1,37 @@
package com.huaga.life_echo.ui.viewmodel
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.huaga.life_echo.data.auth.TokenManager
import com.huaga.life_echo.data.repository.ConversationRepository
import com.huaga.life_echo.data.repository.ChapterRepository
import com.huaga.life_echo.data.repository.MessageRepository
import com.huaga.life_echo.network.WebSocketClient
import com.huaga.life_echo.network.WebSocketMessage
import com.huaga.life_echo.network.MessageType
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.AuthService
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.data.database.Chapter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
class CreateMemoryViewModel(
private val conversationRepository: ConversationRepository,
private val chapterRepository: ChapterRepository,
private val messageRepository: MessageRepository,
private val context: Context
) : ViewModel() {
companion object {
private const val TAG = "CreateMemoryViewModel"
}
private val webSocketClient = WebSocketClient()
private val apiService = ApiService(TokenManager, AuthService())
@@ -33,36 +42,137 @@ class CreateMemoryViewModel(
val conversationId = MutableStateFlow<String?>(null)
val userMessages = MutableStateFlow<List<String>>(emptyList()) // 用户发送的文本消息列表
// 历史消息
val historyMessages = MutableStateFlow<List<MessageDto>>(emptyList())
// 流式消息相关
val isStreaming = MutableStateFlow(false) // 是否正在流式接收
val streamingText = MutableStateFlow("") // 流式文本内容
val isTyping = MutableStateFlow(false) // AI是否正在输入
// WebSocket连接状态用于调试
val wsIsConnected = MutableStateFlow(false)
// 后台处理状态
val isProcessing = MutableStateFlow(false) // 是否正在处理回忆录
val processingStatus = MutableStateFlow("") // 处理状态文本
// 调试信息
val lastMessageType = MutableStateFlow<String?>(null) // 最后收到的消息类型
val lastMessageTime = MutableStateFlow<String?>(null) // 最后消息时间
val errorMessages = MutableStateFlow<List<String>>(emptyList()) // 错误消息列表
val messageCount = MutableStateFlow(0) // 消息计数
init {
TokenManager.initialize(context)
}
fun startConversation() {
/**
* 初始化对话如果提供了conversationId
*/
fun initializeConversation(convId: String?) {
if (convId == null || convId == "new") {
return
}
viewModelScope.launch {
val convId = java.util.UUID.randomUUID().toString()
conversationId.value = convId
isRecording.value = true
connectionStatus.value = "连接中..."
try {
// 清除旧的任务记录(与测试脚本一致)
// 加载历史消息
loadHistoryMessages(convId)
// 获取访问令牌并连接WebSocket
val token = TokenManager.getAccessToken()
Log.d(TAG, "初始化对话conversationId: $convId")
webSocketClient.connect(
convId,
token,
onMessage = { message ->
Log.d(TAG, "收到WebSocket消息: ${message.type}")
handleWebSocketMessage(message)
},
onError = { errorMsg ->
Log.e(TAG, "WebSocket错误: $errorMsg")
connectionStatus.value = "错误: $errorMsg"
// 添加到错误列表最多保留10条
errorMessages.value = (errorMessages.value + errorMsg).takeLast(10)
}
)
// 注意连接状态将在收到服务器的connect消息时更新
// 这里不立即设置为"已连接",等待服务器确认
} catch (e: Exception) {
Log.e(TAG, "初始化对话失败: ${e.message}", e)
connectionStatus.value = "连接失败: ${e.message}"
}
}
}
/**
* 加载历史消息
*/
private suspend fun loadHistoryMessages(convId: String) {
val result = apiService.getMessages(convId)
result.fold(
onSuccess = { messages ->
historyMessages.value = messages
// 同步到本地数据库
messageRepository.syncMessages(convId)
},
onFailure = { exception ->
// 如果加载失败,记录错误但不阻止连接
connectionStatus.value = "加载历史消息失败: ${exception.message}"
}
)
}
fun startConversation() {
viewModelScope.launch {
connectionStatus.value = "创建对话中..."
try {
// 先通过API创建对话
val createResult = apiService.createConversation()
val convId = createResult.fold(
onSuccess = { response -> response.id },
onFailure = { exception ->
connectionStatus.value = "创建对话失败: ${exception.message}"
isRecording.value = false
return@launch
}
)
conversationId.value = convId
isRecording.value = true
connectionStatus.value = "连接中..."
// 清除旧的任务记录
apiService.clearTasks()
// 获取访问令牌
// 获取访问令牌并连接WebSocket
val token = TokenManager.getAccessToken()
webSocketClient.connect(convId, token) { message ->
handleWebSocketMessage(message)
}
Log.d(TAG, "开始对话conversationId: $convId")
webSocketClient.connect(
convId,
token,
onMessage = { message ->
Log.d(TAG, "收到WebSocket消息: ${message.type}")
handleWebSocketMessage(message)
},
onError = { errorMsg ->
Log.e(TAG, "WebSocket错误: $errorMsg")
connectionStatus.value = "错误: $errorMsg"
isRecording.value = false
// 添加到错误列表最多保留10条
errorMessages.value = (errorMessages.value + errorMsg).takeLast(10)
}
)
// 注意连接状态将在收到服务器的connect消息时更新
} catch (e: Exception) {
Log.e(TAG, "开始对话失败: ${e.message}", e)
connectionStatus.value = "连接失败: ${e.message}"
isRecording.value = false
}
@@ -75,6 +185,7 @@ class CreateMemoryViewModel(
webSocketClient.sendEndConversation(id)
webSocketClient.disconnect()
connectionStatus.value = "已断开"
wsIsConnected.value = false
isRecording.value = false
}
}
@@ -91,27 +202,93 @@ class CreateMemoryViewModel(
fun sendTextMessage(text: String) {
if (text.isBlank()) return
Log.d(TAG, "准备发送文本消息: $text")
viewModelScope.launch {
// 确保已连接
if (conversationId.value == null) {
Log.d(TAG, "对话ID为空开始创建新对话")
startConversation()
// 等待连接建立
delay(500)
}
// 检查连接状态
if (!webSocketClient.isConnected()) {
Log.w(TAG, "WebSocket未连接尝试重连")
connectionStatus.value = "未连接,正在重连..."
conversationId.value?.let { id ->
val token = TokenManager.getAccessToken()
try {
webSocketClient.connect(
id,
token,
onMessage = { message ->
Log.d(TAG, "重连后收到消息: ${message.type}")
handleWebSocketMessage(message)
},
onError = { errorMsg ->
Log.e(TAG, "重连错误: $errorMsg")
connectionStatus.value = "错误: $errorMsg"
}
)
delay(500) // 等待连接建立
} catch (e: Exception) {
Log.e(TAG, "重连失败: ${e.message}", e)
connectionStatus.value = "重连失败: ${e.message}"
return@launch
}
} ?: run {
Log.e(TAG, "对话ID为空无法重连")
connectionStatus.value = "对话ID为空无法发送消息"
return@launch
}
}
conversationId.value?.let { id ->
Log.d(TAG, "发送消息到对话: $id")
// 添加到用户消息列表
userMessages.value = userMessages.value + text
// 添加到历史消息(临时,等待服务器确认)
val tempMessage = MessageDto(
id = "temp_user_${System.currentTimeMillis()}",
conversationId = id,
content = text,
senderType = "user",
timestamp = System.currentTimeMillis(),
messageType = "text"
)
historyMessages.value = historyMessages.value + tempMessage
// 发送文本消息
try {
// 立即显示加载动画
isTyping.value = true
webSocketClient.sendTextMessage(text, id)
Log.d(TAG, "消息发送成功")
} catch (e: Exception) {
// 发送失败时隐藏加载动画
isTyping.value = false
Log.e(TAG, "消息发送失败: ${e.message}", e)
connectionStatus.value = "发送失败: ${e.message}"
errorMessages.value = (errorMessages.value + "发送失败: ${e.message}").takeLast(10)
// 移除临时消息
historyMessages.value = historyMessages.value.filter { it.id != tempMessage.id }
}
} ?: run {
Log.e(TAG, "对话ID为空无法发送消息")
connectionStatus.value = "对话ID为空无法发送消息"
}
}
}
private fun handleWebSocketMessage(message: WebSocketMessage) {
// 更新调试信息
lastMessageType.value = message.type.name
lastMessageTime.value = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
messageCount.value = messageCount.value + 1
when (message.type) {
MessageType.transcript -> {
transcript.value = message.getString("text") ?: ""
@@ -122,25 +299,38 @@ class CreateMemoryViewModel(
val index = message.getInt("index") ?: 0
val total = message.getInt("total") ?: 1
// 如果是第一条消息,重置内容
// 收到第一条回复时,隐藏打字指示器
if (index == 0) {
isTyping.value = false
agentResponse.value = text
} else {
// 追加后续消息
agentResponse.value += "\n\n$text"
}
// 如果是最后一条消息,结束流式状态
// 如果是最后一条消息,结束流式状态并添加到历史消息
if (index >= total - 1) {
conversationId.value?.let { id ->
val aiMessage = MessageDto(
id = "ai_${System.currentTimeMillis()}",
conversationId = id,
content = agentResponse.value,
senderType = "assistant",
timestamp = System.currentTimeMillis(),
messageType = "text"
)
historyMessages.value = historyMessages.value + aiMessage
}
isStreaming.value = false
streamingText.value = ""
isTyping.value = false
webSocketClient.setGenerating(false)
}
}
MessageType.agent_response_start -> {
// 流式回复开始
isStreaming.value = true
isTyping.value = false
isTyping.value = false // 流式开始时隐藏打字指示器
streamingText.value = ""
webSocketClient.setGenerating(true)
}
@@ -148,10 +338,28 @@ class CreateMemoryViewModel(
// 流式回复片段
val chunk = message.getString("text") ?: ""
streamingText.value += chunk
// 收到第一个片段时,隐藏打字指示器
if (streamingText.value == chunk) {
isTyping.value = false
}
}
MessageType.agent_response_end -> {
// 流式回复结束
agentResponse.value = streamingText.value
// 添加到历史消息
conversationId.value?.let { id ->
val aiMessage = MessageDto(
id = "ai_${System.currentTimeMillis()}",
conversationId = id,
content = streamingText.value,
senderType = "assistant",
timestamp = System.currentTimeMillis(),
messageType = "text"
)
historyMessages.value = historyMessages.value + aiMessage
}
isStreaming.value = false
streamingText.value = ""
webSocketClient.setGenerating(false)
@@ -164,7 +372,9 @@ class CreateMemoryViewModel(
// 处理文本消息响应(如果需要)
}
MessageType.connect -> {
Log.d(TAG, "收到连接确认消息,设置状态为已连接")
connectionStatus.value = "已连接"
wsIsConnected.value = true
}
MessageType.end_conversation -> {
connectionStatus.value = "对话已结束"
@@ -175,8 +385,12 @@ class CreateMemoryViewModel(
handleConversationEnded()
}
MessageType.error -> {
connectionStatus.value = "错误: ${message.getString("message")}"
val errorMsg = message.getString("message") ?: "未知错误"
connectionStatus.value = "错误: $errorMsg"
errorMessages.value = (errorMessages.value + errorMsg).takeLast(10)
wsIsConnected.value = false
isStreaming.value = false
isTyping.value = false
webSocketClient.setGenerating(false)
}
else -> {}

View File

@@ -1,5 +1,7 @@
package com.huaga.life_echo.ui.viewmodel
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.huaga.life_echo.data.database.Chapter
@@ -7,6 +9,7 @@ import com.huaga.life_echo.data.repository.ChapterRepository
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.models.MemoirStateDto
import com.huaga.life_echo.network.models.TasksStatusDto
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -26,6 +29,11 @@ class MyMemoirViewModel(
val memoirState = MutableStateFlow<MemoirStateDto?>(null)
val tasksStatus = MutableStateFlow<TasksStatusDto?>(null)
// 整理对话进度
val isOrganizing = MutableStateFlow(false)
val organizingProgress = MutableStateFlow(0f) // 0.0 - 1.0
val organizingStatus = MutableStateFlow("") // 状态文本
fun selectChapter(chapter: Chapter) {
selectedChapter.value = chapter
}
@@ -53,6 +61,7 @@ class MyMemoirViewModel(
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun refreshChapters() {
viewModelScope.launch {
isLoading.value = true
@@ -163,5 +172,76 @@ class MyMemoirViewModel(
fun toggleFullTextReading() {
showFullTextReading.value = !showFullTextReading.value
}
/**
* 整理对话内容成章节
*/
fun organizeConversation(conversationId: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
viewModelScope.launch {
isOrganizing.value = true
organizingProgress.value = 0f
organizingStatus.value = "正在提交整理任务..."
error.value = null
try {
val result = apiService.organizeConversation(conversationId)
result.fold(
onSuccess = {
organizingProgress.value = 0.3f
organizingStatus.value = "任务已提交,正在处理..."
// 模拟进度更新实际应该通过任务状态API获取
var progress = 0.3f
val progressSteps = listOf(0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f)
val statusSteps = listOf(
"正在分析对话内容...",
"正在分类整理...",
"正在生成章节...",
"正在优化内容...",
"即将完成..."
)
progressSteps.forEachIndexed { index, step ->
delay(1000)
progress = step
organizingProgress.value = progress
if (index < statusSteps.size) {
organizingStatus.value = statusSteps[index]
}
}
// 等待处理完成
delay(2000)
organizingProgress.value = 1.0f
organizingStatus.value = "整理完成!"
delay(500)
isOrganizing.value = false
organizingProgress.value = 0f
organizingStatus.value = ""
// 刷新章节列表
refreshChapters()
onSuccess()
},
onFailure = { exception ->
isOrganizing.value = false
organizingProgress.value = 0f
organizingStatus.value = ""
val errorMsg = "整理失败: ${exception.message}"
error.value = errorMsg
onError(errorMsg)
}
)
} catch (e: Exception) {
isOrganizing.value = false
organizingProgress.value = 0f
organizingStatus.value = ""
val errorMsg = "整理失败: ${e.message}"
error.value = errorMsg
onError(errorMsg)
}
}
}
}

View File

@@ -3,13 +3,27 @@ package com.huaga.life_echo.ui.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.huaga.life_echo.data.auth.TokenManager
import com.huaga.life_echo.data.database.AppDatabase
import com.huaga.life_echo.data.repository.*
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.AuthService
class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
init {
// 初始化TokenManager
TokenManager.initialize(context)
}
private val database by lazy { AppDatabase.getDatabase(context) }
private val authService by lazy { AuthService() }
private val apiService by lazy {
ApiService(
tokenManager = TokenManager,
authService = authService
)
}
private val conversationRepository by lazy {
ConversationRepository(
conversationDao = database.conversationDao(),
@@ -22,7 +36,12 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
chapterDao = database.chapterDao()
)
}
private val apiService by lazy { ApiService() }
private val messageRepository by lazy {
MessageRepository(
messageDao = database.messageDao(),
apiService = apiService
)
}
private val paymentRepository by lazy {
PaymentRepository(apiService = apiService)
@@ -39,6 +58,7 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
CreateMemoryViewModel(
conversationRepository = conversationRepository,
chapterRepository = chapterRepository,
messageRepository = messageRepository,
context = context
) as T
}