feat: 支持图片生成

This commit is contained in:
ccnetcore
2025-08-03 23:23:32 +08:00
parent faa8131a1b
commit 2a301c4983
14 changed files with 436 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -0,0 +1,39 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IImageService
{
/// <summary>Creates an image given a prompt.</summary>
/// <param name="imageCreate"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImage(
ImageCreateRequest imageCreate,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates an edited or extended image given an original image and a prompt.
/// </summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageEdit(
ImageEditCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>Creates a variation of a given image.</summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageVariation(
ImageVariationCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,112 @@
using OpenAI.Images;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
public class AzureOpenAIServiceImageService : IImageService
{
public async Task<ImageCreateResponse> CreateImage(ImageCreateRequest imageCreate, AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
var createClient = AzureOpenAIFactory.CreateClient(options);
var client = createClient.GetImageClient(imageCreate.Model);
// 将size字符串拆分为宽度和高度
var size = imageCreate.Size.Split('x');
if (size.Length != 2)
{
throw new ArgumentException("Size must be in the format of 'width x height'");
}
var response = await client.GenerateImageAsync(imageCreate.Prompt, new ImageGenerationOptions()
{
Quality = imageCreate.Quality == "standard" ? GeneratedImageQuality.Standard : GeneratedImageQuality.High,
Size = new GeneratedImageSize(Convert.ToInt32(size[0]), Convert.ToInt32(size[1])),
Style = imageCreate.Style == "vivid" ? GeneratedImageStyle.Vivid : GeneratedImageStyle.Natural,
ResponseFormat =
imageCreate.ResponseFormat == "url" ? GeneratedImageFormat.Uri : GeneratedImageFormat.Bytes,
// User = imageCreate.User
EndUserId = imageCreate.User
}, cancellationToken);
var ret = new ImageCreateResponse()
{
Results = new List<ImageCreateResponse.ImageDataResult>()
};
if (response.Value.ImageUri != null)
{
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
{
Url = response.Value.ImageUri.ToString()
});
}
else
{
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
{
B64 = Convert.ToBase64String(response.Value.ImageBytes.ToArray())
});
}
return ret;
}
public async Task<ImageCreateResponse> CreateImageEdit(ImageEditCreateRequest imageEditCreateRequest,
AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
var url = AzureOpenAIFactory.GetEditImageAddress(options, imageEditCreateRequest.Model);
var multipartContent = new MultipartFormDataContent();
if (imageEditCreateRequest.User != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.User), "user");
}
if (imageEditCreateRequest.ResponseFormat != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.ResponseFormat), "response_format");
}
if (imageEditCreateRequest.Size != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.Size), "size");
}
if (imageEditCreateRequest.N != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.N.ToString()!), "n");
}
if (imageEditCreateRequest.Model != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.Model!), "model");
}
if (imageEditCreateRequest.Mask != null)
{
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Mask), "mask",
imageEditCreateRequest.MaskName);
}
multipartContent.Add(new StringContent(imageEditCreateRequest.Prompt), "prompt");
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Image), "image",
imageEditCreateRequest.ImageName);
return await HttpClientFactory.GetHttpClient(url).PostFileAndReadAsAsync<ImageCreateResponse>(
url,
multipartContent, cancellationToken);
}
public Task<ImageCreateResponse> CreateImageVariation(ImageVariationCreateRequest imageEditCreateRequest,
AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}

View File

@@ -1,26 +0,0 @@
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public record ThorBaseResponse
{
/// <summary>
/// 对象类型
/// </summary>
[JsonPropertyName("object")]
public string? ObjectTypeName { get; set; }
/// <summary>
///
/// </summary>
public bool Successful => Error == null;
/// <summary>
///
/// </summary>
[JsonPropertyName("error")]
public ThorError? Error { get; set; }
}

View File

@@ -1,5 +1,6 @@
using SqlSugar;
using Volo.Abp.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Core.Data;
namespace Yi.Framework.AiHub.Domain.Entities.Model;
@@ -8,7 +9,7 @@ namespace Yi.Framework.AiHub.Domain.Entities.Model;
/// ai模型定义
/// </summary>
[SugarTable("Ai_Model")]
public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
{
/// <summary>
/// 处理名
@@ -44,9 +45,14 @@ public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
/// ai应用id
/// </summary>
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public ModelTypeEnum ModelType { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.AspNetCore.Http;
@@ -9,9 +10,12 @@ using Newtonsoft.Json.Serialization;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.Core.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -213,7 +217,7 @@ public class AiGateWayManager : DomainService
catch (Exception e)
{
_logger.LogError(e, $"Ai对话异常");
var errorContent = $"Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
var model = new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
@@ -261,4 +265,61 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
}
/// <summary>
/// 图片生成
/// </summary>
/// <param name="context"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="request"></param>
/// <exception cref="BusinessException"></exception>
/// <exception cref="Exception"></exception>
public async Task CreateImageForStatisticsAsync(HttpContext context,Guid? userId,Guid? sessionId, ImageCreateRequest request)
{
try
{
var model = request.Model;
if (string.IsNullOrEmpty(model)) model = "dall-e-2";
var modelDescribe = await GetModelAsync(model);
// 获取渠道指定的实现类型的服务
var imageService =
LazyServiceProvider.GetRequiredKeyedService<IImageService>(modelDescribe.HandlerName);
var response = await imageService.CreateImage(request, modelDescribe);
if (response.Error != null || response.Results.Count == 0)
{
throw new BusinessException(response.Error?.Message ?? "图片生成失败", response.Error?.Code?.ToString());
}
await context.Response.WriteAsJsonAsync(response);
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = request.Prompt,
ModelId = model,
TokenUsage = response.Usage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = response.Results?.FirstOrDefault()?.Url,
ModelId = model,
TokenUsage = response.Usage
});
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
}
catch (Exception e)
{
var errorContent = $"图片生成Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
throw new UserFriendlyException(errorContent);
}
}
}