(() => { /** * 文件职责:任务页视图,负责调度任务列表、倒计时进度与任务操作入口展示。 */ const { Box, Typography, Button, Chip } = MaterialUI; function normalizeTimestampValue(value) { // 兼容后端可能返回“秒级”或“毫秒级”时间戳;统一规整为毫秒再交给 Date 处理。 if (value === null || value === undefined || value === '') return null; if (typeof value === 'number') { return value < 1000000000000 ? value * 1000 : value; } return value; } function resolveTaskProgress(job, nowMs) { const nextRun = parseDateish(job.next_run_time); const nextTrigger = parseDateish(normalizeTimestampValue(job.next_trigger_time)); const scheduledAtMs = normalizeTimestampValue(job.last_scheduled_at); const randomIntervalSeconds = Number(job.last_schedule_random_interval_seconds); const minIntervalSeconds = Number(job.last_schedule_min_interval_seconds); const maxIntervalSeconds = Number(job.last_schedule_max_interval_seconds); if (!nextRun) { // 若调度器未给出下一次执行时间,则返回一组可安全渲染的默认元信息。 return { nextRun: null, nextTrigger, remainingSeconds: 0, remainingText: '--', countdownText: '暂无有效执行时间', friendlyText: '--', status: 'unknown', statusLabel: '待确认', progressPercent: 0, }; } // remainingSeconds 驱动任务卡片的倒计时、状态颜色与进度条表现。 const remainingSeconds = Math.max(0, Math.ceil((nextRun.getTime() - nowMs) / 1000)); const friendlyText = formatFriendlyTime(nextRun, 'Asia/Shanghai'); const remainingText = remainingSeconds > 0 ? formatDuration(remainingSeconds, { compact: true, maxUnits: 3 }) : '已到触发时间'; let status = 'future'; let statusLabel = '正常排队'; if (remainingSeconds <= 0) { status = 'expired'; statusLabel = '待刷新'; } else if (remainingSeconds <= 300) { status = 'urgent'; statusLabel = '即将触发'; } else if (remainingSeconds <= 1800) { status = 'soon'; statusLabel = '等待触发'; } // 优先使用更明确的随机调度窗口;若缺失,再尝试用 nextTrigger / max / min 区间推导。 const candidateWindowSeconds = [ randomIntervalSeconds, nextTrigger ? Math.max(0, Math.round((nextTrigger.getTime() - nowMs) / 1000)) : 0, maxIntervalSeconds, minIntervalSeconds, ].filter((value) => Number.isFinite(value) && value > 0); const windowSeconds = candidateWindowSeconds.length > 0 ? candidateWindowSeconds[0] : 0; let progressPercent = remainingSeconds <= 0 ? 100 : 0; if (windowSeconds > 0) { let elapsedSeconds = 0; if (scheduledAtMs) { // 若保存了调度创建时刻,则以真实经历时长计算进度更准确。 elapsedSeconds = Math.max(0, (nowMs - scheduledAtMs) / 1000); } else { // 否则退化为“总窗口 - 剩余时间”的近似值。 elapsedSeconds = Math.max(0, windowSeconds - remainingSeconds); } progressPercent = Math.max(0, Math.min(100, Math.round((elapsedSeconds / windowSeconds) * 100))); } return { nextRun, nextTrigger, remainingSeconds, remainingText, countdownText: remainingSeconds > 0 ? `${remainingText} 后执行` : '等待下一轮刷新确认', friendlyText, status, statusLabel, progressPercent, }; } function formatQuietHoursText(value) { const raw = String(value || '').trim(); if (!raw) return '未配置'; const matched = raw.match(/^(\d{1,2})\s*-\s*(\d{1,2})$/); if (!matched) return raw; const startHour = Number(matched[1]); const endHour = Number(matched[2]); if (!Number.isInteger(startHour) || !Number.isInteger(endHour)) return raw; if (startHour < 0 || startHour > 23 || endHour < 0 || endHour > 23) return raw; return `${String(startHour).padStart(2, '0')}:00 - ${String(endHour).padStart(2, '0')}:00`; } function formatScheduleIntervalText(minMinutes, maxMinutes) { const minValue = Number(minMinutes); const maxValue = Number(maxMinutes); const hasMin = Number.isFinite(minValue) && minValue > 0; const hasMax = Number.isFinite(maxValue) && maxValue > 0; if (!hasMin && !hasMax) return '未配置'; if (hasMin && hasMax) { if (minValue > maxValue) { return `配置异常:${minValue} > ${maxValue} 分钟`; } return `${minValue} - ${maxValue} 分钟`; } if (hasMin) return `${minValue} 分钟`; return `${maxValue} 分钟`; } function TasksView({ onRefresh }) { const { state, dispatch } = useAppContext(); const api = useApi(); // 每秒刷新当前时间,驱动任务卡片上的倒计时与进度条更新。 const [nowMs, setNowMs] = React.useState(Date.now()); const [triggerFeedbackMap, setTriggerFeedbackMap] = React.useState({}); const displayTimezone = state.config?.displayTimezone || 'Asia/Shanghai'; React.useEffect(() => { const timer = setInterval(() => { setNowMs(Date.now()); }, 1000); return () => clearInterval(timer); }, []); const triggerNow = async (session) => { setTriggerFeedbackMap((prev) => ({ ...prev, [session]: { status: 'pending', text: '正在触发,等待 LLM 回复完成…' }, })); try { // 手动触发后会重新走一次父级全量刷新,确保状态页与任务页同步更新。 const result = await api.triggerJob(session); setTriggerFeedbackMap((prev) => ({ ...prev, [session]: { status: 'pending', text: result?.message || '已开始立即触发,正在等待 LLM 回复完成…', }, })); await onRefresh(); } catch (e) { setTriggerFeedbackMap((prev) => ({ ...prev, [session]: { status: 'error', text: e.message || '触发任务失败,请稍后重试' }, })); dispatch({ type: 'SET_ERROR', payload: e.message || '触发任务失败' }); } }; const cancelJob = async (session) => { try { await api.cancelJob(session); await onRefresh(); } catch (e) { dispatch({ type: 'SET_ERROR', payload: e.message || '取消任务失败' }); } }; const jobs = state.jobs || []; React.useEffect(() => { setTriggerFeedbackMap((prev) => { let changed = false; const next = { ...prev }; jobs.forEach((job) => { if (job.manual_trigger_in_progress) { const current = next[job.id]; const expectedText = '正在触发,等待 LLM 回复完成…'; if (!current || current.status !== 'pending' || current.text !== expectedText) { next[job.id] = { status: 'pending', text: expectedText }; changed = true; } return; } if (next[job.id]?.status === 'pending') { next[job.id] = { status: 'success', text: '本次立即触发已完成,按钮已恢复可用' }; changed = true; } }); return changed ? next : prev; }); }, [jobs]); React.useEffect(() => { const successEntries = Object.entries(triggerFeedbackMap) .filter(([, value]) => value?.status === 'success'); if (successEntries.length === 0) { return undefined; } const timers = successEntries.map(([sessionId]) => setTimeout(() => { setTriggerFeedbackMap((prev) => { const current = prev[sessionId]; if (!current || current.status !== 'success') { return prev; } const next = { ...prev }; delete next[sessionId]; return next; }); }, 3000)); return () => { timers.forEach((timer) => clearTimeout(timer)); }; }, [triggerFeedbackMap]); return (
当前调度任务 ({jobs.length})
{jobs.length === 0 ? (
🪄
暂无调度任务 当前没有待执行的主动消息任务。当会话满足调度条件后,这里会自动展示任务卡片与倒计时信息。
) : (
{jobs.map((job) => { // 每张任务卡在渲染前先推导出倒计时、状态与进度等派生信息。 const task = resolveTaskProgress(job, nowMs); const chipColor = job.unanswered_count > 0 ? 'warning' : 'default'; const sessionIdText = String(job.id || ''); const sessionDisplayName = String(job.session_display_name || job.session_name || sessionIdText || '--'); const hasAlias = Boolean(sessionDisplayName && sessionIdText && sessionDisplayName !== sessionIdText); const sessionSubText = hasAlias ? sessionIdText : ''; const isTriggerRunning = Boolean(job.manual_trigger_in_progress); const triggerFeedback = triggerFeedbackMap[job.id]; const triggerButtonLabel = isTriggerRunning ? '触发中…' : '立即触发'; const triggerHelperText = isTriggerRunning ? (triggerFeedback?.text || '正在触发,等待 LLM 回复完成…') : triggerFeedback?.text; const scheduleIntervalText = formatScheduleIntervalText( job.schedule_min_interval_minutes, job.schedule_max_interval_minutes, ); const quietHoursText = formatQuietHoursText(job.quiet_hours); return (
会话 {sessionDisplayName} {sessionSubText ? ( {`UMO · ${sessionSubText}`} ) : null}
0 ? 'filled' : 'outlined'} />
下一次执行时间 {task.nextRun ? formatDateTime(task.nextRun, displayTimezone, { includeYear: true, includeSeconds: true }) : '--'}
{task.statusLabel}
{task.countdownText}
调度间隔 {scheduleIntervalText} 免打扰时段 {quietHoursText} {triggerHelperText ? ( {triggerHelperText} ) : null}
); })}
)}
); } // 暴露为全局视图组件,供应用入口按 currentView 切换。 window.TasksView = TasksView; })();