- More automation from start to web, user sessions.
This commit is contained in:
parent
c595380837
commit
9c36aa10fa
17 changed files with 852 additions and 180 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1009,7 +1009,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botserver"
|
name = "botserver"
|
||||||
version = "6.0.4"
|
version = "6.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
|
|
|
||||||
62
Cargo.toml
62
Cargo.toml
|
|
@ -1,39 +1,41 @@
|
||||||
[package]
|
[package]
|
||||||
name = "botserver"
|
name = "botserver"
|
||||||
version = "6.0.4"
|
version = "6.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"@AlanPerdomo",
|
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
|
||||||
"@AnaPaulaGil",
|
"General Bots Community <https://github.com/GeneralBots>",
|
||||||
"@arenasio",
|
"Rodrigo Rodriguez <rodrigorodriguez@pragmatismo.com.br>",
|
||||||
"@AtyllaL",
|
"Alan Perdomo",
|
||||||
"@christopherdecastilho",
|
"Ana Paula Gil",
|
||||||
"@danielolima96",
|
"Arenas.io",
|
||||||
"@Dariojunior3",
|
"Atylla L",
|
||||||
"@davidlerner26",
|
"Christopher de Castilho",
|
||||||
"@ExperimentationGarage",
|
"Dario Lima",
|
||||||
"@flavioandrade91",
|
"Dario Junior",
|
||||||
"@HeraldoAlmeida",
|
"David Lerner",
|
||||||
"@joao-parana",
|
"Experimentation Garage",
|
||||||
"@jonathasc",
|
"Flavio Andrade",
|
||||||
"@jramos-br",
|
"Heraldo Almeida",
|
||||||
"@lpicanco",
|
"Joao Parana",
|
||||||
"@marcosvelasco",
|
"Jonathas C",
|
||||||
"@matheus39x",
|
"J Ramos",
|
||||||
"@oerlabshenrique",
|
"Lucas Picanco",
|
||||||
"@othonlima",
|
"Marcos Velasco",
|
||||||
"@PH-Nascimento",
|
"Matheus 39x",
|
||||||
"@phpussente",
|
"Oerlabs Henrique",
|
||||||
"@RobsonDantasE",
|
"Othon Lima",
|
||||||
"@rodrigorodriguez",
|
"PH Nascimento",
|
||||||
"@SarahLourenco",
|
"Phpussente",
|
||||||
"@thipatriota",
|
"Robson Dantas",
|
||||||
"@webgus",
|
"Sarah Lourenco",
|
||||||
"@ZuilhoSe",
|
"Thi Patriota",
|
||||||
|
"Webgus",
|
||||||
|
"Zuilho Se",
|
||||||
]
|
]
|
||||||
description = "General Bots Server"
|
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://alm.pragmatismo.com.br/generalbots/botserver"
|
repository = "https://github.com/GeneralBots/BotServer"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vectordb"]
|
default = ["vectordb"]
|
||||||
|
|
|
||||||
116
README.md
116
README.md
|
|
@ -52,10 +52,13 @@ editor). LLM and BASIC can be mixed used to build custom dialogs so Bot can be e
|
||||||
|
|
||||||
Before you embark on your General Bots journey, ensure you have the following tools installed:
|
Before you embark on your General Bots journey, ensure you have the following tools installed:
|
||||||
|
|
||||||
- **Node.js (version 20 or later)**: General Bots leverages the latest features of Node.js to provide a robust and efficient runtime environment. Download it from [nodejs.org](https://nodejs.org/en/download/).
|
- **Rust (latest stable version)**: General Bots server is built with Rust for performance and safety. Install from [rustup.rs](https://rustup.rs/).
|
||||||
- **Git (latest stable version)**: Essential for version control and collaborating on bot projects. Get it from [git-scm.com](https://git-scm.com/downloads).
|
- **Git (latest stable version)**: Essential for version control and collaborating on bot projects. Get it from [git-scm.com](https://git-scm.com/downloads).
|
||||||
|
|
||||||
### Quick Start Guide
|
**Optional (for Node.js bots):**
|
||||||
|
- **Node.js (version 20 or later)**: For Node.js-based bot packages. Download from [nodejs.org](https://nodejs.org/en/download/).
|
||||||
|
|
||||||
|
### Quick Start Guide (Rust Version)
|
||||||
|
|
||||||
Follow these steps to get your General Bots server up and running:
|
Follow these steps to get your General Bots server up and running:
|
||||||
|
|
||||||
|
|
@ -71,24 +74,115 @@ Follow these steps to get your General Bots server up and running:
|
||||||
```
|
```
|
||||||
This changes your current directory to the newly cloned BotServer folder.
|
This changes your current directory to the newly cloned BotServer folder.
|
||||||
|
|
||||||
3. Install dependencies and start the server:
|
3. Create your configuration file:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
cp .env.example .env
|
||||||
npm run start
|
|
||||||
```
|
```
|
||||||
The `npm install` command installs all necessary dependencies for the project. `npm run start` builds your bot server locally and serves it through a development server.
|
Edit `.env` with your actual configuration values. See [Configuration](#configuration) section below.
|
||||||
|
|
||||||
|
4. Run the server:
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
On first run, BotServer will automatically:
|
||||||
|
- Install required components (PostgreSQL, MinIO, Redis, LLM)
|
||||||
|
- Set up the database with migrations
|
||||||
|
- Download AI models
|
||||||
|
- Upload template bots from `templates/` folder
|
||||||
|
- Start the HTTP server on `http://127.0.0.1:8080` (or your configured port)
|
||||||
|
|
||||||
|
**Alternative - Build release version:**
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
./target/release/botserver
|
||||||
|
```
|
||||||
|
|
||||||
|
**Management Commands:**
|
||||||
|
```bash
|
||||||
|
botserver start # Start all components
|
||||||
|
botserver stop # Stop all components
|
||||||
|
botserver restart # Restart all components
|
||||||
|
botserver list # List available components
|
||||||
|
botserver status <component> # Check component status
|
||||||
|
botserver install <component> # Install optional component
|
||||||
|
```
|
||||||
|
|
||||||
### Accessing Your Bot
|
### Accessing Your Bot
|
||||||
|
|
||||||
Once the server is running, you can access your bot at `http://localhost:4242/`. This local server allows you to interact with your bot and test its functionality in real-time. If you want to publish
|
Once the server is running, you can access your bot at `http://localhost:8080/` (or your configured `SERVER_PORT`). This local server allows you to interact with your bot and test its functionality in real-time.
|
||||||
without password, define [ADMIN_OPEN_PUBLISH](https://github.com/GeneralBots/BotBook/master/docs/chapter-07-gbot-reference#enviroment-variables-reference) as true in BotServer .env file.
|
|
||||||
|
|
||||||
To publish bot packages and initiate a conversation with the bot, use the command:
|
**Anonymous Access:** Every visitor automatically gets a unique session tracked by cookie. No login required to start chatting!
|
||||||
|
|
||||||
|
**Authentication:** Users can optionally register/login at `/static/auth/login.html` to save conversations across devices.
|
||||||
|
|
||||||
|
**About Page:** Visit `/static/about/index.html` to learn more about BotServer and its maintainers.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
BotServer uses environment variables for configuration. Copy `.env.example` to `.env` and customize:
|
||||||
|
|
||||||
|
#### Required Settings (Auto-installed on first run)
|
||||||
|
```bash
|
||||||
|
# Database (PostgreSQL - auto-installed)
|
||||||
|
TABLES_SERVER=localhost
|
||||||
|
TABLES_PORT=5432
|
||||||
|
TABLES_DATABASE=botserver
|
||||||
|
TABLES_USERNAME=gbuser
|
||||||
|
TABLES_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Storage (MinIO - auto-installed)
|
||||||
|
DRIVE_SERVER=http://localhost:9000
|
||||||
|
DRIVE_ACCESSKEY=minioadmin
|
||||||
|
DRIVE_SECRET=minioadmin
|
||||||
|
DRIVE_ORG_PREFIX=botserver-
|
||||||
|
|
||||||
|
# Cache (Redis - auto-installed)
|
||||||
|
CACHE_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# LLM (llama.cpp - auto-installed with models)
|
||||||
|
LLM_LOCAL=false
|
||||||
|
LLM_URL=http://localhost:8081/v1
|
||||||
|
EMBEDDING_URL=http://localhost:8082
|
||||||
```
|
```
|
||||||
/publish
|
|
||||||
|
#### Optional Settings
|
||||||
|
```bash
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_HOST=127.0.0.1
|
||||||
|
SERVER_PORT=8080
|
||||||
|
|
||||||
|
# External AI API (Groq, OpenAI, Azure, etc.)
|
||||||
|
AI_KEY=your-api-key-here
|
||||||
|
AI_ENDPOINT=https://api.groq.com/openai/v1/chat/completions
|
||||||
|
AI_LLM_MODEL=openai/gpt-4
|
||||||
|
|
||||||
|
# Email (for notifications)
|
||||||
|
EMAIL_FROM=bot@example.com
|
||||||
|
EMAIL_SERVER=smtp.example.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_USER=your-email@example.com
|
||||||
|
EMAIL_PASS=your-password
|
||||||
```
|
```
|
||||||
This command prepares your bot packages for use and allows you to start interacting with your bot immediately.
|
|
||||||
|
#### Legacy Mode (Use existing infrastructure)
|
||||||
|
If you already have PostgreSQL, MinIO, etc. running, set these in `.env`:
|
||||||
|
```bash
|
||||||
|
# Existing database
|
||||||
|
TABLES_SERVER=your-db-host
|
||||||
|
TABLES_USERNAME=your-username
|
||||||
|
TABLES_PASSWORD=your-password
|
||||||
|
|
||||||
|
# Existing MinIO/S3
|
||||||
|
DRIVE_SERVER=https://your-minio-host
|
||||||
|
DRIVE_ACCESSKEY=your-access-key
|
||||||
|
DRIVE_SECRET=your-secret-key
|
||||||
|
|
||||||
|
# Existing AI endpoint
|
||||||
|
AI_ENDPOINT=https://your-llm-endpoint
|
||||||
|
AI_KEY=your-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
BotServer will detect existing infrastructure and skip auto-installation.
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
|
|
|
||||||
2
gbot.sh
2
gbot.sh
|
|
@ -1,3 +1,3 @@
|
||||||
set +e
|
set +e
|
||||||
pkill postgres && rm .env -rf botserver-stack && clear && \
|
clear && \
|
||||||
RUST_LOG=trace,hyper_util=off cargo run
|
RUST_LOG=trace,hyper_util=off cargo run
|
||||||
|
|
|
||||||
|
|
@ -325,9 +325,80 @@ impl AutomationService {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to read script {}: {}", full_path.display(), e);
|
warn!(
|
||||||
self.cleanup_job_flag(&bot_id, param).await;
|
"Script not found locally at {}, attempting to download from MinIO: {}",
|
||||||
return;
|
full_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to download from MinIO
|
||||||
|
if let Some(s3_client) = &self.state.s3_client {
|
||||||
|
let bucket_name = format!(
|
||||||
|
"{}{}.gbai",
|
||||||
|
env::var("MINIO_ORG_PREFIX").unwrap_or_else(|_| "org1_".to_string()),
|
||||||
|
env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string())
|
||||||
|
);
|
||||||
|
let s3_key = format!(".gbdialog/{}", param);
|
||||||
|
|
||||||
|
trace!("Downloading from bucket={} key={}", bucket_name, s3_key);
|
||||||
|
|
||||||
|
match s3_client
|
||||||
|
.get_object()
|
||||||
|
.bucket(&bucket_name)
|
||||||
|
.key(&s3_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => {
|
||||||
|
match response.body.collect().await {
|
||||||
|
Ok(data) => {
|
||||||
|
match String::from_utf8(data.into_bytes().to_vec()) {
|
||||||
|
Ok(content) => {
|
||||||
|
info!("Downloaded script '{}' from MinIO", param);
|
||||||
|
|
||||||
|
// Save to local cache
|
||||||
|
if let Err(e) =
|
||||||
|
std::fs::create_dir_all(&self.scripts_dir)
|
||||||
|
{
|
||||||
|
warn!("Failed to create scripts directory: {}", e);
|
||||||
|
} else if let Err(e) =
|
||||||
|
tokio::fs::write(&full_path, &content).await
|
||||||
|
{
|
||||||
|
warn!("Failed to cache script locally: {}", e);
|
||||||
|
} else {
|
||||||
|
trace!("Cached script to {}", full_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to decode script {}: {}", param, e);
|
||||||
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Failed to read script body from MinIO {}: {}",
|
||||||
|
param, e
|
||||||
|
);
|
||||||
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to download script {} from MinIO: {}", param, e);
|
||||||
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("S3 client not available, cannot download script {}", param);
|
||||||
|
self.cleanup_job_flag(&bot_id, param).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use dotenvy::dotenv;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct BootstrapManager {
|
pub struct BootstrapManager {
|
||||||
pub install_mode: InstallMode,
|
pub install_mode: InstallMode,
|
||||||
|
|
@ -91,6 +92,12 @@ impl BootstrapManager {
|
||||||
match diesel::PgConnection::establish(&database_url) {
|
match diesel::PgConnection::establish(&database_url) {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
info!("Successfully connected to legacy database, loading configuration");
|
info!("Successfully connected to legacy database, loading configuration");
|
||||||
|
|
||||||
|
// Apply migrations
|
||||||
|
if let Err(e) = self.apply_migrations(&mut conn) {
|
||||||
|
log::warn!("Failed to apply migrations: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(AppConfig::from_database(&mut conn));
|
return Ok(AppConfig::from_database(&mut conn));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -198,4 +205,173 @@ impl BootstrapManager {
|
||||||
hasher.update(password.as_bytes());
|
hasher.update(password.as_bytes());
|
||||||
format!("{:x}", hasher.finalize())
|
format!("{:x}", hasher.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn upload_templates_to_minio(&self, config: &AppConfig) -> Result<()> {
|
||||||
|
use aws_sdk_s3::config::Credentials;
|
||||||
|
use aws_sdk_s3::config::Region;
|
||||||
|
|
||||||
|
info!("Uploading template bots to MinIO...");
|
||||||
|
|
||||||
|
let creds = Credentials::new(
|
||||||
|
&config.minio.access_key,
|
||||||
|
&config.minio.secret_key,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"minio",
|
||||||
|
);
|
||||||
|
|
||||||
|
let s3_config = aws_sdk_s3::Config::builder()
|
||||||
|
.credentials_provider(creds)
|
||||||
|
.endpoint_url(&config.minio.server)
|
||||||
|
.region(Region::new("us-east-1"))
|
||||||
|
.force_path_style(true)
|
||||||
|
.behavior_version(aws_sdk_s3::config::BehaviorVersion::latest())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let client = aws_sdk_s3::Client::from_conf(s3_config);
|
||||||
|
|
||||||
|
// Upload templates from templates/ directory
|
||||||
|
let templates_dir = Path::new("templates");
|
||||||
|
if !templates_dir.exists() {
|
||||||
|
trace!("Templates directory not found, skipping upload");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through each .gbai folder in templates/
|
||||||
|
for entry in std::fs::read_dir(templates_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) {
|
||||||
|
let bot_name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let bucket_name = format!("{}{}", config.minio.org_prefix, bot_name);
|
||||||
|
|
||||||
|
trace!("Creating bucket: {}", bucket_name);
|
||||||
|
|
||||||
|
// Create bucket if it doesn't exist
|
||||||
|
match client.create_bucket().bucket(&bucket_name).send().await {
|
||||||
|
Ok(_) => info!("Created bucket: {}", bucket_name),
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
if err_str.contains("BucketAlreadyOwnedByYou")
|
||||||
|
|| err_str.contains("BucketAlreadyExists")
|
||||||
|
{
|
||||||
|
trace!("Bucket {} already exists", bucket_name);
|
||||||
|
} else {
|
||||||
|
log::warn!("Failed to create bucket {}: {}", bucket_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all files recursively
|
||||||
|
self.upload_directory_recursive(&client, &path, &bucket_name, "")
|
||||||
|
.await?;
|
||||||
|
info!("Uploaded template bot: {}", bot_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Template bots uploaded successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_directory_recursive<'a>(
|
||||||
|
&'a self,
|
||||||
|
client: &'a aws_sdk_s3::Client,
|
||||||
|
local_path: &'a Path,
|
||||||
|
bucket: &'a str,
|
||||||
|
prefix: &'a str,
|
||||||
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
|
|
||||||
|
for entry in std::fs::read_dir(local_path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let key = if prefix.is_empty() {
|
||||||
|
file_name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", prefix, file_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
trace!(
|
||||||
|
"Uploading file: {} to bucket: {} with key: {}",
|
||||||
|
path.display(),
|
||||||
|
bucket,
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = ByteStream::from_path(&path).await?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(&key)
|
||||||
|
.body(body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
trace!("Uploaded: {}", key);
|
||||||
|
} else if path.is_dir() {
|
||||||
|
self.upload_directory_recursive(client, &path, bucket, &key)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_migrations(&self, conn: &mut diesel::PgConnection) -> Result<()> {
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
info!("Applying database migrations...");
|
||||||
|
|
||||||
|
let migrations_dir = std::path::Path::new("migrations");
|
||||||
|
if !migrations_dir.exists() {
|
||||||
|
trace!("No migrations directory found, skipping");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all .sql files sorted
|
||||||
|
let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)?
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| {
|
||||||
|
entry
|
||||||
|
.path()
|
||||||
|
.extension()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|s| s == "sql")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sql_files.sort_by_key(|entry| entry.path());
|
||||||
|
|
||||||
|
for entry in sql_files {
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = path.file_name().unwrap().to_string_lossy();
|
||||||
|
|
||||||
|
trace!("Reading migration: {}", filename);
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(sql) => {
|
||||||
|
trace!("Applying migration: {}", filename);
|
||||||
|
match diesel::sql_query(&sql).execute(conn) {
|
||||||
|
Ok(_) => info!("Applied migration: {}", filename),
|
||||||
|
Err(e) => {
|
||||||
|
// Ignore errors for already applied migrations
|
||||||
|
trace!("Migration {} result: {}", filename, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to read migration {}: {}", filename, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Migrations check completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -556,10 +556,14 @@ impl BotOrchestrator {
|
||||||
"Running start script for session: {} with token: {:?}",
|
"Running start script for session: {} with token: {:?}",
|
||||||
session.id, token
|
session.id, token
|
||||||
);
|
);
|
||||||
let start_script_path = "./templates/announcements.gbai/announcements.gbdialog/start.bas";
|
let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
|
||||||
let start_script = match std::fs::read_to_string(start_script_path) {
|
let start_script_path = format!("./{}.gbai/.gbdialog/start.bas", bot_guid);
|
||||||
|
let start_script = match std::fs::read_to_string(&start_script_path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(_) => r#"TALK "Error loading script file.""#.to_string(),
|
Err(_) => {
|
||||||
|
warn!("start.bas not found at {}, skipping", start_script_path);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
"Start script content for session {}: {}",
|
"Start script content for session {}: {}",
|
||||||
|
|
@ -706,7 +710,8 @@ async fn websocket_handler(
|
||||||
let user_id = query
|
let user_id = query
|
||||||
.get("user_id")
|
.get("user_id")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "default_user".to_string());
|
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||||
|
.replace("undefined", &Uuid::new_v4().to_string());
|
||||||
|
|
||||||
let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
|
let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
|
||||||
let (tx, mut rx) = mpsc::channel::<BotResponse>(100);
|
let (tx, mut rx) = mpsc::channel::<BotResponse>(100);
|
||||||
|
|
@ -747,6 +752,20 @@ async fn websocket_handler(
|
||||||
session_id, user_id
|
session_id, user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Trigger auto welcome (start.bas)
|
||||||
|
let orchestrator_clone = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
let user_id_welcome = user_id.clone();
|
||||||
|
let session_id_welcome = session_id.clone();
|
||||||
|
let bot_id_welcome = bot_id.clone();
|
||||||
|
actix_web::rt::spawn(async move {
|
||||||
|
if let Err(e) = orchestrator_clone
|
||||||
|
.trigger_auto_welcome(&session_id_welcome, &user_id_welcome, &bot_id_welcome, None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("Failed to trigger auto welcome: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let web_adapter = data.web_adapter.clone();
|
let web_adapter = data.web_adapter.clone();
|
||||||
let session_id_clone1 = session_id.clone();
|
let session_id_clone1 = session_id.clone();
|
||||||
let session_id_clone2 = session_id.clone();
|
let session_id_clone2 = session_id.clone();
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,22 @@ impl AppConfig {
|
||||||
pub fn database_url(&self) -> String {
|
pub fn database_url(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
self.database.username, self.database.password, self.database.server, self.database.port, self.database.database
|
self.database.username,
|
||||||
|
self.database.password,
|
||||||
|
self.database.server,
|
||||||
|
self.database.port,
|
||||||
|
self.database.database
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn database_custom_url(&self) -> String {
|
pub fn database_custom_url(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
self.database_custom.username, self.database_custom.password, self.database_custom.server, self.database_custom.port, self.database_custom.database
|
self.database_custom.username,
|
||||||
|
self.database_custom.password,
|
||||||
|
self.database_custom.server,
|
||||||
|
self.database_custom.port,
|
||||||
|
self.database_custom.database
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,41 +133,69 @@ impl AppConfig {
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_str = |key: &str, default: &str| -> String {
|
let get_str = |key: &str, default: &str| -> String {
|
||||||
config_map.get(key).map(|v| v.config_value.clone()).unwrap_or_else(|| default.to_string())
|
config_map
|
||||||
|
.get(key)
|
||||||
|
.map(|v| v.config_value.clone())
|
||||||
|
.unwrap_or_else(|| default.to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_u32 = |key: &str, default: u32| -> u32 {
|
let get_u32 = |key: &str, default: u32| -> u32 {
|
||||||
config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default)
|
config_map
|
||||||
|
.get(key)
|
||||||
|
.and_then(|v| v.config_value.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_u16 = |key: &str, default: u16| -> u16 {
|
let get_u16 = |key: &str, default: u16| -> u16 {
|
||||||
config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default)
|
config_map
|
||||||
|
.get(key)
|
||||||
|
.and_then(|v| v.config_value.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_bool = |key: &str, default: bool| -> bool {
|
let get_bool = |key: &str, default: bool| -> bool {
|
||||||
config_map.get(key).map(|v| v.config_value.to_lowercase() == "true").unwrap_or(default)
|
config_map
|
||||||
|
.get(key)
|
||||||
|
.map(|v| v.config_value.to_lowercase() == "true")
|
||||||
|
.unwrap_or(default)
|
||||||
};
|
};
|
||||||
|
|
||||||
let stack_path = PathBuf::from(get_str("STACK_PATH", "./botserver-stack"));
|
let stack_path = PathBuf::from(get_str("STACK_PATH", "./botserver-stack"));
|
||||||
|
|
||||||
|
// For database credentials, prioritize environment variables over database values
|
||||||
|
// because we need the correct credentials to connect to the database in the first place
|
||||||
let database = DatabaseConfig {
|
let database = DatabaseConfig {
|
||||||
username: get_str("TABLES_USERNAME", "gbuser"),
|
username: std::env::var("TABLES_USERNAME")
|
||||||
password: get_str("TABLES_PASSWORD", ""),
|
.unwrap_or_else(|_| get_str("TABLES_USERNAME", "gbuser")),
|
||||||
server: get_str("TABLES_SERVER", "localhost"),
|
password: std::env::var("TABLES_PASSWORD")
|
||||||
port: get_u32("TABLES_PORT", 5432),
|
.unwrap_or_else(|_| get_str("TABLES_PASSWORD", "")),
|
||||||
database: get_str("TABLES_DATABASE", "botserver"),
|
server: std::env::var("TABLES_SERVER")
|
||||||
|
.unwrap_or_else(|_| get_str("TABLES_SERVER", "localhost")),
|
||||||
|
port: std::env::var("TABLES_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or_else(|| get_u32("TABLES_PORT", 5432)),
|
||||||
|
database: std::env::var("TABLES_DATABASE")
|
||||||
|
.unwrap_or_else(|_| get_str("TABLES_DATABASE", "botserver")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let database_custom = DatabaseConfig {
|
let database_custom = DatabaseConfig {
|
||||||
username: get_str("CUSTOM_USERNAME", "gbuser"),
|
username: std::env::var("CUSTOM_USERNAME")
|
||||||
password: get_str("CUSTOM_PASSWORD", ""),
|
.unwrap_or_else(|_| get_str("CUSTOM_USERNAME", "gbuser")),
|
||||||
server: get_str("CUSTOM_SERVER", "localhost"),
|
password: std::env::var("CUSTOM_PASSWORD")
|
||||||
port: get_u32("CUSTOM_PORT", 5432),
|
.unwrap_or_else(|_| get_str("CUSTOM_PASSWORD", "")),
|
||||||
database: get_str("CUSTOM_DATABASE", "botserver"),
|
server: std::env::var("CUSTOM_SERVER")
|
||||||
|
.unwrap_or_else(|_| get_str("CUSTOM_SERVER", "localhost")),
|
||||||
|
port: std::env::var("CUSTOM_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or_else(|| get_u32("CUSTOM_PORT", 5432)),
|
||||||
|
database: std::env::var("CUSTOM_DATABASE")
|
||||||
|
.unwrap_or_else(|_| get_str("CUSTOM_DATABASE", "botserver")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let minio = DriveConfig {
|
let minio = DriveConfig {
|
||||||
server: get_str("DRIVE_SERVER", "localhost:9000"),
|
server: get_str("DRIVE_SERVER", "http://localhost:9000"),
|
||||||
access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"),
|
access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"),
|
||||||
secret_key: get_str("DRIVE_SECRET", "minioadmin"),
|
secret_key: get_str("DRIVE_SECRET", "minioadmin"),
|
||||||
use_ssl: get_bool("DRIVE_USE_SSL", false),
|
use_ssl: get_bool("DRIVE_USE_SSL", false),
|
||||||
|
|
@ -183,7 +219,10 @@ impl AppConfig {
|
||||||
|
|
||||||
AppConfig {
|
AppConfig {
|
||||||
minio,
|
minio,
|
||||||
server: ServerConfig { host: get_str("SERVER_HOST", "127.0.0.1"), port: get_u16("SERVER_PORT", 8080) },
|
server: ServerConfig {
|
||||||
|
host: get_str("SERVER_HOST", "127.0.0.1"),
|
||||||
|
port: get_u16("SERVER_PORT", 8080),
|
||||||
|
},
|
||||||
database,
|
database,
|
||||||
database_custom,
|
database_custom,
|
||||||
email,
|
email,
|
||||||
|
|
@ -198,10 +237,13 @@ impl AppConfig {
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
warn!("Loading configuration from environment variables");
|
warn!("Loading configuration from environment variables");
|
||||||
|
|
||||||
let stack_path = std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string());
|
let stack_path =
|
||||||
|
std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||||
|
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string());
|
let database_url = std::env::var("DATABASE_URL")
|
||||||
let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url);
|
.unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string());
|
||||||
|
let (db_username, db_password, db_server, db_port, db_name) =
|
||||||
|
parse_database_url(&database_url);
|
||||||
|
|
||||||
let database = DatabaseConfig {
|
let database = DatabaseConfig {
|
||||||
username: db_username,
|
username: db_username,
|
||||||
|
|
@ -215,22 +257,35 @@ impl AppConfig {
|
||||||
username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()),
|
username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()),
|
||||||
password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".to_string()),
|
password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".to_string()),
|
||||||
server: std::env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()),
|
server: std::env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()),
|
||||||
port: std::env::var("CUSTOM_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(5432),
|
port: std::env::var("CUSTOM_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(5432),
|
||||||
database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()),
|
database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let minio = DriveConfig {
|
let minio = DriveConfig {
|
||||||
server: std::env::var("DRIVE_SERVER").unwrap_or_else(|_| "localhost:9000".to_string()),
|
server: std::env::var("DRIVE_SERVER")
|
||||||
access_key: std::env::var("DRIVE_ACCESSKEY").unwrap_or_else(|_| "minioadmin".to_string()),
|
.unwrap_or_else(|_| "http://localhost:9000".to_string()),
|
||||||
|
access_key: std::env::var("DRIVE_ACCESSKEY")
|
||||||
|
.unwrap_or_else(|_| "minioadmin".to_string()),
|
||||||
secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()),
|
secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()),
|
||||||
use_ssl: std::env::var("DRIVE_USE_SSL").unwrap_or_else(|_| "false".to_string()).parse().unwrap_or(false),
|
use_ssl: std::env::var("DRIVE_USE_SSL")
|
||||||
org_prefix: std::env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "botserver".to_string()),
|
.unwrap_or_else(|_| "false".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(false),
|
||||||
|
org_prefix: std::env::var("DRIVE_ORG_PREFIX")
|
||||||
|
.unwrap_or_else(|_| "botserver".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let email = EmailConfig {
|
let email = EmailConfig {
|
||||||
from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()),
|
from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()),
|
||||||
server: std::env::var("EMAIL_SERVER").unwrap_or_else(|_| "smtp.example.com".to_string()),
|
server: std::env::var("EMAIL_SERVER")
|
||||||
port: std::env::var("EMAIL_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
.unwrap_or_else(|_| "smtp.example.com".to_string()),
|
||||||
|
port: std::env::var("EMAIL_PORT")
|
||||||
|
.unwrap_or_else(|_| "587".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(587),
|
||||||
username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()),
|
username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()),
|
||||||
password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()),
|
password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()),
|
||||||
};
|
};
|
||||||
|
|
@ -238,28 +293,36 @@ impl AppConfig {
|
||||||
let ai = AIConfig {
|
let ai = AIConfig {
|
||||||
instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()),
|
instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()),
|
||||||
key: std::env::var("AI_KEY").unwrap_or_else(|_| "".to_string()),
|
key: std::env::var("AI_KEY").unwrap_or_else(|_| "".to_string()),
|
||||||
version: std::env::var("AI_VERSION").unwrap_or_else(|_| "2023-12-01-preview".to_string()),
|
version: std::env::var("AI_VERSION")
|
||||||
endpoint: std::env::var("AI_ENDPOINT").unwrap_or_else(|_| "https://api.openai.com".to_string()),
|
.unwrap_or_else(|_| "2023-12-01-preview".to_string()),
|
||||||
|
endpoint: std::env::var("AI_ENDPOINT")
|
||||||
|
.unwrap_or_else(|_| "https://api.openai.com".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
AppConfig {
|
AppConfig {
|
||||||
minio,
|
minio,
|
||||||
server: ServerConfig {
|
server: ServerConfig {
|
||||||
host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
|
host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
|
||||||
port: std::env::var("SERVER_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080),
|
port: std::env::var("SERVER_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(8080),
|
||||||
},
|
},
|
||||||
database,
|
database,
|
||||||
database_custom,
|
database_custom,
|
||||||
email,
|
email,
|
||||||
ai,
|
ai,
|
||||||
s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()),
|
s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()),
|
||||||
site_path: std::env::var("SITES_ROOT").unwrap_or_else(|_| "./botserver-stack/sites".to_string()),
|
site_path: std::env::var("SITES_ROOT")
|
||||||
|
.unwrap_or_else(|_| "./botserver-stack/sites".to_string()),
|
||||||
stack_path: PathBuf::from(stack_path),
|
stack_path: PathBuf::from(stack_path),
|
||||||
db_conn: None,
|
db_conn: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config_from_db(conn: &mut PgConnection) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> {
|
fn load_config_from_db(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> {
|
||||||
let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::<ServerConfigRow>(conn)?;
|
let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::<ServerConfigRow>(conn)?;
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
@ -270,13 +333,26 @@ impl AppConfig {
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_config(&self, conn: &mut PgConnection, key: &str, value: &str) -> Result<(), diesel::result::Error> {
|
pub fn set_config(
|
||||||
diesel::sql_query("SELECT set_config($1, $2)").bind::<Text, _>(key).bind::<Text, _>(value).execute(conn)?;
|
&self,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
) -> Result<(), diesel::result::Error> {
|
||||||
|
diesel::sql_query("SELECT set_config($1, $2)")
|
||||||
|
.bind::<Text, _>(key)
|
||||||
|
.bind::<Text, _>(value)
|
||||||
|
.execute(conn)?;
|
||||||
info!("Updated configuration: {} = {}", key, value);
|
info!("Updated configuration: {} = {}", key, value);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config(&self, conn: &mut PgConnection, key: &str, fallback: Option<&str>) -> Result<String, diesel::result::Error> {
|
pub fn get_config(
|
||||||
|
&self,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
key: &str,
|
||||||
|
fallback: Option<&str>,
|
||||||
|
) -> Result<String, diesel::result::Error> {
|
||||||
let fallback_str = fallback.unwrap_or("");
|
let fallback_str = fallback.unwrap_or("");
|
||||||
|
|
||||||
#[derive(Debug, QueryableByName)]
|
#[derive(Debug, QueryableByName)]
|
||||||
|
|
@ -285,7 +361,11 @@ impl AppConfig {
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = diesel::sql_query("SELECT get_config($1, $2) as value").bind::<Text, _>(key).bind::<Text, _>(fallback_str).get_result::<ConfigValue>(conn).map(|row| row.value)?;
|
let result = diesel::sql_query("SELECT get_config($1, $2) as value")
|
||||||
|
.bind::<Text, _>(key)
|
||||||
|
.bind::<Text, _>(fallback_str)
|
||||||
|
.get_result::<ConfigValue>(conn)
|
||||||
|
.map(|row| row.value)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +383,10 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
|
||||||
|
|
||||||
let host_port: Vec<&str> = host_db[0].split(':').collect();
|
let host_port: Vec<&str> = host_db[0].split(':').collect();
|
||||||
let server = host_port[0].to_string();
|
let server = host_port[0].to_string();
|
||||||
let port = host_port.get(1).and_then(|p| p.parse().ok()).unwrap_or(5432);
|
let port = host_port
|
||||||
|
.get(1)
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(5432);
|
||||||
let database = host_db[1].to_string();
|
let database = host_db[1].to_string();
|
||||||
|
|
||||||
return (username, password, server, port, database);
|
return (username, password, server, port, database);
|
||||||
|
|
@ -311,7 +394,13 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
("gbuser".to_string(), "".to_string(), "localhost".to_string(), 5432, "botserver".to_string())
|
(
|
||||||
|
"gbuser".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
"localhost".to_string(),
|
||||||
|
5432,
|
||||||
|
"botserver".to_string(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfigManager {
|
pub struct ConfigManager {
|
||||||
|
|
@ -323,17 +412,25 @@ impl ConfigManager {
|
||||||
Self { conn }
|
Self { conn }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_gbot_config(&self, bot_id: &uuid::Uuid, config_path: &str) -> Result<usize, String> {
|
pub fn sync_gbot_config(
|
||||||
|
&self,
|
||||||
|
bot_id: &uuid::Uuid,
|
||||||
|
config_path: &str,
|
||||||
|
) -> Result<usize, String> {
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
let content = fs::read_to_string(config_path).map_err(|e| format!("Failed to read config file: {}", e))?;
|
let content = fs::read_to_string(config_path)
|
||||||
|
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(content.as_bytes());
|
hasher.update(content.as_bytes());
|
||||||
let file_hash = format!("{:x}", hasher.finalize());
|
let file_hash = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
let mut conn = self.conn.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?;
|
let mut conn = self
|
||||||
|
.conn
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("Failed to acquire lock: {}", e))?;
|
||||||
|
|
||||||
#[derive(QueryableByName)]
|
#[derive(QueryableByName)]
|
||||||
struct SyncHash {
|
struct SyncHash {
|
||||||
|
|
@ -341,12 +438,13 @@ impl ConfigManager {
|
||||||
file_hash: String,
|
file_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_hash: Option<String> = diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1")
|
let last_hash: Option<String> =
|
||||||
.bind::<diesel::sql_types::Uuid, _>(bot_id)
|
diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1")
|
||||||
.get_result::<SyncHash>(&mut *conn)
|
.bind::<diesel::sql_types::Uuid, _>(bot_id)
|
||||||
.optional()
|
.get_result::<SyncHash>(&mut *conn)
|
||||||
.map_err(|e| format!("Database error: {}", e))?
|
.optional()
|
||||||
.map(|row| row.file_hash);
|
.map_err(|e| format!("Database error: {}", e))?
|
||||||
|
.map(|row| row.file_hash);
|
||||||
|
|
||||||
if last_hash.as_ref() == Some(&file_hash) {
|
if last_hash.as_ref() == Some(&file_hash) {
|
||||||
info!("Config file unchanged for bot {}", bot_id);
|
info!("Config file unchanged for bot {}", bot_id);
|
||||||
|
|
@ -378,7 +476,10 @@ impl ConfigManager {
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
.map_err(|e| format!("Failed to update sync record: {}", e))?;
|
.map_err(|e| format!("Failed to update sync record: {}", e))?;
|
||||||
|
|
||||||
info!("Synced {} config values for bot {} from {}", updated, bot_id, config_path);
|
info!(
|
||||||
|
"Synced {} config values for bot {} from {}",
|
||||||
|
updated, bot_id, config_path
|
||||||
|
);
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -296,9 +296,13 @@ impl DriveMonitor {
|
||||||
// Calculate file hash for change detection
|
// Calculate file hash for change detection
|
||||||
let _file_hash = format!("{:x}", source_content.len());
|
let _file_hash = format!("{:x}", source_content.len());
|
||||||
|
|
||||||
// Create work directory
|
// Create work directory using bot from bucket name
|
||||||
let work_dir = "./work/default.gbai/default.gbdialog";
|
let bot_name = self
|
||||||
std::fs::create_dir_all(work_dir)?;
|
.bucket_name
|
||||||
|
.strip_suffix(".gbai")
|
||||||
|
.unwrap_or(&self.bucket_name);
|
||||||
|
let work_dir = format!("./work/{}.gbai/.gbdialog", bot_name);
|
||||||
|
std::fs::create_dir_all(&work_dir)?;
|
||||||
|
|
||||||
// Write source to local file
|
// Write source to local file
|
||||||
let local_source_path = format!("{}/{}.bas", work_dir, tool_name);
|
let local_source_path = format!("{}/{}.bas", work_dir, tool_name);
|
||||||
|
|
@ -306,7 +310,7 @@ impl DriveMonitor {
|
||||||
|
|
||||||
// Compile using BasicCompiler
|
// Compile using BasicCompiler
|
||||||
let compiler = BasicCompiler::new(Arc::clone(&self.state));
|
let compiler = BasicCompiler::new(Arc::clone(&self.state));
|
||||||
let result = compiler.compile_file(&local_source_path, work_dir)?;
|
let result = compiler.compile_file(&local_source_path, &work_dir)?;
|
||||||
|
|
||||||
info!("Tool compiled successfully: {}", tool_name);
|
info!("Tool compiled successfully: {}", tool_name);
|
||||||
info!(" AST: {}", result.ast_path);
|
info!(" AST: {}", result.ast_path);
|
||||||
|
|
|
||||||
40
src/main.rs
40
src/main.rs
|
|
@ -52,7 +52,7 @@ use crate::meet::{voice_start, voice_stop};
|
||||||
use crate::package_manager::InstallMode;
|
use crate::package_manager::InstallMode;
|
||||||
use crate::session::{create_session, get_session_history, get_sessions};
|
use crate::session::{create_session, get_session_history, get_sessions};
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::web_server::{index, static_files};
|
use crate::web_server::{bot_index, index, static_files};
|
||||||
use crate::whatsapp::whatsapp_webhook_verify;
|
use crate::whatsapp::whatsapp_webhook_verify;
|
||||||
use crate::whatsapp::WhatsAppAdapter;
|
use crate::whatsapp::WhatsAppAdapter;
|
||||||
|
|
||||||
|
|
@ -63,18 +63,17 @@ async fn main() -> std::io::Result<()> {
|
||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"install" | "remove" | "list" | "status" | "--help" | "-h" => {
|
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
|
||||||
match package_manager::cli::run().await {
|
| "-h" => match package_manager::cli::run().await {
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => return Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("CLI error: {}", e);
|
eprintln!("CLI error: {}", e);
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
format!("CLI command failed: {}", e),
|
format!("CLI command failed: {}", e),
|
||||||
));
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown command: {}", command);
|
eprintln!("Unknown command: {}", command);
|
||||||
eprintln!("Run 'botserver --help' for usage information");
|
eprintln!("Run 'botserver --help' for usage information");
|
||||||
|
|
@ -126,6 +125,12 @@ async fn main() -> std::io::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = bootstrap.start_all();
|
let _ = bootstrap.start_all();
|
||||||
|
|
||||||
|
// Upload template bots to MinIO on first startup
|
||||||
|
if let Err(e) = bootstrap.upload_templates_to_minio(&cfg).await {
|
||||||
|
log::warn!("Failed to upload templates to MinIO: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
let config = std::sync::Arc::new(cfg.clone());
|
let config = std::sync::Arc::new(cfg.clone());
|
||||||
|
|
||||||
info!("Establishing database connection to {}", cfg.database_url());
|
info!("Establishing database connection to {}", cfg.database_url());
|
||||||
|
|
@ -237,16 +242,16 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let local = tokio::task::LocalSet::new();
|
let local = tokio::task::LocalSet::new();
|
||||||
local.block_on(&rt, async move {
|
local.block_on(&rt, async move {
|
||||||
let automation = AutomationService::new(
|
let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
|
||||||
automation_state,
|
let scripts_dir = format!("work/{}.gbai/.gbdialog", bot_guid);
|
||||||
"templates/announcements.gbai/announcements.gbdialog",
|
let automation = AutomationService::new(automation_state, &scripts_dir);
|
||||||
);
|
|
||||||
automation.spawn().await.ok();
|
automation.spawn().await.ok();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let drive_state = app_state.clone();
|
let drive_state = app_state.clone();
|
||||||
let bucket_name = format!("{}default.gbai", cfg.minio.org_prefix);
|
let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
|
||||||
|
let bucket_name = format!("{}{}.gbai", cfg.minio.org_prefix, bot_guid);
|
||||||
let drive_monitor = Arc::new(DriveMonitor::new(drive_state, bucket_name));
|
let drive_monitor = Arc::new(DriveMonitor::new(drive_state, bucket_name));
|
||||||
let _drive_handle = drive_monitor.spawn();
|
let _drive_handle = drive_monitor.spawn();
|
||||||
|
|
||||||
|
|
@ -267,6 +272,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
app = app
|
app = app
|
||||||
.service(upload_file)
|
.service(upload_file)
|
||||||
.service(index)
|
.service(index)
|
||||||
|
.service(bot_index)
|
||||||
.service(static_files)
|
.service(static_files)
|
||||||
.service(websocket_handler)
|
.service(websocket_handler)
|
||||||
.service(auth_handler)
|
.service(auth_handler)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
use crate::package_manager::{InstallMode, PackageManager};
|
use crate::package_manager::{InstallMode, PackageManager};
|
||||||
|
|
||||||
|
|
@ -15,6 +16,77 @@ pub async fn run() -> Result<()> {
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
|
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
|
"start" => {
|
||||||
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
|
InstallMode::Container
|
||||||
|
} else {
|
||||||
|
InstallMode::Local
|
||||||
|
};
|
||||||
|
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
||||||
|
args.get(idx + 1).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
|
println!("Starting all installed components...");
|
||||||
|
|
||||||
|
let components = vec!["tables", "cache", "drive", "llm"];
|
||||||
|
for component in components {
|
||||||
|
if pm.is_installed(component) {
|
||||||
|
match pm.start(component) {
|
||||||
|
Ok(_) => println!("✓ Started {}", component),
|
||||||
|
Err(e) => eprintln!("✗ Failed to start {}: {}", component, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("✓ BotServer components started");
|
||||||
|
}
|
||||||
|
"stop" => {
|
||||||
|
println!("Stopping all components...");
|
||||||
|
|
||||||
|
// Stop components gracefully
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("redis-server").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("minio").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("postgres").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("llama-server").output();
|
||||||
|
|
||||||
|
println!("✓ BotServer components stopped");
|
||||||
|
}
|
||||||
|
"restart" => {
|
||||||
|
println!("Restarting BotServer...");
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("redis-server").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("minio").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("postgres").output();
|
||||||
|
let _ = Command::new("pkill").arg("-f").arg("llama-server").output();
|
||||||
|
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Start
|
||||||
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
|
InstallMode::Container
|
||||||
|
} else {
|
||||||
|
InstallMode::Local
|
||||||
|
};
|
||||||
|
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
||||||
|
args.get(idx + 1).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
|
|
||||||
|
let components = vec!["tables", "cache", "drive", "llm"];
|
||||||
|
for component in components {
|
||||||
|
if pm.is_installed(component) {
|
||||||
|
let _ = pm.start(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("✓ BotServer restarted");
|
||||||
|
}
|
||||||
"install" => {
|
"install" => {
|
||||||
if args.len() < 3 {
|
if args.len() < 3 {
|
||||||
eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]");
|
eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]");
|
||||||
|
|
@ -120,5 +192,5 @@ pub async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_usage() {
|
fn print_usage() {
|
||||||
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list");
|
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ impl PackageManager {
|
||||||
("MINIO_ROOT_USER".to_string(), "gbdriveuser".to_string()),
|
("MINIO_ROOT_USER".to_string(), "gbdriveuser".to_string()),
|
||||||
("MINIO_ROOT_PASSWORD".to_string(), drive_password)
|
("MINIO_ROOT_PASSWORD".to_string(), drive_password)
|
||||||
]),
|
]),
|
||||||
exec_cmd: "{{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001".to_string(),
|
exec_cmd: "nohup {{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001 > {{LOGS_PATH}}/minio.log 2>&1 &".to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.update_drive_credentials_in_database(&encrypted_drive_password)
|
self.update_drive_credentials_in_database(&encrypted_drive_password)
|
||||||
|
|
@ -230,7 +230,7 @@ impl PackageManager {
|
||||||
pre_install_cmds_windows: vec![],
|
pre_install_cmds_windows: vec![],
|
||||||
post_install_cmds_windows: vec![],
|
post_install_cmds_windows: vec![],
|
||||||
env_vars: HashMap::new(),
|
env_vars: HashMap::new(),
|
||||||
exec_cmd: "{{BIN_PATH}}/llama-server -m {{DATA_PATH}}/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf --port 8081 & {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/bge-small-en-v1.5-f32.gguf --port 8082 --embedding".to_string(),
|
exec_cmd: "nohup {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf --port 8081 > {{LOGS_PATH}}/llm-main.log 2>&1 & nohup {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/bge-small-en-v1.5-f32.gguf --port 8082 --embedding > {{LOGS_PATH}}/llm-embed.log 2>&1 &".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,33 +109,32 @@ impl SessionManager {
|
||||||
self.create_session(uid, bid, session_title).map(Some)
|
self.create_session(uid, bid, session_title).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_session(
|
pub fn get_or_create_anonymous_user(
|
||||||
&mut self,
|
&mut self,
|
||||||
uid: Uuid,
|
uid: Option<Uuid>,
|
||||||
bid: Uuid,
|
) -> Result<Uuid, Box<dyn Error + Send + Sync>> {
|
||||||
session_title: &str,
|
|
||||||
) -> Result<UserSession, Box<dyn Error + Send + Sync>> {
|
|
||||||
use crate::shared::models::user_sessions::dsl::*;
|
|
||||||
use crate::shared::models::users::dsl as users_dsl;
|
use crate::shared::models::users::dsl as users_dsl;
|
||||||
|
|
||||||
let now = Utc::now();
|
let user_id = uid.unwrap_or_else(Uuid::new_v4);
|
||||||
|
|
||||||
let user_exists: Option<Uuid> = users_dsl::users
|
let user_exists: Option<Uuid> = users_dsl::users
|
||||||
.filter(users_dsl::id.eq(uid))
|
.filter(users_dsl::id.eq(user_id))
|
||||||
.select(users_dsl::id)
|
.select(users_dsl::id)
|
||||||
.first(&mut self.conn)
|
.first(&mut self.conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
||||||
if user_exists.is_none() {
|
if user_exists.is_none() {
|
||||||
warn!(
|
let now = Utc::now();
|
||||||
"User {} does not exist in database, creating placeholder user",
|
info!("Creating anonymous user with ID {}", user_id);
|
||||||
uid
|
|
||||||
);
|
|
||||||
diesel::insert_into(users_dsl::users)
|
diesel::insert_into(users_dsl::users)
|
||||||
.values((
|
.values((
|
||||||
users_dsl::id.eq(uid),
|
users_dsl::id.eq(user_id),
|
||||||
users_dsl::username.eq(format!("anonymous_{}", rand::random::<u32>())),
|
users_dsl::username.eq(format!("guest_{}", &user_id.to_string()[..8])),
|
||||||
users_dsl::email.eq(format!("anonymous_{}@local", rand::random::<u32>())),
|
users_dsl::email.eq(format!(
|
||||||
users_dsl::password_hash.eq("placeholder"),
|
"guest_{}@anonymous.local",
|
||||||
|
&user_id.to_string()[..8]
|
||||||
|
)),
|
||||||
|
users_dsl::password_hash.eq(""),
|
||||||
users_dsl::is_active.eq(true),
|
users_dsl::is_active.eq(true),
|
||||||
users_dsl::created_at.eq(now),
|
users_dsl::created_at.eq(now),
|
||||||
users_dsl::updated_at.eq(now),
|
users_dsl::updated_at.eq(now),
|
||||||
|
|
@ -143,10 +142,25 @@ impl SessionManager {
|
||||||
.execute(&mut self.conn)?;
|
.execute(&mut self.conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_session(
|
||||||
|
&mut self,
|
||||||
|
uid: Uuid,
|
||||||
|
bid: Uuid,
|
||||||
|
session_title: &str,
|
||||||
|
) -> Result<UserSession, Box<dyn Error + Send + Sync>> {
|
||||||
|
use crate::shared::models::user_sessions::dsl::*;
|
||||||
|
|
||||||
|
// Ensure user exists (create anonymous if needed)
|
||||||
|
let verified_uid = self.get_or_create_anonymous_user(Some(uid))?;
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
let inserted: UserSession = diesel::insert_into(user_sessions)
|
let inserted: UserSession = diesel::insert_into(user_sessions)
|
||||||
.values((
|
.values((
|
||||||
id.eq(Uuid::new_v4()),
|
id.eq(Uuid::new_v4()),
|
||||||
user_id.eq(uid),
|
user_id.eq(verified_uid),
|
||||||
bot_id.eq(bid),
|
bot_id.eq(bid),
|
||||||
title.eq(session_title),
|
title.eq(session_title),
|
||||||
context_data.eq(serde_json::json!({})),
|
context_data.eq(serde_json::json!({})),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ async fn index() -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::get("/{botname}")]
|
||||||
|
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
|
let botname = req.match_info().query("botname");
|
||||||
|
debug!("Serving bot interface for: {}", botname);
|
||||||
|
match fs::read_to_string("web/index.html") {
|
||||||
|
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to load index page for bot {}: {}", botname, e);
|
||||||
|
Ok(HttpResponse::InternalServerError().body("Failed to load index page"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::get("/static/{filename:.*}")]
|
#[actix_web::get("/static/{filename:.*}")]
|
||||||
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
|
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
let filename = req.match_info().query("filename");
|
let filename = req.match_info().query("filename");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,67 @@
|
||||||
REM result = POST "http://0.0.0.0/api/isvalid", "token=" + token;
|
REM Simple KISS authentication - signup/login only, no recovery
|
||||||
|
REM This script is called when user needs authentication
|
||||||
|
|
||||||
REM user = FIND "users", "external_id=" + result.user_id
|
TALK "Welcome! Please choose an option:"
|
||||||
|
TALK "Type 'signup' to create a new account"
|
||||||
|
TALK "Type 'login' to access your existing account"
|
||||||
|
|
||||||
SET_USER "92fcffaa-bf0a-41a9-8d99-5541709d695b"
|
HEAR choice
|
||||||
return true;
|
|
||||||
|
IF choice = "signup" THEN
|
||||||
|
TALK "Great! Let's create your account."
|
||||||
|
TALK "Enter your email:"
|
||||||
|
HEAR email
|
||||||
|
|
||||||
|
TALK "Enter your password:"
|
||||||
|
HEAR password
|
||||||
|
|
||||||
|
TALK "Confirm your password:"
|
||||||
|
HEAR confirm_password
|
||||||
|
|
||||||
|
IF password <> confirm_password THEN
|
||||||
|
TALK "Passwords don't match. Please try again."
|
||||||
|
RETURN false
|
||||||
|
END IF
|
||||||
|
|
||||||
|
REM Create user in database
|
||||||
|
LET user_id = GENERATE_UUID()
|
||||||
|
LET result = EXEC "INSERT INTO users (id, email, password_hash, created_at) VALUES (?, ?, ?, NOW())", user_id, email, SHA256(password)
|
||||||
|
|
||||||
|
IF result > 0 THEN
|
||||||
|
SET_USER user_id
|
||||||
|
TALK "Account created successfully! You are now logged in."
|
||||||
|
RETURN true
|
||||||
|
ELSE
|
||||||
|
TALK "Error creating account. Email may already exist."
|
||||||
|
RETURN false
|
||||||
|
END IF
|
||||||
|
|
||||||
|
ELSE IF choice = "login" THEN
|
||||||
|
TALK "Please enter your email:"
|
||||||
|
HEAR email
|
||||||
|
|
||||||
|
TALK "Enter your password:"
|
||||||
|
HEAR password
|
||||||
|
|
||||||
|
REM Query user from database
|
||||||
|
LET user = FIND "users", "email=" + email
|
||||||
|
|
||||||
|
IF user = NULL THEN
|
||||||
|
TALK "Invalid email or password."
|
||||||
|
RETURN false
|
||||||
|
END IF
|
||||||
|
|
||||||
|
LET password_hash = SHA256(password)
|
||||||
|
IF user.password_hash = password_hash THEN
|
||||||
|
SET_USER user.id
|
||||||
|
TALK "Welcome back! You are now logged in."
|
||||||
|
RETURN true
|
||||||
|
ELSE
|
||||||
|
TALK "Invalid email or password."
|
||||||
|
RETURN false
|
||||||
|
END IF
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
TALK "Invalid option. Please type 'signup' or 'login'."
|
||||||
|
RETURN false
|
||||||
|
END IF
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
let resume = GET_BOT_MEMORY ("resume")
|
REM start.bas - Runs automatically when user connects via web
|
||||||
TALK resume
|
REM This is the entry point for each session
|
||||||
|
|
||||||
|
LET resume = GET_BOT_MEMORY("resume")
|
||||||
|
|
||||||
|
IF resume <> "" THEN
|
||||||
|
TALK resume
|
||||||
|
ELSE
|
||||||
|
TALK "Welcome! I'm loading the latest information..."
|
||||||
|
END IF
|
||||||
|
|
||||||
|
REM Add knowledge base for weekly announcements
|
||||||
ADD_KB "weekly"
|
ADD_KB "weekly"
|
||||||
|
|
||||||
TALK "Olá, pode me perguntar sobre qualquer coisa destas circulares..."
|
TALK "You can ask me about any of the announcements or circulars."
|
||||||
|
TALK "If you'd like to login or signup, just type 'auth'."
|
||||||
|
|
|
||||||
|
|
@ -689,7 +689,9 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #90ee90;
|
background: #90ee90;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: width 0.3s ease, background-color 0.3s ease;
|
transition:
|
||||||
|
width 0.3s ease,
|
||||||
|
background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-progress-bar.warning {
|
.context-progress-bar.warning {
|
||||||
|
|
@ -880,15 +882,27 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New elements for improvements -->
|
<!-- New elements for improvements -->
|
||||||
<button class="scroll-to-bottom" id="scrollToBottom" style="display: none">
|
<button
|
||||||
|
class="scroll-to-bottom"
|
||||||
|
id="scrollToBottom"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
↓
|
↓
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="context-indicator" id="contextIndicator" style="display: none">
|
<div
|
||||||
|
class="context-indicator"
|
||||||
|
id="contextIndicator"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<div>Contexto</div>
|
<div>Contexto</div>
|
||||||
<div id="contextPercentage">0%</div>
|
<div id="contextPercentage">0%</div>
|
||||||
<div class="context-progress">
|
<div class="context-progress">
|
||||||
<div class="context-progress-bar" id="contextProgressBar" style="width: 0%"></div>
|
<div
|
||||||
|
class="context-progress-bar"
|
||||||
|
id="contextProgressBar"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -918,11 +932,15 @@
|
||||||
const input = document.getElementById("messageInput");
|
const input = document.getElementById("messageInput");
|
||||||
const sendBtn = document.getElementById("sendBtn");
|
const sendBtn = document.getElementById("sendBtn");
|
||||||
const newChatBtn = document.getElementById("newChatBtn");
|
const newChatBtn = document.getElementById("newChatBtn");
|
||||||
const connectionStatus = document.getElementById("connectionStatus");
|
const connectionStatus =
|
||||||
|
document.getElementById("connectionStatus");
|
||||||
const scrollToBottomBtn = document.getElementById("scrollToBottom");
|
const scrollToBottomBtn = document.getElementById("scrollToBottom");
|
||||||
const contextIndicator = document.getElementById("contextIndicator");
|
const contextIndicator =
|
||||||
const contextPercentage = document.getElementById("contextPercentage");
|
document.getElementById("contextIndicator");
|
||||||
const contextProgressBar = document.getElementById("contextProgressBar");
|
const contextPercentage =
|
||||||
|
document.getElementById("contextPercentage");
|
||||||
|
const contextProgressBar =
|
||||||
|
document.getElementById("contextProgressBar");
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
|
@ -940,7 +958,10 @@
|
||||||
function getWebSocketUrl() {
|
function getWebSocketUrl() {
|
||||||
const protocol =
|
const protocol =
|
||||||
window.location.protocol === "https:" ? "wss:" : "ws:";
|
window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
return `${protocol}//${window.location.host}/ws?session_id=${currentSessionId}&user_id=${currentUserId}`;
|
// Generate UUIDs if not set yet
|
||||||
|
const sessionId = currentSessionId || crypto.randomUUID();
|
||||||
|
const userId = currentUserId || crypto.randomUUID();
|
||||||
|
return `${protocol}//${window.location.host}/ws?session_id=${sessionId}&user_id=${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-focus on input when page loads
|
// Auto-focus on input when page loads
|
||||||
|
|
@ -964,9 +985,11 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scroll management
|
// Scroll management
|
||||||
messagesDiv.addEventListener("scroll", function() {
|
messagesDiv.addEventListener("scroll", function () {
|
||||||
// Check if user is scrolling manually
|
// Check if user is scrolling manually
|
||||||
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
|
const isAtBottom =
|
||||||
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
||||||
|
messagesDiv.clientHeight + 100;
|
||||||
|
|
||||||
if (!isAtBottom) {
|
if (!isAtBottom) {
|
||||||
isUserScrolling = true;
|
isUserScrolling = true;
|
||||||
|
|
@ -1003,9 +1026,11 @@
|
||||||
|
|
||||||
// Update color based on usage
|
// Update color based on usage
|
||||||
if (percentage >= 90) {
|
if (percentage >= 90) {
|
||||||
contextProgressBar.className = "context-progress-bar danger";
|
contextProgressBar.className =
|
||||||
|
"context-progress-bar danger";
|
||||||
} else if (percentage >= 70) {
|
} else if (percentage >= 70) {
|
||||||
contextProgressBar.className = "context-progress-bar warning";
|
contextProgressBar.className =
|
||||||
|
"context-progress-bar warning";
|
||||||
} else {
|
} else {
|
||||||
contextProgressBar.className = "context-progress-bar";
|
contextProgressBar.className = "context-progress-bar";
|
||||||
}
|
}
|
||||||
|
|
@ -1308,7 +1333,9 @@
|
||||||
thinkingTimeout = setTimeout(() => {
|
thinkingTimeout = setTimeout(() => {
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
hideThinkingIndicator();
|
hideThinkingIndicator();
|
||||||
showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
|
showWarning(
|
||||||
|
"O servidor pode estar ocupado. A resposta está demorando demais.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
|
|
@ -1317,7 +1344,8 @@
|
||||||
|
|
||||||
function hideThinkingIndicator() {
|
function hideThinkingIndicator() {
|
||||||
if (!isThinking) return;
|
if (!isThinking) return;
|
||||||
const thinkingDiv = document.getElementById("thinking-indicator");
|
const thinkingDiv =
|
||||||
|
document.getElementById("thinking-indicator");
|
||||||
if (thinkingDiv) {
|
if (thinkingDiv) {
|
||||||
gsap.to(thinkingDiv, {
|
gsap.to(thinkingDiv, {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
|
@ -1417,8 +1445,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the continue button
|
// Remove the continue button
|
||||||
const continueButtons = document.querySelectorAll('.continue-button');
|
const continueButtons =
|
||||||
continueButtons.forEach(button => {
|
document.querySelectorAll(".continue-button");
|
||||||
|
continueButtons.forEach((button) => {
|
||||||
button.parentElement.parentElement.parentElement.remove();
|
button.parentElement.parentElement.parentElement.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue