yuanrf před 2 týdny
rodič
revize
79294c4a57

+ 185 - 5
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -1,4 +1,4 @@
-using Aspose.Cells;
+using Aspose.Cells;
 using Aspose.Words;
 using Aspose.Words.Drawing;
 using Aspose.Words.Tables;
@@ -13850,7 +13850,7 @@ FROM
                 return Ok(jw);
             }
 
-            Console.WriteLine(chat);
+            //Console.WriteLine(chat);
 
             // var chatResult = await _doubaoService.CompleteChatAsync(new List<DouBaoChatMessage> { new DouBaoChatMessage { Role = DouBaoRole.user, Content = chat } });
             var apiResp = await _deepSeekService.ChatAsync(chat, false, "deepseek-reasoner", 0.7f, 60000);
@@ -13875,6 +13875,180 @@ FROM
         }
 
 
+        /// <summary>
+        /// 请示文件内容生成 (AI 流式输出)。响应体为 NDJSON:每行 <c>{"phase":"reasoning"|"content","text":"..."}</c>,分别对应思考过程与正式结果。
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost]
+        public async Task<IActionResult> BusinessInvitationInstructionsFileContentStream(BusinessInvitationInstructionsFileDto dto)
+        {
+            if (dto.Diid < 1)
+            {
+                Response.ContentType = "text/plain; charset=utf-8";
+                await Response.WriteAsync("团组Id不能为空!");
+                return new EmptyResult();
+            }
+
+            var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.Id == dto.Diid);
+            if (groupInfo == null)
+            {
+                Response.ContentType = "text/plain; charset=utf-8";
+                await Response.WriteAsync("团组信息不存在!");
+                return new EmptyResult();
+            }
+
+            var DeleClientList = _sqlSugar.Queryable<Grp_TourClientList>()
+                     .LeftJoin<Crm_DeleClient>((tcl, dc) => tcl.ClientId == dc.Id && dc.IsDel == 0)
+                     .LeftJoin<Crm_CustomerCompany>((tcl, dc, cc) => dc.CrmCompanyId == cc.Id && dc.IsDel == 0)
+                     .Where((tcl, dc, cc) => tcl.IsDel == 0 && tcl.DiId == dto.Diid)
+                     .Select((tcl, dc, cc) => new ClientInfo
+                     {
+                         LastName = dc.LastName,
+                         FirstName = dc.FirstName,
+                         Sex = dc.Sex,
+                         Birthday = dc.BirthDay,
+                         Company = cc.CompanyFullName,
+                         Job = dc.Job
+                     })
+                     .ToList();
+            foreach (var item in DeleClientList)
+            {
+                EncryptionProcessor.DecryptProperties(item);
+            }
+
+            if (!DeleClientList.Any())
+            {
+                Response.ContentType = "text/plain; charset=utf-8";
+                await Response.WriteAsync("团组客户信息不存在!");
+                return new EmptyResult();
+            }
+
+            var travelList = await _sqlSugar.Queryable<Grp_ApprovalTravel>()
+              .LeftJoin<Grp_ApprovalTravelDetails>((x, a) => x.Id == a.ParentId && a.IsDel == 0)
+              .Where((x, a) => x.IsDel == 0 && x.Diid == dto.Diid)
+              .Select((x, a) => new
+              {
+                  日期 = x.Date,
+                  具体时间 = a.Time,
+                  行程安排 = a.Details,
+                  数据id = a.Id,
+              })
+              .ToListAsync();
+
+            if (!travelList.Any())
+            {
+                Response.ContentType = "text/plain; charset=utf-8";
+                await Response.WriteAsync("团组行程信息不存在!");
+                return new EmptyResult();
+            }
+
+            string chat = string.Empty;
+
+            var templateSetting = await _sqlSugar.Queryable<Sys_SetData>().FirstAsync(x => x.Id == 1550 && x.IsDel == 0);
+            if (templateSetting != null && !string.IsNullOrEmpty(templateSetting.Remark))
+            {
+                var stringFormatResp = await GeneralMethod.StringFormatAsync(new StringFormatDto
+                {
+                    FormatTemplate = templateSetting.Remark,
+                    Parameters = new List<string> {
+                            JsonConvert.SerializeObject(DeleClientList),
+                             JsonConvert.SerializeObject(travelList)
+                        }
+                });
+
+                if (stringFormatResp.Success)
+                {
+                    chat = stringFormatResp.FormattedResult;
+                }
+                else
+                {
+                    Response.ContentType = "text/plain; charset=utf-8";
+                    await Response.WriteAsync("对话内容模板格式化失败! templateSetting.Id: " + templateSetting.Id + ",错误:" + stringFormatResp.Message);
+                    return new EmptyResult();
+                }
+            }
+            else
+            {
+                Response.ContentType = "text/plain; charset=utf-8";
+                await Response.WriteAsync("获取对话内容模板失败! templateSetting.Id: " + (templateSetting?.Id.ToString() ?? "无"));
+                return new EmptyResult();
+            }
+
+            //Console.WriteLine(chat);
+            Response.ContentType = "application/x-ndjson; charset=utf-8";
+            Response.Headers["Cache-Control"] = "no-cache";
+
+            string NdjsonLine(DeepSeekStreamChunk c) => JsonConvert.SerializeObject(new
+            {
+                phase = c.Phase == DeepSeekStreamPhase.Reasoning ? "reasoning" : "content",
+                text = c.Text
+            });
+
+            try
+            {
+                await foreach (var chunk in _deepSeekService.ChatStreamAsync(chat, "deepseek-reasoner", 0.7f, 60000))
+                {
+                    await Response.WriteAsync(NdjsonLine(chunk) + "\n");
+                    await Response.Body.FlushAsync();
+                }
+            }
+            catch (Exception ex)
+            {
+                await Response.WriteAsync(JsonConvert.SerializeObject(new { phase = "error", text = ex.Message }) + "\n");
+            }
+
+            return new EmptyResult();
+        }
+
+        /// <summary>
+        /// 请示文件下载
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost]
+        [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
+        public async Task<IActionResult> BusinessInvitationInstructionsFileDown(BusinessInvitationInstructionsFileDownDto dto)
+        {
+            var jw = JsonView(false);
+            var content = dto.Content;
+            var diid = dto.Diid;
+
+            if (string.IsNullOrEmpty(content))
+            {
+                jw.Msg = "请示文件内容不能为空!";
+                return Ok(jw);
+            }
+
+            var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.Id == diid);
+            if (groupInfo == null)
+            {
+                jw.Msg = "团组信息不存在!";
+                return Ok(jw);
+            }
+
+            string outputPath = $"{AppSettingsHelper.Get("GrpFileBasePath")}/商邀相关文件/{groupInfo.TeamName}请示文件.docx";
+
+            try
+            {
+                string json = content;
+                byte[] wordBytes = JsonToWordHelper.ConvertJsonToWord(json);
+                System.IO.File.WriteAllBytes(outputPath, wordBytes);
+
+                //WordExporter.MarkdownToWord(chatResult, outputPath);
+                string url = outputPath.Replace(AppSettingsHelper.Get("GrpFileBasePath"), AppSettingsHelper.Get("GrpFileBaseUrl") + AppSettingsHelper.Get("GrpFileFtpPath"));
+
+                return Ok(JsonView(true, "请示文件生成成功!", url));
+            }
+            catch (Exception ex)
+            {
+                jw.Msg = "请示文件生成失败!" + ex.Message;
+                return Ok(jw);
+            }
+        }
+
+
+
         // [HttpPost("export-word")]
         // public IActionResult ExportWord([FromBody] string json)
         // {
@@ -14087,13 +14261,19 @@ FROM
         [HttpGet("chat-stream")]
         public async Task ChatStream([FromQuery] string question)
         {
-            Response.ContentType = "text/plain; charset=utf-8";
+            Response.ContentType = "application/x-ndjson; charset=utf-8";
             Response.Headers.CacheControl = "no-cache";
 
+            string NdjsonLine(DeepSeekStreamChunk c) => JsonConvert.SerializeObject(new
+            {
+                phase = c.Phase == DeepSeekStreamPhase.Reasoning ? "reasoning" : "content",
+                text = c.Text
+            });
+
             await foreach (var chunk in _deepSeekService.ChatStreamAsync(question))
             {
-                await Response.WriteAsync(chunk);
-                await Response.Body.FlushAsync(); // 尽快把本段发给客户端(视反向代理是否缓冲而定)
+                await Response.WriteAsync(NdjsonLine(chunk) + "\n");
+                await Response.Body.FlushAsync();
             }
         }
 

+ 22 - 0
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/DeepSeekModels.cs

@@ -22,6 +22,28 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
         public string BaseAddress { get; set; }
     }
 
+    /// <summary>
+    /// 流式片段阶段(deepseek-reasoner:先 reasoning 再 content;deepseek-chat 通常仅有 Content)
+    /// </summary>
+    public enum DeepSeekStreamPhase
+    {
+        /// <summary>思考过程(对应 delta.reasoning_content)</summary>
+        Reasoning,
+
+        /// <summary>正式输出(对应 delta.content)</summary>
+        Content
+    }
+
+    /// <summary>
+    /// 单段流式输出,便于前端区分思考与结果
+    /// </summary>
+    public class DeepSeekStreamChunk
+    {
+        public DeepSeekStreamPhase Phase { get; set; }
+
+        public string Text { get; set; } = "";
+    }
+
     /// <summary>
     /// 文件上传到DeepSeek API的请求参数
     /// </summary>

+ 17 - 10
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/DeepSeekService.cs

@@ -389,14 +389,13 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
         }
 
         /// <summary>
-        /// 流式 chat
+        /// 流式 chat;思考与正文分 Phase 产出
         /// </summary>
         /// <param name="question">问题</param>
         /// <param name="model">模型名称</param>
         /// <param name="temperature">温度参数</param>
         /// <param name="maxTokens">最大token数</param>
-        /// <returns>聊天响应</returns>
-        public async IAsyncEnumerable<string> ChatStreamAsync(string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000)
+        public async IAsyncEnumerable<DeepSeekStreamChunk> ChatStreamAsync(string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000)
         {
             var messageContent = new List<object>
             {
@@ -452,7 +451,8 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
                 if (data == "[DONE]")
                     yield break;
 
-                string? deltaText = null;
+                string? reasoningPiece = null;
+                string? contentPiece = null;
                 try
                 {
                     using var jsonDoc = JsonDocument.Parse(data);
@@ -460,19 +460,26 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
                         continue;
                     if (!choices[0].TryGetProperty("delta", out var delta))
                         continue;
-                    if (delta.TryGetProperty("content", out var contentVal))
+
+                    // deepseek-reasoner:思考过程在 reasoning_content,正式输出在 content
+                    if (delta.TryGetProperty("reasoning_content", out var reasoningVal))
                     {
-                        var text = contentVal.GetString();
-                        if (!string.IsNullOrEmpty(text))
-                            deltaText = text;
+                        var r = reasoningVal.GetString();
+                        if (!string.IsNullOrEmpty(r))
+                            reasoningPiece = r;
                     }
+
+                    if (delta.TryGetProperty("content", out var contentVal))
+                        contentPiece = contentVal.GetString();
                 }
                 catch (System.Text.Json.JsonException)
                 {
                 }
 
-                if (deltaText != null)
-                    yield return deltaText;
+                if (!string.IsNullOrEmpty(reasoningPiece))
+                    yield return new DeepSeekStreamChunk { Phase = DeepSeekStreamPhase.Reasoning, Text = reasoningPiece };
+                if (!string.IsNullOrEmpty(contentPiece))
+                    yield return new DeepSeekStreamChunk { Phase = DeepSeekStreamPhase.Content, Text = contentPiece };
             }
         }
 

+ 2 - 3
OASystem/OASystem.Api/OAMethodLib/DeepSeekAPI/IDeepSeekService.cs

@@ -76,14 +76,13 @@ namespace OASystem.API.OAMethodLib.DeepSeekAPI
         Task<ApiResponse> ChatAsync(string question, bool stream = false, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000);
 
         /// <summary>
-        /// 流式 chat
+        /// 流式 chat;每段带 <see cref="DeepSeekStreamPhase"/>,reasoner 模型下思考与正文都会产出。
         /// </summary>
         /// <param name="question">问题</param>
         /// <param name="model">模型名称</param>
         /// <param name="temperature">温度参数</param>
         /// <param name="maxTokens">最大token数</param>
-        /// <returns>聊天响应</returns>
-        IAsyncEnumerable<string> ChatStreamAsync(string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000);
+        IAsyncEnumerable<DeepSeekStreamChunk> ChatStreamAsync(string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000);
 
         /// <summary>
         /// 等待文件处理完成

+ 6 - 0
OASystem/OASystem.Domain/Dtos/Groups/InvitationOfficialActivitiesListDto.cs

@@ -232,6 +232,12 @@ namespace OASystem.Domain.Dtos.Groups
         public int Diid { get; set; }
     }
 
+    public class BusinessInvitationInstructionsFileDownDto
+    {
+        public string Content { get; set; }
+        public int Diid { get; set; }
+    }
+
     public class BusinessEnterpriseContinueWriteDto
     {
         public int Diid { get; set; }