1011 lines
38 KiB
Rust
1011 lines
38 KiB
Rust
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,
|
|
};
|
|
|
|
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";
|
|
|
|
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
|
#[derive(Clone, Debug)]
|
|
pub struct AppConfig {
|
|
pub bind_host: String,
|
|
pub bind_port: u16,
|
|
pub log_filter: String,
|
|
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_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_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 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 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,
|
|
log_filter: "info,tower_http=info".to_string(),
|
|
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_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_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,
|
|
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,
|
|
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;
|
|
}
|
|
|
|
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"]);
|
|
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_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"]);
|
|
|
|
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(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_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_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, 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.ark_character_video_base_url.is_empty());
|
|
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("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://image.internal.example/v1");
|
|
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://image.internal.example/v1");
|
|
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("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_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");
|
|
}
|
|
}
|
|
}
|