diff --git a/src/config/settings.py b/src/config/settings.py index 2d3fb1a..910b417 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -178,6 +178,37 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = { description="代理密码", is_secret=True ), + "proxy_dynamic_enabled": SettingDefinition( + db_key="proxy.dynamic_enabled", + default_value=False, + category=SettingCategory.PROXY, + description="是否启用动态代理" + ), + "proxy_dynamic_api_url": SettingDefinition( + db_key="proxy.dynamic_api_url", + default_value="", + category=SettingCategory.PROXY, + description="动态代理 API 地址,返回代理 URL 字符串" + ), + "proxy_dynamic_api_key": SettingDefinition( + db_key="proxy.dynamic_api_key", + default_value="", + category=SettingCategory.PROXY, + description="动态代理 API 密钥(可选)", + is_secret=True + ), + "proxy_dynamic_api_key_header": SettingDefinition( + db_key="proxy.dynamic_api_key_header", + default_value="X-API-Key", + category=SettingCategory.PROXY, + description="动态代理 API 密钥请求头名称" + ), + "proxy_dynamic_result_field": SettingDefinition( + db_key="proxy.dynamic_result_field", + default_value="", + category=SettingCategory.PROXY, + description="从 JSON 响应中提取代理 URL 的字段路径(留空则使用响应原文)" + ), # 注册配置 "registration_max_retries": SettingDefinition( @@ -335,6 +366,7 @@ SETTING_TYPES: Dict[str, Type] = { "log_retention_days": int, "proxy_enabled": bool, "proxy_port": int, + "proxy_dynamic_enabled": bool, "registration_max_retries": int, "registration_timeout": int, "registration_default_password_length": int, @@ -534,6 +566,11 @@ class Settings(BaseModel): proxy_port: int = 7890 proxy_username: Optional[str] = None proxy_password: Optional[SecretStr] = None + proxy_dynamic_enabled: bool = False + proxy_dynamic_api_url: str = "" + proxy_dynamic_api_key: Optional[SecretStr] = None + proxy_dynamic_api_key_header: str = "X-API-Key" + proxy_dynamic_result_field: str = "" @property def proxy_url(self) -> Optional[str]: diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index 5ba459e..ac015a7 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -37,7 +37,8 @@ def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]: 策略: 1. 优先从代理列表中随机选择一个启用的代理 - 2. 如果代理列表为空,使用系统设置中的默认代理 + 2. 如果代理列表为空且启用了动态代理,调用动态代理 API 获取 + 3. 否则使用系统设置中的静态默认代理 Returns: Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表) @@ -47,10 +48,11 @@ def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]: if proxy: return proxy.proxy_url, proxy.id - # 代理列表为空,使用系统设置中的默认代理 - settings = get_settings() - if settings.proxy_enabled and settings.proxy_url: - return settings.proxy_url, None + # 代理列表为空,尝试动态代理或静态代理 + from ...core.dynamic_proxy import get_proxy_url_for_task + proxy_url = get_proxy_url_for_task() + if proxy_url: + return proxy_url, None return None, None diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index c5fc99f..077ae17 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -79,6 +79,11 @@ async def get_all_settings(): "port": settings.proxy_port, "username": settings.proxy_username, "has_password": bool(settings.proxy_password), + "dynamic_enabled": settings.proxy_dynamic_enabled, + "dynamic_api_url": settings.proxy_dynamic_api_url, + "dynamic_api_key_header": settings.proxy_dynamic_api_key_header, + "dynamic_result_field": settings.proxy_dynamic_result_field, + "has_dynamic_api_key": bool(settings.proxy_dynamic_api_key and settings.proxy_dynamic_api_key.get_secret_value()), }, "registration": { "max_retries": settings.registration_max_retries, @@ -199,6 +204,91 @@ async def test_proxy_settings(request: ProxySettings): } +@router.get("/proxy/dynamic") +async def get_dynamic_proxy_settings(): + """获取动态代理设置""" + settings = get_settings() + return { + "enabled": settings.proxy_dynamic_enabled, + "api_url": settings.proxy_dynamic_api_url, + "api_key_header": settings.proxy_dynamic_api_key_header, + "result_field": settings.proxy_dynamic_result_field, + "has_api_key": bool(settings.proxy_dynamic_api_key and settings.proxy_dynamic_api_key.get_secret_value()), + } + + +class DynamicProxySettings(BaseModel): + """动态代理设置""" + enabled: bool = False + api_url: str = "" + api_key: Optional[str] = None + api_key_header: str = "X-API-Key" + result_field: str = "" + + +@router.post("/proxy/dynamic") +async def update_dynamic_proxy_settings(request: DynamicProxySettings): + """更新动态代理设置""" + update_dict = { + "proxy_dynamic_enabled": request.enabled, + "proxy_dynamic_api_url": request.api_url, + "proxy_dynamic_api_key_header": request.api_key_header, + "proxy_dynamic_result_field": request.result_field, + } + if request.api_key is not None: + update_dict["proxy_dynamic_api_key"] = request.api_key + + update_settings(**update_dict) + return {"success": True, "message": "动态代理设置已更新"} + + +@router.post("/proxy/dynamic/test") +async def test_dynamic_proxy(request: DynamicProxySettings): + """测试动态代理 API""" + from ...core.dynamic_proxy import fetch_dynamic_proxy + + if not request.api_url: + raise HTTPException(status_code=400, detail="请填写动态代理 API 地址") + + # 若未传入 api_key,使用已保存的 + api_key = request.api_key or "" + if not api_key: + settings = get_settings() + if settings.proxy_dynamic_api_key: + api_key = settings.proxy_dynamic_api_key.get_secret_value() + + proxy_url = fetch_dynamic_proxy( + api_url=request.api_url, + api_key=api_key, + api_key_header=request.api_key_header, + result_field=request.result_field, + ) + + if not proxy_url: + return {"success": False, "message": "动态代理 API 返回为空或请求失败"} + + # 用获取到的代理测试连通性 + import time + from curl_cffi import requests as cffi_requests + try: + proxies = {"http": proxy_url, "https": proxy_url} + start = time.time() + resp = cffi_requests.get( + "https://api.ipify.org?format=json", + proxies=proxies, + timeout=10, + impersonate="chrome110" + ) + elapsed = round((time.time() - start) * 1000) + if resp.status_code == 200: + ip = resp.json().get("ip", "") + return {"success": True, "proxy_url": proxy_url, "ip": ip, "response_time": elapsed, + "message": f"动态代理可用,出口 IP: {ip},响应时间: {elapsed}ms"} + return {"success": False, "proxy_url": proxy_url, "message": f"代理连接失败: HTTP {resp.status_code}"} + except Exception as e: + return {"success": False, "proxy_url": proxy_url, "message": f"代理连接失败: {e}"} + + @router.get("/registration") async def get_registration_settings(): """获取注册设置""" diff --git a/static/js/settings.js b/static/js/settings.js index 648f798..bf7e042 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -38,6 +38,9 @@ const elements = { closeProxyModal: document.getElementById('close-proxy-modal'), cancelProxyBtn: document.getElementById('cancel-proxy-btn'), proxyModalTitle: document.getElementById('proxy-modal-title'), + // 动态代理设置 + 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'), @@ -202,6 +205,14 @@ function initEventListeners() { elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem); } + // 动态代理设置 + if (elements.dynamicProxyForm) { + elements.dynamicProxyForm.addEventListener('submit', handleSaveDynamicProxy); + } + if (elements.testDynamicProxyBtn) { + elements.testDynamicProxyBtn.addEventListener('click', handleTestDynamicProxy); + } + // CPA 设置 if (elements.cpaForm) { elements.cpaForm.addEventListener('submit', handleSaveCpa); @@ -234,6 +245,12 @@ async function loadSettings() { 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 || ''; + document.getElementById('dynamic-proxy-api-key-header').value = data.proxy?.dynamic_api_key_header || 'X-API-Key'; + document.getElementById('dynamic-proxy-result-field').value = data.proxy?.dynamic_result_field || ''; + // 注册配置 document.getElementById('max-retries').value = data.registration?.max_retries || 3; document.getElementById('timeout').value = data.registration?.timeout || 120; @@ -1000,3 +1017,52 @@ async function handleTestCpa() { elements.testCpaBtn.textContent = '🔌 测试连接'; } } + +// ============== 动态代理设置 ============== + +async function handleSaveDynamicProxy(e) { + e.preventDefault(); + const data = { + enabled: document.getElementById('dynamic-proxy-enabled').checked, + api_url: document.getElementById('dynamic-proxy-api-url').value.trim(), + api_key: document.getElementById('dynamic-proxy-api-key').value || null, + api_key_header: document.getElementById('dynamic-proxy-api-key-header').value.trim() || 'X-API-Key', + result_field: document.getElementById('dynamic-proxy-result-field').value.trim() + }; + try { + await api.post('/settings/proxy/dynamic', data); + toast.success('动态代理设置已保存'); + document.getElementById('dynamic-proxy-api-key').value = ''; + } catch (error) { + toast.error('保存失败: ' + error.message); + } +} + +async function handleTestDynamicProxy() { + const apiUrl = document.getElementById('dynamic-proxy-api-url').value.trim(); + if (!apiUrl) { + toast.warning('请先填写动态代理 API 地址'); + return; + } + const btn = elements.testDynamicProxyBtn; + btn.disabled = true; + btn.textContent = '测试中...'; + try { + const result = await api.post('/settings/proxy/dynamic/test', { + api_url: apiUrl, + api_key: document.getElementById('dynamic-proxy-api-key').value || null, + api_key_header: document.getElementById('dynamic-proxy-api-key-header').value.trim() || 'X-API-Key', + result_field: document.getElementById('dynamic-proxy-result-field').value.trim() + }); + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + } catch (error) { + toast.error('测试失败: ' + error.message); + } finally { + btn.disabled = false; + btn.textContent = '🔌 测试动态代理'; + } +} diff --git a/templates/settings.html b/templates/settings.html index eec2a26..1c2bdac 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -99,6 +99,48 @@ + +
+
+

动态代理配置

+ 通过 API 每次获取新代理 IP,优先级高于静态代理和代理列表 +
+
+
+
+ +
+
+ + + 每次注册任务启动时调用此 API 获取代理 URL +
+
+
+ + +
+
+ + +
+
+
+ + + 若 API 返回 JSON,填写点号分隔的字段路径提取代理 URL;留空则将响应原文作为代理 URL +
+
+ + +
+
+
+
+