(() => { /** * 文件职责:状态页视图,负责服务状态、计时器可视化与手动刷新交互展示。 */ const { Box, Typography, Chip } = MaterialUI; function dedupeStatusTimerCards(cards) { const priorityMap = { // 同一会话若同时命中多类计时器,群沉默卡优先级更高,避免信息重复。 group_silence: 3, auto_trigger: 2, }; const merged = new Map(); cards.forEach((card) => { const key = String(card.session_id || ''); if (!key) return; const existing = merged.get(key); if (!existing) { merged.set(key, card); return; } const currentPriority = priorityMap[card.timer_kind] ?? 0; const existingPriority = priorityMap[existing.timer_kind] ?? 0; if (currentPriority > existingPriority) { merged.set(key, card); return; } if (currentPriority === existingPriority) { // 若类型优先级相同,则保留“更快触发”的那张卡,突出最紧迫状态。 const currentRemaining = Number(card.remaining_seconds ?? Number.MAX_SAFE_INTEGER); const existingRemaining = Number(existing.remaining_seconds ?? Number.MAX_SAFE_INTEGER); if (currentRemaining < existingRemaining) { merged.set(key, card); } } }); return Array.from(merged.values()); } function StatusMetricRow({ label, value, emphasize = false, status = '' }) { return (
{label} {value}
); } function resolveStatusTimerCard(timer, nowMs, displayTimezone) { // 后端 target_time / started_at 以秒级时间戳返回,这里统一转为 Date 便于格式化和比较。 const targetTime = parseDateish(timer.target_time ? Number(timer.target_time) * 1000 : null); const startedAt = parseDateish(timer.started_at ? Number(timer.started_at) * 1000 : null); const remainingSeconds = Number.isFinite(Number(timer.remaining_seconds)) ? Math.max(0, Number(timer.remaining_seconds)) : (targetTime ? Math.max(0, Math.ceil((targetTime.getTime() - nowMs) / 1000)) : 0); const windowSeconds = Math.max(0, Number(timer.window_seconds ?? 0)); // 若后端未给出 progress_percent,则前端基于总窗口时长与剩余秒数推导一个近似值。 const fallbackProgress = windowSeconds > 0 ? Math.max(0, Math.min(100, Math.round(((windowSeconds - remainingSeconds) / windowSeconds) * 100))) : 0; const progressPercent = Math.max(0, Math.min(100, Math.round(Number(timer.progress_percent ?? fallbackProgress) || 0))); // 根据剩余时间给卡片打上状态标签,供颜色、文案和动画统一使用。 let status = 'future'; let statusLabel = '稳定运行'; if (!targetTime) { status = 'unknown'; statusLabel = '待确认'; } else if (remainingSeconds <= 0) { status = 'expired'; statusLabel = '待刷新'; } else if (remainingSeconds <= 300) { status = 'urgent'; statusLabel = '即将结束'; } else if (remainingSeconds <= 1800) { status = 'soon'; statusLabel = '正常计时'; } const isGroupSession = timer.session_category === 'group'; const isGroupSilence = timer.timer_kind === 'group_silence'; const categoryLabel = isGroupSession ? '群会话' : '私聊会话'; const sectionKey = isGroupSilence ? 'group_silence' : 'auto_trigger'; const sectionTitle = isGroupSilence ? '群沉默倒计时' : '自动触发检测'; const accentClass = isGroupSilence ? 'accent-group-silence' : (isGroupSession ? 'accent-auto-group' : 'accent-auto-friend'); const kindBadgeLabel = isGroupSilence ? '沉默重置型' : (isGroupSession ? '群自动触发' : '私聊自动触发'); const countdownText = targetTime ? (remainingSeconds > 0 ? `${formatDuration(remainingSeconds, { compact: true, maxUnits: 3 })} 后到期` : '等待下一轮刷新确认') : '暂无有效目标时间'; const sessionIdText = String(timer.session_id || ''); const sessionDisplayName = String(timer.session_display_name || timer.session_name || sessionIdText || '--'); const hasAlias = Boolean(sessionDisplayName && sessionIdText && sessionDisplayName !== sessionIdText); const sessionSubText = hasAlias ? sessionIdText : ''; return { ...timer, startedAt, targetTime, remainingSeconds, progressPercent, status, statusLabel, categoryLabel, // 这些派生文案与样式字段统一在这里计算,减少渲染层的模板噪声。 sectionKey, sectionTitle, accentClass, kindBadgeLabel, countdownText, sessionDisplayName, sessionSubText, hasAlias, targetText: targetTime ? formatDateTime(targetTime, displayTimezone, { includeYear: true, includeSeconds: true }) : '--', }; } function StatusTimerCard({ timer, displayTimezone, nowMs, resetHint }) { const meta = resolveStatusTimerCard(timer, nowMs, displayTimezone); // 未回复次数大于 0 时用 warning 色,帮助管理员快速定位“机器人已被晾着”的会话。 const chipColor = Number(meta.unanswered_count ?? 0) > 0 ? 'warning' : 'default'; return (
{meta.sectionTitle} {meta.sessionDisplayName} {meta.sessionSubText ? ( {`UMO · ${meta.sessionSubText}`} ) : null}
{meta.kindBadgeLabel}
0 ? 'filled' : 'outlined'} />
{meta.statusLabel}
{meta.categoryLabel}
{resetHint ? ( // 群聊出现新消息导致目标时间明显后移时,短暂显示“已重置”提示,帮助理解状态变化原因。
群聊刚刚有新消息,沉默计时器已重新开始计时
) : null}
倒计时 {meta.countdownText}
{meta.progressPercent}%
目标时间 {meta.targetText}
计时窗口 {meta.window_seconds ? formatDuration(meta.window_seconds, { compact: true, maxUnits: 2 }) : '--'}
); } function StatusView({ onRefresh }) { const { state } = useAppContext(); const status = state.status || {}; // nowMs 每秒更新一次,用于驱动倒计时与相对时间文本实时刷新。 const [nowMs, setNowMs] = React.useState(Date.now()); // resetHintMap 记录哪些群沉默卡片当前需要显示“刚刚被重置”的短暂提示。 const [resetHintMap, setResetHintMap] = React.useState({}); // 手动刷新按钮的视觉反馈状态机:idle -> loading -> success / error。 const [manualRefreshState, setManualRefreshState] = React.useState({ phase: 'idle', finishedAt: 0 }); const previousTimerSnapshotRef = React.useRef({}); const resetHintTimersRef = React.useRef({}); const displayTimezone = state.config?.displayTimezone || 'Asia/Shanghai'; React.useEffect(() => { const timer = setInterval(() => { setNowMs(Date.now()); }, 1000); return () => clearInterval(timer); }, []); React.useEffect(() => { return () => { // 组件卸载时清理所有“重置提示”的超时器,避免状态在卸载后回写。 Object.values(resetHintTimersRef.current).forEach((timerId) => { clearTimeout(timerId); }); }; }, []); // 统一把后端状态字段规整为前端友好的基础数字类型。 const uptimeSeconds = Number(status.uptime_seconds ?? 0); const jobsCount = Number(status.jobs_count ?? 0); const sessionsCount = Number(status.sessions_count ?? 0); const autoTriggerTimers = Number(status.auto_trigger_timers ?? 0); const groupTimers = Number(status.group_timers ?? 0); const schedulerRunning = Boolean(status.scheduler_running); const pluginRunning = Boolean(status.running); const autoTriggerCards = Array.isArray(status.auto_trigger_cards) ? status.auto_trigger_cards : []; const groupTimerCards = Array.isArray(status.group_timer_cards) ? status.group_timer_cards : []; // 合并并去重所有会话计时器,保证一个会话最多出现一张最关键卡片。 const timerCards = dedupeStatusTimerCards([...groupTimerCards, ...autoTriggerCards]); const timerSections = [ { key: 'group_silence', title: '群沉默计时器', description: '群内一旦有新消息就会重新计时,适合观察沉默窗口是否被活跃消息重置。', emptyText: '当前没有正在运行的群沉默计时器。', cards: timerCards.filter((timer) => timer.timer_kind === 'group_silence'), }, { key: 'auto_trigger', title: '自动触发计时器', description: '用于观察会话的自动触发状况,并按群聊 / 私聊显示不同标签。通常情况下,插件运行一段时间后,这里将不会出现新的卡片。', emptyText: '当前没有正在运行的自动触发计时器。', cards: timerCards.filter((timer) => timer.timer_kind === 'auto_trigger'), }, ]; React.useEffect(() => { const nextSnapshot = {}; const nextHints = {}; const activeKeys = new Set(); timerCards.forEach((timer) => { const key = `${timer.timer_kind}-${timer.session_id}`; const currentRemaining = Math.max(0, Number(timer.remaining_seconds ?? 0)); const currentTarget = Number(timer.target_time ?? 0); const currentStatus = String(timer.status || ''); const previous = previousTimerSnapshotRef.current[key] || null; activeKeys.add(key); nextSnapshot[key] = { remainingSeconds: currentRemaining, targetTime: currentTarget, status: currentStatus, }; // 真正的“被重置”应表现为:同一张群沉默卡片的剩余时间突然回升,且当前仍处于有效运行态。 const isGroupReset = timer.timer_kind === 'group_silence' && previous && currentStatus !== 'expired' && currentStatus !== 'unknown' && currentRemaining - Number(previous.remainingSeconds ?? 0) > 3; if (isGroupReset) { nextHints[key] = true; if (resetHintTimersRef.current[key]) { clearTimeout(resetHintTimersRef.current[key]); } resetHintTimersRef.current[key] = setTimeout(() => { setResetHintMap((current) => { const updated = { ...current }; delete updated[key]; return updated; }); delete resetHintTimersRef.current[key]; }, 4200); } }); Object.keys(resetHintTimersRef.current).forEach((key) => { if (!activeKeys.has(key)) { clearTimeout(resetHintTimersRef.current[key]); delete resetHintTimersRef.current[key]; } }); previousTimerSnapshotRef.current = nextSnapshot; setResetHintMap((current) => { const filtered = Object.fromEntries( Object.entries(current).filter(([key]) => activeKeys.has(key)) ); return Object.keys(nextHints).length > 0 ? { ...filtered, ...nextHints } : filtered; }); }, [timerCards]); const handleManualRefresh = async () => { if (manualRefreshState.phase === 'loading') return; setManualRefreshState((current) => ({ ...current, phase: 'loading' })); try { // 手动刷新会回到入口层执行一次完整 loadAll,确保状态、会话、配置、任务全部同步。 await onRefresh(); setManualRefreshState({ phase: 'success', finishedAt: Date.now() }); } catch (e) { setManualRefreshState({ phase: 'error', finishedAt: Date.now() }); } }; const refreshButtonLabel = manualRefreshState.phase === 'loading' ? '正在刷新控制台数据...' : manualRefreshState.phase === 'success' ? '刷新完成' : manualRefreshState.phase === 'error' ? '刷新失败,请重试' : '刷新控制台数据'; const refreshNoteText = manualRefreshState.phase === 'loading' ? '正在重新拉取运行状态、会话列表、配置与任务列表。' : manualRefreshState.phase === 'success' ? `已完成手动刷新 · ${formatFriendlyTime(manualRefreshState.finishedAt, displayTimezone)}` : manualRefreshState.phase === 'error' ? '本次手动刷新失败,请检查服务连接或稍后重试。' : '手动刷新会重新拉取运行状态、会话列表、配置与任务列表。'; return (
服务状态
⏱️
调度概览
🚀
快捷操作
{refreshNoteText}
会话计时器可视化 ({timerCards.length}) 实时展示自动触发检测与群沉默检测的倒计时、进度和会话状态。此处卡片的倒计时结束后会进入任务管理页面
{timerCards.length === 0 ? (
🫧
暂无运行中的会话计时器 当前没有正在计时的自动触发或群沉默会话。等插件进入下一轮会话调度后,这里会自动出现对应卡片。
) : (
{timerSections.map((section) => (
{section.title} {section.description}
{section.cards.length} 张
{section.cards.length === 0 ? (
{section.emptyText}
) : (
{section.cards.map((timer) => ( ))}
)}
))}
)}
); } // 暴露为全局视图组件,供应用入口按当前路由态切换展示。 window.StatusView = StatusView; })();