110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""core/config_paths.py —— 统一的配置文件路径解析与容错加载(P0-01 新增)
|
||
|
||
所有配置读取入口必须经过本模块,不得在别处直接定位 data/config.json:
|
||
|
||
1. 环境变量 HAOCODE_CONFIG_FILE 优先级最高(自动化测试用它指向临时文件);
|
||
2. 未设置环境变量时才回落到项目内 data/config.json(锚定 core/ 目录,不受 CWD 影响)。
|
||
|
||
行为约定(REPAIR_BACKLOG P0-01 硬约束):
|
||
· 路径解析与加载过程只在出错时输出可见警告(只含路径与错误类型,不含配置内容);
|
||
· 缺失 / 损坏 / 非 JSON 对象 → 返回安全空 dict 继续,不抛异常、不阻断
|
||
不需要该配置的源码路径;
|
||
· import 本模块时不打印、不复制、不迁移任何配置内容。
|
||
"""
|
||
import json
|
||
import os
|
||
import tempfile
|
||
|
||
ENV_KEY = "HAOCODE_CONFIG_FILE"
|
||
|
||
_DEFAULT_CONFIG_PATH = os.path.join(
|
||
os.path.dirname(os.path.abspath(__file__)), "..", "data", "config.json")
|
||
|
||
|
||
def config_path() -> str:
|
||
"""返回当前生效的配置文件路径(环境变量优先,调用时解析)。"""
|
||
p = os.environ.get(ENV_KEY, "").strip()
|
||
return p if p else _DEFAULT_CONFIG_PATH
|
||
|
||
|
||
def load_config() -> dict:
|
||
"""容错加载配置。缺失/损坏/非对象 → 可见警告 + 安全空 dict(绝不抛异常)。"""
|
||
p = config_path()
|
||
if not os.path.exists(p):
|
||
print(f"[config] 配置文件缺失: {p} → 使用安全默认值(providers 为空)继续启动")
|
||
return {}
|
||
try:
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
except Exception as e:
|
||
print(f"[config] 配置文件读取/解析失败: {p}({type(e).__name__})"
|
||
f" → 使用安全默认值继续启动")
|
||
return {}
|
||
if not isinstance(data, dict):
|
||
print(f"[config] 配置文件内容不是 JSON 对象: {p}"
|
||
f" → 使用安全默认值继续启动")
|
||
return {}
|
||
return data
|
||
|
||
|
||
def save_config(data: dict) -> bool:
|
||
"""原子保存当前配置;失败时保留原文件并返回 False。"""
|
||
if not isinstance(data, dict):
|
||
print("[config] 拒绝保存:配置内容不是 JSON 对象")
|
||
return False
|
||
|
||
path = os.path.abspath(config_path())
|
||
parent = os.path.dirname(path)
|
||
temp_path = ""
|
||
try:
|
||
os.makedirs(parent, exist_ok=True)
|
||
fd, temp_path = tempfile.mkstemp(
|
||
prefix=".haocode_config_", suffix=".tmp", dir=parent)
|
||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||
json.dump(data, handle, ensure_ascii=False, indent=2)
|
||
handle.write("\n")
|
||
handle.flush()
|
||
os.fsync(handle.fileno())
|
||
os.replace(temp_path, path)
|
||
return True
|
||
except Exception as exc:
|
||
print(f"[config] 配置保存失败: {path}({type(exc).__name__})")
|
||
return False
|
||
finally:
|
||
if temp_path and os.path.exists(temp_path):
|
||
try:
|
||
os.remove(temp_path)
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
# ----------------------------------------------------------------------
|
||
# P1-01:渲染窗口配置解析(render_window_mode / render_window_size)
|
||
# 规则(与 ui/web/render_window.js 的 JS 侧守卫保持一致):
|
||
# size:只接受非布尔整数 10..200,缺失/布尔/字符串/小数/零/负数/越界 → 静默回落 40;
|
||
# mode:只接受 "auto"/"manual",否则回落 "auto"。
|
||
# ----------------------------------------------------------------------
|
||
DEFAULT_RENDER_WINDOW_SIZE = 40
|
||
ALLOWED_RENDER_WINDOW_SIZES = (10, 40, 200)
|
||
|
||
|
||
def render_window_settings(cfg: dict | None = None) -> dict:
|
||
"""解析渲染窗口配置。cfg 缺省时读取当前生效配置。永不抛异常。"""
|
||
if cfg is None:
|
||
cfg = load_config()
|
||
if not isinstance(cfg, dict):
|
||
cfg = {}
|
||
|
||
raw_size = cfg.get("render_window_size", None)
|
||
if isinstance(raw_size, bool) or not isinstance(raw_size, int) \
|
||
or raw_size < 10 or raw_size > 200:
|
||
size = DEFAULT_RENDER_WINDOW_SIZE
|
||
else:
|
||
size = raw_size
|
||
|
||
raw_mode = cfg.get("render_window_mode", None)
|
||
mode = raw_mode if raw_mode in ("auto", "manual") else "auto"
|
||
|
||
return {"mode": mode, "size": size}
|