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

集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
2026-09-24 09:57:41 +08:00
parent 3ad29356c9
commit f3a8a38acd
194 changed files with 32918 additions and 236 deletions
+136 -7
View File
@@ -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(