fix(team manager): 更改team上传router路径

This commit is contained in:
cnlimiter
2026-03-19 19:01:44 +08:00
parent 11c0b622aa
commit cd01280b56
4 changed files with 287 additions and 191 deletions

View File

@@ -7,7 +7,7 @@ import logging
from typing import List, Optional from typing import List, Optional
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks from fastapi import APIRouter, HTTPException, Query, BackgroundTasks, Body
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
@@ -579,28 +579,6 @@ class BatchValidateRequest(BaseModel):
search_filter: Optional[str] = None search_filter: Optional[str] = None
@router.post("/{account_id}/refresh")
async def refresh_account_token(account_id: int, request: TokenRefreshRequest = None):
"""刷新单个账号的 Token"""
from ...core.openai.token_refresh import refresh_account_token as do_refresh
# 使用传入的代理或全局代理配置
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
result = do_refresh(account_id, proxy)
if result.success:
return {
"success": True,
"message": "Token 刷新成功",
"expires_at": result.expires_at.isoformat() if result.expires_at else None
}
else:
return {
"success": False,
"error": result.error_message
}
@router.post("/batch-refresh") @router.post("/batch-refresh")
async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: BackgroundTasks): async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: BackgroundTasks):
"""批量刷新账号 Token""" """批量刷新账号 Token"""
@@ -636,20 +614,26 @@ async def batch_refresh_tokens(request: BatchRefreshRequest, background_tasks: B
return results return results
@router.post("/{account_id}/validate") @router.post("/{account_id}/refresh")
async def validate_account_token(account_id: int, request: TokenValidateRequest = None): async def refresh_account_token(account_id: int, request: Optional[TokenRefreshRequest] = Body(default=None)):
"""验证单个账号的 Token 有效性""" """刷新单个账号的 Token"""
from ...core.openai.token_refresh import validate_account_token as do_validate from ...core.openai.token_refresh import refresh_account_token as do_refresh
# 使用传入的代理或全局代理配置 # 使用传入的代理或全局代理配置
proxy = request.proxy if request and request.proxy else get_settings().proxy_url proxy = request.proxy if request and request.proxy else get_settings().proxy_url
is_valid, error = do_validate(account_id, proxy) result = do_refresh(account_id, proxy)
return { if result.success:
"id": account_id, return {
"valid": is_valid, "success": True,
"error": error "message": "Token 刷新成功",
} "expires_at": result.expires_at.isoformat() if result.expires_at else None
}
else:
return {
"success": False,
"error": result.error_message
}
@router.post("/batch-validate") @router.post("/batch-validate")
@@ -695,6 +679,22 @@ async def batch_validate_tokens(request: BatchValidateRequest):
return results return results
@router.post("/{account_id}/validate")
async def validate_account_token(account_id: int, request: Optional[TokenValidateRequest] = Body(default=None)):
"""验证单个账号的 Token 有效性"""
from ...core.openai.token_refresh import validate_account_token as do_validate
# 使用传入的代理或全局代理配置
proxy = request.proxy if request and request.proxy else get_settings().proxy_url
is_valid, error = do_validate(account_id, proxy)
return {
"id": account_id,
"valid": is_valid,
"error": error
}
# ============== CPA 上传相关 ============== # ============== CPA 上传相关 ==============
class CPAUploadRequest(BaseModel): class CPAUploadRequest(BaseModel):
@@ -714,8 +714,36 @@ class BatchCPAUploadRequest(BaseModel):
cpa_service_id: Optional[int] = None # 指定 CPA 服务 ID不传则使用全局配置 cpa_service_id: Optional[int] = None # 指定 CPA 服务 ID不传则使用全局配置
@router.post("/batch-upload-cpa")
async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
"""批量上传账号到 CPA"""
from ...core.upload.cpa_upload import batch_upload_to_cpa
proxy = request.proxy if request.proxy else get_settings().proxy_url
# 解析指定的 CPA 服务
cpa_api_url = None
cpa_api_token = None
if request.cpa_service_id:
with get_db() as db:
svc = crud.get_cpa_service_by_id(db, request.cpa_service_id)
if not svc:
raise HTTPException(status_code=404, detail="指定的 CPA 服务不存在")
cpa_api_url = svc.api_url
cpa_api_token = svc.api_token
with get_db() as db:
ids = resolve_account_ids(
db, request.ids, request.select_all,
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)
return results
@router.post("/{account_id}/upload-cpa") @router.post("/{account_id}/upload-cpa")
async def upload_account_to_cpa(account_id: int, request: CPAUploadRequest = None): async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequest] = Body(default=None)):
"""上传单个账号到 CPA""" """上传单个账号到 CPA"""
from ...core.upload.cpa_upload import upload_to_cpa, generate_token_json from ...core.upload.cpa_upload import upload_to_cpa, generate_token_json
@@ -759,34 +787,6 @@ async def upload_account_to_cpa(account_id: int, request: CPAUploadRequest = Non
return {"success": False, "error": message} return {"success": False, "error": message}
@router.post("/batch-upload-cpa")
async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest):
"""批量上传账号到 CPA"""
from ...core.upload.cpa_upload import batch_upload_to_cpa
proxy = request.proxy if request.proxy else get_settings().proxy_url
# 解析指定的 CPA 服务
cpa_api_url = None
cpa_api_token = None
if request.cpa_service_id:
with get_db() as db:
svc = crud.get_cpa_service_by_id(db, request.cpa_service_id)
if not svc:
raise HTTPException(status_code=404, detail="指定的 CPA 服务不存在")
cpa_api_url = svc.api_url
cpa_api_token = svc.api_token
with get_db() as db:
ids = resolve_account_ids(
db, request.ids, request.select_all,
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)
return results
class Sub2ApiUploadRequest(BaseModel): class Sub2ApiUploadRequest(BaseModel):
"""单账号 Sub2API 上传请求""" """单账号 Sub2API 上传请求"""
service_id: Optional[int] = None service_id: Optional[int] = None
@@ -794,51 +794,6 @@ class Sub2ApiUploadRequest(BaseModel):
priority: int = 50 priority: int = 50
@router.post("/{account_id}/upload-sub2api")
async def upload_account_to_sub2api(account_id: int, request: Sub2ApiUploadRequest = None):
"""上传单个账号到 Sub2API"""
from ...core.upload.sub2api_upload import upload_to_sub2api
service_id = request.service_id if request else None
concurrency = request.concurrency if request else 3
priority = request.priority if request else 50
api_url = None
api_key = None
if service_id:
with get_db() as db:
svc = crud.get_sub2api_service_by_id(db, service_id)
if not svc:
raise HTTPException(status_code=404, detail="指定的 Sub2API 服务不存在")
api_url = svc.api_url
api_key = svc.api_key
else:
with get_db() as db:
svcs = crud.get_sub2api_services(db, enabled=True)
if svcs:
api_url = svcs[0].api_url
api_key = svcs[0].api_key
if not api_url or not api_key:
raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务,请先在设置中配置")
with get_db() as db:
account = crud.get_account_by_id(db, account_id)
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
if not account.access_token:
return {"success": False, "error": "账号缺少 Token无法上传"}
success, message = upload_to_sub2api(
[account], api_url, api_key,
concurrency=concurrency, priority=priority
)
if success:
return {"success": True, "message": message}
else:
return {"success": False, "error": message}
class BatchSub2ApiUploadRequest(BaseModel): class BatchSub2ApiUploadRequest(BaseModel):
"""批量 Sub2API 上传请求""" """批量 Sub2API 上传请求"""
ids: List[int] = [] ids: List[int] = []
@@ -888,3 +843,118 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest):
priority=request.priority, priority=request.priority,
) )
return results return results
@router.post("/{account_id}/upload-sub2api")
async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUploadRequest] = Body(default=None)):
"""上传单个账号到 Sub2API"""
from ...core.upload.sub2api_upload import upload_to_sub2api
service_id = request.service_id if request else None
concurrency = request.concurrency if request else 3
priority = request.priority if request else 50
api_url = None
api_key = None
if service_id:
with get_db() as db:
svc = crud.get_sub2api_service_by_id(db, service_id)
if not svc:
raise HTTPException(status_code=404, detail="指定的 Sub2API 服务不存在")
api_url = svc.api_url
api_key = svc.api_key
else:
with get_db() as db:
svcs = crud.get_sub2api_services(db, enabled=True)
if svcs:
api_url = svcs[0].api_url
api_key = svcs[0].api_key
if not api_url or not api_key:
raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务,请先在设置中配置")
with get_db() as db:
account = crud.get_account_by_id(db, account_id)
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
if not account.access_token:
return {"success": False, "error": "账号缺少 Token无法上传"}
success, message = upload_to_sub2api(
[account], api_url, api_key,
concurrency=concurrency, priority=priority
)
if success:
return {"success": True, "message": message}
else:
return {"success": False, "error": message}
# ============== Team Manager 上传 ==============
class UploadTMRequest(BaseModel):
service_id: Optional[int] = None
class BatchUploadTMRequest(BaseModel):
ids: List[int] = []
select_all: bool = False
status_filter: Optional[str] = None
email_service_filter: Optional[str] = None
search_filter: Optional[str] = None
service_id: Optional[int] = None
@router.post("/batch-upload-tm")
async def batch_upload_accounts_to_tm(request: BatchUploadTMRequest):
"""批量上传账号到 Team Manager"""
from ...core.upload.team_manager_upload import batch_upload_to_team_manager
with get_db() as db:
if request.service_id:
svc = crud.get_tm_service_by_id(db, request.service_id)
else:
svcs = crud.get_tm_services(db, enabled=True)
svc = svcs[0] if svcs else None
if not svc:
raise HTTPException(status_code=400, detail="未找到可用的 Team Manager 服务,请先在设置中配置")
api_url = svc.api_url
api_key = svc.api_key
ids = resolve_account_ids(
db, request.ids, request.select_all,
request.status_filter, request.email_service_filter, request.search_filter
)
results = batch_upload_to_team_manager(ids, api_url, api_key)
return results
@router.post("/{account_id}/upload-tm")
async def upload_account_to_tm(account_id: int, request: Optional[UploadTMRequest] = Body(default=None)):
"""上传单账号到 Team Manager"""
from ...core.upload.team_manager_upload import upload_to_team_manager
service_id = request.service_id if request else None
with get_db() as db:
if service_id:
svc = crud.get_tm_service_by_id(db, service_id)
else:
svcs = crud.get_tm_services(db, enabled=True)
svc = svcs[0] if svcs else None
if not svc:
raise HTTPException(status_code=400, detail="未找到可用的 Team Manager 服务,请先在设置中配置")
api_url = svc.api_url
api_key = svc.api_key
account = crud.get_account_by_id(db, account_id)
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
success, message = upload_to_team_manager(account, api_url, api_key)
return {"success": success, "message": message}

View File

@@ -20,10 +20,6 @@ from ...core.openai.payment import (
open_url_incognito, open_url_incognito,
check_subscription_status, check_subscription_status,
) )
from ...core.upload.team_manager_upload import (
upload_to_team_manager,
batch_upload_to_team_manager,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@@ -60,20 +56,6 @@ class BatchCheckSubscriptionRequest(BaseModel):
search_filter: Optional[str] = None search_filter: Optional[str] = None
class UploadTMRequest(BaseModel):
proxy: Optional[str] = None # 保留TM 上传不走代理
service_id: Optional[int] = None # 指定 TM 服务 ID不传则使用第一个启用的
class BatchUploadTMRequest(BaseModel):
ids: List[int] = []
select_all: bool = False
status_filter: Optional[str] = None
service_id: Optional[int] = None # 指定 TM 服务 ID不传则使用第一个启用的
email_service_filter: Optional[str] = None
search_filter: Optional[str] = None
# ============== 支付链接生成 ============== # ============== 支付链接生成 ==============
@router.post("/generate-link") @router.post("/generate-link")
@@ -140,25 +122,6 @@ def open_browser_incognito(request: OpenIncognitoRequest):
# ============== 订阅状态 ============== # ============== 订阅状态 ==============
@router.post("/accounts/{account_id}/mark-subscription")
def mark_subscription(account_id: int, request: MarkSubscriptionRequest):
"""手动标记账号订阅类型"""
allowed = ("free", "plus", "team")
if request.subscription_type not in allowed:
raise HTTPException(status_code=400, detail=f"subscription_type 必须为 {allowed}")
with get_db() as db:
account = db.query(Account).filter(Account.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
account.subscription_type = None if request.subscription_type == "free" else request.subscription_type
account.subscription_at = datetime.utcnow() if request.subscription_type != "free" else None
db.commit()
return {"success": True, "subscription_type": request.subscription_type}
@router.post("/accounts/batch-check-subscription") @router.post("/accounts/batch-check-subscription")
def batch_check_subscription(request: BatchCheckSubscriptionRequest): def batch_check_subscription(request: BatchCheckSubscriptionRequest):
"""批量检测账号订阅状态""" """批量检测账号订阅状态"""
@@ -198,56 +161,22 @@ def batch_check_subscription(request: BatchCheckSubscriptionRequest):
return results return results
# ============== Team Manager 上传 ============== @router.post("/accounts/{account_id}/mark-subscription")
def mark_subscription(account_id: int, request: MarkSubscriptionRequest):
@router.post("/accounts/{account_id}/upload-tm") """手动标记账号订阅类型"""
def upload_account_tm(account_id: int, request: UploadTMRequest = None): allowed = ("free", "plus", "team")
"""上传单账号到 Team Manager""" if request.subscription_type not in allowed:
service_id = request.service_id if request and hasattr(request, 'service_id') else None raise HTTPException(status_code=400, detail=f"subscription_type 必须为 {allowed}")
with get_db() as db: with get_db() as db:
if service_id:
svc = crud.get_tm_service_by_id(db, service_id)
else:
svcs = crud.get_tm_services(db, enabled=True)
svc = svcs[0] if svcs else None
if not svc:
raise HTTPException(status_code=400, detail="未找到可用的 Team Manager 服务,请先在设置中配置")
api_url = svc.api_url
api_key = svc.api_key
account = db.query(Account).filter(Account.id == account_id).first() account = db.query(Account).filter(Account.id == account_id).first()
if not account: if not account:
raise HTTPException(status_code=404, detail="账号不存在") raise HTTPException(status_code=404, detail="账号不存在")
success, message = upload_to_team_manager(account, api_url, api_key)
return {"success": success, "message": message} account.subscription_type = None if request.subscription_type == "free" else request.subscription_type
account.subscription_at = datetime.utcnow() if request.subscription_type != "free" else None
db.commit()
return {"success": True, "subscription_type": request.subscription_type}
@router.post("/accounts/batch-upload-tm")
def batch_upload_tm(request: BatchUploadTMRequest):
"""批量上传账号到 Team Manager"""
service_id = request.service_id if hasattr(request, 'service_id') else None
with get_db() as db:
if service_id:
svc = crud.get_tm_service_by_id(db, service_id)
else:
svcs = crud.get_tm_services(db, enabled=True)
svc = svcs[0] if svcs else None
if not svc:
raise HTTPException(status_code=400, detail="未找到可用的 Team Manager 服务,请先在设置中配置")
api_url = svc.api_url
api_key = svc.api_key
ids = resolve_account_ids(
db, request.ids, request.select_all,
request.status_filter, request.email_service_filter, request.search_filter
)
results = batch_upload_to_team_manager(ids, api_url, api_key)
return results

View File

@@ -1075,11 +1075,82 @@ async function uploadToSub2Api(id) {
} }
} }
// 弹出 Team Manager 服务选择框,返回 Promise<{service_id: number|null}|null>
// null 表示用户取消,{service_id: null} 表示自动选择
function selectTmService() {
return new Promise(async (resolve) => {
const modal = document.getElementById('tm-service-modal');
const listEl = document.getElementById('tm-service-list');
const closeBtn = document.getElementById('close-tm-modal');
const cancelBtn = document.getElementById('cancel-tm-modal-btn');
const autoBtn = document.getElementById('tm-use-auto-btn');
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted)">加载中...</div>';
modal.classList.add('active');
let services = [];
try {
services = await api.get('/tm-services?enabled=true');
} catch (e) {
services = [];
}
if (services.length === 0) {
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:12px;">暂无已启用的 Team Manager 服务,将自动选择第一个</div>';
} else {
listEl.innerHTML = services.map(s => `
<div class="tm-service-item" data-id="${s.id}" style="
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
display: flex;
justify-content: space-between;
align-items: center;
">
<div>
<div style="font-weight:500;">${escapeHtml(s.name)}</div>
<div style="font-size:0.8rem;color:var(--text-muted);">${escapeHtml(s.api_url)}</div>
</div>
<span class="badge" style="background:var(--primary);color:#fff;font-size:0.7rem;padding:2px 8px;border-radius:10px;">选择</span>
</div>
`).join('');
listEl.querySelectorAll('.tm-service-item').forEach(item => {
item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)');
item.addEventListener('mouseleave', () => item.style.background = '');
item.addEventListener('click', () => {
cleanup();
resolve({ service_id: parseInt(item.dataset.id) });
});
});
}
function cleanup() {
modal.classList.remove('active');
closeBtn.removeEventListener('click', onCancel);
cancelBtn.removeEventListener('click', onCancel);
autoBtn.removeEventListener('click', onAuto);
}
function onCancel() { cleanup(); resolve(null); }
function onAuto() { cleanup(); resolve({ service_id: null }); }
closeBtn.addEventListener('click', onCancel);
cancelBtn.addEventListener('click', onCancel);
autoBtn.addEventListener('click', onAuto);
});
}
// 上传单账号到 Team Manager // 上传单账号到 Team Manager
async function uploadToTm(id) { async function uploadToTm(id) {
const choice = await selectTmService();
if (choice === null) return;
try { try {
toast.info('正在上传到 Team Manager...'); toast.info('正在上传到 Team Manager...');
const result = await api.post(`/payment/accounts/${id}/upload-tm`); const payload = {};
if (choice.service_id != null) payload.service_id = choice.service_id;
const result = await api.post(`/accounts/${id}/upload-tm`, payload);
if (result.success) { if (result.success) {
toast.success('上传成功'); toast.success('上传成功');
} else { } else {
@@ -1094,6 +1165,10 @@ async function uploadToTm(id) {
async function handleBatchUploadTm() { async function handleBatchUploadTm() {
const count = getEffectiveCount(); const count = getEffectiveCount();
if (count === 0) return; if (count === 0) return;
const choice = await selectTmService();
if (choice === null) return; // 用户取消
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`); const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`);
if (!confirmed) return; if (!confirmed) return;
@@ -1101,7 +1176,9 @@ async function handleBatchUploadTm() {
elements.batchUploadBtn.textContent = '上传中...'; elements.batchUploadBtn.textContent = '上传中...';
try { try {
const result = await api.post('/payment/accounts/batch-upload-tm', buildBatchPayload()); const payload = buildBatchPayload();
if (choice.service_id != null) payload.service_id = choice.service_id;
const result = await api.post('/accounts/batch-upload-tm', payload);
let message = `成功: ${result.success_count}`; let message = `成功: ${result.success_count}`;
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`; if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`; if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;

View File

@@ -237,6 +237,26 @@
</div> </div>
</div> </div>
<!-- Team Manager 服务选择模态框 -->
<div class="modal" id="tm-service-modal">
<div class="modal-content" style="max-width: 480px;">
<div class="modal-header">
<h3>🚀 选择 Team Manager 服务</h3>
<button class="modal-close" id="close-tm-modal">&times;</button>
</div>
<div class="modal-body">
<p style="color: var(--text-muted); margin-bottom: 12px; font-size: 0.9rem;">选择要上传到的 Team Manager 服务,或自动选择第一个启用的服务。</p>
<div id="tm-service-list" style="display: flex; flex-direction: column; gap: 8px; max-height: 300px; overflow-y: auto;">
<div style="text-align: center; color: var(--text-muted);">加载中...</div>
</div>
</div>
<div class="modal-footer" style="padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end;">
<button class="btn btn-secondary" id="tm-use-auto-btn">自动选择</button>
<button class="btn btn-secondary" id="cancel-tm-modal-btn">取消</button>
</div>
</div>
</div>
<!-- Sub2API 服务选择模态框 --> <!-- Sub2API 服务选择模态框 -->
<div class="modal" id="sub2api-service-modal"> <div class="modal" id="sub2api-service-modal">
<div class="modal-content" style="max-width: 480px;"> <div class="modal-content" style="max-width: 480px;">