文档: 添加注释
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
package com.huaga.life_echo.app
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* 应用组合根(Composition Root)。
|
||||
*
|
||||
* 采用端口-适配器(Hexagonal)架构:
|
||||
* - **Port**:业务层依赖的接口(如 [ConversationApiPort]、[MemoirApiPort])
|
||||
* - **Adapter**:将 Port 委托给具体实现(ApiService、WebSocketClient)
|
||||
*
|
||||
* 所有网络相关依赖在此统一创建与注入,ViewModel、Repository 只依赖 Port,
|
||||
* 不直接依赖 Ktor/OkHttp,便于测试与替换实现。
|
||||
*
|
||||
* 生命周期:由 [LifeEchoApp] 在 [android.app.Application.onCreate] 创建,
|
||||
* [onTerminate] 时调用 [close] 释放连接(注:onTerminate 在真机上通常不会被调用)。
|
||||
*/
|
||||
import com.huaga.life_echo.config.AppConfig
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.data.database.AppDatabase
|
||||
@@ -62,13 +76,13 @@ class AppContainer(private val context: Context) {
|
||||
|
||||
val webSocketClient: WebSocketClient by lazy { WebSocketClient(realtimeTransportProvider) }
|
||||
|
||||
// Feature ports
|
||||
/** 各功能模块的 API Port,由 Adapter 委托给 [apiService] */
|
||||
val conversationApi: ConversationApiPort by lazy { ConversationApiAdapter(apiService) }
|
||||
val memoirApi: MemoirApiPort by lazy { MemoirApiAdapter(apiService) }
|
||||
val profileApi: ProfileApiPort by lazy { ProfileApiAdapter(apiService) }
|
||||
val paymentApi: PaymentApiPort by lazy { PaymentApiAdapter(apiService) }
|
||||
|
||||
// Repositories
|
||||
/** Repository 依赖 Port 而非 ApiService,便于单测时注入 Fake */
|
||||
val conversationRepository by lazy {
|
||||
ConversationRepository(
|
||||
conversationDao = database.conversationDao(),
|
||||
@@ -103,11 +117,16 @@ class AppContainer(private val context: Context) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 释放所有网络客户端,应在 Application 销毁时调用 */
|
||||
fun close() {
|
||||
restClientProvider.close()
|
||||
realtimeTransportProvider.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 profile 创建 REST 客户端。
|
||||
* AUTH 与 API 使用不同插件配置,见 [RestClientProfile]。
|
||||
*/
|
||||
private fun createRestClient(profile: RestClientProfile): HttpClient {
|
||||
return when (profile) {
|
||||
RestClientProfile.AUTH -> HttpClient(Android) {
|
||||
@@ -174,6 +193,7 @@ class AppContainer(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建 WebSocket 用 HttpClient,使用 OkHttp 引擎以支持长连接 */
|
||||
private fun createRealtimeClient(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
install(WebSockets)
|
||||
|
||||
@@ -4,6 +4,12 @@ import android.app.Application
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.ui.settings.AppSettings
|
||||
|
||||
/**
|
||||
* 应用入口,负责初始化全局依赖。
|
||||
*
|
||||
* [container] 为组合根,在 [onCreate] 中创建;[onTerminate] 时调用 [AppContainer.close] 释放网络连接。
|
||||
* 注:真机上 [onTerminate] 通常不会被调用,进程多由系统直接回收。
|
||||
*/
|
||||
class LifeEchoApp : Application() {
|
||||
|
||||
lateinit var container: AppContainer
|
||||
|
||||
@@ -4,6 +4,13 @@ import com.huaga.life_echo.data.database.*
|
||||
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* 对话数据仓库。
|
||||
*
|
||||
* 依赖 [ConversationApiPort] 而非 ApiService,便于单测时注入 Fake。
|
||||
* 本地数据通过 [ConversationDao]/[ConversationSegmentDao] 持久化,
|
||||
* 远程操作(创建、删除、同步)通过 [conversationApi] 完成。
|
||||
*/
|
||||
class ConversationRepository(
|
||||
private val conversationDao: ConversationDao,
|
||||
private val segmentDao: ConversationSegmentDao,
|
||||
|
||||
@@ -5,6 +5,12 @@ import com.huaga.life_echo.data.database.MessageDao
|
||||
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* 消息数据仓库。
|
||||
*
|
||||
* 依赖 [ConversationApiPort] 获取远程消息,[syncMessages] 将 API 返回的 DTO 转为本地 [Message] 并入库。
|
||||
* 本地读写通过 [MessageDao] 完成。
|
||||
*/
|
||||
class MessageRepository(
|
||||
private val messageDao: MessageDao,
|
||||
private val conversationApi: ConversationApiPort,
|
||||
@@ -13,6 +19,7 @@ class MessageRepository(
|
||||
return messageDao.getMessagesByConversationId(conversationId)
|
||||
}
|
||||
|
||||
/** 从 [conversationApi] 拉取消息并写入本地数据库 */
|
||||
suspend fun syncMessages(conversationId: String) {
|
||||
val result = conversationApi.getMessages(conversationId)
|
||||
result.getOrNull()?.let { messages ->
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package com.huaga.life_echo.data.repository
|
||||
|
||||
import com.huaga.life_echo.feature.payment.ports.PaymentApiPort
|
||||
|
||||
/**
|
||||
* 支付与订阅数据仓库。
|
||||
*
|
||||
* 依赖 [PaymentApiPort] 而非 ApiService,便于单测注入 Fake。
|
||||
* 本仓库无本地持久化,数据直接来自 API。
|
||||
*/
|
||||
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
|
||||
import com.huaga.life_echo.network.models.CurrentPlanDto
|
||||
import com.huaga.life_echo.network.models.OrderDto
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package com.huaga.life_echo.data.repository
|
||||
|
||||
import com.huaga.life_echo.feature.profile.ports.ProfileApiPort
|
||||
|
||||
/**
|
||||
* 个人资料数据仓库。
|
||||
*
|
||||
* 依赖 [ProfileApiPort] 而非 ApiService,便于单测注入 Fake。
|
||||
* 本仓库无本地持久化,数据直接来自 API。
|
||||
*/
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ import com.huaga.life_echo.network.models.CreateConversationResponse
|
||||
import com.huaga.life_echo.network.models.MessageDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
|
||||
/**
|
||||
* [ConversationApiPort] 的适配器实现。
|
||||
*
|
||||
* 将 Port 接口委托给 [ApiService],不包含业务逻辑。
|
||||
* 若需切换后端实现,只需在此处修改委托目标。
|
||||
*/
|
||||
class ConversationApiAdapter(
|
||||
private val apiService: ApiService,
|
||||
) : ConversationApiPort {
|
||||
|
||||
@@ -8,6 +8,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* [ConversationRealtimePort] 的适配器实现。
|
||||
*
|
||||
* 委托给 [WebSocketClient],并维护 [state] 供 UI 展示连接状态。
|
||||
* [prepare] 会调用 [WebSocketClient.prepare] 预热 HttpClient,再设置 state 为 Preparing。
|
||||
*/
|
||||
class ConversationRealtimeAdapter(
|
||||
private val webSocketClient: WebSocketClient,
|
||||
) : ConversationRealtimePort {
|
||||
|
||||
@@ -6,6 +6,12 @@ import com.huaga.life_echo.network.models.CreateConversationResponse
|
||||
import com.huaga.life_echo.network.models.MessageDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
|
||||
/**
|
||||
* 对话相关 REST API 端口。
|
||||
*
|
||||
* 业务层(ViewModel、Repository)依赖此接口而非 [com.huaga.life_echo.network.ApiService],
|
||||
* 便于单测时注入 Fake 实现。具体实现由 [com.huaga.life_echo.feature.conversation.adapters.ConversationApiAdapter] 提供。
|
||||
*/
|
||||
interface ConversationApiPort {
|
||||
suspend fun createConversation(): Result<CreateConversationResponse>
|
||||
suspend fun getConversationList(): Result<List<ConversationListItemDto>>
|
||||
|
||||
@@ -3,6 +3,13 @@ package com.huaga.life_echo.feature.conversation.ports
|
||||
import com.huaga.life_echo.network.WebSocketMessage
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* 对话实时通信(WebSocket)端口。
|
||||
*
|
||||
* 封装连接、发送文本/语音、取消生成等能力,业务层不直接依赖 [com.huaga.life_echo.network.WebSocketClient]。
|
||||
* [prepare] 应在 [connect] 前调用,用于预热 HttpClient,减少首次连接延迟。
|
||||
* [state] 反映连接状态,UI 可据此展示加载或已连接。
|
||||
*/
|
||||
interface ConversationRealtimePort {
|
||||
val state: StateFlow<State>
|
||||
|
||||
@@ -27,6 +34,7 @@ interface ConversationRealtimePort {
|
||||
fun isGenerating(): Boolean
|
||||
fun setGenerating(generating: Boolean)
|
||||
|
||||
/** 实时连接状态,用于 UI 展示 */
|
||||
sealed class State {
|
||||
data object NotConnected : State()
|
||||
data object Preparing : State()
|
||||
@@ -34,6 +42,12 @@ interface ConversationRealtimePort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段语音请求参数。
|
||||
*
|
||||
* 长语音边录边传时,每段音频携带会话 ID、段索引、是否最后一段等信息。
|
||||
* 重写 [equals]/[hashCode] 以支持 [ByteArray] 的内容比较。
|
||||
*/
|
||||
data class AudioSegmentRequest(
|
||||
val audioBytes: ByteArray,
|
||||
val conversationId: String,
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.huaga.life_echo.feature.memoir.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
|
||||
/** [MemoirApiPort] 的适配器,委托给 [ApiService] */
|
||||
import com.huaga.life_echo.network.models.BookDto
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.ChapterDto
|
||||
|
||||
@@ -6,6 +6,12 @@ import com.huaga.life_echo.network.models.ChapterDto
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
|
||||
/**
|
||||
* 回忆录相关 REST API 端口。
|
||||
*
|
||||
* 业务层(如 [MyMemoirViewModel])依赖此接口而非 ApiService,便于单测注入 Fake。
|
||||
* 实现由 [com.huaga.life_echo.feature.memoir.adapters.MemoirApiAdapter] 提供。
|
||||
*/
|
||||
interface MemoirApiPort {
|
||||
suspend fun getBookInfo(): Result<BookDto>
|
||||
suspend fun getChapters(): Result<List<ChapterDto>>
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.huaga.life_echo.feature.payment.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.payment.ports.PaymentApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
|
||||
/** [PaymentApiPort] 的适配器,委托给 [ApiService] */
|
||||
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
|
||||
import com.huaga.life_echo.network.models.CurrentPlanDto
|
||||
import com.huaga.life_echo.network.models.OrderDto
|
||||
|
||||
@@ -8,6 +8,11 @@ import com.huaga.life_echo.network.models.PlanDto
|
||||
import com.huaga.life_echo.network.models.QuotaCheckDto
|
||||
import com.huaga.life_echo.network.models.TestSubscriptionResponse
|
||||
|
||||
/**
|
||||
* 支付与订阅相关 REST API 端口。
|
||||
*
|
||||
* 业务层依赖此接口而非 ApiService,便于单测。实现由 [PaymentApiAdapter] 提供。
|
||||
*/
|
||||
interface PaymentApiPort {
|
||||
suspend fun getPlans(): Result<List<PlanDto>>
|
||||
suspend fun getCurrentPlan(): Result<CurrentPlanDto>
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.huaga.life_echo.feature.profile.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.profile.ports.ProfileApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
|
||||
/** [ProfileApiPort] 的适配器,委托给 [ApiService] */
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ package com.huaga.life_echo.feature.profile.ports
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
/**
|
||||
* 个人资料相关 REST API 端口。
|
||||
*
|
||||
* 业务层依赖此接口而非 ApiService,便于单测。实现由 [ProfileApiAdapter] 提供。
|
||||
*/
|
||||
interface ProfileApiPort {
|
||||
suspend fun getUserProfile(): Result<UserProfileDto>
|
||||
suspend fun getFAQs(): Result<List<FAQDto>>
|
||||
|
||||
@@ -5,6 +5,13 @@ import com.huaga.life_echo.network.models.*
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import io.ktor.client.*
|
||||
|
||||
/**
|
||||
* 业务 API 服务,使用 [RestClientProfile.API] 的 HttpClient。
|
||||
*
|
||||
* 通过 [RestClientProvider] 获取客户端,不自行创建,便于统一生命周期与 Bearer 刷新。
|
||||
* 主构造由 [com.huaga.life_echo.app.AppContainer] 注入;无参构造已废弃,仅用于遗留调用。
|
||||
*/
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.android.*
|
||||
import io.ktor.client.plugins.auth.*
|
||||
@@ -25,6 +32,7 @@ class ApiService(
|
||||
private val tokenManager: TokenManager? = null,
|
||||
private val authService: AuthService? = null
|
||||
) {
|
||||
/** 遗留构造:内部创建临时 RestClientProvider,供尚未迁移到 AppContainer 的调用方使用 */
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the provider-based constructor via AppContainer")
|
||||
constructor(
|
||||
@@ -36,6 +44,7 @@ class ApiService(
|
||||
authService = authService,
|
||||
)
|
||||
|
||||
/** 每次请求时从 Provider 获取 API 档位客户端,保证使用带 Bearer 的配置 */
|
||||
private val client get() = provider.getClient(RestClientProfile.API)
|
||||
|
||||
companion object {
|
||||
@@ -110,7 +119,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 对话相关API ====================
|
||||
// ==================== 对话相关 API ====================
|
||||
|
||||
suspend fun createConversation(): Result<CreateConversationResponse> {
|
||||
return try {
|
||||
@@ -167,7 +176,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回忆录相关API ====================
|
||||
// ==================== 回忆录相关 API ====================
|
||||
|
||||
suspend fun getBookInfo(): Result<BookDto> {
|
||||
return try {
|
||||
@@ -248,7 +257,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 付费系统相关API ====================
|
||||
// ==================== 付费系统相关 API ====================
|
||||
|
||||
suspend fun getPlans(): Result<List<PlanDto>> {
|
||||
return try {
|
||||
@@ -317,7 +326,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 支付订单API ====================
|
||||
// ==================== 支付订单 API ====================
|
||||
|
||||
suspend fun createPaymentOrder(
|
||||
planId: String,
|
||||
@@ -380,7 +389,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 用户相关API ====================
|
||||
// ==================== 用户相关 API ====================
|
||||
|
||||
suspend fun setTestSubscription(
|
||||
action: String,
|
||||
@@ -435,7 +444,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回忆录状态相关API ====================
|
||||
// ==================== 回忆录状态相关 API ====================
|
||||
|
||||
suspend fun getMemoirState(): Result<MemoirStateDto> {
|
||||
return try {
|
||||
@@ -448,7 +457,7 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 任务状态相关API ====================
|
||||
// ==================== 任务状态相关 API ====================
|
||||
|
||||
suspend fun getTasksStatus(): Result<TasksStatusDto> {
|
||||
return try {
|
||||
|
||||
@@ -4,6 +4,13 @@ import com.huaga.life_echo.config.AppConfig
|
||||
import com.huaga.life_echo.network.models.*
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
|
||||
/**
|
||||
* 认证 API 服务,使用 [RestClientProfile.AUTH] 的 HttpClient。
|
||||
*
|
||||
* 登录、注册、刷新 token 等无需 Bearer,插件栈更轻量。通过 [RestClientProvider] 获取客户端,
|
||||
* 不自行创建。无参构造内部创建临时 Provider,供单测或遗留调用使用。
|
||||
*/
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.android.Android
|
||||
@@ -29,6 +36,7 @@ import java.io.File
|
||||
class AuthService(
|
||||
private val provider: RestClientProvider,
|
||||
) {
|
||||
/** 无参构造:内部创建独立 Provider,供单测或未接入 AppContainer 的场景使用 */
|
||||
constructor() : this(
|
||||
RestClientProvider { _ ->
|
||||
HttpClient(Android) {
|
||||
@@ -43,6 +51,7 @@ class AuthService(
|
||||
}
|
||||
)
|
||||
|
||||
/** 使用 AUTH 档位客户端,无 Bearer 插件 */
|
||||
private val client get() = provider.getClient(RestClientProfile.AUTH)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -15,9 +15,18 @@ import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* WebSocket 客户端,负责与对话服务的实时通信。
|
||||
*
|
||||
* 通过 [RealtimeTransportProvider] 获取 [HttpClient],不自行创建,便于统一生命周期管理。
|
||||
* 无参构造用于单测中继承并覆盖 [connect]/[disconnect] 等方法的 Fake 实现。
|
||||
*
|
||||
* [prepare] 调用 [RealtimeTransportProvider.warmUp] 预创建 HttpClient,减少首次连接延迟。
|
||||
*/
|
||||
open class WebSocketClient(
|
||||
private val transportProvider: RealtimeTransportProvider,
|
||||
) {
|
||||
/** 无参构造:内部创建独立 Provider,供单测中 Fake 子类使用 */
|
||||
constructor() : this(
|
||||
RealtimeTransportProvider {
|
||||
HttpClient(OkHttp) {
|
||||
@@ -47,6 +56,7 @@ open class WebSocketClient(
|
||||
private const val MAX_RECONNECT_ATTEMPTS = 5
|
||||
}
|
||||
|
||||
/** 预热 HttpClient,应在 [connect] 前调用以减少首次连接延迟 */
|
||||
open fun prepare() {
|
||||
transportProvider.warmUp()
|
||||
}
|
||||
@@ -121,6 +131,7 @@ open class WebSocketClient(
|
||||
}
|
||||
}
|
||||
|
||||
/** 持续接收 WebSocket 帧,解析后回调 onMessage;连接异常时触发重连 */
|
||||
private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) {
|
||||
try {
|
||||
while (true) {
|
||||
@@ -167,6 +178,7 @@ open class WebSocketClient(
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送 WebSocket 消息,connect 类型可在未确认连接时发送(用于握手) */
|
||||
suspend fun sendMessage(message: WebSocketMessage) {
|
||||
if (!isConnected && message.type != MessageType.connect) {
|
||||
Log.w(TAG, "尝试发送消息但未连接: ${message.type}")
|
||||
@@ -318,6 +330,7 @@ open class WebSocketClient(
|
||||
Log.d(TAG, "WebSocket连接已断开")
|
||||
}
|
||||
|
||||
/** 指数退避重连,最多 [MAX_RECONNECT_ATTEMPTS] 次 */
|
||||
private suspend fun reconnectWithBackoff(
|
||||
onMessage: (WebSocketMessage) -> Unit,
|
||||
attempt: Int = 1
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
/**
|
||||
* 网络客户端就绪状态。
|
||||
*
|
||||
* 用于 [RestClientProvider] 和 [RealtimeTransportProvider] 对外暴露当前是否已创建并可用。
|
||||
* 业务层可据此决定是否发起请求或展示加载状态。
|
||||
*/
|
||||
enum class NetworkReadiness {
|
||||
/** 尚未创建任何客户端 */
|
||||
NotInitialized,
|
||||
/** 至少有一个客户端已创建并可用 */
|
||||
ClientReady,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* 实时传输(WebSocket)客户端提供者。
|
||||
*
|
||||
* 单例模式:多次 [getClient] 返回同一 [HttpClient]。
|
||||
* [warmUp] 在进入对话页面前预创建客户端,避免首次连接时的冷启动延迟。
|
||||
* 使用 [synchronized] 保证多线程下创建与关闭的线程安全。
|
||||
*/
|
||||
class RealtimeTransportProvider(
|
||||
private val factory: () -> HttpClient,
|
||||
) {
|
||||
|
||||
@@ -5,8 +5,22 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* REST 客户端配置档位。
|
||||
*
|
||||
* AUTH 与 API 使用不同的 [HttpClient] 实例,因为:
|
||||
* - AUTH:登录/注册等无需 Bearer token,插件栈更轻量
|
||||
* - API:业务接口需要 Auth 插件自动附加 token、401 时刷新,并配置更长超时
|
||||
*/
|
||||
enum class RestClientProfile { AUTH, API }
|
||||
|
||||
/**
|
||||
* REST 客户端提供者。
|
||||
*
|
||||
* 按 [RestClientProfile] 缓存 [HttpClient],同一 profile 多次 [getClient] 返回同一实例。
|
||||
* 使用 [synchronized] 保证多线程下单例创建安全。
|
||||
* [warmUp] 可提前创建所有 profile 的客户端,减少首次请求延迟。
|
||||
*/
|
||||
class RestClientProvider(
|
||||
private val factory: (RestClientProfile) -> HttpClient,
|
||||
) {
|
||||
|
||||
@@ -11,6 +11,12 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 认证 ViewModel,负责登录、注册、登出、token 刷新等。
|
||||
*
|
||||
* 依赖 [AuthService] 由 [ViewModelFactory] 从 [com.huaga.life_echo.app.AppContainer] 注入,
|
||||
* 便于单测时替换为 Fake。认证策略见 [checkAuthStatus] 与 [tryRefreshTokenSilently]。
|
||||
*/
|
||||
class AuthViewModel(
|
||||
context: Context,
|
||||
private val authService: AuthService,
|
||||
|
||||
@@ -47,6 +47,12 @@ import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* 创建回忆 ViewModel,负责对话创建、语音录制、消息收发、流式回复处理等。
|
||||
*
|
||||
* 依赖 [ConversationApiPort] 与 [ConversationRealtimePort] 而非 ApiService/WebSocketClient,
|
||||
* 由 [ViewModelFactory] 注入。进入对话前需调用 [conversationRealtime.prepare] 预热 WebSocket 客户端。
|
||||
*/
|
||||
class CreateMemoryViewModel(
|
||||
private val conversationRepository: ConversationRepository,
|
||||
private val chapterRepository: ChapterRepository,
|
||||
@@ -136,6 +142,10 @@ class CreateMemoryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化已有对话:先 [prepare] 预热 WebSocket,再加载历史消息并连接。
|
||||
* 若 convId 为 null 或 "new" 则直接返回。
|
||||
*/
|
||||
fun initializeConversation(convId: String?) {
|
||||
if (convId == null || convId == "new") {
|
||||
return
|
||||
@@ -173,6 +183,7 @@ class CreateMemoryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 [conversationApi] 拉取历史消息并同步到 [messageRepository] */
|
||||
private suspend fun loadHistoryMessages(convId: String) {
|
||||
val result = conversationApi.getMessages(convId)
|
||||
result.fold(
|
||||
@@ -186,6 +197,7 @@ class CreateMemoryViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
/** 创建新对话:调用 API 创建后连接 WebSocket,清空任务状态并准备录制 */
|
||||
fun startConversation() {
|
||||
viewModelScope.launch {
|
||||
connectionStatus.value = "创建对话中..."
|
||||
@@ -800,6 +812,7 @@ class CreateMemoryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据 [WebSocketMessage.type] 分发处理:agent_response、transcript、connect、error 等 */
|
||||
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())
|
||||
|
||||
@@ -7,6 +7,13 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.huaga.life_echo.data.database.Chapter
|
||||
import com.huaga.life_echo.data.repository.ChapterRepository
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
|
||||
/**
|
||||
* 我的回忆录 ViewModel。
|
||||
*
|
||||
* 依赖 [MemoirApiPort] 和 [ChapterRepository],由 [ViewModelFactory] 从 [AppContainer] 注入。
|
||||
* 章节数据本地存于 Room,远程同步通过 [memoirApi] 拉取并写入 [chapterRepository]。
|
||||
*/
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -7,6 +7,13 @@ import com.huaga.life_echo.app.AppContainer
|
||||
import com.huaga.life_echo.app.LifeEchoApp
|
||||
import com.huaga.life_echo.feature.conversation.adapters.ConversationRealtimeAdapter
|
||||
|
||||
/**
|
||||
* ViewModel 工厂,从 [AppContainer] 获取依赖并注入到各 ViewModel。
|
||||
*
|
||||
* 各 ViewModel 依赖 Port 接口(如 [ConversationApiPort])而非 ApiService/WebSocketClient,
|
||||
* 依赖由 container 提供;[ConversationRealtimeAdapter] 在此处用 container.webSocketClient 构造,
|
||||
* 因 Adapter 内部持有连接 state,每次创建新实例以保持 state 与当前屏幕一致。
|
||||
*/
|
||||
class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||
|
||||
private val container: AppContainer
|
||||
|
||||
Reference in New Issue
Block a user