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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user