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
+44 -2
View File
@@ -5,7 +5,7 @@ namespace MiGu.Server.Configs;
/// <summary>
/// 配置中心存储(内存 + JSON 文件持久化占位)。
/// 14 个 section 对应 ARCHITECTURE.md §9 的 13+1 维度。
/// 14 个 section 对应 ARCHITECTURE.md §9 的 13+1 维度;外加 deployment —— 登录后「配置向导」的部署画像
/// 真实落地时由 SimpleShared.Persistence 接入 EF Core,并配合 YARP 下发至 SimpleLite。
/// </summary>
public sealed class ConfigStore
@@ -13,7 +13,8 @@ public sealed class ConfigStore
public static readonly string[] AllSections =
{
"system", "integrations", "routing", "vehicle", "charge", "task",
"traffic", "auth", "device", "fleet", "scenario", "location", "ops", "widget"
"traffic", "auth", "device", "fleet", "scenario", "location", "ops", "widget",
"deployment"
};
public sealed record Envelope(string Section, int Version, DateTimeOffset UpdatedAt, object Payload);
@@ -67,6 +68,46 @@ public sealed class ConfigStore
public IEnumerable<Envelope> List() => AllSections.Select(Get);
/// <summary>
/// 强类型读取部署画像(<c>deployment</c> section)。兼容 Payload 为 <see cref="DeploymentProfile"/>(默认值场景)
/// 或 <see cref="JsonElement"/>(已持久化场景)两种形态,并把 null 列表/空字符串规整为安全默认值,
/// 供 AuthControllerNeedsWizard/ WizardController / Launcher 直接使用。
/// </summary>
public DeploymentProfile GetDeployment()
{
var env = Get("deployment");
var dp = env.Payload switch
{
DeploymentProfile d => d,
JsonElement el => SafeDeserializeDeployment(el),
_ => DeploymentProfile.Default()
};
return new DeploymentProfile(
dp.Configured,
string.IsNullOrWhiteSpace(dp.PlatformType) ? "standard" : dp.PlatformType,
dp.Modules ?? new List<string>(),
dp.NavigationKinds ?? new List<string>(),
dp.Scenarios ?? new List<string>(),
dp.UpdatedBy ?? "");
}
/// <summary>以强类型保存部署画像(统一经 <see cref="Put"/> 走版本/持久化/camelCase 序列化)。</summary>
public Envelope PutDeployment(DeploymentProfile profile)
{
var el = JsonSerializer.SerializeToElement(profile, _jsonOpts);
return Put("deployment", el);
}
private DeploymentProfile SafeDeserializeDeployment(JsonElement el)
{
try { return el.Deserialize<DeploymentProfile>(_jsonOpts) ?? DeploymentProfile.Default(); }
catch (Exception ex)
{
_logger.LogWarning(ex, "反序列化 deployment 失败,回退默认值");
return DeploymentProfile.Default();
}
}
private static object JsonElementToObject(JsonElement el)
{
// 简化:直接保留 JsonElement,让 STJ 在响应时再原样写回
@@ -126,6 +167,7 @@ public sealed class ConfigStore
"location" => LocationManagement.Default(),
"ops" => OpsConfig.Default(),
"widget" => CustomWidgetConfig.Default(),
"deployment" => DeploymentProfile.Default(),
_ => new { }
};
+74
View File
@@ -0,0 +1,74 @@
namespace MiGu.Server.Configs;
/// <summary>
/// 配置向导「可选项目录」与「选型 → 平台能力」映射的单一事实来源。
///
/// <para>前端向导从 <c>GET /api/wizard/options</c> 拿到这些可选项渲染勾选框;后端
/// <see cref="ModuleToPages"/> 在「按选型裁剪菜单」时把模块
/// 映射为 PageCatalog 的可见页 id。集中定义,避免前后端各写一份导致漂移。</para>
/// </summary>
public static class DeploymentCatalog
{
/// <summary>一个可勾选项。<see cref="Id"/> 是稳定机器标识,<see cref="Group"/> 用于前端分组展示。</summary>
public record Option(string Id, string Name, string Group, string Description);
/// <summary>导航方式(多选,一等维度)。id 与 <see cref="DeploymentProfile.NavKindToSceneId"/> 的 key 对齐。</summary>
public static readonly IReadOnlyList<Option> NavigationKinds = new[]
{
new Option("magnetic", "磁导航", "navigation", "磁条循迹 + 地标 / RFID 定位"),
new Option("qrcode", "二维码导航", "navigation", "二维码地标 + 码值地图"),
new Option("laser", "激光导航", "navigation", "反光板 / SLAM + 激光避障"),
};
/// <summary>功能模块(多选)。暂定保留 WMS / PTL 两项,后续按需扩展(参考 RIOT 的 WMS/WCS/MES/APS 分层)。</summary>
public static readonly IReadOnlyList<Option> Modules = new[]
{
new Option("wms", "WMS 仓储管理", "module", "库位 / 库存 / 出入库管理"),
new Option("ptl", "PTL 拣选系统", "module", "Pick-to-Light 亮灯拣选与播种"),
};
/// <summary>功能模块 → PageCatalog 页面 Key 列表的映射。PTL 暂无专属配置页,未纳入映射(选中不影响菜单)。</summary>
public static readonly IReadOnlyDictionary<string, string[]> ModuleToPages =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["wms"] = new[] { "admin-config-location" },
};
/// <summary>所有「可被选型控制」的页面 Key(Module → 页 映射值的并集)。</summary>
public static IReadOnlyCollection<string> TailorablePages()
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var v in ModuleToPages.Values) foreach (var p in v) set.Add(p);
return set;
}
/// <summary>当前部署画像下被「点亮」的可裁剪页(已启用 Module 映射到的页)。</summary>
public static IReadOnlyCollection<string> EnabledPages(DeploymentProfile dp)
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (dp == null) return set;
foreach (var m in dp.Modules ?? new List<string>())
if (ModuleToPages.TryGetValue(m, out var ps)) foreach (var p in ps) set.Add(p);
return set;
}
/// <summary>当前部署画像下应隐藏的页(可裁剪但未被点亮)。向导未完成(Configured=false)则不隐藏任何页。</summary>
public static IReadOnlyCollection<string> HiddenPages(DeploymentProfile dp)
{
if (dp == null || !dp.Configured) return Array.Empty<string>();
var enabled = EnabledPages(dp);
return TailorablePages().Where(p => !enabled.Contains(p)).ToList();
}
/// <summary>
/// 从 RBAC 计算出的可见页集合中移除「被部署画像隐藏」的页 —— 这是「按选型裁剪菜单」的核心。
/// 不在任何映射中的页不受影响;向导未完成时原样返回(避免首登空菜单)。
/// </summary>
public static List<string> FilterPagesByDeployment(IEnumerable<string> pages, DeploymentProfile dp)
{
var input = (pages ?? Enumerable.Empty<string>()).ToList();
if (dp == null || !dp.Configured) return input;
var hidden = new HashSet<string>(HiddenPages(dp), StringComparer.OrdinalIgnoreCase);
return input.Where(p => !hidden.Contains(p)).ToList();
}
}
+65
View File
@@ -0,0 +1,65 @@
namespace MiGu.Server.Configs;
/// <summary>
/// 部署画像 —— 登录后「配置向导」的结果,对应 ConfigStore 的 <c>deployment</c> section
/// (持久化于 <c>data/config-deployment.json</c>)。
///
/// <para>它是「按选型裁剪」的单一事实来源:</para>
/// <list type="number">
/// <item><see cref="Configured"/>=false 时,登录返回 <c>NeedsWizard=true</c>,前端跳转配置向导;</item>
/// <item><see cref="NavigationKinds"/> 经 <see cref="ToActiveSceneIds"/> 映射为 SimpleLite 场景 id
/// 由 Launcher 写入 <c>plugins/active-scenes.json</c> / 透传 <c>--scenes</c>,驱动内核「选择性加载」导航场景插件;</item>
/// <item><see cref="Modules"/> 驱动平台菜单与能力裁剪(PageCatalog 可见页)。</item>
/// </list>
/// </summary>
/// <param name="Configured">向导是否已完成。false = 登录后强制进入配置向导。</param>
/// <param name="PlatformType">平台主定位(仅用于 UI 标题/默认值),如 <c>standard</c> / <c>wcs</c> / <c>wms-wcs</c> / <c>custom</c>。</param>
/// <param name="Modules">启用的功能模块(多选),暂定 <c>wms</c> / <c>ptl</c>。</param>
/// <param name="NavigationKinds">导航方式(多选,一等维度),取值 <c>magnetic</c> / <c>qrcode</c> / <c>laser</c>,与 SimpleCore.NavKind 对齐。</param>
/// <param name="Scenarios">选用的业务场景模板 id(来自 ScenarioTemplateConfig),如 <c>tpl-sps</c> / <c>tpl-pack</c>。</param>
/// <param name="UpdatedBy">最近一次保存向导的用户名(审计用)。</param>
public record DeploymentProfile(
bool Configured,
string PlatformType,
List<string> Modules,
List<string> NavigationKinds,
List<string> Scenarios,
string UpdatedBy)
{
public static DeploymentProfile Default() => new(
Configured: false,
PlatformType: "standard",
Modules: new List<string>(),
NavigationKinds: new List<string>(),
Scenarios: new List<string>(),
UpdatedBy: "");
/// <summary>
/// 导航方式 → SimpleLite 场景 id 映射。与 <c>scene.json.id</c>、<c>SimpleCore.Navigation.NavKind</c>
/// 以及内核 <c>active-scenes.json.activeScenes</c> 一致;这是平台与内核之间「导航选型」的契约约定。
/// </summary>
public static readonly IReadOnlyDictionary<string, string> NavKindToSceneId =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["magnetic"] = "scene.magnetic",
["qrcode"] = "scene.qrcode",
["laser"] = "scene.laser",
};
/// <summary>
/// 把已选 <see cref="NavigationKinds"/> 映射为 SimpleLite 激活场景 id 列表(去重、忽略未知项、保持选择顺序)。
/// 供 Launcher 写 <c>active-scenes.json</c> / 拼 <c>--scenes</c> 参数使用。
/// </summary>
public IReadOnlyList<string> ToActiveSceneIds()
{
var result = new List<string>();
if (NavigationKinds == null) return result;
foreach (var k in NavigationKinds)
{
if (string.IsNullOrWhiteSpace(k)) continue;
if (NavKindToSceneId.TryGetValue(k.Trim(), out var sceneId) && !result.Contains(sceneId))
result.Add(sceneId);
}
return result;
}
}
+4 -2
View File
@@ -5,10 +5,12 @@ public record EndpointDescriptor(string Id, string Name, string Url, bool Enable
public record ExternalIntegrations(
List<EndpointDescriptor> Mes,
List<EndpointDescriptor> Wms,
List<EndpointDescriptor> Rcs)
List<EndpointDescriptor> Rcs,
List<EndpointDescriptor> Ptl)
{
public static ExternalIntegrations Default() => new(
Mes: new List<EndpointDescriptor> { new("mes-1", "MES 主线", "http://mes.lan/api", true) },
Wms: new List<EndpointDescriptor> { new("wms-1", "WMS 仓储", "http://wms.lan/api", true) },
Rcs: new List<EndpointDescriptor>());
Rcs: new List<EndpointDescriptor>(),
Ptl: new List<EndpointDescriptor> { new("ptl-1", "PTL 拣选", "http://ptl.lan/api", true) });
}