diff --git a/src/database/crud.py b/src/database/crud.py index 6a29a3c..2e11508 100644 --- a/src/database/crud.py +++ b/src/database/crud.py @@ -484,14 +484,31 @@ def update_proxy_last_used(db: Session, proxy_id: int) -> bool: def get_random_proxy(db: Session) -> Optional[Proxy]: - """随机获取一个启用的代理""" + """随机获取一个启用的代理,优先返回 is_default=True 的代理""" import random + # 优先返回默认代理 + default_proxy = db.query(Proxy).filter(Proxy.enabled == True, Proxy.is_default == True).first() + if default_proxy: + return default_proxy proxies = get_enabled_proxies(db) if not proxies: return None return random.choice(proxies) +def set_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]: + """将指定代理设为默认,同时清除其他代理的默认标记""" + # 清除所有默认标记 + db.query(Proxy).filter(Proxy.is_default == True).update({"is_default": False}) + # 设置新的默认代理 + proxy = db.query(Proxy).filter(Proxy.id == proxy_id).first() + if proxy: + proxy.is_default = True + db.commit() + db.refresh(proxy) + return proxy + + def get_proxies_count(db: Session, enabled: Optional[bool] = None) -> int: """获取代理数量""" query = db.query(func.count(Proxy.id)) diff --git a/src/database/models.py b/src/database/models.py index 87926c6..7e4f52f 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -156,6 +156,7 @@ class Proxy(Base): username = Column(String(100)) password = Column(String(255)) enabled = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) # 是否为默认代理 priority = Column(Integer, default=0) # 优先级(保留字段) last_used = Column(DateTime) # 最后使用时间 created_at = Column(DateTime, default=datetime.utcnow) @@ -171,6 +172,7 @@ class Proxy(Base): 'port': self.port, 'username': self.username, 'enabled': self.enabled, + 'is_default': self.is_default or False, 'priority': self.priority, 'last_used': self.last_used.isoformat() if self.last_used else None, 'created_at': self.created_at.isoformat() if self.created_at else None, diff --git a/src/database/session.py b/src/database/session.py index 1d3a36c..08fd1ae 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -110,6 +110,7 @@ class DatabaseSessionManager: ("accounts", "subscription_type", "VARCHAR(20)"), ("accounts", "subscription_at", "DATETIME"), ("accounts", "cookies", "TEXT"), + ("proxies", "is_default", "BOOLEAN DEFAULT 0"), ] # 确保新表存在(create_tables 已处理,此处兜底) diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index a7297f6..f3d0aa0 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -347,18 +347,22 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: saved_account = db.query(AccountModel).filter_by(email=result.email).first() if saved_account and saved_account.access_token: token_data = generate_token_json(saved_account) - # 解析指定 CPA 服务 + # 解析指定 CPA 服务,未指定则取第一个启用的服务 _cpa_api_url = None _cpa_api_token = None + _svc = None if cpa_service_id: try: _svc = crud.get_cpa_service_by_id(db, cpa_service_id) - if _svc: - _cpa_api_url = _svc.api_url - _cpa_api_token = _svc.api_token - log_callback(f"[CPA] 使用服务: {_svc.name}") except Exception: pass + if _svc is None: + svcs = crud.get_cpa_services(db, enabled=True) + _svc = svcs[0] if svcs else None + if _svc: + _cpa_api_url = _svc.api_url + _cpa_api_token = _svc.api_token + log_callback(f"[CPA] 使用服务: {_svc.name}") cpa_success, cpa_msg = upload_to_cpa(token_data, api_url=_cpa_api_url, api_token=_cpa_api_token) if cpa_success: saved_account.cpa_uploaded = True diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index 0762ab3..63bc107 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -111,101 +111,6 @@ async def get_all_settings(): } -@router.get("/proxy") -async def get_proxy_settings(): - """获取代理设置""" - settings = get_settings() - - return { - "enabled": settings.proxy_enabled, - "type": settings.proxy_type, - "host": settings.proxy_host, - "port": settings.proxy_port, - "username": settings.proxy_username, - "has_password": bool(settings.proxy_password), - "proxy_url": settings.proxy_url, - } - - -@router.post("/proxy") -async def update_proxy_settings(request: ProxySettings): - """更新代理设置""" - update_dict = { - "proxy_enabled": request.enabled, - "proxy_type": request.type, - "proxy_host": request.host, - "proxy_port": request.port, - "proxy_username": request.username, - } - - if request.password: - update_dict["proxy_password"] = request.password - - update_settings(**update_dict) - - return {"success": True, "message": "代理设置已更新"} - - -@router.post("/proxy/test") -async def test_proxy_settings(request: ProxySettings): - """测试代理连接""" - import time - from curl_cffi import requests as cffi_requests - - # 构建代理 URL - if request.type == "http": - scheme = "http" - elif request.type == "socks5": - scheme = "socks5" - else: - raise HTTPException(status_code=400, detail="不支持的代理类型") - - auth = "" - if request.username and request.password: - auth = f"{request.username}:{request.password}@" - - proxy_url = f"{scheme}://{auth}{request.host}:{request.port}" - - # 测试连接 - test_url = "https://api.ipify.org?format=json" - start_time = time.time() - - try: - proxies = { - "http": proxy_url, - "https": proxy_url - } - - 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: - return { - "success": False, - "message": f"代理连接失败: {str(e)}" - } - - @router.get("/proxy/dynamic") async def get_dynamic_proxy_settings(): """获取动态代理设置""" @@ -639,6 +544,16 @@ async def delete_proxy_item(proxy_id: int): return {"success": True, "message": "代理已删除"} +@router.post("/proxies/{proxy_id}/set-default") +async def set_proxy_default(proxy_id: int): + """将指定代理设为默认""" + with get_db() as db: + proxy = crud.set_proxy_default(db, proxy_id) + if not proxy: + raise HTTPException(status_code=404, detail="代理不存在") + return {"success": True, "proxy": proxy.to_dict()} + + @router.post("/proxies/{proxy_id}/test") async def test_proxy_item(proxy_id: int): """测试单个代理""" @@ -774,77 +689,6 @@ async def disable_proxy(proxy_id: int): return {"success": True, "message": "代理已禁用"} -# ============== CPA 设置 ============== - -class CPASettings(BaseModel): - """CPA 设置""" - enabled: bool = False - api_url: str = "" - api_token: str = "" - - -class CPATestRequest(BaseModel): - """CPA 测试请求""" - api_url: str - api_token: str - - -@router.get("/cpa") -async def get_cpa_settings(): - """获取 CPA 设置""" - settings = get_settings() - - return { - "enabled": settings.cpa_enabled, - "api_url": settings.cpa_api_url, - "has_token": bool(settings.cpa_api_token and settings.cpa_api_token.get_secret_value()), - } - - -@router.post("/cpa") -async def update_cpa_settings(request: CPASettings): - """更新 CPA 设置""" - update_dict = { - "cpa_enabled": request.enabled, - "cpa_api_url": request.api_url, - } - - # 只有提供了 token 才更新 - if request.api_token: - update_dict["cpa_api_token"] = request.api_token - - update_settings(**update_dict) - - return {"success": True, "message": "CPA 设置已更新"} - - -@router.post("/cpa/test") -async def test_cpa_connection(request: CPATestRequest): - """测试 CPA 连接""" - from ...core.cpa_upload import test_cpa_connection as do_test - - settings = get_settings() - proxy = settings.proxy_url - - # 如果传入 'use_saved_token',使用已保存的 token - api_token = request.api_token - if api_token == 'use_saved_token' or not api_token: - if settings.cpa_api_token: - api_token = settings.cpa_api_token.get_secret_value() - else: - return { - "success": False, - "message": "未配置 API Token" - } - - success, message = do_test(request.api_url, api_token, proxy) - - return { - "success": success, - "message": message - } - - # ============== Outlook 设置 ============== class OutlookSettings(BaseModel): diff --git a/static/js/app.js b/static/js/app.js index 7069223..bdac53d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -102,19 +102,19 @@ document.addEventListener('DOMContentLoaded', () => { // 检查 CPA 是否启用,未启用则禁用复选框;同时加载 CPA 服务列表 async function checkCpaEnabled() { if (!elements.autoUploadCpa) return; + // 加载 CPA 服务列表,列表为空则禁用复选框 + await loadCpaServiceOptions(); try { - const data = await api.get('/settings/cpa'); - if (!data.enabled) { + const services = await api.get('/cpa-services?enabled=true'); + if (!services || services.length === 0) { elements.autoUploadCpa.disabled = true; - elements.autoUploadCpa.title = '请先在设置中启用 CPA 上传'; + elements.autoUploadCpa.title = '请先在设置中添加 CPA 服务'; const label = elements.autoUploadCpa.closest('label'); if (label) label.style.opacity = '0.5'; } } catch (e) { elements.autoUploadCpa.disabled = true; } - // 加载 CPA 服务列表 - await loadCpaServiceOptions(); // 复选框联动显示/隐藏服务选择器 if (elements.autoUploadCpa) { elements.autoUploadCpa.addEventListener('change', () => { @@ -130,8 +130,7 @@ async function loadCpaServiceOptions() { if (!elements.cpaServiceSelect) return; try { const services = await api.get('/cpa-services?enabled=true'); - // 保留「使用全局配置」选项 - const defaultOpt = '使用全局配置'; + const defaultOpt = '自动选择(第一个启用的服务)'; const opts = services.map(s => `${s.name.replace(/` ).join(''); diff --git a/static/js/settings.js b/static/js/settings.js index be45a95..022ae3a 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -7,9 +7,7 @@ const elements = { tabs: document.querySelectorAll('.tab-btn'), tabContents: document.querySelectorAll('.tab-content'), - proxyForm: document.getElementById('proxy-form'), registrationForm: document.getElementById('registration-settings-form'), - testProxyBtn: document.getElementById('test-proxy-btn'), backupBtn: document.getElementById('backup-btn'), cleanupBtn: document.getElementById('cleanup-btn'), addEmailServiceBtn: document.getElementById('add-email-service-btn'), @@ -41,9 +39,6 @@ const elements = { // 动态代理设置 dynamicProxyForm: document.getElementById('dynamic-proxy-form'), testDynamicProxyBtn: document.getElementById('test-dynamic-proxy-btn'), - // CPA 设置 - cpaForm: document.getElementById('cpa-form'), - testCpaBtn: document.getElementById('test-cpa-btn'), // CPA 服务管理 addCpaServiceBtn: document.getElementById('add-cpa-service-btn'), cpaServicesTable: document.getElementById('cpa-services-table'), @@ -95,16 +90,6 @@ function initTabs() { // 事件监听 function initEventListeners() { - // 代理表单 - if (elements.proxyForm) { - elements.proxyForm.addEventListener('submit', handleSaveProxy); - } - - // 测试代理 - if (elements.testProxyBtn) { - elements.testProxyBtn.addEventListener('click', handleTestProxy); - } - // 注册配置表单 if (elements.registrationForm) { elements.registrationForm.addEventListener('submit', handleSaveRegistration); @@ -228,15 +213,6 @@ function initEventListeners() { elements.testDynamicProxyBtn.addEventListener('click', handleTestDynamicProxy); } - // CPA 设置 - if (elements.cpaForm) { - elements.cpaForm.addEventListener('submit', handleSaveCpa); - } - - if (elements.testCpaBtn) { - elements.testCpaBtn.addEventListener('click', handleTestCpa); - } - // 验证码设置 if (elements.emailCodeForm) { elements.emailCodeForm.addEventListener('submit', handleSaveEmailCode); @@ -286,13 +262,6 @@ async function loadSettings() { try { const data = await api.get('/settings'); - // 代理设置 - document.getElementById('proxy-enabled').checked = data.proxy?.enabled || false; - document.getElementById('proxy-type').value = data.proxy?.type || 'http'; - document.getElementById('proxy-host').value = data.proxy?.host || '127.0.0.1'; - document.getElementById('proxy-port').value = data.proxy?.port || 7890; - document.getElementById('proxy-username').value = data.proxy?.username || ''; - // 动态代理设置 document.getElementById('dynamic-proxy-enabled').checked = data.proxy?.dynamic_enabled || false; document.getElementById('dynamic-proxy-api-url').value = data.proxy?.dynamic_api_url || ''; @@ -312,8 +281,6 @@ async function loadSettings() { document.getElementById('email-code-poll-interval').value = data.email_code.poll_interval || 3; } - // 加载 CPA 设置 - loadCpaSettings(); // 加载 Outlook 设置 loadOutlookSettings(); // 加载 Team Manager 设置 @@ -445,57 +412,6 @@ async function loadDatabaseInfo() { } } -// 保存代理设置 -async function handleSaveProxy(e) { - e.preventDefault(); - - const data = { - enabled: document.getElementById('proxy-enabled').checked, - type: document.getElementById('proxy-type').value, - host: document.getElementById('proxy-host').value, - port: parseInt(document.getElementById('proxy-port').value), - username: document.getElementById('proxy-username').value || null, - password: document.getElementById('proxy-password').value || null, - }; - - try { - await api.post('/settings/proxy', data); - toast.success('代理设置已保存'); - } catch (error) { - toast.error('保存失败: ' + error.message); - } -} - -// 测试代理 -async function handleTestProxy() { - elements.testProxyBtn.disabled = true; - elements.testProxyBtn.innerHTML = ' 测试中...'; - - try { - const data = { - enabled: document.getElementById('proxy-enabled').checked, - type: document.getElementById('proxy-type').value, - host: document.getElementById('proxy-host').value, - port: parseInt(document.getElementById('proxy-port').value), - username: document.getElementById('proxy-username').value || null, - password: document.getElementById('proxy-password').value || null, - }; - - const result = await api.post('/settings/proxy/test', data); - - if (result.success) { - toast.success(result.message); - } else { - toast.error(result.message); - } - } catch (error) { - toast.error('测试失败: ' + error.message); - } finally { - elements.testProxyBtn.disabled = false; - elements.testProxyBtn.textContent = '🔌 测试连接'; - } -} - // 保存注册配置 async function handleSaveRegistration(e) { e.preventDefault(); @@ -838,6 +754,12 @@ function renderProxies(proxies) { ${escapeHtml(proxy.name)} ${proxy.type.toUpperCase()} ${escapeHtml(proxy.host)}:${proxy.port} + + ${proxy.is_default + ? '默认' + : `设默认` + } + ${proxy.enabled ? '已启用' : '已禁用'} @@ -864,6 +786,17 @@ function renderProxies(proxies) { `).join(''); } +// 设为默认代理 +async function handleSetProxyDefault(id) { + try { + await api.post(`/settings/proxies/${id}/set-default`); + toast.success('已设为默认代理'); + loadProxies(); + } catch (error) { + toast.error('操作失败: ' + error.message); + } +} + // 打开代理模态框 function openProxyModal(proxy = null) { elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理'; @@ -987,45 +920,6 @@ async function handleTestAllProxies() { } -// ============================================================================ -// CPA 设置管理 -// ============================================================================ - -// 加载 CPA 设置 -async function loadCpaSettings() { - try { - const data = await api.get('/settings/cpa'); - - document.getElementById('cpa-enabled').checked = data.enabled || false; - document.getElementById('cpa-api-url').value = data.api_url || ''; - // 不填充 token,只显示是否有值 - document.getElementById('cpa-api-token').value = ''; - document.getElementById('cpa-api-token').placeholder = data.has_token ? '已配置,留空保持不变' : '请输入 API Token'; - - } catch (error) { - console.error('加载 CPA 设置失败:', error); - } -} - -// 保存 CPA 设置 -async function handleSaveCpa(e) { - e.preventDefault(); - - const data = { - enabled: document.getElementById('cpa-enabled').checked, - api_url: document.getElementById('cpa-api-url').value, - api_token: document.getElementById('cpa-api-token').value || '' - }; - - try { - await api.post('/settings/cpa', data); - toast.success('CPA 设置已保存'); - loadCpaSettings(); - } catch (error) { - toast.error('保存失败: ' + error.message); - } -} - // ============================================================================ // Outlook 设置管理 // ============================================================================ @@ -1055,47 +949,6 @@ async function handleSaveOutlookSettings(e) { } } -// 测试 CPA 连接 -async function handleTestCpa() { - const apiUrl = document.getElementById('cpa-api-url').value; - const apiToken = document.getElementById('cpa-api-token').value; - - if (!apiUrl) { - toast.warning('请输入 API URL'); - return; - } - - // 如果 token 为空,尝试使用已保存的 token 进行测试 - if (!apiToken) { - const cpaSettings = await api.get('/settings/cpa'); - if (!cpaSettings.has_token) { - toast.warning('请输入 API Token'); - return; - } - } - - elements.testCpaBtn.disabled = true; - elements.testCpaBtn.innerHTML = ' 测试中...'; - - try { - const result = await api.post('/settings/cpa/test', { - api_url: apiUrl, - api_token: apiToken || 'use_saved_token' - }); - - if (result.success) { - toast.success(result.message); - } else { - toast.error(result.message); - } - } catch (error) { - toast.error('测试失败: ' + error.message); - } finally { - elements.testCpaBtn.disabled = false; - elements.testCpaBtn.textContent = '🔌 测试连接'; - } -} - // ============== 动态代理设置 ============== async function handleSaveDynamicProxy(e) { diff --git a/templates/settings.html b/templates/settings.html index 082a881..109d52a 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -48,66 +48,11 @@ - - - - 默认代理配置 - 当代理列表为空时使用此配置 - - - - - - - 启用代理 - - - - - - 代理类型 - - HTTP - SOCKS5 - - - - - 主机地址 - - - - - 端口 - - - - - - - 用户名 (可选) - - - - - 密码 (可选) - - - - - - 💾 保存设置 - 🔌 测试连接 - - - - - 动态代理配置 - 通过 API 每次获取新代理 IP,优先级高于静态代理和代理列表 + 通过 API 每次获取新代理 IP,优先级高于代理列表 @@ -163,9 +108,10 @@ 名称 类型 地址 + 默认 状态 最后使用 - 操作 + 操作 @@ -258,43 +204,8 @@ - - - CPA 上传配置 - 配置 CliProxyApi 上传功能 - - - - - - - 启用 CPA 上传 - - 启用后可在账号管理页面上传账号到 CPA 管理平台 - - - - API URL - - CPA 管理平台的 API 地址 - - - - API Token - - CPA 管理平台的认证 Token - - - - 💾 保存设置 - 🔌 测试连接 - - - - - - + CPA 服务管理 + 添加服务
${escapeHtml(proxy.host)}:${proxy.port}
启用后可在账号管理页面上传账号到 CPA 管理平台
CPA 管理平台的 API 地址
CPA 管理平台的认证 Token