Added a confirmation dialog before importing assistant plugins

This commit is contained in:
Thorsten Sommer 2026-08-06 07:57:09 +02:00
parent c5103485c4
commit f0c35d8914
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
14 changed files with 466 additions and 39 deletions

View File

@ -5359,6 +5359,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
@ -5449,6 +5452,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Accept the ter
-- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept GPL and archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking \"Accept GPL and archive,\" you agree to the terms of the GPL license. Software under GPL is free of charge and free to use."
-- Install plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Install plugin"
-- Version
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version"
-- Source
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Source"
-- You are about to install a language plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "You are about to install a language plugin from a file."
-- Authors
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Authors"
-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source."
-- You are about to install an assistant plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "You are about to install an assistant plugin from a file."
-- You are about to install a configuration plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "You are about to install a configuration plugin from a file."
-- You are about to install a theme plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "You are about to install a theme plugin from a file."
-- Support contact
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact"
-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}."
-- Unknown
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unknown"
-- Type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Type"
-- Replace plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin"
-- The authors marked this plugin as deprecated: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}"
-- You are about to install a plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "You are about to install a plugin from a file."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel"
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
@ -7942,6 +7996,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
-- Revise Assistant Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Revise Assistant Plugin"
-- Import not possible
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import not possible"
-- The assistant plugin '{0}' has been successfully saved.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
@ -7975,6 +8032,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
-- Export plugin archive
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Export plugin archive"
-- Install Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Install Plugin"
-- Assistant updated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistant updated."
@ -9517,6 +9577,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
-- Internal assistant plugins cannot be deleted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted."
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
-- The assistant plugin directory is outside the local assistant plugin directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory."
@ -9550,6 +9613,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223
-- Unexpected error: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
-- Config server managed assistant plugins cannot be replaced.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Config server managed assistant plugins cannot be replaced."
-- The assistant plugin has no local directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."

View File

@ -0,0 +1,16 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudStack Row="true" AlignItems="AlignItems.Start" Spacing="3">
<MudIcon Icon="@this.Icon" Color="@this.IconColor" Size="Size.Large"/>
<MudJustifiedText Typo="Typo.body1">
@this.Message
</MudJustifiedText>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -0,0 +1,35 @@
using AIStudio.Components;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
/// <summary>
/// A dialog that informs the user about something without asking for a decision. Use it when a
/// message must not be missed, e.g., when an action was refused.
/// </summary>
public partial class InformationDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The message shown to the user.
/// </summary>
[Parameter]
public string Message { get; set; } = string.Empty;
/// <summary>
/// The icon shown next to the message.
/// </summary>
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.Info;
/// <summary>
/// The color of the icon.
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Info;
private void Close() => this.MudDialog.Close(DialogResult.Ok(true));
}

View File

@ -0,0 +1,60 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@this.IntroductionText @T("Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.")
</MudJustifiedText>
<MudPaper Class="pa-3 mb-3 border-dashed border rounded-lg">
<MudText Typo="Typo.h6">
@this.Preview.Plugin.Name
</MudText>
<MudText Typo="Typo.body2" Class="mb-2">
@this.Preview.Plugin.Description
</MudText>
<MudText Typo="Typo.body2">
@T("Type"): <strong>@this.TypeLabel</strong>
</MudText>
<MudText Typo="Typo.body2">
@T("Version"): <strong>@this.Preview.Plugin.Version</strong>
</MudText>
<MudText Typo="Typo.body2">
@T("Authors"): <strong>@this.AuthorsLabel</strong>
</MudText>
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SourceURL))
{
<MudText Typo="Typo.body2">
@T("Source"): <strong>@this.Preview.Plugin.SourceURL</strong>
</MudText>
}
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.SupportContact))
{
<MudText Typo="Typo.body2">
@T("Support contact"): <strong>@this.Preview.Plugin.SupportContact</strong>
</MudText>
}
</MudPaper>
@if (!string.IsNullOrWhiteSpace(this.Preview.Plugin.DeprecationMessage))
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
@string.Format(T("The authors marked this plugin as deprecated: {0}"), this.Preview.Plugin.DeprecationMessage)
</MudAlert>
}
@if (this.Preview.ExistingPlugin is { } existingPlugin)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
@string.Format(T("This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}."), existingPlugin.Name, existingPlugin.Version, this.Preview.Plugin.Version)
</MudAlert>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Warning">
@(this.Preview.ReplacesExisting ? T("Replace plugin") : T("Install plugin"))
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -0,0 +1,47 @@
using AIStudio.Components;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
/// <summary>
/// Asks the user whether a plugin archive may be installed. It shows the metadata the archive
/// declares about itself, so the user can judge the plugin before its code runs.
/// </summary>
public partial class PluginImportDialog : MSGComponentBase
{
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The metadata of the plugin archive about to be installed.
/// </summary>
[Parameter]
public PluginImportPreview Preview { get; set; } = null!;
/// <summary>
/// Names the kind of plugin the user is about to install. Each plugin type gets its own
/// sentence instead of a placeholder because articles and word order differ between languages.
/// </summary>
private string IntroductionText => this.Preview.Plugin.Type switch
{
PluginType.LANGUAGE => this.T("You are about to install a language plugin from a file."),
PluginType.ASSISTANT => this.T("You are about to install an assistant plugin from a file."),
PluginType.CONFIGURATION => this.T("You are about to install a configuration plugin from a file."),
PluginType.THEME => this.T("You are about to install a theme plugin from a file."),
_ => this.T("You are about to install a plugin from a file."),
};
private string TypeLabel => this.Preview.Plugin.Type.GetName();
private string AuthorsLabel => this.Preview.Plugin.Authors.Length > 0
? string.Join(", ", this.Preview.Plugin.Authors)
: this.T("Unknown");
private void Cancel() => this.MudDialog.Cancel();
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
}

View File

@ -328,11 +328,17 @@ public partial class Plugins : MSGComponentBase
if (selection.UserCancelled)
return;
var result = await this.AssistantPluginInstallService.InstallArchiveAsync(selection.SelectedFilePath, CancellationToken.None);
var result = await this.AssistantPluginInstallService.InstallArchiveAsync(selection.SelectedFilePath, this.ConfirmPluginImportAsync, CancellationToken.None);
if (result.Cancelled)
return;
if (!result.Success)
{
LOG.LogError("Failed to import assistant plugin archive '{ArchivePath}': {Issue}", selection.SelectedFilePath, result.Issue);
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, string.Format(this.T("The assistant plugin could not be imported: {0}"), result.Issue)));
// The user actively started this import, so we report the reason in a dialog
// instead of a snackbar. Refused imports must not be missed:
await this.ShowImportRefusedDialogAsync(result.Issue);
return;
}
@ -350,6 +356,36 @@ public partial class Plugins : MSGComponentBase
}
}
/// <summary>
/// Shows the metadata of a validated plugin archive and asks whether it may be installed.
/// </summary>
/// <param name="preview">The metadata the archive declares about itself.</param>
/// <returns>True when the user confirmed the installation.</returns>
private async Task<bool> ConfirmPluginImportAsync(PluginImportPreview preview)
{
var dialogParameters = new DialogParameters<PluginImportDialog>
{
{ x => x.Preview, preview },
};
var dialogReference = await this.DialogService.ShowAsync<PluginImportDialog>(this.T("Install Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
return dialogResult is { Canceled: false };
}
private async Task ShowImportRefusedDialogAsync(string issue)
{
var dialogParameters = new DialogParameters<InformationDialog>
{
{ x => x.Message, string.Format(this.T("The assistant plugin could not be imported: {0}"), issue) },
{ x => x.Icon, Icons.Material.Filled.ReportProblem },
{ x => x.IconColor, Color.Error },
};
var dialogReference = await this.DialogService.ShowAsync<InformationDialog>(this.T("Import not possible"), dialogParameters, DialogOptions.FULLSCREEN);
await dialogReference.Result;
}
private static bool IsSendingMail(string sourceUrl) => sourceUrl.TrimStart().StartsWith("mailto:", StringComparison.OrdinalIgnoreCase);
private PluginAssistants? TryGetAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == pluginId);

View File

@ -5361,6 +5361,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einb
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Schließen"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems."
@ -5451,6 +5454,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Akzeptieren Si
-- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept the GPL and download the archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc wird unter der GNU General Public License v2 (GPL) vertrieben. Wenn Sie auf „GPL akzeptieren und Archiv herunterladen“ klicken, stimmen Sie den Bedingungen der GPL-Lizenz zu. Software unter der GPL ist kostenlos und frei nutzbar."
-- Install plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Plugin installieren"
-- Version
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version"
-- Source
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Quelle"
-- You are about to install a language plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "Sie sind dabei, ein Sprach-Plugin aus einer Datei zu installieren."
-- Authors
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Autor:innen"
-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins enthalten Code, der innerhalb von AI Studio ausgeführt wird. Installieren Sie Plugins nur, wenn Sie der Quelle vertrauen."
-- You are about to install an assistant plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "Sie sind dabei, ein Assistenten-Plugin aus einer Datei zu installieren."
-- You are about to install a configuration plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "Sie sind dabei, ein Konfigurations-Plugin aus einer Datei zu installieren."
-- You are about to install a theme plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "Sie sind dabei, ein Design-Plugin aus einer Datei zu installieren."
-- Support contact
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Supportkontakt"
-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "Dies ersetzt das bereits installierte Plugin „{0}“. Version {1} wird durch Version {2} ersetzt."
-- Unknown
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unbekannt"
-- Type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Typ"
-- Replace plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Plugin ersetzen"
-- The authors marked this plugin as deprecated: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "Die Autoren haben dieses Plugin als veraltet gekennzeichnet: {0}"
-- You are about to install a plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "Sie sind dabei, ein Plugin aus einer Datei zu installieren."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Abbrechen"
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Teilen Sie der KI mit, was sie machen soll. Was sind ihre Ziele oder was möchten Sie erreichen? Zum Beispiel, dass die KI Sie duzt."
@ -7944,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Aktivierte Plugins"
-- Revise Assistant Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Assistenten-Plugin überarbeiten"
-- Import not possible
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import nicht möglich"
-- The assistant plugin '{0}' has been successfully saved.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "Das Assistent-Plugin „{0}“ wurde erfolgreich gespeichert."
@ -7959,7 +8016,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Ihre Organisation ha
-- Share plugin archive
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Plugin-Archiv teilen"
-- Your organization has disabled sharing plugins.
-- Your organization has disabled sharing plugins
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Ihre Organisation hat das Teilen von Plugins deaktiviert"
-- Close
@ -7977,6 +8034,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Aktionen"
-- Export plugin archive
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Plugin-Archiv exportieren"
-- Install Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Plugin installieren"
-- Assistant updated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistent aktualisiert."
@ -9519,6 +9579,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
-- Internal assistant plugins cannot be deleted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Interne Assistenten-Plugins können nicht gelöscht werden."
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "Dieses Plugin-Archiv gibt an, von einem Konfigurationsserver verwaltet zu werden. Nur die IT-Abteilung Ihrer Organisation kann solche Plugins bereitstellen."
-- The assistant plugin directory is outside the local assistant plugin directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "Das Assistenten-Plugin-Verzeichnis befindet sich außerhalb des lokalen Assistenten-Plugin-Verzeichnisses."
@ -9552,6 +9615,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223
-- Unexpected error: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unerwarteter Fehler: {0}"
-- Config server managed assistant plugins cannot be replaced.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Vom Konfigurationsserver verwaltete Assistenten-Plugins können nicht ersetzt werden."
-- The assistant plugin has no local directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "Das Assistenten-Plugin hat kein lokales Verzeichnis."

View File

@ -5361,6 +5361,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close"
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
@ -5451,6 +5454,57 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T504404155"] = "Accept the ter
-- Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking "Accept the GPL and download the archive," you agree to the terms of the GPL license. Software under GPL is free of charge and free to use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is distributed under the GNU General Public License v2 (GPL). By clicking \"Accept the GPL and download the archive,\" you agree to the terms of the GPL license. Software under GPL is free of charge and free to use."
-- Install plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1525735539"] = "Install plugin"
-- Version
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1573770551"] = "Version"
-- Source
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1642243064"] = "Source"
-- You are about to install a language plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1974491324"] = "You are about to install a language plugin from a file."
-- Authors
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1985367263"] = "Authors"
-- Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2053517490"] = "Plugins contain code that runs inside AI Studio. Install plugins only when you trust their source."
-- You are about to install an assistant plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2063808316"] = "You are about to install an assistant plugin from a file."
-- You are about to install a configuration plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T21052500"] = "You are about to install a configuration plugin from a file."
-- You are about to install a theme plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2163853103"] = "You are about to install a theme plugin from a file."
-- Support contact
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact"
-- This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3415610475"] = "This replaces the already installed plugin '{0}'. Version {1} gets replaced by version {2}."
-- Unknown
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3424652889"] = "Unknown"
-- Type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3512062061"] = "Type"
-- Replace plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin"
-- The authors marked this plugin as deprecated: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}"
-- You are about to install a plugin from a file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T841685558"] = "You are about to install a plugin from a file."
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel"
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
@ -7944,6 +7998,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
-- Revise Assistant Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Revise Assistant Plugin"
-- Import not possible
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3051566124"] = "Import not possible"
-- The assistant plugin '{0}' has been successfully saved.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
@ -7959,7 +8016,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Your organization ha
-- Share plugin archive
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Share plugin archive"
-- Your organization has disabled sharing plugins.
-- Your organization has disabled sharing plugins
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Your organization has disabled sharing plugins"
-- Close
@ -7977,6 +8034,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
-- Export plugin archive
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3872669664"] = "Export plugin archive"
-- Install Plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3902690643"] = "Install Plugin"
-- Assistant updated.
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T40397082"] = "Assistant updated."
@ -9519,6 +9579,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
-- Internal assistant plugins cannot be deleted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted."
-- This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
-- The assistant plugin directory is outside the local assistant plugin directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory."
@ -9552,6 +9615,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223
-- Unexpected error: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
-- Config server managed assistant plugins cannot be replaced.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2594571117"] = "Config server managed assistant plugins cannot be replaced."
-- The assistant plugin has no local directory.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Services;
public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Services;
public sealed record AssistantPluginDeleteResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Services;
public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue, bool Cancelled = false);

View File

@ -8,14 +8,6 @@ using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
public sealed record AssistantPluginInstallResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, bool ReplacedExisting, string Issue);
public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue);
public sealed record AssistantPluginDeleteResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);
public sealed record AssistantPluginUpdateResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);
public sealed class AssistantPluginInstallService
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginInstallService).Namespace, nameof(AssistantPluginInstallService));
@ -33,6 +25,8 @@ public sealed class AssistantPluginInstallService
private readonly SemaphoreSlim installSemaphore = new(1, 1);
private static AssistantPluginInstallResult Error(string issue) => new(false, Guid.Empty, string.Empty, string.Empty, false, issue);
private static AssistantPluginInstallResult CancelledByUser() => new(false, Guid.Empty, string.Empty, string.Empty, false, string.Empty, true);
private static AssistantPluginCheckResult CheckError(string issue) => new(false, Guid.Empty, string.Empty, issue);
@ -40,11 +34,7 @@ public sealed class AssistantPluginInstallService
private static AssistantPluginUpdateResult UpdateError(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, issue);
public AssistantPluginInstallService(
ILogger<AssistantPluginInstallService> logger,
SettingsManager settingsManager,
AssistantSessionService assistantSessionService,
MediaTranscriptionService mediaTranscriptionService)
public AssistantPluginInstallService(ILogger<AssistantPluginInstallService> logger, SettingsManager settingsManager, AssistantSessionService assistantSessionService, MediaTranscriptionService mediaTranscriptionService)
{
this.logger = logger;
this.settingsManager = settingsManager;
@ -129,10 +119,9 @@ public sealed class AssistantPluginInstallService
return Error(rootIssue);
await this.installSemaphore.WaitAsync(token);
AssistantPluginValidationResult validation;
try
{
validation = await this.ValidateIntoStagingAsync(lua, token);
var validation = await this.ValidateIntoStagingAsync(lua, token);
if (!validation.Success || validation.AssistantPlugin is null)
return Error(validation.Issue);
@ -149,9 +138,13 @@ public sealed class AssistantPluginInstallService
/// Companion files are validated from and moved with the same staging directory.
/// </summary>
/// <param name="archivePath">The local <c>.mwplugin</c> or <c>.zip</c> archive path.</param>
/// <param name="confirmAsync">
/// Asks the user whether the validated archive may be installed. It is called after all checks
/// passed and before anything gets written. Returning false aborts the installation.
/// </param>
/// <param name="token">Cancellation token for extraction, validation, file IO, and plugin reload.</param>
/// <returns>Installation result that contains success state, installed plugin metadata, and a user-facing issue when installation failed.</returns>
public async Task<AssistantPluginInstallResult> InstallArchiveAsync(string archivePath, CancellationToken token)
public async Task<AssistantPluginInstallResult> InstallArchiveAsync(string archivePath, Func<PluginImportPreview, Task<bool>> confirmAsync, CancellationToken token)
{
if (!this.settingsManager.ConfigurationData.App.AllowUserToImportPlugins)
return Error(TB("Your organization has disabled importing plugins."));
@ -184,7 +177,7 @@ public sealed class AssistantPluginInstallService
var pluginFile = pluginFiles[0];
var pluginDirectory = Path.GetDirectoryName(pluginFile)!;
var pluginCode = await File.ReadAllTextAsync(pluginFile, Encoding.UTF8, token);
var validation = await this.ValidateAssistantPluginCodeAsync(
var validation = await ValidateAssistantPluginCodeAsync(
pluginDirectory,
pluginCode.Trim(),
TB("The imported plugin is not an assistant plugin. Issue: {0}"),
@ -201,6 +194,18 @@ public sealed class AssistantPluginInstallService
if (validation.AssistantPlugin.IsManagedByConfigServer)
return Error(TB("This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."));
// The archive would replace an existing plugin: reject it when that plugin belongs
// to the IT department. We check this before asking the user, so that the
// confirmation never offers something we would refuse afterwards anyway:
var replacementIssue = GetAssistantReplacementIssue(validation.AssistantPlugin.Id);
if (!string.IsNullOrEmpty(replacementIssue))
return Error(replacementIssue);
// Everything is validated, but nothing was written yet. This is the point where the
// user decides, because the plugin code comes from an untrusted source:
if (!await confirmAsync(CreateImportPreview(validation.AssistantPlugin)))
return CancelledByUser();
return await this.InstallStagedAssistantAsync(assistantPluginsRoot, validation with { StagingDirectory = pluginDirectory }, token);
}
catch (Exception e) when (e is not OperationCanceledException)
@ -304,9 +309,10 @@ public sealed class AssistantPluginInstallService
Directory.Move(pluginDirectory, backupDirectory);
wasEnabled = this.settingsManager.ConfigurationData.EnabledPlugins.Remove(plugin.Id);
removedAudits = this.settingsManager.ConfigurationData.AssistantPluginAudits
.Where(audit => audit.PluginId == plugin.Id)
.ToList();
removedAudits =
[
.. this.settingsManager.ConfigurationData.AssistantPluginAudits.Where(audit => audit.PluginId == plugin.Id)
];
if (removedAudits.Count > 0)
this.settingsManager.ConfigurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id);
@ -517,7 +523,7 @@ public sealed class AssistantPluginInstallService
var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME);
await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token);
var validation = await this.ValidateAssistantPluginCodeAsync(
var validation = await ValidateAssistantPluginCodeAsync(
stagingDirectory,
pluginCode,
TB("The generated plugin is not an assistant plugin. Issue: {0}"),
@ -548,7 +554,7 @@ public sealed class AssistantPluginInstallService
try
{
return await this.ValidateAssistantPluginCodeAsync(
return await ValidateAssistantPluginCodeAsync(
pluginDirectory,
lua.Trim(),
TB("The edited plugin is not an assistant plugin. Issue: {0}"),
@ -563,13 +569,8 @@ public sealed class AssistantPluginInstallService
}
}
private async Task<AssistantPluginValidationResult> ValidateAssistantPluginCodeAsync(
string pluginDirectory,
string pluginCode,
string notAssistantIssue,
string invalidAssistantIssue,
string internalPluginIdIssue,
CancellationToken token)
private static async Task<AssistantPluginValidationResult> ValidateAssistantPluginCodeAsync(string pluginDirectory, string pluginCode,
string notAssistantIssue, string invalidAssistantIssue, string internalPluginIdIssue, CancellationToken token)
{
var plugin = await PluginFactory.Load(pluginDirectory, pluginCode, token);
if (plugin is not PluginAssistants assistantPlugin)
@ -635,11 +636,8 @@ public sealed class AssistantPluginInstallService
: TB("The assistant plugin directory does not exist.");
}
private void TryDeleteStagingDirectory(string stagingDirectory)
{
TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger);
}
private void TryDeleteStagingDirectory(string stagingDirectory) => TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger);
private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin)
{
var existingPlugin = FindReplaceableAssistantPlugin(assistantPlugin.Id);
@ -657,6 +655,14 @@ public sealed class AssistantPluginInstallService
.OfType<IAvailablePlugin>()
.FirstOrDefault(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == pluginId && !plugin.IsInternal);
/// <summary>
/// Collects the metadata an archive declares about itself, together with the information about
/// the installed plugin it would replace.
/// </summary>
/// <param name="assistantPlugin">The validated assistant plugin from the archive.</param>
/// <returns>The preview shown to the user before the installation starts.</returns>
private static PluginImportPreview CreateImportPreview(PluginAssistants assistantPlugin) => new(assistantPlugin, FindReplaceableAssistantPlugin(assistantPlugin.Id));
/// <summary>
/// Checks whether an installation may replace the assistant plugin that currently uses the given ID.
/// Plugins deployed by a Config Server belong to the organization's IT, so neither an import nor
@ -836,4 +842,4 @@ public sealed class AssistantPluginInstallService
{
public static AssistantPluginValidationResult Failure(string issue) => new(false, string.Empty, null, issue);
}
}
}

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Services;
public sealed record AssistantPluginUpdateResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);

View File

@ -0,0 +1,17 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Services;
/// <summary>
/// What the user gets to see before a plugin archive is installed. It is not bound to a specific
/// plugin type, so it also serves upcoming import paths for other plugin types.
/// </summary>
/// <param name="Plugin">The plugin from the archive, with the metadata it declares about itself.</param>
/// <param name="ExistingPlugin">The installed plugin that gets replaced or null when the archive adds a new plugin.</param>
public sealed record PluginImportPreview(IPluginMetadata Plugin, IAvailablePlugin? ExistingPlugin)
{
/// <summary>
/// True when an installed plugin with the same ID gets replaced.
/// </summary>
public bool ReplacesExisting => this.ExistingPlugin is not null;
}