支持直接审查 PDF 与位图图纸,全链路离线(不连 CAD、不连 8080 端口)。 渲染在内存内完成,复用既有墨迹热力图 + 自适应瓦片规划。 - 新增 dwg_review/sources.py - detect_source / plan_file / collect 统一入口 - PdfReader:pypdfium2 惰性导入,页序即图框序,处理 /Rotate 与 crop 边距 - ImageReader:世界单位 = 源像素,snap_box 整数对齐保证窗口与像素严格一致 - ink_grid:纯 PIL 灰度阈值 → 占用网格(不引入 numpy) - ReviewContext.create(connect_cad=False):离线时 client/tools 均为 None - 全链路 ctx.tools is None 守卫:review 只读子循环 / inspect_region / annotator / checks 的窗口类检查 / 绘图区最大化 - checklist.build_system_prompt(allow_tools=False, offline=True):离线提示词 - __main__:位置参数 source,按扩展名自动判定源类型并决定是否连 CAD - config 新增 8 个常量(PDF_MAX_PAGES / 文本最小字数 / 各源缩放上限 / 墨迹阈值等) - tiling.content_predicate 与 heatmap.cell_box 提升为公开函数 验证: - pytest 457 passed - verify_pdf_source.py:PDF 23/23、图片 9/9(不需 CAD),含实测瓦片字高像素与 旋转映射自检(文本区墨迹 0.311 vs 忽略 /Rotate 反向对照 0.145) - 真实 LLM 端到端:20 页 HVAC 图纸前 3 页跑通,结论具体可核验且未编造 handle 已知局限:页边线在墨迹网格里会把整页连成单簇,瓦片常退化为整页象限(CAD 侧 靠 drop_oversized 丢图框块,离线侧暂无等价手段),留待后续打磨。
145 lines
12 KiB
Markdown
145 lines
12 KiB
Markdown
# 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>/`。
|
||
|
||
```bash
|
||
# 配置 + 构建单个版本(以 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、不联网、秒级):
|
||
|
||
```bash
|
||
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, ...)`。
|
||
`tests/test_sources_pdf.py` 里的 `_build_pdf()` 手工构造最小 PDF,PDF 路径无需样本文件即可回归。
|
||
|
||
**慢层 —— 端到端冒烟脚本**(仓库根 `verify_*.py`):
|
||
|
||
需要**已启动已加载插件的 CAD、8080 端口在监听**(`verify_pdf_source.py` 除外):
|
||
|
||
```bash
|
||
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 取证工具调用
|
||
python verify_collisions.py # find_collisions:全模型/逐图框粗筛、容器过滤、阈值/分页/细筛;--inject 注入干涉正例后清理
|
||
```
|
||
|
||
这些脚本通过 TCP 连接 `127.0.0.1:8080` 收发 JSON-RPC。
|
||
|
||
**离线源验收(PDF / 图片,不需要 CAD)**:
|
||
|
||
```bash
|
||
python verify_pdf_source.py "E:/path/图纸.pdf" # 离线:页序 / evidence 同形 / 瓦片字高实测 / 旋转映射自检
|
||
python verify_pdf_source.py "E:/path/scan.png" --dump out/c1
|
||
python verify_pdf_source.py "E:/path/图纸.pdf" --llm # 追加真实 python -m dwg_review 端到端
|
||
```
|
||
|
||
## 架构
|
||
|
||
### 两个构建目标
|
||
|
||
| 目标 | 产物 | 作用 |
|
||
| --- | --- | --- |
|
||
| `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/`)
|
||
|
||
1. CAD 加载 `.arx` → `acrxEntryPoint(kInitAppMsg)` → `mcp::init_mcp_server()`(`cad_mcp_frame.cpp:41`)。
|
||
2. `init_mcp_server()`(`mcp_server.cpp:225`)在主 UI 线程创建**仅消息窗口**(`HWND_MESSAGE`,`McpWndProc`),并启动后台 `TcpServerLoop` 线程监听 `127.0.0.1:8080`。
|
||
3. **跨线程安全是核心设计**:TCP 线程只负责 `recv`,把请求封装成 `TaskData`(含 socket)后 `PostMessage(WM_MCP_EXECUTE_TASK)` 投递给消息窗口。真正的工具执行 `ExecuteTaskInMainThread()` 运行在 **CAD 主线程**,从而安全调用 ARX API。执行结果再通过该 socket 发回。
|
||
4. 配置加载:`mcp_ConfigParser::LoadAndRegister()` 读取模块所在目录下的 `mcp_config.json`(`getModelDirPath()`,即 `.arx` 同级目录),按 `tools[].backend.type` 注册工具到全局 `mcp_Tools` 注册表(`unordered_map<name, shared_ptr<mcp_Tool>>`)。
|
||
5. 卸载:`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 → 多模态返回)是完整参考实现。
|
||
|
||
### Python 侧:图纸审查 agent(`dwg_review/`)
|
||
|
||
CLI:`python -m dwg_review [source] [options]`。**来源决定运行模式**:
|
||
|
||
| `source` | 模式 | 取证方式 |
|
||
| --- | --- | --- |
|
||
| 省略 / `.dwg` / `.dxf` | **CAD 在线** | 连 8080,逐图框调 MCP 工具 + 截屏 |
|
||
| `.pdf` / 常见图片格式 | **完全离线**(不连 CAD) | `sources.py` 内存渲染 + 墨迹瓦片 |
|
||
|
||
四阶段流水线:`phases/planning`(识别图框 / 分页)→ `phases/evidence`(取证)→
|
||
`phases/review`(LLM 审查 + 受控只读取证子循环)→ `phases/report`(汇总落盘)。
|
||
|
||
- **统一证据模型** `evidence_model.py`:`source_type` + `images:[{b64,mime,label,window}]`;
|
||
`window` 是该图**实际可见的世界范围**(供 `inspect_region` 把截图归一化坐标映射回 WCS)。
|
||
- **多源分发 seam**:`phases/evidence.collect(ctx, index, unit)` 按 `source_type_of(unit)`
|
||
分发;非 CAD 源走 `sources.collect`,产出与 CAD **同形**的 evidence(结构化键给
|
||
`evidence_model.empty_envelope()`,PDF 另有 best-effort 文本层放进 `texts` 信封)。
|
||
- **纯函数复用**:`tiling.py`(比例换算 / 瓦片切分)、`heatmap.py`(占用网格 / 聚类)
|
||
与 CAD 无关,CAD 用**实体 bbox** 驱动、PDF/图片用**像素墨迹**驱动,链路完全一致。
|
||
- **离线守卫**:`connect_cad=False` 时 `ctx.tools`/`ctx.client` 均为 `None`;所有访问点
|
||
(`maximized_drawing_area` / `_review_tools` / `_run_readonly_tool` / `_reset_viewport` /
|
||
`run_deterministic_checks` / `annotator.annotate`&`clear`)都先判 `None`。
|
||
`checklist.build_system_prompt(offline=True)` 换成纯视觉提示词(无工具、**禁止输出 handle**)。
|
||
- **世界坐标系**(都 y 向上):CAD = 图形单位;PDF = **页内点**(已应用 `/Rotate`);
|
||
图片 = **源像素**(`sources.snap_box` 对齐整数,保证「窗口 ↔ 像素」精确对应)。
|
||
- 运行时依赖见 `requirements.txt`(`openai` / `Pillow` / `pypdfium2`)。
|
||
|
||
### 关键头文件
|
||
|
||
- `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`(当前注册 28 个工具,含 `zoom_window_normalized`、`zoom_extents`、`get_viewport_screenshot`、`set_drawing_area`、`find_collisions` 等);`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`)。
|
||
- **Python 侧不得把 CAD 能力下沉到 MCP**:PDF / 图片的解析与渲染属于 agent 侧感知层(`dwg_review/sources.py`),**不塞进 MCP、不编 DLL**;只有「CAD 进程内的正交感知能力」才考虑新增 MCP 工具。
|
||
- `bin/`、`lib/`、`out/` 均为构建产物,已在 `.gitignore` 中忽略;源码在 `src/`、`inc/`、`cmake/`。
|