using System.IO; using System.Text.Json; using System.Text.Json.Nodes; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// Chrome / Edge 브라우저 북마크를 검색합니다. /// 프리픽스 없음 — 일반 퍼지 검색에 통합됩니다. /// 지원 브라우저: Google Chrome, Microsoft Edge /// public class BookmarkHandler : IActionHandler { public string? Prefix => null; // 퍼지 검색에 통합 public PluginMetadata Metadata => new( "Bookmarks", "Chrome / Edge 북마크 검색", "1.0", "AX"); // 캐시 (앱 세션 중 북마크가 자주 바뀌지 않으므로 한 번 로드 후 재사용) private List? _cache; private DateTime _cacheTime = DateTime.MinValue; private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5); public Task> GetItemsAsync(string query, CancellationToken ct) { if (string.IsNullOrWhiteSpace(query)) return Task.FromResult>(Array.Empty()); RefreshCacheIfNeeded(); if (_cache == null || _cache.Count == 0) return Task.FromResult>(Array.Empty()); var q = query.Trim().ToLowerInvariant(); var results = _cache .Where(b => b.Name.ToLowerInvariant().Contains(q) || (b.Url?.ToLowerInvariant().Contains(q) ?? false)) .Take(8) .Select(b => new LauncherItem( b.Name, b.Url ?? "", null, b.Url, Symbol: Symbols.Globe, Group: "📑 북마크")) .ToList(); return Task.FromResult>(results); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is string url && !string.IsNullOrWhiteSpace(url)) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"북마크 열기 실패: {ex.Message}"); } } return Task.CompletedTask; } // ─── 캐시 ──────────────────────────────────────────────────────────────── private void RefreshCacheIfNeeded() { if (_cache != null && DateTime.Now - _cacheTime < CacheTtl) return; var bookmarks = new List(); foreach (var file in GetBookmarkFiles()) { try { var json = File.ReadAllText(file); ParseChromeBookmarks(json, bookmarks); } catch (Exception ex) { LogService.Warn($"북마크 파일 읽기 실패: {file} — {ex.Message}"); } } _cache = bookmarks; _cacheTime = DateTime.Now; LogService.Info($"북마크 로드 완료: {bookmarks.Count}개"); } // ─── 북마크 파일 경로 ───────────────────────────────────────────────────── private static IEnumerable GetBookmarkFiles() { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); // Chrome (안정, 베타, 개발, 카나리아) var chromePaths = new[] { 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"), }; // Edge var edgePaths = new[] { 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 (var profileRoot in chromePaths.Concat(edgePaths)) { if (!Directory.Exists(profileRoot)) continue; // Default 프로파일 var defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks"); if (File.Exists(defaultBookmark)) yield return defaultBookmark; // Profile 1, 2, ... 프로파일 foreach (var dir in Directory.GetDirectories(profileRoot, "Profile *")) { var f = Path.Combine(dir, "Bookmarks"); if (File.Exists(f)) yield return f; } } } // ─── Chrome/Edge JSON 파싱 ─────────────────────────────────────────────── private static void ParseChromeBookmarks(string json, List result) { var doc = JsonNode.Parse(json); var roots = doc?["roots"]; if (roots == null) return; foreach (var rootKey in new[] { "bookmark_bar", "other", "synced" }) { var node = roots[rootKey]; if (node != null) WalkNode(node, result); } } private static void WalkNode(JsonNode node, List result) { var type = node["type"]?.GetValue(); if (type == "url") { var name = node["name"]?.GetValue() ?? ""; var url = node["url"]?.GetValue() ?? ""; if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url)) result.Add(new BookmarkEntry(name, url)); return; } if (type == "folder") { var children = node["children"]?.AsArray(); if (children == null) return; foreach (var child in children) { if (child != null) WalkNode(child, result); } } } private record BookmarkEntry(string Name, string? Url); }