using System.Net; using System.Net.Http.Json; using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; using MAX.Framework.Commons; using MAX.Framework.Security; using MAX.Plugins; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Store.Modules.CloudProduct.Controllers; using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.Services; /// 通过真实 HTTP 路由验证图片识别的 DTO 绑定、ApiResult 包装及缓存。 internal static class HttpChecks { public static async Task RunAsync(IFreeSql database) { var userName = "http-check-" + Guid.NewGuid().ToString("N"); var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.WebHost.UseUrls("http://127.0.0.1:0"); using var context = new PluginContext("CloudProduct", "Plugins/CloudProduct", builder.Environment); context.Configuration["ProductInspection:Model"] = "test-vision"; context.Configuration["ProductInspection:ApiKey"] = "test-key"; builder.Services.AddSingleton(new PluginContext(context)); builder.Services.AddKeyedSingleton("ProductInspectionAnalysisGate", (_, _) => new SemaphoreSlim(5, 5)); builder.Services.AddKeyedSingleton("ProductInspectionConnection", database); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddAuthentication("test").AddScheme("test", _ => { }); builder.Services.AddAuthorization(); builder.Services.AddControllers(options => { options.Conventions.Add(new MAX.Framework.Conventions.ApiRoutePrefixConvention()); options.Filters.Add(); options.Filters.Add(); }).AddApplicationPart(typeof(ProductInspectionController).Assembly); await using var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapPost("/test/chat", RecognitionEndpoint.RespondAsync); await app.StartAsync(); var address = app.Services.GetRequiredService().Features.Get()!.Addresses.Single(); context.Configuration["ProductInspection:Endpoint"] = address + "/test/chat"; using var client = new HttpClient { BaseAddress = new Uri(address + "/api/ProductInspection/") }; var checks = 0; void Check(bool condition, string message) { if (!condition) throw new Exception(message); checks++; } // 随机化图片内容及登录身份,清理时只删除本次检查创建的行。 var data = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jZQAAAABJRU5ErkJggg==") .Concat(Guid.NewGuid().ToByteArray()).ToArray(); var image = Convert.ToBase64String(data); var key = Guid.NewGuid().ToString("N"); async Task Inspect(string image, string? md5) { using var response = await client.PostAsJsonAsync("InspectImage", new { userName, md5, image }); using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); return doc.RootElement.Clone(); } try { client.DefaultRequestHeaders.Add("X-Test-User", userName); var first = await Inspect(image, key.ToUpperInvariant()); Check(first.TryGetProperty("code", out var firstCode) && firstCode.GetInt32() == 200, first.ToString()); string[] expected = ["急速发货", "螺旋伸缩", "金属挂钩", "可调节", "不挡扇叶", "倒挂钩卡口"]; Check(first.GetProperty("data").Deserialize()!.SequenceEqual(expected), "Ordered recognition array"); Check((await Inspect(image, key)).GetProperty("data").Deserialize()!.SequenceEqual(expected), "Cache hit preserves item order"); var stored = await database.Select().Where(x => x.CacheKey == key).FirstAsync(); Check(stored.RecognitionResult == """["急速发货","螺旋伸缩","金属挂钩","可调节","不挡扇叶","倒挂钩卡口"]""", "Cache stores distinct items with readable Chinese"); await database.Update().Set(x => x.RecognitionResult, "[]").Where(x => x.CacheKey == key).ExecuteAffrowsAsync(); var otherImage = Convert.ToBase64String(data.Append((byte)42).ToArray()); var hit = (await Inspect(otherImage, key)).GetProperty("data"); Check(hit.GetArrayLength() == 0, "Same client key reuses cached result for different image bytes"); await database.Update().Set(x => x.RecognitionResult, """{"text":"legacy","logoBrands":[]}""") .Where(x => x.CacheKey == key).ExecuteAffrowsAsync(); Check((await Inspect(image, key)).GetProperty("data").Deserialize()!.SequenceEqual(expected), "Refresh legacy cache"); Check((await Inspect(Convert.ToBase64String(new byte[30]), key)).GetProperty("code").GetInt32() != 200, "Invalid image denied"); Check((await Inspect("invalid!", key)).GetProperty("code").GetInt32() != 200, "Malformed Base64 denied before cache hit"); foreach (var invalid in new[] { "invalid", new string('z', 32), null }) { var rejected = await Inspect(otherImage, invalid); Check(!rejected.TryGetProperty("code", out var code) || code.GetInt32() != 200, "Reject missing or malformed MD5 before cache hit"); } Console.WriteLine($"PASS {checks} HTTP and MySQL checks"); } finally { await app.StopAsync(); await database.Delete().Where(x => x.CacheKey == key).ExecuteAffrowsAsync(); } } } /// 仅测试宿主使用的身份处理器,生产插件始终沿用宿主 Bearer 认证。 internal sealed class TestAuthentication(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { protected override Task HandleAuthenticateAsync() { var name = Request.Headers["X-Test-User"].ToString(); if (string.IsNullOrEmpty(name)) return Task.FromResult(AuthenticateResult.NoResult()); var principal = new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.UserName, name)], Scheme.Name)); return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name))); } } /// 返回模型协议示例,使 HTTP 检查无需外部密钥。 internal static class RecognitionEndpoint { private const string Content = """["\u6025\u901F\u53D1\u8D27","急速发货","螺旋伸缩","急速发货","金属挂钩","可调节","不挡扇叶","倒挂钩卡口"]"""; internal static async Task RespondAsync(HttpRequest request, CancellationToken cancellationToken) { using var body = await JsonDocument.ParseAsync(request.Body, cancellationToken: cancellationToken); var root = body.RootElement; var parts = root.GetProperty("messages")[0].GetProperty("content"); if (root.GetProperty("model").GetString() != "test-vision" || request.Headers.Authorization.ToString() != "Bearer test-key" || root.GetProperty("stream").GetBoolean() || root.GetProperty("enable_thinking").GetBoolean() || !parts[0].GetProperty("image_url").GetProperty("url").GetString()!.StartsWith("data:image/png;base64,iVBOR") || !parts[1].GetProperty("text").GetString()!.Contains("JSON 字符串数组")) throw new Exception("Invalid model request"); return Results.Json(new { choices = new[] { new { message = new { content = Content } } } }); } }