端到端实测(/b 脚本按 AecRxBase -> CoreCommand -> cad_mcp_frame 顺序加载)
暴露两个真问题:
1) 版本校验拿到垃圾值,15/16 个工具被误踢
原实现 GetProcAddress(hMod,"acrxGetApiVersion") 转函数指针调用,
和基座自己的 acrxGetApiVersion() 比数值。实测两边返回值是乱的
(1507328 / 2052390912 / 0 交替,其中 1507328=0x170000 才是正确的 23.0),
结果只有第 3 个工具加载成功,其余全被判 SDK 版本不匹配。
改为插件导出数据符号 g_mcpPluginApiVersion=(ARX<<16)|SUB_ARX
(mcp_plugin_api.h 新增 EXPORT_MCP_PLUGIN_VERSION(),每个插件 DLL 调用一次),
基座 GetProcAddress 读内存比对,不涉及调用约定;旧插件没有该符号时只告警按兼容处理。
2) 加载期 AfxMessageBox 会卡住主线程
mcp_ConfigParser 里的几处弹窗全部换成 McpLog:
同时写 %TEMP%\mcp_plugin_load.log 和命令行,并补上每个工具的
[try]/[skip] 明细(dll 是否存在、版本、create/destroy 指针)。
这也是"看起来只加载 3 个工具"的原因——弹窗把主线程挡住了。
实测结果(R230,自动化烟测):
端口 127.0.0.1:8080 6 s 内监听
tools/list 返回 18 个工具(2 lisp_inline + 16 c++_dll)
5 KB 大请求 -32601,证明整包被读完(旧实现会截断)
tools/call get_layers 真正执行插件,返回 {"count":1,"items":[{"name":"0",...}]}
加载日志 16 个 c++_dll 全部 [try] 成功,0 个 [skip]
另外把配置文件加入构建部署:copyarx.exe 能拷任意文件,照 set_debug_post_build 的写法
再加一条 POST_BUILD,把 cad-mcp-frame.json 也拷到部署目录(ZWCAD 走 zw_output_path.json)。
不部署的话 frame 起来后 LoadAndRegister 找不到配置会直接自关。
验证:R230 与 Z2026 的 frame + plugins 均编译通过并部署;R230 已做上述端到端实测。
168 lines
4.7 KiB
C++
168 lines
4.7 KiB
C++
#include "KTCad.h"
|
|
#include "mcp_plugin_api.h"
|
|
#include <Windows.h>
|
|
#include "tchar.h"
|
|
#include "rxregsvc.h"
|
|
#include <gdiplus.h>
|
|
#pragma comment(lib, "gdiplus.lib")
|
|
|
|
#ifdef ZWCAD
|
|
// 中望的库是 ZwRx.lib,符号也换成了 zcrx 家族(acrxEntryPoint 会被 SDK 宏改名,
|
|
// 但显式写出来的 GetApiVersion 不会,所以这里按平台分开)。
|
|
#pragma comment(linker, "/export:zcrxGetApiVersion,PRIVATE")
|
|
#else
|
|
#pragma comment(lib, "rxapi.lib")
|
|
#pragma comment(linker, "/export:acrxGetApiVersion,PRIVATE")
|
|
#endif
|
|
|
|
EXPORT_MCP_PLUGIN_VERSION()
|
|
|
|
namespace
|
|
{
|
|
// 辅助函数:Base64 编码
|
|
std::string Base64Encode(const unsigned char *data, size_t length)
|
|
{
|
|
static const char encoding_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
size_t out_len = 4 * ((length + 2) / 3);
|
|
std::string ret(out_len, '\0');
|
|
size_t i;
|
|
char *p = const_cast<char *>(ret.c_str());
|
|
|
|
for (i = 0; i < length - 2; i += 3)
|
|
{
|
|
*p++ = encoding_table[(data[i] >> 2) & 0x3F];
|
|
*p++ = encoding_table[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)];
|
|
*p++ = encoding_table[((data[i + 1] & 0xF) << 2) | ((int)(data[i + 2] & 0xC0) >> 6)];
|
|
*p++ = encoding_table[data[i + 2] & 0x3F];
|
|
}
|
|
if (i < length)
|
|
{
|
|
*p++ = encoding_table[(data[i] >> 2) & 0x3F];
|
|
if (i == (length - 1))
|
|
{
|
|
*p++ = encoding_table[((data[i] & 0x3) << 4)];
|
|
*p++ = '=';
|
|
}
|
|
else
|
|
{
|
|
*p++ = encoding_table[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)];
|
|
*p++ = encoding_table[((data[i + 1] & 0xF) << 2)];
|
|
}
|
|
*p++ = '=';
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// 辅助函数:获取 GDI+ 图像编码器的 Clsid (用于转 PNG)
|
|
int GetEncoderClsid(const WCHAR *format, CLSID *pClsid)
|
|
{
|
|
UINT num = 0, size = 0;
|
|
Gdiplus::GetImageEncodersSize(&num, &size);
|
|
if (size == 0) return -1;
|
|
Gdiplus::ImageCodecInfo *pImageCodecInfo = (Gdiplus::ImageCodecInfo *)(malloc(size));
|
|
if (pImageCodecInfo == NULL) return -1;
|
|
Gdiplus::GetImageEncoders(num, size, pImageCodecInfo);
|
|
for (UINT j = 0; j < num; ++j)
|
|
{
|
|
if (_tcscmp(pImageCodecInfo[j].MimeType, format) == 0)
|
|
{
|
|
*pClsid = pImageCodecInfo[j].Clsid;
|
|
free(pImageCodecInfo);
|
|
return j;
|
|
}
|
|
}
|
|
free(pImageCodecInfo);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
class mcp_Tool_ViewportScreenshot: public mcp_Tool
|
|
{
|
|
public:
|
|
virtual nlohmann::json Execute(const nlohmann::json &args) override
|
|
{
|
|
// 1. 获取 AutoCAD 当前文档的绘图区句柄
|
|
HWND hWnd = adsw_acadDocWnd();
|
|
if (!hWnd)
|
|
return mcp_Tool_Utility::make_error(-32000, L"无法获取CAD绘图区窗口句柄");
|
|
|
|
// 2. 初始化 GDI+
|
|
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
|
|
ULONG_PTR gdiplusToken;
|
|
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
|
|
|
|
std::string base64Result;
|
|
bool success = false;
|
|
|
|
// 3. 开始 Windows GDI 抓屏逻辑 (使用花括号限定生命周期)
|
|
{
|
|
RECT rc;
|
|
GetClientRect(hWnd, &rc);
|
|
int width = rc.right - rc.left;
|
|
int height = rc.bottom - rc.top;
|
|
|
|
HDC hdcScreen = GetDC(hWnd);
|
|
HDC hdcMem = CreateCompatibleDC(hdcScreen);
|
|
HBITMAP hBitmap = CreateCompatibleBitmap(hdcScreen, width, height);
|
|
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hdcMem, hBitmap);
|
|
|
|
// 将屏幕内容块拷贝到内存位图中
|
|
BitBlt(hdcMem, 0, 0, width, height, hdcScreen, 0, 0, SRCCOPY);
|
|
|
|
// 将 GDI 位图转换为 GDI+ 位图
|
|
Gdiplus::Bitmap bmp(hBitmap, NULL);
|
|
|
|
// 创建内存流,将位图以 PNG 格式保存到内存,而非硬盘
|
|
IStream *pStream = nullptr;
|
|
if (CreateStreamOnHGlobal(NULL, TRUE, &pStream) == S_OK)
|
|
{
|
|
CLSID pngClsid;
|
|
GetEncoderClsid(L"image/png", &pngClsid);
|
|
|
|
if (bmp.Save(pStream, &pngClsid, NULL) == Gdiplus::Ok)
|
|
{
|
|
// 从内存流中读取二进制数据
|
|
STATSTG stat;
|
|
pStream->Stat(&stat, STATFLAG_NONAME);
|
|
ULONG size = stat.cbSize.LowPart;
|
|
|
|
std::vector<BYTE> buffer(size);
|
|
LARGE_INTEGER liZero = {};
|
|
pStream->Seek(liZero, STREAM_SEEK_SET, NULL);
|
|
|
|
ULONG bytesRead;
|
|
pStream->Read(buffer.data(), size, &bytesRead);
|
|
|
|
// 转换为 Base64
|
|
base64Result = Base64Encode(buffer.data(), size);
|
|
success = true;
|
|
}
|
|
pStream->Release();
|
|
}
|
|
|
|
// 清理 GDI 资源
|
|
SelectObject(hdcMem, hOldBitmap);
|
|
DeleteObject(hBitmap);
|
|
DeleteDC(hdcMem);
|
|
ReleaseDC(hWnd, hdcScreen);
|
|
}
|
|
|
|
// 4. 关闭 GDI+
|
|
Gdiplus::GdiplusShutdown(gdiplusToken);
|
|
|
|
// 5. 按照 MCP 多模态协议返回数据
|
|
if (success)
|
|
{
|
|
return mcp_Tool_Utility::make_mixed_result(L"已成功截取 AutoCAD 当前视口画面。", base64Result, "image/png");
|
|
}
|
|
else
|
|
{
|
|
return mcp_Tool_Utility::make_error(-32001, L"截图或图像编码失败");
|
|
}
|
|
}
|
|
};
|
|
|
|
EXPORT_MCP_TOOL(mcp_Tool_ViewportScreenshot, CreateViewportScreenshotTool)
|
|
|
|
|