第一次提交

This commit is contained in:
2026-03-13 16:40:08 +08:00
commit 0a49bcfb2d
277 changed files with 61890 additions and 0 deletions

View File

@ -0,0 +1,97 @@
"""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