Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions 3rd/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)..))"/>
<PropertyGroup>
<!-- This can filiter out the Pure.DI package from the central management, so that it can be updated independently. -->
<!-- <ManagePackageVersionsCentrally Condition="$([System.String]::Copy('$(MSBuildProjectDirectory)').Contains('Pure.DI'))">false</ManagePackageVersionsCentrally> -->
<ManagePackageVersionsCentrally Condition="'$(ManagePackageVersionsCentrally)' == ''">true</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<MicrosoftPackageVersion>10.0.9</MicrosoftPackageVersion>
<MEAIVersion>10.5.2</MEAIVersion>
<EntityFrameworkVersion>10.0.9</EntityFrameworkVersion>
Expand Down
4 changes: 4 additions & 0 deletions Everywhere.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<File Path=".github/workflows/windows-release.yml" />
</Folder>
<Folder Name="/3rd/">
<File Path="3rd/Directory.Packages.props" />
<Project Path="3rd/ClassicDiagnostics.Avalonia/src/ClassicDiagnostics.Avalonia.csproj" />
<Project Path="3rd/EverythingNetCore/EverythingNet/EverythingNet.csproj" />
<Project Path="3rd/shad-ui/src/ShadUI/ShadUI.csproj" />
Expand Down Expand Up @@ -57,6 +58,8 @@
<Project Path="src/Everywhere.I18N.SourceGenerator/Everywhere.I18N.SourceGenerator.csproj" />
</Folder>
<Folder Name="/patches/">
<File Path="patches/Directory.Build.props" />
<File Path="patches/Directory.Packages.props" />
<Project Path="patches/Everywhere.BuildTask.Patcher/Everywhere.BuildTask.Patcher.csproj" />
<Project Path="patches/Everywhere.Patches.Avalonia.Controls/Everywhere.Patches.Avalonia.Controls.csproj" />
<Project Path="patches/Everywhere.Patches.Avalonia.Native/Everywhere.Patches.Avalonia.Native.csproj" />
Expand All @@ -71,6 +74,7 @@
<File Path="src/Build.Watchdog.targets" />
</Folder>
<Folder Name="/tests/">
<File Path="tests/Directory.Packages.props" />
<Project Path="3rd/WritableJsonConfiguration/src/WritableJsonConfiguration.Tests/WritableJsonConfiguration.Tests.csproj" />
<Project Path="tests/Everywhere.Abstractions.Tests/Everywhere.Abstractions.Tests.csproj" />
<Project Path="tests/Everywhere.Core.Tests/Everywhere.Core.Tests.csproj" />
Expand Down
6 changes: 6 additions & 0 deletions patches/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)..))" />
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Build.Pure.DI.MS.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>

<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\3rd\Pure.DI\src\Pure.DI.Core\Pure.DI.Core.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" PrivateAssets="all" />
<Compile Include="$(MSBuildThisFilePath)..\..\3rd\Pure.DI\src\Pure.DI.MS\any\Pure.DI\MS\ServiceCollectionFactory.g.cs" Link="DependencyInjection\Pure.DI.MS\ServiceCollectionFactory.g.cs" />
<Compile Include="$(MSBuildThisFilePath)..\..\3rd\Pure.DI\src\Pure.DI.MS\any\Pure.DI\MS\ServiceProviderFactory.g.cs" Link="DependencyInjection\Pure.DI.MS\ServiceProviderFactory.g.cs" />
</ItemGroup>

</Project>
10 changes: 10 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)..))" />
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Pure.DI" Version="2.5.0" />
<PackageVersion Include="Pure.DI.MS" Version="2.5.0" />
</ItemGroup>
</Project>
41 changes: 41 additions & 0 deletions src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Everywhere.Database;
using Microsoft.Extensions.Logging;
using Pure.DI;
using Pure.DI.MS;
using static Pure.DI.Lifetime;

namespace Everywhere.Cloud.DependencyInjection;

public partial class CloudComposition : ServiceProviderFactory<CloudComposition>
{
// ReSharper disable once UnusedMember.Local
private static void SetupCloudServices() =>
DI.Setup()
// Pure.DI.MS hooks and framework fallbacks.
.Hint(Hint.OnCannotResolve, "On")
.Hint(Hint.OnCannotResolvePartial, "Off")
.Hint(Hint.OnNewRoot, "On")
.Hint(Hint.OnNewRootPartial, "Off")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.AspNetCore.*")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Maui.*")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*")
.Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*")

// Logging facade instances are created by Pure.DI; ILoggerFactory stays in MS DI.
.Bind<ILogger<TT>>().As(Singleton).To<Logger<TT>>()

// Cloud service implementations.
.Bind<OAuthCloudClient>().Bind<ICloudClient>().As(Singleton).To<OAuthCloudClient>()
.Bind<CloudChatDbSynchronizer>().Bind<IChatDbSynchronizer>().As(Singleton).To<CloudChatDbSynchronizer>()
.Bind<OfficialModelProvider>().Bind<IOfficialModelProvider>().As(Singleton).To<OfficialModelProvider>()

// Cloud roots exported to the final MS provider.
.Root<OAuthCloudClient>(kind: RootKinds.Exported)
.Root<ICloudClient>(kind: RootKinds.Exported)
.Root<CloudChatDbSynchronizer>(kind: RootKinds.Exported)
.Root<IChatDbSynchronizer>(kind: RootKinds.Exported)
.Root<OfficialModelProvider>(kind: RootKinds.Exported)
.Root<IOfficialModelProvider>(kind: RootKinds.Exported);
}
53 changes: 53 additions & 0 deletions src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Everywhere.Common;
using Microsoft.Extensions.DependencyInjection;

namespace Everywhere.Cloud.DependencyInjection;

public static class CloudServiceCollection
{
public static IServiceCollection Configure(IServiceCollection services)
{
// Cloud HTTP user-agent handler.
services.AddTransient<CloudUserAgentHandler>();

// Cloud API named HTTP client.
services
.AddHttpClient(
nameof(ICloudClient),
client => client.Timeout = TimeSpan.FromSeconds(30))
.ConfigurePrimaryHttpMessageHandler(sp => CreateHttpClientHandler(sp.GetRequiredService<IWebProxy>()))
.AddHttpMessageHandler(sp => sp.GetRequiredService<ICloudClient>().CreateAuthenticationHandler())
.AddHttpMessageHandler<CloudUserAgentHandler>();

return services;
}

public static IServiceCollection ConfigureAliases(IServiceCollection services)
{
// Cloud startup initializer aliases.
services.AddSingleton<IAsyncInitializer>(sp => sp.GetRequiredService<OAuthCloudClient>());
services.AddSingleton<IAsyncInitializer>(sp => sp.GetRequiredService<CloudChatDbSynchronizer>());
return services;
}

private static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy) =>
new()
{
Proxy = proxy,
UseProxy = true,
AllowAutoRedirect = true,
};

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private sealed class CloudUserAgentHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Remove("User-Agent");
request.Headers.Add("User-Agent", $"Everywhere/{App.Version}");
return base.SendAsync(request, cancellationToken);
}
}
}
19 changes: 14 additions & 5 deletions src/Everywhere.Cloud/Everywhere.Cloud.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@
<PropertyGroup>
<IsDeploymentTarget>true</IsDeploymentTarget>
<EverywhereI18NNamespace>Everywhere.Cloud.I18N</EverywhereI18NNamespace>
<NoWarn>$(NoWarn);CS0436</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\3rd\MessagePack-CSharp\src\MessagePack.SourceGenerator\MessagePack.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
<ProjectReference Include="..\..\3rd\MessagePack-CSharp\src\MessagePack\MessagePack.csproj"/>
<ProjectReference Include="..\Everywhere.Abstractions\Everywhere.Abstractions.csproj" />
<ProjectReference Include="..\Everywhere.Core\Everywhere.Core.csproj" />
<ProjectReference Include="..\Everywhere.I18N.Abstractions\Everywhere.I18N.Abstractions.csproj" />
<ProjectReference Include="..\Everywhere.I18N.SourceGenerator\Everywhere.I18N.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<ProjectReference Include="..\Everywhere.Abstractions\Everywhere.Abstractions.csproj"/>
<ProjectReference Include="..\Everywhere.Core\Everywhere.Core.csproj"/>
<ProjectReference Include="..\Everywhere.I18N.Abstractions\Everywhere.I18N.Abstractions.csproj"/>
<ProjectReference Include="..\Everywhere.I18N.SourceGenerator\Everywhere.I18N.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer"/>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Remove="I18N\Strings*.resx"/>
<AdditionalFiles Include="I18N\Strings*.resx"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Pure.DI">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pure.DI.MS"/>
</ItemGroup>

<Import Project="..\Build.Generate.Cloud.targets"/>

</Project>
4 changes: 3 additions & 1 deletion src/Everywhere.Core/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public override void Initialize()
#if DEBUG
if (Design.IsDesignMode)
{
ServiceLocator.Build(x => x.AddAvaloniaBasicServices());
var services = new ServiceCollection();
services.AddAvaloniaBasicServices();
ServiceLocator.SetProvider(services.BuildServiceProvider());
return;
}

Expand Down
149 changes: 75 additions & 74 deletions src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;

namespace Everywhere.Chat.Plugins.Mcp;
Expand Down Expand Up @@ -36,94 +37,94 @@ public static IServiceCollection AddManagedMcp(this IServiceCollection services)

return services;
}

/// <summary>
/// A delegating handler that buffers the request content to compute and set the
/// Content-Length header. This is useful for servers that do not support
/// chunked transfer encoding.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private sealed class ContentLengthBufferingHandler : DelegatingHandler
}

/// <summary>
/// A delegating handler that buffers the request content to compute and set the
/// Content-Length header. This is useful for servers that do not support
/// chunked transfer encoding.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public sealed class ContentLengthBufferingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
if (request.Content is not null)
{
if (request.Content is not null)
{
// By calling LoadIntoBufferAsync, we force the content to be buffered in memory.
// This allows the HttpContent instance to calculate its length, which then gets
// automatically set as the Content-Length header when the request is sent.
// This effectively disables chunked transfer encoding.
await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}

return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// By calling LoadIntoBufferAsync, we force the content to be buffered in memory.
// This allows the HttpContent instance to calculate its length, which then gets
// automatically set as the Content-Length header when the request is sent.
// This effectively disables chunked transfer encoding.
await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}

/// <summary>
/// A delegating handler that intercepts non-404 4xx responses from MCP servers
/// and converts them to 404 if the response body indicates a session expired error.
/// This allows the SDK's standard <c>SetSessionExpired</c> path to handle it.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private sealed class McpSessionExpiryHandler : DelegatingHandler
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// A delegating handler that intercepts non-404 4xx responses from MCP servers
/// and converts them to 404 if the response body indicates a session expired error.
/// This allows the SDK's standard <c>SetSessionExpired</c> path to handle it.
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public sealed class McpSessionExpiryHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

// Only intercept non-404 4xx responses.
if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500)
{
// Buffer the response content so we can read it and still return it if no match.
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!ContainsSessionExpiredKeyword(body)) return response;
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

var newContent = new StringContent(body);
// Only intercept non-404 4xx responses.
if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500)
{
// Buffer the response content so we can read it and still return it if no match.
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!ContainsSessionExpiredKeyword(body)) return response;

// Preserve original content headers (such as Content-Type/charset).
// Skip Content-Length because it is computed from the replacement content.
foreach (var header in response.Content.Headers)
{
if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
var newContent = new StringContent(body);

var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound)
{
RequestMessage = response.RequestMessage,
ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)",
Version = response.Version,
Content = newContent,
};

// Copy response headers.
foreach (var header in response.Headers)
// Preserve original content headers (such as Content-Type/charset).
// Skip Content-Length because it is computed from the replacement content.
foreach (var header in response.Content.Headers)
{
if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
{
newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

// Preserve trailing headers as well.
foreach (var header in response.TrailingHeaders)
{
newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value);
}
var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound)
{
RequestMessage = response.RequestMessage,
ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)",
Version = response.Version,
Content = newContent,
};

// Copy response headers.
foreach (var header in response.Headers)
{
newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
}

response.Dispose();
return newResponse;
// Preserve trailing headers as well.
foreach (var header in response.TrailingHeaders)
{
newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value);
}

return response;
response.Dispose();
return newResponse;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool ContainsSessionExpiredKeyword(string body) =>
body.Contains("session", StringComparison.OrdinalIgnoreCase) &&
(body.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
body.Contains("expires", StringComparison.OrdinalIgnoreCase) ||
body.Contains("not found", StringComparison.OrdinalIgnoreCase));
return response;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool ContainsSessionExpiredKeyword(string body) =>
body.Contains("session", StringComparison.OrdinalIgnoreCase) &&
(body.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
body.Contains("expires", StringComparison.OrdinalIgnoreCase) ||
body.Contains("not found", StringComparison.OrdinalIgnoreCase));
}
Loading
Loading