| use crate::{ |
| app::constant::{ |
| EMPTY_STRING, ERR_INVALID_PATH, ROUTE_ABOUT_PATH, ROUTE_API_PATH, ROUTE_BUILD_KEY_PATH, |
| ROUTE_CONFIG_PATH, ROUTE_LOGS_PATH, ROUTE_README_PATH, ROUTE_ROOT_PATH, |
| ROUTE_SHARED_JS_PATH, ROUTE_SHARED_STYLES_PATH, ROUTE_TOKENS_PATH, |
| }, |
| chat::model::Message, |
| common::{ |
| client::rebuild_http_client, |
| model::{userinfo::TokenProfile, ApiStatus}, |
| utils::{generate_checksum_with_repair, parse_bool_from_env, parse_string_from_env}, |
| }, |
| }; |
| use parking_lot::RwLock; |
| use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize}; |
| use serde::{Deserialize, Serialize}; |
| use std::sync::LazyLock; |
|
|
| mod usage_check; |
| pub use usage_check::UsageCheck; |
| mod config; |
| mod proxies; |
| pub use proxies::Proxies; |
| mod build_key; |
| pub use build_key::*; |
|
|
| use super::constant::{STATUS_FAILED, STATUS_PENDING, STATUS_SUCCESS}; |
|
|
| |
| #[derive(Clone, Serialize, Deserialize, Archive, RkyvDeserialize, RkyvSerialize)] |
| #[serde(tag = "type", content = "content")] |
| pub enum PageContent { |
| #[serde(rename = "default")] |
| Default, |
| #[serde(rename = "text")] |
| Text(String), |
| #[serde(rename = "html")] |
| Html(String), |
| } |
|
|
| impl Default for PageContent { |
| fn default() -> Self { |
| Self::Default |
| } |
| } |
|
|
| |
| #[derive(Default, Clone)] |
| pub struct AppConfig { |
| vision_ability: VisionAbility, |
| slow_pool: bool, |
| allow_claude: bool, |
| pages: Pages, |
| usage_check: UsageCheck, |
| dynamic_key: bool, |
| share_token: String, |
| is_share: bool, |
| proxies: Proxies, |
| web_refs: bool, |
| } |
|
|
| #[derive(Serialize, Deserialize, Clone, Copy, PartialEq)] |
| pub enum VisionAbility { |
| #[serde(rename = "none", alias = "disabled")] |
| None, |
| #[serde(rename = "base64", alias = "base64-only")] |
| Base64, |
| #[serde(rename = "all", alias = "base64-http")] |
| All, |
| } |
|
|
| impl VisionAbility { |
| pub fn from_str(s: &str) -> Self { |
| match s.to_lowercase().as_str() { |
| "none" | "disabled" => Self::None, |
| "base64" | "base64-only" => Self::Base64, |
| "all" | "base64-http" => Self::All, |
| _ => Self::default(), |
| } |
| } |
|
|
| pub fn is_none(&self) -> bool { |
| matches!(self, VisionAbility::None) |
| } |
| } |
|
|
| impl Default for VisionAbility { |
| fn default() -> Self { |
| Self::Base64 |
| } |
| } |
|
|
| #[derive(Clone, Default, Archive, RkyvDeserialize, RkyvSerialize)] |
| pub struct Pages { |
| pub root_content: PageContent, |
| pub logs_content: PageContent, |
| pub config_content: PageContent, |
| pub tokeninfo_content: PageContent, |
| pub shared_styles_content: PageContent, |
| pub shared_js_content: PageContent, |
| pub about_content: PageContent, |
| pub readme_content: PageContent, |
| pub api_content: PageContent, |
| pub build_key_content: PageContent, |
| } |
|
|
| |
| pub struct AppState { |
| pub total_requests: u64, |
| pub active_requests: u64, |
| pub error_requests: u64, |
| pub request_logs: Vec<RequestLog>, |
| pub token_infos: Vec<TokenInfo>, |
| } |
|
|
| |
| pub static APP_CONFIG: LazyLock<RwLock<AppConfig>> = |
| LazyLock::new(|| RwLock::new(AppConfig::default())); |
|
|
| macro_rules! config_methods { |
| ($($field:ident: $type:ty, $default:expr;)*) => { |
| $( |
| paste::paste! { |
| pub fn [<get_ $field>]() -> $type |
| where |
| $type: Copy + PartialEq, |
| { |
| APP_CONFIG.read().$field |
| } |
|
|
| pub fn [<update_ $field>](value: $type) |
| where |
| $type: Copy + PartialEq, |
| { |
| let current = Self::[<get_ $field>](); |
| if current != value { |
| APP_CONFIG.write().$field = value; |
| } |
| } |
|
|
| pub fn [<reset_ $field>]() |
| where |
| $type: Copy + PartialEq, |
| { |
| let default_value = $default; |
| let current = Self::[<get_ $field>](); |
| if current != default_value { |
| APP_CONFIG.write().$field = default_value; |
| } |
| } |
| } |
| )* |
| }; |
| } |
|
|
| macro_rules! config_methods_clone { |
| ($($field:ident: $type:ty, $default:expr;)*) => { |
| $( |
| paste::paste! { |
| pub fn [<get_ $field>]() -> $type |
| where |
| $type: Clone + PartialEq, |
| { |
| APP_CONFIG.read().$field.clone() |
| } |
|
|
| pub fn [<update_ $field>](value: $type) |
| where |
| $type: Clone + PartialEq, |
| { |
| let current = Self::[<get_ $field>](); |
| if current != value { |
| APP_CONFIG.write().$field = value; |
| } |
| } |
|
|
| pub fn [<reset_ $field>]() |
| where |
| $type: Clone + PartialEq, |
| { |
| let default_value = $default; |
| let current = Self::[<get_ $field>](); |
| if current != default_value { |
| APP_CONFIG.write().$field = default_value; |
| } |
| } |
| } |
| )* |
| }; |
| } |
|
|
| impl AppConfig { |
| pub fn init() { |
| let mut config = APP_CONFIG.write(); |
| config.vision_ability = |
| VisionAbility::from_str(&parse_string_from_env("VISION_ABILITY", EMPTY_STRING)); |
| config.slow_pool = parse_bool_from_env("ENABLE_SLOW_POOL", false); |
| config.allow_claude = parse_bool_from_env("PASS_ANY_CLAUDE", false); |
| config.usage_check = |
| UsageCheck::from_str(&parse_string_from_env("USAGE_CHECK", EMPTY_STRING)); |
| config.dynamic_key = parse_bool_from_env("DYNAMIC_KEY", false); |
| config.share_token = parse_string_from_env("SHARED_TOKEN", EMPTY_STRING); |
| config.is_share = !config.share_token.is_empty(); |
| config.proxies = match std::env::var("PROXIES") { |
| Ok(proxies) => Proxies::from_str(proxies.as_str()), |
| Err(_) => Proxies::default(), |
| }; |
| config.web_refs = parse_bool_from_env("INCLUDE_WEB_REFERENCES", false) |
| } |
|
|
| config_methods! { |
| slow_pool: bool, false; |
| allow_claude: bool, false; |
| dynamic_key: bool, false; |
| web_refs: bool, false; |
| } |
|
|
| config_methods_clone! { |
| vision_ability: VisionAbility, VisionAbility::default(); |
| usage_check: UsageCheck, UsageCheck::default(); |
| } |
|
|
| pub fn get_share_token() -> String { |
| APP_CONFIG.read().share_token.clone() |
| } |
|
|
| pub fn update_share_token(value: String) { |
| let current = Self::get_share_token(); |
| if current != value { |
| let mut config = APP_CONFIG.write(); |
| config.share_token = value; |
| config.is_share = !config.share_token.is_empty(); |
| } |
| } |
|
|
| pub fn reset_share_token() { |
| let current = Self::get_share_token(); |
| if !current.is_empty() { |
| let mut config = APP_CONFIG.write(); |
| config.share_token = String::new(); |
| config.is_share = false; |
| } |
| } |
|
|
| pub fn get_proxies() -> Proxies { |
| APP_CONFIG.read().proxies.clone() |
| } |
|
|
| pub fn update_proxies(value: Proxies) { |
| let current = Self::get_proxies(); |
| if current != value { |
| let mut config = APP_CONFIG.write(); |
| config.proxies = value; |
| rebuild_http_client(); |
| } |
| } |
|
|
| pub fn reset_proxies() { |
| let default_value = Proxies::default(); |
| let current = Self::get_proxies(); |
| if current != default_value { |
| let mut config = APP_CONFIG.write(); |
| config.proxies = default_value; |
| rebuild_http_client(); |
| } |
| } |
|
|
| pub fn get_page_content(path: &str) -> Option<PageContent> { |
| match path { |
| ROUTE_ROOT_PATH => Some(APP_CONFIG.read().pages.root_content.clone()), |
| ROUTE_LOGS_PATH => Some(APP_CONFIG.read().pages.logs_content.clone()), |
| ROUTE_CONFIG_PATH => Some(APP_CONFIG.read().pages.config_content.clone()), |
| ROUTE_TOKENS_PATH => Some(APP_CONFIG.read().pages.tokeninfo_content.clone()), |
| ROUTE_SHARED_STYLES_PATH => Some(APP_CONFIG.read().pages.shared_styles_content.clone()), |
| ROUTE_SHARED_JS_PATH => Some(APP_CONFIG.read().pages.shared_js_content.clone()), |
| ROUTE_ABOUT_PATH => Some(APP_CONFIG.read().pages.about_content.clone()), |
| ROUTE_README_PATH => Some(APP_CONFIG.read().pages.readme_content.clone()), |
| ROUTE_API_PATH => Some(APP_CONFIG.read().pages.api_content.clone()), |
| ROUTE_BUILD_KEY_PATH => Some(APP_CONFIG.read().pages.build_key_content.clone()), |
| _ => None, |
| } |
| } |
|
|
| pub fn update_page_content(path: &str, content: PageContent) -> Result<(), &'static str> { |
| let mut config = APP_CONFIG.write(); |
| match path { |
| ROUTE_ROOT_PATH => config.pages.root_content = content, |
| ROUTE_LOGS_PATH => config.pages.logs_content = content, |
| ROUTE_CONFIG_PATH => config.pages.config_content = content, |
| ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = content, |
| ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = content, |
| ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = content, |
| ROUTE_ABOUT_PATH => config.pages.about_content = content, |
| ROUTE_README_PATH => config.pages.readme_content = content, |
| ROUTE_API_PATH => config.pages.api_content = content, |
| ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = content, |
| _ => return Err(ERR_INVALID_PATH), |
| } |
| Ok(()) |
| } |
|
|
| pub fn reset_page_content(path: &str) -> Result<(), &'static str> { |
| let mut config = APP_CONFIG.write(); |
| match path { |
| ROUTE_ROOT_PATH => config.pages.root_content = PageContent::default(), |
| ROUTE_LOGS_PATH => config.pages.logs_content = PageContent::default(), |
| ROUTE_CONFIG_PATH => config.pages.config_content = PageContent::default(), |
| ROUTE_TOKENS_PATH => config.pages.tokeninfo_content = PageContent::default(), |
| ROUTE_SHARED_STYLES_PATH => config.pages.shared_styles_content = PageContent::default(), |
| ROUTE_SHARED_JS_PATH => config.pages.shared_js_content = PageContent::default(), |
| ROUTE_ABOUT_PATH => config.pages.about_content = PageContent::default(), |
| ROUTE_README_PATH => config.pages.readme_content = PageContent::default(), |
| ROUTE_API_PATH => config.pages.api_content = PageContent::default(), |
| ROUTE_BUILD_KEY_PATH => config.pages.build_key_content = PageContent::default(), |
| _ => return Err(ERR_INVALID_PATH), |
| } |
| Ok(()) |
| } |
|
|
| pub fn is_share() -> bool { |
| APP_CONFIG.read().is_share |
| } |
| } |
|
|
| impl AppState { |
| pub fn new(token_infos: Vec<TokenInfo>) -> Self { |
| |
| let request_logs = tokio::task::block_in_place(|| { |
| tokio::runtime::Handle::current() |
| .block_on(async { Self::load_saved_logs().await.unwrap_or_default() }) |
| }); |
|
|
| Self { |
| total_requests: request_logs.len() as u64, |
| active_requests: 0, |
| error_requests: request_logs |
| .iter() |
| .filter(|log| matches!(log.status, LogStatus::Failed)) |
| .count() as u64, |
| request_logs, |
| token_infos, |
| } |
| } |
|
|
| pub fn update_checksum(&mut self) { |
| for token_info in self.token_infos.iter_mut() { |
| token_info.checksum = generate_checksum_with_repair(&token_info.checksum); |
| } |
| } |
| } |
|
|
| #[derive(Clone, Archive, RkyvDeserialize, RkyvSerialize)] |
| pub enum LogStatus { |
| Pending, |
| Success, |
| Failed, |
| } |
|
|
| impl Serialize for LogStatus { |
| fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
| where |
| S: serde::Serializer, |
| { |
| serializer.serialize_str(self.as_str_name()) |
| } |
| } |
|
|
| impl LogStatus { |
| pub fn as_str_name(&self) -> &'static str { |
| match self { |
| Self::Pending => STATUS_PENDING, |
| Self::Success => STATUS_SUCCESS, |
| Self::Failed => STATUS_FAILED, |
| } |
| } |
|
|
| pub fn from_str_name(s: &str) -> Option<Self> { |
| match s { |
| STATUS_PENDING => Some(Self::Pending), |
| STATUS_SUCCESS => Some(Self::Success), |
| STATUS_FAILED => Some(Self::Failed), |
| _ => None, |
| } |
| } |
| } |
|
|
| |
| #[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] |
| pub struct RequestLog { |
| pub id: u64, |
| pub timestamp: chrono::DateTime<chrono::Local>, |
| pub model: String, |
| pub token_info: TokenInfo, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub prompt: Option<String>, |
| pub timing: TimingInfo, |
| pub stream: bool, |
| pub status: LogStatus, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub error: Option<String>, |
| } |
|
|
| #[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] |
| pub struct TimingInfo { |
| pub total: f64, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub first: Option<f64>, |
| } |
|
|
| |
| #[derive(Deserialize)] |
| pub struct ChatRequest { |
| pub model: String, |
| pub messages: Vec<Message>, |
| #[serde(default)] |
| pub stream: bool, |
| } |
|
|
| |
| #[derive(Serialize, Clone, Archive, RkyvDeserialize, RkyvSerialize)] |
| pub struct TokenInfo { |
| pub token: String, |
| pub checksum: String, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub profile: Option<TokenProfile>, |
| } |
|
|
| |
| #[derive(Deserialize)] |
| pub struct TokenUpdateRequest { |
| pub tokens: String, |
| } |
|
|
| #[derive(Deserialize)] |
| pub struct TokenAddRequestTokenInfo { |
| pub token: String, |
| #[serde(default)] |
| pub checksum: Option<String>, |
| } |
|
|
| |
| #[derive(Deserialize)] |
| pub struct TokensDeleteRequest { |
| #[serde(default)] |
| pub tokens: Vec<String>, |
| #[serde(default)] |
| pub expectation: TokensDeleteResponseExpectation, |
| } |
|
|
| #[derive(Deserialize, Default)] |
| #[serde(rename_all = "snake_case")] |
| pub enum TokensDeleteResponseExpectation { |
| #[default] |
| Simple, |
| UpdatedTokens, |
| FailedTokens, |
| Detailed, |
| } |
|
|
| impl TokensDeleteResponseExpectation { |
| pub fn needs_updated_tokens(&self) -> bool { |
| matches!( |
| self, |
| TokensDeleteResponseExpectation::UpdatedTokens |
| | TokensDeleteResponseExpectation::Detailed |
| ) |
| } |
|
|
| pub fn needs_failed_tokens(&self) -> bool { |
| matches!( |
| self, |
| TokensDeleteResponseExpectation::FailedTokens |
| | TokensDeleteResponseExpectation::Detailed |
| ) |
| } |
| } |
|
|
| |
| #[derive(Serialize)] |
| pub struct TokensDeleteResponse { |
| pub status: ApiStatus, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub updated_tokens: Option<Vec<String>>, |
| #[serde(skip_serializing_if = "Option::is_none")] |
| pub failed_tokens: Option<Vec<String>>, |
| } |
|
|