mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-09-06 07:56:39 +08:00
feat(proxy): 添加动态代理支持
- 在代理获取逻辑中集成动态代理 API 调用 - 新增动态代理配置界面和 API 接口 - 扩展设置模型以支持动态代理参数 - 更新前端设置页面和 JavaScript 逻辑
This commit is contained in:
@@ -178,6 +178,37 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
|
|||||||
description="代理密码",
|
description="代理密码",
|
||||||
is_secret=True
|
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(
|
"registration_max_retries": SettingDefinition(
|
||||||
@@ -335,6 +366,7 @@ SETTING_TYPES: Dict[str, Type] = {
|
|||||||
"log_retention_days": int,
|
"log_retention_days": int,
|
||||||
"proxy_enabled": bool,
|
"proxy_enabled": bool,
|
||||||
"proxy_port": int,
|
"proxy_port": int,
|
||||||
|
"proxy_dynamic_enabled": bool,
|
||||||
"registration_max_retries": int,
|
"registration_max_retries": int,
|
||||||
"registration_timeout": int,
|
"registration_timeout": int,
|
||||||
"registration_default_password_length": int,
|
"registration_default_password_length": int,
|
||||||
@@ -534,6 +566,11 @@ class Settings(BaseModel):
|
|||||||
proxy_port: int = 7890
|
proxy_port: int = 7890
|
||||||
proxy_username: Optional[str] = None
|
proxy_username: Optional[str] = None
|
||||||
proxy_password: Optional[SecretStr] = 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
|
@property
|
||||||
def proxy_url(self) -> Optional[str]:
|
def proxy_url(self) -> Optional[str]:
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]:
|
|||||||
|
|
||||||
策略:
|
策略:
|
||||||
1. 优先从代理列表中随机选择一个启用的代理
|
1. 优先从代理列表中随机选择一个启用的代理
|
||||||
2. 如果代理列表为空,使用系统设置中的默认代理
|
2. 如果代理列表为空且启用了动态代理,调用动态代理 API 获取
|
||||||
|
3. 否则使用系统设置中的静态默认代理
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
||||||
@@ -47,10 +48,11 @@ def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]:
|
|||||||
if proxy:
|
if proxy:
|
||||||
return proxy.proxy_url, proxy.id
|
return proxy.proxy_url, proxy.id
|
||||||
|
|
||||||
# 代理列表为空,使用系统设置中的默认代理
|
# 代理列表为空,尝试动态代理或静态代理
|
||||||
settings = get_settings()
|
from ...core.dynamic_proxy import get_proxy_url_for_task
|
||||||
if settings.proxy_enabled and settings.proxy_url:
|
proxy_url = get_proxy_url_for_task()
|
||||||
return settings.proxy_url, None
|
if proxy_url:
|
||||||
|
return proxy_url, None
|
||||||
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ async def get_all_settings():
|
|||||||
"port": settings.proxy_port,
|
"port": settings.proxy_port,
|
||||||
"username": settings.proxy_username,
|
"username": settings.proxy_username,
|
||||||
"has_password": bool(settings.proxy_password),
|
"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": {
|
"registration": {
|
||||||
"max_retries": settings.registration_max_retries,
|
"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")
|
@router.get("/registration")
|
||||||
async def get_registration_settings():
|
async def get_registration_settings():
|
||||||
"""获取注册设置"""
|
"""获取注册设置"""
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ const elements = {
|
|||||||
closeProxyModal: document.getElementById('close-proxy-modal'),
|
closeProxyModal: document.getElementById('close-proxy-modal'),
|
||||||
cancelProxyBtn: document.getElementById('cancel-proxy-btn'),
|
cancelProxyBtn: document.getElementById('cancel-proxy-btn'),
|
||||||
proxyModalTitle: document.getElementById('proxy-modal-title'),
|
proxyModalTitle: document.getElementById('proxy-modal-title'),
|
||||||
|
// 动态代理设置
|
||||||
|
dynamicProxyForm: document.getElementById('dynamic-proxy-form'),
|
||||||
|
testDynamicProxyBtn: document.getElementById('test-dynamic-proxy-btn'),
|
||||||
// CPA 设置
|
// CPA 设置
|
||||||
cpaForm: document.getElementById('cpa-form'),
|
cpaForm: document.getElementById('cpa-form'),
|
||||||
testCpaBtn: document.getElementById('test-cpa-btn'),
|
testCpaBtn: document.getElementById('test-cpa-btn'),
|
||||||
@@ -202,6 +205,14 @@ function initEventListeners() {
|
|||||||
elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem);
|
elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 动态代理设置
|
||||||
|
if (elements.dynamicProxyForm) {
|
||||||
|
elements.dynamicProxyForm.addEventListener('submit', handleSaveDynamicProxy);
|
||||||
|
}
|
||||||
|
if (elements.testDynamicProxyBtn) {
|
||||||
|
elements.testDynamicProxyBtn.addEventListener('click', handleTestDynamicProxy);
|
||||||
|
}
|
||||||
|
|
||||||
// CPA 设置
|
// CPA 设置
|
||||||
if (elements.cpaForm) {
|
if (elements.cpaForm) {
|
||||||
elements.cpaForm.addEventListener('submit', handleSaveCpa);
|
elements.cpaForm.addEventListener('submit', handleSaveCpa);
|
||||||
@@ -234,6 +245,12 @@ async function loadSettings() {
|
|||||||
document.getElementById('proxy-port').value = data.proxy?.port || 7890;
|
document.getElementById('proxy-port').value = data.proxy?.port || 7890;
|
||||||
document.getElementById('proxy-username').value = data.proxy?.username || '';
|
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('max-retries').value = data.registration?.max_retries || 3;
|
||||||
document.getElementById('timeout').value = data.registration?.timeout || 120;
|
document.getElementById('timeout').value = data.registration?.timeout || 120;
|
||||||
@@ -1000,3 +1017,52 @@ async function handleTestCpa() {
|
|||||||
elements.testCpaBtn.textContent = '🔌 测试连接';
|
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 = '🔌 测试动态代理';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</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" style="margin-top: var(--spacing-lg);">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
Reference in New Issue
Block a user