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:
yml2213
2026-06-23 07:34:17 +08:00
parent 12c31c2e09
commit 2ab6724543
10 changed files with 411 additions and 333 deletions
-4
View File
@@ -1,5 +1 @@
"""核心业务逻辑"""
from .models import Account, ProxyConfig
__all__ = ["Account", "ProxyConfig"]
+13 -3
View File
@@ -4,11 +4,10 @@ import re
import json
import time
import requests
from typing import Mapping, Optional, Tuple
from typing import Mapping, Optional, Protocol, Tuple
from urllib.parse import urlsplit, urlunsplit
from loguru import logger
from core.models import Account
from .crypto import encrypt_password, encrypt_nickname_or_phone
from .email_verifier import EmailVerifier
from .proxy import ProxyManager, get_proxy_manager
@@ -25,6 +24,17 @@ from core.geetest.common.network import (
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:
"""登录结果"""
@@ -57,7 +67,7 @@ class DouyuLogin:
def __init__(
self,
account: Account,
account: AccountLike,
proxy: Optional[str | Mapping[str, str]] = None,
proxy_api_url: Optional[str] = None,
timeout: tuple[float, float] = REQUEST_TIMEOUT,
+4
View File
@@ -266,6 +266,10 @@ def get_picture(gt:str, challenge:str) -> tuple[str, str, list[int], str, str, s
response.raise_for_status()
logger.debug("极验 get.php 原始响应: {}", response.text[:500])
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):
# 新版极验格式
inner_data = data['data']
-28
View File
@@ -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 = ""
+9 -62
View File
@@ -1,41 +1,20 @@
"""账号管理路由"""
import re
import csv
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 ..models import User, Account, AuditLog, LoginTask
from ..schemas import AccountImport, AccountAssign, AccountTag, AccountOut, BatchAssign
from ..deps import get_current_user, require_permission
from ..permissions import user_has_permission
from sqlalchemy import func
from sqlalchemy.orm import joinedload
from ..services.account_service import (
cookie_account_ids_query, parse_and_build_accounts,
)
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])
def list_accounts(
@@ -50,7 +29,7 @@ def list_accounts(
# 只展示已成功登录过的账号
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"):
@@ -91,39 +70,7 @@ def import_accounts(
current: User = Depends(require_permission("account:import")),
):
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)"""
from core.douyu.email_verifier import get_email_config_for_account
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,
))
accounts, skipped = parse_and_build_accounts(req.text)
if accounts:
db.add_all(accounts)
@@ -187,7 +134,7 @@ def batch_assign_accounts(
raise HTTPException(status_code=400, detail="只能分配给客服角色")
# 只能分配已成功登录过的账号(有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(
Account.id.in_(req.account_ids),
Account.id.notin_(cookie_ids_query),
@@ -220,7 +167,7 @@ def assignments_summary(
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()
results = (
+6 -18
View File
@@ -11,14 +11,10 @@ from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel
from ..schemas import LoginBatchRequest, LoginTaskOut
from ..deps import get_current_user, require_permission, authenticate_websocket
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=["登录任务"])
# 运行中的批次: batch_id -> {log_queue, loop, runner}
_active_batches: dict[str, dict] = {}
_active_batches_lock = threading.Lock()
@router.post("/batch")
async def create_batch(
@@ -68,13 +64,8 @@ async def create_batch(
batch_id = runner.batch_id
# 先注册到全局,再启动线程,确保 WebSocket 连接时能找到
with _active_batches_lock:
_active_batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,
"runner": runner,
}
# 先注册到批次注册表,再启动线程,确保 WebSocket 连接时能找到
batch_registry.register(batch_id, log_queue, loop, runner)
# 启动线程
thread = threading.Thread(target=runner.run, daemon=True)
@@ -155,8 +146,7 @@ def stop_batch(
batch_id: str,
current: User = Depends(require_permission("login:batch")),
):
with _active_batches_lock:
batch = _active_batches.get(batch_id)
batch = batch_registry.get(batch_id)
if batch:
batch["runner"].stop()
return {"message": "已发送停止信号", "success": True}
@@ -175,8 +165,7 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
await websocket.accept()
# 从已注册的批次中获取 log_queue(由 create_batch 创建)
with _active_batches_lock:
batch = _active_batches.get(batch_id)
batch = batch_registry.get(batch_id)
if not batch:
await websocket.send_json({"level": "error", "message": "批次不存在或已结束"})
await websocket.close()
@@ -198,5 +187,4 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
except WebSocketDisconnect:
pass
finally:
with _active_batches_lock:
_active_batches.pop(batch_id, None)
batch_registry.pop(batch_id)
+32 -216
View File
@@ -2,39 +2,24 @@
import asyncio
import threading
import uuid
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, ProxyConfig as ProxyConfigModel, AuditLog
from ..models import User
from ..schemas import ProxyConfigOut, ProxyConfigUpdate
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=["代理与白名单"])
# 运行中的测试: 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)
def get_proxy_config(
db: Session = Depends(get_db),
_: User = Depends(require_permission("proxy:manage")),
):
return _get_or_create(db)
return proxy_service.get_or_create(db)
@router.put("", response_model=ProxyConfigOut)
@@ -43,21 +28,17 @@ def update_proxy_config(
db: Session = Depends(get_db),
current: User = Depends(require_permission("proxy:manage")),
):
cfg = _get_or_create(db)
cfg.enabled = req.enabled
cfg.api_url = req.api_url
cfg.http = req.http
cfg.https = req.https
cfg.whitelist_enabled = req.whitelist_enabled
cfg.whitelist_uid = req.whitelist_uid
cfg.whitelist_ukey = req.whitelist_ukey
db.commit()
db.refresh(cfg)
db.add(AuditLog(user_id=current.id, username=current.username,
action="proxy:update", target="proxy_config"))
db.commit()
return cfg
return proxy_service.update_config(
db,
enabled=req.enabled,
api_url=req.api_url,
http=req.http,
https=req.https,
whitelist_enabled=req.whitelist_enabled,
whitelist_uid=req.whitelist_uid,
whitelist_ukey=req.whitelist_ukey,
current_user=current,
)
# ---- WebSocket 日志推送 ----
@@ -65,7 +46,6 @@ def update_proxy_config(
@router.websocket("/ws/test/{test_id}")
async def ws_test_logs(websocket: WebSocket, test_id: str):
"""WebSocket 推送代理/白名单测试实时日志(需认证)。"""
# 认证:从 cookie 或 token query param 验证用户身份
user = authenticate_websocket(websocket)
if not user:
await websocket.close(code=1008, reason="未授权")
@@ -73,8 +53,7 @@ async def ws_test_logs(websocket: WebSocket, test_id: str):
await websocket.accept()
with _active_tests_lock:
test = _active_tests.get(test_id)
test = proxy_service.get_test(test_id)
if not test:
await websocket.send_json({"level": "error", "message": "测试任务不存在"})
await websocket.close()
@@ -96,169 +75,10 @@ async def ws_test_logs(websocket: WebSocket, test_id: str):
except WebSocketDisconnect:
pass
finally:
with _active_tests_lock:
_active_tests.pop(test_id, None)
proxy_service.remove_test(test_id)
# ---- 异步测试执行 ----
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 端点 ----
# ---- 异步测试 API 端点 ----
@router.post("/test")
async def test_proxy(
@@ -266,16 +86,14 @@ async def test_proxy(
current: User = Depends(require_permission("proxy:manage")),
):
"""启动代理测试(异步执行,通过 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]
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
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 = threading.Thread(
target=proxy_service.run_proxy_test,
args=(cfg, log_queue, loop),
daemon=True,
)
thread.start()
return {"test_id": test_id, "success": True}
@@ -287,16 +105,14 @@ async def test_whitelist(
current: User = Depends(require_permission("whitelist:test")),
):
"""启动白名单测试(异步执行,通过 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]
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
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 = threading.Thread(
target=proxy_service.run_whitelist_test,
args=(cfg, log_queue, loop),
daemon=True,
)
thread.start()
return {"test_id": test_id, "success": True}
+76
View File
@@ -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
+31 -2
View File
@@ -3,6 +3,7 @@
import asyncio
import threading
import uuid
from types import SimpleNamespace
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from typing import Optional
@@ -10,7 +11,6 @@ from typing import Optional
from sqlalchemy.orm import Session
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 ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel
@@ -128,7 +128,7 @@ class LoginBatchRunner:
return
try:
account = Account(
account = SimpleNamespace(
username=acc_info["username"],
password=acc_info["password"],
email=acc_info["email"],
@@ -261,5 +261,34 @@ class LoginBatchRunner:
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(避免循环导入)
from ..database import SessionLocal
+240
View File
@@ -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()