Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ tools/*.exe
.*_KEY
*_key.txt
keys/
settings.json
94 changes: 48 additions & 46 deletions src/SupportAI.App.Wpf/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
Expand Down Expand Up @@ -38,10 +39,7 @@ public MainViewModel()
ExportPdfCommand = new AsyncRelayCommand(async _ => await ExportPdfAsync());
AnalyzeWithIaCommand = new AsyncRelayCommand(async _ => await AnalyzeWithIaAsync());
RepairCommand = new RelayCommand(ExecuteRepair);
CancelRepairCommand = new RelayCommand(_ => CancelRepair());
DownloadModelCommand = new RelayCommand(_ => DownloadModel());
AbrirAccionCommand = new RelayCommand(AbrirAccion);
ToggleExpandirCommand = new RelayCommand(ToggleExpandir);
IniciarServicioCommand = new AsyncRelayCommand(async param => await IniciarServicio(param));
OpenSettingsCommand = new RelayCommand(_ => AbrirSettings());
DescargarModeloCommand = new AsyncRelayCommand(async _ => await DescargarModeloAsync());
Expand All @@ -68,10 +66,7 @@ public MainViewModel()
public ICommand ExportPdfCommand { get; }
public ICommand AnalyzeWithIaCommand { get; }
public ICommand RepairCommand { get; }
public ICommand CancelRepairCommand { get; }
public ICommand DownloadModelCommand { get; }
public ICommand AbrirAccionCommand { get; }
public ICommand ToggleExpandirCommand { get; }
public ICommand IniciarServicioCommand { get; }
public ICommand OpenSettingsCommand { get; }
public ICommand DescargarModeloCommand { get; }
Expand All @@ -93,16 +88,26 @@ private async Task IniciarServicio(object? param)
{
if (param is not string nombreCorto) return;

if (!System.Text.RegularExpressions.Regex.IsMatch(nombreCorto, @"^[a-zA-Z0-9._\- ]+$"))
{
StatusText = "❌ Nombre de servicio inválido.";
return;
}

var confirm = MessageBox.Show(
$"Iniciar el servicio '{nombreCorto}' requiere permisos de administrador.\n\nSe abrirá el diálogo de UAC de Windows.\n¿Continuar?",
"Permisos elevados requeridos",
MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;

var script = $"Start-Service '{nombreCorto}'";
var bytes = System.Text.Encoding.Unicode.GetBytes(script);
var encoded = Convert.ToBase64String(bytes);

var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"Start-Service '{nombreCorto}'\"",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encoded}",
UseShellExecute = true,
Verb = "runas",
CreateNoWindow = false
Expand Down Expand Up @@ -137,7 +142,7 @@ private async Task IniciarServicio(object? param)
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
StatusText = $"⏹️ Inicio de '{nombreCorto}' cancelado por el usuario.";
StatusText = $"⏹️ Iniciar de '{nombreCorto}' cancelado por el usuario.";
}
catch (Exception ex)
{
Expand All @@ -158,14 +163,24 @@ private void AbrirAccion(object? param)
if (param is not string target) return;
try
{
var whitelist = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"cleanmgr", "taskmgr", "shutdown", "windowsdefender:", "firewall.cpl",
"devmgmt.msc", "eventvwr.msc", "diskmgmt.msc", "powercfg.cpl"
};

if (target.StartsWith("expand:"))
{
ToggleExpandir(target["expand:".Length..]);
return;
}
if (target.StartsWith("ms-settings:"))
{
Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true });
var allowedSettings = new[] { "ms-settings:network-troubleshoot", "ms-settings:network-status" };
if (allowedSettings.Contains(target, StringComparer.OrdinalIgnoreCase))
{
Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true });
}
return;
}
if (target == "shutdown")
Expand All @@ -182,17 +197,14 @@ private void AbrirAccion(object? param)
});
return;
}
if (target == "taskmgr")

if (whitelist.Contains(target))
{
Process.Start(new ProcessStartInfo { FileName = "taskmgr.exe", UseShellExecute = true });
return;
}
if (target == "windowsdefender:")
{
Process.Start(new ProcessStartInfo { FileName = "windowsdefender:", UseShellExecute = true });
Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true });
return;
}
Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true });

Trace.WriteLine($"[MainViewModel] Bloqueado intento de abrir acción no segura: {target}");
}
catch (Exception ex)
{
Expand All @@ -211,16 +223,9 @@ public string StatusText
public int Puntuacion
{
get => _puntuacion;
set { _puntuacion = value; OnPropertyChanged(); OnPropertyChanged(nameof(ScoreColor)); OnPropertyChanged(nameof(ScoreLabel)); }
set { _puntuacion = value; OnPropertyChanged(); OnPropertyChanged(nameof(ScoreLabel)); }
}

public string ScoreColor => Puntuacion switch
{
>= 80 => "#27ae60",
>= 50 => "#f39c12",
_ => "#e74c3c"
};

public string ScoreLabel => Puntuacion switch
{
>= 80 => "Excelente",
Expand Down Expand Up @@ -259,8 +264,8 @@ public bool Reparando

public bool PuedeReparar => !_reparando;

public List<ModuloCheck> Modulos { get; set; } = [];
public List<Problema> Problemas { get; set; } = [];
public ObservableCollection<ModuloCheck> Modulos { get; } = [];
public ObservableCollection<Problema> Problemas { get; } = [];
public List<IRepairAction> Repairs => RepairCatalog.All.ToList();
public bool HayProblemas => Problemas.Count > 0;
public bool TieneDatos => Puntuacion > 0;
Expand Down Expand Up @@ -327,18 +332,9 @@ public string ModelStatus
public bool ModelListo => ModeloDescargado;
private static bool ModeloDescargado => ModelDownloader.ModelExists;

private void DownloadModel()
private void RefreshModelStatus()
{
RefreshModelStatus();
try
{
Process.Start(new ProcessStartInfo
{
FileName = "https://github.com/ggerganov/llama.cpp/releases/latest",
UseShellExecute = true
});
}
catch { }
ModelStatus = ModelDownloader.GetStatus();
}

private async Task DescargarModeloAsync()
Expand Down Expand Up @@ -372,11 +368,6 @@ private async Task DescargarModeloAsync()
}
}

private void RefreshModelStatus()
{
ModelStatus = ModelDownloader.GetStatus();
}

public async Task ScanAsync()
{
Escaneando = true;
Expand Down Expand Up @@ -419,12 +410,23 @@ public async Task ScanAsync()
_diagnostico = _diagnostico with { Problemas = problemas, Puntuacion = puntuacion };

Puntuacion = puntuacion;
Problemas = problemas;
Modulos = BuildModulos(_diagnostico);

Problemas.Clear();
foreach (var p in problemas)
Problemas.Add(p);

Modulos.Clear();
foreach (var m in BuildModulos(_diagnostico))
Modulos.Add(m);

ScanProgress = 100;
StatusText = $"Diagnóstico completado. Puntuación: {Puntuacion}/100 ({ScoreLabel}). {Problemas.Count} problema(s).";
}
catch (Exception ex)
{
StatusText = $"❌ Error al escanear: {ex.Message}";
Trace.WriteLine($"[MainViewModel] Scan error: {ex.Message}");
}
finally
{
timer.Stop();
Expand Down Expand Up @@ -636,7 +638,7 @@ private static bool ServiceExecutableExists(ServicioInfo svc)
return File.Exists(path);
}

public List<ChatMessage> ChatMessages { get; set; } = [];
public ObservableCollection<ChatMessage> ChatMessages { get; } = [];
public string ChatInput
{
get => _chatInput;
Expand Down
10 changes: 0 additions & 10 deletions src/SupportAI.Collectors.Windows/IColector.cs

This file was deleted.

46 changes: 34 additions & 12 deletions src/SupportAI.Collectors.Windows/PowerShellEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ public class PowerShellEngine
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null
PropertyNameCaseInsensitive = true
};

public async Task<Diagnostico> CollectAllAsync(CancellationToken ct)
{
var script = BuildFullScript();
var json = await RunPowerShellAsync(script, ct);
if (string.IsNullOrWhiteSpace(json))
return new Diagnostico { GeneradoEn = DateTime.UtcNow };
try
{
var json = await RunPowerShellAsync(script, ct);
if (string.IsNullOrWhiteSpace(json))
return new Diagnostico { GeneradoEn = DateTime.UtcNow };

var diag = JsonSerializer.Deserialize<DiagnosticoRaw>(json, JsonOpts);
return MapToDiagnostico(diag);
var diag = JsonSerializer.Deserialize<DiagnosticoRaw>(json, JsonOpts);
return MapToDiagnostico(diag);
}
catch (JsonException ex)
{
Trace.WriteLine($"[PowerShellEngine] JSON Deserialization error: {ex.Message}");
return new Diagnostico { GeneradoEn = DateTime.UtcNow };
}
catch (Exception ex)
{
Trace.WriteLine($"[PowerShellEngine] Error collecting info: {ex.Message}");
return new Diagnostico { GeneradoEn = DateTime.UtcNow };
}
}

private static string BuildFullScript()
Expand Down Expand Up @@ -162,12 +174,16 @@ private static string BuildFullScript()
""";
}

private const int TimeoutSeconds = 90;

private static async Task<string?> RunPowerShellAsync(string script, CancellationToken ct)
{
var bytes = System.Text.Encoding.Unicode.GetBytes(script);
var encodedScript = Convert.ToBase64String(bytes);
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{script.Replace("\"", "\\\"")}\"",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {encodedScript}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
Expand All @@ -181,7 +197,7 @@ private static string BuildFullScript()
var readErrorTask = process.StandardError.ReadToEndAsync(ct);
var processExitTask = process.WaitForExitAsync(ct);

var timeoutTask = Task.Delay(TimeSpan.FromSeconds(90), ct);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(TimeoutSeconds), ct);

var completedTask = await Task.WhenAny(processExitTask, timeoutTask);
if (completedTask == timeoutTask)
Expand All @@ -190,14 +206,19 @@ private static string BuildFullScript()
{
process.Kill(entireProcessTree: true);
}
catch
catch (Exception ex)
{
// Ignorar errores al matar el proceso
Trace.WriteLine($"[PowerShellEngine] Error killing process: {ex.Message}");
}
return null;
}

var output = await readOutputTask;
var error = await readErrorTask;
if (!string.IsNullOrWhiteSpace(error))
{
Trace.WriteLine($"[PowerShellEngine] PowerShell Stderr: {error}");
}
return string.IsNullOrEmpty(output) ? null : output.Trim();
}

Expand Down Expand Up @@ -243,7 +264,8 @@ private static Diagnostico MapToDiagnostico(DiagnosticoRaw? raw)
} : null,
SO = raw.Hardware.So is { } s ? new OsInfo
{
Caption = s.Caption ?? "", Version = s.Version ?? "", Build = s.Build ?? ""
Caption = s.Caption ?? "", Version = s.Version ?? "", Build = s.Build ?? "",
Instalado = s.Instalado, UltimoArranque = s.UltimoArranque
} : null,
Bateria = raw.Hardware.Bateria is { } bat ? new BatteryInfo
{
Expand Down
12 changes: 0 additions & 12 deletions src/SupportAI.Core/Models/DiagnosticContext.cs

This file was deleted.

4 changes: 2 additions & 2 deletions src/SupportAI.Core/Models/DiagnosticEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,11 @@ public static (List<Problema> Problemas, int Puntuacion) Analyze(Diagnostico dia
});
}

var puntuacion = CalcularPuntuacion(diag, problemas);
var puntuacion = CalcularPuntuacion(problemas);
return (problemas, puntuacion);
}

private static int CalcularPuntuacion(Diagnostico diag, List<Problema> problemas)
private static int CalcularPuntuacion(List<Problema> problemas)
{
var puntos = 100;
foreach (var p in problemas)
Expand Down
8 changes: 5 additions & 3 deletions src/SupportAI.Core/Models/HardwareInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public record CpuInfo
public record RamInfo
{
[JsonPropertyName("totalBytes")] public long TotalBytes { get; init; }
[JsonPropertyName("totalGB")] public double TotalGB => Math.Round(TotalBytes / 1073741824.0, 1);
private const double BytesPerGB = 1_073_741_824.0;
[JsonPropertyName("totalGB")] public double TotalGB => Math.Round(TotalBytes / BytesPerGB, 1);
}

public record GpuInfo
Expand All @@ -46,11 +47,12 @@ public record DiscoInfo

public record DiscoLogicoInfo
{
private const double BytesPerGB = 1_073_741_824.0;
[JsonPropertyName("letra")] public string Letra { get; init; } = "";
[JsonPropertyName("sizeBytes")] public long SizeBytes { get; init; }
[JsonPropertyName("freeBytes")] public long FreeBytes { get; init; }
[JsonPropertyName("sizeGB")] public double SizeGB => Math.Round(SizeBytes / 1073741824.0, 1);
[JsonPropertyName("freeGB")] public double FreeGB => Math.Round(FreeBytes / 1073741824.0, 1);
[JsonPropertyName("sizeGB")] public double SizeGB => Math.Round(SizeBytes / BytesPerGB, 1);
[JsonPropertyName("freeGB")] public double FreeGB => Math.Round(FreeBytes / BytesPerGB, 1);
[JsonPropertyName("usoPorcentaje")] public double UsoPorcentaje =>
SizeBytes > 0 ? Math.Round((1.0 - (double)FreeBytes / SizeBytes) * 100, 1) : 0;
}
Expand Down
Loading
Loading