feat(proxy): 添加动态代理支持

- 在代理获取逻辑中集成动态代理 API 调用
- 新增动态代理配置界面和 API 接口
- 扩展设置模型以支持动态代理参数
- 更新前端设置页面和 JavaScript 逻辑
This commit is contained in:
cnlimiter
2026-03-16 02:06:21 +08:00
parent 9dbb6e4e26
commit 97a8c01b9f
5 changed files with 242 additions and 5 deletions

View File

@@ -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]:

View File

@@ -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

View File

@@ -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():
"""获取注册设置"""

View File

@@ -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 = '🔌 测试动态代理';
}
}

View File

@@ -99,6 +99,48 @@
</div>
</div>
<!-- 动态代理配置 -->
<div class="card" style="margin-top: var(--spacing-lg);">
<div class="card-header">
<h3>动态代理配置</h3>
<span class="hint">通过 API 每次获取新代理 IP优先级高于静态代理和代理列表</span>
</div>
<div class="card-body">
<form id="dynamic-proxy-form">
<div class="form-group">
<label>
<input type="checkbox" id="dynamic-proxy-enabled" name="enabled">
启用动态代理
</label>
</div>
<div class="form-group">
<label for="dynamic-proxy-api-url">代理 API 地址</label>
<input type="text" id="dynamic-proxy-api-url" name="api_url" placeholder="http://api.example.com/get_proxy">
<small style="color: var(--text-muted);">每次注册任务启动时调用此 API 获取代理 URL</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="dynamic-proxy-api-key">API 密钥 (可选)</label>
<input type="password" id="dynamic-proxy-api-key" name="api_key" placeholder="留空保持不变" autocomplete="new-password">
</div>
<div class="form-group">
<label for="dynamic-proxy-api-key-header">密钥请求头</label>
<input type="text" id="dynamic-proxy-api-key-header" name="api_key_header" value="X-API-Key">
</div>
</div>
<div class="form-group">
<label for="dynamic-proxy-result-field">JSON 字段路径 (可选)</label>
<input type="text" id="dynamic-proxy-result-field" name="result_field" placeholder="例如: data.proxy 或留空使用响应原文">
<small style="color: var(--text-muted);">若 API 返回 JSON填写点号分隔的字段路径提取代理 URL留空则将响应原文作为代理 URL</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 保存设置</button>
<button type="button" class="btn btn-secondary" id="test-dynamic-proxy-btn">🔌 测试动态代理</button>
</div>
</form>
</div>
</div>
<!-- 代理列表 -->
<div class="card" style="margin-top: var(--spacing-lg);">
<div class="card-header">