using Aspose.Words; using OASystem.API.OAMethodLib.File; using System.Net.Http.Headers; using System.Text; namespace OASystem.API.OAMethodLib.DoubaoAPI { public class DoubaoService : IDoubaoService { private readonly IHttpClientFactory _httpClientFactory; private readonly DoubaoSetting _doubaoSetting; private readonly ILogger _logger; public DoubaoService(IHttpClientFactory httpClientFactory, DoubaoSetting doubaoSetting, ILogger logger) { _httpClientFactory = httpClientFactory; _doubaoSetting = doubaoSetting; _logger = logger; } public async Task CompleteChatAsync(List messages, CompleteChatOptions? options) { if (messages == null || !messages.Any()) { _logger.LogError("消息不能为空 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); return null; } if (!messages.Any(x => x.Role == DouBaoRole.system)) { messages.Insert(0, new DouBaoChatMessage() { Role = DouBaoRole.system, Content = "你是一个专业的AI助手,请根据用户的问题给出回答" }); } options ??= new CompleteChatOptions(); var httpClient = _httpClientFactory.CreateClient("Doubao"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey); var body = new Dictionary { ["model"] = _doubaoSetting.EndpointId, ["messages"] = messages.Select(x => new { role = x.Role.ToString(), content = x.Content }).ToArray() }; if (options.ThinkingOptions.IsThinking) { body["reasoning_effort"] = options.ThinkingOptions.ReasoningEffort.ToString().ToLower(); body["thinking"] = new { type = "enabled", }; } var json = JsonConvert.SerializeObject(body); var response = await httpClient.PostAsync( "chat/completions", new StringContent(json, Encoding.UTF8, "application/json") ); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); var doubaoResponse = JsonConvert.DeserializeObject(responseContent); return doubaoResponse.choices[0].message.content; } /// /// 多模态实现 /// /// /// /// public async Task CompleteMultimodalChatAsync(List messages, CompleteMultimodalChatOptions? options) { // 1. 入参校验 if (messages == null || !messages.Any()) { _logger.LogError("多模态消息不能为空"); return null; } if (string.IsNullOrWhiteSpace(_doubaoSetting.ApiKey) || string.IsNullOrWhiteSpace(_doubaoSetting.EndpointId) || string.IsNullOrWhiteSpace(_doubaoSetting.BaseAddress)) { _logger.LogError("Doubao多模态配置不完整(ApiKey/EndpointId/BaseAddress)"); return null; } options ??= new CompleteMultimodalChatOptions(); var httpClient = _httpClientFactory.CreateClient("Doubao"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey); httpClient.Timeout = TimeSpan.FromMinutes(10); // 超时控制 // 2. 构建请求体(核心:修正type值) var requestBody = new { model = _doubaoSetting.EndpointId, input = messages.Select(msg => new { role = msg.Role.ToLower(), // 接口要求小写 type = "message", // 接口必传的消息类型(固定值) content = msg.Content.Select(contentItem => { // 自动修正type值,避免传入text/image_url var correctedType = contentItem.Type.ToLower() switch { "text" => "input_text", // 把text修正为input_text "image_url" => "input_image", // 把image_url修正为input_image "file" => "input_file", // 文件类型 _ => contentItem.Type // 其他合法值直接使用 }; // 构建内容项(根据类型补充字段) var contentObj = new Dictionary { ["type"] = correctedType }; if (correctedType == "input_text" && !string.IsNullOrWhiteSpace(contentItem.Text)) { contentObj["text"] = contentItem.Text; } else if (correctedType == "input_image" && contentItem.ImageUrl != null && !string.IsNullOrWhiteSpace(contentItem.ImageUrl.Url)) { // Responses API 要求 image_url 为字符串,而不是对象 { url: ... } contentObj["image_url"] = contentItem.ImageUrl.Url; } else if (correctedType == "input_file" && !string.IsNullOrWhiteSpace(contentItem.FileId)) { contentObj["file_id"] = contentItem.FileId; } return contentObj; }).ToArray() }).ToArray(), temperature = options.Temperature, thinking = options.ThinkingOptions.IsThinking ? new { type = "enabled" } : null, reasoning = options.ThinkingOptions.IsThinking ? new { effort = options.ThinkingOptions.ReasoningEffort.ToLower() } : null, max_output_tokens = options.MaxOutputTokens, previous_response_id = options.PreviousResponseId, expire_at = options.ExpireAt }; try { // 3. 发送请求 var json = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); _logger.LogDebug($"Doubao多模态请求体:{json}"); var response = await httpClient.PostAsync( "responses", new StringContent(json, Encoding.UTF8, "application/json") ); var responseContent = await response.Content.ReadAsStringAsync(); _logger.LogDebug($"Doubao多模态响应:{response.StatusCode} {responseContent}"); if (!response.IsSuccessStatusCode) { _logger.LogError($"多模态对话调用失败: {response.StatusCode}, {responseContent}"); return null; } // 4. 解析响应(优先兼容 Responses API 的 output 结构) var parsed = JObject.Parse(responseContent); // 新结构:output -> type=message -> content -> type=output_text -> text var outputText = parsed["output"]? .FirstOrDefault(x => string.Equals(x?["type"]?.ToString(), "message", StringComparison.OrdinalIgnoreCase))?["content"]? .FirstOrDefault(x => string.Equals(x?["type"]?.ToString(), "output_text", StringComparison.OrdinalIgnoreCase))?["text"]? .ToString(); if (!string.IsNullOrWhiteSpace(outputText)) { return outputText; } var doubaoResponse = JsonConvert.DeserializeObject(responseContent); var textContent = doubaoResponse?.Choices?.FirstOrDefault() ?.Message?.Content? .FirstOrDefault(c => string.Equals(c.Type, "input_text", StringComparison.OrdinalIgnoreCase) || string.Equals(c.Type, "output_text", StringComparison.OrdinalIgnoreCase))? .Text; if (string.IsNullOrWhiteSpace(textContent)) { _logger.LogError("Doubao多模态响应无有效结果"); return null; } return textContent; } catch (TaskCanceledException ex) { _logger.LogError(ex, "Doubao多模态接口调用超时"); return null; } catch (Exception ex) { _logger.LogError(ex, "Doubao多模态对话调用异常"); return null; } } public async Task UploadFileAsync(Stream fileStream, string fileName, string purpose = "user_data") { var httpClient = _httpClientFactory.CreateClient("Doubao"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey); using var content = new MultipartFormDataContent(); // 添加 purpose 参数 content.Add(new StringContent(purpose), "purpose"); // 添加文件流 var fileContent = new StreamContent(fileStream); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); content.Add(fileContent, "file", fileName); var response = await httpClient.PostAsync("files", content); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError($"上传文件失败: {response.StatusCode}, {errorContent}"); response.EnsureSuccessStatusCode(); } var responseContent = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(responseContent); } public async Task ListFilesAsync() { var httpClient = _httpClientFactory.CreateClient("Doubao"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey); var response = await httpClient.GetAsync("files"); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(responseContent); } public async Task DeleteFileAsync(string fileId) { var httpClient = _httpClientFactory.CreateClient("Doubao"); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey); var response = await httpClient.DeleteAsync($"files/{fileId}"); return response.IsSuccessStatusCode; } /// /// Word(.docx)二进制流转PDF二进制流。 /// /// Word二进制流(仅支持.docx) /// PDF保存目录,空则使用程序目录下 temp/pdf /// 输出PDF文件名(可不带后缀) /// PDF二进制流(MemoryStream) public static MemoryStream ConvertDocxStreamToPdfStream( Stream docxStream, string? saveDirectory = null, string? outputFileName = null) { if (docxStream == null || !docxStream.CanRead) { throw new ArgumentException("Word二进制流不可读或为空", nameof(docxStream)); } if (docxStream.CanSeek) { docxStream.Position = 0; } try { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); AsposeHelper.removewatermark_v2180(); var wordDoc = new Document(docxStream); using var pdfStream = new MemoryStream(); wordDoc.Save(pdfStream, SaveFormat.Pdf); pdfStream.Position = 0; var pdfBytes = pdfStream.ToArray(); // var finalSaveDirectory = string.IsNullOrWhiteSpace(saveDirectory) // ? global::System.IO.Path.Combine(AppContext.BaseDirectory, "temp", "pdf") // : saveDirectory; // // global::System.IO.Directory.CreateDirectory(finalSaveDirectory); // // var finalFileName = string.IsNullOrWhiteSpace(outputFileName) // ? $"docx_to_pdf_{DateTime.Now:yyyyMMdd_HHmmss_fff}.pdf" // : (outputFileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) ? outputFileName : $"{outputFileName}.pdf"); // // var savePath = global::System.IO.Path.Combine(finalSaveDirectory, finalFileName); // global::System.IO.File.WriteAllBytes(savePath, pdfBytes); return new MemoryStream(pdfBytes); } catch (Exception ex) { var rootMessage = ex.InnerException?.Message ?? ex.Message; throw new InvalidOperationException($"Aspose文档转换失败: {rootMessage}", ex); } } } }