mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-11 23:13:47 +00:00
Merge branch 'main' into custom-tokenizer
This commit is contained in:
commit
e3cb7e9734
165 files changed
+4739
-1738
No files matched your search
@@ -47,6 +47,7 @@ public static class AssistantVisibilityExtensions
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT,
|
||||
Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT,
|
||||
Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT,
|
||||
Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT,
|
||||
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
|
||||
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
|
||||
|
||||
@@ -7,6 +7,7 @@ public enum Components
|
||||
GRAMMAR_SPELLING_ASSISTANT,
|
||||
ICON_FINDER_ASSISTANT,
|
||||
REWRITE_ASSISTANT,
|
||||
PROMPT_OPTIMIZER_ASSISTANT,
|
||||
TRANSLATION_ASSISTANT,
|
||||
AGENDA_ASSISTANT,
|
||||
CODING_ASSISTANT,
|
||||
@@ -33,4 +34,4 @@ public enum Components
|
||||
AGENT_DATA_SOURCE_SELECTION,
|
||||
AGENT_RETRIEVAL_CONTEXT_VALIDATION,
|
||||
AGENT_ASSISTANT_PLUGIN_AUDIT,
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public static class ComponentsExtensions
|
||||
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
|
||||
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
|
||||
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"),
|
||||
Components.AGENDA_ASSISTANT => TB("Agenda Assistant"),
|
||||
Components.CODING_ASSISTANT => TB("Coding Assistant"),
|
||||
Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"),
|
||||
@@ -58,6 +59,7 @@ public static class ComponentsExtensions
|
||||
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
|
||||
Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
|
||||
Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER),
|
||||
Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
|
||||
Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
|
||||
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
|
||||
@@ -80,6 +82,7 @@ public static class ComponentsExtensions
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
|
||||
@@ -108,6 +111,7 @@ public static class ComponentsExtensions
|
||||
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null,
|
||||
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null,
|
||||
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null,
|
||||
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : null,
|
||||
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null,
|
||||
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null,
|
||||
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null,
|
||||
@@ -169,4 +173,4 @@ public static class ComponentsExtensions
|
||||
|
||||
_ => ChatTemplate.NO_CHAT_TEMPLATE,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace AIStudio.Tools.ERIClient;
|
||||
|
||||
public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
||||
{
|
||||
protected readonly IERIDataSource dataSource = dataSource;
|
||||
protected readonly IERIDataSource DataSource = dataSource;
|
||||
|
||||
protected static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
@@ -23,18 +23,18 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
||||
}
|
||||
};
|
||||
|
||||
protected readonly HttpClient httpClient = new()
|
||||
protected readonly HttpClient HttpClient = new()
|
||||
{
|
||||
BaseAddress = new Uri($"{dataSource.Hostname}:{dataSource.Port}"),
|
||||
};
|
||||
|
||||
protected string securityToken = string.Empty;
|
||||
protected string SecurityToken = string.Empty;
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.httpClient.Dispose();
|
||||
this.HttpClient.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await this.httpClient.GetAsync("/auth/methods", cancellationToken);
|
||||
using var response = await this.HttpClient.GetAsync("/auth/methods", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -66,14 +66,14 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
{
|
||||
try
|
||||
{
|
||||
var authMethod = this.dataSource.AuthMethod;
|
||||
var username = this.dataSource.Username;
|
||||
switch (this.dataSource.AuthMethod)
|
||||
var authMethod = this.DataSource.AuthMethod;
|
||||
var username = this.DataSource.Username;
|
||||
switch (this.DataSource.AuthMethod)
|
||||
{
|
||||
case AuthMethod.NONE:
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
|
||||
{
|
||||
using var noneAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var noneAuthResponse = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!noneAuthResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -93,7 +93,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
};
|
||||
}
|
||||
|
||||
this.securityToken = noneAuthResult.Token ?? string.Empty;
|
||||
this.SecurityToken = noneAuthResult.Token ?? string.Empty;
|
||||
return new()
|
||||
{
|
||||
Successful = true,
|
||||
@@ -105,7 +105,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
string password;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var passwordResponse = await rustService.GetSecret(this.dataSource);
|
||||
var passwordResponse = await rustService.GetSecret(this.DataSource);
|
||||
if (!passwordResponse.Success)
|
||||
{
|
||||
return new()
|
||||
@@ -127,7 +127,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
request.Headers.Add("user", username);
|
||||
request.Headers.Add("password", password);
|
||||
|
||||
using var usernamePasswordAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var usernamePasswordAuthResponse = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!usernamePasswordAuthResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -147,7 +147,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
};
|
||||
}
|
||||
|
||||
this.securityToken = usernamePasswordAuthResult.Token ?? string.Empty;
|
||||
this.SecurityToken = usernamePasswordAuthResult.Token ?? string.Empty;
|
||||
return new()
|
||||
{
|
||||
Successful = true,
|
||||
@@ -159,7 +159,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
string token;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var tokenResponse = await rustService.GetSecret(this.dataSource);
|
||||
var tokenResponse = await rustService.GetSecret(this.DataSource);
|
||||
if (!tokenResponse.Success)
|
||||
{
|
||||
return new()
|
||||
@@ -178,7 +178,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
{
|
||||
request.Headers.Add("Authorization", $"Bearer {token}");
|
||||
|
||||
using var tokenAuthResponse = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var tokenAuthResponse = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!tokenAuthResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -198,7 +198,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
};
|
||||
}
|
||||
|
||||
this.securityToken = tokenAuthResult.Token ?? string.Empty;
|
||||
this.SecurityToken = tokenAuthResult.Token ?? string.Empty;
|
||||
return new()
|
||||
{
|
||||
Successful = true,
|
||||
@@ -207,7 +207,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
}
|
||||
|
||||
default:
|
||||
this.securityToken = string.Empty;
|
||||
this.SecurityToken = string.Empty;
|
||||
return new()
|
||||
{
|
||||
Successful = false,
|
||||
@@ -238,9 +238,9 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/dataSource");
|
||||
request.Headers.Add("token", this.securityToken);
|
||||
request.Headers.Add("token", this.SecurityToken);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var response = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -289,9 +289,9 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/embedding/info");
|
||||
request.Headers.Add("token", this.securityToken);
|
||||
request.Headers.Add("token", this.SecurityToken);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var response = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -340,9 +340,9 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/retrieval/info");
|
||||
request.Headers.Add("token", this.securityToken);
|
||||
request.Headers.Add("token", this.SecurityToken);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var response = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -391,12 +391,12 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
try
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/retrieval");
|
||||
requestMessage.Headers.Add("token", this.securityToken);
|
||||
requestMessage.Headers.Add("token", this.SecurityToken);
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(request, JSON_OPTIONS), Encoding.UTF8, "application/json");
|
||||
requestMessage.Content = content;
|
||||
|
||||
using var response = await this.httpClient.SendAsync(requestMessage, cancellationToken);
|
||||
using var response = await this.HttpClient.SendAsync(requestMessage, cancellationToken);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
@@ -445,9 +445,9 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/security/requirements");
|
||||
request.Headers.Add("token", this.securityToken);
|
||||
request.Headers.Add("token", this.SecurityToken);
|
||||
|
||||
using var response = await this.httpClient.SendAsync(request, cancellationToken);
|
||||
using var response = await this.HttpClient.SendAsync(request, cancellationToken);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new()
|
||||
|
||||
@@ -46,11 +46,13 @@ public enum Event
|
||||
SEND_TO_GRAMMAR_SPELLING_ASSISTANT,
|
||||
SEND_TO_ICON_FINDER_ASSISTANT,
|
||||
SEND_TO_REWRITE_ASSISTANT,
|
||||
SEND_TO_PROMPT_OPTIMIZER_ASSISTANT,
|
||||
SEND_TO_TRANSLATION_ASSISTANT,
|
||||
SEND_TO_AGENDA_ASSISTANT,
|
||||
SEND_TO_CODING_ASSISTANT,
|
||||
SEND_TO_TEXT_SUMMARIZER_ASSISTANT,
|
||||
SEND_TO_CHAT,
|
||||
SEND_TO_CHAT_INPUT,
|
||||
SEND_TO_EMAIL_ASSISTANT,
|
||||
SEND_TO_LEGAL_CHECK_ASSISTANT,
|
||||
SEND_TO_SYNONYMS_ASSISTANT,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Markdig;
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
@@ -26,4 +27,123 @@ public static class Markdown
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
public static string RemoveSharedIndentation(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return string.Empty;
|
||||
|
||||
return RemoveSharedIndentation(value.AsSpan());
|
||||
}
|
||||
|
||||
private static string RemoveSharedIndentation(ReadOnlySpan<char> value)
|
||||
{
|
||||
var firstContentLineStart = -1;
|
||||
var lastContentLineStart = -1;
|
||||
var lastContentLineEnd = -1;
|
||||
var commonIndentation = int.MaxValue;
|
||||
var position = 0;
|
||||
|
||||
while (TryGetNextLine(value, position, out var lineStart, out var currentLineEnd, out var nextPosition))
|
||||
{
|
||||
var lineContent = value[lineStart..currentLineEnd];
|
||||
if (IsWhiteSpace(lineContent))
|
||||
{
|
||||
position = nextPosition;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstContentLineStart < 0)
|
||||
firstContentLineStart = lineStart;
|
||||
|
||||
lastContentLineStart = lineStart;
|
||||
lastContentLineEnd = currentLineEnd;
|
||||
commonIndentation = Math.Min(commonIndentation, CountIndentation(lineContent));
|
||||
position = nextPosition;
|
||||
}
|
||||
|
||||
if (firstContentLineStart < 0)
|
||||
return string.Empty;
|
||||
|
||||
if (commonIndentation == int.MaxValue)
|
||||
commonIndentation = 0;
|
||||
|
||||
var builder = new StringBuilder(lastContentLineEnd - firstContentLineStart);
|
||||
var shouldAppendLineBreak = false;
|
||||
position = firstContentLineStart;
|
||||
|
||||
while (TryGetNextLine(value, position, out var lineStart, out var lineEnd, out var nextPosition))
|
||||
{
|
||||
var lineContent = value[lineStart..lineEnd];
|
||||
|
||||
if (shouldAppendLineBreak)
|
||||
builder.Append('\n');
|
||||
|
||||
if (IsWhiteSpace(lineContent))
|
||||
shouldAppendLineBreak = true;
|
||||
else if (lineContent.Length > commonIndentation)
|
||||
{
|
||||
builder.Append(lineContent[commonIndentation..]);
|
||||
shouldAppendLineBreak = true;
|
||||
}
|
||||
else
|
||||
shouldAppendLineBreak = true;
|
||||
|
||||
if (lineStart == lastContentLineStart)
|
||||
break;
|
||||
|
||||
position = nextPosition;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static bool IsWhiteSpace(ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (!char.IsWhiteSpace(character))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CountIndentation(ReadOnlySpan<char> value)
|
||||
{
|
||||
var indentation = 0;
|
||||
while (indentation < value.Length && char.IsWhiteSpace(value[indentation]))
|
||||
indentation++;
|
||||
|
||||
return indentation;
|
||||
}
|
||||
|
||||
private static bool TryGetNextLine(ReadOnlySpan<char> value, int position, out int lineStart, out int lineEnd, out int nextPosition)
|
||||
{
|
||||
if (position > value.Length)
|
||||
{
|
||||
lineStart = 0;
|
||||
lineEnd = 0;
|
||||
nextPosition = position;
|
||||
return false;
|
||||
}
|
||||
|
||||
lineStart = position;
|
||||
for (var i = position; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] != '\n')
|
||||
continue;
|
||||
|
||||
lineEnd = i > lineStart && value[i - 1] == '\r'
|
||||
? i - 1
|
||||
: i;
|
||||
|
||||
nextPosition = i + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
lineEnd = value.Length;
|
||||
nextPosition = value.Length + 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ public static partial class Pandoc
|
||||
/// </summary>
|
||||
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
|
||||
|
||||
private static readonly HttpClient WEB_CLIENT = new();
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a Pandoc process by using the Pandoc process builder.
|
||||
/// </summary>
|
||||
@@ -181,21 +183,18 @@ public static partial class Pandoc
|
||||
// Download the latest Pandoc archive from GitHub:
|
||||
//
|
||||
var uri = await GenerateArchiveUriAsync();
|
||||
using (var client = new HttpClient())
|
||||
var response = await WEB_CLIENT.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var response = await client.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
|
||||
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the archive to the temporary file:
|
||||
await using var tempFileStream = File.Create(pandocTempDownloadFile);
|
||||
await response.Content.CopyToAsync(tempFileStream);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
|
||||
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the archive to the temporary file:
|
||||
await using var tempFileStream = File.Create(pandocTempDownloadFile);
|
||||
await response.Content.CopyToAsync(tempFileStream);
|
||||
|
||||
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir);
|
||||
@@ -245,9 +244,7 @@ public static partial class Pandoc
|
||||
/// <remarks>Version numbers can have the following formats: x.x, x.x.x or x.x.x.x</remarks>
|
||||
/// <returns>Latest Pandoc version number</returns>
|
||||
public static async Task<string> FetchLatestVersionAsync() {
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync(LATEST_URL);
|
||||
|
||||
var response = await WEB_CLIENT.GetAsync(LATEST_URL);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage);
|
||||
|
||||
@@ -69,7 +69,7 @@ public static class PandocExport
|
||||
var pandoc = await PandocProcessBuilder
|
||||
.Create()
|
||||
.UseStandaloneMode()
|
||||
.WithInputFormat("markdown")
|
||||
.WithInputFormat("gfm+emoji+tex_math_dollars")
|
||||
.WithOutputFormat("docx")
|
||||
.WithOutputFile(response.SaveFilePath)
|
||||
.WithInputFile(tempMarkdownFilePath)
|
||||
|
||||
@@ -121,6 +121,26 @@ internal sealed class AssistantDropdown : StatefulAssistantComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
internal string ResolveDisplayText(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return this.Default.Display;
|
||||
|
||||
var item = this.GetRenderedItems().FirstOrDefault(item => string.Equals(item.Value, value, StringComparison.Ordinal));
|
||||
return item?.Display ?? value;
|
||||
}
|
||||
|
||||
private List<AssistantDropdownItem> GetRenderedItems()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this.Default.Value))
|
||||
return this.Items;
|
||||
|
||||
if (this.Items.Any(item => string.Equals(item.Value, this.Default.Value, StringComparison.Ordinal)))
|
||||
return this.Items;
|
||||
|
||||
return [this.Default, .. this.Items];
|
||||
}
|
||||
|
||||
public IEnumerable<object> GetParsedDropdownValues()
|
||||
{
|
||||
foreach (var item in this.Items)
|
||||
|
||||
+48
@@ -10,6 +10,11 @@ internal static class AssistantLuaConversion
|
||||
/// </summary>
|
||||
public static LuaTable CreateLuaArray(IEnumerable values) => CreateLuaArrayCore(values);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a readable string representation of a Lua table for debugging and inspection.
|
||||
/// </summary>
|
||||
public static string InspectTable(LuaTable table) => InspectTableCore(table, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a Lua value into either a scalar .NET value or one of the structured assistant data model types.
|
||||
/// Lua itself only exposes scalars and tables, so structured assistant types such as dropdown/list items
|
||||
@@ -268,4 +273,47 @@ internal static class AssistantLuaConversion
|
||||
|
||||
return luaArray;
|
||||
}
|
||||
|
||||
private static string InspectTableCore(LuaTable table, int depth)
|
||||
{
|
||||
if (depth > 8)
|
||||
return "{ ... }";
|
||||
|
||||
var indent = new string(' ', depth * 2);
|
||||
var childIndent = new string(' ', (depth + 1) * 2);
|
||||
var builder = new System.Text.StringBuilder();
|
||||
builder.AppendLine("{");
|
||||
|
||||
foreach (var entry in table)
|
||||
{
|
||||
builder.Append(childIndent);
|
||||
builder.Append(FormatLuaValue(entry.Key));
|
||||
builder.Append(" = ");
|
||||
builder.AppendLine(FormatLuaValue(entry.Value, depth + 1));
|
||||
}
|
||||
|
||||
builder.Append(indent);
|
||||
builder.Append('}');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string FormatLuaValue(LuaValue value, int depth = 0)
|
||||
{
|
||||
if (value.Type is LuaValueType.Nil)
|
||||
return "nil";
|
||||
|
||||
if (value.TryRead<string>(out var stringValue))
|
||||
return $"\"{stringValue.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
|
||||
|
||||
if (value.TryRead<bool>(out var boolValue))
|
||||
return boolValue ? "true" : "false";
|
||||
|
||||
if (value.TryRead<double>(out var doubleValue))
|
||||
return doubleValue.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
if (value.TryRead<LuaTable>(out var tableValue))
|
||||
return InspectTableCore(tableValue, depth);
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -156,12 +156,17 @@ public sealed class AssistantState
|
||||
{
|
||||
if (component is INamedAssistantComponent named)
|
||||
{
|
||||
target[named.Name] = new LuaTable
|
||||
var componentEntry = new LuaTable
|
||||
{
|
||||
["Type"] = Enum.GetName(component.Type) ?? string.Empty,
|
||||
["Value"] = component is IStatefulAssistantComponent ? this.ReadValueForLua(named.Name) : LuaValue.Nil,
|
||||
["Props"] = this.CreatePropsTable(component),
|
||||
};
|
||||
|
||||
if (component is AssistantDropdown dropdown)
|
||||
this.AddDropdownDisplay(componentEntry, dropdown, named.Name);
|
||||
|
||||
target[named.Name] = componentEntry;
|
||||
}
|
||||
|
||||
if (component.Children.Count > 0)
|
||||
@@ -218,6 +223,27 @@ public sealed class AssistantState
|
||||
return table;
|
||||
}
|
||||
|
||||
private void AddDropdownDisplay(LuaTable componentEntry, AssistantDropdown dropdown, string name)
|
||||
{
|
||||
if (dropdown.IsMultiselect)
|
||||
{
|
||||
if (!this.MultiSelect.TryGetValue(name, out var selectedValues))
|
||||
return;
|
||||
|
||||
componentEntry["Display"] = AssistantLuaConversion.CreateLuaArray(
|
||||
selectedValues
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.Select(dropdown.ResolveDisplayText));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.SingleSelect.TryGetValue(name, out var selectedValue))
|
||||
return;
|
||||
|
||||
componentEntry["Display"] = dropdown.ResolveDisplayText(selectedValue);
|
||||
}
|
||||
|
||||
private static HashSet<string> ReadStringValues(LuaTable values)
|
||||
{
|
||||
var parsedValues = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
+29
-19
@@ -73,6 +73,8 @@ public static class PluginAssistantSecurityResolver
|
||||
public static PluginAssistantSecurityState Resolve(SettingsManager settingsManager, PluginAssistants plugin)
|
||||
{
|
||||
var auditSettings = settingsManager.ConfigurationData.AssistantPluginAudit;
|
||||
var enforceAuditBeforeActivation = auditSettings.RequireAuditBeforeActivation;
|
||||
var isEnforcementDisabled = !enforceAuditBeforeActivation;
|
||||
var currentHash = plugin.ComputeAuditHash();
|
||||
var audit = settingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == plugin.Id);
|
||||
var hasAudit = audit is not null && audit.Level is not AssistantAuditLevel.UNKNOWN;
|
||||
@@ -80,9 +82,9 @@ public static class PluginAssistantSecurityResolver
|
||||
var hasHashMismatch = hasAudit && !hashMatches;
|
||||
var isBelowMinimum = hashMatches && audit is not null && audit.Level < auditSettings.MinimumLevel;
|
||||
var meetsMinimum = hashMatches && audit is not null && audit.Level >= auditSettings.MinimumLevel;
|
||||
var requiresAudit = hasHashMismatch || auditSettings.RequireAuditBeforeActivation && !hasAudit;
|
||||
var isBlocked = requiresAudit || isBelowMinimum && auditSettings.BlockActivationBelowMinimum;
|
||||
var canOverride = isBelowMinimum && !auditSettings.BlockActivationBelowMinimum;
|
||||
var requiresAudit = enforceAuditBeforeActivation && (hasHashMismatch || !hasAudit);
|
||||
var isBlocked = requiresAudit || enforceAuditBeforeActivation && isBelowMinimum && auditSettings.BlockActivationBelowMinimum;
|
||||
var canOverride = isBelowMinimum && (!auditSettings.BlockActivationBelowMinimum || isEnforcementDisabled);
|
||||
var canUsePlugin = !isBlocked;
|
||||
|
||||
if (!hasAudit)
|
||||
@@ -132,30 +134,32 @@ public static class PluginAssistantSecurityResolver
|
||||
HasHashMismatch = true,
|
||||
IsBelowMinimum = false,
|
||||
MeetsMinimumLevel = false,
|
||||
RequiresAudit = true,
|
||||
IsBlocked = true,
|
||||
RequiresAudit = requiresAudit,
|
||||
IsBlocked = isBlocked,
|
||||
CanOverride = false,
|
||||
CanActivatePlugin = false,
|
||||
CanStartAssistant = false,
|
||||
CanActivatePlugin = !isBlocked,
|
||||
CanStartAssistant = !isBlocked,
|
||||
AuditLabel = TB("Unknown"),
|
||||
AuditColor = AssistantAuditLevel.UNKNOWN.GetColor(),
|
||||
AuditIcon = AssistantAuditLevel.UNKNOWN.GetIcon(),
|
||||
AvailabilityLabel = GetAvailabilityLabel(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
AvailabilityColor = GetAvailabilityColor(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
StatusLabel = GetAvailabilityLabel(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
Headline = TB("This assistant is locked until it is audited again."),
|
||||
Description = TB("The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used."),
|
||||
StatusColor = GetAvailabilityColor(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
StatusIcon = GetAvailabilityIcon(requiresAudit: true, hasAudit, hasHashMismatch, isBlocked: true, canOverride: false),
|
||||
AvailabilityLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
StatusLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
BadgeIcon = GetSecurityBadgeIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
Headline = requiresAudit ? TB("This assistant is locked until it is audited again.") : TB("This assistant changed after its last audit."),
|
||||
Description = requiresAudit
|
||||
? TB("The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used.")
|
||||
: TB("The plugin code changed after the last security audit. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used."),
|
||||
StatusColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
StatusIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||
ActionLabel = TB("Run Security Check Again"),
|
||||
};
|
||||
}
|
||||
|
||||
if (isBelowMinimum)
|
||||
{
|
||||
var isBlockedByMinimum = auditSettings.BlockActivationBelowMinimum;
|
||||
var isBlockedByMinimum = enforceAuditBeforeActivation && auditSettings.BlockActivationBelowMinimum;
|
||||
var auditLevel = audit!.Level;
|
||||
|
||||
return new PluginAssistantSecurityState
|
||||
@@ -181,10 +185,16 @@ public static class PluginAssistantSecurityResolver
|
||||
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||
StatusLabel = GetAvailabilityLabel(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||
Headline = isBlockedByMinimum ? TB("This assistant is currently locked.") : TB("This assistant can still be used because your settings allow it."),
|
||||
Headline = isBlockedByMinimum
|
||||
? TB("This assistant is currently locked.")
|
||||
: isEnforcementDisabled
|
||||
? TB("This assistant can still be used because audit enforcement is disabled.")
|
||||
: TB("This assistant can still be used because your settings allow it."),
|
||||
Description = isBlockedByMinimum
|
||||
? string.Format(TB("The current audit result '{0}' is below your required minimum level '{1}'. Your security settings therefore block this assistant plugin."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName())
|
||||
: string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Your settings still allow manual activation, but the assistant keeps this security status and should be reviewed carefully."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName()),
|
||||
: isEnforcementDisabled
|
||||
? string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName())
|
||||
: string.Format(TB("The current audit result is '{0}', which is below your required minimum level '{1}'. Your settings still allow manual activation, but the assistant keeps this security status and should be reviewed carefully."), auditLevel.GetName(), auditSettings.MinimumLevel.GetName()),
|
||||
StatusColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||
StatusIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||
ActionLabel = TB("Open Security Check"),
|
||||
|
||||
@@ -497,7 +497,6 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
|
||||
private void RegisterLuaHelpers()
|
||||
{
|
||||
|
||||
this.State.Environment["LogInfo"] = new LuaFunction((context, _) =>
|
||||
{
|
||||
if (context.ArgumentCount == 0) return new(0);
|
||||
@@ -559,6 +558,15 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
var timestamp = DateTime.UtcNow.ToString("o");
|
||||
return new(context.Return(timestamp));
|
||||
});
|
||||
|
||||
this.State.Environment["InspectTable"] = new LuaFunction((context, _) =>
|
||||
{
|
||||
if (context.ArgumentCount == 0)
|
||||
return new(context.Return("{}"));
|
||||
|
||||
var table = context.GetArgument<LuaTable>(0);
|
||||
return new(context.Return(AssistantLuaConversion.InspectTable(table)));
|
||||
});
|
||||
}
|
||||
|
||||
private static void InitializeState(IEnumerable<IAssistantComponent> components, AssistantState state)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Lua;
|
||||
@@ -12,12 +13,18 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
|
||||
|
||||
private List<PluginConfigurationObject> configObjects = [];
|
||||
private List<DataMandatoryInfo> mandatoryInfos = [];
|
||||
|
||||
/// <summary>
|
||||
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
|
||||
/// </summary>
|
||||
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The list of mandatory infos provided by this configuration plugin.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
|
||||
|
||||
/// <summary>
|
||||
/// True/false when explicitly configured in the plugin, otherwise null.
|
||||
/// </summary>
|
||||
@@ -93,6 +100,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
private bool TryProcessConfiguration(bool dryRun, out string message)
|
||||
{
|
||||
this.configObjects.Clear();
|
||||
this.mandatoryInfos.Clear();
|
||||
|
||||
// Ensure that the main CONFIG table exists and is a valid Lua table:
|
||||
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
|
||||
@@ -152,6 +160,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
// 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);
|
||||
|
||||
// Handle configured mandatory infos:
|
||||
this.TryReadMandatoryInfos(mainTable);
|
||||
|
||||
// Config: preselected provider?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||
@@ -165,4 +176,25 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,10 @@ public static partial class PluginFactory
|
||||
// Check document analysis policies:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check left-over mandatory info acceptances:
|
||||
if (SETTINGS_MANAGER.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for a preselected provider:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
@@ -127,4 +128,12 @@ public static partial class PluginFactory
|
||||
|
||||
HOT_RELOAD_WATCHER.Dispose();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<DataMandatoryInfo> GetMandatoryInfos()
|
||||
{
|
||||
return RUNNING_PLUGINS
|
||||
.OfType<PluginConfiguration>()
|
||||
.SelectMany(plugin => plugin.MandatoryInfos)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
public sealed record AppExitResponse(bool Success, string ErrorMessage);
|
||||
@@ -7,7 +7,9 @@ public readonly record struct SendToButton() : IButtonData
|
||||
public Func<string> GetText { get; init; } = () => string.Empty;
|
||||
|
||||
public bool UseResultingContentBlockData { get; init; } = true;
|
||||
|
||||
public bool SendToChatAsInput { get; init; }
|
||||
|
||||
public Components Self { get; init; } = Components.NONE;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
@@ -117,4 +119,35 @@ public sealed partial class RustService
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the Rust runtime to exit the entire desktop application.
|
||||
/// </summary>
|
||||
public async Task<bool> ExitApplication()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await this.http.PostAsync("/app/exit", null);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger?.LogError("Failed to exit the app due to network error: {StatusCode}.", response.StatusCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AppExitResponse>(this.jsonRustSerializerOptions);
|
||||
if (result is null || !result.Success)
|
||||
{
|
||||
this.logger?.LogError("Failed to exit the app: {Error}", result?.ErrorMessage ?? "Unknown error");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger?.LogInformation("Exit request sent to Rust runtime.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger?.LogError(ex, "Exception while requesting application exit.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,22 @@ public sealed partial class RustService
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
||||
|
||||
if (response == default)
|
||||
{
|
||||
this.logger!.LogError("Failed to check for an update: the Rust endpoint returned an empty response.");
|
||||
return new UpdateResponse
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
NewVersion = string.Empty,
|
||||
Changelog = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
if (response.Error)
|
||||
this.logger!.LogWarning("The Rust updater reported an error while checking for updates.");
|
||||
|
||||
this.logger!.LogInformation($"Checked for an update: update available='{response.UpdateIsAvailable}'; error='{response.Error}'; next version='{response.NewVersion}'; changelog len='{response.Changelog.Length}'");
|
||||
return response;
|
||||
}
|
||||
@@ -20,6 +36,8 @@ public sealed partial class RustService
|
||||
{
|
||||
Error = true,
|
||||
UpdateIsAvailable = false,
|
||||
NewVersion = string.Empty,
|
||||
Changelog = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,16 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
private readonly SettingsManager settingsManager;
|
||||
private readonly MessageBus messageBus;
|
||||
private readonly RustService rust;
|
||||
private readonly ILogger<UpdateService> logger;
|
||||
|
||||
private TimeSpan updateInterval;
|
||||
|
||||
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust)
|
||||
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust, ILogger<UpdateService> logger)
|
||||
{
|
||||
this.settingsManager = settingsManager;
|
||||
this.messageBus = messageBus;
|
||||
this.rust = rust;
|
||||
this.logger = logger;
|
||||
|
||||
this.messageBus.RegisterComponent(this);
|
||||
this.ApplyFilters([], [ Event.USER_SEARCH_FOR_UPDATE ]);
|
||||
@@ -113,6 +115,23 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
|
||||
return;
|
||||
|
||||
var response = await this.rust.CheckForUpdate();
|
||||
if (response.Error)
|
||||
{
|
||||
this.logger.LogWarning("Update check failed. The updater did not return a usable result.");
|
||||
|
||||
if (notifyUserWhenNoUpdate)
|
||||
{
|
||||
SNACKBAR!.Add(TB("Failed to check for updates. Please try again later."), Severity.Error, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.Error;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = Color.Error;
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.UpdateIsAvailable)
|
||||
{
|
||||
// ReSharper disable RedundantAssignment
|
||||
|
||||
Reference in new issue
Block a user