mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-09-06 16:06:41 +08:00
Merge pull request #68 from ZHOUKAILIAN/feat/auto-disable-failed-proxies
Feat/auto disable failed proxies
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# OpenAI 账号管理系统 v2
|
# OpenAI 账号管理系统 v2
|
||||||
|
|
||||||
管理 OpenAI 账号的 Web UI 系统,支持多种邮箱服务、并发批量注册、代理管理和账号管理。
|
管理 OpenAI 账号的 Web UI 系统,支持多种邮箱服务、并发批量注册、代理管理和账号管理。
|
||||||
|
|
||||||
> ⚠️ **免责声明**:本工具仅供学习和研究使用,使用本工具产生的一切后果由使用者自行承担。请遵守相关服务的使用条款,不要用于任何违法或不当用途。 如有侵权,请及时联系,会及时删除。
|
> ⚠️ **免责声明**:本工具仅供学习和研究使用,使用本工具产生的一切后果由使用者自行承担。请遵守相关服务的使用条款,不要用于任何违法或不当用途。 如有侵权,请及时联系,会及时删除。
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|||||||
@@ -57,6 +57,35 @@ class TokenRefreshManager:
|
|||||||
session = cffi_requests.Session(impersonate="chrome120", proxy=self.proxy_url)
|
session = cffi_requests.Session(impersonate="chrome120", proxy=self.proxy_url)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
def _parse_oauth_error(self, response: cffi_requests.Response) -> str:
|
||||||
|
"""解析 OAuth 错误信息"""
|
||||||
|
body_text = (response.text or "").strip()
|
||||||
|
error_message = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
error_obj = body.get("error") if isinstance(body, dict) else None
|
||||||
|
if isinstance(error_obj, dict):
|
||||||
|
error_message = str(error_obj.get("message") or "").strip()
|
||||||
|
elif isinstance(body, dict):
|
||||||
|
error_message = str(body.get("error_description") or body.get("message") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
error_lower = error_message.lower()
|
||||||
|
if "refresh token has already been used" in error_lower:
|
||||||
|
return "OAuth refresh_token 已失效(一次性令牌已被使用),请重新登录该账号后再上传 CPA"
|
||||||
|
if response.status_code == 401:
|
||||||
|
if error_message:
|
||||||
|
return f"OAuth token 刷新失败: {error_message}"
|
||||||
|
else:
|
||||||
|
return "OAuth token 刷新失败: refresh_token 无效或已过期,请重新登录账号"
|
||||||
|
if error_message:
|
||||||
|
return f"OAuth token 刷新失败: {error_message}"
|
||||||
|
if body_text:
|
||||||
|
return f"OAuth token 刷新失败: HTTP {response.status_code}, 响应: {body_text[:200]}"
|
||||||
|
return f"OAuth token 刷新失败: HTTP {response.status_code}"
|
||||||
|
|
||||||
def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult:
|
def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult:
|
||||||
"""
|
"""
|
||||||
使用 Session Token 刷新
|
使用 Session Token 刷新
|
||||||
@@ -167,7 +196,7 @@ class TokenRefreshManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
result.error_message = f"OAuth token 刷新失败: HTTP {response.status_code}"
|
result.error_message = self._parse_oauth_error(response)
|
||||||
logger.warning(f"{result.error_message}, 响应: {response.text[:200]}")
|
logger.warning(f"{result.error_message}, 响应: {response.text[:200]}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -472,6 +472,13 @@ def delete_proxy(db: Session, proxy_id: int) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_disabled_proxies(db: Session) -> int:
|
||||||
|
"""删除所有已禁用代理"""
|
||||||
|
deleted = db.query(Proxy).filter(Proxy.enabled == False).delete(synchronize_session=False)
|
||||||
|
db.commit()
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
def update_proxy_last_used(db: Session, proxy_id: int) -> bool:
|
def update_proxy_last_used(db: Session, proxy_id: int) -> bool:
|
||||||
"""更新代理最后使用时间"""
|
"""更新代理最后使用时间"""
|
||||||
db_proxy = get_proxy_by_id(db, proxy_id)
|
db_proxy = get_proxy_by_id(db, proxy_id)
|
||||||
@@ -715,7 +722,6 @@ def delete_tm_service(db: Session, service_id: int) -> bool:
|
|||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def update_outlook_refresh_token(db: Session, service_id: int, email: str, new_refresh_token: str):
|
def update_outlook_refresh_token(db: Session, service_id: int, email: str, new_refresh_token: str):
|
||||||
"""更新 EmailService.config 中指定邮箱的 refresh_token"""
|
"""更新 EmailService.config 中指定邮箱的 refresh_token"""
|
||||||
service = db.query(EmailService).filter(EmailService.id == service_id).first()
|
service = db.query(EmailService).filter(EmailService.id == service_id).first()
|
||||||
|
|||||||
+9
-7
@@ -108,8 +108,9 @@ def create_app() -> FastAPI:
|
|||||||
async def login_page(request: Request, next: Optional[str] = "/"):
|
async def login_page(request: Request, next: Optional[str] = "/"):
|
||||||
"""登录页面"""
|
"""登录页面"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"login.html",
|
"login.html",
|
||||||
{"request": request, "error": "", "next": next or "/"}
|
{"error": "", "next": next or "/"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.post("/login")
|
@app.post("/login")
|
||||||
@@ -118,8 +119,9 @@ def create_app() -> FastAPI:
|
|||||||
expected = get_settings().webui_access_password.get_secret_value()
|
expected = get_settings().webui_access_password.get_secret_value()
|
||||||
if not secrets.compare_digest(password, expected):
|
if not secrets.compare_digest(password, expected):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
request,
|
||||||
"login.html",
|
"login.html",
|
||||||
{"request": request, "error": "密码错误", "next": next or "/"},
|
{"error": "密码错误", "next": next or "/"},
|
||||||
status_code=401
|
status_code=401
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,33 +141,33 @@ def create_app() -> FastAPI:
|
|||||||
"""首页 - 注册页面"""
|
"""首页 - 注册页面"""
|
||||||
if not _is_authenticated(request):
|
if not _is_authenticated(request):
|
||||||
return _redirect_to_login(request)
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse(request, "index.html")
|
||||||
|
|
||||||
@app.get("/accounts", response_class=HTMLResponse)
|
@app.get("/accounts", response_class=HTMLResponse)
|
||||||
async def accounts_page(request: Request):
|
async def accounts_page(request: Request):
|
||||||
"""账号管理页面"""
|
"""账号管理页面"""
|
||||||
if not _is_authenticated(request):
|
if not _is_authenticated(request):
|
||||||
return _redirect_to_login(request)
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("accounts.html", {"request": request})
|
return templates.TemplateResponse(request, "accounts.html")
|
||||||
|
|
||||||
@app.get("/email-services", response_class=HTMLResponse)
|
@app.get("/email-services", response_class=HTMLResponse)
|
||||||
async def email_services_page(request: Request):
|
async def email_services_page(request: Request):
|
||||||
"""邮箱服务管理页面"""
|
"""邮箱服务管理页面"""
|
||||||
if not _is_authenticated(request):
|
if not _is_authenticated(request):
|
||||||
return _redirect_to_login(request)
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("email_services.html", {"request": request})
|
return templates.TemplateResponse(request, "email_services.html")
|
||||||
|
|
||||||
@app.get("/settings", response_class=HTMLResponse)
|
@app.get("/settings", response_class=HTMLResponse)
|
||||||
async def settings_page(request: Request):
|
async def settings_page(request: Request):
|
||||||
"""设置页面"""
|
"""设置页面"""
|
||||||
if not _is_authenticated(request):
|
if not _is_authenticated(request):
|
||||||
return _redirect_to_login(request)
|
return _redirect_to_login(request)
|
||||||
return templates.TemplateResponse("settings.html", {"request": request})
|
return templates.TemplateResponse(request, "settings.html")
|
||||||
|
|
||||||
@app.get("/payment", response_class=HTMLResponse)
|
@app.get("/payment", response_class=HTMLResponse)
|
||||||
async def payment_page(request: Request):
|
async def payment_page(request: Request):
|
||||||
"""支付页面"""
|
"""支付页面"""
|
||||||
return templates.TemplateResponse("payment.html", {"request": request})
|
return templates.TemplateResponse(request, "payment.html")
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
|
|||||||
+84
-69
@@ -469,6 +469,60 @@ class ProxyUpdateRequest(BaseModel):
|
|||||||
priority: Optional[int] = None
|
priority: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _test_proxy_connectivity(proxy_url: str) -> dict:
|
||||||
|
"""测试代理连通性并返回统一结果。"""
|
||||||
|
import time
|
||||||
|
from curl_cffi import requests as cffi_requests
|
||||||
|
|
||||||
|
test_url = "https://api.ipify.org?format=json"
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
proxies_dict = {
|
||||||
|
"http": proxy_url,
|
||||||
|
"https": proxy_url
|
||||||
|
}
|
||||||
|
|
||||||
|
response = cffi_requests.get(
|
||||||
|
test_url,
|
||||||
|
proxies=proxies_dict,
|
||||||
|
timeout=3,
|
||||||
|
impersonate="chrome110"
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
if response.status_code == 200:
|
||||||
|
ip_info = response.json()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"ip": ip_info.get("ip", ""),
|
||||||
|
"response_time": round(elapsed_time * 1000),
|
||||||
|
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"状态码: {response.status_code}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _auto_disable_proxy_on_failure(db, proxy, message: str) -> dict:
|
||||||
|
"""代理测试失败时自动禁用,并返回统一提示。"""
|
||||||
|
auto_disabled = False
|
||||||
|
if proxy.enabled:
|
||||||
|
crud.update_proxy(db, proxy.id, enabled=False)
|
||||||
|
auto_disabled = True
|
||||||
|
|
||||||
|
final_message = message
|
||||||
|
if auto_disabled:
|
||||||
|
final_message = f"{message},已自动禁用"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"auto_disabled": auto_disabled,
|
||||||
|
"message": final_message,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/proxies")
|
@router.get("/proxies")
|
||||||
async def get_proxies_list(enabled: Optional[bool] = None):
|
async def get_proxies_list(enabled: Optional[bool] = None):
|
||||||
"""获取代理列表"""
|
"""获取代理列表"""
|
||||||
@@ -559,107 +613,59 @@ async def set_proxy_default(proxy_id: int):
|
|||||||
@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):
|
||||||
"""测试单个代理"""
|
"""测试单个代理"""
|
||||||
import time
|
|
||||||
from curl_cffi import requests as cffi_requests
|
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
proxy = crud.get_proxy_by_id(db, proxy_id)
|
proxy = crud.get_proxy_by_id(db, proxy_id)
|
||||||
if not proxy:
|
if not proxy:
|
||||||
raise HTTPException(status_code=404, detail="代理不存在")
|
raise HTTPException(status_code=404, detail="代理不存在")
|
||||||
|
|
||||||
proxy_url = proxy.proxy_url
|
|
||||||
test_url = "https://api.ipify.org?format=json"
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proxies = {
|
result = _test_proxy_connectivity(proxy.proxy_url)
|
||||||
"http": proxy_url,
|
if result["success"]:
|
||||||
"https": proxy_url
|
return result
|
||||||
}
|
return _auto_disable_proxy_on_failure(db, proxy, f"代理返回错误状态码: {result['message'].removeprefix('状态码: ')}")
|
||||||
|
|
||||||
response = cffi_requests.get(
|
|
||||||
test_url,
|
|
||||||
proxies=proxies,
|
|
||||||
timeout=3,
|
|
||||||
impersonate="chrome110"
|
|
||||||
)
|
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
ip_info = response.json()
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"ip": ip_info.get("ip", ""),
|
|
||||||
"response_time": round(elapsed_time * 1000),
|
|
||||||
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"代理返回错误状态码: {response.status_code}"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return _auto_disable_proxy_on_failure(db, proxy, f"代理连接失败: {str(e)}")
|
||||||
"success": False,
|
|
||||||
"message": f"代理连接失败: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/proxies/test-all")
|
@router.post("/proxies/test-all")
|
||||||
async def test_all_proxies():
|
async def test_all_proxies():
|
||||||
"""测试所有启用的代理"""
|
"""测试所有启用的代理"""
|
||||||
import time
|
|
||||||
from curl_cffi import requests as cffi_requests
|
|
||||||
|
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
proxies = crud.get_enabled_proxies(db)
|
proxies = crud.get_enabled_proxies(db)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
auto_disabled_count = 0
|
||||||
for proxy in proxies:
|
for proxy in proxies:
|
||||||
proxy_url = proxy.proxy_url
|
|
||||||
test_url = "https://api.ipify.org?format=json"
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proxies_dict = {
|
result = _test_proxy_connectivity(proxy.proxy_url)
|
||||||
"http": proxy_url,
|
if result["success"]:
|
||||||
"https": proxy_url
|
|
||||||
}
|
|
||||||
|
|
||||||
response = cffi_requests.get(
|
|
||||||
test_url,
|
|
||||||
proxies=proxies_dict,
|
|
||||||
timeout=3,
|
|
||||||
impersonate="chrome110"
|
|
||||||
)
|
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
ip_info = response.json()
|
|
||||||
results.append({
|
results.append({
|
||||||
"id": proxy.id,
|
"id": proxy.id,
|
||||||
"name": proxy.name,
|
"name": proxy.name,
|
||||||
"success": True,
|
"success": True,
|
||||||
"ip": ip_info.get("ip", ""),
|
"ip": result.get("ip", ""),
|
||||||
"response_time": round(elapsed_time * 1000)
|
"response_time": result.get("response_time"),
|
||||||
|
"auto_disabled": False,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
|
failure_result = _auto_disable_proxy_on_failure(
|
||||||
|
db,
|
||||||
|
proxy,
|
||||||
|
f"代理返回错误状态码: {result['message'].removeprefix('状态码: ')}"
|
||||||
|
)
|
||||||
|
auto_disabled_count += 1 if failure_result["auto_disabled"] else 0
|
||||||
results.append({
|
results.append({
|
||||||
"id": proxy.id,
|
"id": proxy.id,
|
||||||
"name": proxy.name,
|
"name": proxy.name,
|
||||||
"success": False,
|
**failure_result,
|
||||||
"message": f"状态码: {response.status_code}"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
failure_result = _auto_disable_proxy_on_failure(db, proxy, f"代理连接失败: {str(e)}")
|
||||||
|
auto_disabled_count += 1 if failure_result["auto_disabled"] else 0
|
||||||
results.append({
|
results.append({
|
||||||
"id": proxy.id,
|
"id": proxy.id,
|
||||||
"name": proxy.name,
|
"name": proxy.name,
|
||||||
"success": False,
|
**failure_result,
|
||||||
"message": str(e)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
success_count = sum(1 for r in results if r["success"])
|
success_count = sum(1 for r in results if r["success"])
|
||||||
@@ -667,6 +673,7 @@ async def test_all_proxies():
|
|||||||
"total": len(proxies),
|
"total": len(proxies),
|
||||||
"success": success_count,
|
"success": success_count,
|
||||||
"failed": len(proxies) - success_count,
|
"failed": len(proxies) - success_count,
|
||||||
|
"auto_disabled": auto_disabled_count,
|
||||||
"results": results
|
"results": results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,6 +698,14 @@ async def disable_proxy(proxy_id: int):
|
|||||||
return {"success": True, "message": "代理已禁用"}
|
return {"success": True, "message": "代理已禁用"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/proxies/disabled/batch-delete")
|
||||||
|
async def delete_disabled_proxies():
|
||||||
|
"""批量删除所有已禁用代理"""
|
||||||
|
with get_db() as db:
|
||||||
|
deleted = crud.delete_disabled_proxies(db)
|
||||||
|
return {"success": True, "deleted": deleted, "message": f"已删除 {deleted} 个禁用代理"}
|
||||||
|
|
||||||
|
|
||||||
# ============== Outlook 设置 ==============
|
# ============== Outlook 设置 ==============
|
||||||
|
|
||||||
class OutlookSettings(BaseModel):
|
class OutlookSettings(BaseModel):
|
||||||
|
|||||||
+18
-1
@@ -11,6 +11,8 @@ let selectedAccounts = new Set();
|
|||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let selectAllPages = false; // 是否选中了全部页
|
let selectAllPages = false; // 是否选中了全部页
|
||||||
let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件
|
let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件
|
||||||
|
const refreshingAccountIds = new Set();
|
||||||
|
let isBatchValidating = false;
|
||||||
|
|
||||||
// DOM 元素
|
// DOM 元素
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -488,6 +490,12 @@ function updateBatchButtons() {
|
|||||||
|
|
||||||
// 刷新单个账号Token
|
// 刷新单个账号Token
|
||||||
async function refreshToken(id) {
|
async function refreshToken(id) {
|
||||||
|
if (refreshingAccountIds.has(id)) {
|
||||||
|
toast.info('该账号正在刷新,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshingAccountIds.add(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.info('正在刷新Token...');
|
toast.info('正在刷新Token...');
|
||||||
const result = await api.post(`/accounts/${id}/refresh`);
|
const result = await api.post(`/accounts/${id}/refresh`);
|
||||||
@@ -500,6 +508,8 @@ async function refreshToken(id) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('刷新失败: ' + error.message);
|
toast.error('刷新失败: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
refreshingAccountIds.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,17 +538,24 @@ async function handleBatchRefresh() {
|
|||||||
// 批量验证Token
|
// 批量验证Token
|
||||||
async function handleBatchValidate() {
|
async function handleBatchValidate() {
|
||||||
if (getEffectiveCount() === 0) return;
|
if (getEffectiveCount() === 0) return;
|
||||||
|
if (isBatchValidating) {
|
||||||
|
toast.info('批量验证进行中,请稍候...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBatchValidating = true;
|
||||||
|
|
||||||
elements.batchValidateBtn.disabled = true;
|
elements.batchValidateBtn.disabled = true;
|
||||||
elements.batchValidateBtn.textContent = '验证中...';
|
elements.batchValidateBtn.textContent = '验证中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/accounts/batch-validate', buildBatchPayload());
|
const result = await api.post('/accounts/batch-validate', buildBatchPayload(), { timeoutMs: 120000 });
|
||||||
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('批量验证失败: ' + error.message);
|
toast.error('批量验证失败: ' + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
isBatchValidating = false;
|
||||||
updateBatchButtons();
|
updateBatchButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-5
@@ -31,6 +31,7 @@ const elements = {
|
|||||||
proxiesTable: document.getElementById('proxies-table'),
|
proxiesTable: document.getElementById('proxies-table'),
|
||||||
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'),
|
||||||
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'),
|
||||||
@@ -206,6 +207,10 @@ function initEventListeners() {
|
|||||||
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elements.deleteDisabledProxiesBtn) {
|
||||||
|
elements.deleteDisabledProxiesBtn.addEventListener('click', handleDeleteDisabledProxies);
|
||||||
|
}
|
||||||
|
|
||||||
if (elements.closeProxyModal) {
|
if (elements.closeProxyModal) {
|
||||||
elements.closeProxyModal.addEventListener('click', closeProxyModal);
|
elements.closeProxyModal.addEventListener('click', closeProxyModal);
|
||||||
}
|
}
|
||||||
@@ -772,11 +777,13 @@ async function loadProxies() {
|
|||||||
try {
|
try {
|
||||||
const data = await api.get('/settings/proxies');
|
const data = await api.get('/settings/proxies');
|
||||||
renderProxies(data.proxies);
|
renderProxies(data.proxies);
|
||||||
|
updateProxyBulkActions(data.proxies || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载代理列表失败:', error);
|
console.error('加载代理列表失败:', error);
|
||||||
|
updateProxyBulkActions([]);
|
||||||
elements.proxiesTable.innerHTML = `
|
elements.proxiesTable.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">
|
<td colspan="8">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">❌</div>
|
<div class="empty-state-icon">❌</div>
|
||||||
<div class="empty-state-title">加载失败</div>
|
<div class="empty-state-title">加载失败</div>
|
||||||
@@ -792,7 +799,7 @@ function renderProxies(proxies) {
|
|||||||
if (!proxies || proxies.length === 0) {
|
if (!proxies || proxies.length === 0) {
|
||||||
elements.proxiesTable.innerHTML = `
|
elements.proxiesTable.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">
|
<td colspan="8">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">🌐</div>
|
<div class="empty-state-icon">🌐</div>
|
||||||
<div class="empty-state-title">暂无代理</div>
|
<div class="empty-state-title">暂无代理</div>
|
||||||
@@ -836,6 +843,17 @@ function renderProxies(proxies) {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateProxyBulkActions(proxies) {
|
||||||
|
if (!elements.deleteDisabledProxiesBtn) return;
|
||||||
|
|
||||||
|
const disabledCount = (proxies || []).filter(proxy => !proxy.enabled).length;
|
||||||
|
elements.deleteDisabledProxiesBtn.disabled = disabledCount === 0;
|
||||||
|
elements.deleteDisabledProxiesBtn.dataset.count = String(disabledCount);
|
||||||
|
elements.deleteDisabledProxiesBtn.textContent = disabledCount > 0
|
||||||
|
? `🧹 删除禁用项 (${disabledCount})`
|
||||||
|
: '🧹 删除禁用项';
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSettingsMoreMenu(btn) {
|
function toggleSettingsMoreMenu(btn) {
|
||||||
const menu = btn.nextElementSibling;
|
const menu = btn.nextElementSibling;
|
||||||
const isActive = menu.classList.contains('active');
|
const isActive = menu.classList.contains('active');
|
||||||
@@ -931,7 +949,12 @@ async function testProxyItem(id) {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message);
|
if (result.auto_disabled) {
|
||||||
|
toast.warning(result.message);
|
||||||
|
await loadProxies();
|
||||||
|
} else {
|
||||||
|
toast.error(result.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('测试失败: ' + error.message);
|
toast.error('测试失败: ' + error.message);
|
||||||
@@ -964,6 +987,22 @@ async function deleteProxyItem(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteDisabledProxies() {
|
||||||
|
const count = Number(elements.deleteDisabledProxiesBtn?.dataset.count || 0);
|
||||||
|
if (!count) return;
|
||||||
|
|
||||||
|
const confirmed = await confirm(`确定要删除全部 ${count} 个已禁用代理吗?此操作不可恢复。`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.delete('/settings/proxies/disabled/batch-delete');
|
||||||
|
toast.success(result.message);
|
||||||
|
await loadProxies();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('批量删除失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 测试所有代理
|
// 测试所有代理
|
||||||
async function handleTestAllProxies() {
|
async function handleTestAllProxies() {
|
||||||
elements.testAllProxiesBtn.disabled = true;
|
elements.testAllProxiesBtn.disabled = true;
|
||||||
@@ -971,8 +1010,13 @@ async function handleTestAllProxies() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.post('/settings/proxies/test-all');
|
const result = await api.post('/settings/proxies/test-all');
|
||||||
toast.info(`测试完成: 成功 ${result.success}, 失败 ${result.failed}`);
|
const summary = `测试完成: 成功 ${result.success}, 失败 ${result.failed}`;
|
||||||
loadProxies();
|
if (result.auto_disabled > 0) {
|
||||||
|
toast.warning(`${summary},已自动禁用 ${result.auto_disabled} 个`);
|
||||||
|
} else {
|
||||||
|
toast.info(summary);
|
||||||
|
}
|
||||||
|
await loadProxies();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('测试失败: ' + error.message);
|
toast.error('测试失败: ' + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -187,12 +187,21 @@ class ApiClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const finalOptions = { ...defaultOptions, ...options };
|
const finalOptions = { ...defaultOptions, ...options };
|
||||||
|
const timeoutMs = Number(finalOptions.timeoutMs || 0);
|
||||||
|
delete finalOptions.timeoutMs;
|
||||||
|
|
||||||
if (finalOptions.body && typeof finalOptions.body === 'object') {
|
if (finalOptions.body && typeof finalOptions.body === 'object') {
|
||||||
finalOptions.body = JSON.stringify(finalOptions.body);
|
finalOptions.body = JSON.stringify(finalOptions.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timeoutId = null;
|
||||||
try {
|
try {
|
||||||
|
if (timeoutMs > 0) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
finalOptions.signal = controller.signal;
|
||||||
|
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, finalOptions);
|
const response = await fetch(url, finalOptions);
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
@@ -205,11 +214,19 @@ class ApiClient {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
const timeoutError = new Error('请求超时,请稍后重试');
|
||||||
|
throw timeoutError;
|
||||||
|
}
|
||||||
// 网络错误处理
|
// 网络错误处理
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
toast.error('网络连接失败,请检查网络');
|
toast.error('网络连接失败,请检查网络');
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
<h3>代理列表</h3>
|
<h3>代理列表</h3>
|
||||||
<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-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
<button class="btn btn-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +116,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="proxies-table">
|
<tbody id="proxies-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">
|
<td colspan="8">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-state-icon">🌐</div>
|
<div class="empty-state-icon">🌐</div>
|
||||||
<div class="empty-state-title">暂无代理</div>
|
<div class="empty-state-title">暂无代理</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user