File size: 3,789 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | import { useEffect } from 'react'
import { tasksApi } from '../api/tasks'
import { notifyDesktop } from '../lib/desktopNotifications'
import type { CronTask, TaskRun } from '../types/task'
const POLL_INTERVAL_MS = 30_000
const NOTIFIED_RUNS_STORAGE_KEY = 'cc-haha.notifiedDesktopTaskRuns.v1'
const MAX_STORED_RUN_IDS = 200
function isTerminalRun(run: TaskRun): boolean {
return run.status === 'completed' || run.status === 'failed' || run.status === 'timeout'
}
function hasDesktopNotification(task: CronTask | undefined): boolean {
return !!task?.notification?.enabled && task.notification.channels.includes('desktop')
}
function readNotifiedRunIds(): Set<string> {
try {
const raw = localStorage.getItem(NOTIFIED_RUNS_STORAGE_KEY)
const parsed = raw ? JSON.parse(raw) : []
return new Set(Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : [])
} catch {
return new Set()
}
}
function writeNotifiedRunIds(runIds: Set<string>): void {
try {
const trimmed = [...runIds].slice(-MAX_STORED_RUN_IDS)
localStorage.setItem(NOTIFIED_RUNS_STORAGE_KEY, JSON.stringify(trimmed))
} catch {
// Notification dedupe is best-effort; storage failures should not break the app.
}
}
function formatTaskRunNotification(run: TaskRun): { title: string; body: string } {
const status = run.status === 'completed'
? '完成'
: run.status === 'failed'
? '失败'
: '超时'
const detail = run.error || run.output || run.prompt
const body = detail
? `${status}: ${detail.slice(0, 160)}`
: `状态: ${status}`
return {
title: `定时任务 ${run.taskName || run.taskId}`,
body,
}
}
export function collectDesktopNotifiableRuns(
tasks: CronTask[],
runs: TaskRun[],
notifiedRunIds: Set<string>,
): TaskRun[] {
const taskById = new Map(tasks.map((task) => [task.id, task]))
return runs
.filter((run) => isTerminalRun(run))
.filter((run) => hasDesktopNotification(taskById.get(run.taskId)))
.filter((run) => !notifiedRunIds.has(run.id))
.sort((a, b) => Date.parse(a.completedAt ?? a.startedAt) - Date.parse(b.completedAt ?? b.startedAt))
}
export function useScheduledTaskDesktopNotifications(): void {
useEffect(() => {
let stopped = false
let initialized = false
const poll = async () => {
try {
const [{ tasks }, { runs }] = await Promise.all([
tasksApi.list(),
tasksApi.getRecentRuns(50),
])
if (stopped) return
const notifiedRunIds = readNotifiedRunIds()
const pendingRuns = collectDesktopNotifiableRuns(tasks, runs, notifiedRunIds)
if (!initialized) {
for (const run of pendingRuns) notifiedRunIds.add(run.id)
writeNotifiedRunIds(notifiedRunIds)
initialized = true
return
}
for (const run of pendingRuns) {
const notification = formatTaskRunNotification(run)
const sent = await notifyDesktop({
dedupeKey: `scheduled-task:${run.id}`,
title: notification.title,
body: notification.body,
target: run.sessionId
? { type: 'session', sessionId: run.sessionId, title: run.taskName || run.taskId }
: { type: 'scheduled' },
})
if (sent) notifiedRunIds.add(run.id)
}
writeNotifiedRunIds(notifiedRunIds)
} catch (err) {
if (typeof console !== 'undefined') {
console.warn('[scheduledTaskNotifications] failed to poll task runs:', err)
}
}
}
void poll()
const interval = window.setInterval(() => {
void poll()
}, POLL_INTERVAL_MS)
return () => {
stopped = true
window.clearInterval(interval)
}
}, [])
}
|