From a2b2b6eb76033a4a588007e0f167720d809c58a8 Mon Sep 17 00:00:00 2001 From: yangshilin <2157598560@qq.com> Date: Fri, 13 Mar 2026 17:11:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AF=AD=E9=9F=B3=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=9A=82=E5=AD=98=E6=9C=AC=E5=9C=B0=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/agents/conversation_agent.py | 27 ++++-- api/routers/conversations.py | 2 +- api/routers/websocket.py | 4 + api/services/redis_service.py | 20 +++-- .../com/huaga/life_echo/app/AppContainer.kt | 5 ++ .../life_echo/data/database/AppDatabase.kt | 25 +++++- .../data/database/VoiceAttachment.kt | 20 +++++ .../data/database/VoiceAttachmentDao.kt | 21 +++++ .../repository/VoiceAttachmentRepository.kt | 24 ++++++ .../ui/viewmodel/CreateMemoryViewModel.kt | 82 ++++++++++++++++++- .../ui/viewmodel/ViewModelFactory.kt | 1 + ...MemoryViewModelRecordingCoordinatorTest.kt | 11 +++ .../CreateMemoryViewModelWarmupTest.kt | 11 +++ 13 files changed, 232 insertions(+), 21 deletions(-) create mode 100644 app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachment.kt create mode 100644 app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachmentDao.kt create mode 100644 app-android/app/src/main/java/com/huaga/life_echo/data/repository/VoiceAttachmentRepository.kt diff --git a/api/agents/conversation_agent.py b/api/agents/conversation_agent.py index 909df18..4f6e98c 100644 --- a/api/agents/conversation_agent.py +++ b/api/agents/conversation_agent.py @@ -44,9 +44,17 @@ class ConversationAgent: messages.append(AIMessage(content=msg["content"])) return messages - async def _save_message(self, conversation_id: str, role: str, content: str): + async def _save_message( + self, + conversation_id: str, + role: str, + content: str, + message_type: str = "text", + ): """保存消息到 Redis""" - await redis_service.add_message(conversation_id, role, content) + await redis_service.add_message( + conversation_id, role, content, message_type=message_type + ) def _format_history_string(self, messages: List[Any]) -> str: """将消息列表格式化为字符串(用于 prompt)""" @@ -236,6 +244,7 @@ class ConversationAgent: missing_fields: List[str], filled_fields: Dict[str, str], nickname: str = "", + is_from_voice: bool = False, ) -> List[str]: """在资料收集过程中生成跟进回复""" if not self.llm: @@ -250,7 +259,8 @@ class ConversationAgent: response = await self.llm.ainvoke(full_prompt) response_text = response.content if hasattr(response, 'content') else str(response) - await self._save_message(conversation_id, "human", user_message) + human_msg_type = "audio" if is_from_voice else "text" + await self._save_message(conversation_id, "human", user_message, message_type=human_msg_type) await self._save_message(conversation_id, "ai", response_text) messages = [msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()] @@ -285,6 +295,7 @@ class ConversationAgent: user_message: str, memoir_state: MemoirStateSchema, user_profile_context: str = "", + is_from_voice: bool = False, ) -> List[str]: """ 基于共享状态异步生成引导式回复 @@ -294,6 +305,7 @@ class ConversationAgent: user_message: 用户消息 memoir_state: 共享状态 user_profile_context: 用户基础资料上下文 + is_from_voice: 用户消息是否来自语音转写(用于保存正确的 messageType) Returns: Agent 回应文本列表(支持多条消息) @@ -333,13 +345,14 @@ class ConversationAgent: response = await self.llm.ainvoke(full_prompt) response_text = response.content if hasattr(response, 'content') else str(response) - - await self._save_message(conversation_id, "human", user_message) + + human_msg_type = "audio" if is_from_voice else "text" + await self._save_message(conversation_id, "human", user_message, message_type=human_msg_type) await self._save_message(conversation_id, "ai", response_text) - + messages = [msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()] return messages[:3] if messages else [response_text] - + except Exception as e: logger.error(f"生成回应失败: {e}") return [f"抱歉,生成回应时出现错误: {str(e)}"] diff --git a/api/routers/conversations.py b/api/routers/conversations.py index 628b56c..6bf0235 100644 --- a/api/routers/conversations.py +++ b/api/routers/conversations.py @@ -195,7 +195,7 @@ async def get_messages( "content": msg.get("content", ""), "senderType": "user" if msg.get("role") == "human" else "assistant", "timestamp": int(datetime.now(timezone.utc).timestamp() * 1000), # Redis中没有时间戳,使用当前时间 - "messageType": "text" + "messageType": msg.get("messageType", "text"), # 保留语音消息类型,使重新进入时仍显示为语音条 }) return messages except Exception as e: diff --git a/api/routers/websocket.py b/api/routers/websocket.py index 8f305a8..376d78d 100644 --- a/api/routers/websocket.py +++ b/api/routers/websocket.py @@ -928,12 +928,14 @@ async def process_user_message( remaining = _get_missing_profile_fields(user) filled = _get_filled_profile_fields(user) + is_from_voice = bool(segment.audio_url) responses = await agent.generate_profile_followup( conversation_id=conversation_id, user_message=user_message, missing_fields=remaining, filled_fields=filled, nickname=user.nickname or "", + is_from_voice=is_from_voice, ) segment.agent_response = "\n\n".join(responses) @@ -978,11 +980,13 @@ async def process_user_message( ) try: + is_from_voice = bool(segment.audio_url) responses = await agent.generate_response_with_state( conversation_id=conversation_id, user_message=user_message, memoir_state=state, user_profile_context=user_profile_context, + is_from_voice=is_from_voice, ) segment.agent_response = "\n\n".join(responses) diff --git a/api/services/redis_service.py b/api/services/redis_service.py index ba8d2b7..ebb0000 100644 --- a/api/services/redis_service.py +++ b/api/services/redis_service.py @@ -72,31 +72,33 @@ class RedisService: return [] async def add_message( - self, - conversation_id: str, - role: str, - content: str + self, + conversation_id: str, + role: str, + content: str, + message_type: str = "text", ) -> bool: """ 添加消息到对话历史 - + Args: conversation_id: 对话 ID role: 角色 ("human" 或 "ai") content: 消息内容 - + message_type: 消息类型 ("text" 或 "audio"),用于区分文本消息与语音消息 + Returns: 是否成功 """ try: client = await self.get_client() key = self._conversation_key(conversation_id) - + # 获取现有历史 history = await self.get_conversation_history(conversation_id) - + # 添加新消息 - history.append({"role": role, "content": content}) + history.append({"role": role, "content": content, "messageType": message_type}) # 保存回 Redis(带过期时间) await client.setex(key, self.session_ttl, json.dumps(history, ensure_ascii=False)) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt b/app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt index ca52543..6cac145 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt @@ -7,6 +7,7 @@ import com.huaga.life_echo.data.database.AppDatabase import com.huaga.life_echo.data.repository.ChapterRepository import com.huaga.life_echo.data.repository.ConversationRepository import com.huaga.life_echo.data.repository.MessageRepository +import com.huaga.life_echo.data.repository.VoiceAttachmentRepository import com.huaga.life_echo.data.repository.PaymentRepository import com.huaga.life_echo.data.repository.ProfileRepository import com.huaga.life_echo.feature.conversation.adapters.ConversationApiAdapter @@ -113,6 +114,10 @@ class AppContainer(private val context: Context) { ) } + val voiceAttachmentRepository by lazy { + VoiceAttachmentRepository(voiceAttachmentDao = database.voiceAttachmentDao()) + } + val paymentRepository by lazy { PaymentRepository( paymentApi = paymentApi, diff --git a/app-android/app/src/main/java/com/huaga/life_echo/data/database/AppDatabase.kt b/app-android/app/src/main/java/com/huaga/life_echo/data/database/AppDatabase.kt index aa632b2..8414bda 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/data/database/AppDatabase.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/data/database/AppDatabase.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase @Database( entities = [ @@ -12,9 +14,10 @@ import androidx.room.RoomDatabase ConversationSegment::class, Chapter::class, Book::class, - Message::class + Message::class, + VoiceAttachment::class, ], - version = 2, + version = 3, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { @@ -22,11 +25,26 @@ abstract class AppDatabase : RoomDatabase() { abstract fun conversationSegmentDao(): ConversationSegmentDao abstract fun chapterDao(): ChapterDao abstract fun messageDao(): MessageDao + abstract fun voiceAttachmentDao(): VoiceAttachmentDao companion object { + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL(""" + CREATE TABLE IF NOT EXISTS voice_attachments ( + conversationId TEXT NOT NULL, + userMessageIndex INTEGER NOT NULL, + filePath TEXT NOT NULL, + durationSeconds INTEGER NOT NULL, + PRIMARY KEY(conversationId, userMessageIndex) + ) + """.trimIndent()) + } + } + @Volatile private var INSTANCE: AppDatabase? = null - + fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( @@ -34,6 +52,7 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "life_echo_database" ) + .addMigrations(MIGRATION_2_3) .fallbackToDestructiveMigration() .build() INSTANCE = instance diff --git a/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachment.kt b/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachment.kt new file mode 100644 index 0000000..a45b09c --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachment.kt @@ -0,0 +1,20 @@ +package com.huaga.life_echo.data.database + +import androidx.room.Entity + +/** + * 本地持久化的语音附件元数据 + * 用于在重新进入对话时恢复语音条的显示(时长、播放) + * + * @param conversationId 对话 ID + * @param userMessageIndex 用户消息在对话中的索引(0-based,仅计 user 消息) + * @param filePath 本地持久化文件路径 + * @param durationSeconds 录音时长(秒) + */ +@Entity(tableName = "voice_attachments", primaryKeys = ["conversationId", "userMessageIndex"]) +data class VoiceAttachment( + val conversationId: String, + val userMessageIndex: Int, + val filePath: String, + val durationSeconds: Int, +) diff --git a/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachmentDao.kt b/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachmentDao.kt new file mode 100644 index 0000000..81e33ec --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/data/database/VoiceAttachmentDao.kt @@ -0,0 +1,21 @@ +package com.huaga.life_echo.data.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface VoiceAttachmentDao { + @Query("SELECT * FROM voice_attachments WHERE conversationId = :conversationId ORDER BY userMessageIndex ASC") + suspend fun getByConversationId(conversationId: String): List + + @Query("SELECT * FROM voice_attachments WHERE conversationId = :conversationId AND userMessageIndex = :index") + suspend fun getByConversationAndIndex(conversationId: String, index: Int): VoiceAttachment? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(attachment: VoiceAttachment) + + @Query("DELETE FROM voice_attachments WHERE conversationId = :conversationId") + suspend fun deleteByConversationId(conversationId: String) +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/data/repository/VoiceAttachmentRepository.kt b/app-android/app/src/main/java/com/huaga/life_echo/data/repository/VoiceAttachmentRepository.kt new file mode 100644 index 0000000..6c1fe48 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/data/repository/VoiceAttachmentRepository.kt @@ -0,0 +1,24 @@ +package com.huaga.life_echo.data.repository + +import com.huaga.life_echo.data.database.VoiceAttachment +import com.huaga.life_echo.data.database.VoiceAttachmentDao + +/** + * 语音附件本地持久化仓库 + * 用于保存和恢复用户发送的语音消息的本地文件路径和时长 + */ +class VoiceAttachmentRepository( + private val voiceAttachmentDao: VoiceAttachmentDao, +) { + suspend fun getByConversationId(conversationId: String): List { + return voiceAttachmentDao.getByConversationId(conversationId) + } + + suspend fun insert(attachment: VoiceAttachment) { + voiceAttachmentDao.insert(attachment) + } + + suspend fun deleteByConversationId(conversationId: String) { + voiceAttachmentDao.deleteByConversationId(conversationId) + } +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt index 23b320a..29d5b2c 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt @@ -8,6 +8,7 @@ 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.data.repository.VoiceAttachmentRepository import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort import com.huaga.life_echo.feature.conversation.ports.ConversationRealtimePort import com.huaga.life_echo.feature.conversation.ports.AudioSegmentRequest @@ -22,6 +23,7 @@ import com.huaga.life_echo.network.WebSocketMessage import com.huaga.life_echo.network.MessageType import com.huaga.life_echo.model.MessageDto import com.huaga.life_echo.data.database.Chapter +import com.huaga.life_echo.data.database.VoiceAttachment import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -40,6 +42,7 @@ class CreateMemoryViewModel( private val conversationRepository: ConversationRepository, private val chapterRepository: ChapterRepository, private val messageRepository: MessageRepository, + private val voiceAttachmentRepository: VoiceAttachmentRepository, private val context: Context, private val conversationApi: ConversationApiPort, private val conversationRealtime: ConversationRealtimePort, @@ -51,6 +54,25 @@ class CreateMemoryViewModel( private const val TAG = "CreateMemoryViewModel" private const val MIN_RECORDING_DURATION = 1 private const val SEGMENT_DURATION_SECONDS = 60 + private const val VOICE_RECORDINGS_DIR = "voice_recordings" + } + + /** + * 将录音文件复制到持久化目录,避免 cache 被清理后丢失 + * @return 持久化路径,失败时返回 null + */ + private fun persistVoiceFile(conversationId: String, userMessageIndex: Int, sourcePath: String): String? { + return try { + val baseDir = File(context.filesDir, VOICE_RECORDINGS_DIR) + val convDir = File(baseDir, conversationId) + convDir.mkdirs() + val destFile = File(convDir, "${userMessageIndex}.m4a") + File(sourcePath).copyTo(destFile, overwrite = true) + destFile.absolutePath + } catch (e: Exception) { + Log.e(TAG, "持久化语音文件失败: ${e.message}", e) + null + } } private val audioPlayer = AudioPlayer(context) @@ -165,12 +187,52 @@ class CreateMemoryViewModel( onSuccess = { messages -> historyMessages.value = messages messageRepository.syncMessages(convId) + _audioFilePaths.value = emptyMap() + _audioDurations.value = emptyMap() + mergeLocalVoiceAttachments(convId, messages) }, onFailure = { exception -> connectionStatus.value = "加载历史消息失败: ${exception.message}" } ) } + + /** + * 将本地持久化的语音附件合并到 audioFilePaths 和 audioDurations, + * 使重新进入时语音条显示与首次一致(含时长、可播放) + */ + private suspend fun mergeLocalVoiceAttachments(convId: String, messages: List) { + val attachments = voiceAttachmentRepository.getByConversationId(convId) + if (attachments.isEmpty()) return + + val attachmentMap = attachments.associateBy { it.userMessageIndex } + var userMsgIndex = 0 + val newPaths = mutableMapOf() + val newDurations = mutableMapOf() + + for (msg in messages) { + if (msg.senderType != "user") continue + if (msg.messageType != "audio") { + userMsgIndex++ + continue + } + val att = attachmentMap[userMsgIndex] + if (att == null) { + userMsgIndex++ + continue + } + if (File(att.filePath).exists()) { + newPaths[msg.id] = att.filePath + newDurations[msg.id] = att.durationSeconds + } + userMsgIndex++ + } + + if (newPaths.isNotEmpty() || newDurations.isNotEmpty()) { + _audioFilePaths.value = _audioFilePaths.value + newPaths + _audioDurations.value = _audioDurations.value + newDurations + } + } fun startConversation() { viewModelScope.launch { @@ -373,6 +435,19 @@ class CreateMemoryViewModel( val id = conversationId.value ?: return + val userMessageIndex = historyMessages.value.count { it.senderType == "user" } + val persistentPath = persistVoiceFile(id, userMessageIndex, filePath) + if (persistentPath != null) { + voiceAttachmentRepository.insert( + VoiceAttachment( + conversationId = id, + userMessageIndex = userMessageIndex, + filePath = persistentPath, + durationSeconds = durationSeconds, + ) + ) + } + val tempMessageId = "audio_user_${System.currentTimeMillis()}" val tempMessage = MessageDto( id = tempMessageId, @@ -383,7 +458,8 @@ class CreateMemoryViewModel( messageType = "audio" ) historyMessages.value = historyMessages.value + tempMessage - _audioFilePaths.value = _audioFilePaths.value + (tempMessageId to filePath) + val displayPath = persistentPath ?: filePath + _audioFilePaths.value = _audioFilePaths.value + (tempMessageId to displayPath) _audioDurations.value = _audioDurations.value + (tempMessageId to durationSeconds) val segmentFiles = try { @@ -442,6 +518,10 @@ class CreateMemoryViewModel( // ==================== 音频播放功能 ==================== fun toggleAudioPlayback(messageId: String, filePath: String) { + if (filePath.isBlank()) { + Log.d(TAG, "无本地音频文件,无法播放历史语音消息: $messageId") + return + } Log.d(TAG, "切换音频播放状态: $messageId, $filePath") audioPlayer.play(messageId, filePath) } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt index f60aa86..5c82ff8 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt @@ -29,6 +29,7 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory conversationRepository = container.conversationRepository, chapterRepository = container.chapterRepository, messageRepository = container.messageRepository, + voiceAttachmentRepository = container.voiceAttachmentRepository, context = context, conversationApi = container.conversationApi, conversationRealtime = container.createConversationRealtime(), diff --git a/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt b/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt index f5f7d9b..eb5445f 100644 --- a/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt +++ b/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt @@ -9,9 +9,12 @@ import com.huaga.life_echo.data.database.ConversationSegment import com.huaga.life_echo.data.database.ConversationSegmentDao import com.huaga.life_echo.data.database.Message import com.huaga.life_echo.data.database.MessageDao +import com.huaga.life_echo.data.database.VoiceAttachment +import com.huaga.life_echo.data.database.VoiceAttachmentDao import com.huaga.life_echo.data.repository.ChapterRepository import com.huaga.life_echo.data.repository.ConversationRepository import com.huaga.life_echo.data.repository.MessageRepository +import com.huaga.life_echo.data.repository.VoiceAttachmentRepository import com.huaga.life_echo.feature.conversation.ports.AudioSegmentRequest import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort import com.huaga.life_echo.feature.conversation.ports.ConversationRealtimePort @@ -396,6 +399,7 @@ class CreateMemoryViewModelRecordingCoordinatorTest { messageDao = FakeMessageDao(), conversationApi = conversationApi, ), + voiceAttachmentRepository = VoiceAttachmentRepository(voiceAttachmentDao = FakeVoiceAttachmentDao()), context = context, conversationApi = conversationApi, conversationRealtime = realtime, @@ -521,6 +525,13 @@ class CreateMemoryViewModelRecordingCoordinatorTest { override suspend fun deleteMessagesByConversationId(conversationId: String) = Unit } + private class FakeVoiceAttachmentDao : VoiceAttachmentDao { + override suspend fun getByConversationId(conversationId: String) = emptyList() + override suspend fun getByConversationAndIndex(conversationId: String, index: Int) = null + override suspend fun insert(attachment: VoiceAttachment) = Unit + override suspend fun deleteByConversationId(conversationId: String) = Unit + } + private class NoOpConversationApiPort : ConversationApiPort { override suspend fun createConversation(): Result = Result.failure(Exception("no-op")) diff --git a/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt b/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt index eabd9bb..9104895 100644 --- a/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt +++ b/app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt @@ -9,9 +9,12 @@ import com.huaga.life_echo.data.database.ConversationSegment import com.huaga.life_echo.data.database.ConversationSegmentDao import com.huaga.life_echo.data.database.Message import com.huaga.life_echo.data.database.MessageDao +import com.huaga.life_echo.data.database.VoiceAttachment +import com.huaga.life_echo.data.database.VoiceAttachmentDao import com.huaga.life_echo.data.repository.ChapterRepository import com.huaga.life_echo.data.repository.ConversationRepository import com.huaga.life_echo.data.repository.MessageRepository +import com.huaga.life_echo.data.repository.VoiceAttachmentRepository import com.huaga.life_echo.feature.conversation.ports.AudioSegmentRequest import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort import com.huaga.life_echo.feature.conversation.ports.ConversationRealtimePort @@ -169,6 +172,7 @@ class CreateMemoryViewModelWarmupTest { messageDao = FakeMessageDao(), conversationApi = conversationApi, ), + voiceAttachmentRepository = VoiceAttachmentRepository(voiceAttachmentDao = FakeVoiceAttachmentDao()), context = context, conversationApi = conversationApi, conversationRealtime = realtime, @@ -318,4 +322,11 @@ class CreateMemoryViewModelWarmupTest { override suspend fun deleteMessage(message: Message) = Unit override suspend fun deleteMessagesByConversationId(conversationId: String) = Unit } + + private class FakeVoiceAttachmentDao : VoiceAttachmentDao { + override suspend fun getByConversationId(conversationId: String) = emptyList() + override suspend fun getByConversationAndIndex(conversationId: String, index: Int) = null + override suspend fun insert(attachment: VoiceAttachment) = Unit + override suspend fun deleteByConversationId(conversationId: String) = Unit + } }