2025-06-01 19:14:21 +00:00
using AIStudio.Settings ;
2026-04-10 15:11:05 +00:00
using AIStudio.Settings.DataModel ;
2026-02-07 21:59:41 +00:00
using AIStudio.Tools.Services ;
2025-06-01 19:14:21 +00:00
using Lua ;
namespace AIStudio.Tools.PluginSystem ;
public sealed class PluginConfiguration ( bool isInternal , LuaState state , PluginType type ) : PluginBase ( isInternal , state , type )
{
private static string TB ( string fallbackEN ) = > I18N . I . T ( fallbackEN , typeof ( PluginConfiguration ) . Namespace , nameof ( PluginConfiguration ) ) ;
private static readonly SettingsManager SETTINGS_MANAGER = Program . SERVICE_PROVIDER . GetRequiredService < SettingsManager > ( ) ;
2026-02-07 21:59:41 +00:00
private static readonly ILogger LOG = Program . LOGGER_FACTORY . CreateLogger ( nameof ( PluginConfiguration ) ) ;
2025-08-26 08:59:56 +00:00
private List < PluginConfigurationObject > configObjects = [ ] ;
2026-04-10 15:11:05 +00:00
private List < DataMandatoryInfo > mandatoryInfos = [ ] ;
2025-08-18 18:40:52 +00:00
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable < PluginConfigurationObject > ConfigObjects = > this . configObjects ;
2026-02-19 19:43:47 +00:00
2026-04-10 15:11:05 +00:00
/// <summary>
/// The list of mandatory infos provided by this configuration plugin.
/// </summary>
public IReadOnlyList < DataMandatoryInfo > MandatoryInfos = > this . mandatoryInfos ;
2026-02-19 19:43:47 +00:00
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
public bool? DeployedUsingConfigServer { get ; } = ReadDeployedUsingConfigServer ( state ) ;
2025-08-18 18:40:52 +00:00
2025-08-09 17:29:43 +00:00
public async Task InitializeAsync ( bool dryRun )
2025-06-01 19:14:21 +00:00
{
2025-08-09 17:29:43 +00:00
if ( ! this . TryProcessConfiguration ( dryRun , out var issue ) )
2026-04-09 08:08:37 +00:00
this . PluginIssues . Add ( issue ) ;
2025-08-09 17:29:43 +00:00
if ( ! dryRun )
{
2026-02-07 21:59:41 +00:00
// Store any decrypted API keys from enterprise configuration in the OS keyring:
await StoreEnterpriseApiKeysAsync ( ) ;
2025-08-09 17:29:43 +00:00
await SETTINGS_MANAGER . StoreSettings ( ) ;
await MessageBus . INSTANCE . SendMessage < bool > ( null , Event . CONFIGURATION_CHANGED ) ;
}
2025-06-01 19:14:21 +00:00
}
2025-08-09 17:29:43 +00:00
2026-02-07 21:59:41 +00:00
/// <summary>
/// Stores any pending enterprise API keys in the OS keyring.
/// </summary>
private static async Task StoreEnterpriseApiKeysAsync ( )
{
var pendingKeys = PendingEnterpriseApiKeys . GetAndClear ( ) ;
if ( pendingKeys . Count = = 0 )
return ;
LOG . LogInformation ( $"Storing {pendingKeys.Count} enterprise API key(s) in the OS keyring." ) ;
var rustService = Program . SERVICE_PROVIDER . GetRequiredService < RustService > ( ) ;
foreach ( var pendingKey in pendingKeys )
{
try
{
// Create a temporary secret ID object for storing the key:
var secretId = new TemporarySecretId ( pendingKey . SecretId , pendingKey . SecretName ) ;
var result = await rustService . SetAPIKey ( secretId , pendingKey . ApiKey , pendingKey . StoreType ) ;
if ( result . Success )
LOG . LogDebug ( $"Successfully stored enterprise API key for '{pendingKey.SecretName}' in the OS keyring." ) ;
else
LOG . LogWarning ( $"Failed to store enterprise API key for '{pendingKey.SecretName}': {result.Issue}" ) ;
}
catch ( Exception ex )
{
LOG . LogError ( ex , $"Exception while storing enterprise API key for '{pendingKey.SecretName}'." ) ;
}
}
}
/// <summary>
/// Temporary implementation of ISecretId for storing enterprise API keys.
/// </summary>
private sealed record TemporarySecretId ( string SecretId , string SecretName ) : ISecretId ;
2026-02-19 19:43:47 +00:00
private static bool? ReadDeployedUsingConfigServer ( LuaState state )
{
if ( state . Environment [ "DEPLOYED_USING_CONFIG_SERVER" ] . TryRead < bool > ( out var deployedUsingConfigServer ) )
return deployedUsingConfigServer ;
return null ;
}
2025-06-01 19:14:21 +00:00
/// <summary>
/// Tries to initialize the UI text content of the plugin.
/// </summary>
2025-08-18 18:40:52 +00:00
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
2025-06-01 19:14:21 +00:00
/// <param name="message">The error message, when the UI text content could not be read.</param>
/// <returns>True, when the UI text content could be read successfully.</returns>
2025-08-09 17:29:43 +00:00
private bool TryProcessConfiguration ( bool dryRun , out string message )
2025-06-01 19:14:21 +00:00
{
2025-08-18 18:40:52 +00:00
this . configObjects . Clear ( ) ;
2026-04-10 15:11:05 +00:00
this . mandatoryInfos . Clear ( ) ;
2025-08-18 18:40:52 +00:00
2025-06-01 19:14:21 +00:00
// Ensure that the main CONFIG table exists and is a valid Lua table:
2026-04-09 08:08:37 +00:00
if ( ! this . State . Environment [ "CONFIG" ] . TryRead < LuaTable > ( out var mainTable ) )
2025-06-01 19:14:21 +00:00
{
message = TB ( "The CONFIG table does not exist or is not a valid table." ) ;
return false ;
}
2025-08-26 08:59:56 +00:00
// Check for the main SETTINGS table:
2025-06-01 19:14:21 +00:00
if ( ! mainTable . TryGetValue ( "SETTINGS" , out var settingsValue ) | | ! settingsValue . TryRead < LuaTable > ( out var settingsTable ) )
{
message = TB ( "The SETTINGS table does not exist or is not a valid table." ) ;
return false ;
}
2025-08-09 17:29:43 +00:00
2025-08-26 08:59:56 +00:00
// Config: check for updates, and if so, how often?
2025-08-26 18:06:13 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UpdateInterval , this . Id , settingsTable , dryRun ) ;
// Config: how should updates be installed?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UpdateInstallation , this . Id , settingsTable , dryRun ) ;
2026-03-21 17:05:06 +00:00
// Config: what should be the start page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . StartPage , this . Id , settingsTable , dryRun ) ;
2025-08-09 17:29:43 +00:00
2025-08-26 08:59:56 +00:00
// Config: allow the user to add providers?
2025-08-09 17:29:43 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . AllowUserToAddProvider , this . Id , settingsTable , dryRun ) ;
2026-02-07 21:59:41 +00:00
// Config: show administration settings?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowAdminSettings , this . Id , settingsTable , dryRun ) ;
2025-06-01 19:14:21 +00:00
2025-10-19 09:51:28 +00:00
// Config: preview features visibility
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreviewVisibility , this . Id , settingsTable , dryRun ) ;
2026-02-20 11:40:38 +00:00
// Config: enabled preview features (plugin contribution; users can enable additional features)
ManagedConfiguration . TryProcessConfigurationWithPluginContribution ( x = > x . App , x = > x . EnabledPreviewFeatures , this . Id , settingsTable , dryRun ) ;
2025-10-19 09:51:28 +00:00
2026-01-12 19:43:45 +00:00
// Config: hide some assistants?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . HiddenAssistants , this . Id , settingsTable , dryRun ) ;
2026-01-24 19:05:34 +00:00
// Config: global voice recording shortcut
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShortcutVoiceRecording , this . Id , settingsTable , dryRun ) ;
2025-08-26 08:59:56 +00:00
// Handle configured LLM providers:
2025-09-03 20:41:28 +00:00
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . LLM_PROVIDER , x = > x . Providers , x = > x . NextProviderNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-01-09 14:41:54 +00:00
2026-01-09 14:49:44 +00:00
// Handle configured transcription providers:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . TRANSCRIPTION_PROVIDER , x = > x . TranscriptionProviders , x = > x . NextTranscriptionNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-01-09 14:41:54 +00:00
// Handle configured embedding providers:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . EMBEDDING_PROVIDER , x = > x . EmbeddingProviders , x = > x . NextEmbeddingNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2025-08-26 08:59:56 +00:00
// Handle configured chat templates:
2025-09-03 20:41:28 +00:00
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . CHAT_TEMPLATE , x = > x . ChatTemplates , x = > x . NextChatTemplateNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2025-08-18 18:40:52 +00:00
2025-11-14 11:04:01 +00:00
// Handle configured profiles:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . PROFILE , x = > x . Profiles , x = > x . NextProfileNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-02-01 13:50:19 +00:00
// Handle configured document analysis policies:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . DOCUMENT_ANALYSIS_POLICY , x = > x . DocumentAnalysis . Policies , x = > x . NextDocumentAnalysisPolicyNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-04-10 15:11:05 +00:00
// Handle configured mandatory infos:
this . TryReadMandatoryInfos ( mainTable ) ;
2026-02-01 13:50:19 +00:00
2026-02-20 09:08:06 +00:00
// Config: preselected provider?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreselectedProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2025-11-14 11:04:01 +00:00
// Config: preselected profile?
2025-12-04 14:37:13 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreselectedProfile , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2026-01-09 14:49:44 +00:00
// Config: transcription provider?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UseTranscriptionProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2025-08-26 08:59:56 +00:00
message = string . Empty ;
2025-08-18 18:40:52 +00:00
return true ;
}
2026-04-10 15:11:05 +00:00
private void TryReadMandatoryInfos ( LuaTable mainTable )
{
if ( ! mainTable . TryGetValue ( "MANDATORY_INFOS" , out var mandatoryInfosValue ) | | ! mandatoryInfosValue . TryRead < LuaTable > ( out var mandatoryInfosTable ) )
return ;
for ( var i = 1 ; i < = mandatoryInfosTable . ArrayLength ; i + + )
{
var luaMandatoryInfoValue = mandatoryInfosTable [ i ] ;
if ( ! luaMandatoryInfoValue . TryRead < LuaTable > ( out var luaMandatoryInfoTable ) )
{
LOG . LogWarning ( "The table 'MANDATORY_INFOS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
continue ;
}
if ( DataMandatoryInfo . TryParseConfiguration ( i , luaMandatoryInfoTable , this . Id , out var mandatoryInfo ) )
this . mandatoryInfos . Add ( mandatoryInfo ) ;
else
LOG . LogWarning ( "The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
}
}
2026-02-07 21:59:41 +00:00
}