CrazyIter_Bin 9 kuukautta sitten
vanhempi
commit
bf0e859160

+ 1 - 1
TEAMModelBI/Controllers/BITest/TestController.cs

@@ -1565,7 +1565,7 @@ namespace TEAMModelBI.Controllers.BITest
                 string newurl = $"{blobItem.Name}".Replace($"doc/", $"/SourceFiles/");//替换成新的容器路径
                 string newurl = $"{blobItem.Name}".Replace($"doc/", $"/SourceFiles/");//替换成新的容器路径
                 var urlSas = _azureStorage.GetBlobSAS($"1636016499", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                 var urlSas = _azureStorage.GetBlobSAS($"1636016499", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                 //await azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas));
                 //await azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas));
-                await azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas));
+                await azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas.fullUri));
                 //await azureClient.GetBlobClient(newurl).SyncCopyFromUriAsync(new Uri(urlSas));  //添加复制文件到集合执行复制操作
                 //await azureClient.GetBlobClient(newurl).SyncCopyFromUriAsync(new Uri(urlSas));  //添加复制文件到集合执行复制操作
             }
             }
 
 

+ 1 - 1
TEAMModelBI/Controllers/Census/PaperController.cs

@@ -175,7 +175,7 @@ namespace TEAMModelBI.Controllers.Census
                             await foreach (BlobItem blobItem in oldBobCilent.GetBlobsAsync(BlobTraits.None, BlobStates.None, blobUrl))
                             await foreach (BlobItem blobItem in oldBobCilent.GetBlobsAsync(BlobTraits.None, BlobStates.None, blobUrl))
                             {
                             {
                                 var urlSas = _azureStorage.GetBlobSAS($"{cItem.oldSc}", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                                 var urlSas = _azureStorage.GetBlobSAS($"{cItem.oldSc}", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
-                                filelist.Add(newBobCilent.GetBlobClient(blobItem.Name).StartCopyFromUriAsync(new Uri(urlSas)));
+                                filelist.Add(newBobCilent.GetBlobClient(blobItem.Name).StartCopyFromUriAsync(new Uri(urlSas.fullUri)));
                                 ////序列化数据修改数据
                                 ////序列化数据修改数据
                                 //if (blobItem.Name.EndsWith(".json"))
                                 //if (blobItem.Name.EndsWith(".json"))
                                 //{
                                 //{

+ 101 - 0
TEAMModelOS.Extension/HTEX.Complex/Controllers/ScreenController.cs

@@ -0,0 +1,101 @@
+using Microsoft.AspNetCore.Mvc;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK;
+using Microsoft.AspNetCore.SignalR;
+using HTEX.Complex.Services;
+using System.Text.Json;
+using TEAMModelOS.SDK.Extension;
+
+namespace HTEX.Complex.Controllers
+{
+    [ApiController]
+    [Route("api/screen")]
+    public class ScreenController : ControllerBase
+    {
+        private readonly DingDing _dingDing;
+        private readonly IHttpClientFactory _httpClient;
+        private readonly IConfiguration _configuration;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly IHubContext<SignalRScreenServerHub> _screenServerHub;
+        private readonly ILogger<ScreenController> _logger;
+        public ScreenController(AzureRedisFactory azureRedis,  IHttpClientFactory httpClient, IConfiguration configuration, AzureStorageFactory azureStorage,
+            DingDing dingDing, IHubContext<SignalRScreenServerHub> screenServerHub , ILogger<ScreenController> logger)
+        {
+            _httpClient = httpClient;
+            _configuration = configuration;
+            _azureStorage = azureStorage;
+            _dingDing = dingDing;
+            _azureRedis=azureRedis;
+            _screenServerHub = screenServerHub;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// 外部触发任务发送
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("send-task")]
+        public async Task<IActionResult> SendTask(JsonElement json)
+        {
+            var values =  await _azureRedis.GetRedisClient(8).HashGetAllAsync("ScreenApi:clients");
+            if (values!=null) 
+            {
+                foreach (var value in values)
+                {
+                    ScreenClient screenClient = value.Value.ToString().ToObject<ScreenClient>();
+                    //先检查状态是否是在忙碌,在时间戳范围里,如果不在时间戳范围,强制free。
+                    if (!screenClient.status!.Equals(ScreenConstant.idle) && screenClient.last_time  + screenClient.timeout+ screenClient.delay + ScreenConstant.time_excess < DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
+                    {
+                        screenClient.status = ScreenConstant.idle;
+                    }
+                    if (screenClient.status!.Equals(ScreenConstant.idle) && !string.IsNullOrWhiteSpace(screenClient.connid))
+                    {
+                        _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},分发任务......");
+                        //连接成功,马上分发任务。
+                        var task = await GenPDFService.SentTask(_azureRedis, _azureStorage);
+                        if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
+                        {
+                            screenClient.status =  ScreenConstant.busy;
+                            screenClient.last_time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                            await _screenServerHub.Clients.Client(screenClient.connid!).SendAsync ("ReceiveMessage", new ScreenProcessMessage
+                            {
+                                connid = screenClient.connid,
+                                clientid =screenClient.clientid,
+                                status = ScreenConstant.busy,
+                                grant_type =screenClient.grant_type,
+                                message_type= MessageType.task_send_success,
+                                content =$"{task.genQueue.ToJsonString()}",//从Redis中获取任务信息
+                            });
+                        }
+                        else
+                        {
+                            _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
+                            if (task.genRedis!=null)
+                            {
+                                string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{task.msg}\ngenQueue:{task.genQueue?.ToJsonString()}\ngenRedis:{task.genRedis?.ToJsonString()}";
+                                _logger.LogInformation(msgError);
+                                await _dingDing.SendBotMsg(msgError, GroupNames.成都开发測試群組);
+                            }
+                            else
+                            {
+                                _logger.LogInformation($"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{task.msg}\n");
+                            }
+                            await _screenServerHub.Clients.Client(screenClient.connid!).SendAsync("ReceiveMessage", new ScreenProcessMessage
+                            {
+                                connid = screenClient.connid,
+                                clientid = screenClient.clientid,
+                                status = ScreenConstant.idle,
+                                grant_type = screenClient.grant_type,
+                                message_type= MessageType.task_send_error,
+                                content = task.msg
+                            });
+                        }
+                    }
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", screenClient.clientid, screenClient.ToJsonString());
+                }
+            }
+            return Ok();
+        }
+    }
+}

+ 147 - 23
TEAMModelOS.Extension/HTEX.Complex/Services/SignalRScreenServerHub.cs

@@ -9,6 +9,8 @@ using System.Web;
 using System.Text;
 using System.Text;
 
 
 using StackExchange.Redis;
 using StackExchange.Redis;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Sas;
 namespace HTEX.Complex.Services
 namespace HTEX.Complex.Services
 {
 {
     public class SignalRScreenServerHub : Hub<IClient>
     public class SignalRScreenServerHub : Hub<IClient>
@@ -16,10 +18,14 @@ namespace HTEX.Complex.Services
 
 
         private readonly ILogger<SignalRScreenServerHub> _logger;
         private readonly ILogger<SignalRScreenServerHub> _logger;
         private readonly AzureRedisFactory _azureRedis;
         private readonly AzureRedisFactory _azureRedis;
-        public SignalRScreenServerHub(AzureRedisFactory azureRedis, ILogger<SignalRScreenServerHub> logger)
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly DingDing _dingDing;
+        public SignalRScreenServerHub(AzureRedisFactory azureRedis, ILogger<SignalRScreenServerHub> logger ,AzureStorageFactory azureStorage, DingDing dingDing)
         {
         {
             _logger = logger;
             _logger = logger;
             _azureRedis = azureRedis;
             _azureRedis = azureRedis;
+            _azureStorage = azureStorage;
+            _dingDing = dingDing;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -61,19 +67,19 @@ namespace HTEX.Complex.Services
                                 screenClient = value.ToString().ToObject<ScreenClient>();
                                 screenClient = value.ToString().ToObject<ScreenClient>();
                                 
                                 
                                 // 这里不强制设置free ,因为如果是重连,可能正在执行任务,需要等待执行完成
                                 // 这里不强制设置free ,因为如果是重连,可能正在执行任务,需要等待执行完成
-                                //screenClient.status="free";
+                                
 
 
                                 //先检查状态是否是在忙碌,在时间戳范围里,如果不在时间戳范围,强制free。
                                 //先检查状态是否是在忙碌,在时间戳范围里,如果不在时间戳范围,强制free。
-                                if (!screenClient.status!.Equals(ScreenConstant.free) && screenClient.last_time  + screenClient.timeout+ screenClient.delay + ScreenConstant.time_excess < DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
+                                if (!screenClient.status!.Equals(ScreenConstant.idle) && screenClient.last_time  + screenClient.timeout+ screenClient.delay + ScreenConstant.time_excess < DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
                                 {
                                 {
-                                    screenClient.status = ScreenConstant.free;
+                                    screenClient.status = ScreenConstant.idle;
                                 }
                                 }
                             }
                             }
                             else 
                             else 
                             {
                             {
                                 screenClient = new ScreenClient
                                 screenClient = new ScreenClient
                                 {
                                 {
-                                    status = ScreenConstant.free,
+                                    status = ScreenConstant.idle,
                                 };
                                 };
                             }
                             }
                             screenClient.connid=connid;
                             screenClient.connid=connid;
@@ -90,7 +96,7 @@ namespace HTEX.Complex.Services
                             screenClient.timeout = device.timeout;
                             screenClient.timeout = device.timeout;
                             screenClient.last_time= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                             screenClient.last_time= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                             //连接成功,发送消息给客户端。
                             //连接成功,发送消息给客户端。
-                            await SendConnection(connid, new ConnectionMessageContent
+                            await SendConnection(connid, new ConnectionMessage
                             {
                             {
                                 connid=connid,
                                 connid=connid,
                                 clientid = clientid,
                                 clientid = clientid,
@@ -99,28 +105,45 @@ namespace HTEX.Complex.Services
                                 message_type= MessageType.conn_success,
                                 message_type= MessageType.conn_success,
                                 content = $"连接成功"
                                 content = $"连接成功"
                             });
                             });
-                            _logger.LogInformation($"客户端连接成功=>{screenClient.name},{clientid}:\n{screenClient.ToJsonString()}");
-                            if (screenClient.status!.Equals(ScreenConstant.free)) {
-                                _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{clientid},分发任务......");
+                            _logger.LogInformation($"客户端连接成功=>{screenClient.name},{screenClient.region},{clientid}:\n{screenClient.ToJsonString()}");
+                            if (screenClient.status!.Equals(ScreenConstant.idle)) {
+                                _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},分发任务......");
                                 //连接成功,马上分发任务。
                                 //连接成功,马上分发任务。
-                                //从尾部弹出元素,队列先进先出
-                                var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync("PDFGen:Queue");
-                                if (queueValue!=default && queueValue.HasValue)
+                                var task = await GenPDFService.SentTask(_azureRedis,_azureStorage);
+                                if (task.genQueue!=null && task.genRedis!=null  && !string.IsNullOrWhiteSpace(task.genQueue.cntName))
                                 {
                                 {
-                                    PDFGenQueue genQueue = queueValue.ToString().ToObject<PDFGenQueue>();
-                                    await SendMessage(connid, new ScreenProcessMessageContent
+                                    screenClient.status =  ScreenConstant.busy;
+                                    screenClient.last_time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                                    await SendMessage(connid, new ScreenProcessMessage
                                     {
                                     {
+                                        connid = connid,
                                         clientid = clientid,
                                         clientid = clientid,
                                         status = ScreenConstant.busy,
                                         status = ScreenConstant.busy,
                                         grant_type = grant_type,
                                         grant_type = grant_type,
-                                        content =$"{queueValue.ToString()}",//从Redis中获取任务信息
+                                        message_type= MessageType.task_send_success,
+                                        content =$"{task.genQueue.ToJsonString()}",//从Redis中获取任务信息
                                     });
                                     });
-                                    screenClient.status =  ScreenConstant.busy;
-                                    screenClient.last_time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                                 }
                                 }
                                 else {
                                 else {
-                                    _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{clientid},暂无任务可领取的任务......");
-                                  
+                                    _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{clientid},暂无任务可领取的任务......");
+                                    if (task.genRedis!=null)
+                                    {
+                                        string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{clientid}:{task.msg}\ngenQueue:{task.genQueue?.ToJsonString()}\ngenRedis:{task.genRedis?.ToJsonString()}";
+                                        _logger.LogInformation(msgError);
+                                        await _dingDing.SendBotMsg(msgError, GroupNames.成都开发測試群組);
+                                    }
+                                    else {
+                                        _logger.LogInformation($"分发任务异常原因=>{screenClient.name},{screenClient.region},{clientid}:{task.msg}\n");
+                                    }
+                                    await SendMessage(connid, new ScreenProcessMessage
+                                    {
+                                        connid = connid,
+                                        clientid = clientid,
+                                        status = ScreenConstant.idle,
+                                        grant_type = grant_type,
+                                        message_type= MessageType.task_send_error,
+                                        content = task.msg
+                                    });
                                 }
                                 }
                             }
                             }
                             await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
                             await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
@@ -129,7 +152,7 @@ namespace HTEX.Complex.Services
                 }
                 }
                 else
                 else
                 {
                 {
-                    await SendConnection(connid, new ConnectionMessageContent
+                    await SendConnection(connid, new ConnectionMessage
                     {
                     {
                         clientid = string.Empty,
                         clientid = string.Empty,
                         status =ScreenConstant.error,
                         status =ScreenConstant.error,
@@ -142,7 +165,7 @@ namespace HTEX.Complex.Services
             }
             }
         }
         }
 
 
-        public async override Task OnDisconnectedAsync(Exception? exception)
+        public override async  Task OnDisconnectedAsync(Exception? exception)
         {
         {
             var connid = Context.ConnectionId;
             var connid = Context.ConnectionId;
             var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects", connid);
             var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects", connid);
@@ -159,12 +182,12 @@ namespace HTEX.Complex.Services
                     if (value!=default  && value.HasValue) 
                     if (value!=default  && value.HasValue) 
                     {
                     {
                         ScreenClient screenClient = value.ToString().ToObject<ScreenClient>() ;
                         ScreenClient screenClient = value.ToString().ToObject<ScreenClient>() ;
-                        _logger.LogInformation($"客户端断开连接=>{connid},{screenClient.name},{screenClient.clientid} ");
+                        _logger.LogInformation($"客户端断开连接=>{connid},{screenClient.name},{screenClient.region},{screenClient.clientid} ");
                         long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                         long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                         // 判断是否过期
                         // 判断是否过期
                         if (screenClient.status!.Equals(ScreenConstant.busy )  &&    screenClient.last_time+screenClient.timeout+screenClient.delay+ ScreenConstant.time_excess <=now)
                         if (screenClient.status!.Equals(ScreenConstant.busy )  &&    screenClient.last_time+screenClient.timeout+screenClient.delay+ ScreenConstant.time_excess <=now)
                         {
                         {
-                            screenClient.status=ScreenConstant.down;
+                            screenClient.status=ScreenConstant.offline;
                             screenClient.connid= string.Empty;
                             screenClient.connid= string.Empty;
                             await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
                             await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid, screenClient.ToJsonString());
                         }
                         }
@@ -172,6 +195,107 @@ namespace HTEX.Complex.Services
                 }
                 }
             }
             }
         }
         }
+
+        public   async Task ReceiveMessage(ScreenProcessMessage message) 
+        {
+
+            ////接收消息
+            //如果是超时,放回队列。
+            ///分发新任务。
+            long nowNew = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            var connid = Context.ConnectionId;
+            PDFGenQueue? task =  message.content?.ToObject<PDFGenQueue>();
+            ScreenClient screenClient = null;
+            try
+            {  //释放客户端的忙碌状态。
+                var redisData = await _azureRedis.GetRedisClient(8).HashGetAsync($"SignalRClient:connects", connid);
+                var client = redisData.ToString().ToObject<SignalRClient>();
+                await SendConnection(connid, new ConnectionMessage
+                {
+                    connid=connid,
+                    clientid = client.clientid,
+                    status = ScreenConstant.idle,
+                    grant_type = client.grant_type,
+                    message_type= MessageType.conn_success,
+                    content = $"客户端空闲,等待任务分发......"
+                });
+                var value = await _azureRedis.GetRedisClient(8).HashGetAsync($"ScreenApi:clients", client.clientid);
+                if (value!=default  && value.HasValue)
+                {
+                    screenClient = value.ToString().ToObject<ScreenClient>();
+                    screenClient.status=ScreenConstant.idle;
+                    screenClient.last_time=nowNew;
+                    screenClient.taskComplete++;
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", client.clientid,screenClient.ToJsonString());
+                    _logger.LogInformation($"客户端空闲,等待任务分发......=>{connid},{screenClient.name},{screenClient.region},{screenClient.clientid} ");
+                }
+                
+            } 
+            catch (Exception ex)
+            {
+                _logger.LogError($"客户端状态重置异常,......=>{connid},{ex.Message},{ex.StackTrace}");
+            }
+
+            if (task!=null) 
+            {
+                RedisValue redisValue = await _azureRedis.GetRedisClient(8).HashGetAsync($"PDFGen:{task.sessionId}", task.id);
+                if (redisValue!=default) 
+                {
+                    var genRedis = redisValue.ToString().ToObject<PDFGenRedis>();
+                    genRedis.cost=nowNew-(genRedis.join+genRedis.wait);//拿到分发任务的时间,因为 等待时长=分发时的时间戳-任务生成的时间戳(join)。
+                    genRedis.status=message.result;
+                    genRedis.msg=message.msg;
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"PDFGen:{task.sessionId}", task.id, genRedis.ToJsonString());
+                }
+                ///如果是超时,放回队列。
+                if (message.result==4)
+                {
+                    await _azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue", task.ToJsonString());
+                }
+            }
+            if (screenClient!=null && screenClient.status!.Equals(ScreenConstant.idle))
+            {
+                var taskData = await GenPDFService.SentTask(_azureRedis, _azureStorage);
+                if (taskData.genQueue!=null && taskData.genRedis!=null  && !string.IsNullOrWhiteSpace(taskData.genQueue.cntName))
+                {
+                    screenClient.status =  ScreenConstant.busy;
+                    screenClient.last_time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                    await SendMessage(connid, new ScreenProcessMessage
+                    {
+                        connid = connid,
+                        clientid =screenClient. clientid,
+                        status = ScreenConstant.busy,
+                        grant_type = screenClient.grant_type,
+                        message_type= MessageType.task_send_success,
+                        content =$"{taskData.genQueue.ToJsonString()}",//从Redis中获取任务信息
+                    });
+                }
+                else
+                {
+                    _logger.LogInformation($"客户端当前空闲=>{screenClient.name},{screenClient.region},{screenClient.clientid},暂无任务可领取的任务......");
+                    if (taskData.genRedis!=null)
+                    {
+                        string msgError = $"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{taskData.msg}\ngenQueue:{taskData.genQueue?.ToJsonString()}\ngenRedis:{taskData.genRedis?.ToJsonString()}";
+                        _logger.LogInformation(msgError);
+                        await _dingDing.SendBotMsg(msgError, GroupNames.成都开发測試群組);
+                    }
+                    else
+                    {
+                        _logger.LogInformation($"分发任务异常原因=>{screenClient.name},{screenClient.region},{screenClient.clientid}:{taskData.msg}\n");
+                    }
+                    await SendMessage(connid, new ScreenProcessMessage
+                    {
+                        connid = connid,
+                        clientid = screenClient.clientid,
+                        status = ScreenConstant.idle,
+                        grant_type =screenClient. grant_type,
+                        message_type= MessageType.task_send_error,
+                        content = taskData.msg
+                    });
+                }
+                await _azureRedis.GetRedisClient(8).HashSetAsync($"ScreenApi:clients", screenClient.clientid, screenClient.ToJsonString());
+            }
+        }
         public async Task SendConnection(string connectionId, MessageBody msg)
         public async Task SendConnection(string connectionId, MessageBody msg)
         {
         {
             await Clients.Client(connectionId).ReceiveConnection(msg);
             await Clients.Client(connectionId).ReceiveConnection(msg);

+ 3 - 1
TEAMModelOS.Extension/HTEX.ScreenClient/HTEX.ScreenClient.csproj

@@ -7,7 +7,9 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.7" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
+    <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
+    <PackageReference Include="System.Management" Version="8.0.0" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 154 - 9
TEAMModelOS.Extension/HTEX.ScreenClient/Program.cs

@@ -1,10 +1,10 @@
 using HTEX.ScreenClient.Services;
 using HTEX.ScreenClient.Services;
-using Microsoft.AspNetCore.Hosting.Server;
-using Microsoft.AspNetCore.Hosting.Server.Features;
-using Microsoft.Extensions.Hosting;
+using System.Diagnostics;
+using System.Management;
 using System.Net;
 using System.Net;
-using System.Net.NetworkInformation;
+using System.Net.Sockets;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
 
 
 namespace HTEX.ScreenClient
 namespace HTEX.ScreenClient
 {
 {
@@ -12,14 +12,91 @@ namespace HTEX.ScreenClient
     {
     {
         public static void Main(string[] args)
         public static void Main(string[] args)
         {
         {
-      
+            long  CpuCoreCount = 0;
+            long  MenemorySize = 0;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ) {
+ 
+                // 获取CPU核心数
+                int processorCount = Environment.ProcessorCount;
+                 
+                Console.WriteLine("CPU 核心数: " + processorCount);
+                using (ManagementClass managementClass = new ManagementClass("Win32_Processor"))
+                {
+                    using (ManagementObjectCollection managementObjectCollection = managementClass.GetInstances())
+                    {
+                        foreach (ManagementObject managementObject in managementObjectCollection)
+                        {
+                            CpuCoreCount += Convert.ToInt32(managementObject.Properties["NumberOfLogicalProcessors"].Value);
+                        }
+                    }
+                }
+                using (ManagementClass mc = new ManagementClass("Win32_ComputerSystem"))
+                {
+                    using (ManagementObjectCollection moc = mc.GetInstances())
+                    {
+                        foreach (ManagementObject mo in moc)
+                        {
+                            if (mo["TotalPhysicalMemory"]!= null)
+                            {
+                                MenemorySize = Convert.ToInt64(mo["TotalPhysicalMemory"]);
+                            }
+                        }
+                    }
+                }
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
+                int processorCount = Environment.ProcessorCount;
+                Console.WriteLine("CPU 核心数: " + processorCount);
+                string[] cpu_lines = File.ReadAllLines("/proc/cpuinfo");
+                CpuCoreCount= cpu_lines.Count(line => line.StartsWith("processor", StringComparison.OrdinalIgnoreCase));
+                string[] mem_lines = File.ReadAllLines("/proc/meminfo");
+                var match = mem_lines.FirstOrDefault(line => line.StartsWith("MemTotal:"));
+                if (match != null)
+                {
+                    var matchResult = Regex.Match(match, @"\d+");
+                    if (matchResult.Success)
+                    {
+                        MenemorySize=  long.Parse(matchResult.Value);
+                    }
+                }
+            } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+                using (var process = new Process())
+                {
+                    process.StartInfo.FileName = "/usr/sbin/sysctl";
+                    process.StartInfo.Arguments = "-n hw.ncpu";
+                    process.StartInfo.RedirectStandardOutput = true;
+                    process.StartInfo.UseShellExecute = false;
+                    process.Start();
+                    string output = process.StandardOutput.ReadToEnd().Trim();
+                    int coreCount;
+                    if (int.TryParse(output, out coreCount))
+                    {
+                        CpuCoreCount= coreCount;
+                    }
+                }
+                using (var process = new Process())
+                {
+                    process.StartInfo.FileName = "/usr/sbin/sysctl";
+                    process.StartInfo.Arguments = "-n hw.memsize";
+                    process.StartInfo.RedirectStandardOutput = true;
+                    process.StartInfo.UseShellExecute = false;
+                    process.Start();
+                    string output = process.StandardOutput.ReadToEnd().Trim();
+                    long memorySize;
+                    if (long.TryParse(output, out memorySize))
+                    {
+                        MenemorySize=  memorySize;
+                    }
+                }
+            }
+            Console.WriteLine("CPU 核心数: " + CpuCoreCount+",RAM 大小:"+MenemorySize);
             var builder = WebApplication.CreateBuilder(args);
             var builder = WebApplication.CreateBuilder(args);
    
    
             builder.Services.AddControllers();
             builder.Services.AddControllers();
             builder.Services.AddHttpClient();
             builder.Services.AddHttpClient();
             builder.Services.AddHttpContextAccessor();
             builder.Services.AddHttpContextAccessor();
-
-
+            //CheckOrNewPort(1883)
+            //CheckOrNewPort(5000)
             builder.WebHost.UseKestrel(options => {
             builder.WebHost.UseKestrel(options => {
                 //options.ListenAnyIP(4001, options => {
                 //options.ListenAnyIP(4001, options => {
                 //   // options.UseHttps("Crt/iteden.pfx", "iteden"); 
                 //   // options.UseHttps("Crt/iteden.pfx", "iteden"); 
@@ -28,8 +105,7 @@ namespace HTEX.ScreenClient
                 options.ListenAnyIP(5000, options => {/* options.UseHttps("Configs/Crt/iteden.pfx", "iteden"); */}); // Default HTTP pipeline
                 options.ListenAnyIP(5000, options => {/* options.UseHttps("Configs/Crt/iteden.pfx", "iteden"); */}); // Default HTTP pipeline
             });
             });
 
 
-
-            builder.Services.AddHostedService<SignalRClientHub>();
+            builder.Services.AddHostedService<SignalRScreenClientHub>();
             var app = builder.Build();
             var app = builder.Build();
             // Configure the HTTP request pipeline.
             // Configure the HTTP request pipeline.
 
 
@@ -39,5 +115,74 @@ namespace HTEX.ScreenClient
             app.MapControllers();
             app.MapControllers();
             app.Run();
             app.Run();
         }
         }
+        /// <summary>
+        /// 检测端口是否可用,如果不可用,则递归调用本方法,直到可用为止
+        /// </summary>
+        /// <param name="port"></param>
+        /// <returns></returns>
+        public static int CheckOrNewPort(int port) 
+        {
+            if (IsPortAvailable(port)) 
+            {
+                return port;
+            }
+            else 
+            {
+                return CheckOrNewPort(port + 1);
+            }
+        }
+
+        /// <summary>
+        /// 探测端口是否可用
+        /// </summary>
+        /// <param name="port"></param>
+        /// <returns></returns>
+        public static bool IsPortAvailable(int port) 
+        {
+            bool isPortAvailable = false;
+            IPAddress ipAddress = IPAddress.Parse("127.0.0.1"); // 本地地址
+            try
+            {
+                using (Socket socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
+                {
+                    // 尝试连接到指定的IP地址和端口  
+                    IAsyncResult result = socket.BeginConnect(ipAddress, port, null, null);
+
+                    // 等待连接尝试完成(这里简单地使用Socket的ConnectTimeout,但更常见的做法是使用超时等待或异步回调)  
+                    bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1), true);
+
+                    if (success)
+                    {
+                        // 如果连接成功,则端口被占用  
+                        socket.EndConnect(result);
+                        isPortAvailable = false;
+
+                        // 可选:如果需要,可以在这里关闭连接  
+                        socket.Shutdown(SocketShutdown.Both);
+                        socket.Close();
+                    }
+                    else
+                    {
+                        isPortAvailable = true;
+                    }
+                }
+            }
+            catch (SocketException ex)
+            {
+                Console.WriteLine(ex.Message);
+                // 如果错误代码为10048,则表示地址已被使用  
+                if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
+                {
+                    isPortAvailable = false;
+                }
+            }
+            catch (Exception ex)
+            {
+                isPortAvailable = false;
+                Console.WriteLine(ex.Message);
+            }
+            return isPortAvailable;
+        }
+
     }
     }
 }
 }

+ 0 - 134
TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRClientHub.cs

@@ -1,134 +0,0 @@
-
-using Microsoft.AspNetCore.Hosting.Server;
-using Microsoft.AspNetCore.Hosting.Server.Features;
-using Microsoft.AspNetCore.SignalR.Client;
-using Microsoft.Extensions.Configuration;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-using System.Web;
-using TEAMModelOS.SDK;
-using TEAMModelOS.SDK.Extension;
-
-namespace HTEX.ScreenClient.Services
-{
-    public class SignalRClientHub : BackgroundService, IDisposable
-    {
-        private readonly IConfiguration _configuration; 
-        private readonly ILogger<SignalRClientHub> _logger;
-        private readonly IHttpClientFactory _httpClientFactory;
-        private readonly IServiceProvider _services;
-        private IEnumerable<string>? _url = new List<string>();
-        public SignalRClientHub(IConfiguration configuration,ILogger<SignalRClientHub> logger,IHttpClientFactory httpClientFactory, IServiceProvider services,IHostApplicationLifetime lifetime) 
-        {
-                
-            _configuration=configuration;
-            _logger=logger;
-            _httpClientFactory=httpClientFactory;
-            _services=services;
-            lifetime.ApplicationStarted.Register(() => {
-                var server = _services.GetService<IServer>();
-                _url = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
-            });
-        }
-        private List<string> messages = new List<string>();
-        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
-        {
-            var device =  await GetClientInfo();
-            string hashData = $"{device.name}-{device.remote}-{device.port}-{device.os}-{string.Join(",",device.networks.Select(x=>$"{x.mac}-{x.ip}"))}";
-            string clientid =  ShaHashHelper.GetSHA256(hashData);
-            string? CenterUrl = _configuration.GetSection("ScreenClient:CenterUrl").Value;
-            string? ScreenUrl = _configuration.GetSection("ScreenClient:ScreenUrl").Value;
-            long Timeout = _configuration.GetValue<long>("ScreenClient:Timeout");
-            long Delay = _configuration.GetValue<long>("ScreenClient:Delay");
-            device.timeout = Timeout;
-            device.delay = Delay;
-            device.screenUrl = ScreenUrl;
-            HubConnection hubConnection = new HubConnectionBuilder()
-               .WithUrl($"{CenterUrl}/signalr/screen?grant_type=bookjs_api&clientid={clientid}&device={HttpUtility.UrlEncode(device.ToJsonString(),Encoding.Unicode)}") //only one slash
-               .WithAutomaticReconnect()
-               .ConfigureLogging(logging =>
-               {
-                   logging.SetMinimumLevel(LogLevel.Information);
-                   logging.AddConsole();
-               })
-               .Build();
-            hubConnection.On<JsonElement>("ReceiveConnection", ( message) =>
-            {
-                var encodedMsg = $" {message}";
-                _logger.LogInformation($"连接成功:{message.ToJsonString()}");
-                messages.Add(encodedMsg);
-            });
-            hubConnection.On<JsonElement>("ReceiveMessage", (message) =>
-            {
-                var encodedMsg = $"{message}";
-                messages.Add(encodedMsg);
-            });
-            
-            await hubConnection.StartAsync();
-        }
-         
-        public async Task<ClientDevice> GetClientInfo()
-        {
-            string hostName =$"{Environment.UserName}-{Dns.GetHostName()}" ;
-            string os = RuntimeInformation.OSDescription;
-            //获取当前客户端的服务端口
-            var _httpClient = _httpClientFactory.CreateClient();
-            ClientDevice device = new ClientDevice { name =hostName, os= os };
-            HttpResponseMessage message = await _httpClient.PostAsJsonAsync("https://www.teammodel.cn/core/system-info", new { });
-            if (message.IsSuccessStatusCode)
-            {
-                JsonNode? json = JsonSerializer.Deserialize<JsonNode>(await message.Content.ReadAsStringAsync());
-                var ip =  json?["ip"];
-                var region = json?["region"];
-                _logger.LogInformation($"远程地址:{ip}");
-                _logger.LogInformation($"所属地区:{region}");
-                device.remote=ip?.ToString();
-                device.region=region?.ToString();
-            }
-            _logger.LogInformation($"计算机名:{hostName}");
-            _logger.LogInformation($"系统名称:{RuntimeInformation.OSDescription}");
-           
-            var nics = NetworkInterface.GetAllNetworkInterfaces();
-            foreach (var nic in nics)
-            {
-                if (nic.OperationalStatus == OperationalStatus.Up)
-                {
-                    var mac = nic.GetPhysicalAddress().ToString();
-                    var properties = nic.GetIPProperties();
-                    var unicastAddresses = properties.UnicastAddresses;
-                    foreach (var unicast in unicastAddresses)
-                    {
-                        if (unicast.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
-                        {
-                            var ip = unicast.Address.ToString();
-                            Network network= new Network() { mac=mac, ip=ip };
-                            if (!string.IsNullOrWhiteSpace(mac.ToString()))
-                            {
-                                device.networks.Add(network);
-                                _logger.LogInformation($"网卡地址: {mac}");
-                                _logger.LogInformation($"内网地址: {ip}");
-                            }
-                        }
-                    }
-                }
-            }
-            if (_url!=null) 
-            {
-                List<int> ports = new List<int>();
-                foreach (var url in _url)
-                {
-                    Uri uri = new Uri(url);
-                    ports.Add(uri.Port);
-                }
-                device.port= string.Join(",", ports);
-                _logger.LogInformation($"占用端口: {device.port}");
-            }
-            return device ;
-        }
-       
-    }
-}

+ 311 - 0
TEAMModelOS.Extension/HTEX.ScreenClient/Services/SignalRScreenClientHub.cs

@@ -0,0 +1,311 @@
+
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.Configuration;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using System.Web;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.Extension;
+
+namespace HTEX.ScreenClient.Services
+{
+    public class SignalRScreenClientHub : BackgroundService, IDisposable
+    {
+        private readonly IConfiguration _configuration; 
+        private readonly ILogger<SignalRScreenClientHub> _logger;
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IServiceProvider _services;
+        private IEnumerable<string>? _url = new List<string>();
+        private ClientDevice? device;
+        public SignalRScreenClientHub(IConfiguration configuration,ILogger<SignalRScreenClientHub> logger,IHttpClientFactory httpClientFactory, IServiceProvider services,IHostApplicationLifetime lifetime) 
+        {
+                
+            _configuration=configuration;
+            _logger=logger;
+            _httpClientFactory=httpClientFactory;
+            _services=services;
+            lifetime.ApplicationStarted.Register(() => {
+                var server = _services.GetService<IServer>();
+                _url = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
+            });
+        }
+        
+        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            device =  await GetClientInfo();
+            string hashData = $"{device.name}-{device.remote}-{device.port}-{device.os}-{string.Join(",",device.networks.Select(x=>$"{x.mac}-{x.ip}"))}";
+            string clientid =  ShaHashHelper.GetSHA256(hashData);
+            string? CenterUrl = _configuration.GetSection("ScreenClient:CenterUrl").Value;
+            string? ScreenUrl = _configuration.GetSection("ScreenClient:ScreenUrl").Value;
+            long Timeout = _configuration.GetValue<long>("ScreenClient:Timeout");
+            long Delay = _configuration.GetValue<long>("ScreenClient:Delay");
+            device.timeout = Timeout;
+            device.delay = Delay;
+            device.screenUrl = ScreenUrl;
+            await StartHubConnectionAsync(stoppingToken, clientid, CenterUrl);
+        }
+        private async Task StartHubConnectionAsync(CancellationToken cancellationToken,string clientid, string? CenterUrl)
+        {
+            //重写重连策略,防止服务端更新,断线后,客户端达到最大连接次数,依然连线不上服务端。
+            var reconnectPolicy = new ExponentialBackoffReconnectPolicy(TimeSpan.FromSeconds(10), _logger); // 尝试重连的最大次数,这里使用 int.MaxValue 表示无限次
+            reconnectPolicy.MaxRetryCount = int.MaxValue;
+            HubConnection hubConnection = new HubConnectionBuilder()
+               .WithUrl($"{CenterUrl}/signalr/screen?grant_type=bookjs_api&clientid={clientid}&device={HttpUtility.UrlEncode(device.ToJsonString(), Encoding.Unicode)}") //only one slash
+               .WithAutomaticReconnect(reconnectPolicy)
+               .ConfigureLogging(logging =>
+               {
+                   logging.SetMinimumLevel(LogLevel.Information);
+                   logging.AddConsole();
+               })
+               .Build();
+            try {
+                hubConnection.On<ConnectionMessage>("ReceiveConnection", (message) =>
+                {
+                    _logger.LogInformation($"连接成功:{message.ToJsonString()}");
+                    //重置重连次数。
+                    reconnectPolicy.Reset();
+                });
+                hubConnection.On<ScreenProcessMessage>("ReceiveMessage", async (message) =>
+                {
+                    if (message.message_type.Equals(MessageType.task_send_success))
+                    {
+                        var data = await ReceiveMessage(message);
+                        _logger.LogInformation($"任务执行完成,执行状态{data.status},消息:{data.msg},任务信息:{data.task?.ToJsonString()}");
+                        message.content=data.task?.ToJsonString();
+                        message.result=data.status;
+                        message.msg=data.msg;
+                        if (data.status==2)
+                        {
+                            message.message_type= MessageType.task_execute_success;
+                        }
+                        else
+                        {
+                            message.message_type= MessageType.task_execute_error;
+                        }
+                        await hubConnection.InvokeAsync<ScreenProcessMessage>("ReceiveMessage", message);
+                    }
+                    else
+                    {
+                        _logger.LogInformation($"任务领取失败,{message.ToJsonString()}");
+                    }
+                });
+                await hubConnection.StartAsync(cancellationToken);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError("初次启动连接SignalR失败,等待重连......");
+                int retryCount = 0;
+                const int maxRetries = 360;
+                const int retryDelaySeconds = 10;
+                while (retryCount < maxRetries)
+                {
+                    try
+                    {
+                        await Task.Delay(retryDelaySeconds * 1000, cancellationToken); // 等待一段时间后重试  
+                        await hubConnection.StartAsync(cancellationToken);
+                        _logger.LogInformation("SignalR连接成功(重试后)!");
+                        break; // 连接成功,退出循环  
+                    }
+                    catch (Exception retryEx)
+                    {
+                        retryCount++;
+                        _logger.LogInformation($"SignalR连接重试失败: {retryEx.Message}。重试次数: {retryCount}/{maxRetries}");
+                        // 可以在这里决定是否因为某种原因停止重试  
+                        if (retryCount == maxRetries)
+                        {
+                            _logger.LogInformation("达到最大重试次数,停止重试。");
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        public async Task<(int status, string msg, PDFGenQueue? task )> ReceiveMessage(ScreenProcessMessage message)
+        {
+            int  status = 0;
+            string msg= string.Empty;
+            PDFGenQueue? task = null;
+            try 
+            {
+                task = message.content.ToObject<PDFGenQueue>();
+                long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                var client = _httpClientFactory.CreateClient();
+                string urlpdf = $"{device.screenUrl}/api/pdf";
+                string jsonElement = new
+                {
+                    pageUrl = task.pageUrl,
+                    timeout = task.timeout,
+                    delay = task.delay,
+                    checkPageCompleteJs = task.checkPageCompleteJs
+                }.ToJsonString();
+                var request = new HttpRequestMessage
+                {
+                    Method = new HttpMethod("POST"),
+                    RequestUri = new Uri(urlpdf),
+                    Content = new StringContent(jsonElement)
+                };
+                var mediaTypeHeader = new MediaTypeHeaderValue("application/json")
+                {
+                    CharSet = "UTF-8"
+                };
+                request.Content.Headers.ContentType = mediaTypeHeader;
+                HttpResponseMessage responseMessage = await client.SendAsync(request);
+                if (responseMessage.IsSuccessStatusCode)
+                {
+                    string content = await responseMessage.Content.ReadAsStringAsync();
+                    JsonNode jsonNode = content.ToObject<JsonNode>();
+                    var code = jsonNode["code"];
+                    var file = jsonNode["data"]?["file"];
+                    if (code!=null  && $"{code}".Equals("0")  && file!= null  && !string.IsNullOrWhiteSpace($"{file}"))
+                    {
+                        try 
+                        {
+                            Stream stream = await client.GetStreamAsync($"{device.screenUrl}/{file}");
+                            string content_type = "application/octet-stream";
+                            ContentTypeDict.dict.TryGetValue(".pdf", out string? contenttype);
+                            if (!string.IsNullOrEmpty(contenttype))
+                            {
+                                content_type = contenttype;
+
+                            }
+                            BlobClient blockBlob = new BlobClient(new Uri(task.blobFullUrl));
+                            await blockBlob.UploadAsync(stream, true);
+                            blockBlob.SetHttpHeaders(new BlobHttpHeaders { ContentType = content_type });
+                            status=2;
+                            msg = $"PDF回传保存成功!保存地址{task.blobFullUrl}";
+                        }
+                        catch (Exception ex) {
+                            status=6; 
+                            msg = $"PDF回传保存异常!异常信息{ex.Message},相关参数信息:保存地址{task.blobFullUrl}";
+                        }
+                    }
+                    else {
+                        if (code!= null && $"{code}".Equals("99999"))
+                        {
+                            status= 4;
+                            msg = "PDF生成接口调用超时!";
+
+                        }
+                        else
+                        {
+                            status= 3;
+                            msg =$"PDF生成接口返回参数异常!返回内容:{content}";
+                        }
+                    }
+                }
+                else {
+                    status=3;
+                    msg =$"PDF生成接口调用异常!接口状态:{responseMessage.StatusCode}";
+                }
+            }
+            catch (Exception ex)
+            {
+                status = 3;
+                msg = $"PDF生成接口调用异常,异常信息“{ex.Message}";
+            }
+            return (status, msg, task);
+        }
+
+        public async Task<ClientDevice> GetClientInfo()
+        {
+            string hostName =$"{Environment.UserName}-{Dns.GetHostName()}" ;
+            string os = RuntimeInformation.OSDescription;
+            //获取当前客户端的服务端口
+            var _httpClient = _httpClientFactory.CreateClient();
+            ClientDevice device = new ClientDevice { name =hostName, os= os };
+            HttpResponseMessage message = await _httpClient.PostAsJsonAsync("https://www.teammodel.cn/core/system-info", new { });
+            if (message.IsSuccessStatusCode)
+            {
+                JsonNode? json = JsonSerializer.Deserialize<JsonNode>(await message.Content.ReadAsStringAsync());
+                var ip =  json?["ip"];
+                var region = json?["region"];
+                _logger.LogInformation($"远程地址:{ip}");
+                _logger.LogInformation($"所属地区:{region}");
+                device.remote=ip?.ToString();
+                device.region=region?.ToString();
+            }
+            _logger.LogInformation($"计算机名:{hostName}");
+            _logger.LogInformation($"系统名称:{RuntimeInformation.OSDescription}");
+           
+            var nics = NetworkInterface.GetAllNetworkInterfaces();
+            foreach (var nic in nics)
+            {
+                if (nic.OperationalStatus == OperationalStatus.Up)
+                {
+                    var mac = nic.GetPhysicalAddress().ToString();
+                    var properties = nic.GetIPProperties();
+                    var unicastAddresses = properties.UnicastAddresses;
+                    foreach (var unicast in unicastAddresses)
+                    {
+                        if (unicast.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+                        {
+                            var ip = unicast.Address.ToString();
+                            Network network= new Network() { mac=mac, ip=ip };
+                            if (!string.IsNullOrWhiteSpace(mac.ToString()))
+                            {
+                                device.networks.Add(network);
+                                _logger.LogInformation($"网卡地址: {mac}");
+                                _logger.LogInformation($"内网地址: {ip}");
+                            }
+                        }
+                    }
+                }
+            }
+            if (_url!=null) 
+            {
+                List<int> ports = new List<int>();
+                foreach (var url in _url)
+                {
+                    Uri uri = new Uri(url);
+                    ports.Add(uri.Port);
+                }
+                device.port= string.Join(",", ports);
+                _logger.LogInformation($"占用端口: {device.port}");
+            }
+            return device ;
+        }
+       
+    }
+    public class ExponentialBackoffReconnectPolicy : IRetryPolicy
+    {
+        private readonly TimeSpan _retryInterval;
+       
+        private int _retryCount;
+        public int MaxRetryCount { get; set; } =int.MaxValue;
+        public readonly ILogger<SignalRScreenClientHub>  _logger;
+        public ExponentialBackoffReconnectPolicy(TimeSpan retryInterval, ILogger<SignalRScreenClientHub> logger)
+        {
+            _retryInterval = retryInterval;
+            _retryCount = 0;
+           _logger = logger;
+        }
+
+        public TimeSpan? NextRetryDelay(RetryContext retryContext)
+        {
+            _logger.LogInformation($"重连次数: {_retryCount}");
+            if (_retryCount < MaxRetryCount)
+            {
+                _retryCount++;
+                // 计算下一次重连的延迟时间
+                return _retryInterval;
+            }
+            return null; // 达到最大重连次数后不再重连
+        }
+
+        public void Reset()
+        {
+            _retryCount = 0;
+        }
+    }
+}

+ 7 - 1
TEAMModelOS.Extension/HTEX.ScreenClient/appsettings.json

@@ -5,5 +5,11 @@
       "Microsoft.AspNetCore": "Warning"
       "Microsoft.AspNetCore": "Warning"
     }
     }
   },
   },
-  "AllowedHosts": "*"
+  "AllowedHosts": "*",
+  "ScreenClient": {
+    "ScreenUrl": "http://52.130.252.100:13000",
+    "Timeout": 30000,
+    "Delay": 500,
+    "CenterUrl": "https://localhost:7298"
+  }
 }
 }

+ 6 - 6
TEAMModelOS.SDK/DI/AzureStorage/AzureStorageFactory.cs

@@ -17,6 +17,7 @@ using TEAMModelOS.SDK.Context.Attributes.Azure;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using TEAMModelOS.SDK.Module.AzureBlob.Configuration;
 using TEAMModelOS.SDK.Module.AzureBlob.Configuration;
 using Azure.Data.Tables;
 using Azure.Data.Tables;
+using Microsoft.AspNetCore.Mvc.Rendering;
 
 
 namespace TEAMModelOS.SDK.DI
 namespace TEAMModelOS.SDK.DI
 {
 {
@@ -224,7 +225,7 @@ namespace TEAMModelOS.SDK.DI
         /// <param name="blobSasPermissions">權限(可多選)Flags</param>
         /// <param name="blobSasPermissions">權限(可多選)Flags</param>
         /// <param name="name"></param>
         /// <param name="name"></param>
         /// <returns></returns>
         /// <returns></returns>
-        public string GetBlobSAS(string containerName, string blobName, BlobSasPermissions blobSasPermissions, string name = "Default",int day=1)
+        public (string  fullUri,string sas ) GetBlobSAS(string containerName, string blobName, BlobSasPermissions blobSasPermissions, string name = "Default",int hour=24)
         {
         {
             try
             try
             {
             {
@@ -236,11 +237,11 @@ namespace TEAMModelOS.SDK.DI
                 var blobSasBuilder = new BlobSasBuilder
                 var blobSasBuilder = new BlobSasBuilder
                 {
                 {
                     StartsOn = DateTimeOffset.UtcNow.Subtract(new TimeSpan(0, 15, 0)),
                     StartsOn = DateTimeOffset.UtcNow.Subtract(new TimeSpan(0, 15, 0)),
-                    ExpiresOn = DateTimeOffset.UtcNow.Add(new TimeSpan(day, 0, 15, 0)),
+                    ExpiresOn = DateTimeOffset.UtcNow.Add(new TimeSpan(0, hour, 15, 0)),
                     BlobContainerName = containerName.ToLower(),
                     BlobContainerName = containerName.ToLower(),
                     BlobName = blobName
                     BlobName = blobName
                 };
                 };
-
+            
                 blobSasBuilder.SetPermissions(blobSasPermissions);
                 blobSasBuilder.SetPermissions(blobSasPermissions);
                 var sskc = new StorageSharedKeyCredential(accountname, accountkey);
                 var sskc = new StorageSharedKeyCredential(accountname, accountkey);
                 BlobSasQueryParameters sasQueryParameters = blobSasBuilder.ToSasQueryParameters(sskc);
                 BlobSasQueryParameters sasQueryParameters = blobSasBuilder.ToSasQueryParameters(sskc);
@@ -251,12 +252,11 @@ namespace TEAMModelOS.SDK.DI
                     Path = $"{containerName.ToLower()}/{blobName}",
                     Path = $"{containerName.ToLower()}/{blobName}",
                     Query = sasQueryParameters.ToString()
                     Query = sasQueryParameters.ToString()
                 };
                 };
-
-                return fullUri.Uri.ToString();
+                return (fullUri.Uri.ToString(), sasQueryParameters.ToString());
             }
             }
             catch
             catch
             {
             {
-                return null;
+                return (null,null);
             }
             }
         }
         }
 
 

+ 2 - 2
TEAMModelOS.SDK/Models/Service/BatchCopyFileService.cs

@@ -57,7 +57,7 @@ namespace TEAMModelOS.SDK.Models.Service
                 {
                 {
                     string newurl = $"{blobItem.Name}".Replace($"/{batchCopyFile.oldFileName}/", $"/{batchCopyFile.newFileName}/");//替换成新的容器路径
                     string newurl = $"{blobItem.Name}".Replace($"/{batchCopyFile.oldFileName}/", $"/{batchCopyFile.newFileName}/");//替换成新的容器路径
                     var urlSas = _azureStorage.GetBlobSAS($"{batchCopyFile.blobCntr}", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                     var urlSas = _azureStorage.GetBlobSAS($"{batchCopyFile.blobCntr}", blobItem.Name, BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
-                    filelist.Add(azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas)));    //可复制256M以上文件
+                    filelist.Add(azureClient.GetBlobClient(newurl).StartCopyFromUriAsync(new Uri(urlSas.fullUri)));    //可复制256M以上文件
                     //filelist.Add(azureClient.GetBlobClient(newurl).SyncCopyFromUriAsync(new Uri(urlSas)));  //添加复制文件到集合执行复制操作  复制256M以下文件
                     //filelist.Add(azureClient.GetBlobClient(newurl).SyncCopyFromUriAsync(new Uri(urlSas)));  //添加复制文件到集合执行复制操作  复制256M以下文件
                 }
                 }
                 if (filelist.Count <= 256)
                 if (filelist.Count <= 256)
@@ -102,7 +102,7 @@ namespace TEAMModelOS.SDK.Models.Service
                 string oldFileName = oldFile.Substring(oldFile.IndexOf($"{blobName}/") + $"{blobName}/".Length);
                 string oldFileName = oldFile.Substring(oldFile.IndexOf($"{blobName}/") + $"{blobName}/".Length);
                 var urlSas = _azureStorage.GetBlobSAS($"{blobName}", $"{HttpUtility.UrlDecode(oldFileName)}", BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                 var urlSas = _azureStorage.GetBlobSAS($"{blobName}", $"{HttpUtility.UrlDecode(oldFileName)}", BlobSasPermissions.Read | BlobSasPermissions.List);   //获取容器sas和有效期
                 //var respCopy =  azureClient.GetBlobClient(HttpUtility.UrlDecode(newurl)).SyncCopyFromUri(new Uri(urlSas));    //可复制256M以下文件
                 //var respCopy =  azureClient.GetBlobClient(HttpUtility.UrlDecode(newurl)).SyncCopyFromUri(new Uri(urlSas));    //可复制256M以下文件
-                var respCopy1 = await azureClient.GetBlobClient(HttpUtility.UrlDecode(newurl)).StartCopyFromUriAsync(new Uri(urlSas));    //可复制256M以上文件
+                var respCopy1 = await azureClient.GetBlobClient(HttpUtility.UrlDecode(newurl)).StartCopyFromUriAsync(new Uri(urlSas.fullUri));    //可复制256M以上文件
 
 
                 if (!string.IsNullOrEmpty($"{respCopy1.Id}"))
                 if (!string.IsNullOrEmpty($"{respCopy1.Id}"))
                     return 200;
                     return 200;

+ 158 - 16
TEAMModelOS.SDK/Models/Service/GenPDFService.cs

@@ -21,14 +21,96 @@ using static TEAMModelOS.SDK.CoreAPIHttpService;
 using TEAMModelOS.SDK.Models;
 using TEAMModelOS.SDK.Models;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using System.Net.Http.Headers;
 using System.Net.Http.Headers;
+using Azure.Storage.Sas;
 
 
 namespace TEAMModelOS.SDK
 namespace TEAMModelOS.SDK
 {
 {
     public static class GenPDFService
     public static class GenPDFService
     {
     {
+        public static async Task<(PDFGenRedis genRedis, PDFGenQueue genQueue,string msg )> SentTask(AzureRedisFactory _azureRedis, AzureStorageFactory _azureStorage) 
+        {
+            string msg = string.Empty;
+            //从尾部弹出元素,队列先进先出
+            long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            var queueValue = await _azureRedis.GetRedisClient(8).ListRightPopAsync("PDFGen:Queue");
+            PDFGenRedis genRedis = null;
+            PDFGenQueue genQueue = null;
+            if (queueValue!=default && queueValue.HasValue)
+            {
+                genQueue = queueValue.ToString().ToObject<PDFGenQueue>();
+                RedisValue redisValue = await _azureRedis.GetRedisClient(8).HashGetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id);
+                if (redisValue!=default)
+                {
+                    genRedis = redisValue.ToString().ToObject<PDFGenRedis>();
+                    //计算等待了多久的时间才开始生成。
+                    var wait = now- genRedis.join;
+                    genRedis.wait  = wait;
+                    genRedis.status = 1;
+                    try
+                    {
+                        Uri uri = new Uri(genQueue.pageUrl);
+                        var query = HttpUtility.ParseQueryString(uri.Query);
+                        string? url = query["url"];
+                        if (!string.IsNullOrWhiteSpace(url))
+                        {
+                            url=  HttpUtility.UrlDecode(url);
+                            uri = new Uri(url);
+                            string host = uri.Host;
+                            var blobServiceClient = _azureStorage.GetBlobServiceClient();
+                            if (host.Equals(blobServiceClient.Uri.Host))
+                            {
+                                // 获取容器名,它是路径的第一个部分
+                                string containerName = uri.Segments[1].TrimEnd('/');
+                                // 获取文件的完整同级目录,这是文件路径中除了文件名和扩展名之外的部分
+                                // 由于文件名是路径的最后一个部分,我们可以通过连接除了最后一个部分之外的所有部分来获取目录路径
+                                string directoryPath = string.Join("", uri.Segments, 2, uri.Segments.Length - 3);
+                                string? fileName = Path.GetFileNameWithoutExtension(uri.AbsolutePath);
+                                string blobPath = $"{directoryPath}{fileName}.pdf";
+                                var urlSas = _azureStorage.GetBlobSAS(containerName, blobPath, BlobSasPermissions.Write, hour: 1);
+                                genQueue.blobSas =  urlSas.sas;
+                                genQueue.blobName=blobPath;
+                                genQueue.cntName=containerName;
+                                genQueue.blobFullUrl=urlSas.fullUri;
+                            }
+                            else
+                            {
+                                msg="数据地址与服务提供的站点不一致!";
+                                genRedis.status=3;
+                                genRedis.msg = msg;
+                              
+                            }
+                        }
+                        else
+                        {
+                            msg="数据地址解析异常!";
+                            genRedis.status=3;
+                            genRedis.msg = msg;
+                            
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        msg=$"数据地址处理异常,异常信息:{ex.Message}";
+                        genRedis.status=3;
+                        genRedis.msg = ex.Message;
+                    }
+                    await _azureRedis.GetRedisClient(8).HashSetAsync($"PDFGen:{genQueue.sessionId}", genQueue.id, genRedis.ToJsonString());
+                }
+                else {
+                    msg="队列任务关联数据为空!";
+                }
+            }
+            else
+            {
+                msg="队列暂无任务!";
+            }
+            return (genRedis, genQueue, msg);
+        }
+
+
 
 
 
 
-        public static async Task GenPdf(AzureServiceBusFactory azureServiceBus, AzureRedisFactory _azureRedis, AzureCosmosFactory _azureCosmos,
+        public static async Task GenPdf( AzureRedisFactory _azureRedis, AzureCosmosFactory _azureCosmos,
             IConfiguration _configuration, IHttpClientFactory _httpClient, AzureStorageFactory _azureStorage,
             IConfiguration _configuration, IHttpClientFactory _httpClient, AzureStorageFactory _azureStorage,
             DingDing _dingDing, SnowflakeId _snowflakeId,string json )
             DingDing _dingDing, SnowflakeId _snowflakeId,string json )
         {
         {
@@ -407,11 +489,24 @@ namespace TEAMModelOS.SDK
                         dbData.join= now;
                         dbData.join= now;
                         dbData.wait=0;
                         dbData.wait=0;
                         dbData.name=item.name;
                         dbData.name=item.name;
+                        dbData.url= item.url;
+                        dbData.scope = data.scope;
+                        dbData.owner= data.owner;
                         genRedis.Add(dbData);
                         genRedis.Add(dbData);
                     }
                     }
                 }
                 }
                 else {
                 else {
-                    genRedis.Add(new PDFGenRedis { id = item.id, status=0, cost=0, wait=0, join=now,name=item.name,url= item.url });
+                    genRedis.Add(new PDFGenRedis {
+                        id = item.id,
+                        status=0,
+                        cost=0,
+                        wait=0,
+                        join=now,
+                        name=item.name,
+                        url= item.url,
+                        scope=data.scope,
+                        owner= data.owner
+                    });
                 }
                 }
             }
             }
             //过期时间 当前个数+ reddis的个数
             //过期时间 当前个数+ reddis的个数
@@ -430,9 +525,9 @@ namespace TEAMModelOS.SDK
                     timeout =data.timeout,
                     timeout =data.timeout,
                     name=item.name,
                     name=item.name,
                 };
                 };
-                string message = JsonSerializer.Serialize(genQueue);
+                //string message = JsonSerializer.Serialize(genQueue);
                 //从头部压入元素,队列先进先出
                 //从头部压入元素,队列先进先出
-                await azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue",  item.ToJsonString());
+                await azureRedis.GetRedisClient(8).ListLeftPushAsync($"PDFGen:Queue",  genQueue.ToJsonString());
 
 
                 //var serviceBusMessage = new ServiceBusMessage(message);
                 //var serviceBusMessage = new ServiceBusMessage(message);
                 //serviceBusMessage.ApplicationProperties.Add("name", "BlobRoot");
                 //serviceBusMessage.ApplicationProperties.Add("name", "BlobRoot");
@@ -490,6 +585,14 @@ namespace TEAMModelOS.SDK
         /// 艺术评测报告Art,评测报告Exam,问卷报告Survey,投票报告Vote,为空 则无法回调更新状态
         /// 艺术评测报告Art,评测报告Exam,问卷报告Survey,投票报告Vote,为空 则无法回调更新状态
         /// </summary>
         /// </summary>
         public string taskType { get; set;}
         public string taskType { get; set;}
+        /// <summary>
+        /// 数据所有者
+        /// </summary>
+        public string owner {  get; set; }
+        /// <summary>
+        /// 数据范围
+        /// </summary>
+        public string scope { get; set; } 
     }
     }
     public class GenPDFSchema 
     public class GenPDFSchema 
     {
     {
@@ -528,10 +631,6 @@ namespace TEAMModelOS.SDK
         /// </summary>
         /// </summary>
         public string name { get; set; }
         public string name { get; set; }
         /// <summary>
         /// <summary>
-        /// 0 未执行,1 执行中,2 执行成功,3 执行失败,4超时,5 取消,6 存放异常
-        /// </summary>
-        public int status { get; set; } = 0;
-        /// <summary>
         /// 执行生成 毫秒
         /// 执行生成 毫秒
         /// </summary>
         /// </summary>
         public long cost { get; set;}
         public long cost { get; set;}
@@ -551,6 +650,22 @@ namespace TEAMModelOS.SDK
         /// 數據的url
         /// 數據的url
         /// </summary>
         /// </summary>
         public string url { get; set; }
         public string url { get; set; }
+        /// <summary>
+        /// 0 未执行,1 执行中,2 执行成功,3 执行失败,4超时,5 取消,6 存放异常
+        /// </summary>
+        public int status { get; set; } = 0;
+        /// <summary>
+        /// 状态信息
+        /// </summary>
+        public string msg { get; set; }
+        /// <summary>
+        /// 数据所有者
+        /// </summary>
+        public string owner { get; set; }
+        /// <summary>
+        /// 数据范围
+        /// </summary>
+        public string scope { get; set; }
     }
     }
 
 
 
 
@@ -603,6 +718,23 @@ namespace TEAMModelOS.SDK
         /// 生成会话id, 活动id
         /// 生成会话id, 活动id
         /// </summary>
         /// </summary>
         public string sessionId { get; set; }
         public string sessionId { get; set; }
+        /// <summary>
+        /// blob的sas
+        /// </summary>
+
+        public string blobSas { get; set; }
+        /// <summary>
+        /// blob名称
+        /// </summary>
+        public string blobName { get; set; }
+        /// <summary>
+        /// 容器名称
+        /// </summary>
+        public  string cntName { get; set; }
+        /// <summary>
+        /// 完整blob地址
+        /// </summary>
+        public string blobFullUrl { get; set; }
     }
     }
 
 
 
 
@@ -630,6 +762,10 @@ namespace TEAMModelOS.SDK
         /// 最后更新时间
         /// 最后更新时间
         /// </summary>
         /// </summary>
         public long last_time { get; set; }
         public long last_time { get; set; }
+        /// <summary>
+        /// 任务完成数
+        /// </summary>
+        public int taskComplete { get; set; }
     }
     }
 
 
     public class SignalRClient
     public class SignalRClient
@@ -687,35 +823,38 @@ namespace TEAMModelOS.SDK
         /// 消息类型
         /// 消息类型
         /// </summary>
         /// </summary>
         public virtual MessageType message_type { get; set; }
         public virtual MessageType message_type { get; set; }
+      
     }
     }
     /// <summary>
     /// <summary>
     /// 连接消息
     /// 连接消息
     /// </summary>
     /// </summary>
-    public class ConnectionMessageContent : MessageBody
+    public class ConnectionMessage : MessageBody
     {
     {
 
 
     }
     }
     /// <summary>
     /// <summary>
     /// 断开连接消息
     /// 断开连接消息
     /// </summary>
     /// </summary>
-    public class DisConnectionMessageContent : MessageBody
+    public class DisConnectionMessage : MessageBody
     {
     {
 
 
     }
     }
     /// <summary>
     /// <summary>
     /// 业务处理消息
     /// 业务处理消息
     /// </summary>
     /// </summary>
-    public class ScreenProcessMessageContent : MessageBody
+    public class ScreenProcessMessage : MessageBody
     {
     {
+        public string msg { get; set; }
+        public int result { get; set; }
     }
     }
 
 
 
 
     public static class ScreenConstant 
     public static class ScreenConstant 
     {
     {
         public static readonly string busy = "busy";
         public static readonly string busy = "busy";
-        public static readonly string free = "free";
+        public static readonly string idle = "idle";
         public static readonly string error = "error";
         public static readonly string error = "error";
-        public static readonly string down = "down";
+        public static readonly string offline = "offline";
         public static readonly string grant_type = "bookjs_api";
         public static readonly string grant_type = "bookjs_api";
         /// <summary>
         /// <summary>
         /// 冗余时间
         /// 冗余时间
@@ -723,9 +862,12 @@ namespace TEAMModelOS.SDK
         public static readonly long time_excess = 5000;
         public static readonly long time_excess = 5000;
     }
     }
     public enum MessageType {
     public enum MessageType {
-        conn_success,
-        conn_error,
-        biz_task,
+        conn_success,//连接成功
+        conn_error,// 连接失败
+        task_send_success,// 任务发送成功
+        task_send_error,// 任务发送失败
+        task_execute_success,// 任务执行成功
+        task_execute_error,// 任务执行失败
     }
     }
     public class ClientDevice
     public class ClientDevice
     {
     {

+ 2 - 1
TEAMModelOS/Controllers/Client/HiTeachccControlller.cs

@@ -211,7 +211,8 @@ namespace TEAMModelOS.Controllers.Client
                         {
                         {
                             var root = json.RootElement.GetProperty("Documents").EnumerateArray().First();
                             var root = json.RootElement.GetProperty("Documents").EnumerateArray().First();
                             var url = root.GetProperty("url").GetString();
                             var url = root.GetProperty("url").GetString();
-                            res = _azureStorage.GetBlobSAS((rp ? request.school : tid), url, BlobSasPermissions.Read);
+                            var urlSas = _azureStorage.GetBlobSAS((rp ? request.school : tid), url, BlobSasPermissions.Read);
+                            res= urlSas.fullUri;
                         }
                         }
                     }
                     }
                 }
                 }

+ 2 - 2
TEAMModelOS/Controllers/System/BlobController.cs

@@ -443,7 +443,7 @@ namespace TEAMModelOS.Controllers
                         var turl = _azureStorage.GetBlobSAS($"{_cntr}", thum, BlobSasPermissions.Read | BlobSasPermissions.List);
                         var turl = _azureStorage.GetBlobSAS($"{_cntr}", thum, BlobSasPermissions.Read | BlobSasPermissions.List);
                         try
                         try
                         {
                         {
-                            bcc.GetBlobClient(tname).SyncCopyFromUri(new Uri(turl));
+                            bcc.GetBlobClient(tname).SyncCopyFromUri(new Uri(turl.fullUri));
                             await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, $"{_cntr}", new List<string> { thum });
                             await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, $"{_cntr}", new List<string> { thum });
                         }
                         }
                         catch (Exception)
                         catch (Exception)
@@ -453,7 +453,7 @@ namespace TEAMModelOS.Controllers
                     }
                     }
                     string targetName = item.Name.Replace(px, newName);
                     string targetName = item.Name.Replace(px, newName);
                     var url = _azureStorage.GetBlobSAS($"{_cntr}", item.Name, BlobSasPermissions.Read | BlobSasPermissions.List);
                     var url = _azureStorage.GetBlobSAS($"{_cntr}", item.Name, BlobSasPermissions.Read | BlobSasPermissions.List);
-                    bcc.GetBlobClient(targetName).SyncCopyFromUri(new Uri(url));
+                    bcc.GetBlobClient(targetName).SyncCopyFromUri(new Uri(url.fullUri));
 
 
                 };
                 };
                 await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, $"{_cntr}", new List<string> { px });
                 await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, $"{_cntr}", new List<string> { px });

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 28 - 28
TEAMModelOS/appsettings.Development.json