235 lines
6.2 KiB
TypeScript
235 lines
6.2 KiB
TypeScript
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,
|
||
})
|
||
}
|