解决三个缺口:① 放大没复用标定/瓦片数学(LLM 一次 zoom 只出一张,宽区域仍读不清);
②「看清没」由 VLM 自评(易过度自信/幻觉);③ 只能按 handle / 手算 bbox 指认,
图上看到但没 handle 的东西指认不了。
核心思路:把「放大」升级成一个语义化工具——LLM 只指认「要看哪里」,由 agent 复用
tiling.py 的标定数学算出该切几张、每张覆盖多大,用构造保证可读。
新增 dwg_review/inspect.py(agent 侧,不编 DLL):
- TOOL_SCHEMA:inspect_region(target, purpose);target 三形态 {handle} / {window} /
{label,cx,cy,span},purpose = text(8px) / line(6px)。
- InspectState:单图框内「截图标签 → 世界可见范围」登记表,支持逐级下钻
(返回的 inspect-N 可继续作为 label 指认)。
- resolve_target / _point_box:label+cx+cy → WCS。cy 是图像坐标(上→下),
世界 y 必须翻转(y = y2 - cy*H)——单测专门钉住。
- plan_tiles:复用 world_per_px + tiles_for_box + subdivide;超 INSPECT_MAX_TILES(4)
则硬失败并给出「缩小 target / 分多次请求」的指引(不静默降级);拿不到比例/像素
则退回单张并如实告知。
关键几何:CAD 的 _.ZOOM _W 是等比适配(不拉伸),故「截图上的归一化坐标」必须按
max(W/ew, H/eh) 反算真实可见范围(新增 tiling.visible_box)——直接用送进去的窗口会偏。
zoom_to_window 返回的 view 是「加过 margin 的窗口」,不是可见范围,不能混用。
证据层配套:evidence_model.make_image(..., window=) 记录每张图的可见世界范围;
capture_images 用 view_px(overall 图原始像素)+ visible_box 逐张登记。
子循环接入:_run_readonly_tool 在只读白名单校验之前拦截 inspect_region(它不在
READ_ONLY_TOOLS 里,内部只调白名单内的只读 MCP 工具);_review_tools 启用时用
inspect_region 取代三个裸视口工具;inspect_region 计入 _VIEW_TOOLS → 结束复位视口。
配置:INSPECT_ENABLED(True) / INSPECT_MAX_TILES(4)。
端到端实测(真实图纸 1 图框):取证与审查两阶段均在最大化态、结束精确还原(4 项 PASS);
LLM 的 11 次取证调用里 2 次是 inspect_region,且都显式给 window + purpose:"text"
(x[20000,115000] y[-6500,3000],即 A0 图框底部横条=标题栏所在)。
验收口径达成:报告报出标题栏乱码 ???????????????,而 get_text_content 对全图返回 0 条文字
(本图文字全在块/自定义实体内)→ 块内文字是被视觉放大读出来的。
pytest 343 passed。
8.9 KiB
CODEBUDDY.md
This file provides guidance to CodeBuddy Code when working with code in this repository.
项目概述
CADProject 是一个将 AutoCAD / 中望 CAD (ZWCAD) 变为 MCP (Model Context Protocol) 服务端的插件工程。CAD 进程内运行一个 TCP JSON-RPC 服务,向大模型暴露 tools/list 与 tools/call,让 LLM 能驱动 CAD 执行绘图、缩放、截图等操作。仓库同时附带一组 Python 客户端脚本用于联调。
构建
构建系统为 CMake + vcpkg + Visual Studio 生成器,通过 CMake Presets 区分 CAD 版本。产物输出到 bin/<Config>-<TAG>/ 与 lib/<Config>-<TAG>/。
# 配置 + 构建单个版本(以 R243 为例)
cmake --preset R243
cmake --build --preset R243 --config Release
# 或使用根目录脚本批量处理(内部即上面两条命令的组合)
./preset.bat # 仅 configure: R190 / R230 / R220
./build.bat # build --config release: R230 / R220
- 前置环境:需要
VCPKG_ROOT环境变量;第三方 CAD SDK 位于仓库同级的../envi-code/Arx(AutoCAD)与../envi-code/Zrx(ZWCAD),路径在cmakepresets.json的CAD_SDK_ROOT中配置。 - 可用 preset:
R180–R260(AutoCAD,ARX 18–26)、Z2023–Z2026(ZWCAD,ZRX)。每个 preset 绑定对应的 MSVC toolset(v90–v143)。preset 名称同时决定输出目录标签GENERATE_DIR_TAG(如R243、Z2024)。 - 必需 cache 变量(缺失会
FATAL_ERROR):CAD_SDK_ROOT、DEP_LIB_ROOT、DEP_INC_ROOT、CAD_SDK_VERSION,AutoCAD 版还需CAD_SDK_SUBVERSION。 - vcpkg 依赖见
vcpkg.json:nlohmann-json、curl、ghc-filesystem;triplet 为x64-windows-static-md(静态库 + 动态 CRT,定义于cmake/x64-windows-static-md.cmake)。
测试
分两层,互不替代:
快层 —— pytest 离线单元测试(tests/,无需 CAD、不联网、秒级):
pip install -r requirements-dev.txt # 仅 pytest
python -m pytest # 期望全绿
全部使用假 LLM + 假 MCP 基座(tests/fakes.py),锁定 dwg_review 流水线既有行为。
配置见 pytest.ini(testpaths = tests)与根 conftest.py(把仓库根加入 sys.path,
并提供 RENDER_WAIT=0 / REPORT_DIR=tmp 两个 fixture)。注意:dwg_review.config
的常量在导入期定值,测试改 os.environ 无效,须 monkeypatch.setattr(config, ...)。
慢层 —— 端到端冒烟脚本(仓库根 verify_*.py,必须先启动已加载插件的 CAD,并确认
8080 端口在监听):
python verify_mcp_frame.py # 基座最小集:分帧 / id / isError 映射
python verify_mcp_tcp_hardening.py # TCP 收发路径加固(超长帧 / 粘包 / 半关闭)
python verify_mcp_fault_injection.py # 工具异常保护(类型错误不再崩 CAD)
python verify_stdio_bridge.py # stdio ↔ TCP 桥(协议协商 / 转发 / 通知)
python verify_annotate.py # 批注回写链路
python verify_drawing_area.py # 绘图区最大化:基线 → max → restore 全回位(实测 3.68x)
python verify_review_maximized.py # 审查全链路:取证+审查两阶段均在最大化态、结束精确还原;记录 LLM 取证工具调用
这些脚本通过 TCP 连接 127.0.0.1:8080 收发 JSON-RPC。
架构
两个构建目标
| 目标 | 产物 | 作用 |
|---|---|---|
src/cad_mcp_frame |
cad_mcp_frame.arx(ZWCAD 下为 .zrx) |
MCP 基座。随 CAD 加载,起 TCP 服务、解析工具配置、路由执行。 |
src/cad_mcp_plugins |
cad_mcp_plugins.dll |
示例 C++ 工具插件,通过 ABI 被基座动态加载。 |
基座运行模型(src/cad_mcp_frame/)
- CAD 加载
.arx→acrxEntryPoint(kInitAppMsg)→mcp::init_mcp_server()(cad_mcp_frame.cpp:41)。 init_mcp_server()(mcp_server.cpp:225)在主 UI 线程创建仅消息窗口(HWND_MESSAGE,McpWndProc),并启动后台TcpServerLoop线程监听127.0.0.1:8080。- 跨线程安全是核心设计:TCP 线程只负责
recv,把请求封装成TaskData(含 socket)后PostMessage(WM_MCP_EXECUTE_TASK)投递给消息窗口。真正的工具执行ExecuteTaskInMainThread()运行在 CAD 主线程,从而安全调用 ARX API。执行结果再通过该 socket 发回。 - 配置加载:
mcp_ConfigParser::LoadAndRegister()读取模块所在目录下的mcp_config.json(getModelDirPath(),即.arx同级目录),按tools[].backend.type注册工具到全局mcp_Tools注册表(unordered_map<name, shared_ptr<mcp_Tool>>)。 - 卸载:
close_mcp_server()停线程、销毁消息窗口、清空工具表。
支持的 JSON-RPC 方法:tools/list(返回所有工具描述)、tools/call(路由到 mcp_Tools::Call)。非法 JSON 返回 -32700,未知方法返回 -32601。
三种工具后端(backend.type)
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),仅首次加载时校验版本兼容性: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)
第三方工具只需继承 mcp_Tool,实现纯虚 Execute(args),并用 EXPORT_MCP_TOOL(ToolClass, FactoryName) 宏导出工厂 + 销毁函数。mcp_Tool_Utility 提供 make_text_result / make_image_result / make_mixed_result / make_error 等返回体构造器。mcp_plugins.cpp 中的 mcp_Tool_ViewportScreenshot(GDI+ 抓屏 → PNG → Base64 → 多模态返回)是完整参考实现。
关键头文件
inc/cadsdk.h:ARX 开发总伞头文件,一次性引入几乎所有 ObjectARX/ZWCAD 头文件,处理 ZWCAD 与 AutoCAD 的宏/库差异。业务.cpp通常只需#include "cadsdk.h"。src/cad_mcp_frame/StdAfx.h:预编译头(MFC + Windows),是cad_mcp_frame的 PCH 入口。
配置与产物布局
- 运行时配置:
mcp_config.json,必须与.arx同目录。示例见bin/Debug-R230/mcp_config.json(当前注册 27 个工具,含zoom_window_normalized、zoom_extents、get_viewport_screenshot、set_drawing_area等);src/cad_mcp_frame/cad-mcp-frame.json是模板(文件名带连字符,非运行时文件)。 global_settings.plugin_directory指向 C++ DLL 目录,lisp_directory指向 LISP 目录(默认都相对模块目录)。示例 LISP 见bin/Debug-R230/mcp_plugins/mcp_table_array.lsp。cmake/functions.cmake提供工程级封装:target_link_objectarx(自动按 ZWCAD 与否链接ThirdParty::ZRX/ThirdParty::ARX、注入ARX/SUB_ARX宏)、target_set_name_and_suffix(.arx在 ZWCAD 下自动变.zrx)、add_def_file_name、set_debug_post_build等。新增目标应复用这些函数而非手写链接。cmake/ThirdPartyDeps.cmake注册所有ThirdParty::*INTERFACE 目标(ARX/ZRX/cURL/SQLite3/BitAnswer/modernGlue/CadOcr)。
约定与注意点
- 不要改动线程模型:任何涉及 ARX API 的工具逻辑必须在主线程(
ExecuteTaskInMainThread派发链)中执行,不能在TcpServerLoop线程里直接调用。 - C++ 标准随 SDK 版本浮动:
cmakelists.txt按CAD_SDK_VERSION自动设定 C++98/03/14/20,跨版本改动需兼容旧标准(低版本 SDK 最高只到 C++14)。 - 新增 CAD 版本支持时,需在
cmakepresets.json同步添加configurePresets与buildPresets,并在cmake/ThirdPartyDeps.cmake中确认对应库列表(如CAD_SDK_VERSION GREATER 23时追加acpal.lib/acgeoment.lib)。 bin/、lib/、out/均为构建产物,已在.gitignore中忽略;源码在src/、inc/、cmake/。