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);
}