From 82865631fc14751db2223f3b720b5cbfa84f8202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com> Date: Sat, 8 Mar 2025 22:14:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ai=E5=AE=8C=E6=88=90stock=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StockMarket/CreateStockMarketInputDto.cs | 23 + .../Dtos/StockPrice/StockPriceRecordDto.cs | 5 + .../StockTransactionGetListInputDto.cs | 4 +- .../IServices/IStockMarketService.cs | 6 + ...amework.Stock.Application.Contracts.csproj | 1 - .../Services/StockMarketService.cs | 30 + .../Services/StockNewsService.cs | 19 +- .../Entities/StockMarketAggregateRoot.cs | 4 +- .../Entities/StockPriceRecordEntity.cs | 10 +- .../Managers/NewsManager.cs | 15 +- .../SemanticKernel/Plugins/NewsPlugins.cs | 29 +- .../SemanticKernel/Plugins/StockPlugins.cs | 28 +- .../SemanticKernel/SemanticKernelClient.cs | 64 +- .../SemanticKernel/SemanticKernelOptions.cs | 36 +- .../Managers/StockMarketManager.cs | 94 ++- .../YiFrameworkStockDomainModule.cs | 39 +- .../Managers/AiManager.cs | 107 ++-- .../Yi.Framework.ChatHub.Domain.csproj | 1 - .../Services/TestService.cs | 1 - .../Jobs/ai-stock/GenerateNewsJob.cs | 34 ++ Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs | 10 + Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json | 8 + Yi.Bbs.Vue3/src/apis/stockApi.js | 49 ++ .../src/components/StockBoard/StockBoard.vue | 147 +++++ Yi.Bbs.Vue3/src/views/stock/Index.vue | 558 ++++++++++++++---- .../src/views/stock/components/StockChart.vue | 16 + 26 files changed, 1044 insertions(+), 294 deletions(-) create mode 100644 Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/CreateStockMarketInputDto.cs create mode 100644 Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs create mode 100644 Yi.Bbs.Vue3/src/apis/stockApi.js create mode 100644 Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/CreateStockMarketInputDto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/CreateStockMarketInputDto.cs new file mode 100644 index 00000000..c0320597 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockMarket/CreateStockMarketInputDto.cs @@ -0,0 +1,23 @@ +using System; + +namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockMarket; +/// +/// 创建股市输入DTO +/// +public class CreateStockMarketInputDto +{ + /// + /// 股市代码 + /// + public string MarketCode { get; set; } + + /// + /// 股市名称 + /// + public string MarketName { get; set; } + + /// + /// 股市描述 + /// + public string Description { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockPrice/StockPriceRecordDto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockPrice/StockPriceRecordDto.cs index 8c10c801..8b7b5574 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockPrice/StockPriceRecordDto.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockPrice/StockPriceRecordDto.cs @@ -38,5 +38,10 @@ namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockPrice /// 时间周期类型 /// public PeriodTypeEnum PeriodType { get; set; } + + /// + /// 记录时间 + /// + public DateTime RecordTime { get; set; } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockTransaction/StockTransactionGetListInputDto.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockTransaction/StockTransactionGetListInputDto.cs index f8464c45..9c00eb73 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockTransaction/StockTransactionGetListInputDto.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Dtos/StockTransaction/StockTransactionGetListInputDto.cs @@ -12,12 +12,12 @@ namespace Yi.Framework.Stock.Application.Contracts.Dtos.StockTransaction /// /// 股票代码 /// - public string StockCode { get; set; } + public string? StockCode { get; set; } /// /// 股票名称 /// - public string StockName { get; set; } + public string? StockName { get; set; } /// /// 交易类型 diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs index b788c998..1a7fc6df 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/IServices/IStockMarketService.cs @@ -39,5 +39,11 @@ namespace Yi.Framework.Stock.Application.Contracts.IServices /// 卖出股票参数 /// 操作结果 Task SellStockAsync(SellStockInputDto input); + + /// + /// 生成最新股票记录 + /// + /// 操作结果 + Task GenerateStocksAsync(); } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Yi.Framework.Stock.Application.Contracts.csproj b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Yi.Framework.Stock.Application.Contracts.csproj index 9cc9ecc4..ae605feb 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Yi.Framework.Stock.Application.Contracts.csproj +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application.Contracts/Yi.Framework.Stock.Application.Contracts.csproj @@ -9,7 +9,6 @@ - diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs index 8d69ecc2..d2df8d3e 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockMarketService.cs @@ -12,6 +12,7 @@ using Yi.Framework.Stock.Application.Contracts.IServices; using Yi.Framework.Stock.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.Stock.Domain.Managers; +using Mapster; namespace Yi.Framework.Stock.Application.Services { @@ -34,6 +35,23 @@ namespace Yi.Framework.Stock.Application.Services _stockMarketManager = stockMarketManager; } + /// + /// 创建股市 + /// + [HttpPost("stock/markets")] + [Authorize] + public async Task CreateStockMarketAsync(CreateStockMarketInputDto input) + { + // 使用映射将输入DTO转换为实体 + var stockMarket = input.Adapt(); + + // 保存到数据库 + var result = await _stockMarketRepository.InsertReturnEntityAsync(stockMarket); + + // 使用映射将实体转换为返回DTO + return result.Adapt(); + } + /// /// 获取股市列表 /// @@ -87,6 +105,7 @@ namespace Yi.Framework.Stock.Application.Services Id = p.Id, StockId = p.StockId, CreationTime = p.CreationTime, + RecordTime = p.RecordTime, CurrentPrice = p.CurrentPrice, Volume = p.Volume, Turnover = p.Turnover, @@ -132,5 +151,16 @@ namespace Yi.Framework.Stock.Application.Services input.Quantity ); } + + /// + /// 生成最新股票记录 + /// + [HttpPost("stock/generate")] + [Authorize] + public async Task GenerateStocksAsync() + { + await _stockMarketManager.GenerateStocksAsync(); + } + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockNewsService.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockNewsService.cs index a5a92d9b..1dd4ced1 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockNewsService.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Application/Services/StockNewsService.cs @@ -8,6 +8,7 @@ using Yi.Framework.Stock.Application.Contracts.Dtos.StockNews; using Yi.Framework.Stock.Application.Contracts.IServices; using Yi.Framework.Stock.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; +using Yi.Framework.Stock.Domain.Managers; namespace Yi.Framework.Stock.Application.Services { @@ -17,16 +18,20 @@ namespace Yi.Framework.Stock.Application.Services public class StockNewsService : ApplicationService, IStockNewsService { private readonly ISqlSugarRepository _stockNewsRepository; + private readonly NewsManager _newsManager; - public StockNewsService(ISqlSugarRepository stockNewsRepository) + public StockNewsService( + ISqlSugarRepository stockNewsRepository, + NewsManager newsManager) { _stockNewsRepository = stockNewsRepository; + _newsManager = newsManager; } /// /// 获取股市新闻列表 /// - [HttpGet("/api/stock/news")] + [HttpGet("/api/app/stock/news")] public async Task> GetStockNewsListAsync(StockNewsGetListInputDto input) { RefAsync total = 0; @@ -61,5 +66,15 @@ namespace Yi.Framework.Stock.Application.Services return new PagedResultDto(total, list); } + + /// + /// 生成股市新闻 + /// + /// 生成结果 + [HttpPost("/api/app/stock/news/generate")] + public async Task GenerateNewsAsync() + { + await _newsManager.GenerateNewsAsync(); + } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockMarketAggregateRoot.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockMarketAggregateRoot.cs index ba9d2741..937ff00f 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockMarketAggregateRoot.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockMarketAggregateRoot.cs @@ -22,7 +22,7 @@ namespace Yi.Framework.Stock.Domain.Entities /// /// 创建时间 /// - public DateTime CreationTime { get; set; } = DateTime.Now; + public DateTime CreationTime { get; set; } /// /// 创建者 @@ -52,13 +52,11 @@ namespace Yi.Framework.Stock.Domain.Entities /// /// 股市代码 /// - /// 如:SH、SZ、HK等 public string MarketCode { get; set; } = string.Empty; /// /// 股市名称 /// - /// 如:上海证券交易所、深圳证券交易所等 public string MarketName { get; set; } = string.Empty; /// diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockPriceRecordEntity.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockPriceRecordEntity.cs index 26479c14..08dd09cc 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockPriceRecordEntity.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Entities/StockPriceRecordEntity.cs @@ -21,11 +21,16 @@ namespace Yi.Framework.Stock.Domain.Entities /// 关联到具体的股票 public Guid StockId { get; set; } + /// + /// 创建时间(审计日志) + /// + public DateTime CreationTime { get; set; } + /// /// 记录时间 /// - /// 价格记录的时间点 - public DateTime CreationTime { get; set; } + /// 价格记录的实际时间点 + public DateTime RecordTime { get; set; } /// /// 当前价 @@ -63,6 +68,7 @@ namespace Yi.Framework.Stock.Domain.Entities { StockId = stockId; CreationTime = DateTime.Now; + RecordTime = DateTime.Now; CurrentPrice = currentPrice; Volume = volume; Turnover = turnover; diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs index d170c220..dafc4c12 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/NewsManager.cs @@ -1,7 +1,9 @@ -using Yi.Framework.Stock.Domain.Managers.Plugins; -using Volo.Abp.Domain.Services; +using Volo.Abp.Domain.Services; using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.Stock.Domain.Entities; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins; + namespace Yi.Framework.Stock.Domain.Managers; public class NewsManager:DomainService @@ -19,9 +21,12 @@ public class NewsManager:DomainService /// /// public async Task GenerateNewsAsync() - { - _skClient.RegisterPlugins("news"); - await _skClient.ChatCompletionAsync("帮我生成一个新闻"); + { var question = """ + 生成并保存一个新闻,包含新闻标题、新闻内容、新闻简介、新闻来源 + 内容关于娱乐圈 + 只用生成一次即可 + """; + await _skClient.ChatCompletionAsync(question, ("NewsPlugins","save_news")); } public async Task SaveNewsAsync(NewsModel news) diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs index 42df958d..be6fecfb 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/NewsPlugins.cs @@ -1,23 +1,24 @@ using System.ComponentModel; using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; -using Yi.Framework.Stock.Domain.Managers; -namespace Yi.Framework.Stock.Domain.Managers.Plugins; +namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins; -public class NewsPlugins +public class NewsPlugins { - private readonly NewsManager _newsManager; + private readonly IServiceProvider _serviceProvider; - public NewsPlugins(NewsManager newsManager) + public NewsPlugins(IServiceProvider serviceProvider) { - _newsManager = newsManager; + _serviceProvider = serviceProvider; } - [KernelFunction("save_news"), Description("生成并且保存一个新闻")] + [KernelFunction("save_news"), Description("生成并保存一个新闻")] public async Task SaveAsync(NewsModel news) { - await _newsManager.SaveNewsAsync(news); + var newsManager = _serviceProvider.GetRequiredService(); + await newsManager.SaveNewsAsync(news); } } @@ -29,15 +30,13 @@ public class NewsModel [JsonPropertyName("content")] [DisplayName("新闻内容")] - public string? Content { get; set; } - - //新闻简介 + public string Content { get; set; } + [JsonPropertyName("summary")] [DisplayName("新闻简介")] - public string? Summary { get; set; } - - //新闻来源 + public string Summary { get; set; } + [JsonPropertyName("source")] [DisplayName("新闻来源")] - public string? Source { get; set; } + public string Source { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs index ee53a073..2cecdbf7 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/Plugins/StockPlugins.cs @@ -1,26 +1,34 @@ using System.ComponentModel; using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; -namespace Yi.Framework.Stock.Domain.Managers.Plugins; +namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins; public class StockPlugins { - [KernelFunction("save_stocks"), Description("生成并且保存多个股票记录")] - public async Task SaveAsync(List stockModels) + private readonly IServiceProvider _serviceProvider; + + public StockPlugins(IServiceProvider serviceProvider) { - return "成功"; + _serviceProvider = serviceProvider; + } + + [KernelFunction("save_stocks"), Description("生成并且保存多个股票记录")] + public async Task SaveAsync(List stockModels) + { + var stockMarketManager= _serviceProvider.GetRequiredService(); + await stockMarketManager.SaveStockAsync(stockModels); } } public class StockModel { [JsonPropertyName("id")] - public int Id { get; set; } + [DisplayName("股票id")] + public Guid Id { get; set; } - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("is_on")] - public bool? IsOn { get; set; } + [JsonPropertyName("values")] + [DisplayName("股票未来24小时价格")] + public decimal[] Values { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs index 62783d67..0443b2ef 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelClient.cs @@ -1,57 +1,18 @@ -using Microsoft.Extensions.Options; -using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Volo.Abp.DependencyInjection; -namespace Yi.Framework.Stock.Domain.Managers; +namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel; public class SemanticKernelClient:ITransientDependency { - private Kernel Kernel { get; } - private readonly IKernelBuilder _kernelBuilder; - private SemanticKernelOptions Options { get; } + public Kernel Kernel { get;} - public SemanticKernelClient(IOptions semanticKernelOption) + public SemanticKernelClient(Kernel kernel) { - Options = semanticKernelOption.Value; - _kernelBuilder = Kernel.CreateBuilder(); - RegisterChatCompletion(); - Kernel = _kernelBuilder.Build(); - RegisterDefautlPlugins(); + this.Kernel = kernel; } - - /// - /// 注册 - /// - private void RegisterChatCompletion() - { - _kernelBuilder.AddOpenAIChatCompletion( - modelId: Options.ModelId, - apiKey: Options.ApiKey, - httpClient: new HttpClient() { BaseAddress = new Uri(Options.Endpoint) }); - } - - /// - /// 插件注册 - /// - private void RegisterDefautlPlugins() - { - //动态导入插件 - // this.Kernel.ImportPluginFromPromptDirectory(System.IO.Path.Combine("wwwroot", "plugin","stock"),"stock"); - } - - /// - /// 自定义插件 - /// - /// - /// - public void RegisterPlugins(string pluginName) - { - this.Kernel.Plugins.AddFromType(pluginName); - } - - /// /// 执行插件 /// @@ -70,19 +31,24 @@ public class SemanticKernelClient:ITransientDependency /// 聊天对话,调用方法 /// /// - public async Task> ChatCompletionAsync(string question) + public async Task> ChatCompletionAsync(string question,params (string,string)[] functions) { - OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + if (functions is null) { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), - MaxTokens = Options.MaxTokens + throw new Exception("请选择插件"); + } + var openSettings = new OpenAIPromptExecutionSettings() + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions.Select(x=>this.Kernel.Plugins.GetFunction(x.Item1, x.Item2)).ToList(),true), + // ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, + MaxTokens =1000 }; var chatCompletionService = this.Kernel.GetRequiredService(); var results =await chatCompletionService.GetChatMessageContentsAsync( question, - executionSettings: openAIPromptExecutionSettings, + executionSettings: openSettings, kernel: Kernel); return results; } diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs index 16565ace..15ba372c 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/SemanticKernel/SemanticKernelOptions.cs @@ -1,29 +1,9 @@ -namespace Yi.Framework.Stock.Domain.Managers; - -public class SemanticKernelOptions +namespace Yi.Framework.Stock.Domain.Managers.SemanticKernel { - /// - /// OpenAI 模型 ID - /// - public string ModelId { get; set; } = string.Empty; - - /// - /// OpenAI API 密钥 - /// - public string ApiKey { get; set; } = string.Empty; - - /// - /// API 端点地址 - /// - public string Endpoint { get; set; } = string.Empty; - - /// - /// 最大生成令牌数 - /// - public int MaxTokens { get; set; } = 1000; - - /// - /// 插件目录路径 - /// - public string PluginsDirectoryPath { get; set; } = string.Empty; -} \ No newline at end of file + public class SemanticKernelOptions + { + public string ModelId { 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-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs index dbe00433..e9f086c8 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/Managers/StockMarketManager.cs @@ -8,7 +8,8 @@ using Yi.Framework.Stock.Domain.Entities; using Yi.Framework.Stock.Domain.Shared; using Yi.Framework.Stock.Domain.Shared.Etos; using Yi.Framework.SqlSugarCore.Abstractions; -using Yi.Framework.Stock.Domain.Managers.Plugins; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins; namespace Yi.Framework.Stock.Domain.Managers { @@ -277,27 +278,25 @@ namespace Yi.Framework.Stock.Domain.Managers /// /// 价格记录列表 /// 保存的记录数量 - public async Task BatchSaveStockPriceRecordsAsync(List priceRecords) + public async Task BatchSaveStockPriceRecordsAsync(List priceRecords) { if (priceRecords == null || !priceRecords.Any()) { - return 0; + return; } // 验证数据 - foreach (var record in priceRecords) + for (int i = 0; i < priceRecords.Count; i++) { + var record = priceRecords[i]; if (record.CurrentPrice <= 0) { throw new UserFriendlyException($"股票ID {record.StockId} 的价格必须大于0"); } - // 设置创建时间为当前时间(如果未设置) - if (record.CreationTime == default) - { - record.CreationTime = DateTime.Now; - } - + // 设置记录时间(当前时间加上i个小时),只记录到年月日小时 + record.RecordTime = new DateTime(DateTime.Now.AddHours(i).Year, DateTime.Now.AddHours(i).Month, DateTime.Now.AddHours(i).Day, DateTime.Now.AddHours(i).Hour, 0, 0); + // 计算交易额(如果未设置) if (record.Turnover == 0 && record.Volume > 0) { @@ -306,18 +305,85 @@ namespace Yi.Framework.Stock.Domain.Managers } await _stockPriceRecordRepository.InsertManyAsync(priceRecords); - return priceRecords.Count; } - + public async Task SaveStockAsync(List stockModels) + { + if (stockModels == null || !stockModels.Any()) + { + return; + } + + // 收集所有股票ID + var stockIds = stockModels.Select(m => m.Id).ToList(); + + // 一次性查询所有相关股票信息 + var stockMarkets = await _stockMarketRepository.GetListAsync(s => stockIds.Contains(s.Id)); + + // 构建字典以便快速查找 + var stockMarketsDict = stockMarkets.ToDictionary(s => s.Id); + + // 将StockModel转换为StockPriceRecordEntity + var priceRecords = new List(); + + foreach (var stockModel in stockModels) + { + if (stockModel.Values == null || !stockModel.Values.Any()) + { + continue; + } + + // 从字典中查找股票信息,而不是每次查询数据库 + if (!stockMarketsDict.TryGetValue(stockModel.Id, out var stockMarket)) + { + continue; + } + + // 为每个价格点创建一个记录 + foreach (var priceValue in stockModel.Values) + { + var priceRecord = new StockPriceRecordEntity + { + StockId = stockMarket.Id, + CurrentPrice = priceValue, + Volume = 0, // 可以根据实际情况设置 + Turnover = 0, // 可以根据实际情况设置 + PeriodType = PeriodTypeEnum.Hour + }; + + priceRecords.Add(priceRecord); + } + } + + // 批量保存价格记录 + if (priceRecords.Any()) + { + await BatchSaveStockPriceRecordsAsync(priceRecords); + } + } /// /// 生成最新股票记录 /// /// public async Task GenerateStocksAsync() { - _skClient.RegisterPlugins("stock"); - await _skClient.ChatCompletionAsync("帮我生成多个股市内容"); + var question = """ + 根据给出的模拟新闻,模拟生成多家股票未来价格 + + 以下是近期新闻: + ai势力逐步崛起 + 大量网红下岗 + 近期群星演唱会即将开幕 + + 以下是多个股票: + 伦敦股票:id:3a1886d5-1479-5402-3bcb-063e989898d1 最后一次价格:188.7 + 上海股票:id:3a1886d5-4393-606c-b040-52f1ef7d2158 最后一次价格:125.2 + 纽约股票:id:3a1886d5-6906-8f30-d955-198fbcfe4026 最后一次价格:185.5 + + 根据上面信息,给每个股票返回未来24小时的股票价格,每个小时,整点为单位一条记录,一家共有24条,返回股票id 与 长度为24的价格列表 + 只用生成一次即可 + """; + await _skClient.ChatCompletionAsync(question,("StockPlugins","save_stocks")); } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/YiFrameworkStockDomainModule.cs b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/YiFrameworkStockDomainModule.cs index fff803e3..79b60b3d 100644 --- a/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/YiFrameworkStockDomainModule.cs +++ b/Yi.Abp.Net8/module/ai-stock/Yi.Framework.Stock.Domain/YiFrameworkStockDomainModule.cs @@ -1,14 +1,18 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; using Volo.Abp.Caching; using Volo.Abp.Domain; -using Yi.Framework.Stock.Domain.Shared; using Yi.Framework.Mapster; using Yi.Framework.Stock.Domain.Managers; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel; +using Yi.Framework.Stock.Domain.Managers.SemanticKernel.Plugins; +using Yi.Framework.Stock.Domain.Shared; namespace Yi.Framework.Stock.Domain { [DependsOn( typeof(YiFrameworkStockDomainSharedModule), - typeof(YiFrameworkMapsterModule), typeof(AbpDddDomainModule), typeof(AbpCachingModule) @@ -17,13 +21,30 @@ namespace Yi.Framework.Stock.Domain { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure((options)=>{ - options.Endpoint = "https://api.token-ai.cn/v1"; - options.ApiKey = "sk-V6OqmrloXDAiTM2FWoisGgaop72Ngr0fXAnXL8"; - options.ModelId = "gpt-4o-mini"; - options.MaxTokens = 1000; - options.PluginsDirectoryPath = "plugins"; - }); + var configuration = context.Services.GetConfiguration(); + var services = context.Services; + + // 配置绑定 + var semanticKernelSection = configuration.GetSection("SemanticKernel"); + services.Configure(configuration.GetSection("SemanticKernel")); + + services.AddHttpClient(); +#pragma warning disable SKEXP0010 + // 从配置中获取值 + var options = semanticKernelSection.Get(); + services.AddKernel() + .AddOpenAIChatCompletion( + modelId: options.ModelId, + endpoint: new Uri(options.Endpoint), + apiKey: options.ApiKey); +#pragma warning restore SKEXP0010 + + // 添加插件 + services.AddSingleton(sp => KernelPluginFactory.CreateFromType(serviceProvider: sp)); + services.AddSingleton(sp => KernelPluginFactory.CreateFromType(serviceProvider: sp)); + + // 注册NewsManager + services.AddTransient(); } } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs index 7c64ea9b..44f3a2ca 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Managers/AiManager.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.Net; using Microsoft.Extensions.Options; -using OpenAI; -using OpenAI.Managers; -using OpenAI.ObjectModels; -using OpenAI.ObjectModels.RequestModels; -using OpenAI.ObjectModels.ResponseModels; +// using OpenAI; +// using OpenAI.Managers; +// using OpenAI.ObjectModels; +// using OpenAI.ObjectModels.RequestModels; +// using OpenAI.ObjectModels.ResponseModels; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Services; using Yi.Framework.ChatHub.Domain.Shared.Dtos; @@ -16,58 +16,59 @@ namespace Yi.Framework.ChatHub.Domain.Managers { public AiManager(IOptions options) { - this.OpenAIService = new OpenAIService(new OpenAiOptions() - { - ApiKey = options.Value.ApiKey, - BaseDomain = options.Value.BaseDomain - }); + // this.OpenAIService = new OpenAIService(new OpenAiOptions() + // { + // ApiKey = options.Value.ApiKey, + // BaseDomain = options.Value.BaseDomain + // }); } - private OpenAIService OpenAIService { get; } + // private OpenAIService OpenAIService { get; } public async IAsyncEnumerable ChatAsStreamAsync(string model, List aiChatContextDtos) { - if (aiChatContextDtos.Count == 0) - { - yield return null; - } - - List messages = aiChatContextDtos.Select(x => - { - if (x.AnswererType == AnswererTypeEnum.Ai) - { - return ChatMessage.FromSystem(x.Message); - } - else - { - return ChatMessage.FromUser(x.Message); - } - }).ToList(); - var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest - { - Messages = messages, - Model =model - }); - - HttpStatusCode? error = null; - await foreach (var result in completionResult) - { - if (result.Successful) - { - yield return result.Choices.FirstOrDefault()?.Message.Content ?? null; - } - else - { - error = result.HttpStatusCode; - break; - } - - } - if (error == HttpStatusCode.PaymentRequired) - { - yield return "余额不足,请联系站长充值"; - - } - + throw new NotImplementedException("准备sk重构"); + yield break; + // if (aiChatContextDtos.Count == 0) + // { + // yield return null; + // } + // + // List messages = aiChatContextDtos.Select(x => + // { + // if (x.AnswererType == AnswererTypeEnum.Ai) + // { + // return ChatMessage.FromSystem(x.Message); + // } + // else + // { + // return ChatMessage.FromUser(x.Message); + // } + // }).ToList(); + // var completionResult = OpenAIService.ChatCompletion.CreateCompletionAsStream(new ChatCompletionCreateRequest + // { + // Messages = messages, + // Model =model + // }); + // + // HttpStatusCode? error = null; + // await foreach (var result in completionResult) + // { + // if (result.Successful) + // { + // yield return result.Choices.FirstOrDefault()?.Message.Content ?? null; + // } + // else + // { + // error = result.HttpStatusCode; + // break; + // } + // + // } + // if (error == HttpStatusCode.PaymentRequired) + // { + // yield return "余额不足,请联系站长充值"; + // + // } } } } diff --git a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Yi.Framework.ChatHub.Domain.csproj b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Yi.Framework.ChatHub.Domain.csproj index 023d8888..de7bf76d 100644 --- a/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Yi.Framework.ChatHub.Domain.csproj +++ b/Yi.Abp.Net8/module/chat-hub/Yi.Framework.ChatHub.Domain/Yi.Framework.ChatHub.Domain.csproj @@ -7,7 +7,6 @@ - diff --git a/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs b/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs index 7b0a0833..11466146 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs @@ -30,7 +30,6 @@ namespace Yi.Abp.Application.Services public ISqlSugarRepository sqlSugarRepository { get; set; } /// - /// 动态Api /// /// /// diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs new file mode 100644 index 00000000..23e6e31e --- /dev/null +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/Jobs/ai-stock/GenerateNewsJob.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Yi.Framework.Stock.Domain.Managers; + +namespace Yi.Abp.Web.Jobs.ai_stock +{ + public class GenerateNewsJob : HangfireBackgroundWorkerBase + { + private NewsManager _newsManager; + + public GenerateNewsJob(NewsManager newsManager) + { + _newsManager = newsManager; + + RecurringJobId = "AI股票新闻生成"; + //每个小时整点执行一次 + CronExpression = "0 0 * * * ?"; + } + + public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // 每次触发只有2/24的概率执行生成新闻 + var random = new Random(); + var probability = random.Next(0, 24); + + if (probability < 2) + { + await _newsManager.GenerateNewsAsync(); + } + } + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index fdb307af..dd995436 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -27,6 +27,7 @@ using Volo.Abp.AspNetCore.VirtualFileSystem; using Volo.Abp.Auditing; using Volo.Abp.Autofac; using Volo.Abp.BackgroundJobs.Hangfire; +using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching; using Volo.Abp.MultiTenancy; using Volo.Abp.Swashbuckle; @@ -106,6 +107,15 @@ namespace Yi.Abp.Web var host = context.Services.GetHostingEnvironment(); var service = context.Services; + //本地开发环境,禁用作业执行 + if (host.IsDevelopment()) + { + Configure (options => + { + options.IsEnabled = false; + }); + } + //请求日志 Configure(options => { diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index d8ad0039..ea1edee4 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -108,5 +108,13 @@ "AiOptions": { "ApiKey": "", "BaseDomain": "" + }, + + + //语义内核 + "SemanticKernel": { + "ModelId": "gpt-4o", + "Endpoint": "https://xxx.com/v1", + "ApiKey": "sk-xxxxxx" } } diff --git a/Yi.Bbs.Vue3/src/apis/stockApi.js b/Yi.Bbs.Vue3/src/apis/stockApi.js new file mode 100644 index 00000000..3a9681fa --- /dev/null +++ b/Yi.Bbs.Vue3/src/apis/stockApi.js @@ -0,0 +1,49 @@ +import request from "@/config/axios/service"; + +// 获取股票新闻列表 +export function getStockNews(params) { + return request({ + url: "/stock/news", + method: "get", + params + }); +} + +// 获取用户股票持仓 +export function getUserHoldings() { + return request({ + url: "/stock/user-holdings", + method: "get" + }); +} + +// 获取用户交易记录 +export function getUserTransactions(stockCode) { + return request({ + url: "/stock/user-transactions", + method: "get", + params: { stockCode } + }); +} + +// 获取股票价格记录 +export function getStockPriceRecords(stockId, startTime, endTime, periodType = 'Hour') { + return request({ + url: "/stock/price-records", + method: "get", + params: { + StockId: stockId, + StartTime: startTime, + EndTime: endTime, + PeriodType: periodType + } + }); +} + +// 获取股市列表 +export function getStockMarkets() { + return request({ + url: "/stock/markets", + method: "get" + }); +} \ No newline at end of file diff --git a/Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue b/Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue new file mode 100644 index 00000000..0c928566 --- /dev/null +++ b/Yi.Bbs.Vue3/src/components/StockBoard/StockBoard.vue @@ -0,0 +1,147 @@ + + + + + \ No newline at end of file diff --git a/Yi.Bbs.Vue3/src/views/stock/Index.vue b/Yi.Bbs.Vue3/src/views/stock/Index.vue index 72c42f52..bf34afcc 100644 --- a/Yi.Bbs.Vue3/src/views/stock/Index.vue +++ b/Yi.Bbs.Vue3/src/views/stock/Index.vue @@ -1,18 +1,27 @@  @@ -275,8 +484,8 @@ onMounted(() => { display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 20px; - height: 52vh; - min-height: 320px; + height: 60vh; + min-height: 400px; } .news-section, .stock-panel, .trade-history { @@ -314,13 +523,15 @@ onMounted(() => { } .news-item { - padding: 10px; + padding: 12px; border-bottom: 1px solid #30363d; - transition: background-color 0.3s; + transition: all 0.2s; + cursor: pointer; } .news-item:hover { background-color: #1c2128; + transform: translateX(3px); } .news-date { @@ -331,21 +542,31 @@ onMounted(() => { .news-title { font-size: 0.9em; + line-height: 1.4; + margin-top: 5px; } .stock-info { text-align: center; - margin-bottom: 20px; + margin-bottom: 10px; + padding: 5px; +} + +.stock-info h3 { + margin: 0 0 5px 0; + font-size: 1.2em; } .price-info { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; + justify-content: center; + gap: 10px; } .current-price { - font-size: 2em; + font-size: 1.6em; font-weight: bold; } @@ -359,14 +580,25 @@ onMounted(() => { .stock-chart { flex: 1; - margin: 15px 0; + margin: 10px 0; + height: 320px; } .trade-actions { display: flex; justify-content: center; - gap: 10px; - margin-top: 15px; + align-items: center; + gap: 15px; + margin-top: 10px; +} + +.circle-button { + width: 60px; + height: 60px; + font-size: 14px; + display: flex; + justify-content: center; + align-items: center; } .history-list { @@ -375,18 +607,47 @@ onMounted(() => { } .history-item { + padding: 12px; + border-bottom: 1px solid #30363d; + display: flex; + flex-direction: column; +} + +.history-header { display: flex; justify-content: space-between; - padding: 10px; - border-bottom: 1px solid #30363d; + margin-bottom: 8px; +} + +.stock-name { + font-weight: bold; + color: #e6edf3; +} + +.time { + font-size: 0.8em; + color: #8b949e; +} + +.history-details { + display: flex; + align-items: center; + gap: 10px; } .buy-type { color: #3fb950; + font-weight: bold; } .sell-type { color: #f85149; + font-weight: bold; +} + +.total { + margin-left: auto; + font-weight: bold; } .portfolio { @@ -438,9 +699,108 @@ onMounted(() => { width: 120px; } +:deep(.el-input-number) { + width: 120px; +} + /* 调整表格最大高度 */ :deep(.el-table__body-wrapper) { overflow-y: auto; max-height: calc(35vh - 80px); } + +.news-source { + font-size: 0.75em; + color: #58a6ff; + margin-left: 5px; +} + +.loading-news, .empty-news { + padding: 15px; + text-align: center; + color: #8b949e; +} + +.news-more { + text-align: center; + padding: 10px 0; +} + +.loading-portfolio { + padding: 20px; + margin-top: 10px; +} + +.el-empty { + margin: 10px 0; +} + +.loading-history, .empty-history { + padding: 15px; + text-align: center; + color: #8b949e; +} + +.loading-chart { + height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + +.stock-code { + float: right; + color: #8b949e; + font-size: 0.8em; +} + +.selector-area :deep(.el-select) { + width: 200px; +} + +/* 修改骨架屏颜色,使其适合深色主题 */ +.stock-dashboard :deep(.el-skeleton__item) { + background-color: #30363d !important; +} + +.stock-dashboard :deep(.el-skeleton__paragraph) { + height: auto; +} + +.stock-dashboard :deep(.el-skeleton__p) { + background-color: #30363d !important; +} + +.stock-dashboard :deep(.el-skeleton__first-line), +.stock-dashboard :deep(.el-skeleton__paragraph > li) { + background-color: #30363d !important; + height: 16px !important; + margin-top: 12px !important; +} + +/* 骨架屏动画效果调整 */ +.stock-dashboard :deep(.is-animated .el-skeleton__item) { + background: linear-gradient(90deg, #30363d 25%, #383f47 37%, #30363d 63%) !important; + background-size: 400% 100% !important; +} + +.stock-chart .el-empty{ + margin: 0; + padding: 0; +} + +.stock-dashboard :deep(.el-empty) { + height: auto; + min-height: 60px; +} + +.stock-dashboard :deep(.el-empty__image) { + width: 100px; + height: 100px; +} + +.stock-dashboard :deep(.el-empty__description) { + margin-top: 5px; + font-size: 12px; +} diff --git a/Yi.Bbs.Vue3/src/views/stock/components/StockChart.vue b/Yi.Bbs.Vue3/src/views/stock/components/StockChart.vue index 70cc798e..55ca958c 100644 --- a/Yi.Bbs.Vue3/src/views/stock/components/StockChart.vue +++ b/Yi.Bbs.Vue3/src/views/stock/components/StockChart.vue @@ -149,5 +149,21 @@ onMounted(() => { .stock-chart-container { width: 100%; height: 100%; + min-height: 300px; +} + +:deep(.el-empty) { + padding: 20px 0; + height: auto; + max-height: 150px; +} + +:deep(.el-empty__image) { + width: 60px; + height: 60px; +} + +:deep(.el-empty__description) { + margin-top: 10px; } \ No newline at end of file