From d857cda0714a0a538e438998aefcda7c23a02cff Mon Sep 17 00:00:00 2001
From: "zhaowei.huang" <228127304@qq.com>
Date: Tue, 9 Jun 2026 09:36:25 +0800
Subject: [PATCH] =?UTF-8?q?docs(swagger):=20=E5=90=88=E5=B9=B6=E5=B1=95?=
=?UTF-8?q?=E7=A4=BA=20SimpleLite=20=E6=8A=95=E5=BD=B1=20WebApi=20?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
通过 SimpleLiteOpenApiDocumentFilter 将 SimpleLite OpenAPI 描述注入 Swagger,开发环境可在 /swagger 统一浏览 /api/sl/projection 全部端点。
---
MiGu.Server/MiGu.Server.csproj | 2 +
.../SimpleLiteOpenApiDocumentFilter.cs | 183 ++++++++++++++++++
MiGu.Server/Program.cs | 9 +-
3 files changed, 193 insertions(+), 1 deletion(-)
create mode 100644 MiGu.Server/OpenApi/SimpleLiteOpenApiDocumentFilter.cs
diff --git a/MiGu.Server/MiGu.Server.csproj b/MiGu.Server/MiGu.Server.csproj
index 3e818bc..8bde202 100644
--- a/MiGu.Server/MiGu.Server.csproj
+++ b/MiGu.Server/MiGu.Server.csproj
@@ -20,6 +20,8 @@
+
+
diff --git a/MiGu.Server/OpenApi/SimpleLiteOpenApiDocumentFilter.cs b/MiGu.Server/OpenApi/SimpleLiteOpenApiDocumentFilter.cs
new file mode 100644
index 0000000..07d1468
--- /dev/null
+++ b/MiGu.Server/OpenApi/SimpleLiteOpenApiDocumentFilter.cs
@@ -0,0 +1,183 @@
+using System.Text.Json;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace MiGu.Server.OpenApi;
+
+///
+/// 将 SimpleLite EmbedIO WebApi 的 OpenAPI 描述合并进 MiGu.Server Swagger 文档。
+/// 源文件:Simple/SimpleLite/Docs/openapi/simplelite-projection.json
+///
+public sealed class SimpleLiteOpenApiDocumentFilter : IDocumentFilter
+{
+ private static readonly Dictionary VerbMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["get"] = OperationType.Get,
+ ["post"] = OperationType.Post,
+ ["put"] = OperationType.Put,
+ ["patch"] = OperationType.Patch,
+ ["delete"] = OperationType.Delete,
+ ["head"] = OperationType.Head,
+ ["options"] = OperationType.Options,
+ ["trace"] = OperationType.Trace
+ };
+
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ var json = TryLoadJson();
+ if (json == null) return;
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("tags", out var tagsEl) && tagsEl.ValueKind == JsonValueKind.Array)
+ {
+ swaggerDoc.Tags ??= new List();
+ foreach (var tag in tagsEl.EnumerateArray())
+ {
+ if (!tag.TryGetProperty("name", out var nameEl)) continue;
+ var name = nameEl.GetString();
+ if (string.IsNullOrEmpty(name) || swaggerDoc.Tags.Any(t => t.Name == name)) continue;
+ var desc = tag.TryGetProperty("description", out var d) ? d.GetString() : null;
+ swaggerDoc.Tags.Add(new OpenApiTag { Name = name, Description = desc });
+ }
+ }
+
+ if (!root.TryGetProperty("paths", out var pathsEl)) return;
+
+ foreach (var pathProp in pathsEl.EnumerateObject())
+ {
+ var fullPath = pathProp.Name.StartsWith("/api/sl", StringComparison.Ordinal)
+ ? pathProp.Name
+ : "/api/sl" + pathProp.Name;
+
+ var pathItem = new OpenApiPathItem();
+ foreach (var opProp in pathProp.Value.EnumerateObject())
+ {
+ if (!VerbMap.TryGetValue(opProp.Name, out var verb)) continue;
+ pathItem.Operations[verb] = ParseOperation(opProp.Value);
+ }
+
+ if (pathItem.Operations.Count > 0)
+ swaggerDoc.Paths[fullPath] = pathItem;
+ }
+ }
+
+ private static OpenApiOperation ParseOperation(JsonElement el)
+ {
+ var op = new OpenApiOperation
+ {
+ Summary = el.TryGetProperty("summary", out var s) ? s.GetString() : null,
+ Description = el.TryGetProperty("description", out var d) ? d.GetString() : null
+ };
+
+ if (el.TryGetProperty("tags", out var tags) && tags.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var t in tags.EnumerateArray())
+ {
+ var name = t.GetString();
+ if (!string.IsNullOrEmpty(name))
+ op.Tags.Add(new OpenApiTag { Name = name });
+ }
+ }
+
+ if (el.TryGetProperty("parameters", out var parameters) && parameters.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var p in parameters.EnumerateArray())
+ {
+ var param = new OpenApiParameter
+ {
+ Name = p.TryGetProperty("name", out var n) ? n.GetString() : null,
+ In = p.TryGetProperty("in", out var loc) ? ParameterLocationFrom(loc.GetString()) : null,
+ Required = p.TryGetProperty("required", out var req) && req.GetBoolean(),
+ Description = p.TryGetProperty("description", out var pd) ? pd.GetString() : null
+ };
+ if (p.TryGetProperty("schema", out var schema))
+ param.Schema = ParseSchema(schema);
+ op.Parameters.Add(param);
+ }
+ }
+
+ if (el.TryGetProperty("requestBody", out var body))
+ op.RequestBody = ParseRequestBody(body);
+
+ if (el.TryGetProperty("responses", out var responses))
+ {
+ foreach (var resp in responses.EnumerateObject())
+ {
+ op.Responses[resp.Name] = new OpenApiResponse
+ {
+ Description = resp.Value.TryGetProperty("description", out var rd)
+ ? rd.GetString() ?? ""
+ : ""
+ };
+ }
+ }
+
+ return op;
+ }
+
+ private static OpenApiRequestBody? ParseRequestBody(JsonElement el)
+ {
+ if (!el.TryGetProperty("content", out var content)) return null;
+ var body = new OpenApiRequestBody();
+ foreach (var ct in content.EnumerateObject())
+ {
+ var media = new OpenApiMediaType();
+ if (ct.Value.TryGetProperty("schema", out var schema))
+ media.Schema = ParseSchema(schema);
+ body.Content[ct.Name] = media;
+ }
+ return body.Content.Count > 0 ? body : null;
+ }
+
+ private static OpenApiSchema ParseSchema(JsonElement el)
+ {
+ var schema = new OpenApiSchema();
+ if (el.TryGetProperty("type", out var t)) schema.Type = t.GetString();
+ if (el.TryGetProperty("description", out var d)) schema.Description = d.GetString();
+ if (el.TryGetProperty("$ref", out var r))
+ {
+ var refId = r.GetString()?.TrimStart('#', '/');
+ if (!string.IsNullOrEmpty(refId))
+ schema.Reference = new OpenApiReference { Id = refId, Type = ReferenceType.Schema };
+ }
+ return schema;
+ }
+
+ private static ParameterLocation? ParameterLocationFrom(string? loc) => loc switch
+ {
+ "query" => ParameterLocation.Query,
+ "path" => ParameterLocation.Path,
+ "header" => ParameterLocation.Header,
+ "cookie" => ParameterLocation.Cookie,
+ _ => null
+ };
+
+ private static string? TryLoadJson()
+ {
+ foreach (var candidate in ResolveCandidatePaths())
+ {
+ try
+ {
+ if (File.Exists(candidate))
+ return File.ReadAllText(candidate);
+ }
+ catch { /* next */ }
+ }
+ return null;
+ }
+
+ private static IEnumerable ResolveCandidatePaths()
+ {
+ var roots = new[] { AppContext.BaseDirectory, Directory.GetCurrentDirectory() }
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var root in roots)
+ {
+ yield return Path.GetFullPath(Path.Combine(root, "OpenApi", "simplelite-projection.json"));
+ yield return Path.GetFullPath(Path.Combine(root, "..", "..", "..", "Simple", "SimpleLite", "Docs", "openapi", "simplelite-projection.json"));
+ yield return Path.GetFullPath(Path.Combine(root, "..", "..", "Simple", "SimpleLite", "Docs", "openapi", "simplelite-projection.json"));
+ }
+ }
+}
diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs
index 1cea473..312fc6f 100644
--- a/MiGu.Server/Program.cs
+++ b/MiGu.Server/Program.cs
@@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models;
using MiGu.Server.Auth;
using MiGu.Server.Configs;
using MiGu.Server.Launcher;
+using MiGu.Server.OpenApi;
using Yarp.ReverseProxy.Transforms;
static string? FindSourceContentRoot(string startDir)
@@ -97,7 +98,13 @@ builder.Services.AddControllers()
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
- c.SwaggerDoc("v1", new() { Title = "MiGu.Server", Version = "v1", Description = "Simple-FR 平台后端骨架(含 YARP 反代 SimpleLite 8222)。" });
+ c.SwaggerDoc("v1", new()
+ {
+ Title = "MiGu.Server + SimpleLite",
+ Version = "v1",
+ Description = "咪咕平台后端 API,以及经 YARP 反代的 SimpleLite 数据 WebApi(标签 SimpleLite/*)。详见 Simple/SimpleLite/Docs/MIGU-API.md。"
+ });
+ c.DocumentFilter();
// Swagger 里挂 Bearer 输入框,便于手工测带鉴权的端点。
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{