From 79e3627ae030a60409a5d4ccbec81bc067632f65 Mon Sep 17 00:00:00 2001 From: ChaoMixian Date: Thu, 30 Apr 2026 19:36:32 +0800 Subject: [PATCH 1/9] refactor(WorkflowEditor): extract editor state and ui model seams --- .../vflow/core/execution/WorkflowExecutor.kt | 15 +- .../model/ActionStepExecutionSettings.kt | 29 ++ .../ActionEditorExecutionSettings.kt | 19 + .../ActionEditorSessionState.kt | 56 +++ .../ui/workflow_editor/ActionEditorSheet.kt | 354 ++++++-------- .../ui/workflow_editor/ActionEditorUiModel.kt | 111 +++++ .../vflow/ui/workflow_editor/BundleCompat.kt | 23 + .../vflow/ui/workflow_editor/ParamPath.kt | 29 ++ .../ui/workflow_editor/ParameterTreeEditor.kt | 96 ++++ .../workflow_editor/WorkflowEditorActivity.kt | 433 +++++------------- .../WorkflowEditorExecutionTracker.kt | 95 ++++ ...rkflowEditorMagicVariableCatalogBuilder.kt | 247 ++++++++++ .../ActionEditorUiModelBuilderTest.kt | 214 +++++++++ .../ParameterTreeEditorTest.kt | 103 +++++ 14 files changed, 1299 insertions(+), 525 deletions(-) create mode 100644 app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorExecutionTracker.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorMagicVariableCatalogBuilder.kt create mode 100644 app/src/test/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModelBuilderTest.kt create mode 100644 app/src/test/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditorTest.kt diff --git a/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt b/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt index 42f9f4d0..75504fef 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/execution/WorkflowExecutor.kt @@ -22,6 +22,7 @@ import com.chaomixian.vflow.core.types.basic.VDictionary import com.chaomixian.vflow.core.types.complex.VImage import com.chaomixian.vflow.core.utils.StorageManager import com.chaomixian.vflow.core.workflow.model.ActionStep +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings import com.chaomixian.vflow.core.workflow.model.Workflow import com.chaomixian.vflow.core.workflow.model.WorkflowReentryBehavior import com.chaomixian.vflow.core.workflow.module.logic.* @@ -29,7 +30,6 @@ import com.chaomixian.vflow.services.ExecutionNotificationManager import com.chaomixian.vflow.services.ExecutionNotificationState import com.chaomixian.vflow.services.ExecutionUIService import com.chaomixian.vflow.services.ServiceStateBus -import com.chaomixian.vflow.ui.workflow_editor.ActionEditorSheet import kotlinx.coroutines.* import kotlinx.coroutines.TimeoutCancellationException import java.io.File @@ -538,14 +538,15 @@ object WorkflowExecutor { DebugLogger.d("WorkflowExecutor", "[${workflow.name}][#${pc + 1}] -> 执行: ${module.metadata.name}") // --- 错误处理与重试逻辑 --- - val errorPolicy = step.parameters[ActionEditorSheet.KEY_ERROR_POLICY] as? String ?: ActionEditorSheet.POLICY_STOP - val retryCount = (step.parameters[ActionEditorSheet.KEY_RETRY_COUNT] as? Number)?.toInt() ?: 3 - val retryInterval = (step.parameters[ActionEditorSheet.KEY_RETRY_INTERVAL] as? Number)?.toLong() ?: 1000L + val executionSettings = ActionStepExecutionSettings.fromParameters(step.parameters) + val errorPolicy = executionSettings.policy + val retryCount = executionSettings.retryCount + val retryInterval = executionSettings.retryIntervalMillis var attempt = 0 var finalResult: ExecutionResult? = null - while (attempt <= retryCount || errorPolicy != ActionEditorSheet.POLICY_RETRY) { + while (attempt <= retryCount || errorPolicy != ActionStepExecutionSettings.POLICY_RETRY) { if (attempt > 0) { DebugLogger.w("WorkflowExecutor", "步骤执行失败,正在进行第 $attempt 次重试 (等待 ${retryInterval}ms)...") // 在模块内部进度更新时,也刷新通知 @@ -563,7 +564,7 @@ object WorkflowExecutor { } // 只有策略是 RETRY 且是 Failure 时才继续循环 - if (errorPolicy == ActionEditorSheet.POLICY_RETRY && finalResult is ExecutionResult.Failure) { + if (errorPolicy == ActionStepExecutionSettings.POLICY_RETRY && finalResult is ExecutionResult.Failure) { attempt++ if (attempt > retryCount) break } else { @@ -582,7 +583,7 @@ object WorkflowExecutor { is ExecutionResult.Failure -> { DebugLogger.e("WorkflowExecutor", "模块执行失败: ${result.errorTitle} - ${result.errorMessage}") - if (errorPolicy == ActionEditorSheet.POLICY_SKIP) { + if (errorPolicy == ActionStepExecutionSettings.POLICY_SKIP) { DebugLogger.w("WorkflowExecutor", "根据策略,跳过错误继续执行。") // 1. 如果模块提供了 partialOutputs,使用它们作为基础 diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt new file mode 100644 index 00000000..49686c99 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/model/ActionStepExecutionSettings.kt @@ -0,0 +1,29 @@ +package com.chaomixian.vflow.core.workflow.model + +data class ActionStepExecutionSettings( + val policy: String = POLICY_STOP, + val retryCount: Int = DEFAULT_RETRY_COUNT, + val retryIntervalMillis: Long = DEFAULT_RETRY_INTERVAL_MS +) { + companion object { + const val KEY_ERROR_POLICY = "__error_policy" + const val KEY_RETRY_COUNT = "__retry_count" + const val KEY_RETRY_INTERVAL = "__retry_interval" + + const val POLICY_STOP = "STOP" + const val POLICY_SKIP = "SKIP" + const val POLICY_RETRY = "RETRY" + + const val DEFAULT_RETRY_COUNT = 3 + const val DEFAULT_RETRY_INTERVAL_MS = 1000L + + fun fromParameters(parameters: Map): ActionStepExecutionSettings { + return ActionStepExecutionSettings( + policy = parameters[KEY_ERROR_POLICY] as? String ?: POLICY_STOP, + retryCount = (parameters[KEY_RETRY_COUNT] as? Number)?.toInt() ?: DEFAULT_RETRY_COUNT, + retryIntervalMillis = (parameters[KEY_RETRY_INTERVAL] as? Number)?.toLong() + ?: DEFAULT_RETRY_INTERVAL_MS + ) + } + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt new file mode 100644 index 00000000..67c2681a --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorExecutionSettings.kt @@ -0,0 +1,19 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings + +internal fun ActionEditorSessionState.getExecutionSettings(): ActionStepExecutionSettings { + return ActionStepExecutionSettings.fromParameters(asMap()) +} + +internal fun ActionEditorSessionState.setExecutionSettings(settings: ActionStepExecutionSettings) { + this[ActionStepExecutionSettings.KEY_ERROR_POLICY] = settings.policy + + if (settings.policy == ActionStepExecutionSettings.POLICY_RETRY) { + this[ActionStepExecutionSettings.KEY_RETRY_COUNT] = settings.retryCount + this[ActionStepExecutionSettings.KEY_RETRY_INTERVAL] = settings.retryIntervalMillis + } else { + remove(ActionStepExecutionSettings.KEY_RETRY_COUNT) + remove(ActionStepExecutionSettings.KEY_RETRY_INTERVAL) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt new file mode 100644 index 00000000..5b3fa9c5 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSessionState.kt @@ -0,0 +1,56 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.module.InputDefinition +import com.chaomixian.vflow.core.workflow.model.ActionStep + +internal class ActionEditorSessionState { + private val parameters = mutableMapOf() + private val parameterTreeEditor = ParameterTreeEditor(parameters) + + operator fun get(key: String): Any? = parameters[key] + + operator fun set(key: String, value: Any?) { + parameters[key] = value + } + + fun remove(key: String) { + parameters.remove(key) + } + + fun clear() { + parameters.clear() + } + + fun putAll(values: Map) { + parameters.putAll(values) + } + + fun replaceAll(values: Map) { + parameters.clear() + parameters.putAll(values) + } + + fun initializeDefaults(inputs: List) { + inputs.forEach { def -> + def.defaultValue?.let { parameters[def.id] = it } + } + } + + fun setPath(path: ParamPath, value: Any?) { + parameterTreeEditor.set(path, value) + } + + fun clearPath(path: ParamPath, topLevelDefaultValue: Any?) { + parameterTreeEditor.clear(path, topLevelDefaultValue) + } + + fun asMap(): Map = parameters + + fun snapshot(): MutableMap = parameters.toMutableMap() + + fun toMutableMap(): MutableMap = parameters.toMutableMap() + + fun toActionStep(moduleId: String, stepId: String = ""): ActionStep { + return ActionStep(moduleId = moduleId, parameters = snapshot(), id = stepId) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt index b1f0e55c..ab22ca2e 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorSheet.kt @@ -12,6 +12,7 @@ import android.widget.* import androidx.core.view.isVisible import com.chaomixian.vflow.R import com.chaomixian.vflow.core.module.* +import com.chaomixian.vflow.core.workflow.model.ActionStepExecutionSettings import com.chaomixian.vflow.core.workflow.model.ActionStep import com.chaomixian.vflow.ui.common.ModuleDetailDialog import com.google.android.material.materialswitch.MaterialSwitch @@ -43,7 +44,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { var onStartActivityForResult: ((Intent, (resultCode: Int, data: Intent?) -> Unit) -> Unit)? = null private val inputViews = mutableMapOf() private var customEditorHolder: CustomEditorViewHolder? = null - private val currentParameters = mutableMapOf() + private val currentParameters = ActionEditorSessionState() // 引用容器视图 private var customUiCard: MaterialCardView? = null @@ -64,15 +65,6 @@ class ActionEditorSheet : BottomSheetDialogFragment() { // 单次编辑会话的展开状态缓存 private val expandedSections = mutableMapOf() - // 异常处理策略相关的常量 Key - const val KEY_ERROR_POLICY = "__error_policy" - const val KEY_RETRY_COUNT = "__retry_count" - const val KEY_RETRY_INTERVAL = "__retry_interval" - - const val POLICY_STOP = "STOP" - const val POLICY_SKIP = "SKIP" - const val POLICY_RETRY = "RETRY" - /** 创建 ActionEditorSheet 实例。 */ fun newInstance( module: ActionModule, @@ -98,15 +90,13 @@ class ActionEditorSheet : BottomSheetDialogFragment() { super.onCreate(savedInstanceState) val moduleId = arguments?.getString("moduleId") module = moduleId?.let { ModuleRegistry.getModule(it) } ?: return dismiss() - existingStep = arguments?.getParcelable("existingStep") + existingStep = arguments?.getParcelableCompat("existingStep") focusedInputId = arguments?.getString("focusedInputId") - allSteps = arguments?.getParcelableArrayList("allSteps") + allSteps = arguments?.getParcelableArrayListCompat("allSteps") availableNamedVariables = arguments?.getStringArrayList("namedVariables") // 初始化参数,首先使用模块定义的默认值 - module.getInputs().forEach { def -> - def.defaultValue?.let { currentParameters[def.id] = it } - } + currentParameters.initializeDefaults(module.getInputs()) // 然后用步骤已有的参数覆盖默认值 existingStep?.parameters?.let { currentParameters.putAll(it) } } @@ -155,15 +145,15 @@ class ActionEditorSheet : BottomSheetDialogFragment() { saveButton.setOnClickListener { readParametersFromUi() - readErrorSettingsFromUi() // 读取错误处理配置 + syncExecutionSettingsFromUi() val finalParams = existingStep?.parameters?.toMutableMap() ?: mutableMapOf() - finalParams.putAll(currentParameters) + finalParams.putAll(currentParameters.snapshot()) val stepForValidation = ActionStep(moduleId = module.id, parameters = finalParams, id = existingStep?.id ?: "") // 调用 validate 方法进行验证 val validationResult = module.validate(stepForValidation, allSteps ?: emptyList()) if (validationResult.isValid) { - onSave?.invoke(ActionStep(module.id, currentParameters)) + onSave?.invoke(currentParameters.toActionStep(module.id)) dismiss() } else { Toast.makeText(context, validationResult.errorMessage, Toast.LENGTH_LONG).show() @@ -181,9 +171,18 @@ class ActionEditorSheet : BottomSheetDialogFragment() { errorSettingsContent?.removeAllViews() val radioGroup = RadioGroup(context).apply { orientation = RadioGroup.VERTICAL } - val rbStop = RadioButton(context).apply { text = getString(R.string.editor_error_policy_stop); tag = POLICY_STOP } - val rbSkip = RadioButton(context).apply { text = getString(R.string.editor_error_policy_skip); tag = POLICY_SKIP } - val rbRetry = RadioButton(context).apply { text = getString(R.string.editor_error_policy_retry); tag = POLICY_RETRY } + val rbStop = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_stop) + tag = ActionStepExecutionSettings.POLICY_STOP + } + val rbSkip = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_skip) + tag = ActionStepExecutionSettings.POLICY_SKIP + } + val rbRetry = RadioButton(context).apply { + text = getString(R.string.editor_error_policy_retry) + tag = ActionStepExecutionSettings.POLICY_RETRY + } radioGroup.addView(rbStop) radioGroup.addView(rbSkip) @@ -196,9 +195,10 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } // 恢复状态 - val currentPolicy = currentParameters[KEY_ERROR_POLICY] as? String ?: POLICY_STOP - val currentRetryCount = (currentParameters[KEY_RETRY_COUNT] as? Number)?.toFloat() ?: 3f - val currentRetryInterval = (currentParameters[KEY_RETRY_INTERVAL] as? Number)?.toFloat() ?: 1000f + val executionSettings = currentParameters.getExecutionSettings() + val currentPolicy = executionSettings.policy + val currentRetryCount = executionSettings.retryCount.toFloat() + val currentRetryInterval = executionSettings.retryIntervalMillis.toFloat() // --- 重试次数 --- val sliderRetryCount = StandardControlFactory.createSliderWithLabel( @@ -228,11 +228,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { retryContainer.addView(sliderRetryInterval) when (currentPolicy) { - POLICY_SKIP -> rbSkip.isChecked = true - POLICY_RETRY -> rbRetry.isChecked = true + ActionStepExecutionSettings.POLICY_SKIP -> rbSkip.isChecked = true + ActionStepExecutionSettings.POLICY_RETRY -> rbRetry.isChecked = true else -> rbStop.isChecked = true } - retryContainer.isVisible = (currentPolicy == POLICY_RETRY) + retryContainer.isVisible = (currentPolicy == ActionStepExecutionSettings.POLICY_RETRY) // 获取滑块引用 val sliderRetryCountView = sliderRetryCount.getChildAt(1) as Slider @@ -253,117 +253,120 @@ class ActionEditorSheet : BottomSheetDialogFragment() { errorSettingsContent?.addView(retryContainer) } - /** - * 读取异常处理配置到 currentParameters。 - */ - private fun readErrorSettingsFromUi() { + private fun syncExecutionSettingsFromUi() { + currentParameters.setExecutionSettings(readExecutionSettingsFromUi()) + } + + private fun readExecutionSettingsFromUi(): ActionStepExecutionSettings { val selectedId = errorPolicyGroup?.checkedRadioButtonId val view = errorPolicyGroup?.findViewById(selectedId ?: -1) - val policy = view?.tag as? String ?: POLICY_STOP + val policy = view?.tag as? String ?: ActionStepExecutionSettings.POLICY_STOP - currentParameters[KEY_ERROR_POLICY] = policy + return ActionStepExecutionSettings( + policy = policy, + retryCount = retryCountSlider?.value?.toInt() ?: ActionStepExecutionSettings.DEFAULT_RETRY_COUNT, + retryIntervalMillis = retryIntervalSlider?.value?.toLong() + ?: ActionStepExecutionSettings.DEFAULT_RETRY_INTERVAL_MS + ) + } - if (policy == POLICY_RETRY) { - currentParameters[KEY_RETRY_COUNT] = retryCountSlider?.value?.toInt() ?: 3 - currentParameters[KEY_RETRY_INTERVAL] = retryIntervalSlider?.value?.toLong() ?: 1000L - } else { - // 清理无用参数 - currentParameters.remove(KEY_RETRY_COUNT) - currentParameters.remove(KEY_RETRY_INTERVAL) + private fun requestMagicVariableSelection(inputId: String) { + readParametersFromUi() + onMagicVariableRequested?.invoke(inputId, currentParameters.asMap()) + } + + private fun mutateSession( + readUiFirst: Boolean = false, + rebuildMode: RebuildMode = RebuildMode.IMMEDIATE, + mutation: ActionEditorSessionState.() -> Unit + ) { + if (readUiFirst) { + readParametersFromUi() + } + mutation(currentParameters) + when (rebuildMode) { + RebuildMode.NONE -> Unit + RebuildMode.IMMEDIATE -> buildUi() + RebuildMode.POST -> postBuildUi() } } + private fun replaceSessionAndRebuild(newParameters: Map) { + currentParameters.replaceAll(newParameters) + buildUi() + } + + private fun postBuildUi() { + android.os.Handler(android.os.Looper.getMainLooper()).post { + try { + buildUi() + } catch (_: Exception) { + // 忽略视图已销毁的情况 + } + } + } + + private enum class RebuildMode { + NONE, + IMMEDIATE, + POST + } + /** * 构建UI的逻辑。 */ private fun buildUi() { - // 清空所有容器 + val uiModel = computeUiModel() + applyUiModelCorrections(uiModel) + renderUiModel(uiModel) + } + + private fun computeUiModel(): EditorUiModel { + return ActionEditorUiModelBuilder.build( + module = module, + sessionState = currentParameters, + allSteps = allSteps + ) + } + + private fun applyUiModelCorrections(uiModel: EditorUiModel) { + val correctedParameters = uiModel.effectiveParameters + if (currentParameters.asMap() != correctedParameters) { + currentParameters.replaceAll(correctedParameters) + } + } + + private fun renderUiModel(uiModel: EditorUiModel) { customUiContainer?.removeAllViews() genericInputsContainer?.removeAllViews() editorActionsContainer?.removeAllViews() inputViews.clear() customEditorHolder = null - val stepForUi = ActionStep(module.id, currentParameters) - val inputsToShow = module.getDynamicInputs(stepForUi, allSteps) - - // 校正无效的枚举参数值,防止因模块更新导致崩溃 - inputsToShow.forEach { inputDef -> - if (inputDef.staticType == ParameterType.ENUM) { - val currentValue = currentParameters[inputDef.id] as? String - if (currentValue != null && !inputDef.options.contains(currentValue)) { - // 尝试使用向后兼容映射 - val mappedValue = inputDef.normalizeEnumValueOrNull(currentValue) - if (mappedValue != null && inputDef.options.contains(mappedValue)) { - currentParameters[inputDef.id] = mappedValue - } else { - // 映射失败,使用默认值 - currentParameters[inputDef.id] = inputDef.defaultValue - } - } - } - } - - val uiProvider = module.uiProvider - val editorActions = module.getEditorActions(stepForUi, allSteps) - val handledInputIds = uiProvider?.getHandledInputIds() ?: emptySet() - - // 构建自定义 UI - if (uiProvider != null && uiProvider !is RichTextUIProvider) { + if (uiModel.showCustomUi) { + val uiProvider = uiModel.uiProvider ?: return customEditorHolder = uiProvider.createEditor( context = requireContext(), parent = customUiContainer!!, - currentParameters = currentParameters, + currentParameters = uiModel.effectiveParameters, onParametersChanged = { readParametersFromUi() }, - onMagicVariableRequested = { inputId -> - readParametersFromUi() - this.onMagicVariableRequested?.invoke(inputId, currentParameters) - }, + onMagicVariableRequested = ::requestMagicVariableSelection, allSteps = allSteps, onStartActivityForResult = onStartActivityForResult ) customUiContainer?.addView(customEditorHolder!!.view) } - customUiCard?.isVisible = uiProvider != null && uiProvider !is RichTextUIProvider - /** - * 检查输入参数是否应该显示。 - * 优先使用新的 visibility 条件系统,同时保持对 isHidden 的向后兼容。 - */ - fun isInputVisible(inputDef: InputDefinition, params: Map): Boolean { - // 如果设置了 visibility 条件,使用新系统 - inputDef.visibility?.let { visibility -> - return visibility.isVisible(params) - } - // 否则使用传统的 isHidden 标志 - return !inputDef.isHidden - } + customUiCard?.isVisible = uiModel.showCustomUi - // 构建通用参数列表 (分离普通参数和折叠参数) - val normalInputs = mutableListOf() - val foldedInputs = mutableListOf() - - inputsToShow.forEach { inputDef -> - if (!handledInputIds.contains(inputDef.id) && isInputVisible(inputDef, currentParameters)) { - if (inputDef.isFolded) { - foldedInputs.add(inputDef) - } else { - normalInputs.add(inputDef) - } - } - } - - // 添加普通参数 - normalInputs.forEach { inputDef -> - val inputView = createViewForInputDefinition(inputDef, genericInputsContainer!!) + uiModel.genericInputsSection.fields.forEach { fieldModel -> + val inputView = createViewForFieldModel(fieldModel, genericInputsContainer!!) genericInputsContainer?.addView(inputView) - inputViews[inputDef.id] = inputView + inputViews[fieldModel.inputDefinition.id] = inputView } - // 如果有折叠参数,创建“更多设置”区域 - if (foldedInputs.isNotEmpty()) { - // 创建分隔线 - if (normalInputs.isNotEmpty()) { + if (uiModel.advancedInputsSection.isVisible) { + if (uiModel.shouldShowAdvancedDivider) { val divider = View(context).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -377,20 +380,16 @@ class ActionEditorSheet : BottomSheetDialogFragment() { genericInputsContainer?.addView(divider) } - // 创建折叠容器结构 - val advancedSection = createAdvancedSection(foldedInputs) + val advancedSection = createAdvancedSection(uiModel.advancedInputsSection.fields) genericInputsContainer?.addView(advancedSection) } - // 只有当有通用参数(普通或折叠)时才显示卡片 - genericInputsCard?.isVisible = normalInputs.isNotEmpty() || foldedInputs.isNotEmpty() + genericInputsCard?.isVisible = uiModel.showsAnyGenericInputs - if (editorActions.isNotEmpty()) { - editorActionsContainer?.addView(createEditorActionsSection(editorActions)) - editorActionsCard?.isVisible = true - } else { - editorActionsCard?.isVisible = false + if (uiModel.editorActionsSection.isVisible) { + editorActionsContainer?.addView(createEditorActionsSection(uiModel.editorActionsSection.actions)) } + editorActionsCard?.isVisible = uiModel.editorActionsSection.isVisible } private fun createEditorActionsSection(actions: List): View { @@ -428,7 +427,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { /** * 动态创建"更多设置"折叠区域。 */ - private fun createAdvancedSection(inputs: List): View { + private fun createAdvancedSection(fields: List): View { val context = requireContext() val density = resources.displayMetrics.density @@ -508,11 +507,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } // 填充参数到内容容器 - inputs.forEach { inputDef -> - val inputView = createViewForInputDefinition(inputDef, contentLayout) + fields.forEach { fieldModel -> + val inputView = createViewForFieldModel(fieldModel, contentLayout) contentLayout.addView(inputView) // 注册到 inputViews,这样 readParametersFromUi 就能自动读取它们 - inputViews[inputDef.id] = inputView + inputViews[fieldModel.inputDefinition.id] = inputView } // 设置点击事件 @@ -536,13 +535,20 @@ class ActionEditorSheet : BottomSheetDialogFragment() { return rootLayout } + private fun createViewForFieldModel(fieldModel: EditorFieldModel, parent: ViewGroup): View { + return createViewForInputDefinition(fieldModel, parent) + } + /** * 为输入参数创建视图。 * 对于 CHIP_GROUP 风格,使用简化的布局(不带标签和魔法变量按钮)。 * 对于 PICKER 类型,使用带选择器图标的输入框。 * 对于其他风格,使用完整的参数输入行布局。 */ - private fun createViewForInputDefinition(inputDef: InputDefinition, parent: ViewGroup): View { + private fun createViewForInputDefinition(fieldModel: EditorFieldModel, parent: ViewGroup): View { + val inputDef = fieldModel.inputDefinition + val currentValue = fieldModel.currentValue + // CHIP_GROUP 风格不需要魔法变量按钮,使用简化布局 if (inputDef.inputStyle == InputStyle.CHIP_GROUP) { val row = LayoutInflater.from(requireContext()).inflate(R.layout.row_editor_input, null, false) @@ -555,17 +561,11 @@ class ActionEditorSheet : BottomSheetDialogFragment() { val chipGroupView = StandardControlFactory.createChipGroup( context = requireContext(), options = inputDef.options, - currentValue = currentParameters[inputDef.id] as? String, + currentValue = currentValue as? String, onSelectionChanged = { selectedItem -> if (currentParameters[inputDef.id] != selectedItem) { - readParametersFromUi() - currentParameters[inputDef.id] = selectedItem - android.os.Handler(android.os.Looper.getMainLooper()).post { - try { - buildUi() - } catch (e: Exception) { - // 忽略视图已销毁的情况 - } + mutateSession(readUiFirst = true, rebuildMode = RebuildMode.POST) { + this[inputDef.id] = selectedItem } } }, @@ -587,7 +587,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { val pickerInputView = StandardControlFactory.createPickerInput( context = requireContext(), - currentValue = currentParameters[inputDef.id], + currentValue = currentValue, pickerType = inputDef.pickerType, hint = inputDef.getLocalizedHint(requireContext()), onPickerClicked = { @@ -603,25 +603,14 @@ class ActionEditorSheet : BottomSheetDialogFragment() { return StandardControlFactory.createParameterInputRow( context = requireContext(), inputDef = inputDef, - currentValue = currentParameters[inputDef.id], + currentValue = currentValue, allSteps = allSteps, - onMagicVariableRequested = { inputId -> - readParametersFromUi() - this.onMagicVariableRequested?.invoke(inputId, currentParameters) - }, + onMagicVariableRequested = ::requestMagicVariableSelection, onEnumItemSelected = { selectedItem -> // 防止重复触发:只在值真正改变时才处理 if (currentParameters[inputDef.id] != selectedItem) { - // 先读取当前UI中的所有参数值,防止切换条件时丢失其他输入框的值 - readParametersFromUi() - currentParameters[inputDef.id] = selectedItem - // 使用 Handler 延迟重建UI以避免在事件处理期间冻结 - android.os.Handler(android.os.Looper.getMainLooper()).post { - try { - buildUi() - } catch (e: Exception) { - // 忽略视图已销毁的情况 - } + mutateSession(readUiFirst = true, rebuildMode = RebuildMode.POST) { + this[inputDef.id] = selectedItem } } } @@ -654,8 +643,9 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } fun updateParametersAndRebuildUi(newParameters: Map) { - currentParameters.putAll(newParameters) - buildUi() + mutateSession { + putAll(newParameters) + } } private fun isVariableReference(value: Any?): Boolean { @@ -669,7 +659,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { } inputViews.forEach { (id, view) -> - val stepForUi = ActionStep(module.id, currentParameters) + val stepForUi = currentParameters.toActionStep(module.id) val inputDef = module.getDynamicInputs(stepForUi, allSteps).find { it.id == id } if (inputDef?.supportsRichText == false && isVariableReference(currentParameters[id])) { @@ -706,8 +696,9 @@ class ActionEditorSheet : BottomSheetDialogFragment() { * 更新输入框的变量。 */ fun updateInputWithVariable(inputId: String, variableReference: String) { - val stepForUi = ActionStep(module.id, currentParameters) + val stepForUi = currentParameters.toActionStep(module.id) val inputDef = module.getDynamicInputs(stepForUi, allSteps).find { it.id == inputId } + val path = ParamPath.parse(inputId) var richTextView: RichTextView? = null @@ -734,66 +725,20 @@ class ActionEditorSheet : BottomSheetDialogFragment() { return // 直接操作视图后返回,避免重建UI } - if (inputId.contains('.')) { - val parts = inputId.split('.', limit = 2) - val mainInputId = parts[0] - val subKey = parts[1] - - // 获取当前参数值 - val currentValue = currentParameters[mainInputId] - - if (currentValue is List<*>) { - // 如果当前是列表,则按索引更新列表 - val mutableList = currentValue.toMutableList() - val index = subKey.toIntOrNull() - // 确保索引有效 - if (index != null && index >= 0 && index < mutableList.size) { - mutableList[index] = variableReference // 更新指定位置 - currentParameters[mainInputId] = mutableList - } - } else { - // 如果是字典或默认情况,按 Map 更新 - val dict = (currentValue as? Map<*, *>)?.toMutableMap() ?: mutableMapOf() - dict[subKey] = variableReference - currentParameters[mainInputId] = dict - } - } else { - currentParameters[inputId] = variableReference + mutateSession { + setPath(path, variableReference) } - buildUi() } /** * 清除输入框的变量。 */ fun clearInputVariable(inputId: String) { - if (inputId.contains('.')) { - val parts = inputId.split('.', limit = 2) - val mainInputId = parts[0] - val subKey = parts[1] - - val currentValue = currentParameters[mainInputId] - - if (currentValue is List<*>) { - // 列表:清除指定索引的内容(置为空字符串) - val mutableList = currentValue.toMutableList() - val index = subKey.toIntOrNull() - if (index != null && index >= 0 && index < mutableList.size) { - mutableList[index] = "" - currentParameters[mainInputId] = mutableList - } - } else { - // 字典:清除指定 Key 的内容 - val dict = (currentValue as? Map<*, *>)?.toMutableMap() ?: return - dict[subKey] = "" - currentParameters[mainInputId] = dict - } - } else { - val inputDef = module.getInputs().find { it.id == inputId } ?: return - currentParameters[inputId] = inputDef.defaultValue + val path = ParamPath.parse(inputId) + val topLevelDefaultValue = module.getInputs().find { it.id == path.rootId }?.defaultValue + mutateSession { + clearPath(path, topLevelDefaultValue) } - // 清除后重建UI - buildUi() } /** @@ -813,10 +758,7 @@ class ActionEditorSheet : BottomSheetDialogFragment() { val newParametersFromServer = module.onParameterUpdated(stepForUpdate, updatedId, updatedValue) // 使用模块返回的参数集,完全更新编辑器内部的当前参数状态 - currentParameters.clear() - currentParameters.putAll(newParametersFromServer) - // 使用新的参数状态,重建整个UI - buildUi() + replaceSessionAndRebuild(newParametersFromServer) } private fun createBaseViewForInputType(inputDef: InputDefinition, currentValue: Any?): View { diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt new file mode 100644 index 00000000..0e32e92c --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ActionEditorUiModel.kt @@ -0,0 +1,111 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import com.chaomixian.vflow.core.module.ActionModule +import com.chaomixian.vflow.core.module.EditorAction +import com.chaomixian.vflow.core.module.InputDefinition +import com.chaomixian.vflow.core.module.ModuleUIProvider +import com.chaomixian.vflow.core.module.ParameterType +import com.chaomixian.vflow.core.workflow.model.ActionStep + +internal data class EditorFieldModel( + val inputDefinition: InputDefinition, + val currentValue: Any? +) + +internal data class EditorSectionModel( + val fields: List = emptyList(), + val actions: List = emptyList(), + val isVisible: Boolean = false +) + +internal data class EditorUiModel( + val uiProvider: ModuleUIProvider?, + val effectiveParameters: Map, + val showCustomUi: Boolean, + val genericInputsSection: EditorSectionModel, + val advancedInputsSection: EditorSectionModel, + val editorActionsSection: EditorSectionModel +) { + val showsAnyGenericInputs: Boolean + get() = genericInputsSection.isVisible || advancedInputsSection.isVisible + + val shouldShowAdvancedDivider: Boolean + get() = genericInputsSection.isVisible && advancedInputsSection.isVisible +} + +internal object ActionEditorUiModelBuilder { + fun build( + module: ActionModule, + sessionState: ActionEditorSessionState, + allSteps: List? + ): EditorUiModel { + val baseStep = sessionState.toActionStep(module.id) + val baseInputs = module.getDynamicInputs(baseStep, allSteps) + val correctedParameterValues = computeEnumCorrections(baseInputs, sessionState.asMap()) + val effectiveParameters = sessionState.snapshot().apply { + putAll(correctedParameterValues) + } + val effectiveStep = ActionStep(module.id, effectiveParameters) + val inputsToShow = module.getDynamicInputs(effectiveStep, allSteps) + val uiProvider = module.uiProvider + val handledInputIds = uiProvider?.getHandledInputIds() ?: emptySet() + val visibleFields = inputsToShow + .asSequence() + .filterNot { handledInputIds.contains(it.id) } + .filter { it.isVisibleForEditor(effectiveParameters) } + .map { inputDef -> + EditorFieldModel( + inputDefinition = inputDef, + currentValue = effectiveParameters[inputDef.id] + ) + } + .toList() + val genericFields = visibleFields.filterNot { it.inputDefinition.isFolded } + val advancedFields = visibleFields.filter { it.inputDefinition.isFolded } + val editorActions = module.getEditorActions(effectiveStep, allSteps) + + return EditorUiModel( + uiProvider = uiProvider, + effectiveParameters = effectiveParameters, + showCustomUi = uiProvider != null && uiProvider !is RichTextUIProvider, + genericInputsSection = EditorSectionModel( + fields = genericFields, + isVisible = genericFields.isNotEmpty() + ), + advancedInputsSection = EditorSectionModel( + fields = advancedFields, + isVisible = advancedFields.isNotEmpty() + ), + editorActionsSection = EditorSectionModel( + actions = editorActions, + isVisible = editorActions.isNotEmpty() + ) + ) + } + + private fun computeEnumCorrections( + inputsToShow: List, + currentParameters: Map + ): Map { + val correctedValues = linkedMapOf() + + inputsToShow.forEach { inputDef -> + if (inputDef.staticType != ParameterType.ENUM) return@forEach + + val currentValue = currentParameters[inputDef.id] as? String ?: return@forEach + if (inputDef.options.contains(currentValue)) return@forEach + + val correctedValue = inputDef.normalizeEnumValueOrNull(currentValue) + ?.takeIf { inputDef.options.contains(it) } + ?: inputDef.defaultValue + correctedValues[inputDef.id] = correctedValue + } + + return correctedValues + } +} + +private fun InputDefinition.isVisibleForEditor(currentParameters: Map): Boolean { + visibility?.let { return it.isVisible(currentParameters) } + return !isHidden +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt new file mode 100644 index 00000000..e1d4d272 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/BundleCompat.kt @@ -0,0 +1,23 @@ +package com.chaomixian.vflow.ui.workflow_editor + +import android.os.Build +import android.os.Bundle +import android.os.Parcelable + +internal inline fun Bundle.getParcelableCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelable(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelable(key) + } +} + +internal inline fun Bundle.getParcelableArrayListCompat(key: String): ArrayList? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableArrayList(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableArrayList(key) + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt new file mode 100644 index 00000000..d3a62c79 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParamPath.kt @@ -0,0 +1,29 @@ +package com.chaomixian.vflow.ui.workflow_editor + +internal data class ParamPath( + val rootId: String, + val segments: List +) { + val isTopLevel: Boolean + get() = segments.isEmpty() + + sealed interface Segment { + data class Key(val value: String) : Segment + data class Index(val value: Int) : Segment + } + + companion object { + fun parse(rawPath: String): ParamPath { + val parts = rawPath.split('.') + require(parts.isNotEmpty() && parts.first().isNotBlank()) { + "Parameter path must start with a root id" + } + return ParamPath( + rootId = parts.first(), + segments = parts.drop(1).map { part -> + part.toIntOrNull()?.let(Segment::Index) ?: Segment.Key(part) + } + ) + } + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt new file mode 100644 index 00000000..1e592601 --- /dev/null +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/ParameterTreeEditor.kt @@ -0,0 +1,96 @@ +package com.chaomixian.vflow.ui.workflow_editor + +internal class ParameterTreeEditor( + private val parameters: MutableMap +) { + fun set(path: ParamPath, value: Any?) { + if (path.isTopLevel) { + parameters[path.rootId] = value + return + } + + val updatedRoot = setNested(parameters[path.rootId], path.segments, value) ?: return + parameters[path.rootId] = updatedRoot + } + + fun clear(path: ParamPath, topLevelDefaultValue: Any?) { + if (path.isTopLevel) { + parameters[path.rootId] = topLevelDefaultValue + return + } + + val currentRoot = parameters[path.rootId] ?: return + val updatedRoot = clearNested(currentRoot, path.segments) ?: return + parameters[path.rootId] = updatedRoot + } + + private fun setNested( + currentValue: Any?, + segments: List, + newValue: Any? + ): Any? { + val segment = segments.first() + val remaining = segments.drop(1) + return when (segment) { + is ParamPath.Segment.Key -> { + val map = currentValue.toEditableMap() ?: mutableMapOf() + if (remaining.isEmpty()) { + map[segment.value] = newValue + } else { + val updatedChild = setNested(map[segment.value], remaining, newValue) ?: return null + map[segment.value] = updatedChild + } + map + } + + is ParamPath.Segment.Index -> { + val list = (currentValue as? List<*>)?.toMutableList() ?: return null + if (segment.value !in list.indices) return null + if (remaining.isEmpty()) { + list[segment.value] = newValue + } else { + val updatedChild = setNested(list[segment.value], remaining, newValue) ?: return null + list[segment.value] = updatedChild + } + list + } + } + } + + private fun clearNested( + currentValue: Any?, + segments: List + ): Any? { + val segment = segments.first() + val remaining = segments.drop(1) + return when (segment) { + is ParamPath.Segment.Key -> { + val map = currentValue.toEditableMap() ?: return null + if (remaining.isEmpty()) { + map[segment.value] = "" + } else { + val updatedChild = clearNested(map[segment.value], remaining) ?: return null + map[segment.value] = updatedChild + } + map + } + + is ParamPath.Segment.Index -> { + val list = (currentValue as? List<*>)?.toMutableList() ?: return null + if (segment.value !in list.indices) return null + if (remaining.isEmpty()) { + list[segment.value] = "" + } else { + val updatedChild = clearNested(list[segment.value], remaining) ?: return null + list[segment.value] = updatedChild + } + list + } + } + } + + private fun Any?.toEditableMap(): MutableMap? { + val map = this as? Map<*, *> ?: return null + return map.entries.associate { it.key to it.value }.toMutableMap() + } +} diff --git a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt index 68d49c67..f5818375 100644 --- a/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt +++ b/app/src/main/java/com/chaomixian/vflow/ui/workflow_editor/WorkflowEditorActivity.kt @@ -34,7 +34,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.chaomixian.vflow.R -import com.chaomixian.vflow.core.execution.ExecutionState import com.chaomixian.vflow.core.execution.ExecutionStateBus import com.chaomixian.vflow.core.execution.WorkflowExecutor import com.chaomixian.vflow.core.module.* @@ -66,8 +65,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.gson.Gson -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import java.util.* /** @@ -96,10 +93,8 @@ class WorkflowEditorActivity : BaseActivity() { private var initialWorkflowJson: String? = null private var pendingExecutionWorkflow: Workflow? = null - - // 跟踪当前正在执行的工作流 ID(包括未保存的工作流) - private var currentlyExecutingWorkflowId: String? = null - private var currentlyExecutingExecutionInstanceId: String? = null + private lateinit var executionTracker: WorkflowEditorExecutionTracker + private lateinit var magicVariableCatalogBuilder: WorkflowEditorMagicVariableCatalogBuilder private var listBeforeDrag: List? = null private var dragStartPosition: Int = -1 @@ -129,20 +124,14 @@ class WorkflowEditorActivity : BaseActivity() { ) { result -> if (result.resultCode == Activity.RESULT_OK) { pendingExecutionWorkflow?.let { - // 记录正在执行的工作流 ID,以便正确跟踪状态 - currentlyExecutingWorkflowId = it.id - currentlyExecutingExecutionInstanceId = null + executionTracker.beginExecutionTracking(it.id) toast(getString(R.string.editor_toast_execution_start, it.name)) val executionInstanceId = WorkflowExecutor.execute( workflow = it, context = this, triggerStepId = it.manualTrigger()?.id ) - if (executionInstanceId.isBlank()) { - currentlyExecutingWorkflowId = null - } else { - currentlyExecutingExecutionInstanceId = executionInstanceId - } + executionTracker.finishExecutionLaunch(executionInstanceId) } } pendingExecutionWorkflow = null @@ -206,14 +195,42 @@ class WorkflowEditorActivity : BaseActivity() { applyWindowInsets() workflowManager = WorkflowManager(this) + magicVariableCatalogBuilder = WorkflowEditorMagicVariableCatalogBuilder(this, workflowManager) + bindViews() + setupExecutionTracker() + setupNavigation() + setupRecyclerView() + setupInspectorInsertController() + setupRecyclerFocusDismiss() + setupPickerHandler() + restoreOrLoadEditorState(savedInstanceState) + setupDragAndDrop() + setupUndoControls() + setupPrimaryActions() + observeExecutionState() + } + + private fun bindViews() { nameEditText = findViewById(R.id.edit_text_workflow_name) editorMoreButton = findViewById(R.id.btn_editor_more) undoButton = findViewById(R.id.button_undo_edit) executeButton = findViewById(R.id.button_execute_workflow) recyclerView = findViewById(R.id.recycler_view_action_steps) configureExecuteButtonShadow() + } + private fun setupExecutionTracker() { + executionTracker = WorkflowEditorExecutionTracker( + isWorkflowRunning = WorkflowExecutor::isRunning, + stopWorkflowExecution = WorkflowExecutor::stopExecution, + updateExecuteButton = ::updateExecuteButton, + highlightStep = ::highlightStep, + highlightStepAsFailed = ::highlightStepAsFailed, + clearHighlight = ::clearHighlight + ) + } + private fun setupNavigation() { val toolbar = findViewById(R.id.toolbar_editor) toolbar.setNavigationOnClickListener { handleExitRequest() } onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { @@ -221,8 +238,9 @@ class WorkflowEditorActivity : BaseActivity() { handleExitRequest() } }) + } - setupRecyclerView() + private fun setupInspectorInsertController() { inspectorInsertController = WorkflowInspectorInsertController( activity = this, actionSteps = actionSteps, @@ -231,16 +249,18 @@ class WorkflowEditorActivity : BaseActivity() { onStepsChanged = { recalculateAndNotify() } ) inspectorInsertController.register() + } - // 点击内容区域时清除标题输入框焦点并关闭键盘 + private fun setupRecyclerFocusDismiss() { recyclerView.setOnTouchListener { _, _ -> val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(nameEditText.windowToken, 0) nameEditText.clearFocus() false } + } - // 初始化 PickerHandler + private fun setupPickerHandler() { pickerHandler = PickerHandler( activity = this, appPickerLauncher = generalIntentLauncher, @@ -252,108 +272,70 @@ class WorkflowEditorActivity : BaseActivity() { currentEditorSheet?.updateParametersAndRebuildUi(params) } ) + } - // 检查是否有已保存的状态,如果有则恢复,否则才从数据库加载 + private fun restoreOrLoadEditorState(savedInstanceState: Bundle?) { if (savedInstanceState != null) { - val savedTriggerSteps = savedInstanceState.getParcelableArrayList(STATE_TRIGGER_STEPS) - val savedSteps = savedInstanceState.getParcelableArrayList(STATE_ACTION_STEPS) - if (savedTriggerSteps != null) { - triggerSteps.clear() - triggerSteps.addAll(savedTriggerSteps) - } - if (savedSteps != null) { - actionSteps.clear() - actionSteps.addAll(savedSteps) - } - nameEditText.setText(savedInstanceState.getString(STATE_WORKFLOW_NAME)) - - // 恢复 currentWorkflow 对象以正确显示执行按钮状态 - val workflowId = intent.getStringExtra(EXTRA_WORKFLOW_ID) - if (workflowId != null) { - currentWorkflow = workflowManager.getWorkflow(workflowId) - currentWorkflow?.let { - val isRunning = WorkflowExecutor.isRunning(it.id) - updateExecuteButton(isRunning) - if (isRunning) { - currentlyExecutingWorkflowId = it.id - currentlyExecutingExecutionInstanceId = null - } - initialWorkflowJson = gson.toJson(getCurrentWorkflowState()) - } - } - recalculateAndNotify() // 通知适配器数据已恢复 - } else { - // 首次创建时,从数据库加载数据 - loadWorkflowData() + restoreEditorState(savedInstanceState) + return } + loadWorkflowData() + } - setupDragAndDrop() - setupUndoControls() + private fun restoreEditorState(savedInstanceState: Bundle) { + restoreSavedSteps(savedInstanceState) + nameEditText.setText(savedInstanceState.getString(STATE_WORKFLOW_NAME)) + restoreCurrentWorkflow() + currentWorkflow?.let { initialWorkflowJson = gson.toJson(getCurrentWorkflowState()) } + recalculateAndNotify() + } - // “添加动作”按钮现在只负责添加普通动作,不再处理触发器 - findViewById