mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 19:52:10 +00:00
Merge branch 'main' into chart-generation
# Conflicts: # app/MindWork AI Studio/Assistants/I18N/allTexts.lua # app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua # app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
This commit is contained in:
commit
319ca8806e
52
AGENTS.md
52
AGENTS.md
@ -29,14 +29,44 @@ dotnet run build
|
|||||||
```
|
```
|
||||||
This builds the .NET app as a Tauri "sidecar" binary, which is required even for development.
|
This builds the .NET app as a Tauri "sidecar" binary, which is required even for development.
|
||||||
|
|
||||||
### Running .NET builds from an agent
|
### Running builds from an agent
|
||||||
- Do not run `.NET` builds such as `dotnet run build`, `dotnet build`, or similar build commands from an agent. Codex agents can hit a known sandbox issue during `.NET` builds, typically surfacing as `CSSM_ModuleLoad()` or other sandbox-related failures.
|
Agents must not start builds through their own shell: agent shells run sandboxed, and `.NET` builds
|
||||||
- Instead, ask the user to run the `.NET` build locally in their IDE and report the result back.
|
hit a known sandbox issue there, typically surfacing as `CSSM_ModuleLoad()` or other sandbox-related
|
||||||
- Recommend the canonical repo build flow for the user: open an IDE terminal in the repository and run `cd app/Build && dotnet run build`.
|
failures (for reference: https://github.com/openai/codex/issues/4915). This applies to `dotnet run build`,
|
||||||
- If the context fits better, it is also acceptable to ask the user to start the build using their IDE's built-in build action, as long as it is clear the build must be run locally by the user.
|
`dotnet build`, `cargo build`, and similar commands.
|
||||||
- After asking for the build, wait for the user's feedback before diagnosing issues, making follow-up changes, or suggesting the next step.
|
|
||||||
- Treat the user's build output, error messages, or success confirmation as the source of truth for further troubleshooting.
|
Instead, use the JetBrains IDE MCP servers. They execute in the IDE process, which runs outside the
|
||||||
- For reference: https://github.com/openai/codex/issues/4915
|
agent sandbox:
|
||||||
|
|
||||||
|
- `rider` for the .NET solution at `app/MindWork AI Studio.sln`
|
||||||
|
- `rustrover` for the Rust runtime at `runtime/`
|
||||||
|
|
||||||
|
Pass the `rootFolder` parameter on every call, e.g. the absolute path of the `app` directory for Rider
|
||||||
|
and of `runtime` for RustRover. It avoids ambiguous calls when several IDE windows are open.
|
||||||
|
|
||||||
|
**Compile check of the .NET code:** start `mcp__rider__build_solution_start`, then poll
|
||||||
|
`mcp__rider__build_solution_state` until its state is `Completed` and read `buildIsSuccess` plus the
|
||||||
|
collected problems. `mcp__rider__get_project_problems` reports the current Problems View without
|
||||||
|
triggering a new build.
|
||||||
|
|
||||||
|
**Build script commands** such as the canonical build or the I18N collection run through the IDE
|
||||||
|
terminal, because they are more than a solution build:
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp__rider__execute_terminal_command command: "cd app/Build && dotnet run build"
|
||||||
|
mcp__rider__execute_terminal_command command: "cd app/Build && dotnet run collect-i18n"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rust builds** work the same way through the matching `rustrover` tools.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The IDE may ask the user to confirm a terminal command. Wait for the result instead of retrying the
|
||||||
|
command in the agent shell.
|
||||||
|
- When the IDE is not running or its MCP server is unavailable, fall back to asking the user to run
|
||||||
|
`cd app/Build && dotnet run build` locally, and wait for their feedback before diagnosing issues or
|
||||||
|
making follow-up changes.
|
||||||
|
- Treat the build output, error messages, or success confirmation as the source of truth for further
|
||||||
|
troubleshooting, no matter whether it came from the MCP server or from the user.
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
Currently, no automated test suite exists in the repository.
|
Currently, no automated test suite exists in the repository.
|
||||||
@ -113,12 +143,12 @@ Plugins can configure:
|
|||||||
- etc.
|
- etc.
|
||||||
|
|
||||||
Configuration plugins provide three kinds of values:
|
Configuration plugins provide three kinds of values:
|
||||||
- **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults.
|
- **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults. Which configuration plugin owns a locked setting is persisted in `Data.ManagedLockedConfigurations`, and organization defaults are tracked in `Data.ManagedEditableDefaults`. Both are cleaned up generically by `ManagedConfiguration.CleanupLeftOverManagedConfigurations(...)` when the owning plugin is gone. The value a setting had before a configuration plugin took it over is kept in `Data.ManagedUserValueSnapshots` and restored by that same clean-up, so removing a plugin hands the user's own value back instead of the app default.
|
||||||
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
|
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
|
||||||
- **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
|
- **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
|
||||||
|
|
||||||
When adding configuration plugin capabilities:
|
When adding configuration plugin capabilities:
|
||||||
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, process the setting in `PluginConfiguration.TryProcessConfiguration`, and check for leftover managed configuration in `PluginFactory.Loading.LoadAll`.
|
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)` and process the setting in `PluginConfiguration.TryProcessConfiguration`. Cleaning up the setting when its configuration plugin was removed needs no extra step: `ManagedConfiguration.CleanupLeftOverManagedConfigurations(...)` iterates all registered settings. Do not add per-setting cleanup calls to `PluginFactory.Loading.LoadAll`.
|
||||||
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
|
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
|
||||||
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
|
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
|
||||||
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
|
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
|
||||||
@ -193,7 +223,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
|||||||
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
|
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
|
||||||
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
|
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
|
||||||
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
||||||
- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing.
|
- **Agent-run builds** - Never start `.NET` or Rust builds in the agent's own shell; it is sandboxed. Use the `rider` and `rustrover` MCP servers instead, which build in the IDE outside that sandbox. See "Running builds from an agent" above.
|
||||||
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
||||||
- **Production environment** - Runtime launches .NET sidecar with environment variables
|
- **Production environment** - Runtime launches .NET sidecar with environment variables
|
||||||
- **MudBlazor** - Component library requires DI setup in Program.cs
|
- **MudBlazor** - Component library requires DI setup in Program.cs
|
||||||
|
|||||||
2
app/.codex/config.toml
Normal file
2
app/.codex/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[mcp_servers.rider]
|
||||||
|
url = "http://127.0.0.1:64482/stream"
|
||||||
@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
|
|
||||||
|
<!-- Pins Cocona's transitive Microsoft.Extensions.Hosting 6.0.0, which pulled in the vulnerable System.Text.Json 6.0.0 (GHSA-8g4q-xg66-9fp4) -->
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.18" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
8
app/Directory.Build.props
Normal file
8
app/Directory.Build.props
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<Project>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Audit direct and transitive packages, so vulnerable transitive dependencies surface during restore instead of only in the IDE -->
|
||||||
|
<NuGetAuditMode>all</NuGetAuditMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -659,9 +659,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
await this.AssistantSessionService.ClearAsync(this.assistantSessionKey);
|
await this.AssistantSessionService.ClearAsync(this.assistantSessionKey);
|
||||||
this.MediaTranscriptionService.ClearOwnerState(this.CurrentMediaImportOwner);
|
this.MediaTranscriptionService.ClearOwnerState(this.CurrentMediaImportOwner);
|
||||||
this.assistantSessionId = null;
|
this.assistantSessionId = null;
|
||||||
|
this.ChatThread = null;
|
||||||
|
this.LastUserPrompt = null;
|
||||||
this.ResultingContentBlock = null;
|
this.ResultingContentBlock = null;
|
||||||
this.ProviderSettings = Settings.Provider.NONE;
|
this.ProviderSettings = Settings.Provider.NONE;
|
||||||
|
|
||||||
|
await this.JsRuntime.ClearDiv(BEFORE_RESULT_DIV_ID);
|
||||||
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
|
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
|
||||||
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
|
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
private IDialogService DialogService { get; init; } = null!;
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginGenerationService AssistantPluginGenerationService { get; init; } = null!;
|
private AssistantPluginGenerationService AssistantPluginGenerationService { get; init; } = null!;
|
||||||
@ -500,7 +500,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
this.isCheckingPlugin = true;
|
this.isCheckingPlugin = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await this.AssistantPluginInstallService.CheckInstallabilityAsync(this.generatedLuaAssistant, CancellationToken.None);
|
var result = await this.PluginInstallService.CheckInstallabilityAsync(this.generatedLuaAssistant, CancellationToken.None);
|
||||||
this.pluginCheckResult = result;
|
this.pluginCheckResult = result;
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
@ -530,7 +530,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
|||||||
this.isInstallingPlugin = true;
|
this.isInstallingPlugin = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await this.AssistantPluginInstallService.InstallAsync(this.generatedLuaAssistant, CancellationToken.None);
|
var result = await this.PluginInstallService.InstallAsync(this.generatedLuaAssistant, CancellationToken.None);
|
||||||
this.pluginInstallResult = result;
|
this.pluginInstallResult = result;
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2677,6 +2677,126 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRE
|
|||||||
-- Build progress
|
-- Build progress
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Build progress"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Build progress"
|
||||||
|
|
||||||
|
-- The model did not fill every planned content slot exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1003911239"] = "The model did not fill every planned content slot exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The sources of this briefing could not be prepared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1034452233"] = "The sources of this briefing could not be prepared."
|
||||||
|
|
||||||
|
-- This operation did not change the briefing, so no new version was created.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1058618049"] = "This operation did not change the briefing, so no new version was created."
|
||||||
|
|
||||||
|
-- The model filled a content slot with the wrong kind of value. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1099589813"] = "The model filled a content slot with the wrong kind of value. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1198458597"] = "The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model did not cover every source of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1209705994"] = "The model did not cover every source of this briefing exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An accessibility text of the model response was empty or invalid. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1437512295"] = "An accessibility text of the model response was empty or invalid. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response used a prohibited attribute. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1677678770"] = "The model response used a prohibited attribute. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A chart of the model response contained invalid categories or data series. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T181588270"] = "A chart of the model response contained invalid categories or data series. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A source of this briefing can no longer be reached. Please relink or remove the affected source.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1878061605"] = "A source of this briefing can no longer be reached. Please relink or remove the affected source."
|
||||||
|
|
||||||
|
-- The selected provider could not complete this briefing stage.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1905087799"] = "The selected provider could not complete this briefing stage."
|
||||||
|
|
||||||
|
-- A calculation of the model response used an invalid operation. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1992964953"] = "A calculation of the model response used an invalid operation. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response did not match the required contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T214297315"] = "The model response did not match the required contract. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response contained unexpected fields. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2192261405"] = "The model response contained unexpected fields. Please try again or select another model."
|
||||||
|
|
||||||
|
-- AI Studio was closed while this briefing was being built. You can resume the build.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2197645770"] = "AI Studio was closed while this briefing was being built. You can resume the build."
|
||||||
|
|
||||||
|
-- The presentation of the model response did not match the briefing contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2376983148"] = "The presentation of the model response did not match the briefing contract. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This visual briefing operation was canceled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T240791538"] = "This visual briefing operation was canceled."
|
||||||
|
|
||||||
|
-- The model response contained markup or code, which this briefing does not allow. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2529598303"] = "The model response contained markup or code, which this briefing does not allow. Please try again or select another model."
|
||||||
|
|
||||||
|
-- AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2668127220"] = "AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."
|
||||||
|
|
||||||
|
-- This briefing could not be assembled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2678882954"] = "This briefing could not be assembled."
|
||||||
|
|
||||||
|
-- An interactive control of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2714042531"] = "An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model did not return valid JSON. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2784808603"] = "The model did not return valid JSON. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A calculation of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2795934353"] = "A calculation of the model response targeted an invalid briefing element. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid initial state. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2796279475"] = "An interactive control of the model response used an invalid initial state. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The accessibility texts of the model response did not match the briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2815870761"] = "The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The new version of this briefing could not be saved.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2818947691"] = "The new version of this briefing could not be saved."
|
||||||
|
|
||||||
|
-- The model did not plan every visual asset of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2853629903"] = "The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The assembled briefing did not pass the security validation.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T295498807"] = "The assembled briefing did not pass the security validation."
|
||||||
|
|
||||||
|
-- The charts of the model response did not match the planned briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3326200304"] = "The charts of the model response did not match the planned briefing elements. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3412185985"] = "An interactive control of the model response used an invalid identifier. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response referenced content that does not exist. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T344215744"] = "The model response referenced content that does not exist. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The updated content no longer fits the current presentation. You can continue as a rebuild.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3515116214"] = "The updated content no longer fits the current presentation. You can continue as a rebuild."
|
||||||
|
|
||||||
|
-- The model response contained a value of the wrong type. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3668896836"] = "The model response contained a value of the wrong type. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This briefing has no provider selected. Please select a provider before you generate a briefing.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3834145318"] = "This briefing has no provider selected. Please select a provider before you generate a briefing."
|
||||||
|
|
||||||
|
-- The selected model lacks a capability this briefing needs. Please select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T4066127340"] = "The selected model lacks a capability this briefing needs. Please select another model."
|
||||||
|
|
||||||
|
-- A media transcript of this briefing is missing or outdated. Please transcribe the affected media again.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T449544952"] = "A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."
|
||||||
|
|
||||||
|
-- The model response used an invalid briefing layout. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T686008237"] = "The model response used an invalid briefing layout. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A briefing element of the model response was missing its required interactive controls. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T762236598"] = "A briefing element of the model response was missing its required interactive controls. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T875151112"] = "This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."
|
||||||
|
|
||||||
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
||||||
|
|
||||||
-- This chart cannot be displayed: {0}
|
-- This chart cannot be displayed: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
||||||
|
|
||||||
@ -2797,24 +2917,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assistan
|
|||||||
-- The result is ready.
|
-- The result is ready.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
|
||||||
|
|
||||||
-- Delete assistant plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1692493145"] = "Delete assistant plugin"
|
|
||||||
|
|
||||||
-- Delete Assistant Plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3637071001"] = "Delete Assistant Plugin"
|
|
||||||
|
|
||||||
-- The '{0}' assistant plugin has been successfully removed.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3815023384"] = "The '{0}' assistant plugin has been successfully removed."
|
|
||||||
|
|
||||||
-- The assistant plugin '{0}' could not be deleted: {1}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3985264168"] = "The assistant plugin '{0}' could not be deleted: {1}"
|
|
||||||
|
|
||||||
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T4033722845"] = "Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."
|
|
||||||
|
|
||||||
-- Show or hide the detailed security information.
|
-- Show or hide the detailed security information.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."
|
||||||
|
|
||||||
@ -3262,6 +3364,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Co
|
|||||||
-- Cannot copy this content type to clipboard.
|
-- Cannot copy this content type to clipboard.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
||||||
|
|
||||||
|
-- Delete assistant plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1692493145"] = "Delete assistant plugin"
|
||||||
|
|
||||||
|
-- Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1744561175"] = "Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically."
|
||||||
|
|
||||||
|
-- Delete language plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2707495447"] = "Delete language plugin"
|
||||||
|
|
||||||
|
-- The plugin '{0}' could not be deleted: {1}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2738963920"] = "The plugin '{0}' could not be deleted: {1}"
|
||||||
|
|
||||||
|
-- Delete Language Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2990518039"] = "Delete Language Plugin"
|
||||||
|
|
||||||
|
-- Delete Configuration Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3395354991"] = "Delete Configuration Plugin"
|
||||||
|
|
||||||
|
-- The plugin '{0}' has been successfully removed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3476138264"] = "The plugin '{0}' has been successfully removed."
|
||||||
|
|
||||||
|
-- Delete Assistant Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3637071001"] = "Delete Assistant Plugin"
|
||||||
|
|
||||||
|
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T4033722845"] = "Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."
|
||||||
|
|
||||||
|
-- Delete configuration plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T459830575"] = "Delete configuration plugin"
|
||||||
|
|
||||||
-- Alpha phase means that we are working on the last details before the beta phase.
|
-- Alpha phase means that we are working on the last details before the beta phase.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase."
|
||||||
|
|
||||||
@ -3874,6 +4009,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T40680
|
|||||||
-- Edit Embedding Provider
|
-- Edit Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Edit Embedding Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Edit Embedding Provider"
|
||||||
|
|
||||||
|
-- This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T438107040"] = "This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings."
|
||||||
|
|
||||||
-- Configure Embedding Providers
|
-- Configure Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Configure Embedding Providers"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Configure Embedding Providers"
|
||||||
|
|
||||||
@ -3958,6 +4096,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T386503
|
|||||||
-- Delete LLM Provider
|
-- Delete LLM Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
|
||||||
|
|
||||||
|
-- This self-hosted provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks."
|
||||||
|
|
||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
|
||||||
|
|
||||||
@ -3985,6 +4126,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T17
|
|||||||
-- Add Transcription Provider
|
-- Add Transcription Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
||||||
|
|
||||||
|
-- This self-hosted transcription provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2175189736"] = "This self-hosted transcription provider is trusted for data source security checks."
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Model"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Model"
|
||||||
|
|
||||||
@ -4624,6 +4768,84 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow th
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- {0} LLM providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers"
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1238255445"] = "{0} profiles"
|
||||||
|
|
||||||
|
-- No
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1642511898"] = "No"
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2107991661"] = "{0} introductions on the welcome page"
|
||||||
|
|
||||||
|
-- {0} mandatory information
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2150386772"] = "{0} mandatory information"
|
||||||
|
|
||||||
|
-- You can install the plugin again later, but any changes you made to its settings are lost.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2156367745"] = "You can install the plugin again later, but any changes you made to its settings are lost."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2342765572"] = "{0} profile"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2426110502"] = "{0} introduction on the welcome page"
|
||||||
|
|
||||||
|
-- {0} embedding providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2438407498"] = "{0} embedding providers"
|
||||||
|
|
||||||
|
-- Yes, delete it
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2466176832"] = "Yes, delete it"
|
||||||
|
|
||||||
|
-- This also removes everything the configuration plugin had set up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T264970454"] = "This also removes everything the configuration plugin had set up:"
|
||||||
|
|
||||||
|
-- {0} transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2681055470"] = "{0} transcription provider"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3235448458"] = "{0} chat templates"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3278137746"] = "{0} document analysis policy"
|
||||||
|
|
||||||
|
-- The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T330559934"] = "The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well."
|
||||||
|
|
||||||
|
-- {0} LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3410030691"] = "{0} LLM provider"
|
||||||
|
|
||||||
|
-- Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3616855807"] = "Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files."
|
||||||
|
|
||||||
|
-- {0} settings return to their default values
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3841220170"] = "{0} settings return to their default values"
|
||||||
|
|
||||||
|
-- {0} setting returns to its default value
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T384701293"] = "{0} setting returns to its default value"
|
||||||
|
|
||||||
|
-- {0} mandatory informations
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3971735909"] = "{0} mandatory informations"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4147879421"] = "{0} chat template"
|
||||||
|
|
||||||
|
-- {0} data sources, including their credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4193757254"] = "{0} data sources, including their credentials in your operating system's keychain"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T449490978"] = "{0} document analysis policies"
|
||||||
|
|
||||||
|
-- {0} data source, including its credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T511418335"] = "{0} data source, including its credentials in your operating system's keychain"
|
||||||
|
|
||||||
|
-- {0} transcription providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"] = "{0} transcription providers"
|
||||||
|
|
||||||
|
-- {0} embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} embedding provider"
|
||||||
|
|
||||||
-- No
|
-- No
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "No"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "No"
|
||||||
|
|
||||||
@ -5242,6 +5464,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe
|
|||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -5332,6 +5557,117 @@ 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1238255445"] = "{0} profiles"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- Data source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2034620186"] = "Data source"
|
||||||
|
|
||||||
|
-- A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2051328106"] = "A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2107991661"] = "{0} introductions on the welcome page"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2342765572"] = "{0} profile"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2426110502"] = "{0} introduction on the welcome page"
|
||||||
|
|
||||||
|
-- Support contact
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact"
|
||||||
|
|
||||||
|
-- Name
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- {0} setting it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2868009192"] = "{0} setting it takes control of"
|
||||||
|
|
||||||
|
-- {0} settings it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3190775003"] = "{0} settings it takes control of"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3235448458"] = "{0} chat templates"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3278137746"] = "{0} document analysis policy"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- {0} mandatory information you have to accept before using AI Studio
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3540986519"] = "{0} mandatory information you have to accept before using AI Studio"
|
||||||
|
|
||||||
|
-- Transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3566003684"] = "Transcription provider"
|
||||||
|
|
||||||
|
-- Replace plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin"
|
||||||
|
|
||||||
|
-- LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4099016901"] = "LLM provider"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4147879421"] = "{0} chat template"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T449490978"] = "{0} document analysis policies"
|
||||||
|
|
||||||
|
-- The authors marked this plugin as deprecated: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}"
|
||||||
|
|
||||||
|
-- It also brings:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T713968030"] = "It also brings:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- Embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T877326195"] = "Embedding provider"
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- Sends data to
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T914647109"] = "Sends data to"
|
||||||
|
|
||||||
|
-- Destination
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T994314591"] = "Destination"
|
||||||
|
|
||||||
-- 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -7405,6 +7741,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Building on .NET
|
|||||||
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files."
|
||||||
|
|
||||||
|
-- Plugin directory:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:"
|
||||||
|
|
||||||
-- Consent:
|
-- Consent:
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
||||||
|
|
||||||
@ -7435,6 +7774,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is
|
|||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||||
|
|
||||||
|
-- The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1985806792"] = "The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others."
|
||||||
|
|
||||||
-- Copies the number of loaded root certificates to the clipboard
|
-- Copies the number of loaded root certificates to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
|
||||||
|
|
||||||
@ -7444,6 +7786,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo
|
|||||||
-- Copies the server URL to the clipboard
|
-- Copies the server URL to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
||||||
|
|
||||||
|
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||||
|
|
||||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
||||||
|
|
||||||
@ -7564,6 +7909,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3002755581"] = "This library ide
|
|||||||
-- Changelog
|
-- Changelog
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
|
||||||
|
|
||||||
|
-- Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3019585985"] = "Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio."
|
||||||
|
|
||||||
-- External HTTPS custom root certificates are configured but not active.
|
-- External HTTPS custom root certificates are configured but not active.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
|
||||||
|
|
||||||
@ -7579,6 +7927,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio
|
|||||||
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements."
|
||||||
|
|
||||||
|
-- Copies the plugin directory to the clipboard
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3182878147"] = "Copies the plugin directory to the clipboard"
|
||||||
|
|
||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||||
|
|
||||||
@ -7660,9 +8011,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version doe
|
|||||||
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user."
|
||||||
|
|
||||||
-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration."
|
|
||||||
|
|
||||||
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
|
||||||
|
|
||||||
@ -7705,6 +8053,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code
|
|||||||
-- Executable path
|
-- Executable path
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
|
||||||
|
|
||||||
|
-- AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4172838224"] = "AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running."
|
||||||
|
|
||||||
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||||
|
|
||||||
@ -7774,6 +8125,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data tra
|
|||||||
-- How to update
|
-- How to update
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "How to update"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "How to update"
|
||||||
|
|
||||||
|
-- A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T923110805"] = "A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app."
|
||||||
|
|
||||||
-- Install Pandoc
|
-- Install Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Install Pandoc"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Install Pandoc"
|
||||||
|
|
||||||
@ -7783,18 +8137,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1229643769"] = "Potentially Dangerou
|
|||||||
-- Disable plugin
|
-- Disable plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Disable plugin"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Disable plugin"
|
||||||
|
|
||||||
|
-- Import
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1463683828"] = "Import"
|
||||||
|
|
||||||
|
-- Import plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1467093263"] = "Import plugin"
|
||||||
|
|
||||||
-- Assistant Audit
|
-- Assistant Audit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistant Audit"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistant Audit"
|
||||||
|
|
||||||
-- Internal Plugins
|
-- Internal Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
|
||||||
|
|
||||||
|
-- Plugin updated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1646565893"] = "Plugin updated."
|
||||||
|
|
||||||
|
-- Import plugin from a file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T169921408"] = "Import plugin from a file"
|
||||||
|
|
||||||
-- Disabled Plugins
|
-- Disabled Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
|
||||||
|
|
||||||
-- Edit assistant plugin
|
-- Edit assistant plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Edit assistant plugin"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Edit assistant plugin"
|
||||||
|
|
||||||
|
-- Plugin installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1889482678"] = "Plugin installed."
|
||||||
|
|
||||||
-- Send a mail
|
-- Send a mail
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
|
||||||
|
|
||||||
@ -7816,18 +8185,45 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
|
|||||||
-- Revise Assistant Plugin
|
-- Revise Assistant Plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "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.
|
-- The assistant plugin '{0}' has been successfully saved.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
|
||||||
|
|
||||||
|
-- An error occurred while sharing the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3184210266"] = "An error occurred while sharing the plugin."
|
||||||
|
|
||||||
|
-- Your organization has disabled exporting plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Your organization has disabled exporting plugins."
|
||||||
|
|
||||||
|
-- Share plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Share plugin archive"
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Your organization has disabled sharing plugins."
|
||||||
|
|
||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Close"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Close"
|
||||||
|
|
||||||
|
-- Please drop a plugin archive with the extension {0} or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3785427568"] = "Please drop a plugin archive with the extension {0} or .zip."
|
||||||
|
|
||||||
-- Revise assistant plugin with AI
|
-- Revise assistant plugin with AI
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Revise assistant plugin with AI"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Revise assistant plugin with AI"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
|
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"
|
||||||
|
|
||||||
|
-- Please drop only one plugin archive at a time.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3974628410"] = "Please drop only one plugin archive at a time."
|
||||||
|
|
||||||
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually."
|
||||||
|
|
||||||
@ -7840,6 +8236,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website"
|
|||||||
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \"{2}\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \"{2}\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \\\"{2}\\\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level \\\"{2}\\\". Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"
|
||||||
|
|
||||||
|
-- The plugin archive was exported to '{0}'.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "The plugin archive was exported to '{0}'."
|
||||||
|
|
||||||
|
-- An error occurred while exporting the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T759681732"] = "An error occurred while exporting the plugin."
|
||||||
|
|
||||||
|
-- The plugin could not be imported: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T837269472"] = "The plugin could not be imported: {0}"
|
||||||
|
|
||||||
-- Settings
|
-- Settings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
|
||||||
|
|
||||||
@ -9253,6 +9658,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like p
|
|||||||
-- Document
|
-- Document
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
||||||
|
|
||||||
|
-- Plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive"
|
||||||
|
|
||||||
-- The Assistant Builder context could not be loaded.
|
-- The Assistant Builder context could not be loaded.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
||||||
|
|
||||||
@ -9355,75 +9763,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
|
|||||||
-- Please create an assistant draft first.
|
-- Please create an assistant draft first.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Please create an assistant draft first."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Please create an assistant draft first."
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted."
|
|
||||||
|
|
||||||
-- 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."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1288328479"] = "Only assistant plugins can be edited."
|
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
|
||||||
|
|
||||||
-- No Lua plugin code was generated.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1839013358"] = "No Lua plugin code was generated."
|
|
||||||
|
|
||||||
-- The edited assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2061233834"] = "The edited assistant plugin uses the ID of an internal AI Studio plugin."
|
|
||||||
|
|
||||||
-- The assistant plugin directory does not exist.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2148384567"] = "The assistant plugin directory does not exist."
|
|
||||||
|
|
||||||
-- The resolved plugin directory is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223071618"] = "The resolved plugin directory is outside the assistant plugin directory."
|
|
||||||
|
|
||||||
-- Unexpected error: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
|
|
||||||
|
|
||||||
-- The assistant plugin has no local directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."
|
|
||||||
|
|
||||||
-- The AI Studio data directory is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2712481762"] = "The AI Studio data directory is not initialized yet."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2864597027"] = "Only assistant plugins can be deleted."
|
|
||||||
|
|
||||||
-- The generated plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2955055168"] = "The generated plugin is not an assistant plugin. Issue: {0}"
|
|
||||||
|
|
||||||
-- The generated assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3162363526"] = "The generated assistant plugin uses the ID of an internal AI Studio plugin."
|
|
||||||
|
|
||||||
-- Config Server managed assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3751820312"] = "Config Server managed assistant plugins cannot be deleted."
|
|
||||||
|
|
||||||
-- Only assistants generated by the Assistant Builder can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3940247198"] = "Only assistants generated by the Assistant Builder can be deleted."
|
|
||||||
|
|
||||||
-- The edited plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984111892"] = "The edited plugin is not an assistant plugin. Issue: {0}"
|
|
||||||
|
|
||||||
-- The plugin system is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984839613"] = "The plugin system is not initialized yet."
|
|
||||||
|
|
||||||
-- The plugin file is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T4062980447"] = "The plugin file is outside the assistant plugin directory."
|
|
||||||
|
|
||||||
-- The edited assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T554567780"] = "The edited assistant plugin is invalid. Issue: {0}"
|
|
||||||
|
|
||||||
-- The edited assistant plugin must keep the same plugin ID.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T693124809"] = "The edited assistant plugin must keep the same plugin ID."
|
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T816339833"] = "Internal assistant plugins cannot be edited."
|
|
||||||
|
|
||||||
-- The generated assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}"
|
|
||||||
|
|
||||||
-- The voice recording shortcut currently works only while AI Studio is focused.
|
-- The voice recording shortcut currently works only while AI Studio is focused.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "The voice recording shortcut currently works only while AI Studio is focused."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "The voice recording shortcut currently works only while AI Studio is focused."
|
||||||
|
|
||||||
@ -9475,6 +9814,144 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||||
|
|
||||||
|
-- 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::PLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
|
||||||
|
|
||||||
|
-- The imported plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1195382910"] = "The imported plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- The assistant plugin directory is outside the local assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory."
|
||||||
|
|
||||||
|
-- Only assistant plugins can be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1288328479"] = "Only assistant plugins can be edited."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1348456011"] = "Plugins deployed by your organization cannot be deleted."
|
||||||
|
|
||||||
|
-- The resolved plugin directory is outside the plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1559620698"] = "The resolved plugin directory is outside the plugin directory."
|
||||||
|
|
||||||
|
-- Please select a plugin archive with the extension .mwplugin or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1809137998"] = "Please select a plugin archive with the extension .mwplugin or .zip."
|
||||||
|
|
||||||
|
-- The selected plugin archive does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1821013825"] = "The selected plugin archive does not exist."
|
||||||
|
|
||||||
|
-- No Lua plugin code was generated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1839013358"] = "No Lua plugin code was generated."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1878846406"] = "Only assistant, configuration, and language plugins can be deleted."
|
||||||
|
|
||||||
|
-- Your organization has disabled importing configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2134532120"] = "Your organization has disabled importing configuration plugins."
|
||||||
|
|
||||||
|
-- The assistant plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2148384567"] = "The assistant plugin directory does not exist."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2221093487"] = "The plugin directory does not exist."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
|
||||||
|
|
||||||
|
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "The generated assistant plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "This individual plugin’s directory is outside the expected plugins directory."
|
||||||
|
|
||||||
|
-- The assistant plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."
|
||||||
|
|
||||||
|
-- The AI Studio data directory is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2712481762"] = "The AI Studio data directory is not initialized yet."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be imported.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2909113247"] = "Only assistant, configuration, and language plugins can be imported."
|
||||||
|
|
||||||
|
-- The generated plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2955055168"] = "The generated plugin is not an assistant plugin. Issue: {0}"
|
||||||
|
|
||||||
|
-- Your organization has disabled importing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3212529834"] = "Your organization has disabled importing plugins."
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3284289028"] = "The plugin has no local directory."
|
||||||
|
|
||||||
|
-- The plugin archive must contain exactly one plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3355918609"] = "The plugin archive must contain exactly one plugin.lua file."
|
||||||
|
|
||||||
|
-- Your organization deployed a configuration with the same ID. An imported configuration must not take its place.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T352004699"] = "Your organization deployed a configuration with the same ID. An imported configuration must not take its place."
|
||||||
|
|
||||||
|
-- The imported plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3634046009"] = "The imported plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- Plugins shipped with AI Studio cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3841213017"] = "Plugins shipped with AI Studio cannot be deleted."
|
||||||
|
|
||||||
|
-- The edited plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984111892"] = "The edited plugin is not an assistant plugin. Issue: {0}"
|
||||||
|
|
||||||
|
-- The plugin system is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984839613"] = "The plugin system is not initialized yet."
|
||||||
|
|
||||||
|
-- The plugin file is outside the assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T4062980447"] = "The plugin file is outside the assistant plugin directory."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be replaced.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T553820956"] = "Plugins deployed by your organization cannot be replaced."
|
||||||
|
|
||||||
|
-- The edited assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T554567780"] = "The edited assistant plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- The edited assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T584770023"] = "The edited assistant plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- The edited assistant plugin must keep the same plugin ID.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T693124809"] = "The edited assistant plugin must keep the same plugin ID."
|
||||||
|
|
||||||
|
-- Internal assistant plugins cannot be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T816339833"] = "Internal assistant plugins cannot be edited."
|
||||||
|
|
||||||
|
-- The generated assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- Internal plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T1668534561"] = "Internal plugins cannot be shared."
|
||||||
|
|
||||||
|
-- Config Server managed plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2077776546"] = "Config Server managed plugins cannot be shared."
|
||||||
|
|
||||||
|
-- The native share dialog could not be opened.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2101116016"] = "The native share dialog could not be opened."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2221093487"] = "The plugin directory does not exist."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2350673880"] = "Unexpected error: {0}"
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3284289028"] = "The plugin has no local directory."
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3379469503"] = "Your organization has disabled sharing plugins."
|
||||||
|
|
||||||
|
-- The plugin directory is invalid: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3774594541"] = "The plugin directory is invalid: {0}"
|
||||||
|
|
||||||
|
-- Export plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3872669664"] = "Export plugin archive"
|
||||||
|
|
||||||
|
-- The plugin directory does not contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T409411078"] = "The plugin directory does not contain a plugin.lua file."
|
||||||
|
|
||||||
-- Failed to store the secret data due to an API issue.
|
-- Failed to store the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||||
|
|
||||||
|
|||||||
@ -94,13 +94,13 @@
|
|||||||
<MudPaper Outlined="true" Class="pa-4 mb-4">
|
<MudPaper Outlined="true" Class="pa-4 mb-4">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="7">
|
<MudItem xs="12" md="7">
|
||||||
<MudTextField T="string" @bind-Text="@this.editor.Name" Label="@T("Briefing name")" Validation="@this.ValidateProjectName" Immediate="@true" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy"/>
|
<MudTextField T="string" @bind-Text="@this.editor.Name" Label="@T("Briefing name")" Validation="@this.ValidateProjectName" Immediate="@true" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" md="5">
|
<MudItem xs="12" md="5">
|
||||||
<MudTextField T="string" @bind-Text="@this.editor.Author" Label="@T("Author (optional)")" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy"/>
|
<MudTextField T="string" @bind-Text="@this.editor.Author" Label="@T("Author (optional)")" Variant="Variant.Outlined" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
<MudTextField T="string" @bind-Text="@this.editor.Instruction" Label="@T("Briefing scope, notes, or current change instruction (optional)")" Variant="Variant.Outlined" AutoGrow="true" Lines="3" Class="mt-3" Disabled="@this.IsCurrentBusy"/>
|
<MudTextField T="string" @bind-Text="@this.editor.Instruction" Label="@T("Briefing scope, notes, or current change instruction (optional)")" Variant="Variant.Outlined" AutoGrow="true" Lines="3" Class="mt-3" Disabled="@this.IsCurrentBusy" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||||
|
|
||||||
<EnumSelection T="VisualBriefingProtectionLevel"
|
<EnumSelection T="VisualBriefingProtectionLevel"
|
||||||
NameFunc="@this.ProtectionLevelName"
|
NameFunc="@this.ProtectionLevelName"
|
||||||
@ -224,7 +224,7 @@
|
|||||||
<MudStack Row="true" Spacing="2" Wrap="Wrap.Wrap" Class="mb-4">
|
<MudStack Row="true" Spacing="2" Wrap="Wrap.Wrap" Class="mb-4">
|
||||||
@if (this.selectedBriefing.Versions.Count == 0)
|
@if (this.selectedBriefing.Versions.Count == 0)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AutoAwesome" OnClick="@(() => this.GenerateAsync(VisualBriefingEditMode.INITIAL))" Disabled="@this.CannotGenerate(VisualBriefingEditMode.INITIAL)" Style="@this.ConfidenceBorderStyle">@T("Create briefing")</MudButton>
|
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.AutoAwesome" OnClick="@(() => this.GenerateAsync(VisualBriefingEditMode.INITIAL))" Disabled="@this.CannotGenerate(VisualBriefingEditMode.INITIAL)" Style="@this.ConfidenceBorderStyle">@T("Create briefing")</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@ -100,9 +100,12 @@ public partial class VisualBriefingAssistant
|
|||||||
terminalStatus = result.FailureCode is VisualBriefingFailureCode.CANCELED ? AssistantSessionStatus.CANCELED : AssistantSessionStatus.FAILED;
|
terminalStatus = result.FailureCode is VisualBriefingFailureCode.CANCELED ? AssistantSessionStatus.CANCELED : AssistantSessionStatus.FAILED;
|
||||||
this.reusableContentBuildId = result.CanContinueAsRebuild ? result.Diagnostics.BuildId : null;
|
this.reusableContentBuildId = result.CanContinueAsRebuild ? result.Diagnostics.BuildId : null;
|
||||||
|
|
||||||
terminalIssue = result.Issue;
|
// The issue carried by the result is stable English contract language, because it also
|
||||||
|
// goes back to the model and into the persisted build record. What the user reads is
|
||||||
|
// derived from the stable enums in the current language instead:
|
||||||
|
terminalIssue = VisualBriefingFailureExtensions.ToUserMessage(result.FailureCode, result.Diagnostics.ValidationRule);
|
||||||
if (terminalStatus is not AssistantSessionStatus.CANCELED)
|
if (terminalStatus is not AssistantSessionStatus.CANCELED)
|
||||||
await this.MessageBus.SendError(new(Icons.Material.Filled.AutoAwesome, result.Issue));
|
await this.MessageBus.SendError(new(Icons.Material.Filled.AutoAwesome, terminalIssue));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,6 +127,9 @@ public partial class VisualBriefingAssistant : MSGComponentBase
|
|||||||
/// <summary>Stores whether this component instance has already left the renderer.</summary>
|
/// <summary>Stores whether this component instance has already left the renderer.</summary>
|
||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
|
|
||||||
|
/// <summary>Carries the spellchecking configuration to every text input of this assistant.</summary>
|
||||||
|
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines <c>IsCurrentBusy</c> for the visual briefing feature.
|
/// Defines <c>IsCurrentBusy</c> for the visual briefing feature.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -169,6 +172,16 @@ public partial class VisualBriefingAssistant : MSGComponentBase
|
|||||||
await this.ResumeSelectedBuildAsync();
|
await this.ResumeSelectedBuildAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines <c>OnParametersSetAsync</c> for the visual briefing feature.
|
||||||
|
/// </summary>
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
// Configure the spellchecking for the user input:
|
||||||
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
|
await base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines <c>DisposeResources</c> for the visual briefing feature.
|
/// Defines <c>DisposeResources</c> for the visual briefing feature.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -234,7 +247,12 @@ public partial class VisualBriefingAssistant : MSGComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
|
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
|
||||||
|
{
|
||||||
|
// The spellchecking setting might have changed. Since this page is not re-parameterized
|
||||||
|
// while the user stays on it, we have to read the setting again here:
|
||||||
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||||
this.StateHasChanged();
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -279,9 +279,14 @@ public partial class VisualBriefingBuildProgress : MSGComponentBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the safe failure reason for a UI group.
|
/// Gets the safe failure reason for a UI group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The recorded issue text of a failure is stable English contract language, because it also goes
|
||||||
|
/// back to the model and into the persisted build record. The text shown here is therefore derived
|
||||||
|
/// from the stable enums in the current language instead.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="index">The zero-based index of the group.</param>
|
/// <param name="index">The zero-based index of the group.</param>
|
||||||
/// <returns>The user-facing failure message.</returns>
|
/// <returns>The user-facing failure message.</returns>
|
||||||
private string BuildGroupFailure(int index) => this.Build is null ? string.Empty : STAGE_GROUPS[index]
|
private string BuildGroupFailure(int index) => this.Build is null ? string.Empty : STAGE_GROUPS[index]
|
||||||
.Select(stage => this.Build.Stages.FirstOrDefault(item => item.Stage == stage)?.Failure)
|
.Select(stage => this.Build.Stages.FirstOrDefault(item => item.Stage == stage)?.Failure)
|
||||||
.FirstOrDefault(failure => failure is not null)?.UserMessage ?? this.Build.Failure?.UserMessage ?? string.Empty;
|
.FirstOrDefault(failure => failure is not null)?.ToUserMessage() ?? this.Build.Failure?.ToUserMessage() ?? string.Empty;
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ namespace AIStudio.Assistants.VisualBriefing;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Success">Whether a revision was committed.</param>
|
/// <param name="Success">Whether a revision was committed.</param>
|
||||||
/// <param name="Version">The committed immutable version.</param>
|
/// <param name="Version">The committed immutable version.</param>
|
||||||
/// <param name="Issue">The user-safe issue.</param>
|
/// <param name="Issue">The user-safe issue in stable English, never localized. Use <see cref="VisualBriefingFailureExtensions"/> for the text shown to the user.</param>
|
||||||
/// <param name="FailureCode">The stable failure code.</param>
|
/// <param name="FailureCode">The stable failure code.</param>
|
||||||
/// <param name="Diagnostics">Safe technical diagnostics.</param>
|
/// <param name="Diagnostics">Safe technical diagnostics.</param>
|
||||||
/// <param name="CanContinueAsRebuild">Whether incompatible valid content can continue without another content call.</param>
|
/// <param name="CanContinueAsRebuild">Whether incompatible valid content can continue without another content call.</param>
|
||||||
|
|||||||
@ -16,8 +16,14 @@ public sealed class VisualBriefingFailure
|
|||||||
public VisualBriefingBuildStage Stage { get; set; }
|
public VisualBriefingBuildStage Stage { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the localized or user-safe message.
|
/// Gets or sets the user-safe issue text in stable English.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This text is never localized: it is sent back to the model as a repair instruction and it is
|
||||||
|
/// persisted with the build record, so both a translation and a later language switch would break
|
||||||
|
/// it. Use <see cref="VisualBriefingFailureExtensions.ToUserMessage(VisualBriefingFailure)"/> to
|
||||||
|
/// obtain the text shown to the user.
|
||||||
|
/// </remarks>
|
||||||
public string UserMessage { get; set; } = string.Empty;
|
public string UserMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
namespace AIStudio.Assistants.VisualBriefing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Translates the stable failure enums of one visual briefing operation into user-facing text.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The issue texts that travel with a failure are contract language: they are sent back to the model
|
||||||
|
/// as repair instructions, and they are persisted into the build record on disk. Both uses require
|
||||||
|
/// stable English, so they can never be localized at their origin. The UI therefore keeps only the
|
||||||
|
/// stable enums and asks for its text here, at render time, in the language selected right now.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class VisualBriefingFailureExtensions
|
||||||
|
{
|
||||||
|
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(VisualBriefingFailureExtensions).Namespace, nameof(VisualBriefingFailureExtensions));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the localized message for one recorded failure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failure">The recorded failure.</param>
|
||||||
|
/// <returns>The localized message.</returns>
|
||||||
|
internal static string ToUserMessage(this VisualBriefingFailure failure) => ToUserMessage(failure.Code, failure.ValidationRule);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the localized message for one failure code and validation rule.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The failure code decides because it is the only value that is always about the failure at hand.
|
||||||
|
/// A validation rule is not: a failure records the rule of whichever stage recorded one, so a failed
|
||||||
|
/// commit or an incompatible content signature can carry the rule of an earlier stage. The two codes
|
||||||
|
/// below are the exception. They say no more than "the response was rejected", so there the rule
|
||||||
|
/// names the concrete violation and gives the better text.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="code">The stable failure code.</param>
|
||||||
|
/// <param name="rule">The stable validation rule.</param>
|
||||||
|
/// <returns>The localized message.</returns>
|
||||||
|
internal static string ToUserMessage(VisualBriefingFailureCode code, VisualBriefingValidationRule rule) => code switch
|
||||||
|
{
|
||||||
|
VisualBriefingFailureCode.RESPONSE_JSON_INVALID or VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID when rule is not VisualBriefingValidationRule.NONE => rule.ToUserMessage(),
|
||||||
|
|
||||||
|
_ => code.ToUserMessage(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the localized message for one validation rule.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rule">The stable validation rule.</param>
|
||||||
|
/// <returns>The localized message.</returns>
|
||||||
|
private static string ToUserMessage(this VisualBriefingValidationRule rule) => rule switch
|
||||||
|
{
|
||||||
|
VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.UNKNOWN_FIELD => TB("The model response contained unexpected fields. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CONTRACT_VERSION_UNSUPPORTED => TB("The model response used an unsupported contract version. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.ID_INVALID => TB("The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.REFERENCE_INVALID => TB("The model response referenced content that does not exist. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID => TB("The model did not fill every planned content slot exactly once. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.SLOT_VALUE_TYPE_INVALID => TB("The model filled a content slot with the wrong kind of value. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CHART_SET_INVALID => TB("The charts of the model response did not match the planned briefing elements. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CHART_DATA_INVALID => TB("A chart of the model response contained invalid categories or data series. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CONTROL_ID_INVALID => TB("An interactive control of the model response used an invalid identifier. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CONTROL_TARGET_INVALID => TB("An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CONTROL_STATE_INVALID => TB("An interactive control of the model response used an invalid initial state. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.CONTROL_REQUIREMENT_INVALID => TB("A briefing element of the model response was missing its required interactive controls. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.FORMULA_TARGET_INVALID => TB("A calculation of the model response targeted an invalid briefing element. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.FORMULA_AST_INVALID => TB("A calculation of the model response used an invalid operation. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.ACCESSIBILITY_SET_INVALID => TB("The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.ACCESSIBILITY_TEXT_INVALID => TB("An accessibility text of the model response was empty or invalid. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.LAYOUT_INVALID => TB("The model response used an invalid briefing layout. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.TEMPLATE_ATTRIBUTE_PROHIBITED => TB("The model response used a prohibited attribute. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.MODEL_MARKUP_PROHIBITED => TB("The model response contained markup or code, which this briefing does not allow. Please try again or select another model."),
|
||||||
|
VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."),
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the localized message for one failure code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">The stable failure code.</param>
|
||||||
|
/// <returns>The localized message.</returns>
|
||||||
|
private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch
|
||||||
|
{
|
||||||
|
VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."),
|
||||||
|
VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."),
|
||||||
|
VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."),
|
||||||
|
VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."),
|
||||||
|
VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED => TB("The sources of this briefing could not be prepared."),
|
||||||
|
VisualBriefingFailureCode.PROVIDER_CALL_FAILED => TB("The selected provider could not complete this briefing stage."),
|
||||||
|
VisualBriefingFailureCode.RESPONSE_JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."),
|
||||||
|
VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID => TB("The model response did not match the required contract. Please try again or select another model."),
|
||||||
|
VisualBriefingFailureCode.COMPILER_INVARIANT_VIOLATED => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."),
|
||||||
|
VisualBriefingFailureCode.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."),
|
||||||
|
VisualBriefingFailureCode.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."),
|
||||||
|
VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE => TB("The updated content no longer fits the current presentation. You can continue as a rebuild."),
|
||||||
|
VisualBriefingFailureCode.PRESENTATION_INVALID => TB("The presentation of the model response did not match the briefing contract. Please try again or select another model."),
|
||||||
|
VisualBriefingFailureCode.ASSEMBLY_FAILED => TB("This briefing could not be assembled."),
|
||||||
|
VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED => TB("The assembled briefing did not pass the security validation."),
|
||||||
|
VisualBriefingFailureCode.STORE_FAILED => TB("The new version of this briefing could not be saved."),
|
||||||
|
VisualBriefingFailureCode.NO_CHANGES => TB("This operation did not change the briefing, so no new version was created."),
|
||||||
|
VisualBriefingFailureCode.CANCELED => TB("This visual briefing operation was canceled."),
|
||||||
|
VisualBriefingFailureCode.BUILD_INTERRUPTED => TB("AI Studio was closed while this briefing was being built. You can resume the build."),
|
||||||
|
VisualBriefingFailureCode.UNEXPECTED => TB("This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."),
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
|||||||
|
|
||||||
namespace AIStudio.Components;
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
public partial class AssistantBlock<TSettings> : MSGComponentBase where TSettings : IComponent
|
public partial class AssistantBlock<TSettings> : MSGComponentBase, IAssistantCategoryMember where TSettings : IComponent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Describes the assistant session indicator shown on top of the assistant icon.
|
/// Describes the assistant session indicator shown on top of the assistant icon.
|
||||||
@ -58,6 +58,12 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE;
|
public PreviewFeatures RequiredPreviewFeature { get; set; } = PreviewFeatures.NONE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the assistant category this block belongs to, if any.
|
||||||
|
/// </summary>
|
||||||
|
[CascadingParameter]
|
||||||
|
public AssistantCategoryBlock? Category { get; set; }
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private MudTheme ColorTheme { get; init; } = null!;
|
private MudTheme ColorTheme { get; init; } = null!;
|
||||||
|
|
||||||
@ -88,7 +94,8 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
|
|||||||
|
|
||||||
private string BlockStyle => $"border-width: 3px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
|
private string BlockStyle => $"border-width: 3px; border-color: {this.BorderColor}; border-radius: 12px; border-style: solid; max-width: 20em;";
|
||||||
|
|
||||||
private bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature);
|
/// <inheritdoc />
|
||||||
|
public bool IsVisible => this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Name, requiredPreviewFeature: this.RequiredPreviewFeature);
|
||||||
|
|
||||||
private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
|
private bool HasSettingsPanel => typeof(TSettings) != typeof(NoSettingsPanel);
|
||||||
|
|
||||||
@ -153,6 +160,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
|
|||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||||
|
this.Category?.RegisterAssistant(this);
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +173,7 @@ public partial class AssistantBlock<TSettings> : MSGComponentBase where TSetting
|
|||||||
protected override void DisposeResources()
|
protected override void DisposeResources()
|
||||||
{
|
{
|
||||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||||
|
this.Category?.UnregisterAssistant(this);
|
||||||
base.DisposeResources();
|
base.DisposeResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
@if (this.HasVisibleAssistant)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h4" Class="@this.HeaderClass">
|
||||||
|
@this.Title
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
<CascadingValue Value="this" IsFixed="@true">
|
||||||
|
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="@this.StackClass">
|
||||||
|
@this.ChildContent
|
||||||
|
</MudStack>
|
||||||
|
</CascadingValue>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders one category of assistants together with its heading.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The heading is derived from the assistant blocks inside this category: it is rendered only when
|
||||||
|
/// at least one of them is visible. Thus, hiding assistants by configuration can never leave an
|
||||||
|
/// empty category heading behind.
|
||||||
|
/// </remarks>
|
||||||
|
public partial class AssistantCategoryBlock : ComponentBase
|
||||||
|
{
|
||||||
|
private readonly HashSet<IAssistantCategoryMember> members = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The heading of this category.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The CSS classes used for the heading.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string HeaderClass { get; set; } = "mb-2 mr-3 mt-6";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an assistant block to this category.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Assistant blocks call this while they initialize, i.e. after this category was rendered for
|
||||||
|
/// the first time. Hence, we have to render again to show the heading.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="member">The assistant block which belongs to this category.</param>
|
||||||
|
internal void RegisterAssistant(IAssistantCategoryMember member)
|
||||||
|
{
|
||||||
|
if (this.members.Add(member))
|
||||||
|
this.StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an assistant block from this category.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="member">The assistant block which no longer belongs to this category.</param>
|
||||||
|
internal void UnregisterAssistant(IAssistantCategoryMember member) => this.members.Remove(member);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether at least one assistant of this category is visible right now.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// We evaluate this live instead of caching it. That way, changes to the configuration take
|
||||||
|
/// effect as soon as the assistants page renders again.
|
||||||
|
/// </remarks>
|
||||||
|
private bool HasVisibleAssistant => this.members.Any(member => member.IsVisible);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the CSS classes used for the assistant stack.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The stack must be rendered even when no assistant is visible, because the assistant blocks
|
||||||
|
/// register themselves while rendering. Without any visible assistant, we drop the margin so
|
||||||
|
/// that a hidden category leaves no gap behind.
|
||||||
|
/// </remarks>
|
||||||
|
private string StackClass => this.HasVisibleAssistant ? "mb-3" : string.Empty;
|
||||||
|
}
|
||||||
@ -1,90 +0,0 @@
|
|||||||
using AIStudio.Dialogs;
|
|
||||||
using AIStudio.Tools.Media;
|
|
||||||
using AIStudio.Tools.PluginSystem;
|
|
||||||
using AIStudio.Tools.Services;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
|
||||||
|
|
||||||
namespace AIStudio.Components;
|
|
||||||
|
|
||||||
public partial class AssistantPluginDeleteAction : MSGComponentBase
|
|
||||||
{
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public IAvailablePlugin Plugin { get; set; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IDialogService DialogService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private ILogger<AssistantPluginDeleteAction> Logger { get; init; } = null!;
|
|
||||||
|
|
||||||
private bool CanDelete => AssistantPluginInstallService.CanDeleteInstalledAssistant(this.Plugin);
|
|
||||||
|
|
||||||
private bool IsBlockedByActiveWork => this.AssistantPluginInstallService.HasActiveAssistantWork(this.Plugin.Id);
|
|
||||||
|
|
||||||
private string Tooltip => this.IsBlockedByActiveWork
|
|
||||||
? this.T("The assistant cannot be deleted while background work is still running.")
|
|
||||||
: this.T("Delete assistant plugin");
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
this.ApplyFilters([], [ Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED ]);
|
|
||||||
this.MediaTranscriptionService.StateChanged += this.OnMediaTranscriptionStateChanged;
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteAssistantPluginAsync()
|
|
||||||
{
|
|
||||||
if (!this.CanDelete || this.IsBlockedByActiveWork)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
x => x.Message,
|
|
||||||
string.Format(this.T("Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."), this.Plugin.Name)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(this.T("Delete Assistant Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
|
|
||||||
var dialogResult = await dialogReference.Result;
|
|
||||||
if (dialogResult is null || dialogResult.Canceled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var result = await this.AssistantPluginInstallService.DeleteInstalledAssistantAsync(this.Plugin, CancellationToken.None);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
this.Logger.LogError("Failed to delete assistant plugin '{PluginName}' ({PluginId}) from '{PluginDirectory}' with issue '{Issue}'.", result.PluginName, result.PluginId, result.PluginDirectory, result.Issue);
|
|
||||||
await this.MessageBus.SendError(new(Icons.Material.Filled.DeleteForever, string.Format(this.T("The assistant plugin '{0}' could not be deleted: {1}"), this.Plugin.Name, result.Issue)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Check, string.Format(this.T("The '{0}' assistant plugin has been successfully removed."), result.PluginName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMediaTranscriptionStateChanged(MediaImportOwner owner)
|
|
||||||
{
|
|
||||||
if (owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.EndsWith($":{this.Plugin.Id}", StringComparison.Ordinal))
|
|
||||||
_ = this.InvokeAsync(this.StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
|
||||||
{
|
|
||||||
if (triggeredEvent is Event.ASSISTANT_SESSION_CHANGED or Event.ASSISTANT_SESSION_FINISHED)
|
|
||||||
this.StateHasChanged();
|
|
||||||
|
|
||||||
return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DisposeResources()
|
|
||||||
{
|
|
||||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaTranscriptionStateChanged;
|
|
||||||
base.DisposeResources();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -222,6 +222,11 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
protected override void DisposeResources()
|
protected override void DisposeResources()
|
||||||
{
|
{
|
||||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||||
|
|
||||||
|
// Release the drop area. Without this, drop areas below this one would count this component
|
||||||
|
// forever and would stop catching dropped files:
|
||||||
|
_ = this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer);
|
||||||
|
|
||||||
base.DisposeResources();
|
base.DisposeResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,13 @@ public abstract partial class ConfigurationBase : MSGComponentBase
|
|||||||
|
|
||||||
protected bool IsDisabled => this.Disabled() || this.IsLocked();
|
protected bool IsDisabled => this.Disabled() || this.IsLocked();
|
||||||
|
|
||||||
private string Classes => $"{this.GetClassForBase} {JUSTIFIED_HELP_CLASS} {MARGIN_CLASS}";
|
private string Classes => $"{this.GetClassForBase} {JUSTIFIED_HELP_CLASS} {this.MarginClass}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The bottom margin of the option. Options inside settings panels need the default
|
||||||
|
/// spacing; standalone usages like toolbar buttons can remove it.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string MarginClass => MARGIN_CLASS;
|
||||||
|
|
||||||
private protected virtual RenderFragment? Body => null;
|
private protected virtual RenderFragment? Body => null;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an assistant block which belongs to an assistant category.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Assistant blocks are generic over their settings dialog. This interface gives the category block
|
||||||
|
/// access to their visibility without the need to know that type parameter.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IAssistantCategoryMember
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the assistant is visible right now.
|
||||||
|
/// </summary>
|
||||||
|
bool IsVisible { get; }
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
@inherits ConfigurationBaseCore
|
@inherits ConfigurationBaseCore
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@this.Icon" Disabled="@this.IsDisabled" OnClick="@(async () => await this.ClickAsync())">
|
@* The tooltip is suppressed while the button is locked, so that the lock icon's tooltip is the only one shown: *@
|
||||||
|
<MudTooltip Text="@this.Tooltip" Disabled="@(this.IsLocked() || string.IsNullOrWhiteSpace(this.Tooltip))" RootStyle="display:inline-flex;">
|
||||||
|
<MudButton Variant="@this.ButtonVariant" Color="@this.ButtonColor" StartIcon="@this.Icon" Disabled="@this.IsDisabled" OnClick="@(async () => await this.ClickAsync())">
|
||||||
@this.Text
|
@this.Text
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
</MudTooltip>
|
||||||
@ -19,6 +19,32 @@ public partial class LockableButton : ConfigurationBaseCore
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string Class { get; set; } = string.Empty;
|
public string Class { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional tooltip for the button. It is not shown while the button is locked,
|
||||||
|
/// because the lock icon explains the situation in that case.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string Tooltip { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The visual variant of the button.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Variant ButtonVariant { get; set; } = Variant.Filled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The color of the button.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Color ButtonColor { get; set; } = Color.Primary;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the default bottom margin be removed? Useful when the button is placed in a
|
||||||
|
/// toolbar instead of a settings panel.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool NoMargin { get; set; }
|
||||||
|
|
||||||
#region Overrides of ConfigurationBase
|
#region Overrides of ConfigurationBase
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -26,6 +52,8 @@ public partial class LockableButton : ConfigurationBaseCore
|
|||||||
|
|
||||||
protected override string GetClassForBase => this.Class;
|
protected override string GetClassForBase => this.Class;
|
||||||
|
|
||||||
|
protected override string MarginClass => this.NoMargin ? string.Empty : base.MarginClass;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task ClickAsync()
|
private async Task ClickAsync()
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
Variant="Variant.Text"
|
Variant="Variant.Text"
|
||||||
Size="Size.Medium"
|
Size="Size.Medium"
|
||||||
Disabled="@this.IsBlockedByActiveWork"
|
Disabled="@(this.isDeleting || this.IsBlockedByActiveWork)"
|
||||||
OnClick="@this.DeleteAssistantPluginAsync" />
|
OnClick="@this.DeletePluginAsync" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
169
app/MindWork AI Studio/Components/PluginDeleteAction.razor.cs
Normal file
169
app/MindWork AI Studio/Components/PluginDeleteAction.razor.cs
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
using AIStudio.Dialogs;
|
||||||
|
using AIStudio.Tools.Media;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets users remove a plugin they installed or placed themselves.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Without this action, such a plugin could only be removed from the data directory by hand. That is
|
||||||
|
/// especially painful for configuration plugins, which have no activation switch at all. Plugins
|
||||||
|
/// shipped with AI Studio and plugins deployed by an organization stay untouched: the action does
|
||||||
|
/// not appear for them.
|
||||||
|
/// </remarks>
|
||||||
|
public partial class PluginDeleteAction : MSGComponentBase
|
||||||
|
{
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public IAvailablePlugin Plugin { get; set; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IDialogService DialogService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private ILogger<PluginDeleteAction> Logger { get; init; } = null!;
|
||||||
|
|
||||||
|
private bool isDeleting;
|
||||||
|
|
||||||
|
private bool IsAssistant => this.Plugin.Type is PluginType.ASSISTANT;
|
||||||
|
|
||||||
|
private bool CanDelete => PluginInstallService.CanDeletePlugin(this.Plugin);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True while an assistant still owns background work. We keep the action visible and block it
|
||||||
|
/// instead of hiding it, so that the tooltip can explain why it does nothing right now.
|
||||||
|
/// </summary>
|
||||||
|
private bool IsBlockedByActiveWork => this.IsAssistant && this.PluginInstallService.HasActiveAssistantWork(this.Plugin.Id);
|
||||||
|
|
||||||
|
private string Tooltip
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.IsBlockedByActiveWork)
|
||||||
|
return this.T("The assistant cannot be deleted while background work is still running.");
|
||||||
|
|
||||||
|
return this.Plugin.Type switch
|
||||||
|
{
|
||||||
|
PluginType.ASSISTANT => this.T("Delete assistant plugin"),
|
||||||
|
PluginType.CONFIGURATION => this.T("Delete configuration plugin"),
|
||||||
|
|
||||||
|
_ => this.T("Delete language plugin"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Only an assistant can be busy. We watch its sessions and transcriptions, so the action
|
||||||
|
// reflects the current state without the user reloading the page:
|
||||||
|
this.ApplyFilters([], this.IsAssistant ? [Event.ASSISTANT_SESSION_CHANGED, Event.ASSISTANT_SESSION_FINISHED] : []);
|
||||||
|
if (this.IsAssistant)
|
||||||
|
this.MediaTranscriptionService.StateChanged += this.OnMediaTranscriptionStateChanged;
|
||||||
|
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||||
|
{
|
||||||
|
if (triggeredEvent is Event.ASSISTANT_SESSION_CHANGED or Event.ASSISTANT_SESSION_FINISHED)
|
||||||
|
this.StateHasChanged();
|
||||||
|
|
||||||
|
return base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisposeResources()
|
||||||
|
{
|
||||||
|
if (this.IsAssistant)
|
||||||
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaTranscriptionStateChanged;
|
||||||
|
|
||||||
|
base.DisposeResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private async Task DeletePluginAsync()
|
||||||
|
{
|
||||||
|
if (!this.CanDelete || this.isDeleting || this.IsBlockedByActiveWork)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!await this.ConfirmDeletionAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isDeleting = true;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await this.PluginInstallService.DeletePluginAsync(this.Plugin, CancellationToken.None);
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
this.Logger.LogError("Failed to delete {PluginType} plugin '{PluginName}' ({PluginId}) from '{PluginDirectory}' with issue '{Issue}'.", this.Plugin.Type, result.PluginName, result.PluginId, result.PluginDirectory, result.Issue);
|
||||||
|
await this.MessageBus.SendError(new(Icons.Material.Filled.DeleteForever, string.Format(this.T("The plugin '{0}' could not be deleted: {1}"), this.Plugin.Name, result.Issue)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Check, string.Format(this.T("The plugin '{0}' has been successfully removed."), result.PluginName)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isDeleting = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the user before the deletion. A configuration gets the dialog listing its consequences,
|
||||||
|
/// because removing it also removes the providers and settings it brought. Assistants and
|
||||||
|
/// language plugins only own their own files, so a plain confirmation is enough.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ConfirmDeletionAsync()
|
||||||
|
{
|
||||||
|
if (this.Plugin.Type is PluginType.CONFIGURATION)
|
||||||
|
{
|
||||||
|
var configurationParameters = new DialogParameters<ConfigurationPluginDeleteDialog>
|
||||||
|
{
|
||||||
|
{ x => x.PluginName, this.Plugin.Name },
|
||||||
|
{ x => x.Summary, this.PluginInstallService.BuildConfigurationDeleteSummary(this.Plugin) },
|
||||||
|
};
|
||||||
|
|
||||||
|
var configurationDialog = await this.DialogService.ShowAsync<ConfigurationPluginDeleteDialog>(this.T("Delete Configuration Plugin"), configurationParameters, DialogOptions.FULLSCREEN);
|
||||||
|
return await configurationDialog.Result is { Canceled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = this.IsAssistant
|
||||||
|
? this.T("Delete Assistant Plugin")
|
||||||
|
: this.T("Delete Language Plugin");
|
||||||
|
|
||||||
|
var message = this.IsAssistant
|
||||||
|
? string.Format(this.T("Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."), this.Plugin.Name)
|
||||||
|
: string.Format(this.T("Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically."), this.Plugin.Name);
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<ConfirmDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Message, message },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await this.DialogService.ShowAsync<ConfirmDialog>(title, parameters, DialogOptions.FULLSCREEN);
|
||||||
|
return await dialog.Result is { Canceled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMediaTranscriptionStateChanged(MediaImportOwner owner)
|
||||||
|
{
|
||||||
|
if (owner.Kind is MediaImportOwnerKind.ASSISTANT && owner.Id.EndsWith($":{this.Plugin.Id}", StringComparison.Ordinal))
|
||||||
|
_ = this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -174,10 +174,16 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
this.MediaTranscriptionService.AcknowledgeDelivery(delivery);
|
this.MediaTranscriptionService.AcknowledgeDelivery(delivery);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Unsubscribes from the singleton media service.</summary>
|
/// <summary>Unsubscribes from the singleton media service and releases the drop area.</summary>
|
||||||
protected override void DisposeResources()
|
protected override void DisposeResources()
|
||||||
{
|
{
|
||||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||||
|
|
||||||
|
// Release the drop area. Without this, drop areas below this one would count this component
|
||||||
|
// forever and would stop catching dropped files:
|
||||||
|
if (this.EnableDragDrop)
|
||||||
|
_ = this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer);
|
||||||
|
|
||||||
base.DisposeResources();
|
base.DisposeResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,8 +108,10 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
|||||||
|
|
||||||
private HashSet<PreviewFeatures> GetPluginContributedPreviewFeatures()
|
private HashSet<PreviewFeatures> GetPluginContributedPreviewFeatures()
|
||||||
{
|
{
|
||||||
|
// Several configuration plugins may contribute at the same time, e.g. one preview feature
|
||||||
|
// for the whole organization and another one for a single department:
|
||||||
if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution)
|
if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution)
|
||||||
return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet();
|
return meta.PluginContributions.Values.SelectMany(contribution => contribution).Where(x => !x.IsReleased()).ToHashSet();
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -122,7 +124,7 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
|||||||
if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution)
|
if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return meta.PluginContribution.Contains(feature);
|
return meta.PluginContributions.Values.Any(contribution => contribution.Contains(feature));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashSet<PreviewFeatures> GetSelectedPreviewFeatures()
|
private HashSet<PreviewFeatures> GetSelectedPreviewFeatures()
|
||||||
|
|||||||
@ -40,9 +40,9 @@
|
|||||||
|
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.")">
|
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings.") : T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings."))">
|
||||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,9 +31,9 @@
|
|||||||
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
|
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@T("This provider is trusted by your organization for data source security checks.")">
|
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted provider is trusted for data source security checks.") : T("This provider is trusted by your organization for data source security checks."))">
|
||||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,9 @@
|
|||||||
|
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||||
@if (context.IsTrustedByConfiguration(this.SettingsManager))
|
@if (context.IsTrustedForDataSourceSecurityChecks(this.SettingsManager))
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@T("This transcription provider is trusted by your organization for data source security checks.")">
|
<MudTooltip Text="@(context.IsSelfHosted ? T("This self-hosted transcription provider is trusted for data source security checks.") : T("This transcription provider is trusted by your organization for data source security checks."))">
|
||||||
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,6 @@
|
|||||||
<MudDialog DefaultFocus="DefaultFocus.None">
|
<MudDialog DefaultFocus="DefaultFocus.None">
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
@if (!string.IsNullOrWhiteSpace(this.issue))
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Error" Dense="true">
|
|
||||||
@this.issue
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.isLoading)
|
@if (this.isLoading)
|
||||||
{
|
{
|
||||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||||
@ -35,6 +28,12 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.issue))
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Dense="true" Class="mx-3">
|
||||||
|
@this.issue
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
<MudButton OnClick="@this.Cancel" Disabled="@this.isSaving" Size="Size.Small">
|
<MudButton OnClick="@this.Cancel" Disabled="@this.isSaving" Size="Size.Small">
|
||||||
@T("Cancel")
|
@T("Cancel")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
|||||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid PluginId { get; set; }
|
public Guid PluginId { get; set; }
|
||||||
@ -105,7 +105,7 @@ public partial class AssistantPluginEditorDialog : MSGComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var editedLua = await this.codeEditor.GetCodeAsync();
|
var editedLua = await this.codeEditor.GetCodeAsync();
|
||||||
var result = await this.AssistantPluginInstallService.UpdateInstalledAssistantAsync(this.plugin, editedLua, CancellationToken.None);
|
var result = await this.PluginInstallService.UpdateInstalledAssistantAsync(this.plugin, editedLua, CancellationToken.None);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
LOGGER.LogError($"Failed to update assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
LOGGER.LogError($"Failed to update assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
||||||
|
|||||||
@ -23,7 +23,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
|||||||
private AssistantPluginGenerationService AssistantPluginGenerationService { get; init; } = null!;
|
private AssistantPluginGenerationService AssistantPluginGenerationService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||||
@ -144,7 +144,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
|||||||
if (this.availablePlugin is null)
|
if (this.availablePlugin is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.revisionCheckResult = await this.AssistantPluginInstallService.CheckInstalledAssistantUpdateAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
this.revisionCheckResult = await this.PluginInstallService.CheckInstalledAssistantUpdateAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||||
if (this.revisionCheckResult.Success)
|
if (this.revisionCheckResult.Success)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ public partial class AssistantPluginRevisionDialog : MSGComponentBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await this.AssistantPluginInstallService.UpdateInstalledAssistantAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
var result = await this.PluginInstallService.UpdateInstalledAssistantAsync(this.availablePlugin, this.revisedLua, CancellationToken.None);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
LOGGER.LogError($"Failed to revise assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
LOGGER.LogError($"Failed to revise assistant plugin '{result.PluginName}' ({result.PluginId}) in '{result.PluginDirectory}' with issue '{result.Issue}'.");
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
@inherits MSGComponentBase
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
@(string.Format(T("Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files."), this.PluginName))
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
@if (this.Consequences.Count > 0)
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-1">
|
||||||
|
@T("This also removes everything the configuration plugin had set up:")
|
||||||
|
</MudJustifiedText>
|
||||||
|
|
||||||
|
<MudList T="string" Class="mb-3">
|
||||||
|
@foreach (var consequence in this.Consequences)
|
||||||
|
{
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Filled.RemoveCircleOutline" IconColor="Color.Error">
|
||||||
|
@consequence
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||||
|
@T("The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well.")
|
||||||
|
</MudJustifiedText>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudJustifiedText Typo="Typo.body2">
|
||||||
|
@T("You can install the plugin again later, but any changes you made to its settings are lost.")
|
||||||
|
</MudJustifiedText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||||
|
@T("No")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Error">
|
||||||
|
@T("Yes, delete it")
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the user whether a local configuration plugin may be deleted, and shows what the deletion
|
||||||
|
/// takes with it.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ConfigurationPluginDeleteDialog : MSGComponentBase
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the configuration plugin about to be deleted.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string PluginName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What the deletion removes besides the plugin directory.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public ConfigurationPluginDeleteSummary Summary { get; set; } = ConfigurationPluginDeleteSummary.EMPTY;
|
||||||
|
|
||||||
|
private List<string> Consequences => this.BuildConsequences();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Turns the summary into the lines shown to the user. Only what is actually affected is listed,
|
||||||
|
/// so the dialog stays short for a configuration plugin that just locks a single setting.
|
||||||
|
/// </summary>
|
||||||
|
private List<string> BuildConsequences()
|
||||||
|
{
|
||||||
|
var consequences = new List<string>();
|
||||||
|
var summary = this.Summary;
|
||||||
|
|
||||||
|
Add(summary.LlmProviders, this.T("{0} LLM provider"), this.T("{0} LLM providers"));
|
||||||
|
Add(summary.TranscriptionProviders, this.T("{0} transcription provider"), this.T("{0} transcription providers"));
|
||||||
|
Add(summary.EmbeddingProviders, this.T("{0} embedding provider"), this.T("{0} embedding providers"));
|
||||||
|
Add(summary.ChatTemplates, this.T("{0} chat template"), this.T("{0} chat templates"));
|
||||||
|
Add(summary.Profiles, this.T("{0} profile"), this.T("{0} profiles"));
|
||||||
|
Add(summary.DocumentAnalysisPolicies, this.T("{0} document analysis policy"), this.T("{0} document analysis policies"));
|
||||||
|
Add(summary.MandatoryInfos, this.T("{0} mandatory information"), this.T("{0} mandatory informations"));
|
||||||
|
Add(summary.Introductions, this.T("{0} introduction on the welcome page"), this.T("{0} introductions on the welcome page"));
|
||||||
|
|
||||||
|
// Data sources are called out separately: removing them also deletes their credentials from
|
||||||
|
// the operating system's keychain, which the user cannot undo by reinstalling the plugin.
|
||||||
|
Add(summary.DataSources,
|
||||||
|
this.T("{0} data source, including its credentials in your operating system's keychain"),
|
||||||
|
this.T("{0} data sources, including their credentials in your operating system's keychain"));
|
||||||
|
|
||||||
|
Add(summary.LockedSettings, this.T("{0} setting returns to its default value"), this.T("{0} settings return to their default values"));
|
||||||
|
|
||||||
|
return consequences;
|
||||||
|
|
||||||
|
void Add(int count, string singular, string plural)
|
||||||
|
{
|
||||||
|
if (count > 0)
|
||||||
|
consequences.Add(string.Format(count == 1 ? singular : plural, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel() => this.MudDialog.Cancel();
|
||||||
|
|
||||||
|
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
16
app/MindWork AI Studio/Dialogs/InformationDialog.razor
Normal file
16
app/MindWork AI Studio/Dialogs/InformationDialog.razor
Normal 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>
|
||||||
35
app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs
Normal file
35
app/MindWork AI Studio/Dialogs/InformationDialog.razor.cs
Normal 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));
|
||||||
|
}
|
||||||
105
app/MindWork AI Studio/Dialogs/PluginImportDialog.razor
Normal file
105
app/MindWork AI Studio/Dialogs/PluginImportDialog.razor
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
@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 (this.Preview.ConfigurationSummary is { HasAnyContent: true } configurationSummary)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-3">
|
||||||
|
@T("A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:")
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
@if (configurationSummary.Destinations.Count > 0)
|
||||||
|
{
|
||||||
|
<MudSimpleTable Dense="@true" Striped="@true" Class="mb-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@T("Sends data to")</th>
|
||||||
|
<th>@T("Name")</th>
|
||||||
|
<th>@T("Destination")</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var destination in configurationSummary.Destinations)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@this.DestinationTypeLabel(destination.Type)</td>
|
||||||
|
<td>@destination.Name</td>
|
||||||
|
<td><strong>@destination.Endpoint</strong></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</MudSimpleTable>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.ConfigurationContents.Count > 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mb-1">
|
||||||
|
@T("It also brings:")
|
||||||
|
</MudText>
|
||||||
|
<MudList T="string" Class="mb-3">
|
||||||
|
@foreach (var content in this.ConfigurationContents)
|
||||||
|
{
|
||||||
|
<MudListItem T="string" Icon="@Icons.Material.Filled.AddCircleOutline">
|
||||||
|
@content
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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>
|
||||||
89
app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs
Normal file
89
app/MindWork AI Studio/Dialogs/PluginImportDialog.razor.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Names the kind of a destination a configuration plugin brings.
|
||||||
|
/// </summary>
|
||||||
|
private string DestinationTypeLabel(PluginConfigurationObjectType objectType) => objectType switch
|
||||||
|
{
|
||||||
|
PluginConfigurationObjectType.LLM_PROVIDER => this.T("LLM provider"),
|
||||||
|
PluginConfigurationObjectType.EMBEDDING_PROVIDER => this.T("Embedding provider"),
|
||||||
|
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => this.T("Transcription provider"),
|
||||||
|
PluginConfigurationObjectType.DATA_SOURCE => this.T("Data source"),
|
||||||
|
|
||||||
|
_ => this.T("Unknown"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Everything a configuration plugin brings besides its providers and data sources. Only what is
|
||||||
|
/// actually there gets listed, so the dialog stays short for a small configuration.
|
||||||
|
/// </summary>
|
||||||
|
private List<string> ConfigurationContents
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var contents = new List<string>();
|
||||||
|
if (this.Preview.ConfigurationSummary is not { } summary)
|
||||||
|
return contents;
|
||||||
|
|
||||||
|
Add(summary.DeclaredSettings, this.T("{0} setting it takes control of"), this.T("{0} settings it takes control of"));
|
||||||
|
Add(summary.ChatTemplates, this.T("{0} chat template"), this.T("{0} chat templates"));
|
||||||
|
Add(summary.Profiles, this.T("{0} profile"), this.T("{0} profiles"));
|
||||||
|
Add(summary.DocumentAnalysisPolicies, this.T("{0} document analysis policy"), this.T("{0} document analysis policies"));
|
||||||
|
Add(summary.MandatoryInfos, this.T("{0} mandatory information you have to accept before using AI Studio"), this.T("{0} mandatory information you have to accept before using AI Studio"));
|
||||||
|
Add(summary.Introductions, this.T("{0} introduction on the welcome page"), this.T("{0} introductions on the welcome page"));
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
|
||||||
|
void Add(int count, string singular, string plural)
|
||||||
|
{
|
||||||
|
if (count > 0)
|
||||||
|
contents.Add(string.Format(count == 1 ? singular : plural, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel() => this.MudDialog.Cancel();
|
||||||
|
|
||||||
|
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
@ -12,20 +12,7 @@
|
|||||||
|
|
||||||
<InnerScrolling>
|
<InnerScrolling>
|
||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("General",
|
<AssistantCategoryBlock Title="@T("General")" HeaderClass="mb-2 mr-3">
|
||||||
(Components.TEXT_SUMMARIZER_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.TRANSLATION_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.GRAMMAR_SPELLING_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.REWRITE_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.PROMPT_OPTIMIZER_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.SYNONYMS_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.META_ASSISTANT, PreviewFeatures.PRE_META_ASSISTANT_V1)
|
|
||||||
))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3">
|
|
||||||
@T("General")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
<AssistantBlock TSettings="SettingsDialogTextSummarizer" Component="Components.TEXT_SUMMARIZER_ASSISTANT" Name="@T("Text Summarizer")" Description="@T("Use an LLM to summarize a given text.")" Icon="@Icons.Material.Filled.TextSnippet" Link="@Routes.ASSISTANT_SUMMARIZER"/>
|
<AssistantBlock TSettings="SettingsDialogTextSummarizer" Component="Components.TEXT_SUMMARIZER_ASSISTANT" Name="@T("Text Summarizer")" Description="@T("Use an LLM to summarize a given text.")" Icon="@Icons.Material.Filled.TextSnippet" Link="@Routes.ASSISTANT_SUMMARIZER"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogTranslation" Component="Components.TRANSLATION_ASSISTANT" Name="@T("Translation")" Description="@T("Translate text into another language.")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_TRANSLATION"/>
|
<AssistantBlock TSettings="SettingsDialogTranslation" Component="Components.TRANSLATION_ASSISTANT" Name="@T("Translation")" Description="@T("Translate text into another language.")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_TRANSLATION"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogGrammarSpelling" Component="Components.GRAMMAR_SPELLING_ASSISTANT" Name="@T("Grammar & Spelling")" Description="@T("Check grammar and spelling of a given text.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_GRAMMAR_SPELLING"/>
|
<AssistantBlock TSettings="SettingsDialogGrammarSpelling" Component="Components.GRAMMAR_SPELLING_ASSISTANT" Name="@T("Grammar & Spelling")" Description="@T("Check grammar and spelling of a given text.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_GRAMMAR_SPELLING"/>
|
||||||
@ -33,15 +20,11 @@
|
|||||||
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
|
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
|
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
|
||||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.AssistantPlugins.Count > 0)
|
@if (this.AssistantPlugins.Count > 0)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
<AssistantCategoryBlock Title="@T("Installed Assistants")">
|
||||||
@T("Installed Assistants")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
@foreach (var assistantPlugin in this.AssistantPlugins)
|
@foreach (var assistantPlugin in this.AssistantPlugins)
|
||||||
{
|
{
|
||||||
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, assistantPlugin);
|
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, assistantPlugin);
|
||||||
@ -58,7 +41,7 @@
|
|||||||
<AdditionalActions>
|
<AdditionalActions>
|
||||||
@if (availablePlugin is not null)
|
@if (availablePlugin is not null)
|
||||||
{
|
{
|
||||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||||
}
|
}
|
||||||
</AdditionalActions>
|
</AdditionalActions>
|
||||||
<SecurityBadge>
|
<SecurityBadge>
|
||||||
@ -66,25 +49,10 @@
|
|||||||
</SecurityBadge>
|
</SecurityBadge>
|
||||||
</AssistantBlock>
|
</AssistantBlock>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business",
|
<AssistantCategoryBlock Title="@T("Business")">
|
||||||
(Components.EMAIL_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.MY_TASKS_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.AGENDA_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.LEGAL_CHECK_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.SLIDE_BUILDER_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.VISUAL_BRIEFING_ASSISTANT, Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature())
|
|
||||||
))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
|
||||||
@T("Business")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
<AssistantBlock TSettings="SettingsDialogWritingEMails" Component="Components.EMAIL_ASSISTANT" Name="@T("E-Mail")" Description="@T("Generate an e-mail for a given context.")" Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
|
<AssistantBlock TSettings="SettingsDialogWritingEMails" Component="Components.EMAIL_ASSISTANT" Name="@T("E-Mail")" Description="@T("Generate an e-mail for a given context.")" Icon="@Icons.Material.Filled.Email" Link="@Routes.ASSISTANT_EMAIL"/>
|
||||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Name="@T("Document Analysis")" Description="@T("Analyze a document regarding defined rules and extract key information.")" Icon="@Icons.Material.Filled.DocumentScanner" Link="@Routes.ASSISTANT_DOCUMENT_ANALYSIS"/>
|
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Name="@T("Document Analysis")" Description="@T("Analyze a document regarding defined rules and extract key information.")" Icon="@Icons.Material.Filled.DocumentScanner" Link="@Routes.ASSISTANT_DOCUMENT_ANALYSIS"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogMyTasks" Component="Components.MY_TASKS_ASSISTANT" Name="@T("My Tasks")" Description="@T("Analyze a text or an email for tasks you need to complete.")" Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
|
<AssistantBlock TSettings="SettingsDialogMyTasks" Component="Components.MY_TASKS_ASSISTANT" Name="@T("My Tasks")" Description="@T("Analyze a text or an email for tasks you need to complete.")" Icon="@Icons.Material.Filled.Task" Link="@Routes.ASSISTANT_MY_TASKS"/>
|
||||||
@ -94,48 +62,21 @@
|
|||||||
<AssistantBlock TSettings="SettingsDialogIconFinder" Component="Components.ICON_FINDER_ASSISTANT" Name="@T("Icon Finder")" Description="@T("Use an LLM to find an icon for a given context.")" Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
|
<AssistantBlock TSettings="SettingsDialogIconFinder" Component="Components.ICON_FINDER_ASSISTANT" Name="@T("Icon Finder")" Description="@T("Use an LLM to find an icon for a given context.")" Icon="@Icons.Material.Filled.FindInPage" Link="@Routes.ASSISTANT_ICON_FINDER"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogSlideBuilder" Component="Components.SLIDE_BUILDER_ASSISTANT" Name="@T("Slide Planner Assistant")" Description="@T("Develop slide content based on a given topic and content.")" Icon="@Icons.Material.Filled.Slideshow" Link="@Routes.ASSISTANT_SLIDE_BUILDER"/>
|
<AssistantBlock TSettings="SettingsDialogSlideBuilder" Component="Components.SLIDE_BUILDER_ASSISTANT" Name="@T("Slide Planner Assistant")" Description="@T("Develop slide content based on a given topic and content.")" Icon="@Icons.Material.Filled.Slideshow" Link="@Routes.ASSISTANT_SLIDE_BUILDER"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogVisualBriefing" Component="Components.VISUAL_BRIEFING_ASSISTANT" RequiredPreviewFeature="Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature()" Name="@T("Visual Briefing Assistant")" Description="@T("Turn documents, data, images, audio, and video into an audience-ready interactive briefing.")" Icon="@Icons.Material.Filled.DashboardCustomize" Link="@Routes.ASSISTANT_VISUAL_BRIEFING" />
|
<AssistantBlock TSettings="SettingsDialogVisualBriefing" Component="Components.VISUAL_BRIEFING_ASSISTANT" RequiredPreviewFeature="Components.VISUAL_BRIEFING_ASSISTANT.RequiredPreviewFeature()" Name="@T("Visual Briefing Assistant")" Description="@T("Turn documents, data, images, audio, and video into an audience-ready interactive briefing.")" Icon="@Icons.Material.Filled.DashboardCustomize" Link="@Routes.ASSISTANT_VISUAL_BRIEFING" />
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Learning",
|
<AssistantCategoryBlock Title="@T("Learning")">
|
||||||
(Components.BIAS_DAY_ASSISTANT, PreviewFeatures.NONE)
|
|
||||||
))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
|
||||||
@T("Learning")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
<AssistantBlock TSettings="SettingsDialogAssistantBias" Component="Components.BIAS_DAY_ASSISTANT" Name="@T("Bias of the Day")" Description="@T("Learn about one cognitive bias every day.")" Icon="@Icons.Material.Filled.Psychology" Link="@Routes.ASSISTANT_BIAS"/>
|
<AssistantBlock TSettings="SettingsDialogAssistantBias" Component="Components.BIAS_DAY_ASSISTANT" Name="@T("Bias of the Day")" Description="@T("Learn about one cognitive bias every day.")" Icon="@Icons.Material.Filled.Psychology" Link="@Routes.ASSISTANT_BIAS"/>
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("Software Engineering",
|
<AssistantCategoryBlock Title="@T("Software Engineering")">
|
||||||
(Components.CODING_ASSISTANT, PreviewFeatures.NONE),
|
|
||||||
(Components.ERI_ASSISTANT, PreviewFeatures.PRE_RAG_2024),
|
|
||||||
(Components.LOG_VIEWER_ASSISTANT, PreviewFeatures.NONE)
|
|
||||||
))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
|
||||||
@T("Software Engineering")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
<AssistantBlock TSettings="SettingsDialogCoding" Component="Components.CODING_ASSISTANT" Name="@T("Coding")" Description="@T("Get coding and debugging support from an LLM.")" Icon="@Icons.Material.Filled.Code" Link="@Routes.ASSISTANT_CODING"/>
|
||||||
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
<AssistantBlock TSettings="SettingsDialogERIServer" Component="Components.ERI_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_RAG_2024" Name="@T("ERI Server")" Description="@T("Generate an ERI server to integrate business systems.")" Icon="@Icons.Material.Filled.PrivateConnectivity" Link="@Routes.ASSISTANT_ERI"/>
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
|
||||||
|
|
||||||
@if (this.SettingsManager.IsAnyCategoryAssistantVisible("AI Studio Development",
|
<AssistantCategoryBlock Title="@T("AI Studio Development")">
|
||||||
(Components.I18N_ASSISTANT, PreviewFeatures.NONE)
|
|
||||||
))
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
|
|
||||||
@T("AI Studio Development")
|
|
||||||
</MudText>
|
|
||||||
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
|
|
||||||
<AssistantBlock TSettings="SettingsDialogI18N" Component="Components.I18N_ASSISTANT" Name="@T("Localization")" Description="@T("Translate AI Studio text content into other languages")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_AI_STUDIO_I18N"/>
|
<AssistantBlock TSettings="SettingsDialogI18N" Component="Components.I18N_ASSISTANT" Name="@T("Localization")" Description="@T("Translate AI Studio text content into other languages")" Icon="@Icons.Material.Filled.Translate" Link="@Routes.ASSISTANT_AI_STUDIO_I18N"/>
|
||||||
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.LOG_VIEWER_ASSISTANT" Name="@T("Log Viewer")" Description="@T("View and filter AI Studio log files.")" Icon="@Icons.Material.Filled.Article" Link="@Routes.ASSISTANT_LOG_VIEWER"/>
|
||||||
</MudStack>
|
</AssistantCategoryBlock>
|
||||||
}
|
|
||||||
|
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
</div>
|
</div>
|
||||||
@ -158,6 +158,31 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@*
|
||||||
|
A staged test configuration speaks for the organization without anybody
|
||||||
|
having deployed it. We report it without the details having to be expanded:
|
||||||
|
*@
|
||||||
|
@if (this.testConfigPlugins.Count > 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1" Class="mt-2">
|
||||||
|
@T("A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app.")
|
||||||
|
</MudText>
|
||||||
|
@foreach (var testConfigPlugin in this.testConfigPlugins)
|
||||||
|
{
|
||||||
|
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Science"
|
||||||
|
HeaderText="@testConfigPlugin.Name"
|
||||||
|
Items="@this.BuildTestConfigurationItems(testConfigPlugin)"
|
||||||
|
ShowWarning="@true"
|
||||||
|
WarningText="@T("Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio.")"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (PluginFactory.RemovedTestConfigurationsAtStartup > 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1" Class="mt-2">
|
||||||
|
@string.Format(T("AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running."), PluginFactory.RemovedTestConfigurationsAtStartup)
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
|
||||||
@if (this.HasEnterpriseConfigurationDetails)
|
@if (this.HasEnterpriseConfigurationDetails)
|
||||||
{
|
{
|
||||||
<MudButton StartIcon="@(this.showEnterpriseConfigDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
<MudButton StartIcon="@(this.showEnterpriseConfigDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||||
@ -312,7 +337,8 @@
|
|||||||
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
|
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
|
||||||
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
|
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
|
||||||
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@T("For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose.")"/>
|
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@T("For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose.")"/>
|
||||||
<ThirdPartyComponent Name="windows-registry" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.")"/>
|
<ThirdPartyComponent Name="windows-rs" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.")"/>
|
||||||
|
<ThirdPartyComponent Name="objc2" Developer="Steven Sheldon, Mads Marquart, silvanshade, Dzmitry Malyshau, Felix Nemo Kaaman, adamnemecek, Samuel Sleight, Paul Mabileau & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/madsmtm/objc2/blob/main/LICENSE-MIT.txt" RepositoryUrl="https://github.com/madsmtm/objc2" UseCase="@T("The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.")"/>
|
||||||
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library identifies files by their content. It is used for document streaming and as the first safety and media classification step before local audio processing.")"/>
|
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library identifies files by their content. It is used for document streaming and as the first safety and media classification step before local audio processing.")"/>
|
||||||
<ThirdPartyComponent Name="Symphonia" Developer="Philip Deljanov & Open Source Community" LicenseName="MPL-2.0" LicenseUrl="https://github.com/pdeljanov/Symphonia/blob/v0.6.0/LICENSE" RepositoryUrl="https://github.com/pdeljanov/Symphonia" UseCase="@T("Symphonia is used for media container demuxing and audio decoding. The exact MPL-covered source is available from the repository linked and is identified in the offline notices bundled with AI Studio.")"/>
|
<ThirdPartyComponent Name="Symphonia" Developer="Philip Deljanov & Open Source Community" LicenseName="MPL-2.0" LicenseUrl="https://github.com/pdeljanov/Symphonia/blob/v0.6.0/LICENSE" RepositoryUrl="https://github.com/pdeljanov/Symphonia" UseCase="@T("Symphonia is used for media container demuxing and audio decoding. The exact MPL-covered source is available from the repository linked and is identified in the offline notices bundled with AI Studio.")"/>
|
||||||
<ThirdPartyComponent Name="Ropus" Developer="0x4D44, Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon & Open Source Community" LicenseName="BSD-3-Clause" LicenseUrl="https://github.com/0x4D44/ropus/blob/main/LICENSE" RepositoryUrl="https://github.com/0x4d44/ropus" UseCase="@T("Ropus provides the Opus encoder and decoder used by the media pipeline.")"/>
|
<ThirdPartyComponent Name="Ropus" Developer="0x4D44, Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon & Open Source Community" LicenseName="BSD-3-Clause" LicenseUrl="https://github.com/0x4D44/ropus/blob/main/LICENSE" RepositoryUrl="https://github.com/0x4d44/ropus" UseCase="@T("Ropus provides the Opus encoder and decoder used by the media pipeline.")"/>
|
||||||
|
|||||||
@ -107,10 +107,16 @@ public partial class Information : MSGComponentBase
|
|||||||
private bool showVectorStoreDetails;
|
private bool showVectorStoreDetails;
|
||||||
private bool showExternalHttpCustomRootCertificateDetails;
|
private bool showExternalHttpCustomRootCertificateDetails;
|
||||||
|
|
||||||
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
|
private List<IAvailablePlugin> configPlugins = [];
|
||||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
|
||||||
.OfType<IAvailablePlugin>()
|
/// <summary>
|
||||||
.ToList();
|
/// The configuration plugins an administrator staged for a test.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// They are kept apart from the other configuration plugins: nobody deployed them, yet they act
|
||||||
|
/// on behalf of the organization while they are loaded. That deserves its own note.
|
||||||
|
/// </remarks>
|
||||||
|
private List<IAvailablePlugin> testConfigPlugins = [];
|
||||||
|
|
||||||
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
||||||
|
|
||||||
@ -201,11 +207,14 @@ public partial class Information : MSGComponentBase
|
|||||||
|
|
||||||
private void RefreshEnterpriseConfigurationState()
|
private void RefreshEnterpriseConfigurationState()
|
||||||
{
|
{
|
||||||
this.configPlugins = PluginFactory.AvailablePlugins
|
var availableConfigPlugins = PluginFactory.AvailablePlugins
|
||||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||||
.OfType<IAvailablePlugin>()
|
.OfType<IAvailablePlugin>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
this.testConfigPlugins = availableConfigPlugins.Where(plugin => PluginFactory.IsEnterpriseTestConfigurationPath(plugin.LocalPath)).ToList();
|
||||||
|
this.configPlugins = availableConfigPlugins.Except(this.testConfigPlugins).ToList();
|
||||||
|
|
||||||
this.enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
this.enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
|
||||||
this.mandatoryInfoPanels = PluginFactory.GetMandatoryInfos()
|
this.mandatoryInfoPanels = PluginFactory.GetMandatoryInfos()
|
||||||
.Select(info =>
|
.Select(info =>
|
||||||
@ -404,6 +413,27 @@ public partial class Information : MSGComponentBase
|
|||||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collects what a user needs to find and judge a staged test configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// There is no enterprise environment behind it, so we show what identifies it instead: the plugin
|
||||||
|
/// ID it claims and the directory it was staged in.
|
||||||
|
/// </remarks>
|
||||||
|
private IReadOnlyList<ConfigInfoRowItem> BuildTestConfigurationItems(IAvailablePlugin plugin) =>
|
||||||
|
[
|
||||||
|
new(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Configuration plugin ID:")} {plugin.Id}",
|
||||||
|
plugin.Id.ToString(),
|
||||||
|
T("Copies the configuration plugin ID to the clipboard")),
|
||||||
|
|
||||||
|
new(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Plugin directory:")} {plugin.LocalPath}",
|
||||||
|
plugin.LocalPath,
|
||||||
|
T("Copies the plugin directory to the clipboard"),
|
||||||
|
"margin-top: 4px;"),
|
||||||
|
];
|
||||||
|
|
||||||
private string ExternalHttpCustomRootCertificateWarningText
|
private string ExternalHttpCustomRootCertificateWarningText
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@ -5,13 +5,26 @@
|
|||||||
@attribute [Route(Routes.PLUGINS)]
|
@attribute [Route(Routes.PLUGINS)]
|
||||||
|
|
||||||
<div class="inner-scrolling-context">
|
<div class="inner-scrolling-context">
|
||||||
<MudText Typo="Typo.h3" Class="mb-2">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
||||||
|
<MudText Typo="Typo.h3">
|
||||||
@T("Plugins")
|
@T("Plugins")
|
||||||
</MudText>
|
</MudText>
|
||||||
|
<MudSpacer />
|
||||||
|
<LockableButton Text="@T("Import")"
|
||||||
|
Tooltip="@T("Import plugin from a file")"
|
||||||
|
Icon="@IMPORT_ICON"
|
||||||
|
ButtonVariant="Variant.Outlined"
|
||||||
|
ButtonColor="Color.Default"
|
||||||
|
NoMargin="@true"
|
||||||
|
Class="flex-none"
|
||||||
|
Disabled="@(() => this.isImportingAssistantPlugin)"
|
||||||
|
IsLocked="@(() => !this.AllowPluginImport)"
|
||||||
|
OnClickAsync="@this.ImportAssistantPluginAsync"/>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
<InnerScrolling>
|
<InnerScrolling>
|
||||||
|
|
||||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="border-dashed border rounded-lg">
|
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="@this.PluginTableClass">
|
||||||
<ColGroup>
|
<ColGroup>
|
||||||
<col style="width: 2em;" />
|
<col style="width: 2em;" />
|
||||||
<col style="width: 2.1em; "/>
|
<col style="width: 2.1em; "/>
|
||||||
@ -65,12 +78,13 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center">
|
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd">
|
||||||
@if (context.Type is PluginType.ASSISTANT)
|
@if (context.Type is PluginType.ASSISTANT)
|
||||||
{
|
{
|
||||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == context.Id);
|
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == context.Id);
|
||||||
<AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true"/>
|
<AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
|
@if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
|
||||||
{
|
{
|
||||||
var isEnabled = this.SettingsManager.IsPluginEnabled(context);
|
var isEnabled = this.SettingsManager.IsPluginEnabled(context);
|
||||||
@ -80,7 +94,7 @@
|
|||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|
||||||
<MudButtonGroup Class="ms-3">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0" Class="ms-3" Style="gap: 4px;">
|
||||||
@if (context is { IsInternal: false } && !string.IsNullOrWhiteSpace(context.SourceURL))
|
@if (context is { IsInternal: false } && !string.IsNullOrWhiteSpace(context.SourceURL))
|
||||||
{
|
{
|
||||||
var sourceUrl = context.SourceURL;
|
var sourceUrl = context.SourceURL;
|
||||||
@ -108,6 +122,13 @@
|
|||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (context is IAvailablePlugin shareablePlugin && CanSharePlugin(shareablePlugin))
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@(this.AllowPluginSharing ? this.SharePluginTooltip : this.SharePluginLockText)">
|
||||||
|
<MudIconButton Icon="@SharePluginIcon" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(this.isSharingPlugin || !this.AllowPluginSharing)"/>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
|
||||||
@if (context is IAvailablePlugin revisionPlugin && CanReviseAssistantPlugin(revisionPlugin))
|
@if (context is IAvailablePlugin revisionPlugin && CanReviseAssistantPlugin(revisionPlugin))
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@T("Revise assistant plugin with AI")">
|
<MudTooltip Text="@T("Revise assistant plugin with AI")">
|
||||||
@ -117,9 +138,9 @@
|
|||||||
|
|
||||||
@if (context is IAvailablePlugin availablePlugin)
|
@if (context is IAvailablePlugin availablePlugin)
|
||||||
{
|
{
|
||||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
<PluginDeleteAction Plugin="@availablePlugin" />
|
||||||
}
|
}
|
||||||
</MudButtonGroup>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
|
|||||||
@ -4,7 +4,8 @@ using AIStudio.Dialogs;
|
|||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.PluginSystem.Assistants;
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ public partial class Plugins : MSGComponentBase
|
|||||||
private const string GROUP_DISABLED = "Disabled";
|
private const string GROUP_DISABLED = "Disabled";
|
||||||
private const string GROUP_INTERNAL = "Internal";
|
private const string GROUP_INTERNAL = "Internal";
|
||||||
private bool isAutoAuditing;
|
private bool isAutoAuditing;
|
||||||
|
private bool isImportingAssistantPlugin;
|
||||||
|
|
||||||
private DataAssistantPluginAudit AssistantPluginAuditSettings => this.SettingsManager.ConfigurationData.AssistantPluginAudit;
|
private DataAssistantPluginAudit AssistantPluginAuditSettings => this.SettingsManager.ConfigurationData.AssistantPluginAudit;
|
||||||
|
|
||||||
@ -27,13 +29,42 @@ public partial class Plugins : MSGComponentBase
|
|||||||
[Inject]
|
[Inject]
|
||||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private PluginShareService PluginShareService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private PluginInstallService PluginInstallService { get; init; } = null!;
|
||||||
|
|
||||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(Plugins));
|
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(Plugins));
|
||||||
|
|
||||||
|
private bool isSharingPlugin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of active drop areas above this page. While there is any, another component owns the
|
||||||
|
/// dropped files and this page must not catch them.
|
||||||
|
/// </summary>
|
||||||
|
private uint numDropAreasAboveThis;
|
||||||
|
|
||||||
|
private bool isDraggingOverPage;
|
||||||
|
|
||||||
|
private const string IMPORT_ICON =
|
||||||
|
@"<svg class=""mud-icon-root mud-svg-icon mud-dark-text mud-icon-size-medium"" focusable=""false"" viewBox=""0 0 24 24"" aria-hidden=""true"" role=""img"">
|
||||||
|
<path d=""M0 0h24v24H0V0z"" fill=""none""></path>
|
||||||
|
<path d=""M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4z"" transform=""rotate(180 12 10)""></path>
|
||||||
|
<path d=""M20 10v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z""></path>
|
||||||
|
</svg>";
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED ]);
|
this.ApplyFilters([], [ Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED, Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||||
|
|
||||||
|
// Register the whole page as a drop area, so users can drop a plugin archive anywhere on it:
|
||||||
|
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||||
|
|
||||||
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
||||||
{
|
{
|
||||||
@ -59,6 +90,13 @@ public partial class Plugins : MSGComponentBase
|
|||||||
await this.TryAutoAuditAssistantsAsync();
|
await this.TryAutoAuditAssistantsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void DisposeResources()
|
||||||
|
{
|
||||||
|
// Release the drop area again, so lower layers can catch dropped files:
|
||||||
|
_ = this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||||
|
base.DisposeResources();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||||
@ -184,16 +222,59 @@ public partial class Plugins : MSGComponentBase
|
|||||||
: this.T("Enable plugin");
|
: this.T("Enable plugin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// These methods decide whether an action exists for a plugin at all. They must not depend on
|
||||||
|
// transient state like an ongoing share: they gate the markup, so a transient value would make
|
||||||
|
// the action buttons disappear and reappear. Transient state belongs into the buttons' Disabled.
|
||||||
|
//
|
||||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||||
|
|
||||||
private static bool CanReviseAssistantPlugin(IAvailablePlugin plugin)
|
private static bool CanReviseAssistantPlugin(IAvailablePlugin plugin)
|
||||||
{
|
{
|
||||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == plugin.Id);
|
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == plugin.Id);
|
||||||
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } &&
|
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath) && assistantPlugin?.IsManagedByConfigServer is false;
|
||||||
!string.IsNullOrWhiteSpace(plugin.LocalPath) &&
|
|
||||||
assistantPlugin?.IsManagedByConfigServer is false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The plugin types users may share. This list has to match what the import accepts, otherwise
|
||||||
|
/// users would create archives nobody can install.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly PluginType[] SHAREABLE_PLUGIN_TYPES = [PluginType.ASSISTANT, PluginType.CONFIGURATION, PluginType.LANGUAGE];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin may be shared or exported as an archive. Plugins shipped with
|
||||||
|
/// AI Studio and plugins deployed by an organization stay with their owner.
|
||||||
|
/// </summary>
|
||||||
|
private static bool CanSharePlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false } && SHAREABLE_PLUGIN_TYPES.Contains(plugin.Type) && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Highlights the plugin table while the user drags a file over the page, so it is visible
|
||||||
|
/// where the file would land.
|
||||||
|
/// </summary>
|
||||||
|
private string PluginTableClass => this.isDraggingOverPage
|
||||||
|
? "border-dashed border rounded-lg mud-border-primary border-4"
|
||||||
|
: "border-dashed border rounded-lg";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Organizations may disable importing plugin archives by using a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
private bool AllowPluginImport => this.SettingsManager.ConfigurationData.App.AllowUserToImportPlugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Organizations may disable sharing and exporting plugins by using a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
private bool AllowPluginSharing => this.SettingsManager.ConfigurationData.App.AllowUserToSharePlugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Linux has no native share sheet, hence the plugin archive is exported to a location of the
|
||||||
|
/// user's choice there. The action must be labeled accordingly.
|
||||||
|
/// </summary>
|
||||||
|
private static string SharePluginIcon => OperatingSystem.IsLinux() ? Icons.Material.Filled.FileDownload : Icons.Material.Filled.IosShare;
|
||||||
|
|
||||||
|
private string SharePluginTooltip => OperatingSystem.IsLinux() ? this.T("Export plugin archive") : this.T("Share plugin archive");
|
||||||
|
|
||||||
|
private string SharePluginLockText => OperatingSystem.IsLinux() ? this.T("Your organization has disabled exporting plugins.") : this.T("Your organization has disabled sharing plugins.");
|
||||||
|
|
||||||
private async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
private async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
||||||
{
|
{
|
||||||
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
||||||
@ -209,7 +290,9 @@ public partial class Plugins : MSGComponentBase
|
|||||||
|
|
||||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Save, string.Format(this.T("The assistant plugin '{0}' has been successfully saved."), result.PluginName)));
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Save, string.Format(this.T("The assistant plugin '{0}' has been successfully saved."), result.PluginName)));
|
||||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully updated.");
|
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully updated.");
|
||||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
|
||||||
|
// Saving the plugin ran LoadAll, which already sent PLUGINS_RELOADED. Editing the plugin
|
||||||
|
// code changes no settings, so there is nothing else to announce:
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,11 +311,139 @@ public partial class Plugins : MSGComponentBase
|
|||||||
|
|
||||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.AutoFixHigh, string.Format(this.T("The assistant plugin '{0}' has been successfully revised."), result.PluginName)));
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.AutoFixHigh, string.Format(this.T("The assistant plugin '{0}' has been successfully revised."), result.PluginName)));
|
||||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully revised.");
|
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully revised.");
|
||||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
|
||||||
|
// Saving the revision ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
|
||||||
|
// configuration change: with automatic audits enabled, the dialog stored an audit result:
|
||||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SharePluginAsync(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
if (this.isSharingPlugin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isSharingPlugin = true;
|
||||||
|
// invoke a state change right away to guard action buttons
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shareResult = await this.PluginShareService.ShareAsync(plugin, CancellationToken.None);
|
||||||
|
if (shareResult.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!shareResult.Success)
|
||||||
|
{
|
||||||
|
LOG.LogError($"Sharing the plugin '{shareResult.PluginName}' from archive '{shareResult.ArchivePath}' failed with Issue: '{shareResult.Issue}'.");
|
||||||
|
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, OperatingSystem.IsLinux() ? T("An error occurred while exporting the plugin.") : T("An error occurred while sharing the plugin.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Linux, the user chose the target location, so we confirm where the archive was stored:
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileDownload, string.Format(T("The plugin archive was exported to '{0}'."), shareResult.ArchivePath)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isSharingPlugin = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportAssistantPluginAsync()
|
||||||
|
{
|
||||||
|
if (this.isImportingAssistantPlugin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.AllowPluginImport)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var selection = await this.RustService.SelectFile(this.T("Import plugin"), [FileTypes.PLUGIN_ARCHIVE]);
|
||||||
|
if (selection.UserCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.ImportPluginArchiveAsync(selection.SelectedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs a plugin archive, no matter whether the user picked it through the import button or
|
||||||
|
/// dropped it onto the page.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archivePath">The local plugin archive to install.</param>
|
||||||
|
private async Task ImportPluginArchiveAsync(string archivePath)
|
||||||
|
{
|
||||||
|
if (this.isImportingAssistantPlugin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.AllowPluginImport)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isImportingAssistantPlugin = true;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await this.PluginInstallService.InstallArchiveAsync(archivePath, this.ConfirmPluginImportAsync, CancellationToken.None);
|
||||||
|
if (result.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
LOG.LogError("Failed to import assistant plugin archive '{ArchivePath}': {Issue}", archivePath, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = result.ReplacedExisting
|
||||||
|
? this.T("Plugin updated.")
|
||||||
|
: this.T("Plugin installed.");
|
||||||
|
|
||||||
|
// We do not announce the reload ourselves: a successful installation ran LoadAll, which
|
||||||
|
// already sent PLUGINS_RELOADED. The import changes no settings either, so there is
|
||||||
|
// nothing to report as a configuration change:
|
||||||
|
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Extension, message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isImportingAssistantPlugin = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 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 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);
|
private PluginAssistants? TryGetAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == pluginId);
|
||||||
@ -302,8 +513,71 @@ public partial class Plugins : MSGComponentBase
|
|||||||
case Event.CONFIGURATION_CHANGED:
|
case Event.CONFIGURATION_CHANGED:
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||||
|
if (data is int registeredLayer && registeredLayer > DropLayers.PAGES)
|
||||||
|
this.numDropAreasAboveThis++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||||
|
if (data is int unregisteredLayer && unregisteredLayer > DropLayers.PAGES && this.numDropAreasAboveThis > 0)
|
||||||
|
this.numDropAreasAboveThis--;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||||
|
if (!this.CanCatchDroppedFile())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isDraggingOverPage = true;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||||
|
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||||
|
this.isDraggingOverPage = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var droppedPaths }:
|
||||||
|
this.isDraggingOverPage = false;
|
||||||
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
|
if (!this.CanCatchDroppedFile())
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.ImportDroppedPluginArchiveAsync(droppedPaths);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decides whether this page may process dropped files: only when no drop area above it is
|
||||||
|
/// active and when the organization allows importing plugins at all.
|
||||||
|
/// </summary>
|
||||||
|
private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && this.AllowPluginImport && !this.isImportingAssistantPlugin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports a plugin archive the user dropped onto the page. Anything that is not exactly one
|
||||||
|
/// plugin archive is reported instead of guessing what the user meant.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="droppedPaths">The paths of the dropped files.</param>
|
||||||
|
private async Task ImportDroppedPluginArchiveAsync(IReadOnlyList<string> droppedPaths)
|
||||||
|
{
|
||||||
|
var archivePaths = droppedPaths.Where(path => FileTypes.IsAllowedPath(path, FileTypes.PLUGIN_ARCHIVE)).ToList();
|
||||||
|
switch (archivePaths.Count)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, string.Format(this.T("Please drop a plugin archive with the extension {0} or .zip."), PluginArchive.PLUGIN_FILE_EXTENSION)));
|
||||||
|
return;
|
||||||
|
|
||||||
|
case > 1:
|
||||||
|
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, this.T("Please drop only one plugin archive at a time.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ImportPluginArchiveAsync(archivePaths[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -27,6 +27,26 @@ TYPE = "CONFIGURATION"
|
|||||||
-- True when this plugin is deployed by an enterprise configuration server:
|
-- True when this plugin is deployed by an enterprise configuration server:
|
||||||
DEPLOYED_USING_CONFIG_SERVER = false
|
DEPLOYED_USING_CONFIG_SERVER = false
|
||||||
|
|
||||||
|
-- The priority of this configuration plugin. Optional, defaults to 0.
|
||||||
|
--
|
||||||
|
-- It only matters when your organization deploys more than one configuration
|
||||||
|
-- plugin. A plugin with a higher priority is applied later and therefore wins
|
||||||
|
-- whenever two of your configuration plugins manage the same setting or define
|
||||||
|
-- the same object, e.g. the same LLM provider.
|
||||||
|
--
|
||||||
|
-- A typical setup: deploy one base configuration for everybody with PRIORITY = 0
|
||||||
|
-- and one configuration per department with PRIORITY = 100. The department
|
||||||
|
-- configuration may then override the default model, while everything it does
|
||||||
|
-- not mention stays at the values of the base configuration.
|
||||||
|
--
|
||||||
|
-- Give two plugins that must override each other different priorities. With an
|
||||||
|
-- equal priority, the order is stable but arbitrary.
|
||||||
|
--
|
||||||
|
-- The priority never lifts a local configuration plugin above one of your
|
||||||
|
-- organization: configuration plugins your IT department deployed are always
|
||||||
|
-- applied first, whatever a local plugin declares.
|
||||||
|
PRIORITY = 0
|
||||||
|
|
||||||
-- The authors of the plugin:
|
-- The authors of the plugin:
|
||||||
AUTHORS = {"<Company Name>"}
|
AUTHORS = {"<Company Name>"}
|
||||||
|
|
||||||
@ -199,6 +219,40 @@ CONFIG["DATA_SOURCES"] = {}
|
|||||||
|
|
||||||
CONFIG["SETTINGS"] = {}
|
CONFIG["SETTINGS"] = {}
|
||||||
|
|
||||||
|
-- ------
|
||||||
|
-- How settings combine when your organization deploys more than one configuration
|
||||||
|
-- ------
|
||||||
|
--
|
||||||
|
-- A configuration with a higher PRIORITY is applied later and wins. This works per
|
||||||
|
-- setting: everything a later configuration does not mention keeps the value of the
|
||||||
|
-- configuration below it.
|
||||||
|
--
|
||||||
|
-- For a setting that holds a list or a table, the winning configuration replaces the
|
||||||
|
-- whole collection instead of merging the entries. A department configuration that
|
||||||
|
-- lists a single entry therefore drops every entry the base configuration had set for
|
||||||
|
-- that setting. That is intentional: replacing is the only way a department can take
|
||||||
|
-- something back that the base configuration has set.
|
||||||
|
--
|
||||||
|
-- The affected settings below carry a note. Two settings are the exception and add up
|
||||||
|
-- across configurations instead: DataApp.EnabledPreviewFeatures and
|
||||||
|
-- DataAssistantPluginAudit.EnterpriseApprovedPlugins.
|
||||||
|
-- ------
|
||||||
|
|
||||||
|
-- ------
|
||||||
|
-- What happens to a setting when your configuration is removed
|
||||||
|
-- ------
|
||||||
|
--
|
||||||
|
-- AI Studio remembers the value a setting had before a configuration took it over.
|
||||||
|
-- Once no configuration manages that setting anymore -- because your IT department
|
||||||
|
-- stopped deploying this configuration, because the user deleted it, or because a test
|
||||||
|
-- configuration ended -- the user gets that value back. When there is nothing to
|
||||||
|
-- restore, e.g. for a setting the user had never changed, AI Studio falls back to its
|
||||||
|
-- own default value.
|
||||||
|
--
|
||||||
|
-- One case differs: when you allow users to override a setting and somebody makes use
|
||||||
|
-- of that, their choice outlives your configuration and stays as it is.
|
||||||
|
-- ------
|
||||||
|
|
||||||
-- Configure the update check interval:
|
-- Configure the update check interval:
|
||||||
-- Allowed values are: NO_CHECK, DISABLE_UPDATES, ONCE_STARTUP, HOURLY, DAILY, WEEKLY
|
-- Allowed values are: NO_CHECK, DISABLE_UPDATES, ONCE_STARTUP, HOURLY, DAILY, WEEKLY
|
||||||
-- NO_CHECK disables automatic checks, but users can still check and install updates manually.
|
-- NO_CHECK disables automatic checks, but users can still check and install updates manually.
|
||||||
@ -235,6 +289,22 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Configure the user permission to add providers:
|
-- Configure the user permission to add providers:
|
||||||
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
|
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
|
||||||
|
|
||||||
|
-- Configure the user permission to import plugin archives from disk.
|
||||||
|
-- When set to false, the import button on the plugins page stays visible but is disabled.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportPlugins"] = false
|
||||||
|
|
||||||
|
-- Configure the user permission to import configuration plugin archives from disk.
|
||||||
|
-- This is a second gate on top of DataApp.AllowUserToImportPlugins: both must allow the
|
||||||
|
-- import. Configuration plugins get their own switch because they can do far more than an
|
||||||
|
-- assistant: they define LLM providers and data sources, and they lock settings. You may
|
||||||
|
-- therefore let users import assistants while keeping configurations to your IT department.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportConfigurationPlugins"] = false
|
||||||
|
|
||||||
|
-- Configure the user permission to share or export plugins as archives.
|
||||||
|
-- When set to false, the share button on the plugins page stays visible but is disabled.
|
||||||
|
-- On Linux, this button exports the plugin archive instead of using a native share sheet.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.AllowUserToSharePlugins"] = false
|
||||||
|
|
||||||
-- Configure whether administration settings are visible in the UI:
|
-- Configure whether administration settings are visible in the UI:
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
|
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
|
||||||
|
|
||||||
@ -248,6 +318,12 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Configure the enabled preview features:
|
-- Configure the enabled preview features:
|
||||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||||
-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
|
-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
|
||||||
|
--
|
||||||
|
-- Adds up, does not replace: this is the one setting where all configurations
|
||||||
|
-- contribute together. Enable one preview feature for the whole organization and
|
||||||
|
-- another one for a single department, and users of that department get both. Each
|
||||||
|
-- configuration keeps its own contribution, so removing one of them only withdraws
|
||||||
|
-- the features that this configuration had enabled.
|
||||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
|
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
|
||||||
|
|
||||||
-- Configure the preselected provider.
|
-- Configure the preselected provider.
|
||||||
@ -292,6 +368,12 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourcesAutomaticValidation"] = true
|
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourcesAutomaticValidation"] = true
|
||||||
|
|
||||||
-- Must contain IDs from CONFIG["DATA_SOURCES"] or user-configured data sources.
|
-- Must contain IDs from CONFIG["DATA_SOURCES"] or user-configured data sources.
|
||||||
|
-- IDs from another configuration of your organization work as well: they are resolved
|
||||||
|
-- against every known data source, not only against the ones defined here. IDs that
|
||||||
|
-- resolve to nothing are ignored.
|
||||||
|
--
|
||||||
|
-- Replaces, does not merge: a configuration with a higher priority replaces this list
|
||||||
|
-- completely. To keep an entry of the base configuration, list that ID here again.
|
||||||
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourceIds"] = {
|
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourceIds"] = {
|
||||||
-- "00000000-0000-0000-0000-000000000000",
|
-- "00000000-0000-0000-0000-000000000000",
|
||||||
-- }
|
-- }
|
||||||
@ -327,6 +409,11 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT,
|
-- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT,
|
||||||
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, VISUAL_BRIEFING_ASSISTANT, I18N_ASSISTANT,
|
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, VISUAL_BRIEFING_ASSISTANT, I18N_ASSISTANT,
|
||||||
-- LOG_VIEWER_ASSISTANT
|
-- LOG_VIEWER_ASSISTANT
|
||||||
|
--
|
||||||
|
-- Replaces, does not merge: a configuration with a higher priority replaces this list
|
||||||
|
-- completely. This is what lets a department show an assistant again that the base
|
||||||
|
-- configuration hides. The department configuration must then list every other
|
||||||
|
-- assistant that is supposed to stay hidden, otherwise those become visible too.
|
||||||
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
|
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
|
||||||
|
|
||||||
-- Configure organization defaults for the Visual Briefing Assistant.
|
-- Configure organization defaults for the Visual Briefing Assistant.
|
||||||
@ -395,6 +482,17 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- no user-run security audit is required.
|
-- no user-run security audit is required.
|
||||||
-- You can generate the exact hash with the build-script command:
|
-- You can generate the exact hash with the build-script command:
|
||||||
-- dotnet run --project app/Build -- assistant-plugin-hash "<plugin-dir>" --lua-snippet
|
-- dotnet run --project app/Build -- assistant-plugin-hash "<plugin-dir>" --lua-snippet
|
||||||
|
--
|
||||||
|
-- Only works in configurations your configuration server deploys. An approval marks an
|
||||||
|
-- assistant plugin as safe without any audit, and AI Studio then tells users that their
|
||||||
|
-- organization approved it. A configuration plugin that a user placed locally therefore
|
||||||
|
-- cannot approve anything: AI Studio ignores its approvals and writes a warning to the
|
||||||
|
-- log. This is decided by where the plugin is stored, not by DEPLOYED_USING_CONFIG_SERVER.
|
||||||
|
--
|
||||||
|
-- Adds up, does not replace: approvals of all your configurations are combined, so a
|
||||||
|
-- department configuration can approve additional assistant plugins without repeating
|
||||||
|
-- the approvals of the base configuration. Each configuration keeps its own approvals,
|
||||||
|
-- so removing one of them only withdraws the approvals it had granted.
|
||||||
-- CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = {
|
-- CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = {
|
||||||
-- {
|
-- {
|
||||||
-- ["PluginHash"] = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
-- ["PluginHash"] = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||||
@ -434,6 +532,11 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
|
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH=/path/in/sandbox/company-root-cas.pem
|
||||||
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;data.example.org
|
-- MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS=*.intra.example.org;data.example.org
|
||||||
--
|
--
|
||||||
|
-- Replaces, does not merge: a configuration with a higher priority replaces the host
|
||||||
|
-- list completely. Deploy this setting in one configuration only, or repeat every host
|
||||||
|
-- of the base configuration. Otherwise, hosts of the base configuration silently stop
|
||||||
|
-- trusting your root certificates.
|
||||||
|
--
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
|
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificatesEnabled"] = true
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
|
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
|
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
|
||||||
@ -468,6 +571,11 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
|
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
|
||||||
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
|
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
|
||||||
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||||
|
--
|
||||||
|
-- Replaces, does not merge: a configuration with a higher priority replaces the whole
|
||||||
|
-- table. Every configuration that sets this must therefore list all providers it wants
|
||||||
|
-- to cover. A partial table is not completed from the configuration below it, and the
|
||||||
|
-- providers left out fall back to the app default.
|
||||||
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme"] = {
|
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme"] = {
|
||||||
-- ["OPEN_AI"] = "MODERATE",
|
-- ["OPEN_AI"] = "MODERATE",
|
||||||
-- ["ANTHROPIC"] = "MODERATE",
|
-- ["ANTHROPIC"] = "MODERATE",
|
||||||
@ -493,6 +601,10 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- These IDs may refer to LLM providers, embedding providers, or transcription providers
|
-- These IDs may refer to LLM providers, embedding providers, or transcription providers
|
||||||
-- defined in this configuration. Trusted providers are treated like self-hosted providers
|
-- defined in this configuration. Trusted providers are treated like self-hosted providers
|
||||||
-- only for data-source security checks and related local data warnings.
|
-- only for data-source security checks and related local data warnings.
|
||||||
|
--
|
||||||
|
-- Replaces, does not merge: a configuration with a higher priority replaces this list
|
||||||
|
-- completely, so providers trusted by the base configuration lose that status. Repeat
|
||||||
|
-- them here to keep them trusted.
|
||||||
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = {
|
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = {
|
||||||
-- "00000000-0000-0000-0000-000000000000",
|
-- "00000000-0000-0000-0000-000000000000",
|
||||||
-- "00000000-0000-0000-0000-000000000001",
|
-- "00000000-0000-0000-0000-000000000001",
|
||||||
|
|||||||
@ -2679,6 +2679,126 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRE
|
|||||||
-- Build progress
|
-- Build progress
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Erstellungsfortschritt"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Erstellungsfortschritt"
|
||||||
|
|
||||||
|
-- The model did not fill every planned content slot exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1003911239"] = "Das Modell hat nicht jeden vorgesehenen Inhaltsplatz genau einmal ausgefüllt. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The sources of this briefing could not be prepared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1034452233"] = "Die Quellen für dieses Briefing konnten nicht aufbereitet werden."
|
||||||
|
|
||||||
|
-- This operation did not change the briefing, so no new version was created.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1058618049"] = "Durch diesen Vorgang wurde das Briefing nicht geändert, daher wurde keine neue Version erstellt."
|
||||||
|
|
||||||
|
-- The model filled a content slot with the wrong kind of value. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1099589813"] = "Das Modell hat einen Platzhalter für den Inhalt mit einem Wert des falschen Typs ausgefüllt. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1198458597"] = "Die Modellantwort enthielt eine leere, fehlerhafte oder doppelte Kennung. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model did not cover every source of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1209705994"] = "Das Modell hat nicht jede Quelle dieses Briefings genau einmal berücksichtigt. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell."
|
||||||
|
|
||||||
|
-- An accessibility text of the model response was empty or invalid. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1437512295"] = "Ein Barrierefreiheitstext der Modellantwort war leer oder ungültig. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model response used a prohibited attribute. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1677678770"] = "Die Modellantwort verwendete ein unzulässiges Attribut. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- A chart of the model response contained invalid categories or data series. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T181588270"] = "Ein Diagramm der Modellantwort enthielt ungültige Kategorien oder Datenreihen. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- A source of this briefing can no longer be reached. Please relink or remove the affected source.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1878061605"] = "Eine Quelle dieses Briefings ist nicht mehr erreichbar. Bitte verknüpfen Sie die betroffene Quelle erneut oder entfernen Sie sie."
|
||||||
|
|
||||||
|
-- The selected provider could not complete this briefing stage.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1905087799"] = "Der ausgewählte Anbieter konnte diese Briefing-Phase nicht abschließen."
|
||||||
|
|
||||||
|
-- A calculation of the model response used an invalid operation. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1992964953"] = "Bei der Berechnung der Modellantwort wurde eine ungültige Operation verwendet. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model response did not match the required contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T214297315"] = "Die Modellantwort entsprach nicht dem erforderlichen Vertrag. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model response contained unexpected fields. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2192261405"] = "Die Antwort des Modells enthielt unerwartete Felder. Bitte versuche es erneut oder wähle ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- AI Studio was closed while this briefing was being built. You can resume the build.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2197645770"] = "AI Studio wurde geschlossen, während dieses Briefing erstellt wurde. Du kannst die Erstellung fortsetzen."
|
||||||
|
|
||||||
|
-- The presentation of the model response did not match the briefing contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2376983148"] = "Die Darstellung der Modellantwort entsprach nicht den Vorgaben des Briefings. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- This visual briefing operation was canceled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T240791538"] = "Dieser Vorgang für das visuelle Briefing wurde abgebrochen."
|
||||||
|
|
||||||
|
-- The model response contained markup or code, which this briefing does not allow. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2529598303"] = "Die Modellantwort enthielt Markup oder Code, was in diesem Briefing nicht zulässig ist. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2668127220"] = "AI Studio hat aus diesem Briefing ein widersprüchliches Ergebnis erstellt. Bitte kopieren Sie die technischen Details und melden Sie dieses Problem."
|
||||||
|
|
||||||
|
-- This briefing could not be assembled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2678882954"] = "Dieses Briefing konnte nicht erstellt werden."
|
||||||
|
|
||||||
|
-- An interactive control of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2714042531"] = "Ein interaktives Steuerelement für die Modellantwort verwies auf ein ungültiges Briefing-Element. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model did not return valid JSON. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2784808603"] = "Das Modell hat kein gültiges JSON zurückgegeben. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- A calculation of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2795934353"] = "Eine Berechnung der Modellantwort bezog sich auf ein ungültiges Briefing-Element. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid initial state. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2796279475"] = "Ein interaktives Steuerelement der Modellantwort wurde mit einem ungültigen Anfangszustand verwendet. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The accessibility texts of the model response did not match the briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2815870761"] = "Die Texte zur Barrierefreiheit der Modellantwort stimmten nicht mit den Briefing-Elementen überein. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The new version of this briefing could not be saved.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2818947691"] = "Die neue Version dieses Briefings konnte nicht gespeichert werden."
|
||||||
|
|
||||||
|
-- The model did not plan every visual asset of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2853629903"] = "Das Modell hat nicht jedes visuelle Element dieses Briefings genau einmal geplant. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The assembled briefing did not pass the security validation.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T295498807"] = "Die zusammengestellte Zusammenfassung hat die Sicherheitsprüfung nicht bestanden."
|
||||||
|
|
||||||
|
-- The charts of the model response did not match the planned briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3326200304"] = "Die Diagramme der Modellantwort entsprachen nicht den geplanten Briefing-Elementen. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3412185985"] = "Ein interaktives Steuerelement in der Modellantwort verwendete eine ungültige Kennung. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The model response referenced content that does not exist. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T344215744"] = "Die Modellantwort bezog sich auf Inhalte, die nicht vorhanden sind. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- The updated content no longer fits the current presentation. You can continue as a rebuild.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3515116214"] = "Die aktualisierten Inhalte passen nicht mehr zur aktuellen Präsentation. Sie können mit einer Neuerstellung fortfahren."
|
||||||
|
|
||||||
|
-- The model response contained a value of the wrong type. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3668896836"] = "Die Modellantwort enthielt einen Wert des falschen Typs. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- This briefing has no provider selected. Please select a provider before you generate a briefing.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3834145318"] = "Für dieses Briefing ist kein Anbieter ausgewählt. Bitte wählen Sie einen Anbieter aus, bevor Sie ein Briefing erstellen."
|
||||||
|
|
||||||
|
-- The selected model lacks a capability this briefing needs. Please select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T4066127340"] = "Dem ausgewählten Modell fehlt eine für dieses Briefing erforderliche Fähigkeit. Bitte wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- A media transcript of this briefing is missing or outdated. Please transcribe the affected media again.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T449544952"] = "Ein Medientranskript dieses Briefings fehlt oder ist veraltet. Bitte transkribieren Sie die betroffenen Medien erneut."
|
||||||
|
|
||||||
|
-- The model response used an invalid briefing layout. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T686008237"] = "Die Modellantwort verwendete ein ungültiges Briefing-Layout. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- A briefing element of the model response was missing its required interactive controls. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T762236598"] = "Ein Briefing-Element der Modellantwort enthielt nicht die erforderlichen interaktiven Bedienelemente. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
|
-- This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T875151112"] = "Dieser Vorgang für das visuelle Briefing ist aufgrund eines unerwarteten internen Fehlers fehlgeschlagen. Bitte kopieren Sie die technischen Details für den Support."
|
||||||
|
|
||||||
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "Die Modellantwort verwendet eine nicht unterstützte Vertragsversion. Bitte versuchen Sie es erneut oder wählen Sie ein anderes Modell aus."
|
||||||
|
|
||||||
-- This chart cannot be displayed: {0}
|
-- This chart cannot be displayed: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "Dieses Diagramm kann nicht angezeigt werden: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "Dieses Diagramm kann nicht angezeigt werden: {0}"
|
||||||
|
|
||||||
@ -2799,24 +2919,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assisten
|
|||||||
-- The result is ready.
|
-- The result is ready.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "Das Ergebnis ist fertig."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "Das Ergebnis ist fertig."
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1318944584"] = "Der Assistent kann nicht gelöscht werden, solange noch Hintergrundaufgaben ausgeführt werden."
|
|
||||||
|
|
||||||
-- Delete assistant plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1692493145"] = "Assistenten-Plugin löschen"
|
|
||||||
|
|
||||||
-- Delete Assistant Plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3637071001"] = "Assistenten-Plugin löschen"
|
|
||||||
|
|
||||||
-- The '{0}' assistant plugin has been successfully removed.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3815023384"] = "Das Assistenten-Plugin „{0}“ wurde erfolgreich entfernt."
|
|
||||||
|
|
||||||
-- The assistant plugin '{0}' could not be deleted: {1}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3985264168"] = "Das Assistenten-Plugin „{0}“ konnte nicht gelöscht werden: {1}"
|
|
||||||
|
|
||||||
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T4033722845"] = "Möchtest du das Assistenten-Plug-in „{0}“ wirklich löschen? Dadurch werden die lokalen Plug-in-Dateien dauerhaft gelöscht."
|
|
||||||
|
|
||||||
-- Show or hide the detailed security information.
|
-- Show or hide the detailed security information.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Detaillierte Sicherheitsinformationen anzeigen oder ausblenden."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Detaillierte Sicherheitsinformationen anzeigen oder ausblenden."
|
||||||
|
|
||||||
@ -3264,6 +3366,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Ko
|
|||||||
-- Cannot copy this content type to clipboard.
|
-- Cannot copy this content type to clipboard.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Dieser Inhaltstyp kann nicht in die Zwischenablage kopiert werden."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Dieser Inhaltstyp kann nicht in die Zwischenablage kopiert werden."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1318944584"] = "Der Assistent kann nicht gelöscht werden, solange noch Hintergrundaufgaben ausgeführt werden."
|
||||||
|
|
||||||
|
-- Delete assistant plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1692493145"] = "Assistenten-Plugin löschen"
|
||||||
|
|
||||||
|
-- Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1744561175"] = "Möchten Sie das Sprach-Plugin „{0}“ wirklich löschen? Dadurch werden die lokalen Plugin-Dateien dauerhaft gelöscht. Wenn dies Ihre ausgewählte Sprache ist, stellt AI Studio wieder auf die automatische Sprachauswahl um."
|
||||||
|
|
||||||
|
-- Delete language plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2707495447"] = "Sprach-Plugin löschen"
|
||||||
|
|
||||||
|
-- The plugin '{0}' could not be deleted: {1}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2738963920"] = "Das Plugin „{0}“ konnte nicht gelöscht werden: {1}"
|
||||||
|
|
||||||
|
-- Delete Language Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2990518039"] = "Sprach-Plugin löschen"
|
||||||
|
|
||||||
|
-- Delete Configuration Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3395354991"] = "Konfigurations-Plugin löschen"
|
||||||
|
|
||||||
|
-- The plugin '{0}' has been successfully removed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3476138264"] = "Das Plugin „{0}“ wurde erfolgreich entfernt."
|
||||||
|
|
||||||
|
-- Delete Assistant Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3637071001"] = "Assistenten-Plugin löschen"
|
||||||
|
|
||||||
|
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T4033722845"] = "Möchten Sie das Assistenten-Plugin „{0}“ wirklich löschen? Dadurch werden die lokalen Plugin-Dateien dauerhaft gelöscht."
|
||||||
|
|
||||||
|
-- Delete configuration plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T459830575"] = "Konfigurations-Plugin löschen"
|
||||||
|
|
||||||
-- Alpha phase means that we are working on the last details before the beta phase.
|
-- Alpha phase means that we are working on the last details before the beta phase.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha-Phase bedeutet, dass wir an den letzten Details arbeiten, bevor die Beta-Phase beginnt."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha-Phase bedeutet, dass wir an den letzten Details arbeiten, bevor die Beta-Phase beginnt."
|
||||||
|
|
||||||
@ -3876,6 +4011,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T40680
|
|||||||
-- Edit Embedding Provider
|
-- Edit Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Einbettungsanbieter bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Einbettungsanbieter bearbeiten"
|
||||||
|
|
||||||
|
-- This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T438107040"] = "Dieser selbstgehostete Embedding-Anbieter ist für Sicherheitsprüfungen von Datenquellen vertrauenswürdig. Lokale Daten können ohne Sicherheitswarnungen an ihn gesendet werden."
|
||||||
|
|
||||||
-- Configure Embedding Providers
|
-- Configure Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Anbieter für Einbettungen konfigurieren"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Anbieter für Einbettungen konfigurieren"
|
||||||
|
|
||||||
@ -3960,6 +4098,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T386503
|
|||||||
-- Delete LLM Provider
|
-- Delete LLM Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "LLM-Anbieter löschen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "LLM-Anbieter löschen"
|
||||||
|
|
||||||
|
-- This self-hosted provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "Dieser selbstgehostete Anbieter ist für Sicherheitsprüfungen von Datenquellen vertrauenswürdig."
|
||||||
|
|
||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen"
|
||||||
|
|
||||||
@ -3987,6 +4128,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T17
|
|||||||
-- Add Transcription Provider
|
-- Add Transcription Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Anbieter für Transkriptionen hinzufügen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Anbieter für Transkriptionen hinzufügen"
|
||||||
|
|
||||||
|
-- This self-hosted transcription provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2175189736"] = "Diesem selbstgehostete Transkriptionsanbieter wird für Sicherheitsprüfungen von Datenquellen vertraut."
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Modell"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Modell"
|
||||||
|
|
||||||
@ -4626,6 +4770,84 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Erlauben
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen"
|
||||||
|
|
||||||
|
-- {0} LLM providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM-Anbieter"
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1238255445"] = "{0} Profile"
|
||||||
|
|
||||||
|
-- No
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1642511898"] = "Nein"
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2107991661"] = "{0} Einführungen auf der Willkommensseite"
|
||||||
|
|
||||||
|
-- {0} mandatory information
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2150386772"] = "{0} Pflichtangabe"
|
||||||
|
|
||||||
|
-- You can install the plugin again later, but any changes you made to its settings are lost.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2156367745"] = "Du kannst das Plugin später erneut installieren, aber alle Änderungen an seinen Einstellungen gehen verloren."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2342765572"] = "{0} Profil"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2426110502"] = "{0} Einführung auf der Willkommensseite"
|
||||||
|
|
||||||
|
-- {0} embedding providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2438407498"] = "{0} Anbieter für Einbettungen"
|
||||||
|
|
||||||
|
-- Yes, delete it
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2466176832"] = "Ja, löschen"
|
||||||
|
|
||||||
|
-- This also removes everything the configuration plugin had set up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T264970454"] = "Dadurch wird auch alles entfernt, was das Konfigurations-Plugin eingerichtet hat:"
|
||||||
|
|
||||||
|
-- {0} transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2681055470"] = "{0} Anbieter für Transkriptionen"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3235448458"] = "{0} Chat-Vorlagen"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3278137746"] = "{0} Regelwerk der Dokumentenanalyse"
|
||||||
|
|
||||||
|
-- The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T330559934"] = "Das Konfigurations-Plugin wird nicht ausgeführt, daher können wir nicht feststellen, was es eingerichtet hat. Alles, was es konfiguriert hat, wird ebenfalls entfernt."
|
||||||
|
|
||||||
|
-- {0} LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3410030691"] = "{0} LLM-Anbieter"
|
||||||
|
|
||||||
|
-- Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3616855807"] = "Möchten Sie das Konfigurations-Plugin „{0}“ wirklich löschen? Dadurch werden seine lokalen Plugin-Dateien dauerhaft gelöscht."
|
||||||
|
|
||||||
|
-- {0} settings return to their default values
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3841220170"] = "{0} Einstellungen werden auf ihre Standardwerte zurückgesetzt."
|
||||||
|
|
||||||
|
-- {0} setting returns to its default value
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T384701293"] = "{0} Einstellung wird auf den Standardwert zurückgesetzt."
|
||||||
|
|
||||||
|
-- {0} mandatory informations
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3971735909"] = "{0} Pflichtangaben"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4147879421"] = "{0} Chat-Vorlage"
|
||||||
|
|
||||||
|
-- {0} data sources, including their credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4193757254"] = "{0} Datenquellen, einschließlich ihrer Zugangsdaten im Schlüsselbund Ihres Betriebssystems"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T449490978"] = "{0} Regelwerke der Dokumentenanalyse"
|
||||||
|
|
||||||
|
-- {0} data source, including its credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T511418335"] = "{0} Datenquelle einschließlich ihrer Zugangsdaten im Schlüsselbund Ihres Betriebssystems"
|
||||||
|
|
||||||
|
-- {0} transcription providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"] = "{0} Anbieter für Transkriptionen"
|
||||||
|
|
||||||
|
-- {0} embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} Anbieter für Einbettungen"
|
||||||
|
|
||||||
-- No
|
-- No
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "Nein"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "Nein"
|
||||||
|
|
||||||
@ -5244,6 +5466,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einb
|
|||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen"
|
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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -5334,6 +5559,117 @@ 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1238255445"] = "{0} Profile"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- Data source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2034620186"] = "Datenquelle"
|
||||||
|
|
||||||
|
-- A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2051328106"] = "Eine Konfiguration wird direkt nach der Installation wirksam und kann nicht ein- oder ausgeschaltet werden. Bitte prüfen Sie, was sie einrichtet:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2107991661"] = "{0} Einführungen auf der Willkommensseite"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2342765572"] = "{0} Profil"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2426110502"] = "{0} Einführung auf der Willkommensseite"
|
||||||
|
|
||||||
|
-- Support contact
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Supportkontakt"
|
||||||
|
|
||||||
|
-- Name
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- {0} setting it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2868009192"] = "{0} Einstellung, die es übernimmt"
|
||||||
|
|
||||||
|
-- {0} settings it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3190775003"] = "{0} Einstellungen, die es übernimmt"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3235448458"] = "{0} Chat-Vorlagen"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3278137746"] = "{0} Regelwerk für die Dokumentenanalyse"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- {0} mandatory information you have to accept before using AI Studio
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3540986519"] = "{0} Pflichtinformationen, die Sie vor der Nutzung von AI Studio akzeptieren müssen"
|
||||||
|
|
||||||
|
-- Transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3566003684"] = "Transkriptionsanbieter"
|
||||||
|
|
||||||
|
-- Replace plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Plugin ersetzen"
|
||||||
|
|
||||||
|
-- LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4099016901"] = "LLM-Anbieter"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4147879421"] = "{0} Chat-Vorlage"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T449490978"] = "{0} Regelwerke für die Dokumentanalyse"
|
||||||
|
|
||||||
|
-- The authors marked this plugin as deprecated: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "Die Autoren haben dieses Plugin als veraltet gekennzeichnet: {0}"
|
||||||
|
|
||||||
|
-- It also brings:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T713968030"] = "Außerdem bietet es:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- Embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T877326195"] = "Anbieter für Einbettungen"
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Abbrechen"
|
||||||
|
|
||||||
|
-- Sends data to
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T914647109"] = "Sendet Daten an"
|
||||||
|
|
||||||
|
-- Destination
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T994314591"] = "Ziel"
|
||||||
|
|
||||||
-- 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -7407,6 +7743,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Basierend auf .N
|
|||||||
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstellt beim Start eine Protokolldatei, in der Ereignisse während des Starts aufgezeichnet werden. Nach dem Start wird eine weitere Protokolldatei erstellt, die alle Ereignisse während der Nutzung der App dokumentiert. Dazu gehören auch eventuell auftretende Fehler. Je nachdem, wann ein Fehler auftritt (beim Start oder während der Nutzung), können die Inhalte dieser Protokolldateien bei der Fehlerbehebung hilfreich sein. Sensible Informationen wie Passwörter werden nicht in den Protokolldateien gespeichert."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstellt beim Start eine Protokolldatei, in der Ereignisse während des Starts aufgezeichnet werden. Nach dem Start wird eine weitere Protokolldatei erstellt, die alle Ereignisse während der Nutzung der App dokumentiert. Dazu gehören auch eventuell auftretende Fehler. Je nachdem, wann ein Fehler auftritt (beim Start oder während der Nutzung), können die Inhalte dieser Protokolldateien bei der Fehlerbehebung hilfreich sein. Sensible Informationen wie Passwörter werden nicht in den Protokolldateien gespeichert."
|
||||||
|
|
||||||
|
-- Plugin directory:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin-Verzeichnis:"
|
||||||
|
|
||||||
-- Consent:
|
-- Consent:
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:"
|
||||||
|
|
||||||
@ -7437,6 +7776,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek
|
|||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
|
||||||
|
|
||||||
|
-- The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1985806792"] = "Das Projekt objc2 ermöglicht den Zugriff auf die Objective-C-Frameworks von Apple aus Rust. Unter macOS verwenden wir die Bibliotheken objc2, objc2-app-kit und objc2-foundation, um den nativen macOS-Teilen-Dialog zu öffnen, beispielsweise wenn Sie ein Plugin mit anderen teilen."
|
||||||
|
|
||||||
-- Copies the number of loaded root certificates to the clipboard
|
-- Copies the number of loaded root certificates to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Kopiert die Anzahl der geladenen Stammzertifikate in die Zwischenablage"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Kopiert die Anzahl der geladenen Stammzertifikate in die Zwischenablage"
|
||||||
|
|
||||||
@ -7446,6 +7788,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende
|
|||||||
-- Copies the server URL to the clipboard
|
-- Copies the server URL to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage"
|
||||||
|
|
||||||
|
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen."
|
||||||
|
|
||||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "Diese Bibliothek wird verwendet, um temporäre Ordner bei Laufzeittests zu erstellen und Dateisystemoperationen zu unterstützen."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "Diese Bibliothek wird verwendet, um temporäre Ordner bei Laufzeittests zu erstellen und Dateisystemoperationen zu unterstützen."
|
||||||
|
|
||||||
@ -7566,6 +7911,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3002755581"] = "Diese Bibliothek
|
|||||||
-- Changelog
|
-- Changelog
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
|
||||||
|
|
||||||
|
-- Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3019585985"] = "Testkonfiguration: Niemand hat diese Konfiguration bereitgestellt. Sie ist gültig, bis Sie AI Studio neu starten."
|
||||||
|
|
||||||
-- External HTTPS custom root certificates are configured but not active.
|
-- External HTTPS custom root certificates are configured but not active.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "Externe benutzerdefinierte Stammzertifikate sind konfiguriert, aber nicht aktiv."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "Externe benutzerdefinierte Stammzertifikate sind konfiguriert, aber nicht aktiv."
|
||||||
|
|
||||||
@ -7581,6 +7929,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Verbinden Sie AI
|
|||||||
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen für neue Funktionen? Senden Sie uns Vorschläge für zukünftige Verbesserungen von AI Studio."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen für neue Funktionen? Senden Sie uns Vorschläge für zukünftige Verbesserungen von AI Studio."
|
||||||
|
|
||||||
|
-- Copies the plugin directory to the clipboard
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3182878147"] = "Kopiert den Plugin-Ordner in die Zwischenablage"
|
||||||
|
|
||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
|
||||||
|
|
||||||
@ -7662,9 +8013,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version er
|
|||||||
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "Unter Linux ermöglicht ashpd den Zugriff auf Desktop-Portale, sodass AI Studio Ordner und Dateien für den Nutzer öffnen kann."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "Unter Linux ermöglicht ashpd den Zugriff auf Desktop-Portale, sodass AI Studio Ordner und Dateien für den Nutzer öffnen kann."
|
||||||
|
|
||||||
-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "Diese Bibliothek wird verwendet, um auf die Windows-Registry zuzugreifen. Wir nutzen sie in Windows-Unternehmensumgebungen, um die gewünschte Konfiguration auszulesen."
|
|
||||||
|
|
||||||
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Jetzt haben wir mehrere Systeme, einige entwickelt in .NET und andere in Rust. Das Datenformat JSON ist dafür zuständig, Daten zwischen beiden Welten zu übersetzen (dies nennt man Serialisierung und Deserialisierung von Daten). In der Rust-Welt übernimmt Serde diese Aufgabe. Das Pendant in der .NET-Welt ist ein fester Bestandteil von .NET und findet sich in System.Text.Json."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Jetzt haben wir mehrere Systeme, einige entwickelt in .NET und andere in Rust. Das Datenformat JSON ist dafür zuständig, Daten zwischen beiden Welten zu übersetzen (dies nennt man Serialisierung und Deserialisierung von Daten). In der Rust-Welt übernimmt Serde diese Aufgabe. Das Pendant in der .NET-Welt ist ein fester Bestandteil von .NET und findet sich in System.Text.Json."
|
||||||
|
|
||||||
@ -7707,6 +8055,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code
|
|||||||
-- Executable path
|
-- Executable path
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Pfad der ausführbaren Datei"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Pfad der ausführbaren Datei"
|
||||||
|
|
||||||
|
-- AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4172838224"] = "AI Studio hat beim Starten {0} Testkonfigurationen entfernt. Eine Testkonfiguration gilt nur für eine Sitzung: Fügen Sie sie erneut hinzu, während AI Studio ausgeführt wird."
|
||||||
|
|
||||||
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "Wir verwenden das HtmlAgilityPack, um Inhalte aus dem Internet zu extrahieren. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "Wir verwenden das HtmlAgilityPack, um Inhalte aus dem Internet zu extrahieren. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
|
||||||
|
|
||||||
@ -7776,6 +8127,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Daten
|
|||||||
-- How to update
|
-- How to update
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "Update-Anleitung"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "Update-Anleitung"
|
||||||
|
|
||||||
|
-- A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T923110805"] = "Eine Testkonfiguration ist aktiv. Sie funktioniert wie eine Konfiguration Ihrer Organisation und kann beispielsweise Plugins für Assistenten genehmigen. AI Studio entfernt sie beim nächsten Start der App."
|
||||||
|
|
||||||
-- Install Pandoc
|
-- Install Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren"
|
||||||
|
|
||||||
@ -7785,18 +8139,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1229643769"] = "Potenziell gefährli
|
|||||||
-- Disable plugin
|
-- Disable plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Plugin deaktivieren"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Plugin deaktivieren"
|
||||||
|
|
||||||
|
-- Import
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1463683828"] = "Importieren"
|
||||||
|
|
||||||
|
-- Import plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1467093263"] = "Plugin importieren"
|
||||||
|
|
||||||
-- Assistant Audit
|
-- Assistant Audit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistentenprüfung"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistentenprüfung"
|
||||||
|
|
||||||
-- Internal Plugins
|
-- Internal Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Interne Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Interne Plugins"
|
||||||
|
|
||||||
|
-- Plugin updated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1646565893"] = "Plugin aktualisiert."
|
||||||
|
|
||||||
|
-- Import plugin from a file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T169921408"] = "Plugin aus einer Datei importieren"
|
||||||
|
|
||||||
-- Disabled Plugins
|
-- Disabled Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Deaktivierte Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Deaktivierte Plugins"
|
||||||
|
|
||||||
-- Edit assistant plugin
|
-- Edit assistant plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Assistent-Plugin bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Assistent-Plugin bearbeiten"
|
||||||
|
|
||||||
|
-- Plugin installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1889482678"] = "Plugin installiert."
|
||||||
|
|
||||||
-- Send a mail
|
-- Send a mail
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "E-Mail senden"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "E-Mail senden"
|
||||||
|
|
||||||
@ -7818,18 +8187,45 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Aktivierte Plugins"
|
|||||||
-- Revise Assistant Plugin
|
-- Revise Assistant Plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "Assistenten-Plugin überarbeiten"
|
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.
|
-- The assistant plugin '{0}' has been successfully saved.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "Das Assistent-Plugin „{0}“ wurde erfolgreich gespeichert."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "Das Assistent-Plugin „{0}“ wurde erfolgreich gespeichert."
|
||||||
|
|
||||||
|
-- An error occurred while sharing the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3184210266"] = "Beim Teilen des Plugins ist ein Fehler aufgetreten."
|
||||||
|
|
||||||
|
-- Your organization has disabled exporting plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Ihre Organisation hat das Exportieren von Plugins deaktiviert."
|
||||||
|
|
||||||
|
-- Share plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Plugin-Archiv teilen"
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Ihre Organisation hat das Teilen von Plugins deaktiviert"
|
||||||
|
|
||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Schließen"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Schließen"
|
||||||
|
|
||||||
|
-- Please drop a plugin archive with the extension {0} or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3785427568"] = "Bitte legen Sie ein Plugin-Archiv mit der Erweiterung {0} oder .zip hier ab."
|
||||||
|
|
||||||
-- Revise assistant plugin with AI
|
-- Revise assistant plugin with AI
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Assistenten-Plugin mit KI überarbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Assistenten-Plugin mit KI überarbeiten"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Aktionen"
|
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"
|
||||||
|
|
||||||
|
-- Please drop only one plugin archive at a time.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3974628410"] = "Bitte legen Sie jeweils nur ein Plugin-Archiv gleichzeitig ab."
|
||||||
|
|
||||||
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "Die automatische Sicherheitsprüfung für das Assistenten-Plugin „{0}“ ist fehlgeschlagen. Bitte führen Sie sie manuell aus."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "Die automatische Sicherheitsprüfung für das Assistenten-Plugin „{0}“ ist fehlgeschlagen. Bitte führen Sie sie manuell aus."
|
||||||
|
|
||||||
@ -7842,6 +8238,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Website öffnen"
|
|||||||
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "Das Assistenten-Plugin „{0}“ wurde mit der Stufe „{1}“ geprüft, die unter der erforderlichen Mindeststufe „{2}“ liegt. Ihre aktuellen Einstellungen erlauben die Aktivierung dennoch, dies kann jedoch potenziell gefährlich sein. Möchten Sie dieses Plugin wirklich aktivieren?"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "Das Assistenten-Plugin „{0}“ wurde mit der Stufe „{1}“ geprüft, die unter der erforderlichen Mindeststufe „{2}“ liegt. Ihre aktuellen Einstellungen erlauben die Aktivierung dennoch, dies kann jedoch potenziell gefährlich sein. Möchten Sie dieses Plugin wirklich aktivieren?"
|
||||||
|
|
||||||
|
-- The plugin archive was exported to '{0}'.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "Das Plugin-Archiv wurde nach „{0}“ exportiert."
|
||||||
|
|
||||||
|
-- An error occurred while exporting the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T759681732"] = "Beim Exportieren des Plugins ist ein Fehler aufgetreten."
|
||||||
|
|
||||||
|
-- The plugin could not be imported: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T837269472"] = "Das Plugin konnte nicht importiert werden: {0}"
|
||||||
|
|
||||||
-- Settings
|
-- Settings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Einstellungen"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Einstellungen"
|
||||||
|
|
||||||
@ -9255,6 +9660,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code
|
|||||||
-- Document
|
-- Document
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument"
|
||||||
|
|
||||||
|
-- Plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin-Archiv"
|
||||||
|
|
||||||
-- The Assistant Builder context could not be loaded.
|
-- The Assistant Builder context could not be loaded.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "Der Kontext des Assistenten-Builders konnte nicht geladen werden."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "Der Kontext des Assistenten-Builders konnte nicht geladen werden."
|
||||||
|
|
||||||
@ -9357,75 +9765,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
|
|||||||
-- Please create an assistant draft first.
|
-- Please create an assistant draft first.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Bitte erstellen Sie zuerst einen Entwurf für den Assistenten."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Bitte erstellen Sie zuerst einen Entwurf für den Assistenten."
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Interne Assistenten-Plugins können nicht gelöscht werden."
|
|
||||||
|
|
||||||
-- 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."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1288328479"] = "Nur Assistant-Plugins können bearbeitet werden."
|
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1318944584"] = "Der Assistent kann nicht gelöscht werden, solange noch Hintergrundaktivitäten ausgeführt werden."
|
|
||||||
|
|
||||||
-- No Lua plugin code was generated.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1839013358"] = "Es wurde kein Lua-Plugin-Code generiert."
|
|
||||||
|
|
||||||
-- The edited assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2061233834"] = "Das bearbeitete Assistenten-Plugin verwendet die ID eines internen AI-Studio-Plugins."
|
|
||||||
|
|
||||||
-- The assistant plugin directory does not exist.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2148384567"] = "Das Verzeichnis für das Assistenten-Plugin existiert nicht."
|
|
||||||
|
|
||||||
-- The resolved plugin directory is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223071618"] = "Das ermittelte Plugin-Verzeichnis liegt außerhalb des Plugin-Verzeichnisses des Assistenten."
|
|
||||||
|
|
||||||
-- Unexpected error: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unerwarteter Fehler: {0}"
|
|
||||||
|
|
||||||
-- The assistant plugin has no local directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "Das Assistenten-Plugin hat kein lokales Verzeichnis."
|
|
||||||
|
|
||||||
-- The AI Studio data directory is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2712481762"] = "Das Datenverzeichnis von AI Studio ist noch nicht initialisiert."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2864597027"] = "Nur Assistant-Plugins können gelöscht werden."
|
|
||||||
|
|
||||||
-- The generated plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2955055168"] = "Das generierte Plugin ist kein Assistenten-Plugin. Problem: {0}"
|
|
||||||
|
|
||||||
-- The generated assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3162363526"] = "Das generierte Assistent-Plugin verwendet die ID eines internen AI-Studio-Plugins."
|
|
||||||
|
|
||||||
-- Config Server managed assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3751820312"] = "Von einem Config-Server verwaltete Assistenten-Plugins können nicht gelöscht werden."
|
|
||||||
|
|
||||||
-- Only assistants generated by the Assistant Builder can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3940247198"] = "Nur mit dem Assistant Builder erstellte Assistenten können gelöscht werden."
|
|
||||||
|
|
||||||
-- The edited plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984111892"] = "Das bearbeitete Plugin ist kein Assistenten-Plugin. Problem: {0}"
|
|
||||||
|
|
||||||
-- The plugin system is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984839613"] = "Das Plugin-System ist noch nicht initialisiert."
|
|
||||||
|
|
||||||
-- The plugin file is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T4062980447"] = "Die Plugin-Datei befindet sich außerhalb des Assistenten-Plugin-Verzeichnisses."
|
|
||||||
|
|
||||||
-- The edited assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T554567780"] = "Das bearbeitete Assistenten-Plugin ist ungültig. Problem: {0}"
|
|
||||||
|
|
||||||
-- The edited assistant plugin must keep the same plugin ID.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T693124809"] = "Das bearbeitete Assistant-Plugin muss dieselbe Plugin-ID beibehalten."
|
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T816339833"] = "Interne Assistenten-Plugins können nicht bearbeitet werden."
|
|
||||||
|
|
||||||
-- The generated assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "Das generierte Assistenten-Plugin ist ungültig. Problem: {0}"
|
|
||||||
|
|
||||||
-- The voice recording shortcut currently works only while AI Studio is focused.
|
-- The voice recording shortcut currently works only while AI Studio is focused.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "Die Tastenkombination für Sprachaufnahmen funktioniert derzeit nur, wenn AI Studio im Vordergrund aktiv ist."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "Die Tastenkombination für Sprachaufnahmen funktioniert derzeit nur, wenn AI Studio im Vordergrund aktiv ist."
|
||||||
|
|
||||||
@ -9477,6 +9816,144 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
||||||
|
|
||||||
|
-- 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::PLUGININSTALLSERVICE::T1138181282"] = "Dieses Plugin-Archiv gibt an, von einem Konfigurationsserver verwaltet zu werden. Nur die IT-Abteilung Ihrer Organisation kann solche Plugins bereitstellen."
|
||||||
|
|
||||||
|
-- The imported plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1195382910"] = "Das importierte Plugin verwendet die ID eines anderen installierten Plugins."
|
||||||
|
|
||||||
|
-- The assistant plugin directory is outside the local assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1211881977"] = "Das Assistenten-Plugin-Verzeichnis befindet sich außerhalb des lokalen Assistenten-Plugin-Verzeichnisses."
|
||||||
|
|
||||||
|
-- Only assistant plugins can be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1288328479"] = "Nur Assistant-Plugins können bearbeitet werden."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1318944584"] = "Der Assistent kann nicht gelöscht werden, solange noch Hintergrundaktivitäten ausgeführt werden."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1348456011"] = "Von Ihrer Organisation bereitgestellte Plugins können nicht gelöscht werden."
|
||||||
|
|
||||||
|
-- The resolved plugin directory is outside the plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1559620698"] = "Das ermittelte Plugin-Verzeichnis befindet sich außerhalb des Plugin-Verzeichnisses."
|
||||||
|
|
||||||
|
-- Please select a plugin archive with the extension .mwplugin or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1809137998"] = "Bitte wählen Sie ein Plugin-Archiv mit der Dateiendung .mwplugin oder .zip aus."
|
||||||
|
|
||||||
|
-- The selected plugin archive does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1821013825"] = "Das ausgewählte Plugin-Archiv existiert nicht."
|
||||||
|
|
||||||
|
-- No Lua plugin code was generated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1839013358"] = "Es wurde kein Lua-Plugin-Code generiert."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1878846406"] = "Nur Assistenten-, Konfigurations- und Sprach-Plugins können gelöscht werden."
|
||||||
|
|
||||||
|
-- Your organization has disabled importing configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2134532120"] = "Ihre Organisation hat das Importieren von Konfigurations-Plugins deaktiviert."
|
||||||
|
|
||||||
|
-- The assistant plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2148384567"] = "Das Verzeichnis für das Assistenten-Plugin existiert nicht."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2221093487"] = "Das Plugin-Verzeichnis existiert nicht."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"] = "Unerwarteter Fehler: {0}"
|
||||||
|
|
||||||
|
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "Das generierte Assistenten-Plugin verwendet die ID eines anderen installierten Plugins."
|
||||||
|
|
||||||
|
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "Das Verzeichnis dieses einzelnen Plugins liegt außerhalb des erwarteten Plugin-Verzeichnisses."
|
||||||
|
|
||||||
|
-- The assistant plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2682912892"] = "Das Assistenten-Plugin hat kein lokales Verzeichnis."
|
||||||
|
|
||||||
|
-- The AI Studio data directory is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2712481762"] = "Das Datenverzeichnis von AI Studio ist noch nicht initialisiert."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be imported.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2909113247"] = "Es können nur Assistenten-, Konfigurations- und Sprach-Plugins importiert werden."
|
||||||
|
|
||||||
|
-- The generated plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2955055168"] = "Das generierte Plugin ist kein Assistenten-Plugin. Problem: {0}"
|
||||||
|
|
||||||
|
-- Your organization has disabled importing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3212529834"] = "Ihre Organisation hat das Importieren von Plugins deaktiviert."
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3284289028"] = "Das Plugin hat kein lokales Verzeichnis."
|
||||||
|
|
||||||
|
-- The plugin archive must contain exactly one plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3355918609"] = "Das Plugin-Archiv muss genau eine plugin.lua-Datei enthalten."
|
||||||
|
|
||||||
|
-- Your organization deployed a configuration with the same ID. An imported configuration must not take its place.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T352004699"] = "Ihre Organisation hat bereits eine Konfiguration mit derselben ID bereitgestellt. Eine importierte Konfiguration darf diese nicht ersetzen."
|
||||||
|
|
||||||
|
-- The imported plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3634046009"] = "Das importierte Plugin ist ungültig. Problem: {0}"
|
||||||
|
|
||||||
|
-- Plugins shipped with AI Studio cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3841213017"] = "Mit AI Studio ausgelieferte Plugins können nicht gelöscht werden."
|
||||||
|
|
||||||
|
-- The edited plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984111892"] = "Das bearbeitete Plugin ist kein Assistenten-Plugin. Problem: {0}"
|
||||||
|
|
||||||
|
-- The plugin system is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984839613"] = "Das Plugin-System ist noch nicht initialisiert."
|
||||||
|
|
||||||
|
-- The plugin file is outside the assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T4062980447"] = "Die Plugin-Datei befindet sich außerhalb des Assistenten-Plugin-Verzeichnisses."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be replaced.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T553820956"] = "Von Ihrer Organisation bereitgestellte Plugins können nicht ersetzt werden."
|
||||||
|
|
||||||
|
-- The edited assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T554567780"] = "Das bearbeitete Assistenten-Plugin ist ungültig. Problem: {0}"
|
||||||
|
|
||||||
|
-- The edited assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T584770023"] = "Das bearbeitete Assistenten-Plugin verwendet die ID eines anderen installierten Plugins."
|
||||||
|
|
||||||
|
-- The edited assistant plugin must keep the same plugin ID.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T693124809"] = "Das bearbeitete Assistant-Plugin muss dieselbe Plugin-ID beibehalten."
|
||||||
|
|
||||||
|
-- Internal assistant plugins cannot be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T816339833"] = "Interne Assistenten-Plugins können nicht bearbeitet werden."
|
||||||
|
|
||||||
|
-- The generated assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T939708112"] = "Das generierte Assistenten-Plugin ist ungültig. Problem: {0}"
|
||||||
|
|
||||||
|
-- Internal plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T1668534561"] = "Interne Plugins können nicht geteilt werden."
|
||||||
|
|
||||||
|
-- Config Server managed plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2077776546"] = "Vom Konfigurationsserver verwaltete Plugins können nicht geteilt werden."
|
||||||
|
|
||||||
|
-- The native share dialog could not be opened.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2101116016"] = "Der systemeigene Dialog zum Teilen konnte nicht geöffnet werden."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2221093487"] = "Das Plugin-Verzeichnis existiert nicht."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2350673880"] = "Unerwarteter Fehler: {0}"
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3284289028"] = "Das Plugin hat kein lokales Verzeichnis."
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3379469503"] = "Ihre Organisation hat das Teilen von Plugins deaktiviert."
|
||||||
|
|
||||||
|
-- The plugin directory is invalid: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3774594541"] = "Das Plugin-Verzeichnis ist ungültig: {0}"
|
||||||
|
|
||||||
|
-- Export plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3872669664"] = "Plugin-Archiv exportieren"
|
||||||
|
|
||||||
|
-- The plugin directory does not contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T409411078"] = "Das Plugin-Verzeichnis enthält keine Datei `plugin.lua`."
|
||||||
|
|
||||||
-- Failed to store the secret data due to an API issue.
|
-- Failed to store the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems."
|
||||||
|
|
||||||
|
|||||||
@ -2679,6 +2679,126 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRE
|
|||||||
-- Build progress
|
-- Build progress
|
||||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Build progress"
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGBUILDPROGRESS::T909046610"] = "Build progress"
|
||||||
|
|
||||||
|
-- The model did not fill every planned content slot exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1003911239"] = "The model did not fill every planned content slot exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The sources of this briefing could not be prepared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1034452233"] = "The sources of this briefing could not be prepared."
|
||||||
|
|
||||||
|
-- This operation did not change the briefing, so no new version was created.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1058618049"] = "This operation did not change the briefing, so no new version was created."
|
||||||
|
|
||||||
|
-- The model filled a content slot with the wrong kind of value. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1099589813"] = "The model filled a content slot with the wrong kind of value. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1198458597"] = "The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model did not cover every source of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1209705994"] = "The model did not cover every source of this briefing exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An accessibility text of the model response was empty or invalid. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1437512295"] = "An accessibility text of the model response was empty or invalid. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response used a prohibited attribute. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1677678770"] = "The model response used a prohibited attribute. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A chart of the model response contained invalid categories or data series. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T181588270"] = "A chart of the model response contained invalid categories or data series. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A source of this briefing can no longer be reached. Please relink or remove the affected source.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1878061605"] = "A source of this briefing can no longer be reached. Please relink or remove the affected source."
|
||||||
|
|
||||||
|
-- The selected provider could not complete this briefing stage.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1905087799"] = "The selected provider could not complete this briefing stage."
|
||||||
|
|
||||||
|
-- A calculation of the model response used an invalid operation. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T1992964953"] = "A calculation of the model response used an invalid operation. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response did not match the required contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T214297315"] = "The model response did not match the required contract. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response contained unexpected fields. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2192261405"] = "The model response contained unexpected fields. Please try again or select another model."
|
||||||
|
|
||||||
|
-- AI Studio was closed while this briefing was being built. You can resume the build.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2197645770"] = "AI Studio was closed while this briefing was being built. You can resume the build."
|
||||||
|
|
||||||
|
-- The presentation of the model response did not match the briefing contract. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2376983148"] = "The presentation of the model response did not match the briefing contract. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This visual briefing operation was canceled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T240791538"] = "This visual briefing operation was canceled."
|
||||||
|
|
||||||
|
-- The model response contained markup or code, which this briefing does not allow. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2529598303"] = "The model response contained markup or code, which this briefing does not allow. Please try again or select another model."
|
||||||
|
|
||||||
|
-- AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2668127220"] = "AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."
|
||||||
|
|
||||||
|
-- This briefing could not be assembled.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2678882954"] = "This briefing could not be assembled."
|
||||||
|
|
||||||
|
-- An interactive control of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2714042531"] = "An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model did not return valid JSON. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2784808603"] = "The model did not return valid JSON. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A calculation of the model response targeted an invalid briefing element. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2795934353"] = "A calculation of the model response targeted an invalid briefing element. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid initial state. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2796279475"] = "An interactive control of the model response used an invalid initial state. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The accessibility texts of the model response did not match the briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2815870761"] = "The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The new version of this briefing could not be saved.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2818947691"] = "The new version of this briefing could not be saved."
|
||||||
|
|
||||||
|
-- The model did not plan every visual asset of this briefing exactly once. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T2853629903"] = "The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The assembled briefing did not pass the security validation.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T295498807"] = "The assembled briefing did not pass the security validation."
|
||||||
|
|
||||||
|
-- The charts of the model response did not match the planned briefing elements. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3326200304"] = "The charts of the model response did not match the planned briefing elements. Please try again or select another model."
|
||||||
|
|
||||||
|
-- An interactive control of the model response used an invalid identifier. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3412185985"] = "An interactive control of the model response used an invalid identifier. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The model response referenced content that does not exist. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T344215744"] = "The model response referenced content that does not exist. Please try again or select another model."
|
||||||
|
|
||||||
|
-- The updated content no longer fits the current presentation. You can continue as a rebuild.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3515116214"] = "The updated content no longer fits the current presentation. You can continue as a rebuild."
|
||||||
|
|
||||||
|
-- The model response contained a value of the wrong type. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3668896836"] = "The model response contained a value of the wrong type. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This briefing has no provider selected. Please select a provider before you generate a briefing.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T3834145318"] = "This briefing has no provider selected. Please select a provider before you generate a briefing."
|
||||||
|
|
||||||
|
-- The selected model lacks a capability this briefing needs. Please select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T4066127340"] = "The selected model lacks a capability this briefing needs. Please select another model."
|
||||||
|
|
||||||
|
-- A media transcript of this briefing is missing or outdated. Please transcribe the affected media again.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T449544952"] = "A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."
|
||||||
|
|
||||||
|
-- The model response used an invalid briefing layout. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T686008237"] = "The model response used an invalid briefing layout. Please try again or select another model."
|
||||||
|
|
||||||
|
-- A briefing element of the model response was missing its required interactive controls. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T762236598"] = "A briefing element of the model response was missing its required interactive controls. Please try again or select another model."
|
||||||
|
|
||||||
|
-- This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T875151112"] = "This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."
|
||||||
|
|
||||||
|
-- The model response used an unsupported contract version. Please try again or select another model.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::VISUALBRIEFING::VISUALBRIEFINGFAILUREEXTENSIONS::T921285247"] = "The model response used an unsupported contract version. Please try again or select another model."
|
||||||
|
|
||||||
-- This chart cannot be displayed: {0}
|
-- This chart cannot be displayed: {0}
|
||||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
UI_TEXT_CONTENT["AISTUDIO::CHAT::CHARTBLOCK::T1070038198"] = "This chart cannot be displayed: {0}"
|
||||||
|
|
||||||
@ -2799,24 +2919,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T3571008422"] = "Assistan
|
|||||||
-- The result is ready.
|
-- The result is ready.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T661906146"] = "The result is ready."
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
|
||||||
|
|
||||||
-- Delete assistant plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T1692493145"] = "Delete assistant plugin"
|
|
||||||
|
|
||||||
-- Delete Assistant Plugin
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3637071001"] = "Delete Assistant Plugin"
|
|
||||||
|
|
||||||
-- The '{0}' assistant plugin has been successfully removed.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3815023384"] = "The '{0}' assistant plugin has been successfully removed."
|
|
||||||
|
|
||||||
-- The assistant plugin '{0}' could not be deleted: {1}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T3985264168"] = "The assistant plugin '{0}' could not be deleted: {1}"
|
|
||||||
|
|
||||||
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINDELETEACTION::T4033722845"] = "Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."
|
|
||||||
|
|
||||||
-- Show or hide the detailed security information.
|
-- Show or hide the detailed security information.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."
|
||||||
|
|
||||||
@ -3264,6 +3366,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Co
|
|||||||
-- Cannot copy this content type to clipboard.
|
-- Cannot copy this content type to clipboard.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
||||||
|
|
||||||
|
-- Delete assistant plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1692493145"] = "Delete assistant plugin"
|
||||||
|
|
||||||
|
-- Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T1744561175"] = "Do you really want to delete the language plugin '{0}'? This permanently deletes its local plugin files. When it is your chosen language, AI Studio returns to choosing the language automatically."
|
||||||
|
|
||||||
|
-- Delete language plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2707495447"] = "Delete language plugin"
|
||||||
|
|
||||||
|
-- The plugin '{0}' could not be deleted: {1}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2738963920"] = "The plugin '{0}' could not be deleted: {1}"
|
||||||
|
|
||||||
|
-- Delete Language Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T2990518039"] = "Delete Language Plugin"
|
||||||
|
|
||||||
|
-- Delete Configuration Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3395354991"] = "Delete Configuration Plugin"
|
||||||
|
|
||||||
|
-- The plugin '{0}' has been successfully removed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3476138264"] = "The plugin '{0}' has been successfully removed."
|
||||||
|
|
||||||
|
-- Delete Assistant Plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T3637071001"] = "Delete Assistant Plugin"
|
||||||
|
|
||||||
|
-- Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T4033722845"] = "Do you really want to delete the assistant plugin '{0}'? This will permanently delete the local plugin files."
|
||||||
|
|
||||||
|
-- Delete configuration plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PLUGINDELETEACTION::T459830575"] = "Delete configuration plugin"
|
||||||
|
|
||||||
-- Alpha phase means that we are working on the last details before the beta phase.
|
-- Alpha phase means that we are working on the last details before the beta phase.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase."
|
||||||
|
|
||||||
@ -3876,6 +4011,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T40680
|
|||||||
-- Edit Embedding Provider
|
-- Edit Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Edit Embedding Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T4264602229"] = "Edit Embedding Provider"
|
||||||
|
|
||||||
|
-- This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T438107040"] = "This self-hosted embedding provider is trusted for data source security checks. Local data can be sent to it without security warnings."
|
||||||
|
|
||||||
-- Configure Embedding Providers
|
-- Configure Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Configure Embedding Providers"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T488419116"] = "Configure Embedding Providers"
|
||||||
|
|
||||||
@ -3960,6 +4098,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T386503
|
|||||||
-- Delete LLM Provider
|
-- Delete LLM Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
|
||||||
|
|
||||||
|
-- This self-hosted provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T485526152"] = "This self-hosted provider is trusted for data source security checks."
|
||||||
|
|
||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
|
||||||
|
|
||||||
@ -3987,6 +4128,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T17
|
|||||||
-- Add Transcription Provider
|
-- Add Transcription Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2066315685"] = "Add Transcription Provider"
|
||||||
|
|
||||||
|
-- This self-hosted transcription provider is trusted for data source security checks.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2175189736"] = "This self-hosted transcription provider is trusted for data source security checks."
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Model"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T2189814010"] = "Model"
|
||||||
|
|
||||||
@ -4626,6 +4770,84 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow th
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- {0} LLM providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T121235760"] = "{0} LLM providers"
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1238255445"] = "{0} profiles"
|
||||||
|
|
||||||
|
-- No
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T1642511898"] = "No"
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2107991661"] = "{0} introductions on the welcome page"
|
||||||
|
|
||||||
|
-- {0} mandatory information
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2150386772"] = "{0} mandatory information"
|
||||||
|
|
||||||
|
-- You can install the plugin again later, but any changes you made to its settings are lost.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2156367745"] = "You can install the plugin again later, but any changes you made to its settings are lost."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2342765572"] = "{0} profile"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2426110502"] = "{0} introduction on the welcome page"
|
||||||
|
|
||||||
|
-- {0} embedding providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2438407498"] = "{0} embedding providers"
|
||||||
|
|
||||||
|
-- Yes, delete it
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2466176832"] = "Yes, delete it"
|
||||||
|
|
||||||
|
-- This also removes everything the configuration plugin had set up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T264970454"] = "This also removes everything the configuration plugin had set up:"
|
||||||
|
|
||||||
|
-- {0} transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T2681055470"] = "{0} transcription provider"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3235448458"] = "{0} chat templates"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3278137746"] = "{0} document analysis policy"
|
||||||
|
|
||||||
|
-- The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T330559934"] = "The configuration plugin is not running, so we cannot tell what it had set up. Anything it configured will be removed as well."
|
||||||
|
|
||||||
|
-- {0} LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3410030691"] = "{0} LLM provider"
|
||||||
|
|
||||||
|
-- Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3616855807"] = "Do you really want to delete the configuration plugin '{0}'? This permanently deletes its local plugin files."
|
||||||
|
|
||||||
|
-- {0} settings return to their default values
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3841220170"] = "{0} settings return to their default values"
|
||||||
|
|
||||||
|
-- {0} setting returns to its default value
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T384701293"] = "{0} setting returns to its default value"
|
||||||
|
|
||||||
|
-- {0} mandatory informations
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T3971735909"] = "{0} mandatory informations"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4147879421"] = "{0} chat template"
|
||||||
|
|
||||||
|
-- {0} data sources, including their credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T4193757254"] = "{0} data sources, including their credentials in your operating system's keychain"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T449490978"] = "{0} document analysis policies"
|
||||||
|
|
||||||
|
-- {0} data source, including its credentials in your operating system's keychain
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T511418335"] = "{0} data source, including its credentials in your operating system's keychain"
|
||||||
|
|
||||||
|
-- {0} transcription providers
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"] = "{0} transcription providers"
|
||||||
|
|
||||||
|
-- {0} embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} embedding provider"
|
||||||
|
|
||||||
-- No
|
-- No
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "No"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "No"
|
||||||
|
|
||||||
@ -5244,6 +5466,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embe
|
|||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -5334,6 +5559,117 @@ 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
|
-- {0} profiles
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T1238255445"] = "{0} profiles"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- Data source
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2034620186"] = "Data source"
|
||||||
|
|
||||||
|
-- A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2051328106"] = "A configuration takes effect right after the installation and has no on/off switch. Please check what it sets up:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} introductions on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2107991661"] = "{0} introductions on the welcome page"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- {0} profile
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2342765572"] = "{0} profile"
|
||||||
|
|
||||||
|
-- {0} introduction on the welcome page
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2426110502"] = "{0} introduction on the welcome page"
|
||||||
|
|
||||||
|
-- Support contact
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2434966596"] = "Support contact"
|
||||||
|
|
||||||
|
-- Name
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- {0} setting it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T2868009192"] = "{0} setting it takes control of"
|
||||||
|
|
||||||
|
-- {0} settings it takes control of
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3190775003"] = "{0} settings it takes control of"
|
||||||
|
|
||||||
|
-- {0} chat templates
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3235448458"] = "{0} chat templates"
|
||||||
|
|
||||||
|
-- {0} document analysis policy
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3278137746"] = "{0} document analysis policy"
|
||||||
|
|
||||||
|
-- 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"
|
||||||
|
|
||||||
|
-- {0} mandatory information you have to accept before using AI Studio
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3540986519"] = "{0} mandatory information you have to accept before using AI Studio"
|
||||||
|
|
||||||
|
-- Transcription provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T3566003684"] = "Transcription provider"
|
||||||
|
|
||||||
|
-- Replace plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4068580334"] = "Replace plugin"
|
||||||
|
|
||||||
|
-- LLM provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4099016901"] = "LLM provider"
|
||||||
|
|
||||||
|
-- {0} chat template
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T4147879421"] = "{0} chat template"
|
||||||
|
|
||||||
|
-- {0} document analysis policies
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T449490978"] = "{0} document analysis policies"
|
||||||
|
|
||||||
|
-- The authors marked this plugin as deprecated: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T497068698"] = "The authors marked this plugin as deprecated: {0}"
|
||||||
|
|
||||||
|
-- It also brings:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T713968030"] = "It also brings:"
|
||||||
|
|
||||||
|
-- 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."
|
||||||
|
|
||||||
|
-- Embedding provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T877326195"] = "Embedding provider"
|
||||||
|
|
||||||
|
-- Cancel
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- Sends data to
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T914647109"] = "Sends data to"
|
||||||
|
|
||||||
|
-- Destination
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PLUGINIMPORTDIALOG::T994314591"] = "Destination"
|
||||||
|
|
||||||
-- 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.
|
-- 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."
|
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."
|
||||||
|
|
||||||
@ -7407,6 +7743,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Building on .NET
|
|||||||
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
-- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files."
|
||||||
|
|
||||||
|
-- Plugin directory:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:"
|
||||||
|
|
||||||
-- Consent:
|
-- Consent:
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:"
|
||||||
|
|
||||||
@ -7437,6 +7776,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is
|
|||||||
-- Encryption secret: is configured
|
-- Encryption secret: is configured
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||||
|
|
||||||
|
-- The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1985806792"] = "The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others."
|
||||||
|
|
||||||
-- Copies the number of loaded root certificates to the clipboard
|
-- Copies the number of loaded root certificates to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2015329654"] = "Copies the number of loaded root certificates to the clipboard"
|
||||||
|
|
||||||
@ -7446,6 +7788,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo
|
|||||||
-- Copies the server URL to the clipboard
|
-- Copies the server URL to the clipboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard"
|
||||||
|
|
||||||
|
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||||
|
|
||||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
||||||
|
|
||||||
@ -7566,6 +7911,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3002755581"] = "This library ide
|
|||||||
-- Changelog
|
-- Changelog
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
|
||||||
|
|
||||||
|
-- Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3019585985"] = "Test configuration: nobody deployed this configuration. It is valid until you restart AI Studio."
|
||||||
|
|
||||||
-- External HTTPS custom root certificates are configured but not active.
|
-- External HTTPS custom root certificates are configured but not active.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active."
|
||||||
|
|
||||||
@ -7581,6 +7929,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T313276297"] = "Connect AI Studio
|
|||||||
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
-- Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements."
|
||||||
|
|
||||||
|
-- Copies the plugin directory to the clipboard
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3182878147"] = "Copies the plugin directory to the clipboard"
|
||||||
|
|
||||||
-- Hide Details
|
-- Hide Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||||
|
|
||||||
@ -7662,9 +8013,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version doe
|
|||||||
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
-- On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3871176264"] = "On Linux, ashpd provides access to desktop portals, allowing AI Studio to open folders and files for the user."
|
||||||
|
|
||||||
-- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration."
|
|
||||||
|
|
||||||
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
-- Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3908558992"] = "Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json."
|
||||||
|
|
||||||
@ -7707,6 +8055,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4158546761"] = "Community & Code
|
|||||||
-- Executable path
|
-- Executable path
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4164953312"] = "Executable path"
|
||||||
|
|
||||||
|
-- AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4172838224"] = "AI Studio removed {0} test configuration(s) while starting. A test configuration is valid for one session: place it again while AI Studio is running."
|
||||||
|
|
||||||
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
-- We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4184485147"] = "We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||||
|
|
||||||
@ -7776,6 +8127,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data tra
|
|||||||
-- How to update
|
-- How to update
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "How to update"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T906183311"] = "How to update"
|
||||||
|
|
||||||
|
-- A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T923110805"] = "A test configuration is active. It acts like a configuration of your organization and may, for example, approve assistant plugins. AI Studio removes it the next time you start the app."
|
||||||
|
|
||||||
-- Install Pandoc
|
-- Install Pandoc
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Install Pandoc"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Install Pandoc"
|
||||||
|
|
||||||
@ -7785,18 +8139,33 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1229643769"] = "Potentially Dangerou
|
|||||||
-- Disable plugin
|
-- Disable plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Disable plugin"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1430375822"] = "Disable plugin"
|
||||||
|
|
||||||
|
-- Import
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1463683828"] = "Import"
|
||||||
|
|
||||||
|
-- Import plugin
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1467093263"] = "Import plugin"
|
||||||
|
|
||||||
-- Assistant Audit
|
-- Assistant Audit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistant Audit"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1506922856"] = "Assistant Audit"
|
||||||
|
|
||||||
-- Internal Plugins
|
-- Internal Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T158493184"] = "Internal Plugins"
|
||||||
|
|
||||||
|
-- Plugin updated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1646565893"] = "Plugin updated."
|
||||||
|
|
||||||
|
-- Import plugin from a file
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T169921408"] = "Import plugin from a file"
|
||||||
|
|
||||||
-- Disabled Plugins
|
-- Disabled Plugins
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1724138133"] = "Disabled Plugins"
|
||||||
|
|
||||||
-- Edit assistant plugin
|
-- Edit assistant plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Edit assistant plugin"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1851885496"] = "Edit assistant plugin"
|
||||||
|
|
||||||
|
-- Plugin installed.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1889482678"] = "Plugin installed."
|
||||||
|
|
||||||
-- Send a mail
|
-- Send a mail
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T1999487139"] = "Send a mail"
|
||||||
|
|
||||||
@ -7818,18 +8187,45 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T2738444034"] = "Enabled Plugins"
|
|||||||
-- Revise Assistant Plugin
|
-- Revise Assistant Plugin
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T284393424"] = "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.
|
-- The assistant plugin '{0}' has been successfully saved.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3143506997"] = "The assistant plugin '{0}' has been successfully saved."
|
||||||
|
|
||||||
|
-- An error occurred while sharing the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3184210266"] = "An error occurred while sharing the plugin."
|
||||||
|
|
||||||
|
-- Your organization has disabled exporting plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3342440765"] = "Your organization has disabled exporting plugins."
|
||||||
|
|
||||||
|
-- Share plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3355474457"] = "Share plugin archive"
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3379469503"] = "Your organization has disabled sharing plugins"
|
||||||
|
|
||||||
-- Close
|
-- Close
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Close"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3448155331"] = "Close"
|
||||||
|
|
||||||
|
-- Please drop a plugin archive with the extension {0} or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3785427568"] = "Please drop a plugin archive with the extension {0} or .zip."
|
||||||
|
|
||||||
-- Revise assistant plugin with AI
|
-- Revise assistant plugin with AI
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Revise assistant plugin with AI"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3801095542"] = "Revise assistant plugin with AI"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3865031940"] = "Actions"
|
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"
|
||||||
|
|
||||||
|
-- Please drop only one plugin archive at a time.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T3974628410"] = "Please drop only one plugin archive at a time."
|
||||||
|
|
||||||
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
-- The automatic security audit for the assistant plugin '{0}' failed. Please run it manually.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually."
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4066679817"] = "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually."
|
||||||
|
|
||||||
@ -7842,6 +8238,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T4239378936"] = "Open website"
|
|||||||
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
-- The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T448946658"] = "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?"
|
||||||
|
|
||||||
|
-- The plugin archive was exported to '{0}'.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T659549952"] = "The plugin archive was exported to '{0}'."
|
||||||
|
|
||||||
|
-- An error occurred while exporting the plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T759681732"] = "An error occurred while exporting the plugin."
|
||||||
|
|
||||||
|
-- The plugin could not be imported: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::PLUGINS::T837269472"] = "The plugin could not be imported: {0}"
|
||||||
|
|
||||||
-- Settings
|
-- Settings
|
||||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
|
UI_TEXT_CONTENT["AISTUDIO::PAGES::SETTINGS::T1258653480"] = "Settings"
|
||||||
|
|
||||||
@ -9255,6 +9660,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like p
|
|||||||
-- Document
|
-- Document
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
||||||
|
|
||||||
|
-- Plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive"
|
||||||
|
|
||||||
-- The Assistant Builder context could not be loaded.
|
-- The Assistant Builder context could not be loaded.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
||||||
|
|
||||||
@ -9357,75 +9765,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4
|
|||||||
-- Please create an assistant draft first.
|
-- Please create an assistant draft first.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Please create an assistant draft first."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T4269176489"] = "Please create an assistant draft first."
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1084244321"] = "Internal assistant plugins cannot be deleted."
|
|
||||||
|
|
||||||
-- 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."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1288328479"] = "Only assistant plugins can be edited."
|
|
||||||
|
|
||||||
-- The assistant cannot be deleted while background work is still running.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
|
||||||
|
|
||||||
-- No Lua plugin code was generated.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T1839013358"] = "No Lua plugin code was generated."
|
|
||||||
|
|
||||||
-- The edited assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2061233834"] = "The edited assistant plugin uses the ID of an internal AI Studio plugin."
|
|
||||||
|
|
||||||
-- The assistant plugin directory does not exist.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2148384567"] = "The assistant plugin directory does not exist."
|
|
||||||
|
|
||||||
-- The resolved plugin directory is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2223071618"] = "The resolved plugin directory is outside the assistant plugin directory."
|
|
||||||
|
|
||||||
-- Unexpected error: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
|
|
||||||
|
|
||||||
-- The assistant plugin has no local directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."
|
|
||||||
|
|
||||||
-- The AI Studio data directory is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2712481762"] = "The AI Studio data directory is not initialized yet."
|
|
||||||
|
|
||||||
-- Only assistant plugins can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2864597027"] = "Only assistant plugins can be deleted."
|
|
||||||
|
|
||||||
-- The generated plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T2955055168"] = "The generated plugin is not an assistant plugin. Issue: {0}"
|
|
||||||
|
|
||||||
-- The generated assistant plugin uses the ID of an internal AI Studio plugin.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3162363526"] = "The generated assistant plugin uses the ID of an internal AI Studio plugin."
|
|
||||||
|
|
||||||
-- Config Server managed assistant plugins cannot be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3751820312"] = "Config Server managed assistant plugins cannot be deleted."
|
|
||||||
|
|
||||||
-- Only assistants generated by the Assistant Builder can be deleted.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3940247198"] = "Only assistants generated by the Assistant Builder can be deleted."
|
|
||||||
|
|
||||||
-- The edited plugin is not an assistant plugin. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984111892"] = "The edited plugin is not an assistant plugin. Issue: {0}"
|
|
||||||
|
|
||||||
-- The plugin system is not initialized yet.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T3984839613"] = "The plugin system is not initialized yet."
|
|
||||||
|
|
||||||
-- The plugin file is outside the assistant plugin directory.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T4062980447"] = "The plugin file is outside the assistant plugin directory."
|
|
||||||
|
|
||||||
-- The edited assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T554567780"] = "The edited assistant plugin is invalid. Issue: {0}"
|
|
||||||
|
|
||||||
-- The edited assistant plugin must keep the same plugin ID.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T693124809"] = "The edited assistant plugin must keep the same plugin ID."
|
|
||||||
|
|
||||||
-- Internal assistant plugins cannot be edited.
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T816339833"] = "Internal assistant plugins cannot be edited."
|
|
||||||
|
|
||||||
-- The generated assistant plugin is invalid. Issue: {0}
|
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}"
|
|
||||||
|
|
||||||
-- The voice recording shortcut currently works only while AI Studio is focused.
|
-- The voice recording shortcut currently works only while AI Studio is focused.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "The voice recording shortcut currently works only while AI Studio is focused."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T1204510649"] = "The voice recording shortcut currently works only while AI Studio is focused."
|
||||||
|
|
||||||
@ -9477,6 +9816,144 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
|||||||
-- Pandoc may be required for importing files.
|
-- Pandoc may be required for importing files.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||||
|
|
||||||
|
-- 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::PLUGININSTALLSERVICE::T1138181282"] = "This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."
|
||||||
|
|
||||||
|
-- The imported plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1195382910"] = "The imported plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- The assistant plugin directory is outside the local assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1211881977"] = "The assistant plugin directory is outside the local assistant plugin directory."
|
||||||
|
|
||||||
|
-- Only assistant plugins can be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1288328479"] = "Only assistant plugins can be edited."
|
||||||
|
|
||||||
|
-- The assistant cannot be deleted while background work is still running.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1318944584"] = "The assistant cannot be deleted while background work is still running."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1348456011"] = "Plugins deployed by your organization cannot be deleted."
|
||||||
|
|
||||||
|
-- The resolved plugin directory is outside the plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1559620698"] = "The resolved plugin directory is outside the plugin directory."
|
||||||
|
|
||||||
|
-- Please select a plugin archive with the extension .mwplugin or .zip.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1809137998"] = "Please select a plugin archive with the extension .mwplugin or .zip."
|
||||||
|
|
||||||
|
-- The selected plugin archive does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1821013825"] = "The selected plugin archive does not exist."
|
||||||
|
|
||||||
|
-- No Lua plugin code was generated.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1839013358"] = "No Lua plugin code was generated."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T1878846406"] = "Only assistant, configuration, and language plugins can be deleted."
|
||||||
|
|
||||||
|
-- Your organization has disabled importing configuration plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2134532120"] = "Your organization has disabled importing configuration plugins."
|
||||||
|
|
||||||
|
-- The assistant plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2148384567"] = "The assistant plugin directory does not exist."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2221093487"] = "The plugin directory does not exist."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2350673880"] = "Unexpected error: {0}"
|
||||||
|
|
||||||
|
-- The generated assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2441747251"] = "The generated assistant plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- This individual plugin’s directory is outside the expected plugins directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2486199999"] = "This individual plugin’s directory is outside the expected plugins directory."
|
||||||
|
|
||||||
|
-- The assistant plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2682912892"] = "The assistant plugin has no local directory."
|
||||||
|
|
||||||
|
-- The AI Studio data directory is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2712481762"] = "The AI Studio data directory is not initialized yet."
|
||||||
|
|
||||||
|
-- Only assistant, configuration, and language plugins can be imported.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2909113247"] = "Only assistant, configuration, and language plugins can be imported."
|
||||||
|
|
||||||
|
-- The generated plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T2955055168"] = "The generated plugin is not an assistant plugin. Issue: {0}"
|
||||||
|
|
||||||
|
-- Your organization has disabled importing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3212529834"] = "Your organization has disabled importing plugins."
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3284289028"] = "The plugin has no local directory."
|
||||||
|
|
||||||
|
-- The plugin archive must contain exactly one plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3355918609"] = "The plugin archive must contain exactly one plugin.lua file."
|
||||||
|
|
||||||
|
-- Your organization deployed a configuration with the same ID. An imported configuration must not take its place.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T352004699"] = "Your organization deployed a configuration with the same ID. An imported configuration must not take its place."
|
||||||
|
|
||||||
|
-- The imported plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3634046009"] = "The imported plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- Plugins shipped with AI Studio cannot be deleted.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3841213017"] = "Plugins shipped with AI Studio cannot be deleted."
|
||||||
|
|
||||||
|
-- The edited plugin is not an assistant plugin. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984111892"] = "The edited plugin is not an assistant plugin. Issue: {0}"
|
||||||
|
|
||||||
|
-- The plugin system is not initialized yet.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T3984839613"] = "The plugin system is not initialized yet."
|
||||||
|
|
||||||
|
-- The plugin file is outside the assistant plugin directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T4062980447"] = "The plugin file is outside the assistant plugin directory."
|
||||||
|
|
||||||
|
-- Plugins deployed by your organization cannot be replaced.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T553820956"] = "Plugins deployed by your organization cannot be replaced."
|
||||||
|
|
||||||
|
-- The edited assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T554567780"] = "The edited assistant plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- The edited assistant plugin uses the ID of another installed plugin.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T584770023"] = "The edited assistant plugin uses the ID of another installed plugin."
|
||||||
|
|
||||||
|
-- The edited assistant plugin must keep the same plugin ID.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T693124809"] = "The edited assistant plugin must keep the same plugin ID."
|
||||||
|
|
||||||
|
-- Internal assistant plugins cannot be edited.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T816339833"] = "Internal assistant plugins cannot be edited."
|
||||||
|
|
||||||
|
-- The generated assistant plugin is invalid. Issue: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}"
|
||||||
|
|
||||||
|
-- Internal plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T1668534561"] = "Internal plugins cannot be shared."
|
||||||
|
|
||||||
|
-- Config Server managed plugins cannot be shared.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2077776546"] = "Config Server managed plugins cannot be shared."
|
||||||
|
|
||||||
|
-- The native share dialog could not be opened.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2101116016"] = "The native share dialog could not be opened."
|
||||||
|
|
||||||
|
-- The plugin directory does not exist.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2221093487"] = "The plugin directory does not exist."
|
||||||
|
|
||||||
|
-- Unexpected error: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T2350673880"] = "Unexpected error: {0}"
|
||||||
|
|
||||||
|
-- The plugin has no local directory.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3284289028"] = "The plugin has no local directory."
|
||||||
|
|
||||||
|
-- Your organization has disabled sharing plugins.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3379469503"] = "Your organization has disabled sharing plugins."
|
||||||
|
|
||||||
|
-- The plugin directory is invalid: {0}
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3774594541"] = "The plugin directory is invalid: {0}"
|
||||||
|
|
||||||
|
-- Export plugin archive
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T3872669664"] = "Export plugin archive"
|
||||||
|
|
||||||
|
-- The plugin directory does not contain a plugin.lua file.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PLUGINSHARESERVICE::T409411078"] = "The plugin directory does not contain a plugin.lua file."
|
||||||
|
|
||||||
-- Failed to store the secret data due to an API issue.
|
-- Failed to store the secret data due to an API issue.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||||
|
|
||||||
|
|||||||
@ -173,7 +173,7 @@ internal sealed class Program
|
|||||||
builder.Services.AddSingleton<VisualBriefingBuildOrchestrator>();
|
builder.Services.AddSingleton<VisualBriefingBuildOrchestrator>();
|
||||||
builder.Services.AddSingleton<VisualBriefingPreviewTokenService>();
|
builder.Services.AddSingleton<VisualBriefingPreviewTokenService>();
|
||||||
builder.Services.AddSingleton<IMediaTranscriptStorage, VisualBriefingTranscriptStorage>();
|
builder.Services.AddSingleton<IMediaTranscriptStorage, VisualBriefingTranscriptStorage>();
|
||||||
builder.Services.AddSingleton<AssistantPluginInstallService>();
|
builder.Services.AddSingleton<PluginInstallService>();
|
||||||
builder.Services.AddSingleton<UpdatePolicy>();
|
builder.Services.AddSingleton<UpdatePolicy>();
|
||||||
builder.Services.AddSingleton<AssistantPluginGenerationService>();
|
builder.Services.AddSingleton<AssistantPluginGenerationService>();
|
||||||
builder.Services.AddSingleton<DataSourceService>();
|
builder.Services.AddSingleton<DataSourceService>();
|
||||||
@ -191,6 +191,8 @@ internal sealed class Program
|
|||||||
builder.Services.AddSingleton<DatabaseClientProvider>();
|
builder.Services.AddSingleton<DatabaseClientProvider>();
|
||||||
builder.Services.AddHostedService<GlobalShortcutService>(serviceProvider => serviceProvider.GetRequiredService<GlobalShortcutService>());
|
builder.Services.AddHostedService<GlobalShortcutService>(serviceProvider => serviceProvider.GetRequiredService<GlobalShortcutService>());
|
||||||
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
||||||
|
builder.Services.AddScoped<NativeShareService>();
|
||||||
|
builder.Services.AddScoped<PluginShareService>();
|
||||||
|
|
||||||
// ReSharper disable AccessToDisposedClosure
|
// ReSharper disable AccessToDisposedClosure
|
||||||
builder.Services.AddHostedService<RustService>(_ => rust);
|
builder.Services.AddHostedService<RustService>(_ => rust);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ namespace AIStudio.Settings;
|
|||||||
/// <typeparam name="TValue">The type of the configuration property value.</typeparam>
|
/// <typeparam name="TValue">The type of the configuration property value.</typeparam>
|
||||||
public record ConfigMeta<TClass, TValue> : ConfigMetaBase
|
public record ConfigMeta<TClass, TValue> : ConfigMetaBase
|
||||||
{
|
{
|
||||||
public ConfigMeta(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression)
|
public ConfigMeta(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression) : base(SettingsManager.ToSettingName(propertyExpression))
|
||||||
{
|
{
|
||||||
this.ConfigSelection = configSelection;
|
this.ConfigSelection = configSelection;
|
||||||
this.PropertyExpression = propertyExpression;
|
this.PropertyExpression = propertyExpression;
|
||||||
@ -27,129 +28,63 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private Expression<Func<TClass, TValue>> PropertyExpression { get; }
|
private Expression<Func<TClass, TValue>> PropertyExpression { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the configuration is locked by a configuration plugin.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsLocked { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the plugin that locked this configuration.
|
|
||||||
/// </summary>
|
|
||||||
public Guid LockedByConfigPluginId { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How this setting is managed by a configuration plugin, if at all.
|
|
||||||
/// </summary>
|
|
||||||
public ManagedConfigurationMode? ManagedMode { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the plugin that currently provides an editable default value.
|
|
||||||
/// </summary>
|
|
||||||
public Guid EditableDefaultByConfigPluginId { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default value for the configuration property. This is used when resetting the property to its default state.
|
/// The default value for the configuration property. This is used when resetting the property to its default state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required TValue Default { get; init; }
|
public required TValue Default { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether a plugin contribution is available.
|
/// The additive value contributions, one per contributing configuration plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPluginContribution { get; private set; }
|
/// <remarks>
|
||||||
|
/// Every configuration plugin keeps its own contribution, so removing one of them leaves the
|
||||||
|
/// contributions of the others intact. Callers that need the overall contribution combine the
|
||||||
|
/// values themselves: only they know how to combine the concrete type.
|
||||||
|
/// </remarks>
|
||||||
|
public IReadOnlyDictionary<Guid, TValue> PluginContributions => this.pluginContributions;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IReadOnlyCollection<Guid> ContributingConfigPluginIds => this.pluginContributions.Keys;
|
||||||
|
|
||||||
|
private readonly Dictionary<Guid, TValue> pluginContributions = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The additive value contribution provided by a configuration plugin.
|
/// Stores the additive contribution of one configuration plugin, replacing its previous one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TValue PluginContribution { get; private set; } = default!;
|
/// <param name="value">The contributed value.</param>
|
||||||
|
/// <param name="pluginId">The contributing configuration plugin.</param>
|
||||||
|
public void SetPluginContribution(TValue value, Guid pluginId) => this.pluginContributions[pluginId] = value;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// The ID of the plugin that provided the additive value contribution.
|
public override bool RemovePluginContribution(Guid configPluginId) => this.pluginContributions.Remove(configPluginId);
|
||||||
/// </summary>
|
|
||||||
public Guid PluginContributionByConfigPluginId { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
|
public override string SerializeCurrentValue() => ManagedConfiguration.SerializeManagedScalarValue(this.GetValue());
|
||||||
/// </summary>
|
|
||||||
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
|
/// <inheritdoc/>
|
||||||
public void LockConfiguration(Guid pluginId)
|
protected override string SerializeCurrentValueAsJson() => JsonSerializer.Serialize(this.GetValue(), SettingsManager.JSON_OPTIONS);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override bool TrySetValueFromJson(string json)
|
||||||
{
|
{
|
||||||
this.IsLocked = true;
|
try
|
||||||
this.LockedByConfigPluginId = pluginId;
|
{
|
||||||
this.ManagedMode = ManagedConfigurationMode.LOCKED;
|
var value = JsonSerializer.Deserialize<TValue>(json, SettingsManager.JSON_OPTIONS);
|
||||||
this.EditableDefaultByConfigPluginId = Guid.Empty;
|
if (value is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
this.SetValue(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.LogWarning(e, $"Was not able to restore the value of the setting '{this.SettingName}' from its snapshot '{json}'. Using the default value instead.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Resets the locked state of the configuration, allowing it to be modified again.
|
protected override void Reset()
|
||||||
/// This will also reset the property to its default value.
|
|
||||||
/// </summary>
|
|
||||||
public void ResetLockedConfiguration()
|
|
||||||
{
|
|
||||||
this.IsLocked = false;
|
|
||||||
this.LockedByConfigPluginId = Guid.Empty;
|
|
||||||
if (this.ManagedMode is ManagedConfigurationMode.LOCKED)
|
|
||||||
this.ManagedMode = null;
|
|
||||||
|
|
||||||
this.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unlocks the configuration state without changing the current value.
|
|
||||||
/// </summary>
|
|
||||||
public void UnlockConfiguration()
|
|
||||||
{
|
|
||||||
this.IsLocked = false;
|
|
||||||
this.LockedByConfigPluginId = Guid.Empty;
|
|
||||||
if (this.ManagedMode is ManagedConfigurationMode.LOCKED)
|
|
||||||
this.ManagedMode = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks the setting as having an editable default provided by a configuration plugin.
|
|
||||||
/// </summary>
|
|
||||||
public void SetEditableDefaultConfiguration(Guid pluginId)
|
|
||||||
{
|
|
||||||
this.IsLocked = false;
|
|
||||||
this.LockedByConfigPluginId = Guid.Empty;
|
|
||||||
this.ManagedMode = ManagedConfigurationMode.EDITABLE_DEFAULT;
|
|
||||||
this.EditableDefaultByConfigPluginId = pluginId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears the editable-default state without changing the current value.
|
|
||||||
/// </summary>
|
|
||||||
public void ClearEditableDefaultConfiguration()
|
|
||||||
{
|
|
||||||
if (this.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
|
|
||||||
this.ManagedMode = null;
|
|
||||||
|
|
||||||
this.EditableDefaultByConfigPluginId = Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores an additive plugin contribution.
|
|
||||||
/// </summary>
|
|
||||||
public void SetPluginContribution(TValue value, Guid pluginId)
|
|
||||||
{
|
|
||||||
this.PluginContribution = value;
|
|
||||||
this.PluginContributionByConfigPluginId = pluginId;
|
|
||||||
this.HasPluginContribution = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears the additive plugin contribution without changing the current value.
|
|
||||||
/// </summary>
|
|
||||||
public void ClearPluginContribution()
|
|
||||||
{
|
|
||||||
this.PluginContribution = default!;
|
|
||||||
this.PluginContributionByConfigPluginId = Guid.Empty;
|
|
||||||
this.HasPluginContribution = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resets the configuration property to its default value.
|
|
||||||
/// </summary>
|
|
||||||
private void Reset()
|
|
||||||
{
|
{
|
||||||
var configInstance = this.ConfigSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
|
var configInstance = this.ConfigSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
|
||||||
var memberExpression = this.PropertyExpression.GetMemberExpression();
|
var memberExpression = this.PropertyExpression.GetMemberExpression();
|
||||||
|
|||||||
@ -1,6 +1,266 @@
|
|||||||
namespace AIStudio.Settings;
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
public abstract record ConfigMetaBase : IConfig
|
/// <summary>
|
||||||
|
/// The type-independent part of the configuration metadata: which configuration plugin manages
|
||||||
|
/// the setting, and in which way.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The managed state lives here so that it can be processed without knowing the setting's type,
|
||||||
|
/// e.g. when cleaning up settings whose configuration plugin was removed.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract record ConfigMetaBase(string SettingName) : IConfig
|
||||||
{
|
{
|
||||||
protected static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
protected static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||||
|
|
||||||
|
protected static ILogger Log => Program.LOGGER_FACTORY.CreateLogger(nameof(ConfigMetaBase));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The persisted name of the configuration setting.
|
||||||
|
/// </summary>
|
||||||
|
public string SettingName { get; } = SettingName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the configuration is locked by a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLocked { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the plugin that locked this configuration.
|
||||||
|
/// </summary>
|
||||||
|
public Guid LockedByConfigPluginId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How this setting is managed by a configuration plugin, if at all.
|
||||||
|
/// </summary>
|
||||||
|
public ManagedConfigurationMode? ManagedMode { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the plugin that currently provides an editable default value.
|
||||||
|
/// </summary>
|
||||||
|
public Guid EditableDefaultByConfigPluginId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration plugins which contribute to this setting.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Contributions are additive, so several configuration plugins may contribute at the same time
|
||||||
|
/// and each of them keeps its own contribution. An organization might enable one preview feature
|
||||||
|
/// for everybody and another one for a single department, for example.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IReadOnlyCollection<Guid> ContributingConfigPluginIds { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether at least one configuration plugin contributes to this setting.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPluginContribution => this.ContributingConfigPluginIds.Count > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
|
||||||
|
public void LockConfiguration(Guid pluginId)
|
||||||
|
{
|
||||||
|
this.IsLocked = true;
|
||||||
|
this.LockedByConfigPluginId = pluginId;
|
||||||
|
this.ManagedMode = ManagedConfigurationMode.LOCKED;
|
||||||
|
this.EditableDefaultByConfigPluginId = Guid.Empty;
|
||||||
|
SettingsManagerAccess.ConfigurationData.ManagedLockedConfigurations[this.SettingName] = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores persisted locked configuration metadata after settings were loaded.
|
||||||
|
/// </summary>
|
||||||
|
public void RestoreLockedConfiguration()
|
||||||
|
{
|
||||||
|
if (this.IsLocked || this.ManagedMode is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!SettingsManagerAccess.ConfigurationData.ManagedLockedConfigurations.TryGetValue(this.SettingName, out var pluginId) || pluginId == Guid.Empty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.IsLocked = true;
|
||||||
|
this.LockedByConfigPluginId = pluginId;
|
||||||
|
this.ManagedMode = ManagedConfigurationMode.LOCKED;
|
||||||
|
this.EditableDefaultByConfigPluginId = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the locked state of the configuration, allowing it to be modified again.
|
||||||
|
/// This will also reset the property to its default value.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetLockedConfiguration()
|
||||||
|
{
|
||||||
|
SettingsManagerAccess.ConfigurationData.ManagedLockedConfigurations.Remove(this.SettingName);
|
||||||
|
|
||||||
|
this.IsLocked = false;
|
||||||
|
this.LockedByConfigPluginId = Guid.Empty;
|
||||||
|
|
||||||
|
if (this.ManagedMode is ManagedConfigurationMode.LOCKED)
|
||||||
|
this.ManagedMode = null;
|
||||||
|
|
||||||
|
this.RestoreUserValueOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlocks the configuration state without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
public void UnlockConfiguration()
|
||||||
|
{
|
||||||
|
SettingsManagerAccess.ConfigurationData.ManagedLockedConfigurations.Remove(this.SettingName);
|
||||||
|
|
||||||
|
this.IsLocked = false;
|
||||||
|
this.LockedByConfigPluginId = Guid.Empty;
|
||||||
|
|
||||||
|
if (this.ManagedMode is ManagedConfigurationMode.LOCKED)
|
||||||
|
this.ManagedMode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the setting as having an editable default provided by a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
public void SetEditableDefaultConfiguration(Guid pluginId)
|
||||||
|
{
|
||||||
|
SettingsManagerAccess.ConfigurationData.ManagedLockedConfigurations.Remove(this.SettingName);
|
||||||
|
|
||||||
|
this.IsLocked = false;
|
||||||
|
this.LockedByConfigPluginId = Guid.Empty;
|
||||||
|
this.ManagedMode = ManagedConfigurationMode.EDITABLE_DEFAULT;
|
||||||
|
this.EditableDefaultByConfigPluginId = pluginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the editable-default state without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearEditableDefaultConfiguration()
|
||||||
|
{
|
||||||
|
if (this.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
|
||||||
|
this.ManagedMode = null;
|
||||||
|
|
||||||
|
this.EditableDefaultByConfigPluginId = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the editable-default state and hands the setting back to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Without a snapshot of the user's value, the current value stays as it is. That is the
|
||||||
|
/// difference to a locked setting: the user was allowed to change an editable default all
|
||||||
|
/// along, so its value is a plausible choice of theirs. Resetting it to the app's default would
|
||||||
|
/// take away something nobody asked us to remove.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="keepCurrentValue">
|
||||||
|
/// True when the user has changed the value in the meantime. Their decision outlives the
|
||||||
|
/// configuration plugin, so the snapshot is dropped instead of applied.
|
||||||
|
/// </param>
|
||||||
|
public void ResetEditableDefaultConfiguration(bool keepCurrentValue)
|
||||||
|
{
|
||||||
|
this.ClearEditableDefaultConfiguration();
|
||||||
|
|
||||||
|
if (keepCurrentValue)
|
||||||
|
this.ClearUserValueSnapshot();
|
||||||
|
else
|
||||||
|
this.TryRestoreUserValueSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contribution of one configuration plugin without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configPluginId">The configuration plugin whose contribution is removed.</param>
|
||||||
|
/// <returns>True when that plugin had a contribution, otherwise false.</returns>
|
||||||
|
public abstract bool RemovePluginContribution(Guid configPluginId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the value the user had chosen before a configuration plugin took over
|
||||||
|
/// this setting is still available.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasUserValueSnapshot => SettingsManagerAccess.ConfigurationData.ManagedUserValueSnapshots.ContainsKey(this.SettingName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remembers the current value as the user's value, so that it can be restored once no
|
||||||
|
/// configuration plugin manages this setting anymore.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only an unmanaged setting holds a value which belongs to the user. When one configuration
|
||||||
|
/// plugin takes a setting over from another, the current value belongs to the previous plugin,
|
||||||
|
/// so the snapshot of the user's value must survive that handover untouched.<br/><br/>
|
||||||
|
/// The persisted editable default counts as managed as well: unlike a locked setting, it is not
|
||||||
|
/// restored into the in-memory state when the settings are loaded, so right after a start it is
|
||||||
|
/// the only evidence that a configuration plugin is already in charge.
|
||||||
|
/// </remarks>
|
||||||
|
public void CaptureUserValueSnapshot()
|
||||||
|
{
|
||||||
|
if (this.ManagedMode is not null || SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.ContainsKey(this.SettingName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var snapshots = SettingsManagerAccess.ConfigurationData.ManagedUserValueSnapshots;
|
||||||
|
if (snapshots.ContainsKey(this.SettingName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
snapshots[this.SettingName] = this.SerializeCurrentValueAsJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the value the user had chosen before a configuration plugin took over this setting.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The snapshot is consumed either way: when it cannot be applied, keeping it would mean trying
|
||||||
|
/// the same broken value again on every start.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>True when a snapshot was available and could be applied, otherwise false.</returns>
|
||||||
|
private bool TryRestoreUserValueSnapshot()
|
||||||
|
{
|
||||||
|
var snapshots = SettingsManagerAccess.ConfigurationData.ManagedUserValueSnapshots;
|
||||||
|
if (!snapshots.Remove(this.SettingName, out var snapshot))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.TrySetValueFromJson(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops the snapshot of the user's value without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True when a snapshot was dropped, otherwise false.</returns>
|
||||||
|
public bool ClearUserValueSnapshot() => SettingsManagerAccess.ConfigurationData.ManagedUserValueSnapshots.Remove(this.SettingName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the current value the same way the managed states record it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is meant for comparisons, e.g. to tell whether the user has changed an editable default
|
||||||
|
/// in the meantime. It is not meant for restoring a value: the representation is lossy.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract string SerializeCurrentValue();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the user's value, or falls back to the default value when no snapshot is available.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Settings which a configuration plugin managed before this app version has no snapshot, and
|
||||||
|
/// neither has a setting whose value the user never changed. The default value is the best
|
||||||
|
/// answer in both cases.
|
||||||
|
/// </remarks>
|
||||||
|
private void RestoreUserValueOrDefault()
|
||||||
|
{
|
||||||
|
if (this.TryRestoreUserValueSnapshot())
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the current value as JSON, so that it can be restored without losing information.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string SerializeCurrentValueAsJson();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a value which was serialized by SerializeCurrentValueAsJson.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="json">The serialized value.</param>
|
||||||
|
/// <returns>True when the value could be applied, otherwise false.</returns>
|
||||||
|
protected abstract bool TrySetValueFromJson(string json);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the configuration property to its default value.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract void Reset();
|
||||||
}
|
}
|
||||||
@ -63,6 +63,23 @@ public sealed class Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, ManagedEditableDefaultState> ManagedEditableDefaults { get; set; } = [];
|
public Dictionary<string, ManagedEditableDefaultState> ManagedEditableDefaults { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration plugin that owns each locked managed setting.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, Guid> ManagedLockedConfigurations { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value each managed setting had before a configuration plugin took it over, as JSON.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A configuration plugin might be removed later, e.g. when a test configuration ends or when an
|
||||||
|
/// organization withdraws its configuration. The value the user had chosen before belongs to the
|
||||||
|
/// user, so we keep it here and restore it instead of falling back to the app's default value.
|
||||||
|
/// The snapshot is taken once, when a setting becomes managed, and is consumed when no
|
||||||
|
/// configuration plugin manages that setting anymore.
|
||||||
|
/// </remarks>
|
||||||
|
public Dictionary<string, string> ManagedUserValueSnapshots { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached audit results for assistant plugins.
|
/// Cached audit results for assistant plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -149,6 +149,26 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true);
|
public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the user be allowed to import plugin archives from disk?
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowUserToImportPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportPlugins, true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the user be allowed to import configuration plugin archives from disk?
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is a second gate on top of AllowUserToImportPlugins, and both must allow the import.
|
||||||
|
/// Configuration plugins deserve their own switch because they are far more powerful than an
|
||||||
|
/// assistant: they define LLM providers and data sources, and they lock settings.
|
||||||
|
/// </remarks>
|
||||||
|
public bool AllowUserToImportConfigurationPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportConfigurationPlugins, true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the user be allowed to share or export plugins as archives?
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowUserToSharePlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToSharePlugins, true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should administration settings be visible in the UI?
|
/// Should administration settings be visible in the UI?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -654,6 +654,11 @@ public static partial class ManagedConfiguration
|
|||||||
if (dryRun)
|
if (dryRun)
|
||||||
return successful;
|
return successful;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Contributions need no protection against a takeover: every configuration plugin has its
|
||||||
|
// own contribution, so no plugin can replace or drop the contribution of another one. This
|
||||||
|
// is also why a local configuration plugin may contribute next to one of an organization.
|
||||||
|
//
|
||||||
if (successful)
|
if (successful)
|
||||||
{
|
{
|
||||||
var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
|
var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
|
||||||
@ -663,10 +668,8 @@ public static partial class ManagedConfiguration
|
|||||||
configMeta.SetValue(merged);
|
configMeta.SetValue(merged);
|
||||||
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
|
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
|
||||||
}
|
}
|
||||||
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
|
else
|
||||||
{
|
configMeta.RemovePluginContribution(configPluginId);
|
||||||
configMeta.ClearPluginContribution();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
|
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
|
||||||
configMeta.UnlockConfiguration();
|
configMeta.UnlockConfiguration();
|
||||||
@ -905,6 +908,18 @@ public static partial class ManagedConfiguration
|
|||||||
if(dryRun)
|
if(dryRun)
|
||||||
return successful;
|
return successful;
|
||||||
|
|
||||||
|
// The setting might belong to the IT department of an organization. In that case, no local
|
||||||
|
// configuration plugin may touch it, no matter what it declares:
|
||||||
|
if (!MayManageSetting(configPluginId, configMeta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remember the value the user had chosen before any configuration plugin took this setting
|
||||||
|
// over. Once no plugin manages it anymore, we hand that value back to the user:
|
||||||
|
//
|
||||||
|
if (successful)
|
||||||
|
configMeta.CaptureUserValueSnapshot();
|
||||||
|
|
||||||
switch (successful)
|
switch (successful)
|
||||||
{
|
{
|
||||||
case true:
|
case true:
|
||||||
@ -924,8 +939,8 @@ public static partial class ManagedConfiguration
|
|||||||
// case only when the setting was locked and managed by the same configuration plugin.
|
// case only when the setting was locked and managed by the same configuration plugin.
|
||||||
//
|
//
|
||||||
// The other case, when the setting was locked and managed by a different configuration plugin,
|
// The other case, when the setting was locked and managed by a different configuration plugin,
|
||||||
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
|
// is handled by the CleanupLeftOverManagedConfigurations method, which checks if the configuration
|
||||||
// is still available. If it is not available, it resets the locked state of the
|
// plugin is still available. If it is not available, it resets the locked state of the
|
||||||
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
||||||
//
|
//
|
||||||
configMeta.ResetLockedConfiguration();
|
configMeta.ResetLockedConfiguration();
|
||||||
@ -954,6 +969,20 @@ public static partial class ManagedConfiguration
|
|||||||
if (dryRun)
|
if (dryRun)
|
||||||
return successful;
|
return successful;
|
||||||
|
|
||||||
|
// The setting might belong to the IT department of an organization. In that case, no local
|
||||||
|
// configuration plugin may touch it, no matter what it declares:
|
||||||
|
if (!MayManageSetting(configPluginId, configMeta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remember the value the user had chosen before any configuration plugin took this setting
|
||||||
|
// over. Once no plugin manages it anymore, we hand that value back to the user. This has to
|
||||||
|
// happen before the managed state below changes, because only an unmanaged setting holds a
|
||||||
|
// value which belongs to the user:
|
||||||
|
//
|
||||||
|
if (successful)
|
||||||
|
configMeta.CaptureUserValueSnapshot();
|
||||||
|
|
||||||
switch (successful)
|
switch (successful)
|
||||||
{
|
{
|
||||||
case true when managedMode is ManagedConfigurationMode.LOCKED:
|
case true when managedMode is ManagedConfigurationMode.LOCKED:
|
||||||
@ -995,7 +1024,7 @@ public static partial class ManagedConfiguration
|
|||||||
case false when configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT
|
case false when configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT
|
||||||
&& TryGetEditableDefaultState(settingName, out var editableDefaultStateToRemove)
|
&& TryGetEditableDefaultState(settingName, out var editableDefaultStateToRemove)
|
||||||
&& editableDefaultStateToRemove.ConfigPluginId == configPluginId:
|
&& editableDefaultStateToRemove.ConfigPluginId == configPluginId:
|
||||||
configMeta.ClearEditableDefaultConfiguration();
|
configMeta.ResetEditableDefaultConfiguration(HasUserChangedEditableDefault(configMeta, editableDefaultStateToRemove));
|
||||||
ClearEditableDefaultState(settingName);
|
ClearEditableDefaultState(settingName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1020,7 +1049,7 @@ public static partial class ManagedConfiguration
|
|||||||
return ManagedConfigurationMode.LOCKED;
|
return ManagedConfigurationMode.LOCKED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SerializeManagedScalarValue<TValue>(TValue value) => value switch
|
internal static string SerializeManagedScalarValue<TValue>(TValue value) => value switch
|
||||||
{
|
{
|
||||||
null => string.Empty,
|
null => string.Empty,
|
||||||
string text => text,
|
string text => text,
|
||||||
|
|||||||
@ -19,10 +19,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||||
/// <returns>The default value.</returns>
|
/// <returns>The default value.</returns>
|
||||||
public static TValue Register<TClass, TValue>(
|
public static TValue Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, TValue>> propertyExpression, TValue defaultValue)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, TValue>> propertyExpression,
|
|
||||||
TValue defaultValue)
|
|
||||||
where TValue : struct
|
where TValue : struct
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
@ -57,10 +54,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
/// <param name="defaultValue">The default value to use when the setting is not configured.</param>
|
||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <returns>The default value.</returns>
|
/// <returns>The default value.</returns>
|
||||||
public static string Register<TClass>(
|
public static string Register<TClass>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, string>> propertyExpression, string defaultValue)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, string>> propertyExpression,
|
|
||||||
string defaultValue)
|
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
// we ignore the register call and return the default value:
|
// we ignore the register call and return the default value:
|
||||||
@ -95,10 +89,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the elements in the list within the configuration class.</typeparam>
|
/// <typeparam name="TValue">The type of the elements in the list within the configuration class.</typeparam>
|
||||||
/// <returns>A list containing the default value.</returns>
|
/// <returns>A list containing the default value.</returns>
|
||||||
public static List<TValue> Register<TClass, TValue>(
|
public static List<TValue> Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, IList<TValue>>> propertyExpression, TValue defaultValue)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
|
||||||
TValue defaultValue)
|
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
// we ignore the register call and return the default value:
|
// we ignore the register call and return the default value:
|
||||||
@ -133,10 +124,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the elements within the property list.</typeparam>
|
/// <typeparam name="TValue">The type of the elements within the property list.</typeparam>
|
||||||
/// <returns>The list of default values.</returns>
|
/// <returns>The list of default values.</returns>
|
||||||
public static List<TValue> Register<TClass, TValue>(
|
public static List<TValue> Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, IList<TValue>>> propertyExpression, IList<TValue> defaultValues)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
|
||||||
IList<TValue> defaultValues)
|
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
// we ignore the register call and return the default value:
|
// we ignore the register call and return the default value:
|
||||||
@ -170,10 +158,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the values within the set.</typeparam>
|
/// <typeparam name="TValue">The type of the values within the set.</typeparam>
|
||||||
/// <returns>A set containing the default value.</returns>
|
/// <returns>A set containing the default value.</returns>
|
||||||
public static HashSet<TValue> Register<TClass, TValue>(
|
public static HashSet<TValue> Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, ISet<TValue>>> propertyExpression, TValue defaultValue)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
|
||||||
TValue defaultValue)
|
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
// we ignore the register call and return the default value:
|
// we ignore the register call and return the default value:
|
||||||
@ -208,10 +193,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the elements in the collection associated with the configuration property.</typeparam>
|
/// <typeparam name="TValue">The type of the elements in the collection associated with the configuration property.</typeparam>
|
||||||
/// <returns>A set containing the default values.</returns>
|
/// <returns>A set containing the default values.</returns>
|
||||||
public static HashSet<TValue> Register<TClass, TValue>(
|
public static HashSet<TValue> Register<TClass, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, ISet<TValue>>> propertyExpression, IList<TValue> defaultValues)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
|
||||||
IList<TValue> defaultValues)
|
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
// we ignore the register call and return the default value:
|
// we ignore the register call and return the default value:
|
||||||
@ -246,10 +228,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
|
||||||
/// <typeparam name="TDict">>The type of the dictionary within the configuration class.</typeparam>
|
/// <typeparam name="TDict">>The type of the dictionary within the configuration class.</typeparam>
|
||||||
/// <returns>A dictionary containing the default values.</returns>
|
/// <returns>A dictionary containing the default values.</returns>
|
||||||
public static TDict Register<TClass, TDict>(
|
public static TDict Register<TClass, TDict>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, IDictionary<string, string>>> propertyExpression, TDict defaultValues)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
|
||||||
TDict defaultValues)
|
|
||||||
where TDict : IDictionary<string, string>, new()
|
where TDict : IDictionary<string, string>, new()
|
||||||
{
|
{
|
||||||
// When called from the JSON deserializer by using the standard constructor,
|
// When called from the JSON deserializer by using the standard constructor,
|
||||||
@ -286,10 +265,7 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
|
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
|
||||||
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
|
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
|
||||||
/// <returns>A dictionary containing the default values.</returns>
|
/// <returns>A dictionary containing the default values.</returns>
|
||||||
public static Dictionary<TKey, TValue> Register<TClass, TKey, TValue>(
|
public static Dictionary<TKey, TValue> Register<TClass, TKey, TValue>(Expression<Func<Data, TClass>>? configSelection, Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression, Dictionary<TKey, TValue> defaultValues)
|
||||||
Expression<Func<Data, TClass>>? configSelection,
|
|
||||||
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
|
|
||||||
Dictionary<TKey, TValue> defaultValues)
|
|
||||||
where TKey : struct, Enum
|
where TKey : struct, Enum
|
||||||
where TValue : struct, Enum
|
where TValue : struct, Enum
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,8 +9,11 @@ namespace AIStudio.Settings;
|
|||||||
public static partial class ManagedConfiguration
|
public static partial class ManagedConfiguration
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
|
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
|
||||||
|
|
||||||
private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||||
|
|
||||||
|
private static ILogger Log => Program.LOGGER_FACTORY.CreateLogger(nameof(ManagedConfiguration));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||||
/// property expression (enum-based).
|
/// property expression (enum-based).
|
||||||
@ -28,15 +31,13 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass, TValue>(
|
public static bool TryGet<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression, out ConfigMeta<TClass, TValue> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, TValue>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, TValue> configMeta)
|
|
||||||
where TValue : Enum
|
where TValue : Enum
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -65,14 +66,12 @@ public static partial class ManagedConfiguration
|
|||||||
/// if found.</param>
|
/// if found.</param>
|
||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass>(
|
public static bool TryGet<TClass>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, string>> propertyExpression, out ConfigMeta<TClass, string> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, string>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, string> configMeta)
|
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, string> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, string> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -104,16 +103,13 @@ public static partial class ManagedConfiguration
|
|||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
|
|
||||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
// ReSharper disable MethodOverloadWithOptionalParameter
|
||||||
public static bool TryGet<TClass, TValue>(
|
public static bool TryGet<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression, out ConfigMeta<TClass, TValue> configMeta, ISpanParsable<TValue>? _ = null)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, TValue>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, TValue> configMeta,
|
|
||||||
ISpanParsable<TValue>? _ = null)
|
|
||||||
where TValue : struct, ISpanParsable<TValue>
|
where TValue : struct, ISpanParsable<TValue>
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, TValue> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -143,14 +139,12 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass, TValue>(
|
public static bool TryGet<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, IList<TValue>>> propertyExpression, out ConfigMeta<TClass, IList<TValue>> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, IList<TValue>> configMeta)
|
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IList<TValue>> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IList<TValue>> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -178,14 +172,12 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass, TValue>(
|
public static bool TryGet<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, ISet<TValue>>> propertyExpression, out ConfigMeta<TClass, ISet<TValue>> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, ISet<TValue>> configMeta)
|
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, ISet<TValue>> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, ISet<TValue>> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -212,14 +204,12 @@ public static partial class ManagedConfiguration
|
|||||||
/// if found.</param>
|
/// if found.</param>
|
||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass>(
|
public static bool TryGet<TClass>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, IDictionary<string, string>>> propertyExpression, out ConfigMeta<TClass, IDictionary<string, string>> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, IDictionary<string, string>> configMeta)
|
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IDictionary<string, string>> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, IDictionary<string, string>> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -248,16 +238,14 @@ public static partial class ManagedConfiguration
|
|||||||
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
|
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
|
||||||
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
|
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
|
||||||
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
|
||||||
public static bool TryGet<TClass, TKey, TValue>(
|
public static bool TryGet<TClass, TKey, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression, out ConfigMeta<TClass, Dictionary<TKey, TValue>> configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
|
|
||||||
out ConfigMeta<TClass, Dictionary<TKey, TValue>> configMeta)
|
|
||||||
where TKey : struct, Enum
|
where TKey : struct, Enum
|
||||||
where TValue : struct, Enum
|
where TValue : struct, Enum
|
||||||
{
|
{
|
||||||
var configPath = Path(configSelection, propertyExpression);
|
var configPath = Path(configSelection, propertyExpression);
|
||||||
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, Dictionary<TKey, TValue>> meta)
|
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, Dictionary<TKey, TValue>> meta)
|
||||||
{
|
{
|
||||||
|
meta.RestoreLockedConfiguration();
|
||||||
configMeta = meta;
|
configMeta = meta;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -270,209 +258,174 @@ public static partial class ManagedConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a configuration setting is left over from a configuration plugin that is no longer available.
|
/// Checks whether a configuration plugin may manage a setting, or whether that setting belongs
|
||||||
/// If the configuration setting is locked and managed by a configuration plugin that is not available,
|
/// to the IT department of an organization.
|
||||||
/// it resets the managed state of the configuration setting and returns true.
|
|
||||||
/// Otherwise, it returns false.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configSelection">The expression to select the configuration class.</param>
|
/// <remarks>
|
||||||
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
/// A local configuration plugin must not take over a setting an organization manages. Otherwise,
|
||||||
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
|
/// anyone could hand out a configuration plugin that quietly replaces parts of the organization
|
||||||
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
/// configuration, e.g. the address of a self-hosted provider.<br/><br/>
|
||||||
/// <typeparam name="TValue">The type of the property within the configuration class.</typeparam>
|
/// Between two configuration plugins of the same organization, we do not interfere: both belong
|
||||||
/// <returns>True if the configuration setting is left over and was reset, otherwise false.</returns>
|
/// to the IT department, so the one processed later wins, as before.
|
||||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
/// </remarks>
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
/// <param name="configPluginId">The configuration plugin which wants to manage the setting.</param>
|
||||||
Expression<Func<TClass, TValue>> propertyExpression,
|
/// <param name="configMeta">The configuration metadata of the setting.</param>
|
||||||
IReadOnlyList<IAvailablePlugin> availablePlugins)
|
/// <returns>True when the plugin may manage this setting, otherwise false.</returns>
|
||||||
where TValue : Enum
|
private static bool MayManageSetting(Guid configPluginId, ConfigMetaBase configMeta)
|
||||||
{
|
{
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
var owningConfigPluginId = GetSettingOwner(configMeta);
|
||||||
return false;
|
if (owningConfigPluginId == Guid.Empty || owningConfigPluginId == configPluginId)
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked)
|
|
||||||
{
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ResetLockedConfiguration();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins);
|
if (!PluginFactory.IsOrganizationConfigurationPlugin(owningConfigPluginId))
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsConfigurationLeftOver<TClass>(
|
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, string>> propertyExpression,
|
|
||||||
IReadOnlyList<IAvailablePlugin> availablePlugins)
|
|
||||||
{
|
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked)
|
|
||||||
{
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ResetLockedConfiguration();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins);
|
if (PluginFactory.IsOrganizationConfigurationPlugin(configPluginId))
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper disable MethodOverloadWithOptionalParameter
|
|
||||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, TValue>> propertyExpression,
|
|
||||||
IReadOnlyList<IAvailablePlugin> availablePlugins,
|
|
||||||
ISpanParsable<TValue>? _ = null)
|
|
||||||
where TValue : struct, ISpanParsable<TValue>
|
|
||||||
{
|
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked)
|
|
||||||
{
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ResetLockedConfiguration();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReSharper restore MethodOverloadWithOptionalParameter
|
|
||||||
|
|
||||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, IList<TValue>>> propertyExpression,
|
|
||||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
|
||||||
{
|
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
|
|
||||||
return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins.ToList());
|
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is not null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
configMeta.ResetLockedConfiguration();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsConfigurationLeftOver<TClass, TValue>(
|
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
|
||||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
|
||||||
{
|
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ResetLockedConfiguration();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Log.LogWarning($"The configuration plugin '{configPluginId}' tried to manage the setting '{configMeta.SettingName}', which is managed by the configuration plugin '{owningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a plugin contribution is left over from a configuration plugin that is no longer available.
|
/// Determines the configuration plugin which currently manages a setting, if any.
|
||||||
/// If so, it clears the contribution and returns true.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsPluginContributionLeftOver<TClass, TValue>(
|
private static Guid GetSettingOwner(ConfigMetaBase configMeta)
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
|
||||||
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
|
||||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
|
||||||
{
|
{
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty)
|
||||||
return false;
|
return configMeta.LockedByConfigPluginId;
|
||||||
|
|
||||||
if (!configMeta.HasPluginContribution || configMeta.PluginContributionByConfigPluginId == Guid.Empty)
|
// The editable default is persisted as well, so we prefer it over the in-memory state:
|
||||||
return false;
|
if (TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState) && editableDefaultState.ConfigPluginId != Guid.Empty)
|
||||||
|
return editableDefaultState.ConfigPluginId;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.PluginContributionByConfigPluginId);
|
return configMeta.EditableDefaultByConfigPluginId;
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ClearPluginContribution();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
/// <summary>
|
||||||
}
|
/// Removes all managed states whose configuration plugin is not available anymore.
|
||||||
|
/// </summary>
|
||||||
public static bool IsConfigurationLeftOver<TClass>(
|
/// <remarks>
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
/// This covers every registered setting, regardless of its type: locked settings, editable
|
||||||
Expression<Func<TClass, IDictionary<string, string>>> propertyExpression,
|
/// defaults, and additive plugin contributions. Settings do not need to be listed anywhere for
|
||||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
/// this cleanup to work, so adding a new managed setting cannot be forgotten here.<br/><br/>
|
||||||
|
/// A locked setting whose plugin is gone is reset to its default value. That is intended: the
|
||||||
|
/// value belonged to the organization, not to the user, and the user might not be able to
|
||||||
|
/// change it at all.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
|
||||||
|
/// <param name="deployedEnterpriseConfigPluginIds">
|
||||||
|
/// The IDs of the configuration plugins which an organization deployed on this machine, including
|
||||||
|
/// those which could not be loaded. A deployed plugin was not removed, so its settings must stay
|
||||||
|
/// untouched.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>True when at least one setting was changed, otherwise false.</returns>
|
||||||
|
public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds)
|
||||||
{
|
{
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
var wasChanged = false;
|
||||||
return false;
|
var registeredSettingNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
foreach (var config in METADATA.Values)
|
||||||
return false;
|
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
|
||||||
if (plugin is null)
|
|
||||||
{
|
{
|
||||||
|
if (config is not ConfigMetaBase configMeta)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
registeredSettingNames.Add(configMeta.SettingName);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Restore the persisted ownership first. Otherwise, we would not recognize a left-over
|
||||||
|
// lock when nobody has read this setting since the settings were loaded:
|
||||||
|
//
|
||||||
|
configMeta.RestoreLockedConfiguration();
|
||||||
|
|
||||||
|
// Check the locked state:
|
||||||
|
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.LockedByConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
|
||||||
|
{
|
||||||
|
Log.LogInformation($"Resetting the setting '{configMeta.SettingName}': it was locked by the configuration plugin '{configMeta.LockedByConfigPluginId}', which is not available anymore.");
|
||||||
configMeta.ResetLockedConfiguration();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
wasChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Check the editable default state:
|
||||||
}
|
if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedEnterpriseConfigPluginIds))
|
||||||
|
wasChanged = true;
|
||||||
|
|
||||||
public static bool IsConfigurationLeftOver<TClass, TKey, TValue>(
|
// Check the additive plugin contributions. Every contributing plugin is checked on its
|
||||||
Expression<Func<Data, TClass>> configSelection,
|
// own, so one removed plugin does not take the contributions of the others with it:
|
||||||
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
|
foreach (var contributingConfigPluginId in configMeta.ContributingConfigPluginIds.ToList())
|
||||||
IEnumerable<IAvailablePlugin> availablePlugins)
|
|
||||||
where TKey : struct, Enum
|
|
||||||
where TValue : struct, Enum
|
|
||||||
{
|
{
|
||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (contributingConfigPluginId != Guid.Empty && IsPluginPresent(contributingConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
|
||||||
return false;
|
continue;
|
||||||
|
|
||||||
if (configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
|
Log.LogInformation($"Clearing the contribution of the configuration plugin '{contributingConfigPluginId}' to the setting '{configMeta.SettingName}': the plugin is not available anymore.");
|
||||||
{
|
configMeta.RemovePluginContribution(contributingConfigPluginId);
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.EditableDefaultByConfigPluginId);
|
wasChanged = true;
|
||||||
if (plugin is null)
|
|
||||||
{
|
|
||||||
configMeta.ClearEditableDefaultConfiguration();
|
|
||||||
ClearEditableDefaultState(SettingName(propertyExpression));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
//
|
||||||
}
|
// Finally, drop any snapshot of the user's value which nobody claims anymore. Without
|
||||||
|
// this, a setting which stopped being managed outside of the paths above would keep its
|
||||||
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
// snapshot in the settings file forever. The persisted editable default counts as a
|
||||||
return false;
|
// claim as well: it survives a configuration plugin which is deployed but could not be
|
||||||
|
// loaded, and that plugin is still in charge:
|
||||||
var lockedPlugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
//
|
||||||
if (lockedPlugin is null)
|
if (configMeta.ManagedMode is null && !TryGetEditableDefaultState(configMeta.SettingName, out _) && configMeta.ClearUserValueSnapshot())
|
||||||
{
|
{
|
||||||
configMeta.ResetLockedConfiguration();
|
Log.LogInformation($"Dropping the snapshot of the user's value for the setting '{configMeta.SettingName}': no configuration plugin manages it anymore.");
|
||||||
return true;
|
wasChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Remove persisted states which belong to settings that do not exist anymore:
|
||||||
|
if (RemoveUnknownManagedStates(registeredSettingNames))
|
||||||
|
wasChanged = true;
|
||||||
|
|
||||||
|
return wasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a configuration plugin is still present on this machine.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A plugin counts as present when it was loaded, or when it is deployed but could not be loaded.
|
||||||
|
/// The latter matters for organizations: a broken configuration plugin is still in charge, so we
|
||||||
|
/// must not treat its settings as left over.
|
||||||
|
/// </remarks>
|
||||||
|
private static bool IsPluginPresent(Guid configPluginId, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds) => deployedEnterpriseConfigPluginIds.Contains(configPluginId) || availablePlugins.Any(x => x.Id == configPluginId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes persisted managed states which belong to settings that are not registered anymore.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Without this, states of removed or renamed settings would stay in the settings file forever.
|
||||||
|
/// </remarks>
|
||||||
|
private static bool RemoveUnknownManagedStates(IReadOnlySet<string> registeredSettingNames)
|
||||||
|
{
|
||||||
|
var wasChanged = false;
|
||||||
|
var configurationData = SettingsManagerAccess.ConfigurationData;
|
||||||
|
|
||||||
|
foreach (var settingName in configurationData.ManagedLockedConfigurations.Keys.Where(x => !registeredSettingNames.Contains(x)).ToList())
|
||||||
|
{
|
||||||
|
Log.LogInformation($"Removing the persisted lock of the setting '{settingName}': this setting does not exist anymore.");
|
||||||
|
configurationData.ManagedLockedConfigurations.Remove(settingName);
|
||||||
|
wasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var settingName in configurationData.ManagedEditableDefaults.Keys.Where(x => !registeredSettingNames.Contains(x)).ToList())
|
||||||
|
{
|
||||||
|
Log.LogInformation($"Removing the persisted editable default of the setting '{settingName}': this setting does not exist anymore.");
|
||||||
|
configurationData.ManagedEditableDefaults.Remove(settingName);
|
||||||
|
wasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var settingName in configurationData.ManagedUserValueSnapshots.Keys.Where(x => !registeredSettingNames.Contains(x)).ToList())
|
||||||
|
{
|
||||||
|
Log.LogInformation($"Removing the snapshot of the user's value for the setting '{settingName}': this setting does not exist anymore.");
|
||||||
|
configurationData.ManagedUserValueSnapshots.Remove(settingName);
|
||||||
|
wasChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Path<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression)
|
private static string Path<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression)
|
||||||
@ -507,25 +460,32 @@ public static partial class ManagedConfiguration
|
|||||||
|
|
||||||
private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName);
|
private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName);
|
||||||
|
|
||||||
private static bool CleanupEditableDefaultState<TClass, TValue>(
|
private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds)
|
||||||
ConfigMeta<TClass, TValue> configMeta,
|
|
||||||
string settingName,
|
|
||||||
IReadOnlyList<IAvailablePlugin> availablePlugins)
|
|
||||||
{
|
{
|
||||||
if (!TryGetEditableDefaultState(settingName, out var editableDefaultState))
|
if (!TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState))
|
||||||
{
|
{
|
||||||
if (configMeta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT)
|
if (configMeta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
configMeta.ClearEditableDefaultConfiguration();
|
configMeta.ResetEditableDefaultConfiguration(keepCurrentValue: false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == editableDefaultState.ConfigPluginId);
|
if (IsPluginPresent(editableDefaultState.ConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
|
||||||
if (plugin is not null)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
configMeta.ClearEditableDefaultConfiguration();
|
Log.LogInformation($"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore.");
|
||||||
return ClearEditableDefaultState(settingName);
|
configMeta.ResetEditableDefaultConfiguration(HasUserChangedEditableDefault(configMeta, editableDefaultState));
|
||||||
|
return ClearEditableDefaultState(configMeta.SettingName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the user has changed an editable default themselves.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The user may change an editable default at any time. When the current value is not the one
|
||||||
|
/// the configuration plugin applied last, the user decided against that value, and their
|
||||||
|
/// decision outlives the plugin.
|
||||||
|
/// </remarks>
|
||||||
|
private static bool HasUserChangedEditableDefault(ConfigMetaBase configMeta, ManagedEditableDefaultState editableDefaultState) => !string.Equals(configMeta.SerializeCurrentValue(), editableDefaultState.LastAppliedValue, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ public sealed class SettingsManager
|
|||||||
|
|
||||||
private readonly record struct CurrentSettingsReadResult(Data? SettingsData, SettingsWriteBlockReason FailureReason);
|
private readonly record struct CurrentSettingsReadResult(Data? SettingsData, SettingsWriteBlockReason FailureReason);
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
Converters = { new TolerantEnumConverter() },
|
Converters = { new TolerantEnumConverter() },
|
||||||
|
|||||||
@ -84,21 +84,4 @@ public static class AssistantVisibilityExtensions
|
|||||||
|
|
||||||
return !isHidden;
|
return !isHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if any assistant in a category should be visible.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settingsManager">The settings manager to check configuration against.</param>
|
|
||||||
/// <param name="categoryName">The name of the assistant category (for logging purposes).</param>
|
|
||||||
/// <param name="assistants">The assistants in the category with their optional preview feature requirements.</param>
|
|
||||||
/// <returns>True if at least one assistant in the category should be visible, false otherwise.</returns>
|
|
||||||
public static bool IsAnyCategoryAssistantVisible(this SettingsManager settingsManager, string categoryName, params (Components Component, PreviewFeatures RequiredPreviewFeature)[] assistants)
|
|
||||||
{
|
|
||||||
foreach (var (component, requiredPreviewFeature) in assistants)
|
|
||||||
if (settingsManager.IsAssistantVisible(component, withLogging: false, requiredPreviewFeature: requiredPreviewFeature))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
LOGGER.LogInformation("No assistants in category '{CategoryName}' are visible.", categoryName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,15 @@ public interface IAvailablePlugin : IPluginMetadata
|
|||||||
public bool IsManagedByConfigServer { get; }
|
public bool IsManagedByConfigServer { get; }
|
||||||
|
|
||||||
public Guid? ManagedConfigurationId { get; }
|
public Guid? ManagedConfigurationId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The priority of a configuration plugin. Zero for every other plugin type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Configuration plugins with a higher priority start later and therefore win when two of them
|
||||||
|
/// manage the same setting or define the same configuration object. The priority only orders
|
||||||
|
/// plugins of the same origin: a local configuration plugin never starts before one which an
|
||||||
|
/// organization deployed, no matter which priority it declares.
|
||||||
|
/// </remarks>
|
||||||
|
public int ConfigurationPriority { get; }
|
||||||
}
|
}
|
||||||
80
app/MindWork AI Studio/Tools/PluginSystem/PluginArchive.cs
Normal file
80
app/MindWork AI Studio/Tools/PluginSystem/PluginArchive.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
public static class PluginArchive
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The file extension of plugin archives.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Keep in sync with SHARE_FILE_EXTENSION in runtime/src/share_sheet.rs: the runtime only hands
|
||||||
|
/// archives with this extension to the native share sheet.
|
||||||
|
/// </remarks>
|
||||||
|
public const string PLUGIN_FILE_EXTENSION = ".mwplugin";
|
||||||
|
|
||||||
|
|
||||||
|
// Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620);
|
||||||
|
// remove after dotnet/runtime#27620 and #41914 are fixed.
|
||||||
|
// See documentation/compatibility-shims/2026-07-plugin-archive-zip-backslashes.md.
|
||||||
|
public static void Extract(string sourceArchiveFileName, string destinationDirectory)
|
||||||
|
{
|
||||||
|
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
|
||||||
|
Directory.CreateDirectory(destinationDirectory);
|
||||||
|
|
||||||
|
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
|
||||||
|
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
|
||||||
|
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var normalizedEntryName = NormalizeEntryName(entry.FullName);
|
||||||
|
var destinationPath = GetEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName);
|
||||||
|
|
||||||
|
if (normalizedEntryName.EndsWith('/'))
|
||||||
|
{
|
||||||
|
if (entry.Length != 0)
|
||||||
|
throw new InvalidDataException($"The plugin archive contains a directory entry with data: '{entry.FullName}'.");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(destinationPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||||
|
entry.ExtractToFile(destinationPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeEntryName(string entryName)
|
||||||
|
{
|
||||||
|
var normalizedEntryName = entryName.Replace('\\', '/');
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedEntryName))
|
||||||
|
throw new InvalidDataException("The plugin archive contains an empty entry name.");
|
||||||
|
|
||||||
|
if (normalizedEntryName.Contains('\0'))
|
||||||
|
throw new InvalidDataException($"The plugin archive contains an invalid entry name: '{entryName}'.");
|
||||||
|
|
||||||
|
if (normalizedEntryName.StartsWith('/'))
|
||||||
|
throw new InvalidDataException($"The plugin archive contains a rooted entry name: '{entryName}'.");
|
||||||
|
|
||||||
|
if (normalizedEntryName is [_, ':', ..])
|
||||||
|
throw new InvalidDataException($"The plugin archive contains a drive-qualified entry name: '{entryName}'.");
|
||||||
|
|
||||||
|
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or ".."))
|
||||||
|
throw new InvalidDataException($"The plugin archive contains an unsafe entry name: '{entryName}'.");
|
||||||
|
|
||||||
|
return normalizedEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName)
|
||||||
|
{
|
||||||
|
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var relativePath = Path.Combine(pathSegments);
|
||||||
|
var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath));
|
||||||
|
if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal))
|
||||||
|
throw new InvalidDataException($"The plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'.");
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,6 +39,28 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state);
|
public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The priority of this configuration plugin. Defaults to zero when the plugin declares none.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Configuration plugins with a higher priority are applied later and therefore win when two of
|
||||||
|
/// them manage the same setting or define the same configuration object. This lets an
|
||||||
|
/// organization deploy one base configuration for everybody and additional configurations which
|
||||||
|
/// refine it, e.g. per department.
|
||||||
|
/// </remarks>
|
||||||
|
public int Priority { get; } = ReadPriority(state);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many settings this configuration plugin declares.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This counts the entries of the Lua SETTINGS table, without the <c>.AllowUserOverride</c>
|
||||||
|
/// companions. We need it for the import preview: a dry run does not lock anything, so the
|
||||||
|
/// number of settings the plugin would take over cannot be read from the managed configuration
|
||||||
|
/// at that point.
|
||||||
|
/// </remarks>
|
||||||
|
public int DeclaredSettingsCount { get; private set; }
|
||||||
|
|
||||||
public async Task InitializeAsync(bool dryRun)
|
public async Task InitializeAsync(bool dryRun)
|
||||||
{
|
{
|
||||||
if(!this.TryProcessConfiguration(dryRun, out var issue))
|
if(!this.TryProcessConfiguration(dryRun, out var issue))
|
||||||
@ -129,6 +151,34 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ReadPriority(LuaState state)
|
||||||
|
{
|
||||||
|
if (state.Environment["PRIORITY"].TryRead<int>(out var priority))
|
||||||
|
return priority;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts the settings a configuration plugin declares, ignoring the <c>.AllowUserOverride</c>
|
||||||
|
/// companion keys: those refine a setting instead of adding one.
|
||||||
|
/// </summary>
|
||||||
|
private static int CountDeclaredSettings(LuaTable settingsTable)
|
||||||
|
{
|
||||||
|
const string USER_OVERRIDE_SUFFIX = ".AllowUserOverride";
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
var previousKey = LuaValue.Nil;
|
||||||
|
while (settingsTable.TryGetNext(previousKey, out var pair))
|
||||||
|
{
|
||||||
|
previousKey = pair.Key;
|
||||||
|
if (pair.Key.TryRead<string>(out var settingName) && !settingName.EndsWith(USER_OVERRIDE_SUFFIX, StringComparison.Ordinal))
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to initialize the UI text content of the plugin.
|
/// Tries to initialize the UI text content of the plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -155,6 +205,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.DeclaredSettingsCount = CountDeclaredSettings(settingsTable);
|
||||||
|
|
||||||
// Config: check for updates, and if so, how often?
|
// Config: check for updates, and if so, how often?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInterval, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInterval, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
@ -179,6 +231,15 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Config: allow the user to add providers?
|
// Config: allow the user to add providers?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: allow the user to import plugin archives?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportPlugins, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: allow the user to import configuration plugin archives?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportConfigurationPlugins, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: allow the user to share or export plugins?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToSharePlugins, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Config: show administration settings?
|
// Config: show administration settings?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
@ -330,19 +391,92 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
if (dryRun)
|
if (dryRun)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Only a configuration which speaks for an organization may approve assistant plugins: one
|
||||||
|
// deployed by a configuration server, or one staged in the test directory. An approval marks
|
||||||
|
// a plugin as safe without any security audit, and the user interface states that the
|
||||||
|
// organization approved it. No local configuration plugin may make that claim: it would
|
||||||
|
// disable the security audit for arbitrary assistant plugins while telling the user that
|
||||||
|
// their organization vouched for them.
|
||||||
|
//
|
||||||
|
// We decide by the plugin path. The self-declared DEPLOYED_USING_CONFIG_SERVER field would
|
||||||
|
// not do, because any plugin can set it to true.
|
||||||
|
//
|
||||||
|
if (!PluginFactory.IsOrganizationConfigurationPath(this.PluginPath))
|
||||||
|
{
|
||||||
|
if (successful)
|
||||||
|
LOG.LogWarning("The configuration plugin '{ConfigPluginId}' at '{PluginPath}' declares enterprise approvals for assistant plugins, but your organization's IT did not deploy it. Ignoring these approvals: only configuration plugins from a configuration server or from the test directory may approve assistant plugins.", this.Id, this.PluginPath);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PluginFactory.IsEnterpriseTestConfigurationPath(this.PluginPath))
|
||||||
|
LOG.LogWarning("The test configuration plugin '{ConfigPluginId}' at '{PluginPath}' approves assistant plugins. These approvals are valid for this session only: AI Studio empties the test directory on every start.", this.Id, this.PluginPath);
|
||||||
|
|
||||||
switch (successful)
|
switch (successful)
|
||||||
{
|
{
|
||||||
case true:
|
case true:
|
||||||
configMeta.SetValue(configuredApprovals);
|
//
|
||||||
|
// Approvals of several configuration plugins add up. An approval list is a pure
|
||||||
|
// allowlist over hashes: not listing a plugin already means "not approved", so
|
||||||
|
// replacing the list would only ever withdraw the approvals of another
|
||||||
|
// configuration without expressing anything new.
|
||||||
|
//
|
||||||
|
configMeta.SetPluginContribution(configuredApprovals, this.Id);
|
||||||
|
|
||||||
|
// Merge into the stored list right away, so the approvals of this plugin take
|
||||||
|
// effect immediately. PluginFactory.LoadAll recomputes the authoritative list once
|
||||||
|
// every configuration plugin has contributed:
|
||||||
|
var mergedApprovals = new List<DataAssistantPluginEnterpriseApproval>(configMeta.GetValue());
|
||||||
|
var knownHashes = mergedApprovals.Select(approval => approval.PluginHash).ToHashSet(StringComparer.Ordinal);
|
||||||
|
mergedApprovals.AddRange(configuredApprovals.Where(approval => knownHashes.Add(approval.PluginHash)));
|
||||||
|
|
||||||
|
configMeta.SetValue(mergedApprovals);
|
||||||
configMeta.LockConfiguration(this.Id);
|
configMeta.LockConfiguration(this.Id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == this.Id:
|
case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == this.Id:
|
||||||
|
configMeta.RemovePluginContribution(this.Id);
|
||||||
configMeta.ResetLockedConfiguration();
|
configMeta.ResetLockedConfiguration();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case false:
|
||||||
|
configMeta.RemovePluginContribution(this.Id);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recomputes the effective enterprise approvals from the contributions of all configuration plugins.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every configuration plugin merges its own approvals into the stored list while it starts, but
|
||||||
|
/// nothing there can withdraw the approvals of a plugin which was removed in the meantime. This
|
||||||
|
/// method rebuilds the list from the remaining contributions and is therefore called once all
|
||||||
|
/// configuration plugins have been started.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>True when the effective approvals changed, otherwise false.</returns>
|
||||||
|
public static bool RefreshEnterpriseApprovedAssistantPlugins()
|
||||||
|
{
|
||||||
|
if (!ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var effectiveApprovals = new List<DataAssistantPluginEnterpriseApproval>();
|
||||||
|
var effectiveHashes = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var approval in configMeta.PluginContributions.Values.SelectMany(contribution => contribution))
|
||||||
|
if (effectiveHashes.Add(approval.PluginHash))
|
||||||
|
effectiveApprovals.Add(approval);
|
||||||
|
|
||||||
|
// Compare by hash, so a different order alone does not rewrite the settings on every start:
|
||||||
|
var currentApprovals = configMeta.GetValue();
|
||||||
|
if (currentApprovals.Count == effectiveApprovals.Count && effectiveHashes.SetEquals(currentApprovals.Select(approval => approval.PluginHash)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
LOG.LogInformation($"The enterprise approvals for assistant plugins changed from {currentApprovals.Count} to {effectiveApprovals.Count} entries, contributed by {configMeta.PluginContributions.Count} configuration plugin(s).");
|
||||||
|
configMeta.SetValue(effectiveApprovals);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryParseEnterpriseApprovedAssistantPlugin(int index, LuaTable table, Guid configPluginId, out DataAssistantPluginEnterpriseApproval approval)
|
private static bool TryParseEnterpriseApprovedAssistantPlugin(int index, LuaTable table, Guid configPluginId, out DataAssistantPluginEnterpriseApproval approval)
|
||||||
{
|
{
|
||||||
approval = new();
|
approval = new();
|
||||||
|
|||||||
@ -34,6 +34,41 @@ public sealed record PluginConfigurationObject
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;
|
public required PluginConfigurationObjectType Type { get; init; } = PluginConfigurationObjectType.NONE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the configuration object, e.g. the name of a provider.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where this configuration object sends data to: the host of a self-hosted provider or data
|
||||||
|
/// source, or the name of the cloud provider. Empty for objects without a destination, such as
|
||||||
|
/// chat templates or profiles.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// We keep this next to the object metadata so the import preview can tell users where a
|
||||||
|
/// configuration would send their prompts before its providers are stored.
|
||||||
|
/// </remarks>
|
||||||
|
public string Endpoint { get; private init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the destination of a configuration object for the import preview.
|
||||||
|
/// </summary>
|
||||||
|
private static string DescribeEndpoint(IConfigurationObject configObject) => configObject switch
|
||||||
|
{
|
||||||
|
Settings.Provider { IsSelfHosted: true } provider => provider.Hostname,
|
||||||
|
Settings.Provider provider => Provider.LLMProvidersExtensions.ToName(provider.UsedLLMProvider),
|
||||||
|
|
||||||
|
EmbeddingProvider { IsSelfHosted: true } embeddingProvider => embeddingProvider.Hostname,
|
||||||
|
EmbeddingProvider embeddingProvider => Provider.LLMProvidersExtensions.ToName(embeddingProvider.UsedLLMProvider),
|
||||||
|
|
||||||
|
TranscriptionProvider { IsSelfHosted: true } transcriptionProvider => transcriptionProvider.Hostname,
|
||||||
|
TranscriptionProvider transcriptionProvider => Provider.LLMProvidersExtensions.ToName(transcriptionProvider.UsedLLMProvider),
|
||||||
|
|
||||||
|
DataSourceERI_V1 dataSource => dataSource.Hostname,
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses Lua table entries into configuration objects of the specified type, populating the
|
/// Parses Lua table entries into configuration objects of the specified type, populating the
|
||||||
/// provided list with results.
|
/// provided list with results.
|
||||||
@ -125,6 +160,8 @@ public sealed record PluginConfigurationObject
|
|||||||
ConfigPluginId = configPluginId,
|
ConfigPluginId = configPluginId,
|
||||||
Id = Guid.Parse(configObject.Id),
|
Id = Guid.Parse(configObject.Id),
|
||||||
Type = configObjectType,
|
Type = configObjectType,
|
||||||
|
Name = configObject.Name,
|
||||||
|
Endpoint = DescribeEndpoint(configObject),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dryRun)
|
if (dryRun)
|
||||||
@ -136,6 +173,9 @@ public sealed record PluginConfigurationObject
|
|||||||
if (objectIndex > -1)
|
if (objectIndex > -1)
|
||||||
{
|
{
|
||||||
var existingObject = storedObjects[objectIndex];
|
var existingObject = storedObjects[objectIndex];
|
||||||
|
if (!MayReplaceConfigurationObject(existingObject, configPluginId))
|
||||||
|
continue;
|
||||||
|
|
||||||
configObject = configObject with { Num = existingObject.Num };
|
configObject = configObject with { Num = existingObject.Num };
|
||||||
storedObjects[objectIndex] = (TClass)configObject;
|
storedObjects[objectIndex] = (TClass)configObject;
|
||||||
}
|
}
|
||||||
@ -211,6 +251,8 @@ public sealed record PluginConfigurationObject
|
|||||||
ConfigPluginId = configPluginId,
|
ConfigPluginId = configPluginId,
|
||||||
Id = Guid.Parse(configObject.Id),
|
Id = Guid.Parse(configObject.Id),
|
||||||
Type = PluginConfigurationObjectType.DATA_SOURCE,
|
Type = PluginConfigurationObjectType.DATA_SOURCE,
|
||||||
|
Name = configObject.Name,
|
||||||
|
Endpoint = DescribeEndpoint(configObject),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dryRun)
|
if (dryRun)
|
||||||
@ -220,6 +262,9 @@ public sealed record PluginConfigurationObject
|
|||||||
if (objectIndex > -1)
|
if (objectIndex > -1)
|
||||||
{
|
{
|
||||||
var existingObject = storedObjects[objectIndex];
|
var existingObject = storedObjects[objectIndex];
|
||||||
|
if (!MayReplaceConfigurationObject(existingObject, configPluginId))
|
||||||
|
continue;
|
||||||
|
|
||||||
configObject = configObject with { Num = existingObject.Num };
|
configObject = configObject with { Num = existingObject.Num };
|
||||||
storedObjects[objectIndex] = configObject;
|
storedObjects[objectIndex] = configObject;
|
||||||
}
|
}
|
||||||
@ -248,6 +293,35 @@ public sealed record PluginConfigurationObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a configuration plugin may replace a stored configuration object, or whether
|
||||||
|
/// that object belongs to the IT department of an organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Configuration objects are matched by their ID alone. Without this check, a local configuration
|
||||||
|
/// plugin could claim the ID of an object an organization deployed and replace it, e.g. to point
|
||||||
|
/// a self-hosted LLM provider at a different host.<br/><br/>
|
||||||
|
/// Between two configuration plugins of the same organization, we do not interfere: both belong
|
||||||
|
/// to the IT department, so the one processed later wins, as before.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="existingObject">The configuration object which is stored already.</param>
|
||||||
|
/// <param name="configPluginId">The configuration plugin which wants to replace that object.</param>
|
||||||
|
/// <returns>True when the plugin may replace the object, otherwise false.</returns>
|
||||||
|
private static bool MayReplaceConfigurationObject(IConfigurationObject existingObject, Guid configPluginId)
|
||||||
|
{
|
||||||
|
if (!existingObject.IsEnterpriseConfiguration || existingObject.EnterpriseConfigurationPluginId == configPluginId)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!PluginFactory.IsOrganizationConfigurationPlugin(existingObject.EnterpriseConfigurationPluginId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (PluginFactory.IsOrganizationConfigurationPlugin(configPluginId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
LOG.LogWarning("The configuration plugin '{ConfigPluginId}' tried to replace the object '{ConfigObjectName}' (id={ConfigObjectId}), which belongs to the configuration plugin '{OwningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.", configPluginId, existingObject.Name, existingObject.Id, existingObject.EnterpriseConfigurationPluginId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -255,6 +329,11 @@ public sealed record PluginConfigurationObject
|
|||||||
/// <param name="configObjectType">The type of configuration object to process.</param>
|
/// <param name="configObjectType">The type of configuration object to process.</param>
|
||||||
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
|
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
|
||||||
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
||||||
|
/// <param name="deployedEnterpriseConfigPluginIds">
|
||||||
|
/// The IDs of the configuration plugins which an organization deployed on this machine, including
|
||||||
|
/// those which could not be loaded. Objects of a deployed plugin are never removed, because the
|
||||||
|
/// plugin was not removed either.
|
||||||
|
/// </param>
|
||||||
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
||||||
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
||||||
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
||||||
@ -263,6 +342,7 @@ public sealed record PluginConfigurationObject
|
|||||||
PluginConfigurationObjectType configObjectType,
|
PluginConfigurationObjectType configObjectType,
|
||||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||||
IList<IAvailablePlugin> availablePlugins,
|
IList<IAvailablePlugin> availablePlugins,
|
||||||
|
IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds,
|
||||||
IList<PluginConfigurationObject> configObjectList,
|
IList<PluginConfigurationObject> configObjectList,
|
||||||
SecretStoreType? secretStoreType = null,
|
SecretStoreType? secretStoreType = null,
|
||||||
bool deleteSecret = false) where TClass : IConfigurationObject
|
bool deleteSecret = false) where TClass : IConfigurationObject
|
||||||
@ -282,6 +362,16 @@ public sealed record PluginConfigurationObject
|
|||||||
if(configObjectSourcePluginId == Guid.Empty)
|
if(configObjectSourcePluginId == Guid.Empty)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Is the source plugin deployed, but could not be loaded? Then we must not touch any of
|
||||||
|
// its objects. The plugin was not removed, it is broken: it might be invalid Lua code,
|
||||||
|
// a missing `plugin.lua`, or an incomplete download. Removing the objects would delete
|
||||||
|
// the organization's providers and data sources, including their secrets, although the
|
||||||
|
// organization still manages this AI Studio instance:
|
||||||
|
//
|
||||||
|
if(deployedEnterpriseConfigPluginIds.Contains(configObjectSourcePluginId) && availablePlugins.All(plugin => plugin.Id != configObjectSourcePluginId))
|
||||||
|
continue;
|
||||||
|
|
||||||
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
||||||
// over and should be removed:
|
// over and should be removed:
|
||||||
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
|
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
@ -46,7 +45,7 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
|
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
|
||||||
var tempDownloadFile = Path.GetTempFileName();
|
var tempDownloadFile = Path.GetTempFileName();
|
||||||
var stagedDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.staging-{Guid.NewGuid():N}");
|
var stagedDirectory = Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.staging-{Guid.NewGuid():N}");
|
||||||
string? backupDirectory = null;
|
string? backupDirectory = null;
|
||||||
var wasSuccessful = false;
|
var wasSuccessful = false;
|
||||||
try
|
try
|
||||||
@ -67,10 +66,10 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory);
|
ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory);
|
||||||
|
|
||||||
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
var configDirectory = Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||||
if (Directory.Exists(configDirectory))
|
if (Directory.Exists(configDirectory))
|
||||||
{
|
{
|
||||||
backupDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.backup-{Guid.NewGuid():N}");
|
backupDirectory = Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.backup-{Guid.NewGuid():N}");
|
||||||
Directory.Move(configDirectory, backupDirectory);
|
Directory.Move(configDirectory, backupDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ public static partial class PluginFactory
|
|||||||
{
|
{
|
||||||
LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin.");
|
LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin.");
|
||||||
|
|
||||||
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
var configDirectory = Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||||
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !Directory.Exists(configDirectory))
|
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !Directory.Exists(configDirectory))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -130,69 +129,11 @@ public static partial class PluginFactory
|
|||||||
return wasSuccessful;
|
return wasSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620).
|
|
||||||
// See documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md.
|
|
||||||
private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory)
|
private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory)
|
||||||
{
|
{
|
||||||
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
|
PluginArchive.Extract(sourceArchiveFileName, destinationDirectory);
|
||||||
Directory.CreateDirectory(destinationDirectory);
|
|
||||||
|
|
||||||
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
|
|
||||||
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
|
|
||||||
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
|
|
||||||
|
|
||||||
foreach (var entry in archive.Entries)
|
|
||||||
{
|
|
||||||
var normalizedEntryName = NormalizeConfigPluginZipEntryName(entry.FullName);
|
|
||||||
var destinationPath = GetConfigPluginZipEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName);
|
|
||||||
|
|
||||||
if (normalizedEntryName.EndsWith('/'))
|
|
||||||
{
|
|
||||||
if (entry.Length != 0)
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains a directory entry with data: '{entry.FullName}'.");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
|
||||||
entry.ExtractToFile(destinationPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any())
|
if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any())
|
||||||
throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file.");
|
throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeConfigPluginZipEntryName(string entryName)
|
|
||||||
{
|
|
||||||
var normalizedEntryName = entryName.Replace('\\', '/');
|
|
||||||
if (string.IsNullOrWhiteSpace(normalizedEntryName))
|
|
||||||
throw new InvalidDataException("The enterprise configuration plugin archive contains an empty entry name.");
|
|
||||||
|
|
||||||
if (normalizedEntryName.Contains('\0'))
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains an invalid entry name: '{entryName}'.");
|
|
||||||
|
|
||||||
if (normalizedEntryName.StartsWith('/'))
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains a rooted entry name: '{entryName}'.");
|
|
||||||
|
|
||||||
if (normalizedEntryName is [_, ':', ..])
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains a drive-qualified entry name: '{entryName}'.");
|
|
||||||
|
|
||||||
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or ".."))
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains an unsafe entry name: '{entryName}'.");
|
|
||||||
|
|
||||||
return normalizedEntryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetConfigPluginZipEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName)
|
|
||||||
{
|
|
||||||
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var relativePath = Path.Combine(pathSegments);
|
|
||||||
var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath));
|
|
||||||
if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal))
|
|
||||||
throw new InvalidDataException($"The enterprise configuration plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'.");
|
|
||||||
|
|
||||||
return destinationPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.PluginSystem.Assistants;
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
using Lua;
|
using Lua;
|
||||||
using Lua.Standard;
|
using Lua.Standard;
|
||||||
@ -44,11 +46,15 @@ public static partial class PluginFactory
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
LOG.LogInformation("Start loading plugins.");
|
LOG.LogInformation("Start loading plugins.");
|
||||||
if (!Directory.Exists(PLUGINS_ROOT))
|
|
||||||
{
|
//
|
||||||
LOG.LogInformation("No plugins found.");
|
// Without the plugins directory, we cannot load or start any plugin. Still, we must not
|
||||||
return;
|
// stop here: the clean-up at the end of this method has to run. Otherwise, settings which
|
||||||
}
|
// a configuration plugin has locked would stay locked forever.
|
||||||
|
//
|
||||||
|
var pluginsDirectoryExists = Directory.Exists(PLUGINS_ROOT);
|
||||||
|
if (!pluginsDirectoryExists)
|
||||||
|
LOG.LogWarning("No plugins found. Checking for left-over configurations of removed configuration plugins.");
|
||||||
|
|
||||||
AVAILABLE_PLUGINS.Clear();
|
AVAILABLE_PLUGINS.Clear();
|
||||||
|
|
||||||
@ -56,7 +62,7 @@ public static partial class PluginFactory
|
|||||||
// The easiest way to load all plugins is to find all `plugin.lua` files and load them.
|
// The easiest way to load all plugins is to find all `plugin.lua` files and load them.
|
||||||
// By convention, each plugin is enforced to have a `plugin.lua` file.
|
// By convention, each plugin is enforced to have a `plugin.lua` file.
|
||||||
//
|
//
|
||||||
var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories);
|
IEnumerable<string> pluginMainFiles = pluginsDirectoryExists ? Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories) : [];
|
||||||
foreach (var pluginMainFile in pluginMainFiles)
|
foreach (var pluginMainFile in pluginMainFiles)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -104,21 +110,43 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
||||||
|
|
||||||
var isConfigurationPluginInConfigDirectory =
|
//
|
||||||
plugin.Type is PluginType.CONFIGURATION &&
|
// Plugin IDs must be unique: many lookups resolve a plugin by its ID alone, e.g.
|
||||||
pluginPath.StartsWith(CONFIGURATION_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
|
// the base language plugin in PluginFactory.Starting or the owner of a locked
|
||||||
|
// setting. When two plugins share an ID, the one deployed by the organization's
|
||||||
|
// IT wins. Otherwise, a manually placed copy could outrank the enterprise
|
||||||
|
// configuration, which is the exact opposite of what an organization expects:
|
||||||
|
//
|
||||||
|
if (AVAILABLE_PLUGINS.FirstOrDefault(candidate => candidate.Id == plugin.Id) is { } duplicatePlugin)
|
||||||
|
{
|
||||||
|
if (GetConfigurationAuthority(pluginPath) <= GetConfigurationAuthority(duplicatePlugin.LocalPath))
|
||||||
|
{
|
||||||
|
LOG.LogWarning($"Ignoring the plugin '{pluginMainFile}': its ID ('{plugin.Id}') is already used by the plugin at '{duplicatePlugin.LocalPath}'. Plugin IDs must be unique. Please remove one of these plugins.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEnterpriseTestConfigurationPath(pluginPath))
|
||||||
|
LOG.LogWarning($"Ignoring the plugin at '{duplicatePlugin.LocalPath}': it uses the ID ('{plugin.Id}') of the test configuration plugin at '{pluginPath}'. A test configuration takes precedence until AI Studio is restarted.");
|
||||||
|
else
|
||||||
|
LOG.LogWarning($"Ignoring the plugin at '{duplicatePlugin.LocalPath}': it uses the ID ('{plugin.Id}') of the enterprise configuration plugin at '{pluginPath}'. Plugins deployed by your organization's IT take precedence.");
|
||||||
|
|
||||||
|
AVAILABLE_PLUGINS.Remove(duplicatePlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConfigurationPluginInConfigDirectory = plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(pluginPath);
|
||||||
var isManagedByConfigServer = false;
|
var isManagedByConfigServer = false;
|
||||||
Guid? managedConfigurationId = null;
|
Guid? managedConfigurationId = null;
|
||||||
|
var configurationPriority = 0;
|
||||||
if (plugin is PluginConfiguration configPlugin)
|
if (plugin is PluginConfiguration configPlugin)
|
||||||
{
|
{
|
||||||
|
configurationPriority = configPlugin.Priority;
|
||||||
if (configPlugin.DeployedUsingConfigServer.HasValue)
|
if (configPlugin.DeployedUsingConfigServer.HasValue)
|
||||||
isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value;
|
isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value;
|
||||||
|
|
||||||
else if (isConfigurationPluginInConfigDirectory)
|
else if (isConfigurationPluginInConfigDirectory)
|
||||||
{
|
{
|
||||||
isManagedByConfigServer = true;
|
isManagedByConfigServer = true;
|
||||||
LOG.LogWarning($"The configuration plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{CONFIGURATION_PLUGINS_ROOT}'.");
|
LOG.LogWarning($"The configuration plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{ENTERPRISE_CONFIGURATION_PLUGINS_ROOT}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (plugin is PluginAssistants assistantPlugin)
|
else if (plugin is PluginAssistants assistantPlugin)
|
||||||
@ -139,7 +167,7 @@ public static partial class PluginFactory
|
|||||||
LOG.LogWarning($"Could not determine the managed configuration ID for configuration plugin '{plugin.Id}'. The plugin directory '{pluginPath}' does not end with a valid GUID.");
|
LOG.LogWarning($"Could not determine the managed configuration ID for configuration plugin '{plugin.Id}'. The plugin directory '{pluginPath}' does not end with a valid GUID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId));
|
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId, configurationPriority));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -149,9 +177,12 @@ public static partial class PluginFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start or restart all plugins:
|
// Start or restart all plugins:
|
||||||
|
if (pluginsDirectoryExists)
|
||||||
|
{
|
||||||
var configObjects = await RestartAllPlugins(cancellationToken);
|
var configObjects = await RestartAllPlugins(cancellationToken);
|
||||||
configObjectList.AddRange(configObjects);
|
configObjectList.AddRange(configObjects);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
PLUGIN_LOAD_SEMAPHORE.Release();
|
PLUGIN_LOAD_SEMAPHORE.Release();
|
||||||
@ -166,208 +197,73 @@ public static partial class PluginFactory
|
|||||||
// =========================================================
|
// =========================================================
|
||||||
//
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// Enterprise configuration plugins which are deployed but could not be loaded count as
|
||||||
|
// present: they were not removed, so everything they manage must stay as it is. Otherwise,
|
||||||
|
// one broken configuration plugin would wipe the entire organization configuration:
|
||||||
|
//
|
||||||
|
var deployedEnterpriseConfigPluginIds = GetDeployedEnterpriseConfigPluginIds();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test configurations manage settings and objects like a deployed configuration, so those must
|
||||||
|
// not be treated as left over while the test runs. They are only ever loaded, never merely
|
||||||
|
// present: the test directory is emptied on every start.
|
||||||
|
//
|
||||||
|
foreach (var testConfigurationPlugin in AVAILABLE_PLUGINS.Where(plugin => plugin.Type is PluginType.CONFIGURATION && IsEnterpriseTestConfigurationPath(plugin.LocalPath)))
|
||||||
|
deployedEnterpriseConfigPluginIds.Add(testConfigurationPlugin.Id);
|
||||||
|
|
||||||
|
var unloadedEnterpriseConfigPluginIds = deployedEnterpriseConfigPluginIds.Where(x => AVAILABLE_PLUGINS.All(plugin => plugin.Id != x)).ToList();
|
||||||
|
foreach (var unloadedEnterpriseConfigPluginId in unloadedEnterpriseConfigPluginIds)
|
||||||
|
LOG.LogWarning($"The configuration plugin '{unloadedEnterpriseConfigPluginId}' is deployed, but was not loaded. Everything it manages stays unchanged, because the plugin was not removed. Please check the errors above and fix the plugin.");
|
||||||
|
|
||||||
// Check LLM providers:
|
// Check LLM providers:
|
||||||
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.LLM_PROVIDER);
|
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||||
|
|
||||||
// Check transcription providers:
|
// Check transcription providers:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check embedding providers:
|
// Check embedding providers:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check data sources:
|
// Check data sources:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check chat templates:
|
// Check chat templates:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check profiles:
|
// Check profiles:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, configObjectList))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check document analysis policies:
|
// Check document analysis policies:
|
||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check left-over mandatory info acceptances:
|
// Check left-over mandatory info acceptances:
|
||||||
if (SettingsManagerAccess.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
|
if (SettingsManagerAccess.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for a preselected provider:
|
// Check all managed settings, i.e. settings which a configuration plugin can lock,
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
|
// provide as an editable default, or contribute to:
|
||||||
|
if(ManagedConfiguration.CleanupLeftOverManagedConfigurations(AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for a preselected profile:
|
//
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
|
// The enterprise approvals of all configuration plugins add up. Now that every plugin has
|
||||||
|
// contributed and the clean-up above has dropped the removed ones, we rebuild the effective
|
||||||
|
// list. We skip that while a configuration plugin is deployed but could not be loaded: its
|
||||||
|
// approvals are missing from the contributions, and withdrawing them would demand a new
|
||||||
|
// security audit for assistant plugins the organization has approved:
|
||||||
|
//
|
||||||
|
if(unloadedEnterpriseConfigPluginIds.Count == 0 && PluginConfiguration.RefreshEnterpriseApprovedAssistantPlugins())
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for preselected chat options:
|
// Compatibility shim, see documentation/compatibility-shims/2026-08-orphaned-config-locks.md (remove after 2027-08-06):
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectOptions, AVAILABLE_PLUGINS))
|
if (RepairLegacyConfigOnlySettings(unloadedEnterpriseConfigPluginIds.Count > 0))
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedChatTemplate, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesDisabled, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesAutomaticValidation, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourceIds, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.SendToChatDataSourceBehavior, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the update interval:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the update installation method:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the start page:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the built-in introduction visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowIntroduction, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the quick start guide visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the last changelog visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowLastChangelog, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the vision panel visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowVision, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for users allowed to added providers:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for admin settings visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowAdminSettings, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for preview visibility:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for enabled preview features:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsPluginContributionLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the transcription provider:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for hidden assistants:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HiddenAssistants, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the voice recording shortcut:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for the external HTTP client timeout:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HttpClientTimeoutSeconds, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check for custom root certificates for external HTTP requests:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check provider confidence settings:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.GlobalMinimumConfidence, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.ShowProviderConfidence, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.ConfidenceScheme, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.CustomConfidenceScheme, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check data source security settings:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.DataSourceSecurity, x => x.TrustedProviderIds, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check data source selection agent settings:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentDataSourceSelection, x => x.PreselectAgentOptions, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentDataSourceSelection, x => x.PreselectedAgentProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check retrieval context validation agent settings:
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.EnableRetrievalContextValidation, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.PreselectAgentOptions, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.PreselectedAgentProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.NumParallelValidations, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check if audit is required before it can be activated
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Register new preselected provider for the security audit
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.PreselectedAgentProvider, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Change the minimum required audit level that is required for the allowance of assistants
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.MinimumLevel, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check if external plugins are strictly forbidden, when the minimum audit level is fell below
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.BlockActivationBelowMinimum, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check if security audits are invoked automatically and transparent for the user
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.AutomaticallyAuditAssistants, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
|
||||||
|
|
||||||
// Check enterprise-managed assistant plugin approvals
|
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, AVAILABLE_PLUGINS))
|
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
if (wasConfigurationChanged)
|
if (wasConfigurationChanged)
|
||||||
@ -377,7 +273,49 @@ public static partial class PluginFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<PluginBase> Load(string? pluginPath, string code, CancellationToken cancellationToken = default)
|
/// <summary>
|
||||||
|
/// Determines the IDs of all configuration plugins which an organization deployed on this machine.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Local configuration plugins are not part of this: they belong to the user, not to an
|
||||||
|
/// organization, and they can live in any directory below the plugins root.<br/><br/>
|
||||||
|
/// We read these IDs from the file system instead of taking them from the loaded plugins. A
|
||||||
|
/// configuration plugin might be present but not loadable, e.g. due to invalid Lua code, a
|
||||||
|
/// missing `plugin.lua`, or an incomplete download. Such a plugin still manages this AI Studio
|
||||||
|
/// instance, so we must not treat its settings as left over. Configuration plugins deployed by a
|
||||||
|
/// configuration server live in a directory named after their ID, which is the only information
|
||||||
|
/// left when the plugin itself cannot be read.
|
||||||
|
/// </remarks>
|
||||||
|
private static HashSet<Guid> GetDeployedEnterpriseConfigPluginIds()
|
||||||
|
{
|
||||||
|
var deployedEnterpriseConfigPluginIds = new HashSet<Guid>();
|
||||||
|
if (!Directory.Exists(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
return deployedEnterpriseConfigPluginIds;
|
||||||
|
|
||||||
|
foreach (var configPluginDirectory in Directory.EnumerateDirectories(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(Path.GetFileName(configPluginDirectory), out var configPluginId) || configPluginId == Guid.Empty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// An empty directory is a left-over of a removed plugin, not a deployed plugin:
|
||||||
|
if (!Directory.EnumerateFileSystemEntries(configPluginDirectory).Any())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
deployedEnterpriseConfigPluginIds.Add(configPluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployedEnterpriseConfigPluginIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
|
||||||
|
/// <param name="code">The Lua code of the plugin's main file.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for running the Lua code.</param>
|
||||||
|
/// <param name="allowedBaseDirectory">
|
||||||
|
/// The directory the plugin path must be nested in. Without it, the installed plugins directory
|
||||||
|
/// is used. Validating a plugin before its installation needs this, because the plugin lives in
|
||||||
|
/// a staging directory at that point and could not load any of its own Lua modules otherwise.
|
||||||
|
/// </param>
|
||||||
|
public static async Task<PluginBase> Load(string? pluginPath, string code, CancellationToken cancellationToken = default, string? allowedBaseDirectory = null)
|
||||||
{
|
{
|
||||||
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
|
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
|
||||||
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
|
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
|
||||||
@ -386,7 +324,7 @@ public static partial class PluginFactory
|
|||||||
if (!string.IsNullOrWhiteSpace(pluginPath))
|
if (!string.IsNullOrWhiteSpace(pluginPath))
|
||||||
{
|
{
|
||||||
// Add the module loader so that the plugin can load other Lua modules:
|
// Add the module loader so that the plugin can load other Lua modules:
|
||||||
state.ModuleLoader = new PluginLoader(pluginPath);
|
state.ModuleLoader = new PluginLoader(pluginPath, allowedBaseDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some useful libraries:
|
// Add some useful libraries:
|
||||||
@ -420,7 +358,10 @@ public static partial class PluginFactory
|
|||||||
if(type is PluginType.NONE)
|
if(type is PluginType.NONE)
|
||||||
return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues<PluginType>()}");
|
return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues<PluginType>()}");
|
||||||
|
|
||||||
var isInternal = !string.IsNullOrWhiteSpace(pluginPath) && pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
|
// Whether a plugin is internal is decided by its path, never by the plugin itself. We use the
|
||||||
|
// same nesting check as everywhere else, so that a directory like `.internal-old` next to the
|
||||||
|
// internal plugins does not count as internal:
|
||||||
|
var isInternal = IsPathInside(INTERNAL_PLUGINS_ROOT, pluginPath);
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case PluginType.LANGUAGE:
|
case PluginType.LANGUAGE:
|
||||||
@ -444,4 +385,111 @@ public static partial class PluginFactory
|
|||||||
return new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.");
|
return new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// =========================================================
|
||||||
|
// Compatibility shim. Please read the related document
|
||||||
|
// before you change anything here:
|
||||||
|
//
|
||||||
|
// documentation/compatibility-shims/2026-08-orphaned-config-locks.md
|
||||||
|
//
|
||||||
|
// Remove after 2027-08-06. Everything from here down to the
|
||||||
|
// end of this file belongs to the shim and can be deleted
|
||||||
|
// in one piece.
|
||||||
|
// =========================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repairs settings that were configured by a configuration plugin which was removed before
|
||||||
|
/// AI Studio started to persist the configuration ownership.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// All settings listed here share two properties: a configuration plugin can set them, and
|
||||||
|
/// there is no user interface to change them back. Therefore, any value that differs from the
|
||||||
|
/// default must originate from a configuration plugin. When such a setting is not managed
|
||||||
|
/// anymore, its plugin is gone and we restore the default value.<br/><br/>
|
||||||
|
/// This is only valid as long as none of these settings gets a user interface. When you add
|
||||||
|
/// one, remove the setting from this method and from the shim's document.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hasUnloadedConfigPlugins" >
|
||||||
|
/// True when at least one configuration plugin is deployed but could not be loaded. In that case,
|
||||||
|
/// we cannot tell whether a value comes from that plugin or from a removed one, so we repair
|
||||||
|
/// nothing at all.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>True when at least one setting was repaired, otherwise false.</returns>
|
||||||
|
private static bool RepairLegacyConfigOnlySettings(bool hasUnloadedConfigPlugins)
|
||||||
|
{
|
||||||
|
if (hasUnloadedConfigPlugins)
|
||||||
|
{
|
||||||
|
LOG.LogWarning("Skipping the repair of configuration-only settings: at least one configuration plugin is deployed, but could not be loaded. We try again the next time AI Studio starts.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = SettingsManagerAccess.ConfigurationData;
|
||||||
|
var wasRepaired = false;
|
||||||
|
|
||||||
|
// Settings which are enabled by default and which only a configuration plugin can switch off:
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowIntroduction, data.App.ShowIntroduction);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowQuickStartGuide, data.App.ShowQuickStartGuide);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowLastChangelog, data.App.ShowLastChangelog);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.ShowVision, data.App.ShowVision);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToAddProvider, data.App.AllowUserToAddProvider);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToImportPlugins, data.App.AllowUserToImportPlugins);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyFlag(x => x.App, x => x.AllowUserToSharePlugins, data.App.AllowUserToSharePlugins);
|
||||||
|
|
||||||
|
// Collections which stay empty unless a configuration plugin fills them:
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.App, x => x.HiddenAssistants, data.App.HiddenAssistants.Count);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.DataSourceSecurity, x => x.TrustedProviderIds, data.DataSourceSecurity.TrustedProviderIds.Count);
|
||||||
|
wasRepaired |= RepairLegacyConfigOnlyCollection(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, data.AssistantPluginAudit.EnterpriseApprovedPlugins.Count);
|
||||||
|
|
||||||
|
return wasRepaired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the default of a boolean setting when it is switched off without being managed.
|
||||||
|
/// </summary>
|
||||||
|
private static bool RepairLegacyConfigOnlyFlag<TClass>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, bool>> propertyExpression, bool currentValue)
|
||||||
|
{
|
||||||
|
if (currentValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was switched off by a configuration plugin which is not available anymore.");
|
||||||
|
configMeta.ResetLockedConfiguration();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears a set-based setting when it contains entries without being managed.
|
||||||
|
/// </summary>
|
||||||
|
private static bool RepairLegacyConfigOnlyCollection<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, ISet<TValue>>> propertyExpression, int currentCount)
|
||||||
|
{
|
||||||
|
if (currentCount is 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was filled by a configuration plugin which is not available anymore.");
|
||||||
|
configMeta.ResetLockedConfiguration();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears a list-based setting when it contains entries without being managed.
|
||||||
|
/// </summary>
|
||||||
|
private static bool RepairLegacyConfigOnlyCollection<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, IList<TValue>>> propertyExpression, int currentCount)
|
||||||
|
{
|
||||||
|
if (currentCount is 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ManagedConfiguration.TryGet(configSelection, propertyExpression, out var configMeta) || configMeta.ManagedMode is not null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
LOG.LogWarning($"Repairing the setting '{configMeta.SettingName}': it was filled by a configuration plugin which is not available anymore.");
|
||||||
|
configMeta.ResetLockedConfiguration();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,129 +1,90 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
public static partial class PluginFactory
|
public static partial class PluginFactory
|
||||||
{
|
{
|
||||||
private const string REASON_NO_LONGER_REFERENCED = "no longer referenced by active enterprise environments";
|
private const string REASON_NO_LONGER_REFERENCED = "no longer referenced by active enterprise environments";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the configuration plugins an organization deployed once but does not reference anymore.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is how an organization withdraws a configuration: it removes the configuration ID from the
|
||||||
|
/// devices, e.g. through a group policy. The next time AI Studio syncs, the local copy has to go.
|
||||||
|
/// A device which was offline while the policy changed applies the withdrawal when it starts again.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// What an organization deployed is decided by the plugin path alone. We must not ask the plugin
|
||||||
|
/// itself: `DEPLOYED_USING_CONFIG_SERVER` is part of the plugin, so a configuration declaring
|
||||||
|
/// `false` could never be withdrawn again once it was deployed, while it would keep every right of
|
||||||
|
/// an organization configuration, including the approval of assistant plugins.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="activeConfigurationIds">The IDs of the enterprise configurations which are currently referenced.</param>
|
||||||
public static void RemoveUnreferencedManagedConfigurationPlugins(ISet<Guid> activeConfigurationIds)
|
public static void RemoveUnreferencedManagedConfigurationPlugins(ISet<Guid> activeConfigurationIds)
|
||||||
{
|
{
|
||||||
if (!IsInitialized)
|
if (!IsInitialized || !Directory.Exists(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var pluginIdsToRemove = new HashSet<Guid>();
|
foreach (var configurationDirectory in Directory.EnumerateDirectories(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
|
||||||
// Case 1: Plugins are already loaded and metadata is available.
|
|
||||||
foreach (var plugin in AVAILABLE_PLUGINS.Where(plugin =>
|
|
||||||
plugin.Type is PluginType.CONFIGURATION &&
|
|
||||||
plugin.IsManagedByConfigServer &&
|
|
||||||
!activeConfigurationIds.Contains(plugin.Id)))
|
|
||||||
pluginIdsToRemove.Add(plugin.Id);
|
|
||||||
|
|
||||||
// Case 2: Startup cleanup before the initial plugin load.
|
|
||||||
// In this case, we inspect the .config directories directly.
|
|
||||||
if (Directory.Exists(CONFIGURATION_PLUGINS_ROOT))
|
|
||||||
{
|
{
|
||||||
foreach (var pluginDirectory in Directory.EnumerateDirectories(CONFIGURATION_PLUGINS_ROOT))
|
var directoryName = Path.GetFileName(configurationDirectory);
|
||||||
{
|
|
||||||
var directoryName = Path.GetFileName(pluginDirectory);
|
// A download in flight stages and backs up next to the configuration directories. Those
|
||||||
if (!Guid.TryParse(directoryName, out var pluginId))
|
// directories belong to a running update, not to a withdrawn configuration:
|
||||||
|
if (IsTransientDownloadDirectory(directoryName))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (activeConfigurationIds.Contains(pluginId))
|
//
|
||||||
|
// A configuration server downloads each configuration into a directory named after its
|
||||||
|
// ID. Any other directory name cannot be referenced by an enterprise environment, so it
|
||||||
|
// has no place here either:
|
||||||
|
//
|
||||||
|
if (Guid.TryParse(directoryName, out var configurationId) && activeConfigurationIds.Contains(configurationId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var deployFlag = ReadDeployFlagFromPluginFile(pluginDirectory);
|
RemoveConfigurationDirectory(configurationDirectory, REASON_NO_LONGER_REFERENCED);
|
||||||
var isManagedByConfigServer = deployFlag ?? true;
|
|
||||||
if (!deployFlag.HasValue)
|
|
||||||
LOG.LogWarning($"Configuration plugin '{pluginId}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{CONFIGURATION_PLUGINS_ROOT}'.");
|
|
||||||
|
|
||||||
if (isManagedByConfigServer)
|
|
||||||
pluginIdsToRemove.Add(pluginId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var pluginId in pluginIdsToRemove)
|
/// <summary>
|
||||||
RemovePluginAsync(pluginId, REASON_NO_LONGER_REFERENCED);
|
/// Checks whether a directory below the enterprise configuration directory belongs to a running
|
||||||
}
|
/// download instead of to an installed configuration.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsTransientDownloadDirectory(string directoryName) =>
|
||||||
|
directoryName.Contains(".staging-", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
directoryName.Contains(".backup-", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private static void RemovePluginAsync(Guid pluginId, string reason)
|
/// <summary>
|
||||||
|
/// Unloads every plugin stored in the given directory and deletes the directory afterwards.
|
||||||
|
/// </summary>
|
||||||
|
private static void RemoveConfigurationDirectory(string configurationDirectory, string reason)
|
||||||
{
|
{
|
||||||
if (!IsInitialized)
|
LOG.LogWarning("Removing the enterprise configuration directory '{Directory}'. Reason: {Reason}.", configurationDirectory, reason);
|
||||||
return;
|
|
||||||
|
|
||||||
LOG.LogWarning("Removing plugin with ID '{PluginId}'. Reason: {Reason}.", pluginId, reason);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Remove the plugin from the available plugins list:
|
// We collect the plugins by path, not by the ID the directory is named after: a plugin may
|
||||||
|
// declare an ID which differs from its directory name, and a single directory may even hold
|
||||||
|
// several plugins:
|
||||||
//
|
//
|
||||||
var availablePluginToRemove = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
foreach (var plugin in AVAILABLE_PLUGINS.Where(plugin => IsPathInside(configurationDirectory, plugin.LocalPath)).ToList())
|
||||||
if (availablePluginToRemove != null)
|
{
|
||||||
AVAILABLE_PLUGINS.Remove(availablePluginToRemove);
|
AVAILABLE_PLUGINS.Remove(plugin);
|
||||||
else
|
|
||||||
LOG.LogWarning("No available plugin found with ID '{PluginId}' while removing plugin. Reason: {Reason}.", pluginId, reason);
|
|
||||||
|
|
||||||
//
|
if (RUNNING_PLUGINS.FirstOrDefault(runningPlugin => runningPlugin.Id == plugin.Id) is { } runningPluginToRemove)
|
||||||
// Remove the plugin from the running plugins list:
|
|
||||||
//
|
|
||||||
var runningPluginToRemove = RUNNING_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
|
||||||
if (runningPluginToRemove == null)
|
|
||||||
LOG.LogWarning("No running plugin found with ID '{PluginId}' while removing plugin. Reason: {Reason}.", pluginId, reason);
|
|
||||||
else
|
|
||||||
RUNNING_PLUGINS.Remove(runningPluginToRemove);
|
RUNNING_PLUGINS.Remove(runningPluginToRemove);
|
||||||
|
|
||||||
//
|
LOG.LogInformation("Unloaded the plugin '{PluginName}' ({PluginId}). Reason: {Reason}.", plugin.Name, plugin.Id, reason);
|
||||||
// Delete the plugin directory:
|
|
||||||
//
|
|
||||||
DeleteConfigurationPluginDirectory(pluginId);
|
|
||||||
|
|
||||||
LOG.LogInformation("Plugin with ID '{PluginId}' removed successfully. Reason: {Reason}.", pluginId, reason);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool? ReadDeployFlagFromPluginFile(string pluginDirectory)
|
if (!Directory.Exists(configurationDirectory))
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var pluginFile = Path.Join(pluginDirectory, "plugin.lua");
|
|
||||||
if (!File.Exists(pluginFile))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var pluginCode = File.ReadAllText(pluginFile);
|
|
||||||
var match = DeployedByConfigServerRegex().Match(pluginCode);
|
|
||||||
if (!match.Success)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return bool.TryParse(match.Groups[1].Value, out var deployFlag)
|
|
||||||
? deployFlag
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LOG.LogWarning(ex, $"Failed to parse deployment flag from plugin directory '{pluginDirectory}'.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DeleteConfigurationPluginDirectory(Guid pluginId)
|
|
||||||
{
|
|
||||||
var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, pluginId.ToString());
|
|
||||||
if (!Directory.Exists(pluginDirectory))
|
|
||||||
{
|
|
||||||
LOG.LogWarning($"Plugin directory '{pluginDirectory}' does not exist.");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(pluginDirectory, true);
|
Directory.Delete(configurationDirectory, true);
|
||||||
LOG.LogInformation($"Plugin directory '{pluginDirectory}' deleted successfully.");
|
LOG.LogInformation($"Plugin directory '{configurationDirectory}' deleted successfully.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
LOG.LogError(ex, $"Failed to delete plugin directory '{pluginDirectory}'.");
|
LOG.LogError(e, $"Failed to delete plugin directory '{configurationDirectory}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"^\s*DEPLOYED_USING_CONFIG_SERVER\s*=\s*(true|false)\s*(?:--.*)?$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
|
|
||||||
private static partial Regex DeployedByConfigServerRegex();
|
|
||||||
}
|
}
|
||||||
@ -52,9 +52,25 @@ public static partial class PluginFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Iterate over all available plugins and try to start them.
|
// Iterate over all available plugins and try to start them. We do that in a deterministic
|
||||||
|
// order, starting with the configuration plugins of the organization. Three reasons:
|
||||||
//
|
//
|
||||||
foreach (var availablePlugin in AVAILABLE_PLUGINS)
|
// - Configuration plugins write settings and configuration objects. Whoever writes one
|
||||||
|
// first owns it, so the organization has to come first: its configuration is the baseline
|
||||||
|
// every other plugin has to respect.
|
||||||
|
//
|
||||||
|
// - Within one origin, the declared priority decides. An organization can deploy a base
|
||||||
|
// configuration for everybody and refine it, e.g. per department: the higher priority is
|
||||||
|
// applied later and therefore wins.
|
||||||
|
//
|
||||||
|
// - Without an explicit order, the sequence is the one Directory.EnumerateFiles produced in
|
||||||
|
// LoadAll. That order is not guaranteed, so the same installation could behave
|
||||||
|
// differently on two machines. The plugin directory breaks any remaining tie.
|
||||||
|
//
|
||||||
|
foreach (var availablePlugin in AVAILABLE_PLUGINS
|
||||||
|
.OrderBy(GetStartupRank)
|
||||||
|
.ThenBy(plugin => plugin.ConfigurationPriority)
|
||||||
|
.ThenBy(plugin => plugin.LocalPath, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if(cancellationToken.IsCancellationRequested)
|
if(cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -89,19 +105,51 @@ public static partial class PluginFactory
|
|||||||
return configObjects;
|
return configObjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the position of a plugin in the startup sequence. Plugins with a lower rank start earlier.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The configuration plugins an organization deployed go first: they are the baseline for
|
||||||
|
/// everything else. A test configuration follows, so that an administrator sees their draft take
|
||||||
|
/// effect over the deployed baseline. Local configuration plugins come last, so they can add to
|
||||||
|
/// that baseline instead of replacing parts of it. All remaining plugin types write no settings at
|
||||||
|
/// all, so their rank is irrelevant for the outcome.<br/><br/>
|
||||||
|
/// The rank comes before the declared priority on purpose: a local configuration plugin must not
|
||||||
|
/// be able to jump ahead of an organization by declaring a high priority.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="plugin">The plugin about to be started.</param>
|
||||||
|
/// <returns>The startup rank of the plugin.</returns>
|
||||||
|
private static int GetStartupRank(IAvailablePlugin plugin) => plugin.Type switch
|
||||||
|
{
|
||||||
|
PluginType.CONFIGURATION when IsEnterpriseConfigurationPath(plugin.LocalPath) => 0,
|
||||||
|
PluginType.CONFIGURATION when IsEnterpriseTestConfigurationPath(plugin.LocalPath) => 1,
|
||||||
|
PluginType.CONFIGURATION => 2,
|
||||||
|
|
||||||
|
_ => 3,
|
||||||
|
};
|
||||||
|
|
||||||
private static void LogAssistantPluginStartupState()
|
private static void LogAssistantPluginStartupState()
|
||||||
{
|
{
|
||||||
ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta);
|
ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta);
|
||||||
var approvedByConfigPluginId = configMeta is { IsLocked: true } ? configMeta.LockedByConfigPluginId : Guid.Empty;
|
|
||||||
var approvedByConfigPluginName = approvedByConfigPluginId == Guid.Empty
|
|
||||||
? string.Empty
|
|
||||||
: AVAILABLE_PLUGINS.FirstOrDefault(x => x.Id == approvedByConfigPluginId)?.Name ?? string.Empty;
|
|
||||||
|
|
||||||
foreach (var assistantPlugin in RUNNING_PLUGINS.OfType<PluginAssistants>())
|
foreach (var assistantPlugin in RUNNING_PLUGINS.OfType<PluginAssistants>())
|
||||||
{
|
{
|
||||||
var securityState = PluginAssistantSecurityResolver.Resolve(SettingsManagerAccess, assistantPlugin);
|
var securityState = PluginAssistantSecurityResolver.Resolve(SettingsManagerAccess, assistantPlugin);
|
||||||
if (securityState.IsEnterpriseApproved)
|
if (securityState.IsEnterpriseApproved)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Several configuration plugins may approve assistant plugins. We look up the one
|
||||||
|
// which approved this particular plugin instead of naming an arbitrary contributor:
|
||||||
|
//
|
||||||
|
var approvedByConfigPluginId = configMeta.PluginContributions
|
||||||
|
.Where(contribution => contribution.Value.Any(approval => string.Equals(approval.PluginHash, securityState.CurrentHash, StringComparison.Ordinal)))
|
||||||
|
.Select(contribution => contribution.Key)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var approvedByConfigPluginName = approvedByConfigPluginId == Guid.Empty
|
||||||
|
? string.Empty
|
||||||
|
: AVAILABLE_PLUGINS.FirstOrDefault(x => x.Id == approvedByConfigPluginId)?.Name ?? string.Empty;
|
||||||
|
|
||||||
LOG.LogInformation(
|
LOG.LogInformation(
|
||||||
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}', SecuritySource='EnterpriseApproval', ApprovedByConfigPluginId='{approvedByConfigPluginId}', ApprovedByConfigPluginName='{approvedByConfigPluginName}'");
|
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}', SecuritySource='EnterpriseApproval', ApprovedByConfigPluginId='{approvedByConfigPluginId}', ApprovedByConfigPluginName='{approvedByConfigPluginName}'");
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -11,10 +11,41 @@ public static partial class PluginFactory
|
|||||||
private static string DATA_DIR = string.Empty;
|
private static string DATA_DIR = string.Empty;
|
||||||
private static string PLUGINS_ROOT = string.Empty;
|
private static string PLUGINS_ROOT = string.Empty;
|
||||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||||
private static string CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directory the config server downloads the configuration plugins of an organization into.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is not the home of configuration plugins in general: a local configuration plugin can
|
||||||
|
/// live in any directory below the plugins root. Only the IT department of an organization
|
||||||
|
/// deploys plugins here, each in a directory named after its configuration ID.
|
||||||
|
/// </remarks>
|
||||||
|
private static string ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directory administrators use to try out a configuration before their organization deploys it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Everything stored here acts on behalf of the organization, so that a test behaves like the
|
||||||
|
/// later rollout, including the approval of assistant plugins. In exchange, the directory is
|
||||||
|
/// emptied on every start: a test configuration lives for one session only. It also never gets
|
||||||
|
/// the protection of a deployed configuration, so users can remove or replace it through the user
|
||||||
|
/// interface.
|
||||||
|
/// </remarks>
|
||||||
|
private static string ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||||
|
|
||||||
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
||||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many test configurations were removed while AI Studio was starting.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The user interface reports this: an administrator who placed a test configuration and restarted
|
||||||
|
/// AI Studio would otherwise face an empty directory without any explanation.
|
||||||
|
/// </remarks>
|
||||||
|
public static int RemovedTestConfigurationsAtStartup { get; private set; }
|
||||||
|
|
||||||
public static ILanguagePlugin BaseLanguage { get; private set; } = NoPluginLanguage.INSTANCE;
|
public static ILanguagePlugin BaseLanguage { get; private set; } = NoPluginLanguage.INSTANCE;
|
||||||
|
|
||||||
public static bool IsInitialized { get; private set; }
|
public static bool IsInitialized { get; private set; }
|
||||||
@ -65,17 +96,200 @@ public static partial class PluginFactory
|
|||||||
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||||
HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock");
|
HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock");
|
||||||
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||||
CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
|
ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config");
|
||||||
|
ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config-tests");
|
||||||
|
|
||||||
if (!Directory.Exists(PLUGINS_ROOT))
|
if (!Directory.Exists(PLUGINS_ROOT))
|
||||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||||
|
|
||||||
|
ClearTestConfigurationPlugins();
|
||||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||||
IsInitialized = true;
|
IsInitialized = true;
|
||||||
LOG.LogInformation("Plugin factory initialized successfully.");
|
LOG.LogInformation("Plugin factory initialized successfully.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin directory belongs to the enterprise configuration area.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only the IT department of an organization deploys plugins there: the config server downloads
|
||||||
|
/// them into a directory named after their configuration ID. We decide by path on purpose. The
|
||||||
|
/// Lua field DEPLOYED_USING_CONFIG_SERVER is self-declared, so any plugin could claim to be
|
||||||
|
/// deployed by an organization.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||||
|
/// <returns>True when the directory is nested in the enterprise configuration directory.</returns>
|
||||||
|
public static bool IsEnterpriseConfigurationPath(string? pluginPath) => IsPathInside(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, pluginPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin directory belongs to the test configuration area.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||||
|
/// <returns>True when the directory is nested in the test configuration directory.</returns>
|
||||||
|
public static bool IsEnterpriseTestConfigurationPath(string? pluginPath) => IsPathInside(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT, pluginPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin acts on behalf of an organization, either deployed by a configuration
|
||||||
|
/// server or staged for a test.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Use this wherever a configuration speaks for the organization, e.g. when it approves assistant
|
||||||
|
/// plugins or claims a setting against a local configuration plugin. Do not use it where a
|
||||||
|
/// deployed configuration is protected against the user, e.g. against deletion: an administrator
|
||||||
|
/// must be able to get rid of their own test configuration.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||||
|
/// <returns>True when the directory belongs to the enterprise or the test configuration area.</returns>
|
||||||
|
public static bool IsOrganizationConfigurationPath(string? pluginPath) => IsEnterpriseConfigurationPath(pluginPath) || IsEnterpriseTestConfigurationPath(pluginPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ranks how much say a configuration plugin has, based on where it is stored. The higher rank
|
||||||
|
/// wins when two configuration plugins claim the same plugin ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A test configuration outranks a deployed one on purpose: an administrator tries out the next
|
||||||
|
/// version of a configuration under the ID it will have later. Local configuration plugins rank
|
||||||
|
/// lowest, so nobody can push aside what an organization deployed.
|
||||||
|
/// </remarks>
|
||||||
|
private static int GetConfigurationAuthority(string? pluginPath)
|
||||||
|
{
|
||||||
|
if (IsEnterpriseTestConfigurationPath(pluginPath))
|
||||||
|
return 2;
|
||||||
|
|
||||||
|
return IsEnterpriseConfigurationPath(pluginPath) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empties the test configuration directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A test configuration carries the rights of an organization configuration without anybody having
|
||||||
|
/// deployed it. It must therefore never outlive the session it was placed in, and administrators
|
||||||
|
/// get a predictable lifetime instead of a configuration which is swept away at some point.
|
||||||
|
/// </remarks>
|
||||||
|
private static void ClearTestConfigurationPlugins()
|
||||||
|
{
|
||||||
|
RemovedTestConfigurationsAtStartup = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
{
|
||||||
|
var removedTestConfigurations = Directory.EnumerateDirectories(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT).Count();
|
||||||
|
Directory.Delete(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT, true);
|
||||||
|
RemovedTestConfigurationsAtStartup = removedTestConfigurations;
|
||||||
|
|
||||||
|
if (removedTestConfigurations > 0)
|
||||||
|
LOG.LogWarning($"Removed {removedTestConfigurations} test configuration(s) from '{ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT}'. Test configurations are valid for one session only.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOG.LogError(e, $"Failed to empty the test configuration directory '{ENTERPRISE_TEST_CONFIGURATION_PLUGINS_ROOT}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin directory is stored below the plugins directory of AI Studio.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Everything that removes or replaces plugin files checks this first, so a plugin directory
|
||||||
|
/// which points somewhere else can never be touched.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||||
|
/// <returns>True when the directory is nested in the plugins directory.</returns>
|
||||||
|
public static bool IsInsidePluginsRoot(string? pluginPath) => IsPathInside(PLUGINS_ROOT, pluginPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin directory is the plugins directory itself.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A `plugin.lua` placed directly in the plugins directory makes that directory the plugin
|
||||||
|
/// directory. Removing or replacing such a plugin means touching its directory, which would take
|
||||||
|
/// every other plugin with it.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pluginPath">The directory of the plugin.</param>
|
||||||
|
/// <returns>True when the directory is the plugins directory.</returns>
|
||||||
|
public static bool IsPluginsRoot(string? pluginPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(PLUGINS_ROOT))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var root = Path.GetFullPath(PLUGINS_ROOT).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
return string.Equals(root, pluginDirectory, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' is the plugins directory. Treating it as the plugins directory.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPathInside(string rootDirectory, string? pluginPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(rootDirectory))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var root = Path.GetFullPath(rootDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
return pluginDirectory.StartsWith(root, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' is nested in '{rootDirectory}'. Treating it as unrelated.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a configuration plugin was deployed by the IT department of an organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A plugin which is deployed but could not be loaded still counts: it might be broken, e.g. due
|
||||||
|
/// to invalid Lua code or an incomplete download, but it was not removed. Everything it manages
|
||||||
|
/// stays under the control of the organization until the plugin is gone for good.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="configPluginId">The ID of the configuration plugin.</param>
|
||||||
|
/// <returns>True when the plugin belongs to an organization, false when it is local or unknown.</returns>
|
||||||
|
public static bool IsEnterpriseConfigurationPlugin(Guid configPluginId)
|
||||||
|
{
|
||||||
|
if (configPluginId == Guid.Empty || !IsInitialized)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(plugin.LocalPath)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return Directory.Exists(Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPluginId.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a configuration plugin speaks for an organization: either deployed by its IT
|
||||||
|
/// department, or staged as a test configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A test configuration is only ever loaded, never merely present: it is emptied on every start,
|
||||||
|
/// so there is no unloadable leftover to account for.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="configPluginId">The ID of the configuration plugin.</param>
|
||||||
|
/// <returns>True when the plugin speaks for an organization, false when it is local or unknown.</returns>
|
||||||
|
public static bool IsOrganizationConfigurationPlugin(Guid configPluginId)
|
||||||
|
{
|
||||||
|
if (configPluginId == Guid.Empty || !IsInitialized)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsEnterpriseConfigurationPlugin(configPluginId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseTestConfigurationPath(plugin.LocalPath));
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task LockHotReloadAsync()
|
private static async Task LockHotReloadAsync()
|
||||||
{
|
{
|
||||||
if (!IsInitialized)
|
if (!IsInitialized)
|
||||||
|
|||||||
@ -14,10 +14,17 @@ namespace AIStudio.Tools.PluginSystem;
|
|||||||
/// Loading other modules outside the plugin directory is not allowed.
|
/// Loading other modules outside the plugin directory is not allowed.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="pluginDirectory">The directory where the plugin is located.</param>
|
/// <param name="pluginDirectory">The directory where the plugin is located.</param>
|
||||||
public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader
|
/// <param name="allowedBaseDirectory">
|
||||||
|
/// The directory the plugin directory must be nested in. Without it, the installed plugins directory
|
||||||
|
/// is used. Validating a plugin before its installation needs this, because the plugin is not
|
||||||
|
/// installed yet and lives in a staging directory outside the installed plugins directory.
|
||||||
|
/// </param>
|
||||||
|
public sealed class PluginLoader(string pluginDirectory, string? allowedBaseDirectory = null) : ILuaModuleLoader
|
||||||
{
|
{
|
||||||
private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins");
|
private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins");
|
||||||
|
|
||||||
|
private readonly string baseDirectory = string.IsNullOrWhiteSpace(allowedBaseDirectory) ? PLUGIN_BASE_PATH : allowedBaseDirectory;
|
||||||
|
|
||||||
#region Implementation of ILuaModuleLoader
|
#region Implementation of ILuaModuleLoader
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -27,8 +34,8 @@ public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader
|
|||||||
if (moduleName.Contains("..") || pluginDirectory.Contains(".."))
|
if (moduleName.Contains("..") || pluginDirectory.Contains(".."))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Ensure that the plugin directory is nested in the plugin base path:
|
// Ensure that the plugin directory is nested in the allowed base directory:
|
||||||
if (!pluginDirectory.StartsWith(PLUGIN_BASE_PATH, StringComparison.OrdinalIgnoreCase))
|
if (!pluginDirectory.StartsWith(this.baseDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
|
var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isManagedByConfigServer = false, Guid? managedConfigurationId = null) : IAvailablePlugin
|
public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isManagedByConfigServer = false, Guid? managedConfigurationId = null, int configurationPriority = 0) : IAvailablePlugin
|
||||||
{
|
{
|
||||||
#region Implementation of IPluginMetadata
|
#region Implementation of IPluginMetadata
|
||||||
|
|
||||||
@ -56,5 +56,8 @@ public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isM
|
|||||||
|
|
||||||
public Guid? ManagedConfigurationId { get; } = managedConfigurationId;
|
public Guid? ManagedConfigurationId { get; } = managedConfigurationId;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int ConfigurationPriority { get; } = configurationPriority;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
// ReSharper disable MemberCanBePrivate.Global
|
// ReSharper disable MemberCanBePrivate.Global
|
||||||
|
|
||||||
namespace AIStudio.Tools.Rust;
|
namespace AIStudio.Tools.Rust;
|
||||||
@ -81,6 +82,7 @@ public static class FileTypes
|
|||||||
// Other standalone types
|
// Other standalone types
|
||||||
public static readonly FileTypeFilter CERTIFICATE_BUNDLE = FileTypeFilter.Leaf(TB("Certificate bundle"), "pem", "crt", "cer");
|
public static readonly FileTypeFilter CERTIFICATE_BUNDLE = FileTypeFilter.Leaf(TB("Certificate bundle"), "pem", "crt", "cer");
|
||||||
public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage");
|
public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage");
|
||||||
|
public static readonly FileTypeFilter PLUGIN_ARCHIVE = FileTypeFilter.Leaf(TB("Plugin archive"), PluginArchive.PLUGIN_FILE_EXTENSION.TrimStart('.'), "zip");
|
||||||
|
|
||||||
public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types)
|
public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed record AssistantPluginCheckResult(bool Success, Guid PluginId, string PluginName, string Issue);
|
||||||
@ -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);
|
||||||
@ -1,709 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using AIStudio.Settings;
|
|
||||||
using AIStudio.Tools.AssistantSessions;
|
|
||||||
using AIStudio.Tools.Media;
|
|
||||||
using AIStudio.Tools.PluginSystem;
|
|
||||||
using AIStudio.Tools.PluginSystem.Assistants;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
|
||||||
private const string ASSISTANT_BUILDER_DIRECTORY_PREFIX = "assistant-builder";
|
|
||||||
private const string DELETE_BACKUP_DIRECTORY = ".plugin-delete-backups";
|
|
||||||
private const int DIRECTORY_PREFIX_MAX_LEN = 80;
|
|
||||||
|
|
||||||
private readonly ILogger<AssistantPluginInstallService> logger;
|
|
||||||
private readonly SettingsManager settingsManager;
|
|
||||||
private readonly AssistantSessionService assistantSessionService;
|
|
||||||
private readonly MediaTranscriptionService mediaTranscriptionService;
|
|
||||||
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 AssistantPluginCheckResult CheckError(string issue) => new(false, Guid.Empty, string.Empty, issue);
|
|
||||||
|
|
||||||
private static AssistantPluginDeleteResult DeleteError(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, issue);
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.settingsManager = settingsManager;
|
|
||||||
this.assistantSessionService = assistantSessionService;
|
|
||||||
this.mediaTranscriptionService = mediaTranscriptionService;
|
|
||||||
this.logger.LogInformation("The assistant plugin install service has been initialized.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether a local plugin is an Assistant Builder generated assistant that users may delete.
|
|
||||||
/// </summary>
|
|
||||||
public static bool CanDeleteInstalledAssistant(IAvailablePlugin plugin) => string.IsNullOrWhiteSpace(GetAssistantDeletionEligibilityIssue(plugin));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether an assistant still owns running or canceling background work.
|
|
||||||
/// </summary>
|
|
||||||
public bool HasActiveAssistantWork(Guid pluginId)
|
|
||||||
{
|
|
||||||
var instanceId = pluginId.ToString();
|
|
||||||
if (this.assistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && string.Equals(snapshot.Key.InstanceId, instanceId, StringComparison.Ordinal)))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var ownerIdSuffix = $":{instanceId}";
|
|
||||||
return this.mediaTranscriptionService.GetSnapshots().Any(snapshot =>
|
|
||||||
snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.ASSISTANT } &&
|
|
||||||
snapshot.Owner.Id.EndsWith(ownerIdSuffix, StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether generated Lua assistant plugin code can be loaded and installed.
|
|
||||||
/// The plugin is written to a temporary staging directory and validated through the
|
|
||||||
/// normal plugin loader, but it is not moved into the user plugin directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lua">The full generated <c>plugin.lua</c> content.</param>
|
|
||||||
/// <param name="token">A cancellation token for file IO and Lua validation.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Check result that contains success state, plugin metadata, and a user-facing issue when validation failed.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<AssistantPluginCheckResult> CheckInstallabilityAsync(string lua, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
|
||||||
return CheckError(rootIssue);
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
|
||||||
var stagingDirectory = string.Empty;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var validation = await this.ValidateIntoStagingAsync(lua, token);
|
|
||||||
if (!validation.Success || validation.AssistantPlugin is null)
|
|
||||||
return CheckError(validation.Issue);
|
|
||||||
|
|
||||||
stagingDirectory = validation.StagingDirectory;
|
|
||||||
var finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, validation.AssistantPlugin);
|
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
|
||||||
return CheckError(TB("The resolved plugin directory is outside the assistant plugin directory."));
|
|
||||||
|
|
||||||
return new(true, validation.AssistantPlugin.Id, validation.AssistantPlugin.Name, string.Empty);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
|
||||||
this.installSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Installs generated Lua assistant plugin code into the user plugin directory.
|
|
||||||
/// Writes the plugin into a temporary staging directory first, validates it through the
|
|
||||||
/// normal plugin loader, then moves into <c>data/plugins/assistants</c>.
|
|
||||||
/// If plugin with same ID already exists, the existing directory is moved
|
|
||||||
/// aside as backup and restored when replacement fails.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lua">The full generated <c>plugin.lua</c> content.</param>
|
|
||||||
/// <param name="token">A cancellation token for file IO, Lua validation, and plugin reload.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Installation result that contains success state, installed plugin metadata, final directory,
|
|
||||||
/// whether an existing plugin was replaced, and user-facing issue when installation failed.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<AssistantPluginInstallResult> InstallAsync(string lua, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
|
||||||
return Error(rootIssue);
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
|
||||||
AssistantPluginValidationResult validation;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
validation = await this.ValidateIntoStagingAsync(lua, token);
|
|
||||||
if (!validation.Success || validation.AssistantPlugin is null)
|
|
||||||
return Error(validation.Issue);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(assistantPluginsRoot);
|
|
||||||
|
|
||||||
var stagingDirectory = validation.StagingDirectory;
|
|
||||||
var assistantPlugin = validation.AssistantPlugin;
|
|
||||||
string? backupDirectory = null;
|
|
||||||
string? finalDirectory = null;
|
|
||||||
var replacedExisting = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, assistantPlugin);
|
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
|
||||||
return Error(TB("The resolved plugin directory is outside the assistant plugin directory."));
|
|
||||||
|
|
||||||
if (Directory.Exists(finalDirectory))
|
|
||||||
{
|
|
||||||
replacedExisting = true;
|
|
||||||
backupDirectory = Path.Join(assistantPluginsRoot, $".{Path.GetFileName(finalDirectory)}.backup-{Guid.NewGuid():N}");
|
|
||||||
Directory.Move(finalDirectory, backupDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Move(stagingDirectory, finalDirectory);
|
|
||||||
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(backupDirectory, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, $"Failed to delete assistant plugin backup directory '{backupDirectory}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await PluginFactory.LoadAll(token);
|
|
||||||
this.logger.LogInformation($"Installed assistant plugin '{assistantPlugin.Name}' ({assistantPlugin.Id}) to '{finalDirectory}'.");
|
|
||||||
return new(true, assistantPlugin.Id, assistantPlugin.Name, finalDirectory, replacedExisting, string.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, "Failed to install assistant plugin.");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !string.IsNullOrWhiteSpace(finalDirectory) && !Directory.Exists(finalDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Move(backupDirectory, finalDirectory);
|
|
||||||
}
|
|
||||||
catch (Exception restoreException)
|
|
||||||
{
|
|
||||||
this.logger.LogError(restoreException, "Failed to restore the previous assistant plugin after a failed installation.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Error(string.Format(TB("Unexpected error: {0}"), e.Message));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.installSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether edited assistant plugin code can replace an installed local assistant plugin
|
|
||||||
/// without writing the file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="plugin">The installed local assistant plugin to validate against.</param>
|
|
||||||
/// <param name="lua">The edited <c>plugin.lua</c> content.</param>
|
|
||||||
/// <param name="token">Cancellation token for Lua validation.</param>
|
|
||||||
/// <returns>Check result that contains success state, plugin metadata, and a user-facing issue when validation failed.</returns>
|
|
||||||
public async Task<AssistantPluginCheckResult> CheckInstalledAssistantUpdateAsync(IAvailablePlugin plugin, string lua, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (plugin.Type is not PluginType.ASSISTANT)
|
|
||||||
return CheckError(TB("Only assistant plugins can be edited."));
|
|
||||||
|
|
||||||
if (plugin.IsInternal)
|
|
||||||
return CheckError(TB("Internal assistant plugins cannot be edited."));
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
|
||||||
return CheckError(TB("The assistant plugin has no local directory."));
|
|
||||||
|
|
||||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
|
||||||
return CheckError(rootIssue);
|
|
||||||
|
|
||||||
var pluginDirectory = plugin.LocalPath;
|
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, pluginDirectory) || IsSameDirectory(assistantPluginsRoot, pluginDirectory))
|
|
||||||
return CheckError(TB("The assistant plugin directory is outside the local assistant plugin directory."));
|
|
||||||
|
|
||||||
if (!Directory.Exists(pluginDirectory))
|
|
||||||
return CheckError(TB("The assistant plugin directory does not exist."));
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var validation = await this.ValidateInPluginDirectoryAsync(lua, pluginDirectory, token);
|
|
||||||
if (!validation.Success || validation.AssistantPlugin is null)
|
|
||||||
return CheckError(validation.Issue);
|
|
||||||
|
|
||||||
var assistantPlugin = validation.AssistantPlugin;
|
|
||||||
return assistantPlugin.Id != plugin.Id
|
|
||||||
? CheckError(TB("The edited assistant plugin must keep the same plugin ID."))
|
|
||||||
: new(true, assistantPlugin.Id, assistantPlugin.Name, string.Empty);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.installSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes installed local assistant plugin directories.
|
|
||||||
/// The directory gets moved to a backup dir outside the plugin root so the
|
|
||||||
/// plugin loader cannot discover it during reload. On failure, the directory
|
|
||||||
/// and related assistant settings are restored.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="plugin">Assistant plugin metadata</param>
|
|
||||||
/// <param name="token">Cancellation token for settings storage and plugin reload</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Delete result that contains success state, deleted plugin metadata, the original plugin directory,
|
|
||||||
/// and a user-facing issue when deletion failed.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<AssistantPluginDeleteResult> DeleteInstalledAssistantAsync(IAvailablePlugin plugin, CancellationToken token)
|
|
||||||
{
|
|
||||||
var eligibilityIssue = GetAssistantDeletionEligibilityIssue(plugin);
|
|
||||||
if (!string.IsNullOrEmpty(eligibilityIssue))
|
|
||||||
return DeleteError(plugin, plugin.LocalPath, eligibilityIssue);
|
|
||||||
|
|
||||||
if (this.HasActiveAssistantWork(plugin.Id))
|
|
||||||
return DeleteError(plugin, plugin.LocalPath, TB("The assistant cannot be deleted while background work is still running."));
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
|
||||||
var pluginDirectory = plugin.LocalPath;
|
|
||||||
var backupDirectory = string.Empty;
|
|
||||||
var wasEnabled = false;
|
|
||||||
var removedAudits = new List<PluginAssistantAudit>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
eligibilityIssue = GetAssistantDeletionEligibilityIssue(plugin);
|
|
||||||
if (!string.IsNullOrEmpty(eligibilityIssue))
|
|
||||||
return DeleteError(plugin, pluginDirectory, eligibilityIssue);
|
|
||||||
|
|
||||||
if (this.HasActiveAssistantWork(plugin.Id))
|
|
||||||
return DeleteError(plugin, pluginDirectory, TB("The assistant cannot be deleted while background work is still running."));
|
|
||||||
|
|
||||||
backupDirectory = CreateDeleteBackupDirectory(plugin);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(backupDirectory)!);
|
|
||||||
Directory.Move(pluginDirectory, backupDirectory);
|
|
||||||
|
|
||||||
wasEnabled = this.settingsManager.ConfigurationData.EnabledPlugins.Remove(plugin.Id);
|
|
||||||
removedAudits = this.settingsManager.ConfigurationData.AssistantPluginAudits
|
|
||||||
.Where(audit => audit.PluginId == plugin.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (removedAudits.Count > 0)
|
|
||||||
this.settingsManager.ConfigurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id);
|
|
||||||
|
|
||||||
await this.settingsManager.StoreSettings();
|
|
||||||
await PluginFactory.LoadAll(token);
|
|
||||||
|
|
||||||
TryDeleteDirectory(backupDirectory, "assistant plugin delete backup", this.logger);
|
|
||||||
this.logger.LogInformation($"Deleted assistant plugin '{plugin.Name}' ({plugin.Id}) from '{pluginDirectory}'.");
|
|
||||||
return new(true, plugin.Id, plugin.Name, pluginDirectory, string.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, $"Failed to delete assistant plugin '{plugin.Name}' ({plugin.Id}) from '{pluginDirectory}'.");
|
|
||||||
|
|
||||||
await this.TryRestoreDeletedAssistantPluginAsync(plugin, pluginDirectory, backupDirectory, wasEnabled, removedAudits, token);
|
|
||||||
return DeleteError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), e.Message));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.installSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates installed assistant plugin <c>plugin.lua</c> file.
|
|
||||||
/// The edited Lua code is validated from the provided string before it is written,
|
|
||||||
/// but validation uses existing plugin directory as loader context so
|
|
||||||
/// <c>require(...)</c> can resolve companion files such as <c>icon.lua</c>.
|
|
||||||
/// After successful validation, the current <c>plugin.lua</c> is backed up,
|
|
||||||
/// replaced atomically through a temporary file in the plugin directory, and
|
|
||||||
/// restored when the plugin reload fails.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="plugin">The installed local assistant plugin to update.</param>
|
|
||||||
/// <param name="lua">The edited <c>plugin.lua</c> content.</param>
|
|
||||||
/// <param name="token">Cancellation token for Lua validation, file IO, and plugin reload.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Update result that contains success state, updated plugin metadata, the plugin directory,
|
|
||||||
/// and a user-facing issue when the update failed.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<AssistantPluginUpdateResult> UpdateInstalledAssistantAsync(IAvailablePlugin plugin, string lua, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (plugin.Type is not PluginType.ASSISTANT)
|
|
||||||
return UpdateError(plugin, plugin.LocalPath, TB("Only assistant plugins can be edited."));
|
|
||||||
|
|
||||||
if (plugin.IsInternal)
|
|
||||||
return UpdateError(plugin, plugin.LocalPath, TB("Internal assistant plugins cannot be edited."));
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
|
||||||
return UpdateError(plugin, string.Empty, TB("The assistant plugin has no local directory."));
|
|
||||||
|
|
||||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
|
||||||
return UpdateError(plugin, plugin.LocalPath, rootIssue);
|
|
||||||
|
|
||||||
var pluginDirectory = plugin.LocalPath;
|
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, pluginDirectory) || IsSameDirectory(assistantPluginsRoot, pluginDirectory))
|
|
||||||
return UpdateError(plugin, pluginDirectory, TB("The assistant plugin directory is outside the local assistant plugin directory."));
|
|
||||||
|
|
||||||
if (!Directory.Exists(pluginDirectory))
|
|
||||||
return UpdateError(plugin, pluginDirectory, TB("The assistant plugin directory does not exist."));
|
|
||||||
|
|
||||||
var pluginFile = Path.Join(pluginDirectory, PLUGIN_FILE_NAME);
|
|
||||||
if (!IsPathInsideDirectory(pluginDirectory, pluginFile))
|
|
||||||
return UpdateError(plugin, pluginDirectory, TB("The plugin file is outside the assistant plugin directory."));
|
|
||||||
|
|
||||||
await this.installSemaphore.WaitAsync(token);
|
|
||||||
var tempFile = string.Empty;
|
|
||||||
var backupFile = string.Empty;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var validation = await this.ValidateInPluginDirectoryAsync(lua, pluginDirectory, token);
|
|
||||||
if (!validation.Success || validation.AssistantPlugin is null)
|
|
||||||
return UpdateError(plugin, pluginDirectory, validation.Issue);
|
|
||||||
|
|
||||||
var assistantPlugin = validation.AssistantPlugin;
|
|
||||||
if (assistantPlugin.Id != plugin.Id)
|
|
||||||
return UpdateError(plugin, pluginDirectory, TB("The edited assistant plugin must keep the same plugin ID."));
|
|
||||||
|
|
||||||
var pluginCode = lua.Trim();
|
|
||||||
tempFile = Path.Join(pluginDirectory, $"{PLUGIN_FILE_NAME}.tmp-{Guid.NewGuid():N}");
|
|
||||||
backupFile = Path.Join(pluginDirectory, $"{PLUGIN_FILE_NAME}.backup-{Guid.NewGuid():N}");
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(tempFile, pluginCode, Encoding.UTF8, token);
|
|
||||||
|
|
||||||
if (File.Exists(pluginFile))
|
|
||||||
File.Replace(tempFile, pluginFile, backupFile);
|
|
||||||
else
|
|
||||||
File.Move(tempFile, pluginFile);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await PluginFactory.LoadAll(token);
|
|
||||||
if (File.Exists(backupFile))
|
|
||||||
File.Delete(backupFile);
|
|
||||||
|
|
||||||
this.logger.LogInformation($"Updated assistant plugin '{assistantPlugin.Name}' ({assistantPlugin.Id}) at '{pluginFile}'.");
|
|
||||||
return new(true, assistantPlugin.Id, assistantPlugin.Name, pluginDirectory, string.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception reloadException)
|
|
||||||
{
|
|
||||||
this.logger.LogError(reloadException, $"Failed to reload plugins after editing assistant plugin '{plugin.Name}' ({plugin.Id}).");
|
|
||||||
await this.TryRestoreEditedAssistantPluginAsync(pluginFile, backupFile, token);
|
|
||||||
return UpdateError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), reloadException.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, $"Failed to update assistant plugin '{plugin.Name}' ({plugin.Id}) at '{pluginDirectory}'.");
|
|
||||||
await this.TryRestoreEditedAssistantPluginAsync(pluginFile, backupFile, token);
|
|
||||||
return UpdateError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), e.Message));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.TryDeleteFile(tempFile, "assistant plugin edit temp file");
|
|
||||||
|
|
||||||
this.installSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<AssistantPluginValidationResult> ValidateIntoStagingAsync(string lua, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(lua))
|
|
||||||
return AssistantPluginValidationResult.Failure(TB("No Lua plugin code was generated."));
|
|
||||||
|
|
||||||
if (!PluginFactory.IsInitialized)
|
|
||||||
return AssistantPluginValidationResult.Failure(TB("The plugin system is not initialized yet."));
|
|
||||||
|
|
||||||
var pluginCode = lua.Trim();
|
|
||||||
var stagingDirectory = Path.Join(Path.GetTempPath(), $"{ASSISTANT_BUILDER_DIRECTORY_PREFIX}.staging-{Guid.NewGuid():N}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(stagingDirectory);
|
|
||||||
var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME);
|
|
||||||
await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token);
|
|
||||||
|
|
||||||
var validation = await this.ValidateAssistantPluginCodeAsync(
|
|
||||||
stagingDirectory,
|
|
||||||
pluginCode,
|
|
||||||
TB("The generated plugin is not an assistant plugin. Issue: {0}"),
|
|
||||||
TB("The generated assistant plugin is invalid. Issue: {0}"),
|
|
||||||
TB("The generated assistant plugin uses the ID of an internal AI Studio plugin."),
|
|
||||||
token);
|
|
||||||
|
|
||||||
if (!validation.Success || validation.AssistantPlugin is null)
|
|
||||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
|
||||||
|
|
||||||
return validation with { StagingDirectory = stagingDirectory };
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, "Failed to validate generated assistant plugin.");
|
|
||||||
this.TryDeleteStagingDirectory(stagingDirectory);
|
|
||||||
return AssistantPluginValidationResult.Failure(string.Format(TB("Unexpected error: {0}"), e.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<AssistantPluginValidationResult> ValidateInPluginDirectoryAsync(string lua, string pluginDirectory, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(lua))
|
|
||||||
return AssistantPluginValidationResult.Failure(TB("No Lua plugin code was generated."));
|
|
||||||
|
|
||||||
if (!PluginFactory.IsInitialized)
|
|
||||||
return AssistantPluginValidationResult.Failure(TB("The plugin system is not initialized yet."));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await this.ValidateAssistantPluginCodeAsync(
|
|
||||||
pluginDirectory,
|
|
||||||
lua.Trim(),
|
|
||||||
TB("The edited plugin is not an assistant plugin. Issue: {0}"),
|
|
||||||
TB("The edited assistant plugin is invalid. Issue: {0}"),
|
|
||||||
TB("The edited assistant plugin uses the ID of an internal AI Studio plugin."),
|
|
||||||
token);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, "Failed to validate edited assistant plugin.");
|
|
||||||
return AssistantPluginValidationResult.Failure(string.Format(TB("Unexpected error: {0}"), e.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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)
|
|
||||||
return AssistantPluginValidationResult.Failure(string.Format(notAssistantIssue, string.Join("; ", plugin.Issues)));
|
|
||||||
|
|
||||||
if (!assistantPlugin.IsValid)
|
|
||||||
return AssistantPluginValidationResult.Failure(string.Format(invalidAssistantIssue, string.Join("; ", assistantPlugin.Issues)));
|
|
||||||
|
|
||||||
if (PluginFactory.AvailablePlugins.Any(availablePlugin => availablePlugin.Type is PluginType.ASSISTANT && availablePlugin.Id == assistantPlugin.Id && availablePlugin.IsInternal))
|
|
||||||
return AssistantPluginValidationResult.Failure(internalPluginIdIssue);
|
|
||||||
|
|
||||||
return new(true, string.Empty, assistantPlugin, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetAssistantPluginsRoot(out string assistantPluginsRoot, out string issue)
|
|
||||||
{
|
|
||||||
assistantPluginsRoot = string.Empty;
|
|
||||||
issue = string.Empty;
|
|
||||||
|
|
||||||
var dataDirectory = SettingsManager.DataDirectory;
|
|
||||||
if (string.IsNullOrWhiteSpace(dataDirectory))
|
|
||||||
{
|
|
||||||
issue = TB("The AI Studio data directory is not initialized yet.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assistantPluginsRoot = Path.Join(dataDirectory, "plugins", PluginType.ASSISTANT.GetDirectory());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAssistantDeletionEligibilityIssue(IAvailablePlugin plugin)
|
|
||||||
{
|
|
||||||
if (plugin.Type is not PluginType.ASSISTANT)
|
|
||||||
return TB("Only assistant plugins can be deleted.");
|
|
||||||
|
|
||||||
if (plugin.IsInternal)
|
|
||||||
return TB("Internal assistant plugins cannot be deleted.");
|
|
||||||
|
|
||||||
if (plugin.IsManagedByConfigServer)
|
|
||||||
return TB("Config Server managed assistant plugins cannot be deleted.");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
|
||||||
return TB("The assistant plugin has no local directory.");
|
|
||||||
|
|
||||||
var assistantPlugin = PluginFactory.RunningPlugins
|
|
||||||
.OfType<PluginAssistants>()
|
|
||||||
.FirstOrDefault(candidate => candidate.Id == plugin.Id && IsSameDirectory(candidate.PluginPath, plugin.LocalPath));
|
|
||||||
|
|
||||||
if (assistantPlugin is null || assistantPlugin.IsInternal || !assistantPlugin.IsAssistantBuilderGenerated)
|
|
||||||
return TB("Only assistants generated by the Assistant Builder can be deleted.");
|
|
||||||
|
|
||||||
if (assistantPlugin.IsManagedByConfigServer)
|
|
||||||
return TB("Config Server managed assistant plugins cannot be deleted.");
|
|
||||||
|
|
||||||
if (!TryGetAssistantPluginsRoot(out var assistantPluginsRoot, out var rootIssue))
|
|
||||||
return rootIssue;
|
|
||||||
|
|
||||||
if (!IsPathInsideDirectory(assistantPluginsRoot, plugin.LocalPath) || IsSameDirectory(assistantPluginsRoot, plugin.LocalPath))
|
|
||||||
return TB("The assistant plugin directory is outside the local assistant plugin directory.");
|
|
||||||
|
|
||||||
return Directory.Exists(plugin.LocalPath)
|
|
||||||
? string.Empty
|
|
||||||
: TB("The assistant plugin directory does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryDeleteStagingDirectory(string stagingDirectory)
|
|
||||||
{
|
|
||||||
TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin)
|
|
||||||
{
|
|
||||||
var existingPlugin = PluginFactory.AvailablePlugins
|
|
||||||
.OfType<IAvailablePlugin>()
|
|
||||||
.FirstOrDefault(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == assistantPlugin.Id && !plugin.IsInternal);
|
|
||||||
|
|
||||||
return existingPlugin is not null
|
|
||||||
? existingPlugin.LocalPath
|
|
||||||
: Path.Join(assistantPluginsRoot, CreatePluginDirectoryName(assistantPlugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreatePluginDirectoryName(PluginAssistants assistantPlugin)
|
|
||||||
{
|
|
||||||
var safeName = CreateSafeDirectoryNamePart(assistantPlugin.Name);
|
|
||||||
return $"{safeName}-{assistantPlugin.Id:N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateSafeDirectoryNamePart(string name)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
var invalidChars = Path.GetInvalidFileNameChars().ToHashSet();
|
|
||||||
|
|
||||||
foreach (var character in name.Trim())
|
|
||||||
{
|
|
||||||
if (char.IsLetterOrDigit(character))
|
|
||||||
{
|
|
||||||
sb.Append(char.ToLowerInvariant(character));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (character is '-' or '_' or '.' && !invalidChars.Contains(character))
|
|
||||||
{
|
|
||||||
sb.Append(character);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendSeparator();
|
|
||||||
}
|
|
||||||
|
|
||||||
var safeName = sb.ToString().Trim('-', '.');
|
|
||||||
if (safeName.Length > DIRECTORY_PREFIX_MAX_LEN)
|
|
||||||
safeName = safeName[..DIRECTORY_PREFIX_MAX_LEN].Trim('-', '.');
|
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(safeName)
|
|
||||||
? ASSISTANT_BUILDER_DIRECTORY_PREFIX
|
|
||||||
: safeName;
|
|
||||||
|
|
||||||
void AppendSeparator()
|
|
||||||
{
|
|
||||||
if (sb.Length == 0 || sb[^1] == '-')
|
|
||||||
return;
|
|
||||||
|
|
||||||
sb.Append('-');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPathInsideDirectory(string parentDirectory, string path)
|
|
||||||
{
|
|
||||||
var parentPath = Path.GetFullPath(parentDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
||||||
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
||||||
return childPath.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSameDirectory(string firstDirectory, string secondDirectory)
|
|
||||||
{
|
|
||||||
var firstPath = Path.GetFullPath(firstDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
||||||
var secondPath = Path.GetFullPath(secondDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
||||||
return string.Equals(firstPath, secondPath, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateDeleteBackupDirectory(IAvailablePlugin plugin)
|
|
||||||
{
|
|
||||||
var backupRoot = Path.Join(SettingsManager.DataDirectory, DELETE_BACKUP_DIRECTORY);
|
|
||||||
return Path.Join(backupRoot, $"assistant-{plugin.Id:N}-{Guid.NewGuid():N}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryRestoreDeletedAssistantPluginAsync(IAvailablePlugin plugin, string pluginDirectory, string backupDirectory, bool wasEnabled, List<PluginAssistantAudit> removedAudits, CancellationToken token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(pluginDirectory) && Directory.Exists(backupDirectory))
|
|
||||||
Directory.Move(backupDirectory, pluginDirectory);
|
|
||||||
|
|
||||||
if (wasEnabled && !this.settingsManager.ConfigurationData.EnabledPlugins.Contains(plugin.Id))
|
|
||||||
this.settingsManager.ConfigurationData.EnabledPlugins.Add(plugin.Id);
|
|
||||||
|
|
||||||
if (removedAudits.Count > 0)
|
|
||||||
{
|
|
||||||
this.settingsManager.ConfigurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id);
|
|
||||||
this.settingsManager.ConfigurationData.AssistantPluginAudits.AddRange(removedAudits);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.settingsManager.StoreSettings();
|
|
||||||
await PluginFactory.LoadAll(token);
|
|
||||||
}
|
|
||||||
catch (Exception restoreException)
|
|
||||||
{
|
|
||||||
this.logger.LogError(restoreException, $"Failed to restore assistant plugin '{plugin.Name}' ({plugin.Id}) after a failed delete.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryRestoreEditedAssistantPluginAsync(string pluginFile, string backupFile, CancellationToken token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(backupFile) || !File.Exists(backupFile))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (File.Exists(pluginFile))
|
|
||||||
File.Delete(pluginFile);
|
|
||||||
|
|
||||||
File.Move(backupFile, pluginFile);
|
|
||||||
await PluginFactory.LoadAll(token);
|
|
||||||
}
|
|
||||||
catch (Exception restoreException)
|
|
||||||
{
|
|
||||||
this.logger.LogError(restoreException, $"Failed to restore assistant plugin file '{pluginFile}' after a failed edit.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryDeleteDirectory(string directory, string directoryDescription, ILogger logger)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(directory))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogError(e, $"Failed to delete {directoryDescription} directory '{directory}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryDeleteFile(string filePath, string fileDescription)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(filePath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
this.logger.LogError(e, $"Failed to delete {fileDescription} '{filePath}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record AssistantPluginValidationResult(bool Success, string StagingDirectory, PluginAssistants? AssistantPlugin, string Issue)
|
|
||||||
{
|
|
||||||
public static AssistantPluginValidationResult Failure(string issue) => new(false, string.Empty, null, issue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed record AssistantPluginUpdateResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What deleting a local configuration plugin takes with it, besides the plugin directory itself.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A configuration plugin owns everything it configured. Removing it therefore removes its providers,
|
||||||
|
/// data sources, chat templates, and profiles, and it resets the settings it had locked. Users cannot
|
||||||
|
/// see any of that on the plugins page, so we show it before they confirm the deletion.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed record ConfigurationPluginDeleteSummary(
|
||||||
|
int LlmProviders,
|
||||||
|
int TranscriptionProviders,
|
||||||
|
int EmbeddingProviders,
|
||||||
|
int DataSources,
|
||||||
|
int ChatTemplates,
|
||||||
|
int Profiles,
|
||||||
|
int DocumentAnalysisPolicies,
|
||||||
|
int LockedSettings,
|
||||||
|
int MandatoryInfos,
|
||||||
|
int Introductions)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An empty summary, used when the configuration plugin is not running and we cannot tell what it configured.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ConfigurationPluginDeleteSummary EMPTY = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when the deletion affects anything beyond the plugin directory.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasAnyConsequence =>
|
||||||
|
this.LlmProviders > 0 ||
|
||||||
|
this.TranscriptionProviders > 0 ||
|
||||||
|
this.EmbeddingProviders > 0 ||
|
||||||
|
this.DataSources > 0 ||
|
||||||
|
this.ChatTemplates > 0 ||
|
||||||
|
this.Profiles > 0 ||
|
||||||
|
this.DocumentAnalysisPolicies > 0 ||
|
||||||
|
this.LockedSettings > 0 ||
|
||||||
|
this.MandatoryInfos > 0 ||
|
||||||
|
this.Introductions > 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A provider or data source a configuration plugin brings, and where it sends data to.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Type">The kind of configuration object.</param>
|
||||||
|
/// <param name="Name">The name the configuration gives it.</param>
|
||||||
|
/// <param name="Endpoint">The host of a self-hosted destination, or the name of the cloud provider.</param>
|
||||||
|
public sealed record ConfigurationPluginDestination(PluginConfigurationObjectType Type, string Name, string Endpoint);
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What a configuration plugin would set up, read from the archive before anything is installed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A configuration takes effect the moment it is installed, and it has no on/off switch. The import
|
||||||
|
/// dialog is therefore the only place where users can see what they are about to accept, which is
|
||||||
|
/// why this carries the destinations of providers and data sources and not just their number.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="Destinations">The providers and data sources, together with where they send data to.</param>
|
||||||
|
/// <param name="ChatTemplates">How many chat templates the configuration adds.</param>
|
||||||
|
/// <param name="Profiles">How many profiles the configuration adds.</param>
|
||||||
|
/// <param name="DocumentAnalysisPolicies">How many document analysis policies the configuration adds.</param>
|
||||||
|
/// <param name="DeclaredSettings">How many settings the configuration takes over.</param>
|
||||||
|
/// <param name="MandatoryInfos">How many mandatory information texts users must accept.</param>
|
||||||
|
/// <param name="Introductions">How many introductions the configuration adds to the welcome page.</param>
|
||||||
|
public sealed record ConfigurationPluginImportSummary(
|
||||||
|
IReadOnlyList<ConfigurationPluginDestination> Destinations,
|
||||||
|
int ChatTemplates,
|
||||||
|
int Profiles,
|
||||||
|
int DocumentAnalysisPolicies,
|
||||||
|
int DeclaredSettings,
|
||||||
|
int MandatoryInfos,
|
||||||
|
int Introductions)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True when the configuration sets up anything at all.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasAnyContent =>
|
||||||
|
this.Destinations.Count > 0 ||
|
||||||
|
this.ChatTemplates > 0 ||
|
||||||
|
this.Profiles > 0 ||
|
||||||
|
this.DocumentAnalysisPolicies > 0 ||
|
||||||
|
this.DeclaredSettings > 0 ||
|
||||||
|
this.MandatoryInfos > 0 ||
|
||||||
|
this.Introductions > 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed class NativeShareService(RustService rustService)
|
||||||
|
{
|
||||||
|
public Task<bool> Share(string filePath) => rustService.ShareFile(filePath);
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed record PluginDeleteResult(bool Success, Guid PluginId, string PluginName, string PluginDirectory, string Issue);
|
||||||
20
app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs
Normal file
20
app/MindWork AI Studio/Tools/Services/PluginImportPreview.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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>
|
||||||
|
/// <param name="ConfigurationSummary">
|
||||||
|
/// What a configuration plugin would set up. Null for every other plugin type.
|
||||||
|
/// </param>
|
||||||
|
public sealed record PluginImportPreview(IPluginMetadata Plugin, IAvailablePlugin? ExistingPlugin, ConfigurationPluginImportSummary? ConfigurationSummary = null)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True when an installed plugin with the same ID gets replaced.
|
||||||
|
/// </summary>
|
||||||
|
public bool ReplacesExisting => this.ExistingPlugin is not null;
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
using System.Text;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether generated Lua assistant plugin code can be loaded and installed.
|
||||||
|
/// The plugin is written to a temporary staging directory and validated through the
|
||||||
|
/// normal plugin loader, but it is not moved into the user plugin directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lua">The full generated <c>plugin.lua</c> content.</param>
|
||||||
|
/// <param name="token">A cancellation token for file IO and Lua validation.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Check result that contains success state, plugin metadata, and a user-facing issue when validation failed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<AssistantPluginCheckResult> CheckInstallabilityAsync(string lua, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue))
|
||||||
|
return CheckError(rootIssue);
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
var stagingDirectory = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validation = await this.ValidateIntoStagingAsync(lua, token);
|
||||||
|
if (!validation.Success || validation.AssistantPlugin is null)
|
||||||
|
return CheckError(validation.Issue);
|
||||||
|
|
||||||
|
stagingDirectory = validation.StagingDirectory;
|
||||||
|
var finalDirectory = DetermineFinalDirectory(assistantPluginsRoot, validation.AssistantPlugin, PluginType.ASSISTANT);
|
||||||
|
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
|
||||||
|
return CheckError(TB("The resolved plugin directory is outside the plugin directory."));
|
||||||
|
|
||||||
|
return new(true, validation.AssistantPlugin.Id, validation.AssistantPlugin.Name, string.Empty);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs generated Lua assistant plugin code into the user plugin directory.
|
||||||
|
/// Writes the plugin into a temporary staging directory first, validates it through the
|
||||||
|
/// normal plugin loader, then moves into <c>data/plugins/assistants</c>.
|
||||||
|
/// If plugin with same ID already exists, the existing directory is moved
|
||||||
|
/// aside as backup and restored when replacement fails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lua">The full generated <c>plugin.lua</c> content.</param>
|
||||||
|
/// <param name="token">A cancellation token for file IO, Lua validation, and plugin reload.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Installation result that contains success state, installed plugin metadata, final directory,
|
||||||
|
/// whether an existing plugin was replaced, and user-facing issue when installation failed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<AssistantPluginInstallResult> InstallAsync(string lua, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue))
|
||||||
|
return Error(rootIssue);
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validation = await this.ValidateIntoStagingAsync(lua, token);
|
||||||
|
if (!validation.Success || validation.AssistantPlugin is null)
|
||||||
|
return Error(validation.Issue);
|
||||||
|
|
||||||
|
return await this.InstallStagedPluginAsync(assistantPluginsRoot, validation, PluginType.ASSISTANT, token);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PluginValidationResult> ValidateIntoStagingAsync(string lua, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(lua))
|
||||||
|
return PluginValidationResult.Failure(TB("No Lua plugin code was generated."));
|
||||||
|
|
||||||
|
if (!PluginFactory.IsInitialized)
|
||||||
|
return PluginValidationResult.Failure(TB("The plugin system is not initialized yet."));
|
||||||
|
|
||||||
|
var pluginCode = lua.Trim();
|
||||||
|
var stagingDirectory = Path.Join(Path.GetTempPath(), $"{ASSISTANT_BUILDER_DIRECTORY_PREFIX}.staging-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(stagingDirectory);
|
||||||
|
var stagedPluginFile = Path.Join(stagingDirectory, PLUGIN_FILE_NAME);
|
||||||
|
await File.WriteAllTextAsync(stagedPluginFile, pluginCode, Encoding.UTF8, token);
|
||||||
|
|
||||||
|
var validation = await ValidatePluginCodeAsync(
|
||||||
|
stagingDirectory,
|
||||||
|
pluginCode,
|
||||||
|
[PluginType.ASSISTANT],
|
||||||
|
TB("The generated plugin is not an assistant plugin. Issue: {0}"),
|
||||||
|
TB("The generated assistant plugin is invalid. Issue: {0}"),
|
||||||
|
TB("The generated assistant plugin uses the ID of another installed plugin."),
|
||||||
|
token);
|
||||||
|
|
||||||
|
if (!validation.Success || validation.AssistantPlugin is null)
|
||||||
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
|
|
||||||
|
return validation with { StagingDirectory = stagingDirectory };
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, "Failed to validate generated assistant plugin.");
|
||||||
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
|
return PluginValidationResult.Failure(string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.Media;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The plugin types users may remove through the user interface.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly PluginType[] DELETABLE_PLUGIN_TYPES = [PluginType.ASSISTANT, PluginType.CONFIGURATION, PluginType.LANGUAGE];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin is one that users may delete.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This decides whether the delete action is offered at all. Whether it may run right now is a
|
||||||
|
/// different question: an assistant with running background work stays visible but blocked.
|
||||||
|
/// </remarks>
|
||||||
|
public static bool CanDeletePlugin(IAvailablePlugin plugin) => string.IsNullOrWhiteSpace(GetDeletionEligibilityIssue(plugin));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collects what deleting a local configuration plugin removes besides the plugin directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The configuration plugin about to be deleted.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The summary shown to the user before the deletion starts. It is empty when the plugin is not
|
||||||
|
/// running, because we cannot tell what an unloadable plugin had configured.
|
||||||
|
/// </returns>
|
||||||
|
public ConfigurationPluginDeleteSummary BuildConfigurationDeleteSummary(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
var configurationPlugin = PluginFactory.RunningPlugins.OfType<PluginConfiguration>().FirstOrDefault(candidate => candidate.Id == plugin.Id);
|
||||||
|
if (configurationPlugin is null)
|
||||||
|
return ConfigurationPluginDeleteSummary.EMPTY;
|
||||||
|
|
||||||
|
var configObjects = configurationPlugin.ConfigObjects.ToList();
|
||||||
|
var configurationData = this.settingsManager.ConfigurationData;
|
||||||
|
|
||||||
|
// Both maps record which configuration plugin manages a setting. Everything this plugin owns
|
||||||
|
// returns to its default value once the plugin is gone:
|
||||||
|
var lockedSettings =
|
||||||
|
configurationData.ManagedLockedConfigurations.Count(entry => entry.Value == plugin.Id) +
|
||||||
|
configurationData.ManagedEditableDefaults.Count(entry => entry.Value.ConfigPluginId == plugin.Id);
|
||||||
|
|
||||||
|
return new(
|
||||||
|
LlmProviders: CountObjects(PluginConfigurationObjectType.LLM_PROVIDER),
|
||||||
|
TranscriptionProviders: CountObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER),
|
||||||
|
EmbeddingProviders: CountObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER),
|
||||||
|
DataSources: CountObjects(PluginConfigurationObjectType.DATA_SOURCE),
|
||||||
|
ChatTemplates: CountObjects(PluginConfigurationObjectType.CHAT_TEMPLATE),
|
||||||
|
Profiles: CountObjects(PluginConfigurationObjectType.PROFILE),
|
||||||
|
DocumentAnalysisPolicies: CountObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY),
|
||||||
|
LockedSettings: lockedSettings,
|
||||||
|
MandatoryInfos: configurationPlugin.MandatoryInfos.Count,
|
||||||
|
Introductions: configurationPlugin.Introductions.Count);
|
||||||
|
|
||||||
|
int CountObjects(PluginConfigurationObjectType type) => configObjects.Count(configObject => configObject.Type == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether an assistant still owns running or canceling background work.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasActiveAssistantWork(Guid pluginId)
|
||||||
|
{
|
||||||
|
var instanceId = pluginId.ToString();
|
||||||
|
if (this.assistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && string.Equals(snapshot.Key.InstanceId, instanceId, StringComparison.Ordinal)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var ownerIdSuffix = $":{instanceId}";
|
||||||
|
return this.mediaTranscriptionService.GetSnapshots().Any(snapshot =>
|
||||||
|
snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.ASSISTANT } &&
|
||||||
|
snapshot.Owner.Id.EndsWith(ownerIdSuffix, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the directory of a plugin the user installed or placed themselves.
|
||||||
|
/// The directory gets moved to a backup dir outside the plugin root so the plugin loader cannot
|
||||||
|
/// discover it during reload. On failure, the directory and the related settings are restored.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For a configuration plugin, we do not remove its providers, data sources, chat templates,
|
||||||
|
/// profiles, or locked settings ourselves. The reload does that: it recognizes them as left over
|
||||||
|
/// once their configuration plugin is gone, and it also deletes the related secrets from the OS
|
||||||
|
/// keyring.<br/><br/>
|
||||||
|
/// What the reload cannot recognize as left over is everything the user decided about the plugin
|
||||||
|
/// itself: its activation state, the language choice of a language plugin, and the security audit
|
||||||
|
/// of an assistant. Those are removed here, see ApplyDeleteSideEffects.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="plugin">Metadata of the plugin to delete.</param>
|
||||||
|
/// <param name="token">Cancellation token for settings storage and plugin reload.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Delete result that contains a success state, deleted plugin metadata, the original plugin directory,
|
||||||
|
/// and a user-facing issue when deletion failed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<PluginDeleteResult> DeletePluginAsync(IAvailablePlugin plugin, CancellationToken token)
|
||||||
|
{
|
||||||
|
var deletionIssue = this.GetDeletionIssue(plugin);
|
||||||
|
if (!string.IsNullOrWhiteSpace(deletionIssue))
|
||||||
|
return DeleteError(plugin, plugin.LocalPath, deletionIssue);
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
var pluginDirectory = plugin.LocalPath;
|
||||||
|
var backupDirectory = string.Empty;
|
||||||
|
var sideEffects = PluginDeleteSideEffects.NONE;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check again under the semaphore: another operation might have changed the plugin state
|
||||||
|
// while we were waiting:
|
||||||
|
deletionIssue = this.GetDeletionIssue(plugin);
|
||||||
|
if (!string.IsNullOrWhiteSpace(deletionIssue))
|
||||||
|
return DeleteError(plugin, pluginDirectory, deletionIssue);
|
||||||
|
|
||||||
|
backupDirectory = CreateDeleteBackupDirectory(plugin);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(backupDirectory)!);
|
||||||
|
Directory.Move(pluginDirectory, backupDirectory);
|
||||||
|
|
||||||
|
sideEffects = this.ApplyDeleteSideEffects(plugin);
|
||||||
|
if (sideEffects.HasChanges)
|
||||||
|
await this.settingsManager.StoreSettings();
|
||||||
|
|
||||||
|
await PluginFactory.LoadAll(token);
|
||||||
|
|
||||||
|
TryDeleteDirectory(backupDirectory, "plugin delete backup", this.logger);
|
||||||
|
this.logger.LogInformation($"Deleted {plugin.Type} plugin '{plugin.Name}' ({plugin.Id}) from '{pluginDirectory}'.");
|
||||||
|
return new(true, plugin.Id, plugin.Name, pluginDirectory, string.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, $"Failed to delete {plugin.Type} plugin '{plugin.Name}' ({plugin.Id}) from '{pluginDirectory}'.");
|
||||||
|
|
||||||
|
await this.TryRestoreDeletedPluginAsync(plugin, pluginDirectory, backupDirectory, sideEffects, token);
|
||||||
|
return DeleteError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks everything that prevents deleting a plugin right now.
|
||||||
|
/// </summary>
|
||||||
|
private string GetDeletionIssue(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
var eligibilityIssue = GetDeletionEligibilityIssue(plugin);
|
||||||
|
if (!string.IsNullOrWhiteSpace(eligibilityIssue))
|
||||||
|
return eligibilityIssue;
|
||||||
|
|
||||||
|
// An assistant must not be pulled away from under a user while it is still working:
|
||||||
|
if (plugin.Type is PluginType.ASSISTANT && this.HasActiveAssistantWork(plugin.Id))
|
||||||
|
return TB("The assistant cannot be deleted while background work is still running.");
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a plugin is one users may delete at all, regardless of its current state.
|
||||||
|
/// </summary>
|
||||||
|
private static string GetDeletionEligibilityIssue(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
if (!DELETABLE_PLUGIN_TYPES.Contains(plugin.Type))
|
||||||
|
return TB("Only assistant, configuration, and language plugins can be deleted.");
|
||||||
|
|
||||||
|
if (plugin.IsInternal)
|
||||||
|
return TB("Plugins shipped with AI Studio cannot be deleted.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||||
|
return TB("The plugin has no local directory.");
|
||||||
|
|
||||||
|
//
|
||||||
|
// We decide by the plugin path, not by what a plugin declares about itself. Both
|
||||||
|
// DEPLOYED_USING_CONFIG_SERVER and the Assistant Builder metadata are self-declared: a
|
||||||
|
// locally placed plugin could claim to be deployed by an organization, or simply omit the
|
||||||
|
// builder metadata, and would then be impossible to remove through the user interface, which
|
||||||
|
// is exactly the situation this deletion is meant to resolve.
|
||||||
|
//
|
||||||
|
if (PluginFactory.IsEnterpriseConfigurationPath(plugin.LocalPath))
|
||||||
|
return TB("Plugins deployed by your organization cannot be deleted.");
|
||||||
|
|
||||||
|
if (!PluginFactory.IsInsidePluginsRoot(plugin.LocalPath) || PluginFactory.IsPluginsRoot(plugin.LocalPath))
|
||||||
|
return TB("This individual plugin’s directory is outside the expected plugins directory.");
|
||||||
|
|
||||||
|
return Directory.Exists(plugin.LocalPath) ? string.Empty : TB("The plugin directory does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes everything the user decided about the plugin, and reports what was removed so a failed
|
||||||
|
/// deletion can put it back.
|
||||||
|
/// </summary>
|
||||||
|
private PluginDeleteSideEffects ApplyDeleteSideEffects(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
var configurationData = this.settingsManager.ConfigurationData;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Nothing removes the activation state of a plugin which is gone. Should the user install
|
||||||
|
// a plugin with the same ID again later, it would start enabled without ever having been
|
||||||
|
// switched on. We ask for removal regardless of the plugin type: a configuration plugin
|
||||||
|
// is never listed there, so this simply does nothing for it:
|
||||||
|
//
|
||||||
|
var wasEnabled = configurationData.EnabledPlugins.Remove(plugin.Id);
|
||||||
|
|
||||||
|
//
|
||||||
|
// When the user had chosen this language plugin, the app would silently fall back to
|
||||||
|
// English while the settings still point to the deleted plugin. We return the language
|
||||||
|
// choice to automatic instead, so the settings stay truthful:
|
||||||
|
//
|
||||||
|
var wasChosenLanguage = plugin.Type is PluginType.LANGUAGE && configurationData.App.LanguagePluginId == plugin.Id;
|
||||||
|
if (wasChosenLanguage)
|
||||||
|
{
|
||||||
|
configurationData.App.LanguageBehavior = LangBehavior.AUTO;
|
||||||
|
configurationData.App.LanguagePluginId = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The security audit belongs to the assistant code we checked. Another assistant installed
|
||||||
|
// under the same ID later is different code, so it must be audited again:
|
||||||
|
//
|
||||||
|
List<PluginAssistantAudit> removedAudits = [];
|
||||||
|
if (plugin.Type is PluginType.ASSISTANT)
|
||||||
|
{
|
||||||
|
removedAudits = [.. configurationData.AssistantPluginAudits.Where(audit => audit.PluginId == plugin.Id)];
|
||||||
|
if (removedAudits.Count > 0)
|
||||||
|
configurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(wasEnabled, wasChosenLanguage, removedAudits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateDeleteBackupDirectory(IAvailablePlugin plugin)
|
||||||
|
{
|
||||||
|
var backupRoot = Path.Join(SettingsManager.DataDirectory, DELETE_BACKUP_DIRECTORY);
|
||||||
|
return Path.Join(backupRoot, $"{plugin.Type.GetDirectory()}-{plugin.Id:N}-{Guid.NewGuid():N}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TryRestoreDeletedPluginAsync(IAvailablePlugin plugin, string pluginDirectory, string backupDirectory, PluginDeleteSideEffects sideEffects, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(pluginDirectory) && Directory.Exists(backupDirectory))
|
||||||
|
Directory.Move(backupDirectory, pluginDirectory);
|
||||||
|
|
||||||
|
var configurationData = this.settingsManager.ConfigurationData;
|
||||||
|
if (sideEffects.WasEnabled && !configurationData.EnabledPlugins.Contains(plugin.Id))
|
||||||
|
configurationData.EnabledPlugins.Add(plugin.Id);
|
||||||
|
|
||||||
|
if (sideEffects.WasChosenLanguage)
|
||||||
|
{
|
||||||
|
configurationData.App.LanguageBehavior = LangBehavior.MANUAL;
|
||||||
|
configurationData.App.LanguagePluginId = plugin.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sideEffects.RemovedAudits.Count > 0)
|
||||||
|
{
|
||||||
|
configurationData.AssistantPluginAudits.RemoveAll(audit => audit.PluginId == plugin.Id);
|
||||||
|
configurationData.AssistantPluginAudits.AddRange(sideEffects.RemovedAudits);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sideEffects.HasChanges)
|
||||||
|
await this.settingsManager.StoreSettings();
|
||||||
|
|
||||||
|
// The reload restores everything the plugin configured, because it is back in place:
|
||||||
|
await PluginFactory.LoadAll(token);
|
||||||
|
}
|
||||||
|
catch (Exception restoreException)
|
||||||
|
{
|
||||||
|
this.logger.LogError(restoreException, $"Failed to restore {plugin.Type} plugin '{plugin.Name}' ({plugin.Id}) after a failed delete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What deleting a plugin changed in the settings, so a failed deletion can undo it.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record PluginDeleteSideEffects(bool WasEnabled, bool WasChosenLanguage, List<PluginAssistantAudit> RemovedAudits)
|
||||||
|
{
|
||||||
|
public static readonly PluginDeleteSideEffects NONE = new(false, false, []);
|
||||||
|
|
||||||
|
public bool HasChanges => this.WasEnabled || this.WasChosenLanguage || this.RemovedAudits.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
using System.Text;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether edited assistant plugin code can replace an installed local assistant plugin
|
||||||
|
/// without writing the file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The installed local assistant plugin to validate against.</param>
|
||||||
|
/// <param name="lua">The edited <c>plugin.lua</c> content.</param>
|
||||||
|
/// <param name="token">Cancellation token for Lua validation.</param>
|
||||||
|
/// <returns>Check result that contains success state, plugin metadata, and a user-facing issue when validation failed.</returns>
|
||||||
|
public async Task<AssistantPluginCheckResult> CheckInstalledAssistantUpdateAsync(IAvailablePlugin plugin, string lua, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (plugin.Type is not PluginType.ASSISTANT)
|
||||||
|
return CheckError(TB("Only assistant plugins can be edited."));
|
||||||
|
|
||||||
|
if (plugin.IsInternal)
|
||||||
|
return CheckError(TB("Internal assistant plugins cannot be edited."));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||||
|
return CheckError(TB("The assistant plugin has no local directory."));
|
||||||
|
|
||||||
|
if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue))
|
||||||
|
return CheckError(rootIssue);
|
||||||
|
|
||||||
|
var pluginDirectory = plugin.LocalPath;
|
||||||
|
if (!IsPathInsideDirectory(assistantPluginsRoot, pluginDirectory) || IsSameDirectory(assistantPluginsRoot, pluginDirectory))
|
||||||
|
return CheckError(TB("The assistant plugin directory is outside the local assistant plugin directory."));
|
||||||
|
|
||||||
|
if (!Directory.Exists(pluginDirectory))
|
||||||
|
return CheckError(TB("The assistant plugin directory does not exist."));
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validation = await this.ValidateInPluginDirectoryAsync(lua, pluginDirectory, token);
|
||||||
|
if (!validation.Success || validation.AssistantPlugin is null)
|
||||||
|
return CheckError(validation.Issue);
|
||||||
|
|
||||||
|
var assistantPlugin = validation.AssistantPlugin;
|
||||||
|
return assistantPlugin.Id != plugin.Id
|
||||||
|
? CheckError(TB("The edited assistant plugin must keep the same plugin ID."))
|
||||||
|
: new(true, assistantPlugin.Id, assistantPlugin.Name, string.Empty);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates installed assistant plugin <c>plugin.lua</c> file.
|
||||||
|
/// The edited Lua code is validated from the provided string before it is written,
|
||||||
|
/// but validation uses existing plugin directory as loader context so
|
||||||
|
/// <c>require(...)</c> can resolve companion files such as <c>icon.lua</c>.
|
||||||
|
/// After successful validation, the current <c>plugin.lua</c> is backed up,
|
||||||
|
/// replaced atomically through a temporary file in the plugin directory, and
|
||||||
|
/// restored when the plugin reload fails.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The installed local assistant plugin to update.</param>
|
||||||
|
/// <param name="lua">The edited <c>plugin.lua</c> content.</param>
|
||||||
|
/// <param name="token">Cancellation token for Lua validation, file IO, and plugin reload.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Update result that contains success state, updated plugin metadata, the plugin directory,
|
||||||
|
/// and a user-facing issue when the update failed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<AssistantPluginUpdateResult> UpdateInstalledAssistantAsync(IAvailablePlugin plugin, string lua, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (plugin.Type is not PluginType.ASSISTANT)
|
||||||
|
return UpdateError(plugin, plugin.LocalPath, TB("Only assistant plugins can be edited."));
|
||||||
|
|
||||||
|
if (plugin.IsInternal)
|
||||||
|
return UpdateError(plugin, plugin.LocalPath, TB("Internal assistant plugins cannot be edited."));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||||
|
return UpdateError(plugin, string.Empty, TB("The assistant plugin has no local directory."));
|
||||||
|
|
||||||
|
if (!TryGetPluginRoot(PluginType.ASSISTANT, out var assistantPluginsRoot, out var rootIssue))
|
||||||
|
return UpdateError(plugin, plugin.LocalPath, rootIssue);
|
||||||
|
|
||||||
|
var pluginDirectory = plugin.LocalPath;
|
||||||
|
if (!IsPathInsideDirectory(assistantPluginsRoot, pluginDirectory) || IsSameDirectory(assistantPluginsRoot, pluginDirectory))
|
||||||
|
return UpdateError(plugin, pluginDirectory, TB("The assistant plugin directory is outside the local assistant plugin directory."));
|
||||||
|
|
||||||
|
if (!Directory.Exists(pluginDirectory))
|
||||||
|
return UpdateError(plugin, pluginDirectory, TB("The assistant plugin directory does not exist."));
|
||||||
|
|
||||||
|
var pluginFile = Path.Join(pluginDirectory, PLUGIN_FILE_NAME);
|
||||||
|
if (!IsPathInsideDirectory(pluginDirectory, pluginFile))
|
||||||
|
return UpdateError(plugin, pluginDirectory, TB("The plugin file is outside the assistant plugin directory."));
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
var tempFile = string.Empty;
|
||||||
|
var backupFile = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validation = await this.ValidateInPluginDirectoryAsync(lua, pluginDirectory, token);
|
||||||
|
if (!validation.Success || validation.AssistantPlugin is null)
|
||||||
|
return UpdateError(plugin, pluginDirectory, validation.Issue);
|
||||||
|
|
||||||
|
var assistantPlugin = validation.AssistantPlugin;
|
||||||
|
if (assistantPlugin.Id != plugin.Id)
|
||||||
|
return UpdateError(plugin, pluginDirectory, TB("The edited assistant plugin must keep the same plugin ID."));
|
||||||
|
|
||||||
|
var pluginCode = lua.Trim();
|
||||||
|
tempFile = Path.Join(pluginDirectory, $"{PLUGIN_FILE_NAME}.tmp-{Guid.NewGuid():N}");
|
||||||
|
backupFile = Path.Join(pluginDirectory, $"{PLUGIN_FILE_NAME}.backup-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(tempFile, pluginCode, Encoding.UTF8, token);
|
||||||
|
|
||||||
|
if (File.Exists(pluginFile))
|
||||||
|
File.Replace(tempFile, pluginFile, backupFile);
|
||||||
|
else
|
||||||
|
File.Move(tempFile, pluginFile);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await PluginFactory.LoadAll(token);
|
||||||
|
if (File.Exists(backupFile))
|
||||||
|
File.Delete(backupFile);
|
||||||
|
|
||||||
|
this.logger.LogInformation($"Updated assistant plugin '{assistantPlugin.Name}' ({assistantPlugin.Id}) at '{pluginFile}'.");
|
||||||
|
return new(true, assistantPlugin.Id, assistantPlugin.Name, pluginDirectory, string.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception reloadException)
|
||||||
|
{
|
||||||
|
this.logger.LogError(reloadException, $"Failed to reload plugins after editing assistant plugin '{plugin.Name}' ({plugin.Id}).");
|
||||||
|
await this.TryRestoreEditedAssistantPluginAsync(pluginFile, backupFile, token);
|
||||||
|
return UpdateError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), reloadException.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, $"Failed to update assistant plugin '{plugin.Name}' ({plugin.Id}) at '{pluginDirectory}'.");
|
||||||
|
await this.TryRestoreEditedAssistantPluginAsync(pluginFile, backupFile, token);
|
||||||
|
return UpdateError(plugin, pluginDirectory, string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.TryDeleteFile(tempFile, "assistant plugin edit temp file");
|
||||||
|
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PluginValidationResult> ValidateInPluginDirectoryAsync(string lua, string pluginDirectory, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(lua))
|
||||||
|
return PluginValidationResult.Failure(TB("No Lua plugin code was generated."));
|
||||||
|
|
||||||
|
if (!PluginFactory.IsInitialized)
|
||||||
|
return PluginValidationResult.Failure(TB("The plugin system is not initialized yet."));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ValidatePluginCodeAsync(
|
||||||
|
pluginDirectory,
|
||||||
|
lua.Trim(),
|
||||||
|
[PluginType.ASSISTANT],
|
||||||
|
TB("The edited plugin is not an assistant plugin. Issue: {0}"),
|
||||||
|
TB("The edited assistant plugin is invalid. Issue: {0}"),
|
||||||
|
TB("The edited assistant plugin uses the ID of another installed plugin."),
|
||||||
|
token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, "Failed to validate edited assistant plugin.");
|
||||||
|
return PluginValidationResult.Failure(string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TryRestoreEditedAssistantPluginAsync(string pluginFile, string backupFile, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(backupFile) || !File.Exists(backupFile))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (File.Exists(pluginFile))
|
||||||
|
File.Delete(pluginFile);
|
||||||
|
|
||||||
|
File.Move(backupFile, pluginFile);
|
||||||
|
await PluginFactory.LoadAll(token);
|
||||||
|
}
|
||||||
|
catch (Exception restoreException)
|
||||||
|
{
|
||||||
|
this.logger.LogError(restoreException, $"Failed to restore assistant plugin file '{pluginFile}' after a failed edit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
private static bool IsPathInsideDirectory(string parentDirectory, string path)
|
||||||
|
{
|
||||||
|
var parentPath = Path.GetFullPath(parentDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
return childPath.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSameDirectory(string firstDirectory, string secondDirectory)
|
||||||
|
{
|
||||||
|
var firstPath = Path.GetFullPath(firstDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
var secondPath = Path.GetFullPath(secondDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
return string.Equals(firstPath, secondPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteStagingDirectory(string stagingDirectory) => TryDeleteDirectory(stagingDirectory, "assistant plugin staging", this.logger);
|
||||||
|
|
||||||
|
private static void TryDeleteDirectory(string directory, string directoryDescription, ILogger logger)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, $"Failed to delete {directoryDescription} directory '{directory}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteFile(string filePath, string fileDescription)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, $"Failed to delete {fileDescription} '{filePath}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,142 @@
|
|||||||
|
using System.Text;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The plugin types a user may import from an archive.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly PluginType[] IMPORTABLE_PLUGIN_TYPES = [PluginType.ASSISTANT, PluginType.CONFIGURATION, PluginType.LANGUAGE];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs a plugin archive that contains exactly one <c>plugin.lua</c> file.
|
||||||
|
/// 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, Func<PluginImportPreview, Task<bool>> confirmAsync, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!this.settingsManager.ConfigurationData.App.AllowUserToImportPlugins)
|
||||||
|
return Error(TB("Your organization has disabled importing plugins."));
|
||||||
|
|
||||||
|
if (!FileTypes.IsAllowedPath(archivePath, FileTypes.PLUGIN_ARCHIVE))
|
||||||
|
return Error(TB("Please select a plugin archive with the extension .mwplugin or .zip."));
|
||||||
|
|
||||||
|
if (!File.Exists(archivePath))
|
||||||
|
return Error(TB("The selected plugin archive does not exist."));
|
||||||
|
|
||||||
|
if (!PluginFactory.IsInitialized)
|
||||||
|
return Error(TB("The plugin system is not initialized yet."));
|
||||||
|
|
||||||
|
await this.installSemaphore.WaitAsync(token);
|
||||||
|
var stagingDirectory = Path.Join(Path.GetTempPath(), $"plugin-import.staging-{Guid.NewGuid():N}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
PluginArchive.Extract(archivePath, stagingDirectory);
|
||||||
|
|
||||||
|
var pluginFiles = Directory.EnumerateFiles(stagingDirectory, PLUGIN_FILE_NAME, SearchOption.AllDirectories).ToArray();
|
||||||
|
if (pluginFiles.Length != 1)
|
||||||
|
return Error(TB("The plugin archive must contain exactly one plugin.lua file."));
|
||||||
|
|
||||||
|
var pluginFile = pluginFiles[0];
|
||||||
|
var pluginDirectory = Path.GetDirectoryName(pluginFile)!;
|
||||||
|
var pluginCode = await File.ReadAllTextAsync(pluginFile, Encoding.UTF8, token);
|
||||||
|
var validation = await ValidatePluginCodeAsync(
|
||||||
|
pluginDirectory,
|
||||||
|
pluginCode.Trim(),
|
||||||
|
IMPORTABLE_PLUGIN_TYPES,
|
||||||
|
TB("Only assistant, configuration, and language plugins can be imported."),
|
||||||
|
TB("The imported plugin is invalid. Issue: {0}"),
|
||||||
|
TB("The imported plugin uses the ID of another installed plugin."),
|
||||||
|
token);
|
||||||
|
|
||||||
|
if (!validation.Success || validation.Plugin is null)
|
||||||
|
return Error(validation.Issue);
|
||||||
|
|
||||||
|
var plugin = validation.Plugin;
|
||||||
|
var eligibilityIssue = this.GetImportEligibilityIssue(plugin);
|
||||||
|
if (!string.IsNullOrEmpty(eligibilityIssue))
|
||||||
|
return Error(eligibilityIssue);
|
||||||
|
|
||||||
|
// 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 = GetReplacementIssue(plugin.Id, plugin.Type);
|
||||||
|
if (!string.IsNullOrEmpty(replacementIssue))
|
||||||
|
return Error(replacementIssue);
|
||||||
|
|
||||||
|
// Local plugins live in the directory of their type, never in the enterprise
|
||||||
|
// configuration directory. Only a config server deploys plugins there:
|
||||||
|
if (!TryGetPluginRoot(plugin.Type, out var pluginRoot, out var rootIssue))
|
||||||
|
return Error(rootIssue);
|
||||||
|
|
||||||
|
// 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(plugin)))
|
||||||
|
return CancelledByUser();
|
||||||
|
|
||||||
|
return await this.InstallStagedPluginAsync(pluginRoot, validation with { StagingDirectory = pluginDirectory }, plugin.Type, token);
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, "Failed to extract or validate plugin archive '{ArchivePath}'.", archivePath);
|
||||||
|
return Error(string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
|
this.installSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the rules that depend on the type of the plugin inside the archive.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The validated plugin from the archive.</param>
|
||||||
|
/// <returns>A user-facing issue when the archive must not be installed, an empty string otherwise.</returns>
|
||||||
|
private string GetImportEligibilityIssue(PluginBase plugin) => plugin switch
|
||||||
|
{
|
||||||
|
// A plugin the user imports by hand never comes from a config server. We reject such
|
||||||
|
// archives because AI Studio trusts this self-declared flag: an imported plugin claiming it
|
||||||
|
// would be neither replaceable nor deletable through the user interface:
|
||||||
|
PluginAssistants { IsManagedByConfigServer: true } => TB("This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins."),
|
||||||
|
|
||||||
|
PluginConfiguration configurationPlugin => this.GetConfigurationImportEligibilityIssue(configurationPlugin),
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the additional rules for importing a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A configuration takes effect immediately and has no on/off switch, so it gets its own
|
||||||
|
/// organization permission on top of the general import permission.
|
||||||
|
/// </remarks>
|
||||||
|
private string GetConfigurationImportEligibilityIssue(PluginConfiguration configurationPlugin)
|
||||||
|
{
|
||||||
|
if (!this.settingsManager.ConfigurationData.App.AllowUserToImportConfigurationPlugins)
|
||||||
|
return TB("Your organization has disabled importing configuration plugins.");
|
||||||
|
|
||||||
|
if (configurationPlugin.DeployedUsingConfigServer is true)
|
||||||
|
return TB("This plugin archive declares itself as managed by a config server. Only the IT department of your organization might deploy such plugins.");
|
||||||
|
|
||||||
|
// Never let an imported configuration take the place of one the organization deployed. This
|
||||||
|
// also covers a deployed configuration which currently cannot be loaded, e.g. because of an
|
||||||
|
// error in its Lua code:
|
||||||
|
if (PluginFactory.IsEnterpriseConfigurationPlugin(configurationPlugin.Id))
|
||||||
|
return TB("Your organization deployed a configuration with the same ID. An imported configuration must not take its place.");
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
using System.Text;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
private async Task<AssistantPluginInstallResult> InstallStagedPluginAsync(string pluginRoot, PluginValidationResult validation, PluginType pluginType, CancellationToken token)
|
||||||
|
{
|
||||||
|
var stagingDirectory = validation.StagingDirectory;
|
||||||
|
var plugin = validation.Plugin!;
|
||||||
|
string? backupDirectory = null;
|
||||||
|
string? finalDirectory = null;
|
||||||
|
var replacedExisting = false;
|
||||||
|
var movedIntoPlace = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(pluginRoot);
|
||||||
|
finalDirectory = DetermineFinalDirectory(pluginRoot, plugin, pluginType);
|
||||||
|
if (!IsPathInsideDirectory(pluginRoot, finalDirectory))
|
||||||
|
return Error(TB("The resolved plugin directory is outside the plugin directory."));
|
||||||
|
|
||||||
|
var replacementIssue = GetReplacementIssue(plugin.Id, pluginType);
|
||||||
|
if (!string.IsNullOrWhiteSpace(replacementIssue))
|
||||||
|
return Error(replacementIssue);
|
||||||
|
|
||||||
|
if (Directory.Exists(finalDirectory))
|
||||||
|
{
|
||||||
|
replacedExisting = true;
|
||||||
|
|
||||||
|
// The backup goes to a directory outside the plugin root, so the plugin loader
|
||||||
|
// cannot discover it during the reload below. Otherwise, the previous version
|
||||||
|
// would be loaded a second time, next to the version we are installing:
|
||||||
|
backupDirectory = CreateInstallBackupDirectory(plugin);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(backupDirectory)!);
|
||||||
|
Directory.Move(finalDirectory, backupDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.Move(stagingDirectory, finalDirectory);
|
||||||
|
movedIntoPlace = true;
|
||||||
|
await PluginFactory.LoadAll(token);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(backupDirectory))
|
||||||
|
TryDeleteDirectory(backupDirectory, "plugin backup", this.logger);
|
||||||
|
|
||||||
|
this.logger.LogInformation("Installed plugin '{PluginName}' ({PluginId}, {PluginType}) to '{PluginDirectory}'.", plugin.Name, plugin.Id, pluginType, finalDirectory);
|
||||||
|
return new(true, plugin.Id, plugin.Name, finalDirectory, replacedExisting, string.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError(e, "Failed to install plugin.");
|
||||||
|
|
||||||
|
// Only remove the target directory when this installation actually moved the plugin
|
||||||
|
// there. Otherwise, when moving the previous plugin into the backup directory failed,
|
||||||
|
// we would delete the still intact previous plugin:
|
||||||
|
if (movedIntoPlace && !string.IsNullOrWhiteSpace(finalDirectory) && Directory.Exists(finalDirectory))
|
||||||
|
TryDeleteDirectory(finalDirectory, "failed assistant plugin installation", this.logger);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !string.IsNullOrWhiteSpace(finalDirectory) && !Directory.Exists(finalDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Move(backupDirectory, finalDirectory);
|
||||||
|
await PluginFactory.LoadAll(CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception restoreException)
|
||||||
|
{
|
||||||
|
this.logger.LogError(restoreException, "Failed to restore the previous assistant plugin after a failed installation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Error(string.Format(TB("Unexpected error: {0}"), e.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.TryDeleteStagingDirectory(stagingDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and validates plugin code that is not installed yet.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginDirectory">The staging directory the plugin currently lives in.</param>
|
||||||
|
/// <param name="pluginCode">The <c>plugin.lua</c> content to validate.</param>
|
||||||
|
/// <param name="acceptedTypes">The plugin types the caller accepts.</param>
|
||||||
|
/// <param name="wrongTypeIssue">Issue when the plugin has another type. Gets the plugin issues as {0}.</param>
|
||||||
|
/// <param name="invalidPluginIssue">Issue when the plugin is of an accepted type, but invalid. Gets the plugin issues as {0}.</param>
|
||||||
|
/// <param name="conflictingPluginIdIssue">Issue when another plugin already uses this plugin ID.</param>
|
||||||
|
/// <param name="token">Cancellation token for running the Lua code.</param>
|
||||||
|
/// <returns>The validation result, including the loaded plugin when it passed.</returns>
|
||||||
|
private static async Task<PluginValidationResult> ValidatePluginCodeAsync(string pluginDirectory, string pluginCode, IReadOnlyCollection<PluginType> acceptedTypes,
|
||||||
|
string wrongTypeIssue, string invalidPluginIssue, string conflictingPluginIdIssue, CancellationToken token)
|
||||||
|
{
|
||||||
|
// The plugin is not installed yet: it sits in a staging directory outside the installed
|
||||||
|
// plugins directory. We allow that directory as the module base, so the plugin can load its
|
||||||
|
// own Lua modules, e.g., an icon.lua, while we validate it:
|
||||||
|
var plugin = await PluginFactory.Load(pluginDirectory, pluginCode, token, pluginDirectory);
|
||||||
|
if (!acceptedTypes.Contains(plugin.Type))
|
||||||
|
return PluginValidationResult.Failure(string.Format(wrongTypeIssue, string.Join("; ", plugin.Issues)));
|
||||||
|
|
||||||
|
if (!plugin.IsValid)
|
||||||
|
return PluginValidationResult.Failure(string.Format(invalidPluginIssue, string.Join("; ", plugin.Issues)));
|
||||||
|
|
||||||
|
// Plugin IDs must be unique across all plugin types: several lookups resolve a plugin by its
|
||||||
|
// ID alone, e.g., the base language plugin in PluginFactory.Starting. A plugin carrying the
|
||||||
|
// ID of a plugin of another type would break those lookups. Reusing the ID of another local
|
||||||
|
// plugin of the same type stays allowed: that is how updating one works.
|
||||||
|
if (PluginFactory.AvailablePlugins.Any(availablePlugin => availablePlugin.Id == plugin.Id && (availablePlugin.IsInternal || availablePlugin.Type != plugin.Type)))
|
||||||
|
return PluginValidationResult.Failure(conflictingPluginIdIssue);
|
||||||
|
|
||||||
|
return new(true, string.Empty, plugin, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the directory local plugins of the given type are installed into.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryGetPluginRoot(PluginType pluginType, out string pluginRoot, out string issue)
|
||||||
|
{
|
||||||
|
pluginRoot = string.Empty;
|
||||||
|
issue = string.Empty;
|
||||||
|
|
||||||
|
var dataDirectory = SettingsManager.DataDirectory;
|
||||||
|
if (string.IsNullOrWhiteSpace(dataDirectory))
|
||||||
|
{
|
||||||
|
issue = TB("The AI Studio data directory is not initialized yet.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginRoot = Path.Join(dataDirectory, "plugins", pluginType.GetDirectory());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DetermineFinalDirectory(string pluginRoot, IPluginMetadata plugin, PluginType pluginType)
|
||||||
|
{
|
||||||
|
var existingPlugin = FindReplaceablePlugin(plugin.Id, pluginType);
|
||||||
|
return existingPlugin is not null
|
||||||
|
? existingPlugin.LocalPath
|
||||||
|
: Path.Join(pluginRoot, CreatePluginDirectoryName(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the local plugin that an installation with the given ID and type would replace.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">The ID of the plugin about to be installed.</param>
|
||||||
|
/// <param name="pluginType">The type of the plugin about to be installed.</param>
|
||||||
|
/// <returns>The plugin that would be replaced, or null when the installation adds a new plugin.</returns>
|
||||||
|
private static IAvailablePlugin? FindReplaceablePlugin(Guid pluginId, PluginType pluginType) => PluginFactory.AvailablePlugins
|
||||||
|
.OfType<IAvailablePlugin>()
|
||||||
|
.FirstOrDefault(plugin => plugin.Type == pluginType && 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="plugin">The validated plugin from the archive.</param>
|
||||||
|
/// <returns>The preview shown to the user before the installation starts.</returns>
|
||||||
|
private static PluginImportPreview CreateImportPreview(PluginBase plugin) => new(
|
||||||
|
plugin,
|
||||||
|
FindReplaceablePlugin(plugin.Id, plugin.Type),
|
||||||
|
plugin is PluginConfiguration configurationPlugin ? CreateConfigurationImportSummary(configurationPlugin) : null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collects what a configuration plugin would set up once it is installed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The plugin was loaded as a dry run, so nothing of this is stored yet. The destinations come
|
||||||
|
/// from the parsed configuration objects, which is why the preview can name the host a provider
|
||||||
|
/// would talk to.
|
||||||
|
/// </remarks>
|
||||||
|
private static ConfigurationPluginImportSummary CreateConfigurationImportSummary(PluginConfiguration configurationPlugin)
|
||||||
|
{
|
||||||
|
var configObjects = configurationPlugin.ConfigObjects.ToList();
|
||||||
|
var destinations = configObjects
|
||||||
|
.Where(configObject => configObject.Type is PluginConfigurationObjectType.LLM_PROVIDER
|
||||||
|
or PluginConfigurationObjectType.EMBEDDING_PROVIDER
|
||||||
|
or PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER
|
||||||
|
or PluginConfigurationObjectType.DATA_SOURCE)
|
||||||
|
.Select(configObject => new ConfigurationPluginDestination(configObject.Type, configObject.Name, configObject.Endpoint))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new(
|
||||||
|
Destinations: destinations,
|
||||||
|
ChatTemplates: CountObjects(PluginConfigurationObjectType.CHAT_TEMPLATE),
|
||||||
|
Profiles: CountObjects(PluginConfigurationObjectType.PROFILE),
|
||||||
|
DocumentAnalysisPolicies: CountObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY),
|
||||||
|
DeclaredSettings: configurationPlugin.DeclaredSettingsCount,
|
||||||
|
MandatoryInfos: configurationPlugin.MandatoryInfos.Count,
|
||||||
|
Introductions: configurationPlugin.Introductions.Count);
|
||||||
|
|
||||||
|
int CountObjects(PluginConfigurationObjectType type) => configObjects.Count(configObject => configObject.Type == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether an installation may replace the plugin that currently uses the given ID.
|
||||||
|
/// Plugins deployed by a Config Server belong to the organization's IT, so neither an import nor
|
||||||
|
/// the Assistant Builder may overwrite them.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">The ID of the plugin about to be installed.</param>
|
||||||
|
/// <param name="pluginType">The type of the plugin about to be installed.</param>
|
||||||
|
/// <returns>A user-facing issue when the existing plugin must not be replaced, an empty string otherwise.</returns>
|
||||||
|
private static string GetReplacementIssue(Guid pluginId, PluginType pluginType)
|
||||||
|
{
|
||||||
|
var existingPlugin = FindReplaceablePlugin(pluginId, pluginType);
|
||||||
|
if (existingPlugin is null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (existingPlugin.IsManagedByConfigServer)
|
||||||
|
return TB("Plugins deployed by your organization cannot be replaced.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(existingPlugin.LocalPath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// The metadata above and the running plugin read the same Lua field. We check both, though,
|
||||||
|
// just like the deletion path does:
|
||||||
|
var runningPlugin = PluginFactory.RunningPlugins
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == pluginId && IsSameDirectory(candidate.PluginPath, existingPlugin.LocalPath));
|
||||||
|
|
||||||
|
var isManagedByConfigServer = runningPlugin switch
|
||||||
|
{
|
||||||
|
PluginAssistants assistantPlugin => assistantPlugin.IsManagedByConfigServer,
|
||||||
|
PluginConfiguration configurationPlugin => configurationPlugin.DeployedUsingConfigServer ?? false,
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return isManagedByConfigServer
|
||||||
|
? TB("Plugins deployed by your organization cannot be replaced.")
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateInstallBackupDirectory(IPluginMetadata plugin)
|
||||||
|
{
|
||||||
|
var backupRoot = Path.Join(SettingsManager.DataDirectory, INSTALL_BACKUP_DIRECTORY);
|
||||||
|
return Path.Join(backupRoot, $"assistant-{plugin.Id:N}-{Guid.NewGuid():N}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreatePluginDirectoryName(IPluginMetadata plugin)
|
||||||
|
{
|
||||||
|
var safeName = CreateSafeDirectoryNamePart(plugin.Name);
|
||||||
|
return $"{safeName}-{plugin.Id:N}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSafeDirectoryNamePart(string name)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars().ToHashSet();
|
||||||
|
|
||||||
|
foreach (var character in name.Trim())
|
||||||
|
{
|
||||||
|
if (char.IsLetterOrDigit(character))
|
||||||
|
{
|
||||||
|
sb.Append(char.ToLowerInvariant(character));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character is '-' or '_' or '.' && !invalidChars.Contains(character))
|
||||||
|
{
|
||||||
|
sb.Append(character);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendSeparator();
|
||||||
|
}
|
||||||
|
|
||||||
|
var safeName = sb.ToString().Trim('-', '.');
|
||||||
|
if (safeName.Length > DIRECTORY_PREFIX_MAX_LEN)
|
||||||
|
safeName = safeName[..DIRECTORY_PREFIX_MAX_LEN].Trim('-', '.');
|
||||||
|
|
||||||
|
// Fallback for a plugin name without any usable character. The plugin ID is appended by the
|
||||||
|
// caller, so the directory stays unique either way:
|
||||||
|
return string.IsNullOrWhiteSpace(safeName)
|
||||||
|
? "plugin"
|
||||||
|
: safeName;
|
||||||
|
|
||||||
|
void AppendSeparator()
|
||||||
|
{
|
||||||
|
if (sb.Length == 0 || sb[^1] == '-')
|
||||||
|
return;
|
||||||
|
|
||||||
|
sb.Append('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.AssistantSessions;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs, updates, and removes the plugins AI Studio manages locally.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The implementation is split across several files:<br/>
|
||||||
|
/// - <c>PluginInstallService.AssistantBuilder.cs</c>: installing generated assistant plugin code<br/>
|
||||||
|
/// - <c>PluginInstallService.Editing.cs</c>: editing an installed assistant plugin<br/>
|
||||||
|
/// - <c>PluginInstallService.Import.cs</c>: importing plugin archives<br/>
|
||||||
|
/// - <c>PluginInstallService.Delete.cs</c>: removing installed plugins<br/>
|
||||||
|
/// - <c>PluginInstallService.Installation.cs</c>: the shared validation and installation steps<br/>
|
||||||
|
/// - <c>PluginInstallService.FileSystem.cs</c>: the shared path and directory helpers
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class PluginInstallService
|
||||||
|
{
|
||||||
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginInstallService).Namespace, nameof(PluginInstallService));
|
||||||
|
|
||||||
|
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||||
|
private const string ASSISTANT_BUILDER_DIRECTORY_PREFIX = "assistant-builder";
|
||||||
|
private const string DELETE_BACKUP_DIRECTORY = ".plugin-delete-backups";
|
||||||
|
private const string INSTALL_BACKUP_DIRECTORY = ".plugin-install-backups";
|
||||||
|
private const int DIRECTORY_PREFIX_MAX_LEN = 80;
|
||||||
|
|
||||||
|
private readonly ILogger<PluginInstallService> logger;
|
||||||
|
private readonly SettingsManager settingsManager;
|
||||||
|
private readonly AssistantSessionService assistantSessionService;
|
||||||
|
private readonly MediaTranscriptionService mediaTranscriptionService;
|
||||||
|
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);
|
||||||
|
|
||||||
|
private static PluginDeleteResult DeleteError(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, issue);
|
||||||
|
|
||||||
|
private static AssistantPluginUpdateResult UpdateError(IPluginMetadata plugin, string pluginDirectory, string issue) => new(false, plugin.Id, plugin.Name, pluginDirectory, issue);
|
||||||
|
|
||||||
|
public PluginInstallService(ILogger<PluginInstallService> logger, SettingsManager settingsManager, AssistantSessionService assistantSessionService, MediaTranscriptionService mediaTranscriptionService)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.settingsManager = settingsManager;
|
||||||
|
this.assistantSessionService = assistantSessionService;
|
||||||
|
this.mediaTranscriptionService = mediaTranscriptionService;
|
||||||
|
this.logger.LogInformation("The plugin install service has been initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PluginValidationResult(bool Success, string StagingDirectory, PluginBase? Plugin, string Issue)
|
||||||
|
{
|
||||||
|
public static PluginValidationResult Failure(string issue) => new(false, string.Empty, null, issue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The validated plugin as an assistant plugin, or null when it has another type.
|
||||||
|
/// </summary>
|
||||||
|
public PluginAssistants? AssistantPlugin => this.Plugin as PluginAssistants;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed record PluginShareResult(bool Success, string PluginName, string ArchivePath, string Issue, bool Cancelled = false);
|
||||||
242
app/MindWork AI Studio/Tools/Services/PluginShareService.cs
Normal file
242
app/MindWork AI Studio/Tools/Services/PluginShareService.cs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed class PluginShareService(NativeShareService nativeShareService, RustService rustService, SettingsManager settingsManager, ILogger<PluginShareService> logger)
|
||||||
|
{
|
||||||
|
private static PluginShareResult ShareError(IAvailablePlugin plugin, string issue) => new(false, plugin.Name, string.Empty, issue);
|
||||||
|
|
||||||
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginShareService).Namespace, nameof(PluginShareService));
|
||||||
|
|
||||||
|
private const string PLUGIN_FILE_NAME = "plugin.lua";
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Keep in sync with SHARE_DIRECTORY_NAME in runtime/src/share_sheet.rs: the runtime only hands
|
||||||
|
/// archives from a directory with this name to the native share sheet.
|
||||||
|
/// </remarks>
|
||||||
|
private const string TEMPORARY_ARCHIVE_DIRECTORY = "mindwork-ai-studio-plugin-shares";
|
||||||
|
|
||||||
|
private const int TEMPORARY_ARCHIVE_RETENTION_HOURS = 24;
|
||||||
|
private const int FILE_NAME_PREFIX_MAX_LEN = 80;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a shareable plugin archive from a local plugin and hands it over to the user.
|
||||||
|
/// The archive contains the plugin root contents, so <c>plugin.lua</c> is located at the archive root.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On Windows and macOS, the archive is created in a temporary directory and handed over to the
|
||||||
|
/// native share sheet. Linux has no such share sheet, since the XDG desktop portals do not provide
|
||||||
|
/// a share interface. Thus, the archive is exported to a location of the user's choice there.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="plugin">The local plugin to archive and share.</param>
|
||||||
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
|
/// <returns>The share result, including the archive path when successful.</returns>
|
||||||
|
public async Task<PluginShareResult> ShareAsync(IAvailablePlugin plugin, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (plugin.IsInternal)
|
||||||
|
return ShareError(plugin, TB("Internal plugins cannot be shared."));
|
||||||
|
|
||||||
|
if (plugin.IsManagedByConfigServer)
|
||||||
|
return ShareError(plugin, TB("Config Server managed plugins cannot be shared."));
|
||||||
|
|
||||||
|
if (!settingsManager.ConfigurationData.App.AllowUserToSharePlugins)
|
||||||
|
return ShareError(plugin, TB("Your organization has disabled sharing plugins."));
|
||||||
|
|
||||||
|
if (!TryGetPluginRoot(plugin, out var pluginRoot, out var issue))
|
||||||
|
return ShareError(plugin, issue);
|
||||||
|
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
return await this.ExportAsync(plugin, pluginRoot, token);
|
||||||
|
|
||||||
|
return await this.ShareViaNativeSheetAsync(plugin, pluginRoot, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the user for a target location and writes the plugin archive to it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The local plugin to archive.</param>
|
||||||
|
/// <param name="pluginRoot">The validated plugin root directory.</param>
|
||||||
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
|
/// <returns>The share result, including the chosen archive path when successful.</returns>
|
||||||
|
private async Task<PluginShareResult> ExportAsync(IAvailablePlugin plugin, string pluginRoot, CancellationToken token)
|
||||||
|
{
|
||||||
|
var suggestedFileName = $"{CreateSafeFileNamePrefix(plugin.Name)}{PluginArchive.PLUGIN_FILE_EXTENSION}";
|
||||||
|
var saveResponse = await rustService.SaveFile(TB("Export plugin archive"), [FileTypes.PLUGIN_ARCHIVE], suggestedFileName);
|
||||||
|
if (saveResponse.UserCancelled)
|
||||||
|
return new(false, plugin.Name, string.Empty, string.Empty, true);
|
||||||
|
|
||||||
|
var archivePath = saveResponse.SaveFilePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// The save dialog already asked the user about overwriting an existing file.
|
||||||
|
// ZipFile.CreateFromDirectory would fail on an existing file, though:
|
||||||
|
if (File.Exists(archivePath))
|
||||||
|
File.Delete(archivePath);
|
||||||
|
|
||||||
|
ZipFile.CreateFromDirectory(pluginRoot, archivePath, CompressionLevel.Optimal, false);
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
logger.LogInformation("Exported plugin '{PluginName}' ({PluginId}) to the archive '{ArchivePath}'.", plugin.Name, plugin.Id, archivePath);
|
||||||
|
return new(true, plugin.Name, archivePath, string.Empty);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
logger.LogError(exception, "Failed to export plugin '{PluginName}' ({PluginId}).", plugin.Name, plugin.Id);
|
||||||
|
return ShareError(plugin, string.Format(TB("Unexpected error: {0}"), exception.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the plugin archive in a temporary directory and opens the native share sheet for it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The local plugin to archive and share.</param>
|
||||||
|
/// <param name="pluginRoot">The validated plugin root directory.</param>
|
||||||
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
|
/// <returns>The share result, including the retained temporary archive path when successful.</returns>
|
||||||
|
private async Task<PluginShareResult> ShareViaNativeSheetAsync(IAvailablePlugin plugin, string pluginRoot, CancellationToken token)
|
||||||
|
{
|
||||||
|
var archiveDirectory = Path.Join(Path.GetTempPath(), TEMPORARY_ARCHIVE_DIRECTORY);
|
||||||
|
var archivePath = Path.Join(archiveDirectory, $"{CreateSafeFileNamePrefix(plugin.Name)}-{plugin.Id:N}-{Guid.NewGuid():N}{PluginArchive.PLUGIN_FILE_EXTENSION}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
Directory.CreateDirectory(archiveDirectory);
|
||||||
|
this.CleanUpExpiredArchives(archiveDirectory);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
ZipFile.CreateFromDirectory(pluginRoot, archivePath, CompressionLevel.Optimal, false);
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (!await nativeShareService.Share(archivePath))
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
return ShareError(plugin, TB("The native share dialog could not be opened."));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Created plugin archive '{ArchivePath}' for plugin '{PluginName}' ({PluginId}).", archivePath, plugin.Name, plugin.Id);
|
||||||
|
return new(true, plugin.Name, archivePath, string.Empty);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
logger.LogError(exception, "Failed to create a share archive for plugin '{PluginName}' ({PluginId}).", plugin.Name, plugin.Id);
|
||||||
|
return ShareError(plugin, string.Format(TB("Unexpected error: {0}"), exception.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetPluginRoot(IAvailablePlugin plugin, out string pluginRoot, out string issue)
|
||||||
|
{
|
||||||
|
pluginRoot = string.Empty;
|
||||||
|
issue = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(plugin.LocalPath))
|
||||||
|
{
|
||||||
|
issue = TB("The plugin has no local directory.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pluginRoot = Path.GetFullPath(plugin.LocalPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
issue = string.Format(TB("The plugin directory is invalid: {0}"), exception.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(pluginRoot))
|
||||||
|
{
|
||||||
|
issue = TB("The plugin directory does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginFile = Path.Join(pluginRoot, PLUGIN_FILE_NAME);
|
||||||
|
if (!IsPathInsideDirectory(pluginRoot, pluginFile) || !File.Exists(pluginFile))
|
||||||
|
{
|
||||||
|
issue = TB("The plugin directory does not contain a plugin.lua file.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanUpExpiredArchives(string archiveDirectory)
|
||||||
|
{
|
||||||
|
var expiry = DateTime.UtcNow.AddHours(-TEMPORARY_ARCHIVE_RETENTION_HOURS);
|
||||||
|
foreach (var archivePath in Directory.EnumerateFiles(archiveDirectory, $"*{PluginArchive.PLUGIN_FILE_EXTENSION}", SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.GetLastWriteTimeUtc(archivePath) < expiry)
|
||||||
|
File.Delete(archivePath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "Failed to delete expired plugin archive '{ArchivePath}'.", archivePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSafeFileNamePrefix(string pluginName)
|
||||||
|
{
|
||||||
|
var invalidCharacters = Path.GetInvalidFileNameChars().ToHashSet();
|
||||||
|
var fileName = new string(pluginName
|
||||||
|
.Trim()
|
||||||
|
.Select(character => char.IsLetterOrDigit(character) || ((character is '-' or '_' or '.') && !invalidCharacters.Contains(character))
|
||||||
|
? character
|
||||||
|
: '-')
|
||||||
|
.ToArray())
|
||||||
|
.Trim('-', '.');
|
||||||
|
|
||||||
|
if (fileName.Length > FILE_NAME_PREFIX_MAX_LEN)
|
||||||
|
fileName = fileName[..FILE_NAME_PREFIX_MAX_LEN].Trim('-', '.');
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(fileName) ? "plugin" : fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPathInsideDirectory(string parentDirectory, string path)
|
||||||
|
{
|
||||||
|
var parentPath = Path.GetFullPath(parentDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
var childPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
return childPath.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteArchive(string archivePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(archivePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(archivePath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "Failed to delete temporary plugin archive '{ArchivePath}'.", archivePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/MindWork AI Studio/Tools/Services/RustService.Share.cs
Normal file
44
app/MindWork AI Studio/Tools/Services/RustService.Share.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
public sealed partial class RustService
|
||||||
|
{
|
||||||
|
public async Task<bool> ShareFile(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await this.http.PostAsJsonAsync("/share/file", new ShareFileRequest(filePath), this.jsonRustSerializerOptions);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
this.logger?.LogError($"The Rust runtime rejected the share request: {response.StatusCode}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ShareFileResponse>(this.jsonRustSerializerOptions);
|
||||||
|
if (result?.Success == true)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
this.logger?.LogError($"The native share sheet could not be opened: {result?.Issue ?? "Unknown error"}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException exception)
|
||||||
|
{
|
||||||
|
this.logger?.LogWarning(exception, "Failed to reach the Rust runtime share endpoint.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException exception)
|
||||||
|
{
|
||||||
|
this.logger?.LogWarning(exception, "Timed out while reaching the Rust runtime share endpoint.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
this.logger?.LogError(exception, "Failed to process the Rust runtime share response.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ShareFileRequest(string FilePath);
|
||||||
|
|
||||||
|
private sealed record ShareFileResponse(bool Success, string Issue);
|
||||||
|
}
|
||||||
@ -1,3 +1,23 @@
|
|||||||
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
|
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
|
||||||
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings.
|
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings. When you want to test it, you have to enable this preview feature in your app settings.
|
||||||
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
|
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
|
||||||
|
- Added a share button for assistants, configurations, and language plugins. It uses the native share dialog on Windows and macOS. For Linux, we added an export option, which stores the plugin archive at a location of your choice. When you work on a translation for a new language, you can now hand your current state to testers or to us with one click.
|
||||||
|
- Added the option to install plugin archives from your files: use the import button on the plugin page, or simply drop an archive onto that page. Assistants, configurations, and language plugins are supported, and plugin archives now have their own file extension `.mwplugin`. Before installing a configuration, AI Studio shows what it sets up: which LLM providers and data sources it adds and where each of them sends your data, plus how many settings it takes control of. A configuration takes effect right away and has no on/off switch, so please install one only when you trust its source. You can remove it again at any time.
|
||||||
|
- Added a delete button for assistants, configurations, and language plugins you installed or placed yourself. Until now, such a plugin could only be removed from the data directory by hand, which was especially painful for configurations, because they have no on/off switch. Before deleting a configuration, AI Studio lists what disappears with it, such as providers, data sources, and settings that return to their default. When you delete the language plugin you had chosen, AI Studio returns to choosing your language automatically. Plugins shipped with AI Studio and plugins deployed by your IT department cannot be deleted.
|
||||||
|
- Added options for organizations to disable importing, sharing, and exporting plugins, with a separate option for configuration plugins. Organizations can now let people import assistants while keeping configurations to their IT department.
|
||||||
|
- Added a priority for configuration plugins. Organizations that deploy several configurations can now decide which one wins: a configuration with a higher priority overrides the settings and providers of a lower one. This allows a company-wide base configuration that each department refines for itself.
|
||||||
|
- Added a way for IT departments to try out a configuration before rolling it out. A configuration placed in the new `.config-tests` directory below the plugins directory acts like one your organization deployed, including the approval of assistant plugins, so a test shows exactly what colleagues will see later. No configuration server is needed for this. AI Studio empties that directory every time it starts, so a test configuration is valid for one session, and the information page reports it while it is active. The Enterprise IT documentation describes the whole procedure.
|
||||||
|
- Improved how your organization's configuration behaves when a configuration plugin is present but cannot be loaded, e.g. because of an error in the plugin. Such a plugin still manages your app, so its settings, providers, data sources, profiles, and chat templates now stay in place instead of being removed.
|
||||||
|
- Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants.
|
||||||
|
- Fixed reset buttons in assistants. As you may have noticed in the Document Analysis Assistant, resetting it could leave content from the previous analysis visible. Reset buttons now clear previous results completely.
|
||||||
|
- Fixed dropping files after you closed a dialog that accepts files itself. Such a dialog takes over dropped files while it is open, but never handed that role back when you closed it. Afterwards, the chat and the assistants silently ignored dropped files until you switched to another page. Each time you opened such a dialog again, the problem got worse.
|
||||||
|
- Fixed configuration-managed settings remaining active after their configuration plugin was removed.
|
||||||
|
- Fixed settings not returning to your own value after a configuration was removed. When a configuration takes control of a setting, AI Studio now remembers the value you had chosen before and hands it back once no configuration manages that setting anymore. This covers an IT department withdrawing a configuration, deleting one yourself, and an administrator ending a test configuration. When a configuration only suggested a value and you changed it afterwards, your choice stays as it is.
|
||||||
|
- Fixed the integrated code editor to keep errors and other issues in plugin code visible in the footer while scrolling.
|
||||||
|
- Fixed the trusted badge so you can now see at a glance which models are trusted. It is shown consistently for self-hosted models and models from trusted providers.
|
||||||
|
- Fixed approvals for assistant plugins being accepted from any configuration plugin. An approval marks an assistant as safe without a security check, and the app states that your organization approved it. Only configurations your IT department deploys, or that an administrator stages for a test, can do that now; approvals from any other locally placed configuration plugin are ignored and reported in the log.
|
||||||
|
- Fixed withdrawing a configuration your organization deployed. A configuration that declared itself as locally managed stayed on the device even after the IT department stopped deploying it, and it kept every right of an organization configuration, such as approving assistant plugins. Where a configuration is stored now decides this instead of what the configuration says about itself, so withdrawing one always takes effect. This also applies to a device that was offline while the organization changed its policy: the withdrawal is applied when AI Studio starts again.
|
||||||
|
- Fixed preview features contributed by several configuration plugins at once. Only the most recent contribution was recognized as coming from your organization, so features enabled by another configuration looked as if you had switched them on yourself. Each configuration is now tracked separately, which lets your organization enable one preview feature company-wide and another one for a single department.
|
||||||
|
- Fixed which configuration wins when two configuration plugins collide, e.g. by claiming the same plugin ID, by managing the same setting, or by defining the same provider. Previously, this was down to chance, so a local configuration plugin could take over parts of the configuration your IT department deployed. Configurations from your organization now always win, and every ignored attempt is reported in the log.
|
||||||
|
- Fixed the assistant categories when your organization hides individual assistants. A category heading could stay visible above an empty area, and the Log Viewer could disappear together with the Localization assistant. Each heading now follows the assistants actually shown below it.
|
||||||
|
- Upgraded dependencies to their latest versions to improve security and stability.
|
||||||
@ -54,7 +54,7 @@ The preferred format is a fixed set of indexed pairs:
|
|||||||
|
|
||||||
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots are supported per device.
|
Each configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). Up to 100,000 indexed configuration slots are supported per device.
|
||||||
|
|
||||||
If multiple configurations define the same setting, the first definition wins. For indexed pairs and policy files, the order is slot `00000`, then `00001`, and so on up to `99999`.
|
The slot order determines which configurations are downloaded, not which one wins a conflict. When two of your configuration plugins define the same setting or the same object, the declared priority decides. See [Priority of configuration plugins](#priority-of-configuration-plugins).
|
||||||
|
|
||||||
For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments.
|
For backwards compatibility, the older slot names `0` to `9` without an underscore are still supported. AI Studio also accepts other numeric slot suffixes with up to five digits. Slot suffixes are matched exactly, so `config_id_1`, `config_id_01`, and `config_id_00001` are treated as separate slots. Use the five-digit format with an underscore for new deployments.
|
||||||
|
|
||||||
@ -284,6 +284,77 @@ DEPLOYED_USING_CONFIG_SERVER = true
|
|||||||
|
|
||||||
Local, manually managed configuration plugins should set this to `false`. If the field is missing, AI Studio falls back to the plugin path (`.config`) to determine whether the plugin is managed and logs a warning.
|
Local, manually managed configuration plugins should set this to `false`. If the field is missing, AI Studio falls back to the plugin path (`.config`) to determine whether the plugin is managed and logs a warning.
|
||||||
|
|
||||||
|
The field describes a plugin, it does not grant it anything. Which configurations belong to your organization is always decided by the plugin path: which approvals for assistant plugins are honored, which configuration wins a conflict, and which configuration AI Studio withdraws once you stop referencing it. A configuration stored under `.config` is therefore removed when your organization no longer references its ID, whatever this field says.
|
||||||
|
|
||||||
|
## Priority of configuration plugins
|
||||||
|
|
||||||
|
When you deploy more than one configuration, two of your configuration plugins may manage the same setting or define the same object, e.g. the same LLM provider. The optional `PRIORITY` field decides which one wins:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
PRIORITY = 100
|
||||||
|
```
|
||||||
|
|
||||||
|
A configuration plugin with a higher priority is applied later and therefore wins. The field is optional and defaults to `0`.
|
||||||
|
|
||||||
|
A typical layered setup:
|
||||||
|
|
||||||
|
| Configuration | `PRIORITY` | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| Organization-wide base | `0` | Providers, update behavior, and security settings for everybody |
|
||||||
|
| Department | `100` | Refines the base, e.g. a different default model |
|
||||||
|
| Project or lab | `200` | Refines the department configuration |
|
||||||
|
|
||||||
|
A configuration only overrides what it actually defines. Everything it does not mention keeps the value of the configuration below it. The same applies when you remove a configuration later: its settings fall back to the configuration below, not to the AI Studio defaults. Once no configuration manages a setting anymore, see [Withdrawing a configuration](#withdrawing-a-configuration).
|
||||||
|
|
||||||
|
Give two configurations that must override each other different priorities. With an equal priority, the order is stable across restarts but arbitrary, so the outcome is not the one you designed.
|
||||||
|
|
||||||
|
Two guarantees are independent of the priority:
|
||||||
|
|
||||||
|
- A local configuration plugin never wins against one your IT department deployed, whatever priority it declares. Local plugins are always applied afterwards, and they may not take over a setting or an object that belongs to one of your configurations.
|
||||||
|
- Two plugins must not share the same plugin ID. If that happens, AI Studio keeps the one your IT department deployed and logs a warning for the other.
|
||||||
|
|
||||||
|
The single exception is a configuration you stage for a test under `.config-tests`. It is applied after your deployed configurations and wins a shared plugin ID, so that you can try out the next version of a configuration under its final ID. See [Local staging and testing](#local-staging-and-testing).
|
||||||
|
|
||||||
|
### Settings that hold a list or a table
|
||||||
|
|
||||||
|
For a setting that holds a list or a table, the winning configuration replaces the whole collection. It does not merge the entries. A department configuration that lists a single entry drops every entry the base configuration had set for that setting.
|
||||||
|
|
||||||
|
This is intentional: replacing is the only way a department can take something back. A department that wants an assistant to be visible again can only achieve that by not listing it.
|
||||||
|
|
||||||
|
Plan for it in these settings:
|
||||||
|
|
||||||
|
| Setting | What a partial list costs you |
|
||||||
|
|---|---|
|
||||||
|
| `DataApp.HiddenAssistants` | Assistants hidden by the base configuration become **visible** again |
|
||||||
|
| `DataSourceSecuritySettings.TrustedProviderIds` | Providers trusted by the base configuration lose that status |
|
||||||
|
| `DataApp.ExternalHttpCustomRootCertificateAllowedHosts` | Hosts of the base configuration stop trusting your root certificates |
|
||||||
|
| `DataConfidence.CustomConfidenceScheme` | Providers left out fall back to the AI Studio default confidence |
|
||||||
|
| `DataChat.PreselectedDataSourceIds` | Data sources preselected by the base configuration are no longer preselected |
|
||||||
|
|
||||||
|
The rule of thumb: whenever a configuration with a higher priority touches one of these settings, it has to repeat every entry it wants to keep. Watch `DataApp.HiddenAssistants` in particular, because it is the only one in this list that opens something up instead of restricting it.
|
||||||
|
|
||||||
|
Two settings are the exception and add up instead of replacing:
|
||||||
|
|
||||||
|
- `DataApp.EnabledPreviewFeatures` — enable one preview feature for the whole organization and another one for a single department, and users of that department get both.
|
||||||
|
- `DataAssistantPluginAudit.EnterpriseApprovedPlugins` — a department configuration can approve additional assistant plugins without repeating the approvals of the base configuration. Approving is a pure allowlist over hashes, so there is nothing a replacing list could express that adding does not.
|
||||||
|
|
||||||
|
In both cases each configuration keeps its own contribution, so removing one of them only withdraws what this configuration had granted. While a configuration plugin is deployed but cannot be loaded, its approvals are kept: AI Studio does not withdraw approvals it cannot currently read.
|
||||||
|
|
||||||
|
One clarification for `DataChat.PreselectedDataSourceIds`: the IDs are not limited to the data sources of the same configuration. They are resolved against every known data source, including those of your other configurations and the ones a user configured. IDs that resolve to nothing are ignored.
|
||||||
|
|
||||||
|
## Withdrawing a configuration
|
||||||
|
|
||||||
|
A configuration does not have to stay forever: you stop deploying it, a user deletes a configuration they installed themselves, or a test configuration ends with the next restart. AI Studio then removes what that configuration brought along, such as its providers, data sources, profiles, chat templates, and its approvals for assistant plugins.
|
||||||
|
|
||||||
|
Settings go one step further. AI Studio remembers the value each setting had before a configuration took it over and hands it back once no configuration manages that setting anymore. Somebody who had chosen a start page before your configuration set one therefore gets their own start page back, not the AI Studio default.
|
||||||
|
|
||||||
|
Two cases differ:
|
||||||
|
|
||||||
|
- **There is nothing to hand back.** When a setting still had its AI Studio default at the moment your configuration took it over, that default returns. The same applies to settings which a configuration already managed before AI Studio v26.8.1, because nothing was remembered back then.
|
||||||
|
- **Somebody used `AllowUserOverride`.** A setting you offered as an organization default, and which the user changed afterwards, keeps the user's value. Their decision outlives your configuration.
|
||||||
|
|
||||||
|
A configuration that is deployed but cannot be loaded, e.g. because of an error in its Lua code, is not withdrawn. It still manages the device, so everything it brought along stays untouched until you actually stop deploying it.
|
||||||
|
|
||||||
## Example AI Studio configuration
|
## Example AI Studio configuration
|
||||||
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files:
|
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files:
|
||||||
|
|
||||||
@ -315,6 +386,16 @@ AI Studio computes the approval hash as a SHA-256 digest over all `.lua` files i
|
|||||||
|
|
||||||
If any Lua file changes, the hash changes automatically and the enterprise approval no longer applies.
|
If any Lua file changes, the hash changes automatically and the enterprise approval no longer applies.
|
||||||
|
|
||||||
|
### Only your configurations may approve
|
||||||
|
|
||||||
|
Approvals are honored only in configuration plugins that speak for your organization: plugins a configuration server deployed, meaning plugins stored under the `.config` directory, and plugins you staged for a test under `.config-tests`. AI Studio ignores the approvals of any other locally placed configuration plugin and writes a warning to the log.
|
||||||
|
|
||||||
|
The reason is what an approval does: it marks an assistant plugin as safe without any security audit, and AI Studio then tells the user that their organization approved it. Anyone who can drop a file into the plugin directory could otherwise disable the security audit for an assistant plugin of their choosing while the app vouches for it in your name.
|
||||||
|
|
||||||
|
This is decided by where the plugin is stored, not by its `DEPLOYED_USING_CONFIG_SERVER` field. That field is part of the plugin itself, so any plugin could claim it.
|
||||||
|
|
||||||
|
If you want to test approvals before rolling a configuration out, see [Local staging and testing](#local-staging-and-testing).
|
||||||
|
|
||||||
### Configuration example
|
### Configuration example
|
||||||
|
|
||||||
Add the approval list to `CONFIG["SETTINGS"]` in your configuration plugin:
|
Add the approval list to `CONFIG["SETTINGS"]` in your configuration plugin:
|
||||||
@ -343,6 +424,66 @@ dotnet run --project app/Build -- assistant-plugin-hash "<plugin-dir>" --lua-sni
|
|||||||
|
|
||||||
This prints the canonical hash and, with `--lua-snippet`, also prints a ready-to-paste Lua snippet for `CONFIG["SETTINGS"]`.
|
This prints the canonical hash and, with `--lua-snippet`, also prints a ready-to-paste Lua snippet for `CONFIG["SETTINGS"]`.
|
||||||
|
|
||||||
|
## Local staging and testing
|
||||||
|
|
||||||
|
Before you roll a configuration out through a configuration web server, you can stage it on a device and test it end to end, including the enterprise approvals for assistant plugins described above. This needs no configuration web server, no registry, policy, or environment entry, and no encryption secret.
|
||||||
|
|
||||||
|
AI Studio has a dedicated directory for this: `.config-tests`. A configuration stored there speaks for your organization exactly like a deployed one. In exchange, AI Studio empties the directory on every start, so a test configuration is valid for one session.
|
||||||
|
|
||||||
|
Do not use the `.config` directory for this. It belongs to your configuration web server, and AI Studio removes everything there that your organization does not reference anymore.
|
||||||
|
|
||||||
|
### The data directory
|
||||||
|
|
||||||
|
Plugins live in the data directory of AI Studio:
|
||||||
|
|
||||||
|
| Platform | Data directory |
|
||||||
|
| --- | --- |
|
||||||
|
| Windows | `%LOCALAPPDATA%\com.github.mindwork-ai.ai-studio\data` |
|
||||||
|
| macOS | `~/Library/Application Support/com.github.mindwork-ai.ai-studio/data` |
|
||||||
|
| Linux | `$XDG_DATA_HOME/com.github.mindwork-ai.ai-studio/data`, usually `~/.local/share/com.github.mindwork-ai.ai-studio/data` |
|
||||||
|
| Linux (Flatpak) | `~/.var/app/org.mindworkai.AIStudio/data/com.github.mindwork-ai.ai-studio/data` |
|
||||||
|
|
||||||
|
### Staging a configuration
|
||||||
|
|
||||||
|
Place the files **while AI Studio is running**: the test directory is emptied whenever the app starts.
|
||||||
|
|
||||||
|
1. Start AI Studio. It creates `<data directory>/plugins/.config-tests/` if it does not exist yet.
|
||||||
|
2. Create a directory below it and place your `plugin.lua` there, e.g. `.config-tests/my-department-draft/`. The directory name is up to you here: a test configuration is identified by the `ID` field inside the plugin, not by the directory it lives in.
|
||||||
|
3. Place the assistant plugin you want to test in `<data directory>/plugins/assistants/<any name>/`.
|
||||||
|
4. AI Studio watches the plugin directory and picks both up without a restart. The security card of the assistant then states that your organization approved it, exactly as it will after the rollout.
|
||||||
|
|
||||||
|
While a test configuration is loaded, the Information page reports it, including the directory it was staged in. After a restart, that same page tells you that a test configuration was removed, so nobody has to wonder where the directory went.
|
||||||
|
|
||||||
|
What behaves like the later rollout:
|
||||||
|
|
||||||
|
- The approvals for assistant plugins are honored.
|
||||||
|
- Settings and configuration objects the test configuration manages are protected against local configuration plugins.
|
||||||
|
- When the test configuration declares the same plugin `ID` as one your organization deployed, the test configuration wins. This is how you try out the next version of an existing configuration under its final ID.
|
||||||
|
|
||||||
|
What deliberately does not:
|
||||||
|
|
||||||
|
- A test configuration has no protection against the user. You can remove it on the plugin page and replace it by importing a new version.
|
||||||
|
- It does not survive a restart.
|
||||||
|
|
||||||
|
### Testing with a small group
|
||||||
|
|
||||||
|
To let colleagues take part in the test, place the same two directories on each of their devices while AI Studio runs, for example through a script, your MDM solution, or a login script. A configuration web server is not involved, and nothing has to be enabled inside AI Studio. Ordinary user accounts can take part: the data directory belongs to the user, so no administrator rights are needed to place the files.
|
||||||
|
|
||||||
|
Keep in mind that everybody in the group loses the test configuration the next time they start AI Studio. Either repeat the step, or let your script place the files at every login.
|
||||||
|
|
||||||
|
### Cleaning up
|
||||||
|
|
||||||
|
Restart AI Studio: the test directory is emptied, the approvals are gone, and the assistant requires a security audit again. Every setting your test configuration had taken over returns to the value it had before the test, as described in [Withdrawing a configuration](#withdrawing-a-configuration). To end a test without restarting, delete the configuration on the plugin page.
|
||||||
|
|
||||||
|
### Security note
|
||||||
|
|
||||||
|
A test configuration carries the rights of an organization configuration without anybody having deployed it. Two properties keep that in check, and you should not work around either of them:
|
||||||
|
|
||||||
|
- The directory is emptied on every start, so nothing staged for a test can settle in unnoticed.
|
||||||
|
- No feature inside AI Studio writes into that directory. Importing, sharing, and deleting plugins never touch it, so a user cannot be talked into staging a configuration by opening a file.
|
||||||
|
|
||||||
|
The data directory belongs to the user account, so whoever can write there can approve assistant plugins in the name of your organization until the next restart. Treat write access to the data directory as equivalent to deploying a configuration, and protect it accordingly on managed devices.
|
||||||
|
|
||||||
## Encrypted API Keys
|
## Encrypted API Keys
|
||||||
|
|
||||||
You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files.
|
You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files.
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
# Enterprise Configuration ZIP Backslashes
|
# Plugin Archive ZIP Backslashes
|
||||||
|
|
||||||
- Status: Active
|
- Status: Active
|
||||||
- Introduced: 2026-07-09
|
- Introduced: 2026-07-09
|
||||||
- Remove after: when Microsoft fixes dotnet/runtime#27620 and dotnet/runtime#41914
|
- Remove after: when Microsoft fixes dotnet/runtime#27620 and dotnet/runtime#41914
|
||||||
- Code references:
|
- Code references:
|
||||||
|
- `app/MindWork AI Studio/Tools/PluginSystem/PluginArchive.cs`
|
||||||
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs`
|
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs`
|
||||||
|
- `app/MindWork AI Studio/Tools/Services/AssistantPluginInstallService.cs`
|
||||||
|
|
||||||
## User Impact
|
## User Impact
|
||||||
|
|
||||||
@ -12,9 +14,11 @@ Some enterprise administrators create configuration plugin ZIP files on Windows.
|
|||||||
|
|
||||||
Without this shim, Unix systems extract those entries as files whose names contain literal backslash characters. The plugin loader then cannot find `plugin.lua`, so the enterprise configuration plugin is not activated.
|
Without this shim, Unix systems extract those entries as files whose names contain literal backslash characters. The plugin loader then cannot find `plugin.lua`, so the enterprise configuration plugin is not activated.
|
||||||
|
|
||||||
|
The same applies to plugin archives that users import from their disk. Archives created by AI Studio itself always use forward slashes. However, users may import archives that were packaged by hand or with another tool on Windows, so the import path needs the same tolerance. Otherwise, importing such an archive fails because `plugin.lua` cannot be found.
|
||||||
|
|
||||||
## Compatibility Behavior
|
## Compatibility Behavior
|
||||||
|
|
||||||
AI Studio manually extracts downloaded enterprise configuration plugin ZIP files. During extraction, entry names are normalized so both `/` and `\` are treated as archive path separators.
|
AI Studio manually extracts enterprise configuration plugin archives and plugin archives imported by users. During extraction, entry names are normalized so both `/` and `\` are treated as archive path separators.
|
||||||
|
|
||||||
The extraction still preserves the archive structure and validates each entry before writing it to disk. Rooted paths, drive-qualified paths, and parent-directory traversal paths are rejected.
|
The extraction still preserves the archive structure and validates each entry before writing it to disk. Rooted paths, drive-qualified paths, and parent-directory traversal paths are rejected.
|
||||||
|
|
||||||
@ -23,6 +27,5 @@ This works around the behavior described in dotnet/runtime#27620. A related upst
|
|||||||
## Removal Checklist
|
## Removal Checklist
|
||||||
|
|
||||||
- Confirm supported .NET runtimes and administrator packaging guidance no longer require accepting backslashes in enterprise ZIP entry names.
|
- Confirm supported .NET runtimes and administrator packaging guidance no longer require accepting backslashes in enterprise ZIP entry names.
|
||||||
- Replace the manual enterprise configuration plugin ZIP extraction with `ZipFile.ExtractToDirectory(...)`.
|
- Replace `PluginArchive.Extract(...)` with `ZipFile.ExtractToDirectory(...)`.
|
||||||
- Remove `ExtractConfigPluginArchive(...)`, `NormalizeConfigPluginZipEntryName(...)`, and `GetConfigPluginZipEntryDestinationPath(...)`.
|
|
||||||
- Update this document's status to `Removed`.
|
- Update this document's status to `Removed`.
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
# Orphaned Configuration Locks
|
||||||
|
|
||||||
|
- Status: Active
|
||||||
|
- Introduced: 2026-08-06
|
||||||
|
- Remove after: 2027-08-06
|
||||||
|
- Code references:
|
||||||
|
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs` (`RepairLegacyConfigOnlySettings`, `RepairLegacyConfigOnlyFlag`, `RepairLegacyConfigOnlyCollection`)
|
||||||
|
|
||||||
|
## User Impact
|
||||||
|
|
||||||
|
Until this release, AI Studio persisted the value a configuration plugin had set, but not the information which plugin owned that value. After a restart, the ownership was lost. When the configuration plugin was removed in the meantime, the cleanup in `PluginFactory.LoadAll` could not recognize the value as left over, so it stayed active forever.
|
||||||
|
|
||||||
|
For most settings, this was an inconvenience only, because users can change them in the settings dialog. For settings without any user interface, it was a dead end: hidden assistants stayed hidden, adding providers stayed disabled, and the home page panels stayed switched off. The only workaround was to edit the settings file by hand.
|
||||||
|
|
||||||
|
Installations that lost the ownership this way cannot be repaired by the new persistence alone, because the missing information cannot be reconstructed. They need this one-time repair.
|
||||||
|
|
||||||
|
## Compatibility Behavior
|
||||||
|
|
||||||
|
At the end of `PluginFactory.LoadAll`, AI Studio checks a fixed list of settings. A setting is repaired when it is not managed by any configuration plugin at that moment and still holds a value that only a configuration plugin could have produced:
|
||||||
|
|
||||||
|
- `DataApp.ShowIntroduction`, `DataApp.ShowQuickStartGuide`, `DataApp.ShowLastChangelog`, `DataApp.ShowVision`, `DataApp.AllowUserToAddProvider`, `DataApp.AllowUserToImportPlugins`, `DataApp.AllowUserToSharePlugins`: enabled by default, so a disabled value is repaired.
|
||||||
|
- `DataApp.HiddenAssistants`, `DataSourceSecuritySettings.TrustedProviderIds`, `DataAssistantPluginAudit.EnterpriseApprovedPlugins`: empty by default, so a filled collection is repaired.
|
||||||
|
|
||||||
|
Repairing means restoring the default value. Each repair is logged as a warning.
|
||||||
|
|
||||||
|
Nothing is repaired at all while a configuration plugin is deployed but could not be loaded, e.g. because of invalid Lua code. In that situation, we cannot tell whether a value comes from that plugin or from a removed one, so the repair is postponed to the next start.
|
||||||
|
|
||||||
|
The check runs on every start, not once. This is safe because none of these settings has a user interface that writes to it, so a non-default value can only originate from a configuration plugin. This is the load-bearing assumption of the whole shim: as soon as one of these settings gets a user interface, the shim would overwrite the user's choice on every start. In that case, remove the setting from `RepairLegacyConfigOnlySettings` and from the list above.
|
||||||
|
|
||||||
|
Settings that a configuration plugin can lock but that users can change themselves are deliberately not part of this list. Their owner is persisted from this release on, and the regular left-over cleanup handles them.
|
||||||
|
|
||||||
|
## Removal Checklist
|
||||||
|
|
||||||
|
- Remove `RepairLegacyConfigOnlySettings`, `RepairLegacyConfigOnlyFlag`, and `RepairLegacyConfigOnlyCollection` from `PluginFactory.Loading.cs`, including the call and the comment in `LoadAll`.
|
||||||
|
- Update this document's status to `Removed`.
|
||||||
|
- No changelog entry is needed, because removing the shim is not user-visible.
|
||||||
@ -23,7 +23,7 @@ Every compatibility shim must have:
|
|||||||
|
|
||||||
- Status: Active
|
- Status: Active
|
||||||
- Introduced: YYYY-MM-DD
|
- Introduced: YYYY-MM-DD
|
||||||
- Remove after: YYYY-MM-DD
|
- Remove after: YYYY-MM-DD or a condition e.g., someone is solving an issue on a dependency
|
||||||
- Code references:
|
- Code references:
|
||||||
- path/to/file.cs
|
- path/to/file.cs
|
||||||
|
|
||||||
|
|||||||
2
runtime/.codex/config.toml
Normal file
2
runtime/.codex/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[mcp_servers.rustrover]
|
||||||
|
url = "http://127.0.0.1:64522/stream"
|
||||||
111
runtime/Cargo.lock
generated
111
runtime/Cargo.lock
generated
@ -2786,7 +2786,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2797,10 +2797,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.13.3+wasi-0.2.2",
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
"wasm-bindgen",
|
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4064,7 +4062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4261,6 +4259,9 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"log",
|
"log",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"pdfium-render",
|
"pdfium-render",
|
||||||
@ -4294,6 +4295,8 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webm-iterable",
|
"webm-iterable",
|
||||||
"whoami",
|
"whoami",
|
||||||
|
"windows 0.61.3",
|
||||||
|
"windows-collections 0.2.0",
|
||||||
"windows-native-keyring-store",
|
"windows-native-keyring-store",
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
@ -4337,7 +4340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -4648,23 +4651,30 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-app-kit"
|
name = "objc2-app-kit"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
|
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
|
"libc",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
|
"objc2-cloud-kit",
|
||||||
|
"objc2-core-data",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-image",
|
||||||
|
"objc2-core-text",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
|
"objc2-quartz-core 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-cloud-kit"
|
name = "objc2-cloud-kit"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
|
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
@ -4673,10 +4683,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-data"
|
name = "objc2-core-data"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
]
|
]
|
||||||
@ -4694,11 +4705,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-graphics"
|
name = "objc2-core-graphics"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
|
"dispatch2",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-io-surface",
|
"objc2-io-surface",
|
||||||
@ -4706,9 +4718,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-image"
|
name = "objc2-core-image"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
|
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
@ -4724,6 +4736,31 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-text"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@ -4776,9 +4813,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-io-surface"
|
name = "objc2-io-surface"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
|
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
@ -4835,9 +4872,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
|
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
@ -4870,7 +4907,7 @@ dependencies = [
|
|||||||
"objc2-core-image",
|
"objc2-core-image",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"objc2-quartz-core 0.3.0",
|
"objc2-quartz-core 0.3.2",
|
||||||
"objc2-user-notifications",
|
"objc2-user-notifications",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5700,15 +5737,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-proto"
|
name = "quinn-proto"
|
||||||
version = "0.11.14"
|
version = "0.11.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.4.2",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand 0.9.4",
|
"rand 0.10.2",
|
||||||
|
"rand_pcg",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
@ -8723,9 +8761,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
@ -8736,13 +8774,22 @@ dependencies = [
|
|||||||
"wit-bindgen-rt",
|
"wit-bindgen-rt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.4+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.45.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.2+wasi-0.2.9"
|
version = "1.0.2+wasi-0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen 0.51.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -8751,7 +8798,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen 0.51.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -8760,7 +8807,7 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
|
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasi 0.13.3+wasi-0.2.2",
|
"wasi 0.14.4+wasi-0.2.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -9722,6 +9769,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.45.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
@ -69,9 +69,14 @@ permutation_iterator = { git = "https://github.com/SommerEngineering/permutation
|
|||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows-registry = "0.6.1"
|
windows-registry = "0.6.1"
|
||||||
windows-native-keyring-store = "1.1.0"
|
windows-native-keyring-store = "1.1.0"
|
||||||
|
windows = { version = "=0.61.3", features = ["ApplicationModel_DataTransfer", "Foundation", "Foundation_Collections", "Storage", "Storage_Streams", "Win32_Foundation", "Win32_System_WinRT", "Win32_UI_Shell"] }
|
||||||
|
windows-collections = "=0.2.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
|
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
|
||||||
|
objc2 = "0.6.3"
|
||||||
|
objc2-app-kit = { version = "0.3.2", features = ["NSResponder", "NSSharingService", "NSView"] }
|
||||||
|
objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSGeometry", "NSString", "NSURL"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
ashpd = { version = "0.13.12", default-features = false, features = ["tokio", "open_uri", "global_shortcuts"] }
|
ashpd = { version = "0.13.12", default-features = false, features = ["tokio", "open_uri", "global_shortcuts"] }
|
||||||
|
|||||||
@ -328,28 +328,11 @@ pub async fn open_path_in_file_manager(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(target) = resolve_file_manager_target(&requested_path) else {
|
match open_file_manager_target(&requested_path).await {
|
||||||
let issue = format!(
|
Ok(()) => Json(OpenPathResponse {
|
||||||
"The path does not exist and its parent folder could not be found: {}",
|
|
||||||
requested_path.to_string_lossy(),
|
|
||||||
);
|
|
||||||
error!(Source = "Tauri"; "{issue}");
|
|
||||||
return Json(OpenPathResponse {
|
|
||||||
success: false,
|
|
||||||
issue,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
return match open_path_in_linux_file_manager(&target).await {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Opened file manager for path: {:?}", target.path);
|
|
||||||
Json(OpenPathResponse {
|
|
||||||
success: true,
|
success: true,
|
||||||
issue: String::new(),
|
issue: String::new(),
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
|
|
||||||
Err(issue) => {
|
Err(issue) => {
|
||||||
error!(Source = "Tauri"; "{issue}");
|
error!(Source = "Tauri"; "{issue}");
|
||||||
@ -358,6 +341,27 @@ pub async fn open_path_in_file_manager(
|
|||||||
issue,
|
issue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_file_manager_target(requested_path: &Path) -> Result<(), String> {
|
||||||
|
let Some(target) = resolve_file_manager_target(requested_path) else {
|
||||||
|
let issue = format!(
|
||||||
|
"The path does not exist and its parent folder could not be found: {}",
|
||||||
|
requested_path.to_string_lossy(),
|
||||||
|
);
|
||||||
|
return Err(issue);
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
return match open_path_in_linux_file_manager(&target).await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Opened file manager for path: {:?}", target.path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(issue) => Err(issue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,19 +375,12 @@ pub async fn open_path_in_file_manager(
|
|||||||
match command.spawn() {
|
match command.spawn() {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Opened file manager for path: {:?}", target.path);
|
info!("Opened file manager for path: {:?}", target.path);
|
||||||
Json(OpenPathResponse {
|
Ok(())
|
||||||
success: true,
|
|
||||||
issue: String::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let issue = format!("Failed to open the file manager: {error}");
|
let issue = format!("Failed to open the file manager: {error}");
|
||||||
error!(Source = "Tauri"; "{issue}");
|
Err(issue)
|
||||||
Json(OpenPathResponse {
|
|
||||||
success: false,
|
|
||||||
issue,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ pub mod qdrant_edge_database;
|
|||||||
pub mod certificate_factory;
|
pub mod certificate_factory;
|
||||||
pub mod runtime_api_token;
|
pub mod runtime_api_token;
|
||||||
pub mod stale_process_cleanup;
|
pub mod stale_process_cleanup;
|
||||||
|
pub mod share_sheet;
|
||||||
mod sidecar_types;
|
mod sidecar_types;
|
||||||
mod file_actions;
|
mod file_actions;
|
||||||
pub mod global_shortcuts;
|
pub mod global_shortcuts;
|
||||||
@ -38,6 +38,7 @@ pub fn start_runtime_api() {
|
|||||||
.route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file))
|
.route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file))
|
||||||
.route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store))
|
.route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store))
|
||||||
.route("/clipboard/set", post(crate::clipboard::set_clipboard))
|
.route("/clipboard/set", post(crate::clipboard::set_clipboard))
|
||||||
|
.route("/share/file", post(crate::share_sheet::share_file))
|
||||||
.route("/events", get(crate::app_window::get_event_stream))
|
.route("/events", get(crate::app_window::get_event_stream))
|
||||||
.route("/updates/check", get(crate::app_window::check_for_update))
|
.route("/updates/check", get(crate::app_window::check_for_update))
|
||||||
.route("/updates/install", get(crate::app_window::install_update))
|
.route("/updates/install", get(crate::app_window::install_update))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user