DoubaoService.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. using Newtonsoft.Json;
  2. using Newtonsoft.Json.Linq;
  3. using System.Net.Http.Headers;
  4. using System.Text;
  5. namespace OASystem.API.OAMethodLib.DoubaoAPI
  6. {
  7. public class DoubaoService : IDoubaoService
  8. {
  9. private readonly IHttpClientFactory _httpClientFactory;
  10. private readonly DoubaoSetting _doubaoSetting;
  11. private readonly ILogger<DoubaoService> _logger;
  12. public DoubaoService(IHttpClientFactory httpClientFactory, DoubaoSetting doubaoSetting, ILogger<DoubaoService> logger)
  13. {
  14. _httpClientFactory = httpClientFactory;
  15. _doubaoSetting = doubaoSetting;
  16. _logger = logger;
  17. }
  18. public async Task<string> CompleteChatAsync(List<DouBaoChatMessage> messages, CompleteChatOptions? options)
  19. {
  20. if (messages == null || !messages.Any())
  21. {
  22. _logger.LogError("消息不能为空 " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
  23. return null;
  24. }
  25. if (!messages.Any(x => x.Role == DouBaoRole.system))
  26. {
  27. messages.Insert(0,
  28. new DouBaoChatMessage()
  29. {
  30. Role = DouBaoRole.system,
  31. Content = "你是一个专业的AI助手,请根据用户的问题给出回答"
  32. });
  33. }
  34. options ??= new CompleteChatOptions();
  35. var httpClient = _httpClientFactory.CreateClient("Doubao");
  36. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey);
  37. var body = new Dictionary<string, object>
  38. {
  39. ["model"] = _doubaoSetting.EndpointId,
  40. ["messages"] = messages.Select(x => new { role = x.Role.ToString(), content = x.Content }).ToArray()
  41. };
  42. if (options.ThinkingOptions.IsThinking)
  43. {
  44. body["reasoning_effort"] = options.ThinkingOptions.ReasoningEffort.ToString().ToLower();
  45. body["thinking"] = new
  46. {
  47. type = "enabled",
  48. };
  49. }
  50. var json = JsonConvert.SerializeObject(body);
  51. var response = await httpClient.PostAsync(
  52. "chat/completions",
  53. new StringContent(json, Encoding.UTF8, "application/json")
  54. );
  55. response.EnsureSuccessStatusCode();
  56. var responseContent = await response.Content.ReadAsStringAsync();
  57. var doubaoResponse = JsonConvert.DeserializeObject<DoubaoResponse>(responseContent);
  58. return doubaoResponse.choices[0].message.content;
  59. }
  60. /// <summary>
  61. /// 多模态实现
  62. /// </summary>
  63. /// <param name="messages"></param>
  64. /// <param name="options"></param>
  65. /// <returns></returns>
  66. public async Task<string> CompleteMultimodalChatAsync(List<DoubaoMultimodalChatMessage> messages, CompleteMultimodalChatOptions? options)
  67. {
  68. // 1. 入参校验
  69. if (messages == null || !messages.Any())
  70. {
  71. _logger.LogError("多模态消息不能为空");
  72. return null;
  73. }
  74. if (string.IsNullOrWhiteSpace(_doubaoSetting.ApiKey) || string.IsNullOrWhiteSpace(_doubaoSetting.EndpointId) || string.IsNullOrWhiteSpace(_doubaoSetting.BaseAddress))
  75. {
  76. _logger.LogError("Doubao多模态配置不完整(ApiKey/EndpointId/BaseAddress)");
  77. return null;
  78. }
  79. options ??= new CompleteMultimodalChatOptions();
  80. var httpClient = _httpClientFactory.CreateClient("Doubao");
  81. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey);
  82. httpClient.Timeout = TimeSpan.FromSeconds(60); // 超时控制
  83. // 2. 构建请求体(核心:修正type值)
  84. var requestBody = new
  85. {
  86. model = _doubaoSetting.EndpointId,
  87. input = messages.Select(msg => new
  88. {
  89. role = msg.Role.ToLower(), // 接口要求小写
  90. type = "message", // 接口必传的消息类型(固定值)
  91. content = msg.Content.Select(contentItem =>
  92. {
  93. // 自动修正type值,避免传入text/image_url
  94. var correctedType = contentItem.Type.ToLower() switch
  95. {
  96. "text" => "input_text", // 把text修正为input_text
  97. "image_url" => "input_image", // 把image_url修正为input_image
  98. "file" => "input_file", // 文件类型
  99. _ => contentItem.Type // 其他合法值直接使用
  100. };
  101. // 构建内容项(根据类型补充字段)
  102. var contentObj = new Dictionary<string, object>
  103. {
  104. ["type"] = correctedType
  105. };
  106. if (correctedType == "input_text" && !string.IsNullOrWhiteSpace(contentItem.Text))
  107. {
  108. contentObj["text"] = contentItem.Text;
  109. }
  110. else if (correctedType == "input_image" && contentItem.ImageUrl != null && !string.IsNullOrWhiteSpace(contentItem.ImageUrl.Url))
  111. {
  112. // Responses API 要求 image_url 为字符串,而不是对象 { url: ... }
  113. contentObj["image_url"] = contentItem.ImageUrl.Url;
  114. }
  115. else if (correctedType == "input_file" && !string.IsNullOrWhiteSpace(contentItem.FileId))
  116. {
  117. contentObj["file_id"] = contentItem.FileId;
  118. }
  119. return contentObj;
  120. }).ToArray()
  121. }).ToArray(),
  122. temperature = options.Temperature,
  123. thinking = options.ThinkingOptions.IsThinking ? new { type = "enabled" } : null,
  124. reasoning = options.ThinkingOptions.IsThinking ? new { effort = options.ThinkingOptions.ReasoningEffort.ToLower() } : null,
  125. max_output_tokens = options.MaxOutputTokens,
  126. previous_response_id = options.PreviousResponseId,
  127. expire_at = options.ExpireAt
  128. };
  129. try
  130. {
  131. // 3. 发送请求
  132. var json = JsonConvert.SerializeObject(requestBody, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
  133. _logger.LogDebug($"Doubao多模态请求体:{json}");
  134. var response = await httpClient.PostAsync(
  135. "responses",
  136. new StringContent(json, Encoding.UTF8, "application/json")
  137. );
  138. var responseContent = await response.Content.ReadAsStringAsync();
  139. _logger.LogDebug($"Doubao多模态响应:{response.StatusCode} {responseContent}");
  140. if (!response.IsSuccessStatusCode)
  141. {
  142. _logger.LogError($"多模态对话调用失败: {response.StatusCode}, {responseContent}");
  143. return null;
  144. }
  145. // 4. 解析响应(优先兼容 Responses API 的 output 结构)
  146. var parsed = JObject.Parse(responseContent);
  147. // 新结构:output -> type=message -> content -> type=output_text -> text
  148. var outputText = parsed["output"]?
  149. .FirstOrDefault(x => string.Equals(x?["type"]?.ToString(), "message", StringComparison.OrdinalIgnoreCase))?["content"]?
  150. .FirstOrDefault(x => string.Equals(x?["type"]?.ToString(), "output_text", StringComparison.OrdinalIgnoreCase))?["text"]?
  151. .ToString();
  152. if (!string.IsNullOrWhiteSpace(outputText))
  153. {
  154. return outputText;
  155. }
  156. // 旧结构兜底:choices -> message -> content -> input_text
  157. var doubaoResponse = JsonConvert.DeserializeObject<DoubaoMultimodalResponse>(responseContent);
  158. var textContent = doubaoResponse?.Choices?.FirstOrDefault()
  159. ?.Message?.Content?
  160. .FirstOrDefault(c =>
  161. string.Equals(c.Type, "input_text", StringComparison.OrdinalIgnoreCase) ||
  162. string.Equals(c.Type, "output_text", StringComparison.OrdinalIgnoreCase))?
  163. .Text;
  164. if (string.IsNullOrWhiteSpace(textContent))
  165. {
  166. _logger.LogError("Doubao多模态响应无有效结果");
  167. return null;
  168. }
  169. return textContent;
  170. }
  171. catch (TaskCanceledException ex)
  172. {
  173. _logger.LogError(ex, "Doubao多模态接口调用超时");
  174. return null;
  175. }
  176. catch (Exception ex)
  177. {
  178. _logger.LogError(ex, "Doubao多模态对话调用异常");
  179. return null;
  180. }
  181. }
  182. public async Task<DoubaoFileResponse> UploadFileAsync(Stream fileStream, string fileName, string purpose = "fine-tune")
  183. {
  184. var httpClient = _httpClientFactory.CreateClient("Doubao");
  185. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey);
  186. using var content = new MultipartFormDataContent();
  187. // 添加 purpose 参数
  188. content.Add(new StringContent(purpose), "purpose");
  189. // 添加文件流
  190. var fileContent = new StreamContent(fileStream);
  191. fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
  192. content.Add(fileContent, "file", fileName);
  193. var response = await httpClient.PostAsync("files", content);
  194. if (!response.IsSuccessStatusCode)
  195. {
  196. var errorContent = await response.Content.ReadAsStringAsync();
  197. _logger.LogError($"上传文件失败: {response.StatusCode}, {errorContent}");
  198. response.EnsureSuccessStatusCode();
  199. }
  200. var responseContent = await response.Content.ReadAsStringAsync();
  201. return JsonConvert.DeserializeObject<DoubaoFileResponse>(responseContent);
  202. }
  203. public async Task<DoubaoFileListResponse> ListFilesAsync()
  204. {
  205. var httpClient = _httpClientFactory.CreateClient("Doubao");
  206. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey);
  207. var response = await httpClient.GetAsync("files");
  208. response.EnsureSuccessStatusCode();
  209. var responseContent = await response.Content.ReadAsStringAsync();
  210. return JsonConvert.DeserializeObject<DoubaoFileListResponse>(responseContent);
  211. }
  212. public async Task<bool> DeleteFileAsync(string fileId)
  213. {
  214. var httpClient = _httpClientFactory.CreateClient("Doubao");
  215. httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _doubaoSetting.ApiKey);
  216. var response = await httpClient.DeleteAsync($"files/{fileId}");
  217. return response.IsSuccessStatusCode;
  218. }
  219. }
  220. }