namespace Service.IconSearch; /// /// Iconify API 响应 /// 实际 API 返回的图标是字符串数组,格式为 "collection:iconName" /// 例如:["mdi:home", "svg-spinners:wind-toy"] /// public record IconifyApiResponse { [JsonPropertyName("icons")] public List? Icons { get; init; } } public record IconifySettings { public string ApiUrl { get; init; } = "https://api.iconify.design/search"; public int DefaultLimit { get; init; } = 20; public int MaxRetryCount { get; init; } = 3; public int RetryDelayMs { get; init; } = 1000; } public class IconifyApiService( IOptions settings, ILogger logger ) : IIconifyApiService { private readonly HttpClient _httpClient = new(); private readonly IconifySettings _settings = settings.Value; public async Task> SearchIconsAsync(List keywords, int limit = 20) { var allIcons = new List(); var actualLimit = limit > 0 ? limit : _settings.DefaultLimit; foreach (var keyword in keywords) { try { var icons = await SearchIconsByKeywordAsync(keyword, actualLimit); allIcons.AddRange(icons); } catch (Exception ex) { logger.LogError(ex, "搜索图标失败,关键字:{Keyword}", keyword); } } return allIcons; } private async Task> SearchIconsByKeywordAsync(string keyword, int limit) { var url = $"{_settings.ApiUrl}?query={Uri.EscapeDataString(keyword)}&limit={limit}"; var response = await CallApiWithRetryAsync(url); if (string.IsNullOrEmpty(response)) { return []; } var apiResponse = JsonSerializer.Deserialize(response); if (apiResponse?.Icons == null) { return []; } // 解析字符串格式 "collection:iconName" 为 IconCandidate var candidates = apiResponse.Icons .Select(iconStr => { var parts = iconStr.Split(':', 2); if (parts.Length != 2) { logger.LogWarning("无效的图标标识符格式:{IconStr}", iconStr); return null; } return new IconCandidate { CollectionName = parts[0], IconName = parts[1] }; }) .Where(c => c != null) .Cast() .ToList(); return candidates; } private async Task CallApiWithRetryAsync(string url) { var retryCount = 0; var delay = _settings.RetryDelayMs; while (retryCount < _settings.MaxRetryCount) { try { var response = await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } catch (HttpRequestException ex) when (retryCount < _settings.MaxRetryCount - 1) { logger.LogWarning(ex, "Iconify API调用失败,等待 {DelayMs}ms 后重试({RetryCount}/{MaxRetryCount})", delay, retryCount + 1, _settings.MaxRetryCount); await Task.Delay(delay); delay *= 2; retryCount++; } } throw new HttpRequestException($"Iconify API调用失败,已重试 {_settings.MaxRetryCount} 次"); } }