# 平台 v3 重构 — 阶段 0~6 实际改动

> 范围：把 SimpleLite 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 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 平台化（已完成）

### 新增文件

| 文件 | 作用 |
|------|------|
| `SimpleLite/Web/MapEditApiController.cs` | 写操作 8 路 API：create/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 API；listKinds / 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 |

### 修改文件

| 文件 | 关键改动 |
|------|----------|
| `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） |

### 关键 API 路径

```
POST  /projection/map-edit/objects/{kind}          # site|track|bezier|arc|nurbs|image|text|model
DELETE /projection/map-edit/objects/{kind}/{id}
POST  /projection/map-edit/objects/batch           # create/delete/patch 混合
POST  /projection/map-edit/objects/{kind}/{id}/fields/copy-to
POST  /projection/map-edit/pick                    # 拾取会话
GET   /projection/map-edit/dashboard/summary       # KPI 聚合
POST  /projection/map-edit/assets/upload           # 图片/模型 base64
POST  /projection/map-edit/ai/map-generate         # 真实 LLM 生图

# 阶段 6 新增（本会话）
POST  /projection/map-edit/project/save            # body: { path?: string }
POST  /projection/map-edit/project/load            # body: { path: string }
GET   /projection/map-edit/project/current
POST  /projection/map-edit/objects/track/{id}/sample-sites
                                                   # body: { spacingMm, includeEndpoints?, layer? }

GET   /projection/ai-config                        # 读 AI 服务配置
POST  /projection/ai-config                        # 写 AI 服务配置

GET   /projection/reflection/kinds | objects/{kind} | methods/{kind}/{id} | ...
SSE   /projection/stream                            # 所有 SSE 事件
```

## 阶段 1：MapEditorView 四栏壳（已完成）

`frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue` 完整重写为：

```
┌─────────────────────────────────────────────────────┐
│ EditTopBar：项目 / 文件 / 编辑 / 视图 / 图层 / 对齐吸附 │
│             + 撤销/重做 + AI 生图 + 保存            │
├──────┬─────────────────────────┬───────────────────┤
│ Edit │   Workspace3D           │ EditPropertyPanel │
│ Tool │  (canvas-only iframe)   │ 对象/类型默认/图层 │
│ Rail │                         │                   │
├──────┴─────────────────────────┴───────────────────┤
│ EditStatusBar：选中 / 鼠标坐标 / 工具 / 吸附 / undo │
└─────────────────────────────────────────────────────┘
```

### 新增组件

- `components/map-editor/EditTopBar.vue` —— 6 个 dropdown 菜单（项目/文件/编辑/视图/图层/对齐吸附）+ 撤销重做按钮 + AI 生图入口 + 保存
- `components/map-editor/EditToolRail.vue` —— 6 组 CAD 工具按钮 + 吸附组（网格/对象/端点/中点 + 网格步长）
- `components/map-editor/EditPropertyPanel.vue` —— 3 Tab（对象 / 类型默认 / 图层），自动反射出 `getFields`，每行可改可保存；多选时统一字段写入；不一致字段显示「-」；执行 reflectionApi.execute 调任何 MethodMember
- `components/map-editor/EditStatusBar.vue` —— 状态栏
- `components/map-editor/AiGenerateDialog.vue` —— AI 生图对话框

### 反射 API 接入

- `api/mapEdit.ts` —— MapEditApiController 全 8+4 路 API 胶水（本会话补的 4 个：projectSave / projectLoad / projectCurrent / sampleSitesAlongTrack）
- `api/reflection.ts` —— ReflectionApiController 全套胶水（含 MOCK 离线 fallback）

## 阶段 2：对齐 / 批量生成 / 复制字段 / 类型默认 / 图层（已完成）

- `composables/useAlignment.ts` —— 11 种对齐模式：left/right/top/bottom/centerH/centerV/center/distributeH/distributeV/toFirst/toLast
- `composables/useBatchGenerate.ts` —— 横向/纵向/矩阵/环形 4 种批量生成器，输出 BatchOp[]
- `composables/useClipboard.ts` —— 对象/字段剪贴板
- `transform.copyFields` 工具：MapEditorView `runCopyFields` 读取多选（第 1 个为源，其余目标），prompt 字段名列表，调 `mapEditApi.copyFieldsTo`；撤销时用前置 patch 回滚
- 类型默认 Tab：站点/路径默认 layer/前缀/颜色/线宽，存 localStorage `mapEditor.defaults`
- 图层 Tab：图层名列表 + 可见 / 可选 / 颜色，本地状态

## 阶段 3：AI 生图 + 状态栏 + 项目保存/加载（已完成）

- AiGenerateDialog 通过 `mapEditApi.aiMapGenerate` 真实调用 LLM，后端走 `AiCompletionService.GenerateMapAsync`：用 OpenAI Function Calling 让 LLM 直接调 create_site / create_track / batch_create 等"工具"，落地到 SimpleLib
- 入口：EditTopBar 「AI 生图」按钮 → 弹对话框，参数：prompt / mode (sites/tracks/sites+tracks/full) / bounds / referenceImageBase64 / layer / constraints
- 项目保存/加载：EditTopBar「保存」/「另存为」/「加载项目」→ prompt 输入路径 → 调 `mapEditApi.projectSave/Load` → 服务端走 `SimpleProject.ToFile / FromFile`；最近路径缓存到 localStorage `mapEditor.lastProjectPath`

## 阶段 4：MapMonitor 浮动报警卡 + KPI + 实时列表（已完成）

`frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue` 改动：

- 删除原 `AlarmTicker.vue` 底部滚动条
- 新增 `components/map-monitor/FloatingAlarmStack.vue` + `FloatingAlarmCard.vue`，可拖动 / 可吸边 / 可堆叠
- 报警来源走 `useMapEditStream.onAlarm`（订阅 SSE 的 `alarm` 事件，由 AlarmStreamService 1s diff 推送）
- KPI 3 卡：在线车辆 / 运行中任务 / 排队任务
- 实时列表：左侧 Workspace3D + 右侧 VehicleMonitorPanel + SelectionDetailPanel
- 「定位」按钮通过 `reflectionApi.setSelection('car', id)` 让 SimpleLite 3D 高亮该车（视觉上等效 flyTo）

## 阶段 5：OrchestrationView 设计与编排（已完成）

`views/admin/OrchestrationView.vue` 5-Tab 壳：

| Tab | 来源 |
|-----|------|
| 车辆 | 复用现有 `CarPanelView` |
| 进程 | 复用现有 `MissionEditorView` |
| 脚本 | `components/orchestration/ReflectionKindTable.vue`（kind=script）：动态列表 + 反射 method 执行 |
| 任务流 | ReflectionKindTable（kind=mission, extraFilter='taskflow'）|
| 触发器 | ReflectionKindTable（kind=mission, extraFilter='trigger'）|

Tab 状态缓存到 sessionStorage。

## 阶段 6：联调 / 撤销栈做实 / 文档（已完成）

### 撤销栈做实

`composables/useHistory.ts` 提供 `run/undo/redo/composite`，每个命令 `apply/revert` 双向。本次把 MapEditorView 里所有占位 revert 改成真实回滚：

| 命令 | 原 revert | 现 revert |
|------|-----------|-----------|
| `runCreateSite` | delete 创建的 id | （未变）|
| `handleTrackPick` 创建路径 | delete 创建的 id | （未变）|
| `deleteSelection` | ⚠️ 仅本地撤销 | **getBundle 抓快照 → batch.create 重建（id 重排）** |
| `onSetField` 字段修改 | ⚠️ 不回滚 | **getBundle 抓原值 → setField 回去** |
| `applyAlignment` | （已用 patch 反向恢复） | （未变）|
| `applyBatchAsCommand` | （已用 delete 反向移除） | （未变）|
| `runAlongPathSampling`（新增）| —— | delete 所有采样产生的 site id |
| `runCopyFields`（新增）| —— | patch 反向把 targets 字段值恢复 |

### 沿路径采样

- 后端：`POST /projection/map-edit/objects/track/{id}/sample-sites { spacingMm, includeEndpoints?, layer? }` —— 当前在 siteA → siteB 之间欧式直线均分（贝塞尔/弧的真实参数化采样留待后续 Track 子类 PR）
- 前端：选中 1+ 条 track，工具栏「沿路径」→ prompt 间距和端点开关 → 调后端，撤销可回退

### 编译相关清理

- `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;` → 补
- `HttpClientHelper.csproj` LangVersion 7.3 → latest

## 已知 TODO（不阻塞 v3 上线）

1. 贝塞尔/弧/Nurbs 路径的真实参数化采样（当前直线均分）
2. 撤销栈跨 Tab / 跨 session 共享（当前仅 MapEditor 视图局部）
3. Workspace3D 真正的 postMessage 'workspace.flyTo' 双向桥（当前用 setSelection 代替）
4. 复制字段 UI 支持类型化字段挑选器（当前 prompt 输入字段名）
5. AI 生图 OAuth/键持久化加密（当前明文存 config）
6. Recording 录制/回放模块未完工（PlaybackController 已修通编译，UI 已挂底栏，但实际录制/回放业务逻辑需要后续单独 PR 完成）

## 启动方式

```
# 1. 后端 build（SimpleLite + MiGu.Server）
dotnet build SimpleLite\SimpleLite.csproj
dotnet build MiGu.Server\MiGu.Server.csproj

# 2. 前端打包并同步到 wwwroot
.\build-platform-frontend.bat

# 3. 启动平台 + SimpleLite
dotnet run --project MiGu.Server  # 监听 0.0.0.0:8080
# SimpleLite 端口 8222/8223 由 SimpleLite 自己启动

# 4. 访问
http://127.0.0.1:8080/admin/map-editor      # 地图编辑（v3 四栏壳）
http://127.0.0.1:8080/admin/map-monitor     # 地图监控（驾驶舱 + 浮动报警）
http://127.0.0.1:8080/admin/orchestration   # 设计与编排（5 Tab）
```
