@page "/modules"
@inject TelegramPanel.Web.Modules.ModuleInstallerService ModuleInstaller
@inject TelegramPanel.Web.Modules.ModuleContributionRegistry Contributions
@inject TelegramPanel.Web.Services.AppRestartService Restart
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using Microsoft.AspNetCore.Components.Forms
@using TelegramPanel.Modules
@using TelegramPanel.Web.Modules
模块管理 - Telegram Panel
模块管理
本程序的模块会在启动时加载(不支持真正的运行时热插拔)。
但在 Docker 部署下可开启“自动重启服务”,会由容器自动拉起(无需手动重启容器),更新体验接近热插拔。
选择模块包(.tpm/.zip)
@(selectedFile == null ? "未选择文件" : $"已选择:{selectedFile.Name}({selectedFile.Size} bytes)")
@if (installing)
{
上传中...
}
else
{
上传
}
@if (Contributions.Diagnostics.Count > 0)
{
检测到模块贡献冲突(已自动忽略部分定义):
@foreach (var d in Contributions.Diagnostics)
{
@d
}
}
已安装模块 (@modules.Count)
刷新
模块
版本
状态
操作
@GetDisplayName(context)
@context.Id
@if (!string.IsNullOrWhiteSpace(context.ManifestError))
{
@context.ManifestError
}
@context.ActiveVersion
@if (!string.IsNullOrWhiteSpace(context.LastGoodVersion))
{
last-good: @context.LastGoodVersion
}
@(context.Enabled ? "启用" : "停用")
@if (context.BuiltIn)
{
内置
}
@if (context.Enabled)
{
停用
}
else
{
启用
}
@if (!context.BuiltIn)
{
@if (context.InstalledVersions.Count > 1)
{
清理旧版本
}
删除
}
详情
暂无模块
@code
{
private bool loading = true;
private bool installing;
private IBrowserFile? selectedFile;
private List modules = new();
private bool autoRestartOnChange = true;
private bool activateAndEnableAfterInstall = true;
protected override async Task OnInitializedAsync()
{
await Reload();
}
private async Task Reload()
{
loading = true;
try
{
var list = await ModuleInstaller.GetOverviewAsync();
modules = list.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载失败:{ex.Message}", Severity.Error);
}
finally
{
loading = false;
}
}
private void OnFilesChanged(IReadOnlyList files)
{
selectedFile = files.FirstOrDefault();
}
private async Task Install()
{
if (selectedFile == null || installing)
return;
installing = true;
try
{
await using var stream = selectedFile.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024);
var result = await ModuleInstaller.InstallAsync(stream, selectedFile.Name, enableAfterInstall: false);
if (!result.Success)
{
Snackbar.Add($"上传失败:{result.Message}", Severity.Error);
return;
}
if (activateAndEnableAfterInstall)
{
var moduleId = (result.ModuleId ?? "").Trim();
var version = (result.Version ?? "").Trim();
if (moduleId.Length == 0 || version.Length == 0)
{
Snackbar.Add("上传成功,但返回的模块信息不完整(ModuleId/Version 为空)", Severity.Warning);
selectedFile = null;
await Reload();
return;
}
var setActive = await ModuleInstaller.SetActiveVersionAsync(moduleId, version);
if (!setActive.Success)
{
Snackbar.Add($"已上传:{moduleId} {version},但切换版本失败:{setActive.Message}", Severity.Warning);
}
else
{
var enable = await ModuleInstaller.EnableAsync(moduleId);
if (!enable.Success)
{
Snackbar.Add($"已上传:{moduleId} {version},但启用失败:{enable.Message}", Severity.Warning);
}
else
{
Snackbar.Add($"已上传并启用:{moduleId} {version}", Severity.Success);
if (autoRestartOnChange)
{
Snackbar.Add("即将自动重启服务以应用模块变更…", Severity.Info);
Restart.RequestRestart(TimeSpan.FromSeconds(1), $"module install {moduleId} {version}");
}
}
}
}
else
{
Snackbar.Add($"上传成功:{result.ModuleId} {result.Version}(请切换版本/启用并重启服务生效)", Severity.Success);
}
selectedFile = null;
await Reload();
}
catch (Exception ex)
{
Snackbar.Add($"上传异常:{ex.Message}", Severity.Error);
}
finally
{
installing = false;
}
}
private async Task Enable(string id)
{
var r = await ModuleInstaller.EnableAsync(id);
if (!r.Success)
{
Snackbar.Add($"启用失败:{r.Message}", Severity.Error);
return;
}
Snackbar.Add("已启用", Severity.Success);
if (autoRestartOnChange)
{
Snackbar.Add("即将自动重启服务以应用模块变更…", Severity.Info);
Restart.RequestRestart(TimeSpan.FromSeconds(1), $"module enable {id}");
}
await Reload();
}
private async Task Disable(string id)
{
var r = await ModuleInstaller.DisableAsync(id);
if (!r.Success)
{
Snackbar.Add($"停用失败:{r.Message}", Severity.Error);
return;
}
Snackbar.Add("已停用", Severity.Success);
if (autoRestartOnChange)
{
Snackbar.Add("即将自动重启服务以应用模块变更…", Severity.Info);
Restart.RequestRestart(TimeSpan.FromSeconds(1), $"module disable {id}");
}
await Reload();
}
private async Task Remove(string id)
{
bool? ok = await DialogService.ShowMessageBox(
"确认删除",
$"确定删除模块「{id}」吗?删除会移动到回收站目录(可手动找回),但仍建议先停用并重启确认无依赖。",
yesText: "删除", cancelText: "取消");
if (ok != true)
return;
try
{
var r = await ModuleInstaller.RemoveModuleAsync(id);
if (!r.Success)
{
Snackbar.Add(r.Message, Severity.Error);
return;
}
Snackbar.Add("已删除", Severity.Success);
if (autoRestartOnChange)
{
Snackbar.Add("即将自动重启服务以应用模块变更…", Severity.Info);
Restart.RequestRestart(TimeSpan.FromSeconds(1), $"module remove {id}");
}
await Reload();
}
catch (Exception ex)
{
Snackbar.Add($"删除异常:{ex.Message}", Severity.Error);
}
}
private async Task PruneOldVersions(ModuleOverview m)
{
if (m.BuiltIn)
return;
var active = (m.ActiveVersion ?? "").Trim();
var lastGood = (m.LastGoodVersion ?? "").Trim();
var keep = new HashSet(StringComparer.Ordinal);
if (!string.IsNullOrWhiteSpace(active)) keep.Add(active);
if (!string.IsNullOrWhiteSpace(lastGood)) keep.Add(lastGood);
var toRemove = m.InstalledVersions
.Where(v => !string.IsNullOrWhiteSpace(v))
.Distinct(StringComparer.Ordinal)
.Where(v => !keep.Contains(v))
.ToList();
if (toRemove.Count == 0)
{
Snackbar.Add("没有可清理的旧版本", Severity.Info);
return;
}
bool? ok = await DialogService.ShowMessageBox(
"清理旧版本",
$"将删除模块「{m.Id}」的旧版本:{string.Join(", ", toRemove)}。\n" +
$"保留版本:{string.Join(", ", keep)}。\n" +
"删除会移动到回收站目录(可手动找回),是否继续?",
yesText: "继续", cancelText: "取消");
if (ok != true)
return;
try
{
var r = await ModuleInstaller.PruneOldVersionsAsync(m.Id);
if (!r.Success)
{
Snackbar.Add(r.Message, Severity.Error);
return;
}
Snackbar.Add("已清理旧版本", Severity.Success);
if (autoRestartOnChange)
{
Snackbar.Add("即将自动重启服务以应用模块变更…", Severity.Info);
Restart.RequestRestart(TimeSpan.FromSeconds(1), $"module prune {m.Id}");
}
await Reload();
}
catch (Exception ex)
{
Snackbar.Add($"清理异常:{ex.Message}", Severity.Error);
}
}
private async Task ShowDetails(ModuleOverview m)
{
var manifest = m.Manifest;
var deps = manifest?.Dependencies?.Count > 0
? string.Join("\n", manifest.Dependencies.Select(d => $"{d.Id} {d.Range}"))
: "-";
var details =
$"Id: {m.Id}\n" +
$"Name: {manifest?.Name ?? "-"}\n" +
$"Version: {m.ActiveVersion ?? "-"}\n" +
$"Enabled: {m.Enabled}\n" +
$"BuiltIn: {m.BuiltIn}\n" +
$"InstalledVersions: {string.Join(", ", m.InstalledVersions)}\n" +
$"Dependencies:\n{deps}\n";
await DialogService.ShowMessageBox("模块详情", details);
}
private static string GetDisplayName(ModuleOverview m)
{
var name = (m.Manifest?.Name ?? "").Trim();
return string.IsNullOrWhiteSpace(name) ? m.Id : name;
}
}