diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index 3fa15d6d6d..2fe8f955cb 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -121,3 +121,9 @@ class NeedAddIdsError(BaseHTTPException): error_code = "need_add_ids" description = "Need to add ids." code = 400 + + +class VariableValidationError(BaseHTTPException): + error_code = "variable_validation_error" + description = "Variable validation failed." + code = 400 diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 9759e0815a..6f19931af2 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -11,7 +11,12 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services from controllers.console import console_ns -from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync +from controllers.console.app.error import ( + ConversationCompletedError, + DraftWorkflowNotExist, + DraftWorkflowNotSync, + VariableValidationError, +) from controllers.console.app.workflow_run import workflow_run_node_execution_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required @@ -32,6 +37,7 @@ from dify_graph.enums import NodeType from dify_graph.file.models import File from dify_graph.graph_engine.manager import GraphEngineManager from dify_graph.model_runtime.utils.encoders import jsonable_encoder +from dify_graph.variables.exc import VariableError from extensions.ext_database import db from extensions.ext_redis import redis_client from factories import file_factory, variable_factory @@ -302,6 +308,8 @@ class DraftWorkflowApi(Resource): ) except WorkflowHashNotEqualError: raise DraftWorkflowNotSync() + except VariableError as e: + raise VariableValidationError(description=str(e)) return { "result": "success", diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index 255e5cde83..079e8642f8 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -72,9 +72,18 @@ SEGMENT_TO_VARIABLE_MAP = { } +_MAX_VARIABLE_DESCRIPTION_LENGTH = 255 + + def build_conversation_variable_from_mapping(mapping: Mapping[str, Any], /) -> VariableBase: if not mapping.get("name"): raise VariableError("missing name") + description = mapping.get("description", "") + if len(description) > _MAX_VARIABLE_DESCRIPTION_LENGTH: + raise VariableError( + f"description of variable '{mapping['name']}' is too long" + f" (max {_MAX_VARIABLE_DESCRIPTION_LENGTH} characters)" + ) return _build_variable_from_mapping(mapping=mapping, selector=[CONVERSATION_VARIABLE_NODE_ID, mapping["name"]]) diff --git a/api/models/workflow.py b/api/models/workflow.py index d728ed83bc..b614166173 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -1589,6 +1589,8 @@ class WorkflowDraftVariable(Base): variable.file_id = file_id variable._set_selector(list(variable_utils.to_selector(node_id, name))) variable.node_execution_id = node_execution_id + variable.visible = True + variable.is_default_value = False return variable @classmethod diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index b6f6fc5490..c7b700f6d9 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -700,6 +700,8 @@ def _model_to_insertion_dict(model: WorkflowDraftVariable) -> dict[str, Any]: d["updated_at"] = model.updated_at if model.description is not None: d["description"] = model.description + if model.is_default_value is not None: + d["is_default_value"] = model.is_default_value return d diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index 4042e05565..4815ff40da 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -25,6 +25,7 @@ from services.workflow_draft_variable_service import ( DraftVariableSaver, VariableResetError, WorkflowDraftVariableService, + _model_to_insertion_dict, ) @@ -475,3 +476,37 @@ class TestWorkflowDraftVariableService: assert node_var.visible == True assert node_var.editable == True assert node_var.node_execution_id == "exec-id" + + +class TestModelToInsertionDict: + """Reproduce two production errors in _model_to_insertion_dict / _new().""" + + def test_visible_and_is_default_value_always_present(self): + """Problem 1: _new() did not set visible/is_default_value, causing + inconsistent dict keys across rows in multi-row INSERT and missing + is_default_value in the insertion dict entirely. + """ + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id="app-1", name="counter", value=StringSegment(value="0"), + ) + # _new() should explicitly set these fields so they are not None + assert conv_var.visible is not None + assert conv_var.is_default_value is not None + + d = _model_to_insertion_dict(conv_var) + # visible must appear in every row's dict + assert "visible" in d + # is_default_value must always be present + assert "is_default_value" in d + + def test_description_truncated_to_255(self): + """Problem 2: StringDataRightTruncation — description longer than 255 + chars causes psycopg2 error on varchar(255) column. + """ + long_desc = "a" * 500 + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id="app-1", name="counter", value=StringSegment(value="0"), + description=long_desc, + ) + d = _model_to_insertion_dict(conv_var) + assert len(d["description"]) <= 255 diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 5c07cca3df..3a5a23d23f 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -231,6 +231,8 @@ const ChatVariableModal = ({ } } + const MAX_DESCRIPTION_LENGTH = 255 + const handleSave = () => { if (!checkVariableName(name)) return @@ -241,6 +243,8 @@ const ChatVariableModal = ({ // return notify({ type: 'error', message: 'value can not be empty' }) if (type === ChatVarType.Object && objectValue.some(item => !item.key && !!item.value)) return notify({ type: 'error', message: 'object key can not be empty' }) + if (description.length > MAX_DESCRIPTION_LENGTH) + return notify({ type: 'error', message: t('chatVariable.modal.descriptionTooLong', { ns: 'workflow', maxLength: MAX_DESCRIPTION_LENGTH }) }) onSave({ id: chatVar ? chatVar.id : uuid4(), @@ -273,7 +277,7 @@ const ChatVariableModal = ({
-
+
{!chatVar ? t('chatVariable.modal.title', { ns: 'workflow' }) : t('chatVariable.modal.editTitle', { ns: 'workflow' })}
{/* name */}
-
{t('chatVariable.modal.name', { ns: 'workflow' })}
+
{t('chatVariable.modal.name', { ns: 'workflow' })}
{/* type */}
-
{t('chatVariable.modal.type', { ns: 'workflow' })}
+
{t('chatVariable.modal.type', { ns: 'workflow' })}
{/* default value */}
-
+
{t('chatVariable.modal.value', { ns: 'workflow' })}
{(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber || type === ChatVarType.ArrayBoolean) && (
{/* description */}
-
{t('chatVariable.modal.description', { ns: 'workflow' })}
+
{t('chatVariable.modal.description', { ns: 'workflow' })}