feat(create_mcp): 添加Dify API错误处理和文件字段类型支持
- 引入DifyAPIError异常类,提供详细的API错误信息 - 实现对file和file-list字段类型的区分处理 - 添加网络请求异常和其他未知错误的捕获机制 - 改进文件上传逻辑,支持单文件和多文件类型 - 优化错误信息返回,提供更友好的用户提示 fix(create_mcp_utils): 修复文件参数处理逻辑 - 添加is_list字段标识多文件类型 - 改进文件上传失败的过滤机制 - 修复文件字段变量查找逻辑,使用映射提高查找效率 chore(config): 更新项目版本号至0.1.2 - 将PKG-INFO和pyproject.toml中的版本从0.1.1更新至0.1.2 - 更新默认配置中的app_sks值
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: lzwcai-demp-tool-server-dify-to-mcp-test
|
||||
Version: 0.1.1
|
||||
Version: 0.1.2
|
||||
Summary: 这是一个Dify to MCP的集成工具;通过Dify的API接口,将Dify的模型部署到MCP平台,并进行推理。
|
||||
Requires-Python: >=3.10
|
||||
Requires-Dist: httpx>=0.28.1
|
||||
|
||||
@@ -19,7 +19,7 @@ def setup_mock_arguments():
|
||||
# 默认配置
|
||||
default_config = {
|
||||
"base_url": "http://192.168.2.236:3001/v1",
|
||||
"app_sks": ["app-Oo0QRJismgQADRSt8Bj0RXWB"],
|
||||
"app_sks": ["app-IfJayK9uu5oTo54Rpr2AS7wl"],
|
||||
"mode_type": "workflow",
|
||||
"transport": "stdio"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "lzwcai-demp-tool-server-dify-to-mcp-test"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
description = "这是一个Dify to MCP的集成工具;通过Dify的API接口,将Dify的模型部署到MCP平台,并进行推理。"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -17,6 +17,7 @@ from src.difyTaskCall.task_instance import TaskInstance
|
||||
from src.utils.dify_workflow_schema import process_user_input_form, extract_file_fields
|
||||
from src.create_mcp_utils import process_file_arguments
|
||||
from src.utils.logger_config import get_logger
|
||||
from src.workflow.workflow_server import DifyAPIError
|
||||
|
||||
# 使用统一的日志配置
|
||||
logger = get_logger(__name__)
|
||||
@@ -147,29 +148,48 @@ async def handle_call_tool(
|
||||
processed_arguments = process_file_arguments(arguments, current_tool_file_fields, dify_api, name)
|
||||
logger.info(f"工具 {name} 处理后的 arguments: {processed_arguments}")
|
||||
|
||||
responses = dify_api.chat_message(
|
||||
tool_sk,
|
||||
inputs=processed_arguments,
|
||||
)
|
||||
try:
|
||||
responses = dify_api.chat_message(
|
||||
tool_sk,
|
||||
inputs=processed_arguments,
|
||||
)
|
||||
|
||||
# 初始化 outputs 变量,避免未定义错误
|
||||
outputs = {}
|
||||
for res in responses:
|
||||
if res["event"] == "workflow_finished":
|
||||
outputs = res["data"]["outputs"]
|
||||
break # 找到 workflow_finished 事件后退出循环
|
||||
|
||||
# 构建 MCP 输出
|
||||
mcp_out = []
|
||||
if outputs:
|
||||
for _, v in outputs.items():
|
||||
mcp_out.append(types.TextContent(type="text", text=v))
|
||||
else:
|
||||
# 如果没有获取到 outputs,返回错误信息
|
||||
logger.warning(f"工具 {name} 未获取到 workflow_finished 事件或 outputs 为空")
|
||||
mcp_out.append(types.TextContent(type="text", text="工具执行完成,但未返回输出结果"))
|
||||
|
||||
return mcp_out
|
||||
# 初始化 outputs 变量,避免未定义错误
|
||||
outputs = {}
|
||||
for res in responses:
|
||||
if res["event"] == "workflow_finished":
|
||||
outputs = res["data"]["outputs"]
|
||||
break # 找到 workflow_finished 事件后退出循环
|
||||
|
||||
# 构建 MCP 输出
|
||||
mcp_out = []
|
||||
if outputs:
|
||||
for _, v in outputs.items():
|
||||
mcp_out.append(types.TextContent(type="text", text=str(v)))
|
||||
else:
|
||||
# 如果没有获取到 outputs,返回错误信息
|
||||
logger.warning(f"工具 {name} 未获取到 workflow_finished 事件或 outputs 为空")
|
||||
mcp_out.append(types.TextContent(type="text", text="工具执行完成,但未返回输出结果"))
|
||||
|
||||
return mcp_out
|
||||
|
||||
except DifyAPIError as e:
|
||||
# 捕获 Dify API 错误,直接返回给用户
|
||||
logger.error(f"工具 {name} 调用 Dify API 失败: {e}")
|
||||
error_message = f"API调用失败: {e.message}"
|
||||
return [types.TextContent(type="text", text=error_message)]
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
# 捕获网络请求错误
|
||||
logger.error(f"工具 {name} 网络请求失败: {e}")
|
||||
error_message = f"网络请求失败: {str(e)}"
|
||||
return [types.TextContent(type="text", text=error_message)]
|
||||
|
||||
except Exception as e:
|
||||
# 捕获其他未知错误
|
||||
logger.error(f"工具 {name} 执行时发生未知错误: {e}", exc_info=True)
|
||||
error_message = f"工具执行失败: {str(e)}"
|
||||
return [types.TextContent(type="text", text=error_message)]
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def process_file_arguments(arguments, current_tool_file_fields, dify_api, tool_n
|
||||
current_tool_file_fields (list): 当前工具的文件字段信息列表
|
||||
数据结构: [{'variable': 'txt', 'label': '输入', 'required': True, 'max_length': 48,
|
||||
'allowed_file_types': ['document'], 'allowed_file_upload_methods': ['local_file', 'remote_url'],
|
||||
'allowed_file_extensions': []}]
|
||||
'allowed_file_extensions': [], 'is_list': False}]
|
||||
dify_api: Dify API实例,包含file_parameter_pretreatment方法
|
||||
tool_name (str): 工具名称,用于日志记录
|
||||
|
||||
@@ -58,8 +58,9 @@ def process_file_arguments(arguments, current_tool_file_fields, dify_api, tool_n
|
||||
logger.info(f"工具 {tool_name}: 无需处理文件字段 (arguments={bool(arguments)}, file_fields={len(current_tool_file_fields) if current_tool_file_fields else 0})")
|
||||
return arguments
|
||||
|
||||
# 创建文件字段变量名的集合,用于快速查找
|
||||
file_field_variables = {field['variable'] for field in current_tool_file_fields}
|
||||
# 创建文件字段变量名到字段信息的映射,用于快速查找
|
||||
file_field_map = {field['variable']: field for field in current_tool_file_fields}
|
||||
file_field_variables = set(file_field_map.keys())
|
||||
logger.info(f"工具 {tool_name} 的文件字段变量名: {file_field_variables}")
|
||||
|
||||
# 创建arguments的副本,避免修改原始数据
|
||||
@@ -101,12 +102,20 @@ def process_file_arguments(arguments, current_tool_file_fields, dify_api, tool_n
|
||||
logger.error(f"工具 {tool_name}: 文件预处理失败,未返回有效结果")
|
||||
continue
|
||||
|
||||
# 取第一个处理后的文件对象
|
||||
processed_files_item = processed_files[0]
|
||||
logger.info(f"工具 {tool_name}: 文件预处理完成,upload_file_id: {processed_files_item.get('upload_file_id', 'N/A')}")
|
||||
# 过滤掉上传失败的文件
|
||||
valid_files = [f for f in processed_files if f.get('upload_file_id') and not f.get('upload_error')]
|
||||
if not valid_files:
|
||||
logger.error(f"工具 {tool_name}: 所有文件上传失败")
|
||||
continue
|
||||
|
||||
logger.info(f"工具 {tool_name}: 文件预处理完成,成功上传 {len(valid_files)} 个文件")
|
||||
|
||||
# 获取当前字段的配置信息,判断是单文件还是多文件
|
||||
field_config = file_field_map.get(arg_name, {})
|
||||
is_list = field_config.get('is_list', False)
|
||||
|
||||
# 更新processed_arguments中的值
|
||||
_update_processed_argument(processed_arguments, arg_name, arg_value, processed_files_item, tool_name)
|
||||
_update_processed_argument(processed_arguments, arg_name, arg_value, valid_files, tool_name, is_list)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"工具 {tool_name}: 处理文件字段 {arg_name} 时发生错误: {str(e)}")
|
||||
@@ -180,27 +189,34 @@ def _is_file_object(obj):
|
||||
return any(key in obj for key in file_indicators)
|
||||
|
||||
|
||||
def _update_processed_argument(processed_arguments, arg_name, original_value, processed_files, tool_name):
|
||||
def _update_processed_argument(processed_arguments, arg_name, original_value, processed_files, tool_name, is_list=False):
|
||||
"""
|
||||
更新处理后的参数值
|
||||
|
||||
重要:Dify API 的文件字段始终需要一个文件对象列表,即使只有一个文件
|
||||
根据字段类型决定输出格式:
|
||||
- file-list (is_list=True): 输出列表格式 []
|
||||
- file (is_list=False): 输出对象格式 {}
|
||||
|
||||
Args:
|
||||
processed_arguments (dict): 处理后的参数字典
|
||||
arg_name (str): 参数名称
|
||||
original_value: 原始参数值(可以是字符串URL、文件对象或文件列表)
|
||||
processed_files: 处理后的文件对象(单个对象,不是列表)
|
||||
processed_files (list): 处理后的文件对象列表
|
||||
tool_name (str): 工具名称
|
||||
is_list (bool): 是否为多文件类型 (file-list=True, file=False)
|
||||
"""
|
||||
# 注意:processed_files 是单个文件对象,需要转换为列表
|
||||
# 因为 Dify API 要求文件字段必须是列表格式
|
||||
if processed_files:
|
||||
# 始终将文件对象包装成列表
|
||||
processed_arguments[arg_name] = [processed_files]
|
||||
logger.info(f"工具 {tool_name}: 已更新文件字段 {arg_name} 为列表格式: {processed_arguments[arg_name]}")
|
||||
else:
|
||||
if not processed_files:
|
||||
logger.warning(f"工具 {tool_name}: 文件处理后为空,保持原值")
|
||||
return
|
||||
|
||||
if is_list:
|
||||
# file-list 类型:使用完整列表
|
||||
processed_arguments[arg_name] = processed_files
|
||||
logger.info(f"工具 {tool_name}: 字段 {arg_name} 是 file-list 类型,输出 {len(processed_files)} 个文件")
|
||||
else:
|
||||
# file 类型:只取第一个文件对象
|
||||
processed_arguments[arg_name] = processed_files[0]
|
||||
logger.info(f"工具 {tool_name}: 字段 {arg_name} 是 file 类型,输出单个对象")
|
||||
|
||||
logger.info(f"工具 {tool_name}: 字段 {arg_name} 最终值: {processed_arguments[arg_name]}")
|
||||
|
||||
|
||||
Binary file not shown.
@@ -247,7 +247,7 @@ def process_user_input_form(user_input_form):
|
||||
if default_value is not None:
|
||||
property_schema["default"] = str(default_value)
|
||||
|
||||
elif control_type == "file":
|
||||
elif control_type in ["file", "file-list"]:
|
||||
# 文件上传控件处理 - 简化版本,仅支持remote_url
|
||||
# 获取允许的文件类型
|
||||
allowed_file_types = control_config.get("allowed_file_types", [])
|
||||
@@ -259,27 +259,21 @@ def process_user_input_form(user_input_form):
|
||||
"type": "object",
|
||||
"description": label or f"文件上传字段: {variable}",
|
||||
"properties": {
|
||||
# "type": {
|
||||
# "type": "string",
|
||||
# "description": file_type_desc
|
||||
# },
|
||||
# "transfer_method": {
|
||||
# "type": "string",
|
||||
# "description": "文件传输方式",
|
||||
# "enum": ["remote_url"],
|
||||
# "default": "remote_url"
|
||||
# },
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": url_description
|
||||
}
|
||||
},
|
||||
"required": [ "url"]
|
||||
"required": ["url"]
|
||||
}
|
||||
|
||||
# 处理文件数量限制
|
||||
# 判断是单文件还是多文件
|
||||
# file-list 类型或 max_length > 1 都视为多文件
|
||||
actual_type = control_config.get("type", control_type)
|
||||
max_length = control_config.get("max_length", 1)
|
||||
if max_length > 1:
|
||||
is_multi_file = actual_type == "file-list" or max_length > 1
|
||||
|
||||
if is_multi_file:
|
||||
# 多文件上传场景
|
||||
property_schema = {
|
||||
"type": "array",
|
||||
@@ -330,6 +324,7 @@ def extract_file_fields(user_input_form):
|
||||
- allowed_file_types (list): 允许的文件类型
|
||||
- allowed_file_upload_methods (list): 允许的上传方式
|
||||
- allowed_file_extensions (list): 允许的文件扩展名
|
||||
- is_list (bool): 是否为多文件类型 (file-list=True, file=False)
|
||||
"""
|
||||
file_fields = []
|
||||
|
||||
@@ -353,6 +348,11 @@ def extract_file_fields(user_input_form):
|
||||
|
||||
# 只处理type为file或file-list的字段
|
||||
if control_type in ["file", "file-list"] or control_config.get("type") in ["file", "file-list"]:
|
||||
# 判断是否为多文件类型
|
||||
# 优先使用 control_config 中的 type,其次使用 control_type
|
||||
actual_type = control_config.get("type", control_type)
|
||||
is_list = actual_type == "file-list"
|
||||
|
||||
# 提取文件字段的详细信息
|
||||
file_field_info = {
|
||||
"variable": control_config.get("variable", ""),
|
||||
@@ -361,7 +361,8 @@ def extract_file_fields(user_input_form):
|
||||
"max_length": control_config.get("max_length", 1),
|
||||
"allowed_file_types": control_config.get("allowed_file_types", []),
|
||||
"allowed_file_upload_methods": control_config.get("allowed_file_upload_methods", []),
|
||||
"allowed_file_extensions": control_config.get("allowed_file_extensions", [])
|
||||
"allowed_file_extensions": control_config.get("allowed_file_extensions", []),
|
||||
"is_list": is_list # 新增:标识是否为多文件类型
|
||||
}
|
||||
|
||||
# 只添加有效的字段(必须有variable)
|
||||
|
||||
Binary file not shown.
@@ -13,6 +13,27 @@ except ImportError:
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DifyAPIError(Exception):
|
||||
"""Dify API 错误异常类"""
|
||||
|
||||
def __init__(self, status_code: int, error_code: str, message: str, request_data: dict = None):
|
||||
self.status_code = status_code
|
||||
self.error_code = error_code
|
||||
self.message = message
|
||||
self.request_data = request_data
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.status_code}] {self.error_code}: {self.message}"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"status_code": self.status_code,
|
||||
"error_code": self.error_code,
|
||||
"message": self.message
|
||||
}
|
||||
|
||||
def pinyin_to_camel(pinyin):
|
||||
"""
|
||||
将中文名称转换为工具名称
|
||||
@@ -131,8 +152,22 @@ class WorkflowDifyAPI(ABC):
|
||||
logger.error(f"API request failed with status {response.status_code}")
|
||||
logger.error(f"Response content: {response.text}")
|
||||
logger.error(f"Request data: {data}")
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析错误响应并抛出带有详细信息的异常
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_message = error_data.get("message", response.text)
|
||||
error_code = error_data.get("code", "unknown_error")
|
||||
except json.JSONDecodeError:
|
||||
error_message = response.text
|
||||
error_code = "unknown_error"
|
||||
|
||||
raise DifyAPIError(
|
||||
status_code=response.status_code,
|
||||
error_code=error_code,
|
||||
message=error_message,
|
||||
request_data=data
|
||||
)
|
||||
if response_mode == "streaming":
|
||||
def stream_generator():
|
||||
for line in response.iter_lines():
|
||||
@@ -148,12 +183,14 @@ class WorkflowDifyAPI(ABC):
|
||||
return response.json()
|
||||
|
||||
def upload_file(self, api_key, file_path, user="pp666"):
|
||||
|
||||
url = f"{self.dify_base_url}/files/upload"
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
files = {"file": open(file_path, "rb")}
|
||||
data = {"user": user}
|
||||
response = requests.post(url, headers=headers, files=files, data=data)
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
files = {"file": f}
|
||||
response = requests.post(url, headers=headers, files=files, data=data)
|
||||
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
def upload_file_remote_url(self, file_url):
|
||||
|
||||
Reference in New Issue
Block a user