Commit Graph
14 Commits
Author SHA1 Message Date
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
gjm d4a9442e30 feat(review): 几何干涉检查 find_collisions(BVH 粗筛 + 容器过滤)
C++(cad_mcp_plugins):
- 新增 bvh.h:header-only 2D BVH(质心中位分割,叶<=4,SelfPairs 交叉对枚举)
- 新增 find_collisions.cpp:粗筛 AABB 相交实体对,附重叠盒 + 逐轴重叠比 ratio
  (= 逐轴重叠长/较短长 之积,退化轴取 1.0),按 ratio 降序 + 分页;
  narrow=true 只对返回页跑 intersectWith 细筛(附 exact)
- 容器过滤(关键):图框块/分区边界会与一切相交,用重叠【度】判定
  deg >= max(exclude_container_deg=8, exclude_container_frac=0.25 * 其他实体数),
  与图幅/缩放无关;被剔除者在 excluded_containers 里如实报告
  (初版用面积判据对多图框并排失效:3 框只占并集 27/13/7% 全部漏过,
   全模型 163 对噪声;改度判据后图框 deg=35/25/23 vs 其余 deg<=3,分离干净)
- cmakelists.txt + 两个 mcp_config.json 注册(28 工具)

agent 侧(Python):
- cad_tools.READ_ONLY_TOOLS += find_collisions(受控子循环可调用)
- checks.check_collisions + run_deterministic_checks(受 CHECK_COLLISIONS 开关,默认关)
- config += CHECK_COLLISIONS=False / COLLISION_MIN_OVERLAP=0.5
- checklist.py / review_checklist.json += 「干涉/碰撞」类别(high)+ 提示词协议
  (先 find_collisions 拿候选 → inspect_region 目视确认;AABB 相交 != 真干涉)

验证:
- verify_collisions.py 17/17 PASS:全模型候选 163→80(全部 ratio=0.000),
  min_overlap=0.5 → 0 对;逐图框 0 对;narrow top-5 中 4 条 exact=true;
  --inject 注入深重叠矩形报出 ratio=0.902 并清理 2/2
- pytest 355 passed(新增 12 项)
- 端到端:LLM 主动调用 find_collisions{min_overlap:0.2,narrow:true,window:<图框>},未误报
2026-10-16 19:46:44 +08:00
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
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
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
gjm 9c4ab62dc1 feat(tiling): CAD 实测标定 R_text/R_line + 瓦片粒度内容过滤 + 默认参数定稿
Stage B2(CAD 实测标定)与收尾:

- 新增 verify_text_resolution.py:临时加字(字高 200 图形单位 = 1:100 纸面 2mm)+
  平行线对,按目标像素换算 zoom 窗口 → 截图 → 问 VLM;测完 delete_entities 清理不保存。
- 标定结论:
  * **可读下限与字体强相关**——离线合成(Consolas 抗锯齿填充字体)12px;
    真实 CAD 单线体 .shx(描边字体)6px 即可读对(3/3,两轮)。
  * 线条「1 条 vs 2 条」:2/3/4px 失败,5/8/12px 成功 → R_line ≈ 5px。
  * **必须用 ACI 7 白字**:实测红字(ACI 1)12px 因对比度不足完全读不出。
- config 定稿:TEXT_READABLE_PX=8(折中)、LINE_READABLE_PX=6、
  EVIDENCE_MAX_IMAGES=12(原 5 会把规划结果截到只剩 4 张瓦片)。
- 新增 heatmap.occupied_boxes:占用格的**世界 bbox**。
- tiling.plan_regions 新增 keep 谓词;evidence._plan_tiles 按占用格剔除**空瓦片**
  (簇 bbox 是包围盒,L 形/斜向分布含大片空白,实测有效)。
- 测试:test_heatmap 补 occupied_boxes;test_tiling 补 keep 过滤;全量 286 项全绿。
- 真实图纸端到端验证:A0 图框出 12 张(1 整体 + 11 瓦片),单框审查 1m38s 通过。
- 记忆库同步:工具语义坑(type_filter 必须传列表 / layer_filter 是通配字符串 /
  add_* 返回句柄在 created 键)、标定结论与默认值。
2026-10-15 23:22:15 +08:00
gjm cba05b0c65 feat(tiling): 自适应瓦片规划器 + VLM 分辨率离线标定
背景:逐图框审查固定「整体 + 2x2 象限」截图,大图幅(A0/A1)读不清细节。
改为按**实体分布**自适应决定截几张、截哪里。

- 新增 dwg_review/tiling.py(纯函数):
  SHEET_SIZES_MM / frame_world_size / frame_scale(sheet_scale>0 用之,否则由
  bbox/纸张反算,否则 default)/ effective_px(与 downsample 同规则)/
  world_per_px / is_text_type(标准名 + 自定义类名含 TEXT/LABEL/DIM/NOTE/TAG/
  ANNO/TITLE)/ cluster_text_flags / tiles_for_box / subdivide / plan_regions
  (文字簇优先、空白区不出图、max_tiles 截断、窗口夹到图框范围)
- 新增 verify_vlm_resolution_offline.py:PIL 合成类图纸图(精确像素字高 +
  可控线间距/线宽/虚线)→ 问 VLM 强约束 JSON → 定 R_text / R_line。
  实测:R_text=12px(黑底;白底 10px);R_line=6px。
  另:实测该模型**无法可靠计数**(6 条线在 2~10px 间距下答 4/10/4/5/4/4),
  故线条判据改用「1 条 vs 2 条」二选一。
- config 新增 TILING_ENABLED / TEXT_HEIGHT_MM / TEXT_READABLE_PX=12 /
  LINE_READABLE_PX=6 / TILE_MAX_COUNT / TILE_OVERLAP / HEATMAP_CELL_DIV /
  HEATMAP_CLUSTER_DILATE / HEATMAP_MAX_AREA_FRAC;EVIDENCE_DETAIL_TILES 与
  EVIDENCE_MAX_IMAGES 保留作回退路径参数
- llm_client 新增 image_size_b64(测量绘图区实际像素,决定单张覆盖范围)
- phases/evidence.capture_images 支持瓦片规划:先截整体图并测像素 →
  plan_regions 成功用规划窗口,否则退回 sub_windows(行为零变化);
  末尾始终复位整体视口。collect_frame 传实体/文字 bbox;任一信封
  total>returned(截断)→ 退回固定策略,避免误删有内容的瓦片
- 测试:tests/test_tiling.py(28 项)、tests/test_evidence.py 新增规划路径与
  降级用例、test_config / test_llm_client 补新常量与 image_size_b64;
  全量 283 项全绿
2026-10-15 23:03:27 +08:00
gjm 9b8a2329df feat(heatmap): 实体分布热力图(占用网格 + 聚类 + 可视化)
- 新增 dwg_review/heatmap.py:entity_boxes / build_grid(counts+coverage)/ occupied_cells /
  cluster_cells(4-连通,可选膨胀合并)/ grid_to_windows / grid_stats / render_overlay
- drop_oversized:过滤 bbox 占满图框的实体(实测每框恰 1 个,即图框块),否则热力图全满
- 新增 verify_heatmap.py:取实体 bbox(分页)-> 建网格 -> 聚类 -> 渲染 PNG,并缓存 bbox JSON 供离线调参
- 新增 tests/test_heatmap.py(22 项)
2026-10-15 22:51:05 +08:00
gjm 1c9fc72839 feat(review): 取证层多源化 + 视觉多图(整体+2x2 象限)
- 新增 evidence_model.py:统一证据模型(source_type + images 列表),访问器兼容旧 image_b64/image_mime 单图字段
- phases/evidence.py:新增 collect() 多源分发 seam(仅 CAD)、capture_images() 整体+等分象限多图(结束复位视口)、sub_windows()
- phases/review.py:多图逐张下采样喂 LLM,payload 增加 source_type
- config.py:EVIDENCE_DETAIL_TILES=4 / EVIDENCE_MAX_IMAGES=5(默认开启多图)
- 新增 tests/test_evidence_model.py,更新 evidence/review 断言与 fakes 归一
2026-10-15 19:48:18 +08:00
gjm 81d8c3d2ca fix(review): 批注去重 + 删除残留调试截图
- annotator.annotate:同一 handle 只批注一次(修复 created 无去重的既有 gap,
  同 handle 出现两次不再产生重复引线)
- 删除 test_screenshot.png(已删除脚本 test_autoagent.py 的残留产物)
2026-10-14 19:53:41 +08:00
gjm f7fa8f14d9 feat(review): 审查结论改为结构化 JSON,移除二次 LLM 抽取
- checklist.py:系统提示加入结构化输出契约(只输出 {summary, issues[]},
  引用 handle 必须真实存在、不得编造)
- review.py:review_frame 返回 {summary, issues, raw};仅在「不再请求工具」的回合
  解析结论,中间工具回合的 content 永不解析;强制收口那次 chat 不传 tools
- report.py:删除 extract_issues 与 _EXTRACT_SYSTEM(不再需要第二次 LLM 调用);
  新增 format_finding(用 .get,顺带修复缺 frame 键 KeyError);save_report 的
  issues 省略时从 findings 拍平
- __main__.py:主流程改为 确定性检查 → review_frame → build_finding(合并+校验),
  issues 由 flatten_issues 派生

顺带修复既有 gap:summarize 硬下标 KeyError、extract_issues 单行围栏解析失败。
2026-10-14 19:53:41 +08:00
gjm f6d61f8d07 feat(review): 新增确定性检查前置与结论结构化内核
- checks.py:确定性检查(find_zero_length / find_duplicate_entities 传 window
  + 纯 Python 派生空文字、未闭合多段线)。问题带 source=deterministic,handle 来自
  工具/数据本身,不依赖 LLM 回忆
- findings.py:解析 LLM 的 JSON 结论 + handle 交叉校验(引用未知 handle 时保留问题、
  剥掉 handle、verified=False)+ findings 装配
- config.py:新增 DETERMINISTIC_CHECKS / VALIDATE_HANDLES / CHECK_UNCLOSED_POLYLINES
  开关与 ZERO_LENGTH_TOLERANCE / DUP_TOLERANCE / DUP_GROUP_LIMIT

实机验证:find_zero_length / find_duplicate_entities 的 window 参数在基座上生效
(schema 未声明,C++ 侧 parseCommonQuery 实际支持)。
2026-10-14 19:53:31 +08:00
gjm daccc734bf test(review): 把 5 处既有瑕疵锁成 *_known_gap 用例
让既有瑕疵可见,而不是被当成正确行为;改 1~3(确定性检查前置/结论结构化/
证据交叉校验)时可据此判断行为是否被改变。

- bbox_to_window 含非数值 → ValueError(上游只判长度)
- summarize / extract_issues 硬下标 f['frame'] → KeyError
- annotate 的 created 无去重(同 handle 产生重复批注)
- extract_issues 单行 ```json[...]``` 围栏剥离失败 → []
- result_json 文本 'null' → None(与「无 text 返回 default」语义易混)
2026-10-14 19:09:56 +08:00
gjm 0656397a68 test(review): 新增 pytest 离线回归网,锁定 dwg_review 既有行为
在改动审查流水线(确定性检查前置 / 结论结构化 / 证据交叉校验)之前,
先立一张离线、确定性、秒级的回归网。

- tests/fakes.py:假 LLM + 假 MCP 基座 + 响应构造器(ok_text/ok_image/err)
  + make_ctx/make_evidence,绝不触网、不读密钥
- 10 个模块共 134 用例:
  mcp_client / llm_client / cad_tools / checklist / config /
  planning / evidence / review / report / annotator
- pytest.ini(testpaths=tests)+ requirements-dev.txt + 根 conftest.py
  (sys.path + RENDER_WAIT=0 / REPORT_DIR=tmp fixture)
2026-10-14 19:07:27 +08:00