diff --git a/BotSharp.sln b/BotSharp.sln
index f2dad6f0..b7545cdc 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -101,6 +101,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.FileHandler
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Planner", "src\Plugins\BotSharp.Plugin.Planner\BotSharp.Plugin.Planner.csproj", "{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileStorages", "FileStorages", "{38B37C0D-1930-4D47-BCBF-E358EC1096B1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.TencentCos", "src\Plugins\BotSharp.Plugin.TencentCos\BotSharp.Plugin.TencentCos.csproj", "{BF029B0A-768B-43A1-8D91-E70B95505716}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -413,6 +417,14 @@ Global
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|Any CPU.Build.0 = Release|Any CPU
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.ActiveCfg = Release|Any CPU
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4}.Release|x64.Build.0 = Release|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Debug|x64.Build.0 = Debug|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.ActiveCfg = Release|Any CPU
+ {BF029B0A-768B-43A1-8D91-E70B95505716}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -461,6 +473,8 @@ Global
{A72B3BEB-E14B-4917-BE44-97EAE4E122D2} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{D6A99D4F-6248-419E-8A43-B38ADEBABA2C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{54E83C6F-54EE-4ADC-8D72-93C009CC4FB4} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
+ {38B37C0D-1930-4D47-BCBF-E358EC1096B1} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
+ {BF029B0A-768B-43A1-8D91-E70B95505716} = {38B37C0D-1930-4D47-BCBF-E358EC1096B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/BotSharp.Plugin.TencentCos.csproj b/src/Plugins/BotSharp.Plugin.TencentCos/BotSharp.Plugin.TencentCos.csproj
new file mode 100644
index 00000000..b52e930a
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/BotSharp.Plugin.TencentCos.csproj
@@ -0,0 +1,21 @@
+
+
+
+ $(TargetFramework)
+ $(LangVersion)
+ enable
+ $(BotSharpVersion)
+ $(GeneratePackageOnBuild)
+ $(GenerateDocumentationFile)
+ $(SolutionDir)packages
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Modules/BucketClient.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Modules/BucketClient.cs
new file mode 100644
index 00000000..1886cfa0
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Modules/BucketClient.cs
@@ -0,0 +1,304 @@
+using COSXML;
+using COSXML.CosException;
+using COSXML.Model.Bucket;
+using COSXML.Model.Object;
+using COSXML.Model.Tag;
+
+namespace BotSharp.Plugin.TencentCos.Modules
+{
+ public class BucketClient
+ {
+ private readonly CosXmlServer _cosXml;
+ private readonly string _fullBucketName;
+ private readonly string _appId;
+ private readonly string _region;
+ public BucketClient(CosXmlServer cosXml, string fullBucketName, string appId, string region)
+ {
+ _cosXml = cosXml;
+ _fullBucketName = fullBucketName;
+ _appId = appId;
+ _region = region;
+ }
+
+ public bool UploadBytes(string key, byte[] fileData)
+ {
+ var result = false;
+ try
+ {
+ var request = new PutObjectRequest(_fullBucketName, key, fileData);
+
+ var resultData = _cosXml.PutObject(request);
+
+ if (resultData != null && resultData.IsSuccessful())
+ {
+ result = true;
+ }
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ return result;
+ }
+
+ public bool UploadStream(string key, Stream stream)
+ {
+ var result = false;
+ try
+ {
+ var request = new PutObjectRequest(_fullBucketName, key, stream);
+
+ var resultData = _cosXml.PutObject(request);
+
+ if (resultData != null && resultData.IsSuccessful())
+ {
+ result = true;
+ }
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ return result;
+ }
+
+ public (string, byte[]) DownloadDirDefaultFileBytes(string dir)
+ {
+ try
+ {
+ var request = new GetBucketRequest(_fullBucketName);
+ request.SetPrefix($"{dir.TrimEnd('/')}/");
+ request.SetDelimiter("/");
+
+ var result = _cosXml.GetBucket(request);
+
+ var info = result.listBucket;
+
+ var objects = info.contentsList;
+
+ var objectData = objects.FirstOrDefault(o => o.size > 0);
+
+ if (objectData != null)
+ {
+ var fileName = Path.GetFileName(objectData.key);
+ var fileBytes = DownloadFileBytes(objectData.key);
+ return (fileName, fileBytes);
+ }
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ return (string.Empty, Array.Empty());
+ }
+
+ public byte[] DownloadFileBytes(string key)
+ {
+ try
+ {
+ var request = new GetObjectBytesRequest(_fullBucketName, key);
+ var result = _cosXml.GetObject(request);
+ if (result != null)
+ {
+ return result.content;
+ }
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ return Array.Empty();
+ }
+
+ public List GetDirFiles(string dir)
+ {
+ try
+ {
+ var request = new GetBucketRequest(_fullBucketName);
+ request.SetPrefix($"{dir.TrimEnd('/')}/");
+ request.SetDelimiter("/");
+
+ var result = _cosXml.GetBucket(request);
+
+ var info = result.listBucket;
+
+ var objects = info.contentsList;
+
+ return objects.Where(o => o.size > 0).Select(o => o.key).ToList();
+
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ }
+
+ public List GetDirectories(string dir)
+ {
+ var dirs = new List();
+ try
+ {
+ var request = new GetBucketRequest(_fullBucketName);
+ request.SetPrefix($"{dir.TrimEnd('/')}/");
+ request.SetDelimiter("/");
+
+ var result = _cosXml.GetBucket(request);
+
+ var info = result.listBucket;
+
+ var objects = info.contentsList;
+
+ var list = objects.Where(o => o.size == 0 && o.key != dir).Select(o => o.key).ToList();
+
+ dirs.AddRange(list);
+
+ var commonPrefixes = info.commonPrefixesList;
+
+ dirs.AddRange(commonPrefixes.Select(c => c.prefix));
+
+ return dirs;
+
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ }
+
+ public bool DirExists(string dir)
+ {
+ try
+ {
+ var request = new GetBucketRequest(_fullBucketName);
+ request.SetPrefix($"{dir.TrimEnd('/')}/");
+ request.SetDelimiter("/");
+
+ var result = _cosXml.GetBucket(request);
+
+ var info = result.listBucket;
+
+ var objects = info.contentsList;
+
+ return objects.Count > 0 || info?.commonPrefixesList.Count > 0;
+
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ }
+
+ public void MoveDir(string sourceDir, string destDir)
+ {
+ var listRequest = new GetBucketRequest(_fullBucketName);
+
+ listRequest.SetPrefix($"{sourceDir.TrimEnd('/')}/");
+ var listResult = _cosXml.GetBucket(listRequest);
+
+ var info = listResult.listBucket;
+
+ var objects = info.contentsList;
+
+ foreach (var obj in objects)
+ {
+ string sourceKey = obj.key;
+ string destinationKey = $"{destDir.TrimEnd('/')}/{sourceKey.Substring(sourceDir.Length)}";
+
+ var copySource = new CopySourceStruct(_appId, _fullBucketName, _region, sourceKey);
+
+ var request = new CopyObjectRequest(_fullBucketName, destinationKey);
+
+ request.SetCopySource(copySource);
+ try
+ {
+
+ var result = _cosXml.CopyObject(request);
+ var deleteRequest = new DeleteObjectRequest(_fullBucketName, sourceKey);
+ var deleteResult = _cosXml.DeleteObject(deleteRequest);
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ }
+
+ }
+
+ public void DeleteDir(string dir)
+ {
+ try
+ {
+ string nextMarker = null;
+ do
+ {
+ var listRequest = new GetBucketRequest(_fullBucketName);
+ listRequest.SetPrefix($"{dir.TrimEnd('/')}/");
+ listRequest.SetMarker(nextMarker);
+ var listResult = _cosXml.GetBucket(listRequest);
+ var info = listResult.listBucket;
+ List objects = info.contentsList;
+ nextMarker = info.nextMarker;
+
+ var deleteRequest = new DeleteMultiObjectRequest(_fullBucketName);
+
+ deleteRequest.SetDeleteQuiet(false);
+ var deleteObjects = new List();
+ foreach (var content in objects)
+ {
+ deleteObjects.Add(content.key);
+ }
+ deleteRequest.SetObjectKeys(deleteObjects);
+
+ var deleteResult = _cosXml.DeleteMultiObjects(deleteRequest);
+
+ } while (nextMarker != null);
+ }
+ catch (CosClientException clientEx)
+ {
+ throw new Exception(clientEx.Message);
+ }
+ catch (CosServerException serverEx)
+ {
+ throw new Exception(serverEx.Message);
+ }
+ }
+
+ public bool DoesObjectExist(string key)
+ {
+ var request = new DoesObjectExistRequest(_fullBucketName, key);
+ return _cosXml.DoesObjectExist(request);
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Common.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Common.cs
new file mode 100644
index 00000000..e9631786
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Common.cs
@@ -0,0 +1,70 @@
+using Microsoft.AspNetCore.StaticFiles;
+
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService
+{
+ public string GetDirectory(string conversationId)
+ {
+ return $"{CONVERSATION_FOLDER}/{conversationId}/attachments/";
+ }
+
+ public (string, byte[]) GetFileInfoFromData(string data)
+ {
+ if (string.IsNullOrEmpty(data))
+ {
+ return (string.Empty, new byte[0]);
+ }
+
+ var typeStartIdx = data.IndexOf(':');
+ var typeEndIdx = data.IndexOf(';');
+ var contentType = data.Substring(typeStartIdx + 1, typeEndIdx - typeStartIdx - 1);
+
+ var base64startIdx = data.IndexOf(',');
+ var base64Str = data.Substring(base64startIdx + 1);
+
+ return (contentType, Convert.FromBase64String(base64Str));
+ }
+
+ public string GetFileContentType(string filePath)
+ {
+ string contentType;
+ var provider = new FileExtensionContentTypeProvider();
+ if (!provider.TryGetContentType(filePath, out contentType))
+ {
+ contentType = string.Empty;
+ }
+
+ return contentType;
+ }
+
+ public byte[] GetFileBytes(string fileStorageUrl)
+ {
+ try
+ {
+ var fileData = _cosClient.BucketClient.DownloadFileBytes(fileStorageUrl);
+
+ return fileData;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when get file bytes: {ex.Message}\r\n{ex.InnerException}");
+ }
+ return Array.Empty();
+ }
+
+ public bool SavefileToPath(string filePath, Stream stream)
+ {
+ if (string.IsNullOrEmpty(filePath)) return false;
+
+ try
+ {
+ return _cosClient.BucketClient.UploadStream(filePath, stream);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when saving file to path: {ex.Message}\r\n{ex.InnerException}");
+ return false;
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs
new file mode 100644
index 00000000..df39e0a9
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs
@@ -0,0 +1,350 @@
+using BotSharp.Abstraction.Files.Converters;
+using BotSharp.Abstraction.Files.Enums;
+using System.Net.Mime;
+
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService
+{
+ public async Task> GetChatFiles(string conversationId, string source,
+ IEnumerable conversations, IEnumerable contentTypes,
+ bool includeScreenShot = false, int? offset = null)
+ {
+ var files = new List();
+ if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty())
+ {
+ return files;
+ }
+
+ var messageIds = GetMessageIds(conversations, offset);
+ var pathPrefix = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}";
+
+ foreach (var messageId in messageIds)
+ {
+ var dir = $"{pathPrefix}/{messageId}/{source}";
+
+ foreach (var subDir in _cosClient.BucketClient.GetDirectories(dir))
+ {
+ var file = _cosClient.BucketClient.GetDirFiles(subDir).FirstOrDefault();
+ if (file == null) continue;
+
+ var contentType = GetFileContentType(file);
+ if (contentTypes?.Contains(contentType) != true) continue;
+
+ var foundFiles = await GetMessageFiles(file, subDir, contentType, messageId, source, includeScreenShot);
+ if (foundFiles.IsNullOrEmpty()) continue;
+
+ files.AddRange(foundFiles);
+ }
+ }
+
+ return files;
+ }
+
+ public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds,
+ string source, bool imageOnly = false)
+ {
+ var files = new List();
+ if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return files;
+
+ foreach (var messageId in messageIds)
+ {
+ var dir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}/{source}";
+ if (!ExistDirectory(dir))
+ {
+ continue;
+ }
+
+ foreach (var subDir in _cosClient.BucketClient.GetDirectories(dir))
+ {
+ foreach (var file in _cosClient.BucketClient.GetDirFiles(subDir))
+ {
+ var contentType = GetFileContentType(file);
+ if (imageOnly && !_imageTypes.Contains(contentType))
+ {
+ continue;
+ }
+
+ var fileName = Path.GetFileNameWithoutExtension(file);
+ var fileType = Path.GetExtension(file).Substring(1);
+ var model = new MessageFileModel()
+ {
+ MessageId = messageId,
+ FileUrl = $"https://{_fullBuketName}.cos.{_settings.Region}.myqcloud.com/{file}",
+ FileStorageUrl = file,
+ FileName = fileName,
+ FileType = fileType,
+ ContentType = contentType,
+ FileSource = source
+ };
+ files.Add(model);
+ }
+ }
+ }
+
+ return files;
+ }
+
+ public string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName)
+ {
+ var dir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{source}/{index}/";
+
+ var fileList = _cosClient.BucketClient.GetDirFiles(dir);
+
+ var found = fileList.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName));
+ return found;
+ }
+
+ public IEnumerable GetMessagesWithFile(string conversationId, IEnumerable messageIds)
+ {
+ var foundMsgs = new List();
+ if (string.IsNullOrWhiteSpace(conversationId) || messageIds.IsNullOrEmpty()) return foundMsgs;
+
+ foreach (var messageId in messageIds)
+ {
+ var prefix = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}";
+ var userDir = $"{prefix}/{FileSourceType.User}/";
+ if (ExistDirectory(userDir))
+ {
+ foundMsgs.Add(new MessageFileModel { MessageId = messageId, FileSource = FileSourceType.User });
+ }
+
+ var botDir = $"{prefix}/{FileSourceType.Bot}";
+ if (ExistDirectory(botDir))
+ {
+ foundMsgs.Add(new MessageFileModel { MessageId = messageId, FileSource = FileSourceType.Bot });
+ }
+ }
+
+ return foundMsgs;
+ }
+
+ public bool SaveMessageFiles(string conversationId, string messageId, string source, List files)
+ {
+ if (files.IsNullOrEmpty()) return false;
+
+ var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true);
+
+ for (int i = 0; i < files.Count; i++)
+ {
+ var file = files[i];
+ if (string.IsNullOrEmpty(file.FileData))
+ {
+ continue;
+ }
+
+ try
+ {
+ var (_, bytes) = GetFileInfoFromData(file.FileData);
+
+ var subDir = $"{dir}/{source}/{i + 1}";
+
+ _cosClient.BucketClient.UploadBytes($"{subDir}/{file.FileName}", bytes);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when saving message file {file.FileName}: {ex.Message}\r\n{ex.InnerException}");
+ continue;
+ }
+ }
+
+ return true;
+ }
+
+
+ public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null)
+ {
+ if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false;
+
+ if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId))
+ {
+ var prevDir = GetConversationFileDirectory(conversationId, targetMessageId);
+ var newDir = $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{newMessageId}/";
+
+ if (ExistDirectory(prevDir))
+ {
+ if (ExistDirectory(newDir))
+ {
+ _cosClient.BucketClient.DeleteDir(newDir);
+ }
+
+ _cosClient.BucketClient.MoveDir(prevDir, newDir);
+
+ var botDir = $"{newDir}/{BOT_FILE_FOLDER}";
+ if (ExistDirectory(botDir))
+ {
+ _cosClient.BucketClient.DeleteDir(newDir);
+ }
+ }
+ }
+
+ foreach (var messageId in messageIds)
+ {
+ var dir = GetConversationFileDirectory(conversationId, messageId);
+ if (!ExistDirectory(dir)) continue;
+ _cosClient.BucketClient.DeleteDir(dir);
+ }
+
+ return true;
+ }
+
+ public bool DeleteConversationFiles(IEnumerable conversationIds)
+ {
+ if (conversationIds.IsNullOrEmpty()) return false;
+
+ foreach (var conversationId in conversationIds)
+ {
+ var convDir = GetConversationDirectory(conversationId);
+ if (!ExistDirectory(convDir)) continue;
+
+ _cosClient.BucketClient.DeleteDir(convDir);
+ }
+ return true;
+ }
+
+ #region Private methods
+ private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false)
+ {
+ if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId))
+ {
+ return string.Empty;
+ }
+
+ return $"{CONVERSATION_FOLDER}/{conversationId}/{FILE_FOLDER}/{messageId}";
+ }
+
+ private string? GetConversationDirectory(string conversationId)
+ {
+ if (string.IsNullOrEmpty(conversationId)) return null;
+
+ var dir = $"{CONVERSATION_FOLDER}/{conversationId}";
+ return dir;
+ }
+
+ private IEnumerable GetMessageIds(IEnumerable conversations, int? offset = null)
+ {
+ if (conversations.IsNullOrEmpty()) return Enumerable.Empty();
+
+ if (offset <= 0)
+ {
+ offset = MIN_OFFSET;
+ }
+ else if (offset > MAX_OFFSET)
+ {
+ offset = MAX_OFFSET;
+ }
+
+ var messageIds = new List();
+ if (offset.HasValue)
+ {
+ messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset.Value).ToList();
+ }
+ else
+ {
+ messageIds = conversations.Select(x => x.MessageId).Distinct().ToList();
+ }
+
+ return messageIds;
+ }
+
+
+ private async Task> GetMessageFiles(string file, string fileDir, string contentType,
+ string messageId, string source, bool includeScreenShot)
+ {
+ var files = new List();
+ try
+ {
+ if (!_imageTypes.Contains(contentType) && includeScreenShot)
+ {
+ var screenShotDir = $"{fileDir}/{SCREENSHOT_FILE_FOLDER}/";
+
+ var fileList = _cosClient.BucketClient.GetDirFiles(screenShotDir);
+
+ if (!fileList.IsNullOrEmpty())
+ {
+ foreach (var screenShot in fileList)
+ {
+ contentType = GetFileContentType(screenShot);
+ if (!_imageTypes.Contains(contentType)) continue;
+
+ var fileName = Path.GetFileNameWithoutExtension(screenShot);
+ var fileType = Path.GetExtension(file).Substring(1);
+ var model = new MessageFileModel()
+ {
+ MessageId = messageId,
+ FileName = fileName,
+ FileType = fileType,
+ FileStorageUrl = screenShot,
+ ContentType = contentType,
+ FileSource = source
+ };
+ files.Add(model);
+ }
+ }
+ else if (contentType == MediaTypeNames.Application.Pdf)
+ {
+ var images = await ConvertPdfToImages(file, screenShotDir);
+ foreach (var image in images)
+ {
+ contentType = GetFileContentType(image);
+ var fileName = Path.GetFileNameWithoutExtension(image);
+ var fileType = Path.GetExtension(image).Substring(1);
+ var model = new MessageFileModel()
+ {
+ MessageId = messageId,
+ FileName = fileName,
+ FileType = fileType,
+ FileStorageUrl = image,
+ ContentType = contentType,
+ FileSource = source
+ };
+ files.Add(model);
+ }
+ }
+ }
+ else
+ {
+ var fileName = Path.GetFileNameWithoutExtension(file);
+ var fileType = Path.GetExtension(file).Substring(1);
+ var model = new MessageFileModel()
+ {
+ MessageId = messageId,
+ FileName = fileName,
+ FileType = fileType,
+ FileStorageUrl = file,
+ ContentType = contentType,
+ FileSource = source
+ };
+ files.Add(model);
+ }
+
+ return files;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when getting message files {file} (messageId: {messageId}), Error: {ex.Message}\r\n{ex.InnerException}");
+ return files;
+ }
+ }
+
+
+ private async Task> ConvertPdfToImages(string pdfLoc, string imageLoc)
+ {
+ var converters = _services.GetServices();
+ if (converters.IsNullOrEmpty()) return Enumerable.Empty();
+
+ var converter = GetPdf2ImageConverter();
+ if (converter == null)
+ {
+ return Enumerable.Empty();
+ }
+ return await converter.ConvertPdfToImages(pdfLoc, imageLoc);
+ }
+
+ private IPdf2ImageConverter? GetPdf2ImageConverter()
+ {
+ var converters = _services.GetServices();
+ return converters.FirstOrDefault();
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Image.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Image.cs
new file mode 100644
index 00000000..e8628ce3
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Image.cs
@@ -0,0 +1,107 @@
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService
+{
+ public async Task GenerateImage(string? provider, string? model, string text)
+ {
+ var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-3");
+ var message = await completion.GetImageGeneration(new Agent()
+ {
+ Id = Guid.Empty.ToString(),
+ }, new RoleDialogModel(AgentRole.User, text));
+ return message;
+ }
+
+ public async Task VaryImage(string? provider, string? model, BotSharpFile image)
+ {
+ if (string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData))
+ {
+ throw new ArgumentException($"Cannot find image url or data!");
+ }
+
+ var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
+ var bytes = await DownloadFile(image);
+ using var stream = new MemoryStream();
+ stream.Write(bytes, 0, bytes.Length);
+ stream.Position = 0;
+
+ var message = await completion.GetImageVariation(new Agent()
+ {
+ Id = Guid.Empty.ToString()
+ }, new RoleDialogModel(AgentRole.User, string.Empty), stream, image.FileName ?? string.Empty);
+
+ stream.Close();
+ return message;
+ }
+
+ public async Task EditImage(string? provider, string? model, string text, BotSharpFile image)
+ {
+ if (string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData))
+ {
+ throw new ArgumentException($"Cannot find image url or data!");
+ }
+
+ var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
+ var bytes = await DownloadFile(image);
+ using var stream = new MemoryStream();
+ stream.Write(bytes, 0, bytes.Length);
+ stream.Position = 0;
+
+ var message = await completion.GetImageEdits(new Agent()
+ {
+ Id = Guid.Empty.ToString()
+ }, new RoleDialogModel(AgentRole.User, text), stream, image.FileName ?? string.Empty);
+
+ stream.Close();
+ return message;
+ }
+
+ public async Task EditImage(string? provider, string? model, string text, BotSharpFile image, BotSharpFile mask)
+ {
+ if ((string.IsNullOrWhiteSpace(image?.FileUrl) && string.IsNullOrWhiteSpace(image?.FileData)) ||
+ (string.IsNullOrWhiteSpace(mask?.FileUrl) && string.IsNullOrWhiteSpace(mask?.FileData)))
+ {
+ throw new ArgumentException($"Cannot find image/mask url or data");
+ }
+
+ var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
+ var imageBytes = await DownloadFile(image);
+ var maskBytes = await DownloadFile(mask);
+
+ using var imageStream = new MemoryStream();
+ imageStream.Write(imageBytes, 0, imageBytes.Length);
+ imageStream.Position = 0;
+
+ using var maskStream = new MemoryStream();
+ maskStream.Write(maskBytes, 0, maskBytes.Length);
+ maskStream.Position = 0;
+
+ var message = await completion.GetImageEdits(new Agent()
+ {
+ Id = Guid.Empty.ToString()
+ }, new RoleDialogModel(AgentRole.User, text), imageStream, image.FileName ?? string.Empty, maskStream, mask.FileName ?? string.Empty);
+
+ imageStream.Close();
+ maskStream.Close();
+ return message;
+ }
+
+ #region Private methods
+ private async Task DownloadFile(BotSharpFile file)
+ {
+ var bytes = new byte[0];
+ if (!string.IsNullOrEmpty(file.FileUrl))
+ {
+ var http = _services.GetRequiredService();
+ using var client = http.CreateClient();
+ bytes = await client.GetByteArrayAsync(file.FileUrl);
+ }
+ else if (!string.IsNullOrEmpty(file.FileData))
+ {
+ (_, bytes) = GetFileInfoFromData(file.FileData);
+ }
+
+ return bytes;
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Pdf.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Pdf.cs
new file mode 100644
index 00000000..1efbad6d
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Pdf.cs
@@ -0,0 +1,130 @@
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService
+{
+ public async Task ReadPdf(string? provider, string? model, string? modelId, string prompt, List files)
+ {
+ var content = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(prompt) || files.IsNullOrEmpty())
+ {
+ return content;
+ }
+
+ var guid = Guid.NewGuid().ToString();
+ var sessionDir = GetSessionDirectory(guid);
+
+ try
+ {
+ var pdfFiles = await DownloadFiles(sessionDir, files);
+ var images = await ConvertPdfToImages(pdfFiles);
+ if (images.IsNullOrEmpty()) return content;
+
+ var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai",
+ model: model, modelId: modelId ?? "gpt-4", multiModal: true);
+ var message = await completion.GetChatCompletions(new Agent()
+ {
+ Id = Guid.Empty.ToString(),
+ }, new List
+ {
+ new RoleDialogModel(AgentRole.User, prompt)
+ {
+ Files = images.Select(x => new BotSharpFile { FileStorageUrl = x }).ToList()
+ }
+ });
+
+ content = message.Content;
+ return content;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error when analyzing pdf in file service: {ex.Message}\r\n{ex.InnerException}");
+ return content;
+ }
+ finally
+ {
+ Directory.Delete(sessionDir, true);
+ }
+ }
+
+ #region Private methods
+ private string GetSessionDirectory(string id)
+ {
+ var dir = $"{SESSION_FOLDER}/{id}";
+ return dir;
+ }
+
+ private async Task> DownloadFiles(string dir, List files, string extension = "pdf")
+ {
+ if (string.IsNullOrWhiteSpace(dir) || files.IsNullOrEmpty())
+ {
+ return Enumerable.Empty();
+ }
+
+ var locs = new List();
+ foreach (var file in files)
+ {
+ try
+ {
+ var bytes = new byte[0];
+ if (!string.IsNullOrEmpty(file.FileUrl))
+ {
+ var http = _services.GetRequiredService();
+ using var client = http.CreateClient();
+ bytes = await client.GetByteArrayAsync(file.FileUrl);
+ }
+ else if (!string.IsNullOrEmpty(file.FileData))
+ {
+ (_, bytes) = GetFileInfoFromData(file.FileData);
+ }
+
+ if (!bytes.IsNullOrEmpty())
+ {
+ var guid = Guid.NewGuid().ToString();
+ var fileDir = $"{dir}/{guid}";
+
+ var pdfDir = $"{fileDir}/{guid}.{extension}";
+
+
+ _cosClient.BucketClient.UploadBytes(pdfDir, bytes);
+ locs.Add(pdfDir);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when saving pdf file: {ex.Message}\r\n{ex.InnerException}");
+ continue;
+ }
+ }
+ return locs;
+ }
+
+ private async Task> ConvertPdfToImages(IEnumerable files)
+ {
+ var images = new List();
+ var converter = GetPdf2ImageConverter();
+ if (converter == null || files.IsNullOrEmpty())
+ {
+ return images;
+ }
+
+ foreach (var file in files)
+ {
+ try
+ {
+ var segs = file.Split(Path.DirectorySeparatorChar);
+ var dir = string.Join(Path.DirectorySeparatorChar, segs.SkipLast(1));
+ var folder = Path.Combine(dir, "screenshots");
+ var urls = await converter.ConvertPdfToImages(file, folder);
+ images.AddRange(urls);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when converting pdf file to images ({file}): {ex.Message}\r\n{ex.InnerException}");
+ continue;
+ }
+ }
+ return images;
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs
new file mode 100644
index 00000000..23ce68c0
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs
@@ -0,0 +1,58 @@
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService
+{
+ public string GetUserAvatar()
+ {
+ var db = _services.GetRequiredService();
+ var user = db.GetUserById(_user.Id);
+ var dir = GetUserAvatarDir(user?.Id);
+
+ if (!ExistDirectory(dir)) return string.Empty;
+
+ var found = _cosClient.BucketClient.GetDirFiles(dir).FirstOrDefault() ?? string.Empty;
+ return found;
+ }
+
+ public bool SaveUserAvatar(BotSharpFile file)
+ {
+ if (file == null || string.IsNullOrEmpty(file.FileData)) return false;
+
+ try
+ {
+ var db = _services.GetRequiredService();
+ var user = db.GetUserById(_user.Id);
+ var dir = GetUserAvatarDir(user?.Id);
+
+ if (string.IsNullOrEmpty(dir)) return false;
+
+ var (_, bytes) = GetFileInfoFromData(file.FileData);
+
+ var extension = Path.GetExtension(file.FileName);
+
+ var fileName = user?.Id == null ? file.FileName : $"{user?.Id}{extension}";
+
+ return _cosClient.BucketClient.UploadBytes($"{dir}/{fileName}", bytes);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning($"Error when saving user avatar: {ex.Message}\r\n{ex.InnerException}");
+ return false;
+ }
+ }
+
+
+ #region Private methods
+ private string GetUserAvatarDir(string? userId, bool createNewDir = false)
+ {
+ if (string.IsNullOrEmpty(userId))
+ {
+ return string.Empty;
+ }
+
+ var dir = $"{USERS_FOLDER}/{userId}/{USER_AVATAR_FOLDER}/";
+
+ return dir;
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs
new file mode 100644
index 00000000..80b8fd78
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs
@@ -0,0 +1,56 @@
+using BotSharp.Abstraction.Files;
+using BotSharp.Abstraction.Users;
+using BotSharp.Plugin.TencentCos.Settings;
+using System.Net.Mime;
+
+namespace BotSharp.Plugin.TencentCos.Services;
+
+public partial class TencentCosService : IBotSharpFileService
+{
+ private readonly TencentCosSettings _settings;
+ private readonly IServiceProvider _services;
+ private readonly IUserIdentity _user;
+ private readonly ILogger _logger;
+ private readonly string _fullBuketName;
+ private readonly IEnumerable _imageTypes = new List
+ {
+ MediaTypeNames.Image.Png,
+ MediaTypeNames.Image.Jpeg
+ };
+
+ private const string CONVERSATION_FOLDER = "conversations";
+ private const string FILE_FOLDER = "files";
+ private const string USER_FILE_FOLDER = "user";
+ private const string SCREENSHOT_FILE_FOLDER = "screenshot";
+ private const string BOT_FILE_FOLDER = "bot";
+ private const string USERS_FOLDER = "users";
+ private const string USER_AVATAR_FOLDER = "avatar";
+ private const string SESSION_FOLDER = "sessions";
+
+ private const int MIN_OFFSET = 1;
+ private const int MAX_OFFSET = 5;
+
+ private readonly TencentCosClient _cosClient;
+
+ public TencentCosService(
+ TencentCosSettings settings,
+ IUserIdentity user,
+ ILogger logger,
+ IServiceProvider services,
+ TencentCosClient cosClient)
+ {
+ _settings = settings;
+ _user = user;
+ _logger = logger;
+ _services = services;
+ _fullBuketName = $"{_settings.BucketName}-{_settings.AppId}";
+ _cosClient = cosClient;
+ }
+
+ #region Private methods
+ private bool ExistDirectory(string? dir)
+ {
+ return !string.IsNullOrEmpty(dir) && _cosClient.BucketClient.DirExists(dir);
+ }
+ #endregion
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Settings/TencentCosSettings.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Settings/TencentCosSettings.cs
new file mode 100644
index 00000000..4a848116
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Settings/TencentCosSettings.cs
@@ -0,0 +1,12 @@
+namespace BotSharp.Plugin.TencentCos.Settings
+{
+ public class TencentCosSettings
+ {
+ public string AppId { get; set; }
+ public string SecretId { get; set; }
+ public string SecretKey { get; set; }
+ public string Region { get; set; }
+ public string BucketName { get; set; }
+ public int KeyDurationSecond { get; set; } = 600;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosClient.cs b/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosClient.cs
new file mode 100644
index 00000000..b7e600f3
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosClient.cs
@@ -0,0 +1,26 @@
+using BotSharp.Plugin.TencentCos.Modules;
+using BotSharp.Plugin.TencentCos.Settings;
+using COSXML;
+using COSXML.Auth;
+
+namespace BotSharp.Plugin.TencentCos
+{
+ public class TencentCosClient
+ {
+ public BucketClient BucketClient { get; private set; }
+ public TencentCosClient(TencentCosSettings settings)
+ {
+ var cosXmlConfig = new CosXmlConfig.Builder()
+ .IsHttps(true)
+ .SetAppid(settings.AppId)
+ .SetRegion(settings.Region)
+ .Build();
+ var cosCredentialProvider = new DefaultQCloudCredentialProvider(
+ settings.SecretId, settings.SecretKey, settings.KeyDurationSecond);
+
+ var cosXml = new CosXmlServer(cosXmlConfig, cosCredentialProvider);
+
+ BucketClient = new BucketClient(cosXml, $"{settings.BucketName}-{settings.AppId}", settings.AppId, settings.Region);
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosPlugin.cs b/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosPlugin.cs
new file mode 100644
index 00000000..93a99c14
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/TencentCosPlugin.cs
@@ -0,0 +1,37 @@
+using BotSharp.Abstraction.Files;
+using BotSharp.Abstraction.Repositories.Enums;
+using BotSharp.Abstraction.Settings;
+using BotSharp.Plugin.TencentCos;
+using BotSharp.Plugin.TencentCos.Services;
+using BotSharp.Plugin.TencentCos.Settings;
+
+namespace BotSharp.Plugin.TencentCosFile.Files;
+
+public class TencentCosPlugin : IBotSharpPlugin
+{
+ public string Id => "3f55b702-8a28-4f9a-907c-affc24f845f1";
+
+ public string Name => "TencentCos";
+
+ public string Description => "Provides connection to Tencent Cloud object storage service.";
+
+
+ public void RegisterDI(IServiceCollection services, IConfiguration config)
+ {
+ var myFileStorageSettings = new FileStorageSettings();
+ config.Bind("FileStorage", myFileStorageSettings);
+
+ if (myFileStorageSettings.Default == FileStorageEnum.TencentCosStorage)
+ {
+ services.AddScoped(provider =>
+ {
+ var settingService = provider.GetRequiredService();
+ return settingService.Bind("TencentCos");
+ });
+
+ services.AddScoped();
+
+ services.AddScoped();
+ }
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Using.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Using.cs
new file mode 100644
index 00000000..7c388e37
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.TencentCos/Using.cs
@@ -0,0 +1,17 @@
+global using BotSharp.Abstraction.Agents.Enums;
+global using BotSharp.Abstraction.Agents.Models;
+global using BotSharp.Abstraction.Conversations.Models;
+global using BotSharp.Abstraction.Files.Models;
+global using BotSharp.Abstraction.Plugins;
+global using BotSharp.Abstraction.Repositories;
+global using BotSharp.Abstraction.Utilities;
+global using BotSharp.Core.Infrastructures;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Net.Http;
+global using System.Threading.Tasks;
diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj
index f4f2cfcd..5a5b1ced 100644
--- a/src/WebStarter/WebStarter.csproj
+++ b/src/WebStarter/WebStarter.csproj
@@ -31,6 +31,7 @@
+
diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json
index 1488440c..b10e2160 100644
--- a/src/WebStarter/appsettings.json
+++ b/src/WebStarter/appsettings.json
@@ -233,6 +233,13 @@
"FileStorage": {
"Default": "LocalFileStorage"
},
+ "TencentCos": {
+ "AppId": "",
+ "SecretId": "",
+ "SecretKey": "",
+ "BucketName": "",
+ "Region": ""
+ },
"Qdrant": {
"Url": "",
"ApiKey": ""
@@ -306,7 +313,8 @@
"BotSharp.Plugin.MetaGLM",
"BotSharp.Plugin.HttpHandler",
"BotSharp.Plugin.FileHandler",
- "BotSharp.Plugin.EmailHandler"
+ "BotSharp.Plugin.EmailHandler",
+ "BotSharp.Plugin.TencentCos"
]
}
}