feat: 加固运行期可靠性并补齐调度与观测接口

- 运行句柄按 run_id 归属,修复取消后重跑时闸门被旧运行删除的竞态
- 调度循环改用取消令牌退出,启动时对账遗留运行,校验 rows_affected
- 三类任务统一参与调度,新增 /api/v1/stats 与 /api/v1/schedule
- SQLite 启用 WAL 与 busy_timeout,日志清理跳过活跃文件,单行上限 256 KiB
- 前端缓存管理令牌并在 403 时重试一次,仪表盘改用统计接口
This commit is contained in:
2026-10-26 00:38:16 +08:00
parent 773196d689
commit 815258c19b
19 changed files with 990 additions and 93 deletions
+53 -3
View File
@@ -1,6 +1,6 @@
use axum::{
Json, Router,
extract::State,
extract::{DefaultBodyLimit, State},
http::{HeaderValue, Method, header},
middleware,
response::IntoResponse,
@@ -31,8 +31,9 @@ use crate::{
state::AppState,
tasks::{
model::{
ConcurrencyPolicy, CreateTaskRequest, HttpMethod, RetryPolicy, TaskAction,
TaskActionType, TaskListResponse, TaskResponse, UpdateTaskRequest,
ConcurrencyPolicy, CreateTaskRequest, HttpMethod, RetryPolicy, ScheduledTaskResponse,
TaskAction, TaskActionType, TaskListResponse, TaskResponse, TaskStatsResponse,
UpdateTaskRequest,
},
routes::{
__path_create_task, __path_delete_task, __path_disable_task, __path_enable_task,
@@ -162,6 +163,47 @@ pub async fn list_directories(
})
}
#[utoipa::path(
get,
path = "/api/v1/stats",
tag = "tasks",
responses((status = 200, description = "Dashboard counters", body = TaskStatsResponse))
)]
pub async fn task_stats(State(state): State<AppState>) -> ApiResult<Json<TaskStatsResponse>> {
let counts = crate::tasks::repository::counts(&state.pool).await?;
Ok(Json(TaskStatsResponse {
total: counts.total,
enabled: counts.enabled,
running: u64::try_from(state.executor.running_count()).unwrap_or_default(),
script: counts.script,
http: counts.http,
process: counts.process,
}))
}
#[utoipa::path(
get,
path = "/api/v1/schedule",
tag = "tasks",
responses(
(status = 200, description = "Next trigger time per enabled task", body = Vec<ScheduledTaskResponse>)
)
)]
pub async fn list_schedule(
State(state): State<AppState>,
) -> ApiResult<Json<Vec<ScheduledTaskResponse>>> {
let tasks = crate::tasks::repository::list_enabled_tasks(&state.pool).await?;
Ok(Json(
tasks
.into_iter()
.map(|task| ScheduledTaskResponse {
next_run_at: state.scheduler.next_run_at(&task.id),
task_id: task.id,
})
.collect(),
))
}
#[derive(OpenApi)]
#[openapi(
info(
@@ -175,6 +217,8 @@ pub async fn list_directories(
preview_cron,
probe_python_venv,
list_directories,
task_stats,
list_schedule,
list_tasks,
create_task,
get_task,
@@ -202,6 +246,8 @@ pub async fn list_directories(
InterpreterKind,
DirectoryEntry,
DirectoryListingResponse,
TaskStatsResponse,
ScheduledTaskResponse,
ErrorResponse,
TaskActionType,
HttpMethod,
@@ -252,10 +298,14 @@ pub fn router(state: AppState) -> Router {
axum::routing::post(probe_python_venv),
)
.route("/api/v1/system/directories", get(list_directories))
.route("/api/v1/stats", get(task_stats))
.route("/api/v1/schedule", get(list_schedule))
.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))
// 本地工具也不需要接受超大请求体;任务载荷与日志上传都用不到 2 MiB 以上。
.layer(DefaultBodyLimit::max(2 * 1024 * 1024))
.layer(cors)
.route_layer(middleware::from_fn_with_state(
state.clone(),