feat: implement full-stack TaskPilot scheduler
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::State,
|
||||
http::{HeaderValue, Method, header},
|
||||
middleware,
|
||||
response::IntoResponse,
|
||||
routing::any,
|
||||
routing::get,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use crate::{
|
||||
cron::{self, CronPreviewRequest, CronPreviewResponse},
|
||||
error::{ApiError, ApiJson, ApiResult, ErrorResponse},
|
||||
runs::{
|
||||
model::{LogChunkResponse, TaskRunListResponse, TaskRunResponse},
|
||||
routes::{
|
||||
__path_cancel_run, __path_download_run_logs, __path_execution_events, __path_get_run,
|
||||
__path_get_run_logs, __path_list_task_runs, __path_run_task,
|
||||
},
|
||||
},
|
||||
security,
|
||||
state::AppState,
|
||||
tasks::{
|
||||
model::{
|
||||
ConcurrencyPolicy, CreateTaskRequest, HttpMethod, RetryPolicy, TaskAction,
|
||||
TaskActionType, TaskListResponse, TaskResponse, UpdateTaskRequest,
|
||||
},
|
||||
routes::{
|
||||
__path_create_task, __path_delete_task, __path_disable_task, __path_enable_task,
|
||||
__path_get_task, __path_list_tasks, __path_update_task,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct HealthResponse {
|
||||
/// Current service status.
|
||||
pub status: String,
|
||||
/// Stable service identifier.
|
||||
pub service: String,
|
||||
/// Backend package version.
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct SessionResponse {
|
||||
/// Random per-process token required for mutating browser requests.
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/health",
|
||||
tag = "system",
|
||||
responses(
|
||||
(status = 200, description = "TaskPilot API is healthy", body = HealthResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn health() -> Json<HealthResponse> {
|
||||
Json(HealthResponse {
|
||||
status: "ok".to_owned(),
|
||||
service: "task-pilot-backend".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/session",
|
||||
tag = "system",
|
||||
responses(
|
||||
(status = 200, description = "Local API session token", body = SessionResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn session(State(state): State<AppState>) -> Json<SessionResponse> {
|
||||
Json(SessionResponse {
|
||||
token: state.auth_token().to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/cron/preview",
|
||||
tag = "system",
|
||||
request_body = CronPreviewRequest,
|
||||
responses(
|
||||
(status = 200, description = "Upcoming cron occurrences", body = CronPreviewResponse),
|
||||
(status = 400, description = "Invalid cron preview request", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn preview_cron(
|
||||
ApiJson(request): ApiJson<CronPreviewRequest>,
|
||||
) -> ApiResult<Json<CronPreviewResponse>> {
|
||||
if request.count == 0 || request.count > 10 {
|
||||
return Err(ApiError::validation("count", "预览次数必须在 1 到 10 之间"));
|
||||
}
|
||||
let preview = cron::preview_in_timezone(
|
||||
&request.expression,
|
||||
&request.timezone,
|
||||
chrono::Utc::now(),
|
||||
request.count,
|
||||
)
|
||||
.map_err(|error| ApiError::validation("expression", error.to_string()))?;
|
||||
Ok(Json(preview))
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "TaskPilot API",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
description = "Local task scheduling and process management API."
|
||||
),
|
||||
paths(
|
||||
health,
|
||||
session,
|
||||
preview_cron,
|
||||
list_tasks,
|
||||
create_task,
|
||||
get_task,
|
||||
update_task,
|
||||
delete_task,
|
||||
enable_task,
|
||||
disable_task,
|
||||
run_task,
|
||||
list_task_runs,
|
||||
get_run,
|
||||
get_run_logs,
|
||||
cancel_run,
|
||||
download_run_logs,
|
||||
execution_events
|
||||
),
|
||||
components(schemas(
|
||||
HealthResponse,
|
||||
SessionResponse,
|
||||
CronPreviewRequest,
|
||||
CronPreviewResponse,
|
||||
ErrorResponse,
|
||||
TaskActionType,
|
||||
HttpMethod,
|
||||
ConcurrencyPolicy,
|
||||
RetryPolicy,
|
||||
TaskAction,
|
||||
CreateTaskRequest,
|
||||
UpdateTaskRequest,
|
||||
TaskResponse,
|
||||
TaskListResponse,
|
||||
TaskRunResponse,
|
||||
TaskRunListResponse,
|
||||
LogChunkResponse
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
pub fn openapi() -> utoipa::openapi::OpenApi {
|
||||
ApiDoc::openapi()
|
||||
}
|
||||
|
||||
async fn openapi_json() -> Json<utoipa::openapi::OpenApi> {
|
||||
Json(openapi())
|
||||
}
|
||||
|
||||
pub fn router(state: AppState) -> Router {
|
||||
let allowed_origins = [
|
||||
HeaderValue::from_static("http://127.0.0.1:5173"),
|
||||
HeaderValue::from_static("http://localhost:5173"),
|
||||
];
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(allowed_origins)
|
||||
.allow_methods([
|
||||
Method::GET,
|
||||
Method::POST,
|
||||
Method::PUT,
|
||||
Method::DELETE,
|
||||
Method::OPTIONS,
|
||||
])
|
||||
.allow_headers([header::ACCEPT, header::CONTENT_TYPE]);
|
||||
|
||||
let api = Router::new()
|
||||
.route("/api/v1/health", get(health))
|
||||
.route("/api/v1/session", get(session))
|
||||
.route("/api/v1/cron/preview", axum::routing::post(preview_cron))
|
||||
.route("/api/v1/openapi.json", get(openapi_json))
|
||||
.merge(crate::tasks::routes::router())
|
||||
.merge(crate::runs::routes::router())
|
||||
.route("/api/{*path}", any(api_not_found))
|
||||
.layer(cors)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
security::api_guard,
|
||||
));
|
||||
|
||||
api.with_state(state)
|
||||
}
|
||||
|
||||
async fn api_not_found() -> impl IntoResponse {
|
||||
ApiError::route_not_found("API 路由不存在")
|
||||
}
|
||||
Reference in New Issue
Block a user