feat(lzwcai-demp-tool-server-dify-to-mcp): 初始化 Dify 集成工具模块
新增 Dify 到 MCP 的集成工具,支持通过 Dify API 将模型部署到 MCP 平台并进行推理。 该模块包含完整的服务器实现、依赖配置和命令行启动脚本。 主要功能: - 支持 Workflow 和 Completion 模式的调用 - 自动翻译工具名称为驼峰命名格式 - 提供文件上传与任务停止接口 - 兼容流式与非流式响应处理
This commit is contained in:
@@ -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
|
||||
@@ -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) |
|
||||
@@ -0,0 +1,2 @@
|
||||
# 此文件用于确保 logs 目录被 Git 跟踪
|
||||
# 日志文件会自动生成在此目录中
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
src
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,4 @@
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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)),
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
Binary file not shown.
@@ -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,
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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("日志配置测试完成!")
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
@@ -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() # 取消注释并配置后运行
|
||||
Binary file not shown.
Binary file not shown.
@@ -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")
|
||||
Reference in New Issue
Block a user