diff --git a/README.md b/README.md index bd1e896..8f0b019 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ docker compose up -d --build 到 https://my.telegram.org/apps **用任意一个 Telegram 账号申请一次** `api_id` / `api_hash`,然后在面板「系统设置」里保存即可。 > 说明:**不需要每个账号都申请**,全站共用这一对 `api_id` / `api_hash` 就能工作。 +> 提示:`api_hash` 是 **32 位十六进制字符串(0-9a-f)**,请不要填 Token/用户名/URL 等其它值。 🏪 推荐 Telegram 开发者 API 购买: https://faka.boxmoe.eu.org/ diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs index 59af436..de54d46 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration; using TelegramPanel.Core.Interfaces; using TelegramPanel.Core.Models; +using TelegramPanel.Core.Utils; using AccountStatus = TelegramPanel.Core.Interfaces.AccountStatus; namespace TelegramPanel.Core.Services.Telegram; @@ -27,13 +28,16 @@ public class AccountService : IAccountService public async Task StartLoginAsync(int accountId, string phone) { - if (!int.TryParse(_configuration["Telegram:ApiId"], out var apiId) || apiId <= 0 - || string.IsNullOrWhiteSpace(_configuration["Telegram:ApiHash"])) + if (!int.TryParse(_configuration["Telegram:ApiId"], out var apiId) || apiId <= 0) { return new LoginResult(false, null, "请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)"); } - var apiHash = _configuration["Telegram:ApiHash"]!.Trim(); + if (!TelegramApiConfigValidator.TryNormalizeApiHash(_configuration["Telegram:ApiHash"], out var apiHash, out var apiHashReason)) + { + return new LoginResult(false, null, $"全局 Telegram API 配置无效:{apiHashReason}"); + } + var sessionsPath = _configuration["Telegram:SessionsPath"] ?? "sessions"; Directory.CreateDirectory(sessionsPath); var normalizedPhone = NormalizePhoneForLogin(phone); @@ -45,35 +49,102 @@ public class AccountService : IAccountService _logger.LogInformation("Starting login for phone {Phone}", normalizedPhone); - var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, sessionKey: apiHash, phoneNumber: normalizedPhone, userId: null); + WTelegram.Client? client = null; - string result; try { - result = await client.Login(normalizedPhone); - } - catch (Exception ex) when (LooksLikeSessionApiMismatchOrCorrupted(ex)) - { - // session 与 ApiId/ApiHash 不匹配或损坏,备份后重新开始登录流程 - TryBackupCorruptedSessionIfExists(sessionPath); - await _clientPool.RemoveClientAsync(accountId); + client = await _clientPool.GetOrCreateClientAsync( + accountId, + apiId, + apiHash, + sessionPath, + sessionKey: apiHash, + phoneNumber: normalizedPhone, + userId: null); - client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, sessionKey: apiHash, phoneNumber: normalizedPhone, userId: null); - result = await client.Login(normalizedPhone); + string result; + try + { + result = await client.Login(normalizedPhone); + } + catch (Exception ex) when (LooksLikeSessionApiMismatchOrCorrupted(ex)) + { + // session 与 ApiId/ApiHash 不匹配或损坏,备份后重新开始登录流程 + TryBackupCorruptedSessionIfExists(sessionPath); + await _clientPool.RemoveClientAsync(accountId); + + client = await _clientPool.GetOrCreateClientAsync( + accountId, + apiId, + apiHash, + sessionPath, + sessionKey: apiHash, + phoneNumber: normalizedPhone, + userId: null); + + result = await client.Login(normalizedPhone); + } + + _logger.LogInformation("Login flow next step for {Phone}: {Step}", normalizedPhone, result); + + var loginResult = result switch + { + "verification_code" => new LoginResult(false, "code", "请输入验证码"), + "password" => new LoginResult(false, "password", "请输入两步验证密码"), + "name" => new LoginResult(false, "signup", "需要注册新账号"), + "email" => new LoginResult(false, "email", "该账号需要邮箱验证(请按提示填写邮箱并完成验证)"), + "email_verification_code" => new LoginResult(false, "email_code", "请输入邮箱验证码"), + _ when client.User != null => new LoginResult(true, null, "登录成功", MapToAccountInfo(accountId, client)), + _ => new LoginResult(false, null, $"未知状态: {result}") + }; + + if (!loginResult.Success && string.IsNullOrWhiteSpace(loginResult.NextStep)) + { + try { await _clientPool.RemoveClientAsync(accountId); } catch { } + } + + return loginResult; + } + catch (Exception ex) + { + try + { + await _clientPool.RemoveClientAsync(accountId); + } + catch + { + } + + var hint = BuildFriendlyStartLoginError(ex); + _logger.LogWarning(ex, "StartLogin failed for phone {Phone} (accountId={AccountId}): {Hint}", normalizedPhone, accountId, hint); + return new LoginResult(false, null, hint); + } + } + + private static string BuildFriendlyStartLoginError(Exception ex) + { + var msg = ex.Message ?? string.Empty; + + if (ex is FormatException + || msg.Contains("hex", StringComparison.OrdinalIgnoreCase) + || msg.Contains("hexadecimal", StringComparison.OrdinalIgnoreCase) + || msg.Contains("十六进制", StringComparison.OrdinalIgnoreCase) + || msg.Contains("16进制", StringComparison.OrdinalIgnoreCase)) + { + return "发送验证码失败:检测到 ApiHash 格式异常。请到【系统设置】重新填写 Telegram ApiHash(my.telegram.org 获取的 32 位十六进制字符串)。"; } - _logger.LogInformation("Login flow next step for {Phone}: {Step}", normalizedPhone, result); - - return result switch + if (ex is IOException + || msg.Contains("being used by another process", StringComparison.OrdinalIgnoreCase) + || msg.Contains("used by another process", StringComparison.OrdinalIgnoreCase) + || msg.Contains("process cannot access", StringComparison.OrdinalIgnoreCase) + || msg.Contains("被另一进程使用", StringComparison.OrdinalIgnoreCase) + || msg.Contains("被另一个进程使用", StringComparison.OrdinalIgnoreCase)) { - "verification_code" => new LoginResult(false, "code", "请输入验证码"), - "password" => new LoginResult(false, "password", "请输入两步验证密码"), - "name" => new LoginResult(false, "signup", "需要注册新账号"), - "email" => new LoginResult(false, "email", "该账号需要邮箱验证(请按提示填写邮箱并完成验证)"), - "email_verification_code" => new LoginResult(false, "email_code", "请输入邮箱验证码"), - _ when client.User != null => new LoginResult(true, null, "登录成功", MapToAccountInfo(accountId, client)), - _ => new LoginResult(false, null, $"未知状态: {result}") - }; + return "发送验证码失败:session 文件被占用。请稍后重试;若你部署了多个实例共享同一 sessions 目录,请改为单实例或为每个实例使用独立 sessions 目录。"; + } + + return $"发送验证码失败:{ex.GetType().Name}: {ex.Message}"; } public async Task ResendCodeAsync(int accountId) diff --git a/src/TelegramPanel.Core/Utils/TelegramApiConfigValidator.cs b/src/TelegramPanel.Core/Utils/TelegramApiConfigValidator.cs new file mode 100644 index 0000000..496b2e7 --- /dev/null +++ b/src/TelegramPanel.Core/Utils/TelegramApiConfigValidator.cs @@ -0,0 +1,55 @@ +namespace TelegramPanel.Core.Utils; + +public static class TelegramApiConfigValidator +{ + public static bool TryValidateApiIdApiHash(int apiId, string? apiHash, out string? reason) + { + if (apiId <= 0) + { + reason = "ApiId 无效(必须为正整数)"; + return false; + } + + if (!TryNormalizeApiHash(apiHash, out _, out reason)) + return false; + + reason = null; + return true; + } + + public static bool TryNormalizeApiHash(string? apiHash, out string normalized, out string? reason) + { + normalized = (apiHash ?? string.Empty).Trim(); + + if (string.IsNullOrWhiteSpace(normalized)) + { + reason = "ApiHash 不能为空"; + return false; + } + + if (normalized.Length != 32) + { + reason = "ApiHash 格式错误:长度应为 32 位十六进制字符串(0-9a-f)"; + return false; + } + + for (var i = 0; i < normalized.Length; i++) + { + var ch = normalized[i]; + var isHex = + (ch >= '0' && ch <= '9') || + (ch >= 'a' && ch <= 'f') || + (ch >= 'A' && ch <= 'F'); + + if (!isHex) + { + reason = "ApiHash 格式错误:必须为十六进制字符串(0-9a-f)"; + return false; + } + } + + reason = null; + return true; + } +} + diff --git a/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor b/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor index 95d374c..4d72f12 100644 --- a/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor +++ b/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor @@ -233,6 +233,11 @@ } Snackbar.Add(result.Message ?? "发送验证码失败", Severity.Error); + if (accountId != 0) + { + try { await AccountService.ReleaseClientAsync(accountId); } catch { } + accountId = 0; + } return; } @@ -298,6 +303,11 @@ catch (Exception ex) { Snackbar.Add($"操作失败:{ex.Message}", Severity.Error); + if (currentStep == LoginStep.Phone && accountId != 0) + { + try { await AccountService.ReleaseClientAsync(accountId); } catch { } + accountId = 0; + } } finally { diff --git a/src/TelegramPanel.Web/Components/Pages/Settings.razor b/src/TelegramPanel.Web/Components/Pages/Settings.razor index 6ddd3bc..98d6ed9 100644 --- a/src/TelegramPanel.Web/Components/Pages/Settings.razor +++ b/src/TelegramPanel.Web/Components/Pages/Settings.razor @@ -14,6 +14,7 @@ @inject NavigationManager Navigation @inject CloudMailClient CloudMail @using TelegramPanel.Web.Services +@using TelegramPanel.Core.Utils 系统设置 - Telegram Panel @@ -29,7 +30,7 @@ + HelperText="用于新账号登录和导入(32 位十六进制字符串)" /> 写入位置:@LocalConfigPath 文件存在:@(LocalConfigExists ? "是" : "否") @@ -315,13 +316,19 @@ return; } + if (!TelegramApiConfigValidator.TryNormalizeApiHash(apiHash, out var normalizedApiHash, out var apiHashReason)) + { + Snackbar.Add($"默认 API Hash 无效:{apiHashReason}", Severity.Error); + return; + } + try { var localConfigPath = LocalConfigPath; var root = await LoadOrCreateLocalConfigAsync(); var telegram = EnsureObject(root, "Telegram"); telegram["ApiId"] = parsedApiId; - telegram["ApiHash"] = apiHash.Trim(); + telegram["ApiHash"] = normalizedApiHash; await SaveLocalConfigAsync(root); // 配置变更后清理客户端缓存,确保后续操作使用新 ApiId/ApiHash