using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; public class BookmarkHandler : IActionHandler { private record BookmarkEntry(string Name, string? Url); private List? _cache; private DateTime _cacheTime = DateTime.MinValue; private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5.0); public string? Prefix => null; public PluginMetadata Metadata => new PluginMetadata("Bookmarks", "Chrome / Edge 북마크 검색", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { if (string.IsNullOrWhiteSpace(query)) { return Task.FromResult((IEnumerable)Array.Empty()); } RefreshCacheIfNeeded(); if (_cache == null || _cache.Count == 0) { return Task.FromResult((IEnumerable)Array.Empty()); } string q = query.Trim().ToLowerInvariant(); List result = (from b in _cache.Where((BookmarkEntry b) => b.Name.ToLowerInvariant().Contains(q) || (b.Url?.ToLowerInvariant().Contains(q) ?? false)).Take(8) select new LauncherItem(b.Name, b.Url ?? "", null, b.Url, null, "\ue774")).ToList(); return Task.FromResult((IEnumerable)result); } 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 void RefreshCacheIfNeeded() { if (_cache != null && DateTime.Now - _cacheTime < CacheTtl) { return; } List list = new List(); foreach (string bookmarkFile in GetBookmarkFiles()) { try { string json = File.ReadAllText(bookmarkFile); ParseChromeBookmarks(json, list); } catch (Exception ex) { LogService.Warn("북마크 파일 읽기 실패: " + bookmarkFile + " — " + ex.Message); } } _cache = list; _cacheTime = DateTime.Now; LogService.Info($"북마크 로드 완료: {list.Count}개"); } private static IEnumerable GetBookmarkFiles() { string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string[] chromePaths = new string[4] { Path.Combine(localAppData, "Google", "Chrome", "User Data"), Path.Combine(localAppData, "Google", "Chrome Beta", "User Data"), Path.Combine(localAppData, "Google", "Chrome Dev", "User Data"), Path.Combine(localAppData, "Google", "Chrome SxS", "User Data") }; string[] edgePaths = new string[4] { Path.Combine(localAppData, "Microsoft", "Edge", "User Data"), Path.Combine(localAppData, "Microsoft", "Edge Beta", "User Data"), Path.Combine(localAppData, "Microsoft", "Edge Dev", "User Data"), Path.Combine(localAppData, "Microsoft", "Edge Canary", "User Data") }; foreach (string profileRoot in chromePaths.Concat(edgePaths)) { if (!Directory.Exists(profileRoot)) { continue; } string defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks"); if (File.Exists(defaultBookmark)) { yield return defaultBookmark; } string[] directories = Directory.GetDirectories(profileRoot, "Profile *"); foreach (string dir in directories) { string f = Path.Combine(dir, "Bookmarks"); if (File.Exists(f)) { yield return f; } } } } private static void ParseChromeBookmarks(string json, List result) { JsonNode jsonNode = JsonNode.Parse(json)?["roots"]; if (jsonNode == null) { return; } string[] array = new string[3] { "bookmark_bar", "other", "synced" }; foreach (string propertyName in array) { JsonNode jsonNode2 = jsonNode[propertyName]; if (jsonNode2 != null) { WalkNode(jsonNode2, result); } } } private static void WalkNode(JsonNode node, List result) { string text = node["type"]?.GetValue(); if (text == "url") { string text2 = node["name"]?.GetValue() ?? ""; string text3 = node["url"]?.GetValue() ?? ""; if (!string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(text3)) { result.Add(new BookmarkEntry(text2, text3)); } } else { if (!(text == "folder")) { return; } JsonArray jsonArray = node["children"]?.AsArray(); if (jsonArray == null) { return; } foreach (JsonNode item in jsonArray) { if (item != null) { WalkNode(item, result); } } } } }