AITestController.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. using Flurl.Http.Configuration;
  2. using Humanizer;
  3. using Microsoft.AspNetCore.Mvc;
  4. using Microsoft.Extensions.Options;
  5. using NodaTime;
  6. using OASystem.API.OAMethodLib;
  7. using OASystem.API.OAMethodLib.DeepSeekAPI;
  8. using OASystem.API.OAMethodLib.DoubaoAPI;
  9. using OASystem.API.OAMethodLib.Hotmail;
  10. using OASystem.API.OAMethodLib.KiMiApi;
  11. using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
  12. using OASystem.API.OAMethodLib.QiYeWeChatAPI;
  13. using OASystem.API.OAMethodLib.Quartz.Business;
  14. using OASystem.API.OAMethodLib.TokenHubAI;
  15. using OASystem.API.OAMethodLib.TokenHubAI.Models;
  16. using OASystem.Domain.AesEncryption;
  17. using OASystem.Domain.Entities.Customer;
  18. using OASystem.Domain.Entities.PersonnelModule;
  19. using OASystem.Domain.ViewModels.QiYeWeChat;
  20. using OASystem.RedisRepository;
  21. using System.IdentityModel.Tokens.Jwt;
  22. using System.Text.Json;
  23. using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
  24. using JsonSerializer = System.Text.Json.JsonSerializer;
  25. namespace OASystem.API.Controllers
  26. {
  27. /// <summary>
  28. /// AI测试控制器
  29. /// </summary>
  30. [Route("api/[controller]")]
  31. public class AITestController : ControllerBase
  32. {
  33. private readonly IDoubaoService _doubaoService;
  34. private readonly ILogger<AITestController> _logger;
  35. private readonly IConfiguration _config;
  36. private readonly IQiYeWeChatApiService _qiYeWeChatApiService;
  37. private readonly System.Net.Http.IHttpClientFactory _httpClientFactory;
  38. private readonly HotmailService _hotmailService;
  39. private readonly IMicrosoftGraphMailboxService _microsoftGraphMailboxService;
  40. private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _microsoftGraphMailboxOptions;
  41. private readonly MessageRepository _message;
  42. private readonly SqlSugarClient _sqlSugar;
  43. private readonly ITokenHubService _tokenHubService;
  44. private readonly IDeepSeekService _deepSeekService;
  45. public AITestController(
  46. IDoubaoService doubaoService,
  47. ILogger<AITestController> logger,
  48. IQiYeWeChatApiService qiYeWeChatApiService,
  49. HotmailService hotmailService,
  50. System.Net.Http.IHttpClientFactory httpClientFactory,
  51. IConfiguration config,
  52. IMicrosoftGraphMailboxService microsoftGraphMailboxService,
  53. IOptionsMonitor<MicrosoftGraphMailboxOptions> microsoftGraphMailboxOptions,
  54. IDeepSeekService deepSeekService,
  55. MessageRepository message,
  56. SqlSugarClient sqlSugar,
  57. ITokenHubService tokenHubService
  58. )
  59. {
  60. _doubaoService = doubaoService;
  61. _logger = logger;
  62. _qiYeWeChatApiService = qiYeWeChatApiService;
  63. _hotmailService = hotmailService;
  64. _httpClientFactory = httpClientFactory;
  65. _config = config;
  66. _microsoftGraphMailboxService = microsoftGraphMailboxService;
  67. _deepSeekService = deepSeekService;
  68. _microsoftGraphMailboxOptions = microsoftGraphMailboxOptions;
  69. _message = message;
  70. _sqlSugar = sqlSugar;
  71. _tokenHubService = tokenHubService;
  72. }
  73. #region 企业微信发送邮件测试
  74. /// <summary>
  75. /// 企业微信发送邮件测试
  76. /// </summary>
  77. [HttpPost("sendEmail")]
  78. public async Task<ActionResult<string>> SendEmail([FromForm] IFormFile[] feils)
  79. {
  80. try
  81. {
  82. var req = new EmailRequestDto()
  83. {
  84. ToEmails = new List<string> { "johnny.yang@pan-american-intl.com" },
  85. CcEmails = new List<string> { "Roy.lei@pan-american-intl.com" },
  86. BccEmails = new List<string> { "Roy.lei@pan-american-intl.com" },
  87. Subject = "测试邮件 - 来自企业微信API",
  88. Body = "这是一封通过企业微信API发送的测试邮件,包含附件。",
  89. Files = feils
  90. };
  91. var response = await _qiYeWeChatApiService.EmailSendAsync(req);
  92. return Ok(response);
  93. }
  94. catch (Exception ex)
  95. {
  96. _logger.LogError(ex, "调用企业微信邮件API失败。");
  97. return StatusCode(500, new { Message = "调用企业微信邮件API失败,请检查配置或网络。", Detail = ex.Message });
  98. }
  99. }
  100. #endregion
  101. #region 豆包 AI
  102. /// <summary>
  103. /// 豆包基础对话
  104. /// </summary>
  105. [HttpPost("doubao-chat")]
  106. public async Task<ActionResult<string>> DoubaoChat(string question, bool isThinking = false)
  107. {
  108. try
  109. {
  110. var messages = new List<DouBaoChatMessage>
  111. {
  112. new DouBaoChatMessage { Role = DouBaoRole.user, Content = question }
  113. };
  114. var options = new CompleteChatOptions
  115. {
  116. ThinkingOptions = new thinkingOptions { IsThinking = isThinking }
  117. };
  118. var response = await _doubaoService.CompleteChatAsync(messages, options);
  119. return Ok(response);
  120. }
  121. catch (Exception ex)
  122. {
  123. _logger.LogError(ex, "调用豆包API失败。");
  124. return StatusCode(500, new { Message = "调用豆包API失败", Detail = ex.Message });
  125. }
  126. }
  127. /// <summary>
  128. /// 豆包上传文件
  129. /// </summary>
  130. [HttpPost("doubao-upload")]
  131. public async Task<ActionResult<DoubaoFileResponse>> DoubaoUpload(IFormFile file, string purpose = "user_data")
  132. {
  133. if (file == null || file.Length == 0)
  134. return BadRequest("请选择要上传的文件");
  135. try
  136. {
  137. var stream = file.OpenReadStream();
  138. var existsFileExpand = new List<string> { "pdf", "docx" };
  139. if (!existsFileExpand.Contains(file.FileName.Split('.').Last().ToLower()))
  140. {
  141. return BadRequest("请上传pdf、docx文件!不支持其他文件");
  142. }
  143. if (file.FileName.Split('.').Last().ToLower() == "docx")
  144. {
  145. using var docxStream = file.OpenReadStream();
  146. var pdfStream = DoubaoService.ConvertDocxStreamToPdfStream(docxStream);
  147. stream = pdfStream;
  148. }
  149. var response = await _doubaoService.UploadFileAsync(stream, file.FileName, purpose);
  150. stream.Dispose();
  151. return Ok(response);
  152. }
  153. catch (Exception ex)
  154. {
  155. _logger.LogError(ex, "豆包上传文件失败");
  156. return StatusCode(500, new { Message = "上传失败", Detail = ex.Message });
  157. }
  158. }
  159. /// <summary>
  160. /// 豆包获取文件列表
  161. /// </summary>
  162. [HttpGet("doubao-files")]
  163. public async Task<ActionResult<DoubaoFileListResponse>> DoubaoListFiles()
  164. {
  165. try
  166. {
  167. var response = await _doubaoService.ListFilesAsync();
  168. return Ok(response);
  169. }
  170. catch (Exception ex)
  171. {
  172. _logger.LogError(ex, "获取豆包文件列表失败");
  173. return StatusCode(500, new { Message = "获取失败", Detail = ex.Message });
  174. }
  175. }
  176. /// <summary>
  177. /// 豆包删除文件
  178. /// </summary>
  179. [HttpDelete("doubao-file/{fileId}")]
  180. public async Task<ActionResult<bool>> DoubaoDeleteFile(string fileId)
  181. {
  182. try
  183. {
  184. var response = await _doubaoService.DeleteFileAsync(fileId);
  185. return Ok(response);
  186. }
  187. catch (Exception ex)
  188. {
  189. _logger.LogError(ex, "删除豆包文件失败");
  190. return StatusCode(500, new { Message = "删除失败", Detail = ex.Message });
  191. }
  192. }
  193. /// <summary>
  194. /// 豆包多模态对话(支持文本+图片)
  195. /// </summary>
  196. /// <param name="request">表单请求参数</param>
  197. [HttpPost("doubao-multimodal-chat")]
  198. public async Task<ActionResult<string>> DoubaoMultimodalChat([FromForm] DoubaoMultimodalChatRequest request)
  199. {
  200. if (string.IsNullOrWhiteSpace(request.Question))
  201. return BadRequest("问题不能为空");
  202. try
  203. {
  204. var contentItems = new List<DoubaoMultimodalContentItem>
  205. {
  206. new DoubaoMultimodalContentItem { Type = "text", Text = request.Question.Trim() }
  207. };
  208. if (!string.IsNullOrWhiteSpace(request.FileId))
  209. {
  210. contentItems.Add(new DoubaoMultimodalContentItem
  211. {
  212. Type = "file",
  213. FileId = request.FileId.Trim(),
  214. });
  215. }
  216. if (request.Image != null && request.Image.Length > 0)
  217. {
  218. using var ms = new MemoryStream();
  219. await request.Image.CopyToAsync(ms);
  220. var base64 = Convert.ToBase64String(ms.ToArray());
  221. var mimeType = request.Image.ContentType ?? "image/jpeg";
  222. var dataUrl = $"data:{mimeType};base64,{base64}";
  223. contentItems.Add(new DoubaoMultimodalContentItem
  224. {
  225. Type = "image_url",
  226. ImageUrl = new DoubaoMultimodalImageUrl { Url = dataUrl }
  227. });
  228. }
  229. var messages = new List<DoubaoMultimodalChatMessage>
  230. {
  231. new DoubaoMultimodalChatMessage
  232. {
  233. Role = "user",
  234. Content = contentItems
  235. }
  236. };
  237. var options = new CompleteMultimodalChatOptions
  238. {
  239. ThinkingOptions = new DoubaoMultimodalThinkingOptions
  240. {
  241. IsThinking = request.IsThinking,
  242. ReasoningEffort = "medium"
  243. }
  244. };
  245. var response = await _doubaoService.CompleteMultimodalChatAsync(messages, options);
  246. return Ok(response ?? string.Empty);
  247. }
  248. catch (Exception ex)
  249. {
  250. _logger.LogError(ex, "调用豆包多模态API失败。");
  251. return StatusCode(500, new { Message = "调用豆包多模态API失败", Detail = ex.Message });
  252. }
  253. }
  254. #endregion
  255. #region Kimi AI
  256. /// <summary>
  257. /// Kimi (Moonshot) 通用对话。默认用于"中文→英文"翻译(如中文国家名转英文国家名)。
  258. /// </summary>
  259. /// <param name="request">对话请求</param>
  260. [HttpPost("kimi-chat")]
  261. public async Task<IActionResult> KimiChat([FromBody] KimiChatRequest request)
  262. {
  263. if (request == null || string.IsNullOrWhiteSpace(request.Text))
  264. return BadRequest(new { success = false, error = "Text 不能为空" });
  265. try
  266. {
  267. var messages = new List<SeedMessages>
  268. {
  269. new SeedMessages { Role = KimiRole.system, Content = "你是一个专业翻译,请将输入的中文国家/地区名称翻译成对应的英文国家/地区名称。只输出翻译结果本身,不要输出任何解释、引号或多余字符。" },
  270. new SeedMessages { Role = KimiRole.user, Content = request.Text.Trim() }
  271. };
  272. var result = await new KiMiApiClient().SeedMessageByFullConterObject(messages);
  273. var content = result?.Choices?.FirstOrDefault()?.Message?.Content;
  274. return Ok(new
  275. {
  276. success = true,
  277. data = content,
  278. model = result?.Model
  279. });
  280. }
  281. catch (Exception ex)
  282. {
  283. _logger.LogError(ex, "调用 Kimi API 失败。");
  284. return StatusCode(500, new { success = false, error = "调用 Kimi API 失败", detail = ex.Message });
  285. }
  286. }
  287. public class KimiChatRequest
  288. {
  289. public string Text { get; set; } = string.Empty;
  290. }
  291. #endregion
  292. #region 腾讯 TokenHub AI
  293. /// <summary>
  294. /// 简单对话
  295. /// </summary>
  296. /// <param name="request">对话请求</param>
  297. /// <returns>AI 回复</returns>
  298. [HttpPost("simple")]
  299. public async Task<IActionResult> SimpleChat([FromBody] ChatRequest request)
  300. {
  301. try
  302. {
  303. _logger.LogInformation("简单对话请求: {Message}", request.Messages);
  304. var reply = await _tokenHubService.ChatAsync(request);
  305. return Ok(new
  306. {
  307. success = true,
  308. data = reply,
  309. model = request.Model
  310. });
  311. }
  312. catch (TokenHubException ex)
  313. {
  314. _logger.LogError(ex, "TokenHub 调用失败");
  315. return StatusCode(ex.StatusCode, new JsonView()
  316. {
  317. Msg = ex.Message,
  318. Code = ex.StatusCode,
  319. Data = ex.ErrorDetail
  320. });
  321. }
  322. catch (Exception ex)
  323. {
  324. _logger.LogError(ex, "对话失败");
  325. return StatusCode(StatusCodes.Status400BadRequest, new JsonView()
  326. {
  327. Msg = ex.Message,
  328. Code = StatusCodes.Status400BadRequest,
  329. Data = ex.Message
  330. });
  331. }
  332. }
  333. /// <summary>
  334. /// 带系统提示的对话
  335. /// </summary>
  336. [HttpPost("system")]
  337. public async Task<IActionResult> ChatWithSystem([FromBody] ChatRequest request)
  338. {
  339. try
  340. {
  341. var reply = await _tokenHubService.ChatAsync(request);
  342. return Ok(new
  343. {
  344. success = true,
  345. data = reply,
  346. model = request.Model ?? "hunyuan-2.0-instruct-20251111"
  347. });
  348. }
  349. catch (TokenHubException ex)
  350. {
  351. return StatusCode(ex.StatusCode, new
  352. {
  353. success = false,
  354. error = ex.Message,
  355. code = ex.ErrorCode,
  356. requestId = ex.RequestId
  357. });
  358. }
  359. catch (Exception ex)
  360. {
  361. _logger.LogError(ex, "对话失败");
  362. return StatusCode(500, new
  363. {
  364. success = false,
  365. error = ex.Message
  366. });
  367. }
  368. }
  369. /// <summary>
  370. /// 完整参数对话
  371. /// </summary>
  372. [HttpPost("full")]
  373. public async Task<IActionResult> FullChat([FromBody] ChatRequest request)
  374. {
  375. try
  376. {
  377. _logger.LogInformation("完整对话请求: Model={Model}, Messages={Count}",
  378. request.Model, request.Messages?.Count ?? 0);
  379. // 添加系统提示
  380. if (!string.IsNullOrEmpty(request.SystemPrompt))
  381. {
  382. request = request.WithSystemPrompt(request.SystemPrompt);
  383. }
  384. var response = await _tokenHubService.ChatAsync(request);
  385. if (!response.IsSuccess)
  386. {
  387. return BadRequest(new
  388. {
  389. success = false,
  390. error = response.ErrorMessage
  391. });
  392. }
  393. return Ok(new
  394. {
  395. success = true,
  396. data = response.Content,
  397. model = response.Model,
  398. usage = response.Usage,
  399. finishReason = response.FinishReason,
  400. toolCalls = response.ToolCalls
  401. });
  402. }
  403. catch (TokenHubException ex)
  404. {
  405. return StatusCode(ex.StatusCode, new
  406. {
  407. success = false,
  408. error = ex.Message,
  409. code = ex.ErrorCode,
  410. requestId = ex.RequestId
  411. });
  412. }
  413. catch (Exception ex)
  414. {
  415. _logger.LogError(ex, "对话失败");
  416. return StatusCode(500, new
  417. {
  418. success = false,
  419. error = ex.Message
  420. });
  421. }
  422. }
  423. /// <summary>
  424. /// 流式对话(Server-Sent Events)
  425. /// </summary>
  426. [HttpPost("stream")]
  427. public async Task StreamChat([FromBody] ChatRequest request)
  428. {
  429. try
  430. {
  431. _logger.LogInformation("流式对话请求: {Message}", request.Messages.FirstOrDefault());
  432. // 设置响应头
  433. Response.ContentType = "text/event-stream; charset=utf-8";
  434. Response.Headers.Append("Cache-Control", "no-cache");
  435. Response.Headers.Append("Connection", "keep-alive");
  436. Response.Headers.Append("X-Accel-Buffering", "no");
  437. if (!string.IsNullOrEmpty(request.SystemPrompt))
  438. {
  439. request = request.WithSystemPrompt(request.SystemPrompt);
  440. }
  441. var fullContent = new StringBuilder();
  442. await _tokenHubService.StreamChatAsync(
  443. request,
  444. chunk =>
  445. {
  446. if (chunk.IsDone)
  447. {
  448. // 发送结束标记
  449. var endData = new
  450. {
  451. type = "done",
  452. fullContent = fullContent.ToString(),
  453. usage = chunk.Usage,
  454. model = chunk.Model
  455. };
  456. Response.WriteAsync($"data: {JsonSerializer.Serialize(endData)}\n\n");
  457. Response.Body.Flush();
  458. return;
  459. }
  460. if (!string.IsNullOrEmpty(chunk.Content))
  461. {
  462. fullContent.Append(chunk.Content);
  463. // 发送数据块
  464. var chunkData = new
  465. {
  466. type = "chunk",
  467. content = chunk.Content,
  468. model = chunk.Model
  469. };
  470. Response.WriteAsync($"data: {JsonSerializer.Serialize(chunkData)}\n\n");
  471. Response.Body.Flush();
  472. }
  473. if (chunk.ToolCalls != null && chunk.ToolCalls.Count > 0)
  474. {
  475. var toolData = new
  476. {
  477. type = "tool_call",
  478. tools = chunk.ToolCalls
  479. };
  480. Response.WriteAsync($"data: {JsonSerializer.Serialize(toolData)}\n\n");
  481. Response.Body.Flush();
  482. }
  483. }
  484. );
  485. }
  486. catch (Exception ex)
  487. {
  488. _logger.LogError(ex, "流式对话失败");
  489. var errorData = new { type = "error", error = ex.Message };
  490. await Response.WriteAsync($"data: {JsonSerializer.Serialize(errorData)}\n\n");
  491. }
  492. }
  493. /// <summary>
  494. /// 带重试的对话
  495. /// </summary>
  496. [HttpPost("retry")]
  497. public async Task<IActionResult> ChatWithRetry([FromBody] ChatRequest request)
  498. {
  499. try
  500. {
  501. var response = await _tokenHubService.ChatWithRetryAsync(request);
  502. if (!response.IsSuccess)
  503. {
  504. return BadRequest(new
  505. {
  506. success = false,
  507. error = response.ErrorMessage
  508. });
  509. }
  510. return Ok(new
  511. {
  512. success = true,
  513. data = response.Content,
  514. model = response.Model,
  515. usage = response.Usage
  516. });
  517. }
  518. catch (TokenHubException ex)
  519. {
  520. return StatusCode(ex.StatusCode, new
  521. {
  522. success = false,
  523. error = ex.Message,
  524. code = ex.ErrorCode,
  525. requestId = ex.RequestId
  526. });
  527. }
  528. catch (Exception ex)
  529. {
  530. _logger.LogError(ex, "对话失败");
  531. return StatusCode(500, new
  532. {
  533. success = false,
  534. error = ex.Message
  535. });
  536. }
  537. }
  538. /// <summary>
  539. /// 获取可用模型列表
  540. /// </summary>
  541. [HttpGet("models")]
  542. public async Task<IActionResult> GetModels()
  543. {
  544. try
  545. {
  546. var models = await _tokenHubService.GetAvailableModelsAsync();
  547. return Ok(new
  548. {
  549. success = true,
  550. data = models,
  551. count = models.Count
  552. });
  553. }
  554. catch (Exception ex)
  555. {
  556. _logger.LogError(ex, "获取模型列表失败");
  557. return StatusCode(500, new
  558. {
  559. success = false,
  560. error = ex.Message
  561. });
  562. }
  563. }
  564. /// <summary>
  565. /// 豆包多模态对话请求体(form-data)
  566. /// </summary>
  567. public class DoubaoMultimodalChatRequest
  568. {
  569. public string Question { get; set; } = string.Empty;
  570. public IFormFile? Image { get; set; }
  571. public bool IsThinking { get; set; } = false;
  572. public string FileId { get; set; } = string.Empty;
  573. }
  574. #endregion
  575. #region DeepSeek 测试
  576. /// <summary>
  577. /// DeepSeek 带上下文的流式对话测试。响应为 NDJSON:每行一条 JSON,phase 为 reasoning、content 或 error。
  578. /// system | user | assistant 角色。
  579. /// </summary>
  580. [HttpPost("deepseek-chat-stream-with-history")]
  581. public async Task<IActionResult> DeepSeekChatStreamWithHistory(
  582. [FromBody] DeepSeekChatStreamHistoryTestRequest request,
  583. CancellationToken cancellationToken = default)
  584. {
  585. if (request?.Messages == null || request.Messages.Count == 0)
  586. return BadRequest(new { message = "Messages 不能为空,且至少包含一条 user/system/assistant 消息。" });
  587. Response.ContentType = "application/x-ndjson; charset=utf-8";
  588. Response.Headers["Cache-Control"] = "no-cache";
  589. static string NdjsonLine(DeepSeekStreamChunk c) => JsonConvert.SerializeObject(new
  590. {
  591. phase = c.Phase == DeepSeekStreamPhase.Reasoning ? "reasoning" : "content",
  592. text = c.Text
  593. });
  594. try
  595. {
  596. await foreach (var chunk in _deepSeekService.ChatStreamWithHistoryAsync(
  597. request.Messages,
  598. string.IsNullOrWhiteSpace(request.Model) ? "deepseek-chat" : request.Model!.Trim(),
  599. request.Temperature,
  600. request.MaxTokens))
  601. {
  602. cancellationToken.ThrowIfCancellationRequested();
  603. await Response.WriteAsync(NdjsonLine(chunk) + "\n", cancellationToken);
  604. await Response.Body.FlushAsync(cancellationToken);
  605. }
  606. await Response.WriteAsync(JsonConvert.SerializeObject(new { phase = "success", text = "结束" }) + "\n", cancellationToken);
  607. await Response.Body.FlushAsync(cancellationToken);
  608. }
  609. catch (OperationCanceledException)
  610. {
  611. }
  612. catch (Exception ex)
  613. {
  614. _logger.LogError(ex, "DeepSeek 带历史流式对话失败");
  615. await Response.WriteAsync(
  616. JsonConvert.SerializeObject(new { phase = "error", text = ex.Message }) + "\n",
  617. cancellationToken);
  618. }
  619. return new EmptyResult();
  620. }
  621. /// <summary>
  622. /// DeepSeek 流式对话(含多轮)请求体
  623. /// </summary>
  624. public class DeepSeekChatStreamHistoryTestRequest
  625. {
  626. public List<DeepSeekHistoryMessage> Messages { get; set; } = new();
  627. public string? Model { get; set; } = "deepseek-chat";
  628. public float Temperature { get; set; } = 0.7f;
  629. public int MaxTokens { get; set; } = 4000;
  630. }
  631. #endregion
  632. /// <summary>
  633. /// hotmail 发送邮件
  634. /// </summary>
  635. [HttpPost("hotmailSeed")]
  636. public async Task<ActionResult<string>> HotmailSeed()
  637. {
  638. await _hotmailService.SendMailAsync(
  639. //"Roy.Lei.Atom@hotmail.com",
  640. "925554512@qq.com",
  641. //"johnny.yang@pan-american-intl.com",
  642. new HotmailService.MailDto()
  643. {
  644. Subject = "系统提醒",
  645. Content = "<p>这是一封Homail 发送的测试邮件</p>",
  646. //To = "Roy.lei@pan-american-intl.com"
  647. To = "johnny.yang@pan-american-intl.com"
  648. });
  649. return StatusCode(200, new { Message = "操作成功。" });
  650. }
  651. /// <summary>
  652. /// hotmail 发送邮件
  653. /// </summary>
  654. [HttpPost("HotmailMerged")]
  655. public async Task<ActionResult<string>> HotmailMerged()
  656. {
  657. // 1. 获取当前北京时间 (CST)
  658. var cstZone = CommonFun.GetCstZone();
  659. var nowInCst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, cstZone);
  660. // 2. 构造昨天的北京时间范围:00:00:00 到 23:59:59
  661. var yesterdayStart = nowInCst.Date.AddDays(-1); // 昨天的 00:00:00
  662. var yesterdayEnd = yesterdayStart.AddDays(1).AddTicks(-1); // 昨天的 23:59:59
  663. var res = await _hotmailService.GetMergedMessagesAsync(
  664. new List<string>() { "925554512@qq.com" },
  665. yesterdayStart,
  666. yesterdayEnd
  667. );
  668. return StatusCode(200, res);
  669. }
  670. /// <summary>
  671. /// hotmail 定时发送邮件 汇总 测试
  672. /// </summary>
  673. [HttpPost("hotmailSummarySeedQW")]
  674. public async Task<ActionResult<string>> HotmailSummary()
  675. {
  676. ProcessAndNotifySummary.ProcessAndNotifySummaryAsync();
  677. return StatusCode(200, "发送成功");
  678. }
  679. #region Microsoft Graph 邮箱测试(仅访问令牌)
  680. private const string GraphAccessTokenHeader = "X-Graph-Access-Token";
  681. /// <summary>
  682. /// 优先级:请求头 X-Graph-Access-Token → 查询 graphAccessToken → bodyToken(发信)。
  683. /// </summary>
  684. private string? ResolveGraphAccessToken(string? queryToken = null, string? bodyToken = null)
  685. {
  686. var header = Request.Headers[GraphAccessTokenHeader].FirstOrDefault();
  687. if (!string.IsNullOrWhiteSpace(header))
  688. return header.Trim();
  689. if (!string.IsNullOrWhiteSpace(queryToken))
  690. return queryToken.Trim();
  691. if (!string.IsNullOrWhiteSpace(bodyToken))
  692. return bodyToken.Trim();
  693. return null;
  694. }
  695. /// <summary>
  696. /// 查询当前用户 GET /v1.0/me。必须提供 Graph 访问令牌。
  697. /// </summary>
  698. [HttpGet("graph-mail/me")]
  699. public async Task<IActionResult> GraphMailMe(
  700. [FromQuery] string? graphAccessToken = null,
  701. CancellationToken cancellationToken = default)
  702. {
  703. var bearer = ResolveGraphAccessToken(graphAccessToken);
  704. if (string.IsNullOrWhiteSpace(bearer))
  705. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
  706. try
  707. {
  708. var json = await _microsoftGraphMailboxService.GetMeRawJsonAsync(bearer, cancellationToken);
  709. if (string.IsNullOrEmpty(json))
  710. return StatusCode(502, new { message = "Graph 返回空正文" });
  711. return Content(json, "application/json");
  712. }
  713. catch (Exception ex)
  714. {
  715. _logger.LogError(ex, "Graph Mail /me 失败");
  716. return StatusCode(500, new { message = ex.Message });
  717. }
  718. }
  719. /// <summary>
  720. /// 查询收件箱。必须提供 Graph 访问令牌(需 Mail.Read)。默认 sinceUtc 为 UTC 近 24 小时。
  721. /// </summary>
  722. /// <param name="sinceUtc">起始时间(UTC),ISO8601</param>
  723. /// <param name="graphAccessToken">或使用请求头 X-Graph-Access-Token</param>
  724. /// <param name="cancellationToken">取消标记</param>
  725. [HttpGet("graph-mail/inbox")]
  726. public async Task<IActionResult> GraphMailInbox(
  727. [FromQuery] DateTime? sinceUtc = null,
  728. [FromQuery] string? graphAccessToken = null,
  729. CancellationToken cancellationToken = default)
  730. {
  731. var bearer = ResolveGraphAccessToken(graphAccessToken);
  732. if (string.IsNullOrWhiteSpace(bearer))
  733. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
  734. var since = sinceUtc ?? DateTime.UtcNow.AddHours(-24);
  735. try
  736. {
  737. var json = await _microsoftGraphMailboxService.GetInboxMessagesJsonSinceAsync(since, bearer, cancellationToken);
  738. if (string.IsNullOrEmpty(json))
  739. return StatusCode(502, new { message = "Graph 返回空正文" });
  740. return Content(json, "application/json");
  741. }
  742. catch (Exception ex)
  743. {
  744. _logger.LogError(ex, "Graph Mail inbox 失败");
  745. return StatusCode(500, new { message = ex.Message });
  746. }
  747. }
  748. /// <summary>
  749. /// Graph sendMail 纯文本。必须提供令牌(需 Mail.Send):头 / 查询 / Body.graphAccessToken。
  750. /// </summary>
  751. [HttpPost("graph-mail/send")]
  752. public async Task<IActionResult> GraphMailSend(
  753. [FromBody] GraphMailSendTestRequest request,
  754. [FromQuery] string? graphAccessToken = null,
  755. CancellationToken cancellationToken = default)
  756. {
  757. if (request == null || string.IsNullOrWhiteSpace(request.ToEmail))
  758. return BadRequest(new { message = "ToEmail 不能为空" });
  759. var bearer = ResolveGraphAccessToken(graphAccessToken, request.GraphAccessToken);
  760. if (string.IsNullOrWhiteSpace(bearer))
  761. return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:X-Graph-Access-Token、?graphAccessToken 或 Body.graphAccessToken" });
  762. var subject = string.IsNullOrWhiteSpace(request.Subject)
  763. ? $"OASystem Graph 测试邮件 {DateTime.Now:yyyy-MM-dd HH:mm:ss}"
  764. : request.Subject!;
  765. var body = request.Body ?? string.Empty;
  766. try
  767. {
  768. await _microsoftGraphMailboxService.SendMailAsync(request.ToEmail.Trim(), subject, body, bearer, cancellationToken);
  769. return Ok(new { ok = true, message = "sendMail 已提交", to = request.ToEmail.Trim(), subject });
  770. }
  771. catch (HttpRequestException ex)
  772. {
  773. return StatusCode(502, new { message = "Graph HTTP 错误", detail = ex.Message });
  774. }
  775. catch (Exception ex)
  776. {
  777. _logger.LogError(ex, "Graph Mail send 失败");
  778. return StatusCode(500, new { message = ex.Message });
  779. }
  780. }
  781. public class EmailAuthRedisCache
  782. {
  783. public string? AccessToken { get; set; }
  784. public string? HomeAccountId { get; set; }
  785. public string? UserTokenCacheBase64 { get; set; }
  786. public string? ClientId { get; set; }
  787. }
  788. /// <summary>
  789. /// 从 Redis 读取 MSAL 缓存与 HomeAccountId,静默刷新 Graph access_token。
  790. /// </summary>
  791. [HttpGet("graph-mail/refresh-token")]
  792. public async Task<IActionResult> RefreshAccessToken([FromQuery] string? redisKey = null)
  793. {
  794. var key = string.IsNullOrWhiteSpace(redisKey) ? "Email:AuthCache:345" : redisKey.Trim();
  795. var redis = RedisFactory.CreateRedisRepository();
  796. var json = await redis.StringGetRawAsync(key);
  797. if (string.IsNullOrWhiteSpace(json))
  798. {
  799. return BadRequest(new { message = $"Redis 键 {key} 不存在或为空" });
  800. }
  801. EmailAuthRedisCache? cacheEntry;
  802. try
  803. {
  804. cacheEntry = JsonConvert.DeserializeObject<EmailAuthRedisCache>(json);
  805. }
  806. catch (System.Text.Json.JsonException ex)
  807. {
  808. _logger.LogWarning(ex, "Redis 键 {Key} 内容不是合法 JSON(应用 StringGetRawAsync + JSON,勿用 StringGetAsync<T>,后者为 BinaryFormatter)", key);
  809. return BadRequest(new { message = "Redis 值为 JSON 文本时须用 StringGetRawAsync 再反序列化;StringGetAsync<T> 仅适用于 BinaryFormatter 写入的数据", detail = ex.Message });
  810. }
  811. if (cacheEntry == null
  812. || string.IsNullOrWhiteSpace(cacheEntry.UserTokenCacheBase64)
  813. || string.IsNullOrWhiteSpace(cacheEntry.HomeAccountId))
  814. {
  815. return BadRequest(new { message = "JSON 中缺少 UserTokenCacheBase64 或 HomeAccountId" });
  816. }
  817. var accessToken = await _microsoftGraphMailboxService.RefreshAccessTokenAsync(
  818. cacheEntry.ClientId,
  819. "common",
  820. new[] { "Mail.Read", "User.Read", "Mail.Send" },
  821. cacheEntry.UserTokenCacheBase64,
  822. cacheEntry.HomeAccountId);
  823. return Ok(new { accessToken });
  824. }
  825. /// <summary>
  826. /// Graph 发信测试请求体
  827. /// </summary>
  828. public class GraphMailSendTestRequest
  829. {
  830. public string ToEmail { get; set; } = string.Empty;
  831. public string? Subject { get; set; }
  832. public string? Body { get; set; }
  833. /// <summary>Microsoft Graph 访问令牌(也可用请求头 X-Graph-Access-Token)</summary>
  834. public string? GraphAccessToken { get; set; }
  835. }
  836. #endregion
  837. #region 客户名单解密
  838. /// <summary>
  839. /// 客户名单解密
  840. /// </summary>
  841. [HttpPost("deleClientList")]
  842. public async Task<ActionResult<string>> DeleClientList()
  843. {
  844. var data = await _message._sqlSugar.Queryable<Crm_DeleClient>().Where(x => x.IsDel == 0 )
  845. .LeftJoin<Crm_CustomerCompany>((x,y) => x.CrmCompanyId == y.Id)
  846. .Select((x, y) => new {
  847. x.Id,
  848. x.FirstName,
  849. x.LastName,
  850. y.CompanyFullName,
  851. x.ClientPhone,
  852. x.Tel,
  853. x.Phone,
  854. x.Job,
  855. x.CreateTime
  856. }).ToListAsync();
  857. var newData = new List<dynamic>();
  858. foreach (var item in data)
  859. {
  860. newData.Add(new
  861. {
  862. item.Id,
  863. LastName = AesEncryptionHelper.Decrypt(item.LastName),
  864. FirstName = AesEncryptionHelper.Decrypt(item.FirstName),
  865. CompanyFullName = AesEncryptionHelper.Decrypt(item.CompanyFullName),
  866. Job = AesEncryptionHelper.Decrypt(item.Job),
  867. ClientPhone = AesEncryptionHelper.Decrypt(item.ClientPhone),
  868. Tel = AesEncryptionHelper.Decrypt(item.Tel),
  869. Phone = AesEncryptionHelper.Decrypt(item.Phone),
  870. item.CreateTime
  871. });
  872. }
  873. return StatusCode(200, newData);
  874. }
  875. #endregion
  876. #region 工资计算测试
  877. /// <summary>
  878. /// 工资计算测试 - 企微考勤信息
  879. /// </summary>
  880. [HttpPost("SalaryCalculator")]
  881. public async Task<ActionResult<string>> SalaryCalculator()
  882. {
  883. // 计算本月工资起止时间 比如是2月的1号-28号,那就是2月1号的零点到3月1号的零点
  884. DateTime startDt = Convert.ToDateTime("2026-03-01");
  885. DateTime endDt = Convert.ToDateTime("2026-03-31");
  886. string thisYearMonth = "2026-03"; //本月工资年月
  887. string preYearMonth = "2026-03"; //上月工资年月
  888. // 检查是否存在
  889. //var isExists = await _sqlSugar.Queryable<Pm_WageSheet>().AnyAsync(it => it.YearMonth == thisYearMonth);
  890. //if (isExists) return Ok(JsonView(false, $"{thisYearMonth} 工资数据已存在。"));
  891. var preWageSheetItems = await _sqlSugar.Queryable<Pm_WageSheet>()
  892. .Where(it => it.IsDel == 0 && it.YearMonth == preYearMonth)
  893. .ToListAsync();
  894. if (!preWageSheetItems.Any()) return Ok(JsonView(false, "上月工资数据不存在。"));
  895. //处理上个月同月同人 多条数据
  896. List<Pm_WageSheet> preWageSheetItems1 = preWageSheetItems
  897. .GroupBy(it => new { it.YearMonth, it.UserId })
  898. .Select(it => it.FirstOrDefault(item => item.Basic != 0))
  899. .Where(it => it != null)
  900. .ToList()!;
  901. var res = await PayrollComputation_v1.SalaryCalculatorAsync(preWageSheetItems1,208, thisYearMonth, startDt, endDt);
  902. return Ok(res);
  903. }
  904. #endregion
  905. }
  906. }