Compare commits

..
5 Commits
Author SHA1 Message Date
gjm 201191dc67 Merge pull request 'fix(mcp_frame): c++_dll 版本校验默认只比大版本,R24.3 单独分组' (#5) from home-lenovo-legion-r9000/home-ai-dev into main
Reviewed-on: #5
2026-10-09 23:44:59 +08:00
gjm 3b1cc88262 fix(mcp_frame): 补齐版本校验源码(上一提交因文件名大小写漏带)
2fa27b1 只带上了文档,未纳入源文件。此处补上 mcp_ConfigParser.cpp 的实际改动:
- acrxGetApiVersion 返回值按 (major<<16)|minor 拆分大/小版本;
- 默认只比大版本(同一大版本内二进制兼容);
- 例外 R24.3 与 R24.0~R24.2 因 MSVC toolset 不同,按 minor>=3 分组判定。
2026-10-09 23:42:31 +08:00
gjm 2fa27b1161 fix(mcp_frame): c++_dll 版本校验默认只比大版本,R24.3 单独分组
- acrxGetApiVersion 返回值编码为 (major<<16)|minor,据此拆分大/小版本
  (实测 R22.0=0x160000、R23.0=0x170000)。
- 同一大版本内二进制兼容(R24.0/R24.1/R24.2 可共用 ARX;2019/R23.0 编的 ARX
  可跑在 2020/R23.1 上),故默认只比大版本,避免为每个小版本各编一份。
- 唯一例外 R24.3:换了 MSVC toolset,与 R24.0~R24.2 不兼容,
  对 major==24 再按 minor>=3 分组判定。
- 同步更新 CODEBUDDY.md 与 ai.memory 说明。
2026-10-09 23:42:05 +08:00
gjm 1c4160fa55 Merge pull request 'chore(memory): 沉淀基座 c++_dll 加载/执行的排查结论' (#4) from home-lenovo-legion-r9000/home-ai-dev into main
Reviewed-on: #4
2026-10-09 22:38:08 +08:00
gjm 0b4ffdec0c chore(memory): 沉淀基座 c++_dll 加载/执行的排查结论
- 新增 ai.memory/project_mcp_frame_dll_loader.md:
  1) 运行时 acrxGetApiVersion() 有状态、不可用于版本比对;
  2) DLL 工具写数据库需文档锁(应用上下文 → eLockViolation);
  3) 同一 DLL 只加载一次(模块缓存)及代理不再 FreeLibrary 的原因。
- 更新索引 MEMORY.md;reference_environment.md 的 c++_dll 说明改为「只加载一次」
  并指向新文件,测试脚本表补 verify_annotate.py。
- 修正 feedback_preferences.md 中 ai.memory「尚未入库」的过时说明。
2026-10-09 22:37:31 +08:00
6 changed files with 95 additions and 9 deletions

No files matched your search

+1 -1
View File
@@ -61,7 +61,7 @@ python agent_loop.py # 完整 Agent 循环(多回合 截图/缩放/结
- **`lisp_inline`**:把 `script_template` 中的 `{占位符}` 替换为调用参数,`sendStringToExecute` 到命令行(异步)。布尔值转 `T`/`nil`。见 `mcp_tool.cpp:43`。
- **`lisp_file`**:先 `(load "文件" nil)` 再执行 `call_template`,`file_name` 相对于配置中的 `lisp_directory` 解析。见 `mcp_tool.cpp:61`。
- **`c++_dll`**:`LoadLibrary` 第三方 DLL。**同一 DLL 全进程只加载一次**(句柄由 `mcp_configParser.cpp` 的模块缓存 `g_loadedDllModules` 持有,`mcp_Tools::clear()` 时统一 `FreeLibrary`),仅**首次加载**时校验 ARX 大版本:用**编译期常量** `(ARX<<16)|SUB_ARX` 与 DLL 导出的 `acrxGetApiVersion` 比对,不匹配则拒绝加载防崩溃。注意运行时的 `acrxGetApiVersion`(`rxapi.lib` 的 `acrxGetApiVersionImpl`)是**有状态实现**,返回值会随调用次数变化,不能用作基座侧版本。取 `factory_function` 与 `Destroy_<factory>` 创建/销毁工具,用 `mcp_Tool_DllProxy` 托管工具对象生命周期(RAII,**不再 FreeLibrary 模块**)。跨 DLL 通过 `SetConfigAbiSafe` / `ExecuteAbiSafe` 传 C 字符串,JSON 序列化全部在 DLL 内部完成。见 `mcp_configParser.cpp`、`mcp_tool.cpp`。
- **`c++_dll`**:`LoadLibrary` 第三方 DLL。**同一 DLL 全进程只加载一次**(句柄由 `mcp_configParser.cpp` 的模块缓存 `g_loadedDllModules` 持有,`mcp_Tools::clear()` 时统一 `FreeLibrary`),仅**首次加载**时校验版本兼容性:`acrxGetApiVersion` 返回值编码为 `(major<<16)|minor`,**默认只比大版本**(同一大版本内二进制兼容,如 R24.0/R24.1/R24.2 可共用同一 ARX);**唯一例外 R24.3**(换了 MSVC toolset,与 R24.0~R24.2 不兼容),对 `major==24` 再按 `minor>=3` 分组。基座侧版本用**编译期常量** `ARX`/`SUB_ARX`。注意运行时的 `acrxGetApiVersion`(`rxapi.lib` 的 `acrxGetApiVersionImpl`)是**有状态实现**,返回值会随调用次数变化,不能用作基座侧版本。取 `factory_function` 与 `Destroy_<factory>` 创建/销毁工具,用 `mcp_Tool_DllProxy` 托管工具对象生命周期(RAII,**不再 FreeLibrary 模块**)。跨 DLL 通过 `SetConfigAbiSafe` / `ExecuteAbiSafe` 传 C 字符串,JSON 序列化全部在 DLL 内部完成。见 `mcp_configParser.cpp`、`mcp_tool.cpp`。
### 插件 ABI(`inc/mcp_plugin_api.h`)
+1
View File
@@ -5,5 +5,6 @@
> 组织方式:本文件是索引(一行一条),各主题内容放在独立文件中。
- [CADProject 图纸审查 Agent 工作](project_dwg_review_agent.md) — 基于 MCP 框架的图纸审查工具集与逐图框审查流程
- [MCP 基座 c++_dll 加载与执行的坑](project_mcp_frame_dll_loader.md) — acrxGetApiVersion 有状态不可比对、写库需文档锁、同一 DLL 只加载一次
- [用户协作偏好](feedback_preferences.md) — 调试用 Debug/R230、模型 deepseek-flash、DLL 集中在 cad_mcp_plugins 等
- [环境与外部资源](reference_environment.md) — 构建/运行要点、CAD SDK 路径、测试脚本
+2 -2
View File
@@ -60,8 +60,8 @@ type: feedback
提交并推送到远程服务器(`cad-agent.git`),以便 AI 在其它机器上接手记忆。
- **Why:** 用户希望记忆可跨机器共享/接管。
- **How to apply:** 不要建议把 `ai.memory/` 加入 `.gitignore`;提交/推送时正常包含它。
截至 **2026-10-08** 用户表示「今天就算了」,故暂未加入/推送——后续时机由用户决定。
- 背景:`CadProject` 已独立为单独仓库(远程 `cad-agent.git`);`ai.memory/` 目前仍是未跟踪状态。
- 现状:`ai.memory/` **已入库**(提交 `chore(memory): 新增 ai.memory 记忆库`),随 `home-ai-dev`
正常提交并推送。(`CadProject` 已独立为单独仓库,远程 `cad-agent.git`。)
## 【重要】新建的 C++ 源文件必须存为 **UTF-8 with BOM**
+61
View File
@@ -0,0 +1,61 @@
---
name: MCP 基座 c++_dll 加载与执行的坑
description: CadProject 基座(cad_mcp_frame)加载/执行 c++_dll 工具的两个非显而易见坑——运行时 acrxGetApiVersion 有状态不可用于版本比对、写数据库需文档锁——及「同一 DLL 只加载一次」的模块缓存决定
type: project
---
# MCP 基座 c++_dll 加载与执行的坑
> 来源:**2026-10-09** 排查「配置 26 个工具、DLL 导出 24 个,却只注册 9 个」的结论。
> 相关提交:`fix(mcp_frame): 修复 c++_dll 工具大量注册失败与写库被文档锁拦截`。
## 坑 1:运行时 `acrxGetApiVersion()` 不能用于版本比对
- **事实**:`acrxGetApiVersion()` 在基座与插件里都是 `rxapi.lib`(成员 `libinit.obj`)的
`acrxGetApiVersionImpl`,**有状态**:它读/写一个进程内全局,并与入参寄存器 ECX 做 XOR 后
回写;首次调用返回硬编码 `0x170000`(即 `(ARX<<16)|SUB_ARX`,对应 R230),之后每次调用
返回值都被污染、随调用次数变化。
- **后果**:AutoCAD 以 ARX 加载基座时**会先调用一次**该函数;此后基座再调用得到的是脏值,
而 plugins DLL(`LoadLibrary` 加载、无人调用过)首次返回正确值 → 两者不等 → 绝大多数 DLL
工具被判为「版本不匹配」而被 `FreeLibrary` 跳过(表现为只注册 9/26)。
- **How to apply**:基座侧版本一律用**编译期常量** `ARX`/`SUB_ARX`;**不要**把运行时
`acrxGetApiVersion()` 的返回值当版本用。实现见 `mcp_configParser.cpp::LoadDllTool`。
### 返回值编码 + 「只比大版本」的兼容判定
- 返回值 = **`(major << 16) | minor`**(大版本=高 16 位,小版本=低 16 位)。实测(反汇编各版本
SDK 的 `rxapi.lib`):R22.0 → `0x160000`(0x16=22)、R23.0 → `0x170000`(0x17=23);
故 R23.1 → `0x170001`,R24.0 → `0x180000`。
- 判定规则:**默认只比大版本**——同一大版本内二进制兼容(如 R24.0/R24.1/R24.2 可共用同一
ARX;AutoCAD 2019=R23.0 编的 ARX 可跑在 2020=R23.1 上)。**唯一例外是 R24.3**:它换了
MSVC toolset,与 R24.0~R24.2 **不兼容**,故对 `major==24` 再按 `minor>=3` 分一次组。
- 若「大小版本全比」,会误拒用同大版本其它小版本 SDK 编的 DLL,逼着为每个小版本各编一份,浪费。
## 坑 2:DLL 工具写数据库必须先锁文档
- **事实**:工具执行链 `TcpServerLoop → PostMessage(WM_MCP_EXECUTE_TASK) → ExecuteTaskInMainThread`
运行在 AutoCAD 主线程的**窗口消息处理**里,此时处于「**应用上下文**」。在应用上下文对数据库
做 `kForWrite` 打开会返回 `eLockViolation`(表现为 `openWriteSpace` 拿不到模型空间,报
`cannot open target space for write`);**读操作不受影响**,所以之前一直没暴露。
- **How to apply**:凡会写库的 DLL 工具都要锁文档。现已在
`mcp_Tool_DllProxy::Execute`(`mcp_tool.cpp`)**统一**加
`acDocManager->lockDocument(pDoc, AcAp::kWrite)` / `unlockDocument`——**一处**覆盖所有 DLL
工具(含 `highlight_entities`),且不波及走 `sendStringToExecute` 的 LISP 工具。
新增写库工具**无需各自加锁**,也**不要**在工具内部重复加锁。
(同级 `envi-code` 工程在对话框里写库也是 `lockDocument(pDoc, AcAp::kWrite)` 这个写法。)
## 架构决定:同一 DLL 只加载一次(模块缓存)
- `mcp_configParser.cpp` 用 `g_loadedDllModules`(规范化路径 → 句柄)缓存:**同一 DLL 全进程
只 `LoadLibrary` 一次**,后续工具复用句柄并**跳过版本校验**;`mcp_Tools::clear()` 时统一
`FreeLibrary`。
- **Why**:一个 DLL 常导出多个工具,原实现每个工具都 `LoadLibrary` + 版本校验,既重复,又放大
了坑 1(每次校验都调用一次有状态的版本函数)。
- **How to apply**:`mcp_Tool_DllProxy` 已**不再**持有/释放模块句柄,只负责销毁 DLL 内的工具
对象。若改回「每个工具各持一个引用」,必须保证 `LoadLibrary` / `FreeLibrary` 次数配平,
否则模块会被提前卸载、代理析构时调用已卸载代码而**崩溃**。
## 诊断日志
- `LoadDllTool` 仅在**首次加载**时写一行 `arx_version=X dll_version=Y` 到
`<模块目录>/mcp_load.log`,每次加载清空重写(`mcpLoadReset`)。属临时诊断,稳定后可删。
+3 -1
View File
@@ -34,6 +34,7 @@ type: reference
| `test_autoagent.py` | 拉一次视口截图存 `test_screenshot.png` |
| `countOfCircle.py` | 伪造 `zoom_window_normalized` 调用,验证 TCP 链路 |
| `agent_loop.py` | 完整 Agent 循环(多回合),需 openai 包与 API Key |
| `verify_annotate.py` | 批注工具运行时验证(绘制/校验/改属性/清理);`--keep` 保留批注 |
## 两个构建目标
@@ -46,7 +47,8 @@ type: reference
- `lisp_inline`:`{占位符}` 替换后 `sendStringToExecute`(**异步**)。
- `lisp_file`:`(load "文件" nil)` 后执行 `call_template`。
- `c++_dll`:`LoadLibrary` + `acrxGetApiVersion` 版本校验 + 工厂/销毁函数(**同步**,可返回图像)。
- `c++_dll`:`LoadLibrary`(**同一 DLL 只加载一次**)+ 工厂/销毁函数(**同步**,可返回图像)。
版本校验与「写库需文档锁」两个坑见 [MCP 基座 c++_dll 加载与执行的坑](project_mcp_frame_dll_loader.md)。
## 远程仓库
+27 -5
View File
@@ -158,13 +158,35 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::
// 基座自身版本取「编译期常量」,它稳定可靠;
// 不能依赖运行时的 acrxGetApiVersion()——该函数由 rxapi.lib 中的有状态实现提供,
// 其返回值会随调用次数变化(首次为正确值,之后被进程内全局污染),无法用于比对。
int arx_version = (ARX << 16) | SUB_ARX;
int dll_version = getVerFn();
mcpLoadLog("arx_version=" + std::to_string(arx_version) + " dll_version=" + std::to_string(dll_version));
// 返回值编码为 (major << 16) | minor:高 16 位=大版本,低 16 位=小版本。
const int arx_version = (ARX << 16) | SUB_ARX;
const int arx_major = ARX;
const int arx_minor = SUB_ARX;
if (arx_version != dll_version)
const int dll_version = getVerFn();
const int dll_major = (dll_version >> 16) & 0xFFFF;
const int dll_minor = dll_version & 0xFFFF;
mcpLoadLog("arx_version=" + std::to_string(arx_version) +
" dll_version=" + std::to_string(dll_version) +
" (arx " + std::to_string(arx_major) + "." + std::to_string(arx_minor) +
" / dll " + std::to_string(dll_major) + "." + std::to_string(dll_minor) + ")");
// 兼容性判定:
// - 同一大版本内通常二进制兼容(如 R24.0/R24.1/R24.2 可共用同一 ARX),故默认只比大版本;
// - 唯一例外是 R24.3:它换了 MSVC toolset,与 R24.0~R24.2 不兼容,需单独分组比较。
bool compatible = false;
if (dll_major == arx_major)
{
// 平台大版本不匹配,强行加载必定闪崩,果断踢掉整个模块!
if (arx_major == 24)
compatible = ((arx_minor >= 3) == (dll_minor >= 3)); // 24.3+ 与 24.0~24.2 互不兼容
else
compatible = true;
}
if (!compatible)
{
// 平台不兼容,强行加载必定闪崩,果断踢掉整个模块!
FreeLibrary(hMod);
g_loadedDllModules.erase(key);
AfxMessageBox(ver_errMsg);