feat(web): 顶栏账号菜单接入 bot 启停 / 插件页分区 / 首页协议端配对信息
- 重启/关闭 bot 从设置页提到顶栏账号下拉(破坏性操作弹确认框),等待回连逻辑抽成 lib/botAction.ts 供两处共用:先等旧进程让出端口,再以 dashboard SSE 真收到一帧为准, 只看 HTTP 200 会在"端口起来但插件未挂接"时误判就绪 - 插件管理页按 带 Web 页面 / 带配置项 / 都没有 分区,卡片加类型与 Web/配置 徽章 - 首页「BOT 协议端信息」分两半:左半登录账号,右半 NoneBot 接入信息 (IP / 运行端口 / WS 路径 / 访问令牌,均可一键复制,另给整条反向 WS 地址), 监听地址非 0.0.0.0 时提示仅本机可连;「BOT 后端信息」移到自己一行 - dashboard.py 新增 endpoint 段:读 driver.config 的 host/port、驱动实际注册的 反向 WS 路由(自动跳过根路径与尾斜杠别名)、适配器令牌与本机网卡 IP Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+111
-9
@@ -3,6 +3,7 @@
|
||||
|
||||
数据来源与 nonebot_plugin_picstatus 类似:
|
||||
- bot 账号 / WS 连通性:从 OneBot V11 适配器的 bots / connections 读取
|
||||
- 协议端接入信息:driver.config 的 host/port + 驱动实际注册的反向 WS 路由 + 本机网卡 IP
|
||||
- host 性能:psutil(CPU/内存/磁盘/进程)+ platform + nonebot 版本
|
||||
仅在 /hub/api/dashboard 被调用时执行,不做常驻采样。
|
||||
同步采集(psutil 等可能阻塞的调用)整体放入线程池执行,避免阻塞事件循环;
|
||||
@@ -15,6 +16,7 @@ psutil.cpu_percent/Process.cpu_percent 会真的 sleep 住调用线程,而 SSE
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
|
||||
@@ -202,6 +204,102 @@ def _backend_sync() -> dict:
|
||||
return info
|
||||
|
||||
|
||||
def _is_private_ip(ip: str) -> bool:
|
||||
"""RFC1918 私有网段(192.168/10/172.16-31),用于把局域网地址排在前面。"""
|
||||
parts = ip.split(".")
|
||||
if len(parts) != 4:
|
||||
return False
|
||||
try:
|
||||
a, b = int(parts[0]), int(parts[1])
|
||||
except ValueError:
|
||||
return False
|
||||
return a == 10 or (a == 192 and b == 168) or (a == 172 and 16 <= b <= 31)
|
||||
|
||||
|
||||
def _local_ips() -> list[str]:
|
||||
"""本机可被协议端访问的 IPv4 列表(私有网段优先,排除环回/链路本地)。"""
|
||||
ips: list[str] = []
|
||||
try:
|
||||
addrs = psutil.net_if_addrs()
|
||||
stats = psutil.net_if_stats()
|
||||
except Exception:
|
||||
return ips
|
||||
for name, addr_list in addrs.items():
|
||||
st = stats.get(name)
|
||||
if st is not None and not st.isup:
|
||||
continue
|
||||
for addr in addr_list:
|
||||
ip = (addr.address or "").strip()
|
||||
# 只保留 IPv4(忽略 MAC / IPv6),排除环回与自动分配地址
|
||||
if not ip or ":" in ip or ip.count(".") < 3:
|
||||
continue
|
||||
if ip.startswith(("127.", "169.254.")):
|
||||
continue
|
||||
if ip not in ips:
|
||||
ips.append(ip)
|
||||
return sorted(ips, key=lambda ip: (not _is_private_ip(ip), ip))
|
||||
|
||||
|
||||
def _ws_path() -> str:
|
||||
"""反向 WS 路径:以驱动实际注册的 websocket 路由为准(跟随适配器版本)。"""
|
||||
default = "/onebot/v11/ws"
|
||||
try:
|
||||
from nonebot import get_driver
|
||||
|
||||
routes = getattr(get_driver().server_app, "routes", None) or []
|
||||
except Exception:
|
||||
return default
|
||||
paths = [
|
||||
str(getattr(route, "path", "") or "")
|
||||
for route in routes
|
||||
if "WebSocket" in type(route).__name__
|
||||
]
|
||||
paths = [p for p in paths if "/onebot/" in p]
|
||||
if not paths:
|
||||
return default
|
||||
# 同一条路由注册了根路径与尾斜杠别名,优先规范路径 /onebot/v11/ws
|
||||
return sorted(paths, key=lambda p: (not p.rstrip("/").endswith("/ws"), len(p)))[0]
|
||||
|
||||
|
||||
def _access_token() -> str:
|
||||
"""反向 WS 授权令牌(OneBot V11 适配器配置;未设置时协议端无需带 Authorization)。"""
|
||||
try:
|
||||
for bot in get_bots().values():
|
||||
adapter = getattr(bot, "adapter", None)
|
||||
cfg = getattr(adapter, "onebot_config", None)
|
||||
token = getattr(cfg, "onebot_access_token", None)
|
||||
if token:
|
||||
return str(token)
|
||||
except Exception:
|
||||
pass
|
||||
return os.environ.get("ONEBOT_V11_ACCESS_TOKEN", "") or ""
|
||||
|
||||
|
||||
def _endpoint_sync() -> dict:
|
||||
"""协议端接入信息:监听地址 / 端口 / 反向 WS 路径 / 授权令牌 / 可用 IP。
|
||||
|
||||
供 Web 首页展示「协议端配对」信息,只读 driver.config 与驱动的路由表,不额外起服务。
|
||||
"""
|
||||
host = ""
|
||||
port = None
|
||||
try:
|
||||
from nonebot import get_driver
|
||||
|
||||
config = get_driver().config
|
||||
host = str(getattr(config, "host", "") or "")
|
||||
raw_port = getattr(config, "port", None)
|
||||
port = int(raw_port) if raw_port else None
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"ws_path": _ws_path(),
|
||||
"access_token": _access_token(),
|
||||
"ips": _local_ips(),
|
||||
}
|
||||
|
||||
|
||||
def _system_sync() -> dict:
|
||||
boot = psutil.boot_time()
|
||||
now = time.time()
|
||||
@@ -303,15 +401,18 @@ async def collect_dashboard() -> dict:
|
||||
now = time.time()
|
||||
if _DASH_CACHE["data"] is not None and now - _DASH_CACHE["time"] < _DASH_CACHE_TTL:
|
||||
return _DASH_CACHE["data"]
|
||||
cpu, memory, process, system, disk, network, backend, bots = await asyncio.gather(
|
||||
asyncio.to_thread(_cpu_sync),
|
||||
asyncio.to_thread(_memory_sync),
|
||||
asyncio.to_thread(_process_sync),
|
||||
asyncio.to_thread(_system_sync),
|
||||
asyncio.to_thread(_disk_sync),
|
||||
asyncio.to_thread(_network_sync),
|
||||
asyncio.to_thread(_backend_sync),
|
||||
_collect_bots(), # 需要走 bot API,必须留在事件循环
|
||||
cpu, memory, process, system, disk, network, backend, endpoint, bots = (
|
||||
await asyncio.gather(
|
||||
asyncio.to_thread(_cpu_sync),
|
||||
asyncio.to_thread(_memory_sync),
|
||||
asyncio.to_thread(_process_sync),
|
||||
asyncio.to_thread(_system_sync),
|
||||
asyncio.to_thread(_disk_sync),
|
||||
asyncio.to_thread(_network_sync),
|
||||
asyncio.to_thread(_backend_sync),
|
||||
asyncio.to_thread(_endpoint_sync),
|
||||
_collect_bots(), # 需要走 bot API,必须留在事件循环
|
||||
)
|
||||
)
|
||||
data = {
|
||||
"bots": bots,
|
||||
@@ -322,6 +423,7 @@ async def collect_dashboard() -> dict:
|
||||
"disk": disk,
|
||||
"network": network,
|
||||
"backend": backend,
|
||||
"endpoint": endpoint,
|
||||
}
|
||||
_DASH_CACHE["time"] = now
|
||||
_DASH_CACHE["data"] = data
|
||||
|
||||
Reference in New Issue
Block a user