feat: add worker operations and fix gitea actions
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 29s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Successful in 33m13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 58s
ui-regression / playwright-regression (push) Failing after 13m24s

This commit is contained in:
2026-04-02 03:43:37 +08:00
parent ee0bec4a78
commit a516be2e91
37 changed files with 3890 additions and 879 deletions

View File

@@ -13,7 +13,7 @@ use crate::{
},
services::{
admin_audit, backups, post_revisions as revision_service,
subscriptions as subscription_service,
subscriptions as subscription_service, worker_jobs,
},
};
@@ -35,6 +35,15 @@ pub struct DeliveriesQuery {
pub limit: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct WorkerJobsQuery {
pub status: Option<String>,
pub job_kind: Option<String>,
pub worker_name: Option<String>,
pub search: Option<String>,
pub limit: Option<u64>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SubscriptionPayload {
#[serde(alias = "channelType")]
@@ -85,6 +94,11 @@ pub struct DigestDispatchRequest {
pub period: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct RetryDeliveriesRequest {
pub limit: Option<u64>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SiteBackupImportRequest {
pub backup: backups::SiteBackupDocument,
@@ -132,6 +146,12 @@ pub struct DeliveryListResponse {
pub deliveries: Vec<notification_deliveries::Model>,
}
#[derive(Clone, Debug, Serialize)]
pub struct WorkerTaskActionResponse {
pub queued: bool,
pub job: worker_jobs::WorkerJobRecord,
}
fn trim_to_option(value: Option<String>) -> Option<String> {
value.and_then(|item| {
let trimmed = item.trim().to_string();
@@ -408,6 +428,13 @@ pub async fn test_subscription(
.ok_or(Error::NotFound)?;
let delivery = subscription_service::send_test_notification(&ctx, &item).await?;
let job = worker_jobs::find_latest_job_by_related_entity(
&ctx,
"notification_delivery",
&delivery.id.to_string(),
Some(worker_jobs::WORKER_NOTIFICATION_DELIVERY),
)
.await?;
admin_audit::log_event(
&ctx,
Some(&actor),
@@ -419,7 +446,12 @@ pub async fn test_subscription(
)
.await?;
format::json(serde_json::json!({ "queued": true, "id": item.id, "delivery_id": delivery.id }))
format::json(serde_json::json!({
"queued": true,
"id": item.id,
"delivery_id": delivery.id,
"job_id": job.as_ref().map(|value| value.id),
}))
}
#[debug_handler]
@@ -450,6 +482,162 @@ pub async fn send_subscription_digest(
format::json(summary)
}
#[debug_handler]
pub async fn workers_overview(
headers: HeaderMap,
State(ctx): State<AppContext>,
) -> Result<Response> {
check_auth(&headers)?;
format::json(worker_jobs::get_overview(&ctx).await?)
}
#[debug_handler]
pub async fn list_worker_jobs(
headers: HeaderMap,
Query(query): Query<WorkerJobsQuery>,
State(ctx): State<AppContext>,
) -> Result<Response> {
check_auth(&headers)?;
format::json(
worker_jobs::list_jobs(
&ctx,
worker_jobs::WorkerJobListQuery {
status: query.status,
job_kind: query.job_kind,
worker_name: query.worker_name,
search: query.search,
limit: query.limit,
},
)
.await?,
)
}
#[debug_handler]
pub async fn get_worker_job(
headers: HeaderMap,
Path(id): Path<i32>,
State(ctx): State<AppContext>,
) -> Result<Response> {
check_auth(&headers)?;
format::json(worker_jobs::get_job_record(&ctx, id).await?)
}
#[debug_handler]
pub async fn cancel_worker_job(
headers: HeaderMap,
Path(id): Path<i32>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let actor = check_auth(&headers)?;
let updated = worker_jobs::request_cancel(&ctx, id).await?;
admin_audit::log_event(
&ctx,
Some(&actor),
"worker.cancel",
"worker_job",
Some(id.to_string()),
Some(updated.worker_name.clone()),
Some(serde_json::json!({ "status": updated.status })),
)
.await?;
format::json(updated)
}
#[debug_handler]
pub async fn retry_worker_job(
headers: HeaderMap,
Path(id): Path<i32>,
State(ctx): State<AppContext>,
) -> Result<Response> {
let actor = check_auth(&headers)?;
let job = worker_jobs::retry_job(
&ctx,
id,
Some(actor.username.clone()),
Some(actor.source.clone()),
)
.await?;
admin_audit::log_event(
&ctx,
Some(&actor),
"worker.retry",
"worker_job",
Some(job.id.to_string()),
Some(job.worker_name.clone()),
Some(serde_json::json!({ "source_job_id": id })),
)
.await?;
format::json(WorkerTaskActionResponse { queued: true, job })
}
#[debug_handler]
pub async fn run_retry_deliveries_job(
headers: HeaderMap,
State(ctx): State<AppContext>,
Json(payload): Json<RetryDeliveriesRequest>,
) -> Result<Response> {
let actor = check_auth(&headers)?;
let job = worker_jobs::spawn_retry_deliveries_task(
&ctx,
payload.limit,
Some(actor.username.clone()),
Some(actor.source.clone()),
None,
Some("manual".to_string()),
)
.await?;
admin_audit::log_event(
&ctx,
Some(&actor),
"worker.task.retry_deliveries",
"worker_job",
Some(job.id.to_string()),
Some(job.worker_name.clone()),
Some(serde_json::json!({ "limit": payload.limit })),
)
.await?;
format::json(WorkerTaskActionResponse { queued: true, job })
}
#[debug_handler]
pub async fn run_digest_worker_job(
headers: HeaderMap,
State(ctx): State<AppContext>,
Json(payload): Json<DigestDispatchRequest>,
) -> Result<Response> {
let actor = check_auth(&headers)?;
let period = payload.period.unwrap_or_else(|| "weekly".to_string());
let job = worker_jobs::spawn_digest_task(
&ctx,
&period,
Some(actor.username.clone()),
Some(actor.source.clone()),
None,
Some("manual".to_string()),
)
.await?;
admin_audit::log_event(
&ctx,
Some(&actor),
"worker.task.digest",
"worker_job",
Some(job.id.to_string()),
Some(job.worker_name.clone()),
Some(serde_json::json!({ "period": period })),
)
.await?;
format::json(WorkerTaskActionResponse { queued: true, job })
}
#[debug_handler]
pub async fn export_site_backup(
headers: HeaderMap,
@@ -481,6 +669,13 @@ pub fn routes() -> Routes {
.add("/subscriptions/digest", post(send_subscription_digest))
.add("/subscriptions/{id}", patch(update_subscription).delete(delete_subscription))
.add("/subscriptions/{id}/test", post(test_subscription))
.add("/workers/overview", get(workers_overview))
.add("/workers/jobs", get(list_worker_jobs))
.add("/workers/jobs/{id}", get(get_worker_job))
.add("/workers/jobs/{id}/cancel", post(cancel_worker_job))
.add("/workers/jobs/{id}/retry", post(retry_worker_job))
.add("/workers/tasks/retry-deliveries", post(run_retry_deliveries_job))
.add("/workers/tasks/digest", post(run_digest_worker_job))
.add("/site-backup/export", get(export_site_backup))
.add("/site-backup/import", post(import_site_backup))
}