| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- 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<DoubaoService> _logger;
- public DoubaoService(IHttpClientFactory httpClientFactory, DoubaoSetting doubaoSetting, ILogger<DoubaoService> logger)
- {
- _httpClientFactory = httpClientFactory;
- _doubaoSetting = doubaoSetting;
- _logger = logger;
- }
- public async Task<string> CompleteChatAsync(List<DouBaoChatMessage> 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<string, object>
- {
- ["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<DoubaoResponse>(responseContent);
- return doubaoResponse.choices[0].message.content;
- }
- /// <summary>
- /// 多模态实现
- /// </summary>
- /// <param name="messages"></param>
- /// <param name="options"></param>
- /// <returns></returns>
- public async Task<string> CompleteMultimodalChatAsync(List<DoubaoMultimodalChatMessage> 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<string, object>
- {
- ["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<DoubaoMultimodalResponse>(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<DoubaoFileResponse> 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<DoubaoFileResponse>(responseContent);
- }
- public async Task<DoubaoFileListResponse> 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<DoubaoFileListResponse>(responseContent);
- }
- public async Task<bool> 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;
- }
- /// <summary>
- /// Word(.docx)二进制流转PDF二进制流。
- /// </summary>
- /// <param name="docxStream">Word二进制流(仅支持.docx)</param>
- /// <param name="saveDirectory">PDF保存目录,空则使用程序目录下 temp/pdf</param>
- /// <param name="outputFileName">输出PDF文件名(可不带后缀)</param>
- /// <returns>PDF二进制流(MemoryStream)</returns>
- 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);
- }
- }
- }
- }
|