From 993784c1f6c28c5a13b22d9777a80823c255aabc Mon Sep 17 00:00:00 2001 From: luoyifan Date: Mon, 30 Jun 2025 00:14:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BF=E7=9C=9F=E7=8E=AF=E5=A2=83=E5=90=AF?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/yvTable/YvTable.vue | 4 +- src/core/engine/Viewport.ts | 5 +- src/core/manager/EntityManager.ts | 62 +++++++++- src/core/manager/EnvManager.ts | 233 +++++++++++++++++++++++++++++++++++++ src/core/manager/RunManager.ts | 67 ----------- src/core/manager/RuntimeManager.ts | 14 ++- src/core/manager/WorldModel.ts | 2 + src/core/script/ModelManager.ts | 68 ++++++----- src/editor/ModelMain.vue | 34 ++++-- src/editor/OpenProject.vue | 151 ++++++++++++------------ src/editor/menus/FileMenu.ts | 4 +- src/modules/cl2/Cl23dObject.ts | 52 +++++---- src/types/LCC.d.ts | 11 -- src/types/Model.d.ts | 11 +- 14 files changed, 490 insertions(+), 228 deletions(-) create mode 100644 src/core/manager/EnvManager.ts delete mode 100644 src/core/manager/RunManager.ts diff --git a/src/components/yvTable/YvTable.vue b/src/components/yvTable/YvTable.vue index 9078346..59e3e36 100644 --- a/src/components/yvTable/YvTable.vue +++ b/src/components/yvTable/YvTable.vue @@ -287,9 +287,7 @@ export default { }) _.forEach(insertRows, (row, index) => { - if (!row._rid) { - row._rid = _.uniqueId('_') - } + row._rid = _.uniqueId('_') }) sel.add = _.cloneDeep(insertRows) if (sel.add.length > 0 || sel.update.length > 0 || sel.remove.length > 0) { diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts index faae13a..a80bf90 100644 --- a/src/core/engine/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -26,6 +26,7 @@ import ItemFindManager from '@/core/manager/ItemFindManager.ts' import { MapControls } from 'three/examples/jsm/controls/MapControls' import ModelManager from '@/core/script/ModelManager.ts' import RuntimeManager from '@/core/manager/RuntimeManager.ts' +import EnvManager from '@/core/manager/EnvManager.ts' /** * 视窗对象 @@ -50,6 +51,7 @@ export default class Viewport { interactionManager = new InteractionManager() modelManager = new ModelManager() runtimeManager = new RuntimeManager() + envManager = new EnvManager() // 状态管理器 stateManager: StateManager @@ -63,7 +65,8 @@ export default class Viewport { markRaw(this.itemFindManager), markRaw(this.interactionManager), markRaw(this.modelManager), - markRaw(this.runtimeManager) + markRaw(this.runtimeManager), + markRaw(this.envManager) ] // 对象实例管理器 moduleName -> InstanceMeshManager diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts index a2c5383..241e67b 100644 --- a/src/core/manager/EntityManager.ts +++ b/src/core/manager/EntityManager.ts @@ -101,6 +101,54 @@ export default class EntityManager { this.lineDiffs.delete.clear() } + deleteEntityOnlyRuntime(id: string) { + const entity = this.___entityMap.get(id) + if (entity) { + this.viewport.itemFindManager.remove(id) + const renderer = getRenderer(entity.t) + if (renderer) { + renderer.tempViewport = this.viewport + renderer.deletePoint(id, { isRuntime: true }) + this.___entityMap.delete(id) + renderer.tempViewport = null + } + } + } + + /** + * 在运行时创建或更新一个实体, 他不会参与 StateManager 运算,也不会参与 Link 运算 + */ + createOrUpdateEntityOnlyRuntime(entity: ItemJson) { + if (!entity?.id) { + throw new Error('Entity must have an id') + } + const originEntity = this.___entityMap.get(entity.id) + const renderer = getRenderer(entity.t) + if (!renderer) { + throw new Error(`Renderer for type ${entity.t} not found`) + } + + this.viewport.itemFindManager.addOrUpdate(entity) + this.___entityMap.set(entity.id, entity) + + const option = { + isRuntime: true, + originEntity: _.cloneDeep(originEntity) + + } as RendererCudOption + + renderer.tempViewport = this.viewport + + if (typeof originEntity === 'undefined') { + renderer.createPointForEntity(entity, option) + + } else { + renderer.updatePointForEntity(entity, option) + } + + renderer.tempViewport = null + } + /** * 创建或更新一个实体, 这个点的 center[] / in[] / out[] 关联的点, 可能都要对应进行关联 */ @@ -509,18 +557,20 @@ export default class EntityManager { return } let item: ItemJson | undefined = undefined - this.___entityMap.forEach((value) => { - if (value.logicX === logicX && value.logicY === logicY) { - item = value - return - } - }) + this.___entityMap.forEach((value) => { + //@ts-ignore + if (value.logicX === logicX && value.logicY === logicY) { + item = value + return + } + }) return item } findItemByLogicXYZ(x: number, y: number, z: number): ItemJson | undefined { let item: ItemJson | undefined = undefined this.___entityMap.forEach((value) => { + //@ts-ignore if (value.tf[0][0] === x && /*value.tf[0][1] === y &&*/ value.tf[0][2] === z && value.logicX) { item = value return diff --git a/src/core/manager/EnvManager.ts b/src/core/manager/EnvManager.ts new file mode 100644 index 0000000..4034393 --- /dev/null +++ b/src/core/manager/EnvManager.ts @@ -0,0 +1,233 @@ +import type Viewport from '@/core/engine/Viewport.ts' +import { worldModel } from '@/core/manager/WorldModel.ts' +import mqtt, { type IConnackPacket, type IPublishPacket } from 'mqtt' +import type { Cl2Task } from '@/modules/cl2/Cl23dObject.ts' +import type { ErrorWithReasonCode } from 'mqtt/src/lib/shared.ts' +import type Cl23dObject from '@/modules/cl2/Cl23dObject.ts' +import { Request } from '@ease-forge/shared' + +export default class EnvManager { + private viewport: Viewport + private client: mqtt.MqttClient = null + + init(viewport: Viewport): void { + this.viewport = viewport + } + + // 从后台读取所有车 + async loadExecutors() { + const res = await Request.request.post('/api/workbench/EnvController@getAllExecutor', { + projectUuid: worldModel.state.project_uuid, + catalogCode: worldModel.state.catalogCode, + envId: worldModel.state.runState.currentEnvId + }) + for (const row of res.data) { + const executor_id = row.executor_id + const payload = JSON.parse(row.virtual_executor_payload) + const wayPointId = row.virtual_location_at + + const point = Model.find(wayPointId) + if (!point) { + console.error(`Waypoint with ID ${wayPointId} not found for executor ${executor_id}.`) + continue + } + + const item = _.cloneDeep(payload) + item.id = executor_id + item.tf[0] = _.cloneDeep(point.tf[0]) + Model.createExecutor(item) + } + } + + // 从后台读取所有库存 + async loadInv() { + const res = await Request.request.post('/api/workbench/EnvController@getAllInv', { + projectUuid: worldModel.state.project_uuid, + catalogCode: worldModel.state.catalogCode, + envId: worldModel.state.runState.currentEnvId + }) + for (const row of res.data) { + const bay = row.bay + const cell = row.cell // : 0 + const level = row.level // : 0 + const loc_code = row.loc_code // : "rack1_0_0_0" + const lpn = row.lpn // : "LPN1" + const rack = row.rack // : "rack1" + const container_type = row.container_type // : "pallet" + Model.createInv(container_type, lpn, rack, bay, level, cell) + } + } + + appendExecutor(id) { + const obj = this.viewport.entityManager.findObjectById(id) + const item = this.viewport.entityManager.findItemById(id) + if (item.t == 'cl2' && obj.userData.t === 'cl2') { + debugger + + const cl2 = obj as Cl23dObject + cl2.onMqttConnect(item, this.client) + // this.client.subscribe(['/wcs_server/' + cl2.id], { qos: 0 }) + // this.client.publish('/agv_robot/status', JSON.stringify(m20020), { retain: true }) + } + } + + onMqttConnect = (packet: IConnackPacket) => { + console.log('Connected') + } + + onMqttMessage = (topic: string, payload: Buffer, packet: IPublishPacket) => { + console.log(`[${topic}] ${msg}`) + debugger + const a: Cl2Task = JSON.parse(msg.toString()) + this.handleMessage(a) + } + + onMqttError = (error: Error | ErrorWithReasonCode) => { + console.error('Error:', error) + } + + async start(env: EnvInfo) { + if (!env) { + system.showErrorDialog('Environment is not specified, cannot start EnvManager.') + return + } + if (!worldModel.state.isOpened) { + system.showErrorDialog('WorldModel is not opened, cannot start EnvManager.') + return + } + if (!this.viewport) { + system.showErrorDialog('Viewport is not initialized, cannot start EnvManager.') + return + } + if (!worldModel.state.runState.currentEnvId) { + system.showErrorDialog('Current environment ID is not set, cannot start EnvManager.') + return + } + if (worldModel.state.runState.isRunning) { + system.showErrorDialog('EnvManager is already running, cannot start again.') + return + } + const payload = env.env_payload + const brokerUrl = payload?.mqtt?.websocket + const username = payload?.mqtt?.username + const password = payload?.mqtt?.password + + if (!brokerUrl || !username || !password) { + system.showErrorDialog('MQTT broker URL, username, or password is not set in the environment payload.') + return + } + + this.stop() + + system.showLoading() + worldModel.state.runState.isLoading = true + worldModel.state.runState.currentEnv = Object.freeze(env) + try { + await this.loadExecutors() + await this.loadInv() + + this.client = mqtt.connect(brokerUrl, { + path: '/mqtt', + clientId: system.createUUID(), + clean: true, + connectTimeout: 300, + username, + password, + unixSocket: true, + keepalive: 60 + }) + + this.client.on('connect', this.onMqttConnect) + this.client.on('message', this.onMqttMessage) + this.client.on('error', this.onMqttError) + worldModel.state.runState.isRunning = true + + } finally { + system.clearLoading() + worldModel.state.runState.isLoading = false + } + } + + async stop() { + system.showLoading() + try { + if (worldModel.state.runState.isRunning) { + worldModel.state.runState.isRunning = false + } + worldModel.state.runState.currentEnv = null + if (this.client) { + this.client.removeAllListeners() + this.client.end() + this.client = null + } + + this.clearExecutors() + this.clearInv() + this.viewport.runtimeManager.clear() + + } finally { + system.clearLoading() + worldModel.state.runState.isLoading = false + } + } + + clearInv() { + + } + + clearExecutors() { + + } + + static CURRENT_ALL_ENV: EnvInfo[] + + /** + * 创建运行环境 + * @param worldId 世界ID + * @param envName 运行环境名称 + * @param isVirtual 是否是虚拟环境 + */ + static async createEnv(worldId: string, envName: string, isVirtual: boolean) { + throw new Error('Method not implemented.') + } + + /** + * 获取所有运行环境 + */ + static async getAllEnv(worldId: string): Promise> { + // system.invokeServer('') + if (!worldId) { + return Promise.resolve({ success: true, data: [], msg: '' }) + } + const res = await Request.request.post('/api/workbench/EnvController@getAllEnv', { + worldId: worldId + }) + if (res.success) { + EnvManager.CURRENT_ALL_ENV = res.data + for (const env of res.data) { + // payload 转换为 json 数据 + if (env.env_payload) { + try { + env.env_payload = JSON.parse(env.env_payload) + } catch (e) { + console.error('解析环境负载失败:', e) + env.env_payload = {} + } + } + } + + return res + } + + EnvManager.CURRENT_ALL_ENV = [] + return res + } + + /** + * 卸载资源 + */ + dispose(): void { + this.viewport = null + this.stop() + } +} diff --git a/src/core/manager/RunManager.ts b/src/core/manager/RunManager.ts deleted file mode 100644 index 72e167c..0000000 --- a/src/core/manager/RunManager.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Request } from '@ease-forge/shared' - -/** - * 运行管理器 - */ -class RunManager { - - // 是否正在运行 - isRunning: boolean = false - - /** - * 获取所有运行环境 - */ - async getAllEnv(worldId: string): Promise> { - // system.invokeServer('') - if (!worldId) { - return Promise.resolve({ success: true, data: [], msg: '' }) - } - const res = await Request.request.post('/api/workbench/EnvController@getAllEnv', { - worldId: worldId - }) - for (const env of res.data) { - // payload 转换为 json 数据 - if (env.env_payload) { - try { - env.env_payload = JSON.parse(env.env_payload) - } catch (e) { - console.error('解析环境负载失败:', e) - env.env_payload = {} - } - } - } - - return res - } - - /** - * 创建运行环境 - * @param worldId 世界ID - * @param envName 运行环境名称 - * @param isVirtual 是否是虚拟环境 - */ - async createEnv(worldId: string, envName: string, isVirtual: boolean) { - throw new Error('Method not implemented.') - } - - /** - * 开始运行 - */ - async run(timeRate: number, envId: number) { - if (this.isRunning) { - await this.stop() - } - - // 启动 websocket 监听等等 - } - - /** - * 停止运行 - */ - async stop() { - // 停止 websocket 监听等等 - } -} - -const runManager = new RunManager() -export default runManager diff --git a/src/core/manager/RuntimeManager.ts b/src/core/manager/RuntimeManager.ts index 468200e..55bf829 100644 --- a/src/core/manager/RuntimeManager.ts +++ b/src/core/manager/RuntimeManager.ts @@ -5,6 +5,8 @@ import type Viewport from '@/core/engine/Viewport.ts' */ export default class RuntimeManager { private viewport: Viewport + private readonly tmpExecutors = new Set + // 货架ID -> 托盘ID private readonly storeRackMap = new Map>() init(viewport: Viewport): void { @@ -58,11 +60,21 @@ export default class RuntimeManager { * 清空所有存储项 */ clear() { + for (const id of this.tmpExecutors) { + this.viewport.entityManager.deleteEntityOnlyRuntime(id) + } + + this.tmpExecutors.clear() this.storeRackMap.clear() } dispose(): void { this.viewport = null - this.storeRackMap.clear() + this.clear() + } + + addEntity(item: ItemJson) { + this.tmpExecutors.add(item.id) + this.viewport.entityManager.createOrUpdateEntityOnlyRuntime(item) } } diff --git a/src/core/manager/WorldModel.ts b/src/core/manager/WorldModel.ts index 07bf596..0eab99b 100644 --- a/src/core/manager/WorldModel.ts +++ b/src/core/manager/WorldModel.ts @@ -26,6 +26,7 @@ export interface WorldModelState { isRunning: boolean, isVirtual: boolean, timeRate: number, + currentEnv: EnvInfo } } @@ -218,6 +219,7 @@ export default class WorldModel { const items = _.cloneDeep(floor.items) delete floor.items console.log("floor", floor); + //@ts-ignore const vdata: VData = { items: items as ItemJson[], infos: floor, diff --git a/src/core/script/ModelManager.ts b/src/core/script/ModelManager.ts index 1cf031a..9a7c3c9 100644 --- a/src/core/script/ModelManager.ts +++ b/src/core/script/ModelManager.ts @@ -94,38 +94,54 @@ export default class ModelManager implements IControls, Model { }) } - createInv(boxType: ContainerT, lpn: string, rack: string, bay?: number, level?: number, cell?: number): void { + createExecutor(item: ItemJson): void { + this.viewport.runtimeManager.addEntity(item) + } + createInv(boxType: ContainerT, lpn: string, rack: string, bay?: number, level?: number, cell?: number): void { const scale = getRenderer(boxType).defaultScale + this.viewport.runtimeManager.addEntity({ + id: lpn, + t: boxType as string, + tf: [[-5000, -1, 0], [0, 0, 0], [scale.x, scale.y, scale.z]], + v: true, + dt: { + in: [], out: [], center: [], + storeAt: { item: rack, bay, level, cell } + } + }) - this.viewport.stateManager.update(({ getEntity, putEntity, addEntity }) => { - debugger - const item = getEntity(lpn) - if (item) { - _.extend(item, { - t: boxType as string, - tf: [[-5000, -1, 0], [0, 0, 0], [scale.x, scale.y, scale.z]], - v: true, - dt: { - in: [], out: [], center: [], - storeAt: { item: rack, bay, level, cell } - } - }) - putEntity(item) - } else { - addEntity({ - id: lpn, - t: boxType as string, - tf: [[-5000, -1, 0], [0, 0, 0], [scale.x, scale.y, scale.z]], - v: true, - dt: { - in: [], out: [], center: [], - storeAt: { item: rack, bay, level, cell } - } - }) + // 这一段代码不要删除,他是用向正式环境提交数据用的 + /* +this.viewport.stateManager.update(({ getEntity, putEntity, addEntity }) => { + debugger + const item = getEntity(lpn) + if (item) { + _.extend(item, { + t: boxType as string, + tf: [[-5000, -1, 0], [0, 0, 0], [scale.x, scale.y, scale.z]], + v: true, + dt: { + in: [], out: [], center: [], + storeAt: { item: rack, bay, level, cell } + } + }) + putEntity(item) + } else { + addEntity({ + id: lpn, + t: boxType as string, + tf: [[-5000, -1, 0], [0, 0, 0], [scale.x, scale.y, scale.z]], + v: true, + dt: { + in: [], out: [], center: [], + storeAt: { item: rack, bay, level, cell } } }) } +}) + */ + } getPositionByLogicXY(logicX: number, logicY: number): THREE.Vector3 { const item = this.viewport.entityManager.findItemByLogicXY(logicX, logicY) diff --git a/src/editor/ModelMain.vue b/src/editor/ModelMain.vue index 6975e40..1514f02 100644 --- a/src/editor/ModelMain.vue +++ b/src/editor/ModelMain.vue @@ -31,11 +31,13 @@ 启动 + :loading="worldModelState.runState.isLoading" + @click="startEnv">启动仿真 停止 + :loading="worldModelState.runState.isLoading" + @click="stopEnv">停止仿真 @@ -150,9 +152,8 @@ import CatalogDefine from './CatalogDefine.vue' import Logo from '@/assets/images/logo.png' import './ModelMain.less' import EventBus from '@/runtime/EventBus.js' -import { useRouter } from 'vue-router' import { worldModel } from '@/core/manager/WorldModel.ts' -import runManager from '@/core/manager/RunManager.js' +import EnvManager from '@/core/manager/EnvManager.js' export default { components: { Model2DEditor, Model3DViewer, Split, SplitArea, CatalogDefine }, @@ -269,15 +270,16 @@ export default { }, watch: { 'worldModelState.isOpened': { - handler(newVal) { - if (newVal.isOpened) { + handler() { + if (this.worldModelState.isOpened) { this.reloadEnvList() } } }, 'worldModelState.project_uuid': { - handler(newVal) { - if (newVal.isOpened) { + immediate: true, + handler() { + if (this.worldModelState.isOpened) { this.reloadEnvList() } } @@ -299,10 +301,20 @@ export default { methods: { renderIcon, getWidgetBySide, + startEnv() { + const env = this.envList.find(env => env.env_id === this.worldModelState.runState.currentEnvId) + if (env) { + this.currentViewport.envManager.start(env) + } + }, + stopEnv() { + this.currentViewport.envManager.stop() + }, reloadEnvList() { - runManager.getAllEnv(this.worldModelState.project_uuid).then(res => { - this.envList = res.data - }) + EnvManager.getAllEnv(this.worldModelState.project_uuid) + .then(res => { + this.envList = res.data + }) }, toHome() { system.router.push({ name: 'home' }) diff --git a/src/editor/OpenProject.vue b/src/editor/OpenProject.vue index 6bf0dc2..b2cde84 100644 --- a/src/editor/OpenProject.vue +++ b/src/editor/OpenProject.vue @@ -1,128 +1,125 @@ \ No newline at end of file + diff --git a/src/editor/menus/FileMenu.ts b/src/editor/menus/FileMenu.ts index cad6a1a..15d3369 100644 --- a/src/editor/menus/FileMenu.ts +++ b/src/editor/menus/FileMenu.ts @@ -109,7 +109,7 @@ export default defineMenu((menus) => { } }), { title: '打开项目', - width: 1500, + width: 600, height: 500, showClose: true, showMax: true, @@ -142,6 +142,8 @@ export default defineMenu((menus) => { envId: 1, otherData: JSON.stringify(worldModel.state.worldData) }) + system.msg('保存成功', 'success') + } finally { system.clearLoading() } diff --git a/src/modules/cl2/Cl23dObject.ts b/src/modules/cl2/Cl23dObject.ts index f959178..cc3d877 100644 --- a/src/modules/cl2/Cl23dObject.ts +++ b/src/modules/cl2/Cl23dObject.ts @@ -409,28 +409,10 @@ export default class Cl23dObject extends THREE.Object3D { on() { } } + debugger - const m20020 = { - 'content': { - 'CreateMonoTime': 233701185, - 'CreateTime': 1750638957541, - 'CurDirection': 0, - 'CurLogicX': 6, - 'CurLogicY': 2, - 'CurOrientation': -3.1375624383367926, - 'CurX': 6, - 'CurY': 2, - 'MarkerType': 1, - 'SendTime': 1750638957541, - 'SeqNo': 11, - 'VehicleId': 3, - 'X': 2652.477598132277, - 'Y': 3944.4427159671854 - }, - 'id': 20020 - } - -// 事件绑定 + /* + // 事件绑定 client.on('connect', () => { console.log('Connected') client.subscribe(['/wcs_server/' + item.id], { qos: 0 }) @@ -447,6 +429,7 @@ export default class Cl23dObject extends THREE.Object3D { client.on('error', (error) => { console.error('Error:', error) }) + */ } catch (e) { console.error(e) } @@ -455,8 +438,33 @@ export default class Cl23dObject extends THREE.Object3D { /*==========消息处理============*/ + onMqttConnect(item: ItemJson, client: mqtt.MqttClient) { + const m20020 = { + 'content': { + 'CreateMonoTime': 233701185, + 'CreateTime': 1750638957541, + 'CurDirection': 0, + 'CurLogicX': 6, + 'CurLogicY': 2, + 'CurOrientation': -3.1375624383367926, + 'CurX': 6, + 'CurY': 2, + 'MarkerType': 1, + 'SendTime': 1750638957541, + 'SeqNo': 11, + 'VehicleId': 3, + 'X': 2652.477598132277, + 'Y': 3944.4427159671854 + }, + 'id': 20020 + } + + client.subscribe(['/wcs_server/' + item.id], { qos: 0 }) + client.publish('/agv_robot/status', JSON.stringify(m20020), { retain: true }) + } + handleMessage(data: Cl2Task) { - return; + return if (data.id === 10010) { if (this.taskList.length <= 0) { diff --git a/src/types/LCC.d.ts b/src/types/LCC.d.ts index 22f3c3e..4b7b59f 100644 --- a/src/types/LCC.d.ts +++ b/src/types/LCC.d.ts @@ -11,17 +11,6 @@ declare interface LCC { loadFloor(projectUUID: string, catalogCode: string, envId: string): Promise> /** - * 在指定位置创建库存物品 - * @param boxType 容器类型 - * @param lpn 货品标签 - * @param rack 货架ID - * @param bay 可选参数, 货架列 - * @param level 可选参数, 货架层 - * @param cell 可选参数, 货架格 - */ - createInv(boxType: ContainerT, lpn: string, rack: string, bay: number = 0, level: number = 0, cell: number = 0): void - - /** * 移动库存物品到指定位置 * @param lpn 货品标签 * @param rack 货架ID diff --git a/src/types/Model.d.ts b/src/types/Model.d.ts index 136793e..c3cab85 100644 --- a/src/types/Model.d.ts +++ b/src/types/Model.d.ts @@ -30,7 +30,7 @@ declare interface Model { /** - * 获取当前楼层的其他数据 + * 在指定位置创建一个流动的库存物品 * @param boxType 容器类型 * @param lpn 货品标签 * @param rack 货架ID @@ -40,6 +40,13 @@ declare interface Model { */ createInv(boxType: ContainerT, lpn: string, rack: string, bay: number = 0, level: number = 0, cell: number = 0): void + + /** + * 在指定位置创建一个流动的库存物品 + * @param item 物品Json + */ + createExecutor(item: ItemJson): void + /** * 物品删除 */ @@ -205,7 +212,7 @@ declare function msg(str: string, type: MSG_TYPE = 1): void * 环境信息 */ interface EnvInfo { - env_id: string + env_id: number world_id: string env_name: string is_virtual: boolean