Files
beaver_project/app-instance/backend/nanobot/config/loader.py
2026-03-13 16:40:08 +08:00

98 lines
3.9 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.

"""Configuration loading utilities."""
import json
from pathlib import Path
from nanobot.config.schema import Config
def get_config_path() -> Path:
"""Get the default configuration file path."""
# 统一约定配置文件位置:~/.nanobot/config.json
# 这样 CLI、Gateway、测试都能复用同一入口不会出现路径分叉。
return Path.home() / ".nanobot" / "config.json"
def get_data_dir() -> Path:
"""Get the nanobot data directory."""
# 延迟导入(函数内 import可以减少模块初始化时的依赖耦合。
# get_data_path() 内部会确保目录存在。
from nanobot.utils.helpers import get_data_path
return get_data_path()
def load_config(config_path: Path | None = None) -> Config:
"""
Load configuration from file or create default.
Args:
config_path: Optional path to config file. Uses default if not provided.
Returns:
Loaded configuration object.
"""
# 如果调用者没传路径,就走默认路径 ~/.nanobot/config.json
path = config_path or get_config_path()
# 只有文件存在才尝试读取;不存在时直接返回默认 Config。
if path.exists():
try:
# 1) 读取 JSON 原始配置
with open(path, encoding="utf-8") as f:
data = json.load(f)
# 2) 做向后兼容迁移(旧字段 -> 新字段)
data = _migrate_config(data)
# 3) 用 Pydantic 做强校验与类型转换
# 例如camelCase/snake_case 映射、默认值补齐、字段类型检查。
return Config.model_validate(data)
except (json.JSONDecodeError, ValueError) as e:
# 容错策略:配置损坏时不让程序崩溃,而是退回默认配置继续运行。
print(f"Warning: Failed to load config from {path}: {e}")
print("Using default configuration.")
# 配置文件不存在,或读取失败 -> 返回 schema 里的默认配置对象。
return Config()
def save_config(config: Config, config_path: Path | None = None) -> None:
"""
Save configuration to file.
Args:
config: Configuration to save.
config_path: Optional path to save to. Uses default if not provided.
"""
# 目标路径:优先用调用方传入路径,否则走默认路径。
path = config_path or get_config_path()
# 先确保父目录存在,避免 open(..., "w") 因目录缺失而失败。
path.parent.mkdir(parents=True, exist_ok=True)
# model_dump(by_alias=True) 的关键点:
# - schema 中很多字段 Python 侧是 snake_case如 api_key
# - 配置文件对外希望保持 camelCase如 apiKey
# - by_alias=True 会把字段按 alias 输出,保证写回文件的键名与用户配置习惯一致
# (否则会写成 snake_case和 README 示例不一致)。
data = config.model_dump(by_alias=True)
# ensure_ascii=False: 保留中文等非 ASCII 字符,不转成 \uXXXX
# indent=2: 让配置文件更易读、可手工编辑。
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def _migrate_config(data: dict) -> dict:
"""Migrate old config formats to current."""
# 这个函数专门做“历史配置兼容”:
# 旧版字段tools.exec.restrictToWorkspace
# 新版字段tools.restrictToWorkspace
#
# 迁移策略:
# - 仅当旧字段存在且新字段不存在时才迁移
# - 避免覆盖用户在新字段里已经明确设置的值
tools = data.get("tools", {})
exec_cfg = tools.get("exec", {})
if "restrictToWorkspace" in exec_cfg and "restrictToWorkspace" not in tools:
tools["restrictToWorkspace"] = exec_cfg.pop("restrictToWorkspace")
# 返回迁移后的原始 dict后续再交给 Config.model_validate() 做结构化校验。
return data