refactor(services): split services into separate folders and update namespaces

This commit is contained in:
ShiYu
2025-05-22 21:18:02 +08:00
parent 9243a26189
commit fba716ba28
33 changed files with 96 additions and 107 deletions
+433
View File
@@ -0,0 +1,433 @@
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using Foxel.Services.Configuration;
using Foxel.Utils;
namespace Foxel.Services.AI;
public class AiService : IAiService
{
private readonly HttpClient _httpClient;
private readonly IConfigService _configService;
public AiService(HttpClient httpClient, IConfigService configService)
{
_httpClient = httpClient;
_configService = configService;
string apiKey = _configService["AI:ApiKey"];
string baseUrl = _configService["AI:ApiEndpoint"];
_httpClient.BaseAddress = new Uri(baseUrl);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
public async Task<(string title, string description)> AnalyzeImageAsync(string base64Image)
{
try
{
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
Url = $"data:image/jpeg;base64,{base64Image}"
};
var imageContent = new ImageUrlContent
{
Type = "image_url",
ImageUrl = imageUrl
};
var textContent = new TextContent
{
Type = "text",
Text =
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { imageContent, textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 800,
Temperature = 0.5,
TopP = 0.8,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return ("未能获取标题", "未能获取描述");
}
var aiMessage = responseContent.Choices[0].Message.Content;
return AiHelper.ExtractTitleAndDescription(aiMessage);
}
catch (Exception ex)
{
Console.WriteLine($"AI分析图片时出错: {ex.Message}");
return ("处理失败", $"AI分析过程中发生错误: {ex.Message}");
}
}
public async Task<List<string>> MatchTagsAsync(string description, List<string> availableTags)
{
try
{
if (availableTags.Count == 0)
return new List<string>();
string model = _configService["AI:Model"];
var tagsText = string.Join(", ", availableTags);
var textContent = new TextContent
{
Type = "text",
Text =
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。"
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 200,
Temperature = 0.1, // 降低温度使结果更确定性
TopP = 0.95,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return new List<string>();
}
var aiMessage = responseContent.Choices[0].Message.Content;
if (string.IsNullOrEmpty(aiMessage))
return new List<string>();
if (aiMessage.Contains("{") && aiMessage.Contains("}"))
{
try
{
int jsonStartIndex = aiMessage.IndexOf('{');
int jsonEndIndex = aiMessage.LastIndexOf('}') + 1;
if (jsonStartIndex >= 0 && jsonEndIndex > jsonStartIndex)
{
string jsonPart = aiMessage[jsonStartIndex..jsonEndIndex];
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var result =
System.Text.Json.JsonSerializer.Deserialize<AiHelper.TagsResult>(jsonPart, options);
if (result is { Tags.Length: > 0 })
{
// 确保返回的标签真的在可用标签列表中
var matchedTags = new List<string>();
foreach (var tagName in result.Tags)
{
if (string.IsNullOrWhiteSpace(tagName))
continue;
// 找到大小写完全匹配的标签
var exactMatch = availableTags.FirstOrDefault(t =>
string.Equals(t, tagName, StringComparison.OrdinalIgnoreCase));
if (exactMatch != null)
{
matchedTags.Add(exactMatch);
}
}
return matchedTags.Distinct().ToList();
}
}
}
catch (System.Text.Json.JsonException)
{
// JSON解析失败,返回空列表
return new List<string>();
}
}
// 解析失败或没有找到匹配标签,返回空列表
return new List<string>();
}
catch (Exception ex)
{
Console.WriteLine($"AI匹配标签时出错: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GenerateTagsFromImageAsync(string base64Image, List<string> availableTags,
bool allowNewTags = false)
{
try
{
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
Url = $"data:image/jpeg;base64,{base64Image}"
};
var imageContent = new ImageUrlContent
{
Type = "image_url",
ImageUrl = imageUrl
};
string promptText;
if (allowNewTags)
{
// 如果允许新标签,则提供现有标签作为参考,但允许生成新标签
promptText = availableTags.Count > 0
? $"可以参考这些现有标签:[{string.Join(", ", availableTags)}],但也可以生成其他与图片内容相关的新标签。\n\n请为图片生成5个最相关的标签,优先使用已有标签,但如果有更恰当的新标签也可以使用。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}}\n\n请确保返回有效的JSON格式。"
: "请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。";
}
else
{
// 如果不允许新标签,则只能从已有标签中选择
if (availableTags.Count == 0)
return new List<string>();
var tagsText = string.Join(", ", availableTags);
promptText =
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式,并且只包含上述列表中的标签名称。";
}
var textContent = new TextContent
{
Type = "text",
Text = promptText
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { imageContent, textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 200,
Temperature = 0.1, // 降低温度使结果更确定性
TopP = 0.95,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return new List<string>();
}
var aiMessage = responseContent.Choices[0].Message.Content;
if (string.IsNullOrEmpty(aiMessage))
return new List<string>();
if (aiMessage.Contains("{") && aiMessage.Contains("}"))
{
try
{
int jsonStartIndex = aiMessage.IndexOf('{');
int jsonEndIndex = aiMessage.LastIndexOf('}') + 1;
if (jsonStartIndex >= 0 && jsonEndIndex > jsonStartIndex)
{
string jsonPart = aiMessage[jsonStartIndex..jsonEndIndex];
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var result =
System.Text.Json.JsonSerializer.Deserialize<AiHelper.TagsResult>(jsonPart, options);
if (result is { Tags.Length: > 0 })
{
var matchedTags = new List<string>();
foreach (var tagName in result.Tags)
{
if (string.IsNullOrWhiteSpace(tagName))
continue;
// 如果允许新标签,直接添加
if (allowNewTags)
{
matchedTags.Add(tagName.Trim());
}
else
{
// 否则只添加已有标签列表中的标签
var exactMatch = availableTags.FirstOrDefault(t =>
string.Equals(t, tagName, StringComparison.OrdinalIgnoreCase));
if (exactMatch != null)
{
matchedTags.Add(exactMatch);
}
}
}
return matchedTags.Distinct().ToList();
}
}
}
catch (System.Text.Json.JsonException)
{
// JSON解析失败,返回空列表
return new List<string>();
}
}
// 解析失败或没有找到匹配标签,返回空列表
return new List<string>();
}
catch (Exception ex)
{
Console.WriteLine($"AI从图片生成标签时出错: {ex.Message}");
return new List<string>();
}
}
public async Task<float[]> GetEmbeddingAsync(string text)
{
try
{
string model = _configService["AI:EmbeddingModel"];
var requestContent = new
{
model,
input = text,
encoding_format = "float"
};
var response = await _httpClient.PostAsJsonAsync("/v1/embeddings", requestContent);
response.EnsureSuccessStatusCode();
var embedResult = await response.Content.ReadFromJsonAsync<EmbeddingResponse>();
if (embedResult?.Data == null || embedResult.Data.Length == 0)
{
Console.WriteLine("嵌入向量API返回空结果");
return Array.Empty<float>();
}
return embedResult.Data[0].Embedding;
}
catch (Exception ex)
{
Console.WriteLine($"获取嵌入向量时出错: {ex.Message}");
return Array.Empty<float>();
}
}
// 从EmbeddingService移植的私有记录类
private record EmbeddingResponse
{
[JsonPropertyName("data")] public EmbeddingData[] Data { get; set; } = Array.Empty<EmbeddingData>();
}
private record EmbeddingData
{
[JsonPropertyName("embedding")] public float[] Embedding { get; set; } = Array.Empty<float>();
}
private class AiResponse
{
[JsonPropertyName("choices")] public Choice[] Choices { get; set; } = Array.Empty<Choice>();
}
private class Choice
{
[JsonPropertyName("message")] public Message Message { get; set; } = new Message();
}
private class Message
{
[JsonPropertyName("content")] public string Content { get; set; } = string.Empty;
}
}
public class ChatCompletionRequest
{
[JsonPropertyName("model")] public string Model { get; set; } = string.Empty;
[JsonPropertyName("messages")] public ChatMessage[] Messages { get; set; } = Array.Empty<ChatMessage>();
[JsonPropertyName("stream")] public bool Stream { get; set; }
[JsonPropertyName("max_tokens")] public int MaxTokens { get; set; }
[JsonPropertyName("temperature")] public double Temperature { get; set; }
[JsonPropertyName("top_p")] public double TopP { get; set; }
[JsonPropertyName("top_k")] public int TopK { get; set; }
}
public class ChatMessage
{
[JsonPropertyName("role")] public string Role { get; set; } = string.Empty;
[JsonPropertyName("content")] public MessageContent[] Content { get; set; } = Array.Empty<MessageContent>();
}
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
[JsonDerivedType(typeof(ImageUrlContent), typeDiscriminator: "image_url")]
public abstract class MessageContent
{
[JsonPropertyName("type")] public string Type { get; set; } = string.Empty;
}
public class TextContent : MessageContent
{
[JsonPropertyName("text")] public string Text { get; set; } = string.Empty;
}
public class ImageUrlContent : MessageContent
{
[JsonPropertyName("image_url")] public ImageUrl ImageUrl { get; set; } = new();
}
public class ImageUrl
{
[JsonPropertyName("url")] public string Url { get; set; } = string.Empty;
}
+35
View File
@@ -0,0 +1,35 @@
namespace Foxel.Services.AI;
public interface IAiService
{
/// <summary>
/// 分析图像并返回标题和描述
/// </summary>
/// <param name="base64Image">Base64格式的图像数据</param>
/// <returns>图像的标题和描述</returns>
Task<(string title, string description)> AnalyzeImageAsync(string base64Image);
/// <summary>
/// 基于描述匹配标签
/// </summary>
/// <param name="description">图片描述</param>
/// <param name="availableTags">可用标签列表</param>
/// <returns>匹配的标签名称列表</returns>
Task<List<string>> MatchTagsAsync(string description, List<string> availableTags);
/// <summary>
/// 直接从图像生成标签
/// </summary>
/// <param name="base64Image">Base64格式的图像数据</param>
/// <param name="availableTags">可用标签列表</param>
/// <param name="allowNewTags">是否允许生成新标签(不在availableTags中的标签)</param>
/// <returns>匹配的标签名称列表</returns>
Task<List<string>> GenerateTagsFromImageAsync(string base64Image, List<string> availableTags, bool allowNewTags = false);
/// <summary>
/// 获取文本的嵌入向量
/// </summary>
/// <param name="text">需要进行嵌入的文本</param>
/// <returns>表示文本语义的浮点数组向量</returns>
Task<float[]> GetEmbeddingAsync(string text);
}