Files
Genarrative/server-rs/crates/api-server/src/config.rs
2026-05-16 22:44:30 +08:00

1364 lines
54 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, 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 DEFAULT_AUTH_STORE_PATH: &str = "server-rs/.data/auth-store.json";
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000;
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
#[derive(Clone, Debug)]
pub struct AppConfig {
pub bind_host: String,
pub bind_port: u16,
pub listen_backlog: i32,
pub worker_threads: Option<usize>,
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 auth_store_path: PathBuf,
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_jsapi_endpoint: String,
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_pool_size: u32,
pub spacetime_procedure_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 rpg_llm_web_search_enabled: bool,
pub creation_agent_llm_web_search_enabled: bool,
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,
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 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 Default for AppConfig {
fn default() -> Self {
Self {
bind_host: "127.0.0.1".to_string(),
bind_port: 3000,
listen_backlog: 1024,
worker_threads: None,
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,
auth_store_path: PathBuf::from(DEFAULT_AUTH_STORE_PATH),
dev_password_entry_auto_register_enabled: false,
sms_auth_enabled: false,
sms_auth_provider: "mock".to_string(),
sms_endpoint: "dypnsapi.aliyuncs.com".to_string(),
sms_access_key_id: None,
sms_access_key_secret: None,
sms_sign_name: "速通互联验证码".to_string(),
sms_template_code: "100001".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_jsapi_endpoint: "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
.to_string(),
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_pool_size: 4,
spacetime_procedure_timeout: Duration::from_secs(30),
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,
rpg_llm_web_search_enabled: true,
creation_agent_llm_web_search_enabled: true,
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,
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 {
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 Ok(log_filter) = env::var("GENARRATIVE_API_LOG")
&& !log_filter.trim().is_empty()
{
config.log_filter = log_filter;
}
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(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(auth_store_path) = read_first_non_empty_env(&["GENARRATIVE_AUTH_STORE_PATH"]) {
config.auth_store_path = PathBuf::from(auth_store_path);
}
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(wechat_pay_jsapi_endpoint) =
read_first_non_empty_env(&["WECHAT_PAY_JSAPI_ENDPOINT"])
{
config.wechat_pay_jsapi_endpoint = wechat_pay_jsapi_endpoint;
}
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"]);
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(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 =
read_first_non_empty_env(&["GENARRATIVE_LLM_API_KEY", "LLM_API_KEY", "ARK_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(rpg_llm_web_search_enabled) = read_first_bool_env(&[
"GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED",
"RPG_LLM_WEB_SEARCH_ENABLED",
]) {
config.rpg_llm_web_search_enabled = rpg_llm_web_search_enabled;
}
if let Some(creation_agent_llm_web_search_enabled) = read_first_bool_env(&[
"GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED",
"CREATION_AGENT_LLM_WEB_SEARCH_ENABLED",
]) {
config.creation_agent_llm_web_search_enabled = creation_agent_llm_web_search_enabled;
}
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;
}
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"])
{
// 中文注释VectorEngine image-2 实测可能超过 500 秒;旧环境文件中常见的 180 秒值不能再提前截断真实生图。
config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms
.max(DEFAULT_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(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(&["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(&["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_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_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 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> {
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_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS, LlmProvider};
use std::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!(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!(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, "dypnsapi.aliyuncs.com");
assert_eq!(
config.wechat_authorize_endpoint,
"https://open.weixin.qq.com/connect/qrconnect"
);
}
#[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_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_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("VECTOR_ENGINE_BASE_URL", "https://vector.internal.example");
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_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,
DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS
);
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_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_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_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_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_OTEL_ENABLED", "true");
}
let config = AppConfig::from_env();
assert_eq!(config.listen_backlog, 2048);
assert_eq!(config.worker_threads, Some(6));
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_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::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",
);
}
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_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")
);
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");
}
}
#[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_rpg_llm_web_search_switch() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED");
std::env::set_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED", "false");
}
let config = AppConfig::from_env();
assert!(!config.rpg_llm_web_search_enabled);
unsafe {
std::env::remove_var("GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED");
}
}
#[test]
fn from_env_reads_creation_agent_llm_web_search_switch() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED");
std::env::set_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED", "false");
}
let config = AppConfig::from_env();
assert!(!config.creation_agent_llm_web_search_enabled);
unsafe {
std::env::remove_var("GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED");
}
}
}