refactor: 补全services层+消除core/models.py冗余+修复network.py缩进错误
高优先级问题1 - Router层业务逻辑泄漏: - 新建 services/proxy_service.py: 从routers/proxy.py提取ProxyService类, 封装代理/白名单测试逻辑、全局状态管理、所有core/ import - 新建 services/account_service.py: 从routers/accounts.py提取 split_account_line、cookie_account_ids_query、parse_and_build_accounts, 封装core.douyu.email_verifier的import - 重写routers/proxy.py为薄HTTP/WS层,移除所有core/直接依赖 - 精简routers/accounts.py,从account_service导入业务函数 - 将_active_batches全局状态移入login_service.py的BatchRegistry类 - routers/login.py改为使用batch_registry单例 高优先级问题2 - 两套models.py冗余: - 删除core/models.py(Account dataclass + ProxyConfig死代码) - 在core/douyu/login.py新增AccountLike Protocol替代Account dataclass - login_service.py中Account(...)构造改为SimpleNamespace(...) - 清理core/__init__.py导出 附带修复: - core/geetest/common/network.py的get_picture()函数缩进错误 (缺少if match:块和data赋值语句)
This commit is contained in:
@@ -1,5 +1 @@
|
|||||||
"""核心业务逻辑"""
|
"""核心业务逻辑"""
|
||||||
|
|
||||||
from .models import Account, ProxyConfig
|
|
||||||
|
|
||||||
__all__ = ["Account", "ProxyConfig"]
|
|
||||||
|
|||||||
+13
-3
@@ -4,11 +4,10 @@ import re
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
from typing import Mapping, Optional, Tuple
|
from typing import Mapping, Optional, Protocol, Tuple
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from core.models import Account
|
|
||||||
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
||||||
from .email_verifier import EmailVerifier
|
from .email_verifier import EmailVerifier
|
||||||
from .proxy import ProxyManager, get_proxy_manager
|
from .proxy import ProxyManager, get_proxy_manager
|
||||||
@@ -25,6 +24,17 @@ from core.geetest.common.network import (
|
|||||||
from utils.http_logger import log_http
|
from utils.http_logger import log_http
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLike(Protocol):
|
||||||
|
"""DouyuLogin 所需的最小账号接口,ORM 对象或 SimpleNamespace 均可满足。"""
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
email: str
|
||||||
|
email_password: str
|
||||||
|
email_imap_server: str
|
||||||
|
email_imap_port: int
|
||||||
|
email_imap_ssl: bool
|
||||||
|
|
||||||
|
|
||||||
class LoginResult:
|
class LoginResult:
|
||||||
"""登录结果"""
|
"""登录结果"""
|
||||||
|
|
||||||
@@ -57,7 +67,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
account: Account,
|
account: AccountLike,
|
||||||
proxy: Optional[str | Mapping[str, str]] = None,
|
proxy: Optional[str | Mapping[str, str]] = None,
|
||||||
proxy_api_url: Optional[str] = None,
|
proxy_api_url: Optional[str] = None,
|
||||||
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
||||||
|
|||||||
@@ -266,6 +266,10 @@ def get_picture(gt:str, challenge:str) -> tuple[str, str, list[int], str, str, s
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.debug("极验 get.php 原始响应: {}", response.text[:500])
|
logger.debug("极验 get.php 原始响应: {}", response.text[:500])
|
||||||
match = re.search(r'\((.*)\)$', response.text)
|
match = re.search(r'\((.*)\)$', response.text)
|
||||||
|
if match:
|
||||||
|
json_str = match.group(1)
|
||||||
|
data = json.loads(json_str)
|
||||||
|
|
||||||
if 'data' in data and isinstance(data['data'], dict):
|
if 'data' in data and isinstance(data['data'], dict):
|
||||||
# 新版极验格式
|
# 新版极验格式
|
||||||
inner_data = data['data']
|
inner_data = data['data']
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
"""核心数据模型"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Account:
|
|
||||||
"""账号配置"""
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
email: str
|
|
||||||
email_password: str
|
|
||||||
email_imap_server: str
|
|
||||||
email_imap_port: int = 993
|
|
||||||
email_imap_ssl: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProxyConfig:
|
|
||||||
"""代理配置"""
|
|
||||||
enabled: bool = False
|
|
||||||
api_url: str = ""
|
|
||||||
http: str = ""
|
|
||||||
https: str = ""
|
|
||||||
# 白名单配置
|
|
||||||
whitelist_enabled: bool = False
|
|
||||||
whitelist_uid: str = ""
|
|
||||||
whitelist_ukey: str = ""
|
|
||||||
@@ -1,41 +1,20 @@
|
|||||||
"""账号管理路由"""
|
"""账号管理路由"""
|
||||||
|
|
||||||
import re
|
|
||||||
import csv
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..models import User, Account, AuditLog, LoginTask
|
from ..models import User, Account, AuditLog, LoginTask
|
||||||
from ..schemas import AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
|
from ..schemas import AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
|
||||||
from ..deps import get_current_user, require_permission
|
from ..deps import get_current_user, require_permission
|
||||||
from ..permissions import user_has_permission
|
from ..permissions import user_has_permission
|
||||||
from sqlalchemy import func
|
from ..services.account_service import (
|
||||||
from sqlalchemy.orm import joinedload
|
cookie_account_ids_query, parse_and_build_accounts,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
|
router = APIRouter(prefix="/api/accounts", tags=["账号管理"])
|
||||||
|
|
||||||
EMAIL_PATTERN = re.compile(r'^[^\s@|]+@[^\s@|]+\.[^\s@|]+$')
|
|
||||||
|
|
||||||
|
|
||||||
def _split_account_line(line: str) -> list[str]:
|
|
||||||
if '|' in line:
|
|
||||||
return line.split('|')
|
|
||||||
if '\t' in line:
|
|
||||||
return line.split('\t')
|
|
||||||
if ',' in line:
|
|
||||||
return next(csv.reader([line]))
|
|
||||||
return line.split()
|
|
||||||
|
|
||||||
|
|
||||||
def _cookie_account_ids_query(db: Session):
|
|
||||||
"""返回有成功登录记录(cookie非空)的账号ID子查询。"""
|
|
||||||
return db.query(LoginTask.account_id).filter(
|
|
||||||
LoginTask.status == 'success',
|
|
||||||
LoginTask.cookie != '',
|
|
||||||
LoginTask.cookie.isnot(None),
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[AccountOut])
|
@router.get("", response_model=list[AccountOut])
|
||||||
def list_accounts(
|
def list_accounts(
|
||||||
@@ -50,7 +29,7 @@ def list_accounts(
|
|||||||
|
|
||||||
# 只展示已成功登录过的账号
|
# 只展示已成功登录过的账号
|
||||||
if has_cookie:
|
if has_cookie:
|
||||||
query = query.filter(Account.id.in_(_cookie_account_ids_query(db)))
|
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
|
||||||
|
|
||||||
# 权限控制:客服只能看分配给自己的
|
# 权限控制:客服只能看分配给自己的
|
||||||
if not user_has_permission(current, "account:view_all"):
|
if not user_has_permission(current, "account:view_all"):
|
||||||
@@ -91,39 +70,7 @@ def import_accounts(
|
|||||||
current: User = Depends(require_permission("account:import")),
|
current: User = Depends(require_permission("account:import")),
|
||||||
):
|
):
|
||||||
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)"""
|
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)"""
|
||||||
from core.douyu.email_verifier import get_email_config_for_account
|
accounts, skipped = parse_and_build_accounts(req.text)
|
||||||
|
|
||||||
accounts = []
|
|
||||||
skipped = 0
|
|
||||||
for line_num, line in enumerate(req.text.strip().split('\n'), 1):
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith('#'):
|
|
||||||
continue
|
|
||||||
parts = _split_account_line(line)
|
|
||||||
if len(parts) < 4:
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
username, password, email, email_password = [p.strip() for p in parts[:4]]
|
|
||||||
tag = parts[4].strip() if len(parts) > 4 else ""
|
|
||||||
if not all([username, password, email, email_password]):
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
if not EMAIL_PATTERN.match(email):
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
email_cfg = get_email_config_for_account(email)
|
|
||||||
accounts.append(Account(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
email=email,
|
|
||||||
email_password=email_password,
|
|
||||||
email_imap_server=email_cfg['server'],
|
|
||||||
email_imap_port=email_cfg.get('port', 993),
|
|
||||||
email_imap_ssl=email_cfg.get('ssl', True),
|
|
||||||
tag=tag,
|
|
||||||
))
|
|
||||||
|
|
||||||
if accounts:
|
if accounts:
|
||||||
db.add_all(accounts)
|
db.add_all(accounts)
|
||||||
@@ -187,7 +134,7 @@ def batch_assign_accounts(
|
|||||||
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
raise HTTPException(status_code=400, detail="只能分配给客服角色")
|
||||||
|
|
||||||
# 只能分配已成功登录过的账号(有cookie)
|
# 只能分配已成功登录过的账号(有cookie)
|
||||||
cookie_ids_query = _cookie_account_ids_query(db).subquery()
|
cookie_ids_query = cookie_account_ids_query(db).subquery()
|
||||||
invalid_ids = db.query(Account.id).filter(
|
invalid_ids = db.query(Account.id).filter(
|
||||||
Account.id.in_(req.account_ids),
|
Account.id.in_(req.account_ids),
|
||||||
Account.id.notin_(cookie_ids_query),
|
Account.id.notin_(cookie_ids_query),
|
||||||
@@ -220,7 +167,7 @@ def assignments_summary(
|
|||||||
current: User = Depends(require_permission("account:assign")),
|
current: User = Depends(require_permission("account:assign")),
|
||||||
):
|
):
|
||||||
"""分配概览:每个客服分配了多少账号(仅统计已成功登录的账号)。"""
|
"""分配概览:每个客服分配了多少账号(仅统计已成功登录的账号)。"""
|
||||||
cookie_subq = _cookie_account_ids_query(db).subquery()
|
cookie_subq = cookie_account_ids_query(db).subquery()
|
||||||
cookie_accounts = db.query(Account).filter(Account.id.in_(cookie_subq)).subquery()
|
cookie_accounts = db.query(Account).filter(Account.id.in_(cookie_subq)).subquery()
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel
|
|||||||
from ..schemas import LoginBatchRequest, LoginTaskOut
|
from ..schemas import LoginBatchRequest, LoginTaskOut
|
||||||
from ..deps import get_current_user, require_permission, authenticate_websocket
|
from ..deps import get_current_user, require_permission, authenticate_websocket
|
||||||
from ..permissions import user_has_permission, get_user_permissions
|
from ..permissions import user_has_permission, get_user_permissions
|
||||||
from ..services.login_service import LoginBatchRunner
|
from ..services.login_service import LoginBatchRunner, batch_registry
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/login", tags=["登录任务"])
|
router = APIRouter(prefix="/api/login", tags=["登录任务"])
|
||||||
|
|
||||||
# 运行中的批次: batch_id -> {log_queue, loop, runner}
|
|
||||||
_active_batches: dict[str, dict] = {}
|
|
||||||
_active_batches_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/batch")
|
@router.post("/batch")
|
||||||
async def create_batch(
|
async def create_batch(
|
||||||
@@ -68,13 +64,8 @@ async def create_batch(
|
|||||||
|
|
||||||
batch_id = runner.batch_id
|
batch_id = runner.batch_id
|
||||||
|
|
||||||
# 先注册到全局,再启动线程,确保 WebSocket 连接时能找到
|
# 先注册到批次注册表,再启动线程,确保 WebSocket 连接时能找到
|
||||||
with _active_batches_lock:
|
batch_registry.register(batch_id, log_queue, loop, runner)
|
||||||
_active_batches[batch_id] = {
|
|
||||||
"log_queue": log_queue,
|
|
||||||
"loop": loop,
|
|
||||||
"runner": runner,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 启动线程
|
# 启动线程
|
||||||
thread = threading.Thread(target=runner.run, daemon=True)
|
thread = threading.Thread(target=runner.run, daemon=True)
|
||||||
@@ -155,8 +146,7 @@ def stop_batch(
|
|||||||
batch_id: str,
|
batch_id: str,
|
||||||
current: User = Depends(require_permission("login:batch")),
|
current: User = Depends(require_permission("login:batch")),
|
||||||
):
|
):
|
||||||
with _active_batches_lock:
|
batch = batch_registry.get(batch_id)
|
||||||
batch = _active_batches.get(batch_id)
|
|
||||||
if batch:
|
if batch:
|
||||||
batch["runner"].stop()
|
batch["runner"].stop()
|
||||||
return {"message": "已发送停止信号", "success": True}
|
return {"message": "已发送停止信号", "success": True}
|
||||||
@@ -175,8 +165,7 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
|
|||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
# 从已注册的批次中获取 log_queue(由 create_batch 创建)
|
# 从已注册的批次中获取 log_queue(由 create_batch 创建)
|
||||||
with _active_batches_lock:
|
batch = batch_registry.get(batch_id)
|
||||||
batch = _active_batches.get(batch_id)
|
|
||||||
if not batch:
|
if not batch:
|
||||||
await websocket.send_json({"level": "error", "message": "批次不存在或已结束"})
|
await websocket.send_json({"level": "error", "message": "批次不存在或已结束"})
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
@@ -198,5 +187,4 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
|
|||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
with _active_batches_lock:
|
batch_registry.pop(batch_id)
|
||||||
_active_batches.pop(batch_id, None)
|
|
||||||
|
|||||||
+32
-216
@@ -2,39 +2,24 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
|
||||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
from ..models import User, ProxyConfig as ProxyConfigModel, AuditLog
|
from ..models import User
|
||||||
from ..schemas import ProxyConfigOut, ProxyConfigUpdate
|
from ..schemas import ProxyConfigOut, ProxyConfigUpdate
|
||||||
from ..deps import require_permission, authenticate_websocket
|
from ..deps import require_permission, authenticate_websocket
|
||||||
from core.douyu.proxy import resolve_working_proxy, verify_proxy_url, parse_proxy_response
|
from ..services.proxy_service import proxy_service
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/proxy", tags=["代理与白名单"])
|
router = APIRouter(prefix="/api/proxy", tags=["代理与白名单"])
|
||||||
|
|
||||||
# 运行中的测试: test_id -> {log_queue, loop, result}
|
|
||||||
_active_tests: dict[str, dict] = {}
|
|
||||||
_active_tests_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_or_create(db: Session) -> ProxyConfigModel:
|
|
||||||
cfg = db.query(ProxyConfigModel).first()
|
|
||||||
if not cfg:
|
|
||||||
cfg = ProxyConfigModel()
|
|
||||||
db.add(cfg)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(cfg)
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=ProxyConfigOut)
|
@router.get("", response_model=ProxyConfigOut)
|
||||||
def get_proxy_config(
|
def get_proxy_config(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
_: User = Depends(require_permission("proxy:manage")),
|
_: User = Depends(require_permission("proxy:manage")),
|
||||||
):
|
):
|
||||||
return _get_or_create(db)
|
return proxy_service.get_or_create(db)
|
||||||
|
|
||||||
|
|
||||||
@router.put("", response_model=ProxyConfigOut)
|
@router.put("", response_model=ProxyConfigOut)
|
||||||
@@ -43,21 +28,17 @@ def update_proxy_config(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current: User = Depends(require_permission("proxy:manage")),
|
current: User = Depends(require_permission("proxy:manage")),
|
||||||
):
|
):
|
||||||
cfg = _get_or_create(db)
|
return proxy_service.update_config(
|
||||||
cfg.enabled = req.enabled
|
db,
|
||||||
cfg.api_url = req.api_url
|
enabled=req.enabled,
|
||||||
cfg.http = req.http
|
api_url=req.api_url,
|
||||||
cfg.https = req.https
|
http=req.http,
|
||||||
cfg.whitelist_enabled = req.whitelist_enabled
|
https=req.https,
|
||||||
cfg.whitelist_uid = req.whitelist_uid
|
whitelist_enabled=req.whitelist_enabled,
|
||||||
cfg.whitelist_ukey = req.whitelist_ukey
|
whitelist_uid=req.whitelist_uid,
|
||||||
db.commit()
|
whitelist_ukey=req.whitelist_ukey,
|
||||||
db.refresh(cfg)
|
current_user=current,
|
||||||
|
)
|
||||||
db.add(AuditLog(user_id=current.id, username=current.username,
|
|
||||||
action="proxy:update", target="proxy_config"))
|
|
||||||
db.commit()
|
|
||||||
return cfg
|
|
||||||
|
|
||||||
|
|
||||||
# ---- WebSocket 日志推送 ----
|
# ---- WebSocket 日志推送 ----
|
||||||
@@ -65,7 +46,6 @@ def update_proxy_config(
|
|||||||
@router.websocket("/ws/test/{test_id}")
|
@router.websocket("/ws/test/{test_id}")
|
||||||
async def ws_test_logs(websocket: WebSocket, test_id: str):
|
async def ws_test_logs(websocket: WebSocket, test_id: str):
|
||||||
"""WebSocket 推送代理/白名单测试实时日志(需认证)。"""
|
"""WebSocket 推送代理/白名单测试实时日志(需认证)。"""
|
||||||
# 认证:从 cookie 或 token query param 验证用户身份
|
|
||||||
user = authenticate_websocket(websocket)
|
user = authenticate_websocket(websocket)
|
||||||
if not user:
|
if not user:
|
||||||
await websocket.close(code=1008, reason="未授权")
|
await websocket.close(code=1008, reason="未授权")
|
||||||
@@ -73,8 +53,7 @@ async def ws_test_logs(websocket: WebSocket, test_id: str):
|
|||||||
|
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
with _active_tests_lock:
|
test = proxy_service.get_test(test_id)
|
||||||
test = _active_tests.get(test_id)
|
|
||||||
if not test:
|
if not test:
|
||||||
await websocket.send_json({"level": "error", "message": "测试任务不存在"})
|
await websocket.send_json({"level": "error", "message": "测试任务不存在"})
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
@@ -96,169 +75,10 @@ async def ws_test_logs(websocket: WebSocket, test_id: str):
|
|||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
with _active_tests_lock:
|
proxy_service.remove_test(test_id)
|
||||||
_active_tests.pop(test_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- 异步测试执行 ----
|
# ---- 异步测试 API 端点 ----
|
||||||
|
|
||||||
def _run_proxy_test(
|
|
||||||
cfg: ProxyConfigModel,
|
|
||||||
log_queue: asyncio.Queue,
|
|
||||||
loop: asyncio.AbstractEventLoop,
|
|
||||||
):
|
|
||||||
"""在线程中执行代理测试。"""
|
|
||||||
|
|
||||||
def push(level, message):
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
log_queue.put({"level": level, "message": message}),
|
|
||||||
loop,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not cfg.enabled:
|
|
||||||
push("error", "代理未启用")
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 静态代理
|
|
||||||
if cfg.http or cfg.https:
|
|
||||||
proxy_url = cfg.http or cfg.https
|
|
||||||
push("info", f"验证静态代理: {proxy_url}")
|
|
||||||
ok, msg = verify_proxy_url(proxy_url)
|
|
||||||
push("success" if ok else "error", msg)
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
# API代理
|
|
||||||
if cfg.api_url:
|
|
||||||
whitelist_uid = cfg.whitelist_uid if cfg.whitelist_enabled else ""
|
|
||||||
whitelist_ukey = cfg.whitelist_ukey if cfg.whitelist_enabled else ""
|
|
||||||
|
|
||||||
proxy_url, msg = resolve_working_proxy(
|
|
||||||
api_url=cfg.api_url,
|
|
||||||
whitelist_uid=whitelist_uid,
|
|
||||||
whitelist_ukey=whitelist_ukey,
|
|
||||||
max_attempts=3,
|
|
||||||
log_func=push,
|
|
||||||
)
|
|
||||||
if proxy_url:
|
|
||||||
push("success", f"代理可用: {proxy_url}")
|
|
||||||
else:
|
|
||||||
push("error", msg)
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
push("error", "未配置代理地址或API")
|
|
||||||
push("result", "")
|
|
||||||
except Exception as e:
|
|
||||||
push("error", f"测试异常: {e}")
|
|
||||||
push("result", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _run_whitelist_test(
|
|
||||||
cfg: ProxyConfigModel,
|
|
||||||
log_queue: asyncio.Queue,
|
|
||||||
loop: asyncio.AbstractEventLoop,
|
|
||||||
):
|
|
||||||
"""在线程中执行白名单测试。"""
|
|
||||||
import requests as req_lib
|
|
||||||
import re
|
|
||||||
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
|
||||||
|
|
||||||
def push(level, message):
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
log_queue.put({"level": level, "message": message}),
|
|
||||||
loop,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not cfg.whitelist_enabled:
|
|
||||||
push("error", "白名单未启用")
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
if not cfg.whitelist_uid or not cfg.whitelist_ukey:
|
|
||||||
push("error", "未配置白名单UID/UKEY")
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
manager = WhitelistManager(cfg.whitelist_uid, cfg.whitelist_ukey)
|
|
||||||
|
|
||||||
# 1. 测试API连接
|
|
||||||
push("info", "测试白名单API连接...")
|
|
||||||
ok, msg = manager.test_connection()
|
|
||||||
push("info" if ok else "error", f"白名单API: {msg}")
|
|
||||||
if not ok:
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. 获取本机公网IP(白名单需要的是本机IP,不是代理出口IP)
|
|
||||||
push("info", "正在获取本机公网IP...")
|
|
||||||
|
|
||||||
# 优先:从代理API响应中提取(代理API返回"请先添加白名单:xxx"时,xxx就是本机IP)
|
|
||||||
local_ip = None
|
|
||||||
if cfg.api_url:
|
|
||||||
try:
|
|
||||||
resp = req_lib.get(cfg.api_url, timeout=10)
|
|
||||||
text = resp.text.strip()
|
|
||||||
push("info", f"代理API响应: {text[:80]}")
|
|
||||||
_, whitelist_ip = parse_proxy_response(text)
|
|
||||||
if whitelist_ip:
|
|
||||||
local_ip = whitelist_ip
|
|
||||||
push("info", f"从代理API获取到本机IP: {local_ip}")
|
|
||||||
except Exception as e:
|
|
||||||
push("warning", f"代理API请求失败: {e}")
|
|
||||||
|
|
||||||
# 备用:直接访问IP检测服务获取本机公网IP
|
|
||||||
if not local_ip:
|
|
||||||
push("info", "通过IP检测服务获取本机公网IP...")
|
|
||||||
for url in [
|
|
||||||
'https://qifu-api.baidubce.com/ip/local/geo/v1/district',
|
|
||||||
'https://myip.ipip.net',
|
|
||||||
'https://4.ipw.cn',
|
|
||||||
]:
|
|
||||||
try:
|
|
||||||
resp = req_lib.get(url, timeout=6, headers={"User-Agent": "Mozilla/5.0"})
|
|
||||||
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', resp.text)
|
|
||||||
if match:
|
|
||||||
local_ip = match.group(1)
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not local_ip:
|
|
||||||
push("error", "无法获取本机公网IP")
|
|
||||||
push("result", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
push("info", f"本机公网IP: {local_ip}")
|
|
||||||
|
|
||||||
# 3. 检查并同步白名单
|
|
||||||
records = manager.get_whitelist_json()
|
|
||||||
in_list = any(r.get('IP') == local_ip for r in records)
|
|
||||||
push("info", f"白名单共 {len(records)} 条记录")
|
|
||||||
|
|
||||||
if in_list:
|
|
||||||
record = next((r for r in records if r.get('IP') == local_ip), {})
|
|
||||||
memo = record.get('MEMO', '')
|
|
||||||
if memo == manager.memo:
|
|
||||||
push("success", f"本机IP {local_ip} 已在白名单中 (备注正确)")
|
|
||||||
else:
|
|
||||||
push("warning", f'本机IP {local_ip} 备注不匹配 (当前: "{memo}"),更新中...')
|
|
||||||
sync_ok, sync_msg = manager.sync_ip(local_ip)
|
|
||||||
push("success" if sync_ok else "error", f"白名单更新: {sync_msg}")
|
|
||||||
else:
|
|
||||||
push("info", f"正在将 {local_ip} 添加到白名单...")
|
|
||||||
sync_ok, sync_msg = manager.sync_ip(local_ip)
|
|
||||||
push("success" if sync_ok else "error", f"白名单同步: {sync_msg}")
|
|
||||||
|
|
||||||
push("result", "")
|
|
||||||
except Exception as e:
|
|
||||||
push("error", f"测试异常: {e}")
|
|
||||||
push("result", "")
|
|
||||||
|
|
||||||
|
|
||||||
# ---- API 端点 ----
|
|
||||||
|
|
||||||
@router.post("/test")
|
@router.post("/test")
|
||||||
async def test_proxy(
|
async def test_proxy(
|
||||||
@@ -266,16 +86,14 @@ async def test_proxy(
|
|||||||
current: User = Depends(require_permission("proxy:manage")),
|
current: User = Depends(require_permission("proxy:manage")),
|
||||||
):
|
):
|
||||||
"""启动代理测试(异步执行,通过 WebSocket 推送日志)。"""
|
"""启动代理测试(异步执行,通过 WebSocket 推送日志)。"""
|
||||||
cfg = _get_or_create(db)
|
cfg = proxy_service.get_or_create(db)
|
||||||
|
test_id, log_queue, loop = proxy_service.register_test()
|
||||||
|
|
||||||
test_id = uuid.uuid4().hex[:12]
|
thread = threading.Thread(
|
||||||
log_queue = asyncio.Queue()
|
target=proxy_service.run_proxy_test,
|
||||||
loop = asyncio.get_running_loop()
|
args=(cfg, log_queue, loop),
|
||||||
|
daemon=True,
|
||||||
with _active_tests_lock:
|
)
|
||||||
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
|
|
||||||
|
|
||||||
thread = threading.Thread(target=_run_proxy_test, args=(cfg, log_queue, loop), daemon=True)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return {"test_id": test_id, "success": True}
|
return {"test_id": test_id, "success": True}
|
||||||
@@ -287,16 +105,14 @@ async def test_whitelist(
|
|||||||
current: User = Depends(require_permission("whitelist:test")),
|
current: User = Depends(require_permission("whitelist:test")),
|
||||||
):
|
):
|
||||||
"""启动白名单测试(异步执行,通过 WebSocket 推送日志)。"""
|
"""启动白名单测试(异步执行,通过 WebSocket 推送日志)。"""
|
||||||
cfg = _get_or_create(db)
|
cfg = proxy_service.get_or_create(db)
|
||||||
|
test_id, log_queue, loop = proxy_service.register_test()
|
||||||
|
|
||||||
test_id = uuid.uuid4().hex[:12]
|
thread = threading.Thread(
|
||||||
log_queue = asyncio.Queue()
|
target=proxy_service.run_whitelist_test,
|
||||||
loop = asyncio.get_running_loop()
|
args=(cfg, log_queue, loop),
|
||||||
|
daemon=True,
|
||||||
with _active_tests_lock:
|
)
|
||||||
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
|
|
||||||
|
|
||||||
thread = threading.Thread(target=_run_whitelist_test, args=(cfg, log_queue, loop), daemon=True)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return {"test_id": test_id, "success": True}
|
return {"test_id": test_id, "success": True}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""账号管理服务层:封装账号导入、查询等业务逻辑,隔离 core/ 依赖。"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
|
||||||
|
from ..models import Account, AuditLog, LoginTask
|
||||||
|
from ..permissions import user_has_permission
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_PATTERN = re.compile(r'^[^\s@|]+@[^\s@|]+\.[^\s@|]+$')
|
||||||
|
|
||||||
|
|
||||||
|
def split_account_line(line: str) -> list[str]:
|
||||||
|
"""拆分一行账号文本,支持 |、tab、逗号、空格分隔。"""
|
||||||
|
if '|' in line:
|
||||||
|
return line.split('|')
|
||||||
|
if '\t' in line:
|
||||||
|
return line.split('\t')
|
||||||
|
if ',' in line:
|
||||||
|
return next(csv.reader([line]))
|
||||||
|
return line.split()
|
||||||
|
|
||||||
|
|
||||||
|
def cookie_account_ids_query(db: Session):
|
||||||
|
"""返回有成功登录记录(cookie非空)的账号ID子查询。"""
|
||||||
|
return db.query(LoginTask.account_id).filter(
|
||||||
|
LoginTask.status == 'success',
|
||||||
|
LoginTask.cookie != '',
|
||||||
|
LoginTask.cookie.isnot(None),
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_and_build_accounts(text: str) -> tuple[list[Account], int]:
|
||||||
|
"""
|
||||||
|
解析批量导入文本,构建 Account ORM 对象列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(accounts, skipped_count)
|
||||||
|
"""
|
||||||
|
from core.douyu.email_verifier import get_email_config_for_account
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
skipped = 0
|
||||||
|
for line in text.strip().split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
parts = split_account_line(line)
|
||||||
|
if len(parts) < 4:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
username, password, email, email_password = [p.strip() for p in parts[:4]]
|
||||||
|
tag = parts[4].strip() if len(parts) > 4 else ""
|
||||||
|
if not all([username, password, email, email_password]):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
if not EMAIL_PATTERN.match(email):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
email_cfg = get_email_config_for_account(email)
|
||||||
|
accounts.append(Account(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
email=email,
|
||||||
|
email_password=email_password,
|
||||||
|
email_imap_server=email_cfg['server'],
|
||||||
|
email_imap_port=email_cfg.get('port', 993),
|
||||||
|
email_imap_ssl=email_cfg.get('ssl', True),
|
||||||
|
tag=tag,
|
||||||
|
))
|
||||||
|
return accounts, skipped
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
from types import SimpleNamespace
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -10,7 +11,6 @@ from typing import Optional
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.douyu import DouyuLogin
|
from core.douyu import DouyuLogin
|
||||||
from core.models import Account, ProxyConfig as DouyuProxyConfig
|
|
||||||
from core.douyu.proxy import resolve_working_proxy, get_proxy_manager
|
from core.douyu.proxy import resolve_working_proxy, get_proxy_manager
|
||||||
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
|
from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class LoginBatchRunner:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
account = Account(
|
account = SimpleNamespace(
|
||||||
username=acc_info["username"],
|
username=acc_info["username"],
|
||||||
password=acc_info["password"],
|
password=acc_info["password"],
|
||||||
email=acc_info["email"],
|
email=acc_info["email"],
|
||||||
@@ -261,5 +261,34 @@ class LoginBatchRunner:
|
|||||||
self.db.close()
|
self.db.close()
|
||||||
|
|
||||||
|
|
||||||
|
class BatchRegistry:
|
||||||
|
"""管理运行中的登录批次状态。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._batches: dict[str, dict] = {}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def register(self, batch_id: str, log_queue: asyncio.Queue,
|
||||||
|
loop: asyncio.AbstractEventLoop, runner: LoginBatchRunner):
|
||||||
|
with self._lock:
|
||||||
|
self._batches[batch_id] = {
|
||||||
|
"log_queue": log_queue,
|
||||||
|
"loop": loop,
|
||||||
|
"runner": runner,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, batch_id: str):
|
||||||
|
with self._lock:
|
||||||
|
return self._batches.get(batch_id)
|
||||||
|
|
||||||
|
def pop(self, batch_id: str):
|
||||||
|
with self._lock:
|
||||||
|
return self._batches.pop(batch_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
# 模块级单例
|
||||||
|
batch_registry = BatchRegistry()
|
||||||
|
|
||||||
|
|
||||||
# 在模块末尾导入 SessionLocal(避免循环导入)
|
# 在模块末尾导入 SessionLocal(避免循环导入)
|
||||||
from ..database import SessionLocal
|
from ..database import SessionLocal
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
"""代理 & 白名单服务层:封装 core/ 代理/白名单逻辑,供路由调用。"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests as req_lib
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.douyu.proxy import resolve_working_proxy, verify_proxy_url, parse_proxy_response
|
||||||
|
from core.douyu.whitelist import WhitelistManager, get_exit_ip_via_proxy
|
||||||
|
from ..models import ProxyConfig as ProxyConfigModel, AuditLog
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyService:
|
||||||
|
"""代理 & 白名单服务:管理代理配置、执行测试。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 运行中的测试: test_id -> {log_queue, loop}
|
||||||
|
self._active_tests: dict[str, dict] = {}
|
||||||
|
self._active_tests_lock = threading.Lock()
|
||||||
|
|
||||||
|
# ---- 配置 CRUD ----
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_or_create(db: Session) -> ProxyConfigModel:
|
||||||
|
"""获取或创建全局代理配置(单条记录)。"""
|
||||||
|
cfg = db.query(ProxyConfigModel).first()
|
||||||
|
if not cfg:
|
||||||
|
cfg = ProxyConfigModel()
|
||||||
|
db.add(cfg)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(cfg)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_config(db: Session, enabled, api_url, http, https,
|
||||||
|
whitelist_enabled, whitelist_uid, whitelist_ukey,
|
||||||
|
current_user) -> ProxyConfigModel:
|
||||||
|
"""更新代理配置并记录审计日志。"""
|
||||||
|
cfg = ProxyService.get_or_create(db)
|
||||||
|
cfg.enabled = enabled
|
||||||
|
cfg.api_url = api_url
|
||||||
|
cfg.http = http
|
||||||
|
cfg.https = https
|
||||||
|
cfg.whitelist_enabled = whitelist_enabled
|
||||||
|
cfg.whitelist_uid = whitelist_uid
|
||||||
|
cfg.whitelist_ukey = whitelist_ukey
|
||||||
|
db.commit()
|
||||||
|
db.refresh(cfg)
|
||||||
|
|
||||||
|
db.add(AuditLog(
|
||||||
|
user_id=current_user.id,
|
||||||
|
username=current_user.username,
|
||||||
|
action="proxy:update",
|
||||||
|
target="proxy_config",
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
# ---- 测试任务管理 ----
|
||||||
|
|
||||||
|
def register_test(self) -> tuple[str, asyncio.Queue, asyncio.AbstractEventLoop]:
|
||||||
|
"""注册一个新测试任务,返回 (test_id, log_queue, loop)。"""
|
||||||
|
test_id = uuid.uuid4().hex[:12]
|
||||||
|
log_queue = asyncio.Queue()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
with self._active_tests_lock:
|
||||||
|
self._active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
|
||||||
|
return test_id, log_queue, loop
|
||||||
|
|
||||||
|
def get_test(self, test_id: str) -> Optional[dict]:
|
||||||
|
"""获取测试任务信息。"""
|
||||||
|
with self._active_tests_lock:
|
||||||
|
return self._active_tests.get(test_id)
|
||||||
|
|
||||||
|
def remove_test(self, test_id: str) -> None:
|
||||||
|
"""移除测试任务。"""
|
||||||
|
with self._active_tests_lock:
|
||||||
|
self._active_tests.pop(test_id, None)
|
||||||
|
|
||||||
|
# ---- 代理测试 ----
|
||||||
|
|
||||||
|
def run_proxy_test(
|
||||||
|
self,
|
||||||
|
cfg: ProxyConfigModel,
|
||||||
|
log_queue: asyncio.Queue,
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
):
|
||||||
|
"""在线程中执行代理测试。"""
|
||||||
|
|
||||||
|
def push(level, message):
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
log_queue.put({"level": level, "message": message}),
|
||||||
|
loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not cfg.enabled:
|
||||||
|
push("error", "代理未启用")
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 静态代理
|
||||||
|
if cfg.http or cfg.https:
|
||||||
|
proxy_url = cfg.http or cfg.https
|
||||||
|
push("info", f"验证静态代理: {proxy_url}")
|
||||||
|
ok, msg = verify_proxy_url(proxy_url)
|
||||||
|
push("success" if ok else "error", msg)
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
# API代理
|
||||||
|
if cfg.api_url:
|
||||||
|
whitelist_uid = cfg.whitelist_uid if cfg.whitelist_enabled else ""
|
||||||
|
whitelist_ukey = cfg.whitelist_ukey if cfg.whitelist_enabled else ""
|
||||||
|
|
||||||
|
proxy_url, msg = resolve_working_proxy(
|
||||||
|
api_url=cfg.api_url,
|
||||||
|
whitelist_uid=whitelist_uid,
|
||||||
|
whitelist_ukey=whitelist_ukey,
|
||||||
|
max_attempts=3,
|
||||||
|
log_func=push,
|
||||||
|
)
|
||||||
|
if proxy_url:
|
||||||
|
push("success", f"代理可用: {proxy_url}")
|
||||||
|
else:
|
||||||
|
push("error", msg)
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
push("error", "未配置代理地址或API")
|
||||||
|
push("result", "")
|
||||||
|
except Exception as e:
|
||||||
|
push("error", f"测试异常: {e}")
|
||||||
|
push("result", "")
|
||||||
|
|
||||||
|
# ---- 白名单测试 ----
|
||||||
|
|
||||||
|
def run_whitelist_test(
|
||||||
|
self,
|
||||||
|
cfg: ProxyConfigModel,
|
||||||
|
log_queue: asyncio.Queue,
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
):
|
||||||
|
"""在线程中执行白名单测试。"""
|
||||||
|
|
||||||
|
def push(level, message):
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
log_queue.put({"level": level, "message": message}),
|
||||||
|
loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not cfg.whitelist_enabled:
|
||||||
|
push("error", "白名单未启用")
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
if not cfg.whitelist_uid or not cfg.whitelist_ukey:
|
||||||
|
push("error", "未配置白名单UID/UKEY")
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
manager = WhitelistManager(cfg.whitelist_uid, cfg.whitelist_ukey)
|
||||||
|
|
||||||
|
# 1. 测试API连接
|
||||||
|
push("info", "测试白名单API连接...")
|
||||||
|
ok, msg = manager.test_connection()
|
||||||
|
push("info" if ok else "error", f"白名单API: {msg}")
|
||||||
|
if not ok:
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 获取本机公网IP
|
||||||
|
push("info", "正在获取本机公网IP...")
|
||||||
|
local_ip = None
|
||||||
|
if cfg.api_url:
|
||||||
|
try:
|
||||||
|
resp = req_lib.get(cfg.api_url, timeout=10)
|
||||||
|
text = resp.text.strip()
|
||||||
|
push("info", f"代理API响应: {text[:80]}")
|
||||||
|
_, whitelist_ip = parse_proxy_response(text)
|
||||||
|
if whitelist_ip:
|
||||||
|
local_ip = whitelist_ip
|
||||||
|
push("info", f"从代理API获取到本机IP: {local_ip}")
|
||||||
|
except Exception as e:
|
||||||
|
push("warning", f"代理API请求失败: {e}")
|
||||||
|
|
||||||
|
if not local_ip:
|
||||||
|
push("info", "通过IP检测服务获取本机公网IP...")
|
||||||
|
for url in [
|
||||||
|
'https://qifu-api.baidubce.com/ip/local/geo/v1/district',
|
||||||
|
'https://myip.ipip.net',
|
||||||
|
'https://4.ipw.cn',
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
resp = req_lib.get(url, timeout=6, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
match = re.search(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', resp.text)
|
||||||
|
if match:
|
||||||
|
local_ip = match.group(1)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not local_ip:
|
||||||
|
push("error", "无法获取本机公网IP")
|
||||||
|
push("result", "")
|
||||||
|
return
|
||||||
|
|
||||||
|
push("info", f"本机公网IP: {local_ip}")
|
||||||
|
|
||||||
|
# 3. 检查并同步白名单
|
||||||
|
records = manager.get_whitelist_json()
|
||||||
|
in_list = any(r.get('IP') == local_ip for r in records)
|
||||||
|
push("info", f"白名单共 {len(records)} 条记录")
|
||||||
|
|
||||||
|
if in_list:
|
||||||
|
record = next((r for r in records if r.get('IP') == local_ip), {})
|
||||||
|
memo = record.get('MEMO', '')
|
||||||
|
if memo == manager.memo:
|
||||||
|
push("success", f"本机IP {local_ip} 已在白名单中 (备注正确)")
|
||||||
|
else:
|
||||||
|
push("warning", f'本机IP {local_ip} 备注不匹配 (当前: "{memo}"),更新中...')
|
||||||
|
sync_ok, sync_msg = manager.sync_ip(local_ip)
|
||||||
|
push("success" if sync_ok else "error", f"白名单更新: {sync_msg}")
|
||||||
|
else:
|
||||||
|
push("info", f"正在将 {local_ip} 添加到白名单...")
|
||||||
|
sync_ok, sync_msg = manager.sync_ip(local_ip)
|
||||||
|
push("success" if sync_ok else "error", f"白名单同步: {sync_msg}")
|
||||||
|
|
||||||
|
push("result", "")
|
||||||
|
except Exception as e:
|
||||||
|
push("error", f"测试异常: {e}")
|
||||||
|
push("result", "")
|
||||||
|
|
||||||
|
|
||||||
|
# 模块级单例
|
||||||
|
proxy_service = ProxyService()
|
||||||
Reference in New Issue
Block a user