feat: 群分析本地消息库 / 处理中占位图 / Web 日志窗口重构

- 群分析: 新增 history_store 只读本地消息源(读 learning_chat 落库,含 uninfo
  昵称补齐/去重/截断判定),适配器优先读本地库、失败回退 OneBot 分页;分页
  锚点字段回退链修复 NapCat 传 message_seq 翻页断裂;新增「本地记录」开关
- core/message_utils: 新增 common_proc_reply 占位图通用回复(引用消息 + 处理中
  动图,支持后台任务显式指定 target),群分析/战况/倒放改用
- web_hub + web: 日志页改固定窗口滚动 + 翻页锚定 + 自动换行,SSE 日志轮转发
  reset 帧,入口 HTML no-cache,行数统计增量缓存,CPU 改非阻塞采样,登录信息缓存
- 插件内 CLAUDE.md / DESIGN.md 不入库(.gitignore),galgame_card 两份文档取消
  跟踪(文件保留在磁盘)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:59:01 +08:00
co-authored by Claude Code
parent 662c28cb2f
commit 51b08ccb68
23 changed files with 1100 additions and 393 deletions
+32 -14
View File
@@ -6,7 +6,10 @@
- host 性能:psutil(CPU/内存/磁盘/进程)+ platform + nonebot 版本
仅在 /hub/api/dashboard 被调用时执行,不做常驻采样。
同步采集(psutil 等可能阻塞的调用)整体放入线程池执行,避免阻塞事件循环;
结果做 1 秒短缓存,多个前端标签页(含 SSE 循环)不至于各自重复采集。
结果做短缓存,多个前端标签页(含 SSE 循环)不至于各自重复采集。
CPU 采样走非阻塞模式(interval=None,与上一次调用求差):带 interval 的
psutil.cpu_percent/Process.cpu_percent 会真的 sleep 住调用线程,而 SSE 循环
每轮都调用一次,会凭空常驻烧掉约四分之一个核。
"""
from __future__ import annotations
@@ -28,21 +31,31 @@ _MODULE_START = time.time()
# 网络速率采样:记录上一次累计计数与时间,用于计算实时 bytes/s 用量
_NET_LAST: dict = {"time": None, "sent": None, "recv": None}
# 采集结果短缓存(多标签页 / SSE 每 2s 循环共用一份结果)
# 采集结果短缓存(多标签页 / SSE 循环共用一份结果)
_DASH_CACHE: dict = {"time": 0.0, "data": None}
_DASH_CACHE_TTL = 1.0
_DASH_CACHE_TTL = 2.0
# 进程对象与 CPU 采样必须跨调用复用:psutil 的非阻塞模式(interval=None)
# 是「与上一次调用」求差,换对象或首次调用只会拿到 0.0。
_PROC = psutil.Process()
psutil.cpu_percent(None, True) # 预热 CPU 采样(首次调用恒为 0.0)
_PROC.cpu_percent(None) # 预热进程采样
# bot 昵称(get_login_info)基本不变,长缓存避免每轮都打一次 OneBot API
_LOGIN_CACHE: dict[str, tuple[float, str]] = {}
_LOGIN_TTL = 600.0
def _cpu_sync() -> dict:
try:
per_core = psutil.cpu_percent(0.3, True)
per_core = psutil.cpu_percent(None, True)
except TypeError:
# 兼容部分平台不接受 percpu 参数
per_core = []
if per_core:
percent = sum(per_core) / len(per_core)
else:
percent = psutil.cpu_percent(0.3)
percent = psutil.cpu_percent(None)
info: dict = {
"percent": round(float(percent), 1),
"per_core": [round(float(x), 1) for x in (per_core or [])],
@@ -84,8 +97,8 @@ def _memory_sync() -> dict:
def _process_sync() -> dict:
try:
proc = psutil.Process()
cpu = proc.cpu_percent(0.2)
proc = _PROC
cpu = proc.cpu_percent(None)
info = {
"pid": proc.pid,
"name": proc.name(),
@@ -253,13 +266,18 @@ async def _collect_bots() -> list:
"msg_sent": None,
}
if ws_connected:
try:
login = await bot.get_login_info()
item["nick"] = login.get("nickname") or item["nick"]
except Exception as e: # noqa: BLE001
logger.warning(
f"获取登录信息失败({bot.self_id}): {type(e).__name__}: {e}"
)
cached = _LOGIN_CACHE.get(bot.self_id)
if cached and time.time() - cached[0] < _LOGIN_TTL:
item["nick"] = cached[1]
else:
try:
login = await bot.get_login_info()
item["nick"] = login.get("nickname") or item["nick"]
_LOGIN_CACHE[bot.self_id] = (time.time(), item["nick"])
except Exception as e: # noqa: BLE001
logger.warning(
f"获取登录信息失败({bot.self_id}): {type(e).__name__}: {e}"
)
try:
status = await bot.get_status()
item["online"] = status.get("online")