diff --git a/src/core/upload/sub2api_upload.py b/src/core/upload/sub2api_upload.py index 8df79609..35e12f7b 100644 --- a/src/core/upload/sub2api_upload.py +++ b/src/core/upload/sub2api_upload.py @@ -3,25 +3,165 @@ 将账号以 sub2api-data 格式批量导入到 Sub2API 平台 """ -import json import logging from datetime import datetime, timezone -from typing import List, Tuple, Optional +from typing import Any, List, Optional, Tuple from curl_cffi import requests as cffi_requests -from ...database.session import get_db from ...database.models import Account +from ...database.session import get_db logger = logging.getLogger(__name__) +def _extract_sub2api_data(response) -> Any: + try: + payload = response.json() + except Exception: + payload = None + + if response.status_code >= 400: + if isinstance(payload, dict) and payload.get("message"): + raise ValueError(str(payload["message"])) + response_text = (response.text or "").strip() + raise ValueError( + f"HTTP {response.status_code}" + (f" - {response_text[:200]}" if response_text else "") + ) + + if isinstance(payload, dict): + if payload.get("code") not in (None, 0): + raise ValueError(str(payload.get("message") or "Sub2API 返回错误")) + return payload.get("data", payload) + + if payload is None: + raise ValueError("Sub2API 返回了无法解析的响应") + + return payload + + +def _normalize_remote_sub2api_proxy(proxy: dict) -> dict: + protocol = str(proxy.get("protocol") or "").strip() + host = str(proxy.get("host") or "").strip() + try: + port = int(proxy.get("port") or 0) + except (TypeError, ValueError): + port = 0 + + normalized = { + "id": proxy.get("id"), + "name": str(proxy.get("name") or "").strip(), + "protocol": protocol, + "host": host, + "port": port, + "username": str(proxy.get("username") or "").strip(), + "password": str(proxy.get("password") or "").strip(), + "status": str(proxy.get("status") or "inactive").strip() or "inactive", + } + if not normalized["name"]: + if normalized["id"] is not None: + normalized["name"] = f"Proxy {normalized['id']}" + else: + normalized["name"] = f"{protocol}://{host}:{port}" + return normalized + + +def _build_sub2api_proxy_key(proxy: dict) -> str: + normalized = _normalize_remote_sub2api_proxy(proxy) + return ( + f"{normalized['protocol']}|{normalized['host']}|{normalized['port']}|" + f"{normalized['username']}|{normalized['password']}" + ) + + +def _build_sub2api_proxy_payload(proxy: dict) -> dict: + normalized = _normalize_remote_sub2api_proxy(proxy) + if not normalized["protocol"] or not normalized["host"] or normalized["port"] <= 0: + raise ValueError(f"远端 Sub2API 代理 {normalized['id']} 配置不完整,无法生成上传数据") + + return { + "proxy_key": _build_sub2api_proxy_key(normalized), + "name": normalized["name"], + "protocol": normalized["protocol"], + "host": normalized["host"], + "port": normalized["port"], + "username": normalized["username"], + "password": normalized["password"], + "status": normalized["status"], + } + + +def fetch_remote_sub2api_proxies(api_url: str, api_key: str) -> List[dict]: + if not api_url: + raise ValueError("Sub2API URL 未配置") + if not api_key: + raise ValueError("Sub2API API Key 未配置") + + url = api_url.rstrip("/") + "/api/v1/admin/proxies/all" + + try: + response = cffi_requests.get( + url, + headers={"x-api-key": api_key}, + proxies=None, + timeout=15, + impersonate="chrome110", + ) + data = _extract_sub2api_data(response) + if not isinstance(data, list): + raise ValueError("远端 Sub2API 代理列表格式异常") + return [_normalize_remote_sub2api_proxy(item) for item in data if isinstance(item, dict)] + except ValueError: + raise + except cffi_requests.exceptions.ConnectionError as e: + raise ValueError(f"无法连接到远端 Sub2API 服务: {e}") from e + except cffi_requests.exceptions.Timeout as e: + raise ValueError("拉取远端 Sub2API 代理列表超时") from e + except Exception as e: + logger.error(f"拉取远端 Sub2API 代理列表异常: {e}") + raise ValueError(f"拉取远端 Sub2API 代理列表失败: {e}") from e + + +def fetch_remote_sub2api_proxy(api_url: str, api_key: str, proxy_id: int) -> dict: + if proxy_id is None: + raise ValueError("远端 Sub2API 代理 ID 不能为空") + if not api_url: + raise ValueError("Sub2API URL 未配置") + if not api_key: + raise ValueError("Sub2API API Key 未配置") + + url = api_url.rstrip("/") + f"/api/v1/admin/proxies/{proxy_id}" + + try: + response = cffi_requests.get( + url, + headers={"x-api-key": api_key}, + proxies=None, + timeout=15, + impersonate="chrome110", + ) + data = _extract_sub2api_data(response) + if not isinstance(data, dict): + raise ValueError("远端 Sub2API 代理详情格式异常") + return _normalize_remote_sub2api_proxy(data) + except ValueError: + raise + except cffi_requests.exceptions.ConnectionError as e: + raise ValueError(f"无法连接到远端 Sub2API 服务: {e}") from e + except cffi_requests.exceptions.Timeout as e: + raise ValueError("拉取远端 Sub2API 代理详情超时") from e + except Exception as e: + logger.error(f"拉取远端 Sub2API 代理详情异常: {e}") + raise ValueError(f"拉取远端 Sub2API 代理详情失败: {e}") from e + + def upload_to_sub2api( accounts: List[Account], api_url: str, api_key: str, concurrency: int = 3, priority: int = 50, + proxy_id: Optional[int] = None, ) -> Tuple[bool, str]: """ 上传账号列表到 Sub2API 平台(不走代理) @@ -32,6 +172,7 @@ def upload_to_sub2api( api_key: Admin API Key(x-api-key header) concurrency: 账号并发数,默认 3 priority: 账号优先级,默认 50 + proxy_id: 远端 Sub2API 代理 ID,用于生成 Sub2API 识别的 proxy_key(可选) Returns: (成功标志, 消息) @@ -47,12 +188,20 @@ def upload_to_sub2api( exported_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + proxy_payload = None + if proxy_id is not None: + try: + remote_proxy = fetch_remote_sub2api_proxy(api_url, api_key, proxy_id) + proxy_payload = _build_sub2api_proxy_payload(remote_proxy) + except ValueError as e: + return False, str(e) + account_items = [] for acc in accounts: if not acc.access_token: continue expires_at = int(acc.expires_at.timestamp()) if acc.expires_at else 0 - account_items.append({ + account_item = { "name": acc.email, "platform": "openai", "type": "oauth", @@ -82,7 +231,10 @@ def upload_to_sub2api( "priority": priority, "rate_multiplier": 1, "auto_pause_on_expired": True, - }) + } + if proxy_payload is not None: + account_item["proxy_key"] = proxy_payload["proxy_key"] + account_items.append(account_item) if not account_items: return False, "所有账号均缺少 access_token,无法上传" @@ -92,7 +244,7 @@ def upload_to_sub2api( "type": "sub2api-data", "version": 1, "exported_at": exported_at, - "proxies": [], + "proxies": [proxy_payload] if proxy_payload is not None else [], "accounts": account_items, }, "skip_default_group_bind": True, @@ -138,6 +290,7 @@ def batch_upload_to_sub2api( api_key: str, concurrency: int = 3, priority: int = 50, + proxy_id: Optional[int] = None, ) -> dict: """ 批量上传指定 ID 的账号到 Sub2API 平台 @@ -169,7 +322,14 @@ def batch_upload_to_sub2api( if not accounts: return results - success, message = upload_to_sub2api(accounts, api_url, api_key, concurrency, priority) + success, message = upload_to_sub2api( + accounts, + api_url, + api_key, + concurrency, + priority, + proxy_id=proxy_id, + ) if success: for acc in accounts: diff --git a/src/database/crud.py b/src/database/crud.py index 4750969c..e14dd3c5 100644 --- a/src/database/crud.py +++ b/src/database/crud.py @@ -596,13 +596,15 @@ def create_sub2api_service( api_url: str, api_key: str, enabled: bool = True, - priority: int = 0 + priority: int = 0, + default_remote_proxy_id: Optional[int] = None, ) -> Sub2ApiService: """创建 Sub2API 服务配置""" svc = Sub2ApiService( name=name, api_url=api_url, api_key=api_key, + default_remote_proxy_id=default_remote_proxy_id, enabled=enabled, priority=priority, ) diff --git a/src/database/models.py b/src/database/models.py index f662917c..17880e04 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -152,6 +152,7 @@ class Sub2ApiService(Base): name = Column(String(100), nullable=False) # 服务名称 api_url = Column(String(500), nullable=False) # API URL (host) api_key = Column(Text, nullable=False) # x-api-key + default_remote_proxy_id = Column(Integer, nullable=True) # 默认远端代理 ID enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) # 优先级 created_at = Column(DateTime, default=datetime.utcnow) diff --git a/src/database/session.py b/src/database/session.py index c16541be..4c1dd244 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -111,6 +111,7 @@ def migrate_tables(self): ("accounts", "subscription_at", "DATETIME"), ("accounts", "cookies", "TEXT"), ("proxies", "is_default", "BOOLEAN DEFAULT 0"), + ("sub2api_services", "default_remote_proxy_id", "INTEGER"), ] # 确保新表存在(create_tables 已处理,此处兜底) diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index a6a597fc..5f6c86f2 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -796,6 +796,7 @@ class Sub2ApiUploadRequest(BaseModel): service_id: Optional[int] = None concurrency: int = 3 priority: int = 50 + proxy_id: Optional[int] = None # 远端 Sub2API 代理 ID class BatchSub2ApiUploadRequest(BaseModel): @@ -808,6 +809,7 @@ class BatchSub2ApiUploadRequest(BaseModel): service_id: Optional[int] = None # 指定 Sub2API 服务 ID,不传则使用第一个启用的 concurrency: int = 3 priority: int = 50 + proxy_id: Optional[int] = None # 远端 Sub2API 代理 ID @router.post("/batch-upload-sub2api") @@ -817,6 +819,7 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest): # 解析指定的 Sub2API 服务 api_url = None api_key = None + svc = None if request.service_id: with get_db() as db: svc = crud.get_sub2api_service_by_id(db, request.service_id) @@ -828,10 +831,11 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest): with get_db() as db: svcs = crud.get_sub2api_services(db, enabled=True) if svcs: - api_url = svcs[0].api_url - api_key = svcs[0].api_key + svc = svcs[0] + api_url = svc.api_url + api_key = svc.api_key - if not api_url or not api_key: + if not api_url or not api_key or not svc: raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务,请先在设置中配置") with get_db() as db: @@ -840,10 +844,13 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest): request.status_filter, request.email_service_filter, request.search_filter ) + selected_proxy_id = request.proxy_id if "proxy_id" in request.model_fields_set else svc.default_remote_proxy_id + results = batch_upload_to_sub2api( ids, api_url, api_key, concurrency=request.concurrency, priority=request.priority, + proxy_id=selected_proxy_id, ) return results @@ -855,9 +862,11 @@ async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUp service_id = request.service_id if request else None concurrency = request.concurrency if request else 3 priority = request.priority if request else 50 + proxy_id = request.proxy_id if request and "proxy_id" in request.model_fields_set else None api_url = None api_key = None + svc = None if service_id: with get_db() as db: svc = crud.get_sub2api_service_by_id(db, service_id) @@ -869,10 +878,11 @@ async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUp with get_db() as db: svcs = crud.get_sub2api_services(db, enabled=True) if svcs: - api_url = svcs[0].api_url - api_key = svcs[0].api_key + svc = svcs[0] + api_url = svc.api_url + api_key = svc.api_key - if not api_url or not api_key: + if not api_url or not api_key or not svc: raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务,请先在设置中配置") with get_db() as db: @@ -884,7 +894,9 @@ async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUp success, message = upload_to_sub2api( [account], api_url, api_key, - concurrency=concurrency, priority=priority + concurrency=concurrency, + priority=priority, + proxy_id=proxy_id if request and "proxy_id" in request.model_fields_set else svc.default_remote_proxy_id, ) if success: return {"success": True, "message": message} diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index daa92e5e..da386fe4 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -462,7 +462,12 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: if not _svc: continue log_callback(f"[Sub2API] 正在把账号发往服务站: {_svc.name}") - _ok, _msg = upload_to_sub2api([saved_account], _svc.api_url, _svc.api_key) + _ok, _msg = upload_to_sub2api( + [saved_account], + _svc.api_url, + _svc.api_key, + proxy_id=_svc.default_remote_proxy_id, + ) log_callback(f"[Sub2API] {'成功' if _ok else '失败'}({_svc.name}): {_msg}") except Exception as _e: log_callback(f"[Sub2API] 异常({_sid}): {_e}") diff --git a/src/web/routes/upload/sub2api_services.py b/src/web/routes/upload/sub2api_services.py index ddd77592..7843ea2b 100644 --- a/src/web/routes/upload/sub2api_services.py +++ b/src/web/routes/upload/sub2api_services.py @@ -8,7 +8,11 @@ from ....database import crud from ....database.session import get_db -from ....core.upload.sub2api_upload import test_sub2api_connection, batch_upload_to_sub2api +from ....core.upload.sub2api_upload import ( + batch_upload_to_sub2api, + fetch_remote_sub2api_proxies, + test_sub2api_connection, +) router = APIRouter() @@ -19,6 +23,7 @@ class Sub2ApiServiceCreate(BaseModel): name: str api_url: str api_key: str + default_remote_proxy_id: Optional[int] = None enabled: bool = True priority: int = 0 @@ -27,6 +32,7 @@ class Sub2ApiServiceUpdate(BaseModel): name: Optional[str] = None api_url: Optional[str] = None api_key: Optional[str] = None + default_remote_proxy_id: Optional[int] = None enabled: Optional[bool] = None priority: Optional[int] = None @@ -36,6 +42,7 @@ class Sub2ApiServiceResponse(BaseModel): name: str api_url: str has_key: bool + default_remote_proxy_id: Optional[int] = None enabled: bool priority: int created_at: Optional[str] = None @@ -46,8 +53,10 @@ class Config: class Sub2ApiTestRequest(BaseModel): + service_id: Optional[int] = None api_url: Optional[str] = None api_key: Optional[str] = None + default_remote_proxy_id: Optional[int] = None class Sub2ApiUploadRequest(BaseModel): @@ -55,6 +64,22 @@ class Sub2ApiUploadRequest(BaseModel): service_id: Optional[int] = None concurrency: int = 3 priority: int = 50 + proxy_id: Optional[int] = None + + +class Sub2ApiRemoteProxyResponse(BaseModel): + id: int + name: str + protocol: str + host: str + port: int + username: str = "" + status: str + + +class Sub2ApiRemoteProxyListResponse(BaseModel): + service: Sub2ApiServiceResponse + proxies: List[Sub2ApiRemoteProxyResponse] def _to_response(svc) -> Sub2ApiServiceResponse: @@ -63,6 +88,7 @@ def _to_response(svc) -> Sub2ApiServiceResponse: name=svc.name, api_url=svc.api_url, has_key=bool(svc.api_key), + default_remote_proxy_id=svc.default_remote_proxy_id, enabled=svc.enabled, priority=svc.priority, created_at=svc.created_at.isoformat() if svc.created_at else None, @@ -70,6 +96,52 @@ def _to_response(svc) -> Sub2ApiServiceResponse: ) +def _resolve_sub2api_service(service_id: Optional[int] = None): + with get_db() as db: + if service_id: + svc = crud.get_sub2api_service_by_id(db, service_id) + else: + services = crud.get_sub2api_services(db, enabled=True) + svc = services[0] if services else None + + if not svc: + raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务") + return svc + + +def _to_remote_proxy_response(proxy: dict) -> Sub2ApiRemoteProxyResponse: + proxy_id = proxy.get("id") + if proxy_id is None: + raise ValueError("远端 Sub2API 代理缺少 id") + + return Sub2ApiRemoteProxyResponse( + id=int(proxy_id), + name=str(proxy.get("name") or "").strip() or f"Proxy {proxy_id}", + protocol=str(proxy.get("protocol") or "").strip(), + host=str(proxy.get("host") or "").strip(), + port=int(proxy.get("port") or 0), + username=str(proxy.get("username") or "").strip(), + status=str(proxy.get("status") or "inactive").strip() or "inactive", + ) + + +def _resolve_temp_sub2api_request(request: Sub2ApiTestRequest): + svc = None + if request.service_id is not None: + with get_db() as db: + svc = crud.get_sub2api_service_by_id(db, request.service_id) + if not svc: + raise HTTPException(status_code=404, detail="Sub2API 服务不存在") + + api_url = (request.api_url or (svc.api_url if svc else "")).strip() + api_key = (request.api_key or (svc.api_key if svc else "")).strip() + + if not api_url or not api_key: + raise HTTPException(status_code=400, detail="api_url 和 api_key 不能为空") + + return svc, api_url, api_key + + # ============== API Endpoints ============== @router.get("", response_model=List[Sub2ApiServiceResponse]) @@ -91,10 +163,55 @@ async def create_sub2api_service(request: Sub2ApiServiceCreate): api_key=request.api_key, enabled=request.enabled, priority=request.priority, + default_remote_proxy_id=request.default_remote_proxy_id, ) return _to_response(svc) +@router.get("/remote-proxies", response_model=Sub2ApiRemoteProxyListResponse) +async def list_remote_sub2api_proxies(service_id: Optional[int] = None): + """拉取目标 Sub2API 服务中的远端代理列表""" + svc = _resolve_sub2api_service(service_id) + + try: + proxies = fetch_remote_sub2api_proxies(svc.api_url, svc.api_key) + proxy_items = [_to_remote_proxy_response(proxy) for proxy in proxies] + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return Sub2ApiRemoteProxyListResponse( + service=_to_response(svc), + proxies=proxy_items, + ) + + +@router.post("/remote-proxies", response_model=Sub2ApiRemoteProxyListResponse) +async def list_remote_sub2api_proxies_direct(request: Sub2ApiTestRequest): + """按临时 Sub2API 配置拉取远端代理列表(用于新增前选择默认代理)""" + svc, api_url, api_key = _resolve_temp_sub2api_request(request) + + try: + proxies = fetch_remote_sub2api_proxies(api_url, api_key) + proxy_items = [_to_remote_proxy_response(proxy) for proxy in proxies] + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return Sub2ApiRemoteProxyListResponse( + service=_to_response(svc) if svc else Sub2ApiServiceResponse( + id=0, + name="未保存服务", + api_url=api_url, + has_key=bool(api_key), + default_remote_proxy_id=request.default_remote_proxy_id, + enabled=True, + priority=0, + created_at=None, + updated_at=None, + ), + proxies=proxy_items, + ) + + @router.get("/{service_id}", response_model=Sub2ApiServiceResponse) async def get_sub2api_service(service_id: int): """获取单个 Sub2API 服务详情""" @@ -117,6 +234,7 @@ async def get_sub2api_service_full(service_id: int): "name": svc.name, "api_url": svc.api_url, "api_key": svc.api_key, + "default_remote_proxy_id": svc.default_remote_proxy_id, "enabled": svc.enabled, "priority": svc.priority, } @@ -138,6 +256,8 @@ async def update_sub2api_service(service_id: int, request: Sub2ApiServiceUpdate) # api_key 留空则保持原值 if request.api_key: update_data["api_key"] = request.api_key + if "default_remote_proxy_id" in request.model_fields_set: + update_data["default_remote_proxy_id"] = request.default_remote_proxy_id if request.enabled is not None: update_data["enabled"] = request.enabled if request.priority is not None: @@ -172,9 +292,8 @@ async def test_sub2api_service(service_id: int): @router.post("/test-connection") async def test_sub2api_connection_direct(request: Sub2ApiTestRequest): """直接测试 Sub2API 连接(用于添加前验证)""" - if not request.api_url or not request.api_key: - raise HTTPException(status_code=400, detail="api_url 和 api_key 不能为空") - success, message = test_sub2api_connection(request.api_url, request.api_key) + _svc, api_url, api_key = _resolve_temp_sub2api_request(request) + success, message = test_sub2api_connection(api_url, api_key) return {"success": success, "message": message} @@ -197,11 +316,14 @@ async def upload_accounts_to_sub2api(request: Sub2ApiUploadRequest): api_url = svc.api_url api_key = svc.api_key + selected_proxy_id = request.proxy_id if "proxy_id" in request.model_fields_set else svc.default_remote_proxy_id + results = batch_upload_to_sub2api( request.account_ids, api_url, api_key, concurrency=request.concurrency, priority=request.priority, + proxy_id=selected_proxy_id, ) return results diff --git a/static/css/style.css b/static/css/style.css index 4c40acc9..76305a2b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -357,6 +357,171 @@ body { border-top: 1px solid var(--border-light); } +.sub2api-remote-proxy-row { + display: flex; + align-items: stretch; + gap: var(--spacing-sm); +} + +.sub2api-remote-proxy-picker { + position: relative; + flex: 1; +} + +.sub2api-remote-proxy-trigger { + width: 100%; + min-height: 44px; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-sm); + cursor: pointer; + transition: all var(--transition); + text-align: left; +} + +.sub2api-remote-proxy-trigger:hover, +.sub2api-remote-proxy-trigger[aria-expanded="true"] { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px var(--primary-light); +} + +.sub2api-remote-proxy-trigger-content { + flex: 1; + min-width: 0; +} + +.sub2api-remote-proxy-trigger-arrow { + color: var(--text-muted); + font-size: 0.875rem; + flex-shrink: 0; +} + +.sub2api-remote-proxy-placeholder { + color: var(--text-muted); + font-size: 0.875rem; +} + +.sub2api-remote-proxy-summary-title-row, +.sub2api-remote-proxy-option-title-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} + +.sub2api-remote-proxy-summary-name, +.sub2api-remote-proxy-option-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); +} + +.sub2api-remote-proxy-summary-meta, +.sub2api-remote-proxy-option-meta { + margin-top: 4px; + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.4; +} + +.sub2api-remote-proxy-pill { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: var(--radius-full); + background: var(--info-light); + color: var(--info-color); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.02em; +} + +.sub2api-remote-proxy-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 20; + display: none; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +.sub2api-remote-proxy-dropdown.active { + display: block; +} + +.sub2api-remote-proxy-options { + max-height: 320px; + overflow-y: auto; + padding: var(--spacing-sm); + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.sub2api-remote-proxy-option { + width: 100%; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + color: inherit; + padding: 10px 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + cursor: pointer; + transition: all var(--transition-fast); + text-align: left; +} + +.sub2api-remote-proxy-option:hover { + background: var(--surface-hover); + border-color: var(--text-muted); +} + +.sub2api-remote-proxy-option.selected { + border-color: var(--primary-color); + background: var(--primary-light); +} + +.sub2api-remote-proxy-option-main { + min-width: 0; + flex: 1; +} + +.sub2api-remote-proxy-option-check { + flex-shrink: 0; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-muted); +} + +.sub2api-remote-proxy-option.selected .sub2api-remote-proxy-option-check { + color: var(--primary-color); +} + +.sub2api-remote-proxy-btn { + flex-shrink: 0; +} + +.sub2api-remote-proxy-hint { + margin-top: var(--spacing-xs); + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; +} + /* Checkbox 样式优化 */ .form-group input[type="checkbox"] { width: auto; diff --git a/static/js/accounts.js b/static/js/accounts.js index fe9848c3..60c86cc3 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -1022,6 +1022,83 @@ function selectSub2ApiService() { }); } +function selectSub2ApiProxy(serviceChoice) { + return new Promise(async (resolve) => { + const modal = document.getElementById('sub2api-proxy-modal'); + const listEl = document.getElementById('sub2api-proxy-list'); + const closeBtn = document.getElementById('close-sub2api-proxy-modal'); + const cancelBtn = document.getElementById('cancel-sub2api-proxy-modal-btn'); + const noProxyBtn = document.getElementById('sub2api-no-proxy-btn'); + + listEl.innerHTML = '