Compare commits

..

2 commits

Author SHA1 Message Date
be17c9b929 Merge branch 'main' of https://alm.pragmatismo.com.br/generalbots/gbserver
All checks were successful
GBCI / build (push) Successful in 7m14s
2025-09-30 08:52:11 -03:00
97e442f20a New keywords format and last. 2025-09-30 08:49:23 -03:00
12 changed files with 351 additions and 36 deletions

45
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'gbserver'",
"cargo": {
"args": [
"build",
"--bin=gbserver",
"--package=gbserver"
],
"filter": {
"name": "gbserver",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'gbserver'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=gbserver",
"--package=gbserver"
],
"filter": {
"name": "gbserver",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

19
Cargo.lock generated
View file

@ -409,6 +409,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
@ -1750,6 +1756,7 @@ dependencies = [
"mailparse",
"minio",
"native-tls",
"num-format",
"regex",
"reqwest 0.11.27",
"rhai",
@ -2599,7 +2606,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"arrayvec 0.5.2",
"bitflags 1.3.2",
"cfg-if",
"ryu",
@ -2994,6 +3001,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-format"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
dependencies = [
"arrayvec 0.7.6",
"itoa",
]
[[package]]
name = "num-integer"
version = "0.1.46"

View file

@ -60,3 +60,4 @@ urlencoding = "2.1"
regex = "1.10"
uuid = { version = "1.4", features = ["serde", "v4"] } # v4, v7, etc. as needed
zip = "4.3.0"
num-format = "0.4"

View file

@ -47,7 +47,7 @@ async fn main() -> std::io::Result<()> {
ensure_llama_servers_running()
.await
.expect("Failed to initialize LLM local server.");
initialize_browser_pool()
.await
.expect("Failed to initialize browser pool");

View file

@ -52,7 +52,7 @@ impl AutomationService {
sqlx::query_as::<_, Automation>(
r#"
SELECT id, kind, target, schedule, param, is_active, last_triggered
FROM system_automations
FROM public.system_automations
WHERE is_active = true
"#,
)
@ -122,7 +122,7 @@ impl AutomationService {
async fn update_last_triggered(&self, automation_id: Uuid) {
if let Some(pool) = &self.state.db {
if let Err(e) = sqlx::query!(
"UPDATE system_automations SET last_triggered = $1 WHERE id = $2",
"UPDATE public.system_automations SET last_triggered = $1 WHERE id = $2",
Utc::now(),
automation_id
)

View file

@ -497,7 +497,7 @@ pub async fn save_click(
state: web::Data<AppState>,
) -> HttpResponse {
let (campaign_id, email) = path.into_inner();
let _ = sqlx::query("INSERT INTO clicks (campaign_id, email, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (campaign_id, email) DO UPDATE SET updated_at = NOW()")
let _ = sqlx::query("INSERT INTO public.clicks (campaign_id, email, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (campaign_id, email) DO UPDATE SET updated_at = NOW()")
.bind(campaign_id)
.bind(email)
.execute(state.db.as_ref().unwrap())

View file

@ -0,0 +1,193 @@
use rhai::{Dynamic, Engine};
use chrono::{NaiveDateTime, Timelike, Datelike};
use num_format::{Locale, ToFormattedString};
use std::str::FromStr;
pub fn format_keyword(engine: &mut Engine) {
engine
.register_custom_syntax(&["FORMAT", "$expr$", "$expr$"], false, {
move |context, inputs| {
let value_dyn = context.eval_expression_tree(&inputs[0])?;
let pattern_dyn = context.eval_expression_tree(&inputs[1])?;
let value_str = value_dyn.to_string();
let pattern = pattern_dyn.to_string();
// --- NUMÉRICO ---
if let Ok(num) = f64::from_str(&value_str) {
let formatted = if pattern.starts_with("N") || pattern.starts_with("C") {
// extrai partes: prefixo, casas decimais, locale
let (prefix, decimals, locale_tag) = parse_pattern(&pattern);
let locale = get_locale(&locale_tag);
let symbol = if prefix == "C" {
get_currency_symbol(&locale_tag)
} else {
""
};
let int_part = num.trunc() as i64;
let frac_part = num.fract();
if decimals == 0 {
format!("{}{}", symbol, int_part.to_formatted_string(&locale))
} else {
let frac_scaled =
((frac_part * 10f64.powi(decimals as i32)).round()) as i64;
format!(
"{}{}.{:0width$}",
symbol,
int_part.to_formatted_string(&locale),
frac_scaled,
width = decimals
)
}
} else {
match pattern.as_str() {
"n" => format!("{:.2}", num),
"F" => format!("{:.2}", num),
"f" => format!("{}", num),
"0%" => format!("{:.0}%", num * 100.0),
_ => format!("{}", num),
}
};
return Ok(Dynamic::from(formatted));
}
// --- DATA ---
if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") {
let formatted = apply_date_format(&dt, &pattern);
return Ok(Dynamic::from(formatted));
}
// --- TEXTO ---
let formatted = apply_text_placeholders(&value_str, &pattern);
Ok(Dynamic::from(formatted))
}
})
.unwrap();
}
// ======================
// Extração de locale + precisão
// ======================
fn parse_pattern(pattern: &str) -> (String, usize, String) {
let mut prefix = String::new();
let mut decimals: usize = 2; // padrão 2 casas
let mut locale_tag = "en".to_string();
// ex: "C2[pt]" ou "N3[fr]"
if pattern.starts_with('C') {
prefix = "C".to_string();
} else if pattern.starts_with('N') {
prefix = "N".to_string();
}
// procura número após prefixo
let rest = &pattern[1..];
let mut num_part = String::new();
for ch in rest.chars() {
if ch.is_ascii_digit() {
num_part.push(ch);
} else {
break;
}
}
if !num_part.is_empty() {
decimals = num_part.parse().unwrap_or(2);
}
// procura locale entre colchetes
if let Some(start) = pattern.find('[') {
if let Some(end) = pattern.find(']') {
if end > start {
locale_tag = pattern[start + 1..end].to_string();
}
}
}
(prefix, decimals, locale_tag)
}
fn get_locale(tag: &str) -> Locale {
match tag {
"en" => Locale::en,
"fr" => Locale::fr,
"de" => Locale::de,
"pt" => Locale::pt,
"it" => Locale::it,
"es" => Locale::es,
_ => Locale::en,
}
}
fn get_currency_symbol(tag: &str) -> &'static str {
match tag {
"en" => "$",
"pt" => "R$ ",
"fr" | "de" | "es" | "it" => "",
_ => "$",
}
}
// ==================
// SUPORTE A DATAS
// ==================
fn apply_date_format(dt: &NaiveDateTime, pattern: &str) -> String {
let mut output = pattern.to_string();
let year = dt.year();
let month = dt.month();
let day = dt.day();
let hour24 = dt.hour();
let minute = dt.minute();
let second = dt.second();
let millis = dt.and_utc().timestamp_subsec_millis();
output = output.replace("yyyy", &format!("{:04}", year));
output = output.replace("yy", &format!("{:02}", year % 100));
output = output.replace("MM", &format!("{:02}", month));
output = output.replace("M", &format!("{}", month));
output = output.replace("dd", &format!("{:02}", day));
output = output.replace("d", &format!("{}", day));
output = output.replace("HH", &format!("{:02}", hour24));
output = output.replace("H", &format!("{}", hour24));
let mut hour12 = hour24 % 12;
if hour12 == 0 { hour12 = 12; }
output = output.replace("hh", &format!("{:02}", hour12));
output = output.replace("h", &format!("{}", hour12));
output = output.replace("mm", &format!("{:02}", minute));
output = output.replace("m", &format!("{}", minute));
output = output.replace("ss", &format!("{:02}", second));
output = output.replace("s", &format!("{}", second));
output = output.replace("fff", &format!("{:03}", millis));
output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" });
output = output.replace("t", if hour24 < 12 { "A" } else { "P" });
output
}
// ==================
// SUPORTE A TEXTO
// ==================
fn apply_text_placeholders(value: &str, pattern: &str) -> String {
let mut result = String::new();
for ch in pattern.chars() {
match ch {
'@' => result.push_str(value),
'&' | '<' => result.push_str(&value.to_lowercase()),
'>' | '!' => result.push_str(&value.to_uppercase()),
_ => result.push(ch), // copia qualquer caractere literal
}
}
result
}

View file

@ -0,0 +1,22 @@
use rhai::Dynamic;
use rhai::Engine;
pub fn last_keyword(engine: &mut Engine) {
engine
.register_custom_syntax(&["LAST", "$expr$"], false, {
move |context, inputs| {
let input_string = context.eval_expression_tree(&inputs[0])?;
let input_str = input_string.to_string();
// Extrai a última palavra dividindo por espaço
let last_word = input_str
.split_whitespace()
.last()
.unwrap_or("")
.to_string();
Ok(Dynamic::from(last_word))
}
})
.unwrap();
}

View file

@ -2,6 +2,8 @@ pub mod create_draft;
pub mod create_site;
pub mod find;
pub mod first;
pub mod last;
pub mod format;
pub mod for_next;
pub mod get;
pub mod get_website;

View file

@ -3,7 +3,7 @@ use dotenv::dotenv;
use log::{error, info};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use std::{env, process::Command};
use tokio::time::{sleep, Duration};
// OpenAI-compatible request/response structures
@ -188,15 +188,26 @@ async fn start_llm_server(
std::env::set_var("OMP_PLACES", "cores");
std::env::set_var("OMP_PROC_BIND", "close");
let mut cmd = Command::new("sh");
// "cd {} && numactl --interleave=all ./llama-server -m {} --host 0.0.0.0 --port {} --threads 20 --threads-batch 40 --temp 0.7 --parallel 1 --repeat-penalty 1.1 --ctx-size 8192 --batch-size 8192 -n 4096 --mlock --no-mmap --flash-attn --no-kv-offload --no-mmap &",
let mut cmd = tokio::process::Command::new("sh");
cmd.arg("-c").arg(format!(
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99 &",
llama_cpp_path, model_path, port
));
#[cfg(target_os = "linux")]
{
cmd.arg("-c").arg(format!(
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99 &",
llama_cpp_path, model_path, port
));
}
#[cfg(target_os = "windows")]
{
cmd.arg("/C").arg(format!(
"cd {} && llama-server.exe -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99",
llama_cpp_path, model_path, port
));
}
cmd.spawn()?;
Ok(())
}
@ -206,13 +217,25 @@ async fn start_embedding_server(
url: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let port = url.split(':').last().unwrap_or("8082");
let mut cmd = Command::new("cmd");
let mut cmd = tokio::process::Command::new("sh");
cmd.arg("-c").arg(format!(
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &",
llama_cpp_path, model_path, port
));
#[cfg(target_os = "windows")]
{
cmd.arg("/C").arg(format!(
"cd {} && llama-server.exe -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99",
llama_cpp_path, model_path, port
));
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
cmd.arg("-c").arg(format!(
"cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &",
llama_cpp_path, model_path, port
));
}
cmd.spawn()?;
Ok(())
}
@ -431,7 +454,8 @@ struct LlamaCppEmbeddingRequest {
// FIXED: Handle the stupid nested array format
#[derive(Debug, Deserialize)]
struct LlamaCppEmbeddingResponseItem {
pub index: usize,
#[serde(rename = "index")]
pub _index: usize,
pub embedding: Vec<Vec<f32>>, // This is the up part - embedding is an array of arrays
}

View file

@ -2,6 +2,8 @@ use crate::services::keywords::create_draft::create_draft_keyword;
use crate::services::keywords::create_site::create_site_keyword;
use crate::services::keywords::find::find_keyword;
use crate::services::keywords::first::first_keyword;
use crate::services::keywords::last::last_keyword;
use crate::services::keywords::format::format_keyword;
use crate::services::keywords::for_next::for_keyword;
use crate::services::keywords::get::get_keyword;
use crate::services::keywords::get_website::get_website_keyword;
@ -32,6 +34,8 @@ impl ScriptService {
find_keyword(state, &mut engine);
for_keyword(state, &mut engine);
first_keyword(&mut engine);
last_keyword(&mut engine);
format_keyword(&mut engine);
llm_keyword(state, &mut engine);
get_website_keyword(state, &mut engine);
get_keyword(state, &mut engine);

View file

@ -10,6 +10,7 @@ use std::future::Future;
use std::pin::Pin;
use std::process::Command;
use std::sync::Arc;
use std::path::PathBuf;
use thirtyfour::{DesiredCapabilities, WebDriver};
use tokio::fs;
use tokio::sync::Semaphore;
@ -75,26 +76,32 @@ impl BrowserSetup {
})
}
async fn find_brave() -> Result<String, Box<dyn std::error::Error>> {
let possible_paths = vec![
// Windows
String::from(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"),
// macOS
String::from("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"),
// Linux
String::from("/usr/bin/brave-browser"),
String::from("/usr/bin/brave"),
];
async fn find_brave() -> Result<String, Box<dyn std::error::Error>> {
let mut possible_paths = vec![
// Windows - Program Files
String::from(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"),
// macOS
String::from("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"),
// Linux
String::from("/usr/bin/brave-browser"),
String::from("/usr/bin/brave"),
];
for path in possible_paths {
if fs::metadata(&path).await.is_ok() {
return Ok(path);
}
}
Err("Brave browser not found. Please install Brave first.".into())
// Windows - AppData (usuário atual)
if let Ok(local_appdata) = env::var("LOCALAPPDATA") {
let mut path = PathBuf::from(local_appdata);
path.push("BraveSoftware\\Brave-Browser\\Application\\brave.exe");
possible_paths.push(path.to_string_lossy().to_string());
}
async fn setup_chromedriver() -> Result<String, Box<dyn std::error::Error>> {
for path in possible_paths {
if fs::metadata(&path).await.is_ok() {
return Ok(path);
}
}
Err("Brave browser not found. Please install Brave first.".into())
} async fn setup_chromedriver() -> Result<String, Box<dyn std::error::Error>> {
// Create chromedriver directory in executable's parent directory
let mut chromedriver_dir = env::current_exe()?.parent().unwrap().to_path_buf();
chromedriver_dir.push("chromedriver");