2026-10-13 23:44:11 +08:00
|
|
|
|
"""集中配置:全部来自环境变量,禁止硬编码密钥。
|
|
|
|
|
|
|
|
|
|
|
|
用法:所有模块统一 `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 ""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-14 19:53:31 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-13 23:44:11 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 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"))
|
|
|
|
|
|
# 报告输出目录
|
|
|
|
|
|
REPORT_DIR = _resolve_path(os.environ.get("REPORT_DIR", "reports"))
|
|
|
|
|
|
# 批注回写使用的图层
|
|
|
|
|
|
ANNOTATE_LAYER = os.environ.get("ANNOTATE_LAYER", "AI_REVIEW")
|
|
|
|
|
|
|
2026-10-14 19:53:31 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 可信度三件套:确定性检查前置 / 结论结构化 / 证据交叉校验
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 是否在 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"))
|
|
|
|
|
|
|
2026-10-13 23:44:11 +08:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 截图(视觉 token 成本控制)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-10-13 23:48:46 +08:00
|
|
|
|
# 长边上限(超过才缩放),0 表示不缩放
|
2026-10-13 23:44:11 +08:00
|
|
|
|
IMAGE_MAX_EDGE = int(os.environ.get("IMAGE_MAX_EDGE", "1280"))
|
2026-10-13 23:48:46 +08:00
|
|
|
|
# 缩放后的输出格式:png(默认,无损,适合 CAD 线稿)或 jpeg(体积更小)
|
|
|
|
|
|
IMAGE_FORMAT = os.environ.get("IMAGE_FORMAT", "png").strip().lower()
|
|
|
|
|
|
# JPEG 质量(仅 IMAGE_FORMAT=jpeg 时生效)
|
2026-10-13 23:44:11 +08:00
|
|
|
|
IMAGE_JPEG_QUALITY = int(os.environ.get("IMAGE_JPEG_QUALITY", "80"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def has_api_key() -> bool:
|
|
|
|
|
|
return bool(API_KEY.strip())
|