项目文件夹

文件
wehub-resource-sync c8733f5f8c
ci / test (3.11) (push) Failing after 1s
ci / test (3.12) (push) Failing after 1s
ci / test (3.13) (push) Failing after 0s
ci / test (3.10) (push) Failing after 2s
ci / wheel-gate (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:05:22 +08:00

131 行
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""Configuration management for Agent Reach.
Stores settings in ~/.agent-reach/config.yaml.
Auto-creates directory on first use.
"""
import os
from pathlib import Path
from typing import Any, Optional
import yaml
from agent_reach.utils.paths import make_private_dir
class Config:
"""Manages Agent Reach configuration."""
CONFIG_DIR = Path.home() / ".agent-reach"
CONFIG_FILE = CONFIG_DIR / "config.yaml"
# Feature → required config keys
FEATURE_REQUIREMENTS = {
"exa_search": ["exa_api_key"],
"twitter_xreach": ["twitter_auth_token", "twitter_ct0"], # legacy key name; used by twitter-cli
"groq_whisper": ["groq_api_key"],
"openai_whisper": ["openai_api_key"],
"github_token": ["github_token"],
}
def __init__(self, config_path: Optional[Path] = None):
self.config_path = Path(config_path) if config_path else self.CONFIG_FILE
self.config_dir = self.config_path.parent
self.data: dict = {}
self._ensure_dir()
self.load()
def _ensure_dir(self):
"""Create config directory if it doesn't exist."""
make_private_dir(self.config_dir)
def load(self):
"""Load config from YAML file."""
if self.config_path.exists():
with open(self.config_path, "r", encoding="utf-8") as f:
self.data = yaml.safe_load(f) or {}
else:
self.data = {}
def save(self):
"""Save config to YAML file."""
self._ensure_dir()
# Create file with restricted permissions from the start to avoid
# a race window where credentials are briefly world-readable.
try:
import stat
fd = os.open(
str(self.config_path),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
stat.S_IRUSR | stat.S_IWUSR, # 0o600
)
if os.name != "nt":
os.chmod(self.config_path, stat.S_IRUSR | stat.S_IWUSR)
with os.fdopen(fd, "w", encoding="utf-8") as f:
yaml.dump(self.data, f, default_flow_style=False, allow_unicode=True)
except OSError:
# Fallback for Windows or other edge cases where os.open flags
# are not fully supported.
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.data, f, default_flow_style=False, allow_unicode=True)
if os.name != "nt":
os.chmod(self.config_path, 0o600)
def get(self, key: str, default: Any = None) -> Any:
"""Get a config value. Also checks environment variables (uppercase)."""
# Config file first
if key in self.data:
return self.data[key]
# Then env var (uppercase)
env_val = os.environ.get(key.upper())
if env_val:
return env_val
return default
def set(self, key: str, value: Any):
"""Set a config value and save."""
self.data[key] = value
self.save()
def delete(self, key: str):
"""Delete a config key and save."""
self.data.pop(key, None)
self.save()
def is_configured(self, feature: str) -> bool:
"""Check if a feature has all required config."""
required = self.FEATURE_REQUIREMENTS.get(feature, [])
return all(self.get(k) for k in required)
def get_configured_features(self) -> dict:
"""Return status of all optional features."""
return {
feature: self.is_configured(feature)
for feature in self.FEATURE_REQUIREMENTS
}
def to_dict(self) -> dict:
"""Return config as dict (masks sensitive values)."""
sensitive_markers = (
"key",
"token",
"password",
"proxy",
"cookie",
"secret",
"session",
"sessdata",
"csrf",
"auth",
"cred",
"ct0",
)
masked = {}
for k, v in self.data.items():
if any(s in k.lower() for s in sensitive_markers):
masked[k] = f"{str(v)[:8]}..." if v else None
else:
masked[k] = v
return masked