feat: implement full-stack TaskPilot scheduler
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import type { components } from './generated'
|
||||
|
||||
export type HealthResponse = components['schemas']['HealthResponse']
|
||||
export type Task = components['schemas']['TaskResponse']
|
||||
export type TaskAction = components['schemas']['TaskAction']
|
||||
export type TaskActionType = components['schemas']['TaskActionType']
|
||||
export type CreateTaskRequest = components['schemas']['CreateTaskRequest']
|
||||
export type UpdateTaskRequest = components['schemas']['UpdateTaskRequest']
|
||||
export type TaskListResponse = components['schemas']['TaskListResponse']
|
||||
export type TaskRun = components['schemas']['TaskRunResponse']
|
||||
export type TaskRunListResponse = components['schemas']['TaskRunListResponse']
|
||||
export type RunStatus = components['schemas']['RunStatus']
|
||||
export type RunTrigger = components['schemas']['RunTrigger']
|
||||
export type LogChunkResponse = components['schemas']['LogChunkResponse']
|
||||
export type ErrorResponse = components['schemas']['ErrorResponse']
|
||||
export type SessionResponse = components['schemas']['SessionResponse']
|
||||
export type CronPreviewRequest = components['schemas']['CronPreviewRequest']
|
||||
export type CronPreviewResponse = components['schemas']['CronPreviewResponse']
|
||||
|
||||
export interface TaskFilters {
|
||||
q?: string
|
||||
enabled?: boolean
|
||||
action_type?: TaskActionType
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface RunFilters {
|
||||
page?: number
|
||||
page_size?: number
|
||||
status?: RunStatus
|
||||
trigger?: RunTrigger
|
||||
}
|
||||
|
||||
export type ExecutionEvent =
|
||||
| {
|
||||
event: 'run'
|
||||
data: {
|
||||
run: TaskRun
|
||||
}
|
||||
}
|
||||
| {
|
||||
event: 'log'
|
||||
data: {
|
||||
run_id: number
|
||||
offset: number
|
||||
next_offset: number
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
readonly status: number
|
||||
readonly code?: string
|
||||
readonly field?: string | null
|
||||
readonly details?: Record<string, string>
|
||||
|
||||
constructor(status: number, body?: ErrorResponse) {
|
||||
super(body?.message ?? `请求失败(HTTP ${status})`)
|
||||
this.name = 'ApiRequestError'
|
||||
this.status = status
|
||||
this.code = body?.code
|
||||
this.field = body?.field
|
||||
this.details = body?.details
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const headers = new Headers(init.headers)
|
||||
headers.set('Accept', 'application/json')
|
||||
const method = (init.method ?? 'GET').toUpperCase()
|
||||
if (init.body) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
headers.set('X-TaskPilot-Token', await fetchSessionToken())
|
||||
}
|
||||
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
let body: ErrorResponse | undefined
|
||||
try {
|
||||
body = (await response.json()) as ErrorResponse
|
||||
} catch {
|
||||
body = undefined
|
||||
}
|
||||
throw new ApiRequestError(response.status, body)
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
async function fetchSessionToken(): Promise<string> {
|
||||
const response = await fetch('/api/v1/session', {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new ApiRequestError(response.status)
|
||||
}
|
||||
const session = (await response.json()) as SessionResponse
|
||||
return session.token
|
||||
}
|
||||
|
||||
function withQuery(path: string, values: object): string {
|
||||
const query = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
query.set(key, String(value))
|
||||
}
|
||||
}
|
||||
const serialized = query.toString()
|
||||
return serialized ? `${path}?${serialized}` : path
|
||||
}
|
||||
|
||||
export function fetchHealth(signal?: AbortSignal): Promise<HealthResponse> {
|
||||
return requestJson<HealthResponse>('/api/v1/health', { signal })
|
||||
}
|
||||
|
||||
export function listTasks(
|
||||
filters: TaskFilters = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<TaskListResponse> {
|
||||
return requestJson<TaskListResponse>(
|
||||
withQuery('/api/v1/tasks', filters),
|
||||
{ signal },
|
||||
)
|
||||
}
|
||||
|
||||
export function getTask(id: string, signal?: AbortSignal): Promise<Task> {
|
||||
return requestJson<Task>(`/api/v1/tasks/${encodeURIComponent(id)}`, {
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
export function createTask(payload: CreateTaskRequest): Promise<Task> {
|
||||
return requestJson<Task>('/api/v1/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTask(
|
||||
id: string,
|
||||
payload: UpdateTaskRequest,
|
||||
): Promise<Task> {
|
||||
return requestJson<Task>(`/api/v1/tasks/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteTask(id: string): Promise<void> {
|
||||
return requestJson<void>(`/api/v1/tasks/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export function setTaskEnabled(
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
): Promise<Task> {
|
||||
const action = enabled ? 'enable' : 'disable'
|
||||
return requestJson<Task>(
|
||||
`/api/v1/tasks/${encodeURIComponent(id)}/${action}`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
export function runTask(id: string): Promise<TaskRun> {
|
||||
return requestJson<TaskRun>(
|
||||
`/api/v1/tasks/${encodeURIComponent(id)}/run`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
export function listTaskRuns(
|
||||
taskId: string,
|
||||
filters: RunFilters = {},
|
||||
signal?: AbortSignal,
|
||||
): Promise<TaskRunListResponse> {
|
||||
return requestJson<TaskRunListResponse>(
|
||||
withQuery(`/api/v1/tasks/${encodeURIComponent(taskId)}/runs`, filters),
|
||||
{ signal },
|
||||
)
|
||||
}
|
||||
|
||||
export function getRun(
|
||||
runId: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TaskRun> {
|
||||
return requestJson<TaskRun>(`/api/v1/runs/${runId}`, { signal })
|
||||
}
|
||||
|
||||
export function getRunLogs(
|
||||
runId: number,
|
||||
offset = 0,
|
||||
signal?: AbortSignal,
|
||||
): Promise<LogChunkResponse> {
|
||||
return requestJson<LogChunkResponse>(
|
||||
withQuery(`/api/v1/runs/${runId}/logs`, { offset }),
|
||||
{ signal },
|
||||
)
|
||||
}
|
||||
|
||||
export function cancelRun(runId: number): Promise<TaskRun> {
|
||||
return requestJson<TaskRun>(`/api/v1/runs/${runId}/cancel`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function runLogDownloadUrl(runId: number): string {
|
||||
return `/api/v1/runs/${runId}/logs/download`
|
||||
}
|
||||
|
||||
export function previewCron(
|
||||
request: CronPreviewRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CronPreviewResponse> {
|
||||
return requestJson<CronPreviewResponse>('/api/v1/cron/preview', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
signal,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user