- hexi core: message handling, rate limiting, cooldown, plugin manager - Custom plugins: BF stats, daily check-in, quotes, persona cards, etc. - Community plugins vendored under hexi/plugins with local fixes - Web admin frontends (learning-chat, persona-admin), unified hexi/web - Tests for rate_limit/cooldown/memes/persona; poetry.lock Co-Authored-By: Claude <noreply@anthropic.com>
125 lines
3.5 KiB
Python
125 lines
3.5 KiB
Python
import asyncio
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import TypeAlias
|
|
|
|
import psutil
|
|
from httpx import AsyncClient, ReadTimeout
|
|
from psutil._common import snetio
|
|
|
|
from ..config import TestSiteCfg, config
|
|
from ..util import match_list_regexp
|
|
from . import (
|
|
BaseTimeBasedCounterCollector,
|
|
NormalTimeBasedCounterCollector,
|
|
PeriodicTimeBasedCounterCollector,
|
|
collector,
|
|
normal_collector,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class NetworkIO:
|
|
name: str
|
|
sent: float
|
|
recv: float
|
|
|
|
|
|
@dataclass
|
|
class NetworkConnectionOK:
|
|
name: str
|
|
status: int
|
|
reason: str
|
|
delay: float
|
|
|
|
|
|
@dataclass
|
|
class NetworkConnectionError:
|
|
name: str
|
|
error: str
|
|
|
|
|
|
NetworkConnectionType: TypeAlias = NetworkConnectionOK | NetworkConnectionError
|
|
|
|
|
|
class BaseNetworkIOCollector(
|
|
BaseTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
|
):
|
|
async def _calc(
|
|
self,
|
|
past: dict[str, snetio],
|
|
now: dict[str, snetio],
|
|
time_passed: float,
|
|
) -> list[NetworkIO]:
|
|
def calc_one(name: str, past_it: snetio, now_it: snetio) -> NetworkIO | None:
|
|
if match_list_regexp(config.ps_ignore_nets, name):
|
|
# logger.info(f"网卡IO统计 {name} 匹配 {regex.re.pattern},忽略")
|
|
return None
|
|
|
|
sent = (now_it.bytes_sent - past_it.bytes_sent) / time_passed
|
|
recv = (now_it.bytes_recv - past_it.bytes_recv) / time_passed
|
|
|
|
if sent == 0 and recv == 0 and config.ps_ignore_0b_net:
|
|
# logger.info(f"网卡IO统计 忽略无IO网卡 {name}")
|
|
return None
|
|
|
|
return NetworkIO(name=name, sent=sent, recv=recv)
|
|
|
|
res = [calc_one(name, past[name], now[name]) for name in past if name in now]
|
|
res = [x for x in res if x]
|
|
if config.ps_sort_nets:
|
|
res.sort(key=lambda x: x.sent + x.recv, reverse=True)
|
|
return res
|
|
|
|
async def _get_obj(self) -> dict[str, snetio]:
|
|
return psutil.net_io_counters(pernic=True)
|
|
|
|
|
|
@collector("network_io")
|
|
class NormalDiskIOCollector(
|
|
BaseNetworkIOCollector,
|
|
NormalTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
|
): ...
|
|
|
|
|
|
@collector("network_io_periodic")
|
|
class PeriodicDiskIOCollector(
|
|
BaseNetworkIOCollector,
|
|
PeriodicTimeBasedCounterCollector[dict[str, snetio], list[NetworkIO]],
|
|
): ...
|
|
|
|
|
|
@normal_collector()
|
|
async def network_connection() -> list[NetworkConnectionType]:
|
|
def format_conn_error(error: Exception) -> str:
|
|
if isinstance(error, ReadTimeout):
|
|
return "超时"
|
|
return error.__class__.__name__
|
|
|
|
async def test_one(site: TestSiteCfg) -> NetworkConnectionType:
|
|
try:
|
|
async with AsyncClient(
|
|
timeout=config.ps_test_timeout,
|
|
proxy=config.proxy if site.use_proxy else None,
|
|
follow_redirects=True,
|
|
) as client:
|
|
start = time.time()
|
|
resp = await client.get(str(site.url))
|
|
delay = (time.time() - start) * 1000
|
|
|
|
except Exception as e:
|
|
return NetworkConnectionError(name=site.name, error=format_conn_error(e))
|
|
|
|
return NetworkConnectionOK(
|
|
name=site.name,
|
|
status=resp.status_code,
|
|
reason=resp.reason_phrase,
|
|
delay=delay,
|
|
)
|
|
|
|
res = await asyncio.gather(*map(test_one, config.ps_test_sites))
|
|
if config.ps_sort_sites:
|
|
res.sort(key=lambda x: x.delay if isinstance(x, NetworkConnectionOK) else -1)
|
|
|
|
return res
|