diff --git a/MiGu.Server/Controllers/AuthController.cs b/MiGu.Server/Controllers/AuthController.cs
index ebbb0d6..d1acc1f 100644
--- a/MiGu.Server/Controllers/AuthController.cs
+++ b/MiGu.Server/Controllers/AuthController.cs
@@ -96,7 +96,9 @@ public class AuthController : ControllerBase
SimpleLiteLauncher.LaunchResult? launchResult = null;
try
{
- launchResult = await Task.Run(() => _launcher.MaybeStart(launchMode));
+ // M1:waitForReady=false —— 拉起 SimpleLite 后立即返回,不在登录请求里同步等端口
+ // 就绪(冷启动可能十几秒)。前端拿 LaunchStatus=Starting 即可,必要时轮询健康检查。
+ launchResult = await Task.Run(() => _launcher.MaybeStart(launchMode, waitForReady: false));
_log.LogInformation("SimpleLite launch result for user={User} launchMode={Mode}: Started={Started} Status={Status} Detail={Detail}",
user.Username, launchMode, launchResult.Value.Started, launchResult.Value.Status, launchResult.Value.Detail);
}
diff --git a/MiGu.Server/Launcher/SimpleLiteLauncher.cs b/MiGu.Server/Launcher/SimpleLiteLauncher.cs
index 81da09f..7038752 100644
--- a/MiGu.Server/Launcher/SimpleLiteLauncher.cs
+++ b/MiGu.Server/Launcher/SimpleLiteLauncher.cs
@@ -61,7 +61,7 @@ public sealed class SimpleLiteLauncher : IDisposable
///
/// "WebOnly" 或 "DesktopAndWeb"(大小写不敏感)。
/// 本次调用产生的状态摘要,可写入登录响应或日志。
- public LaunchResult MaybeStart(string launchMode)
+ public LaunchResult MaybeStart(string launchMode, bool waitForReady = true)
{
var displayMode = NormalizeDisplayMode(launchMode);
@@ -150,7 +150,7 @@ public sealed class SimpleLiteLauncher : IDisposable
var workdir = string.IsNullOrWhiteSpace(_opts.WorkingDirectory)
? Path.GetDirectoryName(resolved) ?? Environment.CurrentDirectory
- : Path.GetFullPath(_opts.WorkingDirectory);
+ : ResolveConfiguredDirectory(_opts.WorkingDirectory) ?? Path.GetDirectoryName(resolved) ?? Environment.CurrentDirectory;
var arguments = BuildArguments(displayMode, _opts.Arguments);
@@ -182,6 +182,17 @@ public sealed class SimpleLiteLauncher : IDisposable
}
}
+ // M1:登录路径传 waitForReady=false —— 进程拉起后立即返回,不再同步阻塞最长
+ // ReadinessTimeoutMs 等端口就绪(避免冷启动登录干等十几秒)。前端可轮询
+ // GET /api/health/simplelite 获知就绪状态。
+ if (!waitForReady)
+ {
+ return new LaunchResult(true, "Starting",
+ $"projection :{_opts.ProjectionPort} readiness wait skipped (async), displayMode={displayMode}",
+ DisplayMode: displayMode,
+ Warning: null);
+ }
+
var ready = WaitForProjectionReady();
return new LaunchResult(true, ready ? "Ready" : "StartedButNotReady",
ready ? $"projection :{_opts.ProjectionPort} reachable, displayMode={displayMode}"
@@ -262,7 +273,7 @@ public sealed class SimpleLiteLauncher : IDisposable
public string? ResolveWorkingDirectory()
{
if (!string.IsNullOrWhiteSpace(_opts.WorkingDirectory))
- return Path.GetFullPath(_opts.WorkingDirectory);
+ return ResolveConfiguredDirectory(_opts.WorkingDirectory);
var resolved = ResolveExecutable(_opts.ExecutablePath);
return resolved == null ? null : (Path.GetDirectoryName(resolved) ?? Environment.CurrentDirectory);
}
@@ -518,8 +529,8 @@ public sealed class SimpleLiteLauncher : IDisposable
{
if (!string.IsNullOrWhiteSpace(configured))
{
- var p = Path.IsPathRooted(configured) ? configured : Path.GetFullPath(configured, _env.ContentRootPath);
- return File.Exists(p) ? p : null;
+ var p = ResolveConfiguredFile(configured);
+ if (p != null) return p;
}
var cwd = _env.ContentRootPath;
@@ -562,6 +573,42 @@ public sealed class SimpleLiteLauncher : IDisposable
return null;
}
+ private string? ResolveConfiguredFile(string configured)
+ {
+ foreach (var baseDir in EnumeratePathBases())
+ {
+ var p = Path.IsPathRooted(configured)
+ ? configured
+ : Path.GetFullPath(configured, baseDir);
+ if (File.Exists(p)) return p;
+ if (Path.IsPathRooted(configured)) break;
+ }
+ return null;
+ }
+
+ private string? ResolveConfiguredDirectory(string configured)
+ {
+ foreach (var baseDir in EnumeratePathBases())
+ {
+ var p = Path.IsPathRooted(configured)
+ ? configured
+ : Path.GetFullPath(configured, baseDir);
+ if (Directory.Exists(p)) return p;
+ if (Path.IsPathRooted(configured)) break;
+ }
+ return null;
+ }
+
+ private IEnumerable EnumeratePathBases()
+ {
+ var dir = new DirectoryInfo(_env.ContentRootPath);
+ while (dir != null)
+ {
+ yield return dir.FullName;
+ dir = dir.Parent;
+ }
+ }
+
private bool WaitForProjectionReady()
{
if (_opts.ReadinessTimeoutMs == 0) return false;
diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs
index ac559df..1cea473 100644
--- a/MiGu.Server/Program.cs
+++ b/MiGu.Server/Program.cs
@@ -1,20 +1,51 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.FileProviders;
using Microsoft.OpenApi.Models;
using MiGu.Server.Auth;
using MiGu.Server.Configs;
using MiGu.Server.Launcher;
using Yarp.ReverseProxy.Transforms;
-var builder = WebApplication.CreateBuilder(args);
+static string? FindSourceContentRoot(string startDir)
+{
+ var dir = new DirectoryInfo(startDir);
+ while (dir != null)
+ {
+ if (File.Exists(Path.Combine(dir.FullName, "MiGu.Server.csproj")))
+ return dir.FullName;
+ dir = dir.Parent;
+ }
+ return null;
+}
+
+var builder = WebApplication.CreateBuilder(new WebApplicationOptions
+{
+ Args = args,
+ // 直接运行 bin/Debug/net8.0/MiGu.Server.exe 时,默认 ContentRoot 会落到 bin 目录,
+ // 导致 appsettings/data/wwwroot 与 dotnet run 不一致。源码构建输出中统一回到项目目录;
+ // 发布包没有 csproj,则使用 exe 所在目录作为常规内容根。
+ ContentRootPath = FindSourceContentRoot(AppContext.BaseDirectory) ?? AppContext.BaseDirectory
+});
+
+if (string.IsNullOrWhiteSpace(builder.Configuration["urls"])
+ && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ASPNETCORE_URLS"))
+ && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOTNET_URLS")))
+{
+ // 直接运行 MiGu.Server.exe 不读取 launchSettings.json;保持与 dotnet run / 文档一致默认监听 8080。
+ builder.WebHost.UseUrls("http://0.0.0.0:8080");
+}
// S2 根治 (会话45):让 MiGu.Server 启动时自动优先把 frontends/apps/simple-platform-vue/dist/
// 作为 WebRoot —— 解决「每次 vite build → wwwroot/index.html 都被 hash 漂移 → 提一个噪音 commit」
// 的死循环。
//
-// 探测策略:从 ContentRootPath 出发向上逐级找 frontends/apps/simple-platform-vue/dist/index.html,
-// 找到则将 WebRootPath 改指到 dist;找不到则保持默认 wwwroot/(用于「克隆后没跑 build」或
-// 生产部署机不带 node 的场景,wwwroot/ 由 build-platform-frontend.bat 的 robocopy /MIR 兜底)。
+// 探测策略:从 ContentRootPath 出发向上逐级找前端产物:
+// 1) frontends/apps/simple-platform-vue/dist/index.html(开发态刚跑完 pnpm build);
+// 2) MiGu.Server/wwwroot/index.html(直接运行 bin/Debug/net8.0/MiGu.Server.exe 时的源码区兜底);
+// 3) 当前目录 wwwroot/index.html(dotnet run / 发布包常规布局)。
+// 找到则将 WebRootPath 改指到对应目录;找不到才保持默认 wwwroot/。
//
// 影响:
// - dev 模式:跑过一次 `pnpm build` 后无需 robocopy,重启 MiGu.Server 即生效;
@@ -22,27 +53,35 @@ var builder = WebApplication.CreateBuilder(args);
// - 部署:CI/CD 走 build-platform-frontend.bat 把 dist 同步到 wwwroot/,dist 此时不存在
// 于发布产物中 → 自动 fallback 到 wwwroot/。行为与改造前一致。
{
- static string? FindDistRoot(string startDir)
+ static string? FindFrontendWebRoot(string startDir)
{
var dir = new DirectoryInfo(startDir);
while (dir != null)
{
- var candidate = Path.Combine(dir.FullName, "frontends", "apps", "simple-platform-vue", "dist", "index.html");
- if (File.Exists(candidate)) return Path.GetDirectoryName(candidate);
+ var distIndex = Path.Combine(dir.FullName, "frontends", "apps", "simple-platform-vue", "dist", "index.html");
+ if (File.Exists(distIndex)) return Path.GetDirectoryName(distIndex);
+
+ var sourceWwwrootIndex = Path.Combine(dir.FullName, "MiGu.Server", "wwwroot", "index.html");
+ if (File.Exists(sourceWwwrootIndex)) return Path.GetDirectoryName(sourceWwwrootIndex);
+
+ var localWwwrootIndex = Path.Combine(dir.FullName, "wwwroot", "index.html");
+ if (File.Exists(localWwwrootIndex)) return Path.GetDirectoryName(localWwwrootIndex);
+
dir = dir.Parent;
}
return null;
}
- var dist = FindDistRoot(builder.Environment.ContentRootPath);
- if (dist != null)
+ var webRoot = FindFrontendWebRoot(builder.Environment.ContentRootPath);
+ if (webRoot != null)
{
- builder.Environment.WebRootPath = dist;
- Console.WriteLine($"[MiGu.Server] WebRoot -> dist: {dist}");
+ builder.Environment.WebRootPath = webRoot;
+ builder.Environment.WebRootFileProvider = new PhysicalFileProvider(webRoot);
+ Console.WriteLine($"[MiGu.Server] WebRoot -> {webRoot}");
}
else
{
- Console.WriteLine($"[MiGu.Server] WebRoot -> wwwroot (dist not found): {builder.Environment.WebRootPath}");
+ Console.WriteLine($"[MiGu.Server] WebRoot -> default wwwroot (frontend index not found): {builder.Environment.WebRootPath}");
}
}
@@ -173,6 +212,10 @@ builder.Services.AddReverseProxy()
// 单例配置仓库(内存 + JSON 文件持久化占位)
builder.Services.AddSingleton();
+// 运维审计持久化存储(替代旧的纯内存队列,MiGu.Server 重启后审计不丢)。
+builder.Services.AddSingleton();
+// OpsController 真实转发 SimpleLite reflection execute 所需的 HttpClient 工厂。
+builder.Services.AddHttpClient();
// 会话 N+1(启动反转):把 SimpleLite 子进程拉起器接入 DI;AuthController 登录成功后按 LaunchMode 调 MaybeStart。
// 配置段绑定 appsettings.json:SimpleLite,可被环境变量 SIMPLELITE__XXX 覆盖。
@@ -216,6 +259,39 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
+// M6:在反向代理 / 负载均衡后运行时,根据 X-Forwarded-Proto 还原真实 scheme,让
+// Request.IsHttps 正确 → 登录 Cookie 的 Secure 标志在生产 HTTPS 下能正确置位。
+// 默认(未配置 KnownProxies):清空 Known* 表 = 信任所有前置转发头,适合「反代与本服务同机 /
+// 可信内网」部署。生产若可能被不可信网络直连,请在 appsettings 配置 ForwardedHeaders:KnownProxies
+// (可信代理 IP 列表)收紧:仅接受来自这些代理的 X-Forwarded-* 头,防止外部伪造 Proto/For。
+var forwardedOptions = new ForwardedHeadersOptions
+{
+ ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
+};
+forwardedOptions.KnownNetworks.Clear();
+forwardedOptions.KnownProxies.Clear();
+var trustedProxies = app.Configuration.GetSection("ForwardedHeaders:KnownProxies").Get()
+ ?? Array.Empty();
+if (trustedProxies.Length > 0)
+{
+ // 转发链深度按可信代理数 +1 收紧,避免外部多塞一层伪造头被采信。
+ forwardedOptions.ForwardLimit = trustedProxies.Length + 1;
+ foreach (var ip in trustedProxies)
+ {
+ if (System.Net.IPAddress.TryParse(ip.Trim(), out var addr))
+ forwardedOptions.KnownProxies.Add(addr);
+ else
+ app.Logger.LogWarning("ForwardedHeaders:KnownProxies 含无法解析的地址,已忽略:{Ip}", ip);
+ }
+ // 全部解析失败时 KnownProxies 为空 → 中间件将拒绝所有转发头(fail-closed,安全方向)。
+ app.Logger.LogInformation("ForwardedHeaders 已限定 {Count} 个可信代理", forwardedOptions.KnownProxies.Count);
+}
+else
+{
+ app.Logger.LogInformation("ForwardedHeaders 未配置 KnownProxies:信任所有前置转发头(适合同机 / 可信内网)");
+}
+app.UseForwardedHeaders(forwardedOptions);
+
app.UseCors();
// 静态资源:MiGu.Server/wwwroot 下可同时存放 admin / monitor / index 三份 SPA