mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
feat(proxy): 优化代理配置管理功能
- 更新 proxy_url 属性为 get_proxy_url 方法,支持多级代理优先级 - 实现代理获取三路优先级:动态代理 > 代理池 > 静态代理 - 添加取消默认代理功能和 unset_proxy_default 接口 - 实现批量导入代理功能,支持多种格式解析 - 在前端界面添加批量导入代理按钮和模态框 - 重构代理设置页面的默认代理切换交互 - 更新支付流程中的代理获取方式 - 添加 UUID 依赖并优化支付请求头配置
This commit is contained in:
@@ -638,25 +638,40 @@ class Settings(BaseModel):
|
||||
proxy_dynamic_api_key_header: str = "X-API-Key"
|
||||
proxy_dynamic_result_field: str = ""
|
||||
|
||||
@property
|
||||
def proxy_url(self) -> Optional[str]:
|
||||
"""获取完整的代理 URL"""
|
||||
if not self.proxy_enabled:
|
||||
return None
|
||||
def get_proxy_url(self, db=None) -> Optional[str]:
|
||||
"""获取当前可用的代理 URL(三路优先级)
|
||||
|
||||
if self.proxy_type == "http":
|
||||
scheme = "http"
|
||||
elif self.proxy_type == "socks5":
|
||||
scheme = "socks5"
|
||||
else:
|
||||
return None
|
||||
优先级:动态代理 > 代理池(默认/随机)> 静态代理 > None
|
||||
|
||||
auth = ""
|
||||
if self.proxy_username and self.proxy_password:
|
||||
auth = f"{self.proxy_username}:{self.proxy_password.get_secret_value()}@"
|
||||
Args:
|
||||
db: 可选的数据库 session,传入时检查代理池;不传则跳过代理池
|
||||
"""
|
||||
# 1. 动态代理
|
||||
if self.proxy_dynamic_enabled and self.proxy_dynamic_api_url:
|
||||
return self.proxy_dynamic_api_url
|
||||
|
||||
return f"{scheme}://{auth}{self.proxy_host}:{self.proxy_port}"
|
||||
# 2 & 3. 代理池(优先 is_default,否则随机)
|
||||
if db is not None:
|
||||
from src.database import crud
|
||||
proxy = crud.get_random_proxy(db)
|
||||
if proxy is not None:
|
||||
return proxy.proxy_url
|
||||
|
||||
# 4. 静态代理
|
||||
if self.proxy_enabled:
|
||||
if self.proxy_type == "http":
|
||||
scheme = "http"
|
||||
elif self.proxy_type == "socks5":
|
||||
scheme = "socks5"
|
||||
else:
|
||||
return None
|
||||
auth = ""
|
||||
if self.proxy_username and self.proxy_password:
|
||||
auth = f"{self.proxy_username}:{self.proxy_password.get_secret_value()}@"
|
||||
return f"{scheme}://{auth}{self.proxy_host}:{self.proxy_port}"
|
||||
|
||||
# 5. 无可用代理
|
||||
return None
|
||||
# 注册配置
|
||||
registration_max_retries: int = 3
|
||||
registration_timeout: int = 120
|
||||
|
||||
@@ -115,4 +115,4 @@ def get_proxy_url_for_task() -> Optional[str]:
|
||||
logger.warning("动态代理获取失败,回退到静态代理")
|
||||
|
||||
# 使用静态代理
|
||||
return settings.proxy_url
|
||||
return settings.get_proxy_url()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from curl_cffi import requests as cffi_requests
|
||||
@@ -154,12 +155,8 @@ def generate_team_link(
|
||||
"Authorization": f"Bearer {account.access_token}",
|
||||
"Content-Type": "application/json",
|
||||
"oai-language": "zh-CN",
|
||||
"oai-device-id": str(uuid.uuid4()),
|
||||
}
|
||||
if account.cookies:
|
||||
headers["cookie"] = account.cookies
|
||||
oai_did = _extract_oai_did(account.cookies)
|
||||
if oai_did:
|
||||
headers["oai-device-id"] = oai_did
|
||||
|
||||
payload = {
|
||||
"plan_name": "chatgptteamplan",
|
||||
@@ -171,7 +168,7 @@ def generate_team_link(
|
||||
"billing_details": {"country": country, "currency": currency},
|
||||
"promo_campaign": {
|
||||
"promo_campaign_id": "team-1-month-free",
|
||||
"is_coupon_from_query_param": True,
|
||||
"is_coupon_from_query_param": False,
|
||||
},
|
||||
"cancel_url": "https://chatgpt.com/#pricing",
|
||||
"checkout_ui_mode": "custom",
|
||||
@@ -187,9 +184,35 @@ def generate_team_link(
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if "checkout_session_id" in data:
|
||||
return TEAM_CHECKOUT_BASE_URL + data["checkout_session_id"]
|
||||
raise ValueError(data.get("detail", "API 未返回 checkout_session_id"))
|
||||
resp2 = cffi_requests.post(
|
||||
"https://api.stripe.com/v1/payment_pages/" + data["checkout_session_id"],
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"accept": "application/json",
|
||||
"referer": "https://js.stripe.com/"
|
||||
},
|
||||
data=f"tax_region[country]={country}"
|
||||
"&elements_session_client[client_betas][0]=custom_checkout_server_updates_1"
|
||||
"&elements_session_client[client_betas][1]=custom_checkout_manual_approval_1"
|
||||
"&elements_session_client[elements_init_source]=custom_checkout"
|
||||
"&elements_session_client[referrer_host]=chatgpt.com"
|
||||
"&elements_session_client[session_id]=elements_session_1rr8sS4PKIY"
|
||||
"&elements_session_client[stripe_js_id]=72d6a553-c2fb-4f85-941e-8022c8335a85"
|
||||
"&elements_session_client[locale]=zh"
|
||||
"&elements_session_client[is_aggregation_expected]=false"
|
||||
"&client_attribution_metadata[merchant_integration_additional_elements][0]=payment"
|
||||
"&client_attribution_metadata[merchant_integration_additional_elements][1]=address"
|
||||
f"&key={data["publishable_key"]}"
|
||||
,
|
||||
proxies=_build_proxies(proxy),
|
||||
timeout=30,
|
||||
impersonate="chrome110",
|
||||
)
|
||||
resp2.raise_for_status()
|
||||
data2 = resp2.json()
|
||||
if "stripe_hosted_url" in data2:
|
||||
return data2["stripe_hosted_url"]
|
||||
raise ValueError(data.get("detail", "API 未返回 stripe_hosted_url"))
|
||||
|
||||
|
||||
def open_url_incognito(url: str, cookies_str: Optional[str] = None) -> bool:
|
||||
|
||||
@@ -579,6 +579,16 @@ def set_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]:
|
||||
return proxy
|
||||
|
||||
|
||||
def unset_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]:
|
||||
"""取消指定代理的默认标记"""
|
||||
proxy = db.query(Proxy).filter(Proxy.id == proxy_id).first()
|
||||
if proxy:
|
||||
proxy.is_default = False
|
||||
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))
|
||||
|
||||
@@ -135,7 +135,7 @@ def _get_proxy(request_proxy: Optional[str] = None) -> Optional[str]:
|
||||
proxy_url = get_proxy_url_for_task()
|
||||
if proxy_url:
|
||||
return proxy_url
|
||||
return get_settings().proxy_url
|
||||
return get_settings().get_proxy_url()
|
||||
|
||||
|
||||
# ============== Pydantic Models ==============
|
||||
|
||||
@@ -66,7 +66,7 @@ def generate_payment_link(request: GenerateLinkRequest):
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
proxy = request.proxy or get_settings().proxy_url
|
||||
proxy = request.proxy or get_settings().get_proxy_url(db=db)
|
||||
|
||||
try:
|
||||
if request.plan_type == "plus":
|
||||
@@ -125,11 +125,10 @@ def open_browser_incognito(request: OpenIncognitoRequest):
|
||||
@router.post("/accounts/batch-check-subscription")
|
||||
def batch_check_subscription(request: BatchCheckSubscriptionRequest):
|
||||
"""批量检测账号订阅状态"""
|
||||
proxy = request.proxy or get_settings().proxy_url
|
||||
|
||||
results = {"success_count": 0, "failed_count": 0, "details": []}
|
||||
|
||||
with get_db() as db:
|
||||
proxy = request.proxy or get_settings().get_proxy_url(db=db)
|
||||
ids = resolve_account_ids(
|
||||
db, request.ids, request.select_all,
|
||||
request.status_filter, request.email_service_filter, request.search_filter
|
||||
|
||||
@@ -53,25 +53,31 @@ def get_proxy_for_registration(
|
||||
获取用于注册的代理
|
||||
|
||||
策略:
|
||||
1. 优先从代理列表中随机选择一个启用的代理
|
||||
2. 如果代理列表为空且启用了动态代理,调用动态代理 API 获取
|
||||
3. 否则使用系统设置中的静态默认代理
|
||||
1. 优先使用动态代理(若已启用)
|
||||
2. 动态代理不可用时,使用代理池(有默认代理则走默认,否则随机轮询)
|
||||
3. 代理池为空时,使用系统静态代理配置兜底
|
||||
|
||||
Returns:
|
||||
Tuple[proxy_url, proxy_id]: 代理 URL 和代理 ID(如果来自代理列表)
|
||||
"""
|
||||
# 先尝试从代理列表中获取
|
||||
from ...core.dynamic_proxy import get_proxy_url_for_task
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# 1. 优先动态代理
|
||||
if settings.proxy_dynamic_enabled and settings.proxy_dynamic_api_url:
|
||||
proxy_url = get_proxy_url_for_task()
|
||||
if proxy_url:
|
||||
return proxy_url, None
|
||||
logger.warning("动态代理获取失败,回退到代理池")
|
||||
|
||||
# 2. 代理池(内部已实现:有默认走默认,无默认随机轮询)
|
||||
proxy = crud.get_random_proxy(db, exclude_ids=exclude_proxy_ids)
|
||||
if proxy:
|
||||
return proxy.proxy_url, proxy.id
|
||||
|
||||
# 代理列表为空,尝试动态代理或静态代理
|
||||
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
|
||||
# 3. 静态代理兜底
|
||||
return settings.get_proxy_url(), None
|
||||
|
||||
|
||||
def update_proxy_usage(db, proxy_id: Optional[int]):
|
||||
|
||||
@@ -608,6 +608,94 @@ async def set_proxy_default(proxy_id: int):
|
||||
return {"success": True, "proxy": proxy.to_dict()}
|
||||
|
||||
|
||||
@router.post("/proxies/{proxy_id}/unset-default")
|
||||
async def unset_proxy_default(proxy_id: int):
|
||||
"""取消指定代理的默认标记"""
|
||||
with get_db() as db:
|
||||
proxy = crud.unset_proxy_default(db, proxy_id)
|
||||
if not proxy:
|
||||
raise HTTPException(status_code=404, detail="代理不存在")
|
||||
return {"success": True, "proxy": proxy.to_dict()}
|
||||
|
||||
|
||||
class ProxyBatchImportRequest(BaseModel):
|
||||
"""批量导入代理请求"""
|
||||
lines: str
|
||||
|
||||
|
||||
def _parse_proxy_line(line: str):
|
||||
"""
|
||||
解析单行代理字符串,支持格式:
|
||||
- host:port
|
||||
- type://host:port
|
||||
- type://user:pass@host:port
|
||||
- 名称|type://user:pass@host:port
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
name = None
|
||||
# 解析可选名称前缀(竖线分隔)
|
||||
if '|' in line:
|
||||
name, line = line.split('|', 1)
|
||||
name = name.strip()
|
||||
line = line.strip()
|
||||
|
||||
# 若没有协议头,默认补 http://
|
||||
if '://' not in line:
|
||||
line = 'http://' + line
|
||||
|
||||
parsed = urlparse(line)
|
||||
proxy_type = (parsed.scheme or 'http').lower()
|
||||
if proxy_type not in ('http', 'https', 'socks5', 'socks4'):
|
||||
proxy_type = 'http'
|
||||
|
||||
host = parsed.hostname
|
||||
port = parsed.port
|
||||
username = parsed.username or None
|
||||
password = parsed.password or None
|
||||
|
||||
if not host or not port:
|
||||
raise ValueError(f"无法解析 host/port")
|
||||
|
||||
if not name:
|
||||
name = f"{proxy_type}://{host}:{port}"
|
||||
|
||||
return name, proxy_type, host, port, username, password
|
||||
|
||||
|
||||
@router.post("/proxies/batch-import")
|
||||
async def batch_import_proxies(request: ProxyBatchImportRequest):
|
||||
"""
|
||||
批量导入代理,每行格式支持:
|
||||
- host:port
|
||||
- type://host:port
|
||||
- type://user:pass@host:port
|
||||
- 名称|type://user:pass@host:port
|
||||
"""
|
||||
results = {"success": 0, "failed": 0, "errors": []}
|
||||
with get_db() as db:
|
||||
for raw_line in request.lines.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
name, proxy_type, host, port, username, password = _parse_proxy_line(line)
|
||||
crud.create_proxy(
|
||||
db,
|
||||
name=name,
|
||||
type=proxy_type,
|
||||
host=host,
|
||||
port=port,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
results["success"] += 1
|
||||
except Exception as e:
|
||||
results["failed"] += 1
|
||||
results["errors"].append(f"{raw_line}: {e}")
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/proxies/{proxy_id}/test")
|
||||
async def test_proxy_item(proxy_id: int):
|
||||
"""测试单个代理"""
|
||||
|
||||
@@ -32,6 +32,11 @@ const elements = {
|
||||
addProxyBtn: document.getElementById('add-proxy-btn'),
|
||||
testAllProxiesBtn: document.getElementById('test-all-proxies-btn'),
|
||||
deleteDisabledProxiesBtn: document.getElementById('delete-disabled-proxies-btn'),
|
||||
batchImportProxyBtn: document.getElementById('batch-import-proxy-btn'),
|
||||
batchImportProxyModal: document.getElementById('batch-import-proxy-modal'),
|
||||
closeBatchImportProxyModal: document.getElementById('close-batch-import-proxy-modal'),
|
||||
cancelBatchImportProxyBtn: document.getElementById('cancel-batch-import-proxy-btn'),
|
||||
confirmBatchImportProxyBtn: document.getElementById('confirm-batch-import-proxy-btn'),
|
||||
addProxyModal: document.getElementById('add-proxy-modal'),
|
||||
proxyItemForm: document.getElementById('proxy-item-form'),
|
||||
closeProxyModal: document.getElementById('close-proxy-modal'),
|
||||
@@ -211,6 +216,38 @@ function initEventListeners() {
|
||||
elements.addProxyBtn.addEventListener('click', () => openProxyModal());
|
||||
}
|
||||
|
||||
if (elements.batchImportProxyBtn) {
|
||||
elements.batchImportProxyBtn.addEventListener('click', () => {
|
||||
document.getElementById('batch-import-proxy-data').value = '';
|
||||
document.getElementById('batch-import-proxy-result').innerHTML = '';
|
||||
elements.batchImportProxyModal.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.closeBatchImportProxyModal) {
|
||||
elements.closeBatchImportProxyModal.addEventListener('click', () => {
|
||||
elements.batchImportProxyModal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.cancelBatchImportProxyBtn) {
|
||||
elements.cancelBatchImportProxyBtn.addEventListener('click', () => {
|
||||
elements.batchImportProxyModal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.confirmBatchImportProxyBtn) {
|
||||
elements.confirmBatchImportProxyBtn.addEventListener('click', handleBatchImportProxies);
|
||||
}
|
||||
|
||||
if (elements.batchImportProxyModal) {
|
||||
elements.batchImportProxyModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.batchImportProxyModal) {
|
||||
elements.batchImportProxyModal.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.testAllProxiesBtn) {
|
||||
elements.testAllProxiesBtn.addEventListener('click', handleTestAllProxies);
|
||||
}
|
||||
@@ -833,7 +870,7 @@ function renderProxies(proxies) {
|
||||
<td><code>${escapeHtml(proxy.host)}:${proxy.port}</code></td>
|
||||
<td>
|
||||
${proxy.is_default
|
||||
? '<span class="status-badge active">默认</span>'
|
||||
? `<button class="btn btn-ghost btn-sm" onclick="handleUnsetProxyDefault(${proxy.id})" title="取消默认">取消默认</button>`
|
||||
: `<button class="btn btn-ghost btn-sm" onclick="handleSetProxyDefault(${proxy.id})" title="设为默认">设默认</button>`
|
||||
}
|
||||
</td>
|
||||
@@ -847,7 +884,10 @@ function renderProxies(proxies) {
|
||||
<div class="dropdown-menu" style="min-width:80px;">
|
||||
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);testProxyItem(${proxy.id})">测试</a>
|
||||
<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);toggleProxyItem(${proxy.id}, ${!proxy.enabled})">${proxy.enabled ? '禁用' : '启用'}</a>
|
||||
${!proxy.is_default ? `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleSetProxyDefault(${proxy.id})">设为默认</a>` : ''}
|
||||
${proxy.is_default
|
||||
? `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleUnsetProxyDefault(${proxy.id})">取消默认</a>`
|
||||
: `<a href="#" class="dropdown-item" onclick="event.preventDefault();closeSettingsMoreMenu(this);handleSetProxyDefault(${proxy.id})">设为默认</a>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteProxyItem(${proxy.id})">删除</button>
|
||||
@@ -880,6 +920,36 @@ async function handleSetProxyDefault(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 取消默认代理
|
||||
async function handleUnsetProxyDefault(id) {
|
||||
try {
|
||||
await api.post(`/settings/proxies/${id}/unset-default`);
|
||||
toast.success('已取消默认代理');
|
||||
loadProxies();
|
||||
} catch (error) {
|
||||
toast.error('操作失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入代理
|
||||
async function handleBatchImportProxies() {
|
||||
const lines = document.getElementById('batch-import-proxy-data').value;
|
||||
const resultEl = document.getElementById('batch-import-proxy-result');
|
||||
if (!lines.trim()) {
|
||||
resultEl.innerHTML = '<span style="color:var(--color-warning)">请输入代理数据</span>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await api.post('/settings/proxies/batch-import', { lines });
|
||||
const color = result.failed > 0 ? 'var(--color-warning)' : 'var(--color-success)';
|
||||
resultEl.innerHTML = `<span style="color:${color}">导入成功 ${result.success} 条,失败 ${result.failed} 条。</span>`
|
||||
+ (result.errors.length ? '<br><pre style="font-size:0.8rem;margin-top:4px;">' + result.errors.join('\n') + '</pre>' : '');
|
||||
if (result.success > 0) await loadProxies();
|
||||
} catch (error) {
|
||||
resultEl.innerHTML = `<span style="color:var(--color-danger)">导入失败: ${error.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开代理模态框
|
||||
function openProxyModal(proxy = null) {
|
||||
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
<div style="display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-secondary btn-sm" id="test-all-proxies-btn">🔌 测试全部</button>
|
||||
<button class="btn btn-danger btn-sm" id="delete-disabled-proxies-btn" disabled>🧹 删除禁用项</button>
|
||||
<button class="btn btn-secondary btn-sm" id="batch-import-proxy-btn">📋 批量导入</button>
|
||||
<button class="btn btn-primary btn-sm" id="add-proxy-btn">➕ 添加代理</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,6 +153,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量导入代理模态框 -->
|
||||
<div class="modal" id="batch-import-proxy-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>批量导入代理</h3>
|
||||
<button class="modal-close" id="close-batch-import-proxy-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted" style="margin-bottom: var(--spacing-sm); font-size: 0.875rem;">每行一条,支持格式:<code>host:port</code>、<code>type://host:port</code>、<code>type://user:pass@host:port</code>、<code>名称|type://user:pass@host:port</code></p>
|
||||
<textarea id="batch-import-proxy-data" rows="10" style="width:100%;font-family:monospace;font-size:0.85rem;" placeholder="127.0.0.1:7890
|
||||
socks5://127.0.0.1:1080
|
||||
http://user:pass@host:port
|
||||
MyProxy|socks5://user:pass@host:port"></textarea>
|
||||
<div id="batch-import-proxy-result" style="margin-top: var(--spacing-sm); font-size: 0.875rem;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" id="cancel-batch-import-proxy-btn">取消</button>
|
||||
<button class="btn btn-primary" id="confirm-batch-import-proxy-btn">导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加代理模态框 -->
|
||||
<div class="modal" id="add-proxy-modal">
|
||||
<div class="modal-content">
|
||||
|
||||
Reference in New Issue
Block a user