refactor(storage): update storage providers to use IConfigService for configuration management

This commit is contained in:
ShiYu
2025-05-22 23:13:03 +08:00
parent 93fb2db208
commit a03e245d67
7 changed files with 167 additions and 158 deletions

View File

@@ -14,9 +14,17 @@ public class AiService : IAiService
{
_httpClient = httpClient;
_configService = configService;
}
private void ConfigureHttpClient()
{
string apiKey = _configService["AI:ApiKey"];
string baseUrl = _configService["AI:ApiEndpoint"];
_httpClient.BaseAddress = new Uri(baseUrl);
if (_httpClient.BaseAddress?.ToString() != baseUrl)
{
_httpClient.BaseAddress = new Uri(baseUrl);
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
@@ -24,6 +32,7 @@ public class AiService : IAiService
{
try
{
ConfigureHttpClient();
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
@@ -83,10 +92,13 @@ public class AiService : IAiService
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
if (availableTags.Count == 0)
return new List<string>();
string model = _configService["AI:Model"];
string model = _configService["AI:Model"];
var tagsText = string.Join(", ", availableTags);
var textContent = new TextContent
{
@@ -189,8 +201,10 @@ public class AiService : IAiService
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
string model = _configService["AI:Model"];
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
@@ -329,6 +343,8 @@ public class AiService : IAiService
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
string model = _configService["AI:EmbeddingModel"];

View File

@@ -39,42 +39,19 @@ public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProv
}
}
}
[StorageProvider(StorageType.Cos)]
public class CosStorageProvider : IStorageProvider
public class CosStorageProvider(IConfigService configService) : IStorageProvider
{
private readonly string _bucketName;
private readonly string _region;
private readonly string _cdnUrl;
private readonly IConfigService _configService;
private readonly CosXml _cosXmlClient;
private readonly bool _isPublicRead;
public CosStorageProvider(IConfigService configService)
{
_configService = configService;
_bucketName = configService["Storage:CosStorageBucketName"];
_region = configService["Storage:CosStorageRegion"];
_cdnUrl = configService["Storage:CosStorageCdnUrl"];
// 检查桶是否为公开读取(从配置获取)
bool.TryParse(configService["Storage:CosStoragePublicRead"], out _isPublicRead);
// 在构造函数中初始化客户端,作为单例使用
_cosXmlClient = CreateClient();
}
private CosXml CreateClient()
{
// 优化配置启用HTTPS和日志
var config = new CosXmlConfig.Builder()
.IsHttps(true) // 设置默认HTTPS请求
.SetRegion(_region)
.SetDebugLog(true) // 显示日志
.IsHttps(true)
.SetRegion(configService["Storage:CosStorageRegion"])
.SetDebugLog(true)
.Build();
// 使用自定义凭证提供者,支持持续更新临时密钥
var cosCredentialProvider = new CustomQCloudCredentialProvider(_configService);
var cosCredentialProvider = new CustomQCloudCredentialProvider(configService);
return new CosXmlServer(config, cosCredentialProvider);
}
@@ -97,9 +74,10 @@ public class CosStorageProvider : IStorageProvider
await fileStream.CopyToAsync(fileStream2);
}
var cosXmlClient = CreateClient();
var transferConfig = new TransferConfig();
var transferManager = new TransferManager(_cosXmlClient, transferConfig);
var uploadTask = new COSXMLUploadTask(_bucketName, objectKey);
var transferManager = new TransferManager(cosXmlClient, transferConfig);
var uploadTask = new COSXMLUploadTask(configService["Storage:CosStorageBucketName"], objectKey);
uploadTask.SetSrcPath(tempPath);
await transferManager.UploadAsync(uploadTask);
return objectKey;
@@ -137,8 +115,9 @@ public class CosStorageProvider : IStorageProvider
if (string.IsNullOrEmpty(storagePath))
return;
var request = new DeleteObjectRequest(_bucketName, storagePath);
await Task.Run(() => _cosXmlClient.DeleteObject(request));
var cosXmlClient = CreateClient();
var request = new DeleteObjectRequest(configService["Storage:CosStorageBucketName"], storagePath);
await Task.Run(() => cosXmlClient.DeleteObject(request));
}
catch (CosClientException clientEx)
{
@@ -161,27 +140,33 @@ public class CosStorageProvider : IStorageProvider
if (string.IsNullOrEmpty(storagePath))
return "/images/unavailable.gif";
string cdnUrl = configService["Storage:CosStorageCdnUrl"];
string bucketName = configService["Storage:CosStorageBucketName"];
string region = configService["Storage:CosStorageRegion"];
bool isPublicRead = bool.TryParse(configService["Storage:CosStoragePublicRead"], out var publicRead) && publicRead;
// 优先使用CDN
if (!string.IsNullOrEmpty(_cdnUrl))
return $"{_cdnUrl}/{storagePath}";
if (!string.IsNullOrEmpty(cdnUrl))
return $"{cdnUrl}/{storagePath}";
// 公开读取的桶可直接访问
if (_isPublicRead)
return $"https://{_bucketName}.cos.{_region}.myqcloud.com/{storagePath}";
if (isPublicRead)
return $"https://{bucketName}.cos.{region}.myqcloud.com/{storagePath}";
var bucketParts = _bucketName.Split('-');
var cosXmlClient = CreateClient();
var bucketParts = bucketName.Split('-');
var request = new PreSignatureStruct
{
bucket = bucketParts[0],
appid = bucketParts[1],
region = _region,
region = region,
key = storagePath,
httpMethod = "GET",
isHttps = true,
signDurationSecond = 3600 * 24
};
var url = _cosXmlClient.GenerateSignURL(request);
var url = cosXmlClient.GenerateSignURL(request);
return url;
}
catch (Exception ex)
@@ -207,13 +192,15 @@ public class CosStorageProvider : IStorageProvider
Directory.CreateDirectory(tempDir);
}
string bucketName = configService["Storage:CosStorageBucketName"];
string fileName = Path.GetFileName(storagePath);
string localFilePath = Path.Combine(tempDir, fileName);
var cosXmlClient = CreateClient();
var transferConfig = new TransferConfig();
var transferManager = new TransferManager(_cosXmlClient, transferConfig);
var downloadTask = new COSXMLDownloadTask(_bucketName, storagePath, tempDir, fileName);
var transferManager = new TransferManager(cosXmlClient, transferConfig);
var downloadTask = new COSXMLDownloadTask(bucketName, storagePath, tempDir, fileName);
await transferManager.DownloadAsync(downloadTask);
return localFilePath;
return Path.Combine(tempDir, fileName);
}
catch (CosClientException clientEx)
{

View File

@@ -4,10 +4,9 @@ using Foxel.Services.Configuration;
namespace Foxel.Services.Storage.Providers;
[StorageProvider(StorageType.Local)]
public class LocalStorageProvider(IConfigService config) : IStorageProvider
public class LocalStorageProvider(IConfigService configService) : IStorageProvider
{
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
private readonly string _serverUrl = config["AppSettings:ServerUrl"];
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
{
@@ -36,9 +35,10 @@ public class LocalStorageProvider(IConfigService config) : IStorageProvider
{
if (string.IsNullOrEmpty(storagePath))
return $"/images/unavailable.gif";
return $"{_serverUrl}{storagePath}";
}
string serverUrl = configService["AppSettings:ServerUrl"];
return $"{serverUrl}{storagePath}";
}
public Task<string> DownloadFileAsync(string storagePath)
{

View File

@@ -7,44 +7,31 @@ using Foxel.Services.Configuration;
namespace Foxel.Services.Storage.Providers;
[StorageProvider(StorageType.S3)]
public class S3StorageProvider : IStorageProvider
public class S3StorageProvider(IConfigService configService) : IStorageProvider
{
private readonly string _accessKey;
private readonly string _secretKey;
private readonly string _bucketName;
private readonly string _region;
private readonly string _endpoint;
private readonly bool _usePathStyleUrls;
private readonly string _cdnUrl;
public S3StorageProvider(IConfigService configService)
{
_accessKey = configService["Storage:S3StorageAccessKey"];
_secretKey = configService["Storage:S3StorageSecretKey"];
_bucketName = configService["Storage:S3StorageBucketName"];
_region = configService["Storage:S3StorageRegion"];
_cdnUrl = configService["Storage:S3StorageCdnUrl"];
_endpoint = configService["Storage:S3StorageEndpoint"];
_usePathStyleUrls = bool.TryParse(configService["Storage:S3StorageUsePathStyleUrls"], out var usePathStyle) && usePathStyle;
}
private AmazonS3Client CreateClient()
{
string accessKey = configService["Storage:S3StorageAccessKey"];
string secretKey = configService["Storage:S3StorageSecretKey"];
string endpoint = configService["Storage:S3StorageEndpoint"];
string region = configService["Storage:S3StorageRegion"];
bool usePathStyleUrls = bool.TryParse(configService["Storage:S3StorageUsePathStyleUrls"], out var usePathStyle) && usePathStyle;
var config = new AmazonS3Config
{
ServiceURL = _endpoint,
UseHttp = !_endpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase),
ForcePathStyle = _usePathStyleUrls
ServiceURL = endpoint,
UseHttp = !endpoint.StartsWith("https", StringComparison.OrdinalIgnoreCase),
ForcePathStyle = usePathStyleUrls
};
if (!string.IsNullOrEmpty(_region) && _endpoint.Contains("amazonaws.com"))
if (!string.IsNullOrEmpty(region) && endpoint.Contains("amazonaws.com"))
{
config.RegionEndpoint = Amazon.RegionEndpoint.GetBySystemName(_region);
config.RegionEndpoint = Amazon.RegionEndpoint.GetBySystemName(region);
}
return new AmazonS3Client(
_accessKey,
_secretKey,
accessKey,
secretKey,
config
);
}
@@ -65,7 +52,7 @@ public class S3StorageProvider : IStorageProvider
{
InputStream = fileStream,
Key = objectKey,
BucketName = _bucketName,
BucketName = configService["Storage:S3StorageBucketName"],
ContentType = contentType
};
@@ -91,7 +78,7 @@ public class S3StorageProvider : IStorageProvider
using var client = CreateClient();
var deleteRequest = new DeleteObjectRequest
{
BucketName = _bucketName,
BucketName = configService["Storage:S3StorageBucketName"],
Key = storagePath
};
@@ -110,17 +97,19 @@ public class S3StorageProvider : IStorageProvider
if (string.IsNullOrEmpty(storagePath))
return "/images/unavailable.gif";
string cdnUrl = configService["Storage:S3StorageCdnUrl"];
// 如果配置了CDN URL使用CDN
if (!string.IsNullOrEmpty(_cdnUrl))
if (!string.IsNullOrEmpty(cdnUrl))
{
return $"{_cdnUrl}/{storagePath}";
return $"{cdnUrl}/{storagePath}";
}
// 否则使用S3直链或生成预签名URL
using var client = CreateClient();
var request = new GetPreSignedUrlRequest
{
BucketName = _bucketName,
BucketName = configService["Storage:S3StorageBucketName"],
Key = storagePath,
Expires = DateTime.UtcNow.AddHours(1) // URL有效期1小时
};
@@ -158,7 +147,7 @@ public class S3StorageProvider : IStorageProvider
using var client = CreateClient();
var request = new GetObjectRequest
{
BucketName = _bucketName,
BucketName = configService["Storage:S3StorageBucketName"],
Key = storagePath
};

View File

@@ -9,15 +9,14 @@ namespace Foxel.Services.Storage.Providers;
[StorageProvider(StorageType.Telegram)]
public class TelegramStorageProvider(IConfigService configService) : IStorageProvider
{
private readonly string _botToken = configService["Storage:TelegramStorageBotToken"];
private readonly string _chatId = configService["Storage:TelegramStorageChatId"];
private readonly string _serverUrl = configService["AppSettings:ServerUrl"];
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
{
string botToken = configService["Storage:TelegramStorageBotToken"];
string chatId = configService["Storage:TelegramStorageChatId"];
using var httpClient = new HttpClient();
using var formData = new MultipartFormDataContent();
formData.Add(new StringContent(_chatId), "chat_id");
formData.Add(new StringContent(chatId), "chat_id");
var safeFileName = Path.GetFileNameWithoutExtension(fileName);
if (safeFileName.Length > 100)
safeFileName = safeFileName.Substring(0, 100);
@@ -35,7 +34,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
try
{
var response =
await httpClient.PostAsync($"https://api.telegram.org/bot{_botToken}/sendDocument", formData);
await httpClient.PostAsync($"https://api.telegram.org/bot{botToken}/sendDocument", formData);
if (!response.IsSuccessStatusCode)
{
@@ -58,7 +57,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
FileId = fileId,
FileUniqueId = responseObj.Result.Document.FileUniqueId,
MessageId = responseObj.Result.MessageId,
ChatId = _chatId,
ChatId = chatId,
OriginalFileName = fileName,
UploadDate = DateTime.UtcNow,
MimeType = contentType
@@ -82,9 +81,11 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
return;
}
string botToken = configService["Storage:TelegramStorageBotToken"];
using var httpClient = new HttpClient();
var url =
$"https://api.telegram.org/bot{_botToken}/deleteMessage?chat_id={metadata.ChatId}&message_id={metadata.MessageId}";
$"https://api.telegram.org/bot{botToken}/deleteMessage?chat_id={metadata.ChatId}&message_id={metadata.MessageId}";
await httpClient.GetAsync(url);
}
catch (Exception ex)
@@ -103,7 +104,8 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
throw new ApplicationException("无效的存储路径或元数据");
}
return $"{_serverUrl}/api/picture/get_telegram_file?fileId={metadata.FileId}";
string serverUrl = configService["AppSettings:ServerUrl"];
return $"{serverUrl}/api/picture/get_telegram_file?fileId={metadata.FileId}";
}
catch (Exception ex)
{
@@ -127,8 +129,10 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
throw new ApplicationException("无效的存储路径或元数据");
}
string botToken = configService["Storage:TelegramStorageBotToken"];
using var httpClient = new HttpClient();
var getFileUrl = $"https://api.telegram.org/bot{_botToken}/getFile?file_id={metadata.FileId}";
var getFileUrl = $"https://api.telegram.org/bot{botToken}/getFile?file_id={metadata.FileId}";
var getFileResponse = await httpClient.GetAsync(getFileUrl);
if (!getFileResponse.IsSuccessStatusCode)
@@ -145,7 +149,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
}
var filePath = getFileResult.Result.FilePath;
var fileUrl = $"https://api.telegram.org/file/bot{_botToken}/{filePath}";
var fileUrl = $"https://api.telegram.org/file/bot{botToken}/{filePath}";
var fileResponse = await httpClient.GetAsync(fileUrl);
if (!fileResponse.IsSuccessStatusCode)

View File

@@ -6,52 +6,49 @@ using Foxel.Services.Configuration;
namespace Foxel.Services.Storage.Providers;
[StorageProvider(StorageType.WebDAV)]
public class WebDavStorageProvider : IStorageProvider
public class WebDavStorageProvider(IConfigService configService) : IStorageProvider
{
private readonly string _webDavServerUrl;
private readonly string _serverUrl;
private readonly string _basePath;
private readonly string _publicUrl;
private readonly HttpClient _httpClient;
public WebDavStorageProvider(IConfigService configService)
private HttpClient CreateClient()
{
_webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
var httpClient = new HttpClient();
var userName = configService["Storage:WebDAVUserName"];
var password = configService["Storage:WebDAVPassword"];
_basePath = configService["Storage:WebDAVBasePath"].Trim('/');
_publicUrl = configService["Storage:WebDAVPublicUrl"].TrimEnd('/');
_serverUrl = configService["AppSettings:ServerUrl"];
_httpClient = new HttpClient();
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue);
}
return httpClient;
}
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
{
try
{
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
string basePath = configService["Storage:WebDAVBasePath"].Trim('/');
// 创建唯一的文件存储路径
string currentDate = DateTime.Now.ToString("yyyy/MM");
string ext = Path.GetExtension(fileName);
string newFileName = $"{Guid.NewGuid()}{ext}";
string relativePath = $"{_basePath}/{currentDate}/{newFileName}";
string relativePath = $"{basePath}/{currentDate}/{newFileName}";
// 确保目录存在
await EnsureDirectoryExistsAsync($"{_basePath}/{currentDate}");
await EnsureDirectoryExistsAsync($"{basePath}/{currentDate}");
// 上传文件内容
var requestUri = $"{_webDavServerUrl}/{relativePath}";
var requestUri = $"{webDavServerUrl}/{relativePath}";
using var client = CreateClient();
using var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
using var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Content = content;
var response = await _httpClient.SendAsync(request);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
return relativePath;
@@ -70,8 +67,11 @@ public class WebDavStorageProvider : IStorageProvider
if (string.IsNullOrEmpty(storagePath))
return;
var requestUri = $"{_webDavServerUrl}/{storagePath}";
var response = await _httpClient.DeleteAsync(requestUri);
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
var requestUri = $"{webDavServerUrl}/{storagePath}";
using var client = CreateClient();
var response = await client.DeleteAsync(requestUri);
if (response.StatusCode != System.Net.HttpStatusCode.NotFound)
{
@@ -91,12 +91,15 @@ public class WebDavStorageProvider : IStorageProvider
if (string.IsNullOrEmpty(storagePath))
return "/images/unavailable.gif";
if (!string.IsNullOrEmpty(_publicUrl))
string publicUrl = configService["Storage:WebDAVPublicUrl"].TrimEnd('/');
string serverUrl = configService["AppSettings:ServerUrl"];
if (!string.IsNullOrEmpty(publicUrl))
{
return $"{_publicUrl}/{storagePath}";
return $"{publicUrl}/{storagePath}";
}
return $"{_serverUrl}/api/picture/proxy?path={Uri.EscapeDataString(storagePath)}";
return $"{serverUrl}/api/picture/proxy?path={Uri.EscapeDataString(storagePath)}";
}
catch (Exception ex)
{
@@ -114,6 +117,8 @@ public class WebDavStorageProvider : IStorageProvider
throw new ArgumentException("存储路径不能为空");
}
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
// 创建临时目录
var tempDir = Path.Combine(Path.GetTempPath(), "FoxelWebDAVTemp");
if (!Directory.Exists(tempDir))
@@ -126,9 +131,10 @@ public class WebDavStorageProvider : IStorageProvider
string tempFilePath = Path.Combine(tempDir, fileName);
// 下载文件
var requestUri = $"{_webDavServerUrl}/{storagePath}";
var requestUri = $"{webDavServerUrl}/{storagePath}";
using var client = CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var response = await _httpClient.SendAsync(request);
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
await using var fileStream = new FileStream(tempFilePath, FileMode.Create);
@@ -150,18 +156,20 @@ public class WebDavStorageProvider : IStorageProvider
{
try
{
var requestUri = $"{_webDavServerUrl}/{directoryPath}";
string webDavServerUrl = configService["Storage:WebDAVServerUrl"].TrimEnd('/');
var requestUri = $"{webDavServerUrl}/{directoryPath}";
using var client = CreateClient();
// 检查目录是否存在 - 使用新的请求对象
using var headRequest = new HttpRequestMessage(HttpMethod.Head, requestUri);
var response = await _httpClient.SendAsync(headRequest);
var response = await client.SendAsync(headRequest);
if (response.IsSuccessStatusCode)
return;
// 创建目录 - 使用新的请求对象
using var mkcolRequest = new HttpRequestMessage(new HttpMethod("MKCOL"), requestUri);
response = await _httpClient.SendAsync(mkcolRequest);
response = await client.SendAsync(mkcolRequest);
// 处理状态码
if (response.StatusCode == System.Net.HttpStatusCode.Conflict ||
@@ -174,13 +182,13 @@ public class WebDavStorageProvider : IStorageProvider
await EnsureDirectoryExistsAsync(parentPath);
using var retryRequest = new HttpRequestMessage(new HttpMethod("MKCOL"), requestUri);
response = await _httpClient.SendAsync(retryRequest);
response = await client.SendAsync(retryRequest);
if (response.StatusCode == System.Net.HttpStatusCode.MethodNotAllowed)
{
using var putRequest = new HttpRequestMessage(HttpMethod.Put, $"{requestUri}/.dummy");
putRequest.Content = new StringContent(string.Empty);
await _httpClient.SendAsync(putRequest);
await client.SendAsync(putRequest);
}
else
{
@@ -192,7 +200,7 @@ public class WebDavStorageProvider : IStorageProvider
{
using var putRequest = new HttpRequestMessage(HttpMethod.Put, $"{requestUri}/.dummy");
putRequest.Content = new StringContent(string.Empty);
var putResponse = await _httpClient.SendAsync(putRequest);
var putResponse = await client.SendAsync(putRequest);
putResponse.EnsureSuccessStatusCode();
}
else

View File

@@ -137,21 +137,43 @@ const SystemConfig: React.FC = () => {
/>
</TabPane>
<TabPane tab="JWT 设置" key="Jwt">
<ConfigGroup
groupName="Jwt"
configs={{
SecretKey: configs.Jwt?.SecretKey || '',
Issuer: configs.Jwt?.Issuer || '',
Audience: configs.Jwt?.Audience || '',
}}
onSave={handleSaveConfig}
descriptions={{
SecretKey: 'JWT 加密密钥',
Issuer: 'JWT 签发者',
Audience: 'JWT 接收者',
}}
/>
<TabPane tab="授权配置" key="Authorization">
<Tabs defaultActiveKey="jwt" type="card" size={isMobile ? "small" : "middle"}>
<TabPane tab="JWT 设置" key="jwt">
<ConfigGroup
groupName="Jwt"
configs={{
SecretKey: configs.Jwt?.SecretKey || '',
Issuer: configs.Jwt?.Issuer || '',
Audience: configs.Jwt?.Audience || '',
}}
onSave={handleSaveConfig}
descriptions={{
SecretKey: 'JWT 加密密钥',
Issuer: 'JWT 签发者',
Audience: 'JWT 接收者',
}}
isMobile={isMobile}
/>
</TabPane>
<TabPane tab="GitHub认证" key="github">
<ConfigGroup
groupName="Authentication"
configs={{
"GitHubClientId": configs.Authentication?.["GitHubClientId"] || '',
"GitHubClientSecret": configs.Authentication?.["GitHubClientSecret"] || '',
"GitHubCallbackUrl": configs.Authentication?.["GitHubCallbackUrl"] || ''
}}
onSave={(_group, key, value) => handleSaveConfig('Authentication', key, value)}
descriptions={{
"GitHubClientId": 'GitHub OAuth 应用客户端ID',
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}}
isMobile={isMobile}
/>
</TabPane>
</Tabs>
</TabPane>
<TabPane tab="应用设置" key="AppSettings">
@@ -167,23 +189,6 @@ const SystemConfig: React.FC = () => {
/>
</TabPane>
<TabPane tab="GitHub认证" key="Authentication">
<ConfigGroup
groupName="Authentication"
configs={{
"GitHubClientId": configs.Authentication?.["GitHubClientId"] || '',
"GitHubClientSecret": configs.Authentication?.["GitHubClientSecret"] || '',
"GitHubCallbackUrl": configs.Authentication?.["GitHubCallbackUrl"] || ''
}}
onSave={(_group, key, value) => handleSaveConfig('Authentication', key, value)}
descriptions={{
"GitHubClientId": 'GitHub OAuth 应用客户端ID',
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}}
/>
</TabPane>
<TabPane tab="存储设置" key="Storage">
<div style={{
marginBottom: isMobile ? 16 : 20,