Files
cad-agent/CODEBUDDY.md
T
gjm 6143bb9ab2 feat(review): PDF/图片离线源适配(C1)—— 无需 CAD 的整条审查链路
支持直接审查 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 丢图框块,离线侧暂无等价手段),留待后续打磨。
2026-10-16 23:07:23 +08:00

145 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`。