using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; public class WebSearchHandler : IActionHandler { private readonly SettingsService _settings; private static readonly string _iconDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Assets", "SearchEngines"); private static readonly Dictionary _engines = new Dictionary { ["g"] = ("Google", "https://www.google.com/search?q={0}", "google"), ["n"] = ("Naver", "https://search.naver.com/search.naver?query={0}", "naver"), ["y"] = ("YouTube", "https://www.youtube.com/results?search_query={0}", "youtube"), ["gh"] = ("GitHub", "https://github.com/search?q={0}", "github"), ["d"] = ("DuckDuckGo", "https://duckduckgo.com/?q={0}", "duckduckgo"), ["w"] = ("Wikipedia", "https://ko.wikipedia.org/w/index.php?search={0}", "wikipedia"), ["nm"] = ("Naver Map", "https://map.naver.com/p/search/{0}", "navermap"), ["nw"] = ("나무위키", "https://namu.wiki/Special:Search?query={0}", "namuwiki"), ["ni"] = ("Naver 이미지", "https://search.naver.com/search.naver?where=image&query={0}", "naver"), ["gi"] = ("Google 이미지", "https://www.google.com/search?tbm=isch&q={0}", "google") }; private const string SecurityWarn = "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지"; public string? Prefix => "?"; public PluginMetadata Metadata => new PluginMetadata("WebSearch", "웹 검색 — ? 뒤에 검색어 또는 URL 입력", "1.0", "AX"); private string DefaultEngineKey => _settings.Settings.Launcher.WebSearchEngine ?? "g"; private (string Name, string UrlTemplate, string Icon) DefaultEngine { get { (string, string, string) value; return _engines.TryGetValue(DefaultEngineKey, out value) ? value : _engines["g"]; } } public WebSearchHandler(SettingsService settings) { _settings = settings; } private static string? GetIconPath(string engineKey) { if (!_engines.TryGetValue(engineKey, out (string, string, string) value)) { return null; } string text = Path.Combine(_iconDir, value.Item3 + ".png"); return File.Exists(text) ? text : null; } public Task> GetItemsAsync(string query, CancellationToken ct) { if (string.IsNullOrWhiteSpace(query)) { string iconPath = GetIconPath(DefaultEngineKey); return Task.FromResult((IEnumerable)new _003C_003Ez__ReadOnlySingleElementList(new LauncherItem("검색어를 입력하세요", "예: ? 오늘 날씨 · ?g python · ?y 음악 · ?n 뉴스 | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", iconPath, null, null, "\ue774"))); } List list = new List(); string text = query.Trim(); if (IsUrl(text)) { string text2 = (text.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? text : ("https://" + text)); string faviconPathForUrl = GetFaviconPathForUrl(text2); list.Add(new LauncherItem("열기: " + text, text2 + " | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", faviconPathForUrl, text2, null, "\ue774")); return Task.FromResult((IEnumerable)list); } string[] array = text.Split(' ', 2); string text3 = array[0].ToLowerInvariant(); if (array.Length == 2 && _engines.TryGetValue(text3, out (string, string, string) value)) { string text4 = array[1].Trim(); if (!string.IsNullOrWhiteSpace(text4)) { string data = string.Format(value.Item2, Uri.EscapeDataString(text4)); list.Add(new LauncherItem(value.Item1 + "에서 '" + text4 + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(text3), data, null, "\ue774")); } } string defaultEngineKey = DefaultEngineKey; (string, string, string) defaultEngine = DefaultEngine; string data2 = string.Format(defaultEngine.Item2, Uri.EscapeDataString(text)); list.Add(new LauncherItem(defaultEngine.Item1 + "에서 '" + text + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(defaultEngineKey), data2, null, "\ue774")); if (text.Length <= 2) { foreach (var (text6, tuple2) in _engines) { list.Add(new LauncherItem("?" + text6 + " — " + tuple2.Item1, $"예: ?{text6} {text} | {"⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지"}", GetIconPath(text6), null, null, "\ue774")); } } return Task.FromResult((IEnumerable)list); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is string text && !string.IsNullOrWhiteSpace(text)) { try { Process.Start(new ProcessStartInfo(text) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn("웹 검색 열기 실패: " + ex.Message); } } return Task.CompletedTask; } private static bool IsUrl(string input) { if (input.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || input.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { return true; } if (!input.Contains(' ') && input.Contains('.') && input.Length > 3) { string text = input.Split('/')[0]; string[] array = text.Split('.'); return array.Length >= 2 && array[^1].Length >= 2 && array[^1].Length <= 6 && array[^1].All(char.IsLetter); } return false; } private static string? GetFaviconPathForUrl(string url) { try { if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { url = "https://" + url; } string text = new Uri(url).Host.ToLowerInvariant(); string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favicons", text + ".png"); if (File.Exists(text2)) { return text2; } FaviconService.GetFavicon(url); return null; } catch { return null; } } }