Files
HeXi/hexi/web/src/pages/home/index.tsx
T
sansenhoshiandClaude 9371a28e35 refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to
Trigger(handlers) → Service(services) → Model(repository/models) + utils):

- Split monolithic __init__.py into handlers/services/utils across
  dailywife, deer_pipe, dice, galgame_card, helldivers_tools,
  huoziyinshua, learning_chat, makeaquote, mc_server_status,
  ncm_saying, picfinder_take, picstatus, random_jm_code, regif,
  steam_info, video_analysis, group_tools
- Move static assets under res/: deer_pipe font/img, makeaquote font,
  helldivers img/templates, huoziyinshua HuoZiYinShua
- Add config.py + register_config_items to ncm_saying, random_jm_code,
  group_tools; learning_chat unified config bridge
- Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in
  dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp
- Disable brash_general_supercredits_tools (stub comment only)
- bot.py: optional stdout/stderr redirect to log file for Web log viewer,
  force ANSI colorize on non-TTY sinks
- Move runtime data (jm_code.json) out of plugin dir into hexi/data
- Docs: plugin-audit-report.md; README reflects removed plugins

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-03 00:44:38 +08:00

395 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react'
import { Avatar, Card } from '@heroui/react'
import { hubDashboard, hubDashboardStream } from '../../api/client'
interface BotInfo {
self_id: string
adapter: string
nick: string
avatar: string
ws_connected: boolean
online: boolean | null
msg_recv: number | null
msg_sent: number | null
}
interface SystemInfo {
hostname: string
os: string
python: string
nonebot: string
boot_time: number
system_uptime_seconds: number
bot_uptime_seconds: number
}
interface CpuInfo {
percent: number
per_core: number[]
logical: number | null
physical: number | null
brand: string
load_avg: number[] | null
}
interface MemInfo {
percent: number
used: number
total: number
available: number
process_rss: number
process_percent: number
}
interface ProcessInfo {
pid: number
name: string
rss: number
cpu_percent: number
memory_percent: number
create_time: number
}
interface DiskInfo {
mount: string
device: string
percent: number
used: number
total: number
}
interface NicInfo {
name: string
up: boolean
sent: number
recv: number
ip: string
}
interface NetworkInfo {
sent: number
recv: number
sent_rate: number
recv_rate: number
packets_sent: number
packets_recv: number
interfaces: NicInfo[]
}
interface BackendInfo {
nonebot_version: string
plugin_count: number
python: string
driver: string
driver_type: string
}
interface DashboardData {
ok: boolean
bots: BotInfo[]
system: SystemInfo
cpu: CpuInfo
memory: MemInfo
process: ProcessInfo
disk: DiskInfo[]
network: NetworkInfo
backend: BackendInfo
}
function fmtBytes(n: number): string {
if (!Number.isFinite(n)) return '—'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let v = n
let i = 0
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
return v.toFixed(1) + ' ' + units[i]
}
function fmtRate(n: number): string {
if (!Number.isFinite(n) || n < 0) return '0 B/s'
return fmtBytes(n) + '/s'
}
function fmtUptime(sec: number): string {
if (!Number.isFinite(sec)) return '—'
const d = Math.floor(sec / 86400)
const h = Math.floor((sec % 86400) / 3600)
const m = Math.floor((sec % 3600) / 60)
const parts: string[] = []
if (d) parts.push(d + ' 天')
if (h) parts.push(h + ' 小时')
parts.push(m + ' 分')
return parts.join(' ')
}
function Bar({ value, color }: { value: number; color?: string }) {
const v = Math.max(0, Math.min(100, value))
const c = color || (v >= 80 ? 'bg-rose-500' : v >= 60 ? 'bg-amber-500' : 'bg-indigo-500')
return (
<div className="h-2 rounded-full bg-slate-100 overflow-hidden">
<div className={'h-full rounded-full transition-all ' + c} style={{ width: v + '%' }} />
</div>
)
}
function coreColor(v: number): string {
if (v >= 80) return 'bg-rose-500'
if (v >= 60) return 'bg-amber-500'
if (v >= 30) return 'bg-indigo-500'
return 'bg-emerald-500'
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between border-b border-slate-100 pb-1.5">
<span className="text-xs text-slate-400">{label}</span>
<span className="text-sm font-medium text-slate-700 text-right">{value || '—'}</span>
</div>
)
}
function SectionTitle({ title, desc }: { title: string; desc: string }) {
return (
<div className="mb-1">
<h3 className="text-base font-semibold text-slate-800">{title}</h3>
<p className="text-xs text-slate-400">{desc}</p>
</div>
)
}
export default function HomePage() {
const [data, setData] = useState<DashboardData | null>(null)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const refreshingRef = useRef(false)
const refresh = useCallback(async (silent = false) => {
if (refreshingRef.current) return
refreshingRef.current = true
if (!silent) setLoading(true)
try {
const d = await hubDashboard()
if (!d || d.ok === false) throw new Error((d && (d as any).msg) || '采集失败')
setData(d)
setError('')
} catch (e: any) {
setError(e.message || '加载失败')
} finally {
refreshingRef.current = false
if (!silent) setLoading(false)
}
}, [])
useEffect(() => {
refresh(true) // 初始静默加载,快速出首屏
const dispose = hubDashboardStream({
onData: (d) => {
if (d && d.ok !== false) { setData(d); setError('') }
},
})
return () => { dispose() }
}, [refresh])
const cpu = data?.cpu
const mem = data?.memory
const proc = data?.process
const sys = data?.system
const net = data?.network
const backend = data?.backend
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-slate-800">总览</h2>
<p className="text-sm text-slate-400">主机与 BOT 综合状态(每 2 秒刷新)</p>
</div>
</div>
{error && !data && <Card className="border border-red-200 bg-red-50"><Card.Content><p className="text-sm text-red-600">{error}</p></Card.Content></Card>}
{/* 1. 主机规格信息:系统 / 处理器 / 内存 / 硬盘 */}
<Card className="border border-slate-200 bg-white">
<Card.Header>
<SectionTitle title="主机规格信息" desc="系统 / 处理器 / 内存 / 硬盘" />
</Card.Header>
<Card.Content>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="rounded-xl border border-slate-200 p-4">
<div className="mb-2 text-sm font-medium text-slate-700">系统</div>
<div className="space-y-1.5">
<InfoRow label="主机名" value={sys?.hostname || ''} />
<InfoRow label="操作系统" value={sys?.os || ''} />
<InfoRow label="运行时长" value={sys ? fmtUptime(sys.system_uptime_seconds) : '—'} />
</div>
</div>
<div className="rounded-xl border border-slate-200 p-4">
<div className="mb-2 text-sm font-medium text-slate-700">处理器</div>
<div className="space-y-1.5">
<InfoRow label="型号" value={cpu?.brand || ''} />
<InfoRow label="逻辑核心" value={cpu?.logical != null ? cpu.logical + ' 核' : '—'} />
<InfoRow label="物理核心" value={cpu?.physical != null ? cpu.physical + ' 核' : '—'} />
</div>
</div>
<div className="rounded-xl border border-slate-200 p-4">
<div className="mb-2 text-sm font-medium text-slate-700">内存</div>
<div className="space-y-1.5">
<InfoRow label="总量" value={mem ? fmtBytes(mem.total) : '—'} />
<InfoRow label="可用" value={mem ? fmtBytes(mem.available) : '—'} />
<InfoRow label="本进程占用" value={mem ? fmtBytes(mem.process_rss) : '—'} />
</div>
</div>
</div>
<div className="mt-4">
<div className="mb-2 text-sm font-medium text-slate-700">硬盘</div>
{!data ? (
<p className="text-sm text-slate-400">加载中…</p>
) : !data.disk.length ? (
<p className="text-sm text-slate-400">未获取到磁盘信息</p>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{data.disk.map((d) => (
<div key={d.mount} className="rounded-xl border border-slate-200 p-4">
<div className="flex items-center justify-between">
<span className="truncate text-xs font-medium text-slate-600" title={d.mount}>{d.mount}</span>
<span className="text-xs text-slate-500">{d.percent}%</span>
</div>
<div className="mt-2"><Bar value={d.percent} /></div>
<p className="mt-2 text-xs text-slate-400">{fmtBytes(d.used)} / {fmtBytes(d.total)}</p>
</div>
))}
</div>
)}
</div>
</Card.Content>
</Card>
{/* 2. 资源占用信息:CPU / 内存 / 网络 */}
<Card className="border border-slate-200 bg-white">
<Card.Header>
<SectionTitle title="资源占用信息" desc="CPU / 内存 / 网络实时占用" />
</Card.Header>
<Card.Content>
<div className="grid gap-4 md:grid-cols-3">
{/* CPU */}
<div className="rounded-xl border border-slate-200 p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">CPU 使用率{cpu?.logical ? ' ' + cpu.logical + ' 核' : ''}</span>
<span className="text-sm font-semibold text-slate-800">{cpu ? cpu.percent + '%' : '—'}</span>
</div>
<div className="mt-3"><Bar value={cpu?.percent || 0} /></div>
<p className="mt-2 text-xs text-slate-400">负载: {cpu?.load_avg ? cpu.load_avg.join(' / ') : '—'}</p>
{cpu?.per_core?.length ? (
<div className="mt-3 grid grid-cols-4 gap-2">
{cpu.per_core.map((v, i) => (
<div key={i} className="flex flex-col items-center rounded-lg border border-slate-100 bg-slate-50 px-2 py-1.5">
<span className="text-xs text-slate-400">{i}</span>
<div className={'mt-1 h-1.5 w-full rounded-full ' + coreColor(v)} />
<span className="mt-1 text-[10px] text-slate-500">{v.toFixed(0)}%</span>
</div>
))}
</div>
) : null}
</div>
{/* 内存 */}
<div className="rounded-xl border border-slate-200 p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">内存使用</span>
<span className="text-sm font-semibold text-slate-800">{mem ? mem.percent + '%' : '—'}</span>
</div>
<div className="mt-3"><Bar value={mem?.percent || 0} /></div>
<p className="mt-2 text-xs text-slate-400">已用 {mem ? fmtBytes(mem.used) : '—'} · 可用 {mem ? fmtBytes(mem.available) : '—'}</p>
<p className="mt-1 text-xs text-slate-400">共 {mem ? fmtBytes(mem.total) : '—'}</p>
</div>
{/* 网络 */}
<div className="rounded-xl border border-slate-200 p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">网络流量</span>
{net?.interfaces?.length ? <span className="text-xs text-slate-400">{net.interfaces.length} 网卡</span> : null}
</div>
<div className="mt-3 space-y-2 text-xs">
<div className="flex justify-between"><dt className="text-slate-400">↓ 下行速率</dt><dd className="text-slate-700">{net ? fmtRate(net.recv_rate) : '—'}</dd></div>
<div className="flex justify-between"><dt className="text-slate-400">↑ 上行速率</dt><dd className="text-slate-700">{net ? fmtRate(net.sent_rate) : '—'}</dd></div>
<div className="flex justify-between"><dt className="text-slate-400">累计下行</dt><dd className="text-slate-700">{net ? fmtBytes(net.recv) : '—'}</dd></div>
<div className="flex justify-between"><dt className="text-slate-400">累计上行</dt><dd className="text-slate-700">{net ? fmtBytes(net.sent) : '—'}</dd></div>
</div>
</div>
</div>
{/* 本进程 */}
<div className="mt-4 rounded-xl border border-slate-200 p-4">
<div className="mb-2 text-sm font-medium text-slate-700">运行进程(bot)</div>
<dl className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-3 lg:grid-cols-5">
<div className="flex flex-col"><dt className="text-slate-400">PID</dt><dd className="text-slate-700">{proc?.pid ?? '—'}</dd></div>
<div className="flex flex-col"><dt className="text-slate-400">进程名</dt><dd className="text-slate-700">{proc?.name || '—'}</dd></div>
<div className="flex flex-col"><dt className="text-slate-400">CPU</dt><dd className="text-slate-700">{proc?.cpu_percent != null ? proc.cpu_percent + '%' : '—'}</dd></div>
<div className="flex flex-col"><dt className="text-slate-400">内存占用</dt><dd className="text-slate-700">{proc?.memory_percent != null ? proc.memory_percent + '%' : '—'}</dd></div>
<div className="flex flex-col"><dt className="text-slate-400">RSS</dt><dd className="text-slate-700">{proc ? fmtBytes(proc.rss) : '—'}</dd></div>
</dl>
</div>
</Card.Content>
</Card>
{/* 3. BOT 后端信息:NoneBot 版本 / 驱动方式 / 装载插件数 */}
<Card className="border border-slate-200 bg-white">
<Card.Header>
<SectionTitle title="BOT 后端信息" desc="NoneBot 版本 / 驱动方式 / 装载插件数" />
</Card.Header>
<Card.Content>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded-xl border border-slate-200 p-4">
<div className="text-xs text-slate-400">NoneBot 版本</div>
<div className="mt-1 text-lg font-semibold text-slate-800">{backend?.nonebot_version || '—'}</div>
</div>
<div className="rounded-xl border border-slate-200 p-4">
<div className="text-xs text-slate-400">驱动方式</div>
<div className="mt-1 truncate text-sm font-medium text-slate-700" title={backend?.driver}>{backend?.driver || '—'}</div>
<div className="text-xs text-slate-400">{backend?.driver_type || ''}</div>
</div>
<div className="rounded-xl border border-slate-200 p-4">
<div className="text-xs text-slate-400">装载插件数</div>
<div className="mt-1 text-lg font-semibold text-slate-800">{backend?.plugin_count ?? '—'} 个</div>
</div>
<div className="rounded-xl border border-slate-200 p-4">
<div className="text-xs text-slate-400">Python</div>
<div className="mt-1 truncate text-sm font-medium text-slate-700" title={backend?.python}>{backend?.python || '—'}</div>
<div className="text-xs text-slate-400">bot 运行 {sys ? fmtUptime(sys.bot_uptime_seconds) : '—'}</div>
</div>
</div>
</Card.Content>
</Card>
{/* 4. BOT 协议端信息:登录账号昵称 / 账号 / 适配器类型 */}
<Card className="border border-slate-200 bg-white">
<Card.Header>
<SectionTitle title="BOT 协议端信息" desc="当前登录的账户昵称 / 账号 / 适配器类型" />
</Card.Header>
<Card.Content>
{!data ? (
<p className="text-sm text-slate-400">加载中…</p>
) : !data.bots.length ? (
<p className="text-sm text-slate-400">暂无在线 QQ 账号</p>
) : (
<div className="space-y-2">
{data.bots.map(bot => (
<div key={bot.self_id} className="flex items-center gap-3 rounded-xl border border-slate-100 px-4 py-3">
<Avatar size="sm">
<Avatar.Image src={bot.avatar} alt={bot.nick} />
<Avatar.Fallback>{(bot.nick || bot.self_id).charAt(0).toUpperCase()}</Avatar.Fallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-slate-800">{bot.nick}</div>
<div className="truncate text-xs text-slate-400">{bot.self_id} · {bot.adapter}</div>
</div>
<div className="flex items-center gap-3">
{bot.msg_recv != null && <span className="text-xs text-slate-400">收 {bot.msg_recv}</span>}
{bot.msg_sent != null && <span className="text-xs text-slate-400">发 {bot.msg_sent}</span>}
{bot.online != null && <span className="text-xs text-slate-400">{bot.online ? '在线' : '离线'}</span>}
<span className="inline-flex items-center gap-1.5 text-xs">
<span className={'size-2 rounded-full ' + (bot.ws_connected ? 'bg-emerald-500' : 'bg-slate-300')} />
<span className={bot.ws_connected ? 'text-emerald-600' : 'text-slate-400'}>{bot.ws_connected ? '已连接' : '未连接'}</span>
</span>
</div>
</div>
))}
</div>
)}
</Card.Content>
</Card>
</div>
)
}