Files
cad-agent/verify_review_maximized.py
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

125 lines
4.1 KiB
Python
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.
"""验收:审查阶段是否也在「绘图区最大化」状态下进行,且结束后精确还原。
做法:在取证 `collect` 与审查 `review_frame` 入口各采一次窗口矩形,
并在 `McpClient.call_tool` 上挂计数器统计子循环的 zoom/截图次数,
同时记录 LLM 实际调用的**取证工具**(含 agent 侧虚拟工具 `inspect_region`)。
用法:先启动已加载插件的 CAD 并打开图纸,然后 `python verify_review_maximized.py`。
"""
import ctypes
import json
import sys
from ctypes import wintypes
from dwg_review import __main__ as cli
from dwg_review.mcp_client import McpClient
from dwg_review.phases import evidence as ev
from dwg_review.phases import review as rv
_u = ctypes.windll.user32
_u.GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
_u.IsWindowVisible.argtypes = [wintypes.HWND]
_u.GetWindowRect.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.RECT)]
_WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
def _find_main_window():
hit = [None]
def cb(hwnd, _lparam):
if not _u.IsWindowVisible(hwnd):
return True
n = _u.GetWindowTextLengthW(hwnd)
if n == 0:
return True
buf = ctypes.create_unicode_buffer(n + 1)
_u.GetWindowTextW(hwnd, buf, n + 1)
if "AutoCAD" in buf.value and "[" in buf.value:
hit[0] = hwnd
return False
return True
_u.EnumWindows(_WNDENUMPROC(cb), 0)
return hit[0]
HWND = _find_main_window()
if not HWND:
print("[X] 未找到 AutoCAD 主窗口")
sys.exit(1)
def rect():
r = wintypes.RECT()
_u.GetWindowRect(HWND, ctypes.byref(r))
return [r.left, r.top, r.right, r.bottom]
def area(r):
return (r[2] - r[0]) * (r[3] - r[1])
# --- 采样点 ---------------------------------------------------------------
phase_rects = {"evidence": [], "review": []}
zoom_calls = []
llm_tools = [] # LLM 在审查子循环里实际调用的取证工具(名称 + 参数)
_orig_collect = ev.collect
_orig_review = rv.review_frame
_orig_call = McpClient.call_tool
_orig_run_tool = rv._run_readonly_tool
def collect(ctx, index, frame):
phase_rects["evidence"].append(rect())
return _orig_collect(ctx, index, frame)
def review(ctx, evidence, system_prompt, log=print):
phase_rects["review"].append(rect())
return _orig_review(ctx, evidence, system_prompt, log=log)
def call_tool(self, name, arguments=None, **kw):
if name in ("zoom_to_window", "zoom_to_entity", "get_viewport_screenshot"):
zoom_calls.append(name)
return _orig_call(self, name, arguments, **kw)
def run_tool(ctx, name, args, budget, **kw):
llm_tools.append((name, args))
return _orig_run_tool(ctx, name, args, budget, **kw)
ev.collect = collect
rv.review_frame = review
McpClient.call_tool = call_tool
rv._run_readonly_tool = run_tool
before = rect()
print(f"起始窗口矩形 = {before} 面积={area(before)}")
rc = cli.main(["--frames", "1"])
after = rect()
print()
print("=== 结果 ===")
print(f"取证阶段采样 = {phase_rects['evidence']}")
print(f"审查阶段采样 = {phase_rects['review']}")
print(f"结束窗口矩形 = {after} 面积={area(after)}")
print(f"子循环 zoom/截图调用 = {len(zoom_calls)} 次")
print(f"LLM 取证工具调用 = {len(llm_tools)} 次")
for name, args in llm_tools:
brief = json.dumps(args, ensure_ascii=False)
print(f" - {name} {brief[:160]}")
inspect_n = sum(1 for name, _ in llm_tools if name == "inspect_region")
ok_ev = all(area(r) > area(before) for r in phase_rects["evidence"]) if phase_rects["evidence"] else False
ok_rv = all(area(r) > area(before) for r in phase_rects["review"]) if phase_rects["review"] else False
print()
print(f"[{'PASS' if ok_ev else 'FAIL'}] 取证阶段处于最大化(面积大于基线)")
print(f"[{'PASS' if ok_rv else 'FAIL'}] 审查阶段处于最大化(面积大于基线)")
print(f"[{'PASS' if after == before else 'FAIL'}] 结束精确还原窗口矩形")
print(f"[{'PASS' if inspect_n else 'INFO'}] LLM 使用了 inspect_region({inspect_n} 次)")
sys.exit(0 if (ok_ev and ok_rv and after == before and rc == 0) else 1)