Add remote Sub2API proxy selection for account uploads.
Use the target Sub2API service's remote proxy list during account uploads so selected proxies are converted into proxy_key bindings the import API recognizes.
This commit is contained in:
1 parent
b5ddbff081
commit
dfebd9a780
5 files changed
+346
-9
No files matched your search
@@ -3,25 +3,165 @@ Sub2API 账号上传功能
|
||||
将账号以 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:
|
||||
|
||||
@@ -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")
|
||||
@@ -844,6 +846,7 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest):
|
||||
ids, api_url, api_key,
|
||||
concurrency=request.concurrency,
|
||||
priority=request.priority,
|
||||
proxy_id=request.proxy_id,
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -855,6 +858,7 @@ 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 else None
|
||||
|
||||
api_url = None
|
||||
api_key = None
|
||||
@@ -884,7 +888,7 @@ 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 success:
|
||||
return {"success": True, "message": message}
|
||||
|
||||
@@ -8,7 +8,11 @@ from pydantic import BaseModel
|
||||
|
||||
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()
|
||||
|
||||
@@ -55,6 +59,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:
|
||||
@@ -70,6 +90,35 @@ 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",
|
||||
)
|
||||
|
||||
|
||||
# ============== API Endpoints ==============
|
||||
|
||||
@router.get("", response_model=List[Sub2ApiServiceResponse])
|
||||
@@ -95,6 +144,23 @@ async def create_sub2api_service(request: Sub2ApiServiceCreate):
|
||||
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.get("/{service_id}", response_model=Sub2ApiServiceResponse)
|
||||
async def get_sub2api_service(service_id: int):
|
||||
"""获取单个 Sub2API 服务详情"""
|
||||
@@ -203,5 +269,6 @@ async def upload_accounts_to_sub2api(request: Sub2ApiUploadRequest):
|
||||
api_key,
|
||||
concurrency=request.concurrency,
|
||||
priority=request.priority,
|
||||
proxy_id=request.proxy_id,
|
||||
)
|
||||
return results
|
||||
@@ -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 = '<div style="text-align:center;color:var(--text-muted)">加载中...</div>';
|
||||
modal.classList.add('active');
|
||||
|
||||
let proxies = [];
|
||||
try {
|
||||
const query = serviceChoice && serviceChoice.service_id != null
|
||||
? `?service_id=${encodeURIComponent(serviceChoice.service_id)}`
|
||||
: '';
|
||||
const result = await api.get(`/sub2api-services/remote-proxies${query}`);
|
||||
proxies = result.proxies || [];
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
toast.error('加载远端 Sub2API 代理失败: ' + e.message);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxies.length === 0) {
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:12px;">该 Sub2API 服务下暂无可选远端代理,可选择不使用代理继续上传</div>';
|
||||
} else {
|
||||
listEl.innerHTML = proxies.map(proxy => `
|
||||
<div class="sub2api-service-item" data-id="${proxy.id}" style="
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
">
|
||||
<div>
|
||||
<div style="font-weight:500;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<span>#${proxy.id} ${escapeHtml(proxy.name)}</span>
|
||||
<span class="badge">${escapeHtml(String(proxy.protocol || '').toUpperCase())}</span>
|
||||
<span class="status-badge ${proxy.status === 'active' ? 'active' : ''}">${escapeHtml(proxy.status || 'inactive')}</span>
|
||||
</div>
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);">${escapeHtml(proxy.host)}:${proxy.port}</div>
|
||||
</div>
|
||||
<span class="badge" style="background:var(--primary);color:#fff;font-size:0.7rem;padding:2px 8px;border-radius:10px;">选择</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.sub2api-service-item').forEach(item => {
|
||||
item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)');
|
||||
item.addEventListener('mouseleave', () => item.style.background = '');
|
||||
item.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve({ proxy_id: parseInt(item.dataset.id) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
modal.classList.remove('active');
|
||||
closeBtn.removeEventListener('click', onCancel);
|
||||
cancelBtn.removeEventListener('click', onCancel);
|
||||
noProxyBtn.removeEventListener('click', onNoProxy);
|
||||
}
|
||||
function onCancel() { cleanup(); resolve(null); }
|
||||
function onNoProxy() { cleanup(); resolve({ proxy_id: null }); }
|
||||
|
||||
closeBtn.addEventListener('click', onCancel);
|
||||
cancelBtn.addEventListener('click', onCancel);
|
||||
noProxyBtn.addEventListener('click', onNoProxy);
|
||||
});
|
||||
}
|
||||
|
||||
// 批量上传到 Sub2API
|
||||
async function handleBatchUploadSub2Api() {
|
||||
const count = getEffectiveCount();
|
||||
@@ -1030,6 +1107,9 @@ async function handleBatchUploadSub2Api() {
|
||||
const choice = await selectSub2ApiService();
|
||||
if (choice === null) return; // 用户取消
|
||||
|
||||
const proxyInput = await selectSub2ApiProxy(choice);
|
||||
if (proxyInput === null) return;
|
||||
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Sub2API 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1039,6 +1119,7 @@ async function handleBatchUploadSub2Api() {
|
||||
try {
|
||||
const payload = buildBatchPayload();
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
if (proxyInput.proxy_id != null) payload.proxy_id = proxyInput.proxy_id;
|
||||
const result = await api.post('/accounts/batch-upload-sub2api', payload);
|
||||
|
||||
let message = `成功: ${result.success_count}`;
|
||||
@@ -1060,10 +1141,15 @@ async function handleBatchUploadSub2Api() {
|
||||
async function uploadToSub2Api(id) {
|
||||
const choice = await selectSub2ApiService();
|
||||
if (choice === null) return;
|
||||
|
||||
const proxyInput = await selectSub2ApiProxy(choice);
|
||||
if (proxyInput === null) return;
|
||||
|
||||
try {
|
||||
toast.info('正在上传到 Sub2API...');
|
||||
const payload = {};
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
if (proxyInput.proxy_id != null) payload.proxy_id = proxyInput.proxy_id;
|
||||
const result = await api.post(`/accounts/${id}/upload-sub2api`, payload);
|
||||
if (result.success) {
|
||||
toast.success('上传成功');
|
||||
|
||||
@@ -277,6 +277,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub2API 代理选择模态框 -->
|
||||
<div class="modal" id="sub2api-proxy-modal">
|
||||
<div class="modal-content" style="max-width: 520px;">
|
||||
<div class="modal-header">
|
||||
<h3>🌐 选择远端代理</h3>
|
||||
<button class="modal-close" id="close-sub2api-proxy-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: var(--text-muted); margin-bottom: 12px; font-size: 0.9rem;">选择目标 Sub2API 服务中已存在的远端代理,或选择不使用代理。</p>
|
||||
<div id="sub2api-proxy-list" style="display: flex; flex-direction: column; gap: 8px; max-height: 320px; overflow-y: auto;">
|
||||
<div style="text-align: center; color: var(--text-muted);">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end;">
|
||||
<button class="btn btn-secondary" id="sub2api-no-proxy-btn">不使用代理</button>
|
||||
<button class="btn btn-secondary" id="cancel-sub2api-proxy-modal-btn">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/utils.js?v={{ static_version }}"></script>
|
||||
<script src="/static/js/accounts.js?v={{ static_version }}"></script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user