feat(config, email_service, telegram_bot): Add dynamic GPTMail API key management and config reload capability

- Add reload_config() function to dynamically reload config.toml and team.json without restart
- Implement GPTMail API key management with support for multiple keys (api_keys list)
- Add functions to manage GPTMail keys: get_gptmail_keys(), add_gptmail_key(), remove_gptmail_key()
- Add key rotation strategies: get_next_gptmail_key() (round-robin) and get_random_gptmail_key()
- Add gptmail_keys.json file support for dynamic key management
- Fix config parsing to handle include_team_owners and proxy settings in multiple locations
- Update email_service.py to use key rotation for GPTMail API calls
- Update telegram_bot.py to support dynamic key management
- Add install_service.sh script for service installation
- Update config.toml.example with new api_keys configuration option
- Improve configuration flexibility by supporting both old (api_key) and new (api_keys) formats
This commit is contained in:
2026-01-17 05:52:05 +08:00
parent 64707768f8
commit b902922d22
5 changed files with 1067 additions and 84 deletions

200
config.py
View File

@@ -211,6 +211,110 @@ def save_team_json():
_log_config("ERROR", "team.json", "保存失败", str(e))
return False
def reload_config() -> dict:
"""重新加载配置文件 (config.toml 和 team.json)
Returns:
dict: 重载结果,包含 success, teams_count, config_updated, message
"""
global _cfg, _raw_teams, TEAMS
global EMAIL_PROVIDER, INCLUDE_TEAM_OWNERS, AUTH_PROVIDER
global BROWSER_HEADLESS, ACCOUNTS_PER_TEAM
global GPTMAIL_API_KEYS, GPTMAIL_DOMAINS, GPTMAIL_PREFIX
global PROXY_ENABLED, PROXIES
global S2A_API_BASE, S2A_ADMIN_KEY, S2A_ADMIN_TOKEN
global S2A_CONCURRENCY, S2A_PRIORITY, S2A_GROUP_NAMES, S2A_GROUP_IDS
result = {
"success": True,
"teams_count": 0,
"config_updated": False,
"message": ""
}
errors = []
# 重载 config.toml
try:
new_cfg = _load_toml()
if new_cfg:
_cfg = new_cfg
result["config_updated"] = True
# 更新关键配置变量
EMAIL_PROVIDER = _cfg.get("email_provider", "kyx")
# 授权服务选择
AUTH_PROVIDER = _cfg.get("auth_provider") or _cfg.get("gptmail", {}).get("auth_provider", "crs")
# Owner 入库开关
_include_owners_top = _cfg.get("include_team_owners")
_include_owners_gptmail = _cfg.get("gptmail", {}).get("include_team_owners")
INCLUDE_TEAM_OWNERS = _include_owners_top if _include_owners_top is not None else (_include_owners_gptmail if _include_owners_gptmail is not None else False)
# 浏览器配置
_browser = _cfg.get("browser", {})
BROWSER_HEADLESS = _browser.get("headless", False)
# 账号配置
_account = _cfg.get("account", {})
ACCOUNTS_PER_TEAM = _account.get("accounts_per_team", 4)
# GPTMail 配置
_gptmail = _cfg.get("gptmail", {})
GPTMAIL_PREFIX = _gptmail.get("prefix", "")
GPTMAIL_DOMAINS = _gptmail.get("domains", [])
_gptmail_api_key = _gptmail.get("api_key", "")
_gptmail_api_keys = _gptmail.get("api_keys", [])
GPTMAIL_API_KEYS = _gptmail_api_keys if _gptmail_api_keys else ([_gptmail_api_key] if _gptmail_api_key else ["gpt-test"])
# 代理配置
_proxy_enabled_top = _cfg.get("proxy_enabled")
_proxy_enabled_browser = _cfg.get("browser", {}).get("proxy_enabled")
PROXY_ENABLED = _proxy_enabled_top if _proxy_enabled_top is not None else (_proxy_enabled_browser if _proxy_enabled_browser is not None else False)
_proxies_top = _cfg.get("proxies")
_proxies_browser = _cfg.get("browser", {}).get("proxies")
PROXIES = (_proxies_top if _proxies_top is not None else (_proxies_browser if _proxies_browser is not None else [])) if PROXY_ENABLED else []
# S2A 配置
_s2a = _cfg.get("s2a", {})
S2A_API_BASE = _s2a.get("api_base", "")
S2A_ADMIN_KEY = _s2a.get("admin_key", "")
S2A_ADMIN_TOKEN = _s2a.get("admin_token", "")
S2A_CONCURRENCY = _s2a.get("concurrency", 10)
S2A_PRIORITY = _s2a.get("priority", 50)
S2A_GROUP_NAMES = _s2a.get("group_names", [])
S2A_GROUP_IDS = _s2a.get("group_ids", [])
except Exception as e:
errors.append(f"config.toml: {e}")
# 重载 team.json
try:
new_raw_teams = _load_teams()
_raw_teams = new_raw_teams
# 重新解析 TEAMS
TEAMS.clear()
for i, t in enumerate(_raw_teams):
team_config = _parse_team_config(t, i)
TEAMS.append(team_config)
result["teams_count"] = len(TEAMS)
except Exception as e:
errors.append(f"team.json: {e}")
if errors:
result["success"] = False
result["message"] = "; ".join(errors)
else:
result["message"] = "配置重载成功"
return result
# 邮箱系统选择
EMAIL_PROVIDER = _cfg.get("email_provider", "kyx") # "kyx" 或 "gptmail"
@@ -226,10 +330,89 @@ EMAIL_WEB_URL = _email.get("web_url", "")
# GPTMail 临时邮箱配置
_gptmail = _cfg.get("gptmail", {})
GPTMAIL_API_BASE = _gptmail.get("api_base", "https://mail.chatgpt.org.uk")
GPTMAIL_API_KEY = _gptmail.get("api_key", "gpt-test")
GPTMAIL_PREFIX = _gptmail.get("prefix", "")
GPTMAIL_DOMAINS = _gptmail.get("domains", [])
# GPTMail API Keys 支持多个 Key 轮询
# 兼容旧配置: api_key (单个) 和新配置: api_keys (列表)
_gptmail_api_key = _gptmail.get("api_key", "")
_gptmail_api_keys = _gptmail.get("api_keys", [])
GPTMAIL_API_KEYS = _gptmail_api_keys if _gptmail_api_keys else ([_gptmail_api_key] if _gptmail_api_key else ["gpt-test"])
# GPTMail Keys 文件 (用于动态管理)
GPTMAIL_KEYS_FILE = BASE_DIR / "gptmail_keys.json"
_gptmail_key_index = 0
def _load_gptmail_keys() -> list:
"""从文件加载 GPTMail API Keys"""
if not GPTMAIL_KEYS_FILE.exists():
return []
try:
with open(GPTMAIL_KEYS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("keys", [])
except Exception:
return []
def _save_gptmail_keys(keys: list):
"""保存 GPTMail API Keys 到文件"""
try:
with open(GPTMAIL_KEYS_FILE, "w", encoding="utf-8") as f:
json.dump({"keys": keys}, f, indent=2)
return True
except Exception:
return False
def get_gptmail_keys() -> list:
"""获取所有 GPTMail API Keys (配置文件 + 动态添加)"""
file_keys = _load_gptmail_keys()
# 合并配置文件和动态添加的 Keys去重
all_keys = list(dict.fromkeys(GPTMAIL_API_KEYS + file_keys))
return all_keys
def add_gptmail_key(key: str) -> bool:
"""添加 GPTMail API Key"""
if not key or key.strip() == "":
return False
key = key.strip()
current_keys = _load_gptmail_keys()
if key in current_keys or key in GPTMAIL_API_KEYS:
return False # 已存在
current_keys.append(key)
return _save_gptmail_keys(current_keys)
def remove_gptmail_key(key: str) -> bool:
"""删除 GPTMail API Key (仅限动态添加的)"""
current_keys = _load_gptmail_keys()
if key in current_keys:
current_keys.remove(key)
return _save_gptmail_keys(current_keys)
return False
def get_next_gptmail_key() -> str:
"""轮询获取下一个 GPTMail API Key"""
global _gptmail_key_index
keys = get_gptmail_keys()
if not keys:
return "gpt-test"
key = keys[_gptmail_key_index % len(keys)]
_gptmail_key_index += 1
return key
def get_random_gptmail_key() -> str:
"""随机获取一个 GPTMail API Key"""
keys = get_gptmail_keys()
if not keys:
return "gpt-test"
return random.choice(keys)
def get_random_gptmail_domain() -> str:
"""随机获取一个 GPTMail 可用域名 (排除黑名单)"""
@@ -301,7 +484,10 @@ _domain_blacklist = _load_blacklist()
AUTH_PROVIDER = _cfg.get("auth_provider") or _cfg.get("gptmail", {}).get("auth_provider", "crs")
# 是否将 Team Owner 也添加到授权服务
INCLUDE_TEAM_OWNERS = _cfg.get("include_team_owners", False)
# 注意: include_team_owners 可能在顶层或被误放在 gptmail section 下
_include_owners_top = _cfg.get("include_team_owners")
_include_owners_gptmail = _cfg.get("gptmail", {}).get("include_team_owners")
INCLUDE_TEAM_OWNERS = _include_owners_top if _include_owners_top is not None else (_include_owners_gptmail if _include_owners_gptmail is not None else False)
# CRS
_crs = _cfg.get("crs", {})
@@ -377,8 +563,14 @@ TELEGRAM_CHECK_INTERVAL = _telegram.get("check_interval", 3600) # 默认1小时
TELEGRAM_LOW_STOCK_THRESHOLD = _telegram.get("low_stock_threshold", 10) # 低库存阈值
# 代理
PROXY_ENABLED = _cfg.get("proxy_enabled", False)
PROXIES = _cfg.get("proxies", []) if PROXY_ENABLED else []
# 注意: proxy_enabled 和 proxies 可能在顶层或被误放在 browser section 下
_proxy_enabled_top = _cfg.get("proxy_enabled")
_proxy_enabled_browser = _cfg.get("browser", {}).get("proxy_enabled")
PROXY_ENABLED = _proxy_enabled_top if _proxy_enabled_top is not None else (_proxy_enabled_browser if _proxy_enabled_browser is not None else False)
_proxies_top = _cfg.get("proxies")
_proxies_browser = _cfg.get("browser", {}).get("proxies")
PROXIES = (_proxies_top if _proxies_top is not None else (_proxies_browser if _proxies_browser is not None else [])) if PROXY_ENABLED else []
_proxy_index = 0