mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-07-08 07:41:53 +08:00
feat(proxy): 优化代理配置管理功能
- 更新 proxy_url 属性为 get_proxy_url 方法,支持多级代理优先级 - 实现代理获取三路优先级:动态代理 > 代理池 > 静态代理 - 添加取消默认代理功能和 unset_proxy_default 接口 - 实现批量导入代理功能,支持多种格式解析 - 在前端界面添加批量导入代理按钮和模态框 - 重构代理设置页面的默认代理切换交互 - 更新支付流程中的代理获取方式 - 添加 UUID 依赖并优化支付请求头配置
This commit is contained in:
@@ -638,25 +638,40 @@ class Settings(BaseModel):
|
|||||||
proxy_dynamic_api_key_header: str = "X-API-Key"
|
proxy_dynamic_api_key_header: str = "X-API-Key"
|
||||||
proxy_dynamic_result_field: str = ""
|
proxy_dynamic_result_field: str = ""
|
||||||
|
|
||||||
@property
|
def get_proxy_url(self, db=None) -> Optional[str]:
|
||||||
def proxy_url(self) -> Optional[str]:
|
"""获取当前可用的代理 URL(三路优先级)
|
||||||
"""获取完整的代理 URL"""
|
|
||||||
if not self.proxy_enabled:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.proxy_type == "http":
|
优先级:动态代理 > 代理池(默认/随机)> 静态代理 > None
|
||||||
scheme = "http"
|
|
||||||
elif self.proxy_type == "socks5":
|
|
||||||
scheme = "socks5"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
auth = ""
|
Args:
|
||||||
if self.proxy_username and self.proxy_password:
|
db: 可选的数据库 session,传入时检查代理池;不传则跳过代理池
|
||||||
auth = f"{self.proxy_username}:{self.proxy_password.get_secret_value()}@"
|
"""
|
||||||
|
# 1. 动态代理
|
||||||
|
if self.proxy_dynamic_enabled and self.proxy_dynamic_api_url:
|
||||||
|
return self.proxy_dynamic_api_url
|
||||||
|
|
||||||
return f"{scheme}://{auth}{self.proxy_host}:{self.proxy_port}"
|
# 2 & 3. 代理池(优先 is_default,否则随机)
|
||||||
|
if db is not None:
|
||||||
|
from src.database import crud
|
||||||
|
proxy = crud.get_random_proxy(db)
|
||||||
|
if proxy is not None:
|
||||||
|
return proxy.proxy_url
|
||||||
|
|
||||||
|
# 4. 静态代理
|
||||||
|
if self.proxy_enabled:
|
||||||
|
if self.proxy_type == "http":
|
||||||
|
scheme = "http"
|
||||||
|
elif self.proxy_type == "socks5":
|
||||||
|
scheme = "socks5"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
auth = ""
|
||||||
|
if self.proxy_username and self.proxy_password:
|
||||||
|
auth = f"{self.proxy_username}:{self.proxy_password.get_secret_value()}@"
|
||||||
|
return f"{scheme}://{auth}{self.proxy_host}:{self.proxy_port}"
|
||||||
|
|
||||||
|
# 5. 无可用代理
|
||||||
|
return None
|
||||||
# 注册配置
|
# 注册配置
|
||||||
registration_max_retries: int = 3
|
registration_max_retries: int = 3
|
||||||
registration_timeout: int = 120
|
registration_timeout: int = 120
|
||||||
|
|||||||
@@ -115,4 +115,4 @@ def get_proxy_url_for_task() -> Optional[str]:
|
|||||||
logger.warning("动态代理获取失败,回退到静态代理")
|
logger.warning("动态代理获取失败,回退到静态代理")
|
||||||
|
|
||||||
# 使用静态代理
|
# 使用静态代理
|
||||||
return settings.proxy_url
|
return settings.get_proxy_url()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from curl_cffi import requests as cffi_requests
|
from curl_cffi import requests as cffi_requests
|
||||||
@@ -154,12 +155,8 @@ def generate_team_link(
|
|||||||
"Authorization": f"Bearer {account.access_token}",
|
"Authorization": f"Bearer {account.access_token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"oai-language": "zh-CN",
|
"oai-language": "zh-CN",
|
||||||
|
"oai-device-id": str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
if account.cookies:
|
|
||||||
headers["cookie"] = account.cookies
|
|
||||||
oai_did = _extract_oai_did(account.cookies)
|
|
||||||
if oai_did:
|
|
||||||
headers["oai-device-id"] = oai_did
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"plan_name": "chatgptteamplan",
|
"plan_name": "chatgptteamplan",
|
||||||
@@ -171,7 +168,7 @@ def generate_team_link(
|
|||||||
"billing_details": {"country": country, "currency": currency},
|
"billing_details": {"country": country, "currency": currency},
|
||||||
"promo_campaign": {
|
"promo_campaign": {
|
||||||
"promo_campaign_id": "team-1-month-free",
|
"promo_campaign_id": "team-1-month-free",
|
||||||
"is_coupon_from_query_param": True,
|
"is_coupon_from_query_param": False,
|
||||||
},
|
},
|
||||||
"cancel_url": "https://chatgpt.com/#pricing",
|
"cancel_url": "https://chatgpt.com/#pricing",
|
||||||
"checkout_ui_mode": "custom",
|
"checkout_ui_mode": "custom",
|
||||||
@@ -187,9 +184,35 @@ def generate_team_link(
|
|||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if "checkout_session_id" in data:
|
resp2 = cffi_requests.post(
|
||||||
return TEAM_CHECKOUT_BASE_URL + data["checkout_session_id"]
|
"https://api.stripe.com/v1/payment_pages/" + data["checkout_session_id"],
|
||||||
raise ValueError(data.get("detail", "API 未返回 checkout_session_id"))
|
headers={
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"accept": "application/json",
|
||||||
|
"referer": "https://js.stripe.com/"
|
||||||
|
},
|
||||||
|
data=f"tax_region[country]={country}"
|
||||||
|
"&elements_session_client[client_betas][0]=custom_checkout_server_updates_1"
|
||||||
|
"&elements_session_client[client_betas][1]=custom_checkout_manual_approval_1"
|
||||||
|
"&elements_session_client[elements_init_source]=custom_checkout"
|
||||||
|
"&elements_session_client[referrer_host]=chatgpt.com"
|
||||||
|
"&elements_session_client[session_id]=elements_session_1rr8sS4PKIY"
|
||||||
|
"&elements_session_client[stripe_js_id]=72d6a553-c2fb-4f85-941e-8022c8335a85"
|
||||||
|
"&elements_session_client[locale]=zh"
|
||||||
|
"&elements_session_client[is_aggregation_expected]=false"
|
||||||
|
"&client_attribution_metadata[merchant_integration_additional_elements][0]=payment"
|
||||||
|
"&client_attribution_metadata[merchant_integration_additional_elements][1]=address"
|
||||||
|
f"&key={data["publishable_key"]}"
|
||||||
|
,
|
||||||
|
proxies=_build_proxies(proxy),
|
||||||
|
timeout=30,
|
||||||
|
impersonate="chrome110",
|
||||||
|
)
|
||||||
|
resp2.raise_for_status()
|
||||||
|
data2 = resp2.json()
|
||||||
|
if "stripe_hosted_url" in data2:
|
||||||
|
return data2["stripe_hosted_url"]
|
||||||
|
raise ValueError(data.get("detail", "API 未返回 stripe_hosted_url"))
|
||||||
|
|
||||||
|
|
||||||
def open_url_incognito(url: str, cookies_str: Optional[str] = None) -> bool:
|
def open_url_incognito(url: str, cookies_str: Optional[str] = None) -> bool:
|
||||||
|
|||||||
@@ -579,6 +579,16 @@ def set_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]:
|
|||||||
return proxy
|
return proxy
|
||||||
|
|
||||||
|
|
||||||
|
def unset_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]:
|
||||||
|
"""取消指定代理的默认标记"""
|
||||||
|
proxy = db.query(Proxy).filter(Proxy.id == proxy_id).first()
|
||||||
|
if proxy:
|
||||||
|
proxy.is_default = False
|
||||||
|
db.commit()
|
||||||
|
db.refresh(proxy)
|
||||||
|
return proxy
|
||||||
|
|
||||||
|
|
||||||
def get_proxies_count(db: Session, enabled: Optional[bool] = None) -> int:
|
def get_proxies_count(db: Session, enabled: Optional[bool] = None) -> int:
|
||||||
"""获取代理数量"""
|
"""获取代理数量"""
|
||||||
query = db.query(func.count(Proxy.id))
|
query = db.query(func.count(Proxy.id))
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def _get_proxy(request_proxy: Optional[str] = None) -> Optional[str]:
|
|||||||
proxy_url = get_proxy_url_for_task()
|
proxy_url = get_proxy_url_for_task()
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
return proxy_url
|
return proxy_url
|
||||||
return get_settings().proxy_url
|
return get_settings().get_proxy_url()
|
||||||
|
|
||||||
|
|
||||||
# ============== Pydantic Models ==============
|
# ============== Pydantic Models ==============
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def generate_payment_link(request: GenerateLinkRequest):
|
|||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(status_code=404, detail="账号不存在")
|
raise HTTPException(status_code=404, detail="账号不存在")
|
||||||
|
|
||||||
proxy = request.proxy or get_settings().proxy_url
|
proxy = request.proxy or get_settings().get_proxy_url(db=db)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if request.plan_type == "plus":
|
if request.plan_type == "plus":
|
||||||
@@ -125,11 +125,10 @@ def open_browser_incognito(request: OpenIncognitoRequest):
|
|||||||
@router.post("/accounts/batch-check-subscription")
|
@router.post("/accounts/batch-check-subscription")
|
||||||
def batch_check_subscription(request: BatchCheckSubscriptionRequest):
|
def batch_check_subscription(request: BatchCheckSubscriptionRequest):
|
||||||
"""批量检测账号订阅状态"""
|
"""批量检测账号订阅状态"""
|
||||||
proxy = request.proxy or get_settings().proxy_url
|
|
||||||
|
|
||||||
results = {"success_count": 0, "failed_count": 0, "details": []}
|
results = {"success_count": 0, "failed_count": 0, "details": []}
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
proxy = request.proxy or get_settings().get_proxy_url(db=db)
|
||||||
ids = resolve_account_ids(
|
ids = resolve_account_ids(
|
||||||
db, request.ids, request.select_all,
|
db, request.ids, request.select_all,
|
||||||
request.status_filter, request.email_service_filter, request.search_filter
|
request.status_filter, request.email_service_filter, request.search_filter
|
||||||
|
|||||||
@@ -53,25 +53,31 @@ def get_proxy_for_registration(
|
|||||||
获取用于注册的代理
|
获取用于注册的代理
|
||||||
|
|
||||||
策略:
|
策略:
|
||||||
1. 优先从代理列表中随机选择一个启用的代理
|
1. 优先使用动态代理(若已启用)
|
||||||
2. 如果代理列表为空且启用了动态代理,调用动态代理 API 获取
|
2. 动态代理不可用时,使用代理池(有默认代理则走默认,否则随机轮询)
|
||||||
3. 否则使用系统设置中的静态默认代理
|
3. 代理池为空时,使用系统静态代理配置兜底
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
||||||
"""
|
"""
|
||||||
# 先尝试从代理列表中获取
|
from ...core.dynamic_proxy import get_proxy_url_for_task
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
# 1. 优先动态代理
|
||||||
|
if settings.proxy_dynamic_enabled and settings.proxy_dynamic_api_url:
|
||||||
|
proxy_url = get_proxy_url_for_task()
|
||||||
|
if proxy_url:
|
||||||
|
return proxy_url, None
|
||||||
|
logger.warning("动态代理获取失败,回退到代理池")
|
||||||
|
|
||||||
|
# 2. 代理池(内部已实现:有默认走默认,无默认随机轮询)
|
||||||
proxy = crud.get_random_proxy(db, exclude_ids=exclude_proxy_ids)
|
proxy = crud.get_random_proxy(db, exclude_ids=exclude_proxy_ids)
|
||||||
if proxy:
|
if proxy:
|
||||||
return proxy.proxy_url, proxy.id
|
return proxy.proxy_url, proxy.id
|
||||||
|
|
||||||
# 代理列表为空,尝试动态代理或静态代理
|
# 3. 静态代理兜底
|
||||||
from ...core.dynamic_proxy import get_proxy_url_for_task
|
return settings.get_proxy_url(), None
|
||||||
proxy_url = get_proxy_url_for_task()
|
|
||||||
if proxy_url:
|
|
||||||
return proxy_url, None
|
|
||||||
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def update_proxy_usage(db, proxy_id: Optional[int]):
|
def update_proxy_usage(db, proxy_id: Optional[int]):
|
||||||
|
|||||||
@@ -608,6 +608,94 @@ async def set_proxy_default(proxy_id: int):
|
|||||||
return {"success": True, "proxy": proxy.to_dict()}
|
return {"success": True, "proxy": proxy.to_dict()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/proxies/{proxy_id}/unset-default")
|
||||||
|
async def unset_proxy_default(proxy_id: int):
|
||||||
|
"""取消指定代理的默认标记"""
|
||||||
|
with get_db() as db:
|
||||||
|
proxy = crud.unset_proxy_default(db, proxy_id)
|
||||||
|
if not proxy:
|
||||||
|
raise HTTPException(status_code=404, detail="代理不存在")
|
||||||
|
return {"success": True, "proxy": proxy.to_dict()}
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyBatchImportRequest(BaseModel):
|
||||||
|
"""批量导入代理请求"""
|
||||||
|
lines: str
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_proxy_line(line: str):
|
||||||
|
"""
|
||||||
|
解析单行代理字符串,支持格式:
|
||||||
|
- host:port
|
||||||
|
- type://host:port
|
||||||
|
- type://user:pass@host:port
|
||||||
|
- 名称|type://user:pass@host:port
|
||||||
|
"""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
name = None
|
||||||
|
# 解析可选名称前缀(竖线分隔)
|
||||||
|
if '|' in line:
|
||||||
|
name, line = line.split('|', 1)
|
||||||
|
name = name.strip()
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# 若没有协议头,默认补 http://
|
||||||
|
if '://' not in line:
|
||||||
|
line = 'http://' + line
|
||||||
|
|
||||||
|
parsed = urlparse(line)
|
||||||
|
proxy_type = (parsed.scheme or 'http').lower()
|
||||||
|
if proxy_type not in ('http', 'https', 'socks5', 'socks4'):
|
||||||
|
proxy_type = 'http'
|
||||||
|
|
||||||
|
host = parsed.hostname
|
||||||
|
port = parsed.port
|
||||||
|
username = parsed.username or None
|
||||||
|
password = parsed.password or None
|
||||||
|
|
||||||
|
if not host or not port:
|
||||||
|
raise ValueError(f"无法解析 host/port")
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = f"{proxy_type}://{host}:{port}"
|
||||||
|
|
||||||
|
return name, proxy_type, host, port, username, password
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/proxies/batch-import")
|
||||||
|
async def batch_import_proxies(request: ProxyBatchImportRequest):
|
||||||
|
"""
|
||||||
|
批量导入代理,每行格式支持:
|
||||||
|
- host:port
|
||||||
|
- type://host:port
|
||||||
|
- type://user:pass@host:port
|
||||||
|
- 名称|type://user:pass@host:port
|
||||||
|
"""
|
||||||
|
results = {"success": 0, "failed": 0, "errors": []}
|
||||||
|
with get_db() as db:
|
||||||
|
for raw_line in request.lines.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
name, proxy_type, host, port, username, password = _parse_proxy_line(line)
|
||||||
|
crud.create_proxy(
|
||||||
|
db,
|
||||||
|
name=name,
|
||||||
|
type=proxy_type,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
results["success"] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results["failed"] += 1
|
||||||
|
results["errors"].append(f"{raw_line}: {e}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
@router.post("/proxies/{proxy_id}/test")
|
@router.post("/proxies/{proxy_id}/test")
|
||||||
async def test_proxy_item(proxy_id: int):
|
async def test_proxy_item(proxy_id: int):
|
||||||
"""测试单个代理"""
|
"""测试单个代理"""
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ const elements = {
|
|||||||
addProxyBtn: document.getElementById('add-proxy-btn'),
|
addProxyBtn: document.getElementById('add-proxy-btn'),
|
||||||
testAllProxiesBtn: document.getElementById('test-all-proxies-btn'),
|
testAllProxiesBtn: document.getElementById('test-all-proxies-btn'),
|
||||||
deleteDisabledProxiesBtn: document.getElementById('delete-disabled-proxies-btn'),
|
deleteDisabledProxiesBtn: document.getElementById('delete-disabled-proxies-btn'),
|
||||||
|
batchImportProxyBtn: document.getElementById('batch-import-proxy-btn'),
|
||||||
|
batchImportProxyModal: document.getElementById('batch-import-proxy-modal'),
|
||||||
|
closeBatchImportProxyModal: document.getElementById('close-batch-import-proxy-modal'),
|
||||||
|
cancelBatchImportProxyBtn: document.getElementById('cancel-batch-import-proxy-btn'),
|
||||||
|
confirmBatchImportProxyBtn: document.getElementById('confirm-batch-import-proxy-btn'),
|
||||||
addProxyModal: document.getElementById('add-proxy-modal'),
|
addProxyModal: document.getElementById('add-proxy-modal'),
|
||||||
proxyItemForm: document.getElementById('proxy-item-form'),
|
proxyItemForm: document.getElementById('proxy-item-form'),
|
||||||
closeProxyModal: document.getElementById('close-proxy-modal'),
|
closeProxyModal: document.getElementById('close-proxy-modal'),
|
||||||
@@ -211,6 +216,38 @@ function initEventListeners() {
|
|||||||
elements.addProxyBtn.addEventListener('click', () => openProxyModal());
|
elements.addProxyBtn.addEventListener('click', () => openProxyModal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elements.batchImportProxyBtn) {
|
||||||
|
elements.batchImportProxyBtn.addEventListener('click', () => {
|
||||||
|
document.getElementById('batch-import-proxy-data').value = '';
|
||||||
|
document.getElementById('batch-import-proxy-result').innerHTML = '';
|
||||||
|
elements.batchImportProxyModal.classList.add('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.closeBatchImportProxyModal) {
|
||||||
|
elements.closeBatchImportProxyModal.addEventListener('click', () => {
|
||||||
|
elements.batchImportProxyModal.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.cancelBatchImportProxyBtn) {
|
||||||
|
elements.cancelBatchImportProxyBtn.addEventListener('click', () => {
|
||||||
|
elements.batchImportProxyModal.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.confirmBatchImportProxyBtn) {
|
||||||
|
elements.confirmBatchImportProxyBtn.addEventListener('click', handleBatchImportProxies);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.batchImportProxyModal) {
|
||||||
|
elements.batchImportProxyModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === elements.batchImportProxyModal) {
|
||||||
|
elements.batchImportProxyModal.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (elements.testAllProxiesBtn) {
|
if (elements.testAllProxiesBtn) {
|
||||||
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
||||||
}
|
}
|
||||||
@@ -833,7 +870,7 @@ function renderProxies(proxies) {
|
|||||||
<td><code>${escapeHtml(proxy.host)}:${proxy.port}</code></td>
|
<td><code>${escapeHtml(proxy.host)}:${proxy.port}</code></td>
|
||||||
<td>
|
<td>
|
||||||
${proxy.is_default
|
${proxy.is_default
|
||||||
? '<span class="status-badge active">默认</span>'
|
? `<button class="btn btn-ghost btn-sm" onclick="handleUnsetProxyDefault(${proxy.id})" title="取消默认">取消默认</button>`
|
||||||
: `<button class="btn btn-ghost btn-sm" onclick="handleSetProxyDefault(${proxy.id})" title="设为默认">设默认</button>`
|
: `<button class="btn btn-ghost btn-sm" onclick="handleSetProxyDefault(${proxy.id})" title="设为默认">设默认</button>`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
@@ -847,7 +884,10 @@ function renderProxies(proxies) {
|
|||||||
<div class="dropdown-menu" style="min-width:80px;">
|
<div class="dropdown-menu" style="min-width:80px;">
|
||||||
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);testProxyItem(${proxy.id})">测试</a>
|
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);testProxyItem(${proxy.id})">测试</a>
|
||||||
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);toggleProxyItem(${proxy.id}, ${!proxy.enabled})">${proxy.enabled ? '禁用' : '启用'}</a>
|
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);toggleProxyItem(${proxy.id}, ${!proxy.enabled})">${proxy.enabled ? '禁用' : '启用'}</a>
|
||||||
${!proxy.is_default ? `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleSetProxyDefault(${proxy.id})">设为默认</a>` : ''}
|
${proxy.is_default
|
||||||
|
? `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleUnsetProxyDefault(${proxy.id})">取消默认</a>`
|
||||||
|
: `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleSetProxyDefault(${proxy.id})">设为默认</a>`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-danger btn-sm" onclick="deleteProxyItem(${proxy.id})">删除</button>
|
<button class="btn btn-danger btn-sm" onclick="deleteProxyItem(${proxy.id})">删除</button>
|
||||||
@@ -880,6 +920,36 @@ async function handleSetProxyDefault(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消默认代理
|
||||||
|
async function handleUnsetProxyDefault(id) {
|
||||||
|
try {
|
||||||
|
await api.post(`/settings/proxies/${id}/unset-default`);
|
||||||
|
toast.success('已取消默认代理');
|
||||||
|
loadProxies();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('操作失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量导入代理
|
||||||
|
async function handleBatchImportProxies() {
|
||||||
|
const lines = document.getElementById('batch-import-proxy-data').value;
|
||||||
|
const resultEl = document.getElementById('batch-import-proxy-result');
|
||||||
|
if (!lines.trim()) {
|
||||||
|
resultEl.innerHTML = '<span style="color:var(--color-warning)">请输入代理数据</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await api.post('/settings/proxies/batch-import', { lines });
|
||||||
|
const color = result.failed > 0 ? 'var(--color-warning)' : 'var(--color-success)';
|
||||||
|
resultEl.innerHTML = `<span style="color:${color}">导入成功 ${result.success} 条,失败 ${result.failed} 条。</span>`
|
||||||
|
+ (result.errors.length ? '<br><pre style="font-size:0.8rem;margin-top:4px;">' + result.errors.join('\n') + '</pre>' : '');
|
||||||
|
if (result.success > 0) await loadProxies();
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.innerHTML = `<span style="color:var(--color-danger)">导入失败: ${error.message}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 打开代理模态框
|
// 打开代理模态框
|
||||||
function openProxyModal(proxy = null) {
|
function openProxyModal(proxy = null) {
|
||||||
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
|
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
<div style="display: flex; gap: var(--spacing-sm);">
|
<div style="display: flex; gap: var(--spacing-sm);">
|
||||||
<button class="btn btn-secondary btn-sm" id="test-all-proxies-btn">🔌 测试全部</button>
|
<button class="btn btn-secondary btn-sm" id="test-all-proxies-btn">🔌 测试全部</button>
|
||||||
<button class="btn btn-danger btn-sm" id="delete-disabled-proxies-btn" disabled>🧹 删除禁用项</button>
|
<button class="btn btn-danger btn-sm" id="delete-disabled-proxies-btn" disabled>🧹 删除禁用项</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="batch-import-proxy-btn">📋 批量导入</button>
|
||||||
<button class="btn btn-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
<button class="btn btn-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,6 +153,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量导入代理模态框 -->
|
||||||
|
<div class="modal" id="batch-import-proxy-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>批量导入代理</h3>
|
||||||
|
<button class="modal-close" id="close-batch-import-proxy-modal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-muted" style="margin-bottom: var(--spacing-sm); font-size: 0.875rem;">每行一条,支持格式:<code>host:port</code>、<code>type://host:port</code>、<code>type://user:pass@host:port</code>、<code>名称|type://user:pass@host:port</code></p>
|
||||||
|
<textarea id="batch-import-proxy-data" rows="10" style="width:100%;font-family:monospace;font-size:0.85rem;" placeholder="127.0.0.1:7890
|
||||||
|
socks5://127.0.0.1:1080
|
||||||
|
http://user:pass@host:port
|
||||||
|
MyProxy|socks5://user:pass@host:port"></textarea>
|
||||||
|
<div id="batch-import-proxy-result" style="margin-top: var(--spacing-sm); font-size: 0.875rem;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" id="cancel-batch-import-proxy-btn">取消</button>
|
||||||
|
<button class="btn btn-primary" id="confirm-batch-import-proxy-btn">导入</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 添加代理模态框 -->
|
<!-- 添加代理模态框 -->
|
||||||
<div class="modal" id="add-proxy-modal">
|
<div class="modal" id="add-proxy-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user