"""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