feat(platform): 部署配置向导 + 地图管理,地图编辑器接入统一存取与 AI 助手

- 配置向导:登录按 deployment 画像引导平台选型(导航方式/模块/场景),未完成则路由守卫强制进入 /wizard;选型驱动菜单按需裁剪,并联动 SimpleLite 写 plugins/active-scenes.json + 透传 --scenes 选择性加载导航场景插件
- 地图管理页:服务器地图列表/使用/重命名/删除、地图合并、多地图连接管理
- 地图编辑器:项目存取改为存入地图管理统一目录(同名替换确认),支持 ?map=/?new= 进入,新增右侧可停靠 AI 助手面板
- 集成 PTL 拣选模块;新增车队分配面板(运维总览/筛选联动)
- SimpleLiteBuildSync 同步运行时依赖 DLL;重新构建前端静态资源

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-06-03 09:37:07 +08:00
co-authored by Cursor
parent e2269e430d
commit 7382e85598
109 changed files with 3481 additions and 398 deletions
+18 -7
View File
@@ -35,7 +35,8 @@ public class AuthController : ControllerBase
string RunMode,
EffectivePermissions EffectivePermissions,
string? LaunchStatus = null,
string? LaunchWarning = null);
string? LaunchWarning = null,
bool NeedsWizard = false);
public record AuthUserDto(string Id, string Username, string DisplayName, List<string> Roles);
@@ -49,23 +50,29 @@ public class AuthController : ControllerBase
AuthUserDto User,
string Scope,
string RunMode,
EffectivePermissions EffectivePermissions);
EffectivePermissions EffectivePermissions,
bool NeedsWizard = false);
private const string CookieName = "simple.auth.token";
private readonly RbacStore _rbac;
private readonly JwtIssuer _jwt;
private readonly SimpleLiteLauncher _launcher;
private readonly ConfigStore _config;
private readonly ILogger<AuthController> _log;
public AuthController(RbacStore rbac, JwtIssuer jwt, SimpleLiteLauncher launcher, ILogger<AuthController> log)
public AuthController(RbacStore rbac, JwtIssuer jwt, SimpleLiteLauncher launcher, ConfigStore config, ILogger<AuthController> log)
{
_rbac = rbac;
_jwt = jwt;
_launcher = launcher;
_config = config;
_log = log;
}
/// <summary>是否需要进入配置向导(部署画像尚未完成)。登录 / me / switchScope 三处一致回填。</summary>
private bool NeedsWizard() => !_config.GetDeployment().Configured;
[HttpPost("login")]
[AllowAnonymous]
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest req)
@@ -106,7 +113,8 @@ public class AuthController : ControllerBase
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new LoginResponse(token, dto, req.Scope, runMode, perm,
LaunchStatus: launchResult?.Status,
LaunchWarning: launchResult?.Warning));
LaunchWarning: launchResult?.Warning,
NeedsWizard: NeedsWizard()));
}
[HttpPost("logout")]
@@ -140,7 +148,7 @@ public class AuthController : ControllerBase
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));
return Ok(new MeResponse(dto, scope, InferRunMode(), perm, NeedsWizard()));
}
/// <summary>用同一身份切换 scope 并重发 token + perms。</summary>
@@ -166,7 +174,8 @@ public class AuthController : ControllerBase
SetAuthCookie(token);
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new LoginResponse(token, dto, req.Scope, InferRunMode(), perm));
return Ok(new LoginResponse(token, dto, req.Scope, InferRunMode(), perm,
NeedsWizard: NeedsWizard()));
}
public record SwitchScopeRequest(string Scope);
@@ -180,7 +189,9 @@ public class AuthController : ControllerBase
private (EffectivePermissions perm, List<string> roleNames, string token) BuildSession(RbacUser user, string scope)
{
var eff = _rbac.ComputeEffective(user, scope);
var perm = new EffectivePermissions(user.Id, 1, eff.Ops, eff.Widgets, eff.Pages);
// 按部署画像裁剪可见页:未启用的功能/模块对应的配置页从菜单隐藏(向导未完成则不裁剪)。
var pages = DeploymentCatalog.FilterPagesByDeployment(eff.Pages, _config.GetDeployment());
var perm = new EffectivePermissions(user.Id, 1, eff.Ops, eff.Widgets, pages);
var roleNames = _rbac.RoleNamesOf(user);
// ops claim 写入有效操作码(含可能的 "*"),供 RbacAdmin policy 判定管理权限。
var token = _jwt.Issue(user.Id, user.Username, scope, roleNames, eff.Ops);
+143
View File
@@ -0,0 +1,143 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MiGu.Server.Configs;
using MiGu.Server.Launcher;
namespace MiGu.Server.Controllers;
/// <summary>
/// 配置向导 API。登录后若 <c>deployment.Configured=false</c>(见 <c>LoginResponse.NeedsWizard</c>),
/// 前端进入向导:
/// <list type="bullet">
/// <item><c>GET /api/wizard/options</c>:可选项目录(导航方式 / 模块 / 业务场景模板)。</item>
/// <item><c>GET /api/wizard/profile</c>:回显当前部署画像(含由导航选型推导的激活场景 id)。</item>
/// <item><c>PUT /api/wizard/profile</c>:保存并置 <c>Configured=true</c>。</item>
/// <item><c>POST /api/wizard/reset</c>:把 <c>Configured</c> 置回 false 以重新引导(保留草稿)。</item>
/// </list>
/// 说明:保存时即把选型固化为单一事实来源 <c>deployment</c> section,并同步联动 Launcher ——
/// <see cref="SaveProfile"/> 调 <c>WriteActiveScenes</c> 写 <c>plugins/active-scenes.json</c> / 透传 <c>--scenes</c>
/// 驱动 SimpleLite 下次启动选择性加载选定的导航场景插件。
/// </summary>
[ApiController]
[Authorize]
[Route("api/wizard")]
public class WizardController : ControllerBase
{
private readonly ConfigStore _store;
private readonly SimpleLiteLauncher _launcher;
private readonly ILogger<WizardController> _log;
public WizardController(ConfigStore store, SimpleLiteLauncher launcher, ILogger<WizardController> log)
{
_store = store;
_launcher = launcher;
_log = log;
}
[HttpGet("options")]
public IActionResult Options()
{
return Ok(new
{
navigationKinds = DeploymentCatalog.NavigationKinds,
modules = DeploymentCatalog.Modules,
scenarios = _store.Get("scenario").Payload
});
}
[HttpGet("profile")]
public IActionResult GetProfile() => Ok(Project(_store.GetDeployment()));
/// <summary>当前部署画像对菜单的裁剪结果(供前端做面板/能力级裁剪与排查)。</summary>
[HttpGet("effective-pages")]
public IActionResult EffectivePages()
{
var dp = _store.GetDeployment();
return Ok(new
{
configured = dp.Configured,
tailorablePages = DeploymentCatalog.TailorablePages(),
enabledPages = DeploymentCatalog.EnabledPages(dp),
hiddenPages = DeploymentCatalog.HiddenPages(dp)
});
}
[HttpPut("profile")]
public IActionResult SaveProfile([FromBody] SaveWizardRequest req)
{
if (req == null)
return BadRequest(new { message = "请求体不能为空" });
var user = User.Identity?.Name ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
var profile = new DeploymentProfile(
Configured: true,
PlatformType: string.IsNullOrWhiteSpace(req.PlatformType) ? "standard" : req.PlatformType.Trim(),
Modules: Clean(req.Modules),
NavigationKinds: Clean(req.NavigationKinds),
Scenarios: Clean(req.Scenarios),
UpdatedBy: user);
_store.PutDeployment(profile);
// 平台 → 内核联动:把导航选型写入 SimpleLite 的 plugins/active-scenes.json(下次启动选择性加载;
// 已运行实例可由前端再调 POST /api/sl/projection/scenes/apply 触发增量 reload)。
var sceneIds = profile.ToActiveSceneIds();
var write = _launcher.WriteActiveScenes(sceneIds, alwaysLoad: null, source: "deployment-profile");
_log.LogInformation("部署向导已保存 by={User} nav=[{Nav}] scenes=[{Scenes}] activeScenesWritten={Ok}",
user, string.Join(",", profile.NavigationKinds), string.Join(",", sceneIds), write.Ok);
return Ok(Project(profile, write));
}
[HttpPost("reset")]
public IActionResult Reset()
{
var reset = _store.GetDeployment() with { Configured = false };
_store.PutDeployment(reset);
return Ok(Project(reset));
}
/// <summary>
/// 统一对外投影:camelCase 字段 + 推导出的 activeSceneIds(导航选型 → 内核场景 id)+
/// hiddenPages(被裁剪的菜单页)+ 可选的 activeScenesWrite(保存时写 active-scenes.json 的结果)。
/// </summary>
private static object Project(DeploymentProfile dp, SimpleLiteLauncher.ActiveScenesWriteResult? write = null) => new
{
configured = dp.Configured,
platformType = dp.PlatformType,
modules = dp.Modules,
navigationKinds = dp.NavigationKinds,
scenarios = dp.Scenarios,
updatedBy = dp.UpdatedBy,
activeSceneIds = dp.ToActiveSceneIds(),
hiddenPages = DeploymentCatalog.HiddenPages(dp),
activeScenesWrite = write == null ? null : new
{
ok = write.Value.Ok,
path = write.Value.Path,
error = write.Value.Error
}
};
/// <summary>去空白、去重(大小写不敏感)、保持顺序。</summary>
private static List<string> Clean(List<string>? items)
{
var result = new List<string>();
if (items == null) return result;
foreach (var s in items)
{
var t = s?.Trim();
if (!string.IsNullOrEmpty(t) && !result.Contains(t, StringComparer.OrdinalIgnoreCase))
result.Add(t!);
}
return result;
}
public record SaveWizardRequest(
string? PlatformType,
List<string>? Modules,
List<string>? NavigationKinds,
List<string>? Scenarios);
}