@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; } }