File size: 5,421 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
/**
 * Scheduled Tasks REST API
 *
 * GET    /api/scheduled-tasks           โ€” ่Žทๅ–ไปปๅŠกๅˆ—่กจ
 * POST   /api/scheduled-tasks           โ€” ๅˆ›ๅปบไปปๅŠก
 * GET    /api/scheduled-tasks/runs      โ€” ่Žทๅ–ๆ‰€ๆœ‰ไปปๅŠก็š„ๆœ€่ฟ‘ๆ‰ง่กŒ่ฎฐๅฝ•
 * GET    /api/scheduled-tasks/:id/runs  โ€” ่Žทๅ–ๆŒ‡ๅฎšไปปๅŠก็š„ๆ‰ง่กŒ่ฎฐๅฝ•
 * POST   /api/scheduled-tasks/:id/run   โ€” ็ซ‹ๅณๆ‰ง่กŒๆŒ‡ๅฎšไปปๅŠก
 * PUT    /api/scheduled-tasks/:id       โ€” ๆ›ดๆ–ฐไปปๅŠก
 * DELETE /api/scheduled-tasks/:id       โ€” ๅˆ ้™คไปปๅŠก
 */

import { CronService, type CronTask } from '../services/cronService.js'
import { cronScheduler } from '../services/cronScheduler.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'

const cronService = new CronService()

export async function handleScheduledTasksApi(
  req: Request,
  _url: URL,
  segments: string[],
): Promise<Response> {
  try {
    const method = req.method
    const taskId = segments[2] // /api/scheduled-tasks/:id  or "runs"
    const subResource = segments[3] // /api/scheduled-tasks/:id/runs

    // โ”€โ”€ GET /api/scheduled-tasks/runs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'GET' && taskId === 'runs') {
      const url = new URL(req.url)
      const limit = parseInt(url.searchParams.get('limit') || '50', 10)
      const runs = await cronScheduler.getRecentRuns(limit)
      return Response.json({ runs })
    }

    // โ”€โ”€ GET /api/scheduled-tasks/:id/runs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'GET' && taskId && subResource === 'runs') {
      const runs = await cronScheduler.getTaskRuns(taskId)
      return Response.json({ runs })
    }

    // โ”€โ”€ GET /api/scheduled-tasks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'GET' && !taskId) {
      const tasks = await cronService.listTasks()
      return Response.json({ tasks })
    }

    // โ”€โ”€ POST /api/scheduled-tasks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'POST' && !taskId) {
      const body = await parseJsonBody(req)
      const task = await cronService.createTask({
        name: body.name as string | undefined,
        description: body.description as string | undefined,
        cron: body.cron as string,
        prompt: body.prompt as string,
        enabled: body.enabled !== undefined ? (body.enabled as boolean) : undefined,
        recurring: body.recurring as boolean | undefined,
        permanent: body.permanent as boolean | undefined,
        permissionMode: body.permissionMode as string | undefined,
        model: body.model as string | undefined,
        providerId: body.providerId as string | null | undefined,
        folderPath: body.folderPath as string | undefined,
        useWorktree: body.useWorktree as boolean | undefined,
        notification: body.notification as CronTask['notification'],
      })
      return Response.json({ task }, { status: 201 })
    }

    // โ”€โ”€ POST /api/scheduled-tasks/:id/run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Fire-and-forget: start execution in background, return immediately.
    // The frontend polls GET /:id/runs to track progress.
    if (method === 'POST' && taskId && subResource === 'run') {
      const tasks = await cronService.listTasks()
      const task = tasks.find((t) => t.id === taskId)
      if (!task) throw ApiError.notFound(`Task ${taskId} not found`)
      cronScheduler.executeTask(task, { createSession: true }).catch((err) => {
        console.error(`[ScheduledTasks] Manual run failed for task ${taskId}:`, err)
      })
      // Small delay to let appendRun() write the "running" entry to disk
      await new Promise((r) => setTimeout(r, 200))
      return Response.json({ ok: true })
    }

    // โ”€โ”€ PUT /api/scheduled-tasks/:id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'PUT' && taskId && !subResource) {
      const body = await parseJsonBody(req)
      const task = await cronService.updateTask(taskId, body)
      return Response.json({ task })
    }

    // โ”€โ”€ DELETE /api/scheduled-tasks/:id โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    if (method === 'DELETE' && taskId && !subResource) {
      await cronService.deleteTask(taskId)
      return Response.json({ ok: true })
    }

    throw new ApiError(
      405,
      `Method ${method} not allowed on /api/scheduled-tasks${taskId ? `/${taskId}` : ''}${subResource ? `/${subResource}` : ''}`,
      'METHOD_NOT_ALLOWED',
    )
  } catch (error) {
    return errorResponse(error)
  }
}

// โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
  try {
    return (await req.json()) as Record<string, unknown>
  } catch {
    throw ApiError.badRequest('Invalid JSON body')
  }
}