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:
200
config.py
200
config.py
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 文件路径
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|
||||||
|
# 收集结果
|
||||||
|
for future in as_completed(futures):
|
||||||
|
task_idx = futures[future]
|
||||||
|
try:
|
||||||
|
email, password = future.result()
|
||||||
if email:
|
if email:
|
||||||
accounts.append({
|
accounts.append({
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password
|
"password": password
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
log.warning(f"跳过第 {i+1} 个邮箱创建")
|
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
196
install_service.sh
Normal 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 "$@"
|
||||||
562
telegram_bot.py
562
telegram_bot.py
@@ -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 <key> - 添加 API Key
|
||||||
|
/gptmail_del <key> - 删除 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,247 @@ 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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_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"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -452,25 +759,57 @@ 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:
|
||||||
|
# 1. 设置全局停止标志
|
||||||
try:
|
try:
|
||||||
import run
|
import run
|
||||||
run._shutdown_requested = True
|
run._shutdown_requested = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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 <n> 开始处理",
|
f"使用 /run_all 或 /run <n> 开始处理",
|
||||||
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 <key> 添加",
|
||||||
|
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 <key> - 添加 Key")
|
||||||
|
lines.append(f"/gptmail_del <key> - 删除动态 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 <key>\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():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
|
|||||||
Reference in New Issue
Block a user