| 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}
 |