将调度内核标识从 SimpleLite 全面重命名为 Simple3。

配置段/环境变量、Launcher、健康检查 API、OpenAPI 与前后端文案同步;兼容探测旧 SimpleLite 进程名。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
黄兆尉
2026-08-26 17:46:52 +08:00
co-authored by Cursor
parent 912c44c9bb
commit 3686abdc78
79 changed files with 922 additions and 832 deletions
+46 -34
View File
@@ -12,19 +12,17 @@ namespace MiGu.Server.Controllers;
public class AuthController : ControllerBase
{
/// <summary>
/// 登录请求体。
/// 会话 N+1(启动反转):新增 <see cref="LaunchMode"/>。前端登录页让用户选 "WebOnly" / "DesktopAndWeb"
/// MiGu.Server 据此拉起 SimpleLite 子进程并透传 <c>--display-mode=web|web+local</c>。
/// 历史调用方不传该字段时默认 "DesktopAndWeb"(与之前 web+local 默认行为一致,向后兼容)。
/// 登录请求体。可选 <see cref="LaunchMode"/>Simple3 仅 Web 宿主,缺省/未知一律按 WebOnly。
/// MiGu.Server 据此拉起子进程并透传 <c>--display-mode=web</c>。
/// </summary>
public record LoginRequest(string Username, string Password, string Scope, string? LaunchMode = null);
public record LoginRequest(string Username, string Password, string? Scope = null, string? LaunchMode = null);
/// <summary>
/// 登录响应。
/// 会话 N+1 增量字段:
/// <list type="bullet">
/// <item><c>RunMode</c>:根据 SimpleLite 真实拉起结果回填(WebEnabled / WebOnly / Detached)。Detached 表示后端未能拉起 SimpleLite,前端可降级展示。</item>
/// <item><c>LaunchStatus</c><see cref="SimpleLiteLauncher.LaunchResult.Status"/> 枚举字符串,前端用于精细化提示。</item>
/// <item><c>RunMode</c>:根据 Simple3 真实拉起结果回填(WebEnabled / WebOnly / Detached)。Detached 表示后端未能拉起 Simple3,前端可降级展示。</item>
/// <item><c>LaunchStatus</c><see cref="Simple3Launcher.LaunchResult.Status"/> 枚举字符串,前端用于精细化提示。</item>
/// <item><c>LaunchWarning</c>:可空告警文本;非空时前端应该弹消息条告知用户。</item>
/// </list>
/// </summary>
@@ -51,17 +49,18 @@ public class AuthController : ControllerBase
string Scope,
string RunMode,
EffectivePermissions EffectivePermissions,
bool NeedsWizard = false);
bool NeedsWizard = false,
string? Token = null);
private const string CookieName = "simple.auth.token";
private readonly RbacStore _rbac;
private readonly JwtIssuer _jwt;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
private readonly ConfigStore _config;
private readonly ILogger<AuthController> _log;
public AuthController(RbacStore rbac, JwtIssuer jwt, SimpleLiteLauncher launcher, ConfigStore config, ILogger<AuthController> log)
public AuthController(RbacStore rbac, JwtIssuer jwt, Simple3Launcher launcher, ConfigStore config, ILogger<AuthController> log)
{
_rbac = rbac;
_jwt = jwt;
@@ -79,41 +78,40 @@ public class AuthController : ControllerBase
{
if (string.IsNullOrWhiteSpace(req.Username))
return BadRequest(new { message = "用户名不能为空" });
if (req.Scope is not ("Platform" or "RCSMonitor"))
return BadRequest(new { message = "无效 scope" });
// 真密码校验:RbacStore.VerifyCredentials 对不存在 / 已禁用 / 密码错统一返回 null,防用户名枚举。
var user = _rbac.VerifyCredentials(req.Username, req.Password);
if (user == null)
return Unauthorized(new { message = "用户名或密码错误,或账号已被停用" });
// scope 必须落在该账号角色覆盖的 scope」集合内(admin 角色 scope=* 覆盖全部)
if (!_rbac.CanUseScope(user, req.Scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {req.Scope} 的权限" });
// 入口由账号角色决定,不再接受登录页挑选「管理端 / 运营端」
var scope = _rbac.ResolveLoginScope(user);
if (scope == null)
return StatusCode(403, new { message = $"账号 {user.Username} 没有任何可登录区域,请联系管理员分配角色" });
// 会话 N+1:按 LaunchMode 拉起 SimpleLite 子进程(线程池执行,避免占用请求线程)。
// 会话 N+1:按 LaunchMode 拉起 Simple3 子进程(线程池执行,避免占用请求线程)。
var launchMode = NormalizeLaunchMode(req.LaunchMode);
SimpleLiteLauncher.LaunchResult? launchResult = null;
Simple3Launcher.LaunchResult? launchResult = null;
try
{
// M1waitForReady=false —— 拉起 SimpleLite 后立即返回,不在登录请求里同步等端口
// M1waitForReady=false —— 拉起 Simple3 后立即返回,不在登录请求里同步等端口
// 就绪(冷启动可能十几秒)。前端拿 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}",
_log.LogInformation("Simple3 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);
}
catch (Exception ex)
{
_log.LogError(ex, "SimpleLite launch threw for user={User} launchMode={Mode}", user.Username, launchMode);
_log.LogError(ex, "Simple3 launch threw for user={User} launchMode={Mode}", user.Username, launchMode);
}
var runMode = ResolveRunMode(launchResult, launchMode);
var (perm, roleNames, token) = BuildSession(user, req.Scope);
var (perm, roleNames, token) = BuildSession(user, scope);
SetAuthCookie(token);
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new LoginResponse(token, dto, req.Scope, runMode, perm,
return Ok(new LoginResponse(token, dto, scope, runMode, perm,
LaunchStatus: launchResult?.Status,
LaunchWarning: launchResult?.Warning,
NeedsWizard: NeedsWizard()));
@@ -144,13 +142,20 @@ public class AuthController : ControllerBase
if (user == null || !user.Enabled)
return Unauthorized(new { message = "账号已失效或被停用" });
// 账号当前是否还允许这个 scope(管理员可能在此期间调整了角色)。
if (!_rbac.CanUseScope(user, scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {scope} 的权限" });
var preferred = _rbac.ResolveLoginScope(user);
if (preferred == null)
return StatusCode(403, new { message = $"账号 {user.Username} 没有任何可登录区域,请联系管理员分配角色" });
var (perm, roleNames, token) = BuildSession(user, preferred);
string? rotated = null;
if (!string.Equals(preferred, scope, StringComparison.OrdinalIgnoreCase))
{
SetAuthCookie(token);
rotated = token;
}
var (perm, roleNames, _) = BuildSession(user, scope);
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new MeResponse(dto, scope, InferRunMode(), perm, NeedsWizard()));
return Ok(new MeResponse(dto, preferred, InferRunMode(), perm, NeedsWizard(), rotated));
}
/// <summary>用同一身份切换 scope 并重发 token + perms。</summary>
@@ -172,6 +177,11 @@ public class AuthController : ControllerBase
if (!_rbac.CanUseScope(user, req.Scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {req.Scope} 的权限" });
// 有管理权限的账号固定走管理入口,不再切到运营壳;请用运营权限账号登录查看运营页。
var preferred = _rbac.ResolveLoginScope(user);
if (preferred == PageCatalog.ScopePlatform && req.Scope == PageCatalog.ScopeMonitor)
return StatusCode(403, new { message = "请使用运营权限账号查看运营页面" });
var (perm, roleNames, token) = BuildSession(user, req.Scope);
SetAuthCookie(token);
@@ -200,22 +210,23 @@ public class AuthController : ControllerBase
return (perm, roleNames, token);
}
/// <summary>me / switchScope 不重启 SimpleLite,依据 Launcher 记录的 LastLaunchMode 反推 RunMode。</summary>
/// <summary>me / switchScope 不重启 Simple3,依据 Launcher 记录的 LastLaunchMode 反推 RunMode。</summary>
private string InferRunMode()
{
var last = _launcher.LastLaunchMode;
if (string.IsNullOrEmpty(last)) return "Detached";
if (last == SimpleLiteLauncher.ExternalReuseLaunchMode) return "WebEnabled";
if (last == Simple3Launcher.ExternalReuseLaunchMode) return "WebOnly";
return last.Contains("local", StringComparison.OrdinalIgnoreCase) ? "WebEnabled" : "WebOnly";
}
/// <summary>根据 Launcher 真实结果决定 RunMode(避免 SimpleLite 没起却假装 WebEnabled)。</summary>
private static string ResolveRunMode(SimpleLiteLauncher.LaunchResult? result, string launchMode)
/// <summary>根据 Launcher 真实结果决定 RunMode(避免内核没起却假装已连接)。</summary>
private static string ResolveRunMode(Simple3Launcher.LaunchResult? result, string launchMode)
{
if (result is not { Started: true })
return "Detached";
// Simple3 无本地端:复用既有实例也按 WebOnly 展示。
if (result.Value.Status == "ReusingExisting")
return "WebEnabled";
return "WebOnly";
if (!string.IsNullOrEmpty(result.Value.DisplayMode))
{
if (result.Value.DisplayMode.Equals("web", StringComparison.OrdinalIgnoreCase))
@@ -230,8 +241,9 @@ public class AuthController : ControllerBase
{
return raw?.Trim().ToLowerInvariant() switch
{
"webonly" or "web-only" or "web" => "WebOnly",
_ => "DesktopAndWeb",
"desktopandweb" or "web+local" or "weblocal" or "local" => "DesktopAndWeb",
// Simple3 默认仅 Web;未传 / 未知也走 WebOnly
_ => "WebOnly",
};
}
+2 -2
View File
@@ -31,7 +31,7 @@ public sealed class FleetController : ControllerBase
/// <summary>
/// CDM 搬运任务列表(读平台快照库 cdm_tasks)。在线时先即时同步一次拿最新,
/// SimpleLite 关闭时回退最近快照,并通过 online/lastSyncAt 告知前端数据是否滞后。
/// Simple3 关闭时回退最近快照,并通过 online/lastSyncAt 告知前端数据是否滞后。
/// </summary>
[HttpGet("tasks")]
public async Task<ActionResult<object>> Tasks([FromQuery] int limit = 1000, CancellationToken ct = default)
@@ -76,7 +76,7 @@ public sealed class FleetController : ControllerBase
}
/// <summary>
/// 车辆报警列表(读平台记录 vehicle_alarms)。含活跃 + 历史;SimpleLite 离线时回退最近记录,
/// 车辆报警列表(读平台记录 vehicle_alarms)。含活跃 + 历史;Simple3 离线时回退最近记录,
/// 通过 online/lastSyncAt 告知数据是否滞后。activeOnly=true 仅返回未恢复的报警。
/// </summary>
[HttpGet("alarms")]
+16 -16
View File
@@ -9,9 +9,9 @@ namespace MiGu.Server.Controllers;
public class HealthController : ControllerBase
{
private static readonly DateTimeOffset StartTime = DateTimeOffset.UtcNow;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
public HealthController(SimpleLiteLauncher launcher) => _launcher = launcher;
public HealthController(Simple3Launcher launcher) => _launcher = launcher;
/// <summary>匿名存活探针:仅返回进程级状态,不暴露端口拓扑等部署细节。</summary>
[HttpGet]
@@ -26,28 +26,28 @@ public class HealthController : ControllerBase
}
/// <summary>
/// SimpleLite 拉起配置诊断:查看当前 ExecutablePath、解析结果、端口是否已有服务。
/// 配置位置:<c>MiGu.Server/appsettings.json</c> → <c>SimpleLite</c> 节点。
/// Simple3 拉起配置诊断:查看当前 ExecutablePath、解析结果、端口是否已有服务。
/// 配置位置:<c>MiGu.Server/appsettings.json</c> → <c>Simple3</c> 节点。
/// 含服务器本地路径等敏感信息,要求登录。
/// </summary>
[HttpGet("simplelite")]
[HttpGet("simple3")]
[Authorize]
public IActionResult GetSimpleLiteDiagnostics() => Ok(_launcher.GetDiagnostics());
public IActionResult GetSimple3Diagnostics() => Ok(_launcher.GetDiagnostics());
/// <summary>关闭本机全部 SimpleLite 进程。仅 Platform 管理端可调。</summary>
[HttpPost("simplelite/stop")]
/// <summary>关闭本机全部 Simple3 进程。仅 Platform 管理端可调。</summary>
[HttpPost("simple3/stop")]
[Authorize(Policy = "PlatformScope")]
public IActionResult StopSimpleLite()
public IActionResult StopSimple3()
{
var killed = _launcher.StopAll();
var diag = _launcher.GetDiagnostics();
return Ok(new { killed, diagnostics = diag });
}
/// <summary>关闭并重新拉起 SimpleLite(不同步 DLL)。仅 Platform 管理端可调。</summary>
[HttpPost("simplelite/restart")]
/// <summary>关闭并重新拉起 Simple3(不同步 DLL)。仅 Platform 管理端可调。</summary>
[HttpPost("simple3/restart")]
[Authorize(Policy = "PlatformScope")]
public IActionResult RestartSimpleLite([FromQuery] string launchMode = "webonly")
public IActionResult RestartSimple3([FromQuery] string launchMode = "webonly")
{
var result = _launcher.Restart(launchMode);
var diag = _launcher.GetDiagnostics();
@@ -55,12 +55,12 @@ public class HealthController : ControllerBase
}
/// <summary>
/// 关闭 SimpleLite、同步最新 DLL、重新拉起。用于「前往站点」API 缺失时一键更新。
/// 会终止本机全部 SimpleLite 进程并重启,仅 Platform 管理端可调。
/// 关闭 Simple3、同步最新 DLL、重新拉起。用于「前往站点」API 缺失时一键更新。
/// 会终止本机全部 Simple3 进程并重启,仅 Platform 管理端可调。
/// </summary>
[HttpPost("simplelite/restart-for-update")]
[HttpPost("simple3/restart-for-update")]
[Authorize(Policy = "PlatformScope")]
public IActionResult RestartSimpleLiteForUpdate([FromQuery] string launchMode = "webonly")
public IActionResult RestartSimple3ForUpdate([FromQuery] string launchMode = "webonly")
{
var result = _launcher.RestartForUpdate(launchMode);
var diag = _launcher.GetDiagnostics();
@@ -8,8 +8,8 @@ using MiGu.Server.Launcher;
namespace MiGu.Server.Controllers;
/// <summary>
/// 读取 SimpleLite 地图固定目录下的 JSON 原文,供平台「地图管理」右侧预览。
/// 先向 SimpleLite 拉取 maps 列表拿到 directory,再读本机同路径文件(与 SimpleLite 同机部署)。
/// 读取 Simple3 地图固定目录下的 JSON 原文,供平台「地图管理」右侧预览。
/// 先向 Simple3 拉取 maps 列表拿到 directory,再读本机同路径文件(与 Simple3 同机部署)。
/// </summary>
[ApiController]
[Authorize]
@@ -18,13 +18,13 @@ public class MapsContentController : ControllerBase
{
private readonly IHttpClientFactory _httpFactory;
private readonly InternalTokenStore _internalToken;
private readonly SimpleLiteOptions _sl;
private readonly Simple3Options _sl;
private readonly ILogger<MapsContentController> _log;
public MapsContentController(
IHttpClientFactory httpFactory,
InternalTokenStore internalToken,
IOptions<SimpleLiteOptions> sl,
IOptions<Simple3Options> sl,
ILogger<MapsContentController> log)
{
_httpFactory = httpFactory;
@@ -47,7 +47,7 @@ public class MapsContentController : ControllerBase
{
var directory = await FetchMapsDirectoryAsync(ct);
if (string.IsNullOrWhiteSpace(directory))
return StatusCode(503, new { message = "无法从 SimpleLite 获取地图目录" });
return StatusCode(503, new { message = "无法从 Simple3 获取地图目录" });
var fileName = name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.json";
var fullPath = Path.GetFullPath(Path.Combine(directory, fileName));
@@ -86,7 +86,7 @@ public class MapsContentController : ControllerBase
using var resp = await client.SendAsync(msg, ct);
var body = await resp.Content.ReadAsStringAsync(ct);
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"SimpleLite maps 列表返回 {(int)resp.StatusCode}");
throw new InvalidOperationException($"Simple3 maps 列表返回 {(int)resp.StatusCode}");
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
@@ -4,8 +4,8 @@ using Microsoft.AspNetCore.Mvc;
namespace MiGu.Server.Controllers;
/// <summary>
/// 投影 API 占位:真实落地时由 YARP 反代到 SimpleLite WebAPI 的 /api/projection/* 路径。
/// 本地 Mock 数据仅用于无 SimpleLite 运行时的开发联调。
/// 投影 API 占位:真实落地时由 YARP 反代到 Simple3 WebAPI 的 /api/projection/* 路径。
/// 本地 Mock 数据仅用于无 Simple3 运行时的开发联调。
///
/// AR-4: 全 class 加 [Authorize] —— 任何登录用户都能读 mock 投影数据;未登录直接 401。
/// </summary>
@@ -9,7 +9,7 @@ namespace MiGu.Server.Controllers;
/// <summary>
/// 迷毂「数据中心」:把 scene.signal 的 Model JSON 以表格读写(PLC 握手 / 磁条交管)。
/// 文件落在 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,不依赖 SimpleLite 进程是否在跑。
/// 文件落在 Simple3 工作目录 <c>Config/Signal/*.json</c>,不依赖 Simple3 进程是否在跑。
/// </summary>
[ApiController]
[Authorize(Policy = "PlatformScope")]
@@ -10,7 +10,7 @@ namespace MiGu.Server.Controllers;
/// 数据持久化于 <c>platform.db</c> 的 <c>simple_fields</c> 表。
///
/// 对应前端「字段管理」页(<c>/admin/simple-fields</c>,页面 key <c>admin-simple-fields</c>)。
/// 前端主流程为「刷新」读库、「默认」从 SimpleLite 拉取模板、「保存」走 <see cref="SaveBatch"/> 全量替换。
/// 前端主流程为「刷新」读库、「默认」从 Simple3 拉取模板、「保存」走 <see cref="SaveBatch"/> 全量替换。
/// </summary>
[ApiController]
[Authorize]
+5 -5
View File
@@ -17,7 +17,7 @@ namespace MiGu.Server.Controllers;
/// </list>
/// 说明:保存时即把选型固化为单一事实来源 <c>deployment</c> section,并同步联动 Launcher ——
/// <see cref="SaveProfile"/> 调 <c>WriteActiveScenes</c> 写 <c>plugins/active-scenes.json</c> / 透传 <c>--scenes</c>
/// 驱动 SimpleLite 下次启动选择性加载选定的导航场景插件。
/// 驱动 Simple3 下次启动选择性加载选定的导航场景插件。
/// </summary>
[ApiController]
[Authorize]
@@ -25,10 +25,10 @@ namespace MiGu.Server.Controllers;
public class WizardController : ControllerBase
{
private readonly ConfigStore _store;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
private readonly ILogger<WizardController> _log;
public WizardController(ConfigStore store, SimpleLiteLauncher launcher, ILogger<WizardController> log)
public WizardController(ConfigStore store, Simple3Launcher launcher, ILogger<WizardController> log)
{
_store = store;
_launcher = launcher;
@@ -81,7 +81,7 @@ public class WizardController : ControllerBase
_store.PutDeployment(profile);
// 平台 → 内核联动:把导航选型写入 SimpleLite 的 plugins/active-scenes.json(下次启动选择性加载;
// 平台 → 内核联动:把导航选型写入 Simple3 的 plugins/active-scenes.json(下次启动选择性加载;
// 已运行实例可由前端再调 POST /api/sl/projection/scenes/apply 触发增量 reload)。
// scene.device(门/充电桩/按钮盒驱动)为各类项目通用能力,向导暂无独立选项,固定并入激活集合;
// 选 SPS / Pack 时 ToLauncherSceneIds 会并入 scene.signalPLC 握手)。
@@ -107,7 +107,7 @@ public class WizardController : ControllerBase
/// 统一对外投影:camelCase 字段 + 推导出的 activeSceneIds(导航选型 → 内核场景 id)+
/// hiddenPages(被裁剪的菜单页)+ 可选的 activeScenesWrite(保存时写 active-scenes.json 的结果)。
/// </summary>
private static object Project(DeploymentProfile dp, SimpleLiteLauncher.ActiveScenesWriteResult? write = null) => new
private static object Project(DeploymentProfile dp, Simple3Launcher.ActiveScenesWriteResult? write = null) => new
{
configured = dp.Configured,
platformType = dp.PlatformType,