feat(workflow): support Dify features conversion and file variable migration

This commit is contained in:
Timebomb2018
2026-04-10 18:30:12 +08:00
parent e5e6699168
commit f4a63f7d55
5 changed files with 106 additions and 16 deletions

View File

@@ -40,6 +40,7 @@ class WorkflowParserResult(BaseModel):
edges: list[EdgeDefinition] = Field(default_factory=list)
nodes: list[NodeDefinition] = Field(default_factory=list)
variables: list[VariableDefinition] = Field(default_factory=list)
features: dict[str, Any] = Field(default_factory=dict)
warnings: list[ExceptionDefinition] = Field(default_factory=list)
errors: list[ExceptionDefinition] = Field(default_factory=list)
@@ -51,6 +52,7 @@ class WorkflowImportResult(BaseModel):
edges: list[EdgeDefinition] = Field(default_factory=list)
nodes: list[NodeDefinition] = Field(default_factory=list)
variables: list[VariableDefinition] = Field(default_factory=list)
features: dict[str, Any] = Field(default_factory=dict)
warnings: list[ExceptionDefinition] = Field(default_factory=list)
errors: list[ExceptionDefinition] = Field(default_factory=list)

View File

@@ -15,7 +15,7 @@ from app.core.workflow.adapters.errors import (
ExceptionType
)
from app.core.workflow.nodes.assigner.config import AssignmentItem
from app.core.workflow.nodes.base_config import VariableDefinition, BaseNodeConfig
from app.core.workflow.nodes.base_config import VariableDefinition as NodeVariableDefinition, BaseNodeConfig
from app.core.workflow.nodes.code.config import InputVariable, OutputVariable
from app.core.workflow.nodes.configs import (
StartNodeConfig,
@@ -36,6 +36,7 @@ from app.core.workflow.nodes.configs import (
ListOperatorNodeConfig,
DocExtractorNodeConfig,
)
from app.schemas.workflow_schema import VariableDefinition as SchemaVariableDefinition
from app.core.workflow.nodes.cycle_graph.config import (
ConditionDetail as LoopConditionDetail,
ConditionsConfig,
@@ -98,6 +99,7 @@ class DifyConverter(BaseConverter):
NodeType.CYCLE_START: lambda x: {},
NodeType.BREAK: lambda x: {},
}
self._file_vars_to_conv: list[SchemaVariableDefinition] = []
def get_node_convert(self, node_type):
func = self.CONFIG_CONVERT_MAP.get(node_type, lambda x: {})
@@ -286,19 +288,25 @@ class DifyConverter(BaseConverter):
)
continue
if var_type in ["file", "array[file]"]:
self.errors.append(
ExceptionDefinition(
type=ExceptionType.VARIABLE,
node_id=node["id"],
node_name=node_data["title"],
name=var["variable"],
detail=f"Unsupported Variable type for start node: {var_type}"
)
)
if var_type in [VariableType.FILE, VariableType.ARRAY_FILE]:
# 开始节点不支持文件变量,转为会话变量
self._file_vars_to_conv.append(SchemaVariableDefinition(
name=var["variable"],
type=var_type.value,
required=var.get("required", False),
default=None,
description=var.get("label", ""),
))
self.warnings.append(ExceptionDefinition(
type=ExceptionType.VARIABLE,
node_id=node["id"],
node_name=node_data["title"],
name=var["variable"],
detail=f"File variable '{var['variable']}' is not supported in start node, moved to conversation variables"
))
continue
var_def = VariableDefinition(
var_def = NodeVariableDefinition(
name=var["variable"],
type=var_type,
required=var["required"],
@@ -837,3 +845,76 @@ class DifyConverter(BaseConverter):
).model_dump()
self.config_validate(node["id"], node["data"]["title"], DocExtractorNodeConfig, result)
return result
@staticmethod
def convert_features(features: dict) -> dict:
"""Convert Dify features to MemoryBear FeaturesConfigForm format."""
if not features:
return {}
result: dict = {}
# opening_statement
opening = features.get("opening_statement", "")
suggested = features.get("suggested_questions", [])
result["opening_statement"] = {
"enabled": bool(opening),
"statement": opening or None,
"suggested_questions": suggested,
}
# citation (对应 Dify retriever_resource)
retriever = features.get("retriever_resource", {})
result["citation"] = {
"enabled": retriever.get("enabled", False) if isinstance(retriever, dict) else False,
}
# file_upload: Dify allowed_file_types 数组 -> 前端扁平字段
file_upload = features.get("file_upload", {})
allowed_types = file_upload.get("allowed_file_types", []) if file_upload else []
allowed_methods = file_upload.get("allowed_file_upload_methods", ["local_file", "remote_url"])
if isinstance(allowed_methods, list):
if len(allowed_methods) >= 2:
transfer_method = "both"
elif allowed_methods:
transfer_method = allowed_methods[0]
else:
transfer_method = "both"
else:
transfer_method = allowed_methods or "both"
file_config = file_upload.get("fileUploadConfig", {})
result["file_upload"] = {
"enabled": file_upload.get("enabled", False) if file_upload else False,
"image_enabled": "image" in allowed_types,
"image_max_size_mb": file_config.get("image_file_size_limit", 10) if file_config else 10,
"image_allowed_extensions": ["png", "jpg", "jpeg"],
"audio_enabled": "audio" in allowed_types,
"audio_max_size_mb": file_config.get("audio_file_size_limit", 50) if file_config else 50,
"audio_allowed_extensions": ["mp3", "wav", "m4a"],
"document_enabled": "document" in allowed_types,
"document_max_size_mb": file_config.get("file_size_limit", 100) if file_config else 100,
"document_allowed_extensions": ["pdf", "docx", "doc", "xlsx", "xls", "txt", "csv", "json", "md"],
"video_enabled": "video" in allowed_types,
"video_max_size_mb": file_config.get("video_file_size_limit", 100) if file_config else 100,
"video_allowed_extensions": ["mp4", "mov"],
"max_file_count": file_upload.get("number_limits", 1) if file_upload else 1,
"allowed_transfer_methods": transfer_method,
}
# text_to_speech
tts = features.get("text_to_speech", {})
result["text_to_speech"] = {
"enabled": tts.get("enabled", False) if isinstance(tts, dict) else False,
"voice": tts.get("voice") if isinstance(tts, dict) else None,
"language": tts.get("language") if isinstance(tts, dict) else None,
"autoplay": False,
}
# suggested_questions_after_answer
sqa = features.get("suggested_questions_after_answer", {})
result["suggested_questions_after_answer"] = {
"enabled": sqa.get("enabled", False) if isinstance(sqa, dict) else False,
}
return result

View File

@@ -119,9 +119,12 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
if variable:
self.conv_variables.append(con_var)
# for variables in config.get("workflow").get("environment_variables"):
# variable = self._convert_variable(variables)
# conv_variables.append(variable)
# 开始节点的文件变量合并到会话变量
self.conv_variables.extend(self._file_vars_to_conv)
features = self.convert_features(
self.config.get("workflow", {}).get("features", {})
)
trigger = self._convert_trigger({})
execution_config = self._convert_execution({})
@@ -135,6 +138,7 @@ class DifyAdapter(BasePlatformAdapter, DifyConverter):
edges=self.edges,
nodes=self.nodes,
variables=self.conv_variables,
features=features,
warnings=self.warnings,
errors=self.errors
)

View File

@@ -411,6 +411,7 @@ class AppService:
edges=[edge.model_dump() for edge in data.edges] if data.edges else [],
variables=[var.model_dump() for var in data.variables] if data.variables else [],
execution_config=data.execution_config.model_dump() if data.execution_config else {},
features=data.features if data.features else {},
triggers=[trigger.model_dump() for trigger in data.triggers] if data.triggers else [],
is_active=True,
created_at=now,

View File

@@ -69,6 +69,7 @@ class WorkflowImportService:
edges=workflow_config.edges,
nodes=workflow_config.nodes,
variables=workflow_config.variables,
features=workflow_config.features,
warnings=workflow_config.warnings,
errors=workflow_config.errors
)
@@ -95,7 +96,8 @@ class WorkflowImportService:
workflow_config=WorkflowConfigCreate(
nodes=config["nodes"],
edges=config["edges"],
variables=config["variables"]
variables=config["variables"],
features=config.get("features", {})
)
)
)