|  | @@ -1,8 +1,11 @@
 | 
	
		
			
				|  |  |  import logging
 | 
	
		
			
				|  |  | +from collections.abc import Mapping
 | 
	
		
			
				|  |  | +from typing import Any
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -import httpx
 | 
	
		
			
				|  |  | -import yaml  # type: ignore
 | 
	
		
			
				|  |  | +import yaml
 | 
	
		
			
				|  |  | +from packaging import version
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +from core.helper import ssrf_proxy
 | 
	
		
			
				|  |  |  from events.app_event import app_model_config_was_updated, app_was_created
 | 
	
		
			
				|  |  |  from extensions.ext_database import db
 | 
	
		
			
				|  |  |  from factories import variable_factory
 | 
	
	
		
			
				|  | @@ -11,6 +14,18 @@ from models.model import App, AppMode, AppModelConfig
 | 
	
		
			
				|  |  |  from models.workflow import Workflow
 | 
	
		
			
				|  |  |  from services.workflow_service import WorkflowService
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +from .exc import (
 | 
	
		
			
				|  |  | +    ContentDecodingError,
 | 
	
		
			
				|  |  | +    DSLVersionNotSupportedError,
 | 
	
		
			
				|  |  | +    EmptyContentError,
 | 
	
		
			
				|  |  | +    FileSizeLimitExceededError,
 | 
	
		
			
				|  |  | +    InvalidAppModeError,
 | 
	
		
			
				|  |  | +    InvalidYAMLFormatError,
 | 
	
		
			
				|  |  | +    MissingAppDataError,
 | 
	
		
			
				|  |  | +    MissingModelConfigError,
 | 
	
		
			
				|  |  | +    MissingWorkflowDataError,
 | 
	
		
			
				|  |  | +)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  logger = logging.getLogger(__name__)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  current_dsl_version = "0.1.2"
 | 
	
	
		
			
				|  | @@ -30,32 +45,21 @@ class AppDslService:
 | 
	
		
			
				|  |  |          :param args: request args
 | 
	
		
			
				|  |  |          :param account: Account instance
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  | -        try:
 | 
	
		
			
				|  |  | -            max_size = 10 * 1024 * 1024  # 10MB
 | 
	
		
			
				|  |  | -            timeout = httpx.Timeout(10.0)
 | 
	
		
			
				|  |  | -            with httpx.stream("GET", url.strip(), follow_redirects=True, timeout=timeout) as response:
 | 
	
		
			
				|  |  | -                response.raise_for_status()
 | 
	
		
			
				|  |  | -                total_size = 0
 | 
	
		
			
				|  |  | -                content = b""
 | 
	
		
			
				|  |  | -                for chunk in response.iter_bytes():
 | 
	
		
			
				|  |  | -                    total_size += len(chunk)
 | 
	
		
			
				|  |  | -                    if total_size > max_size:
 | 
	
		
			
				|  |  | -                        raise ValueError("File size exceeds the limit of 10MB")
 | 
	
		
			
				|  |  | -                    content += chunk
 | 
	
		
			
				|  |  | -        except httpx.HTTPStatusError as http_err:
 | 
	
		
			
				|  |  | -            raise ValueError(f"HTTP error occurred: {http_err}")
 | 
	
		
			
				|  |  | -        except httpx.RequestError as req_err:
 | 
	
		
			
				|  |  | -            raise ValueError(f"Request error occurred: {req_err}")
 | 
	
		
			
				|  |  | -        except Exception as e:
 | 
	
		
			
				|  |  | -            raise ValueError(f"Failed to fetch DSL from URL: {e}")
 | 
	
		
			
				|  |  | +        max_size = 10 * 1024 * 1024  # 10MB
 | 
	
		
			
				|  |  | +        response = ssrf_proxy.get(url.strip(), follow_redirects=True, timeout=(10, 10))
 | 
	
		
			
				|  |  | +        response.raise_for_status()
 | 
	
		
			
				|  |  | +        content = response.content
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if len(content) > max_size:
 | 
	
		
			
				|  |  | +            raise FileSizeLimitExceededError("File size exceeds the limit of 10MB")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if not content:
 | 
	
		
			
				|  |  | -            raise ValueError("Empty content from url")
 | 
	
		
			
				|  |  | +            raise EmptyContentError("Empty content from url")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  |              data = content.decode("utf-8")
 | 
	
		
			
				|  |  |          except UnicodeDecodeError as e:
 | 
	
		
			
				|  |  | -            raise ValueError(f"Error decoding content: {e}")
 | 
	
		
			
				|  |  | +            raise ContentDecodingError(f"Error decoding content: {e}")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          return cls.import_and_create_new_app(tenant_id, data, args, account)
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -71,14 +75,14 @@ class AppDslService:
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  |              import_data = yaml.safe_load(data)
 | 
	
		
			
				|  |  |          except yaml.YAMLError:
 | 
	
		
			
				|  |  | -            raise ValueError("Invalid YAML format in data argument.")
 | 
	
		
			
				|  |  | +            raise InvalidYAMLFormatError("Invalid YAML format in data argument.")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # check or repair dsl version
 | 
	
		
			
				|  |  | -        import_data = cls._check_or_fix_dsl(import_data)
 | 
	
		
			
				|  |  | +        import_data = _check_or_fix_dsl(import_data)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          app_data = import_data.get("app")
 | 
	
		
			
				|  |  |          if not app_data:
 | 
	
		
			
				|  |  | -            raise ValueError("Missing app in data argument")
 | 
	
		
			
				|  |  | +            raise MissingAppDataError("Missing app in data argument")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # get app basic info
 | 
	
		
			
				|  |  |          name = args.get("name") or app_data.get("name")
 | 
	
	
		
			
				|  | @@ -90,11 +94,18 @@ class AppDslService:
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # import dsl and create app
 | 
	
		
			
				|  |  |          app_mode = AppMode.value_of(app_data.get("mode"))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          if app_mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
 | 
	
		
			
				|  |  | +            workflow_data = import_data.get("workflow")
 | 
	
		
			
				|  |  | +            if not workflow_data or not isinstance(workflow_data, dict):
 | 
	
		
			
				|  |  | +                raise MissingWorkflowDataError(
 | 
	
		
			
				|  |  | +                    "Missing workflow in data argument when app mode is advanced-chat or workflow"
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              app = cls._import_and_create_new_workflow_based_app(
 | 
	
		
			
				|  |  |                  tenant_id=tenant_id,
 | 
	
		
			
				|  |  |                  app_mode=app_mode,
 | 
	
		
			
				|  |  | -                workflow_data=import_data.get("workflow"),
 | 
	
		
			
				|  |  | +                workflow_data=workflow_data,
 | 
	
		
			
				|  |  |                  account=account,
 | 
	
		
			
				|  |  |                  name=name,
 | 
	
		
			
				|  |  |                  description=description,
 | 
	
	
		
			
				|  | @@ -104,10 +115,16 @@ class AppDslService:
 | 
	
		
			
				|  |  |                  use_icon_as_answer_icon=use_icon_as_answer_icon,
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          elif app_mode in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION}:
 | 
	
		
			
				|  |  | +            model_config = import_data.get("model_config")
 | 
	
		
			
				|  |  | +            if not model_config or not isinstance(model_config, dict):
 | 
	
		
			
				|  |  | +                raise MissingModelConfigError(
 | 
	
		
			
				|  |  | +                    "Missing model_config in data argument when app mode is chat, agent-chat or completion"
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              app = cls._import_and_create_new_model_config_based_app(
 | 
	
		
			
				|  |  |                  tenant_id=tenant_id,
 | 
	
		
			
				|  |  |                  app_mode=app_mode,
 | 
	
		
			
				|  |  | -                model_config_data=import_data.get("model_config"),
 | 
	
		
			
				|  |  | +                model_config_data=model_config,
 | 
	
		
			
				|  |  |                  account=account,
 | 
	
		
			
				|  |  |                  name=name,
 | 
	
		
			
				|  |  |                  description=description,
 | 
	
	
		
			
				|  | @@ -117,7 +134,7 @@ class AppDslService:
 | 
	
		
			
				|  |  |                  use_icon_as_answer_icon=use_icon_as_answer_icon,
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  | -            raise ValueError("Invalid app mode")
 | 
	
		
			
				|  |  | +            raise InvalidAppModeError("Invalid app mode")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          return app
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -132,26 +149,32 @@ class AppDslService:
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  |              import_data = yaml.safe_load(data)
 | 
	
		
			
				|  |  |          except yaml.YAMLError:
 | 
	
		
			
				|  |  | -            raise ValueError("Invalid YAML format in data argument.")
 | 
	
		
			
				|  |  | +            raise InvalidYAMLFormatError("Invalid YAML format in data argument.")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # check or repair dsl version
 | 
	
		
			
				|  |  | -        import_data = cls._check_or_fix_dsl(import_data)
 | 
	
		
			
				|  |  | +        import_data = _check_or_fix_dsl(import_data)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          app_data = import_data.get("app")
 | 
	
		
			
				|  |  |          if not app_data:
 | 
	
		
			
				|  |  | -            raise ValueError("Missing app in data argument")
 | 
	
		
			
				|  |  | +            raise MissingAppDataError("Missing app in data argument")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # import dsl and overwrite app
 | 
	
		
			
				|  |  |          app_mode = AppMode.value_of(app_data.get("mode"))
 | 
	
		
			
				|  |  |          if app_mode not in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
 | 
	
		
			
				|  |  | -            raise ValueError("Only support import workflow in advanced-chat or workflow app.")
 | 
	
		
			
				|  |  | +            raise InvalidAppModeError("Only support import workflow in advanced-chat or workflow app.")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if app_data.get("mode") != app_model.mode:
 | 
	
		
			
				|  |  |              raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_mode.value}")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        workflow_data = import_data.get("workflow")
 | 
	
		
			
				|  |  | +        if not workflow_data or not isinstance(workflow_data, dict):
 | 
	
		
			
				|  |  | +            raise MissingWorkflowDataError(
 | 
	
		
			
				|  |  | +                "Missing workflow in data argument when app mode is advanced-chat or workflow"
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          return cls._import_and_overwrite_workflow_based_app(
 | 
	
		
			
				|  |  |              app_model=app_model,
 | 
	
		
			
				|  |  | -            workflow_data=import_data.get("workflow"),
 | 
	
		
			
				|  |  | +            workflow_data=workflow_data,
 | 
	
		
			
				|  |  |              account=account,
 | 
	
		
			
				|  |  |          )
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -186,35 +209,12 @@ class AppDslService:
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          return yaml.dump(export_data, allow_unicode=True)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    @classmethod
 | 
	
		
			
				|  |  | -    def _check_or_fix_dsl(cls, import_data: dict) -> dict:
 | 
	
		
			
				|  |  | -        """
 | 
	
		
			
				|  |  | -        Check or fix dsl
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        :param import_data: import data
 | 
	
		
			
				|  |  | -        """
 | 
	
		
			
				|  |  | -        if not import_data.get("version"):
 | 
	
		
			
				|  |  | -            import_data["version"] = "0.1.0"
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        if not import_data.get("kind") or import_data.get("kind") != "app":
 | 
	
		
			
				|  |  | -            import_data["kind"] = "app"
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        if import_data.get("version") != current_dsl_version:
 | 
	
		
			
				|  |  | -            # Currently only one DSL version, so no difference checks or compatibility fixes will be performed.
 | 
	
		
			
				|  |  | -            logger.warning(
 | 
	
		
			
				|  |  | -                f"DSL version {import_data.get('version')} is not compatible "
 | 
	
		
			
				|  |  | -                f"with current version {current_dsl_version}, related to "
 | 
	
		
			
				|  |  | -                f"Dify version {dsl_to_dify_version_mapping.get(current_dsl_version)}."
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        return import_data
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |      @classmethod
 | 
	
		
			
				|  |  |      def _import_and_create_new_workflow_based_app(
 | 
	
		
			
				|  |  |          cls,
 | 
	
		
			
				|  |  |          tenant_id: str,
 | 
	
		
			
				|  |  |          app_mode: AppMode,
 | 
	
		
			
				|  |  | -        workflow_data: dict,
 | 
	
		
			
				|  |  | +        workflow_data: Mapping[str, Any],
 | 
	
		
			
				|  |  |          account: Account,
 | 
	
		
			
				|  |  |          name: str,
 | 
	
		
			
				|  |  |          description: str,
 | 
	
	
		
			
				|  | @@ -238,7 +238,9 @@ class AppDslService:
 | 
	
		
			
				|  |  |          :param use_icon_as_answer_icon: use app icon as answer icon
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          if not workflow_data:
 | 
	
		
			
				|  |  | -            raise ValueError("Missing workflow in data argument when app mode is advanced-chat or workflow")
 | 
	
		
			
				|  |  | +            raise MissingWorkflowDataError(
 | 
	
		
			
				|  |  | +                "Missing workflow in data argument when app mode is advanced-chat or workflow"
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          app = cls._create_app(
 | 
	
		
			
				|  |  |              tenant_id=tenant_id,
 | 
	
	
		
			
				|  | @@ -277,7 +279,7 @@ class AppDslService:
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @classmethod
 | 
	
		
			
				|  |  |      def _import_and_overwrite_workflow_based_app(
 | 
	
		
			
				|  |  | -        cls, app_model: App, workflow_data: dict, account: Account
 | 
	
		
			
				|  |  | +        cls, app_model: App, workflow_data: Mapping[str, Any], account: Account
 | 
	
		
			
				|  |  |      ) -> Workflow:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          Import app dsl and overwrite workflow based app
 | 
	
	
		
			
				|  | @@ -287,7 +289,9 @@ class AppDslService:
 | 
	
		
			
				|  |  |          :param account: Account instance
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          if not workflow_data:
 | 
	
		
			
				|  |  | -            raise ValueError("Missing workflow in data argument when app mode is advanced-chat or workflow")
 | 
	
		
			
				|  |  | +            raise MissingWorkflowDataError(
 | 
	
		
			
				|  |  | +                "Missing workflow in data argument when app mode is advanced-chat or workflow"
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # fetch draft workflow by app_model
 | 
	
		
			
				|  |  |          workflow_service = WorkflowService()
 | 
	
	
		
			
				|  | @@ -323,7 +327,7 @@ class AppDslService:
 | 
	
		
			
				|  |  |          cls,
 | 
	
		
			
				|  |  |          tenant_id: str,
 | 
	
		
			
				|  |  |          app_mode: AppMode,
 | 
	
		
			
				|  |  | -        model_config_data: dict,
 | 
	
		
			
				|  |  | +        model_config_data: Mapping[str, Any],
 | 
	
		
			
				|  |  |          account: Account,
 | 
	
		
			
				|  |  |          name: str,
 | 
	
		
			
				|  |  |          description: str,
 | 
	
	
		
			
				|  | @@ -345,7 +349,9 @@ class AppDslService:
 | 
	
		
			
				|  |  |          :param icon_background: app icon background
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          if not model_config_data:
 | 
	
		
			
				|  |  | -            raise ValueError("Missing model_config in data argument when app mode is chat, agent-chat or completion")
 | 
	
		
			
				|  |  | +            raise MissingModelConfigError(
 | 
	
		
			
				|  |  | +                "Missing model_config in data argument when app mode is chat, agent-chat or completion"
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          app = cls._create_app(
 | 
	
		
			
				|  |  |              tenant_id=tenant_id,
 | 
	
	
		
			
				|  | @@ -448,3 +454,34 @@ class AppDslService:
 | 
	
		
			
				|  |  |              raise ValueError("Missing app configuration, please check.")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          export_data["model_config"] = app_model_config.to_dict()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def _check_or_fix_dsl(import_data: dict[str, Any]) -> Mapping[str, Any]:
 | 
	
		
			
				|  |  | +    """
 | 
	
		
			
				|  |  | +    Check or fix dsl
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    :param import_data: import data
 | 
	
		
			
				|  |  | +    :raises DSLVersionNotSupportedError: if the imported DSL version is newer than the current version
 | 
	
		
			
				|  |  | +    """
 | 
	
		
			
				|  |  | +    if not import_data.get("version"):
 | 
	
		
			
				|  |  | +        import_data["version"] = "0.1.0"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if not import_data.get("kind") or import_data.get("kind") != "app":
 | 
	
		
			
				|  |  | +        import_data["kind"] = "app"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    imported_version = import_data.get("version")
 | 
	
		
			
				|  |  | +    if imported_version != current_dsl_version:
 | 
	
		
			
				|  |  | +        if imported_version and version.parse(imported_version) > version.parse(current_dsl_version):
 | 
	
		
			
				|  |  | +            raise DSLVersionNotSupportedError(
 | 
	
		
			
				|  |  | +                f"The imported DSL version {imported_version} is newer than "
 | 
	
		
			
				|  |  | +                f"the current supported version {current_dsl_version}. "
 | 
	
		
			
				|  |  | +                f"Please upgrade your Dify instance to import this configuration."
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            logger.warning(
 | 
	
		
			
				|  |  | +                f"DSL version {imported_version} is older than "
 | 
	
		
			
				|  |  | +                f"the current version {current_dsl_version}. "
 | 
	
		
			
				|  |  | +                f"This may cause compatibility issues."
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return import_data
 |