diff --git a/src/Midjourney.API/Controllers/AdminController.cs b/src/Midjourney.API/Controllers/AdminController.cs index 76d5e7a8..682c5096 100644 --- a/src/Midjourney.API/Controllers/AdminController.cs +++ b/src/Midjourney.API/Controllers/AdminController.cs @@ -665,8 +665,10 @@ public async Task AccountAdd([FromBody] DiscordAccountConfig accountConf return Result.Fail("演示模式,禁止操作"); } - var model = DbHelper.AccountStore.GetCollection().Query() - .Where(c => c.ChannelId == accountConfig.ChannelId).FirstOrDefault(); + var model = DbHelper.AccountStore.GetCollection() + .Query() + .Where(c => c.ChannelId == accountConfig.ChannelId) + .FirstOrDefault(); if (model != null) { diff --git a/src/Midjourney.API/DiscordAccountInitializer.cs b/src/Midjourney.API/DiscordAccountInitializer.cs index d889552f..a5df0f5f 100644 --- a/src/Midjourney.API/DiscordAccountInitializer.cs +++ b/src/Midjourney.API/DiscordAccountInitializer.cs @@ -186,7 +186,6 @@ public async Task StartAsync(CancellationToken cancellationToken) MongoAutoMigrate(); } - var oss = GlobalConfiguration.Setting.AliyunOss; if (oss?.Enable == true && oss?.IsAutoMigrationLocalFile == true) { @@ -509,7 +508,7 @@ public static void CheckAndDeleteOldDocuments() /// public async Task Initialize(params DiscordAccountConfig[] appends) { - var isLock = await AsyncLocalLock.TryLockAsync("Initialize:all", TimeSpan.FromSeconds(10), async () => + var isLock = await AsyncLocalLock.TryLockAsync("initialize:all", TimeSpan.FromSeconds(10), async () => { var db = DbHelper.AccountStore; var accounts = db.GetAll().OrderBy(c => c.Sort).ToList(); @@ -559,7 +558,7 @@ public async Task Initialize(params DiscordAccountConfig[] appends) }); if (!isLock) { - throw new LogicException("初始化中,请稍后重拾"); + throw new LogicException("初始化中,请稍后重试"); } await Task.CompletedTask; @@ -575,10 +574,18 @@ public async Task StartCheckAccount(DiscordAccount account) return; } - var isLock = await AsyncLocalLock.TryLockAsync($"Initialize:{account.Id}", TimeSpan.FromSeconds(5), async () => + var isLock = await AsyncLocalLock.TryLockAsync($"initialize:{account.Id}", TimeSpan.FromSeconds(5), async () => { var db = DbHelper.AccountStore; - Infrastructure.LoadBalancer.DiscordInstance disInstance = null; + + // 获取获取值 + account = db.Get(account.Id)!; + if (account.Enable != true) + { + return; + } + + DiscordInstance disInstance = null; try { disInstance = _discordLoadBalancer.GetDiscordInstance(account.ChannelId); @@ -648,7 +655,7 @@ public async Task StartCheckAccount(DiscordAccount account) } account.DayDrawCount = dayCount; - db.Update(account); + db.Update("SubChannels,SubChannelValues,FastExhausted,DayDrawCount", account); // 连接前先判断账号是否正常 var success = await _discordAccountHelper.ValidateAccount(account); @@ -658,7 +665,7 @@ public async Task StartCheckAccount(DiscordAccount account) } disInstance = await _discordAccountHelper.CreateDiscordInstance(account); - _discordLoadBalancer.AddInstance((Infrastructure.LoadBalancer.DiscordInstance)disInstance); + _discordLoadBalancer.AddInstance(disInstance); // 这里应该等待初始化完成,并获取用户信息验证,获取用户成功后设置为可用状态 // 多账号启动时,等待一段时间再启动下一个账号 @@ -673,7 +680,7 @@ public async Task StartCheckAccount(DiscordAccount account) if (disInstance?.Account != null && disInstance.Account.FastExhausted) { // 每 3~6 小时,和启动时检查账号快速用量是否用完了 - if (disInstance.Account.InfoUpdated == null || disInstance.Account.InfoUpdated.Value.AddMinutes((double)5) < DateTime.Now) + if (disInstance.Account.InfoUpdated == null || disInstance.Account.InfoUpdated.Value.AddMinutes(5) < DateTime.Now) { // 检查账号快速用量是否用完了 // 随机 3~6 小时,执行一次 @@ -702,16 +709,16 @@ await _memoryCache.GetOrCreateAsync(key, async c => } // 判断 info 检查时间是否在 5 分钟内 - if (disInstance.Account.InfoUpdated != null && disInstance.Account.InfoUpdated.Value.AddMinutes((double)5) >= DateTime.Now) + if (disInstance.Account.InfoUpdated != null && disInstance.Account.InfoUpdated.Value.AddMinutes(5) >= DateTime.Now) { // 提取 fastime // 如果检查完之后,快速超过 1 小时,则标记为快速未用完 - var fastTime = disInstance.Account.FastTimeRemaining?.ToString()?.Split('/')?.FirstOrDefault()?.Trim(); - if (!string.IsNullOrWhiteSpace((string)fastTime) && double.TryParse((string)fastTime, out var ftime) && ftime >= 1) + var fastTime = disInstance.Account.FastTimeRemaining?.ToString()?.Split('/')?.FirstOrDefault()?.Trim(); + if (!string.IsNullOrWhiteSpace(fastTime) && double.TryParse(fastTime, out var ftime) && ftime >= 1) { // 标记未用完快速 disInstance.Account.FastExhausted = false; - db.Update((DiscordAccount)disInstance.Account); + db.Update("FastExhausted", disInstance.Account); disInstance.ClearAccountCache(account.Id); @@ -744,10 +751,9 @@ await _memoryCache.GetOrCreateAsync(key, async c => disInstance = null; } }); - if (!isLock) { - throw new LogicException("初始化中,请稍后重拾"); + throw new LogicException("初始化中,请稍后重试"); } await Task.CompletedTask; @@ -759,98 +765,97 @@ await _memoryCache.GetOrCreateAsync(key, async c => /// public void UpdateAccount(DiscordAccount param) { - DiscordAccount model = null; - - var disInstance = _discordLoadBalancer.GetDiscordInstance(param.ChannelId); - if (disInstance != null) - { - model = disInstance.Account; - } - + var model = DbHelper.AccountStore.Get(param.Id); if (model == null) { - model = DbHelper.AccountStore.Get(param.Id); + throw new LogicException("账号不存在"); } - if (model == null) + // 更新一定要加锁,因为其他进程会修改 account 值,导致值覆盖 + var isLock = AsyncLocalLock.TryLock($"initialize:{param.Id}", TimeSpan.FromSeconds(3), () => { - throw new LogicException("账号不存在"); - } + model = DbHelper.AccountStore.Get(param.Id)!; - // 渠道 ID 和 服务器 ID 禁止修改 - //model.ChannelId = account.ChannelId; - //model.GuildId = account.GuildId; + // 渠道 ID 和 服务器 ID 禁止修改 + //model.ChannelId = account.ChannelId; + //model.GuildId = account.GuildId; - // 更新账号重连时,自动解锁 - model.Lock = false; - model.CfHashCreated = null; - model.CfHashUrl = null; - model.CfUrl = null; + // 更新账号重连时,自动解锁 + model.Lock = false; + model.CfHashCreated = null; + model.CfHashUrl = null; + model.CfUrl = null; - // 验证 Interval - if (param.Interval < 1.2m) - { - param.Interval = 1.2m; - } + // 验证 Interval + if (param.Interval < 1.2m) + { + param.Interval = 1.2m; + } - // 验证 WorkTime - if (!string.IsNullOrEmpty(param.WorkTime)) - { - var ts = param.WorkTime.ToTimeSlots(); - if (ts.Count == 0) + // 验证 WorkTime + if (!string.IsNullOrEmpty(param.WorkTime)) { - param.WorkTime = null; + var ts = param.WorkTime.ToTimeSlots(); + if (ts.Count == 0) + { + param.WorkTime = null; + } } - } - // 验证 FishingTime - if (!string.IsNullOrEmpty(param.FishingTime)) - { - var ts = param.FishingTime.ToTimeSlots(); - if (ts.Count == 0) + // 验证 FishingTime + if (!string.IsNullOrEmpty(param.FishingTime)) { - param.FishingTime = null; + var ts = param.FishingTime.ToTimeSlots(); + if (ts.Count == 0) + { + param.FishingTime = null; + } } - } - model.EnableFastToRelax = param.EnableFastToRelax; - model.IsBlend = param.IsBlend; - model.IsDescribe = param.IsDescribe; - model.IsShorten = param.IsShorten; - model.DayDrawLimit = param.DayDrawLimit; - model.IsVerticalDomain = param.IsVerticalDomain; - model.VerticalDomainIds = param.VerticalDomainIds; - model.SubChannels = param.SubChannels; - - model.PermanentInvitationLink = param.PermanentInvitationLink; - model.FishingTime = param.FishingTime; - model.EnableNiji = param.EnableNiji; - model.EnableMj = param.EnableMj; - model.AllowModes = param.AllowModes; - model.WorkTime = param.WorkTime; - model.Interval = param.Interval; - model.AfterIntervalMin = param.AfterIntervalMin; - model.AfterIntervalMax = param.AfterIntervalMax; - model.Sort = param.Sort; - model.Enable = param.Enable; - model.PrivateChannelId = param.PrivateChannelId; - model.NijiBotChannelId = param.NijiBotChannelId; - model.UserAgent = param.UserAgent; - model.RemixAutoSubmit = param.RemixAutoSubmit; - model.CoreSize = param.CoreSize; - model.QueueSize = param.QueueSize; - model.MaxQueueSize = param.MaxQueueSize; - model.TimeoutMinutes = param.TimeoutMinutes; - model.Weight = param.Weight; - model.Remark = param.Remark; - model.BotToken = param.BotToken; - model.UserToken = param.UserToken; - model.Mode = param.Mode; - model.Sponsor = param.Sponsor; - - DbHelper.AccountStore.Update(model); - - disInstance?.ClearAccountCache(model.Id); + model.EnableFastToRelax = param.EnableFastToRelax; + model.IsBlend = param.IsBlend; + model.IsDescribe = param.IsDescribe; + model.IsShorten = param.IsShorten; + model.DayDrawLimit = param.DayDrawLimit; + model.IsVerticalDomain = param.IsVerticalDomain; + model.VerticalDomainIds = param.VerticalDomainIds; + model.SubChannels = param.SubChannels; + + model.PermanentInvitationLink = param.PermanentInvitationLink; + model.FishingTime = param.FishingTime; + model.EnableNiji = param.EnableNiji; + model.EnableMj = param.EnableMj; + model.AllowModes = param.AllowModes; + model.WorkTime = param.WorkTime; + model.Interval = param.Interval; + model.AfterIntervalMin = param.AfterIntervalMin; + model.AfterIntervalMax = param.AfterIntervalMax; + model.Sort = param.Sort; + model.Enable = param.Enable; + model.PrivateChannelId = param.PrivateChannelId; + model.NijiBotChannelId = param.NijiBotChannelId; + model.UserAgent = param.UserAgent; + model.RemixAutoSubmit = param.RemixAutoSubmit; + model.CoreSize = param.CoreSize; + model.QueueSize = param.QueueSize; + model.MaxQueueSize = param.MaxQueueSize; + model.TimeoutMinutes = param.TimeoutMinutes; + model.Weight = param.Weight; + model.Remark = param.Remark; + model.BotToken = param.BotToken; + model.UserToken = param.UserToken; + model.Mode = param.Mode; + model.Sponsor = param.Sponsor; + + DbHelper.AccountStore.Update(model); + + var disInstance = _discordLoadBalancer.GetDiscordInstance(model.ChannelId); + disInstance?.ClearAccountCache(model.Id); + }); + if (!isLock) + { + throw new LogicException("作业执行中,请稍后重试"); + } } /// diff --git a/src/Midjourney.API/Startup.cs b/src/Midjourney.API/Startup.cs index 64146a68..65df1b4c 100644 --- a/src/Midjourney.API/Startup.cs +++ b/src/Midjourney.API/Startup.cs @@ -21,6 +21,7 @@ // The use of this software for any form of illegal face swapping, // invasion of privacy, or any other unlawful purposes is strictly prohibited. // Violation of these terms may result in termination of the license and may subject the violator to legal action. + global using Midjourney.Infrastructure; global using Midjourney.Infrastructure.Models; diff --git a/src/Midjourney.Infrastructure/BotMessageListener.cs b/src/Midjourney.Infrastructure/BotMessageListener.cs index 7e9ae01d..6ba51d92 100644 --- a/src/Midjourney.Infrastructure/BotMessageListener.cs +++ b/src/Midjourney.Infrastructure/BotMessageListener.cs @@ -310,7 +310,7 @@ public void OnMessage(JsonElement raw) Account.CfHashUrl = hashUrl; Account.CfHashCreated = DateTime.Now; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update(Account); _discordInstance.ClearAccountCache(Account.Id); try @@ -405,7 +405,7 @@ public void OnMessage(JsonElement raw) Account.DisabledReason = "CF 人工验证..."; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update(Account); _discordInstance.ClearAccountCache(Account.Id); } } @@ -525,7 +525,7 @@ public void OnMessage(JsonElement raw) } } - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update("Components,NijiComponents", Account); _discordInstance.ClearAccountCache(Account.Id); return; @@ -540,13 +540,13 @@ public void OnMessage(JsonElement raw) if (applicationId == Constants.NIJI_APPLICATION_ID) { Account.NijiComponents = eventDataMsg.Components; - DbHelper.AccountStore.Update(Account); + DbHelper.AccountStore.Update("NijiComponents", Account); _discordInstance.ClearAccountCache(Account.Id); } else if (applicationId == Constants.MJ_APPLICATION_ID) { Account.Components = eventDataMsg.Components; - DbHelper.AccountStore.Update(Account); + DbHelper.AccountStore.Update("Components", Account); _discordInstance.ClearAccountCache(Account.Id); } } @@ -781,7 +781,7 @@ public void OnMessage(JsonElement raw) // 标记快速模式已经用完了 Account.FastExhausted = true; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update("FastExhausted", Account); _discordInstance?.ClearAccountCache(Account.Id); // 如果开启自动切换慢速模式 @@ -805,7 +805,7 @@ public void OnMessage(JsonElement raw) Account.Enable = false; Account.DisabledReason = "账号用量已经用完"; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update(Account); _discordInstance?.ClearAccountCache(Account.Id); _discordInstance?.Dispose(); @@ -854,7 +854,7 @@ public void OnMessage(JsonElement raw) Account.Enable = false; Account.DisabledReason = title; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update(Account); _discordInstance?.ClearAccountCache(Account.Id); _discordInstance?.Dispose(); @@ -967,7 +967,7 @@ public void OnMessage(JsonElement raw) var db = DbHelper.AccountStore; Account.InfoUpdated = DateTime.Now; - db.Update(Account); + db.Update("InfoUpdated", Account); _discordInstance?.ClearAccountCache(Account.Id); } } @@ -987,7 +987,7 @@ public void OnMessage(JsonElement raw) Account.NijiComponents = eventDataMsg.Components; Account.NijiSettingsMessageId = id; - DbHelper.AccountStore.Update(Account); + DbHelper.AccountStore.Update("NijiComponents,NijiSettingsMessageId", Account); _discordInstance?.ClearAccountCache(Account.Id); } else if (applicationId == Constants.MJ_APPLICATION_ID) @@ -995,7 +995,7 @@ public void OnMessage(JsonElement raw) Account.Components = eventDataMsg.Components; Account.SettingsMessageId = id; - DbHelper.AccountStore.Update(Account); + DbHelper.AccountStore.Update("Components,SettingsMessageId", Account); _discordInstance?.ClearAccountCache(Account.Id); } } diff --git a/src/Midjourney.Infrastructure/Data/LiteDBRepository.cs b/src/Midjourney.Infrastructure/Data/LiteDBRepository.cs index f5c792fb..40a8331e 100644 --- a/src/Midjourney.Infrastructure/Data/LiteDBRepository.cs +++ b/src/Midjourney.Infrastructure/Data/LiteDBRepository.cs @@ -15,11 +15,11 @@ // along with this program. If not, see . // Additional Terms: -// This software shall not be used for any illegal activities. +// This software shall not be used for any illegal activities. // Users must comply with all applicable laws and regulations, -// particularly those related to image and video processing. +// particularly those related to image and video processing. // The use of this software for any form of illegal face swapping, -// invasion of privacy, or any other unlawful purposes is strictly prohibited. +// invasion of privacy, or any other unlawful purposes is strictly prohibited. // Violation of these terms may result in termination of the license and may subject the violator to legal action. using LiteDB; using Midjourney.Infrastructure.Services; @@ -171,6 +171,36 @@ public void Update(T entity) col.Update(entity); } + /// + /// 部分更新 + /// + /// BotToken,IsBlend,Properties + /// + /// + public bool Update(string fields, T item) + { + // 获取现有文档 + var col = _db.GetCollection(); + var model = col.FindById(item.Id); + if (model == null) + return false; + + // 将更新对象的字段值复制到现有文档 + var fieldArray = fields.Split(','); + foreach (var field in fieldArray) + { + var prop = typeof(T).GetProperty(field.Trim()); + if (prop != null) + { + var newValue = prop.GetValue(item); + prop.SetValue(model, newValue); + } + } + + // 更新文档 + return col.Update(model); + } + /// /// 获取所有实体。 /// @@ -344,7 +374,6 @@ public List List() return _db.GetCollection().Query().ToList(); } - public List Where(Expression> filter, Expression> orderBy, bool orderByAsc, int limit) { var query = _db.GetCollection().Query(); diff --git a/src/Midjourney.Infrastructure/GlobalConfiguration.cs b/src/Midjourney.Infrastructure/GlobalConfiguration.cs index afe30d63..16338578 100644 --- a/src/Midjourney.Infrastructure/GlobalConfiguration.cs +++ b/src/Midjourney.Infrastructure/GlobalConfiguration.cs @@ -39,7 +39,7 @@ public class GlobalConfiguration /// /// 版本号 /// - public static string Version { get; set; } = "v5.2.8"; + public static string Version { get; set; } = "v5.3.0"; /// /// 全局配置项 diff --git a/src/Midjourney.Infrastructure/Models/DiscordAccount.cs b/src/Midjourney.Infrastructure/Models/DiscordAccount.cs index acf5b88b..1315e811 100644 --- a/src/Midjourney.Infrastructure/Models/DiscordAccount.cs +++ b/src/Midjourney.Infrastructure/Models/DiscordAccount.cs @@ -498,9 +498,9 @@ public static DiscordAccount Create(DiscordAccountConfig configAccount) Id = Guid.NewGuid().ToString(), ChannelId = configAccount.ChannelId, + UserAgent = string.IsNullOrEmpty(configAccount.UserAgent) ? Constants.DEFAULT_DISCORD_USER_AGENT : configAccount.UserAgent, GuildId = configAccount.GuildId, UserToken = configAccount.UserToken, - UserAgent = string.IsNullOrEmpty(configAccount.UserAgent) ? Constants.DEFAULT_DISCORD_USER_AGENT : configAccount.UserAgent, Enable = configAccount.Enable, CoreSize = configAccount.CoreSize, QueueSize = configAccount.QueueSize, diff --git a/src/Midjourney.Infrastructure/Services/DiscordInstance.cs b/src/Midjourney.Infrastructure/Services/DiscordInstance.cs index 086b7d1f..dece08f3 100644 --- a/src/Midjourney.Infrastructure/Services/DiscordInstance.cs +++ b/src/Midjourney.Infrastructure/Services/DiscordInstance.cs @@ -343,7 +343,7 @@ private void RuningCache() { Account.DayDrawCount = count; - DbHelper.AccountStore.Update(Account); + DbHelper.AccountStore.Update("DayDrawCount", Account); } } catch (Exception ex) diff --git a/src/Midjourney.Infrastructure/Util/AsyncLocalLock.cs b/src/Midjourney.Infrastructure/Util/AsyncLocalLock.cs index 4afbc319..45f78eb5 100644 --- a/src/Midjourney.Infrastructure/Util/AsyncLocalLock.cs +++ b/src/Midjourney.Infrastructure/Util/AsyncLocalLock.cs @@ -44,15 +44,41 @@ private static async Task LockEnterAsync(string key, TimeSpan span) return await semaphore.WaitAsync(span); } + + /// + /// 获取锁 + /// + /// + /// + /// + private static bool LockEnter(string key, TimeSpan span) + { + var semaphore = _lockObjs.GetOrAdd(key, new SemaphoreSlim(1, 1)); + return semaphore.Wait(span); + } + + /// /// 退出锁 /// /// private static void LockExit(string key) { + //if (_lockObjs.TryGetValue(key, out SemaphoreSlim semaphore) && semaphore != null) + //{ + // semaphore.Release(); + //} + if (_lockObjs.TryGetValue(key, out SemaphoreSlim semaphore) && semaphore != null) { semaphore.Release(); + + if (semaphore.CurrentCount == 1) // 表示没有其他线程在等待锁 + { + _lockObjs.TryRemove(key, out _); + + semaphore.Dispose(); // 释放 SemaphoreSlim 的资源 + } } } @@ -80,6 +106,30 @@ public static async Task TryLockAsync(string resource, TimeSpan expiration return false; } + /// + /// 等待并获取锁 + /// + /// + /// 等待锁超时时间,如果超时没有获取到锁,返回 false + /// + /// + public static bool TryLock(string resource, TimeSpan expirationTime, Action action) + { + if (LockEnter(resource, expirationTime)) + { + try + { + action?.Invoke(); + return true; + } + finally + { + LockExit(resource); + } + } + return false; + } + /// /// 判断指定的锁是否可用 /// diff --git a/src/Midjourney.Infrastructure/WebSocketManager.cs b/src/Midjourney.Infrastructure/WebSocketManager.cs index c5dbf205..7cca2f31 100644 --- a/src/Midjourney.Infrastructure/WebSocketManager.cs +++ b/src/Midjourney.Infrastructure/WebSocketManager.cs @@ -834,6 +834,11 @@ private void TryReconnect() { try { + if (_isDispose) + { + return; + } + var success = StartAsync(true).ConfigureAwait(false).GetAwaiter().GetResult(); if (!success) { @@ -857,6 +862,11 @@ private void TryReconnect() /// private void TryNewConnect() { + if (_isDispose) + { + return; + } + var isLock = LocalLock.TryLock("TryNewConnect", TimeSpan.FromSeconds(3), () => { for (int i = 1; i <= CONNECT_RETRY_LIMIT; i++) @@ -913,7 +923,7 @@ public void DisableAccount(string msg) Account.Enable = false; Account.DisabledReason = msg; - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update(Account); _discordInstance?.ClearAccountCache(Account.Id); _discordInstance?.Dispose(); @@ -1070,7 +1080,7 @@ private void NotifyWss(int code, string reason) } // 保存 - DbHelper.AccountStore.Save(Account); + DbHelper.AccountStore.Update("Enable,DisabledReason", Account); _discordInstance?.ClearAccountCache(Account.Id); }