mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-20 00:32:15 +00:00
Support multiple enterprise configurations
This commit is contained in:
parent
ea4e3f0199
commit
3bfd402a28
@ -211,9 +211,12 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
//
|
||||
// Check if there is an enterprise configuration plugin to download:
|
||||
//
|
||||
var enterpriseEnvironment = this.MessageBus.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT).FirstOrDefault();
|
||||
if (enterpriseEnvironment != default)
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl);
|
||||
var enterpriseEnvironments = this.MessageBus
|
||||
.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
|
||||
.Where(env => env != default)
|
||||
.ToList();
|
||||
foreach (var env in enterpriseEnvironments)
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
|
||||
|
||||
// Initialize the enterprise encryption service for decrypting API keys:
|
||||
await PluginFactory.InitializeEnterpriseEncryption(this.RustService);
|
||||
|
||||
@ -49,26 +49,29 @@
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||
@switch (EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive)
|
||||
@switch (HasAnyActiveEnvironment)
|
||||
{
|
||||
case false when this.configPlug is null:
|
||||
case false when this.configPlugins.Count == 0:
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("This is a private AI Studio installation. It runs without an enterprise configuration.")
|
||||
</MudText>
|
||||
break;
|
||||
|
||||
|
||||
case false:
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.")
|
||||
@T("AI Studio runs with an enterprise configuration using configuration plugins, without central configuration management.")
|
||||
</MudText>
|
||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration plugin ID:") @this.configPlug!.Id</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@this.configPlug!.Id.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
@foreach (var plug in this.configPlugins)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration plugin ID:") @plug.Id</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@plug.Id.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
@ -87,26 +90,28 @@
|
||||
</MudCollapse>
|
||||
break;
|
||||
|
||||
case true when this.configPlug is null:
|
||||
case true when this.configPlugins.Count == 0:
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.")
|
||||
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.")
|
||||
</MudText>
|
||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl/>
|
||||
</div>
|
||||
</MudText>
|
||||
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Enterprise configuration ID:") @env.ConfigurationId</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@env.ConfigurationId.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration server:") @env.ConfigurationServerUrl</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@env.ConfigurationServerUrl/>
|
||||
</div>
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
@ -127,32 +132,36 @@
|
||||
|
||||
case true:
|
||||
<MudText Typo="Typo.body1">
|
||||
@T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.")
|
||||
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.")
|
||||
</MudText>
|
||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl/>
|
||||
</div>
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration plugin ID:") @this.configPlug!.Id</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@this.configPlug!.Id.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Enterprise configuration ID:") @env.ConfigurationId</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@env.ConfigurationId.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration server:") @env.ConfigurationServerUrl</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@env.ConfigurationServerUrl/>
|
||||
</div>
|
||||
</MudText>
|
||||
}
|
||||
@foreach (var plug in this.configPlugins)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
<span>@T("Configuration plugin ID:") @plug.Id</span>
|
||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@plug.Id.ToString()/>
|
||||
</div>
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||
|
||||
@ -69,12 +69,14 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private bool showDatabaseDetails;
|
||||
|
||||
private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION);
|
||||
private List<IPluginMetadata> configPlugins = PluginFactory.AvailablePlugins.Where(x => x.Type is PluginType.CONFIGURATION).ToList();
|
||||
|
||||
private sealed record DatabaseDisplayInfo(string Label, string Value);
|
||||
|
||||
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
||||
|
||||
private static bool HasAnyActiveEnvironment => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Any(e => e.IsActive);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the enterprise configuration has details that can be shown/hidden.
|
||||
/// Returns true if there are details available, false otherwise.
|
||||
@ -83,16 +85,16 @@ public partial class Information : MSGComponentBase
|
||||
{
|
||||
get
|
||||
{
|
||||
return EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive switch
|
||||
return HasAnyActiveEnvironment switch
|
||||
{
|
||||
// Case 1: No enterprise config and no plugin - no details available
|
||||
false when this.configPlug is null => false,
|
||||
false when this.configPlugins.Count == 0 => false,
|
||||
|
||||
// Case 2: Enterprise config with plugin but no central management - has details
|
||||
false => true,
|
||||
|
||||
// Case 3: Enterprise config active but no plugin - has details
|
||||
true when this.configPlug is null => true,
|
||||
true when this.configPlugins.Count == 0 => true,
|
||||
|
||||
// Case 4: Enterprise config active with plugin - has details
|
||||
true => true
|
||||
@ -128,7 +130,7 @@ public partial class Information : MSGComponentBase
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION);
|
||||
this.configPlugins = PluginFactory.AvailablePlugins.Where(x => x.Type is PluginType.CONFIGURATION).ToList();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
3
app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs
Normal file
3
app/MindWork AI Studio/Tools/Rust/EnterpriseConfig.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
public sealed record EnterpriseConfig(string Id, string ServerUrl);
|
||||
@ -4,7 +4,7 @@ namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentService> logger, RustService rustService) : BackgroundService
|
||||
{
|
||||
public static EnterpriseEnvironment CURRENT_ENVIRONMENT;
|
||||
public static List<EnterpriseEnvironment> CURRENT_ENVIRONMENTS = [];
|
||||
|
||||
#if DEBUG
|
||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
||||
@ -33,84 +33,125 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Start updating of the enterprise environment.");
|
||||
|
||||
Guid enterpriseRemoveConfigId;
|
||||
try
|
||||
{
|
||||
enterpriseRemoveConfigId = await rustService.EnterpriseEnvRemoveConfigId();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to fetch the enterprise remove configuration ID from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvRemoveConfigId failed");
|
||||
return;
|
||||
}
|
||||
|
||||
var isPlugin2RemoveInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == enterpriseRemoveConfigId);
|
||||
if (enterpriseRemoveConfigId != Guid.Empty && isPlugin2RemoveInUse)
|
||||
{
|
||||
logger.LogWarning("The enterprise environment configuration ID '{EnterpriseRemoveConfigId}' must be removed.", enterpriseRemoveConfigId);
|
||||
PluginFactory.RemovePluginAsync(enterpriseRemoveConfigId);
|
||||
}
|
||||
|
||||
string? enterpriseConfigServerUrl;
|
||||
//
|
||||
// Step 1: Handle deletions first.
|
||||
//
|
||||
List<Guid> deleteConfigIds;
|
||||
try
|
||||
{
|
||||
enterpriseConfigServerUrl = await rustService.EnterpriseEnvConfigServerUrl();
|
||||
deleteConfigIds = await rustService.EnterpriseEnvDeleteConfigIds();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to fetch the enterprise configuration server URL from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvConfigServerUrl failed");
|
||||
logger.LogError(e, "Failed to fetch the enterprise delete configuration IDs from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvDeleteConfigIds failed");
|
||||
return;
|
||||
}
|
||||
|
||||
Guid enterpriseConfigId;
|
||||
try
|
||||
foreach (var deleteId in deleteConfigIds)
|
||||
{
|
||||
enterpriseConfigId = await rustService.EnterpriseEnvConfigId();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to fetch the enterprise configuration ID from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvConfigId failed");
|
||||
return;
|
||||
}
|
||||
|
||||
var etag = await PluginFactory.DetermineConfigPluginETagAsync(enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
var nextEnterpriseEnvironment = new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId, etag);
|
||||
if (CURRENT_ENVIRONMENT != nextEnterpriseEnvironment)
|
||||
{
|
||||
logger.LogInformation("The enterprise environment has changed. Updating the current environment.");
|
||||
CURRENT_ENVIRONMENT = nextEnterpriseEnvironment;
|
||||
|
||||
switch (enterpriseConfigServerUrl)
|
||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == deleteId);
|
||||
if (isPluginInUse)
|
||||
{
|
||||
case null when enterpriseConfigId == Guid.Empty:
|
||||
case not null when string.IsNullOrWhiteSpace(enterpriseConfigServerUrl) && enterpriseConfigId == Guid.Empty:
|
||||
logger.LogInformation("AI Studio runs without an enterprise configuration.");
|
||||
break;
|
||||
|
||||
case null:
|
||||
logger.LogWarning("AI Studio runs with an enterprise configuration id ('{EnterpriseConfigId}'), but the configuration server URL is not set.", enterpriseConfigId);
|
||||
break;
|
||||
|
||||
case not null when !string.IsNullOrWhiteSpace(enterpriseConfigServerUrl) && enterpriseConfigId == Guid.Empty:
|
||||
logger.LogWarning("AI Studio runs with an enterprise configuration server URL ('{EnterpriseConfigServerUrl}'), but the configuration ID is not set.", enterpriseConfigServerUrl);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.LogInformation("AI Studio runs with an enterprise configuration id ('{EnterpriseConfigId}') and configuration server URL ('{EnterpriseConfigServerUrl}').", enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
|
||||
if(isFirstRun)
|
||||
MessageBus.INSTANCE.DeferMessage(null, Event.STARTUP_ENTERPRISE_ENVIRONMENT, new EnterpriseEnvironment(enterpriseConfigServerUrl, enterpriseConfigId, etag));
|
||||
else
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseConfigId, enterpriseConfigServerUrl);
|
||||
break;
|
||||
logger.LogWarning("The enterprise environment configuration ID '{DeleteConfigId}' must be removed.", deleteId);
|
||||
PluginFactory.RemovePluginAsync(deleteId);
|
||||
}
|
||||
}
|
||||
else
|
||||
logger.LogInformation("The enterprise environment has not changed. No update required.");
|
||||
|
||||
//
|
||||
// Step 2: Fetch all active configurations.
|
||||
//
|
||||
List<EnterpriseEnvironment> fetchedConfigs;
|
||||
try
|
||||
{
|
||||
fetchedConfigs = await rustService.EnterpriseEnvConfigs();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to fetch the enterprise configurations from the Rust service.");
|
||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvConfigs failed");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Step 3: Determine ETags and build the next environment list.
|
||||
//
|
||||
var nextEnvironments = new List<EnterpriseEnvironment>();
|
||||
foreach (var config in fetchedConfigs)
|
||||
{
|
||||
if (!config.IsActive)
|
||||
{
|
||||
logger.LogWarning("Skipping inactive enterprise configuration with ID '{ConfigId}'. There is either no valid server URL or config ID set.", config.ConfigurationId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var etag = await PluginFactory.DetermineConfigPluginETagAsync(config.ConfigurationId, config.ConfigurationServerUrl);
|
||||
nextEnvironments.Add(config with { ETag = etag });
|
||||
}
|
||||
|
||||
if (nextEnvironments.Count == 0)
|
||||
{
|
||||
if (CURRENT_ENVIRONMENTS.Count > 0)
|
||||
{
|
||||
logger.LogWarning("AI Studio no longer has any enterprise configurations. Removing previously active configs.");
|
||||
|
||||
// Remove plugins for configs that were previously active:
|
||||
foreach (var oldEnv in CURRENT_ENVIRONMENTS)
|
||||
{
|
||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == oldEnv.ConfigurationId);
|
||||
if (isPluginInUse)
|
||||
PluginFactory.RemovePluginAsync(oldEnv.ConfigurationId);
|
||||
}
|
||||
}
|
||||
else
|
||||
logger.LogInformation("AI Studio runs without any enterprise configurations.");
|
||||
|
||||
CURRENT_ENVIRONMENTS = [];
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Step 4: Compare with current environments and process changes.
|
||||
//
|
||||
var currentIds = CURRENT_ENVIRONMENTS.Select(e => e.ConfigurationId).ToHashSet();
|
||||
var nextIds = nextEnvironments.Select(e => e.ConfigurationId).ToHashSet();
|
||||
|
||||
// Remove plugins for configs that are no longer present:
|
||||
foreach (var oldEnv in CURRENT_ENVIRONMENTS)
|
||||
{
|
||||
if (!nextIds.Contains(oldEnv.ConfigurationId))
|
||||
{
|
||||
logger.LogInformation("Enterprise configuration '{ConfigId}' was removed.", oldEnv.ConfigurationId);
|
||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == oldEnv.ConfigurationId);
|
||||
if (isPluginInUse)
|
||||
PluginFactory.RemovePluginAsync(oldEnv.ConfigurationId);
|
||||
}
|
||||
}
|
||||
|
||||
// Process new or changed configs:
|
||||
foreach (var nextEnv in nextEnvironments)
|
||||
{
|
||||
var currentEnv = CURRENT_ENVIRONMENTS.FirstOrDefault(e => e.ConfigurationId == nextEnv.ConfigurationId);
|
||||
if (currentEnv == nextEnv) // Hint: This relies on the record equality to check if anything relevant has changed (e.g. server URL or ETag).
|
||||
{
|
||||
logger.LogInformation("Enterprise configuration '{ConfigId}' has not changed. No update required.", nextEnv.ConfigurationId);
|
||||
continue;
|
||||
}
|
||||
|
||||
var isNew = !currentIds.Contains(nextEnv.ConfigurationId);
|
||||
if(isNew)
|
||||
logger.LogInformation("Detected new enterprise configuration with ID '{ConfigId}' and server URL '{ServerUrl}'.", nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
||||
else
|
||||
logger.LogInformation("Detected change in enterprise configuration with ID '{ConfigId}'. Server URL or ETag has changed.", nextEnv.ConfigurationId);
|
||||
|
||||
if (isFirstRun)
|
||||
MessageBus.INSTANCE.DeferMessage(null, Event.STARTUP_ENTERPRISE_ENVIRONMENT, nextEnv);
|
||||
else
|
||||
await PluginFactory.TryDownloadingConfigPluginAsync(nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
||||
}
|
||||
|
||||
CURRENT_ENVIRONMENTS = nextEnvironments;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@ -1,71 +1,9 @@
|
||||
namespace AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to read the enterprise environment for the current user's configuration ID.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns the empty Guid when the environment is not set or the request fails.
|
||||
/// Otherwise, the configuration ID.
|
||||
/// </returns>
|
||||
public async Task<Guid> EnterpriseEnvConfigId()
|
||||
{
|
||||
var result = await this.http.GetAsync("/system/enterprise/config/id");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise configuration ID: '{result.StatusCode}'");
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
Guid.TryParse(await result.Content.ReadAsStringAsync(), out var configurationId);
|
||||
return configurationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the enterprise environment for a configuration ID, which must be removed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Removing a configuration ID is necessary when the user moved to another department or
|
||||
/// left the company, or when the configuration ID is no longer valid.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// Returns the empty Guid when the environment is not set or the request fails.
|
||||
/// Otherwise, the configuration ID.
|
||||
/// </returns>
|
||||
public async Task<Guid> EnterpriseEnvRemoveConfigId()
|
||||
{
|
||||
var result = await this.http.DeleteAsync("/system/enterprise/config/id");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise configuration ID for removal: '{result.StatusCode}'");
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
Guid.TryParse(await result.Content.ReadAsStringAsync(), out var configurationId);
|
||||
return configurationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the enterprise environment for the current user's configuration server URL.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns null when the environment is not set or the request fails.
|
||||
/// Otherwise, the configuration server URL.
|
||||
/// </returns>
|
||||
public async Task<string> EnterpriseEnvConfigServerUrl()
|
||||
{
|
||||
var result = await this.http.GetAsync("/system/enterprise/config/server");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise configuration server URL: '{result.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var serverUrl = await result.Content.ReadAsStringAsync();
|
||||
return string.IsNullOrWhiteSpace(serverUrl) ? string.Empty : serverUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the enterprise environment for the configuration encryption secret.
|
||||
/// </summary>
|
||||
@ -85,4 +23,67 @@ public sealed partial class RustService
|
||||
var encryptionSecret = await result.Content.ReadAsStringAsync();
|
||||
return string.IsNullOrWhiteSpace(encryptionSecret) ? string.Empty : encryptionSecret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all enterprise configurations (multi-config support).
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns a list of enterprise environments parsed from the Rust runtime.
|
||||
/// The ETag is not yet determined; callers must resolve it separately.
|
||||
/// </returns>
|
||||
public async Task<List<EnterpriseEnvironment>> EnterpriseEnvConfigs()
|
||||
{
|
||||
var result = await this.http.GetAsync("/system/enterprise/configs");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise configurations: '{result.StatusCode}'");
|
||||
return [];
|
||||
}
|
||||
|
||||
var configs = await result.Content.ReadFromJsonAsync<List<EnterpriseConfig>>(this.jsonRustSerializerOptions);
|
||||
if (configs is null)
|
||||
return [];
|
||||
|
||||
var environments = new List<EnterpriseEnvironment>();
|
||||
foreach (var config in configs)
|
||||
{
|
||||
if (Guid.TryParse(config.Id, out var id))
|
||||
environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, null));
|
||||
else
|
||||
this.logger!.LogWarning($"Skipping enterprise config with invalid ID: '{config.Id}'.");
|
||||
}
|
||||
|
||||
return environments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all enterprise configuration IDs that should be deleted.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns a list of GUIDs representing configuration IDs to remove.
|
||||
/// </returns>
|
||||
public async Task<List<Guid>> EnterpriseEnvDeleteConfigIds()
|
||||
{
|
||||
var result = await this.http.GetAsync("/system/enterprise/delete-configs");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise delete configuration IDs: '{result.StatusCode}'");
|
||||
return [];
|
||||
}
|
||||
|
||||
var ids = await result.Content.ReadFromJsonAsync<List<string>>(this.jsonRustSerializerOptions);
|
||||
if (ids is null)
|
||||
return [];
|
||||
|
||||
var guids = new List<Guid>();
|
||||
foreach (var idStr in ids)
|
||||
{
|
||||
if (Guid.TryParse(idStr, out var id))
|
||||
guids.Add(id);
|
||||
else
|
||||
this.logger!.LogWarning($"Skipping invalid GUID in enterprise delete config IDs: '{idStr}'.");
|
||||
}
|
||||
|
||||
return guids;
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
- Added an app setting to enable administration options for IT staff to configure and maintain organization-wide settings.
|
||||
- Added an option to export all provider types (LLMs, embeddings, transcriptions) so you can use them in a configuration plugin. You'll be asked if you want to export the related API key too. API keys will be encrypted in the export. This feature only shows up when administration options are enabled.
|
||||
- Added an option in the app settings to create an encryption secret, which is required to encrypt values (for example, API keys) in configuration plugins. This feature only shows up when administration options are enabled.
|
||||
- Added support for using multiple enterprise configurations simultaneously. Enabled organizations to apply configurations based on employee affiliations, such as departments and working groups. See the enterprise configuration documentation for details.
|
||||
- Improved the document analysis assistant (in beta) by hiding the export functionality by default. Enable the administration options in the app settings to show and use the export functionality. This streamlines the usage for regular users.
|
||||
- Improved the workspaces experience by using a different color for the delete button to avoid confusion.
|
||||
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use std::env;
|
||||
use std::sync::OnceLock;
|
||||
use log::{debug, warn};
|
||||
use log::{debug, info, warn};
|
||||
use rocket::{delete, get};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
use sys_locale::get_locale;
|
||||
use crate::api_token::APIToken;
|
||||
|
||||
@ -143,23 +145,124 @@ pub fn read_enterprise_env_config_encryption_secret(_token: APIToken) -> String
|
||||
)
|
||||
}
|
||||
|
||||
/// Represents a single enterprise configuration entry with an ID and server URL.
|
||||
#[derive(Serialize)]
|
||||
pub struct EnterpriseConfig {
|
||||
pub id: String,
|
||||
pub server_url: String,
|
||||
}
|
||||
|
||||
/// Returns all enterprise configurations. Supports the new multi-config format
|
||||
/// (`id1@url1;id2@url2`) as well as the legacy single-config environment variables.
|
||||
#[get("/system/enterprise/configs")]
|
||||
pub fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseConfig>> {
|
||||
info!("Trying to read the enterprise environment for all configurations.");
|
||||
|
||||
// Try the new combined format first:
|
||||
let combined = get_enterprise_configuration(
|
||||
"configs",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS",
|
||||
);
|
||||
|
||||
if !combined.is_empty() {
|
||||
// Parse the new format: id1@url1;id2@url2; ...
|
||||
let configs: Vec<EnterpriseConfig> = combined
|
||||
.split(';')
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.trim();
|
||||
if entry.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Split at the first '@' (GUIDs never contain '@'):
|
||||
entry.split_once('@').and_then(|(id, url)| {
|
||||
let id = id.trim().to_string();
|
||||
let url = url.trim().to_string();
|
||||
if id.is_empty() || url.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(EnterpriseConfig { id, server_url: url })
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Json(configs);
|
||||
}
|
||||
|
||||
// Fallback: read the legacy single-config variables:
|
||||
let config_id = get_enterprise_configuration(
|
||||
"config_id",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID",
|
||||
);
|
||||
|
||||
let config_server_url = get_enterprise_configuration(
|
||||
"config_server_url",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL",
|
||||
);
|
||||
|
||||
if !config_id.is_empty() && !config_server_url.is_empty() {
|
||||
return Json(vec![EnterpriseConfig {
|
||||
id: config_id,
|
||||
server_url: config_server_url,
|
||||
}]);
|
||||
}
|
||||
|
||||
Json(vec![])
|
||||
}
|
||||
|
||||
/// Returns all enterprise configuration IDs that should be deleted. Supports the new
|
||||
/// multi-delete format (`id1;id2;id3`) as well as the legacy single-delete variable.
|
||||
#[get("/system/enterprise/delete-configs")]
|
||||
pub fn read_enterprise_delete_config_ids(_token: APIToken) -> Json<Vec<String>> {
|
||||
info!("Trying to read the enterprise environment for configuration IDs to delete.");
|
||||
|
||||
// Try the new combined format first:
|
||||
let combined = get_enterprise_configuration(
|
||||
"delete_config_ids",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_IDS",
|
||||
);
|
||||
|
||||
if !combined.is_empty() {
|
||||
let ids: Vec<String> = combined
|
||||
.split(';')
|
||||
.map(|id| id.trim().to_string())
|
||||
.filter(|id| !id.is_empty())
|
||||
.collect();
|
||||
|
||||
return Json(ids);
|
||||
}
|
||||
|
||||
// Fallback: read the legacy single-delete variable:
|
||||
let delete_id = get_enterprise_configuration(
|
||||
"delete_config_id",
|
||||
"MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID",
|
||||
);
|
||||
|
||||
if !delete_id.is_empty() {
|
||||
return Json(vec![delete_id]);
|
||||
}
|
||||
|
||||
Json(vec![])
|
||||
}
|
||||
|
||||
fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "windows")] {
|
||||
debug!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT' or environment variables.");
|
||||
info!(r"Detected a Windows machine, trying to read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\{}' or the environment variable '{}'.", _reg_value, env_name);
|
||||
use windows_registry::*;
|
||||
let key_path = r"Software\github\MindWork AI Studio\Enterprise IT";
|
||||
let key = match CURRENT_USER.open(key_path) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
debug!(r"Could not read the registry key HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT. Falling back to environment variables.");
|
||||
info!(r"Could not read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\{}'. Falling back to the environment variable '{}'.", _reg_value, env_name);
|
||||
return match env::var(env_name) {
|
||||
Ok(val) => {
|
||||
debug!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
info!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
val
|
||||
},
|
||||
Err(_) => {
|
||||
debug!("Falling back to the environment variable '{}' was not successful.", env_name);
|
||||
info!("Falling back to the environment variable '{}' was not successful. It seems that there is no enterprise environment available.", env_name);
|
||||
"".to_string()
|
||||
},
|
||||
}
|
||||
@ -169,14 +272,14 @@ fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
match key.get_string(_reg_value) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
debug!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to environment variables.", _reg_value);
|
||||
info!(r"We could read the registry key 'HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT', but the value '{}' could not be read. Falling back to the environment variable '{}'.", _reg_value, env_name);
|
||||
match env::var(env_name) {
|
||||
Ok(val) => {
|
||||
debug!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
info!("Falling back to the environment variable '{}' was successful.", env_name);
|
||||
val
|
||||
},
|
||||
Err(_) => {
|
||||
debug!("Falling back to the environment variable '{}' was not successful.", env_name);
|
||||
info!("Falling back to the environment variable '{}' was not successful. It seems that there is no enterprise environment available.", env_name);
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
@ -184,11 +287,11 @@ fn get_enterprise_configuration(_reg_value: &str, env_name: &str) -> String {
|
||||
}
|
||||
} else {
|
||||
// In the case of macOS or Linux, we just read the environment variable:
|
||||
debug!(r"Detected a Unix machine, trying to read the environment variable '{}'.", env_name);
|
||||
info!(r"Detected a Unix machine, trying to read the environment variable '{}'.", env_name);
|
||||
match env::var(env_name) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
debug!("The environment variable '{}' was not found.", env_name);
|
||||
info!("The environment variable '{}' was not found. It seems that there is no enterprise environment available.", env_name);
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +86,8 @@ pub fn start_runtime_api() {
|
||||
crate::environment::delete_enterprise_env_config_id,
|
||||
crate::environment::read_enterprise_env_config_server_url,
|
||||
crate::environment::read_enterprise_env_config_encryption_secret,
|
||||
crate::environment::read_enterprise_configs,
|
||||
crate::environment::read_enterprise_delete_config_ids,
|
||||
crate::file_data::extract_data,
|
||||
crate::log::get_log_paths,
|
||||
crate::log::log_event,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user