/** * 内核日志的常驻收集。 * * 住在模块级单例里而不是请求页组件里,是因为日志要跨页面留存——收集跟着组件 * 走的话,切走一次列表就清空一次。外壳启动时点火,之后跟着 kernel.online 起落。 * * 节奏和缓冲:收到的行先进 pending,每 200ms 一批刷进 rows——日志密集时逐条 * 触发响应式更新会把界面拖死。暂停就是不刷,缓冲照收(也设上限),恢复时补上。 */ import { ref, watch } from 'vue' import { kernel } from './runtime' import { LOG_CAP, pushCapped, streamLogs } from './logs' import type { LogLevel, LogLine } from './logs' export interface LogRow extends LogLine { id: number /** 收到那一刻的时间。日志行本身不带时间戳 */ time: string } /** 日志级别。改它会断开重连:内核的级别是建立连接时定的,中途改不了。 */ export const level = ref('info') export const paused = ref(false) export const rows = ref([]) export const failure = ref('') /** 内核那边把流断了(比如重启)。和主动中止区分开,这个要告诉用户 */ export const ended = ref(false) /** 每刷进一批加一。rows 是原地增改,组件要跟随滚动只能看这个信号 */ export const revision = ref(0) let pending: LogRow[] = [] let nextId = 0 let aborter: AbortController | null = null let retryTimer = 0 let started = false const clock = (d: Date): string => [d.getHours(), d.getMinutes(), d.getSeconds()] .map((n) => String(n).padStart(2, '0')) .join(':') function flush() { if (paused.value) { // 暂停时继续接收但不渲染。缓冲也设上限,暂停再久内存也不涨 if (pending.length > LOG_CAP) pending.splice(0, pending.length - LOG_CAP) return } if (pending.length === 0) return pushCapped(rows.value, pending, LOG_CAP) pending = [] revision.value += 1 } /** 只清界面上攒下的,内核那边的日志不受影响。 */ export function clearLogs() { rows.value = [] pending = [] revision.value += 1 } /** * 断流不等于内核没了:配置重载会掐断流,而内核几秒内就回来、online 从未翻转。 * 断了 5 秒后自动重连,失败会再次走到这里形成重试节奏;真离线时交给下面等 * online 翻转的 watch。 */ function scheduleRetry() { window.clearTimeout(retryTimer) retryTimer = window.setTimeout(() => { if (kernel.value.online) void pump() }, 5000) } async function pump() { // 切换级别或内核恢复在线都从这里重连。旧流先停,不然两条流一起往缓冲里倒 aborter?.abort() window.clearTimeout(retryTimer) const controller = new AbortController() aborter = controller failure.value = '' ended.value = false try { for await (const line of streamLogs(level.value, controller.signal)) { pending.push({ ...line, id: nextId++, time: clock(new Date()) }) } if (!controller.signal.aborted) { ended.value = true scheduleRetry() } } catch (error) { if (controller.signal.aborted) return failure.value = error instanceof Error ? error.message : String(error) scheduleRetry() } } /** 外壳启动时调一次。之后跟着内核在线状态和级别自己重连,应用关掉前不停。 */ export function startLogStream(): void { if (started) return started = true // 刷新节拍常驻,缓冲空转一次是零开销;应用关掉前不需要清 window.setInterval(flush, 200) if (kernel.value.online) void pump() // 挂载时内核不在线就不连;等它上线再开始。反方向(流到一半内核掉了) // 由流自己报错或结束来呈现 watch( () => kernel.value.online, (online) => { if (online) void pump() }, ) watch(level, () => { if (kernel.value.online) void pump() }) }