Files
cad-agent/dwg_review/__main__.py
T
gjm 1e5bff268a feat(review): 截屏前最大化绘图区 + 空瓦片过滤覆盖回退路径
新增 MCP 工具 set_drawing_area(mode="max"|"restore"):最大化主窗口 + CLEANSCREENON
(隐藏功能区/命令行/面板)。实测绘图区 2110x462 -> 3310x1084(面积 3.68x),
restore 精确回位(窗口矩形/最大化态/绘图区尺寸全部还原)。

坑:CLEANSCREENOFF 是异步命令,AutoCAD 会按「进入干净屏幕时的布局」重设窗口几何,
把同步的 SetWindowPlacement 覆盖掉(表现为窗口停在最大化)。解法是插件内
SetTimer 延迟 700ms 再压回;实测窗口在 0.5~1.0s 之间收敛,故调用方等待取 1.5s。

取证层:
- 新增 maximized_drawing_area(ctx) 上下文管理器,__main__ 用它包住整个取证循环
  (多图框只进出一次);受 DRAWING_AREA_MAXIMIZE 控制,工具缺失/调用失败静默降级,
  restore 失败不掩盖业务异常。
- 内容过滤统一到规划与回退两条路径,共用同一谓词;过滤后为空则退回原集合。
- 判定改用「实体 bbox」而非「占用格」:格子量化会把跨界内容算进相邻格,反而更粗
  (实测实体 bbox 正好压象限边界时 4 个象限全判有内容)。heatmap.occupied_boxes 随之删除。

实测(真实图纸 A0/A1/A2 三图框):截图张数 12/8/4 -> 8/5/4;pytest 296 passed。
新增 verify_drawing_area.py 作为该工具的验收脚本。
2026-10-15 23:44:32 +08:00

194 lines
7.0 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.
"""CLI 入口:python -m dwg_review [options]"""
import argparse
import json
import sys
from pathlib import Path
from . import config
from . import checks as checks_phase
from . import findings as findings_mod
from .annotator import annotate, clear
from .checklist import build_system_prompt, checklist_summary
from .context import ReviewContext
from .evidence_model import evidence_images
from .mcp_client import McpError, McpToolError, result_json
from .phases import evidence as evidence_phase
from .phases import planning, report, review
def _dump_evidence(evidences):
out_dir = Path(config.REPORT_DIR)
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / "evidence.json"
drop = ("images", "image_b64", "image_mime")
trimmed = [{k: v for k, v in e.items() if k not in drop} for e in evidences]
path.write_text(json.dumps(trimmed, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def cmd_list_tools(ctx):
tools = ctx.tools.refresh()
print(f"共 {len(tools)} 个工具:")
for tool in tools:
print(f" - {tool['name']}")
return 0
def cmd_check(ctx):
if not ctx.client.is_available():
print(f"[X] {config.HOST}:{config.PORT} 未监听,请先启动已加载插件的 CAD。")
return 1
print(f"[OK] {config.HOST}:{config.PORT} 在监听")
info = result_json(ctx.tools.call("get_drawing_info", {}), {})
print("图纸信息:", json.dumps(info, ensure_ascii=False)[:400])
return 0
def run_review(ctx, args):
print("=== 图纸审查 Agent ===")
print(f"清单:{checklist_summary(ctx.checklist)}")
if ctx.llm is not None:
print(f"模型:{ctx.llm.model}")
print()
print("[1/4] 规划:识别图框与概览 ...")
plan = planning.plan(ctx)
frames = plan.get("frames", [])
print(f" 识别到 {len(frames)} 个图框")
if not frames:
print("未识别到任何图框,退出。")
return 1
system_prompt = build_system_prompt(ctx.checklist, allow_tools=True)
print("[2/4] 取证:逐图框采集(串行) ...")
evidences = []
# 截屏前把绘图区最大化(窗口铺满 + 干净屏幕),采集完在 finally 里还原;
# 旧版插件没有该工具时静默降级。
with evidence_phase.maximized_drawing_area(ctx):
for idx, frame in enumerate(frames, 1):
print(
f" - 图框 {idx}/{len(frames)} kind={frame.get('kind')} "
f"sheet={frame.get('sheet_size')}"
)
ev = evidence_phase.collect(ctx, idx, frame)
if ev is None:
print(" [跳过] 缺少有效包围盒")
continue
evidences.append(ev)
print(
f" 图元 {(ev.get('entities') or {}).get('total')} 条,"
f"截图 {len(evidence_images(ev))} 张"
)
if args.plan_only:
path = _dump_evidence(evidences)
print(f"[evidence] 已写入 {path}")
return 0
print("[3/4] 审查:确定性检查前置 + 逐图框 LLM 审查(含受控只读取证子循环) ...")
frame_findings = []
for ev in evidences:
print(f" - 审查图框 {ev['index']} ...")
checks = (
checks_phase.run_deterministic_checks(ctx, ev)
if config.DETERMINISTIC_CHECKS
else []
)
conclusion = review.review_frame(ctx, ev, system_prompt)
finding = findings_mod.build_finding(
ev, conclusion, checks, validate_handles=config.VALIDATE_HANDLES
)
frame_findings.append(finding)
counts = finding["counts"]
preview = finding["summary"]
if len(preview) > 200:
preview = preview[:200] + "..."
print(
f" 结论:{preview}"
f"(确定性 {counts['deterministic']} / LLM {counts['llm']}"
f" / 未核实 {counts['unverified']})"
)
print("[4/4] 汇总报告 ...")
markdown = report.summarize(ctx, frame_findings)
issues = findings_mod.flatten_issues(frame_findings)
report.save_report(markdown, frame_findings, plan, issues)
print()
print(markdown)
if args.annotate:
print()
print(f"[批注回写] {'(dry-run) ' if args.dry_run else ''}结构化问题 {len(issues)} 条")
created, highlighted = annotate(ctx, issues, dry_run=args.dry_run)
if not args.dry_run and (created or highlighted):
hint = "python -m dwg_review --clear"
if created:
hint += f" {','.join(created)}"
if highlighted:
hint += f" --restore {','.join(highlighted)}"
print(f" 撤销:{hint}")
return 0
def build_parser():
parser = argparse.ArgumentParser(
prog="dwg_review", description="CAD 图纸审查 Agent(基于 cad_mcp_frame MCP 基座)"
)
parser.add_argument("--list-tools", action="store_true", help="列出基座注册的全部工具")
parser.add_argument("--check", action="store_true", help="检查基座连通性并打印图纸概览")
parser.add_argument("--plan-only", action="store_true", help="只做规划+取证,落盘 evidence.json")
parser.add_argument("--frames", type=int, default=None, help="最多审查的图框数(0=全部)")
parser.add_argument("--annotate", action="store_true", help="审查后把问题批注回写到图上")
parser.add_argument("--dry-run", action="store_true", help="批注只打印不回写")
parser.add_argument("--clear", default=None, help="删除给定 handle(逗号分隔)以撤销批注")
parser.add_argument("--restore", default=None, help="还原给定 handle(逗号分隔)的原始颜色")
parser.add_argument("--checklist", default=None, help="审查清单 JSON 路径")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
if args.checklist:
config.CHECKLIST_PATH = Path(args.checklist)
need_llm = not (
args.list_tools or args.check or args.plan_only or args.clear or args.restore
)
try:
ctx = ReviewContext.create(need_llm=need_llm)
except McpError as exc:
print(f"[X] 无法连接 CAD 基座:{exc}")
return 1
except Exception as exc: # noqa: BLE001
print(f"[X] 初始化失败:{exc}")
return 1
if args.frames is not None:
ctx.max_frames = args.frames
try:
if args.list_tools:
return cmd_list_tools(ctx)
if args.check:
return cmd_check(ctx)
if args.clear or args.restore:
handles = [h.strip() for h in (args.clear or "").split(",") if h.strip()]
highlighted = [h.strip() for h in (args.restore or "").split(",") if h.strip()]
clear(ctx, handles, highlighted)
return 0
return run_review(ctx, args)
except (McpError, McpToolError) as exc:
print(f"[X] 调用失败:{exc}")
return 1
except KeyboardInterrupt:
print("\n[中断]")
return 130
if __name__ == "__main__":
sys.exit(main())