use figment::{ providers::{Env, Format, Toml}, Figment, }; use serde::Deserialize; use std::path::PathBuf; #[derive(Debug, Deserialize, Clone)] pub struct AppConfig { pub server: ServerConfig, pub db: DbConfig, pub auth: AuthConfig, pub proxy: ProxyConfig, #[serde(default)] pub comfyui: ComfyUIConfig, #[serde(default)] pub pixelle_video: PixelleVideoConfig, #[serde(default)] pub payment: PaymentConfig, #[serde(default)] pub jimeng: JimengConfig, } #[derive(Debug, Deserialize, Clone)] pub struct ServerConfig { pub host: String, pub port: u16, pub log_level: String, pub cors_origins: Vec, } #[derive(Debug, Deserialize, Clone)] pub struct DbConfig { pub url: String, pub max_connections: u32, pub ssl: bool, pub runtime_db_url: Option, pub runtime_db_dialect: Option, pub runtime_db_ssl: Option, } #[derive(Debug, Deserialize, Clone)] pub struct AuthConfig { pub jwt_secret: String, pub jwt_expiry_hours: i64, pub auth_token: String, pub proxy_token: String, } #[derive(Debug, Deserialize, Clone)] pub struct ProxyConfig { pub upstream_timeout_secs: u64, pub first_byte_timeout_secs: u64, pub max_retries: u32, pub proxy_log_retention_days: u32, #[serde(default)] pub template_proxy_url: Option, } #[derive(Debug, Deserialize, Clone)] pub struct ComfyUIConfig { pub enabled: bool, #[serde(default)] pub backend: ComfyUIBackend, #[serde(default)] pub money_printer_video_cost: f64, #[serde(default)] pub digital_human_video_cost: f64, #[serde(default = "default_workflow_points_cost")] pub workflow_points_cost: f64, #[serde(default = "default_comfyui_docker_image")] pub docker_image: String, #[serde(default = "default_comfyui_container_name")] pub container_name: String, pub install_dir: Option, pub python_bin: String, pub port: u16, pub extra_args: Vec, pub max_restarts: u32, pub external_url: Option, pub base_url: Option, pub api_key: Option, pub agent_model: String, } #[derive(Debug, Deserialize, Clone)] pub struct PixelleVideoConfig { pub enabled: bool, pub api_base_url: Option, pub api_key: Option, #[serde(default = "default_pixelle_backend_provider")] pub backend_provider: String, pub frame_template: String, pub media_workflow: Option, pub tts_workflow: Option, pub runninghub_media_workflow: Option, pub runninghub_tts_workflow: Option, pub mode: String, pub n_scenes: i64, pub video_fps: i64, pub poll_interval_secs: u64, pub timeout_secs: u64, #[serde(default = "default_pixelle_config_path")] pub config_path: String, #[serde(default = "default_pixelle_compose_dir")] pub compose_dir: String, } /// Jimeng (Volcengine Visual / CV) async generation provider. /// /// Credentials are read from config or the `BOOSAPI_JIMENG__*` environment /// variables (e.g. `BOOSAPI_JIMENG__ACCESS_KEY`) and never hard-coded. The /// provider is gated behind `enabled`; when false the routes short-circuit /// with a "capability disabled" response and no upstream calls are made. #[derive(Debug, Deserialize, Clone)] pub struct JimengConfig { /// Master switch. Defaults to false until AK/SK are configured. #[serde(default)] pub enabled: bool, /// Volcengine access key id. #[serde(default)] pub access_key: String, /// Volcengine secret access key. #[serde(default)] pub secret_key: String, /// Optional STS session token (sent as `X-Security-Token` when present). #[serde(default)] pub session_token: Option, /// Signing region. #[serde(default = "default_jimeng_region")] pub region: String, /// API host. #[serde(default = "default_jimeng_host")] pub host: String, /// Poll interval for image tasks (seconds). #[serde(default = "default_jimeng_image_poll_interval_secs")] pub image_poll_interval_secs: u64, /// Overall timeout for image tasks (seconds). #[serde(default = "default_jimeng_image_timeout_secs")] pub image_timeout_secs: u64, /// Poll interval for video tasks (seconds). #[serde(default = "default_jimeng_video_poll_interval_secs")] pub video_poll_interval_secs: u64, /// Overall timeout for video tasks (seconds). #[serde(default = "default_jimeng_video_timeout_secs")] pub video_timeout_secs: u64, /// Cost charged per successful image generation. #[serde(default)] pub image_cost: f64, /// Cost charged per successful video generation. #[serde(default)] pub video_cost: f64, } #[derive(Debug, Deserialize, Clone)] pub struct PaymentConfig { pub enabled: bool, #[serde(default)] pub provider: PaymentProvider, pub public_base_url: Option, pub product_name_prefix: String, pub currency: String, pub min_amount_usd: f64, pub max_amount_usd: Option, pub daily_limit_usd: Option, pub order_timeout_minutes: i64, pub max_pending_orders: i64, pub stripe_secret_key: Option, pub stripe_webhook_secret: Option, pub comfy_customer_token: Option, pub comfy_customer_token_endpoint: Option, pub comfy_firebase_api_key: Option, pub comfy_oauth_client_id: Option, pub comfy_oauth_client_secret: Option, pub comfy_oauth_refresh_token: Option, pub comfy_customer_api_base_url: Option, pub comfy_checkout_endpoint: Option, pub success_path: String, pub cancel_path: String, /// When true, confirming a comfy_cloud recharge grants workflow points /// (amount_usd * usd_to_points_rate) instead of crediting the USD pool. #[serde(default = "default_comfy_recharge_to_points")] pub comfy_recharge_to_points: bool, /// Points granted per 1 USD when comfy_recharge_to_points is enabled. #[serde(default = "default_usd_to_points_rate")] pub usd_to_points_rate: f64, } #[derive(Debug, Deserialize, Clone, Default, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PaymentProvider { #[default] ComfyCloud, Stripe, } #[derive(Debug, Deserialize, Clone, Default)] #[serde(rename_all = "snake_case")] pub enum ComfyUIBackend { #[default] Boosapi, External, Cloud, Runninghub, Docker, } fn default_comfyui_docker_image() -> String { "boosapi-comfyui:multi-tenant".to_string() } fn default_comfyui_container_name() -> String { "boosapi-comfyui-workflow".to_string() } fn default_pixelle_backend_provider() -> String { "comfy".to_string() } fn default_pixelle_config_path() -> String { "/opt/Pixelle-Video/config.yaml".to_string() } fn default_pixelle_compose_dir() -> String { "/opt/Pixelle-Video".to_string() } fn default_jimeng_region() -> String { "cn-north-1".to_string() } fn default_jimeng_host() -> String { "visual.volcengineapi.com".to_string() } fn default_jimeng_image_poll_interval_secs() -> u64 { 2 } fn default_jimeng_image_timeout_secs() -> u64 { 120 } fn default_jimeng_video_poll_interval_secs() -> u64 { 8 } fn default_jimeng_video_timeout_secs() -> u64 { 600 } fn default_workflow_points_cost() -> f64 { 10.0 } fn default_comfy_recharge_to_points() -> bool { true } fn default_usd_to_points_rate() -> f64 { 100.0 } impl Default for ComfyUIConfig { fn default() -> Self { Self { enabled: false, backend: ComfyUIBackend::Boosapi, money_printer_video_cost: 0.0, digital_human_video_cost: 0.0, workflow_points_cost: 10.0, docker_image: default_comfyui_docker_image(), container_name: default_comfyui_container_name(), install_dir: None, python_bin: "python3".to_string(), port: 8188, extra_args: Vec::new(), max_restarts: 5, external_url: None, base_url: Some("http://127.0.0.1:8188".to_string()), api_key: None, agent_model: "gpt-4o".to_string(), } } } impl Default for PixelleVideoConfig { fn default() -> Self { Self { enabled: false, api_base_url: Some("http://127.0.0.1:8000".to_string()), api_key: None, backend_provider: default_pixelle_backend_provider(), frame_template: "1080x1920/image_default.html".to_string(), media_workflow: None, tts_workflow: None, runninghub_media_workflow: Some("runninghub/video_wan2.1_fusionx.json".to_string()), runninghub_tts_workflow: Some("runninghub/tts_edge.json".to_string()), mode: "fixed".to_string(), n_scenes: 5, video_fps: 30, poll_interval_secs: 5, timeout_secs: 900, config_path: default_pixelle_config_path(), compose_dir: default_pixelle_compose_dir(), } } } impl Default for PaymentConfig { fn default() -> Self { Self { enabled: false, provider: PaymentProvider::ComfyCloud, public_base_url: None, product_name_prefix: "BossAI 账户充值".to_string(), currency: "usd".to_string(), min_amount_usd: 1.0, max_amount_usd: Some(100_000.0), daily_limit_usd: None, order_timeout_minutes: 30, max_pending_orders: 3, stripe_secret_key: None, stripe_webhook_secret: None, comfy_customer_token: None, comfy_customer_token_endpoint: None, comfy_firebase_api_key: None, comfy_oauth_client_id: None, comfy_oauth_client_secret: None, comfy_oauth_refresh_token: None, comfy_customer_api_base_url: None, comfy_checkout_endpoint: None, success_path: "/user/recharge?payment=success&order={ORDER_ID}".to_string(), cancel_path: "/user/recharge?payment=cancel&order={ORDER_ID}".to_string(), comfy_recharge_to_points: true, usd_to_points_rate: 100.0, } } } impl Default for JimengConfig { fn default() -> Self { Self { enabled: false, access_key: String::new(), secret_key: String::new(), session_token: None, region: default_jimeng_region(), host: default_jimeng_host(), image_poll_interval_secs: default_jimeng_image_poll_interval_secs(), image_timeout_secs: default_jimeng_image_timeout_secs(), video_poll_interval_secs: default_jimeng_video_poll_interval_secs(), video_timeout_secs: default_jimeng_video_timeout_secs(), image_cost: 0.0, video_cost: 0.0, } } } impl Default for AppConfig { fn default() -> Self { Self { server: ServerConfig { host: "127.0.0.1".to_string(), port: 4000, log_level: "info".to_string(), cors_origins: vec!["*".to_string()], }, db: DbConfig { url: "sqlite::memory:".to_string(), max_connections: 1, ssl: false, runtime_db_url: None, runtime_db_dialect: None, runtime_db_ssl: None, }, auth: AuthConfig { jwt_secret: "test-secret".to_string(), jwt_expiry_hours: 72, auth_token: "test-admin-token".to_string(), proxy_token: "sk-test-proxy".to_string(), }, proxy: ProxyConfig { upstream_timeout_secs: 300, first_byte_timeout_secs: 60, max_retries: 3, proxy_log_retention_days: 30, template_proxy_url: None, }, comfyui: ComfyUIConfig::default(), pixelle_video: PixelleVideoConfig::default(), payment: PaymentConfig::default(), jimeng: JimengConfig::default(), } } } impl ComfyUIConfig { pub fn uses_native_backend(&self) -> bool { matches!( self.backend, ComfyUIBackend::External | ComfyUIBackend::Cloud | ComfyUIBackend::Runninghub ) } pub fn uses_cloud_backend(&self) -> bool { matches!(self.backend, ComfyUIBackend::Cloud) } pub fn uses_runninghub_backend(&self) -> bool { matches!(self.backend, ComfyUIBackend::Runninghub) } pub fn uses_docker_backend(&self) -> bool { matches!(self.backend, ComfyUIBackend::Docker) } pub fn target_base_url(&self) -> String { self.external_url .as_deref() .or(self.base_url.as_deref()) .map(str::trim) .filter(|url| !url.is_empty()) .unwrap_or("http://127.0.0.1:8188") .trim_end_matches('/') .to_string() } } impl AppConfig { pub fn load() -> anyhow::Result { Self::load_from_file("config.toml") } pub fn load_from_file(path: &str) -> anyhow::Result { dotenvy::dotenv().ok(); Ok(Figment::new() .merge(Toml::file(path)) .merge(Env::prefixed("BOOSAPI_").split("__")) .extract()?) } }