Files
cad-agent/dwg_review/config.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

178 lines
9.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.
"""集中配置:全部来自环境变量,禁止硬编码密钥。
用法:所有模块统一 `from . import config` 后读取 config.XXX。
"""
import os
from pathlib import Path
# 仓库根目录(本文件位于 <root>/dwg_review/config.py)
PROJECT_ROOT = Path(__file__).resolve().parent.parent
def _resolve_path(value: str) -> Path:
"""相对路径按仓库根目录解析(避免依赖当前工作目录)。"""
p = Path(value)
return p if p.is_absolute() else (PROJECT_ROOT / p)
def _read_env(name: str) -> str:
"""读取环境变量。
Windows 下新设置的系统/用户环境变量不会传播到**已启动**的进程,
因此当前进程读不到时,回退到注册表中的 Machine / User 级环境变量。
"""
value = os.environ.get(name)
if value:
return value
if os.name != "nt":
return ""
try:
import winreg
except ImportError: # pragma: no cover
return ""
locations = (
(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"),
(winreg.HKEY_CURRENT_USER, r"Environment"),
)
for hive, subkey in locations:
try:
with winreg.OpenKey(hive, subkey) as key:
value, _ = winreg.QueryValueEx(key, name)
if value:
return str(value)
except OSError:
continue
return ""
def _read_bool(name: str, default: bool = True) -> bool:
"""读取布尔型环境变量;未设置或空串时返回 default。"""
value = os.environ.get(name)
if value is None or value.strip() == "":
return default
return value.strip().lower() in ("1", "true", "yes", "on")
# ---------------------------------------------------------------------------
# CAD MCP 基座(TCP JSON-RPC)
# ---------------------------------------------------------------------------
HOST = os.environ.get("CAD_MCP_HOST", "127.0.0.1")
PORT = int(os.environ.get("CAD_MCP_PORT", "8080"))
# 单次 TCP 调用超时(秒)
CALL_TIMEOUT = float(os.environ.get("CAD_MCP_TIMEOUT", "30"))
# 连接/超时类错误的重试次数(写类工具不重试)
CALL_RETRIES = int(os.environ.get("CAD_MCP_RETRIES", "2"))
# 帧大小上限(与基座 8MB 上限对齐,留余量)
MAX_FRAME_BYTES = int(os.environ.get("MAX_FRAME_BYTES", str(4 * 1024 * 1024)))
# ---------------------------------------------------------------------------
# LLM(默认 DeepSeek,视觉模型 deepseek-flash)
# ---------------------------------------------------------------------------
API_KEY = _read_env("DEEPSEEK_API_KEY")
BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
MODEL_NAME = os.environ.get("DEEPSEEK_MODEL", "deepseek-flash")
LLM_TIMEOUT = float(os.environ.get("LLM_TIMEOUT", "120"))
LLM_RETRIES = int(os.environ.get("LLM_RETRIES", "2"))
# ---------------------------------------------------------------------------
# 审查流程
# ---------------------------------------------------------------------------
# 审查清单路径
CHECKLIST_PATH = _resolve_path(os.environ.get("REVIEW_CHECKLIST", "review_checklist.json"))
# 最多审查多少个图框(0 = 全部)
MAX_FRAMES = int(os.environ.get("MAX_FRAMES", "0"))
# 每个图框取数据的条数上限
FRAME_ENTITY_LIMIT = int(os.environ.get("FRAME_ENTITY_LIMIT", "200"))
# 缩放后等待 CAD 渲染的秒数
RENDER_WAIT = float(os.environ.get("RENDER_WAIT", "0.6"))
# 审查阶段受控工具子循环的最大回合数
REVIEW_MAX_ITERS = int(os.environ.get("REVIEW_MAX_ITERS", "4"))
# 审查子循环里回灌给 LLM 的截图数上限(控制视觉 token 成本)。
# 实测:提示词鼓励「看不清→放大重看」后,模型一轮内会发多次 zoom+截图,
# 上限过小会让它白拍(拍了却看不到)。6 可覆盖多数复核需求。
REVIEW_OBS_IMAGES = int(os.environ.get("REVIEW_OBS_IMAGES", "6"))
# 报告输出目录
REPORT_DIR = _resolve_path(os.environ.get("REPORT_DIR", "reports"))
# 批注回写使用的图层
ANNOTATE_LAYER = os.environ.get("ANNOTATE_LAYER", "AI_REVIEW")
# ---------------------------------------------------------------------------
# 可信度三件套:确定性检查前置 / 结论结构化 / 证据交叉校验
# ---------------------------------------------------------------------------
# 是否在 LLM 审查前跑确定性检查(零长度、重复实体、空文字、未闭合多段线)
DETERMINISTIC_CHECKS = _read_bool("DETERMINISTIC_CHECKS", True)
# 是否校验 LLM 引用的 handle 确实出现在已采集证据中(不在则剥掉 handle)
VALIDATE_HANDLES = _read_bool("VALIDATE_HANDLES", True)
# 是否检查未闭合多段线(中心线/开放轮廓多时误报,可关闭)
CHECK_UNCLOSED_POLYLINES = _read_bool("CHECK_UNCLOSED_POLYLINES", True)
# find_zero_length 的长度阈值
ZERO_LENGTH_TOLERANCE = float(os.environ.get("ZERO_LENGTH_TOLERANCE", "0.001"))
# find_duplicate_entities 的几何量化容差
DUP_TOLERANCE = float(os.environ.get("DUP_TOLERANCE", "0.001"))
# find_duplicate_entities 的 limit(语义是「最大重复组数」,非条目数)
DUP_GROUP_LIMIT = int(os.environ.get("DUP_GROUP_LIMIT", "50"))
# ---------------------------------------------------------------------------
# 截图(视觉 token 成本控制)
# ---------------------------------------------------------------------------
# 长边上限(超过才缩放),0 表示不缩放
IMAGE_MAX_EDGE = int(os.environ.get("IMAGE_MAX_EDGE", "1280"))
# 缩放后的输出格式:png(默认,无损,适合 CAD 线稿)或 jpeg(体积更小)
IMAGE_FORMAT = os.environ.get("IMAGE_FORMAT", "png").strip().lower()
# JPEG 质量(仅 IMAGE_FORMAT=jpeg 时生效)
IMAGE_JPEG_QUALITY = int(os.environ.get("IMAGE_JPEG_QUALITY", "80"))
# 取证阶段每图框的局部截图数(0=仅整体;4=整体+2x2 象限)
# —— 仅在自适应瓦片规划不可用时作为**回退**路径参数
EVIDENCE_DETAIL_TILES = int(os.environ.get("EVIDENCE_DETAIL_TILES", "4"))
# 每图框图片硬上限(整体 + 局部总张数)。
# 默认 12 才能装下规划器给出的瓦片(实测测试图 A0 需 12 张);调小会截断规划结果。
EVIDENCE_MAX_IMAGES = int(os.environ.get("EVIDENCE_MAX_IMAGES", "12"))
# ---------------------------------------------------------------------------
# 自适应瓦片规划(按实体分布决定截几张、截哪里)
# ---------------------------------------------------------------------------
# 是否启用瓦片规划;False = 退回固定 EVIDENCE_DETAIL_TILES(行为与旧版一致)
TILING_ENABLED = _read_bool("TILING_ENABLED", True)
# 纸面字高(mm)。国标标注字高 2~3mm,取保守下限
TEXT_HEIGHT_MM = float(os.environ.get("TEXT_HEIGHT_MM", "2.0"))
# VLM 可读的**最小像素字高**。实测与**字体强相关**:
# - 离线合成(Consolas 抗锯齿填充字体)12px(白底 10px);
# - 真实 CAD 单线体 .shx(描边字体)6px 即可读对(3/3,两轮)。
# 取 8 折中:兼顾描边与常见 TrueType。标定脚本见 verify_vlm_resolution_offline.py /
# verify_text_resolution.py(后者须用 ACI 7 白字——红字 12px 因对比度不足完全读不出)。
TEXT_READABLE_PX = int(os.environ.get("TEXT_READABLE_PX", "8"))
# VLM 可分辨的**最小线间距像素**(1 条 vs 2 条二选一)。
# 实测 2/3/4px 失败、5/8/12px 成功;取 6 留一点余量。
LINE_READABLE_PX = int(os.environ.get("LINE_READABLE_PX", "6"))
# 单图框瓦片数硬上界(成本兜底)
TILE_MAX_COUNT = int(os.environ.get("TILE_MAX_COUNT", "36"))
# 相邻瓦片重叠比例(防边界切字)
TILE_OVERLAP = float(os.environ.get("TILE_OVERLAP", "0.0"))
# 热力图网格划分(长边切几份)
HEATMAP_CELL_DIV = int(os.environ.get("HEATMAP_CELL_DIV", "32"))
# 热力图聚类前的膨胀格数(合并相近内容;0=不膨胀)
HEATMAP_CLUSTER_DILATE = int(os.environ.get("HEATMAP_CLUSTER_DILATE", "0"))
# 丢弃 bbox 面积超过图框该比例的实体(去图框边框/图签块)
HEATMAP_MAX_AREA_FRAC = float(os.environ.get("HEATMAP_MAX_AREA_FRAC", "0.5"))
# ---------------------------------------------------------------------------
# 绘图区最大化(截屏前把 CAD 窗口铺满屏幕 + 进入干净屏幕)
# ---------------------------------------------------------------------------
# 是否在取证前最大化绘图区。实测绘图区面积 2110x462 → 3310x1084(3.68x),
# 同样的视觉 token 预算下细节更清楚。工具不存在(旧版插件)时**静默降级**。
DRAWING_AREA_MAXIMIZE = _read_bool("DRAWING_AREA_MAXIMIZE", True)
# 进入干净屏幕后等待 CAD 渲染 / 重排窗口的秒数
DRAWING_AREA_ENTER_WAIT = float(os.environ.get("DRAWING_AREA_ENTER_WAIT", "1.5"))
# 还原后等待的秒数。插件内部是「700ms 延迟 + SetWindowPlacement」(CLEANSCREENOFF 是
# 异步命令,会把窗口几何改回它记住的布局,必须等它跑完再压回),实测窗口在 0.5~1.0s
# 之间收敛,故取 1.5s 留余量——太小会在界面还没回位时就开始下一步。
DRAWING_AREA_RESTORE_WAIT = float(os.environ.get("DRAWING_AREA_RESTORE_WAIT", "1.5"))
def has_api_key() -> bool:
return bool(API_KEY.strip())