|
@@ -1,4 +1,5 @@
|
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json;
|
|
|
|
|
+using Newtonsoft.Json.Linq;
|
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Headers;
|
|
|
using System.Text;
|
|
using System.Text;
|
|
|
|
|
|
|
@@ -26,10 +27,13 @@ namespace OASystem.API.OAMethodLib.DoubaoAPI
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if(!messages.Any(x => x.Role == DouBaoRole.system)){
|
|
|
|
|
- messages.Insert(0,
|
|
|
|
|
- new DouBaoChatMessage() {
|
|
|
|
|
- Role = DouBaoRole.system, Content = "你是一个专业的AI助手,请根据用户的问题给出回答"
|
|
|
|
|
|
|
+ if (!messages.Any(x => x.Role == DouBaoRole.system))
|
|
|
|
|
+ {
|
|
|
|
|
+ messages.Insert(0,
|
|
|
|
|
+ new DouBaoChatMessage()
|
|
|
|
|
+ {
|
|
|
|
|
+ Role = DouBaoRole.system,
|
|
|
|
|
+ Content = "你是一个专业的AI助手,请根据用户的问题给出回答"
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -46,14 +50,15 @@ namespace OASystem.API.OAMethodLib.DoubaoAPI
|
|
|
if (options.ThinkingOptions.IsThinking)
|
|
if (options.ThinkingOptions.IsThinking)
|
|
|
{
|
|
{
|
|
|
body["reasoning_effort"] = options.ThinkingOptions.ReasoningEffort.ToString().ToLower();
|
|
body["reasoning_effort"] = options.ThinkingOptions.ReasoningEffort.ToString().ToLower();
|
|
|
- body["thinking"] = new {
|
|
|
|
|
|
|
+ body["thinking"] = new
|
|
|
|
|
+ {
|
|
|
type = "enabled",
|
|
type = "enabled",
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var json = JsonConvert.SerializeObject(body);
|
|
var json = JsonConvert.SerializeObject(body);
|
|
|
var response = await httpClient.PostAsync(
|
|
var response = await httpClient.PostAsync(
|
|
|
- _doubaoSetting.BaseAddress,
|
|
|
|
|
|
|
+ "chat/completions",
|
|
|
new StringContent(json, Encoding.UTF8, "application/json")
|
|
new StringContent(json, Encoding.UTF8, "application/json")
|
|
|
);
|
|
);
|
|
|
|
|
|
|
@@ -62,5 +67,192 @@ namespace OASystem.API.OAMethodLib.DoubaoAPI
|
|
|
var doubaoResponse = JsonConvert.DeserializeObject<DoubaoResponse>(responseContent);
|
|
var doubaoResponse = JsonConvert.DeserializeObject<DoubaoResponse>(responseContent);
|
|
|
return doubaoResponse.choices[0].message.content;
|
|
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.FromSeconds(60); // 超时控制
|
|
|
|
|
+
|
|
|
|
|
+ // 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 旧结构兜底:choices -> message -> content -> input_text
|
|
|
|
|
+ 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 = "fine-tune")
|
|
|
|
|
+ {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|