cloud_config.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import os
  2. import json
  3. import hashlib
  4. import time
  5. from typing import Any, Tuple, Optional, Callable
  6. from toolkit.vs_cloud_api import VSCloudApi
  7. def compute_config_hash(config_data: Any) -> str:
  8. """计算 JSON 配置数据的 MD5 哈希,用于快速对比判断配置是否发生变更"""
  9. if config_data is None:
  10. return ""
  11. serialized = json.dumps(config_data, sort_keys=True, ensure_ascii=False)
  12. return hashlib.md5(serialized.encode('utf-8')).hexdigest()
  13. def fetch_cloud_config(config_key: str) -> Any:
  14. """
  15. 使用内置 VSCloudApi 从云端读取指定 config_key 的动态配置。
  16. 支持节点专有 key (如 sentinel_config:node01),若无专有配置则自动回退至全局 key (如 sentinel_config)。
  17. """
  18. try:
  19. data = VSCloudApi.Instance().get_dynamic_config(config_key)
  20. if data is not None:
  21. return data
  22. except Exception:
  23. pass
  24. # 若针对节点的特定 Key 未配置或获取失败,自动回退到全局 Key
  25. if ":" in config_key:
  26. base_key = config_key.split(":")[0]
  27. return VSCloudApi.Instance().get_dynamic_config(base_key)
  28. return None
  29. def save_local_cache(path: str, data: Any) -> bool:
  30. """将从云端获取到的最新配置保存到本地缓存文件路径"""
  31. try:
  32. dir_name = os.path.dirname(path)
  33. if dir_name and not os.path.exists(dir_name):
  34. os.makedirs(dir_name, exist_ok=True)
  35. tmp_path = path + ".tmp"
  36. with open(tmp_path, "w", encoding="utf-8") as f:
  37. json.dump(data, f, ensure_ascii=False, indent=2)
  38. os.replace(tmp_path, path)
  39. return True
  40. except Exception as e:
  41. print(f"[CloudConfig] Failed to save local cache to {path}: {e}")
  42. return False
  43. def load_local_config(path: str) -> Any:
  44. """从本地文件读取配置"""
  45. if not os.path.exists(path):
  46. return None
  47. with open(path, "r", encoding="utf-8") as f:
  48. return json.load(f)
  49. def load_remote_or_cache(
  50. config_key: str,
  51. local_cache_path: str,
  52. use_cloud: bool = True,
  53. logger: Optional[Callable[[str], None]] = None
  54. ) -> Tuple[Any, str, bool]:
  55. """
  56. 核心拉取与 Fallback 逻辑:
  57. - 若开启了 use_cloud(默认):尝试调用 VSCloudApi 获取,成功则写入本地缓存文件;失败则 Fallback 读取本地缓存。
  58. - 若未开启 use_cloud(显式传了 -c/--config):直接读取本地配置文件。
  59. """
  60. def log_info(msg: str):
  61. if logger:
  62. logger(f"[CloudConfig] {msg}")
  63. else:
  64. print(f"[CloudConfig] {msg}")
  65. def log_warn(msg: str):
  66. if logger:
  67. logger(f"[CloudConfig][WARNING] {msg}")
  68. else:
  69. print(f"[CloudConfig][WARNING] {msg}")
  70. if use_cloud:
  71. try:
  72. log_info(f"Fetching cloud configuration key '{config_key}' via VSCloudApi...")
  73. data = fetch_cloud_config(config_key)
  74. if data is not None:
  75. config_hash = compute_config_hash(data)
  76. log_info(f"Successfully fetched cloud config '{config_key}' (hash: {config_hash[:8]})")
  77. save_local_cache(local_cache_path, data)
  78. return data, config_hash, True
  79. else:
  80. log_warn(f"Cloud config key '{config_key}' returned empty/null data.")
  81. except Exception as e:
  82. log_warn(f"Failed to fetch cloud config '{config_key}' via VSCloudApi: {e}")
  83. log_warn(f"Fallback to local cache file: {local_cache_path}")
  84. local_data = load_local_config(local_cache_path)
  85. if local_data is None:
  86. log_warn(f"Local config file {local_cache_path} does not exist or is empty.")
  87. local_data = [] if "booker" in local_cache_path or "sentinel" in local_cache_path else {}
  88. config_hash = compute_config_hash(local_data)
  89. return local_data, config_hash, False