1.1.0:二级密码存储/导入优化;版本信息弹窗改进

This commit is contained in:
meoacgx committed 2025-12-26 13:54:29 +08:00
1 parent 4875aefdd7
commit ab0df75600
25 files changed
+498 -43

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
<Project>
<PropertyGroup>
<!-- 集中管理项目版本号 -->
<!-- 版本格式: 主版本.次版本.修订号 -->
<!-- 主版本: 重大变更/不兼容更新 -->
<!-- 次版本: 新功能/兼容性更新 -->
<!-- 修订号: Bug修复/小改进 -->
<Version>1.1.0</Version>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0</InformationalVersion>
<!-- 项目元信息 -->
<Authors>Telegram Panel Contributors</Authors>
<Company>Telegram Panel</Company>
<Product>Telegram Panel</Product>
<Copyright>Copyright (c) 2024-2025 Telegram Panel Contributors</Copyright>
<Description>Telegram Multi-Account Management Panel</Description>
<RepositoryUrl>https://github.com/moeacgx/Telegram-Panel</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
</Project>
@@ -234,7 +234,9 @@ public class AccountManagementService
if (account == null)
return;
account.Phone = PhoneNumberFormatter.FormatWithCountryCode(account.Phone);
// 只设置 DisplayPhone(非持久化字段),不修改 Phone(持久化字段)
// 这样可以避免 EF Core 追踪 Phone 的变化导致 UNIQUE 约束冲突
account.DisplayPhone = PhoneNumberFormatter.FormatWithCountryCode(account.Phone);
}
private static string? TryFindRepoRoot()
@@ -214,7 +214,8 @@ public class AccountImportService
/// </summary>
public async Task<List<ImportResult>> ImportFromZipAsync(
IBrowserFile zipFile,
int? categoryId = null)
int? categoryId = null,
string? twoFactorPassword = null)
{
var results = new List<ImportResult>();
@@ -251,7 +252,7 @@ public class AccountImportService
var importedPhones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var jsonPath in jsonFiles)
{
var result = await ImportFromPackageEntryAsync(jsonPath, categoryId);
var result = await ImportFromPackageEntryAsync(jsonPath, categoryId, twoFactorPassword);
if (result.Phone != null && !importedPhones.Add(result.Phone))
{
results.Add(new ImportResult(false, result.Phone, result.UserId, result.Username, result.SessionPath, "重复账号已跳过"));
@@ -331,7 +332,7 @@ public class AccountImportService
}
}
private async Task<ImportResult> ImportFromPackageEntryAsync(string jsonPath, int? categoryId)
private async Task<ImportResult> ImportFromPackageEntryAsync(string jsonPath, int? categoryId, string? twoFactorPassword)
{
try
{
@@ -375,6 +376,10 @@ public class AccountImportService
return new ImportResult(false, phone, userId, username, null, "未找到对应的 .session 文件");
}
// 尝试从 2fa.txt 读取二级密码(优先于用户输入的统一密码)
var twoFaFromFile = await TryRead2faFileAsync(dir);
var effectiveTwoFactorPassword = !string.IsNullOrWhiteSpace(twoFaFromFile) ? twoFaFromFile : twoFactorPassword;
var sessionsPath = _configuration["Telegram:SessionsPath"] ?? "sessions";
Directory.CreateDirectory(sessionsPath);
var targetSessionPath = Path.Combine(sessionsPath, $"{phone}.session");
@@ -432,6 +437,9 @@ public class AccountImportService
existing.ApiHash = apiHash.Trim();
existing.IsActive = true;
existing.LastSyncAt = DateTime.UtcNow;
// 仅在提供了二级密码时更新,避免覆盖已有密码
if (!string.IsNullOrWhiteSpace(effectiveTwoFactorPassword))
existing.TwoFactorPassword = effectiveTwoFactorPassword.Trim();
await _accountManagement.UpdateAccountAsync(existing);
}
else
@@ -447,6 +455,7 @@ public class AccountImportService
ApiHash = apiHash.Trim(),
IsActive = true,
CategoryId = categoryId,
TwoFactorPassword = string.IsNullOrWhiteSpace(effectiveTwoFactorPassword) ? null : effectiveTwoFactorPassword.Trim(),
CreatedAt = DateTime.UtcNow,
LastSyncAt = DateTime.UtcNow
};
@@ -465,6 +474,35 @@ public class AccountImportService
string extractDirFallback() => Path.GetTempPath();
}
/// <summary>
/// 尝试从目录中读取 2fa.txt 文件内容作为二级密码
/// </summary>
private static async Task<string?> TryRead2faFileAsync(string directory)
{
try
{
// 支持多种常见文件名
var possibleNames = new[] { "2fa.txt", "2FA.txt", "2fa", "2FA", "twofa.txt", "password.txt" };
foreach (var name in possibleNames)
{
var filePath = Path.Combine(directory, name);
if (!File.Exists(filePath))
continue;
var content = await File.ReadAllTextAsync(filePath);
var password = content?.Trim();
if (!string.IsNullOrWhiteSpace(password))
return password;
}
}
catch
{
// 读取失败不影响导入流程
}
return null;
}
private static string? BuildNickname(string? firstName, string? lastName, string? username)
{
firstName = string.IsNullOrWhiteSpace(firstName) ? null : firstName.Trim();
@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace TelegramPanel.Data.Entities;
/// <summary>
@@ -7,6 +9,20 @@ public class Account
{
public int Id { get; set; }
public string Phone { get; set; } = null!;
private string? _displayPhone;
/// <summary>
/// 格式化后的手机号(用于 UI 展示,不持久化),格式如:+86 13800138000
/// 如果未通过 AccountManagementService 设置,则回退到 Phone 的值
/// </summary>
[NotMapped]
public string DisplayPhone
{
get => _displayPhone ?? Phone;
set => _displayPhone = value;
}
public long UserId { get; set; }
/// <summary>
/// 账号昵称(Telegram 显示名称)
@@ -46,6 +62,11 @@ public class Account
/// </summary>
public DateTime? TelegramStatusCheckedAtUtc { get; set; }
/// <summary>
/// 二级密码(两步验证密码),用于系统保存以便后续修改
/// </summary>
public string? TwoFactorPassword { get; set; }
// 导航属性
public AccountCategory? Category { get; set; }
public ICollection<Channel> Channels { get; set; } = new List<Channel>();
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TelegramPanel.Data.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251226000000_AddTwoFactorPassword")]
/// <inheritdoc />
public partial class AddTwoFactorPassword : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TwoFactorPassword",
table: "Accounts",
type: "TEXT",
maxLength: 200,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TwoFactorPassword",
table: "Accounts");
}
}
}
@@ -72,6 +72,10 @@ namespace TelegramPanel.Data.Migrations
b.Property<string>("TelegramStatusSummary")
.HasColumnType("TEXT");
b.Property<string>("TwoFactorPassword")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<long>("UserId")
.HasColumnType("INTEGER");
@@ -0,0 +1,132 @@
@inject AccountManagementService AccountManagement
@inject PanelTimeZoneService TimeZone
@inject ISnackbar Snackbar
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
账号详情
</MudText>
</TitleContent>
<DialogContent>
@if (account == null)
{
<MudProgressCircular Indeterminate="true" />
}
else
{
<MudStack Spacing="3">
<MudTextField Value="@FormatCreatedAt()" Label="创建时间" ReadOnly="true" Variant="Variant.Outlined" />
<MudTextField Value="@account.SessionPath" Label="Session 路径" ReadOnly="true" Variant="Variant.Outlined" />
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle1" Class="mb-1">二级密码</MudText>
@if (isEditingPassword)
{
<MudTextField @bind-Value="newPassword" Label="新密码" Variant="Variant.Outlined"
InputType="@(showPassword ? InputType.Text : InputType.Password)"
Adornment="Adornment.End"
AdornmentIcon="@(showPassword ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
OnAdornmentClick="() => showPassword = !showPassword" />
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="CancelEdit">取消</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePassword" Disabled="@saving">
@if (saving)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
保存
</MudButton>
</MudStack>
}
else
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudTextField Value="@DisplayPassword()" Label="当前保存的密码" ReadOnly="true" Variant="Variant.Outlined"
InputType="@(showPassword ? InputType.Text : InputType.Password)"
Adornment="Adornment.End"
AdornmentIcon="@(showPassword ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
OnAdornmentClick="() => showPassword = !showPassword"
Style="flex: 1;" />
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" OnClick="StartEdit" Title="修改" />
</MudStack>
}
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close">关闭</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public int AccountId { get; set; }
private TelegramPanel.Data.Entities.Account? account;
private bool isEditingPassword;
private string newPassword = "";
private bool showPassword;
private bool saving;
protected override async Task OnInitializedAsync()
{
account = await AccountManagement.GetAccountAsync(AccountId);
}
private string FormatCreatedAt()
{
return account == null ? "-" : TimeZone.Format(account.CreatedAt, "yyyy-MM-dd HH:mm");
}
private string DisplayPassword()
{
if (account == null)
return "-";
return string.IsNullOrEmpty(account.TwoFactorPassword) ? "(未保存)" : account.TwoFactorPassword;
}
private void StartEdit()
{
newPassword = account?.TwoFactorPassword ?? "";
isEditingPassword = true;
}
private void CancelEdit()
{
isEditingPassword = false;
newPassword = "";
}
private async Task SavePassword()
{
if (account == null)
return;
saving = true;
try
{
account.TwoFactorPassword = string.IsNullOrWhiteSpace(newPassword) ? null : newPassword.Trim();
await AccountManagement.UpdateAccountAsync(account);
isEditingPassword = false;
Snackbar.Add("二级密码已保存", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
}
finally
{
saving = false;
}
}
private void Close() => MudDialog.Close();
}
@@ -160,7 +160,7 @@
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, ok, err));
results.Add(new ResultRow(a.DisplayPhone, ok, err));
StateHasChanged();
}
@@ -197,7 +197,7 @@
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, newNickname, ok, err));
results.Add(new ResultRow(a.DisplayPhone, newNickname, ok, err));
if (ok)
{
@@ -188,7 +188,7 @@
{
processed++;
failed++;
results.Add(new ResultRow(a.Phone, "", false, genErr ?? "生成用户名失败"));
results.Add(new ResultRow(a.DisplayPhone, "", false, genErr ?? "生成用户名失败"));
StateHasChanged();
continue;
}
@@ -200,7 +200,7 @@
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, newUsername, ok, err));
results.Add(new ResultRow(a.DisplayPhone, newUsername, ok, err));
if (ok)
{
@@ -150,7 +150,7 @@
{
_cts.Token.ThrowIfCancellationRequested();
currentHint = $"{a.Phone} => {link}";
currentHint = $"{a.DisplayPhone} => {link}";
StateHasChanged();
(bool Success, string? Error, string? Title) result;
@@ -163,7 +163,7 @@
if (!result.Success)
{
failed++;
failures.Add($"{a.Phone} => {link}:{result.Error}");
failures.Add($"{a.DisplayPhone} => {link}:{result.Error}");
}
StateHasChanged();
@@ -8,17 +8,43 @@
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
将对选中的账号批量修改 Telegram 两步验证(二级密码)。请确认你输入的“原二级密码”在这些账号上是一致的,否则对应账号会失败。
</MudAlert>
@if (IsBatchMode)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
将对选中的账号批量修改 Telegram 两步验证(二级密码)。
@if (!useStoredPasswords)
{
<span>请确认你输入的"原二级密码"在这些账号上是一致的,否则对应账号会失败。</span>
}
</MudAlert>
}
<MudText Typo="Typo.subtitle2">账号数量:@accounts.Count</MudText>
<MudTextField @bind-Value="currentPassword" Label="原二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
HelperText="如果账号未开启两步验证,可留空" />
@if (IsBatchMode)
{
<MudSwitch @bind-Value="useStoredPasswords" Label="使用数据库中保存的原二级密码" Color="Color.Primary" />
@if (useStoredPasswords)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
将为每个账号使用其在数据库中保存的二级密码作为原密码。若某账号未保存密码,则以空密码尝试。
</MudAlert>
}
else
{
<MudTextField @bind-Value="currentPassword" Label="原二级密码(统一)" Variant="Variant.Outlined" InputType="InputType.Password"
HelperText="如果账号未开启两步验证,可留空" />
}
}
else
{
<MudTextField @bind-Value="currentPassword" Label="原二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
HelperText="@SingleAccountPasswordHelperText" />
}
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
忘记原二级密码时,可发起“重置二级密码”申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。
忘记原二级密码时,可发起"重置二级密码"申请(通常需要等待 7 天)。等待结束后,再回来设置新的二级密码即可。
</MudAlert>
<MudTextField @bind-Value="newPassword" Label="新二级密码" Variant="Variant.Outlined" InputType="InputType.Password"
@@ -30,6 +56,8 @@
<MudTextField @bind-Value="hint" Label="密码提示(可选)" Variant="Variant.Outlined"
HelperText="Telegram 会展示给你作为提示,建议不要写明文密码" />
<MudCheckBox @bind-Value="saveNewPasswordToDb" Label="修改成功后将新密码保存到数据库" Color="Color.Primary" />
@if (running || results.Count > 0)
{
<MudDivider Class="my-2" />
@@ -93,6 +121,8 @@
private string newPassword = "";
private string confirmPassword = "";
private string hint = "";
private bool useStoredPasswords;
private bool saveNewPasswordToDb = true;
private bool running;
private int processed;
@@ -101,6 +131,23 @@
private readonly List<ResultRow> results = new();
private bool IsBatchMode => accounts.Count > 1;
private string SingleAccountPasswordHelperText
{
get
{
if (accounts.Count != 1)
return "如果账号未开启两步验证,可留空";
var stored = accounts[0].TwoFactorPassword;
if (string.IsNullOrEmpty(stored))
return "如果账号未开启两步验证,可留空(数据库中未保存密码)";
return "已自动填充数据库中保存的密码";
}
}
protected override async Task OnInitializedAsync()
{
var ids = AccountIds?.Distinct().Where(x => x > 0).ToList() ?? new List<int>();
@@ -109,6 +156,12 @@
var all = await AccountManagement.GetAllAccountsAsync();
accounts = all.Where(a => ids.Contains(a.Id)).OrderBy(a => a.Id).ToList();
// 单个账号时,自动填充数据库中保存的原二级密码
if (accounts.Count == 1 && !string.IsNullOrEmpty(accounts[0].TwoFactorPassword))
{
currentPassword = accounts[0].TwoFactorPassword;
}
}
private async Task Start()
@@ -152,16 +205,43 @@
{
foreach (var a in accounts)
{
// 确定使用的原密码:批量模式下可选择使用数据库中保存的密码
var effectiveCurrentPassword = (IsBatchMode && useStoredPasswords)
? (a.TwoFactorPassword ?? "")
: currentPassword;
var (ok, err) = await AccountTelegramTools.ChangeTwoFactorPasswordAsync(
accountId: a.Id,
currentPassword: currentPassword,
currentPassword: effectiveCurrentPassword,
newPassword: newPassword,
hint: hint,
cancellationToken: CancellationToken.None);
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, ok, ok ? null : err));
if (ok)
{
success++;
// 修改成功后,将新密码保存到数据库
if (saveNewPasswordToDb)
{
try
{
a.TwoFactorPassword = newPassword;
await AccountManagement.UpdateAccountAsync(a);
}
catch
{
// 保存失败不影响主流程
}
}
}
else
{
failed++;
}
results.Add(new ResultRow(a.DisplayPhone, ok, ok ? null : err));
StateHasChanged();
}
@@ -190,7 +270,7 @@
bool? confirm = await DialogService.ShowMessageBox(
"确认申请重置",
$"将对 {accounts.Count} 个账号向 Telegram 发起“重置二级密码”申请(通常需要等待 7 天)。是否继续?",
$"将对 {accounts.Count} 个账号向 Telegram 发起【重置二级密码】申请(通常需要等待 7 天)。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
@@ -212,7 +292,7 @@
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, ok, msg));
results.Add(new ResultRow(a.DisplayPhone, ok, msg));
StateHasChanged();
}
@@ -22,7 +22,7 @@
}
else
{
<MudText Typo="Typo.subtitle2">账号:@account.Phone</MudText>
<MudText Typo="Typo.subtitle2">账号:@account.DisplayPhone</MudText>
<MudStack direction="Row" Spacing="2" AlignItems="AlignItems.Center">
<MudChip T="string" Size="Size.Small" Color="@(hasTwoFactorPassword ? Color.Success : Color.Warning)">
@@ -155,7 +155,7 @@
var creator = await AccountManagement.GetAccountAsync(channel.CreatorAccountId.Value);
creatorDisplay = creator == null
? $"账号 {channel.CreatorAccountId.Value}"
: $"{creator.Phone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
: $"{creator.DisplayPhone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
}
else
{
@@ -24,7 +24,7 @@
可修改:昵称、Bio、用户名、头像。用户名暂时仅支持单账号设置。
</MudAlert>
<MudText Typo="Typo.subtitle2">账号:@account.Phone</MudText>
<MudText Typo="Typo.subtitle2">账号:@account.DisplayPhone</MudText>
<MudSwitch @bind-Value="editNickname" Label="修改昵称" Color="Color.Primary" />
<MudTextField @bind-Value="nickname" Label="昵称" Variant="Variant.Outlined" Disabled="@(!editNickname)"
@@ -125,7 +125,7 @@
var creator = await AccountManagement.GetAccountAsync(group.CreatorAccountId);
creatorDisplay = creator == null
? $"账号 {group.CreatorAccountId}"
: $"{creator.Phone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
: $"{creator.DisplayPhone}({creator.Nickname ?? creator.Username ?? creator.UserId.ToString()})";
}
catch (Exception ex)
{
@@ -25,7 +25,7 @@
else
{
<MudText Typo="Typo.subtitle2">
执行账号:@account.Phone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})")
执行账号:@account.DisplayPhone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})")
</MudText>
}
@@ -103,7 +103,7 @@
bool? confirm = await DialogService.ShowMessageBox(
"确认执行",
$"将使用账号 {account.Phone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?",
$"将使用账号 {account.DisplayPhone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
@@ -48,7 +48,7 @@
@foreach (var account in RiskyAccounts)
{
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Class="ma-1">
@account.Phone
@account.DisplayPhone
@if (account.LastLoginAt != null)
{
<text> (登录 @account.GetLoginHoursFormatted() 小时)</text>
@@ -0,0 +1,34 @@
@namespace TelegramPanel.Web.Components.Dialogs
@using TelegramPanel.Web.Services
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">Telegram Panel</MudText>
<MudText Typo="Typo.body2">
<strong>版本:</strong>v@VersionService.Version
</MudText>
<MudText Typo="Typo.body2" Style="overflow-wrap: anywhere; word-break: break-word; user-select: text;">
<strong>完整版本:</strong>@VersionService.FullVersion
</MudText>
<MudLink Href="https://github.com/moeacgx/Telegram-Panel"
Target="_blank"
Rel="noopener noreferrer">
GitHub 仓库
</MudLink>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Close">关闭</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
private void Close()
{
MudDialog.Close();
}
}
@@ -9,6 +9,10 @@
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">Telegram Panel</MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Default" Class="ml-2" Style="cursor: pointer;"
title="点击查看版本信息" OnClick="@ShowVersionInfo">
v@(VersionService.Version)
</MudChip>
<MudSpacer />
<MudIconButton Icon="@Icons.Material.Filled.Settings"
Color="Color.Inherit"
@@ -38,6 +42,8 @@
@using TelegramPanel.Web.Services
@inject UiPreferencesService UiPreferences
@inject NavigationManager Navigation
@inject IDialogService DialogService
@code {
private bool _drawerOpen = true;
@@ -82,4 +88,17 @@
// 强制刷新组件状态,确保在异步操作后 UI 能及时更新
StateHasChanged();
}
private async Task ShowVersionInfo()
{
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
DialogService.Show<TelegramPanel.Web.Components.Dialogs.VersionInfoDialog>("版本信息", new DialogParameters(), options);
await Task.CompletedTask;
}
}
@@ -20,12 +20,14 @@
<ul style="margin: 6px 0 0 18px;">
<li>单账号:Zip 根目录直接包含一个 <strong>.json</strong> + 一个 <strong>.session</strong></li>
<li>批量导入:每个账号一个独立子文件夹,文件夹内包含一个 <strong>.json</strong> + 一个 <strong>.session</strong></li>
<li><strong>二级密码</strong>:自动解析账号目录下的 <strong>2fa.txt</strong> 文件作为二级密码保存到数据库</li>
</ul>
<div style="margin-top: 6px;">批量结构示例:</div>
<pre style="margin: 4px 0 0; white-space: pre; overflow: auto;">xx.zip
├─ 8613111111111
│ ├─ 8613111111111.json
│ └─ 8613111111111.session
│ ├─ 8613111111111.session
│ └─ 2fa.txt (可选,内容为二级密码)
└─ 8615119714541
├─ 8615119714541.json
└─ 8615119714541.session</pre>
@@ -45,6 +47,10 @@
<MudText Typo="Typo.body2" Class="mt-3">已选择:@zipFile.Name(@zipFile.Size bytes)</MudText>
}
<MudTextField @bind-Value="zipTwoFactorPassword" Label="二级密码(可选)" Variant="Variant.Outlined"
InputType="InputType.Password" Class="mt-3"
HelperText="统一密码,用于没有 2fa.txt 的账号;若账号目录有 2fa.txt 则优先使用文件内容" />
<MudButton Variant="Variant.Filled" Color="Color.Success" FullWidth="true"
Disabled="@(zipFile == null || importingZip)" Class="mt-4"
OnClick="@(async (MouseEventArgs _) => await ImportZip())">
@@ -168,6 +174,7 @@
private IBrowserFile? zipFile;
private bool importingZip = false;
private string zipTwoFactorPassword = "";
private string sessionString = "";
private bool importingString = false;
@@ -205,7 +212,8 @@
try
{
var results = await AccountImportService.ImportFromZipAsync(zipFile);
var twoFactorPwd = string.IsNullOrWhiteSpace(zipTwoFactorPassword) ? null : zipTwoFactorPassword.Trim();
var results = await AccountImportService.ImportFromZipAsync(zipFile, twoFactorPassword: twoFactorPwd);
importResults.AddRange(results);
var successCount = results.Count(r => r.Success);
@@ -217,6 +225,7 @@
Snackbar.Add($"{failureCount} 个账号导入失败", Severity.Warning);
zipFile = null;
zipTwoFactorPassword = "";
}
catch (Exception ex)
{
@@ -88,7 +88,7 @@
<MudTh>操作</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="手机号">@context.Phone</MudTd>
<MudTd DataLabel="手机号">@context.DisplayPhone</MudTd>
<MudTd DataLabel="昵称">@(context.Nickname ?? "-")</MudTd>
<MudTd DataLabel="用户名">@(context.Username ?? "-")</MudTd>
<MudTd DataLabel="用户ID">@context.UserId</MudTd>
@@ -248,11 +248,15 @@
private async Task ShowDetails(Account account)
{
var (channels, groups) = await AccountManagement.GetAccountStatisticsAsync(account.Id);
var createdAt = TimeZone.Format(account.CreatedAt, "yyyy-MM-dd HH:mm");
var lastSyncAt = TimeZone.Format(account.LastSyncAt, "yyyy-MM-dd HH:mm");
var message = $"账号详情:\n手机号:{account.Phone}\n用户名:{account.Username ?? "无"}\n用户ID:{account.UserId}\n分类:{account.Category?.Name ?? "未分类"}\n创建的频道:{channels} 个\n创建的群组:{groups} 个\n创建时间:{createdAt}\n最后数据同步:{lastSyncAt}\nSession路径:{account.SessionPath}";
await DialogService.ShowMessageBox("账号详情", message, "关闭");
var parameters = new DialogParameters
{
["AccountId"] = account.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.AccountDetailsDialog>("账号详情", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await LoadAccounts();
}
private async Task OpenChangeTwoFactorPassword(Account account)
@@ -569,7 +573,7 @@
// 资料刷新后重新加载(昵称/用户名/用户ID 可能更新)
await LoadAccounts();
Snackbar.Add($"账号 {account.Phone} 状态:{result.Summary}", result.Ok ? Severity.Success : Severity.Warning);
Snackbar.Add($"账号 {account.DisplayPhone} 状态:{result.Summary}", result.Ok ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
{
@@ -857,7 +861,7 @@
var parameters = new DialogParameters
{
["AccountId"] = account.Id,
["Phone"] = account.Phone
["Phone"] = account.DisplayPhone
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
@@ -870,7 +874,7 @@
var parameters = new DialogParameters
{
["AccountId"] = account.Id,
["Phone"] = account.Phone
["Phone"] = account.DisplayPhone
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
@@ -900,7 +904,7 @@
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除账号 {account.Phone} 吗?此操作不可撤销!",
$"确定要删除账号 {account.DisplayPhone} 吗?此操作不可撤销!",
yesText: "删除", cancelText: "取消");
if (result == true)
@@ -115,7 +115,7 @@
<MudTd DataLabel="创建账号">
@if (context.CreatorAccount != null)
{
<span>@context.CreatorAccount.Phone</span>
<span>@context.CreatorAccount.DisplayPhone</span>
}
else if (context.CreatorAccountId != null)
{
@@ -23,7 +23,7 @@
<MudSelectItem Value="0">全部账号</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@account.Phone (@account.Username)</MudSelectItem>
<MudSelectItem Value="@account.Id">@account.DisplayPhone (@account.Username)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="searchString" Placeholder="搜索群组..." Adornment="Adornment.Start"
@@ -64,7 +64,7 @@
<MudTd DataLabel="创建账号">
@if (context.CreatorAccount != null)
{
<span>@context.CreatorAccount.Phone</span>
<span>@context.CreatorAccount.DisplayPhone</span>
}
else
{
@@ -0,0 +1,58 @@
using System.Reflection;
namespace TelegramPanel.Web.Services;
/// <summary>
/// 版本信息服务(静态类),用于获取应用程序版本号
/// </summary>
public static class VersionService
{
private static readonly Lazy<string> _version = new(GetVersionFromAssembly);
private static readonly Lazy<string> _fullVersion = new(GetFullVersionFromAssembly);
/// <summary>
/// 获取简化版本号(如:1.0.0)
/// </summary>
public static string Version => _version.Value;
/// <summary>
/// 获取完整版本信息(包含 InformationalVersion,如:1.0.0+commitHash)
/// </summary>
public static string FullVersion => _fullVersion.Value;
private static string GetVersionFromAssembly()
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
// 优先使用 InformationalVersion(更友好的版本号)
var infoVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
if (!string.IsNullOrWhiteSpace(infoVersion))
{
// 移除可能的 +commitHash 后缀,只保留版本号部分
var plusIndex = infoVersion.IndexOf('+');
return plusIndex > 0 ? infoVersion[..plusIndex] : infoVersion;
}
// 回退到 AssemblyVersion
var version = assembly.GetName().Version;
return version != null
? $"{version.Major}.{version.Minor}.{version.Build}"
: "Unknown";
}
private static string GetFullVersionFromAssembly()
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
var infoVersion = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
return !string.IsNullOrWhiteSpace(infoVersion)
? infoVersion
: GetVersionFromAssembly();
}
}