using Flurl.Http.Configuration;
using Microsoft.Extensions.Options;
using OASystem.API.OAMethodLib.DoubaoAPI;
using OASystem.API.OAMethodLib.Hotmail;
using OASystem.API.OAMethodLib.HunYuanAPI;
using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
using OASystem.API.OAMethodLib.QiYeWeChatAPI;
using OASystem.API.OAMethodLib.Quartz.Business;
using OASystem.Domain.ViewModels.QiYeWeChat;
using OASystem.RedisRepository;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
namespace OASystem.API.Controllers
{
///
/// AI测试控制器
///
[Route("api/[controller]")]
public class AITestController : ControllerBase
{
private readonly IHunyuanService _hunyuanService;
private readonly IDoubaoService _doubaoService;
private readonly ILogger _logger;
private readonly IConfiguration _config;
private readonly IQiYeWeChatApiService _qiYeWeChatApiService;
private readonly System.Net.Http.IHttpClientFactory _httpClientFactory;
private readonly HotmailService _hotmailService;
private readonly IMicrosoftGraphMailboxService _microsoftGraphMailboxService;
private readonly IOptionsMonitor _microsoftGraphMailboxOptions;
public AITestController(
IHunyuanService hunyuanService,
IDoubaoService doubaoService,
ILogger logger,
IQiYeWeChatApiService qiYeWeChatApiService,
HotmailService hotmailService,
System.Net.Http.IHttpClientFactory httpClientFactory,
IConfiguration config,
IMicrosoftGraphMailboxService microsoftGraphMailboxService,
IOptionsMonitor microsoftGraphMailboxOptions
)
{
_hunyuanService = hunyuanService;
_doubaoService = doubaoService;
_logger = logger;
_qiYeWeChatApiService = qiYeWeChatApiService;
_hotmailService = hotmailService;
_httpClientFactory = httpClientFactory;
_config = config;
_microsoftGraphMailboxService = microsoftGraphMailboxService;
_microsoftGraphMailboxOptions = microsoftGraphMailboxOptions;
}
#region 企业微信发送邮件测试
///
/// 企业微信发送邮件测试
///
[HttpPost("sendEmail")]
public async Task> SendEmail([FromForm] IFormFile[] feils)
{
try
{
var req = new EmailRequestDto()
{
ToEmails = new List { "johnny.yang@pan-american-intl.com" },
CcEmails = new List { "Roy.lei@pan-american-intl.com" },
BccEmails = new List { "Roy.lei@pan-american-intl.com" },
Subject = "测试邮件 - 来自企业微信API",
Body = "这是一封通过企业微信API发送的测试邮件,包含附件。",
Files = feils
};
var response = await _qiYeWeChatApiService.EmailSendAsync(req);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "调用企业微信邮件API失败。");
return StatusCode(500, new { Message = "调用企业微信邮件API失败,请检查配置或网络。", Detail = ex.Message });
}
}
#endregion
#region 豆包 AI
///
/// 豆包基础对话
///
[HttpPost("doubao-chat")]
public async Task> DoubaoChat(string question, bool isThinking = false)
{
try
{
var messages = new List
{
new DouBaoChatMessage { Role = DouBaoRole.user, Content = question }
};
var options = new CompleteChatOptions
{
ThinkingOptions = new thinkingOptions { IsThinking = isThinking }
};
var response = await _doubaoService.CompleteChatAsync(messages, options);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "调用豆包API失败。");
return StatusCode(500, new { Message = "调用豆包API失败", Detail = ex.Message });
}
}
///
/// 豆包上传文件
///
[HttpPost("doubao-upload")]
public async Task> DoubaoUpload(IFormFile file, string purpose = "user_data")
{
if (file == null || file.Length == 0)
return BadRequest("请选择要上传的文件");
try
{
var stream = file.OpenReadStream();
var existsFileExpand = new List { "pdf", "docx" };
if (!existsFileExpand.Contains(file.FileName.Split('.').Last().ToLower()))
{
return BadRequest("请上传pdf、docx文件!不支持其他文件");
}
if (file.FileName.Split('.').Last().ToLower() == "docx")
{
using var docxStream = file.OpenReadStream();
var pdfStream = DoubaoService.ConvertDocxStreamToPdfStream(docxStream);
stream = pdfStream;
}
var response = await _doubaoService.UploadFileAsync(stream, file.FileName, purpose);
stream.Dispose();
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "豆包上传文件失败");
return StatusCode(500, new { Message = "上传失败", Detail = ex.Message });
}
}
///
/// 豆包获取文件列表
///
[HttpGet("doubao-files")]
public async Task> DoubaoListFiles()
{
try
{
var response = await _doubaoService.ListFilesAsync();
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取豆包文件列表失败");
return StatusCode(500, new { Message = "获取失败", Detail = ex.Message });
}
}
///
/// 豆包删除文件
///
[HttpDelete("doubao-file/{fileId}")]
public async Task> DoubaoDeleteFile(string fileId)
{
try
{
var response = await _doubaoService.DeleteFileAsync(fileId);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "删除豆包文件失败");
return StatusCode(500, new { Message = "删除失败", Detail = ex.Message });
}
}
///
/// 豆包多模态对话(支持文本+图片)
///
/// 表单请求参数
[HttpPost("doubao-multimodal-chat")]
public async Task> DoubaoMultimodalChat([FromForm] DoubaoMultimodalChatRequest request)
{
if (string.IsNullOrWhiteSpace(request.Question))
return BadRequest("问题不能为空");
try
{
var contentItems = new List
{
new DoubaoMultimodalContentItem { Type = "text", Text = request.Question.Trim() }
};
if (!string.IsNullOrWhiteSpace(request.FileId))
{
contentItems.Add(new DoubaoMultimodalContentItem
{
Type = "file",
FileId = request.FileId.Trim(),
});
}
if (request.Image != null && request.Image.Length > 0)
{
using var ms = new MemoryStream();
await request.Image.CopyToAsync(ms);
var base64 = Convert.ToBase64String(ms.ToArray());
var mimeType = request.Image.ContentType ?? "image/jpeg";
var dataUrl = $"data:{mimeType};base64,{base64}";
contentItems.Add(new DoubaoMultimodalContentItem
{
Type = "image_url",
ImageUrl = new DoubaoMultimodalImageUrl { Url = dataUrl }
});
}
var messages = new List
{
new DoubaoMultimodalChatMessage
{
Role = "user",
Content = contentItems
}
};
var options = new CompleteMultimodalChatOptions
{
ThinkingOptions = new DoubaoMultimodalThinkingOptions
{
IsThinking = request.IsThinking,
ReasoningEffort = "medium"
}
};
var response = await _doubaoService.CompleteMultimodalChatAsync(messages, options);
return Ok(response ?? string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "调用豆包多模态API失败。");
return StatusCode(500, new { Message = "调用豆包多模态API失败", Detail = ex.Message });
}
}
#endregion
#region 混元 AI
///
/// 基础对话示例
///
[HttpPost("chat")]
public async Task> BasicChat(string question)
{
try
{
var response = await _hunyuanService.ChatCompletionsHunyuan_t1_latestAsync(question);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "调用腾讯云混元API失败。");
return StatusCode(500, new { Message = "调用腾讯云API失败,请检查配置或网络。", Detail = ex.Message });
}
}
///
/// 模拟“根据文件提问”的API端点
/// 注意:此示例中,文件内容通过请求体传入。
/// 实际场景中,文件内容可能来自用户上传并解析(如PDF、TXT解析为文本)后的结果。
///
[HttpPost("ask-with-file")]
public async Task> AskBasedOnFile([FromBody] AskWithFileRequest request)
{
if (string.IsNullOrEmpty(request.FileContent) || string.IsNullOrEmpty(request.Question))
{
return BadRequest(new { Message = "FileContent和Question字段不能为空。" });
}
try
{
var answer = await _hunyuanService.AskWithFileContextAsync(request.FileContent, request.Question, request.Model);
return Ok(answer);
}
catch (Exception ex)
{
_logger.LogError(ex, "处理基于文件的提问失败。");
return StatusCode(500, new { Message = "处理请求失败。", Detail = ex.Message });
}
}
///
/// 用于测试的GET端点,快速验证服务可用性(使用示例数据)
///
[HttpGet("test-file-query")]
public async Task> TestFileQuery()
{
// 示例文件内容和问题
var sampleFileContent = "在软件开发中,依赖注入(Dependency Injection)是一种设计模式,用于实现控制反转(Inversion of Control, IoC)。它允许在类外部创建依赖对象,并通过构造函数、属性或方法将其‘注入’到类中,从而降低类之间的耦合度。";
var sampleQuestion = "依赖注入的主要目的是什么?";
var model = "hunyuan-lite"; // 可使用 "hunyuan-pro" 等
try
{
var answer = await _hunyuanService.AskWithFileContextAsync(sampleFileContent, sampleQuestion, model);
return Ok($"测试成功。问题:'{sampleQuestion}'\n回答:{answer}");
}
catch (Exception ex)
{
_logger.LogError(ex, "测试文件提问失败。");
return StatusCode(500, new { Message = "测试失败。", Detail = ex.Message });
}
}
///
/// 用于“根据文件提问”的请求体
///
public class AskWithFileRequest
{
public string FileContent { get; set; } = string.Empty;
public string Question { get; set; } = string.Empty;
public string Model { get; set; } = "hunyuan-lite";
}
///
/// 豆包多模态对话请求体(form-data)
///
public class DoubaoMultimodalChatRequest
{
public string Question { get; set; } = string.Empty;
public IFormFile? Image { get; set; }
public bool IsThinking { get; set; } = false;
public string FileId { get; set; } = string.Empty;
}
#endregion
///
/// hotmail 发送邮件
///
[HttpPost("hotmailSeed")]
public async Task> HotmailSeed()
{
await _hotmailService.SendMailAsync(
//"Roy.Lei.Atom@hotmail.com",
"925554512@qq.com",
//"johnny.yang@pan-american-intl.com",
new HotmailService.MailDto() {
Subject = "系统提醒",
Content = "这是一封Homail 发送的测试邮件
",
//To = "Roy.lei@pan-american-intl.com"
To = "johnny.yang@pan-american-intl.com"
});
return StatusCode(200, new { Message = "操作成功。" });
}
///
/// hotmail 发送邮件
///
[HttpPost("HotmailMerged")]
public async Task> HotmailMerged()
{
// 1. 获取当前北京时间 (CST)
var cstZone = CommonFun.GetCstZone();
var nowInCst = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, cstZone);
// 2. 构造昨天的北京时间范围:00:00:00 到 23:59:59
var yesterdayStart = nowInCst.Date.AddDays(-1); // 昨天的 00:00:00
var yesterdayEnd = yesterdayStart.AddDays(1).AddTicks(-1); // 昨天的 23:59:59
var res = await _hotmailService.GetMergedMessagesAsync(
new List() { "925554512@qq.com" },
yesterdayStart,
yesterdayEnd
);
return StatusCode(200, res);
}
///
/// hotmail 定时发送邮件 汇总 测试
///
[HttpPost("hotmailSummarySeedQW")]
public async Task> HotmailSummary()
{
ProcessAndNotifySummary.ProcessAndNotifySummaryAsync();
return StatusCode(200, "发送成功");
}
#region 微软 auth
[HttpGet("auth/url")]
public IActionResult GetAuthUrl()
{
var clientId = _config["AzureHotmail:ClientId"];
var redirectUri = _config["AzureHotmail:RedirectUri"]; // 需在 Azure Portal 注册
var scope = Uri.EscapeDataString("offline_access Mail.Read Mail.Send User.Read");
var url = $"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri={redirectUri}&response_mode=query&scope={scope}&state=alchemist";
return Ok(new { authUrl = url });
}
[HttpGet("auth/callback")]
public async Task HandleCallback([FromQuery] string code)
{
if (string.IsNullOrEmpty(code)) return BadRequest("授权码无效");
// 1. 换取令牌
var httpClient = _httpClientFactory.CreateClient();
var tokenRequest = new FormUrlEncodedContent(new Dictionary
{
{ "client_id", _config["AzureHotmail:ClientId"] },
{ "client_secret", _config["AzureHotmail:ClientSecret"] },
{ "code", code },
{ "redirect_uri", _config["AzureHotmail:RedirectUri"] },
{ "grant_type", "authorization_code" }
});
var response = await httpClient.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", tokenRequest);
var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
if (!response.IsSuccessStatusCode) return BadRequest(json.RootElement.ToString());
var root = json.RootElement;
var accessToken = root.GetProperty("access_token").GetString()!;
var refreshToken = root.GetProperty("refresh_token").GetString()!;
var expiresIn = root.GetProperty("expires_in").GetInt32();
// 2. 自动识别账户身份 【核心重构】:不再手动解析 JWT,而是请求 Graph 的 /me 接口
string userEmail = await GetEmailFromGraphApiAsync(accessToken);
// 3. 炼金产物:构造并存入 Redis
var userToken = new UserToken
{
Email = userEmail,
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresAt = DateTime.UtcNow.AddSeconds(expiresIn)
};
// 存入 Redis (使用我们之前的 RedisKeyPrefix: "MailAlchemy:Token:")
var redisKey = $"MailAlchemy:Token:{userEmail}";
await RedisRepository.RedisFactory.CreateRedisRepository().StringSetAsync(redisKey, System.Text.Json.JsonSerializer.Serialize(userToken), TimeSpan.FromDays(90));
return Ok(new
{
status = "Success",
account = userEmail,
message = "该个人账户已成功集成并启用分布式存储"
});
}
private async Task GetEmailFromGraphApiAsync(string accessToken)
{
var httpClient = _httpClientFactory.CreateClient();
// 使用 AccessToken 调用 Graph API 的个人信息接口
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var response = await httpClient.GetAsync("https://graph.microsoft.com/v1.0/me");
if (!response.IsSuccessStatusCode)
throw new Exception("无法通过 Graph API 获取用户信息");
using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var root = doc.RootElement;
// 个人账户优先取 mail,如果没有则取 userPrincipalName
return root.GetProperty("mail").GetString()
?? root.GetProperty("userPrincipalName").GetString()
?? throw new Exception("未能获取有效的 Email 地址");
}
#endregion
#region Microsoft Graph 邮箱测试(仅访问令牌)
private const string GraphAccessTokenHeader = "X-Graph-Access-Token";
///
/// 优先级:请求头 X-Graph-Access-Token → 查询 graphAccessToken → bodyToken(发信)。
///
private string? ResolveGraphAccessToken(string? queryToken = null, string? bodyToken = null)
{
var header = Request.Headers[GraphAccessTokenHeader].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(header))
return header.Trim();
if (!string.IsNullOrWhiteSpace(queryToken))
return queryToken.Trim();
if (!string.IsNullOrWhiteSpace(bodyToken))
return bodyToken.Trim();
return null;
}
///
/// 查询当前用户 GET /v1.0/me。必须提供 Graph 访问令牌。
///
[HttpGet("graph-mail/me")]
public async Task GraphMailMe(
[FromQuery] string? graphAccessToken = null,
CancellationToken cancellationToken = default)
{
var bearer = ResolveGraphAccessToken(graphAccessToken);
if (string.IsNullOrWhiteSpace(bearer))
return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
try
{
var json = await _microsoftGraphMailboxService.GetMeRawJsonAsync(bearer, cancellationToken);
if (string.IsNullOrEmpty(json))
return StatusCode(502, new { message = "Graph 返回空正文" });
return Content(json, "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "Graph Mail /me 失败");
return StatusCode(500, new { message = ex.Message });
}
}
///
/// 查询收件箱。必须提供 Graph 访问令牌(需 Mail.Read)。默认 sinceUtc 为 UTC 近 24 小时。
///
/// 起始时间(UTC),ISO8601
/// 或使用请求头 X-Graph-Access-Token
/// 取消标记
[HttpGet("graph-mail/inbox")]
public async Task GraphMailInbox(
[FromQuery] DateTime? sinceUtc = null,
[FromQuery] string? graphAccessToken = null,
CancellationToken cancellationToken = default)
{
var bearer = ResolveGraphAccessToken(graphAccessToken);
if (string.IsNullOrWhiteSpace(bearer))
return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:请求头 X-Graph-Access-Token 或查询参数 graphAccessToken" });
var since = sinceUtc ?? DateTime.UtcNow.AddHours(-24);
try
{
var json = await _microsoftGraphMailboxService.GetInboxMessagesJsonSinceAsync(since, bearer, cancellationToken);
if (string.IsNullOrEmpty(json))
return StatusCode(502, new { message = "Graph 返回空正文" });
return Content(json, "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "Graph Mail inbox 失败");
return StatusCode(500, new { message = ex.Message });
}
}
///
/// Graph sendMail 纯文本。必须提供令牌(需 Mail.Send):头 / 查询 / Body.graphAccessToken。
///
[HttpPost("graph-mail/send")]
public async Task GraphMailSend(
[FromBody] GraphMailSendTestRequest request,
[FromQuery] string? graphAccessToken = null,
CancellationToken cancellationToken = default)
{
if (request == null || string.IsNullOrWhiteSpace(request.ToEmail))
return BadRequest(new { message = "ToEmail 不能为空" });
var bearer = ResolveGraphAccessToken(graphAccessToken, request.GraphAccessToken);
if (string.IsNullOrWhiteSpace(bearer))
return Unauthorized(new { message = "必须提供 Microsoft Graph 访问令牌:X-Graph-Access-Token、?graphAccessToken 或 Body.graphAccessToken" });
var subject = string.IsNullOrWhiteSpace(request.Subject)
? $"OASystem Graph 测试邮件 {DateTime.Now:yyyy-MM-dd HH:mm:ss}"
: request.Subject!;
var body = request.Body ?? string.Empty;
try
{
await _microsoftGraphMailboxService.SendMailAsync(request.ToEmail.Trim(), subject, body, bearer, cancellationToken);
return Ok(new { ok = true, message = "sendMail 已提交", to = request.ToEmail.Trim(), subject });
}
catch (HttpRequestException ex)
{
return StatusCode(502, new { message = "Graph HTTP 错误", detail = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Graph Mail send 失败");
return StatusCode(500, new { message = ex.Message });
}
}
public class EmailAuthRedisCache
{
public string? AccessToken { get; set; }
public string? HomeAccountId { get; set; }
public string? UserTokenCacheBase64 { get; set; }
public string? ClientId { get; set; }
}
///
/// 从 Redis 读取 MSAL 缓存与 HomeAccountId,静默刷新 Graph access_token。
///
[HttpGet("graph-mail/refresh-token")]
public async Task RefreshAccessToken([FromQuery] string? redisKey = null)
{
var key = string.IsNullOrWhiteSpace(redisKey) ? "Email:AuthCache:345" : redisKey.Trim();
var redis = RedisFactory.CreateRedisRepository();
var json = await redis.StringGetRawAsync(key);
if (string.IsNullOrWhiteSpace(json))
{
return BadRequest(new { message = $"Redis 键 {key} 不存在或为空" });
}
EmailAuthRedisCache? cacheEntry;
try
{
cacheEntry = JsonConvert.DeserializeObject(json);
}
catch (System.Text.Json.JsonException ex)
{
_logger.LogWarning(ex, "Redis 键 {Key} 内容不是合法 JSON(应用 StringGetRawAsync + JSON,勿用 StringGetAsync,后者为 BinaryFormatter)", key);
return BadRequest(new { message = "Redis 值为 JSON 文本时须用 StringGetRawAsync 再反序列化;StringGetAsync 仅适用于 BinaryFormatter 写入的数据", detail = ex.Message });
}
if (cacheEntry == null
|| string.IsNullOrWhiteSpace(cacheEntry.UserTokenCacheBase64)
|| string.IsNullOrWhiteSpace(cacheEntry.HomeAccountId))
{
return BadRequest(new { message = "JSON 中缺少 UserTokenCacheBase64 或 HomeAccountId" });
}
var accessToken = await _microsoftGraphMailboxService.RefreshAccessTokenAsync(
cacheEntry.ClientId,
"common",
new[] { "Mail.Read", "User.Read", "Mail.Send" },
cacheEntry.UserTokenCacheBase64,
cacheEntry.HomeAccountId);
return Ok(new { accessToken });
}
///
/// Graph 发信测试请求体
///
public class GraphMailSendTestRequest
{
public string ToEmail { get; set; } = string.Empty;
public string? Subject { get; set; }
public string? Body { get; set; }
/// Microsoft Graph 访问令牌(也可用请求头 X-Graph-Access-Token)
public string? GraphAccessToken { get; set; }
}
#endregion
}
}