Files
Genarrative/server-rs/crates/api-server/src/config.rs
T
lhk229 38e195060b
Project CI / Repository checks (pull_request) Successful in 1m14s
Project CI / Frontend tests (pull_request) Successful in 2m52s
Project CI / Backend tests (pull_request) Successful in 3m54s
Project CI / Native shell tests (pull_request) Has been cancelled
合并 master 的付费生成预检与调试输出脱敏
合入 master 的编辑器付费生成目标预检与应用状态调试输出密钥泄漏两项修复。

SFX 直连与入队沿用 master 预检,源码顺序回归改为锁定本分支的 ElevenLabs worker 编排入口。

画布 Agent 音效参数恢复通用 null 缺省兼容,但保留 duration null 表示自动时长的 SFX V2 语义。

音效时长越界断言按 SFX V2 的 0.5-30 秒边界上移,重算契约测试期望。

哨兵测试补入 ElevenLabs base URL 与 API key,锁定其默认缺席于 AppConfig 调试输出。
2026-08-08 05:33:40 +00:00

2730 lines
114 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{env, fmt, fs, net::SocketAddr, path::PathBuf, time::Duration};
use platform_llm::{
DEFAULT_ARK_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_REQUEST_TIMEOUT_MS,
DEFAULT_RETRY_BACKOFF_MS, LlmProvider,
};
use platform_speech::{
DEFAULT_ASR_RESOURCE_ID, DEFAULT_ASR_WS_URL,
DEFAULT_REQUEST_TIMEOUT_MS as DEFAULT_SPEECH_REQUEST_TIMEOUT_MS,
DEFAULT_TTS_BIDIRECTION_WS_URL, DEFAULT_TTS_RESOURCE_ID, DEFAULT_TTS_SSE_URL,
};
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
const DEFAULT_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS: u64 = 2;
const DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS: u64 = 600;
const DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS: u64 = 900;
const DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS: u64 = 1_800;
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
pub(crate) const DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS: u64 = 180_000;
const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter";
const DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS: u64 = 5_000;
const BGFILTER_ATTEMPT_SAFETY_FACTOR: u64 = 2;
const BGFILTER_WORKER_RESPONSE_WINDOW_MS: u64 = 1_000;
const DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD: u32 = 3;
const DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS: u64 = 120;
const DEFAULT_ALIYUN_MATTING_ENDPOINT: &str = "imageseg.cn-shanghai.aliyuncs.com";
const DEFAULT_ALIYUN_MATTING_REQUEST_TIMEOUT_MS: u64 = 30_000;
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
#[derive(Clone)]
pub struct AppConfig {
pub bind_host: String,
pub bind_port: u16,
pub listen_backlog: i32,
pub worker_threads: Option<usize>,
pub process_role: ProcessRole,
pub bgfilter_worker_host: String,
pub bgfilter_worker_port: u16,
pub bgfilter_worker_base_url: String,
pub bgfilter_internal_token: Option<String>,
pub bgfilter_worker_concurrency: usize,
pub bgfilter_worker_max_requests: usize,
pub bgfilter_worker_connect_timeout_ms: u64,
pub external_generation_mode: ExternalGenerationMode,
pub external_generation_worker_id: String,
pub external_generation_worker_concurrency: usize,
pub external_generation_worker_poll_interval: Duration,
pub external_generation_worker_lease: Duration,
pub external_generation_worker_job_timeout: Duration,
pub external_generation_worker_long_job_timeout: Duration,
pub external_generation_controller_min_workers: usize,
pub external_generation_controller_max_workers: usize,
pub external_generation_controller_target_jobs_per_worker: usize,
pub external_generation_controller_poll_interval: Duration,
pub external_generation_controller_scale_down_idle_rounds: u32,
pub external_generation_controller_service_template: String,
pub external_generation_controller_dry_run: bool,
pub max_concurrent_requests: Option<usize>,
pub admin_max_concurrent_requests: Option<usize>,
pub shutdown_outbox_flush_timeout: Duration,
pub tracking_outbox_enabled: bool,
pub tracking_outbox_dir: PathBuf,
pub tracking_outbox_batch_size: usize,
pub tracking_outbox_flush_interval: Duration,
pub tracking_outbox_max_bytes: u64,
pub wallet_refund_outbox_enabled: bool,
pub wallet_refund_outbox_dir: PathBuf,
pub wallet_refund_outbox_batch_size: usize,
pub wallet_refund_outbox_flush_interval: Duration,
pub wallet_refund_outbox_max_bytes: u64,
pub editor_generation_pricing_override_path: PathBuf,
pub editor_bgfilter_base_url: String,
pub editor_bgfilter_token: Option<String>,
pub editor_bgfilter_single_image_estimate_ms: u64,
pub aliyun_matting_enabled: bool,
pub aliyun_matting_endpoint: String,
pub aliyun_matting_access_key_id: Option<String>,
pub aliyun_matting_access_key_secret: Option<String>,
pub aliyun_matting_request_timeout_ms: u64,
pub editor_bgfilter_circuit_failure_threshold: u32,
pub editor_bgfilter_circuit_cooldown: Duration,
pub image_editor_agent_sidebar_enabled: bool,
pub log_filter: String,
pub otel_enabled: bool,
pub admin_username: Option<String>,
pub admin_password: Option<String>,
pub admin_token_ttl_seconds: u64,
pub internal_api_secret: Option<String>,
pub jwt_issuer: String,
pub jwt_secret: String,
pub jwt_access_token_ttl_seconds: u64,
pub refresh_cookie_name: String,
pub refresh_cookie_path: String,
pub refresh_cookie_secure: bool,
pub refresh_cookie_same_site: String,
pub refresh_session_ttl_days: u32,
pub dev_password_entry_auto_register_enabled: bool,
pub sms_auth_enabled: bool,
pub sms_auth_provider: String,
pub sms_endpoint: String,
pub sms_access_key_id: Option<String>,
pub sms_access_key_secret: Option<String>,
pub sms_sign_name: String,
pub sms_template_code: String,
pub sms_template_param_key: String,
pub sms_country_code: String,
pub sms_scheme_name: Option<String>,
pub sms_code_length: u8,
pub sms_code_type: u8,
pub sms_valid_time_seconds: u64,
pub sms_interval_seconds: u64,
pub sms_duplicate_policy: u8,
pub sms_case_auth_policy: u8,
pub sms_return_verify_code: bool,
pub sms_mock_verify_code: String,
pub wechat_auth_enabled: bool,
pub wechat_auth_provider: String,
pub wechat_app_id: Option<String>,
pub wechat_app_secret: Option<String>,
pub wechat_mini_program_app_id: Option<String>,
pub wechat_mini_program_app_secret: Option<String>,
pub wechat_callback_path: String,
pub wechat_redirect_path: String,
pub wechat_authorize_endpoint: String,
pub wechat_access_token_endpoint: String,
pub wechat_user_info_endpoint: String,
pub wechat_js_code_session_endpoint: String,
pub wechat_stable_access_token_endpoint: String,
pub wechat_phone_number_endpoint: String,
pub wechat_state_ttl_minutes: u32,
pub wechat_mock_user_id: String,
pub wechat_mock_union_id: Option<String>,
pub wechat_mock_display_name: String,
pub wechat_mock_avatar_url: Option<String>,
pub wechat_pay_enabled: bool,
pub wechat_pay_provider: String,
pub wechat_pay_mch_id: Option<String>,
pub wechat_pay_merchant_serial_no: Option<String>,
pub wechat_pay_private_key_pem: Option<String>,
pub wechat_pay_private_key_path: Option<PathBuf>,
pub wechat_pay_platform_public_key_pem: Option<String>,
pub wechat_pay_platform_public_key_path: Option<PathBuf>,
pub wechat_pay_platform_serial_no: Option<String>,
pub wechat_pay_api_v3_key: Option<String>,
pub wechat_pay_notify_url: Option<String>,
pub wechat_pay_refund_reconciliation_enabled: bool,
pub wechat_pay_jsapi_endpoint: String,
pub wechat_mini_program_virtual_payment_offer_id: Option<String>,
pub wechat_mini_program_virtual_payment_app_key: Option<String>,
pub wechat_mini_program_virtual_payment_sandbox_app_key: Option<String>,
pub wechat_mini_program_virtual_payment_query_order_endpoint: String,
pub wechat_mini_program_virtual_payment_notify_provide_goods_endpoint: String,
pub wechat_mini_program_message_token: Option<String>,
pub wechat_mini_program_message_encoding_aes_key: Option<String>,
pub wechat_mini_program_virtual_payment_env: u8,
pub oss_bucket: Option<String>,
pub oss_endpoint: Option<String>,
pub oss_access_key_id: Option<String>,
pub oss_access_key_secret: Option<String>,
pub oss_read_expire_seconds: u64,
pub oss_post_expire_seconds: u64,
pub oss_post_max_size_bytes: u64,
pub oss_success_action_status: u16,
pub spacetime_server_url: String,
pub spacetime_database: String,
pub spacetime_token: Option<String>,
pub spacetime_runtime_service_bootstrap_secret: Option<String>,
pub spacetime_pool_size: u32,
pub spacetime_procedure_timeout: Duration,
pub spacetime_health_check_timeout: Duration,
pub llm_provider: LlmProvider,
pub llm_base_url: String,
pub llm_api_key: Option<String>,
pub llm_model: String,
pub llm_request_timeout_ms: u64,
pub llm_max_retries: u32,
pub llm_retry_backoff_ms: u64,
pub dashscope_base_url: String,
pub dashscope_api_key: Option<String>,
pub dashscope_scene_image_model: String,
pub dashscope_reference_image_model: String,
pub dashscope_cover_image_model: String,
pub dashscope_image_request_timeout_ms: u64,
// 中文注释:Apimart 已于 2026-06 弃用,LLM 文本调用统一迁移到 VectorEngine(同时支持 Chat Completions / Responses 协议)。
// pub apimart_base_url: String,
// pub apimart_api_key: Option<String>,
// pub apimart_image_request_timeout_ms: u64,
pub vector_engine_base_url: String,
pub vector_engine_api_key: Option<String>,
pub vector_engine_image_request_timeout_ms: u64,
pub vector_engine_audio_request_timeout_ms: u64,
pub elevenlabs_base_url: String,
pub elevenlabs_api_key: Option<String>,
pub elevenlabs_request_timeout_ms: u64,
pub hyper3d_base_url: String,
pub hyper3d_api_key: Option<String>,
pub hyper3d_model_request_timeout_ms: u64,
pub volcengine_speech_api_key: Option<String>,
pub volcengine_speech_app_id: Option<String>,
pub volcengine_speech_access_key: Option<String>,
pub volcengine_speech_asr_resource_id: String,
pub volcengine_speech_tts_resource_id: String,
pub volcengine_speech_asr_ws_url: String,
pub volcengine_speech_tts_bidirection_ws_url: String,
pub volcengine_speech_tts_sse_url: String,
pub volcengine_speech_request_timeout_ms: u64,
pub draft_asset_generation_max_concurrent_requests: usize,
pub ark_character_video_base_url: String,
pub ark_character_video_api_key: Option<String>,
pub ark_character_video_request_timeout_ms: u64,
pub ark_character_video_model: String,
pub character_animation_ffmpeg_path: String,
pub character_animation_ffprobe_path: String,
pub character_animation_frame_extract_timeout_ms: u64,
pub slow_request_threshold_ms: u64,
}
impl fmt::Debug for AppConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 配置由环境变量和私密文件聚合而来。这里只输出排障所需的封闭运行摘要,
// 不递归格式化任一字符串配置,避免 URL 凭据、Token、私钥或未来新增字段进入日志。
f.debug_struct("AppConfig")
.field("bind_port", &self.bind_port)
.field("listen_backlog", &self.listen_backlog)
.field("worker_threads", &self.worker_threads)
.field("process_role", &self.process_role)
.field("external_generation_mode", &self.external_generation_mode)
.field(
"external_generation_worker_concurrency",
&self.external_generation_worker_concurrency,
)
.field("max_concurrent_requests", &self.max_concurrent_requests)
.field(
"admin_max_concurrent_requests",
&self.admin_max_concurrent_requests,
)
.field("spacetime_pool_size", &self.spacetime_pool_size)
.field("sms_auth_enabled", &self.sms_auth_enabled)
.field("wechat_auth_enabled", &self.wechat_auth_enabled)
.field("wechat_pay_enabled", &self.wechat_pay_enabled)
.field("aliyun_matting_enabled", &self.aliyun_matting_enabled)
.field("tracking_outbox_enabled", &self.tracking_outbox_enabled)
.field(
"wallet_refund_outbox_enabled",
&self.wallet_refund_outbox_enabled,
)
.field("otel_enabled", &self.otel_enabled)
.field("credentials", &"<redacted>")
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessRole {
Api,
BgfilterWorker,
ExternalGenerationWorker,
ExternalGenerationController,
All,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExternalGenerationMode {
Inline,
Queue,
}
impl ExternalGenerationMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Inline => "inline",
Self::Queue => "queue",
}
}
pub fn is_inline(self) -> bool {
matches!(self, Self::Inline)
}
}
impl ProcessRole {
pub fn as_str(self) -> &'static str {
match self {
Self::Api => "api",
Self::BgfilterWorker => "bgfilter-worker",
Self::ExternalGenerationWorker => "external-generation-worker",
Self::ExternalGenerationController => "external-generation-controller",
Self::All => "all",
}
}
pub fn runs_http(self) -> bool {
matches!(self, Self::Api | Self::All)
}
pub fn runs_bgfilter_worker(self) -> bool {
matches!(self, Self::BgfilterWorker)
}
pub fn runs_external_generation_worker(self) -> bool {
matches!(self, Self::ExternalGenerationWorker | Self::All)
}
pub fn runs_external_generation_controller(self) -> bool {
matches!(self, Self::ExternalGenerationController)
}
}
impl Default for AppConfig {
fn default() -> Self {
Self {
bind_host: "127.0.0.1".to_string(),
bind_port: 3000,
listen_backlog: 1024,
worker_threads: None,
process_role: ProcessRole::Api,
bgfilter_worker_host: "127.0.0.1".to_string(),
bgfilter_worker_port: 8083,
bgfilter_worker_base_url: "http://127.0.0.1:8083".to_string(),
bgfilter_internal_token: None,
bgfilter_worker_concurrency: 16,
bgfilter_worker_max_requests: 2048,
bgfilter_worker_connect_timeout_ms: 2_000,
external_generation_mode: ExternalGenerationMode::Queue,
external_generation_worker_id: default_external_generation_worker_id(),
external_generation_worker_concurrency: 2,
external_generation_worker_poll_interval: Duration::from_millis(2_000),
external_generation_worker_lease: Duration::from_secs(
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS,
),
external_generation_worker_job_timeout: Duration::from_secs(
DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS,
),
external_generation_worker_long_job_timeout: Duration::from_secs(
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS,
),
external_generation_controller_min_workers: 1,
external_generation_controller_max_workers: 8,
external_generation_controller_target_jobs_per_worker: 2,
external_generation_controller_poll_interval: Duration::from_millis(10_000),
external_generation_controller_scale_down_idle_rounds: 6,
external_generation_controller_service_template:
"genarrative-external-generation-worker@{}.service".to_string(),
external_generation_controller_dry_run: false,
max_concurrent_requests: None,
admin_max_concurrent_requests: None,
shutdown_outbox_flush_timeout: Duration::from_millis(5_000),
tracking_outbox_enabled: true,
tracking_outbox_dir: PathBuf::from("server-rs/.data/tracking-outbox"),
tracking_outbox_batch_size: 500,
tracking_outbox_flush_interval: Duration::from_millis(1_000),
tracking_outbox_max_bytes: 256 * 1024 * 1024,
wallet_refund_outbox_enabled: true,
wallet_refund_outbox_dir: PathBuf::from("server-rs/.data/wallet-refund-outbox"),
wallet_refund_outbox_batch_size: 100,
wallet_refund_outbox_flush_interval: Duration::from_millis(1_000),
wallet_refund_outbox_max_bytes: 64 * 1024 * 1024,
editor_generation_pricing_override_path:
crate::editor_generation_config::default_editor_generation_pricing_override_path(),
editor_bgfilter_base_url: DEFAULT_EDITOR_BGFILTER_BASE_URL.to_string(),
editor_bgfilter_token: None,
editor_bgfilter_single_image_estimate_ms:
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS,
aliyun_matting_enabled: true,
aliyun_matting_endpoint: DEFAULT_ALIYUN_MATTING_ENDPOINT.to_string(),
aliyun_matting_access_key_id: None,
aliyun_matting_access_key_secret: None,
aliyun_matting_request_timeout_ms: DEFAULT_ALIYUN_MATTING_REQUEST_TIMEOUT_MS,
editor_bgfilter_circuit_failure_threshold:
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD,
editor_bgfilter_circuit_cooldown: Duration::from_secs(
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS,
),
image_editor_agent_sidebar_enabled: false,
log_filter: "info,tower_http=info".to_string(),
otel_enabled: false,
admin_username: None,
admin_password: None,
admin_token_ttl_seconds: 4 * 60 * 60,
internal_api_secret: Some(DEFAULT_INTERNAL_API_SECRET.to_string()),
jwt_issuer: "https://auth.genarrative.local".to_string(),
jwt_secret: "genarrative-dev-secret".to_string(),
jwt_access_token_ttl_seconds: 2 * 60 * 60,
refresh_cookie_name: "genarrative_refresh_session".to_string(),
refresh_cookie_path: "/api/auth".to_string(),
refresh_cookie_secure: false,
refresh_cookie_same_site: "Lax".to_string(),
refresh_session_ttl_days: 30,
dev_password_entry_auto_register_enabled: false,
sms_auth_enabled: false,
sms_auth_provider: "mock".to_string(),
sms_endpoint: "dysmsapi.aliyuncs.com".to_string(),
sms_access_key_id: None,
sms_access_key_secret: None,
sms_sign_name: "北京亓盒网络科技".to_string(),
sms_template_code: "SMS_506245486".to_string(),
sms_template_param_key: "code".to_string(),
sms_country_code: "86".to_string(),
sms_scheme_name: None,
sms_code_length: 6,
sms_code_type: 1,
sms_valid_time_seconds: 300,
sms_interval_seconds: 60,
sms_duplicate_policy: 1,
sms_case_auth_policy: 1,
sms_return_verify_code: false,
sms_mock_verify_code: "123456".to_string(),
wechat_auth_enabled: false,
wechat_auth_provider: "mock".to_string(),
wechat_app_id: None,
wechat_app_secret: None,
wechat_mini_program_app_id: None,
wechat_mini_program_app_secret: None,
wechat_callback_path: "/api/auth/wechat/callback".to_string(),
wechat_redirect_path: "/".to_string(),
wechat_authorize_endpoint: "https://open.weixin.qq.com/connect/qrconnect".to_string(),
wechat_access_token_endpoint: "https://api.weixin.qq.com/sns/oauth2/access_token"
.to_string(),
wechat_user_info_endpoint: "https://api.weixin.qq.com/sns/userinfo".to_string(),
wechat_js_code_session_endpoint: "https://api.weixin.qq.com/sns/jscode2session"
.to_string(),
wechat_stable_access_token_endpoint: "https://api.weixin.qq.com/cgi-bin/stable_token"
.to_string(),
wechat_phone_number_endpoint:
"https://api.weixin.qq.com/wxa/business/getuserphonenumber".to_string(),
wechat_state_ttl_minutes: 15,
wechat_mock_user_id: "wx-mock-user".to_string(),
wechat_mock_union_id: Some("wx-mock-union".to_string()),
wechat_mock_display_name: "微信旅人".to_string(),
wechat_mock_avatar_url: None,
wechat_pay_enabled: false,
wechat_pay_provider: "mock".to_string(),
wechat_pay_mch_id: None,
wechat_pay_merchant_serial_no: None,
wechat_pay_private_key_pem: None,
wechat_pay_private_key_path: None,
wechat_pay_platform_public_key_pem: None,
wechat_pay_platform_public_key_path: None,
wechat_pay_platform_serial_no: None,
wechat_pay_api_v3_key: None,
wechat_pay_notify_url: None,
wechat_pay_refund_reconciliation_enabled: false,
wechat_pay_jsapi_endpoint: "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
.to_string(),
wechat_mini_program_virtual_payment_offer_id: None,
wechat_mini_program_virtual_payment_app_key: None,
wechat_mini_program_virtual_payment_sandbox_app_key: None,
wechat_mini_program_virtual_payment_query_order_endpoint:
"https://api.weixin.qq.com/xpay/query_order".to_string(),
wechat_mini_program_virtual_payment_notify_provide_goods_endpoint:
"https://api.weixin.qq.com/xpay/notify_provide_goods".to_string(),
wechat_mini_program_message_token: None,
wechat_mini_program_message_encoding_aes_key: None,
wechat_mini_program_virtual_payment_env: 0,
oss_bucket: None,
oss_endpoint: None,
oss_access_key_id: None,
oss_access_key_secret: None,
oss_read_expire_seconds: 10 * 60,
oss_post_expire_seconds: 10 * 60,
oss_post_max_size_bytes: 20 * 1024 * 1024,
oss_success_action_status: 200,
spacetime_server_url: "http://127.0.0.1:3000".to_string(),
spacetime_database: "genarrative-dev".to_string(),
spacetime_token: None,
spacetime_runtime_service_bootstrap_secret: None,
spacetime_pool_size: 4,
spacetime_procedure_timeout: Duration::from_secs(30),
spacetime_health_check_timeout: Duration::from_secs(
DEFAULT_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS,
),
llm_provider: LlmProvider::Ark,
llm_base_url: String::new(),
llm_api_key: None,
llm_model: String::new(),
llm_request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS,
llm_max_retries: DEFAULT_MAX_RETRIES,
llm_retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
dashscope_base_url: "https://dashscope.aliyuncs.com/api/v1".to_string(),
dashscope_api_key: None,
dashscope_scene_image_model: String::new(),
dashscope_reference_image_model: String::new(),
dashscope_cover_image_model: String::new(),
dashscope_image_request_timeout_ms: 150_000,
// apimart_base_url: String::new(),
// apimart_api_key: None,
// apimart_image_request_timeout_ms: 180_000,
vector_engine_base_url: String::new(),
vector_engine_api_key: None,
vector_engine_image_request_timeout_ms: DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
vector_engine_audio_request_timeout_ms: 180_000,
elevenlabs_base_url: String::new(),
elevenlabs_api_key: None,
elevenlabs_request_timeout_ms: DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS,
hyper3d_base_url: "https://api.hyper3d.com/api/v2".to_string(),
hyper3d_api_key: None,
hyper3d_model_request_timeout_ms: 180_000,
volcengine_speech_api_key: None,
volcengine_speech_app_id: None,
volcengine_speech_access_key: None,
volcengine_speech_asr_resource_id: DEFAULT_ASR_RESOURCE_ID.to_string(),
volcengine_speech_tts_resource_id: DEFAULT_TTS_RESOURCE_ID.to_string(),
volcengine_speech_asr_ws_url: DEFAULT_ASR_WS_URL.to_string(),
volcengine_speech_tts_bidirection_ws_url: DEFAULT_TTS_BIDIRECTION_WS_URL.to_string(),
volcengine_speech_tts_sse_url: DEFAULT_TTS_SSE_URL.to_string(),
volcengine_speech_request_timeout_ms: DEFAULT_SPEECH_REQUEST_TIMEOUT_MS,
draft_asset_generation_max_concurrent_requests: 4,
ark_character_video_base_url: String::new(),
ark_character_video_api_key: None,
ark_character_video_request_timeout_ms: 420_000,
ark_character_video_model: String::new(),
character_animation_ffmpeg_path: "ffmpeg".to_string(),
character_animation_ffprobe_path: "ffprobe".to_string(),
character_animation_frame_extract_timeout_ms: 120_000,
slow_request_threshold_ms: 1_000,
}
}
}
impl AppConfig {
/// 单次 provider attempt 上限:`N × est × 2`。运行时派生,禁止硬编码计算结果;
/// worker 角色在 main 中覆盖 `bgfilter_worker_concurrency` 后自动生效。
pub fn bgfilter_provider_attempt_timeout_ms(&self) -> u64 {
(self.bgfilter_worker_concurrency.max(1) as u64)
.saturating_mul(self.editor_bgfilter_single_image_estimate_ms.max(1))
.saturating_mul(BGFILTER_ATTEMPT_SAFETY_FACTOR)
}
/// 调用预算:自取得 provider permit 起算,覆盖签名、两次顺序 attempt、
/// 结果校验与响应构造;排队时长不消耗它。
pub fn bgfilter_call_budget_ms(&self) -> u64 {
self.bgfilter_provider_attempt_timeout_ms()
.saturating_mul(2)
.saturating_add(BGFILTER_WORKER_RESPONSE_WINDOW_MS)
}
pub fn from_env() -> Self {
let mut config = Self::default();
if let Some(local_spacetime_database) = read_local_spacetime_database() {
config.spacetime_database = local_spacetime_database;
}
if let Ok(bind_host) = env::var("GENARRATIVE_API_HOST")
&& !bind_host.trim().is_empty()
{
config.bind_host = bind_host;
}
if let Ok(bind_port) = env::var("GENARRATIVE_API_PORT")
&& let Ok(parsed_port) = bind_port.parse::<u16>()
{
config.bind_port = parsed_port;
}
if let Some(host) = read_first_non_empty_env(&["GENARRATIVE_BGFILTER_WORKER_HOST"]) {
config.bgfilter_worker_host = host;
}
if let Some(port) = read_first_positive_u16_env(&["GENARRATIVE_BGFILTER_WORKER_PORT"]) {
config.bgfilter_worker_port = port;
}
if let Some(base_url) = read_first_non_empty_env(&["GENARRATIVE_BGFILTER_WORKER_BASE_URL"])
{
config.bgfilter_worker_base_url = base_url;
}
config.bgfilter_internal_token = read_secret_env_or_file(
&["GENARRATIVE_BGFILTER_INTERNAL_TOKEN"],
&["GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE"],
);
if let Some(concurrency) =
read_first_usize_env(&["GENARRATIVE_BGFILTER_WORKER_CONCURRENCY"])
{
config.bgfilter_worker_concurrency = concurrency;
}
if let Some(max_requests) =
read_first_usize_env(&["GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS"])
{
config.bgfilter_worker_max_requests = max_requests;
}
if let Some(connect_timeout_ms) =
read_first_positive_u64_env(&["GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS"])
{
config.bgfilter_worker_connect_timeout_ms = connect_timeout_ms;
}
if let Ok(log_filter) = env::var("GENARRATIVE_API_LOG")
&& !log_filter.trim().is_empty()
{
config.log_filter = log_filter;
}
if let Some(pricing_override_path) =
read_first_non_empty_env(&["GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH"])
{
config.editor_generation_pricing_override_path = PathBuf::from(pricing_override_path);
}
if let Some(base_url) = read_first_non_empty_env(&["GENARRATIVE_EDITOR_BGFILTER_BASE_URL"])
{
config.editor_bgfilter_base_url = base_url;
}
config.editor_bgfilter_token = read_first_non_empty_env(&[
"GENARRATIVE_EDITOR_BGFILTER_TOKEN",
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
]);
if let Some(estimate_ms) =
read_first_positive_u64_env(&["GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS"])
{
config.editor_bgfilter_single_image_estimate_ms = estimate_ms;
}
if let Some(enabled) = read_first_bool_env(&["GENARRATIVE_ALIYUN_MATTING_ENABLED"]) {
config.aliyun_matting_enabled = enabled;
}
if let Some(endpoint) = read_first_non_empty_env(&["GENARRATIVE_ALIYUN_MATTING_ENDPOINT"]) {
config.aliyun_matting_endpoint = endpoint;
}
config.aliyun_matting_access_key_id = read_first_non_empty_env(&[
"GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID",
"ALIBABA_CLOUD_ACCESS_KEY_ID",
]);
config.aliyun_matting_access_key_secret = read_first_non_empty_env(&[
"GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET",
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
]);
if let Some(timeout_ms) =
read_first_positive_u64_env(&["GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS"])
{
config.aliyun_matting_request_timeout_ms = timeout_ms;
}
if let Some(threshold) =
read_first_u32_env(&["GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD"])
{
config.editor_bgfilter_circuit_failure_threshold = threshold;
}
if let Some(cooldown_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS",
]) {
config.editor_bgfilter_circuit_cooldown = Duration::from_secs(cooldown_seconds.max(1));
}
if let Some(enabled) =
read_first_bool_env(&["GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR"])
{
config.image_editor_agent_sidebar_enabled = enabled;
}
if let Some(listen_backlog) =
read_first_positive_i32_env(&["GENARRATIVE_API_LISTEN_BACKLOG"])
{
config.listen_backlog = listen_backlog;
}
if let Some(worker_threads) = read_first_usize_env(&["GENARRATIVE_API_WORKER_THREADS"]) {
config.worker_threads = Some(worker_threads);
}
if let Some(process_role) = read_first_process_role_env(&["GENARRATIVE_PROCESS_ROLE"]) {
config.process_role = process_role;
}
if let Some(external_generation_mode) =
read_first_external_generation_mode_env(&["GENARRATIVE_EXTERNAL_GENERATION_MODE"])
{
config.external_generation_mode = external_generation_mode;
}
if let Some(worker_id) =
read_first_non_empty_env(&["GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID"])
{
config.external_generation_worker_id = worker_id;
}
if let Some(concurrency) =
read_first_usize_env(&["GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY"])
{
config.external_generation_worker_concurrency = concurrency.max(1);
}
if let Some(poll_interval_ms) = read_first_positive_u64_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS",
]) {
config.external_generation_worker_poll_interval =
Duration::from_millis(poll_interval_ms);
}
if let Some(lease_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS",
]) {
config.external_generation_worker_lease = Duration::from_secs(lease_seconds.max(1));
}
if let Some(timeout_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS",
]) {
config.external_generation_worker_job_timeout =
Duration::from_secs(timeout_seconds.max(1));
}
if let Some(timeout_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS",
]) {
config.external_generation_worker_long_job_timeout =
Duration::from_secs(timeout_seconds.max(1));
}
if let Some(min_workers) =
read_first_usize_env(&["GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_MIN_WORKERS"])
{
config.external_generation_controller_min_workers = min_workers;
}
if let Some(max_workers) =
read_first_usize_env(&["GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_MAX_WORKERS"])
{
config.external_generation_controller_max_workers = max_workers;
}
if config.external_generation_controller_max_workers
< config.external_generation_controller_min_workers
{
config.external_generation_controller_max_workers =
config.external_generation_controller_min_workers;
}
if let Some(target_jobs_per_worker) = read_first_usize_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_TARGET_JOBS_PER_WORKER",
]) {
config.external_generation_controller_target_jobs_per_worker =
target_jobs_per_worker.max(1);
}
if let Some(poll_interval_ms) = read_first_positive_u64_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_POLL_INTERVAL_MS",
]) {
config.external_generation_controller_poll_interval =
Duration::from_millis(poll_interval_ms);
}
if let Some(idle_rounds) = read_first_u32_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_SCALE_DOWN_IDLE_ROUNDS",
]) {
config.external_generation_controller_scale_down_idle_rounds = idle_rounds;
}
if let Some(service_template) = read_first_non_empty_env(&[
"GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_SERVICE_TEMPLATE",
]) {
config.external_generation_controller_service_template = service_template;
}
if let Some(dry_run) =
read_first_bool_env(&["GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_DRY_RUN"])
{
config.external_generation_controller_dry_run = dry_run;
}
if let Some(max_concurrent_requests) =
read_first_usize_env(&["GENARRATIVE_API_MAX_CONCURRENT_REQUESTS"])
{
config.max_concurrent_requests = Some(max_concurrent_requests);
}
if let Some(max_concurrent_requests) =
read_first_usize_env(&["GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS"])
{
config.admin_max_concurrent_requests = Some(max_concurrent_requests);
}
if let Some(timeout_ms) =
read_first_positive_u64_env(&["GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS"])
{
config.shutdown_outbox_flush_timeout = Duration::from_millis(timeout_ms);
}
if let Some(enabled) = read_first_bool_env(&["GENARRATIVE_TRACKING_OUTBOX_ENABLED"]) {
config.tracking_outbox_enabled = enabled;
}
if let Some(dir) = read_first_non_empty_env(&["GENARRATIVE_TRACKING_OUTBOX_DIR"]) {
config.tracking_outbox_dir = PathBuf::from(dir);
}
if let Some(batch_size) = read_first_usize_env(&["GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE"])
{
config.tracking_outbox_batch_size = batch_size;
}
if let Some(flush_interval_ms) =
read_first_positive_u64_env(&["GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS"])
{
config.tracking_outbox_flush_interval = Duration::from_millis(flush_interval_ms);
}
if let Some(max_bytes) =
read_first_positive_u64_env(&["GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES"])
{
config.tracking_outbox_max_bytes = max_bytes;
}
if let Some(enabled) = read_first_bool_env(&["GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED"]) {
config.wallet_refund_outbox_enabled = enabled;
}
if let Some(dir) = read_first_non_empty_env(&["GENARRATIVE_WALLET_REFUND_OUTBOX_DIR"]) {
config.wallet_refund_outbox_dir = PathBuf::from(dir);
}
if let Some(batch_size) =
read_first_usize_env(&["GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE"])
{
config.wallet_refund_outbox_batch_size = batch_size;
}
if let Some(flush_interval_ms) =
read_first_positive_u64_env(&["GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS"])
{
config.wallet_refund_outbox_flush_interval = Duration::from_millis(flush_interval_ms);
}
if let Some(max_bytes) =
read_first_positive_u64_env(&["GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES"])
{
config.wallet_refund_outbox_max_bytes = max_bytes;
}
if let Some(otel_enabled) = read_first_bool_env(&["GENARRATIVE_OTEL_ENABLED"]) {
config.otel_enabled = otel_enabled;
}
config.admin_username = read_first_non_empty_env(&["GENARRATIVE_ADMIN_USERNAME"]);
config.admin_password = read_first_non_empty_env(&["GENARRATIVE_ADMIN_PASSWORD"]);
if let Some(admin_token_ttl_seconds) =
read_first_duration_seconds_env(&["GENARRATIVE_ADMIN_TOKEN_TTL_SECONDS"])
{
config.admin_token_ttl_seconds = admin_token_ttl_seconds;
}
config.internal_api_secret = read_first_non_empty_env(&["GENARRATIVE_INTERNAL_API_SECRET"]);
if let Some(jwt_issuer) =
read_first_non_empty_env(&["GENARRATIVE_JWT_ISSUER", "JWT_ISSUER"])
{
config.jwt_issuer = jwt_issuer;
}
if let Some(jwt_secret) =
read_first_non_empty_env(&["GENARRATIVE_JWT_SECRET", "JWT_SECRET"])
{
config.jwt_secret = jwt_secret;
}
if let Some(ttl_seconds) = read_first_duration_seconds_env(&[
"GENARRATIVE_JWT_ACCESS_TOKEN_TTL_SECONDS",
"JWT_EXPIRES_IN",
]) {
config.jwt_access_token_ttl_seconds = ttl_seconds;
}
if let Some(refresh_cookie_name) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_NAME"]) {
config.refresh_cookie_name = refresh_cookie_name;
}
if let Some(refresh_cookie_path) = read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_PATH"]) {
config.refresh_cookie_path = refresh_cookie_path;
}
if let Some(refresh_cookie_same_site) =
read_first_non_empty_env(&["AUTH_REFRESH_COOKIE_SAME_SITE"])
{
config.refresh_cookie_same_site = refresh_cookie_same_site;
}
if let Some(refresh_cookie_secure) = read_first_bool_env(&["AUTH_REFRESH_COOKIE_SECURE"]) {
config.refresh_cookie_secure = refresh_cookie_secure;
}
if let Some(refresh_session_ttl_days) =
read_first_positive_u32_env(&["AUTH_REFRESH_SESSION_TTL_DAYS"])
{
config.refresh_session_ttl_days = refresh_session_ttl_days;
}
if let Some(enabled) =
read_first_bool_env(&["GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED"])
{
config.dev_password_entry_auto_register_enabled = enabled;
}
if let Some(sms_auth_enabled) = read_first_bool_env(&["SMS_AUTH_ENABLED"]) {
config.sms_auth_enabled = sms_auth_enabled;
}
if let Some(sms_auth_provider) = read_first_non_empty_env(&["SMS_AUTH_PROVIDER"]) {
config.sms_auth_provider = sms_auth_provider;
}
if let Some(sms_endpoint) = read_first_non_empty_env(&["ALIYUN_SMS_ENDPOINT"]) {
config.sms_endpoint = sms_endpoint;
}
config.sms_access_key_id = read_first_non_empty_env(&["ALIYUN_SMS_ACCESS_KEY_ID"]);
config.sms_access_key_secret = read_first_non_empty_env(&["ALIYUN_SMS_ACCESS_KEY_SECRET"]);
if let Some(sms_sign_name) = read_first_non_empty_env(&["ALIYUN_SMS_SIGN_NAME"]) {
config.sms_sign_name = sms_sign_name;
}
if let Some(sms_template_code) = read_first_non_empty_env(&["ALIYUN_SMS_TEMPLATE_CODE"]) {
config.sms_template_code = sms_template_code;
}
if let Some(sms_template_param_key) =
read_first_non_empty_env(&["ALIYUN_SMS_TEMPLATE_PARAM_KEY"])
{
config.sms_template_param_key = sms_template_param_key;
}
if let Some(sms_country_code) = read_first_non_empty_env(&["ALIYUN_SMS_COUNTRY_CODE"]) {
config.sms_country_code = sms_country_code;
}
config.sms_scheme_name = read_first_non_empty_env(&["ALIYUN_SMS_SCHEME_NAME"]);
if let Some(sms_code_length) = read_first_u8_env(&["ALIYUN_SMS_CODE_LENGTH"]) {
config.sms_code_length = sms_code_length;
}
if let Some(sms_code_type) = read_first_u8_env(&["ALIYUN_SMS_CODE_TYPE"]) {
config.sms_code_type = sms_code_type;
}
if let Some(sms_valid_time_seconds) =
read_first_duration_seconds_env(&["ALIYUN_SMS_VALID_TIME_SECONDS"])
{
config.sms_valid_time_seconds = sms_valid_time_seconds;
}
if let Some(sms_interval_seconds) =
read_first_duration_seconds_env(&["ALIYUN_SMS_INTERVAL_SECONDS"])
{
config.sms_interval_seconds = sms_interval_seconds;
}
if let Some(sms_duplicate_policy) = read_first_u8_env(&["ALIYUN_SMS_DUPLICATE_POLICY"]) {
config.sms_duplicate_policy = sms_duplicate_policy;
}
if let Some(sms_case_auth_policy) = read_first_u8_env(&["ALIYUN_SMS_CASE_AUTH_POLICY"]) {
config.sms_case_auth_policy = sms_case_auth_policy;
}
if let Some(sms_return_verify_code) =
read_first_bool_env(&["ALIYUN_SMS_RETURN_VERIFY_CODE"])
{
config.sms_return_verify_code = sms_return_verify_code;
}
if let Some(sms_mock_verify_code) = read_first_non_empty_env(&["SMS_AUTH_MOCK_VERIFY_CODE"])
{
config.sms_mock_verify_code = sms_mock_verify_code;
}
if let Some(wechat_auth_enabled) = read_first_bool_env(&["WECHAT_AUTH_ENABLED"]) {
config.wechat_auth_enabled = wechat_auth_enabled;
}
if let Some(wechat_auth_provider) = read_first_non_empty_env(&["WECHAT_AUTH_PROVIDER"]) {
config.wechat_auth_provider = wechat_auth_provider;
}
config.wechat_app_id = read_first_non_empty_env(&["WECHAT_APP_ID"]);
config.wechat_app_secret = read_first_non_empty_env(&["WECHAT_APP_SECRET"]);
config.wechat_mini_program_app_id =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_APP_ID", "WECHAT_APP_ID"]);
config.wechat_mini_program_app_secret =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_APP_SECRET", "WECHAT_APP_SECRET"]);
if let Some(wechat_callback_path) = read_first_non_empty_env(&["WECHAT_CALLBACK_PATH"]) {
config.wechat_callback_path = wechat_callback_path;
}
if let Some(wechat_redirect_path) = read_first_non_empty_env(&["WECHAT_REDIRECT_PATH"]) {
config.wechat_redirect_path = wechat_redirect_path;
}
if let Some(wechat_authorize_endpoint) =
read_first_non_empty_env(&["WECHAT_AUTHORIZE_ENDPOINT"])
{
config.wechat_authorize_endpoint = wechat_authorize_endpoint;
}
if let Some(wechat_access_token_endpoint) =
read_first_non_empty_env(&["WECHAT_ACCESS_TOKEN_ENDPOINT"])
{
config.wechat_access_token_endpoint = wechat_access_token_endpoint;
}
if let Some(wechat_user_info_endpoint) =
read_first_non_empty_env(&["WECHAT_USER_INFO_ENDPOINT"])
{
config.wechat_user_info_endpoint = wechat_user_info_endpoint;
}
if let Some(wechat_js_code_session_endpoint) =
read_first_non_empty_env(&["WECHAT_JS_CODE_SESSION_ENDPOINT"])
{
config.wechat_js_code_session_endpoint = wechat_js_code_session_endpoint;
}
if let Some(wechat_stable_access_token_endpoint) =
read_first_non_empty_env(&["WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT"])
{
config.wechat_stable_access_token_endpoint = wechat_stable_access_token_endpoint;
}
if let Some(wechat_phone_number_endpoint) =
read_first_non_empty_env(&["WECHAT_PHONE_NUMBER_ENDPOINT"])
{
config.wechat_phone_number_endpoint = wechat_phone_number_endpoint;
}
if let Some(wechat_state_ttl_minutes) =
read_first_positive_u32_env(&["WECHAT_STATE_TTL_MINUTES"])
{
config.wechat_state_ttl_minutes = wechat_state_ttl_minutes;
}
if let Some(wechat_mock_user_id) = read_first_non_empty_env(&["WECHAT_MOCK_USER_ID"]) {
config.wechat_mock_user_id = wechat_mock_user_id;
}
config.wechat_mock_union_id = read_first_non_empty_env(&["WECHAT_MOCK_UNION_ID"]);
if let Some(wechat_mock_display_name) =
read_first_non_empty_env(&["WECHAT_MOCK_DISPLAY_NAME"])
{
config.wechat_mock_display_name = wechat_mock_display_name;
}
config.wechat_mock_avatar_url = read_first_non_empty_env(&["WECHAT_MOCK_AVATAR_URL"]);
if let Some(wechat_pay_enabled) = read_first_bool_env(&["WECHAT_PAY_ENABLED"]) {
config.wechat_pay_enabled = wechat_pay_enabled;
}
if let Some(wechat_pay_provider) = read_first_non_empty_env(&["WECHAT_PAY_PROVIDER"]) {
config.wechat_pay_provider = wechat_pay_provider;
}
config.wechat_pay_mch_id = read_first_non_empty_env(&["WECHAT_PAY_MCH_ID"]);
config.wechat_pay_merchant_serial_no =
read_first_non_empty_env(&["WECHAT_PAY_MERCHANT_SERIAL_NO"]);
config.wechat_pay_private_key_pem =
read_first_non_empty_env(&["WECHAT_PAY_PRIVATE_KEY_PEM"]);
config.wechat_pay_private_key_path =
read_first_non_empty_env(&["WECHAT_PAY_PRIVATE_KEY_PATH"]).map(PathBuf::from);
config.wechat_pay_platform_public_key_pem =
read_first_non_empty_env(&["WECHAT_PAY_PLATFORM_PUBLIC_KEY_PEM"]);
config.wechat_pay_platform_public_key_path =
read_first_non_empty_env(&["WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH"]).map(PathBuf::from);
config.wechat_pay_platform_serial_no =
read_first_non_empty_env(&["WECHAT_PAY_PLATFORM_SERIAL_NO"]);
config.wechat_pay_api_v3_key = read_first_non_empty_env(&["WECHAT_PAY_API_V3_KEY"]);
config.wechat_pay_notify_url = read_first_non_empty_env(&["WECHAT_PAY_NOTIFY_URL"]);
if let Some(enabled) = read_first_bool_env(&["WECHAT_PAY_REFUND_RECONCILIATION_ENABLED"]) {
config.wechat_pay_refund_reconciliation_enabled = enabled;
}
if let Some(wechat_pay_jsapi_endpoint) =
read_first_non_empty_env(&["WECHAT_PAY_JSAPI_ENDPOINT"])
{
config.wechat_pay_jsapi_endpoint = wechat_pay_jsapi_endpoint;
}
config.wechat_mini_program_virtual_payment_offer_id =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID"]);
config.wechat_mini_program_virtual_payment_app_key =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY"]);
config.wechat_mini_program_virtual_payment_sandbox_app_key =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY"]);
if let Some(endpoint) =
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT"])
{
config.wechat_mini_program_virtual_payment_query_order_endpoint = endpoint;
}
if let Some(endpoint) = read_first_non_empty_env(&[
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT",
]) {
config.wechat_mini_program_virtual_payment_notify_provide_goods_endpoint = endpoint;
}
config.wechat_mini_program_message_token =
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_TOKEN"]);
config.wechat_mini_program_message_encoding_aes_key =
read_first_non_empty_env(&["WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY"]);
if let Some(env) = read_first_u8_env(&["WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV"])
&& env <= 1
{
config.wechat_mini_program_virtual_payment_env = env;
}
config.oss_bucket = read_first_non_empty_env(&["ALIYUN_OSS_BUCKET"]);
config.oss_endpoint = read_first_non_empty_env(&["ALIYUN_OSS_ENDPOINT"]);
config.oss_access_key_id = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_ID"]);
config.oss_access_key_secret = read_first_non_empty_env(&["ALIYUN_OSS_ACCESS_KEY_SECRET"]);
if let Some(oss_read_expire_seconds) =
read_first_duration_seconds_env(&["ALIYUN_OSS_READ_EXPIRE_SECONDS"])
{
config.oss_read_expire_seconds = oss_read_expire_seconds;
}
if let Some(oss_post_expire_seconds) =
read_first_duration_seconds_env(&["ALIYUN_OSS_POST_EXPIRE_SECONDS"])
{
config.oss_post_expire_seconds = oss_post_expire_seconds;
}
if let Some(oss_post_max_size_bytes) =
read_first_positive_u64_env(&["ALIYUN_OSS_POST_MAX_SIZE_BYTES"])
{
config.oss_post_max_size_bytes = oss_post_max_size_bytes;
}
if let Some(oss_success_action_status) =
read_first_positive_u16_env(&["ALIYUN_OSS_SUCCESS_ACTION_STATUS"])
{
config.oss_success_action_status = oss_success_action_status;
}
if let Some(spacetime_server_url) =
read_first_non_empty_env(&["GENARRATIVE_SPACETIME_SERVER_URL"])
{
config.spacetime_server_url = spacetime_server_url;
}
if let Some(spacetime_database) =
read_first_non_empty_env(&["GENARRATIVE_SPACETIME_DATABASE"])
{
config.spacetime_database = spacetime_database;
}
config.spacetime_token = read_first_non_empty_env(&["GENARRATIVE_SPACETIME_TOKEN"]);
config.spacetime_runtime_service_bootstrap_secret = read_secret_env_or_file(
&["GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET"],
&["GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE"],
);
if let Some(spacetime_pool_size) =
read_first_positive_u32_env(&["GENARRATIVE_SPACETIME_POOL_SIZE"])
{
config.spacetime_pool_size = spacetime_pool_size;
}
if let Some(spacetime_procedure_timeout_seconds) =
read_first_duration_seconds_env(&["GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS"])
{
config.spacetime_procedure_timeout =
Duration::from_secs(spacetime_procedure_timeout_seconds);
}
if let Some(spacetime_health_check_timeout_seconds) =
read_first_duration_seconds_env(&["GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS"])
{
config.spacetime_health_check_timeout =
Duration::from_secs(spacetime_health_check_timeout_seconds);
}
if let Some(llm_provider) =
read_first_llm_provider_env(&["GENARRATIVE_LLM_PROVIDER", "LLM_PROVIDER"])
{
config.llm_provider = llm_provider;
}
if let Some(llm_base_url) =
read_first_non_empty_env(&["GENARRATIVE_LLM_BASE_URL", "LLM_BASE_URL"])
{
config.llm_base_url = llm_base_url;
} else if config.llm_provider == LlmProvider::Ark {
config.llm_base_url = DEFAULT_ARK_BASE_URL.to_string();
}
config.llm_api_key = if config.llm_provider == LlmProvider::OpenAiCompatible {
read_first_non_empty_env(&[
"GENARRATIVE_LLM_API_KEY",
"LLM_API_KEY",
"VECTOR_ENGINE_API_KEY",
"ARK_API_KEY",
])
} else {
read_first_non_empty_env(&[
"GENARRATIVE_LLM_API_KEY",
"LLM_API_KEY",
"ARK_API_KEY",
"VECTOR_ENGINE_API_KEY",
])
};
if let Some(llm_model) =
read_first_non_empty_env(&["GENARRATIVE_LLM_MODEL", "LLM_MODEL", "VITE_LLM_MODEL"])
{
config.llm_model = llm_model;
}
if let Some(llm_request_timeout_ms) = read_first_positive_u64_env(&[
"GENARRATIVE_LLM_REQUEST_TIMEOUT_MS",
"LLM_REQUEST_TIMEOUT_MS",
]) {
config.llm_request_timeout_ms = llm_request_timeout_ms;
}
if let Some(llm_max_retries) =
read_first_u32_env(&["GENARRATIVE_LLM_MAX_RETRIES", "LLM_MAX_RETRIES"])
{
config.llm_max_retries = llm_max_retries;
}
if let Some(llm_retry_backoff_ms) =
read_first_u64_env(&["GENARRATIVE_LLM_RETRY_BACKOFF_MS", "LLM_RETRY_BACKOFF_MS"])
{
config.llm_retry_backoff_ms = llm_retry_backoff_ms;
}
if let Some(dashscope_base_url) = read_first_non_empty_env(&["DASHSCOPE_BASE_URL"]) {
config.dashscope_base_url = dashscope_base_url;
}
config.dashscope_api_key = read_first_non_empty_env(&["DASHSCOPE_API_KEY"]);
if let Some(dashscope_scene_image_model) =
read_first_non_empty_env(&["DASHSCOPE_SCENE_IMAGE_MODEL", "DASHSCOPE_IMAGE_MODEL"])
{
config.dashscope_scene_image_model = dashscope_scene_image_model;
}
if let Some(dashscope_reference_image_model) = read_first_non_empty_env(&[
"DASHSCOPE_REFERENCE_IMAGE_MODEL",
"DASHSCOPE_IMAGE_EDIT_MODEL",
]) {
config.dashscope_reference_image_model = dashscope_reference_image_model;
}
if let Some(dashscope_cover_image_model) =
read_first_non_empty_env(&["DASHSCOPE_COVER_IMAGE_MODEL", "DASHSCOPE_IMAGE_MODEL"])
{
config.dashscope_cover_image_model = dashscope_cover_image_model;
}
if let Some(dashscope_image_request_timeout_ms) =
read_first_positive_u64_env(&["DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS"])
{
config.dashscope_image_request_timeout_ms = dashscope_image_request_timeout_ms;
}
// 中文注释:Apimart 已于 2026-06 弃用,LLM 文本调用统一迁移到 VectorEngine。
// 保留以下历史加载代码,后续删除:
// if let Some(apimart_base_url) = read_first_non_empty_env(&["APIMART_BASE_URL"]) {
// config.apimart_base_url = apimart_base_url;
// }
// config.apimart_api_key = read_first_non_empty_env(&["APIMART_API_KEY"]);
// if let Some(apimart_image_request_timeout_ms) =
// read_first_positive_u64_env(&["APIMART_IMAGE_REQUEST_TIMEOUT_MS"])
// {
// config.apimart_image_request_timeout_ms = apimart_image_request_timeout_ms;
// }
if let Some(vector_engine_base_url) = read_first_non_empty_env(&["VECTOR_ENGINE_BASE_URL"])
{
config.vector_engine_base_url = vector_engine_base_url;
}
config.vector_engine_api_key = read_first_non_empty_env(&["VECTOR_ENGINE_API_KEY"]);
if let Some(vector_engine_image_request_timeout_ms) =
read_first_positive_u64_env(&["VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"])
{
// 单次 attempt 上限允许按环境收短;worker 调用还会受整次任务的绝对 deadline 约束。
config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms;
}
if let Some(vector_engine_audio_request_timeout_ms) =
read_first_positive_u64_env(&["VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS"])
{
config.vector_engine_audio_request_timeout_ms = vector_engine_audio_request_timeout_ms;
}
if let Some(elevenlabs_base_url) = read_first_non_empty_env(&["ELEVENLABS_BASE_URL"]) {
config.elevenlabs_base_url = elevenlabs_base_url;
}
config.elevenlabs_api_key = read_first_non_empty_env(&["ELEVENLABS_API_KEY"]);
if let Some(elevenlabs_request_timeout_ms) =
read_first_positive_u64_env(&["ELEVENLABS_REQUEST_TIMEOUT_MS"])
{
config.elevenlabs_request_timeout_ms = elevenlabs_request_timeout_ms;
}
if let Some(hyper3d_base_url) =
read_first_non_empty_env(&["HYPER3D_BASE_URL", "RODIN_BASE_URL"])
{
config.hyper3d_base_url = hyper3d_base_url;
}
config.hyper3d_api_key = read_first_non_empty_env(&["HYPER3D_API_KEY", "RODIN_API_KEY"]);
if let Some(hyper3d_model_request_timeout_ms) = read_first_positive_u64_env(&[
"HYPER3D_MODEL_REQUEST_TIMEOUT_MS",
"RODIN_MODEL_REQUEST_TIMEOUT_MS",
]) {
config.hyper3d_model_request_timeout_ms = hyper3d_model_request_timeout_ms;
}
config.volcengine_speech_api_key =
read_first_non_empty_env(&["VOLCENGINE_SPEECH_API_KEY", "VOLCENGINE_API_KEY"]);
config.volcengine_speech_app_id =
read_first_non_empty_env(&["VOLCENGINE_SPEECH_APP_ID", "VOLCENGINE_ACCESS_KEY_ID"]);
config.volcengine_speech_access_key = read_first_non_empty_env(&[
"VOLCENGINE_SPEECH_ACCESS_KEY",
"VOLCENGINE_SECRET_ACCESS_KEY",
]);
if let Some(asr_resource_id) =
read_first_non_empty_env(&["VOLCENGINE_SPEECH_ASR_RESOURCE_ID"])
{
config.volcengine_speech_asr_resource_id = asr_resource_id;
}
if let Some(tts_resource_id) =
read_first_non_empty_env(&["VOLCENGINE_SPEECH_TTS_RESOURCE_ID"])
{
config.volcengine_speech_tts_resource_id = tts_resource_id;
}
if let Some(asr_ws_url) = read_first_non_empty_env(&["VOLCENGINE_SPEECH_ASR_WS_URL"]) {
config.volcengine_speech_asr_ws_url = asr_ws_url;
}
if let Some(tts_bidirection_ws_url) =
read_first_non_empty_env(&["VOLCENGINE_SPEECH_TTS_BIDIRECTION_WS_URL"])
{
config.volcengine_speech_tts_bidirection_ws_url = tts_bidirection_ws_url;
}
if let Some(tts_sse_url) = read_first_non_empty_env(&["VOLCENGINE_SPEECH_TTS_SSE_URL"]) {
config.volcengine_speech_tts_sse_url = tts_sse_url;
}
if let Some(request_timeout_ms) =
read_first_positive_u64_env(&["VOLCENGINE_SPEECH_REQUEST_TIMEOUT_MS"])
{
config.volcengine_speech_request_timeout_ms = request_timeout_ms;
}
if let Some(max_concurrent_requests) = read_first_usize_env(&[
"GENARRATIVE_DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS",
"DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS",
]) {
config.draft_asset_generation_max_concurrent_requests = max_concurrent_requests;
}
if let Some(ark_character_video_base_url) = read_first_non_empty_env(&[
"ARK_CHARACTER_VIDEO_BASE_URL",
"ARK_BASE_URL",
"GENARRATIVE_LLM_BASE_URL",
"LLM_BASE_URL",
]) {
config.ark_character_video_base_url = ark_character_video_base_url;
} else if config.llm_provider == LlmProvider::Ark {
config.ark_character_video_base_url = DEFAULT_ARK_BASE_URL.to_string();
}
config.ark_character_video_api_key = read_first_non_empty_env(&[
"ARK_CHARACTER_VIDEO_API_KEY",
"ARK_API_KEY",
"GENARRATIVE_LLM_API_KEY",
"LLM_API_KEY",
]);
if let Some(ark_character_video_request_timeout_ms) = read_first_positive_u64_env(&[
"ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS",
"DASHSCOPE_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS",
]) {
config.ark_character_video_request_timeout_ms = ark_character_video_request_timeout_ms;
}
if let Some(ark_character_video_model) = read_first_non_empty_env(&[
"ARK_CHARACTER_VIDEO_MODEL",
"DASHSCOPE_CHARACTER_VIDEO_MODEL",
]) {
config.ark_character_video_model = ark_character_video_model;
}
if let Some(character_animation_ffmpeg_path) = read_first_non_empty_env(&[
"GENARRATIVE_CHARACTER_ANIMATION_FFMPEG_PATH",
"CHARACTER_ANIMATION_FFMPEG_PATH",
]) {
config.character_animation_ffmpeg_path = character_animation_ffmpeg_path;
}
if let Some(character_animation_ffprobe_path) = read_first_non_empty_env(&[
"GENARRATIVE_CHARACTER_ANIMATION_FFPROBE_PATH",
"CHARACTER_ANIMATION_FFPROBE_PATH",
]) {
config.character_animation_ffprobe_path = character_animation_ffprobe_path;
}
if let Some(character_animation_frame_extract_timeout_ms) =
read_first_positive_u64_env(&["CHARACTER_ANIMATION_FRAME_EXTRACT_TIMEOUT_MS"])
{
config.character_animation_frame_extract_timeout_ms =
character_animation_frame_extract_timeout_ms;
}
if let Some(slow_request_threshold_ms) =
read_first_positive_u64_env(&["GENARRATIVE_SLOW_REQUEST_THRESHOLD_MS"])
{
config.slow_request_threshold_ms = slow_request_threshold_ms;
}
config
}
pub fn bind_socket_addr(&self) -> SocketAddr {
let address = format!("{}:{}", self.bind_host, self.bind_port);
address
.parse()
.unwrap_or_else(|_| SocketAddr::from(([127, 0, 0, 1], 3000)))
}
}
fn read_first_non_empty_env(keys: &[&str]) -> Option<String> {
keys.iter().find_map(|key| {
env::var(key).ok().and_then(|value| {
let value = value.trim().to_string();
if value.is_empty() {
return None;
}
Some(value)
})
})
}
fn read_secret_env_or_file(value_keys: &[&str], file_keys: &[&str]) -> Option<String> {
read_first_non_empty_env(value_keys).or_else(|| {
let path = read_first_non_empty_env(file_keys)?;
let value = fs::read_to_string(path).ok()?;
let value = value.trim().to_string();
(!value.is_empty()).then_some(value)
})
}
fn read_local_spacetime_database() -> Option<String> {
let config_path = find_upward_file(SPACETIME_LOCAL_CONFIG_FILE)?;
let raw_text = fs::read_to_string(config_path).ok()?;
let parsed = serde_json::from_str::<serde_json::Value>(&raw_text).ok()?;
parsed
.get("database")
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn find_upward_file(file_name: &str) -> Option<PathBuf> {
let mut current_dir = env::current_dir().ok()?;
loop {
let candidate = current_dir.join(file_name);
if candidate.is_file() {
return Some(candidate);
}
if !current_dir.pop() {
return None;
}
}
}
fn read_first_duration_seconds_env(keys: &[&str]) -> Option<u64> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_duration_seconds(&value))
})
}
fn read_first_bool_env(keys: &[&str]) -> Option<bool> {
keys.iter()
.find_map(|key| env::var(key).ok().and_then(|value| parse_bool(&value)))
}
fn read_first_llm_provider_env(keys: &[&str]) -> Option<LlmProvider> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_llm_provider(&value))
})
}
fn read_first_process_role_env(keys: &[&str]) -> Option<ProcessRole> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_process_role(&value))
})
}
fn read_first_external_generation_mode_env(keys: &[&str]) -> Option<ExternalGenerationMode> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_external_generation_mode(&value))
})
}
fn read_first_positive_u32_env(keys: &[&str]) -> Option<u32> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_positive_u32(&value))
})
}
fn read_first_positive_i32_env(keys: &[&str]) -> Option<i32> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_positive_i32(&value))
})
}
fn read_first_positive_u64_env(keys: &[&str]) -> Option<u64> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_positive_u64(&value))
})
}
fn read_first_u32_env(keys: &[&str]) -> Option<u32> {
keys.iter()
.find_map(|key| env::var(key).ok().and_then(|value| parse_u32(&value)))
}
fn read_first_u64_env(keys: &[&str]) -> Option<u64> {
keys.iter()
.find_map(|key| env::var(key).ok().and_then(|value| parse_u64(&value)))
}
fn read_first_usize_env(keys: &[&str]) -> Option<usize> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_positive_usize(&value))
})
}
fn read_first_u8_env(keys: &[&str]) -> Option<u8> {
keys.iter()
.find_map(|key| env::var(key).ok().and_then(|value| parse_u8(&value)))
}
fn default_external_generation_worker_id() -> String {
let host = env::var("HOSTNAME")
.or_else(|_| env::var("COMPUTERNAME"))
.unwrap_or_else(|_| "local".to_string());
format!("{}-{}", host.trim(), std::process::id())
}
fn parse_process_role(value: &str) -> Option<ProcessRole> {
match trim_quoted_env_value(value).to_ascii_lowercase().as_str() {
"api" => Some(ProcessRole::Api),
"bgfilter-worker" | "bgfilter_worker" => Some(ProcessRole::BgfilterWorker),
"external-generation-worker" | "external_generation_worker" | "worker" => {
Some(ProcessRole::ExternalGenerationWorker)
}
"external-generation-controller" | "external_generation_controller" | "controller" => {
Some(ProcessRole::ExternalGenerationController)
}
"all" => Some(ProcessRole::All),
_ => None,
}
}
fn parse_external_generation_mode(value: &str) -> Option<ExternalGenerationMode> {
match trim_quoted_env_value(value).to_ascii_lowercase().as_str() {
"inline" | "sync" | "synchronous" => Some(ExternalGenerationMode::Inline),
"queue" | "queued" | "worker" | "async" | "asynchronous" => {
Some(ExternalGenerationMode::Queue)
}
_ => None,
}
}
fn trim_quoted_env_value(raw: &str) -> &str {
let raw = raw.trim();
raw.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| {
raw.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
})
.unwrap_or(raw)
.trim()
}
fn read_first_positive_u16_env(keys: &[&str]) -> Option<u16> {
keys.iter().find_map(|key| {
env::var(key)
.ok()
.and_then(|value| parse_positive_u16(&value))
})
}
fn parse_duration_seconds(raw: &str) -> Option<u64> {
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if let Ok(seconds) = raw.parse::<u64>() {
return Some(seconds);
}
let (number, unit) = raw.split_at(raw.len().checked_sub(1)?);
let unit = unit.to_ascii_lowercase();
let number = number.trim().parse::<u64>().ok()?;
let multiplier = match unit.as_str() {
"s" => 1,
"m" => 60,
"h" => 60 * 60,
"d" => 24 * 60 * 60,
_ => return None,
};
number.checked_mul(multiplier)
}
fn parse_bool(raw: &str) -> Option<bool> {
let raw = raw.trim();
let raw = raw
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| {
raw.strip_prefix('\'')
.and_then(|value| value.strip_suffix('\''))
})
.unwrap_or(raw);
match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
fn parse_llm_provider(raw: &str) -> Option<LlmProvider> {
match raw.trim().to_ascii_lowercase().as_str() {
"ark" => Some(LlmProvider::Ark),
"dash_scope" | "dashscope" => Some(LlmProvider::DashScope),
"openai_compatible" | "openai-compatible" | "openai" => Some(LlmProvider::OpenAiCompatible),
_ => None,
}
}
fn parse_positive_u32(raw: &str) -> Option<u32> {
let value = raw.trim().parse::<u32>().ok()?;
if value == 0 {
return None;
}
Some(value)
}
fn parse_positive_i32(raw: &str) -> Option<i32> {
let value = raw.trim().parse::<i32>().ok()?;
if value <= 0 {
return None;
}
Some(value)
}
fn parse_u32(raw: &str) -> Option<u32> {
raw.trim().parse::<u32>().ok()
}
fn parse_positive_u64(raw: &str) -> Option<u64> {
let value = raw.trim().parse::<u64>().ok()?;
if value == 0 {
return None;
}
Some(value)
}
fn parse_u64(raw: &str) -> Option<u64> {
raw.trim().parse::<u64>().ok()
}
fn parse_positive_usize(raw: &str) -> Option<usize> {
let value = raw.trim().parse::<usize>().ok()?;
if value == 0 {
return None;
}
Some(value)
}
fn parse_u8(raw: &str) -> Option<u8> {
raw.trim().parse::<u8>().ok()
}
fn parse_positive_u16(raw: &str) -> Option<u16> {
let value = raw.trim().parse::<u16>().ok()?;
if value == 0 {
return None;
}
Some(value)
}
#[cfg(test)]
mod tests {
use super::{
AppConfig, DEFAULT_EDITOR_BGFILTER_BASE_URL,
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS,
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD,
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS, DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS,
DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS,
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS,
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS, ExternalGenerationMode,
LlmProvider, ProcessRole, parse_bool, parse_external_generation_mode, parse_process_role,
};
use std::{
fs,
sync::{Mutex, OnceLock},
};
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
#[test]
fn default_keeps_non_public_model_and_base_url_empty() {
let config = AppConfig::default();
assert_eq!(config.bgfilter_worker_host, "127.0.0.1");
assert_eq!(config.bgfilter_worker_port, 8083);
assert_eq!(config.bgfilter_worker_base_url, "http://127.0.0.1:8083");
assert!(config.bgfilter_internal_token.is_none());
assert_eq!(config.bgfilter_worker_concurrency, 16);
assert_eq!(config.bgfilter_worker_max_requests, 2048);
assert_eq!(config.bgfilter_worker_connect_timeout_ms, 2_000);
assert!(config.llm_model.is_empty());
assert!(config.llm_base_url.is_empty());
// assert!(config.apimart_base_url.is_empty());
assert!(config.vector_engine_base_url.is_empty());
assert_eq!(
config.editor_bgfilter_base_url,
DEFAULT_EDITOR_BGFILTER_BASE_URL
);
assert_eq!(
config.editor_bgfilter_single_image_estimate_ms,
DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS
);
assert_eq!(config.bgfilter_provider_attempt_timeout_ms(), 160_000);
assert_eq!(config.bgfilter_call_budget_ms(), 321_000);
assert_eq!(
config.editor_bgfilter_circuit_failure_threshold,
DEFAULT_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD
);
assert_eq!(
config.editor_bgfilter_circuit_cooldown.as_secs(),
DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS
);
assert_eq!(DEFAULT_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS, 120);
assert!(config.editor_bgfilter_token.is_none());
assert_eq!(
config.external_generation_worker_lease.as_secs(),
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS
);
assert_eq!(
config.external_generation_worker_job_timeout.as_secs(),
DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS
);
assert_eq!(
config.external_generation_worker_long_job_timeout.as_secs(),
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS
);
assert!(config.ark_character_video_base_url.is_empty());
assert_eq!(config.hyper3d_base_url, "https://api.hyper3d.com/api/v2");
assert!(config.ark_character_video_model.is_empty());
assert!(config.dashscope_scene_image_model.is_empty());
assert!(config.dashscope_reference_image_model.is_empty());
assert!(config.dashscope_cover_image_model.is_empty());
assert_eq!(
config.dashscope_base_url,
"https://dashscope.aliyuncs.com/api/v1"
);
assert_eq!(config.sms_endpoint, "dysmsapi.aliyuncs.com");
assert_eq!(config.sms_sign_name, "北京亓盒网络科技");
assert_eq!(config.sms_template_code, "SMS_506245486");
assert_eq!(config.sms_template_param_key, "code");
assert_eq!(
config.wechat_authorize_endpoint,
"https://open.weixin.qq.com/connect/qrconnect"
);
}
#[test]
fn parse_bool_accepts_wrapped_quotes_from_shell_env() {
assert_eq!(parse_bool("\"true\""), Some(true));
assert_eq!(parse_bool("'true'"), Some(true));
assert_eq!(parse_bool("\"false\""), Some(false));
assert_eq!(parse_bool("'off'"), Some(false));
}
#[test]
fn process_role_controls_http_and_external_generation_worker_roles() {
assert_eq!(parse_process_role("api"), Some(ProcessRole::Api));
assert_eq!(
parse_process_role("bgfilter-worker"),
Some(ProcessRole::BgfilterWorker)
);
assert_eq!(
parse_process_role("'bgfilter_worker'"),
Some(ProcessRole::BgfilterWorker)
);
assert_eq!(
parse_process_role("\"external-generation-worker\""),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(
parse_process_role("'external_generation_worker'"),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(
parse_process_role("worker"),
Some(ProcessRole::ExternalGenerationWorker)
);
assert_eq!(
parse_process_role("controller"),
Some(ProcessRole::ExternalGenerationController)
);
assert_eq!(
parse_process_role("'external_generation_controller'"),
Some(ProcessRole::ExternalGenerationController)
);
assert_eq!(parse_process_role("all"), Some(ProcessRole::All));
assert_eq!(parse_process_role("unknown"), None);
assert_eq!(ProcessRole::BgfilterWorker.as_str(), "bgfilter-worker");
assert!(ProcessRole::Api.runs_http());
assert!(!ProcessRole::Api.runs_bgfilter_worker());
assert!(!ProcessRole::Api.runs_external_generation_worker());
assert!(!ProcessRole::Api.runs_external_generation_controller());
assert!(!ProcessRole::BgfilterWorker.runs_http());
assert!(ProcessRole::BgfilterWorker.runs_bgfilter_worker());
assert!(!ProcessRole::BgfilterWorker.runs_external_generation_worker());
assert!(!ProcessRole::BgfilterWorker.runs_external_generation_controller());
assert!(!ProcessRole::ExternalGenerationWorker.runs_http());
assert!(!ProcessRole::ExternalGenerationWorker.runs_bgfilter_worker());
assert!(ProcessRole::ExternalGenerationWorker.runs_external_generation_worker());
assert!(!ProcessRole::ExternalGenerationWorker.runs_external_generation_controller());
assert!(!ProcessRole::ExternalGenerationController.runs_http());
assert!(!ProcessRole::ExternalGenerationController.runs_bgfilter_worker());
assert!(!ProcessRole::ExternalGenerationController.runs_external_generation_worker());
assert!(ProcessRole::ExternalGenerationController.runs_external_generation_controller());
assert!(ProcessRole::All.runs_http());
assert!(!ProcessRole::All.runs_bgfilter_worker());
assert!(ProcessRole::All.runs_external_generation_worker());
assert!(!ProcessRole::All.runs_external_generation_controller());
}
#[test]
fn external_generation_mode_parses_inline_and_queue_aliases() {
assert_eq!(
parse_external_generation_mode("inline"),
Some(ExternalGenerationMode::Inline)
);
assert_eq!(
parse_external_generation_mode("'sync'"),
Some(ExternalGenerationMode::Inline)
);
assert_eq!(
parse_external_generation_mode("\"queue\""),
Some(ExternalGenerationMode::Queue)
);
assert_eq!(
parse_external_generation_mode("worker"),
Some(ExternalGenerationMode::Queue)
);
assert_eq!(parse_external_generation_mode("unknown"), None);
assert!(ExternalGenerationMode::Inline.is_inline());
assert!(!ExternalGenerationMode::Queue.is_inline());
}
#[test]
fn from_env_reads_external_generation_mode() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock");
unsafe {
std::env::set_var("GENARRATIVE_EXTERNAL_GENERATION_MODE", "inline");
}
let config = AppConfig::from_env();
assert_eq!(
config.external_generation_mode,
ExternalGenerationMode::Inline
);
unsafe {
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_MODE");
}
}
#[test]
fn from_env_reads_sms_enabled_when_shell_value_keeps_quotes() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("SMS_AUTH_ENABLED");
std::env::set_var("SMS_AUTH_ENABLED", "\"true\"");
}
let config = AppConfig::from_env();
assert!(config.sms_auth_enabled);
unsafe {
std::env::remove_var("SMS_AUTH_ENABLED");
}
}
#[test]
fn default_elevenlabs_settings_fail_closed_with_a_bounded_timeout() {
let config = AppConfig::default();
assert!(config.elevenlabs_base_url.is_empty());
assert!(config.elevenlabs_api_key.is_none());
assert_eq!(
config.elevenlabs_request_timeout_ms,
DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS
);
}
#[test]
fn from_env_reads_elevenlabs_settings() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("ELEVENLABS_BASE_URL");
std::env::remove_var("ELEVENLABS_API_KEY");
std::env::remove_var("ELEVENLABS_REQUEST_TIMEOUT_MS");
std::env::set_var(
"ELEVENLABS_BASE_URL",
"https://elevenlabs.internal.example/v1",
);
std::env::set_var("ELEVENLABS_API_KEY", "elevenlabs-test-key");
std::env::set_var("ELEVENLABS_REQUEST_TIMEOUT_MS", "190000");
}
let config = AppConfig::from_env();
assert_eq!(
config.elevenlabs_base_url,
"https://elevenlabs.internal.example/v1"
);
assert_eq!(
config.elevenlabs_api_key.as_deref(),
Some("elevenlabs-test-key")
);
assert_eq!(config.elevenlabs_request_timeout_ms, 190_000);
unsafe {
std::env::remove_var("ELEVENLABS_BASE_URL");
std::env::remove_var("ELEVENLABS_API_KEY");
std::env::remove_var("ELEVENLABS_REQUEST_TIMEOUT_MS");
}
}
#[test]
fn from_env_reads_non_public_models_and_urls() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
std::env::remove_var("GENARRATIVE_LLM_API_KEY");
std::env::remove_var("LLM_API_KEY");
std::env::remove_var("ARK_API_KEY");
std::env::remove_var("GENARRATIVE_LLM_MODEL");
std::env::remove_var("APIMART_BASE_URL");
std::env::remove_var("APIMART_IMAGE_REQUEST_TIMEOUT_MS");
std::env::remove_var("VECTOR_ENGINE_BASE_URL");
std::env::remove_var("VECTOR_ENGINE_API_KEY");
std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS");
std::env::remove_var("HYPER3D_BASE_URL");
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
std::env::set_var("GENARRATIVE_LLM_PROVIDER", "openai-compatible");
std::env::set_var(
"GENARRATIVE_LLM_BASE_URL",
"https://llm.internal.example/v1",
);
std::env::set_var("GENARRATIVE_LLM_MODEL", "internal-text-model");
std::env::set_var("APIMART_BASE_URL", "https://responses.internal.example/v1");
std::env::set_var("APIMART_IMAGE_REQUEST_TIMEOUT_MS", "190000");
std::env::set_var("ARK_API_KEY", "ark-key");
std::env::set_var("VECTOR_ENGINE_BASE_URL", "https://vector.internal.example");
std::env::set_var("VECTOR_ENGINE_API_KEY", "vector-engine-key");
std::env::set_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS", "210000");
std::env::set_var("HYPER3D_BASE_URL", "https://model.internal.example/api/v2");
std::env::set_var("DASHSCOPE_SCENE_IMAGE_MODEL", "scene-model");
std::env::set_var("DASHSCOPE_REFERENCE_IMAGE_MODEL", "reference-model");
std::env::set_var("DASHSCOPE_COVER_IMAGE_MODEL", "cover-model");
std::env::set_var(
"ARK_CHARACTER_VIDEO_BASE_URL",
"https://video.internal.example/v1",
);
std::env::set_var("ARK_CHARACTER_VIDEO_MODEL", "video-model");
}
let config = AppConfig::from_env();
assert_eq!(config.llm_provider, LlmProvider::OpenAiCompatible);
assert_eq!(config.llm_base_url, "https://llm.internal.example/v1");
assert_eq!(config.llm_api_key.as_deref(), Some("vector-engine-key"));
assert_eq!(config.llm_model, "internal-text-model");
// assert_eq!(
// config.apimart_base_url,
// "https://responses.internal.example/v1"
// );
// assert_eq!(config.apimart_image_request_timeout_ms, 190_000);
assert_eq!(
config.vector_engine_base_url,
"https://vector.internal.example"
);
assert_eq!(config.vector_engine_image_request_timeout_ms, 210_000);
assert_eq!(
config.hyper3d_base_url,
"https://model.internal.example/api/v2"
);
assert_eq!(config.dashscope_scene_image_model, "scene-model");
assert_eq!(config.dashscope_reference_image_model, "reference-model");
assert_eq!(config.dashscope_cover_image_model, "cover-model");
assert_eq!(
config.ark_character_video_base_url,
"https://video.internal.example/v1"
);
assert_eq!(config.ark_character_video_model, "video-model");
unsafe {
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
std::env::remove_var("GENARRATIVE_LLM_API_KEY");
std::env::remove_var("LLM_API_KEY");
std::env::remove_var("ARK_API_KEY");
std::env::remove_var("GENARRATIVE_LLM_MODEL");
std::env::remove_var("APIMART_BASE_URL");
std::env::remove_var("APIMART_IMAGE_REQUEST_TIMEOUT_MS");
std::env::remove_var("VECTOR_ENGINE_BASE_URL");
std::env::remove_var("VECTOR_ENGINE_API_KEY");
std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS");
std::env::remove_var("HYPER3D_BASE_URL");
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
}
}
#[test]
fn from_env_reads_spacetime_pool_size() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_POOL_SIZE");
std::env::set_var("GENARRATIVE_SPACETIME_POOL_SIZE", "7");
}
let config = AppConfig::from_env();
assert_eq!(config.spacetime_pool_size, 7);
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_POOL_SIZE");
}
}
#[test]
fn from_env_reads_runtime_service_bootstrap_secret_from_value_or_file() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
let secret_path = std::env::temp_dir().join(format!(
"genarrative-runtime-service-bootstrap-{}.txt",
std::process::id()
));
fs::write(&secret_path, " file-secret \n").expect("secret file should write");
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET");
std::env::set_var(
"GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE",
&secret_path,
);
}
assert_eq!(
AppConfig::from_env()
.spacetime_runtime_service_bootstrap_secret
.as_deref(),
Some("file-secret")
);
unsafe {
std::env::set_var(
"GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET",
"direct-secret",
);
}
assert_eq!(
AppConfig::from_env()
.spacetime_runtime_service_bootstrap_secret
.as_deref(),
Some("direct-secret")
);
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET");
std::env::remove_var("GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE");
}
let _ = fs::remove_file(secret_path);
}
#[test]
fn from_env_reads_bgfilter_worker_settings_and_secret_from_value_or_file() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
let secret_path = std::env::temp_dir().join(format!(
"genarrative-bgfilter-internal-token-{}.txt",
std::process::id()
));
fs::write(&secret_path, " file-token \n").expect("secret file should write");
unsafe {
std::env::remove_var("GENARRATIVE_BGFILTER_INTERNAL_TOKEN");
std::env::set_var("GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE", &secret_path);
std::env::set_var("GENARRATIVE_BGFILTER_WORKER_HOST", "127.0.0.2");
std::env::set_var("GENARRATIVE_BGFILTER_WORKER_PORT", "18083");
std::env::set_var(
"GENARRATIVE_BGFILTER_WORKER_BASE_URL",
"http://127.0.0.2:18083",
);
std::env::set_var("GENARRATIVE_BGFILTER_WORKER_CONCURRENCY", "6");
std::env::set_var("GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS", "192");
std::env::set_var("GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS", "3500");
std::env::set_var("GENARRATIVE_PROCESS_ROLE", "bgfilter_worker");
}
let config = AppConfig::from_env();
assert_eq!(config.process_role, ProcessRole::BgfilterWorker);
assert_eq!(config.bgfilter_worker_host, "127.0.0.2");
assert_eq!(config.bgfilter_worker_port, 18_083);
assert_eq!(config.bgfilter_worker_base_url, "http://127.0.0.2:18083");
assert_eq!(
config.bgfilter_internal_token.as_deref(),
Some("file-token")
);
assert_eq!(config.bgfilter_worker_concurrency, 6);
assert_eq!(config.bgfilter_worker_max_requests, 192);
assert_eq!(config.bgfilter_worker_connect_timeout_ms, 3_500);
unsafe {
std::env::set_var("GENARRATIVE_BGFILTER_INTERNAL_TOKEN", "direct-token");
}
assert_eq!(
AppConfig::from_env().bgfilter_internal_token.as_deref(),
Some("direct-token")
);
unsafe {
std::env::remove_var("GENARRATIVE_BGFILTER_INTERNAL_TOKEN");
std::env::remove_var("GENARRATIVE_BGFILTER_INTERNAL_TOKEN_FILE");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_HOST");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_PORT");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_BASE_URL");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_CONCURRENCY");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS");
std::env::remove_var("GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS");
std::env::remove_var("GENARRATIVE_PROCESS_ROLE");
}
let _ = fs::remove_file(secret_path);
}
#[test]
fn from_env_reads_api_runtime_performance_settings() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_API_LISTEN_BACKLOG");
std::env::remove_var("GENARRATIVE_API_WORKER_THREADS");
std::env::remove_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_DIR");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_DIR");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES");
std::env::remove_var("GENARRATIVE_OTEL_ENABLED");
std::env::set_var("GENARRATIVE_API_LISTEN_BACKLOG", "2048");
std::env::set_var("GENARRATIVE_API_WORKER_THREADS", "6");
std::env::set_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS", "128");
std::env::set_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS", "16");
std::env::set_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS", "3000");
std::env::set_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED", "false");
std::env::set_var(
"GENARRATIVE_TRACKING_OUTBOX_DIR",
"/tmp/genarrative-tracking-outbox",
);
std::env::set_var("GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE", "250");
std::env::set_var("GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS", "2000");
std::env::set_var("GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES", "1048576");
std::env::set_var("GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED", "false");
std::env::set_var(
"GENARRATIVE_WALLET_REFUND_OUTBOX_DIR",
"/tmp/genarrative-wallet-refund-outbox",
);
std::env::set_var("GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE", "50");
std::env::set_var("GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS", "3000");
std::env::set_var("GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES", "524288");
std::env::set_var("GENARRATIVE_OTEL_ENABLED", "true");
}
let config = AppConfig::from_env();
assert_eq!(config.listen_backlog, 2048);
assert_eq!(config.worker_threads, Some(6));
assert_eq!(config.max_concurrent_requests, Some(128));
assert_eq!(config.admin_max_concurrent_requests, Some(16));
assert_eq!(
config.shutdown_outbox_flush_timeout,
std::time::Duration::from_millis(3_000)
);
assert!(!config.tracking_outbox_enabled);
assert_eq!(
config.tracking_outbox_dir,
std::path::PathBuf::from("/tmp/genarrative-tracking-outbox")
);
assert_eq!(config.tracking_outbox_batch_size, 250);
assert_eq!(
config.tracking_outbox_flush_interval,
std::time::Duration::from_millis(2_000)
);
assert_eq!(config.tracking_outbox_max_bytes, 1_048_576);
assert!(!config.wallet_refund_outbox_enabled);
assert_eq!(
config.wallet_refund_outbox_dir,
std::path::PathBuf::from("/tmp/genarrative-wallet-refund-outbox")
);
assert_eq!(config.wallet_refund_outbox_batch_size, 50);
assert_eq!(
config.wallet_refund_outbox_flush_interval,
std::time::Duration::from_millis(3_000)
);
assert_eq!(config.wallet_refund_outbox_max_bytes, 524_288);
assert!(config.otel_enabled);
unsafe {
std::env::remove_var("GENARRATIVE_API_LISTEN_BACKLOG");
std::env::remove_var("GENARRATIVE_API_WORKER_THREADS");
std::env::remove_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_ADMIN_MAX_CONCURRENT_REQUESTS");
std::env::remove_var("GENARRATIVE_API_SHUTDOWN_OUTBOX_FLUSH_TIMEOUT_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_DIR");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS");
std::env::remove_var("GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_DIR");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS");
std::env::remove_var("GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES");
std::env::remove_var("GENARRATIVE_OTEL_ENABLED");
}
}
#[test]
fn from_env_reads_wechat_pay_settings() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("WECHAT_PAY_ENABLED");
std::env::remove_var("WECHAT_PAY_PROVIDER");
std::env::remove_var("WECHAT_PAY_MCH_ID");
std::env::remove_var("WECHAT_PAY_MERCHANT_SERIAL_NO");
std::env::remove_var("WECHAT_PAY_PRIVATE_KEY_PATH");
std::env::remove_var("WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH");
std::env::remove_var("WECHAT_PAY_PLATFORM_SERIAL_NO");
std::env::remove_var("WECHAT_PAY_API_V3_KEY");
std::env::remove_var("WECHAT_PAY_NOTIFY_URL");
std::env::remove_var("WECHAT_PAY_REFUND_RECONCILIATION_ENABLED");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT");
std::env::remove_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT",
);
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
std::env::set_var("WECHAT_PAY_ENABLED", "true");
std::env::set_var("WECHAT_PAY_PROVIDER", "real");
std::env::set_var("WECHAT_PAY_MCH_ID", "1900000109");
std::env::set_var("WECHAT_PAY_MERCHANT_SERIAL_NO", "serial-001");
std::env::set_var("WECHAT_PAY_PRIVATE_KEY_PATH", "certs/apiclient_key.pem");
std::env::set_var(
"WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH",
"certs/wechatpay_platform.pem",
);
std::env::set_var("WECHAT_PAY_PLATFORM_SERIAL_NO", "platform-serial-001");
std::env::set_var("WECHAT_PAY_API_V3_KEY", "12345678901234567890123456789012");
std::env::set_var(
"WECHAT_PAY_NOTIFY_URL",
"https://api.example.com/api/profile/recharge/wechat/notify",
);
std::env::set_var("WECHAT_PAY_REFUND_RECONCILIATION_ENABLED", "true");
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID", "offer-001");
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY", "app-key-001");
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY",
"sandbox-app-key-001",
);
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT",
"http://127.0.0.1:18080/xpay/query_order",
);
std::env::set_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT",
"http://127.0.0.1:18080/xpay/notify_provide_goods",
);
std::env::set_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN", "message-token-001");
std::env::set_var(
"WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY",
"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
);
std::env::set_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV", "1");
}
let config = AppConfig::from_env();
assert!(config.wechat_pay_enabled);
assert_eq!(config.wechat_pay_provider, "real");
assert_eq!(config.wechat_pay_mch_id.as_deref(), Some("1900000109"));
assert_eq!(
config.wechat_pay_private_key_path.as_deref(),
Some(std::path::Path::new("certs/apiclient_key.pem"))
);
assert_eq!(
config.wechat_pay_notify_url.as_deref(),
Some("https://api.example.com/api/profile/recharge/wechat/notify")
);
assert!(config.wechat_pay_refund_reconciliation_enabled);
assert_eq!(
config.wechat_pay_platform_public_key_path.as_deref(),
Some(std::path::Path::new("certs/wechatpay_platform.pem"))
);
assert_eq!(
config.wechat_pay_platform_serial_no.as_deref(),
Some("platform-serial-001")
);
assert_eq!(
config
.wechat_mini_program_virtual_payment_offer_id
.as_deref(),
Some("offer-001")
);
assert_eq!(
config
.wechat_mini_program_virtual_payment_app_key
.as_deref(),
Some("app-key-001")
);
assert_eq!(
config.wechat_mini_program_message_token.as_deref(),
Some("message-token-001")
);
assert_eq!(
config
.wechat_mini_program_message_encoding_aes_key
.as_deref(),
Some("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG")
);
assert_eq!(
config
.wechat_mini_program_virtual_payment_sandbox_app_key
.as_deref(),
Some("sandbox-app-key-001")
);
assert_eq!(config.wechat_mini_program_virtual_payment_env, 1);
assert_eq!(
config.wechat_mini_program_virtual_payment_query_order_endpoint,
"http://127.0.0.1:18080/xpay/query_order"
);
assert_eq!(
config.wechat_mini_program_virtual_payment_notify_provide_goods_endpoint,
"http://127.0.0.1:18080/xpay/notify_provide_goods"
);
unsafe {
std::env::remove_var("WECHAT_PAY_ENABLED");
std::env::remove_var("WECHAT_PAY_PROVIDER");
std::env::remove_var("WECHAT_PAY_MCH_ID");
std::env::remove_var("WECHAT_PAY_MERCHANT_SERIAL_NO");
std::env::remove_var("WECHAT_PAY_PRIVATE_KEY_PATH");
std::env::remove_var("WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH");
std::env::remove_var("WECHAT_PAY_PLATFORM_SERIAL_NO");
std::env::remove_var("WECHAT_PAY_API_V3_KEY");
std::env::remove_var("WECHAT_PAY_NOTIFY_URL");
std::env::remove_var("WECHAT_PAY_REFUND_RECONCILIATION_ENABLED");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_OFFER_ID");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_SANDBOX_APP_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT");
std::env::remove_var(
"WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT",
);
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_TOKEN");
std::env::remove_var("WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY");
std::env::remove_var("WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_ENV");
}
}
#[test]
fn from_env_ignores_zero_spacetime_pool_size() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_POOL_SIZE");
std::env::set_var("GENARRATIVE_SPACETIME_POOL_SIZE", "0");
}
let config = AppConfig::from_env();
assert_eq!(config.spacetime_pool_size, 4);
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_POOL_SIZE");
}
}
#[test]
fn from_env_reads_spacetime_procedure_timeout() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS");
std::env::set_var("GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS", "45");
}
let config = AppConfig::from_env();
assert_eq!(config.spacetime_procedure_timeout.as_secs(), 45);
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS");
}
}
#[test]
fn from_env_reads_spacetime_health_check_timeout() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS");
std::env::set_var("GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS", "3");
}
let config = AppConfig::from_env();
assert_eq!(config.spacetime_health_check_timeout.as_secs(), 3);
unsafe {
std::env::remove_var("GENARRATIVE_SPACETIME_HEALTH_CHECK_TIMEOUT_SECONDS");
}
}
#[test]
fn from_env_reads_editor_generation_pricing_override_path() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH");
std::env::set_var(
"GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH",
"server-rs/.data/editor-pricing.json",
);
}
let config = AppConfig::from_env();
assert_eq!(
config.editor_generation_pricing_override_path,
std::path::PathBuf::from("server-rs/.data/editor-pricing.json")
);
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH");
}
}
#[test]
fn from_env_reads_editor_bgfilter_settings_and_reuses_background_token() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS");
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
std::env::set_var(
"GENARRATIVE_EDITOR_BGFILTER_BASE_URL",
"http://10.0.0.12/bgfilter",
);
std::env::set_var(
"GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN",
"shared-token",
);
std::env::set_var(
"GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS",
"8000",
);
}
let config = AppConfig::from_env();
assert_eq!(config.editor_bgfilter_base_url, "http://10.0.0.12/bgfilter");
assert_eq!(
config.editor_bgfilter_token.as_deref(),
Some("shared-token")
);
assert_eq!(config.editor_bgfilter_single_image_estimate_ms, 8_000);
// attempt / callBudget 必须随 N 与 est 联动派生,而不是固定值。
assert_eq!(
config.bgfilter_provider_attempt_timeout_ms(),
config.bgfilter_worker_concurrency as u64 * 8_000 * 2
);
assert_eq!(
config.bgfilter_call_budget_ms(),
config.bgfilter_provider_attempt_timeout_ms() * 2 + 1_000
);
unsafe {
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN", "bgfilter-token");
}
let config = AppConfig::from_env();
assert_eq!(
config.editor_bgfilter_token.as_deref(),
Some("bgfilter-token")
);
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_BASE_URL");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_TOKEN");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS");
std::env::remove_var("GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN");
}
}
#[test]
fn from_env_reads_aliyun_matting_settings_and_reuses_alibaba_cloud_keys() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ENABLED");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ENDPOINT");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS");
std::env::remove_var("ALIBABA_CLOUD_ACCESS_KEY_ID");
std::env::remove_var("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
std::env::set_var("ALIBABA_CLOUD_ACCESS_KEY_ID", "shared-ak-id");
std::env::set_var("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "shared-ak-secret");
std::env::set_var("GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS", "45000");
}
let config = AppConfig::from_env();
assert!(config.aliyun_matting_enabled);
assert_eq!(
config.aliyun_matting_endpoint,
"imageseg.cn-shanghai.aliyuncs.com"
);
assert_eq!(
config.aliyun_matting_access_key_id.as_deref(),
Some("shared-ak-id")
);
assert_eq!(
config.aliyun_matting_access_key_secret.as_deref(),
Some("shared-ak-secret")
);
assert_eq!(config.aliyun_matting_request_timeout_ms, 45_000);
unsafe {
std::env::set_var("GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID", "matting-ak-id");
std::env::set_var("GENARRATIVE_ALIYUN_MATTING_ENABLED", "false");
}
let config = AppConfig::from_env();
assert!(!config.aliyun_matting_enabled);
assert_eq!(
config.aliyun_matting_access_key_id.as_deref(),
Some("matting-ak-id")
);
unsafe {
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ENABLED");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID");
std::env::remove_var("GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS");
std::env::remove_var("ALIBABA_CLOUD_ACCESS_KEY_ID");
std::env::remove_var("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
}
}
#[test]
fn from_env_reads_editor_bgfilter_circuit_settings() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS");
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD", "4");
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS", "90");
}
let config = AppConfig::from_env();
assert_eq!(config.editor_bgfilter_circuit_failure_threshold, 4);
assert_eq!(config.editor_bgfilter_circuit_cooldown.as_secs(), 90);
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD");
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS");
}
}
#[test]
fn from_env_allows_zero_editor_bgfilter_circuit_threshold() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD");
std::env::set_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD", "0");
}
let config = AppConfig::from_env();
assert_eq!(config.editor_bgfilter_circuit_failure_threshold, 0);
unsafe {
std::env::remove_var("GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD");
}
}
#[test]
fn from_env_reads_external_generation_worker_timeout_settings() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS");
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS");
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS");
std::env::set_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS", "5m");
std::env::set_var(
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS",
"12m",
);
std::env::set_var(
"GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS",
"30m",
);
}
let config = AppConfig::from_env();
assert_eq!(config.external_generation_worker_lease.as_secs(), 300);
assert_eq!(config.external_generation_worker_job_timeout.as_secs(), 720);
assert_eq!(
config.external_generation_worker_long_job_timeout.as_secs(),
1_800
);
unsafe {
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS");
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS");
std::env::remove_var("GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS");
}
}
#[test]
fn from_env_reads_image_editor_agent_sidebar_runtime_flag() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR");
std::env::set_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR", "true");
}
let config = AppConfig::from_env();
assert!(config.image_editor_agent_sidebar_enabled);
unsafe {
std::env::remove_var("GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR");
}
}
#[test]
fn from_env_reads_prefixed_character_animation_ffmpeg_paths() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_CHARACTER_ANIMATION_FFMPEG_PATH");
std::env::remove_var("GENARRATIVE_CHARACTER_ANIMATION_FFPROBE_PATH");
std::env::remove_var("CHARACTER_ANIMATION_FFMPEG_PATH");
std::env::remove_var("CHARACTER_ANIMATION_FFPROBE_PATH");
std::env::set_var(
"GENARRATIVE_CHARACTER_ANIMATION_FFMPEG_PATH",
"C:\\Genarrative\\ffmpeg\\bin\\ffmpeg.exe",
);
std::env::set_var(
"GENARRATIVE_CHARACTER_ANIMATION_FFPROBE_PATH",
"C:\\Genarrative\\ffmpeg\\bin\\ffprobe.exe",
);
}
let config = AppConfig::from_env();
assert_eq!(
config.character_animation_ffmpeg_path,
"C:\\Genarrative\\ffmpeg\\bin\\ffmpeg.exe"
);
assert_eq!(
config.character_animation_ffprobe_path,
"C:\\Genarrative\\ffmpeg\\bin\\ffprobe.exe"
);
unsafe {
std::env::remove_var("GENARRATIVE_CHARACTER_ANIMATION_FFMPEG_PATH");
std::env::remove_var("GENARRATIVE_CHARACTER_ANIMATION_FFPROBE_PATH");
std::env::remove_var("CHARACTER_ANIMATION_FFMPEG_PATH");
std::env::remove_var("CHARACTER_ANIMATION_FFPROBE_PATH");
}
}
}