diff --git a/Yi.Abp.Net8/.claude/settings.local.json b/Yi.Abp.Net8/.claude/settings.local.json
index 54ea23cd..e399b33d 100644
--- a/Yi.Abp.Net8/.claude/settings.local.json
+++ b/Yi.Abp.Net8/.claude/settings.local.json
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)",
- "Read(//e/code/github/Yi/Yi.Ai.Vue3/**)"
+ "Read(//e/code/github/Yi/Yi.Ai.Vue3/**)",
+ "Bash(dotnet build:*)"
],
"deny": [],
"ask": []
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageGenerationInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageGenerationInput.cs
new file mode 100644
index 00000000..c9bade65
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageGenerationInput.cs
@@ -0,0 +1,22 @@
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+
+///
+/// 图片生成输入
+///
+public class ImageGenerationInput
+{
+ ///
+ /// 提示词
+ ///
+ public string Prompt { get; set; } = string.Empty;
+
+ ///
+ /// 模型ID
+ ///
+ public string ModelId { get; set; } = string.Empty;
+
+ ///
+ /// 参考图Base64列表(可选,包含前缀如 data:image/png;base64,...)
+ ///
+ public List? ReferenceImagesBase64 { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskOutput.cs
new file mode 100644
index 00000000..1834b5a0
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskOutput.cs
@@ -0,0 +1,49 @@
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+
+///
+/// 图片任务输出
+///
+public class ImageTaskOutput
+{
+ ///
+ /// 任务ID
+ ///
+ public Guid Id { get; set; }
+
+ ///
+ /// 提示词
+ ///
+ public string Prompt { get; set; } = string.Empty;
+
+ ///
+ /// 参考图Base64列表
+ ///
+ public List? ReferenceImagesBase64 { get; set; }
+
+ ///
+ /// 参考图URL列表
+ ///
+ public List? ReferenceImagesUrl { get; set; }
+
+ ///
+ /// 生成图片Base64(包含前缀)
+ ///
+ public string? StoreBase64 { get; set; }
+
+ ///
+ /// 生成图片URL
+ ///
+ public string? StoreUrl { get; set; }
+
+ ///
+ /// 任务状态
+ ///
+ public TaskStatusEnum TaskStatus { get; set; }
+
+ ///
+ /// 创建时间
+ ///
+ public DateTime CreationTime { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskPageInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskPageInput.cs
new file mode 100644
index 00000000..c42985d5
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Chat/ImageTaskPageInput.cs
@@ -0,0 +1,24 @@
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+
+namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+
+///
+/// 图片任务分页查询输入
+///
+public class ImageTaskPageInput
+{
+ ///
+ /// 页码(从1开始)
+ ///
+ public int PageIndex { get; set; } = 1;
+
+ ///
+ /// 每页数量
+ ///
+ public int PageSize { get; set; } = 10;
+
+ ///
+ /// 任务状态筛选(可选)
+ ///
+ public TaskStatusEnum? TaskStatus { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJob.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJob.cs
new file mode 100644
index 00000000..fca09694
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJob.cs
@@ -0,0 +1,61 @@
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.DependencyInjection;
+using Yi.Framework.AiHub.Domain.Entities.Chat;
+using Yi.Framework.AiHub.Domain.Managers;
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+using Yi.Framework.SqlSugarCore.Abstractions;
+
+namespace Yi.Framework.AiHub.Application.Jobs;
+
+///
+/// 图片生成后台任务
+///
+public class ImageGenerationJob : AsyncBackgroundJob, ITransientDependency
+{
+ private readonly ILogger _logger;
+ private readonly AiGateWayManager _aiGateWayManager;
+ private readonly ISqlSugarRepository _imageStoreTaskRepository;
+
+ public ImageGenerationJob(
+ ILogger logger,
+ AiGateWayManager aiGateWayManager,
+ ISqlSugarRepository imageStoreTaskRepository)
+ {
+ _logger = logger;
+ _aiGateWayManager = aiGateWayManager;
+ _imageStoreTaskRepository = imageStoreTaskRepository;
+ }
+
+ public override async Task ExecuteAsync(ImageGenerationJobArgs args)
+ {
+ _logger.LogInformation("开始执行图片生成任务,TaskId: {TaskId}, ModelId: {ModelId}, UserId: {UserId}",
+ args.TaskId, args.ModelId, args.UserId);
+
+ try
+ {
+ var request = JsonSerializer.Deserialize(args.RequestJson);
+
+ await _aiGateWayManager.GeminiGenerateContentImageForStatisticsAsync(
+ args.TaskId,
+ args.ModelId,
+ request,
+ args.UserId);
+
+ _logger.LogInformation("图片生成任务完成,TaskId: {TaskId}", args.TaskId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "图片生成任务失败,TaskId: {TaskId}, Error: {Error}", args.TaskId, ex.Message);
+
+ // 更新任务状态为失败
+ var task = await _imageStoreTaskRepository.GetFirstAsync(x => x.Id == args.TaskId);
+ if (task != null)
+ {
+ task.TaskStatus = TaskStatusEnum.Fail;
+ await _imageStoreTaskRepository.UpdateAsync(task);
+ }
+ }
+ }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJobArgs.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJobArgs.cs
new file mode 100644
index 00000000..666b6b59
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Jobs/ImageGenerationJobArgs.cs
@@ -0,0 +1,27 @@
+namespace Yi.Framework.AiHub.Application.Jobs;
+
+///
+/// 图片生成后台任务参数
+///
+public class ImageGenerationJobArgs
+{
+ ///
+ /// 图片任务ID
+ ///
+ public Guid TaskId { get; set; }
+
+ ///
+ /// 模型ID
+ ///
+ public string ModelId { get; set; } = string.Empty;
+
+ ///
+ /// 请求JSON字符串
+ ///
+ public string RequestJson { get; set; } = string.Empty;
+
+ ///
+ /// 用户ID
+ ///
+ public Guid UserId { get; set; }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiImageService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiImageService.cs
new file mode 100644
index 00000000..5bffa03d
--- /dev/null
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiImageService.cs
@@ -0,0 +1,271 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.Guids;
+using Volo.Abp.Users;
+using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
+using Yi.Framework.AiHub.Application.Jobs;
+using Yi.Framework.AiHub.Domain.Entities.Chat;
+using Yi.Framework.AiHub.Domain.Extensions;
+using Yi.Framework.AiHub.Domain.Managers;
+using Yi.Framework.AiHub.Domain.Shared.Consts;
+using Yi.Framework.AiHub.Domain.Shared.Enums;
+using Yi.Framework.SqlSugarCore.Abstractions;
+
+namespace Yi.Framework.AiHub.Application.Services.Chat;
+
+///
+/// AI图片生成服务
+///
+[Authorize]
+public class AiImageService : ApplicationService
+{
+ private readonly ISqlSugarRepository _imageTaskRepository;
+ private readonly IBackgroundJobManager _backgroundJobManager;
+ private readonly AiBlacklistManager _aiBlacklistManager;
+ private readonly PremiumPackageManager _premiumPackageManager;
+ private readonly IGuidGenerator _guidGenerator;
+ private readonly IWebHostEnvironment _webHostEnvironment;
+
+ public AiImageService(
+ ISqlSugarRepository imageTaskRepository,
+ IBackgroundJobManager backgroundJobManager,
+ AiBlacklistManager aiBlacklistManager,
+ PremiumPackageManager premiumPackageManager,
+ IGuidGenerator guidGenerator,
+ IWebHostEnvironment webHostEnvironment)
+ {
+ _imageTaskRepository = imageTaskRepository;
+ _backgroundJobManager = backgroundJobManager;
+ _aiBlacklistManager = aiBlacklistManager;
+ _premiumPackageManager = premiumPackageManager;
+ _guidGenerator = guidGenerator;
+ _webHostEnvironment = webHostEnvironment;
+ }
+
+ ///
+ /// 生成图片(异步任务)
+ ///
+ /// 图片生成输入参数
+ /// 任务ID
+ [HttpPost("ai-image/generate")]
+ [Authorize]
+ public async Task GenerateAsync([FromBody] ImageGenerationInput input)
+ {
+ var userId = CurrentUser.GetId();
+
+ // 黑名单校验
+ await _aiBlacklistManager.VerifiyAiBlacklist(userId);
+
+ // VIP校验
+ if (!CurrentUser.IsAiVip())
+ {
+ throw new UserFriendlyException("图片生成功能需要VIP用户才能使用,请购买VIP后重新登录重试");
+ }
+
+ // 尊享包校验
+ if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
+ {
+ var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
+ if (availableTokens <= 0)
+ {
+ throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
+ }
+ }
+
+ // 创建任务实体
+ var task = new ImageStoreTaskAggregateRoot
+ {
+ Prompt = input.Prompt,
+ ReferenceImagesBase64 = input.ReferenceImagesBase64 ?? new List(),
+ ReferenceImagesUrl = new List(),
+ TaskStatus = TaskStatusEnum.Processing,
+ UserId = userId
+ };
+
+ await _imageTaskRepository.InsertAsync(task);
+ var taskId = task.Id;
+
+ // 构建请求JSON
+ var requestJson = JsonSerializer.Serialize(new
+ {
+ prompt = input.Prompt,
+ referenceImages = input.ReferenceImagesBase64
+ });
+
+ // 入队后台任务
+ await _backgroundJobManager.EnqueueAsync(new ImageGenerationJobArgs
+ {
+ TaskId = taskId,
+ ModelId = input.ModelId,
+ RequestJson = requestJson,
+ UserId = userId
+ });
+
+ return taskId;
+ }
+
+ ///
+ /// 查询任务状态
+ ///
+ /// 任务ID
+ /// 任务详情
+ [HttpGet("ai-image/task/{taskId}")]
+ public async Task GetTaskAsync([FromRoute] Guid taskId)
+ {
+ var userId = CurrentUser.GetId();
+
+ var task = await _imageTaskRepository.GetFirstAsync(x => x.Id == taskId && x.UserId == userId);
+ if (task == null)
+ {
+ throw new UserFriendlyException("任务不存在或无权访问");
+ }
+
+ return new ImageTaskOutput
+ {
+ Id = task.Id,
+ Prompt = task.Prompt,
+ ReferenceImagesBase64 = task.ReferenceImagesBase64,
+ ReferenceImagesUrl = task.ReferenceImagesUrl,
+ StoreBase64 = task.StoreBase64,
+ StoreUrl = task.StoreUrl,
+ TaskStatus = task.TaskStatus,
+ CreationTime = task.CreationTime
+ };
+ }
+
+ ///
+ /// 上传Base64图片转换为URL
+ ///
+ /// Base64图片数据(包含前缀如 data:image/png;base64,)
+ /// 图片访问URL
+ [HttpPost("ai-image/upload-base64")]
+ public async Task UploadBase64ToUrlAsync([FromBody] string base64Data)
+ {
+ if (string.IsNullOrWhiteSpace(base64Data))
+ {
+ throw new UserFriendlyException("Base64数据不能为空");
+ }
+
+ // 解析Base64数据
+ string mimeType = "image/png";
+ string base64Content = base64Data;
+
+ if (base64Data.Contains(","))
+ {
+ var parts = base64Data.Split(',');
+ if (parts.Length == 2)
+ {
+ // 提取MIME类型
+ var header = parts[0];
+ if (header.Contains(":") && header.Contains(";"))
+ {
+ mimeType = header.Split(':')[1].Split(';')[0];
+ }
+ base64Content = parts[1];
+ }
+ }
+
+ // 获取文件扩展名
+ var extension = mimeType switch
+ {
+ "image/png" => ".png",
+ "image/jpeg" => ".jpg",
+ "image/jpg" => ".jpg",
+ "image/gif" => ".gif",
+ "image/webp" => ".webp",
+ _ => ".png"
+ };
+
+ // 解码Base64
+ byte[] imageBytes;
+ try
+ {
+ imageBytes = Convert.FromBase64String(base64Content);
+ }
+ catch (FormatException)
+ {
+ throw new UserFriendlyException("Base64格式无效");
+ }
+
+ // 创建存储目录
+ var uploadPath = Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot", "ai-images");
+ if (!Directory.Exists(uploadPath))
+ {
+ Directory.CreateDirectory(uploadPath);
+ }
+
+ // 生成文件名并保存
+ var fileId = _guidGenerator.Create();
+ var fileName = $"{fileId}{extension}";
+ var filePath = Path.Combine(uploadPath, fileName);
+
+ await File.WriteAllBytesAsync(filePath, imageBytes);
+
+ // 返回访问URL
+ return $"/ai-images/{fileName}";
+ }
+
+ ///
+ /// 分页查询任务列表
+ ///
+ /// 分页查询参数
+ /// 任务列表
+ [HttpGet("ai-image/tasks")]
+ public async Task> GetTaskPageAsync([FromQuery] ImageTaskPageInput input)
+ {
+ var userId = CurrentUser.GetId();
+
+ var query = _imageTaskRepository._DbQueryable
+ .Where(x => x.UserId == userId)
+ .WhereIF(input.TaskStatus.HasValue, x => x.TaskStatus == input.TaskStatus!.Value)
+ .OrderByDescending(x => x.CreationTime);
+
+ var total = await query.CountAsync();
+ var items = await query
+ .Skip((input.PageIndex - 1) * input.PageSize)
+ .Take(input.PageSize)
+ .Select(x => new ImageTaskOutput
+ {
+ Id = x.Id,
+ Prompt = x.Prompt,
+ ReferenceImagesBase64 = x.ReferenceImagesBase64,
+ ReferenceImagesUrl = x.ReferenceImagesUrl,
+ StoreBase64 = x.StoreBase64,
+ StoreUrl = x.StoreUrl,
+ TaskStatus = x.TaskStatus,
+ CreationTime = x.CreationTime
+ })
+ .ToListAsync();
+
+ return new PagedResult(total, items);
+ }
+}
+
+///
+/// 分页结果
+///
+/// 数据类型
+public class PagedResult
+{
+ ///
+ /// 总数
+ ///
+ public long Total { get; set; }
+
+ ///
+ /// 数据列表
+ ///
+ public List Items { get; set; }
+
+ public PagedResult(long total, List items)
+ {
+ Total = total;
+ Items = items;
+ }
+}
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Yi.Framework.AiHub.Application.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Yi.Framework.AiHub.Application.csproj
index 30caa193..dd8b3b65 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Yi.Framework.AiHub.Application.csproj
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Yi.Framework.AiHub.Application.csproj
@@ -2,6 +2,10 @@
+
+
+
+
diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
index 1c30bb17..4bbe1d30 100644
--- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
+++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs
@@ -966,9 +966,14 @@ public class AiGateWayManager : DomainService
//解析json,获取base64字符串
var imageBase64 = GeminiGenerateContentAcquirer.GetImageBase64(data);
- //base64字符串存储,返回绝对路径,用于最后存储
- var storeUrl = Base64ToPng(imageBase64, "存储的路径?这个放什么");
-
+ //远程调用上传接口,将base64转换为URL
+ var httpClient = LazyServiceProvider.LazyGetRequiredService().CreateClient();
+ var uploadUrl = $"https://ccnetcore.com/prod-api/ai-hub/ai-image/upload-base64";
+ var content = new StringContent(JsonSerializer.Serialize(imageBase64), Encoding.UTF8, "application/json");
+ var uploadResponse = await httpClient.PostAsync(uploadUrl, content, cancellationToken);
+ uploadResponse.EnsureSuccessStatusCode();
+ var storeUrl = await uploadResponse.Content.ReadAsStringAsync(cancellationToken);
+ storeUrl = storeUrl.Trim('"'); // 移除JSON字符串的引号
var tokenUsage = new ThorUsageResponse
{
@@ -994,42 +999,12 @@ public class AiGateWayManager : DomainService
await PremiumPackageManager.TryConsumeTokensAsync(userId, totalTokens);
}
- //设置存储url
+ //设置存储base64和url
+ imageStoreTask.StoreBase64 = imageBase64;
imageStoreTask.SetSuccess(storeUrl);
await _imageStoreTaskRepository.UpdateAsync(imageStoreTask);
}
- ///
- /// 将 Base64 字符串转换为 PNG 图片并保存
- ///
- /// Base64 字符串(可以包含或不包含 data:image/png;base64, 前缀)
- /// 输出文件路径
- private string Base64ToPng(string base64String, string outputPath)
- {
- try
- {
- // 移除可能存在的 data URI scheme 前缀
- if (base64String.Contains(","))
- {
- base64String = base64String.Substring(base64String.IndexOf(",") + 1);
- }
-
- // 将 base64 字符串转换为字节数组
- byte[] imageBytes = Convert.FromBase64String(base64String);
-
- // 将字节数组写入文件
- File.WriteAllBytes(outputPath, imageBytes);
- }
- catch (Exception ex)
- {
- throw new UserFriendlyException("gemini Base64转图像失败", innerException: ex);
- }
-
- //todo
- //路径拼接一下?
- return outputPath;
- }
-
#region 流式传输格式Http响应
private static readonly byte[] EventPrefix = "event: "u8.ToArray();