feat(config): 采用列表模式

This commit is contained in:
cnlimiter
2026-03-18 14:42:10 +08:00
parent 931ea798cc
commit 23336e26b3
8 changed files with 67 additions and 436 deletions

View File

@@ -484,14 +484,31 @@ def update_proxy_last_used(db: Session, proxy_id: int) -> bool:
def get_random_proxy(db: Session) -> Optional[Proxy]:
"""随机获取一个启用的代理"""
"""随机获取一个启用的代理,优先返回 is_default=True 的代理"""
import random
# 优先返回默认代理
default_proxy = db.query(Proxy).filter(Proxy.enabled == True, Proxy.is_default == True).first()
if default_proxy:
return default_proxy
proxies = get_enabled_proxies(db)
if not proxies:
return None
return random.choice(proxies)
def set_proxy_default(db: Session, proxy_id: int) -> Optional[Proxy]:
"""将指定代理设为默认,同时清除其他代理的默认标记"""
# 清除所有默认标记
db.query(Proxy).filter(Proxy.is_default == True).update({"is_default": False})
# 设置新的默认代理
proxy = db.query(Proxy).filter(Proxy.id == proxy_id).first()
if proxy:
proxy.is_default = True
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))

View File

@@ -156,6 +156,7 @@ class Proxy(Base):
username = Column(String(100))
password = Column(String(255))
enabled = Column(Boolean, default=True)
is_default = Column(Boolean, default=False) # 是否为默认代理
priority = Column(Integer, default=0) # 优先级(保留字段)
last_used = Column(DateTime) # 最后使用时间
created_at = Column(DateTime, default=datetime.utcnow)
@@ -171,6 +172,7 @@ class Proxy(Base):
'port': self.port,
'username': self.username,
'enabled': self.enabled,
'is_default': self.is_default or False,
'priority': self.priority,
'last_used': self.last_used.isoformat() if self.last_used else None,
'created_at': self.created_at.isoformat() if self.created_at else None,

View File

@@ -110,6 +110,7 @@ class DatabaseSessionManager:
("accounts", "subscription_type", "VARCHAR(20)"),
("accounts", "subscription_at", "DATETIME"),
("accounts", "cookies", "TEXT"),
("proxies", "is_default", "BOOLEAN DEFAULT 0"),
]
# 确保新表存在create_tables 已处理,此处兜底)

View File

@@ -347,18 +347,22 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
saved_account = db.query(AccountModel).filter_by(email=result.email).first()
if saved_account and saved_account.access_token:
token_data = generate_token_json(saved_account)
# 解析指定 CPA 服务
# 解析指定 CPA 服务,未指定则取第一个启用的服务
_cpa_api_url = None
_cpa_api_token = None
_svc = None
if cpa_service_id:
try:
_svc = crud.get_cpa_service_by_id(db, cpa_service_id)
if _svc:
_cpa_api_url = _svc.api_url
_cpa_api_token = _svc.api_token
log_callback(f"[CPA] 使用服务: {_svc.name}")
except Exception:
pass
if _svc is None:
svcs = crud.get_cpa_services(db, enabled=True)
_svc = svcs[0] if svcs else None
if _svc:
_cpa_api_url = _svc.api_url
_cpa_api_token = _svc.api_token
log_callback(f"[CPA] 使用服务: {_svc.name}")
cpa_success, cpa_msg = upload_to_cpa(token_data, api_url=_cpa_api_url, api_token=_cpa_api_token)
if cpa_success:
saved_account.cpa_uploaded = True

View File

@@ -111,101 +111,6 @@ async def get_all_settings():
}
@router.get("/proxy")
async def get_proxy_settings():
"""获取代理设置"""
settings = get_settings()
return {
"enabled": settings.proxy_enabled,
"type": settings.proxy_type,
"host": settings.proxy_host,
"port": settings.proxy_port,
"username": settings.proxy_username,
"has_password": bool(settings.proxy_password),
"proxy_url": settings.proxy_url,
}
@router.post("/proxy")
async def update_proxy_settings(request: ProxySettings):
"""更新代理设置"""
update_dict = {
"proxy_enabled": request.enabled,
"proxy_type": request.type,
"proxy_host": request.host,
"proxy_port": request.port,
"proxy_username": request.username,
}
if request.password:
update_dict["proxy_password"] = request.password
update_settings(**update_dict)
return {"success": True, "message": "代理设置已更新"}
@router.post("/proxy/test")
async def test_proxy_settings(request: ProxySettings):
"""测试代理连接"""
import time
from curl_cffi import requests as cffi_requests
# 构建代理 URL
if request.type == "http":
scheme = "http"
elif request.type == "socks5":
scheme = "socks5"
else:
raise HTTPException(status_code=400, detail="不支持的代理类型")
auth = ""
if request.username and request.password:
auth = f"{request.username}:{request.password}@"
proxy_url = f"{scheme}://{auth}{request.host}:{request.port}"
# 测试连接
test_url = "https://api.ipify.org?format=json"
start_time = time.time()
try:
proxies = {
"http": proxy_url,
"https": proxy_url
}
response = cffi_requests.get(
test_url,
proxies=proxies,
timeout=3,
impersonate="chrome110"
)
elapsed_time = time.time() - start_time
if response.status_code == 200:
ip_info = response.json()
return {
"success": True,
"ip": ip_info.get("ip", ""),
"response_time": round(elapsed_time * 1000), # 毫秒
"message": f"代理连接成功,出口 IP: {ip_info.get('ip', 'unknown')}"
}
else:
return {
"success": False,
"message": f"代理返回错误状态码: {response.status_code}"
}
except Exception as e:
return {
"success": False,
"message": f"代理连接失败: {str(e)}"
}
@router.get("/proxy/dynamic")
async def get_dynamic_proxy_settings():
"""获取动态代理设置"""
@@ -639,6 +544,16 @@ async def delete_proxy_item(proxy_id: int):
return {"success": True, "message": "代理已删除"}
@router.post("/proxies/{proxy_id}/set-default")
async def set_proxy_default(proxy_id: int):
"""将指定代理设为默认"""
with get_db() as db:
proxy = crud.set_proxy_default(db, proxy_id)
if not proxy:
raise HTTPException(status_code=404, detail="代理不存在")
return {"success": True, "proxy": proxy.to_dict()}
@router.post("/proxies/{proxy_id}/test")
async def test_proxy_item(proxy_id: int):
"""测试单个代理"""
@@ -774,77 +689,6 @@ async def disable_proxy(proxy_id: int):
return {"success": True, "message": "代理已禁用"}
# ============== CPA 设置 ==============
class CPASettings(BaseModel):
"""CPA 设置"""
enabled: bool = False
api_url: str = ""
api_token: str = ""
class CPATestRequest(BaseModel):
"""CPA 测试请求"""
api_url: str
api_token: str
@router.get("/cpa")
async def get_cpa_settings():
"""获取 CPA 设置"""
settings = get_settings()
return {
"enabled": settings.cpa_enabled,
"api_url": settings.cpa_api_url,
"has_token": bool(settings.cpa_api_token and settings.cpa_api_token.get_secret_value()),
}
@router.post("/cpa")
async def update_cpa_settings(request: CPASettings):
"""更新 CPA 设置"""
update_dict = {
"cpa_enabled": request.enabled,
"cpa_api_url": request.api_url,
}
# 只有提供了 token 才更新
if request.api_token:
update_dict["cpa_api_token"] = request.api_token
update_settings(**update_dict)
return {"success": True, "message": "CPA 设置已更新"}
@router.post("/cpa/test")
async def test_cpa_connection(request: CPATestRequest):
"""测试 CPA 连接"""
from ...core.cpa_upload import test_cpa_connection as do_test
settings = get_settings()
proxy = settings.proxy_url
# 如果传入 'use_saved_token',使用已保存的 token
api_token = request.api_token
if api_token == 'use_saved_token' or not api_token:
if settings.cpa_api_token:
api_token = settings.cpa_api_token.get_secret_value()
else:
return {
"success": False,
"message": "未配置 API Token"
}
success, message = do_test(request.api_url, api_token, proxy)
return {
"success": success,
"message": message
}
# ============== Outlook 设置 ==============
class OutlookSettings(BaseModel):

View File

@@ -102,19 +102,19 @@ document.addEventListener('DOMContentLoaded', () => {
// 检查 CPA 是否启用,未启用则禁用复选框;同时加载 CPA 服务列表
async function checkCpaEnabled() {
if (!elements.autoUploadCpa) return;
// 加载 CPA 服务列表,列表为空则禁用复选框
await loadCpaServiceOptions();
try {
const data = await api.get('/settings/cpa');
if (!data.enabled) {
const services = await api.get('/cpa-services?enabled=true');
if (!services || services.length === 0) {
elements.autoUploadCpa.disabled = true;
elements.autoUploadCpa.title = '请先在设置中启用 CPA 上传';
elements.autoUploadCpa.title = '请先在设置中添加 CPA 服务';
const label = elements.autoUploadCpa.closest('label');
if (label) label.style.opacity = '0.5';
}
} catch (e) {
elements.autoUploadCpa.disabled = true;
}
// 加载 CPA 服务列表
await loadCpaServiceOptions();
// 复选框联动显示/隐藏服务选择器
if (elements.autoUploadCpa) {
elements.autoUploadCpa.addEventListener('change', () => {
@@ -130,8 +130,7 @@ async function loadCpaServiceOptions() {
if (!elements.cpaServiceSelect) return;
try {
const services = await api.get('/cpa-services?enabled=true');
// 保留「使用全局配置」选项
const defaultOpt = '<option value="">使用全局配置</option>';
const defaultOpt = '<option value="">自动选择(第一个启用的服务)</option>';
const opts = services.map(s =>
`<option value="${s.id}">${s.name.replace(/</g,'&lt;')}</option>`
).join('');

View File

@@ -7,9 +7,7 @@
const elements = {
tabs: document.querySelectorAll('.tab-btn'),
tabContents: document.querySelectorAll('.tab-content'),
proxyForm: document.getElementById('proxy-form'),
registrationForm: document.getElementById('registration-settings-form'),
testProxyBtn: document.getElementById('test-proxy-btn'),
backupBtn: document.getElementById('backup-btn'),
cleanupBtn: document.getElementById('cleanup-btn'),
addEmailServiceBtn: document.getElementById('add-email-service-btn'),
@@ -41,9 +39,6 @@ const elements = {
// 动态代理设置
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'),
// CPA 服务管理
addCpaServiceBtn: document.getElementById('add-cpa-service-btn'),
cpaServicesTable: document.getElementById('cpa-services-table'),
@@ -95,16 +90,6 @@ function initTabs() {
// 事件监听
function initEventListeners() {
// 代理表单
if (elements.proxyForm) {
elements.proxyForm.addEventListener('submit', handleSaveProxy);
}
// 测试代理
if (elements.testProxyBtn) {
elements.testProxyBtn.addEventListener('click', handleTestProxy);
}
// 注册配置表单
if (elements.registrationForm) {
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
@@ -228,15 +213,6 @@ function initEventListeners() {
elements.testDynamicProxyBtn.addEventListener('click', handleTestDynamicProxy);
}
// CPA 设置
if (elements.cpaForm) {
elements.cpaForm.addEventListener('submit', handleSaveCpa);
}
if (elements.testCpaBtn) {
elements.testCpaBtn.addEventListener('click', handleTestCpa);
}
// 验证码设置
if (elements.emailCodeForm) {
elements.emailCodeForm.addEventListener('submit', handleSaveEmailCode);
@@ -286,13 +262,6 @@ async function loadSettings() {
try {
const data = await api.get('/settings');
// 代理设置
document.getElementById('proxy-enabled').checked = data.proxy?.enabled || false;
document.getElementById('proxy-type').value = data.proxy?.type || 'http';
document.getElementById('proxy-host').value = data.proxy?.host || '127.0.0.1';
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 || '';
@@ -312,8 +281,6 @@ async function loadSettings() {
document.getElementById('email-code-poll-interval').value = data.email_code.poll_interval || 3;
}
// 加载 CPA 设置
loadCpaSettings();
// 加载 Outlook 设置
loadOutlookSettings();
// 加载 Team Manager 设置
@@ -445,57 +412,6 @@ async function loadDatabaseInfo() {
}
}
// 保存代理设置
async function handleSaveProxy(e) {
e.preventDefault();
const data = {
enabled: document.getElementById('proxy-enabled').checked,
type: document.getElementById('proxy-type').value,
host: document.getElementById('proxy-host').value,
port: parseInt(document.getElementById('proxy-port').value),
username: document.getElementById('proxy-username').value || null,
password: document.getElementById('proxy-password').value || null,
};
try {
await api.post('/settings/proxy', data);
toast.success('代理设置已保存');
} catch (error) {
toast.error('保存失败: ' + error.message);
}
}
// 测试代理
async function handleTestProxy() {
elements.testProxyBtn.disabled = true;
elements.testProxyBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
const data = {
enabled: document.getElementById('proxy-enabled').checked,
type: document.getElementById('proxy-type').value,
host: document.getElementById('proxy-host').value,
port: parseInt(document.getElementById('proxy-port').value),
username: document.getElementById('proxy-username').value || null,
password: document.getElementById('proxy-password').value || null,
};
const result = await api.post('/settings/proxy/test', data);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
} catch (error) {
toast.error('测试失败: ' + error.message);
} finally {
elements.testProxyBtn.disabled = false;
elements.testProxyBtn.textContent = '🔌 测试连接';
}
}
// 保存注册配置
async function handleSaveRegistration(e) {
e.preventDefault();
@@ -838,6 +754,12 @@ function renderProxies(proxies) {
<td>${escapeHtml(proxy.name)}</td>
<td><span class="badge">${proxy.type.toUpperCase()}</span></td>
<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="handleSetProxyDefault(${proxy.id})" title="设为默认">设默认</button>`
}
</td>
<td>
<span class="status-badge ${proxy.enabled ? 'active' : 'disabled'}">
${proxy.enabled ? '已启用' : '已禁用'}
@@ -864,6 +786,17 @@ function renderProxies(proxies) {
`).join('');
}
// 设为默认代理
async function handleSetProxyDefault(id) {
try {
await api.post(`/settings/proxies/${id}/set-default`);
toast.success('已设为默认代理');
loadProxies();
} catch (error) {
toast.error('操作失败: ' + error.message);
}
}
// 打开代理模态框
function openProxyModal(proxy = null) {
elements.proxyModalTitle.textContent = proxy ? '编辑代理' : '添加代理';
@@ -987,45 +920,6 @@ async function handleTestAllProxies() {
}
// ============================================================================
// CPA 设置管理
// ============================================================================
// 加载 CPA 设置
async function loadCpaSettings() {
try {
const data = await api.get('/settings/cpa');
document.getElementById('cpa-enabled').checked = data.enabled || false;
document.getElementById('cpa-api-url').value = data.api_url || '';
// 不填充 token只显示是否有值
document.getElementById('cpa-api-token').value = '';
document.getElementById('cpa-api-token').placeholder = data.has_token ? '已配置,留空保持不变' : '请输入 API Token';
} catch (error) {
console.error('加载 CPA 设置失败:', error);
}
}
// 保存 CPA 设置
async function handleSaveCpa(e) {
e.preventDefault();
const data = {
enabled: document.getElementById('cpa-enabled').checked,
api_url: document.getElementById('cpa-api-url').value,
api_token: document.getElementById('cpa-api-token').value || ''
};
try {
await api.post('/settings/cpa', data);
toast.success('CPA 设置已保存');
loadCpaSettings();
} catch (error) {
toast.error('保存失败: ' + error.message);
}
}
// ============================================================================
// Outlook 设置管理
// ============================================================================
@@ -1055,47 +949,6 @@ async function handleSaveOutlookSettings(e) {
}
}
// 测试 CPA 连接
async function handleTestCpa() {
const apiUrl = document.getElementById('cpa-api-url').value;
const apiToken = document.getElementById('cpa-api-token').value;
if (!apiUrl) {
toast.warning('请输入 API URL');
return;
}
// 如果 token 为空,尝试使用已保存的 token 进行测试
if (!apiToken) {
const cpaSettings = await api.get('/settings/cpa');
if (!cpaSettings.has_token) {
toast.warning('请输入 API Token');
return;
}
}
elements.testCpaBtn.disabled = true;
elements.testCpaBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
const result = await api.post('/settings/cpa/test', {
api_url: apiUrl,
api_token: apiToken || 'use_saved_token'
});
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
} catch (error) {
toast.error('测试失败: ' + error.message);
} finally {
elements.testCpaBtn.disabled = false;
elements.testCpaBtn.textContent = '🔌 测试连接';
}
}
// ============== 动态代理设置 ==============
async function handleSaveDynamicProxy(e) {

View File

@@ -48,66 +48,11 @@
<!-- 代理设置 -->
<div class="tab-content active" id="proxy-tab">
<!-- 默认代理配置 -->
<div class="card">
<div class="card-header">
<h3>默认代理配置</h3>
<span class="hint">当代理列表为空时使用此配置</span>
</div>
<div class="card-body">
<form id="proxy-form">
<div class="form-group">
<label>
<input type="checkbox" id="proxy-enabled" name="enabled">
启用代理
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="proxy-type">代理类型</label>
<select id="proxy-type" name="type">
<option value="http">HTTP</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
<div class="form-group">
<label for="proxy-host">主机地址</label>
<input type="text" id="proxy-host" name="host" value="127.0.0.1">
</div>
<div class="form-group">
<label for="proxy-port">端口</label>
<input type="number" id="proxy-port" name="port" value="7890">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="proxy-username">用户名 (可选)</label>
<input type="text" id="proxy-username" name="username" autocomplete="off">
</div>
<div class="form-group">
<label for="proxy-password">密码 (可选)</label>
<input type="password" id="proxy-password" name="password" autocomplete="new-password">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 保存设置</button>
<button type="button" class="btn btn-secondary" id="test-proxy-btn">🔌 测试连接</button>
</div>
</form>
</div>
</div>
<!-- 动态代理配置 -->
<div class="card" style="margin-top: var(--spacing-lg);">
<div class="card-header">
<h3>动态代理配置</h3>
<span class="hint">通过 API 每次获取新代理 IP优先级高于静态代理和代理列表</span>
<span class="hint">通过 API 每次获取新代理 IP优先级高于代理列表</span>
</div>
<div class="card-body">
<form id="dynamic-proxy-form">
@@ -163,9 +108,10 @@
<th>名称</th>
<th>类型</th>
<th>地址</th>
<th style="width: 60px;">默认</th>
<th style="width: 80px;">状态</th>
<th style="width: 120px;">最后使用</th>
<th style="width: 150px;">操作</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody id="proxies-table">
@@ -258,43 +204,8 @@
<!-- CPA 上传设置 -->
<div class="tab-content" id="cpa-tab">
<div class="card">
<div class="card-header">
<h3>CPA 上传配置</h3>
<span class="hint">配置 CliProxyApi 上传功能</span>
</div>
<div class="card-body">
<form id="cpa-form">
<div class="form-group">
<label>
<input type="checkbox" id="cpa-enabled" name="enabled">
启用 CPA 上传
</label>
<p class="hint">启用后可在账号管理页面上传账号到 CPA 管理平台</p>
</div>
<div class="form-group">
<label for="cpa-api-url">API URL</label>
<input type="text" id="cpa-api-url" name="api_url" placeholder="例如: https://cpa.example.com">
<p class="hint">CPA 管理平台的 API 地址</p>
</div>
<div class="form-group">
<label for="cpa-api-token">API Token</label>
<input type="password" id="cpa-api-token" name="api_token" placeholder="留空则保持原值" autocomplete="new-password">
<p class="hint">CPA 管理平台的认证 Token</p>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 保存设置</button>
<button type="button" class="btn btn-secondary" id="test-cpa-btn">🔌 测试连接</button>
</div>
</form>
</div>
</div>
<!-- CPA 服务管理 -->
<div class="card" style="margin-top: 16px;">
<div class="card">
<div class="card-header">
<h3>CPA 服务管理</h3>
<button class="btn btn-primary btn-sm" id="add-cpa-service-btn">+ 添加服务</button>