settings.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. """Runtime settings for zhuce6."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass
  4. import os
  5. from pathlib import Path
  6. from .env_loader import bootstrap_env
  7. bootstrap_env()
  8. from ops.common import DEFAULT_POOL_DIR
  9. from .paths import (
  10. DEFAULT_ACCOUNT_SURVIVAL_STATE_FILE,
  11. DEFAULT_DASHBOARD_LOG_FILE,
  12. DEFAULT_ENV_FILE,
  13. LOG_DIR,
  14. PROJECT_ROOT,
  15. DEFAULT_RESPONSES_SURVIVAL_STATE_FILE,
  16. DEFAULT_RUNTIME_STATE_FILE,
  17. CONFIG_DIR,
  18. STATE_DIR,
  19. )
  20. def _env_bool(name: str, default: bool) -> bool:
  21. raw = str(os.getenv(name, str(default))).strip().lower()
  22. return raw in {"1", "true", "yes", "on"}
  23. @dataclass(frozen=True)
  24. class AppSettings:
  25. runtime_mode: str = "full"
  26. host: str = "127.0.0.1"
  27. port: int = 8000
  28. project_root: Path = PROJECT_ROOT
  29. config_dir: Path = CONFIG_DIR
  30. state_dir: Path = STATE_DIR
  31. log_dir: Path = LOG_DIR
  32. env_file: Path = DEFAULT_ENV_FILE
  33. cleanup_enabled: bool = True
  34. validate_enabled: bool = True
  35. cleanup_interval: int = 300
  36. validate_interval: int = 180
  37. d1_cleanup_enabled: bool = True
  38. d1_cleanup_interval: int = 1800
  39. d1_database_id: str = ""
  40. d1_mail_retention_hours: int = 2
  41. d1_address_retention_hours: int = 24
  42. pool_dir: Path = DEFAULT_POOL_DIR
  43. cleanup_proxy: str | None = None
  44. validate_proxy: str | None = None
  45. validate_scope: str = "all"
  46. cpa_management_base_url: str = "http://127.0.0.1:8317/v0/management"
  47. cpa_management_key: str | None = None
  48. backend: str = "cpa"
  49. sub2api_base_url: str = "http://127.0.0.1:8080"
  50. sub2api_admin_email: str = ""
  51. sub2api_admin_password: str = ""
  52. sub2api_api_key: str = ""
  53. validate_max_workers: int = 8
  54. rotate_enabled: bool = True
  55. rotate_interval: int = 300
  56. rotate_probe_workers: int = 8
  57. rotate_fresh_grace_seconds: int = 600
  58. # Registration loop settings
  59. register_enabled: bool = False
  60. register_threads: int = 8
  61. register_interval: int = 5
  62. register_proxy: str | None = "http://127.0.0.1:7899"
  63. register_mail_provider: str = "cfmail"
  64. register_sleep_min: int = 3
  65. register_sleep_max: int = 10
  66. register_target_count: int = 0 # 0 = unlimited
  67. register_batch_threads: int = 1
  68. register_batch_target_count: int = 20
  69. register_batch_interval_seconds: int = 10800
  70. register_max_consecutive_failures: int = 3
  71. register_log_file: str = str(LOG_DIR / "register.log")
  72. dashboard_log_file: str = str(DEFAULT_DASHBOARD_LOG_FILE)
  73. dashboard_allowed_origins: tuple[str, ...] = ()
  74. enable_proxy_pool: bool = True
  75. proxy_pool_config: Path | None = PROJECT_ROOT / "clash_config.yaml"
  76. proxy_pool_direct_urls: str = ""
  77. proxy_pool_regions: tuple[str, ...] = ("tw", "sg", "jp", "hk", "us")
  78. proxy_pool_size: int = 20
  79. proxy_pool_exclude_names: tuple[str, ...] = ()
  80. proxy_pool_preferred_patterns: tuple[str, ...] = ()
  81. register_fresh_proxy_regions: tuple[str, ...] = ("tw", "sg")
  82. runtime_state_file: Path = DEFAULT_RUNTIME_STATE_FILE
  83. recycle_rewarm_cooldown_seconds: int = 1800 # DEPRECATED: unused after single-pool refactor
  84. cpa_runtime_reconcile_enabled: bool = True
  85. cpa_runtime_reconcile_cooldown_seconds: int = 300
  86. cpa_runtime_reconcile_restart_enabled: bool = False
  87. account_survival_enabled: bool = True
  88. account_survival_interval: int = 120
  89. account_survival_cohort_size: int = 10
  90. account_survival_proxy: str | None = None
  91. account_survival_timeout_seconds: int = 15
  92. account_survival_state_file: Path = DEFAULT_ACCOUNT_SURVIVAL_STATE_FILE
  93. responses_survival_state_file: Path = DEFAULT_RESPONSES_SURVIVAL_STATE_FILE
  94. responses_survival_recent_window_seconds: int = 1800
  95. responses_survival_require_provenance: bool = True
  96. warmup_min_age_seconds: int = 600
  97. warmup_min_successful_probes: int = 2
  98. cfmail_rotation_window: int = 10
  99. cfmail_rotation_blacklist_threshold: int = 6
  100. cfmail_rotation_max_successes: int = 2
  101. cfmail_api_token: str = ""
  102. @property
  103. def proxy_pool_configured(self) -> bool:
  104. return bool(self.enable_proxy_pool and (self.proxy_pool_config or self.proxy_pool_direct_urls.strip()))
  105. def validate_cfmail_env(self) -> list[str]:
  106. missing: list[str] = []
  107. token = str(os.getenv("ZHUCE6_CFMAIL_API_TOKEN", "")).strip()
  108. auth_email = str(os.getenv("ZHUCE6_CFMAIL_CF_AUTH_EMAIL", "")).strip()
  109. auth_key = str(os.getenv("ZHUCE6_CFMAIL_CF_AUTH_KEY", "")).strip()
  110. if not token and not (auth_email and auth_key):
  111. missing.append("ZHUCE6_CFMAIL_API_TOKEN")
  112. for key in (
  113. "ZHUCE6_CFMAIL_CF_ACCOUNT_ID",
  114. "ZHUCE6_CFMAIL_CF_ZONE_ID",
  115. "ZHUCE6_CFMAIL_WORKER_NAME",
  116. "ZHUCE6_CFMAIL_ZONE_NAME",
  117. ):
  118. if not os.getenv(key, "").strip():
  119. missing.append(key)
  120. return missing
  121. @classmethod
  122. def from_env(cls) -> "AppSettings":
  123. bootstrap_env()
  124. project_root = Path(
  125. str(os.getenv("ZHUCE6_PROJECT_ROOT", str(PROJECT_ROOT))).strip() or str(PROJECT_ROOT)
  126. ).expanduser().resolve()
  127. config_dir = Path(
  128. str(os.getenv("ZHUCE6_CONFIG_DIR", str(project_root / "config"))).strip() or str(project_root / "config")
  129. ).expanduser().resolve()
  130. state_dir = Path(
  131. str(os.getenv("ZHUCE6_STATE_DIR", str(project_root / "state"))).strip() or str(project_root / "state")
  132. ).expanduser().resolve()
  133. log_dir = Path(
  134. str(os.getenv("ZHUCE6_LOG_DIR", str(project_root / "logs"))).strip() or str(project_root / "logs")
  135. ).expanduser().resolve()
  136. pool_dir = Path(
  137. str(os.getenv("ZHUCE6_POOL_DIR", str(project_root / "pool"))).strip() or str(project_root / "pool")
  138. ).expanduser().resolve()
  139. env_file = Path(
  140. str(os.getenv("ZHUCE6_ENV_FILE", str(project_root / ".env"))).strip() or str(project_root / ".env")
  141. ).expanduser().resolve()
  142. runtime_state_file = Path(
  143. str(os.getenv("ZHUCE6_RUNTIME_STATE_FILE", str(state_dir / "runtime_state.json"))).strip()
  144. or str(state_dir / "runtime_state.json")
  145. ).expanduser().resolve()
  146. account_survival_state_file = Path(
  147. str(os.getenv("ZHUCE6_ACCOUNT_SURVIVAL_STATE_FILE", str(state_dir / "account_survival_tracker.json"))).strip()
  148. or str(state_dir / "account_survival_tracker.json")
  149. ).expanduser().resolve()
  150. responses_survival_state_file = Path(
  151. str(os.getenv("ZHUCE6_RESPONSES_SURVIVAL_STATE_FILE", str(state_dir / "responses_survival_tracker.json"))).strip()
  152. or str(state_dir / "responses_survival_tracker.json")
  153. ).expanduser().resolve()
  154. register_log_file = str(
  155. os.getenv("ZHUCE6_REGISTER_LOG_FILE", str(log_dir / "register.log")).strip() or str(log_dir / "register.log")
  156. )
  157. dashboard_log_file = str(
  158. os.getenv("ZHUCE6_DASHBOARD_LOG_FILE", str(log_dir / "dashboard.log")).strip() or str(log_dir / "dashboard.log")
  159. )
  160. dashboard_allowed_origins = tuple(
  161. part.strip().rstrip("/")
  162. for part in str(os.getenv("ZHUCE6_DASHBOARD_ALLOWED_ORIGINS", "")).split(",")
  163. if part.strip()
  164. )
  165. cleanup_proxy = str(os.getenv("ZHUCE6_CLEANUP_PROXY", "")).strip() or None
  166. validate_proxy = str(os.getenv("ZHUCE6_VALIDATE_PROXY", "")).strip() or None
  167. cpa_management_key = str(os.getenv("ZHUCE6_CPA_MANAGEMENT_KEY", "")).strip() or None
  168. register_proxy = str(os.getenv("ZHUCE6_REGISTER_PROXY", "http://127.0.0.1:7899")).strip() or "http://127.0.0.1:7899"
  169. account_survival_proxy = (
  170. str(os.getenv("ZHUCE6_ACCOUNT_SURVIVAL_PROXY", "")).strip()
  171. or validate_proxy
  172. or register_proxy
  173. )
  174. validate_scope = str(os.getenv("ZHUCE6_VALIDATE_SCOPE", "all")).strip().lower() or "all"
  175. if validate_scope not in {"used", "all"}:
  176. validate_scope = "all"
  177. proxy_pool_config_raw = str(os.getenv("ZHUCE6_PROXY_POOL_CONFIG", str(project_root / "clash_config.yaml"))).strip()
  178. proxy_pool_direct_urls = str(os.getenv("ZHUCE6_PROXY_POOL_DIRECT_URLS", "")).strip()
  179. proxy_pool_regions = tuple(
  180. part.strip().lower()
  181. for part in str(os.getenv("ZHUCE6_PROXY_POOL_REGIONS", "tw,sg,jp,hk,us")).split(",")
  182. if part.strip()
  183. ) or ("tw", "sg", "jp", "hk", "us")
  184. proxy_pool_exclude_names = tuple(
  185. part.strip()
  186. for part in str(os.getenv("ZHUCE6_PROXY_POOL_EXCLUDE_NAMES", "")).split(",")
  187. if part.strip()
  188. )
  189. proxy_pool_preferred_patterns = tuple(
  190. part.strip()
  191. for part in str(os.getenv("ZHUCE6_PROXY_POOL_PREFERRED_PATTERNS", "")).split(",")
  192. if part.strip()
  193. )
  194. register_fresh_proxy_regions = tuple(
  195. part.strip().lower()
  196. for part in str(os.getenv("ZHUCE6_REGISTER_FRESH_PROXY_REGIONS", "tw,sg")).split(",")
  197. if part.strip()
  198. ) or ("tw", "sg")
  199. for directory in (config_dir, state_dir, log_dir, pool_dir):
  200. directory.mkdir(parents=True, exist_ok=True)
  201. runtime_state_file.parent.mkdir(parents=True, exist_ok=True)
  202. account_survival_state_file.parent.mkdir(parents=True, exist_ok=True)
  203. responses_survival_state_file.parent.mkdir(parents=True, exist_ok=True)
  204. return cls(
  205. runtime_mode=str(os.getenv("ZHUCE6_RUNTIME_MODE", "full")).strip() or "full",
  206. host=str(os.getenv("ZHUCE6_HOST", "127.0.0.1")).strip() or "127.0.0.1",
  207. port=max(1, int(os.getenv("ZHUCE6_DASHBOARD_PORT", os.getenv("ZHUCE6_PORT", "8000")))),
  208. project_root=project_root,
  209. config_dir=config_dir,
  210. state_dir=state_dir,
  211. log_dir=log_dir,
  212. env_file=env_file,
  213. cleanup_enabled=_env_bool("ZHUCE6_CLEANUP_ENABLED", True),
  214. validate_enabled=_env_bool("ZHUCE6_VALIDATE_ENABLED", True),
  215. cleanup_interval=max(1, int(os.getenv("ZHUCE6_CLEANUP_INTERVAL", "300"))),
  216. validate_interval=max(1, int(os.getenv("ZHUCE6_VALIDATE_INTERVAL", "180"))),
  217. d1_cleanup_enabled=_env_bool("ZHUCE6_D1_CLEANUP_ENABLED", True),
  218. d1_cleanup_interval=max(1, int(os.getenv("ZHUCE6_D1_CLEANUP_INTERVAL", "1800"))),
  219. d1_database_id=(
  220. str(os.getenv("ZHUCE6_D1_DATABASE_ID", "")).strip()
  221. ),
  222. d1_mail_retention_hours=max(0, int(os.getenv("ZHUCE6_D1_MAIL_RETENTION_HOURS", "2"))),
  223. d1_address_retention_hours=max(0, int(os.getenv("ZHUCE6_D1_ADDRESS_RETENTION_HOURS", "24"))),
  224. pool_dir=pool_dir,
  225. cleanup_proxy=cleanup_proxy,
  226. validate_proxy=validate_proxy,
  227. validate_scope=validate_scope,
  228. cpa_management_base_url=str(
  229. os.getenv("ZHUCE6_CPA_MANAGEMENT_BASE_URL", "http://127.0.0.1:8317/v0/management")
  230. ).strip()
  231. or "http://127.0.0.1:8317/v0/management",
  232. cpa_management_key=cpa_management_key,
  233. backend=(str(os.getenv("ZHUCE6_BACKEND", "cpa")).strip().lower() or "cpa"),
  234. sub2api_base_url=str(os.getenv("ZHUCE6_SUB2API_BASE_URL", "http://127.0.0.1:8080")).strip() or "http://127.0.0.1:8080",
  235. sub2api_admin_email=str(os.getenv("ZHUCE6_SUB2API_ADMIN_EMAIL", "")).strip(),
  236. sub2api_admin_password=str(os.getenv("ZHUCE6_SUB2API_ADMIN_PASSWORD", "")).strip(),
  237. sub2api_api_key=str(os.getenv("ZHUCE6_SUB2API_API_KEY", "")).strip(),
  238. validate_max_workers=max(1, int(os.getenv("ZHUCE6_VALIDATE_MAX_WORKERS", "8"))),
  239. rotate_enabled=_env_bool("ZHUCE6_ROTATE_ENABLED", True),
  240. rotate_interval=max(1, int(os.getenv("ZHUCE6_ROTATE_INTERVAL", "120"))),
  241. rotate_probe_workers=max(1, int(os.getenv("ZHUCE6_ROTATE_PROBE_WORKERS", "8"))),
  242. rotate_fresh_grace_seconds=max(0, int(os.getenv("ZHUCE6_ROTATE_FRESH_GRACE_SECONDS", "600"))),
  243. register_enabled=_env_bool("ZHUCE6_REGISTER_ENABLED", False),
  244. register_threads=max(1, int(os.getenv("ZHUCE6_REGISTER_THREADS", "8"))),
  245. register_interval=max(1, int(os.getenv("ZHUCE6_REGISTER_INTERVAL", "5"))),
  246. register_proxy=register_proxy,
  247. register_mail_provider=str(os.getenv("ZHUCE6_REGISTER_MAIL_PROVIDER", "cfmail")).strip() or "cfmail",
  248. register_sleep_min=max(1, int(os.getenv("ZHUCE6_REGISTER_SLEEP_MIN", "3"))),
  249. register_sleep_max=max(1, int(os.getenv("ZHUCE6_REGISTER_SLEEP_MAX", "10"))),
  250. register_target_count=max(0, int(os.getenv("ZHUCE6_REGISTER_TARGET_COUNT", "0"))),
  251. register_batch_threads=max(1, int(os.getenv("ZHUCE6_REGISTER_BATCH_THREADS", "1"))),
  252. register_batch_target_count=max(1, int(os.getenv("ZHUCE6_REGISTER_BATCH_TARGET_COUNT", "20"))),
  253. register_batch_interval_seconds=max(60, int(os.getenv("ZHUCE6_REGISTER_BATCH_INTERVAL_SECONDS", "10800"))),
  254. register_max_consecutive_failures=max(1, int(os.getenv("ZHUCE6_REGISTER_MAX_CONSECUTIVE_FAILURES", "3"))),
  255. register_log_file=register_log_file,
  256. dashboard_log_file=dashboard_log_file,
  257. dashboard_allowed_origins=dashboard_allowed_origins,
  258. enable_proxy_pool=_env_bool("ZHUCE6_ENABLE_PROXY_POOL", True),
  259. proxy_pool_config=Path(proxy_pool_config_raw).expanduser().resolve() if proxy_pool_config_raw else None,
  260. proxy_pool_direct_urls=proxy_pool_direct_urls,
  261. proxy_pool_regions=proxy_pool_regions,
  262. proxy_pool_size=max(1, int(os.getenv("ZHUCE6_PROXY_POOL_SIZE", "20"))),
  263. proxy_pool_exclude_names=proxy_pool_exclude_names,
  264. proxy_pool_preferred_patterns=proxy_pool_preferred_patterns,
  265. register_fresh_proxy_regions=register_fresh_proxy_regions,
  266. runtime_state_file=runtime_state_file,
  267. recycle_rewarm_cooldown_seconds=max(
  268. 0,
  269. int(os.getenv("ZHUCE6_RECYCLE_REWARM_COOLDOWN_SECONDS", "1800")),
  270. ),
  271. cpa_runtime_reconcile_enabled=_env_bool("ZHUCE6_CPA_RUNTIME_RECONCILE_ENABLED", True),
  272. cpa_runtime_reconcile_cooldown_seconds=max(
  273. 0,
  274. int(os.getenv("ZHUCE6_CPA_RUNTIME_RECONCILE_COOLDOWN_SECONDS", "300")),
  275. ),
  276. cpa_runtime_reconcile_restart_enabled=_env_bool("ZHUCE6_CPA_RUNTIME_RECONCILE_RESTART_ENABLED", False),
  277. account_survival_enabled=_env_bool("ZHUCE6_ACCOUNT_SURVIVAL_ENABLED", True),
  278. account_survival_interval=max(30, int(os.getenv("ZHUCE6_ACCOUNT_SURVIVAL_INTERVAL", "120"))),
  279. account_survival_cohort_size=max(1, int(os.getenv("ZHUCE6_ACCOUNT_SURVIVAL_COHORT_SIZE", "10"))),
  280. account_survival_proxy=account_survival_proxy or None,
  281. account_survival_timeout_seconds=max(5, int(os.getenv("ZHUCE6_ACCOUNT_SURVIVAL_TIMEOUT_SECONDS", "15"))),
  282. account_survival_state_file=account_survival_state_file,
  283. responses_survival_state_file=responses_survival_state_file,
  284. responses_survival_recent_window_seconds=max(
  285. 0, int(os.getenv("ZHUCE6_RESPONSES_SURVIVAL_RECENT_WINDOW_SECONDS", "1800"))
  286. ),
  287. responses_survival_require_provenance=_env_bool(
  288. "ZHUCE6_RESPONSES_SURVIVAL_REQUIRE_PROVENANCE", True
  289. ),
  290. warmup_min_age_seconds=max(0, int(os.getenv("ZHUCE6_WARMUP_MIN_AGE_SECONDS", "600"))),
  291. warmup_min_successful_probes=max(
  292. 1, int(os.getenv("ZHUCE6_WARMUP_MIN_SUCCESSFUL_PROBES", "2"))
  293. ),
  294. cfmail_rotation_window=max(1, int(os.getenv("ZHUCE6_CFMAIL_ROTATION_WINDOW", "10"))),
  295. cfmail_rotation_blacklist_threshold=max(1, int(os.getenv("ZHUCE6_CFMAIL_ROTATION_BLACKLIST_THRESHOLD", "6"))),
  296. cfmail_rotation_max_successes=max(0, int(os.getenv("ZHUCE6_CFMAIL_ROTATION_MAX_SUCCESSES", "2"))),
  297. cfmail_api_token=str(os.getenv("ZHUCE6_CFMAIL_API_TOKEN", "")).strip(),
  298. )