164 lines
6.1 KiB
C#
164 lines
6.1 KiB
C#
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<string, (string Name, string UrlTemplate, string Icon)> _engines = new Dictionary<string, (string, string, string)>
|
|
{
|
|
["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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
{
|
|
string iconPath = GetIconPath(DefaultEngineKey);
|
|
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("검색어를 입력하세요", "예: ? 오늘 날씨 · ?g python · ?y 음악 · ?n 뉴스 | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", iconPath, null, null, "\ue774")));
|
|
}
|
|
List<LauncherItem> list = new List<LauncherItem>();
|
|
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<LauncherItem>)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<LauncherItem>)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;
|
|
}
|
|
}
|
|
}
|