Files
cad-agent/CODEBUDDY.md
T
gjm b757892c67 feat(review): agent 侧虚拟工具 inspect_region —— 按可读分辨率放大指定区域
解决三个缺口:① 放大没复用标定/瓦片数学(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。
2026-10-16 19:15:32 +08:00

106 lines
8.9 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, ...)`。
**慢层 —— 端到端冒烟脚本**(仓库根 `verify_*.py`,**必须先启动已加载插件的 CAD,并确认
8080 端口在监听**):
```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 取证工具调用
```
这些脚本通过 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/`)
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 → 多模态返回)是完整参考实现。
### 关键头文件
- `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/`。