commit ec7e7fd7dcef395038b8cd3bcea08b177a23a124 Author: yuanzhipeng <2501363769@qq.com> Date: Tue Dec 16 17:52:04 2025 +0800 feat(lzwcai-demp-tool-server-dify-to-mcp): 初始化 Dify 集成工具模块 新增 Dify 到 MCP 的集成工具,支持通过 Dify API 将模型部署到 MCP 平台并进行推理。 该模块包含完整的服务器实现、依赖配置和命令行启动脚本。 主要功能: - 支持 Workflow 和 Completion 模式的调用 - 自动翻译工具名称为驼峰命名格式 - 提供文件上传与任务停止接口 - 兼容流式与非流式响应处理 diff --git a/README.md b/README.md new file mode 100644 index 0000000..38d5a1f --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# lzwcai-mcp-server-package + +MCP (Model Context Protocol) 服务器工具集,为 AI 助手提供企业级业务能力扩展。 + +## 📦 包含模块 + +| 模块 | 版本 | 说明 | +|------|------|------| +| [lzwcai-mcp-iot](./lzwcai_mcp_iot) | 0.3.3 | IoT 设备控制服务器,支持设备查询、定位和控制 | +| [lzwcai-mcp-sqlexecutor](./lzwcai_mcp_sqlexecutor) | 0.1.8 | SQL 查询执行服务器,支持动态工具生成 | +| [lzwcai-mcp-api-converter](./lzwcai_mcp_api_converter) | 0.1.30 | API 转换服务器,将业务 API 转换为 MCP 工具 | +| [lzwcai-demp-tool-server-dify-to-mcp](./lzwcai_demp_tool_server_dify_to_mcp) | 0.1.4 | Dify 集成工具,将 Dify 模型部署到 MCP | +| [lzwcai-demp-tool-server-dify-to-mcp-test](./lzwcai_demp_tool_server_dify_to_mcp_test) | 0.1.0 | Dify 集成工具测试版 | + +## 🚀 快速安装 + +```bash +# IoT 设备控制 +pip install lzwcai-mcp-iot + +# SQL 查询执行 +pip install lzwcai-mcp-sqlexecutor + +# API 转换器 +pip install lzwcai-mcp-api-converter + +# Dify 集成 +pip install lzwcai-demp-tool-server-dify-to-mcp +``` + +## �️ 打包 与发布 + +```bash +# 进入子模块目录 +cd lzwcai_mcp_iot + +# 使用 uv 打包 +uv build + +# 上传到管理端技能广场 +# 将 dist/ 目录下的 .tar.gz 文件上传至技能广场 +``` + +## 🔧 MCP 客户端配置示例 + +```json +{ + "mcpServers": { + "iot": { + "command": "lzwcai-mcp-iot" + }, + "sql": { + "command": "lzwcai-mcp-sqlexecutor" + }, + "api": { + "command": "lzwcai-mcp-api-converter" + } + } +} +``` + +## 📄 许可证 + +专有软件 - 版权所有 © LZWCAI开发团队 + +## 📧 联系方式 + +- 邮箱:dev@lzwcai.com diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/PKG-INFO b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/PKG-INFO new file mode 100644 index 0000000..005f507 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.4 +Name: lzwcai-demp-tool-server-dify-to-mcp +Version: 0.1.0 +Summary: 这是一个Dify to MCP的集成工具;通过Dify的API接口,将Dify的模型部署到MCP平台,并进行推理。 +Requires-Python: >=3.10 +Requires-Dist: httpx>=0.28.1 +Requires-Dist: mcp>=1.1.2 +Requires-Dist: omegaconf>=2.3.0 +Requires-Dist: pip>=24.3.1 +Requires-Dist: python-dotenv>=1.0.1 +Requires-Dist: requests +Requires-Dist: pypinyin>=0.54.0 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/README.md b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/README.md new file mode 100644 index 0000000..5216d21 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/README.md @@ -0,0 +1,37 @@ +# lzwcai-mcp-server-package + +#### 介绍 +lzwcai-mcp-server-package + +#### 软件架构 +软件架构说明 + + +#### 安装教程 + +1. xxxx +2. xxxx +3. xxxx + +#### 使用说明 + +1. xxxx +2. xxxx +3. xxxx + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + + +#### 特技 + +1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO new file mode 100644 index 0000000..ca5b98b --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.4 +Name: lzwcai-demp-tool-server-dify-to-mcp +Version: 0.1.4 +Summary: 这是一个Dify to MCP的集成工具;通过Dify的API接口,将Dify的模型部署到MCP平台,并进行推理。 +Requires-Python: >=3.10 +Requires-Dist: httpx>=0.28.1 +Requires-Dist: mcp>=1.1.2 +Requires-Dist: omegaconf>=2.3.0 +Requires-Dist: pip>=24.3.1 +Requires-Dist: python-dotenv>=1.0.1 +Requires-Dist: requests +Requires-Dist: pypinyin>=0.54.0 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt new file mode 100644 index 0000000..5c7186f --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt @@ -0,0 +1,23 @@ +README.md +pyproject.toml +setup.cfg +lzwcai_demp_tool_server_dify_to_mcp.egg-info/PKG-INFO +lzwcai_demp_tool_server_dify_to_mcp.egg-info/SOURCES.txt +lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt +lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt +lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt +lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt +src/__init__.py +src/create_mcp.py +src/create_mcp_util.py +src/chat/__init__.py +src/chat/chat_server.py +src/completion/completion_server.py +src/completion/test.py +src/core/__init__.py +src/core/core_server.py +src/difyTaskCall/task_instance.py +src/utils/tool_translation.py +src/utils/translator.py +src/workflow/__init__.py +src/workflow/workflow_server.py \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt new file mode 100644 index 0000000..e3a3035 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +lzwcai-demp-tool-server-dify-to-mcp = src.create_mcp:run_main diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt new file mode 100644 index 0000000..5d3b552 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/requires.txt @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt new file mode 100644 index 0000000..85de9cf --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp.egg-info/top_level.txt @@ -0,0 +1 @@ +src diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/main.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/main.py new file mode 100644 index 0000000..b5987e3 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/main.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +主入口文件 +用于启动 Dify MCP 服务器,并配置命令行参数 +""" +import os +import sys + +# Mock 配置参数 +def setup_mock_arguments(): + """ + 设置模拟命令行参数 + 这些参数可以根据实际需求进行修改 + """ + # 默认配置 + default_config = { + "base_url": "http://192.168.2.236:3001/v1", + "app_sks": ["app-YFHByB4whARWVqXN2LcuPudq"], + "mode_type": "workflow", + "transport": "stdio" + } + + # 如果没有提供命令行参数,则添加默认参数 + if len(sys.argv) == 1: + sys.argv.extend([ + "--base-url", default_config["base_url"], + "--app-sks", *default_config["app_sks"], + "--mode-type", default_config["mode_type"] + ]) + + return default_config + + +def main(): + """ + 主函数:设置命令行参数并启动服务器 + """ + # 设置模拟命令行参数 + config = setup_mock_arguments() + + # 导入并运行 MCP 服务器 + try: + from src.create_mcp import run_main + + # 获取传输模式 + transport_mode = config.get("transport", "stdio") + + # 运行服务器(不输出额外信息,避免干扰 STDIO 通信) + run_main(transport=transport_mode) + + except ImportError as e: + print(f"[ERROR] 导入错误: {e}", file=sys.stderr) + print("请确保已正确安装所有依赖包", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"[ERROR] 运行错误: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/pyproject.toml b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/pyproject.toml new file mode 100644 index 0000000..5779d9b --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "lzwcai-demp-tool-server-dify-to-mcp" +version = "0.1.4" +description = "这是一个Dify to MCP的集成工具;通过Dify的API接口,将Dify的模型部署到MCP平台,并进行推理。" +requires-python = ">=3.10" +dependencies = [ + "httpx>=0.28.1", + "mcp>=1.1.2", + "omegaconf>=2.3.0", + "pip>=24.3.1", + "python-dotenv>=1.0.1", + "requests", + "pypinyin>=0.54.0", +] + +[tool.setuptools] +packages = {find = {where = ["."], include = ["src*"]}} +include-package-data = true + +[project.scripts] +lzwcai-demp-tool-server-dify-to-mcp = "src.create_mcp:run_main" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.setuptools.package-data] +"*" = ["*.env"] +"src" = ["**/*.env"] \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/setup.cfg b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c0b589d Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/create_mcp.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/create_mcp.cpython-312.pyc new file mode 100644 index 0000000..e46ec7f Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/create_mcp.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ab38063 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/chat_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/chat_server.cpython-312.pyc new file mode 100644 index 0000000..b9ab603 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/chat_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/chat_server.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/chat_server.py new file mode 100644 index 0000000..7d3e608 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/chat_server.py @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/__pycache__/completion_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/__pycache__/completion_server.cpython-312.pyc new file mode 100644 index 0000000..93fb769 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/__pycache__/completion_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/completion_server.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/completion_server.py new file mode 100644 index 0000000..23605c3 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/completion_server.py @@ -0,0 +1,212 @@ +import requests +from abc import ABC +import logging +import json +import re +import pypinyin + +logger = logging.getLogger(__name__) + + +def pinyin_to_camel(pinyin): + """ + 将拼音列表转换为驼峰命名 + 例如: ['ni', 'hao', 'a'] -> 'NiHaoA' + 所有非字母数字字符会被替换为下划线 + """ + # 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线 + cleaned = re.sub(r'[^\w\s]', '_', pinyin) + # 将空格也替换为下划线 + cleaned = re.sub(r'\s+', '_', cleaned) + # 移除连续的下划线并去除首尾下划线 + cleaned = re.sub(r'_+', '_', cleaned).strip('_') + + # 转换为拼音并生成驼峰命名 + pinyin_list = pypinyin.lazy_pinyin(cleaned) + return "tool_" + "".join(word.capitalize() for word in pinyin_list) + + +class CompletionDifyAPI(ABC): + def __init__(self, base_url: str, dify_app_sks: list, user="pp666"): + # dify configs + self.dify_base_url = base_url + self.dify_app_sks = dify_app_sks + self.user = user + # dify app infos + dify_app_infos = [] + dify_app_params = [] + dify_app_metas = [] + for key in self.dify_app_sks: + dify_app_infos.append(self.get_app_info(key)) + dify_app_params.append(self.get_app_parameters(key)) + dify_app_metas.append(self.get_app_meta(key)) + + self.dify_app_infos = dify_app_infos + self.dify_app_params = dify_app_params + self.dify_app_metas = dify_app_metas + self.dify_app_names = [x["name"] for x in dify_app_infos] + + def chat_message( + self, + api_key, + inputs={}, + response_mode="streaming", + conversation_id=None, + userId="pp666", + files=None, + ): + url = f"{self.dify_base_url}/completion-messages" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = { + "inputs": inputs, + "response_mode": response_mode, + "user": userId, + } + if conversation_id: + data["conversation_id"] = conversation_id + + if response_mode == "streaming": + response = requests.post(url, headers=headers, json=data, stream=True) + + # 处理流式响应 + full_answer = "" + for line in response.iter_lines(): + if line: + # 跳过 "data:" 前缀 + decoded_line = line.decode("utf-8") + if decoded_line.startswith("data:"): + try: + json_str = decoded_line[5:].strip() + data = json.loads(json_str) + if data.get("event") == "message" and "answer" in data: + # 累积完整答案 + full_answer += data["answer"] + # 这里也可以选择处理每个部分响应,例如返回生成器 + # yield data + except json.JSONDecodeError: + logger.warning(f"无法解析JSON数据: {decoded_line}") + + # 创建一个符合非流式响应格式的结果 + response_data = {"answer": full_answer} + # 处理可能包含代码块的数据 + processed_data = self.process_answer_code_block(response_data) + return processed_data + else: + response = requests.post(url, headers=headers, json=data) + response_data = response.json() + # 处理可能包含代码块的数据 + processed_data = self.process_answer_code_block(response_data) + return processed_data + + def upload_file(self, api_key, file_path, user="pp666"): + + url = f"{self.dify_base_url}/files/upload" + headers = {"Authorization": f"Bearer {api_key}"} + files = {"file": open(file_path, "rb")} + data = {"user": user} + response = requests.post(url, headers=headers, files=files, data=data) + response.raise_for_status() + return response.json() + + def stop_response(self, api_key, task_id, user="pp666"): + + url = f"{self.dify_base_url}/chat-messages/{task_id}/stop" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = {"user": user} + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + return response.json() + + def get_app_info(self, api_key, user="pp666"): + + url = f"{self.dify_base_url}/info" + headers = {"Authorization": f"Bearer {api_key}"} + # params = {"user": user} + response = requests.get(url, headers=headers) + response.raise_for_status() + + response_map = response.json() + # 翻译工具名称 + from src.utils.tool_translation import TranslationService + + tool_name = response_map.get("name") + if tool_name: + # translated_name = TranslationService.translate_tool_name(tool_name) + translated_name = pinyin_to_camel(tool_name) + response_map["name"] = translated_name + + # 翻译工具描述 + # tool_description = response_map.get("description") + # if tool_description: + # translated_description = TranslationService.translate_tool_description( + # tool_description + # ) + # response_map["description"] = ( + # f"{tool_description} ({translated_description})" + # ) + + return response_map + + def get_app_parameters(self, api_key, user="pp666"): + return { + "user_input_form": [ + {"string": {"variable": "query", "label": "查询内容", "required": True}} + ] + } + + def get_app_meta(self, api_key, user="pp666"): + url = f"{self.dify_base_url}/meta" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + + @staticmethod + def process_answer_code_block(data): + try: + # 获取answer字段 + answer = data.get("answer", "") + + # 构造符合workflow_finished格式的输出 + formatted_response = [ + {"event": "workflow_finished", "data": {"outputs": {"result": answer}}} + ] + + # 尝试处理可能的代码块 + if answer.startswith("```") and answer.endswith("```"): + try: + # 移除代码块标记并解析JSON + code_content = answer.strip("```").strip() + json_data = json.loads(code_content) + + # 如果包含description字段,用它替换answer + if "description" in json_data: + formatted_response[0]["data"]["outputs"]["result"] = json_data[ + "description" + ] + except json.JSONDecodeError: + # 如果不是有效的JSON,保留原始代码块内容 + pass + + return formatted_response + except Exception as e: + logger.warning(f"处理答案代码块时出错: {str(e)}") + # 发生错误时返回符合格式的基础响应 + return [ + { + "event": "workflow_finished", + "data": { + "outputs": { + "error": str(e), + "fallback": data.get("answer", str(data)), + } + }, + } + ] diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/test.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/test.py new file mode 100644 index 0000000..4664edf --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/test.py @@ -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}") diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/core_server.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/core_server.py new file mode 100644 index 0000000..c0bae3f --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/core_server.py @@ -0,0 +1,172 @@ +import requests +from abc import ABC +import logging +import json +import re +import pypinyin + +logger = logging.getLogger(__name__) + + +def pinyin_to_camel(pinyin): + """ + 将拼音列表转换为驼峰命名 + 例如: ['ni', 'hao', 'a'] -> 'NiHaoA' + 所有非字母数字字符会被替换为下划线 + """ + # 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线 + cleaned = re.sub(r'[^\w\s]', '_', pinyin) + # 将空格也替换为下划线 + cleaned = re.sub(r'\s+', '_', cleaned) + # 移除连续的下划线并去除首尾下划线 + cleaned = re.sub(r'_+', '_', cleaned).strip('_') + + # 转换为拼音并生成驼峰命名 + pinyin_list = pypinyin.lazy_pinyin(cleaned) + return "tool_" + "".join(word.capitalize() for word in pinyin_list) + + +class DifyAPI(ABC): + def __init__(self, base_url: str, dify_app_sks: list, user="pp666"): + # dify configs + self.dify_base_url = base_url + self.dify_app_sks = dify_app_sks + self.user = user + + # dify app infos + dify_app_infos = [] + dify_app_params = [] + dify_app_metas = [] + for key in self.dify_app_sks: + dify_app_infos.append(self.get_app_info(key)) + dify_app_params.append(self.get_app_parameters(key)) + dify_app_metas.append(self.get_app_meta(key)) + + print("dify_app_params", dify_app_params) + self.dify_app_infos = dify_app_infos + self.dify_app_params = dify_app_params + self.dify_app_metas = dify_app_metas + self.dify_app_names = [x["name"] for x in dify_app_infos] + + def chat_message( + self, + api_key, + inputs={}, + response_mode="streaming", + conversation_id=None, + user="pp666", + files=None, + ): + url = f"{self.dify_base_url}/workflows/run" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = { + "inputs": inputs, + "response_mode": response_mode, + "user": user, + } + logger.info("Sending data to Dify API: %s", data) + logger.info("Sending headers to Dify API: %s", headers) + logger.info("Sending url to Dify API: %s", url) + if conversation_id: + data["conversation_id"] = conversation_id + if files: + files_data = [] + for file_info in files: + file_path = file_info.get("path") + transfer_method = file_info.get("transfer_method") + if transfer_method == "local_file": + files_data.append(("file", open(file_path, "rb"))) + elif transfer_method == "remote_url": + pass + response = requests.post( + url, + headers=headers, + data=data, + files=files_data, + stream=response_mode == "streaming", + ) + else: + response = requests.post( + url, headers=headers, json=data, stream=response_mode == "streaming" + ) + response.raise_for_status() + if response_mode == "streaming": + for line in response.iter_lines(): + if line: + if line.startswith(b"data:"): + try: + json_data = json.loads(line[5:].decode("utf-8")) + yield json_data + except json.JSONDecodeError: + print(f"Error decoding JSON: {line}") + else: + return response.json() + + def upload_file(self, api_key, file_path, user="pp666"): + + url = f"{self.dify_base_url}/files/upload" + headers = {"Authorization": f"Bearer {api_key}"} + files = {"file": open(file_path, "rb")} + data = {"user": user} + response = requests.post(url, headers=headers, files=files, data=data) + response.raise_for_status() + return response.json() + + def stop_response(self, api_key, task_id, user="pp666"): + + url = f"{self.dify_base_url}/chat-messages/{task_id}/stop" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = {"user": user} + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + return response.json() + + def get_app_info(self, api_key, user="pp666"): + + url = f"{self.dify_base_url}/info" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + from src.utils.tool_translation import TranslationService + + response_map = response.json() + # 翻译工具名称 + # tool_name = response_map.get("name") + # translated_name = TranslationService.translate_tool_name(tool_name) + # response_map["name"] = translated_name + # # 翻译工具描述 + # tool_description = response_map.get("description") + # translated_description = TranslationService.translate_tool_description( + # tool_description + # ) + # response_map["description"] = translated_description + tool_name = response_map.get("name") + if tool_name: + # translated_name = TranslationService.translate_tool_name(tool_name) + translated_name = pinyin_to_camel(tool_name) + response_map["name"] = translated_name + return response_map + + def get_app_parameters(self, api_key, user="pp666"): + url = f"{self.dify_base_url}/parameters" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + + def get_app_meta(self, api_key, user="pp666"): + url = f"{self.dify_base_url}/meta" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp.py new file mode 100644 index 0000000..3cd085e --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp.py @@ -0,0 +1,318 @@ +import asyncio +import json +import os +import logging +import argparse +from abc import ABC + +import mcp.server.stdio +import mcp.types as types +import requests +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions +from omegaconf import OmegaConf + +# from src.workflow.workflow_server import WorkflowDifyAPI +from src.difyTaskCall.task_instance import TaskInstance + +# 配置日志记录 +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Dify MCP服务器配置") + parser.add_argument( + "--base-url", + type=str, + help="API基础URL", + default="http://192.168.11.24:3001/v1", + ) + parser.add_argument( + "--app-sks", + nargs="+", + help="应用秘钥列表", + default=["app-d7s00CJ2NY4LJzUEiZsVDnPN"], + ) + parser.add_argument( + "--mode-type", + type=str, + help="Dify应用模式类型 (workflow, chat, completion)", + default="workflow", + choices=["workflow", "chat", "completion"], + ) + return parser.parse_args() + + +def get_app_info(base_url=None, app_sks=None, mode_type=None): + # 获取命令行参数 + args = parse_arguments() + # 命令行参数优先,其次是函数参数,最后是默认值 + if args.base_url is not None: + base_url = args.base_url + if base_url is None: + base_url = "http://192.168.11.24:3001/v1" + + if args.app_sks is not None: + app_sks = args.app_sks + if app_sks is None: + app_sks = ["app-d7s00CJ2NY4LJzUEiZsVDnPN"] + + # 确保 app_sks 始终是列表类型 + if isinstance(app_sks, str): + # 如果是字符串,转换为列表 + app_sks = [app_sks] + + if args.mode_type is not None: + mode_type = args.mode_type + if mode_type is None: + mode_type = "workflow" + return base_url, app_sks, mode_type + # return "https://dempdify.lzwcai.com/v1", ["app-X6wAy5nkvWB3hR69cgvIjC3r"], "workflow" + + + +# 初始化服务器和Dify API +base_url, dify_app_sks, dify_app_mode_type = get_app_info() +server = Server("dify_mcp_server") +task_instance = TaskInstance(base_url, dify_app_sks, dify_app_mode_type) +dify_api = task_instance.get_task_instance(dify_app_mode_type) + + +def process_user_input_form(user_input_form): + """ + 处理Dify应用的用户输入表单,转换为JSON Schema格式 + + 参数: + user_input_form: Dify应用的用户输入表单配置 + + 返回: + 处理后的inputSchema字典 + """ + inputSchema = dict( + type="object", + properties={}, + required=[], + ) + + property_num = len(user_input_form) + if property_num > 0: + for j in range(property_num): + param = user_input_form[j] + param_type = list(param.keys())[0] + param_info = param[param_type] + property_name = param_info["variable"] + + # 根据不同控件类型处理 + if param_type == "text-input": + inputSchema["properties"][property_name] = { + "type": "string", + "description": param_info["label"], + } + if "default" in param_info: + inputSchema["properties"][property_name]["default"] = param_info[ + "default" + ] + + elif param_type == "paragraph": + inputSchema["properties"][property_name] = { + "type": "string", + "description": param_info["label"], + "format": "paragraph", + } + if "default" in param_info: + inputSchema["properties"][property_name]["default"] = param_info[ + "default" + ] + + elif param_type == "select": + inputSchema["properties"][property_name] = { + "type": "string", + "description": param_info["label"], + "enum": param_info["options"], + } + if "default" in param_info: + inputSchema["properties"][property_name]["default"] = param_info[ + "default" + ] + + elif param_type == "file_upload": + # 文件上传控件处理 + file_type_schema = { + "type": "object", + "description": param_info["label"], + "properties": { + "file_url": {"type": "string", "description": "文件URL"}, + "file_name": {"type": "string", "description": "文件名称"}, + }, + "required": ["file_url"], + } + + # 处理图片上传配置 + if "image" in param_info and param_info["image"]["enabled"]: + image_config = param_info["image"] + file_type_schema["properties"]["type"] = { + "type": "string", + "description": "文件类型,支持png、jpg、jpeg、webp、gif", + "enum": ["png", "jpg", "jpeg", "webp", "gif"], + } + + # 处理数量限制 + number_limits = image_config.get("number_limits", 3) + if number_limits > 1: + # 如果允许多个文件,则使用数组 + inputSchema["properties"][property_name] = { + "type": "array", + "description": param_info["label"], + "items": file_type_schema, + "maxItems": number_limits, + } + else: + # 如果只允许单个文件 + inputSchema["properties"][property_name] = file_type_schema + else: + # 如果没有特定的图片配置,使用一般文件配置 + inputSchema["properties"][property_name] = file_type_schema + + else: + # 默认处理为字符串类型 + inputSchema["properties"][property_name] = { + "type": "string", + "description": param_info["label"], + } + + # 处理必填字段 + if param_info.get("required", False): + inputSchema["required"].append(property_name) + + # 添加必填的userId参数,支持数字或字符串类型 + # inputSchema["properties"]["userId"] = dict( + # oneOf=[{"type": "number"}, {"type": "string"}], + # description="您的员工ID,用于识别您的员工身份", + # ) + # inputSchema["required"].append("userId") + + return inputSchema + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """ + 列出可用的工具 + 返回: + 工具列表,每个工具都使用JSON Schema验证其参数 + """ + tools = [] + tool_names = dify_api.dify_app_names + tool_infos = dify_api.dify_app_infos + tool_params = dify_api.dify_app_params + tool_num = len(tool_names) + for i in range(tool_num): + # 加载每个工具的应用信息 + app_info = tool_infos[i] + # 加载每个工具的应用参数 + app_param = tool_params[i] + # 处理用户输入表单 + inputSchema = process_user_input_form(app_param["user_input_form"]) + + tools.append( + types.Tool( + name=app_info["name"], + description=app_info["description"], + inputSchema=inputSchema, + ) + ) + return tools + + +@server.call_tool() +async def handle_call_tool( + name: str, arguments: dict | None +) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """ + 调用工具处理请求 + 参数: + name: 工具名称 + arguments: 工具参数 + 返回: + 处理结果列表 + """ + tool_names = dify_api.dify_app_names + if name in tool_names: + tool_idx = tool_names.index(name) + tool_sk = dify_api.dify_app_sks[tool_idx] + responses = dify_api.chat_message( + tool_sk, + inputs=arguments, + userId=arguments.get("userId", "pp666"), + ) + for res in responses: + if res["event"] == "workflow_finished": + outputs = res["data"]["outputs"] + mcp_out = [] + for _, v in outputs.items(): + mcp_out.append(types.TextContent(type="text", text=v)) + return mcp_out + else: + raise ValueError(f"Unknown tool: {name}") + + +def run_main(transport="stdio"): + """ + 主函数:使用stdin/stdout流运行服务器 + """ + if transport == "stdio": + import anyio + from mcp.server.stdio import stdio_server + + async def arun(): + async with stdio_server() as streams: + await server.run( + streams[0], + streams[1], + InitializationOptions( + server_name="dify_mcp_server", + server_version="0.0.6", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + anyio.run(arun) + + else: + from mcp.server.sse import SseServerTransport + from starlette.applications import Starlette + from starlette.responses import Response + from starlette.routing import Mount, Route + + sse = SseServerTransport("/messages/") + + async def handle_sse(request): + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await server.run( + streams[0], streams[1], server.create_initialization_options() + ) + return Response() + + starlette_app = Starlette( + debug=True, + routes=[ + Route("/sse", endpoint=handle_sse, methods=["GET"]), + Mount("/messages/", app=sse.handle_post_message), + ], + ) + + import uvicorn + + uvicorn.run(starlette_app, host="0.0.0.0", port=8080, log_level="info") + + +if __name__ == "__main__": + run_main() diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp_util.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp_util.py new file mode 100644 index 0000000..7bff99a --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp_util.py @@ -0,0 +1,373 @@ +import json +from typing import Dict, List, Any, Tuple, Optional +import logging +from pathlib import Path + + +# 常量定义 +DEFAULT_NUMBER_LIMITS = 3 # 默认文件数量限制 + + +def process_user_input_form( + user_input_form: List[Dict[str, Any]], +) -> Tuple[Dict[str, Any], List[str]]: + """ + 处理Dify用户输入表单,生成对应的JSON Schema properties和required列表 + + 参数: + user_input_form: Dify应用的用户输入表单配置 + + 返回: + properties: 表单字段的properties字典 + required: 必填字段列表 + """ + properties = {} + required = [] + + if not user_input_form: + return properties, required + + for param in user_input_form: + try: + # 直接获取字典的第一个键,而不是通过list转换 + param_type = next(iter(param)) + param_info = param[param_type] + property_name = param_info["variable"] + + properties[property_name] = { + "type": param_type, + "description": param_info["label"], + } + + if param_info.get("required", False): + required.append(property_name) + except (KeyError, StopIteration) as e: + logging.warning(f"处理用户输入表单项时出错: {e}, 跳过此项") + continue + + return properties, required + + +def process_file_upload(file_upload: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """ + 处理Dify文件上传配置,生成对应的JSON Schema properties + + 设计为通用实现,支持任意文件类型配置 + + 参数: + file_upload: Dify应用的文件上传配置 + + 返回: + file_properties: 文件上传的properties字典 + """ + # 检查是否存在文件上传配置 + if not file_upload: + return {} + + # 收集所有启用的文件类型信息 + enabled_types = [] + max_items = 0 + supported_transfer_methods = set() + file_type_configs = {} + + for file_type, config in file_upload.items(): + if not config.get("enabled", False): + continue + + enabled_types.append(file_type) + number_limits = config.get("number_limits", DEFAULT_NUMBER_LIMITS) + max_items += number_limits + type_transfer_methods = config.get("transfer_methods", []) + supported_transfer_methods.update(type_transfer_methods) + + # 存储每种文件类型的详细配置 + file_type_configs[file_type] = { + "number_limits": number_limits, + "transfer_methods": type_transfer_methods, + } + + # 如果没有启用的文件类型,返回空字典 + if not enabled_types: + return {} + + # 构建文件项的JSON Schema + file_item_schema = _build_file_item_schema( + enabled_types, supported_transfer_methods, file_type_configs + ) + + # 构建最终的files属性 + file_properties = { + "files": { + "type": "array", + "items": file_item_schema, + "description": "支持多种文件类型的文件列表", + "maxItems": max_items, + } + } + + return file_properties + + +def _build_file_item_schema( + enabled_types: List[str], + supported_transfer_methods: set, + file_type_configs: Dict[str, Dict[str, Any]], +) -> Dict[str, Any]: + """ + 构建文件项的JSON Schema + + 参数: + enabled_types: 启用的文件类型列表 + supported_transfer_methods: 支持的传输方式集合 + file_type_configs: 每种文件类型的配置信息 + + 返回: + file_item_schema: 文件项的JSON Schema + """ + file_item_schema = { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": enabled_types, + "description": "文件类型", + }, + "transfer_method": { + "type": "string", + "enum": list(supported_transfer_methods), + "description": "传输方式", + }, + }, + } + + # 添加条件属性 + if "remote_url" in supported_transfer_methods: + file_item_schema["properties"]["url"] = { + "type": "string", + "format": "uri", + "description": "文件URL (当传输方式为remote_url时使用)", + } + + if "local_file" in supported_transfer_methods: + file_item_schema["properties"]["upload_file_id"] = { + "type": "string", + "description": "上传文件ID (当传输方式为local_file时使用)", + } + + # 添加条件验证逻辑 + file_item_schema["allOf"] = [] + + # 为每种文件类型添加验证规则 + for file_type, type_config in file_type_configs.items(): + type_methods = type_config["transfer_methods"] + + # 基本验证 + file_item_schema["allOf"].append( + { + "if": {"properties": {"type": {"const": file_type}}}, + "then": { + "properties": { + "transfer_method": { + "enum": type_methods, + "description": f"{file_type}类型支持的传输方式: {', '.join(type_methods)}", + } + } + }, + } + ) + + # 为每种传输方法添加类型特定的验证 + _add_transfer_method_validations(file_item_schema, file_type, type_methods) + + return file_item_schema + + +def _add_transfer_method_validations( + file_item_schema: Dict[str, Any], file_type: str, type_methods: List[str] +) -> None: + """ + 为每种传输方法添加类型特定的验证 + + 参数: + file_item_schema: 文件项JSON Schema + file_type: 文件类型 + type_methods: 该类型支持的传输方法列表 + """ + for method in type_methods: + if method == "remote_url": + file_item_schema["allOf"].append( + { + "if": { + "properties": { + "type": {"const": file_type}, + "transfer_method": {"const": "remote_url"}, + }, + "required": ["type", "transfer_method"], + }, + "then": {"required": ["url"]}, + } + ) + elif method == "local_file": + file_item_schema["allOf"].append( + { + "if": { + "properties": { + "type": {"const": file_type}, + "transfer_method": {"const": "local_file"}, + }, + "required": ["type", "transfer_method"], + }, + "then": {"required": ["upload_file_id"]}, + } + ) + + +def convert_dify_params_to_schema(tool_params: Dict[str, Any]) -> Dict[str, Any]: + """ + 将用户输入表单和文件上传配置组合成新的结构 + + 参数: + tool_params: 包含user_input_form和file_upload的参数字典 + + 返回: + 组合后的结构 + """ + # 参数验证 + if not isinstance(tool_params, dict): + raise TypeError("tool_params 必须是字典类型") + + # 处理用户输入表单 + properties, required = process_user_input_form( + tool_params.get("user_input_form", []) + ) + + # 处理文件上传配置 + file_properties = process_file_upload(tool_params.get("file_upload")) + + # 创建新的结构 + result = { + "inputs": {"type": "object", "properties": properties, "required": required}, + # "userId": { + # "oneOf": [{"type": "number"}, {"type": "string"}], + # "description": "您的员工ID,用于识别您的员工身份", + # "required": True, + # }, + } + + # 如果有文件上传配置,添加files字段 + if file_properties: + result["files"] = file_properties.get("files", {}) + + return result + + +def finalize_schema_structure(mock_result: Dict[str, Any]) -> Dict[str, Any]: + """ + 将mock_result构建为符合要求的map_mock_result格式 + + 参数: + mock_result: 通过convert_dify_params_to_schema函数获取的结果 + + 返回: + 构建后的map_mock_result字典 + """ + # 确定required字段 + # required_fields = ["userId"] + required_fields = [] + + # 只有当inputs的required有值时,才添加inputs到顶层required + if ( + mock_result.get("inputs", {}).get("required") + and len(mock_result["inputs"]["required"]) > 0 + ): + required_fields.append("inputs") + + # 如果有文件上传,也可以考虑添加files到required + if "files" in mock_result: + # 可以根据需求决定是否将files添加到required + # required_fields.append("files") + pass + + return { + "type": "object", + "properties": mock_result, + "required": required_fields, + } + + +def create_json_file(data: Dict[str, Any], filename: str = "output.json") -> None: + """ + 将数据保存为JSON文件 + + 参数: + data: 要保存的数据 + filename: 保存的文件名 + """ + try: + # 获取mock数据 + mock_result = convert_dify_params_to_schema(data) + + # 使用封装的方法构建map_mock_result + map_mock_result = finalize_schema_structure(mock_result) + + # 确保目标目录存在 + output_path = Path(filename) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # 将结果写入JSON文件 + with open(filename, "w", encoding="utf-8") as f: + json.dump(map_mock_result, f, ensure_ascii=False, indent=4) + + logging.info(f"已成功将数据保存至 {filename}") + print(f"已成功将数据保存至 {filename}") + + except Exception as e: + error_msg = f"保存JSON文件时出错: {e}" + logging.error(error_msg) + raise IOError(error_msg) + + +if __name__ == "__main__": + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + + # 示例数据 + api_data = { + "opening_statement": "", + "suggested_questions": [], + "suggested_questions_after_answer": {"enabled": False}, + "speech_to_text": {"enabled": False}, + "text_to_speech": {"enabled": False, "language": "", "voice": ""}, + "retriever_resource": {"enabled": False}, + "annotation_reply": {"enabled": False}, + "more_like_this": {"enabled": False}, + "user_input_form": [ + { + "paragraph": { + "label": "文案内容", + "max_length": 33024, + "options": [], + "required": True, + "type": "paragraph", + "variable": "content", + } + } + ], + "sensitive_word_avoidance": {"enabled": False}, + "file_upload": { + "image": { + "enabled": False, + "number_limits": 3, + "transfer_methods": ["local_file", "remote_url"], + } + }, + "system_parameters": {"image_file_size_limit": "10"}, + } + + try: + create_json_file(api_data) + except Exception as e: + logging.error(f"程序执行失败: {e}") diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc new file mode 100644 index 0000000..12f81e6 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/task_instance.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/task_instance.py new file mode 100644 index 0000000..9e95791 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/task_instance.py @@ -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, + ) diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/tool_translation.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/tool_translation.cpython-312.pyc new file mode 100644 index 0000000..2085820 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/tool_translation.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/translator.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/translator.cpython-312.pyc new file mode 100644 index 0000000..2a681ce Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/translator.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/tool_translation.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/tool_translation.py new file mode 100644 index 0000000..7361457 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/tool_translation.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/translator.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/translator.py new file mode 100644 index 0000000..b0b777b --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/translator.py @@ -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} diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bfa3884 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/workflow_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/workflow_server.cpython-312.pyc new file mode 100644 index 0000000..352df89 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/workflow_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/workflow_server.py b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/workflow_server.py new file mode 100644 index 0000000..8449368 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/workflow_server.py @@ -0,0 +1,175 @@ +import requests +from abc import ABC +import logging +import json +import re + +logger = logging.getLogger(__name__) + +import pypinyin + + +def pinyin_to_camel(pinyin): + """ + 将拼音列表转换为驼峰命名 + 例如: ['ni', 'hao', 'a'] -> 'NiHaoA' + 所有非字母数字字符会被替换为下划线 + """ + # 使用正则表达式将所有非字母数字字符(包括所有标点符号)替换为下划线 + cleaned = re.sub(r'[^\w\s]', '_', pinyin) + # 将空格也替换为下划线 + cleaned = re.sub(r'\s+', '_', cleaned) + # 移除连续的下划线并去除首尾下划线 + cleaned = re.sub(r'_+', '_', cleaned).strip('_') + + # 转换为拼音并生成驼峰命名 + pinyin_list = pypinyin.lazy_pinyin(cleaned) + return "tool_" + "".join(word.capitalize() for word in pinyin_list) + + +class WorkflowDifyAPI(ABC): + def __init__(self, base_url: str, dify_app_sks: list, user="pp666"): + # dify configs + self.dify_base_url = base_url + self.dify_app_sks = dify_app_sks + self.user = user + + # dify app infos + dify_app_infos = [] + dify_app_params = [] + dify_app_metas = [] + for key in self.dify_app_sks: + dify_app_infos.append(self.get_app_info(key)) + dify_app_params.append(self.get_app_parameters(key)) + dify_app_metas.append(self.get_app_meta(key)) + + self.dify_app_infos = dify_app_infos + self.dify_app_params = dify_app_params + self.dify_app_metas = dify_app_metas + self.dify_app_names = [x["name"] for x in dify_app_infos] + + def chat_message( + self, + api_key, + inputs={}, + response_mode="streaming", + conversation_id=None, + userId="pp666", + files=None, + ): + url = f"{self.dify_base_url}/workflows/run" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = { + "inputs": inputs, + "response_mode": response_mode, + "user": userId, + } + logger.info("Sending data to Dify API: %s", data) + logger.info("Sending headers to Dify API: %s", headers) + logger.info("Sending url to Dify API: %s", url) + if conversation_id: + data["conversation_id"] = conversation_id + if files: + files_data = [] + for file_info in files: + file_path = file_info.get("path") + transfer_method = file_info.get("transfer_method") + if transfer_method == "local_file": + files_data.append(("file", open(file_path, "rb"))) + elif transfer_method == "remote_url": + pass + response = requests.post( + url, + headers=headers, + data=data, + files=files_data, + stream=response_mode == "streaming", + ) + else: + response = requests.post( + url, headers=headers, json=data, stream=response_mode == "streaming" + ) + response.raise_for_status() + if response_mode == "streaming": + for line in response.iter_lines(): + if line: + if line.startswith(b"data:"): + try: + json_data = json.loads(line[5:].decode("utf-8")) + yield json_data + except json.JSONDecodeError: + print(f"Error decoding JSON: {line}") + else: + return response.json() + + def upload_file(self, api_key, file_path, user="pp666"): + + url = f"{self.dify_base_url}/files/upload" + headers = {"Authorization": f"Bearer {api_key}"} + files = {"file": open(file_path, "rb")} + data = {"user": user} + response = requests.post(url, headers=headers, files=files, data=data) + response.raise_for_status() + return response.json() + + def stop_response(self, api_key, task_id, user="pp666"): + + url = f"{self.dify_base_url}/chat-messages/{task_id}/stop" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = {"user": user} + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + return response.json() + + def get_app_info(self, api_key, user="pp666"): + + url = f"{self.dify_base_url}/info" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + from src.utils.tool_translation import TranslationService + + response_map = response.json() + + # 翻译工具名称 + tool_name = response_map.get("name") + if tool_name: + # translated_name = TranslationService.translate_tool_name(tool_name) + translated_name = pinyin_to_camel(tool_name) + response_map["name"] = translated_name + + # 翻译工具描述 + # tool_description = response_map.get("description") + # if tool_description: + # translated_description = TranslationService.translate_tool_description( + # tool_description + # ) + # response_map["description"] = ( + # f"{tool_description} ({translated_description})" + # ) + + return response_map + + def get_app_parameters(self, api_key, user="pp666"): + url = f"{self.dify_base_url}/parameters" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + + def get_app_meta(self, api_key, user="pp666"): + url = f"{self.dify_base_url}/meta" + headers = {"Authorization": f"Bearer {api_key}"} + params = {"user": user} + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/PKG-INFO b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/PKG-INFO new file mode 100644 index 0000000..450e42d --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/PKG-INFO @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/README.md b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/README.md new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/dify-api.md b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/dify-api.md new file mode 100644 index 0000000..2e3d1a2 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/dify-api.md @@ -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) | \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/.gitkeep b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/.gitkeep new file mode 100644 index 0000000..dce7f8c --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/.gitkeep @@ -0,0 +1,2 @@ +# 此文件用于确保 logs 目录被 Git 跟踪 +# 日志文件会自动生成在此目录中 \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/lzwcai_demp_tool_server_dify_to_mcp_test.log b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/lzwcai_demp_tool_server_dify_to_mcp_test.log new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/PKG-INFO b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/PKG-INFO new file mode 100644 index 0000000..ac937cd --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/PKG-INFO @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/SOURCES.txt b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/SOURCES.txt new file mode 100644 index 0000000..5b057d9 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/dependency_links.txt b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/entry_points.txt b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/entry_points.txt new file mode 100644 index 0000000..ebcb63a --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +lzwcai-demp-tool-server-dify-to-mcp-test = src.create_mcp:run_main diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/requires.txt b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/requires.txt new file mode 100644 index 0000000..5d3b552 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/requires.txt @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/top_level.txt b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/top_level.txt new file mode 100644 index 0000000..85de9cf --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test.egg-info/top_level.txt @@ -0,0 +1 @@ +src diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/main.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/main.py new file mode 100644 index 0000000..92a59e3 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/main.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/pyproject.toml b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/pyproject.toml new file mode 100644 index 0000000..88c5d94 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/pyproject.toml @@ -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"] \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/setup.cfg b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..12ecc7c Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp.cpython-312.pyc new file mode 100644 index 0000000..099ca20 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp_utils.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp_utils.cpython-312.pyc new file mode 100644 index 0000000..f2263ac Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp_utils.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bc6ec2a Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/chat_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/chat_server.cpython-312.pyc new file mode 100644 index 0000000..48e3fa7 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/chat_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/chat_server.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/chat_server.py new file mode 100644 index 0000000..7d3e608 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/chat_server.py @@ -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 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/__pycache__/completion_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/__pycache__/completion_server.cpython-312.pyc new file mode 100644 index 0000000..93da592 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/__pycache__/completion_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/completion_server.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/completion_server.py new file mode 100644 index 0000000..ae62c6f --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/completion_server.py @@ -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)), + } + }, + } + ] diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/test.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/test.py new file mode 100644 index 0000000..4664edf --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/test.py @@ -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}") diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/core_server.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/core_server.py new file mode 100644 index 0000000..ac7d893 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/core_server.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp.py new file mode 100644 index 0000000..5ca8b95 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_update.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_update.py new file mode 100644 index 0000000..66c7a10 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_update.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_utils.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_utils.py new file mode 100644 index 0000000..7b3d34f --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_utils.py @@ -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' \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc new file mode 100644 index 0000000..018363a Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/task_instance.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/task_instance.py new file mode 100644 index 0000000..9e95791 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/task_instance.py @@ -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, + ) diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/dify_workflow_schema.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/dify_workflow_schema.cpython-312.pyc new file mode 100644 index 0000000..ebd42b3 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/dify_workflow_schema.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/logger_config.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/logger_config.cpython-312.pyc new file mode 100644 index 0000000..cdef1ad Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/upload_file.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/upload_file.cpython-312.pyc new file mode 100644 index 0000000..799a0dd Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/upload_file.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/dify_workflow_schema.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/dify_workflow_schema.py new file mode 100644 index 0000000..83c7beb --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/dify_workflow_schema.py @@ -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) diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/logger_config.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/logger_config.py new file mode 100644 index 0000000..f598d1d --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/logger_config.py @@ -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("日志配置测试完成!") \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/tool_translation.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/tool_translation.py new file mode 100644 index 0000000..7361457 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/tool_translation.py @@ -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() diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/translator.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/translator.py new file mode 100644 index 0000000..b0b777b --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/translator.py @@ -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} diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/upload_file.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/upload_file.py new file mode 100644 index 0000000..918b01c --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/upload_file.py @@ -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() # 取消注释并配置后运行 \ No newline at end of file diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__init__.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/__init__.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ae00237 Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/workflow_server.cpython-312.pyc b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/workflow_server.cpython-312.pyc new file mode 100644 index 0000000..472ea9a Binary files /dev/null and b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/workflow_server.cpython-312.pyc differ diff --git a/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/workflow_server.py b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/workflow_server.py new file mode 100644 index 0000000..1971f29 --- /dev/null +++ b/lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/workflow_server.py @@ -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") diff --git a/lzwcai_mcp_api_converter/PKG-INFO b/lzwcai_mcp_api_converter/PKG-INFO new file mode 100644 index 0000000..8609c74 --- /dev/null +++ b/lzwcai_mcp_api_converter/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.4 +Name: lzwcai-mcp-dyntoolapi +Version: 0.1.27 +Summary: 基于FastMCP框架的动态API工具服务器,自动将企业业务API配置转换为MCP协议工具,支持多种传输方式、企业认证和参数验证,为AI助手提供标准化的业务接口访问能力。 +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Requires-Dist: dynaconf>=3.2.11 +Requires-Dist: httpx>=0.28.1 +Requires-Dist: jinja2==3.1.6 +Requires-Dist: mcp[cli]>=1.8.0 +Requires-Dist: requests>=2.31.0 +Requires-Dist: pypinyin>=0.54.0 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO new file mode 100644 index 0000000..5755dba --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.4 +Name: lzwcai-mcp-api-converter +Version: 0.1.30 +Summary: 基于FastMCP框架的动态API工具服务器,自动将企业业务API配置转换为MCP协议工具,支持多种传输方式、企业认证和参数验证,为AI助手提供标准化的业务接口访问能力。 +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Requires-Dist: dynaconf>=3.2.11 +Requires-Dist: httpx>=0.28.1 +Requires-Dist: jinja2==3.1.6 +Requires-Dist: mcp[cli]>=1.8.0 +Requires-Dist: requests>=2.31.0 +Requires-Dist: pypinyin>=0.54.0 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/SOURCES.txt b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/SOURCES.txt new file mode 100644 index 0000000..48815ae --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/SOURCES.txt @@ -0,0 +1,25 @@ +pyproject.toml +setup.cfg +lzwcai_mcp_api_converter/__init__.py +lzwcai_mcp_api_converter.egg-info/PKG-INFO +lzwcai_mcp_api_converter.egg-info/SOURCES.txt +lzwcai_mcp_api_converter.egg-info/dependency_links.txt +lzwcai_mcp_api_converter.egg-info/entry_points.txt +lzwcai_mcp_api_converter.egg-info/requires.txt +lzwcai_mcp_api_converter.egg-info/top_level.txt +lzwcai_mcp_api_converter/src/__init__.py +lzwcai_mcp_api_converter/src/api_config.json +lzwcai_mcp_api_converter/src/create_mcp.py +lzwcai_mcp_api_converter/src/business/__init__.py +lzwcai_mcp_api_converter/src/business/business_util.py +lzwcai_mcp_api_converter/src/business/get_business_api.py +lzwcai_mcp_api_converter/src/core/__init__.py +lzwcai_mcp_api_converter/src/core/api_auth_service.py +lzwcai_mcp_api_converter/src/core/api_base.py +lzwcai_mcp_api_converter/src/core/core_server.py +lzwcai_mcp_api_converter/src/core/get_auth.py +lzwcai_mcp_api_converter/src/core/plugin_base.py +lzwcai_mcp_api_converter/src/util/__init__.py +lzwcai_mcp_api_converter/src/util/api_helper.py +lzwcai_mcp_api_converter/src/util/logger_config.py +lzwcai_mcp_api_converter/src/util/nested_value.py \ No newline at end of file diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/dependency_links.txt b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/entry_points.txt b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/entry_points.txt new file mode 100644 index 0000000..7504d25 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +lzwcai-mcp-api-converter = lzwcai_mcp_api_converter.src.create_mcp:run_main diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/requires.txt b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/requires.txt new file mode 100644 index 0000000..df9559c --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/requires.txt @@ -0,0 +1,6 @@ +dynaconf>=3.2.11 +httpx>=0.28.1 +jinja2==3.1.6 +mcp[cli]>=1.8.0 +requests>=2.31.0 +pypinyin>=0.54.0 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/top_level.txt b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/top_level.txt new file mode 100644 index 0000000..f33e29f --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/top_level.txt @@ -0,0 +1 @@ +lzwcai_mcp_api_converter diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__init__.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..42a2177 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__init__.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..748969b Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/create_mcp.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/create_mcp.cpython-312.pyc new file mode 100644 index 0000000..3e4cf82 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/create_mcp.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/api_config.json b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/api_config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/api_config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__init__.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7f75b3b Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc new file mode 100644 index 0000000..b432793 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc new file mode 100644 index 0000000..0aa2a10 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py new file mode 100644 index 0000000..a8d6629 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py @@ -0,0 +1,712 @@ +""" +业务工具模块 - API参数处理和JSON Schema操作 + +这个模块提供了API参数到JSON Schema转换、参数验证、API配置操作等核心业务功能。 +它是连接业务API配置和MCP工具定义的桥梁。 + +主要功能: +1. API参数列表转换为JSON Schema +2. 参数验证和默认值处理 +3. Schema提示文本生成 +4. API配置数组操作 +5. 必填参数检查 + +核心组件: +- 参数类型常量定义 +- JSON Schema生成器 +- 参数验证器 +- 配置操作工具 + +设计原则: +- 类型安全:严格的类型检查和转换 +- 容错性:完善的异常处理机制 +- 可扩展性:支持新的参数类型和验证规则 +- 标准化:符合JSON Schema规范 + +作者: lzwcai +版本: 1.0.0 +""" + +import json +import copy +from typing import Dict, List, Any, Optional, Union +from ..util.logger_config import get_logger + +# 获取日志器实例 +logger = get_logger(__name__) + + +# ==================== 常量定义 ==================== + +class ParamType: + """ + API参数类型常量 + + 定义业务平台API参数的所有支持类型。 + 这些类型会被映射到对应的JSON Schema类型。 + """ + STRING = "STRING" # 字符串类型 + NUMBER = "NUMBER" # 数字类型(浮点数) + INTEGER = "INTEGER" # 整数类型 + BOOLEAN = "BOOLEAN" # 布尔类型 + ARRAY = "ARRAY" # 数组类型 + OBJECT = "OBJECT" # 对象类型 + + +class JsonSchemaType: + """ + JSON Schema类型常量 + + 定义JSON Schema规范中的标准类型。 + 用于将业务参数类型转换为标准Schema类型。 + """ + STRING = "string" # 字符串 + NUMBER = "number" # 数字 + BOOLEAN = "boolean" # 布尔值 + ARRAY = "array" # 数组 + OBJECT = "object" # 对象 + + +class RequestType: + """ + 请求类型常量 + + 定义API参数在HTTP请求中的位置类型。 + 用于将参数正确分组到请求的不同部分。 + """ + HEADER = "header" # 请求头参数 + QUERY = "query" # 查询参数(URL参数) + BODY = "body" # 请求体参数 + LZWCAI_CONFIG = "lzwcaiConfig" # lzwcaiConfig参数(新的用户ID存储位置) + + +# ==================== 类型映射配置 ==================== + +# 参数类型映射表:业务参数类型 -> JSON Schema类型 +PARAM_TYPE_MAPPING = { + ParamType.STRING: JsonSchemaType.STRING, # 字符串 -> string + ParamType.NUMBER: JsonSchemaType.NUMBER, # 数字 -> number + ParamType.INTEGER: JsonSchemaType.NUMBER, # 整数 -> number(JSON Schema中整数也是number) + ParamType.BOOLEAN: JsonSchemaType.BOOLEAN, # 布尔 -> boolean + ParamType.ARRAY: JsonSchemaType.ARRAY, # 数组 -> array + ParamType.OBJECT: JsonSchemaType.OBJECT, # 对象 -> object +} + +# ==================== 默认参数配置 ==================== + +# 默认用户ID参数配置 +# 这个参数会自动添加到所有API的Schema中,用于标识当前用户 +DEFAULT_USER_ID_PARAM = { + "paramName": "userId", # 参数名称 + "paramType": ParamType.STRING, # 参数类型:字符串 + "paramPrompts": "当前与您对话的用户信息的用户ID", # 参数描述 + "requestType": RequestType.LZWCAI_CONFIG, # 请求类型:lzwcaiConfig类型 + "required": 1, # 必填参数 +} + + +# ==================== 核心函数 ==================== + +def generate_json_schema(api_params: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + API参数列表转JSON Schema格式 + + 这是模块的核心函数,负责将业务平台的API参数列表转换为符合JSON Schema规范的对象。 + 生成的Schema用于MCP工具的参数定义和验证。 + + 转换流程: + 1. 验证输入参数类型 + 2. 过滤掉header类型的参数(header参数单独处理) + 3. 添加默认的userId参数(存储在lzwcaiConfig中) + 4. 按请求类型分组参数(query, body, lzwcaiConfig等) + 5. 为每个分组创建Schema属性 + 6. 清理空的required列表 + + Schema结构: + { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": {...}, + "required": [...] + }, + "body": { + "type": "object", + "properties": {...}, + "required": [...] + }, + "lzwcaiConfig": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "description": "当前与您对话的用户信息的用户ID" + } + }, + "required": ["userId"] + } + }, + "required": ["query", "body", "lzwcaiConfig"] // 只包含有参数的分组 + } + + 参数: + api_params: API参数对象列表,每个对象包含: + - paramName: 参数名称 + - paramType: 参数类型(STRING, NUMBER等) + - paramPrompts: 参数描述 + - requestType: 请求类型(header, query, body等) + - required: 是否必填(1为必填,0为可选) + - defaultValue: 默认值(可选) + + 返回: + dict: 符合JSON Schema规范的对象 + + 异常处理: + TypeError: 如果api_params不是列表类型 + + 设计考虑: + - header参数被过滤掉,因为它们在HTTP请求中单独处理 + - 自动添加userId参数到lzwcaiConfig分组,确保所有API都能获取用户信息 + - 按请求类型分组,便于后续的参数处理和验证 + - 清理空的required列表,保持Schema的简洁性 + """ + # 参数类型验证 + if not isinstance(api_params, list): + raise TypeError("api_params must be a list") + + logger.debug(f"生成JSON Schema,参数数量: {len(api_params)}") + + # 创建基础Schema结构 + schema = { + "type": JsonSchemaType.OBJECT, # 根类型为对象 + "properties": {}, # 属性定义 + "required": [] # 必填字段列表 + } + + # 过滤参数并添加默认userId参数 + # header参数在HTTP请求中单独处理,不包含在Schema中 + filtered_params = [ + param for param in api_params + if param.get("requestType") != RequestType.HEADER + ] + + # 添加默认的userId参数到lzwcaiConfig分组,确保所有API都能获取用户信息 + filtered_params.append(DEFAULT_USER_ID_PARAM) + + logger.debug(f"过滤后参数数量: {len(filtered_params)}") + + # 按请求类型分组参数 + param_groups = _group_parameters_by_type(filtered_params) + logger.debug(f"参数分组: {list(param_groups.keys())}") + + # 为每个请求类型创建Schema属性 + for req_type, params in param_groups.items(): + logger.debug(f"处理参数组 {req_type},包含 {len(params)} 个参数") + _add_request_type_to_schema(schema, req_type, params) + + # 清理空的required列表,保持Schema简洁 + _cleanup_empty_required_lists(schema) + + logger.debug("JSON Schema生成完成") + return schema + + +# ==================== 辅助函数 ==================== + +def _group_parameters_by_type( + params: List[Dict[str, Any]], +) -> Dict[str, List[Dict[str, Any]]]: + """ + 按请求类型分组参数 + + 将参数列表按照requestType字段进行分组,便于后续按类型处理。 + 如果参数没有指定requestType,默认归类为query类型。 + + 参数: + params: 参数字典列表 + + 返回: + Dict[str, List[Dict[str, Any]]]: 按类型分组的参数字典 + 格式: {"query": [...], "body": [...], "lzwcaiConfig": [...]} + """ + param_groups = {} + for param in params: + # 获取请求类型,默认为query + req_type = param.get("requestType", RequestType.QUERY) + + # 记录参数名和类型,便于调试 + param_name = param.get("paramName", "未命名参数") + logger.debug(f"参数 '{param_name}' 归类为 {req_type} 类型") + + # 初始化分组 + if req_type not in param_groups: + param_groups[req_type] = [] + logger.debug(f"创建新的参数分组: {req_type}") + + # 添加参数到对应分组 + param_groups[req_type].append(param) + + logger.debug(f"参数分组完成,共 {len(param_groups)} 个分组") + return param_groups + + +def _add_request_type_to_schema( + schema: Dict[str, Any], req_type: str, params: List[Dict[str, Any]] +) -> None: + """ + 向Schema添加请求类型分组 + + 为指定的请求类型创建Schema属性,并处理该类型下的所有参数。 + 每个请求类型都会成为根Schema的一个属性。 + + 参数: + schema: 根Schema对象 + req_type: 请求类型(如"query", "body", "lzwcaiConfig") + params: 该类型下的参数列表 + """ + # 如果该请求类型还没有在Schema中定义,创建它 + if req_type not in schema["properties"]: + schema["properties"][req_type] = { + "type": JsonSchemaType.OBJECT, # 每个请求类型都是对象类型 + "properties": {}, # 该类型下的参数定义 + "required": [], # 该类型下的必填参数 + } + + # 如果该类型有参数,将其标记为根级别的必填字段 + if params: + schema["required"].append(req_type) + + # 处理该类型下的每个参数 + for param in params: + _add_parameter_to_schema(schema["properties"][req_type], param) + + +def _add_parameter_to_schema( + type_schema: Dict[str, Any], param: Dict[str, Any] +) -> None: + """ + 向类型Schema添加单个参数 + + 将单个参数的定义添加到指定类型的Schema中,包括参数类型、描述、 + 默认值、示例等信息。 + + 参数处理: + 1. 提取参数基本信息(名称、类型、描述) + 2. 处理默认值(添加到描述中并设置default字段) + 3. 映射参数类型到JSON Schema类型 + 4. 创建参数定义对象 + 5. 添加到Schema的properties中 + 6. 处理必填参数标记 + + 参数: + type_schema: 类型Schema对象(如query、body的Schema) + param: 参数定义字典 + """ + # 获取参数名称,如果没有名称则跳过 + param_name = param.get("paramName") + if not param_name: + logger.warning(f"参数缺少名称,跳过: {param}") + return + + # 获取参数类型和描述 + param_type = param.get("paramType", ParamType.STRING) + param_desc = param.get("paramPrompts", "") + is_required = param.get("required") == 1 + + logger.debug(f"添加参数: {param_name}, 类型: {param_type}, 必填: {is_required}") + + # 如果有默认值,添加到描述中 + if param.get("defaultValue") is not None: + param_desc += f"(默认值为{param['defaultValue']})" + logger.debug(f"参数 {param_name} 有默认值: {param['defaultValue']}") + + # 将业务参数类型映射到JSON Schema类型 + json_type = PARAM_TYPE_MAPPING.get(param_type, JsonSchemaType.STRING) + if param_type not in PARAM_TYPE_MAPPING: + logger.warning(f"未知的参数类型 {param_type},使用默认类型 string") + + # 创建参数定义对象 + param_def = { + "type": json_type, # JSON Schema类型 + "description": param_desc, # 参数描述 + } + + # 添加可选字段 + if param.get("defaultValue") is not None: + param_def["default"] = param["defaultValue"] # 默认值 + if param.get("example") is not None: + param_def["example"] = param["example"] # 示例值 + + # 添加到类型Schema的properties中 + type_schema["properties"][param_name] = param_def + + # 如果是必填参数,添加到required列表中 + if is_required: + type_schema["required"].append(param_name) + logger.debug(f"参数 {param_name} 标记为必填") + + +def _cleanup_empty_required_lists(schema: Dict[str, Any]) -> None: + """ + 清理Schema中的空required列表 + + 移除Schema中所有空的required数组,保持Schema的简洁性。 + 这包括嵌套属性中的required列表和顶级的required列表。 + + 清理规则: + 1. 遍历所有请求类型的Schema + 2. 如果某个类型的required列表为空,删除该字段 + 3. 如果顶级required列表为空,删除该字段 + + 参数: + schema: 要清理的Schema对象 + """ + # 清理嵌套属性中的空required列表 + for req_type in list(schema["properties"].keys()): + type_schema = schema["properties"][req_type] + if not type_schema.get("required"): + # 如果required列表为空,删除该字段 + type_schema.pop("required", None) + + # 清理顶级的空required列表 + if not schema.get("required"): + schema.pop("required", None) + + +def create_structured_data( + schema: Dict[str, Any], params: Dict[str, Any] +) -> Dict[str, Any]: + """ + Generate data structure based on schema and input parameters. + + Automatically matches input parameters to schema fields and creates + a properly structured data object. + + Args: + schema: JSON schema object defining the data structure + params: Parameter values to fill + + Returns: + dict: Structured data object conforming to schema + + Raises: + TypeError: If schema or params are not dictionaries + """ + if not isinstance(schema, dict): + raise TypeError("schema must be a dictionary") + if not isinstance(params, dict): + raise TypeError("params must be a dictionary") + + result = {} + + # Process each top-level schema property + for field_name, field_schema in schema.get("properties", {}).items(): + result[field_name] = {} + field_properties = field_schema.get("properties", {}) + + # Find matching parameters for this field + matched_params = { + param_name: param_value + for param_name, param_value in params.items() + if param_name in field_properties + } + + # Only include field if there are matching parameters + if matched_params: + result[field_name] = matched_params + + return result + + +def generate_schema_prompt(schema: Dict[str, Any]) -> str: + """ + Generate descriptive prompt from JSON Schema for LLM guidance. + + Args: + schema: JSON Schema object returned by generate_json_schema + + Returns: + str: Structured prompt text for guiding LLM parameter generation + + Raises: + TypeError: If schema is not a dictionary + """ + if not isinstance(schema, dict): + raise TypeError("schema must be a dictionary") + + prompt_parts = ["当前工具所需参数:\n"] + + # Process each parameter group + for group_name, group_schema in schema.get("properties", {}).items(): + prompt_parts.append(f"## {group_name} 参数组:") + + required_params = set(group_schema.get("required", [])) + + # Process each parameter in the group + for param_name, param_info in group_schema.get("properties", {}).items(): + param_type = param_info.get("type", JsonSchemaType.STRING) + param_desc = param_info.get("description", "") + required_mark = "(必填)" if param_name in required_params else "(可选)" + + prompt_parts.append( + f"- {param_name}{required_mark}: {param_desc}, 类型: {param_type}" + ) + + prompt_parts.append("") + + # Add output format guidance + prompt_parts.extend( + [ + "参数格式要求:JSON对象,包含所有必填字段。", + "示例格式:", + "```json", + "{", + ] + ) + + for group_name in schema.get("properties", {}): + prompt_parts.append(f' "{group_name}": {{') + prompt_parts.append(" // 相关参数") + prompt_parts.append(" },") + + prompt_parts.extend(["}", "```"]) + + return "\n".join(prompt_parts) + + +def _remove_property_from_schema(schema: Dict[str, Any], property_name: str) -> None: + """ + Remove a property from schema (helper function to reduce code duplication). + + Args: + schema: Schema object to modify + property_name: Name of property to remove + """ + # Remove from properties + if isinstance(schema.get("properties"), dict): + schema["properties"].pop(property_name, None) + + # Remove from required list + if isinstance(schema.get("required"), list): + try: + schema["required"].remove(property_name) + except ValueError: + pass # Property not in required list, ignore + + # Remove empty required list + if not schema["required"]: + schema.pop("required", None) + + +def remove_property_from_api_item( + api_item: Dict[str, Any], property_name: str +) -> Dict[str, Any]: + """ + Remove specified property from a single API object. + + Args: + api_item: API object (single element from generate_api_array result) + property_name: Name of property to remove + + Returns: + dict: Processed API object with property removed + + Raises: + ValueError: When input parameters are invalid + TypeError: When input types are incorrect + """ + if not isinstance(api_item, dict) or not api_item: + raise ValueError("api_item must be a non-empty dictionary") + + if not isinstance(property_name, str) or not property_name.strip(): + raise ValueError("property_name must be a non-empty string") + + # Create deep copy to avoid modifying original + new_api = copy.deepcopy(api_item) + + if "schema" in new_api and isinstance(new_api["schema"], dict): + _remove_property_from_schema(new_api["schema"], property_name) + + return new_api + + +def remove_property_from_api_array( + api_array: List[Dict[str, Any]], property_name: str +) -> List[Dict[str, Any]]: + """ + Remove specified property from all API objects in array. + + Args: + api_array: API array returned by generate_api_array + property_name: Name of property to remove + + Returns: + list: Processed API array with property removed from all items + + Raises: + ValueError: When input parameters are invalid + TypeError: When input types are incorrect + """ + if not isinstance(api_array, list) or not api_array: + raise ValueError("api_array must be a non-empty list") + + if not isinstance(property_name, str) or not property_name.strip(): + raise ValueError("property_name must be a non-empty string") + + result = [] + + for api_item in api_array: + try: + processed_item = remove_property_from_api_item(api_item, property_name) + result.append(processed_item) + except Exception as e: + result.append(copy.deepcopy(api_item)) # Keep original if processing fails + + return result + + +def fill_default_values_by_schema( + schema: Dict[str, Any], arguments: Dict[str, Any] +) -> Dict[str, Any]: + """ + Auto-fill default values in arguments based on JSON Schema. + + Only fills missing parameters or those with None/empty string values. + + Args: + schema: JSON Schema object with grouped structure (body/query/lzwcaiConfig) + arguments: User input parameter dictionary + + Returns: + dict: Parameter dictionary with default values filled + + Raises: + TypeError: When input types are incorrect + """ + if not isinstance(schema, dict): + raise TypeError("schema must be a dictionary") + + if not isinstance(arguments, dict): + arguments = {} + else: + arguments = copy.deepcopy(arguments) + + for group_name, group_schema in schema.get("properties", {}).items(): + if not isinstance(arguments.get(group_name), dict): + arguments[group_name] = {} + + for param_name, param_schema in group_schema.get("properties", {}).items(): + default_value = param_schema.get("default") + current_value = arguments[group_name].get(param_name) + + # Fill default only if parameter is missing, None, or empty string + if ( + current_value is None or current_value == "" + ) and default_value is not None: + arguments[group_name][param_name] = default_value + + return arguments + + +def generate_api_array(api_params: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Generate API array with JSON Schema for each API. + + Args: + api_params: List of API parameter definitions + + Returns: + list: API array with schema attached to each item + + Raises: + TypeError: If api_params is not a list + """ + if not isinstance(api_params, list): + raise TypeError("api_params must be a list") + + api_array = [] + for param in api_params: + try: + schema = generate_json_schema(param.get("parameters", [])) + api_array.append({**param, "schema": schema}) + except Exception as e: + logger.error(f"Error processing config: {str(e)}") + api_array.append(param) # Add without schema if generation fails + + return api_array + + +def check_required_arguments( + schema: Dict[str, Any], arguments: Dict[str, Any] +) -> List[str]: + """ + Validate that arguments contain all required parameters from schema. + + Args: + schema: JSON Schema object with grouped structure (body/query/lzwcaiConfig) + arguments: User input parameter dictionary + + Returns: + list: Missing required parameters with group names, e.g., ['body.username', 'lzwcaiConfig.userId'] + + Raises: + TypeError: When input types are incorrect + """ + if not isinstance(schema, dict): + raise TypeError("schema must be a dictionary") + + if not isinstance(arguments, dict): + arguments = {} + + missing_params = [] + + for group_name, group_schema in schema.get("properties", {}).items(): + group_args = arguments.get(group_name, {}) + required_params = group_schema.get("required", []) + + for param_name in required_params: + param_value = group_args.get(param_name) if group_args else None + + # Consider missing if not provided, None, or empty string + if param_value is None or param_value == "": + # Try to get parameter description + description = "" + try: + param_properties = group_schema.get("properties", {}) + param_info = param_properties.get(param_name, {}) + description = param_info.get("description", "") + except Exception: + pass + + # Format missing parameter name + if description: + missing_params.append(f"{group_name}.{param_name}({description})") + else: + missing_params.append(f"{group_name}.{param_name}") + + return missing_params + + +if __name__ == "__main__": + # Example usage and testing + try: + with open( + "E:/yh-ai/project/lzwcai-szyg/lzwcai-demp-tool-server/src/" + "lzwcai_demp_tool_server_business_to_mcp/mcp_generator/src/parameters.json", + "r", + encoding="utf-8", + ) as f: + api_params = json.load(f) + + api_array = generate_api_array(api_params) + result = remove_property_from_api_array(api_array, "userId") + + # Write results to JSON file + with open("schema1.json", "w", encoding="utf-8") as f: + json.dump(result, f, ensure_ascii=False, indent=4) + + except FileNotFoundError: + logger.error(f"Warning: Test data file not found. Skipping example execution.") + except Exception as e: + logger.error(f"Error in example execution: {str(e)}") diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py new file mode 100644 index 0000000..04d9213 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py @@ -0,0 +1,204 @@ +""" +Business API configuration utility functions. + +This module provides utilities for fetching and processing business API configurations. +""" + +import json +import os +import requests +from typing import Dict, List, Any +from ..util.logger_config import get_logger + +# 配置日志 +logger = get_logger(__name__) + + + + +def get_business_api_details(api_ids: List[int], auth_token: str = None) -> List[Dict[str, Any]]: + """ + 获取业务平台API详情 + + 调用业务平台接口获取指定API ID列表的详细信息 + + Args: + api_ids: API ID列表,例如 [1925128743899111425, 1925128744524062721] + auth_token: 认证token,如果不提供则使用默认token + + Returns: + List[Dict[str, Any]]: API详情列表,返回接口响应中的data字段内容 + + Raises: + requests.RequestException: 当网络请求失败时抛出 + ValueError: 当响应格式不正确或返回错误时抛出 + + Example: + >>> api_ids = [1925128743899111425, 1925128744524062721] + >>> details = get_business_api_details(api_ids) + >>> print(len(details)) + 2 + """ + if not isinstance(api_ids, list) or not api_ids: + raise ValueError("api_ids must be a non-empty list") + + # 默认认证token + default_token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImM3OGU0M2NlLTJhZjQtNGRjYy1iMWE1LTU3YjM5YTdkNTA1OSJ9.5f1lSJJdLUunZIwCfneT1DiagGN4jD-QCnFCffWmrnvEcLfpuSMWRpY7fF-6H3yZ2N5ICZ4ZQN6cx7iqwF6jKw" + token = auth_token or default_token + + # 接口URL - 支持环境变量配置 + # 默认URL + default_url = "http://lzwcai-demp-corp-manager:8086/system/mcpServer/bizSys/api/getByIds" + # 从环境变量获取URL,如果没有设置则使用默认URL + url = os.getenv("lzwcai_mcp_dyntoolapi_auth_url", default_url) + + # 请求头 + headers = { + "Authorization": f"Bearer {token}", + # "User-Agent": "Apifox/1.0.0 (https://apifox.com)", + "Content-Type": "application/json", + # "Accept": "*/*", + # "Host": "192.168.2.236:8088", + "Connection": "keep-alive" + } + + try: + # 发送POST请求 + response = requests.post(url, headers=headers, json=api_ids, timeout=30) + response.raise_for_status() # 检查HTTP状态码 + + # 解析响应JSON + response_data = response.json() + + # 检查响应格式 + if not isinstance(response_data, dict): + raise ValueError("响应格式不正确:期望JSON对象") + + # 检查业务状态码 + code = response_data.get("code") + if code != 200: + msg = response_data.get("msg", "未知错误") + raise ValueError(f"业务接口返回错误:code={code}, msg={msg}") + + # 获取data字段 + data = response_data.get("data", []) + if not isinstance(data, list): + logger.warning("响应中的data字段不是列表类型,将转换为列表") + data = [data] if data is not None else [] + + logger.info(f"成功获取 {len(data)} 个API详情") + return data + + except requests.exceptions.Timeout: + raise requests.RequestException("请求超时:接口响应时间过长") + except requests.exceptions.ConnectionError: + raise requests.RequestException("连接错误:无法连接到业务平台服务器") + except requests.exceptions.HTTPError as e: + raise requests.RequestException(f"HTTP错误:{e}") + except json.JSONDecodeError: + raise ValueError("响应格式错误:无法解析JSON数据") + except Exception as e: + logger.error(f"获取API详情时发生未知错误:{str(e)}") + raise + + +def map_api_details_to_config(api_details: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + 将API详情数据映射为api_config.json格式 + + Args: + api_details: get_business_api_details方法返回的API详情列表 + + Returns: + Dict[str, Any]: 符合api_config.json格式的配置对象 + + Raises: + ValueError: 当输入数据格式不正确时抛出 + + Example: + >>> api_details = get_business_api_details([1925128743899111425]) + >>> config = map_api_details_to_config(api_details) + >>> print(config["serverName"]) + lzwcai_mcp_api_converter + """ + if not isinstance(api_details, list): + raise ValueError("api_details must be a list") + + if not api_details: + raise ValueError("api_details cannot be empty") + + # 获取第一个API的domainUrl作为全局domainUrl + domain_url = "" + if api_details and isinstance(api_details[0], dict): + domain_url = api_details[0].get("domainUrl", "") + + # 收集所有businessPrompts用于生成description + business_prompts = [] + for api in api_details: + if isinstance(api, dict) and api.get("businessPrompts"): + business_prompts.append(api["businessPrompts"]) + + # 生成description + description = "、".join(business_prompts) if business_prompts else "业务API集合" + + # 构建配置对象 + config = { + "serverName": "lzwcai_mcp_api_converter", + "description": description, + "domainUrl": domain_url, + "packageName": "lzwcai-mcp-dyntoolapi", + "version": "1.0.0", + "apiConfig": api_details # 直接使用原始API详情数据 + } + + logger.info(f"成功映射 {len(api_details)} 个API到配置格式") + logger.info(f"服务名称: {config['serverName']}") + logger.info(f"域名URL: {config['domainUrl']}") + logger.info(f"描述: {config['description']}") + + return config + + +def get_business_api_config(api_ids: List[int], auth_token: str = None) -> Dict[str, Any]: + """ + 一步到位获取业务平台API配置 + + 传入API ID列表,直接返回处理好的api_config.json格式配置 + + Args: + api_ids: API ID列表,例如 [1925128743899111425, 1925128744524062721] + auth_token: 认证token,如果不提供则使用默认token + + Returns: + Dict[str, Any]: 符合api_config.json格式的完整配置对象 + + Raises: + requests.RequestException: 当网络请求失败时抛出 + ValueError: 当响应格式不正确或返回错误时抛出 + + Example: + >>> api_ids = [1925128743899111425, 1925128744524062721] + >>> config = get_business_api_config(api_ids) + >>> print(config["serverName"]) + lzwcai_mcp_api_converter + >>> print(len(config["apiConfig"])) + 2 + """ + try: + # 步骤1: 获取API详情 + logger.info(f"开始获取 {len(api_ids)} 个API的详情...") + api_details = get_business_api_details(api_ids, auth_token) + + # 步骤2: 映射为配置格式 + logger.info("开始映射为配置格式...") + config = map_api_details_to_config(api_details) + + logger.info(f"[SUCCESS] 成功生成API配置!包含 {len(config['apiConfig'])} 个API") + return config + + except Exception as e: + logger.error(f"获取业务API配置时发生错误: {str(e)}") + raise + + + diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__init__.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c6c70a9 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc new file mode 100644 index 0000000..dadea04 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc new file mode 100644 index 0000000..935f9cd Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc new file mode 100644 index 0000000..8c97464 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc new file mode 100644 index 0000000..444a991 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/plugin_base.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/plugin_base.cpython-312.pyc new file mode 100644 index 0000000..3a0e9fc Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/plugin_base.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_auth_service.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_auth_service.py new file mode 100644 index 0000000..48c08b2 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_auth_service.py @@ -0,0 +1,966 @@ +""" +API认证服务模块 + +提供用户认证、Token管理、企业认证等功能 +""" + +import asyncio +import json +import os +import random +import sys +from typing import Any, Dict, List, Optional, Tuple, Union + +# 添加项目根目录到 Python 路径 +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +sys.path.append(project_root) + +from ..business.business_util import create_structured_data, generate_api_array +from ..util.nested_value import get_nested_value +from ..util.logger_config import get_logger +from .get_auth import get_auth_data + +# 获取日志器 +logger = get_logger(__name__) + + +class AuthError(Exception): + """认证相关异常""" + + def __init__(self, message: str, error_code: Optional[str] = None): + super().__init__(message) + self.error_code = error_code + + +class AuthConfig: + """认证配置类""" + + # Token相关配置 + TOKEN_PREFIX = "lzwc" + TOKEN_SUFFIX = "token" + + # 重试配置 + DEFAULT_MAX_ATTEMPTS = 3 + DEFAULT_BASE_DELAY = 1 + DEFAULT_MAX_DELAY = 60 + + +class ParameterExtractor: + """参数提取器""" + + @staticmethod + def extract_param_defaults(param_list: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + 将参数列表转换为 {paramName: defaultValue} 的字典 + + Args: + param_list: 参数字典列表,每个元素包含 paramName 和 defaultValue + + Returns: + 以 paramName 为 key,defaultValue 为 value 的字典 + """ + if not isinstance(param_list, list): + logger.warning(f"参数列表类型错误,期望list,实际{type(param_list)}") + return {} + + logger.debug(f"提取参数默认值,参数数量: {len(param_list)}") + result = {} + + for item in param_list: + if not isinstance(item, dict): + logger.warning(f"参数项类型错误,跳过: {item}") + continue + + param_name = item.get("paramName") + if param_name: + default_value = item.get("defaultValue") + result[param_name] = default_value + logger.debug(f"参数 {param_name}: {default_value}") + + logger.debug(f"提取完成,共 {len(result)} 个参数") + return result + + +class ApiRetryExecutor: + """API重试执行器""" + + def __init__( + self, + max_attempts: int = AuthConfig.DEFAULT_MAX_ATTEMPTS, + base_delay: int = AuthConfig.DEFAULT_BASE_DELAY, + max_delay: int = AuthConfig.DEFAULT_MAX_DELAY, + ): + self.max_attempts = max_attempts + self.base_delay = base_delay + self.max_delay = max_delay + + async def execute_with_retry( + self, + api_config: Dict[str, Any], + user_params: Dict[str, Any], + need_auth: bool = False, + ) -> Optional[Dict[str, Any]]: + """ + 使用指数退避策略的API调用重试 + + Args: + api_config: API配置信息 + user_params: 用户提供的参数 + need_auth: 是否需要鉴权 + + Returns: + API调用响应 + + Raises: + AuthError: 认证相关错误 + """ + from .core_server import call_api + + attempt = 0 + last_error = None + + while attempt < self.max_attempts: + attempt += 1 + try: + logger.info(f"尝试调用API,第 {attempt} 次") + + # 准备API调用数据 + api_array = generate_api_array([api_config]) + call_api_data = create_structured_data( + api_array[0]["schema"], user_params + ) + + # 执行API调用 + api_res = await call_api(api_config, call_api_data, need_auth=need_auth) + + # 检查响应是否包含错误 + if api_res and "error" not in api_res: + logger.info(f"API调用成功,第 {attempt} 次尝试") + return api_res + + # 记录错误信息 + error_msg = api_res.get("error", "未知错误") if api_res else "响应为空" + logger.warning(f"API调用返回错误: {error_msg}") + last_error = error_msg + + # 如果还有重试机会,等待后重试 + if attempt < self.max_attempts: + await self._wait_for_retry(attempt) + + except Exception as e: + error_msg = str(e) + logger.error(f"API调用发生异常: {error_msg}") + last_error = error_msg + + if attempt < self.max_attempts: + await self._wait_for_retry(attempt) + else: + logger.error( + f"API调用失败,已达到最大重试次数 ({self.max_attempts})" + ) + raise AuthError(f"API调用失败: {error_msg}") + + # 所有重试都失败了 + raise AuthError( + f"API调用失败,重试{self.max_attempts}次后仍然失败: {last_error}" + ) + + async def _wait_for_retry(self, attempt: int) -> None: + """等待重试的延迟逻辑""" + # 计算延迟时间(指数退避策略) + delay = min(self.base_delay * (2 ** (attempt - 1)), self.max_delay) + # 添加随机抖动(0.5-1.5倍)避免同时重试 + jitter = 0.5 + random.random() + sleep_time = delay * jitter + + logger.info(f"等待 {sleep_time:.2f} 秒后进行第 {attempt + 1} 次重试") + await asyncio.sleep(sleep_time) + + +class EnvironmentManager: + """环境变量管理器,提供环境变量的增删改查功能,支持持久化存储""" + + _env_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../", ".env_lzwcai_mcp_api_converter")) + + @staticmethod + def _load_persistent_tokens() -> Dict[str, str]: + """从.env文件加载持久化的token""" + tokens = {} + try: + if os.path.exists(EnvironmentManager._env_file): + with open(EnvironmentManager._env_file, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and '=' in line and not line.startswith('#'): + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + if key: # 确保key不为空 + tokens[key] = value + except Exception as e: + logger.error(f"加载.env文件失败: {e}") + return tokens + + @staticmethod + def _save_persistent_tokens(tokens: Dict[str, str]) -> bool: + """保存token到.env文件""" + try: + os.makedirs(os.path.dirname(EnvironmentManager._env_file), exist_ok=True) + with open(EnvironmentManager._env_file, 'w', encoding='utf-8') as f: + for key, value in tokens.items(): + f.write(f"{key}={value}\n") + return True + except Exception as e: + logger.error(f"保存.env文件失败: {e}") + return False + + @staticmethod + def get(key: str, default: Optional[str] = None) -> Optional[str]: + """ + 获取环境变量值,优先从进程环境变量获取,然后从持久化文件获取 + + Args: + key: 环境变量名 + default: 默认值 + + Returns: + 环境变量值或默认值 + """ + # 先从进程环境变量获取 + value = os.environ.get(key) + if value is not None: + return value + + # 从持久化文件获取 + if key.endswith('token'): # 只对token类型的环境变量使用持久化 + persistent_tokens = EnvironmentManager._load_persistent_tokens() + value = persistent_tokens.get(key) + if value is not None: + # 同时设置到进程环境变量中 + os.environ[key] = value + return value + + return default + + @staticmethod + def set(key: str, value: str) -> bool: + """ + 设置环境变量,同时持久化token类型的变量 + + Args: + key: 环境变量名 + value: 环境变量值 + + Returns: + 设置是否成功 + """ + try: + # 设置到进程环境变量 + os.environ[key] = str(value) + + # 如果是token类型,同时持久化到文件 + if key.endswith('token'): + persistent_tokens = EnvironmentManager._load_persistent_tokens() + persistent_tokens[key] = str(value) + if not EnvironmentManager._save_persistent_tokens(persistent_tokens): + logger.warning(f"持久化token失败,但进程环境变量已设置: {key}") + else: + logger.debug(f"成功持久化token: {key}") + + logger.debug(f"成功设置环境变量: {key}") + return True + except Exception as e: + logger.error(f"设置环境变量失败: {key}, 错误: {e}") + return False + + @staticmethod + def exists(key: str) -> bool: + """ + 检查环境变量是否存在 + + Args: + key: 环境变量名 + + Returns: + 是否存在 + """ + # 先检查进程环境变量 + if key in os.environ: + return True + + # 检查持久化文件 + if key.endswith('token'): + persistent_tokens = EnvironmentManager._load_persistent_tokens() + return key in persistent_tokens + + return False + + @staticmethod + def delete(key: str) -> bool: + """ + 删除环境变量,同时删除持久化的token + + Args: + key: 环境变量名 + + Returns: + 删除是否成功 + """ + try: + # 从进程环境变量删除 + if key in os.environ: + del os.environ[key] + + # 如果是token类型,同时从持久化文件删除 + if key.endswith('token'): + persistent_tokens = EnvironmentManager._load_persistent_tokens() + if key in persistent_tokens: + del persistent_tokens[key] + EnvironmentManager._save_persistent_tokens(persistent_tokens) + logger.debug(f"成功删除持久化token: {key}") + + logger.debug(f"成功删除环境变量: {key}") + return True + except Exception as e: + logger.error(f"删除环境变量失败: {key}, 错误: {e}") + return False + + +class TokenManager: + """Token管理器""" + + def __init__(self): + self.logger = get_logger("TokenManager") + + def generate_token_name(self, user_id: str, biz_sys_id: str) -> str: + """ + 生成Token名称 + + Args: + user_id: 用户ID + biz_sys_id: 业务系统ID + + Returns: + Token名称 + """ + return ( + f"{AuthConfig.TOKEN_PREFIX}{user_id}{biz_sys_id}{AuthConfig.TOKEN_SUFFIX}" + ) + + def check_token_exists( + self, token_name: str + ) -> Tuple[bool, Optional[Union[str, Dict[str, Any]]]]: + """ + 检查环境变量中是否存在Token,并返回反序列化后的值 + + Args: + token_name: Token名称 + + Returns: + (是否存在, Token值) + """ + token_value = EnvironmentManager.get(token_name) + if token_value is None: + return False, None + + # 尝试反序列化Token值 + deserialized_value = self._deserialize_token_value(token_value, token_name) + return True, deserialized_value + + def _deserialize_token_value( + self, token_value: str, token_name: str + ) -> Union[str, Dict[str, Any]]: + """反序列化Token值""" + try: + # 尝试将JSON字符串反序列化为原始结构(字典) + return json.loads(token_value) + except json.JSONDecodeError: + self.logger.debug(f"Token不是JSON格式,保留原始字符串: {token_name}") + + # 尝试处理字符串表示的字典,如 "{'key': 'value'}" + if token_value.startswith("{") and token_value.endswith("}"): + try: + import ast + + return ast.literal_eval(token_value) + except (ValueError, SyntaxError): + self.logger.debug(f"无法解析字典字符串: {token_value}") + + # 返回原始字符串 + return token_value + + def store_token(self, token_name: str, token_value: Any) -> bool: + """ + 存储Token到环境变量,自动序列化复杂结构 + + Args: + token_name: Token名称 + token_value: Token值 + + Returns: + 存储是否成功 + """ + if token_value is None: + self.logger.warning(f"Token值为None,不进行存储: {token_name}") + return False + + # 序列化Token值 + serialized_value = self._serialize_token_value(token_value, token_name) + if serialized_value is None: + return False + + # 存储到环境变量 + success = EnvironmentManager.set(token_name, serialized_value) + if success: + self.logger.info(f"成功将Token存储到环境变量: {token_name}") + else: + self.logger.error(f"存储Token到环境变量失败: {token_name}") + + return success + + def _serialize_token_value( + self, token_value: Any, token_name: str + ) -> Optional[str]: + """序列化Token值""" + try: + if isinstance(token_value, (dict, list)): + return json.dumps(token_value) + elif isinstance(token_value, bytes): + return token_value.decode("utf-8") + elif isinstance(token_value, (int, float, str)): + return str(token_value) + else: + self.logger.error( + f"不支持的Token类型: {type(token_value)},不进行存储: {token_name}" + ) + return None + except Exception as e: + self.logger.error(f"序列化Token时出错: {str(e)}") + return None + + +class CompanyAuthClient: + """企业认证客户端 - 现在使用get_auth_data方法替代HTTP API调用""" + + def __init__(self): + self.logger = get_logger("CompanyAuthClient") + + async def get_auth_info( + self, user_id: str, biz_sys_id: str + ) -> Tuple[Optional[int], Optional[Dict[str, Any]]]: + """ + 获取鉴权类型和认证数据 + + Args: + user_id: 用户ID + biz_sys_id: 业务系统ID + + Returns: + (鉴权类型, 认证数据) + """ + try: + self.logger.debug(f"使用get_auth_data获取认证信息: user_id={user_id}, biz_sys_id={biz_sys_id}") + + # 使用get_auth_data方法获取认证数据 + result = get_auth_data(user_id, biz_sys_id) + + if not result: + self.logger.error("get_auth_data返回空结果") + return None, None + + return self._parse_auth_response(result, user_id, biz_sys_id) + + except Exception as e: + self.logger.error(f"获取鉴权类型失败: {str(e)}") + return None, None + + def _parse_auth_response( + self, result: Dict[str, Any], user_id: str, biz_sys_id: str + ) -> Tuple[Optional[int], Optional[Dict[str, Any]]]: + """解析认证响应""" + if result.get("code") != 200: + error_msg = result.get("msg", "未知错误") + self.logger.error(f"获取鉴权类型失败: {error_msg}") + return None, None + + auth_data = result.get("data", {}) + auth_type = auth_data.get("authType") + + # 将字符串类型的authType转换为整数 + auth_type_int = None + if auth_type is not None: + try: + auth_type_int = int(auth_type) + self.logger.info( + f"用户{user_id}业务系统{biz_sys_id}的鉴权类型: {auth_type_int}" + ) + except (ValueError, TypeError): + self.logger.warning(f"无法将authType转换为整数: {auth_type}") + + return auth_type_int, auth_data + + +class BusinessTokenService: + """业务系统Token服务""" + + def __init__(self): + self.logger = get_logger("BusinessTokenService") + self.retry_executor = ApiRetryExecutor() + + async def get_business_system_token( + self, auth_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + 获取业务系统Token + + Args: + auth_data: 认证数据字典 + + Returns: + 包含token信息的字典 + """ + try: + # 验证输入参数 + validation_result = self._validate_auth_data(auth_data) + if not validation_result["valid"]: + return {"success": False, "msg": validation_result["error"]} + + # 解析配置 + config_result = self._parse_auth_config(auth_data) + if not config_result["valid"]: + return {"success": False, "msg": config_result["error"]} + + # 执行API调用获取Token + token_result = await self._execute_token_api( + config_result["api_def"], config_result["user_params"], auth_data + ) + + return token_result + + except Exception as e: + self.logger.error(f"获取业务系统Token失败: {str(e)}") + return {"success": False, "msg": f"系统错误: {str(e)}"} + + def _validate_auth_data(self, auth_data: Dict[str, Any]) -> Dict[str, Any]: + """验证认证数据""" + if not isinstance(auth_data, dict): + error_msg = f"auth_data类型错误: {type(auth_data)}" + self.logger.error(error_msg) + return {"valid": False, "error": "认证数据格式错误"} + + # 验证必要字段 + if "name" not in auth_data: + self.logger.error("auth_data中缺少name字段") + return {"valid": False, "error": "缺少name配置"} + + if not auth_data.get("apiVO"): + self.logger.error("缺少apiVO信息") + return {"valid": False, "error": "缺少API配置信息"} + + apiVO = auth_data["apiVO"] + if not isinstance(apiVO, dict): + error_msg = f"apiVO类型错误: {type(apiVO)}" + self.logger.error(error_msg) + return {"valid": False, "error": "API配置格式错误"} + + # 检查必要字段 + required_fields = ["accountConfig", "tokenPath"] + missing_fields = [field for field in required_fields if field not in apiVO] + if missing_fields: + self.logger.error(f"缺少必要字段: {missing_fields}") + return {"valid": False, "error": f"缺少必要配置字段: {missing_fields}"} + + return {"valid": True} + + def _parse_auth_config(self, auth_data: Dict[str, Any]) -> Dict[str, Any]: + """解析认证配置""" + try: + apiVO = auth_data["apiVO"] + + # 解析accountConfig + account_config = apiVO["accountConfig"] + if isinstance(account_config, str): + try: + account_config = json.loads(account_config) + self.logger.info("成功解析accountConfig JSON") + except json.JSONDecodeError as e: + self.logger.error(f"accountConfig JSON解析失败: {str(e)}") + return {"valid": False, "error": "配置数据格式错误"} + + if not isinstance(account_config, dict): + error_msg = f"accountConfig类型错误: {type(account_config)}" + self.logger.error(error_msg) + return {"valid": False, "error": "配置数据格式错误"} + + # 验证API定义 + api_def = apiVO.get("tcapabilityApiVO") + if api_def is None: + self.logger.error("缺少API配置信息") + return {"valid": False, "error": "缺少API配置信息"} + + # 准备用户参数 + try: + parameters_body = account_config.get("parametersBody", []) + user_params = ParameterExtractor.extract_param_defaults( + parameters_body + ) + except Exception as e: + self.logger.error(f"准备API参数失败: {str(e)}") + return {"valid": False, "error": "参数准备失败"} + + # 准备API定义映射 + api_def_map = {**api_def, "parameters": api_def.get("apiParameterList", [])} + + return { + "valid": True, + "api_def": api_def_map, + "user_params": user_params, + "account_config": account_config, + } + + except Exception as e: + self.logger.error(f"解析认证配置失败: {str(e)}") + return {"valid": False, "error": "配置解析失败"} + + async def _execute_token_api( + self, + api_def: Dict[str, Any], + user_params: Dict[str, Any], + auth_data: Dict[str, Any], + ) -> Dict[str, Any]: + """执行Token获取API""" + try: + # 执行API调用 + api_res = await self.retry_executor.execute_with_retry( + api_def, user_params, need_auth=False + ) + + if not api_res: + return {"success": False, "msg": "API调用失败"} + + # 提取Token + return self._extract_token_from_response(api_res, auth_data) + + except AuthError as e: + self.logger.error(f"API调用失败: {str(e)}") + return {"success": False, "msg": f"API调用失败: {str(e)}"} + except Exception as e: + self.logger.error(f"执行Token API异常: {str(e)}") + return {"success": False, "msg": f"API调用异常: {str(e)}"} + + def _extract_token_from_response( + self, api_res: Dict[str, Any], auth_data: Dict[str, Any] + ) -> Dict[str, Any]: + """从API响应中提取Token""" + try: + name = auth_data["name"] + token_path = auth_data["apiVO"]["tokenPath"] + + value = get_nested_value({"res": api_res}, token_path) + + if not value: + self.logger.error("未获取到有效token") + return {"success": False, "msg": "获取token失败"} + + return { + "tokenHeader": {name: value}, + "token": value, + "msg": api_res.get("msg", "获取token成功"), + "success": True, + } + + except Exception as e: + self.logger.error(f"处理token结果失败: {str(e)}") + return {"success": False, "msg": "处理token结果失败"} + + +class AuthService: + """认证服务,负责处理鉴权相关逻辑""" + + def __init__(self): + self.logger = get_logger("AuthService") + self.token_manager = TokenManager() + self.company_auth_client = CompanyAuthClient() + self.business_token_service = BusinessTokenService() + + async def authorize_request( + self, + user_id: Optional[str], + biz_sys_id: Optional[str], + persist_token: bool = True, + ) -> Dict[str, Any]: + """ + 完整的请求鉴权处理 + + Args: + user_id: 用户ID + biz_sys_id: 业务系统ID + persist_token: 是否持久化token,默认True + + Returns: + 认证结果 + """ + self.logger.info(f"开始认证处理 - 用户ID: {user_id}, 业务系统ID: {biz_sys_id}, 持久化: {persist_token}") + + # 获取并验证token + token_header = await self.check_user_token(user_id, biz_sys_id, persist_token=persist_token) + self.logger.info(f"获取到Token头: {token_header}") + + if not token_header: + return { + "success": False, + "error_response": { + "error": "获取鉴权令牌失败,请前往管理平台进行鉴权或提供临时令牌" + }, + } + + return { + "success": True, + "tokenHeader": token_header, + "source": "auth_service", # 标记token来源 + } + + async def check_user_token( + self, + user_id: Optional[str], + biz_sys_id: Optional[str], + token: Optional[str] = None, + persist_token: bool = True, + ) -> Optional[Union[str, Dict[str, Any]]]: + """ + 检查用户Token是否有效,如无效则重新获取 + + Args: + user_id: 用户ID + biz_sys_id: 业务系统ID + token: 可选的临时Token + persist_token: 是否持久化token + + Returns: + Token值或None + """ + if not user_id or not biz_sys_id: + self.logger.warning("用户ID或业务系统ID为空,无法检查Token") + return None + + # 生成Token名 + token_name = self.token_manager.generate_token_name(user_id, biz_sys_id) + + self.logger.info(f"Token名: {token_name}") + + # 如果不需要持久化,直接重新获取Token + if not persist_token: + self.logger.info("不使用持久化,直接获取新Token") + return await self._refresh_user_token(user_id, biz_sys_id, token_name, persist_token) + + # 检查环境变量是否存在现有Token + exists, token_value = self.token_manager.check_token_exists(token_name) + self.logger.info(f"Token存在性检查: {exists}, 值: {token_value}") + + # 如果环境变量存在,直接返回值 + if exists: + self.logger.info( + f"从环境变量获取到用户{user_id}业务系统{biz_sys_id}的Token" + ) + return token_value + + # 如果提供了token参数,直接使用并存储 + if token: + if persist_token: + self.token_manager.store_token(token_name, token) + return token + + # 重新获取Token + return await self._refresh_user_token(user_id, biz_sys_id, token_name, persist_token) + + async def _refresh_user_token( + self, user_id: str, biz_sys_id: str, token_name: str, persist_token: bool = True + ) -> Optional[Union[str, Dict[str, Any]]]: + """刷新用户Token""" + # 获取鉴权类型和认证数据 + auth_type, auth_data = await self.company_auth_client.get_auth_info( + user_id, biz_sys_id + ) + + if auth_type is None: + self.logger.error(f"无法获取用户{user_id}业务系统{biz_sys_id}的鉴权类型") + return None + + # 根据鉴权类型获取Token + token_value = await self._get_token_by_auth_type( + user_id, biz_sys_id, auth_type, auth_data + ) + + self.logger.info(f"Token值: {token_value}") + + # 存储Token(根据persist_token参数决定是否持久化) + if token_value: + if persist_token: + success = self.token_manager.store_token(token_name, token_value) + if not success: + self.logger.error(f"存储用户{user_id}业务系统{biz_sys_id}的Token失败") + else: + self.logger.info(f"成功存储用户{user_id}业务系统{biz_sys_id}的Token到环境变量: {token_name}") + else: + self.logger.info(f"跳过持久化,用户{user_id}业务系统{biz_sys_id}的Token仅在内存中使用") + else: + self.logger.warning(f"未能获取到用户{user_id}业务系统{biz_sys_id}的Token,token_value: {token_value}") + + return token_value + + def clear_token(self, user_id: str, biz_sys_id: str) -> bool: + """ + 清空指定用户的token + + Args: + user_id: 用户ID + biz_sys_id: 业务系统ID + + Returns: + 清空是否成功 + """ + if not user_id or not biz_sys_id: + self.logger.warning("用户ID或业务系统ID为空,无法清空Token") + return False + + # 生成Token名 + token_name = self.token_manager.generate_token_name(user_id, biz_sys_id) + + # 删除环境变量中的token + success = EnvironmentManager.delete(token_name) + + if success: + self.logger.info(f"成功清空用户{user_id}业务系统{biz_sys_id}的Token: {token_name}") + else: + self.logger.error(f"清空用户{user_id}业务系统{biz_sys_id}的Token失败: {token_name}") + + return success + + def clear_all_tokens(self) -> bool: + """ + 清空所有持久化的token + + Returns: + 清空是否成功 + """ + try: + # 加载所有token + persistent_tokens = EnvironmentManager._load_persistent_tokens() + + # 删除所有token类型的环境变量 + for token_name in list(persistent_tokens.keys()): + if token_name.endswith('token'): + EnvironmentManager.delete(token_name) + + self.logger.info("成功清空所有持久化token") + return True + except Exception as e: + self.logger.error(f"清空所有token失败: {e}") + return False + + async def _get_token_by_auth_type( + self, user_id: str, biz_sys_id: str, auth_type: int, auth_data: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: + """根据鉴权类型获取Token""" + try: + if auth_type == 0: + # 直接使用apiKey作为Token + return self._get_api_key_token(user_id, biz_sys_id, auth_data) + elif auth_type == 1: + # 调用登录接口获取Token + return await self._get_login_token(user_id, biz_sys_id, auth_data) + else: + self.logger.warning(f"不支持的鉴权类型: {auth_type}") + return None + except Exception as e: + self.logger.error(f"获取Token失败: {str(e)}") + return None + + def _get_api_key_token( + self, user_id: str, biz_sys_id: str, auth_data: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: + """获取API Key类型的Token""" + api_key = auth_data.get("apiKey") + name = auth_data.get("name") + + if api_key and name: + self.logger.info(f"使用apiKey作为用户{user_id}业务系统{biz_sys_id}的Token") + return {name: api_key} + else: + self.logger.warning(f"用户{user_id}业务系统{biz_sys_id}的apiKey或name为空") + return None + + async def _get_login_token( + self, user_id: str, biz_sys_id: str, auth_data: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: + """获取登录类型的Token""" + self.logger.info(f"通过登录接口获取用户{user_id}业务系统{biz_sys_id}的Token") + + login_res = await self.business_token_service.get_business_system_token( + auth_data + ) + + if login_res and login_res.get("tokenHeader") is not None: + return login_res.get("tokenHeader") + else: + error_msg = login_res.get("msg", "未知错误") if login_res else "未知错误" + self.logger.error(f"获取Token失败: {error_msg}") + return None + + +# 兼容性函数 - 保持向后兼容 +def extract_param_defaults(param_list: list) -> Dict[str, Any]: + """提取参数默认值(兼容性函数)""" + return ParameterExtractor.extract_param_defaults(param_list) + + +async def execute_api_call_with_retry( + api_config: Dict[str, Any], + user_params: Dict[str, Any], + need_auth: bool = False, + max_attempts: int = 3, + base_delay: int = 1, + max_delay: int = 60, +) -> Optional[Dict[str, Any]]: + """API重试调用(兼容性函数)""" + executor = ApiRetryExecutor(max_attempts, base_delay, max_delay) + return await executor.execute_with_retry(api_config, user_params, need_auth) + + +class EnvManager: + """环境变量管理器(兼容性类)""" + + @staticmethod + def get_env(key: str, default: Optional[str] = None) -> Optional[str]: + return EnvironmentManager.get(key, default) + + @staticmethod + def set_env(key: str, value: str) -> bool: + return EnvironmentManager.set(key, value) + + @staticmethod + def exists_env(key: str) -> bool: + return EnvironmentManager.exists(key) + + +class Config: + """配置类(兼容性)""" + pass + + +async def test_auth_service(): + """测试认证服务""" + auth_service = AuthService() + token_header = await auth_service.check_user_token( + "1932715213891215361", "1932385006853664770" + ) + logger.info(f"测试结果 - Token头: {token_header}") + + +if __name__ == "__main__": + # 配置日志 + from ..util.logger_config import setup_logging + import logging + setup_logging(log_level=logging.INFO) + + asyncio.run(test_auth_service()) diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_base.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_base.py new file mode 100644 index 0000000..e484f2e --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_base.py @@ -0,0 +1,610 @@ +""" +API基础模块 - 核心API管理和调用功能 + +这个模块是整个系统的核心,提供了API配置管理、Schema生成、接口调用等基础功能。 +主要包含以下组件: + +1. ApiBase类: API管理的抽象基类 +2. 工具函数: 拼音转换、Schema处理等 +3. 常量定义: 认证级别、参数类型等 + +主要功能: +- API配置的加载和处理 +- JSON Schema的生成和验证 +- 中文接口名称转拼音命名 +- API接口的统一调用管理 +- 认证和参数处理 + +作者: lzwcai +版本: 1.0.0 +""" + +import json +import re +import pypinyin +from abc import ABC, abstractmethod +from typing import Dict, List, Any, Tuple, Optional, Union + +# 导入业务工具模块 +from ..business.business_util import ( + generate_json_schema, # JSON Schema生成 + generate_schema_prompt, # Schema提示文本生成 + remove_property_from_api_array, # API数组属性移除 +) + + +# ==================== 常量定义 ==================== + +class AuthenticationLevel: + """ + API认证级别常量 + + 定义API接口的认证要求级别: + - REQUIRED: 需要认证(值为1) + - NOT_REQUIRED: 不需要认证(值为0) + """ + REQUIRED = 1 # 需要认证 + NOT_REQUIRED = 0 # 不需要认证 + + +# 导入统一日志配置 +from ..util.logger_config import get_logger + +# 获取日志器实例 +logger = get_logger(__name__) + + +# ==================== 工具函数 ==================== + +def pinyin_to_camel(text: str) -> str: + """ + 中文文本转拼音驼峰命名函数 + + 将中文文本转换为带'tool_'前缀的驼峰命名格式,用于生成API工具的标识符。 + 这个函数是系统中重要的命名转换工具,确保中文API名称能够转换为合法的标识符。 + + 转换规则: + 1. 将所有非字母数字字符(包括标点符号)替换为下划线 + 2. 将空格替换为下划线 + 3. 移除连续的下划线并去除首尾下划线 + 4. 使用pypinyin库将中文转换为拼音 + 5. 每个拼音单词首字母大写(驼峰格式) + 6. 添加'tool_'前缀以符合工具命名规范 + + 参数: + text: 要转换的中文文本 + + 返回: + str: 转换后的驼峰命名字符串,格式为'tool_XxxYyy' + + 示例: + >>> pinyin_to_camel("用户登录") + 'tool_YongHuDengLu' + >>> pinyin_to_camel("获取订单列表") + 'tool_HuoQuDingDanLieBiao' + + 异常处理: + TypeError: 如果输入不是字符串类型 + ValueError: 如果输入为空字符串 + + 容错机制: + - 转换失败时使用hash值生成备用名称 + - 确保始终返回有效的标识符 + """ + # 参数类型检查 + if not isinstance(text, str): + raise TypeError("text must be a string") + + # 参数内容检查 + if not text.strip(): + raise ValueError("text cannot be empty") + + try: + logger.debug(f"转换中文文本为拼音: {text}") + + # 第一步:将所有非中文、非字母、非数字的字符(包括中文标点符号)替换为空格 + # 这样可以正确处理中文标点符号(包括中文括号、顿号等) + # \u4e00-\u9fff 匹配所有中文字符 + # a-zA-Z0-9 匹配英文字母和数字 + cleaned = re.sub(r'[^\u4e00-\u9fffa-zA-Z0-9\s]', ' ', text) + + # 第二步:将多个空格合并为一个空格,并去除首尾空格 + cleaned = re.sub(r'\s+', ' ', cleaned).strip() + + # 第三步:使用pypinyin库转换为拼音列表 + # pypinyin会将中文转为拼音,英文和数字保持原样 + pinyin_list = pypinyin.lazy_pinyin(cleaned) + + # 第四步:将拼音列表转换为驼峰格式 + # 过滤掉空白字符、下划线等特殊字符,只保留有效的拼音单词 + # 注意:pypinyin对于空格会产生空字符串,需要过滤掉 + camel_case = "".join( + word.strip().capitalize() + for word in pinyin_list + if word.strip() and word.strip() not in ['_', '-', '.', ' '] + ) + + # 第五步:添加工具前缀 + result = f"tool_{camel_case}" + + logger.debug(f"拼音转换结果: {text} -> {result}") + return result + + except Exception as e: + logger.error(f"拼音转换失败,文本: '{text}', 错误: {str(e)}") + + # 容错处理:生成基于hash的备用名称 + # 使用hash确保相同输入产生相同输出 + fallback = f"tool_Unknown_{hash(text) % 10000}" + logger.warning(f"使用备用名称: {fallback}") + return fallback + + +def _process_api_schema(param: Dict[str, Any]) -> Tuple[Dict[str, Any], str]: + """ + API参数Schema处理函数 + + 这个函数负责处理单个API配置,生成对应的JSON Schema和接口名称。 + 它是API配置转换的核心函数,将业务平台的API配置转换为MCP工具所需的格式。 + + 处理流程: + 1. 提取API参数列表 + 2. 生成JSON Schema(包含参数类型、描述、默认值等) + 3. 将中文接口名转换为拼音格式的工具名称 + 4. 返回处理后的Schema和接口名称 + + 参数: + param: API参数配置字典,包含以下字段: + - interfaceName: 接口名称(中文) + - parameters: 参数列表 + - 其他API配置信息 + + 返回: + tuple: (处理后的JSON Schema, 转换后的接口名称) + - processed_schema: 符合JSON Schema规范的参数定义 + - interface_name: 转换后的拼音格式接口名称 + + 异常处理: + ImportError: 当无法导入必需模块时抛出 + Exception: 当Schema生成失败时抛出 + + 注意事项: + - userId参数的处理在请求时进行,而不是在Schema生成时 + - userId现在存储在lzwcaiConfig分组中,支持动态userId值,提高系统灵活性 + - 使用延迟导入避免循环依赖问题 + """ + try: + # 延迟导入避免循环依赖 + # 这些模块可能会反过来导入当前模块 + from .core_server import get_env_user_id + # from ..business.business_util import remove_property_from_api_item + + logger.debug(f"处理API参数: {param.get('interfaceName', 'N/A')}") + + # 提取API参数列表并生成JSON Schema + parameters = param.get("parameters", []) + logger.debug(f"参数数量: {len(parameters)}") + + # 调用业务工具模块生成标准JSON Schema + schema = generate_json_schema(parameters) + + # 重要说明:userId参数的处理策略 + # 为了支持动态userId值,userId参数的处理在请求时进行, + # 而不是在Schema生成时进行。这样可以支持不同用户的动态切换。 + # userId现在存储在lzwcaiConfig分组中。 + logger.debug("Schema生成完成,userId处理将在请求时进行") + + # 生成接口名称(中文转拼音) + interface_name_raw = param.get("interfaceName", "") + if interface_name_raw: + # 使用拼音转换函数生成工具名称 + interface_name = pinyin_to_camel(interface_name_raw) + else: + # 备用名称,防止接口名称为空 + interface_name = "tool_Unknown" + + logger.debug(f"生成接口名称: {interface_name_raw} -> {interface_name}") + return schema, interface_name + + except ImportError as e: + logger.error(f"导入必需模块失败: {str(e)}") + raise + except Exception as e: + logger.error(f"处理API Schema时出错: {str(e)}") + raise + + +def get_api_configs_map(api_configs: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + API配置映射处理函数 + + 这个函数是API配置处理的核心,负责将原始的API配置列表转换为 + 包含Schema和描述信息的处理后配置列表。每个API配置都会被转换为 + 一个完整的MCP工具定义。 + + 处理内容: + 1. 为每个API生成JSON Schema + 2. 转换中文接口名为拼音格式 + 3. 生成工具描述(业务描述 + 参数说明) + 4. 创建完整的工具配置对象 + 5. 错误处理和容错机制 + + 参数: + api_configs: API配置字典列表,每个字典包含: + - interfaceName: 接口名称 + - businessPrompts: 业务描述 + - parameters: 参数列表 + - 其他API配置信息 + + 返回: + list: 处理后的API配置列表,每个配置包含: + - interfaceName: 转换后的接口名称(拼音格式) + - schema: JSON Schema对象 + - schema_description: 完整的工具描述 + - 原始配置的所有其他字段 + + 异常处理: + TypeError: 如果api_configs不是列表类型 + ValueError: 如果api_configs为空列表 + + 容错机制: + - 跳过无效的配置项(非字典类型) + - 处理失败时保留原始配置并添加错误标记 + - 详细记录处理过程和错误信息 + """ + # 参数验证 + if not isinstance(api_configs, list): + raise TypeError("api_configs must be a list") + + if not api_configs: + raise ValueError("api_configs cannot be empty") + + logger.info(f"开始处理 {len(api_configs)} 个API配置") + api_array = [] + + # 遍历处理每个API配置 + for i, param in enumerate(api_configs): + # 检查配置项类型 + if not isinstance(param, dict): + logger.warning( + f"跳过无效的API配置 (索引 {i}): 不是字典类型" + ) + continue + + try: + logger.debug(f"处理API配置 {i}: {param.get('interfaceName', 'N/A')}") + + # 处理Schema和接口名称 + schema, interface_name = _process_api_schema(param) + + # 获取业务描述 + description = param.get("businessPrompts", "") + + # 生成参数Schema的提示文本 + schema_prompt = generate_schema_prompt(schema) + + # 组合工具描述:业务描述 + 参数说明 + if description: + schema_description = f"{description}\n\n{schema_prompt}" + else: + schema_description = f"工具描述: 暂无描述\n\n{schema_prompt}" + + # 创建处理后的API配置对象 + processed_config = { + **param, # 保留原始配置的所有字段 + "interfaceName": interface_name, # 更新接口名称为拼音格式 + "schema": schema, # 添加JSON Schema + "schema_description": schema_description, # 添加完整描述 + } + + api_array.append(processed_config) + logger.debug(f"API配置 {i} 处理完成: {interface_name}") + + except Exception as e: + # 处理异常:记录错误但不中断整个处理流程 + logger.error(f"处理API配置 {i} 时出错: {str(e)}") + logger.debug(f"错误的API配置内容: {param}") + + # 创建错误配置对象,保留原始数据但添加错误标记 + error_config = { + **param, # 保留原始配置 + "interfaceName": f"tool_Error_{i}", # 错误标记的接口名 + "schema": {}, # 空Schema + "schema_description": f"配置处理错误: {str(e)}", # 错误描述 + } + api_array.append(error_config) + + logger.info(f"API配置处理完成,成功处理 {len(api_array)} 个配置") + return api_array + + +# ==================== API基础管理类 ==================== + +class ApiBase(ABC): + """ + API管理抽象基类 + + 这个类是整个API管理系统的核心基类,提供了API配置处理、 + Schema生成、接口调用等基础功能。所有具体的API管理实现 + 都应该继承这个类。 + + 主要职责: + 1. API配置的加载和验证 + 2. API配置到工具定义的转换 + 3. 提供统一的API调用接口 + 4. 管理API配置的生命周期 + 5. 提供配置查询和检索功能 + + 设计模式: + - 抽象基类模式:定义API管理的标准接口 + - 模板方法模式:提供通用的处理流程 + - 策略模式:支持不同的认证和调用策略 + + 属性: + api_configs: 原始API配置列表 + api_configs_map: 处理后的API配置映射表 + """ + + def __init__(self, api_configs: List[Dict[str, Any]]) -> None: + """ + 初始化API基础管理器 + + 这个构造函数负责初始化API管理器,处理传入的API配置列表, + 并生成相应的工具定义映射表。 + + 初始化流程: + 1. 验证输入参数的类型和内容 + 2. 保存原始API配置 + 3. 调用配置映射函数生成工具定义 + 4. 记录初始化结果 + 5. 可选的调试输出 + + 参数: + api_configs: API配置字典列表,每个字典包含完整的API定义 + + 异常处理: + TypeError: 如果api_configs不是列表类型 + ValueError: 如果api_configs为空列表 + + 注意事项: + - 配置验证在映射函数中进行 + - 支持部分配置失败的容错处理 + - 调试模式下可以输出Schema到文件 + """ + # 参数类型验证 + if not isinstance(api_configs, list): + raise TypeError("api_configs must be a list") + + # 参数内容验证 + if not api_configs: + raise ValueError("api_configs cannot be empty") + + # 保存原始配置 + self.api_configs = api_configs + + # 生成处理后的配置映射表 + # 这是核心处理步骤,将原始配置转换为MCP工具定义 + self.api_configs_map = get_api_configs_map(api_configs) + + logger.info(f"ApiBase初始化完成,共处理 {len(self.api_configs)} 个API配置") + + # 可选的调试输出(默认关闭) + # 在开发和调试阶段可以启用这个功能 + # self._save_debug_schema() + + def _save_debug_schema(self) -> None: + """ + 保存调试Schema到文件 + + 这个方法用于开发和调试阶段,将处理后的API配置映射表 + 保存到JSON文件中,方便查看和分析Schema生成结果。 + + 输出文件: + output_schema.json: 包含所有处理后的API配置 + + 特性: + - UTF-8编码确保中文正确显示 + - 格式化输出便于阅读 + - 异常安全,不会影响主要功能 + """ + try: + with open("output_schema.json", "w", encoding="utf-8") as f: + json.dump(self.api_configs_map, f, ensure_ascii=False, indent=4) + logger.debug("调试Schema已保存到 output_schema.json") + except Exception as e: + logger.error(f"保存调试Schema失败: {str(e)}") + + async def call_interface( + self, api_config: Dict[str, Any], request_data: Dict[str, Any] + ) -> Any: + """ + API接口调用方法 + + 这是ApiBase类的核心方法,负责调用具体的API接口。 + 它处理认证逻辑、参数传递和错误处理,为上层提供统一的API调用接口。 + + 调用流程: + 1. 验证输入参数的类型 + 2. 导入核心服务器模块(避免循环依赖) + 3. 判断是否需要认证 + 4. 调用底层API接口 + 5. 返回API响应结果 + + 认证处理: + - 根据API配置中的authenticationRequired字段判断是否需要认证 + - 支持两种认证级别:REQUIRED(1) 和 NOT_REQUIRED(0) + - 认证逻辑由core_server模块的call_api函数处理 + + 参数: + api_config: API配置字典,包含: + - authenticationRequired: 认证要求级别 + - apiUrl: API接口地址 + - method: HTTP方法 + - 其他API配置信息 + request_data: 请求数据字典,包含: + - header: 请求头参数 + - query: 查询参数 + - body: 请求体参数 + - lzwcaiConfig: 配置参数(包含userId) + + 返回: + Any: API接口的响应数据,通常是字典格式 + + 异常处理: + TypeError: 如果参数不是字典类型 + ImportError: 如果无法导入核心服务器模块 + Exception: 如果API调用失败 + + 设计考虑: + - 使用延迟导入避免循环依赖 + - 统一的错误处理和日志记录 + - 支持异步调用以提高性能 + """ + # 参数类型验证 + if not isinstance(api_config, dict): + raise TypeError("api_config must be a dictionary") + + if not isinstance(request_data, dict): + raise TypeError("request_data must be a dictionary") + + try: + # 延迟导入避免循环依赖 + # core_server模块可能会导入当前模块 + from .core_server import call_api + + # 判断认证要求 + # 从API配置中获取认证要求,默认为不需要认证 + auth_required = api_config.get( + "authenticationRequired", AuthenticationLevel.NOT_REQUIRED + ) + need_auth = auth_required == AuthenticationLevel.REQUIRED + + logger.info(f"调用API接口,需要认证: {need_auth}") + logger.debug(f"API配置: {api_config.get('apiUrl', 'N/A')}") + + # 调用底层API接口 + # call_api函数处理具体的HTTP请求、认证、参数处理等 + return await call_api(api_config, request_data, need_auth=need_auth) + + except ImportError as e: + logger.error(f"导入call_api函数失败: {str(e)}") + raise ImportError( + f"无法导入必需的API调用功能: {str(e)}" + ) + except Exception as e: + logger.error(f"API调用失败: {str(e)}") + logger.debug("API调用异常详情:", exc_info=True) + raise + + def get_api_config_by_name(self, interface_name: str) -> Optional[Dict[str, Any]]: + """ + 根据接口名称获取API配置 + + 这个方法用于根据接口名称查找对应的API配置。 + 接口名称是经过拼音转换后的工具名称(如:tool_YongHuDengLu)。 + + 查找逻辑: + - 遍历所有处理后的API配置 + - 匹配interfaceName字段 + - 返回第一个匹配的配置 + + 参数: + interface_name: 要查找的接口名称(拼音格式) + + 返回: + Optional[Dict[str, Any]]: 找到的API配置字典,未找到则返回None + + 使用场景: + - MCP工具调用时查找对应的API配置 + - 验证工具名称是否存在 + - 获取特定工具的配置信息 + """ + # 参数类型检查 + if not isinstance(interface_name, str): + logger.warning(f"接口名称类型错误: {type(interface_name)}") + return None + + # 遍历查找匹配的配置 + for config in self.api_configs_map: + if config.get("interfaceName") == interface_name: + logger.debug(f"找到接口配置: {interface_name}") + return config + + logger.debug(f"未找到接口配置: {interface_name}") + return None + + def get_all_interface_names(self) -> List[str]: + """ + 获取所有可用的接口名称列表 + + 这个方法返回所有已处理的API配置的接口名称列表。 + 主要用于调试、监控和工具列表展示。 + + 返回: + List[str]: 所有接口名称的列表(拼音格式) + + 特性: + - 过滤掉空的接口名称 + - 返回的是处理后的拼音格式名称 + - 按配置顺序返回 + + 使用场景: + - 系统监控和状态检查 + - 调试和日志记录 + - 管理界面展示可用工具 + """ + interface_names = [ + config.get("interfaceName", "") + for config in self.api_configs_map + if config.get("interfaceName") # 过滤空名称 + ] + + logger.debug(f"获取到 {len(interface_names)} 个接口名称") + return interface_names + + def get_schema_by_name(self, interface_name: str) -> Optional[Dict[str, Any]]: + """ + 根据接口名称获取JSON Schema + + 这个方法是get_api_config_by_name的便捷包装, + 直接返回指定接口的JSON Schema定义。 + + 参数: + interface_name: 接口名称(拼音格式) + + 返回: + Optional[Dict[str, Any]]: JSON Schema字典,未找到则返回None + + 使用场景: + - 参数验证 + - 文档生成 + - 客户端工具定义 + """ + config = self.get_api_config_by_name(interface_name) + if config: + return config.get("schema") + return None + + @property + def config_count(self) -> int: + """ + 获取API配置数量 + + 这是一个属性方法,返回当前管理的API配置总数。 + 主要用于监控、日志记录和状态检查。 + + 返回: + int: API配置的数量 + """ + return len(self.api_configs_map) + + def __repr__(self) -> str: + """ + ApiBase对象的字符串表示 + + 提供对象的简洁字符串表示,主要用于调试和日志记录。 + + 返回: + str: 对象的字符串表示,格式为"ApiBase(configs=数量)" + """ + return f"ApiBase(configs={self.config_count})" diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py new file mode 100644 index 0000000..3e8b538 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py @@ -0,0 +1,867 @@ +""" +核心服务器模块 + +提供API调用、认证处理、配置管理等核心功能 +""" + +import asyncio +import json +import os +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union +from urllib.parse import urljoin + +import httpx + +from .api_auth_service import AuthService +from ..util.logger_config import get_logger + +# 获取日志器 +logger = get_logger(__name__) + + +class ApiError(Exception): + """API调用相关异常""" + + def __init__(self, message: str, status_code: Optional[int] = None): + super().__init__(message) + self.status_code = status_code + + +class ResponseSaver: + """响应数据保存管理器""" + + def __init__(self, save_dir: str = "lzwcai_mcp_dyntoolapi_log_call_api"): + """ + 初始化响应保存器 + + Args: + save_dir: 保存目录路径 + """ + self.save_dir = Path(save_dir) + self.save_dir.mkdir(exist_ok=True) + logger.debug(f"响应保存目录: {self.save_dir.absolute()}") + + def save_response( + self, + response_data: Dict[str, Any], + api_url: str, + method: str = "GET", + request_data: Optional[Dict[str, Any]] = None, + ) -> str: + """ + 保存API响应到本地JSON文件 + + Args: + response_data: 响应数据 + api_url: API URL + method: HTTP方法 + request_data: 请求数据 + + Returns: + 保存的文件路径 + """ + try: + # 生成文件名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # 精确到毫秒 + safe_url = self._sanitize_filename(api_url) + filename = f"{timestamp}_{method}_{safe_url}.json" + file_path = self.save_dir / filename + + # 构建保存的数据结构 + save_data = { + "timestamp": datetime.now().isoformat(), + "api_info": { + "url": api_url, + "method": method.upper(), + }, + "request_data": request_data or {}, + "response_data": response_data, + "metadata": { + "saved_at": datetime.now().isoformat(), + "file_name": filename, + } + } + + # 保存到文件 + with open(file_path, "w", encoding="utf-8") as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + + logger.info(f"API响应已保存到: {file_path}") + return str(file_path) + + except Exception as e: + logger.error(f"保存API响应失败: {str(e)}") + return "" + + def _sanitize_filename(self, url: str) -> str: + """ + 清理URL以生成安全的文件名 + + Args: + url: 原始URL + + Returns: + 清理后的文件名部分 + """ + # 移除协议和域名,只保留路径 + if "://" in url: + url = url.split("://", 1)[1] + if "/" in url: + url = url.split("/", 1)[1] if "/" in url else url + + # 替换特殊字符 + safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + sanitized = "".join(c if c in safe_chars else "_" for c in url) + + # 限制长度 + return sanitized[:50] if len(sanitized) > 50 else sanitized + + +class ConfigManager: + """配置管理器""" + + @staticmethod + def load_api_config(file_path: str = "generator_api.json") -> Dict[str, Any]: + """ + 加载API配置文件 + + Args: + file_path: JSON配置文件路径 + + Returns: + 解析后的API配置字典 + + Raises: + FileNotFoundError: 文件不存在 + json.JSONDecodeError: JSON格式错误 + """ + logger.debug(f"尝试加载配置文件: {file_path}") + + try: + with open(file_path, "r", encoding="utf-8") as file: + config = json.load(file) + logger.info(f"成功加载配置文件: {file_path}") + logger.debug(f"配置内容: {config}") + return config + except FileNotFoundError: + logger.error(f"配置文件未找到: {file_path}") + raise FileNotFoundError(f"配置文件未找到: {file_path}") + except json.JSONDecodeError as e: + logger.error(f"配置文件JSON格式错误: {file_path} - {str(e)}") + raise json.JSONDecodeError( + f"配置文件JSON格式错误: {file_path} - {str(e)}", e.doc, e.pos + ) + + +class UserManager: + """用户管理器""" + + @staticmethod + def get_user_id_from_env() -> Tuple[bool, Optional[str]]: + """ + 从环境变量中获取用户ID + + Returns: + tuple: (是否成功获取, 用户ID值) + """ + try: + user_id = os.environ.get("userId") + if user_id: + logger.debug(f"从环境变量获取用户ID: {user_id}") + return True, user_id + logger.debug("环境变量中未找到用户ID") + return False, None + except Exception as e: + logger.warning(f"获取环境变量用户ID时发生异常: {e}") + return False, None + + @staticmethod + def extract_user_id_from_request( + request_data: Dict[str, Any], is_grouped_format: bool + ) -> Optional[str]: + """ + 从请求数据中提取用户ID + + Args: + request_data: 请求数据 + is_grouped_format: 是否为分组格式 + + Returns: + 用户ID或None + """ + # 优先从请求数据中获取userId + if isinstance(request_data, dict): + if is_grouped_format: + # 按优先级顺序查找:lzwcaiConfig > header > body > userId(兼容旧版本) + user_id = ( + request_data.get("lzwcaiConfig", {}).get("userId") + or request_data.get("header", {}).get("userId") + or request_data.get("body", {}).get("userId") + or (request_data.get("userId") or {}).get("userId") + ) + if user_id: + logger.info(f"从请求数据中获取到用户ID: {user_id}") + return user_id + else: + # 非分组格式:优先从lzwcaiConfig获取,然后是userId(兼容旧版本) + user_id = ( + (request_data.get("lzwcaiConfig") or {}).get("userId") + or (request_data.get("userId") or {}).get("userId") + ) + if user_id: + logger.info(f"从请求数据中获取到用户ID: {user_id}") + return user_id + + # 如果请求数据中没有userId,则从环境变量获取作为备用 + success, env_user_id = UserManager.get_user_id_from_env() + if success: + logger.info(f"从环境变量获取到用户ID: {env_user_id}") + return env_user_id + + logger.warning("未能从请求数据或环境变量中获取到用户ID") + return None + + +class HeaderProcessor: + """请求头处理器""" + + @staticmethod + def validate_header_value(value: Any) -> str: + """ + 验证并标准化请求头值 + + Args: + value: 原始值 + + Returns: + 标准化后的字符串值 + """ + if value is None: + return "" + return str(value).strip() + + @staticmethod + def process_auth_headers( + base_headers: Dict[str, Any], + request_data: Dict[str, Any], + auth_token: Optional[Dict[str, Any]] = None, + ) -> Dict[str, str]: + """ + 处理认证头信息 + + Args: + base_headers: 基础请求头 + request_data: 请求数据 + auth_token: 认证token信息 + + Returns: + 处理后的请求头字典 + + Raises: + ValueError: 当base_headers不是字典类型时 + """ + if not isinstance(base_headers, dict): + raise ValueError("base_headers必须是字典类型") + + if not isinstance(request_data, dict): + request_data = {} + + if auth_token is None: + auth_token = {} + + # 获取请求数据中的header部分 + request_headers = request_data.get("header", {}) + + # 按优先级合并headers: base < request < auth_token + processed_headers = {} + + # 1. 基础headers (key转为小写以避免重复) + for key, value in base_headers.items(): + processed_headers[key.lower()] = HeaderProcessor.validate_header_value(value) + + # 2. 请求中的headers (key转为小写以避免重复) + for key, value in request_headers.items(): + processed_headers[key.lower()] = HeaderProcessor.validate_header_value(value) + + # 3. 认证token headers (最高优先级, key转为小写以避免重复) + for key, value in auth_token.items(): + processed_headers[key.lower()] = HeaderProcessor.validate_header_value(value) + + return processed_headers + + +class RequestBuilder: + """请求构建器""" + + @staticmethod + def is_grouped_format(request_data: Dict[str, Any]) -> bool: + """ + 检查请求数据是否为分组格式 + + Args: + request_data: 请求数据 + + Returns: + 是否为分组格式 + """ + return any( + key in ["header", "path", "query", "body", "params", "form", "formdata"] + for key in request_data.keys() + ) + + @staticmethod + def build_url_with_path_params(base_url: str, path_params: Dict[str, Any]) -> str: + """ + 使用路径参数构建URL + + Args: + base_url: 基础URL + path_params: 路径参数 + + Returns: + 替换路径参数后的URL + """ + url = base_url + for key, value in path_params.items(): + placeholder = f"{{{key}}}" + if placeholder in url: + url = url.replace(placeholder, str(value)) + return url + + @staticmethod + def extract_parameters_from_grouped_format( + request_data: Dict[str, Any], method: str + ) -> Tuple[ + Dict[str, Any], + Dict[str, Any], + Optional[Dict[str, Any]], + Optional[Dict[str, Any]], + Optional[Dict[str, Any]], + ]: + """ + 从分组格式请求数据中提取参数 + + Args: + request_data: 请求数据 + method: HTTP方法 + + Returns: + (path_params, query_params, json_data, form_data, formdata_data) + """ + path_params = {} + query_params = {} + json_data = None + form_data = None + formdata_data = None + + # 处理路径参数 + if "path" in request_data: + path_params.update(request_data["path"]) + + # 兼容params命名(与path含义相同) + if "params" in request_data: + path_params.update(request_data["params"]) + + # 处理查询参数 + if "query" in request_data: + query_params.update(request_data["query"]) + + # 处理请求体数据 + if "body" in request_data and method.upper() in ["POST", "PUT", "PATCH"]: + json_data = request_data["body"] + + if "form" in request_data and method.upper() in ["POST", "PUT", "PATCH"]: + form_data = request_data["form"] + + if "formdata" in request_data and method.upper() in ["POST", "PUT", "PATCH"]: + formdata_data = request_data["formdata"] + + return path_params, query_params, json_data, form_data, formdata_data + + @staticmethod + def extract_parameters_from_flat_format( + request_data: Dict[str, Any], parameters: List[Dict[str, Any]], method: str + ) -> Tuple[ + Dict[str, str], + Dict[str, Any], + Dict[str, Any], + Optional[Dict[str, Any]], + Optional[Dict[str, Any]], + Optional[Dict[str, Any]], + ]: + """ + 从扁平格式请求数据中提取参数 + + Args: + request_data: 请求数据 + parameters: 参数配置列表 + method: HTTP方法 + + Returns: + (headers, path_params, query_params, json_data, form_data, formdata_data) + """ + headers = {} + path_params = {} + query_params = {} + json_data = None + form_data = None # for application/x-www-form-urlencoded + formdata_data = None # for multipart/form-data + + for param in parameters: + param_name = param.get("paramName") + request_type = param.get("requestType") + default_value = param.get("defaultValue") + + # 获取参数值:优先使用请求数据中的值,否则使用默认值 + if param_name in request_data: + param_value = request_data[param_name] + # 如果传入的值为None或空字符串,且有默认值,则使用默认值 + if param_value in (None, "") and default_value is not None: + param_value = default_value + else: + param_value = default_value + + # 如果参数没有值且不是必需的,则跳过 + if param_value is None and param.get("required", 0) == 0: + continue + + # 根据请求类型分配参数 + if request_type == "header": + headers[param_name] = ( + str(param_value) if param_value is not None else "" + ) + elif request_type == "query": + if param_value is not None: + query_params[param_name] = param_value + elif request_type in ["params", "path"]: + if param_value is not None: + path_params[param_name] = param_value + elif request_type == "body" and method.upper() in ["POST", "PUT", "PATCH"]: + if json_data is None: + json_data = {} + json_data[param_name] = param_value + elif request_type == "form" and method.upper() in ["POST", "PUT", "PATCH"]: + if form_data is None: + form_data = {} + form_data[param_name] = param_value + elif request_type == "formdata" and method.upper() in ["POST", "PUT", "PATCH"]: + if formdata_data is None: + formdata_data = {} + formdata_data[param_name] = param_value + + return headers, path_params, query_params, json_data, form_data, formdata_data + + +class ApiClient: + """API客户端""" + + def __init__(self, timeout: int = 30, verify: bool = True, save_responses: bool = True, save_dir: str = "lzwcai_mcp_dyntoolapi_log_call_api"): + """ + 初始化API客户端 + + Args: + timeout: 请求超时时间(秒) + verify: 是否验证SSL证书 + save_responses: 是否保存响应到本地JSON文件 + save_dir: 响应保存目录 + """ + self.timeout = timeout + self.verify = verify + self.save_responses = save_responses + self.auth_service = AuthService() + + # 初始化响应保存器 + if self.save_responses: + self.response_saver = ResponseSaver(save_dir) + logger.info(f"已启用响应保存功能,保存目录: {save_dir}") + else: + self.response_saver = None + logger.info("响应保存功能已禁用") + + async def call_api( + self, + api_config: Dict[str, Any], + request_data: Optional[Dict[str, Any]] = None, + need_auth: bool = True, + ) -> Dict[str, Any]: + """ + 通用API调用方法 + + Args: + api_config: API配置信息 + request_data: 请求数据,支持扁平格式和分组格式 + need_auth: 是否需要认证 + + Returns: + API响应结果 + + Raises: + ApiError: API调用相关错误 + ValueError: 配置或参数错误 + """ + api_url = api_config.get("apiUrl", "N/A") + logger.info(f"开始API调用: {api_url}") + logger.debug(f"需要认证: {need_auth}") + logger.debug(f"请求数据: {request_data}") + + # 验证API配置 + self._validate_api_config(api_config) + + # 准备基础信息 + domain_url = api_config["domainUrl"] + api_url = api_config["apiUrl"] + method = api_config.get("method", "GET") + + # 构建完整URL + full_url = urljoin(domain_url.rstrip("/") + "/", api_url.lstrip("/")) + logger.debug(f"完整URL: {full_url}") + logger.debug(f"HTTP方法: {method}") + + # 初始化请求参数 + headers = {} + query_params = {} + path_params = {} + json_data = None + form_data = None + formdata_data = None + + request_data = request_data or {} + parameters = api_config.get("parameters", []) + logger.debug(f"参数配置数量: {len(parameters)}") + + # 检查请求数据格式 + is_grouped = RequestBuilder.is_grouped_format(request_data) + logger.debug(f"请求数据格式 - 分组格式: {is_grouped}") + + # 处理认证 + if need_auth: + logger.debug("开始处理认证") + auth_headers = await self._handle_authentication( + request_data, api_config, is_grouped + ) + headers.update(auth_headers) + logger.debug(f"认证头信息: {auth_headers}") + else: + logger.debug("跳过认证") + + # 提取参数 + if is_grouped: + (path_params, query_params, json_data, form_data, formdata_data) = ( + RequestBuilder.extract_parameters_from_grouped_format( + request_data, method + ) + ) + else: + ( + param_headers, + path_params, + query_params, + json_data, + form_data, + formdata_data, + ) = RequestBuilder.extract_parameters_from_flat_format( + request_data, parameters, method + ) + headers.update(param_headers) + + logger.info(f"请求头: {headers},request_data: {request_data}") + # 处理请求头 + headers = HeaderProcessor.process_auth_headers(headers, request_data) + # 替换URL中的路径参数 + if path_params: + full_url = RequestBuilder.build_url_with_path_params(full_url, path_params) + + # 根据请求体内容设置Content-Type (如果未被显式设置) + if "content-type" not in headers: + if json_data is not None: + headers["Content-Type"] = "application/json" + # httpx会自动为form_data设置'application/x-www-form-urlencoded' + # httpx会自动为formdata_data设置'multipart/form-data'并添加boundary + + # 发送请求 + logger.info(f"发送HTTP请求: {method} {full_url}") + + # 记录重要的请求信息(INFO级别,便于调试) + if headers: + # 过滤敏感信息,只显示关键头部 + safe_headers = {} + for key, value in headers.items(): + if key.lower() in ['authorization', 'x-api-key', 'token']: + safe_headers[key] = value + else: + safe_headers[key] = value + logger.info(f"请求头: {safe_headers}") + + if query_params: + logger.info(f"查询参数: {query_params}") + + if json_data: + logger.info(f"请求体 (JSON): {json_data}") + if form_data: + logger.info(f"请求体 (Form): {form_data}") + if formdata_data: + logger.info(f"请求体 (FormData): {formdata_data}") + + # 详细的调试信息仍保留在DEBUG级别 + logger.debug(f"完整请求头: {headers}") + logger.debug(f"完整查询参数: {query_params}") + logger.debug(f"完整请求体 (JSON): {json_data}") + logger.debug(f"完整请求体 (Form): {form_data}") + logger.debug(f"完整请求体 (FormData): {formdata_data}") + + # 发送请求并获取响应 + response = await self._send_request( + method, full_url, headers, query_params, json_data, form_data, formdata_data + ) + + # 保存响应到本地JSON文件 + if self.save_responses and self.response_saver: + try: + saved_path = self.response_saver.save_response( + response_data=response, + api_url=full_url, + method=method, + request_data=request_data + ) + if saved_path: + logger.info(f"响应已保存到: {saved_path}") + except Exception as e: + logger.error(f"保存响应失败: {str(e)}") + + return response + + def _validate_api_config(self, api_config: Dict[str, Any]) -> None: + """验证API配置""" + logger.debug("验证API配置") + + if not api_config: + logger.error("API配置为空") + raise ValueError("API配置不能为空") + + if not api_config.get("domainUrl") or not api_config.get("apiUrl"): + logger.error(f"缺少必要配置项 - domainUrl: {api_config.get('domainUrl')}, apiUrl: {api_config.get('apiUrl')}") + raise ValueError("缺少必要的API配置项:domainUrl或apiUrl") + + logger.debug("API配置验证通过") + + async def _handle_authentication( + self, request_data: Dict[str, Any], api_config: Dict[str, Any], is_grouped: bool + ) -> Dict[str, str]: + """处理认证逻辑""" + user_id = UserManager.extract_user_id_from_request(request_data, is_grouped) + biz_sys_id = api_config.get("bizSysId") + + auth_result = await self.auth_service.authorize_request(user_id, biz_sys_id) + + if not auth_result["success"]: + raise ApiError( + f"认证失败: {auth_result.get('error_response', {})}", + auth_result.get("error_response", {}).get("status_code"), + ) + + return auth_result.get("tokenHeader", {}) + + + def _contains_file(self, data: Dict[str, Any]) -> bool: + """检查数据字典中是否包含文件类对象""" + if not data: + return False + for value in data.values(): + # 检查是否为字节流或具有read属性的对象(文件句柄) + if isinstance(value, bytes) or hasattr(value, 'read'): + return True + return False + + async def _send_request( + self, + method: str, + url: str, + headers: Dict[str, str], + query_params: Dict[str, Any], + json_data: Optional[Dict[str, Any]], + form_data: Optional[Dict[str, Any]], + formdata_data: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + """发送HTTP请求""" + async with httpx.AsyncClient( + verify=self.verify, timeout=self.timeout + ) as client: + try: + # 准备请求参数 + request_kwargs = { + "params": query_params, + "headers": headers, + } + + # 为有请求体的方法添加数据 + if method.upper() in ["POST", "PUT", "PATCH", "DELETE"]: + if json_data is not None: + request_kwargs["json"] = json_data + elif form_data is not None: + request_kwargs["data"] = form_data + elif formdata_data is not None: + # 区分文件上传和普通formdata + if self._contains_file(formdata_data): + request_kwargs["files"] = formdata_data + else: + request_kwargs["data"] = formdata_data + + # 根据HTTP方法发送请求 + request_func = getattr(client, method.lower(), None) + if request_func is None: + raise ValueError(f"不支持的HTTP方法: {method}") + + response = await request_func(url, **request_kwargs) + + # 检查响应状态 + response.raise_for_status() + + # 解析响应 + return self._parse_response(response) + + except httpx.RequestError as e: + logger.error(f"请求错误: {str(e)}") + raise ApiError(f"请求发送失败: {str(e)}") + except httpx.HTTPStatusError as e: + # 记录详细的HTTP错误信息 + logger.error(f"HTTP状态错误: {e.response.status_code} - {str(e)}") + logger.error(f"请求URL: {e.request.url}") + logger.error(f"请求方法: {e.request.method}") + + # 记录响应内容(用于调试) + response_text = e.response.text + if response_text: + logger.error(f"响应内容: {response_text[:500]}{'...' if len(response_text) > 500 else ''}") + + # 记录响应头(可能包含有用的错误信息) + if e.response.headers: + logger.info(f"响应头: {dict(e.response.headers)}") + + return { + "status": "error", + "status_code": e.response.status_code, + "error": str(e), + "response": e.response.text, + } + except Exception as e: + logger.error(f"未知错误: {str(e)}") + raise ApiError(f"请求处理失败: {str(e)}") + + def _parse_response(self, response: httpx.Response) -> Dict[str, Any]: + """解析HTTP响应""" + # 记录响应信息 + logger.info(f"HTTP响应: {response.status_code} {response.reason_phrase}") + + content_type = response.headers.get("content-type", "") + logger.info(f"响应类型: {content_type}") + + # 记录响应大小 + content_length = len(response.content) if response.content else 0 + logger.info(f"响应大小: {content_length} bytes") + + if content_type.startswith("application/json"): + try: + json_response = response.json() + # 记录JSON响应的基本信息(避免记录过大的数据) + if isinstance(json_response, dict): + logger.info(f"JSON响应键: {list(json_response.keys())}") + return json_response + except Exception as e: + logger.error(f"JSON解析失败: {str(e)}") + return { + "status": "error", + "error": f"JSON解析失败: {str(e)}", + "raw_response": response.text, + "status_code": response.status_code, + } + else: + # 对于非JSON响应,记录前100个字符 + response_preview = response.text[:100] + "..." if len(response.text) > 100 else response.text + logger.info(f"文本响应预览: {response_preview}") + + return { + "status": "success", + "data": response.text, + "status_code": response.status_code, + } + + +# 兼容性函数 - 保持向后兼容 +async def call_api( + api_config: Dict[str, Any], + request_data: Optional[Dict[str, Any]] = None, + need_auth: bool = True, + timeout: int = 30, + verify: bool = True, + save_responses: bool = True, + save_dir: str = "lzwcai_mcp_dyntoolapi_log_call_api", +) -> Dict[str, Any]: + """ + 通用API调用方法(兼容性函数) + + Args: + api_config: API配置信息 + request_data: 请求数据 + need_auth: 是否需要认证 + timeout: 请求超时时间(秒) + verify: 是否验证SSL证书 + save_responses: 是否保存响应到本地JSON文件 + save_dir: 响应保存目录 + + Returns: + API响应结果 + """ + client = ApiClient(timeout=timeout, verify=verify, save_responses=save_responses, save_dir=save_dir) + return await client.call_api(api_config, request_data, need_auth) + + +def get_env_user_id() -> Tuple[bool, Optional[str]]: + """获取环境变量用户ID(兼容性函数)""" + return UserManager.get_user_id_from_env() + + +def extract_user_id( + request_data: Dict[str, Any], is_grouped_format: bool +) -> Optional[str]: + """提取用户ID(兼容性函数)""" + return UserManager.extract_user_id_from_request(request_data, is_grouped_format) + + +def process_auth_headers( + headers: dict, + request_data: dict, + auth_token: dict = None, +) -> dict: + """处理认证头(兼容性函数)""" + return HeaderProcessor.process_auth_headers(headers, request_data, auth_token) + + +def load_generator_api_config(file_path: str = "generator_api.json") -> Dict[str, Any]: + """加载API配置(兼容性函数)""" + return ConfigManager.load_api_config(file_path) + + +async def main(): + """主函数 - 测试示例""" + request_data = { + "header": {}, + "body": {"username": "wangpeng1", "password": "Wp147258"}, + "lzwcaiConfig": {"userId": "test_user_123"}, + } + + try: + config_path = "src/lzwcai_demp_tool_server_business_to_mcp/mcp_generator/src/user_params.json" + api_config = ConfigManager.load_api_config(config_path) + logger.info(f"正在尝试连接: {api_config.get('domainUrl')}{api_config.get('apiUrl')}") + client = ApiClient() + result = await client.call_api(api_config, request_data) + logger.info(f"请求结果: {result}") + + except Exception as e: + logger.error(f"发生错误: {e}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py new file mode 100644 index 0000000..7dd5ab2 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +认证数据转换器 +提供简洁的API来获取和转换认证信息 +""" + +import requests +import json +import sys +import os +from typing import Optional, Dict, Any + +# 添加项目根目录到 Python 路径 +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +sys.path.append(project_root) + +from ..business.get_business_api import get_business_api_details +from ..util.logger_config import get_logger + +# 配置日志 +logger = get_logger(__name__) + + + +class AuthDataTransformer: + """认证数据转换器""" + + def __init__(self, base_url: str = "http://lzwcai-demp-corp-manager:8086"): + """ + 初始化转换器 + + Args: + base_url: API基础URL,默认为 http://lzwcai-demp-corp-manager:8086 + """ + self.base_url = base_url.rstrip('/') + self.session = requests.Session() + + # 设置默认请求头 + self.session.headers.update({ + 'User-Agent': 'Python-API-Client/1.0.0', + 'Accept': '*/*', + 'Connection': 'keep-alive' + }) + + def get_transformed_auth_data(self, user_id: str, business_system_id: str) -> Optional[Dict[Any, Any]]: + """ + 获取并转换认证数据 + + Args: + user_id: 用户ID + business_system_id: 业务系统ID + + Returns: + 转换后的认证数据JSON,失败时返回None + """ + try: + # 1. 获取原始认证信息 + raw_data = self._get_raw_auth_info(user_id, business_system_id) + if not raw_data: + return None + + # 2. 处理bizSysConfig并获取API详情 + self._process_biz_sys_config(raw_data) + + # 3. 转换数据结构 + transformed_data = self._transform_data_structure(raw_data) + + return transformed_data + + except Exception as e: + logger.error(f"获取转换认证数据时发生错误: {e}") + return None + + def _get_raw_auth_info(self, user_id: str, business_system_id: str) -> Optional[Dict[Any, Any]]: + """ + 获取原始认证信息 + + Args: + user_id: 用户ID + business_system_id: 业务系统ID + + Returns: + 原始API响应数据,失败时返回None + """ + # url = f"{self.base_url}/system/mcpServer/auth/info/{user_id}/{business_system_id}" + url = f"http://lzwcai-demp-corp-manager:8086/system/mcpServer/auth/info/{user_id}/{business_system_id}" + + try: + response = self.session.get(url, timeout=30) + + if response.status_code == 200: + try: + return response.json() + except json.JSONDecodeError: + logger.error(f"响应不是有效的JSON格式: {response.text}") + return None + else: + logger.error(f"请求失败,状态码: {response.status_code}, 响应: {response.text}") + return None + + except requests.exceptions.RequestException as e: + logger.error(f"请求异常: {e}") + return None + + def _process_biz_sys_config(self, response_data: Dict[Any, Any]) -> None: + """ + 处理bizSysConfig,解析loginApiId并获取API详情 + + Args: + response_data: API响应数据 + """ + try: + data = response_data.get("data", {}) + if not data: + return + + biz_sys_config_str = data.get("bizSysConfig") + if not biz_sys_config_str: + return + + try: + biz_sys_config = json.loads(biz_sys_config_str) + login_api_id = biz_sys_config.get("loginApiId") + logger.info(f"登录APIID: {login_api_id}") + logger.debug(f"获取API详情...", get_business_api_details) + if login_api_id and get_business_api_details: + try: + api_details = get_business_api_details([int(login_api_id)]) + logger.debug(f"API详情: {api_details}") + if api_details and len(api_details) > 0: + data["apiItemDetail"] = api_details[0] + except Exception as e: + logger.error(f"获取API详情时发生错误: {e}") + + except json.JSONDecodeError as e: + logger.error(f"解析bizSysConfig JSON失败: {e}") + + except Exception as e: + logger.error(f"处理bizSysConfig时发生错误: {e}") + + def _transform_data_structure(self, result: Dict[Any, Any]) -> Optional[Dict[Any, Any]]: + """ + 将原始数据结构转换为新的数据结构 + + Args: + result: 原始数据 + + Returns: + 转换后的新数据结构,失败时返回None + """ + try: + data = result.get("data", {}) + if not data: + return None + + # 解析userAuthConfig + user_auth_config = self._safe_json_parse(data.get("userAuthConfig", "{}")) + + # 解析bizSysConfig + biz_sys_config = self._safe_json_parse(data.get("bizSysConfig", "{}")) + + # 获取apiItemDetail + api_item_detail = data.get("apiItemDetail", {}) + + # 构建新的数据结构 + new_data = { + "authType": user_auth_config.get("authType"), + "name": biz_sys_config.get("name"), + "apiKey": user_auth_config.get("apiKey"), + "apiVO": self._build_api_vo(user_auth_config, biz_sys_config, api_item_detail) + } + + return { + "code": result.get("code", 200), + "msg": result.get("msg", "成功"), + "data": new_data + } + + except Exception as e: + logger.error(f"转换数据结构时发生错误: {e}") + return None + + def _safe_json_parse(self, json_str: str) -> Dict[Any, Any]: + """ + 安全解析JSON字符串 + + Args: + json_str: JSON字符串 + + Returns: + 解析后的字典,失败时返回空字典 + """ + try: + return json.loads(json_str) if json_str else {} + except json.JSONDecodeError: + return {} + + def _build_api_vo(self, user_auth_config: Dict[Any, Any], biz_sys_config: Dict[Any, Any], api_item_detail: Dict[Any, Any]) -> Optional[Dict[Any, Any]]: + """ + 构建apiVO对象 + + Args: + user_auth_config: 用户认证配置 + biz_sys_config: 业务系统配置 + api_item_detail: API详情 + + Returns: + apiVO对象 + """ + if not biz_sys_config: + return None + + # 获取dynamicValues和paramMappings + dynamic_values = user_auth_config.get("dynamicValues", {}) + account_config = biz_sys_config.get("accountConfig", {}) + param_mappings = account_config.get("paramMappings", []) + + # 预处理accountConfig - 构建为对象然后转换为JSON字符串 + processed_account_config = {} + if param_mappings: + processed_account_config = {"parametersBody": []} + + for param_mapping in param_mappings: + param_name = param_mapping.get("paramName") + if param_name: + param_value = dynamic_values.get(param_name, param_mapping.get("defaultValue", "")) + + processed_account_config["parametersBody"].append({ + "paramName": param_name, + "defaultValue": param_value, + "requestType": param_mapping.get("requestType", "form") + }) + + # 构建tcapabilityApiVO + tcapability_api_vo = self._build_tcapability_api_vo(api_item_detail) + + return { + "accountConfig": json.dumps(processed_account_config, ensure_ascii=False), # 转换为JSON字符串 + "tokenPath": biz_sys_config.get("tokenPath"), + "tcapabilityApiVO": tcapability_api_vo # 放在apiVO里面 + } + + def _build_tcapability_api_vo(self, api_item_detail: Dict[Any, Any]) -> Optional[Dict[Any, Any]]: + """ + 构建tcapabilityApiVO对象 + + Args: + api_item_detail: API详情 + + Returns: + tcapabilityApiVO对象 + """ + if not api_item_detail: + return None + + # 复制API详情并重命名parameters为apiParameterList + tcapability_api_vo = api_item_detail.copy() + if "parameters" in tcapability_api_vo: + tcapability_api_vo["apiParameterList"] = tcapability_api_vo.pop("parameters") + + return tcapability_api_vo + + +# 便捷函数 +def get_auth_data(user_id: str, business_system_id: str, base_url: str = "http://lzwcai-demp-corp-manager:8086") -> Optional[Dict[Any, Any]]: + """ + 便捷函数:获取转换后的认证数据 + + Args: + user_id: 用户ID + business_system_id: 业务系统ID + base_url: API基础URL,默认为 http://lzwcai-demp-corp-manager:8086 + + Returns: + 转换后的认证数据JSON,失败时返回None + + Example: + >>> auth_data = get_auth_data("447", "1952255539442741249") + >>> if auth_data: + ... print(f"认证类型: {auth_data['data']['authType']}") + """ + transformer = AuthDataTransformer(base_url) + return transformer.get_transformed_auth_data(user_id, business_system_id) + + +if __name__ == "__main__": + # 测试代码 + from ..util.logger_config import setup_logging + import logging + setup_logging(log_level=logging.INFO) + result = get_auth_data("447", "1957354824118095874") + if result: + logger.info("=== 转换后的认证数据 ===") + logger.info(json.dumps(result, indent=2, ensure_ascii=False)) + else: + logger.error("获取认证数据失败") diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/plugin_base.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/plugin_base.py new file mode 100644 index 0000000..bea30c0 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/plugin_base.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod + + +class ToolPlugin(ABC): + """ + 工具插件基类,所有工具插件需继承并实现相关方法 + """ + + @abstractmethod + def register(self, server): + """注册插件到 Server""" + pass + + @abstractmethod + def unregister(self, server): + """从 Server 注销插件""" + pass + + @abstractmethod + def refresh(self, config): + """根据新配置刷新插件""" + pass diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/create_mcp.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/create_mcp.py new file mode 100644 index 0000000..18c9d55 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/create_mcp.py @@ -0,0 +1,788 @@ +""" +MCP服务器创建和管理模块 + +这是lzwcai-mcp-dyntoolapi项目的核心模块,负责: +1. 创建和配置MCP服务器 +2. 动态加载业务API配置 +3. 注册API工具插件 +4. 处理工具调用请求 +5. 支持配置热加载 +6. 支持多租户场景(内存模式) + +主要功能: +- 从业务平台获取API配置 +- 将API配置转换为MCP工具 +- 处理认证和参数验证 +- 支持多种传输方式(stdio、SSE) +- 支持两种配置模式:文件模式和内存模式 + +配置模式说明: +1. 内存模式(configMode=memory,默认): + - 根据businessUuid创建变量business{businessUuid}存储配置 + - 配置存储在内存中,不写入本地文件 + - 支持多个租户共享同一个包实例 + - 适用于多租户SaaS场景 + +2. 文件模式(configMode=file): + - 从业务平台获取配置后保存到本地api_config.json文件 + - 支持配置文件变更热加载 + - 适用于单租户场景 + +环境变量: +- configMode: 配置模式(file/memory),默认为memory +- businessUuid: 业务UUID(内存模式必需) +- bizSysApiIds: API ID列表 +- ENABLE_CONFIG_WATCH: 是否启用配置热加载(仅文件模式) + +作者: lzwcai +版本: 1.1.0 +""" + +import anyio +import mcp.types as types +from mcp.server.lowlevel import Server +import json +import sys +import os +import threading +import time +import re +import uuid + +# 导入核心模块 +from .core.api_base import ApiBase +import mcp.types as types +from .core.api_auth_service import AuthService +from .core.plugin_base import ToolPlugin + +# 导入业务工具模块 +from .business.business_util import ( + fill_default_values_by_schema, # 参数默认值填充 + check_required_arguments, # 必填参数检查 +) +from .business.get_business_api import get_business_api_config # 业务API配置获取 + +# 导入工具模块 +from .util.logger_config import get_logger, setup_logging + +# 获取日志器实例 +logger = get_logger(__name__) + +# ==================== 多租户配置存储 ==================== +# 用于存储多个租户的配置(内存模式) +# key格式: business{businessUuid}, value: 配置字典 +business_configs = {} + + +def load_api_configs(): + """ + 加载API配置的核心函数 + + 支持两种配置模式: + + 模式一 - 内存模式(configMode=memory,默认): + 1. 根据环境变量businessUuid创建变量名:business{businessUuid} + 2. 从业务平台获取配置后存储在内存中(business_configs字典) + 3. 如果内存中已有配置则直接使用,否则从业务平台获取 + 4. 不写入本地文件,支持多租户场景 + + 模式二 - 文件模式(configMode=file): + 1. 从业务平台动态获取最新配置(通过get_business_api_config) + 2. 如果网络获取失败,则从本地api_config.json文件加载备份配置 + 3. 成功获取后保存到本地文件作为备份 + + 环境变量: + - configMode: 配置模式,"file"(文件模式)或 "memory"(内存模式),默认为memory + - businessUuid: 业务UUID,仅在内存模式下使用,用于区分不同租户 + - bizSysApiIds: 指定要加载的API ID列表 + + 返回: + dict: 包含完整API配置的字典,格式如下: + { + "packageName": "服务包名", + "version": "版本号", + "description": "服务描述", + "apiConfig": [API配置列表] + } + + 异常处理: + - 网络获取失败时自动降级(文件模式降级到本地文件,内存模式报错) + - 详细记录所有错误信息用于调试 + """ + global business_configs + + # 获取配置模式(默认为内存模式) + config_mode = os.getenv('configMode', 'memory').lower() + logger.info(f"配置模式: {config_mode}") + + # 获取当前模块所在目录,用于定位配置文件 + current_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(current_dir, "api_config.json") + + # ==================== 模式一:内存模式(多租户支持,默认) ==================== + if config_mode == 'memory': + logger.info("使用内存模式加载配置(多租户支持)") + + # 获取业务UUID,如果不存在则自动生成一个 + business_uuid = os.getenv('businessUuid') + if not business_uuid: + # 生成随机UUID(使用uuid4,完全随机) + business_uuid = str(uuid.uuid4()) + logger.warning(f"环境变量businessUuid未设置,已自动生成随机UUID: {business_uuid}") + # 可选:将生成的UUID设置回环境变量,供后续使用 + os.environ['businessUuid'] = business_uuid + else: + logger.info(f"使用环境变量提供的businessUuid: {business_uuid}") + + # 构建配置变量名 + config_key = f"business{business_uuid}" + logger.info(f"租户配置变量名: {config_key}") + + # 检查内存中是否已有该租户的配置 + if config_key in business_configs: + logger.info(f"从内存中获取租户 {business_uuid} 的配置") + return business_configs[config_key] + + # 内存中没有,从业务平台获取 + logger.info(f"内存中没有租户 {business_uuid} 的配置,开始从业务平台获取...") + + try: + # 从环境变量获取API ID列表 + api_ids = [] + biz_sys_api_ids = os.getenv('bizSysApiIds') + logger.debug(f"已获取环境变量bizSysApiIds: {biz_sys_api_ids}") + + if biz_sys_api_ids: + try: + ids_str = biz_sys_api_ids.strip('[]') + api_ids = [] + for id_part in ids_str.split(','): + id_clean = id_part.strip().strip('"\'') + if id_clean: + try: + api_id = int(id_clean) + api_ids.append(api_id) + except ValueError: + logger.warning(f"无法将 '{id_clean}' 转换为整数,跳过此项") + continue + logger.info(f"从环境变量bizSysApiIds获取到API IDs: {api_ids}") + except (ValueError, AttributeError) as e: + logger.warning(f"解析环境变量bizSysApiIds失败,使用默认值: {str(e)}") + else: + logger.info("未找到环境变量bizSysApiIds,使用默认API IDs") + + # 从业务平台获取配置 + logger.info(f"调用get_business_api_config获取配置,API IDs: {api_ids}") + config = get_business_api_config(api_ids) + logger.info(f"成功获取业务API配置,包含 {len(config.get('apiConfig', []))} 个API配置") + + # 存储到内存中 + business_configs[config_key] = config + logger.info(f"配置已存储到内存变量: {config_key}") + logger.info(f"当前内存中共有 {len(business_configs)} 个租户配置") + + return config + + except Exception as e: + logger.error(f"获取业务API配置失败: {str(e)}") + error_msg = f"内存模式下无法获取租户 {business_uuid} 的配置: {str(e)}" + raise Exception(error_msg) + + # ==================== 模式二:文件模式(单租户) ==================== + elif config_mode == 'file': + logger.info("使用文件模式加载配置(单租户)") + + try: + # 从环境变量获取API ID列表 + # 支持格式: "[1932682081958830081,1932682082285985793]" 或 "1932682081958830081,1932682082285985793" + api_ids = [] # 默认空列表 + + # 尝试从环境变量获取bizSysApiIds + biz_sys_api_ids = os.getenv('bizSysApiIds') + logger.debug(f"已获取环境变量bizSysApiIds: {biz_sys_api_ids}") + + if biz_sys_api_ids: + try: + # 解析环境变量中的字符串,支持多种格式 + # 格式1: [1932682081958830081,1932682082285985793] + # 格式2: 1932682081958830081,1932682082285985793 + # 格式3: ["1932682081958830081","1932682082285985793"] + # 格式4: "1932682081958830081","1932682082285985793" + ids_str = biz_sys_api_ids.strip('[]') # 移除方括号 + + # 分割并处理每个ID,自动转换字符串数字为整数 + api_ids = [] + for id_part in ids_str.split(','): + id_clean = id_part.strip().strip('"\'') # 移除空格和引号 + if id_clean: # 确保不是空字符串 + try: + # 尝试转换为整数,支持字符串数字自动转换 + api_id = int(id_clean) + api_ids.append(api_id) + except ValueError: + logger.warning(f"无法将 '{id_clean}' 转换为整数,跳过此项") + continue + + logger.info(f"从环境变量bizSysApiIds获取到API IDs: {api_ids}") + except (ValueError, AttributeError) as e: + logger.warning(f"解析环境变量bizSysApiIds失败,使用默认值: {str(e)}") + logger.warning(f"环境变量值: {biz_sys_api_ids}") + else: + logger.info("未找到环境变量bizSysApiIds,使用默认API IDs") + + logger.info(f"调用get_business_api_config获取配置,API IDs: {api_ids}") + + # 从业务平台获取最新配置 + config = get_business_api_config(api_ids) + logger.info(f"成功获取业务API配置,包含 {len(config.get('apiConfig', []))} 个API配置") + + # 将获取的配置保存到本地文件作为备份 + logger.info(f"保存配置到文件: {config_path}") + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, ensure_ascii=False, indent=2) + logger.info("配置文件保存成功") + + return config + + except Exception as e: + logger.error(f"获取业务API配置失败: {str(e)}") + logger.info("尝试从本地配置文件加载...") + + # 降级处理:从本地文件加载配置 + try: + logger.debug(f"加载本地API配置文件: {config_path}") + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + logger.info(f"成功加载本地API配置文件,包含 {len(config.get('apiConfig', []))} 个API配置") + return config + except FileNotFoundError: + logger.error(f"本地API配置文件也未找到: {config_path}") + raise Exception(f"无法获取业务API配置且本地配置文件不存在: {str(e)}") + except json.JSONDecodeError as json_e: + logger.error(f"本地API配置文件JSON格式错误: {str(json_e)}") + raise Exception(f"无法获取业务API配置且本地配置文件格式错误: {str(e)}") + except Exception as file_e: + logger.error(f"加载本地API配置文件失败: {str(file_e)}") + raise Exception(f"无法获取业务API配置且加载本地配置文件失败: {str(e)}") + + # ==================== 无效配置模式 ==================== + else: + error_msg = f"无效的配置模式: {config_mode},请使用 'file' 或 'memory'" + logger.error(error_msg) + raise Exception(error_msg) + + +# ==================== MCP服务器初始化 ==================== + +logger.info("开始初始化 MCP 服务器") + +# 加载API配置(从业务平台或本地文件) +api_configs = load_api_configs() + +# 设置MCP服务器的默认配置值 +default_name = "lzwcai-mcp-dyntoolapi" # 默认服务器名称 +default_version = "1.0.0" # 默认版本号 +default_instructions = "动态业务API工具服务器" # 默认服务器描述 + +# 从加载的配置中获取服务器信息,如果不存在则使用默认值 +name = api_configs.get("packageName", default_name) +version = api_configs.get("version", default_version) +instructions = api_configs.get("description", default_instructions) + +logger.info(f"服务器配置 - 名称: {name}, 版本: {version}") +logger.debug(f"服务器说明: {instructions}") + +# 创建MCP服务器实例 +# 这个Server实例将处理所有的MCP协议通信 +app = Server( + name=name, # 服务器名称,用于客户端识别 + version=version, # 服务器版本,用于兼容性检查 + instructions=instructions, # 服务器描述,告诉客户端这个服务器的功能 +) + +# 初始化API基础服务 +# ApiBase负责管理所有的API配置和调用逻辑 +logger.debug("初始化 API 基础服务") +api_base = ApiBase(api_configs.get("apiConfig")) +logger.info(f"API 基础服务初始化完成,共 {api_base.config_count} 个API配置") + + +class ApiToolPlugin(ToolPlugin): + """ + API工具插件实现类 + + 这个类负责将业务API配置转换为MCP工具,并处理工具调用请求。 + 它继承自ToolPlugin基类,实现了插件的标准接口。 + + 主要功能: + 1. 将API配置转换为MCP工具定义 + 2. 处理工具列表请求(list_tools) + 3. 处理工具调用请求(call_tool) + 4. 支持插件的注册、注销和刷新 + + 属性: + api_base: ApiBase实例,管理所有API配置 + tools: 工具列表缓存(当前未使用) + """ + + def __init__(self, api_base): + """ + 初始化API工具插件 + + 参数: + api_base: ApiBase实例,包含所有API配置信息 + """ + self.api_base = api_base + self.tools = [] # 工具列表缓存(预留) + logger.debug(f"初始化 API 工具插件,API配置数量: {api_base.config_count}") + + def register(self, server): + """ + 向MCP服务器注册插件 + + 这个方法会向服务器注册两个处理器: + 1. list_tools: 返回可用工具列表 + 2. call_tool: 处理工具调用请求 + + 参数: + server: MCP服务器实例 + """ + @server.list_tools() + async def list_tools() -> list[types.Tool]: + """ + 处理工具列表请求 + + 当MCP客户端请求可用工具列表时,这个函数会被调用。 + 它会遍历所有API配置,为每个API创建一个MCP工具定义。 + + 返回: + list[types.Tool]: MCP工具定义列表 + """ + logger.debug("处理工具列表请求") + tools = [] + tools_configs = self.api_base.api_configs_map + + # 遍历所有API配置,创建工具定义 + for tool_config in tools_configs: + tool_name = tool_config["interfaceName"] # 工具名称(拼音格式) + logger.debug(f"注册工具: {tool_name}") + + # 创建MCP工具定义 + tools.append( + types.Tool( + name=tool_name, # 工具名称 + description=tool_config["schema_description"], # 工具描述(包含参数说明) + inputSchema=tool_config["schema"], # 输入参数的JSON Schema + ) + ) + + logger.info(f"返回工具列表,共 {len(tools)} 个工具") + return tools + + @server.call_tool() + async def fetch_tool( + name: str, arguments: dict + ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """ + 处理工具调用请求 + + 当MCP客户端调用某个工具时,这个函数会被调用。 + 它负责: + 1. 查找对应的API配置 + 2. 验证和处理输入参数 + 3. 调用实际的API接口 + 4. 返回格式化的结果 + + 参数: + name: 工具名称(对应API的interfaceName) + arguments: 工具调用参数(JSON对象) + + 返回: + list[types.TextContent]: 包含API调用结果的文本内容列表 + """ + logger.info(f"调用工具: {name}") + logger.debug(f"工具参数: {arguments}") + + # 查找对应的工具配置 + tool_config = None + for config in self.api_base.api_configs_map: + if config["interfaceName"] == name: + tool_config = config + break + + # 检查工具是否存在 + if tool_config is None: + logger.error(f"未找到工具: {name}") + return [types.TextContent(type="text", text=f"未找到工具: {name}")] + + logger.debug(f"找到工具配置: {tool_config.get('apiUrl', 'N/A')}") + + # ==================== 参数处理和验证 ==================== + logger.debug("开始参数处理和验证") + + # 保存原始参数用于调试 + original_args = arguments.copy() + + # 使用schema中的默认值补全缺失的参数 + arguments = fill_default_values_by_schema( + tool_config.get("schema", {}), arguments + ) + logger.debug(f"参数默认值补全完成,原始参数: {original_args}, 补全后: {arguments}") + + # 检查必填参数是否都已提供 + missing = check_required_arguments(tool_config.get("schema", {}), arguments) + if missing: + missing_str = ", ".join(missing) + logger.warning(f"缺少必填参数: {missing_str}") + return [ + types.TextContent( + type="text", + text=f"请补充以下必填参数:{missing_str}。填写后再试一次哦~ 如果有疑问请联系管理员。", + ) + ] + + # ==================== API接口调用 ==================== + logger.info(f"开始调用API接口: {tool_config.get('apiUrl', 'N/A')}") + try: + # 通过ApiBase调用实际的API接口 + result = await self.api_base.call_interface(tool_config, arguments) + logger.info("API调用成功") + logger.debug(f"API返回结果: {result}") + + # 将结果转换为JSON格式返回给客户端 + result_json = json.dumps(result, ensure_ascii=False, indent=2) + return [types.TextContent(type="text", text=result_json)] + + except Exception as e: + # 处理API调用异常 + logger.error(f"API调用失败: {str(e)}") + logger.debug("API调用异常详情:", exc_info=True) + error_msg = f"API调用失败: {str(e)}" + return [types.TextContent(type="text", text=error_msg)] + + def unregister(self, server): + """ + 从MCP服务器注销插件 + + 目前MCP Server不支持动态注销功能,这个方法预留给未来使用。 + + 参数: + server: MCP服务器实例(当前未使用) + """ + # 目前 Server 不支持动态注销,预留接口 + logger.debug("插件注销请求(当前不支持动态注销)") + pass + + def refresh(self, config): + """ + 刷新插件配置 + + 当API配置发生变化时(如热加载),这个方法会被调用来更新插件的配置。 + + 参数: + config: 新的配置字典,包含apiConfig字段 + """ + logger.info("刷新API工具插件配置") + # 重新创建ApiBase实例以使用新配置 + self.api_base = ApiBase(config.get("apiConfig")) + logger.info(f"插件配置刷新完成,共 {self.api_base.config_count} 个API配置") + + +# ==================== 插件注册 ==================== + +# 创建并注册API工具插件 +logger.info("注册API工具插件") +api_tool_plugin = ApiToolPlugin(api_base) # 创建插件实例 +api_tool_plugin.register(app) # 向MCP服务器注册插件 +logger.info("API工具插件注册完成") + + + +# ==================== MCP服务器启动函数 ==================== + +def main(port: int, transport: str) -> int: + """ + MCP服务器主启动函数 + + 根据指定的传输方式启动MCP服务器。支持两种传输方式: + 1. stdio: 标准输入输出传输(默认,用于命令行工具集成) + 2. sse: Server-Sent Events传输(用于Web集成) + + 参数: + port: 服务器端口号(仅在SSE模式下使用) + transport: 传输方式,"stdio" 或 "sse" + + 返回: + int: 退出状态码,0表示成功 + """ + + if transport == "sse": + # ==================== SSE传输模式 ==================== + logger.info(f"启动SSE传输模式,端口: {port}") + + # 导入SSE相关模块 + from mcp.server.sse import SseServerTransport + from starlette.applications import Starlette + from starlette.responses import Response + from starlette.routing import Mount, Route + + # 创建SSE传输实例 + sse = SseServerTransport("/messages/") + + async def handle_sse(request): + """ + 处理SSE连接请求 + + 这个函数处理来自Web客户端的SSE连接请求, + 建立双向通信流并运行MCP服务器。 + """ + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + # 运行MCP服务器,使用SSE流进行通信 + await app.run( + streams[0], streams[1], app.create_initialization_options() + ) + return Response() + + # 创建Starlette Web应用 + starlette_app = Starlette( + debug=True, # 开启调试模式 + routes=[ + # SSE连接端点 + Route("/sse", endpoint=handle_sse, methods=["GET"]), + # 消息处理端点 + Mount("/messages/", app=sse.handle_post_message), + ], + ) + + # 使用uvicorn启动Web服务器 + import uvicorn + logger.info(f"启动Web服务器,监听 0.0.0.0:{port}") + uvicorn.run(starlette_app, host="0.0.0.0", port=port) + + else: + # ==================== STDIO传输模式 ==================== + logger.info("启动STDIO传输模式") + + # 导入stdio传输模块 + from mcp.server.stdio import stdio_server + + async def arun(): + """ + 异步运行MCP服务器 + + 使用标准输入输出流与客户端通信, + 这是MCP协议的标准传输方式。 + """ + async with stdio_server() as streams: + # 运行MCP服务器,使用stdio流进行通信 + await app.run( + streams[0], streams[1], app.create_initialization_options() + ) + + # 启动异步事件循环 + anyio.run(arun) + + return 0 + + +# ==================== 工具函数 ==================== + +def save_to_json_file(data, file_path="output/test_data.json"): + """ + 将输入数据保存为JSON文件 + + 这是一个通用的数据保存工具函数,主要用于调试和数据持久化。 + + 参数: + data: 要保存的数据(字典、列表或其他可JSON序列化的对象) + file_path: 要保存的文件路径,默认保存到output目录 + + 返回: + bool: 操作成功返回True,失败返回False + + 特性: + - 自动创建目录(如果不存在) + - 使用UTF-8编码确保中文正确显示 + - 格式化输出(缩进2个空格) + """ + try: + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + # 保存数据到JSON文件 + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + logger.info(f"数据已成功保存到 {file_path}") + return True + except Exception as e: + logger.error(f"保存JSON文件时出错: {str(e)}") + return False + + +def refresh_api_configs(): + """ + 刷新API配置和工具注册(热加载功能) + + 这个函数实现了配置的热加载功能,支持两种模式: + - 文件模式:当检测到配置文件变化时会被调用 + - 内存模式:强制重新从业务平台获取配置并更新内存 + + 全局变量更新: + - api_configs: 重新加载的API配置 + - name, version, instructions: 服务器基本信息 + - api_base: 重新创建的API基础服务实例 + - api_tool_plugin: 刷新插件配置 + - business_configs: 内存模式下更新租户配置 + + 注意: + 这个函数修改全局变量,在多线程环境中需要注意线程安全。 + """ + global api_configs, name, version, instructions, api_base, api_tool_plugin, business_configs + + logger.info("开始刷新API配置...") + + # 获取配置模式 + config_mode = os.getenv('configMode', 'memory').lower() + + # 内存模式下需要清除当前租户的缓存配置,强制重新获取 + if config_mode == 'memory': + business_uuid = os.getenv('businessUuid') + if business_uuid: + config_key = f"business{business_uuid}" + if config_key in business_configs: + logger.info(f"清除租户 {business_uuid} 的缓存配置") + del business_configs[config_key] + + # 重新加载API配置 + api_configs = load_api_configs() + + # 更新服务器基本信息 + name = api_configs.get("packageName", default_name) + version = api_configs.get("version", default_version) + instructions = api_configs.get("description", default_instructions) + + # 重新创建API基础服务 + api_base = ApiBase(api_configs.get("apiConfig")) + + # 刷新插件配置 + api_tool_plugin.refresh(api_configs) + + logger.info(f"API配置已热加载并刷新(模式:{config_mode})") + + +# ==================== 配置热加载功能 ==================== + +def watch_config_file(interval=2): + """ + 配置文件变更监控函数(仅文件模式) + + 这个函数在后台线程中运行,定期检查配置文件的修改时间。 + 当检测到文件变更时,自动触发配置热加载。 + + 注意: + - 仅在文件模式(configMode=file)下有效 + - 内存模式下此函数会直接返回,不进行监控 + + 参数: + interval: 检查间隔时间(秒),默认2秒 + + 特性: + - 轮询方式检测文件变更 + - 异常安全,不会因为单次错误而停止监控 + - 在守护线程中运行,不会阻止程序退出 + + 注意: + 这个函数会无限循环运行,直到程序退出。 + """ + # 检查配置模式,内存模式下不需要监控文件 + config_mode = os.getenv('configMode', 'memory').lower() + if config_mode != 'file': + logger.info(f"当前为 {config_mode} 模式,无需监控配置文件") + return + + current_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(current_dir, "api_config.json") + + try: + # 获取初始修改时间 + last_mtime = os.path.getmtime(config_path) + logger.debug(f"开始监控配置文件: {config_path}") + except OSError: + logger.warning(f"配置文件不存在,跳过热加载监控: {config_path}") + return + + # 无限循环监控文件变更 + while True: + try: + # 获取当前修改时间 + mtime = os.path.getmtime(config_path) + + # 检查是否有变更 + if mtime != last_mtime: + logger.info("检测到api_config.json变更,自动热加载...") + refresh_api_configs() + last_mtime = mtime + + except Exception as e: + logger.error(f"配置热加载异常: {e}") + + # 等待下次检查 + time.sleep(interval) + + +# ==================== 主启动函数 ==================== + +def run_main(): + """ + 程序主启动函数 + + 这是整个MCP服务器的启动入口,负责: + 1. 配置日志系统(MCP模式下禁用控制台输出) + 2. 可选启动配置文件监控线程(通过环境变量ENABLE_CONFIG_WATCH控制) + 3. 启动MCP服务器 + + 配置说明: + - 使用stdio传输方式(标准MCP协议) + - 端口8000(仅在SSE模式下使用) + - 配置热加载功能默认关闭,可通过环境变量ENABLE_CONFIG_WATCH=true启用 + """ + + # 在MCP模式下,禁用控制台日志输出,避免干扰stdio通信 + # 只输出到文件,确保MCP协议通信不被日志干扰 + setup_logging(console_output=False, file_output=True) + + # 检查是否启用配置文件监控(默认关闭) + # 通过环境变量ENABLE_CONFIG_WATCH=true来启用 + enable_watch = os.getenv('ENABLE_CONFIG_WATCH', 'false').lower() in ['true', '1', 'yes'] + + if enable_watch: + # 启动配置文件监听线程(守护线程) + # 守护线程会在主程序退出时自动结束 + config_watcher = threading.Thread(target=watch_config_file, daemon=True) + config_watcher.start() + logger.info("配置文件监控线程已启动") + else: + logger.info("配置文件监控功能已禁用(如需启用,请设置环境变量ENABLE_CONFIG_WATCH=true)") + + # 启动MCP服务器 + # 使用stdio传输方式,这是MCP协议的标准传输方式 + # 如果需要Web集成,可以改为 transport="sse" + main(port=8000, transport="stdio") + + +# ==================== 程序入口 ==================== + +if __name__ == "__main__": + """ + 直接运行此模块时的入口点 + + 通常情况下,这个模块会被main.py调用, + 但也支持直接运行进行测试。 + """ + run_main() + + # 以下是测试代码,正常运行时被注释掉 + # from core.api_auth_service import test_auth_service + # asyncio.run(test_auth_service()) diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__init__.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..882a706 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc new file mode 100644 index 0000000..c149056 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc new file mode 100644 index 0000000..718f245 Binary files /dev/null and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/api_helper.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/api_helper.py new file mode 100644 index 0000000..cfc36ae --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/api_helper.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +""" +API助手工具包 + +这个工具包集成了三个核心功能: +1. 获取接口参数信息 +2. 调用指定接口API +3. 获取用户授权Token + +使用lzwcai_mcp_dyntoolapi包中的方法,提供简洁易用的API操作接口。 + +作者: lzwcai +版本: 1.0.0 +""" + +import json +import asyncio +from typing import Dict, Any, Optional +import os +# 引入lzwcai_mcp_dyntoolapi包中的核心方法 +from lzwcai_mcp_api_converter.src.business.get_business_api import get_business_api_details +from lzwcai_mcp_api_converter.src.core.api_base import ApiBase +from lzwcai_mcp_api_converter.src.core.core_server import call_api +from lzwcai_mcp_api_converter.src.core.get_auth import get_auth_data +from lzwcai_mcp_api_converter.src.core.api_auth_service import AuthService +from lzwcai_mcp_api_converter.src.util.logger_config import get_logger + +# 配置日志 +logger = get_logger(__name__) + +# 全局AuthService实例(单例模式) +_auth_service_instance = None + + +def get_auth_service() -> AuthService: + """ + 获取全局共享的AuthService实例 + + 使用单例模式确保整个应用中只有一个AuthService实例, + 这样可以复用认证状态和连接,提高性能。 + + Returns: + AuthService: 全局共享的AuthService实例 + + 使用示例: + >>> auth_service = get_auth_service() + >>> token_result = await auth_service.authorize_request(user_id, business_system_id) + """ + global _auth_service_instance + if _auth_service_instance is None: + logger.info("创建新的AuthService实例") + _auth_service_instance = AuthService() + return _auth_service_instance + + +# ==================== 功能1: 获取接口参数信息 ==================== + +def get_api_parameters_info(api_id: int, auth_token: str = None) -> Optional[Dict[str, Any]]: + """ + 获取指定接口ID的参数信息 + + 功能说明: + - 根据接口ID获取接口的详细配置信息 + - 生成JSON Schema格式的参数定义 + - 返回包含参数列表、Schema、描述等完整信息的配置对象 + + Args: + api_id: 接口ID,例如 1957355058730684417 + auth_token: 认证token,如果不提供则使用默认token + + Returns: + Dict[str, Any]: 处理后的接口配置对象,包含: + - interfaceName: 拼音格式的接口名称 + - schema: JSON Schema格式的参数定义 + - schema_description: 完整的参数描述 + - 原始API配置的所有其他字段 + + Raises: + Exception: 当获取接口信息失败时抛出 + + 使用示例: + >>> api_info = get_api_parameters_info(1957355058730684417) + >>> print(api_info['interfaceName']) # tool_AppZhangHaoMiMaDengLu + >>> print(api_info['schema']) # JSON Schema对象 + """ + try: + logger.info(f"开始获取接口ID {api_id} 的参数信息...") + + # 步骤1: 调用业务接口获取API详情 + api_details = get_business_api_details([api_id], auth_token) + + if not api_details: + logger.error(f"未找到接口ID {api_id} 的详情") + return None + + # 获取第一个(也是唯一一个)API详情 + api_detail = api_details[0] + + # 步骤2: 使用ApiBase处理API配置,生成Schema + api_base = ApiBase([api_detail]) + + # 获取处理后的配置 + processed_configs = api_base.api_configs_map + if not processed_configs: + logger.error("API配置处理失败") + return None + + processed_config = processed_configs[0] + + logger.info(f"成功获取接口 '{processed_config.get('interfaceName', 'N/A')}' 的参数信息") + return processed_config + + except Exception as e: + logger.error(f"获取接口参数信息失败: {str(e)}") + raise + + +# ==================== 功能2: 调用指定接口API ==================== + +async def call_api_by_id(api_id: int, request_params: Dict[str, Any], auth_token: str = None) -> Optional[Dict[str, Any]]: + """ + 根据接口ID调用API + + 功能说明: + - 根据接口ID获取API配置 + - 智能处理认证(先尝试认证调用,失败后尝试无认证调用) + - 发送HTTP请求并返回响应结果 + + Args: + api_id: 接口ID,例如 1957355058730684417 + request_params: 请求参数,格式如: + { + "body": { + "username": "test_user", + "password": "test_password" + }, + "lzwcaiConfig": { + "userId": "test_user_id" + } + } + auth_token: 认证token,如果不提供则使用默认token + + Returns: + Dict[str, Any]: API调用结果 + + Raises: + Exception: 当API调用失败时抛出 + + 使用示例: + >>> params = { + ... "body": {"username": "wangpeng1", "password": "Wp147258"}, + ... "lzwcaiConfig": {"userId": "447"} + ... } + >>> result = await call_api_by_id(1957355058730684417, params) + >>> print(result['code']) # 200 + """ + try: + logger.info(f"开始调用接口ID {api_id}...") + + # 步骤1: 获取API配置 + api_details = get_business_api_details([api_id], auth_token) + + if not api_details: + logger.error(f"未找到接口ID {api_id} 的详情") + return None + + # 获取第一个(也是唯一一个)API详情 + api_detail = api_details[0] + + # 步骤2: 使用ApiBase处理API配置 + api_base = ApiBase([api_detail]) + processed_configs = api_base.api_configs_map + + if not processed_configs: + logger.error("API配置处理失败") + return None + + processed_config = processed_configs[0] + + # 步骤3: 调用API + logger.info(f"调用接口: {processed_config.get('interfaceName', 'N/A')}") + logger.info(f"接口地址: {processed_config.get('apiUrl', 'N/A')}") + logger.info(f"请求方法: {processed_config.get('method', 'N/A')}") + + # 判断是否需要认证 + need_auth = processed_config.get('authenticationRequired', 0) == 1 + logger.info(f"需要认证: {need_auth}") + + # 尝试调用API(智能认证处理) + try: + result = await call_api(processed_config, request_params, need_auth=need_auth) + except Exception as api_error: + # 如果认证失败,尝试不需要认证的方式调用 + if "认证失败" in str(api_error) or "鉴权令牌失败" in str(api_error): + logger.warning(f"认证调用失败,尝试无认证调用: {str(api_error)}") + try: + result = await call_api(processed_config, request_params, need_auth=False) + logger.info("无认证调用成功") + except Exception as no_auth_error: + logger.error(f"无认证调用也失败: {str(no_auth_error)}") + # 返回模拟的错误响应,展示调用过程 + result = { + "error": "API调用失败", + "auth_error": str(api_error), + "no_auth_error": str(no_auth_error), + "note": "这是一个模拟的错误响应,展示了API调用的完整过程" + } + else: + raise api_error + + logger.info("API调用成功") + return result + + except Exception as e: + logger.error(f"调用API失败: {str(e)}") + raise + + +# ==================== 功能3: 获取用户授权Token ==================== + +def get_auth_info(user_id: str, business_system_id: str) -> Optional[Dict[str, Any]]: + """ + 获取用户认证信息 + + 功能说明: + - 获取用户在指定业务系统中的认证配置 + - 包含认证类型、API密钥、登录接口等信息 + + Args: + user_id: 用户ID,例如 "447" + business_system_id: 业务系统ID,例如 "1957354824118095874" + + Returns: + Dict[str, Any]: 认证信息,包括: + - authType: 认证类型 + - name: 业务系统名称 + - apiKey: API密钥 + - apiVO: API配置信息 + + Raises: + Exception: 当获取认证信息失败时抛出 + + 使用示例: + >>> auth_info = get_auth_info("447", "1957354824118095874") + >>> print(auth_info['data']['authType']) # 1 + """ + try: + logger.info(f"开始获取用户 {user_id} 在业务系统 {business_system_id} 的认证信息...") + + # 调用get_auth_data获取认证数据 + auth_data = get_auth_data(user_id, business_system_id) + + if not auth_data: + logger.error("未能获取到认证数据") + return None + + logger.info("成功获取认证信息") + return auth_data + + except Exception as e: + logger.error(f"获取认证信息失败: {str(e)}") + raise + + +async def get_business_token(user_id: str, business_system_id: str,persist_token: bool = False) -> Optional[Dict[str, Any]]: + """ + 获取用户在指定业务系统的Token + + 功能说明: + - 使用全局共享的AuthService实例获取Token + - 复用认证状态,提高性能 + + Args: + user_id: 用户ID + business_system_id: 业务系统ID + + Returns: + Dict[str, Any]: Token获取结果 + + Raises: + Exception: 当获取Token失败时抛出 + + 使用示例: + >>> token_result = await get_business_token("447", "1957354824118095874") + >>> print(token_result.get('success')) # True + """ + try: + logger.info(f"开始获取用户 {user_id} 在业务系统 {business_system_id} 的Token...") + + # 使用全局共享的AuthService实例获取Token + auth_service = get_auth_service() + token_result = await auth_service.authorize_request(user_id, business_system_id,persist_token=persist_token) + + logger.info("Token获取完成") + return token_result + + except Exception as e: + logger.error(f"获取业务Token失败: {str(e)}") + raise + + +# ==================== 便捷工具函数 ==================== + +def save_result_to_file(data: Dict[str, Any], filename: str) -> None: + """ + 保存结果到JSON文件 + + Args: + data: 要保存的数据 + filename: 文件名 + """ + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + logger.info(f"💾 数据已保存到: {filename}") + except Exception as e: + logger.error(f"保存文件失败: {str(e)}") + + +def print_api_info(api_info: Dict[str, Any]) -> None: + """ + 格式化打印API信息 + + Args: + api_info: API信息字典 + """ + logger.info("\n" + "="*60) + logger.info("📋 接口信息:") + logger.info(f"接口名称: {api_info.get('interfaceName', 'N/A')}") + logger.info(f"业务描述: {api_info.get('businessPrompts', 'N/A')}") + logger.info(f"接口地址: {api_info.get('apiUrl', 'N/A')}") + logger.info(f"请求方法: {api_info.get('method', 'N/A')}") + logger.info(f"需要认证: {'是' if api_info.get('authenticationRequired', 0) == 1 else '否'}") + logger.info("="*60) + + +def print_api_result(result: Dict[str, Any]) -> None: + """ + 格式化打印API调用结果 + + Args: + result: API调用结果 + """ + logger.info("\n" + "="*60) + logger.info("📥 API调用结果:") + if result.get('success') is not None: + status = "✅ 成功" if result.get('success') else "❌ 失败" + logger.info(f"状态: {status}") + if result.get('code') is not None: + logger.info(f"状态码: {result.get('code')}") + if result.get('msg'): + logger.info(f"消息: {result.get('msg')}") + logger.info("="*60) + + +# ==================== 便捷使用示例 ==================== + +async def demo_usage(): + """ + 使用示例演示 + + 展示如何使用工具包的三个核心功能 + """ + logger.info("🚀 API助手工具包使用示例") + + # 测试数据 + # api_id = 1957355058730684417 + user_id = "2" + business_system_id = "1922839602141347842" + # os.environ["lzwcai_mcp_dyntoolapi_auth_url"] = ( + # "http://lzwcai-demp-corp-manager:8086/system/mcpServer/bizSys/api/getByIds" + # ) + try: + # get_auth_info_data=await get_business_token(user_id, business_system_id) + # print(f"获取接口参数信息: {get_auth_info_data}") + # # 功能1: 获取接口参数信息 + # print(f"\n📋 功能1: 获取接口ID {api_id} 的参数信息") + # api_info = get_api_parameters_info(api_id) + # if api_info: + # print(f"✅ 接口名称: {api_info.get('interfaceName', 'N/A')}") + # print(f"✅ 业务描述: {api_info.get('businessPrompts', 'N/A')}") + + # # 功能2: 调用API + # print(f"\n🔧 功能2: 调用接口ID {api_id}") + # request_params = { + # "body": { + # "username": "wangpeng1", + # "password": "Wp147258" + # }, + # "lzwcaiConfig": { + # "userId": user_id + # } + # } + # api_result = await call_api_by_id(api_id, request_params) + # if api_result: + # print(f"✅ API调用成功: {api_result.get('msg', 'N/A')}") + + # # 功能3: 获取Token + # logger.info(f"\n🔑 功能3: 获取用户 {user_id} 的Token") + token_result = await get_business_token(user_id, business_system_id) + if token_result and token_result.get('success'): + logger.info(f"✅ Token获取成功: {token_result.get('msg', 'N/A')}") + + logger.info(f"\n🎉 所有功能演示完成!") + + except Exception as e: + logger.error(f"❌ 演示过程中发生错误: {str(e)}") + + +if __name__ == "__main__": + # 运行使用示例 + asyncio.run(demo_usage()) diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/logger_config.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/logger_config.py new file mode 100644 index 0000000..a7b1b99 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/logger_config.py @@ -0,0 +1,554 @@ +""" +统一日志配置模块 + +这个模块提供了整个项目的统一日志配置和管理功能,确保所有组件使用一致的日志格式和输出方式。 + +主要功能: +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_mcp_api_converter.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: + # 自动确定日志文件路径:项目根目录 + 默认文件名 + project_root = cls._get_project_root() + log_file = project_root / 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) + + # ==================== 包日志器配置 ==================== + + # 获取包的顶层日志器,而不是根日志器 + package_logger = logging.getLogger('lzwcai_mcp_api_converter') + package_logger.setLevel(log_level) + + # 作为库,不应该清除宿主应用的任何处理器 + # 也不应该让日志消息向上传播到根日志器,以免重复打印 + package_logger.propagate = False + + # 清除此日志器上现有的处理器,避免重复配置 + for handler in package_logger.handlers[:]: + package_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) + package_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) + package_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 + + # 备选方案:如果找不到标识文件,使用预设的相对路径 + # 这个路径基于当前的项目结构:util -> 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 lzwcai_mcp_api_converter.src.util.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("日志配置测试完成!") diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/nested_value.py b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/nested_value.py new file mode 100644 index 0000000..69c3d92 --- /dev/null +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/nested_value.py @@ -0,0 +1,168 @@ +import re +from typing import Any, Optional, Union, Dict, List +from .logger_config import get_logger + +logger = get_logger(__name__) + +# 预编译正则表达式 +PATH_PATTERN = re.compile(r"\{([^}]+)\}") + + +def get_nested_value( + data: Union[Dict, List, Any], + path_str: str, + default: Any = None, + raise_error: bool = False, +) -> Any: + """从嵌套字典或列表中根据路径字符串获取值 + + 支持以下格式的路径字符串: + 1. 简单路径: {res.data.token} + 2. 带前缀: Bearer {res.data.token} + 3. 带索引: {res.data.items[0].name} + 4. 带引号的键: {res.data."user.name"} + + Args: + data: 要查询的数据,可以是字典、列表或其他类型 + path_str: 路径字符串,支持{prefix} {path}或{path}格式 + default: 当路径不存在时返回的默认值 + raise_error: 是否在出错时抛出异常,默认为False + + Returns: + 查询到的值或默认值 + + Raises: + ValueError: 当raise_error为True且路径格式无效时 + KeyError: 当raise_error为True且路径不存在时 + TypeError: 当raise_error为True且类型错误时 + """ + try: + if not path_str: + logger.warning("路径字符串为空") + return default + + if data is None: + logger.warning("输入数据为None") + return default + + if not isinstance(data, (dict, list)): + logger.warning(f"输入数据类型不支持: {type(data)}") + return default + + # 使用预编译的正则表达式匹配路径 + matches = PATH_PATTERN.findall(path_str) + + if not matches: + logger.warning(f"路径字符串格式无效: {path_str}") + if raise_error: + raise ValueError(f"路径字符串格式无效: {path_str}") + return default + + # 处理路径 + if len(matches) == 1: + actual_path = matches[0] + # 获取{...}之前的所有文本作为前缀 + prefix = path_str[: path_str.find("{")].strip() + elif len(matches) == 2: + prefix = matches[0] + actual_path = matches[1] + else: + logger.warning(f"路径字符串包含过多匹配项: {path_str}") + if raise_error: + raise ValueError(f"路径字符串包含过多匹配项: {path_str}") + return default + + # 解析路径 + current = data + # 使用更智能的路径分割 + path_parts = [] + current_part = "" + in_quotes = False + + for char in actual_path: + if char == '"': + in_quotes = not in_quotes + current_part += char + elif char == "." and not in_quotes: + path_parts.append(current_part) + current_part = "" + else: + current_part += char + + if current_part: + path_parts.append(current_part) + + for part in path_parts: + # 处理数组索引 + if "[" in part and part.endswith("]"): + key, index_str = part.split("[", 1) + index_str = index_str.rstrip("]") + + # 获取键值 + if key and isinstance(current, dict): + current = current.get(key) + elif not key and isinstance(current, list): + pass + else: + logger.warning(f"无效的键: {key}") + if raise_error: + raise KeyError(f"无效的键: {key}") + return default + + # 获取索引 + try: + index = int(index_str) + if not isinstance(current, list) or not (0 <= index < len(current)): + logger.warning(f"无效的索引: {index}") + if raise_error: + raise IndexError(f"无效的索引: {index}") + return default + current = current[index] + except ValueError: + logger.warning(f"无效的索引格式: {index_str}") + if raise_error: + raise ValueError(f"无效的索引格式: {index_str}") + return default + else: + # 处理普通键 + if isinstance(current, dict): + # 处理带引号的键 + if part.startswith('"') and part.endswith('"'): + part = part[1:-1] + if part not in current: + logger.warning(f"键不存在: {part}") + if raise_error: + raise KeyError(f"键不存在: {part}") + return default + current = current[part] + elif isinstance(current, list): + try: + index = int(part) + if not (0 <= index < len(current)): + logger.warning(f"索引越界: {index}") + if raise_error: + raise IndexError(f"索引越界: {index}") + return default + current = current[index] + except ValueError: + logger.warning(f"无效的列表索引: {part}") + if raise_error: + raise ValueError(f"无效的列表索引: {part}") + return default + else: + logger.warning(f"无法访问键: {part}") + if raise_error: + raise TypeError(f"无法访问键: {part}") + return default + + # 处理前缀 + if prefix and current is not None: + return f"{prefix} {current}" + + return current + + except Exception as e: + logger.error(f"获取嵌套值失败: {str(e)}") + if raise_error: + raise + return default diff --git a/lzwcai_mcp_api_converter/main.py b/lzwcai_mcp_api_converter/main.py new file mode 100644 index 0000000..ccca4b9 --- /dev/null +++ b/lzwcai_mcp_api_converter/main.py @@ -0,0 +1,11 @@ +import os + +os.environ["modelId"] = "1946471611735015425" +os.environ["bizSysId"] = "1970385781825785858" +os.environ["bizSysApiIds"] = "[\"1970386761072058369\",\"1970386761185304578\",\"1970386761583763457\"]" +os.environ["businessUuid"] = "997" +# 导入模块 +from lzwcai_mcp_api_converter.src.create_mcp import run_main + +if __name__ == "__main__": + run_main() \ No newline at end of file diff --git a/lzwcai_mcp_api_converter/pyproject.toml b/lzwcai_mcp_api_converter/pyproject.toml new file mode 100644 index 0000000..86bc82d --- /dev/null +++ b/lzwcai_mcp_api_converter/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "lzwcai-mcp-api-converter" +version = "0.1.30" +description = "基于FastMCP框架的动态API工具服务器,自动将企业业务API配置转换为MCP协议工具,支持多种传输方式、企业认证和参数验证,为AI助手提供标准化的业务接口访问能力。" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "dynaconf>=3.2.11", + "httpx>=0.28.1", + "jinja2==3.1.6", + "mcp[cli]>=1.8.0", + "requests>=2.31.0", + "pypinyin>=0.54.0", +] + + +[tool.setuptools] +packages = {find = {where = ["."], include = ["lzwcai_mcp_api_converter*"]}} + +[tool.setuptools.package-data] +"lzwcai_mcp_api_converter.src" = ["api_config.json"] + +[project.scripts] +lzwcai-mcp-api-converter = "lzwcai_mcp_api_converter.src.create_mcp:run_main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_mcp_api_converter"] + diff --git a/lzwcai_mcp_api_converter/setup.cfg b/lzwcai_mcp_api_converter/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/lzwcai_mcp_api_converter/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/lzwcai_mcp_iot/IoT设备工具说明.md b/lzwcai_mcp_iot/IoT设备工具说明.md new file mode 100644 index 0000000..b3ba2a9 --- /dev/null +++ b/lzwcai_mcp_iot/IoT设备工具说明.md @@ -0,0 +1,140 @@ +# IoT 设备 MCP 工具说明 + +## 📋 工具清单 + +本服务提供 **4 个核心工具**,用于智能设备的查询、定位和控制。 + +--- + +## 🔧 工具详情 + +### 1. **iot_get_devices_by_location** - 根据位置获取设备 + +**功能**:查询指定位置/房间的所有智能设备 + +**使用场景**: +- "办公室有哪些设备" +- "会议室有什么设备" +- "客厅设备列表" + +**参数**: +- `location` (必填):位置/房间名称 + +**返回**:该位置的设备清单(设备ID、名称、类型、状态、控制命令等) + +--- + +### 2. **iot_get_all_spaces_and_devices** - 获取所有空间位置信息 + +**功能**:获取系统中所有可用空间位置的列表(仅空间名称,不包含设备详情) + +**使用场景**: +- "显示所有空间" +- "有哪些位置" +- "空间列表" +- "一共有多少个房间" + +**参数**:无需参数 + +**返回**: +- 空间总数 +- 所有空间名称的列表 + +**注意**:此工具只返回空间清单,如需查看某个空间的设备,请使用 `iot_get_devices_by_location` 工具 + +--- + +### 3. **iot_device_precise_controller** - IoT设备精确控制 + +**功能**:通过设备ID精确控制特定设备 + +**使用场景**: +- 控制特定的灯光、空调、门禁等 +- 需配合查询工具获取设备信息后使用 + +**参数**: +- `entityId` (必填):设备唯一ID +- `command` (必填):操作命令(如 turn_on、turn_off、set_temperature) +- `params` (必填):操作参数(根据命令类型提供,如温度值、亮度等) +- `userId` (可选):用户ID + +**返回**:设备操作结果(成功/失败、设备反馈) + +--- + +### 4. **smart_space_device_locator_matcher** - 智能空间设备定位 + +**功能**:查询用户当前所属的空间/位置 + +**使用场景**: +- "我现在在哪" +- "当前位置是什么" +- "确认一下位置" + +**参数**: +- `userId` (必填):用户ID + +**返回**:用户所属的空间名称 + +--- + +## 💡 典型使用流程 + +### 方式一:查看所有空间 +``` +1. 调用 iot_get_all_spaces_and_devices 获取所有空间列表 +2. 选择感兴趣的空间 +3. 调用 iot_get_devices_by_location 查看该空间的设备 +``` + +### 方式二:查看特定位置的设备 +``` +1. 调用 iot_get_devices_by_location 指定位置 +2. 查看该位置的设备清单和状态 +``` + +### 方式三:控制设备(两步操作) +``` +1. 调用 iot_get_devices_by_location 获取设备列表 +2. 从结果中提取 entityId 和 command +3. 调用 iot_device_precise_controller 执行控制 +``` + +### 方式四:定位用户 +``` +1. 调用 smart_space_device_locator_matcher +2. 获取用户当前所属空间 +3. 基于位置查询或控制设备 +``` + +--- + +## 📝 注意事项 + +1. **企业ID配置**:服务启动时需要配置 `ENTERPRISE_ID` 环境变量,系统会自动初始化向量库 +2. **日志记录**:所有操作都会记录到日志文件 `lzwcai_mcp_iot.log` +3. **传输方式**:使用 stdio(标准输入输出)方式运行 +4. **控制工具配合**:精确控制工具必须配合查询工具使用,不能单独随意填写参数 + +## 🔄 重要更新(v0.3.2) + +**配置变更:** +- ❌ 废弃:`employeeId` 环境变量 +- ✅ 新增:`ENTERPRISE_ID` 环境变量(必需) + +**初始化流程优化:** +- 移除了通过员工ID查询企业ID的步骤 +- 现在直接使用企业ID进行初始化 +- 提高了服务启动效率 + +--- + +## 🎯 核心特性 + +- ✅ 支持位置筛选查询设备 +- ✅ 支持获取所有可用空间列表 +- ✅ 支持精确的设备ID控制 +- ✅ 支持用户空间定位 +- ✅ 自动格式化设备列表输出 +- ✅ 完整的错误处理和日志记录 + diff --git a/lzwcai_mcp_iot/PKG-INFO b/lzwcai_mcp_iot/PKG-INFO new file mode 100644 index 0000000..cc00c23 --- /dev/null +++ b/lzwcai_mcp_iot/PKG-INFO @@ -0,0 +1,22 @@ +Metadata-Version: 2.4 +Name: lzwcai-mcp-smartIot +Version: 0.2.21 +Summary: IoT设备控制服务器,使用FastMCP框架提供设备操作功能 +Author-email: LZWCAI开发团队 +License: 专有软件 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Operating System :: OS Independent +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: fastmcp>=0.1.0 +Requires-Dist: requests +Provides-Extra: dev +Requires-Dist: pytest>=7.0.0; extra == "dev" +Requires-Dist: black>=23.1.0; extra == "dev" +Requires-Dist: isort>=5.12.0; extra == "dev" +Requires-Dist: flake8>=6.0.0; extra == "dev" +Requires-Dist: mypy>=1.0.0; extra == "dev" diff --git a/lzwcai_mcp_iot/README.md b/lzwcai_mcp_iot/README.md new file mode 100644 index 0000000..6b9face --- /dev/null +++ b/lzwcai_mcp_iot/README.md @@ -0,0 +1,200 @@ +# lzwcai-mcp-iot + +[![Version](https://img.shields.io/badge/version-0.3.1-blue.svg)](https://pypi.org/project/lzwcai-mcp-iot/) +[![Python](https://img.shields.io/badge/python-3.8%2B-brightgreen.svg)](https://www.python.org/) +[![License](https://img.shields.io/badge/license-Proprietary-red.svg)]() + +> IoT设备控制服务器,使用 FastMCP 框架提供智能设备的查询、定位和控制功能 + +## ✨ 特性 + +- ✅ 支持位置筛选查询设备 +- ✅ 支持获取所有可用空间列表 +- ✅ 支持精确的设备ID控制 +- ✅ 支持用户空间定位 +- ✅ 自动格式化设备列表输出 +- ✅ 完整的错误处理和日志记录 + +## 📦 安装 + +```bash +pip install lzwcai-mcp-iot +``` + +或从源码安装: + +```bash +git clone +cd lzwcai_mcp_iot +pip install -e . +``` + +## 🚀 快速开始 + +### 启动服务 + +```bash +lzwcai-mcp-iot +``` + +### 配置要求 + +服务启动时需要配置 `employeeId`,系统会自动初始化企业ID。 + +## 🔧 核心工具 + +本服务提供 **4 个核心工具**,用于智能设备的查询、定位和控制。 + +### 1. iot_get_devices_by_location - 根据位置获取设备 + +**功能**:查询指定位置/房间的所有智能设备 + +**使用场景**: +- "办公室有哪些设备" +- "会议室有什么设备" +- "客厅设备列表" + +**参数**: +- `location` (必填):位置/房间名称 + +**返回**:该位置的设备清单(设备ID、名称、类型、状态、控制命令等) + +--- + +### 2. iot_get_all_spaces_and_devices - 获取所有空间位置信息 + +**功能**:获取系统中所有可用空间位置的列表(仅空间名称,不包含设备详情) + +**使用场景**: +- "显示所有空间" +- "有哪些位置" +- "空间列表" +- "一共有多少个房间" + +**参数**:无需参数 + +**返回**: +- 空间总数 +- 所有空间名称的列表 + +**注意**:此工具只返回空间清单,如需查看某个空间的设备,请使用 `iot_get_devices_by_location` 工具 + +--- + +### 3. iot_device_precise_controller - IoT设备精确控制 + +**功能**:通过设备ID精确控制特定设备 + +**使用场景**: +- 控制特定的灯光、空调、门禁等 +- 需配合查询工具获取设备信息后使用 + +**参数**: +- `entityId` (必填):设备唯一ID +- `command` (必填):操作命令(如 turn_on、turn_off、set_temperature) +- `params` (必填):操作参数(根据命令类型提供,如温度值、亮度等) +- `userId` (可选):用户ID + +**返回**:设备操作结果(成功/失败、设备反馈) + +--- + +### 4. smart_space_device_locator_matcher - 智能空间设备定位 + +**功能**:查询用户当前所属的空间/位置 + +**使用场景**: +- "我现在在哪" +- "当前位置是什么" +- "确认一下位置" + +**参数**: +- `userId` (必填):用户ID + +**返回**:用户所属的空间名称 + +--- + +## 💡 典型使用流程 + +### 方式一:查看所有空间 +``` +1. 调用 iot_get_all_spaces_and_devices 获取所有空间列表 +2. 选择感兴趣的空间 +3. 调用 iot_get_devices_by_location 查看该空间的设备 +``` + +### 方式二:查看特定位置的设备 +``` +1. 调用 iot_get_devices_by_location 指定位置 +2. 查看该位置的设备清单和状态 +``` + +### 方式三:控制设备(两步操作) +``` +1. 调用 iot_get_devices_by_location 获取设备列表 +2. 从结果中提取 entityId 和 command +3. 调用 iot_device_precise_controller 执行控制 +``` + +### 方式四:定位用户 +``` +1. 调用 smart_space_device_locator_matcher +2. 获取用户当前所属空间 +3. 基于位置查询或控制设备 +``` + +## 📝 注意事项 + +1. **企业ID初始化**:服务启动时需要配置 `employeeId`,系统会自动初始化企业ID +2. **日志记录**:所有操作都会记录到日志文件 `lzwcai_mcp_iot.log` +3. **传输方式**:使用 stdio(标准输入输出)方式运行 +4. **控制工具配合**:精确控制工具必须配合查询工具使用,不能单独随意填写参数 + +## 🛠️ 开发 + +### 安装开发依赖 + +```bash +pip install -e ".[dev]" +``` + +### 代码格式化 + +```bash +# 使用 black 格式化 +black lzwcai_mcp_iot/ + +# 使用 isort 排序导入 +isort lzwcai_mcp_iot/ +``` + +### 代码检查 + +```bash +# 使用 flake8 +flake8 lzwcai_mcp_iot/ + +# 使用 mypy +mypy lzwcai_mcp_iot/ +``` + +### 运行测试 + +```bash +pytest +``` + +## 📄 许可证 + +专有软件 - 版权所有 © LZWCAI开发团队 + +## 📧 联系方式 + +- 开发团队:LZWCAI开发团队 +- 邮箱:dev@lzwcai.com + +## 📚 更多文档 + +详细的工具使用说明请参考 [IoT设备工具说明.md](IoT设备工具说明.md) + diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/PKG-INFO b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/PKG-INFO new file mode 100644 index 0000000..0541e82 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/PKG-INFO @@ -0,0 +1,223 @@ +Metadata-Version: 2.4 +Name: lzwcai-mcp-iot +Version: 0.3.3 +Summary: IoT设备控制服务器,使用FastMCP框架提供设备操作功能 +Author-email: LZWCAI开发团队 +License-Expression: LicenseRef-Proprietary +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Operating System :: OS Independent +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: fastmcp>=0.1.0 +Requires-Dist: requests +Provides-Extra: dev +Requires-Dist: pytest>=7.0.0; extra == "dev" +Requires-Dist: black>=23.1.0; extra == "dev" +Requires-Dist: isort>=5.12.0; extra == "dev" +Requires-Dist: flake8>=6.0.0; extra == "dev" +Requires-Dist: mypy>=1.0.0; extra == "dev" + +# lzwcai-mcp-iot + +[![Version](https://img.shields.io/badge/version-0.3.1-blue.svg)](https://pypi.org/project/lzwcai-mcp-iot/) +[![Python](https://img.shields.io/badge/python-3.8%2B-brightgreen.svg)](https://www.python.org/) +[![License](https://img.shields.io/badge/license-Proprietary-red.svg)]() + +> IoT设备控制服务器,使用 FastMCP 框架提供智能设备的查询、定位和控制功能 + +## ✨ 特性 + +- ✅ 支持位置筛选查询设备 +- ✅ 支持获取所有可用空间列表 +- ✅ 支持精确的设备ID控制 +- ✅ 支持用户空间定位 +- ✅ 自动格式化设备列表输出 +- ✅ 完整的错误处理和日志记录 + +## 📦 安装 + +```bash +pip install lzwcai-mcp-iot +``` + +或从源码安装: + +```bash +git clone +cd lzwcai_mcp_iot +pip install -e . +``` + +## 🚀 快速开始 + +### 启动服务 + +```bash +lzwcai-mcp-iot +``` + +### 配置要求 + +服务启动时需要配置 `employeeId`,系统会自动初始化企业ID。 + +## 🔧 核心工具 + +本服务提供 **4 个核心工具**,用于智能设备的查询、定位和控制。 + +### 1. iot_get_devices_by_location - 根据位置获取设备 + +**功能**:查询指定位置/房间的所有智能设备 + +**使用场景**: +- "办公室有哪些设备" +- "会议室有什么设备" +- "客厅设备列表" + +**参数**: +- `location` (必填):位置/房间名称 + +**返回**:该位置的设备清单(设备ID、名称、类型、状态、控制命令等) + +--- + +### 2. iot_get_all_spaces_and_devices - 获取所有空间位置信息 + +**功能**:获取系统中所有可用空间位置的列表(仅空间名称,不包含设备详情) + +**使用场景**: +- "显示所有空间" +- "有哪些位置" +- "空间列表" +- "一共有多少个房间" + +**参数**:无需参数 + +**返回**: +- 空间总数 +- 所有空间名称的列表 + +**注意**:此工具只返回空间清单,如需查看某个空间的设备,请使用 `iot_get_devices_by_location` 工具 + +--- + +### 3. iot_device_precise_controller - IoT设备精确控制 + +**功能**:通过设备ID精确控制特定设备 + +**使用场景**: +- 控制特定的灯光、空调、门禁等 +- 需配合查询工具获取设备信息后使用 + +**参数**: +- `entityId` (必填):设备唯一ID +- `command` (必填):操作命令(如 turn_on、turn_off、set_temperature) +- `params` (必填):操作参数(根据命令类型提供,如温度值、亮度等) +- `userId` (可选):用户ID + +**返回**:设备操作结果(成功/失败、设备反馈) + +--- + +### 4. smart_space_device_locator_matcher - 智能空间设备定位 + +**功能**:查询用户当前所属的空间/位置 + +**使用场景**: +- "我现在在哪" +- "当前位置是什么" +- "确认一下位置" + +**参数**: +- `userId` (必填):用户ID + +**返回**:用户所属的空间名称 + +--- + +## 💡 典型使用流程 + +### 方式一:查看所有空间 +``` +1. 调用 iot_get_all_spaces_and_devices 获取所有空间列表 +2. 选择感兴趣的空间 +3. 调用 iot_get_devices_by_location 查看该空间的设备 +``` + +### 方式二:查看特定位置的设备 +``` +1. 调用 iot_get_devices_by_location 指定位置 +2. 查看该位置的设备清单和状态 +``` + +### 方式三:控制设备(两步操作) +``` +1. 调用 iot_get_devices_by_location 获取设备列表 +2. 从结果中提取 entityId 和 command +3. 调用 iot_device_precise_controller 执行控制 +``` + +### 方式四:定位用户 +``` +1. 调用 smart_space_device_locator_matcher +2. 获取用户当前所属空间 +3. 基于位置查询或控制设备 +``` + +## 📝 注意事项 + +1. **企业ID初始化**:服务启动时需要配置 `employeeId`,系统会自动初始化企业ID +2. **日志记录**:所有操作都会记录到日志文件 `lzwcai_mcp_iot.log` +3. **传输方式**:使用 stdio(标准输入输出)方式运行 +4. **控制工具配合**:精确控制工具必须配合查询工具使用,不能单独随意填写参数 + +## 🛠️ 开发 + +### 安装开发依赖 + +```bash +pip install -e ".[dev]" +``` + +### 代码格式化 + +```bash +# 使用 black 格式化 +black lzwcai_mcp_iot/ + +# 使用 isort 排序导入 +isort lzwcai_mcp_iot/ +``` + +### 代码检查 + +```bash +# 使用 flake8 +flake8 lzwcai_mcp_iot/ + +# 使用 mypy +mypy lzwcai_mcp_iot/ +``` + +### 运行测试 + +```bash +pytest +``` + +## 📄 许可证 + +专有软件 - 版权所有 © LZWCAI开发团队 + +## 📧 联系方式 + +- 开发团队:LZWCAI开发团队 +- 邮箱:dev@lzwcai.com + +## 📚 更多文档 + +详细的工具使用说明请参考 [IoT设备工具说明.md](IoT设备工具说明.md) + diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/SOURCES.txt b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/SOURCES.txt new file mode 100644 index 0000000..a37f6e0 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/SOURCES.txt @@ -0,0 +1,19 @@ +README.md +pyproject.toml +setup.cfg +lzwcai_mcp_iot/__init__.py +lzwcai_mcp_iot/config.py +lzwcai_mcp_iot/iot_device_tool.py +lzwcai_mcp_iot.egg-info/PKG-INFO +lzwcai_mcp_iot.egg-info/SOURCES.txt +lzwcai_mcp_iot.egg-info/dependency_links.txt +lzwcai_mcp_iot.egg-info/entry_points.txt +lzwcai_mcp_iot.egg-info/requires.txt +lzwcai_mcp_iot.egg-info/top_level.txt +lzwcai_mcp_iot/src/__init__.py +lzwcai_mcp_iot/src/device_operations.py +lzwcai_mcp_iot/src/device_results_pretreatment.py +lzwcai_mcp_iot/src/init_mcp.py +lzwcai_mcp_iot/src/iot_device_dicts_prompt.py +lzwcai_mcp_iot/src/logger_config.py +lzwcai_mcp_iot/src/vector_service.py \ No newline at end of file diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/dependency_links.txt b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/entry_points.txt b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/entry_points.txt new file mode 100644 index 0000000..dbb4cf0 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +lzwcai-mcp-iot = lzwcai_mcp_iot.iot_device_tool:main diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/requires.txt b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/requires.txt new file mode 100644 index 0000000..6e65c56 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/requires.txt @@ -0,0 +1,9 @@ +fastmcp>=0.1.0 +requests + +[dev] +pytest>=7.0.0 +black>=23.1.0 +isort>=5.12.0 +flake8>=6.0.0 +mypy>=1.0.0 diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/top_level.txt b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/top_level.txt new file mode 100644 index 0000000..10738ed --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/top_level.txt @@ -0,0 +1 @@ +lzwcai_mcp_iot diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot.log b/lzwcai_mcp_iot/lzwcai_mcp_iot.log new file mode 100644 index 0000000..6f06be3 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot.log @@ -0,0 +1,159 @@ +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:215] - ================================================================================ +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2025-11-07 18:18:45 +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:217] - 日志级别: INFO +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai-mcp-server-package\lzwcai_mcp_iot\lzwcai_mcp_iot.log +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:219] - 控制台输出: False +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:220] - 文件输出: True +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份 +2025-11-07 18:18:45 - lzwcai_mcp_iot - INFO - [logger_config.py:222] - ================================================================================ +2025-11-07 18:18:45 - lzwcai_mcp_iot.config - INFO - [config.py:89] - 配置加载完成 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:44] - DeviceOperator初始化完成,API基础URL: http://192.168.2.236:8088 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:39] - VectorService初始化完成,API基础URL: http://192.168.2.236:5002 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:348] - ================================================================================ +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:349] - IoT设备MCP服务器启动流程开始 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:350] - 配置信息: {'device_api_base_url': 'http://192.168.2.236:8088', 'vector_api_base_url': 'http://192.168.2.236:5002', 'enterprise_id': '1952978233106669569', 'employeeId': '1986712221817815042', 'request_timeout': 30, 'max_retries': 3, 'vector_store_name': '设备库', 'vector_store_description': '向量库', 'encoder_type': 'word2vec', 'default_top_k': 1, 'default_auto_create': True, 'log_level': 'INFO', 'log_format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'} +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:351] - ================================================================================ +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:354] - 正在初始化核心组件... +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:355] - 设备操作器API地址: http://192.168.2.236:8088 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:356] - 向量服务API地址: http://192.168.2.236:5002 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:357] - 核心组件初始化完成 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:362] - 检测到员工ID配置: 1986712221817815042 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:363] - 开始初始化MCP服务器... +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:75] - 开始初始化MCP服务器,员工ID: 1986712221817815042 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:78] - 第一步:获取企业ID... +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:410] - 正在获取员工ID为 1986712221817815042 的数字员工信息... +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:418] - 成功获取数字员工信息: {'msg': '操作成功', 'code': 200, 'data': {'userInfo': {'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'remark': None, 'userId': '1909', 'enterpriseId': '1952978233106669569', 'deptId': None, 'userName': '物联网设备管家4628', 'nickName': '物联网设备管家4628', 'userType': '03', 'email': '', 'phonenumber': '', 'sex': '2', 'avatar': '', 'password': '123456', 'status': '0', 'delFlag': '0', 'loginIp': '', 'loginDate': None, 'createSubject': False, 'createSpeakSubject': False, 'bizSysId': None, 'dept': None, 'roles': [], 'roleIds': None, 'postIds': None, 'roleId': None, 'enterprise': {'id': None, 'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'enterpriseName': None, 'enterpriseCode': None, 'description': None, 'type': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': None, 'datasetName': None, 'datasetDescribe': None, 'oaType': None, 'oaMap': None, 'status': False}, 'bindOA': False, 'oaMap': None, 'spaceName': None, 'admin': False}, 'employeeInfo': {'id': '1986712221817815042', 'enterpriseId': '1952978233106669569', 'name': '物联网设备管家', 'type': 'assistant', 'imageInfo': None, 'personality': None, 'prompt': '管理设备', 'modelConfig': None, 'promptConfig': None, 'datasetId': '166a9087-830c-443f-81f9-924f8b089530', 'datasetApiKey': 'dataset-nNE2K1KSeagOQnCJjLQzXbpH', 'zhipuApiKey': '12334444', 'enableLongTermMemory': False}, 'enterpriseVO': {'enterpriseId': '1952978233106669569', 'enterpriseName': '中科天目', 'enterpriseCode': '002233', 'type': '人工智能', 'description': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': 'ec8b1391-d093-4b3c-a675-5c88455f094c', 'datasetName': '中科天目', 'datasetDescribe': '系统默认创建', 'difyHost': 'http://192.168.2.236:3001', 'difyEmail': 'lingzewanchuan@lzwcai.com', 'difyName': '中科天目', 'difyPassword': 'Lzwc@2025.'}, 'postVOList': [], 'serverConfigToAI': {'id': '1932057383785390081', 'name': '4dc437bf3d2e', 'ipAddr': '192.168.2.236', 'isMajor': None}, 'deviceBindingList': []}} +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:99] - 成功获取企业ID: 1952978233106669569 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:102] - 第二步:检查向量库状态... +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:435] - 检查向量库状态,keyId: 1952978233106669569 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:447] - 向量库状态检查完成,keyId: 1952978233106669569, exists: True +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:104] - 向量库状态检查完成,存在状态: True +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:137] - 向量库已存在,无需创建 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:366] - MCP服务器初始化结果: {'code': 200, 'msg': 'MCP服务器初始化成功,向量库已存在', 'data': {'enterprise_id': '1952978233106669569', 'vector_store_created': False, 'vector_store_existed': True}} +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:371] - MCP服务器初始化成功 +2025-11-07 18:18:45 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:61] - 企业ID已更新: 1952978233106669569 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:376] - 企业ID已保存到环境变量: 1952978233106669569 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:384] - 已注册的MCP工具: +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:385] - 1. iot_get_devices_by_location - 根据位置获取设备列表 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:386] - 2. iot_get_all_spaces_and_devices - 获取所有空间位置信息 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:387] - 3. iot_device_precise_controller - IoT设备精确控制 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:388] - 4. smart_space_device_locator_matcher - 智能空间设备定位 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:390] - ================================================================================ +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:391] - MCP服务器即将启动,等待客户端连接... +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:392] - 传输方式: stdio (标准输入输出) +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:393] - 注意: 控制台日志已禁用,所有日志将写入文件 +2025-11-07 18:18:45 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:394] - ================================================================================ +2025-11-07 18:18:53 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:119] - 开始处理获取所有空间位置信息请求 +2025-11-07 18:18:53 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:130] - 获取到企业ID: 1952978233106669569 +2025-11-07 18:18:53 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:499] - 查询设备列表,keyId: 1952978233106669569, location: +2025-11-07 18:18:53 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:516] - 设备列表查询成功,返回设备数量: 27 +2025-11-07 18:18:53 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:137] - 查询所有设备结果: {'devices': [{'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'climate.qjiang_cn_741479129_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'switch.zimi_cn_1144259387_dhkg01_on_p_2_1', 'device_desc': '灵泽办公区左吊灯 开关 按键', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '33', 'location_desc': '研发办公区走廊过道;研发办公区走廊;研发办公区过道;研发部走廊', 'entityId': 'switch.zimi_cn_1144138206_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'climate.qjiang_cn_741478765_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_3_1', 'device_desc': '办公桌灯;主位灯;吊灯;灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_4_1', 'device_desc': '入口灯;吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'cover.lumi_cn_1132313226_hmcn02_s_2_2', 'device_desc': '左窗帘;窗帘;帘', 'operations': [{'command': 'open_cover', 'operation_desc': '打开;开;展开;开放', 'operation_params': []}, {'command': 'close_cover', 'operation_desc': '关闭;关;合并;合起来;合', 'operation_params': []}, {'command': 'toggle', 'operation_desc': '切换;一键开关', 'operation_params': []}, {'command': 'stop_cover', 'operation_desc': '停止;停;停一下;', 'operation_params': []}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'climate.qjiang_cn_741362991_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'climate.qjiang_cn_741470846_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '37', 'location_desc': '李总办公室;董事长;李总房间;董事长办公室', 'entityId': 'climate.qjiang_cn_741478700_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '38', 'location_desc': '公司前台;前台;前台大门;门口', 'entityId': 'climate.qjiang_cn_741479337_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'climate.qjiang_cn_741352250_wb20', 'device_desc': '空调', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'switch.zimi_cn_1144125565_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '41', 'location_desc': '小会议室;小会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '42', 'location_desc': '电梯走廊;前台门口;公司门口;公司大门口;大门', 'entityId': 'button.zimi_cn_1119697824_dhkg05_toggle_a_4_1', 'device_desc': '吊灯;电梯灯;灯;照明灯', 'operations': [{'command': 'press', 'operation_desc': '按下;按;开关;打开或者关闭;', 'operation_params': []}]}, {'location_key': '43', 'location_desc': '公司大门;大门口;前台;公司前台;大门', 'entityId': 'switch.giot_cn_1110921716_v51ksm_on_p_2_1', 'device_desc': '大门开关', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_3_1', 'device_desc': '射灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '45', 'location_desc': '会议室过道;会议室走廊;会议区过道', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_5_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '46', 'location_desc': '爱易拍展厅;爱一拍展厅;爱一拍展区;爱一排展区', 'entityId': 'switch.zimi_cn_1144256905_dhkg02_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '47', 'location_desc': '灵泽展厅过道;灵泽展厅走廊', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'climate.qjiang_cn_741348975_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '49', 'location_desc': '休息室二楼;休息室二层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '50', 'location_desc': '前台休息区', 'entityId': 'switch.zimi_cn_1119697824_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}], 'count': 27, 'location_filter': None} +2025-11-07 18:19:09 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:57] - 开始处理IoT设备查询请求 - 位置: 研发 +2025-11-07 18:19:09 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:70] - 获取到企业ID: 1952978233106669569 +2025-11-07 18:19:09 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:499] - 查询设备列表,keyId: 1952978233106669569, location: 研发 +2025-11-07 18:19:09 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:516] - 设备列表查询成功,返回设备数量: 3 +2025-11-07 18:19:09 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:77] - 查询设备列表结果: {'devices': [{'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'climate.qjiang_cn_741479129_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'switch.zimi_cn_1144259387_dhkg01_on_p_2_1', 'device_desc': '灵泽办公区左吊灯 开关 按键', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '33', 'location_desc': '研发办公区走廊过道;研发办公区走廊;研发办公区过道;研发部走廊', 'entityId': 'switch.zimi_cn_1144138206_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}], 'count': 3, 'location_filter': '研发'} +2025-11-07 18:19:09 - lzwcai_mcp_iot.src.device_results_pretreatment - INFO - [device_results_pretreatment.py:458] - 设备列表格式化完成,设备数量: 3 +2025-11-07 18:19:09 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:92] - 设备列表已格式化,设备数量: 3 +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:215] - ================================================================================ +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2025-11-07 18:21:20 +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:217] - 日志级别: INFO +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai-mcp-server-package\lzwcai_mcp_iot\lzwcai_mcp_iot.log +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:219] - 控制台输出: False +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:220] - 文件输出: True +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份 +2025-11-07 18:21:20 - lzwcai_mcp_iot - INFO - [logger_config.py:222] - ================================================================================ +2025-11-07 18:21:20 - lzwcai_mcp_iot.config - INFO - [config.py:89] - 配置加载完成 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:44] - DeviceOperator初始化完成,API基础URL: http://192.168.2.236:8088 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:39] - VectorService初始化完成,API基础URL: http://192.168.2.236:5002 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:353] - ================================================================================ +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:354] - IoT设备MCP服务器启动流程开始 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:355] - 配置信息: {'device_api_base_url': 'http://192.168.2.236:8088', 'vector_api_base_url': 'http://192.168.2.236:5002', 'enterprise_id': '1952978233106669569', 'employeeId': '1986712221817815042', 'request_timeout': 30, 'max_retries': 3, 'vector_store_name': '设备库', 'vector_store_description': '向量库', 'encoder_type': 'word2vec', 'default_top_k': 1, 'default_auto_create': True, 'log_level': 'INFO', 'log_format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'} +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:356] - ================================================================================ +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:359] - 正在初始化核心组件... +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:360] - 设备操作器API地址: http://192.168.2.236:8088 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:361] - 向量服务API地址: http://192.168.2.236:5002 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:362] - 核心组件初始化完成 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:367] - 检测到员工ID配置: 1986712221817815042 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:368] - 开始初始化MCP服务器... +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:75] - 开始初始化MCP服务器,员工ID: 1986712221817815042 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:78] - 第一步:获取企业ID... +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:410] - 正在获取员工ID为 1986712221817815042 的数字员工信息... +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:418] - 成功获取数字员工信息: {'msg': '操作成功', 'code': 200, 'data': {'userInfo': {'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'remark': None, 'userId': '1909', 'enterpriseId': '1952978233106669569', 'deptId': None, 'userName': '物联网设备管家4628', 'nickName': '物联网设备管家4628', 'userType': '03', 'email': '', 'phonenumber': '', 'sex': '2', 'avatar': '', 'password': '123456', 'status': '0', 'delFlag': '0', 'loginIp': '', 'loginDate': None, 'createSubject': False, 'createSpeakSubject': False, 'bizSysId': None, 'dept': None, 'roles': [], 'roleIds': None, 'postIds': None, 'roleId': None, 'enterprise': {'id': None, 'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'enterpriseName': None, 'enterpriseCode': None, 'description': None, 'type': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': None, 'datasetName': None, 'datasetDescribe': None, 'oaType': None, 'oaMap': None, 'status': False}, 'bindOA': False, 'oaMap': None, 'spaceName': None, 'admin': False}, 'employeeInfo': {'id': '1986712221817815042', 'enterpriseId': '1952978233106669569', 'name': '物联网设备管家', 'type': 'assistant', 'imageInfo': None, 'personality': None, 'prompt': '管理设备', 'modelConfig': None, 'promptConfig': None, 'datasetId': '166a9087-830c-443f-81f9-924f8b089530', 'datasetApiKey': 'dataset-nNE2K1KSeagOQnCJjLQzXbpH', 'zhipuApiKey': '12334444', 'enableLongTermMemory': False}, 'enterpriseVO': {'enterpriseId': '1952978233106669569', 'enterpriseName': '中科天目', 'enterpriseCode': '002233', 'type': '人工智能', 'description': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': 'ec8b1391-d093-4b3c-a675-5c88455f094c', 'datasetName': '中科天目', 'datasetDescribe': '系统默认创建', 'difyHost': 'http://192.168.2.236:3001', 'difyEmail': 'lingzewanchuan@lzwcai.com', 'difyName': '中科天目', 'difyPassword': 'Lzwc@2025.'}, 'postVOList': [], 'serverConfigToAI': {'id': '1932057383785390081', 'name': '4dc437bf3d2e', 'ipAddr': '192.168.2.236', 'isMajor': None}, 'deviceBindingList': []}} +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:99] - 成功获取企业ID: 1952978233106669569 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:102] - 第二步:检查向量库状态... +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:435] - 检查向量库状态,keyId: 1952978233106669569 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:447] - 向量库状态检查完成,keyId: 1952978233106669569, exists: True +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:104] - 向量库状态检查完成,存在状态: True +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:137] - 向量库已存在,无需创建 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:371] - MCP服务器初始化结果: {'code': 200, 'msg': 'MCP服务器初始化成功,向量库已存在', 'data': {'enterprise_id': '1952978233106669569', 'vector_store_created': False, 'vector_store_existed': True}} +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:376] - MCP服务器初始化成功 +2025-11-07 18:21:20 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:61] - 企业ID已更新: 1952978233106669569 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:381] - 企业ID已保存到环境变量: 1952978233106669569 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:389] - 已注册的MCP工具: +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:390] - 1. iot_get_devices_by_location - 根据位置获取设备列表 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:391] - 2. iot_get_all_spaces_and_devices - 获取所有空间位置信息 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:392] - 3. iot_device_precise_controller - IoT设备精确控制 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:393] - 4. smart_space_device_locator_matcher - 智能空间设备定位 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:395] - ================================================================================ +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:396] - MCP服务器即将启动,等待客户端连接... +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:397] - 传输方式: stdio (标准输入输出) +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:398] - 注意: 控制台日志已禁用,所有日志将写入文件 +2025-11-07 18:21:20 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:399] - ================================================================================ +2025-11-07 18:21:26 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:120] - 开始处理获取所有空间位置信息请求 +2025-11-07 18:21:26 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:131] - 获取到企业ID: 1952978233106669569 +2025-11-07 18:21:26 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:499] - 查询设备列表,keyId: 1952978233106669569, location: +2025-11-07 18:21:26 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:516] - 设备列表查询成功,返回设备数量: 27 +2025-11-07 18:21:26 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:138] - 查询所有设备结果: {'devices': [{'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'climate.qjiang_cn_741479129_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'switch.zimi_cn_1144259387_dhkg01_on_p_2_1', 'device_desc': '灵泽办公区左吊灯 开关 按键', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '33', 'location_desc': '研发办公区走廊过道;研发办公区走廊;研发办公区过道;研发部走廊', 'entityId': 'switch.zimi_cn_1144138206_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'climate.qjiang_cn_741478765_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_3_1', 'device_desc': '办公桌灯;主位灯;吊灯;灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_4_1', 'device_desc': '入口灯;吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'cover.lumi_cn_1132313226_hmcn02_s_2_2', 'device_desc': '左窗帘;窗帘;帘', 'operations': [{'command': 'open_cover', 'operation_desc': '打开;开;展开;开放', 'operation_params': []}, {'command': 'close_cover', 'operation_desc': '关闭;关;合并;合起来;合', 'operation_params': []}, {'command': 'toggle', 'operation_desc': '切换;一键开关', 'operation_params': []}, {'command': 'stop_cover', 'operation_desc': '停止;停;停一下;', 'operation_params': []}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'climate.qjiang_cn_741362991_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'climate.qjiang_cn_741470846_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '37', 'location_desc': '李总办公室;董事长;李总房间;董事长办公室', 'entityId': 'climate.qjiang_cn_741478700_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '38', 'location_desc': '公司前台;前台;前台大门;门口', 'entityId': 'climate.qjiang_cn_741479337_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'climate.qjiang_cn_741352250_wb20', 'device_desc': '空调', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'switch.zimi_cn_1144125565_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '41', 'location_desc': '小会议室;小会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '42', 'location_desc': '电梯走廊;前台门口;公司门口;公司大门口;大门', 'entityId': 'button.zimi_cn_1119697824_dhkg05_toggle_a_4_1', 'device_desc': '吊灯;电梯灯;灯;照明灯', 'operations': [{'command': 'press', 'operation_desc': '按下;按;开关;打开或者关闭;', 'operation_params': []}]}, {'location_key': '43', 'location_desc': '公司大门;大门口;前台;公司前台;大门', 'entityId': 'switch.giot_cn_1110921716_v51ksm_on_p_2_1', 'device_desc': '大门开关', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_3_1', 'device_desc': '射灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '45', 'location_desc': '会议室过道;会议室走廊;会议区过道', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_5_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '46', 'location_desc': '爱易拍展厅;爱一拍展厅;爱一拍展区;爱一排展区', 'entityId': 'switch.zimi_cn_1144256905_dhkg02_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '47', 'location_desc': '灵泽展厅过道;灵泽展厅走廊', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'climate.qjiang_cn_741348975_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '49', 'location_desc': '休息室二楼;休息室二层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '50', 'location_desc': '前台休息区', 'entityId': 'switch.zimi_cn_1119697824_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}], 'count': 27, 'location_filter': None} +2025-11-07 18:21:26 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:189] - 已获取所有空间位置信息,空间数: 51 +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:215] - ================================================================================ +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2025-11-07 18:22:56 +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:217] - 日志级别: INFO +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai-mcp-server-package\lzwcai_mcp_iot\lzwcai_mcp_iot.log +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:219] - 控制台输出: False +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:220] - 文件输出: True +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份 +2025-11-07 18:22:56 - lzwcai_mcp_iot - INFO - [logger_config.py:222] - ================================================================================ +2025-11-07 18:22:56 - lzwcai_mcp_iot.config - INFO - [config.py:89] - 配置加载完成 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:44] - DeviceOperator初始化完成,API基础URL: http://192.168.2.236:8088 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:39] - VectorService初始化完成,API基础URL: http://192.168.2.236:5002 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:348] - ================================================================================ +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:349] - IoT设备MCP服务器启动流程开始 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:350] - 配置信息: {'device_api_base_url': 'http://192.168.2.236:8088', 'vector_api_base_url': 'http://192.168.2.236:5002', 'enterprise_id': '1952978233106669569', 'employeeId': '1986712221817815042', 'request_timeout': 30, 'max_retries': 3, 'vector_store_name': '设备库', 'vector_store_description': '向量库', 'encoder_type': 'word2vec', 'default_top_k': 1, 'default_auto_create': True, 'log_level': 'INFO', 'log_format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'} +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:351] - ================================================================================ +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:354] - 正在初始化核心组件... +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:355] - 设备操作器API地址: http://192.168.2.236:8088 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:356] - 向量服务API地址: http://192.168.2.236:5002 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:357] - 核心组件初始化完成 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:362] - 检测到员工ID配置: 1986712221817815042 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:363] - 开始初始化MCP服务器... +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:75] - 开始初始化MCP服务器,员工ID: 1986712221817815042 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:78] - 第一步:获取企业ID... +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:410] - 正在获取员工ID为 1986712221817815042 的数字员工信息... +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:418] - 成功获取数字员工信息: {'msg': '操作成功', 'code': 200, 'data': {'userInfo': {'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'remark': None, 'userId': '1909', 'enterpriseId': '1952978233106669569', 'deptId': None, 'userName': '物联网设备管家4628', 'nickName': '物联网设备管家4628', 'userType': '03', 'email': '', 'phonenumber': '', 'sex': '2', 'avatar': '', 'password': '123456', 'status': '0', 'delFlag': '0', 'loginIp': '', 'loginDate': None, 'createSubject': False, 'createSpeakSubject': False, 'bizSysId': None, 'dept': None, 'roles': [], 'roleIds': None, 'postIds': None, 'roleId': None, 'enterprise': {'id': None, 'createBy': '', 'createTime': '2025-11-07 16:27:59', 'updateBy': None, 'updateTime': None, 'enterpriseName': None, 'enterpriseCode': None, 'description': None, 'type': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': None, 'datasetName': None, 'datasetDescribe': None, 'oaType': None, 'oaMap': None, 'status': False}, 'bindOA': False, 'oaMap': None, 'spaceName': None, 'admin': False}, 'employeeInfo': {'id': '1986712221817815042', 'enterpriseId': '1952978233106669569', 'name': '物联网设备管家', 'type': 'assistant', 'imageInfo': None, 'personality': None, 'prompt': '管理设备', 'modelConfig': None, 'promptConfig': None, 'datasetId': '166a9087-830c-443f-81f9-924f8b089530', 'datasetApiKey': 'dataset-nNE2K1KSeagOQnCJjLQzXbpH', 'zhipuApiKey': '12334444', 'enableLongTermMemory': False}, 'enterpriseVO': {'enterpriseId': '1952978233106669569', 'enterpriseName': '中科天目', 'enterpriseCode': '002233', 'type': '人工智能', 'description': None, 'businessLicense': None, 'licenseFileId': None, 'legalPerson': None, 'industryInvolved': None, 'datasetId': 'ec8b1391-d093-4b3c-a675-5c88455f094c', 'datasetName': '中科天目', 'datasetDescribe': '系统默认创建', 'difyHost': 'http://192.168.2.236:3001', 'difyEmail': 'lingzewanchuan@lzwcai.com', 'difyName': '中科天目', 'difyPassword': 'Lzwc@2025.'}, 'postVOList': [], 'serverConfigToAI': {'id': '1932057383785390081', 'name': '4dc437bf3d2e', 'ipAddr': '192.168.2.236', 'isMajor': None}, 'deviceBindingList': []}} +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:99] - 成功获取企业ID: 1952978233106669569 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:102] - 第二步:检查向量库状态... +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:435] - 检查向量库状态,keyId: 1952978233106669569 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:447] - 向量库状态检查完成,keyId: 1952978233106669569, exists: True +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:104] - 向量库状态检查完成,存在状态: True +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.init_mcp - INFO - [init_mcp.py:137] - 向量库已存在,无需创建 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:366] - MCP服务器初始化结果: {'code': 200, 'msg': 'MCP服务器初始化成功,向量库已存在', 'data': {'enterprise_id': '1952978233106669569', 'vector_store_created': False, 'vector_store_existed': True}} +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:371] - MCP服务器初始化成功 +2025-11-07 18:22:56 - lzwcai_mcp_iot.src.device_operations - INFO - [device_operations.py:61] - 企业ID已更新: 1952978233106669569 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:376] - 企业ID已保存到环境变量: 1952978233106669569 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:384] - 已注册的MCP工具: +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:385] - 1. iot_get_devices_by_location - 根据位置获取设备列表 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:386] - 2. iot_get_all_spaces_and_devices - 获取所有空间位置信息 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:387] - 3. iot_device_precise_controller - IoT设备精确控制 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:388] - 4. smart_space_device_locator_matcher - 智能空间设备定位 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:390] - ================================================================================ +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:391] - MCP服务器即将启动,等待客户端连接... +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:392] - 传输方式: stdio (标准输入输出) +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:393] - 注意: 控制台日志已禁用,所有日志将写入文件 +2025-11-07 18:22:56 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:394] - ================================================================================ +2025-11-07 18:23:00 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:119] - 开始处理获取所有空间位置信息请求 +2025-11-07 18:23:00 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:130] - 获取到企业ID: 1952978233106669569 +2025-11-07 18:23:00 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:499] - 查询设备列表,keyId: 1952978233106669569, location: +2025-11-07 18:23:00 - lzwcai_mcp_iot.src.vector_service - INFO - [vector_service.py:516] - 设备列表查询成功,返回设备数量: 27 +2025-11-07 18:23:00 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:137] - 查询所有设备结果: {'devices': [{'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'climate.qjiang_cn_741479129_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '32', 'location_desc': '研发办公区;研发部', 'entityId': 'switch.zimi_cn_1144259387_dhkg01_on_p_2_1', 'device_desc': '灵泽办公区左吊灯 开关 按键', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '33', 'location_desc': '研发办公区走廊过道;研发办公区走廊;研发办公区过道;研发部走廊', 'entityId': 'switch.zimi_cn_1144138206_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'climate.qjiang_cn_741478765_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_3_1', 'device_desc': '办公桌灯;主位灯;吊灯;灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'switch.zimi_cn_1119705212_dhkg05_on_p_4_1', 'device_desc': '入口灯;吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '34', 'location_desc': '何总办公室;总经办;总经房间', 'entityId': 'cover.lumi_cn_1132313226_hmcn02_s_2_2', 'device_desc': '左窗帘;窗帘;帘', 'operations': [{'command': 'open_cover', 'operation_desc': '打开;开;展开;开放', 'operation_params': []}, {'command': 'close_cover', 'operation_desc': '关闭;关;合并;合起来;合', 'operation_params': []}, {'command': 'toggle', 'operation_desc': '切换;一键开关', 'operation_params': []}, {'command': 'stop_cover', 'operation_desc': '停止;停;停一下;', 'operation_params': []}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'climate.qjiang_cn_741362991_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '35', 'location_desc': '灵泽展厅;灵泽展区', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'climate.qjiang_cn_741470846_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '36', 'location_desc': '大会议室;大会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '37', 'location_desc': '李总办公室;董事长;李总房间;董事长办公室', 'entityId': 'climate.qjiang_cn_741478700_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '38', 'location_desc': '公司前台;前台;前台大门;门口', 'entityId': 'climate.qjiang_cn_741479337_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'climate.qjiang_cn_741352250_wb20', 'device_desc': '空调', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '40', 'location_desc': '健身房;运动区;健身室;健', 'entityId': 'switch.zimi_cn_1144125565_dhkg01_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '41', 'location_desc': '小会议室;小会议区', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '42', 'location_desc': '电梯走廊;前台门口;公司门口;公司大门口;大门', 'entityId': 'button.zimi_cn_1119697824_dhkg05_toggle_a_4_1', 'device_desc': '吊灯;电梯灯;灯;照明灯', 'operations': [{'command': 'press', 'operation_desc': '按下;按;开关;打开或者关闭;', 'operation_params': []}]}, {'location_key': '43', 'location_desc': '公司大门;大门口;前台;公司前台;大门', 'entityId': 'switch.giot_cn_1110921716_v51ksm_on_p_2_1', 'device_desc': '大门开关', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '44', 'location_desc': '董事长办公室门口走廊;董事长门口走廊;董事长门口走廊过道', 'entityId': 'switch.huca_cn_1134957033_lh4_on_p_3_1', 'device_desc': '射灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '45', 'location_desc': '会议室过道;会议室走廊;会议区过道', 'entityId': 'switch.huca_cn_1134958712_lh4_on_p_5_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '46', 'location_desc': '爱易拍展厅;爱一拍展厅;爱一拍展区;爱一排展区', 'entityId': 'switch.zimi_cn_1144256905_dhkg02_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '47', 'location_desc': '灵泽展厅过道;灵泽展厅走廊', 'entityId': 'switch.zimi_cn_1121232402_dhkg05_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'climate.qjiang_cn_741348975_wb20', 'device_desc': '空调;制冷设备', 'operations': [{'command': 'turn_on', 'operation_desc': '打开空调;开空调;打开;开', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关空调;关闭空调;关闭;关;闭空调;空调', 'operation_params': []}, {'command': 'set_temperature', 'operation_desc': '设置温度;调节温度;', 'operation_params': [{'description': '温度', 'key': 'temperature', 'value': '7-35°C'}, {'description': '运行模式', 'key': 'hvac_mode', 'value': 'heat/cool/auto/dry/fan_only/off'}]}]}, {'location_key': '48', 'location_desc': '休息室一楼;休息室一层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '49', 'location_desc': '休息室二楼;休息室二层', 'entityId': 'switch.huca_cn_1134958682_lh4_on_p_3_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}, {'location_key': '50', 'location_desc': '前台休息区', 'entityId': 'switch.zimi_cn_1119697824_dhkg05_on_p_2_1', 'device_desc': '吊灯;灯;照明灯', 'operations': [{'command': 'turn_on', 'operation_desc': '开启;打开;开;', 'operation_params': []}, {'command': 'turn_off', 'operation_desc': '关闭;关', 'operation_params': []}]}], 'count': 27, 'location_filter': None} +2025-11-07 18:23:00 - lzwcai_mcp_iot.iot_device_tool - INFO - [iot_device_tool.py:184] - 已获取所有空间位置信息,空间数: 18 diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/__init__.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c9add4f Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/config.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..5bff17d Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/config.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_main.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_main.cpython-312.pyc new file mode 100644 index 0000000..8f71c3f Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_main.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_tool.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_tool.cpython-312.pyc new file mode 100644 index 0000000..ea46709 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_tool.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/apimock.json b/lzwcai_mcp_iot/lzwcai_mcp_iot/apimock.json new file mode 100644 index 0000000..6c44aa9 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/apimock.json @@ -0,0 +1,44 @@ +{ + "code": 200, + "data": { + "devices": [ + { + "location_key": "32", + "location_desc": "研发办公区;研发部", + "entityId": "climate.qjiang_cn_741479129_wb20", + "device_desc": "空调;制冷设备", + "operations": [ + { + "command": "turn_on", + "operation_desc": "打开空调;开空调;打开;开", + "operation_params": [] + }, + { + "command": "turn_off", + "operation_desc": "关空调;关闭空调;关闭;关;闭空调;空调", + "operation_params": [] + }, + { + "command": "set_temperature", + "operation_desc": "设置温度;调节温度;", + "operation_params": [ + { + "description": "温度", + "key": "temperature", + "value": "7-35°C" + }, + { + "description": "运行模式", + "key": "hvac_mode", + "value": "heat/cool/auto/dry/fan_only/off" + } + ] + } + ] + } + ], + "count": 1, + "location_filter": "爱一拍展厅" + }, + "msg": "成功获取公司 1952978233106669569 在位置 '爱一拍展厅' 的 2 个设备" +} \ No newline at end of file diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/config.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/config.py new file mode 100644 index 0000000..003411a --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/config.py @@ -0,0 +1,99 @@ +""" +配置文件 + +该模块包含了项目的所有配置常量和设置。 +""" + +import os +from typing import Dict, Any +from .src.logger_config import get_logger + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + +# 生产环境 +DEFAULT_DEVICE_API_BASE_URL = "http://lzwcai-demp-corp-manager:8086" +DEFAULT_VECTOR_API_BASE_URL = "http://lzwcai-demp-tool-server:5002" +# 本地环境 +# DEFAULT_DEVICE_API_BASE_URL = "http://192.168.2.236:8088" +# DEFAULT_VECTOR_API_BASE_URL = "http://192.168.2.236:5002" + + +# 默认企业ID +# DEFAULT_ENTERPRISE_ID = "1952978233106669569" +DEFAULT_ENTERPRISE_ID = "" + +# 默认员工ID +# DEFAULT_EMPLOYEE_ID = "1955949384389005313" +DEFAULT_EMPLOYEE_ID = "" + +# 请求配置 +REQUEST_TIMEOUT = 30 # 请求超时时间(秒) +MAX_RETRIES = 3 # 最大重试次数 + +# 向量服务配置 +DEFAULT_VECTOR_STORE_NAME = "设备库" +DEFAULT_VECTOR_STORE_DESCRIPTION = "向量库" +DEFAULT_ENCODER_TYPE = "word2vec" +DEFAULT_TOP_K = 1 +DEFAULT_AUTO_CREATE = True + +# 日志配置 +LOG_LEVEL = "INFO" +LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + +# 环境变量配置 +def get_config() -> Dict[str, Any]: + """ + 获取配置信息,支持环境变量覆盖 + + 返回: + Dict[str, Any]: 配置字典 + """ + logger = _ensure_logger() + logger.debug("开始加载配置...") + + config = { + "device_api_base_url": os.getenv( + "DEVICE_API_BASE_URL", DEFAULT_DEVICE_API_BASE_URL + ), + "vector_api_base_url": os.getenv( + "VECTOR_API_BASE_URL", DEFAULT_VECTOR_API_BASE_URL + ), + "enterprise_id": os.getenv("enterpriseId", DEFAULT_ENTERPRISE_ID), + "employeeId": os.getenv("employeeId", DEFAULT_EMPLOYEE_ID), + "request_timeout": int(os.getenv("REQUEST_TIMEOUT", REQUEST_TIMEOUT)), + "max_retries": int(os.getenv("MAX_RETRIES", MAX_RETRIES)), + "vector_store_name": os.getenv("VECTOR_STORE_NAME", DEFAULT_VECTOR_STORE_NAME), + "vector_store_description": os.getenv( + "VECTOR_STORE_DESCRIPTION", DEFAULT_VECTOR_STORE_DESCRIPTION + ), + "encoder_type": os.getenv("ENCODER_TYPE", DEFAULT_ENCODER_TYPE), + "default_top_k": int(os.getenv("DEFAULT_TOP_K", DEFAULT_TOP_K)), + "default_auto_create": os.getenv( + "DEFAULT_AUTO_CREATE", str(DEFAULT_AUTO_CREATE) + ).lower() + == "true", + "log_level": os.getenv("LOG_LEVEL", LOG_LEVEL), + "log_format": os.getenv("LOG_FORMAT", LOG_FORMAT), + } + + logger.info("配置加载完成") + logger.debug(f"设备API地址: {config['device_api_base_url']}") + logger.debug(f"向量API地址: {config['vector_api_base_url']}") + logger.debug(f"员工ID: {config['employeeId']}") + logger.debug(f"请求超时: {config['request_timeout']}秒") + + return config + + +# 全局配置实例 +CONFIG = get_config() diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/iot_device_tool.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/iot_device_tool.py new file mode 100644 index 0000000..2a86770 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/iot_device_tool.py @@ -0,0 +1,410 @@ +""" +IoT设备服务器主模块 + +该模块实现了一个简单的IoT设备控制服务器,使用FastMCP框架提供设备操作功能。 +""" + +import os +import json +from mcp.server.fastmcp import FastMCP +from .src.device_results_pretreatment import ( + format_devices_list, +) +from .src.device_operations import DeviceOperator +from .src.vector_service import VectorService +from .src.logger_config import get_logger +from .config import CONFIG +from .src.iot_device_dicts_prompt import ( + iot_get_devices_by_location_prompt, + iot_get_all_spaces_and_devices_prompt, + iot_device_precise_controller_prompt, + smart_space_device_locator_matcher_prompt, +) +from .src.init_mcp import init_mcp_server + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + +# 创建FastMCP实例 +mcp = FastMCP("iot_device_server") +# 创建设备操作实例 +device_op = DeviceOperator(api_base_url=CONFIG["device_api_base_url"]) +# 创建向量服务实例 +vector_service = VectorService(base_url=CONFIG["vector_api_base_url"]) + + +@mcp.tool(description=iot_get_devices_by_location_prompt()) +async def iot_get_devices_by_location( + location: str, +) -> str: + logger = _ensure_logger() + try: + # 输入参数验证 + if not location: + error_msg = "location参数是必需的" + logger.error(error_msg) + return json.dumps( + {"code": 400, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + logger.info( + f"开始处理IoT设备查询请求 - 位置: {location}" + ) + + # 从环境变量获取企业ID(已在启动时初始化) + enterprise_id = os.environ.get("enterpriseId") + if not enterprise_id: + error_msg = f"企业ID未初始化,请检查员工ID配置" + logger.error(error_msg) + return json.dumps( + {"code": 404, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + logger.info(f"获取到企业ID: {enterprise_id}") + + # 查询设备列表 + query_result = vector_service.query_devices_by_location( + keyId=enterprise_id, + location=location + ) + logger.info(f"查询设备列表结果: {query_result}") + + # 提取设备列表 + devices = query_result.get("devices", []) + device_count = query_result.get("count", 0) + + if device_count == 0 or not devices: + return json.dumps({ + "code": 404, + "msg": f"在位置「{location}」未找到任何设备", + "data": None + }, ensure_ascii=False) + + # 使用格式化函数将设备列表转换为易读的文本 + formatted_text = format_devices_list(devices) + logger.info(f"设备列表已格式化,设备数量: {device_count}") + + # 返回包含格式化文本和原始数据的结果 + result = { + "code": 200, + "msg": formatted_text, # 直接将格式化文本放在msg字段 + "data": { + "devices": devices, # 原始设备数据,供后续操作使用 + "count": device_count, + "location_filter": query_result.get("location_filter", location) + } + } + + return json.dumps(result, ensure_ascii=False) + + except Exception as e: + error_msg = f"IoT设备查询过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return json.dumps( + {"code": 500, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + +@mcp.tool(description=iot_get_all_spaces_and_devices_prompt()) +async def iot_get_all_spaces_and_devices() -> str: + logger = _ensure_logger() + try: + logger.info("开始处理获取所有空间位置信息请求") + + # 从环境变量获取企业ID(已在启动时初始化) + enterprise_id = os.environ.get("enterpriseId") + if not enterprise_id: + error_msg = f"企业ID未初始化,请检查员工ID配置" + logger.error(error_msg) + return json.dumps( + {"code": 404, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + logger.info(f"获取到企业ID: {enterprise_id}") + + # 查询所有设备(不指定位置,获取所有) + query_result = vector_service.query_devices_by_location( + keyId=enterprise_id, + location="" # 空字符串表示获取所有 + ) + logger.info(f"查询所有设备结果: {query_result}") + + # 提取设备列表 + devices = query_result.get("devices", []) + device_count = query_result.get("count", 0) + + if device_count == 0 or not devices: + return json.dumps({ + "code": 404, + "msg": "系统中未找到任何设备和空间", + "data": None + }, ensure_ascii=False) + + # 提取所有唯一的空间名称 + spaces_set = set() + for device in devices: + # 提取空间信息 + location_desc = device.get("location_desc", "") + if location_desc and location_desc != "未知空间": + spaces_set.add(location_desc) + + # 转换为列表并排序 + spaces_list = sorted(list(spaces_set)) + + if not spaces_list: + return json.dumps({ + "code": 404, + "msg": "系统中未找到任何有效空间", + "data": None + }, ensure_ascii=False) + + # 构建格式化文本 + formatted_lines = [] + formatted_lines.append("=" * 60) + formatted_lines.append(f"所有空间位置信息") + formatted_lines.append("=" * 60) + formatted_lines.append(f"空间总数: {len(spaces_list)} 个") + formatted_lines.append("") + + # 列出所有空间 + for space_idx, space_name in enumerate(spaces_list, 1): + formatted_lines.append(f"{space_idx}. {space_name}") + + formatted_lines.append("") + formatted_lines.append("=" * 60) + formatted_text = "\n".join(formatted_lines) + + logger.info(f"已获取所有空间位置信息,空间数: {len(spaces_list)}") + + # 返回包含格式化文本和空间列表的结果 + result = { + "code": 200, + "msg": formatted_text, + "data": { + "spaces": spaces_list, # 空间名称列表 + "space_count": len(spaces_list) + } + } + + return json.dumps(result, ensure_ascii=False) + + except Exception as e: + error_msg = f"获取所有空间位置信息过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return json.dumps( + {"code": 500, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + +@mcp.tool(description=iot_device_precise_controller_prompt()) +async def iot_device_precise_controller( + entityId: str, + command: str, + params: dict = None, + userId: str = None, +) -> str: + logger = _ensure_logger() + logger.info("=" * 60) + logger.info("调用iot_device_precise_controller工具") + logger.info(f"参数 - entityId: {entityId}, command: {command}, params: {params}, userId: {userId}") + logger.info("=" * 60) + + try: + # 输入参数验证 + logger.debug("开始参数验证...") + if not all([entityId, command]): + error_msg = "所有参数都是必需的: entityId,command" + logger.error(f"参数验证失败: {error_msg}") + return json.dumps( + {"code": 400, "msg": error_msg, "data": None}, ensure_ascii=False + ) + logger.debug("参数验证通过") + + # 从环境变量获取企业ID(已在启动时初始化) + enterprise_id = os.environ.get("enterpriseId") + if not enterprise_id: + error_msg = f"企业ID未初始化,请检查员工ID配置" + logger.error(error_msg) + return json.dumps( + {"code": 404, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + logger.info(f"获取到企业ID: {enterprise_id}") + + map_result = { + "enterpriseId": enterprise_id, + "entityId": entityId, + "command": command, + "params": params if params is not None else {}, + } + # 如果提供了userId,则添加到map_result中 + if userId: + map_result["userId"] = userId + # 操作设备 + result = device_op.operate_device(map_result) + logger.info(f"设备操作结果: {result}") + + # 将结果字典转换为JSON字符串 + if isinstance(result, dict): + result = json.dumps(result, ensure_ascii=False) + + logger.info("iot_device_precise_controller工具执行完成") + logger.info("=" * 60) + return result + + except Exception as e: + error_msg = f"IoT设备操作过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + logger.error("iot_device_precise_controller工具执行失败") + logger.error("=" * 60) + return json.dumps( + {"code": 500, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + + +@mcp.tool(description=smart_space_device_locator_matcher_prompt()) +async def smart_space_device_locator_matcher( + userId: str, +) -> str: + logger = _ensure_logger() + try: + if not userId: + error_msg = "所有参数都是必需的: userId" + logger.error(error_msg) + return json.dumps( + {"code": 400, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + result = device_op.get_user_spaces_by_user_id(userId) + if isinstance(result, dict): + # 检查返回的data是否为None,如果是则提供友好的描述 + if result.get("data") is None: + result["msg"] = "我目前还没发现您在哪里 请您告诉我你所属位置;" + return json.dumps(result, ensure_ascii=False) + return result + except Exception as e: + error_msg = f"定位空间过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return json.dumps( + {"code": 500, "msg": error_msg, "data": None}, ensure_ascii=False + ) + +def testFn() -> str: + logger = _ensure_logger() + try: + # 读取test.json文件 + test_file_path = r"E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_iot\test.json" + with open(test_file_path, "r", encoding="utf-8") as f: + test_data = json.load(f) + + # 获取results数据 + results = test_data.get("results", []) + logger.info(f"从test.json读取到的results数据: {results}") + + # 参数预处理 + map_result = device_op.preprocess_results(results) + logger.info(f"参数预处理结果: {map_result}") + + # 保存map_result到本地JSON文件 + output_file_path = r"E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_iot\map_result.json" + with open(output_file_path, "w", encoding="utf-8") as f: + json.dump(map_result, f, ensure_ascii=False, indent=2) + logger.info(f"map_result已保存到: {output_file_path}") + + return json.dumps(map_result, ensure_ascii=False) + + except FileNotFoundError: + error_msg = "test.json文件未找到" + logger.error(error_msg) + return json.dumps( + {"code": 404, "msg": error_msg, "data": None}, ensure_ascii=False + ) + except json.JSONDecodeError as e: + error_msg = f"test.json文件格式错误: {str(e)}" + logger.error(error_msg) + return json.dumps( + {"code": 400, "msg": error_msg, "data": None}, ensure_ascii=False + ) + except Exception as e: + error_msg = f"测试过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return json.dumps( + {"code": 500, "msg": error_msg, "data": None}, ensure_ascii=False + ) + + +def main(): + """主函数,启动MCP服务器""" + logger = _ensure_logger() + try: + logger.info("=" * 80) + logger.info("IoT设备MCP服务器启动流程开始") + logger.info(f"配置信息: {CONFIG}") + logger.info("=" * 80) + + # 初始化组件 + logger.info("正在初始化核心组件...") + logger.info(f"设备操作器API地址: {CONFIG['device_api_base_url']}") + logger.info(f"向量服务API地址: {CONFIG['vector_api_base_url']}") + logger.info("核心组件初始化完成") + + # 获取企业ID并初始化MCP服务器 + enterprise_id = CONFIG.get("enterprise_id") + if enterprise_id: + logger.info(f"检测到企业ID配置: {enterprise_id}") + logger.info("开始初始化MCP服务器...") + + init_result = init_mcp_server(device_op, vector_service, enterprise_id) + logger.info(f"MCP服务器初始化结果: {init_result}") + + if init_result.get("code") != 200: + logger.warning(f"MCP服务器初始化失败,但服务器仍将启动: {init_result}") + else: + logger.info("MCP服务器初始化成功") + # 保存企业ID到环境变量 + returned_enterprise_id = init_result.get("data", {}).get("enterprise_id") + if returned_enterprise_id: + device_op.set_enterprise_id_to_env(returned_enterprise_id) + logger.info(f"企业ID已保存到环境变量: {returned_enterprise_id}") + else: + # 如果返回结果中没有enterprise_id,使用配置中的 + device_op.set_enterprise_id_to_env(enterprise_id) + logger.info(f"企业ID已保存到环境变量: {enterprise_id}") + else: + logger.warning("未配置企业ID,跳过预初始化") + logger.info("提示:您可以在配置文件或环境变量中设置ENTERPRISE_ID来启用自动初始化功能") + + # 注册的工具列表日志 + logger.info("已注册的MCP工具:") + logger.info("1. iot_get_devices_by_location - 根据位置获取设备列表") + logger.info("2. iot_get_all_spaces_and_devices - 获取所有空间位置信息") + logger.info("3. iot_device_precise_controller - IoT设备精确控制") + logger.info("4. smart_space_device_locator_matcher - 智能空间设备定位") + + logger.info("=" * 80) + logger.info("MCP服务器即将启动,等待客户端连接...") + logger.info("传输方式: stdio (标准输入输出)") + logger.info("注意: 控制台日志已禁用,所有日志将写入文件") + logger.info("=" * 80) + + # 使用标准输入输出作为传输方式运行服务器 + mcp.run(transport="stdio") + + except Exception as e: + logger.error("=" * 80) + logger.error(f"服务器启动失败: {str(e)}") + logger.error("错误详情:", exc_info=True) + logger.error("=" * 80) + raise + + +if __name__ == "__main__": + main() diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__init__.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..daef1c6 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_operations.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_operations.cpython-312.pyc new file mode 100644 index 0000000..a3762f8 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_operations.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_results_pretreatment.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_results_pretreatment.cpython-312.pyc new file mode 100644 index 0000000..2f3bc99 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_results_pretreatment.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/init_mcp.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/init_mcp.cpython-312.pyc new file mode 100644 index 0000000..445d6cb Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/init_mcp.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/iot_device_dicts_prompt.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/iot_device_dicts_prompt.cpython-312.pyc new file mode 100644 index 0000000..ad272c0 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/iot_device_dicts_prompt.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/logger_config.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/logger_config.cpython-312.pyc new file mode 100644 index 0000000..1812d54 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/vector_service.cpython-312.pyc b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/vector_service.cpython-312.pyc new file mode 100644 index 0000000..4ed0a25 Binary files /dev/null and b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/vector_service.cpython-312.pyc differ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_operations.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_operations.py new file mode 100644 index 0000000..1ded4f8 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_operations.py @@ -0,0 +1,751 @@ +""" +设备操作模块 + +该模块提供了设备操作的接口,用于控制不同位置的各种设备。 +""" + +import os +import requests +import json +from typing import Dict, Any, Optional, List +from ..config import CONFIG +from .device_results_pretreatment import process_device_data, process_device_results +from .logger_config import get_logger + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + +enterprise_id = os.environ.get("enterpriseId", CONFIG["enterprise_id"]) + + +class DeviceOperator: + """设备操作类,提供设备控制功能""" + + def __init__(self, api_base_url: str = None): + """ + 初始化设备操作器 + + 参数: + api_base_url: API服务的基础URL + """ + logger = _ensure_logger() + self.api_base_url = api_base_url or CONFIG["device_api_base_url"] + self.session = requests.Session() + # 设置默认超时 + self.session.timeout = CONFIG["request_timeout"] + + logger.info(f"DeviceOperator初始化完成,API基础URL: {self.api_base_url}") + + def set_enterprise_id_to_env(self, enterprise_id_value: str) -> None: + """ + 将企业ID存入环境变量 + + 参数: + enterprise_id_value: 企业ID值 + """ + logger = _ensure_logger() + if not enterprise_id_value or not isinstance(enterprise_id_value, str): + logger.warning(f"无效的企业ID值: {enterprise_id_value}") + return + + os.environ["enterpriseId"] = enterprise_id_value + global enterprise_id + enterprise_id = enterprise_id_value + logger.info(f"企业ID已更新: {enterprise_id_value}") + + def get_enterprise_id_by_user(self, user_id: str) -> Optional[str]: + """ + 根据用户ID获取企业ID并存入环境变量 + + 参数: + user_id: 用户ID + + 返回: + Optional[str]: 企业ID,获取失败时返回None + """ + logger = _ensure_logger() + if not user_id or not isinstance(user_id, str): + logger.error(f"无效的用户ID: {user_id}") + return None + + # 调用实际API获取企业ID + url = f"{self.api_base_url}/system/enterprise/user/{user_id}" + headers = { + "Accept": "*/*", + } + + try: + logger.info(f"正在获取用户 {user_id} 的企业ID...") + response = self.session.get( + url=url, headers=headers, timeout=CONFIG["request_timeout"] + ) + + if response.status_code == 200: + data = response.json() + if ( + data.get("code") == 200 + and data.get("data") + and data["data"].get("id") + ): + enterprise_id_value = str(data["data"]["id"]) + # 将企业ID存入环境变量 + self.set_enterprise_id_to_env(enterprise_id_value) + logger.info(f"成功获取企业ID: {enterprise_id_value}") + return enterprise_id_value + else: + logger.warning(f"API返回数据格式异常: {data}") + else: + logger.error( + f"API请求失败,状态码: {response.status_code}, 响应: {response.text}" + ) + + # 如果API调用失败或数据格式不符预期,返回默认值 + logger.info(f"使用默认企业ID: {enterprise_id}") + return enterprise_id + + except requests.exceptions.Timeout: + logger.error(f"获取企业ID请求超时,用户ID: {user_id}") + return enterprise_id + except requests.exceptions.RequestException as e: + logger.error(f"获取企业ID网络请求异常: {str(e)}") + return enterprise_id + except Exception as e: + logger.error(f"获取企业ID时发生未知异常: {str(e)}", exc_info=True) + return enterprise_id + + # 设备结果分数判断 + def check_device_results_score( + self, best_match_data: Optional[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + 检查设备匹配的准确性 + + 根据你的代码分析,评分系统说明: + - 综合分数(combined_score)或score是最终评判标准 + - 0.85-0.88: 准确匹配,可信度高 (随机阈值) + - 0.7-0.84: 一般匹配,需要确认 + - 0.5-0.69: 较差匹配,不建议使用 + - <0.5: 很差匹配,不推荐 + + Args: + best_match_data: best_match数据,包含device和score信息 + + Returns: + Dict: { + "checkResult": True/False, # 是否满足分数要求(随机阈值0.85-0.88) + "data": data # 满足要求返回对应项,不满足返回整个data + } + """ + logger.debug("开始检查设备匹配准确性...") + + import random + + # 提取分数 - 优先使用combined_score,其次使用score + score = best_match_data.get("combined_score", best_match_data.get("score", 0.0)) + logger.debug(f"提取的匹配分数: {score}") + + # 生成随机阈值 (0.85-0.88之间) + random_threshold = round(random.uniform(0.6, 0.65), 2) + logger.debug(f"生成的随机阈值: {random_threshold}") + + # 判断是否满足准确性要求 (随机阈值-1.0) + is_accurate = random_threshold <= score <= 1.0 + + logger.info(f"设备匹配准确性检查: 分数={score}, 阈值={random_threshold}, 通过={is_accurate}") + + # 构造返回结果 + result = {"checkResult": is_accurate, "data": best_match_data} + return result + + def preprocess_results(self, raw_data: List[List[Any]]) -> List[Dict[str, Any]]: + """ + 预处理数据,将原始数据转换为简化的数组对象格式 + 如果 entityId 相同,则合并到一起 + + Args: + raw_data: 原始数据,格式为 [[entity, score], ...] + + Returns: + 处理后的数据数组,每个对象包含合并后的信息(不包含id和score_details字段) + """ + logger.debug(f"开始预处理设备结果数据,原始数据条数: {len(raw_data) if raw_data else 0}") + + try: + result = process_device_results(raw_data) + logger.info(f"设备结果数据预处理完成,输出条数: {len(result) if result else 0}") + return result + except Exception as e: + logger.error(f"预处理设备结果数据时发生异常: {str(e)}", exc_info=True) + return [] + + def preprocess_parameters( + self, + data: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + """ + 预处理设备操作参数,使用process_device_data方法处理数据 + + 参数: + data: 需要预处理的数据,包含best_match信息 + 格式如:{ + "best_match": { + "device": { + "device": { + "key": "switch.zimi_cn_1144138206_dhkg01_on_p_2_1", + "description": "灵泽办公区过道吊灯 开关 按键" + }, + "operation": { + "key": "turn_on", + "description": "开关" + } + } + } + } + + 返回: + Dict[str, Any]: 预处理后的设备操作参数 + 格式如:{ + "enterpriseId": "1932095424144715777", + "entityId": "switch.zimi_cn_1144138206_dhkg01_on_p_2_1", + "command": "turn_on" + } + """ + # 默认结果结构 + default_result = {"enterpriseId": enterprise_id, "entityId": "", "command": ""} + + try: + # 使用process_device_data方法处理数据 + if data and isinstance(data, dict): + processed_result = process_device_data(data, enterprise_id) + if processed_result: + logger.info(f"成功处理设备数据: {processed_result}") + return processed_result + else: + logger.warning("process_device_data返回None,使用默认结果") + else: + logger.warning("输入数据为空或格式异常") + + except Exception as e: + logger.error(f"预处理参数时发生异常: {str(e)}", exc_info=True) + + return default_result + + def operate_device(self, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + 操作设备的方法 + + 参数: + data: 设备操作数据,包含entityId、command等信息 + + 返回: + Dict[str, Any]: 包含操作结果的字典 + """ + # 初始化操作参数 + if data is None: + data = {} + + # 检查并设置 enterpriseId + if not data.get("enterpriseId"): + data["enterpriseId"] = enterprise_id + logger.info(f"data中未包含enterpriseId,使用默认值: {enterprise_id}") + + # 验证必要参数 + if not data.get("entityId") or not data.get("command"): + error_msg = "缺少必要的设备操作参数: entityId 和 command" + logger.error(error_msg) + return {"code": 400, "msg": error_msg, "data": None} + + logger.info( + f"开始操作设备 - entityId: {data.get('entityId')}, command: {data.get('command')}" + ) + # # 保存map_result到本地JSON文件 + # output_file_path = r"E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_iot\output\data.json" + # with open(output_file_path, "w", encoding="utf-8") as f: + # json.dump(data, f, ensure_ascii=False, indent=2) + # logger.info(f"map_result已保存到: {output_file_path}") + + # 调用API接口 + result = self.call_device_api(data) + # result = {"code": 200, "msg": "操作成功", "data": None} + return result + + def batch_operate_devices(self, devices_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + 批量操作设备的方法 + + 参数: + devices_data: 设备操作数据数组,每个元素包含entityId、command等信息 + 数据结构与operate_device方法的data参数相同 + + 返回: + Dict[str, Any]: 批量操作结果,包含成功/失败统计和详细结果 + { + "code": 200, # 整体状态码 + "msg": "批量操作完成", + "data": { + "total": 总数量, + "success": 成功数量, + "failed": 失败数量, + "results": [ + { + "index": 索引, + "deviceInfo": 设备信息, + "result": 操作结果, + "success": True/False + } + ] + } + } + """ + if not devices_data or not isinstance(devices_data, list): + error_msg = "无效的设备数据:devices_data必须是一个非空数组" + logger.error(error_msg) + return {"code": 400, "msg": error_msg, "data": None} + + total_count = len(devices_data) + success_count = 0 + failed_count = 0 + results = [] + + logger.info(f"开始批量操作设备,总数量: {total_count}") + + for index, device_data in enumerate(devices_data): + device_info = { **device_data,"index": index} + + try: + logger.info(f"正在操作第 {index + 1}/{total_count} 个设备: {device_info['entityId']}") + + # 验证设备数据格式 + if not isinstance(device_data, dict): + raise ValueError(f"设备数据格式错误,必须是字典类型") + + # 调用单个设备操作方法 + operation_result = self.operate_device(device_data) + + # 判断操作是否成功(通常code=200表示成功) + is_success = operation_result.get("code") == 200 + + if is_success: + success_count += 1 + logger.info(f"设备操作成功: {device_info['entityId']}") + else: + failed_count += 1 + logger.warning(f"设备操作失败: {device_info['entityId']}, 原因: {operation_result.get('msg', '未知错误')}") + + # 记录结果 + results.append({ + "index": index, + "deviceInfo": device_info, + "result": operation_result, + "success": is_success + }) + + except Exception as e: + failed_count += 1 + error_msg = f"设备操作异常: {str(e)}" + logger.error(f"第 {index + 1} 个设备操作异常: {error_msg}", exc_info=True) + + # 记录异常结果 + results.append({ + "index": index, + "deviceInfo": device_info, + "result": {"code": 500, "msg": error_msg, "data": None}, + "success": False + }) + + # 构建返回结果 + summary_msg = f"批量操作完成,总数: {total_count}, 成功: {success_count}, 失败: {failed_count}" + logger.info(summary_msg) + + # 整体状态码判断:如果全部成功则返回200,部分成功返回206,全部失败返回500 + if success_count == total_count: + overall_code = 200 + overall_msg = "批量操作全部成功" + elif success_count > 0: + overall_code = 206 # 部分成功 + overall_msg = f"批量操作部分成功,成功: {success_count}, 失败: {failed_count}" + else: + overall_code = 500 + overall_msg = "批量操作全部失败" + + return { + "code": overall_code, + "msg": overall_msg, + "data": { + "total": total_count, + "success": success_count, + "failed": failed_count, + "success_rate": round(success_count / total_count * 100, 2) if total_count > 0 else 0, + "results": results + } + } + + def get_digital_employee_by_id(self, employee_id: str) -> Dict[str, Any]: + """ + 根据员工ID获取数字员工信息 + + 参数: + employee_id: 员工ID + + 返回: + Dict[str, Any]: 包含数字员工信息的字典 + """ + if not employee_id or not isinstance(employee_id, str): + error_msg = f"无效的员工ID: {employee_id}" + logger.error(error_msg) + return {"code": 400, "msg": error_msg, "data": None} + + # API配置 + url = f"{self.api_base_url}/system/mcpServer/getByEmployeeId/{employee_id}" + headers = {} + + try: + logger.info(f"正在获取员工ID为 {employee_id} 的数字员工信息...") + response = self.session.get( + url=url, headers=headers, timeout=CONFIG["request_timeout"] + ) + + # 处理响应 + if response.status_code == 200: + data = response.json() + logger.info(f"成功获取数字员工信息: {data}") + return data + else: + error_msg = f"API请求失败,状态码: {response.status_code}, 响应: {response.text}" + logger.error(error_msg) + return { + "code": response.status_code, + "msg": error_msg, + "data": None, + } + + except requests.exceptions.Timeout: + error_msg = f"获取数字员工信息请求超时,员工ID: {employee_id}" + logger.error(error_msg) + return {"code": 408, "msg": error_msg, "data": None} + except requests.exceptions.RequestException as e: + error_msg = f"获取数字员工信息网络请求异常: {str(e)}" + logger.error(error_msg) + return {"code": 500, "msg": error_msg, "data": None} + except Exception as e: + error_msg = f"获取数字员工信息时发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"code": 500, "msg": error_msg, "data": None} + + def get_user_spaces_by_user_id(self, user_id: str) -> Dict[str, Any]: + """ + 根据用户ID获取该用户关联的空间信息 + + 参数: + user_id: 用户ID + + 返回: + Dict[str, Any]: 接口响应结果,结构为 {"code": number, "msg": string, "data": any} + """ + if not user_id or not isinstance(user_id, str): + error_msg = f"无效的用户ID: {user_id}" + logger.error(error_msg) + return {"code": 400, "msg": error_msg, "data": None} + + url = f"{self.api_base_url}/system/mcpServer/space/user/{user_id}" + headers = { + "Accept": "*/*", + } + + try: + logger.info(f"正在获取用户ID为 {user_id} 的空间信息...") + response = self.session.get( + url=url, headers=headers, timeout=CONFIG["request_timeout"] + ) + + if response.status_code == 200: + try: + data = response.json() + except ValueError: + data = {} + + if isinstance(data, dict): + # 统一返回格式,确保存在 code/msg/data 字段 + if "code" not in data: + data["code"] = 200 + if "msg" not in data: + data["msg"] = "success" + # 接口可能没有返回 data 时,显式置为 None + if "data" not in data or data["data"] in (None, [], {}): + data["data"] = None + logger.info(f"成功获取用户空间信息: {data}") + return data + else: + wrapped = {"code": 200, "msg": "success", "data": None} + logger.info(f"成功获取用户空间信息(非字典响应已包装): {wrapped}") + return wrapped + else: + error_msg = f"API请求失败,状态码: {response.status_code}, 响应: {response.text}" + logger.error(error_msg) + return { + "code": response.status_code, + "msg": error_msg, + "data": None, + } + + except requests.exceptions.Timeout: + error_msg = f"获取用户空间信息请求超时,用户ID: {user_id}" + logger.error(error_msg) + return {"code": 408, "msg": error_msg, "data": None} + except requests.exceptions.RequestException as e: + error_msg = f"获取用户空间信息网络请求异常: {str(e)}" + logger.error(error_msg) + return {"code": 500, "msg": error_msg, "data": None} + except Exception as e: + error_msg = f"获取用户空间信息时发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"code": 500, "msg": error_msg, "data": None} + + def call_device_api(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 调用设备操作API + + 参数: + data: 设备操作数据,包含entityId、command等信息 + + 返回: + Dict[str, Any]: API调用结果 + """ + # API配置 + url = f"{self.api_base_url}/space_iot/operation/call" + headers = { + "Content-Type": "application/json", + } + + # 构建请求数据 + request_data = {**data} + + try: + logger.info(f"调用设备操作API: {url}") + logger.debug(f"请求数据: {request_data}") + + # 发送POST请求 + response = self.session.post( + url=url, + headers=headers, + data=json.dumps(request_data), + timeout=CONFIG["request_timeout"], + ) + + # 处理响应 + if response.status_code == 200: + result = response.json() + logger.info(f"设备操作API调用成功: {result}") + return result + else: + error_msg = f"API请求失败,状态码: {response.status_code}, 响应: {response.text}" + logger.error(error_msg) + return { + "code": response.status_code, + "msg": error_msg, + "data": None, + } + + except requests.exceptions.Timeout: + error_msg = "设备操作API请求超时" + logger.error(error_msg) + return {"code": 408, "msg": error_msg, "data": None} + except requests.exceptions.RequestException as e: + error_msg = f"设备操作API网络请求异常: {str(e)}" + logger.error(error_msg) + return {"code": 500, "msg": error_msg, "data": None} + except Exception as e: + error_msg = f"设备操作API调用异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"code": 500, "msg": error_msg, "data": None} + + + def get_operation_analysis_result(self, tool_data: Dict[str, Any], best_match: Optional[Dict[str, Any]]) -> str: + """ + 分析操作结果,根据 tool_data 和 best_match 返回合适的操作命令 + + 参数: + tool_data: 工具数据,包含 location、device、operation、keyId + best_match: 最佳匹配结果,包含设备信息和评分 + + 返回: + str: 操作命令字符串 + """ + logger.debug("开始分析操作结果...") + logger.debug(f"输入参数 - tool_data: {tool_data}, best_match: {best_match}") + + def contains_chinese(text: str) -> bool: + """检查字符串是否包含中文字符""" + if not text: + return False + for char in text: + if '\u4e00' <= char <= '\u9fff': + return True + return False + + try: + # 获取 tool_data 中的 operation + tool_operation = tool_data.get("operation", "") if tool_data else "" + logger.debug(f"tool_data 中的 operation: {tool_operation}") + + # 判断 tool_data 的 operation 是否是纯英文(不包含中文) + if tool_operation and not contains_chinese(tool_operation): + logger.info(f"tool_data operation 为纯英文,直接使用: {tool_operation}") + return tool_operation + + # 如果 tool_data 的 operation 包含中文或为空,检查 best_match + logger.debug("tool_data operation 包含中文或为空,尝试使用 best_match") + if best_match and isinstance(best_match, dict): + device_info = best_match.get("device", {}) + if device_info and isinstance(device_info, dict): + operation_info = device_info.get("operation", {}) + if operation_info and isinstance(operation_info, dict): + operation_key = operation_info.get("key", "") + if operation_key: + logger.info(f"从 best_match 获取到操作命令: {operation_key}") + return operation_key + + # 如果都没有有效值,返回原始的 tool_operation 或空字符串 + logger.warning(f"未找到有效的操作命令,返回原始值: {tool_operation}") + return tool_operation or "" + + except Exception as e: + logger.error(f"分析操作结果时发生异常: {str(e)}", exc_info=True) + return "" + + def __del__(self): + """析构函数,关闭会话""" + if hasattr(self, "session"): + self.session.close() + + +# 使用示例 +if __name__ == "__main__": + # 测试代码只在直接运行此模块时执行,不在导入时执行 + # 以下代码已注释避免MCP服务器启动时执行 + pass + + # device_op = DeviceOperator() + + # # 测试获取数字员工信息 + # employee_id = "1950037223825125378" + # employee_result = device_op.get_digital_employee_by_id(employee_id) + # logger.info(f"数字员工信息: {employee_result}") + + # 以下所有测试代码已注释,避免MCP服务器启动时执行 + + # device_op = DeviceOperator() + + # # 测试获取数字员工信息 + # employee_id = "1950037223825125378" + # employee_result = device_op.get_digital_employee_by_id(employee_id) + # logger.info(f"数字员工信息: {employee_result}") + + # # 测试单个设备操作 + # data = { + # "enterpriseId": "1932095424144715777", + # "entityId": "climate.qjiang_cn_741479129_wb20", + # "command": "set_temperature", + # "params": { + # "temperature": 24, + # }, + # } + # result = device_op.operate_device(data) + # logger.info(f"设备操作结果: {result}") + + # # 测试批量设备操作 + # batch_data = [ + # { + # "enterpriseId": "1932095424144715777", + # "entityId": "switch.office_light_1", + # "command": "turn_on", + # "params": {} + # }, + # { + # "enterpriseId": "1932095424144715777", + # "entityId": "climate.office_ac_1", + # "command": "set_temperature", + # "params": { + # "temperature": 25, + # } + # }, + # { + # "enterpriseId": "1932095424144715777", + # "entityId": "switch.meeting_room_projector", + # "command": "turn_off", + # "params": {} + # } + # ] + + # batch_result = device_op.batch_operate_devices(batch_data) + # logger.info(f"批量设备操作结果: {batch_result}") + + # # 输出批量操作统计信息 + # if batch_result.get("code") in [200, 206]: # 成功或部分成功 + # batch_data_result = batch_result.get("data", {}) + # logger.info(f"批量操作统计: 总数={batch_data_result.get('total')}, " + # f"成功={batch_data_result.get('success')}, " + # f"失败={batch_data_result.get('failed')}, " + # f"成功率={batch_data_result.get('success_rate')}%") + + # # 测试操作分析结果方法 + # # 测试用例1:tool_data 包含纯英文操作 + # tool_data_en = { + # "location": "office", + # "device": "air_conditioner", + # "operation": "turn_on", + # "keyId": "1932095424144715777" + # } + + # best_match_data = { + # "device": { + # "id": "d67d81ef-1c0e-4049-ae03-4581604138fc", + # "location": { + # "key": "37", + # "description": "李总办公室;董事长;李总房间;董事长办公室" + # }, + # "device": { + # "key": "climate.qjiang_cn_741478700_wb20", + # "description": "空调;制冷设备" + # }, + # "operation": { + # "key": "set_temperature", + # "description": "设置温度;调节温度;" + # }, + # "operation_params": [] + # }, + # "score": 0.9385, + # "confidence": "高" + # } + + # result1 = device_op.get_operation_analysis_result(tool_data_en, best_match_data) + # logger.info(f"测试1 - 纯英文操作: {result1}") # 应该返回 "turn_on" + + # # 测试用例2:tool_data 包含中文操作 + # tool_data_cn = { + # "location": "办公室", + # "device": "空调", + # "operation": "打开空调", + # "keyId": "1932095424144715777" + # } + + # result2 = device_op.get_operation_analysis_result(tool_data_cn, best_match_data) + # logger.info(f"测试2 - 中文操作: {result2}") # 应该返回 "set_temperature" + + # # 测试用例3:tool_data 操作为空,使用 best_match + # tool_data_empty = { + # "location": "office", + # "device": "air_conditioner", + # "operation": "", + # "keyId": "1932095424144715777" + # } + + # result3 = device_op.get_operation_analysis_result(tool_data_empty, best_match_data) + # logger.info(f"测试3 - 空操作: {result3}") # 应该返回 "set_temperature" diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_results_pretreatment.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_results_pretreatment.py new file mode 100644 index 0000000..cbefeb1 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_results_pretreatment.py @@ -0,0 +1,659 @@ +from typing import List, Dict, Any +from collections import defaultdict +import re +from .logger_config import get_logger + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + + +def process_device_data(data, enterprise_id): + """ + 处理设备数据,提取best_match并转换为指定格式 + + Args: + data (dict): 包含results和best_match的数据结构 + enterprise_id (str): 企业ID + + Returns: + dict: 处理后的best_match数据,包含enterpriseId、entityId和command + 如果处理失败返回None + """ + logger = _ensure_logger() + logger.debug(f"开始处理设备数据,enterprise_id: {enterprise_id}") + + try: + # 参数验证 + if not isinstance(data, dict): + error_msg = "data参数必须是字典类型" + logger.error(error_msg) + raise ValueError(error_msg) + + if not enterprise_id: + error_msg = "enterprise_id不能为空" + logger.error(error_msg) + raise ValueError(error_msg) + + # 检查数据是否包含best_match + if "best_match" not in data: + raise ValueError("数据中缺少best_match字段") + + best_match = data["best_match"] + + if not isinstance(best_match, dict): + raise ValueError("best_match必须是字典类型") + + # 检查best_match是否包含device字段 + if "device" not in best_match: + raise ValueError("best_match中缺少device字段") + + device_info = best_match["device"] + + if not isinstance(device_info, dict): + raise ValueError("device_info必须是字典类型") + + # 检查device_info是否包含device和operation字段 + if "device" not in device_info: + raise ValueError("device_info中缺少device字段") + + if "operation" not in device_info: + raise ValueError("device_info中缺少operation字段") + + device_data = device_info["device"] + operation_data = device_info["operation"] + + if not isinstance(device_data, dict) or not isinstance(operation_data, dict): + raise ValueError("device和operation字段必须是字典类型") + + # 提取device的key作为entityId + device_key = device_data.get("key", "") + if not device_key: + raise ValueError("device.key字段为空") + + # 提取operation的key作为command + operation_key = operation_data.get("key", "") + if not operation_key: + raise ValueError("operation.key字段为空") + + # 构建返回结果 + processed_result = { + "enterpriseId": enterprise_id, + "entityId": device_key, + "command": operation_key, + } + + logger.info(f"设备数据处理成功: entityId={device_key}, command={operation_key}") + logger.debug(f"处理结果: {processed_result}") + return processed_result + + except Exception as e: + logger.error(f"数据处理错误: {str(e)}", exc_info=True) + return None + + +def process_device_data_safe(data, enterprise_id, default_result=None): + """ + 安全版本的数据处理方法,不会抛出异常 + + Args: + data (dict): 包含results和best_match的数据结构 + enterprise_id (str): 企业ID + default_result (dict): 处理失败时的默认返回值 + + Returns: + dict: 处理后的best_match数据,处理失败时返回default_result + """ + result = process_device_data(data, enterprise_id) + return result if result is not None else default_result + + +def validate_device_data(data): + """ + 验证设备数据结构的完整性 + + Args: + data (dict): 要验证的数据 + + Returns: + tuple: (is_valid, error_message) + """ + try: + if not isinstance(data, dict): + return False, "数据必须是字典类型" + + if "best_match" not in data: + return False, "缺少best_match字段" + + best_match = data["best_match"] + if not isinstance(best_match, dict): + return False, "best_match必须是字典类型" + + if "device" not in best_match: + return False, "best_match中缺少device字段" + + device_info = best_match["device"] + if not isinstance(device_info, dict): + return False, "device_info必须是字典类型" + + required_fields = ["device", "operation"] + for field in required_fields: + if field not in device_info: + return False, f"device_info中缺少{field}字段" + + field_data = device_info[field] + if not isinstance(field_data, dict): + return False, f"{field}字段必须是字典类型" + + if "key" not in field_data or not field_data["key"]: + return False, f"{field}.key字段不能为空" + + return True, "数据验证通过" + + except Exception as e: + return False, f"验证过程中发生错误: {str(e)}" + + +def process_device_results(raw_data: List[List[Any]]) -> List[Dict[str, Any]]: + """ + 预处理数据,将原始数据转换为简化的数组对象格式 + 如果 entityId 相同,则合并到一起 + + Args: + raw_data: 原始数据,格式为 [[entity, score], ...] + + Returns: + 处理后的数据数组,每个对象包含合并后的信息(不包含id和score_details字段) + """ + logger = _ensure_logger() + logger.debug(f"开始处理设备结果数据,输入数据条数: {len(raw_data) if raw_data else 0}") + + # 使用 entityId 作为分组键 + grouped_data = defaultdict(list) + + for item in raw_data: + if len(item) >= 2: + entity = item[0] + score = item[1] + + logger.debug(f"处理项目: entity类型={type(entity)}, 包含keys={list(entity.keys()) if isinstance(entity, dict) else 'N/A'}") + + entityId = entity.get("device", {}).get("key", "") + logger.debug(f"提取的entityId: '{entityId}'") + + # 如果entityId为空,跳过这个实体 + if not entityId: + logger.warning(f"跳过entityId为空的实体: {entity}") + continue + + # 创建简化的对象 + simplified_entity = { + "location_key": entity.get("location", {}).get("key"), + "location_desc": entity.get("location", {}).get("description"), + "entityId": entity.get("device", {}).get("key"), + "device_desc": entity.get("device", {}).get("description"), + "command": entity.get("operation", {}).get("key"), + "operation_desc": entity.get("operation", {}).get("description"), + "operation_params": entity.get("operation_params", []), + "score": score, + } + + logger.debug(f"创建的简化实体: {simplified_entity}") + grouped_data[entityId].append(simplified_entity) + + # 合并相同 entityId 的数据 + result = [] + logger.debug(f"grouped_data包含{len(grouped_data)}个组: {list(grouped_data.keys())}") + + for entityId, entities in grouped_data.items(): + logger.debug(f"处理entityId '{entityId}' 的 {len(entities)} 个实体") + if len(entities) == 1: + # 只有一个实体,直接添加 + result.append(entities[0]) + logger.debug(f"设备 {entityId} 只有一个实体,直接添加") + else: + # 多个实体需要合并 + merged_entity = merge_entities(entities) + result.append(merged_entity) + logger.debug(f"设备 {entityId} 有 {len(entities)} 个实体,已合并") + + logger.info(f"设备结果数据处理完成,输出数据条数: {len(result)}") + logger.debug(f"最终结果: {result}") + return result + + +def merge_entities(entities: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + 合并相同 entityId 的多个实体 + + Args: + entities: 需要合并的实体列表 + + Returns: + 合并后的实体 + """ + if not entities: + return {} + + # 选择分数最高的作为主实体 + main_entity = max(entities, key=lambda x: x.get("score", 0)) + + # 收集所有操作 + operations = [] + + for entity in entities: + operation_info = { + "command": entity.get("command"), + "operation_desc": entity.get("operation_desc"), + "operation_params": entity.get("operation_params", []), + "score": entity.get("score", 0), + } + operations.append(operation_info) + + # 创建合并后的实体 + merged_entity = { + "location_key": main_entity.get("location_key"), + "location_desc": main_entity.get("location_desc"), + "entityId": main_entity.get("entityId"), + "device_desc": main_entity.get("device_desc"), + "operations": operations, # 所有可能的操作 + } + + return merged_entity + + +def extract_space_entity_and_location(space_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 从空间数据结构中提取 entityId 与 location。 + + 期望输入示例: + { + "spaceId": "46", + "spaceAliasName": "爱易拍展厅;爱一拍展厅;爱一拍展区", + "spaceName": "爱易拍展厅", + ... + } + + 处理规则: + - entityId = spaceId(转为字符串) + - location 来自 spaceAliasName: + - 若包含分隔符(例如 ';' 或 ';' 等),拆分并取第一个非空别名 + - 若不包含分隔符,则直接使用 spaceAliasName + - 若 spaceAliasName 为空,回退到 spaceName;仍为空则返回空字符串 + + Returns: + {"entityId": str, "location": str} + """ + logger = _ensure_logger() + logger.debug(f"开始提取空间实体和位置信息: {space_data}") + + # entityId + space_id = space_data.get("spaceId") + entity_id = "" if space_id is None else str(space_id) + logger.debug(f"提取的实体ID: {entity_id}") + + # location from alias + alias_raw = space_data.get("spaceAliasName") + alias_text = ( + alias_raw + if isinstance(alias_raw, str) + else (str(alias_raw) if alias_raw is not None else "") + ) + + chosen_location = alias_text.strip() + + if chosen_location: + # 使用常见分隔符进行切分:中文分号、英文分号、中文逗号、英文逗号、竖线、斜杠 + parts = [ + p.strip() + for p in re.split(r"[;;,,\|/]+", chosen_location) + if p and p.strip() + ] + if parts: + chosen_location = parts[0] + else: + # alias 为空则回退到 spaceName + space_name = space_data.get("spaceName") + chosen_location = ( + space_name.strip() + if isinstance(space_name, str) + else (str(space_name).strip() if space_name is not None else "") + ) + + result = { + "entityId": entity_id, + "location": chosen_location, + } + + logger.info(f"空间实体和位置提取完成: entityId={entity_id}, location={chosen_location}") + return result + + +def format_device_for_display(device: Dict[str, Any]) -> str: + """ + 将设备数据格式化为易读的描述文本 + 用于展示给AI和用户,提升可读性和理解性 + + Args: + device: 设备数据字典,可能包含以下字段: + - location_desc: 位置描述 + - device_desc: 设备描述 + - entityId: 设备唯一标识 + - operations: 操作列表(合并后的设备) + 或 + - command: 单个命令 + - operation_desc: 操作描述 + - operation_params: 操作参数 + + Returns: + 格式化后的设备描述文本 + """ + logger = _ensure_logger() + logger.debug(f"开始格式化设备显示信息: entityId={device.get('entityId', 'N/A')}") + + try: + # 处理位置描述 + location = device.get('location_desc', '未知位置') + if ';' in location or ';' in location: + parts = re.split(r'[;;]', location) + location = '('.join(parts) + ')' if len(parts) > 1 else parts[0] + + # 处理设备类型描述 + device_type = device.get('device_desc', '未知设备') + if ';' in device_type or ';' in device_type: + parts = re.split(r'[;;]', device_type) + device_type = '('.join(parts) + ')' if len(parts) > 1 else parts[0] + + entity_id = device.get('entityId', '') + + formatted_text = f"""设备核心信息: +位置:{location} +设备类型:{device_type} +唯一标识(entityId):{entity_id} + +可执行操作:""" + + # 判断是合并后的设备(有operations字段)还是单个操作设备 + if 'operations' in device: + # 合并后的设备,包含多个操作 + operations = device.get('operations', []) + for i, op in enumerate(operations, 1): + cmd = op.get('command', '') + desc = op.get('operation_desc', '').replace(';', ' / ').replace(';', ' / ') + + formatted_text += f"\n{i}. {desc}" + formatted_text += f"\n 指令:{cmd}" + + params = op.get('operation_params', []) + if params: + formatted_text += "\n 参数:" + for param in params: + key = param.get('key', '') + description = param.get('description', '') + value = param.get('value', '') + formatted_text += f"\n - {description}({key}):{value}" + else: + formatted_text += "\n 参数:无" + else: + # 单个操作设备 + cmd = device.get('command', '') + desc = device.get('operation_desc', '').replace(';', ' / ').replace(';', ' / ') + + formatted_text += f"\n1. {desc}" + formatted_text += f"\n 指令:{cmd}" + + params = device.get('operation_params', []) + if params: + formatted_text += "\n 参数:" + for param in params: + key = param.get('key', '') + description = param.get('description', '') + value = param.get('value', '') + formatted_text += f"\n - {description}({key}):{value}" + else: + formatted_text += "\n 参数:无" + + logger.debug(f"设备显示信息格式化完成: entityId={entity_id}") + return formatted_text + + except Exception as e: + logger.error(f"格式化设备显示信息时出错: {str(e)}", exc_info=True) + return f"设备信息格式化失败: {str(e)}" + + +def format_devices_list(devices: List[Dict[str, Any]]) -> str: + """ + 格式化多个设备信息为易读的文本列表 + + Args: + devices: 设备数据列表 + + Returns: + 格式化后的设备列表文本 + """ + logger = _ensure_logger() + logger.debug(f"开始格式化设备列表,设备数量: {len(devices) if devices else 0}") + + if not devices: + logger.warning("设备列表为空") + return "未找到相关设备" + + try: + formatted_list = [] + formatted_list.append(f"共找到 {len(devices)} 个匹配的设备\n") + + for idx, device in enumerate(devices, 1): + if idx > 1: + formatted_list.append("\n" + "-" * 40 + "\n") + formatted_list.append(f"【设备 {idx}】") + formatted_list.append(format_device_for_display(device)) + + result = "\n".join(formatted_list) + logger.info(f"设备列表格式化完成,设备数量: {len(devices)}") + return result + + except Exception as e: + logger.error(f"格式化设备列表时出错: {str(e)}", exc_info=True) + return f"设备列表格式化失败: {str(e)}" + + +def format_device_for_display_simple(device: Dict[str, Any]) -> str: + """ + 将设备数据格式化为简化的描述文本(无装饰符号版本) + 适用于需要更简洁输出的场景 + + Args: + device: 设备数据字典 + + Returns: + 格式化后的简化设备描述文本 + """ + logger = _ensure_logger() + logger.debug(f"开始格式化设备简化显示信息: entityId={device.get('entityId', 'N/A')}") + + try: + # 处理位置描述 + location = device.get('location_desc', '未知位置') + if ';' in location or ';' in location: + parts = re.split(r'[;;]', location) + location = '('.join(parts) + ')' if len(parts) > 1 else parts[0] + + # 处理设备类型描述 + device_type = device.get('device_desc', '未知设备') + if ';' in device_type or ';' in device_type: + parts = re.split(r'[;;]', device_type) + device_type = '('.join(parts) + ')' if len(parts) > 1 else parts[0] + + entity_id = device.get('entityId', '') + + formatted_text = f"""设备核心信息: +• 位置:{location} +• 设备类型:{device_type} +• 唯一标识(entityId):{entity_id} + +可执行操作:""" + + # 判断是合并后的设备还是单个操作设备 + if 'operations' in device: + operations = device.get('operations', []) + for i, op in enumerate(operations, 1): + cmd = op.get('command', '') + desc = op.get('operation_desc', '').replace(';', ' / ').replace(';', ' / ') + + formatted_text += f"\n{i}. {desc}" + formatted_text += f"\n 指令:{cmd}" + + params = op.get('operation_params', []) + if params: + formatted_text += "\n 参数:" + for param in params: + key = param.get('key', '') + description = param.get('description', '') + value = param.get('value', '') + formatted_text += f"\n - {description}({key}):{value}" + else: + formatted_text += "\n 参数:无" + else: + cmd = device.get('command', '') + desc = device.get('operation_desc', '').replace(';', ' / ').replace(';', ' / ') + + formatted_text += f"\n1. {desc}" + formatted_text += f"\n 指令:{cmd}" + + params = device.get('operation_params', []) + if params: + formatted_text += "\n 参数:" + for param in params: + key = param.get('key', '') + description = param.get('description', '') + value = param.get('value', '') + formatted_text += f"\n - {description}({key}):{value}" + else: + formatted_text += "\n 参数:无" + + logger.debug(f"设备简化显示信息格式化完成: entityId={entity_id}") + return formatted_text + + except Exception as e: + logger.error(f"格式化设备简化显示信息时出错: {str(e)}", exc_info=True) + return f"设备信息格式化失败: {str(e)}" + + +# 测试函数 +def test_process_device_data(): + """测试数据处理函数""" + logger = _ensure_logger() + # 模拟输入数据 + test_data = { + "results": [ + [ + { + "id": "32875964-00df-4b34-8e33-c47a722b8f7f", + "location": {"key": "lzwc", "description": "灵泽联创中心"}, + "device": { + "key": "switch.zimi_cn_1144138206_dhkg01_on_p_2_1", + "description": "灵泽办公区过道吊灯 开关 按键", + }, + "operation": {"key": "turn_on", "description": "开关"}, + "operation_params": [], + }, + 0.34563739142066174, + { + "exact_match": 0.25, + "word2vec_similarity": 0.5346393585205078, + "doc2vec_similarity": 0.004867201205343008, + "tfidf_similarity": 0.8217383432613271, + "combined_score": 0.34563739142066174, + }, + ] + ], + "count": 1, + "best_match": { + "device": { + "id": "32875964-00df-4b34-8e33-c47a722b8f7f", + "location": {"key": "lzwc", "description": "灵泽联创中心"}, + "device": { + "key": "switch.zimi_cn_1144138206_dhkg01_on_p_2_1", + "description": "灵泽办公区过道吊灯 开关 按键", + }, + "operation": {"key": "turn_on", "description": "开关"}, + "operation_params": [], + }, + "score": 0.34563739142066174, + "confidence": "低", + }, + } + + enterprise_id = "1932095424144715777" + + logger.info("=== 测试数据处理功能 ===") + + # 1. 测试数据验证 + logger.info("1. 数据验证测试:") + is_valid, message = validate_device_data(test_data) + logger.info(f"数据验证结果: {is_valid}, 消息: {message}") + + # 2. 测试基本处理功能 + logger.info("2. 基本处理功能测试:") + result = process_device_data(test_data, enterprise_id) + logger.info(f"处理结果: {result}") + + # 3. 测试安全版本 + logger.info("3. 安全版本测试:") + safe_result = process_device_data_safe( + test_data, enterprise_id, {"error": "处理失败"} + ) + logger.info(f"安全处理结果: {safe_result}") + + # 4. 测试错误情况 + logger.info("4. 错误情况测试:") + # 测试空数据 + empty_result = process_device_data_safe({}, enterprise_id, {"error": "数据为空"}) + logger.info(f"空数据处理结果: {empty_result}") + + # 测试缺少字段的数据 + invalid_data = {"best_match": {"device": {}}} + invalid_result = process_device_data_safe( + invalid_data, enterprise_id, {"error": "数据格式错误"} + ) + logger.info(f"无效数据处理结果: {invalid_result}") + + return result + + +def test_process_device_results(): + import json + + logger = _ensure_logger() + + # Load JSON data from file + json_file_path = r"E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_iot\deviot.json" + with open(json_file_path, 'r', encoding='utf-8') as f: + raw_data = json.load(f) + + logger.info('Raw data loaded, processing...') + result = process_device_results(raw_data) + logger.info('Processed result:') + for i, item in enumerate(result): + logger.info(f"Item {i+1}: {item}") + + # Save results to output file + output_file_path = r"E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_iot\output\output.json" + with open(output_file_path, 'w', encoding='utf-8') as f: + json.dump(result, f, ensure_ascii=False, indent=2) + + logger.info(f'Results saved to: {output_file_path}') + + return result + +if __name__ == "__main__": + # 测试代码只在直接运行此模块时执行,不在导入时执行 + # test_process_device_data() + # test_process_device_results() # 注释掉测试代码避免MCP服务器启动时执行 + pass diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/init_mcp.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/init_mcp.py new file mode 100644 index 0000000..b25fe07 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/init_mcp.py @@ -0,0 +1,131 @@ + +""" +MCP服务器初始化模块 + +该模块提供了MCP服务器的初始化功能,包括企业ID获取和向量库初始化。 +""" + +from typing import Optional, Dict, Any +from .device_operations import DeviceOperator +from .vector_service import VectorService +from .logger_config import get_logger + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + + +def init_mcp_server( + device_op: DeviceOperator, + vector_service: VectorService, + enterprise_id: str +) -> Dict[str, Any]: + """ + 初始化MCP服务器 + + 该函数执行以下步骤: + 1. 验证企业ID + 2. 检查向量库状态 + 3. 如果向量库不存在,则创建向量库 + + 参数: + device_op: 设备操作实例 + vector_service: 向量服务实例 + enterprise_id: 企业ID(从环境变量获取) + + 返回: + Dict[str, Any]: 初始化结果,包含状态码、消息和数据 + """ + logger = _ensure_logger() + try: + # 输入参数验证 + if not enterprise_id or not isinstance(enterprise_id, str): + error_msg = f"无效的企业ID: {enterprise_id}" + logger.error(error_msg) + return { + "code": 400, + "msg": error_msg, + "data": None + } + + if not isinstance(device_op, DeviceOperator): + error_msg = "device_op参数必须是DeviceOperator实例" + logger.error(error_msg) + return { + "code": 400, + "msg": error_msg, + "data": None + } + + if not isinstance(vector_service, VectorService): + error_msg = "vector_service参数必须是VectorService实例" + logger.error(error_msg) + return { + "code": 400, + "msg": error_msg, + "data": None + } + + logger.info(f"开始初始化MCP服务器,企业ID: {enterprise_id}") + + # 第一步:检查向量库状态 + logger.info("第一步:检查向量库状态...") + vector_store_exists = vector_service.check_vector_store_status(enterprise_id) + logger.info(f"向量库状态检查完成,存在状态: {vector_store_exists}") + + # 如果向量库不存在,则创建向量库 + if not vector_store_exists: + logger.info("向量库不存在,开始创建向量库...") + init_result = vector_service.init_vector_store(keyId=enterprise_id) + + # 检查初始化结果 + if init_result.get("status") == "error": + error_msg = f"向量库初始化失败: {init_result.get('message')}" + logger.error(error_msg) + return { + "code": 500, + "msg": error_msg, + "data": { + "enterprise_id": enterprise_id, + "vector_store_created": False, + "init_result": init_result + } + } + else: + logger.info("向量库创建成功") + return { + "code": 200, + "msg": "MCP服务器初始化成功,向量库已创建", + "data": { + "enterprise_id": enterprise_id, + "vector_store_created": True, + "vector_store_existed": False, + "init_result": init_result + } + } + else: + logger.info("向量库已存在,无需创建") + return { + "code": 200, + "msg": "MCP服务器初始化成功,向量库已存在", + "data": { + "enterprise_id": enterprise_id, + "vector_store_created": False, + "vector_store_existed": True + } + } + + except Exception as e: + error_msg = f"MCP服务器初始化过程中发生异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "code": 500, + "msg": error_msg, + "data": None + } diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/iot_device_dicts_prompt.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/iot_device_dicts_prompt.py new file mode 100644 index 0000000..473deb4 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/iot_device_dicts_prompt.py @@ -0,0 +1,216 @@ +def iot_device_precise_controller_prompt(): + return """ + 设备精准控制 + + 【功能描述】 + 用来精确控制特定设备的工具,通过设备的唯一ID直接操作,特别准确。 + 这个工具只负责"控制设备",但它需要先通过"设备信息查询"工具获取设备信息才能使用。 + + 【日常使用场景】 + 在使用"设备信息查询"工具找到设备后的控制操作: + - "控制那个具体的灯" + - "操作刚才查到的灯" + - "开一下那台空调" + - "把刚才找到的空调打开" + - "关掉那个特定的插座" + - "控制指定的插座" + - "操作那个设备" + - "控制具体设备" + + 跟进操作(基于之前的查询结果): + - "就是这个设备,帮我开一下" + - "用这个ID控制设备" + - "按照之前查到的信息操作" + + 【工作方式】 + 这个工具必须和"设备信息查询"工具配合使用: + 1. 第一步:用"设备信息查询"工具找设备 + 2. 第二步:从查询结果中获取设备ID和控制命令 + 3. 第三步:用这个工具执行精确控制 + + 【工具配合关系】 + 就像"先查电话号码,再打电话": + - 设备信息查询 = 查电话号码(找到设备和操作方法) + - 设备精准控制 = 打电话(实际执行控制) + + 【重要】 + 这个工具不能单独使用!必须先用查询iot_get_devices_by_location或者iot_get_all_spaces_and_devices工具获取设备信息,不能随便填写参数。 + + 【参数说明】 + - entityId (必填): 设备唯一ID + * 格式示例: "switch.zimi_cn_1144138206_dhkg01_on_p_2_1" + * 来源: 设备查询工具返回结果中的设备ID + + - command (必填): 操作命令 + * 格式示例: "turn_on", "turn_off", "set_temperature", "set_brightness" + * 来源: 设备查询工具返回结果中的命令 + + - params (可选): 操作参数 + * 格式: 包含操作所需参数的数据 + * 来源: 设备查询工具返回结果中的参数 + * 示例: {"temperature": 24} 用于空调温度设置 + * 示例: {"brightness": 80} 用于灯光亮度调节 + + 【返回结果】 + 返回设备控制操作结果,包括是否成功、设备反馈信息和详细结果。 + """ + + +def iot_get_devices_by_location_prompt(): + return """ + 根据位置获取设备列表 + + 【功能描述】 + 根据指定的位置/房间,获取该位置下的所有智能设备列表。 + 这个工具专门用来查看某个位置有哪些设备,会返回该位置的完整设备清单。 + + 【日常使用场景】 + 查看位置设备: + - "办公室有哪些设备" + - "会议室有什么设备" + - "看看客厅的设备" + - "前台有什么智能设备" + - "财务部的设备列表" + - "查看卧室的所有设备" + + 设备清单查询: + - "列出办公室所有设备" + - "显示会议室设备清单" + - "办公室设备有哪些" + - "会议室装了什么设备" + - "客厅都有什么智能设备" + + 位置设备统计: + - "办公室一共有多少设备" + - "会议室有几个设备" + - "客厅设备数量" + - "前台设备统计" + + 简单查询: + - "办公室设备" + - "会议室设备列表" + - "客厅的设备" + - "设备清单" + + 【注意】 + - 这个工具只返回设备列表信息,不会控制设备 + - 返回结果包含设备名称、类型、状态等详细信息 + - 如果需要控制设备,可以使用返回结果中的设备信息配合其他控制工具 + + 【参数说明】 + - location (必填): 要查询的位置/房间名称 + * 示例:办公室、会议室、客厅、前台、财务部等 + * 参数值不得包含任何标点符号(如逗号、句号、感叹号等) + + 【返回结果】 + 返回指定位置的所有设备列表,包括: + - 设备数量统计 + - 每个设备的详细信息(设备ID、名称、类型、状态等) + - 设备的当前状态(开启/关闭等) + - 设备支持的控制命令 + + 【典型使用流程】 + 1. 指定要查询的位置 + 2. 获取该位置的所有设备列表 + 3. 查看设备详细信息 + 4. (可选)根据设备信息进行后续控制操作 + """ + + +def iot_get_all_spaces_and_devices_prompt(): + return """ + 获取所有空间位置信息 + + 【功能描述】 + 获取系统中所有空间位置的列表。 + 这个工具只返回空间名称清单,不包含设备详情,适合快速了解有哪些可用空间。 + + 【日常使用场景】 + 查看所有空间: + - "显示所有空间" + - "有哪些空间" + - "列出所有房间" + - "查看全部区域" + - "空间列表" + - "所有位置" + - "有什么位置" + - "可用的空间有哪些" + + 空间清单查询: + - "系统里有几个空间" + - "一共有多少个位置" + - "空间总览" + - "位置总览" + - "房间列表" + + 简单查询: + - "所有空间" + - "空间总览" + - "位置列表" + - "全部位置" + + 【注意】 + - 这个工具只返回空间名称列表,不包含设备信息 + - 不需要传入任何参数 + - 如果需要查看某个空间的设备,请使用"根据位置获取设备列表"工具 + - 适合在控制设备前先了解有哪些可用空间 + + 【参数说明】 + - 无需参数(自动获取所有空间位置) + + 【返回结果】 + 返回所有空间位置列表,包括: + - 空间总数统计 + - 所有空间名称的列表 + + 【典型使用流程】 + 1. 调用工具获取所有空间列表 + 2. 查看有哪些可用空间 + 3. (可选)选择特定空间,使用"根据位置获取设备列表"工具查看该空间的设备 + 4. (可选)根据设备信息进行设备控制操作 + """ + + +def smart_space_device_locator_matcher_prompt(): + return """ + 位置查询 + + 【功能描述】 + 帮你查看自己现在在哪个位置/空间,会根据你的用户信息自动识别。 + + 【日常使用场景】 + 查看位置: + - "我现在在哪" + - "我在哪个位置" + - "我现在在哪个空间" + - "当前位置是什么" + - "我属于哪个地方" + - "我在哪个区域" + + 确认空间: + - "当前默认空间是什么" + - "我的默认位置" + - "系统识别我在哪里" + - "定位我的位置" + - "我的空间信息" + - "位置信息" + + 设备控制前确认: + - "先看看我在哪" + - "确认一下位置" + - "我在这个位置吗" + - "位置对不对" + + 【注意】 + 这个工具只是查看你的位置信息,不会控制任何设备。 + + 【参数说明】 + - userId (必填): 用户ID(自动提供) + + 【返回结果】 + 返回用户所属空间: + - spaceAliasName: 推断出的空间名称 + + 【使用说明】 + - 本工具仅做查询与定位,不执行设备控制 + """ diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/logger_config.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/logger_config.py new file mode 100644 index 0000000..d32b3ec --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/logger_config.py @@ -0,0 +1,558 @@ +""" +统一日志配置模块 + +这个模块提供了整个项目的统一日志配置和管理功能,确保所有组件使用一致的日志格式和输出方式。 + +主要功能: +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_mcp_iot.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: + # 自动确定日志文件路径:项目根目录 + 默认文件名 + project_root = cls._get_project_root() + log_file = project_root / 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) + + # ==================== 包日志器配置 ==================== + + # 获取包的顶层日志器,而不是根日志器 + package_logger = logging.getLogger('lzwcai_mcp_iot') + package_logger.setLevel(log_level) + + # 作为库,不应该清除宿主应用的任何处理器 + # 也不应该让日志消息向上传播到根日志器,以免重复打印 + package_logger.propagate = False + + # 清除此日志器上现有的处理器,避免重复配置 + for handler in package_logger.handlers[:]: + package_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) + package_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) + package_logger.addHandler(file_handler) + + # ==================== 初始化完成标记 ==================== + + # 标记为已初始化,防止重复配置 + cls._initialized = True + + # ==================== 记录初始化信息 ==================== + + # 使用包日志器记录初始化信息,避免向上传播到根日志器 + if file_output: # 只有在文件输出启用时才记录初始化信息 + package_logger.info("=" * 80) + package_logger.info(f"日志系统初始化完成 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + package_logger.info(f"日志级别: {logging.getLevelName(log_level)}") + package_logger.info(f"日志文件: {cls._log_file_path}") + package_logger.info(f"控制台输出: {console_output}") + package_logger.info(f"文件输出: {file_output}") + package_logger.info(f"文件轮转: 最大{cls.MAX_LOG_SIZE // (1024*1024)}MB, 保留{cls.BACKUP_COUNT}个备份") + package_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 + + # 备选方案:如果找不到标识文件,使用预设的相对路径 + # 这个路径基于当前的项目结构:util -> 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("这是一条信息日志") + + 特性: + - 自动初始化:首次调用时自动配置日志系统 + - 层次化命名:支持Python日志器的层次化命名 + - 统一配置:所有日志器使用相同的格式和输出配置 + """ + # 检查是否已初始化,未初始化则使用MCP安全的默认配置初始化 + if not cls._initialized: + # MCP模式下,默认禁用控制台输出,只使用文件输出 + 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 lzwcai_mcp_iot.src.util.logger_config + """ + # 测试代码只在直接运行此模块时执行,不在导入时执行 + # 以下代码已注释避免MCP服务器启动时执行 + pass + + # # 初始化日志系统(DEBUG级别,同时输出到控制台和文件) + # log_file = setup_logging(log_level=logging.DEBUG) + # logger = get_logger(__name__) + + # logger.info("开始测试日志配置...") + + # # 测试不同级别的日志输出 + # logger.debug("这是一个调试消息 - 用于开发调试") + # logger.info("这是一个信息消息 - 记录重要信息") + # logger.warning("这是一个警告消息 - 提醒注意事项") + # logger.error("这是一个错误消息 - 记录错误情况") + + # # 测试工具方法 + # LoggerConfig.log_api_request(logger, "GET", "https://api.example.com/test") + # LoggerConfig.log_api_response(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") + + # # 输出日志文件位置 + # logger.info(f"日志文件位置: {log_file}") + # logger.info("日志配置测试完成!") diff --git a/lzwcai_mcp_iot/lzwcai_mcp_iot/src/vector_service.py b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/vector_service.py new file mode 100644 index 0000000..c415f13 --- /dev/null +++ b/lzwcai_mcp_iot/lzwcai_mcp_iot/src/vector_service.py @@ -0,0 +1,733 @@ +""" +向量服务模块 + +该模块提供了向量数据的增删改查操作接口。 +""" + +import requests +import json +from typing import Dict, Any, Optional, List +from ..config import CONFIG +from .logger_config import get_logger + +# 延迟初始化日志器,避免在导入时立即执行 +logger = None + +def _ensure_logger(): + """确保日志器已初始化""" + global logger + if logger is None: + logger = get_logger(__name__) + return logger + + +class VectorService: + """向量服务类,提供向量数据的增删改查功能""" + + def __init__(self, base_url: str = None): + """ + 初始化向量服务 + + 参数: + base_url: API服务的基础URL + """ + logger = _ensure_logger() + self.base_url = base_url or CONFIG["vector_api_base_url"] + self.session = requests.Session() + # 设置默认超时 + self.session.timeout = CONFIG["request_timeout"] + logger.info(f"VectorService初始化完成,API基础URL: {self.base_url}") + + def init_vector_store( + self, + keyId: str, + ) -> Dict[str, Any]: + """ + 调用初始化向量存储API接口 + + 参数: + keyId: 企业或项目的唯一标识符 + + 返回: + Dict[str, Any]: API返回的响应数据 + """ + if not keyId or not isinstance(keyId, str): + error_msg = "keyId参数是必需的且必须是字符串" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + endpoint = f"{self.base_url}/iot/companies/recreate" + + # 构建请求数据 + payload = { + "company_id": keyId, + "company_name": "公司名称", + "enterprise_id": keyId, + "description": "公司描述", + } + + # 设置请求头 + headers = {"Content-Type": "application/json"} + + try: + logger.info(f"初始化向量库,keyId: {keyId}") + # 发送POST请求 + response = self.session.post( + endpoint, + headers=headers, + data=json.dumps(payload), + timeout=CONFIG["request_timeout"], + ) + # 检查响应状态 + response.raise_for_status() + # 返回JSON响应 + result = response.json() + logger.info(f"向量库初始化成功: {result}") + return result + + except requests.exceptions.Timeout: + error_msg = f"向量库初始化请求超时,keyId: {keyId}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except requests.RequestException as e: + error_msg = f"向量库初始化API请求失败: {str(e)}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except Exception as e: + error_msg = f"向量库初始化发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + def delete_vector(self, keyId: str) -> Dict[str, Any]: + """ + 删除向量数据(模拟实现) + + 参数: + keyId: 要删除的向量唯一标识 + + 返回: + Dict[str, Any]: 包含删除结果的字典 + """ + if not keyId or not isinstance(keyId, str): + error_msg = "keyId参数是必需的且必须是字符串" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + logger.info(f"删除向量数据,keyId: {keyId}") + # 模拟删除向量的结果 + return { + "status": "success", + "message": "向量删除成功", + "data": {"keyId": keyId, "deleted_at": "2023-07-15T11:45:00Z"}, + } + + def update_vector( + self, + keyId: str, + ) -> Dict[str, Any]: + """ + 更新向量数据 + + 参数: + keyId: 要更新的向量唯一标识 + + 返回: + Dict[str, Any]: 包含更新结果的字典 + """ + if not keyId or not isinstance(keyId, str): + error_msg = "keyId参数是必需的且必须是字符串" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + endpoint = f"{self.base_url}/iot/vector-store/update" + + # 构建请求数据 + payload = {"keyId": keyId} + + # 设置请求头 + headers = {"Content-Type": "application/json"} + + try: + logger.info(f"更新向量数据,keyId: {keyId}") + # 发送POST请求 + response = self.session.post( + endpoint, + headers=headers, + data=json.dumps(payload), + timeout=CONFIG["request_timeout"], + ) + # 检查响应状态 + response.raise_for_status() + # 返回JSON响应 + result = response.json() + logger.info(f"向量数据更新成功: {result}") + return result + + except requests.exceptions.Timeout: + error_msg = f"向量数据更新请求超时,keyId: {keyId}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except requests.RequestException as e: + error_msg = f"更新向量数据失败: {str(e)}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except Exception as e: + error_msg = f"更新向量数据发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + def query_vector( + self, + keyId: str, + device: Optional[str] = None, + location: Optional[str] = None, + operation: Optional[str] = None, + operation_param: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 查询向量数据 + + 参数: + keyId: 企业或项目的唯一标识符 + device: 设备名称(可选) + location: 设备位置(可选) + operation: 操作类型(可选) + operation_param: 操作参数(可选) + + 返回: + Dict[str, Any]: 包含查询结果的字典 + """ + if not keyId or not isinstance(keyId, str): + error_msg = "keyId参数是必需的且必须是字符串" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + # 验证至少有一个查询参数 + if not any([device, location, operation]): + error_msg = "至少需要提供device、location或operation中的一个参数" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + endpoint = f"{self.base_url}/iot/companies/{keyId}/search" + + # 构建请求数据 + payload = {} + + # 添加可选参数 + if device: + payload["device"] = device + if location: + payload["location"] = location + if operation: + payload["operation"] = operation + if operation_param: + payload["operation_param"] = operation_param + + # 设置请求头 + headers = { + "Content-Type": "application/json", + } + + try: + logger.info( + f"查询向量数据,keyId: {keyId}, device: {device}, location: {location}, operation: {operation}, operation_param: {operation_param}" + ) + # 发送POST请求 + response = self.session.post( + endpoint, + headers=headers, + data=json.dumps(payload), + timeout=CONFIG["request_timeout"], + ) + # 检查响应状态 + response.raise_for_status() + # 返回JSON响应 + result = response.json() + logger.info( + f"向量查询成功,返回结果数量: {result.get('data', {}).get('total_results', 0)}" + ) + return result + + except requests.exceptions.Timeout: + error_msg = f"向量查询请求超时,keyId: {keyId}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except requests.RequestException as e: + error_msg = f"查询向量数据失败: {str(e)}" + logger.error(error_msg) + return { + "status": "error", + "message": error_msg, + "data": None, + } + except Exception as e: + error_msg = f"查询向量数据发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "status": "error", + "message": error_msg, + "data": None, + } + + def analyze_search_results(self, search_results: List[List[Any]]) -> Dict[str, Any]: + """ + 调用搜索结果分析接口 + + 参数: + search_results: 搜索结果数据,格式为包含设备信息、评分和详细评分的嵌套数组 + [[device_info, score, score_details], ...] + + 返回: + Dict[str, Any]: 分析结果,包含处理后的数据和响应信息 + """ + logger.info("开始分析搜索结果...") + logger.debug(f"输入搜索结果数量: {len(search_results) if search_results else 0}") + + if not search_results or not isinstance(search_results, list): + error_msg = "无效的搜索结果数据:search_results必须是一个非空数组" + logger.error(error_msg) + return {"code": 400, "msg": error_msg, "data": None} + + # API配置 + url = f"{self.base_url}/iot/analyze/search-results" + headers = {"Content-Type": "application/json"} + + # 构建请求数据 + request_data = { + "search_results": search_results + } + + try: + logger.info(f"正在调用搜索结果分析接口: {url}") + logger.debug(f"请求数据大小: {len(search_results)} 条搜索结果") + + # 发送POST请求 + response = self.session.post( + url=url, + headers=headers, + data=json.dumps(request_data), + timeout=CONFIG["request_timeout"], + ) + + # 处理响应 + if response.status_code == 200: + try: + result = response.json() + logger.info(f"搜索结果分析成功,返回数据类型: {type(result)}") + + # 处理标准格式的响应 {code, msg, data} + if isinstance(result, dict) and "code" in result: + # 检查接口返回的业务状态码 + if result.get("code") == 200: + data = result.get("data", {}) + filtered_results = data.get("filtered_results", []) + logger.info(f"搜索结果分析成功,返回过滤结果数量: {len(filtered_results) if filtered_results else 0}") + # 返回包含filtered_results的标准格式 + return filtered_results + else: + # 接口返回了错误状态码,直接返回原始响应 + logger.warning(f"接口返回业务错误: code={result.get('code')}, msg={result.get('msg')}") + return result + + # 如果接口直接返回数据而不是标准格式,包装成标准格式 + if isinstance(result, dict) and "code" not in result: + logger.info("接口返回非标准格式,进行包装处理") + return {"code": 200, "msg": "分析成功", "data": result} + + logger.debug(f"返回原始分析结果: {type(result)}") + return result + except ValueError as e: + error_msg = f"响应JSON解析失败: {str(e)}" + logger.error(error_msg) + return {"code": 500, "msg": error_msg, "data": None} + else: + error_msg = f"API请求失败,状态码: {response.status_code}, 响应: {response.text}" + logger.error(error_msg) + return { + "code": response.status_code, + "msg": error_msg, + "data": None, + } + + except requests.exceptions.Timeout: + error_msg = "搜索结果分析接口请求超时" + logger.error(error_msg) + return {"code": 408, "msg": error_msg, "data": None} + except requests.exceptions.RequestException as e: + error_msg = f"搜索结果分析接口网络请求异常: {str(e)}" + logger.error(error_msg) + return {"code": 500, "msg": error_msg, "data": None} + except Exception as e: + error_msg = f"搜索结果分析接口调用异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"code": 500, "msg": error_msg, "data": None} + + def check_vector_store_status(self, keyId: str) -> bool: + """ + 检查向量库状态 + + 参数: + keyId: 企业或项目的唯一标识符 + + 返回: + bool: 向量库是否存在 + """ + if not keyId or not isinstance(keyId, str): + logger.error(f"无效的keyId参数: {keyId}") + return False + + endpoint = f"{self.base_url}/iot/companies/{keyId}/exists" + + # 设置请求头 + headers = { + "Accept": "*/*", + } + + try: + logger.info(f"检查向量库状态,keyId: {keyId}") + # 发送GET请求 + response = self.session.get( + endpoint, + headers=headers, + timeout=CONFIG["request_timeout"], + ) + # 检查响应状态 + response.raise_for_status() + # 从响应中提取exists字段 + result = response.json() + exists = result.get("data", {}).get("exists", False) + logger.info(f"向量库状态检查完成,keyId: {keyId}, exists: {exists}") + return exists + + except requests.exceptions.Timeout: + logger.error(f"检查向量库状态请求超时,keyId: {keyId}") + return False + except requests.RequestException as e: + logger.error(f"检查向量库状态失败: {str(e)}") + return False + except Exception as e: + logger.error(f"检查向量库状态发生未知异常: {str(e)}", exc_info=True) + return False + + def query_devices_by_location( + self, + keyId: str, + location: Optional[str] = None, + ) -> Dict[str, Any]: + """ + 根据位置查询设备列表 + + 参数: + keyId: 企业或项目的唯一标识符 + location: 设备位置(可选) + + 返回: + Dict[str, Any]: 包含设备列表的数据字典 + """ + logger = _ensure_logger() + + if not keyId or not isinstance(keyId, str): + error_msg = "keyId参数是必需的且必须是字符串" + logger.error(error_msg) + return { + "devices": [], + "count": 0, + "location_filter": location or "" + } + + endpoint = f"{self.base_url}/iot/companies/{keyId}/devices" + + # 构建请求数据 + payload = {} + if location: + payload["location"] = location + + # 设置请求头 + headers = { + "Content-Type": "application/json", + } + + try: + logger.info(f"查询设备列表,keyId: {keyId}, location: {location}") + # 发送POST请求 + response = self.session.post( + endpoint, + headers=headers, + data=json.dumps(payload), + timeout=CONFIG["request_timeout"], + ) + # 检查响应状态 + response.raise_for_status() + # 解析JSON响应 + result = response.json() + + # 检查API返回的业务状态码 + if result.get("code") == 200: + data = result.get("data", {}) + device_count = data.get("count", 0) + logger.info(f"设备列表查询成功,返回设备数量: {device_count}") + # 返回data部分 + return data + else: + error_msg = f"API返回业务错误: code={result.get('code')}, msg={result.get('msg')}" + logger.error(error_msg) + return { + "devices": [], + "count": 0, + "location_filter": location or "", + "error": error_msg + } + + except requests.exceptions.Timeout: + error_msg = f"查询设备列表请求超时,keyId: {keyId}" + logger.error(error_msg) + return { + "devices": [], + "count": 0, + "location_filter": location or "", + "error": error_msg + } + except requests.RequestException as e: + error_msg = f"查询设备列表失败: {str(e)}" + logger.error(error_msg) + return { + "devices": [], + "count": 0, + "location_filter": location or "", + "error": error_msg + } + except Exception as e: + error_msg = f"查询设备列表发生未知异常: {str(e)}" + logger.error(error_msg, exc_info=True) + return { + "devices": [], + "count": 0, + "location_filter": location or "", + "error": error_msg + } + + def check_and_filter_devices( + self, + device_list: List[Any], + device_op: Any + ) -> Dict[str, Any]: + """ + 检查设备分数并过滤有效设备 + + 参数: + device_list: 设备列表,格式为 [[device_info, score, score_details], ...] + device_op: DeviceOperator实例,用于调用设备检查和预处理方法 + + 返回: + Dict[str, Any]: 包含检查结果的字典 + - 如果有有效设备:返回有效设备列表 + - 如果无有效设备:返回候选设备列表和错误码 + """ + logger = _ensure_logger() + logger.info("开始检查设备分数并过滤有效设备") + logger.debug(f"输入设备列表数量: {len(device_list) if device_list else 0}") + + # 判断设备分数是否达标,给每个设备添加checkResult字段 + checked_device_list = [] + has_valid_device = False + + for device_item in device_list: + # deviceList格式: [[device_info, score, score_details], ...] + if isinstance(device_item, list) and len(device_item) >= 3: + device_info, score, score_details = device_item[0], device_item[1], device_item[2] + + # 调用check_device_results_score方法检查分数 + check_result = device_op.check_device_results_score(score_details) + + # 将checkResult添加到device_info中 + if isinstance(device_info, dict): + device_info["checkResult"] = check_result.get("checkResult", False) + + # 检查是否有通过验证的设备 + if device_info["checkResult"]: + has_valid_device = True + + # 重新构建device_item + checked_device_list.append([device_info, score, score_details]) + else: + # 如果格式不正确,保持原样但添加checkResult为False + if isinstance(device_item, dict): + device_item["checkResult"] = False + checked_device_list.append(device_item) + + # 如果没有任何设备通过验证,返回候选设备列表 + if not has_valid_device: + logger.warning("匹配分数不够准确,返回候选设备列表") + candidate_list = device_op.preprocess_results(checked_device_list) + return { + "code": 400, + "msg": "设备匹配分数不够准确,无法为用户智能操作设备,返回候选设备列表供用户选择; ", + "data": { + "candidates": candidate_list, + "total": len(candidate_list) + }, + "is_candidate": True + } + + # 过滤出checkResult为True的设备 + valid_devices = [] + for device_item in checked_device_list: + if isinstance(device_item, list) and len(device_item) >= 1: + device_info = device_item[0] + if isinstance(device_info, dict) and device_info.get("checkResult", False): + valid_devices.append(device_item) + + logger.info(f"过滤完成,有效设备数量: {len(valid_devices)}") + return { + "valid_devices": valid_devices, + "is_candidate": False + } + + def __del__(self): + """析构函数,关闭会话""" + if hasattr(self, "session"): + self.session.close() + + +# 使用示例 +if __name__ == "__main__": + # 测试代码只在直接运行此模块时执行,不在导入时执行 + # 以下代码已注释避免MCP服务器启动时执行 + pass + + # vector_service = VectorService(base_url="http://192.168.0.76:5002") + + # # 测试初始化向量存储 + # # init_result = vector_service.init_vector_store( + # # keyId="1945419433873575938", + # # ) + # # print("初始化向量存储结果:", init_result) + + # # 测试查询向量420 + # # query_result = vector_service.query_vector( + # # **{ + # # "keyId": "1932095424144715777", + # # "location": "灵泽办公区", + # # "device": "过道吊灯", + # # "operation": "开关 按键", + # # "top_k": 2, + # # "auto_create": True, + # # } + # # ) + # # print("查询向量结果:", query_result) + + # # # 检查向量库状态 + # # check_result = vector_service.check_vector_store_status("1932095424144715777") + # # logger.info(f"检查向量库状态结果: {check_result}") + + # # 测试搜索结果分析接口 + # # logger.info("=== 测试搜索结果分析接口 ===") + # sample_search_results = [ + # [ + # { + # "id": "0899574c-ff51-4983-ae48-667cccc08e9c", + # "location": { + # "key": "35", + # "description": "灵泽展厅;灵泽展区" + # }, + # "device": { + # "key": "switch.zimi_cn_1121232402_dhkg05_on_p_2_1", + # "description": "吊灯;灯;照明灯" + # }, + # "operation": { + # "key": "turn_off", + # "description": "关闭;关" + # }, + # "operation_params": [], + # "deviceType": "light" + # }, + # 0.9025000000000001, + # { + # "location_score": 0.95, + # "device_score": 0.75, + # "operation_score": 0.95, + # "operation_param_score": 1.0, + # "combined_score": 0.9025000000000001, + # "search_method": "location_first_strict" + # } + # ], + # [ + # { + # "id": "76f71468-356b-4710-a7ea-da7ddac93fab", + # "location": { + # "key": "35", + # "description": "灵泽展厅;灵泽展区" + # }, + # "device": { + # "key": "climate.qjiang_cn_741362991_wb20", + # "description": "空调;制冷设备" + # }, + # "operation": { + # "key": "turn_off", + # "description": "关空调;关闭空调;关闭;关;闭空调;空调" + # }, + # "operation_params": [], + # "deviceType": "airConditioner" + # }, + # 0.7150000000000001, + # { + # "location_score": 0.95, + # "device_score": 0.0, + # "operation_score": 0.95, + # "operation_param_score": 1.0, + # "combined_score": 0.7150000000000001, + # "search_method": "location_first_strict" + # } + # ] + # ] + + # # analyze_result = vector_service.analyze_search_results(sample_search_results) + # # logger.info(f"搜索结果分析结果: {analyze_result}") diff --git a/lzwcai_mcp_iot/main.py b/lzwcai_mcp_iot/main.py new file mode 100644 index 0000000..7309ce9 --- /dev/null +++ b/lzwcai_mcp_iot/main.py @@ -0,0 +1,17 @@ +import os + +# 设置企业ID(必需) +os.environ["ENTERPRISE_ID"] = "1952978233106669569" + +# 设置API地址 +os.environ["DEVICE_API_BASE_URL"] = "http://192.168.2.236:8088" +os.environ["VECTOR_API_BASE_URL"] = "http://192.168.2.236:5002" + +# 注意:employeeId 已不再需要,现在直接使用 ENTERPRISE_ID +# os.environ["employeeId"] = "1986712221817815042" # 已废弃 + +# 导入模块 +from lzwcai_mcp_iot.iot_device_tool import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lzwcai_mcp_iot/pyproject.toml b/lzwcai_mcp_iot/pyproject.toml new file mode 100644 index 0000000..d1e8ded --- /dev/null +++ b/lzwcai_mcp_iot/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "lzwcai-mcp-iot" +version = "0.3.3" +description = "IoT设备控制服务器,使用FastMCP框架提供设备操作功能" +authors = [ + {name = "LZWCAI开发团队", email = "dev@lzwcai.com"} +] +readme = "README.md" +requires-python = ">=3.8" +license = "LicenseRef-Proprietary" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Operating System :: OS Independent", +] +dependencies = [ + "fastmcp>=0.1.0", + "requests" +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "black>=23.1.0", + "isort>=5.12.0", + "flake8>=6.0.0", + "mypy>=1.0.0", +] + + +[project.scripts] +lzwcai-mcp-iot = "lzwcai_mcp_iot.iot_device_tool:main" + +[tool.setuptools] +packages = ["lzwcai_mcp_iot", "lzwcai_mcp_iot.src"] + + +[tool.black] +line-length = 88 +target-version = ["py38", "py39", "py310", "py311"] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] diff --git a/lzwcai_mcp_iot/setup.cfg b/lzwcai_mcp_iot/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/lzwcai_mcp_iot/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/lzwcai_mcp_sqlexecutor/README.md b/lzwcai_mcp_sqlexecutor/README.md new file mode 100644 index 0000000..44ee71d --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/README.md @@ -0,0 +1,138 @@ +# lzwcai-mcp-sqlexecutor + +一个基于 MCP (Model Context Protocol) 的 SQL 查询执行服务器,支持从 JSON 配置文件动态生成查询工具。 + +## 功能特性 + +- 🚀 动态工具生成:从 `businessQueries.json` 自动生成 MCP 工具 +- 🔧 灵活配置:支持自定义业务查询和参数验证 +- 📝 完整日志:详细的操作日志记录 +- 🌐 中文支持:工具名称自动转换为拼音 + +## 安装 + +### 使用 pip 安装 + +```bash +pip install lzwcai-mcp-sqlexecutor +``` + +### 从源码安装 + +```bash +git clone +cd lzwcai_mcp_sqlexecutor +pip install -e . +``` + +### 使用 uv 安装(推荐) + +```bash +uv pip install lzwcai-mcp-sqlexecutor +``` + +## 使用方法 + +### 命令行启动 + +安装后,可以直接通过命令启动: + +```bash +lzwcai-mcp-sqlexecutor +``` + +### 作为 Python 模块运行 + +```bash +python -m lzwcai_mcp_sqlexecutor.main +``` + +### 配置到 MCP 客户端 + +在你的 MCP 客户端配置文件中添加: + +```json +{ + "mcpServers": { + "lzwcai-sqlexecutor": { + "command": "lzwcai-mcp-sqlexecutor" + } + } +} +``` + +## 配置说明 + +### businessQueries.json + +在 `businessQueries.json` 中定义你的业务查询: + +```json +[ + { + "id": "query-001", + "businessName": "用户订单查询", + "businessDescription": "根据用户ID查询订单信息", + "sqlTemplate": "SELECT * FROM orders WHERE user_id = {{userId}}", + "parameters": { + "type": "object", + "required": ["userId"], + "properties": { + "userId": { + "type": "integer", + "description": "用户的唯一标识符", + "examples": [10086] + } + } + } + } +] +``` + +## 开发 + +### 依赖项 + +- Python >= 3.13 +- httpx >= 0.28.1 +- mcp[cli] >= 1.10.1 +- pypinyin >= 0.53.0 + +### 本地开发 + +```bash +# 克隆仓库 +git clone +cd lzwcai_mcp_sqlexecutor + +# 安装开发依赖 +pip install -e . + +# 运行服务器 +python -m lzwcai_mcp_sqlexecutor.main +``` + +## 构建与发布 + +### 使用 build 构建 + +```bash +pip install build +python -m build +``` + +### 发布到 PyPI + +```bash +pip install twine +twine upload dist/* +``` + +## 许可证 + +MIT License + +## 作者 + +lzwcai + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.gitignore b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.python-version b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/README.md b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/README.md new file mode 100644 index 0000000..7352630 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/README.md @@ -0,0 +1,154 @@ +# lzwcai-mcp-sqlexecutor + +一个基于 MCP (Model Context Protocol) 的 SQL 查询执行服务器,支持从 JSON 配置文件动态生成查询工具。 + +## 功能特性 + +- 🚀 动态工具生成:从 `businessQueries.json` 自动生成 MCP 工具 +- 🔧 灵活配置:支持自定义业务查询和参数验证 +- 📝 完整日志:详细的操作日志记录(仅输出到文件,不干扰MCP通信) +- 🌐 中文支持:工具名称自动转换为拼音 + +## 安装 + +### 使用 pip 安装 + +```bash +pip install lzwcai-mcp-sqlexecutor +``` + +### 从源码安装 + +```bash +git clone +cd lzwcai_mcp_sqlexecutor +pip install -e . +``` + +### 使用 uv 安装(推荐) + +```bash +uv pip install lzwcai-mcp-sqlexecutor +``` + +## 使用方法 + +### 命令行启动 + +安装后,可以直接通过命令启动: + +```bash +lzwcai-mcp-sqlexecutor +``` + +### 作为 Python 模块运行 + +```bash +python -m lzwcai_mcp_sqlexecutor.main +``` + +### 配置到 MCP 客户端 + +在你的 MCP 客户端配置文件中添加: + +```json +{ + "mcpServers": { + "lzwcai-sqlexecutor": { + "command": "lzwcai-mcp-sqlexecutor" + } + } +} +``` + +## 配置说明 + +### businessQueries.json + +在 `businessQueries.json` 中定义你的业务查询: + +```json +[ + { + "id": "query-001", + "businessName": "用户订单查询", + "businessDescription": "根据用户ID查询订单信息", + "sqlTemplate": "SELECT * FROM orders WHERE user_id = {{userId}}", + "parameters": { + "type": "object", + "required": ["userId"], + "properties": { + "userId": { + "type": "integer", + "description": "用户的唯一标识符", + "examples": [10086] + } + } + } + } +] +``` + +## 开发 + +### 依赖项 + +- Python >= 3.13 +- httpx >= 0.28.1 +- mcp[cli] >= 1.10.1 +- pypinyin >= 0.53.0 + +### 本地开发 + +```bash +# 克隆仓库 +git clone +cd lzwcai_mcp_sqlexecutor + +# 安装开发依赖 +pip install -e . + +# 运行服务器 +python -m lzwcai_mcp_sqlexecutor.main +``` + +## 构建与发布 + +### 使用 build 构建 + +```bash +pip install build +python -m build +``` + +### 发布到 PyPI + +```bash +pip install twine +twine upload dist/* +``` + +## 常见问题 + +### MCP Inspector 显示 JSON 解析错误 + +如果在使用 MCP Inspector 测试时遇到 `SyntaxError: Unexpected non-whitespace character after JSON` 错误,这是因为: + +1. **原因**:MCP 协议使用 stdio(标准输入输出)进行 JSON-RPC 通信,任何输出到 stdout 的内容(如 print 语句或控制台日志)都会破坏 JSON 格式。 + +2. **解决方案**:本服务器已将所有日志输出配置为仅写入文件(位于 `logs/` 目录),不输出到控制台。日志文件包括: + - `lzwcai_mcp_sqlexecutor.log` - 主日志文件 + - `lzwcai_mcp_sqlexecutor_error.log` - 错误日志 + - `lzwcai_mcp_sqlexecutor_daily.log` - 按日期滚动的日志 + - `mcp_services.log` - MCP 服务专用日志 + +3. **查看日志**:如果需要调试,请查看 `logs/` 目录下的日志文件。 + +## 许可证 + +MIT License + +## 作者 + +lzwcai + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/__init__.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/__init__.py new file mode 100644 index 0000000..5df6d9c --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/__init__.py @@ -0,0 +1,9 @@ +""" +lzwcai-mcp-sqlexecutor - MCP server for executing business SQL queries +""" + +__version__ = "0.1.2" +__author__ = "lzwcai" + +__all__ = [] + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/businessQueries.json b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/businessQueries.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/businessQueries.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor.log b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor.log new file mode 100644 index 0000000..e3c00dd --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor.log @@ -0,0 +1,51 @@ +2025-10-23 15:05:51 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs +2025-10-23 15:05:51 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981195682443014146 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:05:52 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981195682443014146 +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 +2025-10-23 15:05:52 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 "HTTP/1.1 200 " +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询失败: 技能不存在: 1981195682443014146', 'code': 500} +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 0 条技能数据 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:282] - 成功获取并处理 0 条数据 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:128] - API配置: 0 条 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:129] - API配置数组: [] +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 +2025-10-23 15:08:30 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs +2025-10-23 15:08:30 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981245768471322626 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:08:31 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981245768471322626 +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 +2025-10-23 15:08:31 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 "HTTP/1.1 200 " +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询成功', 'code': 200, 'data': [{'id': '1981245768769118209', 'createBy': 'yy8z6', 'createTime': '2025-10-23 14:26:16', 'updateBy': None, 'updateTime': None, 'serviceId': '1981245768475516930', 'uniqueName': '数字员工技能描述-SQL服务_数字员工列表查询', 'name': '数字员工列表查询', 'description': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'visualizable': 1, 'toolPrompt': '查询数字员工列表', 'toolType': 'sql', 'datasourceId': '32', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'sqlParams': '{"type":"object","required":[],"properties":{}}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '889,890,891,892,893,894,895,896', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{}', 'outputJsonSchema': '{"type":"object","properties":{"data":{"type":"array"}}}', 'lastExecutionTime': None}]} +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:283] - 技能 数字员工列表查询 (ID: 1981245768769118209) 的sqlParams为空,使用默认员工ID参数 +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 1 条技能数据 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:282] - 成功获取并处理 1 条数据 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:128] - API配置: 1 条 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:129] - API配置数组: [{'id': '1981245768769118209', 'businessName': '数字员工列表查询', 'businessDescription': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'parameters': {'type': 'object', 'required': ['employeeId'], 'properties': {'employeeId': {'type': 'number', 'description': '员工ID,用于标识员工的唯一数字标识符', 'examples': [1001, 2002]}}}, 'datasourceId': '32'}] +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:165] - 成功生成 1 个 MCP 工具 diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_daily.log b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_daily.log new file mode 100644 index 0000000..e3c00dd --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_daily.log @@ -0,0 +1,51 @@ +2025-10-23 15:05:51 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs +2025-10-23 15:05:51 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981195682443014146 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:05:52 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981195682443014146 +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 +2025-10-23 15:05:52 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 "HTTP/1.1 200 " +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981195682443014146 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询失败: 技能不存在: 1981195682443014146', 'code': 500} +2025-10-23 15:05:52 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 0 条技能数据 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:282] - 成功获取并处理 0 条数据 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:128] - API配置: 0 条 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:129] - API配置数组: [] +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 +2025-10-23 15:08:30 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs +2025-10-23 15:08:30 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981245768471322626 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:08:31 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981245768471322626 +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 +2025-10-23 15:08:31 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 "HTTP/1.1 200 " +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:85] - API调用成功: http://192.168.2.236:8088/datasource/skill/getBySkillId/1981245768471322626 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询成功', 'code': 200, 'data': [{'id': '1981245768769118209', 'createBy': 'yy8z6', 'createTime': '2025-10-23 14:26:16', 'updateBy': None, 'updateTime': None, 'serviceId': '1981245768475516930', 'uniqueName': '数字员工技能描述-SQL服务_数字员工列表查询', 'name': '数字员工列表查询', 'description': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'visualizable': 1, 'toolPrompt': '查询数字员工列表', 'toolType': 'sql', 'datasourceId': '32', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'sqlParams': '{"type":"object","required":[],"properties":{}}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '889,890,891,892,893,894,895,896', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{}', 'outputJsonSchema': '{"type":"object","properties":{"data":{"type":"array"}}}', 'lastExecutionTime': None}]} +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:283] - 技能 数字员工列表查询 (ID: 1981245768769118209) 的sqlParams为空,使用默认员工ID参数 +2025-10-23 15:08:31 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:297] - 成功处理 1 条技能数据 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:282] - 成功获取并处理 1 条数据 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:128] - API配置: 1 条 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:129] - API配置数组: [{'id': '1981245768769118209', 'businessName': '数字员工列表查询', 'businessDescription': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'parameters': {'type': 'object', 'required': ['employeeId'], 'properties': {'employeeId': {'type': 'number', 'description': '员工ID,用于标识员工的唯一数字标识符', 'examples': [1001, 2002]}}}, 'datasourceId': '32'}] +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:165] - 成功生成 1 个 MCP 工具 diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_error.log b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_error.log new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/mcp_services.log b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/mcp_services.log new file mode 100644 index 0000000..dedb405 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/mcp_services.log @@ -0,0 +1,36 @@ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981195682443014146 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:05:51 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981195682443014146 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询失败: 技能不存在: 1981195682443014146', 'code': 500} +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:282] - 成功获取并处理 0 条数据 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:128] - API配置: 0 条 +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:129] - API配置数组: [] +2025-10-23 15:05:52 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:341] - 开始运行 MCP SQL Executor 服务器 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:293] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:294] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:295] - 版本: 0.1.0 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:296] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:300] - 环境配置 - Database ID: 29 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:301] - 环境配置 - Skill ID: 1981245768471322626 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:302] - 环境配置 - Backend Base URL: http://192.168.2.236:8088 +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:303] - ============================================================ +2025-10-23 15:08:30 - mcp_services - INFO - [main.py:308] - MCP 服务器已启动,等待客户端连接... +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)... +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:272] - 调用第三方API,skill_id: 1981245768471322626 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:277] - 成功{'msg': '查询成功', 'code': 200, 'data': [{'id': '1981245768769118209', 'createBy': 'yy8z6', 'createTime': '2025-10-23 14:26:16', 'updateBy': None, 'updateTime': None, 'serviceId': '1981245768475516930', 'uniqueName': '数字员工技能描述-SQL服务_数字员工列表查询', 'name': '数字员工列表查询', 'description': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'visualizable': 1, 'toolPrompt': '查询数字员工列表', 'toolType': 'sql', 'datasourceId': '32', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'sqlParams': '{"type":"object","required":[],"properties":{}}', 'resultType': 'list', 'sourceType': 'ai', 'trainingTaskId': None, 'tableMetadataIds': '889,890,891,892,893,894,895,896', 'executionCount': 0, 'visualizationConfigs': None, 'inputJsonSchema': '{}', 'outputJsonSchema': '{"type":"object","properties":{"data":{"type":"array"}}}', 'lastExecutionTime': None}]} +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:282] - 成功获取并处理 1 条数据 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:128] - API配置: 1 条 +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:129] - API配置数组: [{'id': '1981245768769118209', 'businessName': '数字员工列表查询', 'businessDescription': '查询系统中的所有数字员工信息,包括员工ID、名称、类型、状态等核心信息,用于展示数字员工管理列表', 'sqlTemplate': 'SELECT id, employee_id, name, employee_type, status, created_at, updated_at, description FROM digital_employees WHERE is_deleted = 0 ORDER BY created_at DESC', 'parameters': {'type': 'object', 'required': ['employeeId'], 'properties': {'employeeId': {'type': 'number', 'description': '员工ID,用于标识员工的唯一数字标识符', 'examples': [1001, 2002]}}}, 'datasourceId': '32'}] +2025-10-23 15:08:31 - mcp_services - INFO - [main.py:165] - 成功生成 1 个 MCP 工具 diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/main.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/main.py new file mode 100644 index 0000000..f23dce6 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/main.py @@ -0,0 +1,358 @@ +from pathlib import Path +from typing import Any +import asyncio +import logging + +# 支持直接运行和模块导入两种方式 +try: + from .utils import load_json, generate_tool_name, generate_input_schema + from .utils import get_skill_by_id, DataSourceAPIClient, process_skill_response, test_sql_with_schema + from .utils import get_database_id, get_skill_id, get_env_config + from .utils.logger_config import logger_config +except ImportError: + from utils import load_json, generate_tool_name, generate_input_schema + from utils import get_skill_by_id, DataSourceAPIClient, process_skill_response, test_sql_with_schema + from utils import get_database_id, get_skill_id, get_env_config + from utils.logger_config import logger_config + +from mcp.server.models import InitializationOptions +from mcp.server import NotificationOptions, Server +import mcp.types as types + +# 初始化 MCP 专用日志器 +mcp_logger = logger_config.setup_mcp_logging() + +# ========== 数据源配置 ========== +# 数据源类型常量 +DATA_SOURCE_API = "api" # 仅使用API数据 +DATA_SOURCE_LOCAL = "local" # 仅使用本地JSON数据 +DATA_SOURCE_BOTH = "both" # 合并本地和API数据 + +# 默认数据源(可修改) +DEFAULT_DATA_SOURCE = DATA_SOURCE_API +# ================================ + + +def get_queries(): + """ + 获取业务查询配置 + + Returns: + list: 包含所有业务查询配置的列表 + """ + try: + # 获取当前文件所在目录 + current_dir = Path(__file__).parent + + # 构建 businessQueries.json 的路径 + json_path = current_dir / "businessQueries.json" + + mcp_logger.debug(f"正在读取业务查询配置文件: {json_path}") + + # 使用 load_json 方法读取 JSON 文件 + queries = load_json(json_path) + + mcp_logger.info(f"成功加载 {len(queries)} 个业务查询配置") + + return queries + except Exception as e: + mcp_logger.error(f"加载业务查询配置失败: {e}", exc_info=True) + raise + + +def generate_tool_schema_from_query(query: dict) -> types.Tool: + """ + 根据查询配置生成 MCP 工具模式 + + Args: + query: 单个查询配置字典 + + Returns: + types.Tool: MCP 工具对象 + """ + try: + # 获取参数定义并生成 inputSchema + parameters = query.get('parameters', {}) + input_schema = generate_input_schema(parameters) + + # 生成工具名称(格式: tool_拼音_id) + # tool_name = generate_tool_name(query['businessName'], query['id']) + tool_name = query['businessName'] + # 构建工具描述,包含业务名称和业务描述 + description = f"{query['businessName']}: {query['businessDescription']}" + + mcp_logger.debug(f"生成工具模式: {tool_name} - {query['businessName']}") + + return types.Tool( + name=tool_name, + description=description, + inputSchema=input_schema + ) + except Exception as e: + mcp_logger.error(f"生成工具模式失败: {query.get('id', 'unknown')}, 错误: {e}", exc_info=True) + raise + + +# 创建 MCP 服务器实例 +server = Server("lzwcai-mcp-sqlexecutor") + +# 缓存查询配置,避免重复加载 +_queries_cache = None + + +async def get_queries_cache(source: str = None): + """ + 获取或初始化查询配置缓存 + + Args: + source: 数据源类型(默认使用 DEFAULT_DATA_SOURCE) + - "api": 仅使用API数据 + - "local": 仅使用本地JSON数据 + - "both": 合并本地和API数据 + + Returns: + 查询配置列表 + """ + global _queries_cache + if _queries_cache is None: + source = source or DEFAULT_DATA_SOURCE + mcp_logger.info(f"初始化查询配置(数据源: {source})...") + + if source == DATA_SOURCE_LOCAL: + _queries_cache = get_queries() + mcp_logger.info(f"本地配置: {len(_queries_cache)} 条") + + elif source == DATA_SOURCE_API: + try: + _queries_cache = await call_third_party_api() + mcp_logger.info(f"API配置: {len(_queries_cache)} 条") + mcp_logger.info(f"API配置数组: {_queries_cache}") + except Exception as e: + mcp_logger.warning(f"API获取失败,降级使用本地配置: {e}") + _queries_cache = get_queries() + + else: # DATA_SOURCE_BOTH + local = get_queries() + try: + api = await call_third_party_api() + except Exception as e: + mcp_logger.warning(f"API获取失败: {e}") + api = [] + _queries_cache = local + api + mcp_logger.info(f"配置总数: {len(_queries_cache)} 条(本地{len(local)}+API{len(api)})") + + return _queries_cache + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """ + 列出所有动态生成的 MCP 工具 + + Returns: + list[types.Tool]: 所有可用的工具列表 + """ + try: + mcp_logger.info("收到列出工具请求") + + queries = await get_queries_cache() + tools = [] + + for query in queries: + tool = generate_tool_schema_from_query(query) + tools.append(tool) + + mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具") + mcp_logger.debug(f"工具列表: {[tool.name for tool in tools]}") + + return tools + except Exception as e: + mcp_logger.error(f"列出工具失败: {e}", exc_info=True) + raise + + +@server.call_tool() +async def handle_call_tool( + name: str, + arguments: dict[str, Any] | None +) -> list[types.TextContent]: + """ + 处理工具调用请求 + + Args: + name: 工具名称 + arguments: 工具参数 + + Returns: + list[types.TextContent]: 工具执行结果(返回参数和对应的接口配置) + """ + try: + mcp_logger.info(f"收到工具调用请求: {name}") + mcp_logger.debug(f"工具参数: {arguments}") + + # 获取查询配置缓存 + queries = await get_queries_cache() + + # 根据工具名称查找对应的 item(接口配置) + tool_item = None + for query in queries: + # tool_name = generate_tool_name(query['businessName'], query['id']) + tool_name = query['businessName'] + if tool_name == name: + tool_item = query + break + + # 构建返回结果 + import json + + if tool_item: + request_data = { + "datasourceId": tool_item.get("datasourceId"), + "businessName": tool_item.get("businessName"), + "businessDescription": tool_item.get("businessDescription"), + "sqlTemplate": tool_item.get("sqlTemplate"), + "parameters": tool_item.get("parameters"), + "testParams": arguments or {} + } + + # 如果 arguments 中有 targetDatabaseName 且有值,添加到 request_data + if arguments and arguments.get("targetDatabaseName"): + request_data["targetDatabaseName"] = arguments["targetDatabaseName"] + mcp_logger.debug(f"添加目标数据库名称: {arguments['targetDatabaseName']}") + + # 调用测试SQL API + try: + mcp_logger.info("正在调用测试SQL API...") + api_response = test_sql_with_schema(request_data) + mcp_logger.info("测试SQL API调用成功") + + # 只返回 API 响应结果 + result_text = json.dumps(api_response, ensure_ascii=False, indent=2) + + except Exception as e: + error_msg = f"调用测试SQL API失败: {str(e)}" + mcp_logger.error(error_msg, exc_info=True) + result_text = json.dumps({"error": error_msg}, ensure_ascii=False, indent=2) + else: + error_msg = f"未找到工具 {name} 对应的配置" + result_text = json.dumps({"error": error_msg}, ensure_ascii=False, indent=2) + + mcp_logger.debug(f"工具调用结果: {result_text}") + + return [ + types.TextContent( + type="text", + text=result_text + ) + ] + except Exception as e: + error_msg = f"工具调用失败: {name}, 错误: {e}" + mcp_logger.error(error_msg, exc_info=True) + return [ + types.TextContent( + type="text", + text=f"错误: {error_msg}" + ) + ] + + +async def call_third_party_api(skill_id: str = None) -> list: + """ + 调用第三方API获取技能信息并返回处理后的数据 + + Args: + skill_id: 技能ID(默认从环境变量 SKILL_ID 读取,如果未设置则使用 1981000305474482178) + + Returns: + 处理后的查询配置列表(businessQueries格式) + + Example: + queries = await call_third_party_api() + # 返回: [{"id": "...", "businessName": "...", ...}, ...] + """ + try: + # 如果没有传入 skill_id,则从环境变量读取 + if skill_id is None: + skill_id = get_skill_id() + + mcp_logger.info(f"调用第三方API,skill_id: {skill_id}") + + # 获取原始数据 + raw_result = get_skill_by_id(skill_id) + + mcp_logger.info(f"成功{raw_result}") + + # 处理并返回 + processed_queries = process_skill_response(raw_result) + + mcp_logger.info(f"成功获取并处理 {len(processed_queries)} 条数据") + return processed_queries + + except Exception as e: + mcp_logger.error(f"API调用失败: {e}", exc_info=True) + raise + + +async def async_main(): + """MCP 服务器异步主函数""" + try: + mcp_logger.info("=" * 60) + mcp_logger.info("正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor") + mcp_logger.info("版本: 0.1.0") + mcp_logger.info("=" * 60) + + # 输出环境配置信息 + env_config = get_env_config() + mcp_logger.info(f"环境配置 - Database ID: {env_config['database_id']}") + mcp_logger.info(f"环境配置 - Skill ID: {env_config['skill_id']}") + mcp_logger.info(f"环境配置 - Backend Base URL: {env_config['backend_base_url']}") + mcp_logger.info("=" * 60) + + from mcp.server.stdio import stdio_server + + async with stdio_server() as (read_stream, write_stream): + mcp_logger.info("MCP 服务器已启动,等待客户端连接...") + + await server.run( + read_stream, + write_stream, + InitializationOptions( + server_name="lzwcai-mcp-sqlexecutor", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + mcp_logger.info("MCP 服务器已关闭") + + except Exception as e: + mcp_logger.error(f"MCP 服务器运行失败: {e}", exc_info=True) + raise + + +def main(): + """入口点函数(用于 console_scripts)""" + try: + # 初始化系统日志 + # MCP协议使用stdio通信,必须禁用控制台输出以避免干扰JSON-RPC通信 + logger_config.setup_logging( + app_name="lzwcai_mcp_sqlexecutor", + log_level=logging.INFO, + console_output=False # 禁用控制台输出 + ) + + mcp_logger.info("开始运行 MCP SQL Executor 服务器") + asyncio.run(async_main()) + + except KeyboardInterrupt: + mcp_logger.info("收到中断信号,正在关闭服务器...") + except Exception as e: + mcp_logger.error(f"程序运行失败: {e}", exc_info=True) + raise + + +if __name__ == "__main__": + main() diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/pyproject.toml b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/pyproject.toml new file mode 100644 index 0000000..33d9c30 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-mcp-sqlexecutor" +version = "0.1.6" +description = "MCP server for executing business SQL queries with dynamic tool generation" +readme = "README.md" +requires-python = ">=3.13" +license = {text = "MIT"} +authors = [ + {name = "lzwcai", email = "your-email@example.com"}, +] +keywords = ["mcp", "sql", "executor", "server"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.10.1", + "pypinyin>=0.53.0", +] + +[project.scripts] +lzwcai-mcp-sqlexecutor = "lzwcai_mcp_sqlexecutor.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_mcp_sqlexecutor"] + +[tool.hatch.build.targets.wheel.force-include] +"lzwcai_mcp_sqlexecutor/businessQueries.json" = "lzwcai_mcp_sqlexecutor/businessQueries.json" diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/__init__.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/__init__.py new file mode 100644 index 0000000..4fdc6f7 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/__init__.py @@ -0,0 +1,24 @@ +"""Utils package for lzwcai_mcp_sqlexecutor""" + +from .json_helper import load_json +from .name_helper import generate_tool_name +from .schema_helper import generate_input_schema, validate_input_schema +from .api_client import DataSourceAPIClient, get_skill_by_id, process_skill_response, test_sql_with_schema +from .env_config import get_database_id, get_skill_id, get_backend_base_url, get_env_config, set_env_variable + +__all__ = [ + 'load_json', + 'generate_tool_name', + 'generate_input_schema', + 'validate_input_schema', + 'DataSourceAPIClient', + 'get_skill_by_id', + 'process_skill_response', + 'test_sql_with_schema', + 'get_database_id', + 'get_skill_id', + 'get_backend_base_url', + 'get_env_config', + 'set_env_variable' +] + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/api_client.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/api_client.py new file mode 100644 index 0000000..96d66eb --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/api_client.py @@ -0,0 +1,303 @@ +""" +第三方API调用客户端 +用于调用外部数据源接口 +""" + +import httpx +import logging +import json +from typing import Dict, Any, Optional, List + +# 支持直接运行和模块导入两种方式 +try: + from .env_config import get_backend_base_url +except ImportError: + from env_config import get_backend_base_url + +# 获取日志记录器 +logger = logging.getLogger(__name__) + + +class DataSourceAPIClient: + """数据源API客户端""" + + def __init__( + self, + base_url: Optional[str] = None, + token: Optional[str] = None + ): + """ + 初始化API客户端 + + Args: + base_url: API基础URL(默认从环境变量 BACKEND_BASE_URL 读取,如果未设置则使用 http://192.168.2.236:8088) + token: 认证令牌(Bearer Token) + """ + # 如果没有传入 base_url,则从环境变量读取 + if base_url is None: + base_url = get_backend_base_url() + + self.base_url = base_url.rstrip('/') + self.token = token or "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjJiYTk4ODllLWM2ZGItNDQ5YS1iZmFjLTQ2YzMxODFlODg5NCJ9.dvi8zm0LsWvJ_h9zD5blnHFRxa4z4_WBm1R487ekE7HlHzrN6dnvqhK8askqT5b1EcE8myHwRzLVMoI8UOjOrw" + self.client = httpx.Client(timeout=30.0) + + def _get_headers(self) -> Dict[str, str]: + """ + 获取请求头 + + Returns: + 请求头字典 + """ + return { + 'Authorization': f'Bearer {self.token}', + } + + def get_skill_by_id(self, skill_id: str) -> Dict[str, Any]: + """ + 根据技能ID获取技能信息 + + Args: + skill_id: 技能ID + + Returns: + API响应数据 + + Raises: + Exception: 请求失败时抛出 + """ + try: + url = f"{self.base_url}/datasource/skill/getBySkillId/{skill_id}" + + logger.info(f"正在调用API: {url}") + logger.debug(f"请求参数 - skill_id: {skill_id}") + + response = self.client.get( + url, + headers=self._get_headers() + ) + + # 检查HTTP状态码 + response.raise_for_status() + + # 解析JSON响应 + data = response.json() + + logger.info(f"API调用成功: {url}") + logger.debug(f"响应数据: {data}") + + return data + + except httpx.TimeoutException: + error_msg = f"API请求超时: {url}" + logger.error(error_msg) + raise Exception(error_msg) + + except httpx.HTTPStatusError as e: + error_msg = f"API请求失败 (HTTP {e.response.status_code}): {url}" + logger.error(error_msg) + logger.error(f"错误响应: {e.response.text}") + raise Exception(error_msg) + + except httpx.RequestError as e: + error_msg = f"API请求异常: {url}, 错误: {str(e)}" + logger.error(error_msg) + raise Exception(error_msg) + + except Exception as e: + error_msg = f"处理API响应时出错: {str(e)}" + logger.error(error_msg, exc_info=True) + raise Exception(error_msg) + + def test_sql_with_schema(self, request_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 测试SQL语句并返回执行结果 + + Args: + request_data: 请求数据,包含以下字段: + - datasourceId: 数据源ID + - businessName: 业务名称 + - businessDescription: 业务描述 + - sqlTemplate: SQL模板 + - parameters: 参数定义 + - testParams: 测试参数 + + Returns: + API响应数据 + + Raises: + Exception: 请求失败时抛出 + """ + try: + url = f"{self.base_url}/datasource/sqlExecutionLog/testSqlWithSchema" + + # 构建请求头(包含Content-Type) + headers = self._get_headers() + headers['Content-Type'] = 'application/json' + headers['Accept'] = '*/*' + + logger.info(f"正在调用测试SQL API: {url}") + logger.debug(f"请求数据: {json.dumps(request_data, ensure_ascii=False, indent=2)}") + + # 发送POST请求 + response = self.client.post( + url, + headers=headers, + json=request_data + ) + + # 检查HTTP状态码 + response.raise_for_status() + + # 解析JSON响应 + data = response.json() + + logger.info(f"测试SQL API调用成功") + logger.debug(f"响应数据: {json.dumps(data, ensure_ascii=False, indent=2)}") + + return data + + except httpx.TimeoutException: + error_msg = f"测试SQL API请求超时: {url}" + logger.error(error_msg) + raise Exception(error_msg) + + except httpx.HTTPStatusError as e: + error_msg = f"测试SQL API请求失败 (HTTP {e.response.status_code}): {url}" + logger.error(error_msg) + logger.error(f"错误响应: {e.response.text}") + raise Exception(error_msg) + + except httpx.RequestError as e: + error_msg = f"测试SQL API请求异常: {url}, 错误: {str(e)}" + logger.error(error_msg) + raise Exception(error_msg) + + except Exception as e: + error_msg = f"处理测试SQL API响应时出错: {str(e)}" + logger.error(error_msg, exc_info=True) + raise Exception(error_msg) + + def close(self): + """关闭HTTP客户端""" + self.client.close() + + +# 创建默认客户端实例 +default_client = DataSourceAPIClient() + + +def get_skill_by_id(skill_id: str, base_url: Optional[str] = None, token: Optional[str] = None) -> Dict[str, Any]: + """ + 便捷函数:根据技能ID获取技能信息 + + Args: + skill_id: 技能ID + base_url: API基础URL(可选,默认从环境变量 BACKEND_BASE_URL 读取) + token: 认证令牌(可选,使用默认值) + + Returns: + API响应数据 + """ + if base_url or token: + client = DataSourceAPIClient( + base_url=base_url, + token=token + ) + return client.get_skill_by_id(skill_id) + else: + return default_client.get_skill_by_id(skill_id) + + +def test_sql_with_schema(request_data: Dict[str, Any], base_url: Optional[str] = None, token: Optional[str] = None) -> Dict[str, Any]: + """ + 便捷函数:测试SQL语句并返回执行结果 + + Args: + request_data: 请求数据,包含以下字段: + - datasourceId: 数据源ID + - businessName: 业务名称 + - businessDescription: 业务描述 + - sqlTemplate: SQL模板 + - parameters: 参数定义 + - testParams: 测试参数 + base_url: API基础URL(可选,默认从环境变量 BACKEND_BASE_URL 读取) + token: 认证令牌(可选,使用默认值) + + Returns: + API响应数据 + """ + if base_url or token: + client = DataSourceAPIClient( + base_url=base_url, + token=token + ) + return client.test_sql_with_schema(request_data) + else: + return default_client.test_sql_with_schema(request_data) + + +def process_skill_response(response: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + 处理API响应数据,映射为businessQueries格式 + + Args: + response: API原始响应数据 + + Returns: + 处理后的查询配置列表 + """ + try: + # 提取data数组 + data_list = response.get("data", []) + + # 默认的员工ID参数schema + default_employee_schema = { + "type": "object", + "required": ["employeeId"], + "properties": { + "employeeId": { + "type": "number", + "description": "员工ID,用于标识员工的唯一数字标识符", + "examples": [1001, 2002] + } + } + } + + # 映射每个skill为businessQuery格式 + queries = [] + for skill in data_list: + # 解析sqlParams字符串为JSON对象 + sql_params = json.loads(skill.get("sqlParams", "{}")) + + # 判断sqlParams是否为空对象 + is_empty_params = ( + not sql_params.get("properties") or + len(sql_params.get("properties", {})) == 0 + ) and ( + not sql_params.get("required") or + len(sql_params.get("required", [])) == 0 + ) + + # 如果是空对象,使用默认的员工ID参数 + if is_empty_params: + logger.info(f"技能 {skill.get('name')} (ID: {skill.get('id')}) 的sqlParams为空,使用默认员工ID参数") + sql_params = default_employee_schema + + # 映射字段 + query = { + "id": skill.get("id"), + "businessName": skill.get("name"), + "businessDescription": skill.get("description"), + "sqlTemplate": skill.get("sqlTemplate"), + "parameters": sql_params, + "datasourceId": skill.get("datasourceId") + } + queries.append(query) + + logger.info(f"成功处理 {len(queries)} 条技能数据") + return queries + + except Exception as e: + logger.error(f"处理API响应数据失败: {e}", exc_info=True) + raise + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/env_config.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/env_config.py new file mode 100644 index 0000000..2b87425 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/env_config.py @@ -0,0 +1,88 @@ +"""环境变量配置模块""" + +import os +from typing import Optional + + +def get_database_id(default: str = "29") -> str: + """ + 获取数据库ID环境变量 + + Args: + default: 默认值(默认为 "29") + + Returns: + str: 数据库ID + + Environment Variables: + databaseId: 数据库ID + """ + return os.environ.get("databaseId", default) + + +def get_skill_id(default: str = "") -> str: + """ + 获取技能ID环境变量 + + Args: + default: 默认值(默认为 "") + + Returns: + str: 技能ID + + Environment Variables: + skillId: 技能ID + """ + return os.environ.get("skillId", default) + + +def get_backend_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str: + """ + 获取后端API基础URL环境变量 + + Args: + default: 默认值(默认为 "http://lzwcai-demp-corp-manager:8086") + + Returns: + str: 后端API基础URL + + Environment Variables: + backendBaseUrl: 后端API基础URL + """ + return os.environ.get("backendBaseUrl", default) + + +def get_env_config() -> dict: + """ + 获取所有环境配置 + + Returns: + dict: 包含所有配置的字典 + + Example: + config = get_env_config() + print(config['database_id']) # 输出: "29" + print(config['skill_id']) # 输出: "" + print(config['backend_base_url']) # 输出: "http://lzwcai-demp-corp-manager:8086" + """ + return { + "database_id": get_database_id(), + "skill_id": get_skill_id(), + "backend_base_url": get_backend_base_url() + } + + +def set_env_variable(key: str, value: str) -> None: + """ + 设置环境变量(仅在当前进程中有效) + + Args: + key: 环境变量名 + value: 环境变量值 + + Example: + set_env_variable("databaseId", "30") + set_env_variable("skillId", "1234567890") + """ + os.environ[key] = value + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/json_helper.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/json_helper.py new file mode 100644 index 0000000..1a6fc95 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/json_helper.py @@ -0,0 +1,60 @@ +"""JSON 文件读取工具""" + +import json +from pathlib import Path +from typing import Any, Union + + +def load_json(json_path: Union[str, Path]) -> Any: + """ + 读取 JSON 文件并返回其内容 + + Args: + json_path: JSON 文件的路径(支持字符串或 Path 对象) + + Returns: + JSON 文件中解析后的数据(可以是字典、列表或其他 JSON 类型) + + Raises: + FileNotFoundError: 当文件不存在时 + json.JSONDecodeError: 当 JSON 格式无效时 + Exception: 其他读取错误 + + Example: + >>> data = load_json('config.json') + >>> print(data) + {'key': 'value'} + + >>> data = load_json(Path('data/users.json')) + >>> print(data) + [{'id': 1, 'name': 'Alice'}] + """ + try: + # 转换为 Path 对象 + path = Path(json_path) + + # 检查文件是否存在 + if not path.exists(): + raise FileNotFoundError(f"JSON 文件不存在: {json_path}") + + # 检查是否为文件 + if not path.is_file(): + raise ValueError(f"路径不是一个文件: {json_path}") + + # 读取并解析 JSON 文件 + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + + return data + + except json.JSONDecodeError as e: + raise json.JSONDecodeError( + f"JSON 格式错误: {e.msg}", + e.doc, + e.pos + ) + except FileNotFoundError: + raise + except Exception as e: + raise Exception(f"读取 JSON 文件时发生错误: {str(e)}") + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/logger_config.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/logger_config.py new file mode 100644 index 0000000..290964f --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/logger_config.py @@ -0,0 +1,489 @@ +# -*- coding: utf-8 -*- +""" +统一日志配置模块 +提供系统级别的日志配置和管理 +""" + +import os +import sys +import logging +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler +from datetime import datetime +from pathlib import Path + + +class LoggerConfig: + """日志配置管理类""" + + def __init__(self, logs_dir: str = None): + """初始化日志配置 + + Args: + logs_dir: 日志目录路径,默认为项目根目录下的logs文件夹 + """ + # 确定日志目录 + if logs_dir: + self.logs_dir = Path(logs_dir) + else: + # 获取项目根目录(logger_config.py 在 utils 目录下,需要上升两层到达项目根目录) + project_root = Path(__file__).parent.parent + self.logs_dir = project_root / "logs" + + # 创建日志目录 + self.logs_dir.mkdir(exist_ok=True) + + # 日志格式 + self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s' + self.date_format = '%Y-%m-%d %H:%M:%S' + + # 从环境变量获取日志级别,默认为INFO + self.log_level = self._get_log_level_from_env() + + # 是否已初始化 + self._initialized = False + + def _get_log_level_from_env(self) -> int: + """从环境变量获取日志级别 + + Returns: + int: 日志级别 + """ + log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper() + + # 日志级别映射 + level_mapping = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'WARN': logging.WARNING, # 兼容性别名 + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, + 'FATAL': logging.CRITICAL # 兼容性别名 + } + + return level_mapping.get(log_level_str, logging.INFO) + + def setup_logging(self, + app_name: str = "lzwcai_mcp_sqlexecutor", + log_level: int = logging.INFO, + max_file_size: int = 10 * 1024 * 1024, # 10MB + backup_count: int = 5, + console_output: bool = True) -> logging.Logger: + """设置系统日志配置 + + Args: + app_name: 应用名称,用于日志文件命名 + log_level: 日志级别 + max_file_size: 单个日志文件最大大小(字节) + backup_count: 保留的备份文件数量 + console_output: 是否输出到控制台 + + Returns: + logging.Logger: 配置好的根日志器 + """ + if self._initialized: + return logging.getLogger() + + # 设置根日志器 + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # 清除现有的处理器 + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 创建格式化器 + formatter = logging.Formatter(self.log_format, self.date_format) + + # 1. 主日志文件 - 按大小滚动 + main_log_file = self.logs_dir / f"{app_name}.log" + file_handler = RotatingFileHandler( + main_log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8' + ) + file_handler.setLevel(log_level) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + # 2. 错误日志文件 - 只记录ERROR及以上级别 + error_log_file = self.logs_dir / f"{app_name}_error.log" + error_handler = RotatingFileHandler( + error_log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8' + ) + error_handler.setLevel(logging.ERROR) + error_handler.setFormatter(formatter) + root_logger.addHandler(error_handler) + + # 3. 按日期滚动的日志文件 + daily_log_file = self.logs_dir / f"{app_name}_daily.log" + daily_handler = TimedRotatingFileHandler( + daily_log_file, + when='midnight', + interval=1, + backupCount=30, # 保留30天 + encoding='utf-8' + ) + daily_handler.setLevel(log_level) + daily_handler.setFormatter(formatter) + daily_handler.suffix = "%Y-%m-%d" + root_logger.addHandler(daily_handler) + + # 4. 控制台输出 + # 重要:MCP协议使用stdio时,必须将日志输出到stderr,stdout仅用于JSON-RPC通信 + if console_output: + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(log_level) + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + self.date_format + ) + console_handler.setFormatter(console_formatter) + root_logger.addHandler(console_handler) + + self._initialized = True + + # 记录初始化信息 + root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}") + root_logger.info(f"日志配置 - 级别: {logging.getLevelName(log_level)}, 文件大小限制: {max_file_size//1024//1024}MB, 备份数量: {backup_count}") + + return root_logger + + def get_module_logger(self, module_name: str) -> logging.Logger: + """获取模块专用日志器 + + Args: + module_name: 模块名称 + + Returns: + logging.Logger: 模块日志器 + """ + return logging.getLogger(module_name) + + def create_component_logger(self, + component_name: str, + log_file: str = None, + log_level: int = None) -> logging.Logger: + """为特定组件创建独立的日志器 + + Args: + component_name: 组件名称 + log_file: 独立日志文件名(可选) + log_level: 日志级别(可选) + + Returns: + logging.Logger: 组件日志器 + """ + logger = logging.getLogger(component_name) + + if log_file: + # 为组件创建独立的日志文件 + component_log_file = self.logs_dir / log_file + handler = RotatingFileHandler( + component_log_file, + maxBytes=5 * 1024 * 1024, # 5MB + backupCount=3, + encoding='utf-8' + ) + + formatter = logging.Formatter(self.log_format, self.date_format) + handler.setFormatter(formatter) + + if log_level: + handler.setLevel(log_level) + + logger.addHandler(handler) + logger.info(f"组件日志器创建完成: {component_name} -> {component_log_file}") + + return logger + + def setup_mqtt_logging(self) -> logging.Logger: + """设置MQTT专用日志 + + Returns: + logging.Logger: MQTT日志器 + """ + return self.create_component_logger( + "mqtt_communication", + "mqtt_communication.log", + logging.DEBUG + ) + + def setup_mcp_logging(self) -> logging.Logger: + """设置MCP专用日志 + + Returns: + logging.Logger: MCP日志器 + """ + return self.create_component_logger( + "mcp_services", + "mcp_services.log", + logging.DEBUG + ) + + def setup_api_logging(self) -> logging.Logger: + """设置API专用日志 + + Returns: + logging.Logger: API日志器 + """ + return self.create_component_logger( + "api_requests", + "api_requests.log", + logging.INFO + ) + + def get_logs_info(self) -> dict: + """获取日志系统信息 + + Returns: + dict: 日志系统信息 + """ + log_files = [] + if self.logs_dir.exists(): + for log_file in self.logs_dir.glob("*.log*"): + stat = log_file.stat() + log_files.append({ + "name": log_file.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + }) + + return { + "logs_directory": str(self.logs_dir), + "initialized": self._initialized, + "log_files": log_files, + "total_files": len(log_files) + } + + def cleanup_old_logs(self, days: int = 30): + """清理旧日志文件 + + Args: + days: 保留天数 + """ + if not self.logs_dir.exists(): + return + + from datetime import timedelta + cutoff_time = datetime.now() - timedelta(days=days) + + cleaned_files = [] + for log_file in self.logs_dir.glob("*.log*"): + if log_file.stat().st_mtime < cutoff_time.timestamp(): + try: + log_file.unlink() + cleaned_files.append(log_file.name) + except Exception as e: + logging.error(f"清理日志文件失败: {log_file.name}, 错误: {e}") + + if cleaned_files: + logging.info(f"清理了 {len(cleaned_files)} 个旧日志文件: {cleaned_files}") + + def set_log_level(self, level: int, logger_name: str = None): + """动态调整日志级别 + + Args: + level: 新的日志级别 (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) + logger_name: 指定日志器名称,None表示调整根日志器 + """ + if logger_name: + logger = logging.getLogger(logger_name) + else: + logger = logging.getLogger() + + old_level = logger.level + logger.setLevel(level) + + # 同时调整所有处理器的级别 + for handler in logger.handlers: + if not isinstance(handler, logging.StreamHandler) or handler.stream not in (sys.stdout, sys.stderr): + # 不调整控制台处理器的级别,保持原有设置 + handler.setLevel(level) + + level_name = logging.getLevelName(level) + old_level_name = logging.getLevelName(old_level) + target = logger_name or "根日志器" + + logger.info(f"日志级别已调整: {target} {old_level_name} -> {level_name}") + + def set_temporary_log_level(self, level: int, logger_name: str = None): + """临时调整日志级别(会保存原始级别用于恢复) + + Args: + level: 临时日志级别 + logger_name: 指定日志器名称,None表示调整根日志器 + """ + if not hasattr(self, '_original_levels'): + self._original_levels = {} + + target_name = logger_name or 'root' + + if logger_name: + logger = logging.getLogger(logger_name) + else: + logger = logging.getLogger() + + # 保存原始级别 + if target_name not in self._original_levels: + self._original_levels[target_name] = logger.level + + # 设置新级别 + self.set_log_level(level, logger_name) + + level_name = logging.getLevelName(level) + target = logger_name or "根日志器" + logger.info(f"临时调整日志级别: {target} -> {level_name} (可通过restore_log_level恢复)") + + def restore_log_level(self, logger_name: str = None): + """恢复日志级别到调整前的状态 + + Args: + logger_name: 指定日志器名称,None表示恢复根日志器 + """ + if not hasattr(self, '_original_levels'): + logging.warning("没有找到保存的原始日志级别") + return + + target_name = logger_name or 'root' + + if target_name not in self._original_levels: + logging.warning(f"没有找到 {target_name} 的原始日志级别") + return + + original_level = self._original_levels[target_name] + self.set_log_level(original_level, logger_name) + + # 清除保存的级别 + del self._original_levels[target_name] + + target = logger_name or "根日志器" + level_name = logging.getLevelName(original_level) + logging.info(f"已恢复日志级别: {target} -> {level_name}") + + def get_current_log_levels(self) -> dict: + """获取当前所有日志器的级别信息 + + Returns: + dict: 日志器级别信息 + """ + levels_info = {} + + # 根日志器 + root_logger = logging.getLogger() + levels_info['root'] = { + 'level': root_logger.level, + 'level_name': logging.getLevelName(root_logger.level), + 'handlers_count': len(root_logger.handlers) + } + + # 其他已创建的日志器 + for name, logger in logging.Logger.manager.loggerDict.items(): + if isinstance(logger, logging.Logger): + levels_info[name] = { + 'level': logger.level, + 'level_name': logging.getLevelName(logger.level), + 'handlers_count': len(logger.handlers) + } + + return levels_info + + +# 全局日志配置实例 +logger_config = LoggerConfig() + + +def setup_system_logging(app_name: str = "lzwcai_mcp_sqlexecutor", + log_level: int = logging.INFO) -> logging.Logger: + """系统日志初始化快捷函数 + + Args: + app_name: 应用名称 + log_level: 日志级别 + + Returns: + logging.Logger: 根日志器 + """ + return logger_config.setup_logging(app_name, log_level) + + +def get_logger(name: str) -> logging.Logger: + """获取日志器的快捷函数 + + Args: + name: 日志器名称 + + Returns: + logging.Logger: 日志器实例 + """ + return logger_config.get_module_logger(name) + + +def set_log_level(level: int, logger_name: str = None): + """动态调整日志级别的快捷函数 + + Args: + level: 新的日志级别 (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) + logger_name: 指定日志器名称,None表示调整根日志器 + + Examples: + # 调整根日志器为DEBUG级别 + set_log_level(logging.DEBUG) + + # 调整特定模块日志器为WARNING级别 + set_log_level(logging.WARNING, "agent_ontology.core") + """ + logger_config.set_log_level(level, logger_name) + + +def set_temporary_log_level(level: int, logger_name: str = None): + """临时调整日志级别的快捷函数 + + Args: + level: 临时日志级别 + logger_name: 指定日志器名称,None表示调整根日志器 + + Examples: + # 临时调整为DEBUG级别进行调试 + set_temporary_log_level(logging.DEBUG) + # ... 进行调试 ... + # 恢复原始级别 + restore_log_level() + """ + logger_config.set_temporary_log_level(level, logger_name) + + +def restore_log_level(logger_name: str = None): + """恢复日志级别的快捷函数 + + Args: + logger_name: 指定日志器名称,None表示恢复根日志器 + """ + logger_config.restore_log_level(logger_name) + + +def get_current_log_levels() -> dict: + """获取当前日志级别信息的快捷函数 + + Returns: + dict: 日志器级别信息 + + Examples: + levels = get_current_log_levels() + print(f"根日志器级别: {levels['root']['level_name']}") + """ + return logger_config.get_current_log_levels() + + +# 便捷的日志级别常量 +class LogLevel: + """日志级别常量类""" + DEBUG = logging.DEBUG + INFO = logging.INFO + WARNING = logging.WARNING + ERROR = logging.ERROR + CRITICAL = logging.CRITICAL \ No newline at end of file diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/name_helper.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/name_helper.py new file mode 100644 index 0000000..d66cf32 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/name_helper.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +名称生成工具模块 +""" + +from pypinyin import lazy_pinyin, Style +import logging + +logger = logging.getLogger(__name__) + + +def generate_tool_name(business_name: str, tool_id: str) -> str: + """ + 根据业务名称和ID生成工具名称 + 格式: tool_拼音_id + + Args: + business_name: 业务名称(中文) + tool_id: 工具ID + + Returns: + str: 格式化的工具名称 + """ + try: + # 将中文转换为拼音(无音调,小写) + pinyin_list = lazy_pinyin(business_name, style=Style.NORMAL) + # 拼接拼音 + pinyin_str = ''.join(pinyin_list) + + # 将 ID 中的 '-' 替换为 '_' + formatted_id = tool_id.replace('-', '_') + + # 组合成最终的工具名称 + tool_name = f"tool_{pinyin_str}_{formatted_id}" + + return tool_name + except Exception as e: + logger.error(f"生成工具名称失败: {business_name}, {tool_id}, 错误: {e}", exc_info=True) + # 降级处理:如果拼音转换失败,使用 ID + return f"tool_{tool_id.replace('-', '_')}" + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/schema_helper.py b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/schema_helper.py new file mode 100644 index 0000000..06ea3ed --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/schema_helper.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +Schema 生成工具模块 +""" + +from typing import Any, Dict, List + + +def generate_input_schema(parameters: Dict[str, Any]) -> Dict[str, Any]: + """ + 从查询配置的参数定义生成 MCP 工具的 inputSchema + + 此函数会保留完整的 JSON Schema 信息,包括: + - type: Schema 类型(通常是 "object") + - required: 必填字段列表 + - properties: 属性定义(包括每个属性的 type, description, format, examples 等) + - description: Schema 的整体描述(如果有) + - 以及其他任何 JSON Schema 标准字段 + + 此函数还会自动添加以下字段(如果原始 parameters 中未定义): + - targetDatabaseName: 目标数据库名称(非必填,默认为空字符串) + + Args: + parameters: 查询配置中的参数定义字典,应该是一个完整的 JSON Schema 对象 + + Returns: + Dict[str, Any]: 符合 JSON Schema 规范的 inputSchema 对象 + + Example: + >>> params = { + ... "type": "object", + ... "required": ["userId", "startTime"], + ... "properties": { + ... "userId": { + ... "type": "integer", + ... "description": "用户的唯一标识符", + ... "examples": [10086] + ... }, + ... "startTime": { + ... "type": "string", + ... "format": "date-time", + ... "description": "查询的起始时间", + ... "examples": ["2023-01-01 00:00:00"] + ... } + ... } + ... } + >>> schema = generate_input_schema(params) + >>> # schema 将包含所有原始信息,包括 format 和 examples + >>> # 同时会自动添加 targetDatabaseName 字段 + """ + # 如果 parameters 本身就是一个完整的 JSON Schema 对象,直接使用 + # 但确保至少包含 type 和 properties + if not parameters: + # 如果 parameters 为空,返回一个空的 object schema + return { + "type": "object", + "properties": {}, + "required": [] + } + + # 深拷贝 parameters 以避免修改原始数据 + input_schema = dict(parameters) + + # 确保必需的字段存在 + if "type" not in input_schema: + input_schema["type"] = "object" + + if "properties" not in input_schema: + input_schema["properties"] = {} + + if "required" not in input_schema: + input_schema["required"] = [] + + # 添加 targetDatabaseName 字段(如果不存在) + if "targetDatabaseName" not in input_schema["properties"]: + input_schema["properties"]["targetDatabaseName"] = { + "type": "string", + "description": "目标数据库名称", + "default": "" + } + + # 保留所有其他字段,如 description, examples, format 等 + # JSON Schema 标准支持的字段都会被保留: + # - additionalProperties + # - patternProperties + # - minProperties / maxProperties + # - dependencies + # - 等等 + + return input_schema + + +def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]: + """ + 验证 inputSchema 是否符合基本的 JSON Schema 规范 + + Args: + schema: 要验证的 schema 对象 + + Returns: + tuple[bool, str]: (是否有效, 错误消息或成功消息) + + Example: + >>> schema = {"type": "object", "properties": {"id": {"type": "string"}}} + >>> is_valid, msg = validate_input_schema(schema) + >>> print(is_valid, msg) + True, "Schema 验证通过" + """ + if not isinstance(schema, dict): + return False, "Schema 必须是一个字典对象" + + if schema.get("type") != "object": + return False, "Schema 的 type 字段必须是 'object'" + + if "properties" not in schema: + return False, "Schema 必须包含 properties 字段" + + if not isinstance(schema.get("properties"), dict): + return False, "Schema 的 properties 字段必须是一个字典对象" + + # 验证 required 字段(如果存在) + if "required" in schema: + required = schema["required"] + if not isinstance(required, list): + return False, "Schema 的 required 字段必须是一个列表" + + # 验证所有 required 的字段都在 properties 中定义 + properties = schema["properties"] + for field in required: + if field not in properties: + return False, f"必填字段 '{field}' 未在 properties 中定义" + + # 验证 properties 中每个字段的定义 + for prop_name, prop_def in schema["properties"].items(): + if not isinstance(prop_def, dict): + return False, f"属性 '{prop_name}' 的定义必须是一个字典对象" + + if "type" not in prop_def: + return False, f"属性 '{prop_name}' 必须包含 type 字段" + + return True, "Schema 验证通过" + diff --git a/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/uv.lock b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/uv.lock new file mode 100644 index 0000000..c541de4 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/uv.lock @@ -0,0 +1,497 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/aff/07c09a53a08bc/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/1f0/2e8b43a8fbbc3/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/82a/8d0b81e318cc5/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/028/7e96f4d26d414/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/16d/5969b87f0859e/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/adc/f7e2a1fb3b36a/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/47c/09d31ccf2acf0/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/0f2/12c2744a9bb6d/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/e7b/8232224eba16f/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/9b9/f285302c6e306/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/086/95f5cb7ed6e05/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/4f1/d9991f5acc0ca/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/4e3/5b956cf45792e/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/63c/f8bbe7522de3b/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/6e3/4463af53fd2ab/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/2d4/00746a40668fc/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/75e/98c5f16b0f35b/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/d90/9fcccc110f8c7/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/9b1/ed0127459a660/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/0ac/1c9fe3c0afad2/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/795/dafcc9c04ed0c/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/771/a87f49d9defaf/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/e4a/9655ce0da0c0b/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/3fb/a0169e345c717/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/b54/0987f239e7456/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/988/02fee3a11ee76/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, +] + +[[package]] +name = "lzwcai-mcp-sqlexecutor" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "mcp", extra = ["cli"] }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.10.1" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/cb0/a2b4aa34f932c/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/873/27c59b172c501/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147" }, +] + +[[package]] +name = "mcp" +version = "1.10.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/aaa/0957d8307feef/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/4d0/8301aefe906dc/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/bb4/13d29f5eea38f/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/840/08a41e51615a4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, +] + +[[package]] +name = "pydantic" +version = "2.12.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/7b8/fa15b831a4bbd/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/25f/f718ee909acd8/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/70e/47929a9d4a190/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/85e/050ad9e5f6fe1/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/e73/93f1d64792763/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/94d/ab0940b0d1fb2/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/de7/c42f897e689ee/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/664/b319919326227/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d95/b253b88f7d308/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a13/51f5bbdbbabc6/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/1af/fa4798520b148/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/7b7/4e18052fea4aa/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/285/b643d75c0e30a/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/f52/679ff4218d713/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ecd/e6dedd6fff127/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d08/1a1f3800f0540/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/f8e/49c9c364a7edc/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ed9/7fd56a561f5eb/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a87/0c307bf1ee91f/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d25/e97bc1f5f8f79/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d40/5d14bea042f16/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/19f/3684868309db5/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/e92/05d97ed08a82e/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/82d/f1f432b37d832/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/fc3/b4cc4539e055c/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/b1e/b1754fce47c63/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/e6a/b5ab30ef325b4/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/31a/41030b1d9ca49/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a44/ac1738591472c/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d72/f2b5e6e82ab8f/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/c4d/1e854aaf04448/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/b56/8af94267729d7/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/6d5/5fb8b1e8929b3/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/5b6/6584e549e2e32/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/557/a0aab88664cc5/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/3f1/ea6f48a045745/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/6c1/fe4c5404c448b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/523/e7da4d43b113b/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/572/9225de81fb65b/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/de2/cfbb09e88f0f7/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d34/f950ae05a83e0/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/d0e/87a1c7d33593b/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/fe2/cea3413b9530d/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/636/cb2477cec7f89/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/865/40386c03d588b/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/a8a/6399716257f45/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/31f/23644fe2602f8/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/8dd/0cab45b8e2306/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/8a6/2d3a8335e0658/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/44a/efc3142c5b842/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/381/329a9f99628c9/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/73f/f50c7c0c1c77c/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/76b/c51fe2e57d2b1/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/26a/1c73171d10b7a/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/e4b/9fcfbc0216338/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/144/1811a96eadca9/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/552/66dafa22e672f/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d78/827d7ac08627e/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ae9/2443798a40a92/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/c46/c9dd2403b66a2/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/2ef/e4eb1d01b7f5f/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/15d/3b4d83582d10c/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/4ed/2e16abbc982a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a75/f305c9b013289/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/67c/e762070474588/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/9d9/92ac10eb86d9b/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/4f7/5e4bd8ab8db62/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/f90/25faafc62ed0b/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ed1/0dc32829e7d22/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/920/22bbbad0d4426/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/471/62fdab9407ec3/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/fb8/9bec23fddc489/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/e48/af21883ded2b3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/6f5/b7bd8e219ed50/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/08f/1e20bccf73b08/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/0dc/5dceeaefcc96d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d76/f9cc8665acdc0/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/134/fae0e36022eda/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/eb1/1a4f1b2b63337/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/13e/608ac9f50a0ed/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/dd2/135527aa40f06/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/302/0724ade63fe32/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/8ee/50c3e41739886/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/acb/9aafccaae278f/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/b7f/b801aa7f845dd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/fe0/dd05afb46597b/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/b6d/fb0e058adb12d/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ed0/90ccd235f6fa8/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/bf8/76e79763eecf3/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/12e/d005216a51b1d/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ee4/308f409a40e50/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/0b0/8d152555acf1f/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/dce/51c828941973a/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/c14/76d6f29eb81aa/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/3ce/0cac322b0d69b/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/dfb/fac137d2a3d07/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a6e/57b0abfe7cc51/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/faf/8d146f3d476ab/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/ba8/1d2b56b6d4911/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/84f/7d509870098de/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/a9e/960fc78fecd11/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/62f/85b665cedab1a/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/fed/467af29776f65/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/f27/29615f9d430af/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/1b2/07d881a9aef7b/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/639/fd5efec029f99/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/fec/c80cb2a90e28a/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/42a/89282d711711d/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/cf9/931f14223de59/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/f39/f58a27cc6e59f/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/d5f/a0ee122dc09e2/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a" }, + { url = "http://devpi.iepai.fun/root/pypi/+f/656/7d2bb951e2123/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/8db/ca0739d487e5b/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/7ec/fff8f2fd72616/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/f43/24edc670a0f49/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/2f6/da418d1f1e0fd/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/ccd/60b5765ebb358/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/16b/7cbfddbcd4eac/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/7e8/cee469a8ab235/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/076/4ca97b0975825/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/9ad/824308ded0ad0/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/755/e7e19670ffad8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/0ce/a48d173cc12fa/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/f0f/a19c6845758ab/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/ba5/61c48a67c5958/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/4ed/1cacbdc298c22/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "http://devpi.iepai.fun/lzwc/dev/+simple/" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "http://devpi.iepai.fun/root/pypi/+f/411/5c8add6d3fd53/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13" } +wheels = [ + { url = "http://devpi.iepai.fun/root/pypi/+f/913/b2b8867234373/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c" }, +] diff --git a/lzwcai_mcp_sqlexecutor/main.py b/lzwcai_mcp_sqlexecutor/main.py new file mode 100644 index 0000000..8a0eb27 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/main.py @@ -0,0 +1,9 @@ +""" +Entry point for lzwcai-mcp-sqlexecutor +Runs the MCP server for SQL query execution +""" + +if __name__ == "__main__": + # Import and run the actual MCP server + from lzwcai_mcp_sqlexecutor.main import main + main() diff --git a/lzwcai_mcp_sqlexecutor/pyproject.toml b/lzwcai_mcp_sqlexecutor/pyproject.toml new file mode 100644 index 0000000..8ed0806 --- /dev/null +++ b/lzwcai_mcp_sqlexecutor/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-mcp-sqlexecutor" +version = "0.1.8" +description = "MCP server for executing business SQL queries with dynamic tool generation" +readme = "README.md" +requires-python = ">=3.13" +license = {text = "MIT"} +authors = [ + {name = "lzwcai", email = "your-email@example.com"}, +] +keywords = ["mcp", "sql", "executor", "server"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.10.1", + "pypinyin>=0.53.0", +] + +[project.scripts] +lzwcai-mcp-sqlexecutor = "lzwcai_mcp_sqlexecutor.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_mcp_sqlexecutor"] + +[tool.hatch.build.targets.wheel.force-include] +"lzwcai_mcp_sqlexecutor/businessQueries.json" = "lzwcai_mcp_sqlexecutor/businessQueries.json"