feat: fpw plugins, validation/crawl perf, WS stats, test DB isolation

- Add Free_Proxy_Website-style fpw_* plugins and register them
- Per-plugin crawl timeout (crawl_timeout_seconds=120); remove global crawl_timeout setting
- Validator: fix connect vs total timeout on save; SOCKS session LRU cache; drop redundant semaphore
- Validation handler uses single DB connection; batch upsert after crawl; WorkerPool put_nowait
- Remove unused max_retries from settings API/UI; settings maintenance SQL + init_db cleanup of deprecated keys
- WebSocket dashboard stats; ProxyList pool_filter and API alignment
- POST /api/proxies/delete-one for IPv6-safe deletes; task poll stops on 404
- pytest uses PROXYPOOL_DB_PATH=db/proxies.test.sqlite so tests do not wipe production DB
- .gitignore: explicit proxies.test.sqlite patterns; fix plugin_service ValidationException import

Made-with: Cursor
This commit is contained in:
祀梦
2026-04-05 13:39:19 +08:00
parent 92c7fa19e2
commit 0131c8b408
63 changed files with 2331 additions and 531 deletions

View File

@@ -3,7 +3,7 @@ import asyncio
from contextlib import AsyncExitStack, asynccontextmanager
from fastapi import FastAPI
from app.core.db import init_db, get_db
from app.core.db import init_db, get_db, get_db_connection
from app.core.config import settings as app_settings
from app.core.log import logger
from app.core.execution import AsyncWorkerPool, JobExecutor
@@ -13,6 +13,8 @@ from app.repositories.settings_repo import SettingsRepository, DEFAULT_SETTINGS
from app.services.validator_service import ValidatorService
from app.services.plugin_runner import PluginRunner
from app.services.scheduler_service import SchedulerService
from app.api.ws_manager import ConnectionManager
from app.api.realtime import stats_broadcaster_loop
settings_repo = SettingsRepository()
proxy_repo = ProxyRepository()
@@ -46,22 +48,50 @@ async def lifespan(app: FastAPI):
# 验证 WorkerPool
async def validation_handler(proxy):
from app.models.domain import ProxyRaw
is_valid, latency = await validator.validate(
proxy.ip, proxy.port, proxy.protocol
)
async with get_db() as db:
if is_valid:
await proxy_repo.insert_or_update(
db, proxy.ip, proxy.port, proxy.protocol, score=app_settings.score_valid
)
if latency:
await proxy_repo.update_response_time(db, proxy.ip, proxy.port, latency)
async with get_db_connection() as db:
existing = await proxy_repo.get_by_ip_port(db, proxy.ip, proxy.port)
is_valid, latency = await validator.validate(
proxy.ip, proxy.port, proxy.protocol
)
if not existing:
return
if existing.validated == 0:
if is_valid:
await proxy_repo.insert_or_update(
db,
proxy.ip,
proxy.port,
proxy.protocol,
score=app_settings.score_valid,
)
if latency:
await proxy_repo.update_response_time(
db, proxy.ip, proxy.port, latency
)
else:
await proxy_repo.delete(db, proxy.ip, proxy.port)
else:
await proxy_repo.update_score(
db, proxy.ip, proxy.port, app_settings.score_invalid,
app_settings.score_min, app_settings.score_max
)
if is_valid:
await proxy_repo.insert_or_update(
db,
proxy.ip,
proxy.port,
proxy.protocol,
score=app_settings.score_valid,
)
if latency:
await proxy_repo.update_response_time(
db, proxy.ip, proxy.port, latency
)
else:
await proxy_repo.update_score(
db,
proxy.ip,
proxy.port,
app_settings.score_invalid,
app_settings.score_min,
app_settings.score_max,
)
worker_pool = AsyncWorkerPool(
worker_count=db_settings.get("default_concurrency", app_settings.validator_max_concurrency),
@@ -75,7 +105,7 @@ async def lifespan(app: FastAPI):
await stack.enter_async_context(executor)
# 插件运行器
plugin_runner = PluginRunner(timeout=db_settings.get("crawl_timeout", 30))
plugin_runner = PluginRunner()
# 调度器
scheduler = SchedulerService(
@@ -91,6 +121,9 @@ async def lifespan(app: FastAPI):
app.state.plugin_runner = plugin_runner
app.state.scheduler = scheduler
app.state.ws_manager = ConnectionManager()
app.state.stats_broadcaster_task = asyncio.create_task(stats_broadcaster_loop(app))
# 启动调度器
if db_settings.get("auto_validate", True):
try:
@@ -101,6 +134,13 @@ async def lifespan(app: FastAPI):
logger.info("API server started")
yield
app.state.stats_broadcaster_task.cancel()
try:
await app.state.stats_broadcaster_task
except asyncio.CancelledError:
pass
await app.state.ws_manager.disconnect_all()
# 停止调度器
await scheduler.stop()