feat: add fnOS packaging, storage workflows and release pipeline

This commit is contained in:
2026-08-11 18:05:49 +08:00
parent c5922f9b08
commit 95932f0199
181 changed files with 24024 additions and 1164 deletions
+76 -22
View File
@@ -20,6 +20,7 @@ using System.Collections.Concurrent;
//using Swashbuckle.AspNetCore.SwaggerGen;
//using Swashbuckle.AspNetCore.SwaggerUI;
using System.IO.Compression;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Text;
@@ -242,27 +243,35 @@ namespace dy.net.extension
{
// 提前创建连接字符串,避免每次创建ISqlSugarClient都调用(减少重复计算)
string sqliteConn = CreateSqliteDBConn(dbpath);
// Schema initialization is a one-time startup gate. Running CodeFirst in every scoped client
// caused concurrent requests/workers to race schema changes and made upgrade failures non-fatal.
using (var initializer = CreateSqlSugarClient(sqliteConn, initializeSchema: false))
{
initializer.DbMaintenance.CreateDatabase();
initializer.CodeFirst.InitTables(_entityTypes);
}
services.AddScoped<ISqlSugarClient>(db =>
{
var sqlSugar = new SqlSugarClient(new ConnectionConfig
{
ConnectionString = sqliteConn,
InitKeyType = InitKeyType.Attribute,
DbType = DbType.Sqlite,
IsAutoCloseConnection = true
}, db =>
{
// 移除空的Debug日志委托,避免空委托的内存占用
db.Aop.OnError = (e) =>
{
Serilog.Log.Error(e, $"SqlSugar执行错误:{e.Message}SQL{e.Sql}");
};
return CreateSqlSugarClient(sqliteConn, initializeSchema: false);
});
}
private static SqlSugarClient CreateSqlSugarClient(string connectionString, bool initializeSchema)
{
return new SqlSugarClient(new ConnectionConfig
{
ConnectionString = connectionString,
InitKeyType = InitKeyType.Attribute,
DbType = DbType.Sqlite,
IsAutoCloseConnection = true
}, db =>
{
db.Aop.OnError = e => Serilog.Log.Error(e, $"SqlSugar执行错误:{e.Message}SQL{e.Sql}");
if (initializeSchema)
{
db.DbMaintenance.CreateDatabase();
// 核心优化:使用缓存的实体类型,避免每次都反射(减少GC和内存)
db.CodeFirst.InitTables(_entityTypes);
});
return sqlSugar;
}
});
}
@@ -279,6 +288,7 @@ namespace dy.net.extension
services.AddScoped<DouyinCollectCustomSyncJob>();
services.AddScoped<DouyinMixSyncJob>();
services.AddScoped<DouyinSeriesSyncJob>();
services.AddScoped<DouyinLiveStatusJob>();
// 提前创建Quartz的SQLite连接字符串,避免重复调用
string quartzConn = CreateSqliteDBConn(dbPath);
@@ -297,8 +307,11 @@ namespace dy.net.extension
config.ConnectionString = quartzConn; // 使用提前创建的连接字符串
config.TablePrefix = "QRTZ_";
});
s.UseProperties = false;
s.UseBinarySerializer();
// All Quartz job data used by this application consists of strings. Persist it as
// properties and use the supported JSON serializer. BinaryFormatter is disabled on
// current .NET runtimes and prevented manual triggers from being stored.
s.UseProperties = true;
s.UseSystemTextJsonSerializer();
});
});
@@ -317,7 +330,7 @@ namespace dy.net.extension
public static void AddHttpClients(this IServiceCollection services)
{
// 通用忽略SSL的Handler工厂:提取为局部方法,避免重复创建逻辑
static HttpMessageHandler IgnoreSslHandlerFactory()
static HttpMessageHandler IgnoreSslHandlerFactory(bool allowAutoRedirect = true)
{
var handler = new SocketsHttpHandler
{
@@ -326,6 +339,7 @@ namespace dy.net.extension
ConnectTimeout = TimeSpan.FromSeconds(30), // 核心修复:移除无限超时,避免请求挂起泄漏
PooledConnectionLifetime = TimeSpan.FromMinutes(5), // 优化:连接池生命周期,自动释放闲置连接
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 优化:闲置连接超时,减少内存占用
AllowAutoRedirect = allowAutoRedirect,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, __, ___, ____) => true
@@ -340,7 +354,7 @@ namespace dy.net.extension
client.DefaultRequestHeaders.UserAgent.ParseAdd(DouyinRequestParamManager.DY_USER_AGENT);
client.BaseAddress = new Uri(DouyinRequestParamManager.DouyinHost);
client.Timeout = TimeSpan.FromSeconds(60); // 设置请求超时,避免无限等待
}).ConfigurePrimaryHttpMessageHandler(IgnoreSslHandlerFactory);
}).ConfigurePrimaryHttpMessageHandler(() => IgnoreSslHandlerFactory());
// 抖音下载客户端
services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT_DOWN, client =>
@@ -348,7 +362,41 @@ namespace dy.net.extension
client.DefaultRequestHeaders.UserAgent.ParseAdd(DouyinRequestParamManager.DY_USER_AGENT);
client.DefaultRequestHeaders.Referrer = new Uri(DouyinRequestParamManager.DouyinHost);
client.Timeout = TimeSpan.FromMinutes(5); // 下载超时设为5分钟,合理且不泄漏
}).ConfigurePrimaryHttpMessageHandler(IgnoreSslHandlerFactory);
}).ConfigurePrimaryHttpMessageHandler(() => IgnoreSslHandlerFactory(false));
// WebDAV 默认严格校验证书;仅当用户在界面明确开启自签证书兼容时使用 insecure 客户端。
services.AddHttpClient("webdav", client =>
{
client.Timeout = TimeSpan.FromMinutes(10);
client.DefaultRequestHeaders.UserAgent.ParseAdd("dysync.net-webdav/1.0");
});
services.AddHttpClient("webdav-insecure", client =>
{
client.Timeout = TimeSpan.FromMinutes(10);
client.DefaultRequestHeaders.UserAgent.ParseAdd("dysync.net-webdav/1.0");
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
// OpenList (AList) 原生 API 客户端
services.AddHttpClient("openlist", client =>
{
// Cloud-backed OpenList drivers can legitimately need more than 30 seconds to
// refresh a large directory. A 30 second HttpClient timeout surfaced as the
// unhelpful "Operation canceled" and incorrectly tripped storage health.
client.Timeout = TimeSpan.FromMinutes(2);
client.DefaultRequestVersion = HttpVersion.Version11;
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 4,
ConnectTimeout = TimeSpan.FromSeconds(10),
UseCookies = false
});
}
/// <summary>
@@ -368,6 +416,7 @@ namespace dy.net.extension
type.IsClass &&
!type.IsAbstract &&
!type.IsGenericTypeDefinition &&
!typeof(IHostedService).IsAssignableFrom(type) &&
type.Namespace != null &&
(includeSubNamespaces
? type.Namespace.StartsWith(@namespace, StringComparison.Ordinal)
@@ -527,7 +576,12 @@ namespace dy.net.extension
.Enrich.FromLogContext()
.Filter.ByExcluding(e => e.Level == LogEventLevel.Information) // 排除Info级别的日志
.Filter.ByExcluding(Matching.FromSource("Microsoft"))
.Filter.ByExcluding(Matching.FromSource("Quartz"))
// Quartz 的 Debug 日志非常密集,但 Warning/Error 必须保留,否则任务在进入
// 业务代码前失败时,任务中心还没有记录,用户也看不到任何错误原因。
.Filter.ByExcluding(e =>
e.Level < LogEventLevel.Warning
&& e.Properties.TryGetValue("SourceContext", out var source)
&& source.ToString().Contains("Quartz", StringComparison.OrdinalIgnoreCase))
.WriteTo.Console(new RenderedCompactJsonFormatter(), LogEventLevel.Debug)
//.WriteTo.MySQL(connectionString: builder.Configuration.GetConnectionString("DbConnectionString"), tableName: "Logs") // 输出到数据库
.WriteTo.Logger(configure => configure