diff --git a/src/RitsukageBotForDiscord/Library/OpenApi/OpenApi.cs b/src/RitsukageBotForDiscord/Library/OpenApi/OpenApi.cs index d0bda1f..360e443 100644 --- a/src/RitsukageBotForDiscord/Library/OpenApi/OpenApi.cs +++ b/src/RitsukageBotForDiscord/Library/OpenApi/OpenApi.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; @@ -14,37 +16,38 @@ private OpenApi() { } + /// + /// Service provider + /// + public IServiceProvider? ServiceProvider { get; private set; } + /// /// Cache provider /// - public IFusionCache? CacheProvider { get; private set; } + public IFusionCache? CacheProvider => ServiceProvider?.GetService(); /// - /// Logger + /// Configuration /// - public ILogger? Logger { get; private set; } + public IConfiguration? Configuration => ServiceProvider?.GetService(); /// - /// Instance of OpenApi + /// Logger /// - public static OpenApi Instance => OpenApiInstance.Value; + public ILogger? Logger => ServiceProvider?.GetService>(); /// - /// Set cache provider + /// Instance of OpenApi /// - /// - public void SetCacheProvider(IFusionCache cacheProvider) - { - CacheProvider = cacheProvider; - } + public static OpenApi Instance => OpenApiInstance.Value; /// - /// Set logger + /// Set service provider /// - /// - public void SetLogger(ILogger logger) + /// + public void SetServiceProvider(IServiceProvider serviceProvider) { - Logger = logger; + ServiceProvider = serviceProvider; } } } \ No newline at end of file diff --git a/src/RitsukageBotForDiscord/Library/OpenApi/Pixiv/OpenApi.Pixiv.cs b/src/RitsukageBotForDiscord/Library/OpenApi/Pixiv/OpenApi.Pixiv.cs index 95cf00a..a92437d 100644 --- a/src/RitsukageBotForDiscord/Library/OpenApi/Pixiv/OpenApi.Pixiv.cs +++ b/src/RitsukageBotForDiscord/Library/OpenApi/Pixiv/OpenApi.Pixiv.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RitsukageBot.Library.OpenApi.Pixiv.Structs; @@ -34,8 +35,11 @@ public static class OpenApiPixiv httpClient ??= NetworkUtility.GetHttpClient(); - if (!httpClient.DefaultRequestHeaders.Contains("Referer")) - httpClient.DefaultRequestHeaders.Add("Referer", "https://www.pixiv.net/"); + httpClient.DefaultRequestHeaders.Referrer = new("https://www.pixiv.net/"); + + var cookieString = instance.Configuration?.GetValue("OpenApi:PixivCookie"); + if (!string.IsNullOrWhiteSpace(cookieString)) + httpClient.DefaultRequestHeaders.Add("Cookie", cookieString); try { @@ -90,8 +94,11 @@ public static class OpenApiPixiv httpClient ??= NetworkUtility.GetHttpClient(); - if (!httpClient.DefaultRequestHeaders.Contains("Referer")) - httpClient.DefaultRequestHeaders.Add("Referer", "https://www.pixiv.net/"); + httpClient.DefaultRequestHeaders.Referrer = new("https://www.pixiv.net/"); + + var cookieString = instance.Configuration?.GetValue("OpenApi:PixivCookie"); + if (!string.IsNullOrWhiteSpace(cookieString)) + httpClient.DefaultRequestHeaders.Add("Cookie", cookieString); try { @@ -121,6 +128,17 @@ public static class OpenApiPixiv return null; } - } -} + /// + /// Returns a proxied Pixiv image URL using the configured proxy template, or the original image URL if no proxy is set. + /// + /// OpenApi instance + /// Original Pixiv image URL + /// Proxied image URL or original image URL + public static string GetPixivImageProxyUrl(this OpenApi instance, string imageUrl) + { + var proxyTemplate = instance.Configuration?.GetValue("OpenApi:PixivImageProxy"); + return string.IsNullOrWhiteSpace(proxyTemplate) ? imageUrl : string.Format(proxyTemplate, imageUrl); + } + } +} \ No newline at end of file diff --git a/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Chatting.cs b/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Chatting.cs index 6911959..72a04e5 100644 --- a/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Chatting.cs +++ b/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Chatting.cs @@ -479,10 +479,9 @@ private async Task ProgressActions(string json) return [.. result]; } - private async Task ProcessingModifyGood(JObject data) + private async Task ProcessingModifyGood(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for good action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for good action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for good action"); @@ -517,10 +516,9 @@ await ChatClientProvider.RecordChatDataChangeHistory(Context.User.Id, "good", pa return embedBuilder; } - private async Task ProcessingAddShortMemory(JObject data) + private async Task ProcessingAddShortMemory(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for add_short_memory action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for add_short_memory action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for add_short_memory action"); @@ -546,10 +544,9 @@ await ChatClientProvider return embed; } - private async Task ProcessingAddLongMemory(JObject data) + private async Task ProcessingAddLongMemory(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for add_long_memory action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for add_long_memory action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for add_long_memory action"); @@ -575,10 +572,9 @@ await ChatClientProvider return embed; } - private async Task ProcessingRemoveLongMemory(JObject data) + private async Task ProcessingRemoveLongMemory(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for remove_long_memory action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for remove_long_memory action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for remove_long_memory action"); @@ -603,10 +599,9 @@ await ChatClientProvider.RemoveMemory(Context.User.Id, ChatMemoryType.LongTerm, return embed; } - private async Task ProcessingUpdateSelfState(JObject data) + private async Task ProcessingUpdateSelfState(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for update_self_state action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for update_self_state action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for update_self_state action"); @@ -631,10 +626,9 @@ await ChatClientProvider return embed; } - private async Task ProcessingRemoveSelfState(JObject data) + private async Task ProcessingRemoveSelfState(JObject? data) { - if (data is null) throw new InvalidDataException("Invalid JSON data for remove_self_state action"); - if (!data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) + if (data is null || !data.TryGetValue("param", out var paramValue) || paramValue is not JObject paramToken) throw new InvalidDataException("Invalid JSON data for remove_self_state action"); var param = paramToken.ToObject() ?? throw new InvalidDataException("Invalid JSON data for remove_self_state action"); diff --git a/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Preprocessing.cs b/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Preprocessing.cs index f9c3e3b..c0a3d3f 100644 --- a/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Preprocessing.cs +++ b/src/RitsukageBotForDiscord/Modules/AI/AiInteractions.Preprocessing.cs @@ -369,6 +369,22 @@ private async Task PreprocessingPixivQueryIllustInfo(JO sb.AppendLine($"点赞数:{result.LikeCount}"); sb.AppendLine($"评论数:{result.CommentCount}"); + if (!string.IsNullOrWhiteSpace(result.Urls.Original)) + { + var (success, explainResult) = + await ChatClientProvider.ExplainImageAsync( + OpenApi.Instance.GetPixivImageProxyUrl(result.Urls.Original)); + if (success && !string.IsNullOrWhiteSpace(explainResult)) + sb.AppendLine($""" + 图像内容描述: + ``` + {explainResult} + ``` + """); + else + sb.AppendLine($"<图像内容解析失败,原因:{explainResult}>"); + } + var otherArtworks = result.UserIllusts?.Where(x => x.Value != null).Select(x => { var illust = x.Value!; diff --git a/src/RitsukageBotForDiscord/Program.cs b/src/RitsukageBotForDiscord/Program.cs index 3594daa..502c0e2 100644 --- a/src/RitsukageBotForDiscord/Program.cs +++ b/src/RitsukageBotForDiscord/Program.cs @@ -128,8 +128,6 @@ logger.LogInformation("Network default User-Agent: {UserAgent}", UserAgent.Default); NetworkUtility.SetHttpClientFactory(host.Services.GetRequiredService()); -OpenApi.Instance.SetCacheProvider(host.Services.GetRequiredService()); -OpenApi.Instance.SetLogger(host.Services.GetRequiredService>()); - +OpenApi.Instance.SetServiceProvider(host.Services); await host.RunAsync(HostCancellationToken.Token).ConfigureAwait(false); \ No newline at end of file diff --git a/src/RitsukageBotForDiscord/Services/Providers/ChatClientProviderService.cs b/src/RitsukageBotForDiscord/Services/Providers/ChatClientProviderService.cs index 9f32633..3314635 100644 --- a/src/RitsukageBotForDiscord/Services/Providers/ChatClientProviderService.cs +++ b/src/RitsukageBotForDiscord/Services/Providers/ChatClientProviderService.cs @@ -31,8 +31,6 @@ public partial class ChatClientProviderService(IServiceProvider serviceProvider) private readonly ILogger _logger = serviceProvider.GetRequiredService>(); - private readonly IServiceProvider _serviceProvider = serviceProvider; - private IChatClient CreateChatClient(EndpointConfig config) { if (string.IsNullOrWhiteSpace(config.Endpoint)) @@ -49,7 +47,7 @@ private IChatClient CreateChatClient(EndpointConfig config) }), config.ModelId); var client = new ChatClientBuilder(innerChatClient) - .UseDistributedCache(_serviceProvider.GetRequiredService()) + .UseDistributedCache(serviceProvider.GetRequiredService()) //.UseLogging(_serviceProvider.GetRequiredService()) .Build(); return client; @@ -84,9 +82,9 @@ public EndpointConfig GetFirstChatEndpoint() { var random = _configuration.GetValue("AI:FirstServiceRandom"); var configs = GetEndpointConfigs(); - if (configs.Length == 0) - throw new InvalidDataException("No endpoint is configured"); - return configs[random ? Random.Shared.Next(configs.Length) : 0]; + return configs.Length == 0 + ? throw new InvalidDataException("No endpoint is configured") + : configs[random ? Random.Shared.Next(configs.Length) : 0]; } /// @@ -97,9 +95,9 @@ public EndpointConfig GetFirstChatEndpoint() public EndpointConfig GetChatEndpointRandomly() { var configs = GetEndpointConfigs(); - if (configs.Length == 0) - throw new InvalidDataException("No endpoint is configured"); - return configs[Random.Shared.Next(configs.Length)]; + return configs.Length == 0 + ? throw new InvalidDataException("No endpoint is configured") + : configs[Random.Shared.Next(configs.Length)]; } internal EndpointConfig[] GetEndpointConfigs() @@ -262,6 +260,79 @@ public string[] GetRoles() return [.. _configuration.GetSection("AI:RoleData").GetChildren().Select(x => x.Key)]; } + /// + /// Check if ImageExplainer is enabled + /// + /// + public bool IsImageExplainerEnabled() + { + var config = _configuration.GetSection("AI:Function:ImageExplainer").Get(); + return config?.Enabled ?? false; + } + + /// + /// Get ImageExplainer configuration + /// + /// + public ImageExplainerConfig? GetImageExplainerConfig() + { + return _configuration.GetSection("AI:Function:ImageExplainer").Get(); + } + + /// + /// Explain image content using AI + /// + /// The URL of the image to explain + /// The explanation of the image content + /// Thrown when ImageExplainer is not enabled or configured properly + public async Task<(bool, string)> ExplainImageAsync(string imageUrl) + { + var config = GetImageExplainerConfig(); + if (config is null || !config.Enabled) + return (false, "ImageExplainer is not enabled or configured"); + + if (string.IsNullOrWhiteSpace(imageUrl)) + return (false, "Image URL cannot be null or empty"); + + try + { + var chatClient = CreateChatClient(config.Service); + + // Get prompt from configuration + if (!GetPrompt(config.PromptConfig, out var prompt)) + { + _logger.LogWarning("ImageExplainer prompt is empty, using default"); + prompt = + "Please describe the content of this image in detail, including the main object, scene, color, composition, etc."; + } + + var messages = new List + { + new(ChatRole.System, + "You are a professional image analysis assistant who can describe image content in detail."), + new(ChatRole.User, [ + new TextContent(prompt), + new ImageContent(imageUrl), + ]), + }; + + var chatOptions = new ChatOptions + { + Temperature = config.PromptConfig.Temperature, + }; + + var response = await chatClient.CompleteAsync(messages, chatOptions).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(response.Message.Text)) + return (true, response.Message.Text); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to explain image: {ImageUrl}", imageUrl); + } + + return (false, "Failed to explain image"); + } + /// /// Get memory /// @@ -686,6 +757,14 @@ public record PromptConfig(string Prompt = "", string PromptFile = "", float Tem /// /// public record AssistantConfig(bool Enabled, EndpointConfig Service, PromptConfig PromptConfig); + + /// + /// Image explainer configuration + /// + /// + /// + /// + public record ImageExplainerConfig(bool Enabled, EndpointConfig Service, PromptConfig PromptConfig); } /// diff --git a/src/RitsukageBotForDiscord/appsettings.json b/src/RitsukageBotForDiscord/appsettings.json index 0a83065..16e7d51 100644 --- a/src/RitsukageBotForDiscord/appsettings.json +++ b/src/RitsukageBotForDiscord/appsettings.json @@ -32,6 +32,10 @@ } ] }, + "OpenApi": { + "PixivImageProxy": "{0}", + "PixivCookie": "" + }, "AI": { "Enabled": false, "Service": [ @@ -87,6 +91,20 @@ "TimeBroadcast": { "Enabled": false, "Prompt": "" + }, + "ImageExplainer": { + "Enabled": false, + "Service": { + "Name": "", + "Endpoint": "", + "ApiKey": "", + "ModelId": "" + }, + "PromptConfig": { + "Prompt": "Please describe the content of this image in detail, including the main object, scene, color, composition, etc.", + "PromptFile": "", + "Temperature": 0.7 + } } }, "Config": {