Compare commits

..
9 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
gjm 1a454b96bf Merge pull request 'feat(annotate): 新增图纸批注工具集(云线/文字/多段线/引线等 8 个工具)' (#3) from home-lenovo-legion-r9000/home-ai-dev into main
Reviewed-on: #3
2026-10-09 22:34:45 +08:00
gjm ec5f62670d fix(mcp_frame): 修复 c++_dll 工具大量注册失败与写库被文档锁拦截
- 版本校验:基座侧改用编译期常量 (ARX<<16)|SUB_ARX,不再调用运行时的
  acrxGetApiVersion()。该函数由 rxapi.lib 的 acrxGetApiVersionImpl 提供,
  是有状态实现(读写进程内全局),返回值随调用次数变化;AutoCAD 加载 ARX
  时已调用过一次,导致基座值与 DLL 值不等,绝大多数 DLL 工具被拒绝注册。
- 新增 DLL 模块缓存:同一 DLL 全进程只 LoadLibrary 一次,后续工具复用句柄
  并跳过版本校验。
- 文档锁:mcp_Tool_DllProxy::Execute 统一 lockDocument/unlockDocument,
  修复 DLL 工具在 AutoCAD 应用上下文写数据库返回 eLockViolation
  (表现为 openWriteSpace 拿不到模型空间)的问题。
- 代理类不再 FreeLibrary 模块(改由模块缓存统一卸载),避免同一 DLL 的多个
  工具重复释放、引用计数被打穿。
- 诊断日志精简为仅记录 arx_version/dll_version,且每次加载清空重写。
- 新增 verify_annotate.py:批注工具集运行时验证脚本(绘制/校验/改属性/清理)。
2026-10-09 22:31:49 +08:00
gjm 0bd290c96e chore(memory): 记录 SSH 密钥变更(改用 id_ed25519_guojianming) 2026-10-09 15:19:02 +08:00
gjm 5b81a71be9 feat(annotate): 新增图纸批注工具集(云线/文字/多段线/引线等 8 个工具)
- 新增 dwg_annotate_common/draw/edit(命名前缀 dwg_annotate)
- 工具:draw_revision_cloud、add_text、add_mtext、add_polyline、
  add_line、add_leader_annotation、delete_entities、set_entity_properties
- 批注默认落在 AI_REVIEW 图层(不存在则自动创建)
- 同步 cmakelists.txt 与 cad-mcp-frame.json(工具数 18 -> 26)
- 记录 UTF-8 BOM 编译约定到 ai.memory
2026-10-09 15:17:54 +08:00
15 changed files with 1539 additions and 61 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,用 `acrxGetApiVersion` 校验 ARX 大版本一致(不匹配则拒绝加载防崩溃),取 `factory_function` 与 `Destroy_<factory>` 两个导出函数创建/销毁工具,用 `mcp_Tool_DllProxy` 托管生命周期(RAII)。跨 DLL 通过 `SetConfigAbiSafe` / `ExecuteAbiSafe` 传 C 字符串,JSON 序列化全部在 DLL 内部完成。见 `mcp_configParser.cpp:55`、`mcp_tool.cpp:105`。
- **`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 路径、测试脚本
+12 -2
View File
@@ -60,5 +60,15 @@ 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**
- 规则:`src/` 下新增的 `.cpp` / `.h` 必须带 **UTF-8 BOM**(文件头 `EF BB BF`),与既有工程文件一致。
- **Why:** 工程未加 `/utf-8` 编译开关;无 BOM 的 UTF-8 文件会被 MSVC 按系统代码页(GBK)解析,
中文注释字节错位后会**吞掉紧随其后的声明/代码**,报出一堆离奇的
`C2447` / `C2039: 'xxx': is not a member of ...` / 行号错位错误,极难定位。
- **How to apply:** 用 Write 新建源文件后,立即补 BOM:
`printf '\xef\xbb\xbf' > f.tmp; cat f >> f.tmp; mv f.tmp f`(或确认 `head -c3 f | xxd -p` == `efbbbf`)。
既有文件(如 `dwg_review_*.cpp`、`StdAfx.h`)都带 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`)。属临时诊断,稳定后可删。
+8 -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)。
## 远程仓库
@@ -56,3 +58,8 @@ type: reference
(`home-ai-dev` → `main`,合并后源分支被删除,故本地 `home-ai-dev` 的 upstream 显示 `[gone]`)。
- 历史已被重写/重新初始化(哈希全新)。
- 历史参考(旧):`new-start.git` = `ssh://gjm@47.93.46.28:1216/srv/git/new-start.git`
- **SSH 密钥(2026-10-08 变更)**:服务器已改用新密钥
`~/.ssh/id_ed25519_guojianming`(注释 `guojianming.go@gmail.com`);
旧密钥 `~/.ssh/id_ed25519`(`deploy@aly-db`)已被服务器移除,会 `Permission denied (publickey)`。
本仓库已设置 `git config core.sshCommand "ssh -i ~/.ssh/id_ed25519_guojianming -o IdentitiesOnly=yes"`。
换机器时需补该配置或加入 `~/.ssh/config`。
+182
View File
@@ -327,6 +327,188 @@
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateHighlightEntitiesTool"
}
},
{
"name": "draw_revision_cloud",
"description": "在图纸上绘制修订云线(圈注批注)。shape=rect 时给 min_x/min_y/max_x/max_y 或 window 或 p1/p2 两角点;shape=polygon 时给 points 顶点列表(>=3);shape=circle 时给 center 与 radius。默认落在 AI_REVIEW 图层。",
"inputSchema": {
"type": "object",
"properties": {
"shape": { "type": "string", "enum": [ "rect", "polygon", "circle" ], "description": "云线形态,默认 rect" },
"min_x": { "type": "number" },
"min_y": { "type": "number" },
"max_x": { "type": "number" },
"max_y": { "type": "number" },
"window": { "type": "object", "properties": { "min_x": { "type": "number" }, "min_y": { "type": "number" }, "max_x": { "type": "number" }, "max_y": { "type": "number" } } },
"p1": { "type": "array", "items": { "type": "number" }, "description": "矩形角点 [x,y]" },
"p2": { "type": "array", "items": { "type": "number" }, "description": "矩形对角点 [x,y]" },
"points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "polygon 顶点列表 [[x,y],...]" },
"center": { "type": "array", "items": { "type": "number" }, "description": "circle 圆心 [x,y]" },
"radius": { "type": "number", "description": "circle 半径" },
"bulge": { "type": "number", "description": "每段凸度,默认 0.5(≈半圆)" },
"chord": { "type": "number", "description": "目标分段弦长,省略则自动估算" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string", "description": "\"Model\" 或布局名,默认 Model" }
}
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateDrawRevisionCloudTool"
}
},
{
"name": "add_text",
"description": "插入单行文字批注。给 text 与 position([x,y]),可设 height(默认 2.5)、rotation(度)、text_style、layer(默认 AI_REVIEW)、color。",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "文字内容" },
"position": { "type": "array", "items": { "type": "number" }, "description": "插入点 [x,y]" },
"x": { "type": "number" },
"y": { "type": "number" },
"height": { "type": "number", "description": "字高,默认 2.5" },
"rotation": { "type": "number", "description": "旋转角度(度),默认 0" },
"text_style": { "type": "string", "description": "文字样式名" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string" }
},
"required": [ "text" ]
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateAddTextTool"
}
},
{
"name": "add_mtext",
"description": "插入多行文字批注。给 text 与 position([x,y]),可设 height(默认 2.5)、width(0=不换行)、rotation(度)、attachment(如 middle_left)、text_style、layer(默认 AI_REVIEW)、color。",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "文字内容,支持 \\n 换行" },
"position": { "type": "array", "items": { "type": "number" }, "description": "插入点 [x,y]" },
"x": { "type": "number" },
"y": { "type": "number" },
"height": { "type": "number", "description": "字高,默认 2.5" },
"width": { "type": "number", "description": "文本框宽度,0=不换行,默认 0" },
"rotation": { "type": "number", "description": "旋转角度(度),默认 0" },
"attachment": { "type": "string", "description": "对齐点:top_left/top_center/top_right/middle_left/middle_center/middle_right/bottom_left/bottom_center/bottom_right,默认 top_left" },
"text_style": { "type": "string" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string" }
},
"required": [ "text" ]
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateAddMTextTool"
}
},
{
"name": "add_polyline",
"description": "绘制多段线批注。给 points 顶点列表(>=2),closed 是否闭合。默认落在 AI_REVIEW 图层。",
"inputSchema": {
"type": "object",
"properties": {
"points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "顶点列表 [[x,y],...]" },
"closed": { "type": "boolean", "description": "是否闭合,默认 false" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string" }
},
"required": [ "points" ]
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateAddPolylineTool"
}
},
{
"name": "add_line",
"description": "绘制直线批注。给 start/end([x,y]) 或 x1/y1/x2/y2。默认落在 AI_REVIEW 图层。",
"inputSchema": {
"type": "object",
"properties": {
"start": { "type": "array", "items": { "type": "number" }, "description": "起点 [x,y]" },
"end": { "type": "array", "items": { "type": "number" }, "description": "终点 [x,y]" },
"x1": { "type": "number" },
"y1": { "type": "number" },
"x2": { "type": "number" },
"y2": { "type": "number" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string" }
}
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateAddLineTool"
}
},
{
"name": "add_leader_annotation",
"description": "引线批注:一条带箭头的引线 + 多行文字。给 text 与 points(>=2,首点为箭头尖),可设 height、arrow、text_position、layer(默认 AI_REVIEW)、color。",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "批注文字" },
"points": { "type": "array", "items": { "type": "array", "items": { "type": "number" } }, "description": "引线顶点列表 [[x,y],...],首点为箭头尖" },
"text_position": { "type": "array", "items": { "type": "number" }, "description": "文字位置,默认取引线末点" },
"height": { "type": "number", "description": "字高,默认 2.5" },
"arrow": { "type": "boolean", "description": "是否显示箭头,默认 true" },
"layer": { "type": "string", "description": "图层,默认 AI_REVIEW" },
"color": { "type": "integer", "description": "ACI 颜色,默认 256(ByLayer)" },
"space": { "type": "string" }
},
"required": [ "text", "points" ]
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateAddLeaderAnnotationTool"
}
},
{
"name": "delete_entities",
"description": "按 handle 列表删除实体。用于清理 AI 自己产生的批注或图纸废线。",
"inputSchema": {
"type": "object",
"properties": {
"handle": { "type": "string" },
"handles": { "type": "array", "items": { "type": "string" } }
}
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateDeleteEntitiesTool"
}
},
{
"name": "set_entity_properties",
"description": "修改已有实体的图层/颜色(ACI)。至少给 layer 或 color 之一。用于把问题实体标红、归层等标记动作。",
"inputSchema": {
"type": "object",
"properties": {
"handle": { "type": "string" },
"handles": { "type": "array", "items": { "type": "string" } },
"layer": { "type": "string", "description": "目标图层(不存在则创建)" },
"color": { "type": "integer", "description": "ACI 颜色索引;256=ByLayer" }
}
},
"backend": {
"type": "c++_dll",
"dll_name": "cad_mcp_plugins.dll",
"factory_function": "CreateSetEntityPropertiesTool"
}
}
]
}
+126 -47
View File
@@ -5,12 +5,52 @@
#include "mcp_utils.h"
#include "ghc/filesystem.hpp"
#include "rxregsvc.h"
#include <fstream>
#include <cstdint>
#include <map>
#include <algorithm>
#include <cwctype>
namespace
{
// 已加载的插件 DLL 缓存:规范化路径 -> 模块句柄。
// 同一个 DLL 只 LoadLibrary 一次;后续工具直接复用句柄,并跳过版本校验。
std::map<std::wstring, HMODULE> g_loadedDllModules;
// 统一卸载缓存中的所有 DLL。必须在所有 mcp_Tool_DllProxy 销毁之后再调用,
// 否则 DLL 已卸载而代理仍持有其工具对象指针,析构时会崩溃。
void unloadAllDllModules()
{
for (auto &kv : g_loadedDllModules)
{
if (kv.second) { FreeLibrary(kv.second); }
}
g_loadedDllModules.clear();
}
// 诊断日志:仅记录版本校验时的两个数值(排查版本不一致问题)
// 每次重新加载工具时清空一次,便于只观察本次的结果。
void mcpLoadReset()
{
std::wstring lp = (mcp_utils::getModelDirPath() / L"mcp_load.log").wstring();
std::ofstream ofs(lp.c_str(), std::ios::trunc);
}
void mcpLoadLog(const std::string &msg)
{
std::wstring lp = (mcp_utils::getModelDirPath() / L"mcp_load.log").wstring();
std::ofstream ofs(lp.c_str(), std::ios::app);
if (ofs.is_open()) { ofs << msg << "\n"; }
}
}
#pragma region mcp_ConfigParser
bool mcp_ConfigParser::LoadAndRegister(const std::wstring &jsonFilePath, mcp_Tools &registry)
{
mcpLoadReset();
std::ifstream ifs(jsonFilePath);
if (!ifs.is_open()) return false;
if (!ifs.is_open()) { return false; }
nlohmann::json config;
try { ifs >> config; }
@@ -51,7 +91,6 @@ 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 &registry)
{
fs::path dllPath
@@ -65,48 +104,94 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::
return;
}
HMODULE hMod = LoadLibrary(dllPath.c_str());
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 );
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)
// 规范化 key(绝对路径,统一小写),用于判断该 DLL 是否已经加载过
std::wstring key = dllPath.wstring();
std::transform(key.begin(), key.end(), key.begin(),
[](wchar_t c) { return (wchar_t)::towlower(c); });
HMODULE hMod = NULL;
bool firstLoad = false;
auto it = g_loadedDllModules.find(key);
if (it != g_loadedDllModules.end())
{
AfxMessageBox(ver_errMsg);
return;
// 该 DLL 已加载:直接复用句柄,不再重复 LoadLibrary,也不再做版本校验
hMod = it->second;
}
else
{
hMod = LoadLibrary(dllPath.c_str());
if (!hMod)
{
AfxMessageBox(ver_errMsg);
return;
}
g_loadedDllModules[key] = hMod;
firstLoad = true;
}
// 1. 定义版本校验函数指针
typedef int (*GetApiVersionFunc)();
GetApiVersionFunc getVerFn = (GetApiVersionFunc)GetProcAddress(hMod, "acrxGetApiVersion");
// 2. 核心拦截逻辑
if (!getVerFn)
// 版本校验:仅在该 DLL 首次加载时执行一次
if (firstLoad)
{
// 连这个函数都没导出,说明根本不是合法的 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;
}
typedef int (*GetApiVersionFunc)();
GetApiVersionFunc getVerFn = (GetApiVersionFunc)GetProcAddress(hMod, "acrxGetApiVersion");
// 拿 DLL 编译时的 SDK 版本,和基座当前运行的 SDK 版本进行比对
int arx_version(acrxGetApiVersion());
int dll_version(getVerFn());
if (arx_version != dll_version)
{
// 平台大版本不匹配,强行加载必定闪崩,果断踢掉!
FreeLibrary(hMod);
AfxMessageBox(ver_errMsg);
return;
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);
g_loadedDllModules.erase(key);
return;
}
// 基座自身版本取「编译期常量」,它稳定可靠;
// 不能依赖运行时的 acrxGetApiVersion()——该函数由 rxapi.lib 中的有状态实现提供,
// 其返回值会随调用次数变化(首次为正确值,之后被进程内全局污染),无法用于比对。
// 返回值编码为 (major << 16) | minor:高 16 位=大版本,低 16 位=小版本。
const int arx_version = (ARX << 16) | SUB_ARX;
const int arx_major = ARX;
const int arx_minor = SUB_ARX;
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);
return;
}
}
typedef mcp_Tool *(*CreateFunc)();
@@ -124,25 +209,18 @@ void mcp_ConfigParser::LoadDllTool(const nlohmann::json &toolConfig, const std::
// 给真正的 DLL 实例也注入配置,以防第三方需要在内部读取
std::string cfgStr = toolConfig.dump();
pRealTool->SetConfigAbiSafe(cfgStr.c_str());
//pRealTool->SetConfig(toolConfig);
// 实例化代理类,接管生命周期
auto pProxy = std::make_shared<mcp_Tool_DllProxy>(hMod, pRealTool, destroyFn);
// 实例化代理类,接管工具对象的生命周期(模块句柄由 g_loadedDllModules 统一管理)
auto pProxy = std::make_shared<mcp_Tool_DllProxy>(pRealTool, destroyFn);
// 代理类继承自基类,也需要配置来处理 GetName 和 ToLLM
pProxy->SetConfig(toolConfig);
registry.Register(pProxy);
}
else
{
FreeLibrary(hMod);
}
}
else
{
FreeLibrary(hMod); // 导出函数找不到,直接卸载
// 工厂返回空:只跳过本工具,不卸载模块(同一个 DLL 的其它工具可能仍然可用)
}
// 工厂/销毁函数缺失:同样只跳过本工具,不卸载模块
}
#pragma endregion mcp_ConfigParser
@@ -191,7 +269,8 @@ void mcp_Tools::print()
void mcp_Tools::clear()
{
m_tools.clear();
m_tools.clear(); // 先销毁所有代理(代理不再 FreeLibrary,仅释放 DLL 内部工具对象)
unloadAllDllModules(); // 所有代理销毁完毕后,再统一卸载缓存的 DLL 模块
}
#pragma endregion mcp_Tools
+17 -6
View File
@@ -89,27 +89,38 @@ nlohmann::json mcp_Tool_LispFile::Execute(const nlohmann::json &args)
#pragma endregion LispFile
#pragma region DllProxy
// RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块
// RAII:基座销毁工具时,通知 DLL 释放工具对象(模块句柄由模块缓存统一管理)
mcp_Tool_DllProxy::~mcp_Tool_DllProxy()
{
if (m_pRealTool && m_destroyFunc)
{
m_destroyFunc(m_pRealTool);
}
if (m_hModule)
{
FreeLibrary(m_hModule);
}
// DLL 模块由 mcp_ConfigParser 的模块缓存统一卸载(mcp_Tools::clear 中),此处不再 FreeLibrary
}
nlohmann::json mcp_Tool_DllProxy::Execute(const nlohmann::json &args)
{
if (m_pRealTool)
{
// DLL 工具通常会写数据库(创建/修改实体、图层等)。基座运行在 AutoCAD 主线程的
// 窗口消息处理中,此时处于「应用上下文」,直接对数据库做 kForWrite 打开会返回
// eLockViolation(表现为 openWriteSpace 拿不到模型空间)。因此统一在此加文档写锁,
// 工具执行完立即解锁。只读工具被锁也无副作用。
AcApDocument* pDoc = acDocManager ? acDocManager->curDocument() : nullptr;
const bool locked = (pDoc != nullptr &&
acDocManager->lockDocument(pDoc, AcAp::kWrite) == Acad::eOk);
std::string argStr = args.dump();
//return m_pRealTool->Execute(args); // 完美转发
const char* retStr = m_pRealTool->ExecuteAbiSafe(argStr.c_str());
return nlohmann::json::parse(retStr);
nlohmann::json result = nlohmann::json::parse(retStr);
if (locked)
{
acDocManager->unlockDocument(pDoc);
}
return result;
}
return mcp_Tool_Utility::make_error(-32603, L"Underlying DLL tool instance is invalid");
}
+5 -4
View File
@@ -24,17 +24,18 @@ public:
class mcp_Tool_DllProxy : public mcp_Tool
{
private:
HMODULE m_hModule;
mcp_Tool* m_pRealTool; // 真正由第三方 DLL 创建的工具对象
typedef void (*DestroyFunc)(mcp_Tool*);
DestroyFunc m_destroyFunc;
public:
mcp_Tool_DllProxy(HMODULE hMod, mcp_Tool* pReal, DestroyFunc pDestroy)
: m_hModule(hMod), m_pRealTool(pReal), m_destroyFunc(pDestroy) {}
mcp_Tool_DllProxy(mcp_Tool* pReal, DestroyFunc pDestroy)
: m_pRealTool(pReal), m_destroyFunc(pDestroy) {}
// RAII:基座销毁工具时,自动通知 DLL 释放内存,并卸载模块
// RAII:基座销毁工具时,通知 DLL 释放工具对象。
// 注意:DLL 模块句柄由 mcp_ConfigParser 的模块缓存统一管理(同一 DLL 只加载一次),
// 这里不再 FreeLibrary,否则多个工具会重复释放同一模块导致引用计数被打穿。
virtual ~mcp_Tool_DllProxy();
virtual nlohmann::json Execute(const nlohmann::json &args) override;
+3
View File
@@ -14,6 +14,9 @@ set(SOURCES
"dwg_review_frame.cpp"
"dwg_review_check.cpp"
"dwg_review_view.cpp"
"dwg_annotate_common.cpp"
"dwg_annotate_draw.cpp"
"dwg_annotate_edit.cpp"
)
#add_def_file_name(SOURCES "${PROJECT_NAME}")
+259
View File
@@ -0,0 +1,259 @@
#include "cadsdk.h"
#include "dwg_annotate_common.h"
#include "dbcolor.h" // AcCmColor
#include <algorithm>
#include <cmath>
namespace dwg_annotate
{
using namespace dwg_review;
const char* kDefaultAnnotLayer = "AI_REVIEW";
// ===========================================================================
// 编码
// ===========================================================================
std::wstring utf8ToWStr(const std::string& s)
{
if (s.empty()) return std::wstring();
int len = ::MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), NULL, 0);
if (len <= 0) return std::wstring();
std::wstring out((size_t)len, L'\0');
::MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &out[0], len);
return out;
}
// ===========================================================================
// 图层
// ===========================================================================
bool ensureLayer(AcDbDatabase* pDb, const std::string& name, int aciColor)
{
if (!pDb || name.empty()) return false;
const std::wstring wn = utf8ToWStr(name);
// 已存在则直接返回
AcDbLayerTable* pLT = nullptr;
if (pDb->getLayerTable(pLT, AcDb::kForRead) == Acad::eOk && pLT)
{
const bool exists = pLT->has(wn.c_str());
pLT->close();
if (exists) return true;
}
// 创建新图层
if (pDb->getLayerTable(pLT, AcDb::kForWrite) != Acad::eOk || !pLT) return false;
AcDbLayerTableRecord* pRec = new AcDbLayerTableRecord();
pRec->setName(wn.c_str());
AcCmColor color;
color.setColorIndex((Adesk::UInt16)aciColor);
pRec->setColor(color);
const Acad::ErrorStatus es = pLT->add(pRec);
pRec->close();
pLT->close();
return es == Acad::eOk || es == Acad::eDuplicateRecordName;
}
// ===========================================================================
// 空间
// ===========================================================================
AcDbBlockTableRecord* openWriteSpace(AcDbDatabase* pDb, const std::string& space)
{
if (!pDb) return nullptr;
AcDbBlockTable* pBT = nullptr;
if (pDb->getBlockTable(pBT, AcDb::kForRead) != Acad::eOk || !pBT) return nullptr;
AcDbBlockTableRecord* pRec = nullptr;
if (space.empty() || space == "Model" || space == "model")
{
pBT->getAt(ACDB_MODEL_SPACE, pRec, AcDb::kForWrite);
}
else
{
AcDbObjectId id;
const std::wstring ws = utf8ToWStr(space);
if (pBT->getAt(ws.c_str(), id) == Acad::eOk)
acdbOpenObject(pRec, id, AcDb::kForWrite);
}
pBT->close();
return pRec;
}
// ===========================================================================
// 参数解析
// ===========================================================================
bool parsePoint(const nlohmann::json& j, AcGePoint3d& out)
{
if (j.is_array() && j.size() >= 2)
{
out.x = j[0].get<double>();
out.y = j[1].get<double>();
out.z = (j.size() >= 3) ? j[2].get<double>() : 0.0;
return true;
}
if (j.is_object() && j.contains("x") && j.contains("y"))
{
out.x = j["x"].get<double>();
out.y = j["y"].get<double>();
out.z = j.contains("z") ? j["z"].get<double>() : 0.0;
return true;
}
return false;
}
bool parsePoints(const nlohmann::json& arr, std::vector<AcGePoint3d>& out)
{
out.clear();
if (!arr.is_array()) return false;
for (size_t i = 0; i < arr.size(); ++i)
{
AcGePoint3d p;
if (!parsePoint(arr[i], p)) { out.clear(); return false; }
out.push_back(p);
}
return !out.empty();
}
bool parseWindow(const nlohmann::json& args, double& x1, double& y1, double& x2, double& y2)
{
if (args.contains("window") && args["window"].is_object())
{
const nlohmann::json& w = args["window"];
if (w.contains("min_x") && w.contains("min_y") && w.contains("max_x") && w.contains("max_y"))
{
x1 = w["min_x"].get<double>(); y1 = w["min_y"].get<double>();
x2 = w["max_x"].get<double>(); y2 = w["max_y"].get<double>();
return true;
}
}
if (args.contains("min_x") && args.contains("min_y") &&
args.contains("max_x") && args.contains("max_y"))
{
x1 = args["min_x"].get<double>(); y1 = args["min_y"].get<double>();
x2 = args["max_x"].get<double>(); y2 = args["max_y"].get<double>();
return true;
}
if (args.contains("p1") && args.contains("p2"))
{
AcGePoint3d a, b;
if (parsePoint(args["p1"], a) && parsePoint(args["p2"], b))
{
x1 = (std::min)(a.x, b.x); y1 = (std::min)(a.y, b.y);
x2 = (std::max)(a.x, b.x); y2 = (std::max)(a.y, b.y);
return true;
}
}
return false;
}
std::vector<std::string> collectHandlesArg(const nlohmann::json& args)
{
std::vector<std::string> hs;
if (args.contains("handle") && args["handle"].is_string())
hs.push_back(args["handle"].get<std::string>());
if (args.contains("handles") && args["handles"].is_array())
{
for (size_t i = 0; i < args["handles"].size(); ++i)
if (args["handles"][i].is_string())
hs.push_back(args["handles"][i].get<std::string>());
}
return hs;
}
// ===========================================================================
// 公共参数默认值
// ===========================================================================
std::string getLayerArg(const nlohmann::json& args)
{
if (args.contains("layer") && args["layer"].is_string())
return args["layer"].get<std::string>();
return std::string(kDefaultAnnotLayer);
}
int getColorArg(const nlohmann::json& args, int def)
{
if (args.contains("color") && args["color"].is_number_integer())
return args["color"].get<int>();
return def;
}
std::string getSpaceArg(const nlohmann::json& args)
{
if (args.contains("space") && args["space"].is_string())
return args["space"].get<std::string>();
return std::string("Model");
}
// ===========================================================================
// 实体写入
// ===========================================================================
void applyLayerColor(AcDbEntity* pEnt, const std::string& layer, int aciColor)
{
if (!pEnt) return;
if (!layer.empty())
{
const std::wstring wl = utf8ToWStr(layer);
pEnt->setLayer(wl.c_str());
}
pEnt->setColorIndex((Adesk::UInt16)aciColor);
}
AcDbObjectId appendToSpace(AcDbBlockTableRecord* pSpace, AcDbEntity* pEnt)
{
AcDbObjectId id = AcDbObjectId::kNull;
if (!pEnt) return id;
if (!pSpace)
{
pEnt->close();
return id;
}
if (pSpace->appendAcDbEntity(id, pEnt) != Acad::eOk)
id = AcDbObjectId::kNull;
pEnt->close();
return id;
}
std::string handleOfId(AcDbDatabase* pDb, const AcDbObjectId& id)
{
if (!pDb || id == AcDbObjectId::kNull) return std::string();
AcDbEntity* pEnt = nullptr;
if (acdbOpenObject(pEnt, id, AcDb::kForRead) != Acad::eOk || !pEnt) return std::string();
const std::string h = handleOf(pEnt);
pEnt->close();
return h;
}
// ===========================================================================
// 结果构造
// ===========================================================================
nlohmann::json makeCreatedItem(AcDbDatabase* pDb, const AcDbObjectId& id,
const std::string& type, const std::string& layer)
{
nlohmann::json item;
item["handle"] = handleOfId(pDb, id);
item["type"] = type;
item["layer"] = layer;
return item;
}
nlohmann::json createdResult(const std::vector<nlohmann::json>& created)
{
nlohmann::json r;
r["ok"] = true;
r["count"] = (int)created.size();
r["created"] = created;
return makeJsonResult(r);
}
} // namespace dwg_annotate
+64
View File
@@ -0,0 +1,64 @@
#pragma once
// ============================================================================
// dwg_annotate_common.h
// 图纸批注工具集 - 公共辅助层
//
// 供 dwg_annotate_draw / dwg_annotate_edit 共用,避免重复实现:
// - 批注图层自动创建(默认 AI_REVIEW)
// - 以 kForWrite 打开目标空间
// - UTF-8 <-> 宽字符、点/窗口参数解析
// - 图层/颜色设置、实体 append 与关闭
// - 统一创建结果构造
//
// 依赖 dwg_review_common(复用其 handleOf / makeJsonResult / makeError /
// objectIdFromHandle / kErr* 等)。
// ============================================================================
#include "cadsdk.h"
#include "dwg_review_common.h"
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace dwg_annotate
{
// 默认批注图层名
extern const char* kDefaultAnnotLayer;
// 确保图层存在(不存在则以给定 ACI 颜色创建)
bool ensureLayer(AcDbDatabase* pDb, const std::string& name, int aciColor);
// 以 kForWrite 打开指定空间的块表记录(成功返回的对象需调用者 close)
AcDbBlockTableRecord* openWriteSpace(AcDbDatabase* pDb, const std::string& space);
// UTF-8 -> 宽字符
std::wstring utf8ToWStr(const std::string& s);
// ---------- 参数解析 ----------
// 点:[x,y] / [x,y,z] / {"x":..,"y":..,"z":..}
bool parsePoint(const nlohmann::json& j, AcGePoint3d& out);
bool parsePoints(const nlohmann::json& arr, std::vector<AcGePoint3d>& out);
// 窗口:window 对象 或 min_x/min_y/max_x/max_y 或 p1/p2 两角点
bool parseWindow(const nlohmann::json& args, double& x1, double& y1, double& x2, double& y2);
// 句柄列表:handle / handles
std::vector<std::string> collectHandlesArg(const nlohmann::json& args);
// ---------- 公共参数默认值 ----------
std::string getLayerArg(const nlohmann::json& args); // 默认 AI_REVIEW
int getColorArg(const nlohmann::json& args, int def = 256); // 默认 ByLayer
std::string getSpaceArg(const nlohmann::json& args); // 默认 Model
// ---------- 实体写入 ----------
void applyLayerColor(AcDbEntity* pEnt, const std::string& layer, int aciColor);
// append 到空间并 close 实体,返回 ObjectId(kNull = 失败)
AcDbObjectId appendToSpace(AcDbBlockTableRecord* pSpace, AcDbEntity* pEnt);
std::string handleOfId(AcDbDatabase* pDb, const AcDbObjectId& id);
// ---------- 结果构造 ----------
nlohmann::json makeCreatedItem(AcDbDatabase* pDb, const AcDbObjectId& id,
const std::string& type, const std::string& layer);
// 包装为 MCP 文本信封:{"ok":true,"count":N,"created":[...]}
nlohmann::json createdResult(const std::vector<nlohmann::json>& created);
} // namespace dwg_annotate
+478
View File
@@ -0,0 +1,478 @@
// ============================================================================
// dwg_annotate_draw.cpp
// 图纸批注 - 绘制类工具(均为 c++_dll 后端)
// draw_revision_cloud : 修订云线(rect / polygon / circle)
// add_text : 单行文字 AcDbText
// add_mtext : 多行文字 AcDbMText
// add_polyline : 多段线 AcDbPolyline
// add_line : 直线 AcDbLine
// add_leader_annotation : 引线批注 AcDbLeader + AcDbMText
//
// 所有批注默认落在 AI_REVIEW 图层(不存在则自动创建),可在参数中覆盖。
// ============================================================================
#include "cadsdk.h"
#include "dwg_annotate_common.h"
#include "mcp_plugin_api.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
using namespace dwg_annotate;
using namespace dwg_review;
namespace
{
const double kPi = 3.14159265358979323846;
// 圆采样为闭合多边形顶点
void circleToPoints(double cx, double cy, double r, int n, std::vector<AcGePoint2d>& out)
{
out.clear();
if (n < 3) n = 3;
for (int i = 0; i < n; ++i)
{
const double a = 2.0 * kPi * i / n;
out.push_back(AcGePoint2d(cx + r * std::cos(a), cy + r * std::sin(a)));
}
}
// 在闭合多边形基础上细分并逐顶点设凸度,得到云线
void buildCloudPolyline(AcDbPolyline& pl, const std::vector<AcGePoint2d>& base,
double bulge, double chord)
{
const size_t n = base.size();
if (n < 3) return;
if (chord <= 1e-9)
{
double minx = base[0].x, maxx = base[0].x, miny = base[0].y, maxy = base[0].y;
for (size_t i = 1; i < n; ++i)
{
minx = (std::min)(minx, base[i].x); maxx = (std::max)(maxx, base[i].x);
miny = (std::min)(miny, base[i].y); maxy = (std::max)(maxy, base[i].y);
}
const double diag = std::sqrt((maxx - minx) * (maxx - minx) + (maxy - miny) * (maxy - miny));
chord = diag / 16.0;
if (chord < 1e-6) chord = 1.0;
}
unsigned int idx = 0;
for (size_t i = 0; i < n; ++i)
{
const AcGePoint2d& a = base[i];
const AcGePoint2d& b = base[(i + 1) % n];
const double len = a.distanceTo(b);
int k = (int)std::floor(len / chord + 0.5);
if (k < 1) k = 1;
for (int s = 0; s < k; ++s)
{
const double t = (double)s / (double)k;
pl.addVertexAt(idx++, AcGePoint2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
}
}
pl.setClosed(Adesk::kTrue);
const unsigned int total = pl.numVerts();
for (unsigned int i = 0; i < total; ++i)
pl.setBulgeAt(i, bulge);
}
// 字符串 -> MText 对齐点
AcDbMText::AttachmentPoint attachmentFromString(const std::string& s,
AcDbMText::AttachmentPoint def)
{
if (s == "top_left") return AcDbMText::kTopLeft;
if (s == "top_center") return AcDbMText::kTopCenter;
if (s == "top_right") return AcDbMText::kTopRight;
if (s == "middle_left") return AcDbMText::kMiddleLeft;
if (s == "middle_center" || s == "center") return AcDbMText::kMiddleCenter;
if (s == "middle_right") return AcDbMText::kMiddleRight;
if (s == "bottom_left") return AcDbMText::kBottomLeft;
if (s == "bottom_center")return AcDbMText::kBottomCenter;
if (s == "bottom_right") return AcDbMText::kBottomRight;
return def;
}
// 按名称解析文字样式 id(失败返回 kNull)
AcDbObjectId resolveTextStyle(AcDbDatabase* pDb, const std::string& name)
{
AcDbObjectId id = AcDbObjectId::kNull;
if (!pDb || name.empty()) return id;
AcDbTextStyleTable* pTST = nullptr;
if (pDb->getTextStyleTable(pTST, AcDb::kForRead) == Acad::eOk && pTST)
{
const std::wstring wn = utf8ToWStr(name);
pTST->getAt(wn.c_str(), id);
pTST->close();
}
return id;
}
}
// ===========================================================================
// draw_revision_cloud
// ===========================================================================
class mcp_Tool_DrawRevisionCloud : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
const std::string shape = args.value("shape", std::string("rect"));
const double bulge = args.value("bulge", 0.5);
const double chord = args.value("chord", 0.0);
std::vector<AcGePoint2d> base;
if (shape == "rect")
{
double x1, y1, x2, y2;
if (!parseWindow(args, x1, y1, x2, y2))
return makeError(kErrBadParam,
"rect cloud requires min_x/min_y/max_x/max_y or window or p1/p2");
base.push_back(AcGePoint2d(x1, y1));
base.push_back(AcGePoint2d(x2, y1));
base.push_back(AcGePoint2d(x2, y2));
base.push_back(AcGePoint2d(x1, y2));
}
else if (shape == "polygon")
{
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 3)
return makeError(kErrBadParam, "polygon cloud requires points (>=3)");
for (size_t i = 0; i < pts.size(); ++i)
base.push_back(AcGePoint2d(pts[i].x, pts[i].y));
}
else if (shape == "circle")
{
AcGePoint3d c;
if (!args.contains("center") || !parsePoint(args["center"], c))
return makeError(kErrBadParam, "circle cloud requires center [x,y]");
const double r = args.value("radius", 0.0);
if (r <= 1e-9) return makeError(kErrBadParam, "circle cloud requires positive radius");
double ch = chord;
if (ch <= 1e-9) ch = 2.0 * kPi * r / 12.0;
if (ch < 1e-6) ch = r / 3.0;
int n = (int)std::floor(2.0 * kPi * r / ch + 0.5);
if (n < 8) n = 8;
circleToPoints(c.x, c.y, r, n, base);
}
else
{
return makeError(kErrBadParam, "shape must be rect|polygon|circle");
}
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbPolyline* pl = new AcDbPolyline();
buildCloudPolyline(*pl, base, bulge, chord);
applyLayerColor(pl, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pl);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LWPOLYLINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_DrawRevisionCloud, CreateDrawRevisionCloudTool)
// ===========================================================================
// add_text(单行文字)
// ===========================================================================
class mcp_Tool_AddText : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
AcGePoint3d pos;
if (args.contains("position"))
{
if (!parsePoint(args["position"], pos)) return makeError(kErrBadParam, "invalid position");
}
else if (args.contains("x") && args.contains("y"))
{
pos.x = args["x"].get<double>(); pos.y = args["y"].get<double>(); pos.z = 0.0;
}
else return makeError(kErrBadParam, "position [x,y] is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const double rot = args.value("rotation", 0.0) * kPi / 180.0;
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
const std::wstring wtext = utf8ToWStr(text);
AcDbText* pText = new AcDbText(pos, wtext.c_str(), AcDbObjectId::kNull, height, rot);
applyLayerColor(pText, layer, getColorArg(args));
if (args.contains("text_style") && args["text_style"].is_string())
{
const AcDbObjectId sid = resolveTextStyle(pDb, args["text_style"].get<std::string>());
if (sid != AcDbObjectId::kNull) pText->setTextStyle(sid);
}
const AcDbObjectId id = appendToSpace(pSpace, pText);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "TEXT", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddText, CreateAddTextTool)
// ===========================================================================
// add_mtext(多行文字)
// ===========================================================================
class mcp_Tool_AddMText : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
AcGePoint3d pos;
if (args.contains("position"))
{
if (!parsePoint(args["position"], pos)) return makeError(kErrBadParam, "invalid position");
}
else if (args.contains("x") && args.contains("y"))
{
pos.x = args["x"].get<double>(); pos.y = args["y"].get<double>(); pos.z = 0.0;
}
else return makeError(kErrBadParam, "position [x,y] is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const double width = args.value("width", 0.0);
const double rot = args.value("rotation", 0.0) * kPi / 180.0;
AcDbMText::AttachmentPoint att = AcDbMText::kTopLeft;
if (args.contains("attachment") && args["attachment"].is_string())
att = attachmentFromString(args["attachment"].get<std::string>(), att);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
const std::wstring wtext = utf8ToWStr(text);
AcDbMText* pM = new AcDbMText();
pM->setContents(wtext.c_str());
pM->setTextHeight(height);
pM->setLocation(pos);
if (width > 1e-9) pM->setWidth(width);
pM->setRotation(rot);
pM->setAttachment(att);
applyLayerColor(pM, layer, getColorArg(args));
if (args.contains("text_style") && args["text_style"].is_string())
{
const AcDbObjectId sid = resolveTextStyle(pDb, args["text_style"].get<std::string>());
if (sid != AcDbObjectId::kNull) pM->setTextStyle(sid);
}
const AcDbObjectId id = appendToSpace(pSpace, pM);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "MTEXT", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddMText, CreateAddMTextTool)
// ===========================================================================
// add_polyline
// ===========================================================================
class mcp_Tool_AddPolyline : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 2)
return makeError(kErrBadParam, "points (>=2) is required");
const bool closed = args.value("closed", false);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbPolyline* pl = new AcDbPolyline();
for (size_t i = 0; i < pts.size(); ++i)
pl->addVertexAt((unsigned int)i, AcGePoint2d(pts[i].x, pts[i].y));
pl->setClosed(closed ? Adesk::kTrue : Adesk::kFalse);
applyLayerColor(pl, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pl);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LWPOLYLINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddPolyline, CreateAddPolylineTool)
// ===========================================================================
// add_line
// ===========================================================================
class mcp_Tool_AddLine : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
AcGePoint3d a, b;
if (args.contains("start") && args.contains("end"))
{
if (!parsePoint(args["start"], a) || !parsePoint(args["end"], b))
return makeError(kErrBadParam, "invalid start/end");
}
else if (args.contains("x1") && args.contains("y1") &&
args.contains("x2") && args.contains("y2"))
{
a = AcGePoint3d(args["x1"].get<double>(), args["y1"].get<double>(), 0.0);
b = AcGePoint3d(args["x2"].get<double>(), args["y2"].get<double>(), 0.0);
}
else return makeError(kErrBadParam, "start/end [x,y] (or x1/y1/x2/y2) is required");
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
ensureLayer(pDb, layer, 1);
AcDbLine* pLine = new AcDbLine(a, b);
applyLayerColor(pLine, layer, getColorArg(args));
const AcDbObjectId id = appendToSpace(pSpace, pLine);
pSpace->close();
if (id == AcDbObjectId::kNull) return makeError(kErrInternal, "append failed");
std::vector<nlohmann::json> created;
created.push_back(makeCreatedItem(pDb, id, "LINE", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddLine, CreateAddLineTool)
// ===========================================================================
// add_leader_annotation(引线 + 多行文字)
// ===========================================================================
class mcp_Tool_AddLeaderAnnotation : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
if (!args.contains("text") || !args["text"].is_string())
return makeError(kErrBadParam, "text is required");
const std::string text = args["text"].get<std::string>();
std::vector<AcGePoint3d> pts;
if (!args.contains("points") || !parsePoints(args["points"], pts) || pts.size() < 2)
return makeError(kErrBadParam, "points (>=2, first = arrow tip) is required");
double height = args.value("height", 2.5);
if (height <= 1e-9) height = 2.5;
const bool arrow = args.value("arrow", true);
AcGePoint3d textPos = pts.back();
if (args.contains("text_position")) parsePoint(args["text_position"], textPos);
AcDbBlockTableRecord* pSpace = openWriteSpace(pDb, getSpaceArg(args));
if (!pSpace) return makeError(kErrNoDocument, "cannot open target space for write");
const std::string layer = getLayerArg(args);
const int color = getColorArg(args);
ensureLayer(pDb, layer, 1);
std::vector<nlohmann::json> created;
// 1) 文字批注(先入库,供引线关联)
const std::wstring wtext = utf8ToWStr(text);
AcDbMText* pM = new AcDbMText();
pM->setContents(wtext.c_str());
pM->setTextHeight(height);
pM->setLocation(textPos);
pM->setAttachment(AcDbMText::kMiddleLeft);
applyLayerColor(pM, layer, color);
const AcDbObjectId mId = appendToSpace(pSpace, pM);
// 2) 引线
AcDbLeader* pL = new AcDbLeader();
pL->setDimstyleData(pDb->dimstyle());
for (size_t i = 0; i < pts.size(); ++i)
pL->appendVertex(pts[i]);
pL->setHasArrowHead(arrow ? Adesk::kTrue : Adesk::kFalse);
applyLayerColor(pL, layer, color);
const AcDbObjectId lId = appendToSpace(pSpace, pL);
pSpace->close();
if (mId == AcDbObjectId::kNull && lId == AcDbObjectId::kNull)
return makeError(kErrInternal, "append failed");
// 3) 关联文字并重算引线(失败则退化为"引线 + 文字"分离放置,视觉仍正确)
if (lId != AcDbObjectId::kNull && mId != AcDbObjectId::kNull)
{
AcDbLeader* pL2 = nullptr;
if (acdbOpenObject(pL2, lId, AcDb::kForWrite) == Acad::eOk && pL2)
{
if (pL2->attachAnnotation(mId) == Acad::eOk)
pL2->evaluateLeader();
pL2->close();
}
}
if (mId != AcDbObjectId::kNull) created.push_back(makeCreatedItem(pDb, mId, "MTEXT", layer));
if (lId != AcDbObjectId::kNull) created.push_back(makeCreatedItem(pDb, lId, "LEADER", layer));
return createdResult(created);
}
};
EXPORT_MCP_TOOL(mcp_Tool_AddLeaderAnnotation, CreateAddLeaderAnnotationTool)
+122
View File
@@ -0,0 +1,122 @@
// ============================================================================
// dwg_annotate_edit.cpp
// 图纸批注 - 编辑类工具(均为 c++_dll 后端)
// delete_entities : 按 handle 列表删除实体
// set_entity_properties : 修改已有实体的图层 / 颜色
//
// 用于审图后清理 AI 自己产生的批注、删除图纸废线,或把问题实体标红/归层。
// ============================================================================
#include "cadsdk.h"
#include "dwg_annotate_common.h"
#include "mcp_plugin_api.h"
#include <string>
#include <vector>
using namespace dwg_annotate;
using namespace dwg_review;
namespace
{
// 打开指定句柄的实体(kForWrite);失败返回 false
bool openForWrite(const std::string& hs, AcDbEntity*& out)
{
out = nullptr;
AcDbObjectId id;
if (!objectIdFromHandle(hs, id)) return false;
if (acdbOpenObject(out, id, AcDb::kForWrite) != Acad::eOk) { out = nullptr; return false; }
return out != nullptr;
}
}
// ===========================================================================
// delete_entities
// ===========================================================================
class mcp_Tool_DeleteEntities : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
const std::vector<std::string> hs = collectHandlesArg(args);
if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required");
int count = 0;
nlohmann::json failed = nlohmann::json::array();
for (size_t i = 0; i < hs.size(); ++i)
{
AcDbEntity* pEnt = nullptr;
if (!openForWrite(hs[i], pEnt)) { failed.push_back(hs[i]); continue; }
const Acad::ErrorStatus es = pEnt->erase();
pEnt->close();
if (es == Acad::eOk) ++count;
else failed.push_back(hs[i]);
}
nlohmann::json r;
r["ok"] = true;
r["count"] = count;
r["failed"] = failed;
return makeJsonResult(r);
}
};
EXPORT_MCP_TOOL(mcp_Tool_DeleteEntities, CreateDeleteEntitiesTool)
// ===========================================================================
// set_entity_properties
// ===========================================================================
class mcp_Tool_SetEntityProperties : public mcp_Tool
{
public:
nlohmann::json Execute(const nlohmann::json& args) override
{
AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase();
if (!pDb) return makeError(kErrNoDocument, "no working database");
const std::vector<std::string> hs = collectHandlesArg(args);
if (hs.empty()) return makeError(kErrBadParam, "handle or handles is required");
const bool hasLayer = args.contains("layer") && args["layer"].is_string();
const bool hasColor = args.contains("color") && args["color"].is_number_integer();
if (!hasLayer && !hasColor)
return makeError(kErrBadParam, "at least one of layer / color is required");
const std::string layer = hasLayer ? args["layer"].get<std::string>() : std::string();
const int color = hasColor ? args["color"].get<int>() : -1;
if (hasLayer) ensureLayer(pDb, layer, 1);
int count = 0;
nlohmann::json failed = nlohmann::json::array();
for (size_t i = 0; i < hs.size(); ++i)
{
AcDbEntity* pEnt = nullptr;
if (!openForWrite(hs[i], pEnt)) { failed.push_back(hs[i]); continue; }
if (hasLayer)
{
const std::wstring wl = utf8ToWStr(layer);
pEnt->setLayer(wl.c_str());
}
if (hasColor)
pEnt->setColorIndex((Adesk::UInt16)color);
pEnt->draw();
pEnt->close();
++count;
}
nlohmann::json r;
r["ok"] = true;
r["count"] = count;
r["failed"] = failed;
return makeJsonResult(r);
}
};
EXPORT_MCP_TOOL(mcp_Tool_SetEntityProperties, CreateSetEntityPropertiesTool)
+200
View File
@@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
"""
verify_annotate.py
批注工具集运行时验证脚本。
前提:CAD 已打开并加载 cad_mcp_frame.arx(插件 DLL),127.0.0.1:8080 在监听。
用法:
python verify_annotate.py # 绘制 -> 校验 -> 清理(默认清理)
python verify_annotate.py --keep # 保留批注,便于肉眼观察
覆盖工具:
draw_revision_cloud / add_text / add_mtext / add_polyline / add_line /
add_leader_annotation / set_entity_properties / delete_entities
"""
import socket
import json
import sys
HOST, PORT = "127.0.0.1", 8080
ANNOT_LAYER = "AI_REVIEW"
# ---------------------------------------------------------------- MCP 通信
def rpc(method, params=None, timeout=20):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((HOST, PORT))
req = {"jsonrpc": "2.0", "id": 1, "method": method}
if params is not None:
req["params"] = params
s.sendall(json.dumps(req).encode("utf-8"))
data = b""
while True:
chunk = s.recv(8192)
if not chunk:
break
data += chunk
if len(chunk) < 8192:
break
s.close()
return json.loads(data.decode("utf-8"))
def call_tool(name, arguments):
return rpc("tools/call", {"name": name, "arguments": arguments})
def payload(resp):
"""从 tools/call 响应里取出数据类工具返回的内层 JSON。"""
if resp is None:
return False, "no response"
if "error" in resp:
return False, resp["error"]
try:
for item in resp["result"]["content"]:
if item.get("type") == "text":
return True, json.loads(item["text"])
except Exception as e: # noqa: BLE001
return False, "parse error: %s" % e
return False, "no text content"
# ---------------------------------------------------------------- 测试驱动
RESULTS = []
def record(name, ok, detail):
RESULTS.append((name, ok, detail))
print(" [%s] %-24s %s" % ("PASS" if ok else "FAIL", name, detail))
def expect_created(name, resp, want_type):
"""校验工具返回 ok 且 created[0].type == want_type,返回 handle 列表。"""
ok, obj = payload(resp)
if not ok:
record(name, False, "调用失败: %s" % obj)
return []
if not obj.get("ok"):
record(name, False, "返回非 ok: %s" % obj)
return []
created = obj.get("created", [])
handles = [c.get("handle", "") for c in created]
types = [c.get("type", "") for c in created]
layers = [c.get("layer", "") for c in created]
if want_type not in types:
record(name, False, "类型不符: 期望 %s 实际 %s" % (want_type, types))
return handles
bad_layer = [l for l in layers if l != ANNOT_LAYER]
if bad_layer:
record(name, False, "图层不符: %s" % layers)
return handles
record(name, True, "created=%s layer=%s" % (types, layers))
return handles
def main():
keep = "--keep" in sys.argv
print("=== 批注工具集运行时验证 ===")
print("目标: %s:%d\n" % (HOST, PORT))
# 0) 连接 & 工具清单
try:
tools_resp = rpc("tools/list")
except Exception as e: # noqa: BLE001
print("[错误] 无法连接 MCP 服务: %s" % e)
print("请确认 CAD 已打开、插件已加载、8080 端口在监听。")
return 2
names = [t["name"] for t in tools_resp.get("result", {}).get("tools", [])]
want = ["draw_revision_cloud", "add_text", "add_mtext", "add_polyline",
"add_line", "add_leader_annotation", "delete_entities",
"set_entity_properties"]
missing = [w for w in want if w not in names]
print("工具总数: %d" % len(names))
if missing:
print("[FAIL] 未注册的工具: %s" % missing)
return 1
print("[PASS] 8 个批注工具均已注册\n")
created_handles = []
# 1) 云线(矩形)
print("1) 绘制工具")
h = expect_created("draw_revision_cloud", call_tool("draw_revision_cloud", {
"shape": "rect", "min_x": 0, "min_y": 0, "max_x": 100, "max_y": 60,
}), "LWPOLYLINE")
created_handles += h
# 2) 单行文字
h = expect_created("add_text", call_tool("add_text", {
"text": "AI批注-单行", "position": [0, 80], "height": 5,
}), "TEXT")
created_handles += h
# 3) 多行文字
h = expect_created("add_mtext", call_tool("add_mtext", {
"text": "AI批注-多行\n第二行", "position": [0, 100], "height": 5,
}), "MTEXT")
created_handles += h
# 4) 多段线
h = expect_created("add_polyline", call_tool("add_polyline", {
"points": [[0, 130], [60, 130], [60, 170]], "closed": False,
}), "LWPOLYLINE")
created_handles += h
# 5) 直线
h = expect_created("add_line", call_tool("add_line", {
"start": [0, 190], "end": [100, 190],
}), "LINE")
created_handles += h
# 6) 引线批注(返回 MTEXT + LEADER 两个)
h = expect_created("add_leader_annotation", call_tool("add_leader_annotation", {
"text": "引线批注", "points": [[300, 200], [200, 150]], "height": 5,
}), "LEADER")
created_handles += h
# 7) 下钻校验:取第一个实体详情,确认图层
print("\n2) 校验")
if created_handles:
ok, obj = payload(call_tool("get_entity_detail", {"handle": created_handles[0]}))
if ok and obj.get("layer") == ANNOT_LAYER:
record("get_entity_detail", True, "handle=%s layer=%s" % (created_handles[0], obj.get("layer")))
else:
record("get_entity_detail", False, "%s" % obj)
else:
record("get_entity_detail", False, "无可用 handle")
# 8) 修改实体属性(把第一个实体改红)
if created_handles:
ok, obj = payload(call_tool("set_entity_properties", {
"handle": created_handles[0], "color": 1,
}))
record("set_entity_properties", bool(ok and obj.get("count") == 1), "%s" % obj)
# 9) 清理
print("\n3) 清理")
if keep:
record("delete_entities", True, "已跳过(--keep);保留 handle: %s" % created_handles)
elif created_handles:
ok, obj = payload(call_tool("delete_entities", {"handles": created_handles}))
good = ok and obj.get("count") == len(created_handles)
record("delete_entities", good, "删除 %s / 共 %s" % (obj.get("count"), len(created_handles)))
else:
record("delete_entities", False, "无 handle 可删")
# 汇总
print("\n=== 结果汇总 ===")
passed = sum(1 for _, ok, _ in RESULTS if ok)
for name, ok, detail in RESULTS:
print(" %-4s %-24s %s" % ("OK" if ok else "NG", name, detail))
print("\n通过 %d / %d" % (passed, len(RESULTS)))
return 0 if passed == len(RESULTS) else 1
if __name__ == "__main__":
sys.exit(main())