"""Pydantic 模型 - 用于 API 请求/响应校验""" from pydantic import BaseModel, Field, field_validator, ConfigDict from typing import Optional, List class ProxyCreate(BaseModel): ip: str port: int = Field(ge=1, le=65535) protocol: str = "http" score: int = Field(default=10, ge=0, le=100) @field_validator("protocol") @classmethod def validate_protocol(cls, v: str): v = v.lower().strip() if v not in ("http", "https", "socks4", "socks5"): raise ValueError("protocol must be http, https, socks4 or socks5") return v class ProxyResponse(BaseModel): ip: str port: int protocol: str score: int response_time_ms: Optional[float] = None last_check: Optional[str] = None validated: int = 0 use_count: int = 0 class PluginResponse(BaseModel): id: str name: str display_name: str description: str enabled: bool last_run: Optional[str] = None success_count: int = 0 failure_count: int = 0 class SettingsSchema(BaseModel): model_config = ConfigDict(extra="ignore") validation_timeout: int = Field(default=6, ge=3, le=60) default_concurrency: int = Field(default=120, ge=10, le=400) min_proxy_score: int = Field(default=0, ge=0, le=100) proxy_expiry_days: int = Field(default=7, ge=1, le=30) auto_validate: bool = True auto_validate_after_crawl: bool = False 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 CrawlSummarySchema(BaseModel): """单次爬取任务结果(与 CrawlJob 返回的 result 对齐)""" plugin_id: str proxy_count: int crawl_failed: bool = False error: Optional[str] = None success_count: int = 0 # 与 proxy_count 相同,兼容旧前端 failure_count: int = 0 class ProxyListRequest(BaseModel): page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100) protocol: Optional[str] = None min_score: int = Field(default=0, ge=0) max_score: Optional[int] = Field(default=None, ge=0) sort_by: str = "last_check" sort_order: str = "DESC" pool_filter: Optional[str] = Field( default=None, description="all 或不传=全部;pending=待验证;available=已验证且可用", ) @field_validator("pool_filter") @classmethod def validate_pool_filter(cls, v: Optional[str]): if v is None or v == "" or v == "all": return None allowed = ("pending", "available") if v not in allowed: raise ValueError(f"pool_filter 必须是 {allowed} 之一或 all") return v @field_validator("protocol") @classmethod def validate_protocol(cls, v): if v is not None and v.lower() not in ("http", "https", "socks4", "socks5"): raise ValueError("协议类型必须是 http, https, socks4 或 socks5") return v.lower() if v else v @field_validator("sort_by") @classmethod def validate_sort_by(cls, v): if v not in ("ip", "port", "protocol", "score", "last_check"): raise ValueError("排序字段必须是 ip, port, protocol, score 或 last_check") return v @field_validator("sort_order") @classmethod def validate_sort_order(cls, v): if v.upper() not in ("ASC", "DESC"): raise ValueError("排序方式必须是 ASC 或 DESC") return v.upper() class ProxyDeleteItem(BaseModel): ip: str port: int = Field(ge=1, le=65535) class BatchDeleteRequest(BaseModel): proxies: List[ProxyDeleteItem] = Field(max_length=1000) class PluginToggleRequest(BaseModel): enabled: bool class ExportRequest(BaseModel): format: str = Field(pattern=r"^(csv|txt|json)$") protocol: Optional[str] = None limit: int = Field(default=10000, ge=1, le=100000)