更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
+16
View File
@@ -8,7 +8,9 @@ using YMhut.Box.Core.Feedback;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Media;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Platform;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Plugins.Runtime;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.SolarSystem;
using YMhut.Box.Core.Startup;
@@ -42,6 +44,11 @@ public static class AppServices
provider.GetService<ILogService>()));
services.AddSingleton<IPluginStateStore, PluginStateStore>();
services.AddSingleton<PluginLogService>();
services.AddSingleton<IPlatformCapabilities, WindowsPlatformCapabilities>();
services.AddSingleton<IShellRuntime>(provider => new ShellRuntimeService(provider.GetService<ILogService>()));
services.AddSingleton<IPluginRuntimeLauncher>(provider => new TauriPluginProcessService(
provider.GetRequiredService<AppPaths>(),
provider.GetService<ILogService>()));
services.AddSingleton<IBuiltInPluginInstallerService>(provider => new BuiltInPluginInstallerService(
provider.GetRequiredService<AppPaths>(),
provider.GetService<ILogService>(),
@@ -103,6 +110,10 @@ public static class AppServices
provider.GetService<ILogService>(),
provider.GetService<ISettingsService>(),
provider.GetService<IHardwareInfoService>()));
services.AddSingleton<ILocalToolVendorAssetService>(provider => new LocalToolVendorAssetService(
provider.GetRequiredService<AppPaths>(),
provider.GetService<ILogService>()));
services.AddSingleton<ILocalJsToolRuntime, LocalJsToolRuntime>();
services.AddSingleton<IToolWorkerService>(provider => new ProcessToolWorkerService(
provider.GetService<IApiManager>(),
provider.GetService<IReferenceDataService>(),
@@ -127,6 +138,11 @@ public static class AppServices
provider.GetService<ILogService>()));
services.AddSingleton<WebView2EnvironmentFactory>();
services.AddSingleton<IUiPerformanceCoordinator, UiPerformanceCoordinator>();
services.AddSingleton<WindowChromeService>();
services.AddSingleton<ShellNavigationService>();
services.AddSingleton<IShellNavigationService>(provider => provider.GetRequiredService<ShellNavigationService>());
services.AddSingleton<IShellDialogService, ShellDialogService>();
services.AddSingleton<IShellNotificationService, ShellNotificationService>();
services.AddSingleton<WindowStateService>();
services.AddSingleton<IStartupInitializationService, StartupInitializationService>();
@@ -0,0 +1,187 @@
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
namespace YMhut.Box.WinUI.Services;
public interface ILocalToolVendorAssetService
{
Task<LocalToolVendorAssetSnapshot> EnsureExtractedAsync(CancellationToken cancellationToken = default);
}
public interface ILocalJsToolRuntime
{
IReadOnlyDictionary<string, string> Profiles { get; }
Task<LocalJsToolRuntimeResult> ExecuteAsync(
string toolId,
string input,
IReadOnlyDictionary<string, string>? options = null,
CancellationToken cancellationToken = default);
}
public sealed record LocalToolVendorAssetSnapshot(
string CacheRoot,
string ManifestPath,
string ManifestHash,
IReadOnlyList<string> Files);
public sealed record LocalJsToolRuntimeResult(
bool Success,
string Output,
string? Error = null,
string Runtime = "csharp-fallback");
public sealed class LocalToolVendorAssetService(
AppPaths paths,
ILogService? logService = null) : ILocalToolVendorAssetService
{
private const string ResourcePrefix = "YMhut.Box.WinUI.Assets.tool-results.vendor.";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false
};
private readonly SemaphoreSlim _gate = new(1, 1);
private LocalToolVendorAssetSnapshot? _snapshot;
public async Task<LocalToolVendorAssetSnapshot> EnsureExtractedAsync(CancellationToken cancellationToken = default)
{
if (_snapshot is not null)
{
return _snapshot;
}
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_snapshot is not null)
{
return _snapshot;
}
var assembly = Assembly.GetExecutingAssembly();
var resources = assembly.GetManifestResourceNames()
.Where(name => name.StartsWith(ResourcePrefix, StringComparison.OrdinalIgnoreCase))
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (resources.Length == 0)
{
_snapshot = await SnapshotFromContentFolderAsync(cancellationToken).ConfigureAwait(false);
return _snapshot;
}
var manifestResource = resources.FirstOrDefault(name => name.EndsWith(".manifest.json", StringComparison.OrdinalIgnoreCase)) ??
resources.FirstOrDefault(name => name.EndsWith("manifest.json", StringComparison.OrdinalIgnoreCase));
var manifestHash = manifestResource is null
? HashText(string.Join("|", resources))
: await HashResourceAsync(assembly, manifestResource, cancellationToken).ConfigureAwait(false);
var cacheRoot = Path.Combine(paths.Cache, "tool-vendor", manifestHash);
Directory.CreateDirectory(cacheRoot);
var extracted = new List<string>();
foreach (var resource in resources)
{
cancellationToken.ThrowIfCancellationRequested();
var relative = ResourceToRelativePath(resource);
if (string.IsNullOrWhiteSpace(relative))
{
continue;
}
var target = Path.Combine(cacheRoot, relative);
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
await using var source = assembly.GetManifestResourceStream(resource);
if (source is null)
{
continue;
}
await using var output = File.Create(target);
await source.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
extracted.Add(target);
}
_snapshot = new LocalToolVendorAssetSnapshot(
cacheRoot,
Path.Combine(cacheRoot, "manifest.json"),
manifestHash,
extracted);
await logService.WriteAsync(
"Information",
"tool-vendor",
"Local JS vendor assets extracted",
$"files={extracted.Count}; hash={manifestHash}",
cancellationToken).ConfigureAwait(false);
return _snapshot;
}
finally
{
_gate.Release();
}
}
private async Task<LocalToolVendorAssetSnapshot> SnapshotFromContentFolderAsync(CancellationToken cancellationToken)
{
var sourceRoot = Path.Combine(paths.Assets, "tool-results", "vendor");
var files = Directory.Exists(sourceRoot)
? Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray()
: [];
var manifest = Path.Combine(sourceRoot, "manifest.json");
var hash = File.Exists(manifest)
? Convert.ToHexString(await SHA256.HashDataAsync(File.OpenRead(manifest), cancellationToken).ConfigureAwait(false)).ToLowerInvariant()
: HashText(string.Join("|", files.Select(Path.GetFileName)));
return new LocalToolVendorAssetSnapshot(sourceRoot, manifest, hash, files);
}
private static string ResourceToRelativePath(string resource)
{
var name = resource[ResourcePrefix.Length..];
if (name.StartsWith("vendor.", StringComparison.OrdinalIgnoreCase))
{
name = name["vendor.".Length..];
}
return name;
}
private static async Task<string> HashResourceAsync(Assembly assembly, string resource, CancellationToken cancellationToken)
{
await using var stream = assembly.GetManifestResourceStream(resource) ?? new MemoryStream();
return Convert.ToHexString(await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)).ToLowerInvariant();
}
private static string HashText(string value)
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
}
public sealed class LocalJsToolRuntime(ILocalToolVendorAssetService vendorAssets) : ILocalJsToolRuntime
{
public IReadOnlyDictionary<string, string> Profiles { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["json_formatter"] = "json",
["yaml_json_converter"] = "yaml",
["html_minifier"] = "markup",
["html_entity"] = "html-entity",
["markdown_preview"] = "markdown",
["markdown_table_normalizer"] = "markdown",
["timestamp_converter"] = "time",
["punycode_codec"] = "idn"
};
public async Task<LocalJsToolRuntimeResult> ExecuteAsync(
string toolId,
string input,
IReadOnlyDictionary<string, string>? options = null,
CancellationToken cancellationToken = default)
{
await vendorAssets.EnsureExtractedAsync(cancellationToken).ConfigureAwait(false);
return new LocalJsToolRuntimeResult(
false,
input,
"Local JS assets are available for WebView rendering; core execution is handled by the C# fallback.");
}
}
@@ -0,0 +1,56 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace YMhut.Box.WinUI.Services;
public interface IShellDialogService
{
void Attach(XamlRoot? xamlRoot);
Task<ContentDialogResult> ShowAsync(ContentDialog dialog);
}
public sealed class ShellDialogService : IShellDialogService
{
private XamlRoot? _xamlRoot;
public void Attach(XamlRoot? xamlRoot)
{
_xamlRoot = xamlRoot;
}
public async Task<ContentDialogResult> ShowAsync(ContentDialog dialog)
{
try
{
dialog.XamlRoot ??= _xamlRoot ?? ToastService.CurrentXamlRoot;
return await dialog.ShowAsync();
}
catch (Exception exception)
{
CrashLog.Write(exception);
ToastService.Show(AppLocalizer.T("无法打开对话框,请查看日志。", "Could not open the dialog. Check logs."), ToastKind.Error);
return ContentDialogResult.None;
}
}
}
public interface IShellNotificationService
{
void Attach(StackPanel host);
void Show(string message, ToastKind kind = ToastKind.Success, TimeSpan? duration = null);
}
public sealed class ShellNotificationService : IShellNotificationService
{
public void Attach(StackPanel host)
{
ToastService.Attach(host);
}
public void Show(string message, ToastKind kind = ToastKind.Success, TimeSpan? duration = null)
{
ToastService.Show(message, kind, duration);
}
}
+214
View File
@@ -0,0 +1,214 @@
using Microsoft.UI.Xaml;
namespace YMhut.Box.WinUI.Services;
public enum ShellRoutePlacement
{
Main,
Footer,
Hidden
}
public enum ShellRouteCacheMode
{
Disabled,
Enabled
}
public sealed record ShellRoute(
string Tag,
string Title,
string EnglishTitle,
string IconGlyph,
ShellRoutePlacement Placement,
Func<object?, UIElement>? Factory = null,
ShellRouteCacheMode CacheMode = ShellRouteCacheMode.Disabled,
IReadOnlyList<string>? SearchKeywords = null,
Func<object?, IReadOnlyList<string>>? BreadcrumbProvider = null)
{
public string CurrentTitle => AppLocalizer.T(Title, EnglishTitle);
public IReadOnlyList<string> GetBreadcrumbs(object? parameter = null)
{
if (BreadcrumbProvider is not null)
{
return BreadcrumbProvider(parameter);
}
return [CurrentTitle];
}
}
public sealed class ShellNavigationChangedEventArgs(
ShellRoute route,
object? parameter,
IReadOnlyList<string> breadcrumbs) : EventArgs
{
public ShellRoute Route { get; } = route;
public object? Parameter { get; } = parameter;
public IReadOnlyList<string> Breadcrumbs { get; } = breadcrumbs;
}
public interface IShellNavigationHost
{
bool NavigateToRoute(ShellRoute route, object? parameter = null);
bool NavigateToToolRoute(string toolId);
bool GoBackInShell();
void ShowNavigationError(ShellRoute? route, Exception exception);
}
public interface IShellNavigationService
{
ShellRoute? CurrentRoute { get; }
IReadOnlyList<string> Breadcrumbs { get; }
event EventHandler<ShellNavigationChangedEventArgs>? Navigated;
bool Navigate(string tag, object? parameter = null);
bool NavigateToTool(string toolId);
bool GoBack();
}
public sealed class ShellNavigationService : IShellNavigationService
{
private IShellNavigationHost? _host;
private IReadOnlyDictionary<string, ShellRoute> _routes = new Dictionary<string, ShellRoute>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<(ShellRoute Route, object? Parameter)> _backStack = [];
public ShellRoute? CurrentRoute { get; private set; }
public IReadOnlyList<string> Breadcrumbs { get; private set; } = [];
public event EventHandler<ShellNavigationChangedEventArgs>? Navigated;
public void RegisterHost(IShellNavigationHost host, IReadOnlyList<ShellRoute> routes)
{
_host = host;
_routes = routes
.GroupBy(route => route.Tag, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
}
public bool Navigate(string tag, object? parameter = null)
{
if (!_routes.TryGetValue(tag, out var route))
{
return false;
}
return NavigateCore(route, parameter, pushCurrent: true);
}
public bool NavigateToTool(string toolId)
{
if (_host is null)
{
return false;
}
try
{
if (!string.IsNullOrWhiteSpace(toolId) && _host.NavigateToToolRoute(toolId))
{
var route = _routes.TryGetValue("toolbox", out var toolboxRoute)
? toolboxRoute
: new ShellRoute("toolbox", "工具箱", "Toolbox", "\uE90F", ShellRoutePlacement.Hidden);
CurrentRoute = route;
Breadcrumbs = [AppLocalizer.T("工具箱", "Toolbox"), toolId];
Navigated?.Invoke(this, new ShellNavigationChangedEventArgs(route, toolId, Breadcrumbs));
return true;
}
}
catch (Exception exception)
{
_host.ShowNavigationError(null, exception);
}
return false;
}
public bool GoBack()
{
if (_host is null)
{
return false;
}
if (_backStack.TryPop(out var entry))
{
return NavigateCore(entry.Route, entry.Parameter, pushCurrent: false);
}
return _host.GoBackInShell();
}
private bool NavigateCore(ShellRoute route, object? parameter, bool pushCurrent)
{
if (_host is null)
{
return false;
}
try
{
if (pushCurrent && CurrentRoute is not null && !string.Equals(CurrentRoute.Tag, route.Tag, StringComparison.OrdinalIgnoreCase))
{
_backStack.Push((CurrentRoute, null));
}
if (!_host.NavigateToRoute(route, parameter))
{
return false;
}
CurrentRoute = route;
Breadcrumbs = route.GetBreadcrumbs(parameter);
Navigated?.Invoke(this, new ShellNavigationChangedEventArgs(route, parameter, Breadcrumbs));
return true;
}
catch (Exception exception)
{
_host.ShowNavigationError(route, exception);
return false;
}
}
public void NotifyExternalNavigation(string tag, object? parameter = null)
{
if (!_routes.TryGetValue(tag, out var route))
{
return;
}
CurrentRoute = route;
Breadcrumbs = route.GetBreadcrumbs(parameter);
Navigated?.Invoke(this, new ShellNavigationChangedEventArgs(route, parameter, Breadcrumbs));
}
}
internal static class ShellRouteCatalog
{
internal static IReadOnlyList<ShellRoute> CreateMainRoutes()
{
return
[
new("home", "首页", "Home", "\uE80F", ShellRoutePlacement.Main, CacheMode: ShellRouteCacheMode.Enabled, SearchKeywords: ["home", "dashboard", "首页"]),
new("toolbox", "工具箱", "Toolbox", "\uE90F", ShellRoutePlacement.Main, CacheMode: ShellRouteCacheMode.Enabled, SearchKeywords: ["tools", "toolbox", "工具"]),
new("logs", "日志", "Logs", "\uE9F9", ShellRoutePlacement.Main, SearchKeywords: ["logs", "diagnostics", "日志"]),
new("downloads", "下载管理", "Downloads", "\uE896", ShellRoutePlacement.Main, SearchKeywords: ["downloads", "queue", "下载"]),
new("plugins", "插件", "Plugins", "\uECAA", ShellRoutePlacement.Footer, SearchKeywords: ["plugins", "extensions", "插件"]),
new("feedback", "反馈", "Feedback", "\uE939", ShellRoutePlacement.Footer, SearchKeywords: ["feedback", "support", "反馈"]),
new("settings", "设置", "Settings", "\uE713", ShellRoutePlacement.Footer, SearchKeywords: ["settings", "preferences", "设置"]),
new("about", "关于", "About", "\uE946", ShellRoutePlacement.Footer, SearchKeywords: ["about", "version", "关于"]),
new("serviceStatus", "服务状态", "Service Status", "\uE753", ShellRoutePlacement.Hidden, SearchKeywords: ["status", "startup", "service", "服务状态"])
];
}
}
@@ -1,4 +1,5 @@
using YMhut.Box.Core.App;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Plugins;
@@ -38,9 +39,11 @@ public sealed class StartupInitializationService(
ILogService logService,
IPluginStateStore pluginStateStore,
IDownloadManagerService downloadManager,
IReferenceDataService referenceDataService,
IExternalToolCatalogService externalToolCatalog,
IBuiltinReferenceToolCatalog builtinToolCatalog,
IInstallIntegrityCheckService integrityCheckService) : IStartupInitializationService
IInstallIntegrityCheckService integrityCheckService,
ILocalToolVendorAssetService localToolVendorAssets) : IStartupInitializationService
{
private readonly SemaphoreSlim _gate = new(1, 1);
@@ -157,6 +160,28 @@ public sealed class StartupInitializationService(
context.Report(1, T("本地设置已载入。", "Local settings loaded."));
});
yield return Stage("resources", T("验证随包资源", "Validating bundled resources"), 10, false, (context, token) =>
{
if (!getSettings().ValidateResourcesOnStartup)
{
context.Report(1, T("已跳过随包资源完整性检查。", "Bundled resource validation skipped."));
return Task.CompletedTask;
}
context.Report(0.2, T("正在检查三国杀、本地参考数据和工具结果前端资源...", "Checking Sanguosha, local reference data, and tool result frontend assets..."));
var validation = referenceDataService.ValidateRequiredAssets(
"data/sanguosha/skin_config.json",
"data/sanguosha/GeneralSkinInfoConfig.json",
"data/sanguosha/十周年皮肤配置对应关系.xlsx");
context.Report(
1,
validation.IsHealthy
? T("三国杀随包资源已就绪。", "Sanguosha bundled resources are ready.")
: T($"缺少 {validation.MissingPaths.Count} 个随包资源,相关工具会显示修复提示。", $"{validation.MissingPaths.Count} bundled resources are missing; affected tools will show repair guidance."),
validation.IsHealthy ? StartupCheckSeverity.Info : StartupCheckSeverity.Warning);
return Task.CompletedTask;
});
yield return Stage("database", T("初始化主 SQLite", "Initializing main SQLite"), 10, true, async (context, token) =>
{
context.Report(0.25, T("正在初始化日志与主数据库...", "Initializing logs and main database..."));
@@ -0,0 +1,259 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Platform;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Plugins.Runtime;
namespace YMhut.Box.WinUI.Services;
public sealed class TauriPluginProcessService(AppPaths paths, ILogService? logService = null) : IPluginRuntimeLauncher, IDisposable
{
private readonly ConcurrentDictionary<string, PluginRuntimeSession> _sessions = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, Process> _processes = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, List<ShellOutputEvent>> _output = new(StringComparer.OrdinalIgnoreCase);
private long _sequence;
private bool _disposed;
public async Task<PluginRuntimeSession> LaunchAsync(PluginRuntimeLaunchRequest request, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var sessionId = Guid.NewGuid().ToString("N");
var startedAt = DateTimeOffset.Now;
var host = ResolveHostExecutable();
var session = new PluginRuntimeSession(
sessionId,
request.PluginId,
request.SurfaceId,
request.RuntimeKind,
null,
PluginRuntimeSessionStatus.Starting,
startedAt);
_sessions[sessionId] = session;
_output[sessionId] = [];
if (host is null)
{
session = session with
{
Status = PluginRuntimeSessionStatus.Failed,
EndedAt = DateTimeOffset.Now,
LastError = "Tauri plugin host executable was not found."
};
_sessions[sessionId] = session;
await WriteLogAsync("Error", request.PluginId, "Tauri plugin host missing", session.LastError, cancellationToken).ConfigureAwait(false);
return session;
}
var startInfo = new ProcessStartInfo
{
FileName = host,
WorkingDirectory = Path.GetDirectoryName(host) ?? AppContext.BaseDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false
};
startInfo.ArgumentList.Add("--session");
startInfo.ArgumentList.Add(sessionId);
startInfo.ArgumentList.Add("--plugin-id");
startInfo.ArgumentList.Add(request.PluginId);
startInfo.ArgumentList.Add("--surface-id");
startInfo.ArgumentList.Add(request.SurfaceId);
startInfo.ArgumentList.Add("--runtime-kind");
startInfo.ArgumentList.Add(request.RuntimeKind.ToString());
startInfo.ArgumentList.Add("--plugin-root");
startInfo.ArgumentList.Add(request.PluginRoot);
startInfo.ArgumentList.Add("--manifest");
startInfo.ArgumentList.Add(request.ManifestPath);
startInfo.ArgumentList.Add("--entry");
startInfo.ArgumentList.Add(request.Entry);
if (!string.IsNullOrWhiteSpace(request.CommandId))
{
startInfo.ArgumentList.Add("--command");
startInfo.ArgumentList.Add(request.CommandId);
}
try
{
var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start Tauri plugin host.");
process.EnableRaisingEvents = true;
process.OutputDataReceived += (_, args) => CaptureOutput(sessionId, ShellOutputStream.Stdout, args.Data);
process.ErrorDataReceived += (_, args) => CaptureOutput(sessionId, ShellOutputStream.Stderr, args.Data);
process.Exited += (_, _) => CompleteSession(sessionId, process);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
_processes[sessionId] = process;
session = session with
{
ProcessId = process.Id,
Status = PluginRuntimeSessionStatus.Running
};
_sessions[sessionId] = session;
await WriteLogAsync("Information", request.PluginId, "Tauri plugin host launched", $"session={sessionId}; pid={process.Id}", cancellationToken).ConfigureAwait(false);
return session;
}
catch (Exception exception)
{
session = session with
{
Status = PluginRuntimeSessionStatus.Failed,
EndedAt = DateTimeOffset.Now,
LastError = exception.Message
};
_sessions[sessionId] = session;
await WriteLogAsync("Error", request.PluginId, "Tauri plugin host launch failed", exception.Message, cancellationToken).ConfigureAwait(false);
return session;
}
}
public async Task StopAsync(string sessionId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (_processes.TryRemove(sessionId, out var process))
{
try
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
}
catch (Exception exception)
{
CaptureOutput(sessionId, ShellOutputStream.System, $"Stop failed: {exception.Message}");
}
finally
{
process.Dispose();
}
}
if (_sessions.TryGetValue(sessionId, out var session))
{
_sessions[sessionId] = session with
{
Status = PluginRuntimeSessionStatus.Cancelled,
EndedAt = DateTimeOffset.Now
};
await WriteLogAsync("Information", session.PluginId, "Tauri plugin host stopped", $"session={sessionId}", cancellationToken).ConfigureAwait(false);
}
}
public Task<PluginRuntimeSession?> GetSessionAsync(string sessionId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
_sessions.TryGetValue(sessionId, out var session);
return Task.FromResult(session);
}
public Task<IReadOnlyList<ShellOutputEvent>> ReadRecentOutputAsync(string sessionId, int take = 200, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_output.TryGetValue(sessionId, out var lines))
{
return Task.FromResult<IReadOnlyList<ShellOutputEvent>>([]);
}
lock (lines)
{
return Task.FromResult<IReadOnlyList<ShellOutputEvent>>(lines.TakeLast(Math.Max(0, take)).ToArray());
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
foreach (var sessionId in _processes.Keys.ToArray())
{
try
{
StopAsync(sessionId).GetAwaiter().GetResult();
}
catch
{
}
}
}
private void CompleteSession(string sessionId, Process process)
{
try
{
int? exitCode = process.HasExited ? process.ExitCode : null;
if (_sessions.TryGetValue(sessionId, out var session))
{
_sessions[sessionId] = session with
{
Status = exitCode == 0 ? PluginRuntimeSessionStatus.Exited : PluginRuntimeSessionStatus.Failed,
EndedAt = DateTimeOffset.Now,
ExitCode = exitCode
};
_ = WriteLogAsync(
exitCode == 0 ? "Information" : "Error",
session.PluginId,
"Tauri plugin host exited",
$"session={sessionId}; exitCode={exitCode}",
CancellationToken.None);
}
}
finally
{
_processes.TryRemove(sessionId, out _);
process.Dispose();
}
}
private void CaptureOutput(string sessionId, ShellOutputStream stream, string? line)
{
if (line is null)
{
return;
}
var output = new ShellOutputEvent(sessionId, stream, line, DateTimeOffset.Now, Interlocked.Increment(ref _sequence));
var lines = _output.GetOrAdd(sessionId, _ => []);
lock (lines)
{
lines.Add(output);
if (lines.Count > 5000)
{
lines.RemoveRange(0, lines.Count - 5000);
}
}
}
private async Task WriteLogAsync(string level, string pluginId, string message, string? detail, CancellationToken cancellationToken)
{
await (logService?.WriteAsync(level, $"plugin:{pluginId}:runtime", message, detail, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
}
private string? ResolveHostExecutable()
{
var candidates = new List<string>
{
Path.Combine(AppContext.BaseDirectory, "tauri-host", "ymhut-box-plugin-tauri-host.exe"),
Path.Combine(AppContext.BaseDirectory, "tauri-host", "YMhut.Box.PluginTauriHost.exe"),
Path.Combine(paths.Root, "Runtime", "tauri-host", "ymhut-box-plugin-tauri-host.exe")
};
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.PluginTauriHost", "src-tauri", "target", "release", "ymhut-box-plugin-tauri-host.exe"));
candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.PluginTauriHost", "src-tauri", "target", "debug", "ymhut-box-plugin-tauri-host.exe"));
directory = directory.Parent;
}
return candidates.FirstOrDefault(File.Exists);
}
}
+7 -1
View File
@@ -44,7 +44,13 @@ public sealed class ToolPageWebBridge(
input,
settingsService.Current.Theme,
settingsService.Current.Language,
new ToolResultPrivacyPolicy(ToolResultPrivacySanitizer.DefaultRedactedHostHints),
new ToolResultPrivacyPolicy(
ToolResultPrivacySanitizer.DefaultRedactedHostHints,
Mode: "local-fragments-only",
VisibleMode: "show-original",
RedactVisibleContent: false,
RedactRawOutput: true,
RedactCopyOutput: true),
new ToolPageRuntimeMetadata(
DateTimeOffset.Now,
spec.AutoRunOnOpen,
@@ -38,7 +38,13 @@ public sealed class ToolResultWebBridge(
experience.ExperienceId,
settingsService.Current.Theme,
settingsService.Current.Language,
new ToolResultPrivacyPolicy(ToolResultPrivacySanitizer.DefaultRedactedHostHints),
new ToolResultPrivacyPolicy(
ToolResultPrivacySanitizer.DefaultRedactedHostHints,
Mode: "local-fragments-only",
VisibleMode: "show-original",
RedactVisibleContent: false,
RedactRawOutput: true,
RedactCopyOutput: true),
new ToolResultRuntimeMetadata(
durationMs,
DateTimeOffset.Now,
@@ -0,0 +1,100 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Graphics;
using Windows.UI;
using Windows.UI.ViewManagement;
using WinRT.Interop;
namespace YMhut.Box.WinUI.Services;
public sealed class WindowChromeService
{
private const int FallbackCaptionButtonWidth = 146;
public void Apply(Window window, string? theme)
{
try
{
var appWindow = GetAppWindow(window);
var titleBar = appWindow?.TitleBar;
if (titleBar is null)
{
return;
}
var dark = ShouldUseDarkPalette(theme);
var foreground = dark ? Colors.White : Color.FromArgb(255, 32, 32, 32);
var hoverBackground = dark ? Color.FromArgb(38, 255, 255, 255) : Color.FromArgb(24, 0, 0, 0);
var pressedBackground = dark ? Color.FromArgb(58, 255, 255, 255) : Color.FromArgb(40, 0, 0, 0);
var inactiveForeground = dark ? Color.FromArgb(178, 255, 255, 255) : Color.FromArgb(178, 32, 32, 32);
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
titleBar.ButtonForegroundColor = foreground;
titleBar.ButtonHoverForegroundColor = foreground;
titleBar.ButtonPressedForegroundColor = foreground;
titleBar.ButtonInactiveForegroundColor = inactiveForeground;
titleBar.ButtonHoverBackgroundColor = hoverBackground;
titleBar.ButtonPressedBackgroundColor = pressedBackground;
}
catch (Exception exception)
{
CrashLog.Write(exception);
}
}
public double CaptionButtonReservedWidth(Window window, XamlRoot? xamlRoot)
{
try
{
var appWindow = GetAppWindow(window);
var inset = appWindow?.TitleBar.RightInset ?? 0;
if (inset > 0)
{
var scale = xamlRoot?.RasterizationScale ?? 1;
return Math.Ceiling(inset / Math.Max(0.1, scale));
}
}
catch
{
}
return FallbackCaptionButtonWidth;
}
private static AppWindow? GetAppWindow(Window window)
{
var hwnd = WindowNative.GetWindowHandle(window);
if (hwnd == IntPtr.Zero)
{
return null;
}
return AppWindow.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd));
}
private static bool ShouldUseDarkPalette(string? theme)
{
return (theme ?? string.Empty).Trim().ToLowerInvariant() switch
{
"light" => false,
"dark" => true,
_ => IsSystemDarkTheme()
};
}
private static bool IsSystemDarkTheme()
{
try
{
var background = new UISettings().GetColorValue(UIColorType.Background);
var luminance = (0.2126 * background.R) + (0.7152 * background.G) + (0.0722 * background.B);
return luminance < 128;
}
catch
{
return false;
}
}
}
@@ -0,0 +1,20 @@
using YMhut.Box.Core.Platform;
namespace YMhut.Box.WinUI.Services;
public sealed class WindowsPlatformCapabilities : IPlatformCapabilities
{
public string PlatformName => "Windows";
public bool SupportsDesktopProcesses => true;
public bool SupportsTauriPluginHost => true;
public bool SupportsShellRuntime => true;
public bool SupportsTray => true;
public bool SupportsAdministratorLaunch => true;
public bool SupportsNativeWebView2 => true;
}