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)
    }
  }, [])
}