feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { RobotConfig } from '../mobile/RobotDescriptor';
|
||||
import { composeMobileScene, prepareBundle } from '../mobile/SceneComposer';
|
||||
import {
|
||||
composeTrainingMap,
|
||||
trainingTerrainFromCompiledScene,
|
||||
@@ -8,6 +10,7 @@ import type { PolicyDeployment, TrainingTerrain } from '../rl/deployment';
|
||||
import type { MainModule } from '@mujoco/mujoco';
|
||||
import type { ProjectFile, ProjectManifest } from '../project/types';
|
||||
import { prepareProjectForMujoco } from '../project/importer';
|
||||
import { prepareLargeMeshes } from '../project/meshCompatibility';
|
||||
import { prepareRobotProject } from '../project/robotProfiles';
|
||||
import { sha256 } from '../robot/registry';
|
||||
import type { ExternalControlStatus } from '../robot/RobotRuntime';
|
||||
@@ -56,6 +59,13 @@ export interface PhysicsLoadProgress {
|
||||
export interface PhysicsLoadOptions {
|
||||
/** Explicit built-in profile; never inferred from actuator count. */
|
||||
robotProfileId?: string;
|
||||
/** false: reuse model recipe without installing the external-control runtime. */
|
||||
configureRobotRuntime?: boolean;
|
||||
/** Optional caller-owned free VFS slot; bounds native asset-cache path identities. */
|
||||
workspaceSlot?: string;
|
||||
/** Trusted application hook, after URDF/map conversion and include expansion. */
|
||||
sceneComposer?: (mjcf: Uint8Array) => Uint8Array;
|
||||
mobileRobot?: RobotConfig;
|
||||
trainingDeployment?: PolicyDeployment;
|
||||
/** Candidate policy is initialized/validated before replacing the active session. */
|
||||
trainingPolicy?: { data: Uint8Array; path: string };
|
||||
@@ -158,6 +168,8 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
}
|
||||
>();
|
||||
private loadGeneration = 0;
|
||||
private readonly workspacePrefix = `adapter_${crypto.randomUUID().replaceAll('-', '_')}`;
|
||||
private readonly candidateRoots = new Set<string>();
|
||||
private disposed = false;
|
||||
private retiredSession: SimulationSession | null = null;
|
||||
private retiredWorkspace: MemfsWorkspace | null = null;
|
||||
@@ -169,6 +181,7 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
options: PhysicsLoadOptions = {},
|
||||
): Promise<SimulationSnapshot> {
|
||||
if (this.disposed) throw new Error('物理适配器已释放');
|
||||
this.session?.stopAgent?.('模型或地图正在重载');
|
||||
if (this.session?.describeRobot?.())
|
||||
this.session.stopExternalControl('模型正在重载,请重新授权');
|
||||
const generation = ++this.loadGeneration;
|
||||
@@ -190,19 +203,76 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
const module = await getMujocoModule();
|
||||
if (this.disposed || generation !== this.loadGeneration) throw new Error('模型加载已取消');
|
||||
report(0.32, '校验并准备模型资源');
|
||||
const workspace = new MemfsWorkspace(module, `${manifest.id}_stage_${generation}`);
|
||||
if (
|
||||
options.robotProfileId &&
|
||||
(urdfMode !== 'mjcf' || baseMode !== 'floating' || options.trainingDeployment)
|
||||
)
|
||||
throw new Error('机器人 profile 需要浮动基座 URDF 转换,不能与 Go2 部署混用');
|
||||
const mobileRobot = options.mobileRobot;
|
||||
if (
|
||||
mobileRobot &&
|
||||
(options.trainingDeployment || urdfMode !== 'mjcf' || baseMode !== 'floating')
|
||||
)
|
||||
throw new Error('移动操作需要浮动基座 MJCF,不能与 Go2 部署混用');
|
||||
const sourceManifest =
|
||||
mobileRobot?.recipe === 'lekiwi-bundle'
|
||||
? prepareBundle(manifest, entryPath, mobileRobot)
|
||||
: manifest;
|
||||
const profileManifest = options.robotProfileId
|
||||
? await prepareRobotProject(manifest, entryPath, options.robotProfileId)
|
||||
: manifest;
|
||||
const prepared = await prepareProjectForMujoco(profileManifest, entryPath);
|
||||
? await prepareRobotProject(sourceManifest, entryPath, options.robotProfileId)
|
||||
: sourceManifest;
|
||||
const normalized = await prepareProjectForMujoco(profileManifest, entryPath);
|
||||
const prepared = await prepareLargeMeshes(normalized.manifest, entryPath, {
|
||||
shouldCancel: () => this.disposed || generation !== this.loadGeneration,
|
||||
onMeshProgress: (path, done, total) =>
|
||||
report(
|
||||
0.34 + (0.1 * done) / total,
|
||||
`兼容高面数网格 ${path}(${done}/${total} 面,保留全部三角形)`,
|
||||
),
|
||||
});
|
||||
prepared.warnings.unshift(...normalized.warnings);
|
||||
// Map files may not be in the robot's include graph. Prepare the selected
|
||||
// physics assets before MapComposer rebases them into the resulting scene.
|
||||
const mapPaths = new Set<string>();
|
||||
for (const selection of placedMapAssets?.map((asset) => asset.selection) ?? [mapSelection]) {
|
||||
if (selection.kind === 'project') {
|
||||
const map = resolveProjectMap(prepared.manifest, selection.descriptorPath);
|
||||
if (map.physicsPath) mapPaths.add(map.physicsPath);
|
||||
}
|
||||
}
|
||||
for (const path of mapPaths) {
|
||||
const next = await prepareLargeMeshes(prepared.manifest, path, {
|
||||
shouldCancel: () => this.disposed || generation !== this.loadGeneration,
|
||||
});
|
||||
prepared.manifest = next.manifest;
|
||||
prepared.warnings.push(...next.warnings);
|
||||
}
|
||||
if (this.disposed || generation !== this.loadGeneration) throw new Error('模型加载已取消');
|
||||
const supportFiles = prepared.manifest.files.filter(
|
||||
(file) => !manifest.files.some((original) => original.path === file.path),
|
||||
);
|
||||
// Bounded path identities stop the native mesh cache accumulating a new
|
||||
// copy on every reload. Three slots permit active + rollback + candidate;
|
||||
// callers with explicit two-slot viewer transactions retain their contract.
|
||||
const freeSlot = [0, 1, 2]
|
||||
.map((i) => `${this.workspacePrefix}_${i}`)
|
||||
.find((slot) => {
|
||||
const root = `/workspace/${slot}`;
|
||||
return (
|
||||
root !== this.workspace?.root &&
|
||||
root !== this.retiredWorkspace?.root &&
|
||||
!this.candidateRoots.has(root)
|
||||
);
|
||||
});
|
||||
if (!options.workspaceSlot && !freeSlot) throw new Error('候选 VFS 槽已满,请等待当前加载结束');
|
||||
const workspace = new MemfsWorkspace(module, options.workspaceSlot ?? freeSlot!);
|
||||
if (
|
||||
workspace.root === this.workspace?.root ||
|
||||
workspace.root === this.retiredWorkspace?.root ||
|
||||
this.candidateRoots.has(workspace.root)
|
||||
)
|
||||
throw new Error('候选 VFS 槽仍被活动/待释放模型占用');
|
||||
this.candidateRoots.add(workspace.root);
|
||||
let nextSession: SimulationSession | null = null;
|
||||
try {
|
||||
report(0.46, '写入浏览器内存文件系统');
|
||||
@@ -237,8 +307,33 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
const bodyId = Number(intermediate.model.jnt_bodyid[id]);
|
||||
const body = intermediate.model.body(bodyId);
|
||||
try {
|
||||
let wheelCenter: number[] | undefined;
|
||||
if (joint.name.startsWith('base_') && joint.name.endsWith('_wheel')) {
|
||||
for (let geomId = 0; geomId < intermediate.model.ngeom; geomId++) {
|
||||
const geom = intermediate.model.geom(geomId);
|
||||
try {
|
||||
if (
|
||||
!geom.name.startsWith('4-Omni-Directional-Wheel_Single_Body') ||
|
||||
Number(intermediate.model.geom_group[geomId]) !== 1
|
||||
)
|
||||
continue;
|
||||
let ancestor = Number(intermediate.model.geom_bodyid[geomId]);
|
||||
while (ancestor && ancestor !== bodyId)
|
||||
ancestor = Number(intermediate.model.body_parentid[ancestor]);
|
||||
if (ancestor === bodyId) {
|
||||
wheelCenter = Array.from(
|
||||
intermediate.data.geom_xpos.slice(geomId * 3, geomId * 3 + 3),
|
||||
);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
geom.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
geometry.push({
|
||||
name: joint.name,
|
||||
wheelCenter,
|
||||
body: body.name,
|
||||
position: Array.from(
|
||||
intermediate.data.xpos.slice(bodyId * 3, bodyId * 3 + 3),
|
||||
@@ -254,9 +349,12 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
body.delete();
|
||||
}
|
||||
}
|
||||
convertedData = enhanceLeKiwiMjcf(convertedData, geometry);
|
||||
const inputHash = await sha256(
|
||||
manifest.files.find((file) => file.path === entryPath)!.data,
|
||||
);
|
||||
convertedData = enhanceLeKiwiMjcf(convertedData, geometry, inputHash);
|
||||
warnings.unshift(
|
||||
'LeKiwi v1:仿真估计参数,简化被动滚子/碰撞与九路伺服;超大轮 STL 已替换为简化视觉,不代表实机标定',
|
||||
'LeKiwi v1:完整 CAD 轮视觉;碰撞仍使用被动滚子/整臂凸分解与九路伺服,轮距/动力学为仿真估计,不代表实机标定',
|
||||
);
|
||||
}
|
||||
workspace.writeGenerated(convertedPath, convertedData);
|
||||
@@ -342,10 +440,36 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
}
|
||||
warnings.push('已用策略配套训练布局替换场景地形;编辑器地图未更改。Go2-W动力学不等同Go2。');
|
||||
}
|
||||
const sceneComposer = mobileRobot
|
||||
? (xml: Uint8Array) => {
|
||||
const mobileScene = composeMobileScene(xml, mobileRobot);
|
||||
return options.sceneComposer ? options.sceneComposer(mobileScene) : mobileScene;
|
||||
}
|
||||
: options.sceneComposer;
|
||||
if (sceneComposer) {
|
||||
if (entry?.format === 'urdf' && urdfMode === 'native')
|
||||
throw new Error('任务场景组合需要 MJCF 转换模式');
|
||||
const intermediate = new SimulationSession(module, modelPath);
|
||||
try {
|
||||
const scenePath = `${modelRelativePath}.task.xml`;
|
||||
// Register before native save so failed composition is cleaned up too.
|
||||
workspace.writeGenerated(scenePath, new Uint8Array());
|
||||
if (!module.mj_saveLastXML(workspace.path(scenePath), intermediate.model))
|
||||
throw new Error('无法展开任务场景');
|
||||
workspace.writeGenerated(
|
||||
scenePath,
|
||||
sceneComposer(new TextEncoder().encode(workspace.readText(scenePath))),
|
||||
);
|
||||
modelRelativePath = scenePath;
|
||||
modelPath = workspace.path(scenePath);
|
||||
} finally {
|
||||
intermediate.dispose();
|
||||
}
|
||||
}
|
||||
report(0.84, '编译模型与物理数据');
|
||||
console.info('[MuJoCo] 编译模型', modelPath);
|
||||
nextSession = new SimulationSession(module, modelPath, warnings);
|
||||
if (options.robotProfileId) {
|
||||
if (options.robotProfileId && options.configureRobotRuntime !== false && !mobileRobot) {
|
||||
const fingerprint = await sha256(
|
||||
new TextEncoder().encode(workspace.readText(modelRelativePath)),
|
||||
);
|
||||
@@ -411,6 +535,7 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mobileRobot) nextSession.configureMobile(mobileRobot);
|
||||
if (options.trainingPolicy) {
|
||||
if (!options.trainingDeployment?.terrain) throw new Error('事务策略加载需要配套训练地图');
|
||||
report(0.9, '校验候选场景的 ONNX 策略');
|
||||
@@ -445,6 +570,10 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
|
||||
`模型编译失败(${entryPath}):${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
} finally {
|
||||
this.candidateRoots.delete(workspace.root);
|
||||
this.session?.mobile?.env.refreshViews();
|
||||
this.retiredSession?.mobile?.env.refreshViews();
|
||||
}
|
||||
}
|
||||
exportTrainingTerrain(
|
||||
|
||||
Reference in New Issue
Block a user