feat: JSON 配置、质量分与仪表盘,及设置与爬取流程

- 后端改为 config/app.json;pytest 使用 config/app.test.json 与 set_config_file,不再依赖环境变量;移除 pydantic-settings。

- 前端 API/WebSocket 由 config/webui.json 经 Vite define 注入。

- 代理分数按延迟与随机取用次数计算,新增 use_count 与 proxy_scoring;保存设置时同步调度器启停。

- 仪表盘双饼图(可用/待验证协议);设置页去掉调度器启停按钮并移动立即验证;爬取全部结束后自动提交全量验证。

- 删除 script/settings_maintain.py(此前已标记删除)。

Made-with: Cursor
This commit is contained in:
祀梦
2026-04-05 16:08:32 +08:00
parent 07248ff4ee
commit 7bc6d4e4de
31 changed files with 643 additions and 280 deletions

View File

@@ -1,77 +1,111 @@
"""全局配置 - 使用 Pydantic Settings 支持环境变量和 .env 文件"""
import os
from typing import List
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
"""全局配置:仅从 JSON 文件加载,不使用环境变量。"""
from __future__ import annotations
import json
import logging
from typing import Any, Dict, List
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
from pydantic import BaseModel, ConfigDict
# 数据库配置(环境变量 PROXYPOOL_DB_PATH 优先,供 pytest 与生产隔离)
db_path: str = Field(
default="db/proxies.sqlite",
validation_alias=AliasChoices("PROXYPOOL_DB_PATH", "DB_PATH", "db_path"),
)
from app.core.config_paths import project_root, resolved_config_path
# API 服务配置
host: str = "127.0.0.1"
port: int = 18080
logger = logging.getLogger("ProxyPool")
# 验证器配置
validator_timeout: int = 5
validator_max_concurrency: int = 200
validator_connect_timeout: int = 3
# 爬虫配置
crawler_num_validators: int = 50
crawler_max_queue_size: int = 500
# 日志配置
log_level: str = "INFO"
log_dir: str = "logs"
# WebSocket统计广播间隔无连接时不查库
ws_stats_interval_seconds: int = 1
# 导出配置
export_max_records: int = 10000
# 代理评分配置
score_valid: int = 10
score_invalid: int = -5
score_min: int = 0
score_max: int = 100
# 验证目标配置
validator_test_urls: List[str] = [
_DEFAULTS: Dict[str, Any] = {
"db_path": "db/proxies.sqlite",
"host": "127.0.0.1",
"port": 18080,
"validator_timeout": 5,
"validator_max_concurrency": 200,
"validator_connect_timeout": 3,
"crawler_num_validators": 50,
"crawler_max_queue_size": 500,
"log_level": "INFO",
"log_dir": "logs",
"ws_stats_interval_seconds": 1,
"export_max_records": 10000,
"score_valid": 10,
"score_invalid": -5,
"score_min": 0,
"score_max": 100,
"score_latency_ref_ms": 500.0,
"score_use_penalty_per_pick": 2.5,
"score_max_use_penalty": 70.0,
"score_default_latency_ms": 1500.0,
"validator_test_urls": [
"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"
# CORS 配置 - Pydantic v2 会自动将逗号分隔的字符串解析为 List[str]
cors_origins: List[str] = [
],
"plugins_dir": "plugins",
"cors_origins": [
"http://localhost:8080",
"http://localhost:5173",
"http://127.0.0.1:18081",
"http://localhost:18081",
]
],
"run_network_tests": False,
}
def _load_merged_dict() -> Dict[str, Any]:
data = dict(_DEFAULTS)
path = resolved_config_path()
if not path.is_file():
logger.warning("配置文件不存在,使用内置默认项: %s", path)
return data
try:
with path.open(encoding="utf-8") as f:
file_data = json.load(f)
if not isinstance(file_data, dict):
logger.error("配置文件须为 JSON 对象,已忽略: %s", path)
return data
data.update(file_data)
except (json.JSONDecodeError, OSError) as e:
logger.error("读取配置文件失败,使用内置默认项: %s (%s)", path, e)
return data
class AppSettings(BaseModel):
"""应用配置(与 config/app.json 字段一致)"""
model_config = ConfigDict(extra="ignore")
db_path: str
host: str
port: int
validator_timeout: int
validator_max_concurrency: int
validator_connect_timeout: int
crawler_num_validators: int
crawler_max_queue_size: int
log_level: str
log_dir: str
ws_stats_interval_seconds: int
export_max_records: int
score_valid: int
score_invalid: int
score_min: int
score_max: int
score_latency_ref_ms: float
score_use_penalty_per_pick: float
score_max_use_penalty: float
score_default_latency_ms: float
validator_test_urls: List[str]
plugins_dir: str
cors_origins: List[str]
run_network_tests: bool = False
@property
def base_dir(self) -> str:
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return str(project_root())
# 全局配置实例(启动时加载一次
settings = Settings()
# 全局单例(进程内首次导入时按当前 resolved_config_path() 加载
settings = AppSettings.model_validate(_load_merged_dict())
# 历史代码别名
Settings = AppSettings