feat: new runtime options

This commit is contained in:
Harry 2026-01-07 00:01:55 +08:00
parent 1c7c475c43
commit 3902929d9f
15 changed files with 108 additions and 7 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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<RuntimeMode>('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)}
/>
</div>
{(appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT) && (
<div>
<div className="system-sm-semibold mb-2 text-text-secondary">
{t('newApp.runtimeLabel', { ns: 'app' })}
</div>
<div className="flex gap-3">
<button
type="button"
className={cn(
'flex h-10 flex-1 items-center gap-2 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3',
runtimeMode === 'new' && 'border-components-option-card-option-border-selected bg-components-option-card-option-selected-bg',
)}
onClick={() => setRuntimeMode('new')}
>
<div
className={cn(
'h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
runtimeMode === 'new' && 'border-[5px] border-components-radio-border-checked',
)}
/>
<div className="system-sm-medium text-text-secondary">
{t('newApp.runtimeOptionNew', { ns: 'app' })}
</div>
</button>
<button
type="button"
className={cn(
'flex h-10 flex-1 items-center gap-2 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3',
runtimeMode === 'classical' && 'border-components-option-card-option-border-selected bg-components-option-card-option-selected-bg',
)}
onClick={() => setRuntimeMode('classical')}
>
<div
className={cn(
'h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
runtimeMode === 'classical' && 'border-[5px] border-components-radio-border-checked',
)}
/>
<div className="system-sm-medium text-text-secondary">
{t('newApp.runtimeOptionClassical', { ns: 'app' })}
</div>
</button>
</div>
</div>
)}
</div>
{isAppsFull && <AppsFull className="mt-4" loc="app-create" />}
<div className="flex items-center justify-between pb-10 pt-5">

View File

@ -54,6 +54,9 @@ export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
annotationReply: {
enabled: false,
},
runtime: {
enabled: false,
},
},
}
return createStore<FeatureStoreState>()(set => ({

View File

@ -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

View File

@ -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,

View File

@ -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: [],

View File

@ -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 })

View File

@ -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 (

View File

@ -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({

View File

@ -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",

View File

@ -173,6 +173,9 @@
"newApp.noTemplateFoundTip": "別のキーワードを使用して検索してみてください。",
"newApp.optional": "任意",
"newApp.previewDemo": "デモをプレビュー",
"newApp.runtimeLabel": "Runtime",
"newApp.runtimeOptionClassical": "Classical",
"newApp.runtimeOptionNew": "推奨",
"newApp.showTemplates": "テンプレートから選択したい",
"newApp.startFromBlank": "最初から作成",
"newApp.startFromTemplate": "テンプレートから作成",

View File

@ -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": "从应用模板创建",