Files
cad-agent/verify_review_maximized.py
T
gjm e8140f2bc5 feat(review): 审查阶段对齐绘图区分辨率 + 放大取证闭环
发现的缺口:maximized_drawing_area 原先只包住取证循环,审查阶段没被包住 →
LLM 在受控子循环里 zoom + 截图拿到的是未最大化的 2110x462(正是上一轮刚解决的
低分辨率问题,在最需要放大看细节的审查阶段又出现了);且审查中的 zoom 不复位视口,
跑完把用户的 CAD 留在最后一次放大处。

改动:
1. __main__.run_review:把 maximized_drawing_area 从「只包取证」扩到「包取证 + 审查」
   (两者都要截图);report/annotate 留在 with 之外。
2. phases/review.py:新增 _VIEW_TOOLS;review_frame 拆成外壳 + _review_frame,
   外壳 try/finally 里仅当确实动过视口才复位到该图框整幅视图
   (zoom_to_window(win, margin=0.02))。纯查结构化数据的路径不受影响。
3. checklist.build_system_prompt:把「可调用只读工具」改写成明确的放大取证协议——
   截图看不清时用 zoom_to_window(坐标由图框/实体 bbox 算)或 zoom_to_entity,
   再 get_viewport_screenshot 复核;并约束不要为看图而反复放大。

次生问题(同轮修复):_MAX_OBS_IMAGES 原为硬编码 3,而模型一轮内会发 8 次截图 →
拍了 8 张只回灌 3 张。改为配置项 REVIEW_OBS_IMAGES(默认 6),并把预算耗尽的占位
文案改成明确告知模型「不要再请求截图,请给出结论」。

实测(真实图纸 1 图框,新增 verify_review_maximized.py):取证与审查两阶段窗口均为
[-4,-2,3444,1442](最大化态),结束精确还原 [68,0,2316,960],三项全 PASS;
子循环里 LLM 连发 8 组 zoom_to_window + get_viewport_screenshot,提示词生效。
pytest 304 passed。
2026-10-16 18:55:31 +08:00

109 lines
3.4 KiB
Python

"""验收:审查阶段是否也在「绘图区最大化」状态下进行,且结束后精确还原。
做法:在取证 `collect` 与审查 `review_frame` 入口各采一次窗口矩形,
并在 `McpClient.call_tool` 上挂计数器,统计 LLM 子循环是否真的用了放大工具。
用法:先启动已加载插件的 CAD 并打开图纸,然后 `python verify_review_maximized.py`。
"""
import ctypes
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 = []
_orig_collect = ev.collect
_orig_review = rv.review_frame
_orig_call = McpClient.call_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)
ev.collect = collect
rv.review_frame = review
McpClient.call_tool = call_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_calls}")
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'}] 结束精确还原窗口矩形")
sys.exit(0 if (ok_ev and ok_rv and after == before and rc == 0) else 1)