From ec7e7fd7dcef395038b8cd3bcea08b177a23a124 Mon Sep 17 00:00:00 2001 From: yuanzhipeng <2501363769@qq.com> Date: Tue, 16 Dec 2025 17:52:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(lzwcai-demp-tool-server-dify-to-mcp):=20?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20Dify=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 Dify 到 MCP 的集成工具,支持通过 Dify API 将模型部署到 MCP 平台并进行推理。 该模块包含完整的服务器实现、依赖配置和命令行启动脚本。 主要功能: - 支持 Workflow 和 Completion 模式的调用 - 自动翻译工具名称为驼峰命名格式 - 提供文件上传与任务停止接口 - 兼容流式与非流式响应处理 --- README.md | 68 ++ .../PKG-INFO | 12 + .../README.md | 37 + .../PKG-INFO | 12 + .../SOURCES.txt | 23 + .../dependency_links.txt | 1 + .../entry_points.txt | 2 + .../requires.txt | 7 + .../top_level.txt | 1 + .../main.py | 61 ++ .../pyproject.toml | 32 + .../setup.cfg | 4 + .../src/__init__.py | 0 .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 265 bytes .../__pycache__/create_mcp.cpython-312.pyc | Bin 0 -> 10218 bytes .../src/chat/__init__.py | 0 .../chat/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 270 bytes .../__pycache__/chat_server.cpython-312.pyc | Bin 0 -> 843 bytes .../src/chat/chat_server.py | 7 + .../completion_server.cpython-312.pyc | Bin 0 -> 8246 bytes .../src/completion/completion_server.py | 212 ++++ .../src/completion/test.py | 104 ++ .../src/core/__init__.py | 0 .../src/core/core_server.py | 172 ++++ .../src/create_mcp.py | 318 ++++++ .../src/create_mcp_util.py | 373 +++++++ .../__pycache__/task_instance.cpython-312.pyc | Bin 0 -> 2038 bytes .../src/difyTaskCall/task_instance.py | 53 + .../tool_translation.cpython-312.pyc | Bin 0 -> 5394 bytes .../__pycache__/translator.cpython-312.pyc | Bin 0 -> 2582 bytes .../src/utils/tool_translation.py | 153 +++ .../src/utils/translator.py | 64 ++ .../src/workflow/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 274 bytes .../workflow_server.cpython-312.pyc | Bin 0 -> 7404 bytes .../src/workflow/workflow_server.py | 175 ++++ .../PKG-INFO | 12 + .../README.md | 0 .../dify-api.md | 267 +++++ .../logs/.gitkeep | 2 + ...wcai_demp_tool_server_dify_to_mcp_test.log | 0 .../PKG-INFO | 12 + .../SOURCES.txt | 27 + .../dependency_links.txt | 1 + .../entry_points.txt | 2 + .../requires.txt | 7 + .../top_level.txt | 1 + .../main.py | 86 ++ .../pyproject.toml | 32 + .../setup.cfg | 4 + .../src/__init__.py | 0 .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 282 bytes .../__pycache__/create_mcp.cpython-312.pyc | Bin 0 -> 9139 bytes .../create_mcp_utils.cpython-312.pyc | Bin 0 -> 12233 bytes .../src/chat/__init__.py | 0 .../chat/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 287 bytes .../__pycache__/chat_server.cpython-312.pyc | Bin 0 -> 860 bytes .../src/chat/chat_server.py | 7 + .../completion_server.cpython-312.pyc | Bin 0 -> 7924 bytes .../src/completion/completion_server.py | 203 ++++ .../src/completion/test.py | 104 ++ .../src/core/__init__.py | 0 .../src/core/core_server.py | 213 ++++ .../src/create_mcp.py | 233 +++++ .../src/create_mcp_update.py | 320 ++++++ .../src/create_mcp_utils.py | 326 ++++++ .../__pycache__/task_instance.cpython-312.pyc | Bin 0 -> 2055 bytes .../src/difyTaskCall/task_instance.py | 53 + .../dify_workflow_schema.cpython-312.pyc | Bin 0 -> 8572 bytes .../__pycache__/logger_config.cpython-312.pyc | Bin 0 -> 19670 bytes .../__pycache__/upload_file.cpython-312.pyc | Bin 0 -> 19696 bytes .../src/utils/dify_workflow_schema.py | 399 ++++++++ .../src/utils/logger_config.py | 552 ++++++++++ .../src/utils/tool_translation.py | 153 +++ .../src/utils/translator.py | 64 ++ .../src/utils/upload_file.py | 461 +++++++++ .../src/workflow/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 291 bytes .../workflow_server.cpython-312.pyc | Bin 0 -> 13560 bytes .../src/workflow/workflow_server.py | 315 ++++++ lzwcai_mcp_api_converter/PKG-INFO | 12 + .../PKG-INFO | 12 + .../SOURCES.txt | 25 + .../dependency_links.txt | 1 + .../entry_points.txt | 2 + .../requires.txt | 6 + .../top_level.txt | 1 + .../lzwcai_mcp_api_converter/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 239 bytes .../lzwcai_mcp_api_converter/src/__init__.py | 0 .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 243 bytes .../__pycache__/create_mcp.cpython-312.pyc | Bin 0 -> 31996 bytes .../src/api_config.json | 1 + .../src/business/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 252 bytes .../__pycache__/business_util.cpython-312.pyc | Bin 0 -> 24998 bytes .../get_business_api.cpython-312.pyc | Bin 0 -> 8223 bytes .../src/business/business_util.py | 712 +++++++++++++ .../src/business/get_business_api.py | 204 ++++ .../src/core/__init__.py | 0 .../core/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 248 bytes .../api_auth_service.cpython-312.pyc | Bin 0 -> 42788 bytes .../core/__pycache__/api_base.cpython-312.pyc | Bin 0 -> 22181 bytes .../__pycache__/core_server.cpython-312.pyc | Bin 0 -> 35070 bytes .../core/__pycache__/get_auth.cpython-312.pyc | Bin 0 -> 12095 bytes .../__pycache__/plugin_base.cpython-312.pyc | Bin 0 -> 1133 bytes .../src/core/api_auth_service.py | 966 ++++++++++++++++++ .../src/core/api_base.py | 610 +++++++++++ .../src/core/core_server.py | 867 ++++++++++++++++ .../src/core/get_auth.py | 297 ++++++ .../src/core/plugin_base.py | 22 + .../src/create_mcp.py | 788 ++++++++++++++ .../src/util/__init__.py | 0 .../util/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 248 bytes .../__pycache__/logger_config.cpython-312.pyc | Bin 0 -> 19603 bytes .../__pycache__/nested_value.cpython-312.pyc | Bin 0 -> 5863 bytes .../src/util/api_helper.py | 412 ++++++++ .../src/util/logger_config.py | 554 ++++++++++ .../src/util/nested_value.py | 168 +++ lzwcai_mcp_api_converter/main.py | 11 + lzwcai_mcp_api_converter/pyproject.toml | 28 + lzwcai_mcp_api_converter/setup.cfg | 4 + lzwcai_mcp_iot/IoT设备工具说明.md | 140 +++ lzwcai_mcp_iot/PKG-INFO | 22 + lzwcai_mcp_iot/README.md | 200 ++++ .../lzwcai_mcp_iot.egg-info/PKG-INFO | 223 ++++ .../lzwcai_mcp_iot.egg-info/SOURCES.txt | 19 + .../dependency_links.txt | 1 + .../lzwcai_mcp_iot.egg-info/entry_points.txt | 2 + .../lzwcai_mcp_iot.egg-info/requires.txt | 9 + .../lzwcai_mcp_iot.egg-info/top_level.txt | 1 + lzwcai_mcp_iot/lzwcai_mcp_iot.log | 159 +++ lzwcai_mcp_iot/lzwcai_mcp_iot/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 219 bytes .../__pycache__/config.cpython-312.pyc | Bin 0 -> 3664 bytes .../iot_device_main.cpython-312.pyc | Bin 0 -> 34766 bytes .../iot_device_tool.cpython-312.pyc | Bin 0 -> 17127 bytes lzwcai_mcp_iot/lzwcai_mcp_iot/apimock.json | 44 + lzwcai_mcp_iot/lzwcai_mcp_iot/config.py | 99 ++ .../lzwcai_mcp_iot/iot_device_tool.py | 410 ++++++++ lzwcai_mcp_iot/lzwcai_mcp_iot/src/__init__.py | 0 .../src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 223 bytes .../device_operations.cpython-312.pyc | Bin 0 -> 26507 bytes ...evice_results_pretreatment.cpython-312.pyc | Bin 0 -> 24910 bytes .../src/__pycache__/init_mcp.cpython-312.pyc | Bin 0 -> 4668 bytes .../iot_device_dicts_prompt.cpython-312.pyc | Bin 0 -> 25831 bytes .../__pycache__/logger_config.cpython-312.pyc | Bin 0 -> 18212 bytes .../vector_service.cpython-312.pyc | Bin 0 -> 22178 bytes .../lzwcai_mcp_iot/src/device_operations.py | 751 ++++++++++++++ .../src/device_results_pretreatment.py | 659 ++++++++++++ lzwcai_mcp_iot/lzwcai_mcp_iot/src/init_mcp.py | 131 +++ .../src/iot_device_dicts_prompt.py | 216 ++++ .../lzwcai_mcp_iot/src/logger_config.py | 558 ++++++++++ .../lzwcai_mcp_iot/src/vector_service.py | 733 +++++++++++++ lzwcai_mcp_iot/main.py | 17 + lzwcai_mcp_iot/pyproject.toml | 63 ++ lzwcai_mcp_iot/setup.cfg | 4 + lzwcai_mcp_sqlexecutor/README.md | 138 +++ .../lzwcai_mcp_sqlexecutor/.gitignore | 10 + .../lzwcai_mcp_sqlexecutor/.python-version | 1 + .../lzwcai_mcp_sqlexecutor/README.md | 154 +++ .../lzwcai_mcp_sqlexecutor/__init__.py | 9 + .../businessQueries.json | 1 + .../logs/lzwcai_mcp_sqlexecutor.log | 51 + .../logs/lzwcai_mcp_sqlexecutor_daily.log | 51 + .../logs/lzwcai_mcp_sqlexecutor_error.log | 0 .../logs/mcp_services.log | 36 + .../lzwcai_mcp_sqlexecutor/main.py | 358 +++++++ .../lzwcai_mcp_sqlexecutor/pyproject.toml | 35 + .../lzwcai_mcp_sqlexecutor/utils/__init__.py | 24 + .../utils/api_client.py | 303 ++++++ .../utils/env_config.py | 88 ++ .../utils/json_helper.py | 60 ++ .../utils/logger_config.py | 489 +++++++++ .../utils/name_helper.py | 41 + .../utils/schema_helper.py | 142 +++ .../lzwcai_mcp_sqlexecutor/uv.lock | 497 +++++++++ lzwcai_mcp_sqlexecutor/main.py | 9 + lzwcai_mcp_sqlexecutor/pyproject.toml | 35 + 179 files changed, 18443 insertions(+) create mode 100644 README.md create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/PKG-INFO create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/README.md create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/main.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/pyproject.toml create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/setup.cfg create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/__pycache__/create_mcp.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/__pycache__/chat_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/chat/chat_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/__pycache__/completion_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/completion_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/completion/test.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/core/core_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/create_mcp_util.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/difyTaskCall/task_instance.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/tool_translation.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/__pycache__/translator.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/tool_translation.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/utils/translator.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/__pycache__/workflow_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp/lzwcai_demp_tool_server_dify_to_mcp/src/workflow/workflow_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/PKG-INFO create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/README.md create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/dify-api.md create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/logs/.gitkeep create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/main.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/pyproject.toml create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/setup.cfg create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/__pycache__/create_mcp_utils.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/__pycache__/chat_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/chat/chat_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/__pycache__/completion_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/completion_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/completion/test.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/core/core_server.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_update.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/create_mcp_utils.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/__pycache__/task_instance.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/difyTaskCall/task_instance.py create mode 100644 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 create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/logger_config.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/__pycache__/upload_file.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/dify_workflow_schema.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/logger_config.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/tool_translation.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/translator.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/utils/upload_file.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__init__.py create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/__pycache__/workflow_server.cpython-312.pyc create mode 100644 lzwcai_demp_tool_server_dify_to_mcp_test/lzwcai_demp_tool_server_dify_to_mcp_test/src/workflow/workflow_server.py create mode 100644 lzwcai_mcp_api_converter/PKG-INFO create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/PKG-INFO create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/SOURCES.txt create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/dependency_links.txt create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/entry_points.txt create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/requires.txt create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.egg-info/top_level.txt create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__init__.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__init__.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/create_mcp.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/api_config.json create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__init__.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/business_util.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/get_business_api.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__init__.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/plugin_base.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_auth_service.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/api_base.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/core_server.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/get_auth.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/plugin_base.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/create_mcp.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__init__.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/api_helper.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/logger_config.py create mode 100644 lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/nested_value.py create mode 100644 lzwcai_mcp_api_converter/main.py create mode 100644 lzwcai_mcp_api_converter/pyproject.toml create mode 100644 lzwcai_mcp_api_converter/setup.cfg create mode 100644 lzwcai_mcp_iot/IoT设备工具说明.md create mode 100644 lzwcai_mcp_iot/PKG-INFO create mode 100644 lzwcai_mcp_iot/README.md create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/PKG-INFO create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/SOURCES.txt create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/dependency_links.txt create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/entry_points.txt create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/requires.txt create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.egg-info/top_level.txt create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot.log create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/__init__.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/config.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_main.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/__pycache__/iot_device_tool.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/apimock.json create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/config.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/iot_device_tool.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__init__.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_operations.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/device_results_pretreatment.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/init_mcp.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/iot_device_dicts_prompt.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/logger_config.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/__pycache__/vector_service.cpython-312.pyc create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_operations.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/device_results_pretreatment.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/init_mcp.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/iot_device_dicts_prompt.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/logger_config.py create mode 100644 lzwcai_mcp_iot/lzwcai_mcp_iot/src/vector_service.py create mode 100644 lzwcai_mcp_iot/main.py create mode 100644 lzwcai_mcp_iot/pyproject.toml create mode 100644 lzwcai_mcp_iot/setup.cfg create mode 100644 lzwcai_mcp_sqlexecutor/README.md create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.gitignore create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/.python-version create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/README.md create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/__init__.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/businessQueries.json create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor.log create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_daily.log create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/lzwcai_mcp_sqlexecutor_error.log create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/logs/mcp_services.log create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/main.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/pyproject.toml create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/__init__.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/api_client.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/env_config.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/json_helper.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/logger_config.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/name_helper.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/utils/schema_helper.py create mode 100644 lzwcai_mcp_sqlexecutor/lzwcai_mcp_sqlexecutor/uv.lock create mode 100644 lzwcai_mcp_sqlexecutor/main.py create mode 100644 lzwcai_mcp_sqlexecutor/pyproject.toml 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 0000000000000000000000000000000000000000..c0b589d82385d341baaca8fc0a2f6b01f3657868 GIT binary patch literal 265 zcmX@j%ge<81dHv{GC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!TH$IHQ<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKjPf5)!h%d>{&xwcV zj8Dl-s{{(f0}X@f#;>NhC^;rRJ~J<~BtBlRpz;@oO>TZlX-=wL5i8K)j6hrrVtiy~ KWMnL22C@Jc^;0td literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e46ec7fa30fff71ec32f2dbed799df9e63e9f76a GIT binary patch literal 10218 zcmbt4YjhLWnRhfJjo#MFZ(|JD!2;XZ=2;&?v4Nx*aM(c7#!IRdn!(8EVP+%)+XIW&%%7az*g+B0~O*AKGKeRi?M=2pJ{<5ttC^R^LoFL%((1 zI{Ie2S^DO>Ie6Yz~`e6KpP%~5hC#Cjpu-Od~$u8)UU!)EIY@j-`(Yc z=_u*05IlnUDAQdDqg9^Eq3&P_>8|RY(_IZvyI{%pnG4?@u}YjHR-XeLe7bAIn%;G4 zy|4rfZKU~v6-HVh&h6b;h@sZCF$J4gCz^XVKaNojpj|q_ew5R2yB7kje|j8KYZr<{ zuiy|D2~0l^E2C0={&9R5?>mn}u@AI#&)!10F$m5AZkmK*!3Ar$L@X2Bd+G_1jId5i zG;SDjfH}Z;OYR`SE-$07?H%j9I>p@;jRkiF1O8Yr8hNGEhI5?y&HTL{ykHbeg1Lt|Z~0nWYXNneVDBM9(NCH4j&nIq z=op%?Yiz(<+pKNG>+Fh`0%Uz~*MXWX>$cuJb8_m{vr})3-FfBYt&jc_*4!e9azF~w z9qTtYHtzJxVq;ti#jVh-ee1@lpS*wTZ1TBhH-A~RI~MC}Te76(8?8+(D^@kNv^2FY zYg^jf+_Gd}iTf2a=KH`^pWT^;x zOAaXdEsc%gs311ti1DQW!TG6#Z;qXv`f%{pFF%-i>$RHt{ZVPpu26J;Lrq|}KL%wq z+!qq@bb1w|*63v=571CJ#8J!zIGkk1G4lf}d|-Dp7!YNzUNHhseSS$66??nX6Az1# z*jD_M6sunle9%f^3MOk)^F?O4D3v z#^||fb7X9l8AnsrMD%6%iC%BWT8ZAGA-FSkXT~-!<6JWCX#P7m#gs@##!;a-TC)~P z#{hKf#Oj)~RHZFd6PD_6u6i2SA%UD)$2vi|q_UrdN}=p)(lWp%*xB^JVe9O0)CozQ zz`&dB%}G5Dq&>9XEbsv)!6aEh*TW1jF;LIylVD<~o#>;PK8Z~*hz0bivDezwH>E!d zd_b4b0beYC-6WJidfL4Px_62Z1>Oe&ukav+J`hyJ9F&7OB`j3} zzG6aQ^P#YLIcYvpSb#+xwsCl-Wt?UWJW@5ntUY4Phtz@*P{IoV)B->G`%omw4NJv@ zr7F4pI&V2PaCl&xugSPeho3z4WXhP~i^loVjJqXe#MZ>g}U<0=_$IMtBiGrT={ z2pY#-tttM#ff!vw3(qY5gr9reT`}ZM@z~|y;e+G+yo{%8xa(9`%9P<9<9u1hQw>#% zd+6IE!EwvNac-eh1FR@TR36}D=j0=hHwVy)8$*xF->45YV=4qDNO}mEhTP$TP0ZB@ z9EiNZfK3qCJ?jY>Vx%3hyy-~fP0D&`d2tINaO~g!p$rXh1;>Hw{R+jdjUuT1^a(DZ zrxXkcgTT=mpU?|>YQN_R2;~tf7|-Yuj9|igf?tggbHcEXNY??wOriN!if=}oNE&Ea zD6RPgv@C-43>?r|sQvmYsl5Z#Y?NBv3^nLsry~~q9h!q*NizvF9h7ERU;tB{EqFFK zW5FP}=-Ul-PY)v$pWz0KLJ6JuX233?BbQ<+t=GN>Owg|kOm6v483DNhvrX|Ta7_}X zUJJY)d4x)#N)z4zbHXgl!4n`kg=&qbgb|=~&u|Z~Tcify53OXR^f8EiX0;yQ$An>4 zerkky8b7jbh&j;)qtxImdQlrZ@+5feTrqGqPi*BteSqqiAO9U7no+!`{{85!q3_Qa z+gooQ1zUbI_1^9OK0Woy+hEJ5PX6#OAD+1V(fOMve|qEM`<>gK>8jZl*e!KED+7?+l6k z=9u_WtdSB_3}DiOksk0g{X4~wSEqUj=rBo(u|OY1F@dS{_elP}-MMijihv3m2#6tI zK;fgRr(B4kaAG7LRxG=MA<)Zxp{QSoqbj)hqhmKd_!T%})H74RriM=5`uPR$d5tPI zXmJy*p=&HHhhCttsS7j1Tmv$o4~D^oPj>+jw?akwwi^XKH@ z-afSy@wH#v*$1zl;4bijSU^x%Xji!2J`qZ6Raj_L*VYDe*$JB)3WkF*`3`EzJE&$A zy%1!;&fvy1Hly zeKI}Oij}UUF8~otut!=09LuN}@Kom>)h3n#A#faJ-y`fv&jI#!?DhjVaY=IDL)@hq zTXDu+oN>DE^PJm|+;rbcJf%a2Cfo~?n=`z7$UniCUUilXB_^D;)8u)^v?kS_@hli^ zPkWw7ZMa%KXGHu-?;E{84o#FV9&1mRH>El=<<%qo>GDNTt9oLzKV8+D+LAR8N5#mF zD@ESxRkfJ~jblCO1uO3A+10Kg%gE-m!#mcPcC5-8iD%BJIqhDQ>D- zHjP@-p2o3o+OzuN3s*c(1DdO9hKt(p4@0 zKcWLyoDDGdId$5cuQx9Lcu%_a*-X=lj}NBno`*G9Py!R0-!N*uTV$+tjXP?xCB#!X z(wlbIk9A+<(~WCCF2B;aNuw~d`Fc%#X5l|8H5-7pH6`Po+R+_pPwRyhX-^w?r*$op zwae1A%P#mn_D|G)YiLWRw03mwmC}0Pqpl&dc=?5%^x`#{y5*Vr<#$c`^-RTxWps18 ztmQ)Em9n*22U*W-XC49>$XaGAli!9bp4C}%!IU1~Mk{Rg;KAbuM|#KV{!rAMaa4|U zrX33>9ZhLR)7Zct98YG$Iy_6+gF0d@9p_3^H_0H0vA7foz!}Xy=&Bxp-*hVo^CrmA z41-TJKw^1>*&J!`lxEtwd^KMg&=s`j!|Hr9cuKLnPn2NJ<-9fUn(~%7!D-O-8ZbhbkI~*EPt2(SdeMP?*Zx zHY32S7^z$A?+YsST&>TC9pr*<)Qxh^fksE4Dp6V5fV7^%0=1gd35B9ZZ7D0rHkt_RkfoprmL1_suqHJx7j~u4JLEe2{g0S#Oh3L{;Q0K|K*P2 zj#7qh56n21O^^fVmS|623g662VSxk{V&;0#kWdHy{?8J++0t1B@e5l(X%<<`M=TmI zWVUz(;t~jUXZ1`OL8p19=UGi!KwkAAxo(i$d?cerF*0+)3i4&39ys;x2lx}k9BLio zA$~S*_FRrLptqQi9k3b$Ch*CP|G0b$;Ip4MY1|e_bR$T#aT|#hYl{HU6!|gvw|tEE zGDD@^5+4E#Ri-Z-0V!35R`pbGzx&?JU%pHmRY9F`Wl=oZYI4Zb)uU*5*7FA<-##Pc zkLeBSwcq;S^v$=>)Q4HvcY!tu+>RYj51$dDL>M6-mFL0~qA;lVrDNZd*=GzsF z1m)eL0PdJS<_F(39s`FRBpW?r%?aKMUHk3NMfOCZ`y(|dzil;Mi}V73B}_ttlNS-h zmlSJWwP41SP#B-W?3>+bbL7-Wz#j@xH6k5^ zt~cQ)|1T6&i`*|Gpek|-B*R-${b{~DG7sL-i+0qaaX~A%z%uRjjfz;FU|6#c+oWBip?n#*mTdFdqihyNv+F(9$I-k zF+R6-qNp`f0ovq^#iPM=Me`{>s|O5lCEUf3C-GDcZ#lK)^fUJeV_L@?U!Q7E?S*j$ zyN-9ASw30WkgjaF@WjQvpH#O0-rA965$SqSNy>i1UN*93tahwqyu5kB-ZE}%`Kyek z^5yoXbr$AQOM3;p9!ip+0z#L5x*inJG4nCYG3ziGIf2O~9v}r0A_E}m0vBS!x>v0X zNm+L>g8rxxR1wpQNtQ>RnFI-3H9(e3Fod|Uet?DRB%Q#A3}JqNqjh7*7&g6%*HSvc z6f%b`1NyLa0IsfdkWLZI=W_1BtCo~GWq~Uf3yjG_8z)$yGzc~@{&vX17ir1|pnAc4 z+|pa|2utt@Irp48?a`fgYIR5=;0)~pCP)X&HWwo#9^8WGeDO@C{{ZVPiMIhsDuXvJ zelT_HBZ!6n^5L7R0heP!FtP+nIGQ*A>+tR04Bb9^BA>Qb1y_$qQ5vD*6^xaBYDug@ z=5hBZs|+aYj>)%g7P94*VYM;g58~?DzHhhn+aw+eaSUUFz(>y5oB$ zwI#3uS70<|S&td_O&dQ*&g-?+gu9d6c-s32TtiacSd}+=r3f&s#}T-~EaV+d3|UD| z1mIT+KhpxpJ5n5QHh)BEtR#9OGy8Q6c>$bO#D9r>f$Ue+G~Y#+R9XwulIo%8*Ecsc zH=$7GtGXOR9!@PxXTG7->J% znR{IY1y#E|v#{xE-Qvl*)#mw6uGARVr4vbc?| z?smu>fE|!x05xrk`6ajsiiz-Hd^TqvV0rahqH!=W`e#A@AnUUvXO!e64=Fx8+*Gi> zi-M(71?xE53tx{u5K-h*M_~&uB-qPg*S&EOWa>5SGl+dypq#tWNajS# zoO1-ArN8QN0h?)Eh+;MhR8%p`lvWHMI&~=HC>^$)vW@Kj$)Pt6UEtrd{n9qg>dNe2 z5IqVY6xW?)4@=-EKgOIjpMPqyVQac!>qNt|&;cb(hY|+G7d-T${qI6@sOogvxOGm} z2CxTN12jL#Vmrv1hDAr2LH(PIe?o9%MMop^Yo^0$c%OyZM@u_g?4@Q9D8r=|4(rP; z0KQC^4hMIcv0~lM0Q|Cp!Me-Pv53E1#&lF#FPHOJudxAqdq1}XH-~+C!*-A;BkV|D zGzdb)hsD_LsDM|XEW~gqZGyNn-lG^Kls37aM|}(bfGgrC?@+*auri3 z+T)`a{N5tSAw~`aqlyDBCshU7q*}(qFgJDo?S*ZCa}rvV68H-PRrs5LjZp5d7G&uM z*yx^V%)5>1xo{S=0Kbp{me8-4P^&8L+!^QtUNuiv;p6*)fv6Pm+7wev(>~JM*y#io z$PFEJhLH3oQnTfU!h1C7JmTTml-|Gsr$;@reAHXBcI{f#2pF}~DZL9=;0_TPNBI)M zT_Z6%hn~K|SAJ&nTr;-Z=WHeygq$Y(py{}2sP<&h=LTY0am~2wnz8wh#@0XCYeuUk z>`f{5bB;Gv-*u7l`EVKcCr9znuF?LB8!oQ=#If#E_zI1&ll9+B*MIX9$J(r(_1N$0 z4dv#PZQ4OhrPqx`gO=l#p)F&ki`;JwDa-GT&!~ANqmP;?pU=y7b%7rvC4olYQ0}s32gX>)eC~$Um1|)P9q@U4G*kZV%gfl=h4QjkV^Y|dvkZG7X z0n~&_co(NQXP`}ZyBU&Ivuldt-=knQS3(`HRKp#36UHj|rzz_o3}}i$1@0p-*Vi>&BmDXbs#}?~GybA#C zX>8@#0Qq`B5CyP_(YORt#E^#`rvD&xgUh>2E;t`Ya2N$E9G{uwFyBWb-xmR=)s;jA&MAYx%WNq&;J)5M)II|mz%H;m7H z?sHbh=%CG9nJS$$RisT7BdaD%wMoN0LoqXVy1A68zuQKd+RlA9O%^4MsfO{gMW33A z#vSt}O!Je5PaRbw-x*DeFMoQ%@vWpSV=0@oEJ|AzjW2$3!m=i5yl2?UFs0euEYTS! z8CRNdO)#FUu7YundPc>c_nz%d&uh6y;CmWSifK!>jUqeKjB|ocshx%tBK$)y|9@ZY>Kt?#_2hfYWt#>Cp1Wst|s ZMi|0bQ~g)i@=rPIE1g-Ea20g9{|`llh1~!E literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ab380630858e7ca7f3d35eeadf260027f864bf64 GIT binary patch literal 270 zcmX@j%ge<81dHv{GC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!TI*^RQ<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKjPf5)!h%d>{&xwcV zj8Dl-s{{(f0}X@f#;>NhC^;rMBe5hVK0Y%qvm`!Vub}c5hfQvNN@-52T@fqL^^8DV P3}Sp_W@Kb6Vg|AR=Nwfu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b9ab6033682ee792a1f9ea0f8922069193d5a981 GIT binary patch literal 843 zcmah{v5wO~5S?8+iMdMv2@t6ubhyZU06Kxtfe=kZ9YtC#-c4fST+FV+$x#Z56jwl< z_9PlkH2en@-N`LK5Md;wtC+E42n|f}=FQB`?Ci{bX|?tM<-?=l_$%sfWK`mAN!~`X z1`f|a!3q@n#LGw@QtmrJb)NL^ocn03K|`M@=yS!iqqug4u5wmx-&HPhu4>3@Y29yR z`$uD$JPC*C;}_2}@1?X_%uSRvWE=}Sv4OL53EfV1-a%Nyk}W|XD`m;iVwIJ`IqUh{ zP+O*k;5(UXbu|3ru~e$l>0Ew(`e2ZbyD}WaX7)-4$sk&s1TyT}MLMeZG>E%an>X5Y zV;M~3NdGq?l%B>Snav_mW(tMxM2Em)+w6azu_hP2sy^in}F4~<__g(Aa_MKDreKSWNWQ|1Hi3nA&-6lB;*+D3B zG!e4;7jtsTLyJ6Jk13 z^N4s;h}Ux&Z6l6NjG^yp=uze>0p%C>8(L!lw0wIHVFeq`Yj@9XYyj0qo0Hmjtkya^ r`T?lQY)b6;tSs<%(M#{J?B$`kg{O+j*N|)+#@J6@XRYsmP}KPaJ0H() literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..93fb769d5fbee13f148aa8918dd6c0c50f13f204 GIT binary patch literal 8246 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 u0U3Wl9DL4k)zQjXt~$w8Pi{ '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 0000000000000000000000000000000000000000..12f81e61c9ac53f9ea2cdadbd0b1cec555ebd056 GIT binary patch literal 2038 zcmbVN%WoS+7@vJ5_S#OI7c^Cxt|CEgN^)p|S{XtKX+de#f&v1`iZpC@H;J8fc00Rn z#jI+23QF;~=i$Df4rGgk0rYJyz*MUsW0hv*#RiyC)EdLo$~7>^Mm(31C&7q%lW1Y?8s8lNTf~tn)#}a$OJ&1;&wTdF9A> z0*bm0jVZ%wt#UCCl^|=ehN6xmTpEB}WOL)qAb144cJH4KjU^^~#mJbBw5TGP zW9saTBu09T*~ElH$0WViP|zg>_3ENDB~B>+se!T*)1j$pDlB_K8=R>@9IWE~?=}XK z#)uE8ZW+NtqvmJYj-RFqyn|9)xUIw`XLYkfT<+=iC_9s_o zu4f?4{_Hwj!b)H@mLtnjwbEGHfPeNYjpEn9y+g?~>75i!5t>oy7?WabnpN2tmtrY^ z-;PZ(GWP|O;*uchCBgTvX!CxaaH|zjlDkBqklYY$skcdT(YAW22c3(u8Cc zR_+z9->Prm-+oxgFD>sZt>y18?0h~q^yWzZ*7ElE*Z8XE5SlPTRgT1}JZDJQUSH2& z{c7Y?&3IHX<7oN``*M!_?R)>2V^oY7$`fAih-zFpjgW>yLjJo?>Y*1FR|=n|3ya?t zt}f+2xSxM;CBL>-1&T;1@R(_SS3^_dsy1^9_d32F7PSm(FF0&GfTM4(T`w%$F3i7=3s(;>B*fdX(H{@yt42F_*YnHQLPF4I zb>Y8XpOd3XRQ}7zj~y+6nAFnlAssW)$yU28=nYP#| zC1F^6t+utn6?7po=}{Be2mnOFBNE8+hvYL&!u4~9tWP1b}mbXjJtTd3?3f@?jdT+_1czepy&dffn59H+`*#{8`-D% zw5Qkvy#B1GC*$eKdIA|wVAJ#5oue7gV6MC8%gASuo1SdoL?&=zGZ5PBelh*VFRfj< z)?;@Dvi)Z={bx3go!vMKH{R1WpuXY1SYqh5rXrwyO+_Ab2rFz>IGz!XXNAFxFt{n4 z%(TCfc0Y6he`n6?|LEL@=kSsB9?EzRl{m)F7Xjnukq@IjA8a6#b6Sik5L!Hh_+t@Q z3GIdOVq8?q8brpI#TX_+0;awJwzFL`M82!`M#^cB$utN4q#5poB7cPAZ?-)G_$b>G zQ}WGX#0wH4^R(>z450`bJK(PCr80<@(Ge^o%1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2085820845da82518bef0892fb50ed6124b2facd GIT binary patch literal 5394 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a681ce5b4ca5310270c646e63c4111ae497c10b GIT binary patch literal 2582 zcmahLTW}NC_3rBZ@B<_aj*C!;V_ONbuqiS2gi_g2i3c!*M+STBY?j@%B_m79-4(_f zxv^UUB9LGQf`{M)cZey819qmJ)Wr}o%@3V^bX89WcS*mhm%n04I@6&)J-aKp?v%E7 zM(5sh?s=bckM?&(MP>x+>W*&zArnG>k&(jE=MRr=!C?%sh$VukL*0oELcz%n5_U4E z>C<*-2}B}JJCzqxFx0E)FQ`;^R>NwiNcG&I2bm{x=~(?TmjSqp|CQ6g8Vj6LL|(6n zHJv~m<^jTGUX)2~E<&;d!!%2=VSx)BlZ?&Y13v1|{@t-UzaT{TCXa`uc|XsDBU~*V z3=Bj=3}4HH`#kWA&_duykaPEiSzRxr=N7y55_Y2$Vx72UhT2F7@BCXo9_a)zM>Y#7WKHq-t!9)9fG0o=M1}2{h z#c>@#4g4P6hCs$p9APp}@H&==Lz>6Q14vNVWB^o6foejmmeu|1?lSt2WcB0ZAjuly z#AU=9;|OTlK@DqS&2bHD87F%PVc9Zk9oGzM1=+f?dJDxb5MURK1qAdJeMrX1xc0N+ zDcM_CgSxoxiN!8CT7wYY47Wup=So;;mO*`7&z6?M6}w7-s}*RIVQK7)kS1+GejD+W#MT47Cr5;qlEOU|t-!;tNW zT{md@u?g%edq2^Sj%(UcKjA8m7DEZmPFUHybGhl!%*9ig@z2sH-%8*7bviMVy?iq> zaVj@`BX@nstgtmjXiWDxnQ#aiRA_SMt_`b_*$+;rVk_AM8uxGl6{JHwQM!j~a;{YP z?d`MaYd@*xLe(|7GoOPI*CuCm%wJuTn!N#j$XG93U6UD`%-?lmz<%+*jAd_}R_$O; zh~f|^R@_gIyu0j&sy_44c>e0+&vU<=OeZeo?p{b={wV$K6|j+h@9g4*OS$Rk)ZL$D zhNl&k(WfDuYABh+#gz`HX5Y#E^iFE_R`!!yE>hBQ7>BV`B49ei2Wf$0DSRDFVf?h9%%>nH zN6CW@gP)^W4)c;$4ten&xC$Em;Q+(&Sbp%5A+HU~4-&)JN71JMtgIyjk$wA%%+PRl zVz|i}m52ByXZrj*>01{7UBAqoyL&2o{R}i!`rK&t!WFgPP0lu#MKa`Pqhwhz@FXMV z-iUI%0PaP?ynwZ!O0w_*1jJJe1dHW2DCyuT5RtTU(Iu1bIKxHc_#_R`U1r5`XpVn> z0eF=xsgGb7*Mg*FX@Qo_@e#-iC+Xo74GK!QQjyYLR6Y+9!SQm0@>5mU;~8I5=YZc$ z2RcDrFUJU-!B{^-2i$yYpr>%|VhphZC>F{`3E^;%QY9%i&^-VI z3Y6ug|2Gehna-#X2=bkUw-Ls*kpan6Ksj6uvB)!%Z-xOhgdUb6tL>6!q5MVhmBY!B zcG1}WH$9LR%3H+4RI=oC(fIn3cB76Wmh@=7XJJFt$NdulasB3pI*rYoFf3V--E;j- zv3AE2LO�NZ4K?cgb4|xm(^=?;#WVB_k?xE^Mrsc{K@^)@}c$qUxbRTWU^dzbi)e z?~QFA-TqC<+C;Co&Lvu(o7wqQ<#!s=ZcZ4N%*blLZ`qKvY?#>c2TRq$n)PE{qh05z z#E%vnl@skrhi8UJI_hT{laB4Pfu!T*`;NAxqix=?FR^#Q{_Mn?qOC!+Hh}jw_w~c~ z-QJ|zD{gB3>bZM|f8X`nu6ujM(#|CfDtZby9M8}8Cv7`L>(0NYEcU-50>WOX*mm1C z`^sF|tWhlYim!GhOJ4h<@wI;}Y2@3-d?n=aqIXAg1Nu#wQ-%$umI}?+sI;X_^K~g9 zpUZRrq3OvmkJkaa+QoYHQv=I?k&+&!HOwZZ<8hfx(RIW3h=AA1{Bq4cL5~0;Tm|5< z@kMz0(IEE&Tni`^67duO@QENEAkza>@&H->j2sX3h_Ie0g3zpM67}aAPdAQqh)*A! zw;UQWE|_hJjptmaU822GtlBki-aTaaQ?YHNb)QogHw{@H>FWrJ Iz;3YdPXZiLWB>pF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bfa3884ff75ad8f7df52402d2fc93898fce86796 GIT binary patch literal 274 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 Swf1kZOv|Ove-g~K>Hiz3g1Ha? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..12ecc7cb483d4eded41610bcff6a7f924178217c GIT binary patch literal 282 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2263ac4c1ca037ca5ba0f9e7046205039bd2209 GIT binary patch literal 12233 zcmc&aYg80hnpORxUv@Y07R3fpXnAN1L`9<^fR9lT1s?&i({`2E2y~~aI|^wgWJV(< zsJMxtjTqZRBheViWHu5Xi8=E~&YAg9NE?}2_Z;U8-QfNSK4#A(bIzXq?yaus28m&J ze{7xZTeqsd``z!}`@Qe|)08P%0R7Q1MV2s01jDR3elnDhbNiW|<|~QvwcwFo0pPv&_X%%?FCGlOacbn(*R}W1Aj&Nyul6tvE*rzr zP}kPcZfk?Hu8w9}Hdbw|$~CVmg@5tJHKpsfl;@f&N>}8XS5>WBn`_>>wsI>jbIqme zOQ2d(xjENdhCZ#UTnV5RRqzr@075UzDmFdu(L01a-ai@!jbB$$0HEsuY8~!iUB${`3^sD^bqS0oJX3+- z;Qr5tuD&C*9RBob@Vz%JN?uJl?Vd)u$;~SrjkY?PSK2(3!^taOc2KmFSJ^$R+u3MW zpji%IXz+6QKh(q9F2YMNGB3$4bc^4Kdh~VNMUh_l34)TjmEs34Au=GmmwQP@=N4I; z@RH)UCSY6mt~?ndz9FH(D5OR%NSVQWV8_ zl_!XkifHWh8j;4x{X)3fs`3)`y2yL6CVnp^I?37sg4iYlnqpejUcmn{!IB;NNl^pv zFj|dQ4K%4DG`XjU{df^Z`GxQdw`#l^cE+SIYcXcsNo53U$7T&NskVBrX51WUN{QN1 z7@?M$;?=%PGD^zeh5Aw;lQN<=FydIs1T{4FM7L3*UrU+LDv4A;DTR{%p%;$Vw+5YSzu?72A5Jh~pf~i7yaTa_f{|PJzBXLTwMZ zhuABBf!IS@QamFt46PY1K~3A3I;>pSp}vct^P_c-px?C^xR|M9i*OA!l^U6|DTm!{ zDKZO2W2E=j!*BNh8o?F-g5dr>sg>QsFrdb)Zl|-sy3^4>LqRuCtU0^E!D1xW-wGZ) zB-tN<7J-VPuTFj_C#U zBwSgxh6d*zni2-!Zg$Zuz}IAnN;<0+aE*cSxEh=`%GyY~cR48mxWQ?+2~!}@(&%*4 zRxpmxcv9U)3>oUXwzXDjMLBjhTWu~!k+~FV=HiOg!B5^B`tr^1uXPBf8_zo!VH?mx zGgcQvyBXT%M)oE2&KIH2j!CHSGBp95Ih%De(i#Ul4-KE|1$?m5Lst%my3Pi_>Kgf^ zH+cP+z$zPQw})wp;ugdrz#-UA2zDL@_K^4|%pthXjW!2MM>Q+kYp1b&hPKz=3Nr&n zGV;}d;hr~vVS`7G4!?bBsPF8(1BdRteSGB7rBK)Bp*Kzh4_v`0|BlR+z)xn+9H6mP;ngB@d5{#EC3z8eGN^zjT@u=a{MUlljsWz?<}yWp8mM}B)2$O?6zANlI-;EDG_-3LR*+OZUN zd*)8q=97+4dndFBivylt2L;p`#=dEhm(ME}=MOLUnECrV@{?c{KNr?sSoI=vxy8V% z1b32GIGT1kdBqM;GrSx`npfDJu4eF_+;k($Pq%{fFgCjzOE7EQ$v7LW;@b0x$SuXR zp=lC6!c_68Pmmj4Ni&R-;pNblpJw&AC>w~gi*ed%mIX~AA=gMO&C8KU8F-EOjpdVn zN6ya}~uK_!dy@@kCH zxuc#pMzCNgK7QN_E1FTBq*()6lQGwiSIUaEHSfx^IkrKgdfM*Z*3h!YZgb?ZEzNae zz0vN#XxRd^KEl`Jdff zRqdZ%<;ty`wedbdRLFM80!=TCLPK(m>=8jY$+a?gYLYqejdY=#OZF1FB`^p=!^#GY z$(%ZQR7EVUBEL(m>YeQ?^Uocap2wwDaf)Op?9nbNOLZC7_%?&{*Wq@0_&oICod zN7fu%o)4p84XtVrk#%8+*A8)dMqYIQ{g$#?888 zO6<}d)%oiD4u4IrrZ2m18dpMd({~0;b%Q1#-aI8`C>n;Fzm{89%dOqcO}7S2F9KcZ zPdy;ivAfC6Rd(Z=ON3`8m^SBl|6(2gItH6YRGbQ zo3S1(Y;1)LW->O1%UO0q{jG)D5gb_chko|~~dU}^}OlK(jx z11)}Jp8{_En$WM5D}HCrTd9`)-k?BbTyYNE{;+&Gl;5Z|tK`aWVzX8*Q+~5d33YpP zaHs+dIFx(?c)p8phcgo3?h{#My`=C_{0aew6f80sl*~&w$X2-!p4i3)NO z)ydEsN`Ic3-+l!i2|x$RR{`EO?U0Wkf+l*tuZ}5FqlKaj^#~>~i44HsaAVLOr$9(p?#d zNx!R$zpb0uubau47Y1~T29sy_nzJtfwu;+S%Tr_c5+0Z(C^7gEZVZQ@Ed zbMadO>aByunVfniG}gv2TiHBO&R^B41d8bQ}&(MRA1?tS+`>6^b}lL{?T%7$;Mkbl@XY- zo&x`~w@gosY5?1PJ)ut;H|XbYEV+@#C07LW8wTUjIDOh(oe_*BLD(fV2@eYRW3#gv zDhI|Go`Vw@9+qq->mYaeKG{k}@|W$zPDr!wQ=?3znEksD?xm8jC_EPdIhK!L`UgW&~FVG3I}wBoT89<4&d$4jtdIRG5`G; zDv&{!Gud8nJ-m?Bo>VRrWyB%p4y7Em31>v3Om0((8!!vk#D*2Q5|ko3n;F#t(Khaa z{3p)C<6a>LK2fjDF&4GwJJE24PW(3XPOp$v5E3(zDa2BN3rMX1bANyBT`{5bSlZ*< zo5NSzA*ljjLw#-IG=0Qa9r|qO`d7gt=OI@R4K|TvMdC~;_>v@T0&>Tu87F0fos6T- z(F93290r3x9zGR3_og)b|GOs;83K--p$EYf;`~>%DMVdi`zWXl5PiX*iW3)cE}rZ+ zAPXfJb7UH!zstW7);HW)ILCz3Ct%YB^g@COY&ud4l7Asm3Sh$@T|pFmbu!fP8k(cF zYYa-_;|(lxsJjhT9%r)3L24zF%q#{Fi;~HO8#c1MVeEV}C@#U4mqkK{NDqtgEc5{t zm*LO80tLj`X@olIu4NIIy_i#{`d0O;XOA;>&)rDAq2+$DjjMHWFE#hqws6~C;o`lw z)U87hn;Q-qd`bQkzmChB7)e(LU_s-G{{q zD+!U`S1>3J^JGFHaXlXQ@U<(!-lJkBO-RfZnJtQlNfOu`bGV$>Ma_WVNuuOu*hmU2 z&8K6J5+#`;z*f7nX{V#k!`RRUG*bbMIMmPs1u%RfxJ`q`xVy2_9}qFhly)U@%4VO5 z4;belZz{??1eUGkwruB8QI6&>amjae3BFDJx{RLLz1o4fOU6N0a#b&ImDOBoO+dd5 zpht~>eRL|JH$8#Xr-8Y^tR6!l9}6F0=!UQsvHcO5T~>$ocR_!&%3y~t+J}R&po^0B z;~+##_$e4lA%@{cWgXeDJ1hrrLZXH*O1TgBH{Oebuw1g*k>J^@Xo15St;z|%$-N4% zl9F{RAe?@3NUy?{G{&nMD?m}Zd9pnO*wa4(iI@>b4quicPl0ui03G!q!-)5<{$FKC z*oKLgk1?cS9gK9HMZqiWP7j!t@&<9k3^*5kGC1+p3>Z-SjKzsTI&F;CiPi>t zohzUs`JAA?kk2_Gub@;>SmB~wX-XFMN>f2)$+1_e;MF26EAAn7gOepp2He3-MBOl0 z0uc_;rmTi~i$zN8V|`_1J&lbvrg;K4^}hg|odJd5r6yPTJiVUF&7U?yN_5k$g_Yg0 z{mE57uxKCWS4rvQyD{mcpn#BT8{R4rR$rK3P#;Q6ASC@gjW`gEm`Vsm*F&?2?qDj> zV-+gEq(F2VCiJl=G-|72%!rs8X!alQXU{_sRwj275(iUK2h(Q4@4iN9(vA{Jjds*P zEG{L#Gu8HZZg{?F{<@jlP;+bLw$6mEl%pxWB0qC0b}nbCZCee+orGiGb3tzW(4^v_<}Pd`K{V0d|Bd`$Q{o7pp`I0ET~s2^3K?` zfzu+!R_!mf%`)L0bymdWxdH5kEnCR{1)Q>g)LyW=H+Zrwyy=T~g@w)Hi7XqWBg71+ zV0FM6N+^T|k2!jfsS_PC0kJtda z>qNL2bnL3!5-<4Wv*97)movz1;o@Zs z&8t}2#@Ki9$~wmBaS0jaSnQ}{7NKaj4R(bY3_K^n=T!?#-$4XTz5C_L^ns{ZS%?-T^Sv!0(BPQG>FGm!|vmTer$++Pdfz+5~ zkWa$1zJxRw(yt6E^dP$I2`In=$RzZ}x9k0JXY7H*xhQl-5pi0N-0$I%@&fAo zL1WUWB2$?>lrY0*f2+1#J7`Qf^6J4?+ZABer%gYVbUf*Hilslr(vvxmGOw5HPg!s~ zWl?|1qJfm6+bPBUDaAKt4Wz7SH{4edrnI}}Vn}1(iBIfWeRMUKHm^5sAil7@>@UXn z!Sp%jQctJ$Z1^DUc6woddf`C&!cGGU(S_}EyKfVh@oZlc_v|_@zC55_58clyxSg@M zKV$Jg#*znwMw8yJPitS@xd(Q~eXDL7Gx07nQA(~Mzg^m2vh{Y!_WqLXoOQ=QiT%4- zR9}g|CI5yHO}DD3vU_=#SLA`|FnNLm$kI-g}yc1 z^i=`V>cJ%QO_O;v4!V7mOlXXlOv?APZ#FkGOd(9&4-aCAOd7UoA=#MlfRHJ1Dqeq3 z-`V1yrgx8Z7HpPEZ34yd2MV@&$zs1m?_WS@bBx>%A?Rz&>XIJ>M+_4@)9 zsxL`{@(XRgSV|1nnyb<0#f#%hH_XI3j8gU-Fc1_(m|I$jU# z*WlQZkC_pR0`nqzp9H5J@`WP~!a13649Y0bSG5%%4Y67UF{*=i__%|x*Mah?I4grA zRqSD6CS%Ibhv!gH0L72$rHxL?(?Bm{(9Q@jAHjA(F)AlX^1e(#Djwfb!$T$AfREzmrjlyCLf) zk|l6>1_~CS*k@jx^dRNF6O{3!|K{99V+k61OhJz;rX9t#GjNr&aC@$Dk#ki+UP-Hd zE!*8ckjay9kUoC-Jo8>jtHsnrSxv^Vup=8dTTQ4t+3Oa<66S0U0$Cw*jutDe6i!*! z=Z3~I6$IbOT&ttOKaZtStyb&&%hLz_^q?)nerzVMb&&L<*-;?FwwTC2tR!gKEiBJWb=Ot$@yNGLA z;Sy3XH6*9FNaG}b>n-j+-#?b#YbSZP0fb#SHhTb&?`VICD!U9&DS8ktN zTLG$1c1>#Ku}b4){}-StvMI6av!cNNMbEv5#Vn7^4UEbwUqP~R7-PSAl{J0>LSE+& Dgk9B+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..93da5927c398f619d3ffc222c093804b80d719ef GIT binary patch literal 7924 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
  • mDEy z7A$hksx4^~l>P#Lg5;MzKzSzHZENBCT|eteRPMatyIJ!^&*afW`AgH*PB>lF##v|0 z?bU1J1s@+ttzG{adzGDV-q&wyfoL_g+0ZRB0fk#umD(I_tEpQxB(A9^aDFv~^J`7*ChFG4*1UF_ z`nO#K6uzVxpkP$t|Bu`@1;WeNT=94mvj+#12EonpC{~Z>jX|$pYY~T_D-L{M9)hfR z2$OD1PGEwq8qtLb#(c$!eK^4f_}&-fM9e4RDNL}Jqwx+7t%;c6D(N_obLc)X9}Bfv5pNe2eV=jU3-1+qcM~=9~V< '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 0000000000000000000000000000000000000000..018363aa1140bf84792da17a4dd1058815f4715b GIT binary patch literal 2055 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 CEoPzs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cdef1ad43034ac5128da848b40fcded4966b3783 GIT binary patch literal 19670 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 WT+M18{j>bQyn>RaF~5N4^?w25BwtAY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae002379f647928fc5c5de300f24e95e043ab229 GIT binary patch literal 291 zcmX@j%ge<81TTE2WPs?$AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdb;#8!rZPh}F*By1 zC_gJTxg;j1sysO{Q@6OPG9AXwO)k(aPAw`+Ez&JWOwLYBPbDZ5pOTte5MPp?pA!$! z8K07wRtXe{2O1V%l3EP2h_FIk13d#hL(`bzqU4zJ{G#l%oc!{b`1s7c%#!$cy@JYL g95%W6DWy57c15f}FEIjfF^KVznURsPh#ANN06yVeG5`Po literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..472ea9a8053abd62858c596f40bd95b414f85cd5 GIT binary patch literal 13560 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..42a2177a2e4ca483d718080b1d2e09473ef6920e GIT binary patch literal 239 zcmX@j%ge<81j1`gGeGoX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!>T|V^lM8fE-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kMDWEXa&c&d&qt zD@iSaYQZ8=T$CIWAD@|*SrQ+wS5Wzj!zMRBr8Fniu80-rL`EPk1~EP|Gcqz3F#}lu DKZQx4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e4cf82b9b0d752436068f98075bc3a0696e2461 GIT binary patch literal 31996 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 V8qHtXvF~T^+L3s{FphQp{{eB?`#t~w literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7f75b3b542e0cd2aed210d4ea1dc21b95661486c GIT binary patch literal 252 zcmX@j%ge<81j1`gGeGoX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!n(b;8Q<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kMDWEXa&c&d&qt zD@iSaYQZ8=T$CJ>R9c*wms(sL6Ca2KczG$)vkyY=vGD`E(S3^ MGBYwV7BK@^06Jw&aR2}S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b4327935208b39c1236f865f8e67f4bea19df08f GIT binary patch literal 24998 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 MTC4fDS}mOaZ=GrGHUIzs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0aa2a10914e854b3ac8324f0c367c8da1149b63b GIT binary patch literal 8223 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 CL+K3w literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c6c70a9b7559e2ab6872cea94fab5d1cf5a176a2 GIT binary patch literal 248 zcmX@j%ge<81j1`gGeGoX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!n(k^9Q<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kMDWEXa&c&d&qt zD@iSaYQZ8=T$CJ>oL`h06Ca2KczG$)vkyY=uSo;E(S3^GBYwV I7BK@^0N*7`ssI20 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dadea0497c2124f08fa1a9be055bce6c6331615f GIT binary patch literal 42788 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 WXnA6LG)~w4+)xVDxFMH8&i@ZZJtY(X literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..935f9cd616425bb30c6f1a81de8105439cbd1188 GIT binary patch literal 22181 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 EKgxBEfdBvi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8c97464abe2a30beaa4f975132c422e258c6e034 GIT binary patch literal 35070 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 Nnmp`FO(8pR|9>34{__9; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..444a991d545515bddfb566acc3f4baf934fa071a GIT binary patch literal 12095 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a0e9fcb1529ef8fbceb5a948cac9d0d8eb7ac71 GIT binary patch literal 1133 zcma)6&ui0A9Dgrq)2yr$*QubFf~N@GA0Q&?5N}n~Qz$&LL!@|J3~E)e7Ez)d?kIvIUcNHr zjA_$$g=u=FNGo~E(1XIj3Zc`x1YiYGq+$c9f`L_$M)>UYut1H1FAn$;_>loWR*Nc< z6TfX)<_)u&E0n6&p*5a@z&v9 z=I(C08?V|+o9&mM+_^1pdDq)|?oZPDd_z`fpuI^VwQHtrfCmX5FEBC2Q6n=vpCd*K4^xF6!lU z(3UQ1`duwYhh_-$5UpGwx>W)O70SYveF(OkiX)_U?# z+R1)!gs&e1(?DGbjU;*i(NK|2LD61oDHJ9JdUc*P0=+U!h9u*2Lv`3FU?*NJ_3x~o z7$9;O9^-w!IRxMhO19*^!A~61OI1&grQ^*}*6k~LT zQ9EBBAD|P)l1I2?6xoppDQQJYh!ZEIXc<+L<55EHRW&or#0W7goe;*~CzjymI5!jA z__FZLd_9=N5jVWno&qzAehWfW?#f7-;3&}(0d+b0kC@-V{P}!pMu|A0Hmx)Meg}N~ o8;`O$kS_$QTx6%A4*nj<=487f#`p(1yDyF7#N*^21P_1qUsJe6cK`qY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..882a7067ab01bdb3a053a5048e682933c4929b1b GIT binary patch literal 248 zcmX@j%ge<81j1`gGeGoX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!n(k^9Q<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kMDWEXa&c&d&qt zD@iSaYQZ8=T$CJBT9TO)6Ca2KczG$)vkyY=uSo;E(S3^GBYwV I7BK@^0OL1HzW@LL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1490563e3b801c8acfe7f15922c65d3f4a31567 GIT binary patch literal 19603 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c9add4f2b12c6d91f4caab45f413afd352aa8511 GIT binary patch literal 219 zcmX@j%ge<81Yuh@XMpI(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdRpn|GQ<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kME>%!e5V=f=lp p=4F<|$LkeT{^GF7%}*)KNwq6t1v-Thh>JmtkIamWj77{q7672sKw|&^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5bff17d7b4e28924d0e4252297e16ff153a53bf7 GIT binary patch literal 3664 zcma)9|8o<^6+cO5Sw35`W%--14KlXDnu#68d?_dljj+!|^%uFa%+yyhH;QiU2qc}l zIzdd6R-_OhxL_wKwu>E`OepD)Lfh#8rBMEW{vt;DL9^pdI}=W}f2yhTQ-131NhfRy zv3KTHZ{NQ6?%lr6+xPSjqmiNT*eMtT-6a(D4Oysz&JnAZF)>LA6h{dfK^xR?8vLsZ zYC}3s7os^@LkYA{5G)AkIX$MezCuBNmf{RTAy*_AI7TSqit$q-Fq}~+=1f8fS1K5} zGQq@|1)3`tN;!*A##IPru2LumI#31ZVXYOvbyzD{&eB}9mIBqca_(?7qgs1K%!QZC z^0{9xKDwQ`dj9F(?lR2Mog102uFJEN^4K|f{MOS4A1z=1>*BTBi@!aaxfsvPT+E8g z_wO&CzaoD!wsiaNSo@p!hs-ju-DlS#x-;OHkj@z%UDjZhU4xhrAn`$QWCTDzky3{a z6XaRlkEcmWQu$Q8R3dz3Na`6DvqW-ea8{q*r{X7Yyocv7DJO1}4J z`NFJx^O8Jv)viPO?AxIpOCkf$hkYTy^S`H7iM{XMV;#pv-);2;jzyq&9QdVU!PBSw zzCdgA^yr96hy0P&D1avcY>oK*CwwDd!yO*$@BvXZmtXqD@Ni(HEi#HsJP1c)5adn= z8nHVj{6uT;6sNw`QwCG~7YpXvnYWVW<_r3TVspF~7URv+P47xl#PRy;IadV`inRJg z7-w*V?`dxFMg3AB1nf~;tF5_(oNJB@f|DS)W*q|2DE7-9 zMS2LN7!3RIoARR!QwkKyakw!Qa-pUu6|x^t?^DO~pvddtbW&lVp`-$pf1CvRg&}Aq zgUY(4%~Bg^=p-eziR>mdXbE2d*|ABD0*M4%8F>w}%Gtm_y;QDBs=DfFNSn1$C{?`D z#`mA6L8^LDa|3^iB!X*YsQzxC!AM*FPh+!RRxH)MsGC2X@BgwAX`8Ak*;Go^RXso6 zsO~)fRnaOKPqa`Q;n5^DshXP&Q-yK!bC`uI@bl70jy7npb5{`TH8d zLdwK7YF4|UbOn~K+_ZERmM++|bTyW)$w~{>S=}06H29L=5({7_850GLK73DLj5!< z!PjiKO1Aal;z05-|;ea>DM)vRN6vI89+SHIWg?n5PPUx&Ms_3+-Ieij)!*)Hc` z4>s)X=evos*`;&P>*hN=tkcUPT7ofvihA5f_#XBs+k+TFcDX&hPOsgFECM(g@B`i# z3GnauqJWRVAhO_FUlQSZrV)fC03$Gfg#rR3xs6I8_*o1@C7x6TF(#qnkncSn-}o>9 zqR5;x6UDZGQiV~?I+g&Uei(>I0Wpk9K-ez|0P@mk1R$dThJCRhHY`T?6U58x(if9N z-VcFK!rz3;Jg*cU#1L{=gdv|4BbQ_@#7Z#b+hdsv<1uoNWJ?{6r9AUTc{c8_L6X(V zR<`b~-6H84BNaiOn3C_^aCJItF>-k?-MN?f(}zoU&t)d>VhbckW296q-u%Q~snm2- zAb@vbBS*VoYU|PQvbZ z=uU3yN!Io*RBxGSpF1#pVE*mDbSG=yOjbJ=>bK1eO%Ek@I#LG*ll4azns(fDU3aBR zcBER~!Y^+oFSpjt6-^h-x24*PVP7Q#8Br*0^J8c>dUvt?h~W-3jxa6umd6 zGk$1(2R@f4+uuku9K4V1y_uq&|IRm{b}K66c|r8^JlsXXsK;p<2#-L5e2BX^3YnwO z-1Y0;b2Gqj{#KHS}r;rD6U>)%}yQujnbp z6xaWkF3^;(lu_GQO@gXTZ|%S{URrAtROO0+ssu0(HQm^j zpls>th6GiSE;GlC>8dSRQKKbZls0z6nH7UwQ$F4~Wts9%)lBrQ;(ssY&o~XmR9vb% IqgUMiA0OkZz5oCK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f71c3f8514a5ff56bce3ef07468829ef604eed2 GIT binary patch literal 34766 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 a#e%ENUHd;#<$S8vKVCboqSU$M3HZN<&QyZ{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ea46709c4dfb08716560efbebe332b8da3355597 GIT binary patch literal 17127 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..daef1c6e3e5b9bf5c5f3f93e2564b3b12fe6e26c GIT binary patch literal 223 zcmX@j%ge<81Yuh@XMpI(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdRp)9IQ<E-0 zl%JKFToRL0Ri2!fsasrCnGR#;CKu=yrxul^7U>owCTAz6rxKKj2kME>%!e5V=N1JmtkIamWj77{q768b1LJI%@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a3762f882804bf76a1d9b1f5cf9afa4b043cca88 GIT binary patch literal 26507 zcmd6Q33OCdy6&lYNM)|f14RO40KzCk7y}_Bf&n2aqh*m(K!pr=s!AbUv7>?(-6-KY z5EUhKi-s1VY;BDU8f~?6yM6DSGUcPJ%YBz#oyxG*TdT{)UbmP1)_U*%_pWp5RHZ`D z+wa|XSnP9ZpS}M*oW1}5`~Us#oj)Wd>KVA^{W{V1n3G}tgg=B!6@Iui01u}amf69u zGFIL!+aV)&`3^a`D|RU0u4q=asCKAY)H~EInjM-J?GCMsVU?_^S=XZ9p@(NRt7%SX zG3+q3B<@HgzqQRtEy+8Q;aP4;VRc8C9jR7X(=?$1k$Y1~MqSi=#9O%&pGWLPuVqywc4!^sZI56jDvolTHhKYH<_;OkEhKljYgwbR4BXM?Yv z((6YrouiM#C%T3{dS&SHQ_@r;FCQ6x_FVA9FaLb?_{j6ULzkZ&e)i3v`~9K5XM$bF zhps&{a_u6d33eSBdGi^X@lTyk=*O^b^<~O{vf9?@2q>yr4+pfH4mxaYt(NA1a-+@e z7{mIP$pY%No9b)o){n{Xr#z5;kZWtS+U;i6`iQO3YGy4COCU2QlC#>K%?>-{*6*`A z%*}24_F1_`EK|4?@PPksErH+^;}GJR&SVG1lgptLp5DUJ;j&0lZiU)8^B{8M?8Tvv zUmZSmF8J}O;7h&14}KkVzZ!h==fUGImMH>iS~_QdlmV^T+-hmDn$72#TS)Kwr`J{P zI=p|j#kLEY&}40N>}qa*w9#UlZErukPk3%=JUH8K%Em9t=v+ba$p4kVeat#&784R;oo4z){z|MqMMI+^d(j5ewBu^Xv* zr)vjOr@PfR5>h+w;Zoscz+i24Sh<6o&2Ft@1BsDeW*ZyT=^99l*R408J5MoI2IdTd z^9ANg9;>h__sUu25tuKm%BqB!A>XTHHAj>?)T|bNTQycCj%AEOxr|;OB?4bhi;By0qT{Nkh^4uWOkj5afta?8TOrvF^e${VO%jyT9Pd) z5NJUTfp<%(k&)wKnh3{_%E*}2!f%(nDOq@q+*t+W6Inzg{uSm`xxg@F%wc7j%6S0Z z>P_KG1TpJJB!Tq%;k}@o%e0oD5ejj zhv&F7Qy`hMKJ2vG9cG8E#oFd{I4hu5(H~c=TJYlWVRsjNS13rNUumi@(-O|WVMlYw z8UordReM0Q75FmnlYqu9JcLUMC~d8K+khD=?bhbKBp;UqneF)Xp=}&;8f1>PR-40Y zZqJIYRfX_k3g+1b0WhQ_Ce1XMI@PWA=a+t!Uw5H~Ur_6vRp(avv^h7jrd-b|@n)3_ zW|g^>#|=YMrt>ovd#5aMD^D1X8GPFOo7$uk3C9xn%+A0KuDhSFet${LrfgTKq!2%GM6l7 zIKh-~h{HVIuw@m?A-79rQBN>zmE&o`5N&}>;s6SIo*KIP_)y==b=ASQyMss1jXZlP z_=_ilC%VU;K5_eT8mHeXO(L<*t6~|>Oek^qRL}4WZWzRp6fn#3=-o%`1SBB z?+w4`sWgShU_fuP+gj}oOKYPwps+i*faXyP*J^9s7m&BXXtcIIV&mFcxeTDfnII@r za8vQQ7=!V{FWU}KA-eXCHi&qny(DHhPLvSmlYJ)y(&70{a?yoy{;t|Di+$zwenZ-c z`eXHc!2*8adKj>LUY*ZyH^knYzT~+4g#MVmYo%9PbTfOVJJBx;(K5b#1z)z3H?I0x zyPA$pjT7(yidT^dLa8`@2s^zWn9~1~krIZN2XJ+$T&gByEHM|WC&2eb zc#A_L<`Tq3huWo%S0RU%QsVb8e9=^RlN&=A zega+Pq;*Ymk*0H$`mt>LLpU?NqCCfn~EaO!qYT{DdAzb7WezE}Gmpy3vYo$$QuPQ(@EY#vv?jsV-x+=H-0-RQY3BO2 zR;yU^zoKvvrH@b^h@STBIrhVLhqYx+L>A1!ip>eAs~Q`v2OZxl%IB05;WYgAFN3Fh zXSSlFZ zT?n9~dbHByEP(U?$!LQBBSP;F_goELJrTV6M6j=~()3p$#ib91-+6-e0P5yHdw;m| z@sXa#Aq7I+S*GA~&jc?&4OXd+d-US#!uNw$CGaY`3ZsPV==(>ZSCMS^ z%R^e6Z_#N-uALmYa+>B4s^^?OE*Ve?o;W%3{F&j?zZyAnPDt5sUKY?=4>bZN1n6j_ zuoH(ohqVf*tQ^d*m!IspI6cQXs?aWS>iJ+^`~cr)J$6D_0%G7Y7w7O%oi`~Ti>^oPg>_z z+%y=wtRaOwUGLU}w9|D{k-~;!8~EJWz59B%d)oNy8lPdkKP&H)>!gd~Y`-z9C+oc# z{L;I<^EZSPOm^ulMxLGIt_^9Kl-p(x`WN=k;EU_NHr{N6kJiqyXh4ZoG zoz)9Z9m+|{NpLBn%W)}PibnY%`2m1Hhh&@zT5Z7RR(Ydb2EbH8FHxvb^Q-s(1$j=g+h`1IwmZg;TrYFSkN#h85|i2T0JQTGKg z``GEu(KjB4wt^UoN{_`YT3Tru{piWy2N%WkXr;b#dGz8%D*q%|md~xY3u^a~J0|(? zv%RCwbVd5HirZ(8bPuxjm`PJ9iJiZQEifTiL_{)hnI(W4c?C0ahvN(_tRKc)k>H|s9V=$m1ow-k#O-@voz%~iE6ej|i z5wr^`P5Ty*>rbop{g*HC8d}AnVnDJW9C3F+7U>v-ER=3&6{)6lnu%4TwT7`nrpO8> zvYc5aI*7$zoiC4m{PYS~&Pb4aID!|-Gz3osRIGK6b01ioEUj!?3%3HO1XNs`vy}~K zoUOLKZCndAYG^1Dsf7ewJq8;vX$s9}cI*caAPJ#E())p-}y4V3U(?;l+Bz+lQYx9SF%J}^sGMX$4WAD^+vr(Fyy5cyMkGT$sb zTj)`E8V09U_Ein$t>m@Yk<|R0ReZ&2KBLN~T?55rmGn;k@0p7{3w)W2+zCU1J*Bem zuD+7~9Dc)2{_b7;E-SzHVcyE|M!QezfLukhJ#F698Zay-XSfroy+zAe9vv?OvG<4! zEWINc=#p^>5@k%3iLJ1H7%e|^OvI+`64Q0aC2~WQWKf6j1!fm{R0*OLdVC2kIZ>fS zt2a?0rSdeMTv9qjE4WKE*kH|B)df*H%b0x(Q6V)QipLb7u9EwsGEs%pI<3%M;)Eaj z#gX9aM@G6(pnx`m+|lz?@YyrS07%T}wF|?~{9>r@^2o(bSW<&I3GpLWjsTY#edjFI zene`)H=Y=N?X(F%wasz3jx~+E(g*91NYwoj$}P}I-5!)0zxg1-YR5Bi;d8?@med5u z(Bhynrky)!A+t<`9M7l<}>5Bvxy znysx|0z45$$5rFgatul!C{t3tcOOP<#h?ZPyAlFqXq20!9ptRS(r}oUwNH)VV&jv% ziG+?r0Ng86;9gmqycU~p^Hxv}e4BUkZ3p>H5AzwEPiqe;)w;|Z88drp2Q%(+*W3`a zrH$TQd%oPt-}}IqJNf-Keq$4#eZXgE_NV8#@A+F8(Pv_6q9&~WQZZ2V2X+4;K4&w( z(~OAzA%A+tGj)GQK%e=&9rgM*=T+q}pXBJ)q$)lsnzqKE_*5-}=T8kP2#NTJ!ssG@ z+8@K8z%v1O&S^!591OEEuHp8J6I`{F8%21Kh>sO61(z#Xl)!Hma{+IP!Qw994U#-@ z`ORBUZ@gp-8jvSVuZm3%MnW7;rg(X#5&O@34hZ&=ffxz=U~h%pw1ORvcLzko&rx{j8a5M1kLb^9}d6!f|N5x zSPRV^4LcE1RhlRO7K;?Q3^G8%H6rLVj5ov5U%Z8_NpC#y=c_M|$C;R{C_)1;Vf-{C z9c|5R)BKQhB<%s~VIWv!f7AwAX+^uO#b$17HP4&3aN)cKOXkmAWM=mt*f)2cxvkZF z&^+HfuV|L|AuxZutBAGQ8#x;>{~+y=$A2^Y?zQ00UJD)t&EhfuwXw&a3wHf%9~*oAV$p4Bcig_{BGLb1(jLNE7lm7n>yOa7AJHya^-1bHVT&RNUD}7^ zV>=?lG@i*86&N(v&N6|^57=zQp-N>Ul@!uBGOj{*FhC|(t`n9pfm#e0kdZqz63O35>8x8 z0fhui!jvV1JBk1#FutOcj>AMEc5ji@P#Z55|IVq94F&hna})_6$U1)+)c9~G6PvQr z1gRq5iEyq7<+q+%Rnblh25tD*970Sg)R7Lczs8Dk6&2-ZE)68H*1Z;R-ZE2p9Y_f` z3ycgjhRS(dDI)`s3i=F*6hCO;z{LtYlI-o%CYJS)R9AtN{WlPRxL+e01}w<5}dz>%7|Qoa>6iv2E~Vf zf(VD57zjuiX$0jH2p}+*KXpDH<}M>|AB(j<-3jleh=Nv;G8DiYpo9dE_K6J$dveT$ zMj=8O>U$ejd1yu{myTgiVDAI{13f9YT@W}ILojmna?o=!`0-J+@V$IQ+Q4u(M6^E8 zj7;ISjdj70KCj^J0ulj5i+vw~$xd8+3tO3pw<{%Qnk~|(G_@zp5{Q7$iJF*TD0d?& ziU}wXbtI77XlZU1OeWxQC8#EZpXtatl^F|0EhR=lNunSC+*pY%u)QFLVMV_VA%PDe z=ww2fj3LFn95e)XHP|dtj3*8qJJg-zOD?%lJl9>_Rp^C<20;_K`^yyrbNK8XKEuvP zTD>p1xOd^DCFhrT>U=X-`m^tK*M&JxcaG0c?9ZRltAn+c^wqp!b=Y>|Pl7d=OZ(67 z_p}dItl?*`?O!(F_-}{)`4GR&GPr3EU*E_ZSXeL7=|iv<1>CoC3m-d_t zSdqpiJ16VVfu$$HisF@_b!k{#JG7aw99$PjaOtQ!2{0JRx`3pgv=OaG?56~WsPBLu zc$m#UAp?gJQR4}BE!x5E^w*B6jTxlaa>-&!9?r@Hf@4Brn5$&{V~i~9nbN_rE%$8dRGFQaazK-kKa@> zYynx3DI_a0Q|O9JN1~*LalV{O>}6p7upUI5j#O-NM-pr5NNzPkU#SjM0}n}Hr$Stj zgtijiE7?4VE$&EmCApH((FxXl6yY3c4rE{CLdxw>Iyz6>Ax5IhopjF==0m$D#O7m{v#pk5U* zcDbWZj=a|+3NB(?j3+>)%2Cua6~aVJ9vSL;Hkz?hxeihd86uTt)NmOQ^>-qWrJX?q zvZ(h{-C+*(2f>BCi)TSdj$V3WLDKqB!{J0~81@6?h+hRl($VIyG((c%&MVNhFa{>pwu|ge@Y}K5F*Q-4 zCe5%H%&At?+rl11lNAuQEkcH!aXaAlT`IgK?XtD9)Jiex#&2iVD1j`(t1NaDTup+e31@~tTf)X;SnuYsJC$69XH?j^pO}Ry1a;|n9V zbe@Vo#Rg$PQTdPdjy(y^hiDiX`>*iQiR!R41-!i_m zgwt>>5EW2U#t<-syT(iwQ#Ik~UzWmAyGJu@U!x`uvRv^1QZr$Gowe23g1fG)+;04> zzaO_1tRtRm`!E$Q^HLWzwdLSJYb#4lB_$ZW2?8i$Dn9SWU@ruL1X4Y4b91&j0*OMs z=uZQ!E`7;4Fc10E3G`DF_#F^_pqpJFMKnqZ_7`xnH;1Vb%L_QxIl-#M9Y&QVU}%F) zX{h}WH@gJk7re6L{17I&a1R@6_7In%Xc0KxSQ9r@77Qc12f%bE^X(EQCI9ux-sY>- zzTzr>_V(N9FPS;rvj#Iu+zI~d9B{4A%61$4X-2n3Tohj3x3lja-gvi9yJ4tkX79$q zqNRUf%5~{pL!oz>E$f@z@8TQG{6kINh6DW8WZx9s9~w_M-- zuy^;vJojjbVcKQ2@>>k^psY>~zjn*+#@n|346l=fFfm6{%!2YnRUA!BkDc@B7X_m>as9@xb1gf(@Jx7ztkhdTk6=V7Z&*9M;! zTV}}V-tbjIi9fH^Gf}>GyI!&@Vg)U{ZC(4^|W{ncxP1gul3GYKalD(ZQxm}Kd59X;!MtIpQ-EaNL``VaRX;0w0!yDeP?U&@}?UMVT}EbV8#g&X`+rjgG1ckG&lu(|E9)e2sqQ7W+- z@)4D|8^SwMiA}<;p~>L;M2ayeh2k<9lpel7^#s>5fcUVl1FVIxr{net(0}6Ij`GR& zc1%+9gxDPr=xWD;$`o%u3G_=Myg@hWmtWbb65juC`?AH|7*S;}=+Lmzy>H@9l{--{qsC{dv^Jm?Y950f zEc6b0AxuK4(B8YC%rwc8_75zRO}`+T4WQ?4TdwHTlHt4zH4w2Zb*~Jb><)f#PS_U# z$P9FVNyR#Aq-n+#}z!@PD{5l2%GVQ(f~6?fO310urUJmmTx87 z(dU3$`kKS*!knv)okNhIbXSBx2|Z?SH&+3F50VQPy~S7`Rp}aL#QC6qm6Nwj# z1SR6^A}EpUWS~e8xiE*G+#KpW+-eMPj&oHQtifO{2GtON>kNU0M=%1RVPs1Jw+?@j z-3T@CWJj{mkSrJX7qGT{Hc+a~;=qH=2sU<{+uy`VpF(i*c*WuNM1? zR{CM2U^I}aQ9BaW^&jZp%_nVjD+p2xDW~YF$l4g$i!k4_%rl!`wT;i- z?lbJT!_5e=*fzmt1QE%t>~r+(<@4A1Qfh!`SHjK0Wqq~Rmv8ni-^?$&m%n!>zl*)T z%jVr><99ak`3E9!VY@G--lrN`E&hw)BO3SZ#By7v=F0AP5aw-22!?kmw*D= zQa7v*t7Vz`Wl+(!Zb2-ZW9}uMMl|?g5&m?#to<%O7S8 zWb@fuBw2bJ(BEi&#N?0JLK@5y-btXTTC{c*^XbYe25!GywPL+a@!86J2tUs)t4q3up}N`DBwwObW)> z_3%3d`8BK9$Jm&?iboj8r@&ZDHAyd}#>Y==GF^J<&`gp)E#7WbHa$K*0{mr!H==nP zsu#ZgFW@f`;)ZKHcNt*#HF7fjL={KfPP=%{;LS@RwZ{pXw!t!2}0AdIX}KkHuplF zFscEe-L_oe%ar9i|0mQS2B8W27peG%qmF0zDCH`k!BPNMFdo3! z;XolP-QLO}qT%L3Fb>5D-9%6vt`{PTllIMc{6_7TERsai8C`WZ!IKozQMyt>VDhV+ zG*Ky-a;c4!b{L+xbo}V(FOQZ@013yJ69Wl_215R)pm+%+9G~@0AmR92w}*u3KNb=` z3biG+#7V%Rbie9E;1JHw;Lz&Gas(x!En7f|VSj1@6Uj&?gxWiejT55ckH&+Inh{gv8Rk*Ru4GIXV`p) zSTSbfYDlg~1rt^AnH4Xscx6>r^>wCBdNC2$@L1frV%qC7?gKMdSN%T>r_Ka3CHSFXV+;PNI;4Q7zkj!ALh!nNU@4z{ z-$dW*KOMZ59*ba>-(7C~@|FtCZ}Vgz_l#NqJ} z>I4=$6I$#5B}dvkNj<<;7cTQZWUJGHt!}E&B@rhef&_m^ZBnJRVGUsC+UUQaz!MV;rs|{AR@t0&yygor3mV{oeFPW z`^WlN;#nW0elboO?KyLZ2*xuW=ZK4TbI=p9-LSR|4rFX~%x*Y*(Au5?BE8wxNKSg3 zgXdt7{jG5u4d4rBH^oM@MF5EK@Df-g77w_fh9vYrqJ}#39g68aN?llMjacdEiaR+p zGd73hToDSvipP_k7ki8wSe+q8=z^lv%%h5XUjo5J>!m8 z-&SKP)O+Jv-^l80HM|v01tFF;vA+Ta;oLnOHq;ik6=@M1HUZS)tdOF;u$nk`YziN} zSP|oB`o9rKeF6dT2aPhGE$5R~ASTM?^DBh2JoHSqse4-2A-5rj<2-xMa^XX9^rHE;aS^n%PH!^Zg?Kruk$1s>N`$l#lKV_~rdtOM# zWS8AyROH}~lo(rE&eSsnFBQBprAsZ^+Lra9Z+m{VFJ+xStDr0KABJHju(f61^}}&W znu&797}*wIRrFO|QF{)-At911w>7fi!v9NarmkJie7X#5Y|EM7F0Wj#P<*x| zAHvU#r8RQJ=gVdA{Ds_rVN!X`O2rpTNX$wV!~}GXwl?rWh9MTvM|Nm6A_NgG5t5`3 zNP-Rl^qTC>Vg;c}0_xg+sfYp{vuH6u{s_DJZc;?UUw@FgA{jf4)I55ldK&{4;sQvv>& z@#rK*dLIs*)4=Y|NH4_exwyy6N%Dxk$08X#*<@UA!&$FzJ_-wmoB?o36Vd`qfMe7o z)G~xOkvjkv5hh0i7dG`mTEticzNP61M-FT0UE*14bnXOJD#8Nv8hWiu>KKRaL*%?O zlQ`8(xYHmq+f39;6E<#-bv}U$UBpi-c=FoNmB@J{krLvZ@+wk>CBF~O1&@9Z11wC< zZB&$l^C|V>fd^6k!!v9XtYgI@P=Ak95B&U2cMe^9mmY&5nwjV-Ar;8f)J0rF@f4US zLgJ5jzNjEO1vf(4UNF#%TzF%s@7H2BBKN72-T2N03liDnMz^C;&;{EuB>mBzG%Knt z)Z#G>#y}%{H}OJ!15H%7PzuBP0%MmQ1~+$b!-qlG0lSu5GT~?*PQxnjSRT9VI2^Zr zSSe$Urz@GmGB^ztWjZ~{Lu>~2q9g)1SD?OzV@R)APC=zVtaghX&K$^pp#oSSGgvs5sG_EPep+?|U1Dv`I_@Xc5bbBoN;~(#s>4FOB~E9q1&OH6w6v z>$B(RR%ghyc7j~eu7y2_bTcAk4|-l5z4QXjE*Sv zc@NTQ2<0O(gN51QV;|wW)5qumw9-USXm}=*a}mgPB;r{(e%y+7y=bIaUzR}Eb_u}5 z(G*bb0Rc_dbxrV_TKsn7Qz-`37$C+JP~-`W=)&N62m(o@qs2bvI!L7Wn;erDNR0f3 zPMOe-S8&?_);{Zp497|wbj<{WMovq}nD5gr5YJ_q=PC2#_07ErDs}SYVF8_zjV>{(n@bB93um7OV@QLev?;vBfsd(hL<+*<(v6?ALKWi`O=4c z`4)fiTt0s;tphpsSEym$Lq3DWpFZ`g^pc(}yrBe+kRvC&Mb3j*dDY%O@5&>6`}v&p z{FeK1efEJH=^4Ul5IEdFkyoXzOJ+VbuE~enXY&dm{5)A#qf&gHU0lN`zF<`FJdQac zx4N|j0_hRKE-*w1Qj*I92QXws9=14Z%&;}C8IAQ%QX%3I%b|6L1jupiA3^(%FyAN_eydDW?U40sy~W@? zRG6%)lJ%_(F?jE$RUrm`_Br~i2hs-`2eLn@ z_pX8(KwPMNzN+wMQf8Nm8ckRi?yh$@cDu@IbqE8ZuY9Qv@{j%w*%$_B^)t+=u zqbK`(y|;K-h=I6%$G}?t-iCqNPaeC4zeDx17MV)#ndWiyt?f_mt9|D&@BEq&gOLLZ zc{p+9zHR*GZU4OT7KsWysF)AU_cVqWyn`uy!GNNF$%n1C$nVg8*;*OCFAg!}p>JBB zy?<@rp${u=(TLF9@+FX8<5WD8W8k=PIeKl!cT{_zlR zVfno_-U3b_P2Q~s=$nJ~TbSZjlT4wSNyBe!XUtIh8^z?&T4VAfTf3SvPWPZNTh#m1RSzS zR`m9#f(S7YSB?Sdnsg&88Fx4vVg}^*+U0a^tmMwaTbz&fW(Z(4KqmWEu9PWn85o)V zUzp^7VG{n0nKj7F`d23J-*;lIesuLDS$^#OcH_! zCfGM5Hh}~&lZTQp;7l?TC6L6EWan~s=8smSTr|4eUC+j{&7M6c0wz1Vx#!%oUsZLt zWFv>nojJQ_&QANd)ZO*fSI@5czTfv%{WdvSO~JMCe6sD0R*L!>JqTA4e{*9cO;IBh zLosw0)lKiDyCpj%-O`;>8oiTr$-3n`<@jFOrP!%pWQ@Eksav^I3GbwBDn{`PwNuR` zL8xJ}809ncPA#K?r({NLrQuJS1sS1#Mgvb0tIjIjEfL=BmND9AWII!s!}jSf<$K};HBNSF_bH$uE2VajxfF)}8oQ#z9Yp~;%TWInc# zqF8#rbidS+eHAsOMH-fEwb>nE**aUhBP?Chv)@98HH>w4o3qQoz`K^K(9#sVRCib% ztzEqx9agp-B^53y+@Sv(^$?6u4&gmDkm5k^@#T;Sckdu}aJ9uz-VOYnIq-7&%(=-= zUzr+tJ@V;Dl+^0 z|7ca4?V&wvZ>P20@laRa-u5rCsz%>J^F5<}~E)kPGoH%ApYM-{JGK=P{CgDR4u@oO8LPF+L8B5E*;> z6yyf!rT_8tjLbznPP3XAcSbJc3EU}0hCac5SzH}t3F;7^UJ*;rB)MdBxGQ6^E*X=A zxy$3Y%WuJ56-y7?<#V{J<6<>8#cE@*E;*mK0`4@p6iIQVP~2P!Rv#CibaT8tz73M& za#D1n;Wg)CbWDmc0{WFM<*d~DxYS9vOs#UMW~EMzOHI3!E)|n@O4_f65r#g7ITvGa zDNj<2(WQb>D-%-E)I6g*-KCyAx?SqKsUpgu7uXh4tfgO*cr-I6he4ooX@tBGKPKaB zrqCKR^#H|W^=qEc?9U>o}D@ON#wOdQ!kE9 z9X*uRN^qP6#ScenL0VnHHrJT>*|DW2(T@6Npc`pl=tBPU;N>Fu#%B4;`DLgeBA zn&lHUVk|4DNrTy$59FwEBFCPBLPb73c=gPMsnPS1pN~Wi`ljE#5P9{86MaFm(oOn~ z&OH4wH2d^hABEMiYGH1qZj-*BPhb2fGBzBG1)fvqe)%oRG45re9K#B-63n3$d6HP3 zpBdGFEF89;-MxvcFo!51r?9GbkCkn6*m`@MMYEZT^dbex+*u{kL1)d0Ojwez^fBxH zFy%Cf39I1+;YJ!1H@C=YZY-?Z+E`y~V!}#cw1kuR!Ga4P|Lu^0Fy$VKJa@XmEYLUh zsVmKVlE!a!P#!{uhyE!T8b)|UI+H|PJ$_>5{P_m+J#W#F>%R6@)Z8r=HC~6qa>n|I zvm>mw*=;>`M_W(3H7rBlTUc&o*i zO}o`@w=%rk5vHwnEwD$^hH$Z{dBO&sw3X+EShF<{gGyt+4wDxMA(@meH;}wwpgAN{ zAKH3gD`#l*b^BVm)NMgoOH{U&mK!HivPO$vlLb>sLYd3mn?l;OV_Oez9nA`Ai>{>Q zjy@1bE910fe@Mvd!^tJw3PKWnjD7vuTQG6{)maFEJnNcmJN;#%Etl><{edWH~@txcqyDmM+8C!$OABB{< zW7@;op(nV~Wn4)emtG%KE)S*TbISbxHK{RPrzCPCPs3$YoNXGT-)Z*NbLmTc4!1U_ zTz4hK@NZE$5PgSbPtW>A&3fuLdesJr^fy`g&5hFEG|J(=Jzj`JB0GA%Q3^r4*y*BN zw6H*euqQnraY^rnMH-9ocZf^kxx%Od zos1m%D01LOO`{ta!rl=3Mz8-!syJo*T^as8MyctEvB=X z6P~jRg4ak3jlWupj0OD-WEu#(cprv<=p>@v0V$B?fCmQ)Z%2?`A;yAUA!;weFtOA7 zX-0yjdr&_?lE=z)KOL8fPM8W5cNYzEHN9Wqq8T}sjU{oZBneX`F>*%PfixNEI%?PL zNM~@#ToT};W>k#YB?Bo_^9rc+?Gl>NVMupRAUy%rDnPeo{+mYm+ zj7rDSgUfj;U5rXb?N0&86m<^1Kr+qlS3p_x4gu1jT%xXmq)@~kWY5%o#S@CV`QJF% zGtD^)jPU5V`u5<=`2+FmClLl=tVYfaPQCd4)WDm!{H$i)I6(M_pXxjV+u_{PWoyoWfdv z?6k44sPDE}yBK>|j(@{y{DXx8b}r^ zERe6h5?jU%brYHlycmZJHL*1qQIIt_WLPnwG2n$4-HPiiq9E&S$gpxEr^xdlm%Z4X z5=t+=PN`I@-Rnas#?dYQl(JApE@xgkcJJ5*-!d+5Etj$OOa0mjSYz+?XH~d2Uok8g z?L1o?G%Nx_Pr4_GD_hIuHE|hDU+SACfX8NkN~yQgmma8IEmAacd8@dLRbT2?MGcTy z)I{ma$?`|bS3|~JX6^g!zJm95kKNCiR&gn-zEZCGx2O~%zq4rmS5o{j7pU_lxBahLvpFULvo- z?2ELic5xDf+z>>9r$`fw#rXA8glzyYnFqF^00fBxYTn`b_P``MTjh;S9}WbI;Y*+f zScqCj4bi*CU;>oJ@e_d@M*7%8Fh=M>gGTH`302_`p#_)B zfdo)e8h$Zgvm7F*FzLe*5)Z5mzCTadbhj#-P}~1t`jlIwzh&F} zh3RjVzy2?*|E<#BD*x0Dacsa6opwsupES=1Fu2fYivvqREKr~&y%Q}Eb1qks&~7w* zVT13Ojjj zzFcB{030UHNrYPAt4-VoB#)eVYML8g)4^9{hznZW=UmK!xbz};JZID-&OZ-9o3loj z67;2f=O&oi)o3-p`qO84?MT26BV*4-o<9x17?C_g>~Q+r02oAwpf-KsnaKGMXWl)- zLy3qJ!Q4dL#5{e{7a1A^bt~dN5gBp=3>k5cP5K6Ae)&NnxlmZt01J%E9pRZ6tpw1= z{NRHGl&Lh^+k08-u1a%tb@i@!Bt}vzf+&)jKnUeV9M2z{^u0|gK#)bgd>DS5{PfJ! zf%D&7bc32mVr>Xw#94x{<7l&W*@;~D&BepG4V|^nVK;+`1R)}ML#{KQybw9~I$xAm z&cIEBnMJGARTQvD!ot!kk0PDoY4;l$ZcA`ybZZ_pPOj)$I59_!FcIc{Vpn)o8_ zlaUJ_L8-{Fi<~26!y<79+w0r|GAja>NDn!Y6asw;{R@a3LgGm)3UxG>VadGA3=GXq zgp!%j2*6C8sO5zDj5I-soeKdPiIFS@Hb!uSxf?GftEh!j2WDzPHmUdZt zSfpu$WnI{c>evs6tQlTlfj=xmLxU|w1W9|_*dAL?M_AFeXOFdq3CnE`YqveD>$b8T zRxlI-gl@Ci*%BbOXxNpA2L=(f@r?{1B%}%}NOQ8A5Ebs%RJ*NvPnQj}1%~k2&!TUE zM_Yk$I2mUo#M+T`iY5#GBD#cW4qU)v3hezr{4@Bse+yp-fCZ+TFvE@hosay^aOwWv zE#MxtaT`0i)W?Fdu0JX$WBw^ez)(D}bwZzcgUXdRx@A{1I=AD<@=^N8st~|`1;abs z%^_pvXzK6=_xcF}E^;}`-go$#-*b)ijBg9t?3|bOUBmw zY62On+#7i`lgn8&ru6ptQn{=ZLG8+GQ1aZ6-ZY{c)^P>5kDJDoT>hq@esf5l3E*8p z*_-M!YH!0B8!$IqHm~xVSNWU)^ZF1(WS`0QZuUt61uHHWto9eI9#0Mww1jdCPj5c4 z*|TpfEs(qPa&Empw;q(6+|{A{lGA-B`n(Ir)&}zHF6XcC=dbW>3*>JOpz4wjJI!y3n)uL`6_rDxl4Eo_bF?SJ}d4ZU0Jp2aK!*72bMiOZPTJ zfl^cjwF_rad)L2TH1_CwRb%c>k z8S1P=)IVsD6R}>l3qC3YP$kqxL@i{Vcu^;sCte;W^(M|i!6XS~Vw9(30FX&KMU4|` z#a}TEkERB9Vz~&_ch1F#652e{mnfgj6E7NI=ZP1MrjB#7F+^Z!l?C#s2K%l0b(~mV3D&4-iFnpq+$naptQSfAFdLMAQ_7Se^g;jcblz8 zSh;o~hW9`aFPtEw8M^~s+R=hbmMy$$v3DYJ6ilPo&mb6}t{K5@6iP{Tt6-)un1&wp z7q1Sc-9E4dj5BG*fz1)IfU0HI+PD?bacqjp4E~%dgBDt^P`BiKYNgm~FF)Pp6zn@#+GZ zbzoOIoqrSY#-k4*&Hwe?o~MQ{OTU1YJvp88M z?wPRK4qi$C?16YSYfzyUz|+nf&_RDLv@}Nzp%Q)eFdX?|ihPR(B#g929fO)&u~_Lm zA1uH~KPVqj!33|KIC$u0j}{g%`W~H2c0dzX4sjX;s7Q(?g-#^a%((=G7@!{oyCnuv zb&3#GECB;F!iTtyI+oNf=>T7>UGN10Y>>p7T}j`U1FQf3_+iOlsSIEO;H6~maVZ6_ zpPm+&77|RpjHqJvtKwQmm7sO>F4gyytS8r{I*=9Da^i&jy{S;EDqK<*SSn6QK>4N< zffQg*fxc!ifHn)P18*E;Uul>0C+(#yTIYu_Sn&iodEt3*#JD>0K9RPGxdO0r;xH4N zz9)Upn*|-_>w%}#IPuh}(^IET5*=>(wT~u0ITG`l`3a^FWZFXf^=x7a8_b1Emlakf zh+NZUYqPh2A0eWdcE39{`oh$aqmg%Cj68qvn~Nvm4JaS;z8U-|A=7#I&7lR2Su9XL zsT$EpX5M@T6m?Rms#s$*n)&L0LJUv{a()SBN{jCyP%RKeU>obSmCP~Cf zesl3oLd*>8!HiJ2l zpxz-59;DlyeD&(U0Kgc80<}1lc=EzgLIHUYV=5|`&`P9@Z*qJ3Nui*d!WaOLj!JW( zFy_dMeq1J}1!f5XEs$YKMldfW{KMen$493>2Jck_S%BuA#CI0K0gwQ|rxFB~f-#ma|Vi*{9DTq3N(bf}gV30ZE}0!L>h1PO~l@_mF^GY+@Sw zG?n5AX|$+fiY!@=%YYqLUP1kL0M-zeuvWMQwbCJ|rEQ1=tEEa@Bye>BFBk0og>S(k z*k*491Sza0w+`z*M>wVZ(O$c?2i#zJG=ts~mIG+vunQ|(4^#~+tcmr~0^q{7wL4l# zrhJCrXC|^_e+(r{fi=sH7D`Z2D{oR!SNeAPwsNVPg0jsZ1n9GmJ#qMnQClEo;lRd_ zDUUOi4okpsLix_NflZ<-OsQwT=P@p|CMa71X;O2rI+(vGpucTk69^Vs(~#{oLr_z4 zO-iZKCd?MjTszo4WEt%7)kKphc?!q}83mV3rG8Usz_f5+`yY}_e^gSY%#r(t@8|Ls zjpYZ@*ZfbpELA(P0&oworO%3H0 zdDfhG^ze2lkjXsq@bJT)ErImg+-fL~J{!z;?ZK4tsFX_1xU9|fYjcCzyosthuBz#H z(NO0Li!P^?`P0gRX_iZAP(oE^R7a)fg^Wel+)>5LIp}! z!W7^+Af)J*z`8DZoU((DFZyL0V=G~&Xf}f~^B1E82?rJ{bb;Yalqq7q&W!3Y5RzFV zqfX=z8%L+<10FrdsHP>p3-gpVnbC!z)SP-CzWN4~m_chleqyqV^{_L~meM5-5{=+k*+?H@pc$2__6x z!m^~tl|?-djVVTxP=Zk~8A6^3mP-R;QPu}oFp@2qY~qj1KPN72 zPF$_EE-iZ}q|4<~XraEKQ1vIfk`vZ+L97=2x_M~w;%HLlp~;V<(L?@bR|@o*+NFcA zz@>*eDM*P1H_E8N&w!$kf4(H6dOn$_KWFzt;&JoW?uR1W0}{Ud=Jr71GW^Z;K#x4u z1M_HG#ptV>Wn2QEA@DasZY%f#f1|qoR7jo3m|fx!4V*5~Umx*fO3#)F-vw-(S~wpa z_EfU;r#_Jio=W&P&Qqz}xkZrs!JrlIrbO&XV9Ela4FvC*^B2G$M%%@{hFC7B6 zK`eNIj5ztx^B|Igus?m_#F|ZD0)lr_?hlYa3dWVAha$($Lt=1qLQ&v`|K`t-%M(%q za4^TS9{b=nnBz+VG9q}7Be@bdJ^e8eizhFV>0|AD<#P6$tS0 z1~ihHpdFC%n2}UJhaK-!4F!wS$Pn|?~}W*9gu(T=6?@kaU5e^aa>5|MAAoQA-*skU*yxL z1smqeYLXYIVAv|ACPc)Xh+gJFLUnwXLthFtz{=Ol-x?8#8iwf9;WuZ_43Zvzv@FKU zMELGIsdRGYs=c1qOPu-^pw6cK~mLW!@*E(-J-Lx4>X7nXxVnbZ260wt?5 z;}+oyfXsR5t(awZ{QQH>Bv`jtJAXu%vfSSlZsrz}^7( z>ca~19blcOtvlA#>;U4u7y>(*si9@(T*NAuMRF1R`{1dSv4L9Fwx4|glHG;0o_#QY zLF*~KN$XiXuJ}jwuho|txE&7#Zr{bNewa&b4a$BLl{L}w`UykUNW*Z$Xs`E)fMJze z8Pe#679L(bYW3{%=PkNoG>vQ=-Z=VzR~axacCWv(w9Y4gZ}WKSFItA2*rHYv$XMvD z{W9aWOY%!?|D<$p1f>LwZJooNo&{sNfN3@O)dR~4zh;3a-CN-=sEzT|1dL08XB}9z z-s>A*^NS}&OHP-cDEHhO$gc2Ozsz28sg}F*u79d`Z<#P=k8B>^Jlf~22^g#0s;G?4 zQ-{=P?(8T<8}%W5j=LZ37UYHU%0?fIQWA?Hnk3IQ0X_?GYUb!NzouX!EB|!%iEPhS zaLCW91qZ~eLQjMD$APTH*C~B+!?1ja9;yj}X105HH&j>>FjWjmLm62kj}Jf2?#{c5j+@op%{mSmW2% z4CzAfx@Bz1SP55H@7LE4=_ayrPMc1cICBk{{IcqX6u&oQg^Y#Z+3zgQ|DhcEaYBds(okjY>nM6>r+k*v66pkS<+2stK7(-c+4YdCR~=XkG^NnWf$h zV_U`=$ED*<{yWb$x+OW&LZbF;wk$V`=eWN{oZKb;<1kXCJ z-D~CYmio0z-7OOYHz(CiuX8v5J|!L6L~Nb~MNr+;yXm3zq3rzAmJ=4wdg$it>Y=7k zVcDCOGZybQu+$c=8rl-dEjYdD#3oM%G)Ha?&>IWANn;vsKlF&y=NxaoRC}q6E4|Ba zybJ24LQJ8$srg)CtzTaY4No>vfRE|;rtuYA`u3pmj!?=1PPstXLs2gpDjP4hj~86n z?Yo~#Z|3g27j2@rFO*t+);_lQoyWb8aw&CRDeLf-iu&(vpt*Z&vu5*3>OZekZCNS( zt-Nweo%FYhX>?yFhcKK(qyUUg05!Ri;0LS8J3&JFbJgS@>LCg?$utoBP+oH`P^n?+ zKo?Ezm#L5k8`?-L!tf*xg-raWSV~9*>VhKPXDd!orefAbP-B;tA~#9{#)25iM%Id1Pykg z76*ZE>hLFk7$Y0;!S@Iic0h_8e3!(K9fa72Xz;_U2akd_Ldeuu7VIlmSN*_CjTwJE zFhtUuo#-q7dSIA!!EcrG^6qhL?n(+Iaf-;I}&D52+2e# z$r3LmA+d;UK>;$xvThV0nSwopf)W%UL5&qfBiOm{B6@*Dqj>QLyFW>7%swJH)gl@! z*r0g_4IsFPG-H@0&DdK}O7;X$okMa=1wn6(H_I{pJ9c~_9pCVUmfQ6(w;J{*{wOGG z`-3J8P1(A`I@tCWR91#kN~1Epygrnb`s@<#-gj1dtGW7ZTwY61 zxqULH*t5gi7Rb2`Rv*=R`tTNa(~v2Yk~Vbbb9-TVx~wnp>r1%Ol|g+YENPdu1%7RT zXER#Z#+Gn}bwO>t_&Qj)!q*%qZ2C&OE~-MzVAV-8jbsgHjjjr$f{7Uw5pV2U`^A#+ zy`QffujY)%?Io0w?oPgW?WqIzm^Bv^WBa)D+b=CeOV1tiEj@OmZ~U5GyRfO8`n+7V zu3Y;0lFD@@(l7F9xc{O=4&mRCS!g5v`a3fFcVu=RjQDR37yLUiD}2=;p#DF>#{cG# z*>9l*v1Io>$ZTx4K^!s*;1pm#Ge0|c_4P4u9EcqIfNb;@u*RzcPZ8XfIMq#^d>`0M zy?+3?Z5%?*nL?6WBM=OFlAoXk8+vTTjR$y%{@yp=r( zEWSiy=6(>!6Z)~QAK(3toxkqn?tGBj^+=$(om)wOPC&^r zjo^hi)ChM4g`vU*`OJbx4{$7w)>p)gYSU> z@bJ~+=fX+om;y+Rcj1vJi##lW6RXxj{RnttLU%|e=y-ce&X)g?_s^?4-~ld<+4Uz1~L?dqUQpip47*@CU_CR7Q9Nq;x3&5I( z&*2Qq+k2fo4szy}Ve#@MHMcFVU0Smg8nJG1ZCza*2GklG8_fy&^SRUD0APklyrB?G zo20kNDIbKsf$ZzV+jRLuQ_$&q=m;_di&dJZe&xnqpw3))tf@9fPJaa64E%mWoB_{> zMFcpbB|sH-V3H7z1U)CRSM!OnsS{^UsL5+#ZzATjwAxHgADMdkVC30f3Pr|7Z(L*^ z6h)&AoJZpav%m+p9ByVjSP2Oc-Uw?a+VTMkz|@QGMCoB42=eD5_~8n4hs14smMWZ>!WFK#A(a>ucz=zswXW3j6Hhm%UmBFv&Ph6HsPL{GOCC?U zl*g^UKVW!ZV5?Y2i?`C7$LZ^XvgME}y}(oNy~|V085R$0y^>xPQW}T4JssSlCaz>1 zWNA_iw8WUFRl;#ql`A=YV^FqgLfp4EOV&W^C6+b*3v@LcAx$$>7u8S2&MFr7i-7FV zFNs^ru_SR;F06gEX!{UDqYtd;qQB{rq9g!& zTN0;>JBA|8+$HX+0plAW#QY%;=-`V)c`L!cOoens>;;$v>$aFLn8^x|nGDuaaeoTz zThTJIo7zG2;1uda!O98dK9I=3ge4)(BM{zYd)i$^{q8*Zfu&3Yy^afAyI+PBMv+A<8?rC#88m4!X zUG3O5bQF=@4FQ~EfJ+)9w4*U9PTgeY&Pgk2kkc0hWyMzvxuf^NhW6{grs8mtn|3ed%{xQZ(KVxOCz?iUJrd7SPbp`#e5C|W ze4vA=J=0EeG9wg()8%=j-j1MZ87j#NUqMKzJ*GOW8Y&$v@Rs;!w<@S?Km}S6D^OZ7 zS5nIrFXQxeL0LVX*#1<2ij3;=-JeW|7%}_Kj#l;4X-yf@&x%Y@;= z@y4T)WP0Uwa|OLR`VhT_rqew4c#FOEw=8E`qZE4f8GX$7l1tL@`VV{j%eURYFQXeJ zC3M#4J)UBZ-P`2Zcc$9EU~!a!cVmoi33!;)zuR*IJ;D-vJ1`k59<%$J#`e8i9i`Co z_&t}3F4-?F{Pn{(@N=|T@&HZOdl>JMF{!uyOiz?T&pyKq{2SdTDS%jGEFQe0q7=OI z72QBT(T((Wp!eQ0Ry1ZGTlm()Q3^eeOK;%c>$@c_kn8nFBpGyCw1$#t-F;t5vtX#n NRZ$fs%f`+5e*hpDCxrk2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..445d6cb60c3e34cd7c93254f1c6be52aa5a03ea3 GIT binary patch literal 4668 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ad272c0bdeb0aa029611a4da2f5b7e9027e50000 GIT binary patch literal 25831 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 N7d_42ZRX{^{}1)7%NGCu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1812d548b7f4ccc7e36688603e475e95ed329f59 GIT binary patch literal 18212 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ed0a250d8a76bca3509b90654fbb3f475b7f8fe GIT binary patch literal 22178 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 t6G=nw)BdE^C^G-A#cfa&UuDo8-C$+ZY 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"