docs: 增加 cad_mcp_frame 的移植说明与代码审查 #108

Merged
gjm merged 5 commits from company-desktop/company-desktop-computer/feat-cad-mcp-frame AGit into main 2026-10-29 16:00:03 +08:00
Collaborator

移植已完成(R230 ARX + Z2026 ZRX 均编译通过并部署),本文记录:

  • 移植做了哪些改动(cadsdk.h->KTCad.h、两个单头依赖、SUB_ARX、中望 .def 与版本号函数)
  • C++11+ 限制:R180/R190 是 C++98,无法编译 nlohmann/json,故按 preset 条件跳过
  • 架构:后台线程收包 -> PostMessage -> 主线程执行,工具调用全在主线程,避开了跨线程访问数据库
  • 问题清单(按严重度):
    P0 收包只 recv 一次、缓冲固定 2048:大报文截断、TCP 分包即失败,且没有长度前缀
    P0 bytesReceived<=0 时 socket 泄漏
    P1 socket/bind/listen 返回值全未检查,端口占用时静默失败但 g_bRunning 仍为 true
    P1 无鉴权:本机任意进程都能调用会改图的工具
    P1 端口 8080 硬编码;代码找 mcp_config.json 而仓库里是 cad-mcp-frame.json
    P2 卸载时挂起的 PostMessage 任务不释放;LISP 工具是异步 fire-and-forget 无结果回传;
    加载期弹 AfxMessageBox 会卡住 CAD 启动;g_hMsgWnd 无同步;
    .def 的 LIBRARY 名与实际输出不一致;/Zc:forScope 语义与 code2026 全局设置相反
  • 移植后仍需处理:统一配置文件名、移植 cad_mcp_plugins、带上 lisp 目录、在 CAD 里实测
移植已完成(R230 ARX + Z2026 ZRX 均编译通过并部署),本文记录: - 移植做了哪些改动(cadsdk.h->KTCad.h、两个单头依赖、SUB_ARX、中望 .def 与版本号函数) - C++11+ 限制:R180/R190 是 C++98,无法编译 nlohmann/json,故按 preset 条件跳过 - 架构:后台线程收包 -> PostMessage -> 主线程执行,工具调用全在主线程,避开了跨线程访问数据库 - 问题清单(按严重度): P0 收包只 recv 一次、缓冲固定 2048:大报文截断、TCP 分包即失败,且没有长度前缀 P0 bytesReceived<=0 时 socket 泄漏 P1 socket/bind/listen 返回值全未检查,端口占用时静默失败但 g_bRunning 仍为 true P1 无鉴权:本机任意进程都能调用会改图的工具 P1 端口 8080 硬编码;代码找 mcp_config.json 而仓库里是 cad-mcp-frame.json P2 卸载时挂起的 PostMessage 任务不释放;LISP 工具是异步 fire-and-forget 无结果回传; 加载期弹 AfxMessageBox 会卡住 CAD 启动;g_hMsgWnd 无同步; .def 的 LIBRARY 名与实际输出不一致;/Zc:forScope 语义与 code2026 全局设置相反 - 移植后仍需处理:统一配置文件名、移植 cad_mcp_plugins、带上 lisp 目录、在 CAD 里实测
company-desktop added 3 commits 2026-10-28 23:20:16 +08:00
把这条线的结论收成一个文档,避免散落在多个 PR 的分析里。

内容:
- 优化前后数字:开图墙钟 113 s -> 93 s,截面恢复 22.4 s -> 9.4 s,
  开图期回写 1140 -> 0,临时文件 6773 -> 1733
- 剩下时间的归属(优化后 81,046 采样):
  base_ViewportDraw 之下 74.6%,Parasolid 67.5%,
  且 Parasolid 的 93.4% 发生在我们的视口绘制里;我们的 arx 自耗时 <1%
- 结论:剩余成本是"构件数量 x 单构件建模成本",
  既不是 CAD 自己在拖时间(绘制之外只剩 6.6%),也不是我们的代码慢(<1%)。
  一次开图要建 1335 个 AcDbX3dSolid + 1248 个 AecDbX3dPart,计数器验证每个只建一次。
- 还能动的方向按性价比排序:减少要建的几何(LOD)、几何跨会话持久化(Phase 3)、
  用基本体替代扫掠、截面再优化(已判定不做,收益 <0.5%)
- 诊断方法与踩过的坑:采样算不出次数必须用计数器;加密构建下函数级符号化不可靠
  (GuardBar::Draw3d 报 27% 实为误判);VSDiagnostics 本机会卡死、wpr -stop 有 COM bug,
  改用 xperf on/stop/merge;RVA 必须用 PDB 解析再下结论;计数器别写进同一份报告文件

验证:纯文档,数据来自 tools/run_acad_perf.ps1 + tools/perf_diagsession.py 的实测。
源工程在 d:\new-start\CadProject\src\cad_mcp_frame(vcpkg 独立工程),
本次按 code2026 的 preset/CMake 体系移植到 SourceCode/Code2026/cad_mcp_frame。

移植改动:
- 源码 + .def + .rc + res/ + cad-mcp-frame.json 拷入 cad_mcp_frame/;
  插件接口头 mcp_plugin_api.h 放到 cad_mcp_frame/inc/
- cadsdk.h -> KTCad.h(两者除尾部 4 个 include 与 using namespace MemOper 外完全一致)
- nlohmann/json 单头放 SourceCode/dep_inc/nlohmann/json.hpp,
  ghc::filesystem 单头放 SourceCode/dep_inc/ghc/filesystem.hpp;
  target_link_objectarx 已把 DEP_INC_ROOT 加进 include 路径,故无需额外配置
- 新增 cad_mcp_frame/cmakelists.txt:按 PipeHangerCmd 的写法
  (add_def_file_name / target_set_name_and_suffix .arx / target_link_objectarx /
  target_include_directories inc / set_debug_post_build)
- 补 SUB_ARX 宏:原工程由 CadProject 的 cmake 注入,code2026 只注入 ARX;
  在模块自己的 CMake 里补,不动共享 functions.cmake(中望给 0)
- 顶层挂载 add_custom_module_2(cad_mcp_frame CadMcpFrame "SH/Other"),
  并加条件:nlohmann/json 与 ghc::filesystem 都需要 C++11+,
  而 R180/R190 是 C++98,故只在 (IS_ZWCAD OR CAD_SDK_VERSION >= 20) 时编译
- 中望适配两处:_zrx.def 要导出 zcrxEntryPoint/zcrxGetApiVersion;
  代码里显式声明的 acrxGetApiVersion() 不会被 SDK 宏改名,
  改为 #ifdef ZWCAD 取 zcrxGetApiVersion()

验证:
- R230 Debug 编译通过,cad_mcp_frame.arx 818 KB,自动部署到
  c:\ShangHuaSoft\Envi3D-2026-M\R230\
- Z2026 Debug 编译通过,cad_mcp_frame.zrx 818 KB,自动部署到 ...\Z2026\
- R190/R180 按条件跳过(C++98 无法编译 nlohmann/json)
- 所有 .cpp/.h 均为 UTF-8 with BOM;cmakelists.txt 保持 ASCII 无 BOM

未做:没有在 CAD 里加载实测;配置文件仍是 cad-mcp-frame.json,
而代码里找的是 mcp_config.json,加载前需要统一(详见 cad_mcp_frame-review.md);
插件工程 cad_mcp_plugins 未移植(18 个工具里 16 个依赖它)。
移植已完成(R230 ARX + Z2026 ZRX 均编译通过并部署),本文记录:
- 移植做了哪些改动(cadsdk.h->KTCad.h、两个单头依赖、SUB_ARX、中望 .def 与版本号函数)
- C++11+ 限制:R180/R190 是 C++98,无法编译 nlohmann/json,故按 preset 条件跳过
- 架构:后台线程收包 -> PostMessage -> 主线程执行,工具调用全在主线程,避开了跨线程访问数据库
- 问题清单(按严重度):
  P0 收包只 recv 一次、缓冲固定 2048:大报文截断、TCP 分包即失败,且没有长度前缀
  P0 bytesReceived<=0 时 socket 泄漏
  P1 socket/bind/listen 返回值全未检查,端口占用时静默失败但 g_bRunning 仍为 true
  P1 无鉴权:本机任意进程都能调用会改图的工具
  P1 端口 8080 硬编码;代码找 mcp_config.json 而仓库里是 cad-mcp-frame.json
  P2 卸载时挂起的 PostMessage 任务不释放;LISP 工具是异步 fire-and-forget 无结果回传;
     加载期弹 AfxMessageBox 会卡住 CAD 启动;g_hMsgWnd 无同步;
     .def 的 LIBRARY 名与实际输出不一致;/Zc:forScope 语义与 code2026 全局设置相反
- 移植后仍需处理:统一配置文件名、移植 cad_mcp_plugins、带上 lisp 目录、在 CAD 里实测
company-desktop added 1 commit 2026-10-28 23:28:02 +08:00
P0 收包协议(大请求被截断 + 分包即失败)
原来只 recv 一次、缓冲固定 2048 字节。改为:
- IsJsonComplete():按 {} 与 [] 配平判断报文完整性(跳过字符串内与转义字符)
- RecvOneRequest():select + 循环 recv 追加,单次等 5s、整请求 30s、上限 4MB
- SendAll():send 循环到发完(tools/list 应答可能超一个包)
不改客户端协议:客户端仍是 sendall 后等响应断开。

P0 socket 泄漏
recv<=0 的分支原来什么都不做。现在收包失败/超时回一个 JSON-RPC 错误帧并 closesocket;
accept 失败直接 continue(原代码对 INVALID_SOCKET 也调 closesocket)。

P1 监听失败不再静默
socket/WSASocket/bind/listen/CreateWindowEx/std::thread 全部检查返回值,
监听 socket 改在主线程创建,失败时 init_mcp_server() 返回 false 并打印 WSAGetLastError,
端口被占用时能看到 "Fatal: bind 127.0.0.1:8080 failed, err=10048"。

P1 端口与配置文件名可配
- server.port 可配(默认 8080)
- 配置先找 mcp_config.json,再找 cad-mcp-frame.json(此前代码只找前者,
  而仓库里带的是后者,导致加载即自关)
- 启动日志打印实际端口与 token 是否启用

P1 可选 token 鉴权
配置写了 server.token 就要求请求根级带 token,不匹配返回 -32001 且不执行工具;
没配 token 保持原行为,不破坏现有客户端。

顺带 P2:close_mcp_server() 先排空挂起的 WM_MCP_EXECUTE_TASK 并 delete TaskData
(原来会漏 socket + 内存),再销毁窗口。

验证:R230 与 Z2026 Debug 均编译通过(818->826 KB,已部署)。
未做:没有在 CAD 里实际跑 tools/list 与 tools/call 验证协议改动。
company-desktop force-pushed company-desktop/company-desktop-computer/feat-cad-mcp-frame from b14ed8c5b8 to b9c4ce56f9 2026-10-28 23:31:45 +08:00 Compare
company-desktop added 1 commit 2026-10-29 00:16:37 +08:00
插件工程在 CadProject/src/cad_mcp_plugins(8 个 cpp/h),本次一并整合进 code2026。

移植改动:
- 源码拷到 SourceCode/Code2026/cad_mcp_plugins/;cadsdk.h -> KTCad.h
- 输出是普通 DLL(.dll),由 cad_mcp_frame 运行时 LoadLibrary,不注册 ARX 命令
- 新增 cad_mcp_plugins/cmakelists.txt:按 code2026 惯例
  (target_set_name_and_suffix ".dll" / target_link_objectarx / 预编译头 / SUB_ARX)
- mcp_plugin_api.h 从 cad_mcp_frame/inc 上移到 SourceCode/Code2026/inc:
  frame 与 plugins 共用,且在 ShsGlobalIncludes 的 include 路径里两边都可见;
  frame 的 CMake 去掉已删除的本地 inc 目录
- 顶层在同一个 C++11+ 条件里挂载 add_custom_module_2(cad_mcp_plugins CadMcpPlugins)
- 中望适配:源码里的 #pragma comment(lib,"rxapi.lib") 在中望下没有这个库(叫 ZwRx.lib),
  该 pragma 与导出名都按 #ifdef ZWCAD 分开(中望导出 zcrxGetApiVersion);
  frame 里 GetProcAddress 查插件版本号的函数名同样按平台分

验证:
- R230:cad_mcp_frame.arx 826 KB + cad_mcp_plugins.dll 866 KB,已自动部署
- Z2026:cad_mcp_frame.zrx 825 KB + cad_mcp_plugins.dll 864 KB,已自动部署
- 配置里 16 个 c++_dll 工具的工厂函数(CreateXxxTool)全部出现在 dll 导出表里
- 所有 .cpp/.h 为 UTF-8 with BOM

未做:没有在 CAD 里实跑。另外配置文件本身没有随构建部署(set_debug_post_build 只拷产物),
需要把 cad-mcp-frame.json(或改名 mcp_config.json)与 LISP 目录放到部署目录,
否则 frame 起来后 LoadAndRegister 失败会直接自关。详见 cad_mcp_frame-review.md。
company-desktop added 1 commit 2026-10-29 06:33:21 +08:00
端到端实测(/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 已做上述端到端实测。
gjm merged commit 01b2099437 into main 2026-10-29 16:00:03 +08:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gjm/envi-code#108