AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
271
src/AxCopilot/Handlers/PingHandler.cs
Normal file
271
src/AxCopilot/Handlers/PingHandler.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L16-1: ping·tracert 빠른 실행 핸들러. "ping" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: ping 8.8.8.8 → ping 결과 (4회)
|
||||
/// ping google.com → 도메인 ping
|
||||
/// ping trace 8.8.8.8 → tracert 실행
|
||||
/// ping local → 로컬 네트워크 어댑터 정보
|
||||
/// ping scan 192.168.1.0 → 간단 네트워크 스캔 (1~254)
|
||||
/// Enter → 결과 복사 또는 외부 터미널 실행.
|
||||
/// 사내 모드: 외부 IP/도메인 ping 차단.
|
||||
/// </summary>
|
||||
public class PingHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "ping";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Ping",
|
||||
"ping·tracert 빠른 실행 — 네트워크 연결 확인·경로 추적",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
private static readonly string[] QuickTargets =
|
||||
["localhost", "8.8.8.8", "1.1.1.1", "google.com", "192.168.1.1"];
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
items.Add(new LauncherItem("ping / tracert 실행기",
|
||||
"예: ping 8.8.8.8 / ping trace 192.168.1.1 / ping local / ping scan 192.168.1",
|
||||
null, null, Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("── 빠른 대상 ──", "", null, null, Symbol: "\uE968"));
|
||||
foreach (var t in QuickTargets)
|
||||
items.Add(new LauncherItem($"ping {t}", t, null, ("ping", t), Symbol: "\uE968"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
// local → 로컬 어댑터 정보
|
||||
if (sub == "local" || sub == "lo")
|
||||
{
|
||||
items.AddRange(BuildLocalNetworkItems());
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// trace / tracert / traceroute
|
||||
if (sub is "trace" or "tracert" or "traceroute")
|
||||
{
|
||||
var target = parts.Length > 1 ? parts[1] : "";
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
items.Add(new LauncherItem("대상 주소 입력", "예: ping trace 8.8.8.8", null, null, Symbol: "\uE783"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var blocked = CheckInternalMode(target);
|
||||
if (blocked != null) { items.Add(blocked); return Task.FromResult<IEnumerable<LauncherItem>>(items); }
|
||||
items.Add(new LauncherItem($"tracert {target}", "Enter → 터미널에서 tracert 실행",
|
||||
null, ("tracert", target), Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("터미널 실행", $"tracert {target}",
|
||||
null, ("tracert", target), Symbol: "\uE968"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// scan → 간단 스캔 (비동기 결과는 실행 시 터미널)
|
||||
if (sub == "scan")
|
||||
{
|
||||
var network = parts.Length > 1 ? parts[1] : "192.168.1";
|
||||
items.Add(new LauncherItem($"네트워크 스캔: {network}.1~254",
|
||||
"Enter → 터미널에서 ping 스캔 스크립트 실행",
|
||||
null, ("scan", network), Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("팁", "결과가 많을 수 있습니다 — 터미널에서 확인하세요",
|
||||
null, null, Symbol: "\uE946"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 직접 ping 대상
|
||||
var host = parts[0];
|
||||
var blocked2 = CheckInternalMode(host);
|
||||
if (blocked2 != null) { items.Add(blocked2); return Task.FromResult<IEnumerable<LauncherItem>>(items); }
|
||||
|
||||
items.Add(new LauncherItem($"ping {host}",
|
||||
"Enter → 비동기 ping (4회) 실행",
|
||||
null, ("ping", host), Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem($"tracert {host}",
|
||||
"Enter → 터미널에서 tracert 실행",
|
||||
null, ("tracert", host), Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem($"ping 연속 {host}",
|
||||
"ping -t (무한 반복) — 터미널",
|
||||
null, ("ping_t", host), Symbol: "\uE968"));
|
||||
|
||||
// 즉시 1회 ping 시도
|
||||
var pingResult = TryPingOnce(host);
|
||||
if (pingResult != null)
|
||||
{
|
||||
items.Insert(0, pingResult);
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
switch (item.Data)
|
||||
{
|
||||
case ("ping", string host):
|
||||
RunInTerminal($"ping {host}");
|
||||
break;
|
||||
|
||||
case ("ping_t", string host):
|
||||
RunInTerminal($"ping -t {host}");
|
||||
break;
|
||||
|
||||
case ("tracert", string host):
|
||||
RunInTerminal($"tracert {host}");
|
||||
break;
|
||||
|
||||
case ("scan", string network):
|
||||
// PowerShell로 간단 스캔
|
||||
var ps = $"1..254 | ForEach-Object {{ $ip = '{network}.$_'; if (Test-Connection $ip -Count 1 -Quiet -TimeoutSeconds 1) {{ Write-Host \"$ip is UP\" }} }}; Read-Host 'Press Enter'";
|
||||
RunInTerminal($"powershell -NoExit -Command \"{ps}\"", usePs: true);
|
||||
break;
|
||||
|
||||
case ("copy", string text):
|
||||
try
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||
() => Clipboard.SetText(text));
|
||||
NotificationService.Notify("Ping", "복사됨");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
break;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static List<LauncherItem> BuildLocalNetworkItems()
|
||||
{
|
||||
var items = new List<LauncherItem>();
|
||||
try
|
||||
{
|
||||
var ifaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(n => n.OperationalStatus == OperationalStatus.Up &&
|
||||
n.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
||||
.ToList();
|
||||
|
||||
items.Add(new LauncherItem($"로컬 네트워크 어댑터 {ifaces.Count}개", "", null, null, Symbol: "\uE968"));
|
||||
|
||||
foreach (var iface in ifaces)
|
||||
{
|
||||
var ipProps = iface.GetIPProperties();
|
||||
var ipv4 = ipProps.UnicastAddresses
|
||||
.FirstOrDefault(a => a.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork);
|
||||
var gateway = ipProps.GatewayAddresses.FirstOrDefault()?.Address?.ToString() ?? "없음";
|
||||
|
||||
if (ipv4 == null) continue;
|
||||
|
||||
var ip = ipv4.Address.ToString();
|
||||
var mask = ipv4.IPv4Mask.ToString();
|
||||
var label = $"{iface.Name} {ip}";
|
||||
var sub2 = $"넷마스크 {mask} · 게이트웨이 {gateway}";
|
||||
items.Add(new LauncherItem(label, sub2, null, ("copy", ip), Symbol: "\uE968"));
|
||||
}
|
||||
|
||||
// 외부 IP 안내 (사외 모드에서만)
|
||||
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
|
||||
if (settings?.InternalModeEnabled == false)
|
||||
items.Add(new LauncherItem("외부 IP 조회", "ping trace 8.8.8.8 으로 경로 확인",
|
||||
null, null, Symbol: "\uE968"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
items.Add(new LauncherItem("네트워크 정보 조회 오류", ex.Message, null, null, Symbol: "\uE783"));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static LauncherItem? TryPingOnce(string host)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var p = new Ping();
|
||||
var reply = p.Send(host, 1000);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
{
|
||||
var label = $"✓ 응답 {reply.RoundtripTime}ms";
|
||||
return new LauncherItem(label, $"TTL {reply.Options?.Ttl ?? 0} · {host}",
|
||||
null, ("copy", $"{reply.RoundtripTime}ms"), Symbol: "\uE968");
|
||||
}
|
||||
return new LauncherItem($"✗ 응답 없음 ({reply.Status})", host, null, null, Symbol: "\uE783");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null; // 오류 시 무시
|
||||
}
|
||||
}
|
||||
|
||||
private static LauncherItem? CheckInternalMode(string host)
|
||||
{
|
||||
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
|
||||
if (settings?.InternalModeEnabled != true) return null;
|
||||
|
||||
// 내부 주소는 허용
|
||||
if (host.StartsWith("192.168.", StringComparison.Ordinal) ||
|
||||
host.StartsWith("10.", StringComparison.Ordinal) ||
|
||||
host.StartsWith("172.", StringComparison.Ordinal) ||
|
||||
host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
host.StartsWith("127.", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
if (IPAddress.TryParse(host, out _))
|
||||
return null; // IP 주소 → 내부로 간주 허용
|
||||
|
||||
return new LauncherItem("사내 모드 — 외부 도메인 차단",
|
||||
"사외 모드에서 외부 주소 ping 가능. 설정에서 변경하세요.",
|
||||
null, null, Symbol: "\uE783");
|
||||
}
|
||||
|
||||
private static void RunInTerminal(string cmd, bool usePs = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var wtPath = FindExe("wt.exe");
|
||||
if (wtPath != null && !usePs)
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = wtPath, Arguments = $"cmd /K {cmd}", UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = usePs ? "powershell" : "cmd",
|
||||
Arguments = usePs ? $"-NoExit -Command \"{cmd}\"" : $"/K {cmd}",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
|
||||
private static string? FindExe(string name)
|
||||
{
|
||||
foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';'))
|
||||
{
|
||||
var full = System.IO.Path.Combine(dir.Trim(), name);
|
||||
if (System.IO.File.Exists(full)) return full;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user