fix(bot-channels): 账号执行自动补全加入并兼容-100 chat_id

- 兼容 Bot API chat_id(-100...) 与 MTProto Channel.id 的多种表示

- 批量邀请/批量设管理员遇到 Channel not found 时:导出邀请链接→执行账号加入→重试一次

- README 重排目录

- 版本号升级到 1.2.8
This commit is contained in:
meoacgx committed 2026-01-02 22:03:32 +08:00
1 parent 6f7354d21a
commit f7873cd66b
6 files changed
+179 -47

No files matched your search

+4 -4
View File
@@ -5,10 +5,10 @@
<!-- 主版本: 重大变更/不兼容更新 -->
<!-- 次版本: 新功能/兼容性更新 -->
<!-- 修订号: Bug修复/小改进 -->
<Version>1.2.7</Version>
<AssemblyVersion>1.2.7.0</AssemblyVersion>
<FileVersion>1.2.7.0</FileVersion>
<InformationalVersion>1.2.7</InformationalVersion>
<Version>1.2.8</Version>
<AssemblyVersion>1.2.8.0</AssemblyVersion>
<FileVersion>1.2.8.0</FileVersion>
<InformationalVersion>1.2.8</InformationalVersion>
<!-- 项目元信息 -->
<Authors>Telegram Panel Contributors</Authors>
+50 -34
View File
@@ -2,7 +2,19 @@
基于 **WTelegramClient** 的 Telegram 多账户管理面板(.NET 8 / Blazor Server),用于批量管理账号、统计与管理频道/群组、执行批量任务。
## 社区
## 目录
- [社区与交流](#社区与交流)
- [功能亮点](#功能亮点)
- [Docker 一键部署(推荐)](#docker-一键部署推荐)
- [反向代理一条龙(可选)](#反向代理一条龙可选)
- [本地开发运行(可选)](#本地开发运行可选)
- [模块扩展(可选)](#模块扩展可选)
- [详细文档](#详细文档)
- [防冻结指南(新号必看)](#防冻结指南新号必看)
- [截图](#截图)
## 社区与交流
- TG 频道:https://t.me/zhanzhangck
- 站长交流群:https://t.me/vpsbbq
@@ -21,36 +33,9 @@
- 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量邀请成员/设置管理员(可按管理员列表匹配系统账号执行);支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates)
- 🏷️ **账号分类(排除操作)**:支持把“养号/不参与运营”的账号分类标记为“排除操作”,不会出现在创建频道/批量任务的执行账号下拉里
## Docker 一键部署(推荐)
## 🧊 防冻结指南(新号必看)
Telegram 对新号/风控号的限制比较敏感;本项目提供了很多“批量/高频”能力,请务必谨慎使用。
- ⚠️ **新号切记:登录面板后不要进行任何操作!!!至少养 24 小时再创建频道/群、批量邀请/加管理员等敏感操作**
- ✅ **如果是购买的协议号/接码号(需要尽快验号)**:建议导入后先做一次「批量 Telegram 状态检测」,必要时用「深度检测」快速筛掉废号(避免隔天再找商家被拒赔付);验号通过后再开始低频养号与后续操作
- ⚠️ **重要提醒(买号必看)**:如果你是用商家提供的 Session 导入到系统里,本质上是在使用“同一个会话”。因此“踢设备”只能踢掉**除当前 Session 会话之外**的其他设备;若商家仍保留该 Session,依然有被重复使用的风险。想要更安全:务必用验证码登录一次(以你自己的登录为准),再踢出其他 Session 会话,并绑定 2FA 找回邮箱
- ✅ 养号完成后:先从少量、低频操作开始,逐步增加频率(宁可慢点,也别一上来就批量)
- ⛔ 如果账号出现限制/冻结迹象:建议先停用该账号,等待恢复后再继续操作
- 📧 **重要账号务必绑定 2FA 找回邮箱**:尤其是接码手机号的账号,存在“官方客户端也可能突然掉登录”的情况;若未绑定邮箱,掉线后可能无法找回(账号就丢了)。建议准备一个稳定的域名邮箱,把重要账号都绑定到同一域名邮箱体系,便于后续通过邮箱验证码重新登录。非重要账号可酌情忽略。
## 截图
> 仓库自带后台截图:`screenshot/`
<details>
<summary>点击展开/收起截图</summary>
| | | |
|---|---|---|
| <img src="screenshot/Dashboard.png" width="300" /> | <img src="screenshot/account.png" width="300" /> | <img src="screenshot/equipment.png" width="300" /> |
| <img src="screenshot/Import account.png" width="300" /> | <img src="screenshot/Login with mobile phone number.png" width="300" /> | <img src="screenshot/System notification.png" width="300" /> |
| <img src="screenshot/Create channel.png" width="300" /> | <img src="screenshot/Invite users in batches.png" width="300" /> | <img src="screenshot/Set up administrators in batches.png" width="300" /> |
</details>
## 🐳 Docker 一键部署(推荐)
面向小白:**`git clone` → `docker compose up` → 浏览器打开 → 登录改密码 → 配置 ApiId/ApiHash**。
🐳 面向小白:**`git clone` → `docker compose up` → 浏览器打开 → 登录改密码 → 配置 ApiId/ApiHash**。
### 环境要求
@@ -93,9 +78,9 @@ https://faka.boxmoe.eu.org/
### Bot Webhook 模式(可选)
> **💡 提示**:如果你不使用BOT相关功能,可以跳过此节。
> **💡 提示**:如果你不使用 BOT 相关功能,可以跳过此节。
默认情况下,Bot 使用**长轮询(Long Polling)**模式接收更新。**生产环境建议使用 Webhook 模式**,优势如下:
默认情况下,Bot 使用**长轮询(Long Polling)**模式接收更新。生产环境可选 Webhook:
- ✅ 更低的资源消耗(无需持续轮询)
- ✅ 更快的响应速度(Telegram 主动推送)
@@ -222,7 +207,7 @@ docker compose build --no-cache
docker compose up -d
```
## 🌐 反向代理一条龙(可选)
## 反向代理一条龙(可选)
Blazor Server 需要 WebSocket(`/_blazor`),反代必须支持 `Upgrade`。
@@ -249,7 +234,7 @@ location / {
dotnet run --project src/TelegramPanel.Web
```
## 🧩 模块扩展(可选)
## 模块扩展(可选)
项目已支持“可安装/可卸载”的模块扩展(任务 / API / UI),用于后续按需扩展能力、按场景自由定制开发自己的模块,而不是把功能写死在核心里。
@@ -284,3 +269,34 @@ dotnet run --project src/TelegramPanel.Web
- `docs/database.md`(数据库/表结构说明)
- `docs/advanced.md`(配置项/数据目录/后台任务等)
- `docs/modules.md`(模块系统:可安装/可卸载/依赖/回滚)
## 防冻结指南(新号必看)
<details>
<summary>点击展开/收起防冻结指南</summary>
Telegram 对新号/风控号的限制比较敏感;本项目提供了很多“批量/高频”能力,请务必谨慎使用。
- ⚠️ **新号切记:登录面板后不要进行任何操作!!!至少养 24 小时再创建频道/群、批量邀请/加管理员等敏感操作**
- ✅ **如果是购买的协议号/接码号(需要尽快验号)**:建议导入后先做一次「批量 Telegram 状态检测」,必要时用「深度检测」快速筛掉废号(避免隔天再找商家被拒赔付);验号通过后再开始低频养号与后续操作
- ⚠️ **重要提醒(买号必看)**:如果你是用商家提供的 Session 导入到系统里,本质上是在使用“同一个会话”。因此“踢设备”只能踢掉**除当前 Session 会话之外**的其他设备;若商家仍保留该 Session,依然有被重复使用的风险。想要更安全:务必用验证码登录一次(以你自己的登录为准),再踢出其他 Session 会话,并绑定 2FA 找回邮箱
- ✅ 养号完成后:先从少量、低频操作开始,逐步增加频率(宁可慢点,也别一上来就批量)
- ⛔ 如果账号出现限制/冻结迹象:建议先停用该账号,等待恢复后再继续操作
- 📧 **重要账号务必绑定 2FA 找回邮箱**:尤其是接码手机号的账号,存在“官方客户端也可能突然掉登录”的情况;若未绑定邮箱,掉线后可能无法找回(账号就丢了)。建议准备一个稳定的域名邮箱,把重要账号都绑定到同一域名邮箱体系,便于后续通过邮箱验证码重新登录。非重要账号可酌情忽略。
</details>
## 截图
> 仓库自带后台截图:`screenshot/`
<details>
<summary>点击展开/收起截图</summary>
| | | |
|---|---|---|
| <img src="screenshot/Dashboard.png" width="300" /> | <img src="screenshot/account.png" width="300" /> | <img src="screenshot/equipment.png" width="300" /> |
| <img src="screenshot/Import account.png" width="300" /> | <img src="screenshot/Login with mobile phone number.png" width="300" /> | <img src="screenshot/System notification.png" width="300" /> |
| <img src="screenshot/Create channel.png" width="300" /> | <img src="screenshot/Invite users in batches.png" width="300" /> | <img src="screenshot/Set up administrators in batches.png" width="300" /> |
</details>
@@ -576,25 +576,32 @@ public class ChannelService : IChannelService
#region Private Methods
private static long NormalizeChannelId(long channelId)
private static IReadOnlyList<long> GetCandidateChannelIds(long channelId)
{
// Bot API 的频道/超级群 chat_id 通常形如:-100xxxxxxxxxx(即 -1000000000000 - channel_id)
// MTProto 的 channel_id 为 int32(可能为负),WTelegram 的 Channel.id 也基于该 int32。
// 因此这里把 -100... 规范化回 MTProto 的 int32 id,避免出现 “Channel -100... not found”。
// 兼容 Bot API 的 chat_id 形式:-100xxxxxxxxxx
// 不同库对 channel_id 的表示可能存在 unsigned/signed 差异,这里同时尝试多个候选值匹配 dialogs 里的 Channel.id。
var list = new List<long>(capacity: 3) { channelId };
if (channelId <= -1000000000000L)
{
var raw = -(channelId + 1000000000000L);
return unchecked((int)raw);
// Bot chat_id = -1000000000000 - channel_id
var raw = (-channelId) - 1000000000000L;
list.Add(raw);
list.Add(unchecked((int)raw));
}
return channelId;
return list.Distinct().ToList();
}
private async Task<Channel?> GetChannelByIdAsync(Client client, long channelId)
{
channelId = NormalizeChannelId(channelId);
var candidates = GetCandidateChannelIds(channelId);
var candidateSet = candidates.Count == 1 ? null : candidates.ToHashSet();
var dialogs = await client.Messages_GetAllDialogs();
return dialogs.chats.Values.OfType<Channel>().FirstOrDefault(c => c.id == channelId);
return dialogs.chats.Values
.OfType<Channel>()
.FirstOrDefault(c => candidateSet?.Contains(c.id) ?? c.id == channelId);
}
private async Task<Client> GetOrCreateConnectedClientAsync(int accountId)
@@ -1,6 +1,7 @@
@inject AccountManagementService AccountManagement
@inject IChannelService ChannelService
@inject BotTelegramService BotTelegram
@inject AccountTelegramToolsService AccountTools
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject TelegramPanel.Web.Services.ChannelAdminDefaultsService DefaultsService
@@ -417,6 +418,8 @@
try
{
var joinTried = new HashSet<string>(StringComparer.Ordinal);
foreach (var ch in channels)
{
var executorId = ResolveExecutorAccountId(ch);
@@ -449,6 +452,35 @@
failures.Add($"{ch.Title} => {username}:失败");
}
}
catch (Exception ex) when (!joinTried.Contains($"{executorId.Value}:{ch.TelegramId}") && BotChannelJoinRetryHelper.LooksLikeChannelNotFound(ex.Message))
{
var key = $"{executorId.Value}:{ch.TelegramId}";
joinTried.Add(key);
var joined = await BotChannelJoinRetryHelper.TryJoinChannelAsync(
BotTelegram, AccountTools, BotId, executorId.Value, ch, failures, _cts.Token);
if (!joined)
{
failed++;
failures.Add($"{ch.Title} => {username}:{ex.Message}");
continue;
}
try
{
var ok = await ChannelService.SetAdminAsync(executorId.Value, ch.TelegramId, username, SelectedRights, title);
if (!ok)
{
failed++;
failures.Add($"{ch.Title} => {username}:失败");
}
}
catch (Exception ex2)
{
failed++;
failures.Add($"{ch.Title} => {username}:{ex2.Message}");
}
}
catch (Exception ex)
{
failed++;
@@ -1,6 +1,7 @@
@inject AccountManagementService AccountManagement
@inject IChannelService ChannelService
@inject BotTelegramService BotTelegram
@inject AccountTelegramToolsService AccountTools
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject TelegramPanel.Web.Services.ChannelInvitePresetsService PresetsService
@@ -348,6 +349,8 @@
try
{
var joinTried = new HashSet<string>(StringComparer.Ordinal);
foreach (var ch in channels)
{
var executorId = ResolveExecutorAccountId(ch);
@@ -380,6 +383,35 @@
failures.Add($"{ch.Title} => {username}:{result.Error}");
}
}
catch (Exception ex) when (!joinTried.Contains($"{executorId.Value}:{ch.TelegramId}") && BotChannelJoinRetryHelper.LooksLikeChannelNotFound(ex.Message))
{
var key = $"{executorId.Value}:{ch.TelegramId}";
joinTried.Add(key);
var joined = await BotChannelJoinRetryHelper.TryJoinChannelAsync(
BotTelegram, AccountTools, BotId, executorId.Value, ch, failures, _cts.Token);
if (!joined)
{
failed++;
failures.Add($"{ch.Title} => {username}:{ex.Message}");
continue;
}
try
{
var result = await ChannelService.InviteUserAsync(executorId.Value, ch.TelegramId, username);
if (!result.Success)
{
failed++;
failures.Add($"{ch.Title} => {username}:{result.Error}");
}
}
catch (Exception ex2)
{
failed++;
failures.Add($"{ch.Title} => {username}:{ex2.Message}");
}
}
catch (Exception ex)
{
failed++;
@@ -0,0 +1,45 @@
using TelegramPanel.Core.Services.Telegram;
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Web.Services;
public static class BotChannelJoinRetryHelper
{
public static bool LooksLikeChannelNotFound(string? message)
{
if (string.IsNullOrWhiteSpace(message))
return false;
return message.Contains("Channel", StringComparison.OrdinalIgnoreCase)
&& message.Contains("not found", StringComparison.OrdinalIgnoreCase);
}
public static async Task<bool> TryJoinChannelAsync(
BotTelegramService botTelegram,
AccountTelegramToolsService accountTools,
int botId,
int accountId,
BotChannel channel,
List<string> failures,
CancellationToken cancellationToken)
{
try
{
var link = await botTelegram.ExportInviteLinkAsync(botId, channel.TelegramId, cancellationToken);
var (success, error, _) = await accountTools.JoinChatOrChannelAsync(accountId, link, cancellationToken);
if (!success)
{
failures.Add($"{channel.Title}:执行账号尝试加入频道失败:{error}");
return false;
}
return true;
}
catch (Exception ex)
{
failures.Add($"{channel.Title}:执行账号尝试加入频道失败:{ex.Message}");
return false;
}
}
}