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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
黄兆尉
2026-08-26 17:46:52 +08:00
co-authored by Cursor
parent 912c44c9bb
commit 3686abdc78
79 changed files with 922 additions and 832 deletions
+116 -116
View File
@@ -1,10 +1,10 @@
# SimpleLite 三端系统架构设计
# Simple3 三端系统架构设计
> **Migu2.0 仓库说明**:平台后端已拆至本仓库,工程名为 **`MiGu.Server`**(原 `Platform.Server`)。SimpleLite 与前端源码仍在兄弟仓库 `Simple`。下文中的 `MiGu.Server` 即指本仓库 `MiGu.Server/` 目录。
> **Migu2.0 仓库说明**:平台后端已拆至本仓库,工程名为 **`MiGu.Server`**(原 `Platform.Server`)。Simple3 与前端源码仍在兄弟仓库 `Simple`。下文中的 `MiGu.Server` 即指本仓库 `MiGu.Server/` 目录。
> 文档版本:v1.7.0
> 更新日期:2026-05-20
> 范围:SimpleLite(主程序 / 设计端 / 调度引擎宿主 / 进程编排器)+ Platform(业务平台 · 同时承载管理员前端 platform-vue 与运营前端 rcsmonitor-vue · 共用一套 WebAPI · **主入口为 MiGu.Server,登录后拉起 SimpleLite**
> 范围:Simple3(主程序 / 设计端 / 调度引擎宿主 / 进程编排器)+ Platform(业务平台 · 同时承载管理员前端 platform-vue 与运营前端 rcsmonitor-vue · 共用一套 WebAPI · **主入口为 MiGu.Server,登录后拉起 Simple3**
> v1.7.0 变更(多主题色板 · 工业紫默认):
> - **6 套可切换主题**`themes.ts` 定义色板(工业紫、深邃蓝、翡翠绿、熔铁赤、石墨钢、琥珀金);默认 **工业紫色** `#6a1b9a`(比 v1.6.2 的 `#641393` 更深更饱和,解决登录后内容区配色过淡)。
@@ -37,13 +37,13 @@
> - 新增 §7.4–§7.7 四类 DB 部署形态详述、选型矩阵、连接串与配置示例、迁移注意事项。
> v1.4 变更(保留):
> - **明确进程编排归属**Platform 后端进程 `MiGu.Server.exe` 不再被视为"独立的业务平台服务",而是 SimpleLite 内置 `SystemMission``StartPlatformMission`)拉起、被 SimpleLite Bootstrapper 守护的**受控子进程**RCSMonitor 不存在独立后端进程,复用 MiGu.Server。
> - **明确进程编排归属**Platform 后端进程 `MiGu.Server.exe` 不再被视为"独立的业务平台服务",而是 Simple3 内置 `SystemMission``StartPlatformMission`)拉起、被 Simple3 Bootstrapper 守护的**受控子进程**RCSMonitor 不存在独立后端进程,复用 MiGu.Server。
> - **整体定位章节重写**:明确"两类后端可执行文件 + 一个进程编排者"的关系,避免读者误以为 Platform 是用 Windows Service / systemd 独立部署的服务。
> v1.3 变更(保留):
> 1. **取消 RCSMonitor.Server**`rcsmonitor-vue` 不再有独立后端进程,直接连 MiGu.Server;由 JWT 中的 `scope=RCSMonitor` + 角色权限码 + `WidgetGrant` 三层限制可见控件与可调用 API。
> 2. **3D 渲染基座定为 webVRender**:复用 SimpleLite WebTerminal 已经提供的 webVRender,以 Web Component / iframe 嵌入到 platform-vue 与 rcsmonitor-vue,两端共享同一份 Workspace 协议(PutModel / PutPointCloud / SetCamera 等 DTO)。
> 3. **反向代理选定 YARP**MiGu.Server 内置 [YARP](https://microsoft.github.io/reverse-proxy/) 做 `/api/sl/*` → SimpleLite WebAPI 的反代;带鉴权与审计中间件。
> 2. **3D 渲染基座定为 webVRender**:复用 Simple3 WebTerminal 已经提供的 webVRender,以 Web Component / iframe 嵌入到 platform-vue 与 rcsmonitor-vue,两端共享同一份 Workspace 协议(PutModel / PutPointCloud / SetCamera 等 DTO)。
> 3. **反向代理选定 YARP**MiGu.Server 内置 [YARP](https://microsoft.github.io/reverse-proxy/) 做 `/api/sl/*` → Simple3 WebAPI 的反代;带鉴权与审计中间件。
> 4. **运行模式持久化到 `simple.json`**:登录窗"记住模式"勾选后写入 `simple.json.runMode`;下次启动直接进入。
> 5. **Web-Enabled 下严禁同时本地操作**CycleGUI 主窗只剩"服务状态窗",所有业务面板硬性隐藏;不提供"临时启用本地 UI"开关。
> 6. **共享前端组件库**:使用 pnpm workspace + `frontends/packages/sl-controls`,封装 Workspace3D / 地图 / 任务 / CAD 等可复用 Vue 组件。
@@ -79,43 +79,43 @@
一套为 AGV/AMR 现场打造的「**设计 → 调度 → 运营监控**」三端协同系统。
**三个"端"对应三套用户体验,但只有两类后端可执行文件,且都由 SimpleLite 统一编排:**
**三个"端"对应三套用户体验,但只有两类后端可执行文件,且都由 Simple3 统一编排:**
| 端 | 前端载体 | 后端可执行文件 | **启动者** | 角色 | 写权限 |
|----|----------|----------------|------------|------|--------|
| **SimpleLite 桌面端** | CycleGUI 客户端 | `SimpleLite.exe` 自身 | 用户双击 / 服务管理器 | 主程序 + 调度引擎宿主 + 全功能 WebAPI 提供者 + **进程编排器(Bootstrapper + SystemMission** | **全功能** |
| **Platform 管理端** | `platform-vue` (Vue 3 SPA) | `MiGu.Server.exe` | **SimpleLite 内置 `SystemMission.StartPlatformMission` 拉起**Web-Enabled 时自动 spawn;崩溃由 SimpleLite Watchdog 重启) | **SimpleLite 全能力套壳**:管理员侧 Vue 前端复刻所有设计/调度控件 + 业务平台特有能力(配置中心 / 外部对接 / 库位 / 账号) | **全功能**(等价 SimpleLite |
| **RCSMonitor 运营端** | `rcsmonitor-vue` (Vue 3 SPA) | **共用 `MiGu.Server.exe`**(无独立后端进程) | 随 MiGu.Server 一起被 SimpleLite 拉起 | 运营监控前端:按 `scope=RCSMonitor` + 角色权限 + WidgetGrant 三层裁剪 UI;可对任务/车辆执行白名单运维动作 | **受限**(仅运维白名单) |
| **Simple3 桌面端** | CycleGUI 客户端 | `Simple3.exe` 自身 | 用户双击 / 服务管理器 | 主程序 + 调度引擎宿主 + 全功能 WebAPI 提供者 + **进程编排器(Bootstrapper + SystemMission** | **全功能** |
| **Platform 管理端** | `platform-vue` (Vue 3 SPA) | `MiGu.Server.exe` | **Simple3 内置 `SystemMission.StartPlatformMission` 拉起**Web-Enabled 时自动 spawn;崩溃由 Simple3 Watchdog 重启) | **Simple3 全能力套壳**:管理员侧 Vue 前端复刻所有设计/调度控件 + 业务平台特有能力(配置中心 / 外部对接 / 库位 / 账号) | **全功能**(等价 Simple3 |
| **RCSMonitor 运营端** | `rcsmonitor-vue` (Vue 3 SPA) | **共用 `MiGu.Server.exe`**(无独立后端进程) | 随 MiGu.Server 一起被 Simple3 拉起 | 运营监控前端:按 `scope=RCSMonitor` + 角色权限 + WidgetGrant 三层裁剪 UI;可对任务/车辆执行白名单运维动作 | **受限**(仅运维白名单) |
**关键澄清(v1.4 重点):**
- **后端可执行文件只有 2 个**`SimpleLite.exe``MiGu.Server.exe`
- **进程启动只有 1 个真正的入口**:用户/服务管理器只启动 `SimpleLite.exe`MiGu.Server 由 SimpleLite 自己拉起,**不需要也不允许独立安装为 Windows Service**。
- **进程编排归属 SimpleLite**`SystemMission` 是 SimpleLite 自带的 Mission 体系扩展(与业务 Mission 区分开),专门负责拉起/守护/优雅停止 MiGu.Server。Bootstrapper 监听心跳、按指数退避重启、统一记录 OpsAuditLog。
- **后端可执行文件只有 2 个**`Simple3.exe``MiGu.Server.exe`
- **进程启动只有 1 个真正的入口**:用户/服务管理器只启动 `Simple3.exe`MiGu.Server 由 Simple3 自己拉起,**不需要也不允许独立安装为 Windows Service**。
- **进程编排归属 Simple3**`SystemMission` 是 Simple3 自带的 Mission 体系扩展(与业务 Mission 区分开),专门负责拉起/守护/优雅停止 MiGu.Server。Bootstrapper 监听心跳、按指数退避重启、统一记录 OpsAuditLog。
- **Vue 两份产物,同一进程托管**MiGu.Server 在 `/admin/*` 路径下托管 `platform-vue`,在 `/monitor/*` 路径下托管 `rcsmonitor-vue`
- **鉴权与限权完全在 MiGu.Server 完成**RCSMonitor 不持有任何独立后端代码(没有 `RCSMonitor.Server.exe`、没有独立库)。
- **Desktop-Only 模式下,SystemMission 不触发**MiGu.Server 根本不会启动,Vue 端无法访问。
> 一句话:**用户点 SimpleLite.exe,由 SimpleLite 通过 Mission 把 Platform 后端拉起来;Platform 后端再把两套 Vue 端服好。**
> 一句话:**用户点 Simple3.exe,由 Simple3 通过 Mission 把 Platform 后端拉起来;Platform 后端再把两套 Vue 端服好。**
### 1.2 核心拆分原则
1. **Crash-Isolation(搞不崩主服务)**
Platform / RCSMonitor 对**地图/任务/车辆等写操作**都通过 SimpleLite`/api/*` 网关;主服务侧做白名单 + 幂等校验 + 频次限流 + 审计。Platform-Vue 走完整 API 集,RCSMonitor-Vue 仅限运维白名单 `/api/ops/*`
Platform / RCSMonitor 对**地图/任务/车辆等写操作**都通过 Simple3`/api/*` 网关;主服务侧做白名单 + 幂等校验 + 频次限流 + 审计。Platform-Vue 走完整 API 集,RCSMonitor-Vue 仅限运维白名单 `/api/ops/*`
2. **共享数据模型,独立运行时**
地图、车辆、任务三端共用 `SimpleCore` 的 DTO;调度引擎只在 SimpleLite 里跑,Platform 只持有"投影"。
地图、车辆、任务三端共用 `SimpleCore` 的 DTO;调度引擎只在 Simple3 里跑,Platform 只持有"投影"。
3. **库分离 + Provider 抽象**(详见 §7
- `simple_main` 由 SimpleLite 持有;`platform` 由 Platform 持有(含 RCSMonitor 审计/标注);不再有独立的 `rcsmonitor_local`
- `simple_main` 由 Simple3 持有;`platform` 由 Platform 持有(含 RCSMonitor 审计/标注);不再有独立的 `rcsmonitor_local`
- 持久层基于 **EF Core****通过 Provider 切换底层数据库**:默认 **SQLite**(边缘单机,配 ROSE HA);可选 **MySQL / PostgreSQL / SQL Server**,匹配现场已有 IT 基础设施与企业级 HA 能力(详见 §7.4–§7.7)。
- 高可用方案随 Provider 不同:SQLite 用 ROSE HAMySQL 用 InnoDB ClusterPostgreSQL 用 PatroniSQL Server 用 Always On AG。
4. **统一协议层**
所有进程间调用统一 **WebAPIREST/JSON+ WebSocket 事件流**Vue 端直接对接 Platform 后端的 REST + WSPlatform 后端用 **YARP** 反代 SimpleLite 的设计/调度 API。
所有进程间调用统一 **WebAPIREST/JSON+ WebSocket 事件流**Vue 端直接对接 Platform 后端的 REST + WSPlatform 后端用 **YARP** 反代 Simple3 的设计/调度 API。
5. **统一身份**
`SimpleShared.Auth` 提供 JWT + RBACPlatform 是主权限源(在线时),SimpleLite 在 Desktop-Only 下用本地用户表降级。
`SimpleShared.Auth` 提供 JWT + RBACPlatform 是主权限源(在线时),Simple3 在 Desktop-Only 下用本地用户表降级。
6. **运行模式互斥(强约束)**
- **Desktop-Only**CycleGUI 客户端独占;WebAPI / Platform **不启动**
@@ -139,7 +139,7 @@ flowchart TB
end
subgraph Host["主控宿主机 / 边缘服务器"]
subgraph SL["SimpleLite 主进程 (CycleGUI + ASP.NET Core 8)"]
subgraph SL["Simple3 主进程 (CycleGUI + ASP.NET Core 8)"]
direction TB
SL_Login["启动登录窗<br/>(模式选择 Desktop / Web)"]
SL_UI["CycleGUI 客户端<br/>(Desktop-Only: 全功能<br/>Web-Enabled: 仅状态窗)"]
@@ -155,7 +155,7 @@ flowchart TB
direction TB
PL_Static["静态资源<br/>/admin/* → platform-vue dist<br/>/monitor/* → rcsmonitor-vue dist"]
PL_API["Platform WebAPI<br/>(配置中心 / 外部对接 / 库位 / 账号)"]
PL_YARP["YARP 反代<br/>/api/sl/* → SimpleLite WebAPI<br/>+ Auth/审计中间件"]
PL_YARP["YARP 反代<br/>/api/sl/* → Simple3 WebAPI<br/>+ Auth/审计中间件"]
PL_Auth["SimpleAuth<br/>(JWT 签发 + Scope 校验)"]
PL_Adp["外部系统适配器<br/>(MES / WMS / RCS)"]
PL_DB[("SQLite: platform.db<br/>WAL")]
@@ -203,11 +203,11 @@ flowchart TB
**要点:**
- **只有两个后端可执行文件**`SimpleLite.exe``MiGu.Server.exe`RCSMonitor 不再有独立后端。
- **`MiGu.Server.exe` 由 SimpleLite 的 SystemMission 拉起**:用户只启动 SimpleLiteWeb-Enabled 时 `StartPlatformMission` 自动 spawn MiGu.Server,子进程心跳异常由 Bootstrapper 重启。
- **只有两个后端可执行文件**`Simple3.exe``MiGu.Server.exe`RCSMonitor 不再有独立后端。
- **`MiGu.Server.exe` 由 Simple3 的 SystemMission 拉起**:用户只启动 Simple3Web-Enabled 时 `StartPlatformMission` 自动 spawn MiGu.Server,子进程心跳异常由 Bootstrapper 重启。
- **`PL_Static` 同一进程双 SPA 托管**:浏览器访问路径决定加载哪份 dist,登录时也按路径决定 `scope` 参数。
- **`PL_YARP``/api/sl/*` 反代到 SimpleLite**:YARP 在请求转发前后挂中间件做 JWT 校验、权限码二次确认、写操作审计、`Idempotency-Key` 透传、Scope 检查(RCSMonitor 的请求仅允许命中 `/api/ops/*` 这一前缀)。
- **3D 由 SimpleLite 的 webVRender 提供**Vue 端通过 iframe 或 Web Component 嵌入 webVRender 的页面,业务交互(选择、点击坐标等)通过 `postMessage` 与 Vue 桥接。
- **`PL_YARP``/api/sl/*` 反代到 Simple3**:YARP 在请求转发前后挂中间件做 JWT 校验、权限码二次确认、写操作审计、`Idempotency-Key` 透传、Scope 检查(RCSMonitor 的请求仅允许命中 `/api/ops/*` 这一前缀)。
- **3D 由 Simple3 的 webVRender 提供**Vue 端通过 iframe 或 Web Component 嵌入 webVRender 的页面,业务交互(选择、点击坐标等)通过 `postMessage` 与 Vue 桥接。
- **Desktop-Only 模式**`SL_API``PL` 都不启动;Vue 无法访问;CycleGUI 是唯一入口。
- **Web-Enabled 模式**CycleGUI 退化为状态窗;所有业务操作通过 Vue 完成;硬性不允许同时本地操作。
@@ -219,8 +219,8 @@ flowchart TB
```mermaid
flowchart TB
Start([SimpleLite.exe 启动]) --> CheckPrev{simple.json 中<br/>runMode 已记忆?}
CheckPrev -- 否 --> Login[弹出登录窗<br/>SimpleLite Boot Login]
Start([Simple3.exe 启动]) --> CheckPrev{simple.json 中<br/>runMode 已记忆?}
CheckPrev -- 否 --> Login[弹出登录窗<br/>Simple3 Boot Login]
CheckPrev -- 是 --> AutoLogin[自动按记忆模式登录<br/>仍需鉴权身份]
Login --> Form
AutoLogin --> ModeSwitch
@@ -269,7 +269,7 @@ flowchart TB
| 元素 | 控件 | 说明 |
|------|------|------|
| 标题 | Label | "SimpleLite · 启动登录" |
| 标题 | Label | "Simple3 · 启动登录" |
| 用户名 | TextInput | 默认上次登录用户 |
| 密码 | TextInput (password) | — |
| 运行模式 | RadioButtons | `Desktop-Only` / `Web-Enabled` |
@@ -281,7 +281,7 @@ flowchart TB
| 登录按钮 | Button | 通过后销毁登录窗,进入选定模式 |
| 退出按钮 | Button | 不进入主程序,退出进程 |
> 实现位置:新增 `SimpleLite/UI/BootLoginPanel.cs`,由 `Program.Main` 在 `Enssentials.Load` 之后、`Startup.EntryPoint` 之前调用 `GUI.PromptAndWaitPanel(BootLoginPanel.Build())`(阻塞式模态)。
> 实现位置:新增 `Simple3/UI/BootLoginPanel.cs`,由 `Program.Main` 在 `Enssentials.Load` 之后、`Startup.EntryPoint` 之前调用 `GUI.PromptAndWaitPanel(BootLoginPanel.Build())`(阻塞式模态)。
#### simple.json 增量字段
@@ -310,18 +310,18 @@ flowchart TB
| 场景 | 行为 |
|------|------|
| Desktop-Only 运行中 | Kestrel 不监听任何端口;MiGu.Server 不 spawn;尝试连接 7001/8080 直接 ECONNREFUSED |
| Desktop-Only → Web-Enabled | **必须退出 SimpleLite 重新登录**;不支持热切(避免半启动状态) |
| Desktop-Only → Web-Enabled | **必须退出 Simple3 重新登录**;不支持热切(避免半启动状态) |
| Web-Enabled 运行中 | CycleGUI 主窗只显示状态窗(运行时长 / 在线用户 / 端口 / 主备状态)+「停止」按钮 |
| Web-Enabled 下用户尝试本地控件 | **所有业务面板硬性不渲染**;Workspace 视口也不显示;用户无法通过任何方式重新启用 |
| Web-Enabled 异常退出 | Watchdog 重新拉起,恢复到 Web-Enabled;不强迫用户重选 |
| 想取消"记住模式" | 启动时按住 `Shift` 或运行 `SimpleLite.exe --re-login` 强制弹出登录窗 |
| 想取消"记住模式" | 启动时按住 `Shift` 或运行 `Simple3.exe --re-login` 强制弹出登录窗 |
| 远程开关模式 | Platform 管理员可以发 `POST /api/sl/runmode/reset``rememberRunMode` 置为 `false`**仅在下次重启时生效** |
### 3.4 服务状态窗(Web-Enabled 唯一可见的 CycleGUI 面板)
```text
┌──────────────────────────────────────────┐
│ SimpleLite Service Status │
│ Simple3 Service Status │
├──────────────────────────────────────────┤
│ 模式: Web-Enabled │
│ 启动时间: 2026-05-18 09:00:00 │
@@ -348,7 +348,7 @@ flowchart TB
```mermaid
flowchart LR
A["SimpleLite.exe<br/>启动 + 登录"] --> Mode{运行模式}
A["Simple3.exe<br/>启动 + 登录"] --> Mode{运行模式}
Mode -- Desktop-Only --> D[CycleGUI 全功能 UI]
D --> SchedD[调度引擎]
@@ -360,7 +360,7 @@ flowchart LR
WHost --> WV[WebTerminal :8223<br/>webVRender]
WHost --> StatusPanel[CycleGUI 状态窗]
subgraph SysMis["SimpleLite SystemMission 体系"]
subgraph SysMis["Simple3 SystemMission 体系"]
E["StartPlatformMission<br/>(SystemMission 子类)"]
SM_Reg["SystemMissionRegistry"]
SM_Hb["HeartbeatChannel<br/>(named pipe)"]
@@ -380,24 +380,24 @@ flowchart LR
### 4.2 SystemMission 体系(v1.4 新增章节)
SimpleLite 现有 `Mission` 是业务任务(运送/搬运),新增 **`SystemMission`** 作为同一基类的"系统级 Mission",用于拉起并守护进程级服务。它**不参与调度引擎的 DispatchLoop**,由 Bootstrapper 直接驱动。
Simple3 现有 `Mission` 是业务任务(运送/搬运),新增 **`SystemMission`** 作为同一基类的"系统级 Mission",用于拉起并守护进程级服务。它**不参与调度引擎的 DispatchLoop**,由 Bootstrapper 直接驱动。
**关键类:**
| 类型 | 类型 / 命名空间 | 职责 |
|------|------------------|------|
| `SystemMission`(抽象) | `SimpleLite.RCS.SystemMissions` | 继承自现有 `Mission`,新增 `Spawn() / Stop() / Heartbeat()` 等抽象方法 |
| `StartPlatformMission` | `SimpleLite.RCS.SystemMissions` | 拉起 `MiGu.Server.exe`;通过命名管道 `\\.\pipe\simple-bootstrap` 接收心跳 |
| `SystemMissionRegistry` | `SimpleLite.Bootstrap` | 单例,注册所有 SystemMissionWeb-Enabled 启动时遍历 `Spawn()`;进程退出时遍历 `Stop()` |
| `ProcessHost` | `SimpleLite.Bootstrap` | 封装 `System.Diagnostics.Process` + 重定向标准输出到日志 + 退出码捕获 |
| `HeartbeatChannel` | `SimpleLite.Bootstrap` | 命名管道服务端;JSON 协议 `{ "ts", "rss", "queueDepth", "status" }`3 个周期未达 → 标记不健康 |
| `ProcessWatchdog` | `SimpleLite.Bootstrap` | 不健康时 `Stop() + Spawn()`;指数退避 1s → 2s → 4s → 8s → 30s(上限) |
| `RunMode`(枚举) | `SimpleLite.Bootstrap` | `DesktopOnly` / `WebEnabled` |
| `SystemMission`(抽象) | `Simple3.RCS.SystemMissions` | 继承自现有 `Mission`,新增 `Spawn() / Stop() / Heartbeat()` 等抽象方法 |
| `StartPlatformMission` | `Simple3.RCS.SystemMissions` | 拉起 `MiGu.Server.exe`;通过命名管道 `\\.\pipe\simple-bootstrap` 接收心跳 |
| `SystemMissionRegistry` | `Simple3.Bootstrap` | 单例,注册所有 SystemMissionWeb-Enabled 启动时遍历 `Spawn()`;进程退出时遍历 `Stop()` |
| `ProcessHost` | `Simple3.Bootstrap` | 封装 `System.Diagnostics.Process` + 重定向标准输出到日志 + 退出码捕获 |
| `HeartbeatChannel` | `Simple3.Bootstrap` | 命名管道服务端;JSON 协议 `{ "ts", "rss", "queueDepth", "status" }`3 个周期未达 → 标记不健康 |
| `ProcessWatchdog` | `Simple3.Bootstrap` | 不健康时 `Stop() + Spawn()`;指数退避 1s → 2s → 4s → 8s → 30s(上限) |
| `RunMode`(枚举) | `Simple3.Bootstrap` | `DesktopOnly` / `WebEnabled` |
**生命周期:**
```text
SimpleLite 启动 (Web-Enabled)
Simple3 启动 (Web-Enabled)
├─ Kestrel Up :7001/7002
├─ WebTerminal Up :8223
├─ SchedulerRuntime.Start()
@@ -414,7 +414,7 @@ SimpleLite 启动 (Web-Enabled)
Watchdog 触发 → ProcessHost.Kill() → 退避等待 → Spawn() 重试
连续 5 次失败 → 状态窗高亮 "Platform Down",停止重试,需人工介入
SimpleLite 优雅停机:
Simple3 优雅停机:
SystemMissionRegistry.StopAll()
└─ StartPlatformMission.Stop()
├─ 发送 SIGTERM 等价(命名管道 "shutdown" 消息)
@@ -424,26 +424,26 @@ SimpleLite 优雅停机:
**实现要点:**
- **位置**:新建 `SimpleLite/Bootstrap/``SimpleLite/RCS/SystemMissions/`
- **位置**:新建 `Simple3/Bootstrap/``Simple3/RCS/SystemMissions/`
- **不写入主库 Missions 表**SystemMission 是进程级配置,不参与项目序列化;存储在 `simple_main.db.SystemMissionConfig`(或直接读 `simple.json`)。
- **崩溃隔离**Platform 异常不影响 SimpleLite 调度循环;MiGu.Server 也实现 `IHostedService.StopAsync` 做 WAL Checkpoint + WS 排空 → 给 ROSE 释放磁盘。
- **多实例预防**:通过命名互斥体 + `simple.json.allowMultiple=false`(已有)确保 SimpleLite 唯一;MiGu.Server 启动时校验"父进程是否为 SimpleLite",否则拒绝启动(防止有人手动双击 `MiGu.Server.exe`)。
- **崩溃隔离**Platform 异常不影响 Simple3 调度循环;MiGu.Server 也实现 `IHostedService.StopAsync` 做 WAL Checkpoint + WS 排空 → 给 ROSE 释放磁盘。
- **多实例预防**:通过命名互斥体 + `simple.json.allowMultiple=false`(已有)确保 Simple3 唯一;MiGu.Server 启动时校验"父进程是否为 Simple3",否则拒绝启动(防止有人手动双击 `MiGu.Server.exe`)。
- **静态资源**MiGu.Server 用 `MapWhen + UseStaticFiles + UseSpa` 各挂一份 `index.html`
---
## 5. 三端职责矩阵
| 维度 | SimpleLite 桌面端 | **Platform 管理端(platform-vue** | **RCSMonitor 运营端(rcsmonitor-vue** |
| 维度 | Simple3 桌面端 | **Platform 管理端(platform-vue** | **RCSMonitor 运营端(rcsmonitor-vue** |
|------|---------------------|------------------------------------------|------------------------------------------|
| 地图(站点/轨道/区域)编辑 | **读写**Desktop-Only | **全功能读写**Vue 地图控件内嵌SimpleLite地图) | 只读投影,不可编辑 |
| 地图(站点/轨道/区域)编辑 | **读写**Desktop-Only | **全功能读写**Vue 地图控件内嵌Simple3地图) | 只读投影,不可编辑 |
| 任务编排(Mission/Recipe | **读写** | **全功能读写**(含 Demand / Recipe | 基础动作:暂停/取消/重派/优先级 |
| 车辆配置(参数 / 维护策略) | **读写** | **全功能读写** | 基础动作:暂停/继续/结束任务/上线/离线/手动充电等 |
| CAD 工具 | **可用** | **可用**Vue 调用 `/api/sl/cad/*` | 不可用 |
| 自定义字段 / 图层 | **可用** | **可用** | 只读 |
| 调度引擎(DispatchLoop) | 进程内拥有 | 远程订阅状态 + 全 API 操作 | 远程订阅状态 + 受限 API |
| 车端通信 | 拥有 | 不直连,只看投影 | 不直连,只看投影 |
| 3D 视口 | 本地 OpenGLCycleGUI 原生) | **嵌入 SimpleLite webVRender**(全功能交互) | **嵌入 SimpleLite webVRender**(只读 + 圈选) |
| 3D 视口 | 本地 OpenGLCycleGUI 原生) | **嵌入 Simple3 webVRender**(全功能交互) | **嵌入 Simple3 webVRender**(只读 + 圈选) |
| 系统级配置 | 局部(Desktop-Only | **全局编辑 + 下发** | 不可编辑 |
| 外部对接(MES/WMS/RCS | 不涉及 | **唯一入口** | 不涉及 |
| 路径规划/交通/充电策略 | 算法 + 调试 | **可视化编辑 + 下发** | 只读 |
@@ -549,7 +549,7 @@ Platform 将典型业务场景抽象为标准模板,支持开箱即用和后
```mermaid
flowchart TB
subgraph 顶层["顶层(按端隔离)"]
L1A["SimpleLite (CycleGUI + WebAPI Host)"]
L1A["Simple3 (CycleGUI + WebAPI Host)"]
L1B["MiGu.Server (ASP.NET Core + YARP)"]
L1B2["platform-vue (SPA, 管理员)"]
L1B3["rcsmonitor-vue (SPA, 运营)"]
@@ -608,17 +608,17 @@ flowchart TB
| `SimpleShared.Persistence` | net8.0 | EF Core 8 多 Provider 抽象(**SQLite / MySQL / PostgreSQL / SQL Server**+ 迁移 + `DbProviderOptions` + 连接串构建器 + `WalSqliteOptions`(仅 SQLite |
| `SimpleShared.OpsCommand` | net8.0 | 运维白名单命令、审计、幂等键、频次限流 |
| `SimpleShared.WebApiHost` | net8.0 | Kestrel 公共启动器:JWT 中间件 / Swagger / CORS / 健康检查 / 请求审计 |
| `MiGu.Server` | net8.0 ASP.NET Core | 业务 BFF + **YARP 反代 SimpleLite** + 静态托管两份 Vue dist;**启动时校验父进程为 SimpleLite,否则拒绝运行**(防误用) |
| `platform-vue` | Vue 3 + Vite + Pinia | 全功能套壳:复刻 SimpleLite 所有控件 |
| `MiGu.Server` | net8.0 ASP.NET Core | 业务 BFF + **YARP 反代 Simple3** + 静态托管两份 Vue dist;**启动时校验父进程为 Simple3,否则拒绝运行**(防误用) |
| `platform-vue` | Vue 3 + Vite + Pinia | 全功能套壳:复刻 Simple3 所有控件 |
| `rcsmonitor-vue` | Vue 3 + Vite + Pinia | 运营监控:按权限渲染,运维动作面板 |
| `packages/sl-controls`pnpm workspace | Vue 3 组件库 | Workspace3D(嵌 webVRender/ 地图列表 / 任务表 / CAD 工具栏等共享组件 |
### 6.1 Platform Vue 控件复刻清单(与 SimpleLite CycleGUI 对照)
### 6.1 Platform Vue 控件复刻清单(与 Simple3 CycleGUI 对照)
| SimpleLite CycleGUI 面板 | Vue 组件(位于 `sl-controls` | 备注 |
| Simple3 CycleGUI 面板 | Vue 组件(位于 `sl-controls` | 备注 |
|--------------------------|--------------------------------|------|
| 主菜单栏 / 工具栏 | `<AppShell>` 顶栏 + 侧栏 | platform-vue 全功能;rcsmonitor-vue 仅显示运营菜单 |
| Workspace 3D 视口 | `<Workspace3D>` | **iframe / Web Component 嵌入 SimpleLite webVRender**;交互事件用 `postMessage` 桥接 |
| Workspace 3D 视口 | `<Workspace3D>` | **iframe / Web Component 嵌入 Simple3 webVRender**;交互事件用 `postMessage` 桥接 |
| 地图面板(Sites / Tracks | `<MapEditor>` + `<TrackTable>` | 表格 + 拓扑视图 |
| Car 面板 | `<CarPanel>` | 表格 + 表单 |
| Mission 面板 | `<MissionEditor>` | 任务流图 + 表单 |
@@ -674,7 +674,7 @@ function onReady() {
</script>
```
`SimpleLite/Rendering/SimpleSceneRenderer.cs` 端按 `scope``ro` 控制:
`Simple3/Rendering/SimpleSceneRenderer.cs` 端按 `scope``ro` 控制:
- `scope=RCSMonitor``ro=1` → 禁用编辑工具(不响应 `SelectObject`/`GetPosition` 中的拖动)
- 通过 `postMessage` 向 iframe 父页面回传选择/点击事件
@@ -690,7 +690,7 @@ builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(ctx =>
{
// 透传当前 JWT 的 userId / scope 给 SimpleLite
// 透传当前 JWT 的 userId / scope 给 Simple3
ctx.AddRequestTransform(async tc =>
{
var ident = tc.HttpContext.User;
@@ -767,7 +767,7 @@ app.Run();
```mermaid
flowchart TB
subgraph App["应用代码 (SimpleLite / MiGu.Server)"]
subgraph App["应用代码 (Simple3 / MiGu.Server)"]
DOM["领域服务 / Repository"]
EFC["EF Core 8 DbContext"]
OPT["DbProviderOptions<br/>{ provider, conn, ha }"]
@@ -813,7 +813,7 @@ flowchart TB
| 逻辑库 | 拥有进程 | 主要表/数据 |
|--------|----------|-------------|
| **simple_main** | SimpleLite | Maps, Sites, Tracks, SpecialProps, Missions, Cars, Plugins, DesignAuditLog, OpsAuditLog, LocalUsers, RunModeHistory |
| **simple_main** | Simple3 | Maps, Sites, Tracks, SpecialProps, Missions, Cars, Plugins, DesignAuditLog, OpsAuditLog, LocalUsers, RunModeHistory |
| **platform** | Platform | Users, Roles, RolePermissions, WidgetGrants, SystemConfig, ExternalAdapters, RoutingPolicies, VehicleMaintenancePolicies, ChargePolicies, TaskAllocationPolicies, TrafficRules, Locations, Inventory, OpsLogs(含 RCSMonitor 来源), Annotations, Snapshots, Versions, CustomWidgets |
> 原 v1.2 中的 `rcsmonitor_local` 取消;其表(OpsLogs / Annotations / Snapshots / WidgetLayoutCache)合并到 `platform`,加 `source='RCSMonitor'` 字段区分。
@@ -824,12 +824,12 @@ flowchart TB
```mermaid
flowchart TB
subgraph A["主节点 NODE-A (Active)"]
SL_A["SimpleLite.exe"]
SL_A["Simple3.exe"]
PL_A["MiGu.Server.exe"]
D_A[("受 ROSE 保护盘 R:\<br/>simple_main.db<br/>platform.db<br/>(WAL)")]
end
subgraph B["备节点 NODE-B (Standby)"]
SL_B["SimpleLite.exe (suspended)"]
SL_B["Simple3.exe (suspended)"]
PL_B["MiGu.Server.exe (suspended)"]
D_B[("ROSE 镜像盘 R:\")]
end
@@ -860,7 +860,7 @@ flowchart TB
|------|------|------|
| `RES_DISK_R` | Mirror Disk | — |
| `RES_VIP_LAN` | Virtual IP | RES_DISK_R |
| `RES_SVC_SL` | Service: SimpleLite (Web-Enabled) | RES_DISK_R, RES_VIP_LAN |
| `RES_SVC_SL` | Service: Simple3 (Web-Enabled) | RES_DISK_R, RES_VIP_LAN |
| `RES_SVC_PL` | Service: MiGu.Server | RES_DISK_R, RES_VIP_LAN, RES_SVC_SL |
| `RES_SCRIPT_CHK` | 数据库健康检查脚本 | RES_DISK_R |
@@ -871,7 +871,7 @@ flowchart TB
```mermaid
flowchart TB
subgraph App["应用层 (VIP + 重试)"]
SL["SimpleLite.exe"]
SL["Simple3.exe"]
PL["MiGu.Server.exe"]
end
subgraph Router["MySQL Router (或 ProxySQL)"]
@@ -911,7 +911,7 @@ flowchart TB
```mermaid
flowchart TB
subgraph App["应用层"]
SL["SimpleLite.exe"]
SL["Simple3.exe"]
PL["MiGu.Server.exe"]
end
subgraph LB["HAProxy / PgBouncer"]
@@ -953,7 +953,7 @@ flowchart TB
```mermaid
flowchart TB
subgraph App["应用层"]
SL["SimpleLite.exe"]
SL["Simple3.exe"]
PL["MiGu.Server.exe"]
end
subgraph Listener["AG Listener (DNS + VIP)"]
@@ -983,7 +983,7 @@ flowchart TB
- **EF Core Provider**`Microsoft.EntityFrameworkCore.SqlServer`;支持 SQL Server 2019+(建议 Enterprise 版以启用同步提交副本数 ≥3 + Read-Only Routing)。Standard 版可降级为 2 副本基本可用性组。
- **HA 形态****WSFCWindows Server Failover Cluster+ Always On Availability Groups**;通过 **AG Listener** 暴露统一虚拟名/VIP,客户端连 Listener 名即可。
- **同步策略**:核心库(`platform` / `simple_main`)配 1 个同步提交副本(RPO=0),1–N 个异步副本(可读分析)。
- **认证**:建议混合身份认证;MiGu.Server 用 SQL LoginSimpleLite 桌面端可走 Windows 集成认证。
- **认证**:建议混合身份认证;MiGu.Server 用 SQL LoginSimple3 桌面端可走 Windows 集成认证。
- **TempDB**:每节点分摊;MultipleActiveResultSetsMARS)建议关闭,EF Core 已能避免。
- **备份**`BACKUP DATABASE ... WITH COMPRESSION` + 事务日志备份每 15min;保留策略与公司 IT 一致。
- **RTO / RPO**RTO ≈ 1030sAG 自动 failover);RPO ≈ 0(同步提交副本)。
@@ -1091,7 +1091,7 @@ flowchart TB
"database": "simple_main",
"user": "simple_app",
"password": "${SECRET_MSSQL_PWD}",
"extra": "Encrypt=True;TrustServerCertificate=False;MultiSubnetFailover=True;Application Name=SimpleLite"
"extra": "Encrypt=True;TrustServerCertificate=False;MultiSubnetFailover=True;Application Name=Simple3"
},
"platform": {
"server": "tcp:rcs-ag.lan,1433",
@@ -1158,7 +1158,7 @@ classDiagram
**落地四件套:**
1. **认证**`SimpleShared.Auth` 提供 `JwtIssuer`Web-Enabled 时 MiGu.Server 签发;Desktop-Only 时 SimpleLite 本地签发短期 Token(24h)。
1. **认证**`SimpleShared.Auth` 提供 `JwtIssuer`Web-Enabled 时 MiGu.Server 签发;Desktop-Only 时 Simple3 本地签发短期 Token(24h)。
2. **授权**:所有 WebAPI + WS 调用走 `IPermissionEvaluator`;权限码 `<area>.<action>`
3. **范围(Scope**`Lite` / `Platform` / `RCSMonitor` / `*`;同一用户在不同端可用集合不同。Vue 端登录时由前端代码根据访问路径自动带入 `scope``/admin/*``Platform``/monitor/*``RCSMonitor`)。
4. **控件级权限**:除 `Permission` 外,角色还可关联 `WidgetGrant`,每个 `WidgetId` 三档可见性:
@@ -1173,7 +1173,7 @@ sequenceDiagram
participant U as 运营用户
participant RV as rcsmonitor-vue
participant P as MiGu.Server
participant L as SimpleLite (YARP 后侧)
participant L as Simple3 (YARP 后侧)
participant DBp as platform.db
U->>RV: 访问 https://host:8080/monitor
@@ -1255,7 +1255,7 @@ flowchart LR
UI11 --> Sch
Sch --> Val --> Ver --> Pub
Pub -- "REST PUT /api/sl/config/{section} (via YARP)" --> SL["SimpleLite (主服务)"]
Pub -- "REST PUT /api/sl/config/{section} (via YARP)" --> SL["Simple3 (主服务)"]
Pub -- "WS broadcast: config.updated" --> RMVue["rcsmonitor-vue"]
Pub -- "WS broadcast: config.updated" --> AdminVue["其他在线 platform-vue"]
```
@@ -1316,7 +1316,7 @@ public record EffectivePermissions(
List<WidgetGrantDto> VisibleWidgets);
```
**发布机制:** Schema → 校验 → 版本 → 灰度 → 全量;MiGu.Server 通过 YARP `PUT /api/sl/config/{section}` 推到 SimpleLite,主服务原子切换 + ack;失败自动回滚,WS `config.rollback` 广播。
**发布机制:** Schema → 校验 → 版本 → 灰度 → 全量;MiGu.Server 通过 YARP `PUT /api/sl/config/{section}` 推到 Simple3,主服务原子切换 + ack;失败自动回滚,WS `config.rollback` 广播。
---
@@ -1327,14 +1327,14 @@ public record EffectivePermissions(
```mermaid
sequenceDiagram
participant U as 用户
participant L as SimpleLite (CycleGUI)
participant L as Simple3 (CycleGUI)
participant BL as BootLoginPanel
participant K as Kestrel (WebAPI)
participant P as MiGu.Server (YARP)
participant V as platform-vue
participant RV as rcsmonitor-vue
U->>L: 启动 SimpleLite.exe
U->>L: 启动 Simple3.exe
L->>L: 读取 simple.json
alt rememberRunMode = true
L->>L: 直接采用 runMode (仍鉴权)
@@ -1373,10 +1373,10 @@ sequenceDiagram
```mermaid
sequenceDiagram
participant U as 用户
participant L as SimpleLite
participant L as Simple3
participant BL as BootLoginPanel
U->>L: 启动 SimpleLite.exe
U->>L: 启动 Simple3.exe
L->>L: 读 simple.json (无记忆 或 选项=DesktopOnly)
L->>BL: 必要时弹出登录窗
U->>BL: 输入凭据 + 选 Desktop-Only
@@ -1387,13 +1387,13 @@ sequenceDiagram
L-->>U: 进入设计端主界面
```
### 10.3 platform-vue 编辑地图站点 → SimpleLite 落库
### 10.3 platform-vue 编辑地图站点 → Simple3 落库
```mermaid
sequenceDiagram
participant V as platform-vue
participant P as MiGu.Server (YARP)
participant L as SimpleLite (WebAPI)
participant L as Simple3 (WebAPI)
participant DBl as simple_main.db
participant RV as rcsmonitor-vue (订阅)
@@ -1406,7 +1406,7 @@ sequenceDiagram
P-->>V: 201 + siteId
L->>L: WS broadcast { type: "map.site.created", payload }
L-->>P: WS event
P-->>V: WS event (前端 Workspace3D iframe 收到 SimpleLite 推送的 PutModelObject 同步)
P-->>V: WS event (前端 Workspace3D iframe 收到 Simple3 推送的 PutModelObject 同步)
P-->>RV: WS event (RCSMonitor 接收并按权限展示)
```
@@ -1416,7 +1416,7 @@ sequenceDiagram
sequenceDiagram
participant V as platform-vue
participant P as Platform
participant L as SimpleLite
participant L as Simple3
participant E as DispatchLoop
participant RV as rcsmonitor-vue
@@ -1440,7 +1440,7 @@ sequenceDiagram
participant U as 运营人员
participant RV as rcsmonitor-vue
participant P as MiGu.Server (YARP)
participant L as SimpleLite (Ops 网关)
participant L as Simple3 (Ops 网关)
participant E as DispatchLoop
participant DBp as platform.db
participant DBl as simple_main.db
@@ -1476,10 +1476,10 @@ sequenceDiagram
VIP->>NA: 路由到 Active
Note over NA: 节点故障
ROSE->>NA: 心跳超时 → 触发切换
ROSE->>NA: 强制停止 SimpleLite / Platform
ROSE->>NA: 强制停止 Simple3 / Platform
ROSE->>NA: 卸载 R: 卷
ROSE->>NB: 挂载 R: 卷
ROSE->>NB: 启动 SimpleLite (WAL 重放 + 主库自检)
ROSE->>NB: 启动 Simple3 (WAL 重放 + 主库自检)
ROSE->>NB: 启动 MiGu.Server
ROSE->>VIP: 浮动 IP 漂移到 NODE-B
CLI--xVIP: 短暂中断
@@ -1495,12 +1495,12 @@ sequenceDiagram
| 通道 | 协议 | 编码 | 端点 | 用途 |
|------|------|------|------|------|
| MiGu.Server (YARP) ↔ SimpleLite | REST / WebSocket | JSON | `:7001` / `:7002` | 全功能 API 反代 + 事件订阅 |
| MiGu.Server (YARP) ↔ Simple3 | REST / WebSocket | JSON | `:7001` / `:7002` | 全功能 API 反代 + 事件订阅 |
| platform-vue ↔ MiGu.Server | REST / WebSocket | JSON | `:8080` / `:8081` (路径前缀 `/admin` | UI 操作、事件推送 |
| rcsmonitor-vue ↔ MiGu.Server | REST / WebSocket | JSON | `:8080` / `:8081` (路径前缀 `/monitor``scope=RCSMonitor`) | UI 操作、事件推送(受限) |
| Vue Workspace3D ↔ SimpleLite WebTerminal | WSSwebVRender 协议) | webVRender 自有 | `:8223` | 3D 视口同步(嵌入 iframe |
| Vue Workspace3D ↔ Simple3 WebTerminal | WSSwebVRender 协议) | webVRender 自有 | `:8223` | 3D 视口同步(嵌入 iframe |
| 子进程心跳 | Named Pipe | JSON | `\\.\pipe\simple-bootstrap` | 守护 / 重启 |
| Vehicle ↔ SimpleLite | TCP / WS / OPC-UA | 业务私有 | `:8222` | 车端协议 |
| Vehicle ↔ Simple3 | TCP / WS / OPC-UA | 业务私有 | `:8222` | 车端协议 |
| ROSE 心跳 | 专用网卡 + 串口 | ROSE 私有 | — | 节点健康监测 |
| ROSE 镜像复制 | 专用网卡 | ROSE 私有 | — | 块级实时数据同步 |
@@ -1510,7 +1510,7 @@ sequenceDiagram
- 鉴权:`Authorization: Bearer <JWT>`;服务间互调附 `X-Svc-Token` + `X-On-Behalf-Of: <userId>` + `X-Scope: <scope>`
- 幂等:写操作支持 `Idempotency-Key` 请求头
- 版本化:配置类资源使用 `ETag` / `If-Match` 做乐观锁
- SwaggerPlatform / SimpleLite 都暴露 `/swagger`(生产可关)
- SwaggerPlatform / Simple3 都暴露 `/swagger`(生产可关)
- **YARP Scope 白名单**`scope=RCSMonitor` 时仅允许 `/api/sl/ops/*``/api/sl/projection/*` 命中;其他路径直接 403
---
@@ -1521,12 +1521,12 @@ sequenceDiagram
flowchart TB
subgraph Cluster["双机 ROSE HA 集群 (Web-Enabled)"]
subgraph NA["NODE-A (Active)"]
SL_A["SimpleLite.exe<br/>Kestrel :7001/7002<br/>WebTerminal :8223"]
SL_A["Simple3.exe<br/>Kestrel :7001/7002<br/>WebTerminal :8223"]
PL_A["MiGu.Server.exe<br/>:8080/8081<br/>(YARP + 双 Vue dist)"]
R_A["ROSE 受保护盘 R:\<br/>两库 (WAL)"]
end
subgraph NB["NODE-B (Standby)"]
SL_B["SimpleLite.exe (suspended)"]
SL_B["Simple3.exe (suspended)"]
PL_B["MiGu.Server.exe (suspended)"]
R_B["ROSE 镜像盘 R:\<br/>(实时同步)"]
end
@@ -1556,12 +1556,12 @@ flowchart TB
| 端口 | 服务 | 启用条件 |
|------|------|----------|
| 7001 | SimpleLite WebAPI | Web-Enabled |
| 7002 | SimpleLite WebSocket | Web-Enabled |
| 7001 | Simple3 WebAPI | Web-Enabled |
| 7002 | Simple3 WebSocket | Web-Enabled |
| 8080 | Platform WebAPI + platform-vue 静态(/admin+ rcsmonitor-vue 静态(/monitor | Web-Enabled |
| 8081 | Platform WebSocket | Web-Enabled |
| 8222 | SimpleLite 车端 / API(现有) | 所有模式 |
| 8223 | SimpleLite WebTerminalwebVRenderVue 嵌入用) | Web-Enabled |
| 8222 | Simple3 车端 / API(现有) | 所有模式 |
| 8223 | Simple3 WebTerminalwebVRenderVue 嵌入用) | Web-Enabled |
> Desktop-Only 模式下:7001 / 7002 / 8080 / 8081 / 8223 全部**不监听**。
@@ -1588,7 +1588,7 @@ flowchart LR
SHH["SimpleShared.WebApiHost"]
end
subgraph apps["两端后端"]
SL["SimpleLite (CycleGUI + WebAPI)"]
SL["Simple3 (CycleGUI + WebAPI)"]
PSrv["MiGu.Server (WebAPI + YARP)"]
end
subgraph frontends["前端 (pnpm workspace)"]
@@ -1709,7 +1709,7 @@ pnpm --filter rcsmonitor-vue build # 产出 apps/rcsmonitor-vue/dist
|--------|----------|----------|
| **M0 - 内核下沉** | 1 周 | 新建 SimpleShared.* 6 个工程;Configuration → IConfigSnapshotProviderEF Core + SQLite + 迁移 |
| **M1 - 启动登录 + 运行模式** | 1 周 | `BootLoginPanel``RunMode` 枚举;CycleGUI 状态窗;`simple.json.runMode` 持久化;Web-Enabled 业务面板硬隐藏 |
| **M2 - WebAPI 协议层** | 1.5 周 | SimpleLite 集成 Kestrel;定义 `/api/projection/*``/api/ops/*``/api/config/*``/api/map/*` 等;WS HubJWT |
| **M2 - WebAPI 协议层** | 1.5 周 | Simple3 集成 Kestrel;定义 `/api/projection/*``/api/ops/*``/api/config/*``/api/map/*` 等;WS HubJWT |
| **M3 - 共享前端组件库** | 1.5 周 | `frontends/packages/sl-controls`Workspace3D(嵌入 webVRender+ 地图列表 + 任务表 + CAD 工具栏 + OpsActionPanel 等 |
| **M4 - Platform 后端 + YARP** | 2 周 | MiGu.ServerWebAPI + Swagger + YARP `/api/sl/*` + Scope 中间件 + 静态托管双 Vue dist+ 配置中心 11 个 schema + 用户/角色/控件权限 |
| **M5 - platform-vue (管理员)** | 2.5 周 | 全功能套壳:登录、地图设计、车辆/任务、CAD、字段/图层、配置中心、库位、自定义控件、回放、设备管理、车队生命周期、场景模板 |
@@ -1726,14 +1726,14 @@ pnpm --filter rcsmonitor-vue build # 产出 apps/rcsmonitor-vue/dist
|---|------|------|
| 1 | 运行模式持久化 | **写入 `simple.json.runMode` / `rememberRunMode`**(不使用 %APPDATA% |
| 2 | Web-Enabled 下是否允许同时本地操作 | **不允许**CycleGUI 仅状态窗;不提供"临时启用本地 UI"开关 |
| 3 | 3D 引擎 | **复用 SimpleLite WebTerminal 的 webVRender**Vue 通过 iframe / Web Component 嵌入 |
| 3 | 3D 引擎 | **复用 Simple3 WebTerminal 的 webVRender**Vue 通过 iframe / Web Component 嵌入 |
| 4 | 反向代理 | **YARP**MiGu.Server 用 YARP 反代 `/api/sl/*` |
| 5 | RCSMonitor 是否需要独立后端 | **否**,完全依赖 MiGu.Server;通过 `scope=RCSMonitor` + 角色 + WidgetGrant 限权 |
| 6 | 共享前端组件库 | **pnpm workspace + `frontends/packages/sl-controls`** |
| 7 | 运营白名单 | 采纳 §5.1 列表(10 条,后续可扩) |
| 8 | ROSE 复制粒度 | 块级镜像盘(RPO≈0)首选;视现场版本兜底文件级 |
| 9 | 车端连接策略 | 走 VIP,统一切换;车端实现 WS 自动重连 |
| 10 | **Platform / RCSMonitor 启动归属(v1.4** | **由 SimpleLite 内置 `SystemMission.StartPlatformMission` 拉起并守护**;不允许独立安装 MiGu.Server.exe 为 Windows ServiceMiGu.Server 启动时校验父进程必须是 SimpleLite |
| 10 | **Platform / RCSMonitor 启动归属(v1.4** | **由 Simple3 内置 `SystemMission.StartPlatformMission` 拉起并守护**;不允许独立安装 MiGu.Server.exe 为 Windows ServiceMiGu.Server 启动时校验父进程必须是 Simple3 |
| 11 | **持久层多 Providerv1.5** | EF Core 抽象,**默认 SQLite + ROSE HA**;可选 **MySQL InnoDB Cluster / PostgreSQL Patroni / SQL Server Always On AG**;每 Provider 一套 EF MigrationsCI 全矩阵跑通;运行期由 `simple.json.database.provider` 决定 |
| 12 | **跨 Provider 切换策略** | 不支持热切;切换走停服窗口 + `tools/db/migrate.ps1`;旧文件作为回退包保留 |
@@ -1743,22 +1743,22 @@ pnpm --filter rcsmonitor-vue build # 产出 apps/rcsmonitor-vue/dist
| 现有文件 | 改动方向 |
|----------|----------|
| `SimpleLite/Program.cs` | 在 `Enssentials.Load` 之后插入 `BootLoginPanel.RunModal()`;根据返回的 `RunMode` 决定后续路径 |
| `SimpleLite/Configuration.cs` | 拆为 `LocalBootConfig`(启动期,含 `runMode``rememberRunMode`、监听地址、各端口)+ `IConfigSnapshotProvider`(运行期);数据库路径走 `%SIMPLE_DATA_ROOT%` |
| `SimpleLite/Startup.cs` | 按 `RunMode` 分支:Desktop-Only 走原有 CycleGUIWeb-Enabled 启动 Kestrel + WebAPI + WebTerminal + Spawn Platform + 切换 CycleGUI 到状态窗 |
| `SimpleLite/UI/SimpleUI.cs` | 增加 `RunMode.WebEnabled` 时**所有业务面板硬性不创建**;新增"服务状态窗"面板(唯一允许) |
| `SimpleLite/UI/BootLoginPanel.cs`(新增) | 启动登录窗实现 + 模式持久化 |
| `SimpleLite/UI/ServiceStatusPanel.cs`(新增) | Web-Enabled 状态窗实现 |
| `SimpleLite/RCS/Mission.cs` | 抽出 `SystemMission` 抽象基类(与业务 Mission 同一基类但走独立 Registry**不进入 DispatchLoop** |
| `SimpleLite/RCS/SystemMissions/StartPlatformMission.cs`(新增) | 拉起 `MiGu.Server.exe`、绑定命名管道、上报心跳与状态到 ServiceStatusPanel |
| `SimpleLite/Bootstrap/ProcessHost.cs`(新增) | 封装 `System.Diagnostics.Process` + 日志重定向 + 优雅停止 |
| `SimpleLite/Bootstrap/HeartbeatChannel.cs`(新增) | 命名管道服务端 + JSON 心跳协议 |
| `SimpleLite/Bootstrap/SystemMissionRegistry.cs`(新增) | 注册/批量 Spawn/Stop SystemMission |
| `SimpleLite/Bootstrap/ProcessWatchdog.cs`(新增) | 指数退避重启 + 多次失败后熔断并通知状态窗 |
| `SimpleLite/Bootstrap/RunMode.cs`(新增) | `DesktopOnly` / `WebEnabled` 枚举 + 启动期校验 |
| `SimpleLite/Rendering/SimpleSceneRenderer.cs` | 按 webVRender 的 `?scope=` / `?ro=` 参数禁用编辑工具 |
| `Simple3/Program.cs` | 在 `Enssentials.Load` 之后插入 `BootLoginPanel.RunModal()`;根据返回的 `RunMode` 决定后续路径 |
| `Simple3/Configuration.cs` | 拆为 `LocalBootConfig`(启动期,含 `runMode``rememberRunMode`、监听地址、各端口)+ `IConfigSnapshotProvider`(运行期);数据库路径走 `%SIMPLE_DATA_ROOT%` |
| `Simple3/Startup.cs` | 按 `RunMode` 分支:Desktop-Only 走原有 CycleGUIWeb-Enabled 启动 Kestrel + WebAPI + WebTerminal + Spawn Platform + 切换 CycleGUI 到状态窗 |
| `Simple3/UI/SimpleUI.cs` | 增加 `RunMode.WebEnabled` 时**所有业务面板硬性不创建**;新增"服务状态窗"面板(唯一允许) |
| `Simple3/UI/BootLoginPanel.cs`(新增) | 启动登录窗实现 + 模式持久化 |
| `Simple3/UI/ServiceStatusPanel.cs`(新增) | Web-Enabled 状态窗实现 |
| `Simple3/RCS/Mission.cs` | 抽出 `SystemMission` 抽象基类(与业务 Mission 同一基类但走独立 Registry**不进入 DispatchLoop** |
| `Simple3/RCS/SystemMissions/StartPlatformMission.cs`(新增) | 拉起 `MiGu.Server.exe`、绑定命名管道、上报心跳与状态到 ServiceStatusPanel |
| `Simple3/Bootstrap/ProcessHost.cs`(新增) | 封装 `System.Diagnostics.Process` + 日志重定向 + 优雅停止 |
| `Simple3/Bootstrap/HeartbeatChannel.cs`(新增) | 命名管道服务端 + JSON 心跳协议 |
| `Simple3/Bootstrap/SystemMissionRegistry.cs`(新增) | 注册/批量 Spawn/Stop SystemMission |
| `Simple3/Bootstrap/ProcessWatchdog.cs`(新增) | 指数退避重启 + 多次失败后熔断并通知状态窗 |
| `Simple3/Bootstrap/RunMode.cs`(新增) | `DesktopOnly` / `WebEnabled` 枚举 + 启动期校验 |
| `Simple3/Rendering/SimpleSceneRenderer.cs` | 按 webVRender 的 `?scope=` / `?ro=` 参数禁用编辑工具 |
| `SimpleScheduler/WebApi.cs` | 调用方从 Nancy 切到 ASP.NET Core Controller;保持 `SchedulerRuntime` 外观稳定 |
| `SimpleLite/SimpleProject.cs` | 持久化双写:JSON(兼容)+ SQLite(新主存,受 ROSE 保护盘) |
| `Simple3/SimpleProject.cs` | 持久化双写:JSON(兼容)+ SQLite(新主存,受 ROSE 保护盘) |
| `Plugins/Sample` | 示例插件读取 `IConfigSnapshotProvider`;不再读 `Configuration.conf` |
| 新增 `tools/ha/*` | ROSE 资源启停脚本、健康检查脚本、切换演练脚本 |
| 新增 `tools/db/*` | 跨 Provider 迁移脚本(`migrate.ps1` / `dump.ps1` / `restore.ps1`);按目标 DB 校验 schema 与索引 |
@@ -1909,12 +1909,12 @@ pnpm --filter rcsmonitor-vue build # 产出 apps/rcsmonitor-vue/dist
|---|-----|------|------|
| B1 | Kestrel + Swagger + CORS | ✅ | `Program.cs` |
| B2 | Auth / Health / Config / Ops / Projection 控制器 | ✅ 骨架 | Mock/内存存储 |
| B3 | YARP `/api/sl/*` → :8222 | ✅ 配置 | 依赖 SimpleLite 实际监听 |
| B3 | YARP `/api/sl/*` → :8222 | ✅ 配置 | 依赖 Simple3 实际监听 |
| B4 | 静态托管 + SPA fallback | ✅ | 单一 `wwwroot/index.html` |
| B5 | JWT 真签发 + 库持久化 | ☐ | 仍为 Mock |
| B6 | 父进程 SimpleLite 校验 | ☐ | §4、§15 #10 |
| B6 | 父进程 Simple3 校验 | ☐ | §4、§15 #10 |
### 18.3 SimpleLite / 共享内核(ARCHITECTURE 主体)
### 18.3 Simple3 / 共享内核(ARCHITECTURE 主体)
| # | 项 | 状态 | 备注 |
|---|-----|------|------|
@@ -1942,5 +1942,5 @@ pnpm --filter rcsmonitor-vue build # 产出 apps/rcsmonitor-vue/dist
## 关联文档
- [SIMPLELITE_DEVELOPMENT_PLAN.md](./SIMPLELITE_DEVELOPMENT_PLAN.md) — SimpleLite 功能对齐执行计划
- [SIMPLELITE_DEVELOPMENT_PLAN.md](./SIMPLELITE_DEVELOPMENT_PLAN.md) — Simple3 功能对齐执行计划
- [SIMPLELITE_COMPOSER_PARITY_PLAN.md](./SIMPLELITE_COMPOSER_PARITY_PLAN.md) — 与 SimpleComposer 差距分析
+19 -19
View File
@@ -1,19 +1,19 @@
# SimpleLite 面向咪咕平台数据 API 文档
# Simple3 面向咪咕平台数据 API 文档
> **版本**:与 SimpleLite 源码同步
> **版本**:与 Simple3 源码同步
> **更新日期**2026-06-18
> **服务栈**EmbedIO WebApi(非 ASP.NET Core
> **OpenAPI**`Docs/openapi/simplelite-projection.json`(已在 MiGu.Server Swagger UI 中展示)
> **OpenAPI**`Docs/openapi/simple3-projection.json`(已在 MiGu.Server Swagger UI 中展示)
---
## 1. 架构与访问路径
SimpleLite 在进程内启动 HTTP 投影服务,默认监听 `http://127.0.0.1:8222`。咪咕平台后端 **MiGu.Server** 通过 YARP 反向代理将请求转发到 SimpleLite
Simple3 在进程内启动 HTTP 投影服务,默认监听 `http://127.0.0.1:8222`。咪咕平台后端 **MiGu.Server** 通过 YARP 反向代理将请求转发到 Simple3
| 层级 | 前缀 | 说明 |
|------|------|------|
| 直连 SimpleLite | `http://127.0.0.1:8222/projection/...` | 开发调试、本机回环 |
| 直连 Simple3 | `http://127.0.0.1:8222/projection/...` | 开发调试、本机回环 |
| 经 MiGu.Server | `http://{host}:8080/api/sl/projection/...` | 平台前端/运维正式入口 |
```
@@ -23,7 +23,7 @@ SimpleLite 在进程内启动 HTTP 投影服务,默认监听 `http://127.0.0.1
MiGu.Server :8080 (/api/sl/*)
│ 追加 X-Platform-Internal-Token
SimpleLite :8222 (/projection/*)
Simple3 :8222 (/projection/*)
```
### 1.1 鉴权
@@ -31,12 +31,12 @@ SimpleLite :8222 (/projection/*)
| 场景 | 要求 |
|------|------|
| 经 MiGu.Server 访问 | 需登录 JWT`Authorization: Bearer` 或 Cookie `simple.auth.token`);YARP 自动注入 `X-Platform-Internal-Token` |
| 直连 SimpleLite | 本机回环(127.0.0.1 / ::1)默认放行;远程需携带与 MiGu.Server 共享的 `X-Platform-Internal-Token` |
| 直连 Simple3 | 本机回环(127.0.0.1 / ::1)默认放行;远程需携带与 MiGu.Server 共享的 `X-Platform-Internal-Token` |
Token 解析优先级(SimpleLite 侧):
Token 解析优先级(Simple3 侧):
1. `simple.json``platform.internalToken`
2. 环境变量 `SIMPLELITE__PLATFORM__INTERNALTOKEN`
2. 环境变量 `SIMPLE3__PLATFORM__INTERNALTOKEN`
3. `Platform.Server/data/.internal-token``MiGu.Server/data/.internal-token`
### 1.2 通用响应格式
@@ -60,8 +60,8 @@ Token 解析优先级(SimpleLite 侧):
开发环境下启动 MiGu.Server 后访问:
- **统一文档**`http://localhost:8080/swagger`(含 MiGu.Server 自有 API + SimpleLite 全部 WebApi
- SimpleLite 相关接口标签前缀为 `SimpleLite/`
- **统一文档**`http://localhost:8080/swagger`(含 MiGu.Server 自有 API + Simple3 全部 WebApi
- Simple3 相关接口标签前缀为 `Simple3/`
---
@@ -112,7 +112,7 @@ Token 解析优先级(SimpleLite 侧):
{
"id": "C01",
"name": "AGV-1",
"typeName": "SimpleLite.RCS.CarTypes.Car",
"typeName": "Simple3.RCS.CarTypes.Car",
"rawId": 1,
"x": 1200.5,
"y": 800.0,
@@ -285,7 +285,7 @@ Token 解析优先级(SimpleLite 侧):
同一车型若多个 Coder 引用不同 Fields 类型,接口会合并后取**最派生、字段最全**的类型(如 Kiva 取 `KivaTrackFields` 而非 `BasicTrackFields`)。返回的 `fields` 数组**包含继承链上全部 public 字段**(例如 `KivaTrackFields` 会同时包含 `BasicTrackFields``IOArea``Speed` 等基类字段)。
> **实现说明**:Fields 类在插件程序集中多为 `internal`;插件已通过 `InternalsVisibleTo("SimpleLite")` 向 SimpleLite 开放。**插件在 collectible ALC 中加载时**SimpleCore 特性类型与宿主不一致,须通过 `CustomAttributeData` 读取 Coder 上的 `siteFields`/`trackFields` 等 metadata(不能依赖 `GetCustomAttributes(typeof(TemplateTrackCoderSettings))`)。`StandardScene.dll` 内 Fields 由 `CoderFieldsMetadata.Describe` 在 Core 程序集内反射导出。
> **实现说明**:Fields 类在插件程序集中多为 `internal`;插件已通过 `InternalsVisibleTo("Simple3")` 向 Simple3 开放。**插件在 collectible ALC 中加载时**SimpleCore 特性类型与宿主不一致,须通过 `CustomAttributeData` 读取 Coder 上的 `siteFields`/`trackFields` 等 metadata(不能依赖 `GetCustomAttributes(typeof(TemplateTrackCoderSettings))`)。`StandardScene.dll` 内 Fields 由 `CoderFieldsMetadata.Describe` 在 Core 程序集内反射导出。
---
@@ -467,7 +467,7 @@ Token 解析优先级(SimpleLite 侧):
咪咕 Vue 前端封装位于 `Migu2.0/frontends/apps/simple-platform-vue/src/api/`
| 文件 | 对应 SimpleLite 前缀 |
| 文件 | 对应 Simple3 前缀 |
|------|----------------------|
| `reflection.ts` | `/api/sl/projection/reflection` |
| `mapEdit.ts` | `/api/sl/projection/map-edit` |
@@ -480,16 +480,16 @@ Token 解析优先级(SimpleLite 侧):
| 服务 | 默认端口 | 路径 |
|------|----------|------|
| MiGu.Server | 8080 | `/api/sl/*` → SimpleLite |
| SimpleLite 投影 API | 8222 | `/projection/*` |
| SimpleLite webVRender | 8223 | `/vr/*`(3D 嵌入 UI,非数据 API |
| MiGu.Server | 8080 | `/api/sl/*` → Simple3 |
| Simple3 投影 API | 8222 | `/projection/*` |
| Simple3 webVRender | 8223 | `/vr/*`(3D 嵌入 UI,非数据 API |
**健康检查**`GET http://localhost:8080/api/health/simplelite`
**健康检查**`GET http://localhost:8080/api/health/simple3`
**本地联调**
```powershell
# 1. 启动 SimpleLite(或登录 MiGu.Server 后自动拉起)
# 1. 启动 Simple3(或登录 MiGu.Server 后自动拉起)
# 2. 启动 MiGu.Server
# 3. 浏览器打开 http://localhost:8080/swagger
# 4. 授权 Bearer token 后测试 /api/sl/projection/cars
+66 -66
View File
@@ -6,10 +6,10 @@
`MIGU2.0` 是“迷毂 · 智能调度平台”的平台仓库,主要包含两部分:
- 平台后端:`MiGu.Server`ASP.NET Core 8 WebAPI,监听默认 `8080`,负责登录、权限、配置中心、静态前端托管、YARP 反向代理和 SimpleLite 子进程拉起。
- 平台后端:`MiGu.Server`ASP.NET Core 8 WebAPI,监听默认 `8080`,负责登录、权限、配置中心、静态前端托管、YARP 反向代理和 Simple3 子进程拉起。
- 平台前端:`frontends/apps/simple-platform-vue`Vue 3 + Vite + Pinia + Element Plus 单页应用,单工程承载管理员端 `/admin/*` 与运营端 `/monitor/*`
调度内核 `SimpleLite` 不在本仓库内,按 README 描述位于相邻仓库 `../Simple/SimpleLite`。当前仓库通过进程拉起、HTTP 代理、webVRender iframe 和本地文件读取等方式与 `SimpleLite` 协作。
调度内核 `Simple3` 不在本仓库内,按 README 描述位于相邻仓库 `../Simple/Simple3`。当前仓库通过进程拉起、HTTP 代理、webVRender iframe 和本地文件读取等方式与 `Simple3` 协作。
## 2. 技术栈总览
@@ -21,7 +21,7 @@
- 授权:ASP.NET Core Authorization Policy + 自研 RBAC。
- 代理:YARP Reverse Proxy。
- 持久化:当前为 JSON 文件持久化,主要落在 `MiGu.Server/data/`;长期蓝图中计划接入 EF Core 多数据库 Provider。
- 进程编排:`SimpleLiteLauncher` 拉起或复用 `SimpleLite.exe`
- 进程编排:`Simple3Launcher` 拉起或复用 `Simple3.exe`
### 前端
@@ -34,9 +34,9 @@
### 外部/相邻系统
- `SimpleLite.exe`:调度内核、地图/任务/车辆领域能力、Projection API、webVRender。
- `Simple3.exe`:调度内核、地图/任务/车辆领域能力、Projection API、webVRender。
- webVRender:默认 `8223`,由前端 `Workspace3D.vue` 以 iframe 方式嵌入。
- SimpleLite Projection/Web API:默认 `8222`,通过 `/api/sl/*` 由 YARP 转发。
- Simple3 Projection/Web API:默认 `8222`,通过 `/api/sl/*` 由 YARP 转发。
## 3. 顶层目录职责
@@ -64,33 +64,33 @@ flowchart LR
Server --> Auth["Auth/RBAC/JWT"]
Server --> Config["ConfigStore<br/>data/config-*.json"]
Server --> Wizard["部署向导<br/>deployment profile"]
Server --> Launcher["SimpleLiteLauncher"]
Launcher -->|登录后按 launchMode 拉起/复用| SL["SimpleLite.exe<br/>相邻仓库"]
Server -->|YARP /api/sl/*| SLApi["SimpleLite :8222"]
Server --> Launcher["Simple3Launcher"]
Launcher -->|登录后按 launchMode 拉起/复用| SL["Simple3.exe<br/>相邻仓库"]
Server -->|YARP /api/sl/*| SLApi["Simple3 :8222"]
Server -->|YARP /vr/*| VR["webVRender :8223"]
Vue -->|iframe 或 /vr 代理| VR
```
关键说明:
- `MiGu.Server` 负责托管 SPA、处理登录与配置、代理 `/api/sl/*` 到 SimpleLite
- 用户登录时可选择 `DesktopAndWeb``WebOnly`,后端转换为 SimpleLite 命令行 `--display-mode=web+local``--display-mode=web`
- `SimpleLiteLauncher` 默认 `FollowParent=false`,即 `MiGu.Server` 退出不会杀掉已启动的 SimpleLite
- 如果 `8222` 已经有 SimpleLite 在运行,后端会做 TCP + HTTP 探测并复用既有实例,避免重复拉起。
- `MiGu.Server` 负责托管 SPA、处理登录与配置、代理 `/api/sl/*` 到 Simple3
- 用户登录时可选择 `DesktopAndWeb``WebOnly`,后端转换为 Simple3 命令行 `--display-mode=web+local``--display-mode=web`
- `Simple3Launcher` 默认 `FollowParent=false`,即 `MiGu.Server` 退出不会杀掉已启动的 Simple3
- 如果 `8222` 已经有 Simple3 在运行,后端会做 TCP + HTTP 探测并复用既有实例,避免重复拉起。
- 前端的 3D/地图画布主要通过 `Workspace3D.vue` 加载 `http://localhost:8223` 或经 `/vr` 同源代理。
## 5. 文档蓝图与当前代码的差异
仓库中存在两类叙述:
- `README.md``MiGu.Server/README.md` 已经描述“用户先启动 `MiGu.Server`,登录后拉起 `SimpleLite`”这一当前实现。
- `ARCHITECTURE.md` 仍保留较多早期蓝图,例如“SimpleLite 通过 SystemMission 拉起 MiGu.Server”“双 SPA 独立托管”“SimpleShared.* 多工程”“EF Core 多 Provider”等。
- `README.md``MiGu.Server/README.md` 已经描述“用户先启动 `MiGu.Server`,登录后拉起 `Simple3`”这一当前实现。
- `ARCHITECTURE.md` 仍保留较多早期蓝图,例如“Simple3 通过 SystemMission 拉起 MiGu.Server”“双 SPA 独立托管”“SimpleShared.* 多工程”“EF Core 多 Provider”等。
当前代码落地状态:
- 已落地:`MiGu.Server` 主入口、登录拉起 SimpleLite、YARP 代理、JWT/Cookie 鉴权、RBAC、配置中心 JSON 持久化、部署向导、日志管理、地图 JSON 预览、单 Vue SPA 管理/运营双域。
- 部分落地:配置中心模型、运维白名单、SimpleLite 反射下发、webVRender 嵌入、地图编辑/监控前端能力。
- 尚未在本仓库落地:`SimpleShared.*` 工程、EF Core 多数据库持久层、SystemMission 由 SimpleLite 反向守护平台、独立 `platform-vue`/`rcsmonitor-vue` 双工程、`packages/sl-controls` 共享组件库。
- 已落地:`MiGu.Server` 主入口、登录拉起 Simple3、YARP 代理、JWT/Cookie 鉴权、RBAC、配置中心 JSON 持久化、部署向导、日志管理、地图 JSON 预览、单 Vue SPA 管理/运营双域。
- 部分落地:配置中心模型、运维白名单、Simple3 反射下发、webVRender 嵌入、地图编辑/监控前端能力。
- 尚未在本仓库落地:`SimpleShared.*` 工程、EF Core 多数据库持久层、SystemMission 由 Simple3 反向守护平台、独立 `platform-vue`/`rcsmonitor-vue` 双工程、`packages/sl-controls` 共享组件库。
## 6. 后端模块
@@ -101,14 +101,14 @@ flowchart LR
- 修正 `ContentRootPath`:直接运行 `bin/Debug/net8.0/MiGu.Server.exe` 时向上寻找 `MiGu.Server.csproj`,确保配置、data、wwwroot 路径一致。
- 默认监听:未配置 `urls` 时使用 `http://0.0.0.0:8080`
- WebRoot 自动探测:优先使用 `frontends/apps/simple-platform-vue/dist/index.html`,其次 `MiGu.Server/wwwroot/index.html`
- 注册 Controller、Swagger、CORS、JWT Bearer、Authorization Policy、YARP、ConfigStore、OpsAuditStore、HttpClient、SimpleLiteLauncher。
- 注册 Controller、Swagger、CORS、JWT Bearer、Authorization Policy、YARP、ConfigStore、OpsAuditStore、HttpClient、Simple3Launcher。
- 设置 Forwarded Headers,兼容反向代理后的 HTTPS Cookie。
- 托管静态资源和 SPA fallback。
核心关系:
- 调用 `JwtIssuer` 颁发/验签 JWT。
- 调用 `InternalTokenStore` 管理转发给 SimpleLite`X-Platform-Internal-Token`
- 调用 `InternalTokenStore` 管理转发给 Simple3`X-Platform-Internal-Token`
- YARP `sl-route` 会自动注入内部 token。
- `app.MapControllers()` 处理平台自身 API`app.MapReverseProxy()` 处理 `/api/sl/*``/vr/*`
@@ -116,7 +116,7 @@ flowchart LR
职责:
- `POST /api/auth/login`:校验用户名密码与 scope,按登录请求 `launchMode` 拉起 SimpleLite,签发 JWT 和 Cookie。
- `POST /api/auth/login`:校验用户名密码与 scope,按登录请求 `launchMode` 拉起 Simple3,签发 JWT 和 Cookie。
- `POST /api/auth/logout`:清理 Cookie。
- `GET /api/auth/me`:让前端对本地 token 进行服务端实校。
- `POST /api/auth/switch-scope`:同一用户在 `Platform``RCSMonitor` 之间切换 scope,并由服务端重新计算权限。
@@ -125,7 +125,7 @@ flowchart LR
- `RbacStore`:用户密码校验、scope 判断、有效权限计算。
- `JwtIssuer`:签发 JWT。
- `SimpleLiteLauncher`:登录后拉起或复用 SimpleLite
- `Simple3Launcher`:登录后拉起或复用 Simple3
- `ConfigStore`:读取部署画像,决定是否需要进入配置向导。
### 6.3 RBAC 权限模块:`Auth/`
@@ -136,7 +136,7 @@ flowchart LR
- `RbacModels.cs`:用户、角色、DTO。
- `PageCatalog.cs`:页面权限目录。
- `JwtIssuer.cs`JWT 颁发和验签参数。
- `InternalTokenStore.cs`:平台与 SimpleLite 内部通信 token。
- `InternalTokenStore.cs`:平台与 Simple3 内部通信 token。
职责:
@@ -182,32 +182,32 @@ flowchart LR
- 前端 `useConfigStore` 统一读写这些 section。
- 许多配置页只是不同 section 的编辑视图。
- `deployment` section 还会参与菜单裁剪与 SimpleLite 插件选择。
- `deployment` section 还会参与菜单裁剪与 Simple3 插件选择。
注意:
- 控制器类加了 `[Authorize]`,但当前 `PUT` 也是普通 `[Authorize]`,注释中曾提到 PlatformScope,实际代码没有强制仅平台 scope 可写。
- 当前 JSON 文件属于占位持久层;长期架构文档计划迁移到 EF Core。
### 6.5 SimpleLite 拉起与诊断:`Launcher/`
### 6.5 Simple3 拉起与诊断:`Launcher/`
主要文件:
- `SimpleLiteLauncher.cs`:拉起、复用、重启、诊断、插件选择联动。
- `SimpleLiteOptions.cs`:绑定 `appsettings.json:SimpleLite`
- `SimpleLiteBuildSync.cs`:辅助同步/探测 SimpleLite 构建与 API。
- `Simple3Launcher.cs`:拉起、复用、重启、诊断、插件选择联动。
- `Simple3Options.cs`:绑定 `appsettings.json:Simple3`
- `Simple3BuildSync.cs`:辅助同步/探测 Simple3 构建与 API。
职责:
- 将业务启动模式翻译为 SimpleLite 内部 display mode
- 将业务启动模式翻译为 Simple3 内部 display mode
- `WebOnly` -> `--display-mode=web`
- `DesktopAndWeb` -> `--display-mode=web+local`
- 拉起前检查 `8222` 是否已有 SimpleLite,并用 `/projection/cars` 确认不是其他进程占用端口。
- 查找 `SimpleLite.exe`:优先配置路径,再尝试相邻仓库、发布包同目录等候选路径。
- 默认 Windows 下通过 `cmd /c start` 脱离父进程,避免关闭平台后端时带走 SimpleLite
- 拉起前检查 `8222` 是否已有 Simple3,并用 `/projection/cars` 确认不是其他进程占用端口。
- 查找 `Simple3.exe`:优先配置路径,再尝试相邻仓库、发布包同目录等候选路径。
- 默认 Windows 下通过 `cmd /c start` 脱离父进程,避免关闭平台后端时带走 Simple3
- `FollowParent=true` 时使用 Windows JobObject 绑定父子进程。
- 写入 `plugins/active-scenes.json`,把部署向导选出的导航场景传给 SimpleLite
- `GET /api/health/simplelite` 返回配置、路径解析、端口、版本 API 可用性等诊断信息。
- 写入 `plugins/active-scenes.json`,把部署向导选出的导航场景传给 Simple3
- `GET /api/health/simple3` 返回配置、路径解析、端口、版本 API 可用性等诊断信息。
### 6.6 运维白名单与审计:`OpsController` + `OpsAuditStore`
@@ -217,7 +217,7 @@ flowchart LR
- 二次校验 JWT 的 `ops` claim。
- 支持 `IdempotencyKey`,避免重复点击/重试重复下发。
- `monitor.note.write` 仅写审计。
- 其他运维动作需要在 `appsettings.json:Ops:Dispatch` 显式配置 `opCode -> "kind:Method"` 后,才会转发到 SimpleLite 反射 API。
- 其他运维动作需要在 `appsettings.json:Ops:Dispatch` 显式配置 `opCode -> "kind:Method"` 后,才会转发到 Simple3 反射 API。
- 审计记录由 `OpsAuditStore` 落盘,重启后不丢。
默认白名单:
@@ -229,9 +229,9 @@ flowchart LR
重要设计:
- 未配置映射时不会“假成功”,而是返回 `ok=false` 并说明“已记录审计但未下发”。
- 真实下发路径为 SimpleLite 反射执行接口:`/projection/reflection/execute/{kind}/{id}/{method}`
- 真实下发路径为 Simple3 反射执行接口:`/projection/reflection/execute/{kind}/{id}/{method}`
### 6.7 投影与 SimpleLite 代理
### 6.7 投影与 Simple3 代理
模块组成:
@@ -249,8 +249,8 @@ flowchart LR
职责:
- `GET /api/health`:平台后端健康检查,返回启动时间、运行时长、端口规划。
- `GET /api/health/simplelite`SimpleLite 路径和端口诊断。
- `POST /api/health/simplelite/restart-for-update`:授权用户可触发关闭 SimpleLite、同步最新 DLL 并重新拉起。
- `GET /api/health/simple3`Simple3 路径和端口诊断。
- `POST /api/health/simple3/restart-for-update`:授权用户可触发关闭 Simple3、同步最新 DLL 并重新拉起。
### 6.9 部署配置向导:`WizardController` + `DeploymentCatalog`
@@ -258,7 +258,7 @@ flowchart LR
- 首次部署时收集导航方式、功能模块、业务场景。
- 保存后写入 `deployment` 配置 section。
- 将导航方式映射为 SimpleLite 场景插件 ID,并调用 `SimpleLiteLauncher.WriteActiveScenes()` 写入 `plugins/active-scenes.json`
- 将导航方式映射为 Simple3 场景插件 ID,并调用 `Simple3Launcher.WriteActiveScenes()` 写入 `plugins/active-scenes.json`
- 按部署画像裁剪页面,例如当前 `wms` 模块会点亮 `admin-config-location`
对外 API
@@ -273,7 +273,7 @@ flowchart LR
职责:
- 读取 SimpleLite 工作目录下的 `log/` 文件。
- 读取 Simple3 工作目录下的 `log/` 文件。
- 支持概览、文件列表、目录浏览、日志条目分页、合订本、日志分析、原文查看、下载。
- 日志行按 `Diagnosis.Log` 的格式解析,支持标签聚合和数值字段识别。
@@ -290,14 +290,14 @@ flowchart LR
职责:
- 先调用 SimpleLite `/projection/map-edit/maps` 获取地图目录。
- 先调用 Simple3 `/projection/map-edit/maps` 获取地图目录。
- 再在本机读取对应地图 JSON 原文,供平台地图管理右侧预览。
- 对地图名做非法字符检查,并校验最终路径必须落在地图目录下。
关系:
- 依赖 SimpleLite 与 MiGu.Server 同机部署。
- 依赖 `SimpleLiteOptions.ProjectionPort``InternalTokenStore`
- 依赖 Simple3 与 MiGu.Server 同机部署。
- 依赖 `Simple3Options.ProjectionPort``InternalTokenStore`
## 7. 前端模块
@@ -328,7 +328,7 @@ flowchart LR
职责:
- 登录页采集用户名、密码、scope 和 SimpleLite 启动模式。
- 登录页采集用户名、密码、scope 和 Simple3 启动模式。
- 登录成功后保存 token、用户、scope、runMode、effectivePermissions 到 Pinia 与 localStorage。
- `auth.validate()` 通过 `/api/auth/me` 处理服务端重启、JWT secret 重生导致的旧 token 失效问题。
- `switchScope()` 不在客户端伪造权限,而是请求后端重新签发 token 和权限。
@@ -346,7 +346,7 @@ flowchart LR
- `api/config.ts`:配置中心读写。
- `api/projection.ts`:站点、路径、车辆、任务投影。
- `api/ops.ts`:运维白名单执行和审计。
- `api/reflection.ts`SimpleLite 反射对象、字段、方法执行。
- `api/reflection.ts`Simple3 反射对象、字段、方法执行。
- `api/mapEdit.ts`:地图编辑相关 API。
- `api/wizard.ts`:部署向导。
- `api/logs.ts`:日志管理。
@@ -354,7 +354,7 @@ flowchart LR
关系:
- 所有平台 API 都以 `/api` 为 baseURL。
- `/api/sl/*` 由 MiGu.Server 反代给 SimpleLite
- `/api/sl/*` 由 MiGu.Server 反代给 Simple3
- 开发环境可通过 `VITE_USE_MOCK=true` 使用前端 mock,但反射、工作台等部分 API 仍要求真实后端。
### 7.4 Shell、主题和权限控制组件
@@ -418,7 +418,7 @@ flowchart LR
- `useHistory`:撤销/重做命令栈。
- `useSelection`:对象选择状态。
- `useAlignment``useBatchGenerate``useClipboard`:编辑辅助能力。
- `mapEditApi``reflectionApi`:与 SimpleLite 的地图编辑和反射 API 通信。
- `mapEditApi``reflectionApi`:与 Simple3 的地图编辑和反射 API 通信。
#### 地图监控
@@ -431,7 +431,7 @@ flowchart LR
- `VehicleMonitorPanel`:车辆监控台。
- `MissionListPanel`:任务列表。
- `MonitorSelectionPanel`:选中对象详情和动作。
- `useProjectionStream`:订阅 SimpleLite/平台 SSE 事件。
- `useProjectionStream`:订阅 Simple3/平台 SSE 事件。
### 7.7 运营端页面:`/monitor/*`
@@ -469,13 +469,13 @@ flowchart LR
- 选择导航方式:磁导航、二维码导航、激光导航。
- 选择功能模块:当前包含 WMS、PTL。
- 选择业务场景模板。
- 预览将激活的 SimpleLite 场景插件,如 `scene.magnetic``scene.qrcode``scene.laser`
- 预览将激活的 Simple3 场景插件,如 `scene.magnetic``scene.qrcode``scene.laser`
保存后:
- 调用 `PUT /api/wizard/profile`
- 后端写入 `deployment` section。
- 后端写入 SimpleLite `plugins/active-scenes.json`
- 后端写入 Simple3 `plugins/active-scenes.json`
- 前端调用 `auth.markWizardDone()`,跳转到对应首页。
## 8. 数据与持久化
@@ -491,9 +491,9 @@ flowchart LR
这些文件采用 JSON 持久化,部分写入走 `AtomicFile`,损坏时会备份后回退默认值。
### SimpleLite 侧文件
### Simple3 侧文件
MiGu.Server 会直接或间接使用 SimpleLite 工作目录:
MiGu.Server 会直接或间接使用 Simple3 工作目录:
- `plugins/active-scenes.json`:部署向导写入,控制内核场景插件选择。
- `log/**/*.log`:日志管理模块读取并分析。
@@ -511,7 +511,7 @@ MiGu.Server 会直接或间接使用 SimpleLite 工作目录:
- `/api/maps/{name}/content`:地图 JSON 预览。
- `/api/health/*`:健康检查。
### 平台后端到 SimpleLite
### 平台后端到 Simple3
- `/api/sl/*` -> YARP -> `http://127.0.0.1:8222/*`
- `/vr/*` -> YARP -> `http://127.0.0.1:8223/*`
@@ -533,7 +533,7 @@ flowchart TB
RBAC["RbacStore / JwtIssuer"]
Config["ConfigStore"]
Wizard["WizardController"]
Launcher["SimpleLiteLauncher"]
Launcher["Simple3Launcher"]
Ops["OpsController"]
Logs["LogsController"]
Maps["MapsContentController"]
@@ -551,8 +551,8 @@ flowchart TB
WizardVue["WizardView.vue"]
end
subgraph SimpleLite["SimpleLite 相邻仓库"]
SLExe["SimpleLite.exe"]
subgraph Simple3["Simple3 相邻仓库"]
SLExe["Simple3.exe"]
Projection["Projection API :8222"]
VRender["webVRender :8223"]
LogsFile["log/**/*.log"]
@@ -591,14 +591,14 @@ flowchart TB
## 11. 典型业务链路
### 11.1 登录并启动 SimpleLite
### 11.1 登录并启动 Simple3
1. 用户打开 `/login`
2. 前端提交用户名、密码、scope、launchMode 到 `/api/auth/login`
3. `AuthController``RbacStore.VerifyCredentials()` 校验密码。
4. `AuthController` 检查用户是否可使用请求的 scope。
5. `AuthController``SimpleLiteLauncher.MaybeStart()`
6. `SimpleLiteLauncher` 复用或拉起 `SimpleLite.exe`
5. `AuthController``Simple3Launcher.MaybeStart()`
6. `Simple3Launcher` 复用或拉起 `Simple3.exe`
7. 后端计算有效权限,签发 JWT,写 httpOnly Cookie。
8. 前端保存登录态,并按 `needsWizard` 跳转 `/wizard` 或业务首页。
@@ -608,7 +608,7 @@ flowchart TB
2. 守卫确保 scope 为 `Platform`,并检查 `admin-map-editor` 页面权限。
3. `MapEditorView` 加载 `Workspace3D`,以 `canvasOnly=true` 打开 `8223` webVRender。
4. Vue 侧顶栏、工具栏、属性面板接管编辑 UI。
5. 创建、删除、批量生成、字段修改等操作通过 `mapEditApi``reflectionApi` 调用 SimpleLite
5. 创建、删除、批量生成、字段修改等操作通过 `mapEditApi``reflectionApi` 调用 Simple3
6. `useHistory` 记录可回滚命令,部分操作会用快照恢复。
### 11.3 运营端执行运维动作
@@ -618,7 +618,7 @@ flowchart TB
3. 用户确认后调用 `/api/sl/ops/execute`
4. `OpsController` 校验白名单、JWT 操作码、幂等键。
5. 若是备注,则只写审计;若是内核动作,读取 `Ops:Dispatch` 映射。
6. 已配置映射时调用 SimpleLite 反射 execute;未配置映射时返回未下发。
6. 已配置映射时调用 Simple3 反射 execute;未配置映射时返回未下发。
7. 审计落盘,前端显示执行结果。
### 11.4 首次部署配置向导
@@ -628,7 +628,7 @@ flowchart TB
3. 前端加载 `/api/wizard/options``/api/wizard/profile`
4. 用户选择导航方式、模块、场景。
5. 保存后端写 `deployment` section。
6. 后端将导航方式转换为场景插件 ID,并写 SimpleLite `plugins/active-scenes.json`
6. 后端将导航方式转换为场景插件 ID,并写 Simple3 `plugins/active-scenes.json`
7. 后续登录时,`AuthController.BuildSession()` 会按部署画像裁剪可见页面。
## 12. 构建与运行
@@ -645,7 +645,7 @@ dotnet run
- 平台:`http://localhost:8080/login`
- 健康检查:`http://localhost:8080/api/health`
- SimpleLite 诊断:`http://localhost:8080/api/health/simplelite`
- Simple3 诊断:`http://localhost:8080/api/health/simple3`
- Swagger:开发环境 `/swagger`
### 前端开发
@@ -679,7 +679,7 @@ pnpm dev
- 架构文档与当前实现存在历史差异,后续应更新 `ARCHITECTURE.md` 中关于启动归属、双 SPA 拆分、共享组件库和持久层状态的描述。
- `ConfigController.Put` 当前只要求登录,未按注释限制 `PlatformScope`,如果运营 scope 可调用配置写入,需要确认是否符合预期。
- 前端登录页仍有 Mock 文案,真实 API 模式下会误导用户。
- `ServiceStatusView.vue` 目前展示大量 Mock 状态,尚未接入 `/api/health``/api/health/simplelite` 的真实数据。
- `ServiceStatusView.vue` 目前展示大量 Mock 状态,尚未接入 `/api/health``/api/health/simple3` 的真实数据。
- `ProjectionController` 是本地 Mock,占位意义大于生产意义,真实链路依赖 `/api/sl/*`
- 运维白名单默认没有 `Ops:Dispatch` 映射,未配置前只会审计,不会真实下发内核动作。
- 多数据库、EF Core、SystemMission、独立前端包和共享组件库仍是蓝图,不应被外部交付文档表述为已完成。
@@ -687,8 +687,8 @@ pnpm dev
## 14. 快速索引
- 后端启动:`MiGu.Server/Program.cs`
- 登录和启动 SimpleLite`MiGu.Server/Controllers/AuthController.cs`
- SimpleLite 拉起器:`MiGu.Server/Launcher/SimpleLiteLauncher.cs`
- 登录和启动 Simple3`MiGu.Server/Controllers/AuthController.cs`
- Simple3 拉起器:`MiGu.Server/Launcher/Simple3Launcher.cs`
- 配置中心:`MiGu.Server/Configs/ConfigStore.cs`
- 权限中心:`MiGu.Server/Auth/RbacStore.cs`
- 权限 API`MiGu.Server/Controllers/RbacController.cs`
+1 -1
View File
@@ -2,7 +2,7 @@ namespace MiGu.DB.Domains.Fleet;
/// <summary>
/// CDM 搬运任务的平台侧快照(表 cdm_tasks)。
/// 以任务 Id 为主键;SimpleLite/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史。
/// 以任务 Id 为主键;Simple3/StandardScene 把终态任务从自身 JSON 里删除,这里则永久保留=完整历史。
/// </summary>
public sealed class CdmTaskRecord
{
+12 -12
View File
@@ -3,24 +3,24 @@
namespace MiGu.Server.Auth;
/// <summary>
/// MiGu.Server ↔ SimpleLite 8222 之间的 **内部共享 token**。
/// MiGu.Server ↔ Simple3 8222 之间的 **内部共享 token**。
///
/// 设计动机:SimpleLite 8222 的 EmbedIO WebApiReflectionApi / PersistenceApi / MapEditApi
/// 历史上完全无鉴权,远程访问 = 接管调度内核。为不在 SimpleLite 侧实现完整 JWT 验签
/// (减少 SimpleLite 复杂度),采用一个轻量约定:
/// 设计动机:Simple3 8222 的 EmbedIO WebApiReflectionApi / PersistenceApi / MapEditApi
/// 历史上完全无鉴权,远程访问 = 接管调度内核。为不在 Simple3 侧实现完整 JWT 验签
/// (减少 Simple3 复杂度),采用一个轻量约定:
///
/// - <b>本机回环</b>127.0.0.1 / ::1SimpleLite 直接放行(开发机直连不受影响);
/// - <b>本机回环</b>127.0.0.1 / ::1Simple3 直接放行(开发机直连不受影响);
/// - <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 都不同)。");
}
}
}
+3 -3
View File
@@ -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()
+46 -34
View File
@@ -12,19 +12,17 @@ namespace MiGu.Server.Controllers;
public class AuthController : ControllerBase
{
/// <summary>
/// 登录请求体。
/// 会话 N+1(启动反转):新增 <see cref="LaunchMode"/>。前端登录页让用户选 "WebOnly" / "DesktopAndWeb"
/// MiGu.Server 据此拉起 SimpleLite 子进程并透传 <c>--display-mode=web|web+local</c>。
/// 历史调用方不传该字段时默认 "DesktopAndWeb"(与之前 web+local 默认行为一致,向后兼容)。
/// 登录请求体。可选 <see cref="LaunchMode"/>Simple3 仅 Web 宿主,缺省/未知一律按 WebOnly。
/// MiGu.Server 据此拉起子进程并透传 <c>--display-mode=web</c>。
/// </summary>
public record LoginRequest(string Username, string Password, string Scope, string? LaunchMode = null);
public record LoginRequest(string Username, string Password, string? Scope = null, string? LaunchMode = null);
/// <summary>
/// 登录响应。
/// 会话 N+1 增量字段:
/// <list type="bullet">
/// <item><c>RunMode</c>:根据 SimpleLite 真实拉起结果回填(WebEnabled / WebOnly / Detached)。Detached 表示后端未能拉起 SimpleLite,前端可降级展示。</item>
/// <item><c>LaunchStatus</c><see cref="SimpleLiteLauncher.LaunchResult.Status"/> 枚举字符串,前端用于精细化提示。</item>
/// <item><c>RunMode</c>:根据 Simple3 真实拉起结果回填(WebEnabled / WebOnly / Detached)。Detached 表示后端未能拉起 Simple3,前端可降级展示。</item>
/// <item><c>LaunchStatus</c><see cref="Simple3Launcher.LaunchResult.Status"/> 枚举字符串,前端用于精细化提示。</item>
/// <item><c>LaunchWarning</c>:可空告警文本;非空时前端应该弹消息条告知用户。</item>
/// </list>
/// </summary>
@@ -51,17 +49,18 @@ public class AuthController : ControllerBase
string Scope,
string RunMode,
EffectivePermissions EffectivePermissions,
bool NeedsWizard = false);
bool NeedsWizard = false,
string? Token = null);
private const string CookieName = "simple.auth.token";
private readonly RbacStore _rbac;
private readonly JwtIssuer _jwt;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
private readonly ConfigStore _config;
private readonly ILogger<AuthController> _log;
public AuthController(RbacStore rbac, JwtIssuer jwt, SimpleLiteLauncher launcher, ConfigStore config, ILogger<AuthController> log)
public AuthController(RbacStore rbac, JwtIssuer jwt, Simple3Launcher launcher, ConfigStore config, ILogger<AuthController> log)
{
_rbac = rbac;
_jwt = jwt;
@@ -79,41 +78,40 @@ public class AuthController : ControllerBase
{
if (string.IsNullOrWhiteSpace(req.Username))
return BadRequest(new { message = "用户名不能为空" });
if (req.Scope is not ("Platform" or "RCSMonitor"))
return BadRequest(new { message = "无效 scope" });
// 真密码校验:RbacStore.VerifyCredentials 对不存在 / 已禁用 / 密码错统一返回 null,防用户名枚举。
var user = _rbac.VerifyCredentials(req.Username, req.Password);
if (user == null)
return Unauthorized(new { message = "用户名或密码错误,或账号已被停用" });
// scope 必须落在该账号角色覆盖的 scope」集合内(admin 角色 scope=* 覆盖全部)
if (!_rbac.CanUseScope(user, req.Scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {req.Scope} 的权限" });
// 入口由账号角色决定,不再接受登录页挑选「管理端 / 运营端」
var scope = _rbac.ResolveLoginScope(user);
if (scope == null)
return StatusCode(403, new { message = $"账号 {user.Username} 没有任何可登录区域,请联系管理员分配角色" });
// 会话 N+1:按 LaunchMode 拉起 SimpleLite 子进程(线程池执行,避免占用请求线程)。
// 会话 N+1:按 LaunchMode 拉起 Simple3 子进程(线程池执行,避免占用请求线程)。
var launchMode = NormalizeLaunchMode(req.LaunchMode);
SimpleLiteLauncher.LaunchResult? launchResult = null;
Simple3Launcher.LaunchResult? launchResult = null;
try
{
// M1waitForReady=false —— 拉起 SimpleLite 后立即返回,不在登录请求里同步等端口
// M1waitForReady=false —— 拉起 Simple3 后立即返回,不在登录请求里同步等端口
// 就绪(冷启动可能十几秒)。前端拿 LaunchStatus=Starting 即可,必要时轮询健康检查。
launchResult = await Task.Run(() => _launcher.MaybeStart(launchMode, waitForReady: false));
_log.LogInformation("SimpleLite launch result for user={User} launchMode={Mode}: Started={Started} Status={Status} Detail={Detail}",
_log.LogInformation("Simple3 launch result for user={User} launchMode={Mode}: Started={Started} Status={Status} Detail={Detail}",
user.Username, launchMode, launchResult.Value.Started, launchResult.Value.Status, launchResult.Value.Detail);
}
catch (Exception ex)
{
_log.LogError(ex, "SimpleLite launch threw for user={User} launchMode={Mode}", user.Username, launchMode);
_log.LogError(ex, "Simple3 launch threw for user={User} launchMode={Mode}", user.Username, launchMode);
}
var runMode = ResolveRunMode(launchResult, launchMode);
var (perm, roleNames, token) = BuildSession(user, req.Scope);
var (perm, roleNames, token) = BuildSession(user, scope);
SetAuthCookie(token);
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new LoginResponse(token, dto, req.Scope, runMode, perm,
return Ok(new LoginResponse(token, dto, scope, runMode, perm,
LaunchStatus: launchResult?.Status,
LaunchWarning: launchResult?.Warning,
NeedsWizard: NeedsWizard()));
@@ -144,13 +142,20 @@ public class AuthController : ControllerBase
if (user == null || !user.Enabled)
return Unauthorized(new { message = "账号已失效或被停用" });
// 账号当前是否还允许这个 scope(管理员可能在此期间调整了角色)。
if (!_rbac.CanUseScope(user, scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {scope} 的权限" });
var preferred = _rbac.ResolveLoginScope(user);
if (preferred == null)
return StatusCode(403, new { message = $"账号 {user.Username} 没有任何可登录区域,请联系管理员分配角色" });
var (perm, roleNames, token) = BuildSession(user, preferred);
string? rotated = null;
if (!string.Equals(preferred, scope, StringComparison.OrdinalIgnoreCase))
{
SetAuthCookie(token);
rotated = token;
}
var (perm, roleNames, _) = BuildSession(user, scope);
var dto = new AuthUserDto(user.Id, user.Username, user.DisplayName, roleNames);
return Ok(new MeResponse(dto, scope, InferRunMode(), perm, NeedsWizard()));
return Ok(new MeResponse(dto, preferred, InferRunMode(), perm, NeedsWizard(), rotated));
}
/// <summary>用同一身份切换 scope 并重发 token + perms。</summary>
@@ -172,6 +177,11 @@ public class AuthController : ControllerBase
if (!_rbac.CanUseScope(user, req.Scope))
return StatusCode(403, new { message = $"账号 {user.Username} 没有访问 {req.Scope} 的权限" });
// 有管理权限的账号固定走管理入口,不再切到运营壳;请用运营权限账号登录查看运营页。
var preferred = _rbac.ResolveLoginScope(user);
if (preferred == PageCatalog.ScopePlatform && req.Scope == PageCatalog.ScopeMonitor)
return StatusCode(403, new { message = "请使用运营权限账号查看运营页面" });
var (perm, roleNames, token) = BuildSession(user, req.Scope);
SetAuthCookie(token);
@@ -200,22 +210,23 @@ public class AuthController : ControllerBase
return (perm, roleNames, token);
}
/// <summary>me / switchScope 不重启 SimpleLite,依据 Launcher 记录的 LastLaunchMode 反推 RunMode。</summary>
/// <summary>me / switchScope 不重启 Simple3,依据 Launcher 记录的 LastLaunchMode 反推 RunMode。</summary>
private string InferRunMode()
{
var last = _launcher.LastLaunchMode;
if (string.IsNullOrEmpty(last)) return "Detached";
if (last == SimpleLiteLauncher.ExternalReuseLaunchMode) return "WebEnabled";
if (last == Simple3Launcher.ExternalReuseLaunchMode) return "WebOnly";
return last.Contains("local", StringComparison.OrdinalIgnoreCase) ? "WebEnabled" : "WebOnly";
}
/// <summary>根据 Launcher 真实结果决定 RunMode(避免 SimpleLite 没起却假装 WebEnabled)。</summary>
private static string ResolveRunMode(SimpleLiteLauncher.LaunchResult? result, string launchMode)
/// <summary>根据 Launcher 真实结果决定 RunMode(避免内核没起却假装已连接)。</summary>
private static string ResolveRunMode(Simple3Launcher.LaunchResult? result, string launchMode)
{
if (result is not { Started: true })
return "Detached";
// Simple3 无本地端:复用既有实例也按 WebOnly 展示。
if (result.Value.Status == "ReusingExisting")
return "WebEnabled";
return "WebOnly";
if (!string.IsNullOrEmpty(result.Value.DisplayMode))
{
if (result.Value.DisplayMode.Equals("web", StringComparison.OrdinalIgnoreCase))
@@ -230,8 +241,9 @@ public class AuthController : ControllerBase
{
return raw?.Trim().ToLowerInvariant() switch
{
"webonly" or "web-only" or "web" => "WebOnly",
_ => "DesktopAndWeb",
"desktopandweb" or "web+local" or "weblocal" or "local" => "DesktopAndWeb",
// Simple3 默认仅 Web;未传 / 未知也走 WebOnly
_ => "WebOnly",
};
}
+2 -2
View File
@@ -31,7 +31,7 @@ public sealed class FleetController : ControllerBase
/// <summary>
/// CDM 搬运任务列表(读平台快照库 cdm_tasks)。在线时先即时同步一次拿最新,
/// SimpleLite 关闭时回退最近快照,并通过 online/lastSyncAt 告知前端数据是否滞后。
/// Simple3 关闭时回退最近快照,并通过 online/lastSyncAt 告知前端数据是否滞后。
/// </summary>
[HttpGet("tasks")]
public async Task<ActionResult<object>> Tasks([FromQuery] int limit = 1000, CancellationToken ct = default)
@@ -76,7 +76,7 @@ public sealed class FleetController : ControllerBase
}
/// <summary>
/// 车辆报警列表(读平台记录 vehicle_alarms)。含活跃 + 历史;SimpleLite 离线时回退最近记录,
/// 车辆报警列表(读平台记录 vehicle_alarms)。含活跃 + 历史;Simple3 离线时回退最近记录,
/// 通过 online/lastSyncAt 告知数据是否滞后。activeOnly=true 仅返回未恢复的报警。
/// </summary>
[HttpGet("alarms")]
+16 -16
View File
@@ -9,9 +9,9 @@ namespace MiGu.Server.Controllers;
public class HealthController : ControllerBase
{
private static readonly DateTimeOffset StartTime = DateTimeOffset.UtcNow;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
public HealthController(SimpleLiteLauncher launcher) => _launcher = launcher;
public HealthController(Simple3Launcher launcher) => _launcher = launcher;
/// <summary>匿名存活探针:仅返回进程级状态,不暴露端口拓扑等部署细节。</summary>
[HttpGet]
@@ -26,28 +26,28 @@ public class HealthController : ControllerBase
}
/// <summary>
/// SimpleLite 拉起配置诊断:查看当前 ExecutablePath、解析结果、端口是否已有服务。
/// 配置位置:<c>MiGu.Server/appsettings.json</c> → <c>SimpleLite</c> 节点。
/// Simple3 拉起配置诊断:查看当前 ExecutablePath、解析结果、端口是否已有服务。
/// 配置位置:<c>MiGu.Server/appsettings.json</c> → <c>Simple3</c> 节点。
/// 含服务器本地路径等敏感信息,要求登录。
/// </summary>
[HttpGet("simplelite")]
[HttpGet("simple3")]
[Authorize]
public IActionResult GetSimpleLiteDiagnostics() => Ok(_launcher.GetDiagnostics());
public IActionResult GetSimple3Diagnostics() => Ok(_launcher.GetDiagnostics());
/// <summary>关闭本机全部 SimpleLite 进程。仅 Platform 管理端可调。</summary>
[HttpPost("simplelite/stop")]
/// <summary>关闭本机全部 Simple3 进程。仅 Platform 管理端可调。</summary>
[HttpPost("simple3/stop")]
[Authorize(Policy = "PlatformScope")]
public IActionResult StopSimpleLite()
public IActionResult StopSimple3()
{
var killed = _launcher.StopAll();
var diag = _launcher.GetDiagnostics();
return Ok(new { killed, diagnostics = diag });
}
/// <summary>关闭并重新拉起 SimpleLite(不同步 DLL)。仅 Platform 管理端可调。</summary>
[HttpPost("simplelite/restart")]
/// <summary>关闭并重新拉起 Simple3(不同步 DLL)。仅 Platform 管理端可调。</summary>
[HttpPost("simple3/restart")]
[Authorize(Policy = "PlatformScope")]
public IActionResult RestartSimpleLite([FromQuery] string launchMode = "webonly")
public IActionResult RestartSimple3([FromQuery] string launchMode = "webonly")
{
var result = _launcher.Restart(launchMode);
var diag = _launcher.GetDiagnostics();
@@ -55,12 +55,12 @@ public class HealthController : ControllerBase
}
/// <summary>
/// 关闭 SimpleLite、同步最新 DLL、重新拉起。用于「前往站点」API 缺失时一键更新。
/// 会终止本机全部 SimpleLite 进程并重启,仅 Platform 管理端可调。
/// 关闭 Simple3、同步最新 DLL、重新拉起。用于「前往站点」API 缺失时一键更新。
/// 会终止本机全部 Simple3 进程并重启,仅 Platform 管理端可调。
/// </summary>
[HttpPost("simplelite/restart-for-update")]
[HttpPost("simple3/restart-for-update")]
[Authorize(Policy = "PlatformScope")]
public IActionResult RestartSimpleLiteForUpdate([FromQuery] string launchMode = "webonly")
public IActionResult RestartSimple3ForUpdate([FromQuery] string launchMode = "webonly")
{
var result = _launcher.RestartForUpdate(launchMode);
var diag = _launcher.GetDiagnostics();
@@ -8,8 +8,8 @@ using MiGu.Server.Launcher;
namespace MiGu.Server.Controllers;
/// <summary>
/// 读取 SimpleLite 地图固定目录下的 JSON 原文,供平台「地图管理」右侧预览。
/// 先向 SimpleLite 拉取 maps 列表拿到 directory,再读本机同路径文件(与 SimpleLite 同机部署)。
/// 读取 Simple3 地图固定目录下的 JSON 原文,供平台「地图管理」右侧预览。
/// 先向 Simple3 拉取 maps 列表拿到 directory,再读本机同路径文件(与 Simple3 同机部署)。
/// </summary>
[ApiController]
[Authorize]
@@ -18,13 +18,13 @@ public class MapsContentController : ControllerBase
{
private readonly IHttpClientFactory _httpFactory;
private readonly InternalTokenStore _internalToken;
private readonly SimpleLiteOptions _sl;
private readonly Simple3Options _sl;
private readonly ILogger<MapsContentController> _log;
public MapsContentController(
IHttpClientFactory httpFactory,
InternalTokenStore internalToken,
IOptions<SimpleLiteOptions> sl,
IOptions<Simple3Options> sl,
ILogger<MapsContentController> log)
{
_httpFactory = httpFactory;
@@ -47,7 +47,7 @@ public class MapsContentController : ControllerBase
{
var directory = await FetchMapsDirectoryAsync(ct);
if (string.IsNullOrWhiteSpace(directory))
return StatusCode(503, new { message = "无法从 SimpleLite 获取地图目录" });
return StatusCode(503, new { message = "无法从 Simple3 获取地图目录" });
var fileName = name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.json";
var fullPath = Path.GetFullPath(Path.Combine(directory, fileName));
@@ -86,7 +86,7 @@ public class MapsContentController : ControllerBase
using var resp = await client.SendAsync(msg, ct);
var body = await resp.Content.ReadAsStringAsync(ct);
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"SimpleLite maps 列表返回 {(int)resp.StatusCode}");
throw new InvalidOperationException($"Simple3 maps 列表返回 {(int)resp.StatusCode}");
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
@@ -4,8 +4,8 @@ using Microsoft.AspNetCore.Mvc;
namespace MiGu.Server.Controllers;
/// <summary>
/// 投影 API 占位:真实落地时由 YARP 反代到 SimpleLite WebAPI 的 /api/projection/* 路径。
/// 本地 Mock 数据仅用于无 SimpleLite 运行时的开发联调。
/// 投影 API 占位:真实落地时由 YARP 反代到 Simple3 WebAPI 的 /api/projection/* 路径。
/// 本地 Mock 数据仅用于无 Simple3 运行时的开发联调。
///
/// AR-4: 全 class 加 [Authorize] —— 任何登录用户都能读 mock 投影数据;未登录直接 401。
/// </summary>
@@ -9,7 +9,7 @@ namespace MiGu.Server.Controllers;
/// <summary>
/// 迷毂「数据中心」:把 scene.signal 的 Model JSON 以表格读写(PLC 握手 / 磁条交管)。
/// 文件落在 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,不依赖 SimpleLite 进程是否在跑。
/// 文件落在 Simple3 工作目录 <c>Config/Signal/*.json</c>,不依赖 Simple3 进程是否在跑。
/// </summary>
[ApiController]
[Authorize(Policy = "PlatformScope")]
@@ -10,7 +10,7 @@ namespace MiGu.Server.Controllers;
/// 数据持久化于 <c>platform.db</c> 的 <c>simple_fields</c> 表。
///
/// 对应前端「字段管理」页(<c>/admin/simple-fields</c>,页面 key <c>admin-simple-fields</c>)。
/// 前端主流程为「刷新」读库、「默认」从 SimpleLite 拉取模板、「保存」走 <see cref="SaveBatch"/> 全量替换。
/// 前端主流程为「刷新」读库、「默认」从 Simple3 拉取模板、「保存」走 <see cref="SaveBatch"/> 全量替换。
/// </summary>
[ApiController]
[Authorize]
+5 -5
View File
@@ -17,7 +17,7 @@ namespace MiGu.Server.Controllers;
/// </list>
/// 说明:保存时即把选型固化为单一事实来源 <c>deployment</c> section,并同步联动 Launcher ——
/// <see cref="SaveProfile"/> 调 <c>WriteActiveScenes</c> 写 <c>plugins/active-scenes.json</c> / 透传 <c>--scenes</c>
/// 驱动 SimpleLite 下次启动选择性加载选定的导航场景插件。
/// 驱动 Simple3 下次启动选择性加载选定的导航场景插件。
/// </summary>
[ApiController]
[Authorize]
@@ -25,10 +25,10 @@ namespace MiGu.Server.Controllers;
public class WizardController : ControllerBase
{
private readonly ConfigStore _store;
private readonly SimpleLiteLauncher _launcher;
private readonly Simple3Launcher _launcher;
private readonly ILogger<WizardController> _log;
public WizardController(ConfigStore store, SimpleLiteLauncher launcher, ILogger<WizardController> log)
public WizardController(ConfigStore store, Simple3Launcher launcher, ILogger<WizardController> log)
{
_store = store;
_launcher = launcher;
@@ -81,7 +81,7 @@ public class WizardController : ControllerBase
_store.PutDeployment(profile);
// 平台 → 内核联动:把导航选型写入 SimpleLite 的 plugins/active-scenes.json(下次启动选择性加载;
// 平台 → 内核联动:把导航选型写入 Simple3 的 plugins/active-scenes.json(下次启动选择性加载;
// 已运行实例可由前端再调 POST /api/sl/projection/scenes/apply 触发增量 reload)。
// scene.device(门/充电桩/按钮盒驱动)为各类项目通用能力,向导暂无独立选项,固定并入激活集合;
// 选 SPS / Pack 时 ToLauncherSceneIds 会并入 scene.signalPLC 握手)。
@@ -107,7 +107,7 @@ public class WizardController : ControllerBase
/// 统一对外投影:camelCase 字段 + 推导出的 activeSceneIds(导航选型 → 内核场景 id)+
/// hiddenPages(被裁剪的菜单页)+ 可选的 activeScenesWrite(保存时写 active-scenes.json 的结果)。
/// </summary>
private static object Project(DeploymentProfile dp, SimpleLiteLauncher.ActiveScenesWriteResult? write = null) => new
private static object Project(DeploymentProfile dp, Simple3Launcher.ActiveScenesWriteResult? write = null) => new
{
configured = dp.Configured,
platformType = dp.PlatformType,
+4 -4
View File
@@ -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)
{
+4 -4
View File
@@ -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 -1
View File
@@ -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; }
+9 -9
View File
@@ -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();
}
}
@@ -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;
}
@@ -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>外部已存在的 SimpleLiteMiGu.Server 重启复用上一轮实例)占位 LaunchMode 值,不参与命令行 displayMode 翻译。</summary>
/// <summary>外部已存在的 Simple3MiGu.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+2MiGu.Server 重启后 _proc 引用丢失,但上一轮拉起的 SimpleLite 可能仍在跑(因为
// 会话 N+2MiGu.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/Simple3OutputPath 常直接写到 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>:本次实际生效的 displayModeweb / 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);
}
}
}
@@ -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.iniCWD 选错会出意外。</summary>
/// <summary>留空时取 <see cref="ExecutablePath"/> 所在目录。内核在 CWD 读写 simple.json / imgui.iniCWD 选错会出意外。</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 调试)。
///
/// 与 LaunchModeweb / web+local,是否保留本地调试窗口)正交,可任意组合。
/// </summary>
+3 -5
View File
@@ -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>
@@ -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"));
}
}
}
+2 -2
View File
@@ -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
View File
@@ -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 8222AR-1/AR-2 配套)。
// YARP + transform:把 Platform 内部 token 透传给 Simple3 8222AR-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 子进程拉起器接入 DIAuthController 登录成功后按 LaunchMode 调 MaybeStart。
// 配置段绑定 appsettings.json:SimpleLite,可被环境变量 SIMPLELITE__XXX 覆盖。
builder.Services.Configure<SimpleLiteOptions>(builder.Configuration.GetSection("SimpleLite"));
builder.Services.AddSingleton<SimpleLiteLauncher>();
// 会话 N+1(启动反转):把 Simple3 子进程拉起器接入 DIAuthController 登录成功后按 LaunchMode 调 MaybeStart。
// 配置段绑定 appsettings.json:Simple3,可被环境变量 SIMPLE3__XXX 覆盖。
builder.Services.Configure<Simple3Options>(builder.Configuration.GetSection("Simple3"));
builder.Services.AddSingleton<Simple3Launcher>();
builder.Services.AddSingleton<SignalDataStore>();
// OTAWatchDog 编排):包库 / 任务 / 出站客户端
@@ -289,35 +291,32 @@ _ = app.Services.GetRequiredService<JwtIssuer>();
_ = app.Services.GetRequiredService<InternalTokenStore>();
// 主动构造 RbacStore:首启时尽早 seed 默认用户 / 角色并打印 data/rbac.json 载入日志。
_ = app.Services.GetRequiredService<RbacStore>();
// 主动实例化 SimpleLiteLauncherFollowParent=true 时注册 ProcessExit 软关闭钩子)。
var simpleLiteLauncher = app.Services.GetRequiredService<SimpleLiteLauncher>();
// 主动实例化 Simple3LauncherFollowParent=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
View File
@@ -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 内部 APIRV-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 内部 APIRV-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/.../distassets/ 与 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 的两种方式
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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.csOnlineCar/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.csOnlineCar/OfflineCar/DisableCar/EnableCar/Repair/Blown/Reset 等;任务 kind=mission)。运营白名单的 pause/resume/gohome/manualCharge 内核暂无一一对应方法——留空则仅记审计并向前端如实返回『未下发』,按现场内核能力填写后即真实生效。示例: 'ops.car.gohome': 'car:Reset'",
"Ops": {
"Dispatch": {
}
+26 -26
View File
@@ -1,43 +1,43 @@
# 平台 v3 重构 — 阶段 0~6 实际改动
> 范围:把 SimpleLite WebTerminal 由"全功能浮动 Panel"模式收敛到"纯 3D Canvas",所有 UI 改由 Vue 平台前端 (`frontends/apps/simple-platform-vue`) 接管;同时补齐反射 / 编辑 / 监控 / 编排 4 大业务面板与撤销栈、AI 生图、报警 SSE 等关键能力。
> 范围:把 Simple3 WebTerminal 由"全功能浮动 Panel"模式收敛到"纯 3D Canvas",所有 UI 改由 Vue 平台前端 (`frontends/apps/simple-platform-vue`) 接管;同时补齐反射 / 编辑 / 监控 / 编排 4 大业务面板与撤销栈、AI 生图、报警 SSE 等关键能力。
>
> 基线:上一份方案见 `ARCHITECTURE.md` v2/v3 章节;后续以本文件为准。
## 编译验证
- `dotnet build SimpleLite/SimpleLite.csproj` —— 0 警告 0 错误
- `dotnet build Simple3/Simple3.csproj` —— 0 警告 0 错误
- `dotnet build MiGu.Server/MiGu.Server.csproj` —— 0 警告 0 错误
- `build-platform-frontend.bat` —— vite build 46.5s,无 vue-tsc / ts 错误,robocopy /MIR 已同步 MiGu.Server/wwwroot
- 旧 `SimpleComposer` (WinForms) 在 v3 之外,不在本次构建目标里
- 旧 `HttpClientHelper` 提升 LangVersion=latest 修通
## 阶段 0:后端 SimpleLite 平台化(已完成)
## 阶段 0:后端 Simple3 平台化(已完成)
### 新增文件
| 文件 | 作用 |
|------|------|
| `SimpleLite/Web/MapEditApiController.cs` | 写操作 8 路 APIcreate/delete/batch/copy-to/pick/dashboard/upload/ai-generate;阶段 6 又补:**project/save**、**project/load**、**project/current**、**sample-sites-along-track** |
| `SimpleLite/Web/ReflectionApiController.cs` | 只读 + execute APIlistKinds / listObjects / methods-by-type / setField / getBundle / selection 等 |
| `SimpleLite/Web/ReflectionApiDtos.cs` | 反射 API DTO + 统一信封 `ReflectionEnvelope<T>` |
| `SimpleLite/Web/AiCompletionService.cs` | OpenAI Function Calling 调用 + map-generate 工具桥(真实 LLM,不再桩) |
| `SimpleLite/Web/AiConfigController.cs` | AI 服务配置(endpoint/apiKey/model/system prompt/temperature/maxTokens/timeout)读写 |
| `SimpleLite/Web/AlarmStreamService.cs` | 1s 周期 diff `car.status.enums.AlarmInfo`(语义与 `E:\Work\FRLD\StandardScene\Forklift.cs``Commons.GetCarStatus(this, "AlarmInfo")` 等价),变化时通过 SSE `alarm` 事件推送 raise/update/clear |
| `SimpleLite/Web/ProjectionStreamModule.cs` | 平台 SSE 总线,对外广播 `object-created` / `object-deleted` / `object-patched` / `object-batch-changed` / `alarm` / `pick-result` / `project-saved` / `project-loaded` |
| `SimpleLite/Web/PluginAssemblyScan.cs` | 反射扫描 plugins/*.dll,把 `MethodMember` 标注的方法 + Car/Mission/CarProgram/Site/Track 子类型暴露给反射 API |
| `Simple3/Web/MapEditApiController.cs` | 写操作 8 路 APIcreate/delete/batch/copy-to/pick/dashboard/upload/ai-generate;阶段 6 又补:**project/save**、**project/load**、**project/current**、**sample-sites-along-track** |
| `Simple3/Web/ReflectionApiController.cs` | 只读 + execute APIlistKinds / listObjects / methods-by-type / setField / getBundle / selection 等 |
| `Simple3/Web/ReflectionApiDtos.cs` | 反射 API DTO + 统一信封 `ReflectionEnvelope<T>` |
| `Simple3/Web/AiCompletionService.cs` | OpenAI Function Calling 调用 + map-generate 工具桥(真实 LLM,不再桩) |
| `Simple3/Web/AiConfigController.cs` | AI 服务配置(endpoint/apiKey/model/system prompt/temperature/maxTokens/timeout)读写 |
| `Simple3/Web/AlarmStreamService.cs` | 1s 周期 diff `car.status.enums.AlarmInfo`(语义与 `E:\Work\FRLD\StandardScene\Forklift.cs``Commons.GetCarStatus(this, "AlarmInfo")` 等价),变化时通过 SSE `alarm` 事件推送 raise/update/clear |
| `Simple3/Web/ProjectionStreamModule.cs` | 平台 SSE 总线,对外广播 `object-created` / `object-deleted` / `object-patched` / `object-batch-changed` / `alarm` / `pick-result` / `project-saved` / `project-loaded` |
| `Simple3/Web/PluginAssemblyScan.cs` | 反射扫描 plugins/*.dll,把 `MethodMember` 标注的方法 + Car/Mission/CarProgram/Site/Track 子类型暴露给反射 API |
### 修改文件
| 文件 | 关键改动 |
|------|----------|
| `SimpleLite/Startup.cs` | EntryPoint 第一步 `EnsureImguiIniFromEmbedded()`;启动 AlarmStreamService、ProjectionStreamModule、AiConfig 加载 |
| `SimpleLite/UI/SimpleUI.cs` | `CreateRemotePanelSetForTerminal` 检测 CanvasOnly 旗标:不创建 Workbench / Toolbar / BottomBar / CAD / Welcome;保留 SelectionAction、SceneInput、SceneDrawable、2DMapView |
| `SimpleLite/Workspace/WorkspaceWebUiContext.cs` | 增加 `IsCanvasOnly(terminal)` 旗标 + `/declareCanvasOnly` 注册器(与 `/declareEmbedUi` 互斥) |
| `SimpleLite/Web/ProjectionWebHost.cs` | `/declareCanvasOnly` 路由注册 |
| `SimpleLite/Web/ProjectionWebApiController.cs` | 现有 projection API 上挂 `selection-detail` SSE hook |
| `SimpleLite/UI/ComposerDockPanel.cs` | CanvasOnly 时 PrepareWelcomePanel 返回空 |
| `SimpleLite/Props/UIHelpers.cs` | 增加 `UI_Image` / `UI_Text` / `UI_Model` PropType。`UI_Model` 用于 CycleGUI 装饰物挂模型(obj/glb/gltf/cyclemodel |
| `Simple3/Startup.cs` | EntryPoint 第一步 `EnsureImguiIniFromEmbedded()`;启动 AlarmStreamService、ProjectionStreamModule、AiConfig 加载 |
| `Simple3/UI/SimpleUI.cs` | `CreateRemotePanelSetForTerminal` 检测 CanvasOnly 旗标:不创建 Workbench / Toolbar / BottomBar / CAD / Welcome;保留 SelectionAction、SceneInput、SceneDrawable、2DMapView |
| `Simple3/Workspace/WorkspaceWebUiContext.cs` | 增加 `IsCanvasOnly(terminal)` 旗标 + `/declareCanvasOnly` 注册器(与 `/declareEmbedUi` 互斥) |
| `Simple3/Web/ProjectionWebHost.cs` | `/declareCanvasOnly` 路由注册 |
| `Simple3/Web/ProjectionWebApiController.cs` | 现有 projection API 上挂 `selection-detail` SSE hook |
| `Simple3/UI/ComposerDockPanel.cs` | CanvasOnly 时 PrepareWelcomePanel 返回空 |
| `Simple3/Props/UIHelpers.cs` | 增加 `UI_Image` / `UI_Text` / `UI_Model` PropType。`UI_Model` 用于 CycleGUI 装饰物挂模型(obj/glb/gltf/cyclemodel |
### 关键 API 路径
@@ -119,7 +119,7 @@ SSE /projection/stream # 所有 SSE 事件
- 报警来源走 `useMapEditStream.onAlarm`(订阅 SSE 的 `alarm` 事件,由 AlarmStreamService 1s diff 推送)
- KPI 3 卡:在线车辆 / 运行中任务 / 排队任务
- 实时列表:左侧 Workspace3D + 右侧 VehicleMonitorPanel + SelectionDetailPanel
- 「定位」按钮通过 `reflectionApi.setSelection('car', id)` 让 SimpleLite 3D 高亮该车(视觉上等效 flyTo)
- 「定位」按钮通过 `reflectionApi.setSelection('car', id)` 让 Simple3 3D 高亮该车(视觉上等效 flyTo)
## 阶段 5OrchestrationView 设计与编排(已完成)
@@ -159,9 +159,9 @@ Tab 状态缓存到 sessionStorage。
### 编译相关清理
- `SimpleLite/Recording/`(之前的录制/回放实验目录)误用 `SimpleLite.Rendering` namespace、`Car` 找不到、`Start(string, out)` 默认参数顺序等 3 个错误 → 修
- `SimpleLite/Props/UISite.cs``SimpleLite.Props` namespace 下 `Workspace.AddProp` 被解析成 `SimpleLite.Workspace.AddProp` → 改为全限定 `CycleGUI.Workspace.AddProp`
- `SimpleLite/Workspace/WorkspaceBottomBar.cs``using SimpleLite.Workspace;` → 补
- `Simple3/Recording/`(之前的录制/回放实验目录)误用 `Simple3.Rendering` namespace、`Car` 找不到、`Start(string, out)` 默认参数顺序等 3 个错误 → 修
- `Simple3/Props/UISite.cs``Simple3.Props` namespace 下 `Workspace.AddProp` 被解析成 `Simple3.Workspace.AddProp` → 改为全限定 `CycleGUI.Workspace.AddProp`
- `Simple3/Workspace/WorkspaceBottomBar.cs``using Simple3.Workspace;` → 补
- `HttpClientHelper.csproj` LangVersion 7.3 → latest
## 已知 TODO(不阻塞 v3 上线)
@@ -176,16 +176,16 @@ Tab 状态缓存到 sessionStorage。
## 启动方式
```
# 1. 后端 buildSimpleLite + MiGu.Server
dotnet build SimpleLite\SimpleLite.csproj
# 1. 后端 buildSimple3 + MiGu.Server
dotnet build Simple3\Simple3.csproj
dotnet build MiGu.Server\MiGu.Server.csproj
# 2. 前端打包并同步到 wwwroot
.\build-platform-frontend.bat
# 3. 启动平台 + SimpleLite
# 3. 启动平台 + Simple3
dotnet run --project MiGu.Server # 监听 0.0.0.0:8080
# SimpleLite 端口 8222/8223 由 SimpleLite 自己启动
# Simple3 端口 8222/8223 由 Simple3 自己启动
# 4. 访问
http://127.0.0.1:8080/admin/map-editor # 地图编辑(v3 四栏壳)
+18 -18
View File
@@ -1,14 +1,14 @@
# Migu2.0
迷毂智能调度平台主仓库(平台后端 + 平台前端)。
该仓库承接原 `Simple` 仓库中的平台层能力,当前以 `MiGu.Server` 作为统一入口;调度内核 `SimpleLite` 仍在相邻仓库 `../Simple` 中维护。
该仓库承接原 `Simple` 仓库中的平台层能力,当前以 `MiGu.Server` 作为统一入口;调度内核 `Simple3` 仍在相邻仓库 `../Simple` 中维护。
## 仓库定位
- **平台后端**`MiGu.Server`ASP.NET Core 8 + YARP,默认 `:8080`
- **平台前端**`frontends/apps/simple-platform-vue`Vue 3 + Vite
- **调度内核**`SimpleLite`(不在本仓库,位于 `../Simple/SimpleLite`
- **运行模式**:用户登录后由 `MiGu.Server` 按启动模式拉起 `SimpleLite.exe`
- **调度内核**`Simple3`(不在本仓库,位于 `../Simple/Simple3`
- **运行模式**:用户登录后由 `MiGu.Server` 按启动模式拉起 `Simple3.exe`
## 目录结构
@@ -16,11 +16,11 @@
Migu2.0/
├── MiGu.Server/ # 平台后端入口工程
│ ├── Controllers/ # Auth/Config/Ops/Health 等 API
│ ├── Launcher/ # SimpleLite 子进程拉起与健康探测
│ ├── Launcher/ # Simple3 子进程拉起与健康探测
│ ├── Auth/ # JWT / 用户鉴权 / 内部 token
│ ├── Configs/ # 平台配置中心模型与存取
│ ├── wwwroot/ # 已构建前端静态资源(部署用)
│ ├── appsettings.json # 启动、YARP、SimpleLite 路径配置
│ ├── appsettings.json # 启动、YARP、Simple3 路径配置
│ └── README.md # 后端详细说明
├── frontends/ # 平台前端源码 workspace
│ └── apps/simple-platform-vue/
@@ -31,35 +31,35 @@ Migu2.0/
└── Doc/
```
## 与 SimpleLite 的关系
## 与 Simple3 的关系
`Migu2.0``SimpleLite` 是分仓协作关系:
`Migu2.0``Simple3` 是分仓协作关系:
- `MiGu.Server` 提供登录、权限、配置中心、平台 UI 托管与网关能力
- `SimpleLite` 提供调度内核、投影接口、3D/仿真运行时能力
- `Simple3` 提供调度内核、投影接口、3D/仿真运行时能力
- 两者通过以下链路协作:
- **进程编排**`MiGu.Server` 登录后调用 `SimpleLiteLauncher` 拉起 `SimpleLite.exe`
- **进程编排**`MiGu.Server` 登录后调用 `Simple3Launcher` 拉起 `Simple3.exe`
- **接口代理**`/api/sl/*` 通过 YARP 转发到 `http://127.0.0.1:8222/`
- **3D 页面代理**`/vr/*` 转发到 `http://127.0.0.1:8223/`
默认开发路径(可改)在 `MiGu.Server/appsettings.json`
```json
"SimpleLite": {
"Simple3": {
"Enabled": true,
"ExecutablePath": "..\\..\\Simple\\SimpleLite\\bin\\Debug\\SimpleLite.exe",
"WorkingDirectory": "..\\..\\Simple\\SimpleLite\\bin\\Debug",
"ExecutablePath": "..\\..\\Simple\\Simple3\\bin\\Debug\\Simple3.exe",
"WorkingDirectory": "..\\..\\Simple\\Simple3\\bin\\Debug",
"ProjectionPort": 8222
}
```
## 快速启动(开发机)
### 1) 先编译 SimpleLite
### 1) 先编译 Simple3
```powershell
cd ..\Simple
dotnet build SimpleLite\SimpleLite.csproj -c Debug
dotnet build Simple3\Simple3.csproj -c Debug
```
### 2) 启动 MiGu.Server
@@ -74,7 +74,7 @@ dotnet run
- `admin / admin`
- `ops / ops`
> 仅调试平台页面、不拉起内核:将 `MiGu.Server/appsettings.json``SimpleLite.Enabled` 改为 `false`
> 仅调试平台页面、不拉起内核:将 `MiGu.Server/appsettings.json``Simple3.Enabled` 改为 `false`
## 前端构建与发布
@@ -106,7 +106,7 @@ cd E:\Work\Core\Simple-FR\Migu2.0
- `Jwt.Secret`
- `Internal.Token`
- `Auth.Users.*.Password`
- `SimpleLite.ExecutablePath` / `WorkingDirectory`(按部署实际路径)
- `Simple3.ExecutablePath` / `WorkingDirectory`(按部署实际路径)
推荐通过环境变量覆盖(`__` 表示层级):
@@ -119,8 +119,8 @@ $env:Auth__Users__admin__Password = "<strong-password>"
## 常见检查点
- `http://localhost:8080/api/health`:后端健康
- `http://localhost:8080/api/health/simplelite`SimpleLite 路径与端口诊断
- 访问平台时若出现 `502`:通常表示 `SimpleLite``8222` 未就绪或未启动
- `http://localhost:8080/api/health/simple3`Simple3 路径与端口诊断
- 访问平台时若出现 `502`:通常表示 `Simple3``8222` 未就绪或未启动
## 相关文档
+6 -6
View File
@@ -5,7 +5,7 @@
> 通过路由前缀 `/admin/*``/monitor/*` 切换,后续可按 ARCHITECTURE.md §13 拆分为独立工程。
>
> 落地范围与依据:本目录是 [ARCHITECTURE.md](../ARCHITECTURE.md) v1.7 §6/§9/§13/§17/§18 中前端形态的初版骨架;
> 真实业务接入由 `Platform.Server`(同仓库新增的 .NET 8 工程)反代 SimpleLite 完成。
> 真实业务接入由 `Platform.Server`(同仓库新增的 .NET 8 工程)反代 Simple3 完成。
>
> 品牌主色:默认 **工业紫** `#6a1b9a`(6 套主题可切换),详见 ARCHITECTURE.md §17.7。
> 工程包名 `simple-platform-vue` / 仓库目录 `frontends/apps/simple-platform-vue` 保持不动,避免破坏 pnpm/CI 脚本,仅用户可见文案统一为「迷毂」。
@@ -82,7 +82,7 @@ pnpm dev
## 地图监控页与 webVRender
地图监控页(`/admin/map-monitor``/monitor/map`)通过 `<Workspace3D>` 组件以 iframe 嵌入
SimpleLite WebTerminal 提供的 webVRender,默认 URL
Simple3 WebTerminal 提供的 webVRender,默认 URL
```
http://localhost:8223/?scope=Platform&token=<jwt>&ro=0 # 管理员,可交互
@@ -93,10 +93,10 @@ http://localhost:8223/?scope=RCSMonitor&token=<jwt>&ro=1 # 运营,只读
### iframe 嵌入注意事项(CORS / X-Frame-Options
- 当前 SimpleLite WebTerminal 暂未声明 `X-Frame-Options`/`Content-Security-Policy: frame-ancestors`
- 当前 Simple3 WebTerminal 暂未声明 `X-Frame-Options`/`Content-Security-Policy: frame-ancestors`
浏览器多数情况下会按默认策略放行同源 / 局域网嵌入。
- 若浏览器控制台报「Refused to display 'http://localhost:8223/' in a frame」,请在 SimpleLite WebTerminal
侧加上 `X-Frame-Options: ALLOWALL``Content-Security-Policy: frame-ancestors *`(本轮代码未改动 SimpleLite)。
- 若浏览器控制台报「Refused to display 'http://localhost:8223/' in a frame」,请在 Simple3 WebTerminal
侧加上 `X-Frame-Options: ALLOWALL``Content-Security-Policy: frame-ancestors *`(本轮代码未改动 Simple3)。
- 若需通过 `Platform.Server` 走同源代理,可以让 iframe 指向 `http://localhost:8080/vr/`,由 YARP
转发到 `http://127.0.0.1:8223/`(见 `Platform.Server/appsettings.json``vrender-route`)。
@@ -107,7 +107,7 @@ http://localhost:8223/?scope=RCSMonitor&token=<jwt>&ro=1 # 运营,只读
- `/api/config/{section}``Platform.Server` `ConfigController`14 维度占位)
- `/api/health``Platform.Server` `HealthController`
- `/api/projection/*``Platform.Server` `ProjectionController`(占位)
- `/api/sl/*` → YARP 反代 SimpleLite WebAPI `http://127.0.0.1:8222/`
- `/api/sl/*` → YARP 反代 Simple3 WebAPI `http://127.0.0.1:8222/`
- `/api/sl/ops/execute``/api/sl/ops/audits``Platform.Server` `OpsController`(白名单网关占位)
### Mock 开关:`VITE_USE_MOCK`
@@ -3,7 +3,7 @@ import http from './http'
/**
* AI API/ axios fetch SSE
* EventSource GET Authorization headerfetch
* axios Cookie + Bearer + X-Scope SimpleLite `Web/Assistant/AssistantApi.cs`
* axios Cookie + Bearer + X-Scope Simple3 `Web/Assistant/AssistantApi.cs`
*/
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
@@ -120,7 +120,7 @@ export async function streamChat(
if (!resp.ok || !resp.body) {
let msg = `请求失败(HTTP ${resp.status}`
if (resp.status === 403) msg = '无权使用 AI 助手(需要 Platform 权限)。'
else if (resp.status === 502 || resp.status === 504) msg = 'SimpleLite 未连接:请先启动后端(端口 8222)。'
else if (resp.status === 502 || resp.status === 504) msg = 'Simple3 未连接:请先启动后端(端口 8222)。'
else {
try {
const t = await resp.text()
@@ -32,7 +32,7 @@ export async function fetchFleetHealth(): Promise<FleetHealthRow[]> {
await new Promise((r) => setTimeout(r, 120))
return mockFleetHealth()
}
// 平台侧聚合:SimpleLite 指标 + WatchDog(:9776) TCP RTT。
// 平台侧聚合:Simple3 指标 + WatchDog(:9776) TCP RTT。
// 不再直打 /sl/projection/fleet/health(其探测车载 :8081,现场多数未开导致假超时 2000ms)。
try {
const { data } = await http.get<FleetHealthRow[]>('/fleet/health')
@@ -7,7 +7,7 @@ export interface HealthInfo {
uptimeSec: number
}
export interface SimpleLiteDiagnostics {
export interface Simple3Diagnostics {
enabled: boolean
isRunning: boolean
lastLaunchMode?: string | null
@@ -18,7 +18,7 @@ export interface SimpleLiteDiagnostics {
deployHint?: string | null
}
export interface SimpleLiteLaunchResult {
export interface Simple3LaunchResult {
started: boolean
status: string
detail: string
@@ -30,29 +30,26 @@ export function getHealth() {
return http.get<HealthInfo>('/health')
}
export function getSimpleLiteDiagnostics() {
return http.get<SimpleLiteDiagnostics>('/health/simplelite')
export function getSimple3Diagnostics() {
return http.get<Simple3Diagnostics>('/health/simple3')
}
export function stopSimpleLite() {
return http.post<{ killed: number; diagnostics: SimpleLiteDiagnostics }>('/health/simplelite/stop')
export function stopSimple3() {
return http.post<{ killed: number; diagnostics: Simple3Diagnostics }>('/health/simple3/stop')
}
export function restartSimpleLite(launchMode: LaunchMode) {
return http.post<{ restart: SimpleLiteLaunchResult; diagnostics: SimpleLiteDiagnostics }>(
'/health/simplelite/restart',
export function restartSimple3(launchMode: LaunchMode) {
return http.post<{ restart: Simple3LaunchResult; diagnostics: Simple3Diagnostics }>(
'/health/simple3/restart',
null,
{ params: { launchMode }, timeout: 60_000 }
)
}
/** 从诊断/会话 runMode 推断重启时使用的 launchMode。 */
/** Simple3 仅 Web;重启一律 WebOnly(保留函数签名供旧调用方)。 */
export function resolveRestartLaunchMode(
lastLaunchMode: string | null | undefined,
runMode: RunMode | null | undefined
_lastLaunchMode?: string | null,
_runMode?: RunMode | null
): LaunchMode {
const mode = (lastLaunchMode ?? '').toLowerCase()
if (mode === 'web') return 'WebOnly'
if (mode === 'web+local') return 'DesktopAndWeb'
return runMode === 'WebOnly' ? 'WebOnly' : 'DesktopAndWeb'
return 'WebOnly'
}
@@ -31,21 +31,21 @@ http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
})
/**
* axios SimpleLite
* axios Simple3
*
* /api/sl/projection/** Platform.Server YARP SimpleLite EmbedIO :8222
* /api/sl/projection/** Platform.Server YARP Simple3 EmbedIO :8222
*
* SimpleLite / YARP 502 504
* SimpleLite SimpleLite ( 8222)
* Simple3 / YARP 502 504
* Simple3 Simple3 ( 8222)
* `Request failed with status code 502` 怀 bug
*/
function describeError(err: AxiosError): string {
const status = err.response?.status
const url = err.config?.url ?? ''
const isSimpleLite = url.startsWith('/sl/') || url.startsWith('sl/')
const isSimple3 = url.startsWith('/sl/') || url.startsWith('sl/')
if (status === 502 || status === 504 || err.code === 'ECONNABORTED' || err.code === 'ERR_NETWORK') {
if (isSimpleLite) {
return 'SimpleLite 未连接:请先启动 SimpleLite 后端(监听端口 8222)后重试。'
if (isSimple3) {
return 'Simple3 未连接:请先启动 Simple3 后端(监听端口 8222)后重试。'
}
return `网关无法连接到下游服务(${status ?? err.code ?? 'network'})。`
}
@@ -2,8 +2,8 @@ import type { AxiosError } from 'axios'
import http from './http'
/**
* SimpleLite `MapEditApiController`
* `/sl/projection/map-edit`YARP SimpleLite `/projection/map-edit`
* Simple3 `MapEditApiController`
* `/sl/projection/map-edit`YARP Simple3 `/projection/map-edit`
*
* AI `/sl/projection/ai-config`GET / POST AiConfigController
*
@@ -205,7 +205,7 @@ export const mapEditApi = {
unwrap<{ count: number; results: unknown[] }>(http.post(`${BASE}/objects/batch`, { ops })),
/**
* CAD SimpleLite CadAlignService
* CAD Simple3 CadAlignService
* mode: left|right|top|bottom|centerH|centerV|center|distributeH|distributeV
*/
cadAlign: (mode: string, targets: Array<{ kind: string; id: number }>) =>
@@ -264,8 +264,15 @@ export const mapEditApi = {
),
// 拾取 / 仪表盘。snap=false 时落点用原始鼠标坐标(文本/图片/站点);布线取端点默认 true。
pick: (opts?: { snap?: boolean }) =>
unwrap<PickResult>(http.post(`${BASE}/pick`, { snap: opts?.snap ?? true })),
pick: (opts?: { snap?: boolean; signal?: AbortSignal }) =>
unwrap<PickResult>(http.post(
`${BASE}/pick`,
{ snap: opts?.snap ?? true },
{ timeout: 0, signal: opts?.signal }
)),
cancelPick: () =>
unwrap<{ cancelled: boolean }>(http.post(`${BASE}/pick/cancel`)),
dashboardSummary: () =>
unwrap<DashboardSummary>(http.get(`${BASE}/dashboard/summary`)),
@@ -300,9 +307,9 @@ export const mapEditApi = {
}>(http.get(`${BASE}/project/browse`, { params: dir ? { dir } : {} })),
/**
* SimpleLite Windows
* Simple3 Windows
* JSON { path, cancelled }cancelled=true path null
* initialDir SimpleLite
* initialDir Simple3
*/
projectNativePickOpen: (initialDir?: string) =>
unwrap<{ path: string | null; cancelled: boolean }>(
@@ -328,7 +335,7 @@ export const mapEditApi = {
/**
* reactive
* SimpleLite
* Simple3
*/
getViewFilter: () => unwrap<ViewFilterSnapshot>(http.get(`${BASE}/view-filter`)),
@@ -348,7 +355,7 @@ export const aiConfigApi = {
// ──────────────────────────────────────────────────────────────────────────
// 地图管理(固定文件夹 + 按名称列表 / 保存 / 删除 / 使用 / 编辑打开)
// 对应 SimpleLite MapEditApiController 的 /map-edit/maps* 接口。
// 对应 Simple3 MapEditApiController 的 /map-edit/maps* 接口。
// ──────────────────────────────────────────────────────────────────────────
export interface MapListItem {
@@ -6,7 +6,7 @@ import type { Mission, MissionStatus } from '@/types/mission'
import { mockSites, mockTracks, mockCars, mockMissions } from '@/mock/server'
import { applyRuntimeEnrichment, deriveCarState } from '@/utils/carRuntime'
/** true 使 Mock YARP SimpleLite :8222
/** true 使 Mock YARP Simple3 :8222
* auth.ts / config.ts / ops.ts VITE_USE_MOCK VITE_PROJECTION_MOCK */
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
@@ -114,7 +114,7 @@ async function enrichCarsRuntime(cars: Car[]): Promise<Car[]> {
)
}
/** 优先走 SimpleLite /projection/cars502 或空列表时回退 reflection 对象列表(与 3D 场景同源)。 */
/** 优先走 Simple3 /projection/cars502 或空列表时回退 reflection 对象列表(与 3D 场景同源)。 */
export async function listCars(): Promise<Car[]> {
if (MOCK) return mockCars()
try {
@@ -19,10 +19,10 @@ import {
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
/**
* SimpleLite `ReflectionApiController`
* `/sl/projection/reflection` YARP SimpleLite EmbedIO
* Simple3 `ReflectionApiController`
* `/sl/projection/reflection` YARP Simple3 EmbedIO
*
* SimpleLite plugins/*.dll Standard
* Simple3 plugins/*.dll Standard
* `MethodMember` Car/Mission/CarProgram/Site/Track
*
*/
@@ -39,7 +39,7 @@ export type ReflectionKind =
| 'map'
| 'special'
| 'script'
// 顶级合成 kind:等于 site + track + special 的并集,由 SimpleLite 后端 /objects/scene 直接返回,
// 顶级合成 kind:等于 site + track + special 的并集,由 Simple3 后端 /objects/scene 直接返回,
// 每行带 subKind 字段,前端 bundle / execute / setField 走对应底层 kind。
| 'scene'
@@ -167,7 +167,7 @@ export interface ScriptExceptionStatusPayload {
// 地图监控可见性配置(与后端 MonitorVisibilityConfig / MonitorVisibilityForKind 对应)
//
// `config` = 管理员勾选的白名单。空数组 = 显示全部;非空 = 仅显示列表中的 key。
// `available` = 当前 SimpleLite 进程里能扫描到的全部可勾选项(基类 + 已加载子类 +
// `available` = 当前 Simple3 进程里能扫描到的全部可勾选项(基类 + 已加载子类 +
// 运行时实例的 Prop.fields / status 反射键)。是 GET 返回的副产物,
// POST 写入时不需要、也不该回传。
// ──────────────────────────────────────────────────────────────────────────
@@ -253,7 +253,7 @@ export function formatReflectionExecuteMessage(
): string {
if (result.returnValue) return `已执行:${result.returnValue}`
if (result.accepted && result.completed === false) {
return `已受理:${label}(后台继续执行,请稍后在 SimpleLite 查看结果)`
return `已受理:${label}(后台继续执行,请稍后在 Simple3 查看结果)`
}
if (result.accepted) return `已执行 ${label}`
return `已执行 ${label}`
@@ -367,7 +367,7 @@ export const reflectionApi = {
: del<{ kind: string; id: number; deleted: boolean }>(`/objects/${kind}/${id}`),
/**
* SimpleLite ./plugins dll UiDiscoveryCache
* Simple3 ./plugins dll UiDiscoveryCache
* dll
* dll assembly mission/car
*/
@@ -515,7 +515,7 @@ export const reflectionApi = {
return data.data as MonitorConfigSaveBody
},
// 选中同步:让 SimpleLite 3D 场景同步高亮被点击对象
// 选中同步:让 Simple3 3D 场景同步高亮被点击对象
getSelection: () => MOCK
? Promise.resolve<ReflectionSelection>({ kind: null, id: 0, name: '' })
: get<ReflectionSelection>('/selection'),
@@ -685,7 +685,7 @@ export const reflectionApi = {
: post<{ path: string }>('/car-style/save')
}
/** 与 SimpleLite 视口样式对话框 / Configuration.conf.viewport 同步。 */
/** 与 Simple3 视口样式对话框 / Configuration.conf.viewport 同步。 */
export interface ViewportStyleSite {
drawScale: number
dotRadiusNormal: number
@@ -1,14 +1,14 @@
import http from './http'
/**
* API SimpleLite `WorkspaceToolbarApiController`
* API Simple3 `WorkspaceToolbarApiController`
*
* 33 SimpleLite iframe ImGui (`Panel_4` /
* 33 Simple3 iframe ImGui (`Panel_4` /
* `WorkspaceBottomBar.DefineForTerminalEmbedMinimal`) 8 iframe
* Vue
* `WorkspaceCanvasToolbar.vue` HTTP API
*
* `/sl/projection/toolbar` (Platform.Server YARP SimpleLite EmbedIO :8222)
* `/sl/projection/toolbar` (Platform.Server YARP Simple3 EmbedIO :8222)
*/
const BASE = '/sl/projection/toolbar'
@@ -155,7 +155,7 @@ export const workspaceToolbarApi = {
/**
* + 2D
* SimpleLite `WorkspaceToolbarApiController.LocateCamera` = +
* Simple3 `WorkspaceToolbarApiController.LocateCamera` = +
*/
locateCamera: (carId: number) =>
unwrap<ToolbarState>(
@@ -1,6 +1,18 @@
<template>
<PermissionGuard widget-id="ConfigCenter">
<el-card shadow="never">
<slot
v-if="$slots.chrome"
name="chrome"
:payload="payload"
:update="update"
:save="save"
:reload="reload"
:saving="saving"
:loading="loading"
:envelope="envelope"
:relative-time="relativeTime"
/>
<el-card v-else shadow="never">
<template #header>
<div class="cpb-header">
<span class="cpb-title">{{ title }}</span>
@@ -9,7 +21,7 @@
<el-tag size="small" type="success" effect="plain" v-if="envelope?.updatedAt">{{ relativeTime }}</el-tag>
<div class="spacer" />
<el-button size="small" :icon="Refresh" :loading="loading" @click="reload(true)">重载</el-button>
<el-button size="small" type="primary" :icon="Check" :loading="saving" @click="save">保存</el-button>
<el-button size="small" type="primary" :icon="Check" :loading="saving" @click="save()">保存</el-button>
</div>
</template>
@@ -50,11 +62,11 @@ const props = defineProps<{
defaults: T
/** 加载/保存前规范化 payload(如 ops 补全 monitor 字段) */
normalizePayload?: (payload: T) => T
/** 加载完成后二次合并(如从 SimpleLite 拉 monitor */
/** 加载完成后二次合并(如从 Simple3 拉 monitor */
afterLoad?: (payload: T, update: (next: T) => void) => void | Promise<void>
/** 写入 Platform 之前先执行(如先落盘 SimpleLite monitor */
/** 写入 Platform 之前先执行(如先落盘 Simple3 monitor */
beforeSave?: (payload: T) => void | Promise<void>
/** 保存成功后副作用(如再次同步 SimpleLite */
/** 保存成功后副作用(如再次同步 Simple3 */
afterSave?: (payload: T) => void | Promise<void>
}>()
@@ -98,7 +110,8 @@ async function reload(force = false) {
await reloadFromServer(force, force)
}
async function save() {
async function save(opts?: { toast?: boolean }): Promise<boolean> {
const showToast = typeof opts === 'object' && opts && 'toast' in opts ? opts.toast !== false : true
saving.value = true
let body = payload.value
if (props.normalizePayload) body = props.normalizePayload(body)
@@ -109,7 +122,7 @@ async function save() {
} catch (e) {
ElMessage.error(`地图监控配置保存失败:${e instanceof Error ? e.message : String(e)}`)
saving.value = false
return
return false
}
}
@@ -122,14 +135,16 @@ async function save() {
try {
await props.afterSave(body)
} catch (e) {
monitorSyncWarn = `(平台配置已保存,但 SimpleLite 同步失败:${e instanceof Error ? e.message : String(e)}`
monitorSyncWarn = `(平台配置已保存,但 Simple3 同步失败:${e instanceof Error ? e.message : String(e)}`
}
}
await reloadFromServer(true, false)
const extra = props.beforeSave || props.afterSave ? ',地图监控配置已写入 SimpleLite' : ''
ElMessage.success(`已保存 v${env.version}${extra}${monitorSyncWarn}`)
const extra = props.beforeSave || props.afterSave ? ',地图监控配置已写入 Simple3' : ''
if (showToast) ElMessage.success(`已保存 v${env.version}${extra}${monitorSyncWarn}`)
return true
} catch (e) {
ElMessage.error(`保存失败:${e instanceof Error ? e.message : String(e)}`)
return false
} finally {
saving.value = false
}
@@ -26,7 +26,7 @@
<div v-if="!loaded" class="workspace-3d-mask">
<el-icon class="is-loading" size="32"><Loading /></el-icon>
<span>{{ loadingHint }}</span>
<span class="muted">如长时间未加载请确认 SimpleLite 已以 Web-Enabled 模式启动且 8223 端口可达</span>
<span class="muted">如长时间未加载请确认 Simple3 已以 Web-Enabled 模式启动且 8223 端口可达</span>
</div>
</div>
</div>
@@ -45,7 +45,7 @@ const props = withDefaults(defineProps<{
scope?: Scope
token?: string
readOnly?: boolean
/** 嵌入平台页时传 trueSimpleLite 仅显示底栏(对齐/选择/右击/图层/内容/录制回放) */
/** 嵌入平台页时传 trueSimple3 仅显示底栏(对齐/选择/右击/图层/内容/录制回放) */
embedUi?: boolean
/**
* 地图编辑器纯画布模式 embedUi 更裸连底栏都不创建
@@ -116,34 +116,73 @@ function declareEndpoint() {
return null
}
/** 单次 HTTP 探测(带超时),用于 declare / vrenderReady。 */
async function pingVrender(path: string): Promise<boolean> {
/** 单次 HTTP 探测(带超时)。Simple3 把 declare* 挂在 Projection :8222,不再走 FairyView :8223。 */
async function pingDeclare(path: string): Promise<boolean> {
const token = localStorage.getItem('simple.auth.token')
// YARP :8222 /projection/ui 退 Simple3 :8223 LeastServer
const urls = [
`/api/sl${path}`,
`/api/sl/projection/ui${path}`,
`http://${resolvedHost.value}${path}`
]
for (const url of urls) {
try {
const res = await fetch(`${vrenderBaseUrl()}${path}`, {
const headers: Record<string, string> = {}
if (token && url.startsWith('/')) headers.Authorization = `Bearer ${token}`
const res = await fetch(url, {
method: 'GET',
cache: 'no-store',
credentials: 'omit',
credentials: url.startsWith('/') ? 'include' : 'omit',
headers,
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
})
return res.ok
if (res.ok) return true
} catch {
return false
/* try next */
}
}
return false
}
/** 画布就绪探测:仍优先打 FairyView :8223iframe 同源),失败再试 Projection 镜像。 */
async function pingVrender(path: string): Promise<boolean> {
const candidates = [
`http://${resolvedHost.value}${path}`,
`/api/sl${path}`,
`/api/sl/projection/ui${path}`
]
const token = localStorage.getItem('simple.auth.token')
for (const url of candidates) {
try {
const headers: Record<string, string> = {}
if (token && url.startsWith('/')) headers.Authorization = `Bearer ${token}`
const res = await fetch(url, {
method: 'GET',
cache: 'no-store',
credentials: url.startsWith('/') ? 'include' : 'omit',
headers,
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
})
if (res.ok) return true
} catch {
/* try next */
}
}
return false
}
/**
* 尽力 declare不阻塞 iframeSimpleLite WebTerminal 已默认 canvas-only
* declare 主要用于多 tab 显式标记失败也不影响平台纯画布模式
* 尽力 declareSimple3 须打 Projection :8222 /api/sl不能只打 :8223
*/
async function postDeclareBestEffort() {
const endpoint = declareEndpoint()
if (!endpoint) return
for (let attempt = 0; attempt < DECLARE_MAX_ATTEMPTS; attempt++) {
if (await pingVrender(endpoint)) return
if (await pingDeclare(endpoint)) return
await sleep(80 * (attempt + 1))
}
console.warn(`[Workspace3D] ${endpoint} 未成功,将依赖 SimpleLite 默认 canvas-only 模式`)
console.warn(`[Workspace3D] ${endpoint} 未成功,将依赖内核默认 canvas-only 模式`)
}
/** 非嵌入模式:短轮询等待 8223 就绪后再加载 iframe。 */
@@ -169,7 +208,7 @@ function startLoadWatchdog(generation: number) {
clearLoadWatchdog()
loadWatchdog = setTimeout(() => {
if (generation !== prepareGeneration || loaded.value) return
loadingHint.value = `连接超时 (http://${resolvedHost.value}),请确认 SimpleLite 已启动`
loadingHint.value = `连接超时 (http://${resolvedHost.value}),请确认 Simple3 已启动`
}, LOAD_WATCHDOG_MS)
}
@@ -199,7 +238,7 @@ async function prepareAndLoad(forceReload = false) {
}
iframeSrc.value = buildIframeSrc(forceReload || !!iframeSrc.value)
} else {
loadingHint.value = '正在等待 SimpleLite webVRender 就绪 ...'
loadingHint.value = '正在等待 Simple3 webVRender 就绪 ...'
await waitForVrenderReady()
if (generation !== prepareGeneration) return
iframeSrc.value = buildIframeSrc(forceReload || !!iframeSrc.value)
@@ -12,7 +12,7 @@
<div v-if="!collapsed">
<el-empty
v-if="!loading && plugins.length === 0"
description="尚未通过 PluginManager 加载任何插件 (重启 SimpleLite 后会自动迁移现有 plugins/ 内的 dll)"
description="尚未通过 PluginManager 加载任何插件 (重启 Simple3 后会自动迁移现有 plugins/ 内的 dll)"
/>
<el-table
v-else
@@ -44,7 +44,7 @@
<el-tag v-if="row.collectible" size="small" type="success">可卸载</el-tag>
<el-tooltip
v-else
content="此插件被加到 default ALC(旧路径加载),重启 SimpleLite 后会自动迁移到 collectible"
content="此插件被加到 default ALC(旧路径加载),重启 Simple3 后会自动迁移到 collectible"
placement="top"
>
<el-tag size="small" type="warning">不可卸载</el-tag>
@@ -98,7 +98,7 @@ async function refresh() {
try {
plugins.value = await reflectionApi.listPlugins()
} catch (err) {
ElMessage.warning(`插件列表加载失败:${(err as Error).message}(旧版 SimpleLite 可能未带 /plugins 接口,请重启)`)
ElMessage.warning(`插件列表加载失败:${(err as Error).message}(旧版 Simple3 可能未带 /plugins 接口,请重启)`)
} finally {
loading.value = false
}
@@ -150,7 +150,7 @@
>
查看对象
</el-button>
<!-- 脚本管理专用 SimpleLite 工作台脚本表格行内按钮组对齐
<!-- 脚本管理专用 Simple3 工作台脚本表格行内按钮组对齐
选不选中都能直接点行内查看脚本 / 查看异常状态弹窗 -->
<template v-if="showScriptActions && kind === 'script'">
<el-button
@@ -492,7 +492,7 @@
* site/track/image/text/model 走弹窗表单收集 x/y/siteA/siteB
* - 删除reflectionApi.deleteObject
* - 方法调用reflectionApi.execute带参时弹 prompt 逐项收集
* - 3D 高亮reflectionApi.setSelection SimpleLite 工作台同步选中
* - 3D 高亮reflectionApi.setSelection Simple3 工作台同步选中
*
* SSE 订阅object-created/deleted/patched/batch-changed 都会自动 refresh
*/
@@ -556,7 +556,7 @@ const props = defineProps<{
* 列表状态列取自运行状态反射中的 key如进程 Mission status
*/
statusReflectionKey?: string
/** 脚本管理专用:在动作区追加 SimpleLite 工作台同款「查看脚本 / 查看异常状态」。 */
/** 脚本管理专用:在动作区追加 Simple3 工作台同款「查看脚本 / 查看异常状态」。 */
showScriptActions?: boolean
/** 增删改后是否自动写回项目 JSON。默认车辆/场景/进程启用。 */
autoSaveProject?: boolean
@@ -363,7 +363,7 @@ async function loadActions() {
async function onExecute(m: ReflectionMethod) {
// methodName = workbench action""
if (!m.methodName) {
ElMessage.info(`动作「${m.label || m.methodName}」需在 SimpleLite 工作台执行(当前对象类型未接入反射 execute)。`)
ElMessage.info(`动作「${m.label || m.methodName}」需在 Simple3 工作台执行(当前对象类型未接入反射 execute)。`)
return
}
const rk = reflectionKind.value
@@ -65,7 +65,7 @@ const search = ref('')
const selectedId = ref<string | null>(null)
const navHints: Record<WorkbenchNav, string> = {
map: '地图/装饰对象列表(对齐 SimpleLite 工作台「地图」)',
map: '地图/装饰对象列表(对齐 Simple3 工作台「地图」)',
process: '自定义进程 Mission 列表',
vehicle: '场景车辆列表(ID / 地址 / 名称 / 概况)',
scene: '站点、路径、装饰物',
@@ -260,10 +260,10 @@ function publishState(next: ToolbarState | null) {
}
/**
* 平台 iframe 画布工具栏对应 SimpleLite `Panel_4` / `WorkspaceBottomBar.DefineForTerminalEmbedMinimal`
* 平台 iframe 画布工具栏对应 Simple3 `Panel_4` / `WorkspaceBottomBar.DefineForTerminalEmbedMinimal`
*
* 设计原则
* - 所有状态都通过 `/sl/projection/toolbar/*` SimpleLite 全局状态 tab 同源
* - 所有状态都通过 `/sl/projection/toolbar/*` Simple3 全局状态 tab 同源
* - `state` 是后端权威单一真值本地不维护"乐观"切换每次 toggle 都拿后端最新返回回写
* - 录制 / 回放期间 1Hz 轮询刷新 elapsed / 帧计数避免页面打开后停留在初值
* - 错误用 ElMessage.error 提示不阻断画布主操作
@@ -386,7 +386,7 @@ async function toggleLayer(name: string, visible: boolean) {
}
function onContextMenuPlaceholder() {
ElMessage.info('寻路右键动作尚未接入 SimpleLite;请使用 SegmentPlan.FindRoute / 工作台脚本。')
ElMessage.info('寻路右键动作尚未接入 Simple3;请使用 SegmentPlan.FindRoute / 工作台脚本。')
}
async function toggleRecording() {
@@ -1,11 +1,11 @@
import { onBeforeUnmount, onMounted } from 'vue'
/**
* Workspace3D iframeSimpleLite WebTerminal postMessage
* Workspace3D iframeSimple3 WebTerminal postMessage
*
* SimpleLite WebTerminal window.postMessage `workspace.pick`
* Simple3 WebTerminal window.postMessage `workspace.pick`
* `workspace.select` Workspace3D.vue emit
* vueiframe SimpleLite APIHTTP/SSE
* vueiframe Simple3 APIHTTP/SSE
* ** / / hover** iframevue
*/
@@ -6,7 +6,7 @@ import { ref, computed } from 'vue'
*
*
* - **** Vue + + pick API
* - **** API `/execute/...` SimpleLite CAD action / /
* - **** API `/execute/...` Simple3 CAD action / /
*
* composable CAD action
*/
@@ -2,7 +2,7 @@ import { onBeforeUnmount, onMounted } from 'vue'
import { useProjectionStream, type StreamEvent } from './useProjectionStream'
/**
* / SimpleLite SSE 5
* / Simple3 SSE 5
*
* - `alarm` ( AlarmStreamService)//
* - `object-created` ( MapEditApiController)
@@ -38,14 +38,14 @@ export interface PickResultEvent {
}
export interface SelectionDetailEvent {
/** 主选中 kind (site|track|car|null);当多选时取首个非空集合的 kind。 */
kind: 'site' | 'track' | 'car' | null
/** 主选中 kind;当多选时取首个非空集合的 kind。 */
kind: 'site' | 'track' | 'car' | 'special' | null
/** 主选中 id;多选时取首个。 */
id: number
siteIds: number[]
trackIds: number[]
carIds: number[]
/** 完整对象名列表,例如 ["UISite-12", "UITrack-7", "Car-3"]。 */
/** 完整对象名列表,例如 ["UISite-12", "UITrack-7", "Car-3", "UIText-5"]。 */
names: string[]
}
@@ -61,7 +61,7 @@ export interface MapEditStreamHandlers {
onObjectBatchChanged?: (e: { count: number; source?: string }) => void
onPickResult?: (e: PickResultEvent) => void
onSelectionDetail?: (e: SelectionDetailEvent) => void
/** iframe 画布聚焦时由 SimpleLite 经 SSE 转发的撤销/重做/删除。 */
/** iframe 画布聚焦时由 Simple3 经 SSE 转发的撤销/重做/删除。 */
onWorkspaceShortcut?: (e: WorkspaceShortcutEvent) => void
/** 视口样式(站点/路径 Painter)在 Web 或桌面对话框中变更后广播。 */
onViewportStyleUpdated?: () => void
@@ -1,7 +1,7 @@
import { onUnmounted, ref, type Ref } from 'vue'
/**
* SimpleLite `/projection/stream`SSE
* Simple3 `/projection/stream`SSE
*
* `text/event-stream` JSON
* ```
@@ -33,7 +33,7 @@ export interface ProjectionStreamOptions {
}
/**
* SimpleLite 广 SSE
* Simple3 广 SSE
* EventSource alarm / object-*
*/
export const KNOWN_EVENT_KINDS = [
@@ -123,7 +123,7 @@ export function useProjectionStream(opts: ProjectionStreamOptions = {}) {
// 命名事件:EventSource 默认只把"无 event: 头"的消息派发给 onmessage
// 任何 `event: foo` 都必须通过 addEventListener('foo', ...) 才能收到。
// 必须覆盖 SimpleLite 后端可能广播的所有事件名 —— 漏一个就等于该事件被丢弃。
// 必须覆盖 Simple3 后端可能广播的所有事件名 —— 漏一个就等于该事件被丢弃。
// 当前后端事件源:
// - ProjectionStreamModule: snapshot-tick (1Hz 心跳/计数)
// - SimpleUI / ReflectionApiController: selection-detail
@@ -5,8 +5,7 @@ import type { ReflectionObject, ReflectionKind } from '@/api/reflection'
* +
* `(kind, id)` `${kind}:${id}` key
*
* SimpleLite selection ReflectionApi 3D
* API
* postMessage / SSE watch SetSelection
*/
export interface SelectionItem extends ReflectionObject {
@@ -70,7 +70,6 @@
</div>
<div class="header-right">
<ThemeSwitcher />
<ScopeSwitcher />
<span class="runmode-tag" :class="auth.runMode">{{ runModeLabel }}</span>
<el-dropdown trigger="click" @command="onUserCommand">
<span class="user-link">
@@ -91,6 +90,7 @@
</el-header>
<el-main class="main mg-content">
<WizardSetupBar />
<!-- 不用 mode="out-in"懒加载子路由切换时易出现离场后新组件未挂载 主区全白刷新才恢复 -->
<router-view v-slot="{ Component, route: rv }">
<component :is="Component" v-if="Component" :key="rv.fullPath" class="route-page" />
@@ -115,9 +115,9 @@ import { Fold, Expand, CaretBottom } from '@element-plus/icons-vue'
import { ADMIN_MENU, MONITOR_MENU, type NavMenuItem } from '@/config/navMenu'
import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
import ScopeSwitcher from '@/components/ScopeSwitcher.vue'
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
import AiAssistantDrawer from '@/components/assistant/AiAssistantDrawer.vue'
import WizardSetupBar from '@/components/wizard/WizardSetupBar.vue'
const route = useRoute()
const router = useRouter()
@@ -129,13 +129,13 @@ const apiBase = computed(() => (import.meta.env.VITE_API_BASE as string | undefi
const initial = computed(() => (auth.user?.displayName ?? auth.user?.username ?? '?').slice(0, 1).toUpperCase())
const scopeLabel = computed(() => (auth.scope === 'Platform' ? '管理员 (Platform)' : '运营 (RCSMonitor)'))
const scopeLabel = computed(() => (auth.scope === 'Platform' ? '管理' : '运营'))
const runModeLabel = computed(() => {
switch (auth.runMode) {
case 'WebOnly': return 'Web-Only'
case 'WebEnabled': return 'Web + Desktop'
case 'Detached': return 'SimpleLite 未连接'
case 'Detached': return 'Simple3 未连接'
default: return String(auth.runMode ?? 'Unknown')
}
})
@@ -498,7 +498,7 @@ function onUserCommand(cmd: string) {
border-color: rgba(var(--mg-primary-hover-rgb), 0.45);
color: var(--mg-accent);
}
/* 会话 N+1 增量:SimpleLite 未拉起时的降级展示,用警示色而非成功色,引导用户感知"实际只有 Platform"。 */
/* 会话 N+1 增量:Simple3 未拉起时的降级展示,用警示色而非成功色,引导用户感知"实际只有 Platform"。 */
.runmode-tag.Detached {
background: rgba(var(--mg-status-warning-rgb), 0.18);
border-color: rgba(var(--mg-status-warning-rgb), 0.45);
@@ -3,10 +3,10 @@ import type { Car } from '@/types/car'
const NOW = new Date().toISOString()
export const CARS: Car[] = [
{ id: 'C01', rawId: 1, name: 'AGV-001', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 1200, y: 2100, theta: 0, batterySoc: 0.86, state: 'running', missionId: 'M01', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.11', onboardUrl: 'http://10.0.1.11:8081', lstatus: '运行' },
{ id: 'C02', rawId: 2, name: 'AGV-002', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 2900, y: 2100, theta: 90, batterySoc: 0.42, state: 'running', missionId: 'M02', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.12', onboardUrl: 'http://10.0.1.12:8081', lstatus: '运行' },
{ id: 'C03', rawId: 3, name: 'AGV-003', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 1000, y: 4900, theta: 180, batterySoc: 1.0, state: 'charging', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.13', onboardUrl: 'http://10.0.1.13:8081', lstatus: '充电' },
{ id: 'C04', rawId: 4, name: 'AGV-004', typeName: 'SimpleLite.RCS.CarTypes.GhostCar', x: 7000, y: 2050, theta: 0, batterySoc: 0.71, state: 'idle', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.14', onboardUrl: 'http://10.0.1.14:8081', lstatus: '空闲' },
{ id: 'C05', rawId: 5, name: 'AGV-005', typeName: 'SimpleLite.RCS.CarTypes.GhostCar', x: 7050, y: 4000, theta: 270, batterySoc: 0.18, state: 'fault', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.15', onboardUrl: 'http://10.0.1.15:8081', lstatus: '故障' },
{ id: 'C06', rawId: 6, name: 'AGV-006', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 9000, y: 2000, theta: 0, batterySoc: 0.95, state: 'offline', lastUpdate: NOW, group: '维护', ip: '10.0.1.16', onboardUrl: 'http://10.0.1.16:8081', lstatus: '下线' }
{ id: 'C01', rawId: 1, name: 'AGV-001', typeName: 'Simple3.RCS.CarTypes.DummyCar', x: 1200, y: 2100, theta: 0, batterySoc: 0.86, state: 'running', missionId: 'M01', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.11', onboardUrl: 'http://10.0.1.11:8081', lstatus: '运行' },
{ id: 'C02', rawId: 2, name: 'AGV-002', typeName: 'Simple3.RCS.CarTypes.DummyCar', x: 2900, y: 2100, theta: 90, batterySoc: 0.42, state: 'running', missionId: 'M02', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.12', onboardUrl: 'http://10.0.1.12:8081', lstatus: '运行' },
{ id: 'C03', rawId: 3, name: 'AGV-003', typeName: 'Simple3.RCS.CarTypes.DummyCar', x: 1000, y: 4900, theta: 180, batterySoc: 1.0, state: 'charging', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.13', onboardUrl: 'http://10.0.1.13:8081', lstatus: '充电' },
{ id: 'C04', rawId: 4, name: 'AGV-004', typeName: 'Simple3.RCS.CarTypes.GhostCar', x: 7000, y: 2050, theta: 0, batterySoc: 0.71, state: 'idle', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.14', onboardUrl: 'http://10.0.1.14:8081', lstatus: '空闲' },
{ id: 'C05', rawId: 5, name: 'AGV-005', typeName: 'Simple3.RCS.CarTypes.GhostCar', x: 7050, y: 4000, theta: 270, batterySoc: 0.18, state: 'fault', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.15', onboardUrl: 'http://10.0.1.15:8081', lstatus: '故障' },
{ id: 'C06', rawId: 6, name: 'AGV-006', typeName: 'Simple3.RCS.CarTypes.DummyCar', x: 9000, y: 2000, theta: 0, batterySoc: 0.95, state: 'offline', lastUpdate: NOW, group: '维护', ip: '10.0.1.16', onboardUrl: 'http://10.0.1.16:8081', lstatus: '下线' }
]
@@ -4,7 +4,7 @@ const NOW = new Date().toISOString()
export const MISSIONS: Mission[] = [
{
id: 'M01', name: 'A 区送料 #1', typeName: 'SimpleLite.RCS.Missions.MoveMission',
id: 'M01', name: 'A 区送料 #1', typeName: 'Simple3.RCS.Missions.MoveMission',
priority: 50, status: 'running', carId: 'C01', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S001', status: 'completed' },
@@ -13,7 +13,7 @@ export const MISSIONS: Mission[] = [
]
},
{
id: 'M02', name: 'A→B 缓存搬运', typeName: 'SimpleLite.RCS.Missions.MoveMission',
id: 'M02', name: 'A→B 缓存搬运', typeName: 'Simple3.RCS.Missions.MoveMission',
priority: 60, status: 'running', carId: 'C02', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S002', status: 'completed' },
@@ -21,7 +21,7 @@ export const MISSIONS: Mission[] = [
]
},
{
id: 'M03', name: 'B 区工位 W1 投料', typeName: 'SimpleLite.RCS.Missions.MoveMission',
id: 'M03', name: 'B 区工位 W1 投料', typeName: 'Simple3.RCS.Missions.MoveMission',
priority: 70, status: 'queued', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S003', status: 'queued' },
@@ -29,7 +29,7 @@ export const MISSIONS: Mission[] = [
]
},
{
id: 'M04', name: 'W2→维护检修', typeName: 'SimpleLite.RCS.Missions.MaintenanceMission',
id: 'M04', name: 'W2→维护检修', typeName: 'Simple3.RCS.Missions.MaintenanceMission',
priority: 30, status: 'paused', carId: 'C05', createdAt: NOW,
steps: [
{ id: 'S1', action: 'goto', targetSiteId: 'S010', status: 'paused' }
@@ -254,7 +254,7 @@ export function mockReflectionKinds(): { kinds: ReflectionKindMeta[]; subKinds:
export function mockReflectionAssemblies(): ReflectionAssembly[] {
return [
{ name: 'SimpleCore', version: '1.0.0' },
{ name: 'SimpleLite', version: '1.0.0' },
{ name: 'Simple3', version: '1.0.0' },
{ name: 'StandardScene', version: '1.4.0', location: 'plugins/StandardScene.dll' },
{ name: 'CustomFlightDeck', version: '0.1.0', location: 'plugins/CustomFlightDeck.dll' }
]
@@ -273,7 +273,7 @@ export function mockReflectionMethodsByType(kind: ReflectionKind): ReflectionTyp
if (kind === 'car' || kind === 'vehicle' || kind === 'script') {
return [
{ typeName: 'GhostCar', assemblyName: 'StandardScene', methods: ms },
{ typeName: 'DummyCar', assemblyName: 'SimpleLite', methods: ms.slice(0, 2) }
{ typeName: 'DummyCar', assemblyName: 'Simple3', methods: ms.slice(0, 2) }
]
}
if (kind === 'mission' || kind === 'process') {
@@ -282,7 +282,7 @@ export function mockReflectionMethodsByType(kind: ReflectionKind): ReflectionTyp
{ typeName: 'PatrolMission', assemblyName: 'CustomFlightDeck', methods: ms.slice(0, 2) }
]
}
return [{ typeName: kind, assemblyName: 'SimpleLite', methods: ms }]
return [{ typeName: kind, assemblyName: 'Simple3', methods: ms }]
}
export function mockReflectionStatus(kind: ReflectionKind, id: number) {
@@ -2,20 +2,19 @@ export type Scope = 'Platform' | 'RCSMonitor'
/**
*
* - `WebEnabled`SimpleLite LocalTerminal+ WebTerminal
* - `WebOnly`SimpleLite WebTerminal使
* - `Detached` N+1 Platform.Server SimpleLiteappsettings / exe /
* Process.Start / SimpleLite /api/sl/*
* - `WebEnabled`Simple3 LocalTerminal+ WebTerminal
* - `WebOnly`Simple3 WebTerminal使
* - `Detached` N+1 Platform.Server Simple3appsettings / exe /
* Process.Start / Simple3 /api/sl/*
*/
export type RunMode = 'WebOnly' | 'WebEnabled' | 'Detached'
/**
* ()
* - `DesktopAndWeb` + Web
* - `WebOnly` Web /
* - `WebOnly` WebSimple3
* - `DesktopAndWeb` + WebSimple3 使
*
* RunMode LaunchMode RunMode
* DesktopAndWeb WebEnabledWebOnly WebOnly
*/
export type LaunchMode = 'DesktopAndWeb' | 'WebOnly'
@@ -50,8 +49,9 @@ export interface AuthUser {
export interface LoginRequest {
username: string
password: string
scope: Scope
/** 会话 N+1:可选;不传时后端按 DesktopAndWeb 处理(向后兼容历史前端)。 */
/** 已废弃:登录入口由账号角色决定,客户端不再挑选管理/运营。 */
scope?: Scope
/** 可选;不传时后端按 WebOnlySimple3)。 */
launchMode?: LaunchMode
}
@@ -62,7 +62,7 @@ export interface LoginResponse {
effectivePermissions: EffectivePermissions
runMode: RunMode
/**
* N+1 SimpleLiteLauncher.LaunchResult.Status
* N+1 Simple3Launcher.LaunchResult.Status
* Disabled / AlreadyRunning / ReusingExisting / PortOccupied / Ready / StartedButNotReady /
* ExecutableNotFound / ProcessStartFailed / Exception
*
@@ -70,7 +70,7 @@ export interface LoginResponse {
launchStatus?: string
/**
* N+1
* SimpleLite
* Simple3
*/
launchWarning?: string
/**
@@ -92,4 +92,6 @@ export interface MeResponse {
effectivePermissions: EffectivePermissions
/** 部署配置向导是否待完成(与 LoginResponse.needsWizard 同义)。 */
needsWizard?: boolean
/** 历史「切运营端」残留 token 被纠正时,后端重发的新 JWT。 */
token?: string
}
@@ -47,7 +47,7 @@ export interface DeploymentProfileDto {
navigationKinds: string[]
scenarios: string[]
updatedBy: string
/** 由导航 + 业务场景推导的 SimpleLite 激活场景 id(如 scene.mag / scene.qrlidarscene.signal 仅 SPS / Pack)。 */
/** 由导航 + 业务场景推导的 Simple3 激活场景 id(如 scene.mag / scene.qrlidarscene.signal 仅 SPS / Pack)。 */
activeSceneIds: string[]
/** 被部署画像裁剪隐藏的页面 Key。 */
hiddenPages: string[]
@@ -30,7 +30,7 @@ export async function loadSitePickOptions(): Promise<SitePickOption[]> {
.sort((a, b) => a.id - b.id)
}
/** 在地图上拾取目标站点(调用 SimpleLite pick 会话)。 */
/** 在地图上拾取目标站点(调用 Simple3 pick 会话)。 */
export async function pickTargetSiteOnMap(): Promise<number | null> {
try {
ElMessage.info({ message: '请在 3D 地图上点击目标站点附近', duration: 4000 })
@@ -77,7 +77,7 @@ export async function executeCarGotoSite(carId: number, siteId: number): Promise
/WebGotoSite|goto-site|不存在可调用方法/i.test(msg2)
ElMessage.error(
needDeploy
? `${msg2}。当前 SimpleLite 仍是旧版 DLL:请先完全关闭 SimpleLite 窗口,再在 PowerShell 执行 Migu2.0/scripts/redeploy-simplelite.ps1,然后重新登录;或调用 POST /api/health/simplelite/restart-for-update`
? `${msg2}。当前 Simple3 仍是旧版 DLL:请先完全关闭 Simple3 窗口,再在 PowerShell 执行 Migu2.0/scripts/redeploy-simple3.ps1,然后重新登录;或调用 POST /api/health/simple3/restart-for-update`
: `前往站点失败:${msg2}`
)
return false
@@ -43,7 +43,7 @@ function compactJsonList(raw: string, maxItems = 3): string {
}
/**
* SimpleLite /projection/cars fault
* Simple3 /projection/cars fault
* / AlarmLevel lstatus + status
*/
export function deriveCarState(car: Pick<Car, 'state' | 'lstatus'>, statusRows?: Kv[] | null): CarState {
@@ -93,7 +93,7 @@ export function deriveCarState(car: Pick<Car, 'state' | 'lstatus'>, statusRows?:
}
/**
* SimpleLite CarVisual.FormatTrafficStatus
* Simple3 CarVisual.FormatTrafficStatus
* 1) TCStat BeforeLock
* 2) blockedBy XY
* 3) holdingLocks
@@ -35,7 +35,7 @@ export function normalizeOpsConfig(raw?: Partial<OpsConfig> | null): OpsConfig {
}
}
/** 深拷贝车型动作表,避免与 SimpleLite 返回对象共享引用导致勾选 UI 不同步。 */
/** 深拷贝车型动作表,避免与 Simple3 返回对象共享引用导致勾选 UI 不同步。 */
export function cloneCarActionByType(
src?: Record<string, string[] | readonly string[]> | null
): Record<string, string[]> {
@@ -57,12 +57,12 @@ function pickMonitorRoot(cfg: MonitorConfigRoot) {
}
/**
* SimpleLite simple.json
* Platform ops monitor SimpleLite
* Simple3 simple.json
* Platform ops monitor Simple3
*/
export interface LoadMonitorSettingsResult {
config: OpsConfig
/** 与 SimpleLite 对齐的 monitor-config 快照,供保存时合并 fields/status,避免每次 POST 前再 GET。 */
/** 与 Simple3 对齐的 monitor-config 快照,供保存时合并 fields/status,避免每次 POST 前再 GET。 */
cache: MonitorConfigRoot | null
}
@@ -80,13 +80,13 @@ export async function loadMonitorSettingsIntoOps(
next.monitor.site.actionKeys = picked.siteActionKeys
next.monitor.track.actionKeys = picked.trackActionKeys
} catch {
// SimpleLite 未连接:退回 Platform payload 中已有的 monitor(若有)
// Simple3 未连接:退回 Platform payload 中已有的 monitor(若有)
}
return { config: next, cache }
}
/**
* SimpleLitesimple.json
* Simple3simple.json
* fields/status
*/
export interface SaveMonitorSettingsOptions {
@@ -132,7 +132,7 @@ export async function saveMonitorSettingsFromOps(
}
/** @deprecated 使用 loadMonitorSettingsIntoOps */
export const mergeMonitorFromSimpleLite = loadMonitorSettingsIntoOps
export const mergeMonitorFromSimple3 = loadMonitorSettingsIntoOps
/** @deprecated 使用 saveMonitorSettingsFromOps */
export const syncOpsMonitorToSimpleLite = saveMonitorSettingsFromOps
export const syncOpsMonitorToSimple3 = saveMonitorSettingsFromOps
@@ -1,5 +1,5 @@
/**
* webVRender (SimpleLite 3D , 默认 :8223) host
* webVRender (Simple3 3D , 默认 :8223) host
*
* VITE_VRENDER_HOST > hostname:8223
* localhost 访 iframe 访
@@ -15,7 +15,7 @@
<el-descriptions-item label="运行时长" :span="2">{{ serverUptime || '—' }}</el-descriptions-item>
</el-descriptions>
<el-descriptions v-if="sl" :column="2" border title="SimpleLite" class="status-block">
<el-descriptions v-if="sl" :column="2" border title="Simple3" class="status-block">
<el-descriptions-item label="托管启用">
<el-tag :type="sl.enabled ? 'success' : 'info'" size="small">{{ sl.enabled ? '是' : '否' }}</el-tag>
</el-descriptions-item>
@@ -43,15 +43,15 @@
<el-alert v-if="error" :title="error" type="warning" :closable="false" class="status-block" />
<div v-if="canControlSimpleLite" class="status-block sl-control">
<div class="sl-control-title">SimpleLite 操作</div>
<div v-if="canControlSimple3" class="status-block sl-control">
<div class="sl-control-title">Simple3 操作</div>
<div class="sl-control-actions">
<el-button
type="warning"
:loading="actionLoading === 'restart'"
:disabled="!!actionLoading"
@click="onRestart">
重启 SimpleLite
重启 Simple3
</el-button>
<el-button
type="danger"
@@ -59,10 +59,10 @@
:loading="actionLoading === 'stop'"
:disabled="!!actionLoading"
@click="onStop">
关闭 SimpleLite
关闭 Simple3
</el-button>
</div>
<p class="sl-control-hint">将终止本机全部 SimpleLite 进程重启后按当前启动模式重新拉起</p>
<p class="sl-control-hint">将终止本机全部 Simple3 进程重启后按当前启动模式重新拉起</p>
</div>
<div class="status-actions">
@@ -79,12 +79,11 @@ import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getHealth,
getSimpleLiteDiagnostics,
restartSimpleLite,
resolveRestartLaunchMode,
stopSimpleLite,
getSimple3Diagnostics,
restartSimple3,
stopSimple3,
type HealthInfo,
type SimpleLiteDiagnostics
type Simple3Diagnostics
} from '@/api/health'
import { useAuthStore } from '@/stores/auth'
@@ -94,10 +93,10 @@ const auth = useAuthStore()
const loading = ref(false)
const actionLoading = ref<'stop' | 'restart' | null>(null)
const health = ref<HealthInfo | null>(null)
const sl = ref<SimpleLiteDiagnostics | null>(null)
const sl = ref<Simple3Diagnostics | null>(null)
const error = ref('')
const canControlSimpleLite = computed(() => auth.token && auth.scope === 'Platform')
const canControlSimple3 = computed(() => auth.token && auth.scope === 'Platform')
let timer: number | undefined
@@ -132,16 +131,16 @@ async function refresh() {
loading.value = true
error.value = ''
try {
// /status public /health simplelite
// /status public /health simple3
// 401
const [h, d] = await Promise.allSettled([
getHealth(),
auth.token ? getSimpleLiteDiagnostics() : Promise.reject(new Error('skipped'))
auth.token ? getSimple3Diagnostics() : Promise.reject(new Error('skipped'))
])
health.value = h.status === 'fulfilled' ? h.value.data : null
sl.value = d.status === 'fulfilled' ? d.value.data : null
if (h.status === 'rejected') error.value = '无法连接 MiGu.Server/api/health'
else if (auth.token && d.status === 'rejected') error.value = 'SimpleLite 诊断获取失败(/api/health/simplelite'
else if (auth.token && d.status === 'rejected') error.value = 'Simple3 诊断获取失败(/api/health/simple3'
} finally {
loading.value = false
}
@@ -150,8 +149,8 @@ async function refresh() {
async function onStop() {
try {
await ElMessageBox.confirm(
'将终止本机全部 SimpleLite 进程,地图监控与 /api/sl/* 功能将不可用,直到重新登录或手动重启。',
'关闭 SimpleLite',
'将终止本机全部 Simple3 进程,地图监控与 /api/sl/* 功能将不可用,直到重新登录或手动重启。',
'关闭 Simple3',
{ type: 'warning', confirmButtonText: '关闭', cancelButtonText: '取消' }
)
} catch {
@@ -160,24 +159,21 @@ async function onStop() {
actionLoading.value = 'stop'
try {
const { data } = await stopSimpleLite()
const { data } = await stopSimple3()
sl.value = data.diagnostics
ElMessage.success(data.killed > 0 ? `已关闭 ${data.killed} 个 SimpleLite 进程` : '当前没有运行中的 SimpleLite 进程')
ElMessage.success(data.killed > 0 ? `已关闭 ${data.killed} 个 Simple3 进程` : '当前没有运行中的 Simple3 进程')
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '关闭 SimpleLite 失败')
ElMessage.error(e instanceof Error ? e.message : '关闭 Simple3 失败')
} finally {
actionLoading.value = null
}
}
async function onRestart() {
const launchMode = resolveRestartLaunchMode(sl.value?.lastLaunchMode, auth.runMode)
const modeLabel = launchMode === 'WebOnly' ? '仅 Web' : '本地 + Web'
try {
await ElMessageBox.confirm(
`将关闭本机全部 SimpleLite 并按「${modeLabel}模式重新拉起,期间 /api/sl/* 可能短暂不可用。`,
'重启 SimpleLite',
'将关闭本机全部 Simple3 并以 Web 模式重新拉起,期间 /api/sl/* 可能短暂不可用。',
'重启内核',
{ type: 'warning', confirmButtonText: '重启', cancelButtonText: '取消' }
)
} catch {
@@ -186,14 +182,14 @@ async function onRestart() {
actionLoading.value = 'restart'
try {
const { data } = await restartSimpleLite(launchMode)
const { data } = await restartSimple3('WebOnly')
sl.value = data.diagnostics
const r = data.restart
if (r.warning) ElMessage.warning(r.warning)
else if (r.started) ElMessage.success('SimpleLite 已重新拉起')
else if (r.started) ElMessage.success('内核已重新拉起')
else ElMessage.error(`重启未完成:${r.status}`)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '重启 SimpleLite 失败')
ElMessage.error(e instanceof Error ? e.message : '重启失败')
} finally {
actionLoading.value = null
void refresh()
@@ -202,7 +198,7 @@ async function onRestart() {
onMounted(() => {
void refresh()
// 10s goto-site 60s SimpleLite
// 10s goto-site 60s Simple3
timer = window.setInterval(() => void refresh(), 10_000)
})
@@ -34,7 +34,7 @@
show-icon
:closable="false"
class="am-offline"
title="SimpleLite 未连接:以下为平台最近一次采集的报警快照。"
title="Simple3 未连接:以下为平台最近一次采集的报警快照。"
:description="lastSyncAt ? `最近同步:${formatTime(lastSyncAt)}` : '暂无同步记录'"
/>
@@ -84,7 +84,7 @@
</el-collapse-item>
</el-collapse>
<el-empty v-if="!loading && types.length === 0" description="未发现任何 Car 子类(请确认 SimpleLite 已加载相应插件 dll" />
<el-empty v-if="!loading && types.length === 0" description="未发现任何 Car 子类(请确认 Simple3 已加载相应插件 dll" />
</el-card>
</div>
</template>
@@ -161,7 +161,7 @@ async function refresh() {
loading.value = true
try {
// N+2car-types alarm-colors await
// SimpleLite / YARP 502 / null payload
// Simple3 / YARP 502 / null payload
const carTypesResp = await reflectionApi.getCarStyleTypes().catch((err) => {
console.warn('[CarStyleEditor] getCarStyleTypes failed:', err)
return null
@@ -196,7 +196,7 @@ async function refresh() {
}
if (!carTypesResp || !paletteResp) {
ElMessage.warning('车型样式 / 报警颜色加载部分失败,请确认 SimpleLite 已启动(端口 8222)。')
ElMessage.warning('车型样式 / 报警颜色加载部分失败,请确认 Simple3 已启动(端口 8222)。')
}
} catch (err) {
ElMessage.error(`加载车型样式失败:${(err as Error).message}`)
@@ -74,7 +74,7 @@
</div>
<p class="cse-model-hint">
现版本仅记录文件路径作为元信息3D 渲染替代矩形将在后续阶段实现CycleGUI Workspace LoadModel
如需上传文件到服务器请先用其它方式将模型放到 SimpleLite 可访问的相对路径
如需上传文件到服务器请先用其它方式将模型放到 Simple3 可访问的相对路径
</p>
</el-form-item>
@@ -260,7 +260,7 @@ import { fetchMonitorConfigCached, invalidateMonitorConfigCache } from '@/utils/
import type { MapFocusKind } from '@/utils/mapObjectFocus'
import { defaultVrHost } from '@/utils/vrender'
defineProps<{
/** 只读模式(运营端复用 MapMonitorView 时传 true):3D 不可编辑,选中信息面板动作改用运维白名单。 */
/** 只读:3D 不可编辑,站点/路径编辑区隐藏。车辆动作一律走运维网关并记审计。 */
readOnly?: boolean
}>()
@@ -513,7 +513,7 @@ function onPick(_p: { x: number; y: number }) {
// Workspace3D pick SSE selection-detail / select
}
// SimpleLite 3D SimpleUI.cs / UISite.cs / TrackUiHelper
// Simple3 3D SimpleUI.cs / UISite.cs / TrackUiHelper
// - `Car-{id}` `UICar-{id}`
// - `UISite-{id}`
// - `UITrack-{id}` / `UICircularArcTrack-{id}` / `UIBezierTrack-{id}` / `UINurbsTrack-{id}`
@@ -596,7 +596,7 @@ function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, na
}
/**
* SimpleLite 画布点选后通过 SSE `selection-detail` 广播 SimpleUI.BroadcastSelectionToWeb
* Simple3 画布点选后通过 SSE `selection-detail` 广播 SimpleUI.BroadcastSelectionToWeb
* embed 模式通常不会 postMessage `workspace.select`因此必须在此同步侧栏
*/
function onSelectionDetailFromStream(e: SelectionDetailEvent) {
@@ -619,10 +619,14 @@ function onSelectionDetailFromStream(e: SelectionDetailEvent) {
}
}
if (e.kind && e.id) {
if (e.kind === 'site' || e.kind === 'track' || e.kind === 'car') {
applySelectionFromKindId(e.kind, e.id, e.names)
return
}
if (e.kind === 'special' && e.id) {
const next = { kind: 'special', id: String(e.id), name: `装饰物 ${e.id}` }
if (!sameSelection(selection.value, next)) selection.value = next
}
if (siteN > 0) {
applySelectionFromKindId('site', e.siteIds![0]!, e.names)
@@ -727,7 +731,7 @@ async function applyPendingPlayback() {
if (!fileName || !workspaceReady.value) return
pendingPlayback.value = null
try {
// iframe SimpleLite StartPlayback webVRender
// iframe Simple3 StartPlayback webVRender
await workspaceToolbarApi.startPlayback(fileName)
ElMessage.success({ message: `已开始回放:${fileName}`, grouping: true })
} catch (err) {
@@ -759,7 +763,7 @@ async function fallbackSelectionFromBackend() {
}
/**
* 选中并定位到指定车辆 同步 SimpleLite 3D 场景高亮 立即把相机定位居中 + 2D 俯视
* 选中并定位到指定车辆 同步 Simple3 3D 场景高亮 立即把相机定位居中 + 2D 俯视
* 到该车与原生双击车辆行 = 选中+定位一致两个请求互不阻塞各自独立提示错误
* 例如 iframe 未连接时相机定位会 503但高亮仍可成功
*/
@@ -872,10 +876,10 @@ async function refreshAll() {
// - snapshot-tick 1Hz cars/missions/sites/tracks ****
// refreshAll"1s listCars+listMissions" 3s
// snapshot-tick `stream.connected`
// - selection-detail SimpleLite 广embed
// - selection-detail Simple3 广embed
// - object-* / pick-result / project-*
//
// SimpleLite / 线退 3s
// Simple3 / 线退 3s
const stream = useProjectionStream({ autoConnect: true })
const streamConnected = computed(() => stream.connected.value)
@@ -17,7 +17,7 @@
* UiDiscoveryCache.MissionCreatableTypes 同源这里直接用 ReflectionManagerPanel
* 接入按下新建任务会列出所有 MissionType 子类 taskflow / trigger
*
* 旧版本的 DataTablePro + listMissions mock 已经下线所有数据走 SimpleLite 反射 API
* 旧版本的 DataTablePro + listMissions mock 已经下线所有数据走 Simple3 反射 API
*/
import ReflectionManagerPanel from '@/components/reflection/ReflectionManagerPanel.vue'
</script>
@@ -6,7 +6,7 @@
v-if="activeTab === 'project'"
ref="projectEditor"
target-label="项目属性"
description="对应 SimpleLite 桌面端主菜单「项目属性…」。编辑字段后点「保存」即可一次完成内存生效 + 写回项目 JSON 文件(含全部场景/车辆/路径/属性)。"
description="对应 Simple3 桌面端主菜单「项目属性…」。编辑字段后点「保存」即可一次完成内存生效 + 写回项目 JSON 文件(含全部场景/车辆/路径/属性)。"
:loader="loadProject"
:on-set="setProjectField"
:on-save="saveProject"
@@ -18,7 +18,7 @@
v-if="activeTab === 'app-config'"
ref="appConfigEditor"
target-label="核心配置"
description="对应 SimpleLite 桌面端主菜单「核心配置 → 编辑配置…」。修改后点「保存」即可一次完成内存生效 + 写入可执行目录下的 simple.json。"
description="对应 Simple3 桌面端主菜单「核心配置 → 编辑配置…」。修改后点「保存」即可一次完成内存生效 + 写入可执行目录下的 simple.json。"
:loader="loadAppConfig"
:on-set="setAppConfigField"
:on-save="saveAppConfig"
@@ -48,7 +48,7 @@ async function loadProject(): Promise<FieldMemberEditorLoadResult> {
return {
fields: r.fields,
pathInfos: [
{ label: '最近加载/保存的项目 JSON', value: r.lastLoadedPath, emptyHint: '尚未通过 SimpleLite 加载或保存过项目' },
{ label: '最近加载/保存的项目 JSON', value: r.lastLoadedPath, emptyHint: '尚未通过 Simple3 加载或保存过项目' },
{ label: 'simple.json autoload', value: r.autoloadPath, emptyHint: '未配置' }
],
saveButtonLabel: '保存(写回项目 JSON'
@@ -1,10 +1,10 @@
<template>
<div class="script-page">
<!--
平台脚本管理= SimpleLite 工作台脚本ComposerDockPanel.RenderScriptTable Web 镜像
平台脚本管理= Simple3 工作台脚本ComposerDockPanel.RenderScriptTable Web 镜像
- 列表 = CarProgram.GetPrograms()最多 300 含历史 Mission 调度运行时自动产生与回收
- ID / 名称(任务名) / 类型(CarProgram 子类) / 车辆(plans[0].usingCar) / 状态(ProgramStatus.state)
- 不渲染新建按钮CarProgram 无法手动 new Mission 调度运行时自动产生 SimpleLite 工作台新建 Mission 后生成
- 不渲染新建按钮CarProgram 无法手动 new Mission 调度运行时自动产生 Simple3 工作台新建 Mission 后生成
- 不渲染删除按钮CarProgram 由调度器在环形队列里维护删除一律会被后端拒
右侧详情面板沿用通用 ReflectionManagerPanel 渲染fields 多为 typed/locked只读 status 通过反射
-->
@@ -12,7 +12,7 @@
kind="script"
kind-label="脚本"
title="脚本管理"
empty-text="当前无 CarProgram 实例。CarProgram 是 Mission 运行时编译用户脚本后产生的对象,请在 SimpleLite 工作台新建 Mission(任务)后由调度运行时自动生成。"
empty-text="当前无 CarProgram 实例。CarProgram 是 Mission 运行时编译用户脚本后产生的对象,请在 Simple3 工作台新建 Mission(任务)后由调度运行时自动生成。"
disable-create
disable-delete
show-summary-column
@@ -5,7 +5,7 @@
<div class="page-header">
<div>
<h2>字段管理</h2>
<p>默认 SimpleLite 拉取全部车型字段刷新从数据库读取保存将全部字段写入数据库</p>
<p>默认 Simple3 拉取全部车型字段刷新从数据库读取保存将全部字段写入数据库</p>
</div>
<div class="header-actions">
<el-button :loading="initializing || loadingDefaults" @click="loadDefaults">默认</el-button>
@@ -339,7 +339,7 @@ function allCarsToRows(cars: CarTypeCoderFieldsRow[]): FieldRow[] {
return cars.flatMap(carTypeToRows)
}
/** 从已加载的数据库记录推导车型下拉(不请求 SimpleLite)。 */
/** 从已加载的数据库记录推导车型下拉(不请求 Simple3)。 */
function syncCarTypesFromFieldRows() {
const labelByKey = new Map(
carTypes.value.map((c) => [buildCarType(c.assemblyName, c.shortName), c.label])
@@ -363,7 +363,7 @@ function syncCarTypesFromFieldRows() {
carTypes.value = options
}
/** 从 SimpleLite 补全车型中文名(轻量接口,不拉字段定义)。 */
/** 从 Simple3 补全车型中文名(轻量接口,不拉字段定义)。 */
async function enrichCarTypeLabels() {
try {
const types = await reflectionApi.listCreatableTypes('car')
@@ -376,7 +376,7 @@ async function enrichCarTypeLabels() {
return label ? { ...c, label } : c
})
} catch {
/* SimpleLite 未连接时保留现有显示 */
/* Simple3 未连接时保留现有显示 */
}
}
@@ -423,7 +423,7 @@ async function loadDefaults() {
try {
const cars = await reflectionApi.getCarTypeCoderFields()
if (!cars.length) {
ElMessage.warning('未获取到车型列表,请确认 SimpleLite 已启动')
ElMessage.warning('未获取到车型列表,请确认 Simple3 已启动')
return
}
carTypes.value = cars.map((c) => ({
@@ -434,7 +434,7 @@ async function loadDefaults() {
}))
ensureDefaultCarType()
fieldRows.value = allCarsToRows(cars)
ElMessage.success(`已从 SimpleLite 加载 ${cars.length} 种车型、共 ${fieldRows.value.length} 条默认字段(请点击保存写入数据库)`)
ElMessage.success(`已从 Simple3 加载 ${cars.length} 种车型、共 ${fieldRows.value.length} 条默认字段(请点击保存写入数据库)`)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '加载默认字段失败')
} finally {
@@ -43,7 +43,7 @@
show-icon
:closable="false"
class="tm-offline"
title="SimpleLite 未连接:以下为平台最近一次快照;取消/重发/新建等操作需 SimpleLite 在线,离线执行会提示失败。"
title="Simple3 未连接:以下为平台最近一次快照;取消/重发/新建等操作需 Simple3 在线,离线执行会提示失败。"
:description="lastSyncAt ? `最近同步:${formatTime(lastSyncAt)}` : '暂无同步记录'"
/>
@@ -17,7 +17,7 @@
<p class="mmc-desc">
勾选地图监控右侧选中信息面板要展示的内容白名单按对象类型 - 车辆 / 站点 / 路径 分组
三组分别控制 <b>属性</b><b>状态</b><b>动作</b> Tab 里可见的字段 / 状态项 / 可执行方法
每组留空 = 显示全部勾选任何一项即进入白名单模式仅显示已勾选保存后直接写入 SimpleLite
每组留空 = 显示全部勾选任何一项即进入白名单模式仅显示已勾选保存后直接写入 Simple3
<code>simple.json</code> <code>Configuration.conf.monitorVisibility</code>
</p>
@@ -28,7 +28,7 @@
title="属性字段(Properties"
:available="available[k.value].fields"
:selected="config[k.value].fields"
empty-tip="该类型暂未发现任何 [FieldMember]。在 SimpleLite 端添加字段后点重载会刷新。"
empty-tip="该类型暂未发现任何 [FieldMember]。在 Simple3 端添加字段后点重载会刷新。"
@update:selected="(v: string[]) => onUpdate(k.value, 'fields', v)" />
<MapMonitorConfigGroup
@@ -51,7 +51,7 @@
<el-option v-for="s in displayedSites" :key="s.id" :label="s.label" :value="s.id" />
</el-select>
<span v-if="sitesLoadError" class="field-hint field-hint-warn">{{ sitesLoadError }}</span>
<span v-else class="field-hint">主数据来自 SimpleLite保存值为站点 id</span>
<span v-else class="field-hint">主数据来自 Simple3保存值为站点 id</span>
</el-form-item>
<el-form-item label="条码"><el-input v-model="storageForm.barcode" /></el-form-item>
<el-form-item label="优先级"><el-input-number v-model="storageForm.priority" :min="0" /></el-form-item>
@@ -155,7 +155,7 @@ import {
AREA_TYPES, AREA_LAYOUT_MODES, STORAGE_TYPES, LOCATION_KINDS, CONTAINER_TYPES,
CONTAINER_LOCATION_STATUSES, MATERIAL_UNITS, MATERIAL_LIFECYCLES
} from './wmsOptions'
import { useSimpleLiteSites } from './useSimpleLiteSites'
import { useSimple3Sites } from './useSimple3Sites'
export type DialogKind = 'area' | 'storage' | 'container' | 'materialType' | 'material' | 'location' | 'containerMaterial' | 'rule'
@@ -186,7 +186,7 @@ const emit = defineEmits<{
const formRef = ref<FormInstance>()
const areaLayoutMode = defineModel<string>('areaLayoutMode', { default: 'Flat' })
const containerBarcode = defineModel<string>('containerBarcode', { default: '' })
const { sites: simpleLiteSites, loading: sitesLoading, loadError: sitesLoadError, load: loadSimpleLiteSites } = useSimpleLiteSites()
const { sites: simple3Sites, loading: sitesLoading, loadError: sitesLoadError, load: loadSimple3Sites } = useSimple3Sites()
const siteSearch = ref('')
function onSiteChange(id: string) {
@@ -203,10 +203,10 @@ function onMaterialTypeChange(code: string) {
}
const siteOptionsWithCurrent = computed(() => {
const opts = [...simpleLiteSites.value]
const opts = [...simple3Sites.value]
const current = props.storageForm.siteId?.trim()
if (current && !opts.some((o) => o.id === current)) {
opts.unshift({ id: current, name: '', label: `#${current}(未在 SimpleLite 列表中)` })
opts.unshift({ id: current, name: '', label: `#${current}(未在 Simple3 列表中)` })
}
return opts
})
@@ -289,7 +289,7 @@ watch(() => props.visible, (v) => {
formRef.value?.clearValidate()
if (props.kind === 'storage') {
siteSearch.value = ''
void loadSimpleLiteSites()
void loadSimple3Sites()
}
}
})
@@ -2,13 +2,13 @@ import { ref } from 'vue'
import { reflectionApi } from '@/api/reflection'
import { listSites } from '@/api/projection'
export interface SimpleLiteSiteOption {
export interface Simple3SiteOption {
id: string
name: string
label: string
}
export async function fetchSimpleLiteSites(): Promise<SimpleLiteSiteOption[]> {
export async function fetchSimple3Sites(): Promise<Simple3SiteOption[]> {
try {
const rows = await reflectionApi.listObjects('site')
if (rows.length > 0) {
@@ -33,8 +33,8 @@ export async function fetchSimpleLiteSites(): Promise<SimpleLiteSiteOption[]> {
.sort((a, b) => Number(a.id) - Number(b.id) || a.name.localeCompare(b.name))
}
export function useSimpleLiteSites() {
const sites = ref<SimpleLiteSiteOption[]>([])
export function useSimple3Sites() {
const sites = ref<Simple3SiteOption[]>([])
const loading = ref(false)
const loadError = ref('')
@@ -42,9 +42,9 @@ export function useSimpleLiteSites() {
loading.value = true
loadError.value = ''
try {
sites.value = await fetchSimpleLiteSites()
sites.value = await fetchSimple3Sites()
} catch (e) {
loadError.value = e instanceof Error ? e.message : '加载 SimpleLite 站点失败'
loadError.value = e instanceof Error ? e.message : '加载 Simple3 站点失败'
sites.value = []
} finally {
loading.value = false
@@ -9,11 +9,10 @@
* 只读模式下
* - 3D 画布不可编辑Workspace3D read-onlyscope MapMonitorView 内部按 auth.scope
* 运营端登录后即 RCSMonitor
* - 右侧选中信息面板的车辆动作改用运维白名单OPS_WHITELIST + executeOp 审计 + 二次确认
* 按当前账号 hasOp 过滤站点/路径的编辑动作区隐藏
* - 右侧选中信息车辆动作与管理端相同monitor-config 按车型勾选 /api/sl/ops/execute
* ops.car.execute转发 Simple3站点/路径的编辑动作区隐藏
*
* 备注原运营端地图页内的任务白名单动作暂停/取消/重派等请走 /monitor/ops 运维操作页
* 地图监控页聚焦于地图对象//的监视与车辆运维动作
* 备注任务白名单动作暂停/取消/重派等请走 /monitor/ops 运维操作页
*/
import MapMonitorView from '@/views/admin/MapMonitorView.vue'
</script>
@@ -115,7 +115,7 @@
<FleetAllocationPanel :cars="cardModels" :can-write="canWrite" @saved="onFleetSaved" />
<p class="footnote">
延迟 = 本机到车辆 WatchDog(:9776) TCP 往返故障率 = 报警占用时长 ÷ 自上线以来运行时长SimpleLite 进程内累计
延迟 = 本机到车辆 WatchDog(:9776) TCP 往返故障率 = 报警占用时长 ÷ 自上线以来运行时长Simple3 进程内累计
</p>
</div>
</el-tab-pane>
@@ -32,7 +32,7 @@ export function assignCarParams(): ParamFieldDef[] {
label: '车辆类型',
type: 'string',
required: true,
placeholder: '如 Forklift / SimpleLite.RCS.Cars.AgvCar',
placeholder: '如 Forklift / Simple3.RCS.Cars.AgvCar',
when: BY_CAR_TYPE
}
]
+29
View File
@@ -0,0 +1,29 @@
# 将最新编译的 Simple3 部署到 bin\Debug 并提示重启。
# 用法:先关闭 Simple3(任务管理器或托盘退出),再运行本脚本。
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$slProj = Join-Path $repoRoot 'Simple\Simple3\Simple3.csproj'
$outDir = Join-Path $repoRoot 'Simple\Simple3\bin\Debug'
$objDll = Join-Path $repoRoot 'Simple\Simple3\obj\Debug\Simple3.dll'
Write-Host '==> 编译 Simple3 (DisableFody)...' -ForegroundColor Cyan
dotnet build $slProj -c Debug -p:DisableFody=true
if ($LASTEXITCODE -ne 0) { throw "dotnet build 失败 (exit $LASTEXITCODE)" }
$builtDll = if (Test-Path $objDll) { $objDll } else { Join-Path $outDir 'Simple3.dll' }
if ((Get-Process Simple3 -ErrorAction SilentlyContinue) -or (Get-Process SimpleLite -ErrorAction SilentlyContinue)) {
Write-Host ''
Write-Host 'Simple3 仍在运行,无法覆盖 DLL。请先关闭 Simple3.exe,再重新运行本脚本。' -ForegroundColor Yellow
exit 1
}
Copy-Item $builtDll (Join-Path $outDir 'Simple3.dll') -Force
$objExe = Join-Path $repoRoot 'Simple\Simple3\obj\Debug\Simple3.exe'
if (Test-Path $objExe) {
Copy-Item $objExe (Join-Path $outDir 'Simple3.exe') -Force
}
Write-Host '==> 已更新' $outDir -ForegroundColor Green
Write-Host '请重新登录迷毂平台(或手动启动 Simple3.exe)以加载新 API。'
-29
View File
@@ -1,29 +0,0 @@
# 将最新编译的 SimpleLite 部署到 bin\Debug 并提示重启。
# 用法:先关闭 SimpleLite(任务管理器或托盘退出),再运行本脚本。
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$slProj = Join-Path $repoRoot 'Simple\SimpleLite\SimpleLite.csproj'
$outDir = Join-Path $repoRoot 'Simple\SimpleLite\bin\Debug'
$objDll = Join-Path $repoRoot 'Simple\SimpleLite\obj\Debug\SimpleLite.dll'
Write-Host '==> 编译 SimpleLite (DisableFody)...' -ForegroundColor Cyan
dotnet build $slProj -c Debug -p:DisableFody=true
if ($LASTEXITCODE -ne 0) { throw "dotnet build 失败 (exit $LASTEXITCODE)" }
$builtDll = if (Test-Path $objDll) { $objDll } else { Join-Path $outDir 'SimpleLite.dll' }
if (Get-Process SimpleLite -ErrorAction SilentlyContinue) {
Write-Host ''
Write-Host 'SimpleLite 仍在运行,无法覆盖 DLL。请先关闭 SimpleLite.exe,再重新运行本脚本。' -ForegroundColor Yellow
exit 1
}
Copy-Item $builtDll (Join-Path $outDir 'SimpleLite.dll') -Force
$objExe = Join-Path $repoRoot 'Simple\SimpleLite\obj\Debug\SimpleLite.exe'
if (Test-Path $objExe) {
Copy-Item $objExe (Join-Path $outDir 'SimpleLite.exe') -Force
}
Write-Host '==> 已更新' $outDir -ForegroundColor Green
Write-Host '请重新登录迷榖平台(或手动启动 SimpleLite.exe)以加载新 API。'