From 05e480a75613a26a24009de9c3cc8e309194c301 Mon Sep 17 00:00:00 2001 From: Jay Hsueh Date: Fri, 20 Mar 2026 15:05:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(newapi):=20=E6=B7=BB=E5=8A=A0=20NEWAPI?= =?UTF-8?q?=20=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=8F=8A=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `newapi_upload.py` 文件,包含上传到 NEWAPI 的功能。 - 在数据库模型中添加 `NewapiService` 表及相关字段。 - 更新 CRUD 操作以支持 NEWAPI 服务的创建、更新、查询和删除。 - 添加新的 API 路由以管理 NEWAPI 服务。 - 前端实现批量上传和单个账号上传到 NEWAPI 的功能。 - 更新相关页面以支持 NEWAPI 服务的选择和管理。 --- src/core/upload/newapi_upload.py | 126 +++++++++++++++++++++ src/database/crud.py | 53 ++++++++- src/database/models.py | 18 +++ src/database/session.py | 2 + src/web/routes/__init__.py | 2 + src/web/routes/accounts.py | 70 ++++++++++++ src/web/routes/registration.py | 62 ++++++++++- src/web/routes/upload/newapi_services.py | 118 ++++++++++++++++++++ static/js/accounts.js | 124 +++++++++++++++++++++ static/js/app.js | 8 ++ static/js/settings.js | 133 +++++++++++++++++++++++ templates/accounts.html | 22 ++++ templates/index.html | 9 ++ templates/settings.html | 71 +++++++++++- 14 files changed, 814 insertions(+), 4 deletions(-) create mode 100644 src/core/upload/newapi_upload.py create mode 100644 src/web/routes/upload/newapi_services.py diff --git a/src/core/upload/newapi_upload.py b/src/core/upload/newapi_upload.py new file mode 100644 index 0000000..983cd07 --- /dev/null +++ b/src/core/upload/newapi_upload.py @@ -0,0 +1,126 @@ +""" +NEWAPI 上传功能 — 通过 PUT /api/channel/ 添加渠道 +""" + +import json +import logging +from datetime import datetime +from typing import List, Tuple + +from curl_cffi import requests as cffi_requests + +from ...database.models import Account +from ...database.session import get_db + +logger = logging.getLogger(__name__) + +NEWAPI_TYPE_OPENAI = 57 +DEFAULT_BASE_URL = "" +DEFAULT_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, +) -> 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" + + url = f"{base}/api/channel/" + account_name = account.email or "" + channel = { + "auto_ban": 1, + "name": account.email or "", + "type": NEWAPI_TYPE_OPENAI, + "key": json.dumps({"access_token": account.access_token or "", "account_id": account_name}, ensure_ascii=False), + "base_url": DEFAULT_BASE_URL, + "models": DEFAULT_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, +) -> 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) + 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 diff --git a/src/database/crud.py b/src/database/crud.py index e7731f9..d4672a3 100644 --- a/src/database/crud.py +++ b/src/database/crud.py @@ -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,57 @@ 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, +) -> NewapiService: + svc = NewapiService( + name=name, + api_url=api_url, + api_key=api_key, + enabled=enabled, + priority=priority, + ) + 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() diff --git a/src/database/models.py b/src/database/models.py index 832cfa8..e7614dc 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -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,20 @@ 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) + 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' diff --git a/src/database/session.py b/src/database/session.py index 4a30cc1..3eb148a 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -112,6 +112,8 @@ 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"), ("proxies", "is_default", "BOOLEAN DEFAULT 0"), ("cpa_services", "include_proxy_url", "BOOLEAN DEFAULT 0"), ] diff --git a/src/web/routes/__init__.py b/src/web/routes/__init__.py index 7748775..c6100cd 100644 --- a/src/web/routes/__init__.py +++ b/src/web/routes/__init__.py @@ -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"]) diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index 7b4aa6f..0a7091a 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -19,6 +19,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 @@ -61,6 +62,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 @@ -140,6 +143,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, @@ -974,6 +979,71 @@ 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) + 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) + 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: diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index a497bbf..06f96ff 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -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,36 @@ 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) + 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 +867,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 +900,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 +1208,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 +1229,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 +1280,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 +1301,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 +1376,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 +1387,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 +1397,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 +1499,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 +1578,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 +1977,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 +2022,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 +2122,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( diff --git a/src/web/routes/upload/newapi_services.py b/src/web/routes/upload/newapi_services.py new file mode 100644 index 0000000..90dbf70 --- /dev/null +++ b/src/web/routes/upload/newapi_services.py @@ -0,0 +1,118 @@ +""" +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 + enabled: bool = True + priority: int = 0 + + +class NewapiServiceUpdate(BaseModel): + name: Optional[str] = None + api_url: Optional[str] = None + api_key: Optional[str] = None + enabled: Optional[bool] = None + priority: Optional[int] = None + + +class NewapiServiceResponse(BaseModel): + id: int + name: str + api_url: str + has_key: bool + 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), + 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, + 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 + + 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} 已删除"} diff --git a/static/js/accounts.js b/static/js/accounts.js index 10f83d4..1a4519a 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -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); @@ -314,6 +315,13 @@ function renderAccounts(accounts) { : `-`} + +
+ ${account.newapi_uploaded + ? `` + : `-`} +
+
${account.subscription_type @@ -841,6 +849,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) => { @@ -870,6 +879,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 @@ -1209,6 +1219,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 = '
加载中...
'; + modal.classList.add('active'); + + let services = []; + try { + services = await api.get('/newapi-services?enabled=true'); + } catch (e) { + services = []; + } + + if (services.length === 0) { + listEl.innerHTML = '
暂无已启用的 NEWAPI 服务,将自动选择第一个
'; + } else { + listEl.innerHTML = services.map(s => ` +
+
+
${escapeHtml(s.name)}
+
${escapeHtml(s.api_url)}
+
+ 选择 +
+ `).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; diff --git a/static/js/app.js b/static/js/app.js index 5571b88..6c1976a 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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} 个账户)...`); diff --git a/static/js/settings.js b/static/js/settings.js index 7835098..3065ae7 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -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,114 @@ 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 = `${e.message}`; + } +} + +function renderNewapiServicesTable(services) { + if (!services || services.length === 0) { + elements.newapiServicesTable.innerHTML = '暂无 NEWAPI 服务,点击「添加服务」新增'; + return; + } + elements.newapiServicesTable.innerHTML = services.map(s => ` + + ${escapeHtml(s.name)} + ${escapeHtml(s.api_url)} + ${s.enabled ? '✅' : '⭕'} + ${s.priority} + + + + + + `).join(''); +} + +function openNewapiServiceModal(service = null) { + 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-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 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 }; + 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 服务管理 ============== diff --git a/templates/accounts.html b/templates/accounts.html index ed25960..3448f1e 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -136,6 +136,7 @@ ☁️ 上传到 CPA 🔗 上传到 Sub2API 🚀 上传到 Team Manager + 🧩 上传到 NEWAPI
+ + + + + + diff --git a/templates/index.html b/templates/index.html index 7023383..9d23815 100644 --- a/templates/index.html +++ b/templates/index.html @@ -301,6 +301,15 @@ + + + +
diff --git a/templates/settings.html b/templates/settings.html index 0233f47..1d1bbe7 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -201,7 +201,7 @@
- +
@@ -281,6 +281,31 @@
+ +
+
+

🧩 NEWAPI 服务

+ +
+
+
+ + + + + + + + + + + + + +
名称API URL状态优先级操作
加载中...
+
+
+
@@ -423,6 +448,50 @@ + +
From 9c6e0d60362fb15b9a740439f3c594fec18efde7 Mon Sep 17 00:00:00 2001 From: Jay Hsueh Date: Wed, 25 Mar 2026 15:18:05 +0800 Subject: [PATCH 2/2] feat(newapi): enhance NEWAPI service management with channel configuration - Added channel_type, channel_base_url, and channel_models fields to the NewapiService model. - Updated CRUD operations to handle new fields for creating and updating NEWAPI services. - Modified upload functions to accept channel configuration parameters. - Enhanced front-end forms and tables to display and manage channel settings for NEWAPI services. - Improved error handling and user feedback in the UI for service management. --- src/core/upload/newapi_upload.py | 33 ++++++++++++++++++------ src/database/crud.py | 6 +++++ src/database/models.py | 3 +++ src/database/session.py | 3 +++ src/web/routes/accounts.py | 18 +++++++++++-- src/web/routes/registration.py | 9 ++++++- src/web/routes/upload/newapi_services.py | 21 +++++++++++++++ static/js/settings.js | 24 ++++++++++++++--- templates/settings.html | 19 +++++++++++++- 9 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/core/upload/newapi_upload.py b/src/core/upload/newapi_upload.py index 983cd07..3e94ee3 100644 --- a/src/core/upload/newapi_upload.py +++ b/src/core/upload/newapi_upload.py @@ -5,7 +5,7 @@ NEWAPI 上传功能 — 通过 PUT /api/channel/ 添加渠道 import json import logging from datetime import datetime -from typing import List, Tuple +from typing import List, Optional, Tuple from curl_cffi import requests as cffi_requests @@ -14,9 +14,9 @@ from ...database.session import get_db logger = logging.getLogger(__name__) -NEWAPI_TYPE_OPENAI = 57 -DEFAULT_BASE_URL = "" -DEFAULT_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" +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: @@ -46,6 +46,9 @@ 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: @@ -55,15 +58,19 @@ def upload_to_newapi( 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": NEWAPI_TYPE_OPENAI, + "type": resolved_channel_type, "key": json.dumps({"access_token": account.access_token or "", "account_id": account_name}, ensure_ascii=False), - "base_url": DEFAULT_BASE_URL, - "models": DEFAULT_MODELS, + "base_url": resolved_channel_base_url, + "models": resolved_channel_models, "multi_key_mode": "random", "group": "default", "groups": ["default"], @@ -92,6 +99,9 @@ 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, @@ -112,7 +122,14 @@ def batch_upload_to_newapi( results["details"].append({"id": account_id, "email": account.email, "success": False, "error": "缺少 Token"}) continue - success, message = upload_to_newapi(account, api_url, api_key) + 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() diff --git a/src/database/crud.py b/src/database/crud.py index d4672a3..95de676 100644 --- a/src/database/crud.py +++ b/src/database/crud.py @@ -788,6 +788,9 @@ def create_newapi_service( 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, @@ -795,6 +798,9 @@ def create_newapi_service( 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() diff --git a/src/database/models.py b/src/database/models.py index e7614dc..6c658cf 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -189,6 +189,9 @@ class NewapiService(Base): 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) diff --git a/src/database/session.py b/src/database/session.py index 3eb148a..eefa228 100644 --- a/src/database/session.py +++ b/src/database/session.py @@ -114,6 +114,9 @@ class DatabaseSessionManager: ("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"), ] diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index 0a7091a..21a49c5 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -1014,7 +1014,14 @@ async def batch_upload_accounts_to_newapi(request: BatchUploadNewapiRequest): request.status_filter, request.email_service_filter, request.search_filter ) - results = batch_upload_to_newapi(ids, api_url, api_key) + 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 @@ -1035,7 +1042,14 @@ async def upload_account_to_newapi(account_id: int, request: Optional[UploadNewa 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) + 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() diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index 06f96ff..f1979d3 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -797,7 +797,14 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: if not _svc: continue log_callback(f"[NEWAPI] 上传到服务: {_svc.name}") - _ok, _msg = upload_to_newapi(saved_account, _svc.api_url, _svc.api_key) + _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() diff --git a/src/web/routes/upload/newapi_services.py b/src/web/routes/upload/newapi_services.py index 90dbf70..a36510f 100644 --- a/src/web/routes/upload/newapi_services.py +++ b/src/web/routes/upload/newapi_services.py @@ -16,6 +16,9 @@ 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 @@ -24,6 +27,9 @@ 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 @@ -33,6 +39,9 @@ class NewapiServiceResponse(BaseModel): 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 @@ -48,6 +57,9 @@ def _to_response(svc) -> NewapiServiceResponse: 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, @@ -70,6 +82,9 @@ async def create_newapi_service(request: NewapiServiceCreate): 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, ) @@ -103,6 +118,12 @@ async def update_newapi_service(service_id: int, request: NewapiServiceUpdate): 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) diff --git a/static/js/settings.js b/static/js/settings.js index 3065ae7..d3546eb 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1246,19 +1246,22 @@ async function loadNewapiServices() { const services = await api.get('/newapi-services'); renderNewapiServicesTable(services); } catch (e) { - elements.newapiServicesTable.innerHTML = `${e.message}`; + elements.newapiServicesTable.innerHTML = `${e.message}`; } } function renderNewapiServicesTable(services) { if (!services || services.length === 0) { - elements.newapiServicesTable.innerHTML = '暂无 NEWAPI 服务,点击「添加服务」新增'; + elements.newapiServicesTable.innerHTML = '暂无 NEWAPI 服务,点击「添加服务」新增'; return; } elements.newapiServicesTable.innerHTML = services.map(s => ` ${escapeHtml(s.name)} ${escapeHtml(s.api_url)} + ${s.channel_type || 57} + ${escapeHtml(s.channel_base_url || '')} + ${escapeHtml(s.channel_models || '')} ${s.enabled ? '✅' : '⭕'} ${s.priority} @@ -1270,10 +1273,14 @@ function renderNewapiServicesTable(services) { } 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) { @@ -1304,6 +1311,9 @@ async function handleSaveNewapiService(e) { 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; @@ -1317,7 +1327,15 @@ async function handleSaveNewapiService(e) { } try { - const payload = { name, api_url: apiUrl, priority, enabled }; + 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) { diff --git a/templates/settings.html b/templates/settings.html index 1d1bbe7..1d1dc65 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -294,13 +294,16 @@ 名称 API URL + Type + Base URL + Models 状态 优先级 操作 - 加载中... + 加载中...
@@ -470,6 +473,20 @@
+
+
+ + +
+
+ + +
+
+
+ + +