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)) _log_config("ERROR", "team.json", "保存失败", str(e))
return False 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" EMAIL_PROVIDER = _cfg.get("email_provider", "kyx") # "kyx" 或 "gptmail"
@@ -226,10 +330,89 @@ EMAIL_WEB_URL = _email.get("web_url", "")
# GPTMail 临时邮箱配置 # GPTMail 临时邮箱配置
_gptmail = _cfg.get("gptmail", {}) _gptmail = _cfg.get("gptmail", {})
GPTMAIL_API_BASE = _gptmail.get("api_base", "https://mail.chatgpt.org.uk") 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_PREFIX = _gptmail.get("prefix", "")
GPTMAIL_DOMAINS = _gptmail.get("domains", []) 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: def get_random_gptmail_domain() -> str:
"""随机获取一个 GPTMail 可用域名 (排除黑名单)""" """随机获取一个 GPTMail 可用域名 (排除黑名单)"""
@@ -301,7 +484,10 @@ _domain_blacklist = _load_blacklist()
AUTH_PROVIDER = _cfg.get("auth_provider") or _cfg.get("gptmail", {}).get("auth_provider", "crs") AUTH_PROVIDER = _cfg.get("auth_provider") or _cfg.get("gptmail", {}).get("auth_provider", "crs")
# 是否将 Team Owner 也添加到授权服务 # 是否将 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
_crs = _cfg.get("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) # 低库存阈值 TELEGRAM_LOW_STOCK_THRESHOLD = _telegram.get("low_stock_threshold", 10) # 低库存阈值
# 代理 # 代理
PROXY_ENABLED = _cfg.get("proxy_enabled", False) # 注意: proxy_enabled 和 proxies 可能在顶层或被误放在 browser section 下
PROXIES = _cfg.get("proxies", []) if PROXY_ENABLED else [] _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 _proxy_index = 0

View File

@@ -8,6 +8,42 @@
# - "gptmail": GPTMail 临时邮箱系统,无需创建用户,直接生成即可收信 # - "gptmail": GPTMail 临时邮箱系统,无需创建用户,直接生成即可收信
email_provider = "gptmail" email_provider = "gptmail"
# ==================== 授权服务选择 ====================
# 选择使用的授权服务: "crs" / "cpa" / "s2a"
# - crs: 原有 CRS 系统,需手动添加账号到 CRS
# - cpa: CPA (Codex/Copilot Authorization) 系统,后台自动处理账号
# - s2a: Sub2API 系统,支持 OAuth 授权和账号入库
auth_provider = "cpa"
# 是否将 team.json 中的 Team Owner 也添加到授权服务
# 开启后,运行时会自动将 team.json 中的 Owner 账号也进行授权入库
# 注意: 请确保 Team Owner 邮箱可以接收验证码
include_team_owners = false
# ==================== 代理配置 ====================
# 是否启用代理 (默认关闭)
proxy_enabled = false
# 支持配置多个代理,程序会轮换使用
# type: 代理类型 (socks5/http/https)
# host: 代理服务器地址
# port: 代理端口
# username/password: 代理认证信息 (可选)
# [[proxies]]
# type = "socks5"
# host = "127.0.0.1"
# port = 1080
# username = ""
# password = ""
# [[proxies]]
# type = "http"
# host = "proxy.example.com"
# port = 8080
# username = "user"
# password = "pass"
# ---------- Cloud Mail 邮箱系统配置 ---------- # ---------- Cloud Mail 邮箱系统配置 ----------
# 仅当 email_provider = "cloudmail" 时生效 # 仅当 email_provider = "cloudmail" 时生效
# 项目地址: https://github.com/maillab/cloud-mail # 项目地址: https://github.com/maillab/cloud-mail
@@ -30,8 +66,16 @@ web_url = "https://your-email-service.com"
[gptmail] [gptmail]
# API 接口地址 # API 接口地址
api_base = "https://mail.chatgpt.org.uk" api_base = "https://mail.chatgpt.org.uk"
# API 密钥 (gpt-test 为测试密钥,每日有调用限制)
# API 密钥配置 (支持两种方式)
# 方式1: 单个 Key (兼容旧配置)
api_key = "gpt-test" api_key = "gpt-test"
# 方式2: 多个 Key 轮询 (推荐,可分散请求限制)
# api_keys = ["key1", "key2", "key3"]
# 注意: 也可以通过 Telegram Bot 的 /gptmail_add 命令动态添加 Key
# 邮箱前缀 (留空则自动生成 {8位随机字符}-oaiteam 格式) # 邮箱前缀 (留空则自动生成 {8位随机字符}-oaiteam 格式)
prefix = "" prefix = ""
# 可用域名列表,生成邮箱时随机选择 # 可用域名列表,生成邮箱时随机选择
@@ -74,18 +118,6 @@ domains = [
"zawauk.org", "zumuntahassociationuk.org" "zawauk.org", "zumuntahassociationuk.org"
] ]
# ==================== 授权服务选择 ====================
# 选择使用的授权服务: "crs" / "cpa" / "s2a"
# - crs: 原有 CRS 系统,需手动添加账号到 CRS
# - cpa: CPA (Codex/Copilot Authorization) 系统,后台自动处理账号
# - s2a: Sub2API 系统,支持 OAuth 授权和账号入库
auth_provider = "cpa"
# 是否将 team.json 中的 Team Owner 也添加到授权服务
# 开启后,运行时会自动将 team.json 中的 Owner 账号也进行授权入库
# 注意: 请确保 Team Owner 邮箱可以接收验证码
include_team_owners = false
# ==================== CRS 服务配置 ==================== # ==================== CRS 服务配置 ====================
# CRS (Central Registration Service) 用于管理注册账号的中心服务 # CRS (Central Registration Service) 用于管理注册账号的中心服务
[crs] [crs]
@@ -171,30 +203,6 @@ short_wait = 10
# 无头模式 (服务器运行时设为 true) # 无头模式 (服务器运行时设为 true)
headless = false headless = false
# ==================== 代理配置 ====================
# 是否启用代理 (默认关闭)
proxy_enabled = false
# 支持配置多个代理,程序会轮换使用
# type: 代理类型 (socks5/http/https)
# host: 代理服务器地址
# port: 代理端口
# username/password: 代理认证信息 (可选)
# [[proxies]]
# type = "socks5"
# host = "127.0.0.1"
# port = 1080
# username = ""
# password = ""
# [[proxies]]
# type = "http"
# host = "proxy.example.com"
# port = 8080
# username = "user"
# password = "pass"
# ==================== 文件配置 ==================== # ==================== 文件配置 ====================
[files] [files]
# 导出账号信息的 CSV 文件路径 # 导出账号信息的 CSV 文件路径

View File

@@ -9,6 +9,7 @@ import requests
from typing import Callable, TypeVar, Optional, Any from typing import Callable, TypeVar, Optional, Any
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from concurrent.futures import ThreadPoolExecutor, as_completed
from config import ( from config import (
EMAIL_API_BASE, EMAIL_API_BASE,
@@ -21,10 +22,11 @@ from config import (
get_random_domain, get_random_domain,
EMAIL_PROVIDER, EMAIL_PROVIDER,
GPTMAIL_API_BASE, GPTMAIL_API_BASE,
GPTMAIL_API_KEY,
GPTMAIL_PREFIX, GPTMAIL_PREFIX,
GPTMAIL_DOMAINS, GPTMAIL_DOMAINS,
get_random_gptmail_domain, get_random_gptmail_domain,
get_next_gptmail_key,
get_gptmail_keys,
) )
from logger import log from logger import log
@@ -136,27 +138,34 @@ def poll_with_retry(
# ==================== GPTMail 临时邮箱服务 ==================== # ==================== GPTMail 临时邮箱服务 ====================
class GPTMailService: class GPTMailService:
"""GPTMail 临时邮箱服务""" """GPTMail 临时邮箱服务 (支持多 Key 轮询)"""
def __init__(self, api_base: str = None, api_key: str = None): def __init__(self, api_base: str = None, api_key: str = None):
self.api_base = api_base or GPTMAIL_API_BASE self.api_base = api_base or GPTMAIL_API_BASE
self.api_key = api_key or GPTMAIL_API_KEY # 如果指定了 api_key 则使用指定的,否则使用轮询
self.headers = { self._fixed_key = api_key
"X-API-Key": self.api_key,
def _get_headers(self, api_key: str = None) -> dict:
"""获取请求头 (支持指定 Key 或轮询)"""
key = api_key or self._fixed_key or get_next_gptmail_key()
return {
"X-API-Key": key,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
def generate_email(self, prefix: str = None, domain: str = None) -> tuple[str, str]: def generate_email(self, prefix: str = None, domain: str = None, api_key: str = None) -> tuple[str, str]:
"""生成临时邮箱地址 """生成临时邮箱地址
Args: Args:
prefix: 邮箱前缀 (可选) prefix: 邮箱前缀 (可选)
domain: 域名 (可选) domain: 域名 (可选)
api_key: 指定使用的 API Key (可选,不指定则轮询)
Returns: Returns:
tuple: (email, error) - 邮箱地址和错误信息 tuple: (email, error) - 邮箱地址和错误信息
""" """
url = f"{self.api_base}/api/generate-email" url = f"{self.api_base}/api/generate-email"
headers = self._get_headers(api_key)
try: try:
if prefix or domain: if prefix or domain:
@@ -165,9 +174,9 @@ class GPTMailService:
payload["prefix"] = prefix payload["prefix"] = prefix
if domain: if domain:
payload["domain"] = domain payload["domain"] = domain
response = http_session.post(url, headers=self.headers, json=payload, timeout=REQUEST_TIMEOUT) response = http_session.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT)
else: else:
response = http_session.get(url, headers=self.headers, timeout=REQUEST_TIMEOUT) response = http_session.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
data = response.json() data = response.json()
@@ -195,9 +204,10 @@ class GPTMailService:
""" """
url = f"{self.api_base}/api/emails" url = f"{self.api_base}/api/emails"
params = {"email": email} params = {"email": email}
headers = self._get_headers()
try: try:
response = http_session.get(url, headers=self.headers, params=params, timeout=REQUEST_TIMEOUT) response = http_session.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT)
data = response.json() data = response.json()
if data.get("success"): if data.get("success"):
@@ -221,9 +231,10 @@ class GPTMailService:
tuple: (email_detail, error) - 邮件详情和错误信息 tuple: (email_detail, error) - 邮件详情和错误信息
""" """
url = f"{self.api_base}/api/email/{email_id}" url = f"{self.api_base}/api/email/{email_id}"
headers = self._get_headers()
try: try:
response = http_session.get(url, headers=self.headers, timeout=REQUEST_TIMEOUT) response = http_session.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
data = response.json() data = response.json()
if data.get("success"): if data.get("success"):
@@ -246,9 +257,10 @@ class GPTMailService:
tuple: (success, error) tuple: (success, error)
""" """
url = f"{self.api_base}/api/email/{email_id}" url = f"{self.api_base}/api/email/{email_id}"
headers = self._get_headers()
try: try:
response = http_session.delete(url, headers=self.headers, timeout=REQUEST_TIMEOUT) response = http_session.delete(url, headers=headers, timeout=REQUEST_TIMEOUT)
data = response.json() data = response.json()
if data.get("success"): if data.get("success"):
@@ -270,9 +282,10 @@ class GPTMailService:
""" """
url = f"{self.api_base}/api/emails/clear" url = f"{self.api_base}/api/emails/clear"
params = {"email": email} params = {"email": email}
headers = self._get_headers()
try: try:
response = http_session.delete(url, headers=self.headers, params=params, timeout=REQUEST_TIMEOUT) response = http_session.delete(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT)
data = response.json() data = response.json()
if data.get("success"): if data.get("success"):
@@ -284,6 +297,35 @@ class GPTMailService:
except Exception as e: except Exception as e:
return 0, str(e) return 0, str(e)
def test_api_key(self, api_key: str) -> tuple[bool, str]:
"""测试 API Key 是否有效
Args:
api_key: 要测试的 API Key
Returns:
tuple: (success, message)
"""
url = f"{self.api_base}/api/generate-email"
headers = {
"X-API-Key": api_key,
"Content-Type": "application/json"
}
try:
response = http_session.get(url, headers=headers, timeout=10)
data = response.json()
if data.get("success"):
email = data.get("data", {}).get("email", "")
return True, f"Key 有效,测试邮箱: {email}"
else:
error = data.get("error", "Unknown error")
return False, f"Key 无效: {error}"
except Exception as e:
return False, f"测试失败: {e}"
def get_verification_code(self, email: str, max_retries: int = None, interval: int = None) -> tuple[str, str, str]: def get_verification_code(self, email: str, max_retries: int = None, interval: int = None) -> tuple[str, str, str]:
"""从邮箱获取验证码 (使用通用轮询重试) """从邮箱获取验证码 (使用通用轮询重试)
@@ -532,29 +574,42 @@ def fetch_email_content(email: str) -> list:
return [] return []
def batch_create_emails(count: int = 4) -> list: def batch_create_emails(count: int = 4, max_workers: int = 4) -> list:
"""批量创建邮箱 (根据 EMAIL_PROVIDER 配置自动选择邮箱系统) """批量创建邮箱 (并行版本,根据 EMAIL_PROVIDER 配置自动选择邮箱系统)
Args: Args:
count: 创建数量 count: 创建数量
max_workers: 最大并行线程数,默认 4
Returns: Returns:
list: [{"email": "...", "password": "..."}, ...] list: [{"email": "...", "password": "..."}, ...]
""" """
accounts = [] accounts = []
failed_count = 0
for i in range(count): # 使用线程池并行创建邮箱
email, password = unified_create_email() with ThreadPoolExecutor(max_workers=min(count, max_workers)) as executor:
# 提交所有创建任务
futures = {executor.submit(unified_create_email): i for i in range(count)}
if email: # 收集结果
accounts.append({ for future in as_completed(futures):
"email": email, task_idx = futures[future]
"password": password try:
}) email, password = future.result()
else: if email:
log.warning(f"跳过第 {i+1} 个邮箱创建") accounts.append({
"email": email,
"password": password
})
else:
failed_count += 1
log.warning(f"邮箱创建失败 (任务 {task_idx + 1})")
except Exception as e:
failed_count += 1
log.warning(f"邮箱创建异常 (任务 {task_idx + 1}): {e}")
log.info(f"邮箱创建完成: {len(accounts)}/{count}", icon="email") log.info(f"邮箱创建完成: {len(accounts)}/{count}" + (f" (失败 {failed_count})" if failed_count else ""), icon="email")
return accounts return accounts

196
install_service.sh Normal file
View File

@@ -0,0 +1,196 @@
#!/bin/bash
# ==================== Telegram Bot Systemd 服务安装脚本 ====================
# 用法: sudo bash install_service.sh [install|uninstall|status|logs]
set -e
# ==================== 配置 ====================
SERVICE_NAME="oai-team-bot"
SERVICE_DESC="OpenAI Team Provisioner Bot"
WORK_DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHON_BIN="${WORK_DIR}/.venv/bin/python"
SCRIPT_NAME="telegram_bot.py"
USER="${SUDO_USER:-root}"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ==================== 检查环境 ====================
check_env() {
# 检查是否为 root
if [ "$EUID" -ne 0 ]; then
log_error "请使用 sudo 运行此脚本"
exit 1
fi
# 检查工作目录
if [ ! -f "${WORK_DIR}/${SCRIPT_NAME}" ]; then
log_error "找不到 ${SCRIPT_NAME},请在项目目录下运行此脚本"
exit 1
fi
# 检查 Python 环境
if [ ! -f "$PYTHON_BIN" ]; then
log_warn "未找到 .venv尝试使用系统 Python"
PYTHON_BIN=$(which python3 || which python)
if [ -z "$PYTHON_BIN" ]; then
log_error "找不到 Python"
exit 1
fi
fi
log_info "工作目录: ${WORK_DIR}"
log_info "Python: ${PYTHON_BIN}"
log_info "运行用户: ${USER}"
}
# ==================== 安装服务 ====================
install_service() {
log_info "正在安装 ${SERVICE_NAME} 服务..."
# 创建 systemd service 文件
cat > /etc/systemd/system/${SERVICE_NAME}.service << EOF
[Unit]
Description=${SERVICE_DESC}
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${USER}
Group=${USER}
WorkingDirectory=${WORK_DIR}
ExecStart=${PYTHON_BIN} ${WORK_DIR}/${SCRIPT_NAME}
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# 环境变量
Environment="PYTHONUNBUFFERED=1"
Environment="LOG_LEVEL=INFO"
# 安全限制
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=${WORK_DIR}
[Install]
WantedBy=multi-user.target
EOF
log_success "服务文件已创建: /etc/systemd/system/${SERVICE_NAME}.service"
# 重新加载 systemd
systemctl daemon-reload
log_success "systemd 配置已重载"
# 启用开机自启
systemctl enable ${SERVICE_NAME}
log_success "已启用开机自启"
# 启动服务
systemctl start ${SERVICE_NAME}
log_success "服务已启动"
# 显示状态
echo ""
systemctl status ${SERVICE_NAME} --no-pager
echo ""
log_success "安装完成!"
echo ""
echo "常用命令:"
echo " 查看状态: sudo systemctl status ${SERVICE_NAME}"
echo " 查看日志: sudo journalctl -u ${SERVICE_NAME} -f"
echo " 重启服务: sudo systemctl restart ${SERVICE_NAME}"
echo " 停止服务: sudo systemctl stop ${SERVICE_NAME}"
echo " 卸载服务: sudo bash $0 uninstall"
}
# ==================== 卸载服务 ====================
uninstall_service() {
log_info "正在卸载 ${SERVICE_NAME} 服务..."
# 停止服务
systemctl stop ${SERVICE_NAME} 2>/dev/null || true
log_info "服务已停止"
# 禁用开机自启
systemctl disable ${SERVICE_NAME} 2>/dev/null || true
log_info "已禁用开机自启"
# 删除服务文件
rm -f /etc/systemd/system/${SERVICE_NAME}.service
log_info "服务文件已删除"
# 重新加载 systemd
systemctl daemon-reload
log_success "卸载完成!"
}
# ==================== 查看状态 ====================
show_status() {
systemctl status ${SERVICE_NAME} --no-pager
}
# ==================== 查看日志 ====================
show_logs() {
journalctl -u ${SERVICE_NAME} -f --no-pager -n 50
}
# ==================== 主函数 ====================
main() {
case "${1:-install}" in
install)
check_env
install_service
;;
uninstall|remove)
uninstall_service
;;
status)
show_status
;;
logs|log)
show_logs
;;
restart)
systemctl restart ${SERVICE_NAME}
log_success "服务已重启"
show_status
;;
stop)
systemctl stop ${SERVICE_NAME}
log_success "服务已停止"
;;
start)
systemctl start ${SERVICE_NAME}
log_success "服务已启动"
show_status
;;
*)
echo "用法: sudo bash $0 [命令]"
echo ""
echo "命令:"
echo " install 安装并启动服务 (默认)"
echo " uninstall 卸载服务"
echo " status 查看服务状态"
echo " logs 查看实时日志"
echo " restart 重启服务"
echo " start 启动服务"
echo " stop 停止服务"
;;
esac
}
main "$@"

View File

@@ -7,7 +7,7 @@ from concurrent.futures import ThreadPoolExecutor
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional
from telegram import Update, Bot from telegram import Update, Bot, BotCommand
from telegram.ext import ( from telegram.ext import (
Application, Application,
CommandHandler, CommandHandler,
@@ -34,10 +34,22 @@ from config import (
S2A_API_BASE, S2A_API_BASE,
CPA_API_BASE, CPA_API_BASE,
CRS_API_BASE, CRS_API_BASE,
get_gptmail_keys,
add_gptmail_key,
remove_gptmail_key,
GPTMAIL_API_KEYS,
INCLUDE_TEAM_OWNERS,
reload_config,
S2A_CONCURRENCY,
S2A_PRIORITY,
S2A_GROUP_NAMES,
S2A_GROUP_IDS,
S2A_ADMIN_KEY,
) )
from utils import load_team_tracker from utils import load_team_tracker
from bot_notifier import BotNotifier, set_notifier, progress_finish from bot_notifier import BotNotifier, set_notifier, progress_finish
from s2a_service import s2a_get_dashboard_stats, format_dashboard_stats from s2a_service import s2a_get_dashboard_stats, format_dashboard_stats
from email_service import gptmail_service, unified_create_email
from logger import log from logger import log
@@ -93,6 +105,13 @@ class ProvisionerBot:
("dashboard", self.cmd_dashboard), ("dashboard", self.cmd_dashboard),
("import", self.cmd_import), ("import", self.cmd_import),
("stock", self.cmd_stock), ("stock", self.cmd_stock),
("gptmail_keys", self.cmd_gptmail_keys),
("gptmail_add", self.cmd_gptmail_add),
("gptmail_del", self.cmd_gptmail_del),
("test_email", self.cmd_test_email),
("include_owners", self.cmd_include_owners),
("reload", self.cmd_reload),
("s2a_config", self.cmd_s2a_config),
] ]
for cmd, handler in handlers: for cmd, handler in handlers:
self.app.add_handler(CommandHandler(cmd, handler)) self.app.add_handler(CommandHandler(cmd, handler))
@@ -125,6 +144,10 @@ class ProvisionerBot:
# 运行 Bot # 运行 Bot
await self.app.initialize() await self.app.initialize()
await self.app.start() await self.app.start()
# 设置命令菜单提示
await self._set_commands()
await self.app.updater.start_polling(drop_pending_updates=True) await self.app.updater.start_polling(drop_pending_updates=True)
# 等待关闭信号 # 等待关闭信号
@@ -140,6 +163,36 @@ class ProvisionerBot:
"""请求关闭 Bot""" """请求关闭 Bot"""
self._shutdown_event.set() self._shutdown_event.set()
async def _set_commands(self):
"""设置 Bot 命令菜单提示"""
commands = [
BotCommand("help", "查看帮助信息"),
BotCommand("list", "查看 team.json 账号列表"),
BotCommand("status", "查看任务处理状态"),
BotCommand("team", "查看指定 Team 详情"),
BotCommand("config", "查看系统配置"),
BotCommand("run", "处理指定 Team"),
BotCommand("run_all", "处理所有 Team"),
BotCommand("stop", "停止当前任务"),
BotCommand("logs", "查看最近日志"),
BotCommand("headless", "切换无头模式"),
BotCommand("dashboard", "查看 S2A 仪表盘"),
BotCommand("stock", "查看账号库存"),
BotCommand("import", "导入账号到 team.json"),
BotCommand("gptmail_keys", "查看 GPTMail API Keys"),
BotCommand("gptmail_add", "添加 GPTMail API Key"),
BotCommand("gptmail_del", "删除 GPTMail API Key"),
BotCommand("test_email", "测试邮箱创建功能"),
BotCommand("include_owners", "切换 Owner 入库开关"),
BotCommand("reload", "重载配置文件"),
BotCommand("s2a_config", "配置 S2A 服务参数"),
]
try:
await self.app.bot.set_my_commands(commands)
log.info("Bot 命令菜单已设置")
except Exception as e:
log.warning(f"设置命令菜单失败: {e}")
@admin_only @admin_only
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""显示帮助信息""" """显示帮助信息"""
@@ -159,19 +212,28 @@ class ProvisionerBot:
<b>⚙️ 配置管理:</b> <b>⚙️ 配置管理:</b>
/headless - 开启/关闭无头模式 /headless - 开启/关闭无头模式
/include_owners - 开启/关闭 Owner 入库
/reload - 重载配置文件 (无需重启)
<b>📊 S2A 专属:</b> <b>📊 S2A 专属:</b>
/dashboard - 查看 S2A 仪表盘 /dashboard - 查看 S2A 仪表盘
/stock - 查看账号库存 /stock - 查看账号库存
/s2a_config - 配置 S2A 参数
<b>📤 导入账号:</b> <b>📤 导入账号:</b>
/import - 导入账号到 team.json /import - 导入账号到 team.json
或直接发送 JSON 文件 或直接发送 JSON 文件
<b>📧 GPTMail 管理:</b>
/gptmail_keys - 查看所有 API Keys
/gptmail_add &lt;key&gt; - 添加 API Key
/gptmail_del &lt;key&gt; - 删除 API Key
/test_email - 测试邮箱创建
<b>💡 示例:</b> <b>💡 示例:</b>
<code>/list</code> - 查看所有待处理账号 <code>/list</code> - 查看所有待处理账号
<code>/run 0</code> - 处理第一个 Team <code>/run 0</code> - 处理第一个 Team
<code>/config</code> - 查看当前配置""" <code>/gptmail_add my-api-key</code> - 添加 Key"""
await update.message.reply_text(help_text, parse_mode="HTML") await update.message.reply_text(help_text, parse_mode="HTML")
@admin_only @admin_only
@@ -300,6 +362,9 @@ class ProvisionerBot:
# 无头模式状态 # 无头模式状态
headless_status = "✅ 已开启" if BROWSER_HEADLESS else "❌ 未开启" headless_status = "✅ 已开启" if BROWSER_HEADLESS else "❌ 未开启"
# Owner 入库状态
include_owners_status = "✅ 已开启" if INCLUDE_TEAM_OWNERS else "❌ 未开启"
lines = [ lines = [
"<b>⚙️ 系统配置</b>", "<b>⚙️ 系统配置</b>",
"", "",
@@ -309,6 +374,7 @@ class ProvisionerBot:
"<b>🔐 授权服务</b>", "<b>🔐 授权服务</b>",
f" 模式: {AUTH_PROVIDER.upper()}", f" 模式: {AUTH_PROVIDER.upper()}",
f" 地址: {auth_url}", f" 地址: {auth_url}",
f" Owner 入库: {include_owners_status}",
"", "",
"<b>🌐 浏览器</b>", "<b>🌐 浏览器</b>",
f" 无头模式: {headless_status}", f" 无头模式: {headless_status}",
@@ -321,7 +387,8 @@ class ProvisionerBot:
f" 状态: {proxy_info}", f" 状态: {proxy_info}",
"", "",
"<b>💡 提示:</b>", "<b>💡 提示:</b>",
"使用 /headless 开启/关闭无头模式", "/headless - 切换无头模式",
"/include_owners - 切换 Owner 入库",
] ]
await update.message.reply_text("\n".join(lines), parse_mode="HTML") await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@@ -354,7 +421,7 @@ class ProvisionerBot:
await update.message.reply_text( await update.message.reply_text(
f"<b>🌐 无头模式</b>\n\n" f"<b>🌐 无头模式</b>\n\n"
f"状态: {status}\n\n" f"状态: {status}\n\n"
f"⚠️ 需要重启 Bot 生效", f"💡 使用 /reload 立即生效",
parse_mode="HTML" parse_mode="HTML"
) )
@@ -366,6 +433,246 @@ class ProvisionerBot:
except Exception as e: except Exception as e:
await update.message.reply_text(f"❌ 修改配置失败: {e}") await update.message.reply_text(f"❌ 修改配置失败: {e}")
@admin_only
async def cmd_include_owners(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""切换 Owner 入库开关"""
import tomli_w
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 获取当前状态 (顶层配置)
current = config.get("include_team_owners", False)
new_value = not current
# 更新配置 (写到顶层)
config["include_team_owners"] = new_value
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
status = "✅ 已开启" if new_value else "❌ 已关闭"
desc = "运行任务时会将 Team Owner 账号也入库到授权服务" if new_value else "运行任务时不会入库 Team Owner 账号"
await update.message.reply_text(
f"<b>👤 Owner 入库开关</b>\n\n"
f"状态: {status}\n"
f"{desc}\n\n"
f"💡 使用 /reload 立即生效",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text(
"❌ 缺少 tomli_w 依赖\n"
"请运行: uv add tomli_w"
)
except Exception as e:
await update.message.reply_text(f"❌ 修改配置失败: {e}")
@admin_only
async def cmd_reload(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""重载配置文件"""
# 检查是否有任务正在运行
if self.current_task and not self.current_task.done():
await update.message.reply_text(
f"⚠️ 有任务正在运行: {self.current_team}\n"
"请等待任务完成或使用 /stop 停止后再重载配置"
)
return
await update.message.reply_text("⏳ 正在重载配置...")
try:
# 调用重载函数
result = reload_config()
if result["success"]:
# 重新导入更新后的配置变量
from config import (
EMAIL_PROVIDER as new_email_provider,
AUTH_PROVIDER as new_auth_provider,
INCLUDE_TEAM_OWNERS as new_include_owners,
BROWSER_HEADLESS as new_headless,
ACCOUNTS_PER_TEAM as new_accounts_per_team,
)
lines = [
"<b>✅ 配置重载成功</b>",
"",
f"📁 team.json: {result['teams_count']} 个账号",
f"📄 config.toml: {'已更新' if result['config_updated'] else '未变化'}",
"",
"<b>当前配置:</b>",
f" 邮箱服务: {new_email_provider}",
f" 授权服务: {new_auth_provider}",
f" Owner 入库: {'' if new_include_owners else ''}",
f" 无头模式: {'' if new_headless else ''}",
f" 每 Team 账号: {new_accounts_per_team}",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
else:
await update.message.reply_text(
f"<b>❌ 配置重载失败</b>\n\n{result['message']}",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 重载配置失败: {e}")
@admin_only
async def cmd_s2a_config(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""配置 S2A 服务参数"""
import tomli_w
# 无参数时显示当前配置
if not context.args:
# 脱敏显示 API Key
key_display = "未配置"
if S2A_ADMIN_KEY:
if len(S2A_ADMIN_KEY) > 10:
key_display = f"{S2A_ADMIN_KEY[:4]}...{S2A_ADMIN_KEY[-4:]}"
else:
key_display = S2A_ADMIN_KEY[:4] + "..."
groups_display = ", ".join(S2A_GROUP_NAMES) if S2A_GROUP_NAMES else "默认分组"
group_ids_display = ", ".join(S2A_GROUP_IDS) if S2A_GROUP_IDS else ""
lines = [
"<b>📊 S2A 服务配置</b>",
"",
f"<b>API 地址:</b> {S2A_API_BASE or '未配置'}",
f"<b>Admin Key:</b> <code>{key_display}</code>",
f"<b>并发数:</b> {S2A_CONCURRENCY}",
f"<b>优先级:</b> {S2A_PRIORITY}",
f"<b>分组名称:</b> {groups_display}",
f"<b>分组 ID:</b> {group_ids_display}",
"",
"<b>💡 修改配置:</b>",
"<code>/s2a_config concurrency 10</code>",
"<code>/s2a_config priority 50</code>",
"<code>/s2a_config groups 分组1,分组2</code>",
"<code>/s2a_config api_base https://...</code>",
"<code>/s2a_config admin_key sk-xxx</code>",
]
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
return
# 解析参数
param = context.args[0].lower()
value = " ".join(context.args[1:]) if len(context.args) > 1 else None
if not value:
await update.message.reply_text(
f"❌ 缺少值\n用法: /s2a_config {param} <值>"
)
return
try:
# 读取当前配置
with open(CONFIG_FILE, "rb") as f:
import tomllib
config = tomllib.load(f)
# 确保 s2a section 存在
if "s2a" not in config:
config["s2a"] = {}
# 根据参数类型处理
updated_key = None
updated_value = None
if param == "concurrency":
try:
new_val = int(value)
if new_val < 1 or new_val > 100:
await update.message.reply_text("❌ 并发数范围: 1-100")
return
config["s2a"]["concurrency"] = new_val
updated_key = "并发数"
updated_value = str(new_val)
except ValueError:
await update.message.reply_text("❌ 并发数必须是数字")
return
elif param == "priority":
try:
new_val = int(value)
if new_val < 0 or new_val > 100:
await update.message.reply_text("❌ 优先级范围: 0-100")
return
config["s2a"]["priority"] = new_val
updated_key = "优先级"
updated_value = str(new_val)
except ValueError:
await update.message.reply_text("❌ 优先级必须是数字")
return
elif param in ("groups", "group_names"):
# 支持逗号分隔的分组名称
groups = [g.strip() for g in value.split(",") if g.strip()]
config["s2a"]["group_names"] = groups
updated_key = "分组名称"
updated_value = ", ".join(groups) if groups else "默认分组"
elif param == "group_ids":
# 支持逗号分隔的分组 ID
ids = [i.strip() for i in value.split(",") if i.strip()]
config["s2a"]["group_ids"] = ids
updated_key = "分组 ID"
updated_value = ", ".join(ids) if ids else ""
elif param == "api_base":
config["s2a"]["api_base"] = value
updated_key = "API 地址"
updated_value = value
elif param == "admin_key":
config["s2a"]["admin_key"] = value
updated_key = "Admin Key"
# 脱敏显示
if len(value) > 10:
updated_value = f"{value[:4]}...{value[-4:]}"
else:
updated_value = value[:4] + "..."
else:
await update.message.reply_text(
f"❌ 未知参数: {param}\n\n"
"可用参数:\n"
"• concurrency - 并发数\n"
"• priority - 优先级\n"
"• groups - 分组名称\n"
"• group_ids - 分组 ID\n"
"• api_base - API 地址\n"
"• admin_key - Admin Key"
)
return
# 写回文件
with open(CONFIG_FILE, "wb") as f:
tomli_w.dump(config, f)
await update.message.reply_text(
f"<b>✅ S2A 配置已更新</b>\n\n"
f"{updated_key}: <code>{updated_value}</code>\n\n"
f"💡 使用 /reload 立即生效",
parse_mode="HTML"
)
except ImportError:
await update.message.reply_text(
"❌ 缺少 tomli_w 依赖\n"
"请运行: uv add tomli_w"
)
except Exception as e:
await update.message.reply_text(f"❌ 修改配置失败: {e}")
@admin_only @admin_only
async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_run(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""启动处理指定 Team""" """启动处理指定 Team"""
@@ -452,24 +759,56 @@ class ProvisionerBot:
@admin_only @admin_only
async def cmd_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_stop(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""停止当前任务""" """强制停止当前任务"""
if not self.current_task or self.current_task.done(): if not self.current_task or self.current_task.done():
await update.message.reply_text("📭 当前没有运行中的任务") await update.message.reply_text("📭 当前没有运行中的任务")
return return
# 注意: 由于任务在线程池中运行,无法直接取消 task_name = self.current_team or "未知任务"
# 这里只能发送信号 await update.message.reply_text(f"🛑 正在强制停止: {task_name}...")
await update.message.reply_text(
f"🛑 正在停止: {self.current_team}\n"
"注意: 当前账号处理完成后才会停止"
)
# 设置全局停止标志
try: try:
import run # 1. 设置全局停止标志
run._shutdown_requested = True try:
except Exception: import run
pass run._shutdown_requested = True
except Exception:
pass
# 2. 取消 asyncio 任务
if self.current_task and not self.current_task.done():
self.current_task.cancel()
# 3. 强制关闭浏览器进程
try:
from browser_automation import cleanup_chrome_processes
cleanup_chrome_processes()
except Exception as e:
log.warning(f"清理浏览器进程失败: {e}")
# 4. 重置状态
self.current_team = None
# 5. 重置停止标志 (以便下次任务可以正常运行)
try:
import run
run._shutdown_requested = False
except Exception:
pass
# 清理进度跟踪
progress_finish()
await update.message.reply_text(
f"<b>✅ 任务已强制停止</b>\n\n"
f"已停止: {task_name}\n"
f"已清理浏览器进程\n\n"
f"使用 /status 查看状态",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 停止任务时出错: {e}")
@admin_only @admin_only
async def cmd_logs(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def cmd_logs(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -711,10 +1050,12 @@ class ProvisionerBot:
await update.message.reply_text("❌ 未找到有效账号 (需要 account/email 和 token 字段)") await update.message.reply_text("❌ 未找到有效账号 (需要 account/email 和 token 字段)")
return return
# 读取现有 team.json # 读取现有 team.json (不存在则自动创建)
team_json_path = Path(TEAM_JSON_FILE) team_json_path = Path(TEAM_JSON_FILE)
existing_accounts = [] existing_accounts = []
if team_json_path.exists(): is_new_file = not team_json_path.exists()
if not is_new_file:
try: try:
with open(team_json_path, "r", encoding="utf-8") as f: with open(team_json_path, "r", encoding="utf-8") as f:
existing_accounts = json.load(f) existing_accounts = json.load(f)
@@ -741,16 +1082,23 @@ class ProvisionerBot:
existing_emails.add(email) existing_emails.add(email)
added += 1 added += 1
# 保存到 team.json # 保存到 team.json (自动创建文件)
try: try:
# 确保父目录存在
team_json_path.parent.mkdir(parents=True, exist_ok=True)
with open(team_json_path, "w", encoding="utf-8") as f: with open(team_json_path, "w", encoding="utf-8") as f:
json.dump(existing_accounts, f, ensure_ascii=False, indent=2) json.dump(existing_accounts, f, ensure_ascii=False, indent=2)
file_status = "📄 已创建 team.json" if is_new_file else "📄 已更新 team.json"
await update.message.reply_text( await update.message.reply_text(
f"<b>✅ 导入完成</b>\n\n" f"<b>✅ 导入完成</b>\n\n"
f"{file_status}\n"
f"新增: {added}\n" f"新增: {added}\n"
f"跳过 (重复): {skipped}\n" f"跳过 (重复): {skipped}\n"
f"team.json 总数: {len(existing_accounts)}\n\n" f"team.json 总数: {len(existing_accounts)}\n\n"
f"💡 使用 /reload 刷新配置\n"
f"使用 /run_all 或 /run &lt;n&gt; 开始处理", f"使用 /run_all 或 /run &lt;n&gt; 开始处理",
parse_mode="HTML" parse_mode="HTML"
) )
@@ -758,6 +1106,190 @@ class ProvisionerBot:
except Exception as e: except Exception as e:
await update.message.reply_text(f"❌ 保存到 team.json 失败: {e}") await update.message.reply_text(f"❌ 保存到 team.json 失败: {e}")
# ==================== GPTMail Key 管理命令 ====================
@admin_only
async def cmd_gptmail_keys(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看所有 GPTMail API Keys"""
keys = get_gptmail_keys()
config_keys = set(GPTMAIL_API_KEYS)
if not keys:
await update.message.reply_text(
"<b>📧 GPTMail API Keys</b>\n\n"
"📭 暂无配置 Key\n\n"
"使用 /gptmail_add &lt;key&gt; 添加",
parse_mode="HTML"
)
return
lines = [f"<b>📧 GPTMail API Keys (共 {len(keys)} 个)</b>\n"]
for i, key in enumerate(keys):
# 脱敏显示
if len(key) > 10:
masked = f"{key[:4]}...{key[-4:]}"
else:
masked = key[:4] + "..." if len(key) > 4 else key
# 标记来源
source = "📁 配置" if key in config_keys else "🔧 动态"
lines.append(f"{i+1}. <code>{masked}</code> {source}")
lines.append(f"\n<b>💡 管理:</b>")
lines.append(f"/gptmail_add &lt;key&gt; - 添加 Key")
lines.append(f"/gptmail_del &lt;key&gt; - 删除动态 Key")
lines.append(f"/test_email - 测试邮箱创建")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_gptmail_add(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""添加 GPTMail API Key (支持批量导入)"""
if not context.args:
await update.message.reply_text(
"<b>📧 添加 GPTMail API Key</b>\n\n"
"<b>单个添加:</b>\n"
"<code>/gptmail_add gpt-xxx</code>\n\n"
"<b>批量添加 (空格分隔):</b>\n"
"<code>/gptmail_add key1 key2 key3</code>\n\n"
"<b>批量添加 (换行分隔):</b>\n"
"<code>/gptmail_add key1\nkey2\nkey3</code>",
parse_mode="HTML"
)
return
# 合并所有参数,支持空格和换行分隔
raw_input = " ".join(context.args)
# 按空格和换行分割
keys = []
for part in raw_input.replace("\n", " ").split():
key = part.strip()
if key:
keys.append(key)
if not keys:
await update.message.reply_text("❌ Key 不能为空")
return
# 获取现有 keys
existing_keys = set(get_gptmail_keys())
# 统计结果
added = []
skipped = []
invalid = []
await update.message.reply_text(f"⏳ 正在验证 {len(keys)} 个 Key...")
for key in keys:
# 检查是否已存在
if key in existing_keys:
skipped.append(key)
continue
# 测试 Key 是否有效
success, message = gptmail_service.test_api_key(key)
if not success:
invalid.append(key)
continue
# 添加 Key
if add_gptmail_key(key):
added.append(key)
existing_keys.add(key)
# 生成结果报告
lines = ["<b>📧 GPTMail Key 导入结果</b>\n"]
if added:
lines.append(f"<b>✅ 成功添加:</b> {len(added)}")
for k in added[:5]: # 最多显示5个
masked = f"{k[:4]}...{k[-4:]}" if len(k) > 10 else k
lines.append(f" • <code>{masked}</code>")
if len(added) > 5:
lines.append(f" • ... 等 {len(added)}")
if skipped:
lines.append(f"\n<b>⏭️ 已跳过 (已存在):</b> {len(skipped)}")
if invalid:
lines.append(f"\n<b>❌ 无效 Key:</b> {len(invalid)}")
for k in invalid[:3]: # 最多显示3个
masked = f"{k[:4]}...{k[-4:]}" if len(k) > 10 else k
lines.append(f" • <code>{masked}</code>")
lines.append(f"\n<b>当前 Key 总数:</b> {len(get_gptmail_keys())}")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
@admin_only
async def cmd_gptmail_del(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""删除 GPTMail API Key"""
if not context.args:
await update.message.reply_text(
"<b>📧 删除 GPTMail API Key</b>\n\n"
"用法: /gptmail_del &lt;key&gt;\n\n"
"注意: 只能删除动态添加的 Key配置文件中的 Key 请直接修改 config.toml",
parse_mode="HTML"
)
return
key = context.args[0].strip()
# 检查是否是配置文件中的 Key
if key in GPTMAIL_API_KEYS:
await update.message.reply_text(
"⚠️ 该 Key 在配置文件中,无法通过 Bot 删除\n"
"请直接修改 config.toml"
)
return
# 删除 Key
if remove_gptmail_key(key):
await update.message.reply_text(
f"<b>✅ Key 已删除</b>\n\n"
f"当前 Key 总数: {len(get_gptmail_keys())}",
parse_mode="HTML"
)
else:
await update.message.reply_text("❌ Key 不存在或删除失败")
@admin_only
async def cmd_test_email(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""测试邮箱创建功能"""
if EMAIL_PROVIDER != "gptmail":
await update.message.reply_text(
f"⚠️ 当前邮箱提供商: {EMAIL_PROVIDER}\n"
f"测试功能仅支持 GPTMail 模式"
)
return
await update.message.reply_text("⏳ 正在测试邮箱创建...")
try:
# 测试创建邮箱
email, password = unified_create_email()
if email:
await update.message.reply_text(
f"<b>✅ 邮箱创建成功</b>\n\n"
f"邮箱: <code>{email}</code>\n"
f"密码: <code>{password}</code>\n\n"
f"当前 Key 数量: {len(get_gptmail_keys())}",
parse_mode="HTML"
)
else:
await update.message.reply_text(
"<b>❌ 邮箱创建失败</b>\n\n"
"请检查 GPTMail API Key 配置",
parse_mode="HTML"
)
except Exception as e:
await update.message.reply_text(f"❌ 测试失败: {e}")
async def main(): async def main():
"""主函数""" """主函数"""