use std::sync::Arc; use tokio::sync::Mutex as TokioMutex; use crate::config::{self, AppSettingsMap, Settings}; use crate::constants; use crate::database::DbPool; use crate::events::BroadcastEventBus; use crate::telegram::bot_polling; pub struct BotState { pub bot_ready: bool, pub bot_running: bool, pub bot_error: Option, pub app_settings: AppSettingsMap, pub shutdown_tx: Option>, } pub struct AppState { pub settings: Settings, pub tera: tera::Tera, pub http_client: reqwest::Client, pub db_pool: DbPool, pub event_bus: BroadcastEventBus, pub bot_state: TokioMutex, pub settings_lock: TokioMutex<()>, } impl AppState { pub fn new( settings: Settings, tera: tera::Tera, http_client: reqwest::Client, db_pool: DbPool, app_settings: AppSettingsMap, bot_ready: bool, ) -> Self { Self { settings, tera, http_client, db_pool, event_bus: BroadcastEventBus::new(constants::EVENT_BUS_CAPACITY), bot_state: TokioMutex::new(BotState { bot_ready, bot_running: false, bot_error: None, app_settings, shutdown_tx: None, }), settings_lock: TokioMutex::new(()), } } } pub async fn start_bot(state: Arc) -> Result<(), String> { let mut bot = state.bot_state.lock().await; let token = bot .app_settings .get("BOT_TOKEN") .and_then(|v| v.clone()) .unwrap_or_default(); let channel = bot .app_settings .get("CHANNEL_NAME") .and_then(|v| v.clone()) .unwrap_or_default(); if token.is_empty() || channel.is_empty() { return Err("BOT_TOKEN or CHANNEL_NAME not configured".into()); } let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); let event_bus = state.event_bus.clone(); let db_pool = state.db_pool.clone(); let base_url = bot .app_settings .get("BASE_URL") .and_then(|v| v.clone()) .unwrap_or_default(); let token_clone = token.clone(); let channel_clone = channel.clone(); let http_client = state.http_client.clone(); tokio::spawn(async move { bot_polling::run_bot_polling( token_clone, channel_clone, db_pool, event_bus, base_url, http_client, shutdown_rx, ) .await; }); bot.shutdown_tx = Some(shutdown_tx); bot.bot_running = true; bot.bot_error = None; tracing::info!("机器人已在后台启动"); Ok(()) } pub async fn stop_bot(state: &AppState) { let mut bot = state.bot_state.lock().await; if let Some(tx) = bot.shutdown_tx.take() { let _ = tx.send(()); } bot.bot_running = false; tracing::info!("机器人已停止"); } pub async fn apply_runtime_settings( state: Arc, start_bot_flag: bool, ) -> Result<(), String> { let _lock = state.settings_lock.lock().await; let current = config::get_app_settings(&state.settings, &state.db_pool); let bot_ready = config::is_bot_ready(¤t); // Soft refresh path: the caller only wants to pick up updated // `app_settings` (e.g. after `/api/auth/login` rotates SESSION_TOKEN). // Previously this code stopped the running bot even for soft refreshes, // which meant logging in as the admin would silently kill the Telegram // bot. Now we only update the in-memory snapshot and leave the bot alone. if !start_bot_flag { let mut bot = state.bot_state.lock().await; bot.app_settings = current; bot.bot_ready = bot_ready; // Do not clobber an existing bot_error on a soft refresh. return Ok(()); } // Hard apply path: stop the bot, swap config, and restart if ready. stop_bot(&state).await; { let mut bot = state.bot_state.lock().await; bot.app_settings = current; bot.bot_ready = bot_ready; bot.bot_error = None; } if bot_ready { if let Err(e) = self::start_bot(state.clone()).await { tracing::error!("应用配置已应用,但启动机器人失败: {}", e); let mut bot = state.bot_state.lock().await; bot.bot_error = Some(e.to_string()); return Err(e); } } Ok(()) }