|
|
@@ -2,6 +2,8 @@
|
|
|
using Aspose.Words;
|
|
|
using Aspose.Words.Tables;
|
|
|
using EyeSoft.Extensions;
|
|
|
+using Microsoft.AspNetCore.Http.Features;
|
|
|
+using Newtonsoft.Json.Serialization;
|
|
|
using NodaTime;
|
|
|
using NPOI.SS.Formula.Functions;
|
|
|
using NPOI.SS.UserModel;
|
|
|
@@ -2320,17 +2322,30 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
.Where(x => x.IsDel == 0 && x.InvName == name)
|
|
|
.FirstAsync();
|
|
|
|
|
|
+ var groupInfo = new Grp_DelegationInfo();
|
|
|
+
|
|
|
if (info == null)
|
|
|
{
|
|
|
+ groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.TeamName.Equals(name)).FirstAsync();
|
|
|
+
|
|
|
+ var entry = new EntryInfo() {
|
|
|
+ OriginUnit = groupInfo?.ClientUnit ?? "",
|
|
|
+ TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? ""),
|
|
|
+ };
|
|
|
+
|
|
|
return Ok(JsonView(true, $"暂无数据",new {
|
|
|
Id = 0,
|
|
|
- GroupId= 0,
|
|
|
- InvName = "",
|
|
|
+ GroupId= groupInfo?.Id ?? 0,
|
|
|
+ InvName = name,
|
|
|
AiCrawledDetails = new List<InvitationAIInfo>(),
|
|
|
+ Entry = entry
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
- var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == info.GroupId).FirstAsync();
|
|
|
+ if (info.GroupId != 0)
|
|
|
+ {
|
|
|
+ groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == info.GroupId).FirstAsync();
|
|
|
+ }
|
|
|
// 设置国家、单位默认值
|
|
|
if (string.IsNullOrEmpty(info.EntryInfo.OriginUnit))
|
|
|
{
|
|
|
@@ -2383,6 +2398,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
|
|
|
var localInvDatas = new List<InvitationAIInfo>(); // 本地数据源(商邀资料)
|
|
|
var aiTasks = new List<CountryAIPormptInfo>(); // 记录:国家 -> 需要补齐的数量
|
|
|
+
|
|
|
#region 本地数据源(商邀资料)
|
|
|
|
|
|
var datas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
|
|
|
@@ -2606,6 +2622,235 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 混元AI查询资料(SSE流式推送)
|
|
|
+ /// </summary>
|
|
|
+ [HttpPost]
|
|
|
+ public async Task InvitationAISearchStreamProgress([FromBody] InvitationAISearchDto dto)
|
|
|
+ {
|
|
|
+ // 强制关闭响应缓冲
|
|
|
+ var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
|
|
|
+ if (syncIOFeature != null) syncIOFeature.AllowSynchronousIO = true;
|
|
|
+
|
|
|
+ var response = HttpContext.Response;
|
|
|
+
|
|
|
+ // --- SSE 核心协议头配置 ---
|
|
|
+ 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 message, object data = null)
|
|
|
+ {
|
|
|
+ var settings = new JsonSerializerSettings
|
|
|
+ {
|
|
|
+ ContractResolver = new CamelCasePropertyNamesContractResolver() // 强制小驼峰转换
|
|
|
+ };
|
|
|
+
|
|
|
+ var payload = JsonConvert.SerializeObject(new { progress, message, data }, settings);
|
|
|
+ var bytes = Encoding.UTF8.GetBytes($"data: {payload}\n\n");
|
|
|
+
|
|
|
+ await response.Body.WriteAsync(bytes, 0, bytes.Length);
|
|
|
+ await response.Body.FlushAsync(); // 立即清空缓冲区发送
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await SendStep(5, "正在加载配置参数...");
|
|
|
+
|
|
|
+ #region 1. 参数与权限验证
|
|
|
+ var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
|
|
|
+ if (invAiInfo?.EntryInfo == null)
|
|
|
+ {
|
|
|
+ await SendStep(-1, "未找到有效的关键字配置信息。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var entryInfo = invAiInfo.EntryInfo;
|
|
|
+ var operatorName = await _sqlSugar.Queryable<Sys_Users>()
|
|
|
+ .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
|
|
|
+ .Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ await SendStep(20, "正在翻阅本地典籍,执行高性能并行解密...");
|
|
|
+
|
|
|
+ #region 2. 本地数据并行解密 (Performance Boost)
|
|
|
+ var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
|
|
|
+ .Where(x => x.IsDel == 0)
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ // 使用 PLINQ 并行解密,极大提升解密密集型任务速度
|
|
|
+ var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
|
|
|
+ {
|
|
|
+ Guid = Guid.NewGuid().ToString("N"),
|
|
|
+ Source = 0,
|
|
|
+ Region = AesEncryptionHelper.Decrypt(item.Country),
|
|
|
+ NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
|
|
|
+ Address = AesEncryptionHelper.Decrypt(item.Address),
|
|
|
+ Scope = AesEncryptionHelper.Decrypt(item.Field),
|
|
|
+ Contact = AesEncryptionHelper.Decrypt(item.Contact),
|
|
|
+ Phone = AesEncryptionHelper.Decrypt(item.Tel),
|
|
|
+ Email = AesEncryptionHelper.Decrypt(item.Email),
|
|
|
+ OperatedAt = DateTime.Now,
|
|
|
+ Operator = operatorName,
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ // 筛选目标国家并计算 AI 缺口
|
|
|
+ var localInvDatas = new List<InvitationAIInfo>();
|
|
|
+ var aiTasks = new List<CountryAIPormptInfo>();
|
|
|
+ int targetPerCountry = entryInfo.NeedCount;
|
|
|
+
|
|
|
+ foreach (var countryName in entryInfo.TargetCountry)
|
|
|
+ {
|
|
|
+ var countryMatched = allDecrypted.Where(x => x.Region == countryName).Take(targetPerCountry).ToList();
|
|
|
+ localInvDatas.AddRange(countryMatched);
|
|
|
+
|
|
|
+ int gap = targetPerCountry - countryMatched.Count;
|
|
|
+ if (gap > 0) aiTasks.Add(new() { Country = countryName, Count = gap });
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region 3. 混元 AI 远程炼金
|
|
|
+ var hunyuanAIInvDatas = new List<InvitationAIInfo>();
|
|
|
+ if (aiTasks.Any())
|
|
|
+ {
|
|
|
+ await SendStep(60, $"Hunyuan AI 正在跨境深度检索 {aiTasks.Count} 个国家的邀请单位信息...");
|
|
|
+
|
|
|
+ string question = BuildHunyuanPrompt(aiTasks, entryInfo); // 抽离 Prompt 构造
|
|
|
+ string aiRawResponse = string.Empty;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ aiRawResponse = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
|
|
|
+
|
|
|
+ // 清理 Markdown 代码块包裹
|
|
|
+ string cleanJson = aiRawResponse.Trim();
|
|
|
+ if (cleanJson.StartsWith("```json")) cleanJson = cleanJson[7..^3].Trim();
|
|
|
+ else if (cleanJson.StartsWith("```")) cleanJson = cleanJson[3..^3].Trim();
|
|
|
+
|
|
|
+ var aiParsed = JsonConvert.DeserializeObject<List<InvitationAIInfo>>(cleanJson);
|
|
|
+ if (aiParsed != null)
|
|
|
+ {
|
|
|
+ hunyuanAIInvDatas = aiParsed.Select(x => {
|
|
|
+ x.Guid = Guid.NewGuid().ToString("N");
|
|
|
+ x.Source = 1; // AI 来源
|
|
|
+ x.Operator = operatorName;
|
|
|
+ x.OperatedAt = DateTime.Now;
|
|
|
+ return x;
|
|
|
+ }).ToList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "Hunyuan AI 接口调用或解析失败");
|
|
|
+ await SendStep(60, "警告:Hunyuan AI 检索部分失败,将仅展示本地数据。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ await SendStep(90, "执行数据合并与持久化...");
|
|
|
+
|
|
|
+ #region 4. 数据合并与入库
|
|
|
+ var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
|
|
|
+ invAiInfo.AiCrawledDetails = finalResult;
|
|
|
+
|
|
|
+ var updateSuccess = await _sqlSugar.Updateable(invAiInfo).ExecuteCommandAsync() > 0;
|
|
|
+ if (!updateSuccess)
|
|
|
+ {
|
|
|
+ await SendStep(-1, "数据库更新异常。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ // 5. 最终推送:带上全量结果
|
|
|
+ await SendStep(100, "操作成功!资料已全部就绪。", new
|
|
|
+ {
|
|
|
+ invAiInfo.Id,
|
|
|
+ invAiInfo.InvName,
|
|
|
+ AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList(),
|
|
|
+ Entry = invAiInfo.EntryInfo
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, "SSE 发生不可预知异常");
|
|
|
+ await SendStep(-1, $"SSE Error:{ex.Message}");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ // 确保最后一次冲刷缓冲区
|
|
|
+ await HttpContext.Response.Body.FlushAsync();
|
|
|
+
|
|
|
+ // 此时方法结束,ASP.NET Core 会自动处理连接关闭
|
|
|
+ _logger.LogInformation("SSE 通道已安全关闭");
|
|
|
+
|
|
|
+ // 直接完成响应流
|
|
|
+ await response.CompleteAsync();
|
|
|
+
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ private string BuildHunyuanPrompt(List<CountryAIPormptInfo> tasks, EntryInfo entryInfo)
|
|
|
+ {
|
|
|
+ // 保持你原有的高质量 Prompt 结构
|
|
|
+ return @$"# [SYSTEM_CONTEXT]
|
|
|
+- Role: Senior Business Consultant (Global Supply Chain & Cross-border Trade Expert)
|
|
|
+- Framework: S.P.A.R. (Situation-Problem-Action-Result)
|
|
|
+- Target Architecture: .NET 6 DTO Compatible Interface
|
|
|
+
|
|
|
+# [INPUT_CONFIG]
|
|
|
+- Tasks: {{{JsonConvert.SerializeObject(tasks)}}}
|
|
|
+- OriginUnit: {{{entryInfo.OriginUnit}}}
|
|
|
+- Objective: {{{entryInfo.Objective}}}
|
|
|
+- OtherConstraints: {{{entryInfo.OtherConstraints}}}
|
|
|
+# [ROLE_DEFINITION]
|
|
|
+你是一位精通全球跨境经贸、海外园区政策及 Tasks 中的国家本地准入法规的【顶级商务咨询顾问】。你擅长通过“产业链对等原则(Chain-Parity Principle)”为跨国企业精准匹配具备落地价值的合作伙伴。
|
|
|
+
|
|
|
+# [BUSINESS_LOGIC_CoT]
|
|
|
+在生成结果前,请严格执行以下思维链拆解:
|
|
|
+1. **ScopePartitioning**: 遍历 Tasks 中的每个国家。
|
|
|
+1. **EntityProfiling**: 识别 {{OriginUnit}} 的核心生态位(如:Supply/Demand/Capital/Logistics)。
|
|
|
+2. **ParityMatching**: 针对每个国家,检索境内业务闭环对等机构。(例如:若 Origin 为分销,则 Target 为源头工厂/种植园)。
|
|
|
+3. **RegulatoryCheck**: 验证目标机构的合规性(如:GACC 备案、SPS 协议、或当地政府特许经营权)。
|
|
|
+4. **DataSynthesis**: 必须严格根据每个国家对应的 Count 生成数据条数。针对无法直接获取的动态(如 PostUrl),优先检索其官网 News 频道或 LinkedIn 企业号。
|
|
|
+
|
|
|
+# [CONSTRAINTS_&_STANDARDS]
|
|
|
+- **NamingConvention**: 所有 JSON Key 必须严格遵循 **PascalCase**(例如:`UnitNameCn` 而非 `unit_name_cn`)。
|
|
|
+- **DataIntegrity**:
|
|
|
+ - 总条数必须等于 Tasks 中所有 Count 的总和。
|
|
|
+ - 优先级:Core (核心机构) > Backup (关联替代机构)。
|
|
|
+- **ValidationRules**:
|
|
|
+ - `Phone`: 必须包含 Tasks 中的国家国际区号(如 +856, +66 等)。
|
|
|
+ - `IntgAdvice`: 必须包含“对等性分析”,解释该机构如何与 {{OriginUnit}} 形成业务闭环(50-100字)。
|
|
|
+ - `Status`: 若字段确实无法获取,统一填充 'N/A',禁止编造。
|
|
|
+- **Safety**: 确保推荐机构不涉及敏感黑名单或已破产企业。
|
|
|
+
|
|
|
+# [INFORMATION_SCHEMA]
|
|
|
+请将结果填充至以下结构的 JSON 数组中:
|
|
|
+- Region:国家(必须与 Tasks 中的 Country 完全匹配)
|
|
|
+- NameCn: 单位名称(中文)
|
|
|
+- NameEn: 单位名称(英文)
|
|
|
+- Address: 详细地理位置(含省市区街道)
|
|
|
+- Scope: 经营范围(需强调其出口配额、生产能力或行业地位)
|
|
|
+- Contact: 联系人姓名及职务
|
|
|
+- Phone: 拨打全号(含区号)
|
|
|
+- Email: 商务联络邮箱
|
|
|
+- SiteUrl: 官方网站或权威社媒主页
|
|
|
+- PostUrl: 近一年内的商务动态/新闻链接
|
|
|
+- RecLevel: 推荐等级(枚举值:Core, Backup)
|
|
|
+- IntgAdvice: 对接深度建议(基于产业链交合、互补逻辑)
|
|
|
+
|
|
|
+# [OUTPUT_PROTOCOL]
|
|
|
+- 仅输出一个标准的 JSON Array 字符串。
|
|
|
+- 严禁任何 Markdown 说明文字、代码块之外的解释或开场白。
|
|
|
+- 确保 JSON 语法在 .NET `JsonSerializer.Deserialize` 下可直接解析。
|
|
|
+
|
|
|
+# [EXECUTION]
|
|
|
+根据以上配置,开始生成。"; ;
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 商邀资料AI 设置词条
|
|
|
/// </summary>
|
|
|
@@ -2661,11 +2906,12 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
CreateUserId = dto.CurrUserId
|
|
|
};
|
|
|
|
|
|
- var insert = await _sqlSugar.Insertable(dataInfo).ExecuteCommandAsync();
|
|
|
+ var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
|
|
|
if (insert < 1)
|
|
|
{
|
|
|
return Ok(JsonView(false, $"词条信息新增失败!"));
|
|
|
}
|
|
|
+ dataInfo.Id = insert;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -2690,6 +2936,70 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 设置复选框选中 批量
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="dto"></param>
|
|
|
+ /// <returns></returns>
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI 设置复选框选中 批量 (支持传入空数组进行重置)
|
|
|
+ /// </summary>
|
|
|
+ [HttpPost]
|
|
|
+ public async Task<IActionResult> InvitationAISetChecked([FromBody] InvitationAISetCheckedDto dto)
|
|
|
+ {
|
|
|
+ // 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() ?? "-";
|
|
|
+
|
|
|
+ // 4. 单次遍历完成 重置 + 更新
|
|
|
+ // 如果 guidSet 为空,循环会将所有项的 IsChecked 置为 false
|
|
|
+ foreach (var item in invAiInfo.AiCrawledDetails)
|
|
|
+ {
|
|
|
+ if (guidSet.Contains(item.Guid))
|
|
|
+ {
|
|
|
+ item.IsChecked = true;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ item.IsChecked = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ item.OperatedAt = now;
|
|
|
+ item.Operator = operatorName;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 排序逻辑
|
|
|
+ // 即使是全量取消选中,也可以按最后操作时间排序,让最近变动的项在前
|
|
|
+ invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
|
|
|
+ .OrderByDescending(x => x.OperatedAt)
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ // 6. 提交数据库
|
|
|
+ // 注意:SqlSugar 的 UpdateColumns 会序列化整个对象列表并覆盖数据库字段
|
|
|
+ await _sqlSugar.Updateable(invAiInfo)
|
|
|
+ .UpdateColumns(x => x.AiCrawledDetails)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+
|
|
|
+ return Ok(JsonView(true, guidSet.Any() ? "设置成功" : "已全部取消选中"));
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 商邀资料AI 保存
|
|
|
/// </summary>
|
|
|
@@ -2716,6 +3026,14 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
|
|
|
var editInfo = dto.AiCrawledDetail;
|
|
|
|
|
|
+ // 如果 Guid 为空,说明是新增数据,需要生成新的 Guid
|
|
|
+ if (string.IsNullOrEmpty(editInfo.Guid))
|
|
|
+ {
|
|
|
+ editInfo.Guid = Guid.NewGuid().ToString("N");
|
|
|
+ editInfo.Source = 2; // 标记为手动新增数据
|
|
|
+ editInfo.OperatedAt = DateTime.Now;
|
|
|
+ }
|
|
|
+
|
|
|
var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
editInfo.Operator = opUserName;
|
|
|
editInfo.OperatedAt = DateTime.Now;
|
|
|
@@ -2769,6 +3087,78 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 商邀资料AI AI查询出的数据添加值商邀资料库
|
|
|
+ /// </summary>
|
|
|
+ /// <returns></returns>
|
|
|
+ [HttpPost]
|
|
|
+ [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
|
|
|
+ public async Task<IActionResult> InvitationAIInsertResource([FromBody] InvitationAIInsertResourceDto dto)
|
|
|
+ {
|
|
|
+ // 基础校验
|
|
|
+ if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
|
|
|
+ return Ok(JsonView(false, "参数不完整,请选择有效的单位数据"));
|
|
|
+
|
|
|
+ // 获取主记录
|
|
|
+ var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
|
|
|
+ .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
|
|
|
+
|
|
|
+ if (dataInfo?.AiCrawledDetails == null)
|
|
|
+ return Ok(JsonView(false, "当前数据信息不存在!"));
|
|
|
+
|
|
|
+ // 筛选出待转正的 AI 数据 (使用 HashSet 优化匹配)
|
|
|
+ var guidSet = dto.Guids.ToHashSet();
|
|
|
+ var targetAiDetails = dataInfo.AiCrawledDetails
|
|
|
+ .Where(x => x.Source == 1 && guidSet.Contains(x.Guid))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ if (!targetAiDetails.Any())
|
|
|
+ return Ok(JsonView(false, "未找到符合条件的 AI 获取信息,无法重复添加或来源错误!"));
|
|
|
+
|
|
|
+ // 准备入库数据(批量构建)
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
|
|
|
+ var rawLocalDatas = targetAiDetails.Select(item => {
|
|
|
+ // 修改内存中的状态为本地数据
|
|
|
+ item.Source = 0;
|
|
|
+ item.OperatedAt = now;
|
|
|
+ item.Operator = opUserName;
|
|
|
+
|
|
|
+ return new Res_InvitationOfficialActivityData()
|
|
|
+ {
|
|
|
+ // 批量加密操作
|
|
|
+ Country = AesEncryptionHelper.Encrypt(item.Region),
|
|
|
+ UnitName = AesEncryptionHelper.Encrypt(item.NameCn),
|
|
|
+ Address = AesEncryptionHelper.Encrypt(item.Address),
|
|
|
+ Field = AesEncryptionHelper.Encrypt(item.Scope),
|
|
|
+ Contact = AesEncryptionHelper.Encrypt(item.Contact),
|
|
|
+ Tel = AesEncryptionHelper.Encrypt(item.Phone),
|
|
|
+ Email = AesEncryptionHelper.Encrypt(item.Email),
|
|
|
+ UnitWeb = AesEncryptionHelper.Encrypt(item.SiteUrl),
|
|
|
+ LastUpdateUserId = dto.CurrUserId,
|
|
|
+ LastUpdateTime = now,
|
|
|
+ CreateUserId = dto.CurrUserId,
|
|
|
+ CreateTime = now
|
|
|
+ };
|
|
|
+ }).ToList();
|
|
|
+
|
|
|
+ // 开启事务执行双表更新
|
|
|
+ var result = await _sqlSugar.UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ // 插入本地资料库
|
|
|
+ await _sqlSugar.Insertable(rawLocalDatas).ExecuteCommandAsync();
|
|
|
+
|
|
|
+ // 更新 AI 主表的状态(全量覆盖 JSON 列)
|
|
|
+ await _sqlSugar.Updateable(dataInfo)
|
|
|
+ .UpdateColumns(x => x.AiCrawledDetails)
|
|
|
+ .ExecuteCommandAsync();
|
|
|
+ });
|
|
|
+
|
|
|
+ return Ok(result.IsSuccess
|
|
|
+ ? JsonView(true, "成功添加至资料库并更新来源状态")
|
|
|
+ : JsonView(false, $"操作失败:{result.ErrorMessage}"));
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 商邀资料AI 混元AI续写
|
|
|
/// </summary>
|
|
|
@@ -2971,6 +3361,133 @@ 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
|
|
|
+ });
|
|
|
+ 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, "Hunyuan 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, $"Hunyuan 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>
|
|
|
@@ -3422,6 +3939,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, "Hunyuan AI 正在分析考察团组背景与访问意图...");
|
|
|
+
|
|
|
+ // 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, $"Hunyuan AI 正在为 {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>
|