mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
feat: support proxy_url in CPA auth files
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
- 多个 CPA 账号打包为 `.zip`,每个账号一个独立文件
|
||||
- Sub2API 格式所有账号合并为单个 JSON
|
||||
- 上传目标(直连不走代理):
|
||||
- **CPA**:支持多服务配置,上传时选择目标服务
|
||||
- **CPA**:支持多服务配置,上传时选择目标服务,可按服务开关将账号实际代理写入 auth file 的 `proxy_url`
|
||||
- **Sub2API**:支持多服务配置,标准 sub2api-data 格式
|
||||
- **Team Manager**:支持多服务配置
|
||||
|
||||
@@ -367,7 +367,7 @@ docker-compose build --no-cache
|
||||
- 所有账号和设置数据存储在 `data/register.db`
|
||||
- 日志文件写入 `logs/` 目录
|
||||
- 代理优先级:动态代理 > 代理列表(随机/默认) > 直连
|
||||
- CPA / Sub2API / Team Manager 上传始终直连,不走代理
|
||||
- CPA / Sub2API / Team Manager 上传始终直连,不走代理;其中 CPA 可选把账号记录的代理写入 auth file 的 `proxy_url`
|
||||
- 注册时自动随机生成用户名和生日(年龄范围 18-45 岁)
|
||||
- 支付链接生成使用账号 access_token 鉴权,走全局代理配置
|
||||
- 无痕浏览器优先使用 playwright(注入 cookie 直达支付页);未安装时降级为系统 Chrome/Edge 无痕模式
|
||||
|
||||
@@ -89,17 +89,23 @@ def _post_cpa_auth_file_raw_json(upload_url: str, filename: str, file_content: b
|
||||
)
|
||||
|
||||
|
||||
def generate_token_json(account: Account) -> dict:
|
||||
def generate_token_json(
|
||||
account: Account,
|
||||
include_proxy_url: bool = False,
|
||||
proxy_url: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
生成 CPA 格式的 Token JSON
|
||||
|
||||
Args:
|
||||
account: 账号模型实例
|
||||
include_proxy_url: 是否将账号代理写入 auth file 的 proxy_url 字段
|
||||
proxy_url: 当账号本身没有记录代理时使用的兜底代理 URL
|
||||
|
||||
Returns:
|
||||
CPA 格式的 Token 字典
|
||||
"""
|
||||
return {
|
||||
token_data = {
|
||||
"type": "codex",
|
||||
"email": account.email,
|
||||
"expired": account.expires_at.strftime("%Y-%m-%dT%H:%M:%S+08:00") if account.expires_at else "",
|
||||
@@ -110,6 +116,12 @@ def generate_token_json(account: Account) -> dict:
|
||||
"refresh_token": account.refresh_token or "",
|
||||
}
|
||||
|
||||
resolved_proxy_url = (getattr(account, "proxy_used", None) or proxy_url or "").strip()
|
||||
if include_proxy_url and resolved_proxy_url:
|
||||
token_data["proxy_url"] = resolved_proxy_url
|
||||
|
||||
return token_data
|
||||
|
||||
|
||||
def upload_to_cpa(
|
||||
token_data: dict,
|
||||
@@ -185,15 +197,17 @@ def batch_upload_to_cpa(
|
||||
proxy: str = None,
|
||||
api_url: str = None,
|
||||
api_token: str = None,
|
||||
include_proxy_url: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
批量上传账号到 CPA 管理平台
|
||||
|
||||
Args:
|
||||
account_ids: 账号 ID 列表
|
||||
proxy: 可选的代理 URL
|
||||
proxy: 可选的代理 URL(用于 auth file proxy_url 的兜底值)
|
||||
api_url: 指定 CPA API URL(优先于全局配置)
|
||||
api_token: 指定 CPA API Token(优先于全局配置)
|
||||
include_proxy_url: 是否将账号代理写入 auth file 的 proxy_url 字段
|
||||
|
||||
Returns:
|
||||
包含成功/失败统计和详情的字典
|
||||
@@ -231,7 +245,11 @@ def batch_upload_to_cpa(
|
||||
continue
|
||||
|
||||
# 生成 Token JSON
|
||||
token_data = generate_token_json(account)
|
||||
token_data = generate_token_json(
|
||||
account,
|
||||
include_proxy_url=include_proxy_url,
|
||||
proxy_url=proxy,
|
||||
)
|
||||
|
||||
# 上传
|
||||
success, message = upload_to_cpa(token_data, proxy, api_url=api_url, api_token=api_token)
|
||||
|
||||
@@ -527,6 +527,7 @@ def create_cpa_service(
|
||||
api_url: str,
|
||||
api_token: str,
|
||||
enabled: bool = True,
|
||||
include_proxy_url: bool = False,
|
||||
priority: int = 0
|
||||
) -> CpaService:
|
||||
"""创建 CPA 服务配置"""
|
||||
@@ -535,6 +536,7 @@ def create_cpa_service(
|
||||
api_url=api_url,
|
||||
api_token=api_token,
|
||||
enabled=enabled,
|
||||
include_proxy_url=include_proxy_url,
|
||||
priority=priority
|
||||
)
|
||||
db.add(db_service)
|
||||
|
||||
@@ -139,6 +139,7 @@ class CpaService(Base):
|
||||
api_url = Column(String(500), nullable=False) # API URL
|
||||
api_token = Column(Text, nullable=False) # API Token
|
||||
enabled = Column(Boolean, default=True)
|
||||
include_proxy_url = Column(Boolean, default=False) # 是否将账号代理写入 auth file
|
||||
priority = Column(Integer, default=0) # 优先级
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
@@ -111,6 +111,7 @@ class DatabaseSessionManager:
|
||||
("accounts", "subscription_at", "DATETIME"),
|
||||
("accounts", "cookies", "TEXT"),
|
||||
("proxies", "is_default", "BOOLEAN DEFAULT 0"),
|
||||
("cpa_services", "include_proxy_url", "BOOLEAN DEFAULT 0"),
|
||||
]
|
||||
|
||||
# 确保新表存在(create_tables 已处理,此处兜底)
|
||||
|
||||
@@ -724,11 +724,12 @@ class BatchCPAUploadRequest(BaseModel):
|
||||
async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
"""批量上传账号到 CPA"""
|
||||
|
||||
proxy = request.proxy if request.proxy else get_settings().proxy_url
|
||||
proxy = request.proxy
|
||||
|
||||
# 解析指定的 CPA 服务
|
||||
cpa_api_url = None
|
||||
cpa_api_token = None
|
||||
include_proxy_url = False
|
||||
if request.cpa_service_id:
|
||||
with get_db() as db:
|
||||
svc = crud.get_cpa_service_by_id(db, request.cpa_service_id)
|
||||
@@ -736,6 +737,7 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
raise HTTPException(status_code=404, detail="指定的 CPA 服务不存在")
|
||||
cpa_api_url = svc.api_url
|
||||
cpa_api_token = svc.api_token
|
||||
include_proxy_url = bool(svc.include_proxy_url)
|
||||
|
||||
with get_db() as db:
|
||||
ids = resolve_account_ids(
|
||||
@@ -743,7 +745,13 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
request.status_filter, request.email_service_filter, request.search_filter
|
||||
)
|
||||
|
||||
results = batch_upload_to_cpa(ids, proxy, api_url=cpa_api_url, api_token=cpa_api_token)
|
||||
results = batch_upload_to_cpa(
|
||||
ids,
|
||||
proxy,
|
||||
api_url=cpa_api_url,
|
||||
api_token=cpa_api_token,
|
||||
include_proxy_url=include_proxy_url,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@@ -751,12 +759,13 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
|
||||
async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequest] = Body(default=None)):
|
||||
"""上传单个账号到 CPA"""
|
||||
|
||||
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
|
||||
proxy = request.proxy if request else None
|
||||
cpa_service_id = request.cpa_service_id if request else None
|
||||
|
||||
# 解析指定的 CPA 服务
|
||||
cpa_api_url = None
|
||||
cpa_api_token = None
|
||||
include_proxy_url = False
|
||||
if cpa_service_id:
|
||||
with get_db() as db:
|
||||
svc = crud.get_cpa_service_by_id(db, cpa_service_id)
|
||||
@@ -764,6 +773,7 @@ async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequ
|
||||
raise HTTPException(status_code=404, detail="指定的 CPA 服务不存在")
|
||||
cpa_api_url = svc.api_url
|
||||
cpa_api_token = svc.api_token
|
||||
include_proxy_url = bool(svc.include_proxy_url)
|
||||
|
||||
with get_db() as db:
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
@@ -777,7 +787,11 @@ async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequ
|
||||
}
|
||||
|
||||
# 生成 Token JSON
|
||||
token_data = generate_token_json(account)
|
||||
token_data = generate_token_json(
|
||||
account,
|
||||
include_proxy_url=include_proxy_url,
|
||||
proxy_url=proxy,
|
||||
)
|
||||
|
||||
# 上传
|
||||
success, message = upload_to_cpa(token_data, proxy, api_url=cpa_api_url, api_token=cpa_api_token)
|
||||
|
||||
@@ -418,7 +418,6 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
from ...database.models import Account as AccountModel
|
||||
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_ids = cpa_service_ids or []
|
||||
if not _cpa_ids:
|
||||
# 未指定则取所有启用的服务
|
||||
@@ -430,6 +429,10 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
_svc = crud.get_cpa_service_by_id(db, _sid)
|
||||
if not _svc:
|
||||
continue
|
||||
token_data = generate_token_json(
|
||||
saved_account,
|
||||
include_proxy_url=bool(_svc.include_proxy_url),
|
||||
)
|
||||
log_callback(f"[CPA] 上传到服务: {_svc.name}")
|
||||
_ok, _msg = upload_to_cpa(token_data, api_url=_svc.api_url, api_token=_svc.api_token)
|
||||
if _ok:
|
||||
|
||||
@@ -20,6 +20,7 @@ class CpaServiceCreate(BaseModel):
|
||||
api_url: str
|
||||
api_token: str
|
||||
enabled: bool = True
|
||||
include_proxy_url: bool = False
|
||||
priority: int = 0
|
||||
|
||||
|
||||
@@ -28,6 +29,7 @@ class CpaServiceUpdate(BaseModel):
|
||||
api_url: Optional[str] = None
|
||||
api_token: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
include_proxy_url: Optional[bool] = None
|
||||
priority: Optional[int] = None
|
||||
|
||||
|
||||
@@ -37,6 +39,7 @@ class CpaServiceResponse(BaseModel):
|
||||
api_url: str
|
||||
has_token: bool
|
||||
enabled: bool
|
||||
include_proxy_url: bool
|
||||
priority: int
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
@@ -57,6 +60,7 @@ def _to_response(svc) -> CpaServiceResponse:
|
||||
api_url=svc.api_url,
|
||||
has_token=bool(svc.api_token),
|
||||
enabled=svc.enabled,
|
||||
include_proxy_url=bool(svc.include_proxy_url),
|
||||
priority=svc.priority,
|
||||
created_at=svc.created_at.isoformat() if svc.created_at else None,
|
||||
updated_at=svc.updated_at.isoformat() if svc.updated_at else None,
|
||||
@@ -83,6 +87,7 @@ async def create_cpa_service(request: CpaServiceCreate):
|
||||
api_url=request.api_url,
|
||||
api_token=request.api_token,
|
||||
enabled=request.enabled,
|
||||
include_proxy_url=request.include_proxy_url,
|
||||
priority=request.priority,
|
||||
)
|
||||
return _to_response(service)
|
||||
@@ -111,6 +116,7 @@ async def get_cpa_service_full(service_id: int):
|
||||
"api_url": service.api_url,
|
||||
"api_token": service.api_token,
|
||||
"enabled": service.enabled,
|
||||
"include_proxy_url": bool(service.include_proxy_url),
|
||||
"priority": service.priority,
|
||||
}
|
||||
|
||||
@@ -133,6 +139,8 @@ async def update_cpa_service(service_id: int, request: CpaServiceUpdate):
|
||||
update_data["api_token"] = request.api_token
|
||||
if request.enabled is not None:
|
||||
update_data["enabled"] = request.enabled
|
||||
if request.include_proxy_url is not None:
|
||||
update_data["include_proxy_url"] = request.include_proxy_url
|
||||
if request.priority is not None:
|
||||
update_data["priority"] = request.priority
|
||||
|
||||
|
||||
@@ -1224,19 +1224,20 @@ async function loadCpaServices() {
|
||||
const services = await api.get('/cpa-services');
|
||||
renderCpaServicesTable(services);
|
||||
} catch (e) {
|
||||
elements.cpaServicesTable.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--danger-color);">${e.message}</td></tr>`;
|
||||
elements.cpaServicesTable.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--danger-color);">${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCpaServicesTable(services) {
|
||||
if (!services || services.length === 0) {
|
||||
elements.cpaServicesTable.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:20px;">暂无 CPA 服务,点击「添加服务」新增</td></tr>';
|
||||
elements.cpaServicesTable.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:20px;">暂无 CPA 服务,点击「添加服务」新增</td></tr>';
|
||||
return;
|
||||
}
|
||||
elements.cpaServicesTable.innerHTML = services.map(s => `
|
||||
<tr>
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td style="font-size:0.85rem;color:var(--text-muted);">${escapeHtml(s.api_url)}</td>
|
||||
<td style="text-align:center;">${s.include_proxy_url ? '🟢' : '⚪'}</td>
|
||||
<td style="text-align:center;" title="${s.enabled ? '已启用' : '已禁用'}">${s.enabled ? '✅' : '⭕'}</td>
|
||||
<td style="text-align:center;">${s.priority}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
@@ -1255,6 +1256,7 @@ function openCpaServiceModal(service = null) {
|
||||
document.getElementById('cpa-service-token').value = '';
|
||||
document.getElementById('cpa-service-priority').value = service ? service.priority : 0;
|
||||
document.getElementById('cpa-service-enabled').checked = service ? service.enabled : true;
|
||||
document.getElementById('cpa-service-include-proxy-url').checked = service ? !!service.include_proxy_url : false;
|
||||
elements.cpaServiceModalTitle.textContent = service ? '编辑 CPA 服务' : '添加 CPA 服务';
|
||||
elements.cpaServiceEditModal.classList.add('active');
|
||||
}
|
||||
@@ -1280,6 +1282,7 @@ async function handleSaveCpaService(e) {
|
||||
const apiToken = document.getElementById('cpa-service-token').value.trim();
|
||||
const priority = parseInt(document.getElementById('cpa-service-priority').value) || 0;
|
||||
const enabled = document.getElementById('cpa-service-enabled').checked;
|
||||
const includeProxyUrl = document.getElementById('cpa-service-include-proxy-url').checked;
|
||||
|
||||
if (!name || !apiUrl) {
|
||||
toast.error('名称和 API URL 不能为空');
|
||||
@@ -1291,7 +1294,7 @@ async function handleSaveCpaService(e) {
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = { name, api_url: apiUrl, priority, enabled };
|
||||
const payload = { name, api_url: apiUrl, priority, enabled, include_proxy_url: includeProxyUrl };
|
||||
if (apiToken) payload.api_token = apiToken;
|
||||
|
||||
if (id) {
|
||||
|
||||
@@ -216,13 +216,14 @@
|
||||
<tr>
|
||||
<th style="width:150px;">名称</th>
|
||||
<th>API URL</th>
|
||||
<th style="width:90px;">代理写入</th>
|
||||
<th style="width:80px;">状态</th>
|
||||
<th style="width:60px;text-align:center;">优先级</th>
|
||||
<th style="width:220px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cpa-services-table">
|
||||
<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:20px;">加载中...</td></tr>
|
||||
<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:20px;">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -406,6 +407,10 @@
|
||||
<label style="margin-top:10px;display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="cpa-service-enabled" checked> 启用
|
||||
</label>
|
||||
<label style="margin-top:10px;display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="cpa-service-include-proxy-url"> 写入 auth file 的 <code>proxy_url</code>
|
||||
</label>
|
||||
<p class="hint">开启后,若账号记录了实际使用代理(含动态代理),上传到 CPA 时会一并写入。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.core.upload import cpa_upload
|
||||
|
||||
|
||||
@@ -108,3 +110,41 @@ def test_test_cpa_connection_uses_get_and_normalized_url(monkeypatch):
|
||||
assert message == "CPA 连接测试成功"
|
||||
assert calls[0]["url"] == "https://cpa.example.com/v0/management/auth-files"
|
||||
assert calls[0]["kwargs"]["headers"]["Authorization"] == "Bearer token-123"
|
||||
|
||||
|
||||
def test_generate_token_json_includes_account_proxy_url_when_enabled():
|
||||
account = SimpleNamespace(
|
||||
email="tester@example.com",
|
||||
expires_at=None,
|
||||
id_token="id-token",
|
||||
account_id="acct-1",
|
||||
access_token="access-token",
|
||||
last_refresh=None,
|
||||
refresh_token="refresh-token",
|
||||
proxy_used="socks5://127.0.0.1:1080",
|
||||
)
|
||||
|
||||
token_data = cpa_upload.generate_token_json(account, include_proxy_url=True)
|
||||
|
||||
assert token_data["proxy_url"] == "socks5://127.0.0.1:1080"
|
||||
|
||||
|
||||
def test_generate_token_json_uses_fallback_proxy_when_account_proxy_missing():
|
||||
account = SimpleNamespace(
|
||||
email="tester@example.com",
|
||||
expires_at=None,
|
||||
id_token="id-token",
|
||||
account_id="acct-1",
|
||||
access_token="access-token",
|
||||
last_refresh=None,
|
||||
refresh_token="refresh-token",
|
||||
proxy_used=None,
|
||||
)
|
||||
|
||||
token_data = cpa_upload.generate_token_json(
|
||||
account,
|
||||
include_proxy_url=True,
|
||||
proxy_url="http://proxy.example.com:8080",
|
||||
)
|
||||
|
||||
assert token_data["proxy_url"] == "http://proxy.example.com:8080"
|
||||
|
||||
Reference in New Issue
Block a user