将调度内核标识从 SimpleLite 全面重命名为 Simple3。
配置段/环境变量、Launcher、健康检查 API、OpenAPI 与前后端文案同步;兼容探测旧 SimpleLite 进程名。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,24 +3,24 @@
|
||||
namespace MiGu.Server.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// MiGu.Server ↔ SimpleLite 8222 之间的 **内部共享 token**。
|
||||
/// MiGu.Server ↔ Simple3 8222 之间的 **内部共享 token**。
|
||||
///
|
||||
/// 设计动机:SimpleLite 8222 的 EmbedIO WebApi(ReflectionApi / PersistenceApi / MapEditApi)
|
||||
/// 历史上完全无鉴权,远程访问 = 接管调度内核。为不在 SimpleLite 侧实现完整 JWT 验签
|
||||
/// (减少 SimpleLite 复杂度),采用一个轻量约定:
|
||||
/// 设计动机:Simple3 8222 的 EmbedIO WebApi(ReflectionApi / PersistenceApi / MapEditApi)
|
||||
/// 历史上完全无鉴权,远程访问 = 接管调度内核。为不在 Simple3 侧实现完整 JWT 验签
|
||||
/// (减少 Simple3 复杂度),采用一个轻量约定:
|
||||
///
|
||||
/// - <b>本机回环</b>(127.0.0.1 / ::1)SimpleLite 直接放行(开发机直连不受影响);
|
||||
/// - <b>本机回环</b>(127.0.0.1 / ::1)Simple3 直接放行(开发机直连不受影响);
|
||||
/// - <b>其它来源</b>必须携带 <c>X-Platform-Internal-Token</c> header;
|
||||
/// - MiGu.Server YARP 反代 <c>/api/sl/*</c> 到 SimpleLite 8222 时,YARP transform
|
||||
/// - MiGu.Server YARP 反代 <c>/api/sl/*</c> 到 Simple3 8222 时,YARP transform
|
||||
/// 会自动追加该 header;
|
||||
/// - SimpleLite 端通过 <c>simple.json:platform.internalToken</c> 或环境变量
|
||||
/// <c>SIMPLELITE__PLATFORM__INTERNALTOKEN</c> 配置同一个 token;两端不一致即拒绝。
|
||||
/// - Simple3 端通过 <c>simple.json:platform.internalToken</c> 或环境变量
|
||||
/// <c>SIMPLE3__PLATFORM__INTERNALTOKEN</c> 配置同一个 token;两端不一致即拒绝。
|
||||
///
|
||||
/// Token 来源优先级:
|
||||
/// 1) appsettings.json:Internal:Token 显式配置(生产推荐:strong, length ≥ 32)
|
||||
/// 2) 环境变量 PLATFORM__INTERNAL__TOKEN
|
||||
/// 3) 兜底:进程随机 64 字节 base64,并在日志告警 + 写入 <c>data/.internal-token</c>
|
||||
/// 文件供本机 SimpleLite 读取(同机部署常见场景)
|
||||
/// 文件供本机 Simple3 读取(同机部署常见场景)
|
||||
/// </summary>
|
||||
public sealed class InternalTokenStore
|
||||
{
|
||||
@@ -39,7 +39,7 @@ public sealed class InternalTokenStore
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底:进程随机 + 落地到 data/.internal-token 便于同机 SimpleLite 读取
|
||||
// 兜底:进程随机 + 落地到 data/.internal-token 便于同机 Simple3 读取
|
||||
var dataDir = Path.Combine(env.ContentRootPath, "data");
|
||||
Directory.CreateDirectory(dataDir);
|
||||
var file = Path.Combine(dataDir, ".internal-token");
|
||||
@@ -71,12 +71,12 @@ public sealed class InternalTokenStore
|
||||
PersistedFilePath = file;
|
||||
logger.LogWarning(
|
||||
"Internal token 未配置 —— 已生成进程随机值并写入 {File}(重启后保留)。" +
|
||||
"若 MiGu.Server 与 SimpleLite 不在同一台机器,请把同一个值写入 SimpleLite 端 simple.json:platform.internalToken。",
|
||||
"若 MiGu.Server 与 Simple3 不在同一台机器,请把同一个值写入 Simple3 端 simple.json:platform.internalToken。",
|
||||
file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "internal token 落盘失败 —— 远程 SimpleLite 调用将无法通过鉴权(每次重启 token 都不同)。");
|
||||
logger.LogError(ex, "internal token 落盘失败 —— 远程 Simple3 调用将无法通过鉴权(每次重启 token 都不同)。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace MiGu.Server.Configs;
|
||||
/// <para>它是「按选型裁剪」的单一事实来源:</para>
|
||||
/// <list type="number">
|
||||
/// <item><see cref="Configured"/>=false 时,登录返回 <c>NeedsWizard=true</c>,前端跳转配置向导;</item>
|
||||
/// <item><see cref="NavigationKinds"/> 经 <see cref="ToActiveSceneIds"/> 映射为 SimpleLite 场景 id,
|
||||
/// <item><see cref="NavigationKinds"/> 经 <see cref="ToActiveSceneIds"/> 映射为 Simple3 场景 id,
|
||||
/// 由 Launcher 写入 <c>plugins/active-scenes.json</c> / 透传 <c>--scenes</c>,驱动内核「选择性加载」导航场景插件;</item>
|
||||
/// <item><see cref="Modules"/> 驱动平台菜单与能力裁剪(PageCatalog 可见页)。</item>
|
||||
/// </list>
|
||||
@@ -35,7 +35,7 @@ public record DeploymentProfile(
|
||||
UpdatedBy: "");
|
||||
|
||||
/// <summary>
|
||||
/// 导航方式 → SimpleLite 场景 id 映射。与 <c>*.scene.json.id</c>、<c>SimpleCore.Navigation.NavKind</c>
|
||||
/// 导航方式 → Simple3 场景 id 映射。与 <c>*.scene.json.id</c>、<c>SimpleCore.Navigation.NavKind</c>
|
||||
/// 以及内核 <c>active-scenes.json.activeScenes</c> 一致;这是平台与内核之间「导航选型」的契约约定。
|
||||
/// <para>两平台制(StandardScene 拆分落地):磁导航 → <c>scene.mag</c>;
|
||||
/// 二维码与激光共用融合平台 <c>scene.qrlidar</c>(激光坐标导航 + 二维码按轨道逐段触发,可融合可单用)。</para>
|
||||
@@ -49,7 +49,7 @@ public record DeploymentProfile(
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 把已选 <see cref="NavigationKinds"/> 映射为 SimpleLite 激活场景 id 列表(去重、忽略未知项、保持选择顺序)。
|
||||
/// 把已选 <see cref="NavigationKinds"/> 映射为 Simple3 激活场景 id 列表(去重、忽略未知项、保持选择顺序)。
|
||||
/// 供 Launcher 写 <c>active-scenes.json</c> / 拼 <c>--scenes</c> 参数使用。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ToActiveSceneIds()
|
||||
|
||||
@@ -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
|
||||
{
|
||||
// M1:waitForReady=false —— 拉起 SimpleLite 后立即返回,不在登录请求里同步等端口
|
||||
// M1:waitForReady=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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.signal(PLC 握手)。
|
||||
@@ -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,
|
||||
|
||||
@@ -8,14 +8,14 @@ using MiGu.Server.Persistence;
|
||||
namespace MiGu.Server.Fleet;
|
||||
|
||||
/// <summary>
|
||||
/// 单例:轮询 SimpleLite 车辆 + 每车状态,读取「车体_AlarmInfo/车体_AlarmLevel」并对帐进 platform.db(vehicle_alarms)。
|
||||
/// 出现→开 active;文案变→更新;消失→置 cleared 并记录恢复时间/时长。永不删=完整历史;SimpleLite 离线仍可查最近记录。
|
||||
/// 单例:轮询 Simple3 车辆 + 每车状态,读取「车体_AlarmInfo/车体_AlarmLevel」并对帐进 platform.db(vehicle_alarms)。
|
||||
/// 出现→开 active;文案变→更新;消失→置 cleared 并记录恢复时间/时长。永不删=完整历史;Simple3 离线仍可查最近记录。
|
||||
/// </summary>
|
||||
public sealed class AlarmCollector
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SimpleLiteOptions _sl;
|
||||
private readonly Simple3Options _sl;
|
||||
private readonly InternalTokenStore _token;
|
||||
private readonly ILogger<AlarmCollector> _log;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
@@ -28,7 +28,7 @@ public sealed class AlarmCollector
|
||||
public AlarmCollector(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IHttpClientFactory httpFactory,
|
||||
IOptions<SimpleLiteOptions> sl,
|
||||
IOptions<Simple3Options> sl,
|
||||
InternalTokenStore token,
|
||||
ILogger<AlarmCollector> log)
|
||||
{
|
||||
|
||||
@@ -32,14 +32,14 @@ public sealed class CdmTaskDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单例:从 SimpleLite 投影拉取 CDM 任务并 upsert 到 platform.db(cdm_tasks),永不删除=保留历史。
|
||||
/// 同时维护「SimpleLite 是否在线 / 最近同步时间」,供任务页离线降级展示。
|
||||
/// 单例:从 Simple3 投影拉取 CDM 任务并 upsert 到 platform.db(cdm_tasks),永不删除=保留历史。
|
||||
/// 同时维护「Simple3 是否在线 / 最近同步时间」,供任务页离线降级展示。
|
||||
/// </summary>
|
||||
public sealed class CdmTaskSyncer
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SimpleLiteOptions _sl;
|
||||
private readonly Simple3Options _sl;
|
||||
private readonly InternalTokenStore _token;
|
||||
private readonly ILogger<CdmTaskSyncer> _log;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
@@ -53,7 +53,7 @@ public sealed class CdmTaskSyncer
|
||||
public CdmTaskSyncer(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IHttpClientFactory httpFactory,
|
||||
IOptions<SimpleLiteOptions> sl,
|
||||
IOptions<Simple3Options> sl,
|
||||
InternalTokenStore token,
|
||||
ILogger<CdmTaskSyncer> log)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MiGu.Server.Fleet;
|
||||
|
||||
/// <summary>与 SimpleLite GET /projection/fleet/health 行对齐,供车队运维前端消费。</summary>
|
||||
/// <summary>与 Simple3 GET /projection/fleet/health 行对齐,供车队运维前端消费。</summary>
|
||||
public sealed class FleetHealthRowDto
|
||||
{
|
||||
public int CarId { get; set; }
|
||||
|
||||
@@ -7,13 +7,13 @@ using MiGu.Server.Ota;
|
||||
namespace MiGu.Server.Fleet;
|
||||
|
||||
/// <summary>
|
||||
/// 车队健康:保留 SimpleLite 的 CPU/故障率等,延迟改为对 WatchDog(:9776) 做 TCP RTT。
|
||||
/// SimpleLite /fleet/health 探测的是车载 HTTP :8081,多数现场未开该端口会假超时 2000ms。
|
||||
/// 车队健康:保留 Simple3 的 CPU/故障率等,延迟改为对 WatchDog(:9776) 做 TCP RTT。
|
||||
/// Simple3 /fleet/health 探测的是车载 HTTP :8081,多数现场未开该端口会假超时 2000ms。
|
||||
/// </summary>
|
||||
public sealed class FleetHealthService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SimpleLiteOptions _sl;
|
||||
private readonly Simple3Options _sl;
|
||||
private readonly InternalTokenStore _token;
|
||||
private readonly WatchDogClient _wd;
|
||||
private readonly ILogger<FleetHealthService> _log;
|
||||
@@ -25,7 +25,7 @@ public sealed class FleetHealthService
|
||||
|
||||
public FleetHealthService(
|
||||
IHttpClientFactory httpFactory,
|
||||
IOptions<SimpleLiteOptions> sl,
|
||||
IOptions<Simple3Options> sl,
|
||||
InternalTokenStore token,
|
||||
WatchDogClient wd,
|
||||
ILogger<FleetHealthService> log)
|
||||
@@ -39,7 +39,7 @@ public sealed class FleetHealthService
|
||||
|
||||
public async Task<List<FleetHealthRowDto>> GetAsync(CancellationToken ct)
|
||||
{
|
||||
var rows = await FetchSimpleLiteHealthAsync(ct);
|
||||
var rows = await FetchSimple3HealthAsync(ct);
|
||||
if (rows.Count == 0)
|
||||
rows = await BuildRowsFromCarsAsync(ct);
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class FleetHealthService
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(row.Ip))
|
||||
{
|
||||
// 无 IP 时保留 SimpleLite 原探测结果
|
||||
// 无 IP 时保留 Simple3 原探测结果
|
||||
row.LatencySource ??= row.LatencyMs != null ? "onboard" : "none";
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public sealed class FleetHealthService
|
||||
}
|
||||
else
|
||||
{
|
||||
// WatchDog 不通:若 SimpleLite 车载探测成功则保留,否则标不可达
|
||||
// WatchDog 不通:若 Simple3 车载探测成功则保留,否则标不可达
|
||||
if (row.Reachable == true && row.LatencyMs is > 0 and < 2000)
|
||||
{
|
||||
row.LatencySource = "onboard";
|
||||
@@ -93,7 +93,7 @@ public sealed class FleetHealthService
|
||||
return rows;
|
||||
}
|
||||
|
||||
private async Task<List<FleetHealthRowDto>> FetchSimpleLiteHealthAsync(CancellationToken ct)
|
||||
private async Task<List<FleetHealthRowDto>> FetchSimple3HealthAsync(CancellationToken ct)
|
||||
{
|
||||
var port = _sl.ProjectionPort > 0 ? _sl.ProjectionPort : 8222;
|
||||
try
|
||||
@@ -114,7 +114,7 @@ public sealed class FleetHealthService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "fleet/health from SimpleLite failed");
|
||||
_log.LogDebug(ex, "fleet/health from Simple3 failed");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
+19
-17
@@ -4,45 +4,46 @@ using Microsoft.Extensions.Logging;
|
||||
namespace MiGu.Server.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// 将 <c>obj/Debug</c> 下最新编译的 SimpleLite 同步到 <c>bin/Debug</c>(仅当 SimpleLite 未运行时)。
|
||||
/// 将 <c>obj/Debug</c> 下最新编译的 Simple3 同步到 <c>bin/Debug</c>(仅当 Simple3 未运行时)。
|
||||
/// </summary>
|
||||
public static class SimpleLiteBuildSync
|
||||
public static class Simple3BuildSync
|
||||
{
|
||||
public static bool TrySyncFromObjToBin(string contentRoot, ILogger? log = null)
|
||||
{
|
||||
if (!TryResolvePaths(contentRoot, out var objDll, out var objExe, out var binDir))
|
||||
return false;
|
||||
|
||||
var binDll = Path.Combine(binDir, "SimpleLite.dll");
|
||||
var binExe = Path.Combine(binDir, "SimpleLite.exe");
|
||||
var binDll = Path.Combine(binDir, "Simple3.dll");
|
||||
var binExe = Path.Combine(binDir, "Simple3.exe");
|
||||
|
||||
if (!File.Exists(objDll))
|
||||
{
|
||||
log?.LogDebug("[SimpleLiteBuildSync] obj DLL 不存在: {Path}", objDll);
|
||||
log?.LogDebug("[Simple3BuildSync] obj DLL 不存在: {Path}", objDll);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Process.GetProcessesByName("SimpleLite").Any(p => !p.HasExited))
|
||||
if (Process.GetProcessesByName("Simple3").Any(p => !p.HasExited)
|
||||
|| Process.GetProcessesByName("SimpleLite").Any(p => !p.HasExited))
|
||||
{
|
||||
log?.LogWarning("[SimpleLiteBuildSync] SimpleLite 仍在运行,跳过 DLL 同步。请先关闭 SimpleLite 窗口。");
|
||||
log?.LogWarning("[Simple3BuildSync] Simple3 仍在运行,跳过 DLL 同步。请先关闭内核窗口。");
|
||||
return false;
|
||||
}
|
||||
|
||||
var objTime = File.GetLastWriteTimeUtc(objDll);
|
||||
if (File.Exists(binDll) && File.GetLastWriteTimeUtc(binDll) >= objTime)
|
||||
{
|
||||
log?.LogDebug("[SimpleLiteBuildSync] bin 已是最新,无需同步");
|
||||
log?.LogDebug("[Simple3BuildSync] bin 已是最新,无需同步");
|
||||
return false;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(binDir);
|
||||
File.Copy(objDll, binDll, true);
|
||||
log?.LogInformation("[SimpleLiteBuildSync] 已同步 {Src} → {Dst}", objDll, binDll);
|
||||
log?.LogInformation("[Simple3BuildSync] 已同步 {Src} → {Dst}", objDll, binDll);
|
||||
|
||||
if (File.Exists(objExe))
|
||||
{
|
||||
File.Copy(objExe, binExe, true);
|
||||
log?.LogInformation("[SimpleLiteBuildSync] 已同步 {Src} → {Dst}", objExe, binExe);
|
||||
log?.LogInformation("[Simple3BuildSync] 已同步 {Src} → {Dst}", objExe, binExe);
|
||||
}
|
||||
|
||||
SyncRuntimeDeps(objDll, binDir, log);
|
||||
@@ -51,7 +52,7 @@ public static class SimpleLiteBuildSync
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 探测 SimpleLite 是否带「前往站点」路由(区分新旧 DLL)。
|
||||
/// 探测 Simple3 是否带「前往站点」路由(区分新旧 DLL)。
|
||||
/// 必须用不存在的对象 id(-1):路由存在时后端进 handler 找不到对象,返回 JSON
|
||||
/// <c>success:false</c>(无任何副作用);路由不存在时 EmbedIO 返回 HTML 404。
|
||||
/// 早期版本误用 car/0 + siteId=0 —— 一旦场景里真有 id=0 的车和站点,每次健康
|
||||
@@ -96,7 +97,7 @@ public static class SimpleLiteBuildSync
|
||||
|
||||
var dst = Path.Combine(binDir, name);
|
||||
File.Copy(src, dst, true);
|
||||
log?.LogInformation("[SimpleLiteBuildSync] 已同步依赖 {Name}", name);
|
||||
log?.LogInformation("[Simple3BuildSync] 已同步依赖 {Name}", name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +107,11 @@ public static class SimpleLiteBuildSync
|
||||
binDir = "";
|
||||
var repo = FindRepoRoot(contentRoot);
|
||||
if (repo == null) return false;
|
||||
var sl = Path.Combine(repo, "Simple", "SimpleLite");
|
||||
objDll = Path.Combine(sl, "obj", "Debug", "SimpleLite.dll");
|
||||
objExe = Path.Combine(sl, "obj", "Debug", "SimpleLite.exe");
|
||||
var sl = Path.Combine(repo, "Simple", "Simple3");
|
||||
objDll = Path.Combine(sl, "obj", "Debug", "Simple3.dll");
|
||||
objExe = Path.Combine(sl, "obj", "Debug", "Simple3.exe");
|
||||
binDir = Path.Combine(sl, "bin", "Debug");
|
||||
return true;
|
||||
return Directory.Exists(sl);
|
||||
}
|
||||
|
||||
private static string? FindRepoRoot(string contentRoot)
|
||||
@@ -118,7 +119,8 @@ public static class SimpleLiteBuildSync
|
||||
var dir = new DirectoryInfo(contentRoot);
|
||||
while (dir != null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(dir.FullName, "Simple", "SimpleLite")))
|
||||
if (Directory.Exists(Path.Combine(dir.FullName, "Simple", "Simple3"))
|
||||
|| Directory.Exists(Path.Combine(dir.FullName, "Simple", "SimpleLite")))
|
||||
return dir.FullName;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
+128
-109
@@ -7,21 +7,21 @@ using Microsoft.Extensions.Options;
|
||||
namespace MiGu.Server.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// 平台登录成功后按 LaunchMode 把 SimpleLite.exe 作为子进程拉起。
|
||||
/// 平台登录成功后按 LaunchMode 把 Simple3.exe 作为子进程拉起。
|
||||
///
|
||||
/// 设计要点(与原 <c>SimpleLite/Platform/PlatformLauncher.cs</c> 镜像):
|
||||
/// 设计要点(与原 <c>Simple3/Platform/PlatformLauncher.cs</c> 镜像):
|
||||
/// - 幂等:第一次登录拉起,后续登录复用同一个子进程(不会重复启)。
|
||||
/// - 子进程退出后下一次登录可以重新拉起(不阻塞用户重试)。
|
||||
/// - 默认独立(<see cref="SimpleLiteOptions.FollowParent"/> = false):SimpleLite 与 MiGu.Server 互不影响,
|
||||
/// - 默认独立(<see cref="Simple3Options.FollowParent"/> = false):Simple3 与 MiGu.Server 互不影响,
|
||||
/// 关闭任一方不会 kill 另一方;Windows 上用 <c>cmd /c start</c> 脱离父进程组/控制台。
|
||||
/// - 可选跟随(FollowParent = true):Windows JobObject 绑定,MiGu.Server 退出时一并结束 SimpleLite。
|
||||
/// - 命令行透传:把 LaunchMode 翻成 SimpleLite 的 <c>--display-mode=web</c> / <c>--display-mode=web+local</c>。
|
||||
/// - 启动后阻塞等待 SimpleLite Projection :8222 端口可达(最长 ReadinessTimeoutMs),让前端 /api/sl/* 不再立刻 502。
|
||||
/// - 可选跟随(FollowParent = true):Windows JobObject 绑定,MiGu.Server 退出时一并结束 Simple3。
|
||||
/// - 命令行透传:把 LaunchMode 翻成 Simple3 的 <c>--display-mode=web</c> / <c>--display-mode=web+local</c>。
|
||||
/// - 启动后阻塞等待 Simple3 Projection :8222 端口可达(最长 ReadinessTimeoutMs),让前端 /api/sl/* 不再立刻 502。
|
||||
/// </summary>
|
||||
public sealed class SimpleLiteLauncher : IDisposable
|
||||
public sealed class Simple3Launcher : IDisposable
|
||||
{
|
||||
private readonly SimpleLiteOptions _opts;
|
||||
private readonly ILogger<SimpleLiteLauncher> _log;
|
||||
private readonly Simple3Options _opts;
|
||||
private readonly ILogger<Simple3Launcher> _log;
|
||||
private readonly IHostEnvironment _env;
|
||||
private readonly object _sync = new();
|
||||
|
||||
@@ -30,13 +30,13 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
private string? _lastLaunchMode;
|
||||
private bool _disposed;
|
||||
|
||||
public SimpleLiteLauncher(IOptions<SimpleLiteOptions> opts, ILogger<SimpleLiteLauncher> log, IHostEnvironment env)
|
||||
public Simple3Launcher(IOptions<Simple3Options> opts, ILogger<Simple3Launcher> log, IHostEnvironment env)
|
||||
{
|
||||
_opts = opts.Value;
|
||||
_log = log;
|
||||
_env = env;
|
||||
// 会话 N+2:默认不再在 ProcessExit 时清理子进程 —— SimpleLite 是「独立程序」,MiGu.Server 关掉
|
||||
// 不应该带走 SimpleLite。仅当用户显式 opt-in FollowParent=true 时才挂软关闭兜底。
|
||||
// 会话 N+2:默认不再在 ProcessExit 时清理子进程 —— Simple3 是「独立程序」,MiGu.Server 关掉
|
||||
// 不应该带走 Simple3。仅当用户显式 opt-in FollowParent=true 时才挂软关闭兜底。
|
||||
if (_opts.FollowParent)
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
|
||||
@@ -53,11 +53,11 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
public string? LastLaunchMode { get { lock (_sync) return _lastLaunchMode; } }
|
||||
|
||||
/// <summary>外部已存在的 SimpleLite(MiGu.Server 重启复用上一轮实例)占位 LaunchMode 值,不参与命令行 displayMode 翻译。</summary>
|
||||
/// <summary>外部已存在的 Simple3(MiGu.Server 重启复用上一轮实例)占位 LaunchMode 值,不参与命令行 displayMode 翻译。</summary>
|
||||
internal const string ExternalReuseLaunchMode = "external";
|
||||
|
||||
/// <summary>
|
||||
/// 按 launchMode 拉起 SimpleLite(已运行则跳过)。
|
||||
/// 按 launchMode 拉起 Simple3(已运行则跳过)。
|
||||
/// </summary>
|
||||
/// <param name="launchMode">"WebOnly" 或 "DesktopAndWeb"(大小写不敏感)。</param>
|
||||
/// <returns>本次调用产生的状态摘要,可写入登录响应或日志。</returns>
|
||||
@@ -67,20 +67,20 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
if (!_opts.Enabled)
|
||||
{
|
||||
_log.LogInformation("[SimpleLite] auto-start disabled (appsettings: SimpleLite.Enabled=false); launchMode={Mode} ignored", launchMode);
|
||||
return new LaunchResult(false, "Disabled", "appsettings:SimpleLite:Enabled=false", DisplayMode: null,
|
||||
Warning: "SimpleLite 自动拉起已被 appsettings:SimpleLite:Enabled=false 关闭;登录已成功但 SimpleLite 未启动,/api/sl/* 反代请求会返回 502。");
|
||||
_log.LogInformation("[Simple3] auto-start disabled (appsettings: Simple3.Enabled=false); launchMode={Mode} ignored", launchMode);
|
||||
return new LaunchResult(false, "Disabled", "appsettings:Simple3:Enabled=false", DisplayMode: null,
|
||||
Warning: "Simple3 自动拉起已被 appsettings:Simple3:Enabled=false 关闭;登录已成功但 Simple3 未启动,/api/sl/* 反代请求会返回 502。");
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_proc is { HasExited: false })
|
||||
{
|
||||
_log.LogInformation("[SimpleLite] already running pid={Pid}, displayMode={DisplayMode}; skip duplicate launch", _proc.Id, _lastLaunchMode);
|
||||
_log.LogInformation("[Simple3] already running pid={Pid}, displayMode={DisplayMode}; skip duplicate launch", _proc.Id, _lastLaunchMode);
|
||||
var warning = !string.IsNullOrEmpty(_lastLaunchMode) &&
|
||||
!string.Equals(_lastLaunchMode, displayMode, StringComparison.OrdinalIgnoreCase)
|
||||
? $"SimpleLite 已经在运行(pid={_proc.Id}, displayMode={_lastLaunchMode})。" +
|
||||
$"本次选择的启动模式『{launchMode}』未被应用;如需切换,请手动关闭旧 SimpleLite 后重新登录。"
|
||||
? $"Simple3 已经在运行(pid={_proc.Id}, displayMode={_lastLaunchMode})。" +
|
||||
$"本次选择的启动模式『{launchMode}』未被应用;如需切换,请手动关闭旧 Simple3 后重新登录。"
|
||||
: null;
|
||||
return new LaunchResult(true, "AlreadyRunning",
|
||||
$"pid={_proc.Id}, displayMode={_lastLaunchMode}",
|
||||
@@ -88,44 +88,44 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
Warning: warning);
|
||||
}
|
||||
|
||||
// 会话 N+2:MiGu.Server 重启后 _proc 引用丢失,但上一轮拉起的 SimpleLite 可能仍在跑(因为
|
||||
// 会话 N+2:MiGu.Server 重启后 _proc 引用丢失,但上一轮拉起的 Simple3 可能仍在跑(因为
|
||||
// 默认 FollowParent=false 不带走它)。这里在拉起前先探测 Projection 端口:能连通就视为复用,
|
||||
// 避免「allowMultiple=false 时新 SimpleLite 检测到多开自杀」+「端口冲突」两类常见崩溃。
|
||||
// 避免「allowMultiple=false 时新 Simple3 检测到多开自杀」+「端口冲突」两类常见崩溃。
|
||||
//
|
||||
// 注意:探测仅判断「有 SimpleLite 在 8222 占着」,无法知道它当时选的 LaunchMode。如果用户本次
|
||||
// 想换模式,这条路径下不会生效;日志里会明确告知,由用户决定是否手动关掉旧 SimpleLite 再登录。
|
||||
// 注意:探测仅判断「有 Simple3 在 8222 占着」,无法知道它当时选的 LaunchMode。如果用户本次
|
||||
// 想换模式,这条路径下不会生效;日志里会明确告知,由用户决定是否手动关掉旧 Simple3 再登录。
|
||||
//
|
||||
// 增强(A3):先做 TCP 探活(快),再做 HTTP JSON 探针验证「真的是 SimpleLite」,避免被某个偶然占
|
||||
// 增强(A3):先做 TCP 探活(快),再做 HTTP JSON 探针验证「真的是 Simple3」,避免被某个偶然占
|
||||
// 用 8222 的无关进程误判成复用。Probe 失败时不再当作复用成功,否则前端会拿到假的 WebEnabled。
|
||||
if (TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(500)))
|
||||
{
|
||||
var probeOk = ProbeSimpleLiteHttp("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(800));
|
||||
var probeOk = ProbeSimple3Http("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(800));
|
||||
if (!probeOk)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"[SimpleLite] projection port :{Port} is occupied, but SimpleLite HTTP probe failed. Skip launch to avoid port conflict.",
|
||||
"[Simple3] projection port :{Port} is occupied, but Simple3 HTTP probe failed. Skip launch to avoid port conflict.",
|
||||
_opts.ProjectionPort);
|
||||
return new LaunchResult(false, "PortOccupied",
|
||||
$"projection :{_opts.ProjectionPort} tcp reachable but /projection/cars probe failed",
|
||||
DisplayMode: null,
|
||||
Warning: $"端口 :{_opts.ProjectionPort} 已被占用,但未识别为 SimpleLite Projection 服务。" +
|
||||
"请关闭占用该端口的进程,或调整 SimpleLite:ProjectionPort / SimpleLite 配置后重试。");
|
||||
Warning: $"端口 :{_opts.ProjectionPort} 已被占用,但未识别为 Simple3 Projection 服务。" +
|
||||
"请关闭占用该端口的进程,或调整 Simple3:ProjectionPort / Simple3 配置后重试。");
|
||||
}
|
||||
|
||||
_log.LogInformation(
|
||||
"[SimpleLite] projection :{Port} already reachable (httpProbe=ok); assume an existing SimpleLite is running. " +
|
||||
"Skip launch. If user picked a different LaunchMode this session, please close the existing SimpleLite window and login again.",
|
||||
"[Simple3] projection :{Port} already reachable (httpProbe=ok); assume an existing Simple3 is running. " +
|
||||
"Skip launch. If user picked a different LaunchMode this session, please close the existing Simple3 window and login again.",
|
||||
_opts.ProjectionPort);
|
||||
|
||||
// A2 修复:保留一个占位 LaunchMode 让 LastLaunchMode 不再为 null —— 这样 SwitchScope
|
||||
// 推断 runMode 时不会落到 "web+local" 兜底,前端 RunMode 角标也不会与实际不符。
|
||||
_lastLaunchMode = ExternalReuseLaunchMode;
|
||||
|
||||
var reuseWarning = $"检测到 SimpleLite 已经在 :{_opts.ProjectionPort} 上运行(可能是 MiGu.Server 重启前残留)。" +
|
||||
$"本次选择的启动模式『{launchMode}』未被应用到既有实例。如需切换,请手动关闭旧 SimpleLite 窗口后重新登录。";
|
||||
if (SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort) == false)
|
||||
var reuseWarning = $"检测到 Simple3 已经在 :{_opts.ProjectionPort} 上运行(可能是 MiGu.Server 重启前残留)。" +
|
||||
$"本次选择的启动模式『{launchMode}』未被应用到既有实例。如需切换,请手动关闭旧 Simple3 窗口后重新登录。";
|
||||
if (Simple3BuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort) == false)
|
||||
{
|
||||
reuseWarning += " 当前 SimpleLite 版本过旧,缺少「前往站点」API;请关闭 SimpleLite 窗口后调用 POST /api/health/simplelite/restart-for-update,或运行 scripts/redeploy-simplelite.ps1。";
|
||||
reuseWarning += " 当前 Simple3 版本过旧,缺少「前往站点」API;请关闭 Simple3 窗口后调用 POST /api/health/simple3/restart-for-update,或运行 scripts/redeploy-simple3.ps1。";
|
||||
}
|
||||
|
||||
return new LaunchResult(true, "ReusingExisting",
|
||||
@@ -134,18 +134,18 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
Warning: reuseWarning);
|
||||
}
|
||||
|
||||
SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||
Simple3BuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||
|
||||
var resolved = ResolveExecutable(_opts.ExecutablePath);
|
||||
if (resolved == null)
|
||||
{
|
||||
_log.LogWarning(
|
||||
"[SimpleLite] auto-start skipped: SimpleLite.exe not found. Configure appsettings:SimpleLite:ExecutablePath or place SimpleLite next to MiGu.Server. ContentRoot={Root}",
|
||||
"[Simple3] auto-start skipped: Simple3.exe not found. Configure appsettings:Simple3:ExecutablePath or place Simple3 next to MiGu.Server. ContentRoot={Root}",
|
||||
_env.ContentRootPath);
|
||||
return new LaunchResult(false, "ExecutableNotFound",
|
||||
"Set appsettings:SimpleLite:ExecutablePath or place SimpleLite next to MiGu.Server.",
|
||||
"Set appsettings:Simple3:ExecutablePath or place Simple3 next to MiGu.Server.",
|
||||
DisplayMode: null,
|
||||
Warning: "找不到 SimpleLite.exe。请在 appsettings:SimpleLite:ExecutablePath 显式配置,或者把 SimpleLite.exe 放到 MiGu.Server 同目录。");
|
||||
Warning: "找不到 Simple3.exe。请在 appsettings:Simple3:ExecutablePath 显式配置,或者把 Simple3.exe 放到 MiGu.Server 同目录。");
|
||||
}
|
||||
|
||||
var workdir = string.IsNullOrWhiteSpace(_opts.WorkingDirectory)
|
||||
@@ -156,12 +156,12 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
var proc = StartSimpleLiteProcess(resolved, arguments, workdir);
|
||||
var proc = StartSimple3Process(resolved, arguments, workdir);
|
||||
if (proc == null)
|
||||
{
|
||||
_log.LogError("[SimpleLite] Process.Start returned null; exe={Exe} args={Args}", resolved, arguments);
|
||||
_log.LogError("[Simple3] Process.Start returned null; exe={Exe} args={Args}", resolved, arguments);
|
||||
return new LaunchResult(false, "ProcessStartFailed", $"exe={resolved}", DisplayMode: null,
|
||||
Warning: $"启动 SimpleLite 进程失败;exe={resolved}");
|
||||
Warning: $"启动 Simple3 进程失败;exe={resolved}");
|
||||
}
|
||||
|
||||
WireProcessExitHandler(proc);
|
||||
@@ -171,20 +171,20 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
if (_opts.FollowParent) AttachToJobObject(proc);
|
||||
|
||||
_log.LogInformation("[SimpleLite] launched pid={Pid} displayMode={Mode} exe={Exe} args=\"{Args}\" workdir={Workdir}; standalone={Standalone}",
|
||||
_log.LogInformation("[Simple3] launched pid={Pid} displayMode={Mode} exe={Exe} args=\"{Args}\" workdir={Workdir}; standalone={Standalone}",
|
||||
proc.Id, displayMode, resolved, arguments, workdir, !_opts.FollowParent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "[SimpleLite] auto-start failed");
|
||||
_log.LogError(ex, "[Simple3] auto-start failed");
|
||||
return new LaunchResult(false, "Exception", ex.Message, DisplayMode: null,
|
||||
Warning: $"启动 SimpleLite 时抛异常:{ex.GetType().Name}: {ex.Message}");
|
||||
Warning: $"启动 Simple3 时抛异常:{ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// M1:登录路径传 waitForReady=false —— 进程拉起后立即返回,不再同步阻塞最长
|
||||
// ReadinessTimeoutMs 等端口就绪(避免冷启动登录干等十几秒)。前端可轮询
|
||||
// GET /api/health/simplelite 获知就绪状态。
|
||||
// GET /api/health/simple3 获知就绪状态。
|
||||
if (!waitForReady)
|
||||
{
|
||||
return new LaunchResult(true, "Starting",
|
||||
@@ -200,12 +200,12 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
DisplayMode: displayMode,
|
||||
Warning: ready
|
||||
? null
|
||||
: $"SimpleLite 进程已起,但 Projection :{_opts.ProjectionPort} 在 {_opts.ReadinessTimeoutMs}ms 内未响应。前端 /api/sl/* 可能短暂 502;可稍后刷新。");
|
||||
: $"Simple3 进程已起,但 Projection :{_opts.ProjectionPort} 在 {_opts.ReadinessTimeoutMs}ms 内未响应。前端 /api/sl/* 可能短暂 502;可稍后刷新。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动关闭 SimpleLite 子进程。
|
||||
/// 会话 N+2 起默认不调(FollowParent=false)—— SimpleLite 是独立程序,MiGu.Server 关闭不带走它。
|
||||
/// 手动关闭 Simple3 子进程。
|
||||
/// 会话 N+2 起默认不调(FollowParent=false)—— Simple3 是独立程序,MiGu.Server 关闭不带走它。
|
||||
/// 仅当用户显式 opt-in FollowParent=true 时由 ProcessExit / ApplicationStopping 钩子调用。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
@@ -227,7 +227,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
// 独立模式:不杀子进程,仅释放 MiGu.Server 侧 Process 句柄。
|
||||
if (proc is { HasExited: false })
|
||||
{
|
||||
_log.LogInformation("[SimpleLite] standalone mode: MiGu.Server stopping — SimpleLite pid={Pid} keeps running (FollowParent=false)", proc.Id);
|
||||
_log.LogInformation("[Simple3] standalone mode: MiGu.Server stopping — Simple3 pid={Pid} keeps running (FollowParent=false)", proc.Id);
|
||||
}
|
||||
try { proc?.Dispose(); } catch { /* ignore */ }
|
||||
return;
|
||||
@@ -236,7 +236,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
if (proc is { HasExited: false })
|
||||
{
|
||||
try { proc.Kill(entireProcessTree: true); }
|
||||
catch (Exception ex) { _log.LogWarning("[SimpleLite] kill failed: {Msg}", ex.Message); }
|
||||
catch (Exception ex) { _log.LogWarning("[Simple3] kill failed: {Msg}", ex.Message); }
|
||||
try { proc.WaitForExit(2000); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -248,21 +248,21 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
private static string NormalizeDisplayMode(string launchMode)
|
||||
{
|
||||
// LaunchMode 是「业务语言」(WebOnly / DesktopAndWeb);displayMode 是 SimpleLite「内部语言」(web / web+local)。
|
||||
// LaunchMode 是「业务语言」(WebOnly / DesktopAndWeb);displayMode 是 Simple3「内部语言」(web / web+local)。
|
||||
// 这里做一次显式翻译,前端 / 后端日志均用业务语言,命令行用内部语言。
|
||||
return launchMode?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"webonly" or "web" or "web-only" => "web",
|
||||
// 任何未知值都按"完整本地+web"兜底,保证最小惊讶(既能桌面用,也能浏览器用)。
|
||||
_ => "web+local",
|
||||
"desktopandweb" or "web+local" or "weblocal" or "local" => "web+local",
|
||||
// Simple3 无本地桌面端:默认 / 未知一律 web
|
||||
_ => "web",
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildArguments(string displayMode, string extra)
|
||||
{
|
||||
var args = $"--display-mode={displayMode}";
|
||||
// 迷榖嵌入:平台 iframe 场景默认带 --migu,让 SimpleLite WebTerminal 默认纯画布(隐藏 ImGui panel),
|
||||
// 业务 UI 由 Vue 平台前端接管;declare 时序失败时也安全回退到画布。可用 appsettings:SimpleLite:EmbeddedCanvas=false 关闭。
|
||||
// 迷榖嵌入:平台 iframe 场景默认带 --migu,让 Simple3 WebTerminal 默认纯画布(隐藏 ImGui panel),
|
||||
// 业务 UI 由 Vue 平台前端接管;declare 时序失败时也安全回退到画布。可用 appsettings:Simple3:EmbeddedCanvas=false 关闭。
|
||||
if (_opts.EmbeddedCanvas) args += " --migu";
|
||||
// 选择性加载:把平台写入的 plugins/active-scenes.json 同步透传为 --scenes(命令行优先级最高,与文件一致,双保险)。
|
||||
var sceneArg = ReadActiveScenesArg();
|
||||
@@ -271,7 +271,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>解析 SimpleLite 工作目录(与拉起时一致):优先显式 <see cref="SimpleLiteOptions.WorkingDirectory"/>,
|
||||
/// <summary>解析 Simple3 工作目录(与拉起时一致):优先显式 <see cref="Simple3Options.WorkingDirectory"/>,
|
||||
/// 否则取解析到的 exe 所在目录。两者都拿不到返回 null。</summary>
|
||||
public string? ResolveWorkingDirectory()
|
||||
{
|
||||
@@ -281,7 +281,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
return resolved == null ? null : (Path.GetDirectoryName(resolved) ?? Environment.CurrentDirectory);
|
||||
}
|
||||
|
||||
/// <summary>SimpleLite 的 plugins 目录(工作目录/plugins);定位不到工作目录时返回 null。</summary>
|
||||
/// <summary>Simple3 的 plugins 目录(工作目录/plugins);定位不到工作目录时返回 null。</summary>
|
||||
public string? ResolvePluginsDir()
|
||||
{
|
||||
var wd = ResolveWorkingDirectory();
|
||||
@@ -289,16 +289,16 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把「配置向导选定的导航场景」写入 SimpleLite 的 <c>plugins/active-scenes.json</c> ——
|
||||
/// 这是「平台 → 内核」选择性加载的主通道。下次 SimpleLite 启动即据此只加载选定导航场景插件;
|
||||
/// 已在运行的实例需重启或调 <c>POST /projection/scenes/apply</c> 才生效。字段名与 SimpleLite 端
|
||||
/// 把「配置向导选定的导航场景」写入 Simple3 的 <c>plugins/active-scenes.json</c> ——
|
||||
/// 这是「平台 → 内核」选择性加载的主通道。下次 Simple3 启动即据此只加载选定导航场景插件;
|
||||
/// 已在运行的实例需重启或调 <c>POST /projection/scenes/apply</c> 才生效。字段名与 Simple3 端
|
||||
/// <c>ActiveScenesConfig</c> 对齐(activeScenes / alwaysLoad / source / updatedAt)。
|
||||
/// </summary>
|
||||
public ActiveScenesWriteResult WriteActiveScenes(IEnumerable<string> activeScenes, IEnumerable<string>? alwaysLoad, string source)
|
||||
{
|
||||
var pluginsDir = ResolvePluginsDir();
|
||||
if (pluginsDir == null)
|
||||
return new ActiveScenesWriteResult(false, null, "未找到 SimpleLite 工作目录/可执行文件,无法定位 plugins 目录");
|
||||
return new ActiveScenesWriteResult(false, null, "未找到 Simple3 工作目录/可执行文件,无法定位 plugins 目录");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(pluginsDir);
|
||||
@@ -313,13 +313,13 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
updatedAt = DateTime.UtcNow
|
||||
};
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
|
||||
_log.LogInformation("[SimpleLite] active-scenes.json 写入 {Path}: active=[{Scenes}]",
|
||||
_log.LogInformation("[Simple3] active-scenes.json 写入 {Path}: active=[{Scenes}]",
|
||||
path, string.Join(",", payload.activeScenes));
|
||||
return new ActiveScenesWriteResult(true, path, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning(ex, "[SimpleLite] 写 active-scenes.json 失败");
|
||||
_log.LogWarning(ex, "[Simple3] 写 active-scenes.json 失败");
|
||||
return new ActiveScenesWriteResult(false, null, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -349,11 +349,12 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
/// <summary>写 active-scenes.json 的结果(供向导保存接口回显)。</summary>
|
||||
public readonly record struct ActiveScenesWriteResult(bool Ok, string? Path, string? Error);
|
||||
|
||||
/// <summary>终止本机全部 SimpleLite 进程并清理 MiGu.Server 侧托管引用。</summary>
|
||||
/// <summary>终止本机全部 Simple3(及旧名 SimpleLite)进程并清理 MiGu.Server 侧托管引用。</summary>
|
||||
public int StopAll()
|
||||
{
|
||||
var killed = 0;
|
||||
foreach (var proc in Process.GetProcessesByName("SimpleLite"))
|
||||
foreach (var name in new[] { "Simple3", "SimpleLite" })
|
||||
foreach (var proc in Process.GetProcessesByName(name))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -361,12 +362,12 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
proc.Kill(entireProcessTree: true);
|
||||
killed++;
|
||||
_log.LogInformation("[SimpleLite] stop: killed pid={Pid}", proc.Id);
|
||||
_log.LogInformation("[Simple3] stop: killed {Name} pid={Pid}", name, proc.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning("[SimpleLite] stop: kill pid={Pid} failed: {Msg}", proc.Id, ex.Message);
|
||||
_log.LogWarning("[Simple3] stop: kill {Name} pid={Pid} failed: {Msg}", name, proc.Id, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -385,7 +386,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
return killed;
|
||||
}
|
||||
|
||||
/// <summary>关闭全部 SimpleLite 后按 launchMode 重新拉起(不同步 DLL)。</summary>
|
||||
/// <summary>关闭全部 Simple3 后按 launchMode 重新拉起(不同步 DLL)。</summary>
|
||||
public LaunchResult Restart(string launchMode = "webonly")
|
||||
{
|
||||
StopAll();
|
||||
@@ -393,19 +394,19 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭所有 SimpleLite 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
|
||||
/// 关闭所有 Simple3 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
|
||||
/// </summary>
|
||||
public LaunchResult RestartForUpdate(string launchMode = "webonly")
|
||||
{
|
||||
StopAll();
|
||||
|
||||
var synced = SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||
var synced = Simple3BuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||
var result = MaybeStart(launchMode);
|
||||
if (!synced && result.Warning == null)
|
||||
{
|
||||
return result with
|
||||
{
|
||||
Warning = "未能从 obj/Debug 同步 DLL(可能未编译或 SimpleLite 仍占用文件)。请先运行 scripts/redeploy-simplelite.ps1。"
|
||||
Warning = "未能从 obj/Debug 同步 DLL(可能未编译或 Simple3 仍占用文件)。请先运行 scripts/redeploy-simple3.ps1。"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -416,12 +417,12 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
private DateTimeOffset _gotoSiteProbeAt = DateTimeOffset.MinValue;
|
||||
private static readonly TimeSpan GotoSiteProbeTtl = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>启动前诊断:当前配置、解析到的 exe、端口占用等(供 /api/health/simplelite 与启动日志)。</summary>
|
||||
public SimpleLiteDiagnostics GetDiagnostics()
|
||||
/// <summary>启动前诊断:当前配置、解析到的 exe、端口占用等(供 /api/health/simple3 与启动日志)。</summary>
|
||||
public Simple3Diagnostics GetDiagnostics()
|
||||
{
|
||||
var resolved = ResolveExecutable(_opts.ExecutablePath);
|
||||
var projectionUp = TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(400));
|
||||
// 探测结果缓存 60s:诊断端点可能被前端轮询,避免每次都对 SimpleLite 发探测请求。
|
||||
// 探测结果缓存 60s:诊断端点可能被前端轮询,避免每次都对 Simple3 发探测请求。
|
||||
bool? gotoSite = null;
|
||||
if (projectionUp)
|
||||
{
|
||||
@@ -431,7 +432,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
gotoSite = SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort);
|
||||
gotoSite = Simple3BuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort);
|
||||
if (gotoSite is not null)
|
||||
{
|
||||
_gotoSiteProbeCache = gotoSite;
|
||||
@@ -440,9 +441,9 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
}
|
||||
var deployHint = gotoSite == false
|
||||
? "关闭 SimpleLite 后:运行 Migu2.0/scripts/redeploy-simplelite.ps1,或 POST /api/health/simplelite/restart-for-update"
|
||||
? "关闭 Simple3 后:运行 Migu2.0/scripts/redeploy-simple3.ps1,或 POST /api/health/simple3/restart-for-update"
|
||||
: null;
|
||||
return new SimpleLiteDiagnostics(
|
||||
return new Simple3Diagnostics(
|
||||
Enabled: _opts.Enabled,
|
||||
FollowParent: _opts.FollowParent,
|
||||
ConfiguredExecutablePath: _opts.ExecutablePath ?? "",
|
||||
@@ -456,26 +457,26 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
ProjectionPortReachable: projectionUp,
|
||||
GotoSiteApiAvailable: gotoSite,
|
||||
DeployHint: deployHint,
|
||||
ConfigHint: "编辑 MiGu.Server/appsettings.json 的 SimpleLite 节点(所有环境生效);开发机可叠加 appsettings.Development.json。" +
|
||||
" FollowParent=false 时关闭 MiGu.Server 不会结束 SimpleLite。");
|
||||
ConfigHint: "编辑 MiGu.Server/appsettings.json 的 Simple3 节点(所有环境生效);开发机可叠加 appsettings.Development.json。" +
|
||||
" FollowParent=false 时关闭 MiGu.Server 不会结束 Simple3。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拉起 SimpleLite。FollowParent=false 时在 Windows 上用 <c>cmd /c start</c> 脱离父进程组,避免平台退出连带结束 SimpleLite。
|
||||
/// 拉起 Simple3。FollowParent=false 时在 Windows 上用 <c>cmd /c start</c> 脱离父进程组,避免平台退出连带结束 Simple3。
|
||||
/// </summary>
|
||||
private Process? StartSimpleLiteProcess(string resolved, string arguments, string workdir)
|
||||
private Process? StartSimple3Process(string resolved, string arguments, string workdir)
|
||||
{
|
||||
if (_opts.FollowParent)
|
||||
return StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
|
||||
return StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
return StartSimpleLiteDetachedWindows(resolved, arguments, workdir)
|
||||
?? StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
|
||||
return StartSimple3DetachedWindows(resolved, arguments, workdir)
|
||||
?? StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
|
||||
|
||||
return StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
|
||||
return StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
|
||||
}
|
||||
|
||||
private static Process? StartSimpleLiteDirect(string resolved, string arguments, string workdir, bool useShellExecute)
|
||||
private static Process? StartSimple3Direct(string resolved, string arguments, string workdir, bool useShellExecute)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
@@ -493,14 +494,15 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
/// <summary>
|
||||
/// 通过 cmd start 在新进程组/新控制台中启动,与 MiGu.Server 控制台 Ctrl+C、进程树结束解耦。
|
||||
/// </summary>
|
||||
private Process? StartSimpleLiteDetachedWindows(string resolved, string arguments, string workdir)
|
||||
private Process? StartSimple3DetachedWindows(string resolved, string arguments, string workdir)
|
||||
{
|
||||
var exeName = Path.GetFileNameWithoutExtension(resolved);
|
||||
var beforeIds = new HashSet<int>(
|
||||
Process.GetProcessesByName(exeName).Select(p => { try { return p.Id; } catch { return -1; } })
|
||||
.Where(id => id > 0));
|
||||
|
||||
var cmdArgs = $"/c start \"SimpleLite\" /D \"{workdir}\" \"{resolved}\" {arguments}";
|
||||
// cmd start 的第一个引号参数是「窗口标题」,不是 exe;写死 Simple3 会让 Simple3 控制台标题错名。
|
||||
var cmdArgs = $"/c start \"{exeName}\" /D \"{workdir}\" \"{resolved}\" {arguments}";
|
||||
var shimPsi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
@@ -521,7 +523,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
if (p.HasExited) continue;
|
||||
if (beforeIds.Contains(p.Id)) continue;
|
||||
_log.LogInformation("[SimpleLite] detached start via cmd.exe; new pid={Pid}", p.Id);
|
||||
_log.LogInformation("[Simple3] detached start via cmd.exe; new pid={Pid}", p.Id);
|
||||
return AttachToExistingProcess(p.Id);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
@@ -529,7 +531,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
_log.LogWarning("[SimpleLite] detached start: no new {Name} process observed after cmd.exe start", exeName);
|
||||
_log.LogWarning("[Simple3] detached start: no new {Name} process observed after cmd.exe start", exeName);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -543,7 +545,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning("[SimpleLite] attach to pid={Pid} failed: {Msg}", pid, ex.Message);
|
||||
_log.LogWarning("[Simple3] attach to pid={Pid} failed: {Msg}", pid, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -554,7 +556,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
int exit;
|
||||
try { exit = proc.ExitCode; } catch { exit = -1; }
|
||||
_log.LogInformation("[SimpleLite] process exited code={Code}; next login will relaunch if needed", exit);
|
||||
_log.LogInformation("[Simple3] process exited code={Code}; next login will relaunch if needed", exit);
|
||||
lock (_sync)
|
||||
{
|
||||
if (ReferenceEquals(_proc, proc))
|
||||
@@ -577,16 +579,26 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
var cwd = _env.ContentRootPath;
|
||||
var staticCandidates = new[]
|
||||
{
|
||||
Path.Combine(cwd, "Simple3.exe"),
|
||||
Path.Combine(cwd, "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "Simple3", "Simple3.exe"),
|
||||
Path.Combine(cwd, "SimpleLite", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "Simple3.exe"),
|
||||
Path.Combine(cwd, "..", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
|
||||
// Migu2.0 与 Simple 并列:Migu2.0/MiGu.Server → ../../Simple/SimpleLite/bin/Debug
|
||||
// 发布布局(本机常见):D:\MDCS\Release\Simple\Simple3
|
||||
@"D:\MDCS\Release\Simple\Simple3\Simple3.exe",
|
||||
// 源码并列:Migu2.0/MiGu.Server → ../../Simple/Simple3(OutputPath 常直接写到 Release)
|
||||
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "Simple3.exe"),
|
||||
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "bin", "Debug", "Simple3.exe"),
|
||||
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "bin", "Release", "Simple3.exe"),
|
||||
Path.Combine(cwd, "..", "Simple", "Simple3", "Simple3.exe"),
|
||||
// 兼容旧 SimpleLite 目录/产物名
|
||||
Path.Combine(cwd, "..", "..", "Simple", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "..", "Simple", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "Simple", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "Simple", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
|
||||
Path.Combine(cwd, "..", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
|
||||
};
|
||||
foreach (var c in staticCandidates)
|
||||
{
|
||||
@@ -599,6 +611,13 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
foreach (var sub in new[]
|
||||
{
|
||||
"Simple3/Simple3.exe",
|
||||
"Simple3/bin/Debug/Simple3.exe",
|
||||
"Simple3/bin/Release/Simple3.exe",
|
||||
"Simple/Simple3/Simple3.exe",
|
||||
"Simple/Simple3/bin/Debug/Simple3.exe",
|
||||
"Simple/Simple3/bin/Release/Simple3.exe",
|
||||
"SimpleLite/SimpleLite.exe",
|
||||
"SimpleLite/bin/Debug/SimpleLite.exe",
|
||||
"SimpleLite/bin/Release/SimpleLite.exe",
|
||||
"Simple/SimpleLite/bin/Debug/SimpleLite.exe",
|
||||
@@ -668,14 +687,14 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
|
||||
if (TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(interval)))
|
||||
{
|
||||
_log.LogInformation("[SimpleLite] projection :{Port} ready", _opts.ProjectionPort);
|
||||
_log.LogInformation("[Simple3] projection :{Port} ready", _opts.ProjectionPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
Thread.Sleep(interval);
|
||||
}
|
||||
|
||||
_log.LogWarning("[SimpleLite] projection :{Port} not ready within {Timeout}ms", _opts.ProjectionPort, _opts.ReadinessTimeoutMs);
|
||||
_log.LogWarning("[Simple3] projection :{Port} not ready within {Timeout}ms", _opts.ProjectionPort, _opts.ReadinessTimeoutMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -694,10 +713,10 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 TCP 通的基础上读取 SimpleLite Projection 的强类型 JSON 端点,判别「真的是 SimpleLite」。
|
||||
/// 在 TCP 通的基础上读取 Simple3 Projection 的强类型 JSON 端点,判别「真的是 Simple3」。
|
||||
/// 只接受 2xx 且响应体像 JSON 数组/对象;任意普通 HTTP 服务占用 8222 不再被误认为可复用。
|
||||
/// </summary>
|
||||
private static bool ProbeSimpleLiteHttp(string host, int port, TimeSpan timeout)
|
||||
private static bool ProbeSimple3Http(string host, int port, TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -718,20 +737,20 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拉起 SimpleLite 的结果摘要。
|
||||
/// 拉起 Simple3 的结果摘要。
|
||||
/// <list type="bullet">
|
||||
/// <item><c>Started</c>:本次调用后是否处于"已运行"状态(包含 AlreadyRunning / ReusingExisting / Ready / StartedButNotReady)。</item>
|
||||
/// <item><c>Status</c>:状态枚举字符串(见上)。</item>
|
||||
/// <item><c>Detail</c>:技术细节,写入服务端日志。</item>
|
||||
/// <item><c>DisplayMode</c>:本次实际生效的 displayMode(web / web+local / external / null)。
|
||||
/// 与 LaunchMode 业务字段区分:external 表示复用了 MiGu.Server 重启前残留的 SimpleLite,对应模式未知。</item>
|
||||
/// 与 LaunchMode 业务字段区分:external 表示复用了 MiGu.Server 重启前残留的 Simple3,对应模式未知。</item>
|
||||
/// <item><c>Warning</c>:透传给前端登录响应的告警文本,null 表示无需告警。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public readonly record struct LaunchResult(bool Started, string Status, string Detail,
|
||||
string? DisplayMode = null, string? Warning = null);
|
||||
|
||||
public sealed record SimpleLiteDiagnostics(
|
||||
public sealed record Simple3Diagnostics(
|
||||
bool Enabled,
|
||||
bool FollowParent,
|
||||
string ConfiguredExecutablePath,
|
||||
@@ -807,7 +826,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
_log.LogInformation("[SimpleLite] JobObject skipped on non-Windows; falling back to ProcessExit-only cleanup");
|
||||
_log.LogInformation("[Simple3] JobObject skipped on non-Windows; falling back to ProcessExit-only cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -818,7 +837,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
_job = CreateJobObject(IntPtr.Zero, null);
|
||||
if (_job == IntPtr.Zero)
|
||||
{
|
||||
_log.LogWarning("[SimpleLite] CreateJobObject failed; child may outlive MiGu.Server");
|
||||
_log.LogWarning("[Simple3] CreateJobObject failed; child may outlive MiGu.Server");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -831,17 +850,17 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
{
|
||||
Marshal.StructureToPtr(info, ptr, false);
|
||||
if (!SetInformationJobObject(_job, JobObjectExtendedLimitInformation, ptr, (uint)len))
|
||||
_log.LogWarning("[SimpleLite] SetInformationJobObject failed; child may outlive MiGu.Server");
|
||||
_log.LogWarning("[Simple3] SetInformationJobObject failed; child may outlive MiGu.Server");
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(ptr); }
|
||||
}
|
||||
|
||||
if (!AssignProcessToJobObject(_job, child.Handle))
|
||||
_log.LogWarning("[SimpleLite] AssignProcessToJobObject failed; child may outlive MiGu.Server");
|
||||
_log.LogWarning("[Simple3] AssignProcessToJobObject failed; child may outlive MiGu.Server");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning("[SimpleLite] JobObject setup error: {Type}: {Msg}", ex.GetType().Name, ex.Message);
|
||||
_log.LogWarning("[Simple3] JobObject setup error: {Type}: {Msg}", ex.GetType().Name, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-23
@@ -1,31 +1,30 @@
|
||||
namespace MiGu.Server.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// MiGu.Server 启动后由「平台登录」按 LaunchMode 拉起的 SimpleLite 子进程配置。
|
||||
/// MiGu.Server 启动后由「平台登录」按 LaunchMode 拉起的 Simple3 子进程配置。
|
||||
///
|
||||
/// 会话 N+1(启动反转):原架构是 SimpleLite 启动 → 拉 MiGu.Server;现在反过来:
|
||||
/// MiGu.Server 作为主入口启动 → 登录页选 LaunchMode → 后端拉起 SimpleLite.exe,
|
||||
/// 通过 <c>--display-mode</c> 参数把 web / web+local 透传给 SimpleLite 的 Configuration。
|
||||
/// 会话 N+1(启动反转):原架构是 Simple3 启动 → 拉 MiGu.Server;现在反过来:
|
||||
/// MiGu.Server 作为主入口启动 → 登录页选 LaunchMode → 后端拉起 Simple3.exe,
|
||||
/// 通过 <c>--display-mode</c> 参数把 web / web+local 透传给 Simple3 的 Configuration。
|
||||
///
|
||||
/// 绑定 <c>appsettings.json:SimpleLite</c>。
|
||||
/// 绑定 <c>appsettings.json:Simple3</c>。
|
||||
/// </summary>
|
||||
public sealed class SimpleLiteOptions
|
||||
public sealed class Simple3Options
|
||||
{
|
||||
/// <summary>主开关。false = 永不拉起;登录无论选什么 LaunchMode 都不会启动子进程,用于「只跑 Platform 调试」场景。</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// SimpleLite 可执行文件绝对/相对路径(相对 MiGu.Server 工作目录)。留空则按以下顺序自动探测:
|
||||
/// 1) <c>./SimpleLite.exe</c>(合并发布布局:SimpleLite 与 MiGu.Server 同目录)
|
||||
/// 2) <c>./SimpleLite/SimpleLite.exe</c>
|
||||
/// 3) <c>../SimpleLite.exe</c>(MiGu.Server 部署到子目录的常见布局)
|
||||
/// 4) <c>../SimpleLite/bin/Debug/SimpleLite.exe</c>(开发态:Visual Studio 默认输出)
|
||||
/// 5) <c>../SimpleLite/bin/Release/SimpleLite.exe</c>
|
||||
/// 6) 沿父目录上行寻找 <c>SimpleLite/bin/{Debug|Release}/SimpleLite.exe</c>
|
||||
/// Simple3 可执行文件绝对/相对路径(相对 MiGu.Server 工作目录)。留空则按以下顺序自动探测:
|
||||
/// 1) <c>./Simple3.exe</c>(合并发布布局;仍兼容探测旧名 <c>./SimpleLite.exe</c>)
|
||||
/// 2) <c>./Simple3/Simple3.exe</c>
|
||||
/// 3) 开发态:兄弟仓库 <c>../Simple/Simple3/</c> 产出目录或 <c>bin/Debug|Release</c>
|
||||
/// 4) 发布目录常见布局:<c>D:\MDCS\Release\Simple\Simple3\Simple3.exe</c>(仅当该绝对路径存在)
|
||||
/// 5) 沿父目录上行寻找 Simple3 产出
|
||||
/// </summary>
|
||||
public string ExecutablePath { get; set; } = "";
|
||||
|
||||
/// <summary>留空时取 <see cref="ExecutablePath"/> 所在目录。SimpleLite 在 CWD 读写 simple.json / imgui.ini,CWD 选错会出意外。</summary>
|
||||
/// <summary>留空时取 <see cref="ExecutablePath"/> 所在目录。内核在 CWD 读写 simple.json / imgui.ini,CWD 选错会出意外。</summary>
|
||||
public string WorkingDirectory { get; set; } = "";
|
||||
|
||||
/// <summary>附加命令行参数(拼在 <c>--display-mode=xxx</c> 之后)。常用于本地调试时强制 autoload 某场景。</summary>
|
||||
@@ -33,7 +32,7 @@ public sealed class SimpleLiteOptions
|
||||
|
||||
/// <summary>
|
||||
/// 子进程启动后阻塞等待 Projection (:8222) 端口就绪的最长毫秒数。
|
||||
/// 0 = 不等待(登录立即返回,前端可能还连不上 SimpleLite WebApi);
|
||||
/// 0 = 不等待(登录立即返回,前端可能还连不上 Simple3 WebApi);
|
||||
/// 负值 = 无限等待(直到子进程退出或就绪)。
|
||||
/// </summary>
|
||||
public int ReadinessTimeoutMs { get; set; } = 8000;
|
||||
@@ -41,17 +40,17 @@ public sealed class SimpleLiteOptions
|
||||
/// <summary>每隔多少毫秒 poll 一次 Projection 端口可达性。</summary>
|
||||
public int ReadinessPollIntervalMs { get; set; } = 250;
|
||||
|
||||
/// <summary>SimpleLite Projection WebApi 监听端口。默认与 SimpleLite Configuration 的 `port` 一致。用于就绪检测。</summary>
|
||||
/// <summary>Simple3 Projection WebApi 监听端口。默认与 Simple3 Configuration 的 `port` 一致。用于就绪检测。</summary>
|
||||
public int ProjectionPort { get; set; } = 8222;
|
||||
|
||||
/// <summary>
|
||||
/// 是否把 SimpleLite 绑定到 MiGu.Server 生命周期,默认 <b>false</b>(会话 N+2 用户反馈)。
|
||||
/// 是否把 Simple3 绑定到 MiGu.Server 生命周期,默认 <b>false</b>(会话 N+2 用户反馈)。
|
||||
///
|
||||
/// 设计原则:SimpleLite 与 MiGu.Server 是「两个独立程序」,Platform 只是登录后顺手拉起 SimpleLite;
|
||||
/// MiGu.Server 关闭不应该带走 SimpleLite,反之亦然。所以 FollowParent 默认 false:
|
||||
/// 设计原则:Simple3 与 MiGu.Server 是「两个独立程序」,Platform 只是登录后顺手拉起 Simple3;
|
||||
/// MiGu.Server 关闭不应该带走 Simple3,反之亦然。所以 FollowParent 默认 false:
|
||||
/// - 子进程走 <c>UseShellExecute=true</c> 创建独立进程组 + 独立控制台窗口;
|
||||
/// - 不挂 JobObject,不在 ApplicationStopping / ProcessExit 时 kill 子进程;
|
||||
/// - SimpleLite 退出由用户自己负责(关窗口 / 任务管理器 / 调度内核异常退出)。
|
||||
/// - Simple3 退出由用户自己负责(关窗口 / 任务管理器 / 调度内核异常退出)。
|
||||
///
|
||||
/// true 仍可用:会启动 Windows JobObject 父子绑定(仅 Windows 有效)+ 注册 ApplicationStopping 软关闭。
|
||||
/// 一般只在临时联调期 / CI 流水线想自动清理时打开。
|
||||
@@ -59,11 +58,11 @@ public sealed class SimpleLiteOptions
|
||||
public bool FollowParent { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 平台拉起 SimpleLite 时是否以「迷榖嵌入画布」模式运行(透传命令行 <c>--migu</c>),默认 <b>true</b>。
|
||||
/// 平台拉起 Simple3 时是否以「迷榖嵌入画布」模式运行(透传命令行 <c>--migu</c>),默认 <b>true</b>。
|
||||
///
|
||||
/// true:平台 iframe 嵌入场景,SimpleLite WebTerminal 默认只显示 3D 画布、隐藏所有 ImGui panel,
|
||||
/// true:平台 iframe 嵌入场景,Simple3 WebTerminal 默认只显示 3D 画布、隐藏所有 ImGui panel,
|
||||
/// 业务 UI 全部由 Vue 平台前端接管;declare 时序失败时也安全回退到纯画布。
|
||||
/// false:平台拉起的 SimpleLite web 端默认显示完整 panel(便于把平台拉起的实例直连 :8223 调试)。
|
||||
/// false:平台拉起的 Simple3 web 端默认显示完整 panel(便于把平台拉起的实例直连 :8223 调试)。
|
||||
///
|
||||
/// 与 LaunchMode(web / web+local,是否保留本地调试窗口)正交,可任意组合。
|
||||
/// </summary>
|
||||
@@ -1,8 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MiGu.Server</RootNamespace>
|
||||
@@ -25,13 +24,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MiGu.DB\MiGu.DB.csproj" />
|
||||
<ProjectReference Include="..\..\..\StandardSence\StandardScene.Signal\StandardScene.Signal.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="data\.gitkeep" Condition="Exists('data\.gitkeep')" />
|
||||
<Content Include="OpenApi\simplelite-projection.json" Link="OpenApi\simplelite-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('OpenApi\simplelite-projection.json')" />
|
||||
<Content Include="..\..\Simple\SimpleLite\Docs\openapi\simplelite-projection.json" Link="OpenApi\simplelite-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('..\..\Simple\SimpleLite\Docs\openapi\simplelite-projection.json')" />
|
||||
<Content Include="OpenApi\simple3-projection.json" Link="OpenApi\simple3-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('OpenApi\simple3-projection.json')" />
|
||||
<Content Include="..\..\Simple\Simple3\Docs\openapi\simple3-projection.json" Link="OpenApi\simple3-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('..\..\Simple\Simple3\Docs\openapi\simple3-projection.json')" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+8
-5
@@ -5,10 +5,10 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
namespace MiGu.Server.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 将 SimpleLite EmbedIO WebApi 的 OpenAPI 描述合并进 MiGu.Server Swagger 文档。
|
||||
/// 源文件:Simple/SimpleLite/Docs/openapi/simplelite-projection.json
|
||||
/// 将 Simple3 EmbedIO WebApi 的 OpenAPI 描述合并进 MiGu.Server Swagger 文档。
|
||||
/// 源文件:Simple/Simple3/Docs/openapi/simple3-projection.json
|
||||
/// </summary>
|
||||
public sealed class SimpleLiteOpenApiDocumentFilter : IDocumentFilter
|
||||
public sealed class Simple3OpenApiDocumentFilter : IDocumentFilter
|
||||
{
|
||||
private static readonly Dictionary<string, OperationType> VerbMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -175,9 +175,12 @@ public sealed class SimpleLiteOpenApiDocumentFilter : IDocumentFilter
|
||||
|
||||
foreach (var root in roots)
|
||||
{
|
||||
yield return Path.GetFullPath(Path.Combine(root, "OpenApi", "simple3-projection.json"));
|
||||
yield return Path.GetFullPath(Path.Combine(root, "..", "..", "..", "Simple", "Simple3", "Docs", "openapi", "simple3-projection.json"));
|
||||
yield return Path.GetFullPath(Path.Combine(root, "..", "..", "Simple", "Simple3", "Docs", "openapi", "simple3-projection.json"));
|
||||
// 兼容旧文件名 / 旧目录
|
||||
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"));
|
||||
yield return Path.GetFullPath(Path.Combine(root, "..", "..", "..", "Simple", "Simple3", "Docs", "openapi", "simplelite-projection.json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,13 @@ namespace MiGu.Server.Ota;
|
||||
public sealed class OtaVehicleSource
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SimpleLiteOptions _sl;
|
||||
private readonly Simple3Options _sl;
|
||||
private readonly InternalTokenStoreAccessor _token;
|
||||
private readonly ILogger<OtaVehicleSource> _log;
|
||||
|
||||
public OtaVehicleSource(
|
||||
IHttpClientFactory httpFactory,
|
||||
IOptions<SimpleLiteOptions> sl,
|
||||
IOptions<Simple3Options> sl,
|
||||
InternalTokenStoreAccessor token,
|
||||
ILogger<OtaVehicleSource> log)
|
||||
{
|
||||
|
||||
+22
-23
@@ -124,11 +124,11 @@ builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new()
|
||||
{
|
||||
Title = "MiGu.Server + SimpleLite",
|
||||
Title = "MiGu.Server + Simple3",
|
||||
Version = "v1",
|
||||
Description = "咪咕平台后端 API,以及经 YARP 反代的 SimpleLite 数据 WebApi(标签 SimpleLite/*)。详见 Simple/SimpleLite/Docs/MIGU-API.md。"
|
||||
Description = "咪咕平台后端 API,以及经 YARP 反代的 Simple3 数据 WebApi(标签 Simple3/*)。详见 Simple/Simple3/Docs/MIGU-API.md。"
|
||||
});
|
||||
c.DocumentFilter<SimpleLiteOpenApiDocumentFilter>();
|
||||
c.DocumentFilter<Simple3OpenApiDocumentFilter>();
|
||||
// Swagger 里挂 Bearer 输入框,便于手工测带鉴权的端点。
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
@@ -162,7 +162,7 @@ builder.Services.AddCors(opts => opts.AddDefaultPolicy(p =>
|
||||
// - 旧 SPA 还在用 localStorage.token + Bearer,逐步迁移到 Cookie;过渡期两路都接受;
|
||||
// - JwtIssuer 集中颁发 + 验签;secret 来自 appsettings:Jwt:Secret 或环境变量
|
||||
// PLATFORM__JWT__SECRET,占位值会被运行时随机化并强制告警。
|
||||
// - InternalTokenStore 管理 SimpleLite 8222 ↔ MiGu.Server 之间的 X-Platform-Internal-Token
|
||||
// - InternalTokenStore 管理 Simple3 8222 ↔ MiGu.Server 之间的 X-Platform-Internal-Token
|
||||
// 共享密钥(YARP transform 自动追加)。
|
||||
builder.Services.AddSingleton<RbacStore>();
|
||||
builder.Services.AddSingleton<JwtIssuer>(sp =>
|
||||
@@ -217,7 +217,7 @@ builder.Services.AddAuthorization(opts =>
|
||||
}));
|
||||
});
|
||||
|
||||
// YARP + transform:把 Platform 内部 token 透传给 SimpleLite 8222(AR-1/AR-2 配套)。
|
||||
// YARP + transform:把 Platform 内部 token 透传给 Simple3 8222(AR-1/AR-2 配套)。
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
|
||||
.AddTransforms(tctx =>
|
||||
@@ -255,16 +255,18 @@ builder.Services.AddReverseProxy()
|
||||
|
||||
// 单例配置仓库(内存 + JSON 文件持久化占位)
|
||||
builder.Services.AddSingleton<ConfigStore>();
|
||||
builder.Services.AddSingleton<MiGu.Server.Logs.LogCleanupService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<MiGu.Server.Logs.LogCleanupService>());
|
||||
// 运维审计持久化存储(替代旧的纯内存队列,MiGu.Server 重启后审计不丢)。
|
||||
builder.Services.AddSingleton<OpsAuditStore>();
|
||||
// OpsController 真实转发 SimpleLite reflection execute 所需的 HttpClient 工厂。
|
||||
// OpsController 真实转发 Simple3 reflection execute 所需的 HttpClient 工厂。
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddPlatformPersistence(builder.Configuration);
|
||||
|
||||
// 会话 N+1(启动反转):把 SimpleLite 子进程拉起器接入 DI;AuthController 登录成功后按 LaunchMode 调 MaybeStart。
|
||||
// 配置段绑定 appsettings.json:SimpleLite,可被环境变量 SIMPLELITE__XXX 覆盖。
|
||||
builder.Services.Configure<SimpleLiteOptions>(builder.Configuration.GetSection("SimpleLite"));
|
||||
builder.Services.AddSingleton<SimpleLiteLauncher>();
|
||||
// 会话 N+1(启动反转):把 Simple3 子进程拉起器接入 DI;AuthController 登录成功后按 LaunchMode 调 MaybeStart。
|
||||
// 配置段绑定 appsettings.json:Simple3,可被环境变量 SIMPLE3__XXX 覆盖。
|
||||
builder.Services.Configure<Simple3Options>(builder.Configuration.GetSection("Simple3"));
|
||||
builder.Services.AddSingleton<Simple3Launcher>();
|
||||
builder.Services.AddSingleton<SignalDataStore>();
|
||||
|
||||
// OTA(WatchDog 编排):包库 / 任务 / 出站客户端
|
||||
@@ -289,35 +291,32 @@ _ = app.Services.GetRequiredService<JwtIssuer>();
|
||||
_ = app.Services.GetRequiredService<InternalTokenStore>();
|
||||
// 主动构造 RbacStore:首启时尽早 seed 默认用户 / 角色并打印 data/rbac.json 载入日志。
|
||||
_ = app.Services.GetRequiredService<RbacStore>();
|
||||
// 主动实例化 SimpleLiteLauncher(FollowParent=true 时注册 ProcessExit 软关闭钩子)。
|
||||
var simpleLiteLauncher = app.Services.GetRequiredService<SimpleLiteLauncher>();
|
||||
// 主动实例化 Simple3Launcher(FollowParent=true 时注册 ProcessExit 软关闭钩子)。
|
||||
var simple3Launcher = app.Services.GetRequiredService<Simple3Launcher>();
|
||||
{
|
||||
var sl = simpleLiteLauncher.GetDiagnostics();
|
||||
var sl = simple3Launcher.GetDiagnostics();
|
||||
app.Logger.LogInformation(
|
||||
"[MiGu.Server] SimpleLite: Enabled={Enabled}, FollowParent={FollowParent}, ConfiguredPath={Cfg}, Resolved={Resolved}, Exists={Exists}, Port:{Port} reachable={PortUp}. 配置见 appsettings.json → SimpleLite",
|
||||
"[MiGu.Server] Simple3: Enabled={Enabled}, FollowParent={FollowParent}, ConfiguredPath={Cfg}, Resolved={Resolved}, Exists={Exists}, Port:{Port} reachable={PortUp}. 配置见 appsettings.json → Simple3",
|
||||
sl.Enabled, sl.FollowParent, sl.ConfiguredExecutablePath, sl.ResolvedExecutablePath ?? "(未找到)", sl.ExecutableExists,
|
||||
sl.ProjectionPort, sl.ProjectionPortReachable);
|
||||
}
|
||||
|
||||
// FollowParent=true 时 MiGu.Server 退出会 kill SimpleLite;默认 false 时不注册停机清理(两进程独立)。
|
||||
if (builder.Configuration.GetValue("SimpleLite:FollowParent", false))
|
||||
// FollowParent=true 时 MiGu.Server 退出会 kill Simple3;默认 false 时不注册停机清理(两进程独立)。
|
||||
if (builder.Configuration.GetValue("Simple3:FollowParent", false))
|
||||
{
|
||||
app.Lifetime.ApplicationStopping.Register(() =>
|
||||
{
|
||||
try { app.Services.GetRequiredService<SimpleLiteLauncher>().Dispose(); }
|
||||
try { app.Services.GetRequiredService<Simple3Launcher>().Dispose(); }
|
||||
catch { /* shutdown best-effort */ }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
app.Logger.LogInformation("[MiGu.Server] SimpleLite: FollowParent=false — MiGu.Server 退出不会结束 SimpleLite");
|
||||
app.Logger.LogInformation("[MiGu.Server] Simple3: FollowParent=false — MiGu.Server 退出不会结束 Simple3");
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
// OTA 回传会话按 TCP 对端 IP 校验;必须在 ForwardedHeaders 改写 RemoteIpAddress 之前捕获。
|
||||
app.Use(async (ctx, next) =>
|
||||
|
||||
+34
-34
@@ -5,8 +5,8 @@
|
||||
>
|
||||
> **会话 N+1(启动反转):现在 `MiGu.Server.exe` 是主入口**。
|
||||
> 用户先启动 MiGu.Server,浏览器登录页选择「启动模式」(本地+Web / 仅Web),
|
||||
> 后端 `AuthController.Login` 调用 `SimpleLiteLauncher.MaybeStart(launchMode)` 按所选模式拉起 `SimpleLite.exe`,
|
||||
> 通过 `--display-mode=web|web+local` 透传给 SimpleLite 的 `Configuration.displayMode`。
|
||||
> 后端 `AuthController.Login` 调用 `Simple3Launcher.MaybeStart(launchMode)` 按所选模式拉起 `Simple3.exe`,
|
||||
> 通过 `--display-mode=web|web+local` 透传给 Simple3 的 `Configuration.displayMode`。
|
||||
>
|
||||
> 工程名 `MiGu.Server` / 程序集 `MiGu.Server.dll` 保持稳定,仅用户可见前端文案改为「迷毂」。
|
||||
|
||||
@@ -19,83 +19,83 @@
|
||||
cd MiGu.Server
|
||||
dotnet build MiGu.Server.csproj -c Debug
|
||||
|
||||
# 2) 启动(登录后按所选模式拉起 SimpleLite)
|
||||
# 2) 启动(登录后按所选模式拉起 Simple3)
|
||||
dotnet run
|
||||
# 或:.\bin\Debug\net8.0\MiGu.Server.exe
|
||||
```
|
||||
|
||||
**更新前端**:在 Simple 仓库执行 `build-platform-frontend.bat`,将 `frontends/apps/simple-platform-vue/dist/` 同步到本目录 `wwwroot/`。
|
||||
|
||||
## 配置 SimpleLite 路径(必读)
|
||||
## 配置 Simple3 路径(必读)
|
||||
|
||||
> **配置文件位置(无 Web 界面):**
|
||||
> **`MiGu.Server/appsettings.json`** → 搜索 **`"SimpleLite"`** 节点。
|
||||
> 登录页选「本地 + Web / 仅 Web」后,后端在此配置的路径拉起 `SimpleLite.exe`。
|
||||
> 启动后可在浏览器打开 **`http://localhost:8080/api/health/simplelite`** 查看路径是否解析成功。
|
||||
> **`MiGu.Server/appsettings.json`** → 搜索 **`"Simple3"`** 节点。
|
||||
> 登录页选「本地 + Web / 仅 Web」后,后端在此配置的路径拉起 `Simple3.exe`。
|
||||
> 启动后可在浏览器打开 **`http://localhost:8080/api/health/simple3`** 查看路径是否解析成功。
|
||||
|
||||
登录成功后,MiGu.Server 会按所选启动模式拉起 **SimpleLite.exe**。默认已在 `appsettings.json` 写好与 Simple 仓库并列的相对路径;开发机可用 `appsettings.Development.json` 覆盖为绝对路径。
|
||||
登录成功后,MiGu.Server 会按所选启动模式拉起 **Simple3.exe**。默认已在 `appsettings.json` 写好与 Simple 仓库并列的相对路径;开发机可用 `appsettings.Development.json` 覆盖为绝对路径。
|
||||
|
||||
### 本机开发(与 Simple 仓库并列)
|
||||
|
||||
仓库默认已在 `appsettings.Development.json` 中写好:
|
||||
|
||||
```json
|
||||
"SimpleLite": {
|
||||
"Simple3": {
|
||||
"Enabled": true,
|
||||
"ExecutablePath": "E:\\Work\\Core\\Simple-FR\\Simple\\SimpleLite\\bin\\Debug\\SimpleLite.exe",
|
||||
"WorkingDirectory": "E:\\Work\\Core\\Simple-FR\\Simple\\SimpleLite\\bin\\Debug",
|
||||
"ExecutablePath": "E:\\Work\\Core\\Simple-FR\\Simple\\Simple3\\bin\\Debug\\Simple3.exe",
|
||||
"WorkingDirectory": "E:\\Work\\Core\\Simple-FR\\Simple\\Simple3\\bin\\Debug",
|
||||
"ProjectionPort": 8222,
|
||||
"ReadinessTimeoutMs": 8000,
|
||||
"FollowParent": false
|
||||
}
|
||||
```
|
||||
|
||||
请先编译 SimpleLite(在 Simple 仓库):
|
||||
请先编译 Simple3(在 Simple 仓库):
|
||||
|
||||
```pwsh
|
||||
cd ..\Simple
|
||||
dotnet build SimpleLite\SimpleLite.csproj -c Debug
|
||||
dotnet build Simple3\Simple3.csproj -c Debug
|
||||
```
|
||||
|
||||
若你的 Simple 不在上述绝对路径,可改为**相对路径**(相对 `MiGu.Server` 工作目录):
|
||||
|
||||
```json
|
||||
"ExecutablePath": "..\\..\\Simple\\SimpleLite\\bin\\Debug\\SimpleLite.exe",
|
||||
"WorkingDirectory": "..\\..\\Simple\\SimpleLite\\bin\\Debug"
|
||||
"ExecutablePath": "..\\..\\Simple\\Simple3\\bin\\Debug\\Simple3.exe",
|
||||
"WorkingDirectory": "..\\..\\Simple\\Simple3\\bin\\Debug"
|
||||
```
|
||||
|
||||
### 配置项说明
|
||||
|
||||
| 键 | 含义 |
|
||||
|----|------|
|
||||
| `Enabled` | `false` 时永不拉起 SimpleLite(只调试平台 UI) |
|
||||
| `ExecutablePath` | `SimpleLite.exe` 绝对或相对路径;留空则自动探测(见 `Launcher/SimpleLiteOptions.cs`) |
|
||||
| `Enabled` | `false` 时永不拉起 Simple3(只调试平台 UI) |
|
||||
| `ExecutablePath` | `Simple3.exe` 绝对或相对路径;留空则自动探测(见 `Launcher/Simple3Options.cs`) |
|
||||
| `WorkingDirectory` | 子进程工作目录;留空则用 exe 所在目录(读写 `simple.json` / `imgui.ini`) |
|
||||
| `ProjectionPort` | 就绪检测端口,默认 `8222` |
|
||||
| `ReadinessTimeoutMs` | 登录后等待 SimpleLite WebAPI 就绪的最长时间(毫秒) |
|
||||
| `FollowParent` | `true` 时 MiGu.Server 退出会结束 SimpleLite;默认 `false`(两进程独立) |
|
||||
| `ReadinessTimeoutMs` | 登录后等待 Simple3 WebAPI 就绪的最长时间(毫秒) |
|
||||
| `FollowParent` | `true` 时 MiGu.Server 退出会结束 Simple3;默认 `false`(两进程独立) |
|
||||
|
||||
### 环境变量覆盖
|
||||
|
||||
```pwsh
|
||||
$env:SimpleLite__ExecutablePath = "D:\apps\SimpleLite.exe"
|
||||
$env:SimpleLite__WorkingDirectory = "D:\apps"
|
||||
$env:SimpleLite__Enabled = "true"
|
||||
$env:Simple3__ExecutablePath = "D:\apps\Simple3.exe"
|
||||
$env:Simple3__WorkingDirectory = "D:\apps"
|
||||
$env:Simple3__Enabled = "true"
|
||||
```
|
||||
|
||||
### 生产 / 合并发布
|
||||
|
||||
将 `SimpleLite.exe` 与依赖 DLL 放到 `MiGu.Server` 同目录,并清空 `ExecutablePath`(走自动探测 `./SimpleLite.exe`),或在 `appsettings.Production.json` 写死部署路径。
|
||||
将 `Simple3.exe` 与依赖 DLL 放到 `MiGu.Server` 同目录,并清空 `ExecutablePath`(走自动探测 `./Simple3.exe`),或在 `appsettings.Production.json` 写死部署路径。
|
||||
|
||||
浏览器访问 `http://localhost:8080/login`:
|
||||
1. 用户名 / 密码(`appsettings.json:Auth.Users` 默认 admin/admin、ops/ops);
|
||||
2. 选 scope(管理员 / 运营);
|
||||
3. 选 **SimpleLite 启动模式**:
|
||||
3. 选 **Simple3 启动模式**:
|
||||
- **本地 + Web** → 透传 `--display-mode=web+local`,桌面 ImGui 窗口 + 浏览器同时启动;
|
||||
- **仅 Web** → 透传 `--display-mode=web`,只起 WebTerminal,不弹本地窗口;
|
||||
4. 点登录。`AuthController.Login` 调用 `SimpleLiteLauncher.MaybeStart(...)` 阻塞等 Projection (`:8222`) 就绪后返回 LoginResponse。
|
||||
4. 点登录。`AuthController.Login` 调用 `Simple3Launcher.MaybeStart(...)` 阻塞等 Projection (`:8222`) 就绪后返回 LoginResponse。
|
||||
|
||||
> 如果只想跑 MiGu.Server 单进程调试(不拉 SimpleLite),把 `appsettings.json:SimpleLite.Enabled` 改为 `false` 即可。
|
||||
> 如果只想跑 MiGu.Server 单进程调试(不拉 Simple3),把 `appsettings.json:Simple3.Enabled` 改为 `false` 即可。
|
||||
|
||||
## 数据库启动流程
|
||||
|
||||
@@ -198,7 +198,7 @@ SchemaMode = Migrate
|
||||
>
|
||||
> 详见代码审核 ISSUE-03(`Doc/CODE_REVIEW_ISSUES_2026-05-29.md`)。
|
||||
>
|
||||
> **SimpleLite 8222 内部 API(RV-04)**:默认 `simple.json:platform.allowLoopbackBypass=true`,本机回环请求免 token;**多租户 / 共享主机的生产环境建议设为 `false`**,强制所有请求携带 `X-Platform-Internal-Token`(配合 `platform.internalToken` 或 `MiGu.Server/data/.internal-token`)。详见复审 `Doc/CODE_REVIEW_WEEK_2026-05-29.md` RV-04。
|
||||
> **Simple3 8222 内部 API(RV-04)**:默认 `simple.json:platform.allowLoopbackBypass=true`,本机回环请求免 token;**多租户 / 共享主机的生产环境建议设为 `false`**,强制所有请求携带 `X-Platform-Internal-Token`(配合 `platform.internalToken` 或 `MiGu.Server/data/.internal-token`)。详见复审 `Doc/CODE_REVIEW_WEEK_2026-05-29.md` RV-04。
|
||||
|
||||
## 能力清单(本轮)
|
||||
|
||||
@@ -208,11 +208,11 @@ SchemaMode = Migrate
|
||||
- 兜底/部署模式:找不到 dist 时回退到 `wwwroot/`(由 `build-platform-frontend.bat` 的 robocopy /MIR 填充);
|
||||
- SPA fallback 让 `/admin/*` `/monitor/*` `/login` `/status` 等所有非 API 路径都回退到 `index.html`,由 vue-router 接管;
|
||||
- YARP 反向代理:
|
||||
- `/api/sl/{**catch-all}` → `http://127.0.0.1:8222/`(SimpleLite WebAPI;当前 SimpleLite 未启 WebAPI 时返回 502);
|
||||
- `/api/sl/{**catch-all}` → `http://127.0.0.1:8222/`(Simple3 WebAPI;当前 Simple3 未启 WebAPI 时返回 502);
|
||||
- `/vr/{**catch-all}` → `http://127.0.0.1:8223/`(webVRender;可用于同源 iframe 解决 X-Frame-Options 限制);
|
||||
- 14 维度配置中心占位(对齐 §9):内存 + `data/config-{section}.json` 持久化;
|
||||
- Mock 鉴权:`/api/auth/login` 返回 Mock JWT + `EffectivePermissions`;LoginRequest 新增 `launchMode: "WebOnly" | "DesktopAndWeb"` 字段,登录成功后 `SimpleLiteLauncher` 据此拉起子进程;
|
||||
- SimpleLite 子进程编排:`Launcher/SimpleLiteLauncher.cs` 幂等 / Stdout 转发 / Windows JobObject 父子绑定 / 端口就绪等待,配置见 `appsettings.json:SimpleLite`;
|
||||
- Mock 鉴权:`/api/auth/login` 返回 Mock JWT + `EffectivePermissions`;LoginRequest 新增 `launchMode: "WebOnly" | "DesktopAndWeb"` 字段,登录成功后 `Simple3Launcher` 据此拉起子进程;
|
||||
- Simple3 子进程编排:`Launcher/Simple3Launcher.cs` 幂等 / Stdout 转发 / Windows JobObject 父子绑定 / 端口就绪等待,配置见 `appsettings.json:Simple3`;
|
||||
- 运维白名单网关占位:`/api/sl/ops/execute`、`/api/sl/ops/audits`;
|
||||
- 投影 API 占位:`/api/projection/{sites,tracks,cars,missions}`;
|
||||
- 健康检查:`/api/health`;
|
||||
@@ -255,7 +255,7 @@ MiGu.Server/
|
||||
└── wwwroot/ # 部署兜底(dev 模式优先用 frontends/.../dist;assets/ 与 index.html 已 ignore)
|
||||
```
|
||||
|
||||
## 独立启动(仅当你不通过 SimpleLite 自启时)
|
||||
## 独立启动(仅当你不通过 Simple3 自启时)
|
||||
|
||||
```pwsh
|
||||
# 1) 还原 + 编译 + 运行
|
||||
@@ -278,7 +278,7 @@ dotnet run
|
||||
> 启动时控制台会打印 `[MiGu.Server] WebRoot -> dist: ...` 或 `WebRoot -> wwwroot ...`,提示当前使用哪一份产物。
|
||||
> 单独启动且既没有跑过 `pnpm build` 也没有跑过 `build-platform-frontend.bat` 时,会看到 SPA fallback 找不到 `index.html`。
|
||||
> 仓库根 `build-platform-frontend.bat` 仍可用于「打部署包」场景(把 dist robocopy /MIR 同步到 wwwroot)。
|
||||
> SimpleLite 自启 MiGu.Server 时,Program.cs 会从 `bin\<Config>\net8.0\` 沿目录向上找 dist;
|
||||
> Simple3 自启 MiGu.Server 时,Program.cs 会从 `bin\<Config>\net8.0\` 沿目录向上找 dist;
|
||||
> 找到则使用源码区 dist,找不到则使用 bin 同级的 wwwroot——两种启动方式共用同一份前端产物。
|
||||
|
||||
## 与前端联调
|
||||
@@ -292,7 +292,7 @@ dotnet run
|
||||
- 若上级目录存在 `frontends/apps/simple-platform-vue/dist/`,Program.cs 会优先挂 dist;
|
||||
- 浏览器访问 `http://localhost:8080/login` 等路径由 vue-router 解析。
|
||||
|
||||
## YARP 与 SimpleLite
|
||||
## YARP 与 Simple3
|
||||
|
||||
YARP 路由配置见 `appsettings.json`:
|
||||
|
||||
@@ -312,7 +312,7 @@ YARP 路由配置见 `appsettings.json`:
|
||||
```
|
||||
|
||||
- `/api/sl/ops/{execute,audits}` 在本工程被 `OpsController` 显式接住(更具体的路由),用于占位测试;
|
||||
- 其他 `/api/sl/*` 由 YARP 透传到 SimpleLite `:8222`,SimpleLite 未启时会得到 502/连接被拒。
|
||||
- 其他 `/api/sl/*` 由 YARP 透传到 Simple3 `:8222`,Simple3 未启时会得到 502/连接被拒。
|
||||
|
||||
## iframe 嵌入 webVRender 的两种方式
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
"Database": {
|
||||
"SchemaMode": "EnsureCreated"
|
||||
},
|
||||
"SimpleLite": {
|
||||
"Simple3": {
|
||||
"Enabled": true,
|
||||
"ExecutablePath": "D:\\工作\\stand\\SimpleLite\\SimpleLite.exe",
|
||||
"WorkingDirectory": "D:\\工作\\stand\\SimpleLite",
|
||||
"ExecutablePath": "D:\\MDCS\\Release\\Simple\\Simple3\\Simple3.exe",
|
||||
"WorkingDirectory": "D:\\MDCS\\Release\\Simple\\Simple3",
|
||||
"ProjectionPort": 8222,
|
||||
"ReadinessTimeoutMs": 8000,
|
||||
"FollowParent": false
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
"Internal": {
|
||||
"Token": "REPLACE_ME"
|
||||
},
|
||||
"_comment_SimpleLite": "登录后拉起 SimpleLite;改路径请编辑下方 SimpleLite 节点。诊断: http://localhost:8080/api/health/simplelite",
|
||||
"SimpleLite": {
|
||||
"_comment_Simple3": "登录后拉起调度内核 Simple3.exe;改路径请编辑下方 Simple3 节点。诊断: http://localhost:8080/api/health/simple3",
|
||||
"Simple3": {
|
||||
"Enabled": true,
|
||||
"ExecutablePath": "D:\\工作\\stand\\SimpleLite\\SimpleLite.exe",
|
||||
"WorkingDirectory": "D:\\工作\\stand\\SimpleLite",
|
||||
"ExecutablePath": "D:\\MDCS\\Release\\Simple\\Simple3\\Simple3.exe",
|
||||
"WorkingDirectory": "D:\\MDCS\\Release\\Simple\\Simple3",
|
||||
"Arguments": "",
|
||||
"ReadinessTimeoutMs": 15000,
|
||||
"ReadinessPollIntervalMs": 250,
|
||||
@@ -39,7 +39,7 @@
|
||||
"ops": { "Password": "ops" }
|
||||
}
|
||||
},
|
||||
"_comment_Ops": "运维操作真实下发映射(M4)。opCode → 'kind:Method',Method 必须是 SimpleLite 反射 [MethodMember] 的真实方法名(车辆 kind=car,见 Car.cs:OnlineCar/OfflineCar/DisableCar/EnableCar/Repair/Blown/Reset 等;任务 kind=mission)。运营白名单的 pause/resume/gohome/manualCharge 内核暂无一一对应方法——留空则仅记审计并向前端如实返回『未下发』,按现场内核能力填写后即真实生效。示例: 'ops.car.gohome': 'car:Reset'",
|
||||
"_comment_Ops": "运维操作真实下发映射(M4)。opCode → 'kind:Method',Method 必须是 Simple3 反射 [MethodMember] 的真实方法名(车辆 kind=car,见 Car.cs:OnlineCar/OfflineCar/DisableCar/EnableCar/Repair/Blown/Reset 等;任务 kind=mission)。运营白名单的 pause/resume/gohome/manualCharge 内核暂无一一对应方法——留空则仅记审计并向前端如实返回『未下发』,按现场内核能力填写后即真实生效。示例: 'ops.car.gohome': 'car:Reset'",
|
||||
"Ops": {
|
||||
"Dispatch": {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user