diff --git a/api/controllers/console/workspace/snippets.py b/api/controllers/console/workspace/snippets.py index 51b66d2e55..428fbd18cd 100644 --- a/api/controllers/console/workspace/snippets.py +++ b/api/controllers/console/workspace/snippets.py @@ -1,6 +1,7 @@ import logging +from urllib.parse import quote -from flask import request +from flask import Response, request from flask_restx import Resource, marshal from sqlalchemy.orm import Session from werkzeug.exceptions import NotFound @@ -241,7 +242,18 @@ class CustomizedSnippetExportApi(Resource): snippet=snippet, include_secret=query.include_secret == "true" ) - return {"data": result}, 200 + # Set filename with .snippet extension + filename = f"{snippet.name}.snippet" + encoded_filename = quote(filename) + + response = Response( + result, + mimetype="application/x-yaml", + ) + response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}" + response.headers["Content-Type"] = "application/x-yaml" + + return response @console_ns.route("/workspaces/current/customized-snippets/imports") diff --git a/api/services/snippet_dsl_service.py b/api/services/snippet_dsl_service.py index fb2a75c5d3..1d43b06970 100644 --- a/api/services/snippet_dsl_service.py +++ b/api/services/snippet_dsl_service.py @@ -32,6 +32,12 @@ IMPORT_INFO_REDIS_EXPIRY = 10 * 60 # 10 minutes DSL_MAX_SIZE = 10 * 1024 * 1024 # 10MB CURRENT_DSL_VERSION = "0.1.0" +# List of node types that are not allowed in snippets +FORBIDDEN_NODE_TYPES = [ + NodeType.START.value, # "start" + NodeType.HUMAN_INPUT.value, # "human-input" +] + class ImportMode(StrEnum): YAML_CONTENT = "yaml-content" @@ -213,6 +219,28 @@ class SnippetDslService: error="Missing snippet data in YAML content", ) + # Validate workflow nodes - check for forbidden node types + workflow_data = data.get("workflow", {}) + if workflow_data: + graph = workflow_data.get("graph", {}) + nodes = graph.get("nodes", []) + forbidden_nodes_found = [] + for node in nodes: + node_data = node.get("data", {}) + if not node_data: + continue + node_type = node_data.get("type", "") + if node_type in FORBIDDEN_NODE_TYPES: + forbidden_nodes_found.append(node_type) + + if forbidden_nodes_found: + forbidden_types_str = ", ".join(set(forbidden_nodes_found)) + return SnippetImportInfo( + id=import_id, + status=ImportStatus.FAILED, + error=f"Snippet cannot contain the following node types: {forbidden_types_str}", + ) + # If snippet_id is provided, check if it exists snippet = None if snippet_id: