diff --git a/cmake/functions.cmake b/cmake/functions.cmake index 4ded680..785bf95 100644 --- a/cmake/functions.cmake +++ b/cmake/functions.cmake @@ -98,6 +98,15 @@ function (get_sdk_version out) set(${out} ${VER} PARENT_SCOPE) endfunction() +function (get_sdk_sub_version out) + IsZWCAD(isZW) + set(VER ${CAD_SDK_SUBVERSION}) + if (isZW) + set(VER "0") + endif() + set(${out} ${VER} PARENT_SCOPE) +endfunction() + # 根据 ZWCAD宏,来判断需要的def文件名称,保存到表_SOURCES 里面 function (add_def_file_name _SOURCES @@ -204,6 +213,7 @@ function(target_link_objectarx target_compile_definitions(${TARGET_NAME} PRIVATE CMAKE ARX=${SDK_VERSION} + SUB_ARX=${SDK_SUBVERSION} ${_EXTENSION} # 指示这是一个 ARX 扩展 ) endfunction() diff --git a/cmakelists.txt b/cmakelists.txt index 5e1179a..bff100b 100644 --- a/cmakelists.txt +++ b/cmakelists.txt @@ -24,6 +24,9 @@ IsZWCAD(IS_ZWCAD) # 如果时中望,取出版本号的后两位,作为宏定义 get_sdk_version(SDK_VERSION) +# 如果时中望,subversion 统一为0,否则取出 subversion +get_sdk_sub_version(SDK_SUBVERSION) + # ThirdPartyDeps里面使用了SDK_VERSION include(ThirdPartyDeps) @@ -136,4 +139,5 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${GLOBAL_LIB_DIR}/Release-${GENERATE message(STATUS ">>> 正在构建业务树...") add_subdirectory(./src/cad_mcp_frame) +add_subdirectory(./src/cad_mcp_plugins) diff --git a/src/cad_mcp_frame/cmakelists.txt b/src/cad_mcp_frame/cmakelists.txt index 90c583c..596b901 100644 --- a/src/cad_mcp_frame/cmakelists.txt +++ b/src/cad_mcp_frame/cmakelists.txt @@ -12,7 +12,7 @@ set(SOURCES "cad_mcp_frame.rc" "mcp_server.cpp" "mcp_tool.cpp" - "mcp_ConfigParser.cpp" + "mcp_configParser.cpp" "mcp_utils.cpp" ) add_def_file_name(SOURCES "${PROJECT_NAME}") diff --git a/src/cad_mcp_frame/mcp_ConfigParser.cpp b/src/cad_mcp_frame/mcp_ConfigParser.cpp index 7a24c7c..a3fa0e7 100644 --- a/src/cad_mcp_frame/mcp_ConfigParser.cpp +++ b/src/cad_mcp_frame/mcp_ConfigParser.cpp @@ -4,6 +4,7 @@ #include "mcp_tool.h" #include "mcp_utils.h" #include "ghc/filesystem.hpp" +#include "rxregsvc.h" #pragma region mcp_ConfigParser bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools ®istry) @@ -50,6 +51,7 @@ bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Too return true; } +extern "C" int acrxGetApiVersion(); void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::wstring &pluginDir, mcp_Tools ®istry) { fs::path dllPath @@ -59,12 +61,53 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std:: if (!fs::exists(dllPath)) { - AfxMessageBox(CString(_T("[MCP Server]Warning: The DLL file does not exist; the associated tools will not load!\n")) + dllPath.c_str()); + AfxMessageBox(CString(_T("[MCP Server]Warning: The DLL file does not exist, the associated tools will not load!\n")) + dllPath.c_str()); return; } HMODULE hMod = LoadLibrary(dllPath.c_str()); - if (!hMod) return; + + + CString verStr; + verStr.Format(_T("Major ver:%d, Minor ver:%d"), ARX, SUB_ARX); + CString ver_errMsg; + ver_errMsg.Format( + CString(_T("[MCP Server]Error: Failed to load the DLL. ")) + + CString(_T("Please check whether the version of the ObjectARX SDK used by ")) + + CString("the DLL matches the current ARX version(%s).\n"), verStr ); + + if (!hMod) + { + AfxMessageBox(ver_errMsg); + return; + } + + // 1. 定义版本校验函数指针 + typedef int (*GetApiVersionFunc)(); + GetApiVersionFunc getVerFn = (GetApiVersionFunc)GetProcAddress(hMod, "acrxGetApiVersion"); + + // 2. 核心拦截逻辑 + if (!getVerFn) + { + // 连这个函数都没导出,说明根本不是合法的 ObjectARX/MCP 插件 + AfxMessageBox(CString( + CString(_T("The ObjectARX version of the DLL could not be determined.")) + + CString(_T("Please ensure that the acrxGetApiVersion function is exported.")) + )); + FreeLibrary(hMod); + return; + } + + // 拿 DLL 编译时的 SDK 版本,和基座当前运行的 SDK 版本进行比对 + int arx_version(acrxGetApiVersion()); + int dll_version(getVerFn()); + if (arx_version != dll_version) + { + // 平台大版本不匹配,强行加载必定闪崩,果断踢掉! + FreeLibrary(hMod); + AfxMessageBox(ver_errMsg); + return; + } typedef mcp_Tool *(*CreateFunc)(); typedef void (*DestroyFunc)(mcp_Tool *); diff --git a/src/cad_mcp_frame/mcp_tool.cpp b/src/cad_mcp_frame/mcp_tool.cpp index 2aadc1d..804ad9d 100644 --- a/src/cad_mcp_frame/mcp_tool.cpp +++ b/src/cad_mcp_frame/mcp_tool.cpp @@ -39,7 +39,7 @@ namespace } } -#pragma region mcp_Tool_LispInline +#pragma region LispInline nlohmann::json mcp_Tool_LispInline::Execute(const nlohmann::json &args) { std::string tpl = m_config["backend"].value("script_template", ""); @@ -55,9 +55,9 @@ nlohmann::json mcp_Tool_LispInline::Execute(const nlohmann::json &args) return mcp_Tool_Utility::make_text_result("LISP inline command issued for execution (asynchronous)"); } -#pragma endregion mcp_Tool_LispInline +#pragma endregion LispInline -#pragma region mcp_Tool_LispFile +#pragma region LispFile nlohmann::json mcp_Tool_LispFile::Execute(const nlohmann::json &args) { fs::path filePath @@ -86,9 +86,9 @@ nlohmann::json mcp_Tool_LispFile::Execute(const nlohmann::json &args) return mcp_Tool_Utility::make_text_result("LISP external file loaded and issued for execution (asynchronous)"); } -#pragma endregion mcp_Tool_LispFile +#pragma endregion LispFile -#pragma region mcp_Tool_DllProxy +#pragma region DllProxy // RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块 mcp_Tool_DllProxy::~mcp_Tool_DllProxy() { @@ -110,5 +110,5 @@ nlohmann::json mcp_Tool_DllProxy::Execute(const nlohmann::json &args) } return mcp_Tool_Utility::make_error(-32603, "Underlying DLL tool instance is invalid"); } -#pragma endregion mcp_Tool_DllProxy +#pragma endregion DllProxy diff --git a/src/cad_mcp_frame/mcp_utils.cpp b/src/cad_mcp_frame/mcp_utils.cpp index 1bab773..8395347 100644 --- a/src/cad_mcp_frame/mcp_utils.cpp +++ b/src/cad_mcp_frame/mcp_utils.cpp @@ -19,6 +19,19 @@ namespace mcp_utils return wstrTo; } + std::string WStringToUtf8(const std::wstring &wstr) + { + if (wstr.empty()) return std::string(); + + // 第一次调用获取需要的 UTF-8 缓冲区大小 + int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); + std::string strTo(sizeNeeded, 0); + + // 第二次调用真正进行转换 + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], sizeNeeded, NULL, NULL); + return strTo; + } + bool mcp_utils::hasFileExist(const std::wstring &filePath) { return _taccess(filePath.c_str(), 0) == 0; } bool mcp_utils::hasDirExist(const std::wstring &filePath) { return _taccess(filePath.c_str(), 0) == 0; } diff --git a/src/cad_mcp_plugins/StdAfx.cpp b/src/cad_mcp_plugins/StdAfx.cpp new file mode 100644 index 0000000..3743d0c --- /dev/null +++ b/src/cad_mcp_plugins/StdAfx.cpp @@ -0,0 +1,6 @@ +// StdAfx.cpp : source file that includes just the standard includes +// StdAfx.pch will be the pre-compiled header +// StdAfx.obj will contain the pre-compiled type information + +#include "StdAfx.h" + diff --git a/src/cad_mcp_plugins/StdAfx.h b/src/cad_mcp_plugins/StdAfx.h new file mode 100644 index 0000000..3fb8896 --- /dev/null +++ b/src/cad_mcp_plugins/StdAfx.h @@ -0,0 +1,50 @@ +#pragma once + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#define WINVER 0x0600 + +#pragma warning(disable: 4786) + +//- 'DEBUG workaround' below prevents the MFC or ATL #include-s from pulling +//- in "Afx.h" that would force the debug CRT through #pragma-s. +#if defined(_DEBUG) && !defined(AC_FULL_DEBUG) +#define _DEBUG_WAS_DEFINED +#undef _DEBUG +#pragma message ("Compiling MFC / STL / ATL header files in release mode.") +#endif + +#pragma pack (push, 8) +#pragma warning(disable: 4786 4996) + +#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers + +#include // MFC core and standard components +#include // MFC extensions + +#ifndef _AFX_NO_OLE_SUPPORT +#include // MFC OLE classes +#include // MFC OLE dialog classes +#include // MFC Automation classes +#endif + +#include // MFC support for Internet Explorer 4 Common Controls +#ifndef _AFX_NO_AFXCMN_SUPPORT +#include // MFC support for Windows Common Controls +#endif + +#include +#include +#include +#include +#include + +extern HINSTANCE _hdllInstance; + +#ifdef _DEBUG_WAS_DEFINED +#define _DEBUG +#undef _DEBUG_WAS_DEFINED +#endif + diff --git a/src/cad_mcp_plugins/cmakelists.txt b/src/cad_mcp_plugins/cmakelists.txt new file mode 100644 index 0000000..cfda590 --- /dev/null +++ b/src/cad_mcp_plugins/cmakelists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.16) + +project(cad_mcp_plugins) + +find_package(nlohmann_json CONFIG REQUIRED) +find_package(ghc_filesystem CONFIG REQUIRED) + +set(SOURCES + "mcp_plugins.cpp" + "stdafx.cpp" + ) +#add_def_file_name(SOURCES "${PROJECT_NAME}") + +add_library(${PROJECT_NAME} SHARED ${SOURCES}) + +target_precompile_headers(${PROJECT_NAME} PRIVATE StdAfx.h) + +target_link_objectarx(${PROJECT_NAME} AECRXBASE_EXPORT) + +target_set_name_and_suffix(${PROJECT_NAME} "${PROJECT_NAME}" ".dll") + +target_compile_options(${PROJECT_NAME} PRIVATE /Zc:forScope) + +target_include_directories(${PROJECT_NAME} INTERFACE "${CMAKE_SOURCE_DIR}/inc") + +target_link_libraries(${PROJECT_NAME} PRIVATE + nlohmann_json::nlohmann_json +) \ No newline at end of file diff --git a/src/cad_mcp_plugins/mcp_plugins.cpp b/src/cad_mcp_plugins/mcp_plugins.cpp new file mode 100644 index 0000000..4637b29 --- /dev/null +++ b/src/cad_mcp_plugins/mcp_plugins.cpp @@ -0,0 +1,76 @@ +#include "cadsdk.h" +#include "mcp_plugin_api.h" +#include +#include "tchar.h" +#include "rxregsvc.h" + +#pragma comment(lib, "rxapi.lib") +#pragma comment(linker, "/export:acrxGetApiVersion,PRIVATE") + +class mcp_Tool_DrawCircleCpp : public mcp_Tool +{ +public: + virtual nlohmann::json Execute(const nlohmann::json &args) override + { + // 1. 从大模型传入的参数中提取坐标和半径 + double x = args.value("cx", 0.0); + double y = args.value("cy", 0.0); + double r = args.value("r", 100.0); + + // 2. 获取当前活动文档 + AcApDocument* pDoc = acDocManager->curDocument(); + if (pDoc == nullptr) + { + return { { "error", { { "code", -32000 }, { "message", "当前没有打开的 CAD 文档" } } } }; + } + + // 3. 极其重要:因为调用发起者是隐式窗口消息,并非标准 CAD 命令,必须显式锁文档! + acDocManager->lockDocument(pDoc); + AcDbDatabase* pDb = pDoc->database(); + + AcDbBlockTable* pBlockTable = nullptr; + Acad::ErrorStatus es = pDb->getBlockTable(pBlockTable, AcDb::kForRead); + + if (es == Acad::eOk) + { + AcDbBlockTableRecord* pModelSpace = nullptr; + es = pBlockTable->getAt(ACDB_MODEL_SPACE, pModelSpace, AcDb::kForWrite); + + if (es == Acad::eOk) + { + // 在底层 C++ 层面构造实体对象 + AcGePoint3d center(x, y, 0.0); + AcGeVector3d normal(0.0, 0.0, 1.0); // Z轴法线 + AcDbCircle* pCircle = new AcDbCircle(center, normal, r); + + // 追加到模型空间 + AcDbObjectId circleId; + pModelSpace->appendAcDbEntity(circleId, pCircle); + + // 释放对象 + pCircle->close(); + pModelSpace->close(); + } + pBlockTable->close(); + } + + // 4. 解锁文档并刷新显示 + acDocManager->unlockDocument(pDoc); + acedUpdateDisplay(); + + // 5. 返回标准 MCP 结果,如果失败可以根据 es 的值返回 make_error + if (es == Acad::eOk) + { + return mcp_Tool_Utility::make_text_result("C++底层接口已成功在模型空间生成圆形!"); + } + else + { + return mcp_Tool_Utility::make_error((int)es, "ObjectARX 内部错误"); + } + } +}; + +// 使用宏导出 C 风格工厂函数,供基座的 LoadLibrary 解析 +EXPORT_MCP_TOOL(mcp_Tool_DrawCircleCpp, CreateDrawCircleToolCpp) + + diff --git a/test_autoagent.py b/test_autoagent.py index e17cd8a..a71c278 100644 --- a/test_autoagent.py +++ b/test_autoagent.py @@ -1,34 +1,64 @@ import socket import json +import time -def send_request(req_data): +def send_request(req_data, silent=False): try: client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("127.0.0.1", 8080)) + + # 发送 UTF-8 编码的 JSON 请求 client.sendall(json.dumps(req_data).encode('utf-8')) + + # 接收并解码结果 response = client.recv(4096) - print(">>> 收到回复:") - print(json.dumps(json.loads(response.decode('utf-8')), indent=2, ensure_ascii=False)) + + if not silent: + print(">>> 收到回复:") + print(json.dumps(json.loads(response.decode('utf-8')), indent=2, ensure_ascii=False)) + print("-" * 50) + client.close() except Exception as e: print(f"连接失败: {e}") if __name__ == "__main__": - print("=== 测试 LISP 外部文件挂载 ===") + print("=== 测试 1: 验证 C++ DLL 工具调用 ===") request_call = { "jsonrpc": "2.0", - "id": 3, + "id": 4, "method": "tools/call", "params": { - "name": "draw_conference_table", + "name": "draw_circle_cpp", "arguments": { - "cx": 1000.0, - "cy": 1000.0, - "length": 1500.0, - "width": 600.0, - "chair_count": 4, - "layer": "MCP_CONF_ROOM" + "cx": 2000.0, + "cy": 2000.0, + "r": 500.0 } } } - send_request(request_call) \ No newline at end of file + send_request(request_call) + + # 稍作停顿,方便观察 CAD 屏幕 + time.sleep(1) + + print("\n=== 测试 2: C++ 并发写入压力测试 (生成10个同心圆) ===") + for i in range(10): + radius = 100.0 + i * 50.0 + req = { + "jsonrpc": "2.0", + "id": 100 + i, + "method": "tools/call", + "params": { + "name": "draw_circle_cpp", + "arguments": { + "cx": 4000.0, + "cy": 2000.0, + "r": radius + } + } + } + # 连续快速发送,不打印详细返回以模拟极限并发 + send_request(req, silent=True) + + print(">>> 并发指令发送完毕,请检查 CAD 屏幕。") \ No newline at end of file