修复:手机号登录验证码发送失败与 session 占用

- 增加 ApiHash 32 位十六进制校验\n- 登录失败/未知状态自动释放临时 Telegram client,避免 session 文件被占用\n- 优化系统设置提示与 README 说明
This commit is contained in:
meoacgx committed 2026-01-12 11:52:56 +08:00
1 parent e60b5fec4d
commit eb9abc71be
5 files changed
+171 -27

No files matched your search

+1
View File
@@ -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/
@@ -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<LoginResult> 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<LoginResult> ResendCodeAsync(int accountId)
@@ -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;
}
}
@@ -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
{
@@ -14,6 +14,7 @@
@inject NavigationManager Navigation
@inject CloudMailClient CloudMail
@using TelegramPanel.Web.Services
@using TelegramPanel.Core.Utils
<PageTitle>系统设置 - Telegram Panel</PageTitle>
@@ -29,7 +30,7 @@
<MudTextField @bind-Value="apiId" Label="默认 API ID" Variant="Variant.Outlined"
HelperText="从 https://my.telegram.org 获取" />
<MudTextField @bind-Value="apiHash" Label="默认 API Hash" Variant="Variant.Outlined" Class="mt-3"
HelperText="用于新账号登录和导入" />
HelperText="用于新账号登录和导入(32 位十六进制字符串)" />
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true" Class="mt-3">
<MudText Typo="Typo.body2"><strong>写入位置:</strong>@LocalConfigPath</MudText>
<MudText Typo="Typo.body2" Class="mt-1"><strong>文件存在:</strong>@(LocalConfigExists ? "是" : "否")</MudText>
@@ -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