From 3902929d9f73e8e32741c29372abd063208ec0f9 Mon Sep 17 00:00:00 2001 From: Harry Date: Wed, 7 Jan 2026 00:01:55 +0800 Subject: [PATCH] feat: new runtime options --- .../app/apps/advanced_chat/app_generator.py | 7 +++ api/core/app/apps/workflow/app_generator.py | 5 ++ api/core/workflow/nodes/command/node.py | 4 -- .../nodes/command/test_command_node.py | 9 ++- .../components/app/create-app-modal/index.tsx | 61 ++++++++++++++++++- web/app/components/base/features/store.ts | 3 + web/app/components/base/features/types.ts | 6 ++ .../hooks/use-nodes-sync-draft.ts | 1 + .../workflow-app/hooks/use-workflow-init.ts | 6 ++ .../workflow-app/hooks/use-workflow-run.ts | 1 + web/app/components/workflow-app/index.tsx | 1 + .../components/workflow/update-dsl-modal.tsx | 1 + web/i18n/en-US/app.json | 4 ++ web/i18n/ja-JP/app.json | 3 + web/i18n/zh-Hans/app.json | 3 + 15 files changed, 108 insertions(+), 7 deletions(-) diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index feb0d3358c..3a348532e4 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -24,6 +24,7 @@ from core.app.apps.message_based_app_generator import MessageBasedAppGenerator from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse +from core.app.layers.sandbox_layer import SandboxLayer from core.helper.trace_id_helper import extract_external_trace_id_from_args from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager @@ -512,6 +513,11 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): if workflow is None: raise ValueError("Workflow not found") + runtime = workflow.features_dict.get("runtime") + graph_engine_layers = () + if isinstance(runtime, dict) and runtime.get("enabled"): + graph_engine_layers = (SandboxLayer(),) + # Determine system_user_id based on invocation source is_external_api_call = application_generate_entity.invoke_from in { InvokeFrom.WEB_APP, @@ -542,6 +548,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): app=app, workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, + graph_engine_layers=graph_engine_layers, ) try: diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 0165c74295..197d988a7b 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -23,6 +23,7 @@ from core.app.apps.workflow.generate_response_converter import WorkflowAppGenera from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse +from core.app.layers.sandbox_layer import SandboxLayer from core.helper.trace_id_helper import extract_external_trace_id_from_args from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager @@ -487,6 +488,10 @@ class WorkflowAppGenerator(BaseAppGenerator): if workflow is None: raise ValueError("Workflow not found") + runtime = workflow.features_dict.get("runtime") + if isinstance(runtime, dict) and runtime.get("enabled"): + graph_engine_layers = (*graph_engine_layers, SandboxLayer()) + # Determine system_user_id based on invocation source is_external_api_call = application_generate_entity.invoke_from in { InvokeFrom.WEB_APP, diff --git a/api/core/workflow/nodes/command/node.py b/api/core/workflow/nodes/command/node.py index 64f5f83d91..e03ee18c90 100644 --- a/api/core/workflow/nodes/command/node.py +++ b/api/core/workflow/nodes/command/node.py @@ -126,10 +126,6 @@ class CommandNode(Node[CommandNodeData]): connection_handle, command ) - # This node currently does not support interactive stdin. - with contextlib.suppress(Exception): - stdin_transport.close() - is_combined_stream = stdout_transport is stderr_transport stdout_thread = threading.Thread( diff --git a/api/tests/unit_tests/core/workflow/nodes/command/test_command_node.py b/api/tests/unit_tests/core/workflow/nodes/command/test_command_node.py index 84dbf13c6b..5aeae28c18 100644 --- a/api/tests/unit_tests/core/workflow/nodes/command/test_command_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/command/test_command_node.py @@ -1,5 +1,6 @@ import time from io import BytesIO +from typing import Any from core.virtual_environment.__base.entities import Arch, CommandStatus, ConnectionHandle, FileState, Metadata from core.virtual_environment.__base.virtual_environment import VirtualEnvironment @@ -139,12 +140,16 @@ def test_command_node_nonzero_exit_code_returns_failed_result(): assert "exited with code" in result.error -def test_command_node_timeout_returns_failed_result_and_closes_transports(): +def test_command_node_timeout_returns_failed_result_and_closes_transports(monkeypatch: Any): + from core.workflow.nodes.command import node as command_node_module + + monkeypatch.setattr(command_node_module, "COMMAND_NODE_TIMEOUT_SECONDS", 1) + node = _make_node(command="sleep 10") sandbox = FakeSandbox( stdout=b"", stderr=b"", - statuses=[CommandStatus(status=CommandStatus.Status.RUNNING, exit_code=None)] * 100, + statuses=[CommandStatus(status=CommandStatus.Status.RUNNING, exit_code=None)] * 1000, close_streams=False, ) node.sandbox = sandbox diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 6e8c94aea6..1ecc386906 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -39,6 +39,10 @@ type CreateAppProps = { defaultAppMode?: AppModeEnum } +type RuntimeMode = 'classical' | 'new' + +const WORKFLOW_RUNTIME_STORAGE_KEY_PREFIX = 'workflow:sandbox-runtime:' + function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: CreateAppProps) { const { t } = useTranslation() const { push } = useRouter() @@ -50,6 +54,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: const [name, setName] = useState('') const [description, setDescription] = useState('') const [isAppTypeExpanded, setIsAppTypeExpanded] = useState(false) + const [runtimeMode, setRuntimeMode] = useState('classical') const { plan, enableBilling } = useProviderContext() const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) @@ -60,6 +65,9 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: useEffect(() => { if (appMode === AppModeEnum.CHAT || appMode === AppModeEnum.AGENT_CHAT || appMode === AppModeEnum.COMPLETION) setIsAppTypeExpanded(true) + + if (appMode !== AppModeEnum.WORKFLOW && appMode !== AppModeEnum.ADVANCED_CHAT) + setRuntimeMode('classical') }, [appMode]) const onCreate = useCallback(async () => { @@ -84,6 +92,9 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: mode: appMode, }) + if (runtimeMode === 'new' && (appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT)) + localStorage.setItem(`${WORKFLOW_RUNTIME_STORAGE_KEY_PREFIX}${app.id}`, '1') + // Track app creation success trackEvent('create_app', { app_mode: appMode, @@ -103,7 +114,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: }) } isCreatingRef.current = false - }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor]) + }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, push, isCurrentWorkspaceEditor, runtimeMode]) const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) useKeyPress(['meta.enter', 'ctrl.enter'], () => { @@ -258,6 +269,54 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: onChange={e => setDescription(e.target.value)} /> + + {(appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT) && ( +
+
+ {t('newApp.runtimeLabel', { ns: 'app' })} +
+
+ + + +
+
+ )} + {isAppsFull && }
diff --git a/web/app/components/base/features/store.ts b/web/app/components/base/features/store.ts index 7a4fe47663..d77eeac7cc 100644 --- a/web/app/components/base/features/store.ts +++ b/web/app/components/base/features/store.ts @@ -54,6 +54,9 @@ export const createFeaturesStore = (initProps?: Partial) => { annotationReply: { enabled: false, }, + runtime: { + enabled: false, + }, }, } return createStore()(set => ({ diff --git a/web/app/components/base/features/types.ts b/web/app/components/base/features/types.ts index f576a4f5d4..37f8997f10 100644 --- a/web/app/components/base/features/types.ts +++ b/web/app/components/base/features/types.ts @@ -77,6 +77,10 @@ export type AnnotationReplyConfig = { } } +export type Runtime = { + enabled: boolean +} + export enum FeatureEnum { moreLikeThis = 'moreLikeThis', opening = 'opening', @@ -87,6 +91,7 @@ export enum FeatureEnum { moderation = 'moderation', file = 'file', annotationReply = 'annotationReply', + runtime = 'runtime', } export type Features = { @@ -99,6 +104,7 @@ export type Features = { [FeatureEnum.moderation]?: SensitiveWordAvoidance [FeatureEnum.file]?: FileUpload [FeatureEnum.annotationReply]?: AnnotationReplyConfig + [FeatureEnum.runtime]?: 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 5d394bab1e..f6b774863a 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,6 +71,7 @@ export const useNodesSyncDraft = () => { retriever_resource: features.citation, sensitive_word_avoidance: features.moderation, file_upload: features.file, + runtime: features.runtime, }, 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 8e976937b5..6507d9e413 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -85,6 +85,11 @@ export const useWorkflowInit = () => { const nodesData = isAdvancedChat ? nodesTemplate : [] const edgesData = isAdvancedChat ? edgesTemplate : [] + const runtimeStorageKey = `workflow:sandbox-runtime:${appDetail.id}` + const enableSandboxRuntime = localStorage.getItem(runtimeStorageKey) === '1' + if (enableSandboxRuntime) + localStorage.removeItem(runtimeStorageKey) + syncWorkflowDraft({ url: `/apps/${appDetail.id}/workflows/draft`, params: { @@ -94,6 +99,7 @@ export const useWorkflowInit = () => { }, features: { retriever_resource: { enabled: true }, + runtime: { 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 49c5d20dde..64ce413e4d 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -748,6 +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 }, } featuresStore?.setState({ features: mappedFeatures }) diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 6a778ab6b8..3d1aa278c3 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -192,6 +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 }, } return ( diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx index d33679ff1b..1de7b55361 100644 --- a/web/app/components/workflow/update-dsl-modal.tsx +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -121,6 +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 }, } eventEmitter?.emit({ diff --git a/web/i18n/en-US/app.json b/web/i18n/en-US/app.json index e4109db4b6..043bdcecd6 100644 --- a/web/i18n/en-US/app.json +++ b/web/i18n/en-US/app.json @@ -159,6 +159,7 @@ "newApp.completionShortDescription": "AI assistant for text generation tasks", "newApp.completionUserDescription": "Quickly build an AI assistant for text generation tasks with simple configuration.", "newApp.dropDSLToCreateApp": "Drop DSL file here to create app", + "newApp.enableSandboxRuntime": "Enable sandbox runtime (supports Command node)", "newApp.forAdvanced": "FOR ADVANCED USERS", "newApp.forBeginners": "More basic app types", "newApp.foundResult": "{{count}} Result", @@ -173,6 +174,9 @@ "newApp.noTemplateFoundTip": "Try searching using different keywords.", "newApp.optional": "Optional", "newApp.previewDemo": "Preview demo", + "newApp.runtimeLabel": "Runtime", + "newApp.runtimeOptionClassical": "Classical", + "newApp.runtimeOptionNew": "Recommend", "newApp.showTemplates": "I want to choose from a template", "newApp.startFromBlank": "Create from Blank", "newApp.startFromTemplate": "Create from Template", diff --git a/web/i18n/ja-JP/app.json b/web/i18n/ja-JP/app.json index e9ac621607..2990f634b6 100644 --- a/web/i18n/ja-JP/app.json +++ b/web/i18n/ja-JP/app.json @@ -173,6 +173,9 @@ "newApp.noTemplateFoundTip": "別のキーワードを使用して検索してみてください。", "newApp.optional": "任意", "newApp.previewDemo": "デモをプレビュー", + "newApp.runtimeLabel": "Runtime", + "newApp.runtimeOptionClassical": "Classical", + "newApp.runtimeOptionNew": "推奨", "newApp.showTemplates": "テンプレートから選択したい", "newApp.startFromBlank": "最初から作成", "newApp.startFromTemplate": "テンプレートから作成", diff --git a/web/i18n/zh-Hans/app.json b/web/i18n/zh-Hans/app.json index ee60cd3413..7d2cf541d4 100644 --- a/web/i18n/zh-Hans/app.json +++ b/web/i18n/zh-Hans/app.json @@ -173,6 +173,9 @@ "newApp.noTemplateFoundTip": "请尝试使用不同的关键字进行搜索。", "newApp.optional": "可选", "newApp.previewDemo": "预览 Demo", + "newApp.runtimeLabel": "Runtime", + "newApp.runtimeOptionClassical": "Classical", + "newApp.runtimeOptionNew": "推荐", "newApp.showTemplates": "我想从范例模板中选择", "newApp.startFromBlank": "创建空白应用", "newApp.startFromTemplate": "从应用模板创建",