独立监控核心和模块
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
|
||||
const logger = createLogger('monitor')
|
||||
|
||||
// ===== 与 Rust 端 / C# Contracts.cs 对应的数据结构(camelCase) =====
|
||||
// schemaVersion=1 契约由 Kernel 维护,前端按 schemaVersion 解析。
|
||||
|
||||
export interface SensorEntry {
|
||||
id: string
|
||||
name: string
|
||||
/** 传感器类型(LHB SensorType 小写):temperature/load/power/voltage/fan/clock/data/smalldata/throughput/level/control 等 */
|
||||
type: string
|
||||
hardwareName: string
|
||||
/** null 表示首轮未就绪或硬件不可读 */
|
||||
value: number | null
|
||||
unit: string
|
||||
}
|
||||
|
||||
export interface SensorGroup {
|
||||
id: string
|
||||
name: string
|
||||
sensors: SensorEntry[]
|
||||
}
|
||||
|
||||
export interface SensorSnapshot {
|
||||
schemaVersion: number
|
||||
timestamp: number
|
||||
/** 仅首个快照有意义,后续为 0 */
|
||||
coldStartMs?: number
|
||||
isAdmin: boolean
|
||||
ready: boolean
|
||||
groups: SensorGroup[]
|
||||
}
|
||||
|
||||
export interface MonitorStatus {
|
||||
running: boolean
|
||||
pid: number | null
|
||||
ready: boolean
|
||||
sensorCount: number
|
||||
restartCount: number
|
||||
}
|
||||
|
||||
export interface MonitorKernelInfo {
|
||||
path: string
|
||||
exists: boolean
|
||||
port: number
|
||||
}
|
||||
|
||||
/** 连接状态机:与后端事件一一对应 */
|
||||
export type ConnectionState = 'idle' | 'loading' | 'connected' | 'disconnected' | 'error'
|
||||
|
||||
/** 5 秒未收到 monitor-data 事件视为掉线(与后端心跳节奏一致) */
|
||||
const STALE_TIMEOUT_MS = 5000
|
||||
|
||||
export const useMonitorStore = defineStore('monitor', () => {
|
||||
// ===== state =====
|
||||
const status = ref<MonitorStatus | null>(null)
|
||||
const snapshot = ref<SensorSnapshot | null>(null)
|
||||
const kernelInfo = ref<MonitorKernelInfo | null>(null)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
/** 累计收到的 monitor-data 事件数,用于诊断与"已连接"判定 */
|
||||
const eventCount = ref(0)
|
||||
/** 最近一次收到 monitor-data 的时间戳(ms) */
|
||||
const lastEventTime = ref(0)
|
||||
/** 是否正在启动 / 停止 Kernel(防止重复点击) */
|
||||
const starting = ref(false)
|
||||
const stopping = ref(false)
|
||||
|
||||
/** 是否已完成首次加载(避免初始 null/false 导致 UI 闪烁误导状态) */
|
||||
const initialized = ref(false)
|
||||
|
||||
let unlistenFns: UnlistenFn[] = []
|
||||
|
||||
// ===== getters =====
|
||||
|
||||
/** 当前连接状态(基于 status + 最近事件时间推断) */
|
||||
const connState = computed<ConnectionState>(() => {
|
||||
if (errorMsg.value) return 'error'
|
||||
if (!status.value) return 'idle'
|
||||
if (!status.value.running) return 'idle'
|
||||
if (!status.value.ready) return 'loading'
|
||||
if (eventCount.value === 0) return 'loading'
|
||||
if (Date.now() - lastEventTime.value > STALE_TIMEOUT_MS) return 'disconnected'
|
||||
return 'connected'
|
||||
})
|
||||
|
||||
/** 是否处于"已就绪 + 收到数据"的健康状态 */
|
||||
const isLive = computed(() => connState.value === 'connected')
|
||||
|
||||
/** 按分组 id 查找快照 */
|
||||
const groupById = computed(() => {
|
||||
const map: Record<string, SensorGroup> = {}
|
||||
for (const g of snapshot.value?.groups ?? []) map[g.id] = g
|
||||
return map
|
||||
})
|
||||
|
||||
/**
|
||||
* 在指定分组下查找首个匹配的传感器值。
|
||||
* @param groupId 分组 id(cpu/memory/gpuintel/storage 等)
|
||||
* @param matcher 传感器名匹配(精确或子串)
|
||||
*/
|
||||
function findSensorValue(groupId: string, matcher: { name?: string; hardwareName?: string; type?: string }): number | null {
|
||||
const g = groupById.value[groupId]
|
||||
if (!g) return null
|
||||
const s = g.sensors.find(s =>
|
||||
(!matcher.name || s.name === matcher.name || s.name.includes(matcher.name)) &&
|
||||
(!matcher.hardwareName || s.hardwareName === matcher.hardwareName || s.hardwareName.includes(matcher.hardwareName)) &&
|
||||
(!matcher.type || s.type === matcher.type)
|
||||
)
|
||||
return s?.value ?? null
|
||||
}
|
||||
|
||||
// ===== actions =====
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
status.value = await invoke<MonitorStatus>('monitor_status')
|
||||
errorMsg.value = null
|
||||
} catch (e) {
|
||||
logger.error('获取状态失败: ' + e)
|
||||
} finally {
|
||||
initialized.value = true
|
||||
}
|
||||
return status.value
|
||||
}
|
||||
|
||||
async function refreshKernelInfo() {
|
||||
try {
|
||||
kernelInfo.value = await invoke<MonitorKernelInfo>('monitor_kernel_info')
|
||||
} catch (e) {
|
||||
logger.error('获取 Kernel 信息失败: ' + e)
|
||||
}
|
||||
return kernelInfo.value
|
||||
}
|
||||
|
||||
async function start() {
|
||||
if (starting.value) return
|
||||
starting.value = true
|
||||
errorMsg.value = null
|
||||
try {
|
||||
await invoke('monitor_start')
|
||||
await refreshStatus()
|
||||
} catch (e) {
|
||||
errorMsg.value = String(e)
|
||||
logger.error('启动失败: ' + e)
|
||||
} finally {
|
||||
starting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
if (stopping.value) return
|
||||
stopping.value = true
|
||||
try {
|
||||
await invoke('monitor_stop')
|
||||
snapshot.value = null
|
||||
eventCount.value = 0
|
||||
lastEventTime.value = 0
|
||||
await refreshStatus()
|
||||
} catch (e) {
|
||||
errorMsg.value = String(e)
|
||||
logger.error('停止失败: ' + e)
|
||||
} finally {
|
||||
stopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 主动拉取一次性快照(切回 tab 时立即填充,不等下一个 SSE tick) */
|
||||
async function fetchSnapshot() {
|
||||
try {
|
||||
snapshot.value = await invoke<SensorSnapshot>('monitor_get_snapshot')
|
||||
} catch (e) {
|
||||
logger.error('拉取快照失败: ' + e)
|
||||
}
|
||||
return snapshot.value
|
||||
}
|
||||
|
||||
/** 订阅 Tauri 事件:monitor-data / monitor-ready / monitor-loading / monitor-disconnected / monitor-error */
|
||||
async function subscribe() {
|
||||
if (unlistenFns.length) return
|
||||
unlistenFns.push(await listen<SensorSnapshot>('monitor-data', (e) => {
|
||||
// schemaVersion 守卫:仅接受 v1,未来版本需在此处显式升级
|
||||
if (e.payload?.schemaVersion !== 1) {
|
||||
logger.warn('收到未知 schemaVersion: ' + e.payload?.schemaVersion)
|
||||
return
|
||||
}
|
||||
snapshot.value = e.payload
|
||||
eventCount.value++
|
||||
lastEventTime.value = Date.now()
|
||||
}))
|
||||
unlistenFns.push(await listen('monitor-ready', () => {
|
||||
refreshStatus()
|
||||
}))
|
||||
unlistenFns.push(await listen('monitor-loading', () => {
|
||||
// 状态由 status 轮询反映
|
||||
}))
|
||||
unlistenFns.push(await listen('monitor-disconnected', () => {
|
||||
logger.warn('SSE 断开,等待自动重连')
|
||||
refreshStatus()
|
||||
}))
|
||||
unlistenFns.push(await listen<{ message?: string }>('monitor-error', (e) => {
|
||||
errorMsg.value = e.payload?.message ?? 'Kernel 错误'
|
||||
logger.error('Kernel 错误: ' + JSON.stringify(e.payload))
|
||||
}))
|
||||
}
|
||||
|
||||
function unsubscribe() {
|
||||
unlistenFns.forEach(fn => fn())
|
||||
unlistenFns = []
|
||||
}
|
||||
|
||||
/** 模块挂载时调用:刷新状态 + 订阅事件 + 拉取一次快照 */
|
||||
async function init() {
|
||||
await Promise.all([refreshStatus(), refreshKernelInfo()])
|
||||
await subscribe()
|
||||
// 若 Kernel 已就绪,立即拉一次快照避免 UI 空白
|
||||
if (status.value?.ready) {
|
||||
await fetchSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
/** 模块卸载时调用:仅取消事件订阅,不停止 Kernel(Kernel 由 ProcessManager 全局管理) */
|
||||
function dispose() {
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
return {
|
||||
// state
|
||||
status,
|
||||
snapshot,
|
||||
kernelInfo,
|
||||
errorMsg,
|
||||
eventCount,
|
||||
lastEventTime,
|
||||
starting,
|
||||
stopping,
|
||||
initialized,
|
||||
// getters
|
||||
connState,
|
||||
isLive,
|
||||
groupById,
|
||||
// actions
|
||||
findSensorValue,
|
||||
refreshStatus,
|
||||
refreshKernelInfo,
|
||||
start,
|
||||
stop,
|
||||
fetchSnapshot,
|
||||
init,
|
||||
dispose,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user