feat: init

This commit is contained in:
zhenyi
2026-06-07 11:30:56 +08:00
commit 563381c1ca
361 changed files with 41327 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn ai_provider_api_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_AI_PROVIDER_API_KEY")
}
pub fn ai_provider_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_AI_PROVIDER_URL")
}
}
+8
View File
@@ -0,0 +1,8 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn app_url(&self) -> AppResult<String> {
self.get_env_or::<String>("APP_URL", "http://localhost:8000".into())
}
}
+32
View File
@@ -0,0 +1,32 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn channel_ai_provider_api_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_CHANNEL_AI_PROVIDER_API_KEY")
}
pub fn channel_ai_provider_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_CHANNEL_AI_PROVIDER_URL")
}
pub fn channel_ai_provider_model(&self) -> AppResult<String> {
self.get_env_or("APP_CHANNEL_AI_PROVIDER_MODEL", "gpt-4o".to_string())
}
pub fn channel_ai_provider_temperature(&self) -> AppResult<f64> {
self.get_env_or("APP_CHANNEL_AI_PROVIDER_TEMPERATURE", 0.7)
}
pub fn channel_ai_provider_max_tokens(&self) -> AppResult<u32> {
self.get_env_or("APP_CHANNEL_AI_PROVIDER_MAX_TOKENS", 4096)
}
pub fn channel_ai_provider_top_p(&self) -> AppResult<f64> {
self.get_env_or("APP_CHANNEL_AI_PROVIDER_TOP_P", 1.0)
}
pub fn channel_ai_provider_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_CHANNEL_AI_PROVIDER_TIMEOUT", 60)
}
}
+87
View File
@@ -0,0 +1,87 @@
use crate::config::AppConfig;
use crate::error::{AppError, AppResult};
impl AppConfig {
pub fn database_url(&self) -> AppResult<String> {
if let Some(url) = self.get_env::<String>("APP_DATABASE_URL")? {
return Ok(url);
}
if let Some(url) = self.get_env::<String>("DATABASE_URL")? {
return Ok(url);
}
Err(AppError::Config(
"Neither APP_DATABASE_URL nor DATABASE_URL is set".into(),
))
}
pub fn database_max_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_DATABASE_MAX_CONNECTIONS", 10)
}
pub fn database_min_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_DATABASE_MIN_CONNECTIONS", 2)
}
pub fn database_idle_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_IDLE_TIMEOUT", 600)
}
pub fn database_max_lifetime(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_MAX_LIFETIME", 3600)
}
pub fn database_connection_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_CONNECTION_TIMEOUT", 8)
}
pub fn database_schema_search_path(&self) -> AppResult<String> {
self.get_env_or("APP_DATABASE_SCHEMA_SEARCH_PATH", "public".to_string())
}
pub fn database_read_write_split(&self) -> AppResult<bool> {
self.get_env_or("APP_DATABASE_READ_WRITE_SPLIT", false)
}
pub fn database_read_replicas(&self) -> AppResult<Vec<String>> {
match self.get_env::<String>("APP_DATABASE_REPLICAS")? {
Some(s) if !s.is_empty() => Ok(s
.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()),
_ => Ok(Vec::new()),
}
}
pub fn database_replica_max_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_DATABASE_REPLICA_MAX_CONNECTIONS", 10)
}
pub fn database_replica_min_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_DATABASE_REPLICA_MIN_CONNECTIONS", 2)
}
pub fn database_replica_idle_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_REPLICA_IDLE_TIMEOUT", 600)
}
pub fn database_replica_max_lifetime(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_REPLICA_MAX_LIFETIME", 3600)
}
pub fn database_replica_connection_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_REPLICA_CONNECTION_TIMEOUT", 8)
}
pub fn database_health_check_interval(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_HEALTH_CHECK_INTERVAL", 30)
}
pub fn database_retry_attempts(&self) -> AppResult<u32> {
self.get_env_or("APP_DATABASE_RETRY_ATTEMPTS", 3)
}
pub fn database_retry_delay(&self) -> AppResult<u64> {
self.get_env_or("APP_DATABASE_RETRY_DELAY", 5)
}
}
+27
View File
@@ -0,0 +1,27 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn embed_ai_provider_api_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_EMBED_AI_PROVIDER_API_KEY")
}
pub fn embed_ai_provider_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_EMBED_AI_PROVIDER_URL")
}
pub fn embed_ai_provider_model(&self) -> AppResult<String> {
self.get_env_or(
"APP_EMBED_AI_PROVIDER_MODEL",
"text-embedding-3-small".to_string(),
)
}
pub fn embed_ai_provider_dimensions(&self) -> AppResult<u32> {
self.get_env_or("APP_EMBED_AI_PROVIDER_DIMENSIONS", 1536)
}
pub fn embed_ai_provider_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_EMBED_AI_PROVIDER_TIMEOUT", 30)
}
}
+59
View File
@@ -0,0 +1,59 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn etcd_endpoints(&self) -> AppResult<Vec<String>> {
match self.get_env::<String>("APP_ETCD_ENDPOINTS")? {
Some(s) if !s.is_empty() => Ok(s
.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()),
_ => Ok(vec!["http://localhost:2379".to_string()]),
}
}
pub fn etcd_username(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_ETCD_USERNAME")
}
pub fn etcd_password(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_ETCD_PASSWORD")
}
pub fn etcd_ca_cert_path(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_ETCD_CA_CERT_PATH")
}
pub fn etcd_client_cert_path(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_ETCD_CLIENT_CERT_PATH")
}
pub fn etcd_client_key_path(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_ETCD_CLIENT_KEY_PATH")
}
pub fn etcd_key_prefix(&self) -> AppResult<String> {
self.get_env_or("APP_ETCD_KEY_PREFIX", "/appks/".to_string())
}
pub fn etcd_connect_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_ETCD_CONNECT_TIMEOUT", 5)
}
pub fn etcd_request_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_ETCD_REQUEST_TIMEOUT", 10)
}
pub fn etcd_keep_alive_interval(&self) -> AppResult<u64> {
self.get_env_or("APP_ETCD_KEEP_ALIVE_INTERVAL", 10)
}
pub fn etcd_lease_ttl(&self) -> AppResult<u64> {
self.get_env_or("APP_ETCD_LEASE_TTL", 15)
}
pub fn etcd_max_retries(&self) -> AppResult<u32> {
self.get_env_or("APP_ETCD_MAX_RETRIES", 3)
}
}
+16
View File
@@ -0,0 +1,16 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn lru_default_capacity(&self) -> AppResult<usize> {
self.get_env_or("APP_LRU_DEFAULT_CAPACITY", 1000)
}
pub fn lru_default_ttl_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_LRU_DEFAULT_TTL_SECS", 300)
}
pub fn lru_cleanup_interval_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_LRU_CLEANUP_INTERVAL_SECS", 60)
}
}
+90
View File
@@ -0,0 +1,90 @@
use crate::error::{AppError, AppResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
use tokio::sync::OnceCell;
pub static GLOBAL_CONFIG: OnceCell<AppConfig> = OnceCell::const_new();
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AppConfig {
pub env: HashMap<String, String>,
}
impl AppConfig {
pub fn main_domain(&self) -> AppResult<String> {
self.get_env::<String>("APP_MAIN_DOMAIN")?
.filter(|s| !s.is_empty())
.ok_or_else(|| AppError::Config("APP_MAIN_DOMAIN is not set".into()))
}
pub const ENV_FILES: &'static [&'static str] = &[
".env",
".env.local",
".env.development",
".env.development.local",
".env.test",
".env.test.local",
".env.production",
".env.production.local",
];
pub fn get_env<T: FromStr>(&self, key: &str) -> AppResult<Option<T>>
where
<T as FromStr>::Err: std::fmt::Display,
{
match self.env.get(key) {
Some(v) if !v.is_empty() => Ok(Some(
v.parse::<T>().map_err(|e| AppError::Parse(e.to_string()))?,
)),
Some(_) => Ok(None),
None => Ok(None),
}
}
pub fn get_env_or<T: FromStr>(&self, key: &str, default: T) -> AppResult<T>
where
<T as FromStr>::Err: std::fmt::Display,
{
Ok(self.get_env(key)?.unwrap_or(default))
}
pub fn load() -> AppConfig {
let mut env = HashMap::new();
for env_file in AppConfig::ENV_FILES {
if let Err(e) = dotenvy::from_path(env_file) {
tracing::debug!(file = %env_file, error = %e, "dotenv load skipped");
}
if let Ok(env_file_content) = std::fs::read_to_string(env_file) {
for line in env_file_content.lines() {
if let Some((key, value)) = line.split_once('=') {
env.insert(key.to_string(), value.to_string());
}
}
}
}
env = env.into_iter().chain(std::env::vars()).collect();
let this = AppConfig { env };
if let Some(config) = GLOBAL_CONFIG.get() {
config.clone()
} else {
let _ = GLOBAL_CONFIG.set(this);
GLOBAL_CONFIG
.get()
.expect("global config should be set after load")
.clone()
}
}
}
pub mod aiprovider;
pub mod app;
pub mod channelaiprovider;
pub mod database;
pub mod embedaiprovider;
pub mod etcd;
pub mod lru;
pub mod nats;
pub mod qdrant;
pub mod redis;
pub mod rpc;
pub mod s3;
+54
View File
@@ -0,0 +1,54 @@
use crate::config::AppConfig;
use crate::error::{AppError, AppResult};
impl AppConfig {
pub fn nats_url(&self) -> AppResult<String> {
self.get_env::<String>("APP_NATS_URL")?
.filter(|s| !s.is_empty())
.ok_or_else(|| AppError::Config("APP_NATS_URL is not set".into()))
}
pub fn nats_username(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_NATS_USERNAME")
}
pub fn nats_password(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_NATS_PASSWORD")
}
pub fn nats_token(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_NATS_TOKEN")
}
pub fn nats_tls_enabled(&self) -> AppResult<bool> {
self.get_env_or("APP_NATS_TLS_ENABLED", false)
}
pub fn nats_connection_timeout_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_NATS_CONNECTION_TIMEOUT", 5)
}
pub fn nats_ping_interval_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_NATS_PING_INTERVAL", 20)
}
pub fn nats_reconnect_delay_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_NATS_RECONNECT_DELAY", 2)
}
pub fn nats_max_reconnects(&self) -> AppResult<usize> {
self.get_env_or("APP_NATS_MAX_RECONNECTS", 60usize)
}
pub fn nats_stream_prefix(&self) -> AppResult<String> {
self.get_env_or("APP_NATS_STREAM_PREFIX", "APPKS".to_string())
}
pub fn nats_default_ack_wait_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_NATS_ACK_WAIT_SECS", 30)
}
pub fn nats_default_max_deliver(&self) -> AppResult<i64> {
self.get_env_or("APP_NATS_MAX_DELIVER", 5i64)
}
}
+63
View File
@@ -0,0 +1,63 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn qdrant_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_QDRANT_URL")
}
pub fn qdrant_cluster_nodes(&self) -> AppResult<Vec<String>> {
match self.get_env::<String>("APP_QDRANT_CLUSTER_NODES")? {
Some(s) if !s.is_empty() => Ok(s
.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()),
_ => Ok(Vec::new()),
}
}
pub fn qdrant_api_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_QDRANT_API_KEY")
}
pub fn qdrant_collection(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_QDRANT_COLLECTION")
}
pub fn qdrant_vector_size(&self) -> AppResult<u32> {
self.get_env_or("APP_QDRANT_VECTOR_SIZE", 1536)
}
pub fn qdrant_distance(&self) -> AppResult<String> {
self.get_env_or("APP_QDRANT_DISTANCE", "Cosine".to_string())
}
pub fn qdrant_max_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_QDRANT_MAX_CONNECTIONS", 10)
}
pub fn qdrant_idle_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_QDRANT_IDLE_TIMEOUT", 300)
}
pub fn qdrant_connection_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_QDRANT_CONNECTION_TIMEOUT", 10)
}
pub fn qdrant_max_retries(&self) -> AppResult<u32> {
self.get_env_or("APP_QDRANT_MAX_RETRIES", 3)
}
pub fn qdrant_tls_enabled(&self) -> AppResult<bool> {
self.get_env_or("APP_QDRANT_TLS_ENABLED", true)
}
pub fn qdrant_search_limit(&self) -> AppResult<u32> {
self.get_env_or("APP_QDRANT_SEARCH_LIMIT", 10)
}
pub fn qdrant_score_threshold(&self) -> AppResult<f64> {
self.get_env_or("APP_QDRANT_SCORE_THRESHOLD", 0.7)
}
}
+87
View File
@@ -0,0 +1,87 @@
use crate::config::AppConfig;
use crate::error::{AppError, AppResult};
impl AppConfig {
pub fn redis_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_REDIS_URL")
}
pub fn redis_cluster_enabled(&self) -> AppResult<bool> {
self.get_env_or("APP_REDIS_CLUSTER_ENABLED", false)
}
pub fn redis_cluster_nodes(&self) -> AppResult<Vec<String>> {
match self.get_env::<String>("APP_REDIS_CLUSTER_NODES")? {
Some(s) if !s.is_empty() => Ok(s
.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()),
_ => Ok(Vec::new()),
}
}
pub fn redis_read_from_replicas(&self) -> AppResult<bool> {
self.get_env_or("APP_REDIS_READ_FROM_REPLICAS", false)
}
pub fn redis_username(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_REDIS_USERNAME")
}
pub fn redis_password(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_REDIS_PASSWORD")
}
pub fn redis_database(&self) -> AppResult<u8> {
self.get_env_or("APP_REDIS_DATABASE", 0u8)
}
pub fn redis_max_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_REDIS_MAX_CONNECTIONS", 20)
}
pub fn redis_min_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_REDIS_MIN_CONNECTIONS", 2)
}
pub fn redis_idle_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_REDIS_IDLE_TIMEOUT", 300)
}
pub fn redis_connection_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_REDIS_CONNECTION_TIMEOUT", 5)
}
pub fn redis_max_retries(&self) -> AppResult<u32> {
self.get_env_or("APP_REDIS_MAX_RETRIES", 3)
}
pub fn redis_retry_delay_ms(&self) -> AppResult<u64> {
self.get_env_or("APP_REDIS_RETRY_DELAY_MS", 100)
}
pub fn redis_tls_enabled(&self) -> AppResult<bool> {
self.get_env_or("APP_REDIS_TLS_ENABLED", false)
}
pub fn redis_key_prefix(&self) -> AppResult<String> {
self.get_env_or("APP_REDIS_KEY_PREFIX", "".to_string())
}
pub fn redis_validate(&self) -> AppResult<()> {
if self.redis_cluster_enabled()? {
let nodes = self.redis_cluster_nodes()?;
if nodes.is_empty() {
return Err(AppError::Config(
"Redis cluster enabled but APP_REDIS_CLUSTER_NODES is empty".into(),
));
}
} else if self.redis_url()?.is_none() {
return Err(AppError::Config(
"Redis cluster disabled but APP_REDIS_URL is not set".into(),
));
}
Ok(())
}
}
+30
View File
@@ -0,0 +1,30 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn rpc_self_host(&self) -> AppResult<String> {
self.get_env_or("APP_RPC_SELF_HOST", "0.0.0.0".to_string())
}
pub fn rpc_self_port(&self) -> AppResult<u16> {
self.get_env_or("APP_RPC_SELF_PORT", 50050u16)
}
pub fn rpc_self_listen_addr(&self) -> AppResult<String> {
let host = self.rpc_self_host()?;
let port = self.rpc_self_port()?;
Ok(format!("{host}:{port}"))
}
pub fn rpc_self_reflection(&self) -> AppResult<bool> {
self.get_env_or("APP_RPC_SELF_REFLECTION", false)
}
pub fn rpc_self_service_name(&self) -> AppResult<String> {
self.get_env_or("APP_RPC_SELF_SERVICE_NAME", "appks".to_string())
}
pub fn rpc_default_timeout_secs(&self) -> AppResult<u64> {
self.get_env_or("APP_RPC_DEFAULT_TIMEOUT_SECS", 10)
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::config::AppConfig;
use crate::error::AppResult;
impl AppConfig {
pub fn s3_endpoint(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_S3_ENDPOINT")
}
pub fn s3_region(&self) -> AppResult<String> {
self.get_env_or("APP_S3_REGION", "us-east-1".to_string())
}
pub fn s3_access_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_S3_ACCESS_KEY")
}
pub fn s3_secret_key(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_S3_SECRET_KEY")
}
pub fn s3_bucket(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_S3_BUCKET")
}
pub fn s3_path_style(&self) -> AppResult<bool> {
self.get_env_or("APP_S3_PATH_STYLE", false)
}
pub fn s3_public_url(&self) -> AppResult<Option<String>> {
self.get_env::<String>("APP_S3_PUBLIC_URL")
}
pub fn s3_max_connections(&self) -> AppResult<u32> {
self.get_env_or("APP_S3_MAX_CONNECTIONS", 50)
}
pub fn s3_idle_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_S3_IDLE_TIMEOUT", 90)
}
pub fn s3_connection_timeout(&self) -> AppResult<u64> {
self.get_env_or("APP_S3_CONNECTION_TIMEOUT", 10)
}
pub fn s3_max_retries(&self) -> AppResult<u32> {
self.get_env_or("APP_S3_MAX_RETRIES", 3)
}
pub fn s3_upload_part_size(&self) -> AppResult<u64> {
self.get_env_or("APP_S3_UPLOAD_PART_SIZE", 8 * 1024 * 1024)
}
pub fn s3_max_upload_size(&self) -> AppResult<u64> {
self.get_env_or("APP_S3_MAX_UPLOAD_SIZE", 100 * 1024 * 1024)
}
pub fn s3_presigned_url_expiry(&self) -> AppResult<u64> {
self.get_env_or("APP_S3_PRESIGNED_URL_EXPIRY", 3600)
}
pub fn s3_force_path_style(&self) -> AppResult<bool> {
self.get_env_or("APP_S3_FORCE_PATH_STYLE", false)
}
}