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:
1 parent
4275295e4f
commit
84ea2dec19
51 files changed
+7857
-573
No files matched your search
Generated
+22
@@ -105,6 +105,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"futures-util",
|
||||
"headless_chrome",
|
||||
"html2text",
|
||||
@@ -1025,6 +1026,15 @@ dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@@ -1045,6 +1055,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
|
||||
@@ -44,3 +44,4 @@ futures-util = "0.3"
|
||||
regex = "1.10"
|
||||
walkdir = "2.5"
|
||||
tauri-plugin-autostart = "2"
|
||||
dirs = "5.0"
|
||||
@@ -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
@@ -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!())
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user