feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退

从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-05-29 18:16:34 +08:00
co-authored by Cursor
parent 804aa68ade
commit 42978930ca
280 changed files with 30046 additions and 8 deletions
@@ -0,0 +1,620 @@
import http from './http'
import {
mockReflectionAssemblies,
mockReflectionBundle,
mockReflectionDeleteField,
mockReflectionExecute,
mockReflectionFields,
mockReflectionKinds,
mockReflectionMethods,
mockReflectionMethodsByType,
mockReflectionObjects,
mockReflectionSetField,
mockReflectionStatus
} from '@/mock/data/reflection'
// 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关,避免 PROJECTION/USE 双命名造成
// .env 改一个忘改另一个的诡异半 mock 状态。env.d.ts 已声明 VITE_USE_MOCK 类型。
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
/**
* 与 SimpleLite `ReflectionApiController` 一一对应的前端胶水。
* 路径前缀:`/sl/projection/reflection`(迷榖平台 YARP 反代下游 → SimpleLite EmbedIO)。
*
* 该客户端覆盖了 SimpleLite 主体以及通过 plugins/*.dll 动态加载的 Standard 等
* 插件中所有标注了 `MethodMember` 的方法与所有继承自 Car/Mission/CarProgram/Site/Track
* 的子类型,平台前端可据此动态渲染列表、属性、动作按钮。
*/
const BASE = '/sl/projection/reflection'
export type ReflectionKind =
| 'car'
| 'vehicle'
| 'mission'
| 'process'
| 'site'
| 'track'
| 'map'
| 'special'
| 'script'
// 顶级合成 kind:等于 site + track + special 的并集,由 SimpleLite 后端 /objects/scene 直接返回,
// 每行带 subKind 字段,前端 bundle / execute / setField 走对应底层 kind。
| 'scene'
export interface ReflectionEnvelope<T> {
success: boolean
code: number
data: T | null
message: string
}
export interface ReflectionParam {
name: string
typeName: string
hasDefault: boolean
defaultValue?: string | null
}
export interface ReflectionMethod {
methodName: string
label?: string | null
description?: string | null
hint?: string | null
returnType: string
hasParams: boolean
params: ReflectionParam[]
}
export interface ReflectionObject {
id: number
name: string
typeName: string
layer?: string | null
status?: string | null
summary?: string | null
/** 当顶级 kind 是合成 kind(如 "scene")时,此字段标记底层真实 kind。 */
subKind?: string | null
}
export interface ReflectionKv {
key: string
value: string
locked?: boolean
/** "typed" = 强类型 [FieldMember],不可删;"dynamic" = Prop.fields 动态字段,可删。 */
source?: 'typed' | 'dynamic'
/** 后端字段的 .NET 类型名(String / Single / Int32 / Boolean 等),用于前端渲染对应控件。 */
typeName?: string
}
export interface ReflectionTypeMethods {
typeName: string
fullTypeName?: string
typeLabel?: string
assemblyName: string
methods: ReflectionMethod[]
}
export interface ReflectionAssembly {
name: string
version?: string
location?: string
}
/** 通过 GET /reflection/types/{kind} 拿到的可创建子类型行(进程 / 脚本 / 车辆管理面板的「新建」下拉用)。 */
export interface ReflectionCreatableType {
typeName: string
shortName: string
label: string
assemblyName: string
}
/** GET /reflection/plugins 返回的单个插件元信息。 */
export interface PluginEntry {
name: string
dllPath: string
assemblyName: string
assemblyVersion: string
collectible: boolean
loadedTypes: number
loadedAt: string
missionTypes: number
carTypes: number
}
export interface ReflectionKindMeta {
kind: ReflectionKind
count: number
label: string
/** "primary" 表示顶级 5 分类;undefined / 其他视为底层 kind。 */
group?: string
}
export interface ReflectionSubKindMeta extends ReflectionKindMeta {
parent: ReflectionKind
}
export interface ReflectionSelection {
kind: ReflectionKind | null
id: number
name: string
typeName?: string
}
export interface ScriptSourcePayload {
id: number
name: string
typeName: string
state?: string | null
script: string
}
export interface ScriptExceptionStatusPayload {
id: number
name: string
typeName: string
car: string
state?: string | null
exception: string
notifies: string
report: string
}
// ──────────────────────────────────────────────────────────────────────────
// 地图监控可见性配置(与后端 MonitorVisibilityConfig / MonitorVisibilityForKind 对应)
//
// `config` = 管理员勾选的白名单。空数组 = 显示全部;非空 = 仅显示列表中的 key。
// `available` = 当前 SimpleLite 进程里能扫描到的全部可勾选项(基类 + 已加载子类 +
// 运行时实例的 Prop.fields / status 反射键)。是 GET 返回的副产物,
// POST 写入时不需要、也不该回传。
// ──────────────────────────────────────────────────────────────────────────
export type MonitorVisibilityKind = 'car' | 'site' | 'track'
export interface MonitorVisibilityForKind {
fields: string[]
status: string[]
methods: string[]
}
export interface MonitorVisibilityMap {
car: MonitorVisibilityForKind
site: MonitorVisibilityForKind
track: MonitorVisibilityForKind
}
/** POST /monitor-config 请求体:三组白名单 + 可选按车型动作。 */
export interface MonitorConfigSaveBody extends MonitorVisibilityMap {
carActionByType?: Record<string, string[]>
}
export interface MonitorConfigPayload {
config: MonitorVisibilityMap & { carActionByType?: Record<string, string[]> }
available: MonitorVisibilityMap
}
const MOCK_MONITOR_STORAGE_KEY = 'simple-platform-mock-monitor-config'
function loadMockMonitorConfig(): MonitorConfigSaveBody {
try {
const raw = localStorage.getItem(MOCK_MONITOR_STORAGE_KEY)
if (raw) return JSON.parse(raw) as MonitorConfigSaveBody
} catch { /* ignore */ }
return emptyMonitorVisibilityMap()
}
function saveMockMonitorConfig(cfg: MonitorConfigSaveBody) {
try {
localStorage.setItem(MOCK_MONITOR_STORAGE_KEY, JSON.stringify(cfg))
} catch { /* ignore */ }
}
export function emptyMonitorVisibilityForKind(): MonitorVisibilityForKind {
return { fields: [], status: [], methods: [] }
}
export function emptyMonitorVisibilityMap(): MonitorVisibilityMap {
return {
car: emptyMonitorVisibilityForKind(),
site: emptyMonitorVisibilityForKind(),
track: emptyMonitorVisibilityForKind()
}
}
// 语义上 available 与 config 同构(都是按 kind 分组的三组 key 列表),所以共用一个工厂。
// 单独保留命名是为了表达"业务含义不同"——可勾选全集 vs 已勾选白名单。
export const emptyMonitorAvailableMap = emptyMonitorVisibilityMap
async function get<T>(path: string): Promise<T> {
const { data } = await http.get<ReflectionEnvelope<T>>(`${BASE}${path}`)
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
return data.data as T
}
async function post<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T> {
const { data } = await http.post<ReflectionEnvelope<T>>(`${BASE}${path}`, null, { params })
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
return data.data as T
}
async function del<T>(path: string): Promise<T> {
const { data } = await http.delete<ReflectionEnvelope<T>>(`${BASE}${path}`)
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
return data.data as T
}
export const reflectionApi = {
listKinds: () => MOCK
? Promise.resolve(mockReflectionKinds())
: get<{ kinds: ReflectionKindMeta[]; subKinds?: ReflectionSubKindMeta[] }>('/kinds'),
listAssemblies: () => MOCK
? Promise.resolve(mockReflectionAssemblies())
: get<ReflectionAssembly[]>('/assemblies'),
listObjects: (kind: ReflectionKind) => MOCK
? Promise.resolve(mockReflectionObjects(kind))
: get<ReflectionObject[]>(`/objects/${kind}`),
/** 列出当前可实例化的子类型(用于「新建」下拉);mock 模式直接给空。 */
listCreatableTypes: (kind: ReflectionKind): Promise<ReflectionCreatableType[]> => MOCK
? Promise.resolve([])
: get<ReflectionCreatableType[]>(`/types/${kind}`),
/**
* 创建一条对象。底层 POST `/objects/{kind}?...`,所有 extras 字段都作为 query 传给后端。
*
* 三类典型用法:
* - process/script + UiDiscoveryCache 子类:`createObject('process', 'TaskFlow')`
* - carDummyCar 兜底,typeName 可空):`createObject('car', '', { name: 'AGV-1', x: 0, y: 0 })`
* - site/track/image/text/model`createObject('site', '', { x: 100, y: 200, name: 'A' })`
*
* 后端 BuildAndPersist 会做完整的字段类型转换;前端只负责传字符串。
*/
createObject: (
kind: ReflectionKind,
typeName?: string,
extras?: Record<string, string | number | boolean | undefined>
) => {
if (MOCK) return Promise.resolve({ kind, id: -1, typeName: typeName ?? '' })
const params: Record<string, string | number | boolean> = {}
if (typeName) params.typeName = typeName
if (extras) {
for (const [k, v] of Object.entries(extras)) {
if (v === undefined || v === null || v === '') continue
params[k] = v
}
}
return post<{ kind: string; id: number; typeName: string }>(`/objects/${kind}`, params)
},
/** 删除一条对象(process / script / car / site / track / special)。 */
deleteObject: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve({ kind, id, deleted: true })
: del<{ kind: string; id: number; deleted: boolean }>(`/objects/${kind}/${id}`),
/**
* 触发 SimpleLite 重新扫描 ./plugins 目录、加载新增 dll 并重建 UiDiscoveryCache。
* 已加载的 dll 不会重复加载。
* 返回:本次发现的 dll 总数、新加载的 assembly 数、当前可创建的 mission/car 类型计数。
*/
reloadPlugins: () => MOCK
? Promise.resolve({ totalDlls: 0, newlyLoaded: 0, failed: 0, missionTypes: 0, carTypes: 0 })
: post<{ totalDlls: number; newlyLoaded: number; failed: number; missionTypes: number; carTypes: number }>(
'/plugins/reload'
),
/** 列出当前已加载的所有 collectible 插件(PluginManager 跟踪范围内)。 */
listPlugins: () => MOCK
? Promise.resolve<PluginEntry[]>([])
: get<PluginEntry[]>('/plugins'),
/**
* 卸载一个 collectible 插件。
* 失败原因常见:仍有 Mission / Car 实例占用插件类型 → 409。
*/
unloadPlugin: (name: string) => MOCK
? Promise.resolve({ name, message: 'mock', missionTypes: 0, carTypes: 0 })
: post<{ name: string; message: string; missionTypes: number; carTypes: number }>(
`/plugins/${encodeURIComponent(name)}/unload`
),
listMethods: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionMethods(kind))
: get<ReflectionMethod[]>(`/methods/${kind}/${id}`),
listMethodsByType: (kind: ReflectionKind) => MOCK
? Promise.resolve(mockReflectionMethodsByType(kind))
: get<ReflectionTypeMethods[]>(`/methods-by-type/${kind}`),
getStatus: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionStatus(kind, id))
: get<ReflectionKv[]>(`/status/${kind}/${id}`),
getFields: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionFields(kind, id))
: get<ReflectionKv[]>(`/fields/${kind}/${id}`),
setField: (kind: ReflectionKind, id: number, field: string, value: string) => MOCK
? Promise.resolve(mockReflectionSetField(kind, id, field, value))
: post<{ kind: ReflectionKind; id: number; field: string; value: string }>(
`/fields/${kind}/${id}/${encodeURIComponent(field)}`,
{ value }
),
deleteField: (kind: ReflectionKind, id: number, field: string) => MOCK
? Promise.resolve(mockReflectionDeleteField(kind, id, field))
: del<{ kind: ReflectionKind; id: number; field: string }>(
`/fields/${kind}/${id}/${encodeURIComponent(field)}`
),
getBundle: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve(mockReflectionBundle(kind, id))
: get<{
kind: string
id: number
typeName: string
fullTypeName?: string
assembly: string
summary: ReflectionObject
methods: ReflectionMethod[]
status: ReflectionKv[]
/** 扁平 key→value 兼容旧组件。 */
fields: Record<string, string>
/** 带 source/locked/typeName 的字段表,新版「对象管理」面板用。 */
fieldList?: ReflectionKv[]
}>(`/bundle/${kind}/${id}`),
execute: (kind: ReflectionKind, id: number, method: string, params?: Record<string, string | number | boolean>) => MOCK
? Promise.resolve(mockReflectionExecute(
kind,
id,
method,
Object.fromEntries(Object.entries(params ?? {}).map(([k, v]) => [k, String(v)]))
))
: post<{ returnValue: string }>(
`/execute/${kind}/${id}/${encodeURIComponent(method)}`,
params
),
/** 车辆前往指定站点(Web「去某地」;不依赖 SimpleUI.GetPoint)。 */
gotoCarSite: (carId: number, siteId: number) => MOCK
? Promise.resolve({ carId, siteId, message: `mock goto ${carId} -> ${siteId}` })
: post<{ carId: number; siteId: number; message: string }>(
`/car/${carId}/goto-site`,
{ siteId }
),
getScriptSource: (id: number): Promise<ScriptSourcePayload> => MOCK
? Promise.resolve({
id,
name: `MockScript#${id}`,
typeName: 'CarProgram',
state: 'Running',
script: '// mock script'
})
: get<ScriptSourcePayload>(`/scripts/${id}/source`),
getScriptExceptionStatus: (id: number): Promise<ScriptExceptionStatusPayload> => MOCK
? Promise.resolve({
id,
name: `MockScript#${id}`,
typeName: 'CarProgram',
car: 'AGV-1(#1)',
state: 'Running',
exception: '(无)',
notifies: '(无)',
report: '=== CarProgram 异常状态报告 ===\nstate : Running\n\n--- exception ---\n(无)'
})
: get<ScriptExceptionStatusPayload>(`/scripts/${id}/exception-status`),
// ──────────────────────────────────────────────────────────────────────────
// 地图监控配置(Configuration.conf.monitorVisibility
// /monitor-config GET 现有配置 + 各 kind 可勾选 fields/status/methods 全集
// /monitor-config POST body JSON 全量覆盖
// 空白名单 = 显示全部;非空 = 仅显示列表中的 key。
// ──────────────────────────────────────────────────────────────────────────
getMonitorConfig: (): Promise<MonitorConfigPayload> => MOCK
? Promise.resolve({
config: loadMockMonitorConfig(),
available: emptyMonitorAvailableMap()
})
: get<MonitorConfigPayload>('/monitor-config'),
saveMonitorConfig: async (cfg: MonitorConfigSaveBody): Promise<MonitorConfigSaveBody> => {
if (MOCK) {
saveMockMonitorConfig(cfg)
return cfg
}
const { data } = await http.post<ReflectionEnvelope<MonitorConfigSaveBody>>(
`${BASE}/monitor-config`,
cfg
)
if (!data?.success) throw new Error(data?.message ?? 'saveMonitorConfig failed')
return data.data as MonitorConfigSaveBody
},
// 选中同步:让 SimpleLite 3D 场景同步高亮被点击对象
getSelection: () => MOCK
? Promise.resolve<ReflectionSelection>({ kind: null, id: 0, name: '' })
: get<ReflectionSelection>('/selection'),
setSelection: (kind: ReflectionKind, id: number) => MOCK
? Promise.resolve({ kind, id, name: `mock(${kind}#${id})` })
: post<{ kind: ReflectionKind; id: number; name: string }>('/selection', { kind, id }),
clearSelection: () => MOCK
? Promise.resolve({ cleared: true })
: post<{ cleared: boolean }>('/selection/clear'),
// ──────────────────────────────────────────────────────────────────────────
// 项目属性(Scene.conf 单例)
// /project/fields GET → 列出 Scene.conf 全部 [FieldMember] 字段
// /project/fields/{key} POST → ?value=xxx 写入单字段
// /project/save POST → 把内存项目(含修改后的 conf)写回 JSON
// ──────────────────────────────────────────────────────────────────────────
getProjectFields: (): Promise<ProjectPropertiesPayload> => MOCK
? Promise.resolve({
target: 'Scene.conf',
lastLoadedPath: null,
autoloadPath: null,
fields: []
})
: get<ProjectPropertiesPayload>('/project/fields'),
setProjectField: (field: string, value: string) => MOCK
? Promise.resolve({ field, value })
: post<{ field: string; value: string }>(
`/project/fields/${encodeURIComponent(field)}`,
{ value }
),
/** 保存当前项目到磁盘。path 为空则后端用 LastLoadedPath / Configuration.conf.autoload。 */
saveProject: (path?: string) => MOCK
? Promise.resolve({ path: path ?? '(mock)' })
: post<{ path: string }>('/project/save', path ? { path } : undefined),
// ──────────────────────────────────────────────────────────────────────────
// 核心配置(simple.json / Configuration.conf
// /app-config/fields GET → 列出 Configuration.conf 全部 [FieldMember]
// /app-config/fields/{key} POST → ?value=xxx 写入单字段
// /app-config/save POST → 调 Configuration.ToFile("simple.json")
// ──────────────────────────────────────────────────────────────────────────
getAppConfigFields: (): Promise<AppConfigPayload> => MOCK
? Promise.resolve({ target: 'Configuration.conf', savePath: 'simple.json', fields: [] })
: get<AppConfigPayload>('/app-config/fields'),
setAppConfigField: (field: string, value: string) => MOCK
? Promise.resolve({ field, value })
: post<{ field: string; value: string }>(
`/app-config/fields/${encodeURIComponent(field)}`,
{ value }
),
saveAppConfig: () => MOCK
? Promise.resolve({ path: 'simple.json' })
: post<{ path: string }>('/app-config/save'),
// ──────────────────────────────────────────────────────────────────────────
// 车型样式(WorkspaceCarStylesByType / WorkspaceAlarmColorScheme
// /car-style/types GET 列出所有 Car 子类型样式
// /car-style/{typeFullName} GET 单个车型当前样式
// /car-style/{typeFullName} POST body JSON 全量覆盖
// /car-style/{typeFullName} DELETE 恢复为默认
// /car-style/alarm-colors GET 7 种报警键 → 颜色
// /car-style/alarm-colors POST body JSON 全量覆盖映射
// /car-style/save POST 写回 simple.json
// ──────────────────────────────────────────────────────────────────────────
getCarStyleTypes: () => MOCK
? Promise.resolve<CarStyleTypesPayload>({ globalDefault: defaultCarStyleDto(), types: [] })
: get<CarStyleTypesPayload>('/car-style/types'),
getCarStyle: (typeFullName: string) => MOCK
? Promise.resolve(defaultCarStyleDto())
: get<CarStyleDto>(`/car-style/${encodeURIComponent(typeFullName)}`),
/** 用 axios 直接以 JSON body 提交,避免拼 query。 */
putCarStyle: async (typeFullName: string, body: CarStyleDto) => {
if (MOCK) return body
const { data } = await http.post<ReflectionEnvelope<CarStyleDto>>(
`${BASE}/car-style/${encodeURIComponent(typeFullName)}`,
body
)
if (!data?.success) throw new Error(data?.message ?? 'putCarStyle failed')
return data.data as CarStyleDto
},
deleteCarStyle: (typeFullName: string) => MOCK
? Promise.resolve({ typeFullName, removed: true })
: del<{ typeFullName: string; removed: boolean }>(`/car-style/${encodeURIComponent(typeFullName)}`),
getAlarmColors: () => MOCK
? Promise.resolve<AlarmColorsPayload>({ defaultKeys: [], entries: [] })
: get<AlarmColorsPayload>('/car-style/alarm-colors'),
putAlarmColors: async (palette: Record<string, number>) => {
if (MOCK) return { count: Object.keys(palette).length }
const { data } = await http.post<ReflectionEnvelope<{ count: number }>>(
`${BASE}/car-style/alarm-colors`,
palette
)
if (!data?.success) throw new Error(data?.message ?? 'putAlarmColors failed')
return data.data as { count: number }
},
saveCarStyle: () => MOCK
? Promise.resolve({ path: 'simple.json' })
: post<{ path: string }>('/car-style/save')
}
export interface CarStyleDto {
bodyLengthM: number
bodyWidthM: number
bodyColorArgb: number
outlineColorArgb: number
labelColorArgb: number
showLabel: boolean
modelPath: string
}
export interface CarStyleTypeRow {
typeName: string
shortName: string
label: string
assemblyName: string
hasOverride: boolean
style: CarStyleDto
}
export interface CarStyleTypesPayload {
globalDefault: CarStyleDto
types: CarStyleTypeRow[]
}
export interface AlarmColorEntry {
key: string
colorArgb: number
isDefault: boolean
}
export interface AlarmColorsPayload {
defaultKeys: string[]
entries: AlarmColorEntry[]
}
function defaultCarStyleDto(): CarStyleDto {
return {
bodyLengthM: 0.64,
bodyWidthM: 0.42,
bodyColorArgb: 0xffffffff,
outlineColorArgb: 0xff2a2a2a,
labelColorArgb: 0xffffffff,
showLabel: true,
modelPath: ''
}
}
export interface ProjectPropertyRow {
key: string
label: string
value: string
typeName: string
locked: boolean
}
export interface ProjectPropertiesPayload {
target: string
lastLoadedPath: string | null
autoloadPath: string | null
fields: ProjectPropertyRow[]
}
export interface AppConfigPayload {
target: string
savePath: string
fields: ProjectPropertyRow[]
}