Coverage for config_manager.py: 62%
89 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
1"""
2配置管理模块
4统一管理用户配置,所有配置文件存放在 ~/.qrclaw/ 目录下:
5- ~/.qrclaw/config.yaml 用户配置(API Key 等)
6- ~/.qrclaw/permissions.yaml 权限配置
7- ~/.qrclaw/MEMORY.md 中期记忆
8- ~/.qrclaw/sessions/ 会话历史
9- ~/.qrclaw/logs/ 日志文件
10"""
12import os
13import yaml
14from pathlib import Path
15from typing import Any
16from qrclaw.logger import get_logger
18logger = get_logger("qrclaw.config_manager")
20# 配置目录
21CONFIG_DIR = Path.home() / ".qrclaw"
22CONFIG_FILE = CONFIG_DIR / "config.yaml"
24# 默认配置
25DEFAULT_CONFIG = {
26 "agent": {
27 "name": "QRClaw",
28 "max_iterations": 50,
29 },
30 "llm": {
31 "provider": "openai", # openai | vertex
32 "openai": {
33 "api_key": "",
34 "model": "gpt-4o",
35 "base_url": "",
36 "max_tokens": 128000,
37 },
38 "vertex": {
39 "api_key": "",
40 },
41 },
42 "search": {
43 "tavily_api_key": "",
44 },
45 "log": {
46 "level": "INFO",
47 "max_days": 30,
48 "to_file": True,
49 "to_console": True,
50 "console_level": "WARNING",
51 },
52 "heartbeat": {
53 "enabled": True,
54 "interval": 3600,
55 },
56 "compress": {
57 "threshold_ratio": 0.6,
58 "target_min_ratio": 0.20,
59 "target_max_ratio": 0.25,
60 },
61}
63# 配置注释
64CONFIG_HEADER = """# ═══════════════════════════════════════════════════════════════
65# QRClaw 配置文件
66# ═══════════════════════════════════════════════════════════════
67# 文档:https://github.com/fu-qingrong/qrclaw
68# 修改配置后无需重启,下次启动自动生效。
69# ═══════════════════════════════════════════════════════════════
71"""
74def ensure_config_dir():
75 """确保配置目录存在"""
76 CONFIG_DIR.mkdir(parents=True, exist_ok=True)
79def _write_config(config: dict):
80 """写入配置文件"""
81 ensure_config_dir()
82 with open(CONFIG_FILE, "w", encoding="utf-8") as f:
83 f.write(CONFIG_HEADER)
84 yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
87def _deep_merge(base: dict, override: dict) -> dict:
88 """深度合并两个字典"""
89 result = base.copy()
90 for key, value in override.items():
91 if key in result and isinstance(result[key], dict) and isinstance(value, dict):
92 result[key] = _deep_merge(result[key], value)
93 else:
94 result[key] = value
95 return result
98def init_config():
99 """初始化配置文件(不存在则创建默认配置)"""
100 ensure_config_dir()
102 if CONFIG_FILE.exists():
103 logger.debug(f"使用配置文件: {CONFIG_FILE}")
104 return
106 _write_config(DEFAULT_CONFIG)
107 logger.info(f"创建默认配置文件: {CONFIG_FILE}")
108 logger.info("请编辑配置文件,填入你的 API Key")
111def load_config():
112 """加载配置并注入到环境变量"""
113 if not CONFIG_FILE.exists():
114 init_config()
116 try:
117 with open(CONFIG_FILE, "r", encoding="utf-8") as f:
118 config = yaml.safe_load(f) or {}
120 config = _deep_merge(DEFAULT_CONFIG, config)
121 _inject_to_env(config)
122 logger.debug(f"已加载配置: {CONFIG_FILE}")
124 except Exception as e:
125 logger.error(f"加载配置失败: {e}", exc_info=True)
128def _inject_to_env(config: dict):
129 """将配置注入到环境变量"""
130 os.environ.setdefault("AGENT_NAME", config["agent"]["name"])
131 os.environ.setdefault("MAX_ITERATIONS", str(config["agent"]["max_iterations"]))
133 os.environ.setdefault("LLM_PROVIDER", config["llm"]["provider"])
134 os.environ.setdefault("OPENAI_API_KEY", config["llm"]["openai"]["api_key"])
135 os.environ.setdefault("OPENAI_MODEL", config["llm"]["openai"]["model"])
136 os.environ.setdefault("OPENAI_BASE_URL", config["llm"]["openai"]["base_url"])
137 os.environ.setdefault("MODEL_MAX_TOKENS", str(config["llm"]["openai"]["max_tokens"]))
138 os.environ.setdefault("VERTEX_API_KEY", config["llm"]["vertex"]["api_key"])
140 os.environ.setdefault("TAVILY_API_KEY", config["search"]["tavily_api_key"])
142 os.environ.setdefault("LOG_LEVEL", config["log"]["level"])
143 os.environ.setdefault("LOG_MAX_DAYS", str(config["log"]["max_days"]))
144 os.environ.setdefault("LOG_TO_FILE", str(config["log"]["to_file"]).lower())
145 os.environ.setdefault("LOG_TO_CONSOLE", str(config["log"]["to_console"]).lower())
146 os.environ.setdefault("LOG_CONSOLE_LEVEL", config["log"]["console_level"])
148 os.environ.setdefault("HEARTBEAT_ENABLED", str(config["heartbeat"]["enabled"]).lower())
149 os.environ.setdefault("HEARTBEAT_INTERVAL", str(config["heartbeat"]["interval"]))
152def get_config() -> dict:
153 """获取完整配置字典"""
154 if not CONFIG_FILE.exists():
155 return DEFAULT_CONFIG.copy()
157 with open(CONFIG_FILE, "r", encoding="utf-8") as f:
158 config = yaml.safe_load(f) or {}
160 return _deep_merge(DEFAULT_CONFIG, config)
163def get(key: str, default: Any = None) -> Any:
164 """获取配置项(支持点号路径,如 llm.openai.api_key)"""
165 config = get_config()
166 keys = key.split(".")
167 value = config
169 for k in keys:
170 if isinstance(value, dict) and k in value:
171 value = value[k]
172 else:
173 return default
175 return value
178def set_config(key: str, value: Any):
179 """设置配置项(支持点号路径)"""
180 config = get_config()
181 keys = key.split(".")
182 target = config
184 for k in keys[:-1]:
185 if k not in target:
186 target[k] = {}
187 target = target[k]
189 target[keys[-1]] = value
190 _write_config(config)
193def get_config_path() -> Path:
194 """获取配置文件路径"""
195 return CONFIG_FILE
198def get_config_dir() -> Path:
199 """获取配置目录路径"""
200 return CONFIG_DIR