├── screenshot
├── account.png
├── Dashboard.png
├── equipment.png
├── Create channel.png
├── Import account.png
├── System notification.png
├── BOT management channel.png
├── Invite users in batches.png
├── Login with mobile phone number.png
└── Set up administrators in batches.png
├── global.json
├── .dockerignore
├── src
├── TelegramPanel.Web
│ ├── Services
│ │ ├── PanelTimeZoneOptions.cs
│ │ ├── AdminAuthOptions.cs
│ │ ├── AdminAuthHelpers.cs
│ │ ├── AppRestartService.cs
│ │ ├── LocalConfigFile.cs
│ │ ├── AccountDataAutoSyncBackgroundService.cs
│ │ ├── UiPreferencesService.cs
│ │ ├── PanelTimeZoneService.cs
│ │ ├── DataSyncService.cs
│ │ └── BotAutoSyncBackgroundService.cs
│ ├── Components
│ │ ├── Dialogs
│ │ │ ├── AccountCategoryEditModel.cs
│ │ │ ├── ExternalApiCurlSampleDialog.razor
│ │ │ ├── BotBatchSetCategoryDialog.razor
│ │ │ ├── BatchSetAccountCategoryDialog.razor
│ │ │ ├── AccountCategoryEditDialog.razor
│ │ │ ├── BotChannelCategoryDialog.razor
│ │ │ ├── SystemInboxDialog.razor
│ │ │ ├── CreateExternalApiDialog.razor
│ │ │ ├── ModulePageDialog.razor
│ │ │ ├── BotBanMemberDialog.razor
│ │ │ ├── RiskWarningDialog.razor
│ │ │ ├── EditGenericApiDialog.razor
│ │ │ ├── ChannelEditDialog.razor
│ │ │ └── BotChannelEditDialog.razor
│ │ ├── Routes.razor
│ │ ├── Shared
│ │ │ └── PanelTime.razor
│ │ ├── App.razor
│ │ ├── _Imports.razor
│ │ ├── Layout
│ │ │ ├── MainLayout.razor
│ │ │ └── NavMenu.razor
│ │ └── Pages
│ │ │ ├── ModulePageHost.razor
│ │ │ └── AdminPassword.razor
│ ├── Modules
│ │ ├── ModuleApplicationExtensions.cs
│ │ ├── ModuleRegistry.cs
│ │ ├── BuiltIn
│ │ │ ├── BuiltInModuleCatalog.cs
│ │ │ ├── TaskCatalogModule.cs
│ │ │ └── KickApiModule.cs
│ │ ├── ModuleStateStore.cs
│ │ ├── ModulePaths.cs
│ │ └── ModuleLoadContext.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── _Imports.razor
│ ├── wwwroot
│ │ └── app.js
│ ├── appsettings.json
│ ├── ExternalApi
│ │ ├── ExternalApiCatalog.cs
│ │ └── ExternalApiModels.cs
│ └── TelegramPanel.Web.csproj
├── TelegramPanel.Core
│ ├── Models
│ │ ├── TelegramSystemMessage.cs
│ │ ├── ChannelAdminInfo.cs
│ │ ├── GroupInfo.cs
│ │ ├── TelegramAuthorizationInfo.cs
│ │ ├── AccountInfo.cs
│ │ ├── ChannelInfo.cs
│ │ ├── BatchTaskInfo.cs
│ │ └── TelegramAccountStatusResult.cs
│ ├── Services
│ │ ├── RiskWarningAction.cs
│ │ ├── ChannelGroupManagementService.cs
│ │ ├── AccountCategoryManagementService.cs
│ │ ├── GroupManagementService.cs
│ │ ├── BatchTaskManagementService.cs
│ │ └── AccountRiskService.cs
│ ├── BatchTasks
│ │ └── BatchTaskTypes.cs
│ ├── Interfaces
│ │ ├── IGroupService.cs
│ │ ├── ITelegramClientPool.cs
│ │ ├── ISessionImporter.cs
│ │ ├── IAccountService.cs
│ │ └── IChannelService.cs
│ ├── TelegramPanel.Core.csproj
│ ├── Utils
│ │ └── PhoneNumberFormatter.cs
│ └── ServiceCollectionExtensions.cs
├── TelegramPanel.Data
│ ├── Repositories
│ │ ├── IBotRepository.cs
│ │ ├── IChannelGroupRepository.cs
│ │ ├── IAccountCategoryRepository.cs
│ │ ├── IBotChannelRepository.cs
│ │ ├── IGroupRepository.cs
│ │ ├── IBotChannelCategoryRepository.cs
│ │ ├── IBatchTaskRepository.cs
│ │ ├── IAccountRepository.cs
│ │ ├── IAccountChannelRepository.cs
│ │ ├── IRepository.cs
│ │ ├── IChannelRepository.cs
│ │ ├── ChannelGroupRepository.cs
│ │ ├── AccountCategoryRepository.cs
│ │ ├── BotRepository.cs
│ │ ├── BotChannelCategoryRepository.cs
│ │ ├── BotChannelRepository.cs
│ │ ├── BatchTaskRepository.cs
│ │ ├── GroupRepository.cs
│ │ ├── Repository.cs
│ │ ├── AccountRepository.cs
│ │ ├── AccountChannelRepository.cs
│ │ └── ChannelRepository.cs
│ ├── Entities
│ │ ├── ChannelGroup.cs
│ │ ├── AccountCategory.cs
│ │ ├── BotChannelCategory.cs
│ │ ├── Group.cs
│ │ ├── BatchTask.cs
│ │ ├── AccountChannel.cs
│ │ ├── Bot.cs
│ │ ├── BotChannel.cs
│ │ ├── Channel.cs
│ │ ├── Account.cs
│ │ └── AccountExtensions.cs
│ ├── TelegramPanel.Data.csproj
│ ├── Migrations
│ │ ├── 20251219000002_AddBotLastUpdateId.cs
│ │ ├── 20251220000000_AddLastLoginAt.cs
│ │ ├── 20251219000004_AccountUserIdIndexNotUnique.cs
│ │ └── 20251219000003_AddAccountTelegramStatusCache.cs
│ └── ServiceCollectionExtensions.cs
└── TelegramPanel.Modules.Abstractions
│ ├── TelegramPanel.Modules.Abstractions.csproj
│ ├── ITelegramPanelModule.cs
│ ├── ModuleManifest.cs
│ ├── ModuleContributions.cs
│ └── SemVer.cs
├── docs
├── README.md
├── sync.md
├── import.md
├── database.md
├── api.md
├── advanced.md
└── reverse-proxy.md
├── Dockerfile
├── docker-compose.yml
├── Dockerfile.local
├── .gitignore
├── TelegramPanel.sln
└── tools
└── package-module.ps1
/screenshot/account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/account.png
--------------------------------------------------------------------------------
/screenshot/Dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Dashboard.png
--------------------------------------------------------------------------------
/screenshot/equipment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/equipment.png
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.416",
4 | "rollForward": "latestPatch"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/screenshot/Create channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Create channel.png
--------------------------------------------------------------------------------
/screenshot/Import account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Import account.png
--------------------------------------------------------------------------------
/screenshot/System notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/System notification.png
--------------------------------------------------------------------------------
/screenshot/BOT management channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/BOT management channel.png
--------------------------------------------------------------------------------
/screenshot/Invite users in batches.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Invite users in batches.png
--------------------------------------------------------------------------------
/screenshot/Login with mobile phone number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Login with mobile phone number.png
--------------------------------------------------------------------------------
/screenshot/Set up administrators in batches.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeacgx/Telegram-Panel/HEAD/screenshot/Set up administrators in batches.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .serena
3 | .tmp
4 | **/bin
5 | **/obj
6 | docker-data
7 | logs
8 | sessions
9 | todolist
10 | *.db
11 | *.log
12 | *.user
13 | *.suo
14 | *.cache
15 |
16 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/PanelTimeZoneOptions.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.Services;
2 |
3 | public sealed class PanelTimeZoneOptions
4 | {
5 | public string? TimeZoneId { get; set; }
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditModel.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.Components.Dialogs;
2 |
3 | public sealed record AccountCategoryEditModel(string Name, string? Color, string? Description);
4 |
5 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/TelegramSystemMessage.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// Telegram 系统通知(常用于接收登录验证码,来自 777000 对话)
5 | ///
6 | public record TelegramSystemMessage(
7 | int Id,
8 | DateTime? DateUtc,
9 | string Text
10 | );
11 |
12 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IBotRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | public interface IBotRepository : IRepository
6 | {
7 | Task GetByNameAsync(string name);
8 | Task> GetAllWithStatsAsync();
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IChannelGroupRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 频道分组仓储接口
7 | ///
8 | public interface IChannelGroupRepository : IRepository
9 | {
10 | Task GetByNameAsync(string name);
11 | }
12 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Routes.razor:
--------------------------------------------------------------------------------
1 | @using TelegramPanel.Web.Components.Layout
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IAccountCategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 账号分类仓储接口
7 | ///
8 | public interface IAccountCategoryRepository : IRepository
9 | {
10 | Task GetByNameAsync(string name);
11 | }
12 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Services/RiskWarningAction.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Services;
2 |
3 | ///
4 | /// 风控警告对话框的用户操作
5 | ///
6 | public enum RiskWarningAction
7 | {
8 | ///
9 | /// 继续操作(包含风险账号)
10 | ///
11 | Continue,
12 |
13 | ///
14 | /// 排除风险账号后继续
15 | ///
16 | ExcludeRisky
17 | }
18 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IBotChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | public interface IBotChannelRepository : IRepository
6 | {
7 | Task GetByTelegramIdAsync(int botId, long telegramId);
8 | Task> GetForBotAsync(int botId, int? categoryId = null);
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IGroupRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 群组仓储接口
7 | ///
8 | public interface IGroupRepository : IRepository
9 | {
10 | Task GetByTelegramIdAsync(long telegramId);
11 | Task> GetByCreatorAccountAsync(int accountId);
12 | }
13 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IBotChannelCategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | public interface IBotChannelCategoryRepository : IRepository
6 | {
7 | Task> GetForBotAsync(int botId);
8 | Task GetByNameAsync(int botId, string name);
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 文档索引
2 |
3 | 如果你只是想快速跑起来:优先看根目录 `README.md` 的 Docker 一键安装。
4 |
5 | ## 目录
6 |
7 | - `docs/import.md`:压缩包批量导入账号结构(推荐)
8 | - `docs/sync.md`:同步功能说明(同步什么/为什么要同步/自动同步)
9 | - `docs/reverse-proxy.md`:反向代理示例(Nginx/Caddy,含 WebSocket)
10 | - `docs/api.md`:接口速查(面向二次开发)
11 | - `docs/database.md`:数据库与主要表结构(面向排障/扩展)
12 | - `docs/advanced.md`:架构/配置项/环境变量/数据持久化位置等
13 | - `docs/modules.md`:模块系统(可安装/可卸载/版本与依赖/回滚)
14 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Shared/PanelTime.razor:
--------------------------------------------------------------------------------
1 | @inject TelegramPanel.Web.Services.PanelTimeZoneService TimeZone
2 |
3 | @Text
4 |
5 | @code
6 | {
7 | [Parameter] public DateTime? Value { get; set; }
8 | [Parameter] public string Format { get; set; } = "yyyy-MM-dd HH:mm";
9 | [Parameter] public string EmptyText { get; set; } = "-";
10 |
11 | private string Text => TimeZone.Format(Value, Format, EmptyText);
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/ModuleApplicationExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace TelegramPanel.Web.Modules;
5 |
6 | public static class ModuleApplicationExtensions
7 | {
8 | public static void MapInstalledModules(this WebApplication app)
9 | {
10 | var registry = app.Services.GetRequiredService();
11 | registry.MapEndpoints(app);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IBatchTaskRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 批量任务仓储接口
7 | ///
8 | public interface IBatchTaskRepository : IRepository
9 | {
10 | Task> GetByStatusAsync(string status);
11 | Task> GetRunningTasksAsync();
12 | Task> GetRecentTasksAsync(int count = 20);
13 | }
14 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/ChannelGroup.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 频道分组实体
5 | ///
6 | public class ChannelGroup
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; } = null!;
10 | public string? Description { get; set; }
11 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
12 |
13 | // 导航属性
14 | public ICollection Channels { get; set; } = new List();
15 | }
16 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IAccountRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 账号仓储接口
7 | ///
8 | public interface IAccountRepository : IRepository
9 | {
10 | Task GetByPhoneAsync(string phone);
11 | Task GetByUserIdAsync(long userId);
12 | Task> GetByCategoryAsync(int categoryId);
13 | Task> GetActiveAccountsAsync();
14 | }
15 |
--------------------------------------------------------------------------------
/docs/sync.md:
--------------------------------------------------------------------------------
1 | # “同步”功能说明
2 |
3 | ## 同步到底同步什么?
4 |
5 | 同步 = 从 Telegram 拉取并更新本地数据库中的“账号创建的数据”,主要用于**列表展示/筛选/分组/批量操作**:
6 |
7 | - 账号创建的频道(Channel)
8 | - 账号创建的群组(Group)
9 |
10 | 它不是:
11 |
12 | - 手机号登录/收验证码
13 | - 检测账号是否冻结/封禁
14 | - 同步消息/聊天记录
15 |
16 | ## 为什么需要同步?
17 |
18 | 面板的频道/群组列表、分类、批量任务都依赖本地数据库。同步负责把 Telegram 侧的最新信息拉下来并落库。
19 |
20 | ## 自动同步是什么?
21 |
22 | “自动同步”就是定时在后台执行同样的同步逻辑。
23 |
24 | - 默认关闭:避免频繁调用 Telegram API。
25 | - 开启后会写入本地覆盖配置(Docker 下在 `./docker-data/appsettings.local.json`)。
26 |
27 | 如果你不需要自动同步:保持关闭,平时用手动同步即可。
28 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/AdminAuthOptions.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.Services;
2 |
3 | public sealed class AdminAuthOptions
4 | {
5 | public bool Enabled { get; set; }
6 | public string InitialUsername { get; set; } = "admin";
7 | public string InitialPassword { get; set; } = "admin123";
8 | public string CredentialsPath { get; set; } = "admin_auth.json";
9 |
10 | public bool IsConfigured =>
11 | !string.IsNullOrWhiteSpace(InitialUsername) && !string.IsNullOrWhiteSpace(InitialPassword);
12 | }
13 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/AccountCategory.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 账号分类实体
5 | ///
6 | public class AccountCategory
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; } = null!;
10 | public string? Color { get; set; }
11 | public string? Description { get; set; }
12 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
13 |
14 | // 导航属性
15 | public ICollection Accounts { get; set; } = new List();
16 | }
17 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/BatchTasks/BatchTaskTypes.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.BatchTasks;
2 |
3 | ///
4 | /// 批量任务类型常量(数据库中 BatchTask.TaskType 的取值)。
5 | ///
6 | public static class BatchTaskTypes
7 | {
8 | // Bot 任务(现有)
9 | public const string Invite = "invite";
10 | public const string SetAdmin = "set_admin";
11 |
12 | // User 任务(新增)
13 | public const string UserJoinSubscribe = "user_join_subscribe";
14 |
15 | // External API(记录到任务中心)
16 | public const string ExternalApiKick = "external_api_kick";
17 | }
18 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IAccountChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 账号-频道关联仓储接口
7 | ///
8 | public interface IAccountChannelRepository : IRepository
9 | {
10 | Task GetAsync(int accountId, int channelId);
11 | Task UpsertAsync(AccountChannel link);
12 | Task DeleteForAccountExceptAsync(int accountId, IReadOnlyCollection keepChannelIds);
13 | Task GetPreferredAdminAccountIdAsync(int channelId);
14 | }
15 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/BotChannelCategory.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// Bot 频道分类(独立于账号频道分类/分组)
5 | ///
6 | public class BotChannelCategory
7 | {
8 | public int Id { get; set; }
9 | public int BotId { get; set; }
10 | public string Name { get; set; } = null!;
11 | public string? Description { get; set; }
12 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
13 |
14 | public Bot? Bot { get; set; }
15 | public ICollection Channels { get; set; } = new List();
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 通用仓储接口
7 | ///
8 | public interface IRepository where T : class
9 | {
10 | Task GetByIdAsync(int id);
11 | Task> GetAllAsync();
12 | Task> FindAsync(Expression> predicate);
13 | Task AddAsync(T entity);
14 | Task UpdateAsync(T entity);
15 | Task DeleteAsync(T entity);
16 | Task CountAsync(Expression>? predicate = null);
17 | }
18 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/AdminAuthHelpers.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.Services;
2 |
3 | public static class AdminAuthHelpers
4 | {
5 | public static bool IsLocalReturnUrl(string? returnUrl)
6 | {
7 | if (string.IsNullOrWhiteSpace(returnUrl))
8 | return false;
9 |
10 | // 仅允许站内跳转,避免 open redirect
11 | if (!returnUrl.StartsWith("/", StringComparison.Ordinal))
12 | return false;
13 |
14 | if (returnUrl.StartsWith("//", StringComparison.Ordinal))
15 | return false;
16 |
17 | return true;
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Modules.Abstractions/TelegramPanel.Modules.Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | TelegramPanel.Modules
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/import.md:
--------------------------------------------------------------------------------
1 | # 压缩包导入(推荐)
2 |
3 | 支持批量导入账号压缩包:**每个账号一个独立子文件夹**,文件夹内包含一个 `.json` + 一个 `.session`。
4 |
5 | ## 批量导入压缩包结构(示例)
6 |
7 | 例如你要导入 2 个账号:
8 |
9 | ```
10 | accounts.zip
11 | ├─ 8613111111111
12 | │ ├─ 8613111111111.json
13 | │ └─ 8613111111111.session
14 | └─ 8615119714541
15 | ├─ 8615119714541.json
16 | └─ 8615119714541.session
17 | ```
18 |
19 | ### 命名规则
20 |
21 | - 子文件夹名建议使用手机号(或你能区分账号的唯一标识)。
22 | - `.json` 与 `.session` 文件名建议与子文件夹同名(上面的例子就是同名)。
23 | - 每个子文件夹内只要能找到 **1 个 `.json` + 1 个 `.session`** 即可。
24 |
25 | ## Docker 部署下导入的文件会存哪?
26 |
27 | 如果你使用 Docker,所有 session 会写入:
28 |
29 | - `./docker-data/sessions/`
30 |
31 | 不要手工乱改里面的文件名,避免程序找不到 session。
32 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/IChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Data.Repositories;
4 |
5 | ///
6 | /// 频道仓储接口
7 | ///
8 | public interface IChannelRepository : IRepository
9 | {
10 | Task GetByTelegramIdAsync(long telegramId);
11 | Task> GetCreatedAsync();
12 | Task> GetByCreatorAccountAsync(int accountId);
13 | Task> GetForAccountAsync(int accountId, bool includeNonCreator);
14 | Task> GetByGroupAsync(int groupId);
15 | Task> GetBroadcastChannelsAsync();
16 | }
17 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/ChannelAdminInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | public record ChannelAdminInfo(
4 | long UserId,
5 | string? Username,
6 | string? FirstName,
7 | string? LastName,
8 | bool IsCreator,
9 | string? Rank
10 | )
11 | {
12 | public string DisplayName
13 | {
14 | get
15 | {
16 | var full = $"{FirstName} {LastName}".Trim();
17 | if (!string.IsNullOrWhiteSpace(full))
18 | return full;
19 | if (!string.IsNullOrWhiteSpace(Username))
20 | return $"@{Username}";
21 | return UserId.ToString();
22 | }
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/docs/database.md:
--------------------------------------------------------------------------------
1 | # 数据库说明(简版)
2 |
3 | 默认使用 SQLite(Docker 下持久化到 `./docker-data/telegram-panel.db`)。
4 |
5 | 本页只列出核心表的“概念与用途”,避免把 README 写得太劝退;具体字段以 `src/TelegramPanel.Data/Migrations/` 为准。
6 |
7 | ## 核心表
8 |
9 | - `Accounts`:账号信息、分类、最近状态检测结果缓存等
10 | - `Channels`:频道信息(主要是账号创建的频道)与分组/展示字段
11 | - `Groups`:群组信息(主要是账号创建的群组)
12 | - `Bots` / `BotChannels`:机器人与其管理的频道(如果启用机器人管理)
13 | - `BatchTasks`:批量任务(pending/running/completed/failed)
14 | - `TaskLogs`:任务日志(用于任务中心展示与排障)
15 |
16 | ## 常见问题
17 |
18 | ### Docker 下数据库/Session 在哪?
19 |
20 | 统一在 `./docker-data`:
21 |
22 | - `./docker-data/telegram-panel.db`
23 | - `./docker-data/sessions/`
24 |
25 | ### 为什么刷新页面任务还在跑?
26 |
27 | 批量任务由后台服务从数据库拉取并执行,前端只是提交任务与展示进度(见 `BatchTasks`/`TaskLogs`)。
28 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/Group.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 群组实体
5 | ///
6 | public class Group
7 | {
8 | public int Id { get; set; }
9 | public long TelegramId { get; set; }
10 | public long? AccessHash { get; set; }
11 | public string Title { get; set; } = null!;
12 | public string? Username { get; set; }
13 | public int MemberCount { get; set; }
14 | public string? About { get; set; }
15 | public int CreatorAccountId { get; set; }
16 | public DateTime? CreatedAt { get; set; }
17 | public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
18 |
19 | // 导航属性
20 | public Account CreatorAccount { get; set; } = null!;
21 | }
22 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # 接口速查(简版)
2 |
3 | 本页用于二次开发/排查问题时快速定位接口;完整行为以代码为准。
4 |
5 | ## 账号
6 |
7 | - `GET /api/accounts`:获取账号列表
8 | - `GET /api/accounts/{id}`:获取账号详情
9 | - `POST /api/accounts/login`:手机号登录(发送验证码)
10 | - `POST /api/accounts/verify`:提交验证码/2FA
11 | - `POST /api/accounts/import`:导入账号(Session/压缩包)
12 | - `POST /api/accounts/{id}/sync`:同步该账号“创建的频道/群组”
13 | - `DELETE /api/accounts/{id}`:删除账号
14 |
15 | ## 频道/群组
16 |
17 | - `GET /api/channels`:频道列表(筛选)
18 | - `GET /api/channels/{id}`:频道详情
19 | - `POST /api/channels/{id}/admins`:设置管理员
20 | - `POST /api/channels/{id}/invite`:邀请用户/Bot
21 |
22 | ## 任务
23 |
24 | - `GET /api/tasks`:任务列表
25 | - `GET /api/tasks/{id}`:任务详情
26 | - `POST /api/tasks/{id}/cancel`:取消
27 | - `POST /api/tasks/{id}/retry`:重试
28 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/BatchTask.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 批量任务实体
5 | ///
6 | public class BatchTask
7 | {
8 | public int Id { get; set; }
9 | public string TaskType { get; set; } = null!; // invite/set_admin/create_channel等
10 | public string Status { get; set; } = "pending"; // pending/running/completed/failed
11 | public int Total { get; set; }
12 | public int Completed { get; set; }
13 | public int Failed { get; set; }
14 | public string? Config { get; set; } // JSON格式的任务配置
15 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
16 | public DateTime? StartedAt { get; set; }
17 | public DateTime? CompletedAt { get; set; }
18 | }
19 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/GroupInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// 群组信息
5 | ///
6 | public record GroupInfo
7 | {
8 | public int Id { get; init; }
9 | public long TelegramId { get; init; }
10 | public long AccessHash { get; init; }
11 | public string Title { get; init; } = string.Empty;
12 | public string? Username { get; init; }
13 | public int MemberCount { get; init; }
14 | public int CreatorAccountId { get; init; }
15 | public DateTime SyncedAt { get; init; }
16 |
17 | public bool IsPublic => !string.IsNullOrEmpty(Username);
18 |
19 | public string Link => IsPublic
20 | ? $"https://t.me/{Username}"
21 | : $"https://t.me/c/{TelegramId}";
22 | }
23 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "http://localhost:5000",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": true,
17 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/AccountChannel.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 账号-频道关联(用于记录“某账号是某频道的创建者/管理员”)
5 | ///
6 | public class AccountChannel
7 | {
8 | public int Id { get; set; }
9 |
10 | public int AccountId { get; set; }
11 | public int ChannelId { get; set; }
12 |
13 | ///
14 | /// 是否为创建者(拥有者)
15 | ///
16 | public bool IsCreator { get; set; }
17 |
18 | ///
19 | /// 是否为管理员(包含创建者)
20 | ///
21 | public bool IsAdmin { get; set; }
22 |
23 | public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
24 |
25 | public Account Account { get; set; } = null!;
26 | public Channel Channel { get; set; } = null!;
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/Bot.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// Telegram 机器人(Bot)实体
5 | ///
6 | public class Bot
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; } = null!;
10 | public string Token { get; set; } = null!;
11 | public string? Username { get; set; }
12 | public bool IsActive { get; set; } = true;
13 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
14 | public DateTime? LastSyncAt { get; set; }
15 | public long? LastUpdateId { get; set; }
16 |
17 | public ICollection Categories { get; set; } = new List();
18 | public ICollection Channels { get; set; } = new List();
19 | }
20 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using System.Net.Http.Json
3 | @using Microsoft.AspNetCore.Components
4 | @using Microsoft.AspNetCore.Components.Forms
5 | @using Microsoft.AspNetCore.Components.Routing
6 | @using Microsoft.AspNetCore.Components.Web
7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode
8 | @using Microsoft.AspNetCore.Components.Web.Virtualization
9 | @using Microsoft.JSInterop
10 | @using MudBlazor
11 | @using MudBlazor.Utilities
12 | @using TelegramPanel.Core.Interfaces
13 | @using TelegramPanel.Core.Services
14 | @using TelegramPanel.Core.Services.Telegram
15 | @using TelegramPanel.Web.Components
16 | @using TelegramPanel.Web.Components.Shared
17 | @using TelegramPanel.Web.Components.Layout
18 | @using TelegramPanel.Web.Services
19 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/wwwroot/app.js:
--------------------------------------------------------------------------------
1 | window.telegramPanel = window.telegramPanel || {};
2 |
3 | window.telegramPanel.copyText = async (text) => {
4 | if (text === null || text === undefined) return;
5 | const value = String(text);
6 |
7 | if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
8 | await navigator.clipboard.writeText(value);
9 | return;
10 | }
11 |
12 | const textarea = document.createElement("textarea");
13 | textarea.value = value;
14 | textarea.setAttribute("readonly", "true");
15 | textarea.style.position = "fixed";
16 | textarea.style.left = "-9999px";
17 | textarea.style.top = "-9999px";
18 | document.body.appendChild(textarea);
19 | textarea.select();
20 | document.execCommand("copy");
21 | document.body.removeChild(textarea);
22 | };
23 |
24 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/TelegramPanel.Data.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | TelegramPanel.Data
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Interfaces/IGroupService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Core.Models;
2 |
3 | namespace TelegramPanel.Core.Interfaces;
4 |
5 | ///
6 | /// 群组服务接口
7 | ///
8 | public interface IGroupService
9 | {
10 | ///
11 | /// 获取账号创建的所有群组
12 | ///
13 | Task> GetOwnedGroupsAsync(int accountId);
14 |
15 | ///
16 | /// 获取群组详情
17 | ///
18 | Task GetGroupInfoAsync(int accountId, long groupId);
19 |
20 | ///
21 | /// 导出加入链接:公开群组返回 t.me 链接;否则导出邀请链接。
22 | ///
23 | Task ExportJoinLinkAsync(int accountId, long groupId);
24 |
25 | ///
26 | /// 获取群组管理员列表(需要权限)
27 | ///
28 | Task> GetAdminsAsync(int accountId, long groupId);
29 | }
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "DefaultConnection": "Data Source=telegram_panel.db"
4 | },
5 | "Telegram": {
6 | "ApiId": 0,
7 | "ApiHash": "",
8 | "SessionsPath": "sessions",
9 | "DefaultDelayMs": 2000,
10 | "MaxRetries": 3
11 | },
12 | "AdminAuth": {
13 | "Enabled": true,
14 | "InitialUsername": "admin",
15 | "InitialPassword": "admin123",
16 | "CredentialsPath": "admin_auth.json"
17 | },
18 | "Hangfire": {
19 | "DashboardPath": "/hangfire"
20 | },
21 | "Serilog": {
22 | "MinimumLevel": {
23 | "Default": "Information",
24 | "Override": {
25 | "Microsoft": "Warning",
26 | "Microsoft.Hosting.Lifetime": "Information",
27 | "System": "Warning"
28 | }
29 | }
30 | },
31 | "AllowedHosts": "*"
32 | }
33 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/BotChannel.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// Bot 加入的频道(用于管理“非创建者频道”)
5 | ///
6 | public class BotChannel
7 | {
8 | public int Id { get; set; }
9 | public int BotId { get; set; }
10 | public long TelegramId { get; set; }
11 | public long? AccessHash { get; set; }
12 | public string Title { get; set; } = null!;
13 | public string? Username { get; set; }
14 | public bool IsBroadcast { get; set; }
15 | public int MemberCount { get; set; }
16 | public string? About { get; set; }
17 | public DateTime? CreatedAt { get; set; }
18 | public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
19 |
20 | public int? CategoryId { get; set; }
21 |
22 | public Bot? Bot { get; set; }
23 | public BotChannelCategory? Category { get; set; }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using System.Net.Http.Json
3 | @using Microsoft.AspNetCore.Hosting
4 | @using Microsoft.AspNetCore.Components
5 | @using Microsoft.AspNetCore.Components.Forms
6 | @using Microsoft.AspNetCore.Components.Routing
7 | @using Microsoft.AspNetCore.Components.Web
8 | @using static Microsoft.AspNetCore.Components.Web.RenderMode
9 | @using Microsoft.AspNetCore.Components.Web.Virtualization
10 | @using Microsoft.JSInterop
11 | @using MudBlazor
12 | @using MudBlazor.Utilities
13 | @using TelegramPanel.Core.Interfaces
14 | @using TelegramPanel.Core.Services
15 | @using TelegramPanel.Core.Services.Telegram
16 | @using TelegramPanel.Core.Models
17 | @using TelegramPanel.Web.Components
18 | @using TelegramPanel.Web.Components.Shared
19 | @using TelegramPanel.Web.Components.Layout
20 | @using TelegramPanel.Web.Services
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
2 | WORKDIR /src
3 |
4 | COPY . .
5 |
6 | RUN dotnet restore "TelegramPanel.sln"
7 | RUN dotnet publish "src/TelegramPanel.Web/TelegramPanel.Web.csproj" -c Release -o /app/publish --no-restore
8 |
9 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
10 | WORKDIR /app
11 |
12 | ENV ASPNETCORE_URLS=http://+:5000
13 | EXPOSE 5000
14 |
15 | # 持久化目录:/data(通过 docker-compose 挂载)
16 | # - 数据库:/data/telegram-panel.db
17 | # - session:/data/sessions/
18 | # - 本地配置:/data/appsettings.local.json(UI 保存 Telegram ApiId/ApiHash/同步开关等)
19 | # - 后台密码:/data/admin_auth.json
20 | RUN mkdir -p /data /data/sessions /data/logs \
21 | && rm -rf /app/logs \
22 | && ln -s /data/logs /app/logs \
23 | && ln -s /data/appsettings.local.json /app/appsettings.local.json || true
24 |
25 | COPY --from=build /app/publish .
26 |
27 | ENTRYPOINT ["dotnet", "TelegramPanel.Web.dll"]
28 |
29 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/TelegramAuthorizationInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// 在线设备 / 会话信息(来自 account.getAuthorizations)
5 | ///
6 | public record TelegramAuthorizationInfo(
7 | long Hash,
8 | bool Current,
9 | int ApiId,
10 | string? AppName,
11 | string? AppVersion,
12 | string? DeviceModel,
13 | string? Platform,
14 | string? SystemVersion,
15 | string? Ip,
16 | string? Country,
17 | string? Region,
18 | DateTime? CreatedAtUtc,
19 | DateTime? LastActiveAtUtc
20 | )
21 | {
22 | public string Title
23 | {
24 | get
25 | {
26 | var app = string.IsNullOrWhiteSpace(AppName) ? "UnknownApp" : AppName;
27 | var device = string.IsNullOrWhiteSpace(DeviceModel) ? "UnknownDevice" : DeviceModel;
28 | return $"{app} - {device}";
29 | }
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/ChannelGroupRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 频道分组仓储实现
8 | ///
9 | public class ChannelGroupRepository : Repository, IChannelGroupRepository
10 | {
11 | public ChannelGroupRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public override async Task> GetAllAsync()
16 | {
17 | return await _dbSet
18 | .Include(g => g.Channels)
19 | .OrderBy(g => g.Name)
20 | .ToListAsync();
21 | }
22 |
23 | public async Task GetByNameAsync(string name)
24 | {
25 | return await _dbSet
26 | .Include(g => g.Channels)
27 | .FirstOrDefaultAsync(g => g.Name == name);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/AccountCategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 账号分类仓储实现
8 | ///
9 | public class AccountCategoryRepository : Repository, IAccountCategoryRepository
10 | {
11 | public AccountCategoryRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public override async Task> GetAllAsync()
16 | {
17 | return await _dbSet
18 | .Include(c => c.Accounts)
19 | .OrderBy(c => c.Name)
20 | .ToListAsync();
21 | }
22 |
23 | public async Task GetByNameAsync(string name)
24 | {
25 | return await _dbSet
26 | .Include(c => c.Accounts)
27 | .FirstOrDefaultAsync(c => c.Name == name);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/Channel.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 频道实体
5 | ///
6 | public class Channel
7 | {
8 | public int Id { get; set; }
9 | public long TelegramId { get; set; }
10 | public long? AccessHash { get; set; }
11 | public string Title { get; set; } = null!;
12 | public string? Username { get; set; }
13 | public bool IsBroadcast { get; set; }
14 | public int MemberCount { get; set; }
15 | public string? About { get; set; }
16 | public int? CreatorAccountId { get; set; }
17 | public int? GroupId { get; set; }
18 | public DateTime? CreatedAt { get; set; }
19 | public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
20 |
21 | // 导航属性
22 | public Account? CreatorAccount { get; set; }
23 | public ICollection AccountChannels { get; set; } = new List();
24 | public ChannelGroup? Group { get; set; }
25 | }
26 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/ExternalApi/ExternalApiCatalog.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.ExternalApi;
2 |
3 | public static class ExternalApiCatalog
4 | {
5 | public static readonly ExternalApiTypeInfo Kick = new(
6 | Type: ExternalApiTypes.Kick,
7 | DisplayName: "踢人/封禁",
8 | Route: "/api/kick",
9 | ProviderModuleId: "builtin.kick-api");
10 |
11 | public static IReadOnlyList All { get; } = new[]
12 | {
13 | Kick
14 | };
15 |
16 | public static ExternalApiTypeInfo? TryGet(string? type)
17 | {
18 | type = (type ?? string.Empty).Trim();
19 | if (type.Length == 0)
20 | return null;
21 |
22 | return All.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
23 | }
24 | }
25 |
26 | public sealed record ExternalApiTypeInfo(
27 | string Type,
28 | string DisplayName,
29 | string Route,
30 | string ProviderModuleId);
31 |
32 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | telegram-panel:
3 | build:
4 | context: .
5 | dockerfile: ${TP_DOCKERFILE:-Dockerfile}
6 | container_name: telegram-panel
7 | restart: unless-stopped
8 | ports:
9 | - "5000:5000"
10 | volumes:
11 | - ./docker-data:/data
12 | environment:
13 | ASPNETCORE_URLS: "http://+:5000"
14 | DOTNET_ENVIRONMENT: "Production"
15 |
16 | # SQLite 数据库(持久化到 ./docker-data)
17 | ConnectionStrings__DefaultConnection: "Data Source=/data/telegram-panel.db"
18 |
19 | # Sessions(持久化到 ./docker-data/sessions)
20 | Telegram__SessionsPath: "/data/sessions"
21 |
22 | # 后台登录凭据文件(持久化到 ./docker-data/admin_auth.json)
23 | AdminAuth__CredentialsPath: "/data/admin_auth.json"
24 |
25 | # 默认关闭,避免频繁调用 Telegram API;可在系统设置里开启并保存到 /data/appsettings.local.json
26 | Sync__AutoSyncEnabled: "false"
27 |
28 | # 默认关闭 bot 轮询自动同步(需要时可手动同步)
29 | Telegram__BotAutoSyncEnabled: "false"
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Migrations/20251219000002_AddBotLastUpdateId.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Infrastructure;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace TelegramPanel.Data.Migrations
7 | {
8 | [DbContext(typeof(AppDbContext))]
9 | [Migration("20251219000002_AddBotLastUpdateId")]
10 | public partial class AddBotLastUpdateId : Migration
11 | {
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.AddColumn(
15 | name: "LastUpdateId",
16 | table: "Bots",
17 | type: "INTEGER",
18 | nullable: true);
19 | }
20 |
21 | protected override void Down(MigrationBuilder migrationBuilder)
22 | {
23 | migrationBuilder.DropColumn(
24 | name: "LastUpdateId",
25 | table: "Bots");
26 | }
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/BotRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | public class BotRepository : Repository, IBotRepository
7 | {
8 | public BotRepository(AppDbContext context) : base(context)
9 | {
10 | }
11 |
12 | public async Task GetByNameAsync(string name)
13 | {
14 | name = (name ?? string.Empty).Trim();
15 | if (string.IsNullOrWhiteSpace(name))
16 | return null;
17 |
18 | return await _dbSet.FirstOrDefaultAsync(x => x.Name == name);
19 | }
20 |
21 | public async Task> GetAllWithStatsAsync()
22 | {
23 | // 只用于列表显示统计信息,避免一次性加载全部频道详情
24 | return await _dbSet
25 | .Include(x => x.Channels)
26 | .Include(x => x.Categories)
27 | .OrderByDescending(x => x.CreatedAt)
28 | .ToListAsync();
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Modules.Abstractions/ITelegramPanelModule.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Routing;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace TelegramPanel.Modules;
5 |
6 | public interface ITelegramPanelModule
7 | {
8 | ModuleManifest Manifest { get; }
9 |
10 | ///
11 | /// 可选:模块注入自身服务(注意:启用/停用通常需要重启才能生效)。
12 | ///
13 | void ConfigureServices(IServiceCollection services, ModuleHostContext context);
14 |
15 | ///
16 | /// 可选:模块映射 API endpoints。
17 | ///
18 | void MapEndpoints(IEndpointRouteBuilder endpoints, ModuleHostContext context);
19 | }
20 |
21 | public sealed class ModuleHostContext
22 | {
23 | public ModuleHostContext(string hostVersion, string modulesRootPath)
24 | {
25 | HostVersion = hostVersion;
26 | ModulesRootPath = modulesRootPath;
27 | }
28 |
29 | public string HostVersion { get; }
30 | public string ModulesRootPath { get; }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/AccountInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// 账号信息
5 | ///
6 | public record AccountInfo
7 | {
8 | public int Id { get; init; }
9 | public long TelegramUserId { get; init; }
10 | public string? Phone { get; init; }
11 | public string? Username { get; init; }
12 | public string? FirstName { get; init; }
13 | public string? LastName { get; init; }
14 | public string? PhotoPath { get; init; }
15 | public AccountStatus Status { get; init; }
16 | public DateTime? LastActiveAt { get; init; }
17 |
18 | public string DisplayName => string.IsNullOrEmpty(Username)
19 | ? $"{FirstName} {LastName}".Trim()
20 | : $"@{Username}";
21 | }
22 |
23 | ///
24 | /// 账号状态枚举
25 | ///
26 | public enum AccountStatus
27 | {
28 | Active, // 正常
29 | Offline, // 离线
30 | Banned, // 被封禁
31 | Limited, // 受限
32 | NeedRelogin // 需要重新登录
33 | }
34 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/BotChannelCategoryRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | public class BotChannelCategoryRepository : Repository, IBotChannelCategoryRepository
7 | {
8 | public BotChannelCategoryRepository(AppDbContext context) : base(context)
9 | {
10 | }
11 |
12 | public async Task> GetForBotAsync(int botId)
13 | {
14 | return await _dbSet
15 | .Where(x => x.BotId == botId)
16 | .OrderBy(x => x.Name)
17 | .ToListAsync();
18 | }
19 |
20 | public async Task GetByNameAsync(int botId, string name)
21 | {
22 | name = (name ?? string.Empty).Trim();
23 | if (string.IsNullOrWhiteSpace(name))
24 | return null;
25 |
26 | return await _dbSet.FirstOrDefaultAsync(x => x.BotId == botId && x.Name == name);
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/ModuleRegistry.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Routing;
2 | using TelegramPanel.Modules;
3 |
4 | namespace TelegramPanel.Web.Modules;
5 |
6 | public sealed class ModuleRegistry
7 | {
8 | private readonly List _modules = new();
9 |
10 | public IReadOnlyList Modules => _modules;
11 |
12 | public void Add(LoadedModule module) => _modules.Add(module);
13 |
14 | public void MapEndpoints(IEndpointRouteBuilder endpoints)
15 | {
16 | foreach (var m in _modules)
17 | {
18 | try
19 | {
20 | m.Instance.MapEndpoints(endpoints, m.Context);
21 | }
22 | catch
23 | {
24 | // 模块出错不应影响主站启动;日志在上层打
25 | }
26 | }
27 | }
28 | }
29 |
30 | public sealed record LoadedModule(
31 | string Id,
32 | string Version,
33 | bool BuiltIn,
34 | ITelegramPanelModule Instance,
35 | ModuleHostContext Context,
36 | ModuleManifest Manifest,
37 | string? ModuleRootPath);
38 |
--------------------------------------------------------------------------------
/Dockerfile.local:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
2 | WORKDIR /src
3 |
4 | COPY . .
5 |
6 | RUN dotnet restore "TelegramPanel.sln"
7 | RUN dotnet publish "src/TelegramPanel.Web/TelegramPanel.Web.csproj" -c Release -o /app/publish --no-restore
8 |
9 | # 说明:
10 | # - 正常生产部署建议使用 mcr.microsoft.com/dotnet/aspnet:8.0 作为运行时镜像(更小)
11 | # - 若当前网络环境导致 Docker/WSL 无法拉取 aspnet 镜像,可临时使用本文件(运行时使用 sdk 镜像)
12 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS final
13 | WORKDIR /app
14 |
15 | ENV ASPNETCORE_URLS=http://+:5000
16 | EXPOSE 5000
17 |
18 | # 持久化目录:/data(通过 docker-compose 挂载)
19 | # - 数据库:/data/telegram-panel.db
20 | # - session:/data/sessions/
21 | # - 本地配置:/data/appsettings.local.json(UI 保存 Telegram ApiId/ApiHash/同步开关等)
22 | # - 后台密码:/data/admin_auth.json
23 | RUN mkdir -p /data /data/sessions /data/logs \
24 | && rm -rf /app/logs \
25 | && ln -s /data/logs /app/logs \
26 | && ln -s /data/appsettings.local.json /app/appsettings.local.json || true
27 |
28 | COPY --from=build /app/publish .
29 |
30 | ENTRYPOINT ["dotnet", "TelegramPanel.Web.dll"]
31 |
32 |
--------------------------------------------------------------------------------
/docs/advanced.md:
--------------------------------------------------------------------------------
1 | # 进阶说明
2 |
3 | ## 技术栈
4 |
5 | - .NET 8 / ASP.NET Core / Blazor Server
6 | - MudBlazor
7 | - EF Core(默认 SQLite)
8 | - WTelegramClient(MTProto)
9 |
10 | ## Docker 数据目录(强相关)
11 |
12 | `docker-compose.yml` 会把宿主机 `./docker-data` 挂载到容器 `/data`,核心文件包括:
13 |
14 | - `/data/telegram-panel.db`:SQLite 数据库
15 | - `/data/sessions/`:账号 session 文件
16 | - `/data/appsettings.local.json`:UI 保存后的本地覆盖配置
17 | - `/data/admin_auth.json`:后台登录账号/密码(首次会用初始默认值生成)
18 |
19 | ## 后台任务(刷新页面不影响)
20 |
21 | 部分批量任务会在后台静默执行(避免“刷新页面就中断”):
22 |
23 | - 批量邀请
24 | - 批量设置管理员
25 |
26 | ## 账号状态检测(深度探测)
27 |
28 | 为更可靠识别冻结/受限等状态,支持深度探测(例如通过创建/删除测试频道来探测权限)。
29 |
30 | 检测结果会持久化到数据库,避免刷新页面又变回“未检测”。
31 |
32 | ## 配置项速查
33 |
34 | Docker 下常用环境变量(见 `docker-compose.yml`):
35 |
36 | - `ConnectionStrings__DefaultConnection`:SQLite 路径(默认 `/data/telegram-panel.db`)
37 | - `Telegram__SessionsPath`:session 目录(默认 `/data/sessions`)
38 | - `AdminAuth__CredentialsPath`:后台密码文件(默认 `/data/admin_auth.json`)
39 | - `Sync__AutoSyncEnabled`:账号创建的频道/群组自动同步(默认关闭)
40 | - `Telegram__BotAutoSyncEnabled`:Bot 频道轮询自动同步(默认关闭)
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # .NET
2 | bin/
3 | obj/
4 | *.dll
5 | *.exe
6 | *.pdb
7 |
8 | # IDE
9 | .vs/
10 | .vscode/
11 | *.user
12 | *.suo
13 |
14 | # Serena
15 | .serena/
16 |
17 | # Local temp
18 | .tmp/
19 |
20 | # Logs
21 | logs/
22 | *.log
23 |
24 | # Local config overrides (secrets)
25 | appsettings.local.json
26 | src/TelegramPanel.Web/appsettings.local.json
27 |
28 | # Admin auth credentials
29 | src/TelegramPanel.Web/admin_auth.json
30 |
31 | # Sessions
32 | sessions/
33 | session数据/
34 |
35 | # Imported account packages (avoid committing phone/session dumps)
36 | */*.session
37 | */*.json
38 |
39 | # OS
40 | .DS_Store
41 | Thumbs.db
42 |
43 | # SQLite
44 | *.db
45 |
46 | # NuGet
47 | *.nupkg
48 | packages/
49 |
50 | # Build results
51 | [Dd]ebug/
52 | [Rr]elease/
53 | x64/
54 | x86/
55 |
56 | # User-specific files
57 | *.rsuser
58 | *.userosscache
59 | *.sln.docstates
60 |
61 | # Local scratch
62 | todolist
63 |
64 | # Docker persistent data
65 | docker-data/
66 |
67 | # Local build artifacts
68 | artifacts/
69 | 模块源码/
70 | src/TelegramPanel.Web/data-protection-keys/
71 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Migrations/20251220000000_AddLastLoginAt.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Infrastructure;
3 | using Microsoft.EntityFrameworkCore.Migrations;
4 |
5 | #nullable disable
6 |
7 | namespace TelegramPanel.Data.Migrations
8 | {
9 | [DbContext(typeof(AppDbContext))]
10 | [Migration("20251220000000_AddLastLoginAt")]
11 | ///
12 | public partial class AddLastLoginAt : Migration
13 | {
14 | ///
15 | protected override void Up(MigrationBuilder migrationBuilder)
16 | {
17 | migrationBuilder.AddColumn(
18 | name: "LastLoginAt",
19 | table: "Accounts",
20 | type: "TEXT",
21 | nullable: true);
22 | }
23 |
24 | ///
25 | protected override void Down(MigrationBuilder migrationBuilder)
26 | {
27 | migrationBuilder.DropColumn(
28 | name: "LastLoginAt",
29 | table: "Accounts");
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/BotChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | public class BotChannelRepository : Repository, IBotChannelRepository
7 | {
8 | public BotChannelRepository(AppDbContext context) : base(context)
9 | {
10 | }
11 |
12 | public async Task GetByTelegramIdAsync(int botId, long telegramId)
13 | {
14 | return await _dbSet
15 | .Include(x => x.Category)
16 | .FirstOrDefaultAsync(x => x.BotId == botId && x.TelegramId == telegramId);
17 | }
18 |
19 | public async Task> GetForBotAsync(int botId, int? categoryId = null)
20 | {
21 | var query = _dbSet
22 | .Include(x => x.Category)
23 | .Where(x => x.BotId == botId);
24 |
25 | if (categoryId.HasValue)
26 | query = query.Where(x => x.CategoryId == categoryId.Value);
27 |
28 | return await query
29 | .OrderByDescending(x => x.SyncedAt)
30 | .ToListAsync();
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/ExternalApi/ExternalApiModels.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Web.ExternalApi;
2 |
3 | using System.Text.Json.Nodes;
4 |
5 | public static class ExternalApiTypes
6 | {
7 | public const string Kick = "kick";
8 | }
9 |
10 | public sealed class ExternalApiDefinition
11 | {
12 | public string Id { get; set; } = Guid.NewGuid().ToString("N");
13 | public string Name { get; set; } = "";
14 | public string Type { get; set; } = ExternalApiTypes.Kick;
15 | public bool Enabled { get; set; }
16 | public string ApiKey { get; set; } = "";
17 |
18 | ///
19 | /// 模块自定义配置(JSON object)。由具体 API 类型自行解释。
20 | ///
21 | public JsonObject Config { get; set; } = new();
22 |
23 | ///
24 | /// 兼容内置 kick 的强类型配置(建议同时写入 Config)。
25 | ///
26 | public KickApiDefinition? Kick { get; set; } = new();
27 | }
28 |
29 | public sealed class KickApiDefinition
30 | {
31 | public int BotId { get; set; } // 0=all bots
32 | public bool UseAllChats { get; set; } = true;
33 | public List ChatIds { get; set; } = new();
34 | public bool PermanentBanDefault { get; set; }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/TelegramPanel.Web.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | TelegramPanel.Web
8 |
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/TelegramPanel.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | TelegramPanel.Core
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/BuiltIn/BuiltInModuleCatalog.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Modules;
2 |
3 | namespace TelegramPanel.Web.Modules.BuiltIn;
4 |
5 | public sealed class BuiltInModuleCatalog
6 | {
7 | private readonly List _modules;
8 | private readonly Dictionary _manifestById;
9 |
10 | public BuiltInModuleCatalog(string hostVersion)
11 | {
12 | _modules = new List
13 | {
14 | new KickApiModule(hostVersion),
15 | new TaskCatalogModule(hostVersion),
16 | };
17 |
18 | _manifestById = _modules
19 | .Select(m => m.Manifest)
20 | .ToDictionary(m => m.Id, m => m, StringComparer.Ordinal);
21 | }
22 |
23 | public IReadOnlyList CreateModules() => _modules;
24 |
25 | public bool TryGetManifest(string id, out ModuleManifest manifest)
26 | {
27 | id = (id ?? string.Empty).Trim();
28 | if (id.Length == 0)
29 | {
30 | manifest = new ModuleManifest();
31 | return false;
32 | }
33 |
34 | return _manifestById.TryGetValue(id, out manifest!);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Interfaces/ITelegramClientPool.cs:
--------------------------------------------------------------------------------
1 | using WTelegram;
2 |
3 | namespace TelegramPanel.Core.Interfaces;
4 |
5 | ///
6 | /// Telegram客户端池接口
7 | /// 管理多个Telegram账号的客户端实例
8 | ///
9 | public interface ITelegramClientPool
10 | {
11 | ///
12 | /// 获取或创建指定账号的客户端
13 | ///
14 | Task GetOrCreateClientAsync(
15 | int accountId,
16 | int apiId,
17 | string apiHash,
18 | string sessionPath,
19 | string? sessionKey = null,
20 | string? phoneNumber = null,
21 | long? userId = null);
22 |
23 | ///
24 | /// 获取已存在的客户端
25 | ///
26 | Client? GetClient(int accountId);
27 |
28 | ///
29 | /// 移除并断开客户端连接
30 | ///
31 | Task RemoveClientAsync(int accountId);
32 |
33 | ///
34 | /// 移除并断开所有客户端连接(用于配置变更后强制重建)
35 | ///
36 | Task RemoveAllClientsAsync();
37 |
38 | ///
39 | /// 获取所有活跃的客户端数量
40 | ///
41 | int ActiveClientCount { get; }
42 |
43 | ///
44 | /// 检查客户端是否已连接
45 | ///
46 | bool IsClientConnected(int accountId);
47 | }
48 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 批量任务仓储实现
8 | ///
9 | public class BatchTaskRepository : Repository, IBatchTaskRepository
10 | {
11 | public BatchTaskRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public async Task> GetByStatusAsync(string status)
16 | {
17 | return await _dbSet
18 | .Where(t => t.Status == status)
19 | .OrderByDescending(t => t.CreatedAt)
20 | .ToListAsync();
21 | }
22 |
23 | public async Task> GetRunningTasksAsync()
24 | {
25 | return await _dbSet
26 | .Where(t => t.Status == "running")
27 | .OrderBy(t => t.StartedAt)
28 | .ToListAsync();
29 | }
30 |
31 | public async Task> GetRecentTasksAsync(int count = 20)
32 | {
33 | return await _dbSet
34 | .OrderByDescending(t => t.CreatedAt)
35 | .Take(count)
36 | .ToListAsync();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/docs/reverse-proxy.md:
--------------------------------------------------------------------------------
1 | # 反向代理(Nginx/Caddy)
2 |
3 | Telegram Panel 是 **Blazor Server**,需要 WebSocket(`/_blazor`)。
4 |
5 | 如果你反代后出现页面卡住/断开/一直重连,九成是 WebSocket 没配对。
6 |
7 | 另外,如果你遇到「打开页面被跳到 `http://localhost/login?ReturnUrl=%2F`」这种情况,说明反代没有把正确的 `Host`/`Proto` 透传给上游应用。
8 |
9 | ## Nginx(HTTP)示例
10 |
11 | 注意:请确保你的上游是 `http://127.0.0.1:5000`(对应 `docker-compose.yml` 暴露端口)。
12 |
13 | ```nginx
14 | server {
15 | listen 80;
16 | server_name example.com;
17 |
18 | location / {
19 | proxy_pass http://127.0.0.1:5000;
20 | proxy_http_version 1.1;
21 | proxy_set_header Upgrade $http_upgrade;
22 | proxy_set_header Connection "Upgrade";
23 | proxy_set_header Host $host;
24 | proxy_set_header X-Forwarded-Host $host;
25 | proxy_set_header X-Forwarded-Proto $scheme;
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27 |
28 | proxy_read_timeout 3600;
29 | proxy_send_timeout 3600;
30 | }
31 | }
32 | ```
33 |
34 | ## Caddy 示例
35 |
36 | ```caddy
37 | example.com {
38 | reverse_proxy 127.0.0.1:5000
39 | }
40 | ```
41 |
42 | 如果你使用了 CDN/面板(例如 Cloudflare),也要确认它对 WebSocket 的支持与超时设置。
43 |
44 | ## 宝塔面板(BT)反向代理要点
45 |
46 | - 反代目标:`http://127.0.0.1:5000`
47 | - 需要透传:`Host` + `X-Forwarded-Host` + `X-Forwarded-Proto`(否则可能跳到 `localhost`)
48 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Migrations/20251219000004_AccountUserIdIndexNotUnique.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Infrastructure;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace TelegramPanel.Data.Migrations
7 | {
8 | [DbContext(typeof(AppDbContext))]
9 | [Migration("20251219000004_AccountUserIdIndexNotUnique")]
10 | public partial class AccountUserIdIndexNotUnique : Migration
11 | {
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | // 兼容:某些历史库可能没有该索引(例如曾用 EnsureCreated 创建过不同 schema)
15 | migrationBuilder.Sql("DROP INDEX IF EXISTS IX_Accounts_UserId;");
16 |
17 | migrationBuilder.CreateIndex(
18 | name: "IX_Accounts_UserId",
19 | table: "Accounts",
20 | column: "UserId");
21 | }
22 |
23 | protected override void Down(MigrationBuilder migrationBuilder)
24 | {
25 | migrationBuilder.Sql("DROP INDEX IF EXISTS IX_Accounts_UserId;");
26 |
27 | migrationBuilder.CreateIndex(
28 | name: "IX_Accounts_UserId",
29 | table: "Accounts",
30 | column: "UserId",
31 | unique: true);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Interfaces/ISessionImporter.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Core.Models;
2 |
3 | namespace TelegramPanel.Core.Interfaces;
4 |
5 | ///
6 | /// Session导入服务接口
7 | ///
8 | public interface ISessionImporter
9 | {
10 | ///
11 | /// 从Session文件导入
12 | ///
13 | Task ImportFromSessionFileAsync(
14 | string filePath,
15 | int apiId,
16 | string apiHash,
17 | long? userId = null,
18 | string? phoneHint = null);
19 |
20 | ///
21 | /// 批量导入Session文件
22 | ///
23 | Task> BatchImportSessionFilesAsync(
24 | string[] filePaths,
25 | int apiId,
26 | string apiHash);
27 |
28 | ///
29 | /// 从StringSession导入
30 | ///
31 | Task ImportFromStringSessionAsync(string sessionString, int apiId, string apiHash);
32 |
33 | ///
34 | /// 验证Session是否有效
35 | ///
36 | Task ValidateSessionAsync(string sessionPath);
37 | }
38 |
39 | ///
40 | /// 导入结果
41 | ///
42 | public record ImportResult(
43 | bool Success,
44 | string? Phone,
45 | long? UserId,
46 | string? Username,
47 | string? SessionPath,
48 | string? Error = null
49 | );
50 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/AppRestartService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Hosting;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace TelegramPanel.Web.Services;
5 |
6 | public sealed class AppRestartService
7 | {
8 | private readonly IHostApplicationLifetime _lifetime;
9 | private readonly ILogger _logger;
10 | private int _requested;
11 |
12 | public AppRestartService(IHostApplicationLifetime lifetime, ILogger logger)
13 | {
14 | _lifetime = lifetime;
15 | _logger = logger;
16 | }
17 |
18 | public bool RestartPending => Volatile.Read(ref _requested) != 0;
19 |
20 | public void RequestRestart(TimeSpan? delay = null, string? reason = null)
21 | {
22 | if (Interlocked.Exchange(ref _requested, 1) != 0)
23 | return;
24 |
25 | delay ??= TimeSpan.FromSeconds(1);
26 | _logger.LogWarning("Restart requested: {Reason}. Stop in {DelayMs}ms", reason ?? "-", (int)delay.Value.TotalMilliseconds);
27 |
28 | _ = Task.Run(async () =>
29 | {
30 | try
31 | {
32 | await Task.Delay(delay.Value);
33 | }
34 | catch
35 | {
36 | // ignore
37 | }
38 |
39 | _lifetime.StopApplication();
40 | });
41 | }
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/GroupRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 群组仓储实现
8 | ///
9 | public class GroupRepository : Repository, IGroupRepository
10 | {
11 | public GroupRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public override async Task GetByIdAsync(int id)
16 | {
17 | return await _dbSet
18 | .Include(g => g.CreatorAccount)
19 | .FirstOrDefaultAsync(g => g.Id == id);
20 | }
21 |
22 | public override async Task> GetAllAsync()
23 | {
24 | return await _dbSet
25 | .Include(g => g.CreatorAccount)
26 | .OrderByDescending(g => g.SyncedAt)
27 | .ToListAsync();
28 | }
29 |
30 | public async Task GetByTelegramIdAsync(long telegramId)
31 | {
32 | return await _dbSet
33 | .Include(g => g.CreatorAccount)
34 | .FirstOrDefaultAsync(g => g.TelegramId == telegramId);
35 | }
36 |
37 | public async Task> GetByCreatorAccountAsync(int accountId)
38 | {
39 | return await _dbSet
40 | .Include(g => g.CreatorAccount)
41 | .Where(g => g.CreatorAccountId == accountId)
42 | .OrderByDescending(g => g.SyncedAt)
43 | .ToListAsync();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/ChannelInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// 频道信息
5 | ///
6 | public record ChannelInfo
7 | {
8 | public int Id { get; init; }
9 | public long TelegramId { get; init; }
10 | public long AccessHash { get; init; }
11 | public string Title { get; init; } = string.Empty;
12 | public string? Username { get; init; }
13 | public bool IsPublic => !string.IsNullOrEmpty(Username);
14 | public bool IsBroadcast { get; init; } // true=频道, false=超级群组
15 | public int MemberCount { get; init; }
16 | public string? About { get; init; }
17 | ///
18 | /// 系统内“创建该频道”的账号(仅当本系统创建过该频道时才有值)
19 | ///
20 | public int? CreatorAccountId { get; init; }
21 | ///
22 | /// 对于当前 accountId:是否为频道创建者(拥有者)
23 | ///
24 | public bool IsCreator { get; init; }
25 | ///
26 | /// 对于当前 accountId:是否为管理员(包含创建者)
27 | ///
28 | public bool IsAdmin { get; init; }
29 | public int? GroupId { get; init; }
30 | public string? GroupName { get; init; }
31 | public DateTime? CreatedAt { get; init; }
32 | public DateTime SyncedAt { get; init; }
33 |
34 | ///
35 | /// 频道链接
36 | ///
37 | public string Link => IsPublic
38 | ? $"https://t.me/{Username}"
39 | : $"https://t.me/c/{TelegramId}";
40 |
41 | ///
42 | /// 频道类型显示名称
43 | ///
44 | public string TypeName => IsBroadcast ? "频道" : "超级群组";
45 | }
46 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/BatchTaskInfo.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Core.Models;
2 |
3 | ///
4 | /// 批量任务信息
5 | ///
6 | public record BatchTaskInfo
7 | {
8 | public int Id { get; init; }
9 | public string Type { get; init; } = string.Empty;
10 | public string Name { get; init; } = string.Empty;
11 | public TaskStatus Status { get; init; }
12 | public int Progress { get; init; }
13 | public int Total { get; init; }
14 | public int SuccessCount { get; init; }
15 | public int FailedCount { get; init; }
16 | public string? ErrorMessage { get; init; }
17 | public DateTime CreatedAt { get; init; }
18 | public DateTime? StartedAt { get; init; }
19 | public DateTime? CompletedAt { get; init; }
20 |
21 | public double ProgressPercent => Total > 0 ? (double)Progress / Total * 100 : 0;
22 | public TimeSpan? Duration => CompletedAt.HasValue && StartedAt.HasValue
23 | ? CompletedAt.Value - StartedAt.Value
24 | : null;
25 | }
26 |
27 | ///
28 | /// 任务状态
29 | ///
30 | public enum TaskStatus
31 | {
32 | Pending, // 等待中
33 | Running, // 执行中
34 | Completed, // 已完成
35 | Failed, // 失败
36 | Cancelled // 已取消
37 | }
38 |
39 | ///
40 | /// 任务类型
41 | ///
42 | public static class TaskTypes
43 | {
44 | public const string InviteUsers = "invite_users";
45 | public const string SetAdmins = "set_admins";
46 | public const string CreateChannel = "create_channel";
47 | public const string SyncData = "sync_data";
48 | }
49 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Services/ChannelGroupManagementService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 | using TelegramPanel.Data.Repositories;
3 |
4 | namespace TelegramPanel.Core.Services;
5 |
6 | ///
7 | /// 频道分组管理服务
8 | ///
9 | public class ChannelGroupManagementService
10 | {
11 | private readonly IChannelGroupRepository _groupRepository;
12 |
13 | public ChannelGroupManagementService(IChannelGroupRepository groupRepository)
14 | {
15 | _groupRepository = groupRepository;
16 | }
17 |
18 | public async Task> GetAllGroupsAsync()
19 | {
20 | return await _groupRepository.GetAllAsync();
21 | }
22 |
23 | public async Task GetGroupAsync(int id)
24 | {
25 | return await _groupRepository.GetByIdAsync(id);
26 | }
27 |
28 | public async Task GetGroupByNameAsync(string name)
29 | {
30 | return await _groupRepository.GetByNameAsync(name);
31 | }
32 |
33 | public async Task CreateGroupAsync(ChannelGroup group)
34 | {
35 | return await _groupRepository.AddAsync(group);
36 | }
37 |
38 | public async Task UpdateGroupAsync(ChannelGroup group)
39 | {
40 | await _groupRepository.UpdateAsync(group);
41 | }
42 |
43 | public async Task DeleteGroupAsync(int id)
44 | {
45 | var group = await _groupRepository.GetByIdAsync(id);
46 | if (group != null)
47 | {
48 | await _groupRepository.DeleteAsync(group);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/BuiltIn/TaskCatalogModule.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Core.BatchTasks;
2 | using TelegramPanel.Modules;
3 | using MudBlazor;
4 |
5 | namespace TelegramPanel.Web.Modules.BuiltIn;
6 |
7 | public sealed class TaskCatalogModule : ITelegramPanelModule, IModuleTaskProvider
8 | {
9 | public TaskCatalogModule(string version)
10 | {
11 | Manifest = new ModuleManifest
12 | {
13 | Id = "builtin.tasks",
14 | Name = "任务:内置批量任务",
15 | Version = version,
16 | Host = new HostCompatibility(),
17 | Entry = new ModuleEntryPoint { Assembly = "", Type = typeof(TaskCatalogModule).FullName ?? "" }
18 | };
19 | }
20 |
21 | public ModuleManifest Manifest { get; }
22 |
23 | public void ConfigureServices(IServiceCollection services, ModuleHostContext context)
24 | {
25 | // 内置任务的执行由宿主 BatchTaskBackgroundService 负责;这里只提供“元数据”用于 UI 展示与创建。
26 | }
27 |
28 | public void MapEndpoints(IEndpointRouteBuilder endpoints, ModuleHostContext context)
29 | {
30 | // 无 endpoints
31 | }
32 |
33 | public IEnumerable GetTasks(ModuleHostContext context)
34 | {
35 | yield return new ModuleTaskDefinition
36 | {
37 | Category = "system",
38 | TaskType = BatchTaskTypes.ExternalApiKick,
39 | DisplayName = "外部 API:踢人/封禁",
40 | Description = "由外部接口触发并记录到任务中心(一般无需手动创建)。",
41 | Icon = Icons.Material.Filled.Link,
42 | Order = 1000
43 | };
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Modules.Abstractions/ModuleManifest.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace TelegramPanel.Modules;
4 |
5 | public sealed class ModuleManifest
6 | {
7 | [JsonPropertyName("id")]
8 | public string Id { get; set; } = "";
9 |
10 | [JsonPropertyName("name")]
11 | public string Name { get; set; } = "";
12 |
13 | [JsonPropertyName("version")]
14 | public string Version { get; set; } = "0.0.0";
15 |
16 | [JsonPropertyName("host")]
17 | public HostCompatibility Host { get; set; } = new();
18 |
19 | [JsonPropertyName("dependencies")]
20 | public List Dependencies { get; set; } = new();
21 |
22 | [JsonPropertyName("entry")]
23 | public ModuleEntryPoint Entry { get; set; } = new();
24 | }
25 |
26 | public sealed class HostCompatibility
27 | {
28 | [JsonPropertyName("min")]
29 | public string? Min { get; set; }
30 |
31 | [JsonPropertyName("max")]
32 | public string? Max { get; set; }
33 | }
34 |
35 | public sealed class ModuleDependency
36 | {
37 | [JsonPropertyName("id")]
38 | public string Id { get; set; } = "";
39 |
40 | ///
41 | /// 版本范围表达式,支持:
42 | /// - 1.2.3(等于)
43 | /// - >=1.2.3
44 | /// - >=1.2.3 <2.0.0(空格分隔多个条件)
45 | ///
46 | [JsonPropertyName("range")]
47 | public string Range { get; set; } = "";
48 | }
49 |
50 | public sealed class ModuleEntryPoint
51 | {
52 | [JsonPropertyName("assembly")]
53 | public string Assembly { get; set; } = "";
54 |
55 | [JsonPropertyName("type")]
56 | public string Type { get; set; } = "";
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/ExternalApiCurlSampleDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @inject ISnackbar Snackbar
3 | @inject IJSRuntime JS
4 |
5 |
6 |
7 |
8 |
9 | 该 URL 来自你当前访问面板的地址(包含真实域名/端口)。点击「复制」会复制完整命令(不会截断 X-API-Key)。
10 |
11 |
12 |
14 |
15 |
16 |
17 | 复制
18 | 关闭
19 |
20 |
21 |
22 | @code
23 | {
24 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
25 |
26 | [Parameter] public string Curl { get; set; } = "";
27 |
28 | private async Task Copy()
29 | {
30 | try
31 | {
32 | await JS.InvokeVoidAsync("telegramPanel.copyText", Curl);
33 | Snackbar.Add("已复制", Severity.Success);
34 | }
35 | catch (Exception ex)
36 | {
37 | Snackbar.Add($"复制失败:{ex.Message}", Severity.Error);
38 | }
39 | }
40 |
41 | private void Close() => MudDialog.Close();
42 | }
43 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/BotBatchSetCategoryDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using TelegramPanel.Data.Entities
3 |
4 |
5 |
6 |
7 |
8 | 将为 @SelectedCount 个频道设置分类
9 |
10 |
11 |
12 | 未分类
13 | @foreach (var category in Categories)
14 | {
15 | @category.Name
16 | }
17 |
18 |
19 |
20 |
21 | 取消
22 |
23 | 确定
24 |
25 |
26 |
27 |
28 | @code
29 | {
30 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
31 | [Parameter] public List Categories { get; set; } = new();
32 | [Parameter] public int SelectedCount { get; set; }
33 |
34 | private int selectedCategoryId = 0;
35 |
36 | private void Cancel()
37 | {
38 | MudDialog.Cancel();
39 | }
40 |
41 | private void Confirm()
42 | {
43 | MudDialog.Close(DialogResult.Ok(selectedCategoryId));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Services/AccountCategoryManagementService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 | using TelegramPanel.Data.Repositories;
3 |
4 | namespace TelegramPanel.Core.Services;
5 |
6 | ///
7 | /// 账号分类管理服务
8 | ///
9 | public class AccountCategoryManagementService
10 | {
11 | private readonly IAccountCategoryRepository _categoryRepository;
12 |
13 | public AccountCategoryManagementService(IAccountCategoryRepository categoryRepository)
14 | {
15 | _categoryRepository = categoryRepository;
16 | }
17 |
18 | public async Task> GetAllCategoriesAsync()
19 | {
20 | return await _categoryRepository.GetAllAsync();
21 | }
22 |
23 | public async Task GetCategoryAsync(int id)
24 | {
25 | return await _categoryRepository.GetByIdAsync(id);
26 | }
27 |
28 | public async Task GetCategoryByNameAsync(string name)
29 | {
30 | return await _categoryRepository.GetByNameAsync(name);
31 | }
32 |
33 | public async Task CreateCategoryAsync(AccountCategory category)
34 | {
35 | return await _categoryRepository.AddAsync(category);
36 | }
37 |
38 | public async Task UpdateCategoryAsync(AccountCategory category)
39 | {
40 | await _categoryRepository.UpdateAsync(category);
41 | }
42 |
43 | public async Task DeleteCategoryAsync(int id)
44 | {
45 | var category = await _categoryRepository.GetByIdAsync(id);
46 | if (category != null)
47 | {
48 | await _categoryRepository.DeleteAsync(category);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/BatchSetAccountCategoryDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using TelegramPanel.Data.Entities
3 |
4 |
5 |
6 |
7 |
8 | 将为 @SelectedCount 个账号设置分类
9 |
10 |
11 |
12 | 未分类
13 | @foreach (var category in Categories)
14 | {
15 | @category.Name
16 | }
17 |
18 |
19 |
20 |
21 | 取消
22 |
23 | 确定
24 |
25 |
26 |
27 |
28 | @code
29 | {
30 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
31 | [Parameter] public List Categories { get; set; } = new();
32 | [Parameter] public int SelectedCount { get; set; }
33 |
34 | private int selectedCategoryId = 0;
35 |
36 | private void Cancel()
37 | {
38 | MudDialog.Cancel();
39 | }
40 |
41 | private void Confirm()
42 | {
43 | MudDialog.Close(DialogResult.Ok(selectedCategoryId));
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Models/TelegramAccountStatusResult.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 |
3 | namespace TelegramPanel.Core.Models;
4 |
5 | ///
6 | /// 账号状态检测结果(用于诊断账号是否可正常连通 Telegram)
7 | ///
8 | public record TelegramAccountStatusResult(
9 | bool Ok,
10 | string Summary,
11 | string? Details,
12 | DateTime CheckedAtUtc,
13 | TelegramAccountProfile? Profile = null
14 | );
15 |
16 | ///
17 | /// Telegram 账号资料快照
18 | ///
19 | public record TelegramAccountProfile(
20 | long UserId,
21 | string? Phone,
22 | string? Username,
23 | string? FirstName,
24 | string? LastName,
25 | bool IsDeleted,
26 | bool IsScam,
27 | bool IsFake,
28 | bool IsRestricted,
29 | bool IsVerified,
30 | bool IsPremium
31 | )
32 | {
33 | public string DisplayName
34 | {
35 | get
36 | {
37 | var full = $"{FirstName} {LastName}".Trim();
38 | if (!string.IsNullOrWhiteSpace(full))
39 | return full;
40 | if (!string.IsNullOrWhiteSpace(Username))
41 | return $"@{Username}";
42 | if (!string.IsNullOrWhiteSpace(Phone))
43 | return Phone;
44 | return UserId.ToString();
45 | }
46 | }
47 |
48 | public void ApplyTo(Account account)
49 | {
50 | if (account.UserId <= 0 && UserId > 0)
51 | account.UserId = UserId;
52 |
53 | if (!string.IsNullOrWhiteSpace(Username))
54 | account.Username = Username;
55 |
56 | var nickname = DisplayName;
57 | if (!string.IsNullOrWhiteSpace(nickname))
58 | account.Nickname = nickname;
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using TelegramPanel.Data.Repositories;
4 |
5 | namespace TelegramPanel.Data;
6 |
7 | ///
8 | /// Data层服务注册扩展
9 | ///
10 | public static class ServiceCollectionExtensions
11 | {
12 | public static IServiceCollection AddTelegramPanelData(this IServiceCollection services, string connectionString)
13 | {
14 | // 注册数据库上下文
15 | services.AddDbContext(options =>
16 | options.UseSqlite(connectionString, sqlite =>
17 | {
18 | // 避免 Include 多个集合导航时的“笛卡尔爆炸”与性能警告
19 | sqlite.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
20 | }));
21 |
22 | // 注册所有Repository
23 | services.AddScoped();
24 | services.AddScoped();
25 | services.AddScoped();
26 | services.AddScoped();
27 | services.AddScoped();
28 | services.AddScoped();
29 | services.AddScoped();
30 | services.AddScoped();
31 | services.AddScoped();
32 | services.AddScoped();
33 | services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
34 |
35 | return services;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Utils/PhoneNumberFormatter.cs:
--------------------------------------------------------------------------------
1 | using PhoneNumbers;
2 |
3 | namespace TelegramPanel.Core.Utils;
4 |
5 | ///
6 | /// 手机号格式化/规范化工具(用于展示国家码)
7 | ///
8 | public static class PhoneNumberFormatter
9 | {
10 | private static readonly PhoneNumberUtil Util = PhoneNumberUtil.GetInstance();
11 |
12 | ///
13 | /// 仅保留数字(用于数据库存储/查询/文件名)
14 | ///
15 | public static string NormalizeToDigits(string? phone)
16 | {
17 | if (string.IsNullOrWhiteSpace(phone))
18 | return string.Empty;
19 |
20 | var digits = new char[phone.Length];
21 | var count = 0;
22 | foreach (var ch in phone)
23 | {
24 | if (ch >= '0' && ch <= '9')
25 | digits[count++] = ch;
26 | }
27 |
28 | return count == 0 ? string.Empty : new string(digits, 0, count);
29 | }
30 |
31 | ///
32 | /// 格式化为 “+国家码 空格 本地号码” 的展示形式,如:+86 13800138000
33 | ///
34 | public static string FormatWithCountryCode(string? phone)
35 | {
36 | var digits = NormalizeToDigits(phone);
37 | if (digits.Length == 0)
38 | return (phone ?? string.Empty).Trim();
39 |
40 | try
41 | {
42 | // Telegram 账号手机号通常是 E.164 数字串(无 +),这里统一按 E.164 解析
43 | var number = Util.Parse("+" + digits, defaultRegion: null);
44 | var nsn = Util.GetNationalSignificantNumber(number);
45 | if (string.IsNullOrWhiteSpace(nsn))
46 | return "+" + digits;
47 |
48 | return $"+{number.CountryCode} {nsn}";
49 | }
50 | catch
51 | {
52 | // 解析失败也至少补上 "+"
53 | return "+" + digits;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/Repository.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 通用仓储实现
8 | ///
9 | public class Repository : IRepository where T : class
10 | {
11 | protected readonly AppDbContext _context;
12 | protected readonly DbSet _dbSet;
13 |
14 | public Repository(AppDbContext context)
15 | {
16 | _context = context;
17 | _dbSet = context.Set();
18 | }
19 |
20 | public virtual async Task GetByIdAsync(int id)
21 | {
22 | return await _dbSet.FindAsync(id);
23 | }
24 |
25 | public virtual async Task> GetAllAsync()
26 | {
27 | return await _dbSet.ToListAsync();
28 | }
29 |
30 | public virtual async Task> FindAsync(Expression> predicate)
31 | {
32 | return await _dbSet.Where(predicate).ToListAsync();
33 | }
34 |
35 | public virtual async Task AddAsync(T entity)
36 | {
37 | await _dbSet.AddAsync(entity);
38 | await _context.SaveChangesAsync();
39 | return entity;
40 | }
41 |
42 | public virtual async Task UpdateAsync(T entity)
43 | {
44 | _dbSet.Update(entity);
45 | await _context.SaveChangesAsync();
46 | }
47 |
48 | public virtual async Task DeleteAsync(T entity)
49 | {
50 | _dbSet.Remove(entity);
51 | await _context.SaveChangesAsync();
52 | }
53 |
54 | public virtual async Task CountAsync(Expression>? predicate = null)
55 | {
56 | return predicate == null
57 | ? await _dbSet.CountAsync()
58 | : await _dbSet.CountAsync(predicate);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TelegramPanel.Core.Interfaces;
3 | using TelegramPanel.Core.Services;
4 | using TelegramPanel.Core.Services.Telegram;
5 |
6 | namespace TelegramPanel.Core;
7 |
8 | ///
9 | /// Core层服务注册扩展
10 | ///
11 | public static class ServiceCollectionExtensions
12 | {
13 | public static IServiceCollection AddTelegramPanelCore(this IServiceCollection services)
14 | {
15 | // 注册 Telegram 客户端池(单例)
16 | services.AddSingleton();
17 |
18 | // 注册 Telegram 操作服务
19 | services.AddScoped();
20 | services.AddScoped();
21 | services.AddScoped();
22 | services.AddScoped();
23 | services.AddScoped();
24 | services.AddScoped();
25 |
26 | // Bot API updates(getUpdates)统一轮询与分发:避免 409 Conflict
27 | services.AddSingleton();
28 |
29 | // 注册账号导入协调服务
30 | services.AddScoped();
31 |
32 | // 注册数据管理服务
33 | services.AddScoped();
34 | services.AddScoped();
35 | services.AddScoped();
36 | services.AddScoped();
37 | services.AddScoped();
38 | services.AddScoped();
39 | services.AddScoped();
40 |
41 | // 注册风控服务
42 | services.AddScoped();
43 |
44 | return services;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/BuiltIn/KickApiModule.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using TelegramPanel.Modules;
6 | using TelegramPanel.Web.ExternalApi;
7 |
8 | namespace TelegramPanel.Web.Modules.BuiltIn;
9 |
10 | public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider
11 | {
12 | public KickApiModule(string version)
13 | {
14 | Manifest = new ModuleManifest
15 | {
16 | Id = "builtin.kick-api",
17 | Name = "外部 API:踢人/封禁",
18 | Version = version,
19 | Host = new HostCompatibility(),
20 | Entry = new ModuleEntryPoint { Assembly = "", Type = typeof(KickApiModule).FullName ?? "" }
21 | };
22 | }
23 |
24 | public ModuleManifest Manifest { get; }
25 |
26 | public void ConfigureServices(IServiceCollection services, ModuleHostContext context)
27 | {
28 | // built-in:无需额外注入
29 | }
30 |
31 | public void MapEndpoints(IEndpointRouteBuilder endpoints, ModuleHostContext context)
32 | {
33 | if (!endpoints.ServiceProvider.GetRequiredService().GetValue("ExternalApi:Enabled", true))
34 | {
35 | // 预留开关;默认开启
36 | }
37 |
38 | endpoints.MapKickApi();
39 | }
40 |
41 | public IEnumerable GetApis(ModuleHostContext context)
42 | {
43 | yield return new ModuleApiTypeDefinition
44 | {
45 | Type = ExternalApiTypes.Kick,
46 | DisplayName = "踢人/封禁",
47 | Route = "/api/kick",
48 | Description = "从配置的 Bot 管理的频道/群组中踢出或封禁指定用户(按 X-API-Key 匹配配置项)。",
49 | Order = 10
50 | };
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Interfaces/IAccountService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Core.Models;
2 |
3 | namespace TelegramPanel.Core.Interfaces;
4 |
5 | ///
6 | /// 账号服务接口
7 | ///
8 | public interface IAccountService
9 | {
10 | ///
11 | /// 发起手机号登录(发送验证码)
12 | ///
13 | Task StartLoginAsync(int accountId, string phone);
14 |
15 | ///
16 | /// 提交验证码完成登录
17 | ///
18 | Task SubmitCodeAsync(int accountId, string code);
19 |
20 | ///
21 | /// 重新发送验证码(可能切换到短信/电话等其它通道,取决于 Telegram 策略)
22 | ///
23 | Task ResendCodeAsync(int accountId);
24 |
25 | ///
26 | /// 提交两步验证密码
27 | ///
28 | Task SubmitPasswordAsync(int accountId, string password);
29 |
30 | ///
31 | /// 获取账号信息
32 | ///
33 | Task GetAccountInfoAsync(int accountId);
34 |
35 | ///
36 | /// 同步账号数据(频道、群组)
37 | ///
38 | Task SyncAccountDataAsync(int accountId);
39 |
40 | ///
41 | /// 检查账号状态
42 | ///
43 | Task CheckStatusAsync(int accountId);
44 |
45 | ///
46 | /// 释放并移除指定账号的 Telegram 客户端(用于避免 session 文件长期被占用)。
47 | ///
48 | Task ReleaseClientAsync(int accountId);
49 | }
50 |
51 | ///
52 | /// 登录结果
53 | ///
54 | public record LoginResult(
55 | bool Success,
56 | string? NextStep, // null=完成, "code"=需要验证码, "password"=需要密码, "signup"=需要注册
57 | string? Message,
58 | AccountInfo? Account = null
59 | );
60 |
61 | ///
62 | /// 账号状态
63 | ///
64 | public enum AccountStatus
65 | {
66 | Active,
67 | Offline,
68 | Banned,
69 | Limited,
70 | NeedRelogin
71 | }
72 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/LocalConfigFile.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.Configuration;
4 |
5 | namespace TelegramPanel.Web.Services;
6 |
7 | public static class LocalConfigFile
8 | {
9 | public static string ResolvePath(IConfiguration configuration, IWebHostEnvironment environment)
10 | {
11 | var configured = (configuration["LocalConfig:Path"] ?? "").Trim();
12 | if (!string.IsNullOrWhiteSpace(configured))
13 | return configured;
14 |
15 | // Docker 部署默认持久化目录为 /data(docker-compose 挂载 ./docker-data:/data)
16 | // 即便没有显式配置,也优先写到 /data,避免写入镜像层 /app 导致丢失或权限问题。
17 | if (Directory.Exists("/data"))
18 | return "/data/appsettings.local.json";
19 |
20 | return Path.Combine(environment.ContentRootPath, "appsettings.local.json");
21 | }
22 |
23 | public static async Task EnsureExistsAsync(string path, CancellationToken cancellationToken = default)
24 | {
25 | var dir = Path.GetDirectoryName(path);
26 | if (!string.IsNullOrWhiteSpace(dir))
27 | Directory.CreateDirectory(dir);
28 |
29 | if (File.Exists(path))
30 | return;
31 |
32 | await File.WriteAllTextAsync(path, "{}", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), cancellationToken);
33 | }
34 |
35 | public static async Task WriteJsonAtomicallyAsync(string path, string json, CancellationToken cancellationToken = default)
36 | {
37 | var dir = Path.GetDirectoryName(path);
38 | if (!string.IsNullOrWhiteSpace(dir))
39 | Directory.CreateDirectory(dir);
40 |
41 | var tmp = $"{path}.tmp";
42 | await File.WriteAllTextAsync(tmp, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), cancellationToken);
43 | File.Move(tmp, path, overwrite: true);
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/AccountRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 账号仓储实现
8 | ///
9 | public class AccountRepository : Repository, IAccountRepository
10 | {
11 | public AccountRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public override async Task GetByIdAsync(int id)
16 | {
17 | return await _dbSet
18 | .Include(a => a.Category)
19 | .Include(a => a.Channels)
20 | .Include(a => a.Groups)
21 | .FirstOrDefaultAsync(a => a.Id == id);
22 | }
23 |
24 | public override async Task> GetAllAsync()
25 | {
26 | return await _dbSet
27 | .Include(a => a.Category)
28 | .ToListAsync();
29 | }
30 |
31 | public async Task GetByPhoneAsync(string phone)
32 | {
33 | return await _dbSet
34 | .Include(a => a.Category)
35 | .FirstOrDefaultAsync(a => a.Phone == phone);
36 | }
37 |
38 | public async Task GetByUserIdAsync(long userId)
39 | {
40 | return await _dbSet
41 | .Include(a => a.Category)
42 | .FirstOrDefaultAsync(a => a.UserId == userId);
43 | }
44 |
45 | public async Task> GetByCategoryAsync(int categoryId)
46 | {
47 | return await _dbSet
48 | .Include(a => a.Category)
49 | .Where(a => a.CategoryId == categoryId)
50 | .ToListAsync();
51 | }
52 |
53 | public async Task> GetActiveAccountsAsync()
54 | {
55 | return await _dbSet
56 | .Include(a => a.Category)
57 | .Where(a => a.IsActive)
58 | .ToListAsync();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/Account.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// 账号实体
5 | ///
6 | public class Account
7 | {
8 | public int Id { get; set; }
9 | public string Phone { get; set; } = null!;
10 | public long UserId { get; set; }
11 | ///
12 | /// 账号昵称(Telegram 显示名称)
13 | ///
14 | public string? Nickname { get; set; }
15 | public string? Username { get; set; }
16 | public string SessionPath { get; set; } = null!;
17 | public int ApiId { get; set; }
18 | public string ApiHash { get; set; } = null!;
19 | public bool IsActive { get; set; } = true;
20 | public int? CategoryId { get; set; }
21 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
22 | public DateTime LastSyncAt { get; set; } = DateTime.UtcNow;
23 |
24 | ///
25 | /// 最后一次登录 Telegram 的时间(UTC),用于风控检查
26 | ///
27 | public DateTime? LastLoginAt { get; set; }
28 |
29 | ///
30 | /// Telegram 状态检测结果摘要(用于页面刷新后仍可展示上次检测结论)
31 | ///
32 | public string? TelegramStatusSummary { get; set; }
33 |
34 | ///
35 | /// Telegram 状态检测详情(错误码/原因等)
36 | ///
37 | public string? TelegramStatusDetails { get; set; }
38 |
39 | ///
40 | /// Telegram 状态检测是否成功(Ok)
41 | ///
42 | public bool? TelegramStatusOk { get; set; }
43 |
44 | ///
45 | /// Telegram 状态检测时间(UTC)
46 | ///
47 | public DateTime? TelegramStatusCheckedAtUtc { get; set; }
48 |
49 | // 导航属性
50 | public AccountCategory? Category { get; set; }
51 | public ICollection Channels { get; set; } = new List();
52 | public ICollection AccountChannels { get; set; } = new List();
53 | public ICollection Groups { get; set; } = new List();
54 | }
55 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Entities/AccountExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Data.Entities;
2 |
3 | ///
4 | /// Account 实体扩展方法
5 | ///
6 | public static class AccountExtensions
7 | {
8 | ///
9 | /// 获取风控检查用的参考时间(优先 LastLoginAt,其次 CreatedAt/LastSyncAt 作为估算兜底)
10 | ///
11 | public static DateTime? GetRiskReferenceAtUtc(this Account account)
12 | {
13 | if (account.LastLoginAt != null)
14 | return account.LastLoginAt.Value;
15 |
16 | // 兼容历史数据:早期导入的账号可能没有写入 LastLoginAt
17 | if (account.CreatedAt != default)
18 | return account.CreatedAt;
19 |
20 | if (account.LastSyncAt != default)
21 | return account.LastSyncAt;
22 |
23 | return null;
24 | }
25 |
26 | ///
27 | /// 风控参考时间是否为估算值(非真实登录时间)
28 | ///
29 | public static bool IsRiskReferenceEstimated(this Account account)
30 | => account.LastLoginAt == null && account.GetRiskReferenceAtUtc() != null;
31 |
32 | ///
33 | /// 获取风控检查用的参考小时数
34 | ///
35 | public static double? GetRiskReferenceHours(this Account account)
36 | {
37 | var at = account.GetRiskReferenceAtUtc();
38 | if (at == null)
39 | return null;
40 |
41 | return (DateTime.UtcNow - at.Value).TotalHours;
42 | }
43 |
44 | ///
45 | /// 获取登录小时数的格式化字符串
46 | ///
47 | public static string GetLoginHoursFormatted(this Account account)
48 | {
49 | if (account.LastLoginAt == null)
50 | return "未知";
51 |
52 | var hours = (DateTime.UtcNow - account.LastLoginAt.Value).TotalHours;
53 | return hours.ToString("F1");
54 | }
55 |
56 | ///
57 | /// 获取登录小时数
58 | ///
59 | public static double? GetLoginHours(this Account account)
60 | {
61 | if (account.LastLoginAt == null)
62 | return null;
63 |
64 | return (DateTime.UtcNow - account.LastLoginAt.Value).TotalHours;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using TelegramPanel.Data.Entities
3 |
4 |
5 |
6 |
7 | 编辑账号分类
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 取消
16 |
17 | 保存
18 |
19 |
20 |
21 |
22 | @code
23 | {
24 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
25 |
26 | [Parameter] public AccountCategory Category { get; set; } = default!;
27 |
28 | private string name = "";
29 | private MudColor color = "#9E9E9E";
30 | private string? description;
31 |
32 | protected override void OnParametersSet()
33 | {
34 | name = (Category?.Name ?? "").Trim();
35 | description = (Category?.Description ?? "").Trim();
36 | color = string.IsNullOrWhiteSpace(Category?.Color) ? "#9E9E9E" : Category!.Color!;
37 | }
38 |
39 | private void Cancel()
40 | {
41 | MudDialog.Cancel();
42 | }
43 |
44 | private void Confirm()
45 | {
46 | var model = new AccountCategoryEditModel(
47 | Name: name.Trim(),
48 | Color: string.IsNullOrWhiteSpace(color.Value) ? null : color.Value,
49 | Description: string.IsNullOrWhiteSpace(description) ? null : description!.Trim());
50 |
51 | MudDialog.Close(DialogResult.Ok(model));
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/AccountChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 账号-频道关联仓储实现
8 | ///
9 | public class AccountChannelRepository : Repository, IAccountChannelRepository
10 | {
11 | public AccountChannelRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public async Task GetAsync(int accountId, int channelId)
16 | {
17 | return await _dbSet.FirstOrDefaultAsync(x => x.AccountId == accountId && x.ChannelId == channelId);
18 | }
19 |
20 | public async Task UpsertAsync(AccountChannel link)
21 | {
22 | var existing = await GetAsync(link.AccountId, link.ChannelId);
23 | if (existing == null)
24 | {
25 | await _dbSet.AddAsync(link);
26 | }
27 | else
28 | {
29 | existing.IsCreator = link.IsCreator;
30 | existing.IsAdmin = link.IsAdmin;
31 | existing.SyncedAt = link.SyncedAt;
32 | _dbSet.Update(existing);
33 | }
34 |
35 | await _context.SaveChangesAsync();
36 | }
37 |
38 | public async Task DeleteForAccountExceptAsync(int accountId, IReadOnlyCollection keepChannelIds)
39 | {
40 | var keep = keepChannelIds.ToHashSet();
41 | var toDelete = await _dbSet
42 | .Where(x => x.AccountId == accountId && !keep.Contains(x.ChannelId))
43 | .ToListAsync();
44 |
45 | if (toDelete.Count == 0)
46 | return;
47 |
48 | _dbSet.RemoveRange(toDelete);
49 | await _context.SaveChangesAsync();
50 | }
51 |
52 | public async Task GetPreferredAdminAccountIdAsync(int channelId)
53 | {
54 | return await _dbSet
55 | .Where(x => x.ChannelId == channelId && x.IsAdmin)
56 | .OrderByDescending(x => x.IsCreator)
57 | .ThenByDescending(x => x.SyncedAt)
58 | .Select(x => (int?)x.AccountId)
59 | .FirstOrDefaultAsync();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Migrations/20251219000003_AddAccountTelegramStatusCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Infrastructure;
3 | using Microsoft.EntityFrameworkCore.Migrations;
4 |
5 | #nullable disable
6 |
7 | namespace TelegramPanel.Data.Migrations
8 | {
9 | [DbContext(typeof(AppDbContext))]
10 | [Migration("20251219000003_AddAccountTelegramStatusCache")]
11 | public partial class AddAccountTelegramStatusCache : Migration
12 | {
13 | protected override void Up(MigrationBuilder migrationBuilder)
14 | {
15 | migrationBuilder.AddColumn(
16 | name: "TelegramStatusSummary",
17 | table: "Accounts",
18 | type: "TEXT",
19 | maxLength: 200,
20 | nullable: true);
21 |
22 | migrationBuilder.AddColumn(
23 | name: "TelegramStatusDetails",
24 | table: "Accounts",
25 | type: "TEXT",
26 | maxLength: 2000,
27 | nullable: true);
28 |
29 | migrationBuilder.AddColumn(
30 | name: "TelegramStatusOk",
31 | table: "Accounts",
32 | type: "INTEGER",
33 | nullable: true);
34 |
35 | migrationBuilder.AddColumn(
36 | name: "TelegramStatusCheckedAtUtc",
37 | table: "Accounts",
38 | type: "TEXT",
39 | nullable: true);
40 | }
41 |
42 | protected override void Down(MigrationBuilder migrationBuilder)
43 | {
44 | migrationBuilder.DropColumn(
45 | name: "TelegramStatusSummary",
46 | table: "Accounts");
47 |
48 | migrationBuilder.DropColumn(
49 | name: "TelegramStatusDetails",
50 | table: "Accounts");
51 |
52 | migrationBuilder.DropColumn(
53 | name: "TelegramStatusOk",
54 | table: "Accounts");
55 |
56 | migrationBuilder.DropColumn(
57 | name: "TelegramStatusCheckedAtUtc",
58 | table: "Accounts");
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/TelegramPanel.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.0.31903.59
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramPanel.Web", "src\TelegramPanel.Web\TelegramPanel.Web.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramPanel.Core", "src\TelegramPanel.Core\TelegramPanel.Core.csproj", "{B2C3D4E5-F678-9012-BCDE-F12345678901}"
8 | EndProject
9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramPanel.Data", "src\TelegramPanel.Data\TelegramPanel.Data.csproj", "{C3D4E5F6-7890-1234-CDEF-123456789012}"
10 | EndProject
11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelegramPanel.Modules.Abstractions", "src\TelegramPanel.Modules.Abstractions\TelegramPanel.Modules.Abstractions.csproj", "{D47E13D1-8E3C-4F40-9B6D-4B8E17A8C5F1}"
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
23 | {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {B2C3D4E5-F678-9012-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {B2C3D4E5-F678-9012-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {C3D4E5F6-7890-1234-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {C3D4E5F6-7890-1234-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {C3D4E5F6-7890-1234-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {C3D4E5F6-7890-1234-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {D47E13D1-8E3C-4F40-9B6D-4B8E17A8C5F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {D47E13D1-8E3C-4F40-9B6D-4B8E17A8C5F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {D47E13D1-8E3C-4F40-9B6D-4B8E17A8C5F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {D47E13D1-8E3C-4F40-9B6D-4B8E17A8C5F1}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Services/GroupManagementService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 | using TelegramPanel.Data.Repositories;
3 |
4 | namespace TelegramPanel.Core.Services;
5 |
6 | ///
7 | /// 群组数据管理服务
8 | ///
9 | public class GroupManagementService
10 | {
11 | private readonly IGroupRepository _groupRepository;
12 |
13 | public GroupManagementService(IGroupRepository groupRepository)
14 | {
15 | _groupRepository = groupRepository;
16 | }
17 |
18 | public async Task GetGroupAsync(int id)
19 | {
20 | return await _groupRepository.GetByIdAsync(id);
21 | }
22 |
23 | public async Task GetGroupByTelegramIdAsync(long telegramId)
24 | {
25 | return await _groupRepository.GetByTelegramIdAsync(telegramId);
26 | }
27 |
28 | public async Task> GetAllGroupsAsync()
29 | {
30 | return await _groupRepository.GetAllAsync();
31 | }
32 |
33 | public async Task> GetGroupsByCreatorAsync(int accountId)
34 | {
35 | return await _groupRepository.GetByCreatorAccountAsync(accountId);
36 | }
37 |
38 | public async Task CreateOrUpdateGroupAsync(Group group)
39 | {
40 | var existing = await _groupRepository.GetByTelegramIdAsync(group.TelegramId);
41 | if (existing != null)
42 | {
43 | // 更新现有群组
44 | existing.Title = group.Title;
45 | existing.Username = group.Username;
46 | existing.MemberCount = group.MemberCount;
47 | existing.About = group.About;
48 | existing.AccessHash = group.AccessHash;
49 | existing.SyncedAt = DateTime.UtcNow;
50 |
51 | await _groupRepository.UpdateAsync(existing);
52 | return existing;
53 | }
54 | else
55 | {
56 | // 创建新群组
57 | group.SyncedAt = DateTime.UtcNow;
58 | return await _groupRepository.AddAsync(group);
59 | }
60 | }
61 |
62 | public async Task DeleteGroupAsync(int id)
63 | {
64 | var group = await _groupRepository.GetByIdAsync(id);
65 | if (group != null)
66 | {
67 | await _groupRepository.DeleteAsync(group);
68 | }
69 | }
70 |
71 | public async Task GetTotalGroupCountAsync()
72 | {
73 | return await _groupRepository.CountAsync();
74 | }
75 |
76 | public async Task GetGroupCountByCreatorAsync(int accountId)
77 | {
78 | return await _groupRepository.CountAsync(g => g.CreatorAccountId == accountId);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/ModuleStateStore.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace TelegramPanel.Web.Modules;
4 |
5 | public sealed class ModuleStateStore
6 | {
7 | private readonly ModuleLayout _layout;
8 | private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
9 |
10 | public ModuleStateStore(ModuleLayout layout)
11 | {
12 | _layout = layout;
13 | }
14 |
15 | public async Task LoadAsync()
16 | {
17 | EnsureDirectories();
18 |
19 | if (!File.Exists(_layout.StateFile))
20 | return new ModuleState();
21 |
22 | var json = await File.ReadAllTextAsync(_layout.StateFile);
23 | if (string.IsNullOrWhiteSpace(json))
24 | return new ModuleState();
25 |
26 | return JsonSerializer.Deserialize(json) ?? new ModuleState();
27 | }
28 |
29 | public async Task SaveAsync(ModuleState state)
30 | {
31 | EnsureDirectories();
32 |
33 | state.SchemaVersion = 1;
34 | state.Modules ??= new List();
35 |
36 | var json = JsonSerializer.Serialize(state, _jsonOptions);
37 | var target = _layout.StateFile;
38 | var dir = Path.GetDirectoryName(target) ?? _layout.Root;
39 | Directory.CreateDirectory(dir);
40 |
41 | var temp = target + ".tmp";
42 | await File.WriteAllTextAsync(temp, json, new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
43 | if (File.Exists(target))
44 | File.Replace(temp, target, destinationBackupFileName: null);
45 | else
46 | File.Move(temp, target);
47 | }
48 |
49 | private void EnsureDirectories()
50 | {
51 | Directory.CreateDirectory(_layout.Root);
52 | Directory.CreateDirectory(_layout.PackagesDir);
53 | Directory.CreateDirectory(_layout.InstalledDir);
54 | Directory.CreateDirectory(_layout.ActiveDir);
55 | Directory.CreateDirectory(_layout.StagingDir);
56 | Directory.CreateDirectory(_layout.TrashDir);
57 | }
58 | }
59 |
60 | public sealed class ModuleState
61 | {
62 | public int SchemaVersion { get; set; } = 1;
63 | public List Modules { get; set; } = new();
64 | }
65 |
66 | public sealed class ModuleStateItem
67 | {
68 | public string Id { get; set; } = "";
69 | public bool Enabled { get; set; }
70 | public string? ActiveVersion { get; set; }
71 | public string? LastGoodVersion { get; set; }
72 | public List InstalledVersions { get; set; } = new();
73 |
74 | public bool BuiltIn { get; set; }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/ModulePaths.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using TelegramPanel.Modules;
3 |
4 | namespace TelegramPanel.Web.Modules;
5 |
6 | public static class ModulePaths
7 | {
8 | public static string GetHostVersion()
9 | {
10 | // 优先取 InformationalVersion(通常包含语义版本);取不到则退回 AssemblyVersion。
11 | var asm = Assembly.GetExecutingAssembly();
12 | var info = asm.GetCustomAttribute()?.InformationalVersion;
13 | info = (info ?? "").Trim();
14 | if (SemVer.TryParse(NormalizeSemVer(info), out var v))
15 | return v.ToString();
16 |
17 | var ver = asm.GetName().Version;
18 | if (ver != null)
19 | return $"{ver.Major}.{ver.Minor}.{(ver.Build < 0 ? 0 : ver.Build)}";
20 |
21 | return "0.0.0";
22 | }
23 |
24 | public static string ResolveModulesRoot(IConfiguration configuration, IWebHostEnvironment environment)
25 | {
26 | var configured = (configuration["Modules:RootPath"] ?? "").Trim();
27 | if (!string.IsNullOrWhiteSpace(configured))
28 | {
29 | if (Path.IsPathRooted(configured))
30 | return configured;
31 |
32 | return Path.Combine(environment.ContentRootPath, configured);
33 | }
34 |
35 | // Docker 默认把持久化目录挂到 /data
36 | if (Directory.Exists("/data"))
37 | return "/data/modules";
38 |
39 | return Path.Combine(environment.ContentRootPath, "modules");
40 | }
41 |
42 | public static ModuleLayout GetLayout(string root)
43 | {
44 | root = root.Trim();
45 | return new ModuleLayout(
46 | Root: root,
47 | StateFile: Path.Combine(root, "state.json"),
48 | PackagesDir: Path.Combine(root, "packages"),
49 | InstalledDir: Path.Combine(root, "installed"),
50 | ActiveDir: Path.Combine(root, "active"),
51 | StagingDir: Path.Combine(root, "staging"),
52 | TrashDir: Path.Combine(root, "trash"));
53 | }
54 |
55 | private static string NormalizeSemVer(string value)
56 | {
57 | // InformationalVersion 可能是 "1.2.3+sha" 或 "1.2.3-rc.1"
58 | var s = value;
59 | var plus = s.IndexOf('+');
60 | if (plus >= 0) s = s.Substring(0, plus);
61 | var dash = s.IndexOf('-');
62 | if (dash >= 0) s = s.Substring(0, dash);
63 | return s.Trim();
64 | }
65 | }
66 |
67 | public sealed record ModuleLayout(
68 | string Root,
69 | string StateFile,
70 | string PackagesDir,
71 | string InstalledDir,
72 | string ActiveDir,
73 | string StagingDir,
74 | string TrashDir);
75 |
--------------------------------------------------------------------------------
/tools/package-module.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [Parameter(Mandatory = $true)]
3 | [string]$Project,
4 |
5 | [Parameter(Mandatory = $true)]
6 | [string]$Manifest,
7 |
8 | [Parameter(Mandatory = $false)]
9 | [string]$OutDir = "artifacts/modules"
10 | )
11 |
12 | $ErrorActionPreference = "Stop"
13 |
14 | $repoRoot = (Resolve-Path ".").Path
15 | $projectPath = Join-Path $repoRoot $Project
16 | $manifestPath = Join-Path $repoRoot $Manifest
17 | $outRoot = Join-Path $repoRoot $OutDir
18 |
19 | if (-not (Test-Path -Path $projectPath)) { throw "Project 不存在:$Project" }
20 | if (-not (Test-Path -Path $manifestPath)) { throw "Manifest 不存在:$Manifest" }
21 |
22 | $manifestObj = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json
23 | $moduleId = ""
24 | $version = ""
25 | if ($null -ne $manifestObj.id) { $moduleId = [string]$manifestObj.id }
26 | if ($null -ne $manifestObj.version) { $version = [string]$manifestObj.version }
27 | $moduleId = $moduleId.Trim()
28 | $version = $version.Trim()
29 | if ([string]::IsNullOrWhiteSpace($moduleId)) { throw "manifest.json 缺少 id" }
30 | if ([string]::IsNullOrWhiteSpace($version)) { throw "manifest.json 缺少 version" }
31 |
32 | $buildRootRel = "artifacts/_modulebuild/$moduleId/$version"
33 | $publishRel = "$buildRootRel/publish"
34 | $stagingRel = "$buildRootRel/staging"
35 |
36 | $publishHost = Join-Path $repoRoot $publishRel
37 | $stagingHost = Join-Path $repoRoot $stagingRel
38 |
39 | New-Item -ItemType Directory -Force -Path $publishHost | Out-Null
40 | New-Item -ItemType Directory -Force -Path $stagingHost | Out-Null
41 |
42 | $publishContainer = "/src/$publishRel"
43 | $projectContainer = "/src/$Project"
44 |
45 | Write-Host "Building module with Docker..." -ForegroundColor Cyan
46 | docker run --rm `
47 | -v "${repoRoot}:/src" `
48 | -w "/src" `
49 | mcr.microsoft.com/dotnet/sdk:8.0 `
50 | dotnet publish "$projectContainer" -c Release -o "$publishContainer" /p:UseAppHost=false
51 | if ($LASTEXITCODE -ne 0) { throw "dotnet publish 失败(退出码:$LASTEXITCODE)" }
52 |
53 | $stagingLib = Join-Path $stagingHost "lib"
54 | New-Item -ItemType Directory -Force -Path $stagingLib | Out-Null
55 |
56 | Copy-Item -Path $manifestPath -Destination (Join-Path $stagingHost "manifest.json") -Force
57 | Copy-Item -Path (Join-Path $publishHost "*") -Destination $stagingLib -Recurse -Force
58 |
59 | New-Item -ItemType Directory -Force -Path $outRoot | Out-Null
60 | $dest = Join-Path $outRoot "$moduleId-$version.tpm"
61 | if (Test-Path -Path $dest) { Remove-Item -Path $dest -Force }
62 |
63 | $destZip = [System.IO.Path]::ChangeExtension($dest, ".zip")
64 | if (Test-Path -Path $destZip) { Remove-Item -Path $destZip -Force }
65 |
66 | Compress-Archive -Path (Join-Path $stagingHost "*") -DestinationPath $destZip -Force
67 | Move-Item -Path $destZip -Destination $dest -Force
68 |
69 | Write-Host "OK: $dest" -ForegroundColor Green
70 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Layout/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Telegram Panel
12 |
13 |
17 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | @Body
35 |
36 |
37 |
38 |
39 | @using TelegramPanel.Web.Services
40 | @inject UiPreferencesService UiPreferences
41 |
42 | @code {
43 | private bool _drawerOpen = true;
44 | private bool _isDarkMode = true;
45 |
46 | private MudTheme _theme = new()
47 | {
48 | PaletteLight = new PaletteLight
49 | {
50 | Primary = "#1976d2",
51 | Secondary = "#00bcd4",
52 | AppbarBackground = "#1976d2"
53 | },
54 | PaletteDark = new PaletteDark
55 | {
56 | Primary = "#90caf9",
57 | Secondary = "#80deea",
58 | AppbarBackground = "#1e1e2d",
59 | Background = "#121212",
60 | Surface = "#1e1e2d"
61 | }
62 | };
63 |
64 | protected override async Task OnInitializedAsync()
65 | {
66 | // 从配置文件读取用户的主题偏好
67 | _isDarkMode = await UiPreferences.GetIsDarkModeAsync();
68 | // 确保初始化后 UI 能正确显示主题
69 | StateHasChanged();
70 | }
71 |
72 | private void ToggleDrawer()
73 | {
74 | _drawerOpen = !_drawerOpen;
75 | }
76 |
77 | private async Task ToggleDarkMode()
78 | {
79 | _isDarkMode = !_isDarkMode;
80 | // 保存主题偏好到配置文件
81 | await UiPreferences.SetIsDarkModeAsync(_isDarkMode);
82 | // 强制刷新组件状态,确保在异步操作后 UI 能及时更新
83 | StateHasChanged();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Pages/ModulePageHost.razor:
--------------------------------------------------------------------------------
1 | @page "/ext/{ModuleId}/{PageKey}"
2 | @using Microsoft.AspNetCore.Components
3 | @using TelegramPanel.Web.Modules
4 | @inject ModuleContributionRegistry Contributions
5 |
6 | 扩展页面 - Telegram Panel
7 |
8 | @if (error != null)
9 | {
10 | @error
11 | }
12 | else if (componentType == null)
13 | {
14 |
15 | }
16 | else
17 | {
18 |
19 | }
20 |
21 | @code
22 | {
23 | [Parameter] public string ModuleId { get; set; } = "";
24 | [Parameter] public string PageKey { get; set; } = "";
25 |
26 | private string? error;
27 | private Type? componentType;
28 | private Dictionary parameters = new();
29 |
30 | protected override void OnParametersSet()
31 | {
32 | error = null;
33 | componentType = null;
34 | parameters = new Dictionary();
35 |
36 | var moduleId = (ModuleId ?? "").Trim();
37 | var pageKey = (PageKey ?? "").Trim();
38 | if (moduleId.Length == 0 || pageKey.Length == 0)
39 | {
40 | error = "参数无效";
41 | return;
42 | }
43 |
44 | var page = Contributions.Pages.FirstOrDefault(p =>
45 | string.Equals(p.Module.Id, moduleId, StringComparison.OrdinalIgnoreCase)
46 | && string.Equals(p.Definition.Key, pageKey, StringComparison.OrdinalIgnoreCase));
47 |
48 | if (page == null)
49 | {
50 | error = $"未找到扩展页面:{moduleId}/{pageKey}";
51 | return;
52 | }
53 |
54 | var typeName = (page.Definition.ComponentType ?? "").Trim();
55 | if (typeName.Length == 0)
56 | {
57 | error = "模块页面未配置 componentType";
58 | return;
59 | }
60 |
61 | var moduleTypeName = NormalizeModuleTypeName(typeName);
62 | componentType =
63 | Type.GetType(typeName, throwOnError: false, ignoreCase: false)
64 | ?? page.Module.Instance.GetType().Assembly.GetType(moduleTypeName, throwOnError: false, ignoreCase: false);
65 |
66 | if (componentType == null)
67 | {
68 | error = $"无法加载组件类型:{typeName}";
69 | return;
70 | }
71 |
72 | parameters = new Dictionary
73 | {
74 | ["ModuleId"] = moduleId,
75 | ["PageKey"] = pageKey
76 | };
77 | }
78 |
79 | private static string NormalizeModuleTypeName(string typeName)
80 | {
81 | typeName = (typeName ?? "").Trim();
82 | if (typeName.Length == 0)
83 | return typeName;
84 |
85 | var comma = typeName.IndexOf(',', StringComparison.Ordinal);
86 | if (comma > 0)
87 | return typeName.Substring(0, comma).Trim();
88 |
89 | return typeName;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/AccountDataAutoSyncBackgroundService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace TelegramPanel.Web.Services;
6 |
7 | ///
8 | /// 账号频道/群组数据自动同步(后台定时)。
9 | ///
10 | public sealed class AccountDataAutoSyncBackgroundService : BackgroundService
11 | {
12 | private readonly IServiceScopeFactory _scopeFactory;
13 | private readonly IConfiguration _configuration;
14 | private readonly ILogger _logger;
15 |
16 | public AccountDataAutoSyncBackgroundService(
17 | IServiceScopeFactory scopeFactory,
18 | IConfiguration configuration,
19 | ILogger logger)
20 | {
21 | _scopeFactory = scopeFactory;
22 | _configuration = configuration;
23 | _logger = logger;
24 | }
25 |
26 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
27 | {
28 | // 延迟一点,避免与启动时 DB 迁移抢资源
29 | await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
30 |
31 | while (!stoppingToken.IsCancellationRequested)
32 | {
33 | var enabled = _configuration.GetValue("Sync:AutoSyncEnabled", false);
34 | var hours = _configuration.GetValue("Sync:IntervalHours", 6);
35 | if (hours < 1) hours = 1;
36 | if (hours > 24) hours = 24;
37 |
38 | if (!enabled)
39 | {
40 | await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
41 | continue;
42 | }
43 |
44 | try
45 | {
46 | using var scope = _scopeFactory.CreateScope();
47 | var dataSync = scope.ServiceProvider.GetRequiredService();
48 |
49 | _logger.LogInformation("Account auto sync started");
50 | var summary = await dataSync.SyncAllActiveAccountsAsync(stoppingToken);
51 | if (summary.AccountFailures.Count > 0)
52 | {
53 | foreach (var f in summary.AccountFailures)
54 | _logger.LogWarning("Account auto sync failed: {Phone} {Error}", f.Phone, f.Error);
55 | }
56 |
57 | _logger.LogInformation("Account auto sync completed: {Channels} channels, {Groups} groups (failures={Failures})",
58 | summary.TotalChannelsSynced, summary.TotalGroupsSynced, summary.AccountFailures.Count);
59 | }
60 | catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
61 | {
62 | // ignore
63 | }
64 | catch (Exception ex)
65 | {
66 | _logger.LogWarning(ex, "Account auto sync failed");
67 | }
68 |
69 | await Task.Delay(TimeSpan.FromHours(hours), stoppingToken);
70 | }
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Modules/ModuleLoadContext.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.Loader;
3 |
4 | namespace TelegramPanel.Web.Modules;
5 |
6 | public sealed class ModuleLoadContext : AssemblyLoadContext
7 | {
8 | private readonly AssemblyDependencyResolver _resolver;
9 | private readonly string _baseDir;
10 |
11 | public ModuleLoadContext(string mainAssemblyPath) : base(isCollectible: false)
12 | {
13 | if (string.IsNullOrWhiteSpace(mainAssemblyPath))
14 | throw new ArgumentException("mainAssemblyPath 不能为空", nameof(mainAssemblyPath));
15 |
16 | _resolver = new AssemblyDependencyResolver(mainAssemblyPath);
17 | _baseDir = Path.GetDirectoryName(mainAssemblyPath) ?? "";
18 | }
19 |
20 | protected override Assembly? Load(AssemblyName assemblyName)
21 | {
22 | var name = (assemblyName.Name ?? "").Trim();
23 | if (name.Length == 0)
24 | return null;
25 |
26 | // 1) 与宿主共享的“边界程序集”必须由 Default ALC 解析,避免同名程序集被模块再次加载导致类型身份不一致。
27 | // 典型问题:模块包携带 Microsoft.Extensions.DependencyInjection*.dll / Microsoft.AspNetCore.Components.dll,
28 | // 会让 IServiceCollection / EventCallback 等类型在不同 ALC 中变成“不同类型”,最终出现:
29 | // - TypeLoadException(看起来像模块没有实现 ConfigureServices)
30 | // - 组件参数绑定失败(表现为 DraftChanged 不生效)
31 | if (name.StartsWith("Microsoft.Extensions.", StringComparison.OrdinalIgnoreCase)
32 | || name.StartsWith("Microsoft.AspNetCore.", StringComparison.OrdinalIgnoreCase)
33 | || name.StartsWith("Microsoft.JSInterop", StringComparison.OrdinalIgnoreCase)
34 | || name.StartsWith("TelegramPanel.", StringComparison.OrdinalIgnoreCase)
35 | || string.Equals(name, "MudBlazor", StringComparison.OrdinalIgnoreCase))
36 | return null;
37 |
38 | // 2) 其次:如果宿主已经加载过同名程序集,则同样交给 Default ALC(避免重复加载)。
39 | if (AssemblyLoadContext.Default.Assemblies.Any(a =>
40 | string.Equals(a.GetName().Name, name, StringComparison.OrdinalIgnoreCase)))
41 | return null;
42 |
43 | var path = _resolver.ResolveAssemblyToPath(assemblyName);
44 | if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
45 | return LoadFromAssemblyPath(path);
46 |
47 | // fallback:如果模块未提供 deps.json,则在 lib 目录按名称寻找
48 | if (name.Length == 0 || string.IsNullOrWhiteSpace(_baseDir))
49 | return null;
50 |
51 | var candidate = Path.Combine(_baseDir, name + ".dll");
52 | if (File.Exists(candidate))
53 | return LoadFromAssemblyPath(candidate);
54 |
55 | return null;
56 | }
57 |
58 | protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
59 | {
60 | var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
61 | if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
62 | return LoadUnmanagedDllFromPath(path);
63 |
64 | return base.LoadUnmanagedDll(unmanagedDllName);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/UiPreferencesService.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.Json;
3 | using System.Text.Json.Nodes;
4 | using Microsoft.AspNetCore.Hosting;
5 |
6 | namespace TelegramPanel.Web.Services;
7 |
8 | ///
9 | /// UI 偏好设置服务,用于管理用户界面相关的配置(如主题模式)
10 | ///
11 | public class UiPreferencesService
12 | {
13 | private readonly IConfiguration _configuration;
14 | private readonly IWebHostEnvironment _environment;
15 | private readonly string _configFilePath;
16 | private readonly SemaphoreSlim _writeLock = new(1, 1);
17 |
18 | public UiPreferencesService(IConfiguration configuration, IWebHostEnvironment environment)
19 | {
20 | _configuration = configuration;
21 | _environment = environment;
22 | _configFilePath = LocalConfigFile.ResolvePath(configuration, environment);
23 | }
24 |
25 | ///
26 | /// 获取当前的深色模式设置
27 | ///
28 | public async Task GetIsDarkModeAsync()
29 | {
30 | try
31 | {
32 | if (!File.Exists(_configFilePath))
33 | return true; // 默认使用深色模式
34 |
35 | var json = await File.ReadAllTextAsync(_configFilePath);
36 | var doc = JsonDocument.Parse(json);
37 |
38 | if (doc.RootElement.TryGetProperty("UI", out var uiSection) &&
39 | uiSection.TryGetProperty("IsDarkMode", out var isDarkModeValue))
40 | {
41 | return isDarkModeValue.GetBoolean();
42 | }
43 |
44 | return true; // 默认使用深色模式
45 | }
46 | catch
47 | {
48 | return true; // 出错时默认使用深色模式
49 | }
50 | }
51 |
52 | ///
53 | /// 设置深色模式
54 | ///
55 | public async Task SetIsDarkModeAsync(bool isDarkMode)
56 | {
57 | await _writeLock.WaitAsync();
58 | try
59 | {
60 | // 确保配置文件存在
61 | await LocalConfigFile.EnsureExistsAsync(_configFilePath);
62 |
63 | // 读取现有配置
64 | var json = await File.ReadAllTextAsync(_configFilePath);
65 | var jsonObj = JsonNode.Parse(json)?.AsObject() ?? new JsonObject();
66 |
67 | // 更新 UI 配置
68 | if (!jsonObj.ContainsKey("UI"))
69 | {
70 | jsonObj["UI"] = new JsonObject();
71 | }
72 |
73 | var uiSection = jsonObj["UI"]!.AsObject();
74 | uiSection["IsDarkMode"] = isDarkMode;
75 |
76 | // 写入配置文件
77 | var options = new JsonSerializerOptions
78 | {
79 | WriteIndented = true,
80 | Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
81 | };
82 | var updatedJson = JsonSerializer.Serialize(jsonObj, options);
83 | await LocalConfigFile.WriteJsonAtomicallyAsync(_configFilePath, updatedJson);
84 | }
85 | finally
86 | {
87 | _writeLock.Release();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Data/Repositories/ChannelRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TelegramPanel.Data.Entities;
3 |
4 | namespace TelegramPanel.Data.Repositories;
5 |
6 | ///
7 | /// 频道仓储实现
8 | ///
9 | public class ChannelRepository : Repository, IChannelRepository
10 | {
11 | public ChannelRepository(AppDbContext context) : base(context)
12 | {
13 | }
14 |
15 | public override async Task GetByIdAsync(int id)
16 | {
17 | return await _dbSet
18 | .Include(c => c.CreatorAccount)
19 | .Include(c => c.Group)
20 | .FirstOrDefaultAsync(c => c.Id == id);
21 | }
22 |
23 | public override async Task> GetAllAsync()
24 | {
25 | return await _dbSet
26 | .Include(c => c.CreatorAccount)
27 | .Include(c => c.Group)
28 | .OrderByDescending(c => c.SyncedAt)
29 | .ToListAsync();
30 | }
31 |
32 | public async Task GetByTelegramIdAsync(long telegramId)
33 | {
34 | return await _dbSet
35 | .Include(c => c.CreatorAccount)
36 | .Include(c => c.Group)
37 | .FirstOrDefaultAsync(c => c.TelegramId == telegramId);
38 | }
39 |
40 | public async Task> GetCreatedAsync()
41 | {
42 | return await _dbSet
43 | .Include(c => c.CreatorAccount)
44 | .Include(c => c.Group)
45 | .Where(c => c.CreatorAccountId != null)
46 | .OrderByDescending(c => c.SyncedAt)
47 | .ToListAsync();
48 | }
49 |
50 | public async Task> GetByCreatorAccountAsync(int accountId)
51 | {
52 | return await _dbSet
53 | .Include(c => c.CreatorAccount)
54 | .Include(c => c.Group)
55 | .Where(c => c.CreatorAccountId == accountId)
56 | .OrderByDescending(c => c.SyncedAt)
57 | .ToListAsync();
58 | }
59 |
60 | public async Task> GetForAccountAsync(int accountId, bool includeNonCreator)
61 | {
62 | var links = _context.Set()
63 | .Where(x => x.AccountId == accountId && (includeNonCreator || x.IsCreator));
64 |
65 | return await _dbSet
66 | .Include(c => c.CreatorAccount)
67 | .Include(c => c.Group)
68 | .Where(c => links.Any(x => x.ChannelId == c.Id))
69 | .OrderByDescending(c => c.SyncedAt)
70 | .ToListAsync();
71 | }
72 |
73 | public async Task> GetByGroupAsync(int groupId)
74 | {
75 | return await _dbSet
76 | .Include(c => c.CreatorAccount)
77 | .Include(c => c.Group)
78 | .Where(c => c.GroupId == groupId)
79 | .OrderByDescending(c => c.SyncedAt)
80 | .ToListAsync();
81 | }
82 |
83 | public async Task> GetBroadcastChannelsAsync()
84 | {
85 | return await _dbSet
86 | .Include(c => c.CreatorAccount)
87 | .Include(c => c.Group)
88 | .Where(c => c.IsBroadcast)
89 | .OrderByDescending(c => c.SyncedAt)
90 | .ToListAsync();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/PanelTimeZoneService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Options;
2 |
3 | namespace TelegramPanel.Web.Services;
4 |
5 | public sealed class PanelTimeZoneService
6 | {
7 | private readonly object _gate = new();
8 | private TimeZoneInfo _timeZone = TimeZoneInfo.Utc;
9 |
10 | public PanelTimeZoneService(IOptionsMonitor optionsMonitor)
11 | {
12 | Apply(optionsMonitor.CurrentValue);
13 | optionsMonitor.OnChange(Apply);
14 | }
15 |
16 | public TimeZoneInfo Current
17 | {
18 | get
19 | {
20 | lock (_gate)
21 | {
22 | return _timeZone;
23 | }
24 | }
25 | }
26 |
27 | public string Format(DateTime? valueUtcOrUnspecified, string format = "yyyy-MM-dd HH:mm", string emptyText = "-")
28 | {
29 | if (valueUtcOrUnspecified == null)
30 | return emptyText;
31 |
32 | var converted = ConvertFromUtcOrUnspecified(valueUtcOrUnspecified.Value);
33 | return converted.ToString(format);
34 | }
35 |
36 | public DateTime ConvertFromUtcOrUnspecified(DateTime valueUtcOrUnspecified)
37 | {
38 | var utc = valueUtcOrUnspecified.Kind switch
39 | {
40 | DateTimeKind.Utc => valueUtcOrUnspecified,
41 | DateTimeKind.Local => valueUtcOrUnspecified.ToUniversalTime(),
42 | _ => DateTime.SpecifyKind(valueUtcOrUnspecified, DateTimeKind.Utc)
43 | };
44 |
45 | var tz = Current;
46 | return TimeZoneInfo.ConvertTimeFromUtc(utc, tz);
47 | }
48 |
49 | private void Apply(PanelTimeZoneOptions options)
50 | {
51 | var id = (options.TimeZoneId ?? string.Empty).Trim();
52 |
53 | var tz = Resolve(id);
54 | lock (_gate)
55 | {
56 | _timeZone = tz;
57 | }
58 | }
59 |
60 | private static TimeZoneInfo Resolve(string timeZoneId)
61 | {
62 | if (string.IsNullOrWhiteSpace(timeZoneId))
63 | return TimeZoneInfo.Utc;
64 |
65 | if (TryFind(timeZoneId, out var tz))
66 | return tz;
67 |
68 | // 常见跨平台兜底:IANA <-> Windows
69 | if (string.Equals(timeZoneId, "Asia/Shanghai", StringComparison.OrdinalIgnoreCase)
70 | && TryFind("China Standard Time", out tz))
71 | return tz;
72 |
73 | if (string.Equals(timeZoneId, "China Standard Time", StringComparison.OrdinalIgnoreCase)
74 | && TryFind("Asia/Shanghai", out tz))
75 | return tz;
76 |
77 | if (string.Equals(timeZoneId, "UTC", StringComparison.OrdinalIgnoreCase) && TryFind("Etc/UTC", out tz))
78 | return tz;
79 |
80 | if (string.Equals(timeZoneId, "Etc/UTC", StringComparison.OrdinalIgnoreCase) && TryFind("UTC", out tz))
81 | return tz;
82 |
83 | return TimeZoneInfo.Utc;
84 | }
85 |
86 | private static bool TryFind(string id, out TimeZoneInfo timeZone)
87 | {
88 | try
89 | {
90 | timeZone = TimeZoneInfo.FindSystemTimeZoneById(id);
91 | return true;
92 | }
93 | catch
94 | {
95 | timeZone = TimeZoneInfo.Utc;
96 | return false;
97 | }
98 | }
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/BotChannelCategoryDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @inject BotManagementService BotManagement
3 | @inject ISnackbar Snackbar
4 | @using TelegramPanel.Data.Entities
5 |
6 |
7 |
8 |
9 |
11 |
13 |
14 |
15 |
16 | 取消
17 |
18 | @if (saving)
19 | {
20 |
21 | 保存中...
22 | }
23 | else
24 | {
25 | 保存
26 | }
27 |
28 |
29 |
30 |
31 | @code
32 | {
33 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
34 | [Parameter] public int BotId { get; set; }
35 | [Parameter] public BotChannelCategory? Category { get; set; }
36 |
37 | private bool saving;
38 | private string name = "";
39 | private string description = "";
40 |
41 | protected override void OnInitialized()
42 | {
43 | // 如果传入了分类,说明是编辑模式
44 | if (Category != null)
45 | {
46 | name = Category.Name ?? "";
47 | description = Category.Description ?? "";
48 | }
49 | }
50 |
51 | private void Cancel()
52 | {
53 | MudDialog.Cancel();
54 | }
55 |
56 | private async Task Save()
57 | {
58 | if (saving)
59 | return;
60 |
61 | name = (name ?? string.Empty).Trim();
62 | if (string.IsNullOrWhiteSpace(name))
63 | {
64 | Snackbar.Add("请输入分类名称", Severity.Warning);
65 | return;
66 | }
67 |
68 | saving = true;
69 | try
70 | {
71 | if (Category == null)
72 | {
73 | // 新增模式:使用现有的 CreateCategoryAsync 方法
74 | var category = await BotManagement.CreateCategoryAsync(BotId, name, description);
75 | Snackbar.Add("分类创建成功", Severity.Success);
76 | MudDialog.Close(DialogResult.Ok(category));
77 | }
78 | else
79 | {
80 | // 编辑模式:需要更新方法(暂未实现)
81 | Category.Name = name;
82 | Category.Description = description;
83 | // TODO: 需要实现 UpdateCategoryAsync 方法
84 | Snackbar.Add("分类更新功能待实现", Severity.Warning);
85 | MudDialog.Close(DialogResult.Ok(Category));
86 | }
87 | }
88 | catch (Exception ex)
89 | {
90 | Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
91 | }
92 | finally
93 | {
94 | saving = false;
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/SystemInboxDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @inject AccountTelegramToolsService AccountTelegramTools
3 | @inject ISnackbar Snackbar
4 |
5 |
6 |
7 |
8 | 账号:@Phone(ID:@AccountId)
9 |
10 |
11 | 这里显示 Telegram 系统通知(通常用于接收登录验证码)。若列表为空,可能是该账号从未在 Telegram 内收到系统消息。
12 |
13 |
14 |
15 |
17 | 刷新
18 |
19 | @if (lastLoadedAtUtc.HasValue)
20 | {
21 | 最后刷新:
22 | }
23 |
24 |
25 | @if (loading)
26 | {
27 |
28 | }
29 | else if (messages.Count == 0)
30 | {
31 | 暂无系统通知
32 | }
33 | else
34 | {
35 |
36 | @foreach (var m in messages)
37 | {
38 |
39 |
40 | @m.Text
41 |
42 | }
43 |
44 | }
45 |
46 |
47 |
48 | 关闭
49 |
50 |
51 |
52 | @code
53 | {
54 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
55 | [Parameter] public int AccountId { get; set; }
56 | [Parameter] public string Phone { get; set; } = "";
57 |
58 | private bool loading;
59 | private DateTime? lastLoadedAtUtc;
60 | private List messages = new();
61 |
62 | protected override async Task OnInitializedAsync()
63 | {
64 | await LoadMessages();
65 | }
66 |
67 | private async Task LoadMessages()
68 | {
69 | loading = true;
70 | try
71 | {
72 | var list = await AccountTelegramTools.GetLatestSystemMessagesAsync(AccountId, limit: 30);
73 | messages = list.ToList();
74 | lastLoadedAtUtc = DateTime.UtcNow;
75 | }
76 | catch (Exception ex)
77 | {
78 | Snackbar.Add($"加载系统通知失败:{ex.Message}", Severity.Error);
79 | }
80 | finally
81 | {
82 | loading = false;
83 | }
84 | }
85 |
86 | private void Close()
87 | {
88 | MudDialog.Close();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Data.Entities;
2 | using TelegramPanel.Data.Repositories;
3 |
4 | namespace TelegramPanel.Core.Services;
5 |
6 | ///
7 | /// 批量任务管理服务
8 | ///
9 | public class BatchTaskManagementService
10 | {
11 | private readonly IBatchTaskRepository _batchTaskRepository;
12 |
13 | public BatchTaskManagementService(IBatchTaskRepository batchTaskRepository)
14 | {
15 | _batchTaskRepository = batchTaskRepository;
16 | }
17 |
18 | public async Task GetTaskAsync(int id)
19 | {
20 | return await _batchTaskRepository.GetByIdAsync(id);
21 | }
22 |
23 | public async Task> GetAllTasksAsync()
24 | {
25 | return await _batchTaskRepository.GetAllAsync();
26 | }
27 |
28 | public async Task> GetTasksByStatusAsync(string status)
29 | {
30 | return await _batchTaskRepository.GetByStatusAsync(status);
31 | }
32 |
33 | public async Task> GetRunningTasksAsync()
34 | {
35 | return await _batchTaskRepository.GetRunningTasksAsync();
36 | }
37 |
38 | public async Task> GetRecentTasksAsync(int count = 20)
39 | {
40 | return await _batchTaskRepository.GetRecentTasksAsync(count);
41 | }
42 |
43 | public async Task CreateTaskAsync(BatchTask task)
44 | {
45 | task.CreatedAt = DateTime.UtcNow;
46 | task.Status = "pending";
47 | return await _batchTaskRepository.AddAsync(task);
48 | }
49 |
50 | public async Task UpdateTaskProgressAsync(int taskId, int completed, int failed)
51 | {
52 | var task = await _batchTaskRepository.GetByIdAsync(taskId);
53 | if (task != null)
54 | {
55 | task.Completed = completed;
56 | task.Failed = failed;
57 | await _batchTaskRepository.UpdateAsync(task);
58 | }
59 | }
60 |
61 | public async Task UpdateTaskConfigAsync(int taskId, string? config)
62 | {
63 | var task = await _batchTaskRepository.GetByIdAsync(taskId);
64 | if (task != null)
65 | {
66 | task.Config = config;
67 | await _batchTaskRepository.UpdateAsync(task);
68 | }
69 | }
70 |
71 | public async Task StartTaskAsync(int taskId)
72 | {
73 | var task = await _batchTaskRepository.GetByIdAsync(taskId);
74 | if (task != null)
75 | {
76 | task.Status = "running";
77 | task.StartedAt = DateTime.UtcNow;
78 | await _batchTaskRepository.UpdateAsync(task);
79 | }
80 | }
81 |
82 | public async Task CompleteTaskAsync(int taskId, bool success = true)
83 | {
84 | var task = await _batchTaskRepository.GetByIdAsync(taskId);
85 | if (task != null)
86 | {
87 | task.Status = success ? "completed" : "failed";
88 | task.CompletedAt = DateTime.UtcNow;
89 | await _batchTaskRepository.UpdateAsync(task);
90 | }
91 | }
92 |
93 | public async Task DeleteTaskAsync(int id)
94 | {
95 | var task = await _batchTaskRepository.GetByIdAsync(id);
96 | if (task != null)
97 | {
98 | await _batchTaskRepository.DeleteAsync(task);
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Layout/NavMenu.razor:
--------------------------------------------------------------------------------
1 | @inject TelegramPanel.Web.Modules.ModuleContributionRegistry Contributions
2 |
3 |
4 |
5 | 仪表盘
6 |
7 |
8 |
9 | 账号列表
10 | 导入账号
11 | 手机号登录
12 | 账号分类
13 |
14 |
15 |
16 | 频道列表
17 | 创建频道
18 | 频道分组
19 |
20 |
21 |
22 | 群组列表
23 |
24 |
25 |
26 | 机器人列表
27 | Bot 频道
28 |
29 |
30 |
31 | 任务中心
32 |
33 |
34 |
35 | 模块管理
36 |
37 |
38 | @if (Contributions.NavItems.Count > 0 || Contributions.Pages.Count > 0)
39 | {
40 |
41 | @foreach (var item in Contributions.NavItems
42 | .OrderBy(x => x.Definition.Group ?? "", StringComparer.OrdinalIgnoreCase)
43 | .ThenBy(x => x.Definition.Order)
44 | .ThenBy(x => x.Definition.Title, StringComparer.OrdinalIgnoreCase))
45 | {
46 |
47 | @item.Definition.Title
48 |
49 | }
50 |
51 | @foreach (var modulePage in Contributions.Pages
52 | .OrderBy(x => x.Definition.Group ?? "", StringComparer.OrdinalIgnoreCase)
53 | .ThenBy(x => x.Definition.Order)
54 | .ThenBy(x => x.Definition.Title, StringComparer.OrdinalIgnoreCase))
55 | {
56 | var href = $"/ext/{modulePage.Module.Id}/{modulePage.Definition.Key}";
57 |
58 | @modulePage.Definition.Title
59 |
60 | }
61 |
62 | }
63 |
64 |
65 | API 管理
66 |
67 |
68 |
69 | 退出登录
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/CreateExternalApiDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @inject ISnackbar Snackbar
3 | @using System.Security.Cryptography
4 | @using TelegramPanel.Modules
5 | @using TelegramPanel.Web.ExternalApi
6 |
7 |
8 |
9 |
10 |
11 | 新建一个外部 API 配置项(可在同一个接口下创建多个配置,使用不同的 `X-API-Key` 区分)。
12 |
13 |
14 |
16 |
17 |
18 | @foreach (var t in AvailableTypes)
19 | {
20 | @t.DisplayName(@t.Route)
21 | }
22 |
23 |
24 |
25 |
26 |
27 |
28 | 取消
29 | 创建
30 |
31 |
32 |
33 | @code
34 | {
35 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
36 | [Parameter] public IReadOnlyList AvailableTypes { get; set; } = Array.Empty();
37 |
38 | private string name = "";
39 | private string type = "";
40 | private bool enabled;
41 |
42 | protected override void OnInitialized()
43 | {
44 | if (string.IsNullOrWhiteSpace(type))
45 | type = AvailableTypes.FirstOrDefault()?.Type ?? "";
46 | }
47 |
48 | private void Cancel() => MudDialog.Cancel();
49 |
50 | private void Confirm()
51 | {
52 | var n = (name ?? "").Trim();
53 | if (string.IsNullOrWhiteSpace(n))
54 | {
55 | Snackbar.Add("API 名称不能为空", Severity.Warning);
56 | return;
57 | }
58 |
59 | if (AvailableTypes.All(x => !string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)))
60 | {
61 | Snackbar.Add("请选择有效的 API 类型", Severity.Warning);
62 | return;
63 | }
64 |
65 | var apiKey = GenerateKey();
66 | var def = new ExternalApiDefinition
67 | {
68 | Name = n,
69 | Type = type,
70 | Enabled = enabled,
71 | ApiKey = apiKey,
72 | Config = new System.Text.Json.Nodes.JsonObject()
73 | };
74 |
75 | if (string.Equals(type, ExternalApiTypes.Kick, StringComparison.OrdinalIgnoreCase))
76 | {
77 | def.Kick = new KickApiDefinition
78 | {
79 | BotId = 0,
80 | UseAllChats = true,
81 | ChatIds = new List(),
82 | PermanentBanDefault = false
83 | };
84 | }
85 |
86 | MudDialog.Close(DialogResult.Ok(def));
87 | }
88 |
89 | private static string GenerateKey()
90 | {
91 | var bytes = RandomNumberGenerator.GetBytes(32);
92 | var key = Convert.ToBase64String(bytes);
93 | return key.Replace('+', '-').Replace('/', '_').TrimEnd('=');
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/ModulePageDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using Microsoft.AspNetCore.Components
3 | @using TelegramPanel.Web.Modules
4 | @inject ModuleContributionRegistry Contributions
5 |
6 |
7 |
8 | @if (!string.IsNullOrWhiteSpace(error))
9 | {
10 | @error
11 | }
12 | else if (componentType == null)
13 | {
14 |
15 | }
16 | else
17 | {
18 |
19 | }
20 |
21 |
22 | 关闭
23 |
24 |
25 |
26 | @code
27 | {
28 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
29 |
30 | [Parameter] public string ModuleId { get; set; } = "";
31 | [Parameter] public string PageKey { get; set; } = "";
32 |
33 | private string? error;
34 | private Type? componentType;
35 | private Dictionary parameters = new();
36 |
37 | protected override void OnParametersSet()
38 | {
39 | error = null;
40 | componentType = null;
41 | parameters = new Dictionary();
42 |
43 | var moduleId = (ModuleId ?? "").Trim();
44 | var pageKey = (PageKey ?? "").Trim();
45 | if (moduleId.Length == 0 || pageKey.Length == 0)
46 | {
47 | error = "参数无效";
48 | return;
49 | }
50 |
51 | var page = Contributions.Pages.FirstOrDefault(p =>
52 | string.Equals(p.Module.Id, moduleId, StringComparison.OrdinalIgnoreCase)
53 | && string.Equals(p.Definition.Key, pageKey, StringComparison.OrdinalIgnoreCase));
54 |
55 | if (page == null)
56 | {
57 | error = $"未找到扩展页面:{moduleId}/{pageKey}";
58 | return;
59 | }
60 |
61 | var typeName = (page.Definition.ComponentType ?? "").Trim();
62 | if (typeName.Length == 0)
63 | {
64 | error = "模块页面未配置 componentType";
65 | return;
66 | }
67 |
68 | var moduleTypeName = NormalizeModuleTypeName(typeName);
69 | componentType =
70 | Type.GetType(typeName, throwOnError: false, ignoreCase: false)
71 | ?? page.Module.Instance.GetType().Assembly.GetType(moduleTypeName, throwOnError: false, ignoreCase: false);
72 |
73 | if (componentType == null)
74 | {
75 | error = $"无法加载组件类型:{typeName}";
76 | return;
77 | }
78 |
79 | parameters = new Dictionary
80 | {
81 | ["ModuleId"] = moduleId,
82 | ["PageKey"] = pageKey
83 | };
84 | }
85 |
86 | private static string NormalizeModuleTypeName(string typeName)
87 | {
88 | typeName = (typeName ?? "").Trim();
89 | if (typeName.Length == 0)
90 | return typeName;
91 |
92 | var comma = typeName.IndexOf(',', StringComparison.Ordinal);
93 | if (comma > 0)
94 | return typeName.Substring(0, comma).Trim();
95 |
96 | return typeName;
97 | }
98 |
99 | private void Close() => MudDialog.Close();
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Core/Interfaces/IChannelService.cs:
--------------------------------------------------------------------------------
1 | using TelegramPanel.Core.Models;
2 |
3 | namespace TelegramPanel.Core.Interfaces;
4 |
5 | ///
6 | /// 频道服务接口
7 | ///
8 | public interface IChannelService
9 | {
10 | ///
11 | /// 获取账号创建的所有频道
12 | ///
13 | Task> GetOwnedChannelsAsync(int accountId);
14 |
15 | ///
16 | /// 获取账号作为创建者/管理员的频道(包含“非本系统创建”的频道)
17 | ///
18 | Task> GetAdminedChannelsAsync(int accountId);
19 |
20 | ///
21 | /// 创建新频道
22 | ///
23 | Task CreateChannelAsync(int accountId, string title, string about, bool isPublic = false);
24 |
25 | ///
26 | /// 设置频道公开/私密
27 | ///
28 | Task SetChannelVisibilityAsync(int accountId, long channelId, bool isPublic, string? username = null);
29 |
30 | ///
31 | /// 邀请用户到频道
32 | ///
33 | Task InviteUserAsync(int accountId, long channelId, string username);
34 |
35 | ///
36 | /// 批量邀请用户
37 | ///
38 | Task> BatchInviteUsersAsync(int accountId, long channelId, List usernames, int delayMs = 2000);
39 |
40 | ///
41 | /// 设置管理员
42 | ///
43 | Task SetAdminAsync(int accountId, long channelId, string username, AdminRights rights, string title = "Admin");
44 |
45 | ///
46 | /// 批量设置管理员
47 | ///
48 | Task> BatchSetAdminsAsync(int accountId, long channelId, List requests);
49 |
50 | ///
51 | /// 设置是否允许转发(关闭后为“保护内容”,禁止转发/保存)
52 | ///
53 | Task SetForwardingAllowedAsync(int accountId, long channelId, bool allowed);
54 |
55 | ///
56 | /// 导出频道加入链接:公开频道返回 t.me 链接;私密频道导出邀请链接(需要权限)。
57 | ///
58 | Task ExportJoinLinkAsync(int accountId, long channelId);
59 |
60 | ///
61 | /// 获取频道管理员列表
62 | ///
63 | Task> GetAdminsAsync(int accountId, long channelId);
64 |
65 | ///
66 | /// 编辑频道信息(标题/简介)
67 | ///
68 | Task UpdateChannelInfoAsync(int accountId, long channelId, string title, string? about);
69 |
70 | ///
71 | /// 从频道踢出用户(通过 username),可选是否永久封禁
72 | ///
73 | Task KickUserAsync(int accountId, long channelId, string username, bool permanentBan = false);
74 | }
75 |
76 | ///
77 | /// 邀请结果
78 | ///
79 | public record InviteResult(string Username, bool Success, string? Error = null);
80 |
81 | ///
82 | /// 设置管理员结果
83 | ///
84 | public record SetAdminResult(string Username, bool Success, string? Error = null);
85 |
86 | ///
87 | /// 管理员请求
88 | ///
89 | public record AdminRequest(string Username, AdminRights Rights, string Title = "Admin");
90 |
91 | ///
92 | /// 管理员权限
93 | ///
94 | [Flags]
95 | public enum AdminRights
96 | {
97 | None = 0,
98 | ChangeInfo = 1,
99 | PostMessages = 2,
100 | EditMessages = 4,
101 | DeleteMessages = 8,
102 | BanUsers = 16,
103 | InviteUsers = 32,
104 | PinMessages = 64,
105 | ManageCall = 128,
106 | AddAdmins = 256,
107 | Anonymous = 512,
108 | ManageTopics = 1024,
109 |
110 | // 常用组合
111 | BasicAdmin = ChangeInfo | PostMessages | EditMessages | DeleteMessages | BanUsers | InviteUsers | PinMessages,
112 | FullAdmin = BasicAdmin | ManageCall | AddAdmins
113 | }
114 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/BotBanMemberDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using TelegramPanel.Data.Entities
3 |
4 |
5 |
6 |
7 |
8 | 将从 @SelectedCount 个频道移除指定用户
9 |
10 |
11 |
14 |
15 |
16 | 如何获取用户 ID?
17 |
18 | - 打开网页版:登录 Telegram 网页版 (web.telegram.org)
19 | - 找到联系人:点击进入你想查询的用户的私聊窗口
20 | - 查看链接:在浏览器地址栏中,链接最后的那串数字(如 web.telegram.org 中的
123456789)就是对方的 UserID
21 |
22 |
23 |
24 |
25 |
26 |
27 | 仅踢出
28 |
29 | (用户被移除,但可以通过邀请链接重新加入)
30 |
31 |
32 |
33 |
34 |
35 | 永久封禁
36 |
37 | (用户被封禁,无法再次加入频道)
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 取消
46 |
47 | @(banMode == BanMode.PermanentBan ? "永久封禁" : "踢出用户")
48 |
49 |
50 |
51 |
52 | @code
53 | {
54 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
55 | [Parameter] public int SelectedCount { get; set; }
56 |
57 | private string userIdInput = "";
58 | private BanMode banMode = BanMode.KickOnly;
59 |
60 | private enum BanMode
61 | {
62 | KickOnly, // 仅踢出
63 | PermanentBan // 永久封禁
64 | }
65 |
66 | private void Cancel() => MudDialog.Cancel();
67 |
68 | private void Confirm()
69 | {
70 | var input = (userIdInput ?? string.Empty).Trim();
71 | if (string.IsNullOrWhiteSpace(input))
72 | {
73 | // 空输入直接取消
74 | MudDialog.Cancel();
75 | return;
76 | }
77 |
78 | if (!long.TryParse(input, out var userId) || userId <= 0)
79 | {
80 | // 无效的用户 ID,返回错误标记
81 | MudDialog.Close(DialogResult.Cancel());
82 | return;
83 | }
84 |
85 | // 返回用户ID和封禁模式
86 | var result = new BanUserRequest(userId, banMode == BanMode.PermanentBan);
87 | MudDialog.Close(DialogResult.Ok(result));
88 | }
89 |
90 | public record BanUserRequest(long UserId, bool PermanentBan);
91 | }
92 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/RiskWarningDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @using TelegramPanel.Core.Services
3 | @using TelegramPanel.Data.Entities
4 |
5 | @* 账号风控警告对话框 *@
6 |
7 |
8 |
9 |
10 |
11 | ⚠️ 风控警告
12 |
13 |
14 | @Message
15 |
16 | @if (!string.IsNullOrWhiteSpace(DetailedMessage))
17 | {
18 |
19 | @DetailedMessage
20 |
21 | }
22 |
23 |
24 | @if (ShowRecommendations)
25 | {
26 |
27 | 建议操作:
28 |
29 |
30 |
31 | 等待满 24 小时后再进行敏感操作
32 |
33 |
34 | 降低操作频率,避免被 Telegram 风控系统标记
35 |
36 |
37 | 优先使用低风险操作替代
38 |
39 |
40 | }
41 |
42 | @if (RiskyAccounts != null && RiskyAccounts.Any())
43 | {
44 |
45 | 风险账号列表:
46 |
47 |
48 | @foreach (var account in RiskyAccounts)
49 | {
50 |
51 | @account.Phone
52 | @if (account.LastLoginAt != null)
53 | {
54 | (登录 @account.GetLoginHoursFormatted() 小时)
55 | }
56 |
57 | }
58 |
59 | }
60 |
61 |
62 |
63 | 取消操作
64 |
65 | @if (ShowExcludeOption && RiskyAccounts != null && RiskyAccounts.Any())
66 | {
67 |
68 | 排除风险账号后继续
69 |
70 | }
71 |
72 | 我了解风险,继续操作
73 |
74 |
75 |
76 |
77 | @code {
78 | [CascadingParameter]
79 | MudDialogInstance? MudDialog { get; set; }
80 |
81 | [Parameter]
82 | public string Message { get; set; } = "存在风控风险";
83 |
84 | [Parameter]
85 | public string? DetailedMessage { get; set; }
86 |
87 | [Parameter]
88 | public bool ShowRecommendations { get; set; } = true;
89 |
90 | [Parameter]
91 | public bool ShowExcludeOption { get; set; } = false;
92 |
93 | [Parameter]
94 | public List? RiskyAccounts { get; set; }
95 |
96 | private void Cancel()
97 | {
98 | MudDialog?.Close(DialogResult.Cancel());
99 | }
100 |
101 | private void Continue()
102 | {
103 | MudDialog?.Close(DialogResult.Ok(RiskWarningAction.Continue));
104 | }
105 |
106 | private void ExcludeRisky()
107 | {
108 | MudDialog?.Close(DialogResult.Ok(RiskWarningAction.ExcludeRisky));
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Pages/AdminPassword.razor:
--------------------------------------------------------------------------------
1 | @page "/admin/password"
2 | @inject TelegramPanel.Web.Services.AdminCredentialStore CredentialStore
3 | @inject ISnackbar Snackbar
4 | @inject NavigationManager Navigation
5 | @using TelegramPanel.Web.Services
6 |
7 | 修改密码 - Telegram Panel
8 |
9 | 修改后台密码
10 |
11 |
12 |
13 | @if (!CredentialStore.Enabled)
14 | {
15 |
16 | 后台验证未启用
17 |
18 | }
19 | else
20 | {
21 | @if (CredentialStore.MustChangePassword)
22 | {
23 |
24 | 当前仍为初始密码,为安全起见请立即修改。
25 |
26 | }
27 |
28 | 当前账号:@CredentialStore.Username
29 |
30 |
31 |
32 |
33 | }
34 |
35 |
36 |
37 | @if (saving)
38 | {
39 |
40 | 保存中...
41 | }
42 | else
43 | {
44 | 保存
45 | }
46 |
47 |
48 |
49 |
50 | @code
51 | {
52 | [SupplyParameterFromQuery(Name = "returnUrl")]
53 | public string? ReturnUrl { get; set; }
54 |
55 | private string currentPassword = "";
56 | private string newPassword = "";
57 | private string confirmPassword = "";
58 | private bool saving;
59 |
60 | protected override async Task OnInitializedAsync()
61 | {
62 | await CredentialStore.EnsureInitializedAsync();
63 | }
64 |
65 | private async Task Save()
66 | {
67 | if (!CredentialStore.Enabled)
68 | {
69 | Snackbar.Add("后台验证未启用", Severity.Warning);
70 | return;
71 | }
72 |
73 | if (saving)
74 | return;
75 |
76 | if (string.IsNullOrWhiteSpace(currentPassword))
77 | {
78 | Snackbar.Add("请输入当前密码", Severity.Warning);
79 | return;
80 | }
81 |
82 | if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Trim().Length < 6)
83 | {
84 | Snackbar.Add("新密码至少 6 位", Severity.Warning);
85 | return;
86 | }
87 |
88 | if (!string.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
89 | {
90 | Snackbar.Add("两次输入的新密码不一致", Severity.Warning);
91 | return;
92 | }
93 |
94 | saving = true;
95 | try
96 | {
97 | await CredentialStore.ChangePasswordAsync(currentPassword, newPassword);
98 | currentPassword = "";
99 | newPassword = "";
100 | confirmPassword = "";
101 | Snackbar.Add("密码已更新", Severity.Success);
102 |
103 | var target = AdminAuthHelpers.IsLocalReturnUrl(ReturnUrl) ? ReturnUrl! : "/";
104 | Navigation.NavigateTo(target);
105 | }
106 | catch (Exception ex)
107 | {
108 | Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
109 | }
110 | finally
111 | {
112 | saving = false;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Modules.Abstractions/ModuleContributions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace TelegramPanel.Modules;
4 |
5 | public interface IModuleUiProvider
6 | {
7 | IEnumerable GetNavItems(ModuleHostContext context);
8 | IEnumerable GetPages(ModuleHostContext context);
9 | }
10 |
11 | public sealed class ModuleNavItem
12 | {
13 | [JsonPropertyName("title")]
14 | public string Title { get; set; } = "";
15 |
16 | [JsonPropertyName("href")]
17 | public string Href { get; set; } = "";
18 |
19 | [JsonPropertyName("icon")]
20 | public string Icon { get; set; } = "";
21 |
22 | [JsonPropertyName("group")]
23 | public string? Group { get; set; }
24 |
25 | [JsonPropertyName("order")]
26 | public int Order { get; set; } = 0;
27 | }
28 |
29 | public sealed class ModulePageDefinition
30 | {
31 | [JsonPropertyName("key")]
32 | public string Key { get; set; } = "";
33 |
34 | [JsonPropertyName("title")]
35 | public string Title { get; set; } = "";
36 |
37 | [JsonPropertyName("icon")]
38 | public string Icon { get; set; } = "";
39 |
40 | ///
41 | /// 组件类型的 AssemblyQualifiedName(用于 DynamicComponent 渲染)。
42 | ///
43 | [JsonPropertyName("componentType")]
44 | public string ComponentType { get; set; } = "";
45 |
46 | [JsonPropertyName("group")]
47 | public string? Group { get; set; }
48 |
49 | [JsonPropertyName("order")]
50 | public int Order { get; set; } = 0;
51 | }
52 |
53 | public interface IModuleTaskProvider
54 | {
55 | IEnumerable GetTasks(ModuleHostContext context);
56 | }
57 |
58 | public sealed class ModuleTaskDefinition
59 | {
60 | ///
61 | /// 任务分类:例如 user / bot / system(建议使用小写)。
62 | ///
63 | [JsonPropertyName("category")]
64 | public string Category { get; set; } = "";
65 |
66 | ///
67 | /// 任务类型常量(数据库 BatchTask.TaskType)。
68 | ///
69 | [JsonPropertyName("taskType")]
70 | public string TaskType { get; set; } = "";
71 |
72 | [JsonPropertyName("displayName")]
73 | public string DisplayName { get; set; } = "";
74 |
75 | [JsonPropertyName("description")]
76 | public string? Description { get; set; }
77 |
78 | [JsonPropertyName("icon")]
79 | public string Icon { get; set; } = "";
80 |
81 | ///
82 | /// 如果提供了创建页面路由,则任务中心“新建任务”会跳转到该页面创建。
83 | ///
84 | [JsonPropertyName("createRoute")]
85 | public string? CreateRoute { get; set; }
86 |
87 | ///
88 | /// 任务创建编辑器组件类型 AssemblyQualifiedName(可选)。\n /// 该组件需要支持参数:\n /// - Draft (ModuleTaskDraft)\n /// - DraftChanged (EventCallback<ModuleTaskDraft>)\n ///
89 | [JsonPropertyName("editorComponentType")]
90 | public string? EditorComponentType { get; set; }
91 |
92 | [JsonPropertyName("order")]
93 | public int Order { get; set; } = 0;
94 | }
95 |
96 | public readonly record struct ModuleTaskDraft(int Total, string? Config, bool CanSubmit, string? ValidationError);
97 |
98 | public interface IModuleApiProvider
99 | {
100 | IEnumerable GetApis(ModuleHostContext context);
101 | }
102 |
103 | public sealed class ModuleApiTypeDefinition
104 | {
105 | [JsonPropertyName("type")]
106 | public string Type { get; set; } = "";
107 |
108 | [JsonPropertyName("displayName")]
109 | public string DisplayName { get; set; } = "";
110 |
111 | [JsonPropertyName("route")]
112 | public string Route { get; set; } = "";
113 |
114 | [JsonPropertyName("description")]
115 | public string? Description { get; set; }
116 |
117 | [JsonPropertyName("order")]
118 | public int Order { get; set; } = 0;
119 | }
120 |
121 | public interface IModuleTaskHandler
122 | {
123 | string TaskType { get; }
124 | Task ExecuteAsync(IModuleTaskExecutionHost host, CancellationToken cancellationToken);
125 | }
126 |
127 | public interface IModuleTaskExecutionHost
128 | {
129 | int TaskId { get; }
130 | string TaskType { get; }
131 | int Total { get; }
132 | string? Config { get; }
133 |
134 | IServiceProvider Services { get; }
135 |
136 | Task IsStillRunningAsync(CancellationToken cancellationToken);
137 | Task UpdateProgressAsync(int completed, int failed, CancellationToken cancellationToken);
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Modules.Abstractions/SemVer.cs:
--------------------------------------------------------------------------------
1 | namespace TelegramPanel.Modules;
2 |
3 | public readonly record struct SemVer(int Major, int Minor, int Patch) : IComparable
4 | {
5 | public static bool TryParse(string? value, out SemVer ver)
6 | {
7 | ver = default;
8 | value = (value ?? string.Empty).Trim();
9 | if (value.Length == 0)
10 | return false;
11 |
12 | // 只支持 x.y.z,忽略预发布/构建元数据
13 | var parts = value.Split('.', StringSplitOptions.RemoveEmptyEntries);
14 | if (parts.Length < 1 || parts.Length > 3)
15 | return false;
16 |
17 | if (!int.TryParse(parts[0], out var major))
18 | return false;
19 | var minor = 0;
20 | var patch = 0;
21 | if (parts.Length >= 2 && !int.TryParse(parts[1], out minor))
22 | return false;
23 | if (parts.Length >= 3 && !int.TryParse(parts[2], out patch))
24 | return false;
25 |
26 | ver = new SemVer(major, minor, patch);
27 | return true;
28 | }
29 |
30 | public int CompareTo(SemVer other)
31 | {
32 | var c = Major.CompareTo(other.Major);
33 | if (c != 0) return c;
34 | c = Minor.CompareTo(other.Minor);
35 | if (c != 0) return c;
36 | return Patch.CompareTo(other.Patch);
37 | }
38 |
39 | public override string ToString() => $"{Major}.{Minor}.{Patch}";
40 | }
41 |
42 | public sealed class VersionRange
43 | {
44 | private readonly List> _predicates = new();
45 |
46 | public static bool TryParse(string? expression, out VersionRange range, out string error)
47 | {
48 | range = new VersionRange();
49 | error = "";
50 |
51 | expression = (expression ?? string.Empty).Trim();
52 | if (expression.Length == 0)
53 | {
54 | error = "range 为空";
55 | return false;
56 | }
57 |
58 | var parts = expression.Split(' ', StringSplitOptions.RemoveEmptyEntries);
59 | foreach (var raw in parts)
60 | {
61 | var token = raw.Trim();
62 | if (token.Length == 0)
63 | continue;
64 |
65 | if (SemVer.TryParse(token, out var exact))
66 | {
67 | range._predicates.Add(v => v == exact);
68 | continue;
69 | }
70 |
71 | if (token.StartsWith(">=", StringComparison.Ordinal))
72 | {
73 | if (!SemVer.TryParse(token.Substring(2), out var min))
74 | {
75 | error = $"无法解析版本:{token}";
76 | return false;
77 | }
78 |
79 | range._predicates.Add(v => v.CompareTo(min) >= 0);
80 | continue;
81 | }
82 |
83 | if (token.StartsWith(">", StringComparison.Ordinal))
84 | {
85 | if (!SemVer.TryParse(token.Substring(1), out var min))
86 | {
87 | error = $"无法解析版本:{token}";
88 | return false;
89 | }
90 |
91 | range._predicates.Add(v => v.CompareTo(min) > 0);
92 | continue;
93 | }
94 |
95 | if (token.StartsWith("<=", StringComparison.Ordinal))
96 | {
97 | if (!SemVer.TryParse(token.Substring(2), out var max))
98 | {
99 | error = $"无法解析版本:{token}";
100 | return false;
101 | }
102 |
103 | range._predicates.Add(v => v.CompareTo(max) <= 0);
104 | continue;
105 | }
106 |
107 | if (token.StartsWith("<", StringComparison.Ordinal))
108 | {
109 | if (!SemVer.TryParse(token.Substring(1), out var max))
110 | {
111 | error = $"无法解析版本:{token}";
112 | return false;
113 | }
114 |
115 | range._predicates.Add(v => v.CompareTo(max) < 0);
116 | continue;
117 | }
118 |
119 | error = $"不支持的 range token:{token}";
120 | return false;
121 | }
122 |
123 | if (range._predicates.Count == 0)
124 | {
125 | error = "range 无有效条件";
126 | return false;
127 | }
128 |
129 | return true;
130 | }
131 |
132 | public bool Contains(SemVer version)
133 | {
134 | foreach (var p in _predicates)
135 | {
136 | if (!p(version))
137 | return false;
138 | }
139 |
140 | return true;
141 | }
142 | }
143 |
144 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Components/Dialogs/EditGenericApiDialog.razor:
--------------------------------------------------------------------------------
1 | @namespace TelegramPanel.Web.Components.Dialogs
2 | @inject ISnackbar Snackbar
3 | @using System.Security.Cryptography
4 | @using System.Text.Json
5 | @using System.Text.Json.Nodes
6 | @using TelegramPanel.Web.ExternalApi
7 |
8 |
9 |
10 |
11 |
12 | 外部接口:@(string.IsNullOrWhiteSpace(Route) ? "-" : Route)(Type: @(string.IsNullOrWhiteSpace(Api.Type) ? "-" : Api.Type))
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 | 重新生成 Key
23 |
24 |
25 |
27 |
28 |
29 |
30 | 取消
31 |
32 | @if (saving)
33 | {
34 |
35 | 保存中...
36 | }
37 | else
38 | {
39 | 保存
40 | }
41 |
42 |
43 |
44 |
45 | @code
46 | {
47 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
48 |
49 | [Parameter] public ExternalApiDefinition Api { get; set; } = new();
50 | [Parameter] public string? Route { get; set; }
51 |
52 | private bool saving;
53 | private ExternalApiDefinition model = new();
54 | private string configJson = "{}";
55 |
56 | protected override void OnInitialized()
57 | {
58 | model = Clone(Api);
59 | var cfg = model.Config ?? new JsonObject();
60 | configJson = cfg.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
61 | }
62 |
63 | private void Cancel() => MudDialog.Cancel();
64 |
65 | private void RegenerateKey()
66 | {
67 | var bytes = RandomNumberGenerator.GetBytes(32);
68 | var key = Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');
69 | model.ApiKey = key;
70 | Snackbar.Add("已重新生成 Key(记得保存)", Severity.Info);
71 | }
72 |
73 | private void Save()
74 | {
75 | if (saving)
76 | return;
77 |
78 | var n = (model.Name ?? "").Trim();
79 | if (string.IsNullOrWhiteSpace(n))
80 | {
81 | Snackbar.Add("API 名称不能为空", Severity.Warning);
82 | return;
83 | }
84 |
85 | var key = (model.ApiKey ?? "").Trim();
86 | if (string.IsNullOrWhiteSpace(key))
87 | {
88 | Snackbar.Add("X-API-Key 不能为空", Severity.Warning);
89 | return;
90 | }
91 |
92 | try
93 | {
94 | var parsed = JsonNode.Parse(configJson ?? "{}");
95 | if (parsed is not JsonObject obj)
96 | {
97 | Snackbar.Add("Config 必须是 JSON 对象(例如 {})", Severity.Warning);
98 | return;
99 | }
100 |
101 | model.Name = n;
102 | model.ApiKey = key;
103 | model.Config = obj;
104 | }
105 | catch (Exception ex)
106 | {
107 | Snackbar.Add($"Config JSON 无效:{ex.Message}", Severity.Warning);
108 | return;
109 | }
110 |
111 | MudDialog.Close(DialogResult.Ok(model));
112 | }
113 |
114 | private static ExternalApiDefinition Clone(ExternalApiDefinition src)
115 | {
116 | // 仅用于编辑器内复制(避免直接改引用)
117 | return new ExternalApiDefinition
118 | {
119 | Id = src.Id,
120 | Name = src.Name,
121 | Type = src.Type,
122 | Enabled = src.Enabled,
123 | ApiKey = src.ApiKey,
124 | Kick = src.Kick,
125 | Config = src.Config ?? new JsonObject()
126 | };
127 | }
128 | }
129 |
130 |
--------------------------------------------------------------------------------
/src/TelegramPanel.Web/Services/DataSyncService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using TelegramPanel.Core.Interfaces;
3 | using TelegramPanel.Core.Services;
4 | using TelegramPanel.Data.Entities;
5 |
6 | namespace TelegramPanel.Web.Services;
7 |
8 | ///
9 | /// 数据同步服务(页面层复用:Home/Settings/Channels)
10 | ///
11 | public class DataSyncService
12 | {
13 | private readonly AccountManagementService _accountManagement;
14 | private readonly ChannelManagementService _channelManagement;
15 | private readonly GroupManagementService _groupManagement;
16 | private readonly IChannelService _channelService;
17 | private readonly IGroupService _groupService;
18 | private readonly ILogger _logger;
19 |
20 | public DataSyncService(
21 | AccountManagementService accountManagement,
22 | ChannelManagementService channelManagement,
23 | GroupManagementService groupManagement,
24 | IChannelService channelService,
25 | IGroupService groupService,
26 | ILogger logger)
27 | {
28 | _accountManagement = accountManagement;
29 | _channelManagement = channelManagement;
30 | _groupManagement = groupManagement;
31 | _channelService = channelService;
32 | _groupService = groupService;
33 | _logger = logger;
34 | }
35 |
36 | public async Task SyncAllActiveAccountsAsync(CancellationToken cancellationToken)
37 | {
38 | var accounts = await _accountManagement.GetActiveAccountsAsync();
39 | return await SyncAccountsAsync(accounts, cancellationToken);
40 | }
41 |
42 | public async Task SyncAccountAsync(int accountId, CancellationToken cancellationToken)
43 | {
44 | var account = await _accountManagement.GetAccountAsync(accountId)
45 | ?? throw new InvalidOperationException($"账号不存在:{accountId}");
46 |
47 | return await SyncAccountsAsync(new[] { account }, cancellationToken);
48 | }
49 |
50 | public async Task SyncAccountsAsync(IEnumerable