2025-10-26 00:02:19 -03:00
|
|
|
#![cfg_attr(feature = "desktop", windows_subsystem = "windows")]
|
2025-11-20 13:28:35 -03:00
|
|
|
use axum::{
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
Router,
|
|
|
|
|
};
|
2025-10-11 12:29:03 -03:00
|
|
|
use dotenvy::dotenv;
|
2025-11-08 07:04:44 -03:00
|
|
|
use log::{error, info};
|
2025-10-12 20:12:49 -03:00
|
|
|
use std::collections::HashMap;
|
2025-11-20 13:28:35 -03:00
|
|
|
use std::net::SocketAddr;
|
2025-11-11 09:42:52 -03:00
|
|
|
use std::sync::Arc;
|
2025-11-20 13:28:35 -03:00
|
|
|
use tower_http::cors::CorsLayer;
|
|
|
|
|
use tower_http::services::ServeDir;
|
|
|
|
|
use tower_http::trace::TraceLayer;
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-22 13:24:53 -03:00
|
|
|
mod api_router;
|
2025-11-22 22:54:45 -03:00
|
|
|
use botserver::basic;
|
|
|
|
|
use botserver::core;
|
|
|
|
|
use botserver::shared;
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
include!("main.test.rs");
|
|
|
|
|
}
|
|
|
|
|
#[cfg(feature = "console")]
|
|
|
|
|
use botserver::console;
|
|
|
|
|
|
|
|
|
|
// Re-exports from core
|
|
|
|
|
use botserver::core::automation;
|
|
|
|
|
use botserver::core::bootstrap;
|
|
|
|
|
use botserver::core::bot;
|
|
|
|
|
use botserver::core::config;
|
|
|
|
|
use botserver::core::package_manager;
|
|
|
|
|
use botserver::core::session;
|
2025-11-23 20:12:09 -03:00
|
|
|
use botserver::core::ui_server;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
|
|
|
|
// Feature-gated modules
|
|
|
|
|
#[cfg(feature = "attendance")]
|
|
|
|
|
mod attendance;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "calendar")]
|
|
|
|
|
mod calendar;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "compliance")]
|
|
|
|
|
mod compliance;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "console")]
|
|
|
|
|
mod console;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "desktop")]
|
|
|
|
|
mod desktop;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
mod directory;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "drive")]
|
2025-11-22 12:26:16 -03:00
|
|
|
mod drive;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
2025-10-07 10:53:09 -03:00
|
|
|
#[cfg(feature = "email")]
|
2025-10-06 10:30:17 -03:00
|
|
|
mod email;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
|
|
|
|
#[cfg(feature = "instagram")]
|
|
|
|
|
mod instagram;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "llm")]
|
2025-11-07 16:40:19 -03:00
|
|
|
mod llm;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
|
|
|
|
#[cfg(feature = "meet")]
|
2025-10-18 12:01:39 -03:00
|
|
|
mod meet;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
|
|
|
|
#[cfg(feature = "msteams")]
|
|
|
|
|
mod msteams;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "nvidia")]
|
2025-11-07 16:40:19 -03:00
|
|
|
mod nvidia;
|
2025-11-22 22:54:45 -03:00
|
|
|
|
|
|
|
|
#[cfg(feature = "tasks")]
|
|
|
|
|
mod tasks;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "vectordb")]
|
|
|
|
|
mod vector_db;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "weba")]
|
|
|
|
|
mod weba;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "whatsapp")]
|
|
|
|
|
mod whatsapp;
|
|
|
|
|
|
2025-10-16 11:43:02 -03:00
|
|
|
use crate::automation::AutomationService;
|
2025-10-18 19:08:00 -03:00
|
|
|
use crate::bootstrap::BootstrapManager;
|
2025-10-07 10:53:09 -03:00
|
|
|
#[cfg(feature = "email")]
|
2025-10-19 11:08:23 -03:00
|
|
|
use crate::email::{
|
`@media (prefers-color-scheme: dark)`
- ✅ Enhanced accessibility features (focus states, reduced motion)
- ✅ Added connection status component styles
- ✅ Improved responsive design
- ✅ Added utility classes for common patterns
- ✅ Added semantic HTML5 elements (`<header>`, `<main>`, `<nav>`)
- ✅ Comprehensive ARIA labels and roles for accessibility
- ✅ Keyboard navigation support (Alt+1-4 for sections, Esc for menus)
- ✅ Better event handling and state management
- ✅ Theme change subscriber with meta theme-color sync
- ✅ Online/offline connection monitoring
- ✅ Enhanced console logging with app info
- ✅ `THEMES.md` (400+ lines) - Complete theme system guide
- ✅ `README.md` (433+ lines) - Main application documentation
- ✅ `COMPONENTS.md` (773+ lines) - UI component library reference
- ✅ `QUICKSTART.md` (359+ lines) - Quick start guide for developers
- ✅ `REBUILD_NOTES.md` - This summary document
**Theme files define base colors:** ```css :root { --primary: 217 91%
60%; /* HSL: blue */ --background: 0 0% 100%; /* HSL: white */ } ```
**App.css bridges to working variables:** ```css :root { --accent-color:
hsl(var(--primary)); --primary-bg: hsl(var(--background));
--accent-light: hsla(var(--primary) / 0.1); } ```
**Components use working variables:** ```css .button { background:
var(--accent-color); color: hsl(var(--primary-foreground)); } ```
- ✅ Keyboard shortcuts (Alt+1-4, Esc)
- ✅ System dark mode detection
- ✅ Theme change event subscription
- ✅ Automatic document title updates
- ✅ Meta theme-color synchronization
- ✅ Enhanced console logging
- ✅ Better error handling
- ✅ Improved accessibility
- ✅ Theme switching via dropdown
- ✅ Theme persistence to localStorage
- ✅ Apps menu with section switching
- ✅ Dynamic section loading (Chat, Drive, Tasks, Mail)
- ✅ WebSocket chat functionality
- ✅ Alpine.js integration for other modules
- ✅ Responsive design
- ✅ Loading states
- [x] Theme switching works across all 19 themes
- [x] All sections load correctly
- [x] Keyboard shortcuts functional
- [x] Responsive on mobile/tablet/desktop
- [x] Accessibility features working
- [x] No console errors
- [x] Theme persistence works
- [x] Dark mode detection works
``` documentation/ ├── README.md # Main docs - start here ├──
QUICKSTART.md # 5-minute guide ├── THEMES.md # Theme system details ├──
COMPONENTS.md # UI component library └── REBUILD_NOTES.md # This summary
```
1. **HSL Bridge System**: Allows theme files to use shadcn-style HSL
variables while the app automatically derives working CSS properties
2. **No Breaking Changes**: All existing functionality preserved and
enhanced
3. **Developer-Friendly**: Comprehensive documentation for customization
4. **Accessibility First**: ARIA labels, keyboard navigation, focus
management
5. **Performance Optimized**: Instant theme switching, minimal reflows
- **Rebuild**: ✅ Complete
- **Testing**: ✅ Passed
- **Documentation**: ✅ Complete
- **Production Ready**: ✅ Yes
The rebuild successfully integrates the theme system throughout the UI
while maintaining all functionality and adding comprehensive
documentation for future development.
2025-11-21 09:28:02 -03:00
|
|
|
add_email_account, delete_email_account, get_emails, get_latest_email_from,
|
|
|
|
|
list_email_accounts, list_emails, list_folders, save_click, save_draft, send_email,
|
2025-10-19 11:08:23 -03:00
|
|
|
};
|
2025-11-22 22:54:45 -03:00
|
|
|
use botserver::core::bot::channels::{VoiceAdapter, WebChannelAdapter};
|
|
|
|
|
use botserver::core::bot::websocket_handler;
|
|
|
|
|
use botserver::core::bot::BotOrchestrator;
|
|
|
|
|
use botserver::core::config::AppConfig;
|
|
|
|
|
// use crate::file::upload_file; // Module doesn't exist
|
|
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
use crate::directory::auth_handler;
|
|
|
|
|
#[cfg(feature = "meet")]
|
2025-10-18 12:01:39 -03:00
|
|
|
use crate::meet::{voice_start, voice_stop};
|
2025-10-18 19:08:00 -03:00
|
|
|
use crate::package_manager::InstallMode;
|
2025-11-02 07:50:57 -03:00
|
|
|
use crate::session::{create_session, get_session_history, get_sessions, start_session};
|
2025-10-11 20:02:14 -03:00
|
|
|
use crate::shared::state::AppState;
|
2025-11-11 09:42:52 -03:00
|
|
|
use crate::shared::utils::create_conn;
|
2025-11-11 15:01:57 -03:00
|
|
|
use crate::shared::utils::create_s3_operator;
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-08 07:04:44 -03:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum BootstrapProgress {
|
2025-11-11 09:42:52 -03:00
|
|
|
StartingBootstrap,
|
|
|
|
|
InstallingComponent(String),
|
|
|
|
|
StartingComponent(String),
|
|
|
|
|
UploadingTemplates,
|
|
|
|
|
ConnectingDatabase,
|
|
|
|
|
StartingLLM,
|
|
|
|
|
BootstrapComplete,
|
|
|
|
|
BootstrapError(String),
|
2025-11-08 07:04:44 -03:00
|
|
|
}
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-20 13:28:35 -03:00
|
|
|
async fn run_axum_server(
|
2025-11-19 14:00:57 -03:00
|
|
|
app_state: Arc<AppState>,
|
|
|
|
|
port: u16,
|
2025-11-20 13:28:35 -03:00
|
|
|
_worker_count: usize,
|
2025-11-19 14:00:57 -03:00
|
|
|
) -> std::io::Result<()> {
|
2025-11-20 13:28:35 -03:00
|
|
|
// CORS configuration
|
|
|
|
|
let cors = CorsLayer::new()
|
|
|
|
|
.allow_origin(tower_http::cors::Any)
|
|
|
|
|
.allow_methods(tower_http::cors::Any)
|
|
|
|
|
.allow_headers(tower_http::cors::Any)
|
|
|
|
|
.max_age(std::time::Duration::from_secs(3600));
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Use unified API router configuration
|
|
|
|
|
let mut api_router = crate::api_router::configure_api_routes();
|
|
|
|
|
|
|
|
|
|
// Add session-specific routes
|
|
|
|
|
api_router = api_router
|
2025-11-20 13:28:35 -03:00
|
|
|
.route("/api/sessions", post(create_session))
|
|
|
|
|
.route("/api/sessions", get(get_sessions))
|
|
|
|
|
.route(
|
|
|
|
|
"/api/sessions/{session_id}/history",
|
|
|
|
|
get(get_session_history),
|
|
|
|
|
)
|
2025-11-26 22:54:22 -03:00
|
|
|
.route("/api/sessions/{session_id}/start", post(start_session))
|
|
|
|
|
// WebSocket route
|
|
|
|
|
.route("/ws", get(websocket_handler));
|
2025-11-22 22:54:45 -03:00
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Add feature-specific routes
|
2025-11-22 22:54:45 -03:00
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
{
|
|
|
|
|
api_router = api_router.route("/api/auth", get(auth_handler));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "meet")]
|
|
|
|
|
{
|
|
|
|
|
api_router = api_router
|
|
|
|
|
.route("/api/voice/start", post(voice_start))
|
|
|
|
|
.route("/api/voice/stop", post(voice_stop))
|
2025-11-26 22:54:22 -03:00
|
|
|
.route("/ws/meet", get(crate::meet::meeting_websocket))
|
|
|
|
|
.merge(crate::meet::configure());
|
2025-11-22 22:54:45 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
#[cfg(feature = "email")]
|
2025-11-22 22:54:45 -03:00
|
|
|
{
|
2025-11-26 22:54:22 -03:00
|
|
|
api_router = api_router.merge(crate::email::configure());
|
2025-11-22 22:54:45 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Add calendar routes with CalDAV if feature is enabled
|
|
|
|
|
#[cfg(feature = "calendar")]
|
|
|
|
|
{
|
|
|
|
|
let calendar_engine =
|
|
|
|
|
Arc::new(crate::calendar::CalendarEngine::new(app_state.conn.clone()));
|
|
|
|
|
|
|
|
|
|
// Start reminder job
|
|
|
|
|
let reminder_engine = Arc::clone(&calendar_engine);
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
crate::calendar::start_reminder_job(reminder_engine).await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add CalDAV router
|
|
|
|
|
api_router = api_router.merge(crate::calendar::caldav::create_caldav_router(
|
|
|
|
|
calendar_engine,
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-11-22 12:26:16 -03:00
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Add task engine routes
|
|
|
|
|
let task_engine = Arc::new(crate::tasks::TaskEngine::new(app_state.conn.clone()));
|
|
|
|
|
api_router = api_router.merge(crate::tasks::configure_task_routes(task_engine));
|
2025-11-20 13:28:35 -03:00
|
|
|
|
|
|
|
|
// Build static file serving
|
|
|
|
|
let static_path = std::path::Path::new("./web/desktop");
|
|
|
|
|
|
|
|
|
|
let app = Router::new()
|
2025-11-20 14:28:21 -03:00
|
|
|
// Static file services must come first to match before other routes
|
2025-11-20 13:28:35 -03:00
|
|
|
.nest_service("/js", ServeDir::new(static_path.join("js")))
|
|
|
|
|
.nest_service("/css", ServeDir::new(static_path.join("css")))
|
2025-11-20 20:39:20 -03:00
|
|
|
.nest_service("/public", ServeDir::new(static_path.join("public")))
|
2025-11-20 13:28:35 -03:00
|
|
|
.nest_service("/drive", ServeDir::new(static_path.join("drive")))
|
|
|
|
|
.nest_service("/chat", ServeDir::new(static_path.join("chat")))
|
|
|
|
|
.nest_service("/mail", ServeDir::new(static_path.join("mail")))
|
|
|
|
|
.nest_service("/tasks", ServeDir::new(static_path.join("tasks")))
|
2025-11-20 14:28:21 -03:00
|
|
|
// API routes
|
2025-11-26 22:54:22 -03:00
|
|
|
.merge(api_router.with_state(app_state.clone()))
|
2025-11-20 14:28:21 -03:00
|
|
|
// Root index route - only matches exact "/"
|
2025-11-23 20:12:09 -03:00
|
|
|
.route("/", get(crate::ui_server::index))
|
2025-11-20 14:28:21 -03:00
|
|
|
// Layers
|
2025-11-20 13:28:35 -03:00
|
|
|
.layer(cors)
|
|
|
|
|
.layer(TraceLayer::new_for_http());
|
|
|
|
|
|
|
|
|
|
// Bind to address
|
|
|
|
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
|
|
|
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
|
|
|
|
|
|
|
|
|
info!("HTTP server listening on {}", addr);
|
|
|
|
|
|
|
|
|
|
// Serve the app
|
|
|
|
|
axum::serve(listener, app.into_make_service())
|
2025-11-19 14:00:57 -03:00
|
|
|
.await
|
2025-11-20 13:28:35 -03:00
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
2025-11-19 14:00:57 -03:00
|
|
|
}
|
|
|
|
|
|
2025-10-19 14:02:47 -03:00
|
|
|
#[tokio::main]
|
2025-10-16 11:43:02 -03:00
|
|
|
async fn main() -> std::io::Result<()> {
|
2025-11-11 15:01:57 -03:00
|
|
|
dotenv().ok();
|
|
|
|
|
println!(
|
|
|
|
|
"Starting {} {}...",
|
|
|
|
|
std::env::var("PLATFORM_NAME").unwrap_or("General Bots".to_string()),
|
|
|
|
|
env!("CARGO_PKG_VERSION")
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
use crate::llm::local::ensure_llama_servers_running;
|
|
|
|
|
use botserver::config::ConfigManager;
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let args: Vec<String> = std::env::args().collect();
|
|
|
|
|
let no_ui = args.contains(&"--noui".to_string());
|
2025-11-14 16:54:55 -03:00
|
|
|
let desktop_mode = args.contains(&"--desktop".to_string());
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
dotenv().ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::<BootstrapProgress>();
|
|
|
|
|
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
2025-11-15 09:48:46 -03:00
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
// Handle CLI commands
|
2025-11-15 19:08:26 -03:00
|
|
|
if args.len() > 1 {
|
|
|
|
|
let command = &args[1];
|
|
|
|
|
match command.as_str() {
|
2025-11-20 13:28:35 -03:00
|
|
|
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
|
|
|
|
|
| "-h" => match package_manager::cli::run().await {
|
2025-11-15 19:08:26 -03:00
|
|
|
Ok(_) => return Ok(()),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("CLI error: {}", e);
|
|
|
|
|
return Err(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::Other,
|
|
|
|
|
format!("CLI command failed: {}", e),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-11-19 14:00:57 -03:00
|
|
|
_ => {}
|
2025-11-15 19:08:26 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
// Start UI thread if not in no-ui mode and not in desktop mode
|
|
|
|
|
let ui_handle = if !no_ui && !desktop_mode {
|
2025-11-11 09:42:52 -03:00
|
|
|
let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx));
|
|
|
|
|
let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
Some(
|
|
|
|
|
std::thread::Builder::new()
|
|
|
|
|
.name("ui-thread".to_string())
|
|
|
|
|
.spawn(move || {
|
2025-11-22 22:54:45 -03:00
|
|
|
#[cfg(feature = "console")]
|
|
|
|
|
{
|
|
|
|
|
let mut ui = botserver::console::XtreeUI::new();
|
|
|
|
|
ui.set_progress_channel(progress_rx.clone());
|
|
|
|
|
|
|
|
|
|
let rt = tokio::runtime::Builder::new_current_thread()
|
|
|
|
|
.enable_all()
|
|
|
|
|
.build()
|
|
|
|
|
.expect("Failed to create UI runtime");
|
|
|
|
|
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
result = async {
|
|
|
|
|
let mut rx = state_rx.lock().await;
|
|
|
|
|
rx.recv().await
|
|
|
|
|
} => {
|
|
|
|
|
if let Some(app_state) = result {
|
|
|
|
|
ui.set_app_state(app_state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
|
|
|
|
|
eprintln!("UI initialization timeout");
|
2025-11-19 14:00:57 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-22 22:54:45 -03:00
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-22 22:54:45 -03:00
|
|
|
if let Err(e) = ui.start_ui() {
|
|
|
|
|
eprintln!("UI error: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(feature = "console"))]
|
|
|
|
|
{
|
|
|
|
|
eprintln!("Console feature not enabled");
|
2025-11-11 09:42:52 -03:00
|
|
|
}
|
2025-11-19 14:00:57 -03:00
|
|
|
})
|
|
|
|
|
.expect("Failed to spawn UI thread"),
|
|
|
|
|
)
|
2025-11-11 09:42:52 -03:00
|
|
|
} else {
|
|
|
|
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
|
|
|
|
.write_style(env_logger::WriteStyle::Always)
|
|
|
|
|
.init();
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let install_mode = if args.contains(&"--container".to_string()) {
|
|
|
|
|
InstallMode::Container
|
|
|
|
|
} else {
|
|
|
|
|
InstallMode::Local
|
|
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
|
|
|
|
args.get(idx + 1).cloned()
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
// Bootstrap
|
2025-11-11 09:42:52 -03:00
|
|
|
let progress_tx_clone = progress_tx.clone();
|
|
|
|
|
let cfg = {
|
|
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::StartingBootstrap)
|
|
|
|
|
.ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
|
|
|
|
let env_path = std::env::current_dir().unwrap().join(".env");
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let cfg = if env_path.exists() {
|
2025-11-11 15:01:57 -03:00
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::StartingComponent(
|
|
|
|
|
"all services".to_string(),
|
|
|
|
|
))
|
|
|
|
|
.ok();
|
|
|
|
|
bootstrap
|
|
|
|
|
.start_all()
|
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
|
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::ConnectingDatabase)
|
|
|
|
|
.ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
match create_conn() {
|
2025-11-14 16:54:55 -03:00
|
|
|
Ok(pool) => AppConfig::from_database(&pool)
|
|
|
|
|
.unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")),
|
2025-11-11 09:42:52 -03:00
|
|
|
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
2025-10-18 19:08:00 -03:00
|
|
|
}
|
2025-11-11 09:42:52 -03:00
|
|
|
} else {
|
2025-11-12 12:48:06 -03:00
|
|
|
_ = bootstrap.bootstrap().await;
|
2025-11-11 15:01:57 -03:00
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::StartingComponent(
|
|
|
|
|
"all services".to_string(),
|
|
|
|
|
))
|
|
|
|
|
.ok();
|
|
|
|
|
bootstrap
|
|
|
|
|
.start_all()
|
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
2025-11-11 11:12:54 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
match create_conn() {
|
|
|
|
|
Ok(pool) => AppConfig::from_database(&pool)
|
|
|
|
|
.unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")),
|
|
|
|
|
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-11 15:01:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::UploadingTemplates)
|
|
|
|
|
.ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await {
|
|
|
|
|
progress_tx_clone
|
|
|
|
|
.send(BootstrapProgress::BootstrapError(format!(
|
|
|
|
|
"Failed to upload templates: {}",
|
|
|
|
|
e
|
|
|
|
|
)))
|
|
|
|
|
.ok();
|
2025-10-18 19:08:00 -03:00
|
|
|
}
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
Ok::<AppConfig, std::io::Error>(cfg)
|
2025-10-18 19:08:00 -03:00
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let cfg = cfg?;
|
|
|
|
|
dotenv().ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
|
|
|
|
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
progress_tx.send(BootstrapProgress::ConnectingDatabase).ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let pool = match create_conn() {
|
|
|
|
|
Ok(pool) => pool,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("Failed to create database pool: {}", e);
|
|
|
|
|
progress_tx
|
|
|
|
|
.send(BootstrapProgress::BootstrapError(format!(
|
|
|
|
|
"Database pool creation failed: {}",
|
|
|
|
|
e
|
|
|
|
|
)))
|
|
|
|
|
.ok();
|
|
|
|
|
return Err(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::ConnectionRefused,
|
|
|
|
|
format!("Database pool creation failed: {}", e),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-20 13:28:35 -03:00
|
|
|
let cache_url =
|
|
|
|
|
std::env::var("CACHE_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
2025-11-11 09:42:52 -03:00
|
|
|
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
|
|
|
|
Ok(client) => Some(Arc::new(client)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
log::warn!("Failed to connect to Redis: {}", e);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let web_adapter = Arc::new(WebChannelAdapter::new());
|
|
|
|
|
let voice_adapter = Arc::new(VoiceAdapter::new());
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 10:34:06 -03:00
|
|
|
let drive = create_s3_operator(&config.drive)
|
2025-11-11 09:42:52 -03:00
|
|
|
.await
|
|
|
|
|
.expect("Failed to initialize Drive");
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
|
|
|
|
|
pool.get().unwrap(),
|
|
|
|
|
redis_client.clone(),
|
|
|
|
|
)));
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
// Create default Zitadel config (can be overridden with env vars)
|
2025-11-22 22:54:45 -03:00
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
let zitadel_config = botserver::directory::client::ZitadelConfig {
|
2025-11-22 12:26:16 -03:00
|
|
|
issuer_url: std::env::var("ZITADEL_ISSUER_URL")
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
|
|
|
|
|
issuer: std::env::var("ZITADEL_ISSUER")
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
|
2025-11-22 22:54:45 -03:00
|
|
|
client_id: std::env::var("ZITADEL_CLIENT_ID").unwrap_or_else(|_| "client_id".to_string()),
|
2025-11-22 12:26:16 -03:00
|
|
|
client_secret: std::env::var("ZITADEL_CLIENT_SECRET")
|
2025-11-22 22:54:45 -03:00
|
|
|
.unwrap_or_else(|_| "client_secret".to_string()),
|
2025-11-22 12:26:16 -03:00
|
|
|
redirect_uri: std::env::var("ZITADEL_REDIRECT_URI")
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8080/callback".to_string()),
|
|
|
|
|
project_id: std::env::var("ZITADEL_PROJECT_ID").unwrap_or_else(|_| "default".to_string()),
|
2025-11-22 22:54:45 -03:00
|
|
|
api_url: std::env::var("ZITADEL_API_URL")
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
|
|
|
|
|
service_account_key: std::env::var("ZITADEL_SERVICE_ACCOUNT_KEY").ok(),
|
2025-11-22 12:26:16 -03:00
|
|
|
};
|
2025-11-22 22:54:45 -03:00
|
|
|
#[cfg(feature = "directory")]
|
|
|
|
|
let auth_service = Arc::new(tokio::sync::Mutex::new(
|
|
|
|
|
botserver::directory::AuthService::new(zitadel_config)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap(),
|
|
|
|
|
));
|
2025-11-11 09:42:52 -03:00
|
|
|
let config_manager = ConfigManager::new(pool.clone());
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let mut bot_conn = pool.get().expect("Failed to get database connection");
|
|
|
|
|
let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut bot_conn);
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let llm_url = config_manager
|
|
|
|
|
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8081".to_string());
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-26 15:27:47 -03:00
|
|
|
// Create base LLM provider
|
|
|
|
|
let base_llm_provider = Arc::new(botserver::llm::OpenAIClient::new(
|
2025-11-11 09:42:52 -03:00
|
|
|
"empty".to_string(),
|
|
|
|
|
Some(llm_url.clone()),
|
2025-11-22 22:54:45 -03:00
|
|
|
)) as Arc<dyn botserver::llm::LLMProvider>;
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-26 15:27:47 -03:00
|
|
|
// Wrap with cache if redis is available
|
|
|
|
|
let llm_provider: Arc<dyn botserver::llm::LLMProvider> = if let Some(ref cache) = redis_client {
|
|
|
|
|
// Set up embedding service for semantic matching
|
|
|
|
|
let embedding_url = config_manager
|
|
|
|
|
.get_config(
|
|
|
|
|
&default_bot_id,
|
|
|
|
|
"embedding-url",
|
|
|
|
|
Some("http://localhost:8082"),
|
|
|
|
|
)
|
|
|
|
|
.unwrap_or_else(|_| "http://localhost:8082".to_string());
|
|
|
|
|
let embedding_model = config_manager
|
|
|
|
|
.get_config(&default_bot_id, "embedding-model", Some("all-MiniLM-L6-v2"))
|
|
|
|
|
.unwrap_or_else(|_| "all-MiniLM-L6-v2".to_string());
|
|
|
|
|
|
|
|
|
|
let embedding_service = Some(Arc::new(botserver::llm::cache::LocalEmbeddingService::new(
|
|
|
|
|
embedding_url,
|
|
|
|
|
embedding_model,
|
|
|
|
|
))
|
|
|
|
|
as Arc<dyn botserver::llm::cache::EmbeddingService>);
|
|
|
|
|
|
|
|
|
|
// Create cache config
|
|
|
|
|
let cache_config = botserver::llm::cache::CacheConfig {
|
|
|
|
|
ttl: 3600, // 1 hour TTL
|
|
|
|
|
semantic_matching: true,
|
|
|
|
|
similarity_threshold: 0.85, // 85% similarity threshold
|
|
|
|
|
max_similarity_checks: 100,
|
|
|
|
|
key_prefix: "llm_cache".to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Arc::new(botserver::llm::cache::CachedLLMProvider::with_db_pool(
|
|
|
|
|
base_llm_provider,
|
|
|
|
|
cache.clone(),
|
|
|
|
|
cache_config,
|
|
|
|
|
embedding_service,
|
|
|
|
|
pool.clone(),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
base_llm_provider
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Initialize Knowledge Base Manager
|
|
|
|
|
let kb_manager = Arc::new(botserver::core::kb::KnowledgeBaseManager::new("work"));
|
|
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let app_state = Arc::new(AppState {
|
|
|
|
|
drive: Some(drive),
|
|
|
|
|
config: Some(cfg.clone()),
|
|
|
|
|
conn: pool.clone(),
|
|
|
|
|
bucket_name: "default.gbai".to_string(),
|
|
|
|
|
cache: redis_client.clone(),
|
|
|
|
|
session_manager: session_manager.clone(),
|
|
|
|
|
llm_provider: llm_provider.clone(),
|
2025-11-22 22:54:45 -03:00
|
|
|
#[cfg(feature = "directory")]
|
2025-11-11 09:42:52 -03:00
|
|
|
auth_service: auth_service.clone(),
|
|
|
|
|
channels: Arc::new(tokio::sync::Mutex::new({
|
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
|
map.insert(
|
|
|
|
|
"web".to_string(),
|
2025-11-22 22:54:45 -03:00
|
|
|
web_adapter.clone() as Arc<dyn botserver::core::bot::channels::ChannelAdapter>,
|
2025-11-11 09:42:52 -03:00
|
|
|
);
|
|
|
|
|
map
|
|
|
|
|
})),
|
|
|
|
|
response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
|
|
|
|
web_adapter: web_adapter.clone(),
|
|
|
|
|
voice_adapter: voice_adapter.clone(),
|
2025-11-26 22:54:22 -03:00
|
|
|
kb_manager: Some(kb_manager.clone()),
|
2025-11-11 09:42:52 -03:00
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-26 22:54:22 -03:00
|
|
|
// Start website crawler service
|
|
|
|
|
if let Err(e) = botserver::core::kb::ensure_crawler_service_running(app_state.clone()).await {
|
|
|
|
|
log::warn!("Failed to start website crawler service: {}", e);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
state_tx.send(app_state.clone()).await.ok();
|
|
|
|
|
progress_tx.send(BootstrapProgress::BootstrapComplete).ok();
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
info!(
|
|
|
|
|
"Starting HTTP server on {}:{}",
|
|
|
|
|
config.server.host, config.server.port
|
|
|
|
|
);
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-11 09:42:52 -03:00
|
|
|
let worker_count = std::thread::available_parallelism()
|
|
|
|
|
.map(|n| n.get())
|
|
|
|
|
.unwrap_or(4);
|
2025-11-15 09:48:46 -03:00
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
// Mount bots
|
2025-11-11 09:42:52 -03:00
|
|
|
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
|
|
|
|
error!("Failed to mount bots: {}", e);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
// Start automation service
|
2025-11-11 09:42:52 -03:00
|
|
|
let automation_state = app_state.clone();
|
2025-11-19 14:00:57 -03:00
|
|
|
tokio::spawn(async move {
|
|
|
|
|
let automation = AutomationService::new(automation_state);
|
|
|
|
|
automation.spawn().await.ok();
|
2025-11-11 09:42:52 -03:00
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
// Start LLM servers
|
2025-11-11 09:42:52 -03:00
|
|
|
let app_state_for_llm = app_state.clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
if let Err(e) = ensure_llama_servers_running(app_state_for_llm).await {
|
|
|
|
|
error!("Failed to start LLM servers: {}", e);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
// Handle desktop mode vs server mode
|
2025-11-14 16:54:55 -03:00
|
|
|
#[cfg(feature = "desktop")]
|
|
|
|
|
if desktop_mode {
|
2025-11-19 14:00:57 -03:00
|
|
|
// For desktop mode: Run HTTP server in a separate thread with its own runtime
|
|
|
|
|
let app_state_for_server = app_state.clone();
|
|
|
|
|
let port = config.server.port;
|
2025-11-20 13:28:35 -03:00
|
|
|
let workers = worker_count; // Capture worker_count for the thread
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"Desktop mode: Starting HTTP server on port {} in background thread",
|
|
|
|
|
port
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
std::thread::spawn(move || {
|
2025-11-20 13:28:35 -03:00
|
|
|
info!("HTTP server thread started, initializing runtime...");
|
2025-11-19 14:00:57 -03:00
|
|
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create HTTP runtime");
|
|
|
|
|
rt.block_on(async move {
|
2025-11-20 13:28:35 -03:00
|
|
|
info!(
|
|
|
|
|
"HTTP server runtime created, starting axum server on port {}...",
|
|
|
|
|
port
|
|
|
|
|
);
|
|
|
|
|
if let Err(e) = run_axum_server(app_state_for_server, port, workers).await {
|
2025-11-19 14:00:57 -03:00
|
|
|
error!("HTTP server error: {}", e);
|
2025-11-20 13:28:35 -03:00
|
|
|
eprintln!("HTTP server error: {}", e);
|
|
|
|
|
} else {
|
|
|
|
|
info!("HTTP server started successfully");
|
2025-11-19 14:00:57 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-20 13:28:35 -03:00
|
|
|
// Give the server thread a moment to start
|
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
2025-11-20 13:46:01 -03:00
|
|
|
info!("Launching General Bots desktop application...");
|
2025-11-20 13:28:35 -03:00
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
// Run Tauri on main thread (GUI requires main thread)
|
2025-11-14 16:54:55 -03:00
|
|
|
let tauri_app = tauri::Builder::default()
|
|
|
|
|
.setup(|app| {
|
|
|
|
|
use tauri::WebviewWindowBuilder;
|
2025-11-19 14:00:57 -03:00
|
|
|
match WebviewWindowBuilder::new(
|
|
|
|
|
app,
|
|
|
|
|
"main",
|
|
|
|
|
tauri::WebviewUrl::App("index.html".into()),
|
|
|
|
|
)
|
2025-11-20 13:46:01 -03:00
|
|
|
.title("General Bots")
|
2025-11-19 14:00:57 -03:00
|
|
|
.build()
|
|
|
|
|
{
|
2025-11-14 16:54:55 -03:00
|
|
|
Ok(_window) => Ok(()),
|
|
|
|
|
Err(e) if e.to_string().contains("WebviewLabelAlreadyExists") => {
|
|
|
|
|
log::warn!("Main window already exists, reusing existing window");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-11-19 14:00:57 -03:00
|
|
|
Err(e) => Err(e.into()),
|
2025-11-14 16:54:55 -03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.build(tauri::generate_context!())
|
2025-11-20 13:46:01 -03:00
|
|
|
.expect("error while running Desktop application");
|
2025-11-14 16:54:55 -03:00
|
|
|
|
|
|
|
|
tauri_app.run(|_app_handle, event| match event {
|
|
|
|
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
|
|
|
|
api.prevent_exit();
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
});
|
2025-11-19 14:00:57 -03:00
|
|
|
|
2025-11-14 16:54:55 -03:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 14:00:57 -03:00
|
|
|
// Non-desktop mode: Run HTTP server directly
|
2025-11-20 13:28:35 -03:00
|
|
|
run_axum_server(app_state, config.server.port, worker_count).await?;
|
2025-11-19 14:00:57 -03:00
|
|
|
|
|
|
|
|
// Wait for UI thread to finish if it was started
|
|
|
|
|
if let Some(handle) = ui_handle {
|
|
|
|
|
handle.join().ok();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 09:48:46 -03:00
|
|
|
Ok(())
|
2025-10-06 10:30:17 -03:00
|
|
|
}
|