From 9dd0361d0e0cb94ecfdd8bde511e0ddeb0d96a31 Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 12 Jan 2026 01:53:26 +0800 Subject: [PATCH] refactor: rename new runtime as sandbox feature --- .../app/apps/advanced_chat/app_generator.py | 5 ++-- api/core/app/apps/workflow/app_generator.py | 5 ++-- api/models/__init__.py | 3 +++ api/models/workflow.py | 5 +++- api/models/workflow_features.py | 26 +++++++++++++++++++ api/services/workflow_service.py | 5 ++-- web/app/components/base/features/store.ts | 2 +- web/app/components/base/features/types.ts | 4 +-- .../hooks/use-nodes-sync-draft.ts | 2 +- .../workflow-app/hooks/use-workflow-init.ts | 2 +- .../workflow-app/hooks/use-workflow-run.ts | 2 +- web/app/components/workflow-app/index.tsx | 2 +- .../components/workflow/update-dsl-modal.tsx | 2 +- 13 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 api/models/workflow_features.py diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 08d9f59acf..2534c553f2 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -41,6 +41,7 @@ from factories import file_factory from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom +from models.workflow_features import WorkflowFeatures from services.conversation_service import ConversationService from services.workflow_draft_variable_service import ( DraftVarLoader, @@ -513,10 +514,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): if workflow is None: raise ValueError("Workflow not found") - # FIXME:(sandbox) Consolidate runtime config checking into a unified location. - runtime = workflow.features_dict.get("runtime") graph_engine_layers: tuple = () - if isinstance(runtime, dict) and runtime.get("enabled"): + if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled: graph_engine_layers = (SandboxLayer(tenant_id=application_generate_entity.app_config.tenant_id),) # Determine system_user_id based on invocation source diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index a6fe9694a4..5f881c2e39 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -38,6 +38,7 @@ from factories import file_factory from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom +from models.workflow_features import WorkflowFeatures from services.workflow_draft_variable_service import DraftVarLoader, WorkflowDraftVariableService SKIP_PREPARE_USER_INPUTS_KEY = "_skip_prepare_user_inputs" @@ -488,9 +489,7 @@ class WorkflowAppGenerator(BaseAppGenerator): if workflow is None: raise ValueError("Workflow not found") - # FIXME:(sandbox) Consolidate runtime config checking into a unified location. - runtime = workflow.features_dict.get("runtime") - if isinstance(runtime, dict) and runtime.get("enabled"): + if workflow.get_feature(WorkflowFeatures.SANDBOX).enabled: graph_engine_layers = ( *graph_engine_layers, SandboxLayer(tenant_id=application_generate_entity.app_config.tenant_id), diff --git a/api/models/__init__.py b/api/models/__init__.py index 62e1e579b4..3fba153f8a 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -109,6 +109,7 @@ from .workflow import ( WorkflowRun, WorkflowType, ) +from .workflow_features import WorkflowFeature, WorkflowFeatures __all__ = [ "APIBasedExtension", @@ -202,6 +203,8 @@ __all__ = [ "Workflow", "WorkflowAppLog", "WorkflowAppLogCreatedFrom", + "WorkflowFeature", + "WorkflowFeatures", "WorkflowNodeExecutionModel", "WorkflowNodeExecutionOffload", "WorkflowNodeExecutionTriggeredFrom", diff --git a/api/models/workflow.py b/api/models/workflow.py index 7e8a0f7c2e..eff37829b5 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -37,13 +37,13 @@ from extensions.ext_storage import Storage from factories.variable_factory import TypeMismatchError, build_segment_with_type from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 +from models.workflow_features import WorkflowFeature, WorkflowFeatures from ._workflow_exc import NodeNotFoundError, WorkflowDataError if TYPE_CHECKING: from .model import AppMode, UploadFile - from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from core.helper import encrypter from core.variables import SecretVariable, Segment, SegmentType, Variable @@ -345,6 +345,9 @@ class Workflow(Base): # bug def features_dict(self) -> dict[str, Any]: return json.loads(self.features) if self.features else {} + def get_feature(self, key: WorkflowFeatures) -> WorkflowFeature: + return WorkflowFeature.from_dict(self.features_dict.get(key.value)) + def walk_nodes( self, specific_node_type: NodeType | None = None ) -> Generator[tuple[str, Mapping[str, Any]], None, None]: diff --git a/api/models/workflow_features.py b/api/models/workflow_features.py new file mode 100644 index 0000000000..81fc0fa11a --- /dev/null +++ b/api/models/workflow_features.py @@ -0,0 +1,26 @@ +from collections.abc import Mapping +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + + +class WorkflowFeatures(StrEnum): + SANDBOX = "sandbox" + SPEECH_TO_TEXT = "speech_to_text" + TEXT_TO_SPEECH = "text_to_speech" + RETRIEVER_RESOURCE = "retriever_resource" + SENSITIVE_WORD_AVOIDANCE = "sensitive_word_avoidance" + FILE_UPLOAD = "file_upload" + SUGGESTED_QUESTIONS_AFTER_ANSWER = "suggested_questions_after_answer" + + +@dataclass(frozen=True) +class WorkflowFeature: + enabled: bool + config: Mapping[str, Any] + + @classmethod + def from_dict(cls, data: Mapping[str, Any] | None) -> "WorkflowFeature": + if data is None or not isinstance(data, dict): + return cls(enabled=False, config={}) + return cls(enabled=bool(data.get("enabled", False)), config=data) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index a8a54ec3f6..a7b2aad0a7 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -38,6 +38,7 @@ from models import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom, WorkflowType +from models.workflow_features import WorkflowFeatures from repositories.factory import DifyAPIRepositoryFactory from services.billing_service import BillingService from services.enterprise.plugin_manager_service import PluginCredentialType @@ -697,11 +698,9 @@ class WorkflowService: else: enclosing_node_id = None - # TODO: Consolidate runtime config checking into a unified location. - runtime = draft_workflow.features_dict.get("runtime") sandbox = None single_step_execution_id: str | None = None - if isinstance(runtime, dict) and runtime.get("enabled"): + if draft_workflow.get_feature(WorkflowFeatures.SANDBOX).enabled: sandbox = SandboxProviderService.create_sandbox(tenant_id=draft_workflow.tenant_id) single_step_execution_id = f"single-step-{uuid.uuid4()}" from core.virtual_environment.sandbox_manager import SandboxManager diff --git a/web/app/components/base/features/store.ts b/web/app/components/base/features/store.ts index d77eeac7cc..2f3d54aec0 100644 --- a/web/app/components/base/features/store.ts +++ b/web/app/components/base/features/store.ts @@ -54,7 +54,7 @@ export const createFeaturesStore = (initProps?: Partial) => { annotationReply: { enabled: false, }, - runtime: { + sandbox: { enabled: false, }, }, diff --git a/web/app/components/base/features/types.ts b/web/app/components/base/features/types.ts index 37f8997f10..f34ad44406 100644 --- a/web/app/components/base/features/types.ts +++ b/web/app/components/base/features/types.ts @@ -91,7 +91,7 @@ export enum FeatureEnum { moderation = 'moderation', file = 'file', annotationReply = 'annotationReply', - runtime = 'runtime', + sandbox = 'sandbox', } export type Features = { @@ -104,7 +104,7 @@ export type Features = { [FeatureEnum.moderation]?: SensitiveWordAvoidance [FeatureEnum.file]?: FileUpload [FeatureEnum.annotationReply]?: AnnotationReplyConfig - [FeatureEnum.runtime]?: Runtime + [FeatureEnum.sandbox]?: Runtime } export type OnFeaturesChange = (features?: Features) => void diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index f6b774863a..97ba72f679 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -71,7 +71,7 @@ export const useNodesSyncDraft = () => { retriever_resource: features.citation, sensitive_word_avoidance: features.moderation, file_upload: features.file, - runtime: features.runtime, + sandbox: features.sandbox, }, environment_variables: environmentVariables, conversation_variables: conversationVariables, diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index 6507d9e413..db9be7dfc2 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -99,7 +99,7 @@ export const useWorkflowInit = () => { }, features: { retriever_resource: { enabled: true }, - runtime: { enabled: enableSandboxRuntime }, + sandbox: { enabled: enableSandboxRuntime }, }, environment_variables: [], conversation_variables: [], diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 64ce413e4d..7628e685e2 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -748,7 +748,7 @@ export const useWorkflowRun = () => { citation: publishedWorkflow.features.retriever_resource, moderation: publishedWorkflow.features.sensitive_word_avoidance, file: publishedWorkflow.features.file_upload, - runtime: publishedWorkflow.features.runtime || { enabled: false }, + sandbox: publishedWorkflow.features.sandbox || { enabled: false }, } featuresStore?.setState({ features: mappedFeatures }) diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 3d1aa278c3..ed16f06212 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -192,7 +192,7 @@ const WorkflowAppWithAdditionalContext = () => { text2speech: features.text_to_speech || { enabled: false }, citation: features.retriever_resource || { enabled: false }, moderation: features.sensitive_word_avoidance || { enabled: false }, - runtime: features.runtime || { enabled: false }, + sandbox: features.sandbox || { enabled: false }, } return ( diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx index 1de7b55361..bcca995a1d 100644 --- a/web/app/components/workflow/update-dsl-modal.tsx +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -121,7 +121,7 @@ const UpdateDSLModal = ({ text2speech: features.text_to_speech || { enabled: false }, citation: features.retriever_resource || { enabled: false }, moderation: features.sensitive_word_avoidance || { enabled: false }, - runtime: features.runtime || { enabled: false }, + sandbox: features.sandbox || { enabled: false }, } eventEmitter?.emit({