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 type VenvProbeRequest = components['schemas']['VenvProbeRequest'] export type VenvProbeResponse = components['schemas']['VenvProbeResponse'] export type CreatePythonVenvRequest = components['schemas']['CreatePythonVenvRequest'] export type VenvProbeStatus = components['schemas']['VenvProbeStatus'] export type VenvSource = components['schemas']['VenvSource'] export type InterpreterKind = components['schemas']['InterpreterKind'] export type DirectoryEntry = components['schemas']['DirectoryEntry'] export type DirectoryListingResponse = components['schemas']['DirectoryListingResponse'] export type TaskStats = components['schemas']['TaskStatsResponse'] export type ScheduledTask = components['schemas']['ScheduledTaskResponse'] export type PythonMirror = components['schemas']['PythonMirrorResponse'] export type CreatePythonMirrorRequest = components['schemas']['CreatePythonMirrorRequest'] export type UpdatePythonMirrorRequest = components['schemas']['UpdatePythonMirrorRequest'] 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 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( path: string, init: RequestInit = {}, ): Promise { const method = (init.method ?? 'GET').toUpperCase() const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(method) const send = async (): Promise => { const headers = new Headers(init.headers) headers.set('Accept', 'application/json') if (init.body) { headers.set('Content-Type', 'application/json') } if (mutating) { headers.set('X-TaskPilot-Token', await fetchSessionToken()) } return fetch(path, { ...init, headers }) } let response = await send() if (mutating && response.status === 403) { // 后端重启会轮换管理令牌:丢弃缓存后重试一次。 invalidateSessionToken() response = await send() } 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 } let cachedSessionToken: string | null = null let pendingSessionToken: Promise | null = null export function invalidateSessionToken(): void { cachedSessionToken = null } async function fetchSessionToken(): Promise { if (cachedSessionToken) { return cachedSessionToken } if (!pendingSessionToken) { pendingSessionToken = fetch('/api/v1/session', { headers: { Accept: 'application/json' }, }) .then(async (response) => { if (!response.ok) { throw new ApiRequestError(response.status) } const session = (await response.json()) as SessionResponse cachedSessionToken = session.token return session.token }) .finally(() => { pendingSessionToken = null }) } return pendingSessionToken } 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 { return requestJson('/api/v1/health', { signal }) } export function listTasks( filters: TaskFilters = {}, signal?: AbortSignal, ): Promise { return requestJson( withQuery('/api/v1/tasks', filters), { signal }, ) } export function getTask(id: string, signal?: AbortSignal): Promise { return requestJson(`/api/v1/tasks/${encodeURIComponent(id)}`, { signal, }) } export function createTask(payload: CreateTaskRequest): Promise { return requestJson('/api/v1/tasks', { method: 'POST', body: JSON.stringify(payload), }) } export function updateTask( id: string, payload: UpdateTaskRequest, ): Promise { return requestJson(`/api/v1/tasks/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(payload), }) } export function deleteTask(id: string): Promise { return requestJson(`/api/v1/tasks/${encodeURIComponent(id)}`, { method: 'DELETE', }) } export function setTaskEnabled( id: string, enabled: boolean, ): Promise { const action = enabled ? 'enable' : 'disable' return requestJson( `/api/v1/tasks/${encodeURIComponent(id)}/${action}`, { method: 'POST' }, ) } export function runTask(id: string): Promise { return requestJson( `/api/v1/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }, ) } export function listTaskRuns( taskId: string, filters: RunFilters = {}, signal?: AbortSignal, ): Promise { return requestJson( withQuery(`/api/v1/tasks/${encodeURIComponent(taskId)}/runs`, filters), { signal }, ) } export function getRun( runId: number, signal?: AbortSignal, ): Promise { return requestJson(`/api/v1/runs/${runId}`, { signal }) } export function getRunLogs( runId: number, offset = 0, signal?: AbortSignal, ): Promise { return requestJson( withQuery(`/api/v1/runs/${runId}/logs`, { offset }), { signal }, ) } export function cancelRun(runId: number): Promise { return requestJson(`/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 { return requestJson('/api/v1/cron/preview', { method: 'POST', body: JSON.stringify(request), signal, }) } export function probePythonVenv( request: VenvProbeRequest, signal?: AbortSignal, ): Promise { return requestJson('/api/v1/python/venv', { method: 'POST', body: JSON.stringify(request), signal, }) } /** 用 uv 为项目创建/同步 .venv;优先复用已有环境。 */ export function createPythonVenv( request: CreatePythonVenvRequest, ): Promise { return requestJson('/api/v1/python/venv/create', { method: 'POST', body: JSON.stringify(request), }) } /** * 列出目录下的子文件夹;不传 path 时返回盘符列表。 * 用于页面内的目录浏览器,选中后把绝对路径回填到表单。 */ export function listDirectories( path?: string, signal?: AbortSignal, ): Promise { return requestJson( withQuery('/api/v1/system/directories', { path }), { signal }, ) } /** 仪表盘计数:一次请求拿到全部指标。 */ export function fetchStats(signal?: AbortSignal): Promise { return requestJson('/api/v1/stats', { signal }) } /** 已启用任务的下次触发时间。 */ export function fetchSchedule(signal?: AbortSignal): Promise { return requestJson('/api/v1/schedule', { signal }) } /** Python 包镜像(默认镜像会在脚本任务运行时注入 pip / uv 环境变量)。 */ export function listPythonMirrors( signal?: AbortSignal, ): Promise { return requestJson('/api/v1/python/mirrors', { signal }) } export function createPythonMirror( request: CreatePythonMirrorRequest, ): Promise { return requestJson('/api/v1/python/mirrors', { method: 'POST', body: JSON.stringify(request), }) } export function updatePythonMirror( id: string, request: UpdatePythonMirrorRequest, ): Promise { return requestJson(`/api/v1/python/mirrors/${id}`, { method: 'PUT', body: JSON.stringify(request), }) } export function deletePythonMirror(id: string): Promise { return requestJson(`/api/v1/python/mirrors/${id}`, { method: 'DELETE', }) } export function setDefaultPythonMirror(id: string): Promise { return requestJson( `/api/v1/python/mirrors/${id}/default`, { method: 'POST' }, ) }