diff --git a/WebUI/src/views/Settings.vue b/WebUI/src/views/Settings.vue index 2dd39f4..2cef704 100644 --- a/WebUI/src/views/Settings.vue +++ b/WebUI/src/views/Settings.vue @@ -130,6 +130,27 @@ /> + + + + + 代理验证时将随机轮询这些地址,建议包含多个国内外站点 + + { if (schedulerRunning.value) { diff --git a/app/api/lifespan.py b/app/api/lifespan.py index 4d8bca1..16e0ad8 100644 --- a/app/api/lifespan.py +++ b/app/api/lifespan.py @@ -41,6 +41,8 @@ async def lifespan(app: FastAPI): connect_timeout=app_settings.validator_connect_timeout, max_concurrency=db_settings.get("default_concurrency", app_settings.validator_max_concurrency), ) + if db_settings.get("validation_targets"): + validator.update_test_urls(db_settings["validation_targets"]) # 验证 WorkerPool async def validation_handler(proxy): diff --git a/app/api/routes/settings.py b/app/api/routes/settings.py index 20b7960..3f41bfb 100644 --- a/app/api/routes/settings.py +++ b/app/api/routes/settings.py @@ -45,9 +45,11 @@ async def save_settings(request: SettingsSchema, http_request: Request): validator._init_timeout = request.validation_timeout validator._init_connect_timeout = request.validation_timeout validator._init_max_concurrency = request.default_concurrency + if request.validation_targets: + validator.update_test_urls(request.validation_targets) # 重新创建 semaphore 和 session validator._semaphore = None await validator.close() - logger.info(f"Validator config updated: timeout={request.validation_timeout}, concurrency={request.default_concurrency}") + logger.info(f"Validator config updated: timeout={request.validation_timeout}, concurrency={request.default_concurrency}, targets={request.validation_targets}") return success_response("保存设置成功", request.model_dump()) diff --git a/app/core/config.py b/app/core/config.py index 4baf881..9230fa5 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -16,7 +16,7 @@ class Settings(BaseSettings): # API 服务配置 host: str = "127.0.0.1" - port: int = 9949 + port: int = 18080 # 验证器配置 validator_timeout: int = 5 @@ -40,6 +40,16 @@ class Settings(BaseSettings): score_min: int = 0 score_max: int = 100 + # 验证目标配置 + validator_test_urls: List[str] = [ + "http://httpbin.org/ip", + "https://httpbin.org/ip", + "http://api.ipify.org", + "https://api.ipify.org", + "http://www.baidu.com", + "http://www.qq.com", + ] + # 插件配置 plugins_dir: str = "plugins" diff --git a/app/models/schemas.py b/app/models/schemas.py index c261caf..3916c4f 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -47,6 +47,16 @@ class SettingsSchema(BaseModel): proxy_expiry_days: int = Field(default=7, ge=1, le=30) auto_validate: bool = True validate_interval_minutes: int = Field(default=30, ge=5, le=1440) + validation_targets: List[str] = Field( + default=[ + "http://httpbin.org/ip", + "https://httpbin.org/ip", + "http://api.ipify.org", + "https://api.ipify.org", + "http://www.baidu.com", + "http://www.qq.com", + ] + ) class CrawlResult(BaseModel): diff --git a/app/repositories/settings_repo.py b/app/repositories/settings_repo.py index fa9afe7..d6487be 100644 --- a/app/repositories/settings_repo.py +++ b/app/repositories/settings_repo.py @@ -14,6 +14,14 @@ DEFAULT_SETTINGS = { "proxy_expiry_days": 7, "auto_validate": True, "validate_interval_minutes": 30, + "validation_targets": [ + "http://httpbin.org/ip", + "https://httpbin.org/ip", + "http://api.ipify.org", + "https://api.ipify.org", + "http://www.baidu.com", + "http://www.qq.com", + ], } @@ -33,6 +41,11 @@ class SettingsRepository: settings[key] = value.lower() == "true" elif isinstance(default, int): settings[key] = int(value) + elif isinstance(default, list): + try: + settings[key] = json.loads(value) + except json.JSONDecodeError: + settings[key] = default else: settings[key] = value except Exception as e: @@ -43,6 +56,10 @@ class SettingsRepository: async def save(db: aiosqlite.Connection, settings: Dict[str, Any]) -> bool: try: for key, value in settings.items(): + if isinstance(value, list): + stored_value = json.dumps(value, ensure_ascii=False) + else: + stored_value = str(value) await db.execute( """ INSERT INTO settings (key, value, updated_at) @@ -51,7 +68,7 @@ class SettingsRepository: value = excluded.value, updated_at = CURRENT_TIMESTAMP """, - (key, str(value)), + (key, stored_value), ) await db.commit() return True diff --git a/app/services/validator_service.py b/app/services/validator_service.py index b41f894..8294176 100644 --- a/app/services/validator_service.py +++ b/app/services/validator_service.py @@ -4,7 +4,7 @@ import random import time import aiohttp import aiohttp_socks -from typing import Tuple, Optional +from typing import Tuple, Optional, List from app.core.config import settings as app_settings from app.core.log import logger @@ -16,10 +16,20 @@ class ValidatorService: 支持动态读取配置,实现设置热更新。 """ - # 测试 URL - TEST_URLS = { - "http": ["http://httpbin.org/ip", "http://api.ipify.org"], - "https": ["https://httpbin.org/ip", "https://api.ipify.org"], + # 测试 URL 默认池 + DEFAULT_TEST_URLS = { + "http": [ + "http://httpbin.org/ip", + "http://api.ipify.org", + "http://www.baidu.com", + "http://www.qq.com", + ], + "https": [ + "https://httpbin.org/ip", + "https://api.ipify.org", + "https://www.baidu.com", + "https://www.qq.com", + ], } def __init__( @@ -37,6 +47,7 @@ class ValidatorService: self._http_session: Optional[aiohttp.ClientSession] = None self._semaphore: Optional[asyncio.Semaphore] = None self._lock = asyncio.Lock() + self._test_urls: Optional[List[str]] = None @property def timeout(self) -> float: @@ -75,7 +86,17 @@ class ValidatorService: return self._semaphore def _get_test_url(self, protocol: str) -> str: - urls = self.TEST_URLS.get(protocol.lower(), self.TEST_URLS["http"]) + custom_urls = self._test_urls + if not custom_urls: + from app.core.config import settings as app_settings + custom_urls = getattr(app_settings, "validator_test_urls", None) + if custom_urls and isinstance(custom_urls, list) and len(custom_urls) > 0: + # 按协议过滤自定义 URL,如果没有匹配的则使用全部 + filtered = [u for u in custom_urls if u.lower().startswith(protocol.lower())] + if filtered: + return random.choice(filtered) + return random.choice(custom_urls) + urls = self.DEFAULT_TEST_URLS.get(protocol.lower(), self.DEFAULT_TEST_URLS["http"]) return random.choice(urls) async def validate(self, ip: str, port: int, protocol: str = "http") -> Tuple[bool, float]: @@ -133,6 +154,10 @@ class ValidatorService: return True, latency return False, 0.0 + def update_test_urls(self, urls: List[str]) -> None: + """运行时更新验证目标 URL 列表""" + self._test_urls = list(urls) if urls else None + async def close(self) -> None: """关闭共享的 HTTP ClientSession""" if self._http_session and not self._http_session.closed: