feat: 优化昵称批量模板并支持活跃任务字典变量
This commit is contained in:
1 parent
7f821fb96d
commit
017fc329fa
3 files changed
+129
-41
No files matched your search
@@ -13,20 +13,23 @@
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
将对选中的账号批量修改昵称。支持两种模式:单个“批量昵称”或“昵称列表”。这两个输入都支持 `{time}` 和文本字典变量。
|
||||
将对选中的账号批量修改昵称。只需要填写“昵称模板”,支持一行一个模板,并支持多个变量组合,例如:<b>{diqu}歌神{geshou}</b>。
|
||||
</MudAlert>
|
||||
|
||||
<MudText Typo="Typo.subtitle2">账号数量:@accounts.Count</MudText>
|
||||
|
||||
<MudTextField @bind-Value="baseNickname" Label="批量昵称" Variant="Variant.Outlined" Required="true"
|
||||
HelperText="单昵称模式:例如“测试号{city}”。若填写了“昵称列表”,则优先使用昵称列表。" />
|
||||
<MudSwitch @bind-Value="appendPhoneLast4" Label="自动追加手机号后 4 位(单昵称模式)" Color="Color.Primary" />
|
||||
<MudTextField @bind-Value="nicknameTemplatesText"
|
||||
Label="昵称模板(换行分隔)"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
Lines="6"
|
||||
HelperText="每行一个昵称模板;按顺序分配给已选账号;用完后从头轮询。支持 {time} 和多个文本字典变量组合。" />
|
||||
|
||||
<MudTextField @bind-Value="nicknameLines" Label="昵称列表(换行分隔)" Variant="Variant.Outlined" Lines="6"
|
||||
HelperText="每行一个昵称模板;按顺序分配给已选账号;昵称用完后从头轮询。每行同样支持 `{time}` 和文本字典变量。" />
|
||||
<MudSwitch @bind-Value="appendPhoneLast4WhenDuplicate" Label="昵称重复时追加手机号后 4 位(昵称列表模式)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="appendPhoneLast4WhenDuplicate"
|
||||
Label="昵称重复时追加手机号后 4 位"
|
||||
Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
例如:昵称列表只有 1 行或轮询后重复时,为避免重复昵称可追加手机号后 4 位。
|
||||
当同一批次生成出重复昵称时,自动在后面追加该账号手机号后 4 位。
|
||||
</MudText>
|
||||
|
||||
@if (textDictionaryNames.Count > 0)
|
||||
@@ -96,9 +99,7 @@
|
||||
private List<Account> accounts = new();
|
||||
private List<string> textDictionaryNames = new();
|
||||
|
||||
private string baseNickname = "";
|
||||
private bool appendPhoneLast4 = true;
|
||||
private string nicknameLines = "";
|
||||
private string nicknameTemplatesText = string.Empty;
|
||||
private bool appendPhoneLast4WhenDuplicate = true;
|
||||
|
||||
private bool running;
|
||||
@@ -139,16 +140,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var nicknameList = ParseNicknameLines(nicknameLines);
|
||||
var useList = nicknameList.Count > 0;
|
||||
|
||||
if (!useList && string.IsNullOrWhiteSpace(baseNickname))
|
||||
var templates = ParseNicknameLines(nicknameTemplatesText);
|
||||
if (templates.Count == 0)
|
||||
{
|
||||
Snackbar.Add("批量昵称不能为空", Severity.Warning);
|
||||
Snackbar.Add("请至少填写一行昵称模板", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var templates = useList ? nicknameList : new List<string> { baseNickname };
|
||||
if (!ValidateTemplates(templates, out var templateError))
|
||||
{
|
||||
Snackbar.Add(templateError ?? "昵称模板不合法", Severity.Warning);
|
||||
@@ -200,10 +198,10 @@
|
||||
|
||||
try
|
||||
{
|
||||
var baseUsedCount = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var usedNicknames = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
var nicknameTemplate = useList ? nicknameList[processed % nicknameList.Count] : baseNickname;
|
||||
var nicknameTemplate = templates[processed % templates.Count];
|
||||
string resolvedNickname;
|
||||
try
|
||||
{
|
||||
@@ -218,15 +216,20 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
var needAppend = appendPhoneLast4;
|
||||
if (useList)
|
||||
if (string.IsNullOrWhiteSpace(resolvedNickname))
|
||||
{
|
||||
baseUsedCount.TryGetValue(resolvedNickname, out var used);
|
||||
baseUsedCount[resolvedNickname] = used + 1;
|
||||
needAppend = appendPhoneLast4WhenDuplicate && used > 0;
|
||||
processed++;
|
||||
failed++;
|
||||
results.Add(new ResultRow(account.DisplayPhone, string.Empty, false, "生成的昵称为空,请检查模板或字典内容"));
|
||||
StateHasChanged();
|
||||
continue;
|
||||
}
|
||||
|
||||
var newNickname = BuildNickname(resolvedNickname, account.Phone, needAppend);
|
||||
usedNicknames.TryGetValue(resolvedNickname, out var usedCount);
|
||||
usedNicknames[resolvedNickname] = usedCount + 1;
|
||||
|
||||
var shouldAppendPhoneTail = appendPhoneLast4WhenDuplicate && usedCount > 0;
|
||||
var newNickname = BuildNickname(resolvedNickname, account.Phone, shouldAppendPhoneTail);
|
||||
|
||||
var (ok, err) = await AccountTelegramTools.UpdateUserProfileAsync(
|
||||
accountId: account.Id,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@inject AccountCategoryManagementService CategoryManagement
|
||||
@inject DataDictionaryService DictionaryService
|
||||
@inject TemplateRenderingService TemplateRendering
|
||||
@using System.Text.Json
|
||||
@using TelegramPanel.Data.Entities
|
||||
@using TelegramPanel.Modules
|
||||
@@ -8,6 +10,7 @@
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
|
||||
持续活跃任务会在后台循环执行并按间隔发送内容。MaxMessages=0 表示持续运行,直到你在任务中心手动取消。
|
||||
词典消息现在支持 <code>{time}</code> 和文本字典变量;任务仍会先按“随机 / 队列”模式选中一行,再解析其中变量后发送。
|
||||
</MudAlert>
|
||||
|
||||
@if (loading)
|
||||
@@ -36,14 +39,23 @@
|
||||
Variant="Variant.Outlined"
|
||||
Lines="5"
|
||||
Immediate="true"
|
||||
HelperText="支持:用户名(可带 at 前缀)、https://t.me/xxx、频道ID(如 -1001234567890 或 1234567890),如私密群组无法复制链接可登录Telegram Web面板打开群组聊天界面查看链接,数字就是ID" />
|
||||
HelperText="支持:用户名(可带 at 前缀)、https://t.me/xxx、频道ID(如 -1001234567890 或 1234567890),如私密群组无法复制链接可登录 Telegram Web 面板打开群组聊天界面查看链接,数字就是 ID" />
|
||||
|
||||
<MudTextField @bind-Value="dictionaryText" @bind-Value:after="OnInputsChanged"
|
||||
Label="词典(每行一个)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="6"
|
||||
Immediate="true"
|
||||
HelperText="任务会按“随机/队列”模式从词典中取一条消息发送。" />
|
||||
HelperText="支持 {time} 和文本字典变量。任务会先按“随机 / 队列”模式选中一行,再解析变量后发送。" />
|
||||
|
||||
@if (textDictionaryNames.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption">可用文本变量:<code>{time}</code>、@string.Join("、", textDictionaryNames.Select(x => $"{{{x}}}"))</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption">当前可用内置变量:<code>{time}</code></MudText>
|
||||
}
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
@@ -93,15 +105,16 @@
|
||||
[Parameter] public string? InitialConfigJson { get; set; }
|
||||
|
||||
private List<AccountCategory> categories = new();
|
||||
private List<string> textDictionaryNames = new();
|
||||
private bool loading = true;
|
||||
private bool initializedFromConfig;
|
||||
|
||||
private IEnumerable<int> selectedCategoryIds = Array.Empty<int>();
|
||||
private string targetsText = "";
|
||||
private string dictionaryText = "";
|
||||
private string targetsText = string.Empty;
|
||||
private string dictionaryText = string.Empty;
|
||||
private int delayMinMs = 15;
|
||||
private int delayMaxMs = 45;
|
||||
private int maxMessages = 0;
|
||||
private int maxMessages;
|
||||
|
||||
private string accountMode = UserChatActiveTaskModes.Random;
|
||||
private string targetMode = UserChatActiveTaskModes.Queue;
|
||||
@@ -118,17 +131,19 @@
|
||||
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (!selectedCategoryIds.Any())
|
||||
{
|
||||
var firstCategoryId = categories.FirstOrDefault()?.Id ?? 0;
|
||||
selectedCategoryIds = firstCategoryId > 0 ? new[] { firstCategoryId } : Array.Empty<int>();
|
||||
}
|
||||
textDictionaryNames = (await DictionaryService.GetAllAsync())
|
||||
.Where(x => x.IsEnabled && string.Equals(x.Type, DataDictionaryTypes.Text, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(x => x.Items.Any(i => i.IsEnabled && !string.IsNullOrWhiteSpace(i.TextValue)))
|
||||
.Select(x => x.Name)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
await PushDraftAsync();
|
||||
}
|
||||
|
||||
await PushDraftAsync();
|
||||
}
|
||||
|
||||
private void ApplyInitialConfigIfNeeded()
|
||||
@@ -148,10 +163,7 @@
|
||||
if (cfg == null)
|
||||
return;
|
||||
|
||||
var categoryIds = NormalizeCategoryIds(cfg.CategoryIds, cfg.CategoryId);
|
||||
if (categoryIds.Count > 0)
|
||||
selectedCategoryIds = categoryIds;
|
||||
|
||||
selectedCategoryIds = NormalizeCategoryIds(cfg.CategoryIds, cfg.CategoryId);
|
||||
targetsText = string.Join(Environment.NewLine, cfg.Targets ?? new List<string>());
|
||||
dictionaryText = string.Join(Environment.NewLine, cfg.Dictionary ?? new List<string>());
|
||||
delayMinMs = ConvertMillisecondsToSeconds(cfg.DelayMinMs);
|
||||
@@ -215,6 +227,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var template in dictionary)
|
||||
{
|
||||
try
|
||||
{
|
||||
await TemplateRendering.ValidateTextTemplateAsync(template);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DraftChanged.InvokeAsync(new ModuleTaskDraft(0, null, false, $"词典消息模板无效:{ex.Message}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (delayMinMs < 0 || delayMaxMs < 0)
|
||||
{
|
||||
await DraftChanged.InvokeAsync(new ModuleTaskDraft(0, null, false, "间隔不能为负数"));
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler
|
||||
var taskManagement = host.Services.GetRequiredService<BatchTaskManagementService>();
|
||||
var accountManagement = host.Services.GetRequiredService<AccountManagementService>();
|
||||
var accountTools = host.Services.GetRequiredService<AccountTelegramToolsService>();
|
||||
var templateRendering = host.Services.GetRequiredService<TemplateRenderingService>();
|
||||
|
||||
var config = DeserializeConfig(host.Config);
|
||||
ValidateAndNormalizeConfig(config);
|
||||
@@ -116,7 +117,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler
|
||||
var targetSlot = accountSlot.Targets[targetIdx];
|
||||
|
||||
var messageIdx = SelectIndex(config.MessageMode, config.Dictionary.Count, ref messageQueueIndex);
|
||||
var text = config.Dictionary[messageIdx];
|
||||
var textTemplate = config.Dictionary[messageIdx];
|
||||
|
||||
if (!await host.IsStillRunningAsync(cancellationToken))
|
||||
{
|
||||
@@ -124,6 +125,65 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler
|
||||
break;
|
||||
}
|
||||
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = (await templateRendering.RenderTextTemplateAsync(textTemplate, cancellationToken)).Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completed++;
|
||||
failed++;
|
||||
var hadTemplateFailure = true;
|
||||
AddFailure(config, accountSlot.Account, targetSlot.RawTarget, $"词典模板解析失败:{ex.Message}");
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config));
|
||||
|
||||
if (ShouldPersistProgress(completed, hadTemplateFailure, lastProgressPersistAt))
|
||||
{
|
||||
await host.UpdateProgressAsync(completed, failed, cancellationToken);
|
||||
lastProgressPersistAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (config.MaxMessages > 0 && completed >= config.MaxMessages)
|
||||
break;
|
||||
|
||||
var templateFailDelayMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs);
|
||||
if (templateFailDelayMs > 0 && !await DelayWithPauseCheckAsync(host, templateFailDelayMs, cancellationToken))
|
||||
{
|
||||
config.Canceled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text.Length == 0)
|
||||
{
|
||||
completed++;
|
||||
failed++;
|
||||
var hadEmptyMessageFailure = true;
|
||||
AddFailure(config, accountSlot.Account, targetSlot.RawTarget, "词典模板解析结果为空,无法发送");
|
||||
await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config));
|
||||
|
||||
if (ShouldPersistProgress(completed, hadEmptyMessageFailure, lastProgressPersistAt))
|
||||
{
|
||||
await host.UpdateProgressAsync(completed, failed, cancellationToken);
|
||||
lastProgressPersistAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (config.MaxMessages > 0 && completed >= config.MaxMessages)
|
||||
break;
|
||||
|
||||
var emptyDelayMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs);
|
||||
if (emptyDelayMs > 0 && !await DelayWithPauseCheckAsync(host, emptyDelayMs, cancellationToken))
|
||||
{
|
||||
config.Canceled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var send = await accountTools.SendMessageToResolvedChatAsync(
|
||||
accountSlot.Account.Id,
|
||||
targetSlot.Resolved,
|
||||
|
||||
Reference in new issue
Block a user