- Adiciona core🪟allow-start-dragging para corrigir erro ACL do drag-region - Adiciona logging detalhado no ChatHubWidget para debug de clicks - Adiciona logging no comando open_chat_window para diagnostico - Ajusta ordem size/position e set_ignore_cursor_events no Hub - Remove set_hub_minimized apos build para evitar conflitos de timing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
826 lines
27 KiB
Rust
826 lines
27 KiB
Rust
mod agent;
|
|
mod chat;
|
|
#[cfg(target_os = "windows")]
|
|
mod rustdesk;
|
|
#[cfg(target_os = "windows")]
|
|
mod service_client;
|
|
mod usb_control;
|
|
|
|
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
|
|
use chat::{ChatRuntime, ChatSession, ChatMessagesResponse, SendMessageResponse};
|
|
use chrono::Local;
|
|
use usb_control::{UsbPolicy, UsbPolicyResult};
|
|
use tauri::{Emitter, Listener, Manager, WindowEvent};
|
|
use tauri_plugin_store::Builder as StorePluginBuilder;
|
|
use std::fs::OpenOptions;
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::OnceLock;
|
|
#[cfg(target_os = "windows")]
|
|
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
|
#[cfg(target_os = "windows")]
|
|
use tauri::tray::TrayIconBuilder;
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::enums::*;
|
|
#[cfg(target_os = "windows")]
|
|
use winreg::RegKey;
|
|
|
|
const DEFAULT_CONVEX_URL: &str = "https://convex.esdrasrenan.com.br";
|
|
|
|
// ============================================================================
|
|
// Sistema de Logging para Agente
|
|
// ============================================================================
|
|
|
|
static AGENT_LOG_FILE: OnceLock<std::sync::Mutex<std::fs::File>> = OnceLock::new();
|
|
|
|
pub fn init_agent_logging() -> Result<(), String> {
|
|
let dir = logs_directory()
|
|
.ok_or("LOCALAPPDATA indisponivel para logging")?;
|
|
|
|
std::fs::create_dir_all(&dir)
|
|
.map_err(|e| format!("Falha ao criar diretorio de logs: {e}"))?;
|
|
|
|
let path = dir.join("raven-agent.log");
|
|
let file = OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&path)
|
|
.map_err(|e| format!("Falha ao abrir raven-agent.log: {e}"))?;
|
|
|
|
let _ = AGENT_LOG_FILE.set(std::sync::Mutex::new(file));
|
|
Ok(())
|
|
}
|
|
|
|
pub fn log_agent(level: &str, message: &str) {
|
|
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
let line = format!("[{timestamp}] [{level}] {message}\n");
|
|
|
|
// Escreve para stderr (util em dev/debug)
|
|
eprint!("{line}");
|
|
|
|
// Escreve para arquivo
|
|
if let Some(mutex) = AGENT_LOG_FILE.get() {
|
|
if let Ok(mut file) = mutex.lock() {
|
|
let _ = file.write_all(line.as_bytes());
|
|
let _ = file.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! log_info {
|
|
($($arg:tt)*) => {
|
|
$crate::log_agent("INFO", format!($($arg)*).as_str())
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! log_error {
|
|
($($arg:tt)*) => {
|
|
$crate::log_agent("ERROR", format!($($arg)*).as_str())
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! log_warn {
|
|
($($arg:tt)*) => {
|
|
$crate::log_agent("WARN", format!($($arg)*).as_str())
|
|
};
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RustdeskProvisioningResult {
|
|
pub id: String,
|
|
pub password: String,
|
|
pub installed_version: Option<String>,
|
|
pub updated: bool,
|
|
pub last_provisioned_at: i64,
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn collect_machine_profile() -> Result<MachineProfile, String> {
|
|
collect_profile().map_err(|error| error.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn collect_machine_inventory() -> Result<serde_json::Value, String> {
|
|
Ok(collect_inventory_plain())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn start_machine_agent(
|
|
state: tauri::State<AgentRuntime>,
|
|
base_url: String,
|
|
token: String,
|
|
status: Option<String>,
|
|
interval_seconds: Option<u64>,
|
|
) -> Result<(), String> {
|
|
state
|
|
.start_heartbeat(base_url, token, status, interval_seconds)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn stop_machine_agent(state: tauri::State<AgentRuntime>) -> Result<(), String> {
|
|
state.stop();
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn open_devtools(window: tauri::WebviewWindow) -> Result<(), String> {
|
|
window.open_devtools();
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn log_app_event(message: String) -> Result<(), String> {
|
|
append_app_log(&message)
|
|
}
|
|
|
|
fn append_app_log(message: &str) -> Result<(), String> {
|
|
let Some(dir) = logs_directory() else {
|
|
return Err("LOCALAPPDATA indisponivel para gravar logs".to_string());
|
|
};
|
|
|
|
std::fs::create_dir_all(&dir)
|
|
.map_err(|error| format!("Falha ao criar pasta de logs: {error}"))?;
|
|
|
|
let path = dir.join("app.log");
|
|
let mut file = OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&path)
|
|
.map_err(|error| format!("Falha ao abrir app.log: {error}"))?;
|
|
|
|
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
|
|
writeln!(file, "[{timestamp}] {message}")
|
|
.map_err(|error| format!("Falha ao escrever log: {error}"))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn logs_directory() -> Option<PathBuf> {
|
|
let base = std::env::var("LOCALAPPDATA").ok()?;
|
|
Some(Path::new(&base).join("br.com.esdrasrenan.sistemadechamados").join("logs"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn ensure_rustdesk_and_emit(
|
|
app: tauri::AppHandle,
|
|
config_string: Option<String>,
|
|
password: Option<String>,
|
|
machine_id: Option<String>,
|
|
) -> Result<RustdeskProvisioningResult, String> {
|
|
let result = tauri::async_runtime::spawn_blocking(move || {
|
|
run_rustdesk_ensure(config_string, password, machine_id)
|
|
})
|
|
.await
|
|
.map_err(|error| error.to_string())??;
|
|
|
|
if let Err(error) = app.emit("raven://remote-access/provisioned", &result) {
|
|
eprintln!("[rustdesk] falha ao emitir evento raven://remote-access/provisioned: {error}");
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn run_rustdesk_ensure(
|
|
config_string: Option<String>,
|
|
password: Option<String>,
|
|
machine_id: Option<String>,
|
|
) -> Result<RustdeskProvisioningResult, String> {
|
|
// Tenta usar o servico primeiro (sem UAC)
|
|
if service_client::is_service_available() {
|
|
log_info!("Usando Raven Service para provisionar RustDesk");
|
|
match service_client::provision_rustdesk(
|
|
config_string.as_deref(),
|
|
password.as_deref(),
|
|
machine_id.as_deref(),
|
|
) {
|
|
Ok(result) => {
|
|
return Ok(RustdeskProvisioningResult {
|
|
id: result.id,
|
|
password: result.password,
|
|
installed_version: result.installed_version,
|
|
updated: result.updated,
|
|
last_provisioned_at: result.last_provisioned_at,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
log_warn!("Falha ao usar servico para RustDesk: {e}");
|
|
// Continua para fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: chamada direta (pode pedir UAC)
|
|
log_info!("Usando chamada direta para provisionar RustDesk (pode pedir UAC)");
|
|
rustdesk::ensure_rustdesk(
|
|
config_string.as_deref(),
|
|
password.as_deref(),
|
|
machine_id.as_deref(),
|
|
)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn run_rustdesk_ensure(
|
|
_config_string: Option<String>,
|
|
_password: Option<String>,
|
|
_machine_id: Option<String>,
|
|
) -> Result<RustdeskProvisioningResult, String> {
|
|
Err("Provisionamento automático do RustDesk está disponível apenas no Windows.".to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn apply_usb_policy(policy: String) -> Result<UsbPolicyResult, String> {
|
|
// Valida a politica primeiro
|
|
let _policy_enum = UsbPolicy::from_str(&policy)
|
|
.ok_or_else(|| format!("Politica USB invalida: {}. Use ALLOW, BLOCK_ALL ou READONLY.", policy))?;
|
|
|
|
// Tenta usar o servico primeiro (sem UAC)
|
|
#[cfg(target_os = "windows")]
|
|
if service_client::is_service_available() {
|
|
log_info!("Usando Raven Service para aplicar politica USB: {}", policy);
|
|
match service_client::apply_usb_policy(&policy) {
|
|
Ok(result) => {
|
|
return Ok(UsbPolicyResult {
|
|
success: result.success,
|
|
policy: result.policy,
|
|
error: result.error,
|
|
applied_at: result.applied_at,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
log_warn!("Falha ao usar servico para USB policy: {e}");
|
|
// Continua para fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: chamada direta (pode pedir UAC)
|
|
log_info!("Usando chamada direta para aplicar politica USB (pode pedir UAC)");
|
|
usb_control::apply_usb_policy(_policy_enum).map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_usb_policy() -> Result<String, String> {
|
|
// Tenta usar o servico primeiro
|
|
#[cfg(target_os = "windows")]
|
|
if service_client::is_service_available() {
|
|
match service_client::get_usb_policy() {
|
|
Ok(policy) => return Ok(policy),
|
|
Err(e) => {
|
|
log_warn!("Falha ao obter USB policy via servico: {e}");
|
|
// Continua para fallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: leitura direta (nao precisa elevacao para ler)
|
|
usb_control::get_current_policy()
|
|
.map(|p| p.as_str().to_string())
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn refresh_usb_policy() -> Result<(), String> {
|
|
usb_control::refresh_group_policy().map_err(|e| e.to_string())
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMANDOS DE CHAT
|
|
// ============================================================================
|
|
|
|
#[tauri::command]
|
|
fn start_chat_polling(
|
|
state: tauri::State<ChatRuntime>,
|
|
app: tauri::AppHandle,
|
|
base_url: String,
|
|
convex_url: Option<String>,
|
|
token: String,
|
|
) -> Result<(), String> {
|
|
let url = convex_url.unwrap_or_else(|| DEFAULT_CONVEX_URL.to_string());
|
|
state.start_polling(base_url, url, token, app)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn stop_chat_polling(state: tauri::State<ChatRuntime>) -> Result<(), String> {
|
|
state.stop();
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn is_chat_using_realtime(state: tauri::State<ChatRuntime>) -> bool {
|
|
state.is_using_sse()
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_chat_sessions(state: tauri::State<ChatRuntime>) -> Vec<ChatSession> {
|
|
state.get_sessions()
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn fetch_chat_sessions(base_url: String, token: String) -> Result<Vec<ChatSession>, String> {
|
|
chat::fetch_sessions(&base_url, &token).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn fetch_chat_messages(
|
|
base_url: String,
|
|
token: String,
|
|
ticket_id: String,
|
|
since: Option<i64>,
|
|
) -> Result<ChatMessagesResponse, String> {
|
|
chat::fetch_messages(&base_url, &token, &ticket_id, since).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn send_chat_message(
|
|
base_url: String,
|
|
token: String,
|
|
ticket_id: String,
|
|
body: String,
|
|
attachments: Option<Vec<chat::AttachmentPayload>>,
|
|
) -> Result<SendMessageResponse, String> {
|
|
chat::send_message(&base_url, &token, &ticket_id, &body, attachments).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn mark_chat_messages_read(
|
|
base_url: String,
|
|
token: String,
|
|
ticket_id: String,
|
|
message_ids: Vec<String>,
|
|
) -> Result<(), String> {
|
|
if message_ids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
chat::mark_messages_read(&base_url, &token, &ticket_id, &message_ids).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn upload_chat_file(
|
|
base_url: String,
|
|
token: String,
|
|
file_path: String,
|
|
) -> Result<chat::AttachmentPayload, String> {
|
|
use std::path::Path;
|
|
|
|
// Ler o arquivo
|
|
let path = Path::new(&file_path);
|
|
let file_name = path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.ok_or("Nome de arquivo inválido")?
|
|
.to_string();
|
|
|
|
let file_data = std::fs::read(&file_path)
|
|
.map_err(|e| format!("Falha ao ler arquivo: {e}"))?;
|
|
|
|
let file_size = file_data.len() as u64;
|
|
|
|
// Validar arquivo
|
|
chat::is_allowed_file(&file_name, file_size)?;
|
|
|
|
// Obter tipo MIME
|
|
let mime_type = chat::get_mime_type(&file_name);
|
|
|
|
// Gerar URL de upload
|
|
let upload_url = chat::generate_upload_url(
|
|
&base_url,
|
|
&token,
|
|
&file_name,
|
|
&mime_type,
|
|
file_size,
|
|
)
|
|
.await?;
|
|
|
|
// Fazer upload
|
|
let storage_id = chat::upload_file(&upload_url, file_data, &mime_type).await?;
|
|
|
|
Ok(chat::AttachmentPayload {
|
|
storage_id,
|
|
name: file_name,
|
|
size: Some(file_size),
|
|
mime_type: Some(mime_type),
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn open_chat_window(app: tauri::AppHandle, ticket_id: String, ticket_ref: u64) -> Result<(), String> {
|
|
log_info!("[CMD] open_chat_window called: ticket_id={}, ticket_ref={}", ticket_id, ticket_ref);
|
|
let result = chat::open_chat_window(&app, &ticket_id, ticket_ref);
|
|
log_info!("[CMD] open_chat_window result: {:?}", result);
|
|
result
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn close_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> {
|
|
chat::close_chat_window(&app, &ticket_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn minimize_chat_window(app: tauri::AppHandle, ticket_id: String) -> Result<(), String> {
|
|
chat::minimize_chat_window(&app, &ticket_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn set_chat_minimized(app: tauri::AppHandle, ticket_id: String, minimized: bool) -> Result<(), String> {
|
|
chat::set_chat_minimized(&app, &ticket_id, minimized)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn open_hub_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
chat::open_hub_window(&app)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn close_hub_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
chat::close_hub_window(&app)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn set_hub_minimized(app: tauri::AppHandle, minimized: bool) -> Result<(), String> {
|
|
chat::set_hub_minimized(&app, minimized)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Handler de Deep Link (raven://)
|
|
// ============================================================================
|
|
|
|
/// Processa URLs do protocolo raven://
|
|
/// Formatos suportados:
|
|
/// - raven://ticket/{token} - Abre visualizacao do chamado
|
|
/// - raven://chat/{ticketId}?token={token} - Abre chat do chamado
|
|
/// - raven://rate/{token} - Abre avaliacao do chamado
|
|
fn handle_deep_link(app: &tauri::AppHandle, url: &str) {
|
|
log_info!("Processando deep link: {url}");
|
|
|
|
// Remove o prefixo raven://
|
|
let path = url.trim_start_matches("raven://");
|
|
|
|
// Parse do path
|
|
let parts: Vec<&str> = path.split('/').collect();
|
|
|
|
if parts.is_empty() {
|
|
log_warn!("Deep link invalido: path vazio");
|
|
return;
|
|
}
|
|
|
|
match parts[0] {
|
|
"ticket" => {
|
|
if parts.len() > 1 {
|
|
let token = parts[1].split('?').next().unwrap_or(parts[1]);
|
|
log_info!("Abrindo ticket com token: {token}");
|
|
|
|
// Mostra a janela principal
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
|
|
// Emite evento para o frontend navegar para o ticket
|
|
let _ = app.emit("raven://deep-link/ticket", serde_json::json!({
|
|
"token": token
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
"chat" => {
|
|
if parts.len() > 1 {
|
|
let ticket_id = parts[1].split('?').next().unwrap_or(parts[1]);
|
|
log_info!("Abrindo chat do ticket: {ticket_id}");
|
|
|
|
// Abre janela de chat (ticket_ref 0 quando vem de deeplink)
|
|
if let Err(e) = chat::open_chat_window(app, ticket_id, 0) {
|
|
log_error!("Falha ao abrir chat: {e}");
|
|
}
|
|
}
|
|
}
|
|
"rate" => {
|
|
if parts.len() > 1 {
|
|
let token = parts[1].split('?').next().unwrap_or(parts[1]);
|
|
log_info!("Abrindo avaliacao com token: {token}");
|
|
|
|
// Mostra a janela principal
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
|
|
// Emite evento para o frontend navegar para avaliacao
|
|
let _ = app.emit("raven://deep-link/rate", serde_json::json!({
|
|
"token": token
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
log_warn!("Deep link desconhecido: {path}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.manage(AgentRuntime::new())
|
|
.manage(ChatRuntime::new())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(StorePluginBuilder::default().build())
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
|
// Quando uma segunda instância tenta iniciar, foca a janela existente
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.unminimize();
|
|
let _ = window.set_focus();
|
|
}
|
|
}))
|
|
.on_window_event(|window, event| {
|
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
|
api.prevent_close();
|
|
let _ = window.hide();
|
|
}
|
|
})
|
|
.setup(|app| {
|
|
// Inicializa sistema de logging primeiro
|
|
if let Err(e) = init_agent_logging() {
|
|
eprintln!("[raven] Falha ao inicializar logging: {e}");
|
|
}
|
|
|
|
log_info!("Raven iniciando...");
|
|
|
|
// Configura handler de deep link (raven://)
|
|
#[cfg(desktop)]
|
|
{
|
|
let handle = app.handle().clone();
|
|
app.listen("deep-link://new-url", move |event| {
|
|
let urls = event.payload();
|
|
log_info!("Deep link recebido: {urls}");
|
|
handle_deep_link(&handle, urls);
|
|
});
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
let start_in_background = std::env::args().any(|arg| arg == "--background");
|
|
setup_raven_autostart();
|
|
setup_tray(app.handle())?;
|
|
if start_in_background {
|
|
if let Some(win) = app.get_webview_window("main") {
|
|
let _ = win.hide();
|
|
}
|
|
}
|
|
|
|
// Tenta iniciar o agente e chat em background se houver credenciais salvas
|
|
let app_handle = app.handle().clone();
|
|
let agent_runtime = app.state::<AgentRuntime>().inner().clone();
|
|
let chat_runtime = app.state::<ChatRuntime>().inner().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
// Aguarda um pouco para o app estabilizar
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
if let Err(e) = try_start_background_agent(&app_handle, agent_runtime, chat_runtime).await {
|
|
log_warn!("Agente nao iniciado em background: {e}");
|
|
}
|
|
});
|
|
}
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
collect_machine_profile,
|
|
collect_machine_inventory,
|
|
start_machine_agent,
|
|
stop_machine_agent,
|
|
open_devtools,
|
|
log_app_event,
|
|
ensure_rustdesk_and_emit,
|
|
apply_usb_policy,
|
|
get_usb_policy,
|
|
refresh_usb_policy,
|
|
// Chat commands
|
|
start_chat_polling,
|
|
stop_chat_polling,
|
|
is_chat_using_realtime,
|
|
get_chat_sessions,
|
|
fetch_chat_sessions,
|
|
fetch_chat_messages,
|
|
send_chat_message,
|
|
mark_chat_messages_read,
|
|
upload_chat_file,
|
|
open_chat_window,
|
|
close_chat_window,
|
|
minimize_chat_window,
|
|
set_chat_minimized,
|
|
// Hub commands
|
|
open_hub_window,
|
|
close_hub_window,
|
|
set_hub_minimized
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn setup_raven_autostart() {
|
|
let exe_path = match std::env::current_exe() {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
log_error!("Falha ao obter caminho do executavel: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let path_str = exe_path.display().to_string();
|
|
// Adiciona flag --background para indicar inicio automatico
|
|
let value = format!("\"{}\" --background", path_str);
|
|
|
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
|
|
|
let key = match hkcu.create_subkey(r"Software\Microsoft\Windows\CurrentVersion\Run") {
|
|
Ok((key, _)) => key,
|
|
Err(e) => {
|
|
log_error!("Falha ao criar/abrir chave de registro Run: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = key.set_value("Raven", &value) {
|
|
log_error!("Falha ao definir valor de auto-start no registro: {e}");
|
|
return;
|
|
}
|
|
|
|
log_info!("Auto-start configurado: {value}");
|
|
|
|
// Valida que foi salvo corretamente
|
|
match key.get_value::<String, _>("Raven") {
|
|
Ok(saved) => {
|
|
if saved == value {
|
|
log_info!("Auto-start validado: entrada existe no registro");
|
|
} else {
|
|
log_warn!("Auto-start: valor difere. Esperado: {value}, Salvo: {saved}");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
log_warn!("Auto-start: nao foi possivel validar entrada: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
|
|
let show_item = MenuItemBuilder::with_id("show", "Mostrar").build(app)?;
|
|
let chat_item = MenuItemBuilder::with_id("chat", "Abrir Chat").build(app)?;
|
|
let quit_item = MenuItemBuilder::with_id("quit", "Sair").build(app)?;
|
|
let menu = MenuBuilder::new(app)
|
|
.items(&[&show_item, &chat_item, &quit_item])
|
|
.build()?;
|
|
|
|
let mut builder = TrayIconBuilder::new()
|
|
.menu(&menu)
|
|
.on_menu_event(|tray, event| {
|
|
match event.id().as_ref() {
|
|
"show" => {
|
|
if let Some(win) = tray.app_handle().get_webview_window("main") {
|
|
let _ = win.show();
|
|
let _ = win.set_focus();
|
|
}
|
|
// Reabrir chat se houver sessao ativa
|
|
if let Some(chat_runtime) = tray.app_handle().try_state::<ChatRuntime>() {
|
|
let sessions = chat_runtime.get_sessions();
|
|
if let Some(session) = sessions.first() {
|
|
let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref);
|
|
}
|
|
}
|
|
}
|
|
"chat" => {
|
|
// Abrir janela de chat se houver sessao ativa
|
|
if let Some(chat_runtime) = tray.app_handle().try_state::<ChatRuntime>() {
|
|
let sessions = chat_runtime.get_sessions();
|
|
if sessions.len() > 1 {
|
|
// Multiplas sessoes - abrir hub
|
|
if let Err(e) = chat::open_hub_window(tray.app_handle()) {
|
|
log_error!("Falha ao abrir hub de chat: {e}");
|
|
}
|
|
} else if let Some(session) = sessions.first() {
|
|
// Uma sessao - abrir diretamente
|
|
if let Err(e) = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref) {
|
|
log_error!("Falha ao abrir janela de chat: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"quit" => {
|
|
tray.app_handle().exit(0);
|
|
}
|
|
_ => {}
|
|
}
|
|
})
|
|
.on_tray_icon_event(|tray, event| {
|
|
if let tauri::tray::TrayIconEvent::DoubleClick { .. } = event {
|
|
if let Some(win) = tray.app_handle().get_webview_window("main") {
|
|
let _ = win.show();
|
|
let _ = win.set_focus();
|
|
}
|
|
// Reabrir chat se houver sessao ativa
|
|
if let Some(chat_runtime) = tray.app_handle().try_state::<ChatRuntime>() {
|
|
let sessions = chat_runtime.get_sessions();
|
|
if let Some(session) = sessions.first() {
|
|
let _ = chat::open_chat_window(tray.app_handle(), &session.ticket_id, session.ticket_ref);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if let Some(icon) = app.default_window_icon() {
|
|
builder = builder.icon(icon.clone());
|
|
}
|
|
|
|
builder = builder.tooltip("Raven");
|
|
|
|
builder.build(app)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
async fn try_start_background_agent(
|
|
app: &tauri::AppHandle,
|
|
agent_runtime: AgentRuntime,
|
|
chat_runtime: ChatRuntime,
|
|
) -> Result<(), String> {
|
|
log_info!("Verificando credenciais salvas para iniciar agente...");
|
|
|
|
let app_data = app
|
|
.path()
|
|
.app_local_data_dir()
|
|
.map_err(|e| format!("Falha ao obter diretorio de dados: {e}"))?;
|
|
|
|
let store_path = app_data.join("machine-agent.json");
|
|
|
|
if !store_path.exists() {
|
|
return Err("Nenhuma configuracao encontrada".to_string());
|
|
}
|
|
|
|
// Ler arquivo JSON diretamente
|
|
let content = std::fs::read_to_string(&store_path)
|
|
.map_err(|e| format!("Falha ao ler machine-agent.json: {e}"))?;
|
|
|
|
let data: serde_json::Value = serde_json::from_str(&content)
|
|
.map_err(|e| format!("Falha ao parsear machine-agent.json: {e}"))?;
|
|
|
|
let token = data
|
|
.get("token")
|
|
.and_then(|v| v.as_str())
|
|
.filter(|t| !t.is_empty())
|
|
.ok_or("Token nao encontrado ou vazio")?;
|
|
|
|
let config = data.get("config");
|
|
|
|
let api_base_url = config
|
|
.and_then(|c| c.get("apiBaseUrl"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("https://tickets.esdrasrenan.com.br");
|
|
|
|
let convex_url = config
|
|
.and_then(|c| c.get("convexUrl"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or(DEFAULT_CONVEX_URL);
|
|
|
|
let interval = config
|
|
.and_then(|c| c.get("heartbeatIntervalSec"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(300);
|
|
|
|
log_info!(
|
|
"Iniciando agente em background: url={}, interval={}s",
|
|
api_base_url,
|
|
interval
|
|
);
|
|
|
|
agent_runtime
|
|
.start_heartbeat(
|
|
api_base_url.to_string(),
|
|
token.to_string(),
|
|
Some("online".to_string()),
|
|
Some(interval),
|
|
)
|
|
.map_err(|e| format!("Falha ao iniciar heartbeat: {e}"))?;
|
|
|
|
// Iniciar sistema de chat (WebSocket + fallback HTTP polling)
|
|
if let Err(e) =
|
|
chat_runtime.start_polling(api_base_url.to_string(), convex_url.to_string(), token.to_string(), app.clone())
|
|
{
|
|
log_warn!("Falha ao iniciar chat em background: {e}");
|
|
} else {
|
|
log_info!("Chat iniciado com sucesso (Convex WebSocket)");
|
|
}
|
|
|
|
log_info!("Agente iniciado com sucesso em background");
|
|
|
|
Ok(())
|
|
}
|