123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- using System.Net.Http;
- using System.Net.Http.Headers;
- using System.Text.Json;
- using System.Text.Json.Serialization;
- using JsonSerializer = System.Text.Json.JsonSerializer;
- using System.IO;
- namespace OASystem.API.OAMethodLib.DeepSeekAPI
- {
- /// <summary>
- /// DeepSeek API 服务实现
- /// </summary>
- public class DeepSeekService : IDeepSeekService
- {
- private readonly HttpClient _httpClient;
- private readonly ILogger<DeepSeekService> _logger;
- private readonly IHostEnvironment _hostEnvironment;
- /// <summary>
- /// 配置文件
- /// </summary>
- private DeepSeek DeepSeek { get; set; }
- /// <summary>
- /// 构造函数
- /// </summary>
- public DeepSeekService(IHostEnvironment hostEnvironment, ILogger<DeepSeekService> logger, HttpClient httpClient)
- {
- _hostEnvironment = hostEnvironment;
- _httpClient = httpClient;
- _logger = logger;
- DeepSeek = AutofacIocManager.Instance.GetService<DeepSeek>();
- // 设置基础地址和认证头
- _httpClient.BaseAddress = new Uri(DeepSeek.BaseAddress ?? "https://api.deepseek.com/v1/");
- _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", DeepSeek.ApiKey);
- _httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
- }
- /// <summary>
- /// 测试API连通性
- /// </summary>
- /// <returns></returns>
- public async Task<bool> TestApiConnectivityAsync()
- {
- try
- {
- var response = await _httpClient.GetAsync("models");
- return response.IsSuccessStatusCode;
- }
- catch
- {
- return false;
- }
- }
- /// <summary>
- /// 检查可用端点
- /// </summary>
- /// <returns></returns>
- public async Task<List<string>> DiscoverAvailableEndpointsAsync()
- {
- var endpoints = new List<string>
- {
- "models",
- "chat/completions",
- "files",
- "uploads",
- "documents",
- "assistants"
- };
- var availableEndpoints = new List<string>();
- foreach (var endpoint in endpoints)
- {
- try
- {
- var response = await _httpClient.GetAsync(endpoint);
- if (response.IsSuccessStatusCode)
- {
- availableEndpoints.Add(endpoint);
- Console.WriteLine($"✅ 端点可用: {endpoint}");
- }
- else
- {
- Console.WriteLine($"❌ 端点不可用: {endpoint} - {response.StatusCode}");
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine($"⚠️ 端点检查错误: {endpoint} - {ex.Message}");
- }
- }
- return availableEndpoints;
- }
- /// <summary>
- /// 上传文件到DeepSeek API
- /// </summary>
- public async Task<DeepSeekFileUploadResponse> UploadFileAsync(IFormFile file, string purpose = "assistants")
- {
- try
- {
- _logger.LogInformation("开始上传文件: {FileName}, 大小: {Size} bytes", file.FileName, file.Length);
- // 检查文件大小
- if (file.Length > 512 * 1024 * 1024) // 512MB限制
- {
- throw new Exception($"文件大小超过限制: {file.Length} bytes");
- }
- using var content = new MultipartFormDataContent();
- using var fileStream = file.OpenReadStream();
- var fileContent = new StreamContent(fileStream);
- fileContent.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(file.FileName));
- content.Add(fileContent, "file", file.FileName);
- content.Add(new StringContent(purpose), "purpose");
- var response = await _httpClient.PostAsync("files", content);
- _logger.LogInformation("文件上传路径:{filePath}", response.RequestMessage.RequestUri);
- response.EnsureSuccessStatusCode();
- var responseContent = await response.Content.ReadAsStringAsync();
- var result = JsonSerializer.Deserialize<DeepSeekFileUploadResponse>(responseContent, new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
- _logger.LogInformation("文件上传成功: {FileName}, FileId: {FileId}", file.FileName, result.Id);
- return result;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "文件上传失败: {FileName}", file.FileName);
- throw;
- }
- }
- /// <summary>
- /// 批量上传文件
- /// </summary>
- public async Task<List<FileUploadResult>> UploadFilesAsync(List<IFormFile> files, string purpose = "assistants")
- {
- var results = new List<FileUploadResult>();
- foreach (var file in files)
- {
- var result = new FileUploadResult
- {
- FileName = file.FileName,
- FileSize = file.Length
- };
- try
- {
- var uploadResponse = await UploadFileAsync(file, purpose);
- result.FileId = uploadResponse.Id;
- result.Success = true;
- result.Status = uploadResponse.Status;
- result.Message = "上传成功";
- }
- catch (Exception ex)
- {
- result.Success = false;
- result.Message = $"上传失败: {ex.Message}";
- result.Status = "error";
- }
- results.Add(result);
- }
- return results;
- }
- /// <summary>
- /// 获取文件列表
- /// </summary>
- public async Task<DeepSeekFileListResponse> ListFilesAsync()
- {
- try
- {
- var response = await _httpClient.GetAsync("files");
- response.EnsureSuccessStatusCode();
- var responseContent = await response.Content.ReadAsStringAsync();
- return JsonSerializer.Deserialize<DeepSeekFileListResponse>(responseContent, new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "获取文件列表失败");
- throw;
- }
- }
- /// <summary>
- /// 获取文件信息
- /// </summary>
- public async Task<DeepSeekFileUploadResponse> GetFileInfoAsync(string fileId)
- {
- try
- {
- var response = await _httpClient.GetAsync($"files/{fileId}");
- response.EnsureSuccessStatusCode();
- var responseContent = await response.Content.ReadAsStringAsync();
- return JsonSerializer.Deserialize<DeepSeekFileUploadResponse>(responseContent, new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "获取文件信息失败: {FileId}", fileId);
- throw;
- }
- }
- /// <summary>
- /// 删除文件
- /// </summary>
- public async Task<bool> DeleteFileAsync(string fileId)
- {
- try
- {
- var response = await _httpClient.DeleteAsync($"files/{fileId}");
- response.EnsureSuccessStatusCode();
- _logger.LogInformation("文件删除成功: {FileId}", fileId);
- return true;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "文件删除失败: {FileId}", fileId);
- return false;
- }
- }
- /// <summary>
- /// 使用已上传的文件进行聊天
- /// </summary>
- public async Task<ApiResponse> ChatWithFilesAsync(List<string> fileIds, string question, string model = "deepseek-chat", float temperature = 0.7f, int maxTokens = 4000)
- {
- try
- {
- // 等待所有文件处理完成
- var processedFiles = new List<DeepSeekFileUploadResponse>();
- foreach (var fileId in fileIds)
- {
- var fileInfo = await WaitForFileProcessingAsync(fileId);
- processedFiles.Add(fileInfo);
- }
- // 构建消息内容
- var messageContent = new List<object>
- {
- new { type = "text", text = question }
- };
- // 添加文件引用
- foreach (var file in processedFiles)
- {
- messageContent.Add(new
- {
- type = "file",
- file_id = file.Id
- });
- }
- var request = new DeepSeekChatWithFilesRequest
- {
- Model = model,
- Messages = new List<FileMessage>
- {
- new FileMessage
- {
- Role = "user",
- Content = messageContent
- }
- },
- Temperature = temperature,
- MaxTokens = maxTokens
- };
- var jsonContent = JsonSerializer.Serialize(request, new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- });
- var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
- var response = await _httpClient.PostAsync("chat/completions", httpContent);
- response.EnsureSuccessStatusCode();
- var responseContent = await response.Content.ReadAsStringAsync();
- var chatResponse = JsonSerializer.Deserialize<DeepSeekResponse>(responseContent, new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true
- });
- return new ApiResponse
- {
- Success = true,
- Message = "聊天成功",
- Answer = chatResponse.Choices[0].Message.Content,
- TokensUsed = chatResponse.Usage.TotalTokens
- };
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "使用文件聊天失败");
- throw;
- }
- }
- /// <summary>
- /// 等待文件处理完成
- /// </summary>
- public async Task<DeepSeekFileUploadResponse> WaitForFileProcessingAsync(string fileId, int timeoutSeconds = 60)
- {
- var startTime = DateTime.UtcNow;
- var timeout = TimeSpan.FromSeconds(timeoutSeconds);
- while (DateTime.UtcNow - startTime < timeout)
- {
- var fileInfo = await GetFileInfoAsync(fileId);
- if (fileInfo.Status == "processed")
- {
- _logger.LogInformation("文件处理完成: {FileId}", fileId);
- return fileInfo;
- }
- else if (fileInfo.Status == "error")
- {
- throw new Exception($"文件处理失败: {fileId}");
- }
- // 等待2秒后重试
- await Task.Delay(2000);
- }
- throw new TimeoutException($"文件处理超时: {fileId}");
- }
- /// <summary>
- /// 根据文件名获取Content-Type
- /// </summary>
- private static string GetContentType(string fileName)
- {
- var extension = Path.GetExtension(fileName).ToLower();
- return extension switch
- {
- ".txt" => "text/plain",
- ".pdf" => "application/pdf",
- ".json" => "application/json",
- ".csv" => "text/csv",
- ".html" => "text/html",
- ".htm" => "text/html",
- ".md" => "text/markdown",
- _ => "application/octet-stream"
- };
- }
- #region 项目相关
- /// <summary>
- /// 读取项目内的指定文件并转换为IFormFile
- /// </summary>
- public async Task<ProjectFileReadResponse> ReadProjectFilesAsync(List<string> relativePaths)
- {
- var response = new ProjectFileReadResponse();
- var projectRoot = GetProjectRootPath();
- if (projectRoot.Contains("OASystem.Api"))
- {
- projectRoot = projectRoot.Replace("OASystem.Api", "");
- }
- _logger.LogInformation("开始读取项目文件,数量: {Count}", relativePaths.Count);
- foreach (var relativePath in relativePaths)
- {
- var fileInfo = new ProjectFileInfo
- {
- RelativePath = relativePath
- };
- try
- {
- var fullPath = Path.Combine(projectRoot, relativePath);
- fileInfo.FullPath = fullPath;
- if (!System.IO.File.Exists(fullPath))
- {
- throw new FileNotFoundException($"文件不存在: {relativePath}");
- }
- var fileInfoObj = new FileInfo(fullPath);
- fileInfo.FileSize = fileInfoObj.Length;
- fileInfo.LastModified = fileInfoObj.LastWriteTime;
- // 转换为IFormFile
- fileInfo.FormFile = await ConvertToFormFileAsync(fullPath);
- fileInfo.Success = true;
- _logger.LogInformation("文件读取成功: {FileName}, 大小: {Size} bytes", relativePath, fileInfo.FileSize);
- }
- catch (Exception ex)
- {
- fileInfo.Success = false;
- fileInfo.ErrorMessage = ex.Message;
- _logger.LogError(ex, "文件读取失败: {FileName}", relativePath);
- }
- response.FileInfos.Add(fileInfo);
- }
- response.Success = response.SuccessCount > 0;
- response.Message = $"成功读取 {response.SuccessCount} 个文件,失败 {response.FailureCount} 个";
- return response;
- }
- /// <summary>
- /// 读取项目内指定目录的文件
- /// </summary>
- public async Task<ProjectFileReadResponse> ReadProjectDirectoryAsync(string directoryPath, string searchPattern = "*.cs", bool recursive = false)
- {
- var response = new ProjectFileReadResponse();
- var projectRoot = GetProjectRootPath();
- var fullDirectoryPath = Path.Combine(projectRoot, directoryPath);
- _logger.LogInformation("开始读取目录文件: {Directory}, 模式: {Pattern}", directoryPath, searchPattern);
- if (!Directory.Exists(fullDirectoryPath))
- {
- response.Success = false;
- response.Message = $"目录不存在: {directoryPath}";
- return response;
- }
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
- var files = Directory.GetFiles(fullDirectoryPath, searchPattern, searchOption);
- _logger.LogInformation("找到 {FileCount} 个文件", files.Length);
- foreach (var filePath in files)
- {
- var relativePath = Path.GetRelativePath(projectRoot, filePath);
- var fileInfo = new ProjectFileInfo
- {
- RelativePath = relativePath,
- FullPath = filePath
- };
- try
- {
- var fileInfoObj = new FileInfo(filePath);
- fileInfo.FileSize = fileInfoObj.Length;
- fileInfo.LastModified = fileInfoObj.LastWriteTime;
- // 转换为IFormFile
- fileInfo.FormFile = await ConvertToFormFileAsync(filePath);
- fileInfo.Success = true;
- _logger.LogDebug("文件读取成功: {FileName}", relativePath);
- }
- catch (Exception ex)
- {
- fileInfo.Success = false;
- fileInfo.ErrorMessage = ex.Message;
- _logger.LogError(ex, "文件读取失败: {FileName}", relativePath);
- }
- response.FileInfos.Add(fileInfo);
- }
- response.Success = response.SuccessCount > 0;
- response.Message = $"成功读取 {response.SuccessCount} 个文件,失败 {response.FailureCount} 个";
- return response;
- }
- /// <summary>
- /// 获取项目根目录路径
- /// </summary>
- public string GetProjectRootPath()
- {
- // 在开发环境中使用ContentRootPath,在生产环境中可能需要调整
- return _hostEnvironment.ContentRootPath;
- }
- /// <summary>
- /// 检查文件是否存在
- /// </summary>
- public bool FileExists(string relativePath)
- {
- var fullPath = Path.Combine(GetProjectRootPath(), relativePath);
- return System.IO.File.Exists(fullPath);
- }
- /// <summary>
- /// 将物理文件转换为IFormFile
- /// </summary>
- public async Task<IFormFile> ConvertToFormFileAsync(string filePath)
- {
- var fileInfo = new FileInfo(filePath);
- var fileName = Path.GetFileName(filePath);
- // 读取文件内容
- byte[] fileBytes;
- using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
- {
- fileBytes = new byte[fileStream.Length];
- await fileStream.ReadAsync(fileBytes, 0, (int)fileStream.Length);
- }
- // 创建IFormFile
- return new FormFile(
- new MemoryStream(fileBytes),
- 0,
- fileBytes.Length,
- "file",
- fileName)
- {
- Headers = new HeaderDictionary(),
- ContentType = GetFileContentType(fileName)
- };
- }
- /// <summary>
- /// 根据文件名获取Content-Type
- /// </summary>
- private string GetFileContentType(string fileName)
- {
- var extension = Path.GetExtension(fileName).ToLower();
- return extension switch
- {
- ".cs" => "text/plain",
- ".txt" => "text/plain",
- ".json" => "application/json",
- ".xml" => "application/xml",
- ".html" => "text/html",
- ".htm" => "text/html",
- ".css" => "text/css",
- ".js" => "application/javascript",
- ".ts" => "application/typescript",
- ".py" => "text/x-python",
- ".java" => "text/x-java",
- ".cpp" => "text/x-c++",
- ".c" => "text/x-c",
- ".h" => "text/x-c",
- ".md" => "text/markdown",
- _ => "application/octet-stream"
- };
- }
- /// <summary>
- /// 读取特定的签证申请表单文件
- /// </summary>
- /// <param name="fileName"></param>
- /// <returns></returns>
- public async Task<ProjectFileReadResponse> ReadVisaFormFileAsync(string fileName)
- {
- var specificPath = $@"OASystem\OASystem.Domain\ViewModels\VisaFormDetails\{fileName}";
- return await ReadProjectFilesAsync(new List<string> { specificPath });
- }
- /// <summary>
- /// 读取OASystem项目中的所有CS文件
- /// </summary>
- public async Task<ProjectFileReadResponse> ReadOASystemFilesAsync(string searchPattern = "*.cs", bool recursive = true)
- {
- return await ReadProjectDirectoryAsync(@"OASystem", searchPattern, recursive);
- }
- /// <summary>
- /// 读取指定域模型中的CS文件
- /// </summary>
- public async Task<ProjectFileReadResponse> ReadDomainViewModelsAsync(string searchPattern = "*.cs", bool recursive = true)
- {
- return await ReadProjectDirectoryAsync(@"OASystem\OASystem.Domain\ViewModels", searchPattern, recursive);
- }
- /// <summary>
- /// 读取签证表单相关的所有CS文件
- /// </summary>
- public async Task<ProjectFileReadResponse> ReadVisaFormFilesAsync(string searchPattern = "*.cs", bool recursive = true)
- {
- return await ReadProjectDirectoryAsync(@"OASystem\OASystem.Domain\ViewModels\VisaFormDetails", searchPattern, recursive);
- }
- #endregion
- }
- #region 私有实体类
- /// <summary>
- /// DeepSeek 聊天响应(用于反序列化)
- /// </summary>
- internal class DeepSeekResponse
- {
- [JsonPropertyName("choices")]
- public List<Choice> Choices { get; set; }
- [JsonPropertyName("usage")]
- public Usage Usage { get; set; }
- }
- internal class Choice
- {
- [JsonPropertyName("message")]
- public Message Message { get; set; }
- }
- internal class Message
- {
- [JsonPropertyName("content")]
- public string Content { get; set; }
- }
- internal class Usage
- {
- [JsonPropertyName("total_tokens")]
- public int TotalTokens { get; set; }
- }
- #endregion
- }
|