mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
448 lines
18 KiB
Rust
448 lines
18 KiB
Rust
fn admin_video_task_status_name(
|
|||
|
|
status: aether_data::repository::video_tasks::VideoTaskStatus,
|
||
|
|
) -> &'static str {
|
||
|
|
match status {
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Pending => "pending",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Submitted => "submitted",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Queued => "queued",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Processing => "processing",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Completed => "completed",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Failed => "failed",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Cancelled => "cancelled",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Expired => "expired",
|
||
|
|
aether_data::repository::video_tasks::VideoTaskStatus::Deleted => "deleted",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn admin_video_task_timestamp(unix_secs: Option<u64>) -> Option<String> {
|
||
|
|
unix_secs.and_then(|value| {
|
||
|
|
chrono::DateTime::<Utc>::from_timestamp(value as i64, 0).map(|timestamp| {
|
||
|
|
timestamp.to_rfc3339_opts(SecondsFormat::Secs, true)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn truncate_admin_video_task_prompt(prompt: Option<&str>) -> Option<String> {
|
||
|
|
prompt.map(|value| {
|
||
|
|
if value.chars().count() <= 100 {
|
||
|
|
value.to_string()
|
||
|
|
} else {
|
||
|
|
let mut truncated = value.chars().take(100).collect::<String>();
|
||
|
|
truncated.push_str("...");
|
||
|
|
truncated
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn build_admin_video_task_provider_names(
|
||
|
|
state: &AppState,
|
||
|
|
tasks: &[aether_data::repository::video_tasks::StoredVideoTask],
|
||
|
|
) -> Result<BTreeMap<String, String>, GatewayError> {
|
||
|
|
let provider_ids = tasks
|
||
|
|
.iter()
|
||
|
|
.filter_map(|task| task.provider_id.as_ref())
|
||
|
|
.cloned()
|
||
|
|
.collect::<BTreeSet<_>>()
|
||
|
|
.into_iter()
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
if provider_ids.is_empty() {
|
||
|
|
return Ok(BTreeMap::new());
|
||
|
|
}
|
||
|
|
Ok(state
|
||
|
|
.read_provider_catalog_providers_by_ids(&provider_ids)
|
||
|
|
.await?
|
||
|
|
.into_iter()
|
||
|
|
.map(|provider| (provider.id, provider.name))
|
||
|
|
.collect())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_admin_video_task_list_item(
|
||
|
|
task: &aether_data::repository::video_tasks::StoredVideoTask,
|
||
|
|
provider_names: &BTreeMap<String, String>,
|
||
|
|
) -> serde_json::Value {
|
||
|
|
let provider_name = task
|
||
|
|
.provider_id
|
||
|
|
.as_deref()
|
||
|
|
.and_then(|provider_id| provider_names.get(provider_id))
|
||
|
|
.cloned()
|
||
|
|
.unwrap_or_else(|| "Unknown".to_string());
|
||
|
|
json!({
|
||
|
|
"id": task.id,
|
||
|
|
"external_task_id": task.external_task_id,
|
||
|
|
"user_id": task.user_id,
|
||
|
|
"username": task.username.clone().unwrap_or_else(|| "Unknown".to_string()),
|
||
|
|
"model": task.model,
|
||
|
|
"prompt": truncate_admin_video_task_prompt(task.prompt.as_deref()),
|
||
|
|
"status": admin_video_task_status_name(task.status),
|
||
|
|
"progress_percent": task.progress_percent,
|
||
|
|
"progress_message": task.progress_message,
|
||
|
|
"provider_id": task.provider_id,
|
||
|
|
"provider_name": provider_name,
|
||
|
|
"duration_seconds": task.duration_seconds,
|
||
|
|
"resolution": task.resolution,
|
||
|
|
"aspect_ratio": task.aspect_ratio,
|
||
|
|
"video_url": task.video_url,
|
||
|
|
"error_code": task.error_code,
|
||
|
|
"error_message": task.error_message,
|
||
|
|
"poll_count": task.poll_count,
|
||
|
|
"max_poll_count": task.max_poll_count,
|
||
|
|
"created_at": admin_video_task_timestamp(Some(task.created_at_unix_secs)),
|
||
|
|
"completed_at": admin_video_task_timestamp(task.completed_at_unix_secs),
|
||
|
|
"submitted_at": admin_video_task_timestamp(task.submitted_at_unix_secs),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn admin_video_task_detail_id_from_path(request_path: &str) -> Option<&str> {
|
||
|
|
let task_id = request_path.strip_prefix("/api/admin/video-tasks/")?;
|
||
|
|
if task_id.is_empty() || task_id.contains('/') || task_id == "stats" {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(task_id)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn admin_video_task_nested_id_from_path<'a>(
|
||
|
|
request_path: &'a str,
|
||
|
|
suffix: &str,
|
||
|
|
) -> Option<&'a str> {
|
||
|
|
let task_id = request_path
|
||
|
|
.strip_prefix("/api/admin/video-tasks/")?
|
||
|
|
.strip_suffix(suffix)?;
|
||
|
|
if task_id.is_empty() || task_id.contains('/') {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(task_id)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn current_admin_video_task_unix_secs() -> u64 {
|
||
|
|
SystemTime::now()
|
||
|
|
.duration_since(UNIX_EPOCH)
|
||
|
|
.unwrap_or_default()
|
||
|
|
.as_secs()
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn maybe_build_local_admin_video_tasks_response(
|
||
|
|
state: &AppState,
|
||
|
|
request_context: &GatewayPublicRequestContext,
|
||
|
|
) -> Result<Option<Response<Body>>, GatewayError> {
|
||
|
|
let Some(decision) = request_context.control_decision.as_ref() else {
|
||
|
|
return Ok(None);
|
||
|
|
};
|
||
|
|
if decision.route_family.as_deref() != Some("video_tasks_manage") {
|
||
|
|
return Ok(None);
|
||
|
|
}
|
||
|
|
|
||
|
|
if request_context.request_method == http::Method::GET
|
||
|
|
&& matches!(
|
||
|
|
request_context.request_path.as_str(),
|
||
|
|
"/api/admin/video-tasks" | "/api/admin/video-tasks/"
|
||
|
|
)
|
||
|
|
{
|
||
|
|
let status = match query_param_value(request_context.request_query_string.as_deref(), "status")
|
||
|
|
{
|
||
|
|
Some(value) => match aether_data::repository::video_tasks::VideoTaskStatus::from_database(&value) {
|
||
|
|
Ok(status) => Some(status),
|
||
|
|
Err(err) => {
|
||
|
|
return Ok(Some(build_proxy_error_response(
|
||
|
|
http::StatusCode::BAD_REQUEST,
|
||
|
|
"invalid_request",
|
||
|
|
err.to_string(),
|
||
|
|
None,
|
||
|
|
)));
|
||
|
|
}
|
||
|
|
},
|
||
|
|
None => None,
|
||
|
|
};
|
||
|
|
let filter = aether_data::repository::video_tasks::VideoTaskQueryFilter {
|
||
|
|
user_id: query_param_value(request_context.request_query_string.as_deref(), "user_id"),
|
||
|
|
status,
|
||
|
|
model_substring: query_param_value(
|
||
|
|
request_context.request_query_string.as_deref(),
|
||
|
|
"model",
|
||
|
|
),
|
||
|
|
client_api_format: None,
|
||
|
|
};
|
||
|
|
let page = query_param_value(request_context.request_query_string.as_deref(), "page")
|
||
|
|
.and_then(|value| value.parse::<usize>().ok())
|
||
|
|
.unwrap_or(1);
|
||
|
|
let page_size = query_param_value(
|
||
|
|
request_context.request_query_string.as_deref(),
|
||
|
|
"page_size",
|
||
|
|
)
|
||
|
|
.and_then(|value| value.parse::<usize>().ok())
|
||
|
|
.unwrap_or(20);
|
||
|
|
let response =
|
||
|
|
crate::gateway::async_task::read_video_task_page(state, &filter, page, page_size)
|
||
|
|
.await?;
|
||
|
|
let provider_names = build_admin_video_task_provider_names(state, &response.items).await?;
|
||
|
|
return Ok(Some(
|
||
|
|
Json(json!({
|
||
|
|
"items": response
|
||
|
|
.items
|
||
|
|
.iter()
|
||
|
|
.map(|task| build_admin_video_task_list_item(task, &provider_names))
|
||
|
|
.collect::<Vec<_>>(),
|
||
|
|
"total": response.total,
|
||
|
|
"page": response.page,
|
||
|
|
"page_size": response.page_size,
|
||
|
|
"pages": response.pages,
|
||
|
|
}))
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
if request_context.request_method == http::Method::GET
|
||
|
|
&& matches!(
|
||
|
|
request_context.request_path.as_str(),
|
||
|
|
"/api/admin/video-tasks/stats" | "/api/admin/video-tasks/stats/"
|
||
|
|
)
|
||
|
|
{
|
||
|
|
let filter = aether_data::repository::video_tasks::VideoTaskQueryFilter {
|
||
|
|
user_id: None,
|
||
|
|
status: None,
|
||
|
|
model_substring: None,
|
||
|
|
client_api_format: None,
|
||
|
|
};
|
||
|
|
let stats = crate::gateway::async_task::read_video_task_stats(
|
||
|
|
state,
|
||
|
|
&filter,
|
||
|
|
current_admin_video_task_unix_secs(),
|
||
|
|
)
|
||
|
|
.await?;
|
||
|
|
let active_users = state.count_distinct_video_task_users(&filter).await?;
|
||
|
|
return Ok(Some(
|
||
|
|
Json(json!({
|
||
|
|
"total": stats.total,
|
||
|
|
"by_status": stats.by_status,
|
||
|
|
"by_model": stats.by_model,
|
||
|
|
"today_count": stats.today_count,
|
||
|
|
"active_users": active_users,
|
||
|
|
"processing_count": stats.processing_count,
|
||
|
|
}))
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
if request_context.request_method == http::Method::POST {
|
||
|
|
let Some(task_id) = admin_video_task_nested_id_from_path(
|
||
|
|
&request_context.request_path,
|
||
|
|
"/cancel",
|
||
|
|
) else {
|
||
|
|
return Ok(None);
|
||
|
|
};
|
||
|
|
let stored = match crate::gateway::async_task::cancel_video_task_record(state, task_id)
|
||
|
|
.await
|
||
|
|
{
|
||
|
|
Ok(stored) => stored,
|
||
|
|
Err(crate::gateway::async_task::CancelVideoTaskError::NotFound) => {
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::NOT_FOUND,
|
||
|
|
Json(json!({ "detail": "Video task not found" })),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
Err(crate::gateway::async_task::CancelVideoTaskError::InvalidStatus(status)) => {
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::BAD_REQUEST,
|
||
|
|
Json(json!({
|
||
|
|
"detail": format!(
|
||
|
|
"Cannot cancel task with status: {}",
|
||
|
|
admin_video_task_status_name(status),
|
||
|
|
),
|
||
|
|
})),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
Err(crate::gateway::async_task::CancelVideoTaskError::Response(response)) => {
|
||
|
|
return Ok(Some(response));
|
||
|
|
}
|
||
|
|
Err(crate::gateway::async_task::CancelVideoTaskError::Gateway(err)) => {
|
||
|
|
return Err(err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
return Ok(Some(
|
||
|
|
Json(json!({
|
||
|
|
"id": stored.id,
|
||
|
|
"status": "cancelled",
|
||
|
|
"message": "Task cancelled successfully",
|
||
|
|
}))
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
if request_context.request_method == http::Method::GET {
|
||
|
|
let Some(task_id) =
|
||
|
|
admin_video_task_nested_id_from_path(&request_context.request_path, "/video")
|
||
|
|
else {
|
||
|
|
// handled by detail branch below
|
||
|
|
let Some(task_id) = admin_video_task_detail_id_from_path(&request_context.request_path)
|
||
|
|
else {
|
||
|
|
return Ok(None);
|
||
|
|
};
|
||
|
|
let Some(task) =
|
||
|
|
crate::gateway::async_task::read_video_task_detail(state, task_id).await?
|
||
|
|
else {
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::NOT_FOUND,
|
||
|
|
Json(json!({ "detail": "Video task not found" })),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
|
||
|
|
let provider_names =
|
||
|
|
build_admin_video_task_provider_names(state, std::slice::from_ref(&task)).await?;
|
||
|
|
let provider_name = task
|
||
|
|
.provider_id
|
||
|
|
.as_deref()
|
||
|
|
.and_then(|provider_id| provider_names.get(provider_id))
|
||
|
|
.cloned()
|
||
|
|
.unwrap_or_else(|| "Unknown".to_string());
|
||
|
|
let endpoint = if let Some(endpoint_id) = task.endpoint_id.as_ref() {
|
||
|
|
let endpoints = state
|
||
|
|
.read_provider_catalog_endpoints_by_ids(std::slice::from_ref(endpoint_id))
|
||
|
|
.await?;
|
||
|
|
endpoints.into_iter().next()
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
let endpoint_payload = endpoint.map(|endpoint| {
|
||
|
|
json!({
|
||
|
|
"id": endpoint.id,
|
||
|
|
"base_url": endpoint.base_url,
|
||
|
|
"api_format": endpoint.api_format,
|
||
|
|
})
|
||
|
|
});
|
||
|
|
let mut payload = serde_json::Map::new();
|
||
|
|
payload.insert("id".to_string(), json!(task.id));
|
||
|
|
payload.insert("external_task_id".to_string(), json!(task.external_task_id));
|
||
|
|
payload.insert("user_id".to_string(), json!(task.user_id));
|
||
|
|
payload.insert(
|
||
|
|
"username".to_string(),
|
||
|
|
json!(task.username.clone().unwrap_or_else(|| "Unknown".to_string())),
|
||
|
|
);
|
||
|
|
payload.insert("api_key_id".to_string(), json!(task.api_key_id));
|
||
|
|
payload.insert("provider_id".to_string(), json!(task.provider_id));
|
||
|
|
payload.insert("provider_name".to_string(), json!(provider_name));
|
||
|
|
payload.insert("endpoint_id".to_string(), json!(task.endpoint_id));
|
||
|
|
payload.insert(
|
||
|
|
"endpoint".to_string(),
|
||
|
|
endpoint_payload.unwrap_or(serde_json::Value::Null),
|
||
|
|
);
|
||
|
|
payload.insert("key_id".to_string(), json!(task.key_id));
|
||
|
|
payload.insert("client_api_format".to_string(), json!(task.client_api_format));
|
||
|
|
payload.insert("provider_api_format".to_string(), json!(task.provider_api_format));
|
||
|
|
payload.insert("format_converted".to_string(), json!(task.format_converted));
|
||
|
|
payload.insert("model".to_string(), json!(task.model));
|
||
|
|
payload.insert("prompt".to_string(), json!(task.prompt));
|
||
|
|
payload.insert(
|
||
|
|
"original_request_body".to_string(),
|
||
|
|
json!(task.original_request_body),
|
||
|
|
);
|
||
|
|
payload.insert(
|
||
|
|
"converted_request_body".to_string(),
|
||
|
|
serde_json::Value::Null,
|
||
|
|
);
|
||
|
|
payload.insert("duration_seconds".to_string(), json!(task.duration_seconds));
|
||
|
|
payload.insert("resolution".to_string(), json!(task.resolution));
|
||
|
|
payload.insert("aspect_ratio".to_string(), json!(task.aspect_ratio));
|
||
|
|
payload.insert("size".to_string(), json!(task.size));
|
||
|
|
payload.insert(
|
||
|
|
"status".to_string(),
|
||
|
|
json!(admin_video_task_status_name(task.status)),
|
||
|
|
);
|
||
|
|
payload.insert("progress_percent".to_string(), json!(task.progress_percent));
|
||
|
|
payload.insert("progress_message".to_string(), json!(task.progress_message));
|
||
|
|
payload.insert("video_url".to_string(), json!(task.video_url));
|
||
|
|
payload.insert("video_urls".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("thumbnail_url".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("video_size_bytes".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("video_duration_seconds".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("video_expires_at".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("stored_video_path".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("storage_provider".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert("error_code".to_string(), json!(task.error_code));
|
||
|
|
payload.insert("error_message".to_string(), json!(task.error_message));
|
||
|
|
payload.insert("retry_count".to_string(), json!(task.retry_count));
|
||
|
|
payload.insert("max_retries".to_string(), serde_json::Value::Null);
|
||
|
|
payload.insert(
|
||
|
|
"poll_interval_seconds".to_string(),
|
||
|
|
json!(task.poll_interval_seconds),
|
||
|
|
);
|
||
|
|
payload.insert(
|
||
|
|
"next_poll_at".to_string(),
|
||
|
|
json!(admin_video_task_timestamp(task.next_poll_at_unix_secs)),
|
||
|
|
);
|
||
|
|
payload.insert("poll_count".to_string(), json!(task.poll_count));
|
||
|
|
payload.insert("max_poll_count".to_string(), json!(task.max_poll_count));
|
||
|
|
payload.insert(
|
||
|
|
"created_at".to_string(),
|
||
|
|
json!(admin_video_task_timestamp(Some(task.created_at_unix_secs))),
|
||
|
|
);
|
||
|
|
payload.insert(
|
||
|
|
"updated_at".to_string(),
|
||
|
|
json!(admin_video_task_timestamp(Some(task.updated_at_unix_secs))),
|
||
|
|
);
|
||
|
|
payload.insert(
|
||
|
|
"submitted_at".to_string(),
|
||
|
|
json!(admin_video_task_timestamp(task.submitted_at_unix_secs)),
|
||
|
|
);
|
||
|
|
payload.insert(
|
||
|
|
"completed_at".to_string(),
|
||
|
|
json!(admin_video_task_timestamp(task.completed_at_unix_secs)),
|
||
|
|
);
|
||
|
|
payload.insert("request_metadata".to_string(), json!(task.request_metadata));
|
||
|
|
|
||
|
|
return Ok(Some(
|
||
|
|
Json(serde_json::Value::Object(payload)).into_response(),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
let Some(task) = crate::gateway::async_task::read_video_task_detail(state, task_id).await?
|
||
|
|
else {
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::NOT_FOUND,
|
||
|
|
Json(json!({ "detail": "Video task not found" })),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
if task
|
||
|
|
.video_url
|
||
|
|
.as_deref()
|
||
|
|
.map(str::trim)
|
||
|
|
.filter(|value| !value.is_empty())
|
||
|
|
.is_none()
|
||
|
|
{
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::NOT_FOUND,
|
||
|
|
Json(json!({ "detail": "Video not available" })),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
let Some(source) = crate::gateway::async_task::read_video_task_video_source(state, task_id)
|
||
|
|
.await?
|
||
|
|
else {
|
||
|
|
return Ok(Some(
|
||
|
|
(
|
||
|
|
http::StatusCode::NOT_FOUND,
|
||
|
|
Json(json!({ "detail": "Video not available" })),
|
||
|
|
)
|
||
|
|
.into_response(),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
return Ok(Some(
|
||
|
|
crate::gateway::async_task::build_video_task_video_response(state, task_id, source)
|
||
|
|
.await?,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(None)
|
||
|
|
}
|