feat: implement full-stack TaskPilot scheduler

This commit is contained in:
2026-10-18 22:05:45 +08:00
parent d88c21753d
commit 8ce02af13a
72 changed files with 17812 additions and 0 deletions
+234
View File
@@ -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,
})
}