mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-26 01:31:47 +08:00
feat(proxy): 添加动态代理支持
- 在代理获取逻辑中集成动态代理 API 调用 - 新增动态代理配置界面和 API 接口 - 扩展设置模型以支持动态代理参数 - 更新前端设置页面和 JavaScript 逻辑
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
"""获取注册设置"""
|
||||
|
||||
@@ -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 = '🔌 测试动态代理';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user