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

@@ -25,6 +25,8 @@ def _to_datetime(value: Union[str, datetime, None]) -> Optional[datetime]:
def _row_to_proxy(row: Tuple) -> Proxy:
validated = int(row[7]) if len(row) > 7 and row[7] is not None else 0
use_count = int(row[8]) if len(row) > 8 and row[8] is not None else 0
return Proxy(
ip=row[0],
port=row[1],
@@ -33,12 +35,13 @@ def _row_to_proxy(row: Tuple) -> Proxy:
response_time_ms=row[4],
last_check=_to_datetime(row[5]),
created_at=_to_datetime(row[6]),
validated=int(row[7]) if len(row) > 7 and row[7] is not None else 0,
validated=validated,
use_count=use_count,
)
_SELECT_PROXY_COLS = (
"ip, port, protocol, score, response_time_ms, last_check, created_at, validated"
"ip, port, protocol, score, response_time_ms, last_check, created_at, validated, use_count"
)
@@ -58,8 +61,8 @@ class ProxyRepository:
try:
await db.execute(
"""
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1)
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated, use_count)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 0)
ON CONFLICT(ip, port) DO UPDATE SET
protocol = excluded.protocol,
score = excluded.score,
@@ -87,13 +90,14 @@ class ProxyRepository:
protocol = "http"
await db.execute(
"""
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated, use_count)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0, 0)
ON CONFLICT(ip, port) DO UPDATE SET
protocol = excluded.protocol,
score = excluded.score,
last_check = CURRENT_TIMESTAMP,
validated = 0
validated = 0,
use_count = 0
""",
(ip, port, protocol, initial_score),
)
@@ -113,13 +117,14 @@ class ProxyRepository:
rows.append((p.ip, p.port, proto, initial_score))
await db.executemany(
"""
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)
INSERT INTO proxies (ip, port, protocol, score, last_check, created_at, validated, use_count)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0, 0)
ON CONFLICT(ip, port) DO UPDATE SET
protocol = excluded.protocol,
score = excluded.score,
last_check = CURRENT_TIMESTAMP,
validated = 0
validated = 0,
use_count = 0
""",
rows,
)
@@ -176,6 +181,29 @@ class ProxyRepository:
logger.error(f"update_response_time failed: {e}", exc_info=True)
return False
@staticmethod
async def set_use_count_and_score(
db: aiosqlite.Connection,
ip: str,
port: int,
use_count: int,
score: int,
) -> bool:
try:
await db.execute(
"""
UPDATE proxies
SET use_count = ?, score = ?, last_check = CURRENT_TIMESTAMP
WHERE ip = ? AND port = ? AND validated = 1
""",
(use_count, score, ip, port),
)
await db.commit()
return db.total_changes > 0
except Exception as e:
logger.error(f"set_use_count_and_score failed: {e}", exc_info=True)
return False
@staticmethod
async def delete(db: aiosqlite.Connection, ip: str, port: int) -> None:
await db.execute("DELETE FROM proxies WHERE ip = ? AND port = ?", (ip, port))
@@ -369,21 +397,34 @@ class ProxyRepository:
@staticmethod
async def get_stats(db: aiosqlite.Connection) -> dict:
"""统计快照。
协议计数http/https/socks*)仅含已验证且 score>0 的可用代理,供首页图表与「可用」口径一致。
pending_* 为待验证池validated=0按协议分布。
"""
query = """
SELECT
COUNT(*) as total,
COUNT(CASE WHEN validated = 0 THEN 1 END) as pending,
COUNT(CASE WHEN validated = 1 AND score > 0 THEN 1 END) as available,
(SELECT AVG(score) FROM proxies WHERE validated = 1 AND score > 0) as avg_score,
COUNT(CASE WHEN protocol = 'http' THEN 1 END) as http_count,
COUNT(CASE WHEN protocol = 'https' THEN 1 END) as https_count,
COUNT(CASE WHEN protocol = 'socks4' THEN 1 END) as socks4_count,
COUNT(CASE WHEN protocol = 'socks5' THEN 1 END) as socks5_count
COUNT(CASE WHEN validated = 1 AND score > 0 AND protocol = 'http' THEN 1 END) as http_count,
COUNT(CASE WHEN validated = 1 AND score > 0 AND protocol = 'https' THEN 1 END) as https_count,
COUNT(CASE WHEN validated = 1 AND score > 0 AND protocol = 'socks4' THEN 1 END) as socks4_count,
COUNT(CASE WHEN validated = 1 AND score > 0 AND protocol = 'socks5' THEN 1 END) as socks5_count,
COUNT(CASE WHEN validated = 0 AND protocol = 'http' THEN 1 END) as pending_http_count,
COUNT(CASE WHEN validated = 0 AND protocol = 'https' THEN 1 END) as pending_https_count,
COUNT(CASE WHEN validated = 0 AND protocol = 'socks4' THEN 1 END) as pending_socks4_count,
COUNT(CASE WHEN validated = 0 AND protocol = 'socks5' THEN 1 END) as pending_socks5_count,
COUNT(CASE WHEN validated = 1 AND score <= 0 THEN 1 END) as invalid_count,
(SELECT AVG(response_time_ms) FROM proxies WHERE validated = 1 AND score > 0
AND response_time_ms IS NOT NULL AND response_time_ms > 0) as avg_response_ms
FROM proxies
"""
async with db.execute(query) as cursor:
row = await cursor.fetchone()
if row:
avg_lat = row[13]
return {
"total": row[0] or 0,
"pending": row[1] or 0,
@@ -393,6 +434,12 @@ class ProxyRepository:
"https_count": row[5] or 0,
"socks4_count": row[6] or 0,
"socks5_count": row[7] or 0,
"pending_http_count": row[8] or 0,
"pending_https_count": row[9] or 0,
"pending_socks4_count": row[10] or 0,
"pending_socks5_count": row[11] or 0,
"invalid_count": row[12] or 0,
"avg_response_ms": round(avg_lat, 2) if avg_lat is not None else None,
}
return {
"total": 0,
@@ -403,6 +450,12 @@ class ProxyRepository:
"https_count": 0,
"socks4_count": 0,
"socks5_count": 0,
"pending_http_count": 0,
"pending_https_count": 0,
"pending_socks4_count": 0,
"pending_socks5_count": 0,
"invalid_count": 0,
"avg_response_ms": None,
}
@staticmethod