diff --git a/README.md b/README.md index e2a9f93..1c2ac1e 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,21 @@ - 单个账号导出为独立 `.json` 文件 - 多个账号打包为 `.zip`,每个账号一个独立文件 - CPA 上传(Codex Protocol API,直连不走代理) + - 订阅状态管理(手动标记 / 自动检测 plus/team) + - Team Manager 上传(直连不走代理) + +- **支付升级** + - 为账号生成 ChatGPT Plus 或 Team 订阅支付链接 + - 后端命令行以无痕模式自动打开 Chrome/Edge + - Team 套餐支持自定义工作区名称、座位数、计费周期 - **系统设置** - 代理配置(静态 + 动态) - Outlook OAuth 参数 - 注册参数(超时、重试、密码长度等) - 验证码等待配置 + - CPA 上传配置 + - Team Manager 配置(API URL + API Key) - 数据库管理(备份、清理) ## 快速开始 @@ -101,7 +110,7 @@ codex-register-v2/ ├── build.sh # Linux/macOS 打包脚本 ├── src/ │ ├── config/ # 配置管理(Pydantic Settings) -│ ├── core/ # 核心功能(注册引擎、HTTP 客户端、CPA 上传) +│ ├── core/ # 核心功能(注册引擎、HTTP 客户端、CPA 上传、支付、TM 上传) │ ├── database/ # 数据库(SQLAlchemy + SQLite) │ ├── services/ # 邮箱服务实现 │ └── web/ # FastAPI Web 应用 @@ -165,6 +174,17 @@ codex-register-v2/ | POST | `/api/accounts/{id}/upload-cpa` | 上传到 CPA | | POST | `/api/accounts/batch-upload-cpa` | 批量上传到 CPA | +### 支付升级 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/payment/generate-link` | 生成 Plus/Team 支付链接 | +| POST | `/api/payment/open-incognito` | 后端无痕模式打开浏览器 | +| POST | `/api/payment/accounts/{id}/mark-subscription` | 手动标记订阅类型 | +| POST | `/api/payment/accounts/batch-check-subscription` | 批量检测订阅状态 | +| POST | `/api/payment/accounts/{id}/upload-tm` | 上传单账号到 Team Manager | +| POST | `/api/payment/accounts/batch-upload-tm` | 批量上传到 Team Manager | + ### 邮箱服务 | 方法 | 路径 | 说明 | @@ -186,6 +206,8 @@ codex-register-v2/ | POST | `/api/settings/dynamic-proxy` | 更新动态代理设置 | | POST | `/api/settings/cpa` | 更新 CPA 设置 | | POST | `/api/settings/cpa/test` | 测试 CPA 连接 | +| GET/POST | `/api/settings/team-manager` | Team Manager 设置 | +| POST | `/api/settings/team-manager/test` | 测试 Team Manager 连接 | | GET | `/api/settings/database` | 数据库信息 | ### WebSocket @@ -254,6 +276,10 @@ docker-compose build --no-cache - 代理设置优先级:动态代理 > 代理列表(随机) > 静态默认代理 - 注册时自动随机生成用户名和生日(年龄范围 18-45 岁) - CPA 上传始终直连,不经过代理 +- Team Manager 上传始终直连,不经过代理 +- 支付链接生成使用账号 access_token 鉴权,走全局代理配置 +- 无痕浏览器依次尝试 Chrome、Edge,未找到时返回失败提示 +- 订阅状态自动检测调用 `chatgpt.com/backend-api/me`,走全局代理 - 批量注册并发数上限为 50,线程池大小已相应调整 ## License diff --git a/src/config/settings.py b/src/config/settings.py index 910b417..05f6500 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -294,6 +294,27 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = { is_secret=True ), + # Team Manager 配置 + "tm_enabled": SettingDefinition( + db_key="tm.enabled", + default_value=False, + category=SettingCategory.GENERAL, + description="是否启用 Team Manager 上传" + ), + "tm_api_url": SettingDefinition( + db_key="tm.api_url", + default_value="", + category=SettingCategory.GENERAL, + description="Team Manager API 地址" + ), + "tm_api_key": SettingDefinition( + db_key="tm.api_key", + default_value="", + category=SettingCategory.GENERAL, + description="Team Manager API Key", + is_secret=True + ), + # CPA 上传配置 "cpa_enabled": SettingDefinition( db_key="cpa.enabled", @@ -375,6 +396,7 @@ SETTING_TYPES: Dict[str, Type] = { "email_service_priority": dict, "tempmail_timeout": int, "tempmail_max_retries": int, + "tm_enabled": bool, "cpa_enabled": bool, "email_code_timeout": int, "email_code_poll_interval": int, @@ -613,6 +635,11 @@ class Settings(BaseModel): # 安全配置 encryption_key: SecretStr = SecretStr("your-encryption-key-change-in-production") + # Team Manager 配置 + tm_enabled: bool = False + tm_api_url: str = "" + tm_api_key: Optional[SecretStr] = None + # CPA 上传配置 cpa_enabled: bool = False cpa_api_url: str = "" diff --git a/src/core/payment.py b/src/core/payment.py new file mode 100644 index 0000000..0b37989 --- /dev/null +++ b/src/core/payment.py @@ -0,0 +1,172 @@ +""" +支付核心逻辑 — 生成 Plus/Team 支付链接、无痕打开浏览器、检测订阅状态 +""" + +import logging +import subprocess +import sys +from typing import Optional + +from curl_cffi import requests as cffi_requests + +from ..database.models import Account + +logger = logging.getLogger(__name__) + +PAYMENT_CHECKOUT_URL = "https://chatgpt.com/backend-api/payments/checkout" +TEAM_CHECKOUT_BASE_URL = "https://chatgpt.com/checkout/openai_llc/" + + +def _build_proxies(proxy: Optional[str]) -> Optional[dict]: + if proxy: + return {"http": proxy, "https": proxy} + return None + + +def generate_plus_link(account: Account, proxy: Optional[str] = None) -> str: + """生成 Plus 支付链接""" + if not account.access_token: + raise ValueError("账号缺少 access_token") + + headers = { + "Authorization": f"Bearer {account.access_token}", + "Content-Type": "application/json", + } + payload = { + "plan_type": "plus", + "checkout_ui_mode": "hosted", + "cancel_url": "https://chatgpt.com/", + "success_url": "https://chatgpt.com/", + } + + resp = cffi_requests.post( + PAYMENT_CHECKOUT_URL, + headers=headers, + json=payload, + proxies=_build_proxies(proxy), + timeout=30, + impersonate="chrome110", + ) + resp.raise_for_status() + data = resp.json() + if "url" in data: + return data["url"] + raise ValueError(data.get("detail", "API 未返回支付链接")) + + +def generate_team_link( + account: Account, + workspace_name: str = "MyTeam", + price_interval: str = "month", + seat_quantity: int = 5, + proxy: Optional[str] = None, +) -> str: + """生成 Team 支付链接""" + if not account.access_token: + raise ValueError("账号缺少 access_token") + + headers = { + "Authorization": f"Bearer {account.access_token}", + "Content-Type": "application/json", + } + payload = { + "plan_name": "chatgptteamplan", + "team_plan_data": { + "workspace_name": workspace_name, + "price_interval": price_interval, + "seat_quantity": seat_quantity, + }, + "promo_campaign": { + "promo_campaign_id": "team-1-month-free", + "is_coupon_from_query_param": True, + }, + "checkout_ui_mode": "custom", + } + + resp = cffi_requests.post( + PAYMENT_CHECKOUT_URL, + headers=headers, + json=payload, + proxies=_build_proxies(proxy), + timeout=30, + impersonate="chrome110", + ) + resp.raise_for_status() + data = resp.json() + if "checkout_session_id" in data: + return TEAM_CHECKOUT_BASE_URL + data["checkout_session_id"] + raise ValueError(data.get("detail", "API 未返回 checkout_session_id")) + + +def open_url_incognito(url: str) -> bool: + """调用本机浏览器以无痕模式打开 URL""" + platform = sys.platform + try: + if platform == "win32": + # 依次尝试 Chrome、Edge + for browser, flag in [("chrome", "--incognito"), ("msedge", "--inprivate")]: + try: + subprocess.Popen( + f'start {browser} {flag} "{url}"', + shell=True, + ) + return True + except Exception: + continue + elif platform == "darwin": + subprocess.Popen( + ["open", "-a", "Google Chrome", "--args", "--incognito", url] + ) + return True + else: + for binary in ["google-chrome", "chromium-browser", "chromium"]: + try: + subprocess.Popen([binary, "--incognito", url]) + return True + except FileNotFoundError: + continue + except Exception as e: + logger.warning(f"无痕打开浏览器失败: {e}") + return False + + +def check_subscription_status(account: Account, proxy: Optional[str] = None) -> str: + """ + 检测账号当前订阅状态。 + + Returns: + 'free' / 'plus' / 'team' + """ + if not account.access_token: + raise ValueError("账号缺少 access_token") + + headers = { + "Authorization": f"Bearer {account.access_token}", + "Content-Type": "application/json", + } + + resp = cffi_requests.get( + "https://chatgpt.com/backend-api/me", + headers=headers, + proxies=_build_proxies(proxy), + timeout=20, + impersonate="chrome110", + ) + resp.raise_for_status() + data = resp.json() + + # 解析订阅类型 + plan = data.get("plan_type") or "" + if "team" in plan.lower(): + return "team" + if "plus" in plan.lower(): + return "plus" + + # 尝试从 orgs 或 workspace 信息判断 + orgs = data.get("orgs", {}).get("data", []) + for org in orgs: + settings_ = org.get("settings", {}) + if settings_.get("workspace_plan_type") in ("team", "enterprise"): + return "team" + + return "free" diff --git a/src/core/team_manager.py b/src/core/team_manager.py new file mode 100644 index 0000000..b51b5fe --- /dev/null +++ b/src/core/team_manager.py @@ -0,0 +1,158 @@ +""" +Team Manager 上传功能 +参照 CPA 上传模式,直连不走代理 +""" + +import logging +from typing import List, Tuple +from datetime import datetime + +from curl_cffi import requests as cffi_requests + +from ..database.session import get_db +from ..database.models import Account +from ..config.settings import get_settings + +logger = logging.getLogger(__name__) + + +def upload_to_team_manager( + account: Account, + api_url: str, + api_key: str, +) -> Tuple[bool, str]: + """ + 上传单账号到 Team Manager(直连,不走代理) + + Returns: + (成功标志, 消息) + """ + if not api_url: + return False, "Team Manager API URL 未配置" + if not api_key: + return False, "Team Manager API Key 未配置" + if not account.access_token: + return False, "账号缺少 access_token" + + url = api_url.rstrip("/") + "/api/accounts/import" + headers = { + "X-API-Key": api_key, + "Content-Type": "application/json", + } + payload = { + "import_type": "single", + "email": account.email, + "access_token": account.access_token or "", + "session_token": account.session_token or "", + "refresh_token": account.refresh_token or "", + "client_id": account.client_id or "", + } + + try: + resp = cffi_requests.post( + url, + headers=headers, + json=payload, + proxies=None, + timeout=30, + impersonate="chrome110", + ) + if resp.status_code in (200, 201): + return True, "上传成功" + error_msg = f"上传失败: HTTP {resp.status_code}" + try: + detail = resp.json() + if isinstance(detail, dict): + error_msg = detail.get("message", error_msg) + except Exception: + error_msg = f"{error_msg} - {resp.text[:200]}" + return False, error_msg + except Exception as e: + logger.error(f"Team Manager 上传异常: {e}") + return False, f"上传异常: {str(e)}" + + +def batch_upload_to_team_manager( + account_ids: List[int], + api_url: str, + api_key: str, +) -> dict: + """ + 批量上传账号到 Team Manager + + Returns: + 包含成功/失败统计和详情的字典 + """ + results = { + "success_count": 0, + "failed_count": 0, + "skipped_count": 0, + "details": [], + } + + with get_db() as db: + for account_id in account_ids: + account = db.query(Account).filter(Account.id == account_id).first() + if not account: + results["failed_count"] += 1 + results["details"].append( + {"id": account_id, "email": None, "success": False, "error": "账号不存在"} + ) + continue + + if not account.access_token: + results["skipped_count"] += 1 + results["details"].append( + {"id": account_id, "email": account.email, "success": False, "error": "缺少 Token"} + ) + continue + + success, message = upload_to_team_manager(account, api_url, api_key) + if success: + results["success_count"] += 1 + results["details"].append( + {"id": account_id, "email": account.email, "success": True, "message": message} + ) + else: + results["failed_count"] += 1 + results["details"].append( + {"id": account_id, "email": account.email, "success": False, "error": message} + ) + + return results + + +def test_team_manager_connection(api_url: str, api_key: str) -> Tuple[bool, str]: + """ + 测试 Team Manager 连接(直连) + + Returns: + (成功标志, 消息) + """ + if not api_url: + return False, "API URL 不能为空" + if not api_key: + return False, "API Key 不能为空" + + url = api_url.rstrip("/") + "/api/accounts/import" + headers = {"X-API-Key": api_key} + + try: + resp = cffi_requests.options( + url, + headers=headers, + proxies=None, + timeout=10, + impersonate="chrome110", + ) + if resp.status_code in (200, 204, 401, 403, 405): + if resp.status_code == 401: + return False, "连接成功,但 API Key 无效" + return True, "Team Manager 连接测试成功" + return False, f"服务器返回异常状态码: {resp.status_code}" + except cffi_requests.exceptions.ConnectionError as e: + return False, f"无法连接到服务器: {str(e)}" + except cffi_requests.exceptions.Timeout: + return False, "连接超时,请检查网络配置" + except Exception as e: + return False, f"连接测试失败: {str(e)}" diff --git a/src/database/models.py b/src/database/models.py index edfef8e..6991b03 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -53,6 +53,8 @@ class Account(Base): cpa_uploaded = Column(Boolean, default=False) # 是否已上传到 CPA cpa_uploaded_at = Column(DateTime) # 上传时间 source = Column(String(20), default='register') # 'register' 或 'login',区分账号来源 + subscription_type = Column(String(20)) # None / 'plus' / 'team' + subscription_at = Column(DateTime) # 订阅开通时间 created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -74,6 +76,8 @@ class Account(Base): 'cpa_uploaded': self.cpa_uploaded, 'cpa_uploaded_at': self.cpa_uploaded_at.isoformat() if self.cpa_uploaded_at else None, 'source': self.source, + 'subscription_type': self.subscription_type, + 'subscription_at': self.subscription_at.isoformat() if self.subscription_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } diff --git a/src/database/session.py b/src/database/session.py index 5ea6d31..d04b983 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -95,6 +95,8 @@ class DatabaseSessionManager: ("accounts", "cpa_uploaded", "BOOLEAN DEFAULT 0"), ("accounts", "cpa_uploaded_at", "DATETIME"), ("accounts", "source", "VARCHAR(20) DEFAULT 'register'"), + ("accounts", "subscription_type", "VARCHAR(20)"), + ("accounts", "subscription_at", "DATETIME"), ] with self.engine.connect() as conn: diff --git a/src/web/app.py b/src/web/app.py index 350a1fd..1dfffb9 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -98,6 +98,11 @@ def create_app() -> FastAPI: """设置页面""" return templates.TemplateResponse("settings.html", {"request": request}) + @app.get("/payment", response_class=HTMLResponse) + async def payment_page(request: Request): + """支付页面""" + return templates.TemplateResponse("payment.html", {"request": request}) + @app.on_event("startup") async def startup_event(): """应用启动事件""" diff --git a/src/web/routes/__init__.py b/src/web/routes/__init__.py index 0d80c60..9b9d280 100644 --- a/src/web/routes/__init__.py +++ b/src/web/routes/__init__.py @@ -8,6 +8,7 @@ from .accounts import router as accounts_router from .registration import router as registration_router from .settings import router as settings_router from .email_services import router as email_services_router +from .payment import router as payment_router api_router = APIRouter() @@ -16,3 +17,4 @@ api_router.include_router(accounts_router, prefix="/accounts", tags=["accounts"] api_router.include_router(registration_router, prefix="/registration", tags=["registration"]) api_router.include_router(settings_router, prefix="/settings", tags=["settings"]) api_router.include_router(email_services_router, prefix="/email-services", tags=["email-services"]) +api_router.include_router(payment_router, prefix="/payment", tags=["payment"]) diff --git a/src/web/routes/payment.py b/src/web/routes/payment.py new file mode 100644 index 0000000..f5a27c3 --- /dev/null +++ b/src/web/routes/payment.py @@ -0,0 +1,205 @@ +""" +支付相关 API 路由 +""" + +import logging +from typing import Optional, List +from datetime import datetime + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from ...database.session import get_db +from ...database.models import Account +from ...config.settings import get_settings +from ...core.payment import ( + generate_plus_link, + generate_team_link, + open_url_incognito, + check_subscription_status, +) +from ...core.team_manager import ( + upload_to_team_manager, + batch_upload_to_team_manager, +) + +logger = logging.getLogger(__name__) +router = APIRouter() + + +# ============== Pydantic Models ============== + +class GenerateLinkRequest(BaseModel): + account_id: int + plan_type: str # 'plus' or 'team' + workspace_name: str = "MyTeam" + price_interval: str = "month" + seat_quantity: int = 5 + proxy: Optional[str] = None + auto_open: bool = False # 生成后是否自动无痕打开 + + +class OpenIncognitoRequest(BaseModel): + url: str + + +class MarkSubscriptionRequest(BaseModel): + subscription_type: str # 'free' / 'plus' / 'team' + + +class BatchCheckSubscriptionRequest(BaseModel): + ids: List[int] + proxy: Optional[str] = None + + +class UploadTMRequest(BaseModel): + proxy: Optional[str] = None # 保留,TM 上传不走代理 + + +class BatchUploadTMRequest(BaseModel): + ids: List[int] + + +# ============== 支付链接生成 ============== + +@router.post("/generate-link") +def generate_payment_link(request: GenerateLinkRequest): + """生成 Plus 或 Team 支付链接,可选自动无痕打开""" + with get_db() as db: + account = db.query(Account).filter(Account.id == request.account_id).first() + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + + proxy = request.proxy or get_settings().proxy_url + + try: + if request.plan_type == "plus": + link = generate_plus_link(account, proxy) + elif request.plan_type == "team": + link = generate_team_link( + account, + workspace_name=request.workspace_name, + price_interval=request.price_interval, + seat_quantity=request.seat_quantity, + proxy=proxy, + ) + else: + raise HTTPException(status_code=400, detail="plan_type 必须为 plus 或 team") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"生成支付链接失败: {e}") + raise HTTPException(status_code=500, detail=f"生成链接失败: {str(e)}") + + opened = False + if request.auto_open and link: + opened = open_url_incognito(link) + + return { + "success": True, + "link": link, + "plan_type": request.plan_type, + "auto_opened": opened, + } + + +@router.post("/open-incognito") +def open_browser_incognito(request: OpenIncognitoRequest): + """后端命令行以无痕模式打开指定 URL""" + if not request.url: + raise HTTPException(status_code=400, detail="URL 不能为空") + success = open_url_incognito(request.url) + if success: + return {"success": True, "message": "已在无痕模式打开浏览器"} + return {"success": False, "message": "未找到可用的 Chrome/Edge,请手动复制链接"} + + +# ============== 订阅状态 ============== + +@router.post("/accounts/{account_id}/mark-subscription") +def mark_subscription(account_id: int, request: MarkSubscriptionRequest): + """手动标记账号订阅类型""" + allowed = ("free", "plus", "team") + if request.subscription_type not in allowed: + raise HTTPException(status_code=400, detail=f"subscription_type 必须为 {allowed}") + + with get_db() as db: + account = db.query(Account).filter(Account.id == account_id).first() + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + + account.subscription_type = None if request.subscription_type == "free" else request.subscription_type + account.subscription_at = datetime.utcnow() if request.subscription_type != "free" else None + db.commit() + + return {"success": True, "subscription_type": request.subscription_type} + + +@router.post("/accounts/batch-check-subscription") +def batch_check_subscription(request: BatchCheckSubscriptionRequest): + """批量检测账号订阅状态""" + proxy = request.proxy or get_settings().proxy_url + + results = {"success_count": 0, "failed_count": 0, "details": []} + + with get_db() as db: + for account_id in request.ids: + account = db.query(Account).filter(Account.id == account_id).first() + if not account: + results["failed_count"] += 1 + results["details"].append( + {"id": account_id, "email": None, "success": False, "error": "账号不存在"} + ) + continue + + try: + status = check_subscription_status(account, proxy) + account.subscription_type = None if status == "free" else status + account.subscription_at = datetime.utcnow() if status != "free" else account.subscription_at + db.commit() + results["success_count"] += 1 + results["details"].append( + {"id": account_id, "email": account.email, "success": True, "subscription_type": status} + ) + except Exception as e: + results["failed_count"] += 1 + results["details"].append( + {"id": account_id, "email": account.email, "success": False, "error": str(e)} + ) + + return results + + +# ============== Team Manager 上传 ============== + +@router.post("/accounts/{account_id}/upload-tm") +def upload_account_tm(account_id: int, request: UploadTMRequest = None): + """上传单账号到 Team Manager""" + settings = get_settings() + if not settings.tm_enabled: + raise HTTPException(status_code=400, detail="Team Manager 上传未启用") + + api_url = settings.tm_api_url + api_key = settings.tm_api_key.get_secret_value() if settings.tm_api_key else "" + + with get_db() as db: + account = db.query(Account).filter(Account.id == account_id).first() + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + success, message = upload_to_team_manager(account, api_url, api_key) + + return {"success": success, "message": message} + + +@router.post("/accounts/batch-upload-tm") +def batch_upload_tm(request: BatchUploadTMRequest): + """批量上传账号到 Team Manager""" + settings = get_settings() + if not settings.tm_enabled: + raise HTTPException(status_code=400, detail="Team Manager 上传未启用") + + api_url = settings.tm_api_url + api_key = settings.tm_api_key.get_secret_value() if settings.tm_api_key else "" + + results = batch_upload_to_team_manager(request.ids, api_url, api_key) + return results diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index 077ae17..418a332 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -858,3 +858,59 @@ async def update_outlook_settings(request: OutlookSettings): update_settings(**update_dict) return {"success": True, "message": "Outlook 设置已更新"} + + +# ============== Team Manager 设置 ============== + +class TeamManagerSettings(BaseModel): + """Team Manager 设置""" + enabled: bool = False + api_url: str = "" + api_key: str = "" + + +class TeamManagerTestRequest(BaseModel): + """Team Manager 测试请求""" + api_url: str + api_key: str + + +@router.get("/team-manager") +async def get_team_manager_settings(): + """获取 Team Manager 设置""" + settings = get_settings() + return { + "enabled": settings.tm_enabled, + "api_url": settings.tm_api_url, + "has_api_key": bool(settings.tm_api_key and settings.tm_api_key.get_secret_value()), + } + + +@router.post("/team-manager") +async def update_team_manager_settings(request: TeamManagerSettings): + """更新 Team Manager 设置""" + update_dict = { + "tm_enabled": request.enabled, + "tm_api_url": request.api_url, + } + if request.api_key: + update_dict["tm_api_key"] = request.api_key + update_settings(**update_dict) + return {"success": True, "message": "Team Manager 设置已更新"} + + +@router.post("/team-manager/test") +async def test_team_manager_connection(request: TeamManagerTestRequest): + """测试 Team Manager 连接""" + from ...core.team_manager import test_team_manager_connection as do_test + + settings = get_settings() + api_key = request.api_key + if api_key == 'use_saved_key' or not api_key: + if settings.tm_api_key: + api_key = settings.tm_api_key.get_secret_value() + else: + return {"success": False, "message": "未配置 API Key"} + + success, message = do_test(request.api_url, api_key) + return {"success": success, "message": message} diff --git a/static/js/accounts.js b/static/js/accounts.js index edfd81c..122ae21 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -24,6 +24,8 @@ const elements = { batchRefreshBtn: document.getElementById('batch-refresh-btn'), batchValidateBtn: document.getElementById('batch-validate-btn'), batchUploadCpaBtn: document.getElementById('batch-upload-cpa-btn'), + batchCheckSubBtn: document.getElementById('batch-check-sub-btn'), + batchUploadTmBtn: document.getElementById('batch-upload-tm-btn'), batchDeleteBtn: document.getElementById('batch-delete-btn'), exportBtn: document.getElementById('export-btn'), exportMenu: document.getElementById('export-menu'), @@ -88,6 +90,12 @@ function initEventListeners() { // 批量上传CPA elements.batchUploadCpaBtn.addEventListener('click', handleBatchUploadCpa); + // 批量检测订阅 + elements.batchCheckSubBtn.addEventListener('click', handleBatchCheckSubscription); + + // 批量上传TM + elements.batchUploadTmBtn.addEventListener('click', handleBatchUploadTm); + // 批量删除 elements.batchDeleteBtn.addEventListener('click', handleBatchDelete); @@ -283,6 +291,13 @@ function renderAccounts(accounts) { : `-`} +