mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
Merge branch 'master' into copilot/codex-auth-f4d0327
This commit is contained in:
143
src/core/upload/newapi_upload.py
Normal file
143
src/core/upload/newapi_upload.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
NEWAPI 上传功能 — 通过 PUT /api/channel/ 添加渠道
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
from ...database.models import Account
|
||||
from ...database.session import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_CHANNEL_TYPE = 1
|
||||
DEFAULT_CHANNEL_BASE_URL = ""
|
||||
DEFAULT_CHANNEL_MODELS = "gpt-5.4,gpt-5,gpt-5-codex,gpt-5-codex-mini,gpt-5.1,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini,gpt-5.2,gpt-5.2-codex,gpt-5.3-codex,gpt-5-openai-compact,gpt-5-codex-openai-compact,gpt-5-codex-mini-openai-compact,gpt-5.1-openai-compact,gpt-5.1-codex-openai-compact,gpt-5.1-codex-max-openai-compact,gpt-5.1-codex-mini-openai-compact,gpt-5.2-openai-compact,gpt-5.2-codex-openai-compact,gpt-5.3-codex-openai-compact"
|
||||
|
||||
|
||||
def _normalize_base(api_url: str) -> str:
|
||||
return (api_url or "").strip().rstrip("/")
|
||||
|
||||
|
||||
def _build_headers(api_key: str) -> dict:
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"New-Api-User": "1",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _extract_error(resp) -> str:
|
||||
error_msg = f"上传失败: HTTP {resp.status_code}"
|
||||
try:
|
||||
detail = resp.json()
|
||||
if isinstance(detail, dict):
|
||||
error_msg = detail.get("message", error_msg)
|
||||
except Exception:
|
||||
error_msg = f"{error_msg} - {resp.text[:200]}"
|
||||
return error_msg
|
||||
|
||||
|
||||
def upload_to_newapi(
|
||||
account: Account,
|
||||
api_url: str,
|
||||
api_key: str,
|
||||
channel_type: Optional[int] = None,
|
||||
channel_base_url: Optional[str] = None,
|
||||
channel_models: Optional[str] = None,
|
||||
) -> Tuple[bool, str]:
|
||||
base = _normalize_base(api_url)
|
||||
if not base:
|
||||
return False, "NEWAPI API URL 未配置"
|
||||
if not api_key:
|
||||
return False, "NEWAPI API Key 未配置"
|
||||
if not account.access_token:
|
||||
return False, "账号缺少 access_token"
|
||||
|
||||
resolved_channel_type = channel_type if isinstance(channel_type, int) and channel_type > 0 else DEFAULT_CHANNEL_TYPE
|
||||
resolved_channel_base_url = (channel_base_url or DEFAULT_CHANNEL_BASE_URL).strip()
|
||||
resolved_channel_models = (channel_models or DEFAULT_CHANNEL_MODELS).strip() or DEFAULT_CHANNEL_MODELS
|
||||
|
||||
url = f"{base}/api/channel/"
|
||||
account_name = account.email or ""
|
||||
channel = {
|
||||
"auto_ban": 1,
|
||||
"name": account.email or "",
|
||||
"type": resolved_channel_type,
|
||||
"key": json.dumps({"access_token": account.access_token or "", "account_id": account_name}, ensure_ascii=False),
|
||||
"base_url": resolved_channel_base_url,
|
||||
"models": resolved_channel_models,
|
||||
"multi_key_mode": "random",
|
||||
"group": "default",
|
||||
"groups": ["default"],
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = cffi_requests.post(
|
||||
url,
|
||||
headers=_build_headers(api_key),
|
||||
json={"mode": "single", "channel": channel},
|
||||
proxies=None,
|
||||
timeout=30,
|
||||
impersonate="chrome110",
|
||||
)
|
||||
if resp.status_code in (200, 201):
|
||||
return True, "上传成功"
|
||||
return False, _extract_error(resp)
|
||||
except Exception as e:
|
||||
logger.error("NEWAPI 上传异常: %s", e)
|
||||
return False, f"上传异常: {str(e)}"
|
||||
|
||||
|
||||
def batch_upload_to_newapi(
|
||||
account_ids: List[int],
|
||||
api_url: str,
|
||||
api_key: str,
|
||||
channel_type: Optional[int] = None,
|
||||
channel_base_url: Optional[str] = None,
|
||||
channel_models: Optional[str] = None,
|
||||
) -> dict:
|
||||
results = {
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"skipped_count": 0,
|
||||
"details": [],
|
||||
}
|
||||
|
||||
with get_db() as db:
|
||||
for account_id in account_ids:
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
if not account:
|
||||
results["failed_count"] += 1
|
||||
results["details"].append({"id": account_id, "email": None, "success": False, "error": "账号不存在"})
|
||||
continue
|
||||
if not account.access_token:
|
||||
results["skipped_count"] += 1
|
||||
results["details"].append({"id": account_id, "email": account.email, "success": False, "error": "缺少 Token"})
|
||||
continue
|
||||
|
||||
success, message = upload_to_newapi(
|
||||
account,
|
||||
api_url,
|
||||
api_key,
|
||||
channel_type=channel_type,
|
||||
channel_base_url=channel_base_url,
|
||||
channel_models=channel_models,
|
||||
)
|
||||
if success:
|
||||
account.newapi_uploaded = True
|
||||
account.newapi_uploaded_at = datetime.utcnow()
|
||||
db.commit()
|
||||
results["success_count"] += 1
|
||||
results["details"].append({"id": account_id, "email": account.email, "success": True, "message": message})
|
||||
else:
|
||||
results["failed_count"] += 1
|
||||
results["details"].append({"id": account_id, "email": account.email, "success": False, "error": message})
|
||||
|
||||
return results
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy import and_, or_, desc, asc, func
|
||||
|
||||
from .models import Account, EmailService, RegistrationTask, Setting, Proxy, CpaService, Sub2ApiService
|
||||
from .models import Account, EmailService, RegistrationTask, Setting, Proxy, CpaService, Sub2ApiService, NewapiService
|
||||
|
||||
|
||||
TOKEN_FIELD_NAMES = ("access_token", "refresh_token", "id_token", "session_token")
|
||||
@@ -781,6 +781,63 @@ def delete_tm_service(db: Session, service_id: int) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def create_newapi_service(
|
||||
db: Session,
|
||||
name: str,
|
||||
api_url: str,
|
||||
api_key: str,
|
||||
enabled: bool = True,
|
||||
priority: int = 0,
|
||||
channel_type: int = 57,
|
||||
channel_base_url: str = "",
|
||||
channel_models: str = "gpt-5.4,gpt-5,gpt-5-codex,gpt-5-codex-mini,gpt-5.1,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini,gpt-5.2,gpt-5.2-codex,gpt-5.3-codex,gpt-5-openai-compact,gpt-5-codex-openai-compact,gpt-5-codex-mini-openai-compact,gpt-5.1-openai-compact,gpt-5.1-codex-openai-compact,gpt-5.1-codex-max-openai-compact,gpt-5.1-codex-mini-openai-compact,gpt-5.2-openai-compact,gpt-5.2-codex-openai-compact,gpt-5.3-codex-openai-compact",
|
||||
) -> NewapiService:
|
||||
svc = NewapiService(
|
||||
name=name,
|
||||
api_url=api_url,
|
||||
api_key=api_key,
|
||||
enabled=enabled,
|
||||
priority=priority,
|
||||
channel_type=channel_type,
|
||||
channel_base_url=channel_base_url,
|
||||
channel_models=channel_models,
|
||||
)
|
||||
db.add(svc)
|
||||
db.commit()
|
||||
db.refresh(svc)
|
||||
return svc
|
||||
|
||||
|
||||
def get_newapi_service_by_id(db: Session, service_id: int) -> Optional[NewapiService]:
|
||||
return db.query(NewapiService).filter(NewapiService.id == service_id).first()
|
||||
|
||||
|
||||
def get_newapi_services(db: Session, enabled=None):
|
||||
q = db.query(NewapiService)
|
||||
if enabled is not None:
|
||||
q = q.filter(NewapiService.enabled == enabled)
|
||||
return q.order_by(NewapiService.priority.asc(), NewapiService.id.asc()).all()
|
||||
|
||||
|
||||
def update_newapi_service(db: Session, service_id: int, **kwargs):
|
||||
svc = get_newapi_service_by_id(db, service_id)
|
||||
if not svc:
|
||||
return None
|
||||
for k, v in kwargs.items():
|
||||
setattr(svc, k, v)
|
||||
db.commit()
|
||||
db.refresh(svc)
|
||||
return svc
|
||||
|
||||
|
||||
def delete_newapi_service(db: Session, service_id: int) -> bool:
|
||||
svc = get_newapi_service_by_id(db, service_id)
|
||||
if not svc:
|
||||
return False
|
||||
db.delete(svc)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def update_outlook_refresh_token(db: Session, service_id: int, email: str, new_refresh_token: str):
|
||||
"""更新 EmailService.config 中指定邮箱的 refresh_token"""
|
||||
service = db.query(EmailService).filter(EmailService.id == service_id).first()
|
||||
|
||||
@@ -54,6 +54,8 @@ class Account(Base):
|
||||
extra_data = Column(JSONEncodedDict) # 额外信息存储
|
||||
cpa_uploaded = Column(Boolean, default=False) # 是否已上传到 CPA
|
||||
cpa_uploaded_at = Column(DateTime) # 上传时间
|
||||
newapi_uploaded = Column(Boolean, default=False)
|
||||
newapi_uploaded_at = Column(DateTime)
|
||||
source = Column(String(20), default='register') # 'register' 或 'login',区分账号来源
|
||||
subscription_type = Column(String(20)) # None / 'plus' / 'team'
|
||||
subscription_at = Column(DateTime) # 订阅开通时间
|
||||
@@ -78,6 +80,8 @@ class Account(Base):
|
||||
'proxy_used': self.proxy_used,
|
||||
'cpa_uploaded': self.cpa_uploaded,
|
||||
'cpa_uploaded_at': self.cpa_uploaded_at.isoformat() if self.cpa_uploaded_at else None,
|
||||
'newapi_uploaded': self.newapi_uploaded,
|
||||
'newapi_uploaded_at': self.newapi_uploaded_at.isoformat() if self.newapi_uploaded_at else None,
|
||||
'source': self.source,
|
||||
'subscription_type': self.subscription_type,
|
||||
'subscription_at': self.subscription_at.isoformat() if self.subscription_at else None,
|
||||
@@ -177,6 +181,23 @@ class TeamManagerService(Base):
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class NewapiService(Base):
|
||||
"""NEWAPI(如 New API)服务配置表"""
|
||||
__tablename__ = 'newapi_services'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
api_url = Column(String(500), nullable=False)
|
||||
api_key = Column(Text, nullable=False)
|
||||
channel_type = Column(Integer, default=57)
|
||||
channel_base_url = Column(String(500), default="")
|
||||
channel_models = Column(Text, default="gpt-5.4,gpt-5,gpt-5-codex,gpt-5-codex-mini,gpt-5.1,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini,gpt-5.2,gpt-5.2-codex,gpt-5.3-codex,gpt-5-openai-compact,gpt-5-codex-openai-compact,gpt-5-codex-mini-openai-compact,gpt-5.1-openai-compact,gpt-5.1-codex-openai-compact,gpt-5.1-codex-max-openai-compact,gpt-5.1-codex-mini-openai-compact,gpt-5.2-openai-compact,gpt-5.2-codex-openai-compact,gpt-5.3-codex-openai-compact")
|
||||
enabled = Column(Boolean, default=True)
|
||||
priority = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class Proxy(Base):
|
||||
"""代理列表表"""
|
||||
__tablename__ = 'proxies'
|
||||
|
||||
@@ -112,6 +112,11 @@ class DatabaseSessionManager:
|
||||
("accounts", "cookies", "TEXT"),
|
||||
("accounts", "token_sync_status", "VARCHAR(20) DEFAULT 'not_ready'"),
|
||||
("accounts", "token_sync_updated_at", "DATETIME"),
|
||||
("accounts", "newapi_uploaded", "BOOLEAN DEFAULT 0"),
|
||||
("accounts", "newapi_uploaded_at", "DATETIME"),
|
||||
("newapi_services", "channel_type", "INTEGER DEFAULT 57"),
|
||||
("newapi_services", "channel_base_url", "VARCHAR(500) DEFAULT ''"),
|
||||
("newapi_services", "channel_models", "TEXT"),
|
||||
("proxies", "is_default", "BOOLEAN DEFAULT 0"),
|
||||
("cpa_services", "include_proxy_url", "BOOLEAN DEFAULT 0"),
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ from .payment import router as payment_router
|
||||
from .upload.cpa_services import router as cpa_services_router
|
||||
from .upload.sub2api_services import router as sub2api_services_router
|
||||
from .upload.tm_services import router as tm_services_router
|
||||
from .upload.newapi_services import router as newapi_services_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -24,3 +25,4 @@ api_router.include_router(payment_router, prefix="/payment", tags=["payment"])
|
||||
api_router.include_router(cpa_services_router, prefix="/cpa-services", tags=["cpa-services"])
|
||||
api_router.include_router(sub2api_services_router, prefix="/sub2api-services", tags=["sub2api-services"])
|
||||
api_router.include_router(tm_services_router, prefix="/tm-services", tags=["tm-services"])
|
||||
api_router.include_router(newapi_services_router, prefix="/newapi-services", tags=["newapi-services"])
|
||||
|
||||
@@ -21,6 +21,7 @@ from ...core.openai.token_refresh import validate_account_token as do_validate
|
||||
from ...core.upload.cpa_upload import generate_token_json, batch_upload_to_cpa, upload_to_cpa
|
||||
from ...core.upload.team_manager_upload import upload_to_team_manager, batch_upload_to_team_manager
|
||||
from ...core.upload.sub2api_upload import batch_upload_to_sub2api, upload_to_sub2api
|
||||
from ...core.upload.newapi_upload import upload_to_newapi, batch_upload_to_newapi
|
||||
|
||||
from ...core.dynamic_proxy import get_proxy_url_for_task
|
||||
from ...database import crud
|
||||
@@ -155,6 +156,8 @@ class AccountResponse(BaseModel):
|
||||
proxy_used: Optional[str] = None
|
||||
cpa_uploaded: bool = False
|
||||
cpa_uploaded_at: Optional[str] = None
|
||||
newapi_uploaded: bool = False
|
||||
newapi_uploaded_at: Optional[str] = None
|
||||
cookies: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
@@ -234,6 +237,8 @@ def account_to_response(account: Account) -> AccountResponse:
|
||||
proxy_used=account.proxy_used,
|
||||
cpa_uploaded=account.cpa_uploaded or False,
|
||||
cpa_uploaded_at=account.cpa_uploaded_at.isoformat() if account.cpa_uploaded_at else None,
|
||||
newapi_uploaded=account.newapi_uploaded or False,
|
||||
newapi_uploaded_at=account.newapi_uploaded_at.isoformat() if account.newapi_uploaded_at else None,
|
||||
cookies=account.cookies,
|
||||
created_at=account.created_at.isoformat() if account.created_at else None,
|
||||
updated_at=account.updated_at.isoformat() if account.updated_at else None,
|
||||
@@ -1413,6 +1418,85 @@ async def upload_account_to_tm(account_id: int, request: Optional[UploadTMReques
|
||||
return {"success": success, "message": message}
|
||||
|
||||
|
||||
# ============== NEWAPI 上传 ==============
|
||||
|
||||
class UploadNewapiRequest(BaseModel):
|
||||
service_id: Optional[int] = None
|
||||
|
||||
|
||||
class BatchUploadNewapiRequest(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-newapi")
|
||||
async def batch_upload_accounts_to_newapi(request: BatchUploadNewapiRequest):
|
||||
with get_db() as db:
|
||||
if request.service_id:
|
||||
svc = crud.get_newapi_service_by_id(db, request.service_id)
|
||||
else:
|
||||
svcs = crud.get_newapi_services(db, enabled=True)
|
||||
svc = svcs[0] if svcs else None
|
||||
|
||||
if not svc:
|
||||
raise HTTPException(status_code=400, detail="未找到可用的 NEWAPI 服务,请先在设置中配置")
|
||||
|
||||
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_newapi(
|
||||
ids,
|
||||
api_url,
|
||||
api_key,
|
||||
channel_type=svc.channel_type,
|
||||
channel_base_url=svc.channel_base_url,
|
||||
channel_models=svc.channel_models,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/{account_id}/upload-newapi")
|
||||
async def upload_account_to_newapi(account_id: int, request: Optional[UploadNewapiRequest] = Body(default=None)):
|
||||
service_id = request.service_id if request else None
|
||||
|
||||
with get_db() as db:
|
||||
if service_id:
|
||||
svc = crud.get_newapi_service_by_id(db, service_id)
|
||||
else:
|
||||
svcs = crud.get_newapi_services(db, enabled=True)
|
||||
svc = svcs[0] if svcs else None
|
||||
|
||||
if not svc:
|
||||
raise HTTPException(status_code=400, detail="未找到可用的 NEWAPI 服务,请先在设置中配置")
|
||||
|
||||
account = crud.get_account_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
success, message = upload_to_newapi(
|
||||
account,
|
||||
svc.api_url,
|
||||
svc.api_key,
|
||||
channel_type=svc.channel_type,
|
||||
channel_base_url=svc.channel_base_url,
|
||||
channel_models=svc.channel_models,
|
||||
)
|
||||
if success:
|
||||
account.newapi_uploaded = True
|
||||
account.newapi_uploaded_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"success": success, "message": message}
|
||||
|
||||
|
||||
# ============== Inbox Code ==============
|
||||
|
||||
def _build_inbox_config(db, service_type, email: str) -> dict:
|
||||
|
||||
@@ -115,6 +115,8 @@ class RegistrationTaskCreate(BaseModel):
|
||||
sub2api_service_ids: List[int] = [] # 指定 Sub2API 服务 ID 列表
|
||||
auto_upload_tm: bool = False
|
||||
tm_service_ids: List[int] = [] # 指定 TM 服务 ID 列表
|
||||
auto_upload_newapi: bool = False
|
||||
newapi_service_ids: List[int] = []
|
||||
|
||||
|
||||
class BatchRegistrationRequest(BaseModel):
|
||||
@@ -134,6 +136,8 @@ class BatchRegistrationRequest(BaseModel):
|
||||
sub2api_service_ids: List[int] = []
|
||||
auto_upload_tm: bool = False
|
||||
tm_service_ids: List[int] = []
|
||||
auto_upload_newapi: bool = False
|
||||
newapi_service_ids: List[int] = []
|
||||
|
||||
|
||||
class MockRegistrationCreateRequest(BaseModel):
|
||||
@@ -216,6 +220,8 @@ class OutlookBatchRegistrationRequest(BaseModel):
|
||||
sub2api_service_ids: List[int] = []
|
||||
auto_upload_tm: bool = False
|
||||
tm_service_ids: List[int] = []
|
||||
auto_upload_newapi: bool = False
|
||||
newapi_service_ids: List[int] = []
|
||||
|
||||
|
||||
class OutlookBatchRegistrationResponse(BaseModel):
|
||||
@@ -527,7 +533,7 @@ def _build_email_service_candidates(
|
||||
return candidates
|
||||
|
||||
|
||||
def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict], email_service_id: Optional[int] = None, log_prefix: str = "", batch_id: str = "", auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, sub2api_service_ids: List[int] = None, auto_upload_tm: bool = False, tm_service_ids: List[int] = None):
|
||||
def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict], email_service_id: Optional[int] = None, log_prefix: str = "", batch_id: str = "", auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, sub2api_service_ids: List[int] = None, auto_upload_tm: bool = False, tm_service_ids: List[int] = None, auto_upload_newapi: bool = False, newapi_service_ids: List[int] = None):
|
||||
"""
|
||||
在线程池中执行的同步注册任务
|
||||
|
||||
@@ -774,6 +780,43 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
except Exception as tm_err:
|
||||
log_callback(f"[TM] 上传异常: {tm_err}")
|
||||
|
||||
if auto_upload_newapi:
|
||||
try:
|
||||
from ...core.upload.newapi_upload import upload_to_newapi
|
||||
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:
|
||||
_na_ids = newapi_service_ids or []
|
||||
if not _na_ids:
|
||||
_na_ids = [s.id for s in crud.get_newapi_services(db, enabled=True)]
|
||||
if not _na_ids:
|
||||
log_callback("[NEWAPI] 无可用 NEWAPI 服务,跳过上传")
|
||||
for _sid in _na_ids:
|
||||
try:
|
||||
_svc = crud.get_newapi_service_by_id(db, _sid)
|
||||
if not _svc:
|
||||
continue
|
||||
log_callback(f"[NEWAPI] 上传到服务: {_svc.name}")
|
||||
_ok, _msg = upload_to_newapi(
|
||||
saved_account,
|
||||
_svc.api_url,
|
||||
_svc.api_key,
|
||||
channel_type=_svc.channel_type,
|
||||
channel_base_url=_svc.channel_base_url,
|
||||
channel_models=_svc.channel_models,
|
||||
)
|
||||
if _ok:
|
||||
saved_account.newapi_uploaded = True
|
||||
saved_account.newapi_uploaded_at = datetime.utcnow()
|
||||
db.commit()
|
||||
log_callback(f"[NEWAPI] 上传成功: {_svc.name}")
|
||||
else:
|
||||
log_callback(f"[NEWAPI] 上传失败({_svc.name}): {_msg}")
|
||||
except Exception as _e:
|
||||
log_callback(f"[NEWAPI] 异常({_sid}): {_e}")
|
||||
except Exception as na_err:
|
||||
log_callback(f"[NEWAPI] 上传异常: {na_err}")
|
||||
|
||||
# 更新任务状态
|
||||
crud.update_registration_task(
|
||||
db, task_uuid,
|
||||
@@ -831,7 +874,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
pass
|
||||
|
||||
|
||||
async def run_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict], email_service_id: Optional[int] = None, log_prefix: str = "", batch_id: str = "", auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, sub2api_service_ids: List[int] = None, auto_upload_tm: bool = False, tm_service_ids: List[int] = None):
|
||||
async def run_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict], email_service_id: Optional[int] = None, log_prefix: str = "", batch_id: str = "", auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, sub2api_service_ids: List[int] = None, auto_upload_tm: bool = False, tm_service_ids: List[int] = None, auto_upload_newapi: bool = False, newapi_service_ids: List[int] = None):
|
||||
"""
|
||||
异步执行注册任务
|
||||
|
||||
@@ -864,6 +907,8 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
|
||||
sub2api_service_ids or [],
|
||||
auto_upload_tm,
|
||||
tm_service_ids or [],
|
||||
auto_upload_newapi,
|
||||
newapi_service_ids or [],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"线程池执行异常: {task_uuid}, 错误: {e}")
|
||||
@@ -1170,6 +1215,8 @@ async def run_batch_parallel(
|
||||
sub2api_service_ids: List[int] = None,
|
||||
auto_upload_tm: bool = False,
|
||||
tm_service_ids: List[int] = None,
|
||||
auto_upload_newapi: bool = False,
|
||||
newapi_service_ids: List[int] = None,
|
||||
):
|
||||
"""
|
||||
并行模式:所有任务同时提交,Semaphore 控制最大并发数
|
||||
@@ -1189,6 +1236,7 @@ async def run_batch_parallel(
|
||||
auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids or [],
|
||||
auto_upload_sub2api=auto_upload_sub2api, sub2api_service_ids=sub2api_service_ids or [],
|
||||
auto_upload_tm=auto_upload_tm, tm_service_ids=tm_service_ids or [],
|
||||
auto_upload_newapi=auto_upload_newapi, newapi_service_ids=newapi_service_ids or [],
|
||||
)
|
||||
with get_db() as db:
|
||||
t = crud.get_registration_task(db, uuid)
|
||||
@@ -1239,6 +1287,8 @@ async def run_batch_pipeline(
|
||||
sub2api_service_ids: List[int] = None,
|
||||
auto_upload_tm: bool = False,
|
||||
tm_service_ids: List[int] = None,
|
||||
auto_upload_newapi: bool = False,
|
||||
newapi_service_ids: List[int] = None,
|
||||
):
|
||||
"""
|
||||
流水线模式:每隔 interval 秒启动一个新任务,Semaphore 限制最大并发数
|
||||
@@ -1258,6 +1308,7 @@ async def run_batch_pipeline(
|
||||
auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids or [],
|
||||
auto_upload_sub2api=auto_upload_sub2api, sub2api_service_ids=sub2api_service_ids or [],
|
||||
auto_upload_tm=auto_upload_tm, tm_service_ids=tm_service_ids or [],
|
||||
auto_upload_newapi=auto_upload_newapi, newapi_service_ids=newapi_service_ids or [],
|
||||
)
|
||||
with get_db() as db:
|
||||
t = crud.get_registration_task(db, uuid)
|
||||
@@ -1332,6 +1383,8 @@ async def run_batch_registration(
|
||||
sub2api_service_ids: List[int] = None,
|
||||
auto_upload_tm: bool = False,
|
||||
tm_service_ids: List[int] = None,
|
||||
auto_upload_newapi: bool = False,
|
||||
newapi_service_ids: List[int] = None,
|
||||
):
|
||||
"""根据 mode 分发到并行或流水线执行"""
|
||||
if mode == "parallel":
|
||||
@@ -1341,6 +1394,7 @@ async def run_batch_registration(
|
||||
auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids,
|
||||
auto_upload_sub2api=auto_upload_sub2api, sub2api_service_ids=sub2api_service_ids,
|
||||
auto_upload_tm=auto_upload_tm, tm_service_ids=tm_service_ids,
|
||||
auto_upload_newapi=auto_upload_newapi, newapi_service_ids=newapi_service_ids,
|
||||
)
|
||||
else:
|
||||
await run_batch_pipeline(
|
||||
@@ -1350,6 +1404,7 @@ async def run_batch_registration(
|
||||
auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids,
|
||||
auto_upload_sub2api=auto_upload_sub2api, sub2api_service_ids=sub2api_service_ids,
|
||||
auto_upload_tm=auto_upload_tm, tm_service_ids=tm_service_ids,
|
||||
auto_upload_newapi=auto_upload_newapi, newapi_service_ids=newapi_service_ids,
|
||||
)
|
||||
|
||||
|
||||
@@ -1451,6 +1506,8 @@ async def start_registration(
|
||||
request.sub2api_service_ids,
|
||||
request.auto_upload_tm,
|
||||
request.tm_service_ids,
|
||||
request.auto_upload_newapi,
|
||||
request.newapi_service_ids,
|
||||
)
|
||||
|
||||
return task_to_response(task)
|
||||
@@ -1528,6 +1585,8 @@ async def start_batch_registration(
|
||||
request.sub2api_service_ids,
|
||||
request.auto_upload_tm,
|
||||
request.tm_service_ids,
|
||||
request.auto_upload_newapi,
|
||||
request.newapi_service_ids,
|
||||
)
|
||||
|
||||
return BatchRegistrationResponse(
|
||||
@@ -1925,6 +1984,8 @@ async def run_outlook_batch_registration(
|
||||
sub2api_service_ids: List[int] = None,
|
||||
auto_upload_tm: bool = False,
|
||||
tm_service_ids: List[int] = None,
|
||||
auto_upload_newapi: bool = False,
|
||||
newapi_service_ids: List[int] = None,
|
||||
):
|
||||
"""
|
||||
异步执行 Outlook 批量注册任务,复用通用并发逻辑
|
||||
@@ -1968,6 +2029,8 @@ async def run_outlook_batch_registration(
|
||||
sub2api_service_ids=sub2api_service_ids,
|
||||
auto_upload_tm=auto_upload_tm,
|
||||
tm_service_ids=tm_service_ids,
|
||||
auto_upload_newapi=auto_upload_newapi,
|
||||
newapi_service_ids=newapi_service_ids,
|
||||
)
|
||||
|
||||
|
||||
@@ -2066,6 +2129,8 @@ async def start_outlook_batch_registration(
|
||||
request.sub2api_service_ids,
|
||||
request.auto_upload_tm,
|
||||
request.tm_service_ids,
|
||||
request.auto_upload_newapi,
|
||||
request.newapi_service_ids,
|
||||
)
|
||||
|
||||
return OutlookBatchRegistrationResponse(
|
||||
|
||||
139
src/web/routes/upload/newapi_services.py
Normal file
139
src/web/routes/upload/newapi_services.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
NEWAPI 服务管理 API 路由
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ....database import crud
|
||||
from ....database.session import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class NewapiServiceCreate(BaseModel):
|
||||
name: str
|
||||
api_url: str
|
||||
api_key: str
|
||||
channel_type: int = 57
|
||||
channel_base_url: str = ""
|
||||
channel_models: str = "gpt-5.4,gpt-5,gpt-5-codex,gpt-5-codex-mini,gpt-5.1,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini,gpt-5.2,gpt-5.2-codex,gpt-5.3-codex,gpt-5-openai-compact,gpt-5-codex-openai-compact,gpt-5-codex-mini-openai-compact,gpt-5.1-openai-compact,gpt-5.1-codex-openai-compact,gpt-5.1-codex-max-openai-compact,gpt-5.1-codex-mini-openai-compact,gpt-5.2-openai-compact,gpt-5.2-codex-openai-compact,gpt-5.3-codex-openai-compact"
|
||||
enabled: bool = True
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class NewapiServiceUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
api_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
channel_type: Optional[int] = None
|
||||
channel_base_url: Optional[str] = None
|
||||
channel_models: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
priority: Optional[int] = None
|
||||
|
||||
|
||||
class NewapiServiceResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
api_url: str
|
||||
has_key: bool
|
||||
channel_type: int = 57
|
||||
channel_base_url: str = ""
|
||||
channel_models: str = ""
|
||||
enabled: bool
|
||||
priority: int
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def _to_response(svc) -> NewapiServiceResponse:
|
||||
return NewapiServiceResponse(
|
||||
id=svc.id,
|
||||
name=svc.name,
|
||||
api_url=svc.api_url,
|
||||
has_key=bool(svc.api_key),
|
||||
channel_type=svc.channel_type if svc.channel_type is not None else 57,
|
||||
channel_base_url=svc.channel_base_url or "",
|
||||
channel_models=svc.channel_models or "",
|
||||
enabled=svc.enabled,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=List[NewapiServiceResponse])
|
||||
async def list_newapi_services(enabled: Optional[bool] = None):
|
||||
with get_db() as db:
|
||||
services = crud.get_newapi_services(db, enabled=enabled)
|
||||
return [_to_response(s) for s in services]
|
||||
|
||||
|
||||
@router.post("", response_model=NewapiServiceResponse)
|
||||
async def create_newapi_service(request: NewapiServiceCreate):
|
||||
with get_db() as db:
|
||||
svc = crud.create_newapi_service(
|
||||
db,
|
||||
name=request.name,
|
||||
api_url=request.api_url,
|
||||
api_key=request.api_key,
|
||||
channel_type=request.channel_type,
|
||||
channel_base_url=request.channel_base_url,
|
||||
channel_models=request.channel_models,
|
||||
enabled=request.enabled,
|
||||
priority=request.priority,
|
||||
)
|
||||
return _to_response(svc)
|
||||
|
||||
|
||||
@router.get("/{service_id}", response_model=NewapiServiceResponse)
|
||||
async def get_newapi_service(service_id: int):
|
||||
with get_db() as db:
|
||||
svc = crud.get_newapi_service_by_id(db, service_id)
|
||||
if not svc:
|
||||
raise HTTPException(status_code=404, detail="NEWAPI 服务不存在")
|
||||
return _to_response(svc)
|
||||
|
||||
|
||||
@router.patch("/{service_id}", response_model=NewapiServiceResponse)
|
||||
async def update_newapi_service(service_id: int, request: NewapiServiceUpdate):
|
||||
with get_db() as db:
|
||||
svc = crud.get_newapi_service_by_id(db, service_id)
|
||||
if not svc:
|
||||
raise HTTPException(status_code=404, detail="NEWAPI 服务不存在")
|
||||
|
||||
update_data = {}
|
||||
if request.name is not None:
|
||||
update_data["name"] = request.name
|
||||
if request.api_url is not None:
|
||||
update_data["api_url"] = request.api_url
|
||||
if request.api_key:
|
||||
update_data["api_key"] = request.api_key
|
||||
if request.enabled is not None:
|
||||
update_data["enabled"] = request.enabled
|
||||
if request.priority is not None:
|
||||
update_data["priority"] = request.priority
|
||||
if request.channel_type is not None:
|
||||
update_data["channel_type"] = request.channel_type
|
||||
if request.channel_base_url is not None:
|
||||
update_data["channel_base_url"] = request.channel_base_url
|
||||
if request.channel_models is not None:
|
||||
update_data["channel_models"] = request.channel_models
|
||||
|
||||
svc = crud.update_newapi_service(db, service_id, **update_data)
|
||||
return _to_response(svc)
|
||||
|
||||
|
||||
@router.delete("/{service_id}")
|
||||
async def delete_newapi_service(service_id: int):
|
||||
with get_db() as db:
|
||||
svc = crud.get_newapi_service_by_id(db, service_id)
|
||||
if not svc:
|
||||
raise HTTPException(status_code=404, detail="NEWAPI 服务不存在")
|
||||
crud.delete_newapi_service(db, service_id)
|
||||
return {"success": True, "message": f"NEWAPI 服务 {svc.name} 已删除"}
|
||||
@@ -107,6 +107,7 @@ function initEventListeners() {
|
||||
document.getElementById('batch-upload-cpa-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadCpa(); });
|
||||
document.getElementById('batch-upload-sub2api-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadSub2Api(); });
|
||||
document.getElementById('batch-upload-tm-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadTm(); });
|
||||
document.getElementById('batch-upload-newapi-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadNewapi(); });
|
||||
|
||||
// 批量删除
|
||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||
@@ -332,6 +333,13 @@ function renderAccounts(accounts) {
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cpa-status">
|
||||
${account.newapi_uploaded
|
||||
? `<span class="badge uploaded" title="已上传于 ${format.date(account.newapi_uploaded_at)}">✓</span>`
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cpa-status">
|
||||
${account.subscription_type
|
||||
@@ -869,6 +877,7 @@ async function uploadAccount(id) {
|
||||
{ label: '☁️ 上传到 CPA', value: 'cpa' },
|
||||
{ label: '🔗 上传到 Sub2API', value: 'sub2api' },
|
||||
{ label: '🚀 上传到 Team Manager', value: 'tm' },
|
||||
{ label: '🧩 上传到 NEWAPI', value: 'newapi' },
|
||||
];
|
||||
|
||||
const choice = await new Promise((resolve) => {
|
||||
@@ -898,6 +907,7 @@ async function uploadAccount(id) {
|
||||
if (choice === 'cpa') return uploadToCpa(id);
|
||||
if (choice === 'sub2api') return uploadToSub2Api(id);
|
||||
if (choice === 'tm') return uploadToTm(id);
|
||||
if (choice === 'newapi') return uploadToNewapi(id);
|
||||
}
|
||||
|
||||
// 上传单个账号到CPA
|
||||
@@ -1237,6 +1247,120 @@ async function handleBatchUploadTm() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============== NEWAPI 上传 ==============
|
||||
|
||||
function selectNewapiService() {
|
||||
return new Promise(async (resolve) => {
|
||||
const modal = document.getElementById('newapi-service-modal');
|
||||
const listEl = document.getElementById('newapi-service-list');
|
||||
const closeBtn = document.getElementById('close-newapi-modal');
|
||||
const cancelBtn = document.getElementById('cancel-newapi-modal-btn');
|
||||
const autoBtn = document.getElementById('newapi-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('/newapi-services?enabled=true');
|
||||
} catch (e) {
|
||||
services = [];
|
||||
}
|
||||
|
||||
if (services.length === 0) {
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:12px;">暂无已启用的 NEWAPI 服务,将自动选择第一个</div>';
|
||||
} else {
|
||||
listEl.innerHTML = services.map(s => `
|
||||
<div class="newapi-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('.newapi-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);
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadToNewapi(id) {
|
||||
const choice = await selectNewapiService();
|
||||
if (choice === null) return;
|
||||
try {
|
||||
toast.info('正在上传到 NEWAPI...');
|
||||
const payload = {};
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
const result = await api.post(`/accounts/${id}/upload-newapi`, payload);
|
||||
if (result.success) {
|
||||
toast.success('上传成功');
|
||||
} else {
|
||||
toast.error('上传失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('上传失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchUploadNewapi() {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const choice = await selectNewapiService();
|
||||
if (choice === null) return;
|
||||
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 NEWAPI 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchUploadBtn.disabled = true;
|
||||
elements.batchUploadBtn.textContent = '上传中...';
|
||||
|
||||
try {
|
||||
const payload = buildBatchPayload();
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
const result = await api.post('/accounts/batch-upload-newapi', payload);
|
||||
let message = `成功: ${result.success_count}`;
|
||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;
|
||||
toast.success(message);
|
||||
loadAccounts();
|
||||
} catch (e) {
|
||||
toast.error('批量上传失败: ' + e.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 更多菜单切换
|
||||
function toggleMoreMenu(btn) {
|
||||
const menu = btn.nextElementSibling;
|
||||
|
||||
@@ -94,6 +94,9 @@ const elements = {
|
||||
autoUploadTm: document.getElementById('auto-upload-tm'),
|
||||
tmServiceSelectGroup: document.getElementById('tm-service-select-group'),
|
||||
tmServiceSelect: document.getElementById('tm-service-select'),
|
||||
autoUploadNewapi: document.getElementById('auto-upload-newapi'),
|
||||
newapiServiceSelectGroup: document.getElementById('newapi-service-select-group'),
|
||||
newapiServiceSelect: document.getElementById('newapi-service-select'),
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -113,6 +116,7 @@ async function initAutoUploadOptions() {
|
||||
loadServiceSelect('/cpa-services?enabled=true', elements.cpaServiceSelect, elements.autoUploadCpa, elements.cpaServiceSelectGroup),
|
||||
loadServiceSelect('/sub2api-services?enabled=true', elements.sub2apiServiceSelect, elements.autoUploadSub2api, elements.sub2apiServiceSelectGroup),
|
||||
loadServiceSelect('/tm-services?enabled=true', elements.tmServiceSelect, elements.autoUploadTm, elements.tmServiceSelectGroup),
|
||||
loadServiceSelect('/newapi-services?enabled=true', elements.newapiServiceSelect, elements.autoUploadNewapi, elements.newapiServiceSelectGroup),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -480,6 +484,8 @@ async function handleStartRegistration(e) {
|
||||
sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
|
||||
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
|
||||
tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
|
||||
auto_upload_newapi: elements.autoUploadNewapi ? elements.autoUploadNewapi.checked : false,
|
||||
newapi_service_ids: elements.autoUploadNewapi && elements.autoUploadNewapi.checked ? getSelectedServiceIds(elements.newapiServiceSelect) : [],
|
||||
};
|
||||
|
||||
// 如果选择了数据库中的服务,传递 service_id
|
||||
@@ -1196,6 +1202,8 @@ async function handleOutlookBatchRegistration() {
|
||||
sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
|
||||
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
|
||||
tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
|
||||
auto_upload_newapi: elements.autoUploadNewapi ? elements.autoUploadNewapi.checked : false,
|
||||
newapi_service_ids: elements.autoUploadNewapi && elements.autoUploadNewapi.checked ? getSelectedServiceIds(elements.newapiServiceSelect) : [],
|
||||
};
|
||||
|
||||
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
|
||||
|
||||
@@ -66,6 +66,13 @@ const elements = {
|
||||
tmServiceForm: document.getElementById('tm-service-form'),
|
||||
tmServiceModalTitle: document.getElementById('tm-service-modal-title'),
|
||||
testTmServiceBtn: document.getElementById('test-tm-service-btn'),
|
||||
addNewapiServiceBtn: document.getElementById('add-newapi-service-btn'),
|
||||
newapiServicesTable: document.getElementById('newapi-services-table'),
|
||||
newapiServiceEditModal: document.getElementById('newapi-service-edit-modal'),
|
||||
closeNewapiServiceModal: document.getElementById('close-newapi-service-modal'),
|
||||
cancelNewapiServiceBtn: document.getElementById('cancel-newapi-service-btn'),
|
||||
newapiServiceForm: document.getElementById('newapi-service-form'),
|
||||
newapiServiceModalTitle: document.getElementById('newapi-service-modal-title'),
|
||||
// 验证码设置
|
||||
emailCodeForm: document.getElementById('email-code-form'),
|
||||
// Outlook 设置
|
||||
@@ -87,6 +94,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadCpaServices();
|
||||
loadSub2ApiServices();
|
||||
loadTmServices();
|
||||
loadNewapiServices();
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
@@ -269,6 +277,23 @@ function initEventListeners() {
|
||||
elements.testTmServiceBtn.addEventListener('click', handleTestTmService);
|
||||
}
|
||||
|
||||
if (elements.addNewapiServiceBtn) {
|
||||
elements.addNewapiServiceBtn.addEventListener('click', () => openNewapiServiceModal());
|
||||
}
|
||||
if (elements.closeNewapiServiceModal) {
|
||||
elements.closeNewapiServiceModal.addEventListener('click', closeNewapiServiceModal);
|
||||
}
|
||||
if (elements.cancelNewapiServiceBtn) {
|
||||
elements.cancelNewapiServiceBtn.addEventListener('click', closeNewapiServiceModal);
|
||||
}
|
||||
if (elements.newapiServiceEditModal) {
|
||||
elements.newapiServiceEditModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.newapiServiceEditModal) closeNewapiServiceModal();
|
||||
});
|
||||
}
|
||||
if (elements.newapiServiceForm) {
|
||||
elements.newapiServiceForm.addEventListener('submit', handleSaveNewapiService);
|
||||
}
|
||||
// CPA 服务管理
|
||||
if (elements.addCpaServiceBtn) {
|
||||
elements.addCpaServiceBtn.addEventListener('click', () => openCpaServiceModal());
|
||||
@@ -1215,6 +1240,132 @@ async function handleTestTmService() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNewapiServices() {
|
||||
if (!elements.newapiServicesTable) return;
|
||||
try {
|
||||
const services = await api.get('/newapi-services');
|
||||
renderNewapiServicesTable(services);
|
||||
} catch (e) {
|
||||
elements.newapiServicesTable.innerHTML = `<tr><td colspan="8" style="text-align:center;color:var(--danger-color);">${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewapiServicesTable(services) {
|
||||
if (!services || services.length === 0) {
|
||||
elements.newapiServicesTable.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-muted);padding:20px;">暂无 NEWAPI 服务,点击「添加服务」新增</td></tr>';
|
||||
return;
|
||||
}
|
||||
elements.newapiServicesTable.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.channel_type || 57}</td>
|
||||
<td style="font-size:0.85rem;color:var(--text-muted);">${escapeHtml(s.channel_base_url || '')}</td>
|
||||
<td style="font-size:0.8rem;color:var(--text-muted);max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(s.channel_models || '')}">${escapeHtml(s.channel_models || '')}</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;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="editNewapiService(${s.id})">编辑</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteNewapiService(${s.id}, '${escapeHtml(s.name)}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openNewapiServiceModal(service = null) {
|
||||
const defaultModels = 'gpt-5.4,gpt-5,gpt-5-codex,gpt-5-codex-mini,gpt-5.1,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini,gpt-5.2,gpt-5.2-codex,gpt-5.3-codex,gpt-5-openai-compact,gpt-5-codex-openai-compact,gpt-5-codex-mini-openai-compact,gpt-5.1-openai-compact,gpt-5.1-codex-openai-compact,gpt-5.1-codex-max-openai-compact,gpt-5.1-codex-mini-openai-compact,gpt-5.2-openai-compact,gpt-5.2-codex-openai-compact,gpt-5.3-codex-openai-compact';
|
||||
document.getElementById('newapi-service-id').value = service ? service.id : '';
|
||||
document.getElementById('newapi-service-name').value = service ? service.name : '';
|
||||
document.getElementById('newapi-service-url').value = service ? service.api_url : '';
|
||||
document.getElementById('newapi-service-key').value = '';
|
||||
document.getElementById('newapi-service-channel-type').value = service ? (service.channel_type || 57) : 57;
|
||||
document.getElementById('newapi-service-channel-base-url').value = service ? (service.channel_base_url || '') : '';
|
||||
document.getElementById('newapi-service-channel-models').value = service ? (service.channel_models || defaultModels) : defaultModels;
|
||||
document.getElementById('newapi-service-priority').value = service ? service.priority : 0;
|
||||
document.getElementById('newapi-service-enabled').checked = service ? service.enabled : true;
|
||||
if (service) {
|
||||
document.getElementById('newapi-service-key').placeholder = service.has_key ? '已配置,留空保持不变' : '请输入 Root Token / API Key';
|
||||
} else {
|
||||
document.getElementById('newapi-service-key').placeholder = '请输入 Root Token / API Key';
|
||||
}
|
||||
elements.newapiServiceModalTitle.textContent = service ? '编辑 NEWAPI 服务' : '添加 NEWAPI 服务';
|
||||
elements.newapiServiceEditModal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeNewapiServiceModal() {
|
||||
elements.newapiServiceEditModal.classList.remove('active');
|
||||
}
|
||||
|
||||
async function editNewapiService(id) {
|
||||
try {
|
||||
const service = await api.get(`/newapi-services/${id}`);
|
||||
openNewapiServiceModal(service);
|
||||
} catch (e) {
|
||||
toast.error('获取服务信息失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveNewapiService(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('newapi-service-id').value;
|
||||
const name = document.getElementById('newapi-service-name').value.trim();
|
||||
const apiUrl = document.getElementById('newapi-service-url').value.trim();
|
||||
const apiKey = document.getElementById('newapi-service-key').value.trim();
|
||||
const channelType = parseInt(document.getElementById('newapi-service-channel-type').value) || 57;
|
||||
const channelBaseUrl = document.getElementById('newapi-service-channel-base-url').value.trim();
|
||||
const channelModels = document.getElementById('newapi-service-channel-models').value.trim();
|
||||
const priority = parseInt(document.getElementById('newapi-service-priority').value) || 0;
|
||||
const enabled = document.getElementById('newapi-service-enabled').checked;
|
||||
|
||||
if (!name || !apiUrl) {
|
||||
toast.error('名称和 API URL 不能为空');
|
||||
return;
|
||||
}
|
||||
if (!id && !apiKey) {
|
||||
toast.error('新增服务时 Root Token / API Key 不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
api_url: apiUrl,
|
||||
priority,
|
||||
enabled,
|
||||
channel_type: channelType,
|
||||
channel_base_url: channelBaseUrl,
|
||||
channel_models: channelModels,
|
||||
};
|
||||
if (apiKey) payload.api_key = apiKey;
|
||||
|
||||
if (id) {
|
||||
await api.patch(`/newapi-services/${id}`, payload);
|
||||
toast.success('服务已更新');
|
||||
} else {
|
||||
payload.api_key = apiKey;
|
||||
await api.post('/newapi-services', payload);
|
||||
toast.success('服务已添加');
|
||||
}
|
||||
closeNewapiServiceModal();
|
||||
loadNewapiServices();
|
||||
} catch (e) {
|
||||
toast.error('保存失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNewapiService(id, name) {
|
||||
const confirmed = await confirm(`确定要删除 NEWAPI 服务「${name}」吗?`);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await api.delete(`/newapi-services/${id}`);
|
||||
toast.success('已删除');
|
||||
loadNewapiServices();
|
||||
} catch (e) {
|
||||
toast.error('删除失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============== CPA 服务管理 ==============
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
<a href="#" class="dropdown-item" id="batch-upload-cpa-item">☁️ 上传到 CPA</a>
|
||||
<a href="#" class="dropdown-item" id="batch-upload-sub2api-item">🔗 上传到 Sub2API</a>
|
||||
<a href="#" class="dropdown-item" id="batch-upload-tm-item">🚀 上传到 Team Manager</a>
|
||||
<a href="#" class="dropdown-item" id="batch-upload-newapi-item">🧩 上传到 NEWAPI</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger" id="batch-delete-btn" disabled>
|
||||
@@ -174,6 +175,7 @@
|
||||
<th style="width: 120px;">邮箱服务</th>
|
||||
<th style="width: 80px;">状态</th>
|
||||
<th style="width: 80px;">CPA</th>
|
||||
<th style="width: 80px;">NEWAPI</th>
|
||||
<th style="width: 80px;">订阅</th>
|
||||
<th style="width: 140px;">最后刷新</th>
|
||||
<th style="width: 160px;">操作</th>
|
||||
@@ -298,6 +300,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- NEWAPI 服务选择模态框 -->
|
||||
<div class="modal" id="newapi-service-modal">
|
||||
<div class="modal-content" style="max-width: 480px;">
|
||||
<div class="modal-header">
|
||||
<h3>🧩 选择 NEWAPI 服务</h3>
|
||||
<button class="modal-close" id="close-newapi-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: var(--text-muted); margin-bottom: 12px; font-size: 0.9rem;">选择要上传到的 NEWAPI 服务,或自动选择第一个启用的服务。</p>
|
||||
<div id="newapi-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="newapi-use-auto-btn">自动选择</button>
|
||||
<button class="btn btn-secondary" id="cancel-newapi-modal-btn">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/utils.js?v={{ static_version }}"></script>
|
||||
<script src="/static/js/accounts.js?v={{ static_version }}"></script>
|
||||
|
||||
@@ -301,6 +301,15 @@
|
||||
<div id="tm-service-select-group" style="display:none; margin-top: 6px; padding-left: 4px;">
|
||||
<div class="multi-select-dropdown" id="tm-service-select"></div>
|
||||
</div>
|
||||
|
||||
<!-- NEWAPI -->
|
||||
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer; margin-top: 6px;">
|
||||
<input type="checkbox" id="auto-upload-newapi">
|
||||
<span>上传到 NEWAPI</span>
|
||||
</label>
|
||||
<div id="newapi-service-select-group" style="display:none; margin-top: 6px; padding-left: 4px;">
|
||||
<div class="multi-select-dropdown" id="newapi-service-select"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions" style="flex-direction: column;">
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传服务设置(CPA + Sub2API + Team Manager) -->
|
||||
<!-- 上传服务设置(CPA + Sub2API + Team Manager + NEWAPI) -->
|
||||
<div class="tab-content" id="upload-tab">
|
||||
<!-- CPA 服务管理 -->
|
||||
<div class="card" style="margin-top: var(--spacing-lg);">
|
||||
@@ -281,6 +281,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top: var(--spacing-lg);">
|
||||
<div class="card-header">
|
||||
<h3>🧩 NEWAPI 服务</h3>
|
||||
<button class="btn btn-primary btn-sm" id="add-newapi-service-btn">+ 添加服务</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:150px;">名称</th>
|
||||
<th>API URL</th>
|
||||
<th style="width:90px;text-align:center;">Type</th>
|
||||
<th style="width:150px;">Base URL</th>
|
||||
<th>Models</th>
|
||||
<th style="width:80px;">状态</th>
|
||||
<th style="width:60px;text-align:center;">优先级</th>
|
||||
<th style="width:220px;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="newapi-services-table">
|
||||
<tr><td colspan="8" style="text-align:center;color:var(--text-muted);padding:20px;">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Manager 服务编辑模态框 -->
|
||||
@@ -423,6 +451,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="newapi-service-edit-modal">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="newapi-service-modal-title">添加 NEWAPI 服务</h3>
|
||||
<button class="modal-close" id="close-newapi-service-modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="newapi-service-form">
|
||||
<input type="hidden" id="newapi-service-id">
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-name">名称 *</label>
|
||||
<input type="text" id="newapi-service-name" placeholder="例如: 自建 New API" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-url">API URL *</label>
|
||||
<input type="text" id="newapi-service-url" placeholder="https://newapi.example.com" required>
|
||||
<p class="hint">填写 New API 根地址;连接测试为 <code>GET /api/user/login</code>,请求头 <code>Authorization: Bearer</code>(与<a href="https://docs.newapi.pro/zh/docs/api/management/auth" target="_blank" rel="noopener">鉴权说明</a>一致,Token 来自系统访问令牌)</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-key">Root Token / API Key *</label>
|
||||
<input type="password" id="newapi-service-key" placeholder="编辑时留空则保持原值" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-channel-type">渠道 Type</label>
|
||||
<input type="number" id="newapi-service-channel-type" value="57" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-channel-base-url">渠道 Base URL</label>
|
||||
<input type="text" id="newapi-service-channel-base-url" placeholder="留空使用默认">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-channel-models">渠道 Models</label>
|
||||
<input type="text" id="newapi-service-channel-models" placeholder="逗号分隔模型列表">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newapi-service-priority">优先级</label>
|
||||
<input type="number" id="newapi-service-priority" value="0" min="0">
|
||||
<p class="hint">数字越小优先级越高</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label> </label>
|
||||
<label style="margin-top:10px;display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="newapi-service-enabled" checked> 启用
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">💾 保存</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-newapi-service-btn">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outlook 配置 -->
|
||||
<div class="tab-content" id="outlook-tab">
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user