Files
envi-code/SourceCode/Code2026/inc/mcp_plugin_api.h
T
company-desktop c25f3edb23 fix: 修 MCP 插件的版本校验与加载期弹窗,18 个工具全部可用
端到端实测(/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 已做上述端到端实测。
2026-10-29 06:33:19 +08:00

169 lines
4.9 KiB
C++

#pragma once
#include <string>
#include <nlohmann/json.hpp>
class mcp_Tool_Utility
{
public:
static std::wstring utf8ToWString(const std::string &utf8Str)
{
if (utf8Str.empty()) return std::wstring();
// 第一次调用获取需要的宽字符缓冲区大小
int sizeNeeded = ::MultiByteToWideChar(CP_UTF8, 0, &utf8Str[0], (int)utf8Str.size(), NULL, 0);
std::wstring wstrTo(sizeNeeded, 0);
// 第二次调用真正进行转换
::MultiByteToWideChar(CP_UTF8, 0, &utf8Str[0], (int)utf8Str.size(), &wstrTo[0], sizeNeeded);
return wstrTo;
}
static 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;
}
// 1. 纯文本响应
static nlohmann::json make_text_result(const std::wstring &_text)
{
std::string text = WStringToUtf8(_text);
return {
{ "result", {
{ "content", {
{ { "type", "text" }, { "text", text } }
} }
} }
};
}
// 2. 图片响应 (传入 base64 编码的图片数据)
static nlohmann::json make_image_result(const std::string &base64Data
, const std::string &mimeType = "image/png")
{
return {
{ "result", {
{ "content", {
{ { "type", "image" }, { "data", base64Data }, { "mimeType", mimeType } }
} }
} }
};
}
// 3. 图文混合响应 (文字汇报结果 + 附带 CAD 视口截图)
static nlohmann::json make_mixed_result(const std::wstring &_text
, const std::string &base64Data
, const std::string &mimeType = "image/png")
{
std::string text = WStringToUtf8(_text);
return {
{ "result", {
{ "content", {
{ { "type", "text" }, { "text", text } },
{ { "type", "image" }, { "data", base64Data }, { "mimeType", mimeType } }
} }
} }
};
}
static nlohmann::json make_error(int code, const std::wstring &_message)
{
std::string message = WStringToUtf8(_message);
return {
{ "error",{
{ "code", code },
{ "message", message }
} }
};
}
};
class mcp_Tool
{
protected:
nlohmann::json m_config;
std::string m_abiBuffer; // 存在于对象实例自己的堆内存中,确保指针生命周期安全
public:
virtual ~mcp_Tool() = default;
void SetConfig(const nlohmann::json &config) { m_config = config; }
// 兜底三剑客:当 JSON 中不写这些字段时,才调用这里(均可选重载)
virtual std::string private_name() const { return "unnamed_tool"; }
virtual std::string private_description() const { return ""; }
virtual nlohmann::json private_input_schema() const { return nlohmann::json::object(); }
// 统一由框架接管 Name 的获取 (供路由字典 mcp_Tools::Register 调用)
virtual std::string GetName() const final { return m_config.value("name", private_name()); }
// 统一由框架接管 ToLLM 组装
virtual nlohmann::json ToLLM() const final
{
nlohmann::json tool;
tool["name"] = GetName();
std::string desc = m_config.value("description", "");
tool["description"] = desc.empty() ? private_description() : desc;
if (m_config.contains("inputSchema") && !m_config["inputSchema"].empty())
{
tool["inputSchema"] = m_config["inputSchema"];
}
else
{
tool["inputSchema"] = private_input_schema();
}
return tool;
}
// 整个类唯一一个纯虚函数:全心全意干活!
virtual nlohmann::json Execute(const nlohmann::json &args) = 0;
// 安全配置注入
virtual void SetConfigAbiSafe(const char* config_json_str)
{
// 在 DLL 内部的安全环境中完成字符串到 JSON 的反序列化
m_config = nlohmann::json::parse(config_json_str);
}
// 安全执行路由
virtual const char* ExecuteAbiSafe(const char* args_json_str)
{
// 1. 在 DLL 的上下文中,将传入的 C 字符串转为 JSON
nlohmann::json args = nlohmann::json::parse(args_json_str);
// 2. 调用真实的业务逻辑
nlohmann::json result = Execute(args);
// 3. 将结果序列化为字符串,存入属于该 DLL 的内存缓冲区
m_abiBuffer = result.dump();
// 4. 返回安全的 C 指针
return m_abiBuffer.c_str();
}
};
// 每个插件 DLL 里调用一次:把本 DLL 编译时的 SDK 版本写成数据符号导出。
// 基座用 GetProcAddress 读这块内存即可比对,不涉及调用约定
// (此前用 acrxGetApiVersion 函数指针比对,实测拿到垃圾值,15/16 个工具被误踢)
#define EXPORT_MCP_PLUGIN_VERSION() \
extern "C" __declspec(dllexport) const int g_mcpPluginApiVersion = ((ARX) << 16) | (SUB_ARX);
#define EXPORT_MCP_TOOL(ToolClass, FactoryName) \
extern "C" __declspec(dllexport) mcp_Tool* FactoryName() \
{ \
return new ToolClass(); \
} \
extern "C" __declspec(dllexport) void Destroy_##FactoryName(mcp_Tool* pTool) \
{ \
delete pTool; \
}