diff --git a/.gitignore b/.gitignore
index 8b8b3d2..2252c96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ obj/
# Serena
.serena/
+# Local temp
+.tmp/
+
# Logs
logs/
*.log
diff --git a/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs b/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs
index 4b9a083..193af86 100644
--- a/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs
+++ b/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs
@@ -11,7 +11,7 @@ public interface ITelegramClientPool
///
/// 获取或创建指定账号的客户端
///
- Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath);
+ Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath, string? phoneNumber = null, long? userId = null);
///
/// 获取已存在的客户端
diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
index 8270d5a..600f90e 100644
--- a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
@@ -348,12 +348,14 @@ public class AccountImportService
return new ImportResult(false, phone, userId, username, null, "该 .session 为 SQLite 格式且缺少 session_string,无法导入(请重新登录生成新 session)");
}
- var ok = await TryCreateSessionFromSessionStringAsync(
+ var ok = await SessionDataConverter.TryCreateWTelegramSessionFromSessionStringAsync(
+ sessionString: sessionKey,
apiId: apiId,
apiHash: apiHash.Trim(),
- sessionPath: targetSessionPath,
- sessionString: sessionKey
- );
+ targetSessionPath: targetSessionPath,
+ phone: phone,
+ userId: userId,
+ logger: _logger);
if (!ok)
{
@@ -437,68 +439,7 @@ public class AccountImportService
}
}
- private async Task TryCreateSessionFromSessionStringAsync(int apiId, string apiHash, string sessionPath, string sessionString)
- {
- try
- {
- var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync(
- phone: Path.GetFileNameWithoutExtension(sessionPath),
- apiId: apiId,
- apiHash: apiHash,
- targetSessionPath: sessionPath,
- logger: _logger
- );
-
- // 上面的转换是从 session数据 json 来的;这里导入时并不一定落到 session数据,
- // 所以当找不到 json 时,直接按 base64url 解码写文件并验证连接。
- if (!ok)
- {
- var bytes = DecodeBase64Url(sessionString);
- if (bytes.Length == 0) return false;
-
- var sessionsDir = Path.GetDirectoryName(Path.GetFullPath(sessionPath)) ?? Directory.GetCurrentDirectory();
- Directory.CreateDirectory(sessionsDir);
- await File.WriteAllBytesAsync(sessionPath, bytes);
-
- string Config(string what) => what switch
- {
- "api_id" => apiId.ToString(),
- "api_hash" => apiHash,
- "session_pathname" => sessionPath,
- _ => null!
- };
-
- using var client = new Client(Config);
- await client.ConnectAsync();
- ok = client.User != null;
- }
-
- return ok;
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to create session from session_string");
- try { if (File.Exists(sessionPath)) File.Delete(sessionPath); } catch { }
- return false;
- }
- }
-
- private static byte[] DecodeBase64Url(string input)
- {
- var s = input.Replace('-', '+').Replace('_', '/');
- var mod = s.Length % 4;
- if (mod == 2) s += "==";
- else if (mod == 3) s += "=";
-
- try
- {
- return Convert.FromBase64String(s);
- }
- catch
- {
- return Array.Empty();
- }
- }
+ // session_string 转换逻辑已收敛到 SessionDataConverter
private static bool TryGetString(System.Text.Json.JsonElement root, out string? value, params string[] names)
{
diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs
index 481b995..a7c0aec 100644
--- a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs
@@ -44,7 +44,7 @@ public class AccountService : IAccountService
_logger.LogInformation("Starting login for phone {Phone}", phone);
- var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath);
+ var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, phoneNumber: phone, userId: null);
string result;
try
@@ -57,7 +57,7 @@ public class AccountService : IAccountService
TryBackupCorruptedSessionIfExists(sessionPath);
await _clientPool.RemoveClientAsync(accountId);
- client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath);
+ client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath, phoneNumber: phone, userId: null);
result = await client.Login(phone);
}
diff --git a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs
index d9426c2..e8b5261 100644
--- a/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/ChannelService.cs
@@ -258,11 +258,11 @@ public class ChannelService : IChannelService
var absoluteSessionPath = Path.GetFullPath(account.SessionPath);
if (File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath))
{
- var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync(
+ var ok = await SessionDataConverter.TryConvertSqliteSessionFromJsonAsync(
phone: account.Phone,
apiId: account.ApiId,
apiHash: account.ApiHash,
- targetSessionPath: absoluteSessionPath,
+ sqliteSessionPath: absoluteSessionPath,
logger: _logger
);
@@ -270,17 +270,19 @@ public class ChannelService : IChannelService
{
throw new InvalidOperationException(
$"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" +
- "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" +
+ "已尝试从本地 json(例如 sessions/<手机号>.json 或 session数据/<手机号>/*.json)读取 session_string 自动转换但失败;" +
"请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。");
}
}
await _clientPool.RemoveClientAsync(accountId);
- var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath);
+ var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath, account.Phone, account.UserId);
try
{
await client.ConnectAsync();
+ if (client.User == null && (client.UserId != 0 || account.UserId != 0))
+ await client.LoginUserIfNeeded(reloginOnFailedResume: false);
}
catch (Exception ex)
{
diff --git a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs
index b0a8444..cc30cbe 100644
--- a/src/TelegramPanel.Core/Services/Telegram/GroupService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/GroupService.cs
@@ -125,11 +125,11 @@ public class GroupService : IGroupService
var absoluteSessionPath = Path.GetFullPath(account.SessionPath);
if (System.IO.File.Exists(absoluteSessionPath) && LooksLikeSqliteSession(absoluteSessionPath))
{
- var ok = await SessionDataConverter.TryConvertSessionFromSessionDataAsync(
+ var ok = await SessionDataConverter.TryConvertSqliteSessionFromJsonAsync(
phone: account.Phone,
apiId: account.ApiId,
apiHash: account.ApiHash,
- targetSessionPath: absoluteSessionPath,
+ sqliteSessionPath: absoluteSessionPath,
logger: _logger
);
@@ -137,17 +137,19 @@ public class GroupService : IGroupService
{
throw new InvalidOperationException(
$"该账号的 Session 文件为 SQLite 格式:{account.SessionPath},本项目无法直接复用。" +
- "已尝试从仓库根目录 `session数据/<手机号>/*.json` 读取 session_string 自动转换但失败;" +
+ "已尝试从本地 json(例如 sessions/<手机号>.json 或 session数据/<手机号>/*.json)读取 session_string 自动转换但失败;" +
"请到【账号-手机号登录】重新登录生成新的 sessions/*.session 后再操作。");
}
}
await _clientPool.RemoveClientAsync(accountId);
- var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath);
+ var client = await _clientPool.GetOrCreateClientAsync(accountId, account.ApiId, account.ApiHash, account.SessionPath, account.Phone, account.UserId);
try
{
await client.ConnectAsync();
+ if (client.User == null && (client.UserId != 0 || account.UserId != 0))
+ await client.LoginUserIfNeeded(reloginOnFailedResume: false);
}
catch (Exception ex)
{
diff --git a/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs
index e367b88..fbce9a6 100644
--- a/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/SessionDataConverter.cs
@@ -1,22 +1,28 @@
+using System.Buffers.Binary;
+using System.Net;
+using System.Reflection;
+using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
+using TL;
using WTelegram;
namespace TelegramPanel.Core.Services.Telegram;
internal static class SessionDataConverter
{
- public static async Task TryConvertSessionFromSessionDataAsync(
+ public static async Task TryConvertSqliteSessionFromJsonAsync(
string phone,
int apiId,
string apiHash,
- string targetSessionPath,
+ string sqliteSessionPath,
ILogger logger)
{
try
{
- var jsonPath = TryFindSessionDataJson(phone);
+ var absoluteSqliteSessionPath = Path.GetFullPath(sqliteSessionPath);
+ var jsonPath = TryFindAnySessionJsonPath(phone, absoluteSqliteSessionPath);
if (string.IsNullOrWhiteSpace(jsonPath))
return false;
@@ -30,37 +36,95 @@ internal static class SessionDataConverter
if (string.IsNullOrWhiteSpace(sessionString))
return false;
- var sessionBytes = DecodeBase64Url(sessionString.Trim());
- if (sessionBytes.Length == 0)
- return false;
+ _ = doc.RootElement.TryGetProperty("user_id", out var userIdProp);
+ _ = doc.RootElement.TryGetProperty("uid", out var uidProp);
+ long? userId = null;
+ if (userIdProp.ValueKind == JsonValueKind.Number && userIdProp.TryGetInt64(out var uid1)) userId = uid1;
+ if (userId == null && uidProp.ValueKind == JsonValueKind.Number && uidProp.TryGetInt64(out var uid2)) userId = uid2;
- // 备份旧文件(如果存在)
- TryBackupIfExists(targetSessionPath, suffix: "bak");
+ var ok = await TryCreateWTelegramSessionFromSessionStringAsync(
+ sessionString: sessionString.Trim(),
+ apiId: apiId,
+ apiHash: apiHash,
+ targetSessionPath: absoluteSqliteSessionPath,
+ phone: phone,
+ userId: userId,
+ logger: logger);
+ if (!ok) return false;
- var sessionsDir = Path.GetDirectoryName(Path.GetFullPath(targetSessionPath)) ?? Directory.GetCurrentDirectory();
- Directory.CreateDirectory(sessionsDir);
- await File.WriteAllBytesAsync(targetSessionPath, sessionBytes);
-
- // 用 WTelegram 连接一次验证 session 是否可用
- string Config(string what) => what switch
- {
- "api_id" => apiId.ToString(),
- "api_hash" => apiHash,
- "session_pathname" => targetSessionPath,
- _ => null!
- };
-
- using var client = new Client(Config);
- await client.ConnectAsync();
- if (client.User == null)
- return false;
-
- logger.LogInformation("Converted session for {Phone} from session数据 json: {JsonPath}", phone, jsonPath);
+ logger.LogInformation("Converted sqlite session for {Phone} using json: {JsonPath}", phone, jsonPath);
return true;
}
catch (Exception ex)
{
- logger.LogWarning(ex, "Failed to convert session for {Phone} from session数据 json", phone);
+ logger.LogWarning(ex, "Failed to convert sqlite session for {Phone} from json", phone);
+ return false;
+ }
+ }
+
+ public static async Task TryCreateWTelegramSessionFromSessionStringAsync(
+ string sessionString,
+ int apiId,
+ string apiHash,
+ string targetSessionPath,
+ string phone,
+ long? userId,
+ ILogger logger)
+ {
+ string? backupPath = null;
+ try
+ {
+ if (string.IsNullOrWhiteSpace(sessionString))
+ return false;
+
+ var absoluteTargetSessionPath = Path.GetFullPath(targetSessionPath);
+ var normalizedPhone = NormalizePhone(phone);
+ if (string.IsNullOrWhiteSpace(normalizedPhone))
+ normalizedPhone = NormalizePhone(Path.GetFileNameWithoutExtension(absoluteTargetSessionPath));
+
+ if (!TryParseTelethonStringSession(sessionString.Trim(), out var telethon))
+ return false;
+
+ // 先备份旧 sqlite session,再生成 WTelegram session 覆盖原路径
+ if (File.Exists(absoluteTargetSessionPath))
+ {
+ var suffix = LooksLikeSqliteSession(absoluteTargetSessionPath) ? "sqlite.bak" : "bak";
+ backupPath = BuildBackupPath(absoluteTargetSessionPath, suffix);
+ Directory.CreateDirectory(Path.GetDirectoryName(backupPath) ?? Directory.GetCurrentDirectory());
+ File.Move(absoluteTargetSessionPath, backupPath, overwrite: true);
+ }
+
+ var sessionsDir = Path.GetDirectoryName(absoluteTargetSessionPath) ?? Directory.GetCurrentDirectory();
+ Directory.CreateDirectory(sessionsDir);
+
+ // 使用 WTelegram 的 Session 存储格式生成可用 session 文件(加密 JSON)
+ var ok = await WriteWTelegramSessionFileAsync(
+ apiId: apiId,
+ apiHash: apiHash,
+ sessionPath: absoluteTargetSessionPath,
+ phoneDigits: normalizedPhone,
+ userId: userId,
+ dcId: telethon.DcId,
+ ipAddress: telethon.IpAddress,
+ port: telethon.Port,
+ authKey: telethon.AuthKey,
+ logger: logger
+ );
+ if (!ok)
+ throw new InvalidOperationException("WTelegram session 生成失败或校验失败");
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to create WTelegram session from session_string");
+ try { if (File.Exists(targetSessionPath)) File.Delete(targetSessionPath); } catch { }
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(backupPath) && File.Exists(backupPath) && !File.Exists(targetSessionPath))
+ File.Move(backupPath, targetSessionPath, overwrite: true);
+ }
+ catch { }
return false;
}
}
@@ -82,24 +146,53 @@ internal static class SessionDataConverter
}
}
- private static string? TryFindSessionDataJson(string phone)
+ private static string? TryFindAnySessionJsonPath(string phone, string absoluteSessionPath)
{
- var current = Directory.GetCurrentDirectory();
- for (int i = 0; i < 6 && !string.IsNullOrWhiteSpace(current); i++)
- {
- var candidate = Path.Combine(current, "session数据", phone, $"{phone}.json");
- if (File.Exists(candidate))
- return candidate;
+ var normalizedPhone = NormalizePhone(phone);
+ if (string.IsNullOrWhiteSpace(normalizedPhone))
+ return null;
- var dirCandidate = Path.Combine(current, "session数据", phone);
- if (Directory.Exists(dirCandidate))
+ // 1) 优先找与 session 同目录的 phone.json(最常见:sessions/.json)
+ var sessionDir = Path.GetDirectoryName(absoluteSessionPath);
+ if (!string.IsNullOrWhiteSpace(sessionDir))
+ {
+ var direct = Path.Combine(sessionDir, $"{normalizedPhone}.json");
+ if (File.Exists(direct))
+ return direct;
+
+ var anyInSessionDir = Directory.EnumerateFiles(sessionDir, "*.json", SearchOption.TopDirectoryOnly)
+ .FirstOrDefault(p => string.Equals(NormalizePhone(Path.GetFileNameWithoutExtension(p)), normalizedPhone, StringComparison.Ordinal));
+ if (!string.IsNullOrWhiteSpace(anyInSessionDir))
+ return anyInSessionDir;
+ }
+
+ // 2) 尝试在仓库根目录的 session数据/ 下找
+ var repoRoot = TryFindRepoRoot();
+ if (!string.IsNullOrWhiteSpace(repoRoot))
+ {
+ var sessionDataDir = Path.Combine(repoRoot, "session数据", normalizedPhone);
+ var direct = Path.Combine(sessionDataDir, $"{normalizedPhone}.json");
+ if (File.Exists(direct))
+ return direct;
+
+ if (Directory.Exists(sessionDataDir))
{
- var any = Directory.EnumerateFiles(dirCandidate, "*.json", SearchOption.TopDirectoryOnly).FirstOrDefault();
+ var any = Directory.EnumerateFiles(sessionDataDir, "*.json", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(any))
return any;
}
- current = Directory.GetParent(current)?.FullName;
+ // 3) 兜底:在 session数据 下递归扫描 phone 字段匹配
+ var sessionDataRoot = Path.Combine(repoRoot, "session数据");
+ var scanned = TryScanJsonByPhone(sessionDataRoot, normalizedPhone);
+ if (!string.IsNullOrWhiteSpace(scanned))
+ return scanned;
+
+ // 4) 再兜底:在 sessions 下递归扫描(部分用户会把 json 放在 sessions/ 里)
+ var sessionsRoot = Path.Combine(repoRoot, "sessions");
+ scanned = TryScanJsonByPhone(sessionsRoot, normalizedPhone);
+ if (!string.IsNullOrWhiteSpace(scanned))
+ return scanned;
}
return null;
@@ -122,24 +215,210 @@ internal static class SessionDataConverter
}
}
- private static void TryBackupIfExists(string path, string suffix)
+ private readonly record struct TelethonSessionData(int DcId, string IpAddress, ushort Port, byte[] AuthKey);
+
+ private static bool TryParseTelethonStringSession(string sessionString, out TelethonSessionData data)
{
try
{
- var fullPath = Path.GetFullPath(path);
- if (!File.Exists(fullPath))
- return;
+ data = default;
+ if (string.IsNullOrWhiteSpace(sessionString) || sessionString.Length < 16)
+ return false;
- var dir = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory();
- var name = Path.GetFileNameWithoutExtension(fullPath);
- var ext = Path.GetExtension(fullPath);
- var backupPath = Path.Combine(dir, $"{name}.{suffix}{ext}");
- File.Move(fullPath, backupPath, overwrite: true);
+ // Telethon StringSession: first char is version digit
+ if (!char.IsDigit(sessionString[0]))
+ return false;
+
+ var body = sessionString.Substring(1);
+ var packed = DecodeBase64Url(body);
+
+ // 常见 Telethon packed bytes 长度:
+ // - IPv4: 263 = 1(dc_id)+4(ip)+2(port)+256(auth_key)
+ // - IPv6: 275 = 1+16+2+256
+ if (packed.Length is not (263 or 275))
+ return false;
+
+ var dcId = packed[0];
+ var ipLen = packed.Length - 1 - 2 - 256;
+ if (ipLen is not (4 or 16))
+ return false;
+
+ var ipBytes = packed.AsSpan(1, ipLen).ToArray();
+ var ip = new IPAddress(ipBytes).ToString();
+ var port = BinaryPrimitives.ReadUInt16BigEndian(packed.AsSpan(1 + ipLen, 2));
+ var authKey = packed.AsSpan(1 + ipLen + 2, 256).ToArray();
+
+ data = new TelethonSessionData(dcId, ip, port, authKey);
+ return true;
}
catch
{
- // 忽略备份失败
+ data = default;
+ return false;
}
}
-}
+ private static async Task WriteWTelegramSessionFileAsync(
+ int apiId,
+ string apiHash,
+ string sessionPath,
+ string phoneDigits,
+ long? userId,
+ int dcId,
+ string ipAddress,
+ ushort port,
+ byte[] authKey,
+ ILogger logger)
+ {
+ string Config(string what) => what switch
+ {
+ "api_id" => apiId.ToString(),
+ "api_hash" => apiHash,
+ "session_key" => apiHash,
+ "session_pathname" => sessionPath,
+ "phone_number" => phoneDigits,
+ "user_id" => userId?.ToString() ?? "-1",
+ _ => null!
+ };
+
+ // 1) 创建空 session 文件
+ await using (var builder = new Client(Config))
+ {
+ var clientType = typeof(Client);
+ var sessionField = clientType.GetField("_session", BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("无法访问 WTelegram.Client._session");
+ var dcSessionField = clientType.GetField("_dcSession", BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("无法访问 WTelegram.Client._dcSession");
+
+ var sessionObj = sessionField.GetValue(builder) ?? throw new InvalidOperationException("WTelegram session 未初始化");
+ var sessionType = sessionObj.GetType();
+
+ var dcSessionType = sessionType.GetNestedType("DCSession", BindingFlags.Public | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("无法访问 WTelegram.Session.DCSession");
+ var dcSessionObj = Activator.CreateInstance(dcSessionType) ?? throw new InvalidOperationException("无法创建 DCSession");
+
+ // TL.DcOption 是公开类型,直接构造
+ var dcOption = new DcOption { id = dcId, ip_address = ipAddress, port = port, flags = 0 };
+
+ // 填充 DCSession:AuthKey + DataCenter + UserId + authKeyID
+ dcSessionType.GetField("AuthKey")?.SetValue(dcSessionObj, authKey);
+ dcSessionType.GetField("UserId")?.SetValue(dcSessionObj, userId ?? 0);
+ dcSessionType.GetField("DataCenter")?.SetValue(dcSessionObj, dcOption);
+ dcSessionType.GetField("Layer")?.SetValue(dcSessionObj, 0);
+
+ using (var sha1 = SHA1.Create())
+ {
+ var hash = sha1.ComputeHash(authKey);
+ var authKeyId = BinaryPrimitives.ReadInt64LittleEndian(hash.AsSpan(12, 8));
+ dcSessionType.GetField("authKeyID", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(dcSessionObj, authKeyId);
+ }
+
+ dcSessionType.GetField("Client", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(dcSessionObj, builder);
+
+ // 填充 Session:MainDC + UserId + DcOptions + DCSessions
+ sessionType.GetField("MainDC")?.SetValue(sessionObj, dcId);
+ sessionType.GetField("UserId")?.SetValue(sessionObj, userId ?? 0);
+ sessionType.GetField("DcOptions")?.SetValue(sessionObj, new[] { dcOption });
+
+ var dcSessionsField = sessionType.GetField("DCSessions");
+ var dcSessions = dcSessionsField?.GetValue(sessionObj) as System.Collections.IDictionary;
+ if (dcSessions == null)
+ throw new InvalidOperationException("无法访问 Session.DCSessions");
+ dcSessions[dcId] = dcSessionObj;
+
+ // 同步 builder 当前 dcSession 引用
+ dcSessionField.SetValue(builder, dcSessionObj);
+
+ // 保存 session(写入加密 JSON)
+ var save = sessionType.GetMethod("Save", BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new InvalidOperationException("无法访问 Session.Save()");
+ lock (sessionObj) save.Invoke(sessionObj, null);
+ }
+
+ // 2) 用 LoginUserIfNeeded 验证(不允许自动重新登录,避免进入要验证码流程)
+ await using var probe = new Client(Config);
+ try
+ {
+ _ = await probe.LoginUserIfNeeded(reloginOnFailedResume: false);
+ logger.LogInformation("WTelegram session validated for {Phone} on DC {DcId}", phoneDigits, dcId);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "WTelegram session validation failed for {Phone}", phoneDigits);
+ return false;
+ }
+ }
+
+ private static string BuildBackupPath(string originalPath, string suffix)
+ {
+ var fullPath = Path.GetFullPath(originalPath);
+ var dir = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory();
+ var name = Path.GetFileNameWithoutExtension(fullPath);
+ var ext = Path.GetExtension(fullPath);
+ return Path.Combine(dir, $"{name}.{suffix}{ext}");
+ }
+
+ private static string? TryFindRepoRoot()
+ {
+ var current = Directory.GetCurrentDirectory();
+ for (int i = 0; i < 10 && !string.IsNullOrWhiteSpace(current); i++)
+ {
+ if (File.Exists(Path.Combine(current, "TelegramPanel.sln")))
+ return current;
+ current = Directory.GetParent(current)?.FullName;
+ }
+
+ return null;
+ }
+
+ private static string? TryScanJsonByPhone(string rootDir, string normalizedPhone)
+ {
+ try
+ {
+ if (!Directory.Exists(rootDir))
+ return null;
+
+ foreach (var jsonPath in Directory.EnumerateFiles(rootDir, "*.json", SearchOption.AllDirectories))
+ {
+ try
+ {
+ var text = File.ReadAllText(jsonPath);
+ using var doc = JsonDocument.Parse(text);
+ if (doc.RootElement.TryGetProperty("phone", out var phoneProp) && phoneProp.ValueKind == JsonValueKind.String)
+ {
+ var p = NormalizePhone(phoneProp.GetString());
+ if (string.Equals(p, normalizedPhone, StringComparison.Ordinal))
+ return jsonPath;
+ }
+ }
+ catch
+ {
+ // ignore single json parse error
+ }
+ }
+
+ return null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static string NormalizePhone(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ return string.Empty;
+
+ var digits = new StringBuilder(phone.Length);
+ foreach (var ch in phone)
+ {
+ if (ch >= '0' && ch <= '9')
+ digits.Append(ch);
+ }
+ return digits.ToString();
+ }
+
+ // 备份逻辑在 TryCreateWTelegramSessionFromSessionStringAsync 内集中处理
+}
diff --git a/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs b/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs
index 05221b8..ad17205 100644
--- a/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/TelegramClientPool.cs
@@ -23,7 +23,7 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable
public int ActiveClientCount => _clients.Count;
- public async Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath)
+ public async Task GetOrCreateClientAsync(int accountId, int apiId, string apiHash, string sessionPath, string? phoneNumber = null, long? userId = null)
{
if (_clients.TryGetValue(accountId, out var existingClient))
{
@@ -49,6 +49,7 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable
_logger.LogInformation("Creating new Telegram client for account {AccountId}", accountId);
// 使用 config 回调设置 session 路径
+ phoneNumber = NormalizePhone(phoneNumber);
string Config(string what)
{
return what switch
@@ -56,6 +57,8 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable
"api_id" => apiId.ToString(),
"api_hash" => apiHash,
"session_pathname" => sessionPath,
+ "phone_number" => string.IsNullOrWhiteSpace(phoneNumber) ? null! : phoneNumber,
+ "user_id" => userId.HasValue && userId.Value > 0 ? userId.Value.ToString() : null!,
_ => null! // 使用 null! 抑制警告,这是 WTelegramClient 的预期行为
};
}
@@ -78,6 +81,21 @@ public class TelegramClientPool : ITelegramClientPool, IDisposable
}
}
+ private static string? NormalizePhone(string? phone)
+ {
+ if (string.IsNullOrWhiteSpace(phone))
+ return null;
+
+ var digits = new char[phone.Length];
+ var count = 0;
+ foreach (var ch in phone)
+ {
+ if (ch >= '0' && ch <= '9')
+ digits[count++] = ch;
+ }
+ return count == 0 ? null : new string(digits, 0, count);
+ }
+
public Client? GetClient(int accountId)
{
return _clients.TryGetValue(accountId, out var client) ? client : null;