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": {