"""集中配置:全部来自环境变量,禁止硬编码密钥。 用法:所有模块统一 `from . import config` 后读取 config.XXX。 """ import os from pathlib import Path # 仓库根目录(本文件位于 /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())