feat: add emoji-regex and remove-accents dependencies; enhance chat and setup pages

- Added `emoji-regex` and `remove-accents` packages to improve text processing capabilities.
- Implemented automatic Ollama startup in the chat page for better user experience.
- Enhanced setup page to include GPU selection based on hardware specifications.
- Updated chat input to support real-time validation and improved user feedback.
- Refactored message handling in chat to incorporate new processing features.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-26 16:12:16 -03:00
1 parent 4275295e4f
commit 84ea2dec19
51 files changed
+7857 -573

No files matched your search

+208
View File
@@ -0,0 +1,208 @@
use regex::Regex;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum QueryIntent {
Factual, // Requer busca web (fatos, notícias, dados atualizados)
Conversational, // Não requer busca (saudações, conversa)
Technical, // Pode requerer busca (documentação, tutoriais)
Opinion, // Não requer busca (opiniões, subjetivo)
Calculation, // Não requer busca (cálculos, matemática)
Unknown, // Fallback
}
pub struct IntentClassifier;
impl IntentClassifier {
/// Classifica a intenção de uma query usando heurísticas baseadas em palavras-chave
pub fn classify(query: &str) -> QueryIntent {
if query.is_empty() {
return QueryIntent::Unknown;
}
let query_lower = query.to_lowercase();
let query_normalized = Self::normalize_query(&query_lower);
// Scoring para cada intent
let mut scores: std::collections::HashMap<QueryIntent, i32> = std::collections::HashMap::new();
scores.insert(QueryIntent::Factual, 0);
scores.insert(QueryIntent::Conversational, 0);
scores.insert(QueryIntent::Technical, 0);
scores.insert(QueryIntent::Opinion, 0);
scores.insert(QueryIntent::Calculation, 0);
// Padrões para Factual
let factual_patterns = vec![
r"\b(o que|que|qual|quais|quem|onde|quando)\b",
r"\b(what|which|who|where|when)\b",
r"\b(como funciona|how does|how works)\b",
r"\b(preço|price|preco|custo|cost)\b",
r"\b(notícia|noticia|news|notícias|noticias)\b",
r"\b(hoje|today|agora|now|atual|current)\b",
r"\b(último|ultimo|última|ultima|latest|recent)\b",
r"\b(história|historia|history|origem|origin)\b",
r"\b(é|e|is|are|was|were)\b.*\?",
];
// Padrões para Conversational
let conversational_patterns = vec![
r"^(oi|olá|ola|hello|hi|hey)\s*$",
r"\b(como você está|how are you|como vai|how is it going)\b",
r"\b(obrigado|obrigada|thanks|thank you|thank)\b",
r"\b(por favor|please|por favor)\b",
r"\b(tchau|bye|goodbye|até logo|see you)\b",
r"\b(bom dia|good morning|boa tarde|good afternoon|boa noite|good night)\b",
];
// Padrões para Technical
let technical_patterns = vec![
r"\b(como fazer|how to|how do|tutorial|tutoriais)\b",
r"\b(documentação|documentacao|documentation|docs)\b",
r"\b(exemplo|example|exemplos|examples)\b",
r"\b(código|codigo|code|implementação|implementacao|implementation)\b",
r"\b(api|sdk|framework|library|biblioteca)\b",
r"\b(erro|error|bug|problema|problem|issue)\b",
r"\b(guia|guide|manual|instalação|instalacao|installation)\b",
];
// Padrões para Opinion
let opinion_patterns = vec![
r"\b(você acha|you think|você pensa|you believe)\b",
r"\b(opinião|opiniao|opinion|pensar sobre|think about)\b",
r"\b(gostar|like|preferir|prefer|gosto|taste)\b",
r"\b(melhor|best|pior|worst|recomendar|recommend)\b",
r"\b(concordar|agree|discordar|disagree)\b",
];
// Padrões para Calculation
let calculation_patterns = vec![
r"\b(calcular|calculate|calcule|calculo)\b",
r"\b(quanto é|how much|quanto|how many)\b",
r"\b(\d+\s*[+\-*/]\s*\d+)", // Expressões matemáticas básicas
r"\b(soma|sum|subtração|subtraction|multiplicação|multiplication|divisão|division)\b",
r"\b(porcentagem|percentage|percent|por cento)\b",
];
// Calcular scores
Self::score_patterns(&query_normalized, &factual_patterns, &mut scores, QueryIntent::Factual);
Self::score_patterns(&query_normalized, &conversational_patterns, &mut scores, QueryIntent::Conversational);
Self::score_patterns(&query_normalized, &technical_patterns, &mut scores, QueryIntent::Technical);
Self::score_patterns(&query_normalized, &opinion_patterns, &mut scores, QueryIntent::Opinion);
Self::score_patterns(&query_normalized, &calculation_patterns, &mut scores, QueryIntent::Calculation);
// Encontrar intent com maior score
let mut max_score = 0;
let mut best_intent = QueryIntent::Unknown;
for (intent, score) in scores.iter() {
if *score > max_score {
max_score = *score;
best_intent = intent.clone();
}
}
// Se não houver score significativo, retornar Unknown
if max_score == 0 {
QueryIntent::Unknown
} else {
best_intent
}
}
/// Normaliza a query removendo acentos e caracteres especiais (simplificado)
fn normalize_query(query: &str) -> String {
query
.chars()
.map(|c| {
match c {
'á' | 'à' | 'ã' | 'â' | 'ä' => 'a',
'é' | 'è' | 'ê' | 'ë' => 'e',
'í' | 'ì' | 'î' | 'ï' => 'i',
'ó' | 'ò' | 'õ' | 'ô' | 'ö' => 'o',
'ú' | 'ù' | 'û' | 'ü' => 'u',
'ç' => 'c',
'ñ' => 'n',
'Á' | 'À' | 'Ã' | 'Â' | 'Ä' => 'a',
'É' | 'È' | 'Ê' | 'Ë' => 'e',
'Í' | 'Ì' | 'Î' | 'Ï' => 'i',
'Ó' | 'Ò' | 'Õ' | 'Ô' | 'Ö' => 'o',
'Ú' | 'Ù' | 'Û' | 'Ü' => 'u',
'Ç' => 'c',
'Ñ' => 'n',
_ => c,
}
})
.collect::<String>()
.to_lowercase()
}
/// Calcula score para um conjunto de padrões
fn score_patterns(
query: &str,
patterns: &[&str],
scores: &mut std::collections::HashMap<QueryIntent, i32>,
intent: QueryIntent,
) {
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if re.is_match(query) {
*scores.entry(intent.clone()).or_insert(0) += 1;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factual_intent() {
assert_eq!(
IntentClassifier::classify("O que é Python?"),
QueryIntent::Factual
);
assert_eq!(
IntentClassifier::classify("Qual o preço do Bitcoin?"),
QueryIntent::Factual
);
}
#[test]
fn test_conversational_intent() {
assert_eq!(
IntentClassifier::classify("Oi, como você está?"),
QueryIntent::Conversational
);
assert_eq!(
IntentClassifier::classify("Obrigado!"),
QueryIntent::Conversational
);
}
#[test]
fn test_technical_intent() {
assert_eq!(
IntentClassifier::classify("Como fazer um loop em Python?"),
QueryIntent::Technical
);
assert_eq!(
IntentClassifier::classify("Documentação da API"),
QueryIntent::Technical
);
}
#[test]
fn test_calculation_intent() {
assert_eq!(
IntentClassifier::classify("Quanto é 2 + 2?"),
QueryIntent::Calculation
);
assert_eq!(
IntentClassifier::classify("Calcular porcentagem"),
QueryIntent::Calculation
);
}
}
+644 -29
View File
@@ -1,6 +1,7 @@
use std::process::{Command, Stdio, Child};
use std::io::{BufRead, BufReader, Write};
use std::time::Duration;
use std::io::{BufRead, BufReader, Write, Read};
use std::time::{Duration, Instant};
use futures_util::StreamExt;
use std::fs;
use std::path::PathBuf;
use std::collections::HashMap;
@@ -18,6 +19,7 @@ mod task_executor;
mod scheduler_loop;
mod sources_config;
mod system_monitor;
mod intent_classifier;
use web_scraper::{
ScrapedContent,
@@ -34,10 +36,9 @@ use web_scraper::{
use headless_chrome::Browser;
use scheduler::{SentinelTask, SchedulerService, SchedulerState, TaskAction};
use sources_config::{SourcesConfig, load_sources_config, save_sources_config};
use system_monitor::{SystemStats, SystemMonitorState};
use system_monitor::{SystemStats, SystemMonitorState, GpuInfo};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
// CommandExt é importado localmente onde necessário
#[derive(serde::Serialize, serde::Deserialize, Clone)]
struct Message {
@@ -47,6 +48,27 @@ struct Message {
metadata: Option<serde_json::Value>,
}
#[derive(serde::Serialize)]
struct DownloadProgress {
status: String, // "pulling", "verifying", "success"
percent: Option<u8>, // 0-100
downloaded: Option<String>, // "552 MB"
total: Option<String>, // "1.2 GB"
speed: Option<String>, // "25 MB/s"
raw: String, // linha original para fallback
}
#[derive(serde::Deserialize)]
struct PullProgress {
status: String,
#[serde(default)]
digest: String, // Mantido para compatibilidade com API, mas não usado atualmente
#[serde(default)]
total: u64,
#[serde(default)]
completed: u64,
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
struct ChatSession {
id: String,
@@ -75,6 +97,7 @@ struct SystemSpecs {
total_memory: u64,
cpu_count: usize,
os_name: String,
gpus: Vec<GpuInfo>,
}
// SystemStats movido para system_monitor.rs
@@ -440,13 +463,39 @@ fn get_system_specs() -> SystemSpecs {
let mut sys = System::new_all();
sys.refresh_all();
// Detectar todas as GPUs
let gpus = system_monitor::detect_all_gpus();
SystemSpecs {
total_memory: sys.total_memory(),
cpu_count: sys.cpus().len(),
os_name: System::name().unwrap_or("Unknown".to_string()),
gpus,
}
}
/// Retorna o sistema operacional atual: 'windows', 'mac', ou 'linux'
#[command]
fn get_operating_system() -> String {
#[cfg(target_os = "windows")]
{
return "windows".to_string();
}
#[cfg(target_os = "macos")]
{
return "mac".to_string();
}
#[cfg(target_os = "linux")]
{
return "linux".to_string();
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
return "unknown".to_string();
}
#[command]
fn start_system_monitor(window: Window) {
std::thread::spawn(move || {
@@ -542,34 +591,248 @@ fn check_if_model_installed(name: String) -> bool {
}
}
#[command]
async fn pull_model(window: Window, name: String) -> Result<(), String> {
let mut child = Command::new("ollama")
.arg("pull")
.arg(&name)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| e.to_string())?;
let stdout = child.stdout.take().ok_or("Failed to open stdout")?;
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(l) => {
window.emit("download-progress", l).unwrap_or(());
// Função auxiliar para ler linha até encontrar \r ou \n (mantida para fallback)
#[allow(dead_code)]
fn read_line_until_delimiter<R: Read>(reader: &mut BufReader<R>, buffer: &mut Vec<u8>) -> Result<usize, std::io::Error> {
buffer.clear();
let mut byte = [0u8; 1];
let mut count = 0;
loop {
match reader.read(&mut byte)? {
0 => break, // EOF
_ => {
if byte[0] == b'\r' {
// Se for \r, verificar se o próximo é \n e pular ambos
let mut peek = [0u8; 1];
if reader.read(&mut peek).unwrap_or(0) > 0 && peek[0] == b'\n' {
// É \r\n, já consumimos ambos
} else {
// É apenas \r, já consumimos
}
break;
} else if byte[0] == b'\n' {
break;
}
buffer.push(byte[0]);
count += 1;
}
Err(_) => break,
}
}
let status = child.wait().map_err(|e| e.to_string())?;
if status.success() {
Ok(())
Ok(count)
}
// Função auxiliar para formatar bytes em formato legível
fn format_bytes(bytes: u64) -> Option<String> {
if bytes == 0 {
return None;
}
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
Some(if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
Err("Failed to pull model".to_string())
format!("{} B", bytes)
})
}
// Função para parsear linha do Ollama e extrair informações (mantida para fallback)
#[allow(dead_code)]
fn parse_ollama_progress(line: &str) -> DownloadProgress {
let line_lower = line.to_lowercase();
let mut status = "downloading".to_string();
let mut percent: Option<u8> = None;
let mut downloaded: Option<String> = None;
let mut total: Option<String> = None;
let mut speed: Option<String> = None;
// Determinar status
if line_lower.contains("pulling manifest") || line_lower.contains("pulling") {
status = "pulling".to_string();
} else if line_lower.contains("verifying") {
status = "verifying".to_string();
} else if line_lower.contains("writing manifest") {
status = "writing".to_string();
} else if line_lower.contains("success") || line_lower.contains("complete") || line_lower.contains("pulled") {
status = "success".to_string();
}
// Extrair porcentagem: "45%" ou "45 %"
if let Some(caps) = regex::Regex::new(r"(\d+)\s*%").unwrap().captures(line) {
if let Ok(p) = caps[1].parse::<u8>() {
percent = Some(p);
}
}
// Extrair tamanho baixado/total: "552 MB/1.2 GB" ou "552MB / 1.2GB"
if let Some(caps) = regex::Regex::new(r"(\d+(?:\.\d+)?)\s*([KMGT]?B)\s*/\s*(\d+(?:\.\d+)?)\s*([KMGT]?B)").unwrap().captures(line) {
downloaded = Some(format!("{} {}", &caps[1], &caps[2]));
total = Some(format!("{} {}", &caps[3], &caps[4]));
}
// Extrair velocidade: "25 MB/s" ou "25MB/s"
if let Some(caps) = regex::Regex::new(r"(\d+(?:\.\d+)?)\s*([KMGT]?B/s)").unwrap().captures(line) {
speed = Some(format!("{} {}", &caps[1], &caps[2]));
}
DownloadProgress {
status,
percent,
downloaded,
total,
speed,
raw: line.to_string(),
}
}
#[command]
async fn pull_model(window: Window, name: String) -> Result<(), String> {
let client = reqwest::Client::new();
// Fazer requisição POST para API do Ollama com streaming
let response = client
.post("http://localhost:11434/api/pull")
.json(&serde_json::json!({ "name": name, "stream": true }))
.send()
.await
.map_err(|e| format!("Failed to connect to Ollama API: {}", e))?;
if !response.status().is_success() {
return Err(format!("Ollama API returned error: {}", response.status()));
}
let mut stream = response.bytes_stream();
let mut buffer = String::new();
let mut last_completed: u64 = 0;
let mut last_time = Instant::now();
// Processar stream NDJSON (Newline Delimited JSON)
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| format!("Stream error: {}", e))?;
let chunk_str = String::from_utf8_lossy(&chunk);
buffer.push_str(&chunk_str);
// Processar linhas completas (separadas por \n)
while let Some(pos) = buffer.find('\n') {
let line = buffer[..pos].trim().to_string();
buffer = buffer[pos + 1..].to_string();
if line.is_empty() {
continue;
}
// Tentar deserializar como PullProgress
match serde_json::from_str::<PullProgress>(&line) {
Ok(json_progress) => {
// Calcular porcentagem se tiver total/completed
let percent = if json_progress.total > 0 {
Some(((json_progress.completed as f64 / json_progress.total as f64) * 100.0) as u8)
} else {
None
};
// Calcular velocidade
let now = Instant::now();
let delta_time = now.duration_since(last_time).as_secs_f64();
let speed = if delta_time > 0.0 && json_progress.completed > last_completed {
let delta_bytes = json_progress.completed - last_completed;
let bytes_per_sec = delta_bytes as f64 / delta_time;
Some(format_speed(bytes_per_sec))
} else {
None
};
last_completed = json_progress.completed;
last_time = now;
// Criar DownloadProgress estruturado
let progress = DownloadProgress {
status: json_progress.status.clone(),
percent,
downloaded: format_bytes(json_progress.completed),
total: format_bytes(json_progress.total),
speed,
raw: line.clone(),
};
// Emitir evento para frontend
if let Ok(json) = serde_json::to_string(&progress) {
window.emit("download-progress", json).unwrap_or(());
}
// Se status for "success", finalizar
if json_progress.status == "success" {
let success_progress = DownloadProgress {
status: "success".to_string(),
percent: Some(100),
downloaded: format_bytes(json_progress.completed),
total: format_bytes(json_progress.total),
speed: None,
raw: "success".to_string(),
};
if let Ok(json) = serde_json::to_string(&success_progress) {
window.emit("download-progress", json).unwrap_or(());
}
return Ok(());
}
}
Err(_) => {
// Se não conseguir parsear como JSON, tratar como linha raw (fallback)
let progress = DownloadProgress {
status: "downloading".to_string(),
percent: None,
downloaded: None,
total: None,
speed: None,
raw: line,
};
if let Ok(json) = serde_json::to_string(&progress) {
window.emit("download-progress", json).unwrap_or(());
}
}
}
}
}
// Se chegou aqui, o stream terminou sem "success" explícito
// Emitir sucesso final
let success_progress = DownloadProgress {
status: "success".to_string(),
percent: Some(100),
downloaded: format_bytes(last_completed),
total: None,
speed: None,
raw: "success".to_string(),
};
if let Ok(json) = serde_json::to_string(&success_progress) {
window.emit("download-progress", json).unwrap_or(());
}
Ok(())
}
// Função auxiliar para formatar velocidade
fn format_speed(bytes_per_sec: f64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
if bytes_per_sec >= GB {
format!("{:.1} GB/s", bytes_per_sec / GB)
} else if bytes_per_sec >= MB {
format!("{:.1} MB/s", bytes_per_sec / MB)
} else if bytes_per_sec >= KB {
format!("{:.1} KB/s", bytes_per_sec / KB)
} else {
format!("{:.0} B/s", bytes_per_sec)
}
}
@@ -589,6 +852,84 @@ async fn check_ollama_running() -> bool {
}
}
/// Verificação completa do Ollama: instalação e execução
#[derive(serde::Serialize)]
struct OllamaCheckResult {
installed: bool,
running: bool,
status: String, // "not_installed" | "installed_stopped" | "running"
}
/// Inicia o Ollama automaticamente se estiver instalado mas não estiver rodando
#[command]
async fn auto_start_ollama() -> Result<bool, String> {
// Verificar se está instalado
let installed = check_ollama_installed();
if !installed {
log::info!("Ollama não está instalado, pulando inicialização automática");
return Ok(false);
}
// Verificar se já está rodando
let running = check_ollama_running().await;
if running {
log::info!("Ollama já está rodando");
return Ok(true);
}
// Tentar iniciar
log::info!("Iniciando Ollama automaticamente...");
match start_ollama_server() {
Ok(_) => {
// Aguardar um pouco para o servidor iniciar
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Verificar se iniciou com sucesso
let is_running = check_ollama_running().await;
if is_running {
log::info!("Ollama iniciado com sucesso");
Ok(true)
} else {
log::warn!("Ollama foi iniciado mas ainda não está respondendo");
Ok(false)
}
}
Err(e) => {
log::error!("Falha ao iniciar Ollama automaticamente: {}", e);
Err(e)
}
}
}
#[command]
async fn check_ollama_full() -> Result<OllamaCheckResult, String> {
let installed = check_ollama_installed();
if !installed {
return Ok(OllamaCheckResult {
installed: false,
running: false,
status: "not_installed".to_string(),
});
}
let running = check_ollama_running().await;
if !running {
return Ok(OllamaCheckResult {
installed: true,
running: false,
status: "installed_stopped".to_string(),
});
}
Ok(OllamaCheckResult {
installed: true,
running: true,
status: "running".to_string(),
})
}
#[command]
fn start_ollama_server() -> Result<(), String> {
let mut cmd = Command::new("ollama");
@@ -596,6 +937,7 @@ fn start_ollama_server() -> Result<(), String> {
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
@@ -759,6 +1101,7 @@ fn start_mcp_server(
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
@@ -864,6 +1207,7 @@ fn restart_mcp_server(
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
@@ -970,6 +1314,7 @@ fn restart_all_mcp_servers(
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
@@ -1518,6 +1863,228 @@ fn save_sources_config_command(app_handle: AppHandle, config: SourcesConfig) ->
save_sources_config(&app_handle, config)
}
// ========== Ollama Installer Download Commands ==========
/// Verifica se uma URL de download está disponível
#[command]
async fn check_download_url(url: String) -> Result<bool, String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
match client.head(&url).send().await {
Ok(response) => Ok(response.status().is_success()),
Err(_) => Ok(false),
}
}
/// Obtém o caminho do instalador local se existir
#[command]
fn get_local_installer_path(filename: String, app_handle: AppHandle) -> Result<Option<String>, String> {
// Tentar no diretório do executável (dev e produção)
// Em desenvolvimento, os arquivos estão em public/ relativo ao projeto
// Em produção, tentamos encontrar o arquivo em vários locais possíveis
if let Ok(exe_dir) = app_handle.path().executable_dir() {
// Tentar vários caminhos possíveis
let possible_paths = vec![
// Caminho relativo ao executável (dev) - subir até a raiz do projeto
exe_dir.parent()
.and_then(|p| p.parent())
.map(|p| p.join("public").join("installers").join(&filename)),
// Caminho direto do executável (dev)
exe_dir.parent()
.map(|p| p.join("public").join("installers").join(&filename)),
// Caminho absoluto do workspace (dev) - tentar encontrar a raiz
exe_dir.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.map(|p| p.join("public").join("installers").join(&filename)),
// Em produção, arquivos podem estar no diretório do executável
Some(exe_dir.join("installers").join(&filename)),
// Ou no diretório pai do executável
exe_dir.parent()
.map(|p| p.join("installers").join(&filename)),
];
for path_opt in possible_paths {
if let Some(path) = path_opt {
if path.exists() {
return Ok(Some(path.to_string_lossy().to_string()));
}
}
}
}
Ok(None)
}
/// Faz download do instalador da URL oficial ou usa fallback local
#[command]
async fn download_installer(
url: String,
filename: String,
window: Window,
app_handle: AppHandle,
) -> Result<String, String> {
use std::io::Write;
use futures_util::StreamExt;
// Primeiro, tentar usar instalador local como fallback
if let Some(local_path) = get_local_installer_path(filename.clone(), app_handle.clone())? {
let local_path_buf = PathBuf::from(&local_path);
if local_path_buf.exists() {
// Copiar para app_data_dir/installers
let app_data_dir = app_handle.path().app_data_dir()
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
let installers_dir = app_data_dir.join("installers");
if !installers_dir.exists() {
fs::create_dir_all(&installers_dir)
.map_err(|e| format!("Failed to create installers directory: {}", e))?;
}
let dest_path = installers_dir.join(&filename);
fs::copy(&local_path_buf, &dest_path)
.map_err(|e| format!("Failed to copy local installer: {}", e))?;
window.emit("installer-download-progress", serde_json::json!({
"progress": 100,
"status": "Concluído (versão local)"
})).ok();
return Ok(dest_path.to_string_lossy().to_string());
}
}
// Fazer download da URL
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(300)) // 5 minutos de timeout
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to download installer: {}", e))?;
if !response.status().is_success() {
return Err(format!("Download failed with status: {}", response.status()));
}
// Obter tamanho total do arquivo
let total_size = response.content_length().unwrap_or(0);
// Criar diretório de instaladores
let app_data_dir = app_handle.path().app_data_dir()
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
let installers_dir = app_data_dir.join("installers");
if !installers_dir.exists() {
fs::create_dir_all(&installers_dir)
.map_err(|e| format!("Failed to create installers directory: {}", e))?;
}
let dest_path = installers_dir.join(&filename);
let mut file = fs::File::create(&dest_path)
.map_err(|e| format!("Failed to create file: {}", e))?;
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item.map_err(|e| format!("Failed to read chunk: {}", e))?;
file.write_all(&chunk)
.map_err(|e| format!("Failed to write chunk: {}", e))?;
downloaded += chunk.len() as u64;
// Emitir progresso
let progress = if total_size > 0 {
(downloaded * 100) / total_size
} else {
0
};
window.emit("installer-download-progress", serde_json::json!({
"progress": progress,
"downloaded": downloaded,
"total": total_size,
"status": format!("Baixando... {}%", progress)
})).ok();
}
window.emit("installer-download-progress", serde_json::json!({
"progress": 100,
"status": "Download concluído"
})).ok();
log::info!("Instalador baixado para: {:?}", dest_path);
Ok(dest_path.to_string_lossy().to_string())
}
/// Executa o instalador baixado
#[command]
fn run_installer(file_path: String) -> Result<(), String> {
let path = PathBuf::from(&file_path);
if !path.exists() {
return Err(format!("Instalador não encontrado: {}", file_path));
}
#[cfg(target_os = "windows")]
{
// No Windows, executar o .exe diretamente
Command::new(&path)
.spawn()
.map_err(|e| format!("Failed to run installer: {}", e))?;
}
#[cfg(target_os = "linux")]
{
// No Linux, dar permissão de execução e executar
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path)
.map_err(|e| format!("Failed to get file metadata: {}", e))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms)
.map_err(|e| format!("Failed to set executable permissions: {}", e))?;
Command::new(&path)
.spawn()
.map_err(|e| format!("Failed to run installer: {}", e))?;
}
#[cfg(target_os = "macos")]
{
// No macOS, executar o .zip (precisa ser extraído primeiro)
// Por enquanto, apenas abrir o arquivo
Command::new("open")
.arg(&path)
.spawn()
.map_err(|e| format!("Failed to open installer: {}", e))?;
}
log::info!("Instalador executado: {:?}", path);
Ok(())
}
/// Verifica se o instalador já foi baixado
#[command]
fn get_downloaded_installer_path(filename: String, app_handle: AppHandle) -> Result<Option<String>, String> {
let app_data_dir = app_handle.path().app_data_dir()
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
let installer_path = app_data_dir.join("installers").join(&filename);
if installer_path.exists() {
Ok(Some(installer_path.to_string_lossy().to_string()))
} else {
Ok(None)
}
}
// ========== Export & Backup Commands ==========
/// Exporta todos os dados do app (chats, tasks, sources, settings) para um arquivo ZIP
@@ -1671,6 +2238,19 @@ fn get_recent_logs(app_handle: AppHandle, lines: usize) -> Result<Vec<String>, S
}
}
/// Recebe logs do frontend e os imprime no terminal
#[command]
fn log_to_terminal(level: String, message: String) -> Result<(), String> {
match level.as_str() {
"info" => log::info!("{}", message),
"warn" => log::warn!("{}", message),
"error" => log::error!("{}", message),
"debug" => log::debug!("{}", message),
_ => log::info!("{}", message),
}
Ok(())
}
// ========== System Monitor Commands ==========
/// Obtém estatísticas do sistema em tempo real
@@ -1755,6 +2335,20 @@ async fn toggle_task(
}
}
#[command]
fn classify_intent(query: String) -> String {
use intent_classifier::{IntentClassifier, QueryIntent};
let intent = IntentClassifier::classify(&query);
match intent {
QueryIntent::Factual => "factual".to_string(),
QueryIntent::Conversational => "conversational".to_string(),
QueryIntent::Technical => "technical".to_string(),
QueryIntent::Opinion => "opinion".to_string(),
QueryIntent::Calculation => "calculation".to_string(),
QueryIntent::Unknown => "unknown".to_string(),
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -1798,6 +2392,17 @@ pub fn run() {
let app_handle = app.handle().clone();
let scheduler_clone = scheduler_state.clone();
// Inicializar Ollama automaticamente se estiver instalado
tauri::async_runtime::spawn(async move {
// Aguardar um pouco para o app inicializar completamente
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Tentar iniciar Ollama automaticamente
if let Err(e) = auto_start_ollama().await {
log::warn!("Falha ao iniciar Ollama automaticamente: {}", e);
}
});
// BrowserState não é mais necessário - o scheduler criará o browser quando necessário
// Usar o runtime async do Tauri ao invés de tokio::spawn
tauri::async_runtime::spawn(async move {
@@ -1826,6 +2431,7 @@ pub fn run() {
check_ollama_installed,
check_ollama_running,
get_system_specs,
get_operating_system,
check_if_model_installed,
pull_model,
start_ollama_server,
@@ -1862,12 +2468,21 @@ pub fn run() {
load_sources_config_command,
save_sources_config_command,
get_recent_logs,
log_to_terminal,
get_system_stats,
create_task,
list_tasks,
update_task,
delete_task,
toggle_task
toggle_task,
check_download_url,
get_local_installer_path,
download_installer,
run_installer,
get_downloaded_installer_path,
check_ollama_full,
auto_start_ollama,
classify_intent
])
.manage(Arc::new(Mutex::new(HashMap::<String, McpProcessHandle>::new())) as McpProcessMap)
.run(tauri::generate_context!())
+1
View File
@@ -152,6 +152,7 @@ impl SchedulerService {
}
/// Helper para obter diretório de tasks
#[allow(dead_code)]
pub fn get_tasks_file_path(app_handle: &AppHandle) -> Result<PathBuf, String> {
let app_data_dir = app_handle
.path()
+491 -27
View File
@@ -1,6 +1,15 @@
use serde::Serialize;
use sysinfo::System;
/// Informações sobre uma GPU
#[derive(Serialize, Clone, Debug)]
pub struct GpuInfo {
pub id: String,
pub name: String,
pub vendor: Option<String>,
pub memory_mb: Option<u64>,
}
/// Estatísticas do sistema em tempo real
#[derive(Serialize, Clone, Debug)]
pub struct SystemStats {
@@ -17,6 +26,7 @@ pub struct SystemStats {
/// Estado persistente do sistema para cálculo de CPU
pub struct SystemMonitorState {
system: System,
#[allow(dead_code)]
last_cpu_check: std::time::Instant,
}
@@ -89,65 +99,519 @@ impl SystemMonitorState {
}
/// Tenta detectar o nome da GPU (implementação básica)
/// Mantido para compatibilidade com SystemStats
fn detect_gpu_name() -> Option<String> {
let gpus = detect_all_gpus();
gpus.first().map(|gpu| gpu.name.clone())
}
/// Detecta todas as GPUs disponíveis no sistema
pub fn detect_all_gpus() -> Vec<GpuInfo> {
log::info!("Iniciando detecção de GPUs...");
let mut gpus = Vec::new();
// Primeiro, tentar obter informações do Ollama se estiver rodando
if let Ok(ollama_gpus) = detect_gpus_ollama_api() {
log::info!("GPUs detectadas via API do Ollama: {}", ollama_gpus.len());
gpus.extend(ollama_gpus);
}
#[cfg(target_os = "windows")]
{
use std::process::Command;
// Tentar múltiplas fontes de detecção
let windows_gpus = detect_gpus_windows();
// Tenta usar wmic para detectar GPU no Windows
if let Ok(output) = Command::new("wmic")
.args(&["path", "win32_VideoController", "get", "name"])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
for line in stdout.lines() {
let line = line.trim();
if !line.is_empty() && line != "Name" {
return Some(line.to_string());
// Mesclar com GPUs do Ollama, evitando duplicatas
for windows_gpu in windows_gpus {
if !gpus.iter().any(|g| g.name == windows_gpu.name) {
log::info!("GPU detectada via wmic: {}", windows_gpu.name);
gpus.push(windows_gpu);
}
}
// Se não encontrou GPUs NVIDIA, tentar nvidia-smi
let has_nvidia = gpus.iter().any(|g| g.vendor.as_ref().map(|v| v == "NVIDIA").unwrap_or(false));
if !has_nvidia {
log::info!("Tentando nvidia-smi como fallback...");
if let Ok(nvidia_gpus) = detect_gpus_nvidia_smi() {
// Mesclar resultados, evitando duplicatas
for nvidia_gpu in nvidia_gpus {
if !gpus.iter().any(|g| g.name == nvidia_gpu.name) {
log::info!("GPU detectada via nvidia-smi: {}", nvidia_gpu.name);
gpus.push(nvidia_gpu);
}
}
}
}
log::info!("Total de GPUs detectadas no Windows: {}", gpus.len());
}
#[cfg(target_os = "linux")]
{
use std::process::Command;
log::info!("Tentando detectar GPUs no Linux...");
gpus = detect_gpus_linux();
log::info!("Total de GPUs detectadas no Linux: {}", gpus.len());
}
#[cfg(target_os = "macos")]
{
log::info!("Tentando detectar GPUs no macOS...");
gpus = detect_gpus_macos();
log::info!("Total de GPUs detectadas no macOS: {}", gpus.len());
}
// Se não encontrou nenhuma GPU, retornar GPU genérica
if gpus.is_empty() {
log::warn!("Nenhuma GPU detectada, usando fallback genérico");
gpus.push(GpuInfo {
id: "gpu_default".to_string(),
name: "GPU não detectada".to_string(),
vendor: None,
memory_mb: None,
});
}
log::info!("Detecção de GPUs concluída: {} GPU(s) encontrada(s)", gpus.len());
gpus
}
/// Detecta GPUs no Windows usando wmic (formato CSV melhorado)
#[cfg(target_os = "windows")]
fn detect_gpus_windows() -> Vec<GpuInfo> {
use std::process::Command;
let mut gpus = Vec::new();
log::info!("Tentando detectar GPUs via wmic...");
// Tentar formato CSV primeiro (mais confiável)
if let Ok(output) = Command::new("wmic")
.args(&["path", "win32_VideoController", "get", "name,AdapterRAM,PNPDeviceID", "/format:csv"])
.output()
{
// wmic pode retornar UTF-16LE no Windows, precisamos converter
let stdout = if let Ok(utf8) = String::from_utf8(output.stdout.clone()) {
utf8
} else {
// Tentar UTF-16LE (little-endian)
let bytes = output.stdout;
let mut utf16_chars = Vec::new();
let mut i = 0;
while i + 1 < bytes.len() {
let low = bytes[i] as u16;
let high = bytes[i + 1] as u16;
utf16_chars.push(low | (high << 8));
i += 2;
}
String::from_utf16_lossy(&utf16_chars)
};
// Tenta usar lspci no Linux
if let Ok(output) = Command::new("lspci")
.args(&["-v", "-s", "$(lspci | grep VGA | cut -d' ' -f1)"])
log::debug!("wmic output (primeiros 500 chars): {}", stdout.chars().take(500).collect::<String>());
// Parse CSV: Node,Name,AdapterRAM,PNPDeviceID
let lines: Vec<&str> = stdout.lines().collect();
for (idx, line) in lines.iter().enumerate() {
if idx == 0 {
continue; // Skip header
}
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() >= 4 {
let name = parts[1].trim_matches('"').to_string();
if !name.is_empty() && name != "Name" {
let adapter_ram_str = parts[2].trim_matches('"');
let pnp_id = parts[3].trim_matches('"').to_string();
let memory_mb = if !adapter_ram_str.is_empty() && adapter_ram_str != "AdapterRAM" {
adapter_ram_str.parse::<u64>().ok().map(|bytes| bytes / (1024 * 1024))
} else {
None
};
let vendor = detect_vendor_from_name(&name);
let id = if !pnp_id.is_empty() && pnp_id != "PNPDeviceID" {
format!("gpu_{}", pnp_id.replace("\\", "_").replace("/", "_"))
} else {
format!("gpu_{}", gpus.len())
};
log::info!("GPU detectada via wmic: {} (VRAM: {:?} MB)", name, memory_mb);
gpus.push(GpuInfo {
id,
name,
vendor,
memory_mb,
});
}
}
}
} else {
log::warn!("Falha ao executar wmic, tentando formato list...");
// Fallback para formato list
if let Ok(output) = Command::new("wmic")
.args(&["path", "win32_VideoController", "get", "name,AdapterRAM,PNPDeviceID", "/format:list"])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
let mut current_gpu: Option<GpuInfo> = None;
for line in stdout.lines() {
if line.contains("VGA") || line.contains("Display") {
return Some(line.to_string());
let line = line.trim();
if line.is_empty() {
if let Some(gpu) = current_gpu.take() {
gpus.push(gpu);
}
continue;
}
if line.starts_with("Name=") {
let name = line.replace("Name=", "").trim().to_string();
if !name.is_empty() {
let vendor = detect_vendor_from_name(&name);
current_gpu = Some(GpuInfo {
id: format!("gpu_{}", gpus.len()),
name,
vendor,
memory_mb: None,
});
}
} else if line.starts_with("AdapterRAM=") {
if let Some(gpu) = &mut current_gpu {
if let Ok(memory_bytes) = line.replace("AdapterRAM=", "").trim().parse::<u64>() {
gpu.memory_mb = Some(memory_bytes / (1024 * 1024));
}
}
} else if line.starts_with("PNPDeviceID=") {
if let Some(gpu) = &mut current_gpu {
let pnp_id = line.replace("PNPDeviceID=", "").trim().to_string();
if !pnp_id.is_empty() {
gpu.id = format!("gpu_{}", pnp_id.replace("\\", "_").replace("/", "_"));
}
}
}
}
if let Some(gpu) = current_gpu {
gpus.push(gpu);
}
}
}
}
gpus
}
/// Detecta GPUs NVIDIA usando nvidia-smi
#[cfg(target_os = "windows")]
fn detect_gpus_nvidia_smi() -> Result<Vec<GpuInfo>, String> {
use std::process::Command;
log::info!("Executando nvidia-smi...");
let output = Command::new("nvidia-smi")
.args(&["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
.output()
.map_err(|e| format!("nvidia-smi não encontrado: {}", e))?;
if !output.status.success() {
return Err("nvidia-smi falhou".to_string());
}
let stdout = String::from_utf8(output.stdout)
.map_err(|e| format!("Erro ao parsear output do nvidia-smi: {}", e))?;
log::debug!("nvidia-smi output: {}", stdout);
let mut gpus = Vec::new();
for (idx, line) in stdout.lines().enumerate() {
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() >= 2 {
let name = parts[0].trim().to_string();
let memory_mb = parts[1].trim().parse::<u64>().ok();
if !name.is_empty() {
log::info!("GPU NVIDIA detectada via nvidia-smi: {} ({} MB)", name, memory_mb.unwrap_or(0));
gpus.push(GpuInfo {
id: format!("nvidia_gpu_{}", idx),
name,
vendor: Some("NVIDIA".to_string()),
memory_mb,
});
}
}
}
Ok(gpus)
}
/// Tenta detectar GPUs via API do Ollama (quando disponível)
/// Nota: O Ollama não expõe diretamente informações de GPU via API pública
/// Esta função verifica se o Ollama está rodando, mas a detecção real
/// é feita via métodos do sistema operacional (wmic, nvidia-smi, etc)
fn detect_gpus_ollama_api() -> Result<Vec<GpuInfo>, String> {
// Por enquanto, retornamos vazio pois o Ollama não tem endpoint público de GPU
// A detecção é feita via métodos do sistema operacional que são mais confiáveis
// Esta função pode ser expandida no futuro se o Ollama adicionar um endpoint de GPU
log::debug!("Verificação de API do Ollama não implementada (não há endpoint público de GPU)");
Ok(Vec::new())
}
/// Detecta GPUs no Linux usando múltiplas fontes
#[cfg(target_os = "linux")]
fn detect_gpus_linux() -> Vec<GpuInfo> {
use std::process::Command;
let mut gpus = Vec::new();
// 1. Tentar lspci primeiro
log::info!("Tentando lspci...");
if let Ok(output) = Command::new("lspci")
.args(&["-m", "-d", "::0300"]) // Apenas dispositivos VGA
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
for (index, line) in stdout.lines().enumerate() {
let parts: Vec<&str> = line.split('"').collect();
if parts.len() >= 2 {
let name = parts[1].trim().to_string();
if !name.is_empty() {
let vendor = detect_vendor_from_name(&name);
log::info!("GPU detectada via lspci: {}", name);
gpus.push(GpuInfo {
id: format!("gpu_{}", index),
name,
vendor,
memory_mb: None,
});
}
}
}
}
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
// Tenta usar system_profiler no macOS
if let Ok(output) = Command::new("system_profiler")
.args(&["SPDisplaysDataType"])
// 2. Se não encontrou, tentar método alternativo
if gpus.is_empty() {
log::info!("Tentando lspci | grep como fallback...");
if let Ok(output) = Command::new("sh")
.args(&["-c", "lspci | grep -i vga"])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
for line in stdout.lines() {
if line.contains("Chipset Model") {
return Some(line.replace("Chipset Model:", "").trim().to_string());
for (index, line) in stdout.lines().enumerate() {
let line = line.trim();
if !line.is_empty() && (line.contains("VGA") || line.contains("Display")) {
let parts: Vec<&str> = line.splitn(2, ':').collect();
if parts.len() >= 2 {
let name = parts[1].trim().to_string();
if !name.is_empty() {
let vendor = detect_vendor_from_name(&name);
log::info!("GPU detectada via lspci grep: {}", name);
gpus.push(GpuInfo {
id: format!("gpu_{}", index),
name,
vendor,
memory_mb: None,
});
}
}
}
}
}
}
}
// 3. Tentar nvidia-smi se não encontrou GPUs NVIDIA
let has_nvidia = gpus.iter().any(|g| g.vendor.as_ref().map(|v| v == "NVIDIA").unwrap_or(false));
if !has_nvidia {
log::info!("Tentando nvidia-smi...");
if let Ok(nvidia_gpus) = detect_gpus_nvidia_smi_linux() {
for nvidia_gpu in nvidia_gpus {
if !gpus.iter().any(|g| g.name == nvidia_gpu.name) {
gpus.push(nvidia_gpu);
}
}
}
}
// 4. Tentar /sys/class/drm/ como último recurso
if gpus.is_empty() {
log::info!("Tentando /sys/class/drm/...");
if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
for (idx, entry) in entries.enumerate() {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with("card") && name != "card" {
if let Ok(name_file) = std::fs::read_to_string(path.join("name")) {
let gpu_name = name_file.trim().to_string();
if !gpu_name.is_empty() {
let vendor = detect_vendor_from_name(&gpu_name);
log::info!("GPU detectada via /sys/class/drm/: {}", gpu_name);
gpus.push(GpuInfo {
id: format!("gpu_{}", idx),
name: gpu_name,
vendor,
memory_mb: None,
});
}
}
}
}
}
}
}
}
gpus
}
/// Detecta GPUs NVIDIA no Linux usando nvidia-smi
#[cfg(target_os = "linux")]
fn detect_gpus_nvidia_smi_linux() -> Result<Vec<GpuInfo>, String> {
use std::process::Command;
let output = Command::new("nvidia-smi")
.args(&["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
.output()
.map_err(|e| format!("nvidia-smi não encontrado: {}", e))?;
if !output.status.success() {
return Err("nvidia-smi falhou".to_string());
}
let stdout = String::from_utf8(output.stdout)
.map_err(|e| format!("Erro ao parsear output do nvidia-smi: {}", e))?;
let mut gpus = Vec::new();
for (idx, line) in stdout.lines().enumerate() {
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
if parts.len() >= 2 {
let name = parts[0].trim().to_string();
let memory_mb = parts[1].trim().parse::<u64>().ok();
if !name.is_empty() {
log::info!("GPU NVIDIA detectada via nvidia-smi: {} ({} MB)", name, memory_mb.unwrap_or(0));
gpus.push(GpuInfo {
id: format!("nvidia_gpu_{}", idx),
name,
vendor: Some("NVIDIA".to_string()),
memory_mb,
});
}
}
}
Ok(gpus)
}
/// Detecta GPUs no macOS usando system_profiler
#[cfg(target_os = "macos")]
fn detect_gpus_macos() -> Vec<GpuInfo> {
use std::process::Command;
let mut gpus = Vec::new();
log::info!("Executando system_profiler...");
if let Ok(output) = Command::new("system_profiler")
.args(&["SPDisplaysDataType"])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
let mut current_gpu: Option<GpuInfo> = None;
let mut gpu_index = 0;
for line in stdout.lines() {
let line = line.trim();
if line.starts_with("Chipset Model:") {
// Finalizar GPU anterior se houver
if let Some(gpu) = current_gpu.take() {
gpus.push(gpu);
}
let name = line.replace("Chipset Model:", "").trim().to_string();
if !name.is_empty() {
let vendor = detect_vendor_from_name(&name);
log::info!("GPU detectada no macOS: {}", name);
current_gpu = Some(GpuInfo {
id: format!("gpu_{}", gpu_index),
name,
vendor,
memory_mb: None,
});
gpu_index += 1;
}
} else if line.starts_with("VRAM (Total):") {
if let Some(gpu) = &mut current_gpu {
let vram_str = line.replace("VRAM (Total):", "").trim();
if let Some(mb) = parse_memory_string(&vram_str) {
gpu.memory_mb = Some(mb);
log::info!("VRAM detectada: {} MB", mb);
}
}
}
}
// Adicionar última GPU se houver
if let Some(gpu) = current_gpu {
gpus.push(gpu);
}
}
}
gpus
}
/// Detecta o vendor (fabricante) da GPU baseado no nome
fn detect_vendor_from_name(name: &str) -> Option<String> {
let name_lower = name.to_lowercase();
if name_lower.contains("nvidia") || name_lower.contains("geforce") || name_lower.contains("quadro") || name_lower.contains("tesla") {
return Some("NVIDIA".to_string());
}
if name_lower.contains("amd") || name_lower.contains("radeon") || name_lower.contains("firepro") {
return Some("AMD".to_string());
}
if name_lower.contains("intel") || name_lower.contains("iris") || name_lower.contains("uhd") || name_lower.contains("hd graphics") {
return Some("Intel".to_string());
}
if name_lower.contains("apple") {
return Some("Apple".to_string());
}
None
}
/// Parse string de memória (ex: "8 GB" -> 8192 MB)
#[allow(dead_code)]
fn parse_memory_string(s: &str) -> Option<u64> {
let s = s.trim().to_lowercase();
// Tentar extrair número e unidade
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.is_empty() {
return None;
}
if let Ok(value) = parts[0].parse::<f64>() {
let multiplier = if s.contains("gb") || s.contains("g") {
1024 // GB para MB
} else if s.contains("mb") || s.contains("m") {
1 // Já está em MB
} else {
// Assumir GB se não especificado
1024
};
return Some((value * multiplier as f64) as u64);
}
None
}
+158 -1
View File
@@ -1,5 +1,5 @@
use anyhow::Result;
use headless_chrome::{Browser, LaunchOptions};
use headless_chrome::{Browser, LaunchOptions, Tab};
use reqwest::header::USER_AGENT;
use scraper::{Html, Selector};
use std::sync::Arc;
@@ -715,6 +715,21 @@ fn fetch_and_convert_sync(browser: &Browser, url: &str) -> Result<ScrapedContent
return Err(anyhow::anyhow!("Timeout ao processar página"));
}
// Injetar script para bloquear autoplay de mídia
// Isso garante que nenhum vídeo/áudio seja reproduzido durante o scraping
match disable_media_autoplay(&tab) {
Ok(_) => {
log::debug!("Autoplay de mídia bloqueado para: {}", url);
}
Err(e) => {
log::warn!("Aviso: Falha ao bloquear autoplay em {}: {}", url, e);
// Não falhar o scraping por causa disso, apenas logar
}
}
// Aguardar pequeno delay para garantir que script foi executado
std::thread::sleep(Duration::from_millis(100));
// Extrair HTML renderizado
let content = match tab.get_content() {
Ok(c) => c,
@@ -773,10 +788,152 @@ fn fetch_and_convert_sync(browser: &Browser, url: &str) -> Result<ScrapedContent
}
}
/// Desabilita autoplay de mídia injetando JavaScript na página
/// Esta função pausa todos os elementos de vídeo/áudio e previne autoplay
fn disable_media_autoplay(tab: &Tab) -> Result<()> {
let script = r#"
(function() {
// Função para pausar todos os elementos de mídia
const pauseAllMedia = () => {
const mediaElements = document.querySelectorAll('video, audio');
let pausedCount = 0;
mediaElements.forEach(media => {
if (!media.paused) {
media.pause();
pausedCount++;
}
// Remover atributo autoplay
media.removeAttribute('autoplay');
media.autoplay = false;
// Silenciar mídia
media.muted = true;
media.volume = 0;
});
return pausedCount;
};
// Executar imediatamente
const initialPaused = pauseAllMedia();
// Observar mudanças no DOM para novos elementos de mídia
const observer = new MutationObserver(() => {
pauseAllMedia();
});
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Prevenir autoplay interceptando o método play()
const originalPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
// Bloquear play se página não estiver em foco ou estiver oculta
if (document.hidden || !document.hasFocus()) {
return Promise.reject(new Error('Autoplay blocked by scraper'));
}
// Pausar imediatamente após tentativa de play
const result = originalPlay.call(this);
if (result && typeof result.then === 'function') {
result.then(() => {
this.pause();
this.muted = true;
}).catch(() => {});
} else {
this.pause();
this.muted = true;
}
return Promise.reject(new Error('Autoplay blocked'));
};
// Pausar quando página perder foco
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
pauseAllMedia();
}
});
// Bloquear Web Audio API
if (window.AudioContext) {
const OriginalAudioContext = window.AudioContext;
window.AudioContext = function() {
console.warn('AudioContext blocked by scraper');
return null;
};
window.AudioContext.prototype = OriginalAudioContext.prototype;
}
if (window.webkitAudioContext) {
const OriginalWebkitAudioContext = window.webkitAudioContext;
window.webkitAudioContext = function() {
console.warn('webkitAudioContext blocked by scraper');
return null;
};
window.webkitAudioContext.prototype = OriginalWebkitAudioContext.prototype;
}
// Bloquear mídia em iframes também
const iframes = document.querySelectorAll('iframe');
iframes.forEach(iframe => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
const iframeMedia = iframeDoc.querySelectorAll('video, audio');
iframeMedia.forEach(media => {
media.pause();
media.muted = true;
media.volume = 0;
media.removeAttribute('autoplay');
});
}
} catch (e) {
// Ignorar erros de cross-origin
}
});
// Retornar contagem para logging
return initialPaused;
})();
"#;
match tab.evaluate(script, false) {
Ok(result) => {
// Tentar extrair contagem de elementos pausados do resultado
if let Some(count) = result.value {
log::info!("Script de bloqueio de mídia injetado: {} elementos pausados", count);
} else {
log::debug!("Script de bloqueio de mídia injetado com sucesso");
}
Ok(())
}
Err(e) => {
log::warn!("Erro ao injetar script de bloqueio de mídia: {}", e);
// Não falhar o scraping por causa disso, apenas logar
Ok(())
}
}
}
/// Cria uma instância do Browser (singleton para reutilização)
pub fn create_browser() -> Result<Browser> {
use std::ffi::OsStr;
// Argumentos do Chrome para bloquear autoplay de mídia
// Nota: O bloqueio principal será feito via JavaScript injection, mas esses args ajudam
let chrome_args: Vec<&OsStr> = vec![
OsStr::new("--autoplay-policy=document-user-activation-required"), // Exige interação do usuário para autoplay
OsStr::new("--disable-background-media-playback"), // Desabilita reprodução de mídia em segundo plano
OsStr::new("--mute-audio"), // Silencia todo áudio (mais agressivo, mas garante silêncio)
OsStr::new("--disable-features=AutoplayIgnoreWebAudio"), // Desabilita autoplay de Web Audio
];
let options = LaunchOptions {
headless: true,
args: chrome_args,
..Default::default()
};