feat: 优化昵称批量模板并支持活跃任务字典变量

This commit is contained in:
meoacgx committed 2026-03-10 02:13:14 +08:00
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,