|
|
@@ -2638,7 +2638,6 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
response.Headers.Add("Content-Type", "text/event-stream");
|
|
|
response.Headers.Add("Cache-Control", "no-cache");
|
|
|
response.Headers.Add("Connection", "keep-alive");
|
|
|
- // 关键:禁用代理缓存,确保数据实时流向前端
|
|
|
response.Headers.Add("X-Accel-Buffering", "no");
|
|
|
|
|
|
// 定义流式推送匿名函数
|
|
|
@@ -2674,8 +2673,6 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
.Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
#endregion
|
|
|
|
|
|
- await Task.Delay(2000);
|
|
|
-
|
|
|
await SendStep(20, "正在翻阅本地典籍,执行高性能并行解密...");
|
|
|
|
|
|
#region 2. 本地数据并行解密 (Performance Boost)
|
|
|
@@ -2944,60 +2941,63 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
/// </summary>
|
|
|
/// <param name="dto"></param>
|
|
|
/// <returns></returns>
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 设置复选框选中 批量 (支持传入空数组进行重置)
|
|
|
+ /// </summary>
|
|
|
[HttpPost]
|
|
|
public async Task<IActionResult> InvitationAISetChecked([FromBody] InvitationAISetCheckedDto dto)
|
|
|
{
|
|
|
- // 参数校验
|
|
|
- if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
|
|
|
+ // 1. 基础参数校验 (注意:这里移除了对 Guids.Any() 的强校验,允许空集合)
|
|
|
+ if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null)
|
|
|
return Ok(JsonView(false, "请求参数不完整"));
|
|
|
|
|
|
- // 获取主记录
|
|
|
+ // 2. 获取主记录
|
|
|
var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
|
|
|
.FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
|
|
|
|
|
|
if (invAiInfo?.AiCrawledDetails == null || !invAiInfo.AiCrawledDetails.Any())
|
|
|
return Ok(JsonView(false, "数据不存在或集合为空"));
|
|
|
|
|
|
- // 准备更新所需数据
|
|
|
+ // 3. 准备更新所需数据
|
|
|
var guidSet = dto.Guids.ToHashSet();
|
|
|
var now = DateTime.Now;
|
|
|
+
|
|
|
+ // 获取操作人姓名
|
|
|
string operatorName = await _sqlSugar.Queryable<Sys_Users>()
|
|
|
- .Where(x => x.Id == dto.CurrUserId)
|
|
|
- .Select(x => x.CnName)
|
|
|
- .FirstAsync() ?? "-";
|
|
|
+ .Where(x => x.Id == dto.CurrUserId)
|
|
|
+ .Select(x => x.CnName)
|
|
|
+ .FirstAsync() ?? "-";
|
|
|
|
|
|
- // 单次遍历完成 重置 + 更新
|
|
|
- bool hasMatch = false;
|
|
|
+ // 4. 单次遍历完成 重置 + 更新
|
|
|
+ // 如果 guidSet 为空,循环会将所有项的 IsChecked 置为 false
|
|
|
foreach (var item in invAiInfo.AiCrawledDetails)
|
|
|
{
|
|
|
if (guidSet.Contains(item.Guid))
|
|
|
{
|
|
|
item.IsChecked = true;
|
|
|
- item.OperatedAt = now;
|
|
|
- item.Operator = operatorName;
|
|
|
- hasMatch = true;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
item.IsChecked = false;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (!hasMatch)
|
|
|
- return Ok(JsonView(false, "未匹配到任何有效的邀请项"));
|
|
|
+ item.OperatedAt = now;
|
|
|
+ item.Operator = operatorName;
|
|
|
+ }
|
|
|
|
|
|
- // 排序逻辑(按需保留:仅当需要置顶显示选中项时开启)
|
|
|
+ // 5. 排序逻辑
|
|
|
+ // 即使是全量取消选中,也可以按最后操作时间排序,让最近变动的项在前
|
|
|
invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
|
|
|
.OrderByDescending(x => x.OperatedAt)
|
|
|
.ToList();
|
|
|
|
|
|
- // 提交数据库
|
|
|
- // 注意:UpdateColumns 会序列化整个 List 覆盖原字段
|
|
|
- int rowCount = await _sqlSugar.Updateable(invAiInfo)
|
|
|
+ // 6. 提交数据库
|
|
|
+ // 注意:SqlSugar 的 UpdateColumns 会序列化整个对象列表并覆盖数据库字段
|
|
|
+ await _sqlSugar.Updateable(invAiInfo)
|
|
|
.UpdateColumns(x => x.AiCrawledDetails)
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
- return Ok(JsonView(true, "设置成功"));
|
|
|
+ return Ok(JsonView(true, guidSet.Any() ? "设置成功" : "已全部取消选中"));
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -3353,6 +3353,134 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
return Ok(JsonView(false, $"AI续写失败!"));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 混元AI续写(SSE流式推送)
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="dto"></param>
|
|
|
+ /// <returns></returns>
|
|
|
+ [HttpPost]
|
|
|
+ public async Task InvitationAICompleteTextStream([FromBody] InvitationAICompleteTextDto dto)
|
|
|
+ {
|
|
|
+ var response = HttpContext.Response;
|
|
|
+ response.Headers.Add("Content-Type", "text/event-stream");
|
|
|
+ response.Headers.Add("Cache-Control", "no-cache");
|
|
|
+ response.Headers.Add("Connection", "keep-alive");
|
|
|
+ response.Headers.Add("X-Accel-Buffering", "no"); // 禁用 Nginx 缓存
|
|
|
+
|
|
|
+ // 内部推送工具
|
|
|
+ async Task SendStep(int progress, string msg, object data = null)
|
|
|
+ {
|
|
|
+ var payload = JsonConvert.SerializeObject(new
|
|
|
+ {
|
|
|
+ progress,
|
|
|
+ message = msg,
|
|
|
+ data,
|
|
|
+ operatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
|
|
+ });
|
|
|
+ await response.WriteAsync($"data: {payload}\n\n");
|
|
|
+ await response.Body.FlushAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 初始化检查 (Progress: 5%)
|
|
|
+ var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
|
|
|
+ if (invAiInfo?.EntryInfo == null)
|
|
|
+ {
|
|
|
+ await SendStep(-1, "请先设置关键字信息!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await SendStep(10, "任务初始化完成,正在计算补齐缺口...");
|
|
|
+
|
|
|
+ // 任务拆解逻辑
|
|
|
+ var entryInfo = invAiInfo.EntryInfo;
|
|
|
+ var aiTasks = new List<CountryAIPormptInfo>();
|
|
|
+ int targetPerCountry = entryInfo.NeedCount;
|
|
|
+
|
|
|
+ foreach (var countryName in entryInfo.TargetCountry)
|
|
|
+ {
|
|
|
+ var countryDataCount = invAiInfo.AiCrawledDetails.Count(x => x.Region == countryName);
|
|
|
+ int aiNeedCount = targetPerCountry - countryDataCount;
|
|
|
+ if (aiNeedCount > 0) aiTasks.Add(new() { Country = countryName, Count = aiNeedCount });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!aiTasks.Any())
|
|
|
+ {
|
|
|
+ await SendStep(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 准备 AI Prompt (Progress: 20%)
|
|
|
+ var existingNames = new HashSet<string>(invAiInfo.AiCrawledDetails.Where(x => x.Source == 1).Select(x => x.NameCn));
|
|
|
+ string promptOther = $"请基于以下已存在的名称列表进行推荐,避免重复:{string.Join(", ", existingNames)}。{entryInfo.OtherConstraints}";
|
|
|
+ entryInfo.OtherConstraints = promptOther; // 将去重提示注入 entryInfo,确保 Prompt 构建时包含该信息
|
|
|
+
|
|
|
+ // 构建 Question
|
|
|
+ string question = BuildHunyuanPrompt(aiTasks, entryInfo);
|
|
|
+
|
|
|
+ await SendStep(30, "AI 正在深度检索跨境商邀数据,请稍候...");
|
|
|
+
|
|
|
+ // 调用 AI (Progress: 30% - 80%)
|
|
|
+ // 注意:这里如果能换成流式接口(ChatCompletionsStream)会更好,
|
|
|
+ // 如果依然用非流式,这里会有一段较长的等待。
|
|
|
+ string aiRawResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
|
|
|
+
|
|
|
+ await SendStep(85, "数据已捕获,正在进行格式校验与去重...");
|
|
|
+
|
|
|
+ // 5. 解析与清洗数据
|
|
|
+ var hunyuanAIInvDatas = ProcessAIResponse(aiRawResponse);
|
|
|
+ string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
+
|
|
|
+ foreach (var x in hunyuanAIInvDatas)
|
|
|
+ {
|
|
|
+ x.Guid = Guid.NewGuid().ToString("N");
|
|
|
+ x.Source = 1;
|
|
|
+ x.Operator = operatorName;
|
|
|
+ x.OperatedAt = DateTime.Now;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 数据库操作 (Progress: 95%)
|
|
|
+ invAiInfo.AiCrawledDetails.AddRange(hunyuanAIInvDatas);
|
|
|
+ invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
|
|
|
+
|
|
|
+ var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ if (update > 0)
|
|
|
+ {
|
|
|
+ await SendStep(100, $"AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据", invAiInfo.AiCrawledDetails);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ await SendStep(-1, "数据库更新失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "SSE 续写异常");
|
|
|
+ await SendStep(-1, $"炼金炸炉:{ex.Message}");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ await response.Body.FlushAsync();
|
|
|
+ await response.CompleteAsync(); // 彻底关闭连接
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<InvitationAIInfo> ProcessAIResponse(string response)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(response)) return new List<InvitationAIInfo>();
|
|
|
+ string cleanJson = response.Trim();
|
|
|
+ if (cleanJson.Contains("```json"))
|
|
|
+ {
|
|
|
+ cleanJson = Regex.Match(cleanJson, @"```json([\s\S]*?)```").Groups[1].Value.Trim();
|
|
|
+ }
|
|
|
+ else if (cleanJson.Contains("```"))
|
|
|
+ {
|
|
|
+ cleanJson = Regex.Match(cleanJson, @"```([\s\S]*?)```").Groups[1].Value.Trim();
|
|
|
+ }
|
|
|
+ return JsonConvert.DeserializeObject<List<InvitationAIInfo>>(cleanJson) ?? new List<InvitationAIInfo>();
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 商邀资料AI 文件生成(基于混元AI已爬取数据进行格式化输出,供用户下载使用)
|
|
|
/// </summary>
|
|
|
@@ -3804,6 +3932,209 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
return Ok(JsonView(true, msgSb.ToString()));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 生成邮件(SSE流式推送)
|
|
|
+ /// </summary>
|
|
|
+ /// <returns></returns>
|
|
|
+ [HttpPost]
|
|
|
+ public async Task InvitationAIGenerateEmailStream([FromBody] InvitationAIGenerateEmailDto dto)
|
|
|
+ {
|
|
|
+ var response = HttpContext.Response;
|
|
|
+ response.Headers.Add("Content-Type", "text/event-stream");
|
|
|
+ response.Headers.Add("Cache-Control", "no-cache");
|
|
|
+ response.Headers.Add("Connection", "keep-alive");
|
|
|
+ response.Headers.Add("X-Accel-Buffering", "no");
|
|
|
+
|
|
|
+ async Task SendStep(int progress, string msg, object data = null)
|
|
|
+ {
|
|
|
+ var payload = JsonConvert.SerializeObject(new
|
|
|
+ {
|
|
|
+ progress,
|
|
|
+ message = msg,
|
|
|
+ data,
|
|
|
+ operatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
|
|
+ });
|
|
|
+ await response.WriteAsync($"data: {payload}\n\n");
|
|
|
+ await response.Body.FlushAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 1. 基础校验与资源加载 (Progress: 5%)
|
|
|
+ if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
|
|
|
+ {
|
|
|
+ await SendStep(-1, "请求参数不完整,请选择单位");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
|
|
|
+ if (invAiInfo?.AiCrawledDetails == null)
|
|
|
+ {
|
|
|
+ await SendStep(-1, "基础商邀资料不存在");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await SendStep(10, "正在分析考察团组背景与访问意图...");
|
|
|
+
|
|
|
+ // 2. 准备 AI 上下文数据 (Progress: 15%)
|
|
|
+ var clientInfoSources = invAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
|
|
|
+ var clientInfosForAI = clientInfoSources.Select(x => new AICreateEmailInfo()
|
|
|
+ {
|
|
|
+ Guid = x.Guid,
|
|
|
+ NameCn = x.NameCn,
|
|
|
+ Scope = x.Scope
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
|
|
|
+ .Where(x => x.IsDel == 0 && x.Id == invAiInfo.GroupId)
|
|
|
+ .Select(x => new { x.TeamName, x.VisitPurpose, x.VisitDate })
|
|
|
+ .FirstAsync();
|
|
|
+
|
|
|
+ string operatorName = await _sqlSugar.Queryable<Sys_Users>()
|
|
|
+ .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
+
|
|
|
+ // 3. 构建 Prompt 并调用 AI (Progress: 25%)
|
|
|
+ await SendStep(30, $"首席联络官已就位,正在为 {clientInfosForAI.Count} 家单位撰写定制化正式邮件...");
|
|
|
+
|
|
|
+ // 此处沿用你原有的庞大 Prompt 逻辑
|
|
|
+ string pormpt = $@"
|
|
|
+# Role
|
|
|
+你是一位精通国际政企关系的【首席联络官】。你具备极强的行业分析能力,能通过 [SourceEntity] 的名称自动检索并推导其行政职能与行业地位,并以此撰写具有战略高度、语调优雅的正式商务邮件。
|
|
|
+
|
|
|
+# Intelligence Task: Source Profiling
|
|
|
+在生成邮件前,请先执行以下逻辑:
|
|
|
+1. **职能推导**:基于 [SourceEntity] 的名称,自动识别其在所属领域的具体行政职能与政策影响力。
|
|
|
+2. **战略对齐**:将其职能与全球大趋势(如:Sustainable Urbanization, Digital Transformation, Carbon Neutrality)挂钩,作为邮件第二段的叙事背景。
|
|
|
+
|
|
|
+# [RICH_TEXT_STANDARDS_FOR_QUILL]
|
|
|
+- **Prohibited Tags**: 绝对禁止包含 <html>, <body>, <head>, <!DOCTYPE>, <hr>, <style>。
|
|
|
+- **Whitelisted Tags**: 仅允许使用 <h3> (用于主题), <p> (用于段落), <ul>, <li>, <strong>, <br>。
|
|
|
+- **Structural Integrity**:
|
|
|
+ - 严禁出现裸露文本,所有段落必须由 <p> 包裹。
|
|
|
+ - 列表必须使用标准 <ul><li> 嵌套结构。
|
|
|
+- **No Markdown**: 严禁在 Content 中出现 ###, **, --- 等 Markdown 符号。
|
|
|
+
|
|
|
+# Reference Model (Few-Shot Example)
|
|
|
+在生成时,请严格参考以下范例的【语气】、【五段式结构】和【外交辞令】:
|
|
|
+- Subject: Official Study Visit Inquiry – [Delegation Name] ([Date])
|
|
|
+- Para 1: ""On behalf of [SourceEntity], I am writing to formally propose...""
|
|
|
+- Para 2: ""As a pivotal megacity [AI根据Source地位补全]... we recognize [Target]’s leadership in...""
|
|
|
+- Para 3: ""We are particularly keen to explore... [基于Target经营范围推导的3个技术点]...""
|
|
|
+- Para 4: ""We propose a 2–3 day visit... during the week of [Logistics]...""
|
|
|
+- Para 5: ""Enclosed please find the [PascalCase_File]...""
|
|
|
+
|
|
|
+# Task
|
|
|
+根据 [TargetList] 中每个单位的【经营范围】,为 [SourceEntity] 生成独立的英文访问请求邮件。
|
|
|
+
|
|
|
+# Inputs
|
|
|
+- [SourceEntity]: [{invAiInfo.EntryInfo?.OriginUnit ?? ""}]
|
|
|
+- [VisitPurpose]: [{groupInfo?.VisitPurpose}]
|
|
|
+- [TargetList]: [{JsonConvert.SerializeObject(clientInfosForAI)}]
|
|
|
+- [Logistics]: [{groupInfo?.VisitDate.ToString("yyyy-MM-dd")}]
|
|
|
+
|
|
|
+# Execution Logic (Chain of Thought)
|
|
|
+1. **Scope-to-Focus Analysis**:
|
|
|
+ - 深入分析每个 Target 的【经营范围】。
|
|
|
+ - 自动推导 3 个与 [VisitPurpose] 高度对齐的专业考察点(如:Policy Frameworks, Technical Standards, Operational Case Studies)。
|
|
|
+2. **Modular Drafting**:
|
|
|
+ - 必须严格遵循参考范例的 5 段式逻辑。
|
|
|
+ - 针对不同职能属性(监管/技术/运营)动态微调邮件主题(Subject)。
|
|
|
+3. **No Personnel Reference**: 严禁提及“名单、人数、成员、审核中”等任何具体人员信息,保持机构对等对话的高度。
|
|
|
+
|
|
|
+# Constraints & Standards
|
|
|
+- **Tone**: Formal, Strategic, and Executive (庄重、具战略高度、执行力强)。
|
|
|
+- **Naming Protocol**: 附件引用统一使用 `JointVisitAgenda`, `StrategicInquiryBrief` (PascalCase)。
|
|
|
+- **Escaping**: 必须对 HTML 内部的所有双引号进行严格转义(\""),确保 JSON 字符串合法。
|
|
|
+- **Output Format**: 仅输出一个合规的 JSON 数组,不包含任何解释性文字。
|
|
|
+- **Field Mapping**:
|
|
|
+ - `Guid`: 原样保留。
|
|
|
+ - `NameCn`: 原样保留。
|
|
|
+ - `Scope`: 原样保留。
|
|
|
+ - `Subject`: 动态生成的英文主题。
|
|
|
+ - `Content`: 包含 QUILL的英文正文。
|
|
|
+
|
|
|
+# JSON Structure Template
|
|
|
+[
|
|
|
+ {{{{
|
|
|
+ ""Guid"": ""ID"",
|
|
|
+ ""NameCn"": ""Entity Name"",
|
|
|
+ ""Scope"": ""Original Scope Description"",
|
|
|
+ ""Subject"": ""Dynamic English Subject Line"",
|
|
|
+ ""Content"": ""<h3>Subject: ...</h3><p>Dear Leadership Team of <strong>[Target]</strong>,</p><p>On behalf of <strong>[SourceEntity]</strong>, I am writing to propose...</p><ul><li><strong>Focus Area:</strong> Technical analysis of...</li></ul><p>Sincerely,<br><strong>Office of the Chief Liaison</strong><br>[SourceEntity]</p>""
|
|
|
+ }}}}
|
|
|
+]";
|
|
|
+
|
|
|
+ // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
|
|
|
+ string aiResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(pormpt);
|
|
|
+
|
|
|
+ await SendStep(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
|
|
|
+
|
|
|
+ // 4. 解析结果 (Progress: 85%)
|
|
|
+ var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
|
|
|
+ if (!string.IsNullOrWhiteSpace(aiResponse))
|
|
|
+ {
|
|
|
+ // 预处理:过滤 AI 可能返回的 Markdown 标记
|
|
|
+ string cleanJson = aiResponse.Trim();
|
|
|
+ if (cleanJson.StartsWith("```json"))
|
|
|
+ cleanJson = cleanJson.Substring(7, cleanJson.Length - 10).Trim();
|
|
|
+ else if (cleanJson.StartsWith("```"))
|
|
|
+ cleanJson = cleanJson.Substring(3, cleanJson.Length - 6).Trim();
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 解析并注入 Source 标识
|
|
|
+ hunyuanAIEmailDatas = JsonConvert.DeserializeObject<List<AICreateEmailInfo>>(cleanJson);
|
|
|
+
|
|
|
+ }
|
|
|
+ catch (JsonException ex)
|
|
|
+ {
|
|
|
+ // 记录日志并考虑 fallback 策略
|
|
|
+ _logger.LogError(ex, "Hunyuan AI 响应解析失败。原始数据:{Response}", aiResponse);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
|
|
|
+ {
|
|
|
+ await SendStep(-1, "AI 格式解析失败,请尝试重新生成");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 更新数据模型 (Progress: 90%)
|
|
|
+ foreach (var client in clientInfoSources)
|
|
|
+ {
|
|
|
+ var aiEmail = hunyuanAIEmailDatas.FirstOrDefault(x => x.Guid == client.Guid);
|
|
|
+ if (aiEmail != null)
|
|
|
+ {
|
|
|
+ client.EmailInfo.Status = 2; // 已生成
|
|
|
+ client.EmailInfo.EmailTitle = aiEmail.Subject;
|
|
|
+ client.EmailInfo.EmailContent = aiEmail.Content;
|
|
|
+ client.EmailInfo.Operator = operatorName;
|
|
|
+ client.EmailInfo.OperatedAt = DateTime.Now;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 数据库持久化 (Progress: 95%)
|
|
|
+ // 采用增量更新策略,避免直接 Where 过滤掉其他未选中的数据
|
|
|
+ var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
|
|
|
+ if (update > 0)
|
|
|
+ {
|
|
|
+ await SendStep(100, "邮件全部生成完毕并已存入团组资料库", invAiInfo.AiCrawledDetails);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ await SendStep(-1, "数据库写入失败,请联系管理员");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "邮件生成异常");
|
|
|
+ await SendStep(-1, $"生成失败:{ex.Message}");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ await response.CompleteAsync();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 商邀资料AI 邮件保存
|
|
|
/// </summary>
|