config.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """配置加载:优先环境变量,其次 config.yaml,最后 config.example.yaml。"""
  2. from __future__ import annotations
  3. import os
  4. from dataclasses import dataclass, field
  5. from pathlib import Path
  6. from typing import List
  7. import yaml
  8. try:
  9. from dotenv import load_dotenv
  10. load_dotenv() # 若存在 .env 则自动加载
  11. except ImportError:
  12. pass # python-dotenv 非必须
  13. @dataclass
  14. class PanelConfig:
  15. host: str = "0.0.0.0"
  16. port: int = 8080
  17. password: str = ""
  18. @dataclass
  19. class Config:
  20. app_id: str
  21. app_secret: str
  22. source_chat_ids: List[str]
  23. target_chat_ids: List[str]
  24. dedup_cache_size: int = 2000
  25. max_qps: int = 4
  26. max_retry: int = 2
  27. log_level: str = "INFO"
  28. panel: PanelConfig = field(default_factory=PanelConfig)
  29. def validate(self) -> None:
  30. if not self.app_id or not self.app_secret:
  31. raise ValueError("app_id / app_secret 不能为空(请在 config.yaml 或环境变量中配置)")
  32. if not self.source_chat_ids:
  33. raise ValueError("source_chat_ids 至少配置 1 个源群 chat_id")
  34. if not self.target_chat_ids:
  35. raise ValueError("target_chat_ids 至少配置 1 个目标群 chat_id")
  36. if self.max_qps <= 0 or self.max_qps > 5:
  37. raise ValueError(f"max_qps 必须在 (0, 5] 区间,当前 {self.max_qps}")
  38. @property
  39. def panel_password(self) -> str:
  40. """面板密码:环境变量优先,其次配置文件。"""
  41. return os.getenv("LARK_PANEL_PASSWORD") or self.panel.password
  42. def _load_yaml(path: Path) -> dict:
  43. if not path.exists():
  44. return {}
  45. with path.open("r", encoding="utf-8") as f:
  46. return yaml.safe_load(f) or {}
  47. def load_config(config_path: str | Path | None = None) -> Config:
  48. """加载顺序:环境变量 > config.yaml > config.example.yaml。"""
  49. base = Path(__file__).resolve().parent
  50. candidates = []
  51. if config_path:
  52. candidates.append(Path(config_path))
  53. candidates.append(base / "config.yaml")
  54. candidates.append(base / "config.example.yaml")
  55. merged: dict = {}
  56. for p in candidates:
  57. merged.update(_load_yaml(p))
  58. # 环境变量覆盖(优先级最高)
  59. env_app_id = os.getenv("APP_ID")
  60. env_app_secret = os.getenv("APP_SECRET")
  61. if env_app_id:
  62. merged["app_id"] = env_app_id
  63. if env_app_secret:
  64. merged["app_secret"] = env_app_secret
  65. # 标准化白名单:去空白、去空串
  66. for key in ("source_chat_ids", "target_chat_ids"):
  67. val = merged.get(key) or []
  68. merged[key] = [str(x).strip() for x in val if str(x).strip()]
  69. # panel 段
  70. panel_raw = merged.get("panel") or {}
  71. panel = PanelConfig(
  72. host=str(panel_raw.get("host", "0.0.0.0")),
  73. port=int(panel_raw.get("port", 8080)),
  74. password=str(panel_raw.get("password", "")),
  75. )
  76. try:
  77. cfg = Config(
  78. app_id=merged.get("app_id", ""),
  79. app_secret=merged.get("app_secret", ""),
  80. source_chat_ids=merged["source_chat_ids"],
  81. target_chat_ids=merged["target_chat_ids"],
  82. dedup_cache_size=int(merged.get("dedup_cache_size", 2000)),
  83. max_qps=int(merged.get("max_qps", 4)),
  84. max_retry=int(merged.get("max_retry", 2)),
  85. log_level=str(merged.get("log_level", "INFO")).upper(),
  86. panel=panel,
  87. )
  88. except KeyError as e:
  89. raise ValueError(f"配置缺失字段: {e}") from e
  90. cfg.validate()
  91. return cfg