From 3b74dfd49ab0dfc1e01e91388c7f64bbef30e777 Mon Sep 17 00:00:00 2001 From: ccnetcore Date: Sat, 21 Jun 2025 01:08:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90ai=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Options/AiGateWayOptions.cs | 17 ++ ...rameworkAiHubApplicationContractsModule.cs | 11 +- .../Services/AiService.cs | 155 ++++++++---------- .../AiChat/IChatService.cs | 8 + .../AiChat/Impl/AzureChatService.cs | 40 +++++ .../Managers/AiGateWayManager.cs | 33 ++++ .../Managers/OpenAiManager.cs | 39 ----- .../Yi.Framework.AiHub.Domain.csproj | 1 + .../YiFrameworkAiHubDomainModule.cs | 13 +- Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json | 92 ++++++----- 10 files changed, 236 insertions(+), 173 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Options/AiGateWayOptions.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs create mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs delete mode 100644 Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/OpenAiManager.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Options/AiGateWayOptions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Options/AiGateWayOptions.cs new file mode 100644 index 00000000..914b8a6a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Options/AiGateWayOptions.cs @@ -0,0 +1,17 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Options; + +public class AiGateWayOptions +{ + public AiChatOptionDic Chats { get; set; } +} + +public class AiChatOptionDic : Dictionary +{ +} + +public class AiChatModelOptions +{ + public List ModelIds { get; set; } + public string Endpoint { get; set; } + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/YiFrameworkAiHubApplicationContractsModule.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/YiFrameworkAiHubApplicationContractsModule.cs index 562cb258..8f546800 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/YiFrameworkAiHubApplicationContractsModule.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/YiFrameworkAiHubApplicationContractsModule.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Yi.Framework.AiHub.Application.Contracts.Options; using Yi.Framework.AiHub.Domain.Shared; using Yi.Framework.Ddd.Application.Contracts; @@ -5,11 +7,12 @@ namespace Yi.Framework.AiHub.Application.Contracts { [DependsOn( typeof(YiFrameworkAiHubDomainSharedModule), - - typeof(YiFrameworkDddApplicationContractsModule))] - public class YiFrameworkAiHubApplicationContractsModule:AbpModule + public class YiFrameworkAiHubApplicationContractsModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + var build = context.Services.GetConfiguration(); + } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiService.cs index b6d17b61..8661bf4f 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiService.cs @@ -1,19 +1,28 @@ -using Microsoft.AspNetCore.Http; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using OpenAI.Chat; using Volo.Abp.Application.Services; using Yi.Framework.AiHub.Application.Contracts.Dtos; +using Yi.Framework.AiHub.Application.Contracts.Options; +using Yi.Framework.AiHub.Domain.Managers; namespace Yi.Framework.AiHub.Application.Services; public class AiService : ApplicationService { - // private readonly SemanticKernelClient _skClient; - private IHttpContextAccessor httpContextAccessor; + private readonly AiGateWayOptions _options; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AiService(IOptions options, IHttpContextAccessor httpContextAccessor) + { + _options = options.Value; + this._httpContextAccessor = httpContextAccessor; + } - // public AiService(SemanticKernelClient skClient, IHttpContextAccessor httpContextAccessor) - // { - // _skClient = skClient; - // this.httpContextAccessor = httpContextAccessor; - // } /// /// 获取模型列表 @@ -21,96 +30,68 @@ public class AiService : ApplicationService /// public async Task> GetModelAsync() { - return new List() - { - new ModelGetListOutput + var output = _options.Chats.SelectMany(x => x.Value.ModelIds) + .Select(x => new ModelGetListOutput() { Id = 001, Category = "chat", - ModelName = "gpt-4.1-mini", - ModelDescribe = "gpt下的ai", + ModelName = x, + ModelDescribe = "这是一个直连模型", ModelPrice = 4, ModelType = "1", ModelShow = "0", - SystemPrompt = "", - ApiHost = "", - ApiKey = "", - Remark = "牛逼" - }, - new ModelGetListOutput - { - Id = 002, - Category = "chat", - ModelName = "grok-3-mini", - ModelDescribe = "马斯克的ai", - ModelPrice = 5, - ModelType = "1", - ModelShow = "0", - SystemPrompt = "", - ApiHost = "", - ApiKey = "", - Remark = "牛逼啊" - } - }; + Remark = "直连模型" + }).ToList(); + return output; } - // /// - // /// 发送消息 - // /// - // /// - // /// - // public async Task PostSendAsync(SendMessageInput input,CancellationToken cancelToken) - // { - // var httpContext = this.httpContextAccessor.HttpContext; - // var response = httpContext.Response; - // // 设置响应头,声明是 SSE 流 - // response.ContentType = "text/event-stream"; - // response.Headers.Append("Cache-Control", "no-cache"); - // response.Headers.Append("Connection", "keep-alive"); - // - // - // var chatCompletionService = this._skClient.Kernel.GetRequiredService(input.Model); - // var history = new ChatHistory(); - // var openSettings = new AzureOpenAIPromptExecutionSettings() - // { - // MaxTokens = 3000 - // }; - // foreach (var aiChatContextDto in input.Messages) - // { - // if (aiChatContextDto.Role == "ai") - // { - // history.AddAssistantMessage(aiChatContextDto.Content); - // } - // else if (aiChatContextDto.Role == "user") - // { - // history.AddUserMessage(aiChatContextDto.Content); - // } - // } - // - // var results = chatCompletionService.GetStreamingChatMessageContentsAsync( - // chatHistory: history, - // executionSettings: openSettings, - // kernel: _skClient.Kernel, - // cancelToken); - // - // - // await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true); - // await foreach (var result in results) - // { - // var modle = GetMessage(input.Model, result.Content); - // var message = JsonConvert.SerializeObject(modle, new JsonSerializerSettings - // { - // ContractResolver = new CamelCasePropertyNamesContractResolver() - // }); - // - // await writer.WriteLineAsync($"data: {message}\n"); - // await writer.FlushAsync(cancelToken); // 确保立即推送数据 - // } - // } + /// + /// 发送消息 + /// + /// + /// + public async Task PostSendAsync(SendMessageInput input, CancellationToken cancelToken) + { + var httpContext = this._httpContextAccessor.HttpContext; + var response = httpContext.Response; + // 设置响应头,声明是 SSE 流 + response.ContentType = "text/event-stream"; + response.Headers.Append("Cache-Control", "no-cache"); + response.Headers.Append("Connection", "keep-alive"); - private SendMessageOutputDto GetMessage(string modelId, string content) + var history = new List(); + foreach (var aiChatContextDto in input.Messages) + { + if (aiChatContextDto.Role == "ai") + { + history.Add(ChatMessage.CreateAssistantMessage(aiChatContextDto.Content)); + } + else if (aiChatContextDto.Role == "user") + { + history.Add(ChatMessage.CreateUserMessage(aiChatContextDto.Content)); + } + } + + var gateWay = LazyServiceProvider.GetRequiredService(); + var completeChatResponse = gateWay.CompleteChatAsync(input.Model, history); + await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true); + await foreach (var data in completeChatResponse) + { + var model = MapToMessage(input.Model, data); + var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + await writer.WriteLineAsync($"data: {message}\n"); + await writer.FlushAsync(cancelToken); // 确保立即推送数据 + } + } + + + private SendMessageOutputDto MapToMessage(string modelId, string content) { var output = new SendMessageOutputDto { diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs new file mode 100644 index 00000000..04bfc081 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/IChatService.cs @@ -0,0 +1,8 @@ +using OpenAI.Chat; + +namespace Yi.Framework.AiHub.Domain.AiChat; + +public interface IChatService +{ + public IAsyncEnumerable CompleteChatAsync(string modelId, List messages); +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs new file mode 100644 index 00000000..8ce00bab --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/AiChat/Impl/AzureChatService.cs @@ -0,0 +1,40 @@ +using Azure; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Options; +using OpenAI.Chat; +using Yi.Framework.AiHub.Application.Contracts.Options; + +namespace Yi.Framework.AiHub.Domain.AiChat.Impl; + +public class AzureChatService : IChatService +{ + private readonly AiChatModelOptions _options; + + public AzureChatService(IOptions options) + { + this._options = options.Value.Chats[nameof(AzureChatService)]; + } + + public async IAsyncEnumerable CompleteChatAsync(string modelId, List messages) + { + var endpoint = new Uri(_options.Endpoint); + + var deploymentName = modelId; + var apiKey = _options.ApiKey; + + AzureOpenAIClient azureClient = new( + endpoint, + new AzureKeyCredential(apiKey)); + ChatClient chatClient = azureClient.GetChatClient(deploymentName); + + var response = chatClient.CompleteChatStreamingAsync(messages); + + await foreach (StreamingChatCompletionUpdate update in response) + { + foreach (ChatMessageContentPart updatePart in update.ContentUpdate) + { + yield return updatePart.Text; + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..36ce17d9 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -0,0 +1,33 @@ +using Azure; +using Azure.AI.OpenAI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenAI.Chat; +using Volo.Abp.Domain.Services; +using Yi.Framework.AiHub.Application.Contracts.Options; +using Yi.Framework.AiHub.Domain.AiChat; + +namespace Yi.Framework.AiHub.Domain.Managers; + +public class AiGateWayManager : DomainService +{ + private readonly AiGateWayOptions _options; + + public AiGateWayManager(IOptions options) + { + this._options = options.Value; + } + + public IAsyncEnumerable CompleteChatAsync(string modelId, List messages) + { + foreach (var chat in _options.Chats) + { + if (chat.Value.ModelIds.Contains(modelId)) + { + var chatService = LazyServiceProvider.GetRequiredKeyedService(chat.Key); + return chatService.CompleteChatAsync(modelId, messages); + } + } + throw new UserFriendlyException($"当前暂不支持该模型-【{modelId}】"); + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/OpenAiManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/OpenAiManager.cs deleted file mode 100644 index fbf6642b..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/OpenAiManager.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Azure; -using Azure.AI.OpenAI; -using OpenAI.Chat; -using Volo.Abp.Domain.Services; - -namespace Yi.Framework.AiHub.Domain.Managers; - -public class OpenAiManager : DomainService -{ - public static async Task TestAsync() - { - var endpoint = new Uri("https://japan-ccnetcore-resource.cognitiveservices.azure.com/"); - // var deploymentName = "gpt-4.1-mini"; - var deploymentName = "o4-mini"; - var apiKey = "FaccnRh7Zvz25OCGH07kHPe2z1aCXMliLdr3esgWHgXQ2aivwFgDJQQJ99BFACi0881XJ3w3AAAAACOGAJ2G"; - - AzureOpenAIClient azureClient = new( - endpoint, - new AzureKeyCredential(apiKey)); - ChatClient chatClient = azureClient.GetChatClient(deploymentName); - - List messages = new List() - { - new UserChatMessage("使用c#写一个贪吃蛇代码"), - }; - - var response = chatClient.CompleteChatStreamingAsync(messages); - - await foreach (StreamingChatCompletionUpdate update in response) - { - foreach (ChatMessageContentPart updatePart in update.ContentUpdate) - { - System.Console.Write(updatePart.Text); - } - } - - System.Console.WriteLine("结束"); - } -} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj index 87f561bf..dcca5457 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Yi.Framework.AiHub.Domain.csproj @@ -9,6 +9,7 @@ + diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs index f682354f..b5a60c2a 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/YiFrameworkAiHubDomainModule.cs @@ -1,6 +1,11 @@ using Microsoft.Extensions.DependencyInjection; +using OpenAI.Chat; using Volo.Abp.Caching; using Volo.Abp.Domain; +using Yi.Framework.AiHub.Application.Contracts.Options; +using Yi.Framework.AiHub.Domain.AiChat; +using Yi.Framework.AiHub.Domain.AiChat.Impl; +using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.AiHub.Domain.Shared; using Yi.Framework.Mapster; @@ -17,11 +22,17 @@ namespace Yi.Framework.AiHub.Domain { var configuration = context.Services.GetConfiguration(); var services = context.Services; + + Configure(configuration.GetSection("AiGateWay")); + + + services.AddKeyedTransient(nameof(AzureChatService)); } public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { - await Yi.Framework.AiHub.Domain.Managers.OpenAiManager.Test2Async(); + var service = context.ServiceProvider; + } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index 1b1eee0e..fcb4516b 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -1,16 +1,16 @@ { //多租户,支持多库,DbConnOptions会自动创建到默认租户,支持配置文件方式+数据库方式,AbpDefaultTenantStoreOptions -// "Tenants": [ -// { -// "Id": "33333333-3d72-4339-9adc-845151f8ada0", -// "Name": "Mes@MySql", -// "ConnectionStrings": { -// "Default": "DataSource=mes-dev.db" -// }, -// "IsActive": false -// } -// ], - + // "Tenants": [ + // { + // "Id": "33333333-3d72-4339-9adc-845151f8ada0", + // "Name": "Mes@MySql", + // "ConnectionStrings": { + // "Default": "DataSource=mes-dev.db" + // }, + // "IsActive": false + // } + // ], + "Logging": { "LogLevel": { //"Default": "Information", @@ -27,36 +27,39 @@ "Settings": { "Test": "hello" }, - //数据库类型列表 - "DbList": [ "Sqlite", "Mysql", "Sqlserver", "Oracle", "PostgreSQL" ], - - "DbConnOptions": { - "Url": "DataSource=yi-abp-dev.db", - "DbType": "Sqlite", - "EnabledReadWrite": false, - "EnabledCodeFirst": true, - "EnabledSqlLog": true, - "EnabledDbSeed": true, - "EnableUnderLine": false, // 启用驼峰转下划线 - //SAAS多租户 - "EnabledSaasMultiTenancy": true - //读写分离地址 - //"ReadUrl": [ - // "DataSource=[xxxx]", //Sqlite - // "server=[xxxx];port=3306;database=[xxxx];user id=[xxxx];password=[xxxx]", //Mysql - // "Data Source=[xxxx];Initial Catalog=[xxxx];User ID=[xxxx];password=[xxxx]" //Sqlserver - // "HOST=[xxxx];PORT=5432;DATABASE=[xxxx];USERID=[xxxx];PASSWORD=[xxxx]" //PostgreSQL - //] - }, - + "DbList": [ + "Sqlite", + "Mysql", + "Sqlserver", + "Oracle", + "PostgreSQL" + ], + "DbConnOptions": { + "Url": "DataSource=yi-abp-dev.db", + "DbType": "Sqlite", + "EnabledReadWrite": false, + "EnabledCodeFirst": true, + "EnabledSqlLog": true, + "EnabledDbSeed": true, + "EnableUnderLine": false, + // 启用驼峰转下划线 + //SAAS多租户 + "EnabledSaasMultiTenancy": true + //读写分离地址 + //"ReadUrl": [ + // "DataSource=[xxxx]", //Sqlite + // "server=[xxxx];port=3306;database=[xxxx];user id=[xxxx];password=[xxxx]", //Mysql + // "Data Source=[xxxx];Initial Catalog=[xxxx];User ID=[xxxx];password=[xxxx]" //Sqlserver + // "HOST=[xxxx];PORT=5432;DATABASE=[xxxx];USERID=[xxxx];PASSWORD=[xxxx]" //PostgreSQL + //] + }, //redis使用freeesql参数在“FreeSqlOptions的ConnectionStringBuilder中” "Redis": { "IsEnabled": false, "Configuration": "127.0.0.1:6379,password=123,defaultDatabase=13", "JobDb": 13 }, - //鉴权 "JwtOptions": { "Issuer": "https://ccnetcore.com", @@ -71,8 +74,6 @@ "SecurityKey": "67ij4o6jo4i5j6io45j6i4j74p5k6i54ojoi5t9g8ergoj34ofgkrtbmreog894jbioemgropihj48rj4io5juopjgior", "ExpiresMinuteTime": 172800 }, - - //第三方登录 "OAuth": { //QQ @@ -88,26 +89,33 @@ "RedirectUri": "" } }, - //Rbac模块 "RbacOptions": { //超级管理员种子数据默认密码 "AdminPassword": "123456", - //是否开启验证码验证 "EnableCaptcha": true, - //是否开启注册功能 "EnableRegister": false, - //开启定时数据库备份 "EnableDataBaseBackup": false }, - //语义内核 "SemanticKernel": { - "ModelIds": ["gpt-4o"], + "ModelIds": [ + "gpt-4o" + ], "Endpoint": "https://xxx.com/v1", "ApiKey": "sk-xxxxxx" + }, + //AI网关 + "AiGateWay": { + "Chats": { + "AzureChatService": { + "ModelIds": ["gpt-4o"], + "Endpoint": "https://xxx.com/v1", + "ApiKey": "sk-xxxxxx" + } + } } }