feat(lzwcai-demp-tool-server-dify-to-mcp): 初始化 Dify 集成工具模块

新增 Dify 到 MCP 的集成工具,支持通过 Dify API 将模型部署到 MCP 平台并进行推理。
该模块包含完整的服务器实现、依赖配置和命令行启动脚本。

主要功能:
- 支持 Workflow 和 Completion 模式的调用
- 自动翻译工具名称为驼峰命名格式
- 提供文件上传与任务停止接口
- 兼容流式与非流式响应处理
This commit is contained in:
2025-12-16 17:52:04 +08:00
commit ec7e7fd7dc
179 changed files with 18443 additions and 0 deletions

68
README.md Normal file
View File

@@ -0,0 +1,68 @@
# lzwcai-mcp-server-package
MCP (Model Context Protocol) 服务器工具集,为 AI 助手提供企业级业务能力扩展。
## 📦 包含模块
| 模块 | 版本 | 说明 |
|------|------|------|
| [lzwcai-mcp-iot](./lzwcai_mcp_iot) | 0.3.3 | IoT 设备控制服务器,支持设备查询、定位和控制 |
| [lzwcai-mcp-sqlexecutor](./lzwcai_mcp_sqlexecutor) | 0.1.8 | SQL 查询执行服务器,支持动态工具生成 |
| [lzwcai-mcp-api-converter](./lzwcai_mcp_api_converter) | 0.1.30 | API 转换服务器,将业务 API 转换为 MCP 工具 |
| [lzwcai-demp-tool-server-dify-to-mcp](./lzwcai_demp_tool_server_dify_to_mcp) | 0.1.4 | Dify 集成工具,将 Dify 模型部署到 MCP |
| [lzwcai-demp-tool-server-dify-to-mcp-test](./lzwcai_demp_tool_server_dify_to_mcp_test) | 0.1.0 | Dify 集成工具测试版 |
## 🚀 快速安装
```bash
# IoT 设备控制
pip install lzwcai-mcp-iot
# SQL 查询执行
pip install lzwcai-mcp-sqlexecutor
# API 转换器
pip install lzwcai-mcp-api-converter
# Dify 集成
pip install lzwcai-demp-tool-server-dify-to-mcp
```
## <20> 打包 与发布
```bash
# 进入子模块目录
cd lzwcai_mcp_iot
# 使用 uv 打包
uv build
# 上传到管理端技能广场
# 将 dist/ 目录下的 .tar.gz 文件上传至技能广场
```
## 🔧 MCP 客户端配置示例
```json
{
"mcpServers": {
"iot": {
"command": "lzwcai-mcp-iot"
},
"sql": {
"command": "lzwcai-mcp-sqlexecutor"
},
"api": {
"command": "lzwcai-mcp-api-converter"
}
}
}
```
## 📄 许可证
专有软件 - 版权所有 © LZWCAI开发团队
## 📧 联系方式
- 邮箱dev@lzwcai.com

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp
Version: 0.1.0
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,37 @@
# lzwcai-mcp-server-package
#### 介绍
lzwcai-mcp-server-package
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp
Version: 0.1.4
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,23 @@
README.md
pyproject.toml
setup.cfg
lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO
lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt
lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt
src/__init__.py
src/create_mcp.py
src/create_mcp_util.py
src/chat/__init__.py
src/chat/chat_server.py
src/completion/completion_server.py
src/completion/test.py
src/core/__init__.py
src/core/core_server.py
src/difyTaskCall/task_instance.py
src/utils/tool_translation.py
src/utils/translator.py
src/workflow/__init__.py
src/workflow/workflow_server.py

View File

@@ -0,0 +1,2 @@
[console_scripts]
lzwcai-demp-tool-server-dify-to-mcp = src.create_mcp:run_main

View File

@@ -0,0 +1,7 @@
httpx>=0.28.1
mcp>=1.1.2
omegaconf>=2.3.0
pip>=24.3.1
python-dotenv>=1.0.1
requests
pypinyin>=0.54.0

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
主入口文件
用于启动 Dify MCP 服务器,并配置命令行参数
"""
import os
import sys
# Mock 配置参数
def setup_mock_arguments():
"""
设置模拟命令行参数
这些参数可以根据实际需求进行修改
"""
# 默认配置
default_config = {
"base_url": "http://192.168.2.236:3001/v1",
"app_sks": ["app-YFHByB4whARWVqXN2LcuPudq"],
"mode_type": "workflow",
"transport": "stdio"
}
# 如果没有提供命令行参数,则添加默认参数
if len(sys.argv) == 1:
sys.argv.extend([
"--base-url", default_config["base_url"],
"--app-sks", *default_config["app_sks"],
"--mode-type", default_config["mode_type"]
])
return default_config
def main():
"""
主函数:设置命令行参数并启动服务器
"""
# 设置模拟命令行参数
config = setup_mock_arguments()
# 导入并运行 MCP 服务器
try:
from src.create_mcp import run_main
# 获取传输模式
transport_mode = config.get("transport", "stdio")
# 运行服务器(不输出额外信息,避免干扰 STDIO 通信)
run_main(transport=transport_mode)
except ImportError as e:
print(f"[ERROR] 导入错误: {e}", file=sys.stderr)
print("请确保已正确安装所有依赖包", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"[ERROR] 运行错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lzwcai-demp-tool-server-dify-to-mcp"
version = "0.1.4"
description = "这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.1.2",
"omegaconf>=2.3.0",
"pip>=24.3.1",
"python-dotenv>=1.0.1",
"requests",
"pypinyin>=0.54.0",
]
[tool.setuptools]
packages = {find = {where = ["."], include = ["src*"]}}
include-package-data = true
[project.scripts]
lzwcai-demp-tool-server-dify-to-mcp = "src.create_mcp:run_main"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.setuptools.package-data]
"*" = ["*.env"]
"src" = ["**/*.env"]

View File

@@ -0,0 +1,4 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -0,0 +1,7 @@
class ChatDifyAPI:
def __init__(self, base_url: str, app_sks: str):
self.base_url = base_url
self.app_sks = app_sks
def process_task(self, task_id: str, **kwargs):
pass

View File

@@ -0,0 +1,212 @@
import requests
from abc import ABC
import logging
import json
import re
import pypinyin
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class CompletionDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(url, headers=headers, json=data, stream=True)
# 处理流式响应
full_answer = ""
for line in response.iter_lines():
if line:
# 跳过 "data:" 前缀
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data:"):
try:
json_str = decoded_line[5:].strip()
data = json.loads(json_str)
if data.get("event") == "message" and "answer" in data:
# 累积完整答案
full_answer += data["answer"]
# 这里也可以选择处理每个部分响应,例如返回生成器
# yield data
except json.JSONDecodeError:
logger.warning(f"无法解析JSON数据: {decoded_line}")
# 创建一个符合非流式响应格式的结果
response_data = {"answer": full_answer}
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
else:
response = requests.post(url, headers=headers, json=data)
response_data = response.json()
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
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)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
# params = {"user": user}
response = requests.get(url, headers=headers)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
from src.utils.tool_translation import TranslationService
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
return {
"user_input_form": [
{"string": {"variable": "query", "label": "查询内容", "required": True}}
]
}
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
@staticmethod
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 构造符合workflow_finished格式的输出
formatted_response = [
{"event": "workflow_finished", "data": {"outputs": {"result": answer}}}
]
# 尝试处理可能的代码块
if answer.startswith("```") and answer.endswith("```"):
try:
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 如果包含description字段用它替换answer
if "description" in json_data:
formatted_response[0]["data"]["outputs"]["result"] = json_data[
"description"
]
except json.JSONDecodeError:
# 如果不是有效的JSON保留原始代码块内容
pass
return formatted_response
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回符合格式的基础响应
return [
{
"event": "workflow_finished",
"data": {
"outputs": {
"error": str(e),
"fallback": data.get("answer", str(data)),
}
},
}
]

View File

@@ -0,0 +1,104 @@
import requests
from abc import ABC
import logging
import json
logger = logging.getLogger(__name__)
res = {
"event": "message",
"task_id": "49c9ea1b-7b43-475b-a680-d769fb238a45",
"id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"message_id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"mode": "completion",
"answer": '```\n{\n "description": "该API的具体功能描述暂时不明确因为提供的API信息 \'今天打老虎啊按时啊啊\' 并不是有效的API名称或描述。请提供正确的API名称和相关输入输出信息以便我能为其补充完善的API描述。"\n}\n```',
"metadata": {
"usage": {
"prompt_tokens": 73,
"prompt_unit_price": "0.0",
"prompt_price_unit": "0.0",
"prompt_price": "0.0",
"completion_tokens": 61,
"completion_unit_price": "0.0",
"completion_price_unit": "0.0",
"completion_price": "0.0",
"total_tokens": 134,
"total_price": "0.0",
"currency": "USD",
"latency": 1.896302318200469,
}
},
"created_at": 1747233054,
}
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 检查answer是否是代码块格式
if answer.startswith("```") and answer.endswith("```"):
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 获取description字段
if "description" in json_data:
return json_data["description"]
# 如果不是预期格式则返回原始answer
return data.get("answer", data)
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回原始数据
return data.get("answer", data)
def chat_message_test(
api_key,
inputs={},
response_mode="blocking",
conversation_id=None,
userId="pp666",
files=None,
):
url = "https://ops.lzwcai.com/v1/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
return response
else:
response = requests.post(url, headers=headers, json=data)
return response.json()
if __name__ == "__main__":
print("开始执行主程序")
try:
print("准备调用chat_message方法")
res = chat_message_test(
api_key="app-Ppemii3c0ROPoLvRwskgZ7Il",
inputs={"query": "今天打老虎啊按时啊啊"},
response_mode="streaming",
userId="abc-123",
)
print("chat_message方法调用完成")
# 打印响应内容
print("响应内容:", res)
# print(process_answer_code_block(res))
except Exception as e:
print(f"执行过程中出现错误: {e}")

View File

@@ -0,0 +1,172 @@
import requests
from abc import ABC
import logging
import json
import re
import pypinyin
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class DifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
print("dify_app_params", dify_app_params)
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
user="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": user,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
print(f"Error decoding JSON: {line}")
else:
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)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
# tool_name = response_map.get("name")
# translated_name = TranslationService.translate_tool_name(tool_name)
# response_map["name"] = translated_name
# # 翻译工具描述
# tool_description = response_map.get("description")
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = translated_description
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,318 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
from src.difyTaskCall.task_instance import TaskInstance
# 配置日志记录
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.11.24:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-d7s00CJ2NY4LJzUEiZsVDnPN"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.11.24:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-d7s00CJ2NY4LJzUEiZsVDnPN"]
# 确保 app_sks 始终是列表类型
if isinstance(app_sks, str):
# 如果是字符串,转换为列表
app_sks = [app_sks]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# return "https://dempdify.lzwcai.com/v1", ["app-X6wAy5nkvWB3hR69cgvIjC3r"], "workflow"
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
def process_user_input_form(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
处理后的inputSchema字典
"""
inputSchema = dict(
type="object",
properties={},
required=[],
)
property_num = len(user_input_form)
if property_num > 0:
for j in range(property_num):
param = user_input_form[j]
param_type = list(param.keys())[0]
param_info = param[param_type]
property_name = param_info["variable"]
# 根据不同控件类型处理
if param_type == "text-input":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "paragraph":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"format": "paragraph",
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "select":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"enum": param_info["options"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "file_upload":
# 文件上传控件处理
file_type_schema = {
"type": "object",
"description": param_info["label"],
"properties": {
"file_url": {"type": "string", "description": "文件URL"},
"file_name": {"type": "string", "description": "文件名称"},
},
"required": ["file_url"],
}
# 处理图片上传配置
if "image" in param_info and param_info["image"]["enabled"]:
image_config = param_info["image"]
file_type_schema["properties"]["type"] = {
"type": "string",
"description": "文件类型支持png、jpg、jpeg、webp、gif",
"enum": ["png", "jpg", "jpeg", "webp", "gif"],
}
# 处理数量限制
number_limits = image_config.get("number_limits", 3)
if number_limits > 1:
# 如果允许多个文件,则使用数组
inputSchema["properties"][property_name] = {
"type": "array",
"description": param_info["label"],
"items": file_type_schema,
"maxItems": number_limits,
}
else:
# 如果只允许单个文件
inputSchema["properties"][property_name] = file_type_schema
else:
# 如果没有特定的图片配置,使用一般文件配置
inputSchema["properties"][property_name] = file_type_schema
else:
# 默认处理为字符串类型
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
# 处理必填字段
if param_info.get("required", False):
inputSchema["required"].append(property_name)
# 添加必填的userId参数支持数字或字符串类型
# inputSchema["properties"]["userId"] = dict(
# oneOf=[{"type": "number"}, {"type": "string"}],
# description="您的员工ID用于识别您的员工身份",
# )
# inputSchema["required"].append("userId")
return inputSchema
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
responses = dify_api.chat_message(
tool_sk,
inputs=arguments,
userId=arguments.get("userId", "pp666"),
)
for res in responses:
if res["event"] == "workflow_finished":
outputs = res["data"]["outputs"]
mcp_out = []
for _, v in outputs.items():
mcp_out.append(types.TextContent(type="text", text=v))
return mcp_out
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -0,0 +1,373 @@
import json
from typing import Dict, List, Any, Tuple, Optional
import logging
from pathlib import Path
# 常量定义
DEFAULT_NUMBER_LIMITS = 3 # 默认文件数量限制
def process_user_input_form(
user_input_form: List[Dict[str, Any]],
) -> Tuple[Dict[str, Any], List[str]]:
"""
处理Dify用户输入表单生成对应的JSON Schema properties和required列表
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
properties: 表单字段的properties字典
required: 必填字段列表
"""
properties = {}
required = []
if not user_input_form:
return properties, required
for param in user_input_form:
try:
# 直接获取字典的第一个键,而不是通过list转换
param_type = next(iter(param))
param_info = param[param_type]
property_name = param_info["variable"]
properties[property_name] = {
"type": param_type,
"description": param_info["label"],
}
if param_info.get("required", False):
required.append(property_name)
except (KeyError, StopIteration) as e:
logging.warning(f"处理用户输入表单项时出错: {e}, 跳过此项")
continue
return properties, required
def process_file_upload(file_upload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""
处理Dify文件上传配置生成对应的JSON Schema properties
设计为通用实现,支持任意文件类型配置
参数:
file_upload: Dify应用的文件上传配置
返回:
file_properties: 文件上传的properties字典
"""
# 检查是否存在文件上传配置
if not file_upload:
return {}
# 收集所有启用的文件类型信息
enabled_types = []
max_items = 0
supported_transfer_methods = set()
file_type_configs = {}
for file_type, config in file_upload.items():
if not config.get("enabled", False):
continue
enabled_types.append(file_type)
number_limits = config.get("number_limits", DEFAULT_NUMBER_LIMITS)
max_items += number_limits
type_transfer_methods = config.get("transfer_methods", [])
supported_transfer_methods.update(type_transfer_methods)
# 存储每种文件类型的详细配置
file_type_configs[file_type] = {
"number_limits": number_limits,
"transfer_methods": type_transfer_methods,
}
# 如果没有启用的文件类型,返回空字典
if not enabled_types:
return {}
# 构建文件项的JSON Schema
file_item_schema = _build_file_item_schema(
enabled_types, supported_transfer_methods, file_type_configs
)
# 构建最终的files属性
file_properties = {
"files": {
"type": "array",
"items": file_item_schema,
"description": "支持多种文件类型的文件列表",
"maxItems": max_items,
}
}
return file_properties
def _build_file_item_schema(
enabled_types: List[str],
supported_transfer_methods: set,
file_type_configs: Dict[str, Dict[str, Any]],
) -> Dict[str, Any]:
"""
构建文件项的JSON Schema
参数:
enabled_types: 启用的文件类型列表
supported_transfer_methods: 支持的传输方式集合
file_type_configs: 每种文件类型的配置信息
返回:
file_item_schema: 文件项的JSON Schema
"""
file_item_schema = {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": enabled_types,
"description": "文件类型",
},
"transfer_method": {
"type": "string",
"enum": list(supported_transfer_methods),
"description": "传输方式",
},
},
}
# 添加条件属性
if "remote_url" in supported_transfer_methods:
file_item_schema["properties"]["url"] = {
"type": "string",
"format": "uri",
"description": "文件URL (当传输方式为remote_url时使用)",
}
if "local_file" in supported_transfer_methods:
file_item_schema["properties"]["upload_file_id"] = {
"type": "string",
"description": "上传文件ID (当传输方式为local_file时使用)",
}
# 添加条件验证逻辑
file_item_schema["allOf"] = []
# 为每种文件类型添加验证规则
for file_type, type_config in file_type_configs.items():
type_methods = type_config["transfer_methods"]
# 基本验证
file_item_schema["allOf"].append(
{
"if": {"properties": {"type": {"const": file_type}}},
"then": {
"properties": {
"transfer_method": {
"enum": type_methods,
"description": f"{file_type}类型支持的传输方式: {', '.join(type_methods)}",
}
}
},
}
)
# 为每种传输方法添加类型特定的验证
_add_transfer_method_validations(file_item_schema, file_type, type_methods)
return file_item_schema
def _add_transfer_method_validations(
file_item_schema: Dict[str, Any], file_type: str, type_methods: List[str]
) -> None:
"""
为每种传输方法添加类型特定的验证
参数:
file_item_schema: 文件项JSON Schema
file_type: 文件类型
type_methods: 该类型支持的传输方法列表
"""
for method in type_methods:
if method == "remote_url":
file_item_schema["allOf"].append(
{
"if": {
"properties": {
"type": {"const": file_type},
"transfer_method": {"const": "remote_url"},
},
"required": ["type", "transfer_method"],
},
"then": {"required": ["url"]},
}
)
elif method == "local_file":
file_item_schema["allOf"].append(
{
"if": {
"properties": {
"type": {"const": file_type},
"transfer_method": {"const": "local_file"},
},
"required": ["type", "transfer_method"],
},
"then": {"required": ["upload_file_id"]},
}
)
def convert_dify_params_to_schema(tool_params: Dict[str, Any]) -> Dict[str, Any]:
"""
将用户输入表单和文件上传配置组合成新的结构
参数:
tool_params: 包含user_input_form和file_upload的参数字典
返回:
组合后的结构
"""
# 参数验证
if not isinstance(tool_params, dict):
raise TypeError("tool_params 必须是字典类型")
# 处理用户输入表单
properties, required = process_user_input_form(
tool_params.get("user_input_form", [])
)
# 处理文件上传配置
file_properties = process_file_upload(tool_params.get("file_upload"))
# 创建新的结构
result = {
"inputs": {"type": "object", "properties": properties, "required": required},
# "userId": {
# "oneOf": [{"type": "number"}, {"type": "string"}],
# "description": "您的员工ID用于识别您的员工身份",
# "required": True,
# },
}
# 如果有文件上传配置添加files字段
if file_properties:
result["files"] = file_properties.get("files", {})
return result
def finalize_schema_structure(mock_result: Dict[str, Any]) -> Dict[str, Any]:
"""
将mock_result构建为符合要求的map_mock_result格式
参数:
mock_result: 通过convert_dify_params_to_schema函数获取的结果
返回:
构建后的map_mock_result字典
"""
# 确定required字段
# required_fields = ["userId"]
required_fields = []
# 只有当inputs的required有值时才添加inputs到顶层required
if (
mock_result.get("inputs", {}).get("required")
and len(mock_result["inputs"]["required"]) > 0
):
required_fields.append("inputs")
# 如果有文件上传也可以考虑添加files到required
if "files" in mock_result:
# 可以根据需求决定是否将files添加到required
# required_fields.append("files")
pass
return {
"type": "object",
"properties": mock_result,
"required": required_fields,
}
def create_json_file(data: Dict[str, Any], filename: str = "output.json") -> None:
"""
将数据保存为JSON文件
参数:
data: 要保存的数据
filename: 保存的文件名
"""
try:
# 获取mock数据
mock_result = convert_dify_params_to_schema(data)
# 使用封装的方法构建map_mock_result
map_mock_result = finalize_schema_structure(mock_result)
# 确保目标目录存在
output_path = Path(filename)
output_path.parent.mkdir(parents=True, exist_ok=True)
# 将结果写入JSON文件
with open(filename, "w", encoding="utf-8") as f:
json.dump(map_mock_result, f, ensure_ascii=False, indent=4)
logging.info(f"已成功将数据保存至 {filename}")
print(f"已成功将数据保存至 {filename}")
except Exception as e:
error_msg = f"保存JSON文件时出错: {e}"
logging.error(error_msg)
raise IOError(error_msg)
if __name__ == "__main__":
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# 示例数据
api_data = {
"opening_statement": "",
"suggested_questions": [],
"suggested_questions_after_answer": {"enabled": False},
"speech_to_text": {"enabled": False},
"text_to_speech": {"enabled": False, "language": "", "voice": ""},
"retriever_resource": {"enabled": False},
"annotation_reply": {"enabled": False},
"more_like_this": {"enabled": False},
"user_input_form": [
{
"paragraph": {
"label": "文案内容",
"max_length": 33024,
"options": [],
"required": True,
"type": "paragraph",
"variable": "content",
}
}
],
"sensitive_word_avoidance": {"enabled": False},
"file_upload": {
"image": {
"enabled": False,
"number_limits": 3,
"transfer_methods": ["local_file", "remote_url"],
}
},
"system_parameters": {"image_file_size_limit": "10"},
}
try:
create_json_file(api_data)
except Exception as e:
logging.error(f"程序执行失败: {e}")

View File

@@ -0,0 +1,53 @@
from abc import ABC
# class WorkflowDifyAPI和chatDifyApi和completionDifyApi
# dify_app_mode_type workflow, chat, completion
class TaskInstance(ABC):
def __init__(self, base_url, dify_app_sks, dify_app_mode_type):
self.base_url = base_url
self.dify_app_sks = dify_app_sks
self.dify_app_mode_type = dify_app_mode_type
def get_task_instance(self, task_id: str):
"""
根据dify_app_mode_type返回相应的API实例
Args:
task_id: 任务ID
Returns:
返回对应的API实例
Raises:
ValueError: 当dify_app_mode_type无效时抛出异常
"""
from src.workflow.workflow_server import WorkflowDifyAPI
from src.completion.completion_server import CompletionDifyAPI
from src.chat.chat_server import ChatDifyAPI
# 使用字典映射提高代码灵活性和可维护性
api_classes = {
"workflow": WorkflowDifyAPI,
"chat": ChatDifyAPI,
"completion": CompletionDifyAPI,
}
# 检查mode_type是否有效
if self.dify_app_mode_type.lower() not in api_classes:
supported_types = ", ".join(api_classes.keys())
raise ValueError(
f"不支持的dify_app_mode_type: {self.dify_app_mode_type},支持的类型: {supported_types}"
)
# 获取对应的API类
api_class = api_classes[self.dify_app_mode_type.lower()]
# 这里假设所有API类都接受相同的参数集
# 如果各API类构造函数参数不同需要针对每种类型单独处理
return api_class(
self.base_url,
self.dify_app_sks,
)

View File

@@ -0,0 +1,153 @@
import os
import sys
from typing import Dict, Any, Optional
# 导入翻译函数
from .translator import translate
class TranslationService:
"""翻译服务类,用于处理各种翻译需求"""
@staticmethod
def create_prompt(
content: str,
target_lang: str,
use_case: str,
style: str,
prompt_type: str = "general",
keep_terms_desc: str = "核心术语",
) -> str:
"""
创建翻译提示
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景
style: 翻译风格
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
keep_terms_desc: 保留术语的描述
Returns:
str: 格式化的翻译提示
"""
if prompt_type == "tool_name":
keep_terms_desc = "核心术语(如 小写,词语需要用下划线连接)"
elif prompt_type == "tool_description":
keep_terms_desc = "核心术语(这是一段话)"
return f"""
角色:专业本地化翻译专家
任务:将以下内容翻译为{target_lang}(目标用途:{use_case}
要求:
1. 仅返回译文,不含解释或原文;
2. 保留{keep_terms_desc}
3. 符合{style}风格;
4. 特殊符号保持原样。
示例输出格式:
Translated Text
待翻译内容:
{content}
"""
@staticmethod
def translate_text(
content: str,
target_lang: str,
use_case: str = "",
style: str = "正式且符合技术品牌调性",
prompt_type: str = "general",
) -> Dict[str, Any]:
"""
翻译文本
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景,默认为空
style: 翻译风格,默认为"正式且符合技术品牌调性"
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
Returns:
Dict: 包含翻译结果的字典
"""
prompt = TranslationService.create_prompt(
content=content,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type=prompt_type,
)
try:
result = translate(prompt, target_lang)
return result
except Exception as e:
print(f"翻译出错: {str(e)}")
return {"translated_text": "", "error": str(e)}
@staticmethod
def translate_tool_name(
name: str,
target_lang: str = "英语",
use_case: str = "工具名称",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具名称的便捷方法
Returns:
str: 翻译后的工具名称
"""
result = TranslationService.translate_text(
content=name,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_name",
)
return result.get("translated_text", "")
@staticmethod
def translate_tool_description(
description: str,
target_lang: str = "英语",
use_case: str = "工具描述",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具描述的便捷方法
Returns:
str: 翻译后的工具描述
"""
result = TranslationService.translate_text(
content=description,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_description",
)
return result.get("translated_text", "")
def translation_example():
"""翻译功能使用示例"""
# 示例1: 翻译工具名称
tool_name = "万川AI新媒体平台【测试环境】"
translated_name = TranslationService.translate_tool_name(tool_name)
print(f"工具名称翻译: {translated_name}")
# 示例2: 翻译工具描述
description = "21日辛柏青发布讣告宣布妻子朱媛媛抗癌五年后离世。此前在一次路演现场当观众问及朱媛媛时辛柏青2秒停顿藏着"
translated_desc = TranslationService.translate_tool_description(description)
print(f"工具描述翻译: {translated_desc}")
if __name__ == "__main__":
translation_example()

View File

@@ -0,0 +1,64 @@
import os
import requests
import json
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# ========== 模型相关 ==========
# 从.env文件获取模型API配置
BASE_URL = os.getenv("BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
API_KEY = os.getenv("OPENAI_API_KEY", "sk-c5a912a6bc8e4c9cbdbdf68232352a03")
TEMPERATURE = float(os.getenv("MODEL_TEMPERATURE", "0.7"))
def translate(content, target_language):
"""
翻译文本内容到目标语言
:param content: 要翻译的内容
:param target_language: 目标语言,如'en'(英语), 'zh'(中文), 'ja'(日语), 'fr'(法语)等
:return: 翻译后的内容,如果翻译失败则返回原文和错误信息
"""
if not content or not target_language:
return {"error": "内容或目标语言不能为空", "translated_text": content}
# 确保API密钥已设置
if not API_KEY:
return {"error": "API密钥未设置请检查.env文件", "translated_text": content}
try:
# 构建API请求头
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
# 构建翻译提示
prompt = f"请将以下内容翻译成{target_language},只返回翻译结果,不要包含任何解释或原文:\n\n{content}"
# 构建API请求体
data = {
"model": "qwen-max", # 使用通义千问模型,可以根据实际需要更改
"messages": [{"role": "user", "content": prompt}],
"temperature": TEMPERATURE,
}
# 发送API请求
response = requests.post(
f"{BASE_URL}/chat/completions", headers=headers, json=data
)
# 解析响应
if response.status_code == 200:
result = response.json()
translated_text = result["choices"][0]["message"]["content"].strip()
return {"translated_text": translated_text}
else:
error_message = (
f"翻译失败,状态码: {response.status_code}, 响应: {response.text}"
)
return {"error": error_message, "translated_text": content}
except Exception as e:
return {"error": f"翻译过程中发生错误: {str(e)}", "translated_text": content}

View File

@@ -0,0 +1,175 @@
import requests
from abc import ABC
import logging
import json
import re
logger = logging.getLogger(__name__)
import pypinyin
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
所有非字母数字字符会被替换为下划线
"""
# 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线
cleaned = re.sub(r'[^\w\s]', '_', pinyin)
# 将空格也替换为下划线
cleaned = re.sub(r'\s+', '_', cleaned)
# 移除连续的下划线并去除首尾下划线
cleaned = re.sub(r'_+', '_', cleaned).strip('_')
# 转换为拼音并生成驼峰命名
pinyin_list = pypinyin.lazy_pinyin(cleaned)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class WorkflowDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
print(f"Error decoding JSON: {line}")
else:
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)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp-test
Version: 0.0.15
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,267 @@
# Dify Workflow API 文档
## 概述
Workflow 应用无会话支持,适合用于翻译、文章写作、总结等 AI 场景。
**Base URL:** `http://192.168.2.236:3001/v1`
## 认证
所有 API 请求需在 HTTP Header 中包含 API-Key:
```
Authorization: Bearer {API_KEY}
```
## 1. 执行 Workflow
**接口:** `POST /workflows/run`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| inputs | object | ✓ | 允许传入 App 定义的各变量值。inputs 参数包含了多组键值对,每组的键对应一个特定变量,每组的值则是该变量的具体值 |
| response_mode | string | ✓ | 返回响应模式:`streaming`(流式,推荐)或`blocking`(阻塞,Cloudflare 限制 100 秒超时) |
| user | string | ✓ | 用户标识,用于定义终端用户的身份,方便检索、统计。需保证用户标识在应用内唯一 |
### 文件列表类型变量
| 字段 | 类型 | 说明 |
|------|------|------|
| type | string | 文件类型:document/image/audio/video/custom |
| transfer_method | string | 传递方式:remote_url(图片地址)或local_file(上传文件) |
| url | string | 图片地址(仅当传递方式为 remote_url 时) |
| upload_file_id | string | 上传文件 ID(仅当传递方式为 local_file 时) |
**支持的文件类型:**
- **document**: TXT, MD, PDF, HTML, XLSX, DOCX, CSV, PPTX, XML, EPUB
- **image**: JPG, PNG, GIF, WEBP, SVG
- **audio**: MP3, WAV, M4A, WEBM, AMR
- **video**: MP4, MOV, MPEG
### 响应格式
#### CompletionResponse(阻塞模式)
| 字段 | 类型 | 说明 |
|------|------|------|
| workflow_run_id | string | workflow 执行 ID |
| task_id | string | 任务 ID,用于请求跟踪和停止响应接口 |
| data.id | string | workflow 执行 ID |
| data.workflow_id | string | 关联 Workflow ID |
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.elapsed_time | float | 可选,耗时(秒) |
| data.total_tokens | int | 可选,总使用 tokens |
| data.total_steps | int | 总步数,默认 0 |
| data.created_at | timestamp | 开始时间 |
| data.finished_at | timestamp | 结束时间 |
#### ChunkCompletionResponse(流式模式)
**事件类型:**
**1. workflow_started**
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | string | 任务 ID |
| workflow_run_id | string | workflow 执行 ID |
| event | string | 固定为 workflow_started |
| data.id | string | workflow 执行 ID |
| data.workflow_id | string | 关联 Workflow ID |
| data.sequence_number | int | 自增序号,从 1 开始 |
| data.created_at | timestamp | 开始时间 |
**2. node_started**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.node_id | string | 节点 ID |
| data.node_type | string | 节点类型 |
| data.title | string | 节点名称 |
| data.index | int | 执行序号 |
| data.predecessor_node_id | string | 前置节点 ID |
| data.inputs | object | 节点中所有使用到的前置节点变量内容 |
**3. node_finished**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.node_id | string | 节点 ID |
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.elapsed_time | float | 可选,耗时(秒) |
| data.execution_metadata.total_tokens | int | 可选,总使用 tokens |
| data.execution_metadata.total_price | decimal | 可选,总费用 |
| data.execution_metadata.currency | string | 可选,货币(USD/RMB) |
**4. workflow_finished**
| 字段 | 类型 | 说明 |
|------|------|------|
| data.status | string | 执行状态:running/succeeded/failed/stopped |
| data.outputs | json | 可选,输出内容 |
| data.error | string | 可选,错误原因 |
| data.total_tokens | int | 可选,总使用 tokens |
| data.finished_at | timestamp | 结束时间 |
**5. tts_message** - TTS 音频流事件(Mp3 格式,base64 编码)
| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | string | 任务 ID |
| message_id | string | 消息唯一 ID |
| audio | string | Base64 编码的音频内容 |
| created_at | int | 创建时间戳 |
**6. tts_message_end** - TTS 音频流结束
**7. ping** - 每 10 秒心跳保活
### 错误码
| 状态码 | 错误码 | 说明 |
|--------|--------|------|
| 400 | invalid_param | 传入参数异常 |
| 400 | app_unavailable | App 配置不可用 |
| 400 | provider_not_initialize | 无可用模型凭据配置 |
| 400 | provider_quota_exceeded | 模型调用额度不足 |
| 400 | workflow_request_error | workflow 执行失败 |
| 500 | - | 服务内部异常 |
---
## 2. 查询 Workflow 执行状态
**接口:** `GET /workflows/run/:workflow_run_id`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | workflow 执行 ID |
| workflow_id | string | 关联的 Workflow ID |
| status | string | 执行状态:running/succeeded/failed/stopped |
| inputs | json | 任务输入内容 |
| outputs | json | 任务输出内容 |
| error | string | 错误原因 |
| total_steps | int | 任务执行总步数 |
| total_tokens | int | 任务执行总 tokens |
| created_at | timestamp | 任务开始时间 |
| finished_at | timestamp | 任务结束时间 |
| elapsed_time | float | 耗时(秒) |
---
## 3. 停止 Workflow 执行
**接口:** `POST /workflows/tasks/:task_id/stop`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| user | string | ✓ | 用户标识,必须和发送消息接口传入 user 保持一致 |
---
## 4. 上传文件
**接口:** `POST /files/upload`
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | ✓ | 要上传的文件 |
| user | string | ✓ | 用户标识 |
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | uuid | ID |
| name | string | 文件名 |
| size | int | 文件大小(byte) |
| extension | string | 文件后缀 |
| mime_type | string | 文件 mime-type |
| created_by | uuid | 上传人 ID |
| created_at | timestamp | 上传时间 |
### 错误码
| 状态码 | 错误码 | 说明 |
|--------|--------|------|
| 400 | no_file_uploaded | 必须提供文件 |
| 413 | file_too_large | 文件太大 |
| 415 | unsupported_file_type | 不支持的文件类型 |
| 503 | s3_connection_failed | 无法连接到 S3 服务 |
---
## 5. 获取 Workflow 日志
**接口:** `GET /workflows/logs`
### 查询参数
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| keyword | string | 关键字 | - |
| status | string | 执行状态:succeeded/failed/stopped | - |
| page | int | 当前页码 | 1 |
| limit | int | 每页条数 | 20 |
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| page | int | 当前页码 |
| limit | int | 每页条数 |
| total | int | 总条数 |
| has_more | bool | 是否还有更多数据 |
| data[].workflow_run.id | string | 标识 |
| data[].workflow_run.status | string | 执行状态 |
| data[].workflow_run.elapsed_time | float | 耗时(秒) |
| data[].workflow_run.total_tokens | int | 消耗的 token 数量 |
---
## 6. 获取应用信息
**接口:** `GET /info`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 应用名称 |
| description | string | 应用描述 |
| tags | array[string] | 应用标签 |
---
## 7. 获取应用参数
**接口:** `GET /parameters`
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| user_input_form[].text-input.label | string | 控件展示标签名 |
| user_input_form[].text-input.variable | string | 控件 ID |
| user_input_form[].text-input.required | bool | 是否必填 |
| user_input_form[].text-input.default | string | 默认值 |
| file_upload.image.enabled | bool | 是否开启 |
| file_upload.image.number_limits | int | 图片数量限制,默认 3 |
| file_upload.image.transfer_methods | array[string] | 传递方式:remote_url/local_file |
| system_parameters.file_size_limit | int | 文档上传大小限制(MB) |
| system_parameters.image_file_size_limit | int | 图片文件上传大小限制(MB) |
| system_parameters.audio_file_size_limit | int | 音频文件上传大小限制(MB) |
| system_parameters.video_file_size_limit | int | 视频文件上传大小限制(MB) |

View File

@@ -0,0 +1,2 @@
# 此文件用于确保 logs 目录被 Git 跟踪
# 日志文件会自动生成在此目录中

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-demp-tool-server-dify-to-mcp-test
Version: 0.1.0
Summary: 这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。
Requires-Python: >=3.10
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.1.2
Requires-Dist: omegaconf>=2.3.0
Requires-Dist: pip>=24.3.1
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: requests
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,27 @@
README.md
pyproject.toml
setup.cfg
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/PKG-INFO
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/SOURCES.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/dependency_links.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/entry_points.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/requires.txt
lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/top_level.txt
src/__init__.py
src/create_mcp.py
src/create_mcp_update.py
src/create_mcp_utils.py
src/chat/__init__.py
src/chat/chat_server.py
src/completion/completion_server.py
src/completion/test.py
src/core/__init__.py
src/core/core_server.py
src/difyTaskCall/task_instance.py
src/utils/dify_workflow_schema.py
src/utils/logger_config.py
src/utils/tool_translation.py
src/utils/translator.py
src/utils/upload_file.py
src/workflow/__init__.py
src/workflow/workflow_server.py

View File

@@ -0,0 +1,2 @@
[console_scripts]
lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main

View File

@@ -0,0 +1,7 @@
httpx>=0.28.1
mcp>=1.1.2
omegaconf>=2.3.0
pip>=24.3.1
python-dotenv>=1.0.1
requests
pypinyin>=0.54.0

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
主入口文件
用于启动 Dify MCP 服务器,并配置命令行参数
"""
import os
import sys
import logging
# 导入日志配置
from src.utils.logger_config import setup_logging, get_logger
# Mock 配置参数
def setup_mock_arguments():
"""
设置模拟命令行参数
这些参数可以根据实际需求进行修改
"""
# 默认配置
default_config = {
"base_url": "http://192.168.2.236:3001/v1",
"app_sks": ["app-Oo0QRJismgQADRSt8Bj0RXWB"],
"mode_type": "workflow",
"transport": "stdio"
}
# 如果没有提供命令行参数,则添加默认参数
if len(sys.argv) == 1:
sys.argv.extend([
"--base-url", default_config["base_url"],
"--app-sks", *default_config["app_sks"],
"--mode-type", default_config["mode_type"]
])
return default_config
def main():
"""
主函数:设置命令行参数并启动服务器
"""
# 初始化日志系统MCP模式下禁用控制台输出避免干扰stdio通信
try:
log_file_path = setup_logging(
log_level=logging.INFO,
console_output=False, # MCP模式下禁用控制台输出
file_output=True
)
logger = get_logger(__name__)
logger.info("=" * 80)
logger.info("Dify MCP 服务器启动")
logger.info(f"日志文件: {log_file_path}")
logger.info("=" * 80)
except Exception as e:
# 如果日志初始化失败使用stderr输出错误
print(f"[ERROR] 日志系统初始化失败: {e}", file=sys.stderr)
# 设置模拟命令行参数
config = setup_mock_arguments()
# 导入并运行 MCP 服务器
try:
from src.create_mcp import run_main
# 获取传输模式
transport_mode = config.get("transport", "stdio")
logger.info(f"传输模式: {transport_mode}")
logger.info(f"配置参数: {config}")
# 运行服务器(不输出额外信息,避免干扰 STDIO 通信)
run_main(transport=transport_mode)
except ImportError as e:
logger.error(f"导入错误: {e}", exc_info=True)
print(f"[ERROR] 导入错误: {e}", file=sys.stderr)
print("请确保已正确安装所有依赖包", file=sys.stderr)
sys.exit(1)
except Exception as e:
logger.error(f"运行错误: {e}", exc_info=True)
print(f"[ERROR] 运行错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "lzwcai-demp-tool-server-dify-to-mcp-test"
version = "0.1.0"
description = "这是一个Dify to MCP的集成工具通过Dify的API接口将Dify的模型部署到MCP平台并进行推理。"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"mcp>=1.1.2",
"omegaconf>=2.3.0",
"pip>=24.3.1",
"python-dotenv>=1.0.1",
"requests",
"pypinyin>=0.54.0",
]
[tool.setuptools]
packages = {find = {where = ["."], include = ["src*"]}}
include-package-data = true
[project.scripts]
lzwcai-demp-tool-server-dify-to-mcp-test = "src.create_mcp:run_main"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.setuptools.package-data]
"*" = ["*.env"]
"src" = ["**/*.env"]

View File

@@ -0,0 +1,4 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -0,0 +1,7 @@
class ChatDifyAPI:
def __init__(self, base_url: str, app_sks: str):
self.base_url = base_url
self.app_sks = app_sks
def process_task(self, task_id: str, **kwargs):
pass

View File

@@ -0,0 +1,203 @@
import requests
from abc import ABC
import logging
import json
import pypinyin
from src.utils.logger_config import get_logger
logger = get_logger(__name__)
def pinyin_to_camel(pinyin):
"""
将拼音列表转换为驼峰命名
例如: ['ni', 'hao', 'a'] -> 'NiHaoA'
"""
pinyin_list = pypinyin.lazy_pinyin(pinyin)
return "tool_" + "".join(word.capitalize() for word in pinyin_list)
class CompletionDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(url, headers=headers, json=data, stream=True)
# 处理流式响应
full_answer = ""
for line in response.iter_lines():
if line:
# 跳过 "data:" 前缀
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data:"):
try:
json_str = decoded_line[5:].strip()
data = json.loads(json_str)
if data.get("event") == "message" and "answer" in data:
# 累积完整答案
full_answer += data["answer"]
# 这里也可以选择处理每个部分响应,例如返回生成器
# yield data
except json.JSONDecodeError:
logger.warning(f"无法解析JSON数据: {decoded_line}")
# 创建一个符合非流式响应格式的结果
response_data = {"answer": full_answer}
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
else:
response = requests.post(url, headers=headers, json=data)
response_data = response.json()
# 处理可能包含代码块的数据
processed_data = self.process_answer_code_block(response_data)
return processed_data
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)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
# params = {"user": user}
response = requests.get(url, headers=headers)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
from src.utils.tool_translation import TranslationService
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
return {
"user_input_form": [
{"string": {"variable": "query", "label": "查询内容", "required": True}}
]
}
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
@staticmethod
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 构造符合workflow_finished格式的输出
formatted_response = [
{"event": "workflow_finished", "data": {"outputs": {"result": answer}}}
]
# 尝试处理可能的代码块
if answer.startswith("```") and answer.endswith("```"):
try:
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 如果包含description字段用它替换answer
if "description" in json_data:
formatted_response[0]["data"]["outputs"]["result"] = json_data[
"description"
]
except json.JSONDecodeError:
# 如果不是有效的JSON保留原始代码块内容
pass
return formatted_response
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回符合格式的基础响应
return [
{
"event": "workflow_finished",
"data": {
"outputs": {
"error": str(e),
"fallback": data.get("answer", str(data)),
}
},
}
]

View File

@@ -0,0 +1,104 @@
import requests
from abc import ABC
import logging
import json
logger = logging.getLogger(__name__)
res = {
"event": "message",
"task_id": "49c9ea1b-7b43-475b-a680-d769fb238a45",
"id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"message_id": "432ab98e-5e36-4a29-abe5-e01281c3678c",
"mode": "completion",
"answer": '```\n{\n "description": "该API的具体功能描述暂时不明确因为提供的API信息 \'今天打老虎啊按时啊啊\' 并不是有效的API名称或描述。请提供正确的API名称和相关输入输出信息以便我能为其补充完善的API描述。"\n}\n```',
"metadata": {
"usage": {
"prompt_tokens": 73,
"prompt_unit_price": "0.0",
"prompt_price_unit": "0.0",
"prompt_price": "0.0",
"completion_tokens": 61,
"completion_unit_price": "0.0",
"completion_price_unit": "0.0",
"completion_price": "0.0",
"total_tokens": 134,
"total_price": "0.0",
"currency": "USD",
"latency": 1.896302318200469,
}
},
"created_at": 1747233054,
}
def process_answer_code_block(data):
try:
# 获取answer字段
answer = data.get("answer", "")
# 检查answer是否是代码块格式
if answer.startswith("```") and answer.endswith("```"):
# 移除代码块标记并解析JSON
code_content = answer.strip("```").strip()
json_data = json.loads(code_content)
# 获取description字段
if "description" in json_data:
return json_data["description"]
# 如果不是预期格式则返回原始answer
return data.get("answer", data)
except Exception as e:
logger.warning(f"处理答案代码块时出错: {str(e)}")
# 发生错误时返回原始数据
return data.get("answer", data)
def chat_message_test(
api_key,
inputs={},
response_mode="blocking",
conversation_id=None,
userId="pp666",
files=None,
):
url = "https://ops.lzwcai.com/v1/completion-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
if conversation_id:
data["conversation_id"] = conversation_id
if response_mode == "streaming":
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
return response
else:
response = requests.post(url, headers=headers, json=data)
return response.json()
if __name__ == "__main__":
print("开始执行主程序")
try:
print("准备调用chat_message方法")
res = chat_message_test(
api_key="app-Ppemii3c0ROPoLvRwskgZ7Il",
inputs={"query": "今天打老虎啊按时啊啊"},
response_mode="streaming",
userId="abc-123",
)
print("chat_message方法调用完成")
# 打印响应内容
print("响应内容:", res)
# print(process_answer_code_block(res))
except Exception as e:
print(f"执行过程中出现错误: {e}")

View File

@@ -0,0 +1,213 @@
import requests
from abc import ABC
import logging
import json
# 导入 pypinyin 用于中文转拼音
try:
import pypinyin
except ImportError:
pypinyin = None
logging.warning("pypinyin 模块未安装,将使用简化的命名方式")
logger = logging.getLogger(__name__)
def pinyin_to_camel(pinyin):
"""
将中文名称转换为工具名称
处理逻辑:
1. 如果安装了 pypinyin将中文转换为拼音然后转为驼峰命名
2. 如果未安装 pypinyin将所有非字母数字字符替换为下划线
3. 所有符号都会被替换成下划线
示例:
"你好啊" -> "tool_NiHaoA" (有pypinyin)
"测试-工具" -> "tool_测试_工具" (无pypinyin)
"Hello World!" -> "tool_Hello_World_" (无pypinyin)
Args:
pinyin: 输入的字符串(可能包含中文、英文、符号等)
Returns:
str: 格式化后的工具名称,以 "tool_" 开头
"""
import re
if pypinyin is None:
# 如果 pypinyin 未安装,使用简化的命名方式
# 将所有非字母数字字符(包括空格、符号等)替换为下划线
cleaned = re.sub(r'[^\w]', '_', str(pinyin))
# 移除连续的下划线
cleaned = re.sub(r'_+', '_', cleaned)
# 移除首尾的下划线
cleaned = cleaned.strip('_')
return "tool_" + cleaned if cleaned else "tool_unnamed"
# 使用 pypinyin 转换中文为拼音
pinyin_list = pypinyin.lazy_pinyin(pinyin)
# 处理每个拼音单词
processed_words = []
for word in pinyin_list:
# 将所有非字母数字字符替换为下划线
cleaned_word = re.sub(r'[^\w]', '_', word)
# 移除连续的下划线
cleaned_word = re.sub(r'_+', '_', cleaned_word)
# 移除首尾的下划线
cleaned_word = cleaned_word.strip('_')
if cleaned_word:
# 首字母大写(驼峰命名)
processed_words.append(cleaned_word.capitalize())
# 拼接所有单词
result = "".join(processed_words) if processed_words else "Unnamed"
return "tool_" + result
class DifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
logger.info(f"Dify应用参数: {dify_app_params}")
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
user="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": user,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = []
for file_info in files:
file_path = file_info.get("path")
transfer_method = file_info.get("transfer_method")
if transfer_method == "local_file":
files_data.append(("file", open(file_path, "rb")))
elif transfer_method == "remote_url":
pass
response = requests.post(
url,
headers=headers,
data=data,
files=files_data,
stream=response_mode == "streaming",
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
response.raise_for_status()
if response_mode == "streaming":
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
logger.error(f"JSON解码错误: {line}")
else:
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)
response.raise_for_status()
return response.json()
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
from src.utils.tool_translation import TranslationService
response_map = response.json()
# 翻译工具名称
# tool_name = response_map.get("name")
# translated_name = TranslationService.translate_tool_name(tool_name)
# response_map["name"] = translated_name
# # 翻译工具描述
# tool_description = response_map.get("description")
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = translated_description
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,233 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
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
# 使用统一的日志配置
logger = get_logger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.2.236:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-RBS0TuYEnqm8Q1cRQingkuhf"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.2.236:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-RBS0TuYEnqm8Q1cRQingkuhf"]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
# 创建工具
file_config = {
"file_fields": {}, # 字典key为工具名称value为该工具的文件字段列表
"file_type_dicts": {} # 字典key为工具名称value为该工具的文件类型字典
}
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 记录 parameters API 返回的原始数据
logger.info(f"工具 {app_info['name']} 的 parameters 数据: {app_param}")
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
# 提取所有文件字段并存储到全局字典中
tool_file_fields = extract_file_fields(app_param["user_input_form"])
logger.info(f"工具 {app_info['name']} 提取的文件字段: {tool_file_fields}")
file_config["file_fields"][app_info["name"]] = tool_file_fields
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
# 获取当前工具的文件字段信息
current_tool_file_fields = file_config["file_fields"].get(name, [])
logger.info(f"工具 {name} 的文件字段信息: {current_tool_file_fields}")
logger.info(f"工具 {name} 调用前的 arguments: {arguments}")
# 使用工具函数处理文件字段
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,
)
# 初始化 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
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -0,0 +1,320 @@
import asyncio
import json
import os
import logging
import argparse
from abc import ABC
import mcp.server.stdio
import mcp.types as types
import requests
from mcp.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from omegaconf import OmegaConf
# from src.workflow.workflow_server import WorkflowDifyAPI
from src.difyTaskCall.task_instance import TaskInstance
from src.utils.dify_workflow_schema import process_user_input_form
# 配置日志记录
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def parse_arguments():
parser = argparse.ArgumentParser(description="Dify MCP服务器配置")
parser.add_argument(
"--base-url",
type=str,
help="API基础URL",
default="http://192.168.2.236:3001/v1",
)
parser.add_argument(
"--app-sks",
nargs="+",
help="应用秘钥列表",
default=["app-XaRWpeL2Yfdguc5ul3ScXvPE"],
)
parser.add_argument(
"--mode-type",
type=str,
help="Dify应用模式类型 (workflow, chat, completion)",
default="workflow",
choices=["workflow", "chat", "completion"],
)
return parser.parse_args()
def get_app_info(base_url=None, app_sks=None, mode_type=None):
# 获取命令行参数
args = parse_arguments()
# 命令行参数优先,其次是函数参数,最后是默认值
if args.base_url is not None:
base_url = args.base_url
if base_url is None:
base_url = "http://192.168.2.236:3001/v1"
if args.app_sks is not None:
app_sks = args.app_sks
if app_sks is None:
app_sks = ["app-XaRWpeL2Yfdguc5ul3ScXvPE"]
if args.mode_type is not None:
mode_type = args.mode_type
if mode_type is None:
mode_type = "workflow"
return base_url, app_sks, mode_type
# 初始化服务器和Dify API
base_url, dify_app_sks, dify_app_mode_type = get_app_info()
server = Server("dify_mcp_server")
task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type)
dify_api = task_instance.get_task_instance(dify_app_mode_type)
def process_user_input_form1(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
参数:
user_input_form: Dify应用的用户输入表单配置
返回:
处理后的inputSchema字典
"""
inputSchema = dict(
type="object",
properties={},
required=[],
)
property_num = len(user_input_form)
if property_num > 0:
for j in range(property_num):
param = user_input_form[j]
param_type = list(param.keys())[0]
param_info = param[param_type]
property_name = param_info["variable"]
# 根据不同控件类型处理
if param_type == "text-input":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "paragraph":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"format": "paragraph",
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "select":
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
"enum": param_info["options"],
}
if "default" in param_info:
inputSchema["properties"][property_name]["default"] = param_info[
"default"
]
elif param_type == "file_upload":
# 文件上传控件处理
file_type_schema = {
"type": "object",
"description": param_info["label"],
"properties": {
"file_url": {"type": "string", "description": "文件URL"},
"file_name": {"type": "string", "description": "文件名称"},
},
"required": ["file_url"],
}
# 处理图片上传配置
if "image" in param_info and param_info["image"]["enabled"]:
image_config = param_info["image"]
file_type_schema["properties"]["type"] = {
"type": "string",
"description": "文件类型支持png、jpg、jpeg、webp、gif",
"enum": ["png", "jpg", "jpeg", "webp", "gif"],
}
# 处理数量限制
number_limits = image_config.get("number_limits", 3)
if number_limits > 1:
# 如果允许多个文件,则使用数组
inputSchema["properties"][property_name] = {
"type": "array",
"description": param_info["label"],
"items": file_type_schema,
"maxItems": number_limits,
}
else:
# 如果只允许单个文件
inputSchema["properties"][property_name] = file_type_schema
else:
# 如果没有特定的图片配置,使用一般文件配置
inputSchema["properties"][property_name] = file_type_schema
else:
# 默认处理为字符串类型
inputSchema["properties"][property_name] = {
"type": "string",
"description": param_info["label"],
}
# 处理必填字段
if param_info.get("required", False):
inputSchema["required"].append(property_name)
# 添加必填的userId参数支持数字或字符串类型
inputSchema["properties"]["userId"] = dict(
oneOf=[{"type": "number"}, {"type": "string"}],
description="您的员工ID用于识别您的员工身份",
)
inputSchema["required"].append("userId")
return inputSchema
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
列出可用的工具
返回:
工具列表每个工具都使用JSON Schema验证其参数
"""
tools = []
tool_names = dify_api.dify_app_names
tool_infos = dify_api.dify_app_infos
tool_params = dify_api.dify_app_params
tool_num = len(tool_names)
for i in range(tool_num):
# 加载每个工具的应用信息
app_info = tool_infos[i]
# 加载每个工具的应用参数
app_param = tool_params[i]
# 处理用户输入表单
inputSchema = process_user_input_form(app_param["user_input_form"])
tools.append(
types.Tool(
name=app_info["name"],
description=app_info["description"],
inputSchema=inputSchema,
)
)
return tools
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
调用工具处理请求
参数:
name: 工具名称
arguments: 工具参数
返回:
处理结果列表
"""
tool_names = dify_api.dify_app_names
if name in tool_names:
tool_idx = tool_names.index(name)
tool_sk = dify_api.dify_app_sks[tool_idx]
# 提取files参数并创建不包含files的inputs对象
files = arguments.get("files", None) if arguments else None
inputs = {k: v for k, v in (arguments or {}).items() if k != "files"}
responses = dify_api.chat_message(
tool_sk,
inputs=inputs,
userId=arguments.get("userId", "pp666") if arguments else "pp666",
files=files,
)
for res in responses:
if res["event"] == "workflow_finished":
outputs = res["data"]["outputs"]
mcp_out = []
for _, v in outputs.items():
mcp_out.append(types.TextContent(type="text", text=v))
return mcp_out
else:
raise ValueError(f"Unknown tool: {name}")
def run_main(transport="stdio"):
"""
主函数使用stdin/stdout流运行服务器
"""
if transport == "stdio":
import anyio
from mcp.server.stdio import stdio_server
async def arun():
async with stdio_server() as streams:
await server.run(
streams[0],
streams[1],
InitializationOptions(
server_name="dify_mcp_server",
server_version="0.0.6",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
anyio.run(arun)
else:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Mount, Route
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
return Response()
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
],
)
import uvicorn
uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info")
if __name__ == "__main__":
run_main()

View File

@@ -0,0 +1,326 @@
"""
MCP创建工具的辅助函数模块
包含文件字段处理、参数预处理等功能
"""
import logging
import re
from urllib.parse import urlparse
import os
from src.utils.logger_config import get_logger
logger = get_logger(__name__)
file_type_details = {
"document": {
"extensions": "TXT, MD, MARKDOWN, PDF, HTML, XLSX, XLS, DOCX, CSV, EML, MSG, PPTX, PPT, XML, EPUB",
"description": "文档文件"
},
"image": {
"extensions": "JPG, JPEG, PNG, GIF, WEBP, SVG",
"description": "图片文件"
},
"audio": {
"extensions": "MP3, M4A, WAV, WEBM, AMR",
"description": "音频文件"
},
"video": {
"extensions": "MP4, MOV, MPEG, MPGA",
"description": "视频文件"
},
"custom": {
"extensions": "",
"description": "其他文件类型"
}
}
def process_file_arguments(arguments, current_tool_file_fields, dify_api, tool_name):
"""
处理arguments中的文件类型字段
Args:
arguments (dict): 工具调用的参数字典
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': []}]
dify_api: Dify API实例包含file_parameter_pretreatment方法
tool_name (str): 工具名称,用于日志记录
Returns:
dict: 处理后的arguments字典
Raises:
Exception: 当文件处理过程中发生严重错误时抛出
"""
if not arguments or not current_tool_file_fields:
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}
logger.info(f"工具 {tool_name} 的文件字段变量名: {file_field_variables}")
# 创建arguments的副本避免修改原始数据
processed_arguments = arguments.copy()
# 处理arguments中的文件字段
for arg_name, arg_value in arguments.items():
# 检查参数名是否是文件字段
if arg_name in file_field_variables:
logger.info(f"工具 {tool_name}: 发现文件字段 {arg_name},值: {arg_value}")
# 检查参数值是否包含文件信息
files_to_process = _extract_files_from_argument(arg_value, arg_name, tool_name)
if not files_to_process:
logger.info(f"工具 {tool_name}: 字段 {arg_name} 不是文件格式,跳过处理")
continue
# 调用文件预处理方法
try:
logger.info(f"工具 {tool_name}: 准备处理文件列表: {files_to_process}")
# 为每个文件对象添加必要的字段
for file_obj in files_to_process:
# 设置传输方式为 remote_url从URL下载并上传
if 'transfer_method' not in file_obj:
file_obj['transfer_method'] = 'remote_url'
# 自动识别文件类型
if 'type' not in file_obj and 'url' in file_obj:
file_obj['type'] = get_file_type_from_url(file_obj['url'])
logger.info(f"工具 {tool_name}: 自动识别文件类型为 {file_obj['type']}")
# 调用文件预处理下载文件并上传到Dify获取upload_file_id
processed_files = dify_api.file_parameter_pretreatment(files_to_process)
if not processed_files or len(processed_files) == 0:
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')}")
# 更新processed_arguments中的值
_update_processed_argument(processed_arguments, arg_name, arg_value, processed_files_item, tool_name)
except Exception as e:
logger.error(f"工具 {tool_name}: 处理文件字段 {arg_name} 时发生错误: {str(e)}")
# 继续执行,不中断整个流程
continue
return processed_arguments
def _extract_files_from_argument(arg_value, arg_name, tool_name):
"""
从参数值中提取文件信息
Args:
arg_value: 参数值可以是字符串URL、文件对象或文件列表
arg_name (str): 参数名称
tool_name (str): 工具名称
Returns:
list: 文件列表如果不是文件格式则返回None
"""
# 情况1: 单个字符串URL
if isinstance(arg_value, str):
# 检查是否是有效的URL
if arg_value.startswith(('http://', 'https://')):
file_obj = {'url': arg_value}
logger.info(f"工具 {tool_name}: 将字符串URL转换为文件对象: {file_obj}")
return [file_obj]
else:
logger.warning(f"工具 {tool_name}: 字符串不是有效的URL: {arg_value}")
return None
# 情况2: 单个文件对象
if isinstance(arg_value, dict) and _is_file_object(arg_value):
files_to_process = [arg_value]
logger.info(f"工具 {tool_name}: 处理单个文件对象: {files_to_process}")
return files_to_process
# 情况3: 文件列表
elif isinstance(arg_value, list) and len(arg_value) > 0:
# 检查列表中是否包含文件对象或URL字符串
valid_files = []
for item in arg_value:
if isinstance(item, dict) and _is_file_object(item):
valid_files.append(item)
elif isinstance(item, str) and item.startswith(('http://', 'https://')):
valid_files.append({'url': item})
if valid_files:
logger.info(f"工具 {tool_name}: 处理文件列表: {valid_files}")
return valid_files
return None
def _is_file_object(obj):
"""
判断对象是否是文件对象
Args:
obj (dict): 要检查的对象
Returns:
bool: 如果是文件对象返回True否则返回False
"""
if not isinstance(obj, dict):
return False
# 检查是否包含文件对象的关键字段
file_indicators = ['type', 'transfer_method', 'url', 'upload_file_id', 'file_url', 'file_name']
return any(key in obj for key in file_indicators)
def _update_processed_argument(processed_arguments, arg_name, original_value, processed_files, tool_name):
"""
更新处理后的参数值
重要Dify API 的文件字段始终需要一个文件对象列表,即使只有一个文件
Args:
processed_arguments (dict): 处理后的参数字典
arg_name (str): 参数名称
original_value: 原始参数值可以是字符串URL、文件对象或文件列表
processed_files: 处理后的文件对象(单个对象,不是列表)
tool_name (str): 工具名称
"""
# 注意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:
logger.warning(f"工具 {tool_name}: 文件处理后为空,保持原值")
logger.info(f"工具 {tool_name}: 字段 {arg_name} 最终值: {processed_arguments[arg_name]}")
def validate_file_field_configuration(file_fields, tool_name):
"""
验证文件字段配置的有效性
Args:
file_fields (list): 文件字段配置列表
tool_name (str): 工具名称
Returns:
bool: 配置是否有效
"""
if not file_fields:
return True
for field in file_fields:
if not isinstance(field, dict):
logger.warning(f"工具 {tool_name}: 文件字段配置不是字典格式: {field}")
return False
if 'variable' not in field or not field['variable']:
logger.warning(f"工具 {tool_name}: 文件字段缺少variable字段: {field}")
return False
return True
def get_file_field_summary(file_fields, tool_name):
"""
获取文件字段的摘要信息
Args:
file_fields (list): 文件字段配置列表
tool_name (str): 工具名称
Returns:
dict: 文件字段摘要信息
"""
if not file_fields:
return {
'count': 0,
'variables': [],
'required_count': 0,
'optional_count': 0
}
variables = [field.get('variable', '') for field in file_fields if field.get('variable')]
required_count = sum(1 for field in file_fields if field.get('required', False))
optional_count = len(file_fields) - required_count
summary = {
'count': len(file_fields),
'variables': variables,
'required_count': required_count,
'optional_count': optional_count
}
logger.info(f"工具 {tool_name} 文件字段摘要: {summary}")
return summary
def get_file_type_from_url(url):
"""
根据URL地址返回文件类型
Args:
url (str): 文件的URL地址
Returns:
str: 文件类型 ('document', 'image', 'audio', 'video', 'custom')
Examples:
>>> get_file_type_from_url("http://example.com/file.pdf")
'document'
>>> get_file_type_from_url("http://example.com/image.jpg")
'image'
>>> get_file_type_from_url("http://example.com/video.mp4")
'video'
"""
if not url or not isinstance(url, str):
logger.warning(f"无效的URL: {url}")
return 'custom'
try:
# 解析URL获取路径
parsed_url = urlparse(url)
path = parsed_url.path
# 从路径中提取文件扩展名
file_extension = os.path.splitext(path)[1].lower().lstrip('.')
# 如果没有扩展名,尝试从查询参数中提取
if not file_extension:
# 使用正则表达式匹配常见的文件扩展名模式
extension_pattern = r'\.([a-zA-Z0-9]{2,5})(?:\?|$|&)'
match = re.search(extension_pattern, url)
if match:
file_extension = match.group(1).lower()
logger.info(f"从URL {url} 提取的文件扩展名: {file_extension}")
# 根据扩展名判断文件类型
for file_type, details in file_type_details.items():
if file_type == 'custom':
continue
# 将支持的扩展名转换为小写列表
supported_extensions = [ext.strip().lower() for ext in details['extensions'].split(',')]
if file_extension in supported_extensions:
logger.info(f"URL {url} 匹配文件类型: {file_type}")
return file_type
# 如果没有匹配到任何类型返回custom
logger.info(f"URL {url} 未匹配到已知文件类型返回custom")
return 'custom'
except Exception as e:
logger.error(f"解析URL {url} 时发生错误: {str(e)}")
return 'custom'

View File

@@ -0,0 +1,53 @@
from abc import ABC
# class WorkflowDifyAPI和chatDifyApi和completionDifyApi
# dify_app_mode_type workflow, chat, completion
class TaskInstance(ABC):
def __init__(self, base_url, dify_app_sks, dify_app_mode_type):
self.base_url = base_url
self.dify_app_sks = dify_app_sks
self.dify_app_mode_type = dify_app_mode_type
def get_task_instance(self, task_id: str):
"""
根据dify_app_mode_type返回相应的API实例
Args:
task_id: 任务ID
Returns:
返回对应的API实例
Raises:
ValueError: 当dify_app_mode_type无效时抛出异常
"""
from src.workflow.workflow_server import WorkflowDifyAPI
from src.completion.completion_server import CompletionDifyAPI
from src.chat.chat_server import ChatDifyAPI
# 使用字典映射提高代码灵活性和可维护性
api_classes = {
"workflow": WorkflowDifyAPI,
"chat": ChatDifyAPI,
"completion": CompletionDifyAPI,
}
# 检查mode_type是否有效
if self.dify_app_mode_type.lower() not in api_classes:
supported_types = ", ".join(api_classes.keys())
raise ValueError(
f"不支持的dify_app_mode_type: {self.dify_app_mode_type},支持的类型: {supported_types}"
)
# 获取对应的API类
api_class = api_classes[self.dify_app_mode_type.lower()]
# 这里假设所有API类都接受相同的参数集
# 如果各API类构造函数参数不同需要针对每种类型单独处理
return api_class(
self.base_url,
self.dify_app_sks,
)

View File

@@ -0,0 +1,399 @@
import json
import logging
# 获取日志器
logger = logging.getLogger(__name__)
data={
"user_input_form": [
{
"file": {
"variable": "files",
"label": "files",
"type": "file",
"max_length": 48,
"required": True,
"options": [],
"allowed_file_upload_methods": [
"local_file",
"remote_url"
],
"allowed_file_types": [
"image",
"document"
],
"allowed_file_extensions": []
}
}
],
}
data2={
"user_input_form": [
{
"paragraph": {
"label": "产品名称",
"max_length": 33024,
"options": [],
"required": True,
"type": "paragraph",
"variable": "name"
}
},
{
"paragraph": {
"label": "补充描述",
"max_length": 33024,
"options": [],
"required": False,
"type": "paragraph",
"variable": "desc"
}
}
],
}
data3={
"user_input_form": [
{
"file": {
"allowed_file_extensions": [],
"allowed_file_types": [
"document","image"
],
"allowed_file_upload_methods": [
"local_file",
"remote_url"
],
"label": "输入",
"max_length": 48,
"options": [],
"required": True,
"type": "file",
"variable": "txt"
}
}
]
}
def generate_file_type_description(allowed_file_types):
"""
根据allowed_file_types生成文件类型描述
参数:
allowed_file_types (list): 允许的文件类型列表
返回:
str: 生成的文件类型描述
"""
# 定义各种文件类型的具体格式和中文描述
file_type_details = {
"document": {
"extensions": "TXT, MD, MARKDOWN, PDF, HTML, XLSX, XLS, DOCX, CSV, EML, MSG, PPTX, PPT, XML, EPUB",
"description": "文档文件"
},
"image": {
"extensions": "JPG, JPEG, PNG, GIF, WEBP, SVG",
"description": "图片文件"
},
"audio": {
"extensions": "MP3, M4A, WAV, WEBM, AMR",
"description": "音频文件"
},
"video": {
"extensions": "MP4, MOV, MPEG, MPGA",
"description": "视频文件"
},
"custom": {
"extensions": "",
"description": "其他文件类型"
}
}
if not allowed_file_types or len(allowed_file_types) == 0:
return "请提供有效的文件URL地址"
# 生成描述
descriptions = []
all_extensions = []
for file_type in allowed_file_types:
if file_type in file_type_details:
detail = file_type_details[file_type]
if detail["extensions"]:
descriptions.append(f"{detail['description']}({detail['extensions']})")
all_extensions.extend(detail["extensions"].split(", "))
else:
descriptions.append(detail["description"])
if descriptions:
if len(descriptions) == 1:
return f"请提供{descriptions[0]}的URL地址"
else:
return f"请提供文件URL地址支持的文件类型{' | '.join(descriptions)}"
else:
return "请提供有效的文件URL地址"
def process_user_input_form(user_input_form):
"""
处理Dify应用的用户输入表单转换为JSON Schema格式
支持的控件类型:
- text-input (object): 文本输入控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
- paragraph (object): 段落文本输入控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
- select (object): 下拉控件
* label (string): 控件展示标签名
* variable (string): 控件 ID
* required (bool): 是否必填
* default (string): 默认值
* options (array[string]): 选项值
- file (object): 文件上传控件 (支持复杂的文件处理逻辑)
参数:
user_input_form (array[object]): 用户输入表单配置
返回:
dict: 处理后的inputSchema字典符合JSON Schema规范
"""
# 初始化基础schema结构
inputSchema = {
"type": "object",
"properties": {},
"required": [],
}
# 如果没有用户输入表单配置,跳过处理
if not user_input_form or len(user_input_form) == 0:
pass
else:
# 遍历处理每个表单控件
for form_item in user_input_form:
# 检查form_item是否为None或空
if not form_item or not isinstance(form_item, dict):
continue
# 获取控件类型和配置信息
control_type = list(form_item.keys())[0]
logger.debug(f"处理控件类型: {control_type}")
control_config = form_item[control_type]
# 检查control_config是否为None
if not control_config or not isinstance(control_config, dict):
continue
# 提取控件基础属性
variable = control_config.get("variable", "")
label = control_config.get("label", "")
required = control_config.get("required", False)
default_value = control_config.get("default")
# 跳过没有variable的无效控件
if not variable:
continue
# 根据控件类型进行相应处理
property_schema = None
if control_type == "text-input":
# 文本输入控件处理
property_schema = {
"type": "string",
"description": label or f"文本输入字段: {variable}",
}
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type == "paragraph":
# 段落文本输入控件处理
property_schema = {
"type": "string",
"description": label or f"段落文本字段: {variable}",
"format": "textarea", # 标识为多行文本输入
}
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type == "select":
# 下拉控件处理
options = control_config.get("options", [])
property_schema = {
"type": "string",
"description": label or f"下拉选择字段: {variable}",
}
# 设置选项枚举值
if options and len(options) > 0:
property_schema["enum"] = options
# 设置默认值
if default_value is not None:
property_schema["default"] = str(default_value)
elif control_type == "file":
# 文件上传控件处理 - 简化版本仅支持remote_url
# 获取允许的文件类型
allowed_file_types = control_config.get("allowed_file_types", [])
# 生成动态的URL描述
url_description = generate_file_type_description(allowed_file_types)
file_schema = {
"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"]
}
# 处理文件数量限制
max_length = control_config.get("max_length", 1)
if max_length > 1:
# 多文件上传场景
property_schema = {
"type": "array",
"description": label or f"多文件上传字段: {variable}",
"items": file_schema,
"maxItems": max_length,
"minItems": 1 if required else 0
}
else:
# 单文件上传场景
property_schema = file_schema
else:
# 未知控件类型的默认处理
property_schema = {
"type": "string",
"description": label or f"未知类型字段: {variable} (类型: {control_type})",
}
if default_value is not None:
property_schema["default"] = str(default_value)
# 将处理后的属性添加到schema中
if property_schema:
inputSchema["properties"][variable] = property_schema
# 处理必填字段约束
if required:
inputSchema["required"].append(variable)
return inputSchema
def extract_file_fields(user_input_form):
"""
从用户输入表单中提取所有type为file的字段信息
参数:
user_input_form (array[object]): 用户输入表单配置
返回:
list: 包含所有file类型字段信息的列表每个元素包含
- variable (str): 字段变量名
- label (str): 字段标签
- required (bool): 是否必填
- max_length (int): 最大文件数量
- allowed_file_types (list): 允许的文件类型
- allowed_file_upload_methods (list): 允许的上传方式
- allowed_file_extensions (list): 允许的文件扩展名
"""
file_fields = []
# 如果没有用户输入表单配置,返回空列表
if not user_input_form or len(user_input_form) == 0:
return file_fields
# 遍历处理每个表单控件
for form_item in user_input_form:
# 检查form_item是否为None或空
if not form_item or not isinstance(form_item, dict):
continue
# 获取控件类型和配置信息
control_type = list(form_item.keys())[0]
control_config = form_item[control_type]
# 检查control_config是否为None
if not control_config or not isinstance(control_config, dict):
continue
# 只处理type为file或file-list的字段
if control_type in ["file", "file-list"] or control_config.get("type") in ["file", "file-list"]:
# 提取文件字段的详细信息
file_field_info = {
"variable": control_config.get("variable", ""),
"label": control_config.get("label", ""),
"required": control_config.get("required", False),
"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", [])
}
# 只添加有效的字段必须有variable
if file_field_info["variable"]:
file_fields.append(file_field_info)
return file_fields
if __name__ == "__main__":
# run_main()
result = process_user_input_form(data3["user_input_form"])
print("开始生成 Schema...", result)
# 保存到当前目录下的JSON文件
output_file = "process_user_input_form_output.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
# print(f"结果已保存到: {output_file}")
# # 测试新的extract_file_fields方法
# print("\n=== 测试 extract_file_fields 方法 ===")
# # 测试data包含file字段
# file_fields_data = extract_file_fields(data["user_input_form"])
# print("data中的file字段:", file_fields_data)
# # 测试data2不包含file字段
# file_fields_data2 = extract_file_fields(data2["user_input_form"])
# print("data2中的file字段:", file_fields_data2)
# 测试data3包含file字段
# file_fields_data3 = extract_file_fields(data3["user_input_form"])
# print("data3中的file字段:", file_fields_data3)

View File

@@ -0,0 +1,552 @@
"""
统一日志配置模块
这个模块提供了整个项目的统一日志配置和管理功能,确保所有组件使用一致的日志格式和输出方式。
主要功能:
1. 统一的日志格式配置
2. 支持控制台和文件双重输出
3. 日志文件轮转管理
4. MCP模式下的特殊处理禁用控制台输出
5. 便捷的日志器获取接口
6. 丰富的日志工具函数
设计特点:
- 单例模式确保配置一致性
- 支持动态配置调整
- 异常安全的编码处理
- 详细的调试信息记录
作者: lzwcai
版本: 1.0.0
"""
import logging
import logging.handlers
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
class LoggerConfig:
"""
日志配置管理器
这个类采用单例模式管理整个项目的日志配置。
它提供了统一的日志格式、文件轮转、编码处理等功能。
主要特性:
- 单例模式:确保全局日志配置一致
- 双重输出:同时支持控制台和文件输出
- 文件轮转:自动管理日志文件大小和数量
- 编码安全:正确处理中文字符
- MCP兼容支持MCP模式下的特殊需求
配置参数:
DEFAULT_LOG_LEVEL: 默认日志级别INFO
DEFAULT_LOG_FORMAT: 日志格式模板
DEFAULT_DATE_FORMAT: 时间格式
LOG_FILE_NAME: 日志文件名
MAX_LOG_SIZE: 单个日志文件最大大小10MB
BACKUP_COUNT: 保留的备份文件数量5个
"""
# ==================== 默认配置常量 ====================
# 默认日志级别INFO级别平衡了信息量和性能
DEFAULT_LOG_LEVEL = logging.INFO
# 默认日志格式:包含时间、模块名、级别、文件位置、消息内容
DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
# 默认时间格式:标准的年-月-日 时:分:秒格式
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
# ==================== 日志文件配置 ====================
# 日志文件名:使用项目名称作为前缀
LOG_FILE_NAME = "lzwcai_demp_tool_server_dify_to_mcp_test.log"
# 单个日志文件最大大小10MB
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB
# 保留的备份文件数量5个总共约50MB的日志存储
BACKUP_COUNT = 5
# ==================== 单例模式状态 ====================
# 初始化标志:确保只初始化一次
_initialized = False
# 日志文件路径:记录当前使用的日志文件路径
_log_file_path = None
@classmethod
def setup_logging(
cls,
log_level: int = DEFAULT_LOG_LEVEL,
log_file: Optional[str] = None,
console_output: bool = True,
file_output: bool = True
) -> str:
"""
设置项目统一日志配置
这是日志系统的核心初始化方法,负责配置整个项目的日志输出。
采用单例模式,确保在整个应用生命周期中只初始化一次。
配置流程:
1. 检查是否已经初始化(单例模式)
2. 确定日志文件路径(自动或手动指定)
3. 创建必要的目录结构
4. 配置根日志器和处理器
5. 设置日志格式化器
6. 添加控制台和文件处理器
7. 记录初始化信息
特殊处理:
- MCP模式下通常禁用控制台输出避免干扰stdio通信
- Windows系统下的UTF-8编码处理
- 日志文件的自动轮转管理
参数:
log_level: 日志级别DEBUG, INFO, WARNING, ERROR, CRITICAL
log_file: 日志文件路径None时使用默认路径
console_output: 是否输出到控制台MCP模式下通常为False
file_output: 是否输出到文件通常为True
返回:
str: 实际使用的日志文件路径
注意事项:
- 这个方法是线程安全的
- 重复调用会直接返回已配置的路径
- 日志文件会自动创建必要的目录
"""
# 单例模式检查:如果已经初始化,直接返回
if cls._initialized:
return cls._log_file_path
# ==================== 日志文件路径配置 ====================
if log_file is None:
# 自动确定日志文件路径:项目根目录/logs/ + 默认文件名
project_root = cls._get_project_root()
logs_dir = project_root / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
log_file = logs_dir / cls.LOG_FILE_NAME
else:
# 使用指定的日志文件路径
log_file = Path(log_file)
# 确保日志目录存在(递归创建)
log_file.parent.mkdir(parents=True, exist_ok=True)
cls._log_file_path = str(log_file)
# ==================== 根日志器配置 ====================
# 配置根日志器,这样可以捕获所有模块的日志
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# 清除根日志器上现有的处理器,避免重复配置
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# ==================== 日志格式化器 ====================
# 创建统一的日志格式化器
formatter = logging.Formatter(
fmt=cls.DEFAULT_LOG_FORMAT, # 日志格式模板
datefmt=cls.DEFAULT_DATE_FORMAT # 时间格式
)
# ==================== 控制台处理器配置 ====================
if console_output:
# 控制台输出处理器支持彩色输出和UTF-8编码
import io
# 处理Windows系统的编码问题
if hasattr(sys.stdout, 'buffer'):
# 在Windows上强制使用UTF-8编码避免中文乱码
# errors='replace'确保即使有编码问题也不会崩溃
console_stream = io.TextIOWrapper(
sys.stdout.buffer,
encoding='utf-8',
errors='replace'
)
else:
# Unix/Linux系统通常默认支持UTF-8
console_stream = sys.stdout
# 创建控制台处理器
console_handler = logging.StreamHandler(console_stream)
console_handler.setLevel(log_level)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# ==================== 文件处理器配置 ====================
if file_output:
# 文件输出处理器,支持自动轮转
file_handler = logging.handlers.RotatingFileHandler(
filename=cls._log_file_path, # 日志文件路径
maxBytes=cls.MAX_LOG_SIZE, # 单文件最大大小
backupCount=cls.BACKUP_COUNT, # 备份文件数量
encoding='utf-8' # 文件编码
)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# ==================== 初始化完成标记 ====================
# 标记为已初始化,防止重复配置
cls._initialized = True
# ==================== 记录初始化信息 ====================
# 获取当前模块的日志器并记录初始化信息
logger = logging.getLogger(__name__)
logger.info("=" * 80)
logger.info(f"日志系统初始化完成 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info(f"日志级别: {logging.getLevelName(log_level)}")
logger.info(f"日志文件: {cls._log_file_path}")
logger.info(f"控制台输出: {console_output}")
logger.info(f"文件输出: {file_output}")
logger.info(f"文件轮转: 最大{cls.MAX_LOG_SIZE // (1024*1024)}MB, 保留{cls.BACKUP_COUNT}个备份")
logger.info("=" * 80)
return cls._log_file_path
@classmethod
def _get_project_root(cls) -> Path:
"""
获取项目根目录
这个方法通过向上遍历目录树来查找项目根目录。
它会寻找常见的项目标识文件来确定根目录位置。
查找策略:
1. 从当前文件所在目录开始向上查找
2. 寻找项目标识文件pyproject.toml, setup.py, main.py
3. 找到任一标识文件的目录即为项目根目录
4. 如果都找不到,使用当前文件的上级目录作为备选
返回:
Path: 项目根目录的路径对象
注意事项:
- 这个方法假设项目结构相对标准
- 在特殊的部署环境中可能需要调整
- 备选方案确保总是返回有效路径
"""
# 从当前文件向上查找项目根目录
current_path = Path(__file__).parent
# 向上遍历目录树
while current_path.parent != current_path: # 避免到达文件系统根目录
# 检查常见的项目标识文件
if (current_path / "pyproject.toml").exists() or \
(current_path / "setup.py").exists() or \
(current_path / "main.py").exists():
return current_path
current_path = current_path.parent
# 备选方案:如果找不到标识文件,使用预设的相对路径
# 这个路径基于当前的项目结构utils -> src -> 项目根
return Path(__file__).parent.parent.parent
@classmethod
def get_logger(cls, name: str) -> logging.Logger:
"""
获取配置好的日志器
这是获取日志器的标准方法,确保返回的日志器使用统一的配置。
如果日志系统尚未初始化,会自动进行初始化。
参数:
name: 日志器名称,通常使用模块的 __name__ 变量
返回:
logging.Logger: 配置好的日志器实例
使用示例:
logger = LoggerConfig.get_logger(__name__)
logger.info("这是一条信息日志")
特性:
- 自动初始化首次调用时自动配置日志系统MCP模式下禁用控制台输出
- 层次化命名支持Python日志器的层次化命名
- 统一配置:所有日志器使用相同的格式和输出配置
"""
# 检查是否已初始化,未初始化则使用默认配置初始化
# 重要在MCP模式下禁用控制台输出避免干扰stdio通信
if not cls._initialized:
cls.setup_logging(console_output=False, file_output=True)
# 返回指定名称的日志器
return logging.getLogger(name)
# ==================== 日志工具方法 ====================
@classmethod
def log_function_entry(cls, logger: logging.Logger, func_name: str, **kwargs):
"""
记录函数入口日志
用于调试和性能分析,记录函数被调用时的参数信息。
通常在DEBUG级别输出不会影响生产环境的性能。
参数:
logger: 日志器实例
func_name: 函数名称
**kwargs: 函数参数(键值对形式)
使用示例:
LoggerConfig.log_function_entry(logger, "process_data", user_id=123, action="login")
"""
args_str = ", ".join([f"{k}={v}" for k, v in kwargs.items()])
logger.debug(f"进入函数 {func_name}({args_str})")
@classmethod
def log_function_exit(cls, logger: logging.Logger, func_name: str, result=None):
"""
记录函数出口日志
与log_function_entry配对使用记录函数执行完成和返回值。
有助于跟踪函数执行流程和调试返回值问题。
参数:
logger: 日志器实例
func_name: 函数名称
result: 函数返回值(可选)
使用示例:
LoggerConfig.log_function_exit(logger, "process_data", result={"status": "success"})
"""
if result is not None:
logger.debug(f"退出函数 {func_name},返回值: {result}")
else:
logger.debug(f"退出函数 {func_name}")
@classmethod
def log_api_request(cls, logger: logging.Logger, method: str, url: str, **kwargs):
"""
记录API请求日志
标准化API请求的日志记录包含HTTP方法、URL和请求参数。
有助于API调用的监控和调试。
参数:
logger: 日志器实例
method: HTTP方法GET, POST, PUT, DELETE等
url: 请求URL
**kwargs: 请求参数(可选)
使用示例:
LoggerConfig.log_api_request(logger, "POST", "https://api.example.com/users",
headers={"Authorization": "Bearer xxx"})
"""
logger.info(f"API请求 - {method} {url}")
if kwargs:
logger.debug(f"请求参数: {kwargs}")
@classmethod
def log_api_response(cls, logger: logging.Logger, status_code: int, response_time: float = None):
"""
记录API响应日志
记录API响应的状态码和响应时间用于性能监控和问题诊断。
参数:
logger: 日志器实例
status_code: HTTP状态码
response_time: 响应时间(秒,可选)
使用示例:
LoggerConfig.log_api_response(logger, 200, 0.156)
"""
if response_time:
logger.info(f"API响应 - 状态码: {status_code}, 响应时间: {response_time:.3f}s")
else:
logger.info(f"API响应 - 状态码: {status_code}")
@classmethod
def log_error_with_context(cls, logger: logging.Logger, error: Exception, context: str = ""):
"""
记录带上下文的错误日志
提供丰富的错误信息记录,包含异常类型、错误消息、上下文信息和详细堆栈。
这是错误处理的标准方法。
参数:
logger: 日志器实例
error: 异常对象
context: 错误发生的上下文描述(可选)
使用示例:
try:
risky_operation()
except Exception as e:
LoggerConfig.log_error_with_context(logger, e, "处理用户请求时")
"""
if context:
logger.error(f"错误发生在 {context}: {type(error).__name__}: {str(error)}")
else:
logger.error(f"错误: {type(error).__name__}: {str(error)}")
# 记录详细的异常堆栈信息仅在DEBUG级别显示
logger.debug("错误详情:", exc_info=True)
# ==================== 便捷函数 ====================
def get_logger(name: str) -> logging.Logger:
"""
获取日志器的便捷函数
这是LoggerConfig.get_logger的简化版本提供更简洁的调用方式。
推荐在模块级别使用这个函数获取日志器。
参数:
name: 日志器名称,通常使用 __name__
返回:
logging.Logger: 配置好的日志器实例
使用示例:
logger = get_logger(__name__)
"""
return LoggerConfig.get_logger(name)
def setup_logging(**kwargs) -> str:
"""
设置日志的便捷函数
这是LoggerConfig.setup_logging的简化版本支持所有相同的参数。
参数:
**kwargs: 传递给LoggerConfig.setup_logging的所有参数
返回:
str: 日志文件路径
使用示例:
log_file = setup_logging(log_level=logging.DEBUG, console_output=False)
"""
return LoggerConfig.setup_logging(**kwargs)
# ==================== 装饰器 ====================
def log_function_calls(logger: Optional[logging.Logger] = None):
"""
函数调用日志装饰器
这个装饰器自动记录函数的调用和返回,包括参数和返回值。
主要用于调试和性能分析在生产环境中通常设置为DEBUG级别。
特性:
- 自动记录函数入口和出口
- 记录函数参数kwargs
- 记录返回值
- 自动处理异常并记录错误上下文
- 支持自定义日志器或自动获取
参数:
logger: 可选的日志器实例None时自动获取函数所在模块的日志器
返回:
装饰器函数
使用示例:
@log_function_calls()
def process_user_data(user_id, action="login"):
# 函数实现
return {"status": "success"}
# 或者指定日志器
@log_function_calls(logger=my_logger)
def another_function():
pass
注意事项:
- 会记录所有kwargs参数注意不要记录敏感信息
- 返回值也会被记录,大对象可能影响性能
- 异常会被重新抛出,不会被吞掉
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal logger
# 如果没有提供日志器,自动获取函数所在模块的日志器
if logger is None:
logger = get_logger(func.__module__)
func_name = func.__name__
# 记录函数入口只记录kwargs避免记录过多信息
LoggerConfig.log_function_entry(logger, func_name, **kwargs)
try:
# 执行原函数
result = func(*args, **kwargs)
# 记录函数出口和返回值
LoggerConfig.log_function_exit(logger, func_name, result)
return result
except Exception as e:
# 记录异常信息并重新抛出
LoggerConfig.log_error_with_context(logger, e, f"函数 {func_name}")
raise
return wrapper
return decorator
# ==================== 测试代码 ====================
if __name__ == "__main__":
"""
日志配置测试代码
这个测试代码演示了日志系统的基本功能,包括:
1. 日志系统初始化
2. 不同级别的日志输出
3. 日志文件路径获取
4. 装饰器功能测试
运行方式:
python -m src.utils.logger_config
"""
# 初始化日志系统DEBUG级别同时输出到控制台和文件
log_file = setup_logging(log_level=logging.DEBUG)
test_logger = get_logger(__name__)
test_logger.info("开始测试日志配置...")
# 测试不同级别的日志输出
test_logger.debug("这是一个调试消息 - 用于开发调试")
test_logger.info("这是一个信息消息 - 记录重要信息")
test_logger.warning("这是一个警告消息 - 提醒注意事项")
test_logger.error("这是一个错误消息 - 记录错误情况")
# 测试工具方法
LoggerConfig.log_api_request(test_logger, "GET", "https://api.example.com/test")
LoggerConfig.log_api_response(test_logger, 200, 0.123)
# 测试装饰器
@log_function_calls()
def test_function(param1, param2="default"):
"""测试函数"""
return {"result": "success", "param1": param1}
# 调用测试函数
result = test_function("test_value", param2="custom")
# 输出日志文件位置
test_logger.info(f"日志文件位置: {log_file}")
test_logger.info("日志配置测试完成!")

View File

@@ -0,0 +1,153 @@
import os
import sys
from typing import Dict, Any, Optional
# 导入翻译函数
from .translator import translate
class TranslationService:
"""翻译服务类,用于处理各种翻译需求"""
@staticmethod
def create_prompt(
content: str,
target_lang: str,
use_case: str,
style: str,
prompt_type: str = "general",
keep_terms_desc: str = "核心术语",
) -> str:
"""
创建翻译提示
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景
style: 翻译风格
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
keep_terms_desc: 保留术语的描述
Returns:
str: 格式化的翻译提示
"""
if prompt_type == "tool_name":
keep_terms_desc = "核心术语(如 小写,词语需要用下划线连接)"
elif prompt_type == "tool_description":
keep_terms_desc = "核心术语(这是一段话)"
return f"""
角色:专业本地化翻译专家
任务:将以下内容翻译为{target_lang}(目标用途:{use_case}
要求:
1. 仅返回译文,不含解释或原文;
2. 保留{keep_terms_desc}
3. 符合{style}风格;
4. 特殊符号保持原样。
示例输出格式:
Translated Text
待翻译内容:
{content}
"""
@staticmethod
def translate_text(
content: str,
target_lang: str,
use_case: str = "",
style: str = "正式且符合技术品牌调性",
prompt_type: str = "general",
) -> Dict[str, Any]:
"""
翻译文本
Args:
content: 待翻译内容
target_lang: 目标语言
use_case: 使用场景,默认为空
style: 翻译风格,默认为"正式且符合技术品牌调性"
prompt_type: 提示类型,可选值: "general", "tool_name", "tool_description"
Returns:
Dict: 包含翻译结果的字典
"""
prompt = TranslationService.create_prompt(
content=content,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type=prompt_type,
)
try:
result = translate(prompt, target_lang)
return result
except Exception as e:
print(f"翻译出错: {str(e)}")
return {"translated_text": "", "error": str(e)}
@staticmethod
def translate_tool_name(
name: str,
target_lang: str = "英语",
use_case: str = "工具名称",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具名称的便捷方法
Returns:
str: 翻译后的工具名称
"""
result = TranslationService.translate_text(
content=name,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_name",
)
return result.get("translated_text", "")
@staticmethod
def translate_tool_description(
description: str,
target_lang: str = "英语",
use_case: str = "工具描述",
style: str = "正式且符合技术品牌调性,大模型能理解",
) -> str:
"""
翻译工具描述的便捷方法
Returns:
str: 翻译后的工具描述
"""
result = TranslationService.translate_text(
content=description,
target_lang=target_lang,
use_case=use_case,
style=style,
prompt_type="tool_description",
)
return result.get("translated_text", "")
def translation_example():
"""翻译功能使用示例"""
# 示例1: 翻译工具名称
tool_name = "万川AI新媒体平台【测试环境】"
translated_name = TranslationService.translate_tool_name(tool_name)
print(f"工具名称翻译: {translated_name}")
# 示例2: 翻译工具描述
description = "21日辛柏青发布讣告宣布妻子朱媛媛抗癌五年后离世。此前在一次路演现场当观众问及朱媛媛时辛柏青2秒停顿藏着"
translated_desc = TranslationService.translate_tool_description(description)
print(f"工具描述翻译: {translated_desc}")
if __name__ == "__main__":
translation_example()

View File

@@ -0,0 +1,64 @@
import os
import requests
import json
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# ========== 模型相关 ==========
# 从.env文件获取模型API配置
BASE_URL = os.getenv("BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
API_KEY = os.getenv("OPENAI_API_KEY", "sk-c5a912a6bc8e4c9cbdbdf68232352a03")
TEMPERATURE = float(os.getenv("MODEL_TEMPERATURE", "0.7"))
def translate(content, target_language):
"""
翻译文本内容到目标语言
:param content: 要翻译的内容
:param target_language: 目标语言,如'en'(英语), 'zh'(中文), 'ja'(日语), 'fr'(法语)等
:return: 翻译后的内容,如果翻译失败则返回原文和错误信息
"""
if not content or not target_language:
return {"error": "内容或目标语言不能为空", "translated_text": content}
# 确保API密钥已设置
if not API_KEY:
return {"error": "API密钥未设置请检查.env文件", "translated_text": content}
try:
# 构建API请求头
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
# 构建翻译提示
prompt = f"请将以下内容翻译成{target_language},只返回翻译结果,不要包含任何解释或原文:\n\n{content}"
# 构建API请求体
data = {
"model": "qwen-max", # 使用通义千问模型,可以根据实际需要更改
"messages": [{"role": "user", "content": prompt}],
"temperature": TEMPERATURE,
}
# 发送API请求
response = requests.post(
f"{BASE_URL}/chat/completions", headers=headers, json=data
)
# 解析响应
if response.status_code == 200:
result = response.json()
translated_text = result["choices"][0]["message"]["content"].strip()
return {"translated_text": translated_text}
else:
error_message = (
f"翻译失败,状态码: {response.status_code}, 响应: {response.text}"
)
return {"error": error_message, "translated_text": content}
except Exception as e:
return {"error": f"翻译过程中发生错误: {str(e)}", "translated_text": content}

View File

@@ -0,0 +1,461 @@
"""
Dify文件上传工具 - 优化版
主要功能:
- 从URL下载文件并自动上传到Dify API
- 支持常见文件类型JPG、PNG、GIF、PDF、DOCX、TXT等
- 自动处理文件大小检查、MIME类型识别、临时文件清理
使用方法:
from upload_file import upload_file_from_url
result = upload_file_from_url(
file_url="http://example.com/image.jpg",
base_url="http://192.168.2.236:3001/v1",
api_key="app-QdfDKqHAI3dlB6tvnibuh6rv"
)
file_id = result['id'] # 获取上传后的文件ID
"""
import os
import tempfile
import requests
import logging
from urllib.parse import urlparse, unquote
from typing import Optional
# 获取模块级别的logger避免影响全局日志配置
logger = logging.getLogger(__name__)
# 常用MIME类型映射
MIME_TYPE_MAP = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
def download_file_from_url(
url: str,
download_dir: Optional[str] = None,
filename: Optional[str] = None,
timeout: int = 30,
max_retries: int = 3
) -> str:
"""
从URL下载文件到本地并返回文件路径
Args:
url: 文件的URL地址
download_dir: 下载目录如果为None则使用系统临时目录
filename: 指定文件名如果为None则从URL中提取
timeout: 请求超时时间(秒)
max_retries: 最大重试次数
Returns:
str: 下载后的本地文件路径
Raises:
Exception: 下载失败时抛出异常
"""
# 设置下载目录
if download_dir is None:
download_dir = tempfile.gettempdir()
# 确保下载目录存在
os.makedirs(download_dir, exist_ok=True)
# 提取文件名
if filename is None:
filename = _extract_filename_from_url(url)
# 构建完整的文件路径
file_path = os.path.join(download_dir, filename)
# 下载文件(带重试机制)
for attempt in range(max_retries):
try:
logger.info(f"正在下载文件: {url} (尝试 {attempt + 1}/{max_retries})")
# 发送GET请求下载文件
response = requests.get(url, timeout=timeout, stream=True)
response.raise_for_status()
# 写入文件
with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# 验证文件是否下载成功
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
logger.info(f"文件下载成功: {file_path} (大小: {os.path.getsize(file_path)} 字节)")
return file_path
else:
raise Exception("下载的文件为空或不存在")
except requests.exceptions.Timeout:
error_msg = f"请求超时 (超过 {timeout} 秒)"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
except requests.exceptions.RequestException as e:
error_msg = f"请求异常: {str(e)}"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
except Exception as e:
error_msg = f"下载过程中发生错误: {str(e)}"
logger.warning(f"{error_msg}, 尝试 {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise Exception(f"下载失败:{error_msg}")
raise Exception("下载失败:已达到最大重试次数")
def _extract_filename_from_url(url: str) -> str:
"""
从URL中提取文件名
Args:
url: 文件URL
Returns:
str: 提取的文件名
"""
try:
# 解析URL
parsed_url = urlparse(url)
path = unquote(parsed_url.path)
# 从路径中提取文件名
filename = os.path.basename(path)
# 如果没有找到文件名或文件名为空,使用默认名称
if not filename or filename == '/':
filename = "downloaded_file"
# 移除查询参数(如果文件名中包含)
if '?' in filename:
filename = filename.split('?')[0]
return filename
except Exception as e:
logger.warning(f"无法从URL提取文件名: {str(e)}, 使用默认文件名")
return "downloaded_file"
def upload_file_to_dify(
file_path: str,
base_url: str,
api_key: str,
user: str = "default_user",
verify_ssl: bool = False
) -> dict:
"""
上传文件到Dify API
Args:
file_path: 本地文件路径
base_url: Dify API基础URL (例如: http://192.168.2.236:3001/v1)
api_key: API密钥
user: 用户标识
verify_ssl: 是否验证SSL证书
Returns:
dict: 上传响应结果
Raises:
Exception: 上传失败时抛出异常
"""
# 检查文件是否存在
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
# 检查文件大小
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024)
logger.info(f"准备上传文件: {file_path} (大小: {file_size_mb:.2f} MB)")
# 检查文件大小是否超过限制
if file_size_mb > 10:
logger.warning(f"文件大小 {file_size_mb:.2f} MB 可能超过服务器限制 (10 MB)")
upload_url = f"{base_url}/files/upload"
headers = {"Authorization": f"Bearer {api_key}"}
try:
with open(file_path, "rb") as f:
# 获取文件扩展名和MIME类型
file_ext = os.path.splitext(file_path)[1].lower()
mime_type = MIME_TYPE_MAP.get(file_ext, "application/octet-stream")
# 构建文件上传数据
files = {"file": (os.path.basename(file_path), f, mime_type)}
data = {"user": user}
# 发送上传请求
response = requests.post(
upload_url,
headers=headers,
files=files,
data=data,
verify=verify_ssl
)
# 检查响应
if response.status_code == 201:
result = response.json()
logger.info(f"文件上传成功: {result.get('name', 'unknown')} (ID: {result.get('id', 'unknown')})")
return result
else:
error_msg = f"上传失败 (状态码: {response.status_code}): {response.text}"
logger.error(error_msg)
raise requests.exceptions.HTTPError(error_msg)
except requests.exceptions.RequestException as e:
logger.error(f"上传文件请求失败: {str(e)}")
raise
except Exception as e:
logger.error(f"上传文件失败: {str(e)}")
raise
def check_app_config(base_url: str, api_key: str):
"""
检查Dify应用的文件上传配置
Args:
base_url: Dify API基础URL
api_key: API密钥
"""
try:
# 获取应用参数配置
config_url = f"{base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(config_url, headers=headers, verify=False)
response.raise_for_status()
config = response.json()
logger.info(f"应用配置获取成功")
# 检查文件上传配置
file_upload = config.get("file_upload", {})
if file_upload.get("enabled", False):
logger.info("✓ 文件上传功能已启用")
logger.info(f" - 允许的文件类型: {file_upload.get('allowed_file_types', [])}")
logger.info(f" - 允许的文件扩展名: {file_upload.get('allowed_file_extensions', [])}")
logger.info(f" - 文件大小限制: {file_upload.get('fileUploadConfig', {}).get('image_file_size_limit', 'N/A')} MB")
else:
logger.warning("✗ 文件上传功能未启用")
return config
except Exception as e:
logger.error(f"检查应用配置失败: {str(e)}")
return None
def test_download_and_upload():
"""
测试下载和上传功能的示例
"""
# 测试用的文件URL
file_url = "http://192.168.2.236:9000/lzwcai/upload/2025-07-29/34b28da03f3c43b0921ba1b76857bbc0/34b28da03f3c43b0921ba1b76857bbc0.JPG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20250729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250729T075242Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=80f58d37c3bd52fb2b25efa36b5df0d73c8da7c773e7a7066dd6563710c619d6"
# 上传API配置
base_url = "http://192.168.2.236:3001/v1"
upload_url = f"{base_url}/files/upload"
api_key = "app-QdfDKqHAI3dlB6tvnibuh6rv"
headers = {"Authorization": f"Bearer {api_key}"}
user = "abc-123"
try:
logger.info("开始测试下载和上传流程...")
# 0. 检查应用配置
logger.info("检查Dify应用配置...")
config = check_app_config(base_url, api_key)
if not config:
logger.error("无法获取应用配置,终止测试")
return
# 1. 下载文件
logger.info(f"正在下载文件: {file_url}")
file_path = download_file_from_url(file_url)
logger.info(f"文件下载成功,保存路径: {file_path}")
# 2. 上传文件
logger.info(f"正在上传文件到: {upload_url}")
# 检查文件大小
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024)
logger.info(f"文件大小: {file_size_mb:.2f} MB")
# 检查文件大小是否超过限制通常为10MB对于图片
if file_size_mb > 10:
logger.warning(f"文件大小 {file_size_mb:.2f} MB 可能超过服务器限制")
with open(file_path, "rb") as f:
# 获取文件扩展名来确定MIME类型
file_ext = os.path.splitext(file_path)[1].lower()
mime_type = "image/jpeg" if file_ext in ['.jpg', '.jpeg'] else "application/octet-stream"
# 构建文件上传数据指定正确的MIME类型
files = {"file": (os.path.basename(file_path), f, mime_type)}
data = {"user": user}
# 不要在headers中设置Content-Type让requests自动处理multipart/form-data
upload_headers = headers.copy()
if "Content-Type" in upload_headers:
del upload_headers["Content-Type"]
# 添加SSL验证跳过选项用于自签名证书
response = requests.post(upload_url, headers=upload_headers, files=files, data=data, verify=False)
# 打印详细的响应信息用于调试
logger.info(f"响应状态码: {response.status_code}")
logger.info(f"响应头: {response.headers}")
logger.info(f"响应内容: {response.text}")
response.raise_for_status()
result = response.json()
logger.info(f"文件上传成功,响应: {result}")
# 3. 清理临时文件
try:
os.remove(file_path)
logger.info(f"已清理临时文件: {file_path}")
except Exception as e:
logger.warning(f"清理临时文件失败: {str(e)}")
return result
except Exception as e:
logger.error(f"测试失败: {str(e)}")
raise
def upload_file_from_url(
file_url: str,
base_url: str,
api_key: str,
user: str = "default_user",
verify_ssl: bool = False
) -> dict:
"""
从URL下载文件并上传到Dify API - 一站式解决方案
这是一个优化后的方法只需要提供URL地址、base_url和api_key就能自动完成下载和上传。
支持常见的文件类型JPG、PNG、GIF、PDF、DOCX、TXT等。
自动处理文件大小检查、MIME类型识别、临时文件清理等。
Args:
file_url: 要下载的文件URL
base_url: Dify API基础URL (例如: http://192.168.2.236:3001/v1)
api_key: API密钥 (例如: app-QdfDKqHAI3dlB6tvnibuh6rv)
user: 用户标识 (可选,默认为 default_user)
verify_ssl: 是否验证SSL证书 (可选,默认为 False)
Returns:
dict: 上传成功后的结果包含文件ID、名称、大小等信息
示例: {
'id': 'a239b623-40a8-482c-859f-bb8368d5b1fe',
'name': 'example.jpg',
'size': 495240,
'extension': 'jpg',
'mime_type': 'image/jpeg',
'created_by': '92c4b250-e0e7-4123-900d-f5c2187679a2',
'created_at': 1753777420
}
使用示例:
result = upload_file_from_url(
file_url="http://example.com/image.jpg",
base_url="http://192.168.2.236:3001/v1",
api_key="app-QdfDKqHAI3dlB6tvnibuh6rv"
)
file_id = result['id'] # 获取上传后的文件ID
Raises:
Exception: 下载或上传失败时抛出异常
"""
temp_file_path = None
try:
logger.info(f"开始处理文件: {file_url}")
# 1. 下载文件到临时目录
temp_file_path = download_file_from_url(file_url)
# 2. 上传文件到Dify (复用已有的上传函数)
result = upload_file_to_dify(temp_file_path, base_url, api_key, user, verify_ssl)
return result
except Exception as e:
logger.error(f"处理文件失败: {str(e)}")
raise
finally:
# 清理临时文件
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
logger.info(f"已清理临时文件: {temp_file_path}")
except Exception as e:
logger.warning(f"清理临时文件失败: {str(e)}")
def test_upload_functionality():
"""
测试文件上传功能的示例
注意:这个函数包含示例配置,实际使用时请替换为真实的配置
"""
# 示例配置 - 实际使用时请替换为真实的配置
file_url = "https://example.com/test-image.jpg" # 替换为实际的文件URL
base_url = "http://localhost:3001/v1" # 替换为实际的Dify API地址
api_key = "your-api-key-here" # 替换为实际的API密钥
try:
logger.info("=== 开始测试文件上传功能 ===")
# 检查应用配置
logger.info("检查Dify应用配置...")
config = check_app_config(base_url, api_key)
if not config:
logger.warning("无法获取应用配置,但继续测试上传功能")
# 调用上传方法
result = upload_file_from_url(file_url, base_url, api_key)
logger.info("=== 上传成功!结果如下 ===")
logger.info(f"文件ID: {result.get('id')}")
logger.info(f"文件名: {result.get('name')}")
logger.info(f"文件大小: {result.get('size')} 字节")
logger.info(f"文件类型: {result.get('mime_type')}")
logger.info(f"扩展名: {result.get('extension')}")
return result
except Exception as e:
logger.error(f"测试失败: {str(e)}")
raise
if __name__ == "__main__":
# 运行测试 - 注意需要先配置正确的URL和API密钥
print("警告:测试函数包含示例配置,请先修改为实际配置后再运行")
# test_upload_functionality() # 取消注释并配置后运行

View File

@@ -0,0 +1,315 @@
import requests
from abc import ABC
import logging
import json
from src.utils.logger_config import get_logger
# 导入 pypinyin 用于中文转拼音
try:
import pypinyin
except ImportError:
pypinyin = None
logging.warning("pypinyin 模块未安装,将使用简化的命名方式")
logger = get_logger(__name__)
def pinyin_to_camel(pinyin):
"""
将中文名称转换为工具名称
处理逻辑:
1. 如果安装了 pypinyin将中文转换为拼音然后转为驼峰命名
2. 如果未安装 pypinyin将所有非字母数字字符替换为下划线
3. 所有符号都会被替换成下划线
示例:
"你好啊" -> "tool_NiHaoA" (有pypinyin)
"测试-工具" -> "tool_测试_工具" (无pypinyin)
"Hello World!" -> "tool_Hello_World_" (无pypinyin)
Args:
pinyin: 输入的字符串(可能包含中文、英文、符号等)
Returns:
str: 格式化后的工具名称,以 "tool_" 开头
"""
import re
if pypinyin is None:
# 如果 pypinyin 未安装,使用简化的命名方式
# 将所有非字母数字字符(包括空格、符号等)替换为下划线
cleaned = re.sub(r'[^\w]', '_', str(pinyin))
# 移除连续的下划线
cleaned = re.sub(r'_+', '_', cleaned)
# 移除首尾的下划线
cleaned = cleaned.strip('_')
return "tool_" + cleaned if cleaned else "tool_unnamed"
# 使用 pypinyin 转换中文为拼音
pinyin_list = pypinyin.lazy_pinyin(pinyin)
# 处理每个拼音单词
processed_words = []
for word in pinyin_list:
# 将所有非字母数字字符替换为下划线
cleaned_word = re.sub(r'[^\w]', '_', word)
# 移除连续的下划线
cleaned_word = re.sub(r'_+', '_', cleaned_word)
# 移除首尾的下划线
cleaned_word = cleaned_word.strip('_')
if cleaned_word:
# 首字母大写(驼峰命名)
processed_words.append(cleaned_word.capitalize())
# 拼接所有单词
result = "".join(processed_words) if processed_words else "Unnamed"
return "tool_" + result
class WorkflowDifyAPI(ABC):
def __init__(self, base_url: str, dify_app_sks: list, user="pp666"):
# dify configs
self.dify_base_url = base_url
self.dify_app_sks = dify_app_sks
self.user = user
# dify app infos
dify_app_infos = []
dify_app_params = []
dify_app_metas = []
for key in self.dify_app_sks:
dify_app_infos.append(self.get_app_info(key))
dify_app_params.append(self.get_app_parameters(key))
dify_app_metas.append(self.get_app_meta(key))
self.dify_app_infos = dify_app_infos
self.dify_app_params = dify_app_params
self.dify_app_metas = dify_app_metas
self.dify_app_names = [x["name"] for x in dify_app_infos]
def chat_message(
self,
api_key,
inputs={},
response_mode="streaming",
conversation_id=None,
userId="pp666",
files=None,
):
url = f"{self.dify_base_url}/workflows/run"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"inputs": inputs,
"response_mode": response_mode,
"user": userId,
}
logger.info("Sending data to Dify API: %s", data)
logger.info("Sending headers to Dify API: %s", headers)
logger.info("Sending url to Dify API: %s", url)
if conversation_id:
data["conversation_id"] = conversation_id
if files:
files_data = self.file_parameter_pretreatment(files)
if files_data and len(files_data) > 0:
data["inputs"]["files"] = files_data[0]
# For workflow API, we send files data in the JSON payload, not as multipart files
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
else:
response = requests.post(
url, headers=headers, json=data, stream=response_mode == "streaming"
)
logger.info(f"Response1:{data} {response.status_code} {response.reason}")
# Add debugging for error responses
if response.status_code != 200:
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()
if response_mode == "streaming":
def stream_generator():
for line in response.iter_lines():
if line:
if line.startswith(b"data:"):
try:
json_data = json.loads(line[5:].decode("utf-8"))
yield json_data
except json.JSONDecodeError:
logger.error(f"Error decoding JSON: {line}")
return stream_generator()
else:
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)
response.raise_for_status()
return response.json()
def upload_file_remote_url(self, file_url):
from src.utils.upload_file import upload_file_from_url
base_url = self.dify_base_url
api_key = self.dify_app_sks[0]
return upload_file_from_url(file_url, base_url, api_key)
def file_parameter_pretreatment(self, files):
"""
文件参数预处理方法
传入的"files"数据结构是这样的: [
{
"type": "image",
"transfer_method": "remote_url",
"url": "http://example.com/image.jpg"
}
]
处理逻辑:
1. 遍历files列表中的每个文件对象
2. 对于transfer_method为"remote_url"的文件调用upload_file_remote_url方法
3. 将返回的对象的id字段存入原对象的upload_file_id字段
4. 设置transfer_method为"local_file"因为已经上传到Dify服务器
5. 返回处理好的files列表
Args:
files (list): 文件列表每个元素包含type、transfer_method、url等字段
Returns:
list: 处理后的文件列表每个文件对象包含upload_file_id和transfer_method字段
"""
if not files or not isinstance(files, list):
logger.warning("文件参数为空或格式不正确")
return files
processed_files = []
for file_obj in files:
# 创建文件对象的副本,避免修改原始数据
processed_file = file_obj.copy()
# 检查是否需要处理远程URL文件
if (processed_file.get("transfer_method") == "remote_url" and
processed_file.get("url")):
try:
logger.info(f"开始上传远程文件: {processed_file['url']}")
# 调用upload_file_remote_url方法下载文件并上传到Dify
upload_result = self.upload_file_remote_url(processed_file["url"])
# 将返回的对象的id存入upload_file_id字段
if upload_result and "id" in upload_result:
processed_file["upload_file_id"] = upload_result["id"]
# 修改transfer_method为local_file因为文件已经上传到Dify服务器
processed_file["transfer_method"] = "local_file"
# 移除url字段因为已经不需要了
processed_file.pop("url", None)
logger.info(f"文件上传成功 - ID: {upload_result['id']}, "
f"名称: {upload_result.get('name', 'N/A')}, "
f"大小: {upload_result.get('size', 'N/A')} bytes")
else:
logger.error(f"文件上传失败未获取到有效的文件ID响应: {upload_result}")
processed_file["upload_error"] = "未获取到有效的文件ID"
except Exception as e:
logger.error(f"文件上传过程中发生错误: {str(e)}", exc_info=True)
# 记录错误信息,但继续处理其他文件
processed_file["upload_error"] = str(e)
elif processed_file.get("transfer_method") == "local_file":
# 如果已经是local_file确保有upload_file_id
if not processed_file.get("upload_file_id"):
logger.warning("local_file类型的文件缺少upload_file_id字段")
processed_files.append(processed_file)
logger.info(f"文件预处理完成,共处理 {len(processed_files)} 个文件")
return processed_files
def stop_response(self, api_key, task_id, user="pp666"):
url = f"{self.dify_base_url}/chat-messages/{task_id}/stop"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {"user": user}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()
def get_app_info(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/info"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
response_map = response.json()
# 翻译工具名称
tool_name = response_map.get("name")
if tool_name:
# translated_name = TranslationService.translate_tool_name(tool_name)
translated_name = pinyin_to_camel(tool_name)
response_map["name"] = translated_name
# 翻译工具描述
# tool_description = response_map.get("description")
# if tool_description:
# translated_description = TranslationService.translate_tool_description(
# tool_description
# )
# response_map["description"] = (
# f"{tool_description} ({translated_description})"
# )
return response_map
def get_app_parameters(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/parameters"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
logger.info(f"调用 /parameters API: {url}")
logger.info(f"请求头: {headers}")
logger.info(f"请求参数: {params}")
response = requests.get(url, headers=headers, params=params)
logger.info(f"/parameters API 响应状态码: {response.status_code}")
response.raise_for_status()
response_data = response.json()
logger.info(f"/parameters API 响应数据: {response_data}")
return response_data
def get_app_meta(self, api_key, user="pp666"):
url = f"{self.dify_base_url}/meta"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"user": user}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
dify_api = WorkflowDifyAPI(
"https://ops.lzwcai.com/v1",
["app-ZmLuBlRmViseUdOonqLyNSku", "app-AHjfp8k4nawQSJi0us8x3J5Q"],
)
dify_api.upload_file_remote_url("http://192.168.2.236:9000/lzwcai/upload/2025-07-29/34b28da03f3c43b0921ba1b76857bbc0/34b28da03f3c43b0921ba1b76857bbc0.JPG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20250729%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250729T075242Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=80f58d37c3bd52fb2b25efa36b5df0d73c8da7c773e7a7066dd6563710c619d6")

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-mcp-dyntoolapi
Version: 0.1.27
Summary: 基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: dynaconf>=3.2.11
Requires-Dist: httpx>=0.28.1
Requires-Dist: jinja2==3.1.6
Requires-Dist: mcp[cli]>=1.8.0
Requires-Dist: requests>=2.31.0
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: lzwcai-mcp-api-converter
Version: 0.1.30
Summary: 基于FastMCP框架的动态API工具服务器自动将企业业务API配置转换为MCP协议工具支持多种传输方式、企业认证和参数验证为AI助手提供标准化的业务接口访问能力。
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: dynaconf>=3.2.11
Requires-Dist: httpx>=0.28.1
Requires-Dist: jinja2==3.1.6
Requires-Dist: mcp[cli]>=1.8.0
Requires-Dist: requests>=2.31.0
Requires-Dist: pypinyin>=0.54.0

View File

@@ -0,0 +1,25 @@
pyproject.toml
setup.cfg
lzwcai_mcp_api_converter/__init__.py
lzwcai_mcp_api_converter.egg-info/PKG-INFO
lzwcai_mcp_api_converter.egg-info/SOURCES.txt
lzwcai_mcp_api_converter.egg-info/dependency_links.txt
lzwcai_mcp_api_converter.egg-info/entry_points.txt
lzwcai_mcp_api_converter.egg-info/requires.txt
lzwcai_mcp_api_converter.egg-info/top_level.txt
lzwcai_mcp_api_converter/src/__init__.py
lzwcai_mcp_api_converter/src/api_config.json
lzwcai_mcp_api_converter/src/create_mcp.py
lzwcai_mcp_api_converter/src/business/__init__.py
lzwcai_mcp_api_converter/src/business/business_util.py
lzwcai_mcp_api_converter/src/business/get_business_api.py
lzwcai_mcp_api_converter/src/core/__init__.py
lzwcai_mcp_api_converter/src/core/api_auth_service.py
lzwcai_mcp_api_converter/src/core/api_base.py
lzwcai_mcp_api_converter/src/core/core_server.py
lzwcai_mcp_api_converter/src/core/get_auth.py
lzwcai_mcp_api_converter/src/core/plugin_base.py
lzwcai_mcp_api_converter/src/util/__init__.py
lzwcai_mcp_api_converter/src/util/api_helper.py
lzwcai_mcp_api_converter/src/util/logger_config.py
lzwcai_mcp_api_converter/src/util/nested_value.py

View File

@@ -0,0 +1,2 @@
[console_scripts]
lzwcai-mcp-api-converter = lzwcai_mcp_api_converter.src.create_mcp:run_main

View File

@@ -0,0 +1,6 @@
dynaconf>=3.2.11
httpx>=0.28.1
jinja2==3.1.6
mcp[cli]>=1.8.0
requests>=2.31.0
pypinyin>=0.54.0

View File

@@ -0,0 +1 @@
lzwcai_mcp_api_converter

View File

@@ -0,0 +1,712 @@
"""
业务工具模块 - API参数处理和JSON Schema操作
这个模块提供了API参数到JSON Schema转换、参数验证、API配置操作等核心业务功能。
它是连接业务API配置和MCP工具定义的桥梁。
主要功能:
1. API参数列表转换为JSON Schema
2. 参数验证和默认值处理
3. Schema提示文本生成
4. API配置数组操作
5. 必填参数检查
核心组件:
- 参数类型常量定义
- JSON Schema生成器
- 参数验证器
- 配置操作工具
设计原则:
- 类型安全:严格的类型检查和转换
- 容错性:完善的异常处理机制
- 可扩展性:支持新的参数类型和验证规则
- 标准化符合JSON Schema规范
作者: lzwcai
版本: 1.0.0
"""
import json
import copy
from typing import Dict, List, Any, Optional, Union
from ..util.logger_config import get_logger
# 获取日志器实例
logger = get_logger(__name__)
# ==================== 常量定义 ====================
class ParamType:
"""
API参数类型常量
定义业务平台API参数的所有支持类型。
这些类型会被映射到对应的JSON Schema类型。
"""
STRING = "STRING" # 字符串类型
NUMBER = "NUMBER" # 数字类型(浮点数)
INTEGER = "INTEGER" # 整数类型
BOOLEAN = "BOOLEAN" # 布尔类型
ARRAY = "ARRAY" # 数组类型
OBJECT = "OBJECT" # 对象类型
class JsonSchemaType:
"""
JSON Schema类型常量
定义JSON Schema规范中的标准类型。
用于将业务参数类型转换为标准Schema类型。
"""
STRING = "string" # 字符串
NUMBER = "number" # 数字
BOOLEAN = "boolean" # 布尔值
ARRAY = "array" # 数组
OBJECT = "object" # 对象
class RequestType:
"""
请求类型常量
定义API参数在HTTP请求中的位置类型。
用于将参数正确分组到请求的不同部分。
"""
HEADER = "header" # 请求头参数
QUERY = "query" # 查询参数URL参数
BODY = "body" # 请求体参数
LZWCAI_CONFIG = "lzwcaiConfig" # lzwcaiConfig参数新的用户ID存储位置
# ==================== 类型映射配置 ====================
# 参数类型映射表:业务参数类型 -> JSON Schema类型
PARAM_TYPE_MAPPING = {
ParamType.STRING: JsonSchemaType.STRING, # 字符串 -> string
ParamType.NUMBER: JsonSchemaType.NUMBER, # 数字 -> number
ParamType.INTEGER: JsonSchemaType.NUMBER, # 整数 -> numberJSON Schema中整数也是number
ParamType.BOOLEAN: JsonSchemaType.BOOLEAN, # 布尔 -> boolean
ParamType.ARRAY: JsonSchemaType.ARRAY, # 数组 -> array
ParamType.OBJECT: JsonSchemaType.OBJECT, # 对象 -> object
}
# ==================== 默认参数配置 ====================
# 默认用户ID参数配置
# 这个参数会自动添加到所有API的Schema中用于标识当前用户
DEFAULT_USER_ID_PARAM = {
"paramName": "userId", # 参数名称
"paramType": ParamType.STRING, # 参数类型:字符串
"paramPrompts": "当前与您对话的用户信息的用户ID", # 参数描述
"requestType": RequestType.LZWCAI_CONFIG, # 请求类型lzwcaiConfig类型
"required": 1, # 必填参数
}
# ==================== 核心函数 ====================
def generate_json_schema(api_params: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
API参数列表转JSON Schema格式
这是模块的核心函数负责将业务平台的API参数列表转换为符合JSON Schema规范的对象。
生成的Schema用于MCP工具的参数定义和验证。
转换流程:
1. 验证输入参数类型
2. 过滤掉header类型的参数header参数单独处理
3. 添加默认的userId参数存储在lzwcaiConfig中
4. 按请求类型分组参数query, body, lzwcaiConfig等
5. 为每个分组创建Schema属性
6. 清理空的required列表
Schema结构:
{
"type": "object",
"properties": {
"query": {
"type": "object",
"properties": {...},
"required": [...]
},
"body": {
"type": "object",
"properties": {...},
"required": [...]
},
"lzwcaiConfig": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "当前与您对话的用户信息的用户ID"
}
},
"required": ["userId"]
}
},
"required": ["query", "body", "lzwcaiConfig"] // 只包含有参数的分组
}
参数:
api_params: API参数对象列表每个对象包含
- paramName: 参数名称
- paramType: 参数类型STRING, NUMBER等
- paramPrompts: 参数描述
- requestType: 请求类型header, query, body等
- required: 是否必填1为必填0为可选
- defaultValue: 默认值(可选)
返回:
dict: 符合JSON Schema规范的对象
异常处理:
TypeError: 如果api_params不是列表类型
设计考虑:
- header参数被过滤掉因为它们在HTTP请求中单独处理
- 自动添加userId参数到lzwcaiConfig分组确保所有API都能获取用户信息
- 按请求类型分组,便于后续的参数处理和验证
- 清理空的required列表保持Schema的简洁性
"""
# 参数类型验证
if not isinstance(api_params, list):
raise TypeError("api_params must be a list")
logger.debug(f"生成JSON Schema参数数量: {len(api_params)}")
# 创建基础Schema结构
schema = {
"type": JsonSchemaType.OBJECT, # 根类型为对象
"properties": {}, # 属性定义
"required": [] # 必填字段列表
}
# 过滤参数并添加默认userId参数
# header参数在HTTP请求中单独处理不包含在Schema中
filtered_params = [
param for param in api_params
if param.get("requestType") != RequestType.HEADER
]
# 添加默认的userId参数到lzwcaiConfig分组确保所有API都能获取用户信息
filtered_params.append(DEFAULT_USER_ID_PARAM)
logger.debug(f"过滤后参数数量: {len(filtered_params)}")
# 按请求类型分组参数
param_groups = _group_parameters_by_type(filtered_params)
logger.debug(f"参数分组: {list(param_groups.keys())}")
# 为每个请求类型创建Schema属性
for req_type, params in param_groups.items():
logger.debug(f"处理参数组 {req_type},包含 {len(params)} 个参数")
_add_request_type_to_schema(schema, req_type, params)
# 清理空的required列表保持Schema简洁
_cleanup_empty_required_lists(schema)
logger.debug("JSON Schema生成完成")
return schema
# ==================== 辅助函数 ====================
def _group_parameters_by_type(
params: List[Dict[str, Any]],
) -> Dict[str, List[Dict[str, Any]]]:
"""
按请求类型分组参数
将参数列表按照requestType字段进行分组便于后续按类型处理。
如果参数没有指定requestType默认归类为query类型。
参数:
params: 参数字典列表
返回:
Dict[str, List[Dict[str, Any]]]: 按类型分组的参数字典
格式: {"query": [...], "body": [...], "lzwcaiConfig": [...]}
"""
param_groups = {}
for param in params:
# 获取请求类型默认为query
req_type = param.get("requestType", RequestType.QUERY)
# 记录参数名和类型,便于调试
param_name = param.get("paramName", "未命名参数")
logger.debug(f"参数 '{param_name}' 归类为 {req_type} 类型")
# 初始化分组
if req_type not in param_groups:
param_groups[req_type] = []
logger.debug(f"创建新的参数分组: {req_type}")
# 添加参数到对应分组
param_groups[req_type].append(param)
logger.debug(f"参数分组完成,共 {len(param_groups)} 个分组")
return param_groups
def _add_request_type_to_schema(
schema: Dict[str, Any], req_type: str, params: List[Dict[str, Any]]
) -> None:
"""
向Schema添加请求类型分组
为指定的请求类型创建Schema属性并处理该类型下的所有参数。
每个请求类型都会成为根Schema的一个属性。
参数:
schema: 根Schema对象
req_type: 请求类型(如"query", "body", "lzwcaiConfig"
params: 该类型下的参数列表
"""
# 如果该请求类型还没有在Schema中定义创建它
if req_type not in schema["properties"]:
schema["properties"][req_type] = {
"type": JsonSchemaType.OBJECT, # 每个请求类型都是对象类型
"properties": {}, # 该类型下的参数定义
"required": [], # 该类型下的必填参数
}
# 如果该类型有参数,将其标记为根级别的必填字段
if params:
schema["required"].append(req_type)
# 处理该类型下的每个参数
for param in params:
_add_parameter_to_schema(schema["properties"][req_type], param)
def _add_parameter_to_schema(
type_schema: Dict[str, Any], param: Dict[str, Any]
) -> None:
"""
向类型Schema添加单个参数
将单个参数的定义添加到指定类型的Schema中包括参数类型、描述、
默认值、示例等信息。
参数处理:
1. 提取参数基本信息(名称、类型、描述)
2. 处理默认值添加到描述中并设置default字段
3. 映射参数类型到JSON Schema类型
4. 创建参数定义对象
5. 添加到Schema的properties中
6. 处理必填参数标记
参数:
type_schema: 类型Schema对象如query、body的Schema
param: 参数定义字典
"""
# 获取参数名称,如果没有名称则跳过
param_name = param.get("paramName")
if not param_name:
logger.warning(f"参数缺少名称,跳过: {param}")
return
# 获取参数类型和描述
param_type = param.get("paramType", ParamType.STRING)
param_desc = param.get("paramPrompts", "")
is_required = param.get("required") == 1
logger.debug(f"添加参数: {param_name}, 类型: {param_type}, 必填: {is_required}")
# 如果有默认值,添加到描述中
if param.get("defaultValue") is not None:
param_desc += f"(默认值为{param['defaultValue']}"
logger.debug(f"参数 {param_name} 有默认值: {param['defaultValue']}")
# 将业务参数类型映射到JSON Schema类型
json_type = PARAM_TYPE_MAPPING.get(param_type, JsonSchemaType.STRING)
if param_type not in PARAM_TYPE_MAPPING:
logger.warning(f"未知的参数类型 {param_type},使用默认类型 string")
# 创建参数定义对象
param_def = {
"type": json_type, # JSON Schema类型
"description": param_desc, # 参数描述
}
# 添加可选字段
if param.get("defaultValue") is not None:
param_def["default"] = param["defaultValue"] # 默认值
if param.get("example") is not None:
param_def["example"] = param["example"] # 示例值
# 添加到类型Schema的properties中
type_schema["properties"][param_name] = param_def
# 如果是必填参数添加到required列表中
if is_required:
type_schema["required"].append(param_name)
logger.debug(f"参数 {param_name} 标记为必填")
def _cleanup_empty_required_lists(schema: Dict[str, Any]) -> None:
"""
清理Schema中的空required列表
移除Schema中所有空的required数组保持Schema的简洁性。
这包括嵌套属性中的required列表和顶级的required列表。
清理规则:
1. 遍历所有请求类型的Schema
2. 如果某个类型的required列表为空删除该字段
3. 如果顶级required列表为空删除该字段
参数:
schema: 要清理的Schema对象
"""
# 清理嵌套属性中的空required列表
for req_type in list(schema["properties"].keys()):
type_schema = schema["properties"][req_type]
if not type_schema.get("required"):
# 如果required列表为空删除该字段
type_schema.pop("required", None)
# 清理顶级的空required列表
if not schema.get("required"):
schema.pop("required", None)
def create_structured_data(
schema: Dict[str, Any], params: Dict[str, Any]
) -> Dict[str, Any]:
"""
Generate data structure based on schema and input parameters.
Automatically matches input parameters to schema fields and creates
a properly structured data object.
Args:
schema: JSON schema object defining the data structure
params: Parameter values to fill
Returns:
dict: Structured data object conforming to schema
Raises:
TypeError: If schema or params are not dictionaries
"""
if not isinstance(schema, dict):
raise TypeError("schema must be a dictionary")
if not isinstance(params, dict):
raise TypeError("params must be a dictionary")
result = {}
# Process each top-level schema property
for field_name, field_schema in schema.get("properties", {}).items():
result[field_name] = {}
field_properties = field_schema.get("properties", {})
# Find matching parameters for this field
matched_params = {
param_name: param_value
for param_name, param_value in params.items()
if param_name in field_properties
}
# Only include field if there are matching parameters
if matched_params:
result[field_name] = matched_params
return result
def generate_schema_prompt(schema: Dict[str, Any]) -> str:
"""
Generate descriptive prompt from JSON Schema for LLM guidance.
Args:
schema: JSON Schema object returned by generate_json_schema
Returns:
str: Structured prompt text for guiding LLM parameter generation
Raises:
TypeError: If schema is not a dictionary
"""
if not isinstance(schema, dict):
raise TypeError("schema must be a dictionary")
prompt_parts = ["当前工具所需参数:\n"]
# Process each parameter group
for group_name, group_schema in schema.get("properties", {}).items():
prompt_parts.append(f"## {group_name} 参数组:")
required_params = set(group_schema.get("required", []))
# Process each parameter in the group
for param_name, param_info in group_schema.get("properties", {}).items():
param_type = param_info.get("type", JsonSchemaType.STRING)
param_desc = param_info.get("description", "")
required_mark = "(必填)" if param_name in required_params else "(可选)"
prompt_parts.append(
f"- {param_name}{required_mark}: {param_desc}, 类型: {param_type}"
)
prompt_parts.append("")
# Add output format guidance
prompt_parts.extend(
[
"参数格式要求JSON对象包含所有必填字段。",
"示例格式:",
"```json",
"{",
]
)
for group_name in schema.get("properties", {}):
prompt_parts.append(f' "{group_name}": {{')
prompt_parts.append(" // 相关参数")
prompt_parts.append(" },")
prompt_parts.extend(["}", "```"])
return "\n".join(prompt_parts)
def _remove_property_from_schema(schema: Dict[str, Any], property_name: str) -> None:
"""
Remove a property from schema (helper function to reduce code duplication).
Args:
schema: Schema object to modify
property_name: Name of property to remove
"""
# Remove from properties
if isinstance(schema.get("properties"), dict):
schema["properties"].pop(property_name, None)
# Remove from required list
if isinstance(schema.get("required"), list):
try:
schema["required"].remove(property_name)
except ValueError:
pass # Property not in required list, ignore
# Remove empty required list
if not schema["required"]:
schema.pop("required", None)
def remove_property_from_api_item(
api_item: Dict[str, Any], property_name: str
) -> Dict[str, Any]:
"""
Remove specified property from a single API object.
Args:
api_item: API object (single element from generate_api_array result)
property_name: Name of property to remove
Returns:
dict: Processed API object with property removed
Raises:
ValueError: When input parameters are invalid
TypeError: When input types are incorrect
"""
if not isinstance(api_item, dict) or not api_item:
raise ValueError("api_item must be a non-empty dictionary")
if not isinstance(property_name, str) or not property_name.strip():
raise ValueError("property_name must be a non-empty string")
# Create deep copy to avoid modifying original
new_api = copy.deepcopy(api_item)
if "schema" in new_api and isinstance(new_api["schema"], dict):
_remove_property_from_schema(new_api["schema"], property_name)
return new_api
def remove_property_from_api_array(
api_array: List[Dict[str, Any]], property_name: str
) -> List[Dict[str, Any]]:
"""
Remove specified property from all API objects in array.
Args:
api_array: API array returned by generate_api_array
property_name: Name of property to remove
Returns:
list: Processed API array with property removed from all items
Raises:
ValueError: When input parameters are invalid
TypeError: When input types are incorrect
"""
if not isinstance(api_array, list) or not api_array:
raise ValueError("api_array must be a non-empty list")
if not isinstance(property_name, str) or not property_name.strip():
raise ValueError("property_name must be a non-empty string")
result = []
for api_item in api_array:
try:
processed_item = remove_property_from_api_item(api_item, property_name)
result.append(processed_item)
except Exception as e:
result.append(copy.deepcopy(api_item)) # Keep original if processing fails
return result
def fill_default_values_by_schema(
schema: Dict[str, Any], arguments: Dict[str, Any]
) -> Dict[str, Any]:
"""
Auto-fill default values in arguments based on JSON Schema.
Only fills missing parameters or those with None/empty string values.
Args:
schema: JSON Schema object with grouped structure (body/query/lzwcaiConfig)
arguments: User input parameter dictionary
Returns:
dict: Parameter dictionary with default values filled
Raises:
TypeError: When input types are incorrect
"""
if not isinstance(schema, dict):
raise TypeError("schema must be a dictionary")
if not isinstance(arguments, dict):
arguments = {}
else:
arguments = copy.deepcopy(arguments)
for group_name, group_schema in schema.get("properties", {}).items():
if not isinstance(arguments.get(group_name), dict):
arguments[group_name] = {}
for param_name, param_schema in group_schema.get("properties", {}).items():
default_value = param_schema.get("default")
current_value = arguments[group_name].get(param_name)
# Fill default only if parameter is missing, None, or empty string
if (
current_value is None or current_value == ""
) and default_value is not None:
arguments[group_name][param_name] = default_value
return arguments
def generate_api_array(api_params: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Generate API array with JSON Schema for each API.
Args:
api_params: List of API parameter definitions
Returns:
list: API array with schema attached to each item
Raises:
TypeError: If api_params is not a list
"""
if not isinstance(api_params, list):
raise TypeError("api_params must be a list")
api_array = []
for param in api_params:
try:
schema = generate_json_schema(param.get("parameters", []))
api_array.append({**param, "schema": schema})
except Exception as e:
logger.error(f"Error processing config: {str(e)}")
api_array.append(param) # Add without schema if generation fails
return api_array
def check_required_arguments(
schema: Dict[str, Any], arguments: Dict[str, Any]
) -> List[str]:
"""
Validate that arguments contain all required parameters from schema.
Args:
schema: JSON Schema object with grouped structure (body/query/lzwcaiConfig)
arguments: User input parameter dictionary
Returns:
list: Missing required parameters with group names, e.g., ['body.username', 'lzwcaiConfig.userId']
Raises:
TypeError: When input types are incorrect
"""
if not isinstance(schema, dict):
raise TypeError("schema must be a dictionary")
if not isinstance(arguments, dict):
arguments = {}
missing_params = []
for group_name, group_schema in schema.get("properties", {}).items():
group_args = arguments.get(group_name, {})
required_params = group_schema.get("required", [])
for param_name in required_params:
param_value = group_args.get(param_name) if group_args else None
# Consider missing if not provided, None, or empty string
if param_value is None or param_value == "":
# Try to get parameter description
description = ""
try:
param_properties = group_schema.get("properties", {})
param_info = param_properties.get(param_name, {})
description = param_info.get("description", "")
except Exception:
pass
# Format missing parameter name
if description:
missing_params.append(f"{group_name}.{param_name}{description}")
else:
missing_params.append(f"{group_name}.{param_name}")
return missing_params
if __name__ == "__main__":
# Example usage and testing
try:
with open(
"E:/yh-ai/project/lzwcai-szyg/lzwcai-demp-tool-server/src/"
"lzwcai_demp_tool_server_business_to_mcp/mcp_generator/src/parameters.json",
"r",
encoding="utf-8",
) as f:
api_params = json.load(f)
api_array = generate_api_array(api_params)
result = remove_property_from_api_array(api_array, "userId")
# Write results to JSON file
with open("schema1.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=4)
except FileNotFoundError:
logger.error(f"Warning: Test data file not found. Skipping example execution.")
except Exception as e:
logger.error(f"Error in example execution: {str(e)}")

View File

@@ -0,0 +1,204 @@
"""
Business API configuration utility functions.
This module provides utilities for fetching and processing business API configurations.
"""
import json
import os
import requests
from typing import Dict, List, Any
from ..util.logger_config import get_logger
# 配置日志
logger = get_logger(__name__)
def get_business_api_details(api_ids: List[int], auth_token: str = None) -> List[Dict[str, Any]]:
"""
获取业务平台API详情
调用业务平台接口获取指定API ID列表的详细信息
Args:
api_ids: API ID列表例如 [1925128743899111425, 1925128744524062721]
auth_token: 认证token如果不提供则使用默认token
Returns:
List[Dict[str, Any]]: API详情列表返回接口响应中的data字段内容
Raises:
requests.RequestException: 当网络请求失败时抛出
ValueError: 当响应格式不正确或返回错误时抛出
Example:
>>> api_ids = [1925128743899111425, 1925128744524062721]
>>> details = get_business_api_details(api_ids)
>>> print(len(details))
2
"""
if not isinstance(api_ids, list) or not api_ids:
raise ValueError("api_ids must be a non-empty list")
# 默认认证token
default_token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM3OGU0M2NlLTJhZjQtNGRjYy1iMWE1LTU3YjM5YTdkNTA1OSJ9.5f1lSJJdLUunZIwCfneT1DiagGN4jD-QCnFCffWmrnvEcLfpuSMWRpY7fF-6H3yZ2N5ICZ4ZQN6cx7iqwF6jKw"
token = auth_token or default_token
# 接口URL - 支持环境变量配置
# 默认URL
default_url = "http://lzwcai-demp-corp-manager:8086/system/mcpServer/bizSys/api/getByIds"
# 从环境变量获取URL如果没有设置则使用默认URL
url = os.getenv("lzwcai_mcp_dyntoolapi_auth_url", default_url)
# 请求头
headers = {
"Authorization": f"Bearer {token}",
# "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
"Content-Type": "application/json",
# "Accept": "*/*",
# "Host": "192.168.2.236:8088",
"Connection": "keep-alive"
}
try:
# 发送POST请求
response = requests.post(url, headers=headers, json=api_ids, timeout=30)
response.raise_for_status() # 检查HTTP状态码
# 解析响应JSON
response_data = response.json()
# 检查响应格式
if not isinstance(response_data, dict):
raise ValueError("响应格式不正确期望JSON对象")
# 检查业务状态码
code = response_data.get("code")
if code != 200:
msg = response_data.get("msg", "未知错误")
raise ValueError(f"业务接口返回错误code={code}, msg={msg}")
# 获取data字段
data = response_data.get("data", [])
if not isinstance(data, list):
logger.warning("响应中的data字段不是列表类型将转换为列表")
data = [data] if data is not None else []
logger.info(f"成功获取 {len(data)} 个API详情")
return data
except requests.exceptions.Timeout:
raise requests.RequestException("请求超时:接口响应时间过长")
except requests.exceptions.ConnectionError:
raise requests.RequestException("连接错误:无法连接到业务平台服务器")
except requests.exceptions.HTTPError as e:
raise requests.RequestException(f"HTTP错误{e}")
except json.JSONDecodeError:
raise ValueError("响应格式错误无法解析JSON数据")
except Exception as e:
logger.error(f"获取API详情时发生未知错误{str(e)}")
raise
def map_api_details_to_config(api_details: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
将API详情数据映射为api_config.json格式
Args:
api_details: get_business_api_details方法返回的API详情列表
Returns:
Dict[str, Any]: 符合api_config.json格式的配置对象
Raises:
ValueError: 当输入数据格式不正确时抛出
Example:
>>> api_details = get_business_api_details([1925128743899111425])
>>> config = map_api_details_to_config(api_details)
>>> print(config["serverName"])
lzwcai_mcp_api_converter
"""
if not isinstance(api_details, list):
raise ValueError("api_details must be a list")
if not api_details:
raise ValueError("api_details cannot be empty")
# 获取第一个API的domainUrl作为全局domainUrl
domain_url = ""
if api_details and isinstance(api_details[0], dict):
domain_url = api_details[0].get("domainUrl", "")
# 收集所有businessPrompts用于生成description
business_prompts = []
for api in api_details:
if isinstance(api, dict) and api.get("businessPrompts"):
business_prompts.append(api["businessPrompts"])
# 生成description
description = "".join(business_prompts) if business_prompts else "业务API集合"
# 构建配置对象
config = {
"serverName": "lzwcai_mcp_api_converter",
"description": description,
"domainUrl": domain_url,
"packageName": "lzwcai-mcp-dyntoolapi",
"version": "1.0.0",
"apiConfig": api_details # 直接使用原始API详情数据
}
logger.info(f"成功映射 {len(api_details)} 个API到配置格式")
logger.info(f"服务名称: {config['serverName']}")
logger.info(f"域名URL: {config['domainUrl']}")
logger.info(f"描述: {config['description']}")
return config
def get_business_api_config(api_ids: List[int], auth_token: str = None) -> Dict[str, Any]:
"""
一步到位获取业务平台API配置
传入API ID列表直接返回处理好的api_config.json格式配置
Args:
api_ids: API ID列表例如 [1925128743899111425, 1925128744524062721]
auth_token: 认证token如果不提供则使用默认token
Returns:
Dict[str, Any]: 符合api_config.json格式的完整配置对象
Raises:
requests.RequestException: 当网络请求失败时抛出
ValueError: 当响应格式不正确或返回错误时抛出
Example:
>>> api_ids = [1925128743899111425, 1925128744524062721]
>>> config = get_business_api_config(api_ids)
>>> print(config["serverName"])
lzwcai_mcp_api_converter
>>> print(len(config["apiConfig"]))
2
"""
try:
# 步骤1: 获取API详情
logger.info(f"开始获取 {len(api_ids)} 个API的详情...")
api_details = get_business_api_details(api_ids, auth_token)
# 步骤2: 映射为配置格式
logger.info("开始映射为配置格式...")
config = map_api_details_to_config(api_details)
logger.info(f"[SUCCESS] 成功生成API配置包含 {len(config['apiConfig'])} 个API")
return config
except Exception as e:
logger.error(f"获取业务API配置时发生错误: {str(e)}")
raise

Some files were not shown because too many files have changed in this diff Show More