diff --git a/README.md b/README.md
index 252e0e8a..8a1d7f42 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@
- [x] 支持客户端路径指定模式,默认地址例子 https://{BASE_URL}/mj/submit/imagine, /mj-turbo/mj 是 turbo mode, /mj-relax/mj 是 relax mode, /mj-fast/mj 是 fast mode, /mj 不指定模式
- [x] CloudFlare 手动真人验证,触发后自动锁定账号,通过 GUI 直接验证或通过邮件通知验证
- [x] [⚠⚠暂不稳定] CloudFlare 自动真人验证,配置验证服务器地址(自动验证器仅支持 Windows 部署)
+- [x] 支持工作时间段配置,24 小时不间断绘图可能会触发警告,建议休息 6~8 小时
## 在线预览
@@ -371,7 +372,6 @@ https://discord.com/oauth2/authorize?client_id=xxx&permissions=8&scope=bot
## 路线图
-- [ ] 工作时间段配置,非工作时间段释放连接,到了非工作时间自动释放,24 小时不间断作业将会触发警告
- [ ] 优化指令和状态进度显示
- [ ] 优化任务和队列满时的提醒
- [ ] 优化共享账号的并发队列可能出现的问题
@@ -388,7 +388,6 @@ https://discord.com/oauth2/authorize?client_id=xxx&permissions=8&scope=bot
- [ ] 支付接入支持、支持微信、支付宝,支持绘图定价策略等
- [ ] 增加公告功能
- [ ] 账号增加咸鱼模式/放松模式,避免高频作业(此模式下不可创建新的绘图,仍可以执行其他命令,可以配置为多个时间段或定时等策略)
-- [ ] 增加强制休眠模式,或定时休眠模式
- [ ] 图生文 seed 值处理
- [ ] 自动读私信消息
- [ ] 增加允许速度模式配置
diff --git a/src/Midjourney.API/DiscordAccountInitializer.cs b/src/Midjourney.API/DiscordAccountInitializer.cs
index e2555a15..92132645 100644
--- a/src/Midjourney.API/DiscordAccountInitializer.cs
+++ b/src/Midjourney.API/DiscordAccountInitializer.cs
@@ -2,6 +2,8 @@
using Midjourney.Infrastructure.Domain;
using Midjourney.Infrastructure.LoadBalancer;
using Midjourney.Infrastructure.Services;
+using Midjourney.Infrastructure.Util;
+using MimeKit;
using Serilog;
using ILogger = Serilog.ILogger;
@@ -19,6 +21,8 @@ public class DiscordAccountInitializer : IHostedService
private readonly ProxyProperties _properties;
private readonly ILogger _logger;
+ private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
+ private Timer _timer;
public DiscordAccountInitializer(
DiscordLoadBalancer discordLoadBalancer,
@@ -39,20 +43,6 @@ public DiscordAccountInitializer(
///
/// 取消令牌。
public async Task StartAsync(CancellationToken cancellationToken)
- {
- _ = Task.Run(async () =>
- {
- await Initialize();
- });
-
- await Task.CompletedTask;
- }
-
- ///
- /// 初始化所有账号
- ///
- ///
- public async Task Initialize(params DiscordAccountConfig[] appends)
{
var proxy = _properties.Proxy;
if (!string.IsNullOrEmpty(proxy.Host))
@@ -63,103 +53,169 @@ public async Task Initialize(params DiscordAccountConfig[] appends)
Environment.SetEnvironmentVariable("https_proxyPort", proxy.Port.ToString());
}
- var db = DbHelper.AccountStore;
- var accounts = db.GetAll().OrderBy(c => c.Sort).ToList();
+ _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
+
+ await Task.CompletedTask;
+ }
- // 将启动配置中的 account 添加到数据库
- var configAccounts = _properties.Accounts.ToList();
- if (!string.IsNullOrEmpty(_properties.Discord.ChannelId))
+ private async void DoWork(object state)
+ {
+ if (_semaphoreSlim.CurrentCount == 0)
{
- configAccounts.Add(_properties.Discord);
+ return;
}
- if (appends?.Length > 0)
+ await _semaphoreSlim.WaitAsync();
+
+ try
+ {
+ _logger.Information("开始例行检查");
+
+ await Initialize();
+
+ _logger.Information("例行检查完成");
+ }
+ catch (Exception ex)
{
- configAccounts.AddRange(appends);
+ _logger.Error(ex, "执行例行检查时发生异常");
}
+ finally
+ {
+ _semaphoreSlim.Release();
+ }
+ }
- foreach (var configAccount in configAccounts)
+ ///
+ /// 初始化所有账号
+ ///
+ ///
+ public async Task Initialize(params DiscordAccountConfig[] appends)
+ {
+ var isLock = LocalLock.TryLock("Initialize", TimeSpan.FromSeconds(10), async () =>
{
- var account = accounts.FirstOrDefault(c => c.ChannelId == configAccount.ChannelId);
- if (account == null)
+ var proxy = _properties.Proxy;
+ var db = DbHelper.AccountStore;
+ var accounts = db.GetAll().OrderBy(c => c.Sort).ToList();
+
+ // 将启动配置中的 account 添加到数据库
+ var configAccounts = _properties.Accounts.ToList();
+ if (!string.IsNullOrEmpty(_properties.Discord.ChannelId))
{
- if (configAccount.Interval < 1.2m)
- {
- configAccount.Interval = 1.2m;
- }
+ configAccounts.Add(_properties.Discord);
+ }
- account = new DiscordAccount
- {
- Id = configAccount.ChannelId,
- ChannelId = configAccount.ChannelId,
-
- 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,
- BotToken = configAccount.BotToken,
- TimeoutMinutes = configAccount.TimeoutMinutes,
- PrivateChannelId = configAccount.PrivateChannelId,
- NijiBotChannelId = configAccount.NijiBotChannelId,
- MaxQueueSize = configAccount.MaxQueueSize,
- Mode = configAccount.Mode,
- Weight = configAccount.Weight,
- Remark = configAccount.Remark,
- RemixAutoSubmit = configAccount.RemixAutoSubmit,
- Sponsor = configAccount.Sponsor,
- Sort = configAccount.Sort,
- Interval = configAccount.Interval
- };
-
- db.Add(account);
- accounts.Add(account);
+ if (appends?.Length > 0)
+ {
+ configAccounts.AddRange(appends);
}
- }
- var instances = _discordLoadBalancer.GetAllInstances();
- foreach (var account in accounts)
- {
- if (!account.Enable)
+ foreach (var configAccount in configAccounts)
{
- continue;
+ var account = accounts.FirstOrDefault(c => c.ChannelId == configAccount.ChannelId);
+ if (account == null)
+ {
+ if (configAccount.Interval < 1.2m)
+ {
+ configAccount.Interval = 1.2m;
+ }
+
+ account = new DiscordAccount
+ {
+ Id = configAccount.ChannelId,
+ ChannelId = configAccount.ChannelId,
+
+ 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,
+ BotToken = configAccount.BotToken,
+ TimeoutMinutes = configAccount.TimeoutMinutes,
+ PrivateChannelId = configAccount.PrivateChannelId,
+ NijiBotChannelId = configAccount.NijiBotChannelId,
+ MaxQueueSize = configAccount.MaxQueueSize,
+ Mode = configAccount.Mode,
+ Weight = configAccount.Weight,
+ Remark = configAccount.Remark,
+ RemixAutoSubmit = configAccount.RemixAutoSubmit,
+ Sponsor = configAccount.Sponsor,
+ Sort = configAccount.Sort,
+ Interval = configAccount.Interval,
+ WorkTime = configAccount.WorkTime
+ };
+
+ db.Add(account);
+ accounts.Add(account);
+ }
}
- IDiscordInstance disInstance = null;
- try
+ var instances = _discordLoadBalancer.GetAllInstances();
+ foreach (var account in accounts)
{
- disInstance = _discordLoadBalancer.GetDiscordInstance(account.ChannelId);
- if (disInstance == null)
+ if (!account.Enable)
{
- disInstance = await _discordAccountHelper.CreateDiscordInstance(account);
- instances.Add(disInstance);
- _discordLoadBalancer.AddInstance(disInstance);
+ continue;
+ }
- // TODO 这里应该等待初始化完成,并获取用户信息验证,获取用户成功后设置为可用状态
- // 多账号启动时,等待一段时间再启动下一个账号
- await Task.Delay(1000 * 5);
+ IDiscordInstance disInstance = null;
+ try
+ {
+ disInstance = _discordLoadBalancer.GetDiscordInstance(account.ChannelId);
+
+ // 判断是否在工作时间内
+ if (DateTime.Now.IsInWorkTime(account.WorkTime))
+ {
+ if (disInstance == null)
+ {
+ disInstance = await _discordAccountHelper.CreateDiscordInstance(account);
+ instances.Add(disInstance);
+ _discordLoadBalancer.AddInstance(disInstance);
+
+ // 这里应该等待初始化完成,并获取用户信息验证,获取用户成功后设置为可用状态
+ // 多账号启动时,等待一段时间再启动下一个账号
+ await Task.Delay(1000 * 5);
+
+ // 启动后执行 info setting 操作
+ await _taskService.InfoSetting(account.ChannelId);
+ }
+ }
+ else
+ {
+ // 非工作时间内,如果存在实例则释放
+ if (disInstance != null)
+ {
+ _discordLoadBalancer.RemoveInstance(disInstance);
+
+ disInstance.Dispose();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Account({@0}) init fail, disabled: {@1}", account.GetDisplay(), ex.Message);
- // 启动后执行 info setting 操作
- await _taskService.InfoSetting(account.ChannelId);
+ account.Enable = false;
+ account.DisabledReason = "初始化失败";
+
+ db.Update(account);
+
+ disInstance?.ClearAccountCache(account.Id);
}
}
- catch (Exception ex)
- {
- _logger.Error("Account({@0}) init fail, disabled: {@1}", account.GetDisplay(), ex.Message);
- account.Enable = false;
+ var enableInstanceIds = instances.Where(instance => instance.IsAlive)
+ .Select(instance => instance.GetInstanceId)
+ .ToHashSet();
- db.Update(account);
- disInstance?.ClearAccountCache(account.Id);
- }
+ _logger.Information("当前可用账号数 [{@0}] - {@1}", enableInstanceIds.Count, string.Join(", ", enableInstanceIds));
+ });
+ if (!isLock)
+ {
+ throw new LogicException("初始化中,请稍后重拾");
}
- var enableInstanceIds = instances.Where(instance => instance.IsAlive)
- .Select(instance => instance.GetInstanceId)
- .ToHashSet();
-
- _logger.Information("当前可用账号数 [{@0}] - {@1}", enableInstanceIds.Count, string.Join(", ", enableInstanceIds));
+ await Task.CompletedTask;
}
///
@@ -196,11 +252,24 @@ public void UpdateAccount(DiscordAccount param)
model.CfHashUrl = null;
model.CfUrl = null;
+
+ // 验证 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)
+ {
+ param.WorkTime = null;
+ }
+ }
+
+ model.WorkTime = param.WorkTime;
model.Interval = param.Interval;
model.Sort = param.Sort;
model.Enable = param.Enable;
@@ -282,9 +351,13 @@ public void DeleteAccount(string id)
///
///
///
- public Task StopAsync(CancellationToken cancellationToken)
+ public async Task StopAsync(CancellationToken cancellationToken)
{
- return Task.CompletedTask;
+ _logger.Information("例行检查服务已停止");
+
+ _timer?.Change(Timeout.Infinite, 0);
+ await _semaphoreSlim.WaitAsync();
+ _semaphoreSlim.Release();
}
}
}
\ No newline at end of file
diff --git a/src/Midjourney.API/wwwroot/index.html b/src/Midjourney.API/wwwroot/index.html
index cfcf9a95..15e722bb 100644
--- a/src/Midjourney.API/wwwroot/index.html
+++ b/src/Midjourney.API/wwwroot/index.html
@@ -7,10 +7,10 @@
Midjourney Proxy Admin
-
+
-
+