diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index ba93f82756..ef31a5a9bb 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -17,8 +17,8 @@ from core.variables.segment_group import SegmentGroup from core.variables.segments import ArrayFileSegment, FileSegment, Segment from core.variables.types import SegmentType from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from factories import variable_factory from factories.file_factory import build_from_mapping, build_from_mappings -from factories.variable_factory import build_segment_with_type from libs.login import current_user, login_required from models import App, AppMode, db from models.workflow import WorkflowDraftVariable @@ -295,7 +295,7 @@ class VariableApi(Resource): if len(raw_value) > 0 and not isinstance(raw_value[0], dict): raise InvalidArgumentError(description=f"expected dict for files[0], got {type(raw_value)}") raw_value = build_from_mappings(mappings=raw_value, tenant_id=app_model.tenant_id) - new_value = build_segment_with_type(variable.value_type, raw_value) + new_value = variable_factory.build_segment_with_type(variable.value_type, raw_value) draft_var_srv.update_variable(variable, name=new_name, value=new_value) db.session.commit() return variable @@ -411,6 +411,34 @@ class EnvironmentVariableCollectionApi(Resource): ) return {"items": env_vars_list} + + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + def post(self, app_model: App): + # The role of the current user in the ta table must be admin, owner, or editor + if not current_user.is_editor: + raise Forbidden() + + parser = reqparse.RequestParser() + parser.add_argument("environment_variables", type=list, required=True, location="json") + args = parser.parse_args() + + workflow_service = WorkflowService() + + environment_variables_list = args.get("environment_variables") or [] + environment_variables = [ + variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list + ] + + workflow_service.update_draft_workflow_environment_variables( + app_model=app_model, + account=current_user, + environment_variables=environment_variables, + ) + + return { "result": "success" } api.add_resource( diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 677bc74237..499b86360b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -244,6 +244,28 @@ class WorkflowService: # return draft workflow return workflow + + def update_draft_workflow_environment_variables( + self, *, + app_model: App, + environment_variables: Sequence[Variable], + account: Account, + ): + """ + Update draft workflow environment variables + """ + # fetch draft workflow by app_model + workflow = self.get_draft_workflow(app_model=app_model) + + if not workflow: + raise ValueError("No draft workflow found.") + + workflow.environment_variables = environment_variables + workflow.updated_by = account.id + workflow.updated_at = datetime.now(UTC).replace(tzinfo=None) + + # commit db session changes + db.session.commit() def publish_workflow( self, diff --git a/web/app/components/workflow/panel/env-panel/index.tsx b/web/app/components/workflow/panel/env-panel/index.tsx index 052a80bd59..2f11174ef0 100644 --- a/web/app/components/workflow/panel/env-panel/index.tsx +++ b/web/app/components/workflow/panel/env-panel/index.tsx @@ -17,9 +17,9 @@ import type { import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils' import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm' import cn from '@/utils/classnames' -import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' import { useStore as useWorkflowStore } from '@/app/components/workflow/store' +import { updateEnvironmentVariables } from '@/service/workflow' const EnvPanel = () => { const { t } = useTranslation() @@ -29,7 +29,6 @@ const EnvPanel = () => { const envSecrets = useStore(s => s.envSecrets) const updateEnvList = useStore(s => s.setEnvironmentVariables) const setEnvSecrets = useStore(s => s.setEnvSecrets) - const { doSyncWorkflowDraft } = useNodesSyncDraft() const appId = useWorkflowStore(s => s.appId) const [showVariableModal, setShowVariableModal] = useState(false) @@ -70,18 +69,31 @@ const EnvPanel = () => { const handleDelete = useCallback(async (env: EnvironmentVariable) => { removeUsedVarInNodes(env) - updateEnvList(envList.filter(e => e.id !== env.id)) + const newEnvList = envList.filter(e => e.id !== env.id) + updateEnvList(newEnvList) setCacheForDelete(undefined) setShowRemoveConfirm(false) - await doSyncWorkflowDraft() - // Emit update event to other connected clients - const socket = webSocketClient.getSocket(appId) - if (socket?.connected) { - socket.emit('collaboration_event', { - type: 'varsAndFeaturesUpdate', - timestamp: Date.now(), + // Use new dedicated environment variables API instead of workflow draft sync + try { + await updateEnvironmentVariables({ + appId, + environmentVariables: newEnvList, }) + + // Emit update event to other connected clients + const socket = webSocketClient.getSocket(appId) + if (socket?.connected) { + socket.emit('collaboration_event', { + type: 'varsAndFeaturesUpdate', + timestamp: Date.now(), + }) + } + } + catch (error) { + console.error('Failed to update environment variables:', error) + // Revert local state on error + updateEnvList(envList) } if (env.value_type === 'secret') { @@ -89,7 +101,7 @@ const EnvPanel = () => { delete newMap[env.id] setEnvSecrets(newMap) } - }, [doSyncWorkflowDraft, envList, envSecrets, removeUsedVarInNodes, setEnvSecrets, updateEnvList, appId]) + }, [envList, envSecrets, removeUsedVarInNodes, setEnvSecrets, updateEnvList, appId]) const deleteCheck = useCallback((env: EnvironmentVariable) => { const effectedNodes = getEffectedNodes(env) @@ -105,26 +117,46 @@ const EnvPanel = () => { const handleSave = useCallback(async (env: EnvironmentVariable) => { // add env let newEnv = env + let newList: EnvironmentVariable[] + if (!currentVar) { + // Adding new environment variable if (env.value_type === 'secret') { setEnvSecrets({ ...envSecrets, [env.id]: formatSecret(env.value), }) } - const newList = [env, ...envList] + newList = [env, ...envList] updateEnvList(newList) - await doSyncWorkflowDraft() - const socket = webSocketClient.getSocket(appId) - if (socket) { - socket.emit('collaboration_event', { - type: 'varsAndFeaturesUpdate', + + // Use new dedicated environment variables API + try { + await updateEnvironmentVariables({ + appId, + environmentVariables: newList, }) + + const socket = webSocketClient.getSocket(appId) + if (socket) { + socket.emit('collaboration_event', { + type: 'varsAndFeaturesUpdate', + }) + } + + // Hide secret values in UI + updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e)) + } + catch (error) { + console.error('Failed to update environment variables:', error) + // Revert local state on error + updateEnvList(envList) } - updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e)) return } - else if (currentVar.value_type === 'secret') { + + // Updating existing environment variable + if (currentVar.value_type === 'secret') { if (env.value_type === 'secret') { if (envSecrets[currentVar.id] !== env.value) { newEnv = env @@ -147,8 +179,10 @@ const EnvPanel = () => { }) } } - const newList = envList.map(e => e.id === currentVar.id ? newEnv : e) + + newList = envList.map(e => e.id === currentVar.id ? newEnv : e) updateEnvList(newList) + // side effects of rename env if (currentVar.name !== env.name) { const { getNodes, setNodes } = store.getState() @@ -161,15 +195,30 @@ const EnvPanel = () => { }) setNodes(newNodes) } - await doSyncWorkflowDraft() - const socket = webSocketClient.getSocket(appId) - if (socket) { - socket.emit('collaboration_event', { - type: 'varsAndFeaturesUpdate', + + // Use new dedicated environment variables API + try { + await updateEnvironmentVariables({ + appId, + environmentVariables: newList, }) + + const socket = webSocketClient.getSocket(appId) + if (socket) { + socket.emit('collaboration_event', { + type: 'varsAndFeaturesUpdate', + }) + } + + // Hide secret values in UI + updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e)) } - updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e)) - }, [currentVar, doSyncWorkflowDraft, envList, envSecrets, getEffectedNodes, setEnvSecrets, store, updateEnvList, appId]) + catch (error) { + console.error('Failed to update environment variables:', error) + // Revert local state on error + updateEnvList(envList) + } + }, [currentVar, envList, envSecrets, getEffectedNodes, setEnvSecrets, store, updateEnvList, appId]) return (