From 713106df146bbf52d4dece19af569a8bc09375d1 Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Wed, 26 Aug 2026 20:15:54 +0800 Subject: [PATCH] feat: stream scene execution progress --- .../ui/scene/SceneExecutionViewModel.kt | 22 +++++++-- .../manager/domain/DefaultSceneExecutor.kt | 46 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.kt index 8bc1c00..200b14d 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/scene/SceneExecutionViewModel.kt @@ -8,6 +8,9 @@ import com.flagship.abox.manager.domain.Scene import com.flagship.abox.manager.domain.SceneExecutionResult import com.flagship.abox.manager.domain.SceneExecutor import com.flagship.abox.manager.domain.SceneId +import com.flagship.abox.manager.domain.SceneExecutionId +import com.flagship.abox.manager.domain.SceneExecutionSource +import com.flagship.abox.manager.domain.SceneExecutionUpdate import com.flagship.abox.manager.domain.SceneRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -15,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.collect sealed interface SceneExecutionUiState { data object Loading : SceneExecutionUiState @@ -71,11 +75,19 @@ class SceneExecutionViewModel @Inject constructor( _uiState.value = SceneExecutionUiState.Failed("Active profile mismatch") return@launch } - when (val result = sceneExecutor.execute(scene)) { - is DomainResult.Success -> _uiState.value = SceneExecutionUiState.Completed(result.value) - is DomainResult.Failure -> _uiState.value = SceneExecutionUiState.Failed( - result.error.detail ?: "Execution failed", - ) + sceneExecutor.execute( + scene = scene, + executionId = SceneExecutionId("manual-${scene.id.value}"), + source = SceneExecutionSource.USER, + ).collect { update -> + when (update) { + is SceneExecutionUpdate.Completed -> _uiState.value = SceneExecutionUiState.Completed(update.result) + is SceneExecutionUpdate.Rejected -> _uiState.value = SceneExecutionUiState.Failed( + update.error.detail ?: "Execution failed", + ) + is SceneExecutionUpdate.Started -> _uiState.value = SceneExecutionUiState.Running(update.scene) + else -> Unit + } } } } diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.kt index d0111c9..fe01285 100644 --- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.kt +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/DefaultSceneExecutor.kt @@ -9,6 +9,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** * Production implementation of [SceneExecutor]. @@ -24,6 +26,50 @@ class DefaultSceneExecutor( ) : SceneExecutor { private val runningScenes = ConcurrentHashMap() + override fun execute( + scene: Scene, + executionId: SceneExecutionId, + source: SceneExecutionSource, + ): Flow = flow { + val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null + if (alreadyRunning) { + emit(SceneExecutionUpdate.Rejected(DomainError.OperationFailed(detail = "Scene is already running"))) + return@flow + } + val startTime = System.currentTimeMillis() + try { + emit(SceneExecutionUpdate.Started(executionId, scene)) + val results = mutableListOf() + var aborted = false + for (action in scene.actions) { + if (aborted) { + val skipped = SceneActionExecutionResult(action, SceneActionOutcome.SKIPPED, null) + results += skipped + emit(SceneExecutionUpdate.StepFinished(skipped)) + continue + } + emit(SceneExecutionUpdate.StepPending(action)) + emit(SceneExecutionUpdate.StepRunning(action)) + val started = System.currentTimeMillis() + val result = executeAction(scene.profileId, action).copy( + executionDurationMillis = System.currentTimeMillis() - started, + ) + results += result + emit(SceneExecutionUpdate.StepFinished(result)) + if (result.outcome == SceneActionOutcome.FAILURE && scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) { + aborted = true + } + } + emit( + SceneExecutionUpdate.Completed( + SceneExecutionResult(scene, results, System.currentTimeMillis() - startTime), + ), + ) + } finally { + runningScenes.remove(scene.id) + } + } + override suspend fun execute(scene: Scene): DomainResult { val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null if (alreadyRunning) {