refactor(workflow_ui): 抽出 comfy_proxy 子模块

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
boos
2026-06-18 14:47:24 +08:00
parent bbf0a3b81a
commit 9cad69b88c
2 changed files with 341 additions and 298 deletions
+321
View File
@@ -0,0 +1,321 @@
use super::auth::{auth_caller, caller_is_admin, caller_user_id};
use super::models::workflow_route_models;
use super::node_defs::{inject_boosapi_workflow_template_payload, workflow_template_payload};
use super::{
auth_caller_optional, inject_boosapi_object_info, native_workflow_json_cached,
native_workflow_metadata_ttl, native_workflow_status_ttl, parse_boolish, safe_view_path,
user_home, workflow_node_defs_with_models, UserDataQuery, WorkflowCaller,
};
use crate::{
error::{AppError, AppResult},
AppState,
};
use axum::{
extract::{Path, Query, State},
http::{header, HeaderMap, Method, StatusCode},
response::{IntoResponse, Response},
Json,
};
use serde_json::{json, Value};
use std::collections::HashMap;
pub(super) async fn get_user_config(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let caller = auth_caller(&state, &headers).await?;
let audience = match caller {
WorkflowCaller::Admin => "admin",
WorkflowCaller::User { .. } => "user",
};
Ok(Json(json!({
"username": audience,
"role": audience,
"id": caller_user_id(&caller),
})))
}
pub(super) async fn create_user(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Response> {
let _ = auth_caller(&state, &headers).await?;
Ok((
StatusCode::METHOD_NOT_ALLOWED,
Json(json!({ "error": "read-only" })),
)
.into_response())
}
pub(super) async fn get_settings(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
if state.config.comfyui.uses_cloud_backend() {
return Ok(Json(json!({
"Comfy.Workflow.ShowMissingModelsWarning": false
})));
}
Ok(Json(json!({})))
}
pub(super) async fn get_setting(
State(state): State<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
if state.config.comfyui.uses_cloud_backend() && id == "Comfy.Workflow.ShowMissingModelsWarning"
{
return Ok(Json(Value::Bool(false)));
}
Ok(Json(Value::Null))
}
pub(super) async fn store_settings(
State(state): State<AppState>,
headers: HeaderMap,
Json(_body): Json<Value>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
Ok(Json(json!({ "success": true })))
}
pub(super) async fn store_setting(
State(state): State<AppState>,
headers: HeaderMap,
Path(_id): Path<String>,
Json(_body): Json<Value>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
Ok(Json(json!({ "success": true })))
}
pub(super) async fn userdata_list(
State(state): State<AppState>,
headers: HeaderMap,
Query(query): Query<UserDataQuery>,
) -> AppResult<Response> {
let caller =
auth_caller_optional(&state, &headers).unwrap_or(WorkflowCaller::User { user_id: 0 });
let user_id = caller_user_id(&caller);
let dir = query.dir.unwrap_or_else(|| "workflow".to_string());
let base = user_home(user_id).join("userdata").join(&dir);
if tokio::fs::metadata(&base).await.is_err() {
return Ok(Json(json!([])).into_response());
}
if query.full_info {
let mut entries = Vec::new();
let mut reader = match tokio::fs::read_dir(&base).await {
Ok(value) => value,
Err(_) => return Ok(Json(json!([])).into_response()),
};
while let Ok(Some(entry)) = reader.next_entry().await {
let path = entry.path();
let meta = match entry.metadata().await {
Ok(meta) => meta,
Err(_) => continue,
};
entries.push(json!({
"path": path.to_string_lossy().to_string(),
"name": entry.file_name().to_string_lossy().to_string(),
"type": if meta.is_dir() { "directory" } else { "file" },
"size": meta.len(),
}));
}
return Ok(Json(Value::Array(entries)).into_response());
}
let mut items = Vec::new();
let mut reader = match tokio::fs::read_dir(&base).await {
Ok(value) => value,
Err(_) => return Ok(Json(json!([])).into_response()),
};
while let Ok(Some(entry)) = reader.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if query.split {
items.push(json!([name.clone()]));
} else {
items.push(Value::String(name));
}
}
Ok(Json(Value::Array(items)).into_response())
}
pub(super) async fn userdata_get(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
let bytes = tokio::fs::read(&path)
.await
.map_err(|_| AppError::NotFound("User data file not found".into()))?;
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
)
.into_response())
}
pub(super) async fn userdata_store(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
Query(query): Query<HashMap<String, String>>,
body: axum::body::Bytes,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
}
tokio::fs::write(&path, body)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
Ok(Json(json!({
"name": file,
"subfolder": query.get("subfolder").cloned().unwrap_or_default(),
"type": query.get("type").cloned().unwrap_or_else(|| "input".to_string())
}))
.into_response())
}
pub(super) async fn userdata_delete(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
let _ = tokio::fs::remove_file(&path).await;
Ok(StatusCode::NO_CONTENT.into_response())
}
pub(super) async fn move_userdata(
State(state): State<AppState>,
headers: HeaderMap,
Path((source, dest)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let src = root.join(safe_view_path(&source));
let dst = root.join(safe_view_path(&dest));
if let Some(parent) = dst.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
}
let overwrite = parse_boolish(query.get("overwrite").map(String::as_str));
if !overwrite && tokio::fs::metadata(&dst).await.is_ok() {
return Err(AppError::BadRequest("destination already exists".into()));
}
tokio::fs::rename(&src, &dst)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
Ok(Json(json!({ "success": true })).into_response())
}
pub(super) async fn extensions(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
Ok(Json(json!(["extensions/core"])))
}
pub(super) async fn workflow_templates(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
if state.config.comfyui.uses_cloud_backend() {
let mut value = native_workflow_json_cached(
&state,
Method::GET,
"/workflow_templates",
None,
native_workflow_metadata_ttl(&state),
)
.await
.map(|value| value.0)
.unwrap_or_else(|_| json!({}));
inject_boosapi_workflow_template_payload(&mut value);
return Ok(Json(value));
}
Ok(Json(workflow_template_payload()))
}
pub(super) async fn embeddings(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
Ok(Json(json!([])))
}
pub(super) async fn object_info(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
let route_models = workflow_route_models(&state).await.unwrap_or_default();
if state.config.comfyui.uses_native_backend() {
let mut value = native_workflow_json_cached(
&state,
Method::GET,
"/object_info",
None,
native_workflow_metadata_ttl(&state),
)
.await?
.0;
inject_boosapi_object_info(&mut value, &route_models);
return Ok(Json(value));
}
Ok(Json(Value::Object(workflow_node_defs_with_models(
&route_models,
true,
))))
}
pub(super) async fn system_stats(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
if state.config.comfyui.uses_native_backend() {
let _ = auth_caller_optional(&state, &headers);
return native_workflow_json_cached(
&state,
Method::GET,
"/system_stats",
None,
native_workflow_status_ttl(&state),
)
.await;
}
let caller =
auth_caller_optional(&state, &headers).unwrap_or(WorkflowCaller::User { user_id: 0 });
let host = if state.config.server.host == "0.0.0.0" || state.config.server.host == "::" {
"127.0.0.1"
} else {
state.config.server.host.as_str()
};
Ok(Json(json!({
"system": {
"host": host,
"port": state.config.server.port,
"role": if caller_is_admin(&caller) { "admin" } else { "user" }
},
"devices": [],
"queue_remaining": 0,
})))
}
+20 -298
View File
@@ -90,12 +90,12 @@ struct ViewQuery {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct UserDataQuery { pub(super) struct UserDataQuery {
dir: Option<String>, pub(super) dir: Option<String>,
#[serde(default)] #[serde(default)]
split: bool, pub(super) split: bool,
#[serde(default)] #[serde(default)]
full_info: bool, pub(super) full_info: bool,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -124,8 +124,7 @@ use auth::{auth_caller, caller_from_headers, caller_is_admin, caller_user_id};
mod node_defs; mod node_defs;
use node_defs::{ use node_defs::{
core_node_definitions, core_workflow_template_index, default_boosapi_workflow_graph, core_node_definitions, core_workflow_template_index, default_boosapi_workflow_graph,
inject_boosapi_workflow_template_payload, node_type_def_to_comfy, normalize_prompt_payload, node_type_def_to_comfy, normalize_prompt_payload, text_to_video_workflow_graph,
text_to_video_workflow_graph, workflow_template_payload,
}; };
mod models; mod models;
use models::{ use models::{
@@ -138,6 +137,12 @@ use assets::{
embedded_workflow_ui_for_config, official_workflow_template_asset, workflow_cloud_api_key, embedded_workflow_ui_for_config, official_workflow_template_asset, workflow_cloud_api_key,
workflow_ui_index, workflow_ui_runtime, workflow_ui_static, workflow_ui_index, workflow_ui_runtime, workflow_ui_static,
}; };
mod comfy_proxy;
use comfy_proxy::{
create_user, embeddings, extensions, get_setting, get_settings, get_user_config, move_userdata,
object_info, store_setting, store_settings, system_stats, userdata_delete, userdata_get,
userdata_list, userdata_store, workflow_templates,
};
pub fn routes() -> Router<AppState> { pub fn routes() -> Router<AppState> {
Router::new() Router::new()
@@ -230,11 +235,11 @@ fn default_history_limit() -> i64 {
50 50
} }
fn safe_view_path(value: &str) -> String { pub(super) fn safe_view_path(value: &str) -> String {
value.trim().replace('\\', "/") value.trim().replace('\\', "/")
} }
fn user_home(user_id: Option<i64>) -> PathBuf { pub(super) fn user_home(user_id: Option<i64>) -> PathBuf {
let base = std::env::var("BOOSAPI_WORKSPACE_DATA_ROOT") let base = std::env::var("BOOSAPI_WORKSPACE_DATA_ROOT")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/data/boosapi-workspace")); .unwrap_or_else(|_| PathBuf::from("/data/boosapi-workspace"));
@@ -285,7 +290,7 @@ fn workspace_kind_from_mime(mime_type: &str) -> Option<&'static str> {
} }
} }
fn parse_boolish(input: Option<&str>) -> bool { pub(super) fn parse_boolish(input: Option<&str>) -> bool {
matches!( matches!(
input.map(|v| v.trim().to_ascii_lowercase()).as_deref(), input.map(|v| v.trim().to_ascii_lowercase()).as_deref(),
Some("1" | "true" | "yes" | "on") Some("1" | "true" | "yes" | "on")
@@ -370,7 +375,7 @@ fn is_boosapi_extension_node_type(node_type: &str) -> bool {
) )
} }
fn workflow_node_defs_with_models( pub(super) fn workflow_node_defs_with_models(
models: &[String], models: &[String],
include_compat_core: bool, include_compat_core: bool,
) -> Map<String, Value> { ) -> Map<String, Value> {
@@ -390,7 +395,7 @@ fn workflow_node_defs_with_models(
defs defs
} }
fn inject_boosapi_object_info(value: &mut Value, models: &[String]) { pub(super) fn inject_boosapi_object_info(value: &mut Value, models: &[String]) {
let Some(object) = value.as_object_mut() else { let Some(object) = value.as_object_mut() else {
return; return;
}; };
@@ -743,293 +748,10 @@ fn cache_control_for_path(path: &StdPath) -> &'static str {
} }
} }
fn auth_caller_optional(state: &AppState, headers: &HeaderMap) -> Option<WorkflowCaller> { pub(super) fn auth_caller_optional(state: &AppState, headers: &HeaderMap) -> Option<WorkflowCaller> {
caller_from_headers(state, headers).ok() caller_from_headers(state, headers).ok()
} }
async fn get_user_config(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let caller = auth_caller(&state, &headers).await?;
let audience = match caller {
WorkflowCaller::Admin => "admin",
WorkflowCaller::User { .. } => "user",
};
Ok(Json(json!({
"username": audience,
"role": audience,
"id": caller_user_id(&caller),
})))
}
async fn create_user(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Response> {
let _ = auth_caller(&state, &headers).await?;
Ok((
StatusCode::METHOD_NOT_ALLOWED,
Json(json!({ "error": "read-only" })),
)
.into_response())
}
async fn get_settings(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
if state.config.comfyui.uses_cloud_backend() {
return Ok(Json(json!({
"Comfy.Workflow.ShowMissingModelsWarning": false
})));
}
Ok(Json(json!({})))
}
async fn get_setting(
State(state): State<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
if state.config.comfyui.uses_cloud_backend() && id == "Comfy.Workflow.ShowMissingModelsWarning"
{
return Ok(Json(Value::Bool(false)));
}
Ok(Json(Value::Null))
}
async fn store_settings(
State(state): State<AppState>,
headers: HeaderMap,
Json(_body): Json<Value>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
Ok(Json(json!({ "success": true })))
}
async fn store_setting(
State(state): State<AppState>,
headers: HeaderMap,
Path(_id): Path<String>,
Json(_body): Json<Value>,
) -> AppResult<Json<Value>> {
let _ = auth_caller(&state, &headers).await?;
Ok(Json(json!({ "success": true })))
}
async fn userdata_list(
State(state): State<AppState>,
headers: HeaderMap,
Query(query): Query<UserDataQuery>,
) -> AppResult<Response> {
let caller =
auth_caller_optional(&state, &headers).unwrap_or(WorkflowCaller::User { user_id: 0 });
let user_id = caller_user_id(&caller);
let dir = query.dir.unwrap_or_else(|| "workflow".to_string());
let base = user_home(user_id).join("userdata").join(&dir);
if tokio::fs::metadata(&base).await.is_err() {
return Ok(Json(json!([])).into_response());
}
if query.full_info {
let mut entries = Vec::new();
let mut reader = match tokio::fs::read_dir(&base).await {
Ok(value) => value,
Err(_) => return Ok(Json(json!([])).into_response()),
};
while let Ok(Some(entry)) = reader.next_entry().await {
let path = entry.path();
let meta = match entry.metadata().await {
Ok(meta) => meta,
Err(_) => continue,
};
entries.push(json!({
"path": path.to_string_lossy().to_string(),
"name": entry.file_name().to_string_lossy().to_string(),
"type": if meta.is_dir() { "directory" } else { "file" },
"size": meta.len(),
}));
}
return Ok(Json(Value::Array(entries)).into_response());
}
let mut items = Vec::new();
let mut reader = match tokio::fs::read_dir(&base).await {
Ok(value) => value,
Err(_) => return Ok(Json(json!([])).into_response()),
};
while let Ok(Some(entry)) = reader.next_entry().await {
let name = entry.file_name().to_string_lossy().to_string();
if query.split {
items.push(json!([name.clone()]));
} else {
items.push(Value::String(name));
}
}
Ok(Json(Value::Array(items)).into_response())
}
async fn userdata_get(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
let bytes = tokio::fs::read(&path)
.await
.map_err(|_| AppError::NotFound("User data file not found".into()))?;
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, "application/octet-stream")],
bytes,
)
.into_response())
}
async fn userdata_store(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
Query(query): Query<HashMap<String, String>>,
body: axum::body::Bytes,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
}
tokio::fs::write(&path, body)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
Ok(Json(json!({
"name": file,
"subfolder": query.get("subfolder").cloned().unwrap_or_default(),
"type": query.get("type").cloned().unwrap_or_else(|| "input".to_string())
}))
.into_response())
}
async fn userdata_delete(
State(state): State<AppState>,
headers: HeaderMap,
Path(file): Path<String>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let path = root.join(safe_view_path(&file));
let _ = tokio::fs::remove_file(&path).await;
Ok(StatusCode::NO_CONTENT.into_response())
}
async fn move_userdata(
State(state): State<AppState>,
headers: HeaderMap,
Path((source, dest)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> AppResult<Response> {
let caller = auth_caller(&state, &headers).await?;
let root = user_home(caller_user_id(&caller)).join("userdata");
let src = root.join(safe_view_path(&source));
let dst = root.join(safe_view_path(&dest));
if let Some(parent) = dst.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
}
let overwrite = parse_boolish(query.get("overwrite").map(String::as_str));
if !overwrite && tokio::fs::metadata(&dst).await.is_ok() {
return Err(AppError::BadRequest("destination already exists".into()));
}
tokio::fs::rename(&src, &dst)
.await
.map_err(|err| AppError::Internal(anyhow::anyhow!(err)))?;
Ok(Json(json!({ "success": true })).into_response())
}
async fn extensions(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
Ok(Json(json!(["extensions/core"])))
}
async fn workflow_templates(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
if state.config.comfyui.uses_cloud_backend() {
let mut value = native_workflow_json_cached(
&state,
Method::GET,
"/workflow_templates",
None,
native_workflow_metadata_ttl(&state),
)
.await
.map(|value| value.0)
.unwrap_or_else(|_| json!({}));
inject_boosapi_workflow_template_payload(&mut value);
return Ok(Json(value));
}
Ok(Json(workflow_template_payload()))
}
async fn embeddings(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
Ok(Json(json!([])))
}
async fn object_info(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
let _ = auth_caller_optional(&state, &headers);
let route_models = workflow_route_models(&state).await.unwrap_or_default();
if state.config.comfyui.uses_native_backend() {
let mut value = native_workflow_json_cached(
&state,
Method::GET,
"/object_info",
None,
native_workflow_metadata_ttl(&state),
)
.await?
.0;
inject_boosapi_object_info(&mut value, &route_models);
return Ok(Json(value));
}
Ok(Json(Value::Object(workflow_node_defs_with_models(
&route_models,
true,
))))
}
async fn system_stats(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
if state.config.comfyui.uses_native_backend() {
let _ = auth_caller_optional(&state, &headers);
return native_workflow_json_cached(
&state,
Method::GET,
"/system_stats",
None,
native_workflow_status_ttl(&state),
)
.await;
}
let caller =
auth_caller_optional(&state, &headers).unwrap_or(WorkflowCaller::User { user_id: 0 });
let host = if state.config.server.host == "0.0.0.0" || state.config.server.host == "::" {
"127.0.0.1"
} else {
state.config.server.host.as_str()
};
Ok(Json(json!({
"system": {
"host": host,
"port": state.config.server.port,
"role": if caller_is_admin(&caller) { "admin" } else { "user" }
},
"devices": [],
"queue_remaining": 0,
})))
}
async fn workflow_cache_status( async fn workflow_cache_status(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
@@ -2716,7 +2438,7 @@ fn native_workflow_enabled(state: &AppState) -> bool {
state.config.comfyui.uses_native_backend() state.config.comfyui.uses_native_backend()
} }
fn native_workflow_metadata_ttl(state: &AppState) -> Duration { pub(super) fn native_workflow_metadata_ttl(state: &AppState) -> Duration {
if state.config.comfyui.uses_runninghub_backend() { if state.config.comfyui.uses_runninghub_backend() {
Duration::from_secs(21600) Duration::from_secs(21600)
} else { } else {
@@ -2724,7 +2446,7 @@ fn native_workflow_metadata_ttl(state: &AppState) -> Duration {
} }
} }
fn native_workflow_status_ttl(state: &AppState) -> Duration { pub(super) fn native_workflow_status_ttl(state: &AppState) -> Duration {
if state.config.comfyui.uses_runninghub_backend() { if state.config.comfyui.uses_runninghub_backend() {
Duration::from_secs(120) Duration::from_secs(120)
} else { } else {
@@ -2804,7 +2526,7 @@ async fn native_workflow_json(
Ok(Json(value)) Ok(Json(value))
} }
async fn native_workflow_json_cached( pub(super) async fn native_workflow_json_cached(
state: &AppState, state: &AppState,
method: Method, method: Method,
path: &str, path: &str,